为Microsoft Todo写一个Markdown支持油猴插件

169 阅读6分钟
title: 为Microsoft Todo开发一个Markdown支持油猴插件
date: 2024-08-30
tags:
  - post

引言

我是 Microsoft ToDo 的重度依赖用户, 基本上是把它当作一个笔记应用来使用的,收集收集面试题啊,或者零碎的知识点之类的。 不过它有一点很遗憾,那就是不支持 Markdown, 早都有人反应过这个问题,也不知道为什么直径不支持。 我觉得很大原因还是产品的定位不同吧。 Todo 应用本身就不需要过于繁杂的文本描述。

为什么不适用 Notion 呢 ? Notion 确实很强大,功能也非常丰富。 不过也正是它太过于强大,我觉得有点杀鸡用牛刀的感觉。 相比之下,我更喜欢轻量,功能专一,有 Microsoft 背景支持下的 Todo。

考虑到我们需要在不修改Microsoft ToDo原始代码的情况下注入新功能,油猴插件成为了一个理想的选择。油猴插件允许我们将自定义JavaScript代码注入到网页中,这为我们提供了极大的灵活性。此外不需要像 Google Extension 一样需要严格的发布审核流程。

那么就来看看怎么实现这个小插件吧。

meow_bongo.png

Setup

本次开发我们使用 Vite + vite-plugin-monkey 插件来开发,感谢插件作者

pnpm create monkey

基本配置:

