ekp/ekp.el
2025-07-27 21:35:44 +08:00

767 lines
34 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;; -*- lexical-binding: t; -*-
(require 'ekp-utils)
(require 'ekp-hyphen)
(defconst ekp-load-file-name (or load-file-name (buffer-file-name)))
(defvar ekp-latin-lang "en_US")
(defvar ekp-param-use-default-p t
"Used in internal, you should not modify it!")
(defvar ekp-lws-ideal-pixel nil
"The ideal pixel of whitespace between latin words.")
(defvar ekp-lws-stretch-pixel nil
"The stretch pixel of whitespace between latin words.")
(defvar ekp-lws-shrink-pixel nil
"The shrink pixel of whitespace between latin words.")
(defvar ekp-mws-ideal-pixel nil
"The ideal pixel of whitespace between latin word and cjk char.")
(defvar ekp-mws-stretch-pixel nil
"The stretch pixel of whitespace between latin word and cjk char.")
(defvar ekp-mws-shrink-pixel nil
"The shrink pixel of whitespace between latin word and cjk char.")
(defvar ekp-cws-ideal-pixel nil
"The ideal pixel of non-whitespace, such between cjk chars.")
(defvar ekp-cws-stretch-pixel nil
"The stretch pixel of non-whitespace, such between cjk chars.")
(defvar ekp-cws-shrink-pixel nil
"The shrink pixel of non-whitespace, such between cjk chars.")
(defvar ekp-lws-max-pixel nil)
(defvar ekp-lws-min-pixel nil)
(defvar ekp-mws-max-pixel nil)
(defvar ekp-mws-min-pixel nil)
(defvar ekp-cws-max-pixel nil)
(defvar ekp-cws-min-pixel nil)
(defvar ekp-caches
(make-hash-table
:test 'equal :size 100 :rehash-size 1.5 :weakness nil)
"Key of ekp-caches is the hash of string.")
(defun ekp-root-dir ()
(when ekp-load-file-name
(file-name-directory ekp-load-file-name)))
(defun ekp-load-dicts ()
(ekp-hyphen-load-languages
(expand-file-name "./dictionaries" (ekp-root-dir))))
(ekp-load-dicts)
(defun ekp-param-check ()
"Check whether all params are set."
(and ekp-lws-ideal-pixel ekp-lws-stretch-pixel ekp-lws-shrink-pixel
ekp-mws-ideal-pixel ekp-mws-stretch-pixel ekp-mws-shrink-pixel
ekp-cws-ideal-pixel ekp-cws-stretch-pixel ekp-cws-shrink-pixel))
(defun ekp-param-set-default (string)
(let* ((lws-pixel (ekp-word-spacing-pixel string))
(mws-pixel (- lws-pixel 2)))
(ekp-param-set
lws-pixel (round (* lws-pixel 0.5)) (round (* lws-pixel 0.33))
mws-pixel (round (* mws-pixel 0.5)) (round (* mws-pixel 0.33))
0 2 0)))
(defun ekp-param-set ( lws-ideal lws-stretch lws-shrink
mws-ideal mws-stretch mws-shrink
cws-ideal cws-stretch cws-shrink)
(setq ekp-lws-ideal-pixel lws-ideal)
(setq ekp-lws-stretch-pixel lws-stretch)
(setq ekp-lws-shrink-pixel lws-shrink)
(setq ekp-mws-ideal-pixel mws-ideal)
(setq ekp-mws-stretch-pixel mws-stretch)
(setq ekp-mws-shrink-pixel mws-shrink)
(setq ekp-cws-ideal-pixel cws-ideal)
(setq ekp-cws-stretch-pixel cws-stretch)
(setq ekp-cws-shrink-pixel cws-shrink)
(unless (ekp-param-check)
(error "all pixel args should not be nil!"))
(setq ekp-lws-max-pixel (+ ekp-lws-ideal-pixel ekp-lws-stretch-pixel))
(setq ekp-lws-min-pixel (- ekp-lws-ideal-pixel ekp-lws-shrink-pixel))
(setq ekp-mws-max-pixel (+ ekp-mws-ideal-pixel ekp-mws-stretch-pixel))
(setq ekp-mws-min-pixel (- ekp-mws-ideal-pixel ekp-mws-shrink-pixel))
(setq ekp-cws-max-pixel (+ ekp-cws-ideal-pixel ekp-cws-stretch-pixel))
(setq ekp-cws-min-pixel (- ekp-cws-ideal-pixel ekp-cws-shrink-pixel))
(setq ekp-param-use-default-p nil))
(defun ekp-param-fmtstr ()
(format "%s-%s-%s-%s-%s-%s-%s-%s-%s"
ekp-lws-ideal-pixel ekp-lws-stretch-pixel ekp-lws-shrink-pixel
ekp-mws-ideal-pixel ekp-mws-stretch-pixel ekp-mws-shrink-pixel
ekp-cws-ideal-pixel ekp-cws-stretch-pixel ekp-cws-shrink-pixel))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 正确版本:包含完整字符集和组合标记
(defvar ekp-latin-regexp
(concat
"[" ; 开始字符集
"A-Za-z'-" ; 基础拉丁字母
"\300-\326\330-\366\370-\417" ; ISO-8859-1补充
"\u00C0-\u00D6\u00D8-\u00F6" ; Unicode基本补充
"\u00F8-\u00FF\u0100-\u024F" ; 扩展A/B
"\u1E00-\u1EFF" ; 扩展附加
"\uA780-\uA7F9" ; 拉丁扩展-D
"]" ; 闭合字符集
)
"正则表达式匹配所有拉丁字符及其变体,包括组合变音符")
(defun ekp-split-string (string)
;; return (boxes-vector . hyphen-positions-vector)
(let* ((boxes (ekp-split-to-boxes string))
(idx 0)
new-boxes idxs)
(dolist (box (append boxes nil))
(save-match-data
(if (string-match
(format
"^\\([[{<„‚¿¡*@\"']*\\)\\(%s+\\)\\([]}>.,*?\"']*\\)$"
ekp-latin-regexp)
box)
(let* ((pure-word (match-string 2 box))
(left-punct (match-string 1 box))
(right-punct (match-string 3 box))
(word-lst (ekp-hyphen-boxes
(ekp-hyphen-create ekp-latin-lang)
pure-word))
(num (length word-lst)))
(when left-punct
(setf (car word-lst) (concat left-punct (car word-lst))))
(when right-punct
(setf (car (last word-lst)) (concat (car (last word-lst))
right-punct)))
(push word-lst new-boxes)
(dotimes (i num)
(when (< i (1- num))
(push idx idxs))
(cl-incf idx)))
(push (list box) new-boxes)
(cl-incf idx))))
(let* ((boxes-lst (apply #'append (nreverse new-boxes)))
(boxes-lst
(seq-map (lambda (box)
(propertize
box 'face `(:family ,(ekp-latin-font string))))
boxes-lst)))
(cons (vconcat boxes-lst)
(vconcat (nreverse idxs))))))
(defun ekp-str-type (str)
"STR should be single letter string."
(cond
;; a half-width cjk punct
((or (string= "" str) (string= "" str)) 'cjk)
((= (string-width str) 1) 'latin)
((= (string-width str) 2)
(if (ekp-cjk-fw-punct-p (string-to-char str))
'cjk-punct
'cjk))
(t (error "Abnormal string width %s for %s"
(string-width str) str))))
(defun ekp-box-type (box)
(unless (or (null box) (string-empty-p box))
(cons (ekp-str-type (substring box 0 1))
(ekp-str-type (substring box -1)))))
(defun ekp-glue-type (prev-box-type curr-box-type)
"Lws means whitespace between latin words; cws means
whitespace between cjk words; mws means whitespace between
cjk and latin words; nws means no whitespace."
(let ((before (cdr prev-box-type))
(after (car curr-box-type)))
(if before
(cond
((and (eq before 'latin) (eq after 'latin)) 'lws)
((and (eq before 'cjk) (eq after 'cjk)) 'cws)
((or (and (eq before 'cjk) (eq after 'latin))
(and (eq before 'latin) (eq after 'cjk)))
'mws)
((or (eq before 'cjk-punct) (eq after 'cjk-punct)) 'cws))
'nws)))
(defun ekp--glues-types (boxes boxes-types hyphen_positions)
"Set type of all glue in boxes using `ekp-glue-type',
set type to 'nws for each glue after position in hyphen_positions."
;; IMPORTANT!
(let* ((num (length boxes))
(glues-types (make-vector num nil))
prev-box-type curr-box-type)
;; set hyphen position to 'nws
(dolist (i (append hyphen_positions nil))
(aset glues-types (1+ i) 'nws))
(dotimes (i num)
(unless (aref glues-types i)
(let ((curr-box-type (aref boxes-types i)))
(aset glues-types
i (ekp-glue-type prev-box-type curr-box-type))
(setq prev-box-type curr-box-type))))
glues-types))
(defun ekp-glue-ideal-pixel (type)
(cond ((or (null type) (eq 'nws type)) 0)
((eq 'lws type) ekp-lws-ideal-pixel)
((eq 'mws type) ekp-mws-ideal-pixel)
((eq 'cws type) ekp-cws-ideal-pixel)))
(defun ekp-glue-min-pixel (type)
(cond ((or (null type) (eq 'nws type)) 0)
((eq 'lws type) ekp-lws-min-pixel)
((eq 'mws type) ekp-mws-min-pixel)
((eq 'cws type) ekp-cws-min-pixel)))
(defun ekp-glue-max-pixel (type)
(cond ((or (null type) (eq 'nws type)) 0)
((eq 'lws type) ekp-lws-max-pixel)
((eq 'mws type) ekp-mws-max-pixel)
((eq 'cws type) ekp-cws-max-pixel)))
(defun ekp-text-hash (string)
(let ((latin-font (ekp-latin-font string))
(cjk-font (ekp-cjk-font string)))
(secure-hash
'md5 (format "%s|%s|%s" latin-font cjk-font string))))
(defun ekp-text-cache (string)
;; consider font
;; (text-data . param-cache(param-data . dp-cache(dp-data)))
"Return the text cache of STRING. Text cache consists of
(data . param-cache). Data is a plist (:boxes boxes :boxes-widths
boxes-widths :glues-types glues-types)."
(let ((text-hash (ekp-text-hash string)))
(if-let ((_ ekp-caches)
(cache (gethash text-hash ekp-caches)))
cache
(when (or ekp-param-use-default-p
(null (ekp-param-check)))
(ekp-param-set-default string))
(setq ekp-param-use-default-p t)
(let* ((cjk-font (ekp-cjk-font string))
(latin-font (ekp-latin-font string))
(cons (ekp-split-string string))
(boxes (car cons))
(hyphen_after_positions (cdr cons))
(boxes-widths (vconcat (mapcar #'string-pixel-width boxes)))
(boxes-types (vconcat (mapcar #'ekp-box-type boxes)))
(glues-types (ekp--glues-types boxes boxes-types
hyphen_after_positions))
(plist (list :boxes boxes
:latin-font latin-font
:cjk-font cjk-font
:boxes-widths boxes-widths
:boxes-types boxes-types
:glues-types glues-types))
(cache (cons plist nil)))
(unless ekp-caches
(setq ekp-caches (make-hash-table
:test 'equal :size 100
:rehash-size 1.5 :weakness nil)))
(puthash text-hash cache ekp-caches)
cache))))
(defun ekp-text-data (string &optional key)
"Return the data plist of text cache. If KEY is non-nil,
return the value of KEY in plist."
(let ((data (car (ekp-text-cache string))))
(if key
(plist-get data key)
data)))
(defun ekp-boxes (string)
(ekp-text-data string :boxes))
(defun ekp-hyphen-str (string)
(propertize
"-" 'face '(:family (ekp-text-data string :latin-font))))
(defun ekp-hyphen-pixel (string)
(string-pixel-width (ekp-hyphen-str string)))
(defun ekp-boxes-widths (string)
(ekp-text-data string :boxes-widths))
(defun ekp-boxes-types (string)
(ekp-text-data string :boxes-types))
(defun ekp-glues-types (string)
(ekp-text-data string :glues-types))
(defun ekp-param-cache (string)
;; (param-data . dp-cache(dp-data))
"Return a plist of ideal-prefixs, min-prefixs and max-prefixs
by caculating with string and other params."
(if-let* ((param-record (cdr (ekp-text-cache string)))
(param-cache (gethash (ekp-param-fmtstr) param-record)))
param-cache
(let* ((boxes-num (length (ekp-boxes string)))
(boxes-widths (ekp-boxes-widths string))
(glues-types (ekp-glues-types string))
(ideal-prefixs (make-vector (1+ boxes-num) 0))
(min-prefixs (make-vector (1+ boxes-num) 0))
(max-prefixs (make-vector (1+ boxes-num) 0)))
(dotimes (i boxes-num)
(aset ideal-prefixs (1+ i)
(+ (aref ideal-prefixs i) (aref boxes-widths i)
(ekp-glue-ideal-pixel (aref glues-types i))))
(aset min-prefixs (1+ i)
(+ (aref min-prefixs i) (aref boxes-widths i)
(ekp-glue-min-pixel (aref glues-types i))))
(aset max-prefixs (1+ i)
(+ (aref max-prefixs i) (aref boxes-widths i)
(ekp-glue-max-pixel (aref glues-types i)))))
(let* ((param-data (list :ideal-prefixs ideal-prefixs
:min-prefixs min-prefixs
:max-prefixs max-prefixs))
(param-cache (cons param-data nil)))
(if-let ((param-record (cdr (ekp-text-cache string))))
(puthash (ekp-param-fmtstr) param-cache param-record)
(let ((param-record (make-hash-table
:test 'equal :size 100
:rehash-size 1.5 :weakness nil)))
(puthash (ekp-param-fmtstr) param-cache param-record)
(puthash (ekp-text-hash string)
(cons (ekp-text-data string) param-record)
ekp-caches)))
param-cache))))
(defun ekp-param-data (string &optional key)
"Return the data plist of param cache. If KEY is non-nil,
return the value of KEY in plist."
(let ((data (car (ekp-param-cache string))))
(if key
(plist-get data key)
data)))
(defun ekp-ideal-prefixs (string)
(ekp-param-data string :ideal-prefixs))
(defun ekp-min-prefixs (string)
(ekp-param-data string :min-prefixs))
(defun ekp-max-prefixs (string)
(ekp-param-data string :max-prefixs))
(defun ekp--gaps-list (glues-types)
(list (seq-count (lambda (it) (eq 'lws it)) glues-types)
(seq-count (lambda (it) (eq 'mws it)) glues-types)
(seq-count (lambda (it) (eq 'cws it)) glues-types)))
(defun ekp--line-cost-and-gaps (ideal-pixel line-pixel glues-types)
"Return the cost ratio of WORDS limited to LINE-PIXEL."
(let* ((glues-types (seq-drop glues-types 1))
(gaps-list (ekp--gaps-list glues-types))
(latin-gaps (nth 0 gaps-list))
(mix-gaps (nth 1 gaps-list))
(cjk-gaps (nth 2 gaps-list))
(rest-pixel (- line-pixel ideal-pixel))
ratio)
(if (> rest-pixel 0)
;; should stretch
(setq ratio
(/ rest-pixel
(float (+ (* latin-gaps ekp-lws-stretch-pixel)
(* cjk-gaps ekp-cws-stretch-pixel)
(* mix-gaps ekp-mws-stretch-pixel)))))
;; should shrink
(setq ratio
(/ rest-pixel
(float (+ (* latin-gaps ekp-lws-shrink-pixel)
(* mix-gaps ekp-mws-shrink-pixel))))))
(list :cost (* 100 (expt ratio 3)) :gaps gaps-list)))
(defun ekp-hyphenate-p (glues-types n)
(and (< n (length glues-types))
(eq 'nws (aref glues-types n))))
(defun ekp-dp-cache (string line-pixel)
(if-let* ((dp-record (cdr (ekp-param-cache string)))
(dp-cache (gethash line-pixel dp-record)))
dp-cache
(let* ((glues-types (ekp-glues-types string))
(boxes (ekp-boxes string))
(hyphen-pixel (ekp-hyphen-pixel string))
(n (length boxes))
(ideal-prefixs (ekp-ideal-prefixs string))
(min-prefixs (ekp-min-prefixs string))
(max-prefixs (ekp-max-prefixs string))
(backptrs (make-vector (1+ n) nil))
(costs (make-vector (1+ n) nil))
;; rest pixel = line-pixel - ideal-pixel
(rests (make-vector (1+ n) nil))
(gaps (make-vector (1+ n) nil))
;; 连续行 hyphen 结尾计数
(hyphen-line-count 0))
(dotimes (i (1+ n))
(aset costs i (if (= i 0) 0.0 nil)))
(dotimes (i (1+ n))
(when (aref costs i)
(setq hyphen-line-count 0)
(catch 'break
(dotimes (j (- n i))
(let* ((k (+ i j 1)) ;; k: end word index (exclusive)
(is-last (= k n))
(end-with-hyphenp (ekp-hyphenate-p glues-types k))
(ideal-pixel (- (aref ideal-prefixs k)
(aref ideal-prefixs i)
(ekp-glue-ideal-pixel
(aref glues-types i))))
(max-pixel (- (aref max-prefixs k)
(aref max-prefixs i)
(ekp-glue-max-pixel
(aref glues-types i))))
(min-pixel (- (aref min-prefixs k)
(aref min-prefixs i)
(ekp-glue-min-pixel
(aref glues-types i)))))
;; ends with hyphen, plus the pixel of hyphen
(when end-with-hyphenp
(cl-incf ideal-pixel hyphen-pixel)
(cl-incf max-pixel hyphen-pixel)
(cl-incf min-pixel hyphen-pixel))
;; back to last word
(when (or (> min-pixel line-pixel)
(and is-last (> ideal-pixel line-pixel)))
(when (null (aref costs (1- k)))
;; can not find a proper line break,
;; break line at prev box
(let* ((hyphenate-p (ekp-hyphenate-p glues-types (1- k)))
(ideal-pixel (- (aref ideal-prefixs (1- k))
(aref ideal-prefixs i)
(ekp-glue-ideal-pixel
(aref glues-types i))))
(rest-pixel (- line-pixel ideal-pixel)))
(when hyphenate-p (cl-incf ideal-pixel hyphen-pixel))
(aset costs (1- k) (+ 100 (expt rest-pixel 3)))
(aset rests (1- k) rest-pixel)
(aset backptrs (1- k) i)
(aset gaps (1- k)
(ekp--gaps-list
(seq-drop (cl-subseq glues-types i (1- k)) 1)))
;; (elog-debug "1-k:%s; rests:%S" (1- k)
;; (aref rests (1- k)))
))
(throw 'break nil))
(when (or (<= min-pixel line-pixel max-pixel)
(and is-last (<= ideal-pixel line-pixel)))
(let* ((line-gaps)
(line-cost
(cond
;; only has one word
((= j 0)
(expt (- ideal-pixel line-pixel) 3))
(is-last 0.0)
;; has more than one word
(t (let* ((cost-and-gaps
(ekp--line-cost-and-gaps
ideal-pixel line-pixel
(seq-subseq glues-types i k)))
(cost (plist-get cost-and-gaps :cost))
(gaps (plist-get cost-and-gaps :gaps)))
(setq line-gaps gaps)
(if end-with-hyphenp
(progn
;; add extra cost of hypen
(cl-incf hyphen-line-count)
(+ cost (* 1000 hyphen-line-count)))
(setq hyphen-line-count 0)
cost)))))
(total-cost (+ (aref costs i) line-cost)))
;; (message "total cost:%S" total-cost)
;; (message "hyphen-line-count:%S" hyphen-line-count)
(when (or (null (aref costs k))
(< (abs total-cost) (abs (aref costs k))))
;; set all for cost is smaller!
(aset rests k (- line-pixel ideal-pixel))
(aset gaps k line-gaps)
(aset costs k total-cost)
;; 断点设置为 当前行的起点 = 上一行的和结束点
(aset backptrs k i)))))))))
(let ((breaks (list n))
(index n))
(while (> index 0)
(let ((prev (aref backptrs index)))
(if prev (progn (push prev breaks)
(setq index prev))
(setq index (1- index)))))
(let* ((breaks (cdr breaks))
lines-rests lines-gaps dp-cache)
(dolist (i breaks)
(push (aref rests i) lines-rests)
(push (aref gaps i) lines-gaps))
;; set dp cache
(setq dp-cache (list :rests (nreverse lines-rests)
:gaps (nreverse lines-gaps)
:breaks breaks
:cost (aref costs (length boxes))))
;; update param cache
(if-let ((dp-record (cdr (ekp-param-cache string))))
(puthash line-pixel dp-cache dp-record)
(let ((dp-record (make-hash-table
:test 'equal :size 100
:rehash-size 1.5 :weakness nil)))
(puthash line-pixel dp-cache dp-record)
(puthash (ekp-param-fmtstr)
(cons (ekp-param-data string) dp-record)
(cdr (ekp-text-cache string)))))
dp-cache)))))
(defun ekp-dp-data (string line-pixel &optional key)
"Return the data plist of dp cache. If KEY is non-nil,
return the value of KEY in plist."
(let ((data (ekp-dp-cache string line-pixel)))
(if key
(plist-get data key)
data)))
(defun ekp-total-cost (string line-pixel)
"Return the COST of kp algorithm."
(ekp-dp-data string line-pixel :cost))
(defun ekp-line-breaks (string line-pixel)
"Return the break points of kp algorithm."
(ekp-dp-data string line-pixel :breaks))
(defun ekp-line-glues (string line-pixel)
"Line glues include glues before first box and after last box.
So the length of line glues is: line-boxes-num + 1"
(let* ((boxes-widths (ekp-boxes-widths string))
(boxes-num (length (ekp-boxes string)))
(glues-types (ekp-glues-types string))
(ideal-prefixs (ekp-ideal-prefixs string))
(max-prefixs (ekp-max-prefixs string))
(breaks (ekp-line-breaks string line-pixel))
(line-glues (make-vector (length breaks) nil))
(start 0))
(dotimes (i (length breaks))
(let* ((end (nth i breaks))
(line-boxes-widths (cl-subseq boxes-widths start end))
(line-glues-types
;; exclude glue before word at the start of line
(seq-drop (cl-subseq glues-types start end) 1))
(is-last (>= end boxes-num))
line-glue)
(setq line-glue
(cond
((= 1 (length line-boxes-widths))
(list 0 (- line-pixel (aref line-boxes-widths 0))))
(is-last
(append '(0)
(mapcar #'ekp-glue-ideal-pixel line-glues-types)
(list
(- line-pixel (- (aref ideal-prefixs end)
(aref ideal-prefixs start)
(ekp-glue-ideal-pixel
(aref glues-types start)))))))
(t
;; (elog-debug "-----------------")
;; (elog-debug "glues-types:%s" line-glues-types)
(let ((max-pixel (- (aref max-prefixs end)
(aref max-prefixs start)
(ekp-glue-max-pixel
(aref glues-types start)))))
;; ends with hyphen
(when (ekp-hyphenate-p glues-types end)
(cl-incf max-pixel (ekp-hyphen-pixel string)))
;; (elog-debug "start:%s; end:%s; max:%s" start end max-pixel)
(if (< max-pixel line-pixel)
(progn
;; 行尾直接断行的情况
;; (elog-debug "暴力断行 i:%s pixel:%s"
;; i (- line-pixel max-pixel))
(append '(0)
(mapcar #'ekp-glue-max-pixel line-glues-types)
(list (- line-pixel max-pixel))))
;; 正常情况
(let ((ideal-pixel (- (aref ideal-prefixs end)
(aref ideal-prefixs start)
(ekp-glue-ideal-pixel
(aref glues-types start)))))
;; ideal-pixel 包含第一个box之前的glue
;; (elog-debug "boxes:%S" (cl-subseq boxes start end))
;; (elog-debug "line:%s; ideal:%s; rest:%s; stored-rest:%s"
;; line-pixel ideal-pixel (- line-pixel ideal-pixel)
;; (nth i (ekp-dp-data string line-pixel :rests)))
)
(let* ((lines-rests (ekp-dp-data string line-pixel :rests))
(curr-rest-pixel (nth i lines-rests)))
;; (elog-debug "curr-rest-pixel:%s" curr-rest-pixel)
(cond
((= curr-rest-pixel 0)
(append '(0) (mapcar #'ekp-glue-ideal-pixel
line-glues-types)
'(0)))
(t
(let* ((lines-gaps (ekp-dp-data string line-pixel :gaps))
(line-gaps (nth i lines-gaps))
(_ ;; (elog-debug "line-gaps:%S" line-gaps)
)
(latin-gaps (nth 0 line-gaps))
(mix-gaps (nth 1 line-gaps))
(cjk-gaps (nth 2 line-gaps))
(latin-gap-pixel 0) (latin-extra-gaps 0)
(mix-gap-pixel 0) (mix-extra-gaps 0)
(cjk-gap-pixel 0) (cjk-extra-gaps 0)
(line-rest-pixel (abs curr-rest-pixel)))
;; max-latin-change include stretch or shrink
(let* ((latin-gap-change (if (> curr-rest-pixel 0)
ekp-lws-stretch-pixel
ekp-lws-shrink-pixel))
(latin-max-pixel (* latin-gaps latin-gap-change)))
(if (< (- line-rest-pixel latin-max-pixel) 0)
(when (> latin-gaps 0)
;; only stretch latin glues
;; (elog-debug "latin-gap-pixel:%s"
;; (/ line-rest-pixel latin-gaps))
;; (elog-debug "latin-extra-gaps:%s"
;; (% line-rest-pixel latin-gaps))
(setq latin-gap-pixel (/ line-rest-pixel latin-gaps))
(setq latin-extra-gaps (% line-rest-pixel latin-gaps)))
;; stretch latin glues max and continue to stretch mix glues
(setq latin-gap-pixel latin-gap-change)
(setq line-rest-pixel (- line-rest-pixel latin-max-pixel))
(let* ((mix-gap-change (if (> curr-rest-pixel 0)
ekp-mws-stretch-pixel
ekp-mws-shrink-pixel))
(mix-max-pixel (* mix-gaps mix-gap-change)))
(if (< (- line-rest-pixel mix-max-pixel) 0)
(when (> mix-gaps 0)
;; only stretch mix glues
(setq mix-gap-pixel (/ line-rest-pixel mix-gaps))
(setq mix-extra-gaps (% line-rest-pixel mix-gaps)))
;; stretch mix glues max and continue to stretch cjk glues
(setq mix-gap-pixel mix-gap-change)
(setq line-rest-pixel (- line-rest-pixel mix-max-pixel))
(when (> cjk-gaps 0)
(setq cjk-gap-pixel (/ line-rest-pixel cjk-gaps))
(setq cjk-extra-gaps (% line-rest-pixel cjk-gaps)))))))
(let* ((latin-extra-index -1)
(mix-extra-index -1)
(cjk-extra-index -1)
(glue-pixel-lst
(mapcar
(lambda (type)
(let ((pixel (cond
((eq type 'lws)
(cl-incf latin-extra-index)
(if (< latin-extra-index latin-extra-gaps)
(1+ latin-gap-pixel)
latin-gap-pixel))
((eq type 'mws)
(cl-incf mix-extra-index)
(if (< mix-extra-index mix-extra-gaps)
(1+ mix-gap-pixel)
mix-gap-pixel))
((eq type 'cws)
(cl-incf cjk-extra-index)
(if (< cjk-extra-index cjk-extra-gaps)
(1+ cjk-gap-pixel)
cjk-gap-pixel))
((eq type 'nws) 0))))
(if (> curr-rest-pixel 0)
;; stretch
(+ (ekp-glue-ideal-pixel type) pixel)
;; shrink
(- (ekp-glue-ideal-pixel type) pixel))))
line-glues-types)))
;; (elog-debug "glue-pixel-lst:%S" glue-pixel-lst)
;; (elog-debug "--------------")
(append '(0) glue-pixel-lst '(0))))))))))))
(aset line-glues i (vconcat line-glue nil))
(setq start end)))
line-glues))
(defun ekp-combine-glues-and-boxes (glues boxes)
(let* ((glues (append glues nil))
(last-glue (car (last glues)))
(glues (-drop-last 1 glues))
(boxes (append boxes nil)))
(if (= (length glues) (length boxes))
(string-join (append (-interleave glues boxes)
(list last-glue)))
(error "(length glues) + 1 != (length boxes)"))))
(defun ekp--pixel-justify (string line-pixel)
"Justify single STRING to LINE-PIXEL."
(let* ((boxes (ekp-boxes string))
(hyphen (ekp-hyphen-str string))
(breaks (ekp-line-breaks string line-pixel))
(num (length breaks))
(lines-glues (ekp-line-glues string line-pixel))
(glues-types (ekp-glues-types string))
(start 0) strings)
(dotimes (i num)
(let* ((end (nth i breaks))
(line-boxes (cl-subseq boxes start end))
(line-glues (mapcar #'ekp-pixel-spacing
(aref lines-glues i))))
;; not last line and glue is 'nws, should add hyphen
(when (ekp-hyphenate-p glues-types end)
(setf (aref line-boxes (- end start 1))
(concat (aref line-boxes (- end start 1)) hyphen)))
(push (ekp-combine-glues-and-boxes line-glues line-boxes)
strings)
(setq start end)))
(mapconcat 'identity (nreverse strings) "\n")))
(defun ekp-pixel-justify (string line-pixel)
"Justify multiline STRING to LINE-PIXEL."
(let ((strs (split-string string "\n")))
(mapconcat (lambda (str)
(if (string-blank-p str)
""
(ekp--pixel-justify str line-pixel)))
strs "\n")))
(defun ekp-pixel-range-justify (string min-pixel max-pixel)
"Find the optimal breakpoint for STRING typesetting between
a MIN-PIXEL and MAX-PIXEL width and return a cons-cell. The car
of it is the typeset tex and cdr is the best pixel."
(let* ((strings (split-string string "\n"))
(best-pixel max-pixel)
(best-cost-lst
(mapcar (lambda (string)
(if (string-blank-p string)
0
(abs (ekp-total-cost string max-pixel))))
strings))
;; get average cost of all lines' costs
(best-cost (/ (float (apply #'+ best-cost-lst))
(length best-cost-lst)))
(curr-pixel max-pixel))
(while (>= curr-pixel min-pixel)
(let* ((cost-lst
(mapcar (lambda (string)
(if (string-blank-p string)
0
(abs (ekp-total-cost string curr-pixel))))
strings))
(curr-cost (/ (float (apply #'+ cost-lst))
(length cost-lst))))
(when (< curr-cost best-cost)
(progn
(setq best-cost curr-cost)
(setq best-pixel curr-pixel)))
(cl-decf curr-pixel 1)))
(cons (ekp-pixel-justify string best-pixel) best-pixel)))
(provide 'ekp)