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