The renderer (shared by both engines) is now lossless: synthesized
glue carries the original text it replaced (ekp-glue), soft line
breaks carry the whitespace swallowed around the break
(ekp-soft-break), break hyphens are marked (ekp-soft-hyphen), and
paragraph-edge whitespace survives as zero-display ekp-hidden text.
Orphaned ekp--combine-glues-and-boxes / ekp--interleave removed.
New ekp-region.el:
- ekp-justify-region / ekp-unjustify-region: in-place justification
with exact structural restore (character- and property-exact),
robust to edits made while justified
- ekp-auto-justify-mode: keeps the buffer justified at the window
width; debounced re-flow on window resize, incremental
per-paragraph re-justification after edits (served by the
paragraph cache)
- fix found in live GUI testing: buffer-local members of
window-size-change-functions receive the WINDOW as argument and
may run with an unrelated buffer current; the handler now resolves
window and buffer explicitly (regression test included)
Tests: 47 ERT (36 core + 11 region) passing; 300-case fuzz 0
failures (C/elisp parity unchanged); byte-compile clean with
error-on-warn. Verified interactively in GUI Emacs: justified at
1403px maximized, auto-reflowed to 614px on frame resize.
docs: interactive-use section in readme.md / readme_zh.md
ci: byte-compile list includes ekp-region.el
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two jobs on ubuntu-latest:
- elisp-only matrix (Emacs 29.1 floor + 30.1): byte-compile with
error-on-warn, then the ERT suite (C-module tests auto-skip)
- C-module job (apt Emacs 29.3): build ekp.so, full ERT suite with
C/elisp parity tests, then the 300-case property fuzz
Both paths were dry-run in a local ubuntu:24.04 container before
committing.
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>
Opening brackets ((, 「, 《, etc.) are now attached to the following
character instead of the preceding one, preventing them from appearing
at the end of a line which violates kinsoku typographic rules.
Fixes#9
Agent-Logs-Url: https://github.com/Kinneyzhang/emacs-kp/sessions/d05069f4-db64-4601-9816-f582edad4a1b
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.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>
Code review found additional functions using global glue params instead
of para's stored params:
- ekp--compute-stretch-capacity: now takes para param
- ekp--compute-shrink-capacity: now takes para param
- ekp--line-badness-and-fitness: now takes para param
- ekp--dp-compute-line-demerits: now takes para param
- ekp--distribute-gap-adjustment: now takes para param
All capacity/distribution calculations now use para's stored glue params
for full consistency across multi-font paragraphs.
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
When multiple paragraphs with different fonts were processed, subsequent
executions would use wrong glue parameters from the cache. This caused
misalignment because different fonts have different spacing values.
Changes:
- Add glue-params field to ekp-para struct to store params at creation
- Add helper functions: ekp--para-glue-ideal/shrink/stretch/min/max
- Update all C module interface functions to use para's stored params
- Update all glue computation functions for consistency
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>