From ac6638247fc72bc4f21a3a6d8b7ae3d19189a9ad Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Wed, 19 Aug 2026 22:30:59 +0800 Subject: [PATCH] =?UTF-8?q?build:=20=E6=8F=90=E4=BA=A4=20dist=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E4=BA=A7=E7=89=A9=EF=BC=88v0.4.2=20=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=205=20=E6=A0=BC=E5=BC=8F=20+=20sourcemap=20+=20=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=A3=B0=E6=98=8E=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/metona-editor.cjs | 178 +++++++++++++++++++++++++++++-------- dist/metona-editor.cjs.map | 2 +- dist/metona-editor.d.ts | 6 +- dist/metona-editor.js | 178 +++++++++++++++++++++++++++++-------- dist/metona-editor.js.map | 2 +- dist/metona-editor.min.js | 2 +- dist/metona-editor.mjs | 178 +++++++++++++++++++++++++++++-------- dist/metona-editor.mjs.map | 2 +- 8 files changed, 432 insertions(+), 116 deletions(-) diff --git a/dist/metona-editor.cjs b/dist/metona-editor.cjs index e79e2b3..c914ebb 100644 --- a/dist/metona-editor.cjs +++ b/dist/metona-editor.cjs @@ -189,7 +189,7 @@ const LOCALES = { renderError: '렌더링 실패', outline: '개요', cut: '잘라내기', copy: '복사', paste: '붙여넣기', selectAll: '전체 선택', - regex: '정규식', shortcuts: '단축키', imageTooLarge: '이미지가 너무 큽니다({size}KB > {max}KB)', + regex: '정규식', shortcuts: '단축키', imageTooLarge: '이미지가 너무 큽니다 ({size}KB > {max}KB)', }, fr: { bold: 'Gras', italic: 'Italique', underline: 'Souligné', strikethrough: 'Barré', @@ -778,6 +778,18 @@ const createInstanceI18n = (editor) => { } }); } + // 语言切换即时刷新已渲染的 UI(状态栏统计标签、大纲标题),无需等待下一次输入 + if (typeof editor._updateWordCount === 'function') { + try { + editor._updateWordCount(); + } + catch (_) { } + } + if (editor.el && typeof editor.el.querySelector === 'function') { + const outlineTitle = editor.el.querySelector('.me-outline-title'); + if (outlineTitle) + outlineTitle.textContent = instanceT('outline') || 'Outline'; + } return instanceLocale; }; return { @@ -1771,6 +1783,9 @@ const validateConfig = (schema = {}, config = {}) => { }; // ============ Preset plugins ============ const escapeAttr = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); +/** 实例级翻译:优先用 editor.t(实例 locale),否则退回全局 t。 + * 插件面板文案跟随实例语言而非全局语言。 */ +const instanceT = (editor) => (key, params) => (typeof editor.t === 'function' ? editor.t(key, params) : t(key, params)); const autoSavePlugin = { name: 'autoSave', version: '0.1.1', description: 'Auto-save to localStorage', priority: 100, install(editor, options) { @@ -1947,6 +1962,7 @@ const searchReplacePlugin = { install(editor) { if (!editor || !editor.textarea || typeof document === 'undefined') return; + const tt = instanceT(editor); // Inject style once globally if (!document.getElementById('me-search-style')) { const style = document.createElement('style'); @@ -2013,7 +2029,7 @@ const searchReplacePlugin = { const panel = document.createElement('div'); panel.className = 'me-search'; panel.dataset.replace = showReplace ? '1' : '0'; - panel.innerHTML = `
`; + panel.innerHTML = `
`; editor.el.appendChild(panel); state._panel = panel; _updateReplaceVisible(); @@ -2239,6 +2255,7 @@ const imagePastePlugin = { install(editor, options = {}) { if (!editor || !editor.textarea || typeof document === 'undefined') return; + const tt = instanceT(editor); const maxSizeKB = options.maxSizeKB || 500; const _onPaste = (e) => { const items = e.clipboardData?.items; @@ -2253,7 +2270,7 @@ const imagePastePlugin = { if (maxSizeKB > 0 && sizeKB > maxSizeKB) { e.preventDefault(); if (typeof editor.toast === 'function') - editor.toast(t('imageTooLarge', { size: sizeKB.toFixed(0), max: maxSizeKB }) || `Image too large (${sizeKB.toFixed(0)}KB > ${maxSizeKB}KB)`, { type: 'warning' }); + editor.toast(tt('imageTooLarge', { size: sizeKB.toFixed(0), max: maxSizeKB }) || `Image too large (${sizeKB.toFixed(0)}KB > ${maxSizeKB}KB)`, { type: 'warning' }); break; } e.preventDefault(); @@ -2280,62 +2297,66 @@ const shortcutHelpPlugin = { install(editor) { if (!editor || !editor.textarea || typeof document === 'undefined') return; + const tt = instanceT(editor); if (!document.getElementById('me-shortcut-style')) { const s = document.createElement('style'); s.id = 'me-shortcut-style'; s.textContent = `.me-shortcut-overlay{position:fixed;top:0;left:0;right:0;bottom:0;z-index:50;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center}.me-shortcut-panel{background:var(--md-bg,#fff);border-radius:12px;padding:24px;max-width:560px;width:90%;max-height:80vh;overflow-y:auto;box-shadow:0 12px 40px rgba(0,0,0,0.3)}.me-shortcut-panel h3{font-size:16px;margin:0 0 16px;color:var(--md-text)}.me-shortcut-panel table{width:100%;border-collapse:collapse;font-size:13px}.me-shortcut-panel td{padding:6px 10px;border-bottom:1px solid var(--md-border)}.me-shortcut-panel td:first-child{font-family:var(--md-mono);font-size:12px;color:var(--md-accent);white-space:nowrap;width:40%}.me-shortcut-panel .me-shortcut-close{position:absolute;top:16px;right:20px;background:none;border:none;font-size:20px;cursor:pointer;color:var(--md-muted)}`; document.head.appendChild(s); } - let _panel = null; - const _close = () => { if (_panel) { - _panel.remove(); - _panel = null; + // 面板引用放入 holder:install 时无法预知未来打开的面板, + // 卸载时经 holder 取实时引用,避免面板残留 DOM。 + const st = { panel: null }; + const _close = () => { if (st.panel) { + st.panel.remove(); + st.panel = null; } }; const _open = () => { - if (_panel) { + if (st.panel) { _close(); return; } - // i18n-aware shortcut labels + // i18n-aware shortcut labels(跟随实例语言) const builtin = [ - ['Ctrl+B', t('bold') || 'Bold'], ['Ctrl+I', t('italic') || 'Italic'], - ['Ctrl+U', t('underline') || 'Underline'], ['Ctrl+K', t('link') || 'Link'], - ['Ctrl+E', t('code') || 'Code'], ['Ctrl+1/2/3', t('h1') || 'Heading'], - ['Ctrl+Q', t('quote') || 'Quote'], ['Ctrl+Z', t('undo') || 'Undo'], - ['Ctrl+Y', t('redo') || 'Redo'], ['Ctrl+S', t('save') || 'Save'], - ['Ctrl+F', t('search') || 'Search'], ['Ctrl+H', t('replace') || 'Replace'], - ['Tab', t('indent') || 'Indent'], ['Shift+Tab', t('outdent') || 'Outdent'], - ['?', t('shortcuts') || 'Shortcuts'], + ['Ctrl+B', tt('bold') || 'Bold'], ['Ctrl+I', tt('italic') || 'Italic'], + ['Ctrl+U', tt('underline') || 'Underline'], ['Ctrl+K', tt('link') || 'Link'], + ['Ctrl+E', tt('code') || 'Code'], ['Ctrl+1/2/3', tt('h1') || 'Heading'], + ['Ctrl+Q', tt('quote') || 'Quote'], ['Ctrl+Z', tt('undo') || 'Undo'], + ['Ctrl+Y', tt('redo') || 'Redo'], ['Ctrl+S', tt('save') || 'Save'], + ['Ctrl+F', tt('search') || 'Search'], ['Ctrl+H', tt('replace') || 'Replace'], + ['Tab', tt('indent') || 'Indent'], ['Shift+Tab', tt('outdent') || 'Outdent'], + ['?', tt('shortcuts') || 'Shortcuts'], ]; let rows = ''; builtin.forEach(([c, d]) => { rows += `${c}${d}`; }); const overlay = document.createElement('div'); overlay.className = 'me-shortcut-overlay'; - overlay.innerHTML = `

⌨️ ${t('shortcuts') || 'Shortcuts'}

${rows}
`; + overlay.innerHTML = `

⌨️ ${tt('shortcuts') || 'Shortcuts'}

${rows}
`; overlay.addEventListener('click', (e) => { if (e.target === overlay || e.target.classList.contains('me-shortcut-close')) _close(); }); document.body.appendChild(overlay); - _panel = overlay; + st.panel = overlay; }; const _onKeydown = (e) => { if (e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); _open(); } - if (e.key === 'Escape' && _panel) + if (e.key === 'Escape' && st.panel) _close(); }; editor.textarea.addEventListener('keydown', _onKeydown); - setPluginState(editor, K_SHORTCUT, { _panel, _open, _close, _onKeydown }); + setPluginState(editor, K_SHORTCUT, { state: st, _open, _close, _onKeydown }); }, destroy(editor) { - const state = pluginState(editor, K_SHORTCUT); - if (state) { - if (state._panel) { - state._panel.remove(); + const exposed = pluginState(editor, K_SHORTCUT); + if (exposed) { + if (exposed.state && exposed.state.panel) { + exposed.state.panel.remove(); + exposed.state.panel = null; } } - const _onKeydown = state?._onKeydown; + const _onKeydown = exposed?._onKeydown; if (_onKeydown && editor?.textarea) editor.textarea.removeEventListener('keydown', _onKeydown); deletePluginState(editor, K_SHORTCUT); @@ -3000,7 +3021,6 @@ function initFloatingToolbar() { const paddingTop = parseFloat(taStyle.paddingTop) || 16; const paddingLeft = parseFloat(taStyle.paddingLeft) || 18; const fontSize = parseFloat(taStyle.fontSize) || 13.5; - const charWidth = fontSize * 0.6; const textBefore = ta.value.substring(0, start); const lines = textBefore.split('\n'); const currentLine = lines.length - 1; @@ -3013,7 +3033,18 @@ function initFloatingToolbar() { let top = lineLocalTop - toolbarHeight - gap; if (top < gap) top = lineLocalTop + lineHeight + gap; - const colX = taRect.left + paddingLeft + midCol * charWidth - ta.scrollLeft; + // 逐字符估算列偏移:CJK 全角字符宽约 1em,其余按 0.6em, + // 纯 ASCII 选区回退到原 0.6em 估算,中文选区不再整体偏左。 + const CJK_RE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/; + const lineText = lines[lines.length - 1] || ''; + const measureX = (col) => { + let w = 0; + const n = Math.max(0, Math.min(col, lineText.length)); + for (let i = 0; i < n; i++) + w += CJK_RE.test(lineText[i]) ? fontSize : fontSize * 0.6; + return w; + }; + const colX = taRect.left + paddingLeft + measureX(midCol) - ta.scrollLeft; let left = colX - paneRect.left - 80; left = Math.max(4, Math.min(left, paneRect.width - 200)); this._floatingToolbar.style.top = top + 'px'; @@ -3213,6 +3244,12 @@ const installContextMenu = (proto) => { * @module outline * @version 0.4.0 */ +/** querySelector 用的 id 转义:优先原生 CSS.escape,环境缺失时退化为简易转义 */ +const cssEscape = (id) => { + if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') + return CSS.escape(id); + return id.replace(/[^a-zA-Z0-9_\u00A0-\uFFFF-]/g, '\\$&'); +}; function buildOutline() { if (!this.config.outline || !this.previewEl) return; @@ -3223,10 +3260,40 @@ function buildOutline() { const headingRe = /]*>(.+?)<\/h\1>/gi; let m; while ((m = headingRe.exec(this.previewEl.innerHTML)) !== null) { - headings.push({ level: parseInt(m[1], 10), id: m[2], text: m[3].replace(/<[^>]+>/g, '') }); + headings.push({ level: parseInt(m[1], 10), id: m[2], text: m[3].replace(/<[^>]+>/g, ''), srcLine: -1 }); } if (!headings.length) return; + // 计算每个渲染标题在源码中的行号(跳过围栏代码块内部), + // 点击时按行号精确定位光标,重复标题文本也不会串位。 + const srcLines = []; + const lines = this._value.split('\n'); + let inFence = false; + let fenceChar = ''; + const isSetextText = (l) => /\S/.test(l) && !/^\s{0,3}([-*+]|\d+\.)\s/.test(l) && !/^\s{0,3}>/.test(l) && !/^\s{0,3}(#{1,6}\s|`{3,}|~{3,})/.test(l); + for (let li = 0; li < lines.length; li++) { + const l = lines[li]; + const fence = l.match(/^\s{0,3}(`{3,}|~{3,})/); + if (fence) { + if (!inFence) { + inFence = true; + fenceChar = fence[1][0]; + } + else if (fence[1][0] === fenceChar) + inFence = false; + continue; + } + if (inFence) + continue; + if (/^ {0,3}(?:>\s*)*#{1,6}\s+\S/.test(l)) { + srcLines.push(li); + continue; + } + const next = lines[li + 1] || ''; + if (li + 1 < lines.length && /^ {0,3}(={3,}|-{3,})\s*$/.test(next) && isSetextText(l)) + srcLines.push(li); + } + headings.forEach((h, i) => { h.srcLine = i < srcLines.length ? srcLines[i] : -1; }); const panel = document.createElement('div'); panel.className = 'me-outline'; panel.innerHTML = `
${this.t('outline') || '大纲'}
`; @@ -3245,7 +3312,7 @@ function buildOutline() { continue; closeTo(item.level); h += '