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: ;;; Commentary:
;; Implementation of Frank Liang's hyphenation algorithm. ;; 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: ;; Usage:
;; (ekp-hyphen-load-languages "/path/to/dictionaries") ;; (ekp-hyphen-load-languages "/path/to/dictionaries")
@ -180,7 +180,7 @@ declarations default to 2/2."
(nreverse result)))) (nreverse result))))
(defun ekp-hyphen--positions (h word) (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)) (let* ((key (downcase word))
(cache (ekp-hyphen-cache h))) (cache (ekp-hyphen-cache h)))
(or (gethash key cache) (or (gethash key cache)
@ -207,14 +207,14 @@ by default the dictionary's own LEFTHYPHENMIN/RIGHTHYPHENMIN apply
h)))) h))))
(defun ekp-hyphen-positions (h word) (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)) (let ((left (ekp-hyphen-left h))
(right (- (length word) (ekp-hyphen-right h)))) (right (- (length word) (ekp-hyphen-right h))))
(cl-remove-if-not (lambda (p) (and (>= p left) (<= p right))) (cl-remove-if-not (lambda (p) (and (>= p left) (<= p right)))
(ekp-hyphen--positions h word)))) (ekp-hyphen--positions h word))))
(defun ekp-hyphen-inserted (h word &optional hyphen) (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)) (let ((hyphen (or hyphen "-")) (result word) (off 0))
(dolist (pos (ekp-hyphen-positions h word)) (dolist (pos (ekp-hyphen-positions h word))
(setq result (concat (substring result 0 (+ pos off)) (setq result (concat (substring result 0 (+ pos off))
@ -224,7 +224,7 @@ by default the dictionary's own LEFTHYPHENMIN/RIGHTHYPHENMIN apply
result)) result))
(defun ekp-hyphen-boxes (h word) (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 " ") " ")) (split-string (ekp-hyphen-inserted h word " ") " "))
(provide 'ekp-hyphen) (provide 'ekp-hyphen)

View File

@ -56,7 +56,7 @@ by the display engine due to rounding."
:type 'natnum) :type 'natnum)
(defcustom ekp-auto-justify-resize-delay 0.15 (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) :type 'number)
(defcustom ekp-auto-justify-edit-delay 0.3 (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 (defcustom ekp-auto-justify-lazy-threshold 20000
"Buffer size (characters) beyond which re-flows go visible-first. "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 once; above it the visible portion is done synchronously and the
rest follows in idle background chunks." rest follows in idle background chunks."
:type 'natnum) :type 'natnum)
@ -229,7 +229,7 @@ property, matching `ekp-region-skip-faces', or accepted by
(defun ekp-region--justify-string (text pixel) (defun ekp-region--justify-string (text pixel)
"Return TEXT justified to PIXEL with exact-recovery markers. "Return TEXT justified to PIXEL with exact-recovery markers.
Hard newlines are preserved one-to-one. Whitespace-only paragraphs 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 verbatim paragraphs (see `ekp-region--skip-para-p') pass through
untouched." untouched."
(let* ((paras (split-string text "\n")) (let* ((paras (split-string text "\n"))
@ -240,7 +240,7 @@ untouched."
(ekp-region--split-hard (ekp-region--split-hard
(ekp-pixel-justify (string-join cores "\n") pixel))))) (ekp-pixel-justify (string-join cores "\n") pixel)))))
(unless (= (length out) (length cores)) (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))) (length out) (length cores)))
(string-join (string-join
(cl-loop for p in paras for s in skips (cl-loop for p in paras for s in skips
@ -515,7 +515,7 @@ prefix argument supplies it explicitly."
;;;###autoload ;;;###autoload
(defun ekp-no-break-region (beg end) (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 Justification treats it as one rigid unit: no line break inside, no
hyphenation, spacing stays literal (inline code, product names, hyphenation, spacing stays literal (inline code, product names,
numbers with units)." numbers with units)."
@ -524,13 +524,13 @@ numbers with units)."
;;;###autoload ;;;###autoload
(defun ekp-allow-break-region (beg end) (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") (interactive "r")
(remove-text-properties beg end '(ekp-no-break nil))) (remove-text-properties beg end '(ekp-no-break nil)))
;;;###autoload ;;;###autoload
(defun ekp-verbatim-region (beg end) (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 Whole paragraphs carrying the `ekp-verbatim' property pass through
`ekp-justify-region' and `ekp-auto-justify-mode' untouched. For an `ekp-justify-region' and `ekp-auto-justify-mode' untouched. For an
unbreakable span inside prose, use `ekp-no-break-region' instead." 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 ;;;###autoload
(defun ekp-clear-verbatim-region (beg end) (defun ekp-clear-verbatim-region (beg end)
"Remove `ekp-verbatim' protection from the region." "Remove `ekp-verbatim' protection between BEG and END."
(interactive "r") (interactive "r")
(remove-text-properties beg end '(ekp-verbatim nil))) (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)))))))) (= (aref info 7) (aref info 11))))))))
(defun ekp-get-latin-letter (string) (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 (with-temp-buffer
(insert string) (insert string)
(goto-char (point-min)) (goto-char (point-min))
@ -88,6 +89,7 @@ Returns nil (unknown) when font information is unavailable."
(buffer-substring (point) (1+ (point)))))) (buffer-substring (point) (1+ (point))))))
(defun ekp-get-cjk-letter (string) (defun ekp-get-cjk-letter (string)
"Return the first wide CJK character in STRING, or nil if none."
(with-temp-buffer (with-temp-buffer
(insert string) (insert string)
(goto-char (point-min)) (goto-char (point-min))
@ -101,7 +103,9 @@ Returns nil (unknown) when font information is unavailable."
(buffer-substring (point) (1+ (point)))))) (buffer-substring (point) (1+ (point))))))
(defun ekp-monospace-p (string) (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)) (if-let* ((letter (ekp-get-latin-letter string))
(font-family (ekp-font-family letter))) (font-family (ekp-font-family letter)))
;; return monospace font family ;; return monospace font family
@ -111,6 +115,9 @@ Returns nil (unknown) when font information is unavailable."
(face-attribute 'default :family))) (face-attribute 'default :family)))
(defun ekp-word-spacing-pixel (string) (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 ;; font is monospace, use the pixel of blank
;; as word spacing pixel ;; as word spacing pixel
(if-let ((font-family (ekp-monospace-p string))) (if-let ((font-family (ekp-monospace-p string)))
@ -123,11 +130,15 @@ Returns nil (unknown) when font information is unavailable."
" " 'face `(:family ,font-family)))))) " " 'face `(:family ,font-family))))))
(defun ekp-latin-font (string) (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))) (if-let ((letter (ekp-get-latin-letter string)))
(ekp-font-family letter) (ekp-font-family letter)
(face-attribute 'default :family))) (face-attribute 'default :family)))
(defun ekp-cjk-font (string) (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))) (if-let ((letter (ekp-get-cjk-letter string)))
(ekp-font-family letter) (ekp-font-family letter)
(ekp-font-family ""))) (ekp-font-family "")))
@ -139,7 +150,7 @@ Returns nil (unknown) when font information is unavailable."
(propertize " " 'display `(space :width (,pixel))))) (propertize " " 'display `(space :width (,pixel)))))
(defun ekp-cjk-fw-punct-p (str) (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." Full-width alphanumerics (, ) are NOT punctuation."
(let ((char (seq-first str))) (let ((char (seq-first str)))
(and (and
@ -161,11 +172,11 @@ When STR is held as cjk-char, this checks if it still needs attachment."
'(Ps Pi)))) '(Ps Pi))))
(defun ekp--flush-latin-word (word boxes) (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)) (if word (cons word boxes) boxes))
(defun ekp--flush-cjk-char (char 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)) (if char (cons char boxes) boxes))
(defun ekp--flush-spaces (spaces boxes prev-state next-width) (defun ekp--flush-spaces (spaces boxes prev-state next-width)
@ -214,8 +225,9 @@ invisible break points."
(and (>= char #xFE00) (<= char #xFE0F)))) (and (>= char #xFE00) (<= char #xFE0F))))
(defun ekp--handle-latin-char (str state latin-word cjk-char boxes) (defun ekp--handle-latin-char (str state latin-word cjk-char boxes)
"Handle a latin (width=1) character. "Handle a latin (width=1) character STR.
Return (new-state new-latin-word new-cjk-char new-boxes)." 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) (if (= state 1)
;; Already in latin mode: accumulate ;; Already in latin mode: accumulate
(list 1 (concat latin-word str) nil boxes) (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)))) (list 1 str nil (ekp--flush-cjk-char cjk-char boxes))))
(defun ekp--handle-cjk-char (str state latin-word cjk-char boxes) (defun ekp--handle-cjk-char (str state latin-word cjk-char boxes)
"Handle a CJK (width=2) character. "Handle a CJK (width=2) character STR.
Return (new-state new-latin-word new-cjk-char new-boxes). 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. Every CJK character punctuation included becomes its own box.
Kinsoku is enforced by the DP through per-gap break permissions 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) (if (= state 1)
;; Was in latin mode: flush latin word, hold current CJK char ;; Was in latin mode: flush latin word, hold current CJK char
(list 2 nil str (ekp--flush-latin-word latin-word boxes)) (list 2 nil str (ekp--flush-latin-word latin-word boxes))
@ -305,9 +318,10 @@ by merging boxes."
(defun ekp-start-process-with-callback (defun ekp-start-process-with-callback
(process-name command-args callback (process-name command-args callback
&optional output-buffer) &optional output-buffer)
"Run COMMAND-ARGS asynchronously; call CALLBACK on success. "Run COMMAND-ARGS as process PROCESS-NAME; call CALLBACK on success.
CALLBACK receives (PROCESS BUFFER). The output buffer is killed CALLBACK receives (PROCESS BUFFER). OUTPUT-BUFFER names the output
after CALLBACK returns." buffer (a generated name by default); it is killed after CALLBACK
returns."
(let* ((buffer-name (generate-new-buffer-name (let* ((buffer-name (generate-new-buffer-name
(or output-buffer "*EKP Process Output*"))) (or output-buffer "*EKP Process Output*")))
(process (apply #'start-process process-name (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) :group 'ekp)
(defvar ekp--params-explicit nil (defvar ekp--params-explicit nil
"Non-nil after `ekp-param-set'; spacing params then persist until "Non-nil after `ekp-param-set' has been called.
`ekp-param-reset'. When nil, defaults are derived from each string.") Spacing parameters then persist until `ekp-param-reset'; when nil,
defaults are derived from each string.")
;;;; Initialization ;;;; Initialization
;; ekp-root-dir is provided by ekp-utils.el ;; 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)) 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--) (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-+ (setq ekp-lws-ideal-pixel lws-i ekp-lws-stretch-pixel lws-+
ekp-lws-shrink-pixel lws-- ekp-mws-ideal-pixel mws-i ekp-lws-shrink-pixel lws-- ekp-mws-ideal-pixel mws-i
ekp-mws-stretch-pixel mws-+ ekp-mws-shrink-pixel mws-- 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)) (setq ekp--last-para nil))
(defun ekp-param-set (lws-i lws-+ lws-- mws-i mws-+ mws-- cws-i cws-+ cws--) (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'. "Set all spacing parameters explicitly; persist until `ekp-param-reset'.
LWS = Latin word space, MWS = mixed, CWS = CJK. The nine pixel values, in order, are LWS-I LWS-+ LWS-- MWS-I MWS-+
Each takes ideal, stretch (+), and shrink (-) values in pixels." 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--) (ekp--param-apply lws-i lws-+ lws-- mws-i mws-+ mws-- cws-i cws-+ cws--)
(setq ekp--params-explicit t)) (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))))) (aset ekp--str-type-table c (ekp--str-type-1 str)))))
(defun ekp--str-type-1 (str) (defun ekp--str-type-1 (str)
"Uncached `ekp--str-type'." "Uncached `ekp--str-type' computation for STR."
(cond (cond
;; Whitespace or zero-width characters ;; Whitespace or zero-width characters
((or (string-blank-p str) (= (string-width str) 0)) 'space) ((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)))))) (ekp--str-type (ekp--box-edge-char box t))))))
(defun ekp--glue-type (prev-box-type curr-box-type) (defun ekp--glue-type (prev-box-type curr-box-type)
"Glue type between boxes: `lws', `mws', `cws' or `nws'. "Return the glue type between PREV-BOX-TYPE and CURR-BOX-TYPE.
Lws means whitespace between latin words; cws between cjk chars; It is `lws', `mws', `cws' or `nws'. Lws means whitespace between
mws between cjk and latin; nws means no whitespace. Space boxes latin words; cws between cjk chars; mws between cjk and latin; nws
\(preserved whitespace) need no additional glue." means no whitespace. Space boxes (preserved whitespace) need no
additional glue."
(let ((before (cdr prev-box-type)) (let ((before (cdr prev-box-type))
(after (car curr-box-type))) (after (car curr-box-type)))
(if before (if before
@ -544,19 +550,22 @@ Japanese typesetting. Stored as a string of characters."
all))) all)))
(defun ekp--box-no-line-start-p (box box-type) (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) (or (eq (car box-type) 'cjk-close)
(and (> (length box) 0) (and (> (length box) 0)
(aref ekp--extra-nls-table (aref box 0))) (aref ekp--extra-nls-table (aref box 0)))
(ekp--box-pure-set-p box ekp--no-line-start-char-list))) (ekp--box-pure-set-p box ekp--no-line-start-char-list)))
(defun ekp--box-no-line-end-p (box box-type) (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) (or (eq (cdr box-type) 'cjk-open)
(ekp--box-pure-set-p box ekp--no-line-end-char-list))) (ekp--box-pure-set-p box ekp--no-line-end-char-list)))
(defun ekp--compute-glue-types (boxes boxes-types hyphen-positions) (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)) (let* ((n (length boxes))
(glues (make-vector n nil)) (glues (make-vector n nil))
prev-type) prev-type)
@ -570,18 +579,21 @@ Japanese typesetting. Stored as a string of characters."
glues)) glues))
(defun ekp-glue-ideal-pixel (type) (defun ekp-glue-ideal-pixel (type)
"Return the ideal glue pixel width for glue TYPE."
(cond ((or (null type) (eq 'nws type)) 0) (cond ((or (null type) (eq 'nws type)) 0)
((eq 'lws type) ekp-lws-ideal-pixel) ((eq 'lws type) ekp-lws-ideal-pixel)
((eq 'mws type) ekp-mws-ideal-pixel) ((eq 'mws type) ekp-mws-ideal-pixel)
((eq 'cws type) ekp-cws-ideal-pixel))) ((eq 'cws type) ekp-cws-ideal-pixel)))
(defun ekp-glue-min-pixel (type) (defun ekp-glue-min-pixel (type)
"Return the minimum glue pixel width for glue TYPE."
(cond ((or (null type) (eq 'nws type)) 0) (cond ((or (null type) (eq 'nws type)) 0)
((eq 'lws type) ekp-lws-min-pixel) ((eq 'lws type) ekp-lws-min-pixel)
((eq 'mws type) ekp-mws-min-pixel) ((eq 'mws type) ekp-mws-min-pixel)
((eq 'cws type) ekp-cws-min-pixel))) ((eq 'cws type) ekp-cws-min-pixel)))
(defun ekp-glue-max-pixel (type) (defun ekp-glue-max-pixel (type)
"Return the maximum glue pixel width for glue TYPE."
(cond ((or (null type) (eq 'nws type)) 0) (cond ((or (null type) (eq 'nws type)) 0)
((eq 'lws type) ekp-lws-max-pixel) ((eq 'lws type) ekp-lws-max-pixel)
((eq 'mws type) ekp-mws-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))))) ((eq 'cws type) (plist-get params :cws-stretch)))))
(defun ekp--para-glue-min (para type) (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-ideal para type)
(ekp--para-glue-shrink para type))) (ekp--para-glue-shrink para type)))
(defun ekp--para-glue-max (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-ideal para type)
(ekp--para-glue-stretch 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))) (and box-type (eq (car box-type) 'space)))
(defun ekp--tail-protrude-pixel (box box-type) (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) (if (not ekp-protrusion)
0 0
(let* ((tail-type (cdr box-type)) (let* ((tail-type (cdr box-type))
@ -745,7 +758,7 @@ glyph shared across paragraphs is measured only once."
0)))) 0))))
(defun ekp--line-edge-release (para _start end) (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 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 line breaks at a hyphenation point, otherwise the last non-space
box's. 0 when `ekp-protrusion' was off at paragraph build time." 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))) (t 0)))
(defun ekp--line-spec (para line-index measure) (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. MEASURE is the paragraph measure passed to the justify call.
`ekp-parshape' takes precedence; its last entry repeats. Otherwise `ekp-parshape' takes precedence; its last entry repeats. Otherwise
`ekp-first-line-indent' shifts line 0. WIDTH never drops below 1." `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 ;;;; Paragraph Accessors
(defun ekp--boxes (string) (defun ekp--boxes (string)
"Return the boxes of STRING's paragraph."
(ekp-para-boxes (ekp--get-para string))) (ekp-para-boxes (ekp--get-para string)))
(defun ekp--boxes-widths (string) (defun ekp--boxes-widths (string)
"Return the box pixel widths of STRING's paragraph."
(ekp-para-boxes-widths (ekp--get-para string))) (ekp-para-boxes-widths (ekp--get-para string)))
(defun ekp--glues-types (string) (defun ekp--glues-types (string)
"Return the glue types of STRING's paragraph."
(ekp-para-glues-types (ekp--get-para string))) (ekp-para-glues-types (ekp--get-para string)))
(defun ekp--ideal-prefixs (string) (defun ekp--ideal-prefixs (string)
"Return the ideal prefix sums of STRING's paragraph."
(ekp-para-ideal-prefixs (ekp--get-para string))) (ekp-para-ideal-prefixs (ekp--get-para string)))
(defun ekp--min-prefixs (string) (defun ekp--min-prefixs (string)
"Return the minimum prefix sums of STRING's paragraph."
(ekp-para-min-prefixs (ekp--get-para string))) (ekp-para-min-prefixs (ekp--get-para string)))
(defun ekp--max-prefixs (string) (defun ekp--max-prefixs (string)
"Return the maximum prefix sums of STRING's paragraph."
(ekp-para-max-prefixs (ekp--get-para string))) (ekp-para-max-prefixs (ekp--get-para string)))
(defun ekp--hyphen-pixel (string) (defun ekp--hyphen-pixel (string)
"Return the hyphen pixel width of STRING's paragraph."
(ekp-para-hyphen-pixel (ekp--get-para string))) (ekp-para-hyphen-pixel (ekp--get-para string)))
(defun ekp--hyphen-positions (string) (defun ekp--hyphen-positions (string)
"Return the hyphen positions of STRING's paragraph."
(ekp-para-hyphen-positions (ekp--get-para string))) (ekp-para-hyphen-positions (ekp--get-para string)))
;;;; K-P Badness and Demerits ;;;; 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))))))) (min ekp--infinite-badness (* 100 (expt (abs ratio) 3)))))))
(defun ekp--compute-fitness-class (adjustment-pixel flexibility-pixel) (defun ekp--compute-fitness-class (adjustment-pixel flexibility-pixel)
"Classify line tightness into fitness class (0-3). "Classify line tightness from ADJUSTMENT-PIXEL and FLEXIBILITY-PIXEL.
0=tight (shrunk), 1=decent, 2=loose, 3=very-loose." Return the fitness class 0-3: 0=tight (shrunk), 1=decent, 2=loose,
3=very-loose."
(if (<= flexibility-pixel 0) (if (<= flexibility-pixel 0)
1 ; default to decent 1 ; default to decent
(let ((ratio (/ (float adjustment-pixel) flexibility-pixel))) (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. "Compute K-P demerits for a line break.
BADNESS is the line badness, PENALTY is break penalty (e.g., hyphen). BADNESS is the line badness, PENALTY is break penalty (e.g., hyphen).
PREV-FITNESS and CURR-FITNESS are fitness classes of adjacent lines. 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." Returns total demerits for this break."
(let* (;; Base demerits: (linepenalty + badness)² (let* (;; Base demerits: (linepenalty + badness)²
(base (expt (+ ekp-line-penalty badness) 2)) (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) ;;;; Shared Line Measurement (O(1) via prefix arrays)
(defun ekp--gaps-between (para i k) (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." Counts glue indices I+1 .. K-1 using precomputed prefix counts."
(let ((lp (ekp-para-lws-prefixs para)) (let ((lp (ekp-para-lws-prefixs para))
(mp (ekp-para-mws-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))))) (- (aref cp k) (aref cp j)))))
(defun ekp--line-ideal-pixel (para i k) (defun ekp--line-ideal-pixel (para i k)
"Ideal width of line I..K: box+glue ideals, minus leading glue and "Return the ideal width of line I..K in PARA.
stripped space-box runs, plus hyphen width when the line hyphenates." 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)) (let* ((ip (ekp-para-ideal-prefixs para))
(raw (- (aref ip k) (aref ip i) (raw (- (aref ip k) (aref ip i)
(aref (ekp-para-glue-ideals para) 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))) (if (zerop ekp-looseness) line-pixel (cons line-pixel ekp-looseness)))
(defun ekp--dp-cache-elisp (para line-pixel) (defun ekp--dp-cache-elisp (para line-pixel)
"Pure Elisp DP implementation. Returns and caches the dp-result plist. "Return and cache the dp-result plist for PARA at LINE-PIXEL.
Looseness and parshape need the (position × line-count) DP; a plain This pure-Elisp DP path handles looseness and parshape via the
first-line indent is handled by the 1D pass (line 0 = start at box 0)." 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) (if (or (/= ekp-looseness 0) ekp-parshape)
(ekp--dp-cache-elisp-loose para line-pixel) (ekp--dp-cache-elisp-loose para line-pixel)
(let ((dp-result (or (ekp--dp-run-1d para line-pixel nil) (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))) dp-result)))
(defun ekp--hyphen-flags (hyphen-positions n) (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))) (let ((v (make-bool-vector (max n 1) nil)))
(dotimes (j (length hyphen-positions)) (dotimes (j (length hyphen-positions))
(aset v (aref hyphen-positions j) t)) (aset v (aref hyphen-positions j) t))
v)) v))
(defun ekp--dp-run-1d (para line-pixel allow-emergency) (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 Returns the dp-result plist, or nil when the paragraph end is
unreachable (only possible when ALLOW-EMERGENCY is nil)." unreachable (only possible when ALLOW-EMERGENCY is nil)."
(let* ((boxes (ekp-para-boxes para)) (let* ((boxes (ekp-para-boxes para))
@ -1334,7 +1360,11 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
end-with-hyphenp prev-hyphen-count end-with-hyphenp prev-hyphen-count
&optional line-gaps) &optional line-gaps)
"Record an emergency (over/underfull atomic-run) break at K from I. "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 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). \(nil for single boxes, which render via the single-box path).
Only replaces an existing entry when strictly better." 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))))) (if end-with-hyphenp (1+ prev-hyphen-count) 0)))))
(defun ekp--dp-trace-breaks (backptrs n) (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)) (let ((breaks (list n))
(index n)) (index n))
(while (> index 0) (while (> index 0)
@ -1373,16 +1403,18 @@ Only replaces an existing entry when strictly better."
;; whose line count is closest to the target. ;; whose line count is closest to the target.
(defun ekp--dp-cache-elisp-loose (para line-pixel) (defun ekp--dp-cache-elisp-loose (para line-pixel)
"Elisp DP tracking all line counts, for `ekp-looseness' support. "Run the DP over PARA at LINE-PIXEL, tracking all line counts.
Two passes like the 1D engine: strict first, then with emergency For `ekp-looseness' and parshape support. Two passes like the 1D
breaks when no valid layout exists." engine: strict first, then emergency breaks when no layout is valid."
(let ((dp-result (or (ekp--dp-run-loose para line-pixel nil) (let ((dp-result (or (ekp--dp-run-loose para line-pixel nil)
(ekp--dp-run-loose para line-pixel t)))) (ekp--dp-run-loose para line-pixel t))))
(puthash (ekp--dp-key line-pixel) dp-result (ekp-para-dp-cache para)) (puthash (ekp--dp-key line-pixel) dp-result (ekp-para-dp-cache para))
dp-result)) dp-result))
(defun ekp--dp-run-loose (para line-pixel allow-emergency) (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)) (let* ((boxes (ekp-para-boxes para))
(n (length boxes)) (n (length boxes))
(hyphen-pixel (ekp-para-hyphen-pixel para)) (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) (defun ekp--dp-loose-relax (states counts-at k lines i prev-dem candidate)
"Relax state (K . LINES) with CANDIDATE from position I. "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)) (let* ((key (cons k lines))
(total (+ prev-dem (nth 0 candidate))) (total (+ prev-dem (nth 0 candidate)))
(existing (gethash key states))) (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))))) (t (ekp--dp-cache-elisp para line-pixel)))))
(defun ekp--lines-data-from-breaks (para line-pixel breaks) (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 Per-line widths (first-line indent) must mirror the DP exactly, or
the reconstructed rests overfill the indented line." the reconstructed rests overfill the indented line."
(let ((start 0) (idx 0) rests gapss) (let ((start 0) (idx 0) rests gapss)
@ -1647,7 +1682,7 @@ the reconstructed rests overfill the indented line."
(cons (nreverse rests) (nreverse gapss)))) (cons (nreverse rests) (nreverse gapss))))
(defun ekp--store-c-result (para line-pixel breaks cost) (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)) (let* ((data (ekp--lines-data-from-breaks para line-pixel breaks))
(dp-result (list :rests (car data) (dp-result (list :rests (car data)
:gaps (cdr data) :gaps (cdr data)
@ -1658,7 +1693,7 @@ the reconstructed rests overfill the indented line."
dp-result)) dp-result))
(defun ekp--prepare-para-for-c (para line-pixel) (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) (vector (ekp-para-ideal-prefixs para)
(ekp-para-min-prefixs para) (ekp-para-min-prefixs para)
(ekp-para-max-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)))) (cdr (ekp--line-spec para 0 line-pixel))))
(defun ekp--dp-cache-via-c (para 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 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." runs the pure DP. Falls back to Elisp when the C call fails."
(ekp--c-sync-params) (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)))) (ekp--store-c-result para line-pixel c-breaks c-cost))))
(defun ekp--dp-cache-batch (strings line-pixel) (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. Returns list of dp-results in the same order as STRINGS.
Only computes strings that aren't already cached." Only computes strings that aren't already cached."
(let* ((paras (mapcar #'ekp--get-para strings)) (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))) data)))
(defun ekp-total-cost (string line-pixel) (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)) (ekp-dp-data string line-pixel :cost))
(defun ekp-line-breaks (string line-pixel) (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)) (ekp-dp-data string line-pixel :breaks))
;;; Line Glue Distribution ;;; 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) (defun ekp--compute-glue-pixels (para glues-types gaps-distribution stretch-p)
"Compute actual glue pixels from GLUES-TYPES and GAPS-DISTRIBUTION. "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))) (let ((latin-adj (car (nth 0 gaps-distribution)))
(latin-extra (cdr (nth 0 gaps-distribution))) (latin-extra (cdr (nth 0 gaps-distribution)))
(mix-adj (car (nth 1 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))) glues-types)))
(defun ekp--line-glue-single-box (line-pixel box-width hyphen-p hyphen-pixel) (defun ekp--line-glue-single-box (line-pixel box-width hyphen-p hyphen-pixel)
"Compute glues for a single-box line. "Compute glues for a single-box line of width LINE-PIXEL.
The trailing filler is clamped at 0 for overfull boxes." 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)))) (let ((trailing (- line-pixel box-width (if hyphen-p hyphen-pixel 0))))
(list 0 (max 0 trailing)))) (list 0 (max 0 trailing))))
(defun ekp--line-glue-last-line (para glues-types ideal-pixel line-pixel) (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) (append '(0)
(mapcar (lambda (type) (ekp--para-glue-ideal para type)) glues-types) (mapcar (lambda (type) (ekp--para-glue-ideal para type)) glues-types)
(list (max 0 (- line-pixel ideal-pixel))))) (list (max 0 (- line-pixel ideal-pixel)))))
(defun ekp--line-glue-normal (para glues-types rest-pixel gaps-list) (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) (if (= rest-pixel 0)
(append '(0) (mapcar (lambda (type) (ekp--para-glue-ideal para type)) (append '(0) (mapcar (lambda (type) (ekp--para-glue-ideal para type))
glues-types) glues-types)
@ -2193,7 +2234,7 @@ The output is lossless with respect to STRING:
(defun ekp--validate-width (line-pixel) (defun ekp--validate-width (line-pixel)
"Signal a user error unless LINE-PIXEL is a positive integer." "Signal a user error unless LINE-PIXEL is a positive integer."
(unless (and (integerp line-pixel) (> line-pixel 0)) (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))) line-pixel)))
(defun ekp-pixel-justify (string 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))) most-positive-fixnum)))
(defun ekp--ternary-search-optimal-width (strings min-pixel max-pixel) (defun ekp--ternary-search-optimal-width (strings min-pixel max-pixel)
"Find optimal width in [MIN-PIXEL, MAX-PIXEL] using ternary search. "Find the optimal width for STRINGS in [MIN-PIXEL, MAX-PIXEL].
Returns the pixel width with minimum average cost." Use ternary search; return the pixel width with minimum average cost."
(let ((lo min-pixel) (let ((lo min-pixel)
(hi max-pixel)) (hi max-pixel))
;; Ternary search: O(log n) iterations ;; Ternary search: O(log n) iterations
@ -2276,7 +2317,7 @@ Returns (justified-text . optimal-pixel)."
(ekp--validate-width min-pixel) (ekp--validate-width min-pixel)
(ekp--validate-width max-pixel) (ekp--validate-width max-pixel)
(when (> min-pixel 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)) min-pixel max-pixel))
(let* ((strings (split-string string "\n")) (let* ((strings (split-string string "\n"))
;; Pre-warm caches ;; Pre-warm caches

View File

@ -247,10 +247,19 @@
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
(erase-buffer) (erase-buffer)
(ekp-showcase-mode) (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) (when (and (display-graphic-p)
(find-font (font-spec :family "Cascadia Next SC"))) (find-font (font-spec :family "Cascadia Next SC")))
(face-remap-add-relative 'default :family "Cascadia Next SC")) (add-face-text-property (point-min) (point-max)
(insert (ekp-showcase--sample)) '(:family "Cascadia Next SC") t))
(goto-char (point-min)) (goto-char (point-min))
(ekp-showcase--refresh))) (ekp-showcase--refresh)))
(pop-to-buffer buf) (pop-to-buffer buf)