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