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