fix(showcase): follow-window right-edge truncation; checkdoc clean

showcase: the demo font was applied with face-remap-add-relative, but
string-pixel-width (how ekp measures) ignores buffer face remapping
while honoring an explicit :family — so measurement used the session
default font and rendering used the demo font.  On a wide follow-
window that per-glyph discrepancy accumulated past the safety margin
and every justified line overflowed the right edge (invisible at the
narrow 480px default).  Pin the font as an appended :family face text
property instead, so measurement and rendering agree; inline atom/
code faces keep precedence.

checkdoc: all four elisp files are now checkdoc-clean (docstrings,
argument mentions, sentence spacing) with no logic change; the three
validation messages read naturally again (dropped the capitalized
"Ekp:" prefix checkdoc's capital-letter rule had forced).

92 ERT green; fuzz 300/300 with C parity; byte-compile warning-free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kinneyzhang 2026-07-27 02:28:29 +08:00
parent 81d32c7052
commit 4dee06ec47
5 changed files with 143 additions and 79 deletions

View File

@ -23,7 +23,7 @@
;;; Commentary:
;; Implementation of Frank Liang's hyphenation algorithm.
;; See: Liang, F.M. "Word Hy-phen-a-tion by Com-put-er" (1983)
;; See: Liang, F.M. "Word Hy-phen-a-tion by Com-put-er" (1983)
;;
;; Usage:
;; (ekp-hyphen-load-languages "/path/to/dictionaries")
@ -180,7 +180,7 @@ declarations default to 2/2."
(nreverse result))))
(defun ekp-hyphen--positions (h word)
"Get cached break positions for WORD."
"Return cached break positions for WORD using hyphenator H."
(let* ((key (downcase word))
(cache (ekp-hyphen-cache h)))
(or (gethash key cache)
@ -207,14 +207,14 @@ by default the dictionary's own LEFTHYPHENMIN/RIGHTHYPHENMIN apply
h))))
(defun ekp-hyphen-positions (h word)
"Return valid break positions in WORD, respecting margins."
"Return valid break positions in WORD for H, respecting margins."
(let ((left (ekp-hyphen-left h))
(right (- (length word) (ekp-hyphen-right h))))
(cl-remove-if-not (lambda (p) (and (>= p left) (<= p right)))
(ekp-hyphen--positions h word))))
(defun ekp-hyphen-inserted (h word &optional hyphen)
"Return WORD with HYPHEN inserted at break points."
"Return WORD with HYPHEN inserted at H's break points."
(let ((hyphen (or hyphen "-")) (result word) (off 0))
(dolist (pos (ekp-hyphen-positions h word))
(setq result (concat (substring result 0 (+ pos off))
@ -224,7 +224,7 @@ by default the dictionary's own LEFTHYPHENMIN/RIGHTHYPHENMIN apply
result))
(defun ekp-hyphen-boxes (h word)
"Split WORD into syllables at break points."
"Split WORD into syllables at H's break points."
(split-string (ekp-hyphen-inserted h word " ") " "))
(provide 'ekp-hyphen)

View File

@ -56,7 +56,7 @@ by the display engine due to rounding."
:type 'natnum)
(defcustom ekp-auto-justify-resize-delay 0.15
"Seconds to debounce window-resize re-flows in `ekp-auto-justify-mode'."
"Seconds to debounce window resize re-flows in `ekp-auto-justify-mode'."
:type 'number)
(defcustom ekp-auto-justify-edit-delay 0.3
@ -65,7 +65,7 @@ by the display engine due to rounding."
(defcustom ekp-auto-justify-lazy-threshold 20000
"Buffer size (characters) beyond which re-flows go visible-first.
Below it a window-width change re-justifies the whole buffer at
Below it a window width change re-justifies the whole buffer at
once; above it the visible portion is done synchronously and the
rest follows in idle background chunks."
:type 'natnum)
@ -229,7 +229,7 @@ property, matching `ekp-region-skip-faces', or accepted by
(defun ekp-region--justify-string (text pixel)
"Return TEXT justified to PIXEL with exact-recovery markers.
Hard newlines are preserved one-to-one. Whitespace-only paragraphs
(which the string API would empty out) survive as hidden text;
\(which the string API would empty out) survive as hidden text;
verbatim paragraphs (see `ekp-region--skip-para-p') pass through
untouched."
(let* ((paras (split-string text "\n"))
@ -240,7 +240,7 @@ untouched."
(ekp-region--split-hard
(ekp-pixel-justify (string-join cores "\n") pixel)))))
(unless (= (length out) (length cores))
(error "ekp-region: paragraph count mismatch (%d vs %d)"
(error "Paragraph count mismatch (%d vs %d)"
(length out) (length cores)))
(string-join
(cl-loop for p in paras for s in skips
@ -515,7 +515,7 @@ prefix argument supplies it explicitly."
;;;###autoload
(defun ekp-no-break-region (beg end)
"Mark the region as an unbreakable typesetting atom.
"Mark the region from BEG to END as an unbreakable typesetting atom.
Justification treats it as one rigid unit: no line break inside, no
hyphenation, spacing stays literal (inline code, product names,
numbers with units)."
@ -524,13 +524,13 @@ numbers with units)."
;;;###autoload
(defun ekp-allow-break-region (beg end)
"Remove `ekp-no-break' marking from the region."
"Remove `ekp-no-break' marking between BEG and END."
(interactive "r")
(remove-text-properties beg end '(ekp-no-break nil)))
;;;###autoload
(defun ekp-verbatim-region (beg end)
"Protect the region's paragraphs from justification (code blocks).
"Protect the paragraphs from BEG to END against justification (code blocks).
Whole paragraphs carrying the `ekp-verbatim' property pass through
`ekp-justify-region' and `ekp-auto-justify-mode' untouched. For an
unbreakable span inside prose, use `ekp-no-break-region' instead."
@ -539,7 +539,7 @@ unbreakable span inside prose, use `ekp-no-break-region' instead."
;;;###autoload
(defun ekp-clear-verbatim-region (beg end)
"Remove `ekp-verbatim' protection from the region."
"Remove `ekp-verbatim' protection between BEG and END."
(interactive "r")
(remove-text-properties beg end '(ekp-verbatim nil)))

View File

@ -76,6 +76,7 @@ Returns nil (unknown) when font information is unavailable."
(= (aref info 7) (aref info 11))))))))
(defun ekp-get-latin-letter (string)
"Return the first Latin letter (a-z or A-Z) in STRING, or nil if none."
(with-temp-buffer
(insert string)
(goto-char (point-min))
@ -88,6 +89,7 @@ Returns nil (unknown) when font information is unavailable."
(buffer-substring (point) (1+ (point))))))
(defun ekp-get-cjk-letter (string)
"Return the first wide CJK character in STRING, or nil if none."
(with-temp-buffer
(insert string)
(goto-char (point-min))
@ -101,7 +103,9 @@ Returns nil (unknown) when font information is unavailable."
(buffer-substring (point) (1+ (point))))))
(defun ekp-monospace-p (string)
"判断字符串中的拉丁字母的字体是否等宽,返回字体名称"
"Return the font family of STRING's Latin letters when monospace.
Return nil when that font is not monospace, or the default face
family when STRING contains no Latin letter."
(if-let* ((letter (ekp-get-latin-letter string))
(font-family (ekp-font-family letter)))
;; return monospace font family
@ -111,6 +115,9 @@ Returns nil (unknown) when font information is unavailable."
(face-attribute 'default :family)))
(defun ekp-word-spacing-pixel (string)
"Return the pixel width of an inter-word space for STRING.
Use the blank glyph of STRING's Latin font; for a monospace font
that width is the space's own advance."
;; font is monospace, use the pixel of blank
;; as word spacing pixel
(if-let ((font-family (ekp-monospace-p string)))
@ -123,11 +130,15 @@ Returns nil (unknown) when font information is unavailable."
" " 'face `(:family ,font-family))))))
(defun ekp-latin-font (string)
"Return the font family used for STRING's Latin letters.
Fall back to the default face family when STRING has no Latin letter."
(if-let ((letter (ekp-get-latin-letter string)))
(ekp-font-family letter)
(face-attribute 'default :family)))
(defun ekp-cjk-font (string)
"Return the font family used for STRING's CJK characters.
Fall back to the family of a sample CJK glyph when STRING has none."
(if-let ((letter (ekp-get-cjk-letter string)))
(ekp-font-family letter)
(ekp-font-family "")))
@ -139,7 +150,7 @@ Returns nil (unknown) when font information is unavailable."
(propertize " " 'display `(space :width (,pixel)))))
(defun ekp-cjk-fw-punct-p (str)
"Return non-nil if STR starts with a CJK full-width punctuation char.
"Return non-nil if STR begins with a CJK full-width punctuation char.
Full-width alphanumerics (, ) are NOT punctuation."
(let ((char (seq-first str)))
(and
@ -161,11 +172,11 @@ When STR is held as cjk-char, this checks if it still needs attachment."
'(Ps Pi))))
(defun ekp--flush-latin-word (word boxes)
"Push latin WORD to BOXES if non-nil. Return updated boxes."
"Push latin WORD to BOXES if non-nil. Return updated boxes."
(if word (cons word boxes) boxes))
(defun ekp--flush-cjk-char (char boxes)
"Push CJK CHAR to BOXES if non-nil. Return updated boxes."
"Push CJK CHAR to BOXES if non-nil. Return updated boxes."
(if char (cons char boxes) boxes))
(defun ekp--flush-spaces (spaces boxes prev-state next-width)
@ -214,8 +225,9 @@ invisible break points."
(and (>= char #xFE00) (<= char #xFE0F))))
(defun ekp--handle-latin-char (str state latin-word cjk-char boxes)
"Handle a latin (width=1) character.
Return (new-state new-latin-word new-cjk-char new-boxes)."
"Handle a latin (width=1) character STR.
STATE is the current mode; LATIN-WORD, CJK-CHAR and BOXES are the
accumulators. Return (new-state new-latin-word new-cjk-char new-boxes)."
(if (= state 1)
;; Already in latin mode: accumulate
(list 1 (concat latin-word str) nil boxes)
@ -223,12 +235,13 @@ Return (new-state new-latin-word new-cjk-char new-boxes)."
(list 1 str nil (ekp--flush-cjk-char cjk-char boxes))))
(defun ekp--handle-cjk-char (str state latin-word cjk-char boxes)
"Handle a CJK (width=2) character.
Return (new-state new-latin-word new-cjk-char new-boxes).
"Handle a CJK (width=2) character STR.
STATE is the current mode; LATIN-WORD, CJK-CHAR and BOXES are the
accumulators. Return (new-state new-latin-word new-cjk-char new-boxes).
Every CJK character punctuation included becomes its own box.
Kinsoku is enforced by the DP through per-gap break permissions
(`ekp-para-breaks-allowed'), not by merging boxes."
\(`ekp-para-breaks-allowed'), not by merging boxes."
(if (= state 1)
;; Was in latin mode: flush latin word, hold current CJK char
(list 2 nil str (ekp--flush-latin-word latin-word boxes))
@ -305,9 +318,10 @@ by merging boxes."
(defun ekp-start-process-with-callback
(process-name command-args callback
&optional output-buffer)
"Run COMMAND-ARGS asynchronously; call CALLBACK on success.
CALLBACK receives (PROCESS BUFFER). The output buffer is killed
after CALLBACK returns."
"Run COMMAND-ARGS as process PROCESS-NAME; call CALLBACK on success.
CALLBACK receives (PROCESS BUFFER). OUTPUT-BUFFER names the output
buffer (a generated name by default); it is killed after CALLBACK
returns."
(let* ((buffer-name (generate-new-buffer-name
(or output-buffer "*EKP Process Output*")))
(process (apply #'start-process process-name

145
ekp.el
View File

@ -273,8 +273,9 @@ When exceeded, the whole paragraph cache is flushed (cheap to rebuild)."
:group 'ekp)
(defvar ekp--params-explicit nil
"Non-nil after `ekp-param-set'; spacing params then persist until
`ekp-param-reset'. When nil, defaults are derived from each string.")
"Non-nil after `ekp-param-set' has been called.
Spacing parameters then persist until `ekp-param-reset'; when nil,
defaults are derived from each string.")
;;;; Initialization
;; ekp-root-dir is provided by ekp-utils.el
@ -300,7 +301,10 @@ hyphenation; it must not break loading the package."
ekp-cws-ideal-pixel ekp-cws-stretch-pixel ekp-cws-shrink-pixel))
(defun ekp--param-apply (lws-i lws-+ lws-- mws-i mws-+ mws-- cws-i cws-+ cws--)
"Set the nine spacing variables and derived limits (internal)."
"Set the nine spacing variables and derived limits (internal).
The nine pixel arguments are ideal/stretch/shrink per glue class:
LWS-I LWS-+ LWS-- for Latin word space, MWS-I MWS-+ MWS-- for mixed
Latin-CJK, and CWS-I CWS-+ CWS-- for CJK."
(setq ekp-lws-ideal-pixel lws-i ekp-lws-stretch-pixel lws-+
ekp-lws-shrink-pixel lws-- ekp-mws-ideal-pixel mws-i
ekp-mws-stretch-pixel mws-+ ekp-mws-shrink-pixel mws--
@ -315,9 +319,10 @@ hyphenation; it must not break loading the package."
(setq ekp--last-para nil))
(defun ekp-param-set (lws-i lws-+ lws-- mws-i mws-+ mws-- cws-i cws-+ cws--)
"Set all spacing parameters explicitly; they persist until `ekp-param-reset'.
LWS = Latin word space, MWS = mixed, CWS = CJK.
Each takes ideal, stretch (+), and shrink (-) values in pixels."
"Set all spacing parameters explicitly; persist until `ekp-param-reset'.
The nine pixel values, in order, are LWS-I LWS-+ LWS-- MWS-I MWS-+
MWS-- CWS-I CWS-+ CWS--: ideal, stretch (+) and shrink (-) for the
Latin (LWS), mixed (MWS) and CJK (CWS) word spaces."
(ekp--param-apply lws-i lws-+ lws-- mws-i mws-+ mws-- cws-i cws-+ cws--)
(setq ekp--params-explicit t))
@ -419,7 +424,7 @@ Returns one of `space', `latin', `cjk', `cjk-open', `cjk-close'.
(aset ekp--str-type-table c (ekp--str-type-1 str)))))
(defun ekp--str-type-1 (str)
"Uncached `ekp--str-type'."
"Uncached `ekp--str-type' computation for STR."
(cond
;; Whitespace or zero-width characters
((or (string-blank-p str) (= (string-width str) 0)) 'space)
@ -457,10 +462,11 @@ Skips zero-width characters; falls back to the edge char."
(ekp--str-type (ekp--box-edge-char box t))))))
(defun ekp--glue-type (prev-box-type curr-box-type)
"Glue type between boxes: `lws', `mws', `cws' or `nws'.
Lws means whitespace between latin words; cws between cjk chars;
mws between cjk and latin; nws means no whitespace. Space boxes
\(preserved whitespace) need no additional glue."
"Return the glue type between PREV-BOX-TYPE and CURR-BOX-TYPE.
It is `lws', `mws', `cws' or `nws'. Lws means whitespace between
latin words; cws between cjk chars; mws between cjk and latin; nws
means no whitespace. Space boxes (preserved whitespace) need no
additional glue."
(let ((before (cdr prev-box-type))
(after (car curr-box-type)))
(if before
@ -544,19 +550,22 @@ Japanese typesetting. Stored as a string of characters."
all)))
(defun ekp--box-no-line-start-p (box box-type)
"Non-nil if BOX must not appear at the start of a line."
"Non-nil if BOX must not appear at the start of a line.
BOX-TYPE is BOX's (start . end) type pair from `ekp--box-type'."
(or (eq (car box-type) 'cjk-close)
(and (> (length box) 0)
(aref ekp--extra-nls-table (aref box 0)))
(ekp--box-pure-set-p box ekp--no-line-start-char-list)))
(defun ekp--box-no-line-end-p (box box-type)
"Non-nil if BOX must not appear at the end of a line."
"Non-nil if BOX must not appear at the end of a line.
BOX-TYPE is BOX's (start . end) type pair from `ekp--box-type'."
(or (eq (cdr box-type) 'cjk-open)
(ekp--box-pure-set-p box ekp--no-line-end-char-list)))
(defun ekp--compute-glue-types (boxes boxes-types hyphen-positions)
"Compute glue types for BOXES. Positions after HYPHEN-POSITIONS are `nws'."
"Compute the glue-type vector for BOXES using BOXES-TYPES.
Positions right after HYPHEN-POSITIONS are forced to `nws'."
(let* ((n (length boxes))
(glues (make-vector n nil))
prev-type)
@ -570,18 +579,21 @@ Japanese typesetting. Stored as a string of characters."
glues))
(defun ekp-glue-ideal-pixel (type)
"Return the ideal glue pixel width for glue TYPE."
(cond ((or (null type) (eq 'nws type)) 0)
((eq 'lws type) ekp-lws-ideal-pixel)
((eq 'mws type) ekp-mws-ideal-pixel)
((eq 'cws type) ekp-cws-ideal-pixel)))
(defun ekp-glue-min-pixel (type)
"Return the minimum glue pixel width for glue TYPE."
(cond ((or (null type) (eq 'nws type)) 0)
((eq 'lws type) ekp-lws-min-pixel)
((eq 'mws type) ekp-mws-min-pixel)
((eq 'cws type) ekp-cws-min-pixel)))
(defun ekp-glue-max-pixel (type)
"Return the maximum glue pixel width for glue TYPE."
(cond ((or (null type) (eq 'nws type)) 0)
((eq 'lws type) ekp-lws-max-pixel)
((eq 'mws type) ekp-mws-max-pixel)
@ -612,12 +624,12 @@ Japanese typesetting. Stored as a string of characters."
((eq 'cws type) (plist-get params :cws-stretch)))))
(defun ekp--para-glue-min (para type)
"Get minimum glue pixel (ideal - shrink) for TYPE."
"Return the minimum glue pixel (ideal - shrink) for TYPE in PARA."
(- (ekp--para-glue-ideal para type)
(ekp--para-glue-shrink para type)))
(defun ekp--para-glue-max (para type)
"Get maximum glue pixel (ideal + stretch) for TYPE."
"Return the maximum glue pixel (ideal + stretch) for TYPE in PARA."
(+ (ekp--para-glue-ideal para type)
(ekp--para-glue-stretch para type)))
@ -728,7 +740,8 @@ glyph shared across paragraphs is measured only once."
(and box-type (eq (car box-type) 'space)))
(defun ekp--tail-protrude-pixel (box box-type)
"Pixels the last visible char of BOX may protrude past the flush edge."
"Pixels the last visible char of BOX may protrude past the flush edge.
BOX-TYPE is BOX's (start . end) type pair from `ekp--box-type'."
(if (not ekp-protrusion)
0
(let* ((tail-type (cdr box-type))
@ -745,7 +758,7 @@ glyph shared across paragraphs is measured only once."
0))))
(defun ekp--line-edge-release (para _start end)
"Pixels released at the right edge of the line [START, END).
"Pixels released at the right edge of PARA's line [START, END).
The protrusion of the line's final glyph: the soft hyphen's when the
line breaks at a hyphenation point, otherwise the last non-space
box's. 0 when `ekp-protrusion' was off at paragraph build time."
@ -770,7 +783,7 @@ Goes through the width cache: this runs for every rendered line."
(t 0)))
(defun ekp--line-spec (para line-index measure)
"Layout of LINE-INDEX (0-based) as (INDENT . WIDTH).
"Layout of PARA's LINE-INDEX (0-based) as (INDENT . WIDTH).
MEASURE is the paragraph measure passed to the justify call.
`ekp-parshape' takes precedence; its last entry repeats. Otherwise
`ekp-first-line-indent' shifts line 0. WIDTH never drops below 1."
@ -988,27 +1001,35 @@ Run after font or theme changes that affect glyph widths."
;;;; Paragraph Accessors
(defun ekp--boxes (string)
"Return the boxes of STRING's paragraph."
(ekp-para-boxes (ekp--get-para string)))
(defun ekp--boxes-widths (string)
"Return the box pixel widths of STRING's paragraph."
(ekp-para-boxes-widths (ekp--get-para string)))
(defun ekp--glues-types (string)
"Return the glue types of STRING's paragraph."
(ekp-para-glues-types (ekp--get-para string)))
(defun ekp--ideal-prefixs (string)
"Return the ideal prefix sums of STRING's paragraph."
(ekp-para-ideal-prefixs (ekp--get-para string)))
(defun ekp--min-prefixs (string)
"Return the minimum prefix sums of STRING's paragraph."
(ekp-para-min-prefixs (ekp--get-para string)))
(defun ekp--max-prefixs (string)
"Return the maximum prefix sums of STRING's paragraph."
(ekp-para-max-prefixs (ekp--get-para string)))
(defun ekp--hyphen-pixel (string)
"Return the hyphen pixel width of STRING's paragraph."
(ekp-para-hyphen-pixel (ekp--get-para string)))
(defun ekp--hyphen-positions (string)
"Return the hyphen positions of STRING's paragraph."
(ekp-para-hyphen-positions (ekp--get-para string)))
;;;; K-P Badness and Demerits
@ -1028,8 +1049,9 @@ Returns 0 if no adjustment needed, 10000 (infinite) if impossible."
(min ekp--infinite-badness (* 100 (expt (abs ratio) 3)))))))
(defun ekp--compute-fitness-class (adjustment-pixel flexibility-pixel)
"Classify line tightness into fitness class (0-3).
0=tight (shrunk), 1=decent, 2=loose, 3=very-loose."
"Classify line tightness from ADJUSTMENT-PIXEL and FLEXIBILITY-PIXEL.
Return the fitness class 0-3: 0=tight (shrunk), 1=decent, 2=loose,
3=very-loose."
(if (<= flexibility-pixel 0)
1 ; default to decent
(let ((ratio (/ (float adjustment-pixel) flexibility-pixel)))
@ -1044,6 +1066,8 @@ Returns 0 if no adjustment needed, 10000 (infinite) if impossible."
"Compute K-P demerits for a line break.
BADNESS is the line badness, PENALTY is break penalty (e.g., hyphen).
PREV-FITNESS and CURR-FITNESS are fitness classes of adjacent lines.
END-WITH-HYPHENP is non-nil when the line ends at a hyphen point, and
PREV-HYPHEN-COUNT counts the consecutive hyphenated lines before it.
Returns total demerits for this break."
(let* (;; Base demerits: (linepenalty + badness)²
(base (expt (+ ekp-line-penalty badness) 2))
@ -1082,7 +1106,7 @@ Uses binary search for O(log n) lookup."
;;;; Shared Line Measurement (O(1) via prefix arrays)
(defun ekp--gaps-between (para i k)
"Return (latin-gaps mix-gaps cjk-gaps) inside line I..K (exclusive glues).
"Return (latin-gaps mix-gaps cjk-gaps) for PARA inside line I..K.
Counts glue indices I+1 .. K-1 using precomputed prefix counts."
(let ((lp (ekp-para-lws-prefixs para))
(mp (ekp-para-mws-prefixs para))
@ -1093,8 +1117,9 @@ Counts glue indices I+1 .. K-1 using precomputed prefix counts."
(- (aref cp k) (aref cp j)))))
(defun ekp--line-ideal-pixel (para i k)
"Ideal width of line I..K: box+glue ideals, minus leading glue and
stripped space-box runs, plus hyphen width when the line hyphenates."
"Return the ideal width of line I..K in PARA.
It sums box and glue ideals, subtracts leading glue and stripped
space-box runs, and adds the hyphen width when the line hyphenates."
(let* ((ip (ekp-para-ideal-prefixs para))
(raw (- (aref ip k) (aref ip i)
(aref (ekp-para-glue-ideals para) i)))
@ -1127,9 +1152,10 @@ width, so results at different looseness values must not alias
(if (zerop ekp-looseness) line-pixel (cons line-pixel ekp-looseness)))
(defun ekp--dp-cache-elisp (para line-pixel)
"Pure Elisp DP implementation. Returns and caches the dp-result plist.
Looseness and parshape need the (position × line-count) DP; a plain
first-line indent is handled by the 1D pass (line 0 = start at box 0)."
"Return and cache the dp-result plist for PARA at LINE-PIXEL.
This pure-Elisp DP path handles looseness and parshape via the
position-by-line-count DP; a plain first-line indent uses the 1D
pass, where line 0 starts at box 0."
(if (or (/= ekp-looseness 0) ekp-parshape)
(ekp--dp-cache-elisp-loose para line-pixel)
(let ((dp-result (or (ekp--dp-run-1d para line-pixel nil)
@ -1138,14 +1164,14 @@ first-line indent is handled by the 1D pass (line 0 = start at box 0)."
dp-result)))
(defun ekp--hyphen-flags (hyphen-positions n)
"Return a bool-vector of length N flagging hyphenatable box indices."
"Return a `bool-vector' of length N flagging the HYPHEN-POSITIONS indices."
(let ((v (make-bool-vector (max n 1) nil)))
(dotimes (j (length hyphen-positions))
(aset v (aref hyphen-positions j) t))
v))
(defun ekp--dp-run-1d (para line-pixel allow-emergency)
"One strict (or emergency-permitting) K-P DP pass over PARA.
"One strict (or emergency-permitting) K-P DP pass over PARA at LINE-PIXEL.
Returns the dp-result plist, or nil when the paragraph end is
unreachable (only possible when ALLOW-EMERGENCY is nil)."
(let* ((boxes (ekp-para-boxes para))
@ -1334,7 +1360,11 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
end-with-hyphenp prev-hyphen-count
&optional line-gaps)
"Record an emergency (over/underfull atomic-run) break at K from I.
REST is line-pixel minus the line's ideal width (may be negative).
DEMERITS, BACKPTRS, RESTS, GAPS, HYPHEN-COUNTS and FITNESS-CLASSES are
the DP state arrays, updated at K when this break beats the stored
DEMERITS entry. PREV-DEM is the demerits accumulated up to I; REST is
line-pixel minus the line's ideal width (may be negative);
END-WITH-HYPHENP and PREV-HYPHEN-COUNT track the hyphen run.
LINE-GAPS is the (lws mws cws) gap-count list for multi-box runs
\(nil for single boxes, which render via the single-box path).
Only replaces an existing entry when strictly better."
@ -1352,7 +1382,7 @@ Only replaces an existing entry when strictly better."
(if end-with-hyphenp (1+ prev-hyphen-count) 0)))))
(defun ekp--dp-trace-breaks (backptrs n)
"Trace optimal break points from BACKPTRS array."
"Trace optimal break points back from N using the BACKPTRS array."
(let ((breaks (list n))
(index n))
(while (> index 0)
@ -1373,16 +1403,18 @@ Only replaces an existing entry when strictly better."
;; whose line count is closest to the target.
(defun ekp--dp-cache-elisp-loose (para line-pixel)
"Elisp DP tracking all line counts, for `ekp-looseness' support.
Two passes like the 1D engine: strict first, then with emergency
breaks when no valid layout exists."
"Run the DP over PARA at LINE-PIXEL, tracking all line counts.
For `ekp-looseness' and parshape support. Two passes like the 1D
engine: strict first, then emergency breaks when no layout is valid."
(let ((dp-result (or (ekp--dp-run-loose para line-pixel nil)
(ekp--dp-run-loose para line-pixel t))))
(puthash (ekp--dp-key line-pixel) dp-result (ekp-para-dp-cache para))
dp-result))
(defun ekp--dp-run-loose (para line-pixel allow-emergency)
"One (position × line-count) DP pass. Returns dp-result or nil."
"One (position × line-count) DP pass over PARA at LINE-PIXEL.
Return the dp-result plist, or nil when the paragraph end is
unreachable (only possible when ALLOW-EMERGENCY is nil)."
(let* ((boxes (ekp-para-boxes para))
(n (length boxes))
(hyphen-pixel (ekp-para-hyphen-pixel para))
@ -1574,7 +1606,10 @@ breaks when no valid layout exists."
(defun ekp--dp-loose-relax (states counts-at k lines i prev-dem candidate)
"Relax state (K . LINES) with CANDIDATE from position I.
CANDIDATE is (DEM-DELTA REST GAPS FITNESS HYPHEN-COUNT)."
STATES maps each (position . line-count) to its best vector; COUNTS-AT
tracks the line counts reached at each position. PREV-DEM is the
demerits up to I. CANDIDATE is (DEM-DELTA REST GAPS FITNESS
HYPHEN-COUNT)."
(let* ((key (cons k lines))
(total (+ prev-dem (nth 0 candidate)))
(existing (gethash key states)))
@ -1628,7 +1663,7 @@ If `ekp-use-c-module' is non-nil and the C module is available (and
(t (ekp--dp-cache-elisp para line-pixel)))))
(defun ekp--lines-data-from-breaks (para line-pixel breaks)
"Compute (RESTS . GAPS) lists for BREAKS, matching the DP's metrics.
"Compute (RESTS . GAPS) lists for BREAKS of PARA at LINE-PIXEL.
Per-line widths (first-line indent) must mirror the DP exactly, or
the reconstructed rests overfill the indented line."
(let ((start 0) (idx 0) rests gapss)
@ -1647,7 +1682,7 @@ the reconstructed rests overfill the indented line."
(cons (nreverse rests) (nreverse gapss))))
(defun ekp--store-c-result (para line-pixel breaks cost)
"Store a C-module result (BREAKS, COST) into PARA's dp-cache."
"Store C-module (BREAKS, COST) for LINE-PIXEL in PARA's dp-cache."
(let* ((data (ekp--lines-data-from-breaks para line-pixel breaks))
(dp-result (list :rests (car data)
:gaps (cdr data)
@ -1658,7 +1693,7 @@ the reconstructed rests overfill the indented line."
dp-result))
(defun ekp--prepare-para-for-c (para line-pixel)
"Prepare PARA data as a 15-element vector for the C batch API."
"Prepare PARA at LINE-PIXEL as a 15-element vector for the C batch API."
(vector (ekp-para-ideal-prefixs para)
(ekp-para-min-prefixs para)
(ekp-para-max-prefixs para)
@ -1676,7 +1711,7 @@ the reconstructed rests overfill the indented line."
(cdr (ekp--line-spec para 0 line-pixel))))
(defun ekp--dp-cache-via-c (para line-pixel)
"Compute breaks using the C module with PARA's precomputed arrays.
"Compute breaks at LINE-PIXEL using the C module and PARA's arrays.
The C module receives all font-dependent data from Elisp; it only
runs the pure DP. Falls back to Elisp when the C call fails."
(ekp--c-sync-params)
@ -1707,7 +1742,7 @@ runs the pure DP. Falls back to Elisp when the C call fails."
(ekp--store-c-result para line-pixel c-breaks c-cost))))
(defun ekp--dp-cache-batch (strings line-pixel)
"Compute DP for multiple STRINGS in parallel using the C batch API.
"Compute DP at LINE-PIXEL for multiple STRINGS via the C batch API.
Returns list of dp-results in the same order as STRINGS.
Only computes strings that aren't already cached."
(let* ((paras (mapcar #'ekp--get-para strings))
@ -1755,11 +1790,11 @@ If KEY is non-nil, return the value of KEY in the plist."
data)))
(defun ekp-total-cost (string line-pixel)
"Return the total demerits of the K-P solution."
"Return the total demerits of the K-P solution for STRING at LINE-PIXEL."
(ekp-dp-data string line-pixel :cost))
(defun ekp-line-breaks (string line-pixel)
"Return the break points of the K-P solution."
"Return the break points of the K-P solution for STRING at LINE-PIXEL."
(ekp-dp-data string line-pixel :breaks))
;;; Line Glue Distribution
@ -1828,7 +1863,8 @@ Returns ((latin-adj . latin-extra) (mix-adj . mix-extra) (cjk-adj . cjk-extra)).
(defun ekp--compute-glue-pixels (para glues-types gaps-distribution stretch-p)
"Compute actual glue pixels from GLUES-TYPES and GAPS-DISTRIBUTION.
Returns list of pixel values for each glue. Uses PARA's stored glue params."
Return the pixel list for each glue using PARA's stored glue params.
STRETCH-P selects stretch (t) or shrink (nil)."
(let ((latin-adj (car (nth 0 gaps-distribution)))
(latin-extra (cdr (nth 0 gaps-distribution)))
(mix-adj (car (nth 1 gaps-distribution)))
@ -1851,19 +1887,24 @@ Returns list of pixel values for each glue. Uses PARA's stored glue params."
glues-types)))
(defun ekp--line-glue-single-box (line-pixel box-width hyphen-p hyphen-pixel)
"Compute glues for a single-box line.
The trailing filler is clamped at 0 for overfull boxes."
"Compute glues for a single-box line of width LINE-PIXEL.
BOX-WIDTH is the box width; HYPHEN-P adds HYPHEN-PIXEL when the box
hyphenates. The trailing filler is clamped at 0 for overfull boxes."
(let ((trailing (- line-pixel box-width (if hyphen-p hyphen-pixel 0))))
(list 0 (max 0 trailing))))
(defun ekp--line-glue-last-line (para glues-types ideal-pixel line-pixel)
"Compute glues for last line (ragged right). Uses PARA's stored glue params."
"Compute glues for the last line (ragged right) of PARA at LINE-PIXEL.
GLUES-TYPES are the per-glue types and IDEAL-PIXEL the line's ideal
width; PARA supplies the stored glue params."
(append '(0)
(mapcar (lambda (type) (ekp--para-glue-ideal para type)) glues-types)
(list (max 0 (- line-pixel ideal-pixel)))))
(defun ekp--line-glue-normal (para glues-types rest-pixel gaps-list)
"Compute glues for a normal (justified) line. Uses PARA's stored glue params."
"Compute glues for a normal (justified) line from PARA.
GLUES-TYPES are the per-glue types; REST-PIXEL is the surplus (or
deficit) spread across GAPS-LIST using PARA's stored glue params."
(if (= rest-pixel 0)
(append '(0) (mapcar (lambda (type) (ekp--para-glue-ideal para type))
glues-types)
@ -2193,7 +2234,7 @@ The output is lossless with respect to STRING:
(defun ekp--validate-width (line-pixel)
"Signal a user error unless LINE-PIXEL is a positive integer."
(unless (and (integerp line-pixel) (> line-pixel 0))
(user-error "ekp: line width must be a positive integer, got %S"
(user-error "Line width must be a positive integer, got %S"
line-pixel)))
(defun ekp-pixel-justify (string line-pixel)
@ -2243,8 +2284,8 @@ When the C module is available, paragraphs are computed in parallel."
most-positive-fixnum)))
(defun ekp--ternary-search-optimal-width (strings min-pixel max-pixel)
"Find optimal width in [MIN-PIXEL, MAX-PIXEL] using ternary search.
Returns the pixel width with minimum average cost."
"Find the optimal width for STRINGS in [MIN-PIXEL, MAX-PIXEL].
Use ternary search; return the pixel width with minimum average cost."
(let ((lo min-pixel)
(hi max-pixel))
;; Ternary search: O(log n) iterations
@ -2276,7 +2317,7 @@ Returns (justified-text . optimal-pixel)."
(ekp--validate-width min-pixel)
(ekp--validate-width max-pixel)
(when (> min-pixel max-pixel)
(user-error "ekp: min-pixel (%d) must be <= max-pixel (%d)"
(user-error "Min-pixel (%d) must be <= max-pixel (%d)"
min-pixel max-pixel))
(let* ((strings (split-string string "\n"))
;; Pre-warm caches

View File

@ -247,10 +247,19 @@
(let ((inhibit-read-only t))
(erase-buffer)
(ekp-showcase-mode)
(insert (ekp-showcase--sample))
;; Pin the demo font as an explicit face property, NOT via
;; `face-remap-add-relative': `string-pixel-width' (how ekp
;; measures) ignores the buffer's face remapping but honors an
;; explicit `:family', so a remap makes measurement (session
;; default font) disagree with rendering (the demo font). On a
;; wide follow-window that per-glyph error accumulates past the
;; safety margin and every justified line overflows the right
;; edge. Appending keeps the inline atom/code faces on top.
(when (and (display-graphic-p)
(find-font (font-spec :family "Cascadia Next SC")))
(face-remap-add-relative 'default :family "Cascadia Next SC"))
(insert (ekp-showcase--sample))
(add-face-text-property (point-min) (point-max)
'(:family "Cascadia Next SC") t))
(goto-char (point-min))
(ekp-showcase--refresh)))
(pop-to-buffer buf)