Continue implementation.
[gnus] / lisp / gnus-html.el
1 ;;; gnus-html.el --- Render HTML in a buffer.
2
3 ;; Copyright (C) 2010  Free Software Foundation, Inc.
4
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;; Keywords: html, web
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 ;; The idea is to provide a simple, fast and pretty minimal way to
26 ;; render HTML (including links and images) in a buffer, based on an
27 ;; external HTML renderer (i.e., w3m).
28
29 ;;; Code:
30
31 (eval-when-compile (require 'cl))
32 (eval-when-compile (require 'mm-decode))
33
34 (require 'gnus-art)
35 (require 'mm-url)
36 (require 'url)
37 (require 'url-cache)
38 (require 'xml)
39 (require 'browse-url)
40 (require 'help-fns)
41
42 (defcustom gnus-html-image-cache-ttl (days-to-time 7)
43   "Time used to determine if we should use images from the cache."
44   :version "24.1"
45   :group 'gnus-art
46   :type 'integer)
47
48 (defcustom gnus-html-image-automatic-caching t
49   "Whether automatically cache retrieve images."
50   :version "24.1"
51   :group 'gnus-art
52   :type 'boolean)
53
54 (defcustom gnus-html-frame-width 70
55   "What width to use when rendering HTML."
56   :version "24.1"
57   :group 'gnus-art
58   :type 'integer)
59
60 (defcustom gnus-blocked-images "."
61   "Images that have URLs matching this regexp will be blocked."
62   :version "24.1"
63   :group 'gnus-art
64   :type 'regexp)
65
66 (defcustom gnus-max-image-proportion 0.9
67   "How big pictures displayed are in relation to the window they're in.
68 A value of 0.7 means that they are allowed to take up 70% of the
69 width and height of the window.  If they are larger than this,
70 and Emacs supports it, then the images will be rescaled down to
71 fit these criteria."
72   :version "24.1"
73   :group 'gnus-art
74   :type 'float)
75
76 (defvar gnus-html-image-map
77   (let ((map (make-sparse-keymap)))
78     (define-key map "u" 'gnus-article-copy-string)
79     (define-key map "i" 'gnus-html-insert-image)
80     (define-key map "v" 'gnus-html-browse-url)
81     map))
82
83 (defvar gnus-html-displayed-image-map
84   (let ((map (make-sparse-keymap)))
85     (define-key map "a" 'gnus-html-show-alt-text)
86     (define-key map "i" 'gnus-html-browse-image)
87     (define-key map "\r" 'gnus-html-browse-url)
88     (define-key map "u" 'gnus-article-copy-string)
89     (define-key map [tab] 'widget-forward)
90     map))
91
92 (eval-and-compile
93   (defalias 'gnus-html-encode-url-chars
94     (if (fboundp 'browse-url-url-encode-chars)
95         'browse-url-url-encode-chars
96       (lambda (text chars)
97         "URL-encode the chars in TEXT that match CHARS.
98 CHARS is a regexp-like character alternative (e.g., \"[)$]\")."
99         (let ((encoded-text (copy-sequence text))
100               (s 0))
101           (while (setq s (string-match chars encoded-text s))
102             (setq encoded-text
103                   (replace-match (format "%%%x"
104                                          (string-to-char
105                                           (match-string 0 encoded-text)))
106                                  t t encoded-text)
107                   s (1+ s)))
108           encoded-text))))
109   ;; XEmacs does not have window-inside-pixel-edges
110   (defalias 'gnus-window-inside-pixel-edges
111     (if (fboundp 'window-inside-pixel-edges)
112         'window-inside-pixel-edges
113       'window-pixel-edges)))
114
115 (defun gnus-html-encode-url (url)
116   "Encode URL."
117   (gnus-html-encode-url-chars url "[)$ ]"))
118
119 (defun gnus-html-cache-expired (url ttl)
120   "Check if URL is cached for more than TTL."
121   (cond (url-standalone-mode
122          (not (file-exists-p (url-cache-create-filename url))))
123         (t (let ((cache-time (url-is-cached url)))
124              (if cache-time
125                  (time-less-p
126                   (time-add
127                    cache-time
128                    ttl)
129                   (current-time))
130                t)))))
131
132 ;;;###autoload
133 (defun gnus-article-html (&optional handle)
134   (let ((article-buffer (current-buffer)))
135     (unless handle
136       (setq handle (mm-dissect-buffer t)))
137     (save-restriction
138       (narrow-to-region (point) (point))
139       (save-excursion
140         (mm-with-part handle
141           (let* ((coding-system-for-read 'utf-8)
142                  (coding-system-for-write 'utf-8)
143                  (default-process-coding-system
144                    (cons coding-system-for-read coding-system-for-write))
145                  (charset (mail-content-type-get (mm-handle-type handle)
146                                                  'charset)))
147             (when (and charset
148                        (setq charset (mm-charset-to-coding-system charset))
149                        (not (eq charset 'ascii)))
150               (insert (prog1
151                           (mm-decode-coding-string (buffer-string) charset)
152                         (erase-buffer)
153                         (mm-enable-multibyte))))
154             (call-process-region (point-min) (point-max)
155                                  "w3m"
156                                  nil article-buffer nil
157                                  "-halfdump"
158                                  "-no-cookie"
159                                  "-I" "UTF-8"
160                                  "-O" "UTF-8"
161                                  "-o" "ext_halfdump=1"
162                                  "-o" "display_ins_del=2"
163                                  "-o" "pre_conv=1"
164                                  "-t" (format "%s" tab-width)
165                                  "-cols" (format "%s" gnus-html-frame-width)
166                                  "-o" "display_image=on"
167                                  "-T" "text/html"))))
168       (gnus-html-wash-tags))))
169
170 (defvar gnus-article-mouse-face)
171
172 (defun gnus-html-pre-wash ()
173   (goto-char (point-min))
174   (while (re-search-forward " *<pre_int> *</pre_int> *\n" nil t)
175     (replace-match "" t t))
176   (goto-char (point-min))
177   (while (re-search-forward "<a name[^\n>]+>" nil t)
178     (replace-match "" t t)))
179
180 (defun gnus-html-wash-images ()
181   "Run through current buffer and replace img tags by images."
182   (let (tag parameters string start end images url)
183     (goto-char (point-min))
184     ;; Search for all the images first.
185     (while (re-search-forward "<img_alt \\([^>]*\\)>" nil t)
186       (setq parameters (match-string 1)
187             start (match-beginning 0))
188       (delete-region start (point))
189       (when (search-forward "</img_alt>" (line-end-position) t)
190         (delete-region (match-beginning 0) (match-end 0)))
191       (setq end (point))
192       (when (string-match "src=\"\\([^\"]+\\)" parameters)
193         (setq url (gnus-html-encode-url (match-string 1 parameters)))
194         (gnus-message 8 "gnus-html-wash-tags: fetching image URL %s" url)
195         (if (string-match "^cid:\\(.*\\)" url)
196             ;; URLs with cid: have their content stashed in other
197             ;; parts of the MIME structure, so just insert them
198             ;; immediately.
199             (let ((handle (mm-get-content-id
200                            (setq url (match-string 1 url))))
201                   image)
202               (when handle
203                 (mm-with-part handle
204                   (setq image (gnus-create-image (buffer-string)
205                                                  nil t))))
206               (when image
207                 (let ((string (buffer-substring start end)))
208                   (delete-region start end)
209                   (gnus-put-image image (gnus-string-or string "*") 'cid)
210                   (gnus-add-image 'cid image))))
211           ;; Normal, external URL.
212           (let ((alt-text (when (string-match "\\(alt\\|title\\)=\"\\([^\"]+\\)"
213                                               parameters)
214                             (xml-substitute-special (match-string 2 parameters)))))
215             (gnus-put-text-property start end 'gnus-image-url url)
216             (if (gnus-html-image-url-blocked-p
217                  url
218                  (if (buffer-live-p gnus-summary-buffer)
219                      (with-current-buffer gnus-summary-buffer
220                        gnus-blocked-images)
221                    gnus-blocked-images))
222                 (progn
223                   (widget-convert-button
224                    'link start end
225                    :action 'gnus-html-insert-image
226                    :help-echo url
227                    :keymap gnus-html-image-map
228                    :button-keymap gnus-html-image-map)
229                   (let ((overlay (gnus-make-overlay start end))
230                         (spec (list url start end alt-text)))
231                     (gnus-overlay-put overlay 'local-map gnus-html-image-map)
232                     (gnus-overlay-put overlay 'gnus-image spec)
233                     (gnus-put-text-property
234                      start end
235                      'gnus-image spec)))
236               ;; Non-blocked url
237               (let ((width
238                      (when (string-match "width=\"?\\([0-9]+\\)" parameters)
239                        (string-to-number (match-string 1 parameters))))
240                     (height
241                      (when (string-match "height=\"?\\([0-9]+\\)" parameters)
242                        (string-to-number (match-string 1 parameters)))))
243                 ;; Don't fetch images that are really small.  They're
244                 ;; probably tracking pictures.
245                 (when (and (or (null height)
246                                (> height 4))
247                            (or (null width)
248                                (> width 4)))
249                   (gnus-html-display-image url start end alt-text))))))))))
250
251 (defun gnus-html-display-image (url start end alt-text)
252   "Display image at URL on text from START to END.
253 Use ALT-TEXT for the image string."
254   (if (gnus-html-cache-expired url gnus-html-image-cache-ttl)
255       ;; We don't have it, so schedule it for fetching
256       ;; asynchronously.
257       (gnus-html-schedule-image-fetching
258        (current-buffer)
259        (list url alt-text))
260     ;; It's already cached, so just insert it.
261     (gnus-html-put-image (gnus-html-get-image-data url) url alt-text)))
262
263 (defun gnus-html-wash-tags ()
264   (let (tag parameters string start end images url)
265     (gnus-html-pre-wash)
266     (gnus-html-wash-images)
267
268     (goto-char (point-min))
269     ;; Then do the other tags.
270     (while (re-search-forward "<\\([^ />]+\\)\\([^>]*\\)>" nil t)
271       (setq tag (match-string 1)
272             parameters (match-string 2)
273             start (match-beginning 0))
274       (when (plusp (length parameters))
275         (set-text-properties 0 (1- (length parameters)) nil parameters))
276       (delete-region start (point))
277       (when (search-forward (concat "</" tag ">") nil t)
278         (delete-region (match-beginning 0) (match-end 0)))
279       (setq end (point))
280       (cond
281        ;; Fetch and insert a picture.
282        ((equal tag "img_alt"))
283        ;; Add a link.
284        ((or (equal tag "a")
285             (equal tag "A"))
286         (when (string-match "href=\"\\([^\"]+\\)" parameters)
287           (setq url (match-string 1 parameters))
288           (gnus-message 8 "gnus-html-wash-tags: fetching link URL %s" url)
289           (gnus-article-add-button start end
290                                    'browse-url url
291                                    url)
292           (let ((overlay (gnus-make-overlay start end)))
293             (gnus-overlay-put overlay 'evaporate t)
294             (gnus-overlay-put overlay 'gnus-button-url url)
295             (gnus-put-text-property start end 'gnus-string url)
296             (when gnus-article-mouse-face
297               (gnus-overlay-put overlay 'mouse-face gnus-article-mouse-face)))))
298        ;; The upper-case IMG_ALT is apparently just an artifact that
299        ;; should be deleted.
300        ((equal tag "IMG_ALT")
301         (delete-region start end))
302        ;; w3m does not normalize the case
303        ((or (equal tag "b")
304             (equal tag "B"))
305         (gnus-overlay-put (gnus-make-overlay start end) 'face 'gnus-emphasis-bold))
306        ((or (equal tag "u")
307             (equal tag "U"))
308         (gnus-overlay-put (gnus-make-overlay start end) 'face 'gnus-emphasis-underline))
309        ((or (equal tag "i")
310             (equal tag "I"))
311         (gnus-overlay-put (gnus-make-overlay start end) 'face 'gnus-emphasis-italic))
312        ((or (equal tag "s")
313             (equal tag "S"))
314         (gnus-overlay-put (gnus-make-overlay start end) 'face 'gnus-emphasis-strikethru))
315        ((or (equal tag "ins")
316             (equal tag "INS"))
317         (gnus-overlay-put (gnus-make-overlay start end) 'face 'gnus-emphasis-underline))
318        ;; Handle different UL types
319        ((equal tag "_SYMBOL")
320         (when (string-match "TYPE=\\(.+\\)" parameters)
321           (let ((type (string-to-number (match-string 1 parameters))))
322             (delete-region start end)
323             (cond ((= type 33) (insert " "))
324                   ((= type 34) (insert " "))
325                   ((= type 35) (insert " "))
326                   ((= type 36) (insert " "))
327                   ((= type 37) (insert " "))
328                   ((= type 38) (insert " "))
329                   ((= type 39) (insert " "))
330                   ((= type 40) (insert " "))
331                   ((= type 42) (insert " "))
332                   ((= type 43) (insert " "))
333                   (t (insert " "))))))
334        ;; Whatever.  Just ignore the tag.
335        (t
336         ))
337       (goto-char start))
338     (goto-char (point-min))
339     ;; The output from -halfdump isn't totally regular, so strip
340     ;; off any </pre_int>s that were left over.
341     (while (re-search-forward "</pre_int>\\|</internal>" nil t)
342       (replace-match "" t t))
343     (mm-url-decode-entities)))
344
345 (defun gnus-html-insert-image ()
346   "Fetch and insert the image under point."
347   (interactive)
348   (apply 'gnus-html-display-image (get-text-property (point) 'gnus-image)))
349
350 (defun gnus-html-show-alt-text ()
351   "Show the ALT text of the image under point."
352   (interactive)
353   (message "%s" (get-text-property (point) 'gnus-alt-text)))
354
355 (defun gnus-html-browse-image ()
356   "Browse the image under point."
357   (interactive)
358   (browse-url (get-text-property (point) 'gnus-image-url)))
359
360 (defun gnus-html-browse-url ()
361   "Browse the image under point."
362   (interactive)
363   (let ((url (get-text-property (point) 'gnus-string)))
364     (if (not url)
365         (message "No URL at point")
366       (browse-url url))))
367
368 (defun gnus-html-schedule-image-fetching (buffer image)
369   "Retrieve IMAGE, and place it into BUFFER on arrival."
370   (gnus-message 8 "gnus-html-schedule-image-fetching: buffer %s, image %s"
371                 buffer image)
372   (let ((args (list (car image)
373                     'gnus-html-image-fetched
374                     (list buffer image))))
375     (when (> (length (help-function-arglist 'url-retrieve)) 4)
376       (setq args (nconc args (list t))))
377     (apply #'url-retrieve args)))
378
379 (defun gnus-html-image-fetched (status buffer image)
380   "Callback function called when image has been fetched."
381   (unless (plist-get status :error)
382     (when gnus-html-image-automatic-caching
383       (url-store-in-cache (current-buffer)))
384     (when (and (or (search-forward "\n\n" nil t)
385                    (search-forward "\r\n\r\n" nil t))
386                (buffer-live-p buffer))
387       (let ((data (buffer-substring (point) (point-max))))
388         (with-current-buffer buffer
389           (let ((inhibit-read-only t))
390             (gnus-html-put-image data (car image) (cadr image)))))))
391   (kill-buffer (current-buffer)))
392
393 (defun gnus-html-get-image-data (url)
394   "Get image data for URL.
395 Return a string with image data."
396   (with-temp-buffer
397     (mm-disable-multibyte)
398     (url-cache-extract (url-cache-create-filename url))
399     (when (or (search-forward "\n\n" nil t)
400               (search-forward "\r\n\r\n" nil t))
401       (buffer-substring (point) (point-max)))))
402
403 (defun gnus-html-put-image (data url &optional alt-text)
404   (when (gnus-graphic-display-p)
405     (let* ((start (text-property-any (point-min) (point-max) 'gnus-image-url url))
406            (end (when start
407                   (next-single-property-change start 'gnus-image-url))))
408       ;; Image found?
409       (when start
410         (let* ((image
411                 (ignore-errors
412                   (gnus-create-image data nil t)))
413                (size (and image
414                           (if (featurep 'xemacs)
415                               (cons (glyph-width image) (glyph-height image))
416                             (image-size image t)))))
417           (save-excursion
418             (goto-char start)
419             (let ((alt-text (or alt-text (buffer-substring-no-properties start end))))
420               (if (and image
421                        ;; Kludge to avoid displaying 30x30 gif images, which
422                        ;; seems to be a signal of a broken image.
423                        (not (and (if (featurep 'xemacs)
424                                      (glyphp image)
425                                    (listp image))
426                                  (eq (if (featurep 'xemacs)
427                                          (let ((d (cdadar (specifier-spec-list
428                                                            (glyph-image image)))))
429                                            (and (vectorp d)
430                                                 (aref d 0)))
431                                        (plist-get (cdr image) :type))
432                                      'gif)
433                                  (= (car size) 30)
434                                  (= (cdr size) 30))))
435                   ;; Good image, add it!
436                   (let ((image (gnus-html-rescale-image image data size)))
437                     (delete-region start end)
438                     (gnus-put-image image alt-text 'external)
439                     (gnus-put-text-property start (point) 'help-echo alt-text)
440                     (gnus-overlay-put (gnus-make-overlay start (point)) 'local-map
441                                       gnus-html-displayed-image-map)
442                     (gnus-put-text-property start (point) 'gnus-alt-text alt-text)
443                     (when url
444                       (gnus-put-text-property start (point) 'gnus-image-url url))
445                     (gnus-add-image 'external image)
446                     t)
447                 ;; Bad image, try to show something else
448                 (when (fboundp 'find-image)
449                   (delete-region start end)
450                   (setq image (find-image '((:type xpm :file "lock-broken.xpm"))))
451                   (gnus-put-image image alt-text 'internal)
452                   (gnus-add-image 'internal image))
453                 nil))))))))
454
455 (defun gnus-html-rescale-image (image data size)
456   (if (or (not (fboundp 'imagemagick-types))
457           (not (get-buffer-window (current-buffer))))
458       image
459     (let* ((width (car size))
460            (height (cdr size))
461            (edges (gnus-window-inside-pixel-edges (get-buffer-window (current-buffer))))
462            (window-width (truncate (* gnus-max-image-proportion
463                                       (- (nth 2 edges) (nth 0 edges)))))
464            (window-height (truncate (* gnus-max-image-proportion
465                                        (- (nth 3 edges) (nth 1 edges)))))
466            scaled-image)
467       (when (> height window-height)
468         (setq image (or (create-image data 'imagemagick t
469                                       :height window-height)
470                         image))
471         (setq size (image-size image t)))
472       (when (> (car size) window-width)
473         (setq image (or
474                      (create-image data 'imagemagick t
475                                    :width window-width)
476                      image)))
477       image)))
478
479 (defun gnus-html-image-url-blocked-p (url blocked-images)
480   "Find out if URL is blocked by BLOCKED-IMAGES."
481   (let ((ret (and blocked-images
482                   (string-match blocked-images url))))
483     (if ret
484         (gnus-message 8 "gnus-html-image-url-blocked-p: %s blocked by regex %s"
485                       url blocked-images)
486       (gnus-message 9 "gnus-html-image-url-blocked-p: %s passes regex %s"
487                     url blocked-images))
488     ret))
489
490 (defun gnus-html-show-images ()
491   "Show any images that are in the HTML-rendered article buffer.
492 This only works if the article in question is HTML."
493   (interactive)
494   (gnus-with-article-buffer
495     (dolist (overlay (overlays-in (point-min) (point-max)))
496       (let ((o (overlay-get overlay 'gnus-image)))
497         (when o
498           (apply 'gnus-html-display-image o))))))
499
500 ;;;###autoload
501 (defun gnus-html-prefetch-images (summary)
502   (when (buffer-live-p summary)
503     (let ((blocked-images (with-current-buffer summary
504                             gnus-blocked-images)))
505       (save-match-data
506         (while (re-search-forward "<img[^>]+src=[\"']\\([^\"']+\\)" nil t)
507           (let ((url (gnus-html-encode-url (match-string 1))))
508             (unless (gnus-html-image-url-blocked-p url blocked-images)
509               (when (gnus-html-cache-expired url gnus-html-image-cache-ttl)
510                 (gnus-html-schedule-image-fetching nil
511                                                    (list url))))))))))
512
513 (provide 'gnus-html)
514
515 ;;; gnus-html.el ends here