eww: add command to view page source
[bpt/emacs.git] / lisp / net / eww.el
CommitLineData
266c63b5
AK
1;;; eww.el --- Emacs Web Wowser
2
3;; Copyright (C) 2013 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;;; Code:
26
27(eval-when-compile (require 'cl))
7545bd25 28(require 'format-spec)
266c63b5
AK
29(require 'shr)
30(require 'url)
2644071e 31(require 'mm-url)
266c63b5 32
c74cb344
G
33(defgroup eww nil
34 "Emacs Web Wowser"
35 :version "24.4"
36 :group 'hypermedia
37 :prefix "eww-")
38
39(defcustom eww-header-line-format "%t: %u"
40 "Header line format.
41- %t is replaced by the title.
42- %u is replaced by the URL."
a3ca09b9
IK
43 :version "24.4"
44 :group 'eww
45 :type 'string)
46
47(defcustom eww-search-prefix "https://duckduckgo.com/html/?q="
48 "Prefix URL to search engine"
49 :version "24.4"
c74cb344
G
50 :group 'eww
51 :type 'string)
52
bfbc93a1
IK
53(defcustom eww-download-path "~/Downloads/"
54 "Path where files will downloaded."
55 :version "24.4"
56 :group 'eww
57 :type 'string)
58
b2afb3ea
RS
59(defcustom eww-use-external-browser-for-content-type
60 "\\`\\(video/\\|audio/\\|application/ogg\\)"
61 "Always use external browser for specified content-type."
62 :version "24.4"
63 :group 'eww
64 :type '(choice (const :tag "Never" nil)
65 regexp))
66
4570dd16
RS
67(defcustom eww-form-checkbox-selected-symbol "[X]"
68 "Symbol used to represent a selected checkbox.
69See also `eww-form-checkbox-symbol'."
70 :version "24.4"
71 :group 'eww
72 :type '(choice (const "[X]")
73 (const "☒") ; Unicode BALLOT BOX WITH X
74 (const "☑") ; Unicode BALLOT BOX WITH CHECK
75 string))
76
77(defcustom eww-form-checkbox-symbol "[ ]"
78 "Symbol used to represent a checkbox.
79See also `eww-form-checkbox-selected-symbol'."
80 :version "24.4"
81 :group 'eww
82 :type '(choice (const "[ ]")
83 (const "☐") ; Unicode BALLOT BOX
84 string))
85
970ad972
G
86(defface eww-form-submit
87 '((((type x w32 ns) (class color)) ; Like default mode line
88 :box (:line-width 2 :style released-button)
89 :background "#808080" :foreground "black"))
90 "Face for eww buffer buttons."
91 :version "24.4"
92 :group 'eww)
93
94(defface eww-form-checkbox
95 '((((type x w32 ns) (class color)) ; Like default mode line
96 :box (:line-width 2 :style released-button)
97 :background "lightgrey" :foreground "black"))
98 "Face for eww buffer buttons."
99 :version "24.4"
100 :group 'eww)
101
102(defface eww-form-select
be2aa135
LMI
103 '((((type x w32 ns) (class color)) ; Like default mode line
104 :box (:line-width 2 :style released-button)
105 :background "lightgrey" :foreground "black"))
106 "Face for eww buffer buttons."
107 :version "24.4"
108 :group 'eww)
109
970ad972
G
110(defface eww-form-text
111 '((t (:background "#505050"
112 :foreground "white"
113 :box (:line-width 1))))
114 "Face for eww text inputs."
115 :version "24.4"
116 :group 'eww)
117
266c63b5 118(defvar eww-current-url nil)
ab6dea82 119(defvar eww-current-dom nil)
ff69c18f 120(defvar eww-current-source nil)
c74cb344
G
121(defvar eww-current-title ""
122 "Title of current page.")
266c63b5 123(defvar eww-history nil)
d3f0f918 124(defvar eww-history-position 0)
266c63b5 125
924d6997
G
126(defvar eww-next-url nil)
127(defvar eww-previous-url nil)
128(defvar eww-up-url nil)
970ad972
G
129(defvar eww-home-url nil)
130(defvar eww-start-url nil)
131(defvar eww-contents-url nil)
924d6997 132
604ede6c
TZ
133(defvar eww-local-regex "localhost"
134 "When this regex is found in the URL, it's not a keyword but an address.")
135
1af66437
LMI
136(defvar eww-link-keymap
137 (let ((map (copy-keymap shr-map)))
138 (define-key map "\r" 'eww-follow-link)
139 map))
140
d583b36b 141;;;###autoload
266c63b5 142(defun eww (url)
a3ca09b9
IK
143 "Fetch URL and render the page.
144If the input doesn't look like an URL or a domain name, the
145word(s) will be searched for via `eww-search-prefix'."
146 (interactive "sEnter URL or keywords: ")
604ede6c
TZ
147 (cond ((string-match-p "\\`file:" url))
148 (t
149 (if (and (= (length (split-string url)) 1)
150 (or (> (length (split-string url "\\.")) 1)
151 (string-match eww-local-regex url)))
152 (progn
153 (unless (string-match-p "\\`[a-zA-Z][-a-zA-Z0-9+.]*://" url)
154 (setq url (concat "http://" url)))
155 ;; some site don't redirect final /
156 (when (string= (url-filename (url-generic-parse-url url)) "")
157 (setq url (concat url "/"))))
158 (setq url (concat eww-search-prefix
159 (replace-regexp-in-string " " "+" url))))))
266c63b5
AK
160 (url-retrieve url 'eww-render (list url)))
161
924d6997
G
162;;;###autoload
163(defun eww-open-file (file)
164 "Render a file using EWW."
165 (interactive "fFile: ")
166 (eww (concat "file://" (expand-file-name file))))
167
266c63b5 168(defun eww-render (status url &optional point)
c74cb344
G
169 (let ((redirect (plist-get status :redirect)))
170 (when redirect
171 (setq url redirect)))
924d6997
G
172 (set (make-local-variable 'eww-next-url) nil)
173 (set (make-local-variable 'eww-previous-url) nil)
174 (set (make-local-variable 'eww-up-url) nil)
970ad972
G
175 (set (make-local-variable 'eww-home-url) nil)
176 (set (make-local-variable 'eww-start-url) nil)
177 (set (make-local-variable 'eww-contents-url) nil)
266c63b5
AK
178 (let* ((headers (eww-parse-headers))
179 (content-type
180 (mail-header-parse-content-type
181 (or (cdr (assoc "content-type" headers))
182 "text/plain")))
183 (charset (intern
184 (downcase
185 (or (cdr (assq 'charset (cdr content-type)))
d652f4d0
G
186 (eww-detect-charset (equal (car content-type)
187 "text/html"))
266c63b5
AK
188 "utf8"))))
189 (data-buffer (current-buffer)))
190 (unwind-protect
191 (progn
450c7b35 192 (setq eww-current-title "")
266c63b5 193 (cond
b2afb3ea
RS
194 ((and eww-use-external-browser-for-content-type
195 (string-match-p eww-use-external-browser-for-content-type
196 (car content-type)))
197 (eww-browse-with-external-browser url))
266c63b5 198 ((equal (car content-type) "text/html")
513562a1 199 (eww-display-html charset url nil point))
b2afb3ea 200 ((string-match-p "\\`image/" (car content-type))
39fa32d6 201 (eww-display-image)
eff0a2bd 202 (eww-update-header-line-format))
266c63b5 203 (t
513562a1
LMI
204 (eww-display-raw)
205 (eww-update-header-line-format)))
62ad85e6 206 (setq eww-current-url url
513562a1 207 eww-history-position 0))
266c63b5
AK
208 (kill-buffer data-buffer))))
209
210(defun eww-parse-headers ()
211 (let ((headers nil))
d652f4d0 212 (goto-char (point-min))
266c63b5
AK
213 (while (and (not (eobp))
214 (not (eolp)))
215 (when (looking-at "\\([^:]+\\): *\\(.*\\)")
216 (push (cons (downcase (match-string 1))
217 (match-string 2))
218 headers))
219 (forward-line 1))
220 (unless (eobp)
221 (forward-line 1))
222 headers))
223
db5a34ca
KY
224(defun eww-detect-charset (html-p)
225 (let ((case-fold-search t)
226 (pt (point)))
227 (or (and html-p
228 (re-search-forward
b89fc156 229 "<meta[\t\n\r ]+[^>]*charset=\"?\\([^\t\n\r \"/>]+\\)[\\\"'.*]" nil t)
db5a34ca
KY
230 (goto-char pt)
231 (match-string 1))
232 (and (looking-at
233 "[\t\n\r ]*<\\?xml[\t\n\r ]+[^>]*encoding=\"\\([^\"]+\\)")
234 (match-string 1)))))
235
5148da15
GM
236(declare-function libxml-parse-html-region "xml.c"
237 (start end &optional base-url))
238
513562a1 239(defun eww-display-html (charset url &optional document point)
5148da15
GM
240 (or (fboundp 'libxml-parse-html-region)
241 (error "This function requires Emacs to be compiled with libxml2"))
266c63b5 242 (unless (eq charset 'utf8)
3e41a054
LMI
243 (condition-case nil
244 (decode-coding-region (point) (point-max) charset)
245 (coding-system-error nil)))
266c63b5 246 (let ((document
513562a1
LMI
247 (or document
248 (list
249 'base (list (cons 'href url))
250 (libxml-parse-html-region (point) (point-max))))))
ff69c18f 251 (setq eww-current-source (buffer-substring (point) (point-max)))
266c63b5 252 (eww-setup-buffer)
ab6dea82 253 (setq eww-current-dom document)
2644071e 254 (let ((inhibit-read-only t)
970ad972 255 (after-change-functions nil)
c74cb344 256 (shr-width nil)
513562a1 257 (shr-target-id (url-target (url-generic-parse-url url)))
2644071e 258 (shr-external-rendering-functions
c74cb344
G
259 '((title . eww-tag-title)
260 (form . eww-tag-form)
2644071e 261 (input . eww-tag-input)
c74cb344
G
262 (textarea . eww-tag-textarea)
263 (body . eww-tag-body)
924d6997
G
264 (select . eww-tag-select)
265 (link . eww-tag-link)
266 (a . eww-tag-a))))
513562a1
LMI
267 (shr-insert-document document)
268 (cond
269 (point
270 (goto-char point))
271 (shr-target-id
272 (let ((point (next-single-property-change
273 (point-min) 'shr-target-id)))
274 (when point
275 (goto-char (1+ point)))))
276 (t
277 (goto-char (point-min)))))
278 (setq eww-current-url url
279 eww-history-position 0)
280 (eww-update-header-line-format)))
266c63b5 281
924d6997
G
282(defun eww-handle-link (cont)
283 (let* ((rel (assq :rel cont))
284 (href (assq :href cont))
970ad972
G
285 (where (assoc
286 ;; The text associated with :rel is case-insensitive.
287 (if rel (downcase (cdr rel)))
924d6997 288 '(("next" . eww-next-url)
970ad972
G
289 ;; Texinfo uses "previous", but HTML specifies
290 ;; "prev", so recognize both.
924d6997 291 ("previous" . eww-previous-url)
970ad972
G
292 ("prev" . eww-previous-url)
293 ;; HTML specifies "start" but also "contents",
294 ;; and Gtk seems to use "home". Recognize
295 ;; them all; but store them in different
296 ;; variables so that we can readily choose the
297 ;; "best" one.
298 ("start" . eww-start-url)
299 ("home" . eww-home-url)
300 ("contents" . eww-contents-url)
924d6997
G
301 ("up" . eww-up-url)))))
302 (and href
303 where
304 (set (cdr where) (cdr href)))))
305
306(defun eww-tag-link (cont)
307 (eww-handle-link cont)
308 (shr-generic cont))
309
310(defun eww-tag-a (cont)
311 (eww-handle-link cont)
513562a1
LMI
312 (let ((start (point)))
313 (shr-tag-a cont)
314 (put-text-property start (point) 'keymap eww-link-keymap)))
924d6997 315
c74cb344
G
316(defun eww-update-header-line-format ()
317 (if eww-header-line-format
d80a808f
LMI
318 (setq header-line-format
319 (replace-regexp-in-string
320 "%" "%%"
62ad85e6
GM
321 ;; FIXME? Title can be blank. Default to, eg, last component
322 ;; of url?
d80a808f
LMI
323 (format-spec eww-header-line-format
324 `((?u . ,eww-current-url)
325 (?t . ,eww-current-title)))))
c74cb344
G
326 (setq header-line-format nil)))
327
328(defun eww-tag-title (cont)
329 (setq eww-current-title "")
330 (dolist (sub cont)
331 (when (eq (car sub) 'text)
332 (setq eww-current-title (concat eww-current-title (cdr sub)))))
333 (eww-update-header-line-format))
334
335(defun eww-tag-body (cont)
336 (let* ((start (point))
337 (fgcolor (cdr (or (assq :fgcolor cont)
338 (assq :text cont))))
339 (bgcolor (cdr (assq :bgcolor cont)))
340 (shr-stylesheet (list (cons 'color fgcolor)
341 (cons 'background-color bgcolor))))
342 (shr-generic cont)
343 (eww-colorize-region start (point) fgcolor bgcolor)))
344
345(defun eww-colorize-region (start end fg &optional bg)
346 (when (or fg bg)
347 (let ((new-colors (shr-color-check fg bg)))
348 (when new-colors
349 (when fg
544d4594 350 (add-face-text-property start end
970ad972
G
351 (list :foreground (cadr new-colors))
352 t))
c74cb344 353 (when bg
544d4594 354 (add-face-text-property start end
970ad972
G
355 (list :background (car new-colors))
356 t))))))
c74cb344 357
fde38d49 358(defun eww-display-raw ()
266c63b5
AK
359 (let ((data (buffer-substring (point) (point-max))))
360 (eww-setup-buffer)
361 (let ((inhibit-read-only t))
362 (insert data))
363 (goto-char (point-min))))
364
365(defun eww-display-image ()
21c58ae2 366 (let ((data (shr-parse-image-data)))
266c63b5
AK
367 (eww-setup-buffer)
368 (let ((inhibit-read-only t))
369 (shr-put-image data nil))
370 (goto-char (point-min))))
371
372(defun eww-setup-buffer ()
997798bf 373 (switch-to-buffer (get-buffer-create "*eww*"))
266c63b5 374 (let ((inhibit-read-only t))
8308f184 375 (remove-overlays)
266c63b5 376 (erase-buffer))
8308f184
LMI
377 (unless (eq major-mode 'eww-mode)
378 (eww-mode)))
266c63b5 379
ff69c18f
TZ
380(defun eww-view-source ()
381 (interactive)
382 (let ((buf (get-buffer-create "*eww-source*"))
383 (source eww-current-source))
384 (with-current-buffer buf
385 (delete-region (point-min) (point-max))
386 (insert (or eww-current-source "no source"))
387 (goto-char (point-min))
388 (when (featurep 'html-mode)
389 (html-mode)))
390 (view-buffer buf)))
391
266c63b5
AK
392(defvar eww-mode-map
393 (let ((map (make-sparse-keymap)))
394 (suppress-keymap map)
1f6e1bb0 395 (define-key map "q" 'quit-window)
f22255bd 396 (define-key map "g" 'eww-reload)
7304e4dd
LMI
397 (define-key map [tab] 'shr-next-link)
398 (define-key map [backtab] 'shr-previous-link)
266c63b5
AK
399 (define-key map [delete] 'scroll-down-command)
400 (define-key map "\177" 'scroll-down-command)
401 (define-key map " " 'scroll-up-command)
924d6997 402 (define-key map "l" 'eww-back-url)
d3f0f918 403 (define-key map "f" 'eww-forward-url)
924d6997 404 (define-key map "n" 'eww-next-url)
266c63b5 405 (define-key map "p" 'eww-previous-url)
924d6997
G
406 (define-key map "u" 'eww-up-url)
407 (define-key map "t" 'eww-top-url)
16f74f10 408 (define-key map "&" 'eww-browse-with-external-browser)
bfbc93a1 409 (define-key map "d" 'eww-download)
16f74f10 410 (define-key map "w" 'eww-copy-page-url)
23a75d7f 411 (define-key map "C" 'url-cookie-list)
ff69c18f 412 (define-key map "v" 'eww-view-source)
23a75d7f 413
2b4f0506
LMI
414 (define-key map "b" 'eww-add-bookmark)
415 (define-key map "B" 'eww-list-bookmarks)
416 (define-key map [(meta n)] 'eww-next-bookmark)
417 (define-key map [(meta p)] 'eww-previous-bookmark)
418
23a75d7f 419 (easy-menu-define nil map ""
6ee877c7 420 '("Eww"
23a75d7f
LMI
421 ["Quit" eww-quit t]
422 ["Reload" eww-reload t]
423 ["Back to previous page" eww-back-url
424 :active (not (zerop (length eww-history)))]
425 ["Forward to next page" eww-forward-url
426 :active (not (zerop eww-history-position))]
427 ["Browse with external browser" eww-browse-with-external-browser t]
428 ["Download" eww-download t]
ff69c18f 429 ["View page source" eww-view-source]
23a75d7f 430 ["Copy page URL" eww-copy-page-url t]
2b4f0506
LMI
431 ["Add bookmark" eww-add-bookmark t]
432 ["List bookmarks" eww-copy-page-url t]
23a75d7f 433 ["List cookies" url-cookie-list t]))
266c63b5
AK
434 map))
435
d652f4d0 436(define-derived-mode eww-mode nil "eww"
266c63b5
AK
437 "Mode for browsing the web.
438
439\\{eww-mode-map}"
62ad85e6 440 ;; FIXME? This seems a strange default.
266c63b5 441 (set (make-local-variable 'eww-current-url) 'author)
ab6dea82 442 (set (make-local-variable 'eww-current-dom) nil)
ff69c18f 443 (set (make-local-variable 'eww-current-source) nil)
970ad972
G
444 (set (make-local-variable 'browse-url-browser-function) 'eww-browse-url)
445 (set (make-local-variable 'after-change-functions) 'eww-process-text-input)
8308f184
LMI
446 (set (make-local-variable 'eww-history) nil)
447 (set (make-local-variable 'eww-history-position) 0)
843571cb 448 (buffer-disable-undo)
970ad972
G
449 ;;(setq buffer-read-only t)
450 )
266c63b5 451
d3f0f918 452(defun eww-save-history ()
8308f184 453 (push (list :url eww-current-url
2b4f0506 454 :title eww-current-title
8308f184 455 :point (point)
ab6dea82 456 :dom eww-current-dom
ff69c18f 457 :source eww-current-source
8308f184
LMI
458 :text (buffer-string))
459 eww-history))
d3f0f918 460
75dbaf9d 461;;;###autoload
6c42fc3e 462(defun eww-browse-url (url &optional _new-window)
970ad972
G
463 (when (and (equal major-mode 'eww-mode)
464 eww-current-url)
d3f0f918 465 (eww-save-history))
5c3087e9 466 (eww url))
266c63b5 467
924d6997 468(defun eww-back-url ()
266c63b5
AK
469 "Go to the previously displayed page."
470 (interactive)
d3f0f918 471 (when (>= eww-history-position (length eww-history))
266c63b5 472 (error "No previous page"))
8308f184
LMI
473 (eww-save-history)
474 (setq eww-history-position (+ eww-history-position 2))
475 (eww-restore-history (elt eww-history (1- eww-history-position))))
d3f0f918
LMI
476
477(defun eww-forward-url ()
478 "Go to the next displayed page."
479 (interactive)
480 (when (zerop eww-history-position)
481 (error "No next page"))
8308f184
LMI
482 (eww-save-history)
483 (eww-restore-history (elt eww-history (1- eww-history-position))))
d3f0f918
LMI
484
485(defun eww-restore-history (elem)
486 (let ((inhibit-read-only t))
e82b0991 487 (erase-buffer)
d3f0f918 488 (insert (plist-get elem :text))
ff69c18f 489 (setq eww-current-source (plist-get elem :source))
ab6dea82 490 (setq eww-current-dom (plist-get elem :dom))
d3f0f918 491 (goto-char (plist-get elem :point))
2b4f0506 492 (setq eww-current-url (plist-get elem :url)
3e9876de
LMI
493 eww-current-title (plist-get elem :title))
494 (eww-update-header-line-format)))
266c63b5 495
924d6997
G
496(defun eww-next-url ()
497 "Go to the page marked `next'.
498A page is marked `next' if rel=\"next\" appears in a <link>
499or <a> tag."
500 (interactive)
501 (if eww-next-url
502 (eww-browse-url (shr-expand-url eww-next-url eww-current-url))
503 (error "No `next' on this page")))
504
505(defun eww-previous-url ()
506 "Go to the page marked `previous'.
507A page is marked `previous' if rel=\"previous\" appears in a <link>
508or <a> tag."
509 (interactive)
510 (if eww-previous-url
511 (eww-browse-url (shr-expand-url eww-previous-url eww-current-url))
512 (error "No `previous' on this page")))
513
514(defun eww-up-url ()
515 "Go to the page marked `up'.
516A page is marked `up' if rel=\"up\" appears in a <link>
517or <a> tag."
518 (interactive)
519 (if eww-up-url
520 (eww-browse-url (shr-expand-url eww-up-url eww-current-url))
521 (error "No `up' on this page")))
522
523(defun eww-top-url ()
524 "Go to the page marked `top'.
970ad972
G
525A page is marked `top' if rel=\"start\", rel=\"home\", or rel=\"contents\"
526appears in a <link> or <a> tag."
924d6997 527 (interactive)
970ad972
G
528 (let ((best-url (or eww-start-url
529 eww-contents-url
530 eww-home-url)))
531 (if best-url
532 (eww-browse-url (shr-expand-url best-url eww-current-url))
533 (error "No `top' for this page"))))
924d6997 534
f22255bd
LMI
535(defun eww-reload ()
536 "Reload the current page."
537 (interactive)
538 (url-retrieve eww-current-url 'eww-render
539 (list eww-current-url (point))))
540
2644071e
LMI
541;; Form support.
542
543(defvar eww-form nil)
544
970ad972
G
545(defvar eww-submit-map
546 (let ((map (make-sparse-keymap)))
547 (define-key map "\r" 'eww-submit)
e854cfc7 548 (define-key map [(control c) (control c)] 'eww-submit)
970ad972
G
549 map))
550
551(defvar eww-checkbox-map
552 (let ((map (make-sparse-keymap)))
553 (define-key map [space] 'eww-toggle-checkbox)
554 (define-key map "\r" 'eww-toggle-checkbox)
e854cfc7 555 (define-key map [(control c) (control c)] 'eww-submit)
970ad972
G
556 map))
557
558(defvar eww-text-map
559 (let ((map (make-keymap)))
560 (set-keymap-parent map text-mode-map)
561 (define-key map "\r" 'eww-submit)
562 (define-key map [(control a)] 'eww-beginning-of-text)
e854cfc7 563 (define-key map [(control c) (control c)] 'eww-submit)
970ad972
G
564 (define-key map [(control e)] 'eww-end-of-text)
565 (define-key map [tab] 'shr-next-link)
566 (define-key map [backtab] 'shr-previous-link)
567 map))
568
569(defvar eww-textarea-map
570 (let ((map (make-keymap)))
571 (set-keymap-parent map text-mode-map)
572 (define-key map "\r" 'forward-line)
e854cfc7 573 (define-key map [(control c) (control c)] 'eww-submit)
970ad972
G
574 (define-key map [tab] 'shr-next-link)
575 (define-key map [backtab] 'shr-previous-link)
576 map))
577
578(defvar eww-select-map
579 (let ((map (make-sparse-keymap)))
580 (define-key map "\r" 'eww-change-select)
e854cfc7 581 (define-key map [(control c) (control c)] 'eww-submit)
970ad972
G
582 map))
583
584(defun eww-beginning-of-text ()
585 "Move to the start of the input field."
586 (interactive)
587 (goto-char (eww-beginning-of-field)))
588
589(defun eww-end-of-text ()
590 "Move to the end of the text in the input field."
591 (interactive)
592 (goto-char (eww-end-of-field))
593 (let ((start (eww-beginning-of-field)))
594 (while (and (equal (following-char) ? )
595 (> (point) start))
596 (forward-char -1))
597 (when (> (point) start)
598 (forward-char 1))))
599
600(defun eww-beginning-of-field ()
601 (cond
602 ((bobp)
603 (point))
604 ((not (eq (get-text-property (point) 'eww-form)
605 (get-text-property (1- (point)) 'eww-form)))
606 (point))
607 (t
608 (previous-single-property-change
609 (point) 'eww-form nil (point-min)))))
610
611(defun eww-end-of-field ()
612 (1- (next-single-property-change
613 (point) 'eww-form nil (point-max))))
614
2644071e
LMI
615(defun eww-tag-form (cont)
616 (let ((eww-form
617 (list (assq :method cont)
618 (assq :action cont)))
619 (start (point)))
620 (shr-ensure-paragraph)
621 (shr-generic cont)
3d95242e
LMI
622 (unless (bolp)
623 (insert "\n"))
624 (insert "\n")
001b9fbe
LMI
625 (when (> (point) start)
626 (put-text-property start (1+ start)
627 'eww-form eww-form))))
2644071e 628
970ad972
G
629(defun eww-form-submit (cont)
630 (let ((start (point))
631 (value (cdr (assq :value cont))))
632 (setq value
633 (if (zerop (length value))
634 "Submit"
635 value))
636 (insert value)
637 (add-face-text-property start (point) 'eww-form-submit)
638 (put-text-property start (point) 'eww-form
639 (list :eww-form eww-form
640 :value value
641 :type "submit"
642 :name (cdr (assq :name cont))))
643 (put-text-property start (point) 'keymap eww-submit-map)
644 (insert " ")))
645
646(defun eww-form-checkbox (cont)
647 (let ((start (point)))
648 (if (cdr (assq :checked cont))
4570dd16
RS
649 (insert eww-form-checkbox-selected-symbol)
650 (insert eww-form-checkbox-symbol))
970ad972
G
651 (add-face-text-property start (point) 'eww-form-checkbox)
652 (put-text-property start (point) 'eww-form
653 (list :eww-form eww-form
654 :value (cdr (assq :value cont))
655 :type (downcase (cdr (assq :type cont)))
656 :checked (cdr (assq :checked cont))
657 :name (cdr (assq :name cont))))
658 (put-text-property start (point) 'keymap eww-checkbox-map)
659 (insert " ")))
660
661(defun eww-form-text (cont)
662 (let ((start (point))
663 (type (downcase (or (cdr (assq :type cont))
664 "text")))
665 (value (or (cdr (assq :value cont)) ""))
666 (width (string-to-number
667 (or (cdr (assq :size cont))
668 "40"))))
669 (insert value)
670 (when (< (length value) width)
671 (insert (make-string (- width (length value)) ? )))
672 (put-text-property start (point) 'face 'eww-form-text)
673 (put-text-property start (point) 'local-map eww-text-map)
674 (put-text-property start (point) 'inhibit-read-only t)
675 (put-text-property start (point) 'eww-form
676 (list :eww-form eww-form
677 :value value
678 :type type
679 :name (cdr (assq :name cont))))
680 (insert " ")))
681
10240949
RS
682(defconst eww-text-input-types '("text" "password" "textarea"
683 "color" "date" "datetime" "datetime-local"
684 "email" "month" "number" "search" "tel"
685 "time" "url" "week")
686 "List of input types which represent a text input.
687See URL `https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input'.")
688
970ad972 689(defun eww-process-text-input (beg end length)
dfbc66e3 690 (let* ((form (get-text-property (min (1+ end) (point-max)) 'eww-form))
970ad972
G
691 (properties (text-properties-at end))
692 (type (plist-get form :type)))
693 (when (and form
10240949 694 (member type eww-text-input-types))
970ad972
G
695 (cond
696 ((zerop length)
697 ;; Delete some space at the end.
698 (save-excursion
699 (goto-char
700 (if (equal type "textarea")
701 (1- (line-end-position))
702 (eww-end-of-field)))
703 (let ((new (- end beg)))
704 (while (and (> new 0)
705 (eql (following-char) ? ))
706 (delete-region (point) (1+ (point)))
707 (setq new (1- new))))
708 (set-text-properties beg end properties)))
709 ((> length 0)
710 ;; Add padding.
711 (save-excursion
712 (goto-char
713 (if (equal type "textarea")
714 (1- (line-end-position))
715 (eww-end-of-field)))
716 (let ((start (point)))
717 (insert (make-string length ? ))
718 (set-text-properties start (point) properties)))))
719 (let ((value (buffer-substring-no-properties
720 (eww-beginning-of-field)
721 (eww-end-of-field))))
722 (when (string-match " +\\'" value)
723 (setq value (substring value 0 (match-beginning 0))))
724 (plist-put form :value value)
725 (when (equal type "password")
726 ;; Display passwords as asterisks.
727 (let ((start (eww-beginning-of-field)))
728 (put-text-property start (+ start (length value))
729 'display (make-string (length value) ?*))))))))
9ddf23f0 730
c74cb344 731(defun eww-tag-textarea (cont)
970ad972
G
732 (let ((start (point))
733 (value (or (cdr (assq :value cont)) ""))
734 (lines (string-to-number
735 (or (cdr (assq :rows cont))
736 "10")))
737 (width (string-to-number
738 (or (cdr (assq :cols cont))
739 "10")))
740 end)
741 (shr-ensure-newline)
742 (insert value)
743 (shr-ensure-newline)
744 (when (< (count-lines start (point)) lines)
745 (dotimes (i (- lines (count-lines start (point))))
746 (insert "\n")))
747 (setq end (point-marker))
748 (goto-char start)
749 (while (< (point) end)
750 (end-of-line)
751 (let ((pad (- width (- (point) (line-beginning-position)))))
752 (when (> pad 0)
753 (insert (make-string pad ? ))))
754 (add-face-text-property (line-beginning-position)
755 (point) 'eww-form-text)
756 (put-text-property (line-beginning-position) (point)
757 'local-map eww-textarea-map)
758 (forward-line 1))
759 (put-text-property start (point) 'eww-form
760 (list :eww-form eww-form
761 :value value
762 :type "textarea"
763 :name (cdr (assq :name cont))))))
764
765(defun eww-tag-input (cont)
766 (let ((type (downcase (or (cdr (assq :type cont))
767 "text")))
768 (start (point)))
769 (cond
770 ((or (equal type "checkbox")
771 (equal type "radio"))
772 (eww-form-checkbox cont))
773 ((equal type "submit")
774 (eww-form-submit cont))
775 ((equal type "hidden")
776 (let ((form eww-form)
777 (name (cdr (assq :name cont))))
778 ;; Don't add <input type=hidden> elements repeatedly.
779 (while (and form
780 (or (not (consp (car form)))
781 (not (eq (caar form) 'hidden))
782 (not (equal (plist-get (cdr (car form)) :name)
783 name))))
784 (setq form (cdr form)))
785 (unless form
786 (nconc eww-form (list
787 (list 'hidden
788 :name name
789 :value (cdr (assq :value cont))))))))
790 (t
791 (eww-form-text cont)))
792 (unless (= start (point))
793 (put-text-property start (1+ start) 'help-echo "Input field"))))
c74cb344 794
9ddf23f0
LMI
795(defun eww-tag-select (cont)
796 (shr-ensure-paragraph)
970ad972 797 (let ((menu (list :name (cdr (assq :name cont))
9ddf23f0
LMI
798 :eww-form eww-form))
799 (options nil)
970ad972 800 (start (point))
9dd99753
KN
801 (max 0)
802 opelem)
803 (if (eq (car (car cont)) 'optgroup)
804 (dolist (groupelem cont)
805 (unless (cdr (assq :disabled (cdr groupelem)))
806 (setq opelem (append opelem (cdr (cdr groupelem))))))
807 (setq opelem cont))
808 (dolist (elem opelem)
9ddf23f0
LMI
809 (when (eq (car elem) 'option)
810 (when (cdr (assq :selected (cdr elem)))
811 (nconc menu (list :value
812 (cdr (assq :value (cdr elem))))))
970ad972
G
813 (let ((display (or (cdr (assq 'text (cdr elem))) "")))
814 (setq max (max max (length display)))
815 (push (list 'item
816 :value (cdr (assq :value (cdr elem)))
817 :display display)
818 options))))
be2aa135 819 (when options
970ad972 820 (setq options (nreverse options))
be2aa135 821 ;; If we have no selected values, default to the first value.
970ad972 822 (unless (plist-get menu :value)
be2aa135
LMI
823 (nconc menu (list :value (nth 2 (car options)))))
824 (nconc menu options)
970ad972
G
825 (let ((selected (eww-select-display menu)))
826 (insert selected
827 (make-string (- max (length selected)) ? )))
828 (put-text-property start (point) 'eww-form menu)
829 (add-face-text-property start (point) 'eww-form-select)
830 (put-text-property start (point) 'keymap eww-select-map)
be2aa135 831 (shr-ensure-paragraph))))
2644071e 832
970ad972
G
833(defun eww-select-display (select)
834 (let ((value (plist-get select :value))
835 display)
836 (dolist (elem select)
837 (when (and (consp elem)
838 (eq (car elem) 'item)
839 (equal value (plist-get (cdr elem) :value)))
840 (setq display (plist-get (cdr elem) :display))))
841 display))
842
843(defun eww-change-select ()
844 "Change the value of the select drop-down menu under point."
845 (interactive)
846 (let* ((input (get-text-property (point) 'eww-form))
970ad972
G
847 (completion-ignore-case t)
848 (options
849 (delq nil
850 (mapcar (lambda (elem)
851 (and (consp elem)
852 (eq (car elem) 'item)
853 (cons (plist-get (cdr elem) :display)
854 (plist-get (cdr elem) :value))))
855 input)))
856 (display
857 (completing-read "Change value: " options nil 'require-match))
858 (inhibit-read-only t))
859 (plist-put input :value (cdr (assoc-string display options t)))
860 (goto-char
861 (eww-update-field display))))
862
863(defun eww-update-field (string)
864 (let ((properties (text-properties-at (point)))
865 (start (eww-beginning-of-field))
866 (end (1+ (eww-end-of-field))))
867 (delete-region start end)
868 (insert string
869 (make-string (- (- end start) (length string)) ? ))
870 (set-text-properties start end properties)
871 start))
872
873(defun eww-toggle-checkbox ()
874 "Toggle the value of the checkbox under point."
875 (interactive)
876 (let* ((input (get-text-property (point) 'eww-form))
877 (type (plist-get input :type)))
878 (if (equal type "checkbox")
879 (goto-char
880 (1+
881 (if (plist-get input :checked)
882 (progn
883 (plist-put input :checked nil)
4570dd16 884 (eww-update-field eww-form-checkbox-symbol))
970ad972 885 (plist-put input :checked t)
4570dd16 886 (eww-update-field eww-form-checkbox-selected-symbol))))
970ad972
G
887 ;; Radio button. Switch all other buttons off.
888 (let ((name (plist-get input :name)))
889 (save-excursion
890 (dolist (elem (eww-inputs (plist-get input :eww-form)))
891 (when (equal (plist-get (cdr elem) :name) name)
892 (goto-char (car elem))
893 (if (not (eq (cdr elem) input))
894 (progn
895 (plist-put input :checked nil)
4570dd16 896 (eww-update-field eww-form-checkbox-symbol))
970ad972 897 (plist-put input :checked t)
4570dd16 898 (eww-update-field eww-form-checkbox-selected-symbol)))))
970ad972
G
899 (forward-char 1)))))
900
901(defun eww-inputs (form)
902 (let ((start (point-min))
903 (inputs nil))
904 (while (and start
905 (< start (point-max)))
906 (when (or (get-text-property start 'eww-form)
907 (setq start (next-single-property-change start 'eww-form)))
908 (when (eq (plist-get (get-text-property start 'eww-form) :eww-form)
909 form)
910 (push (cons start (get-text-property start 'eww-form))
911 inputs))
912 (setq start (next-single-property-change start 'eww-form))))
913 (nreverse inputs)))
914
915(defun eww-input-value (input)
916 (let ((type (plist-get input :type))
917 (value (plist-get input :value)))
918 (cond
919 ((equal type "textarea")
920 (with-temp-buffer
921 (insert value)
922 (goto-char (point-min))
923 (while (re-search-forward "^ +\n\\| +$" nil t)
924 (replace-match "" t t))
925 (buffer-string)))
926 (t
927 (if (string-match " +\\'" value)
928 (substring value 0 (match-beginning 0))
929 value)))))
930
931(defun eww-submit ()
932 "Submit the current form."
933 (interactive)
934 (let* ((this-input (get-text-property (point) 'eww-form))
935 (form (plist-get this-input :eww-form))
936 values next-submit)
937 (dolist (elem (sort (eww-inputs form)
938 (lambda (o1 o2)
939 (< (car o1) (car o2)))))
940 (let* ((input (cdr elem))
941 (input-start (car elem))
942 (name (plist-get input :name)))
943 (when name
944 (cond
945 ((member (plist-get input :type) '("checkbox" "radio"))
946 (when (plist-get input :checked)
947 (push (cons name (plist-get input :value))
948 values)))
949 ((equal (plist-get input :type) "submit")
950 ;; We want the values from buttons if we hit a button if
951 ;; we hit enter on it, or if it's the first button after
952 ;; the field we did hit return on.
953 (when (or (eq input this-input)
954 (and (not (eq input this-input))
955 (null next-submit)
956 (> input-start (point))))
957 (setq next-submit t)
958 (push (cons name (plist-get input :value))
959 values)))
960 (t
961 (push (cons name (eww-input-value input))
962 values))))))
f22255bd
LMI
963 (dolist (elem form)
964 (when (and (consp elem)
965 (eq (car elem) 'hidden))
966 (push (cons (plist-get (cdr elem) :name)
967 (plist-get (cdr elem) :value))
968 values)))
c74cb344
G
969 (if (and (stringp (cdr (assq :method form)))
970 (equal (downcase (cdr (assq :method form))) "post"))
971 (let ((url-request-method "POST")
972 (url-request-extra-headers
973 '(("Content-Type" . "application/x-www-form-urlencoded")))
974 (url-request-data (mm-url-encode-www-form-urlencoded values)))
975 (eww-browse-url (shr-expand-url (cdr (assq :action form))
976 eww-current-url)))
977 (eww-browse-url
978 (concat
979 (if (cdr (assq :action form))
980 (shr-expand-url (cdr (assq :action form))
981 eww-current-url)
982 eww-current-url)
983 "?"
984 (mm-url-encode-www-form-urlencoded values))))))
2644071e 985
b2afb3ea 986(defun eww-browse-with-external-browser (&optional url)
f865b474 987 "Browse the current URL with an external browser.
0ebd92a3 988The browser to used is specified by the `shr-external-browser' variable."
f865b474 989 (interactive)
b2afb3ea 990 (funcall shr-external-browser (or url eww-current-url)))
f865b474 991
513562a1
LMI
992(defun eww-follow-link (&optional external mouse-event)
993 "Browse the URL under point.
994If EXTERNAL, browse the URL using `shr-external-browser'."
995 (interactive (list current-prefix-arg last-nonmenu-event))
996 (mouse-set-point mouse-event)
997 (let ((url (get-text-property (point) 'shr-url)))
998 (cond
999 ((not url)
1000 (message "No link under point"))
1001 ((string-match "^mailto:" url)
1002 (browse-url-mail url))
1003 (external
1004 (funcall shr-external-browser url))
1005 ;; This is a #target url in the same page as the current one.
1006 ((and (url-target (url-generic-parse-url url))
1007 (eww-same-page-p url eww-current-url))
1008 (eww-save-history)
1009 (eww-display-html 'utf8 url eww-current-dom))
1010 (t
1011 (eww-browse-url url)))))
1012
1013(defun eww-same-page-p (url1 url2)
f224e500 1014 "Return non-nil if both URLs represent the same page.
513562a1
LMI
1015Differences in #targets are ignored."
1016 (let ((obj1 (url-generic-parse-url url1))
1017 (obj2 (url-generic-parse-url url2)))
1018 (setf (url-target obj1) nil)
1019 (setf (url-target obj2) nil)
1020 (equal (url-recreate-url obj1) (url-recreate-url obj2))))
1021
16f74f10 1022(defun eww-copy-page-url ()
b89fc156 1023 (interactive)
16f74f10 1024 (message "%s" eww-current-url)
b89fc156 1025 (kill-new eww-current-url))
16f74f10 1026
bfbc93a1
IK
1027(defun eww-download ()
1028 "Download URL under point to `eww-download-directory'."
1029 (interactive)
1030 (let ((url (get-text-property (point) 'shr-url)))
1031 (if (not url)
1032 (message "No URL under point")
1033 (url-retrieve url 'eww-download-callback (list url)))))
1034
1035(defun eww-download-callback (status url)
1036 (unless (plist-get status :error)
1037 (let* ((obj (url-generic-parse-url url))
1038 (path (car (url-path-and-query obj)))
1039 (file (eww-make-unique-file-name (file-name-nondirectory path)
1040 eww-download-path)))
1041 (write-file file)
1042 (message "Saved %s" file))))
1043
1044(defun eww-make-unique-file-name (file directory)
1045 (cond
1046 ((zerop (length file))
1047 (setq file "!"))
1048 ((string-match "\\`[.]" file)
1049 (setq file (concat "!" file))))
fde38d49 1050 (let ((count 1))
bfbc93a1
IK
1051 (while (file-exists-p (expand-file-name file directory))
1052 (setq file
1053 (if (string-match "\\`\\(.*\\)\\([.][^.]+\\)" file)
1054 (format "%s(%d)%s" (match-string 1 file)
1055 count (match-string 2 file))
1056 (format "%s(%d)" file count)))
1057 (setq count (1+ count)))
1058 (expand-file-name file directory)))
1059
2b4f0506
LMI
1060;;; Bookmarks code
1061
1062(defvar eww-bookmarks nil)
1063
1064(defun eww-add-bookmark ()
1065 "Add the current page to the bookmarks."
1066 (interactive)
1067 (eww-read-bookmarks)
1068 (dolist (bookmark eww-bookmarks)
1069 (when (equal eww-current-url
1070 (plist-get bookmark :url))
1071 (error "Already bookmarked")))
e47112ee
TZ
1072 (if (y-or-n-p "bookmark this page? ")
1073 (progn
1074 (let ((title (replace-regexp-in-string "[\n\t\r]" " " eww-current-title)))
1075 (setq title (replace-regexp-in-string "\\` +\\| +\\'" "" title))
1076 (push (list :url eww-current-url
1077 :title title
1078 :time (current-time-string))
1079 eww-bookmarks))
1080 (eww-write-bookmarks)
1081 (message "Bookmarked %s (%s)" eww-current-url eww-current-title))))
2b4f0506
LMI
1082
1083(defun eww-write-bookmarks ()
1084 (with-temp-file (expand-file-name "eww-bookmarks" user-emacs-directory)
1085 (insert ";; Auto-generated file; don't edit\n")
1086 (pp eww-bookmarks (current-buffer))))
1087
1088(defun eww-read-bookmarks ()
99906aa0
LL
1089 (let ((file (expand-file-name "eww-bookmarks" user-emacs-directory)))
1090 (setq eww-bookmarks
1091 (unless (zerop (or (nth 7 (file-attributes file)) 0))
1092 (with-temp-buffer
1093 (insert-file-contents file)
1094 (read (current-buffer)))))))
2b4f0506
LMI
1095
1096(defun eww-list-bookmarks ()
1097 "Display the bookmarks."
1098 (interactive)
1099 (eww-bookmark-prepare)
1100 (pop-to-buffer "*eww bookmarks*"))
1101
1102(defun eww-bookmark-prepare ()
1103 (eww-read-bookmarks)
1104 (when (null eww-bookmarks)
1105 (error "No bookmarks are defined"))
1106 (set-buffer (get-buffer-create "*eww bookmarks*"))
1107 (eww-bookmark-mode)
1108 (let ((format "%-40s %s")
1109 (inhibit-read-only t)
1110 start url)
1111 (erase-buffer)
1112 (setq header-line-format (concat " " (format format "URL" "Title")))
1113 (dolist (bookmark eww-bookmarks)
1114 (setq start (point))
1115 (setq url (plist-get bookmark :url))
1116 (when (> (length url) 40)
1117 (setq url (substring url 0 40)))
1118 (insert (format format url
1119 (plist-get bookmark :title))
1120 "\n")
1121 (put-text-property start (1+ start) 'eww-bookmark bookmark))
1122 (goto-char (point-min))))
1123
1124(defvar eww-bookmark-kill-ring nil)
1125
1126(defun eww-bookmark-kill ()
1127 "Kill the current bookmark."
1128 (interactive)
1129 (let* ((start (line-beginning-position))
1130 (bookmark (get-text-property start 'eww-bookmark))
1131 (inhibit-read-only t))
1132 (unless bookmark
1133 (error "No bookmark on the current line"))
1134 (forward-line 1)
1135 (push (buffer-substring start (point)) eww-bookmark-kill-ring)
1136 (delete-region start (point))
1137 (setq eww-bookmarks (delq bookmark eww-bookmarks))
1138 (eww-write-bookmarks)))
1139
1140(defun eww-bookmark-yank ()
1141 "Yank a previously killed bookmark to the current line."
1142 (interactive)
1143 (unless eww-bookmark-kill-ring
1144 (error "No previously killed bookmark"))
1145 (beginning-of-line)
1146 (let ((inhibit-read-only t)
1147 (start (point))
1148 bookmark)
1149 (insert (pop eww-bookmark-kill-ring))
1150 (setq bookmark (get-text-property start 'eww-bookmark))
1151 (if (= start (point-min))
1152 (push bookmark eww-bookmarks)
1153 (let ((line (count-lines start (point))))
1154 (setcdr (nthcdr (1- line) eww-bookmarks)
1155 (cons bookmark (nthcdr line eww-bookmarks)))))
1156 (eww-write-bookmarks)))
1157
1158(defun eww-bookmark-quit ()
1159 "Kill the current buffer."
1160 (interactive)
1161 (kill-buffer (current-buffer)))
1162
1163(defun eww-bookmark-browse ()
1164 "Browse the bookmark under point in eww."
1165 (interactive)
1166 (let ((bookmark (get-text-property (line-beginning-position) 'eww-bookmark)))
1167 (unless bookmark
1168 (error "No bookmark on the current line"))
47fd571b
LMI
1169 ;; We wish to leave this window, but if it's the only window here,
1170 ;; just let it remain.
1171 (ignore-errors
1172 (delete-window))
e47112ee 1173 (eww-browse-url (plist-get bookmark :url))))
2b4f0506
LMI
1174
1175(defun eww-next-bookmark ()
1176 "Go to the next bookmark in the list."
1177 (interactive)
1178 (let ((first nil)
1179 bookmark)
1180 (unless (get-buffer "*eww bookmarks*")
1181 (setq first t)
1182 (eww-bookmark-prepare))
1183 (with-current-buffer (get-buffer "*eww bookmarks*")
1184 (when (and (not first)
1185 (not (eobp)))
1186 (forward-line 1))
1187 (setq bookmark (get-text-property (line-beginning-position)
1188 'eww-bookmark))
1189 (unless bookmark
1190 (error "No next bookmark")))
1191 (eww-browse-url (plist-get bookmark :url))))
1192
1193(defun eww-previous-bookmark ()
1194 "Go to the previous bookmark in the list."
1195 (interactive)
1196 (let ((first nil)
1197 bookmark)
1198 (unless (get-buffer "*eww bookmarks*")
1199 (setq first t)
1200 (eww-bookmark-prepare))
1201 (with-current-buffer (get-buffer "*eww bookmarks*")
1202 (if first
1203 (goto-char (point-max))
1204 (beginning-of-line))
1205 ;; On the final line.
1206 (when (eolp)
1207 (forward-line -1))
1208 (if (bobp)
1209 (error "No previous bookmark")
1210 (forward-line -1))
1211 (setq bookmark (get-text-property (line-beginning-position)
1212 'eww-bookmark)))
1213 (eww-browse-url (plist-get bookmark :url))))
1214
1215(defvar eww-bookmark-mode-map
1216 (let ((map (make-sparse-keymap)))
1217 (suppress-keymap map)
1218 (define-key map "q" 'eww-bookmark-quit)
1219 (define-key map [(control k)] 'eww-bookmark-kill)
1220 (define-key map [(control y)] 'eww-bookmark-yank)
1221 (define-key map "\r" 'eww-bookmark-browse)
1222 map))
1223
1224(define-derived-mode eww-bookmark-mode nil "eww bookmarks"
1225 "Mode for listing bookmarks.
1226
1227\\{eww-bookmark-mode-map}"
1228 (buffer-disable-undo)
1229 (setq buffer-read-only t
1230 truncate-lines t))
1231
266c63b5
AK
1232(provide 'eww)
1233
1234;;; eww.el ends here