Subject: Re: bug#77299: eww-auto-rename-buffer 'title interaction with eww-readable-urls

From: Jim Porter <[email-masked]>
Date: Tue, 17 Jun 2025 09:33:01 -0700

On 6/12/2025 9:47 AM, Keith Amidon wrote:
> If you have the time and interest to write the patch based on your 
> superior knowledge of the code Jim that would be great. I'm motivated to 
> have this work because I find using eww-readable-urls to make eww much 
> more useful for my day-to-day usage (thanks for contributing it!) but I 
> also heavily use org links which need the document title to work well.

It took a bit longer than I thought to do this, but after a few failed 
attempts, I think this patch is pretty close (aside from the couple of 
small FIXMEs in there).

As a bonus, this implementation is about 4x faster than the old one, 
since it does all the work in a single pass over the DOM. For large 
pages, simply computing the readable DOM can take a while; doing it for 
the Wikipedia page for "Sun" takes a full second. Here are the stats 
from calling the relevant code before and after (sum of 20 iterations):

   BEFORE:  20.635833s (15.510534s in 270 GCs)
   AFTER:    4.320405s (3.041672s in 55 GCs)

We could probably make this even faster by doing something more 
efficient than '(length (split-string FOO))', but one thing at a time. 
That's how the code was before, so I didn't change that bit.

> My current personal workaround is working but I'd like to get a solution 
> upstream and stop carrying it. If you come up with something you'd like 
> help testing I'd be happy to help with that. If you can't get to it for 
> a while I'll make time to poke at it.

If the attached changed is too risky for the release branch, I'm open to 
the idea of doing something like your workaround there, maybe only when 
'eww-auto-rename-buffer' is set to 'title'. That way, people without 
that setting don't suffer any performance penalty.

From 586645d27ac3a1bc2b5882743f9ba3fcea4de3cb Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Tue, 17 Jun 2025 09:11:55 -0700
Subject: [PATCH] When making a readable page in EWW, include the <title> and
 similar tags

* lisp/net/eww.el (eww--walk-readability, eww-readable-dom): New
functions.
(eww-display-html): Call 'eww-readable-dom'.
(eww-readable): Call 'eww-readable-dom'.  Don't copy over 'eww-data'
properties that our new readable page can handle on its own.
(eww-score-readability): Rewrite in terms of 'eww--walk-readability'.
Make obsolete.
(eww-highest-readability): Make obsolete.
---
 lisp/net/eww.el | 95 +++++++++++++++++++++++++++++++++++--------------
 1 file changed, 69 insertions(+), 26 deletions(-)

diff --git a/lisp/net/eww.el b/lisp/net/eww.el
index 5462b3c78f4..458f0ddcc48 100644
--- a/lisp/net/eww.el
+++ b/lisp/net/eww.el
@@ -863,8 +863,7 @@ eww-display-html
   (unless document
     (let ((dom (eww--parse-html-region (point) (point-max) charset)))
       (when (eww-default-readable-p url)
-        (eww-score-readability dom)
-        (setq dom (eww-highest-readability dom))
+        (setq dom (eww-readable-dom dom))
         (with-current-buffer buffer
           (plist-put eww-data :readable t)))
       (setq document (eww-document-base url dom))))
@@ -1163,42 +1162,86 @@ eww-readable
                 (eww--parse-html-region (point-min) (point-max))))
          (base (plist-get eww-data :url)))
     (when make-readable
-      (eww-score-readability dom)
-      (setq dom (eww-highest-readability dom)))
+      (setq dom (eww-readable-dom dom)))
     (when eww-readable-adds-to-history
       (eww-save-history)
       (eww--before-browse)
-      (dolist (elem '(:source :url :title :next :previous :up :peer))
+      (dolist (elem '(:source :url :peer))
         (plist-put eww-data elem (plist-get old-data elem))))
     (eww-display-document (eww-document-base base dom))
     (plist-put eww-data :readable make-readable)
     (eww--after-page-change)))
 
-(defun eww-score-readability (node)
-  (let ((score -1))
-    (cond
-     ((memq (dom-tag node) '(script head comment))
-      (setq score -2))
-     ((eq (dom-tag node) 'meta)
-      (setq score -1))
-     ((eq (dom-tag node) 'img)
-      (setq score 2))
-     ((eq (dom-tag node) 'a)
-      (setq score (- (length (split-string (dom-text node))))))
-     (t
+(defun eww--walk-readability (node callback &optional noscore)
+  "Walk through all children of NODE to score readability.
+After scoring, call CALLBACK with the node and score.  If NOSCORE is
+non-nil, don't actually compute a score; just call the callback."
+  (let ((score nil))
+    (unless noscore
+      (cond
+       ((stringp node)
+        (setq score (length (split-string node))
+              noscore t))
+       ((memq (dom-tag node) '(script head comment))
+        (setq score -2
+              noscore t))
+       ((eq (dom-tag node) 'meta)
+        (setq score -1
+              noscore t))
+       ((eq (dom-tag node) 'img)
+        (setq score 2
+              noscore t))
+       ((eq (dom-tag node) 'a)
+        (setq score (- (length (split-string (dom-text node))))
+              noscore t))
+       (t
+        (setq score -1))))
+    (when (consp node)
       (dolist (elem (dom-children node))
-	(cond
-         ((stringp elem)
-          (setq score (+ score (length (split-string elem)))))
-         ((consp elem)
-	  (setq score (+ score
-			 (or (cdr (assoc :eww-readability-score (cdr elem)))
-			     (eww-score-readability elem)))))))))
-    ;; Cache the score of the node to avoid recomputing all the time.
-    (dom-set-attribute node :eww-readability-score score)
+        (let ((subscore (eww--walk-readability elem callback noscore)))
+          (when (and (not noscore) subscore)
+            (incf score subscore)))))
+    (funcall callback node score)
     score))
 
+(defun eww-readable-dom (dom)
+  "Return a readable version of DOM."
+  (let ((head-nodes nil)
+        (best-node nil)
+        (best-score most-negative-fixnum))
+    (eww--walk-readability
+     dom
+     (lambda (node score)
+       (when (consp node)
+         (when (and score (> score best-score)
+                    ;; We set a lower bound to how long we accept that
+                    ;; the readable portion of the page is going to be.
+                    (> (length (split-string (dom-texts node))) 100))
+           (setq best-score score
+                 best-node node))
+         ;; FIXME: We should probably filter out stylesheet <link> tags.
+         ;; Not that it really matters since we don't *do* anything with
+         ;; stylesheets...
+         (when (memq (dom-tag node) '(title link))
+           ;; FIXME: Uniquify the DOM so we don't include a node and its
+           ;; children separately?
+           (push node head-nodes)))))
+    (if best-node
+        `(html nil
+             (head nil ,@head-nodes)
+             (body nil ,best-node))
+      dom)))
+
+(defun eww-score-readability (node)
+  (declare (obsolete 'eww--walk-readability "31.1"))
+  (eww--walk-readability
+   node
+   (lambda (node score)
+     (when (and score (consp node))
+       (dom-set-attribute node :eww-readability-score score)))))
+
 (defun eww-highest-readability (node)
+  (declare (obsolete 'eww-readable-dom "31.1"))
   (let ((result node)
 	highest)
     (dolist (elem (dom-non-text-children node))
-- 
2.25.1