feat: typography wave — dictionary HYPHENMIN, JIS kinsoku, fast indent
Three quality gaps closed, one of them unlocking the C engine for the most common Chinese configuration: - first-line indent now runs on the 1D DP and the C engine (module 1.5: ekp-c-break-with-arrays grows a 15th arg FIRST-LINE-WIDTH). A line starts at box 0 exactly when i = 0, so no (position x line-count) state is needed; only true parshape and looseness still take the 2D Elisp path. Indented Chinese sample, w=400: C engine 82 ms (this configuration previously bypassed C entirely and ran the heaviest Elisp DP). The C-result reconstruction mirrors the per-line width, and an equivalence test pins indent == the equivalent parshape across engines. - LEFTHYPHENMIN / RIGHTHYPHENMIN are parsed from dictionaries and applied (en_US declares 2/3; the hardcoded 2/2 allowed breaks like "gen-cy" that the dictionary forbids). Explicit overrides still win; partial overrides keep the dictionary's other side. - JIS X 4051 line-start prohibition for small kana, the prolonged sound mark and iteration marks (new defcustom ekp-cjk-no-line-start-extra, char-table backed): っょー々 can no longer start a line. Pure break-permission change; spacing and the DP are untouched. Also fixed in passing: - ekp--split-with-hyphen extracted regexp groups AFTER resolving the hyphenator; dictionary compilation clobbers the match data (latent since the resolver was hoisted; exposed by the HYPHENMIN parser) — groups are captured first now. - ekp--first-indent-pixel goes through the width cache; it runs once per rendered line and was re-measuring the reference glyph every time (C path was 397 ms before, 82 ms after). 4 new ERT tests (92 total); fuzz 300/300 against C 1.5. C module must be rebuilt: make -C ekp_c clean all Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
bed359d439
commit
3fc05c1405
@ -112,9 +112,13 @@ E.g., \"a1bc2\" -> letters=\"abc\", values=(0 1 0 2)."
|
|||||||
(list letters start (cl-subseq values start end))))))
|
(list letters start (cl-subseq values start end))))))
|
||||||
|
|
||||||
(defun ekp-hyphen--compile (path)
|
(defun ekp-hyphen--compile (path)
|
||||||
"Compile dictionary at PATH into ekp-hyphen struct."
|
"Compile dictionary at PATH into ekp-hyphen struct.
|
||||||
|
Honors the dictionary's LEFTHYPHENMIN / RIGHTHYPHENMIN declarations
|
||||||
|
\(minimum characters kept before/after any break — e.g. en_US
|
||||||
|
declares 2/3, so \"quick-ly\" is not a valid break); absent
|
||||||
|
declarations default to 2/2."
|
||||||
(let ((patterns (make-hash-table :test 'equal))
|
(let ((patterns (make-hash-table :test 'equal))
|
||||||
(maxlen 0))
|
(maxlen 0) (left 2) (right 2))
|
||||||
(with-temp-buffer
|
(with-temp-buffer
|
||||||
(insert-file-contents path)
|
(insert-file-contents path)
|
||||||
(forward-line 1) ; skip encoding line
|
(forward-line 1) ; skip encoding line
|
||||||
@ -124,7 +128,15 @@ E.g., \"a1bc2\" -> letters=\"abc\", values=(0 1 0 2)."
|
|||||||
(skip (or (string-empty-p line)
|
(skip (or (string-empty-p line)
|
||||||
(string-match-p "^[%#]\\|HYPHENMIN" line)
|
(string-match-p "^[%#]\\|HYPHENMIN" line)
|
||||||
(string-match-p "/" line)))) ; skip alt patterns
|
(string-match-p "/" line)))) ; skip alt patterns
|
||||||
(unless skip
|
(cond
|
||||||
|
((string-match "^\\(LEFT\\|RIGHT\\)HYPHENMIN[ \t]*\\([0-9]+\\)"
|
||||||
|
line)
|
||||||
|
(let ((n (string-to-number (match-string 2 line))))
|
||||||
|
(if (equal (match-string 1 line) "LEFT")
|
||||||
|
(setq left n)
|
||||||
|
(setq right n))))
|
||||||
|
(skip nil)
|
||||||
|
(t
|
||||||
;; Handle ^^XX hex escapes
|
;; Handle ^^XX hex escapes
|
||||||
(setq line (replace-regexp-in-string
|
(setq line (replace-regexp-in-string
|
||||||
"\\^\\^\\([0-9a-fA-F]\\{2\\}\\)"
|
"\\^\\^\\([0-9a-fA-F]\\{2\\}\\)"
|
||||||
@ -133,12 +145,12 @@ E.g., \"a1bc2\" -> letters=\"abc\", values=(0 1 0 2)."
|
|||||||
line))
|
line))
|
||||||
(when-let ((parsed (ekp-hyphen--parse-pattern line)))
|
(when-let ((parsed (ekp-hyphen--parse-pattern line)))
|
||||||
(puthash (car parsed) (cdr parsed) patterns)
|
(puthash (car parsed) (cdr parsed) patterns)
|
||||||
(setq maxlen (max maxlen (length (car parsed)))))))
|
(setq maxlen (max maxlen (length (car parsed))))))))
|
||||||
(forward-line 1)))
|
(forward-line 1)))
|
||||||
(ekp-hyphen--create :patterns patterns
|
(ekp-hyphen--create :patterns patterns
|
||||||
:cache (make-hash-table :test 'equal)
|
:cache (make-hash-table :test 'equal)
|
||||||
:maxlen maxlen
|
:maxlen maxlen
|
||||||
:left 2 :right 2)))
|
:left left :right right)))
|
||||||
|
|
||||||
;;; Hyphenation Algorithm
|
;;; Hyphenation Algorithm
|
||||||
|
|
||||||
@ -178,7 +190,9 @@ E.g., \"a1bc2\" -> letters=\"abc\", values=(0 1 0 2)."
|
|||||||
|
|
||||||
(defun ekp-hyphen-create (&optional lang file left right)
|
(defun ekp-hyphen-create (&optional lang file left right)
|
||||||
"Create hyphenator for LANG or dictionary FILE.
|
"Create hyphenator for LANG or dictionary FILE.
|
||||||
LEFT/RIGHT: min chars before/after breaks (default 2)."
|
LEFT/RIGHT override the minimum characters kept before/after breaks;
|
||||||
|
by default the dictionary's own LEFTHYPHENMIN/RIGHTHYPHENMIN apply
|
||||||
|
\(2/2 when it declares none)."
|
||||||
(let ((path (or (and lang (ekp-hyphen--resolve-lang lang)) file)))
|
(let ((path (or (and lang (ekp-hyphen--resolve-lang lang)) file)))
|
||||||
(unless path (error "No dictionary for: %s" lang))
|
(unless path (error "No dictionary for: %s" lang))
|
||||||
(let ((h (or (gethash path ekp-hyphen--cache)
|
(let ((h (or (gethash path ekp-hyphen--cache)
|
||||||
@ -188,7 +202,8 @@ LEFT/RIGHT: min chars before/after breaks (default 2)."
|
|||||||
(ekp-hyphen--create :patterns (ekp-hyphen-patterns h)
|
(ekp-hyphen--create :patterns (ekp-hyphen-patterns h)
|
||||||
:cache (ekp-hyphen-cache h)
|
:cache (ekp-hyphen-cache h)
|
||||||
:maxlen (ekp-hyphen-maxlen h)
|
:maxlen (ekp-hyphen-maxlen h)
|
||||||
:left (or left 2) :right (or right 2))
|
:left (or left (ekp-hyphen-left h))
|
||||||
|
:right (or right (ekp-hyphen-right h)))
|
||||||
h))))
|
h))))
|
||||||
|
|
||||||
(defun ekp-hyphen-positions (h word)
|
(defun ekp-hyphen-positions (h word)
|
||||||
|
|||||||
@ -363,7 +363,7 @@ after CALLBACK returns."
|
|||||||
(defalias 'ekp-c-module-reload #'ekp--module-reload
|
(defalias 'ekp-c-module-reload #'ekp--module-reload
|
||||||
"Load MODULE from a temp copy to allow rebuilding.")
|
"Load MODULE from a temp copy to allow rebuilding.")
|
||||||
|
|
||||||
(defconst ekp-c-module-required-version "1.4"
|
(defconst ekp-c-module-required-version "1.5"
|
||||||
"Minimum C module version compatible with this Elisp code.")
|
"Minimum C module version compatible with this Elisp code.")
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
|
|||||||
141
ekp.el
141
ekp.el
@ -370,34 +370,39 @@ Returns (boxes-vector . hyphen-positions-vector)."
|
|||||||
(hyphenator 'unset)
|
(hyphenator 'unset)
|
||||||
(idx 0) new-boxes hyphen-idxs)
|
(idx 0) new-boxes hyphen-idxs)
|
||||||
(dolist (box (append boxes nil))
|
(dolist (box (append boxes nil))
|
||||||
(if (and (string-match word-re box)
|
(let ((parts nil))
|
||||||
;; Never hyphenate inside a no-break span (verbatim atoms)
|
(when (and (string-match word-re box)
|
||||||
(null (text-property-not-all 0 (length box)
|
;; Never hyphenate inside a no-break span
|
||||||
'ekp-no-break nil box))
|
(null (text-property-not-all 0 (length box)
|
||||||
(or (and (eq hyphenator 'unset)
|
'ekp-no-break nil box)))
|
||||||
(setq hyphenator
|
;; Extract the groups BEFORE resolving the hyphenator:
|
||||||
(condition-case nil
|
;; dictionary compilation runs regexps of its own and
|
||||||
(ekp-hyphen-create ekp-latin-lang)
|
;; clobbers the match data.
|
||||||
(error nil))))
|
(let ((left (match-string 1 box))
|
||||||
hyphenator))
|
(word (match-string 2 box))
|
||||||
;; Latin word: apply hyphenation
|
(right (match-string 3 box)))
|
||||||
(let* ((left (match-string 1 box))
|
(when (eq hyphenator 'unset)
|
||||||
(word (match-string 2 box))
|
(setq hyphenator
|
||||||
(right (match-string 3 box))
|
(condition-case nil
|
||||||
(parts (ekp-hyphen-boxes hyphenator word))
|
(ekp-hyphen-create ekp-latin-lang)
|
||||||
(n (length parts)))
|
(error nil))))
|
||||||
(when (> (length left) 0)
|
(when hyphenator
|
||||||
(setcar parts (concat left (car parts))))
|
(setq parts (ekp-hyphen-boxes hyphenator word))
|
||||||
(when (> (length right) 0)
|
(when (> (length left) 0)
|
||||||
(setcar (last parts)
|
(setcar parts (concat left (car parts))))
|
||||||
(concat (car (last parts)) right)))
|
(when (> (length right) 0)
|
||||||
(push parts new-boxes)
|
(setcar (last parts)
|
||||||
(dotimes (i n)
|
(concat (car (last parts)) right))))))
|
||||||
(when (< i (1- n)) (push idx hyphen-idxs))
|
(if parts
|
||||||
(cl-incf idx)))
|
;; Latin word: hyphenated into syllable boxes
|
||||||
;; Non-Latin: single box
|
(let ((n (length parts)))
|
||||||
(push (list box) new-boxes)
|
(push parts new-boxes)
|
||||||
(cl-incf idx)))
|
(dotimes (i n)
|
||||||
|
(when (< i (1- n)) (push idx hyphen-idxs))
|
||||||
|
(cl-incf idx)))
|
||||||
|
;; Non-Latin box, or hyphenation unavailable
|
||||||
|
(push (list box) new-boxes)
|
||||||
|
(cl-incf idx))))
|
||||||
(cons (vconcat (apply #'append (nreverse new-boxes)))
|
(cons (vconcat (apply #'append (nreverse new-boxes)))
|
||||||
(vconcat (nreverse hyphen-idxs)))))
|
(vconcat (nreverse hyphen-idxs)))))
|
||||||
|
|
||||||
@ -498,6 +503,36 @@ are covered by the `cjk-open' class.")
|
|||||||
(defconst ekp--no-line-start-char-list (append ekp--no-line-start-chars nil))
|
(defconst ekp--no-line-start-char-list (append ekp--no-line-start-chars nil))
|
||||||
(defconst ekp--no-line-end-char-list (append ekp--no-line-end-chars nil))
|
(defconst ekp--no-line-end-char-list (append ekp--no-line-end-chars nil))
|
||||||
|
|
||||||
|
(defcustom ekp-cjk-no-line-start-extra
|
||||||
|
(concat "ぁぃぅぇぉっゃゅょゎゕゖァィゥェォッャュョヮヵヶ"
|
||||||
|
"ㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ"
|
||||||
|
"ーゝゞヽヾ々〻")
|
||||||
|
"CJK letters that must not start a line (JIS X 4051 kinsoku).
|
||||||
|
Small kana, the prolonged sound mark ー and iteration marks are
|
||||||
|
letters for spacing purposes but are line-start-prohibited in
|
||||||
|
Japanese typesetting. Stored as a string of characters."
|
||||||
|
:type 'string
|
||||||
|
:group 'ekp)
|
||||||
|
|
||||||
|
(defvar ekp--extra-nls-table nil
|
||||||
|
"Char-table view of `ekp-cjk-no-line-start-extra' (fast lookup).")
|
||||||
|
|
||||||
|
(defun ekp--extra-nls-rebuild (chars)
|
||||||
|
"Rebuild `ekp--extra-nls-table' from the string CHARS."
|
||||||
|
(let ((table (make-char-table 'ekp-extra-nls)))
|
||||||
|
(dolist (c (append (if (stringp chars) chars "") nil))
|
||||||
|
(aset table c t))
|
||||||
|
(setq ekp--extra-nls-table table)))
|
||||||
|
|
||||||
|
(ekp--extra-nls-rebuild ekp-cjk-no-line-start-extra)
|
||||||
|
|
||||||
|
(add-variable-watcher
|
||||||
|
'ekp-cjk-no-line-start-extra
|
||||||
|
(lambda (_sym new op _where)
|
||||||
|
(when (memq op '(set let unlet makunbound))
|
||||||
|
(ekp--extra-nls-rebuild new)
|
||||||
|
(setq ekp--last-para nil))))
|
||||||
|
|
||||||
(defun ekp--box-pure-set-p (box chars)
|
(defun ekp--box-pure-set-p (box chars)
|
||||||
"Non-nil when BOX is non-empty and every char is a member of CHARS."
|
"Non-nil when BOX is non-empty and every char is a member of CHARS."
|
||||||
(let ((len (length box)) (i 0) (all t))
|
(let ((len (length box)) (i 0) (all t))
|
||||||
@ -511,6 +546,8 @@ are covered by the `cjk-open' class.")
|
|||||||
(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."
|
||||||
(or (eq (car box-type) 'cjk-close)
|
(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)))
|
(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)
|
||||||
@ -651,6 +688,7 @@ are derived per string)."
|
|||||||
(and ekp-protrusion ekp-protrusion-ratios)
|
(and ekp-protrusion ekp-protrusion-ratios)
|
||||||
ekp-parshape
|
ekp-parshape
|
||||||
ekp-first-line-indent
|
ekp-first-line-indent
|
||||||
|
ekp-cjk-no-line-start-extra
|
||||||
(if (and ekp--params-explicit (ekp--params-set-p))
|
(if (and ekp--params-explicit (ekp--params-set-p))
|
||||||
(list ekp-lws-ideal-pixel ekp-lws-stretch-pixel
|
(list ekp-lws-ideal-pixel ekp-lws-stretch-pixel
|
||||||
ekp-lws-shrink-pixel ekp-mws-ideal-pixel
|
ekp-lws-shrink-pixel ekp-mws-ideal-pixel
|
||||||
@ -720,16 +758,13 @@ box's. 0 when `ekp-protrusion' was off at paragraph build time."
|
|||||||
(or ekp-ragged-stretch-pixel
|
(or ekp-ragged-stretch-pixel
|
||||||
(max 1 (* 8 (or ekp-lws-ideal-pixel 1)))))
|
(max 1 (* 8 (or ekp-lws-ideal-pixel 1)))))
|
||||||
|
|
||||||
(defun ekp--parshape-active-p ()
|
|
||||||
"Non-nil when per-line widths are in effect (parshape or indent)."
|
|
||||||
(or ekp-parshape ekp-first-line-indent))
|
|
||||||
|
|
||||||
(defun ekp--first-indent-pixel (para)
|
(defun ekp--first-indent-pixel (para)
|
||||||
"Resolve `ekp-first-line-indent' to pixels for PARA."
|
"Resolve `ekp-first-line-indent' to pixels for PARA.
|
||||||
|
Goes through the width cache: this runs for every rendered line."
|
||||||
(cond
|
(cond
|
||||||
((numberp ekp-first-line-indent) ekp-first-line-indent)
|
((numberp ekp-first-line-indent) ekp-first-line-indent)
|
||||||
(ekp-first-line-indent
|
(ekp-first-line-indent
|
||||||
(* 2 (string-pixel-width
|
(* 2 (ekp--measured-width
|
||||||
(propertize "字" 'face
|
(propertize "字" 'face
|
||||||
(list :family (ekp-para-cjk-font para))))))
|
(list :family (ekp-para-cjk-font para))))))
|
||||||
(t 0)))
|
(t 0)))
|
||||||
@ -1093,9 +1128,9 @@ width, so results at different looseness values must not alias
|
|||||||
|
|
||||||
(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.
|
"Pure Elisp DP implementation. Returns and caches the dp-result plist.
|
||||||
Looseness and per-line widths (parshape/first-line indent) need the
|
Looseness and parshape need the (position × line-count) DP; a plain
|
||||||
\(position × line-count) DP."
|
first-line indent is handled by the 1D pass (line 0 = start at box 0)."
|
||||||
(if (or (/= ekp-looseness 0) (ekp--parshape-active-p))
|
(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)
|
||||||
(ekp--dp-run-1d para line-pixel t))))
|
(ekp--dp-run-1d para line-pixel t))))
|
||||||
@ -1132,6 +1167,10 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(breaks-ok (ekp-para-breaks-allowed para))
|
(breaks-ok (ekp-para-breaks-allowed para))
|
||||||
(tail-protrudes (ekp-para-tail-protrudes para))
|
(tail-protrudes (ekp-para-tail-protrudes para))
|
||||||
(hyphen-protrude (ekp-para-hyphen-protrude para))
|
(hyphen-protrude (ekp-para-hyphen-protrude para))
|
||||||
|
;; First-line indent shrinks line 0 only; a line starts at
|
||||||
|
;; box 0 exactly when i = 0, so the 1D DP handles it without
|
||||||
|
;; the (position × line-count) state (parshape still needs it).
|
||||||
|
(first-line-pixel (cdr (ekp--line-spec para 0 line-pixel)))
|
||||||
(params (ekp-para-glue-params para))
|
(params (ekp-para-glue-params para))
|
||||||
(lws-stretch (plist-get params :lws-stretch))
|
(lws-stretch (plist-get params :lws-stretch))
|
||||||
(mws-stretch (plist-get params :mws-stretch))
|
(mws-stretch (plist-get params :mws-stretch))
|
||||||
@ -1176,7 +1215,7 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(end-with-hyphenp (aref hyph-flags (1- k)))
|
(end-with-hyphenp (aref hyph-flags (1- k)))
|
||||||
(hyph-w (if end-with-hyphenp hyphen-pixel 0))
|
(hyph-w (if end-with-hyphenp hyphen-pixel 0))
|
||||||
;; right-edge protrusion releases width at this k
|
;; right-edge protrusion releases width at this k
|
||||||
(lw (+ line-pixel
|
(lw (+ (if (= i 0) first-line-pixel line-pixel)
|
||||||
(if end-with-hyphenp
|
(if end-with-hyphenp
|
||||||
hyphen-protrude
|
hyphen-protrude
|
||||||
(aref tail-protrudes k))))
|
(aref tail-protrudes k))))
|
||||||
@ -1557,10 +1596,10 @@ CANDIDATE is (DEM-DELTA REST GAPS FITNESS HYPHEN-COUNT)."
|
|||||||
(and ekp-use-c-module
|
(and ekp-use-c-module
|
||||||
(boundp 'ekp-c-module-loaded) ekp-c-module-loaded
|
(boundp 'ekp-c-module-loaded) ekp-c-module-loaded
|
||||||
(fboundp 'ekp-c-break-with-arrays)
|
(fboundp 'ekp-c-break-with-arrays)
|
||||||
;; looseness and per-line widths need the (position × line-count)
|
;; looseness and parshape need the (position × line-count) DP,
|
||||||
;; DP, Elisp only
|
;; Elisp only; first-line indent is a scalar the C engine takes
|
||||||
(= ekp-looseness 0)
|
(= ekp-looseness 0)
|
||||||
(not (ekp--parshape-active-p))))
|
(not ekp-parshape)))
|
||||||
|
|
||||||
(defun ekp--c-sync-params ()
|
(defun ekp--c-sync-params ()
|
||||||
"Push current K-P penalty settings to the C module."
|
"Push current K-P penalty settings to the C module."
|
||||||
@ -1589,10 +1628,13 @@ 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, matching the DP's metrics.
|
||||||
(let ((start 0) rests gapss)
|
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)
|
||||||
(dolist (end breaks)
|
(dolist (end breaks)
|
||||||
(push (- (+ line-pixel (ekp--line-edge-release para start end))
|
(push (- (+ (cdr (ekp--line-spec para idx line-pixel))
|
||||||
|
(ekp--line-edge-release para start end))
|
||||||
(ekp--line-ideal-pixel para start end))
|
(ekp--line-ideal-pixel para start end))
|
||||||
rests)
|
rests)
|
||||||
(push (if (or (= end (1+ start))
|
(push (if (or (= end (1+ start))
|
||||||
@ -1600,7 +1642,8 @@ If `ekp-use-c-module' is non-nil and the C module is available (and
|
|||||||
nil
|
nil
|
||||||
(ekp--gaps-between para start end))
|
(ekp--gaps-between para start end))
|
||||||
gapss)
|
gapss)
|
||||||
(setq start end))
|
(setq start end
|
||||||
|
idx (1+ idx)))
|
||||||
(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)
|
||||||
@ -1615,7 +1658,7 @@ If `ekp-use-c-module' is non-nil and the C module is available (and
|
|||||||
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 14-element vector for the C batch API."
|
"Prepare PARA data 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)
|
||||||
@ -1629,7 +1672,8 @@ If `ekp-use-c-module' is non-nil and the C module is available (and
|
|||||||
(ekp-para-trail-spaces para)
|
(ekp-para-trail-spaces para)
|
||||||
(ekp-para-forbidden-positions para)
|
(ekp-para-forbidden-positions para)
|
||||||
(ekp-para-tail-protrudes para)
|
(ekp-para-tail-protrudes para)
|
||||||
(ekp-para-hyphen-protrude para)))
|
(ekp-para-hyphen-protrude para)
|
||||||
|
(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 using the C module with PARA's precomputed arrays.
|
||||||
@ -1650,7 +1694,8 @@ runs the pure DP. Falls back to Elisp when the C call fails."
|
|||||||
(ekp-para-trail-spaces para)
|
(ekp-para-trail-spaces para)
|
||||||
(ekp-para-forbidden-positions para)
|
(ekp-para-forbidden-positions para)
|
||||||
(ekp-para-tail-protrudes para)
|
(ekp-para-tail-protrudes para)
|
||||||
(ekp-para-hyphen-protrude para)))
|
(ekp-para-hyphen-protrude para)
|
||||||
|
(cdr (ekp--line-spec para 0 line-pixel))))
|
||||||
(c-breaks (car result))
|
(c-breaks (car result))
|
||||||
(c-cost (cdr result)))
|
(c-cost (cdr result)))
|
||||||
(if (null c-breaks)
|
(if (null c-breaks)
|
||||||
|
|||||||
48
ekp_c/ekp.c
48
ekp_c/ekp.c
@ -320,11 +320,13 @@ static emacs_value Fekp_c_thread_count(emacs_env *env, ptrdiff_t nargs,
|
|||||||
/*
|
/*
|
||||||
* ekp-c-break-with-arrays: Pure DP with Elisp-provided prefix arrays
|
* ekp-c-break-with-arrays: Pure DP with Elisp-provided prefix arrays
|
||||||
*
|
*
|
||||||
* Args: (ideal-prefix min-prefix max-prefix glue-ideals glue-shrinks glue-stretches
|
* Args: (ideal-prefix min-prefix max-prefix glue-ideals glue-shrinks
|
||||||
* hyphen-positions hyphen-width line-width)
|
* glue-stretches hyphen-positions hyphen-width line-width
|
||||||
|
* lead-spaces trail-spaces forbidden-positions tail-protrudes
|
||||||
|
* hyphen-protrude first-line-width)
|
||||||
*
|
*
|
||||||
* All 6 arrays must have consistent sizes:
|
* Array sizes must be consistent:
|
||||||
* - ideal/min/max-prefix: (n+1) elements
|
* - ideal/min/max-prefix, lead/trail-spaces, tail-protrudes: n+1
|
||||||
* - glue-ideals/shrinks/stretches: n elements
|
* - glue-ideals/shrinks/stretches: n elements
|
||||||
*
|
*
|
||||||
* Returns: (breaks . total-cost) where breaks is a list of box indices.
|
* Returns: (breaks . total-cost) where breaks is a list of box indices.
|
||||||
@ -337,7 +339,7 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
|
|||||||
{
|
{
|
||||||
(void)data;
|
(void)data;
|
||||||
|
|
||||||
if (!ekp_global || nargs < 14)
|
if (!ekp_global || nargs < 15)
|
||||||
return env->intern(env, "nil");
|
return env->intern(env, "nil");
|
||||||
|
|
||||||
/* Get prefix array sizes (n+1 elements) */
|
/* Get prefix array sizes (n+1 elements) */
|
||||||
@ -405,6 +407,7 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
int32_t hyphen_protrude = env->extract_integer(env, args[13]);
|
int32_t hyphen_protrude = env->extract_integer(env, args[13]);
|
||||||
|
int32_t first_line_width = env->extract_integer(env, args[14]);
|
||||||
|
|
||||||
/* Forbidden break positions (sorted gap indices, may be empty) */
|
/* Forbidden break positions (sorted gap indices, may be empty) */
|
||||||
ptrdiff_t forb_count = env->vec_size(env, args[11]);
|
ptrdiff_t forb_count = env->vec_size(env, args[11]);
|
||||||
@ -427,7 +430,7 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
|
|||||||
hyph_width, line_width,
|
hyph_width, line_width,
|
||||||
lead_spaces, trail_spaces,
|
lead_spaces, trail_spaces,
|
||||||
forb_pos, (forb_pos && forb_count > 0) ? (size_t)forb_count : 0,
|
forb_pos, (forb_pos && forb_count > 0) ? (size_t)forb_count : 0,
|
||||||
tail_pro, hyphen_protrude);
|
tail_pro, hyphen_protrude, first_line_width);
|
||||||
|
|
||||||
free(ideal_prefix); free(min_prefix); free(max_prefix);
|
free(ideal_prefix); free(min_prefix); free(max_prefix);
|
||||||
free(glue_ideals); free(glue_shrinks); free(glue_stretches);
|
free(glue_ideals); free(glue_shrinks); free(glue_stretches);
|
||||||
@ -469,7 +472,8 @@ static bool extract_paragraph_data(
|
|||||||
int32_t *hyph_width, int32_t *line_width,
|
int32_t *hyph_width, int32_t *line_width,
|
||||||
int32_t **lead_spaces, int32_t **trail_spaces,
|
int32_t **lead_spaces, int32_t **trail_spaces,
|
||||||
int32_t **forb_pos, ptrdiff_t *forb_count,
|
int32_t **forb_pos, ptrdiff_t *forb_count,
|
||||||
int32_t **tail_pro, int32_t *hyphen_protrude)
|
int32_t **tail_pro, int32_t *hyphen_protrude,
|
||||||
|
int32_t *first_line_width)
|
||||||
{
|
{
|
||||||
ptrdiff_t prefix_len = env->vec_size(env, args[0]);
|
ptrdiff_t prefix_len = env->vec_size(env, args[0]);
|
||||||
if (prefix_len <= 1)
|
if (prefix_len <= 1)
|
||||||
@ -541,6 +545,7 @@ static bool extract_paragraph_data(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*hyphen_protrude = env->extract_integer(env, args[13]);
|
*hyphen_protrude = env->extract_integer(env, args[13]);
|
||||||
|
*first_line_width = env->extract_integer(env, args[14]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -595,15 +600,15 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
|
|||||||
for (ptrdiff_t p = 0; p < para_count; p++) {
|
for (ptrdiff_t p = 0; p < para_count; p++) {
|
||||||
emacs_value para_vec = env->vec_get(env, args[0], p);
|
emacs_value para_vec = env->vec_get(env, args[0], p);
|
||||||
|
|
||||||
/* Extract 14 arguments from this paragraph's vector */
|
/* Extract 15 arguments from this paragraph's vector */
|
||||||
emacs_value para_args[14];
|
emacs_value para_args[15];
|
||||||
for (int i = 0; i < 14; i++) {
|
for (int i = 0; i < 15; i++) {
|
||||||
para_args[i] = env->vec_get(env, para_vec, i);
|
para_args[i] = env->vec_get(env, para_vec, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t n;
|
size_t n;
|
||||||
ptrdiff_t hyph_count, forb_count;
|
ptrdiff_t hyph_count, forb_count;
|
||||||
int32_t hyph_width, line_width, hyphen_protrude;
|
int32_t hyph_width, line_width, hyphen_protrude, first_line_width;
|
||||||
|
|
||||||
if (!extract_paragraph_data(env, para_args,
|
if (!extract_paragraph_data(env, para_args,
|
||||||
&all_ideal[p], &all_min[p], &all_max[p],
|
&all_ideal[p], &all_min[p], &all_max[p],
|
||||||
@ -612,7 +617,8 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
|
|||||||
&hyph_width, &line_width,
|
&hyph_width, &line_width,
|
||||||
&all_lead[p], &all_trail[p],
|
&all_lead[p], &all_trail[p],
|
||||||
&all_forb[p], &forb_count,
|
&all_forb[p], &forb_count,
|
||||||
&all_pro[p], &hyphen_protrude)) {
|
&all_pro[p], &hyphen_protrude,
|
||||||
|
&first_line_width)) {
|
||||||
/* Cleanup on failure */
|
/* Cleanup on failure */
|
||||||
for (ptrdiff_t j = 0; j < p; j++) {
|
for (ptrdiff_t j = 0; j < p; j++) {
|
||||||
free(all_ideal[j]); free(all_min[j]); free(all_max[j]);
|
free(all_ideal[j]); free(all_min[j]); free(all_max[j]);
|
||||||
@ -644,6 +650,7 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
|
|||||||
(all_forb[p] && forb_count > 0) ? (size_t)forb_count : 0;
|
(all_forb[p] && forb_count > 0) ? (size_t)forb_count : 0;
|
||||||
inputs[p].tail_protrudes = all_pro[p];
|
inputs[p].tail_protrudes = all_pro[p];
|
||||||
inputs[p].hyphen_protrude = hyphen_protrude;
|
inputs[p].hyphen_protrude = hyphen_protrude;
|
||||||
|
inputs[p].first_line_width = first_line_width;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Process all paragraphs in parallel */
|
/* Process all paragraphs in parallel */
|
||||||
@ -768,7 +775,7 @@ MEASURE-FUNC: function that takes a string and returns pixel width\n\n\
|
|||||||
Returns (BREAKS . TOTAL-COST) where BREAKS is list of break positions.\n\n\
|
Returns (BREAKS . TOTAL-COST) where BREAKS is list of break positions.\n\n\
|
||||||
(fn STRING HYPHENATOR-INDEX LINE-WIDTH MEASURE-FUNC)");
|
(fn STRING HYPHENATOR-INDEX LINE-WIDTH MEASURE-FUNC)");
|
||||||
|
|
||||||
defun(env, "ekp-c-break-with-arrays", 14, 14, Fekp_c_break_with_arrays,
|
defun(env, "ekp-c-break-with-arrays", 15, 15, Fekp_c_break_with_arrays,
|
||||||
"Break lines using Elisp's pre-computed prefix arrays (preferred API).\n\n\
|
"Break lines using Elisp's pre-computed prefix arrays (preferred API).\n\n\
|
||||||
IDEAL-PREFIX: vector of ideal width prefix sums (n+1 elements)\n\
|
IDEAL-PREFIX: vector of ideal width prefix sums (n+1 elements)\n\
|
||||||
MIN-PREFIX: vector of min width prefix sums (n+1 elements)\n\
|
MIN-PREFIX: vector of min width prefix sums (n+1 elements)\n\
|
||||||
@ -780,11 +787,16 @@ HYPHEN-POS: vector of hyphenable box indices (sorted)\n\
|
|||||||
HYPHEN-WIDTH: pixel width of hyphen character\n\
|
HYPHEN-WIDTH: pixel width of hyphen character\n\
|
||||||
LINE-WIDTH: target line width in pixels\n\
|
LINE-WIDTH: target line width in pixels\n\
|
||||||
LEAD-SPACES: vector (n+1) of space-box run widths starting at box i\n\
|
LEAD-SPACES: vector (n+1) of space-box run widths starting at box i\n\
|
||||||
TRAIL-SPACES: vector (n+1) of space-box run widths ending at box k-1\n\n\
|
TRAIL-SPACES: vector (n+1) of space-box run widths ending at box k-1\n\
|
||||||
|
FORBIDDEN-POS: vector of gap indices where breaking is forbidden (sorted)\n\
|
||||||
|
TAIL-PROTRUDES: vector (n+1) of right-edge protrusion pixels per gap\n\
|
||||||
|
HYPHEN-PROTRUDE: protrusion pixels for the soft hyphen\n\
|
||||||
|
FIRST-LINE-WIDTH: width of line 0 (first-line indent); <=0 = LINE-WIDTH\n\n\
|
||||||
Returns (BREAKS . TOTAL-COST) where BREAKS is list of box indices.\n\
|
Returns (BREAKS . TOTAL-COST) where BREAKS is list of box indices.\n\
|
||||||
This API ensures C uses Elisp's font-dependent measurements.\n\n\
|
This API ensures C uses Elisp's font-dependent measurements.\n\n\
|
||||||
(fn IDEAL-PREFIX MIN-PREFIX MAX-PREFIX GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES \
|
(fn IDEAL-PREFIX MIN-PREFIX MAX-PREFIX GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES \
|
||||||
HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH LEAD-SPACES TRAIL-SPACES)");
|
HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH LEAD-SPACES TRAIL-SPACES FORBIDDEN-POS \
|
||||||
|
TAIL-PROTRUDES HYPHEN-PROTRUDE FIRST-LINE-WIDTH)");
|
||||||
|
|
||||||
defun(env, "ekp-c-version", 0, 0, Fekp_c_version,
|
defun(env, "ekp-c-version", 0, 0, Fekp_c_version,
|
||||||
"Return EKP C module version string.");
|
"Return EKP C module version string.");
|
||||||
@ -794,10 +806,8 @@ HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH LEAD-SPACES TRAIL-SPACES)");
|
|||||||
|
|
||||||
defun(env, "ekp-c-break-batch", 1, 1, Fekp_c_break_batch,
|
defun(env, "ekp-c-break-batch", 1, 1, Fekp_c_break_batch,
|
||||||
"Break multiple paragraphs in parallel.\n\n\
|
"Break multiple paragraphs in parallel.\n\n\
|
||||||
PARAGRAPHS: vector of paragraph data, each element is a vector of 11 items:\n\
|
PARAGRAPHS: vector of paragraph data, each element a vector of the\n\
|
||||||
[ideal-prefix min-prefix max-prefix glue-ideals glue-shrinks\n\
|
same 15 items `ekp-c-break-with-arrays' takes, in the same order.\n\n\
|
||||||
glue-stretches hyphen-positions hyphen-width line-width\n\
|
|
||||||
lead-spaces trail-spaces]\n\n\
|
|
||||||
Returns vector of (BREAKS . COST) for each paragraph.\n\
|
Returns vector of (BREAKS . COST) for each paragraph.\n\
|
||||||
This is the high-performance API for multi-paragraph processing.\n\n\
|
This is the high-performance API for multi-paragraph processing.\n\n\
|
||||||
(fn PARAGRAPHS)");
|
(fn PARAGRAPHS)");
|
||||||
|
|||||||
@ -170,6 +170,10 @@ typedef struct {
|
|||||||
/* Dimensions */
|
/* Dimensions */
|
||||||
size_t n; /* box count */
|
size_t n; /* box count */
|
||||||
int32_t line_width;
|
int32_t line_width;
|
||||||
|
/* Width of line 0 (first-line indent support); equals line_width
|
||||||
|
* when no indent is active. In the forward DP a line starts at
|
||||||
|
* box 0 exactly when i == 0, so this needs no extra state. */
|
||||||
|
int32_t first_line_width;
|
||||||
|
|
||||||
/* K-P parameters */
|
/* K-P parameters */
|
||||||
int line_penalty;
|
int line_penalty;
|
||||||
@ -281,7 +285,10 @@ static void dp_process_position(
|
|||||||
int32_t *line_counts)
|
int32_t *line_counts)
|
||||||
{
|
{
|
||||||
size_t n = in->n;
|
size_t n = in->n;
|
||||||
int32_t line_width = in->line_width;
|
/* Line 0 (i == 0) may have a different width: first-line indent */
|
||||||
|
int32_t line_width = (i == 0 && in->first_line_width > 0)
|
||||||
|
? in->first_line_width
|
||||||
|
: in->line_width;
|
||||||
|
|
||||||
/* Get leading glue for line starting at i */
|
/* Get leading glue for line starting at i */
|
||||||
int32_t lead_ideal = (in->glue_ideals && i < n) ? in->glue_ideals[i] : 0;
|
int32_t lead_ideal = (in->glue_ideals && i < n) ? in->glue_ideals[i] : 0;
|
||||||
@ -670,10 +677,13 @@ ekp_result_t *ekp_break_with_prefixes(
|
|||||||
const int32_t *forbidden_positions,
|
const int32_t *forbidden_positions,
|
||||||
size_t forbidden_count,
|
size_t forbidden_count,
|
||||||
const int32_t *tail_protrudes,
|
const int32_t *tail_protrudes,
|
||||||
int32_t hyphen_protrude)
|
int32_t hyphen_protrude,
|
||||||
|
int32_t first_line_width)
|
||||||
{
|
{
|
||||||
if (!ideal_prefix || !min_prefix || !max_prefix || n == 0 || line_width <= 0)
|
if (!ideal_prefix || !min_prefix || !max_prefix || n == 0 || line_width <= 0)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
if (first_line_width <= 0)
|
||||||
|
first_line_width = line_width;
|
||||||
|
|
||||||
/* Allocate DP arrays */
|
/* Allocate DP arrays */
|
||||||
double *demerits = malloc((n + 1) * sizeof(double));
|
double *demerits = malloc((n + 1) * sizeof(double));
|
||||||
@ -728,6 +738,7 @@ ekp_result_t *ekp_break_with_prefixes(
|
|||||||
.trail_spaces = trail_spaces,
|
.trail_spaces = trail_spaces,
|
||||||
.n = n,
|
.n = n,
|
||||||
.line_width = line_width,
|
.line_width = line_width,
|
||||||
|
.first_line_width = first_line_width,
|
||||||
.line_penalty = lp,
|
.line_penalty = lp,
|
||||||
.hyphen_penalty = hp,
|
.hyphen_penalty = hp,
|
||||||
.fitness_penalty = fp,
|
.fitness_penalty = fp,
|
||||||
@ -847,7 +858,8 @@ static void batch_worker(void *arg)
|
|||||||
in->hyphen_width, in->line_width,
|
in->hyphen_width, in->line_width,
|
||||||
in->lead_spaces, in->trail_spaces,
|
in->lead_spaces, in->trail_spaces,
|
||||||
in->forbidden_positions, in->forbidden_count,
|
in->forbidden_positions, in->forbidden_count,
|
||||||
in->tail_protrudes, in->hyphen_protrude);
|
in->tail_protrudes, in->hyphen_protrude,
|
||||||
|
in->first_line_width);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -877,7 +889,8 @@ ekp_result_t **ekp_break_batch(ekp_batch_input_t *inputs, size_t count)
|
|||||||
in->hyphen_width, in->line_width,
|
in->hyphen_width, in->line_width,
|
||||||
in->lead_spaces, in->trail_spaces,
|
in->lead_spaces, in->trail_spaces,
|
||||||
in->forbidden_positions, in->forbidden_count,
|
in->forbidden_positions, in->forbidden_count,
|
||||||
in->tail_protrudes, in->hyphen_protrude);
|
in->tail_protrudes, in->hyphen_protrude,
|
||||||
|
in->first_line_width);
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@ -896,7 +909,8 @@ ekp_result_t **ekp_break_batch(ekp_batch_input_t *inputs, size_t count)
|
|||||||
in->hyphen_width, in->line_width,
|
in->hyphen_width, in->line_width,
|
||||||
in->lead_spaces, in->trail_spaces,
|
in->lead_spaces, in->trail_spaces,
|
||||||
in->forbidden_positions, in->forbidden_count,
|
in->forbidden_positions, in->forbidden_count,
|
||||||
in->tail_protrudes, in->hyphen_protrude);
|
in->tail_protrudes, in->hyphen_protrude,
|
||||||
|
in->first_line_width);
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
/* Version */
|
/* Version */
|
||||||
#define EKP_VERSION_MAJOR 1
|
#define EKP_VERSION_MAJOR 1
|
||||||
#define EKP_VERSION_MINOR 4
|
#define EKP_VERSION_MINOR 5
|
||||||
|
|
||||||
/* Limits */
|
/* Limits */
|
||||||
#define EKP_MAX_PATTERN_LEN 64
|
#define EKP_MAX_PATTERN_LEN 64
|
||||||
@ -270,7 +270,8 @@ ekp_result_t *ekp_break_with_prefixes(
|
|||||||
const int32_t *forbidden_positions,
|
const int32_t *forbidden_positions,
|
||||||
size_t forbidden_count,
|
size_t forbidden_count,
|
||||||
const int32_t *tail_protrudes,
|
const int32_t *tail_protrudes,
|
||||||
int32_t hyphen_protrude);
|
int32_t hyphen_protrude,
|
||||||
|
int32_t first_line_width);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Batch input for parallel processing
|
* Batch input for parallel processing
|
||||||
@ -293,6 +294,7 @@ typedef struct {
|
|||||||
size_t forbidden_count;
|
size_t forbidden_count;
|
||||||
const int32_t *tail_protrudes; /* nullable, n+1 elements */
|
const int32_t *tail_protrudes; /* nullable, n+1 elements */
|
||||||
int32_t hyphen_protrude;
|
int32_t hyphen_protrude;
|
||||||
|
int32_t first_line_width; /* width of line 0; <=0 = line_width */
|
||||||
} ekp_batch_input_t;
|
} ekp_batch_input_t;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -79,8 +79,10 @@ Used to verify no content is lost by justification."
|
|||||||
(let ((h (ekp-hyphen-create "en_US")))
|
(let ((h (ekp-hyphen-create "en_US")))
|
||||||
(should (equal (ekp-hyphen-boxes h "hyphenation")
|
(should (equal (ekp-hyphen-boxes h "hyphenation")
|
||||||
'("hy" "phen" "ation")))
|
'("hy" "phen" "ation")))
|
||||||
|
;; RIGHTHYPHENMIN 3 (declared by en_US): no "gen-cy" break,
|
||||||
|
;; "cy" would leave only 2 characters after the hyphen.
|
||||||
(should (equal (ekp-hyphen-boxes h "emergency")
|
(should (equal (ekp-hyphen-boxes h "emergency")
|
||||||
'("emer" "gen" "cy")))
|
'("emer" "gency")))
|
||||||
;; Words with no break points come back whole
|
;; Words with no break points come back whole
|
||||||
(should (equal (ekp-hyphen-boxes h "cat") '("cat")))))
|
(should (equal (ekp-hyphen-boxes h "cat") '("cat")))))
|
||||||
|
|
||||||
@ -696,6 +698,72 @@ kept returning the paragraph resolved under the previous style."
|
|||||||
(ekp-clear-caches)
|
(ekp-clear-caches)
|
||||||
(should (equal-including-properties out-j (ekp-pixel-justify s 30)))))
|
(should (equal-including-properties out-j (ekp-pixel-justify s 30)))))
|
||||||
|
|
||||||
|
;;;; Typography quality (M3 wave)
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-hyphenmin-honored ()
|
||||||
|
"Dictionary LEFTHYPHENMIN/RIGHTHYPHENMIN are parsed and applied.
|
||||||
|
en_US declares 2/3; the old hardcoded 2/2 allowed \"quick-ly\"."
|
||||||
|
(let ((h (ekp-hyphen-create "en_US")))
|
||||||
|
(should (= (ekp-hyphen-left h) 2))
|
||||||
|
(should (= (ekp-hyphen-right h) 3))
|
||||||
|
(dolist (w '("quickly" "mainly" "activity" "hyphenation" "reader"))
|
||||||
|
(dolist (p (ekp-hyphen-positions h w))
|
||||||
|
(should (>= p 2))
|
||||||
|
(should (<= p (- (length w) 3)))))
|
||||||
|
;; explicit overrides still work, partial override keeps the
|
||||||
|
;; dictionary's value for the other side
|
||||||
|
(let ((h2 (ekp-hyphen-create "en_US" nil 1 1))
|
||||||
|
(h3 (ekp-hyphen-create "en_US" nil 4 nil)))
|
||||||
|
(should (= (ekp-hyphen-left h2) 1))
|
||||||
|
(should (= (ekp-hyphen-right h2) 1))
|
||||||
|
(should (= (ekp-hyphen-left h3) 4))
|
||||||
|
(should (= (ekp-hyphen-right h3) 3)))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-jis-kinsoku-line-start ()
|
||||||
|
"Small kana and the prolonged sound mark never start a line.
|
||||||
|
JIS X 4051 line-start prohibition for っゃー々 etc."
|
||||||
|
(ekp-tests--with-clean-state
|
||||||
|
(let ((text "がっこうへいくよラーメンをたべたいなあそうかなぁいいなぁと")
|
||||||
|
(forbidden (append ekp-cjk-no-line-start-extra nil)))
|
||||||
|
(dolist (w '(20 28 40 60))
|
||||||
|
(let ((out (ekp-pixel-justify text w)))
|
||||||
|
(dolist (line (split-string out "\n"))
|
||||||
|
(when (> (length line) 0)
|
||||||
|
(should-not (memq (aref line 0) forbidden)))))))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-first-line-indent-1d-matches-parshape ()
|
||||||
|
"A plain first-line indent equals the equivalent parshape.
|
||||||
|
The indent runs on the 1D DP (and the C engine); parshape runs on
|
||||||
|
the (position × line-count) Elisp DP — they must agree."
|
||||||
|
(ekp-tests--with-clean-state
|
||||||
|
(let* ((s "首行缩进等价性检查内容足够长会断行几次的样子哦")
|
||||||
|
(w 30)
|
||||||
|
(via-indent (let ((ekp-first-line-indent 8))
|
||||||
|
(ekp-pixel-justify s w)))
|
||||||
|
(via-parshape (progn
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(let ((ekp-parshape (list (cons 8 (- w 8))
|
||||||
|
(cons 0 w))))
|
||||||
|
(ekp-pixel-justify s w)))))
|
||||||
|
(should (equal-including-properties via-indent via-parshape)))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-first-line-indent-c-parity ()
|
||||||
|
"First-line indent: C and Elisp engines agree byte-for-byte."
|
||||||
|
(skip-unless (ekp-tests--c-available))
|
||||||
|
(ekp-tests--with-clean-state
|
||||||
|
(dolist (indent '(t 8))
|
||||||
|
(let ((ekp-first-line-indent indent))
|
||||||
|
(dolist (s '("中文首行缩进检查内容足够长会断行几次的样子哦"
|
||||||
|
"Mixed 混排 first line indent parity with words"))
|
||||||
|
(dolist (w '(30 60 90))
|
||||||
|
(let* ((via-c (let ((ekp-use-c-module t))
|
||||||
|
(ekp-pixel-justify s w)))
|
||||||
|
(_ (ekp-clear-caches))
|
||||||
|
(via-el (let ((ekp-use-c-module nil))
|
||||||
|
(ekp-pixel-justify s w))))
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(should (equal-including-properties via-c via-el)))))))))
|
||||||
|
|
||||||
(provide 'ekp-tests)
|
(provide 'ekp-tests)
|
||||||
|
|
||||||
;;; ekp-tests.el ends here
|
;;; ekp-tests.el ends here
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user