* net/eww.el (eww): Add a trailing slash to domain names.
[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
970ad972
G
53(defface eww-form-submit
54 '((((type x w32 ns) (class color)) ; Like default mode line
55 :box (:line-width 2 :style released-button)
56 :background "#808080" :foreground "black"))
57 "Face for eww buffer buttons."
58 :version "24.4"
59 :group 'eww)
60
61(defface eww-form-checkbox
62 '((((type x w32 ns) (class color)) ; Like default mode line
63 :box (:line-width 2 :style released-button)
64 :background "lightgrey" :foreground "black"))
65 "Face for eww buffer buttons."
66 :version "24.4"
67 :group 'eww)
68
69(defface eww-form-select
be2aa135
LMI
70 '((((type x w32 ns) (class color)) ; Like default mode line
71 :box (:line-width 2 :style released-button)
72 :background "lightgrey" :foreground "black"))
73 "Face for eww buffer buttons."
74 :version "24.4"
75 :group 'eww)
76
970ad972
G
77(defface eww-form-text
78 '((t (:background "#505050"
79 :foreground "white"
80 :box (:line-width 1))))
81 "Face for eww text inputs."
82 :version "24.4"
83 :group 'eww)
84
266c63b5 85(defvar eww-current-url nil)
c74cb344
G
86(defvar eww-current-title ""
87 "Title of current page.")
266c63b5
AK
88(defvar eww-history nil)
89
924d6997
G
90(defvar eww-next-url nil)
91(defvar eww-previous-url nil)
92(defvar eww-up-url nil)
970ad972
G
93(defvar eww-home-url nil)
94(defvar eww-start-url nil)
95(defvar eww-contents-url nil)
924d6997 96
d583b36b 97;;;###autoload
266c63b5 98(defun eww (url)
a3ca09b9
IK
99 "Fetch URL and render the page.
100If the input doesn't look like an URL or a domain name, the
101word(s) will be searched for via `eww-search-prefix'."
102 (interactive "sEnter URL or keywords: ")
103 (if (and (= (length (split-string url)) 1)
104 (> (length (split-string url "\\.")) 1))
71d4c19d
IK
105 (progn
106 (unless (string-match-p "\\`[a-zA-Z][-a-zA-Z0-9+.]*://" url)
107 (setq url (concat "http://" url)))
108 ;; some site don't redirect final /
109 (when (string= (url-filename (url-generic-parse-url url)) "")
110 (setq url (concat url "/"))))
b89fc156
IK
111 (unless (string-match-p "^file:" url)
112 (setq url (concat eww-search-prefix
113 (replace-regexp-in-string " " "+" url)))))
266c63b5
AK
114 (url-retrieve url 'eww-render (list url)))
115
924d6997
G
116;;;###autoload
117(defun eww-open-file (file)
118 "Render a file using EWW."
119 (interactive "fFile: ")
120 (eww (concat "file://" (expand-file-name file))))
121
266c63b5 122(defun eww-render (status url &optional point)
c74cb344
G
123 (let ((redirect (plist-get status :redirect)))
124 (when redirect
125 (setq url redirect)))
924d6997
G
126 (set (make-local-variable 'eww-next-url) nil)
127 (set (make-local-variable 'eww-previous-url) nil)
128 (set (make-local-variable 'eww-up-url) nil)
970ad972
G
129 (set (make-local-variable 'eww-home-url) nil)
130 (set (make-local-variable 'eww-start-url) nil)
131 (set (make-local-variable 'eww-contents-url) nil)
266c63b5 132 (let* ((headers (eww-parse-headers))
c74cb344
G
133 (shr-target-id
134 (and (string-match "#\\(.*\\)" url)
135 (match-string 1 url)))
266c63b5
AK
136 (content-type
137 (mail-header-parse-content-type
138 (or (cdr (assoc "content-type" headers))
139 "text/plain")))
140 (charset (intern
141 (downcase
142 (or (cdr (assq 'charset (cdr content-type)))
d652f4d0
G
143 (eww-detect-charset (equal (car content-type)
144 "text/html"))
266c63b5
AK
145 "utf8"))))
146 (data-buffer (current-buffer)))
147 (unwind-protect
148 (progn
149 (cond
150 ((equal (car content-type) "text/html")
151 (eww-display-html charset url))
152 ((string-match "^image/" (car content-type))
153 (eww-display-image))
154 (t
155 (eww-display-raw charset)))
c74cb344
G
156 (cond
157 (point
158 (goto-char point))
159 (shr-target-id
160 (let ((point (next-single-property-change
161 (point-min) 'shr-target-id)))
162 (when point
163 (goto-char (1+ point)))))))
266c63b5
AK
164 (kill-buffer data-buffer))))
165
166(defun eww-parse-headers ()
167 (let ((headers nil))
d652f4d0 168 (goto-char (point-min))
266c63b5
AK
169 (while (and (not (eobp))
170 (not (eolp)))
171 (when (looking-at "\\([^:]+\\): *\\(.*\\)")
172 (push (cons (downcase (match-string 1))
173 (match-string 2))
174 headers))
175 (forward-line 1))
176 (unless (eobp)
177 (forward-line 1))
178 headers))
179
db5a34ca
KY
180(defun eww-detect-charset (html-p)
181 (let ((case-fold-search t)
182 (pt (point)))
183 (or (and html-p
184 (re-search-forward
b89fc156 185 "<meta[\t\n\r ]+[^>]*charset=\"?\\([^\t\n\r \"/>]+\\)[\\\"'.*]" nil t)
db5a34ca
KY
186 (goto-char pt)
187 (match-string 1))
188 (and (looking-at
189 "[\t\n\r ]*<\\?xml[\t\n\r ]+[^>]*encoding=\"\\([^\"]+\\)")
190 (match-string 1)))))
191
266c63b5
AK
192(defun eww-display-html (charset url)
193 (unless (eq charset 'utf8)
194 (decode-coding-region (point) (point-max) charset))
195 (let ((document
196 (list
197 'base (list (cons 'href url))
198 (libxml-parse-html-region (point) (point-max)))))
199 (eww-setup-buffer)
200 (setq eww-current-url url)
c74cb344 201 (eww-update-header-line-format)
2644071e 202 (let ((inhibit-read-only t)
970ad972 203 (after-change-functions nil)
c74cb344 204 (shr-width nil)
2644071e 205 (shr-external-rendering-functions
c74cb344
G
206 '((title . eww-tag-title)
207 (form . eww-tag-form)
2644071e 208 (input . eww-tag-input)
c74cb344
G
209 (textarea . eww-tag-textarea)
210 (body . eww-tag-body)
924d6997
G
211 (select . eww-tag-select)
212 (link . eww-tag-link)
213 (a . eww-tag-a))))
970ad972 214 (shr-insert-document document))
266c63b5
AK
215 (goto-char (point-min))))
216
924d6997
G
217(defun eww-handle-link (cont)
218 (let* ((rel (assq :rel cont))
219 (href (assq :href cont))
970ad972
G
220 (where (assoc
221 ;; The text associated with :rel is case-insensitive.
222 (if rel (downcase (cdr rel)))
924d6997 223 '(("next" . eww-next-url)
970ad972
G
224 ;; Texinfo uses "previous", but HTML specifies
225 ;; "prev", so recognize both.
924d6997 226 ("previous" . eww-previous-url)
970ad972
G
227 ("prev" . eww-previous-url)
228 ;; HTML specifies "start" but also "contents",
229 ;; and Gtk seems to use "home". Recognize
230 ;; them all; but store them in different
231 ;; variables so that we can readily choose the
232 ;; "best" one.
233 ("start" . eww-start-url)
234 ("home" . eww-home-url)
235 ("contents" . eww-contents-url)
924d6997
G
236 ("up" . eww-up-url)))))
237 (and href
238 where
239 (set (cdr where) (cdr href)))))
240
241(defun eww-tag-link (cont)
242 (eww-handle-link cont)
243 (shr-generic cont))
244
245(defun eww-tag-a (cont)
246 (eww-handle-link cont)
247 (shr-tag-a cont))
248
c74cb344
G
249(defun eww-update-header-line-format ()
250 (if eww-header-line-format
d80a808f
LMI
251 (setq header-line-format
252 (replace-regexp-in-string
253 "%" "%%"
254 (format-spec eww-header-line-format
255 `((?u . ,eww-current-url)
256 (?t . ,eww-current-title)))))
c74cb344
G
257 (setq header-line-format nil)))
258
259(defun eww-tag-title (cont)
260 (setq eww-current-title "")
261 (dolist (sub cont)
262 (when (eq (car sub) 'text)
263 (setq eww-current-title (concat eww-current-title (cdr sub)))))
264 (eww-update-header-line-format))
265
266(defun eww-tag-body (cont)
267 (let* ((start (point))
268 (fgcolor (cdr (or (assq :fgcolor cont)
269 (assq :text cont))))
270 (bgcolor (cdr (assq :bgcolor cont)))
271 (shr-stylesheet (list (cons 'color fgcolor)
272 (cons 'background-color bgcolor))))
273 (shr-generic cont)
274 (eww-colorize-region start (point) fgcolor bgcolor)))
275
276(defun eww-colorize-region (start end fg &optional bg)
277 (when (or fg bg)
278 (let ((new-colors (shr-color-check fg bg)))
279 (when new-colors
280 (when fg
544d4594 281 (add-face-text-property start end
970ad972
G
282 (list :foreground (cadr new-colors))
283 t))
c74cb344 284 (when bg
544d4594 285 (add-face-text-property start end
970ad972
G
286 (list :background (car new-colors))
287 t))))))
c74cb344 288
266c63b5
AK
289(defun eww-display-raw (charset)
290 (let ((data (buffer-substring (point) (point-max))))
291 (eww-setup-buffer)
292 (let ((inhibit-read-only t))
293 (insert data))
294 (goto-char (point-min))))
295
296(defun eww-display-image ()
297 (let ((data (buffer-substring (point) (point-max))))
298 (eww-setup-buffer)
299 (let ((inhibit-read-only t))
300 (shr-put-image data nil))
301 (goto-char (point-min))))
302
303(defun eww-setup-buffer ()
304 (pop-to-buffer (get-buffer-create "*eww*"))
2644071e 305 (remove-overlays)
266c63b5
AK
306 (let ((inhibit-read-only t))
307 (erase-buffer))
308 (eww-mode))
309
310(defvar eww-mode-map
311 (let ((map (make-sparse-keymap)))
312 (suppress-keymap map)
313 (define-key map "q" 'eww-quit)
f22255bd 314 (define-key map "g" 'eww-reload)
7304e4dd
LMI
315 (define-key map [tab] 'shr-next-link)
316 (define-key map [backtab] 'shr-previous-link)
266c63b5
AK
317 (define-key map [delete] 'scroll-down-command)
318 (define-key map "\177" 'scroll-down-command)
319 (define-key map " " 'scroll-up-command)
924d6997
G
320 (define-key map "l" 'eww-back-url)
321 (define-key map "n" 'eww-next-url)
266c63b5 322 (define-key map "p" 'eww-previous-url)
924d6997
G
323 (define-key map "u" 'eww-up-url)
324 (define-key map "t" 'eww-top-url)
f865b474 325 (define-key map "w" 'eww-browse-with-external-browser)
b89fc156 326 (define-key map "y" 'eww-yank-page-url)
266c63b5
AK
327 map))
328
d652f4d0 329(define-derived-mode eww-mode nil "eww"
266c63b5
AK
330 "Mode for browsing the web.
331
332\\{eww-mode-map}"
266c63b5 333 (set (make-local-variable 'eww-current-url) 'author)
970ad972
G
334 (set (make-local-variable 'browse-url-browser-function) 'eww-browse-url)
335 (set (make-local-variable 'after-change-functions) 'eww-process-text-input)
336 ;;(setq buffer-read-only t)
337 )
266c63b5
AK
338
339(defun eww-browse-url (url &optional new-window)
970ad972
G
340 (when (and (equal major-mode 'eww-mode)
341 eww-current-url)
342 (push (list eww-current-url (point))
343 eww-history))
5c3087e9 344 (eww url))
266c63b5
AK
345
346(defun eww-quit ()
347 "Exit the Emacs Web Wowser."
348 (interactive)
349 (setq eww-history nil)
350 (kill-buffer (current-buffer)))
351
924d6997 352(defun eww-back-url ()
266c63b5
AK
353 "Go to the previously displayed page."
354 (interactive)
355 (when (zerop (length eww-history))
356 (error "No previous page"))
357 (let ((prev (pop eww-history)))
358 (url-retrieve (car prev) 'eww-render (list (car prev) (cadr prev)))))
359
924d6997
G
360(defun eww-next-url ()
361 "Go to the page marked `next'.
362A page is marked `next' if rel=\"next\" appears in a <link>
363or <a> tag."
364 (interactive)
365 (if eww-next-url
366 (eww-browse-url (shr-expand-url eww-next-url eww-current-url))
367 (error "No `next' on this page")))
368
369(defun eww-previous-url ()
370 "Go to the page marked `previous'.
371A page is marked `previous' if rel=\"previous\" appears in a <link>
372or <a> tag."
373 (interactive)
374 (if eww-previous-url
375 (eww-browse-url (shr-expand-url eww-previous-url eww-current-url))
376 (error "No `previous' on this page")))
377
378(defun eww-up-url ()
379 "Go to the page marked `up'.
380A page is marked `up' if rel=\"up\" appears in a <link>
381or <a> tag."
382 (interactive)
383 (if eww-up-url
384 (eww-browse-url (shr-expand-url eww-up-url eww-current-url))
385 (error "No `up' on this page")))
386
387(defun eww-top-url ()
388 "Go to the page marked `top'.
970ad972
G
389A page is marked `top' if rel=\"start\", rel=\"home\", or rel=\"contents\"
390appears in a <link> or <a> tag."
924d6997 391 (interactive)
970ad972
G
392 (let ((best-url (or eww-start-url
393 eww-contents-url
394 eww-home-url)))
395 (if best-url
396 (eww-browse-url (shr-expand-url best-url eww-current-url))
397 (error "No `top' for this page"))))
924d6997 398
f22255bd
LMI
399(defun eww-reload ()
400 "Reload the current page."
401 (interactive)
402 (url-retrieve eww-current-url 'eww-render
403 (list eww-current-url (point))))
404
2644071e
LMI
405;; Form support.
406
407(defvar eww-form nil)
408
970ad972
G
409(defvar eww-submit-map
410 (let ((map (make-sparse-keymap)))
411 (define-key map "\r" 'eww-submit)
e854cfc7 412 (define-key map [(control c) (control c)] 'eww-submit)
970ad972
G
413 map))
414
415(defvar eww-checkbox-map
416 (let ((map (make-sparse-keymap)))
417 (define-key map [space] 'eww-toggle-checkbox)
418 (define-key map "\r" 'eww-toggle-checkbox)
e854cfc7 419 (define-key map [(control c) (control c)] 'eww-submit)
970ad972
G
420 map))
421
422(defvar eww-text-map
423 (let ((map (make-keymap)))
424 (set-keymap-parent map text-mode-map)
425 (define-key map "\r" 'eww-submit)
426 (define-key map [(control a)] 'eww-beginning-of-text)
e854cfc7 427 (define-key map [(control c) (control c)] 'eww-submit)
970ad972
G
428 (define-key map [(control e)] 'eww-end-of-text)
429 (define-key map [tab] 'shr-next-link)
430 (define-key map [backtab] 'shr-previous-link)
431 map))
432
433(defvar eww-textarea-map
434 (let ((map (make-keymap)))
435 (set-keymap-parent map text-mode-map)
436 (define-key map "\r" 'forward-line)
e854cfc7 437 (define-key map [(control c) (control c)] 'eww-submit)
970ad972
G
438 (define-key map [tab] 'shr-next-link)
439 (define-key map [backtab] 'shr-previous-link)
440 map))
441
442(defvar eww-select-map
443 (let ((map (make-sparse-keymap)))
444 (define-key map "\r" 'eww-change-select)
e854cfc7 445 (define-key map [(control c) (control c)] 'eww-submit)
970ad972
G
446 map))
447
448(defun eww-beginning-of-text ()
449 "Move to the start of the input field."
450 (interactive)
451 (goto-char (eww-beginning-of-field)))
452
453(defun eww-end-of-text ()
454 "Move to the end of the text in the input field."
455 (interactive)
456 (goto-char (eww-end-of-field))
457 (let ((start (eww-beginning-of-field)))
458 (while (and (equal (following-char) ? )
459 (> (point) start))
460 (forward-char -1))
461 (when (> (point) start)
462 (forward-char 1))))
463
464(defun eww-beginning-of-field ()
465 (cond
466 ((bobp)
467 (point))
468 ((not (eq (get-text-property (point) 'eww-form)
469 (get-text-property (1- (point)) 'eww-form)))
470 (point))
471 (t
472 (previous-single-property-change
473 (point) 'eww-form nil (point-min)))))
474
475(defun eww-end-of-field ()
476 (1- (next-single-property-change
477 (point) 'eww-form nil (point-max))))
478
2644071e
LMI
479(defun eww-tag-form (cont)
480 (let ((eww-form
481 (list (assq :method cont)
482 (assq :action cont)))
483 (start (point)))
484 (shr-ensure-paragraph)
485 (shr-generic cont)
3d95242e
LMI
486 (unless (bolp)
487 (insert "\n"))
488 (insert "\n")
001b9fbe
LMI
489 (when (> (point) start)
490 (put-text-property start (1+ start)
491 'eww-form eww-form))))
2644071e 492
970ad972
G
493(defun eww-form-submit (cont)
494 (let ((start (point))
495 (value (cdr (assq :value cont))))
496 (setq value
497 (if (zerop (length value))
498 "Submit"
499 value))
500 (insert value)
501 (add-face-text-property start (point) 'eww-form-submit)
502 (put-text-property start (point) 'eww-form
503 (list :eww-form eww-form
504 :value value
505 :type "submit"
506 :name (cdr (assq :name cont))))
507 (put-text-property start (point) 'keymap eww-submit-map)
508 (insert " ")))
509
510(defun eww-form-checkbox (cont)
511 (let ((start (point)))
512 (if (cdr (assq :checked cont))
513 (insert "[X]")
514 (insert "[ ]"))
515 (add-face-text-property start (point) 'eww-form-checkbox)
516 (put-text-property start (point) 'eww-form
517 (list :eww-form eww-form
518 :value (cdr (assq :value cont))
519 :type (downcase (cdr (assq :type cont)))
520 :checked (cdr (assq :checked cont))
521 :name (cdr (assq :name cont))))
522 (put-text-property start (point) 'keymap eww-checkbox-map)
523 (insert " ")))
524
525(defun eww-form-text (cont)
526 (let ((start (point))
527 (type (downcase (or (cdr (assq :type cont))
528 "text")))
529 (value (or (cdr (assq :value cont)) ""))
530 (width (string-to-number
531 (or (cdr (assq :size cont))
532 "40"))))
533 (insert value)
534 (when (< (length value) width)
535 (insert (make-string (- width (length value)) ? )))
536 (put-text-property start (point) 'face 'eww-form-text)
537 (put-text-property start (point) 'local-map eww-text-map)
538 (put-text-property start (point) 'inhibit-read-only t)
539 (put-text-property start (point) 'eww-form
540 (list :eww-form eww-form
541 :value value
542 :type type
543 :name (cdr (assq :name cont))))
544 (insert " ")))
545
546(defun eww-process-text-input (beg end length)
547 (let* ((form (get-text-property end 'eww-form))
548 (properties (text-properties-at end))
549 (type (plist-get form :type)))
550 (when (and form
551 (member type '("text" "password" "textarea")))
552 (cond
553 ((zerop length)
554 ;; Delete some space at the end.
555 (save-excursion
556 (goto-char
557 (if (equal type "textarea")
558 (1- (line-end-position))
559 (eww-end-of-field)))
560 (let ((new (- end beg)))
561 (while (and (> new 0)
562 (eql (following-char) ? ))
563 (delete-region (point) (1+ (point)))
564 (setq new (1- new))))
565 (set-text-properties beg end properties)))
566 ((> length 0)
567 ;; Add padding.
568 (save-excursion
569 (goto-char
570 (if (equal type "textarea")
571 (1- (line-end-position))
572 (eww-end-of-field)))
573 (let ((start (point)))
574 (insert (make-string length ? ))
575 (set-text-properties start (point) properties)))))
576 (let ((value (buffer-substring-no-properties
577 (eww-beginning-of-field)
578 (eww-end-of-field))))
579 (when (string-match " +\\'" value)
580 (setq value (substring value 0 (match-beginning 0))))
581 (plist-put form :value value)
582 (when (equal type "password")
583 ;; Display passwords as asterisks.
584 (let ((start (eww-beginning-of-field)))
585 (put-text-property start (+ start (length value))
586 'display (make-string (length value) ?*))))))))
9ddf23f0 587
c74cb344 588(defun eww-tag-textarea (cont)
970ad972
G
589 (let ((start (point))
590 (value (or (cdr (assq :value cont)) ""))
591 (lines (string-to-number
592 (or (cdr (assq :rows cont))
593 "10")))
594 (width (string-to-number
595 (or (cdr (assq :cols cont))
596 "10")))
597 end)
598 (shr-ensure-newline)
599 (insert value)
600 (shr-ensure-newline)
601 (when (< (count-lines start (point)) lines)
602 (dotimes (i (- lines (count-lines start (point))))
603 (insert "\n")))
604 (setq end (point-marker))
605 (goto-char start)
606 (while (< (point) end)
607 (end-of-line)
608 (let ((pad (- width (- (point) (line-beginning-position)))))
609 (when (> pad 0)
610 (insert (make-string pad ? ))))
611 (add-face-text-property (line-beginning-position)
612 (point) 'eww-form-text)
613 (put-text-property (line-beginning-position) (point)
614 'local-map eww-textarea-map)
615 (forward-line 1))
616 (put-text-property start (point) 'eww-form
617 (list :eww-form eww-form
618 :value value
619 :type "textarea"
620 :name (cdr (assq :name cont))))))
621
622(defun eww-tag-input (cont)
623 (let ((type (downcase (or (cdr (assq :type cont))
624 "text")))
625 (start (point)))
626 (cond
627 ((or (equal type "checkbox")
628 (equal type "radio"))
629 (eww-form-checkbox cont))
630 ((equal type "submit")
631 (eww-form-submit cont))
632 ((equal type "hidden")
633 (let ((form eww-form)
634 (name (cdr (assq :name cont))))
635 ;; Don't add <input type=hidden> elements repeatedly.
636 (while (and form
637 (or (not (consp (car form)))
638 (not (eq (caar form) 'hidden))
639 (not (equal (plist-get (cdr (car form)) :name)
640 name))))
641 (setq form (cdr form)))
642 (unless form
643 (nconc eww-form (list
644 (list 'hidden
645 :name name
646 :value (cdr (assq :value cont))))))))
647 (t
648 (eww-form-text cont)))
649 (unless (= start (point))
650 (put-text-property start (1+ start) 'help-echo "Input field"))))
c74cb344 651
9ddf23f0
LMI
652(defun eww-tag-select (cont)
653 (shr-ensure-paragraph)
970ad972 654 (let ((menu (list :name (cdr (assq :name cont))
9ddf23f0
LMI
655 :eww-form eww-form))
656 (options nil)
970ad972
G
657 (start (point))
658 (max 0))
9ddf23f0
LMI
659 (dolist (elem cont)
660 (when (eq (car elem) 'option)
661 (when (cdr (assq :selected (cdr elem)))
662 (nconc menu (list :value
663 (cdr (assq :value (cdr elem))))))
970ad972
G
664 (let ((display (or (cdr (assq 'text (cdr elem))) "")))
665 (setq max (max max (length display)))
666 (push (list 'item
667 :value (cdr (assq :value (cdr elem)))
668 :display display)
669 options))))
be2aa135 670 (when options
970ad972 671 (setq options (nreverse options))
be2aa135 672 ;; If we have no selected values, default to the first value.
970ad972 673 (unless (plist-get menu :value)
be2aa135
LMI
674 (nconc menu (list :value (nth 2 (car options)))))
675 (nconc menu options)
970ad972
G
676 (let ((selected (eww-select-display menu)))
677 (insert selected
678 (make-string (- max (length selected)) ? )))
679 (put-text-property start (point) 'eww-form menu)
680 (add-face-text-property start (point) 'eww-form-select)
681 (put-text-property start (point) 'keymap eww-select-map)
be2aa135 682 (shr-ensure-paragraph))))
2644071e 683
970ad972
G
684(defun eww-select-display (select)
685 (let ((value (plist-get select :value))
686 display)
687 (dolist (elem select)
688 (when (and (consp elem)
689 (eq (car elem) 'item)
690 (equal value (plist-get (cdr elem) :value)))
691 (setq display (plist-get (cdr elem) :display))))
692 display))
693
694(defun eww-change-select ()
695 "Change the value of the select drop-down menu under point."
696 (interactive)
697 (let* ((input (get-text-property (point) 'eww-form))
698 (properties (text-properties-at (point)))
699 (completion-ignore-case t)
700 (options
701 (delq nil
702 (mapcar (lambda (elem)
703 (and (consp elem)
704 (eq (car elem) 'item)
705 (cons (plist-get (cdr elem) :display)
706 (plist-get (cdr elem) :value))))
707 input)))
708 (display
709 (completing-read "Change value: " options nil 'require-match))
710 (inhibit-read-only t))
711 (plist-put input :value (cdr (assoc-string display options t)))
712 (goto-char
713 (eww-update-field display))))
714
715(defun eww-update-field (string)
716 (let ((properties (text-properties-at (point)))
717 (start (eww-beginning-of-field))
718 (end (1+ (eww-end-of-field))))
719 (delete-region start end)
720 (insert string
721 (make-string (- (- end start) (length string)) ? ))
722 (set-text-properties start end properties)
723 start))
724
725(defun eww-toggle-checkbox ()
726 "Toggle the value of the checkbox under point."
727 (interactive)
728 (let* ((input (get-text-property (point) 'eww-form))
729 (type (plist-get input :type)))
730 (if (equal type "checkbox")
731 (goto-char
732 (1+
733 (if (plist-get input :checked)
734 (progn
735 (plist-put input :checked nil)
736 (eww-update-field "[ ]"))
737 (plist-put input :checked t)
738 (eww-update-field "[X]"))))
739 ;; Radio button. Switch all other buttons off.
740 (let ((name (plist-get input :name)))
741 (save-excursion
742 (dolist (elem (eww-inputs (plist-get input :eww-form)))
743 (when (equal (plist-get (cdr elem) :name) name)
744 (goto-char (car elem))
745 (if (not (eq (cdr elem) input))
746 (progn
747 (plist-put input :checked nil)
748 (eww-update-field "[ ]"))
749 (plist-put input :checked t)
750 (eww-update-field "[X]")))))
751 (forward-char 1)))))
752
753(defun eww-inputs (form)
754 (let ((start (point-min))
755 (inputs nil))
756 (while (and start
757 (< start (point-max)))
758 (when (or (get-text-property start 'eww-form)
759 (setq start (next-single-property-change start 'eww-form)))
760 (when (eq (plist-get (get-text-property start 'eww-form) :eww-form)
761 form)
762 (push (cons start (get-text-property start 'eww-form))
763 inputs))
764 (setq start (next-single-property-change start 'eww-form))))
765 (nreverse inputs)))
766
767(defun eww-input-value (input)
768 (let ((type (plist-get input :type))
769 (value (plist-get input :value)))
770 (cond
771 ((equal type "textarea")
772 (with-temp-buffer
773 (insert value)
774 (goto-char (point-min))
775 (while (re-search-forward "^ +\n\\| +$" nil t)
776 (replace-match "" t t))
777 (buffer-string)))
778 (t
779 (if (string-match " +\\'" value)
780 (substring value 0 (match-beginning 0))
781 value)))))
782
783(defun eww-submit ()
784 "Submit the current form."
785 (interactive)
786 (let* ((this-input (get-text-property (point) 'eww-form))
787 (form (plist-get this-input :eww-form))
788 values next-submit)
789 (dolist (elem (sort (eww-inputs form)
790 (lambda (o1 o2)
791 (< (car o1) (car o2)))))
792 (let* ((input (cdr elem))
793 (input-start (car elem))
794 (name (plist-get input :name)))
795 (when name
796 (cond
797 ((member (plist-get input :type) '("checkbox" "radio"))
798 (when (plist-get input :checked)
799 (push (cons name (plist-get input :value))
800 values)))
801 ((equal (plist-get input :type) "submit")
802 ;; We want the values from buttons if we hit a button if
803 ;; we hit enter on it, or if it's the first button after
804 ;; the field we did hit return on.
805 (when (or (eq input this-input)
806 (and (not (eq input this-input))
807 (null next-submit)
808 (> input-start (point))))
809 (setq next-submit t)
810 (push (cons name (plist-get input :value))
811 values)))
812 (t
813 (push (cons name (eww-input-value input))
814 values))))))
f22255bd
LMI
815 (dolist (elem form)
816 (when (and (consp elem)
817 (eq (car elem) 'hidden))
818 (push (cons (plist-get (cdr elem) :name)
819 (plist-get (cdr elem) :value))
820 values)))
c74cb344
G
821 (if (and (stringp (cdr (assq :method form)))
822 (equal (downcase (cdr (assq :method form))) "post"))
823 (let ((url-request-method "POST")
824 (url-request-extra-headers
825 '(("Content-Type" . "application/x-www-form-urlencoded")))
826 (url-request-data (mm-url-encode-www-form-urlencoded values)))
827 (eww-browse-url (shr-expand-url (cdr (assq :action form))
828 eww-current-url)))
829 (eww-browse-url
830 (concat
831 (if (cdr (assq :action form))
832 (shr-expand-url (cdr (assq :action form))
833 eww-current-url)
834 eww-current-url)
835 "?"
836 (mm-url-encode-www-form-urlencoded values))))))
2644071e 837
f865b474
IK
838(defun eww-browse-with-external-browser ()
839 "Browse the current URL with an external browser.
0ebd92a3 840The browser to used is specified by the `shr-external-browser' variable."
f865b474 841 (interactive)
0ebd92a3 842 (funcall shr-external-browser eww-current-url))
f865b474 843
b89fc156
IK
844(defun eww-yank-page-url ()
845 (interactive)
846 (message eww-current-url)
847 (kill-new eww-current-url))
266c63b5
AK
848(provide 'eww)
849
850;;; eww.el ends here