Go to file
Kinneyzhang fd1210e557 fix: close repository audit findings
Complete task002 through task016 across cache correctness, editor lifecycle, C boundaries, build and release governance, dictionary provenance, performance, interactive workflows, tests, documentation, and final cleanup.
2026-07-28 22:43:10 +08:00
.github/workflows fix: close repository audit findings 2026-07-28 22:43:10 +08:00
.phrase fix: close repository audit findings 2026-07-28 22:43:10 +08:00
dictionaries fix: close repository audit findings 2026-07-28 22:43:10 +08:00
Docs fix: close repository audit findings 2026-07-28 22:43:10 +08:00
ekp_c fix: close repository audit findings 2026-07-28 22:43:10 +08:00
postmortem fix: close repository audit findings 2026-07-28 22:43:10 +08:00
tests fix: close repository audit findings 2026-07-28 22:43:10 +08:00
.gitignore fix: close repository audit findings 2026-07-28 22:43:10 +08:00
CHANGELOG.md fix: close repository audit findings 2026-07-28 22:43:10 +08:00
CONTRIBUTING.md fix: close repository audit findings 2026-07-28 22:43:10 +08:00
COPYING chore: licensing, customize support, and packaging hygiene 2026-07-27 00:52:37 +08:00
DEVELOPER_ZH.md fix: close repository audit findings 2026-07-28 22:43:10 +08:00
DEVELOPER.md fix: close repository audit findings 2026-07-28 22:43:10 +08:00
ekp-hyphen.el fix: close repository audit findings 2026-07-28 22:43:10 +08:00
ekp-region.el fix: close repository audit findings 2026-07-28 22:43:10 +08:00
ekp-utils.el fix: close repository audit findings 2026-07-28 22:43:10 +08:00
ekp.el fix: close repository audit findings 2026-07-28 22:43:10 +08:00
readme_zh.md fix: close repository audit findings 2026-07-28 22:43:10 +08:00
readme.md fix: close repository audit findings 2026-07-28 22:43:10 +08:00

Emacs-KP: Knuth-Plass Line Breaking for Emacs

中文文档 | Developer Guide | Repository Audit

Emacs-kp implements the Knuth-Plass optimal line breaking algorithm with full support for CJK (Chinese, Japanese, Korean) and Latin mixed text typesetting, entirely inside Emacs.

Features

  • Optimal line breaking — the Knuth-Plass dynamic program finds the globally optimal set of breaks for a paragraph, not greedy first-fit.
  • CJK support — every CJK character is a breakable box; kinsoku rules keep punctuation attached (,。 never start a line, 「《 never end one); dedicated inter-CJK and CJK↔Latin spacing.
  • Hyphenation — Frank Liang's algorithm (the TeX algorithm) with 49 checksum-pinned Hunspell pattern dictionaries bundled.
  • Pixel-accurate justification — every justified line renders at exactly the requested pixel width, using display (space :width ...) properties; works with variable-width fonts.
  • Text properties preserved — faces, colors and other properties survive justification; inserted hyphens inherit the face of the word they break.
  • Robust on hard input — unbreakable overlong tokens (URLs, long words at narrow widths) degrade to emergency breaks instead of losing text; every input produces output.
  • Optional C module — a dynamic module runs the DP in C with a thread pool that processes paragraphs in parallel (see benchmarks).

Requirements

  • Emacs 29.1+ (uses string-pixel-width and object-intervals)
  • Optional, for the C module: a C11 compiler and pthreads

Installation

Clone the repository and add it to your load-path (the dictionaries/ directory must sit next to the .el files):

