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