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