(add-to-list 'load-path "/path/to/emacs-kp")
(require 'ekp)
(require 'ekp-region)   ; buffer/region commands

Byte-compiling is strongly recommended — the Elisp engine is about 10× faster compiled.

Quick Start

(require 'ekp)

;; Justify a paragraph to 600 pixels
(insert (ekp-pixel-justify "Your paragraph text here..." 600))

;; Find the best width in a range; returns (justified-text . width)
(ekp-pixel-range-justify "Your text" 400 800)

Multiline strings are treated as one paragraph per line; blank lines are preserved.

cd ekp_c && make PROFILE=portable # default; produces ekp.dylib/.so/.dll
(ekp-c-module-load)     ; prints "ekp-c module loaded (version 1.6, N threads)"
(ekp-c-module-build)    ; prompts for portable/native/debug/sanitize

Once loaded (and since ekp-use-c-module defaults to t), all justification calls automatically use the C engine. The Elisp and C engines produce identical output; Elisp is the always-available path when no module is enabled or C returns no result. An enabled module signal is surfaced as a backend contract failure. If the module on disk is older than the Elisp code expects, loading refuses with a message asking you to rebuild.

Interactive Use (buffer & region)

ekp-region.el turns the string API into buffer-level commands:

(require 'ekp-region)
  • M-x ekp-justify-region — justify the region to the window text width (with a numeric prefix argument, to that many pixels). With no active region, it justifies the paragraph at point.
  • M-x ekp-justify-buffer — justify the whole buffer.
  • M-x ekp-unjustify-region / ekp-unjustify-buffer — restore the original text exactly, including collapsed whitespace runs. Justification is lossless: every synthesized space, soft line break, and soft hyphen carries the original text it replaced, so restoring is a structural transform that also works after you edited the justified text.
  • M-x ekp-auto-justify-mode — keep the whole buffer justified to the window width. Re-flows (debounced by ekp-auto-justify-resize-delay) when the window width changes, and after edits re-justifies only the touched paragraphs (ekp-auto-justify-edit-delay), so unchanged paragraphs hit the paragraph cache. Turning the mode off restores the buffer exactly. While active, the standard EKP menu exposes formatting, protection, and window-fit diagnostic commands; C-h m describes the same workflow.

The buffer is treated as a live document, not just a canvas:

  • Saving writes the logical text — soft line breaks, glue spaces and break hyphens never reach disk; the on-screen buffer stays justified even if writing fails or is interrupted. This guarantee applies to whole-buffer saves; an explicit region-only write-region writes the selected physical buffer representation.
  • Searching (isearch) sees the logical text, so CJK phrases and hyphenated words are found across the layout.
  • Copying puts the logical text on the kill ring, so pasted text carries words, not pixel spacing. Existing mode/user substring filters remain active and are restored when the final layout span is removed.
  • Merely enabling the mode never marks the buffer modified (no stray lock files or auto-saves), and undo is not fought by the re-flow timer.

ekp-region-margin-pixel (default 2) is subtracted from the window width as a rounding safety margin.

Large buffers (over ekp-auto-justify-lazy-threshold characters, default 20 000) re-flow visible-first: the portion on screen updates synchronously and the rest follows in idle background chunks, with a per-tick time budget (ekp-auto-justify-tick-budget) and priority for whatever you scroll to.

Mode presets for verbatim protection — one call each:

(add-hook 'org-mode-hook      #'ekp-org-setup)
(add-hook 'markdown-mode-hook #'ekp-markdown-setup)

ekp-auto-justify-mode also applies the matching preset automatically in Org and Markdown buffers when you have not configured your own.

Protecting code and other verbatim text

  • Block level: paragraphs carrying the ekp-verbatim text property (M-x ekp-verbatim-region), wearing a face listed in ekp-region-skip-faces (e.g. org-block, markdown-code-face), or matched by the buffer-local function ekp-region-skip-predicate pass through completely untouched.
  • Inline level: spans carrying ekp-no-break (M-x ekp-no-break-region) become rigid atoms — never broken, never hyphenated, spacing kept literal — ideal for inline code, product names, or numbers with units.

Manual properties are deliberately current-buffer-session only: plain-text saving and reopening do not persist them. Use M-x ekp-allow-break-region / ekp-clear-verbatim-region to remove them. For protection derived from persistent document syntax, use mode faces or the buffer-local ekp-region-skip-predicate (the Org/Markdown presets do this automatically).

Typography

  • Alignmentekp-alignment: justify (default), ragged-right, ragged-left, or center. Non-justify modes keep word spacing natural while Knuth-Plass still minimizes raggedness within ekp-ragged-stretch-pixel (≈2 em) per line.
  • Hanging punctuation — set ekp-protrusion to t and line-final punctuation (。、」 as well as periods, commas and break hyphens) hangs past the flush edge by ekp-protrusion-ratios. The 0.5 default for fullwidth closers is visually equivalent to CLREQ line-end punctuation compression. ekp-auto-justify-mode reserves the protrusion width automatically.
  • Paragraph shapesekp-first-line-indent (t = 2 em) for the CJK paragraph convention, or full TeX-style ekp-parshape with per-line (INDENT . WIDTH). First-line indent runs on the fast 1D path and the C engine; only full ekp-parshape and ekp-looseness fall back to the Elisp-only 2D dynamic program.
  • Unbreakables — NO-BREAK SPACE, NARROW NBSP, FIGURE SPACE and WORD JOINER characters keep their neighbors together out of the box.
  • Kinsoku covers full- and halfwidth punctuation: a line never starts with 。、」!? or a lone .,;:!?, never ends with 「( etc. Japanese line-start prohibition also covers small kana, the prolonged sound mark and iteration marks (っ ょ ー 々), configurable via ekp-cjk-no-line-start-extra.

Limitations worth knowing: mid-line CLREQ punctuation compression (e.g. 「字。下」 squeezed inside a line) cannot be rendered — Emacs cannot shrink a glyph's advance — which is why line-edge compression is delivered via protrusion instead; left-edge protrusion is likewise not renderable (text cannot start before the line origin).

Configuration

Hyphenation language

(setq ekp-latin-lang "de_DE")   ; default "en_US"

Short codes like "de" resolve to the first matching dictionary. Each supported dictionary's own LEFTHYPHENMIN / RIGHTHYPHENMIN are honored (English keeps ≥2 letters before and ≥3 after a break); pass explicit margins to ekp-hyphen-create to override.

EKP supports ordinary Liang patterns. It fails closed with ekp-hyphen-unsupported-pattern for eo, ca, hu_HU, and sq_AL because those files contain slash/replacement rules that conditionally rewrite glyphs at a chosen break. Treating them as ordinary positions would produce linguistically wrong text and incorrect DP widths. The exact inventory, SHA-256 checksums, pinned source paths, and license evidence live in dictionaries/MANIFEST.tsv and dictionaries/LICENSES.md.

Spacing parameters

Three glue classes control spacing (all values in pixels):

Group Between
lws-* two Latin words
mws-* a Latin word and a CJK char
cws-* two CJK characters

Each class has an ideal width, a maximum stretch and a maximum shrink:

(ekp-param-set lws-ideal lws-stretch lws-shrink
               mws-ideal mws-stretch mws-shrink
               cws-ideal cws-stretch cws-shrink)
;; e.g. (ekp-param-set 7 3 2  5 2 1  0 2 0)
  • If you never call ekp-param-set, defaults are derived automatically from the font of each string.
  • Explicit parameters persist until you call ekp-param-reset, which returns to automatic per-string defaults.

Algorithm parameters

Variable Default Meaning
ekp-line-penalty 10 Base cost per line; higher prefers fewer lines
ekp-hyphen-penalty 50 Cost of a hyphenated break (added as penalty²)
ekp-adjacent-fitness-penalty 100 Cost when adjacent lines differ in tightness by >1 class
ekp-consecutive-hyphen-penalty 100 Multiplier for runs of hyphenated lines (× count²)
ekp-last-line-min-ratio 0.5 Minimum fill ratio for the last line
ekp-last-line-short-penalty 50 Cost multiplier for a too-short last line
ekp-looseness 0 Target line count offset: +1 = one line more than optimal, 1 = one fewer

Both engines implement these parameters: before an actual C computation, the Elisp side synchronizes their current values. The DP cache signature includes every parameter in the table, so changes take effect on the next call without manually clearing caches. ekp-looseness is part of the cache key and uses a dedicated Elisp path (the C module is bypassed automatically while it is non-zero).

Caching

Tokenization, measurement, and DP results are cached per paragraph; box widths are additionally cached session-wide, so a glyph shared across paragraphs is measured only once. Both explicit spacing values and the automatic ekp-default-cws-stretch-pixel input participate in paragraph cache identity. The same complete structural key also governs the same-string fast path, so adding or removing layout properties such as ekp-no-break takes effect immediately on an already cached string object.

  • ekp-para-cache-limit (default 256): max cached paragraphs; the cache is flushed when the limit is reached.
  • M-x ekp-clear-caches clears everything (use after changing fonts or themes that affect glyph widths).

Performance

Measured on the bundled sample texts (tests/ekp-bench.el), batch Emacs 30.2, Apple Silicon; see DEVELOPER.md for methodology:

Case (text-zh.txt ≈ 3.6 KB) Elisp (byte-compiled) C module
justify, width 200px 150 ms 41 ms
optimal-width search 340380 529 ms 106 ms
DP only, width 400px 30 ms 2.5 ms

Byte-compile the package — the Elisp engine is ~10× faster compiled. Both engines produce identical output; the C module pays off most for optimal-width search and long multi-paragraph texts. (Absolute numbers vary with machine and power state; the ratios are the point.)

Known Limitations

  • Measurement follows the current buffer's face remappings (text-scale-mode, themes, ekp-org-setup-style tweaks) and reserves the truncation-indicator column in windows without fringes, so justified lines fit the real display. If lines ever look truncated or short in an exotic setup, run M-x ekp-diagnose in that buffer — it reports whether measurement matches rendering. The full fit matrix is a developer tool in tests/ekp-gui-verify.el; load that file before invoking M-x ekp-gui-verify.
  • One font is assumed per Latin/CJK script per paragraph when computing spacing defaults; mixed-font paragraphs work but spacing defaults come from the first font found.
  • ekp-pixel-range-justify minimizes average demerits with a ternary search plus a local scan; cost is not perfectly unimodal in width, so the result is a very good, but not guaranteed global, optimum.
  • In batch/tty Emacs, pixel widths degrade to character columns (the full pipeline still works; useful for testing).

Interactive Demo

emacs -Q -L /path/to/emacs-kp -l tests/ekp-showcase.el -f ekp-showcase

One buffer, live keys: -/+ change the pixel width (per-reflow time in the header line), d runs an animated width sweep with an fps report, a cycles alignment, p toggles hanging punctuation, i first-line indent, s a wedge parshape, c compares the C engine with pure Elisp, w follows the window width via ekp-auto-justify-mode. The sample text includes a protected code block, an inline no-break atom and NBSP-joined numbers.

Testing

tests/run-tests.sh /path/to/emacs     # batch-safe ERT suite

# Full interactive GUI fit matrix
emacs -Q -L /path/to/emacs-kp -L /path/to/emacs-kp/tests \
  -l /path/to/emacs-kp/tests/ekp-gui-verify.el \
  -f ekp-gui-verify-matrix

The matrix prints every row and exits with status 1 if any fit check fails, so the same command can gate local release automation. The verifier is a developer tool under tests/; it is not loaded by (require 'ekp-region).

Credits

  • Core algorithm: "Breaking Paragraphs into Lines" by Donald E. Knuth and Michael F. Plass (1981)
  • Hyphenation: Frank Liang's algorithm, adapted from Pyphen
  • Dictionaries: hyphenation patterns from the LibreOffice dictionaries; see dictionaries/MANIFEST.tsv, dictionaries/LICENSES.md, and the bundled per-dictionary notices for exact source, checksum, and license evidence.