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