ekp-alignment selects the paragraph alignment. Non-justify modes keep
inter-word glue rigid (arrays and class params zeroed at para build)
while the DP gains a per-line flexibility ekp-ragged-stretch-pixel
(default ≈2 em): badness = 100·(shortfall/R)³, so Knuth-Plass still
globally minimizes raggedness — this is \raggedright with a finite
\rightskip stretch, not first-fit.
Rendering distributes each line's leftover by mode: trailing
(ragged-right), split evenly (center), leading (ragged-left); the
spacers are ekp-glue-marked, so buffer roundtrips stay exact.
C module 1.3: set-penalties gains an extra-stretch scalar (reset when
omitted); max_w widens by it, so the prefix-based flexibility picks it
up automatically. Cache keys include alignment and R.
Tests: 56 ERT green incl. per-mode invariants (fit, rigid interior
glue, flush edges, centered leftover ±1px) and four-mode C parity;
fuzz 300/300.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kinsoku moves from merge-based box attachment to a general break-
permission layer, the foundation for no-break spans, NBSP, verbatim
atoms and punctuation protrusion:
- tokenizer: every CJK char (punctuation included) is its own box;
the hold/attach machinery is gone. Fixes two latent bugs: a second
consecutive closer (字。」) could start a line, and an opener held
across spaces reordered box content vs the original string
- ekp--str-type: cjk-punct splits into cjk-open (Ps/Pi) / cjk-close
- ekp-para gains breaks-allowed (bool-vector) + forbidden-positions
(sparse, for C); glue at unbreakable punct gaps is nws, so
punctuation hugs its content and justification stretch no longer
opens gaps at 「x or x。
- halfwidth kinsoku: boxes consisting purely of .,;:!?)]}’”»›… may
not start a line ((&[{‘“«‹ may not end one) — an ASCII comma after
a CJK char no longer dangles at line start (old behavior violated
CLREQ)
- DP (elisp 1D, elisp loose 2D, C): forbidden gaps are skipped as
candidates while the line keeps extending; the emergency fallback
generalizes from single boxes to atomic runs (no permitted break
inside), recording real gap counts so multi-box emergency lines
render correctly
- C module 1.2: ekp-c-break-with-arrays 11→12 args
(forbidden-positions), batch vectors 12 elements, version gate bumped
Hyphenation improvement: 「Hello / Hello」 previously failed the
word regexp as merged boxes and were never hyphenated; as separate
boxes they hyphenate normally.
Tests: 48 ERT green (split tests migrated to the new box contract;
new unit test for break permissions and a rendered-output kinsoku
sweep across widths); 300-case fuzz 0 failures with C 1.2 parity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ekp_hyphen.c: check fgets return when skipping the dictionary
encoding line (gcc -Wunused-result via glibc fortify); behavior
unchanged — an empty file already fell through to EOF in the loops
- ekp-utils.el: declare-function for font-info, which does not exist
in non-window-system builds (emacs-nox); call site is runtime-guarded
by display-multi-font-p. Keeps byte-compile-error-on-warn green.
Verified: clean gcc build + 36/36 ERT + 300-case fuzz on Ubuntu 24.04
(Emacs 29.3, arm64), clean clang build + 36/36 ERT on macOS (30.2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- fix: ekp-param-set silently reset after first justify (now persists;
ekp-param-reset added)
- fix: narrow-width CJK returned empty string (data loss); two-pass
emergency-break strategy guarantees output for any input
- fix: K-P penalties never synced to C module; space-box metrics
divergence between C and Elisp engines
- fix: para cache ignored ekp-latin-lang (stale hyphenation after
language switch) and used collision-prone sxhash keys
- fix: fullwidth letters/digits misclassified as CJK punctuation
- fix: combining chars split from their base char in the tokenizer
- fix: punctuation-wrapped words (word!/(word)/word;) never hyphenated
- fix: renderer double-counted stripped space widths; negative glue
clamped; batch/tty font detection no longer crashes
- feat: real looseness support via (position × line-count) DP
- perf: O(1) line metrics and gap counts via prefix arrays (inner loop
previously allocated O(n) subsequences → O(n³) total); box measurement
dedupe; eq fast-path para lookup; prebuilt per-para glue arrays
→ zh justify 7547ms → 96ms (compiled elisp) / 57ms (C);
range-justify 68.5s → 0.48s / 34ms; C module itself 3–19× faster
- test: 36 batch-safe ERT tests + 300-case property fuzz (C/elisp
byte-identical output, zero content loss) replacing ad-hoc suite
- docs: readme/readme_zh/DEVELOPER/DEVELOPER_ZH/ekp_c-README rewritten
to match the implementation; phase handoff in .phrase/phases/
BREAKING: requires Emacs 29.1+; C module must be rebuilt (v1.1, new
arities); ekp-threshold-factor / ekp-flagged-penalty /
ekp-forced-break-penalty removed; Rust module stubs removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bug: C module and Elisp produced different results for same text.
Root cause: Different condition checking order for single-box vs last-line:
- Elisp: single-box check comes BEFORE is-last check
- C: is_last check came BEFORE is_single_box check (now fixed)
When the last line has only one box, Elisp uses single-box calculation
while C was using last-line calculation, resulting in different demerits
and different final breakpoints.
Fix: Moved is_single_box check before is_last check in dp_process_position()
to match Elisp's ekp--dp-compute-line-demerits behavior.
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
Extracted shared DP core algorithm into dp_process_position() function:
1. Added dp_input_t struct - unified input interface for DP algorithm
- Contains prefix arrays, glue arrays, hyphen info
- Contains K-P parameters (penalties, ratios)
2. Added dp_is_hyphen() function - unified hyphen position detection
- Uses O(log n) binary search
- Replaces is_hyphen_pos() function
3. Added dp_process_position() function - core DP algorithm
- Processes single start position i, tries all end positions k
- Contains complete demerits calculation, fitness classification
- Both entry functions now call this shared core
4. Refactored process_dp_range()
- Builds temporary glue arrays from ekp_paragraph_t
- Creates dp_input_t and calls dp_process_position()
5. Refactored ekp_break_with_prefixes()
- Directly uses input arrays to create dp_input_t
- Calls dp_process_position() for each position
Benefits:
- Single point of maintenance: DP bugs only need fixing once
- Consistency guaranteed: both paths use identical algorithm
- ~100 lines of duplicate code removed
- Better testability: core algorithm can be tested independently
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
When is_last && ideal > line_width, the C module now correctly
triggers force break logic, matching ekp--dp-cache-elisp behavior.
This ensures consistent results between C module and pure Elisp paths.
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>