From ed860b2b065777d11ecfcd53e5d67e78b02fd3f2 Mon Sep 17 00:00:00 2001 From: Cash Prokop-Weaver Date: Fri, 30 May 2025 06:58:20 -0700 Subject: [PATCH] feat(eww): Add jump-to-heading in EWW 1. Add jump-to-heading functionality for EWW buffers 2. Bind within imenu (replaces +eww/jump-to-url-on-page; bound to `:localleader l`) The new functions are based on existing `eww--capture-url-on-page` and `+eww/jump-to-url-on-page`. However, I took a different approach and used alists to hide the position/coordinates from the `completing-read`. Also, unlike `+eww/jump-to-url-on-page`, we don't give the user an option of limiting the scope to a region or visible portion of the buffer. `+eww/jump-to-heading-on-page` always prompts based on the entire buffer. Examples: 1. `

H1

` - "H1" 2. `

H1

H2

` - "H1" - "H1/h2" 3. `

H1

H2

H3

` - "H1" - "H1/H2" - "H1/H2/H3" 4. `

H1-1

H2

H1-2

` - "H1-1" - "H1-1/H2" - "H1-2" ![screenshot on the Emacs Wikipedia entry](https://github.com/user-attachments/assets/c2210f0f-c026-4325-9b1b-c2427ec13cd5) Gaps in the hierarchy (for example a `

` followed by an `

`) are not represented in the labels presented to the user. Take the Wikipedia entry for Emacs (above) as an example. The `

` "Content" is the first heading on the page, there's no preceeding `

`, so it's shown to the user as "Content" without any prefix. Examples: 1. `

H2

` - "H2" 2. `

H2

H4

` - "H2" - "H2/H4" 3. `

H2

H4

H5
` - "H2" - "H2/H4" - "H2/H4/H5" 4. `

H2

H1

