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
|
||||
|
||||
(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)
|
||||
"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
|
||||
|
||||
|
||||
@ -347,7 +347,7 @@ after CALLBACK returns."
|
||||
(defalias 'ekp-c-module-reload #'ekp--module-reload
|
||||
"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.")
|
||||
|
||||
(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
|
||||
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
|
||||
"Target line count offset: 0=optimal, +1=looser (more lines), -1=tighter.
|
||||
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
|
||||
;; the same information as a sparse int vector for the C bridge.
|
||||
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
|
||||
(dp-cache nil :type hash-table))
|
||||
@ -459,6 +482,7 @@ are derived per string)."
|
||||
ekp-latin-lang
|
||||
ekp-alignment
|
||||
ekp-ragged-stretch-pixel
|
||||
(and ekp-protrusion ekp-protrusion-ratios)
|
||||
(if (and ekp--params-explicit (ekp--params-set-p))
|
||||
(list ekp-lws-ideal-pixel ekp-lws-stretch-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."
|
||||
(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 ()
|
||||
"Resolve the per-line flexibility for non-justify alignment."
|
||||
(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))
|
||||
(trail-spaces (make-vector (1+ n) 0))
|
||||
(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:
|
||||
;; - kinsoku: the line would end with an opener or start with a
|
||||
;; 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)
|
||||
(aset glues-types k 'nws))))
|
||||
(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
|
||||
(dotimes (i n)
|
||||
(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
|
||||
:breaks-allowed breaks-allowed
|
||||
:forbidden-positions (vconcat (nreverse forbidden))
|
||||
:tail-protrudes tail-protrudes
|
||||
:hyphen-protrude hyphen-protrude
|
||||
:glue-params (let ((justify (eq ekp-alignment 'justify)))
|
||||
(list :lws-ideal ekp-lws-ideal-pixel
|
||||
: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))
|
||||
(trail-spaces (ekp-para-trail-spaces 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))
|
||||
(lws-stretch (plist-get params :lws-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))
|
||||
(end-with-hyphenp (aref hyph-flags (1- k)))
|
||||
(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))
|
||||
(space-w (min raw-ideal
|
||||
(+ lead-space (aref trail-spaces k))))
|
||||
@ -899,20 +979,20 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
||||
(cond
|
||||
;; Line already too long: emergency-record atomic run,
|
||||
;; then stop extending.
|
||||
((or (> minw line-pixel)
|
||||
(and is-last (> ideal line-pixel)))
|
||||
((or (> minw lw)
|
||||
(and is-last (> ideal lw)))
|
||||
(when (and atomic-run allow-emergency)
|
||||
(ekp--dp-relax-emergency
|
||||
demerits backptrs rests gaps hyphen-counts
|
||||
fitness-classes i k prev-dem
|
||||
(- line-pixel ideal) end-with-hyphenp
|
||||
(- lw ideal) end-with-hyphenp
|
||||
prev-hyphen-count
|
||||
(unless single-box (ekp--gaps-between para i k))))
|
||||
(throw 'break nil))
|
||||
;; Valid break point
|
||||
((or (<= minw line-pixel maxw)
|
||||
(and is-last (<= ideal line-pixel)))
|
||||
(let* ((adjustment (- line-pixel ideal))
|
||||
((or (<= minw lw maxw)
|
||||
(and is-last (<= ideal lw)))
|
||||
(let* ((adjustment (- lw ideal))
|
||||
dem line-gaps fitness new-hyphen)
|
||||
(cond
|
||||
;; 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))))
|
||||
;; Last line: minimal demerits if reasonably filled
|
||||
(is-last
|
||||
(let* ((fill-ratio (/ (float ideal) line-pixel))
|
||||
(let* ((fill-ratio (/ (float ideal) lw))
|
||||
(badness (if (< fill-ratio
|
||||
ekp-last-line-min-ratio)
|
||||
(* ekp-last-line-short-penalty
|
||||
@ -982,7 +1062,7 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
||||
(ekp--dp-relax-emergency
|
||||
demerits backptrs rests gaps hyphen-counts
|
||||
fitness-classes i k prev-dem
|
||||
(- line-pixel ideal) end-with-hyphenp
|
||||
(- lw ideal) end-with-hyphenp
|
||||
prev-hyphen-count
|
||||
(unless single-box (ekp--gaps-between para i k)))))
|
||||
(setq saw-allowed t)
|
||||
@ -1063,6 +1143,8 @@ breaks when no valid layout exists."
|
||||
(lead-spaces (ekp-para-lead-spaces para))
|
||||
(trail-spaces (ekp-para-trail-spaces 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))
|
||||
(lws-stretch (plist-get params :lws-stretch))
|
||||
(mws-stretch (plist-get params :mws-stretch))
|
||||
@ -1102,6 +1184,11 @@ breaks when no valid layout exists."
|
||||
(end-with-hyphenp
|
||||
(ekp--hyphenate-p hyphen-positions (1- k)))
|
||||
(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))
|
||||
(space-w (min raw-ideal
|
||||
(+ 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
|
||||
space-w)
|
||||
hyph-w extra-stretch))
|
||||
(adjustment (- line-pixel ideal))
|
||||
(adjustment (- lw ideal))
|
||||
candidate)
|
||||
(cond
|
||||
((or (> minw line-pixel)
|
||||
(and is-last (> ideal line-pixel)))
|
||||
((or (> minw lw)
|
||||
(and is-last (> ideal lw)))
|
||||
(when (and atomic-run allow-emergency)
|
||||
(setq candidate
|
||||
(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
|
||||
prev-dem candidate))
|
||||
(throw 'break nil))
|
||||
((or (<= minw line-pixel maxw)
|
||||
(and is-last (<= ideal line-pixel)))
|
||||
((or (<= minw lw maxw)
|
||||
(and is-last (<= ideal lw)))
|
||||
(setq candidate
|
||||
(cond
|
||||
(single-box
|
||||
@ -1146,7 +1233,7 @@ breaks when no valid layout exists."
|
||||
end-with-hyphenp prev-hyphen-count)
|
||||
adjustment nil 1 nh)))
|
||||
(is-last
|
||||
(let* ((fill-ratio (/ (float ideal) line-pixel))
|
||||
(let* ((fill-ratio (/ (float ideal) lw))
|
||||
(badness (if (< fill-ratio
|
||||
ekp-last-line-min-ratio)
|
||||
(* 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."
|
||||
(let ((start 0) rests gapss)
|
||||
(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))
|
||||
(= end (length (ekp-para-boxes para))))
|
||||
nil
|
||||
@ -1308,7 +1397,7 @@ If `ekp-use-c-module' is non-nil and the C module is available (and
|
||||
dp-result))
|
||||
|
||||
(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)
|
||||
(ekp-para-min-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
|
||||
(ekp-para-lead-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)
|
||||
"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
|
||||
(ekp-para-lead-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-cost (cdr result)))
|
||||
(if (null c-breaks)
|
||||
@ -1534,6 +1627,11 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
||||
nil))
|
||||
(is-last (>= end boxes-num))
|
||||
(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.)
|
||||
(ideal-pixel (ekp--line-ideal-pixel para start end))
|
||||
(max-pixel (let* ((mx (ekp-para-max-prefixs para))
|
||||
@ -1556,7 +1654,7 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
||||
(cond
|
||||
;; Single box: just trailing space
|
||||
((= 1 (- end start))
|
||||
(ekp--line-glue-single-box line-pixel
|
||||
(ekp--line-glue-single-box eff-pixel
|
||||
(- ideal-pixel
|
||||
(if hyphen-p hyphen-pixel 0))
|
||||
hyphen-p hyphen-pixel))
|
||||
@ -1564,15 +1662,15 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
||||
;; natural glue widths plus a trailing filler.
|
||||
((or is-last ragged)
|
||||
(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):
|
||||
;; set glues to max and pad with trailing filler.
|
||||
((< max-pixel line-pixel)
|
||||
((< max-pixel eff-pixel)
|
||||
(append '(0)
|
||||
(mapcar (lambda (type)
|
||||
(ekp--para-glue-max para type))
|
||||
line-glues-types)
|
||||
(list (max 0 (- line-pixel max-pixel)))))
|
||||
(list (max 0 (- eff-pixel max-pixel)))))
|
||||
;; Normal justified line
|
||||
(t
|
||||
(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;
|
||||
|
||||
if (!ekp_global || nargs < 12)
|
||||
if (!ekp_global || nargs < 14)
|
||||
return env->intern(env, "nil");
|
||||
|
||||
/* 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 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) */
|
||||
ptrdiff_t forb_count = env->vec_size(env, args[11]);
|
||||
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_width, line_width,
|
||||
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(glue_ideals); free(glue_shrinks); free(glue_stretches);
|
||||
free(lead_spaces); free(trail_spaces);
|
||||
free(hyph_pos);
|
||||
free(forb_pos);
|
||||
free(tail_pro);
|
||||
|
||||
if (!result)
|
||||
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_width, int32_t *line_width,
|
||||
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]);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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_trail = 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 ||
|
||||
!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(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");
|
||||
}
|
||||
|
||||
@ -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++) {
|
||||
emacs_value para_vec = env->vec_get(env, args[0], p);
|
||||
|
||||
/* Extract 12 arguments from this paragraph's vector */
|
||||
emacs_value para_args[12];
|
||||
for (int i = 0; i < 12; i++) {
|
||||
/* Extract 14 arguments from this paragraph's vector */
|
||||
emacs_value para_args[14];
|
||||
for (int i = 0; i < 14; i++) {
|
||||
para_args[i] = env->vec_get(env, para_vec, i);
|
||||
}
|
||||
|
||||
size_t n;
|
||||
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,
|
||||
&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,
|
||||
&hyph_width, &line_width,
|
||||
&all_lead[p], &all_trail[p],
|
||||
&all_forb[p], &forb_count)) {
|
||||
&all_forb[p], &forb_count,
|
||||
&all_pro[p], &hyphen_protrude)) {
|
||||
/* Cleanup on failure */
|
||||
for (ptrdiff_t j = 0; j < p; 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_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(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");
|
||||
}
|
||||
|
||||
@ -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_count =
|
||||
(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 */
|
||||
@ -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_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_forb[p]);
|
||||
free(all_forb[p]); free(all_pro[p]);
|
||||
}
|
||||
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_lead); free(all_trail); free(all_forb);
|
||||
free(all_lead); free(all_trail); free(all_forb); free(all_pro);
|
||||
|
||||
if (!results)
|
||||
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\
|
||||
(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\
|
||||
IDEAL-PREFIX: vector of ideal 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;
|
||||
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):
|
||||
* 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
|
||||
@ -292,6 +298,11 @@ static void dp_process_position(
|
||||
bool end_hyphen = dp_is_hyphen(in, k - 1);
|
||||
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
|
||||
* space-box runs the renderer strips (leading + trailing). */
|
||||
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;
|
||||
|
||||
/* 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)
|
||||
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,
|
||||
fitness, hyphen_counts, line_counts);
|
||||
break; /* No point trying longer lines */
|
||||
}
|
||||
|
||||
/* Valid break? */
|
||||
bool valid = (min_w <= line_width && max_w >= line_width) ||
|
||||
(is_last && ideal <= line_width);
|
||||
bool valid = (min_w <= lw && max_w >= lw) ||
|
||||
(is_last && ideal <= lw);
|
||||
|
||||
if (!valid) {
|
||||
/* Rigid underfull atomic run: emergency-record so the
|
||||
* position after it stays reachable (2nd pass only). */
|
||||
if (atomic_run && in->allow_emergency)
|
||||
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,
|
||||
fitness, hyphen_counts, line_counts);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Compute demerits */
|
||||
int32_t adjustment = line_width - ideal;
|
||||
int32_t adjustment = lw - ideal;
|
||||
int32_t flexibility = (adjustment > 0) ?
|
||||
(max_w - ideal) : (ideal - min_w);
|
||||
|
||||
@ -356,7 +367,7 @@ static void dp_process_position(
|
||||
in->consec_hyphen_penalty);
|
||||
} else if (is_last) {
|
||||
/* 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) {
|
||||
badness = in->last_line_short_penalty * (1.0 - fill_ratio);
|
||||
} else {
|
||||
@ -647,7 +658,9 @@ ekp_result_t *ekp_break_with_prefixes(
|
||||
const int32_t *lead_spaces,
|
||||
const int32_t *trail_spaces,
|
||||
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)
|
||||
return NULL;
|
||||
@ -699,6 +712,8 @@ ekp_result_t *ekp_break_with_prefixes(
|
||||
.hyphen_width = hyphen_width,
|
||||
.forbidden_positions = forbidden_positions,
|
||||
.forbidden_count = forbidden_count,
|
||||
.tail_protrudes = tail_protrudes,
|
||||
.hyphen_protrude = hyphen_protrude,
|
||||
.lead_spaces = lead_spaces,
|
||||
.trail_spaces = trail_spaces,
|
||||
.n = n,
|
||||
@ -821,7 +836,8 @@ static void batch_worker(void *arg)
|
||||
in->hyphen_positions, in->hyphen_count,
|
||||
in->hyphen_width, in->line_width,
|
||||
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_width, in->line_width,
|
||||
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;
|
||||
}
|
||||
@ -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_width, in->line_width,
|
||||
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;
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
/* Version */
|
||||
#define EKP_VERSION_MAJOR 1
|
||||
#define EKP_VERSION_MINOR 3
|
||||
#define EKP_VERSION_MINOR 4
|
||||
|
||||
/* Limits */
|
||||
#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 *trail_spaces,
|
||||
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
|
||||
@ -279,6 +281,8 @@ typedef struct {
|
||||
const int32_t *trail_spaces; /* nullable, n+1 elements */
|
||||
const int32_t *forbidden_positions; /* nullable, sorted gap indices */
|
||||
size_t forbidden_count;
|
||||
const int32_t *tail_protrudes; /* nullable, n+1 elements */
|
||||
int32_t hyphen_protrude;
|
||||
} ekp_batch_input_t;
|
||||
|
||||
/*
|
||||
|
||||
@ -198,6 +198,41 @@ Used to verify no content is lost by justification."
|
||||
(ekp-pixel-justify text 60))))
|
||||
(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 ()
|
||||
"An ekp-no-break span never splits, stretches, or hyphenates."
|
||||
(let* ((code (propertize "foo bar baz" 'ekp-no-break t))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user