//...
export default defineConfig({
  plugins: [
    monkey({
      entry: 'src/main.ts',
      userscript: {
        name: 'Microsoft To-Do Markdown Preview Support - mstodo-md-preview',
        author: 'Zhongyi Sun',
        namespace:'https://github.com/joisun',
        description: 'Microsoft To-Do Markdown Preview Support',
        icon: 'https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://to-do.live.com/&size=64',
        match: ['https://to-do.live.com/*'],
        license: 'MIT',
        version: pack.version,
      },
//...        

基本思路

我们要做的事情似乎很简单,也很单一, 那就是:

  1. 不影响现有文本的情况下把内容预览为 Markdown
  2. 支持动态的解析用户输入
  3. 受限于页面元素大小, 我们需要支持用户自定义的切换展示模式
  4. 在切换其他 todo item 的时候,我们需要更新预览内容。

对于Markdown解析,我们选择了广受欢迎的markdown-it库。这个库不仅性能优秀,还提供了丰富的扩展选项,使我们未来能够根据需求自由的自定义Markdown的渲染行为。

为了让代码块更加美观,我们还需要代码块的语法高亮, 对此,我们选用 highlight.js 来实现。

深入代码实现

让我们从代码层面深入了解这个插件是如何工作的。

首先,我们需要设置基础结构:

import { behindDebounce } from "./debounce";
import { waitForElement } from "./getElement";
import markdownit from "markdown-it";
import hljs from "highlight.js";
import "highlight.js/styles/tokyo-night-dark.min.css";
import "./style.css";

这些导入语句不仅引入了核心的markdown-it和highlight.js库,还包括了一些自定义的工具函数。behindDebounce函数用于性能优化,而waitForElement函数则是为了确保我们的代码在正确的DOM元素出现后才执行。

waitForElement函数的实现,我们使用 MutationObserver 来实现,返回一个Promise:

export function waitForElement(selector: string) {
  return new Promise((resolve) => {
    if (document.querySelector(selector)) {
      return resolve(document.querySelector(selector));
    }
​
    const observer = new MutationObserver(() => {
      if (document.querySelector(selector)) {
        resolve(document.querySelector(selector));
        observer.disconnect();
      }
    });
​
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  });
}

这个函数利用了Promise和MutationObserver API。它会持续监视DOM的变化,直到指定的元素出现。

为什么不直接在 document.onload 钩子中去获取元素呢?

答案是,获取不到。像Microsoft ToDo这样的单页应用,DOM元素可能不会立即可用,所以我们需要监听元素出现了我们再获取。当然我们也可以用一个循环方法不断去判断。

接下来,我们配置markdown-it解析器:

const md = markdownit({
  html: false,
  xhtmlOut: false,
  breaks: false,
  langPrefix: "language-",
  linkify: true,
  typographer: true,
  highlight: function (str, lang) {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return hljs.highlight(str, { language: lang }).value;
      } catch (__) {}
    }
    return "";
  },
});

这个配置禁用了HTML标签支持以增强安全性,启用了URL自动链接和排版美化功能。特别值得注意的是highlight函数,它集成了highlight.js,为代码块提供语法高亮功能。

为了使插件能够和ToDo的界面无缝集成,我们需要创建一些自定义的UI元素:

const initBtns = () => {
  if (document.getElementById("mstodo:btns")) return;
  const detailNote = document.querySelector(".detailNote") as HTMLDivElement;
  if (!detailNote) return;
​
  const edit = createBtnWithIcon(
    EDIT_BTN_ID,
    `<svg ...</svg>`
  );
  const view = createBtnWithIcon(
    VIEW_BTN_ID,
    `<svg ...</svg>`
  );
  const btns = document.createElement("div");
  btns.id = "mstodo:btns";
  btns.style.display = "flex";
  btns.style.gap = ".5em";
  btns.style.justifyContent = "flex-end";
  btns.style.padding = "0.5em 1em";
​
  btns.appendChild(edit);
  btns.appendChild(view);
  detailNote.parentElement &&
    detailNote.parentElement?.insertBefore(btns, detailNote);
  edit.addEventListener("click", () => {
    showEditor();
  });
  view.addEventListener("click", () => {
    isEdit = false;
    hideEditor();
  });
};

这个函数创建了编辑和预览按钮,并将它们插入到ToDo的界面中。通过这些按钮,用户可以在编辑Markdown文本和查看渲染结果之间自由切换。

插件的核心功能是动态渲染Markdown。这部分的实现依赖于MutationObserver API 和点击事件:

const observerHandler = behindDebounce(function () {
  const qlEditor = document.querySelector(".ql-editor") as HTMLDivElement;
  console.log("mstodo:editor existed: ", !!qlEditor);
​
  const mdViewer =
    document.getElementById("tstodo:mdViewer") ||
    (createContainer(qlEditor) as HTMLDivElement);
​
  console.log("mstodo:mdviewer existed: ", !!mdViewer);
​
  initBtns();
  if (!qlEditor) return;
  const mdContent = qlEditor.innerText;
  try {
    console.log("mstodo:parsing....");
    let result = md.render(mdContent);
    mdViewer.innerHTML = result;
​
    if (!isEdit) hideEditor();
  } catch (err) {
    console.error(err);
  }
}, 100);
​
const observer = new MutationObserver(observerHandler);
function observe() {
  observer.observe(document.querySelector(".ql-editor")!, {
    characterData: true,
    childList: true,
    subtree: true,
    characterDataOldValue: true
  });
}

这段代码创建了一个MutationObserver,通过开启 characterData 来监听编辑器内容的变化。每当内容发生变化时,它就会触发Markdown的重新渲染。注意到我们使用了behindDebounce函数来包装处理函数,这是为了避免在用户快速输入时触发过于频繁的渲染,从而提高性能。

为了处理用户的直接交互,我们还实现了点击监听:

function clickListen() {
  document.addEventListener("click", function (e) {
    const target = e.target as HTMLButtonElement;
    const parent = document.querySelector('.tasks') || document.querySelector('.grid-body')
    if (target.className === "taskItem-titleWrapper" || parent?.contains(target)) {
      console.log("mstodo: click listener triggered")
      observerHandler();
    }
  });
}

这个函数确保了在用户切换不同的任务项时能够正确更新Markdown渲染。

整个插件的初始化过程如下:

waitForElement(".ql-editor").then(() => {
  observerHandler();
  hideEditor();
  observe();
  clickListen();
});

这里我们用到了前面提到的waitForElement函数,确保在编辑器元素加载完成后才开始初始化插件。

通过这种方式,我们成功地为Microsoft ToDo添加了Markdown支持,而不需要修改ToDo的原始代码。用户现在可以在任务描述中使用Markdown语法,并实时预览渲染效果。

这个小插件展示了油猴插件的强大功能,也体现了前端开发中解决实际问题的创新思路。通过合理利用现代Web API如MutationObserver,结合成熟的开源库如markdown-it和highlight.js,我们能够相对简单地实现复杂的功能。

让我们来看看效果吧:

greasyfork.org/zh-CN/scrip…

2024-08-30_15-21-25-2.webp

源码在这里 here

原文首发地址 here

More

我之前写过一些小插件工具,也乘这个机会推荐给需要的朋友。

还有一些就不过多展开了, 有兴趣可以看这里 here