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