summaryrefslogtreecommitdiff
path: root/lisp/net/shr.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/net/shr.el')
-rw-r--r--lisp/net/shr.el320
1 files changed, 198 insertions, 122 deletions
diff --git a/lisp/net/shr.el b/lisp/net/shr.el
index 4e584e131fa..fbd1a9b7661 100644
--- a/lisp/net/shr.el
+++ b/lisp/net/shr.el
@@ -30,7 +30,7 @@
;;; Code:
-(eval-when-compile (require 'cl-lib))
+(require 'cl-lib)
(eval-when-compile (require 'url)) ;For url-filename's setf handler.
(require 'browse-url)
(eval-when-compile (require 'subr-x))
@@ -38,6 +38,8 @@
(require 'seq)
(require 'svg)
(require 'image)
+(require 'puny)
+(require 'text-property-search)
(defgroup shr nil
"Simple HTML Renderer"
@@ -51,46 +53,44 @@ width and height of the window. If they are larger than this,
and Emacs supports it, then the images will be rescaled down to
fit these criteria."
:version "24.1"
- :group 'shr
:type 'float)
(defcustom shr-blocked-images nil
"Images that have URLs matching this regexp will be blocked."
:version "24.1"
- :group 'shr
:type '(choice (const nil) regexp))
(defcustom shr-use-fonts t
"If non-nil, use proportional fonts for text."
:version "25.1"
- :group 'shr
+ :type 'boolean)
+
+(defcustom shr-discard-aria-hidden nil
+ "If non-nil, don't render tags with `aria-hidden=\"true\"'.
+This attribute is meant to tell screen readers to ignore a tag."
+ :version "27.1"
:type 'boolean)
(defcustom shr-use-colors t
"If non-nil, respect color specifications in the HTML."
:version "26.1"
- :group 'shr
:type 'boolean)
(defcustom shr-table-horizontal-line nil
"Character used to draw horizontal table lines.
If nil, don't draw horizontal table lines."
- :group 'shr
:type '(choice (const nil) character))
(defcustom shr-table-vertical-line ?\s
"Character used to draw vertical table lines."
- :group 'shr
:type 'character)
(defcustom shr-table-corner ?\s
"Character used to draw table corners."
- :group 'shr
:type 'character)
(defcustom shr-hr-line ?-
"Character used to draw hr lines."
- :group 'shr
:type 'character)
(defcustom shr-width nil
@@ -101,8 +101,7 @@ If `shr-use-fonts' is set, the mean character width is used to
compute the pixel width, which is used instead."
:version "25.1"
:type '(choice (integer :tag "Fixed width in characters")
- (const :tag "Use the width of the window" nil))
- :group 'shr)
+ (const :tag "Use the width of the window" nil)))
(defcustom shr-bullet "* "
"Bullet used for unordered lists.
@@ -110,19 +109,14 @@ Alternative suggestions are:
- \" \"
- \" \""
:version "24.4"
- :type 'string
- :group 'shr)
+ :type 'string)
-(defcustom shr-external-browser 'browse-url-default-browser
- "Function used to launch an external browser."
- :version "24.4"
- :group 'shr
- :type 'function)
+(define-obsolete-variable-alias 'shr-external-browser
+ 'browse-url-secondary-browser-function "27.1")
(defcustom shr-image-animate t
"Non nil means that images that can be animated will be."
:version "24.4"
- :group 'shr
:type 'boolean)
(defvar shr-content-function nil
@@ -133,14 +127,26 @@ cid: URL as the argument.")
(defvar shr-put-image-function 'shr-put-image
"Function called to put image and alt string.")
-(defface shr-strike-through '((t (:strike-through t)))
- "Font for <s> elements."
- :group 'shr)
+(defface shr-strike-through '((t :strike-through t))
+ "Face for <s> elements."
+ :version "24.1")
(defface shr-link
- '((t (:inherit link)))
- "Font for link elements."
- :group 'shr)
+ '((t :inherit link))
+ "Face for link elements."
+ :version "24.1")
+
+(defface shr-selected-link
+ '((t :inherit shr-link :background "red"))
+ "Temporary face for externally visited link elements.
+When a link is visited with an external browser, the link
+temporarily blinks with this face."
+ :version "27.1")
+
+(defface shr-abbreviation
+ '((t :inherit underline :underline (:style wave)))
+ "Face for <abbr> elements."
+ :version "27.1")
(defvar shr-inhibit-images nil
"If non-nil, inhibit loading images.")
@@ -267,7 +273,9 @@ DOM should be a parse tree as generated by
(if (and (null shr-width)
(not (shr--have-one-fringe-p)))
(* (frame-char-width) 2)
- 0)))))
+ 0)
+ 1))))
+ (max-specpdl-size max-specpdl-size)
bidi-display-reordering)
;; If the window was hscrolled for some reason, shr-fill-lines
;; below will misbehave, because it silently assumes that it
@@ -344,52 +352,45 @@ If the URL is already at the front of the kill ring act like
(shr-probe-and-copy-url url)
(shr-copy-url url)))
+(defun shr--current-link-region ()
+ (let ((current (get-text-property (point) 'shr-url))
+ start)
+ (save-excursion
+ ;; Go to the beginning.
+ (while (and (not (bobp))
+ (equal (get-text-property (point) 'shr-url) current))
+ (forward-char -1))
+ (unless (equal (get-text-property (point) 'shr-url) current)
+ (forward-char 1))
+ (setq start (point))
+ ;; Go to the end.
+ (while (and (not (eobp))
+ (equal (get-text-property (point) 'shr-url) current))
+ (forward-char 1))
+ (list start (point)))))
+
+(defun shr--blink-link ()
+ (let* ((region (shr--current-link-region))
+ (overlay (make-overlay (car region) (cadr region))))
+ (overlay-put overlay 'face 'shr-selected-link)
+ (run-at-time 1 nil (lambda ()
+ (delete-overlay overlay)))))
+
(defun shr-next-link ()
"Skip to the next link."
(interactive)
- (let ((current (get-text-property (point) 'shr-url))
- (start (point))
- skip)
- (while (and (not (eobp))
- (equal (get-text-property (point) 'shr-url) current))
- (forward-char 1))
- (cond
- ((and (not (eobp))
- (get-text-property (point) 'shr-url))
- ;; The next link is adjacent.
- (message "%s" (get-text-property (point) 'help-echo)))
- ((or (eobp)
- (not (setq skip (text-property-not-all (point) (point-max)
- 'shr-url nil))))
- (goto-char start)
- (message "No next link"))
- (t
- (goto-char skip)
- (message "%s" (get-text-property (point) 'help-echo))))))
+ (let ((match (text-property-search-forward 'shr-url nil nil t)))
+ (if (not match)
+ (message "No next link")
+ (goto-char (prop-match-beginning match))
+ (message "%s" (get-text-property (point) 'help-echo)))))
(defun shr-previous-link ()
"Skip to the previous link."
(interactive)
- (let ((start (point))
- (found nil))
- ;; Skip past the current link.
- (while (and (not (bobp))
- (get-text-property (point) 'help-echo))
- (forward-char -1))
- ;; Find the previous link.
- (while (and (not (bobp))
- (not (setq found (get-text-property (point) 'help-echo))))
- (forward-char -1))
- (if (not found)
- (progn
- (message "No previous link")
- (goto-char start))
- ;; Put point at the start of the link.
- (while (and (not (bobp))
- (get-text-property (point) 'help-echo))
- (forward-char -1))
- (forward-char 1)
- (message "%s" (get-text-property (point) 'help-echo)))))
+ (if (not (text-property-search-backward 'shr-url nil nil t))
+ (message "No previous link")
+ (message "%s" (get-text-property (point) 'help-echo))))
(defun shr-show-alt-text ()
"Show the ALT text of the image under point."
@@ -493,15 +494,20 @@ size, and full-buffer size."
(shr-depth (1+ shr-depth))
(start (point)))
;; shr uses many frames per nested node.
- (if (> shr-depth (/ max-specpdl-size 15))
- (setq shr-warning "Too deeply nested to render properly; consider increasing `max-specpdl-size'")
+ (if (and (> shr-depth (/ max-specpdl-size 15))
+ (not (and (y-or-n-p "Too deeply nested to render properly; increase `max-specpdl-size'?")
+ (setq max-specpdl-size (* max-specpdl-size 2)))))
+ (setq shr-warning
+ "Not rendering the complete page because of too-deep nesting")
(when style
(if (string-match "color\\|display\\|border-collapse" style)
(setq shr-stylesheet (nconc (shr-parse-style style)
shr-stylesheet))
(setq style nil)))
;; If we have a display:none, then just ignore this part of the DOM.
- (unless (equal (cdr (assq 'display shr-stylesheet)) "none")
+ (unless (or (equal (cdr (assq 'display shr-stylesheet)) "none")
+ (and shr-discard-aria-hidden
+ (equal (dom-attr dom 'aria-hidden) "true")))
;; We don't use shr-indirect-call here, since shr-descend is
;; the central bit of shr.el, and should be as fast as
;; possible. Having one more level of indirection with its
@@ -689,37 +695,49 @@ size, and full-buffer size."
`,(shr-face-background face))))
(setq start (point))
(setq shr-indentation (or continuation shr-indentation))
- (shr-vertical-motion shr-internal-width)
- (when (looking-at " $")
- (delete-region (point) (line-end-position)))
- (while (not (eolp))
- ;; We have to do some folding. First find the first
- ;; previous point suitable for folding.
- (if (or (not (shr-find-fill-point (line-beginning-position)))
- (= (point) start))
- ;; We had unbreakable text (for this width), so just go to
- ;; the first space and carry on.
- (progn
- (beginning-of-line)
- (skip-chars-forward " ")
- (search-forward " " (line-end-position) 'move)))
- ;; Success; continue.
- (when (= (preceding-char) ?\s)
- (delete-char -1))
- (let ((props `(face ,(get-text-property (point) 'face)
- ;; Don't break the image-displayer property
- ;; as it will cause `gnus-article-show-images'
- ;; to show the two or more same images.
- image-displayer
- ,(get-text-property (point) 'image-displayer)))
- (gap-start (point)))
- (insert "\n")
- (shr-indent)
- (add-text-properties gap-start (point) props))
- (setq start (point))
+ ;; If we have an indentation that's wider than the width we're
+ ;; trying to fill to, then just give up and don't do any filling.
+ (when (< shr-indentation shr-internal-width)
(shr-vertical-motion shr-internal-width)
(when (looking-at " $")
- (delete-region (point) (line-end-position))))))
+ (delete-region (point) (line-end-position)))
+ (while (not (eolp))
+ ;; We have to do some folding. First find the first
+ ;; previous point suitable for folding.
+ (if (or (not (shr-find-fill-point (line-beginning-position)))
+ (= (point) start))
+ ;; We had unbreakable text (for this width), so just go to
+ ;; the first space and carry on.
+ (progn
+ (beginning-of-line)
+ (skip-chars-forward " ")
+ (search-forward " " (line-end-position) 'move)))
+ ;; Success; continue.
+ (when (= (preceding-char) ?\s)
+ (delete-char -1))
+ (let ((gap-start (point)))
+ (insert "\n")
+ (shr-indent)
+ (when (and (> (1- gap-start) (point-min))
+ ;; The link on both sides of the newline are the
+ ;; same...
+ (equal (get-text-property (point) 'shr-url)
+ (get-text-property (1- gap-start) 'shr-url)))
+ ;; ... so we join the two bits into one link logically, but
+ ;; not visually. This makes navigation between links work
+ ;; well, but avoids underscores before the link on the next
+ ;; line when indented.
+ (let* ((props (copy-sequence (text-properties-at (point))))
+ (face (plist-get props 'face)))
+ ;; We don't want to use the faces on the indentation, because
+ ;; that's ugly, but we do want to use the background colour.
+ (when face
+ (setq props (plist-put props 'face (shr-face-background face))))
+ (add-text-properties gap-start (point) props))))
+ (setq start (point))
+ (shr-vertical-motion shr-internal-width)
+ (when (looking-at " $")
+ (delete-region (point) (line-end-position)))))))
(defun shr-find-fill-point (start)
(let ((bp (point))
@@ -936,7 +954,7 @@ size, and full-buffer size."
(defun shr-browse-url (&optional external mouse-event)
"Browse the URL at point using `browse-url'.
If EXTERNAL is non-nil (interactively, the prefix argument), browse
-the URL using `shr-external-browser'.
+the URL using `browse-url-secondary-browser-function'.
If this function is invoked by a mouse click, it will browse the URL
at the position of the click. Optional argument MOUSE-EVENT describes
the mouse click event."
@@ -950,7 +968,9 @@ the mouse click event."
(browse-url-mail url))
(t
(if external
- (funcall shr-external-browser url)
+ (progn
+ (funcall browse-url-secondary-browser-function url)
+ (shr--blink-link))
(browse-url url))))))
(defun shr-save-contents (directory)
@@ -1064,6 +1084,16 @@ element is the data blob and the second element is the content-type."
image)
(insert (or alt ""))))
+(defun shr--image-type ()
+ "Emacs image type to use when displaying images.
+If Emacs has native image scaling support, that's used, but if
+not, `imagemagick' is preferred if it's present."
+ (if (or (and (fboundp 'image-transforms-p)
+ (image-transforms-p))
+ (not (fboundp 'imagemagick-types)))
+ nil
+ 'imagemagick))
+
(defun shr-rescale-image (data content-type width height
&optional max-width max-height)
"Rescale DATA, if too big, to fit the current buffer.
@@ -1072,8 +1102,7 @@ WIDTH and HEIGHT are the sizes given in the HTML data, if any.
The size of the displayed image will not exceed
MAX-WIDTH/MAX-HEIGHT. If not given, use the current window
width/height instead."
- (if (or (not (fboundp 'imagemagick-types))
- (not (get-buffer-window (current-buffer))))
+ (if (not (get-buffer-window (current-buffer)))
(create-image data nil t :ascent 100)
(let* ((edges (window-inside-pixel-edges
(get-buffer-window (current-buffer))))
@@ -1094,13 +1123,13 @@ width/height instead."
(< (* width scaling) max-width)
(< (* height scaling) max-height))
(create-image
- data 'imagemagick t
+ data (shr--image-type) t
:ascent 100
:width width
:height height
:format content-type)
(create-image
- data 'imagemagick t
+ data (shr--image-type) t
:ascent 100
:max-width max-width
:max-height max-height
@@ -1178,12 +1207,26 @@ START, and END. Note that START and END should be markers."
(add-text-properties
start (point)
(list 'shr-url url
- 'help-echo (let ((iri (or (ignore-errors
- (decode-coding-string
- (url-unhex-string url)
- 'utf-8 t))
- url)))
- (if title (format "%s (%s)" iri title) iri))
+ 'button t
+ 'category 'shr ; For button.el button buffers.
+ 'help-echo (let ((parsed (url-generic-parse-url
+ (or (ignore-errors
+ (decode-coding-string
+ (url-unhex-string url)
+ 'utf-8 t))
+ url)))
+ iri)
+ ;; If we have an IDNA domain, then show the
+ ;; decoded version in the mouseover to let the
+ ;; user know that there's something possibly
+ ;; fishy.
+ (when (url-host parsed)
+ (setf (url-host parsed)
+ (puny-encode-domain (url-host parsed))))
+ (setq iri (url-recreate-url parsed))
+ (if title
+ (format "%s (%s)" iri title)
+ iri))
'follow-link t
'mouse-face 'highlight))
;; Don't overwrite any keymaps that are already in the buffer (i.e.,
@@ -1319,19 +1362,19 @@ ones, in case fg and bg are nil."
(shr-generic dom)
(put-text-property start (point) 'display '(raise -0.5))))
-(defun shr-tag-label (dom)
- (shr-generic dom)
- (shr-ensure-paragraph))
-
(defun shr-tag-p (dom)
(shr-ensure-paragraph)
(shr-generic dom)
(shr-ensure-paragraph))
(defun shr-tag-div (dom)
- (shr-ensure-newline)
- (shr-generic dom)
- (shr-ensure-newline))
+ (let ((display (cdr (assq 'display shr-stylesheet))))
+ (if (or (equal display "inline")
+ (equal display "inline-block"))
+ (shr-generic dom)
+ (shr-ensure-newline)
+ (shr-generic dom)
+ (shr-ensure-newline))))
(defun shr-tag-s (dom)
(shr-fontize-dom dom 'shr-strike-through))
@@ -1351,10 +1394,14 @@ ones, in case fg and bg are nil."
(defun shr-tag-u (dom)
(shr-fontize-dom dom 'underline))
-(defun shr-tag-tt (dom)
+(defun shr-tag-code (dom)
(let ((shr-current-font 'default))
(shr-generic dom)))
+(defun shr-tag-tt (dom)
+ ;; The `tt' tag is deprecated in favor of `code'.
+ (shr-tag-code dom))
+
(defun shr-tag-ins (cont)
(let* ((start (point))
(color "green")
@@ -1416,6 +1463,21 @@ ones, in case fg and bg are nil."
(when url
(shr-urlify (or shr-start start) (shr-expand-url url) title))))
+(defun shr-tag-abbr (dom)
+ (when-let* ((title (dom-attr dom 'title))
+ (start (point)))
+ (shr-generic dom)
+ (shr-add-font start (point) 'shr-abbreviation)
+ (add-text-properties
+ start (point)
+ (list
+ 'help-echo title
+ 'mouse-face 'highlight))))
+
+(defun shr-tag-acronym (dom)
+ ;; `acronym' is deprecated in favor of `abbr'.
+ (shr-tag-abbr dom))
+
(defun shr-tag-object (dom)
(unless shr-inhibit-images
(let ((start (point))
@@ -1455,7 +1517,6 @@ The key element should be a regexp matched against the type of the source or
url if no type is specified. The value should be a float in the range 0.0 to
1.0. Media elements with higher value are preferred."
:version "24.4"
- :group 'shr
:type '(alist :key-type regexp :value-type float))
(defun shr--get-media-pref (elem)
@@ -1528,6 +1589,10 @@ The preference is a float determined from `shr-prefer-media-type'."
(when (zerop (length alt))
(setq alt "*"))
(cond
+ ((null url)
+ ;; After further expansion, there turned out to be no valid
+ ;; src in the img after all.
+ )
((or (member (dom-attr dom 'height) '("0" "1"))
(member (dom-attr dom 'width) '("0" "1")))
;; Ignore zero-sized or single-pixel images.
@@ -1662,7 +1727,7 @@ The preference is a float determined from `shr-prefer-media-type'."
(svg-gradient svg "background" 'linear '((0 . "#b0b0b0") (100 . "#808080")))
(svg-rectangle svg 0 0 width height :gradient "background"
:stroke-width 2 :stroke-color "black")
- (let ((image (svg-image svg)))
+ (let ((image (svg-image svg :scale 1)))
(setf (image-property image :ascent) 100)
image)))
@@ -1710,7 +1775,14 @@ The preference is a float determined from `shr-prefer-media-type'."
(defun shr-tag-ol (dom)
(shr-ensure-paragraph)
- (let ((shr-list-mode 1))
+ (let* ((attrs (dom-attributes dom))
+ (start-attr (alist-get 'start attrs))
+ ;; Start at 1 if there is no start attribute
+ ;; or if start can't be parsed as an integer.
+ (start-index (condition-case _
+ (cl-parse-integer start-attr)
+ (t 1)))
+ (shr-list-mode start-index))
(shr-generic dom))
(shr-ensure-paragraph))
@@ -1738,7 +1810,10 @@ The preference is a float determined from `shr-prefer-media-type'."
(defun shr-mark-fill (start)
;; We may not have inserted any text to fill.
- (unless (= start (point))
+ (when (and (/= start (point))
+ ;; Tables insert themselves with the correct indentation,
+ ;; so don't do anything if we're at the start of a table.
+ (not (get-text-property start 'shr-table-id)))
(put-text-property start (1+ start)
'shr-indentation shr-indentation)))
@@ -2035,7 +2110,8 @@ flags that control whether to collect or render objects."
(setq max (max max (nth 2 column))))
max)))
(dotimes (_ (max height 1))
- (shr-indent)
+ (when (bolp)
+ (shr-indent))
(insert shr-table-vertical-line "\n"))
(dolist (column row)
(when (> (nth 2 column) -1)