// ==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);
}
})();