;;; shr.el --- Simple HTML Renderer -*- lexical-binding: t -*-
;; Copyright (C) 2010-2017 Free Software Foundation, Inc.
;; Author: Lars Magne Ingebrigtsen
;; Keywords: html
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see .
;;; Commentary:
;; This package takes a HTML parse tree (as provided by
;; libxml-parse-html-region) and renders it in the current buffer. It
;; does not do CSS, JavaScript or anything advanced: It's geared
;; towards rendering typical short snippets of HTML, like what you'd
;; find in HTML email and the like.
;;; Code:
(eval-when-compile (require 'cl))
(eval-when-compile (require 'url)) ;For url-filename's setf handler.
(require 'browse-url)
(require 'subr-x)
(require 'dom)
(require 'seq)
(require 'svg)
(require 'image)
(defgroup shr nil
"Simple HTML Renderer"
:version "25.1"
:group 'web)
(defcustom shr-max-image-proportion 0.9
"How big pictures displayed are in relation to the window they're in.
A value of 0.7 means that they are allowed to take up 70% of the
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-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
"Frame width to use for rendering.
May either be an integer specifying a fixed width in characters,
or nil, meaning that the full width of the window should be
used."
:version "25.1"
:type '(choice (integer :tag "Fixed width in characters")
(const :tag "Use the width of the window" nil))
:group 'shr)
(defcustom shr-bullet "* "
"Bullet used for unordered lists.
Alternative suggestions are:
- \" \"
- \" \""
:version "24.4"
:type 'string
:group 'shr)
(defcustom shr-external-browser 'browse-url-default-browser
"Function used to launch an external browser."
:version "24.4"
:group 'shr
:type 'function)
(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
"If bound, this should be a function that will return the content.
This is used for cid: URLs, and the function is called with the
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 elements."
:group 'shr)
(defface shr-link
'((t (:inherit link)))
"Font for link elements."
:group 'shr)
(defvar shr-inhibit-images nil
"If non-nil, inhibit loading images.")
(defvar shr-external-rendering-functions nil
"Alist of tag/function pairs used to alter how shr renders certain tags.
For instance, eww uses this to alter rendering of title, forms
and other things:
((title . eww-tag-title)
(form . eww-tag-form)
...)")
;;; Internal variables.
(defvar shr-folding-mode nil)
(defvar shr-start nil)
(defvar shr-indentation 0)
(defvar shr-internal-width nil)
(defvar shr-list-mode nil)
(defvar shr-content-cache nil)
(defvar shr-kinsoku-shorten nil)
(defvar shr-table-depth 0)
(defvar shr-stylesheet nil)
(defvar shr-base nil)
(defvar shr-depth 0)
(defvar shr-warning nil)
(defvar shr-ignore-cache nil)
(defvar shr-target-id nil)
(defvar shr-table-separator-length 1)
(defvar shr-table-separator-pixel-width 0)
(defvar shr-table-id nil)
(defvar shr-current-font nil)
(defvar shr-internal-bullet nil)
(defvar shr-map
(let ((map (make-sparse-keymap)))
(define-key map "a" 'shr-show-alt-text)
(define-key map "i" 'shr-browse-image)
(define-key map "z" 'shr-zoom-image)
(define-key map [?\t] 'shr-next-link)
(define-key map [?\M-\t] 'shr-previous-link)
(define-key map [follow-link] 'mouse-face)
(define-key map [mouse-2] 'shr-browse-url)
(define-key map "I" 'shr-insert-image)
(define-key map "w" 'shr-copy-url)
(define-key map "u" 'shr-copy-url)
(define-key map "v" 'shr-browse-url)
(define-key map "O" 'shr-save-contents)
(define-key map "\r" 'shr-browse-url)
map))
(defvar shr-image-map
(let ((map (copy-keymap shr-map)))
(when (boundp 'image-map)
(set-keymap-parent map image-map))
map))
;; Public functions and commands.
(declare-function libxml-parse-html-region "xml.c"
(start end &optional base-url discard-comments))
(defun shr-render-buffer (buffer)
"Display the HTML rendering of the current buffer."
(interactive (list (current-buffer)))
(or (fboundp 'libxml-parse-html-region)
(error "This function requires Emacs to be compiled with libxml2"))
(pop-to-buffer "*html*")
(erase-buffer)
(shr-insert-document
(with-current-buffer buffer
(libxml-parse-html-region (point-min) (point-max))))
(goto-char (point-min)))
;;;###autoload
(defun shr-render-region (begin end &optional buffer)
"Display the HTML rendering of the region between BEGIN and END."
(interactive "r")
(unless (fboundp 'libxml-parse-html-region)
(error "This function requires Emacs to be compiled with libxml2"))
(with-current-buffer (or buffer (current-buffer))
(let ((dom (libxml-parse-html-region begin end)))
(delete-region begin end)
(goto-char begin)
(shr-insert-document dom))))
(defun shr--have-one-fringe-p ()
"Return non-nil if we know at least one of the fringes has non-zero width."
(and (fboundp 'fringe-columns)
(or (not (zerop (fringe-columns 'right)))
(not (zerop (fringe-columns 'left))))))
;;;###autoload
(defun shr-insert-document (dom)
"Render the parsed document DOM into the current buffer.
DOM should be a parse tree as generated by
`libxml-parse-html-region' or similar."
(setq shr-content-cache nil)
(let ((start (point))
(shr-start nil)
(shr-base nil)
(shr-depth 0)
(shr-table-id 0)
(shr-warning nil)
(shr-table-separator-pixel-width (shr-string-pixel-width "-"))
(shr-internal-bullet (cons shr-bullet
(shr-string-pixel-width shr-bullet)))
(shr-internal-width (or (and shr-width
(if (not shr-use-fonts)
shr-width
(* shr-width (frame-char-width))))
;; We need to adjust the available
;; width for when the user disables
;; the fringes, which will cause the
;; display engine usurp one column for
;; the continuation glyph.
(if (not shr-use-fonts)
(- (window-body-width) 1
(if (and (null shr-width)
(not (shr--have-one-fringe-p)))
0
1))
(- (window-body-width nil t)
(* 2 (frame-char-width))
(if (and (null shr-width)
(not (shr--have-one-fringe-p)))
(* (frame-char-width) 2)
0)))))
bidi-display-reordering)
;; If the window was hscrolled for some reason, shr-fill-lines
;; below will misbehave, because it silently assumes that it
;; starts with a non-hscrolled window (vertical-motion will move
;; to a wrong place otherwise).
(set-window-hscroll nil 0)
(shr-descend dom)
(shr-fill-lines start (point))
(shr--remove-blank-lines-at-the-end start (point))
(when shr-warning
(message "%s" shr-warning))))
(defun shr--remove-blank-lines-at-the-end (start end)
(save-restriction
(save-excursion
(narrow-to-region start end)
(goto-char end)
(when (and (re-search-backward "[^ \n]" nil t)
(not (eobp)))
(forward-line 1)
(delete-region (point) (point-max))))))
(defun shr-copy-url (&optional image-url)
"Copy the URL under point to the kill ring.
If IMAGE-URL (the prefix) is non-nil, or there is no link under
point, but there is an image under point then copy the URL of the
image under point instead.
If called twice, then try to fetch the URL and see whether it
redirects somewhere else."
(interactive "P")
(let ((url (if image-url
(get-text-property (point) 'image-url)
(or (get-text-property (point) 'shr-url)
(get-text-property (point) 'image-url)))))
(cond
((not url)
(message "No URL under point"))
;; Resolve redirected URLs.
((equal url (car kill-ring))
(url-retrieve
url
(lambda (a)
(when (and (consp a)
(eq (car a) :redirect))
(with-temp-buffer
(insert (cadr a))
(goto-char (point-min))
;; Remove common tracking junk from the URL.
(when (re-search-forward ".utm_.*" nil t)
(replace-match "" t t))
(message "Copied %s" (buffer-string))
(copy-region-as-kill (point-min) (point-max)))))
nil t))
;; Copy the URL to the kill ring.
(t
(with-temp-buffer
(insert (url-encode-url url))
(copy-region-as-kill (point-min) (point-max))
(message "Copied %s" (buffer-string)))))))
(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))))))
(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)))))
(defun shr-show-alt-text ()
"Show the ALT text of the image under point."
(interactive)
(let ((text (get-text-property (point) 'shr-alt)))
(if (not text)
(message "No image under point")
(message "%s" (shr-fill-text text)))))
(defun shr-browse-image (&optional copy-url)
"Browse the image under point.
If COPY-URL (the prefix if called interactively) is non-nil, copy
the URL of the image to the kill buffer instead."
(interactive "P")
(let ((url (get-text-property (point) 'image-url)))
(cond
((not url)
(message "No image under point"))
(copy-url
(with-temp-buffer
(insert url)
(copy-region-as-kill (point-min) (point-max))
(message "Copied %s" url)))
(t
(message "Browsing %s..." url)
(browse-url url)))))
(defun shr-insert-image ()
"Insert the image under point into the buffer."
(interactive)
(let ((url (get-text-property (point) 'image-url)))
(if (not url)
(message "No image under point")
(message "Inserting %s..." url)
(url-retrieve url 'shr-image-fetched
(list (current-buffer) (1- (point)) (point-marker))
t t))))
(defun shr-zoom-image ()
"Toggle the image size.
The size will be rotated between the default size, the original
size, and full-buffer size."
(interactive)
(let ((url (get-text-property (point) 'image-url))
(size (get-text-property (point) 'image-size))
(buffer-read-only nil))
(if (not url)
(message "No image under point")
;; Delete the old picture.
(while (get-text-property (point) 'image-url)
(forward-char -1))
(forward-char 1)
(let ((start (point)))
(while (get-text-property (point) 'image-url)
(forward-char 1))
(forward-char -1)
(put-text-property start (point) 'display nil)
(when (> (- (point) start) 2)
(delete-region start (1- (point)))))
(message "Inserting %s..." url)
(url-retrieve url 'shr-image-fetched
(list (current-buffer) (1- (point)) (point-marker)
(list (cons 'size
(cond ((or (eq size 'default)
(null size))
'original)
((eq size 'original)
'full)
((eq size 'full)
'default)))))
t))))
;;; Utility functions.
(defsubst shr-generic (dom)
(dolist (sub (dom-children dom))
(if (stringp sub)
(shr-insert sub)
(shr-descend sub))))
(defun shr-descend (dom)
(let ((function
(intern (concat "shr-tag-" (symbol-name (dom-tag dom))) obarray))
;; Allow other packages to override (or provide) rendering
;; of elements.
(external (cdr (assq (dom-tag dom) shr-external-rendering-functions)))
(style (dom-attr dom 'style))
(shr-stylesheet shr-stylesheet)
(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'")
(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")
(cond (external
(funcall external dom))
((fboundp function)
(funcall function dom))
(t
(shr-generic dom)))
(when (and shr-target-id
(equal (dom-attr dom 'id) shr-target-id))
;; If the element was empty, we don't have anything to put the
;; anchor on. So just insert a dummy character.
(when (= start (point))
(insert "*"))
(put-text-property start (1+ start) 'shr-target-id shr-target-id))
;; If style is set, then this node has set the color.
(when style
(shr-colorize-region
start (point)
(cdr (assq 'color shr-stylesheet))
(cdr (assq 'background-color shr-stylesheet))))))))
(defun shr-fill-text (text)
(if (zerop (length text))
text
(with-temp-buffer
(let ((shr-indentation 0)
(shr-start nil)
(shr-internal-width (- (window-body-width nil t)
(* 2 (frame-char-width))
;; Adjust the window width for when
;; the user disables the fringes,
;; which causes the display engine
;; to usurp one column for the
;; continuation glyph.
(if (and (null shr-width)
(not (shr--have-one-fringe-p)))
(* (frame-char-width) 2)
0))))
(shr-insert text)
(buffer-string)))))
(define-inline shr-char-breakable-p (char)
"Return non-nil if a line can be broken before and after CHAR."
(inline-quote (aref fill-find-break-point-function-table ,char)))
(define-inline shr-char-nospace-p (char)
"Return non-nil if no space is required before and after CHAR."
(inline-quote (aref fill-nospace-between-words-table ,char)))
;; KINSOKU is a Japanese word meaning a rule that should not be violated.
;; In Emacs, it is a term used for characters, e.g. punctuation marks,
;; parentheses, and so on, that should not be placed in the beginning
;; of a line or the end of a line.
(define-inline shr-char-kinsoku-bol-p (char)
"Return non-nil if a line ought not to begin with CHAR."
(inline-letevals (char)
(inline-quote (and (not (eq ,char ?'))
(aref (char-category-set ,char) ?>)))))
(define-inline shr-char-kinsoku-eol-p (char)
"Return non-nil if a line ought not to end with CHAR."
(inline-quote (aref (char-category-set ,char) ?<)))
(unless (shr-char-kinsoku-bol-p (make-char 'japanese-jisx0208 33 35))
(load "kinsoku" nil t))
(defun shr-pixel-column ()
(if (not shr-use-fonts)
(current-column)
(if (not (get-buffer-window (current-buffer)))
(save-window-excursion
;; Avoid errors if the selected window is a dedicated one,
;; and they just want to insert a document into it.
(set-window-dedicated-p nil nil)
(set-window-buffer nil (current-buffer))
(car (window-text-pixel-size nil (line-beginning-position) (point))))
(car (window-text-pixel-size nil (line-beginning-position) (point))))))
(defun shr-pixel-region ()
(- (shr-pixel-column)
(save-excursion
(goto-char (mark))
(shr-pixel-column))))
(defun shr-string-pixel-width (string)
(if (not shr-use-fonts)
(length string)
(with-temp-buffer
(insert string)
(shr-pixel-column))))
(defsubst shr--translate-insertion-chars ()
;; Remove soft hyphens.
(goto-char (point-min))
(while (search-forward "" nil t)
(replace-match "" t t))
;; Translate non-breaking spaces into real spaces.
(goto-char (point-min))
(while (search-forward " " nil t)
(replace-match " " t t)))
(defun shr-insert (text)
(when (and (not (bolp))
(get-text-property (1- (point)) 'image-url))
(insert "\n"))
(cond
((eq shr-folding-mode 'none)
(let ((start (point)))
(insert text)
(save-restriction
(narrow-to-region start (point))
(shr--translate-insertion-chars)
(goto-char (point-max)))))
(t
(let ((font-start (point)))
(when (and (string-match "\\`[ \t\n\r]" text)
(not (bolp))
(not (eq (char-after (1- (point))) ? )))
(insert " "))
(let ((start (point))
(bolp (bolp)))
(insert text)
(save-restriction
(narrow-to-region start (point))
(goto-char start)
(when (looking-at "[ \t\n\r]+")
(replace-match "" t t))
(while (re-search-forward "[ \t\n\r]+" nil t)
(replace-match " " t t))
(shr--translate-insertion-chars)
(goto-char (point-max)))
;; We may have removed everything we inserted if if was just
;; spaces.
(unless (= font-start (point))
;; Mark all lines that should possibly be folded afterwards.
(when bolp
(shr-mark-fill start))
(when shr-use-fonts
(put-text-property font-start (point)
'face
(or shr-current-font 'variable-pitch)))))))))
(defun shr-fill-lines (start end)
(if (<= shr-internal-width 0)
nil
(save-restriction
(narrow-to-region start end)
(goto-char start)
(when (get-text-property (point) 'shr-indentation)
(shr-fill-line))
(while (setq start (next-single-property-change start 'shr-indentation))
(goto-char start)
(when (bolp)
(shr-fill-line)))
(goto-char (point-max)))))
(defun shr-vertical-motion (column)
(if (not shr-use-fonts)
(move-to-column column)
(unless (eolp)
(forward-char 1))
(vertical-motion (cons (/ column (frame-char-width)) 0))
(unless (eolp)
(forward-char 1))))
(defun shr-fill-line ()
(let ((shr-indentation (get-text-property (point) 'shr-indentation))
(continuation (get-text-property
(point) 'shr-continuation-indentation))
start)
(put-text-property (point) (1+ (point)) 'shr-indentation nil)
(let ((face (get-text-property (point) 'face))
(background-start (point)))
(shr-indent)
(when face
(put-text-property background-start (point) 'face
`,(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 (text-properties-at (point)))
(gap-start (point)))
(insert "\n")
(shr-indent)
(when props
(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))
(end (point))
failed)
(while (not (or (setq failed (<= (point) start))
(eq (preceding-char) ? )
(eq (following-char) ? )
(shr-char-breakable-p (preceding-char))
(shr-char-breakable-p (following-char))
(and (shr-char-kinsoku-bol-p (preceding-char))
(shr-char-breakable-p (following-char))
(not (shr-char-kinsoku-bol-p (following-char))))
(shr-char-kinsoku-eol-p (following-char))
(bolp)))
(backward-char 1))
(if failed
;; There's no breakable point, so we give it up.
(let (found)
(goto-char bp)
;; Don't overflow the window edge, even if
;; shr-kinsoku-shorten is nil.
(unless (or shr-kinsoku-shorten (null shr-width))
(while (setq found (re-search-forward
"\\(\\c>\\)\\| \\|\\c<\\|\\c|"
(line-end-position) 'move)))
(if (and found
(not (match-beginning 1)))
(goto-char (match-beginning 0)))))
(or
(eolp)
;; Don't put kinsoku-bol characters at the beginning of a line,
;; or kinsoku-eol characters at the end of a line.
(cond
;; Don't overflow the window edge, even if shr-kinsoku-shorten
;; is nil.
((or shr-kinsoku-shorten (null shr-width))
(while (and (not (memq (preceding-char) (list ?\C-@ ?\n ? )))
(or (shr-char-kinsoku-eol-p (preceding-char))
(shr-char-kinsoku-bol-p (following-char))))
(backward-char 1))
(when (setq failed (<= (point) start))
;; There's no breakable point that doesn't violate kinsoku,
;; so we look for the second best position.
(while (and (progn
(forward-char 1)
(<= (point) end))
(progn
(setq bp (point))
(shr-char-kinsoku-eol-p (following-char)))))
(goto-char bp)))
((shr-char-kinsoku-eol-p (preceding-char))
;; Find backward the point where kinsoku-eol characters begin.
(let ((count 4))
(while
(progn
(backward-char 1)
(and (> (setq count (1- count)) 0)
(not (memq (preceding-char) (list ?\C-@ ?\n ? )))
(or (shr-char-kinsoku-eol-p (preceding-char))
(shr-char-kinsoku-bol-p (following-char)))))))
(when (setq failed (<= (point) start))
;; There's no breakable point that doesn't violate kinsoku,
;; so we go to the second best position.
(if (looking-at "\\(\\c<+\\)\\c<")
(goto-char (match-end 1))
(forward-char 1))))
((shr-char-kinsoku-bol-p (following-char))
;; Find forward the point where kinsoku-bol characters end.
(let ((count 4))
(while (progn
(forward-char 1)
(and (>= (setq count (1- count)) 0)
(shr-char-kinsoku-bol-p (following-char))
(shr-char-breakable-p (following-char))))))))
(when (eq (following-char) ? )
(forward-char 1))))
(not failed)))
(defun shr-parse-base (url)
;; Always chop off anchors.
(when (string-match "#.*" url)
(setq url (substring url 0 (match-beginning 0))))
;; NB: URI may itself be relative to the document s URI
(setq url (shr-expand-url url))
(let* ((parsed (url-generic-parse-url url))
(local (url-filename parsed)))
(setf (url-filename parsed) "")
;; Chop off the bit after the last slash.
(when (string-match "\\`\\(.*/\\)[^/]+\\'" local)
(setq local (match-string 1 local)))
;; Always make the local bit end with a slash.
(when (and (not (zerop (length local)))
(not (eq (aref local (1- (length local))) ?/)))
(setq local (concat local "/")))
(list (url-recreate-url parsed)
local
(url-type parsed)
url)))
(autoload 'url-expand-file-name "url-expand")
;; FIXME This needs some tests writing.
;; Does it even need to exist, given that url-expand-file-name does?
(defun shr-expand-url (url &optional base)
(setq base
(if base
;; shr-parse-base should never call this with non-nil base!
(shr-parse-base base)
;; Bound by the parser.
shr-base))
(when (zerop (length url))
(setq url nil))
;; Strip leading whitespace
(and url (string-match "\\`\\s-+" url)
(setq url (substring url (match-end 0))))
(cond ((zerop (length url))
(nth 3 base))
((or (not base)
(string-match "\\`[a-z]*:" url))
;; Absolute or empty URI
url)
((eq (aref url 0) ?/)
(if (and (> (length url) 1)
(eq (aref url 1) ?/))
;; //host...; just use the protocol
(concat (nth 2 base) ":" url)
;; Just use the host name part.
(concat (car base) url)))
((eq (aref url 0) ?#)
;; A link to an anchor.
(concat (nth 3 base) url))
(t
;; Totally relative.
(url-expand-file-name url (concat (car base) (cadr base))))))
(defun shr-ensure-newline ()
(unless (bobp)
(let ((prefix (get-text-property (line-beginning-position)
'shr-prefix-length)))
(unless (or (zerop (current-column))
(and prefix
(= prefix (- (point) (line-beginning-position)))))
(insert "\n")))))
(defun shr-ensure-paragraph ()
(unless (bobp)
(let ((prefix (get-text-property (line-beginning-position)
'shr-prefix-length)))
(cond
((and (bolp)
(save-excursion
(forward-line -1)
(looking-at " *$")))
;; We're already at a new paragraph; do nothing.
)
((and prefix
(= prefix (- (point) (line-beginning-position))))
;; Do nothing; we're at the start of a