feat: right-edge protrusion — hanging punctuation (C 1.4)
ekp-protrusion enables microtype-style character protrusion at the right edge: a line ending in fullwidth punctuation (。、」…) hangs the whitespace half of the glyph past the flush edge — visually identical to CLREQ line-end punctuation compression — and latin periods/commas/quotes and soft hyphens protrude by their configured ratios (ekp-protrusion-ratios). Mechanics: per-box tail protrusion, folded through trailing space boxes into a per-gap array; each DP candidate widens its effective target (lw = width + release), mirrored exactly in the renderer's per-line effective width and in the C-result reconstruction, so DP, rendering and both engines stay coherent. Left-edge protrusion is documented as out of scope (Emacs cannot render before the line origin). ekp-region reserves the protrusion width in auto layouts (ekp-region-protrusion-reserve). Off by default; all-zero arrays make it a no-op on the shared path. C module 1.4: break-with-arrays 12→14 args (tail-protrudes array, hyphen-protrude scalar), batch vectors 14 elements. Tests: 59 ERT green (deterministic hang check, flush-when-off, protrusion C parity); fuzz 300/300. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
57a3abe853
commit
f6aa64b3b1
@ -59,9 +59,20 @@ by the display engine due to rounding."
|
|||||||
|
|
||||||
;;;; Width
|
;;;; Width
|
||||||
|
|
||||||
|
(defun ekp-region-protrusion-reserve ()
|
||||||
|
"Pixels reserved at the right margin for hanging punctuation.
|
||||||
|
Non-zero only while `ekp-protrusion' is enabled: protruding glyphs
|
||||||
|
extend past the flush edge, so the layout width must leave room."
|
||||||
|
(if ekp-protrusion
|
||||||
|
(max 2 (ceiling (* (alist-get 'cjk-close ekp-protrusion-ratios 0.5)
|
||||||
|
(string-pixel-width "。"))))
|
||||||
|
0))
|
||||||
|
|
||||||
(defun ekp-region--window-pixel (&optional window)
|
(defun ekp-region--window-pixel (&optional window)
|
||||||
"Usable text width in pixels of WINDOW (default: selected window)."
|
"Usable text width in pixels of WINDOW (default: selected window)."
|
||||||
(max 1 (- (window-body-width window t) ekp-region-margin-pixel)))
|
(max 1 (- (window-body-width window t)
|
||||||
|
ekp-region-margin-pixel
|
||||||
|
(ekp-region-protrusion-reserve))))
|
||||||
|
|
||||||
;;;; Pure string transforms
|
;;;; Pure string transforms
|
||||||
|
|
||||||
|
|||||||
@ -347,7 +347,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.3"
|
(defconst ekp-c-module-required-version "1.4"
|
||||||
"Minimum C module version compatible with this Elisp code.")
|
"Minimum C module version compatible with this Elisp code.")
|
||||||
|
|
||||||
(defun ekp-c-module-load ()
|
(defun ekp-c-module-load ()
|
||||||
|
|||||||
144
ekp.el
144
ekp.el
@ -102,6 +102,24 @@ This is what a ragged line may fall short of the target width without
|
|||||||
badness reaching infinity (like \\raggedright with a finite \\rightskip
|
badness reaching infinity (like \\raggedright with a finite \\rightskip
|
||||||
stretch). nil derives 8× the Latin word-space ideal (≈2 em).")
|
stretch). nil derives 8× the Latin word-space ideal (≈2 em).")
|
||||||
|
|
||||||
|
(defvar ekp-protrusion nil
|
||||||
|
"Non-nil enables right-edge character protrusion (hanging punctuation).
|
||||||
|
A line ending in punctuation lets part of that glyph hang past the
|
||||||
|
flush edge, per `ekp-protrusion-ratios' — CLREQ line-end punctuation
|
||||||
|
squeeze and microtype-style hanging periods/hyphens in one mechanism.
|
||||||
|
Left-edge protrusion is not implemented: Emacs cannot render text
|
||||||
|
before the line origin. When enabled, reserve the protrusion width
|
||||||
|
in the layout (see `ekp-region-protrusion-reserve')." )
|
||||||
|
|
||||||
|
(defvar ekp-protrusion-ratios
|
||||||
|
'((cjk-close . 0.5) (latin-close . 0.5) (hyphen . 1.0))
|
||||||
|
"Alist CLASS → RATIO of the glyph width allowed to protrude.
|
||||||
|
`cjk-close': fullwidth closers (。、」); 0.5 hangs exactly the
|
||||||
|
whitespace half of the glyph — visually equivalent to CLREQ line-end
|
||||||
|
compression. `latin-close': chars from `ekp--no-line-start-chars'
|
||||||
|
ending a word (period, comma, quotes). `hyphen': the soft hyphen
|
||||||
|
inserted at a break.")
|
||||||
|
|
||||||
(defvar ekp-looseness 0
|
(defvar ekp-looseness 0
|
||||||
"Target line count offset: 0=optimal, +1=looser (more lines), -1=tighter.
|
"Target line count offset: 0=optimal, +1=looser (more lines), -1=tighter.
|
||||||
When non-zero, a full (position × line-count) dynamic program is run
|
When non-zero, a full (position × line-count) dynamic program is run
|
||||||
@ -137,6 +155,11 @@ when non-zero the C module is bypassed automatically.")
|
|||||||
;; index n (paragraph end) is always allowed. forbidden-positions is
|
;; index n (paragraph end) is always allowed. forbidden-positions is
|
||||||
;; the same information as a sparse int vector for the C bridge.
|
;; the same information as a sparse int vector for the C bridge.
|
||||||
breaks-allowed forbidden-positions
|
breaks-allowed forbidden-positions
|
||||||
|
;; Right-edge protrusion: tail-protrudes[k] = pixels the last
|
||||||
|
;; non-space box before gap k may hang past the flush edge (all
|
||||||
|
;; zeros when `ekp-protrusion' is off); hyphen-protrude = same for
|
||||||
|
;; the soft hyphen at a hyphenated break.
|
||||||
|
tail-protrudes hyphen-protrude
|
||||||
;; Glue params snapshot at para creation time (plist)
|
;; Glue params snapshot at para creation time (plist)
|
||||||
glue-params
|
glue-params
|
||||||
(dp-cache nil :type hash-table))
|
(dp-cache nil :type hash-table))
|
||||||
@ -459,6 +482,7 @@ are derived per string)."
|
|||||||
ekp-latin-lang
|
ekp-latin-lang
|
||||||
ekp-alignment
|
ekp-alignment
|
||||||
ekp-ragged-stretch-pixel
|
ekp-ragged-stretch-pixel
|
||||||
|
(and ekp-protrusion ekp-protrusion-ratios)
|
||||||
(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
|
||||||
@ -497,6 +521,32 @@ reduces the number of `string-pixel-width' calls."
|
|||||||
"Return non-nil if BOX-TYPE describes a whitespace box."
|
"Return non-nil if BOX-TYPE describes a whitespace box."
|
||||||
(and box-type (eq (car box-type) 'space)))
|
(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."
|
||||||
|
(if (not ekp-protrusion)
|
||||||
|
0
|
||||||
|
(let* ((tail-type (cdr box-type))
|
||||||
|
(last-str (substring box -1))
|
||||||
|
(ratio (cond
|
||||||
|
((eq tail-type 'cjk-close)
|
||||||
|
(alist-get 'cjk-close ekp-protrusion-ratios 0))
|
||||||
|
((memq (aref box (1- (length box)))
|
||||||
|
(append ekp--no-line-start-chars nil))
|
||||||
|
(alist-get 'latin-close ekp-protrusion-ratios 0))
|
||||||
|
(t 0))))
|
||||||
|
(if (> ratio 0)
|
||||||
|
(floor (* ratio (string-pixel-width last-str)))
|
||||||
|
0))))
|
||||||
|
|
||||||
|
(defun ekp--line-edge-release (para _start end)
|
||||||
|
"Pixels released at the right edge of the 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."
|
||||||
|
(if (ekp--hyphenate-p (ekp-para-hyphen-positions para) (1- end))
|
||||||
|
(ekp-para-hyphen-protrude para)
|
||||||
|
(aref (ekp-para-tail-protrudes para) end)))
|
||||||
|
|
||||||
(defun ekp--ragged-extra-stretch ()
|
(defun ekp--ragged-extra-stretch ()
|
||||||
"Resolve the per-line flexibility for non-justify alignment."
|
"Resolve the per-line flexibility for non-justify alignment."
|
||||||
(or ekp-ragged-stretch-pixel
|
(or ekp-ragged-stretch-pixel
|
||||||
@ -537,7 +587,13 @@ Computes ALL data in one pass: text, params, and prefix arrays."
|
|||||||
(lead-spaces (make-vector (1+ n) 0))
|
(lead-spaces (make-vector (1+ n) 0))
|
||||||
(trail-spaces (make-vector (1+ n) 0))
|
(trail-spaces (make-vector (1+ n) 0))
|
||||||
(breaks-allowed (make-bool-vector (1+ n) t))
|
(breaks-allowed (make-bool-vector (1+ n) t))
|
||||||
(forbidden nil))
|
(forbidden nil)
|
||||||
|
(tail-protrudes (make-vector (1+ n) 0))
|
||||||
|
(hyphen-protrude
|
||||||
|
(if ekp-protrusion
|
||||||
|
(floor (* (alist-get 'hyphen ekp-protrusion-ratios 0)
|
||||||
|
hyphen-pixel))
|
||||||
|
0)))
|
||||||
;; Break permissions. A gap is unbreakable when:
|
;; Break permissions. A gap is unbreakable when:
|
||||||
;; - kinsoku: the line would end with an opener or start with a
|
;; - kinsoku: the line would end with an opener or start with a
|
||||||
;; closer (full- and halfwidth alike),
|
;; closer (full- and halfwidth alike),
|
||||||
@ -565,6 +621,21 @@ Computes ALL data in one pass: text, params, and prefix arrays."
|
|||||||
(unless (eq (aref glues-types k) 'nws)
|
(unless (eq (aref glues-types k) 'nws)
|
||||||
(aset glues-types k 'nws))))
|
(aset glues-types k 'nws))))
|
||||||
(setq k (1+ k))))
|
(setq k (1+ k))))
|
||||||
|
;; Right-edge protrusion: tail-protrudes[k] = protrusion of the
|
||||||
|
;; last non-space box before gap k (renderer strips trailing
|
||||||
|
;; space boxes, so look through them).
|
||||||
|
(when ekp-protrusion
|
||||||
|
(let ((pro (make-vector (max n 1) 0)))
|
||||||
|
(dotimes (b n)
|
||||||
|
(aset pro b (ekp--tail-protrude-pixel (aref boxes b)
|
||||||
|
(aref boxes-types b))))
|
||||||
|
(let ((k 1))
|
||||||
|
(while (<= k n)
|
||||||
|
(aset tail-protrudes k
|
||||||
|
(if (ekp--space-box-type-p (aref boxes-types (1- k)))
|
||||||
|
(aref tail-protrudes (1- k))
|
||||||
|
(aref pro (1- k))))
|
||||||
|
(setq k (1+ k))))))
|
||||||
;; Single loop for all prefix computations
|
;; Single loop for all prefix computations
|
||||||
(dotimes (i n)
|
(dotimes (i n)
|
||||||
(let* ((box-w (aref boxes-widths i))
|
(let* ((box-w (aref boxes-widths i))
|
||||||
@ -628,6 +699,8 @@ Computes ALL data in one pass: text, params, and prefix arrays."
|
|||||||
:trail-spaces trail-spaces
|
:trail-spaces trail-spaces
|
||||||
:breaks-allowed breaks-allowed
|
:breaks-allowed breaks-allowed
|
||||||
:forbidden-positions (vconcat (nreverse forbidden))
|
:forbidden-positions (vconcat (nreverse forbidden))
|
||||||
|
:tail-protrudes tail-protrudes
|
||||||
|
:hyphen-protrude hyphen-protrude
|
||||||
:glue-params (let ((justify (eq ekp-alignment 'justify)))
|
:glue-params (let ((justify (eq ekp-alignment 'justify)))
|
||||||
(list :lws-ideal ekp-lws-ideal-pixel
|
(list :lws-ideal ekp-lws-ideal-pixel
|
||||||
:lws-stretch (if justify ekp-lws-stretch-pixel 0)
|
:lws-stretch (if justify ekp-lws-stretch-pixel 0)
|
||||||
@ -843,6 +916,8 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(lead-spaces (ekp-para-lead-spaces para))
|
(lead-spaces (ekp-para-lead-spaces para))
|
||||||
(trail-spaces (ekp-para-trail-spaces para))
|
(trail-spaces (ekp-para-trail-spaces para))
|
||||||
(breaks-ok (ekp-para-breaks-allowed para))
|
(breaks-ok (ekp-para-breaks-allowed para))
|
||||||
|
(tail-protrudes (ekp-para-tail-protrudes para))
|
||||||
|
(hyphen-protrude (ekp-para-hyphen-protrude para))
|
||||||
(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))
|
||||||
@ -886,6 +961,11 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(atomic-run (not saw-allowed))
|
(atomic-run (not saw-allowed))
|
||||||
(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
|
||||||
|
(lw (+ line-pixel
|
||||||
|
(if end-with-hyphenp
|
||||||
|
hyphen-protrude
|
||||||
|
(aref tail-protrudes k))))
|
||||||
(raw-ideal (- (aref ideal-prefixs k) ip-i lead-glue-ideal))
|
(raw-ideal (- (aref ideal-prefixs k) ip-i lead-glue-ideal))
|
||||||
(space-w (min raw-ideal
|
(space-w (min raw-ideal
|
||||||
(+ lead-space (aref trail-spaces k))))
|
(+ lead-space (aref trail-spaces k))))
|
||||||
@ -899,20 +979,20 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(cond
|
(cond
|
||||||
;; Line already too long: emergency-record atomic run,
|
;; Line already too long: emergency-record atomic run,
|
||||||
;; then stop extending.
|
;; then stop extending.
|
||||||
((or (> minw line-pixel)
|
((or (> minw lw)
|
||||||
(and is-last (> ideal line-pixel)))
|
(and is-last (> ideal lw)))
|
||||||
(when (and atomic-run allow-emergency)
|
(when (and atomic-run allow-emergency)
|
||||||
(ekp--dp-relax-emergency
|
(ekp--dp-relax-emergency
|
||||||
demerits backptrs rests gaps hyphen-counts
|
demerits backptrs rests gaps hyphen-counts
|
||||||
fitness-classes i k prev-dem
|
fitness-classes i k prev-dem
|
||||||
(- line-pixel ideal) end-with-hyphenp
|
(- lw ideal) end-with-hyphenp
|
||||||
prev-hyphen-count
|
prev-hyphen-count
|
||||||
(unless single-box (ekp--gaps-between para i k))))
|
(unless single-box (ekp--gaps-between para i k))))
|
||||||
(throw 'break nil))
|
(throw 'break nil))
|
||||||
;; Valid break point
|
;; Valid break point
|
||||||
((or (<= minw line-pixel maxw)
|
((or (<= minw lw maxw)
|
||||||
(and is-last (<= ideal line-pixel)))
|
(and is-last (<= ideal lw)))
|
||||||
(let* ((adjustment (- line-pixel ideal))
|
(let* ((adjustment (- lw ideal))
|
||||||
dem line-gaps fitness new-hyphen)
|
dem line-gaps fitness new-hyphen)
|
||||||
(cond
|
(cond
|
||||||
;; Single box line: fixed flexibility of 1
|
;; Single box line: fixed flexibility of 1
|
||||||
@ -929,7 +1009,7 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
end-with-hyphenp prev-hyphen-count))))
|
end-with-hyphenp prev-hyphen-count))))
|
||||||
;; Last line: minimal demerits if reasonably filled
|
;; Last line: minimal demerits if reasonably filled
|
||||||
(is-last
|
(is-last
|
||||||
(let* ((fill-ratio (/ (float ideal) line-pixel))
|
(let* ((fill-ratio (/ (float ideal) lw))
|
||||||
(badness (if (< fill-ratio
|
(badness (if (< fill-ratio
|
||||||
ekp-last-line-min-ratio)
|
ekp-last-line-min-ratio)
|
||||||
(* ekp-last-line-short-penalty
|
(* ekp-last-line-short-penalty
|
||||||
@ -982,7 +1062,7 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(ekp--dp-relax-emergency
|
(ekp--dp-relax-emergency
|
||||||
demerits backptrs rests gaps hyphen-counts
|
demerits backptrs rests gaps hyphen-counts
|
||||||
fitness-classes i k prev-dem
|
fitness-classes i k prev-dem
|
||||||
(- line-pixel ideal) end-with-hyphenp
|
(- lw ideal) end-with-hyphenp
|
||||||
prev-hyphen-count
|
prev-hyphen-count
|
||||||
(unless single-box (ekp--gaps-between para i k)))))
|
(unless single-box (ekp--gaps-between para i k)))))
|
||||||
(setq saw-allowed t)
|
(setq saw-allowed t)
|
||||||
@ -1063,6 +1143,8 @@ breaks when no valid layout exists."
|
|||||||
(lead-spaces (ekp-para-lead-spaces para))
|
(lead-spaces (ekp-para-lead-spaces para))
|
||||||
(trail-spaces (ekp-para-trail-spaces para))
|
(trail-spaces (ekp-para-trail-spaces para))
|
||||||
(breaks-ok (ekp-para-breaks-allowed para))
|
(breaks-ok (ekp-para-breaks-allowed para))
|
||||||
|
(tail-protrudes (ekp-para-tail-protrudes para))
|
||||||
|
(hyphen-protrude (ekp-para-hyphen-protrude para))
|
||||||
(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))
|
||||||
@ -1102,6 +1184,11 @@ breaks when no valid layout exists."
|
|||||||
(end-with-hyphenp
|
(end-with-hyphenp
|
||||||
(ekp--hyphenate-p hyphen-positions (1- k)))
|
(ekp--hyphenate-p hyphen-positions (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
|
||||||
|
(lw (+ line-pixel
|
||||||
|
(if end-with-hyphenp
|
||||||
|
hyphen-protrude
|
||||||
|
(aref tail-protrudes k))))
|
||||||
(raw-ideal (- (aref ideal-prefixs k) ip-i lead-glue-ideal))
|
(raw-ideal (- (aref ideal-prefixs k) ip-i lead-glue-ideal))
|
||||||
(space-w (min raw-ideal
|
(space-w (min raw-ideal
|
||||||
(+ lead-space (aref trail-spaces k))))
|
(+ lead-space (aref trail-spaces k))))
|
||||||
@ -1112,11 +1199,11 @@ breaks when no valid layout exists."
|
|||||||
(maxw (+ (- (aref max-prefixs k) mx-i lead-glue-max
|
(maxw (+ (- (aref max-prefixs k) mx-i lead-glue-max
|
||||||
space-w)
|
space-w)
|
||||||
hyph-w extra-stretch))
|
hyph-w extra-stretch))
|
||||||
(adjustment (- line-pixel ideal))
|
(adjustment (- lw ideal))
|
||||||
candidate)
|
candidate)
|
||||||
(cond
|
(cond
|
||||||
((or (> minw line-pixel)
|
((or (> minw lw)
|
||||||
(and is-last (> ideal line-pixel)))
|
(and is-last (> ideal lw)))
|
||||||
(when (and atomic-run allow-emergency)
|
(when (and atomic-run allow-emergency)
|
||||||
(setq candidate
|
(setq candidate
|
||||||
(list (+ (expt (+ ekp-line-penalty
|
(list (+ (expt (+ ekp-line-penalty
|
||||||
@ -1131,8 +1218,8 @@ breaks when no valid layout exists."
|
|||||||
(ekp--dp-loose-relax states counts-at k (1+ lc) i
|
(ekp--dp-loose-relax states counts-at k (1+ lc) i
|
||||||
prev-dem candidate))
|
prev-dem candidate))
|
||||||
(throw 'break nil))
|
(throw 'break nil))
|
||||||
((or (<= minw line-pixel maxw)
|
((or (<= minw lw maxw)
|
||||||
(and is-last (<= ideal line-pixel)))
|
(and is-last (<= ideal lw)))
|
||||||
(setq candidate
|
(setq candidate
|
||||||
(cond
|
(cond
|
||||||
(single-box
|
(single-box
|
||||||
@ -1146,7 +1233,7 @@ breaks when no valid layout exists."
|
|||||||
end-with-hyphenp prev-hyphen-count)
|
end-with-hyphenp prev-hyphen-count)
|
||||||
adjustment nil 1 nh)))
|
adjustment nil 1 nh)))
|
||||||
(is-last
|
(is-last
|
||||||
(let* ((fill-ratio (/ (float ideal) line-pixel))
|
(let* ((fill-ratio (/ (float ideal) lw))
|
||||||
(badness (if (< fill-ratio
|
(badness (if (< fill-ratio
|
||||||
ekp-last-line-min-ratio)
|
ekp-last-line-min-ratio)
|
||||||
(* ekp-last-line-short-penalty
|
(* ekp-last-line-short-penalty
|
||||||
@ -1287,7 +1374,9 @@ If `ekp-use-c-module' is non-nil and the C module is available (and
|
|||||||
"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)
|
(let ((start 0) rests gapss)
|
||||||
(dolist (end breaks)
|
(dolist (end breaks)
|
||||||
(push (- line-pixel (ekp--line-ideal-pixel para start end)) rests)
|
(push (- (+ line-pixel (ekp--line-edge-release para start end))
|
||||||
|
(ekp--line-ideal-pixel para start end))
|
||||||
|
rests)
|
||||||
(push (if (or (= end (1+ start))
|
(push (if (or (= end (1+ start))
|
||||||
(= end (length (ekp-para-boxes para))))
|
(= end (length (ekp-para-boxes para))))
|
||||||
nil
|
nil
|
||||||
@ -1308,7 +1397,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 12-element vector for the C batch API."
|
"Prepare PARA data as a 14-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)
|
||||||
@ -1320,7 +1409,9 @@ If `ekp-use-c-module' is non-nil and the C module is available (and
|
|||||||
line-pixel
|
line-pixel
|
||||||
(ekp-para-lead-spaces para)
|
(ekp-para-lead-spaces para)
|
||||||
(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-hyphen-protrude para)))
|
||||||
|
|
||||||
(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.
|
||||||
@ -1339,7 +1430,9 @@ runs the pure DP. Falls back to Elisp when the C call fails."
|
|||||||
line-pixel
|
line-pixel
|
||||||
(ekp-para-lead-spaces para)
|
(ekp-para-lead-spaces para)
|
||||||
(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-hyphen-protrude para)))
|
||||||
(c-breaks (car result))
|
(c-breaks (car result))
|
||||||
(c-cost (cdr result)))
|
(c-cost (cdr result)))
|
||||||
(if (null c-breaks)
|
(if (null c-breaks)
|
||||||
@ -1534,6 +1627,11 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
|||||||
nil))
|
nil))
|
||||||
(is-last (>= end boxes-num))
|
(is-last (>= end boxes-num))
|
||||||
(hyphen-p (ekp--hyphenate-p hyphen-positions (1- end)))
|
(hyphen-p (ekp--hyphenate-p hyphen-positions (1- end)))
|
||||||
|
;; right-edge protrusion widens this line's effective target
|
||||||
|
(eff-pixel (+ line-pixel
|
||||||
|
(if hyphen-p
|
||||||
|
(ekp-para-hyphen-protrude para)
|
||||||
|
(aref (ekp-para-tail-protrudes para) end))))
|
||||||
;; DP-consistent metrics (space-box runs excluded, hyphen incl.)
|
;; DP-consistent metrics (space-box runs excluded, hyphen incl.)
|
||||||
(ideal-pixel (ekp--line-ideal-pixel para start end))
|
(ideal-pixel (ekp--line-ideal-pixel para start end))
|
||||||
(max-pixel (let* ((mx (ekp-para-max-prefixs para))
|
(max-pixel (let* ((mx (ekp-para-max-prefixs para))
|
||||||
@ -1556,7 +1654,7 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
|||||||
(cond
|
(cond
|
||||||
;; Single box: just trailing space
|
;; Single box: just trailing space
|
||||||
((= 1 (- end start))
|
((= 1 (- end start))
|
||||||
(ekp--line-glue-single-box line-pixel
|
(ekp--line-glue-single-box eff-pixel
|
||||||
(- ideal-pixel
|
(- ideal-pixel
|
||||||
(if hyphen-p hyphen-pixel 0))
|
(if hyphen-p hyphen-pixel 0))
|
||||||
hyphen-p hyphen-pixel))
|
hyphen-p hyphen-pixel))
|
||||||
@ -1564,15 +1662,15 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
|||||||
;; natural glue widths plus a trailing filler.
|
;; natural glue widths plus a trailing filler.
|
||||||
((or is-last ragged)
|
((or is-last ragged)
|
||||||
(ekp--line-glue-last-line
|
(ekp--line-glue-last-line
|
||||||
para line-glues-types ideal-pixel line-pixel))
|
para line-glues-types ideal-pixel eff-pixel))
|
||||||
;; Emergency underfull line (can't stretch to width):
|
;; Emergency underfull line (can't stretch to width):
|
||||||
;; set glues to max and pad with trailing filler.
|
;; set glues to max and pad with trailing filler.
|
||||||
((< max-pixel line-pixel)
|
((< max-pixel eff-pixel)
|
||||||
(append '(0)
|
(append '(0)
|
||||||
(mapcar (lambda (type)
|
(mapcar (lambda (type)
|
||||||
(ekp--para-glue-max para type))
|
(ekp--para-glue-max para type))
|
||||||
line-glues-types)
|
line-glues-types)
|
||||||
(list (max 0 (- line-pixel max-pixel)))))
|
(list (max 0 (- eff-pixel max-pixel)))))
|
||||||
;; Normal justified line
|
;; Normal justified line
|
||||||
(t
|
(t
|
||||||
(ekp--line-glue-normal para line-glues-types
|
(ekp--line-glue-normal para line-glues-types
|
||||||
|
|||||||
54
ekp_c/ekp.c
54
ekp_c/ekp.c
@ -327,7 +327,7 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
|
|||||||
{
|
{
|
||||||
(void)data;
|
(void)data;
|
||||||
|
|
||||||
if (!ekp_global || nargs < 12)
|
if (!ekp_global || nargs < 14)
|
||||||
return env->intern(env, "nil");
|
return env->intern(env, "nil");
|
||||||
|
|
||||||
/* Get prefix array sizes (n+1 elements) */
|
/* Get prefix array sizes (n+1 elements) */
|
||||||
@ -387,6 +387,15 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
|
|||||||
int32_t hyph_width = env->extract_integer(env, args[7]);
|
int32_t hyph_width = env->extract_integer(env, args[7]);
|
||||||
int32_t line_width = env->extract_integer(env, args[8]);
|
int32_t line_width = env->extract_integer(env, args[8]);
|
||||||
|
|
||||||
|
/* Right-edge protrusion: per-gap array (n+1) and hyphen scalar */
|
||||||
|
int32_t *tail_pro = malloc(prefix_len * sizeof(int32_t));
|
||||||
|
if (tail_pro) {
|
||||||
|
for (ptrdiff_t i = 0; i < prefix_len; i++) {
|
||||||
|
tail_pro[i] = env->extract_integer(env, env->vec_get(env, args[12], i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int32_t hyphen_protrude = env->extract_integer(env, args[13]);
|
||||||
|
|
||||||
/* 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]);
|
||||||
int32_t *forb_pos = NULL;
|
int32_t *forb_pos = NULL;
|
||||||
@ -407,13 +416,15 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
|
|||||||
hyph_pos, hyph_count > 0 ? (size_t)hyph_count : 0,
|
hyph_pos, hyph_count > 0 ? (size_t)hyph_count : 0,
|
||||||
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);
|
||||||
|
|
||||||
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);
|
||||||
free(lead_spaces); free(trail_spaces);
|
free(lead_spaces); free(trail_spaces);
|
||||||
free(hyph_pos);
|
free(hyph_pos);
|
||||||
free(forb_pos);
|
free(forb_pos);
|
||||||
|
free(tail_pro);
|
||||||
|
|
||||||
if (!result)
|
if (!result)
|
||||||
return env->intern(env, "nil");
|
return env->intern(env, "nil");
|
||||||
@ -447,7 +458,8 @@ static bool extract_paragraph_data(
|
|||||||
int32_t **hyph_pos, size_t *n, ptrdiff_t *hyph_count,
|
int32_t **hyph_pos, size_t *n, ptrdiff_t *hyph_count,
|
||||||
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)
|
||||||
{
|
{
|
||||||
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)
|
||||||
@ -512,6 +524,14 @@ static bool extract_paragraph_data(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*tail_pro = malloc(prefix_len * sizeof(int32_t));
|
||||||
|
if (*tail_pro) {
|
||||||
|
for (ptrdiff_t i = 0; i < prefix_len; i++) {
|
||||||
|
(*tail_pro)[i] = env->extract_integer(env, env->vec_get(env, args[12], i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*hyphen_protrude = env->extract_integer(env, args[13]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -550,13 +570,14 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
|
|||||||
int32_t **all_lead = calloc(para_count, sizeof(int32_t *));
|
int32_t **all_lead = calloc(para_count, sizeof(int32_t *));
|
||||||
int32_t **all_trail = calloc(para_count, sizeof(int32_t *));
|
int32_t **all_trail = calloc(para_count, sizeof(int32_t *));
|
||||||
int32_t **all_forb = calloc(para_count, sizeof(int32_t *));
|
int32_t **all_forb = calloc(para_count, sizeof(int32_t *));
|
||||||
|
int32_t **all_pro = calloc(para_count, sizeof(int32_t *));
|
||||||
|
|
||||||
if (!inputs || !all_ideal || !all_min || !all_max ||
|
if (!inputs || !all_ideal || !all_min || !all_max ||
|
||||||
!all_glue_i || !all_glue_sh || !all_glue_st || !all_hyph ||
|
!all_glue_i || !all_glue_sh || !all_glue_st || !all_hyph ||
|
||||||
!all_lead || !all_trail || !all_forb) {
|
!all_lead || !all_trail || !all_forb || !all_pro) {
|
||||||
free(inputs); free(all_ideal); free(all_min); free(all_max);
|
free(inputs); free(all_ideal); free(all_min); free(all_max);
|
||||||
free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph);
|
free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph);
|
||||||
free(all_lead); free(all_trail); free(all_forb);
|
free(all_lead); free(all_trail); free(all_forb); free(all_pro);
|
||||||
return env->intern(env, "nil");
|
return env->intern(env, "nil");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,15 +585,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 12 arguments from this paragraph's vector */
|
/* Extract 14 arguments from this paragraph's vector */
|
||||||
emacs_value para_args[12];
|
emacs_value para_args[14];
|
||||||
for (int i = 0; i < 12; i++) {
|
for (int i = 0; i < 14; 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;
|
int32_t hyph_width, line_width, hyphen_protrude;
|
||||||
|
|
||||||
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],
|
||||||
@ -580,17 +601,18 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
|
|||||||
&all_hyph[p], &n, &hyph_count,
|
&all_hyph[p], &n, &hyph_count,
|
||||||
&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)) {
|
||||||
/* 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]);
|
||||||
free(all_glue_i[j]); free(all_glue_sh[j]); free(all_glue_st[j]);
|
free(all_glue_i[j]); free(all_glue_sh[j]); free(all_glue_st[j]);
|
||||||
free(all_hyph[j]); free(all_lead[j]); free(all_trail[j]);
|
free(all_hyph[j]); free(all_lead[j]); free(all_trail[j]);
|
||||||
free(all_forb[j]);
|
free(all_forb[j]); free(all_pro[j]);
|
||||||
}
|
}
|
||||||
free(inputs); free(all_ideal); free(all_min); free(all_max);
|
free(inputs); free(all_ideal); free(all_min); free(all_max);
|
||||||
free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph);
|
free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph);
|
||||||
free(all_lead); free(all_trail); free(all_forb);
|
free(all_lead); free(all_trail); free(all_forb); free(all_pro);
|
||||||
return env->intern(env, "nil");
|
return env->intern(env, "nil");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,6 +632,8 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
|
|||||||
inputs[p].forbidden_positions = all_forb[p];
|
inputs[p].forbidden_positions = all_forb[p];
|
||||||
inputs[p].forbidden_count =
|
inputs[p].forbidden_count =
|
||||||
(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].hyphen_protrude = hyphen_protrude;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Process all paragraphs in parallel */
|
/* Process all paragraphs in parallel */
|
||||||
@ -620,11 +644,11 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
|
|||||||
free(all_ideal[p]); free(all_min[p]); free(all_max[p]);
|
free(all_ideal[p]); free(all_min[p]); free(all_max[p]);
|
||||||
free(all_glue_i[p]); free(all_glue_sh[p]); free(all_glue_st[p]);
|
free(all_glue_i[p]); free(all_glue_sh[p]); free(all_glue_st[p]);
|
||||||
free(all_hyph[p]); free(all_lead[p]); free(all_trail[p]);
|
free(all_hyph[p]); free(all_lead[p]); free(all_trail[p]);
|
||||||
free(all_forb[p]);
|
free(all_forb[p]); free(all_pro[p]);
|
||||||
}
|
}
|
||||||
free(inputs); free(all_ideal); free(all_min); free(all_max);
|
free(inputs); free(all_ideal); free(all_min); free(all_max);
|
||||||
free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph);
|
free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph);
|
||||||
free(all_lead); free(all_trail); free(all_forb);
|
free(all_lead); free(all_trail); free(all_forb); free(all_pro);
|
||||||
|
|
||||||
if (!results)
|
if (!results)
|
||||||
return env->intern(env, "nil");
|
return env->intern(env, "nil");
|
||||||
@ -734,7 +758,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", 12, 12, Fekp_c_break_with_arrays,
|
defun(env, "ekp-c-break-with-arrays", 14, 14, 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\
|
||||||
|
|||||||
@ -143,6 +143,12 @@ typedef struct {
|
|||||||
const int32_t *forbidden_positions;
|
const int32_t *forbidden_positions;
|
||||||
size_t forbidden_count;
|
size_t forbidden_count;
|
||||||
|
|
||||||
|
/* Right-edge protrusion (nullable, n+1): pixels the line's final
|
||||||
|
* glyph may hang past the flush edge when breaking at gap k;
|
||||||
|
* hyphen_protrude is the same for soft hyphens. */
|
||||||
|
const int32_t *tail_protrudes;
|
||||||
|
int32_t hyphen_protrude;
|
||||||
|
|
||||||
/* Space-box run widths (nullable, n+1 elements each):
|
/* Space-box run widths (nullable, n+1 elements each):
|
||||||
* lead_spaces[i] = width of space-box run starting at box i
|
* lead_spaces[i] = width of space-box run starting at box i
|
||||||
* trail_spaces[k] = width of space-box run ending at box k-1
|
* trail_spaces[k] = width of space-box run ending at box k-1
|
||||||
@ -292,6 +298,11 @@ static void dp_process_position(
|
|||||||
bool end_hyphen = dp_is_hyphen(in, k - 1);
|
bool end_hyphen = dp_is_hyphen(in, k - 1);
|
||||||
int32_t hyph_w = end_hyphen ? in->hyphen_width : 0;
|
int32_t hyph_w = end_hyphen ? in->hyphen_width : 0;
|
||||||
|
|
||||||
|
/* Right-edge protrusion widens this candidate's target */
|
||||||
|
int32_t lw = line_width +
|
||||||
|
(end_hyphen ? in->hyphen_protrude
|
||||||
|
: (in->tail_protrudes ? in->tail_protrudes[k] : 0));
|
||||||
|
|
||||||
/* Line metrics from i to k, excluding leading glue and the
|
/* Line metrics from i to k, excluding leading glue and the
|
||||||
* space-box runs the renderer strips (leading + trailing). */
|
* space-box runs the renderer strips (leading + trailing). */
|
||||||
int32_t raw_ideal = in->ideal_prefix[k] - in->ideal_prefix[i] - lead_ideal;
|
int32_t raw_ideal = in->ideal_prefix[k] - in->ideal_prefix[i] - lead_ideal;
|
||||||
@ -308,32 +319,32 @@ static void dp_process_position(
|
|||||||
in->extra_stretch;
|
in->extra_stretch;
|
||||||
|
|
||||||
/* Too long? (last line is never shrunk below its ideal) */
|
/* Too long? (last line is never shrunk below its ideal) */
|
||||||
if (min_w > line_width || (is_last && ideal > line_width)) {
|
if (min_w > lw || (is_last && ideal > lw)) {
|
||||||
if (atomic_run && in->allow_emergency)
|
if (atomic_run && in->allow_emergency)
|
||||||
dp_relax_emergency(in, i, k, prev_dem, prev_hyph, prev_lines,
|
dp_relax_emergency(in, i, k, prev_dem, prev_hyph, prev_lines,
|
||||||
line_width - ideal, end_hyphen,
|
lw - ideal, end_hyphen,
|
||||||
demerits, backptrs, rest_pixels,
|
demerits, backptrs, rest_pixels,
|
||||||
fitness, hyphen_counts, line_counts);
|
fitness, hyphen_counts, line_counts);
|
||||||
break; /* No point trying longer lines */
|
break; /* No point trying longer lines */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Valid break? */
|
/* Valid break? */
|
||||||
bool valid = (min_w <= line_width && max_w >= line_width) ||
|
bool valid = (min_w <= lw && max_w >= lw) ||
|
||||||
(is_last && ideal <= line_width);
|
(is_last && ideal <= lw);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
/* Rigid underfull atomic run: emergency-record so the
|
/* Rigid underfull atomic run: emergency-record so the
|
||||||
* position after it stays reachable (2nd pass only). */
|
* position after it stays reachable (2nd pass only). */
|
||||||
if (atomic_run && in->allow_emergency)
|
if (atomic_run && in->allow_emergency)
|
||||||
dp_relax_emergency(in, i, k, prev_dem, prev_hyph, prev_lines,
|
dp_relax_emergency(in, i, k, prev_dem, prev_hyph, prev_lines,
|
||||||
line_width - ideal, end_hyphen,
|
lw - ideal, end_hyphen,
|
||||||
demerits, backptrs, rest_pixels,
|
demerits, backptrs, rest_pixels,
|
||||||
fitness, hyphen_counts, line_counts);
|
fitness, hyphen_counts, line_counts);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compute demerits */
|
/* Compute demerits */
|
||||||
int32_t adjustment = line_width - ideal;
|
int32_t adjustment = lw - ideal;
|
||||||
int32_t flexibility = (adjustment > 0) ?
|
int32_t flexibility = (adjustment > 0) ?
|
||||||
(max_w - ideal) : (ideal - min_w);
|
(max_w - ideal) : (ideal - min_w);
|
||||||
|
|
||||||
@ -356,7 +367,7 @@ static void dp_process_position(
|
|||||||
in->consec_hyphen_penalty);
|
in->consec_hyphen_penalty);
|
||||||
} else if (is_last) {
|
} else if (is_last) {
|
||||||
/* Last line: minimal penalty if reasonably filled */
|
/* Last line: minimal penalty if reasonably filled */
|
||||||
double fill_ratio = (double)ideal / line_width;
|
double fill_ratio = (double)ideal / lw;
|
||||||
if (fill_ratio < in->last_line_ratio) {
|
if (fill_ratio < in->last_line_ratio) {
|
||||||
badness = in->last_line_short_penalty * (1.0 - fill_ratio);
|
badness = in->last_line_short_penalty * (1.0 - fill_ratio);
|
||||||
} else {
|
} else {
|
||||||
@ -647,7 +658,9 @@ ekp_result_t *ekp_break_with_prefixes(
|
|||||||
const int32_t *lead_spaces,
|
const int32_t *lead_spaces,
|
||||||
const int32_t *trail_spaces,
|
const int32_t *trail_spaces,
|
||||||
const int32_t *forbidden_positions,
|
const int32_t *forbidden_positions,
|
||||||
size_t forbidden_count)
|
size_t forbidden_count,
|
||||||
|
const int32_t *tail_protrudes,
|
||||||
|
int32_t hyphen_protrude)
|
||||||
{
|
{
|
||||||
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;
|
||||||
@ -699,6 +712,8 @@ ekp_result_t *ekp_break_with_prefixes(
|
|||||||
.hyphen_width = hyphen_width,
|
.hyphen_width = hyphen_width,
|
||||||
.forbidden_positions = forbidden_positions,
|
.forbidden_positions = forbidden_positions,
|
||||||
.forbidden_count = forbidden_count,
|
.forbidden_count = forbidden_count,
|
||||||
|
.tail_protrudes = tail_protrudes,
|
||||||
|
.hyphen_protrude = hyphen_protrude,
|
||||||
.lead_spaces = lead_spaces,
|
.lead_spaces = lead_spaces,
|
||||||
.trail_spaces = trail_spaces,
|
.trail_spaces = trail_spaces,
|
||||||
.n = n,
|
.n = n,
|
||||||
@ -821,7 +836,8 @@ static void batch_worker(void *arg)
|
|||||||
in->hyphen_positions, in->hyphen_count,
|
in->hyphen_positions, in->hyphen_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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -850,7 +866,8 @@ ekp_result_t **ekp_break_batch(ekp_batch_input_t *inputs, size_t count)
|
|||||||
in->hyphen_positions, in->hyphen_count,
|
in->hyphen_positions, in->hyphen_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);
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@ -868,7 +885,8 @@ ekp_result_t **ekp_break_batch(ekp_batch_input_t *inputs, size_t count)
|
|||||||
in->hyphen_positions, in->hyphen_count,
|
in->hyphen_positions, in->hyphen_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);
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
/* Version */
|
/* Version */
|
||||||
#define EKP_VERSION_MAJOR 1
|
#define EKP_VERSION_MAJOR 1
|
||||||
#define EKP_VERSION_MINOR 3
|
#define EKP_VERSION_MINOR 4
|
||||||
|
|
||||||
/* Limits */
|
/* Limits */
|
||||||
#define EKP_MAX_PATTERN_LEN 64
|
#define EKP_MAX_PATTERN_LEN 64
|
||||||
@ -258,7 +258,9 @@ ekp_result_t *ekp_break_with_prefixes(
|
|||||||
const int32_t *lead_spaces,
|
const int32_t *lead_spaces,
|
||||||
const int32_t *trail_spaces,
|
const int32_t *trail_spaces,
|
||||||
const int32_t *forbidden_positions,
|
const int32_t *forbidden_positions,
|
||||||
size_t forbidden_count);
|
size_t forbidden_count,
|
||||||
|
const int32_t *tail_protrudes,
|
||||||
|
int32_t hyphen_protrude);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Batch input for parallel processing
|
* Batch input for parallel processing
|
||||||
@ -279,6 +281,8 @@ typedef struct {
|
|||||||
const int32_t *trail_spaces; /* nullable, n+1 elements */
|
const int32_t *trail_spaces; /* nullable, n+1 elements */
|
||||||
const int32_t *forbidden_positions; /* nullable, sorted gap indices */
|
const int32_t *forbidden_positions; /* nullable, sorted gap indices */
|
||||||
size_t forbidden_count;
|
size_t forbidden_count;
|
||||||
|
const int32_t *tail_protrudes; /* nullable, n+1 elements */
|
||||||
|
int32_t hyphen_protrude;
|
||||||
} ekp_batch_input_t;
|
} ekp_batch_input_t;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -198,6 +198,41 @@ Used to verify no content is lost by justification."
|
|||||||
(ekp-pixel-justify text 60))))
|
(ekp-pixel-justify text 60))))
|
||||||
(should (string= a b))))))
|
(should (string= a b))))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-protrusion-hangs-line-end-punct ()
|
||||||
|
"Protrusion lets line-final fullwidth punctuation hang past the edge."
|
||||||
|
(let ((ekp-protrusion t)
|
||||||
|
;; periodic 5-char sentences: at 20px exactly two per line, so
|
||||||
|
;; every interior break lands right after 。
|
||||||
|
(text (mapconcat #'identity (make-list 6 "四字一句。") "")))
|
||||||
|
(let* ((lines (split-string (ekp-pixel-justify text 20) "\n")))
|
||||||
|
(should (> (length lines) 1))
|
||||||
|
;; at least one non-last line hangs its punctuation
|
||||||
|
(should (seq-some (lambda (l) (> (string-pixel-width l) 20))
|
||||||
|
(butlast lines)))
|
||||||
|
;; and never beyond the configured ratio (0.5 × 2px in batch)
|
||||||
|
(dolist (l (butlast lines))
|
||||||
|
(should (<= (string-pixel-width l) 21))))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-protrusion-off-is-flush ()
|
||||||
|
"With protrusion off (default), no line exceeds the target width."
|
||||||
|
(let ((text "第一句话结束。第二句话继续写下去,内容足够长才会断行成很多行。"))
|
||||||
|
(dolist (l (butlast (split-string (ekp-pixel-justify text 20) "\n")))
|
||||||
|
(should (<= (string-pixel-width l) 20)))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-protrusion-c-parity ()
|
||||||
|
"C and elisp agree with protrusion enabled."
|
||||||
|
(skip-unless (ekp-tests--c-available))
|
||||||
|
(let ((ekp-protrusion t)
|
||||||
|
(text "悬挂 parity 检查。标点很多,逗号,句号。分号;更多内容写在这里。"))
|
||||||
|
(dolist (w '(24 40 60))
|
||||||
|
(let ((a (let ((ekp-use-c-module nil))
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(ekp-pixel-justify text w)))
|
||||||
|
(b (let ((ekp-use-c-module t))
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(ekp-pixel-justify text w))))
|
||||||
|
(should (string= a b))))))
|
||||||
|
|
||||||
(ert-deftest ekp-test-no-break-span-atomic ()
|
(ert-deftest ekp-test-no-break-span-atomic ()
|
||||||
"An ekp-no-break span never splits, stretches, or hyphenates."
|
"An ekp-no-break span never splits, stretches, or hyphenates."
|
||||||
(let* ((code (propertize "foo bar baz" 'ekp-no-break t))
|
(let* ((code (propertize "foo bar baz" 'ekp-no-break t))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user