527 lines
17 KiB
EmacsLisp
527 lines
17 KiB
EmacsLisp
;;; my-yatex-review-highlight.el --- Highlight review macros in YaTeX -*- lexical-binding: t; -*-
|
|
|
|
(require 'cl-lib)
|
|
|
|
;; ============================================================
|
|
;; Faces
|
|
;;
|
|
;; \RA-\RD:
|
|
;; 第1引数: 文字色 + 背景色 + 太字
|
|
;; 第2引数: 文字色のみ、背景は変更しない
|
|
;;
|
|
;; \TO:
|
|
;; 第1引数: 赤太字、背景は変更しない
|
|
;; \XX:
|
|
;; 学生が修正した部分。第1引数を黄色太字で表示する。
|
|
;;
|
|
;; \CA-\CD:
|
|
;; \CA は \RA と同じ色
|
|
;; \CB は \RB と同じ色
|
|
;; \CC は \RC と同じ色
|
|
;; \CD は \RD と同じ色
|
|
;;
|
|
;; {A1} などのラベル部分: label-face
|
|
;; その後の本文: body-face
|
|
;; ============================================================
|
|
|
|
(defface my-latex-RA-label-face '((t ())) "Face for first argument of \\RA.")
|
|
(defface my-latex-RA-body-face '((t ())) "Face for second argument of \\RA.")
|
|
|
|
(defface my-latex-RB-label-face '((t ())) "Face for first argument of \\RB.")
|
|
(defface my-latex-RB-body-face '((t ())) "Face for second argument of \\RB.")
|
|
|
|
(defface my-latex-RC-label-face '((t ())) "Face for first argument of \\RC.")
|
|
(defface my-latex-RC-body-face '((t ())) "Face for second argument of \\RC.")
|
|
|
|
(defface my-latex-RD-label-face '((t ())) "Face for first argument of \\RD.")
|
|
(defface my-latex-RD-body-face '((t ())) "Face for second argument of \\RD.")
|
|
|
|
(defface my-latex-TO-body-face '((t ())) "Face for argument of \\TO.")
|
|
(defface my-latex-XX-body-face '((t ())) "Face for argument of \\XX.")
|
|
|
|
|
|
(defvar my-yatex-review-label-background "gray30"
|
|
"Background color for review macro label faces.")
|
|
|
|
|
|
(defun my-yatex-review-set-faces ()
|
|
"Set review macro faces explicitly."
|
|
|
|
;; RA / CA: red
|
|
(set-face-attribute
|
|
'my-latex-RA-label-face nil
|
|
:foreground "tomato"
|
|
:background my-yatex-review-label-background
|
|
:weight 'bold)
|
|
(set-face-attribute
|
|
'my-latex-RA-body-face nil
|
|
:foreground "tomato"
|
|
:background 'unspecified
|
|
:weight 'unspecified)
|
|
|
|
;; RB / CB: light blue
|
|
(set-face-attribute
|
|
'my-latex-RB-label-face nil
|
|
:foreground "DeepSkyBlue"
|
|
:background my-yatex-review-label-background
|
|
:weight 'bold)
|
|
(set-face-attribute
|
|
'my-latex-RB-body-face nil
|
|
:foreground "DeepSkyBlue"
|
|
:background 'unspecified
|
|
:weight 'unspecified)
|
|
|
|
;; RC / CC: green
|
|
(set-face-attribute
|
|
'my-latex-RC-label-face nil
|
|
:foreground "SpringGreen2"
|
|
:background my-yatex-review-label-background
|
|
:weight 'bold)
|
|
(set-face-attribute
|
|
'my-latex-RC-body-face nil
|
|
:foreground "SpringGreen2"
|
|
:background 'unspecified
|
|
:weight 'unspecified)
|
|
|
|
;; RD / CD: purple
|
|
(set-face-attribute
|
|
'my-latex-RD-label-face nil
|
|
:foreground "MediumOrchid1"
|
|
:background my-yatex-review-label-background
|
|
:weight 'bold)
|
|
(set-face-attribute
|
|
'my-latex-RD-body-face nil
|
|
:foreground "MediumOrchid1"
|
|
:background 'unspecified
|
|
:weight 'unspecified)
|
|
|
|
;; TO: red bold
|
|
(set-face-attribute
|
|
'my-latex-TO-body-face nil
|
|
:foreground "tomato"
|
|
:background 'unspecified
|
|
:weight 'bold)
|
|
|
|
;; XX: student revision
|
|
(set-face-attribute
|
|
'my-latex-XX-body-face nil
|
|
:foreground "DeepPink"
|
|
:background 'unspecified
|
|
:weight 'bold))
|
|
|
|
|
|
(my-yatex-review-set-faces)
|
|
|
|
|
|
;; ============================================================
|
|
;; Variables
|
|
;; ============================================================
|
|
|
|
(defvar-local my-yatex-review-overlays nil
|
|
"Overlays used for review macro highlighting.")
|
|
|
|
(defvar-local my-yatex-review-highlight-enabled nil
|
|
"Non-nil means review macro highlighting is enabled.")
|
|
|
|
(defvar-local my-yatex-review-refresh-timer nil
|
|
"Idle timer for automatic review macro highlighting refresh.")
|
|
|
|
(defvar-local my-yatex-review-font-lock-removed-p nil
|
|
"Non-nil means old font-lock review highlighting has already been removed.")
|
|
|
|
(defvar my-yatex-review-idle-delay 0.8
|
|
"Idle delay seconds before automatic review macro highlighting refresh.")
|
|
|
|
|
|
;; ============================================================
|
|
;; Remove old font-lock highlighting
|
|
;; ============================================================
|
|
|
|
(defun my-yatex-review-remove-old-font-lock ()
|
|
"Remove old font-lock based review macro highlighting."
|
|
(interactive)
|
|
|
|
(when (boundp 'my-yatex-review-macro-keywords)
|
|
(font-lock-remove-keywords
|
|
nil
|
|
(symbol-value 'my-yatex-review-macro-keywords)))
|
|
|
|
(font-lock-remove-keywords
|
|
nil
|
|
'(("\\\\RA{\\([^{}\n]*\\)}{\\([^{}\n]*\\)}"
|
|
(1 'my-latex-RA-label-face t)
|
|
(2 'my-latex-RA-body-face t))
|
|
("\\\\RB{\\([^{}\n]*\\)}{\\([^{}\n]*\\)}"
|
|
(1 'my-latex-RB-label-face t)
|
|
(2 'my-latex-RB-body-face t))
|
|
("\\\\RC{\\([^{}\n]*\\)}{\\([^{}\n]*\\)}"
|
|
(1 'my-latex-RC-label-face t)
|
|
(2 'my-latex-RC-body-face t))
|
|
("\\\\RD{\\([^{}\n]*\\)}{\\([^{}\n]*\\)}"
|
|
(1 'my-latex-RD-label-face t)
|
|
(2 'my-latex-RD-body-face t))
|
|
("\\\\TO{\\([^{}\n]*\\)}"
|
|
(1 'my-latex-TO-body-face t))
|
|
("\\\\XX{\\([^{}\n]*\\)}"
|
|
(1 'my-latex-XX-body-face t))))
|
|
|
|
;; 重いので refresh のたびには呼ばない
|
|
(when font-lock-mode
|
|
(font-lock-flush (point-min) (point-max))
|
|
(font-lock-ensure (point-min) (point-max)))
|
|
|
|
(when (called-interactively-p 'interactive)
|
|
(message "Old font-lock review highlighting removed")))
|
|
|
|
|
|
;; ============================================================
|
|
;; Overlay utilities
|
|
;; ============================================================
|
|
|
|
(defun my-yatex-review--clear-overlays (&optional thorough)
|
|
"Clear review macro overlays.
|
|
|
|
If THOROUGH is non-nil, also remove untracked overlays with
|
|
property `my-yatex-review'."
|
|
(mapc #'delete-overlay my-yatex-review-overlays)
|
|
(setq my-yatex-review-overlays nil)
|
|
|
|
(when thorough
|
|
(remove-overlays (point-min) (point-max) 'my-yatex-review t)))
|
|
|
|
|
|
(defun my-yatex-review--make-overlay (beg end face &optional priority)
|
|
"Create overlay from BEG to END with FACE.
|
|
Optional PRIORITY specifies overlay priority."
|
|
(when (< beg end)
|
|
(let ((ov (make-overlay beg end)))
|
|
(overlay-put ov 'face face)
|
|
(overlay-put ov 'priority (or priority 1000))
|
|
(overlay-put ov 'my-yatex-review t)
|
|
(push ov my-yatex-review-overlays))))
|
|
|
|
|
|
;; ============================================================
|
|
;; Brace parser
|
|
;; ============================================================
|
|
|
|
(defun my-yatex-review--escaped-p (pos)
|
|
"Return non-nil if character at POS is escaped by backslashes."
|
|
(let ((n 0)
|
|
(p (1- pos)))
|
|
(while (and (>= p (point-min))
|
|
(eq (char-after p) ?\\))
|
|
(setq n (1+ n))
|
|
(setq p (1- p)))
|
|
(= (mod n 2) 1)))
|
|
|
|
|
|
(defun my-yatex-review--read-brace-arg ()
|
|
"Read one LaTeX brace argument at point.
|
|
|
|
Point may be before spaces/newlines followed by `{`.
|
|
Return (INNER-BEG INNER-END OUTER-END), or nil."
|
|
(skip-chars-forward " \t\r\n")
|
|
(if (not (eq (char-after) ?{))
|
|
nil
|
|
(let ((open (point))
|
|
(depth 0)
|
|
close)
|
|
(while (and (not close) (not (eobp)))
|
|
(let ((ch (char-after)))
|
|
(cond
|
|
((and (eq ch ?{)
|
|
(not (my-yatex-review--escaped-p (point))))
|
|
(setq depth (1+ depth)))
|
|
((and (eq ch ?})
|
|
(not (my-yatex-review--escaped-p (point))))
|
|
(setq depth (1- depth))
|
|
(when (= depth 0)
|
|
(setq close (point)))))
|
|
(forward-char 1)))
|
|
(when close
|
|
;; point is now just after closing brace
|
|
(list (1+ open) close (point))))))
|
|
|
|
|
|
;; ============================================================
|
|
;; Macro faces
|
|
;; ============================================================
|
|
|
|
(defun my-yatex-review--faces-for-macro (macro)
|
|
"Return label/body faces for \\RA, \\RB, \\RC, or \\RD."
|
|
(cond
|
|
((string= macro "RA")
|
|
'(my-latex-RA-label-face my-latex-RA-body-face))
|
|
((string= macro "RB")
|
|
'(my-latex-RB-label-face my-latex-RB-body-face))
|
|
((string= macro "RC")
|
|
'(my-latex-RC-label-face my-latex-RC-body-face))
|
|
((string= macro "RD")
|
|
'(my-latex-RD-label-face my-latex-RD-body-face))
|
|
(t nil)))
|
|
|
|
|
|
(defun my-yatex-review--faces-for-citem-macro (macro)
|
|
"Return label/body faces for \\CA, \\CB, \\CC, or \\CD."
|
|
(cond
|
|
((string= macro "CA")
|
|
'(my-latex-RA-label-face my-latex-RA-body-face))
|
|
((string= macro "CB")
|
|
'(my-latex-RB-label-face my-latex-RB-body-face))
|
|
((string= macro "CC")
|
|
'(my-latex-RC-label-face my-latex-RC-body-face))
|
|
((string= macro "CD")
|
|
'(my-latex-RD-label-face my-latex-RD-body-face))
|
|
(t nil)))
|
|
|
|
|
|
;; ============================================================
|
|
;; \CA-\CD item-body highlighting
|
|
;; ============================================================
|
|
|
|
(defvar my-yatex-review-citem-stop-regexp
|
|
"^[ \t]*\\(?:\\\\C[OABCD]\\_>\\|\\\\end[ \t\r\n]*{\\(?:enumerate\\|document\\)}\\)"
|
|
"Regexp that stops highlighting body text after \\CA, \\CB, \\CC, or \\CD.")
|
|
|
|
|
|
(defun my-yatex-review--highlight-citem-macros ()
|
|
"Highlight \\CA, \\CB, \\CC, and \\CD item bodies.
|
|
|
|
The body is highlighted until the next \\CO, \\CA, \\CB, \\CC,
|
|
\\CD, \\end{enumerate}, or \\end{document}.
|
|
|
|
Return number of highlighted answer macros."
|
|
(let ((count 0))
|
|
(goto-char (point-min))
|
|
|
|
;; 行頭またはインデント後の \CA, \CB, \CC, \CD を対象にする
|
|
(while (re-search-forward "\\\\\\(R[ABCD]\\|TO\\|XX\\)\\_>" nil t)
|
|
(let* ((macro (match-string-no-properties 1))
|
|
(faces (my-yatex-review--faces-for-citem-macro macro))
|
|
(label-face (nth 0 faces))
|
|
(body-face (nth 1 faces)))
|
|
(let ((arg1 (my-yatex-review--read-brace-arg)))
|
|
(when arg1
|
|
(let* ((body-beg (nth 2 arg1))
|
|
(body-end
|
|
(save-excursion
|
|
(goto-char body-beg)
|
|
(if (re-search-forward
|
|
my-yatex-review-citem-stop-regexp nil t)
|
|
(match-beginning 0)
|
|
(point-max)))))
|
|
(setq count (1+ count))
|
|
|
|
;; \CA{A1} の A1 部分
|
|
(my-yatex-review--make-overlay
|
|
(nth 0 arg1) (nth 1 arg1) label-face 1000)
|
|
|
|
;; A1 に続く本文部分
|
|
;; \RA などの局所 overlay より少し低い priority にする
|
|
(my-yatex-review--make-overlay
|
|
body-beg body-end body-face 900)
|
|
|
|
;; 次の item まで飛ぶ
|
|
(goto-char body-end))))))
|
|
|
|
count))
|
|
|
|
|
|
;; ============================================================
|
|
;; Main refresh
|
|
;; ============================================================
|
|
|
|
(defun my-yatex-review-highlight-refresh ()
|
|
"Refresh highlighting for \\RA-\\RD, \\TO, \\XX, and \\CA-\\CD."
|
|
(interactive)
|
|
|
|
;; refresh 時は font-lock 削除や face 再設定をしない。
|
|
;; それらは enable 時に一度だけ行う。
|
|
|
|
(my-yatex-review--clear-overlays)
|
|
|
|
(let ((count 0))
|
|
(save-excursion
|
|
(save-restriction
|
|
(widen)
|
|
|
|
;; ------------------------------------------------------------
|
|
;; \RA, \RB, \RC, \RD, \TO
|
|
;; ------------------------------------------------------------
|
|
(goto-char (point-min))
|
|
|
|
(while (re-search-forward "\\\\\\(R[ABCD]\\|TO\\|XX\\)\\_>" nil t)
|
|
(let ((macro (match-string-no-properties 1)))
|
|
(cond
|
|
|
|
;; \RA{label}{body}, \RB{label}{body}, ...
|
|
((member macro '("RA" "RB" "RC" "RD"))
|
|
(let* ((faces (my-yatex-review--faces-for-macro macro))
|
|
(label-face (nth 0 faces))
|
|
(body-face (nth 1 faces)))
|
|
(let ((arg1 (my-yatex-review--read-brace-arg)))
|
|
(when arg1
|
|
(let ((arg2 (my-yatex-review--read-brace-arg)))
|
|
(when arg2
|
|
(setq count (1+ count))
|
|
|
|
;; first argument
|
|
(my-yatex-review--make-overlay
|
|
(nth 0 arg1) (nth 1 arg1) label-face 1000)
|
|
|
|
;; second argument
|
|
(my-yatex-review--make-overlay
|
|
(nth 0 arg2) (nth 1 arg2) body-face 1000)))))))
|
|
;; \TO{body}
|
|
((string= macro "TO")
|
|
(let ((arg1 (my-yatex-review--read-brace-arg)))
|
|
(when arg1
|
|
(setq count (1+ count))
|
|
(my-yatex-review--make-overlay
|
|
(nth 0 arg1) (nth 1 arg1)
|
|
'my-latex-TO-body-face 1000))))
|
|
|
|
;; \XX{body}: student revision
|
|
((string= macro "XX")
|
|
(let ((arg1 (my-yatex-review--read-brace-arg)))
|
|
(when arg1
|
|
(setq count (1+ count))
|
|
(my-yatex-review--make-overlay
|
|
(nth 0 arg1) (nth 1 arg1)
|
|
'my-latex-XX-body-face 1000)))))))
|
|
|
|
;; ------------------------------------------------------------
|
|
;; \CA, \CB, \CC, \CD
|
|
;; ------------------------------------------------------------
|
|
(setq count
|
|
(+ count
|
|
(my-yatex-review--highlight-citem-macros)))))
|
|
|
|
(when (called-interactively-p 'interactive)
|
|
(message "Review macro highlight refreshed: %d macros/items found" count))))
|
|
|
|
|
|
;; ============================================================
|
|
;; Automatic refresh after editing
|
|
;; ============================================================
|
|
|
|
(defun my-yatex-review-highlight-refresh-silent ()
|
|
"Refresh highlighting silently."
|
|
(cl-letf (((symbol-function 'message)
|
|
(lambda (&rest _args) nil)))
|
|
(my-yatex-review-highlight-refresh)))
|
|
|
|
|
|
(defun my-yatex-review-schedule-refresh (&rest _args)
|
|
"Schedule automatic refresh after editing."
|
|
(when my-yatex-review-highlight-enabled
|
|
(when my-yatex-review-refresh-timer
|
|
(cancel-timer my-yatex-review-refresh-timer))
|
|
(setq my-yatex-review-refresh-timer
|
|
(run-with-idle-timer
|
|
my-yatex-review-idle-delay nil
|
|
(lambda (buf)
|
|
(when (buffer-live-p buf)
|
|
(with-current-buffer buf
|
|
(when my-yatex-review-highlight-enabled
|
|
(my-yatex-review-highlight-refresh-silent)))))
|
|
(current-buffer)))))
|
|
|
|
|
|
;; ============================================================
|
|
;; Enable / disable / toggle
|
|
;; ============================================================
|
|
|
|
(defun my-yatex-review-highlight-enable ()
|
|
"Enable review macro highlighting."
|
|
(interactive)
|
|
|
|
(my-yatex-review-install-keybindings)
|
|
(setq my-yatex-review-highlight-enabled t)
|
|
|
|
;; 古い font-lock 版の削除は一度だけ行う
|
|
(unless my-yatex-review-font-lock-removed-p
|
|
(my-yatex-review-remove-old-font-lock)
|
|
(setq my-yatex-review-font-lock-removed-p t))
|
|
|
|
;; face 設定も enable 時だけで十分
|
|
(my-yatex-review-set-faces)
|
|
|
|
(add-hook 'after-change-functions
|
|
#'my-yatex-review-schedule-refresh
|
|
nil t)
|
|
|
|
(my-yatex-review-highlight-refresh)
|
|
(message "Review macro highlight: ON"))
|
|
|
|
|
|
(defun my-yatex-review-highlight-disable ()
|
|
"Disable review macro highlighting."
|
|
(interactive)
|
|
|
|
(setq my-yatex-review-highlight-enabled nil)
|
|
|
|
(remove-hook 'after-change-functions
|
|
#'my-yatex-review-schedule-refresh
|
|
t)
|
|
|
|
(when my-yatex-review-refresh-timer
|
|
(cancel-timer my-yatex-review-refresh-timer)
|
|
(setq my-yatex-review-refresh-timer nil))
|
|
|
|
;; disable 時は念のため thorough に消す
|
|
(my-yatex-review--clear-overlays t)
|
|
|
|
;; 古い font-lock 版が残っていれば削除
|
|
(unless my-yatex-review-font-lock-removed-p
|
|
(my-yatex-review-remove-old-font-lock)
|
|
(setq my-yatex-review-font-lock-removed-p t))
|
|
|
|
(message "Review macro highlight: OFF"))
|
|
|
|
|
|
(defun my-yatex-review-highlight-toggle ()
|
|
"Toggle review macro highlighting."
|
|
(interactive)
|
|
(if (or my-yatex-review-highlight-enabled
|
|
my-yatex-review-overlays)
|
|
(my-yatex-review-highlight-disable)
|
|
(my-yatex-review-highlight-enable)))
|
|
|
|
|
|
;; ============================================================
|
|
;; Key bindings
|
|
;; ============================================================
|
|
|
|
(defun my-yatex-review-install-keybindings ()
|
|
"Install keybindings for review macro highlighting in current buffer."
|
|
(interactive)
|
|
|
|
;; C-c h : toggle ON/OFF
|
|
;; C-c r : refresh
|
|
(local-set-key (kbd "C-c h") #'my-yatex-review-highlight-toggle)
|
|
(local-set-key (kbd "C-c r") #'my-yatex-review-highlight-refresh)
|
|
|
|
(when (called-interactively-p 'interactive)
|
|
(message "Review macro keybindings installed: C-c h toggle, C-c r refresh")))
|
|
|
|
|
|
(defun my-yatex-review-setup ()
|
|
"Setup review macro highlighting for current YaTeX buffer."
|
|
(interactive)
|
|
(my-yatex-review-install-keybindings)
|
|
(my-yatex-review-highlight-enable))
|
|
|
|
|
|
;; ============================================================
|
|
;; Hooks
|
|
;; ============================================================
|
|
|
|
(add-hook 'yatex-mode-hook #'my-yatex-review-setup)
|
|
(add-hook 'YaTeX-mode-hook #'my-yatex-review-setup)
|
|
|
|
|
|
(provide 'my-yatex-review-highlight)
|
|
;;; my-yatex-review-highlight.el ends here
|