StackEdit一键格式化内容

为 StackEdit 添加“一键格式化内容”和“格式化表格”按钮,自动格式化输入区内容(换行、LaTeX、表格等)

目前為 2025-09-23 提交的版本,檢視 最新版本

// ==UserScript==
// @name         StackEdit一键格式化内容
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  为 StackEdit 添加“一键格式化内容”和“格式化表格”按钮,自动格式化输入区内容(换行、LaTeX、表格等)
// @author       damu
// @match        https://stackedit.io/app*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  /** ---------- 读取编辑器内容(优先 CodeMirror) ---------- */
  function getEditorContent() {
    try {
      const cmHost = document.querySelector('.CodeMirror');
      if (cmHost && cmHost.CodeMirror) return cmHost.CodeMirror.getValue();
    } catch (e) { }
    const pre = document.querySelector('pre.editor__inner, pre.editor__inner.markdown-highlighting');
    if (pre) return pre.innerText || pre.textContent || '';
    const ta = document.querySelector('textarea, .editormd-markdown-textarea');
    if (ta) return ta.value || '';
    return '';
  }

  /** ---------- 写回编辑器内容(多重策略) ---------- */
  function setEditorContent(content) {
    try {
      const cmHost = document.querySelector('.CodeMirror');
      if (cmHost && cmHost.CodeMirror) {
        cmHost.CodeMirror.setValue(content);
        cmHost.CodeMirror.refresh && cmHost.CodeMirror.refresh();
        return true;
      }
    } catch (e) { }
    const pre = document.querySelector('pre.editor__inner, pre.editor__inner.markdown-highlighting');
    if (pre) {
      try {
        pre.focus();
        const sel = window.getSelection();
        sel.removeAllRanges();
        const range = document.createRange();
        range.selectNodeContents(pre);
        sel.addRange(range);
        const execOk = document.execCommand('insertText', false, content);
        if (!execOk || pre.innerText !== content) {
          sel.removeAllRanges();
          const r2 = document.createRange();
          r2.selectNodeContents(pre);
          r2.deleteContents();
          r2.insertNode(document.createTextNode(content));
        }
        ['input', 'keyup', 'change'].forEach(ev => pre.dispatchEvent(new Event(ev, { bubbles: true })));
        setTimeout(() => {
          try {
            pre.blur();
          } catch (e) { }
        }, 60);
        return true;
      } catch (e) {
        try {
          pre.textContent = content;
          pre.dispatchEvent(new Event('input', { bubbles: true }));
          return true;
        } catch (ee) { }
      }
    }
    const ta = document.querySelector('textarea, .editormd-markdown-textarea');
    if (ta) {
      ta.value = content;['input', 'change'].forEach(ev => ta.dispatchEvent(new Event(ev, { bubbles: true })));
      return true;
    }
    return false;
  }

  /** ---------- 表格格式化函数 ---------- */
  const formatTableLine = line => {
    const cells = line.split('|');
    let result = '|';
    for (let i = 1;
      i < cells.length - 1;
      i++) {
        const content = cells[i].trim() === '' ? '  ' : ` ${cells[i].trim()} `;
      result += content + '|';
    } return result;
  };
  const formatSeparatorLine = line => {
    const cells = line.split('|').map(c => c.trim());
    let result = '|';
    for (let i = 1;
      i < cells.length - 1;
      i++) result += ' --- |';
    return result;
  };
  const formatTable = content => {
    const lines = content.split('\n');
    let tableStart = -1, tableEnd = -1, tables = [];
    for (let i = 0;
      i < lines.length;
      i++) {
      const line = lines[i].trim();
      if (line.startsWith('|') && line.endsWith('|')) {
        if (tableStart === -1) tableStart = i;
        tableEnd = i;
      }
      else if (tableStart !== -1) {
        if (i === tableStart + 1 && line.includes('|') && line.replace(/[^|-]/g, '').length > 0) tableEnd = i;
        else {
          if (tableEnd - tableStart >= 1) tables.push({ start: tableStart, end: tableEnd });
          tableStart = -1;
          tableEnd = -1;
        }
      }
    }
    if (tableStart !== -1 && tableEnd - tableStart >= 1) tables.push({ start: tableStart, end: tableEnd });
    if (tables.length === 0) return content;
    const newLines = [...lines];
    tables.forEach(({ start, end }) => {
      newLines[start] = formatTableLine(newLines[start]);
      if (start + 1 <= end) newLines[start + 1] = formatSeparatorLine(newLines[start + 1]);
      for (let i = start + 2;
        i <= end;
        i++) newLines[i] = formatTableLine(newLines[i]);
    });
    return newLines.join('\n');
  };

  /** ---------- 主格式化流程 ---------- */
  function formatAllContent() {
    let content = getEditorContent();
    if (!content) {
      showToast('未获取到编辑器内容,无法格式化!', 2);
      return;
    }
    const needFormat = content.includes('\\n') || /\\\\\[/.test(content) || /\\\\\(/.test(content) || /\\\\\]/.test(content) || /\\\\\)/.test(content);
    if (!needFormat) {
      showToast('内容无需格式化!', 3);
      return;
    }
    content = content.replace(/\\n/g, '\n').replace(/\\\\\[/g, '$$ ').replace(/\\\\\(/g, '$$').replace(/\\\\\]/g, ' $$').replace(/\\\\\)/g, '$$').replace(/\\\\/g, '\\');
    content = formatTable(content).trim();
    setEditorContent(content) ? showToast('一键格式化完成!') : showToast('写回编辑器失败!', 1);
  }

  /** ---------- 创建按钮并安装快捷键 ---------- */
  function createButtonIfMissing() {
    const nav = document.querySelector('.navigation-bar__inner.navigation-bar__inner--edit-pagedownButtons') || document.querySelector('.navigation-bar__inner');
    if (!nav || document.getElementById('stackedit-format-one-click')) return;
    const btn = document.createElement('button');
    btn.id = 'stackedit-format-one-click';
    btn.title = '一键格式化内容 – alt+Shift+F';
    btn.innerText = '一键格式化内容';
    btn.style.cssText = 'margin-left:4px;padding:2px 6px;font-size:13px;cursor:pointer;border:1px solid #ccc;border-radius:3px;background:#f0f0f0;color:#333;white-space:nowrap;';
    btn.addEventListener('click', formatAllContent);
    nav.appendChild(btn);
  }

  /** ---------- 新增单独格式化表格按钮 ---------- */
  function createFormatTableButton() {
    const nav = document.querySelector('.navigation-bar__inner.navigation-bar__inner--edit-pagedownButtons') || document.querySelector('.navigation-bar__inner');
    if (!nav || document.getElementById('stackedit-format-table')) return;
    const btn = document.createElement('button');
    btn.id = 'stackedit-format-table';
    btn.title = '仅格式化表格';
    btn.innerText = '格式化表格';
    btn.style.cssText = 'margin-left:4px;padding:2px 6px;font-size:13px;cursor:pointer;border:1px solid #ccc;border-radius:3px;background:#f9f9f9;color:#333;white-space:nowrap;';
    btn.addEventListener('click', () => {
      let content = getEditorContent();
      if (!content) return showToast('未获取到编辑器内容', 2);
      if (!/\|.*\|/.test(content)) return showToast('内容无需格式化!', 3);
      const formatted = formatTable(content);
      if (formatted === content) return showToast('没有发现表格需要格式化', 2);
      setEditorContent(formatted);
      showToast('表格已格式化', 1);
    });
    nav.appendChild(btn);
  }

  // 初始化按钮
  const checker = setInterval(() => {
    const navExists = !!document.querySelector('.navigation-bar__inner, .navigation-bar__inner.navigation-bar__inner--edit-pagedownButtons');
    const editorExists = !!document.querySelector('pre.editor__inner, .CodeMirror, textarea');
    if (navExists && editorExists) {
      createButtonIfMissing();
      createFormatTableButton();
      clearInterval(checker);
    }
  }, 150);

  const mo = new MutationObserver(() => {
    createButtonIfMissing();
    createFormatTableButton();
  });
  mo.observe(document.body, { childList: true, subtree: true });

  // 快捷键 alt+Shift+F
  document.addEventListener('keydown', e => {
    if (e.altKey && e.shiftKey && (e.key === 'F' || e.key === 'f')) {
      e.preventDefault();
      formatAllContent();
    }
  });

  /** ---------- 显示轻量通知 ---------- */
  function showToast(msg, type = 0, duration = 1500) {
    const div = document.createElement('div');
    div.textContent = msg;
    let bgColor = '#4caf50';
    switch (type) {
      case 1: bgColor = '#f44336';
        break;
      case 2: bgColor = '#9e9e9e';
        break;
      case 3: bgColor = '#ffffff';
        break;
    }
    div.style.cssText = `
      position: fixed;
      top: 50px;
      right: 50px;
      background: ${bgColor};
      color: ${type === 3 ? '#333' : 'white'};
      padding: 6px 12px;
      border-radius: 4px;
      box-shadow: 0 2px 6px rgba(0,0,0,0.2);
      z-index: 9999;
      font-size: 14px;
      pointer-events: none;
      opacity: 0;
      transition: opacity 0.2s;`;
    document.body.appendChild(div);
    requestAnimationFrame(() => div.style.opacity = 1);
    setTimeout(() => {
      div.style.opacity = 0;
      setTimeout(() => div.remove(), 200);
    }, duration);
  }

})();