From 504e0080a3f94c9824ac305b1a24b30c7d1fb051 Mon Sep 17 00:00:00 2001 From: HallowDem <75336799+Cytrogen@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:54:31 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8D=9A=E5=AE=A2?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=8F=92=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加图片链接导出转换,支持 [[./image.png]] 和 [[file:image.png]] 格式 - 新增 my/blog-insert-image 函数,支持本地文件和网络 URL - 网络图片自动下载保存到文章资源目录 --- config/pkg-blog.el | 164 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 2 deletions(-) diff --git a/config/pkg-blog.el b/config/pkg-blog.el index fce793c1f66abc3fa5c232bd3e6521ede55861e1..d1e9f1f7058857e8b2536da0a001a5ec607ad11e 100644 --- a/config/pkg-blog.el +++ b/config/pkg-blog.el @@ -38,6 +38,60 @@ :type 'directory :group 'my/blog) +;;; Dynamic Directory Setup + +(defvar my/blog-dirs-config-file (concat user-emacs-directory ".blog-dirs") + "File to store the user's Blog directories.") + +(defun my/setup-blog-directories () + "Load Blog directories from config file or prompt user." + (let ((source-dir nil) + (export-dir nil) + (config-changed nil)) + + ;; 1. Try to read from file + (when (file-exists-p my/blog-dirs-config-file) + (with-temp-buffer + (insert-file-contents my/blog-dirs-config-file) + (goto-char (point-min)) + (while (not (eobp)) + (let ((line-start (point))) + (end-of-line) + (let ((line (buffer-substring-no-properties line-start (point)))) + (cond + ((string-match "^SOURCE=\\(.*\\)" line) + (setq source-dir (string-trim (match-string 1 line)))) + ((string-match "^EXPORT=\\(.*\\)" line) + (setq export-dir (string-trim (match-string 1 line)))))) + (forward-line 1))))) + + ;; 2. Validate Source Dir + (unless (and source-dir (file-directory-p source-dir)) + (setq source-dir (read-directory-name "请选择博客 Org 源码目录 (Select Blog Source Dir): " (bound-and-true-p org-directory))) + (unless (file-directory-p source-dir) + (make-directory source-dir t)) + (setq config-changed t)) + + ;; 3. Validate Export Dir + (unless (and export-dir (file-directory-p export-dir)) + (setq export-dir (read-directory-name "请选择博客发布目录 (Select Blog Export Dir, e.g. source/_posts): " "D:/")) + (unless (file-directory-p export-dir) + (make-directory export-dir t)) + (setq config-changed t)) + + ;; 4. Save if changed + (when config-changed + (with-temp-file my/blog-dirs-config-file + (insert (format "SOURCE=%s\nEXPORT=%s\n" source-dir export-dir)))) + + ;; 5. Apply settings + (setq my/blog-org-dir (file-name-as-directory source-dir)) + (setq my/blog-export-dir (file-name-as-directory export-dir)) + (message "Blog directories loaded.\nSource: %s\nExport: %s" my/blog-org-dir my/blog-export-dir))) + +;; Execute setup immediately +(my/setup-blog-directories) + (defcustom my/blog-monthly-sections '(("商业与社会" . "商业与社会") ("心理与关系" . "心理与关系") @@ -512,6 +566,15 @@ Returns list of (level title properties content subsections)." (goto-char (point-min)) (while (re-search-forward "_\\([^_\n]+\\)_" nil t) (replace-match "\\1")) + ;; Convert image links BEFORE other link conversions + ;; [[file:path/image.png][alt]] or [[./path/image.png][alt]] → ![alt](image.png) + (goto-char (point-min)) + (while (re-search-forward "\\[\\[\\(?:file:\\|\\.?/\\)?\\([^]]*\\.\\(png\\|jpg\\|jpeg\\|gif\\|webp\\|svg\\)\\)\\]\\[\\([^]]+\\)\\]\\]" nil t) + (replace-match "![\\3](\\1)")) + ;; [[file:image.png]] or [[./image.png]] → ![](image.png) + (goto-char (point-min)) + (while (re-search-forward "\\[\\[\\(?:file:\\|\\.?/\\)?\\([^]]*\\.\\(png\\|jpg\\|jpeg\\|gif\\|webp\\|svg\\)\\)\\]\\]" nil t) + (replace-match "![](\\1)")) ;; Convert links: [[url][text]] -> [text](url) (goto-char (point-min)) (while (re-search-forward "\\[\\[\\([^]]+\\)\\]\\[\\([^]]+\\)\\]\\]" nil t) @@ -550,6 +613,32 @@ Returns list of (level title properties content subsections)." (unless (member tag '("src" "quote")) (replace-match (format "{%% end%s %%}" tag))))) + ;; Remove any remaining PROPERTIES drawers + (goto-char (point-min)) + (while (re-search-forward "^[ \t]*:PROPERTIES:[ \t]*\n\\(?:.*\n\\)*?[ \t]*:END:[ \t]*\n?" nil t) + (replace-match "")) + + ;; Step 2.5: Clean up spaces around CJK characters and style markers + ;; Remove space between CJK char and opening style marker + (goto-char (point-min)) + (while (re-search-forward "\\(\\cC\\) +\\(\\*\\*\\|`\\|\\)" nil t) + (replace-match "\\1\\2")) + ;; Remove space between closing style marker and CJK char + (goto-char (point-min)) + (while (re-search-forward "\\(\\*\\*\\|`\\|\\) +\\(\\cC\\)" nil t) + (replace-match "\\1\\2")) + ;; Handle single * (italic) separately to avoid conflict with ** + (goto-char (point-min)) + (while (re-search-forward "\\(\\cC\\) +\\(\\*[^*]\\)" nil t) + (replace-match "\\1\\2")) + (goto-char (point-min)) + (while (re-search-forward "\\([^*]\\*\\) +\\(\\cC\\)" nil t) + (replace-match "\\1\\2")) + ;; Remove space before CJK punctuation + (goto-char (point-min)) + (while (re-search-forward "\\(\\cC\\|\\*\\*\\|`\\|\\) +\\(\\cP\\)" nil t) + (replace-match "\\1\\2")) + ;; Step 3: Restore code blocks (dolist (block code-blocks) (goto-char (point-min)) @@ -814,13 +903,23 @@ If FILE is nil, use current buffer's file." (message "Exported to: %s" export-file))))) (defun my/blog--get-post-body () - "Get the body content of the org file (excluding properties header)." + "Get the body content of the org file (excluding properties header and drawers)." (save-excursion (goto-char (point-min)) - ;; Skip past all #+KEYWORD lines + ;; Skip past all #+KEYWORD lines and empty lines at beginning (while (and (not (eobp)) (looking-at "^\\(#\\+\\|$\\)")) (forward-line 1)) + ;; Skip any file-level drawers (PROPERTIES, LOGBOOK, etc.) + (while (and (not (eobp)) + (looking-at "^[ \t]*:\\([A-Z]+\\):[ \t]*$")) + (if (re-search-forward "^[ \t]*:END:" nil t) + (forward-line 1) + (forward-line 1))) + ;; Skip any blank lines after drawers + (while (and (not (eobp)) + (looking-at "^[ \t]*$")) + (forward-line 1)) ;; Get everything from here to end (buffer-substring-no-properties (point) (point-max)))) @@ -889,6 +988,67 @@ Creates a new org file in the blog posts directory with front matter." (find-file filepath) (message "已创建博客文章: %s\n用 C-c b p 导出为 Markdown" filepath))) +;;; Image Insertion + +(defun my/blog--url-p (string) + "Return non-nil if STRING looks like a URL." + (string-match-p "^https?://" string)) + +(defun my/blog--download-image (url target-file) + "Download image from URL to TARGET-FILE." + (require 'url) + (let ((url-request-method "GET")) + (with-current-buffer (url-retrieve-synchronously url t) + ;; Skip HTTP headers + (goto-char (point-min)) + (re-search-forward "\r?\n\r?\n" nil t) + ;; Write binary content to file + (let ((coding-system-for-write 'binary)) + (write-region (point) (point-max) target-file)) + (kill-buffer)))) + +(defun my/blog-insert-image () + "Insert an image link for blog post. +Supports both local files and web URLs. +For local files: select and copy to post's asset folder. +For web URLs: download and save with a specified filename." + (interactive) + (unless (buffer-file-name) + (error "Buffer must be visiting a file")) + (let* ((post-name (file-name-base (buffer-file-name))) + (target-dir (expand-file-name post-name my/blog-export-dir)) + ;; Get source: can be file path or URL + (source (read-string "Image file or URL: ")) + (is-url (my/blog--url-p source)) + ;; For URLs without extension, ask for filename; for local files, use original name + (image-name (if is-url + (let ((default-name (file-name-nondirectory (url-filename (url-generic-parse-url source))))) + (read-string "Save as (e.g. photo.jpg): " + (if (string-match-p "\\." default-name) default-name ""))) + (file-name-nondirectory source))) + (target-file (expand-file-name image-name target-dir)) + (alt-text (read-string "Alt text (optional): "))) + ;; Validate filename for URLs + (when (and is-url (string-empty-p image-name)) + (error "Filename is required for web images")) + ;; Create target directory if needed + (unless (file-directory-p target-dir) + (make-directory target-dir t) + (message "Created directory: %s" target-dir)) + ;; Download or copy image + (unless (file-exists-p target-file) + (if is-url + (progn + (message "Downloading %s..." source) + (my/blog--download-image source target-file) + (message "Downloaded to %s" target-file)) + (copy-file source target-file) + (message "Copied %s to %s" image-name target-dir))) + ;; Insert org link + (if (string-empty-p alt-text) + (insert (format "[[./%s]]" image-name)) + (insert (format "[[./%s][%s]]" image-name alt-text))))) + ;;; Provide (provide 'pkg-blog)