feat: parshape and first-line indentation (Elisp 2D path)
ekp-parshape gives each line its own (INDENT . WIDTH) — TeX \parshape — with the last entry repeating; ekp-first-line-indent is sugar for the CJK 2-em paragraph convention (t derives 2 em from the paragraph's CJK font, or pass pixels). Line-number-dependent widths require the (position × line-count) DP, so both ride the loose engine and bypass the C module, exactly like looseness. Indents render as leading spacers marked ekp-glue, so buffer roundtrips remain character- and property-exact. Composes with alignment modes (indent adds to the centering lead) and protrusion. Tests: 63 ERT green; fuzz 300/300. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f6aa64b3b1
commit
720b1cd0e4
73
ekp.el
73
ekp.el
@ -120,6 +120,19 @@ 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-parshape nil
|
||||
"Per-line layout, as a sequence of (INDENT . WIDTH) cons cells.
|
||||
Line i (0-based) uses element i; lines beyond the last element reuse
|
||||
it (like TeX \\parshape). INDENT is the left offset in pixels,
|
||||
WIDTH the text width — the rendered line occupies INDENT + WIDTH.
|
||||
Line-number-dependent widths require the (position × line-count) DP,
|
||||
so this is Elisp-only: the C module is bypassed while set.")
|
||||
|
||||
(defvar ekp-first-line-indent nil
|
||||
"First-line indentation: pixels, or t for 2 em of the paragraph font.
|
||||
Sugar for the common CJK paragraph convention; ignored when
|
||||
`ekp-parshape' is set. Elisp-only, like `ekp-parshape'.")
|
||||
|
||||
(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
|
||||
@ -483,6 +496,8 @@ are derived per string)."
|
||||
ekp-alignment
|
||||
ekp-ragged-stretch-pixel
|
||||
(and ekp-protrusion ekp-protrusion-ratios)
|
||||
ekp-parshape
|
||||
ekp-first-line-indent
|
||||
(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
|
||||
@ -552,6 +567,39 @@ box's. 0 when `ekp-protrusion' was off at paragraph build time."
|
||||
(or ekp-ragged-stretch-pixel
|
||||
(max 1 (* 8 (or ekp-lws-ideal-pixel 1)))))
|
||||
|
||||
(defun ekp--parshape-active-p ()
|
||||
"Non-nil when per-line widths are in effect (parshape or indent)."
|
||||
(or ekp-parshape ekp-first-line-indent))
|
||||
|
||||
(defun ekp--first-indent-pixel (para)
|
||||
"Resolve `ekp-first-line-indent' to pixels for PARA."
|
||||
(cond
|
||||
((numberp ekp-first-line-indent) ekp-first-line-indent)
|
||||
(ekp-first-line-indent
|
||||
(* 2 (string-pixel-width
|
||||
(propertize "字" 'face
|
||||
(list :family (ekp-para-cjk-font para))))))
|
||||
(t 0)))
|
||||
|
||||
(defun ekp--line-spec (para line-index measure)
|
||||
"Layout of LINE-INDEX (0-based) as (INDENT . WIDTH).
|
||||
MEASURE is the paragraph measure passed to the justify call.
|
||||
`ekp-parshape' takes precedence; its last entry repeats. Otherwise
|
||||
`ekp-first-line-indent' shifts line 0. WIDTH never drops below 1."
|
||||
(cond
|
||||
(ekp-parshape
|
||||
(let* ((shape (if (vectorp ekp-parshape)
|
||||
(append ekp-parshape nil)
|
||||
ekp-parshape))
|
||||
(spec (or (nth line-index shape) (car (last shape)))))
|
||||
(cons (max 0 (car spec)) (max 1 (cdr spec)))))
|
||||
(ekp-first-line-indent
|
||||
(if (= line-index 0)
|
||||
(let ((indent (ekp--first-indent-pixel para)))
|
||||
(cons indent (max 1 (- measure indent))))
|
||||
(cons 0 measure)))
|
||||
(t (cons 0 measure))))
|
||||
|
||||
(defun ekp--make-para (string)
|
||||
"Create and fully initialize `ekp-para' struct for STRING.
|
||||
Computes ALL data in one pass: text, params, and prefix arrays."
|
||||
@ -880,8 +928,10 @@ stripped space-box runs, plus hyphen width when the line hyphenates."
|
||||
;; least as bad as the worst regular line.
|
||||
|
||||
(defun ekp--dp-cache-elisp (para line-pixel)
|
||||
"Pure Elisp DP implementation. Returns and caches the dp-result plist."
|
||||
(if (/= ekp-looseness 0)
|
||||
"Pure Elisp DP implementation. Returns and caches the dp-result plist.
|
||||
Looseness and per-line widths (parshape/first-line indent) need the
|
||||
\(position × line-count) DP."
|
||||
(if (or (/= ekp-looseness 0) (ekp--parshape-active-p))
|
||||
(ekp--dp-cache-elisp-loose para line-pixel)
|
||||
(let ((dp-result (or (ekp--dp-run-1d para line-pixel nil)
|
||||
(ekp--dp-run-1d para line-pixel t))))
|
||||
@ -1161,6 +1211,8 @@ breaks when no valid layout exists."
|
||||
(dotimes (i n)
|
||||
(dolist (lc (aref counts-at i))
|
||||
(let* ((st (gethash (cons i lc) states))
|
||||
;; per-line layout: line LC (0-based) may have its own width
|
||||
(this-width (cdr (ekp--line-spec para lc line-pixel)))
|
||||
(prev-dem (aref st 0))
|
||||
(prev-fitness (aref st 2))
|
||||
(prev-hyphen-count (aref st 3))
|
||||
@ -1185,7 +1237,7 @@ breaks when no valid layout exists."
|
||||
(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
|
||||
(lw (+ this-width
|
||||
(if end-with-hyphenp
|
||||
hyphen-protrude
|
||||
(aref tail-protrudes k))))
|
||||
@ -1341,8 +1393,10 @@ CANDIDATE is (DEM-DELTA REST GAPS FITNESS HYPHEN-COUNT)."
|
||||
(and ekp-use-c-module
|
||||
(boundp 'ekp-c-module-loaded) ekp-c-module-loaded
|
||||
(fboundp 'ekp-c-break-with-arrays)
|
||||
;; looseness needs the (position × line-count) DP, Elisp only
|
||||
(= ekp-looseness 0)))
|
||||
;; looseness and per-line widths need the (position × line-count)
|
||||
;; DP, Elisp only
|
||||
(= ekp-looseness 0)
|
||||
(not (ekp--parshape-active-p))))
|
||||
|
||||
(defun ekp--c-sync-params ()
|
||||
"Push current K-P penalty settings to the C module."
|
||||
@ -1627,8 +1681,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)))
|
||||
;; per-line layout (parshape / first-line indent)
|
||||
(line-spec (ekp--line-spec para i line-pixel))
|
||||
(line-indent (car line-spec))
|
||||
;; right-edge protrusion widens this line's effective target
|
||||
(eff-pixel (+ line-pixel
|
||||
(eff-pixel (+ (cdr line-spec)
|
||||
(if hyphen-p
|
||||
(ekp-para-hyphen-protrude para)
|
||||
(aref (ekp-para-tail-protrudes para) end))))
|
||||
@ -1691,6 +1748,10 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
||||
(append (list filler)
|
||||
(cdr (butlast glue-list))
|
||||
(list 0))))))
|
||||
;; left indent renders as a leading spacer
|
||||
(when (> line-indent 0)
|
||||
(setq glue-list (cons (+ (car glue-list) line-indent)
|
||||
(cdr glue-list))))
|
||||
(aset line-glues i (vconcat glue-list))
|
||||
(setq start end)))
|
||||
line-glues))
|
||||
|
||||
@ -166,6 +166,15 @@
|
||||
;; re-enable so with-mode's cleanup disable is a no-op state-wise
|
||||
(ekp-auto-justify-mode 1))))
|
||||
|
||||
(ert-deftest ekp-region-test-indent-roundtrip ()
|
||||
"First-line indent spacers vanish exactly on unjustify."
|
||||
(let ((ekp-first-line-indent 6)
|
||||
(text "首行缩进往返检查内容足够长断行几次"))
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) 30)
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (equal-including-properties (buffer-string) text)))))
|
||||
|
||||
(ert-deftest ekp-region-test-resize-hook-window-arg ()
|
||||
"The resize hook handles its WINDOW argument and foreign current buffer.
|
||||
Regression: buffer-local `window-size-change-functions' members get
|
||||
|
||||
@ -198,6 +198,33 @@ Used to verify no content is lost by justification."
|
||||
(ekp-pixel-justify text 60))))
|
||||
(should (string= a b))))))
|
||||
|
||||
(ert-deftest ekp-test-first-line-indent ()
|
||||
"First line carries an indent spacer; all lines fill the measure."
|
||||
(let ((ekp-first-line-indent 6)
|
||||
(text "缩进段落的内容足够长可以断成好几行来验证首行缩进的正确表现"))
|
||||
(let ((lines (split-string (ekp-pixel-justify text 30) "\n")))
|
||||
(should (> (length lines) 2))
|
||||
(should (get-text-property 0 'ekp-glue (car lines)))
|
||||
(should-not (get-text-property 0 'ekp-glue (cadr lines)))
|
||||
(dolist (l (butlast lines))
|
||||
(should (= (string-pixel-width l) 30))))))
|
||||
|
||||
(ert-deftest ekp-test-parshape ()
|
||||
"Per-line (INDENT . WIDTH) specs shape the paragraph."
|
||||
(let ((ekp-parshape '((0 . 20) (6 . 24) (0 . 30)))
|
||||
(text "参差形状段落内容也要足够长以便验证每一行宽度设置都生效呢"))
|
||||
(let ((lines (split-string (ekp-pixel-justify text 30) "\n")))
|
||||
(should (>= (length lines) 3))
|
||||
(should (= (string-pixel-width (nth 0 lines)) 20))
|
||||
(should (= (string-pixel-width (nth 1 lines)) 30))
|
||||
(dolist (l (butlast (cddr lines)))
|
||||
(should (= (string-pixel-width l) 30))))))
|
||||
|
||||
(ert-deftest ekp-test-parshape-bypasses-c ()
|
||||
"Per-line widths force the Elisp 2D path."
|
||||
(let ((ekp-first-line-indent 6))
|
||||
(should-not (ekp--c-available-p))))
|
||||
|
||||
(ert-deftest ekp-test-protrusion-hangs-line-end-punct ()
|
||||
"Protrusion lets line-final fullwidth punctuation hang past the edge."
|
||||
(let ((ekp-protrusion t)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user