lisp/window.el: Fix previous change (2013-06-25T15:08:47Z!lekktu@gmail.com).
[bpt/emacs.git] / lisp / net / shr.el
CommitLineData
367f7f81
LMI
1;;; shr.el --- Simple HTML Renderer
2
ab422c4d 3;; Copyright (C) 2010-2013 Free Software Foundation, Inc.
367f7f81
LMI
4
5;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6;; Keywords: html
7
8;; This file is part of GNU Emacs.
9
10;; GNU Emacs is free software: you can redistribute it and/or modify
11;; it under the terms of the GNU General Public License as published by
12;; the Free Software Foundation, either version 3 of the License, or
13;; (at your option) any later version.
14
15;; GNU Emacs is distributed in the hope that it will be useful,
16;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;; GNU General Public License for more details.
19
20;; You should have received a copy of the GNU General Public License
21;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
22
23;;; Commentary:
24
25;; This package takes a HTML parse tree (as provided by
26;; libxml-parse-html-region) and renders it in the current buffer. It
27;; does not do CSS, JavaScript or anything advanced: It's geared
28;; towards rendering typical short snippets of HTML, like what you'd
29;; find in HTML email and the like.
30
31;;; Code:
32
3d319c8f 33(eval-when-compile (require 'cl))
7545bd25 34(eval-when-compile (require 'url)) ;For url-filename's setf handler.
71e691a5
G
35(require 'browse-url)
36
870409d4
G
37(defgroup shr nil
38 "Simple HTML Renderer"
2bed3f04 39 :version "24.1"
0ebd92a3 40 :group 'hypermedia)
870409d4
G
41
42(defcustom shr-max-image-proportion 0.9
43 "How big pictures displayed are in relation to the window they're in.
44A value of 0.7 means that they are allowed to take up 70% of the
45width and height of the window. If they are larger than this,
46and Emacs supports it, then the images will be rescaled down to
47fit these criteria."
48 :version "24.1"
49 :group 'shr
50 :type 'float)
51
52(defcustom shr-blocked-images nil
53 "Images that have URLs matching this regexp will be blocked."
54 :version "24.1"
55 :group 'shr
a931698a 56 :type '(choice (const nil) regexp))
870409d4 57
924d6997
G
58(defcustom shr-table-horizontal-line nil
59 "Character used to draw horizontal table lines.
60If nil, don't draw horizontal table lines."
d3098750
LMI
61 :group 'shr
62 :type 'character)
63
e37df674 64(defcustom shr-table-vertical-line ?\s
d3098750 65 "Character used to draw vertical table lines."
afba0c4b 66 :group 'shr
030158f3 67 :type 'character)
afba0c4b 68
e37df674 69(defcustom shr-table-corner ?\s
d3098750 70 "Character used to draw table corners."
6b7df8d3 71 :group 'shr
030158f3 72 :type 'character)
6b7df8d3
G
73
74(defcustom shr-hr-line ?-
d3098750 75 "Character used to draw hr lines."
afba0c4b 76 :group 'shr
030158f3 77 :type 'character)
afba0c4b 78
d0e0de31 79(defcustom shr-width fill-column
bb7f5cbc
G
80 "Frame width to use for rendering.
81May either be an integer specifying a fixed width in characters,
82or nil, meaning that the full width of the window should be
83used."
84 :type '(choice (integer :tag "Fixed width in characters")
85 (const :tag "Use the width of the window" nil))
d0e0de31
JD
86 :group 'shr)
87
c74cb344
G
88(defcustom shr-bullet "* "
89 "Bullet used for unordered lists.
90Alternative suggestions are:
91- \" \"
92- \" \""
93 :type 'string
94 :group 'shr)
95
0ebd92a3
LMI
96(defcustom shr-external-browser 'browse-url-default-browser
97 "Function used to launch an external browser."
98 :version "24.4"
99 :group 'shr
100 :type 'function)
101
130e977f
LMI
102(defvar shr-content-function nil
103 "If bound, this should be a function that will return the content.
104This is used for cid: URLs, and the function is called with the
105cid: URL as the argument.")
106
b9bdaf74
KY
107(defvar shr-put-image-function 'shr-put-image
108 "Function called to put image and alt string.")
109
6eee2678
LMI
110(defface shr-strike-through '((t (:strike-through t)))
111 "Font for <s> elements."
112 :group 'shr)
113
f8774e35 114(defface shr-link
7ef1d634 115 '((t (:inherit link)))
df26ce09 116 "Font for link elements."
c2f51e23
G
117 :group 'shr)
118
66627fa9
G
119;;; Internal variables.
120
870409d4
G
121(defvar shr-folding-mode nil)
122(defvar shr-state nil)
123(defvar shr-start nil)
a41c2e6d 124(defvar shr-indentation 0)
130e977f 125(defvar shr-inhibit-images nil)
66627fa9 126(defvar shr-list-mode nil)
3d319c8f 127(defvar shr-content-cache nil)
83ffd571 128(defvar shr-kinsoku-shorten nil)
99e65b2d 129(defvar shr-table-depth 0)
04db63bc 130(defvar shr-stylesheet nil)
dbd5ffad 131(defvar shr-base nil)
728518c3 132(defvar shr-ignore-cache nil)
2644071e 133(defvar shr-external-rendering-functions nil)
c74cb344 134(defvar shr-target-id nil)
be2aa135 135(defvar shr-inhibit-decoration nil)
924d6997 136(defvar shr-table-separator-length 1)
870409d4 137
71e691a5
G
138(defvar shr-map
139 (let ((map (make-sparse-keymap)))
140 (define-key map "a" 'shr-show-alt-text)
141 (define-key map "i" 'shr-browse-image)
89b163db 142 (define-key map "z" 'shr-zoom-image)
7304e4dd
LMI
143 (define-key map [tab] 'shr-next-link)
144 (define-key map [backtab] 'shr-previous-link)
970ad972 145 (define-key map [follow-link] 'mouse-face)
71e691a5
G
146 (define-key map "I" 'shr-insert-image)
147 (define-key map "u" 'shr-copy-url)
148 (define-key map "v" 'shr-browse-url)
cdf1fca4 149 (define-key map "o" 'shr-save-contents)
71e691a5
G
150 (define-key map "\r" 'shr-browse-url)
151 map))
152
66627fa9 153;; Public functions and commands.
0143b8a3
GM
154(declare-function libxml-parse-html-region "xml.c"
155 (start end &optional base-url))
66627fa9 156
7b953864
SM
157(defun shr-render-buffer (buffer)
158 "Display the HTML rendering of the current buffer."
159 (interactive (list (current-buffer)))
0143b8a3
GM
160 (or (fboundp 'libxml-parse-html-region)
161 (error "This function requires Emacs to be compiled with libxml2"))
1518e4f0
G
162 (pop-to-buffer "*html*")
163 (erase-buffer)
164 (shr-insert-document
7b953864 165 (with-current-buffer buffer
edd9679c
LMI
166 (libxml-parse-html-region (point-min) (point-max))))
167 (goto-char (point-min)))
1518e4f0 168
7b953864
SM
169(defun shr-visit-file (file)
170 "Parse FILE as an HTML document, and render it in a new buffer."
171 (interactive "fHTML file name: ")
172 (with-temp-buffer
173 (insert-file-contents file)
174 (shr-render-buffer (current-buffer))))
175
66627fa9
G
176;;;###autoload
177(defun shr-insert-document (dom)
9ed5a258
LI
178 "Render the parsed document DOM into the current buffer.
179DOM should be a parse tree as generated by
180`libxml-parse-html-region' or similar."
3d319c8f 181 (setq shr-content-cache nil)
9ed5a258
LI
182 (let ((start (point))
183 (shr-state nil)
bb7f5cbc 184 (shr-start nil)
dbd5ffad 185 (shr-base nil)
4452891d 186 (shr-preliminary-table-render 0)
924d6997 187 (shr-width (or shr-width (1- (window-width)))))
9ed5a258
LI
188 (shr-descend (shr-transform-dom dom))
189 (shr-remove-trailing-whitespace start (point))))
190
191(defun shr-remove-trailing-whitespace (start end)
7c4bbb69
LI
192 (let ((width (window-width)))
193 (save-restriction
194 (narrow-to-region start end)
195 (goto-char start)
196 (while (not (eobp))
197 (end-of-line)
888ab661 198 (when (> (shr-previous-newline-padding-width (current-column)) width)
7c4bbb69
LI
199 (dolist (overlay (overlays-at (point)))
200 (when (overlay-get overlay 'before-string)
201 (overlay-put overlay 'before-string nil))))
202 (forward-line 1)))))
66627fa9
G
203
204(defun shr-copy-url ()
205 "Copy the URL under point to the kill ring.
206If called twice, then try to fetch the URL and see whether it
207redirects somewhere else."
208 (interactive)
209 (let ((url (get-text-property (point) 'shr-url)))
210 (cond
211 ((not url)
212 (message "No URL under point"))
213 ;; Resolve redirected URLs.
214 ((equal url (car kill-ring))
215 (url-retrieve
216 url
217 (lambda (a)
218 (when (and (consp a)
219 (eq (car a) :redirect))
220 (with-temp-buffer
221 (insert (cadr a))
222 (goto-char (point-min))
223 ;; Remove common tracking junk from the URL.
224 (when (re-search-forward ".utm_.*" nil t)
225 (replace-match "" t t))
226 (message "Copied %s" (buffer-string))
038b3495
LI
227 (copy-region-as-kill (point-min) (point-max)))))
228 nil t))
66627fa9
G
229 ;; Copy the URL to the kill ring.
230 (t
231 (with-temp-buffer
232 (insert url)
233 (copy-region-as-kill (point-min) (point-max))
234 (message "Copied %s" url))))))
235
7304e4dd
LMI
236(defun shr-next-link ()
237 "Skip to the next link."
238 (interactive)
be2aa135 239 (let ((skip (text-property-any (point) (point-max) 'help-echo nil)))
7304e4dd 240 (if (not (setq skip (text-property-not-all skip (point-max)
be2aa135 241 'help-echo nil)))
7304e4dd
LMI
242 (message "No next link")
243 (goto-char skip)
244 (message "%s" (get-text-property (point) 'help-echo)))))
245
246(defun shr-previous-link ()
247 "Skip to the previous link."
248 (interactive)
249 (let ((start (point))
250 (found nil))
251 ;; Skip past the current link.
252 (while (and (not (bobp))
be2aa135 253 (get-text-property (point) 'help-echo))
7304e4dd
LMI
254 (forward-char -1))
255 ;; Find the previous link.
256 (while (and (not (bobp))
be2aa135 257 (not (setq found (get-text-property (point) 'help-echo))))
7304e4dd
LMI
258 (forward-char -1))
259 (if (not found)
260 (progn
261 (message "No previous link")
262 (goto-char start))
263 ;; Put point at the start of the link.
264 (while (and (not (bobp))
be2aa135 265 (get-text-property (point) 'help-echo))
7304e4dd
LMI
266 (forward-char -1))
267 (forward-char 1)
268 (message "%s" (get-text-property (point) 'help-echo)))))
269
66627fa9
G
270(defun shr-show-alt-text ()
271 "Show the ALT text of the image under point."
272 (interactive)
273 (let ((text (get-text-property (point) 'shr-alt)))
274 (if (not text)
275 (message "No image under point")
276 (message "%s" text))))
277
2da9c605
G
278(defun shr-browse-image (&optional copy-url)
279 "Browse the image under point.
280If COPY-URL (the prefix if called interactively) is non-nil, copy
281the URL of the image to the kill buffer instead."
282 (interactive "P")
8b6f6573 283 (let ((url (get-text-property (point) 'image-url)))
2da9c605
G
284 (cond
285 ((not url)
286 (message "No image under point"))
287 (copy-url
288 (with-temp-buffer
289 (insert url)
290 (copy-region-as-kill (point-min) (point-max))
291 (message "Copied %s" url)))
292 (t
66627fa9 293 (message "Browsing %s..." url)
2da9c605 294 (browse-url url)))))
66627fa9 295
3d319c8f
LMI
296(defun shr-insert-image ()
297 "Insert the image under point into the buffer."
298 (interactive)
8b6f6573 299 (let ((url (get-text-property (point) 'image-url)))
3d319c8f
LMI
300 (if (not url)
301 (message "No image under point")
302 (message "Inserting %s..." url)
303 (url-retrieve url 'shr-image-fetched
304 (list (current-buffer) (1- (point)) (point-marker))
038b3495 305 t t))))
3d319c8f 306
89b163db
G
307(defun shr-zoom-image ()
308 "Toggle the image size.
309The size will be rotated between the default size, the original
310size, and full-buffer size."
311 (interactive)
312 (let ((url (get-text-property (point) 'image-url))
313 (size (get-text-property (point) 'image-size))
314 (buffer-read-only nil))
315 (if (not url)
316 (message "No image under point")
317 ;; Delete the old picture.
318 (while (get-text-property (point) 'image-url)
319 (forward-char -1))
320 (forward-char 1)
321 (let ((start (point)))
322 (while (get-text-property (point) 'image-url)
323 (forward-char 1))
324 (forward-char -1)
325 (put-text-property start (point) 'display nil)
326 (when (> (- (point) start) 2)
327 (delete-region start (1- (point)))))
328 (message "Inserting %s..." url)
329 (url-retrieve url 'shr-image-fetched
330 (list (current-buffer) (1- (point)) (point-marker)
331 (list (cons 'size
332 (cond ((or (eq size 'default)
333 (null size))
334 'original)
335 ((eq size 'original)
336 'full)
337 ((eq size 'full)
338 'default)))))
339 t))))
340
66627fa9
G
341;;; Utility functions.
342
870409d4
G
343(defun shr-transform-dom (dom)
344 (let ((result (list (pop dom))))
345 (dolist (arg (pop dom))
346 (push (cons (intern (concat ":" (symbol-name (car arg))) obarray)
347 (cdr arg))
348 result))
349 (dolist (sub dom)
350 (if (stringp sub)
953d41c4 351 (push (cons 'text sub) result)
870409d4
G
352 (push (shr-transform-dom sub) result)))
353 (nreverse result)))
354
870409d4 355(defun shr-descend (dom)
2644071e
LMI
356 (let ((function
357 (or
358 ;; Allow other packages to override (or provide) rendering
359 ;; of elements.
360 (cdr (assq (car dom) shr-external-rendering-functions))
361 (intern (concat "shr-tag-" (symbol-name (car dom))) obarray)))
ebe79557 362 (style (cdr (assq :style (cdr dom))))
04db63bc 363 (shr-stylesheet shr-stylesheet)
ebe79557 364 (start (point)))
b31b26b4 365 (when style
be2aa135 366 (if (string-match "color\\|display\\|border-collapse" style)
b31b26b4
G
367 (setq shr-stylesheet (nconc (shr-parse-style style)
368 shr-stylesheet))
369 (setq style nil)))
c74cb344
G
370 ;; If we have a display:none, then just ignore this part of the
371 ;; DOM.
372 (unless (equal (cdr (assq 'display shr-stylesheet)) "none")
373 (if (fboundp function)
374 (funcall function (cdr dom))
375 (shr-generic (cdr dom)))
376 (when (and shr-target-id
377 (equal (cdr (assq :id (cdr dom))) shr-target-id))
378 (put-text-property start (1+ start) 'shr-target-id shr-target-id))
379 ;; If style is set, then this node has set the color.
380 (when style
381 (shr-colorize-region start (point)
382 (cdr (assq 'color shr-stylesheet))
383 (cdr (assq 'background-color shr-stylesheet)))))))
870409d4
G
384
385(defun shr-generic (cont)
386 (dolist (sub cont)
387 (cond
953d41c4 388 ((eq (car sub) 'text)
870409d4 389 (shr-insert (cdr sub)))
a41c2e6d 390 ((listp (cdr sub))
870409d4
G
391 (shr-descend sub)))))
392
ed797193
G
393(defmacro shr-char-breakable-p (char)
394 "Return non-nil if a line can be broken before and after CHAR."
395 `(aref fill-find-break-point-function-table ,char))
396(defmacro shr-char-nospace-p (char)
397 "Return non-nil if no space is required before and after CHAR."
398 `(aref fill-nospace-between-words-table ,char))
399
400;; KINSOKU is a Japanese word meaning a rule that should not be violated.
401;; In Emacs, it is a term used for characters, e.g. punctuation marks,
402;; parentheses, and so on, that should not be placed in the beginning
403;; of a line or the end of a line.
404(defmacro shr-char-kinsoku-bol-p (char)
405 "Return non-nil if a line ought not to begin with CHAR."
406 `(aref (char-category-set ,char) ?>))
407(defmacro shr-char-kinsoku-eol-p (char)
408 "Return non-nil if a line ought not to end with CHAR."
409 `(aref (char-category-set ,char) ?<))
410(unless (shr-char-kinsoku-bol-p (make-char 'japanese-jisx0208 33 35))
411 (load "kinsoku" nil t))
412
66627fa9 413(defun shr-insert (text)
6b7df8d3 414 (when (and (eq shr-state 'image)
89b163db 415 (not (bolp))
6b7df8d3 416 (not (string-match "\\`[ \t\n]+\\'" text)))
66627fa9
G
417 (insert "\n")
418 (setq shr-state nil))
419 (cond
420 ((eq shr-folding-mode 'none)
421 (insert text))
422 (t
c38e0c97 423 (when (and (string-match "\\`[ \t\n ]" text)
73db8b08
KY
424 (not (bolp))
425 (not (eq (char-after (1- (point))) ? )))
426 (insert " "))
c38e0c97 427 (dolist (elem (split-string text "[ \f\t\n\r\v ]+" t))
73db8b08
KY
428 (when (and (bolp)
429 (> shr-indentation 0))
430 (shr-indent))
73db8b08 431 ;; No space is needed behind a wide character categorized as
b41c2f65
KY
432 ;; kinsoku-bol, between characters both categorized as nospace,
433 ;; or at the beginning of a line.
73db8b08 434 (let (prev)
48ba8195
KY
435 (when (and (> (current-column) shr-indentation)
436 (eq (preceding-char) ? )
20438017 437 (or (= (line-beginning-position) (1- (point)))
ed797193
G
438 (and (shr-char-breakable-p
439 (setq prev (char-after (- (point) 2))))
440 (shr-char-kinsoku-bol-p prev))
441 (and (shr-char-nospace-p prev)
442 (shr-char-nospace-p (aref elem 0)))))
73db8b08 443 (delete-char -1)))
48ba8195
KY
444 ;; The shr-start is a special variable that is used to pass
445 ;; upwards the first point in the buffer where the text really
446 ;; starts.
447 (unless shr-start
448 (setq shr-start (point)))
73db8b08 449 (insert elem)
e76917e6 450 (setq shr-state nil)
e7102c0a
KY
451 (let (found)
452 (while (and (> (current-column) shr-width)
453 (progn
454 (setq found (shr-find-fill-point))
b40950bf 455 (not (eolp))))
fe98a42f
KY
456 (when (eq (preceding-char) ? )
457 (delete-char -1))
458 (insert "\n")
459 (unless found
e7102c0a
KY
460 ;; No space is needed at the beginning of a line.
461 (when (eq (following-char) ? )
462 (delete-char 1)))
463 (when (> shr-indentation 0)
464 (shr-indent))
465 (end-of-line))
466 (insert " ")))
c38e0c97 467 (unless (string-match "[ \t\r\n ]\\'" text)
73db8b08 468 (delete-char -1)))))
66627fa9 469
6b7df8d3 470(defun shr-find-fill-point ()
83ffd571
KY
471 (when (> (move-to-column shr-width) shr-width)
472 (backward-char 1))
ed797193
G
473 (let ((bp (point))
474 failed)
475 (while (not (or (setq failed (= (current-column) shr-indentation))
476 (eq (preceding-char) ? )
477 (eq (following-char) ? )
478 (shr-char-breakable-p (preceding-char))
479 (shr-char-breakable-p (following-char))
7454326a
G
480 (if (eq (preceding-char) ?')
481 (not (memq (char-after (- (point) 2))
482 (list nil ?\n ? )))
7454326a 483 (and (shr-char-kinsoku-bol-p (preceding-char))
6568edea 484 (shr-char-breakable-p (following-char))
7454326a 485 (not (shr-char-kinsoku-bol-p (following-char)))))
ed797193 486 (shr-char-kinsoku-eol-p (following-char))))
83ffd571 487 (backward-char 1))
ed797193
G
488 (if (and (not (or failed (eolp)))
489 (eq (preceding-char) ?'))
490 (while (not (or (setq failed (eolp))
491 (eq (following-char) ? )
492 (shr-char-breakable-p (following-char))
493 (shr-char-kinsoku-eol-p (following-char))))
494 (forward-char 1)))
83ffd571 495 (if failed
20438017 496 ;; There's no breakable point, so we give it up.
ed797193
G
497 (let (found)
498 (goto-char bp)
499 (unless shr-kinsoku-shorten
500 (while (and (setq found (re-search-forward
501 "\\(\\c>\\)\\| \\|\\c<\\|\\c|"
502 (line-end-position) 'move))
503 (eq (preceding-char) ?')))
504 (if (and found (not (match-beginning 1)))
505 (goto-char (match-beginning 0)))))
b40950bf
KY
506 (or
507 (eolp)
ed797193
G
508 ;; Don't put kinsoku-bol characters at the beginning of a line,
509 ;; or kinsoku-eol characters at the end of a line.
510 (cond
511 (shr-kinsoku-shorten
512 (while (and (not (memq (preceding-char) (list ?\C-@ ?\n ? )))
513 (shr-char-kinsoku-eol-p (preceding-char)))
514 (backward-char 1))
515 (when (setq failed (= (current-column) shr-indentation))
516 ;; There's no breakable point that doesn't violate kinsoku,
517 ;; so we look for the second best position.
518 (while (and (progn
519 (forward-char 1)
520 (<= (current-column) shr-width))
521 (progn
522 (setq bp (point))
523 (shr-char-kinsoku-eol-p (following-char)))))
524 (goto-char bp)))
525 ((shr-char-kinsoku-eol-p (preceding-char))
89b163db
G
526 ;; Find backward the point where kinsoku-eol characters begin.
527 (let ((count 4))
528 (while
529 (progn
530 (backward-char 1)
531 (and (> (setq count (1- count)) 0)
532 (not (memq (preceding-char) (list ?\C-@ ?\n ? )))
533 (or (shr-char-kinsoku-eol-p (preceding-char))
534 (shr-char-kinsoku-bol-p (following-char)))))))
535 (if (setq failed (= (current-column) shr-indentation))
536 ;; There's no breakable point that doesn't violate kinsoku,
537 ;; so we go to the second best position.
538 (if (looking-at "\\(\\c<+\\)\\c<")
539 (goto-char (match-end 1))
540 (forward-char 1))))
541 ((shr-char-kinsoku-bol-p (following-char))
542 ;; Find forward the point where kinsoku-bol characters end.
543 (let ((count 4))
544 (while (progn
545 (forward-char 1)
546 (and (>= (setq count (1- count)) 0)
ed797193 547 (shr-char-kinsoku-bol-p (following-char))
89b163db 548 (shr-char-breakable-p (following-char))))))))
ed797193
G
549 (when (eq (following-char) ? )
550 (forward-char 1))))
551 (not failed)))
6b7df8d3 552
c74cb344
G
553(defun shr-parse-base (url)
554 ;; Always chop off anchors.
555 (when (string-match "#.*" url)
556 (setq url (substring url 0 (match-beginning 0))))
557 (let* ((parsed (url-generic-parse-url url))
558 (local (url-filename parsed)))
559 (setf (url-filename parsed) "")
560 ;; Chop off the bit after the last slash.
561 (when (string-match "\\`\\(.*/\\)[^/]+\\'" local)
562 (setq local (match-string 1 local)))
563 ;; Always make the local bit end with a slash.
564 (when (and (not (zerop (length local)))
565 (not (eq (aref local (1- (length local))) ?/)))
566 (setq local (concat local "/")))
567 (list (url-recreate-url parsed)
568 local
569 (url-type parsed)
570 url)))
571
572(defun shr-expand-url (url &optional base)
573 (setq base
574 (if base
575 (shr-parse-base base)
576 ;; Bound by the parser.
577 shr-base))
578 (when (zerop (length url))
579 (setq url nil))
580 (cond ((or (not url)
581 (not base)
582 (string-match "\\`[a-z]*:" url))
583 ;; Absolute URL.
584 (or url (car base)))
585 ((eq (aref url 0) ?/)
586 (if (and (> (length url) 1)
587 (eq (aref url 1) ?/))
588 ;; //host...; just use the protocol
589 (concat (nth 2 base) ":" url)
590 ;; Just use the host name part.
591 (concat (car base) url)))
592 ((eq (aref url 0) ?#)
593 ;; A link to an anchor.
594 (concat (nth 3 base) url))
595 (t
596 ;; Totally relative.
597 (concat (car base) (cadr base) url))))
dbd5ffad 598
66627fa9
G
599(defun shr-ensure-newline ()
600 (unless (zerop (current-column))
601 (insert "\n")))
a41c2e6d
G
602
603(defun shr-ensure-paragraph ()
604 (unless (bobp)
f7aa248a 605 (if (<= (current-column) shr-indentation)
71e691a5
G
606 (unless (save-excursion
607 (forward-line -1)
608 (looking-at " *$"))
a41c2e6d
G
609 (insert "\n"))
610 (if (save-excursion
611 (beginning-of-line)
be2aa135
LMI
612 ;; If the current line is totally blank, and doesn't even
613 ;; have any face properties set, then delete the blank
614 ;; space.
615 (and (looking-at " *$")
616 (not (get-text-property (point) 'face))
617 (not (= (next-single-property-change (point) 'face nil
618 (line-end-position))
619 (line-end-position)))))
89b163db 620 (delete-region (match-beginning 0) (match-end 0))
a41c2e6d
G
621 (insert "\n\n")))))
622
66627fa9 623(defun shr-indent ()
f7aa248a
G
624 (when (> shr-indentation 0)
625 (insert (make-string shr-indentation ? ))))
870409d4 626
a41c2e6d 627(defun shr-fontize-cont (cont &rest types)
870409d4
G
628 (let (shr-start)
629 (shr-generic cont)
a41c2e6d
G
630 (dolist (type types)
631 (shr-add-font (or shr-start (point)) (point) type))))
870409d4 632
7304e4dd
LMI
633;; Add face to the region, but avoid putting the font properties on
634;; blank text at the start of the line, and the newline at the end, to
635;; avoid ugliness.
870409d4 636(defun shr-add-font (start end type)
be2aa135
LMI
637 (unless shr-inhibit-decoration
638 (save-excursion
639 (goto-char start)
640 (while (< (point) end)
641 (when (bolp)
642 (skip-chars-forward " "))
643 (add-face-text-property (point) (min (line-end-position) end) type t)
644 (if (< (line-end-position) end)
645 (forward-line 1)
646 (goto-char end))))))
870409d4 647
bdaa086b
LMI
648(defun shr-browse-url (&optional external)
649 "Browse the URL under point.
650If EXTERNAL, browse the URL using `shr-external-browser'."
5196f88a 651 (interactive "P")
71e691a5 652 (let ((url (get-text-property (point) 'shr-url)))
181cb5fb
G
653 (cond
654 ((not url)
655 (message "No link under point"))
656 ((string-match "^mailto:" url)
39ddff39 657 (browse-url-mail url))
181cb5fb 658 (t
bdaa086b
LMI
659 (if external
660 (funcall shr-external-browser url)
661 (browse-url url))))))
71e691a5 662
cdf1fca4
LMI
663(defun shr-save-contents (directory)
664 "Save the contents from URL in a file."
665 (interactive "DSave contents of URL to directory: ")
666 (let ((url (get-text-property (point) 'shr-url)))
667 (if (not url)
668 (message "No link under point")
669 (url-retrieve (shr-encode-url url)
038b3495
LI
670 'shr-store-contents (list url directory)
671 nil t))))
cdf1fca4
LMI
672
673(defun shr-store-contents (status url directory)
674 (unless (plist-get status :error)
675 (when (or (search-forward "\n\n" nil t)
676 (search-forward "\r\n\r\n" nil t))
677 (write-region (point) (point-max)
678 (expand-file-name (file-name-nondirectory url)
679 directory)))))
680
89b163db 681(defun shr-image-fetched (status buffer start end &optional flags)
0e2cebe5
LI
682 (let ((image-buffer (current-buffer)))
683 (when (and (buffer-name buffer)
684 (not (plist-get status :error)))
685 (url-store-in-cache image-buffer)
686 (when (or (search-forward "\n\n" nil t)
687 (search-forward "\r\n\r\n" nil t))
688 (let ((data (buffer-substring (point) (point-max))))
689 (with-current-buffer buffer
690 (save-excursion
691 (let ((alt (buffer-substring start end))
89b163db 692 (properties (text-properties-at start))
0e2cebe5
LI
693 (inhibit-read-only t))
694 (delete-region start end)
695 (goto-char start)
89b163db
G
696 (funcall shr-put-image-function data alt flags)
697 (while properties
698 (let ((type (pop properties))
699 (value (pop properties)))
700 (unless (memq type '(display image-size))
701 (put-text-property start (point) type value))))))))))
0e2cebe5 702 (kill-buffer image-buffer)))
870409d4 703
2250b351
DE
704(defun shr-image-from-data (data)
705 "Return an image from the data: URI content DATA."
706 (when (string-match
707 "\\(\\([^/;,]+\\(/[^;,]+\\)?\\)\\(;[^;,]+\\)*\\)?,\\(.*\\)"
708 data)
709 (let ((param (match-string 4 data))
710 (payload (url-unhex-string (match-string 5 data))))
711 (when (string-match "^.*\\(;[ \t]*base64\\)$" param)
712 (setq payload (base64-decode-string payload)))
713 payload)))
714
89b163db 715(defun shr-put-image (data alt &optional flags)
b9bdaf74 716 "Put image DATA with a string ALT. Return image."
4abff904 717 (if (display-graphic-p)
89b163db
G
718 (let* ((size (cdr (assq 'size flags)))
719 (start (point))
720 (image (cond
721 ((eq size 'original)
722 (create-image data nil t :ascent 100))
723 ((eq size 'full)
724 (ignore-errors
725 (shr-rescale-image data t)))
726 (t
727 (ignore-errors
728 (shr-rescale-image data))))))
4abff904 729 (when image
c0f9edce
G
730 ;; When inserting big-ish pictures, put them at the
731 ;; beginning of the line.
732 (when (and (> (current-column) 0)
733 (> (car (image-size image t)) 400))
734 (insert "\n"))
89b163db 735 (if (eq size 'original)
7304e4dd 736 (insert-sliced-image image (or alt "*") nil 20 1)
89b163db
G
737 (insert-image image (or alt "*")))
738 (put-text-property start (point) 'image-size size)
dd8620de
GM
739 (when (cond ((fboundp 'image-multi-frame-p)
740 ;; Only animate multi-frame things that specify a
741 ;; delay; eg animated gifs as opposed to
742 ;; multi-page tiffs. FIXME?
743 (cdr (image-multi-frame-p image)))
744 ((fboundp 'image-animated-p)
745 (image-animated-p image)))
c146ad85 746 (image-animate image nil 60)))
b9bdaf74 747 image)
99e65b2d 748 (insert alt)))
870409d4 749
89b163db
G
750(defun shr-rescale-image (data &optional force)
751 "Rescale DATA, if too big, to fit the current buffer.
752If FORCE, rescale the image anyway."
f3f9606c
LMI
753 (if (or (not (fboundp 'imagemagick-types))
754 (not (get-buffer-window (current-buffer))))
755 (create-image data nil t :ascent 100)
756 (let ((edges (window-inside-pixel-edges
757 (get-buffer-window (current-buffer)))))
758 (create-image
759 data 'imagemagick t
760 :ascent 100
761 :max-width (truncate (* shr-max-image-proportion
762 (- (nth 2 edges) (nth 0 edges))))
763 :max-height (truncate (* shr-max-image-proportion
764 (- (nth 3 edges) (nth 1 edges))))))))
870409d4 765
85a45a69
GM
766;; url-cache-extract autoloads url-cache.
767(declare-function url-cache-create-filename "url-cache" (url))
768(autoload 'mm-disable-multibyte "mm-util")
39ddff39 769(autoload 'browse-url-mail "browse-url")
85a45a69 770
870409d4
G
771(defun shr-get-image-data (url)
772 "Get image data for URL.
773Return a string with image data."
774 (with-temp-buffer
775 (mm-disable-multibyte)
71e691a5 776 (when (ignore-errors
ab67634f 777 (url-cache-extract (url-cache-create-filename (shr-encode-url url)))
71e691a5
G
778 t)
779 (when (or (search-forward "\n\n" nil t)
780 (search-forward "\r\n\r\n" nil t))
781 (buffer-substring (point) (point-max))))))
870409d4 782
40de2c6d
KY
783(defun shr-image-displayer (content-function)
784 "Return a function to display an image.
785CONTENT-FUNCTION is a function to retrieve an image for a cid url that
786is an argument. The function to be returned takes three arguments URL,
53964682 787START, and END. Note that START and END should be markers."
40de2c6d 788 `(lambda (url start end)
f8d8a97b
KY
789 (when url
790 (if (string-match "\\`cid:" url)
791 ,(when content-function
792 `(let ((image (funcall ,content-function
793 (substring url (match-end 0)))))
794 (when image
795 (goto-char start)
b9bdaf74 796 (funcall shr-put-image-function
195b2593 797 image (buffer-substring start end))
e2d0ba98 798 (delete-region (point) end))))
f8d8a97b
KY
799 (url-retrieve url 'shr-image-fetched
800 (list (current-buffer) start end)
038b3495 801 t t)))))
40de2c6d 802
66627fa9
G
803(defun shr-heading (cont &rest types)
804 (shr-ensure-paragraph)
805 (apply #'shr-fontize-cont cont types)
806 (shr-ensure-paragraph))
807
04db63bc 808(defun shr-urlify (start url &optional title)
be2aa135 809 (when (and title (string-match "ctx" title)) (debug))
ca3cf0a5 810 (shr-add-font start (point) 'shr-link)
7304e4dd
LMI
811 (add-text-properties
812 start (point)
813 (list 'shr-url url
be2aa135 814 'help-echo (if title (format "%s (%s)" url title) url)
970ad972 815 'keymap shr-map)))
de635afe
G
816
817(defun shr-encode-url (url)
818 "Encode URL."
819 (browse-url-url-encode-chars url "[)$ ]"))
820
ebe79557
LMI
821(autoload 'shr-color-visible "shr-color")
822(autoload 'shr-color->hexadecimal "shr-color")
144b7b5c
G
823
824(defun shr-color-check (fg bg)
825 "Check that FG is visible on BG.
826Returns (fg bg) with corrected values.
827Returns nil if the colors that would be used are the default
828ones, in case fg and bg are nil."
829 (when (or fg bg)
830 (let ((fixed (cond ((null fg) 'fg)
831 ((null bg) 'bg))))
832 ;; Convert colors to hexadecimal, or set them to default.
833 (let ((fg (or (shr-color->hexadecimal fg)
834 (frame-parameter nil 'foreground-color)))
835 (bg (or (shr-color->hexadecimal bg)
836 (frame-parameter nil 'background-color))))
837 (cond ((eq fixed 'bg)
838 ;; Only return the new fg
839 (list nil (cadr (shr-color-visible bg fg t))))
840 ((eq fixed 'fg)
841 ;; Invert args and results and return only the new bg
842 (list (cadr (shr-color-visible fg bg t)) nil))
843 (t
844 (shr-color-visible bg fg)))))))
845
04db63bc 846(defun shr-colorize-region (start end fg &optional bg)
be2aa135
LMI
847 (when (and (not shr-inhibit-decoration)
848 (or fg bg))
04db63bc 849 (let ((new-colors (shr-color-check fg bg)))
144b7b5c 850 (when new-colors
60568d74 851 (when fg
be2aa135
LMI
852 (add-face-text-property start end
853 (list :foreground (cadr new-colors))
854 t))
04db63bc 855 (when bg
be2aa135
LMI
856 (add-face-text-property start end
857 (list :background (car new-colors))
858 t)))
ec72bf63 859 new-colors)))
04db63bc 860
c5ecc769
G
861(defun shr-expand-newlines (start end color)
862 (save-restriction
d709b79a
LI
863 ;; Skip past all white space at the start and ends.
864 (goto-char start)
865 (skip-chars-forward " \t\n")
866 (beginning-of-line)
867 (setq start (point))
868 (goto-char end)
869 (skip-chars-backward " \t\n")
870 (forward-line 1)
871 (setq end (point))
c5ecc769 872 (narrow-to-region start end)
160ae063 873 (let ((width (shr-buffer-width))
c5ecc769
G
874 column)
875 (goto-char (point-min))
876 (while (not (eobp))
877 (end-of-line)
19e0dbe0
KY
878 (when (and (< (setq column (current-column)) width)
879 (< (setq column (shr-previous-newline-padding-width column))
d709b79a 880 width))
7304e4dd 881 (let ((overlay (make-overlay (point) (1+ (point)))))
c5ecc769 882 (overlay-put overlay 'before-string
d709b79a
LI
883 (concat
884 (mapconcat
885 (lambda (overlay)
14596870
KY
886 (let ((string (plist-get
887 (overlay-properties overlay)
888 'before-string)))
d709b79a
LI
889 (if (not string)
890 ""
891 (overlay-put overlay 'before-string "")
892 string)))
893 (overlays-at (point))
894 "")
19e0dbe0 895 (propertize (make-string (- width column) ? )
d709b79a 896 'face (list :background color))))))
c5ecc769 897 (forward-line 1)))))
04db63bc 898
d709b79a
LI
899(defun shr-previous-newline-padding-width (width)
900 (let ((overlays (overlays-at (point)))
901 (previous-width 0))
902 (if (null overlays)
903 width
904 (dolist (overlay overlays)
905 (setq previous-width
906 (+ previous-width
14596870
KY
907 (length (plist-get (overlay-properties overlay)
908 'before-string)))))
d709b79a
LI
909 (+ width previous-width))))
910
66627fa9
G
911;;; Tag-specific rendering rules.
912
144b7b5c 913(defun shr-tag-body (cont)
04db63bc 914 (let* ((start (point))
af4e5f4c
G
915 (fgcolor (cdr (or (assq :fgcolor cont)
916 (assq :text cont))))
04db63bc 917 (bgcolor (cdr (assq :bgcolor cont)))
b31b26b4
G
918 (shr-stylesheet (list (cons 'color fgcolor)
919 (cons 'background-color bgcolor))))
144b7b5c 920 (shr-generic cont)
04db63bc 921 (shr-colorize-region start (point) fgcolor bgcolor)))
144b7b5c 922
b31b26b4
G
923(defun shr-tag-style (cont)
924 )
925
f73341e2
LMI
926(defun shr-tag-script (cont)
927 )
928
fb1b0ef6
LMI
929(defun shr-tag-comment (cont)
930 )
931
c74cb344
G
932(defun shr-dom-to-xml (dom)
933 "Convert DOM into a string containing the xml representation."
934 (let ((arg " ")
935 (text ""))
936 (dolist (sub (cdr dom))
937 (cond
938 ((listp (cdr sub))
939 (setq text (concat text (shr-dom-to-xml sub))))
940 ((eq (car sub) 'text)
941 (setq text (concat text (cdr sub))))
942 (t
943 (setq arg (concat arg (format "%s=\"%s\" "
944 (substring (symbol-name (car sub)) 1)
945 (cdr sub)))))))
946 (format "<%s%s>%s</%s>"
947 (car dom)
948 (substring arg 0 (1- (length arg)))
949 text
950 (car dom))))
951
65e704b9 952(defun shr-tag-svg (cont)
c74cb344
G
953 (when (image-type-available-p 'svg)
954 (funcall shr-put-image-function
955 (shr-dom-to-xml (cons 'svg cont))
956 "SVG Image")))
65e704b9 957
a3af2929
LMI
958(defun shr-tag-sup (cont)
959 (let ((start (point)))
960 (shr-generic cont)
961 (put-text-property start (point) 'display '(raise 0.5))))
962
963(defun shr-tag-sub (cont)
964 (let ((start (point)))
965 (shr-generic cont)
966 (put-text-property start (point) 'display '(raise -0.5))))
967
7bafe9bc
LMI
968(defun shr-tag-label (cont)
969 (shr-generic cont)
970 (shr-ensure-paragraph))
971
66627fa9
G
972(defun shr-tag-p (cont)
973 (shr-ensure-paragraph)
f7aa248a 974 (shr-indent)
66627fa9
G
975 (shr-generic cont)
976 (shr-ensure-paragraph))
977
036d93bc
KY
978(defun shr-tag-div (cont)
979 (shr-ensure-newline)
980 (shr-indent)
981 (shr-generic cont)
982 (shr-ensure-newline))
983
6eee2678
LMI
984(defun shr-tag-s (cont)
985 (shr-fontize-cont cont 'shr-strike-through))
986
55385ebc
JD
987(defun shr-tag-del (cont)
988 (shr-fontize-cont cont 'shr-strike-through))
989
66627fa9
G
990(defun shr-tag-b (cont)
991 (shr-fontize-cont cont 'bold))
992
993(defun shr-tag-i (cont)
994 (shr-fontize-cont cont 'italic))
995
996(defun shr-tag-em (cont)
087d8265 997 (shr-fontize-cont cont 'italic))
66627fa9 998
530f7b67
LMI
999(defun shr-tag-strong (cont)
1000 (shr-fontize-cont cont 'bold))
1001
66627fa9
G
1002(defun shr-tag-u (cont)
1003 (shr-fontize-cont cont 'underline))
1004
2e76c12c
LMI
1005(defun shr-parse-style (style)
1006 (when style
a2994808
JD
1007 (save-match-data
1008 (when (string-match "\n" style)
1009 (setq style (replace-match " " t t style))))
2e76c12c
LMI
1010 (let ((plist nil))
1011 (dolist (elem (split-string style ";"))
1012 (when elem
1013 (setq elem (split-string elem ":"))
1014 (when (and (car elem)
1015 (cadr elem))
1016 (let ((name (replace-regexp-in-string "^ +\\| +$" "" (car elem)))
1017 (value (replace-regexp-in-string "^ +\\| +$" "" (cadr elem))))
144b7b5c
G
1018 (when (string-match " *!important\\'" value)
1019 (setq value (substring value 0 (match-beginning 0))))
2e76c12c
LMI
1020 (push (cons (intern name obarray)
1021 value)
1022 plist)))))
1023 plist)))
1024
dbd5ffad 1025(defun shr-tag-base (cont)
be2aa135
LMI
1026 (let ((base (cdr (assq :href cont))))
1027 (when base
1028 (setq shr-base (shr-parse-base base))))
266c63b5 1029 (shr-generic cont))
dbd5ffad 1030
66627fa9
G
1031(defun shr-tag-a (cont)
1032 (let ((url (cdr (assq :href cont)))
04db63bc 1033 (title (cdr (assq :title cont)))
66627fa9
G
1034 (start (point))
1035 shr-start)
1036 (shr-generic cont)
be2aa135
LMI
1037 (when (and url
1038 (not shr-inhibit-decoration))
cc21c235 1039 (shr-urlify (or shr-start start) (shr-expand-url url) title))))
de635afe
G
1040
1041(defun shr-tag-object (cont)
99e65b2d
G
1042 (let ((start (point))
1043 url)
1044 (dolist (elem cont)
1045 (when (eq (car elem) 'embed)
1046 (setq url (or url (cdr (assq :src (cdr elem))))))
1047 (when (and (eq (car elem) 'param)
1048 (equal (cdr (assq :name (cdr elem))) "movie"))
1049 (setq url (or url (cdr (assq :value (cdr elem)))))))
de635afe
G
1050 (when url
1051 (shr-insert " [multimedia] ")
dbd5ffad 1052 (shr-urlify start (shr-expand-url url)))
99e65b2d
G
1053 (shr-generic cont)))
1054
1055(defun shr-tag-video (cont)
1056 (let ((image (cdr (assq :poster cont)))
1057 (url (cdr (assq :src cont)))
1058 (start (point)))
1059 (shr-tag-img nil image)
dbd5ffad 1060 (shr-urlify start (shr-expand-url url))))
ab67634f 1061
99e65b2d
G
1062(defun shr-tag-img (cont &optional url)
1063 (when (or url
1064 (and cont
1065 (cdr (assq :src cont))))
68f6bd17
KY
1066 (when (and (> (current-column) 0)
1067 (not (eq shr-state 'image)))
1068 (insert "\n"))
1069 (let ((alt (cdr (assq :alt cont)))
dbd5ffad 1070 (url (shr-expand-url (or url (cdr (assq :src cont))))))
68f6bd17
KY
1071 (let ((start (point-marker)))
1072 (when (zerop (length alt))
953d41c4 1073 (setq alt "*"))
68f6bd17 1074 (cond
99e65b2d
G
1075 ((or (member (cdr (assq :height cont)) '("0" "1"))
1076 (member (cdr (assq :width cont)) '("0" "1")))
1077 ;; Ignore zero-sized or single-pixel images.
1078 )
2250b351
DE
1079 ((and (not shr-inhibit-images)
1080 (string-match "\\`data:" url))
1081 (let ((image (shr-image-from-data (substring url (match-end 0)))))
1082 (if image
1083 (funcall shr-put-image-function image alt)
1084 (insert alt))))
68f6bd17
KY
1085 ((and (not shr-inhibit-images)
1086 (string-match "\\`cid:" url))
1087 (let ((url (substring url (match-end 0)))
1088 image)
1089 (if (or (not shr-content-function)
1090 (not (setq image (funcall shr-content-function url))))
1091 (insert alt)
b9bdaf74 1092 (funcall shr-put-image-function image alt))))
68f6bd17
KY
1093 ((or shr-inhibit-images
1094 (and shr-blocked-images
1095 (string-match shr-blocked-images url)))
1096 (setq shr-start (point))
1097 (let ((shr-state 'space))
b354bc53
KY
1098 (if (> (string-width alt) 8)
1099 (shr-insert (truncate-string-to-width alt 8))
68f6bd17 1100 (shr-insert alt))))
728518c3
LMI
1101 ((and (not shr-ignore-cache)
1102 (url-is-cached (shr-encode-url url)))
b9bdaf74 1103 (funcall shr-put-image-function (shr-get-image-data url) alt))
68f6bd17 1104 (t
64522086 1105 (insert alt " ")
728518c3
LMI
1106 (when (and shr-ignore-cache
1107 (url-is-cached (shr-encode-url url)))
1108 (let ((file (url-cache-create-filename (shr-encode-url url))))
1109 (when (file-exists-p file)
1110 (delete-file file))))
038b3495 1111 (url-queue-retrieve
f3b146e9 1112 (shr-encode-url url) 'shr-image-fetched
64522086 1113 (list (current-buffer) start (set-marker (make-marker) (1- (point))))
038b3495 1114 t t)))
a959fc40
KY
1115 (when (zerop shr-table-depth) ;; We are not in a table.
1116 (put-text-property start (point) 'keymap shr-map)
1117 (put-text-property start (point) 'shr-alt alt)
1118 (put-text-property start (point) 'image-url url)
1119 (put-text-property start (point) 'image-displayer
1120 (shr-image-displayer shr-content-function))
1121 (put-text-property start (point) 'help-echo alt))
68f6bd17 1122 (setq shr-state 'image)))))
66627fa9
G
1123
1124(defun shr-tag-pre (cont)
1125 (let ((shr-folding-mode 'none))
1126 (shr-ensure-newline)
f7aa248a 1127 (shr-indent)
66627fa9
G
1128 (shr-generic cont)
1129 (shr-ensure-newline)))
1130
1131(defun shr-tag-blockquote (cont)
1132 (shr-ensure-paragraph)
f7aa248a 1133 (shr-indent)
66627fa9
G
1134 (let ((shr-indentation (+ shr-indentation 4)))
1135 (shr-generic cont))
1136 (shr-ensure-paragraph))
a41c2e6d 1137
d2aa9780
LMI
1138(defun shr-tag-dl (cont)
1139 (shr-ensure-paragraph)
1140 (shr-generic cont)
1141 (shr-ensure-paragraph))
1142
1143(defun shr-tag-dt (cont)
1144 (shr-ensure-newline)
1145 (shr-generic cont)
1146 (shr-ensure-newline))
1147
1148(defun shr-tag-dd (cont)
1149 (shr-ensure-newline)
1150 (let ((shr-indentation (+ shr-indentation 4)))
1151 (shr-generic cont)))
1152
a41c2e6d
G
1153(defun shr-tag-ul (cont)
1154 (shr-ensure-paragraph)
1155 (let ((shr-list-mode 'ul))
3d319c8f
LMI
1156 (shr-generic cont))
1157 (shr-ensure-paragraph))
a41c2e6d
G
1158
1159(defun shr-tag-ol (cont)
3d319c8f 1160 (shr-ensure-paragraph)
a41c2e6d 1161 (let ((shr-list-mode 1))
3d319c8f
LMI
1162 (shr-generic cont))
1163 (shr-ensure-paragraph))
a41c2e6d
G
1164
1165(defun shr-tag-li (cont)
c74cb344 1166 (shr-ensure-newline)
f7aa248a 1167 (shr-indent)
8028ed5c
LMI
1168 (let* ((bullet
1169 (if (numberp shr-list-mode)
1170 (prog1
1171 (format "%d " shr-list-mode)
1172 (setq shr-list-mode (1+ shr-list-mode)))
c74cb344 1173 shr-bullet))
8028ed5c
LMI
1174 (shr-indentation (+ shr-indentation (length bullet))))
1175 (insert bullet)
1176 (shr-generic cont)))
a41c2e6d
G
1177
1178(defun shr-tag-br (cont)
89b163db
G
1179 (when (and (not (bobp))
1180 ;; Only add a newline if we break the current line, or
1181 ;; the previous line isn't a blank line.
1182 (or (not (bolp))
1183 (and (> (- (point) 2) (point-min))
1184 (not (= (char-after (- (point) 2)) ?\n)))))
f7aa248a
G
1185 (insert "\n")
1186 (shr-indent))
a41c2e6d
G
1187 (shr-generic cont))
1188
308c9d24 1189(defun shr-tag-span (cont)
be2aa135 1190 (shr-generic cont))
308c9d24 1191
a41c2e6d 1192(defun shr-tag-h1 (cont)
e4dbdb09 1193 (shr-heading cont 'bold 'underline))
a41c2e6d
G
1194
1195(defun shr-tag-h2 (cont)
e4dbdb09 1196 (shr-heading cont 'bold))
a41c2e6d
G
1197
1198(defun shr-tag-h3 (cont)
e4dbdb09 1199 (shr-heading cont 'italic))
a41c2e6d
G
1200
1201(defun shr-tag-h4 (cont)
e4dbdb09 1202 (shr-heading cont))
a41c2e6d
G
1203
1204(defun shr-tag-h5 (cont)
e4dbdb09 1205 (shr-heading cont))
a41c2e6d
G
1206
1207(defun shr-tag-h6 (cont)
e4dbdb09 1208 (shr-heading cont))
a41c2e6d 1209
3d319c8f
LMI
1210(defun shr-tag-hr (cont)
1211 (shr-ensure-newline)
6b7df8d3 1212 (insert (make-string shr-width shr-hr-line) "\n"))
3d319c8f 1213
144b7b5c
G
1214(defun shr-tag-title (cont)
1215 (shr-heading cont 'bold 'underline))
1216
1110d53b 1217(defun shr-tag-font (cont)
b31b26b4
G
1218 (let* ((start (point))
1219 (color (cdr (assq :color cont)))
1220 (shr-stylesheet (nconc (list (cons 'color color))
1221 shr-stylesheet)))
1110d53b 1222 (shr-generic cont)
b31b26b4
G
1223 (when color
1224 (shr-colorize-region start (point) color
1225 (cdr (assq 'background-color shr-stylesheet))))))
1110d53b 1226
66627fa9 1227;;; Table rendering algorithm.
a41c2e6d 1228
a0ec382a
LMI
1229;; Table rendering is the only complicated thing here. We do this by
1230;; first counting how many TDs there are in each TR, and registering
1231;; how wide they think they should be ("width=45%", etc). Then we
1232;; render each TD separately (this is done in temporary buffers, so
1233;; that we can use all the rendering machinery as if we were in the
1234;; main buffer). Now we know how much space each TD really takes, so
1235;; we then render everything again with the new widths, and finally
1236;; insert all these boxes into the main buffer.
6c769311 1237(defun shr-tag-table-1 (cont)
71e691a5
G
1238 (setq cont (or (cdr (assq 'tbody cont))
1239 cont))
130e977f 1240 (let* ((shr-inhibit-images t)
99e65b2d 1241 (shr-table-depth (1+ shr-table-depth))
83ffd571 1242 (shr-kinsoku-shorten t)
a0ec382a 1243 ;; Find all suggested widths.
130e977f 1244 (columns (shr-column-specs cont))
a0ec382a 1245 ;; Compute how many characters wide each TD should be.
71e691a5 1246 (suggested-widths (shr-pro-rate-columns columns))
a0ec382a
LMI
1247 ;; Do a "test rendering" to see how big each TD is (this can
1248 ;; be smaller (if there's little text) or bigger (if there's
1249 ;; unbreakable text).
71e691a5 1250 (sketch (shr-make-table cont suggested-widths))
160ae063
LMI
1251 ;; Compute the "natural" width by setting each column to 500
1252 ;; characters and see how wide they really render.
1253 (natural (shr-make-table cont (make-vector (length columns) 500)))
1254 (sketch-widths (shr-table-widths sketch natural suggested-widths)))
030158f3 1255 ;; This probably won't work very well.
83ffd571
KY
1256 (when (> (+ (loop for width across sketch-widths
1257 summing (1+ width))
1258 shr-indentation 1)
030158f3
G
1259 (frame-width))
1260 (setq truncate-lines t))
a0ec382a 1261 ;; Then render the table again with these new "hard" widths.
f462d10a 1262 (shr-insert-table (shr-make-table cont sketch-widths t) sketch-widths)))
130e977f 1263
6c769311
KY
1264(defun shr-tag-table (cont)
1265 (shr-ensure-paragraph)
1266 (let* ((caption (cdr (assq 'caption cont)))
1267 (header (cdr (assq 'thead cont)))
1268 (body (or (cdr (assq 'tbody cont)) cont))
1269 (footer (cdr (assq 'tfoot cont)))
144b7b5c 1270 (bgcolor (cdr (assq :bgcolor cont)))
60568d74
LMI
1271 (start (point))
1272 (shr-stylesheet (nconc (list (cons 'background-color bgcolor))
1273 shr-stylesheet))
6c769311
KY
1274 (nheader (if header (shr-max-columns header)))
1275 (nbody (if body (shr-max-columns body)))
1276 (nfooter (if footer (shr-max-columns footer))))
2146e256
LMI
1277 (if (and (not caption)
1278 (not header)
1279 (not (cdr (assq 'tbody cont)))
1280 (not (cdr (assq 'tr cont)))
1281 (not footer))
1282 ;; The table is totally invalid and just contains random junk.
1283 ;; Try to output it anyway.
1284 (shr-generic cont)
1285 ;; It's a real table, so render it.
1286 (shr-tag-table-1
1287 (nconc
1288 (if caption `((tr (td ,@caption))))
1289 (if header
1290 (if footer
1291 ;; hader + body + footer
1292 (if (= nheader nbody)
1293 (if (= nbody nfooter)
1294 `((tr (td (table (tbody ,@header ,@body ,@footer)))))
1295 (nconc `((tr (td (table (tbody ,@header ,@body)))))
1296 (if (= nfooter 1)
1297 footer
1298 `((tr (td (table (tbody ,@footer))))))))
1299 (nconc `((tr (td (table (tbody ,@header)))))
1300 (if (= nbody nfooter)
1301 `((tr (td (table (tbody ,@body ,@footer)))))
1302 (nconc `((tr (td (table (tbody ,@body)))))
1303 (if (= nfooter 1)
1304 footer
1305 `((tr (td (table (tbody ,@footer))))))))))
1306 ;; header + body
3c066373 1307 (if (= nheader nbody)
2146e256
LMI
1308 `((tr (td (table (tbody ,@header ,@body)))))
1309 (if (= nheader 1)
1310 `(,@header (tr (td (table (tbody ,@body)))))
1311 `((tr (td (table (tbody ,@header))))
1312 (tr (td (table (tbody ,@body))))))))
1313 (if footer
1314 ;; body + footer
1315 (if (= nbody nfooter)
1316 `((tr (td (table (tbody ,@body ,@footer)))))
1317 (nconc `((tr (td (table (tbody ,@body)))))
1318 (if (= nfooter 1)
1319 footer
1320 `((tr (td (table (tbody ,@footer))))))))
1321 (if caption
1322 `((tr (td (table (tbody ,@body)))))
1323 body))))))
60568d74
LMI
1324 (when bgcolor
1325 (shr-colorize-region start (point) (cdr (assq 'color shr-stylesheet))
f462d10a
LMI
1326 bgcolor))
1327 ;; Finally, insert all the images after the table. The Emacs buffer
1328 ;; model isn't strong enough to allow us to put the images actually
1329 ;; into the tables.
1330 (when (zerop shr-table-depth)
1331 (dolist (elem (shr-find-elements cont 'img))
1332 (shr-tag-img (cdr elem))))))
6c769311 1333
130e977f
LMI
1334(defun shr-find-elements (cont type)
1335 (let (result)
1336 (dolist (elem cont)
1337 (cond ((eq (car elem) type)
1338 (push elem result))
1339 ((consp (cdr elem))
1340 (setq result (nconc (shr-find-elements (cdr elem) type) result)))))
1341 (nreverse result)))
71e691a5
G
1342
1343(defun shr-insert-table (table widths)
be2aa135
LMI
1344 (let* ((collapse (equal (cdr (assq 'border-collapse shr-stylesheet))
1345 "collapse"))
924d6997 1346 (shr-table-separator-length (if collapse 0 1))
be2aa135
LMI
1347 (shr-table-vertical-line (if collapse "" shr-table-vertical-line)))
1348 (unless collapse
1349 (shr-insert-table-ruler widths))
1350 (dolist (row table)
1351 (let ((start (point))
1352 (height (let ((max 0))
1353 (dolist (column row)
1354 (setq max (max max (cadr column))))
1355 max)))
1356 (dotimes (i height)
1357 (shr-indent)
1358 (insert shr-table-vertical-line "\n"))
1359 (dolist (column row)
1360 (goto-char start)
1361 (let ((lines (nth 2 column)))
1362 (dolist (line lines)
1363 (end-of-line)
1364 (insert line shr-table-vertical-line)
1365 (forward-line 1))
1366 ;; Add blank lines at padding at the bottom of the TD,
1367 ;; possibly.
1368 (dotimes (i (- height (length lines)))
1369 (end-of-line)
1370 (let ((start (point)))
1371 (insert (make-string (string-width (car lines)) ? )
1372 shr-table-vertical-line)
1373 (when (nth 4 column)
1374 (shr-add-font start (1- (point))
1375 (list :background (nth 4 column)))))
1376 (forward-line 1)))))
1377 (unless collapse
1378 (shr-insert-table-ruler widths)))))
71e691a5
G
1379
1380(defun shr-insert-table-ruler (widths)
924d6997
G
1381 (when shr-table-horizontal-line
1382 (when (and (bolp)
1383 (> shr-indentation 0))
1384 (shr-indent))
1385 (insert shr-table-corner)
1386 (dotimes (i (length widths))
1387 (insert (make-string (aref widths i) shr-table-horizontal-line)
1388 shr-table-corner))
1389 (insert "\n")))
71e691a5 1390
160ae063 1391(defun shr-table-widths (table natural-table suggested-widths)
a7dcc87b
G
1392 (let* ((length (length suggested-widths))
1393 (widths (make-vector length 0))
1394 (natural-widths (make-vector length 0)))
71e691a5
G
1395 (dolist (row table)
1396 (let ((i 0))
1397 (dolist (column row)
160ae063
LMI
1398 (aset widths i (max (aref widths i) column))
1399 (setq i (1+ i)))))
1400 (dolist (row natural-table)
1401 (let ((i 0))
1402 (dolist (column row)
1403 (aset natural-widths i (max (aref natural-widths i) column))
a7dcc87b 1404 (setq i (1+ i)))))
863b61d6
KY
1405 (let ((extra (- (apply '+ (append suggested-widths nil))
1406 (apply '+ (append widths nil))))
a7dcc87b 1407 (expanded-columns 0))
160ae063
LMI
1408 ;; We have extra, unused space, so divide this space amongst the
1409 ;; columns.
a7dcc87b 1410 (when (> extra 0)
160ae063
LMI
1411 ;; If the natural width is wider than the rendered width, we
1412 ;; want to allow the column to expand.
a7dcc87b 1413 (dotimes (i length)
a7dcc87b
G
1414 (when (> (aref natural-widths i) (aref widths i))
1415 (setq expanded-columns (1+ expanded-columns))))
1416 (dotimes (i length)
1417 (when (> (aref natural-widths i) (aref widths i))
1418 (aset widths i (min
160ae063 1419 (aref natural-widths i)
a7dcc87b
G
1420 (+ (/ extra expanded-columns)
1421 (aref widths i))))))))
71e691a5
G
1422 widths))
1423
1424(defun shr-make-table (cont widths &optional fill)
c74cb344
G
1425 (or (cadr (assoc (list cont widths fill) shr-content-cache))
1426 (let ((data (shr-make-table-1 cont widths fill)))
1427 (push (list (list cont widths fill) data)
1428 shr-content-cache)
1429 data)))
1430
1431(defun shr-make-table-1 (cont widths &optional fill)
be2aa135 1432 (let ((trs nil)
924d6997
G
1433 (shr-inhibit-decoration (not fill))
1434 (rowspans (make-vector (length widths) 0))
1435 width colspan)
71e691a5
G
1436 (dolist (row cont)
1437 (when (eq (car row) 'tr)
a0ec382a
LMI
1438 (let ((tds nil)
1439 (columns (cdr row))
1440 (i 0)
924d6997 1441 (width-column 0)
a0ec382a
LMI
1442 column)
1443 (while (< i (length widths))
924d6997
G
1444 ;; If we previously had a rowspan definition, then that
1445 ;; means that we now have a "missing" td/th element here.
1446 ;; So just insert a dummy, empty one to (sort of) emulate
1447 ;; rowspan.
1448 (setq column
1449 (if (zerop (aref rowspans i))
1450 (pop columns)
1451 (aset rowspans i (1- (aref rowspans i)))
1452 '(td)))
a0ec382a 1453 (when (or (memq (car column) '(td th))
924d6997
G
1454 (not column))
1455 (when (cdr (assq :rowspan (cdr column)))
1456 (aset rowspans i (+ (aref rowspans i)
1457 (1- (string-to-number
1458 (cdr (assq :rowspan (cdr column))))))))
970ad972
G
1459 ;; Sanity check for invalid column-spans.
1460 (when (>= width-column (length widths))
1461 (setq width-column 0))
924d6997
G
1462 (setq width
1463 (if column
1464 (aref widths width-column)
1465 0))
1466 (when (and fill
1467 (setq colspan (cdr (assq :colspan (cdr column)))))
1468 (setq colspan (string-to-number colspan))
1469 (dotimes (j (1- colspan))
1470 (if (> (+ i 1 j) (1- (length widths)))
1471 (setq width (aref widths (1- (length widths))))
1472 (setq width (+ width
1473 shr-table-separator-length
1474 (aref widths (+ i 1 j))))))
1475 (setq width-column (+ width-column (1- colspan))))
1476 (when (or column
1477 (not fill))
1478 (push (shr-render-td (cdr column) width fill)
1479 tds))
1480 (setq i (1+ i)
1481 width-column (1+ width-column))))
71e691a5
G
1482 (push (nreverse tds) trs))))
1483 (nreverse trs)))
1484
1485(defun shr-render-td (cont width fill)
04db63bc 1486 (with-temp-buffer
60568d74
LMI
1487 (let ((bgcolor (cdr (assq :bgcolor cont)))
1488 (fgcolor (cdr (assq :fgcolor cont)))
1489 (style (cdr (assq :style cont)))
1490 (shr-stylesheet shr-stylesheet)
7304e4dd 1491 actual-colors)
60568d74
LMI
1492 (when style
1493 (setq style (and (string-match "color" style)
1494 (shr-parse-style style))))
1495 (when bgcolor
1496 (setq style (nconc (list (cons 'background-color bgcolor)) style)))
1497 (when fgcolor
1498 (setq style (nconc (list (cons 'color fgcolor)) style)))
1499 (when style
1500 (setq shr-stylesheet (append style shr-stylesheet)))
c74cb344
G
1501 (let ((shr-width width)
1502 (shr-indentation 0))
1503 (shr-descend (cons 'td cont)))
1504 ;; Delete padding at the bottom of the TDs.
1505 (delete-region
1506 (point)
1507 (progn
1508 (skip-chars-backward " \t\n")
1509 (end-of-line)
1510 (point)))
60568d74
LMI
1511 (goto-char (point-min))
1512 (let ((max 0))
1513 (while (not (eobp))
1514 (end-of-line)
1515 (setq max (max max (current-column)))
1516 (forward-line 1))
1517 (when fill
1518 (goto-char (point-min))
1519 ;; If the buffer is totally empty, then put a single blank
1520 ;; line here.
1521 (if (zerop (buffer-size))
1522 (insert (make-string width ? ))
1523 ;; Otherwise, fill the buffer.
544d4594
LMI
1524 (let ((align (cdr (assq :align cont)))
1525 length)
1526 (while (not (eobp))
1527 (end-of-line)
1528 (setq length (- width (current-column)))
1529 (when (> length 0)
1530 (cond
1531 ((equal align "right")
1532 (beginning-of-line)
1533 (insert (make-string length ? )))
1534 ((equal align "center")
1535 (insert (make-string (/ length 2) ? ))
1536 (beginning-of-line)
1537 (insert (make-string (- length (/ length 2)) ? )))
1538 (t
1539 (insert (make-string length ? )))))
1540 (forward-line 1))))
abb97fbb 1541 (when style
ec72bf63
G
1542 (setq actual-colors
1543 (shr-colorize-region
1544 (point-min) (point-max)
1545 (cdr (assq 'color shr-stylesheet))
1546 (cdr (assq 'background-color shr-stylesheet))))))
60568d74
LMI
1547 (if fill
1548 (list max
1549 (count-lines (point-min) (point-max))
1550 (split-string (buffer-string) "\n")
7304e4dd 1551 nil
ec72bf63 1552 (car actual-colors))
160ae063 1553 max)))))
a7dcc87b 1554
160ae063 1555(defun shr-buffer-width ()
a7dcc87b 1556 (goto-char (point-min))
160ae063 1557 (let ((max 0))
a7dcc87b
G
1558 (while (not (eobp))
1559 (end-of-line)
160ae063 1560 (setq max (max max (current-column)))
a7dcc87b
G
1561 (forward-line 1))
1562 max))
130e977f 1563
71e691a5
G
1564(defun shr-pro-rate-columns (columns)
1565 (let ((total-percentage 0)
1566 (widths (make-vector (length columns) 0)))
1567 (dotimes (i (length columns))
a7dcc87b 1568 (setq total-percentage (+ total-percentage (aref columns i))))
71e691a5
G
1569 (setq total-percentage (/ 1.0 total-percentage))
1570 (dotimes (i (length columns))
1571 (aset widths i (max (truncate (* (aref columns i)
1572 total-percentage
a7dcc87b 1573 (- shr-width (1+ (length columns)))))
71e691a5
G
1574 10)))
1575 widths))
1576
1577;; Return a summary of the number and shape of the TDs in the table.
1578(defun shr-column-specs (cont)
1579 (let ((columns (make-vector (shr-max-columns cont) 1)))
1580 (dolist (row cont)
1581 (when (eq (car row) 'tr)
1582 (let ((i 0))
1583 (dolist (column (cdr row))
1584 (when (memq (car column) '(td th))
1585 (let ((width (cdr (assq :width (cdr column)))))
1586 (when (and width
5d852256
LMI
1587 (string-match "\\([0-9]+\\)%" width)
1588 (not (zerop (setq width (string-to-number
1589 (match-string 1 width))))))
1590 (aset columns i (/ width 100.0))))
130e977f 1591 (setq i (1+ i)))))))
71e691a5
G
1592 columns))
1593
1594(defun shr-count (cont elem)
1595 (let ((i 0))
1596 (dolist (sub cont)
1597 (when (eq (car sub) elem)
1598 (setq i (1+ i))))
1599 i))
1600
1601(defun shr-max-columns (cont)
1602 (let ((max 0))
1603 (dolist (row cont)
1604 (when (eq (car row) 'tr)
130e977f
LMI
1605 (setq max (max max (+ (shr-count (cdr row) 'td)
1606 (shr-count (cdr row) 'th))))))
71e691a5
G
1607 max))
1608
7304e4dd
LMI
1609;; Emacs less than 24.3
1610(unless (fboundp 'add-face-text-property)
544d4594 1611 (defun add-face-text-property (beg end face &optional appendp object)
7304e4dd
LMI
1612 "Combine FACE BEG and END."
1613 (let ((b beg))
1614 (while (< b end)
1615 (let ((oldval (get-text-property b 'face)))
1616 (put-text-property
1617 b (setq b (next-single-property-change b 'face nil end))
1618 'face (cond ((null oldval)
1619 face)
1620 ((and (consp oldval)
1621 (not (keywordp (car oldval))))
544d4594
LMI
1622 (if appendp
1623 (nconc oldval (list face))
1624 (cons face oldval)))
7304e4dd 1625 (t
544d4594
LMI
1626 (if appendp
1627 (list oldval face)
1628 (list face oldval))))))))))
7304e4dd 1629
f3fd95db 1630(provide 'shr)
367f7f81 1631
b4543a28 1632;; Local Variables:
c38e0c97 1633;; coding: utf-8
b4543a28
G
1634;; End:
1635
367f7f81 1636;;; shr.el ends here