Microsoft To-Do Markdown Preview Support - mstodo-md-preview

Microsoft To-Do Markdown Preview Support

// ==UserScript==
// @name         Microsoft To-Do Markdown Preview Support - mstodo-md-preview
// @namespace    https://github.com/joisun
// @version      1.1.3
// @author       Zhongyi Sun
// @description  Microsoft To-Do Markdown Preview Support
// @license      MIT
// @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/*
// @require      https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11.10.0/highlight.min.js
// @require      https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js
// @resource     highlight.js/styles/tokyo-night-dark.min.css  https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11.10.0/styles/tokyo-night-dark.min.css
// @grant        GM_addStyle
// @grant        GM_getResourceText
// ==/UserScript==

(o=>{if(typeof GM_addStyle=="function"){GM_addStyle(o);return}const e=document.createElement("style");e.textContent=o,document.head.append(e)})(' .markdown-body{color:#fffffff1;font-size:1em;font-weight:lighter;line-height:1.75;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.markdown-body u{text-underline-offset:6px}.markdown-body hr{border:none;height:1px;background-color:#ffffff0a;margin:2.5em 0}.markdown-body abbr:where([title]){text-decoration:underline dotted}.markdown-body a{color:var(--fg-deeper);text-decoration:none;font-weight:500}.markdown-body b,.markdown-body code,.markdown-body kbd,.markdown-body samp,.markdown-body pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}.markdown-body sub{bottom:-.25em}.markdown-body sup{top:-.5em}.markdown-body blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}.markdown-body fieldset{margin:0;padding:0}.markdown-body legend{padding:0}.markdown-body ol,.markdown-body ul,.markdown-body menu{list-style:none;margin:0;padding:0}.markdown-body dialog{padding:0}.markdown-body textarea{resize:vertical}.markdown-body button,.markdown-body [role=button]{cursor:pointer}:disabled{cursor:default}.markdown-body [class~=lead]{color:#4b5563;font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.markdown-body strong{font-weight:600;margin:0 .2em;color:#ffffffda}.markdown-body i{padding:0 .4em;color:inherit;font-style:italic;background-color:transparent}.markdown-body em{padding:0 .4em;background-color:#00f73685;font-style:normal}.markdown-body u{font-weight:600;font-weight:400;margin:0 .2em;border-radius:4px;padding:.1em .4em;text-underline-offset:.4em;text-decoration:underline 1px dashed;text-decoration-color:#f44}.markdown-body ol>li,.markdown-body ul>li{position:relative;padding-left:1.25em}.markdown-body ol>li:before{content:counter(list-item,var(--list-counter-style, decimal)) ".";position:absolute;font-weight:400;color:#6b7280;left:0}.markdown-body ul>li:before{content:"";position:absolute;background-color:#d1d5db;border-radius:50%;width:.375em;height:.375em;top:.6875em;left:.25em}.markdown-body blockquote{font-weight:500;font-style:italic;color:inherit;font-size:.9em;opacity:.8;border-left:solid #444;background-color:#4444444a;border-left-width:.4rem;quotes:"\u201C" "\u201D" "\u2018" "\u2019";margin-top:.6em;margin-bottom:.6em;margin-left:auto;padding:.5em .5em .5em 1em}.markdown-body blockquote p:first-of-type:before{content:open-quote}.markdown-body blockquote p:last-of-type:after{content:close-quote}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{color:#ffffffd0;font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif;letter-spacing:.1em;margin:0}.markdown-body h1{font-weight:800;font-size:3rem;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.markdown-body h2{font-weight:700;font-size:2.25rem;line-height:2.5rem;margin-top:2em;margin-bottom:1em}.markdown-body h3{font-weight:700;font-size:1.875rem;line-height:2.25rem;margin-top:1.6em;margin-bottom:.6em}.markdown-body h4{font-weight:700;font-size:1.5rem;line-height:2rem;margin-top:1.5em;margin-bottom:.5em}.markdown-body h5{font-weight:700;font-size:1.25rem;line-height:1.75rem;margin-top:1.5em;margin-bottom:.5em}.markdown-body h6{font-weight:700;font-size:1.125rem;line-height:1.75rem;margin-top:1.5em;opacity:.7;margin-bottom:.5em}.markdown-body h2+*{margin-top:0}.markdown-body figure figcaption{color:#6b7280;font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.markdown-body code{color:inherit;font-weight:600;font-size:.875em}.markdown-body p code{font-weight:700;margin:0 .5em;font-size:1.1em}.markdown-body p code:before{content:"`"}.markdown-body p code:after{content:"`"}.markdown-body a code{color:#111827}.markdown-body pre code:before,.markdown-body pre code:after{content:none}.markdown-body table{width:100%;table-layout:auto;text-align:left;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857;border-collapse:collapse}.markdown-body thead{color:inherit;font-weight:600;border-bottom-width:1px;border-bottom-color:#d1d5db}.markdown-body thead th{vertical-align:bottom;white-space:nowrap;padding-right:.5714286em;padding-bottom:.5714286em;padding-left:.5714286em}.markdown-body tbody tr{border-bottom:1px solid #ffffff0f}.markdown-body tbody tr:last-child{border-bottom-width:0}.markdown-body tbody td{vertical-align:top;padding:.5714286em}.markdown-body p{margin-top:.25em;margin-bottom:.25em}.markdown-body img{width:auto;height:auto;max-width:100%;margin-top:2em;margin-bottom:2em}.markdown-body video{margin-top:2em;margin-bottom:2em}.markdown-body figure{margin-top:2em;margin-bottom:2em}.markdown-body figure>*{margin-top:0;margin-bottom:0}.markdown-body h2 code{font-size:.875em}.markdown-body h3 code{font-size:.9em}.markdown-body ol,.markdown-body ul{margin-top:0;margin-bottom:1em;margin-left:1em;list-style-type:none}.markdown-body pre{border-radius:.375em;border-width:1px;overflow:auto;padding:1.25em;background-color:#00000073;margin:.2em .4em}.markdown-body pre code{color:inherit;background:none;font-size:.975em;font-weight:400} ');