H5
` - "H2" - "H1" - "H1/H5" - modules/emacs/eww/autoload.el - (eww--capture-url-on-page): Rename to `eww--capture-urls-on-page` - (eww--capture-headings-on-page): Add; based on existing `eww--capture-urls-on-page` - (+eww/jump-to-heading-on-page): Add; based on existing `+eww/jump-to-url-on-page` - modules/emacs/eww/config.el - (keybind) Bind `+eww/jump-to-heading-on-page` to `<:localleader.>`; based on existing org-mode jump-to-heading keybind (`consult-org-heading`) --- modules/emacs/eww/autoload.el | 66 +++++++++++++++++++++++++++++++---- modules/emacs/eww/config.el | 3 +- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/modules/emacs/eww/autoload.el b/modules/emacs/eww/autoload.el index b83c551a0..7a7d00c48 100644 --- a/modules/emacs/eww/autoload.el +++ b/modules/emacs/eww/autoload.el @@ -4,17 +4,15 @@ ;; dotfiles. See https://protesilaos.com/codelog/2021-03-25-emacs-eww ;; Adapted from `prot-eww-jump-to-url-on-page' -(defun eww--capture-url-on-page (&optional position) +(defun eww--capture-urls-on-page (&optional position) "Capture all the links on the current web page. -Return a list of strings. Strings are in the form LABEL @ URL. +Return a list of strings. Strings are in the form \"LABEL @ URL\". When optional argument POSITION is non-nil, include position info in the strings -too, so strings take the form: LABEL @ URL ~ POSITION." +too, so strings take the form: \"POSITION ~ LABEL @ URL\"." (let (links match) (save-excursion (goto-char (point-max)) - ;; NOTE 2021-07-25: The first clause in the `or' is meant to address a bug - ;; where if a URL is in `point-min' it does not get captured. (while (setq match (text-property-search-backward 'shr-url)) (let* ((raw-url (prop-match-value match)) (start-point-prop (prop-match-beginning match)) @@ -38,6 +36,48 @@ too, so strings take the form: LABEL @ URL ~ POSITION." links))))) links)) +(defun eww--capture-headings-on-page () + "Return an alist in the form \"LABEL . POINT\" for the current buffer." + (let ((heading-stack '()) + headings match) + (save-excursion + (goto-char (point-min)) + (while (setq match (text-property-search-forward 'outline-level)) + (let* ((level (prop-match-value match)) + (start-point-prop (prop-match-beginning match)) + (end-point-prop (prop-match-end match)) + (text (replace-regexp-in-string + "\n" " " ; NOTE 2021-07-25: newlines break completion + (buffer-substring-no-properties + start-point-prop end-point-prop)))) + (cond + ((= level (length heading-stack)) + (pop heading-stack) + (push text heading-stack)) + ((< level (length heading-stack)) + ;; There's an upward gap between headings (for example: h5, then h2) + (dotimes (_ (1+ (- (length heading-stack) level))) + (pop heading-stack)) + (push text heading-stack)) + ((> level (length heading-stack)) + ;; There's a downward gap between headings (for example: h2, then h5) + (dotimes (_ (1- (- level (length heading-stack)))) + (push nil heading-stack)) + (push text heading-stack))) + (push (cons + (concat + (let ((preceeding-heading-stack (remove nil (cdr heading-stack)))) + (when preceeding-heading-stack + (propertize + (concat + (string-join (reverse preceeding-heading-stack) "/") + "/") + 'face 'shadow))) + (car heading-stack)) + start-point-prop) + headings)))) + headings)) + ;; Adapted from `prot-eww--rename-buffer' (defun +eww-page-title-or-url (&rest _) (let ((prop (if (string-empty-p (plist-get eww-data :title)) :url :title))) @@ -75,12 +115,12 @@ consider whole buffer." (user-error "Not in an eww buffer!")) (let* ((links (if arg - (eww--capture-url-on-page t) + (eww--capture-urls-on-page t) (save-restriction (if (use-region-p) (narrow-to-region (region-beginning) (region-end)) (narrow-to-region (window-start) (window-end))) - (eww--capture-url-on-page t)))) + (eww--capture-urls-on-page t)))) (prompt-scope (if arg (propertize "URL on the page" 'face 'warning) "visible URL")) @@ -91,6 +131,18 @@ consider whole buffer." (goto-char point) (recenter))) +;; Adapted from `prot-eww-jump-to-url-on-page' +;;;###autoload +(defun +eww/jump-to-heading-on-page () + "Jump to heading position on the page (whole buffer) using completion." + (interactive nil 'eww-mode) + (unless (derived-mode-p 'eww-mode) + (user-error "Not in an eww buffer!")) + (let* ((headings (eww--capture-headings-on-page)) + (selection (completing-read "Jump to heading: " headings nil t))) + (goto-char (alist-get selection headings nil nil #'string=)) + (recenter))) + ;; Adapted from `prot-eww-open-in-other-window' ;;;###autoload (defun +eww/open-in-other-window () diff --git a/modules/emacs/eww/config.el b/modules/emacs/eww/config.el index 37c168035..ba60d3ced 100644 --- a/modules/emacs/eww/config.el +++ b/modules/emacs/eww/config.el @@ -6,7 +6,7 @@ (map! :map eww-mode-map [remap text-scale-increase] #'+eww/increase-font-size [remap text-scale-decrease] #'+eww/decrease-font-size - [remap imenu] #'+eww/jump-to-url-on-page + [remap imenu] #'+eww/jump-to-heading-on-page [remap quit-window] #'+eww/quit :ni [C-return] #'+eww/open-in-other-window :n "yy" #'+eww/copy-current-url @@ -16,6 +16,7 @@ (:localleader :desc "external browser" "e" #'eww-browse-with-external-browser :desc "buffers" "b" #'eww-switch-to-buffer + :desc "jump to link" "l" #'+eww/jump-to-url-on-page (:prefix ("t" . "toggle") :desc "readable" "r" #'eww-readable