feat: alignment modes — ragged-right, ragged-left, center (C 1.3)
ekp-alignment selects the paragraph alignment. Non-justify modes keep inter-word glue rigid (arrays and class params zeroed at para build) while the DP gains a per-line flexibility ekp-ragged-stretch-pixel (default ≈2 em): badness = 100·(shortfall/R)³, so Knuth-Plass still globally minimizes raggedness — this is \raggedright with a finite \rightskip stretch, not first-fit. Rendering distributes each line's leftover by mode: trailing (ragged-right), split evenly (center), leading (ragged-left); the spacers are ekp-glue-marked, so buffer roundtrips stay exact. C module 1.3: set-penalties gains an extra-stretch scalar (reset when omitted); max_w widens by it, so the prefix-based flexibility picks it up automatically. Cache keys include alignment and R. Tests: 56 ERT green incl. per-mode invariants (fit, rigid interior glue, flush edges, centered leftover ±1px) and four-mode C parity; fuzz 300/300. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ea96a6dc96
commit
57a3abe853
@ -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.2"
|
(defconst ekp-c-module-required-version "1.3"
|
||||||
"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 ()
|
||||||
|
|||||||
91
ekp.el
91
ekp.el
@ -86,6 +86,22 @@ Applied as: this × (1 - fill-ratio) when fill < `ekp-last-line-min-ratio'.")
|
|||||||
(defvar ekp-last-line-min-ratio 0.5
|
(defvar ekp-last-line-min-ratio 0.5
|
||||||
"Minimum fill ratio for last line (0.0-1.0).")
|
"Minimum fill ratio for last line (0.0-1.0).")
|
||||||
|
|
||||||
|
(defvar ekp-alignment 'justify
|
||||||
|
"Paragraph alignment mode.
|
||||||
|
`justify' — flush both edges (default)
|
||||||
|
`ragged-right' — natural spacing, lines end ragged on the right
|
||||||
|
`ragged-left' — natural spacing, lines start ragged on the left
|
||||||
|
`center' — natural spacing, both edges share the leftover
|
||||||
|
Non-justify modes keep inter-word glue rigid; the K-P optimizer still
|
||||||
|
picks breaks that minimize raggedness within
|
||||||
|
`ekp-ragged-stretch-pixel' per line.")
|
||||||
|
|
||||||
|
(defvar ekp-ragged-stretch-pixel nil
|
||||||
|
"Per-line end-of-line flexibility (pixels) for non-justify alignment.
|
||||||
|
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-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
|
||||||
@ -441,6 +457,8 @@ are derived per string)."
|
|||||||
(prin1-to-string (object-intervals string))
|
(prin1-to-string (object-intervals string))
|
||||||
latin-font cjk-font
|
latin-font cjk-font
|
||||||
ekp-latin-lang
|
ekp-latin-lang
|
||||||
|
ekp-alignment
|
||||||
|
ekp-ragged-stretch-pixel
|
||||||
(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
|
||||||
@ -479,6 +497,11 @@ 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--ragged-extra-stretch ()
|
||||||
|
"Resolve the per-line flexibility for non-justify alignment."
|
||||||
|
(or ekp-ragged-stretch-pixel
|
||||||
|
(max 1 (* 8 (or ekp-lws-ideal-pixel 1)))))
|
||||||
|
|
||||||
(defun ekp--make-para (string)
|
(defun ekp--make-para (string)
|
||||||
"Create and fully initialize `ekp-para' struct for STRING.
|
"Create and fully initialize `ekp-para' struct for STRING.
|
||||||
Computes ALL data in one pass: text, params, and prefix arrays."
|
Computes ALL data in one pass: text, params, and prefix arrays."
|
||||||
@ -547,8 +570,14 @@ Computes ALL data in one pass: text, params, and prefix arrays."
|
|||||||
(let* ((box-w (aref boxes-widths i))
|
(let* ((box-w (aref boxes-widths i))
|
||||||
(glue-type (aref glues-types i))
|
(glue-type (aref glues-types i))
|
||||||
(g-ideal (ekp-glue-ideal-pixel glue-type))
|
(g-ideal (ekp-glue-ideal-pixel glue-type))
|
||||||
(g-min (ekp-glue-min-pixel glue-type))
|
;; Non-justify alignment: inter-word glue is rigid; the
|
||||||
(g-max (ekp-glue-max-pixel glue-type)))
|
;; per-line flexibility comes from :extra-stretch instead.
|
||||||
|
(g-min (if (eq ekp-alignment 'justify)
|
||||||
|
(ekp-glue-min-pixel glue-type)
|
||||||
|
g-ideal))
|
||||||
|
(g-max (if (eq ekp-alignment 'justify)
|
||||||
|
(ekp-glue-max-pixel glue-type)
|
||||||
|
g-ideal)))
|
||||||
(aset glue-ideals i g-ideal)
|
(aset glue-ideals i g-ideal)
|
||||||
(aset glue-shrinks i (- g-ideal g-min))
|
(aset glue-shrinks i (- g-ideal g-min))
|
||||||
(aset glue-stretches i (- g-max g-ideal))
|
(aset glue-stretches i (- g-max g-ideal))
|
||||||
@ -599,15 +628,19 @@ 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))
|
||||||
:glue-params (list :lws-ideal ekp-lws-ideal-pixel
|
:glue-params (let ((justify (eq ekp-alignment 'justify)))
|
||||||
:lws-stretch ekp-lws-stretch-pixel
|
(list :lws-ideal ekp-lws-ideal-pixel
|
||||||
:lws-shrink ekp-lws-shrink-pixel
|
:lws-stretch (if justify ekp-lws-stretch-pixel 0)
|
||||||
|
:lws-shrink (if justify ekp-lws-shrink-pixel 0)
|
||||||
:mws-ideal ekp-mws-ideal-pixel
|
:mws-ideal ekp-mws-ideal-pixel
|
||||||
:mws-stretch ekp-mws-stretch-pixel
|
:mws-stretch (if justify ekp-mws-stretch-pixel 0)
|
||||||
:mws-shrink ekp-mws-shrink-pixel
|
:mws-shrink (if justify ekp-mws-shrink-pixel 0)
|
||||||
:cws-ideal ekp-cws-ideal-pixel
|
:cws-ideal ekp-cws-ideal-pixel
|
||||||
:cws-stretch ekp-cws-stretch-pixel
|
:cws-stretch (if justify ekp-cws-stretch-pixel 0)
|
||||||
:cws-shrink ekp-cws-shrink-pixel)
|
:cws-shrink (if justify ekp-cws-shrink-pixel 0)
|
||||||
|
:alignment ekp-alignment
|
||||||
|
:extra-stretch (if justify 0
|
||||||
|
(ekp--ragged-extra-stretch))))
|
||||||
:dp-cache (make-hash-table :test 'eql :size 20))))
|
:dp-cache (make-hash-table :test 'eql :size 20))))
|
||||||
|
|
||||||
(defun ekp--get-para (string)
|
(defun ekp--get-para (string)
|
||||||
@ -817,6 +850,7 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(lws-shrink (plist-get params :lws-shrink))
|
(lws-shrink (plist-get params :lws-shrink))
|
||||||
(mws-shrink (plist-get params :mws-shrink))
|
(mws-shrink (plist-get params :mws-shrink))
|
||||||
(cws-shrink (plist-get params :cws-shrink))
|
(cws-shrink (plist-get params :cws-shrink))
|
||||||
|
(extra-stretch (or (plist-get params :extra-stretch) 0))
|
||||||
(backptrs (make-vector (1+ n) nil))
|
(backptrs (make-vector (1+ n) nil))
|
||||||
(demerits (make-vector (1+ n) nil))
|
(demerits (make-vector (1+ n) nil))
|
||||||
(rests (make-vector (1+ n) nil))
|
(rests (make-vector (1+ n) nil))
|
||||||
@ -861,7 +895,7 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
hyph-w))
|
hyph-w))
|
||||||
(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)))
|
hyph-w extra-stretch)))
|
||||||
(cond
|
(cond
|
||||||
;; Line already too long: emergency-record atomic run,
|
;; Line already too long: emergency-record atomic run,
|
||||||
;; then stop extending.
|
;; then stop extending.
|
||||||
@ -916,7 +950,8 @@ unreachable (only possible when ALLOW-EMERGENCY is nil)."
|
|||||||
(if (> adjustment 0)
|
(if (> adjustment 0)
|
||||||
(+ (* lcnt lws-stretch)
|
(+ (* lcnt lws-stretch)
|
||||||
(* mcnt mws-stretch)
|
(* mcnt mws-stretch)
|
||||||
(* ccnt cws-stretch))
|
(* ccnt cws-stretch)
|
||||||
|
extra-stretch)
|
||||||
(+ (* lcnt lws-shrink)
|
(+ (* lcnt lws-shrink)
|
||||||
(* mcnt mws-shrink)
|
(* mcnt mws-shrink)
|
||||||
(* ccnt cws-shrink))))
|
(* ccnt cws-shrink))))
|
||||||
@ -1035,6 +1070,7 @@ breaks when no valid layout exists."
|
|||||||
(lws-shrink (plist-get params :lws-shrink))
|
(lws-shrink (plist-get params :lws-shrink))
|
||||||
(mws-shrink (plist-get params :mws-shrink))
|
(mws-shrink (plist-get params :mws-shrink))
|
||||||
(cws-shrink (plist-get params :cws-shrink))
|
(cws-shrink (plist-get params :cws-shrink))
|
||||||
|
(extra-stretch (or (plist-get params :extra-stretch) 0))
|
||||||
;; state: (pos . lines) -> [dem backptr fitness hyph rest gaps]
|
;; state: (pos . lines) -> [dem backptr fitness hyph rest gaps]
|
||||||
(states (make-hash-table :test 'equal :size (* 4 (1+ n))))
|
(states (make-hash-table :test 'equal :size (* 4 (1+ n))))
|
||||||
(counts-at (make-vector (1+ n) nil)))
|
(counts-at (make-vector (1+ n) nil)))
|
||||||
@ -1075,7 +1111,7 @@ breaks when no valid layout exists."
|
|||||||
hyph-w))
|
hyph-w))
|
||||||
(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))
|
hyph-w extra-stretch))
|
||||||
(adjustment (- line-pixel ideal))
|
(adjustment (- line-pixel ideal))
|
||||||
candidate)
|
candidate)
|
||||||
(cond
|
(cond
|
||||||
@ -1127,7 +1163,8 @@ breaks when no valid layout exists."
|
|||||||
(if (> adjustment 0)
|
(if (> adjustment 0)
|
||||||
(+ (* lcnt lws-stretch)
|
(+ (* lcnt lws-stretch)
|
||||||
(* mcnt mws-stretch)
|
(* mcnt mws-stretch)
|
||||||
(* ccnt cws-stretch))
|
(* ccnt cws-stretch)
|
||||||
|
extra-stretch)
|
||||||
(+ (* lcnt lws-shrink)
|
(+ (* lcnt lws-shrink)
|
||||||
(* mcnt mws-shrink)
|
(* mcnt mws-shrink)
|
||||||
(* ccnt cws-shrink))))
|
(* ccnt cws-shrink))))
|
||||||
@ -1228,7 +1265,10 @@ CANDIDATE is (DEM-DELTA REST GAPS FITNESS HYPHEN-COUNT)."
|
|||||||
ekp-adjacent-fitness-penalty
|
ekp-adjacent-fitness-penalty
|
||||||
(float ekp-last-line-min-ratio)
|
(float ekp-last-line-min-ratio)
|
||||||
ekp-consecutive-hyphen-penalty
|
ekp-consecutive-hyphen-penalty
|
||||||
(float ekp-last-line-short-penalty))))
|
(float ekp-last-line-short-penalty)
|
||||||
|
(if (eq ekp-alignment 'justify)
|
||||||
|
0
|
||||||
|
(ekp--ragged-extra-stretch)))))
|
||||||
|
|
||||||
(defun ekp-dp-cache (string line-pixel)
|
(defun ekp-dp-cache (string line-pixel)
|
||||||
"Compute optimal line breaks for STRING at LINE-PIXEL width.
|
"Compute optimal line breaks for STRING at LINE-PIXEL width.
|
||||||
@ -1478,6 +1518,9 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
|||||||
(let* ((para (ekp--get-para string))
|
(let* ((para (ekp--get-para string))
|
||||||
(boxes-num (length (ekp-para-boxes para)))
|
(boxes-num (length (ekp-para-boxes para)))
|
||||||
(glues-types (ekp-para-glues-types para))
|
(glues-types (ekp-para-glues-types para))
|
||||||
|
(alignment (or (plist-get (ekp-para-glue-params para) :alignment)
|
||||||
|
'justify))
|
||||||
|
(ragged (not (eq alignment 'justify)))
|
||||||
(hyphen-positions (ekp-para-hyphen-positions para))
|
(hyphen-positions (ekp-para-hyphen-positions para))
|
||||||
(breaks (ekp-line-breaks string line-pixel))
|
(breaks (ekp-line-breaks string line-pixel))
|
||||||
(lines-rests (ekp-dp-data string line-pixel :rests))
|
(lines-rests (ekp-dp-data string line-pixel :rests))
|
||||||
@ -1517,8 +1560,9 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
|||||||
(- ideal-pixel
|
(- ideal-pixel
|
||||||
(if hyphen-p hyphen-pixel 0))
|
(if hyphen-p hyphen-pixel 0))
|
||||||
hyphen-p hyphen-pixel))
|
hyphen-p hyphen-pixel))
|
||||||
;; Last line: ragged right
|
;; Last line, or any line under non-justify alignment:
|
||||||
(is-last
|
;; natural glue widths plus a trailing filler.
|
||||||
|
((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 line-pixel))
|
||||||
;; Emergency underfull line (can't stretch to width):
|
;; Emergency underfull line (can't stretch to width):
|
||||||
@ -1534,6 +1578,21 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
|||||||
(ekp--line-glue-normal para line-glues-types
|
(ekp--line-glue-normal para line-glues-types
|
||||||
(nth i lines-rests)
|
(nth i lines-rests)
|
||||||
(nth i lines-gaps)))))
|
(nth i lines-gaps)))))
|
||||||
|
;; Non-justify alignment: place the leftover per mode
|
||||||
|
;; (ragged-right keeps it trailing; center splits it; ragged-left
|
||||||
|
;; moves it to the head).
|
||||||
|
(when (and ragged (>= (length glue-list) 2)
|
||||||
|
(memq alignment '(center ragged-left)))
|
||||||
|
(let ((filler (car (last glue-list))))
|
||||||
|
(setq glue-list
|
||||||
|
(if (eq alignment 'center)
|
||||||
|
(let ((lead (/ filler 2)))
|
||||||
|
(append (list lead)
|
||||||
|
(cdr (butlast glue-list))
|
||||||
|
(list (- filler lead))))
|
||||||
|
(append (list filler)
|
||||||
|
(cdr (butlast glue-list))
|
||||||
|
(list 0))))))
|
||||||
(aset line-glues i (vconcat glue-list))
|
(aset line-glues i (vconcat glue-list))
|
||||||
(setq start end)))
|
(setq start end)))
|
||||||
line-glues))
|
line-glues))
|
||||||
|
|||||||
@ -152,6 +152,10 @@ static emacs_value Fekp_c_set_penalties(emacs_env *env, ptrdiff_t nargs,
|
|||||||
ekp_global->consec_hyphen_penalty = env->extract_integer(env, args[4]);
|
ekp_global->consec_hyphen_penalty = env->extract_integer(env, args[4]);
|
||||||
if (nargs > 5)
|
if (nargs > 5)
|
||||||
ekp_global->last_line_short_penalty = env->extract_float(env, args[5]);
|
ekp_global->last_line_short_penalty = env->extract_float(env, args[5]);
|
||||||
|
/* Per-line extra stretch for non-justify alignment; reset to 0
|
||||||
|
* when the caller omits it so stale values never leak. */
|
||||||
|
ekp_global->extra_stretch =
|
||||||
|
(nargs > 6) ? (int32_t)env->extract_integer(env, args[6]) : 0;
|
||||||
|
|
||||||
return env->intern(env, "t");
|
return env->intern(env, "t");
|
||||||
}
|
}
|
||||||
@ -707,7 +711,7 @@ Arguments are: LWS-IDEAL LWS-STRETCH LWS-SHRINK\n\
|
|||||||
LWS = Latin Word Space, MWS = Mixed, CWS = CJK.\n\n\
|
LWS = Latin Word Space, MWS = Mixed, CWS = CJK.\n\n\
|
||||||
(fn LWS-I LWS-+ LWS-- MWS-I MWS-+ MWS-- CWS-I CWS-+ CWS--)");
|
(fn LWS-I LWS-+ LWS-- MWS-I MWS-+ MWS-- CWS-I CWS-+ CWS--)");
|
||||||
|
|
||||||
defun(env, "ekp-c-set-penalties", 4, 6, Fekp_c_set_penalties,
|
defun(env, "ekp-c-set-penalties", 4, 7, Fekp_c_set_penalties,
|
||||||
"Set Knuth-Plass algorithm penalties.\n\n\
|
"Set Knuth-Plass algorithm penalties.\n\n\
|
||||||
LINE-PENALTY: base penalty per line break (default 10)\n\
|
LINE-PENALTY: base penalty per line break (default 10)\n\
|
||||||
HYPHEN-PENALTY: penalty for hyphenated breaks (default 50)\n\
|
HYPHEN-PENALTY: penalty for hyphenated breaks (default 50)\n\
|
||||||
|
|||||||
@ -162,6 +162,9 @@ typedef struct {
|
|||||||
double last_line_ratio;
|
double last_line_ratio;
|
||||||
int consec_hyphen_penalty;
|
int consec_hyphen_penalty;
|
||||||
double last_line_short_penalty;
|
double last_line_short_penalty;
|
||||||
|
/* Per-line flexibility for non-justify alignment (0 = justify):
|
||||||
|
* widens max_w, so flexibility = max_w - ideal includes it. */
|
||||||
|
int32_t extra_stretch;
|
||||||
|
|
||||||
/* Two-pass strategy: strict K-P first; emergency single-box
|
/* Two-pass strategy: strict K-P first; emergency single-box
|
||||||
* breaks only in the second pass (when no valid layout exists). */
|
* breaks only in the second pass (when no valid layout exists). */
|
||||||
@ -301,7 +304,8 @@ static void dp_process_position(
|
|||||||
int32_t min_w = in->min_prefix[k] - in->min_prefix[i] -
|
int32_t min_w = in->min_prefix[k] - in->min_prefix[i] -
|
||||||
(lead_ideal - lead_shrink) - space_w + hyph_w;
|
(lead_ideal - lead_shrink) - space_w + hyph_w;
|
||||||
int32_t max_w = in->max_prefix[k] - in->max_prefix[i] -
|
int32_t max_w = in->max_prefix[k] - in->max_prefix[i] -
|
||||||
(lead_ideal + lead_stretch) - space_w + hyph_w;
|
(lead_ideal + lead_stretch) - space_w + hyph_w +
|
||||||
|
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 > line_width || (is_last && ideal > line_width)) {
|
||||||
@ -680,6 +684,7 @@ ekp_result_t *ekp_break_with_prefixes(
|
|||||||
double last_ratio = ekp_global ? ekp_global->last_line_ratio : 0.5;
|
double last_ratio = ekp_global ? ekp_global->last_line_ratio : 0.5;
|
||||||
int chp = ekp_global ? ekp_global->consec_hyphen_penalty : 100;
|
int chp = ekp_global ? ekp_global->consec_hyphen_penalty : 100;
|
||||||
double llsp = ekp_global ? ekp_global->last_line_short_penalty : 50.0;
|
double llsp = ekp_global ? ekp_global->last_line_short_penalty : 50.0;
|
||||||
|
int32_t xstretch = ekp_global ? ekp_global->extra_stretch : 0;
|
||||||
|
|
||||||
/* Create unified input structure */
|
/* Create unified input structure */
|
||||||
dp_input_t in = {
|
dp_input_t in = {
|
||||||
@ -704,6 +709,7 @@ ekp_result_t *ekp_break_with_prefixes(
|
|||||||
.last_line_ratio = last_ratio,
|
.last_line_ratio = last_ratio,
|
||||||
.consec_hyphen_penalty = chp,
|
.consec_hyphen_penalty = chp,
|
||||||
.last_line_short_penalty = llsp,
|
.last_line_short_penalty = llsp,
|
||||||
|
.extra_stretch = xstretch,
|
||||||
.allow_emergency = false
|
.allow_emergency = false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
/* Version */
|
/* Version */
|
||||||
#define EKP_VERSION_MAJOR 1
|
#define EKP_VERSION_MAJOR 1
|
||||||
#define EKP_VERSION_MINOR 2
|
#define EKP_VERSION_MINOR 3
|
||||||
|
|
||||||
/* Limits */
|
/* Limits */
|
||||||
#define EKP_MAX_PATTERN_LEN 64
|
#define EKP_MAX_PATTERN_LEN 64
|
||||||
@ -193,6 +193,8 @@ typedef struct {
|
|||||||
double last_line_ratio;
|
double last_line_ratio;
|
||||||
int consec_hyphen_penalty; /* multiplier for consecutive hyphens */
|
int consec_hyphen_penalty; /* multiplier for consecutive hyphens */
|
||||||
double last_line_short_penalty; /* multiplier for short last lines */
|
double last_line_short_penalty; /* multiplier for short last lines */
|
||||||
|
int32_t extra_stretch; /* per-line flexibility for non-justify
|
||||||
|
* alignment (0 = justify) */
|
||||||
} ekp_state_t;
|
} ekp_state_t;
|
||||||
|
|
||||||
/* Global state instance */
|
/* Global state instance */
|
||||||
|
|||||||
@ -137,6 +137,67 @@ Used to verify no content is lost by justification."
|
|||||||
(dolist (k '(1 5 7 10))
|
(dolist (k '(1 5 7 10))
|
||||||
(should (aref ok k)))))
|
(should (aref ok k)))))
|
||||||
|
|
||||||
|
(defun ekp-test--line-glue-widths (line)
|
||||||
|
"Pixel widths of synthesized glue spaces in LINE, in order."
|
||||||
|
(let (ws)
|
||||||
|
(dotimes (i (length line))
|
||||||
|
(let ((d (get-text-property i 'display line)))
|
||||||
|
(when (and (consp d) (eq (car d) 'space)
|
||||||
|
(get-text-property i 'ekp-glue line))
|
||||||
|
(push (car (plist-get (cdr d) :width)) ws))))
|
||||||
|
(nreverse ws)))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-alignment-ragged-right ()
|
||||||
|
"Ragged-right: lines fit, interior spacing stays at ideal."
|
||||||
|
(let ((ekp-alignment 'ragged-right)
|
||||||
|
(text "aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll"))
|
||||||
|
(let ((lines (split-string (ekp-pixel-justify text 12) "\n")))
|
||||||
|
(should (> (length lines) 1))
|
||||||
|
(dolist (line lines)
|
||||||
|
(should (<= (string-pixel-width line) 12))
|
||||||
|
;; all interior glues at ideal (1px word space in batch);
|
||||||
|
;; only the trailing filler may be wider
|
||||||
|
(let ((interior (butlast (ekp-test--line-glue-widths line))))
|
||||||
|
(dolist (w interior) (should (<= w 1))))))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-alignment-ragged-left ()
|
||||||
|
"Ragged-left: every line is pushed flush to the right edge."
|
||||||
|
(let ((ekp-alignment 'ragged-left)
|
||||||
|
(text "aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll"))
|
||||||
|
(dolist (line (split-string (ekp-pixel-justify text 12) "\n"))
|
||||||
|
(should (= (string-pixel-width line) 12)))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-alignment-center ()
|
||||||
|
"Center: leftover splits evenly between the two edges."
|
||||||
|
(let ((ekp-alignment 'center)
|
||||||
|
(text "aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll"))
|
||||||
|
(dolist (line (split-string (ekp-pixel-justify text 12) "\n"))
|
||||||
|
(should (= (string-pixel-width line) 12))
|
||||||
|
(let* ((len (length line))
|
||||||
|
(lead (if (and (> len 0)
|
||||||
|
(get-text-property 0 'ekp-glue line))
|
||||||
|
(or (car (ekp-test--line-glue-widths line)) 0)
|
||||||
|
0))
|
||||||
|
(trail (if (and (> len 0)
|
||||||
|
(get-text-property (1- len) 'ekp-glue line))
|
||||||
|
(or (car (last (ekp-test--line-glue-widths line))) 0)
|
||||||
|
0)))
|
||||||
|
(should (<= (abs (- lead trail)) 1))))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-alignment-c-parity ()
|
||||||
|
"C and elisp engines agree under every alignment mode."
|
||||||
|
(skip-unless (ekp-tests--c-available))
|
||||||
|
(dolist (align '(justify ragged-right ragged-left center))
|
||||||
|
(let ((ekp-alignment align)
|
||||||
|
(text "对齐 parity 检查内容 mixed 中英文字 several words here too"))
|
||||||
|
(let ((a (let ((ekp-use-c-module nil))
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(ekp-pixel-justify text 60)))
|
||||||
|
(b (let ((ekp-use-c-module t))
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(ekp-pixel-justify text 60))))
|
||||||
|
(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