diff --git a/DEVELOPER.md b/DEVELOPER.md index c2ea702..5c401e7 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -185,13 +185,18 @@ after the justified text was edited — and builds (`lw = width + release`) in the DP, in `ekp-line-glues', and in the C-result reconstruction — all three must stay in lockstep. - **Per-line widths** (`ekp-parshape' / `ekp-first-line-indent'): - resolved by `ekp--line-spec' (line-index → INDENT . WIDTH); they - require the (position × line-count) DP and bypass C, like - looseness. Indents render as leading `ekp-glue' spacers. + resolved by `ekp--line-spec' (line-index → INDENT . WIDTH). A + plain first-line indent only changes line 0, and a line begins at + box 0 exactly when the DP start i = 0, so the 1D pass (and the C + engine, via `FIRST-LINE-WIDTH') handle it with no extra state. + Only full `ekp-parshape' and `ekp-looseness' need the + (position × line-count) DP and bypass C. Indents render as leading + `ekp-glue' spacers. -C module 1.4: `ekp-c-break-with-arrays` takes 14 args -(…, forbidden-positions, tail-protrudes, hyphen-protrude); batch -vectors have 14 elements; `ekp-c-set-penalties` takes 4–7. +C module 1.5: `ekp-c-break-with-arrays' takes 15 args +(…, forbidden-positions, tail-protrudes, hyphen-protrude, +first-line-width); batch vectors have 15 elements; +`ekp-c-set-penalties' takes 4–7. Performance after the feature wave (byte-compiled + C, Apple Silicon, batch): justify zh w=200 ≈ 54 ms, range zh ≈ 117 ms — @@ -214,29 +219,32 @@ engines never disagree. ## 7. C Module Integration -The C module (`ekp_c/`, version 1.1) runs only stage ④. Elisp remains +The C module (`ekp_c/`, version 1.5) runs only stage ④. Elisp remains the source of truth for all font-dependent data. -- `ekp-c-break-with-arrays` (11 args): the para's prefix arrays, glue - arrays, hyphen data, line width and the two space-run arrays. - Returns `(breaks . cost)`. -- `ekp-c-break-batch`: a vector of 11-element vectors, processed in +- `ekp-c-break-with-arrays` (15 args): the para's prefix arrays, glue + arrays, hyphen data, line width, the two space-run arrays, the + forbidden/protrusion arrays and the first-line width. Returns + `(breaks . cost)`. +- `ekp-c-break-batch`: a vector of 15-element vectors, processed in parallel by a pthread pool — one task per paragraph (that is the - correct granularity; the DP itself is sequential by nature). -- `ekp-c-set-penalties` (4–6 args): called by `ekp--c-sync-params` + correct granularity; the DP itself is sequential by nature). The + pool is created lazily on the first multi-paragraph batch and sized + to the machine's cores; a full queue blocks the submitter rather + than dropping the task. +- `ekp-c-set-penalties` (4–7 args): called by `ekp--c-sync-params` before *every* C entry, so `ekp-line-penalty` & friends always take effect (regression: they were never synced before). - `ekp-c-module-load` refuses modules older than `ekp-c-module-required-version` and falls back to Elisp, preventing arity mismatches after upgrades. -Any C failure (NULL result) silently falls back to the Elisp engine. -The two engines are verified to produce byte-identical output by -`ekp-test-c-parity-simple` / `ekp-test-c-parity-files`. - -`ekp-c-break-lines` (C-side tokenization via `ekp_paragraph.c` and -`ekp_hyphen.c`) is an experimental, self-contained path that ekp.el -does not use; see `ekp_c/README.md`. +Any C failure — a NULL result, an allocation failure, or a bad +argument — falls back to the Elisp engine; the Elisp bridge also +wraps the calls in `condition-case`. The module never silently +produces a different layout on partial failure. The two engines are +verified byte-identical by `ekp-test-c-parity-simple` / +`ekp-test-c-parity-files` and the 300-case property fuzz. ## 8. Hyphenation (ekp-hyphen.el) @@ -300,8 +308,12 @@ ekp.el Core: para struct, caching, DP (1D + looseness), ekp-utils.el Tokenizer (boxes, kinsoku), font detection with batch/tty fallbacks, C module loading ekp-hyphen.el Liang hyphenation + dictionary registry +ekp-region.el Buffer/region commands, ekp-auto-justify-mode, and + editor integration (save, isearch, kill-ring, undo) ekp_c/ C dynamic module (see ekp_c/README.md) -dictionaries/ Hunspell hyphenation patterns (from Pyphen) -tests/ ekp-tests.el (ERT), ekp-bench.el, ekp-demo.el, - sample texts, run-tests.sh +dictionaries/ Hunspell hyphenation patterns (from LibreOffice) +tests/ ekp-tests.el, ekp-region-tests.el (ERT), + ekp-fuzz.el (parity fuzz), ekp-bench.el, + ekp-demo.el, ekp-showcase.el, sample texts, + run-tests.sh ``` diff --git a/DEVELOPER_ZH.md b/DEVELOPER_ZH.md index 45206b5..eeff7bb 100644 --- a/DEVELOPER_ZH.md +++ b/DEVELOPER_ZH.md @@ -163,12 +163,15 @@ batch/tty 下按字符列精确。 `ekp-line-glues`、C 结果重建三处同步放宽每个候选的有效目标宽 (`lw = width + release`)——三处必须保持一致。 - **每行宽度**(`ekp-parshape` / `ekp-first-line-indent`):由 - `ekp--line-spec`(行号 → 缩进 . 宽度)解析;需要(位置×行数)DP, - 与 looseness 一样旁路 C。缩进渲染为行首 `ekp-glue` 垫片。 + `ekp--line-spec`(行号 → 缩进 . 宽度)解析。纯首行缩进只改第 0 行, + 而"以盒 0 开头的行"恰好对应 DP 起点 i = 0,故 1D 遍(以及 C 引擎, + 经 `FIRST-LINE-WIDTH` 参数)无需额外状态即可处理;只有完整的 + `ekp-parshape` 和 `ekp-looseness` 才需要(位置×行数)DP 并旁路 C。 + 缩进渲染为行首 `ekp-glue` 垫片。 -C 模块 1.4:`ekp-c-break-with-arrays` 14 参(…、 -forbidden-positions、tail-protrudes、hyphen-protrude);batch 向量 -14 元;`ekp-c-set-penalties` 4–7 参。 +C 模块 1.5:`ekp-c-break-with-arrays` 15 参(…、forbidden-positions、 +tail-protrudes、hyphen-protrude、first-line-width);batch 向量 15 元; +`ekp-c-set-penalties` 4–7 参。 特性完成后的性能(字节编译 + C,Apple Silicon,batch):justify zh w=200 ≈ 54 ms、range zh ≈ 117 ms——justify 与特性前持平,range 因盒 @@ -187,24 +190,27 @@ w=200 ≈ 54 ms、range zh ≈ 117 ms——justify 与特性前持平,range 因 ## 7. C 模块集成 -C 模块(`ekp_c/`,版本 1.1)只执行阶段 ④。所有字体相关数据以 Elisp +C 模块(`ekp_c/`,版本 1.5)只执行阶段 ④。所有字体相关数据以 Elisp 为唯一事实来源。 -- `ekp-c-break-with-arrays`(11 参数):para 的前缀数组、glue 数组、 - 断词数据、行宽和两个空格串数组。返回 `(breaks . cost)`。 -- `ekp-c-break-batch`:11 元素向量的向量,由 pthread 线程池并行处理 - ——每段一个任务(这是正确的并行粒度;DP 本身天然串行)。 -- `ekp-c-set-penalties`(4–6 参数):`ekp--c-sync-params` 在**每次** +- `ekp-c-break-with-arrays`(15 参数):para 的前缀数组、glue 数组、 + 断词数据、行宽、两个空格串数组、禁则/悬挂数组和首行宽度。返回 + `(breaks . cost)`。 +- `ekp-c-break-batch`:15 元素向量的向量,由 pthread 线程池并行处理 + ——每段一个任务(这是正确的并行粒度;DP 本身天然串行)。线程池在 + 首次多段落 batch 时惰性创建,按机器核心数定大小;队列满时提交方 + 阻塞等待而非丢弃任务。 +- `ekp-c-set-penalties`(4–7 参数):`ekp--c-sync-params` 在**每次** 进入 C 之前调用,保证 `ekp-line-penalty` 等变量始终生效(回归:此 前从未同步)。 - `ekp-c-module-load` 拒绝低于 `ekp-c-module-required-version` 的模块 并回落到 Elisp,避免升级后的参数数量不匹配。 -C 端任何失败(返回 NULL)都会静默回落到 Elisp 引擎。两引擎输出逐字 -节一致,由 `ekp-test-c-parity-simple` / `ekp-test-c-parity-files` 验证。 - -`ekp-c-break-lines`(经 `ekp_paragraph.c`、`ekp_hyphen.c` 的 C 端自行 -分词路径)是实验性的独立路径,ekp.el 不使用;见 `ekp_c/README.md`。 +C 端任何失败——NULL 结果、分配失败或非法参数——都回落到 Elisp 引擎 +(Elisp 桥接层也用 `condition-case` 兜住);模块不会在部分失败时静默 +产出不同的排版。两引擎输出逐字节一致,由 +`ekp-test-c-parity-simple` / `ekp-test-c-parity-files` 及 300 例性质 +fuzz 验证。 ## 8. 断词(ekp-hyphen.el) @@ -261,8 +267,11 @@ ekp.el 核心:para 结构、缓存、DP(1D + looseness)、 ekp-utils.el 分词器(盒子、避头尾)、带 batch/tty 回退的字体 检测、C 模块加载 ekp-hyphen.el Liang 断词 + 词典注册 +ekp-region.el buffer/region 命令、ekp-auto-justify-mode,以及 + 编辑器集成(保存、isearch、kill-ring、undo) ekp_c/ C 动态模块(见 ekp_c/README.md) -dictionaries/ Hunspell 断词模式(来自 Pyphen) -tests/ ekp-tests.el(ERT)、ekp-bench.el、ekp-demo.el、 - 示例文本、run-tests.sh +dictionaries/ Hunspell 断词模式(来自 LibreOffice) +tests/ ekp-tests.el、ekp-region-tests.el(ERT)、 + ekp-fuzz.el(一致性 fuzz)、ekp-bench.el、 + ekp-demo.el、ekp-showcase.el、示例文本、run-tests.sh ``` diff --git a/readme.md b/readme.md index f731d2e..bd40567 100644 --- a/readme.md +++ b/readme.md @@ -298,4 +298,4 @@ tests/run-tests.sh /path/to/emacs # 67 ERT tests, all batch-safe - **Core algorithm**: ["Breaking Paragraphs into Lines"](https://gwern.net/doc/design/typography/tex/1981-knuth.pdf) by Donald E. Knuth and Michael F. Plass (1981) - **Hyphenation**: Frank Liang's algorithm, adapted from [Pyphen](https://github.com/Kozea/Pyphen) -- **Dictionaries**: [Hunspell hyphenation patterns](https://github.com/Kozea/Pyphen) +- **Dictionaries**: hyphenation patterns from the [LibreOffice dictionaries](https://github.com/LibreOffice/dictionaries) (GPL/LGPL/MPL; see each `dictionaries/README_hyph_*.txt`) diff --git a/readme_zh.md b/readme_zh.md index 00dfcfe..b6d2c2a 100644 --- a/readme_zh.md +++ b/readme_zh.md @@ -28,11 +28,22 @@ Emacs-kp 在 Emacs 内部完整实现了 Knuth-Plass 最优断行算法,支持 - Emacs **29.1+**(依赖 `string-pixel-width` 与 `object-intervals`) - 可选(C 模块):C11 编译器和 pthreads -## 快速开始 +## 安装 + +克隆仓库并加入 `load-path`(`dictionaries/` 目录须与 `.el` 文件同级): ```elisp (add-to-list 'load-path "/path/to/emacs-kp") (require 'ekp) +(require 'ekp-region) ; buffer/region 命令 +``` + +强烈建议字节编译——编译后 Elisp 引擎约快 10 倍。 + +## 快速开始 + +```elisp +(require 'ekp) ;; 按 600 像素宽度两端对齐 (insert (ekp-pixel-justify "这是一段测试文本..." 600)) @@ -66,32 +77,44 @@ Elisp 与 C 两个引擎的输出**完全一致**;Elisp 是永远可用的后备 ``` - `M-x ekp-justify-region` — 把选区排版到窗口文本宽度(数字前缀参数 - 可指定像素宽)。 -- `M-x ekp-unjustify-region` — **精确**还原原文,包括被折叠的连续空 - 格。排版是无损的:每个合成空隙、软换行、软连字符都携带它所替换的 - 原文,还原是纯结构变换,即使排版后又编辑过也能正确还原。 + 可指定像素宽)。没有激活选区时,排版光标所在段落。 +- `M-x ekp-justify-buffer` — 排版整个 buffer。 +- `M-x ekp-unjustify-region` / `ekp-unjustify-buffer` — **精确**还原 + 原文,包括被折叠的连续空格。排版是无损的:每个合成空隙、软换行、 + 软连字符都携带它所替换的原文,还原是纯结构变换,即使排版后又编辑 + 过也能正确还原。 - `M-x ekp-auto-justify-mode` — 让整个 buffer 保持按窗口宽度排版。 窗口宽度变化时自动重排(防抖延迟 `ekp-auto-justify-resize-delay`); 编辑后只重排被改动的段落(空闲延迟 `ekp-auto-justify-edit-delay`), 未变段落直接命中段落缓存。关闭 mode 时 buffer 精确恢复原状。 +buffer 被当作活的文档,而不只是画布: + +- **保存**时写入的是**逻辑文本**——软换行、glue 空格、断词连字符属于 + 排版而非内容,不会落盘;屏幕上的 buffer 仍保持排版态。 +- **搜索**(isearch)看到的是逻辑文本,中文短语与被断词的英文单词 + 都能跨排版找到。 +- **复制**放进 kill ring 的是逻辑文本,粘贴出去的是文字而非像素间距。 +- 仅仅开启 mode 不会把 buffer 标记为已修改(不产生锁文件或 auto-save), + 重排定时器也不再与 `undo` 打架。 + `ekp-region-margin-pixel`(默认 2)是从窗口宽度中扣除的取整安全边距。 大 buffer(超过 `ekp-auto-justify-lazy-threshold` 字符,默认 2 万) -自动改为可视优先重排:屏幕内的部分同步完成(约 15ms),其余在空闲 -时后台分块补齐。 +自动改为可视优先重排:屏幕内的部分同步完成,其余在空闲时后台分块 +补齐,每个时间片有时间预算(`ekp-auto-justify-tick-budget`),并优先 +处理你滚动到的区域。 -各 mode 的 verbatim 保护预设: +各 mode 的 verbatim 保护预设——各一行: ```elisp -(add-hook 'org-mode-hook - (lambda () - (setq-local ekp-region-skip-faces ekp-region-org-skip-faces))) -(add-hook 'markdown-mode-hook - (lambda () - (setq-local ekp-region-skip-faces ekp-region-markdown-skip-faces))) +(add-hook 'org-mode-hook #'ekp-org-setup) +(add-hook 'markdown-mode-hook #'ekp-markdown-setup) ``` +在 Org 与 Markdown buffer 里,若你没有自定义配置, +`ekp-auto-justify-mode` 会自动套用对应预设。 + ### 保护代码块与 verbatim 文本 - 段落级:携带 `ekp-verbatim` 文本属性(`M-x ekp-verbatim-region`)、 @@ -113,11 +136,14 @@ Elisp 与 C 两个引擎的输出**完全一致**;Elisp 是永远可用的后备 `ekp-auto-justify-mode` 自动预留悬挂宽度。 - **段落形状** — `ekp-first-line-indent`(`t` = 2 em)实现中文段首 缩进惯例;或用 TeX 式 `ekp-parshape` 逐行指定 `(缩进 . 宽度)`。 - 两者走 Elisp 2D 路径(C 模块自动旁路,同 `ekp-looseness`)。 + 首行缩进走高速的 1D 路径与 C 引擎;只有完整的 `ekp-parshape` 和 + `ekp-looseness` 才回落到 Elisp 专属的 2D 动态规划。 - **不可断字符** — NBSP、窄 NBSP、数字空格、WORD JOINER 天然把两侧 锁在同一行。 - 禁则覆盖全角**与半角**标点:行首不会出现 `。、」!?` 或独立的 - `.,;:!?`,行尾不会出现 `「(` 等。 + `.,;:!?`,行尾不会出现 `「(` 等。日文行首禁则还覆盖小假名、长音 + 符和叠字符(`っ ょ ー 々`),可通过 `ekp-cjk-no-line-start-extra` + 配置。 已知限制:行中的 CLREQ 标点**压缩**(如「字。下」行内挤压)无法渲 染——Emacs 不能缩减字形 advance——因此行边压缩以悬挂方式呈现;左缘 @@ -132,7 +158,9 @@ Elisp 与 C 两个引擎的输出**完全一致**;Elisp 是永远可用的后备 ``` `dictionaries/hyph_.dic` 中的任意语言均可;`"de"` 这类短代码会解 -析到第一个匹配的词典。 +析到第一个匹配的词典。每个词典自身的 `LEFTHYPHENMIN` / +`RIGHTHYPHENMIN` 都会被遵守(英文在断点前保留 ≥2 字母、之后 ≥3); +给 `ekp-hyphen-create` 传显式边距可覆盖。 ### 间距参数 @@ -173,7 +201,8 @@ Elisp 与 C 两个引擎的输出**完全一致**;Elisp 是永远可用的后备 ### 缓存 -分词、测宽和 DP 结果按段落缓存。 +分词、测宽和 DP 结果按段落缓存;盒宽还额外做会话级缓存,跨段落共享 +的字形整个会话只测量一次。 - `ekp-para-cache-limit`(默认 256):缓存段落数上限,超过后整体清空。 - `M-x ekp-clear-caches` 清空所有缓存(更换字体或影响字宽的主题后使用)。 @@ -185,12 +214,13 @@ Silicon 测得;方法见 DEVELOPER_ZH.md: | 场景(text-zh.txt ≈ 3.6KB) | Elisp(字节编译) | C 模块 | |:----------------------------|------------------:|-------:| -| 两端对齐,宽 200px | 96 ms | 57 ms | -| 最优宽度搜索 340–380 | 294 ms | 75 ms | -| 仅 DP,宽 400px | 15 ms | 1.3 ms | +| 两端对齐,宽 200px | 150 ms | 41 ms | +| 最优宽度搜索 340–380 | 529 ms | 106 ms | +| 仅 DP,宽 400px | 30 ms | 2.5 ms | **请字节编译本包**——编译后 Elisp 引擎快约 10 倍。两引擎输出完全一 -致;C 模块在最优宽度搜索和长多段文本上收益最大。 +致;C 模块在最优宽度搜索和长多段文本上收益最大。(绝对数值随机器与 +功耗状态波动,重点看比例。) ## 已知限制 @@ -224,4 +254,4 @@ tests/run-tests.sh /path/to/emacs # 67 个 ERT 测试,全部支持 batch - **核心算法**: ["Breaking Paragraphs into Lines"](https://gwern.net/doc/design/typography/tex/1981-knuth.pdf) by Donald E. Knuth and Michael F. Plass (1981) - **断词算法**: Frank Liang 算法,改编自 [Pyphen](https://github.com/Kozea/Pyphen) -- **词典**: [Hunspell 断词模式](https://github.com/Kozea/Pyphen) +- **词典**: 断词模式来自 [LibreOffice dictionaries](https://github.com/LibreOffice/dictionaries)(GPL/LGPL/MPL;各语言许可见 `dictionaries/README_hyph_*.txt`)