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