From 50d41ed30f53a7d8b8025e598a27ec4a28ce83da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 May 2026 15:22:52 +0000 Subject: [PATCH] fix: enforce kinsoku rules for CJK opening punctuation in box splitting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening brackets ((, 「, 《, etc.) are now attached to the following character instead of the preceding one, preventing them from appearing at the end of a line which violates kinsoku typographic rules. Fixes #9 Agent-Logs-Url: https://github.com/Kinneyzhang/emacs-kp/sessions/d05069f4-db64-4601-9816-f582edad4a1b Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com> --- ekp-utils.el | 52 ++++++++++++++++++++++++++++++++++++++++---------- ekp-utils.elc | Bin 0 -> 11533 bytes 2 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 ekp-utils.elc diff --git a/ekp-utils.el b/ekp-utils.el index 49bb77c..80149bb 100644 --- a/ekp-utils.el +++ b/ekp-utils.el @@ -115,6 +115,14 @@ (and (>= char #x3000) (<= char #x303F)) (and (>= char #xFF00) (<= char #xFF60))))) +(defun ekp-cjk-opening-punct-p (str) + "Return non-nil if STR ends with a CJK opening punctuation. +These characters must not appear at the end of a line (kinsoku rule). +When STR is held as cjk-char, this checks if it still needs attachment." + (let ((char (aref str (1- (length str))))) + (memq (get-char-code-property char 'general-category) + '(Ps Pi)))) + (defun ekp--flush-latin-word (word boxes) "Push latin WORD to BOXES if non-nil. Return updated boxes." (if word (cons word boxes) boxes)) @@ -161,20 +169,41 @@ Return (new-state new-latin-word new-cjk-char new-boxes)." ;; Already in latin mode: accumulate (list 1 (concat latin-word str) nil boxes) ;; Was in CJK mode: flush CJK char, switch to latin - (list 1 str nil (ekp--flush-cjk-char cjk-char boxes)))) + ;; If held cjk-char is opening punct, prepend it to the latin word + (if (and cjk-char (ekp-cjk-opening-punct-p cjk-char)) + (list 1 (concat cjk-char str) nil boxes) + (list 1 str nil (ekp--flush-cjk-char cjk-char boxes))))) (defun ekp--handle-cjk-char (str state latin-word cjk-char boxes) "Handle a CJK (width=2) character. Return (new-state new-latin-word new-cjk-char new-boxes)." (if (= state 1) ;; Was in latin mode: flush latin word, push CJK directly - (list 2 nil nil (cons str (ekp--flush-latin-word latin-word boxes))) + (let ((new-boxes (ekp--flush-latin-word latin-word boxes))) + (if (ekp-cjk-opening-punct-p str) + ;; Opening punct: hold as cjk-char (will attach to next char) + (list 2 nil str new-boxes) + (list 2 nil nil (cons str new-boxes)))) ;; Already in CJK mode - (if (ekp-cjk-fw-punct-p str) - ;; Punctuation attaches to previous CJK char - (list 2 nil nil (cons (concat cjk-char str) boxes)) - ;; Regular CJK char: flush previous, hold current - (list 2 nil str (ekp--flush-cjk-char cjk-char boxes))))) + (cond + ((ekp-cjk-opening-punct-p str) + ;; Opening punct: cannot end a line (kinsoku rule). + ;; If previous held char is also opening punct, concatenate them. + ;; Otherwise flush previous and hold this opening punct. + (if (and cjk-char (ekp-cjk-opening-punct-p cjk-char)) + (list 2 nil (concat cjk-char str) boxes) + (list 2 nil str (ekp--flush-cjk-char cjk-char boxes)))) + ((ekp-cjk-fw-punct-p str) + ;; Closing/other punct: attaches to previous CJK char + (list 2 nil nil (cons (concat cjk-char str) boxes))) + (t + ;; Regular CJK char: prepend any held opening punct + (if (and cjk-char (ekp-cjk-opening-punct-p cjk-char)) + ;; Previous was opening punct: combine with current char and hold. + ;; Now last char is regular, so this won't be detected as opening. + (list 2 nil (concat cjk-char str) boxes) + ;; Normal case: flush previous, hold current + (list 2 nil str (ekp--flush-cjk-char cjk-char boxes))))))) (defun ekp-split-to-boxes (string) "Split STRING into typographic boxes. @@ -197,9 +226,12 @@ Whitespace runs are preserved as separate boxes; CJK punctuation attaches to pre (cond ;; Whitespace or zero-width: flush content, accumulate spaces ((or (string-blank-p str) (= 0 width)) - (setq boxes (ekp--flush-cjk-char cjk-char boxes)) - (when cjk-char (setq prev-state 2)) - (setq cjk-char nil) + ;; Don't flush opening punct - keep it held for attachment to next char + (if (and cjk-char (ekp-cjk-opening-punct-p cjk-char)) + nil ; keep cjk-char as-is + (setq boxes (ekp--flush-cjk-char cjk-char boxes)) + (when cjk-char (setq prev-state 2)) + (setq cjk-char nil)) (setq boxes (ekp--flush-latin-word latin-word boxes)) (when latin-word (setq prev-state 1)) (setq latin-word nil) diff --git a/ekp-utils.elc b/ekp-utils.elc new file mode 100644 index 0000000000000000000000000000000000000000..62e9dc2b7856f55bd3ab55d8c36ae9f46a7ec6be GIT binary patch literal 11533 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 literal 0 HcmV?d00001