From 44a61443fa1866ff5e2a0d1832aac0d9f530a180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 05:15:42 -0400 Subject: [PATCH] ElyMark: tables compare by alignment and cell source, in quotes and lists too; rewritten cells stay raw (R2.13) --- agents.md | 2 +- apps/ely-mark/web/src/preserve.js | 12 +++++++----- apps/ely-mark/web/src/style.js | 3 ++- apps/ely-mark/web/src/tables.js | 30 ++++++++++++++++++++++++++++++ tests/scripts/mark-session.sh | 6 ++++++ 5 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 apps/ely-mark/web/src/tables.js diff --git a/agents.md b/agents.md index be37c5f..2d47cfa 100644 --- a/agents.md +++ b/agents.md @@ -265,7 +265,7 @@ 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 与 5caf5aa:`DiffViewer` 按路径的扩展名判定散文与纯文本(md、markdown、txt、rst、adoc、org、csv、tsv、log 等,无扩展名同,不着色),代码按语言的行注释记号着色(`#`:py、toml、sh、yaml 等与 Makefile;`--`:sql、lua;`;`:ini、lisp;`%`:tex、erl;其余 `//`),关键字表各语言共用(原先一律套 `//` 的通用词法,Markdown 的 `#`、Python 的注释都被当作代码上色)。同批带入上游的块编辑器跨块选择、浮层滚轮、输入法候选框位置、插入点按设备像素对齐与格式修正。 -- 2026-10-09:ElyMark 的表格(R2.13)。表格的空格与分隔行写法有许多种(`|a|b|`、`|---|`、对齐空格),逐字重现做不到,原先凡写不回原样的表格都成了原文块。现在表格按单元格比较:把原文这一段单独解析,与编辑器里的表格节点相同即为可视块(`preserve.js` 的 `kept`);未改的表格照旧逐字写回,改过的表格按文档多数表格的写法重写(`style.js` 投票:单元格两侧有无空格、是否按列对齐,经 remark-gfm 的 `tableCellPadding` 与 `tablePipeAlign`;原先投票的解析器不认 GFM,表格从未计票)。改过的表格分隔行按此写法重写(如 `|---|` 成 `| - |`)。 +- 2026-10-09:ElyMark 的表格(R2.13)。表格的空格与分隔行写法有许多种(`|a|b|`、`|---|`、对齐空格),逐字重现做不到,原先凡写不回原样的表格都成了原文块。现在一块的序列化与原文在把其中每张表格化成规范形式(各列对齐方式加各格的原文,`tables.js` 的 `canon`)后相同,即为可视块(`preserve.js` 的 `kept`),引用与列表里的表格同样;格内有 Milkdown 会改写的写法(`[[…]]`、引用式链接等)即不同,整块仍为原文块,与段落的规则一致。Milkdown 的列表项必须以段落开头,表格作列表项的第一块时无法放进编辑器,照原文块保留。未改的表格照旧逐字写回,改过的表格按文档多数表格的写法重写(`style.js` 按表格的行节点投票:单元格两侧有无空格、是否按列对齐,经 remark-gfm 的 `tableCellPadding` 与 `tablePipeAlign`;原先投票的解析器不认 GFM,表格从未计票),分隔行随之重写(如 `|---|` 成 `| - |`)。 - 2026-10-09:ElyTable 新插图表的位置(R2.10)。内核按“定位区域”把图表放在其旁的空处;带参数的请求(Ely 经 `FN_PARAM_5` 给区域)原把定位区域取为光标格,图表紧贴光标、压在数据上。补丁 `0041-lok-calc-chart-beside-its-data.patch`:定位区域取请求里的数据区域(多段取并集),同对话框路径取所选单元格。 - 2026-10-09:ElyPDF 编辑与缩放不闪白(R2.6)。旧图块不再随编辑或比例变化即刻丢弃:编辑落地时全部当前图块、比例变化时离开视图的同页图块,连同当时的页面尺寸进入“底图”(`apps/ely-pdf/src/backdrop.rs`),按其在页面中的比例位置画在该页新图块之下,直到该页在视口里的新图块到齐、或页面尺寸与旋转变了才释放;每页画什么由 `PdfView::pieces` 决定,绘制与测试同用它。原先比较整份尺寸表(超过 100 页的混合纸张文档尺寸表后段为占位,比较失败即全部清空)且只认同比例的图块(编辑后右栏收起、比例变化即清空),都会闪白。 - 2026-10-09:文档的缩放锚点(R2.7)。窄于视口的文档居中,缩放原按视口坐标求锚点、漏了居中的偏移,锚点随缩放漂移;现锚点先减去缩放前的偏移,缩放后按新的偏移(`LokView::slack`,绘制同用)换回。按键与菜单的缩放(无指针)另保持首页左缘在视口内;指针与捏合的缩放保持指针下的点不动(文档边界以外即止)。 diff --git a/apps/ely-mark/web/src/preserve.js b/apps/ely-mark/web/src/preserve.js index bb37cb2..10cbbd3 100644 --- a/apps/ely-mark/web/src/preserve.js +++ b/apps/ely-mark/web/src/preserve.js @@ -1,5 +1,6 @@ // Blocks Milkdown cannot write back stay raw text (§14.3). -import { editorViewCtx, parserCtx, remarkCtx, schemaCtx, serializerCtx } from '@milkdown/kit/core' +import { editorViewCtx, remarkCtx, schemaCtx, serializerCtx } from '@milkdown/kit/core' +import { canon } from './tables.js' import { Fragment } from '@milkdown/kit/prose/model' import { Selection } from '@milkdown/kit/prose/state' import { $nodeSchema } from '@milkdown/kit/utils' @@ -51,10 +52,11 @@ 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)) + // Table spacing is free; every cell's source must match. + const kept = (node, text) => { + const own = written([node]) + return own === text || canon(own) === canon(text) + } const rawOf = ([from, to]) => { const text = original.slice(from, to) return schema.nodes.ely_raw.create(null, text ? schema.text(text) : null) diff --git a/apps/ely-mark/web/src/style.js b/apps/ely-mark/web/src/style.js index d2cb9bf..3f4a3c3 100644 --- a/apps/ely-mark/web/src/style.js +++ b/apps/ely-mark/web/src/style.js @@ -36,7 +36,8 @@ export function style(text) { 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()) + // Row nodes leave out quote and list prefixes. + const rows = node.children.map((row) => text.slice(row.position.start.offset, row.position.end.offset).trim()) const padded = /\| /.test(rows[0]) const cells = rows.flatMap((row) => row.replace(/^\||\|$/g, '').split('|')) const inner = (cell) => (padded ? cell.replace(/^ | $/g, '') : cell) diff --git a/apps/ely-mark/web/src/tables.js b/apps/ely-mark/web/src/tables.js new file mode 100644 index 0000000..2693483 --- /dev/null +++ b/apps/ely-mark/web/src/tables.js @@ -0,0 +1,30 @@ +// Tables compare by alignment and cell source, not spacing. +import { unified } from 'unified' +import remarkParse from 'remark-parse' +import remarkGfm from 'remark-gfm' +import { SKIP, visit } from 'unist-util-visit' + +const reader = unified().use(remarkParse).use(remarkGfm) + +/** A cell's own source, without its padding. */ +function cell(text, node) { + const kids = node.children + return kids.length ? text.slice(kids[0].position.start.offset, kids.at(-1).position.end.offset) : '' +} + +/** `text` with each table's spacing made canonical. */ +export function canon(text) { + const tables = [] + visit(reader.parse(text), 'table', (node) => { + tables.push(node) + return SKIP + }) + let out = text + // Later tables first, so earlier offsets hold. + for (const table of tables.reverse()) { + const rows = table.children.map((row) => row.children.map((node) => cell(text, node)).join('|')) + const shape = [table.align.join(','), ...rows].join('\n') + out = `${out.slice(0, table.position.start.offset)}\u0000${shape}\u0000${out.slice(table.position.end.offset)}` + } + return out +} diff --git a/tests/scripts/mark-session.sh b/tests/scripts/mark-session.sh index 8bd943c..0b03f59 100755 --- a/tests/scripts/mark-session.sh +++ b/tests/scripts/mark-session.sh @@ -127,6 +127,12 @@ check defs.md $'[r]: https://example.com\n' 'keystroke "x"' $'x[r]: https://exam # 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' +check quoted.md $'> |alpha|beta|\n> |---|---|\n> |1|2|\n' 'keystroke "x"' $'> |xalpha|beta|\n> |-|-|\n> |1|2|\n' +check listed.md $'- item\n\n |alpha|beta|\n |---|---|\n |1|2|\n' 'keystroke "x"' $'- xitem\n\n |alpha|beta|\n |-|-|\n |1|2|\n' +# Items open with a paragraph; a leading table stays raw. +check leading.md $'- |alpha|beta|\n |---|---|\n' 'keystroke "x"' $'x- |alpha|beta|\n |---|---|\n' +# Cells Milkdown would rewrite keep the table raw. +check wikicell.md $'|a|[[Wiki Link]]|\n|-|-|\n|1|2|\n' 'keystroke "x"' $'x|a|[[Wiki Link]]|\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"