(function (markdownit, hljs) {
  'use strict';

  function behindDebounce(func, duration) {
    let timer = null;
    return () => {
      timer && clearTimeout(timer);
      timer = setTimeout(() => {
        func();
      }, duration);
    };
  }
  function waitForElement(selector) {
    return new Promise((resolve) => {
      if (document.querySelector(selector)) {
        return resolve(document.querySelector(selector));
      }
      const observer2 = new MutationObserver(() => {
        if (document.querySelector(selector)) {
          resolve(document.querySelector(selector));
          observer2.disconnect();
        }
      });
      observer2.observe(document.body, {
        childList: true,
        subtree: true
      });
    });
  }
  const cssLoader = (e) => {
    const t = GM_getResourceText(e);
    return GM_addStyle(t), t;
  };
  cssLoader("highlight.js/styles/tokyo-night-dark.min.css");
  const createBtnWithIcon = function(id, icon) {
    const btn = document.createElement("button");
    btn.id = id;
    btn.innerHTML = icon;
    btn.style.width = "auto";
    btn.style.height = "auto";
    btn.style.color = "inherit";
    btn.style.padding = ".2em .5em";
    btn.style.backgroundColor = "#555";
    btn.style.border = "none";
    btn.style.borderRadius = "4px";
    btn.style.display = "inline-flex";
    btn.style.alignItems = "center";
    btn.style.justifyContent = "center";
    btn.style.cursor = "pointer";
    btn.style.transition = "background-color 0.3s ease";
    btn.addEventListener("mouseenter", () => {
      btn.style.backgroundColor = "#444";
    });
    btn.addEventListener("mouseleave", () => {
      btn.style.backgroundColor = "#555";
    });
    return btn;
  };
  const listenPasteImg = () => {
    document.addEventListener("paste", (event) => {
      var _a;
      const items = (_a = event.clipboardData) == null ? void 0 : _a.items;
      if (!items) return;
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        if (item.type.indexOf("image") !== -1) {
          const blob = item.getAsFile();
          if (blob) {
            const reader = new FileReader();
            reader.onload = (e) => {
              var _a2;
              const base64Image = (_a2 = e.target) == null ? void 0 : _a2.result;
              const markdownImage = `![Image](${base64Image})`;
              insertMarkdownAtCursor(markdownImage);
            };
            reader.readAsDataURL(blob);
          }
        }
      }
    });
    function insertMarkdownAtCursor(markdown) {
      const activeElement = document.activeElement;
      if (activeElement.tagName === "TEXTAREA" || activeElement.tagName === "INPUT") {
        const inputElement = activeElement;
        const start = inputElement.selectionStart || 0;
        const end = inputElement.selectionEnd || 0;
        const text = inputElement.value;
        inputElement.value = text.slice(0, start) + markdown + text.slice(end);
        inputElement.selectionStart = inputElement.selectionEnd = start + markdown.length;
      } else if (activeElement.isContentEditable) {
        const selection = window.getSelection();
        if (selection && selection.rangeCount > 0) {
          const range = selection.getRangeAt(0);
          range.deleteContents();
          const markdownNode = document.createTextNode(markdown);
          range.insertNode(markdownNode);
          range.setStartAfter(markdownNode);
          range.setEndAfter(markdownNode);
          selection.removeAllRanges();
          selection.addRange(range);
        }
      }
    }
  };
  const md = markdownit({
    // Enable HTML tags in source
    html: true,
    // Use '/' to close single tags (<br />)
    xhtmlOut: false,
    // Convert '\n' in paragraphs into <br>
    breaks: false,
    // CSS language prefix for fenced blocks
    langPrefix: "language-",
    // autoconvert URL-like texts to links
    linkify: true,
    // Enable smartypants and other sweet transforms
    typographer: true,
    highlight: function(str, lang) {
      if (lang && hljs.getLanguage(lang)) {
        try {
          return hljs.highlight(str, { language: lang }).value;
        } catch (__) {
        }
      }
      return "";
    }
  });
  const EDIT_BTN_ID = "mstodo:editBtn";
  const VIEW_BTN_ID = "mstodo:viewBtn";
  waitForElement(".ql-editor").then(() => {
    observerHandler();
    hideEditor();
    observe();
    clickListen();
    listenPasteImg();
  });
  let isEdit = false;
  function hideEditor() {
    if (isEdit) return;
    const editor = document.querySelector(".ql-editor");
    editor && (editor.style.height = "0px");
    const viewer = document.getElementById("tstodo:mdViewer");
    viewer && (viewer.style.height = "auto");
  }
  function showEditor() {
    isEdit = true;
    const editor = document.querySelector(".ql-editor");
    editor && (editor.style.height = "auto");
    const viewer = document.getElementById("tstodo:mdViewer");
    viewer && (viewer.style.height = "0px");
  }
  const initBtns = () => {
    var _a;
    if (document.getElementById("mstodo:btns")) return;
    const detailNote = document.querySelector(".detailNote");
    if (!detailNote) return;
    const edit = createBtnWithIcon(
      EDIT_BTN_ID,
      `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M6 22q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h7.175q.4 0 .763.15t.637.425l4.85 4.85q.275.275.425.638t.15.762V10.4q0 .275-.162.475t-.413.3q-.4.15-.763.388T18 12.1l-5.4 5.4q-.275.275-.437.638T12 18.9V21q0 .425-.288.713T11 22zm8-1v-1.65q0-.2.075-.387t.225-.338l5.225-5.2q.225-.225.5-.325t.55-.1q.3 0 .575.113t.5.337l.925.925q.2.225.313.5t.112.55t-.1.563t-.325.512l-5.2 5.2q-.15.15-.337.225T16.65 22H15q-.425 0-.712-.287T14 21m6.575-4.6l.925-.975l-.925-.925l-.95.95zM14 9h4l-5-5l5 5l-5-5v4q0 .425.288.713T14 9"/></svg>`
    );
    const view = createBtnWithIcon(
      VIEW_BTN_ID,
      `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024"><path fill="currentColor" d="M854.6 288.7c6 6 9.4 14.1 9.4 22.6V928c0 17.7-14.3 32-32 32H192c-17.7 0-32-14.3-32-32V96c0-17.7 14.3-32 32-32h424.7c8.5 0 16.7 3.4 22.7 9.4zM790.2 326L602 137.8V326zM426.13 600.93l59.11 132.97a16 16 0 0 0 14.62 9.5h24.06a16 16 0 0 0 14.63-9.51l59.1-133.35V758a16 16 0 0 0 16.01 16H641a16 16 0 0 0 16-16V486a16 16 0 0 0-16-16h-34.75a16 16 0 0 0-14.67 9.62L512.1 662.2l-79.48-182.59a16 16 0 0 0-14.67-9.61H383a16 16 0 0 0-16 16v272a16 16 0 0 0 16 16h27.13a16 16 0 0 0 16-16z"/></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 && ((_a = detailNote.parentElement) == null ? void 0 : _a.insertBefore(btns, detailNote));
    edit.addEventListener("click", () => {
      showEditor();
    });
    view.addEventListener("click", () => {
      isEdit = false;
      hideEditor();
    });
  };
  const createContainer = (qlEditor) => {
    var _a;
    if (!qlEditor) return;
    const container = document.createElement("div");
    container.id = "tstodo:mdViewer";
    container.classList.add("markdown-body");
    container.style = `
    overflow: hidden;
    background-color: inherit;
  `;
    container.addEventListener("click", (event) => {
      event.stopPropagation();
      event.preventDefault();
    });
    qlEditor.parentElement && ((_a = qlEditor.parentElement) == null ? void 0 : _a.insertBefore(container, qlEditor));
    return container;
  };
  const observerHandler = behindDebounce(function() {
    const qlEditor = document.querySelector(".ql-editor");
    console.log("mstodo:editor existed: ", !!qlEditor);
    const mdViewer = document.getElementById("tstodo:mdViewer") || createContainer(qlEditor);
    console.log("mstodo:mdviewer existed: ", !!mdViewer);
    initBtns();
    if (!qlEditor) return;
    const mdContent = qlEditor.innerText;
    try {
      console.log("mstodo:parsing....");
      let result = md.render(mdContent.replace(/\u00A0/g, "  "));
      mdViewer.innerHTML = result;
      if (!isEdit) hideEditor();
    } catch (err) {
      console.error(err);
    }
  }, 100);
  const observer = new MutationObserver(observerHandler);
  function disconnect() {
    observer.disconnect();
  }
  function observe() {
    disconnect();
    observer.observe(document.querySelector(".ql-editor"), {
      characterData: true,
      // 观察目标节点内文本内容的变化
      childList: true,
      // 观察目标节点中直接子节点的增删
      subtree: true,
      // 观察所有后代节点
      characterDataOldValue: true
      // 记录文本内容的变化前信息
    });
  }
  function clickListen() {
    document.addEventListener("click", function(e) {
      const target = e.target;
      const parent = document.querySelector(".tasks") || document.querySelector(".grid-body");
      if (target.className === "taskItem-titleWrapper" || (parent == null ? void 0 : parent.contains(target))) {
        console.log("mstodo: click listener triggered");
        observerHandler();
        observe();
      }
    });
  }

})(markdownit, hljs);