From 40b26f90a0a33e58fafcaa386388cc51a0ab64cc Mon Sep 17 00:00:00 2001 From: Cytrogen Date: Wed, 11 Mar 2026 19:18:05 -0400 Subject: [PATCH] =?UTF-8?q?feat(org):=20=E6=B7=BB=E5=8A=A0=E5=8A=A8?= =?UTF-8?q?=E6=80=81=20Capture/Refile=20=E7=B3=BB=E7=BB=9F=E5=92=8C?= =?UTF-8?q?=E9=98=85=E8=AF=BB=E5=88=97=E8=A1=A8=20Agenda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 动态结构系统: 由 org-structure.org 数据驱动生成 Capture 模板 和 Refile 目标,支持 datetree、自定义模板、文件覆盖 - 一级标题定义分类(FILE + KEY 属性) - 二级标题定义子项(HEADLINE + TEMPLATE + KEY) 快速 Refile: C-c w 两步交互式 Refile(先选分类再选目标) 阅读列表: 自定义 Agenda 视图(r 前缀),支持想读/在读/已读 三种状态,Agenda 操作 S 开始阅读、F 完成阅读 其他: CJK 内联标记支持、源代码块原生 TAB、RET 直接跟踪链接 + Babel shell/elisp 支持 + 数学公式 pretty entities --- config/pkg-org.el | 163 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 2 deletions(-) diff --git a/config/pkg-org.el b/config/pkg-org.el index 68a4ba597c8a065d37cc34b2a17e07b28627a80b..eaf5ec6cce216440efdfd10995ac488185679f4e 100644 --- a/config/pkg-org.el +++ b/config/pkg-org.el @@ -1,6 +1,6 @@ ;;; pkg-org.el --- Org-mode configuration and dynamic system -*- lexical-binding: t -*- -;; Copyright (C) 2024 Cytrogen +;; Copyright (C) 2026 Cytrogen ;; This file contains: ;; - Org directory setup @@ -232,12 +232,56 @@ Format: ((key label file ((subkey sublabel headline template override-file) ...) (define-key org-agenda-mode-map (kbd "S") #'my/reading-start-book) (define-key org-agenda-mode-map (kbd "F") #'my/reading-finish-book)) + ;; --- 课程笔记 Capture --- + (defvar my/class-notes-date nil "临时存储日期,供模板使用") + + (defun my/get-class-subjects () + "获取 reading.org 中 课程笔记 下的所有科目。" + (let ((reading-file (expand-file-name "reading.org" org-directory)) + (subjects '())) + (when (file-exists-p reading-file) + (with-temp-buffer + (insert-file-contents reading-file) + (goto-char (point-min)) + (when (re-search-forward "^\\* 课程笔记" nil t) + (let ((end (save-excursion + (or (re-search-forward "^\\* " nil t) (point-max))))) + (while (re-search-forward "^\\*\\* \\(.+\\)" end t) + (push (match-string-no-properties 1) subjects)))))) + (nreverse subjects))) + + (defun my/class-notes-goto-subject () + "提示选择科目和日期,定位到科目标题下。" + (let* ((subjects (my/get-class-subjects)) + (subject (completing-read "科目: " subjects nil nil)) + (date (read-string "日期: " (format-time-string "%Y-%m-%d")))) + (setq my/class-notes-date date) + (goto-char (point-min)) + (if (re-search-forward "^\\* 课程笔记" nil t) + (let ((subtree-end (save-excursion (org-end-of-subtree t) (point)))) + (if (re-search-forward (format "^\\*\\* %s$" (regexp-quote subject)) subtree-end t) + (beginning-of-line) + ;; 科目不存在,在子树末尾创建 + (goto-char subtree-end) + (insert "\n** " subject) + (beginning-of-line))) + ;; 课程笔记也不存在 + (goto-char (point-max)) + (insert "\n* 课程笔记\n** " subject) + (beginning-of-line)))) + ;; --- Refile Configuration --- ;; 强化 Refile 功能,允许将条目移动到项目或分类的具体标题下 (setq org-refile-use-outline-path 'file) (setq org-outline-path-complete-in-steps nil) (setq org-refile-allow-creating-parent-nodes 'confirm) + ;; --- 数学公式显示配置 --- + ;; 启用 pretty entities 显示上标/下标 + (setq org-pretty-entities t) + ;; 可选:只在需要时显示(光标不在实体上时) + (setq org-pretty-entities-include-sub-superscripts t) + ;; --- Source Block Editing Configuration --- ;; 让 TAB 在源代码块中按语言原生方式工作 (setq org-src-tab-acts-natively t) @@ -248,7 +292,21 @@ Format: ((key label file ((subkey sublabel headline template override-file) ...) ;; 在当前窗口编辑源代码块(避免分割窗口) (setq org-src-window-setup 'current-window) ;; 禁用返回编辑 buffer 时的确认提示 - (setq org-src-ask-before-returning-to-edit-buffer nil)) + (setq org-src-ask-before-returning-to-edit-buffer nil) + + ;; --- Link Configuration --- + ;; 按 RET 时直接打开链接 + (setq org-return-follows-link t) + + ;; --- Org-Babel Configuration --- + ;; 启用语言支持,实现类似 Jupyter Notebook 的体验 + (org-babel-do-load-languages + 'org-babel-load-languages + '((shell . t) ; 用于编译和运行命令 + (emacs-lisp . t))) ; 默认支持 + + ;; 执行代码块时不询问确认(方便快速执行) + (setq org-confirm-babel-evaluate nil)) ;; Custom Org Functions ;; Helper functions for enhanced workflow @@ -308,6 +366,17 @@ Format: ((key label file ((subkey sublabel headline template override-file) ...) (push (list combined-key label type capture-target template) templates))))) (setq org-capture-templates (reverse templates)) + ;; 替换课程笔记模板为使用 file+function + (setq org-capture-templates + (mapcar (lambda (tpl) + (if (equal (car tpl) "Rc") + `("Rc" "课程笔记" entry + (file+function ,(expand-file-name "reading.org" org-directory) + my/class-notes-goto-subject) + "*** %(symbol-value 'my/class-notes-date)\n%?" + :empty-lines 1) + tpl)) + org-capture-templates)) (message "Org capture templates updated."))) (defun my/quick-refile () @@ -383,5 +452,95 @@ Step 2: Choose Sub Category (Context) using completing-read. (with-eval-after-load 'org (my/apply-org-structure)) +;; 中文行内标记支持 +;; 允许中文字符紧贴 org 强调标记符(*粗体*、/斜体/ 等),无需额外空格 +(with-eval-after-load 'org + (setcar org-emphasis-regexp-components + "-[:space:]('\"{\x200B[:nonascii:]") + (setcar (nthcdr 1 org-emphasis-regexp-components) + "-[:space:].,:!?;'\")}\\[\x200B[:nonascii:]") + (org-set-emph-re 'org-emphasis-regexp-components org-emphasis-regexp-components) + ;; 使用 _{xxx}_ 语法时,防止下划线被解释为下标 + (setq org-use-sub-superscripts '{}) + (setq org-export-with-sub-superscripts '{})) + +;;; ---- 下划线语法迁移 ---- + +(defun my/migrate-underline-syntax (&optional dry-run) + "Migrate _xxx_ to _{xxx}_ in all org files under `org-directory'. +With prefix argument (C-u), perform a DRY-RUN showing changes without modifying files." + (interactive "P") + (let ((files (directory-files-recursively org-directory "\\.org$")) + (total-replacements 0) + (modified-files '()) + ;; Regex matching Org emphasis _xxx_ pattern + ;; Matches _ + non-space/non-underscore + optional middle + non-space/non-underscore + _ + ;; but NOT already in _{xxx}_ form + (underline-re "_\\([^ _\n]\\(?:[^_\n]*[^ _\n]\\)?\\)_")) + (dolist (file files) + (with-temp-buffer + (insert-file-contents file) + (let ((file-replacements 0) + (regions-to-skip '())) + ;; Collect regions to skip: src blocks, example blocks, property drawers + (goto-char (point-min)) + (while (re-search-forward + "^[ \t]*#\\+BEGIN_\\(SRC\\|EXAMPLE\\)\\b.*\n" nil t) + (let ((beg (match-beginning 0))) + (when (re-search-forward + (format "^[ \t]*#\\+END_%s" (match-string 1)) nil t) + (push (cons beg (line-end-position)) regions-to-skip)))) + (goto-char (point-min)) + (while (re-search-forward "^[ \t]*:PROPERTIES:" nil t) + (let ((beg (match-beginning 0))) + (when (re-search-forward "^[ \t]*:END:" nil t) + (push (cons beg (line-end-position)) regions-to-skip)))) + ;; Collect inline code =...= and ~...~ + (goto-char (point-min)) + (while (re-search-forward "\\(?:=\\([^=\n]+\\)=\\)\\|\\(?:~\\([^~\n]+\\)~\\)" nil t) + (push (cons (match-beginning 0) (match-end 0)) regions-to-skip)) + ;; Collect links [[...]] + (goto-char (point-min)) + (while (re-search-forward "\\[\\[\\([^]]*\\)\\]\\(?:\\[\\([^]]*\\)\\]\\)?\\]" nil t) + (push (cons (match-beginning 0) (match-end 0)) regions-to-skip)) + ;; Collect #+KEYWORD: lines + (goto-char (point-min)) + (while (re-search-forward "^[ \t]*#\\+[A-Z_]+:.*$" nil t) + (push (cons (match-beginning 0) (match-end 0)) regions-to-skip)) + ;; Now do replacements, skipping protected regions + (goto-char (point-min)) + (while (re-search-forward underline-re nil t) + (let ((pos (match-beginning 0)) + (skip nil)) + ;; Check if inside a skip region + (dolist (region regions-to-skip) + (when (and (>= pos (car region)) (<= pos (cdr region))) + (setq skip t))) + ;; Skip if already in _{...}_ form (preceded by _ and { ) + (when (and (not skip) + (> pos 0) + (eq (char-before (match-beginning 0)) ?{)) + (setq skip t)) + (unless skip + (cl-incf file-replacements) + (unless dry-run + (replace-match "_{\\1}_"))))) + (when (> file-replacements 0) + (cl-incf total-replacements file-replacements) + (push (cons file file-replacements) modified-files) + (unless dry-run + (write-region (point-min) (point-max) file)))))) + ;; Report results + (if (= total-replacements 0) + (message "No _xxx_ patterns found to migrate.") + (with-current-buffer (get-buffer-create "*underline-migration*") + (erase-buffer) + (insert (format "%s — %d replacements in %d files:\n\n" + (if dry-run "DRY RUN" "MIGRATED") + total-replacements (length modified-files))) + (dolist (entry (nreverse modified-files)) + (insert (format " %s: %d\n" (car entry) (cdr entry)))) + (display-buffer (current-buffer)))))) + (provide 'pkg-org) ;;; pkg-org.el ends here \ No newline at end of file