中文文档 | 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.
- 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 — one semantic layout plan drives both
renderers. The string API uses pixel spaces; buffer layout combines
space-widthwith absolute-pixelmin-width, so it works with variable-width fonts without inserting layout characters. - Clean editable buffers — buffer commands create no overlays and add
no glue spaces, soft newlines, or discretionary hyphens to the character
stream.
buffer-string,char-after, search, syntax, save, and ordinary Elisp text consumers see the source characters. - Text properties preserved — faces, colors and other properties survive justification; inserted hyphens inherit the face of the word they break.
- Robust on hard input — unprotected 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).
- Emacs 29.1+ (uses
string-pixel-widthandobject-intervals) - Optional, for the C module: a C11 compiler and pthreads
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-buffer) ; buffer/region commandsByte-compiling is strongly recommended — the Elisp engine is about 10× faster compiled.
(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/sanitizeOnce 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.
ekp-buffer.el turns the string API into buffer-level commands:
(require 'ekp-buffer)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— remove EKP's display projection. The source text does not need restoration because buffer layout never replaced it.M-x ekp-auto-justify-mode— keep completed hard paragraphs justified while ordinary typing remains stable. The active hard line has a committed projection plus one local edit transaction. Typing within the same native visual row does no whole-line planning and leaves unaffected projected rows untouched. Editing a projected middle row naturalizes only that row's dirty island, so later break anchors stay in place and native wrapping handles local word migration. When input naturally crosses into the next visual row, EKP runs or reuses one completeekp-layout-planand atomically publishes all completed rows; the new row remains natural. Deleting and reinserting the same source restores the saved projection exactly, including text properties. Point motion never plans or writes layout properties, even when point leaves the paragraph. Global commits happen only at a visual-row crossing, hard newline/paragraph completion, the next real edit elsewhere, explicit refill, or a width/font/layout-context change. There is no edit-idle whole-paragraph snap. Window resize re-flow is debounced byekp-auto-justify-resize-delay. The mode temporarily disables both explicit line truncation and Emacs's narrow partial-window truncation, so a side-by-side editing window still soft-wraps normally. Disabling the mode restores the prior buffer-local or global ownership of both settings. A leading or trailing space/tab on the active line is visible in the same input turn; deleting a following glyph does not hide that source whitespace. Reprojection also preserves an inactive mark as inactive, so width changes do not create an accidental selection. While active, the standard EKP menu exposes formatting, protection, and window-fit diagnostic commands;C-h mdescribes the same workflow.
The projection uses text properties on existing source graphemes only:
- Existing ASCII spaces receive
((space-width FACTOR) (min-width ((TARGET-PIXELS)))). - A CJK or mixed gap with no source space adds
min-widthto the preceding complete grapheme; the target is its natural advance plus the glue. line-prefixsupplies indentation. A break or discretionary hyphen is a replacing display string on an existing complete grapheme.- EKP never creates an overlay. It also never steals a foreign replacing
display,line-prefix,wrap-prefix,composition, orinvisibleowner; that hard paragraph stays natural andM-x ekp-diagnosereports the conflict.
Consequently:
- Elisp APIs and saving see the original character sequence. Visual
spaces, newlines, and hyphens cannot reach disk or syntax/search logic.
buffer-substringcan still carry the EKP display properties because it preserves text properties;buffer-substring-no-propertiesis the plain source string. - Searching (including isearch) operates directly on source text, so a word remains one word across a visual discretionary break.
- Copying and killing strip EKP-owned projection properties while composing with any existing substring filter. Pasted text contains only the logical content and its non-EKP properties.
- Projection updates run inside
with-silent-modifications: enabling, editing, resizing, and disabling layout do not create layout-only undo entries, modified-state changes, or character-modified ticks.
ekp-buffer-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.
Automatic planning is also bounded per hard paragraph.
ekp-auto-justify-paragraph-limit defaults to 2 048 characters. A longer
single paragraph stays naturally wrapped and fully editable instead of
blocking input in an unbounded Knuth-Plass pass; M-x ekp-diagnose reports
the reason. Run M-x ekp-refill-paragraph when you explicitly want the
unbounded full-quality pass for that paragraph.
Mode presets for explicit local protection — one call each:
(add-hook 'org-mode-hook #'ekp-org-setup)
(add-hook 'markdown-mode-hook #'ekp-markdown-setup)ekp-auto-justify-mode consults ekp-buffer-mode-policy-alist
automatically in Org and Markdown buffers. It does not copy profile
values into buffer-local variables unless you explicitly call the setup
functions above.
- Block level: paragraphs carrying the
ekp-verbatimtext property (M-x ekp-verbatim-region), wearing a face listed inekp-buffer-skip-faces(e.g.org-block,markdown-code-face), or matched by the buffer-local functionekp-buffer-skip-predicatepass through completely untouched. - Automatic inline level: faces listed in
ekp-buffer-inline-faces(Orgorg-code/org-verbatim, Markdown inline code by default through mode profiles) useekp-inline-code-policy. The defaultno-hyphenkeeps source spaces literal and suppresses dictionary hyphenation, but it may still wrap at legal source boundaries. Automaticno-breakspans downgrade tono-hyphenwhen wider than the effective measure. - Explicit hard atom 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. An atom wider than the measure stays intact but is not guaranteed a line of its own: the final pass may place it on one overflow line with preceding ordinary content. Ordinary underfull candidates use fixed emergency stretch and normal K-P costs. If an overfull candidate would otherwise extinguish every active final-pass path, the core preserves the last path with TeX-style artificial demerits. There is no CJK-orphan, unit, or screenshot-specific rule.
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-buffer-skip-predicate (the Org/Markdown presets do
this automatically).
- Alignment —
ekp-alignment:justify(default),ragged-right,ragged-left, orcenter. Non-justify modes keep word spacing natural while Knuth-Plass still minimizes raggedness withinekp-ragged-stretch-pixel(≈2 em) per line. - Hanging punctuation — set
ekp-protrusiontotand line-final punctuation (。、」 as well as periods, commas and break hyphens) hangs past the flush edge byekp-protrusion-ratios. The 0.5 default for fullwidth closers is visually equivalent to CLREQ line-end punctuation compression.ekp-auto-justify-modereserves the protrusion width automatically. - Paragraph shapes —
ekp-first-line-indent(t= 2 em) for the CJK paragraph convention, or full TeX-styleekp-parshapewith per-line(INDENT . WIDTH). First-line indent runs on the fast 1D path and the C engine; only fullekp-parshapeandekp-loosenessfall 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 viaekp-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).
The break-policy defaults are intended to make code readable without making every code-looking span a hard atom:
| Option | Default | Scope | Effect |
|---|---|---|---|
ekp-inline-code-policy |
no-hyphen |
global, profile, local | normal, no-hyphen, or automatic fitting no-break for inline-face spans |
ekp-hyphenation |
auto |
global, profile, local, region via command | auto/on use dictionaries when available; off suppresses discretionary hyphens |
ekp-token-break-policies |
URL/path/identifier no-hyphen, number-unit no-break |
global, profile, local | per-token automatic policies; local/profile values merge by token category |
ekp-number-unit-suffixes |
common CSS, time, data, frequency, and metric units | global, profile, local | suffixes recognized by the compact number-unit classifier |
ekp-kinsoku-profile |
common |
global, profile, local | common, zh, ja, off, or custom CJK line-start/end prohibitions |
ekp-cjk-no-line-start-extra / ekp-cjk-no-line-end-extra |
"" |
global, profile, local | additions used by the custom profile |
ekp-overlong-token-policy |
emergency |
global, profile, local | emergency, overflow, or natural for ordinary overlong Latin-like tokens |
ekp-buffer-measure |
narrowest-window |
global, profile, local | narrowest-window, fixed pixel integer, or (max . PIXELS) |
ekp-buffer-skip-faces |
profile-dependent | global, profile, local | paragraph-level verbatim faces |
ekp-buffer-inline-faces |
profile-dependent | global, profile, local | exact inline spans using ekp-inline-code-policy |
ekp-buffer-mode-policy-alist |
Org and Markdown profiles | global/local safe value | mode profiles consulted by automatic and manual buffer layout |
Effective precedence is deterministic: explicit region text properties
first, then explicit buffer/file/dir-local values, then the first matching
major-mode profile, then global defaults. ekp-break-policy region values
are normal, hyphenate, and no-hyphen; they override automatic token
or inline policies for the exact region but never create a hard atom.
ekp-no-break remains the only explicit hard-atom property and wins over
every automatic policy.
All listed variables have closed safe-local predicates where file/dir local
configuration is supported. ekp-diagnose reports the requested measure,
the narrowest live window, the effective measure, overflow risk, conflict
count, and the active inline/hyphenation/kinsoku/overlong policy summary.
The EKP menu exposes diagnose plus region commands for normal break,
hyphenation on, hyphenation off, clear break policy, no-break, and verbatim.
(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.
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.
| 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).
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-cachesclears everything (use after changing fonts or themes that affect glyph widths).
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 340–380 | 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.)
- Text properties are buffer-wide, so one buffer cannot carry independent plans for windows of different widths. EKP uses the narrowest live window as the authoritative width; wider windows can show unused space but never overflow.
- EKP-owned layout properties are visible to APIs that explicitly inspect text properties. The character stream is clean; copy/kill removes the owned projection metadata.
- Tabs and non-ASCII whitespace cannot be shrunk with
space-width. If an exact plan would require that operation, EKP leaves the affected hard paragraph natural and reports the conflict. - 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, runM-x ekp-diagnosein that buffer — it reports whether measurement matches rendering. The full fit matrix is a developer tool intests/ekp-gui-verify.el; load that file before invokingM-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-justifyminimizes 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).
emacs -Q -L /path/to/emacs-kp -l tests/ekp-showcase.el -f ekp-showcaseOne 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.
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-matrixThe 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-buffer).
- 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.