]> code.delx.au - gnu-emacs/blob - lisp/net/shr.el
shr space widths fixup
[gnu-emacs] / lisp / net / shr.el
1 ;;; shr.el --- Simple HTML Renderer
2
3 ;; Copyright (C) 2010-2015 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 (eval-when-compile (require 'url)) ;For url-filename's setf handler.
35 (require 'browse-url)
36 (require 'subr-x)
37 (require 'dom)
38
39 (defgroup shr nil
40 "Simple HTML Renderer"
41 :version "25.1"
42 :group 'web)
43
44 (defcustom shr-max-image-proportion 0.9
45 "How big pictures displayed are in relation to the window they're in.
46 A value of 0.7 means that they are allowed to take up 70% of the
47 width and height of the window. If they are larger than this,
48 and Emacs supports it, then the images will be rescaled down to
49 fit these criteria."
50 :version "24.1"
51 :group 'shr
52 :type 'float)
53
54 (defcustom shr-blocked-images nil
55 "Images that have URLs matching this regexp will be blocked."
56 :version "24.1"
57 :group 'shr
58 :type '(choice (const nil) regexp))
59
60 (defcustom shr-use-fonts nil
61 "If non-nil, use proportional fonts for text."
62 :version "25.1"
63 :group 'shr
64 :type 'boolean)
65
66 (defcustom shr-table-horizontal-line nil
67 "Character used to draw horizontal table lines.
68 If nil, don't draw horizontal table lines."
69 :group 'shr
70 :type '(choice (const nil) character))
71
72 (defcustom shr-table-vertical-line ?\s
73 "Character used to draw vertical table lines."
74 :group 'shr
75 :type 'character)
76
77 (defcustom shr-table-corner ?\s
78 "Character used to draw table corners."
79 :group 'shr
80 :type 'character)
81
82 (defcustom shr-hr-line ?-
83 "Character used to draw hr lines."
84 :group 'shr
85 :type 'character)
86
87 (defcustom shr-width nil
88 "Frame width to use for rendering.
89 May either be an integer specifying a fixed width in characters,
90 or nil, meaning that the full width of the window should be
91 used."
92 :version "25.1"
93 :type '(choice (integer :tag "Fixed width in characters")
94 (const :tag "Use the width of the window" nil))
95 :group 'shr)
96
97 (defcustom shr-bullet "* "
98 "Bullet used for unordered lists.
99 Alternative suggestions are:
100 - \" \"
101 - \" \""
102 :version "24.4"
103 :type 'string
104 :group 'shr)
105
106 (defcustom shr-external-browser 'browse-url-default-browser
107 "Function used to launch an external browser."
108 :version "24.4"
109 :group 'shr
110 :type 'function)
111
112 (defcustom shr-image-animate t
113 "Non nil means that images that can be animated will be."
114 :version "24.4"
115 :group 'shr
116 :type 'boolean)
117
118 (defvar shr-content-function nil
119 "If bound, this should be a function that will return the content.
120 This is used for cid: URLs, and the function is called with the
121 cid: URL as the argument.")
122
123 (defvar shr-put-image-function 'shr-put-image
124 "Function called to put image and alt string.")
125
126 (defface shr-strike-through '((t (:strike-through t)))
127 "Font for <s> elements."
128 :group 'shr)
129
130 (defface shr-link
131 '((t (:inherit link)))
132 "Font for link elements."
133 :group 'shr)
134
135 (defvar shr-inhibit-images nil
136 "If non-nil, inhibit loading images.")
137
138 ;;; Internal variables.
139
140 (defvar shr-folding-mode nil)
141 (defvar shr-start nil)
142 (defvar shr-indentation 0)
143 (defvar shr-internal-width nil)
144 (defvar shr-list-mode nil)
145 (defvar shr-content-cache nil)
146 (defvar shr-kinsoku-shorten nil)
147 (defvar shr-table-depth 0)
148 (defvar shr-stylesheet nil)
149 (defvar shr-base nil)
150 (defvar shr-depth 0)
151 (defvar shr-warning nil)
152 (defvar shr-ignore-cache nil)
153 (defvar shr-external-rendering-functions nil)
154 (defvar shr-target-id nil)
155 (defvar shr-inhibit-decoration nil)
156 (defvar shr-table-separator-length 1)
157 (defvar shr-table-separator-pixel-width 0)
158 (defvar shr-table-id nil)
159 (defvar shr-current-font nil)
160
161 (defvar shr-map
162 (let ((map (make-sparse-keymap)))
163 (define-key map "a" 'shr-show-alt-text)
164 (define-key map "i" 'shr-browse-image)
165 (define-key map "z" 'shr-zoom-image)
166 (define-key map [?\t] 'shr-next-link)
167 (define-key map [?\M-\t] 'shr-previous-link)
168 (define-key map [follow-link] 'mouse-face)
169 (define-key map [mouse-2] 'shr-browse-url)
170 (define-key map "I" 'shr-insert-image)
171 (define-key map "w" 'shr-copy-url)
172 (define-key map "u" 'shr-copy-url)
173 (define-key map "v" 'shr-browse-url)
174 (define-key map "o" 'shr-save-contents)
175 (define-key map "\r" 'shr-browse-url)
176 map))
177
178 ;; Public functions and commands.
179 (declare-function libxml-parse-html-region "xml.c"
180 (start end &optional base-url))
181
182 (defun shr-render-buffer (buffer)
183 "Display the HTML rendering of the current buffer."
184 (interactive (list (current-buffer)))
185 (or (fboundp 'libxml-parse-html-region)
186 (error "This function requires Emacs to be compiled with libxml2"))
187 (pop-to-buffer "*html*")
188 (erase-buffer)
189 (shr-insert-document
190 (with-current-buffer buffer
191 (libxml-parse-html-region (point-min) (point-max))))
192 (goto-char (point-min)))
193
194 ;;;###autoload
195 (defun shr-render-region (begin end &optional buffer)
196 "Display the HTML rendering of the region between BEGIN and END."
197 (interactive "r")
198 (unless (fboundp 'libxml-parse-html-region)
199 (error "This function requires Emacs to be compiled with libxml2"))
200 (with-current-buffer (or buffer (current-buffer))
201 (let ((dom (libxml-parse-html-region begin end)))
202 (delete-region begin end)
203 (goto-char begin)
204 (shr-insert-document dom))))
205
206 ;;;###autoload
207 (defun shr-insert-document (dom)
208 "Render the parsed document DOM into the current buffer.
209 DOM should be a parse tree as generated by
210 `libxml-parse-html-region' or similar."
211 (setq shr-content-cache nil)
212 (let ((start (point))
213 (shr-start nil)
214 (shr-base nil)
215 (shr-depth 0)
216 (shr-table-id 0)
217 (shr-warning nil)
218 (shr-table-separator-pixel-width (shr-string-pixel-width "-"))
219 (shr-internal-width (or (and shr-width
220 (if (not shr-use-fonts)
221 shr-width
222 (* shr-width (frame-char-width))))
223 (if (not shr-use-fonts)
224 (- (window-width) 2)
225 (- (window-pixel-width)
226 (* (frame-fringe-width) 2))))))
227 (shr-descend dom)
228 (shr-fill-lines start (point))
229 (shr-remove-trailing-whitespace start (point))
230 (when shr-warning
231 (message "%s" shr-warning))))
232
233 (defun shr-remove-trailing-whitespace (start end)
234 (let ((width (window-width)))
235 (save-restriction
236 (narrow-to-region start end)
237 (goto-char start)
238 (while (not (eobp))
239 (end-of-line)
240 (when (> (shr-previous-newline-padding-width (current-column)) width)
241 (dolist (overlay (overlays-at (point)))
242 (when (overlay-get overlay 'before-string)
243 (overlay-put overlay 'before-string nil))))
244 (forward-line 1)))))
245
246 (defun shr-copy-url (&optional image-url)
247 "Copy the URL under point to the kill ring.
248 If IMAGE-URL (the prefix) is non-nil, or there is no link under
249 point, but there is an image under point then copy the URL of the
250 image under point instead.
251 If called twice, then try to fetch the URL and see whether it
252 redirects somewhere else."
253 (interactive "P")
254 (let ((url (or (get-text-property (point) 'shr-url)
255 (get-text-property (point) 'image-url))))
256 (cond
257 ((not url)
258 (message "No URL under point"))
259 ;; Resolve redirected URLs.
260 ((equal url (car kill-ring))
261 (url-retrieve
262 url
263 (lambda (a)
264 (when (and (consp a)
265 (eq (car a) :redirect))
266 (with-temp-buffer
267 (insert (cadr a))
268 (goto-char (point-min))
269 ;; Remove common tracking junk from the URL.
270 (when (re-search-forward ".utm_.*" nil t)
271 (replace-match "" t t))
272 (message "Copied %s" (buffer-string))
273 (copy-region-as-kill (point-min) (point-max)))))
274 nil t))
275 ;; Copy the URL to the kill ring.
276 (t
277 (with-temp-buffer
278 (insert (url-encode-url url))
279 (copy-region-as-kill (point-min) (point-max))
280 (message "Copied %s" (buffer-string)))))))
281
282 (defun shr-next-link ()
283 "Skip to the next link."
284 (interactive)
285 (let ((skip (text-property-any (point) (point-max) 'help-echo nil)))
286 (if (or (eobp)
287 (not (setq skip (text-property-not-all skip (point-max)
288 'help-echo nil))))
289 (message "No next link")
290 (goto-char skip)
291 (message "%s" (get-text-property (point) 'help-echo)))))
292
293 (defun shr-previous-link ()
294 "Skip to the previous link."
295 (interactive)
296 (let ((start (point))
297 (found nil))
298 ;; Skip past the current link.
299 (while (and (not (bobp))
300 (get-text-property (point) 'help-echo))
301 (forward-char -1))
302 ;; Find the previous link.
303 (while (and (not (bobp))
304 (not (setq found (get-text-property (point) 'help-echo))))
305 (forward-char -1))
306 (if (not found)
307 (progn
308 (message "No previous link")
309 (goto-char start))
310 ;; Put point at the start of the link.
311 (while (and (not (bobp))
312 (get-text-property (point) 'help-echo))
313 (forward-char -1))
314 (forward-char 1)
315 (message "%s" (get-text-property (point) 'help-echo)))))
316
317 (defun shr-show-alt-text ()
318 "Show the ALT text of the image under point."
319 (interactive)
320 (let ((text (get-text-property (point) 'shr-alt)))
321 (if (not text)
322 (message "No image under point")
323 (message "%s" (shr-fill-text text)))))
324
325 (defun shr-browse-image (&optional copy-url)
326 "Browse the image under point.
327 If COPY-URL (the prefix if called interactively) is non-nil, copy
328 the URL of the image to the kill buffer instead."
329 (interactive "P")
330 (let ((url (get-text-property (point) 'image-url)))
331 (cond
332 ((not url)
333 (message "No image under point"))
334 (copy-url
335 (with-temp-buffer
336 (insert url)
337 (copy-region-as-kill (point-min) (point-max))
338 (message "Copied %s" url)))
339 (t
340 (message "Browsing %s..." url)
341 (browse-url url)))))
342
343 (defun shr-insert-image ()
344 "Insert the image under point into the buffer."
345 (interactive)
346 (let ((url (get-text-property (point) 'image-url)))
347 (if (not url)
348 (message "No image under point")
349 (message "Inserting %s..." url)
350 (url-retrieve url 'shr-image-fetched
351 (list (current-buffer) (1- (point)) (point-marker))
352 t t))))
353
354 (defun shr-zoom-image ()
355 "Toggle the image size.
356 The size will be rotated between the default size, the original
357 size, and full-buffer size."
358 (interactive)
359 (let ((url (get-text-property (point) 'image-url))
360 (size (get-text-property (point) 'image-size))
361 (buffer-read-only nil))
362 (if (not url)
363 (message "No image under point")
364 ;; Delete the old picture.
365 (while (get-text-property (point) 'image-url)
366 (forward-char -1))
367 (forward-char 1)
368 (let ((start (point)))
369 (while (get-text-property (point) 'image-url)
370 (forward-char 1))
371 (forward-char -1)
372 (put-text-property start (point) 'display nil)
373 (when (> (- (point) start) 2)
374 (delete-region start (1- (point)))))
375 (message "Inserting %s..." url)
376 (url-retrieve url 'shr-image-fetched
377 (list (current-buffer) (1- (point)) (point-marker)
378 (list (cons 'size
379 (cond ((or (eq size 'default)
380 (null size))
381 'original)
382 ((eq size 'original)
383 'full)
384 ((eq size 'full)
385 'default)))))
386 t))))
387
388 ;;; Utility functions.
389
390 (defsubst shr-generic (dom)
391 (dolist (sub (dom-children dom))
392 (if (stringp sub)
393 (shr-insert sub)
394 (shr-descend sub))))
395
396 (defun shr-descend (dom)
397 (let ((function
398 (or
399 ;; Allow other packages to override (or provide) rendering
400 ;; of elements.
401 (cdr (assq (dom-tag dom) shr-external-rendering-functions))
402 (intern (concat "shr-tag-" (symbol-name (dom-tag dom))) obarray)))
403 (style (dom-attr dom 'style))
404 (shr-stylesheet shr-stylesheet)
405 (shr-depth (1+ shr-depth))
406 (start (point)))
407 ;; shr uses about 12 frames per nested node.
408 (if (> shr-depth (/ max-specpdl-size 12))
409 (setq shr-warning "Too deeply nested to render properly; consider increasing `max-specpdl-size'")
410 (when style
411 (if (string-match "color\\|display\\|border-collapse" style)
412 (setq shr-stylesheet (nconc (shr-parse-style style)
413 shr-stylesheet))
414 (setq style nil)))
415 ;; If we have a display:none, then just ignore this part of the DOM.
416 (unless (equal (cdr (assq 'display shr-stylesheet)) "none")
417 (if (fboundp function)
418 (funcall function dom)
419 (shr-generic dom))
420 (when (and shr-target-id
421 (equal (dom-attr dom 'id) shr-target-id))
422 ;; If the element was empty, we don't have anything to put the
423 ;; anchor on. So just insert a dummy character.
424 (when (= start (point))
425 (insert "*"))
426 (put-text-property start (1+ start) 'shr-target-id shr-target-id))
427 ;; If style is set, then this node has set the color.
428 (when style
429 (shr-colorize-region
430 start (point)
431 (cdr (assq 'color shr-stylesheet))
432 (cdr (assq 'background-color shr-stylesheet))))))))
433
434 (defun shr-fill-text (text)
435 (if (zerop (length text))
436 text
437 (with-temp-buffer
438 (let ((shr-indentation 0)
439 (shr-start nil)
440 (shr-internal-width (- (window-pixel-width)
441 (* (frame-fringe-width) 2))))
442 (shr-insert text)
443 (buffer-string)))))
444
445 (define-inline shr-char-breakable-p (char)
446 "Return non-nil if a line can be broken before and after CHAR."
447 (inline-quote (aref fill-find-break-point-function-table ,char)))
448 (define-inline shr-char-nospace-p (char)
449 "Return non-nil if no space is required before and after CHAR."
450 (inline-quote (aref fill-nospace-between-words-table ,char)))
451
452 ;; KINSOKU is a Japanese word meaning a rule that should not be violated.
453 ;; In Emacs, it is a term used for characters, e.g. punctuation marks,
454 ;; parentheses, and so on, that should not be placed in the beginning
455 ;; of a line or the end of a line.
456 (define-inline shr-char-kinsoku-bol-p (char)
457 "Return non-nil if a line ought not to begin with CHAR."
458 (inline-letevals (char)
459 (inline-quote (and (not (eq ,char ?'))
460 (aref (char-category-set ,char) ?>)))))
461 (define-inline shr-char-kinsoku-eol-p (char)
462 "Return non-nil if a line ought not to end with CHAR."
463 (inline-quote (aref (char-category-set ,char) ?<)))
464 (unless (shr-char-kinsoku-bol-p (make-char 'japanese-jisx0208 33 35))
465 (load "kinsoku" nil t))
466
467 (defun shr-pixel-column ()
468 (if (not shr-use-fonts)
469 (current-column)
470 (if (not (get-buffer-window (current-buffer)))
471 (save-window-excursion
472 (set-window-buffer nil (current-buffer))
473 (car (window-text-pixel-size nil (line-beginning-position) (point))))
474 (car (window-text-pixel-size nil (line-beginning-position) (point))))))
475
476 (defun shr-pixel-region ()
477 (- (shr-pixel-column)
478 (save-excursion
479 (goto-char (mark))
480 (shr-pixel-column))))
481
482 (defun shr-string-pixel-width (string)
483 (if (not shr-use-fonts)
484 (length string)
485 (with-temp-buffer
486 (insert string)
487 (shr-pixel-column))))
488
489 (defun shr-insert (text)
490 (when (and (not (bolp))
491 (get-text-property (1- (point)) 'image-url))
492 (insert "\n"))
493 (cond
494 ((eq shr-folding-mode 'none)
495 (insert text))
496 (t
497 (let ((font-start (point)))
498 (when (and (string-match "\\`[ \t\n\r ]" text)
499 (not (bolp))
500 (not (eq (char-after (1- (point))) ? )))
501 (insert " "))
502 (let ((start (point))
503 (bolp (bolp)))
504 (insert text)
505 (save-restriction
506 (narrow-to-region start (point))
507 (goto-char start)
508 (when (looking-at "[ \t\n\r ]+")
509 (replace-match "" t t))
510 (while (re-search-forward "[ \t\n\r ]+" nil t)
511 (replace-match " " t t))
512 (goto-char (point-max)))
513 ;; We may have removed everything we inserted if if was just
514 ;; spaces.
515 (unless (= font-start (point))
516 ;; Mark all lines that should possibly be folded afterwards.
517 (when bolp
518 (shr-mark-fill start))
519 (when shr-use-fonts
520 (put-text-property font-start (point)
521 'face
522 (or shr-current-font 'variable-pitch)))))))))
523
524 (defun shr-fill-lines (start end)
525 (if (<= shr-internal-width 0)
526 nil
527 (save-restriction
528 (narrow-to-region start end)
529 (goto-char start)
530 (when (get-text-property (point) 'shr-indentation)
531 (shr-fill-line))
532 (while (setq start (next-single-property-change start 'shr-indentation))
533 (goto-char start)
534 (when (bolp)
535 (shr-fill-line)))
536 (goto-char (point-max)))))
537
538 (defun shr-vertical-motion (column)
539 (if (not shr-use-fonts)
540 (move-to-column column)
541 (unless (eolp)
542 (forward-char 1))
543 (vertical-motion (cons (/ column (frame-char-width)) 0))
544 (unless (eolp)
545 (forward-char 1))))
546
547 (defun shr-fill-line ()
548 (let ((shr-indentation (get-text-property (point) 'shr-indentation))
549 (continuation (get-text-property
550 (point) 'shr-continuation-indentation))
551 start)
552 (put-text-property (point) (1+ (point)) 'shr-indentation nil)
553 (shr-indent)
554 (setq start (point))
555 (setq shr-indentation (or continuation shr-indentation))
556 (shr-vertical-motion shr-internal-width)
557 (when (looking-at " $")
558 (delete-region (point) (line-end-position)))
559 (while (not (eolp))
560 ;; We have to do some folding. First find the first
561 ;; previous point suitable for folding.
562 (if (or (not (shr-find-fill-point (line-beginning-position)))
563 (= (point) start))
564 ;; We had unbreakable text (for this width), so just go to
565 ;; the first space and carry on.
566 (progn
567 (beginning-of-line)
568 (skip-chars-forward " ")
569 (search-forward " " (line-end-position) 'move)))
570 ;; Success; continue.
571 (when (= (preceding-char) ?\s)
572 (delete-char -1))
573 (insert "\n")
574 (shr-indent)
575 (setq start (point))
576 (shr-vertical-motion shr-internal-width)
577 (when (looking-at " $")
578 (delete-region (point) (line-end-position))))))
579
580 (defun shr-find-fill-point (start)
581 (let ((bp (point))
582 (end (point))
583 failed)
584 (while (not (or (setq failed (<= (point) start))
585 (eq (preceding-char) ? )
586 (eq (following-char) ? )
587 (shr-char-breakable-p (preceding-char))
588 (shr-char-breakable-p (following-char))
589 (and (shr-char-kinsoku-bol-p (preceding-char))
590 (shr-char-breakable-p (following-char))
591 (not (shr-char-kinsoku-bol-p (following-char))))
592 (shr-char-kinsoku-eol-p (following-char))
593 (bolp)))
594 (backward-char 1))
595 (if failed
596 ;; There's no breakable point, so we give it up.
597 (let (found)
598 (goto-char bp)
599 (unless shr-kinsoku-shorten
600 (while (setq found (re-search-forward
601 "\\(\\c>\\)\\| \\|\\c<\\|\\c|"
602 (line-end-position) 'move)))
603 (if (and found
604 (not (match-beginning 1)))
605 (goto-char (match-beginning 0)))))
606 (or
607 (eolp)
608 ;; Don't put kinsoku-bol characters at the beginning of a line,
609 ;; or kinsoku-eol characters at the end of a line.
610 (cond
611 (shr-kinsoku-shorten
612 (while (and (not (memq (preceding-char) (list ?\C-@ ?\n ? )))
613 (shr-char-kinsoku-eol-p (preceding-char)))
614 (backward-char 1))
615 (when (setq failed (<= (point) start))
616 ;; There's no breakable point that doesn't violate kinsoku,
617 ;; so we look for the second best position.
618 (while (and (progn
619 (forward-char 1)
620 (<= (point) end))
621 (progn
622 (setq bp (point))
623 (shr-char-kinsoku-eol-p (following-char)))))
624 (goto-char bp)))
625 ((shr-char-kinsoku-eol-p (preceding-char))
626 ;; Find backward the point where kinsoku-eol characters begin.
627 (let ((count 4))
628 (while
629 (progn
630 (backward-char 1)
631 (and (> (setq count (1- count)) 0)
632 (not (memq (preceding-char) (list ?\C-@ ?\n ? )))
633 (or (shr-char-kinsoku-eol-p (preceding-char))
634 (shr-char-kinsoku-bol-p (following-char)))))))
635 (when (setq failed (<= (point) start))
636 ;; There's no breakable point that doesn't violate kinsoku,
637 ;; so we go to the second best position.
638 (if (looking-at "\\(\\c<+\\)\\c<")
639 (goto-char (match-end 1))
640 (forward-char 1))))
641 ((shr-char-kinsoku-bol-p (following-char))
642 ;; Find forward the point where kinsoku-bol characters end.
643 (let ((count 4))
644 (while (progn
645 (forward-char 1)
646 (and (>= (setq count (1- count)) 0)
647 (shr-char-kinsoku-bol-p (following-char))
648 (shr-char-breakable-p (following-char))))))))
649 (when (eq (following-char) ? )
650 (forward-char 1))))
651 (not failed)))
652
653 (defun shr-parse-base (url)
654 ;; Always chop off anchors.
655 (when (string-match "#.*" url)
656 (setq url (substring url 0 (match-beginning 0))))
657 ;; NB: <base href="" > URI may itself be relative to the document s URI
658 (setq url (shr-expand-url url))
659 (let* ((parsed (url-generic-parse-url url))
660 (local (url-filename parsed)))
661 (setf (url-filename parsed) "")
662 ;; Chop off the bit after the last slash.
663 (when (string-match "\\`\\(.*/\\)[^/]+\\'" local)
664 (setq local (match-string 1 local)))
665 ;; Always make the local bit end with a slash.
666 (when (and (not (zerop (length local)))
667 (not (eq (aref local (1- (length local))) ?/)))
668 (setq local (concat local "/")))
669 (list (url-recreate-url parsed)
670 local
671 (url-type parsed)
672 url)))
673
674 (autoload 'url-expand-file-name "url-expand")
675
676 ;; FIXME This needs some tests writing.
677 ;; Does it even need to exist, given that url-expand-file-name does?
678 (defun shr-expand-url (url &optional base)
679 (setq base
680 (if base
681 ;; shr-parse-base should never call this with non-nil base!
682 (shr-parse-base base)
683 ;; Bound by the parser.
684 shr-base))
685 (when (zerop (length url))
686 (setq url nil))
687 (cond ((or (not url)
688 (not base)
689 (string-match "\\`[a-z]*:" url))
690 ;; Absolute or empty URI
691 (or url (nth 3 base)))
692 ((eq (aref url 0) ?/)
693 (if (and (> (length url) 1)
694 (eq (aref url 1) ?/))
695 ;; //host...; just use the protocol
696 (concat (nth 2 base) ":" url)
697 ;; Just use the host name part.
698 (concat (car base) url)))
699 ((eq (aref url 0) ?#)
700 ;; A link to an anchor.
701 (concat (nth 3 base) url))
702 (t
703 ;; Totally relative.
704 (url-expand-file-name url (concat (car base) (cadr base))))))
705
706 (defun shr-ensure-newline ()
707 (unless (zerop (current-column))
708 (insert "\n")))
709
710 (defun shr-ensure-paragraph ()
711 (unless (bobp)
712 (if (<= (current-column) shr-indentation)
713 (unless (save-excursion
714 (forward-line -1)
715 (looking-at " *$"))
716 (insert "\n"))
717 (if (save-excursion
718 (beginning-of-line)
719 ;; If the current line is totally blank, and doesn't even
720 ;; have any face properties set, then delete the blank
721 ;; space.
722 (and (looking-at " *$")
723 (not (get-text-property (point) 'face))
724 (not (= (next-single-property-change (point) 'face nil
725 (line-end-position))
726 (line-end-position)))))
727 (delete-region (match-beginning 0) (match-end 0))
728 (insert "\n\n")))))
729
730 (defun shr-indent ()
731 (when (> shr-indentation 0)
732 (insert
733 (if (not shr-use-fonts)
734 (make-string shr-indentation ?\s)
735 (propertize " "
736 'display
737 `(space :width (,shr-indentation)))))))
738
739 (defun shr-fontize-dom (dom &rest types)
740 (let ((start (point)))
741 (shr-generic dom)
742 (dolist (type types)
743 (shr-add-font start (point) type))))
744
745 ;; Add face to the region, but avoid putting the font properties on
746 ;; blank text at the start of the line, and the newline at the end, to
747 ;; avoid ugliness.
748 (defun shr-add-font (start end type)
749 (unless shr-inhibit-decoration
750 (save-excursion
751 (goto-char start)
752 (while (< (point) end)
753 (when (bolp)
754 (skip-chars-forward " "))
755 (add-face-text-property (point) (min (line-end-position) end) type t)
756 (if (< (line-end-position) end)
757 (forward-line 1)
758 (goto-char end))))))
759
760 (defun shr-mouse-browse-url (ev)
761 "Browse the URL under the mouse cursor."
762 (interactive "e")
763 (mouse-set-point ev)
764 (shr-browse-url))
765
766 (defun shr-browse-url (&optional external mouse-event)
767 "Browse the URL under point.
768 If EXTERNAL, browse the URL using `shr-external-browser'."
769 (interactive (list current-prefix-arg last-nonmenu-event))
770 (mouse-set-point mouse-event)
771 (let ((url (get-text-property (point) 'shr-url)))
772 (cond
773 ((not url)
774 (message "No link under point"))
775 ((string-match "^mailto:" url)
776 (browse-url-mail url))
777 (t
778 (if external
779 (funcall shr-external-browser url)
780 (browse-url url))))))
781
782 (defun shr-save-contents (directory)
783 "Save the contents from URL in a file."
784 (interactive "DSave contents of URL to directory: ")
785 (let ((url (get-text-property (point) 'shr-url)))
786 (if (not url)
787 (message "No link under point")
788 (url-retrieve (shr-encode-url url)
789 'shr-store-contents (list url directory)
790 nil t))))
791
792 (defun shr-store-contents (status url directory)
793 (unless (plist-get status :error)
794 (when (or (search-forward "\n\n" nil t)
795 (search-forward "\r\n\r\n" nil t))
796 (write-region (point) (point-max)
797 (expand-file-name (file-name-nondirectory url)
798 directory)))))
799
800 (defun shr-image-fetched (status buffer start end &optional flags)
801 (let ((image-buffer (current-buffer)))
802 (when (and (buffer-name buffer)
803 (not (plist-get status :error)))
804 (url-store-in-cache image-buffer)
805 (when (or (search-forward "\n\n" nil t)
806 (search-forward "\r\n\r\n" nil t))
807 (let ((data (shr-parse-image-data)))
808 (with-current-buffer buffer
809 (save-excursion
810 (let ((alt (buffer-substring start end))
811 (properties (text-properties-at start))
812 (inhibit-read-only t))
813 (delete-region start end)
814 (goto-char start)
815 (funcall shr-put-image-function data alt flags)
816 (while properties
817 (let ((type (pop properties))
818 (value (pop properties)))
819 (unless (memq type '(display image-size))
820 (put-text-property start (point) type value))))))))))
821 (kill-buffer image-buffer)))
822
823 (defun shr-image-from-data (data)
824 "Return an image from the data: URI content DATA."
825 (when (string-match
826 "\\(\\([^/;,]+\\(/[^;,]+\\)?\\)\\(;[^;,]+\\)*\\)?,\\(.*\\)"
827 data)
828 (let ((param (match-string 4 data))
829 (payload (url-unhex-string (match-string 5 data))))
830 (when (string-match "^.*\\(;[ \t]*base64\\)$" param)
831 (setq payload (base64-decode-string payload)))
832 payload)))
833
834 ;; Behind display-graphic-p test.
835 (declare-function image-size "image.c" (spec &optional pixels frame))
836 (declare-function image-animate "image" (image &optional index limit))
837
838 (defun shr-put-image (spec alt &optional flags)
839 "Insert image SPEC with a string ALT. Return image.
840 SPEC is either an image data blob, or a list where the first
841 element is the data blob and the second element is the content-type."
842 (if (display-graphic-p)
843 (let* ((size (cdr (assq 'size flags)))
844 (data (if (consp spec)
845 (car spec)
846 spec))
847 (content-type (and (consp spec)
848 (cadr spec)))
849 (start (point))
850 (image (cond
851 ((eq size 'original)
852 (create-image data nil t :ascent 100
853 :format content-type))
854 ((eq content-type 'image/svg+xml)
855 (create-image data 'svg t :ascent 100))
856 ((eq size 'full)
857 (ignore-errors
858 (shr-rescale-image data content-type)))
859 (t
860 (ignore-errors
861 (shr-rescale-image data content-type))))))
862 (when image
863 ;; When inserting big-ish pictures, put them at the
864 ;; beginning of the line.
865 (when (and (> (current-column) 0)
866 (> (car (image-size image t)) 400))
867 (insert "\n"))
868 (if (eq size 'original)
869 (insert-sliced-image image (or alt "*") nil 20 1)
870 (insert-image image (or alt "*")))
871 (put-text-property start (point) 'image-size size)
872 (when (and shr-image-animate
873 (cond ((fboundp 'image-multi-frame-p)
874 ;; Only animate multi-frame things that specify a
875 ;; delay; eg animated gifs as opposed to
876 ;; multi-page tiffs. FIXME?
877 (cdr (image-multi-frame-p image)))
878 ((fboundp 'image-animated-p)
879 (image-animated-p image))))
880 (image-animate image nil 60)))
881 image)
882 (insert alt)))
883
884 (defun shr-rescale-image (data &optional content-type)
885 "Rescale DATA, if too big, to fit the current buffer."
886 (if (not (and (fboundp 'imagemagick-types)
887 (get-buffer-window (current-buffer))))
888 (create-image data nil t :ascent 100)
889 (let ((edges (window-inside-pixel-edges
890 (get-buffer-window (current-buffer)))))
891 (create-image
892 data 'imagemagick t
893 :ascent 100
894 :max-width (truncate (* shr-max-image-proportion
895 (- (nth 2 edges) (nth 0 edges))))
896 :max-height (truncate (* shr-max-image-proportion
897 (- (nth 3 edges) (nth 1 edges))))
898 :format content-type))))
899
900 ;; url-cache-extract autoloads url-cache.
901 (declare-function url-cache-create-filename "url-cache" (url))
902 (autoload 'mm-disable-multibyte "mm-util")
903 (autoload 'browse-url-mail "browse-url")
904
905 (defun shr-get-image-data (url)
906 "Get image data for URL.
907 Return a string with image data."
908 (with-temp-buffer
909 (mm-disable-multibyte)
910 (when (ignore-errors
911 (url-cache-extract (url-cache-create-filename (shr-encode-url url)))
912 t)
913 (when (or (search-forward "\n\n" nil t)
914 (search-forward "\r\n\r\n" nil t))
915 (shr-parse-image-data)))))
916
917 (defun shr-parse-image-data ()
918 (let ((data (buffer-substring (point) (point-max)))
919 (content-type
920 (save-excursion
921 (save-restriction
922 (narrow-to-region (point-min) (point))
923 (let ((content-type (mail-fetch-field "content-type")))
924 (and content-type
925 ;; Remove any comments in the type string.
926 (intern (replace-regexp-in-string ";.*" "" content-type)
927 obarray)))))))
928 ;; SVG images may contain references to further images that we may
929 ;; want to block. So special-case these by parsing the XML data
930 ;; and remove the blocked bits.
931 (when (eq content-type 'image/svg+xml)
932 (setq data
933 (shr-dom-to-xml
934 (libxml-parse-xml-region (point) (point-max)))))
935 (list data content-type)))
936
937 (defun shr-image-displayer (content-function)
938 "Return a function to display an image.
939 CONTENT-FUNCTION is a function to retrieve an image for a cid url that
940 is an argument. The function to be returned takes three arguments URL,
941 START, and END. Note that START and END should be markers."
942 `(lambda (url start end)
943 (when url
944 (if (string-match "\\`cid:" url)
945 ,(when content-function
946 `(let ((image (funcall ,content-function
947 (substring url (match-end 0)))))
948 (when image
949 (goto-char start)
950 (funcall shr-put-image-function
951 image (buffer-substring start end))
952 (delete-region (point) end))))
953 (url-retrieve url 'shr-image-fetched
954 (list (current-buffer) start end)
955 t t)))))
956
957 (defun shr-heading (dom &rest types)
958 (shr-ensure-paragraph)
959 (apply #'shr-fontize-dom dom types)
960 (shr-ensure-paragraph))
961
962 (defun shr-urlify (start url &optional title)
963 (shr-add-font start (point) 'shr-link)
964 (add-text-properties
965 start (point)
966 (list 'shr-url url
967 'help-echo (let ((iri (or (ignore-errors
968 (decode-coding-string
969 (url-unhex-string url)
970 'utf-8 t))
971 url)))
972 (if title (format "%s (%s)" iri title) iri))
973 'follow-link t
974 'mouse-face 'highlight
975 'keymap shr-map)))
976
977 (defun shr-encode-url (url)
978 "Encode URL."
979 (browse-url-url-encode-chars url "[)$ ]"))
980
981 (autoload 'shr-color-visible "shr-color")
982 (autoload 'shr-color->hexadecimal "shr-color")
983
984 (defun shr-color-check (fg bg)
985 "Check that FG is visible on BG.
986 Returns (fg bg) with corrected values.
987 Returns nil if the colors that would be used are the default
988 ones, in case fg and bg are nil."
989 (when (or fg bg)
990 (let ((fixed (cond ((null fg) 'fg)
991 ((null bg) 'bg))))
992 ;; Convert colors to hexadecimal, or set them to default.
993 (let ((fg (or (shr-color->hexadecimal fg)
994 (frame-parameter nil 'foreground-color)))
995 (bg (or (shr-color->hexadecimal bg)
996 (frame-parameter nil 'background-color))))
997 (cond ((eq fixed 'bg)
998 ;; Only return the new fg
999 (list nil (cadr (shr-color-visible bg fg t))))
1000 ((eq fixed 'fg)
1001 ;; Invert args and results and return only the new bg
1002 (list (cadr (shr-color-visible fg bg t)) nil))
1003 (t
1004 (shr-color-visible bg fg)))))))
1005
1006 (defun shr-colorize-region (start end fg &optional bg)
1007 (when (and (not shr-inhibit-decoration)
1008 (or fg bg))
1009 (let ((new-colors (shr-color-check fg bg)))
1010 (when new-colors
1011 (when fg
1012 (add-face-text-property start end
1013 (list :foreground (cadr new-colors))
1014 t))
1015 (when bg
1016 (add-face-text-property start end
1017 (list :background (car new-colors))
1018 t)))
1019 new-colors)))
1020
1021 (defun shr-previous-newline-padding-width (width)
1022 (let ((overlays (overlays-at (point)))
1023 (previous-width 0))
1024 (if (null overlays)
1025 width
1026 (dolist (overlay overlays)
1027 (setq previous-width
1028 (+ previous-width
1029 (length (plist-get (overlay-properties overlay)
1030 'before-string)))))
1031 (+ width previous-width))))
1032
1033 ;;; Tag-specific rendering rules.
1034
1035 (defun shr-tag-body (dom)
1036 (let* ((start (point))
1037 (fgcolor (or (dom-attr dom 'fgcolor) (dom-attr dom 'text)))
1038 (bgcolor (dom-attr dom 'bgcolor))
1039 (shr-stylesheet (list (cons 'color fgcolor)
1040 (cons 'background-color bgcolor))))
1041 (shr-generic dom)
1042 (shr-colorize-region start (point) fgcolor bgcolor)))
1043
1044 (defun shr-tag-style (_dom)
1045 )
1046
1047 (defun shr-tag-script (_dom)
1048 )
1049
1050 (defun shr-tag-comment (_dom)
1051 )
1052
1053 (defun shr-dom-to-xml (dom)
1054 (with-temp-buffer
1055 (shr-dom-print dom)
1056 (buffer-string)))
1057
1058 (defun shr-dom-print (dom)
1059 "Convert DOM into a string containing the xml representation."
1060 (insert (format "<%s" (dom-tag dom)))
1061 (dolist (attr (dom-attributes dom))
1062 ;; Ignore attributes that start with a colon because they are
1063 ;; private elements.
1064 (unless (= (aref (format "%s" (car attr)) 0) ?:)
1065 (insert (format " %s=\"%s\"" (car attr) (cdr attr)))))
1066 (insert ">")
1067 (let (url)
1068 (dolist (elem (dom-children dom))
1069 (cond
1070 ((stringp elem)
1071 (insert elem))
1072 ((eq (dom-tag elem) 'comment)
1073 )
1074 ((or (not (eq (dom-tag elem) 'image))
1075 ;; Filter out blocked elements inside the SVG image.
1076 (not (setq url (dom-attr elem ':xlink:href)))
1077 (not shr-blocked-images)
1078 (not (string-match shr-blocked-images url)))
1079 (insert " ")
1080 (shr-dom-print elem)))))
1081 (insert (format "</%s>" (dom-tag dom))))
1082
1083 (defun shr-tag-svg (dom)
1084 (when (and (image-type-available-p 'svg)
1085 (not shr-inhibit-images))
1086 (funcall shr-put-image-function (list (shr-dom-to-xml dom) 'image/svg+xml)
1087 "SVG Image")))
1088
1089 (defun shr-tag-sup (dom)
1090 (let ((start (point)))
1091 (shr-generic dom)
1092 (put-text-property start (point) 'display '(raise 0.5))))
1093
1094 (defun shr-tag-sub (dom)
1095 (let ((start (point)))
1096 (shr-generic dom)
1097 (put-text-property start (point) 'display '(raise -0.5))))
1098
1099 (defun shr-tag-label (dom)
1100 (shr-generic dom)
1101 (shr-ensure-paragraph))
1102
1103 (defun shr-tag-p (dom)
1104 (shr-ensure-paragraph)
1105 (shr-generic dom)
1106 (shr-ensure-paragraph))
1107
1108 (defun shr-tag-div (dom)
1109 (shr-ensure-newline)
1110 (shr-generic dom)
1111 (shr-ensure-newline))
1112
1113 (defun shr-tag-s (dom)
1114 (shr-fontize-dom dom 'shr-strike-through))
1115
1116 (defun shr-tag-del (dom)
1117 (shr-fontize-dom dom 'shr-strike-through))
1118
1119 (defun shr-tag-b (dom)
1120 (shr-fontize-dom dom 'bold))
1121
1122 (defun shr-tag-i (dom)
1123 (shr-fontize-dom dom 'italic))
1124
1125 (defun shr-tag-em (dom)
1126 (shr-fontize-dom dom 'italic))
1127
1128 (defun shr-tag-strong (dom)
1129 (shr-fontize-dom dom 'bold))
1130
1131 (defun shr-tag-u (dom)
1132 (shr-fontize-dom dom 'underline))
1133
1134 (defun shr-tag-tt (dom)
1135 (let ((shr-current-font 'default))
1136 (shr-generic dom)))
1137
1138 (defun shr-parse-style (style)
1139 (when style
1140 (save-match-data
1141 (when (string-match "\n" style)
1142 (setq style (replace-match " " t t style))))
1143 (let ((plist nil))
1144 (dolist (elem (split-string style ";"))
1145 (when elem
1146 (setq elem (split-string elem ":"))
1147 (when (and (car elem)
1148 (cadr elem))
1149 (let ((name (replace-regexp-in-string "^ +\\| +$" "" (car elem)))
1150 (value (replace-regexp-in-string "^ +\\| +$" "" (cadr elem))))
1151 (when (string-match " *!important\\'" value)
1152 (setq value (substring value 0 (match-beginning 0))))
1153 (unless (equal value "inherit")
1154 (push (cons (intern name obarray)
1155 value)
1156 plist))))))
1157 plist)))
1158
1159 (defun shr-tag-base (dom)
1160 (when-let (base (dom-attr dom 'href))
1161 (setq shr-base (shr-parse-base base)))
1162 (shr-generic dom))
1163
1164 (defun shr-tag-a (dom)
1165 (let ((url (dom-attr dom 'href))
1166 (title (dom-attr dom 'title))
1167 (start (point))
1168 shr-start)
1169 (shr-generic dom)
1170 (when (and shr-target-id
1171 (equal (dom-attr dom 'name) shr-target-id))
1172 ;; We have a zero-length <a name="foo"> element, so just
1173 ;; insert... something.
1174 (when (= start (point))
1175 (shr-ensure-newline)
1176 (insert " "))
1177 (put-text-property start (1+ start) 'shr-target-id shr-target-id))
1178 (when (and url
1179 (not shr-inhibit-decoration))
1180 (shr-urlify (or shr-start start) (shr-expand-url url) title))))
1181
1182 (defun shr-tag-object (dom)
1183 (unless shr-inhibit-images
1184 (let ((start (point))
1185 url multimedia image)
1186 (when-let (type (dom-attr dom 'type))
1187 (when (string-match "\\`image/svg" type)
1188 (setq url (dom-attr dom 'data)
1189 image t)))
1190 (dolist (child (dom-non-text-children dom))
1191 (cond
1192 ((eq (dom-tag child) 'embed)
1193 (setq url (or url (dom-attr child 'src))
1194 multimedia t))
1195 ((and (eq (dom-tag child) 'param)
1196 (equal (dom-attr child 'name) "movie"))
1197 (setq url (or url (dom-attr child 'value))
1198 multimedia t))))
1199 (when url
1200 (cond
1201 (image
1202 (shr-tag-img dom url)
1203 (setq dom nil))
1204 (multimedia
1205 (shr-insert " [multimedia] ")
1206 (shr-urlify start (shr-expand-url url)))))
1207 (when dom
1208 (shr-generic dom)))))
1209
1210 (defcustom shr-prefer-media-type-alist '(("webm" . 1.0)
1211 ("ogv" . 1.0)
1212 ("ogg" . 1.0)
1213 ("opus" . 1.0)
1214 ("flac" . 0.9)
1215 ("wav" . 0.5))
1216 "Preferences for media types.
1217 The key element should be a regexp matched against the type of the source or
1218 url if no type is specified. The value should be a float in the range 0.0 to
1219 1.0. Media elements with higher value are preferred."
1220 :version "24.4"
1221 :group 'shr
1222 :type '(alist :key-type regexp :value-type float))
1223
1224 (defun shr--get-media-pref (elem)
1225 "Determine the preference for ELEM.
1226 The preference is a float determined from `shr-prefer-media-type'."
1227 (let ((type (dom-attr elem 'type))
1228 (p 0.0))
1229 (unless type
1230 (setq type (dom-attr elem 'src)))
1231 (when type
1232 (dolist (pref shr-prefer-media-type-alist)
1233 (when (and
1234 (> (cdr pref) p)
1235 (string-match-p (car pref) type))
1236 (setq p (cdr pref)))))
1237 p))
1238
1239 (defun shr--extract-best-source (dom &optional url pref)
1240 "Extract the best `:src' property from <source> blocks in DOM."
1241 (setq pref (or pref -1.0))
1242 (let (new-pref)
1243 (dolist (elem (dom-non-text-children dom))
1244 (when (and (eq (dom-tag elem) 'source)
1245 (< pref
1246 (setq new-pref
1247 (shr--get-media-pref elem))))
1248 (setq pref new-pref
1249 url (dom-attr elem 'src))
1250 ;; libxml's html parser isn't HTML5 compliant and non terminated
1251 ;; source tags might end up as children. So recursion it is...
1252 (dolist (child (dom-non-text-children elem))
1253 (when (eq (dom-tag child) 'source)
1254 (let ((ret (shr--extract-best-source (list child) url pref)))
1255 (when (< pref (cdr ret))
1256 (setq url (car ret)
1257 pref (cdr ret)))))))))
1258 (cons url pref))
1259
1260 (defun shr-tag-video (dom)
1261 (let ((image (dom-attr dom 'poster))
1262 (url (dom-attr dom 'src))
1263 (start (point)))
1264 (unless url
1265 (setq url (car (shr--extract-best-source dom))))
1266 (if image
1267 (shr-tag-img nil image)
1268 (shr-insert " [video] "))
1269 (shr-urlify start (shr-expand-url url))))
1270
1271 (defun shr-tag-audio (dom)
1272 (let ((url (dom-attr dom 'src))
1273 (start (point)))
1274 (unless url
1275 (setq url (car (shr--extract-best-source dom))))
1276 (shr-insert " [audio] ")
1277 (shr-urlify start (shr-expand-url url))))
1278
1279 (defun shr-tag-img (dom &optional url)
1280 (when (or url
1281 (and dom
1282 (> (length (dom-attr dom 'src)) 0)))
1283 (when (> (current-column) 0)
1284 (insert "\n"))
1285 (let ((alt (dom-attr dom 'alt))
1286 (url (shr-expand-url (or url (dom-attr dom 'src)))))
1287 (let ((start (point-marker)))
1288 (when (zerop (length alt))
1289 (setq alt "*"))
1290 (cond
1291 ((or (member (dom-attr dom 'height) '("0" "1"))
1292 (member (dom-attr dom 'width) '("0" "1")))
1293 ;; Ignore zero-sized or single-pixel images.
1294 )
1295 ((and (not shr-inhibit-images)
1296 (string-match "\\`data:" url))
1297 (let ((image (shr-image-from-data (substring url (match-end 0)))))
1298 (if image
1299 (funcall shr-put-image-function image alt)
1300 (insert alt))))
1301 ((and (not shr-inhibit-images)
1302 (string-match "\\`cid:" url))
1303 (let ((url (substring url (match-end 0)))
1304 image)
1305 (if (or (not shr-content-function)
1306 (not (setq image (funcall shr-content-function url))))
1307 (insert alt)
1308 (funcall shr-put-image-function image alt))))
1309 ((or shr-inhibit-images
1310 (and shr-blocked-images
1311 (string-match shr-blocked-images url)))
1312 (setq shr-start (point))
1313 (if (> (string-width alt) 8)
1314 (shr-insert (truncate-string-to-width alt 8))
1315 (shr-insert alt)))
1316 ((and (not shr-ignore-cache)
1317 (url-is-cached (shr-encode-url url)))
1318 (funcall shr-put-image-function (shr-get-image-data url) alt))
1319 (t
1320 (insert alt " ")
1321 (when (and shr-ignore-cache
1322 (url-is-cached (shr-encode-url url)))
1323 (let ((file (url-cache-create-filename (shr-encode-url url))))
1324 (when (file-exists-p file)
1325 (delete-file file))))
1326 (url-queue-retrieve
1327 (shr-encode-url url) 'shr-image-fetched
1328 (list (current-buffer) start (set-marker (make-marker) (1- (point))))
1329 t t)))
1330 (when (zerop shr-table-depth) ;; We are not in a table.
1331 (put-text-property start (point) 'keymap shr-map)
1332 (put-text-property start (point) 'shr-alt alt)
1333 (put-text-property start (point) 'image-url url)
1334 (put-text-property start (point) 'image-displayer
1335 (shr-image-displayer shr-content-function))
1336 (put-text-property start (point) 'help-echo
1337 (shr-fill-text
1338 (or (dom-attr dom 'title) alt))))))))
1339
1340 (defun shr-tag-pre (dom)
1341 (let ((shr-folding-mode 'none)
1342 (shr-current-font 'default))
1343 (shr-ensure-newline)
1344 (shr-generic dom)
1345 (shr-ensure-newline)))
1346
1347 (defun shr-tag-blockquote (dom)
1348 (shr-ensure-paragraph)
1349 (let ((start (point))
1350 (shr-indentation (+ shr-indentation
1351 (* 4 shr-table-separator-pixel-width))))
1352 (shr-generic dom)
1353 (shr-ensure-paragraph)
1354 (shr-mark-fill start)))
1355
1356 (defun shr-tag-dl (dom)
1357 (shr-ensure-paragraph)
1358 (shr-generic dom)
1359 (shr-ensure-paragraph))
1360
1361 (defun shr-tag-dt (dom)
1362 (shr-ensure-newline)
1363 (shr-generic dom)
1364 (shr-ensure-newline))
1365
1366 (defun shr-tag-dd (dom)
1367 (shr-ensure-newline)
1368 (let ((shr-indentation (+ shr-indentation
1369 (* 4 shr-table-separator-pixel-width))))
1370 (shr-generic dom)))
1371
1372 (defun shr-tag-ul (dom)
1373 (shr-ensure-paragraph)
1374 (let ((shr-list-mode 'ul))
1375 (shr-generic dom))
1376 (shr-ensure-paragraph))
1377
1378 (defun shr-tag-ol (dom)
1379 (shr-ensure-paragraph)
1380 (let ((shr-list-mode 1))
1381 (shr-generic dom))
1382 (shr-ensure-paragraph))
1383
1384 (defun shr-tag-li (dom)
1385 (shr-ensure-newline)
1386 (let ((start (point)))
1387 (let* ((bullet
1388 (if (numberp shr-list-mode)
1389 (prog1
1390 (format "%d " shr-list-mode)
1391 (setq shr-list-mode (1+ shr-list-mode)))
1392 shr-bullet)))
1393 (insert bullet)
1394 (shr-mark-fill start)
1395 (let ((shr-indentation (+ shr-indentation
1396 (shr-string-pixel-width bullet))))
1397 (put-text-property start (1+ start)
1398 'shr-continuation-indentation shr-indentation)
1399 (shr-generic dom)))))
1400
1401 (defun shr-mark-fill (start)
1402 ;; We may not have inserted any text to fill.
1403 (unless (= start (point))
1404 (put-text-property start (1+ start)
1405 'shr-indentation shr-indentation)))
1406
1407 (defun shr-tag-br (dom)
1408 (when (and (not (bobp))
1409 ;; Only add a newline if we break the current line, or
1410 ;; the previous line isn't a blank line.
1411 (or (not (bolp))
1412 (and (> (- (point) 2) (point-min))
1413 (not (= (char-after (- (point) 2)) ?\n)))))
1414 (insert "\n"))
1415 (shr-generic dom))
1416
1417 (defun shr-tag-span (dom)
1418 (shr-generic dom))
1419
1420 (defun shr-tag-h1 (dom)
1421 (shr-heading dom (if shr-use-fonts
1422 '(variable-pitch (:height 1.3 :weight bold))
1423 'bold)))
1424
1425 (defun shr-tag-h2 (dom)
1426 (shr-heading dom 'bold))
1427
1428 (defun shr-tag-h3 (dom)
1429 (shr-heading dom 'italic))
1430
1431 (defun shr-tag-h4 (dom)
1432 (shr-heading dom))
1433
1434 (defun shr-tag-h5 (dom)
1435 (shr-heading dom))
1436
1437 (defun shr-tag-h6 (dom)
1438 (shr-heading dom))
1439
1440 (defun shr-tag-hr (_dom)
1441 (shr-ensure-newline)
1442 (insert (make-string (if (not shr-use-fonts)
1443 shr-internal-width
1444 (1+ (/ shr-internal-width
1445 shr-table-separator-pixel-width)))
1446 shr-hr-line)
1447 "\n"))
1448
1449 (defun shr-tag-title (dom)
1450 (shr-heading dom 'bold 'underline))
1451
1452 (defun shr-tag-font (dom)
1453 (let* ((start (point))
1454 (color (dom-attr dom 'color))
1455 (shr-stylesheet (nconc (list (cons 'color color))
1456 shr-stylesheet)))
1457 (shr-generic dom)
1458 (when color
1459 (shr-colorize-region start (point) color
1460 (cdr (assq 'background-color shr-stylesheet))))))
1461
1462 ;;; Table rendering algorithm.
1463
1464 ;; Table rendering is the only complicated thing here. We do this by
1465 ;; first counting how many TDs there are in each TR, and registering
1466 ;; how wide they think they should be ("width=45%", etc). Then we
1467 ;; render each TD separately (this is done in temporary buffers, so
1468 ;; that we can use all the rendering machinery as if we were in the
1469 ;; main buffer). Now we know how much space each TD really takes, so
1470 ;; we then render everything again with the new widths, and finally
1471 ;; insert all these boxes into the main buffer.
1472 (defun shr-tag-table-1 (dom)
1473 (setq dom (or (dom-child-by-tag dom 'tbody) dom))
1474 (let* ((shr-inhibit-images t)
1475 (shr-table-depth (1+ shr-table-depth))
1476 (shr-kinsoku-shorten t)
1477 ;; Find all suggested widths.
1478 (columns (shr-column-specs dom))
1479 ;; Compute how many pixels wide each TD should be.
1480 (suggested-widths (shr-pro-rate-columns columns))
1481 ;; Do a "test rendering" to see how big each TD is (this can
1482 ;; be smaller (if there's little text) or bigger (if there's
1483 ;; unbreakable text).
1484 (elems (or (dom-attr dom 'shr-suggested-widths)
1485 (shr-make-table dom suggested-widths nil
1486 'shr-suggested-widths)))
1487 (sketch (loop for line in elems
1488 collect (mapcar #'car line)))
1489 (natural (loop for line in elems
1490 collect (mapcar #'cdr line)))
1491 (sketch-widths (shr-table-widths sketch natural suggested-widths)))
1492 ;; This probably won't work very well.
1493 (when (> (+ (loop for width across sketch-widths
1494 summing (1+ width))
1495 shr-indentation shr-table-separator-pixel-width)
1496 (frame-width))
1497 (setq truncate-lines t))
1498 ;; Then render the table again with these new "hard" widths.
1499 (shr-insert-table (shr-make-table dom sketch-widths t) sketch-widths)))
1500
1501 (defun shr-tag-table (dom)
1502 (shr-ensure-paragraph)
1503 (let* ((caption (dom-children (dom-child-by-tag dom 'caption)))
1504 (header (dom-non-text-children (dom-child-by-tag dom 'thead)))
1505 (body (dom-non-text-children (or (dom-child-by-tag dom 'tbody)
1506 dom)))
1507 (footer (dom-non-text-children (dom-child-by-tag dom 'tfoot)))
1508 (bgcolor (dom-attr dom 'bgcolor))
1509 (start (point))
1510 (shr-stylesheet (nconc (list (cons 'background-color bgcolor))
1511 shr-stylesheet))
1512 (nheader (if header (shr-max-columns header)))
1513 (nbody (if body (shr-max-columns body)))
1514 (nfooter (if footer (shr-max-columns footer))))
1515 (if (and (not caption)
1516 (not header)
1517 (not (dom-child-by-tag dom 'tbody))
1518 (not (dom-child-by-tag dom 'tr))
1519 (not footer))
1520 ;; The table is totally invalid and just contains random junk.
1521 ;; Try to output it anyway.
1522 (shr-generic dom)
1523 ;; It's a real table, so render it.
1524 (if (dom-attr dom 'shr-fixed-table)
1525 (shr-tag-table-1 dom)
1526 ;; Only fix up the table once.
1527 (let ((table
1528 (nconc
1529 (list 'table nil)
1530 (if caption `((tr nil (td nil ,@caption))))
1531 (cond
1532 (header
1533 (if footer
1534 ;; header + body + footer
1535 (if (= nheader nbody)
1536 (if (= nbody nfooter)
1537 `((tr nil (td nil (table nil
1538 (tbody nil ,@header
1539 ,@body ,@footer)))))
1540 (nconc `((tr nil (td nil (table nil
1541 (tbody nil ,@header
1542 ,@body)))))
1543 (if (= nfooter 1)
1544 footer
1545 `((tr nil (td nil (table
1546 nil (tbody
1547 nil ,@footer))))))))
1548 (nconc `((tr nil (td nil (table nil (tbody
1549 nil ,@header)))))
1550 (if (= nbody nfooter)
1551 `((tr nil (td nil (table
1552 nil (tbody nil ,@body
1553 ,@footer)))))
1554 (nconc `((tr nil (td nil (table
1555 nil (tbody nil
1556 ,@body)))))
1557 (if (= nfooter 1)
1558 footer
1559 `((tr nil (td nil (table
1560 nil
1561 (tbody
1562 nil
1563 ,@footer))))))))))
1564 ;; header + body
1565 (if (= nheader nbody)
1566 `((tr nil (td nil (table nil (tbody nil ,@header
1567 ,@body)))))
1568 (if (= nheader 1)
1569 `(,@header (tr nil (td nil (table
1570 nil (tbody nil ,@body)))))
1571 `((tr nil (td nil (table nil (tbody nil ,@header))))
1572 (tr nil (td nil (table nil (tbody nil ,@body)))))))))
1573 (footer
1574 ;; body + footer
1575 (if (= nbody nfooter)
1576 `((tr nil (td nil (table
1577 nil (tbody nil ,@body ,@footer)))))
1578 (nconc `((tr nil (td nil (table nil (tbody nil ,@body)))))
1579 (if (= nfooter 1)
1580 footer
1581 `((tr nil (td nil (table
1582 nil (tbody nil ,@footer)))))))))
1583 (caption
1584 `((tr nil (td nil (table nil (tbody nil ,@body))))))
1585 (body)))))
1586 (dom-set-attribute table 'shr-fixed-table t)
1587 (setcdr dom (cdr table))
1588 (shr-tag-table-1 dom))))
1589 (when bgcolor
1590 (shr-colorize-region start (point) (cdr (assq 'color shr-stylesheet))
1591 bgcolor))
1592 ;; Finally, insert all the images after the table. The Emacs buffer
1593 ;; model isn't strong enough to allow us to put the images actually
1594 ;; into the tables.
1595 (when (zerop shr-table-depth)
1596 (save-excursion
1597 (shr-expand-alignments start (point)))
1598 (dolist (elem (dom-by-tag dom 'object))
1599 (shr-tag-object elem))
1600 (dolist (elem (dom-by-tag dom 'img))
1601 (shr-tag-img elem)))))
1602
1603 (defun shr-insert-table (table widths)
1604 (let* ((collapse (equal (cdr (assq 'border-collapse shr-stylesheet))
1605 "collapse"))
1606 (shr-table-separator-length (if collapse 0 1))
1607 (shr-table-vertical-line (if collapse "" shr-table-vertical-line))
1608 (start (point)))
1609 (setq shr-table-id (1+ shr-table-id))
1610 (unless collapse
1611 (shr-insert-table-ruler widths))
1612 (dolist (row table)
1613 (let ((start (point))
1614 (align 0)
1615 (column-number 0)
1616 (height (let ((max 0))
1617 (dolist (column row)
1618 (setq max (max max (nth 2 column))))
1619 max)))
1620 (dotimes (i (max height 1))
1621 (shr-indent)
1622 (insert shr-table-vertical-line "\n"))
1623 (dolist (column row)
1624 (when (> (nth 2 column) -1)
1625 (goto-char start)
1626 ;; Sum up all the widths from the column. (There may be
1627 ;; more than one if this is a "colspan" column.)
1628 (dotimes (i (nth 4 column))
1629 ;; The colspan directive may be wrong and there may not be
1630 ;; that number of columns.
1631 (when (<= column-number (1- (length widths)))
1632 (setq align (+ align
1633 (aref widths column-number)
1634 (* 2 shr-table-separator-pixel-width))))
1635 (setq column-number (1+ column-number)))
1636 (let ((lines (nth 3 column))
1637 (pixel-align (if (not shr-use-fonts)
1638 (* align (frame-char-width))
1639 align)))
1640 (dolist (line lines)
1641 (end-of-line)
1642 (let ((start (point)))
1643 (insert
1644 line
1645 (propertize " "
1646 'display `(space :align-to (,pixel-align))
1647 'face (and (> (length line) 0)
1648 (shr-face-background
1649 (get-text-property
1650 (1- (length line)) 'face line)))
1651 'shr-table-indent shr-table-id)
1652 shr-table-vertical-line)
1653 (shr-colorize-region
1654 start (1- (point)) (nth 5 column) (nth 6 column)))
1655 (forward-line 1))
1656 ;; Add blank lines at padding at the bottom of the TD,
1657 ;; possibly.
1658 (dotimes (i (- height (length lines)))
1659 (end-of-line)
1660 (let ((start (point)))
1661 (insert (propertize " "
1662 'display `(space :align-to (,pixel-align))
1663 'shr-table-indent shr-table-id)
1664 shr-table-vertical-line)
1665 (shr-colorize-region
1666 start (1- (point)) (nth 5 column) (nth 6 column)))
1667 (forward-line 1))))))
1668 (unless collapse
1669 (shr-insert-table-ruler widths)))
1670 (unless (= start (point))
1671 (put-text-property start (1+ start) 'shr-table-id shr-table-id))))
1672
1673 (defun shr-face-background (face)
1674 (and (consp face)
1675 (let ((background nil))
1676 (dolist (elem face)
1677 (when (and (consp elem)
1678 (eq (car elem) :background))
1679 (setq background (cadr elem))))
1680 (and background
1681 (list :background background)))))
1682
1683 (defun shr-expand-alignments (start end)
1684 (while (< (setq start (next-single-property-change
1685 start 'shr-table-id nil end))
1686 end)
1687 (goto-char start)
1688 (let* ((shr-use-fonts t)
1689 (id (get-text-property (point) 'shr-table-id))
1690 (base (shr-pixel-column))
1691 elem)
1692 (when id
1693 (save-excursion
1694 (while (setq elem (text-property-any
1695 (point) end 'shr-table-indent id))
1696 (goto-char elem)
1697 (let ((align (get-text-property (point) 'display)))
1698 (put-text-property (point) (1+ (point)) 'display
1699 `(space :align-to (,(+ (car (nth 2 align))
1700 base)))))
1701 (forward-char 1)))))
1702 (setq start (1+ start))))
1703
1704 (defun shr-insert-table-ruler (widths)
1705 (when shr-table-horizontal-line
1706 (when (and (bolp)
1707 (> shr-indentation 0))
1708 (shr-indent))
1709 (insert shr-table-corner)
1710 (let ((total-width 0))
1711 (dotimes (i (length widths))
1712 (setq total-width (+ total-width (aref widths i)
1713 (* shr-table-separator-pixel-width 2)))
1714 (insert (make-string (1+ (/ (aref widths i)
1715 shr-table-separator-pixel-width))
1716 shr-table-horizontal-line)
1717 (propertize " "
1718 'display `(space :align-to (,total-width))
1719 'shr-table-indent shr-table-id)
1720 shr-table-corner)))
1721 (insert "\n")))
1722
1723 (defun shr-table-widths (table natural-table suggested-widths)
1724 (let* ((length (length suggested-widths))
1725 (widths (make-vector length 0))
1726 (natural-widths (make-vector length 0)))
1727 (dolist (row table)
1728 (let ((i 0))
1729 (dolist (column row)
1730 (aset widths i (max (aref widths i) column))
1731 (setq i (1+ i)))))
1732 (dolist (row natural-table)
1733 (let ((i 0))
1734 (dolist (column row)
1735 (aset natural-widths i (max (aref natural-widths i) column))
1736 (setq i (1+ i)))))
1737 (let ((extra (- (apply '+ (append suggested-widths nil))
1738 (apply '+ (append widths nil))
1739 (* shr-table-separator-pixel-width (1+ (length widths)))))
1740 (expanded-columns 0))
1741 ;; We have extra, unused space, so divide this space amongst the
1742 ;; columns.
1743 (when (> extra 0)
1744 ;; If the natural width is wider than the rendered width, we
1745 ;; want to allow the column to expand.
1746 (dotimes (i length)
1747 (when (> (aref natural-widths i) (aref widths i))
1748 (setq expanded-columns (1+ expanded-columns))))
1749 (dotimes (i length)
1750 (when (> (aref natural-widths i) (aref widths i))
1751 (aset widths i (min
1752 (aref natural-widths i)
1753 (+ (/ extra expanded-columns)
1754 (aref widths i))))))))
1755 widths))
1756
1757 (defun shr-make-table (dom widths &optional fill storage-attribute)
1758 (or (cadr (assoc (list dom widths fill) shr-content-cache))
1759 (let ((data (shr-make-table-1 dom widths fill)))
1760 (push (list (list dom widths fill) data)
1761 shr-content-cache)
1762 (when storage-attribute
1763 (dom-set-attribute dom storage-attribute data))
1764 data)))
1765
1766 (defun shr-make-table-1 (dom widths &optional fill)
1767 (let ((trs nil)
1768 (shr-inhibit-decoration (not fill))
1769 (rowspans (make-vector (length widths) 0))
1770 (colspan-remaining 0)
1771 colspan-width colspan-count
1772 width colspan)
1773 (dolist (row (dom-non-text-children dom))
1774 (when (eq (dom-tag row) 'tr)
1775 (let ((tds nil)
1776 (columns (dom-non-text-children row))
1777 (i 0)
1778 (width-column 0)
1779 column)
1780 (while (< i (length widths))
1781 ;; If we previously had a rowspan definition, then that
1782 ;; means that we now have a "missing" td/th element here.
1783 ;; So just insert a dummy, empty one to (sort of) emulate
1784 ;; rowspan.
1785 (setq column
1786 (if (zerop (aref rowspans i))
1787 (pop columns)
1788 (aset rowspans i (1- (aref rowspans i)))
1789 '(td)))
1790 (when (and (not (stringp column))
1791 (or (memq (dom-tag column) '(td th))
1792 (not column)))
1793 (when-let (span (dom-attr column 'rowspan))
1794 (aset rowspans i (+ (aref rowspans i)
1795 (1- (string-to-number span)))))
1796 ;; Sanity check for invalid column-spans.
1797 (when (>= width-column (length widths))
1798 (setq width-column 0))
1799 (setq width
1800 (if column
1801 (aref widths width-column)
1802 (* 10 shr-table-separator-pixel-width)))
1803 (when (setq colspan (dom-attr column 'colspan))
1804 (setq colspan (min (string-to-number colspan)
1805 ;; The colspan may be wrong, so
1806 ;; truncate it to the length of the
1807 ;; remaining columns.
1808 (- (length widths) i)))
1809 (dotimes (j (1- colspan))
1810 (setq width
1811 (if (> (+ i 1 j) (1- (length widths)))
1812 ;; If we have a colspan spec that's longer
1813 ;; than the table is wide, just use the last
1814 ;; width as the width.
1815 (aref widths (1- (length widths)))
1816 ;; Sum up the widths of the columns we're
1817 ;; spanning.
1818 (+ width
1819 shr-table-separator-length
1820 (aref widths (+ i 1 j))))))
1821 (setq width-column (+ width-column (1- colspan))
1822 colspan-count colspan
1823 colspan-remaining colspan))
1824 (when column
1825 (let ((data (shr-render-td column width fill)))
1826 (if (and (not fill)
1827 (> colspan-remaining 0))
1828 (progn
1829 (setq colspan-width (car data))
1830 (let ((this-width (/ colspan-width colspan-count)))
1831 (push (cons this-width (cadr data)) tds)
1832 (setq colspan-remaining (1- colspan-remaining))))
1833 (if (not fill)
1834 (push (cons (car data) (cadr data)) tds)
1835 (push data tds)))))
1836 (when (and colspan
1837 (> colspan 1))
1838 (dotimes (c (1- colspan))
1839 (setq i (1+ i))
1840 (push
1841 (if fill
1842 (list 0 0 -1 nil 1 nil nil)
1843 '(0 . 0))
1844 tds)))
1845 (setq i (1+ i)
1846 width-column (1+ width-column))))
1847 (push (nreverse tds) trs))))
1848 (nreverse trs)))
1849
1850 (defun shr-pixel-buffer-width ()
1851 (if (not shr-use-fonts)
1852 (save-excursion
1853 (goto-char (point-min))
1854 (let ((max 0))
1855 (while (not (eobp))
1856 (end-of-line)
1857 (setq max (max max (current-column)))
1858 (forward-line 1))
1859 max))
1860 (if (get-buffer-window)
1861 (car (window-text-pixel-size nil (point-min) (point-max)))
1862 (save-window-excursion
1863 (set-window-buffer nil (current-buffer))
1864 (car (window-text-pixel-size nil (point-min) (point-max)))))))
1865
1866 (defun shr-render-td (dom width fill)
1867 (let ((cache (intern (format "shr-td-cache-%s-%s" width fill))))
1868 (or (dom-attr dom cache)
1869 (and fill
1870 (let (result)
1871 (dolist (attr (dom-attributes dom))
1872 (let ((name (symbol-name (car attr))))
1873 (when (string-match "shr-td-cache-\\([0-9]+\\)-nil" name)
1874 (let ((cache-width (string-to-number
1875 (match-string 1 name))))
1876 (when (and (>= cache-width width)
1877 (<= (car (cdr attr)) width))
1878 (setq result (cdr attr)))))))
1879 result))
1880 (let ((result (shr-render-td-1 dom width fill)))
1881 (dom-set-attribute dom cache result)
1882 result))))
1883
1884 (defun shr-render-td-1 (dom width fill)
1885 (with-temp-buffer
1886 (let ((bgcolor (dom-attr dom 'bgcolor))
1887 (fgcolor (dom-attr dom 'fgcolor))
1888 (style (dom-attr dom 'style))
1889 (shr-stylesheet shr-stylesheet)
1890 (max-width 0)
1891 natural-width)
1892 (when style
1893 (setq style (and (string-match "color" style)
1894 (shr-parse-style style))))
1895 (when bgcolor
1896 (setq style (nconc (list (cons 'background-color bgcolor))
1897 style)))
1898 (when fgcolor
1899 (setq style (nconc (list (cons 'color fgcolor)) style)))
1900 (when style
1901 (setq shr-stylesheet (append style shr-stylesheet)))
1902 (let ((shr-internal-width width)
1903 (shr-indentation 0))
1904 (shr-descend dom))
1905 (save-window-excursion
1906 (set-window-buffer nil (current-buffer))
1907 (unless fill
1908 (setq natural-width
1909 (or (dom-attr dom 'shr-td-cache-natural)
1910 (let ((natural (max (shr-pixel-buffer-width)
1911 (shr-dom-max-natural-width dom 0))))
1912 (dom-set-attribute dom 'shr-td-cache-natural natural)
1913 natural))))
1914 (if (and natural-width
1915 (<= natural-width width))
1916 (setq max-width natural-width)
1917 (let ((shr-internal-width width))
1918 (shr-fill-lines (point-min) (point-max))
1919 (setq max-width (shr-pixel-buffer-width)))))
1920 (goto-char (point-max))
1921 ;; Delete padding at the bottom of the TDs.
1922 (delete-region
1923 (point)
1924 (progn
1925 (skip-chars-backward " \t\n")
1926 (end-of-line)
1927 (point)))
1928 (goto-char (point-min))
1929 (list max-width
1930 natural-width
1931 (count-lines (point-min) (point-max))
1932 (split-string (buffer-string) "\n")
1933 (if (dom-attr dom 'colspan)
1934 (string-to-number (dom-attr dom 'colspan))
1935 1)
1936 (cdr (assq 'color shr-stylesheet))
1937 (cdr (assq 'background-color shr-stylesheet))))))
1938
1939 (defun shr-dom-max-natural-width (dom max)
1940 (if (eq (dom-tag dom) 'table)
1941 (max max (or
1942 (loop for line in (dom-attr dom 'shr-suggested-widths)
1943 maximize (+
1944 shr-table-separator-length
1945 (loop for elem in line
1946 summing
1947 (+ (cdr elem)
1948 (* 2 shr-table-separator-length)))))
1949 0))
1950 (dolist (child (dom-children dom))
1951 (unless (stringp child)
1952 (setq max (max (shr-dom-max-natural-width child max)))))
1953 max))
1954
1955 (defun shr-buffer-width ()
1956 (goto-char (point-min))
1957 (let ((max 0))
1958 (while (not (eobp))
1959 (end-of-line)
1960 (setq max (max max (current-column)))
1961 (forward-line 1))
1962 max))
1963
1964 (defun shr-pro-rate-columns (columns)
1965 (let ((total-percentage 0)
1966 (widths (make-vector (length columns) 0)))
1967 (dotimes (i (length columns))
1968 (setq total-percentage (+ total-percentage (aref columns i))))
1969 (setq total-percentage (/ 1.0 total-percentage))
1970 (dotimes (i (length columns))
1971 (aset widths i (max (truncate (* (aref columns i)
1972 total-percentage
1973 (- shr-internal-width
1974 (* (1+ (length columns))
1975 shr-table-separator-pixel-width))))
1976 10)))
1977 widths))
1978
1979 ;; Return a summary of the number and shape of the TDs in the table.
1980 (defun shr-column-specs (dom)
1981 (let ((columns (make-vector (shr-max-columns dom) 1)))
1982 (dolist (row (dom-non-text-children dom))
1983 (when (eq (dom-tag row) 'tr)
1984 (let ((i 0))
1985 (dolist (column (dom-non-text-children row))
1986 (when (memq (dom-tag column) '(td th))
1987 (let ((width (dom-attr column 'width)))
1988 (when (and width
1989 (string-match "\\([0-9]+\\)%" width)
1990 (not (zerop (setq width (string-to-number
1991 (match-string 1 width))))))
1992 (aset columns i (/ width 100.0))))
1993 (setq i (1+ i)))))))
1994 columns))
1995
1996 (defun shr-count (dom elem)
1997 (let ((i 0))
1998 (dolist (sub (dom-children dom))
1999 (when (and (not (stringp sub))
2000 (eq (dom-tag sub) elem))
2001 (setq i (1+ i))))
2002 i))
2003
2004 (defun shr-max-columns (dom)
2005 (let ((max 0))
2006 (dolist (row (dom-children dom))
2007 (when (and (not (stringp row))
2008 (eq (dom-tag row) 'tr))
2009 (setq max (max max (+ (shr-count row 'td)
2010 (shr-count row 'th))))))
2011 max))
2012
2013 (provide 'shr)
2014
2015 ;; Local Variables:
2016 ;; coding: utf-8
2017 ;; End:
2018
2019 ;;; shr.el ends here