docs: sync all docs with the M1–M4 changes

- readme_zh: installation, editor-integration behavior, JIS kinsoku,
  HYPHENMIN, refreshed perf table (parity with English readme)
- DEVELOPER/DEVELOPER_ZH: C module 1.5 (15-arg API, first-line-width,
  lazy pool, clean failure fallback), first-line indent on the 1D/C
  path, file map includes ekp-region.el and the real tests/ layout
- correct dictionary attribution to LibreOffice (per dictionaries/
  update.sh and the bundled README_hyph_*.txt)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kinneyzhang 2026-07-27 02:01:44 +08:00
parent 03096facc5
commit 4ba97deec7
4 changed files with 117 additions and 66 deletions

View File

@ -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 47.
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 47.
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` (46 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` (47 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
```

View File

@ -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` 47 参。
C 模块 1.5:`ekp-c-break-with-arrays` 15 参(…、forbidden-positions
tail-protrudes、hyphen-protrude、first-line-width);batch 向量 15 元;
`ekp-c-set-penalties` 47 参。
特性完成后的性能(字节编译 + 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`(46 参数):`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`(47 参数):`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
```

View File

@ -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`)

View File

@ -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_<lang>.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 |
| 最优宽度搜索 340380 | 294 ms | 75 ms |
| 仅 DP,宽 400px | 15 ms | 1.3 ms |
| 两端对齐,宽 200px | 150 ms | 41 ms |
| 最优宽度搜索 340380 | 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`)