让 Gemini 3 为 nov.el 实现了注释预览功能

原先在 nov-mode 中点击正文里的注释链接,会跳到注释,然后点击注释的链接又会跳回原文,这样反复跳跃很影响阅读体验。我自己能写简单的 elisp 但对 epub 和 nov.el 的具体原理不了解,于是用 Gemini 3 试了一下。

下面代码会让 nov-mode 直接在 echo area 中预览注释的内容,而目录页的链接直接跳转不会预览。另外让 Gemini 写了一个函数作为预览行为的开关(有时在非目录页也需要跳转而非预览)。大概交互了五次之后就彻底实现,目前测试了五本电子书,未发现问题,非常满意。(不过后来用gemini 试了一些更复杂的 elisp 任务又不行了orz)

(with-eval-after-load 'nov
  ;; 1. DEFINITION: The Toggle Variable
  (defvar nov-enable-link-peeking t
    "When non-nil, preview internal links in the minibuffer instead of jumping.
This is automatically disabled when viewing the Table of Contents.")

  ;; 2. COMMAND: Toggle Function
  (defun nov-toggle-link-peeking ()
    "Toggle the peeking behavior for internal links."
    (interactive)
    (setq nov-enable-link-peeking (not nov-enable-link-peeking))
    (message "Link peeking %s" (if nov-enable-link-peeking "enabled" "disabled")))

  ;; 3. HELPER: The Smart Content Fetcher
  (defun nov-content-at-target (filename target)
    "Retrieve text content from FILENAME at anchor TARGET.
Smartly handles duplicates by ignoring self-referencing links (source anchors)
while accepting back-links (target anchors)."
    (let* ((current-path (cdr (aref nov-documents nov-documents-index)))
           (directory (file-name-directory current-path))
           (full-path (if (or (null filename) (string= filename ""))
                          current-path
                        (if (file-name-absolute-p filename)
                            filename
                          (file-truename (nov-make-path directory filename))))))
      (when (file-exists-p full-path)
        (with-temp-buffer
          (insert-file-contents full-path)
          (let* ((dom (libxml-parse-html-region (point-min) (point-max)))
                 ;; A "real" target has the ID but is NOT a link to that same ID
                 (is-real-target-p
                  (lambda (node)
                    (and (equal (dom-attr node 'id) target)
                         (let ((href (dom-attr node 'href)))
                           (or (null href)
                               (not (string-suffix-p (concat "#" target) href)))))))
                 (node (seq-find is-real-target-p (dom-search dom is-real-target-p))))

            (when node
              ;; If the node is inline, find its block-level parent
              (when (memq (dom-tag node) '(a span sup sub em strong i b font small))
                (setq node
                      (or (seq-find (lambda (block)
                                      (dom-search block is-real-target-p))
                                    (append (dom-by-tag dom 'li)
                                            (dom-by-tag dom 'aside)
                                            (dom-by-tag dom 'p)
                                            (dom-by-tag dom 'div)
                                            (dom-by-tag dom 'dd)))
                          node)))
              (replace-regexp-in-string "[ \t\n]+" " " (dom-texts node " "))))))))

  ;; 4. OVERRIDE: The Logic to Check TOC and Toggle State
  (defun nov-browse-url (&optional mouse-event)
    "Follow external links, or preview internal footnotes if enabled and valid."
    (interactive (list last-nonmenu-event))
    (mouse-set-point mouse-event)
    (let ((url (get-text-property (point) 'shr-url)))
      (when (not url)
        (user-error "No link under point"))

      ;; LOGIC CHECK:
      ;; 1. Is peeking enabled?
      ;; 2. Are we currently OUTSIDE the Table of Contents?
      ;; 3. Is it an internal link?
      (let* ((curr-doc (aref nov-documents nov-documents-index))
             (curr-id (car curr-doc))
             (is-toc (eq curr-id nov-toc-id)) ;; nov.el specific check
             (should-peek (and nov-enable-link-peeking
                               (not is-toc)
                               (not (nov-external-url-p url)))))

        (if (not should-peek)
            ;; STANDARD BEHAVIOR (Jump)
            (if (nov-external-url-p url)
                (browse-url url)
              (apply 'nov-visit-relative-file (nov-url-filename-and-target url)))

          ;; PEEK BEHAVIOR
          (seq-let (filename target) (nov-url-filename-and-target url)
            (let ((preview-text (and target (nov-content-at-target filename target))))
              (if preview-text
                  (message "%s" preview-text)
                ;; Fallback to jump if peek fails
                (apply 'nov-visit-relative-file (list filename target))))))))))

4 个赞

效果图如下:

1 个赞

特别好

我认为可以实现一个eldoc-documentation-functions 这样用eldoc-mouse 就可以支持弹出预览当mouse hover.

对你的code稍加调整,就让eldoc-mouse支持了nov-mode

1 个赞

谢谢大佬,这个很酷 :smiley: 我个人没有这个需求,因为我一般是在手机上看书,不用鼠标。但是或许你可以把代码贴上来,让有需要的人参考一下?

跟你的代码几乎一样,我只是调整了一下使其适配eldoc-documentation-functions, 再配合eldoc-mouse, 就有mouse hover 的效果,现在还不完美,主要是我不了解nov-mode, 等调好了,我会把它集成到eldoc-mouse, 需要的朋友就方便使用。

另外,我不是大佬,我也是最近不得已(为实现eldoc-mouse)才学了点elisp.

1 个赞

org-mode下我也碰到了类似的问题,大型文档的脚注查看跳来跳去很不方便,改成inline的话脚注多了又影响阅读正文。。请问你这个是否可以改成支持org-mode?

可以,需要单独实现一个eldoc-documentation-function, 你去eldoc-mouse 的github 页面看看吧,主要是如何扩展部分,GitHub - huangfeiyu/eldoc-mouse: Show document for mouse hover utilizing eldoc and posframe

你可以自己做一个扩展,如果行的话

我觉得org mode的逻辑应该更简单,因为非匿名的脚注会在文本中出现两次:写一个函数,直接isearch 或者跳转到该脚注,抓取该行的内容,message 到 echo area,再让光标返回正文的位置即可,应该会用到 save-excursion。nov.el 难的地方在于不同的电子书 html 的脚注形式不是统一的。

最近有空,再研究了一下,除了点小问题,基本可以了

有没有样例org文件,我有时间的时候研究一下。这个看来不少人有这个需求。