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