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