release: v0.1.2 — readOnly, new syntax, plugins, optimizations
Features: - feat(core): readOnly mode with config option and setReadOnly() API - feat(parser): superscript ^text^ and subscript ~text~ support - feat(parser): highlight/mark ==text== syntax - feat(parser): table column alignment with colons (:---, :---:, ---:) - feat(parser): emoji shortcodes 😄 → 😊 (80+ common emojis) - feat(plugins): imagePaste preset — paste clipboard images as base64 - feat(core): instance-level beforeRender/afterRender events - feat(styles): print stylesheet (@media print) - feat(styles): mark element CSS styling - feat(styles): readOnly mode CSS (.me-readonly) Optimizations: - perf(core): history debounce now configurable via historyDebounce option - perf(core): skip _render() in edit-only mode (already guarded) - fix(core): divider drag division-by-zero guard - fix(core): config.style deep merge with DEFAULTS - fix(core): scroll position preserved across mode switches Tests: - 9 new test cases: readOnly (3), parser syntax (6) - Total: 418 tests passing (+9 from v0.1.1) Chores: - version bump 0.1.1 → 0.1.2 - TypeScript definitions updated for new APIs
This commit is contained in:
+58
-4
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaEditor Parser - 轻量 Markdown 解析器
|
||||
* @module parser
|
||||
* @version 0.1.1
|
||||
* @version 0.1.2
|
||||
* @description 自研零依赖 Markdown 解析器,覆盖 CommonMark 子集 + GFM 扩展
|
||||
*
|
||||
* 支持语法:
|
||||
@@ -28,6 +28,30 @@
|
||||
|
||||
import { escapeHTML } from './utils.js';
|
||||
|
||||
/**
|
||||
* 常用 Emoji 短码映射(子集)
|
||||
*/
|
||||
const EMOJI_MAP = {
|
||||
smile: '😊', grinning: '😀', joy: '😂', rofl: '🤣', wink: '😉',
|
||||
heart: '❤️', heart_eyes: '😍', star: '⭐', fire: '🔥', rocket: '🚀',
|
||||
thumbsup: '👍', thumbsdown: '👎', clap: '👏', pray: '🙏', muscle: '💪',
|
||||
ok: '👌', point_up: '👆', point_down: '👇', point_left: '👈', point_right: '👉',
|
||||
check: '✅', cross: '❌', warning: '⚠️', info: 'ℹ️', question: '❓',
|
||||
bulb: '💡', book: '📖', memo: '📝', pin: '📌', link: '🔗',
|
||||
lock: '🔒', unlock: '🔓', key: '🔑', hammer: '🔨', wrench: '🔧',
|
||||
gear: '⚙️', tools: '🛠️', magnet: '🧲', zap: '⚡', cloud: '☁️',
|
||||
sun: '☀️', moon: '🌙', rain: '🌧️', snow: '❄️', umbrella: '☂️',
|
||||
coffee: '☕', pizza: '🍕', cake: '🎂', beer: '🍺', wine: '🍷',
|
||||
tada: '🎉', gift: '🎁', crown: '👑', gem: '💎', ring: '💍',
|
||||
eye: '👁️', ear: '👂', nose: '👃', tongue: '👅', lips: '👄',
|
||||
brain: '🧠', speech: '💬', thought: '💭', anger: '💢', sweat: '💦',
|
||||
one: '1️⃣', two: '2️⃣', three: '3️⃣', four: '4️⃣', five: '5️⃣',
|
||||
arrow_up: '⬆️', arrow_down: '⬇️', arrow_left: '⬅️', arrow_right: '➡️',
|
||||
copyright: '©️', registered: '®️', tm: '™️', x: '❌', o: '⭕',
|
||||
hundred: '💯', boom: '💥', dash: '💨', hole: '🕳️', bomb: '💣',
|
||||
email: '📧', phone: '📞', clock: '🕐', hourglass: '⏳', calendar: '📅',
|
||||
};
|
||||
|
||||
/**
|
||||
* 危险协议过滤
|
||||
*/
|
||||
@@ -105,13 +129,14 @@ export const parseMarkdown = (md, env = {}) => {
|
||||
// 表格:当前行含 |,下一行是分隔行
|
||||
if (/\|/.test(line) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
|
||||
const header = line;
|
||||
const align = parseAlign(lines[i + 1]);
|
||||
i += 2;
|
||||
const rows = [];
|
||||
while (i < lines.length && /\|/.test(lines[i]) && !/^\s*$/.test(lines[i])) {
|
||||
rows.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
tokens.push({ type: 'table', header, rows });
|
||||
tokens.push({ type: 'table', header, rows, align });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -170,6 +195,21 @@ const isTableSeparator = (line) => {
|
||||
return /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line) && /-/.test(line);
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析表格分隔行中的对齐信息
|
||||
*/
|
||||
const parseAlign = (line) => {
|
||||
const cells = line.replace(/^\s*\|?\s*|\s*\|?\s*$/g, '').split(/\s*\|\s*/);
|
||||
return cells.map((c) => {
|
||||
const l = /^:/.test(c);
|
||||
const r = /:$/.test(c);
|
||||
if (l && r) return 'center';
|
||||
if (r) return 'right';
|
||||
if (l) return 'left';
|
||||
return 'left';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染单个块级 token
|
||||
*/
|
||||
@@ -233,12 +273,14 @@ const renderCode = (code, lang, env) => {
|
||||
const renderTable = (tok, env) => {
|
||||
const splitRow = (r) => r.replace(/^\s*\|?\s*|\s*\|?\s*$/g, '').split(/\s*\|\s*/);
|
||||
const headers = splitRow(tok.header);
|
||||
const align = tok.align || [];
|
||||
const alignStyle = (i) => align[i] ? ` style="text-align:${align[i]}"` : '';
|
||||
let html = '<div class="me-table-wrap"><table><thead><tr>';
|
||||
html += headers.map((h) => `<th>${renderInline(h, env)}</th>`).join('');
|
||||
html += headers.map((h, i) => `<th${alignStyle(i)}>${renderInline(h, env)}</th>`).join('');
|
||||
html += '</tr></thead><tbody>';
|
||||
html += tok.rows.map((r) => {
|
||||
const cells = splitRow(r);
|
||||
return `<tr>${cells.map((c) => `<td>${renderInline(c, env)}</td>`).join('')}</tr>`;
|
||||
return `<tr>${cells.map((c, i) => `<td${alignStyle(i)}>${renderInline(c, env)}</td>`).join('')}</tr>`;
|
||||
}).join('');
|
||||
html += '</tbody></table></div>';
|
||||
return html;
|
||||
@@ -297,9 +339,21 @@ const renderInline = (text, env) => {
|
||||
// 8. 删除线 ~~text~~(非贪婪匹配,支持内含单个 ~ 字符)
|
||||
s = s.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
||||
|
||||
// 8b. 高亮标记 ==text==
|
||||
s = s.replace(/==(.+?)==/g, '<mark>$1</mark>');
|
||||
|
||||
// 8c. 上标 ^text^(排除空白和首尾脱字符)
|
||||
s = s.replace(/\^([^\s^].*?[^\s^]|[^\s^])\^/g, '<sup>$1</sup>');
|
||||
|
||||
// 8d. 下标 ~text~(排除空白和首尾波浪线)
|
||||
s = s.replace(/~([^\s~].*?[^\s~]|[^\s~])~/g, '<sub>$1</sub>');
|
||||
|
||||
// 9. 还原行内代码
|
||||
s = s.replace(/\u0000(\d+)\u0000/g, (m, idx) => `<code>${escapeHTML(codes[+idx])}</code>`);
|
||||
|
||||
// 9b. Emoji 短码 :name:
|
||||
s = s.replace(/:(\w+):/g, (m, name) => EMOJI_MAP[name] || m);
|
||||
|
||||
// 10. 软换行
|
||||
s = s.replace(/\n/g, '<br/>');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user