diff --git a/agents.md b/agents.md index cf59679..9fc4dec 100644 --- a/agents.md +++ b/agents.md @@ -265,3 +265,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-09(用户报:ElyPDF 改字报“这种字体画不出 V”):文档自带的子集字体常无可用的 Unicode 表(LibreOffice 导出的子集表为占位,0x00 起逐一对应字形号),按 Unicode 找字形一律失败。现以“文档自带字体”写字时直接用页面上的那个字体对象(`shim/ely_pen.c`;`pdf_load_font` 按对象缓存,与文字层的字体同一对象),以它自己的编码写:按文字提取的读法(ToUnicode,缺项或坏值退回 cid_to_ucs,同 pdf-op-run.c)倒出 Unicode 到编码的表,排版取它的宽度(`pdf_lookup_hmtx`),原有的 ToUnicode 与宽度不动,两个字共用一个字形时各写各的编码(原先按字形号写、重建 ToUnicode,共用字形的字被改写,且 CMap 漏写条目数,PDFKit 读不出字)。删去原文字块后页面资源里可能已无该字体,故按对象号以新名字重新引用。只支持编码即 CID 的字体(简单字体与 Identity-H),其余拒绝并说明;页面上没有的字仍报缺字。排版按同一套宽度量出的框,比较时留 0.01 点的余量(原块宽度即这些宽度之和,浮点误差曾使末字折行)。 - 2026-10-09:ElyTable 的自动筛选(R2.9)。内核会重新推算未命名的自动筛选区:每次打开筛选列表时向下扩到紧邻的数据行(`ExtendDataArea`、`ExtendBackColorArea`,为接上后来追加的行),命令从单格找到它时按连续数据区重算(`ScDocShell::GetDBData`:应用条件、关闭筛选及其撤销);选区下方紧邻的合计行因此进了列表、随数据一起被隐藏,存后重开的筛选区同样会扩。补丁 `0040-lok-calc-autofilter-keeps-its-rows.patch`:LOK 下已有的自动筛选区保持设定时的行(同 Excel),不再隐式扩展(光标在区内时;光标在同表另一块数据里即按那一块);单格开筛选时仍取整个数据区。代价:紧贴在筛选区下方追加的行不自动并入,需重新选定后再开筛选。 - 2026-10-09:ElyTree 的差异按文件类型着色(R2.12)。组件库 7f2ac3e:`DiffViewer` 按路径的扩展名判定散文与纯文本(md、markdown、txt、rst、adoc、org、csv、tsv、log 等,无扩展名同),这些文件不按代码着色(原先一律套通用的代码词法,Markdown 的 `#`、引号等被当作代码上色)。同批带入上游的块编辑器跨块选择、浮层滚轮、输入法候选框位置、插入点按设备像素对齐与格式修正。 +- 2026-10-09:ElyMark 的表格(R2.13)。表格的空格与分隔行写法有许多种(`|a|b|`、`|---|`、对齐空格),逐字重现做不到,原先凡写不回原样的表格都成了原文块。现在表格按单元格比较:把原文这一段单独解析,与编辑器里的表格节点相同即为可视块(`preserve.js` 的 `kept`);未改的表格照旧逐字写回,改过的表格按文档多数表格的写法重写(`style.js` 投票:单元格两侧有无空格、是否按列对齐,经 remark-gfm 的 `tableCellPadding` 与 `tablePipeAlign`;原先投票的解析器不认 GFM,表格从未计票)。改过的表格分隔行按此写法重写(如 `|---|` 成 `| - |`)。 diff --git a/apps/ely-mark/web/src/preserve.js b/apps/ely-mark/web/src/preserve.js index 21d4e11..bb37cb2 100644 --- a/apps/ely-mark/web/src/preserve.js +++ b/apps/ely-mark/web/src/preserve.js @@ -1,5 +1,5 @@ // Blocks Milkdown cannot write back stay raw text (§14.3). -import { editorViewCtx, remarkCtx, schemaCtx, serializerCtx } from '@milkdown/kit/core' +import { editorViewCtx, parserCtx, remarkCtx, schemaCtx, serializerCtx } from '@milkdown/kit/core' import { Fragment } from '@milkdown/kit/prose/model' import { Selection } from '@milkdown/kit/prose/state' import { $nodeSchema } from '@milkdown/kit/utils' @@ -51,6 +51,10 @@ export function faithful(ctx, original) { const schema = ctx.get(schemaCtx) const serializer = ctx.get(serializerCtx) const written = (nodes) => serializer(schema.topNodeType.create(null, nodes)).replace(/\n+$/, '') + const parser = ctx.get(parserCtx) + // A table's spacing is free; its cells must match. + const kept = (node, text) => + written([node]) === text || (node.type.name === 'table' && parser(text).firstChild?.eq(node)) const rawOf = ([from, to]) => { const text = original.slice(from, to) return schema.nodes.ely_raw.create(null, text ? schema.text(text) : null) @@ -68,8 +72,7 @@ export function faithful(ctx, original) { if (!blank) parsed.forEach((node, _, index) => { gap(last, at[index][0]) - const exact = written([node]) === original.slice(...at[index]) - nodes.push(exact ? node : rawOf(at[index])) + nodes.push(kept(node, original.slice(...at[index])) ? node : rawOf(at[index])) places.push(at[index]) last = at[index][1] }) diff --git a/apps/ely-mark/web/src/style.js b/apps/ely-mark/web/src/style.js index 914a85b..d2cb9bf 100644 --- a/apps/ely-mark/web/src/style.js +++ b/apps/ely-mark/web/src/style.js @@ -1,16 +1,17 @@ // The document's own markers, so blocks write back unchanged. import { unified } from 'unified' import remarkParse from 'remark-parse' +import remarkGfm from 'remark-gfm' import { visit } from 'unist-util-visit' -/** remark-stringify options most of `text` already uses. */ +/** Writing options most of `text` already uses; tables' for remark-gfm. */ export function style(text) { const votes = {} const vote = (key, value) => { votes[key] ??= new Map() votes[key].set(value, (votes[key].get(value) ?? 0) + 1) } - visit(unified().use(remarkParse).parse(text), (node) => { + visit(unified().use(remarkParse).use(remarkGfm).parse(text), (node) => { const at = node.position?.start.offset if (at === undefined) return const head = text.slice(at, at + 12) @@ -34,6 +35,16 @@ export function style(text) { case 'heading': if (node.depth <= 2) vote('setext', head[0] !== '#') break + case 'table': { + const rows = text.slice(at, node.position.end.offset).split('\n').map((row) => row.trim()) + const padded = /\| /.test(rows[0]) + const cells = rows.flatMap((row) => row.replace(/^\||\|$/g, '').split('|')) + const inner = (cell) => (padded ? cell.replace(/^ | $/g, '') : cell) + vote('tableCellPadding', padded) + // Aligned cells carry spaces beyond the padding. + vote('tablePipeAlign', cells.some((cell) => /^\s|\s$/.test(inner(cell)))) + break + } } }) const most = (key) => [...votes[key].entries()].sort((a, b) => b[1] - a[1])[0][0] diff --git a/apps/ely-mark/web/src/visual.js b/apps/ely-mark/web/src/visual.js index ec08f40..4959a48 100644 --- a/apps/ely-mark/web/src/visual.js +++ b/apps/ely-mark/web/src/visual.js @@ -19,7 +19,7 @@ import { keymap } from '@milkdown/kit/prose/keymap' import { liftListItem, sinkListItem } from '@milkdown/kit/prose/schema-list' import { redo, redoDepth, undoDepth } from '@milkdown/kit/prose/history' import { $prose, callCommand } from '@milkdown/kit/utils' -import { insertTableCommand } from '@milkdown/kit/preset/gfm' +import { insertTableCommand, remarkGFMPlugin } from '@milkdown/kit/preset/gfm' import { addColumnAfter, addColumnBefore, @@ -87,7 +87,9 @@ export async function visual(root, markdown, editable, texts, changed, edge) { .config((ctx) => { ctx.set(rootCtx, root) ctx.set(defaultValueCtx, markdown) - ctx.update(remarkStringifyOptionsCtx, (prev) => ({ ...prev, ...style(markdown) })) + const { tableCellPadding, tablePipeAlign, ...marks } = style(markdown) + ctx.update(remarkStringifyOptionsCtx, (prev) => ({ ...prev, ...marks })) + ctx.set(remarkGFMPlugin.options.key, { tableCellPadding, tablePipeAlign }) ctx.update(editorViewOptionsCtx, (prev) => ({ ...prev, editable: () => editable })) }) .use(commonmark) diff --git a/tests/scripts/mark-session.sh b/tests/scripts/mark-session.sh index 57fdb42..8bd943c 100755 --- a/tests/scripts/mark-session.sh +++ b/tests/scripts/mark-session.sh @@ -88,6 +88,10 @@ Setext html +|a|b| +|-|-| +|1|2| + See [[Wiki Link]], ~sub~ and [a ref][r]. next line @@ -120,6 +124,9 @@ check wiki.md $'Alpha with [[Wiki Link]].\n' 'keystroke "x"' $'xAlpha with [[Wik quote=$'> First.\n>\n> Setext\n> ======\n>\n> Last with [[Wiki]].\n' check quote.md "$quote" 'keystroke "x"' "x$quote" check defs.md $'[r]: https://example.com\n' 'keystroke "x"' $'x[r]: https://example.com\n' +# A compact table stays a table; edits keep its style. +check table.md $'|a|b|\n|-|-|\n|1|2|\n' 'keystroke "x"' $'|xa|b|\n|-|-|\n|1|2|\n' +check dashes.md $'| a | b |\n|---|---|\n| 1 | 2 |\n' 'keystroke "x"' $'| xa | b |\n| - | - |\n| 1 | 2 |\n' # Deleting forward at a hidden definition moves nothing. gap=$'Alpha.\n\n[r]: /path " T " \n\nBeta.\n\nTail.\n' check gap.md "$gap" 'key code 119' "$gap"