]> code.delx.au - gnu-emacs/blobdiff - lisp/net/eww.el
* net/eww.el (eww-search-words): Mention `eww-search-prefix'.
[gnu-emacs] / lisp / net / eww.el
index 3329c3263c9ca20442c583ec9f64571cf57aec46..93aecb6895cdda2ba2361f370bbd431e85b6923e 100644 (file)
@@ -28,7 +28,9 @@
 (require 'format-spec)
 (require 'shr)
 (require 'url)
+(require 'url-queue)
 (require 'mm-url)
+(eval-when-compile (require 'subr-x)) ;; for string-trim
 
 (defgroup eww nil
   "Emacs Web Wowser"
   :type '(choice (const :tag "Never" nil)
                  regexp))
 
+(defcustom eww-after-render-hook nil
+  "A hook called after eww has finished rendering the buffer."
+  :version "25.1"
+  :group 'eww
+  :type 'hook)
+
 (defcustom eww-form-checkbox-selected-symbol "[X]"
   "Symbol used to represent a selected checkbox.
 See also `eww-form-checkbox-symbol'."
@@ -98,6 +106,14 @@ See also `eww-form-checkbox-selected-symbol'."
   :version "24.4"
   :group 'eww)
 
+(defface eww-form-file
+  '((((type x w32 ns) (class color))   ; Like default mode line
+     :box (:line-width 2 :style released-button)
+     :background "#808080" :foreground "black"))
+  "Face for eww buffer buttons."
+  :version "25.1"
+  :group 'eww)
+
 (defface eww-form-checkbox
   '((((type x w32 ns) (class color))   ; Like default mode line
      :box (:line-width 2 :style released-button)
@@ -130,21 +146,10 @@ See also `eww-form-checkbox-selected-symbol'."
   :version "24.4"
   :group 'eww)
 
-(defvar eww-current-url nil)
-(defvar eww-current-dom nil)
-(defvar eww-current-source nil)
-(defvar eww-current-title ""
-  "Title of current page.")
+(defvar eww-data nil)
 (defvar eww-history nil)
 (defvar eww-history-position 0)
 
-(defvar eww-next-url nil)
-(defvar eww-previous-url nil)
-(defvar eww-up-url nil)
-(defvar eww-home-url nil)
-(defvar eww-start-url nil)
-(defvar eww-contents-url nil)
-
 (defvar eww-local-regex "localhost"
   "When this regex is found in the URL, it's not a keyword but an address.")
 
@@ -159,14 +164,16 @@ See also `eww-form-checkbox-selected-symbol'."
 If the input doesn't look like an URL or a domain name, the
 word(s) will be searched for via `eww-search-prefix'."
   (interactive "sEnter URL or keywords: ")
-  (cond ((string-match-p "\\`file://" url))
+  (setq url (string-trim url))
+  (cond ((string-match-p "\\`file:/" url))
+       ;; Don't mangle file: URLs at all.
         ((string-match-p "\\`ftp://" url)
          (user-error "FTP is not supported."))
         (t
          (if (and (= (length (split-string url)) 1)
-                 (or (and (not (string-match-p "\\`[\"\'].*[\"\']\\'" url))
-                          (> (length (split-string url "\\.")) 1))
-                     (string-match eww-local-regex url)))
+                 (or (and (not (string-match-p "\\`[\"\'].*[\"\']\\'" url))
+                          (> (length (split-string url "[.:]")) 1))
+                     (string-match eww-local-regex url)))
              (progn
                (unless (string-match-p "\\`[a-zA-Z][-a-zA-Z0-9+.]*://" url)
                  (setq url (concat "http://" url)))
@@ -175,7 +182,10 @@ word(s) will be searched for via `eww-search-prefix'."
                  (setq url (concat url "/"))))
            (setq url (concat eww-search-prefix
                              (replace-regexp-in-string " " "+" url))))))
-  (url-retrieve url 'eww-render (list url)))
+  (url-retrieve url 'eww-render
+               (list url nil
+                     (and (eq major-mode 'eww-mode)
+                          (current-buffer)))))
 
 ;;;###autoload (defalias 'browse-web 'eww)
 
@@ -188,7 +198,14 @@ word(s) will be searched for via `eww-search-prefix'."
                    "/")
               (expand-file-name file))))
 
-(defun eww-render (status url &optional point)
+;;;###autoload
+(defun eww-search-words (&optional beg end)
+  "Search the web for the text between the point and marker.
+See the `eww-search-prefix' variable for the search engine used."
+  (interactive "r")
+  (eww (buffer-substring beg end)))
+
+(defun eww-render (status url &optional point buffer)
   (let ((redirect (plist-get status :redirect)))
     (when redirect
       (setq url redirect)))
@@ -206,22 +223,25 @@ word(s) will be searched for via `eww-search-prefix'."
         (data-buffer (current-buffer)))
     (unwind-protect
        (progn
-          (setq eww-current-title "")
+          (plist-put eww-data :title "")
          (cond
            ((and eww-use-external-browser-for-content-type
                  (string-match-p eww-use-external-browser-for-content-type
                                  (car content-type)))
             (eww-browse-with-external-browser url))
           ((equal (car content-type) "text/html")
-           (eww-display-html charset url nil point))
+           (eww-display-html charset url nil point buffer))
+          ((equal (car content-type) "application/pdf")
+           (eww-display-pdf))
           ((string-match-p "\\`image/" (car content-type))
-           (eww-display-image)
+           (eww-display-image buffer)
            (eww-update-header-line-format))
           (t
-           (eww-display-raw)
+           (eww-display-raw buffer)
            (eww-update-header-line-format)))
-         (setq eww-current-url url
-               eww-history-position 0))
+         (plist-put eww-data :url url)
+         (setq eww-history-position 0)
+         (run-hooks 'eww-after-render-hook))
       (kill-buffer data-buffer))))
 
 (defun eww-parse-headers ()
@@ -253,21 +273,27 @@ word(s) will be searched for via `eww-search-prefix'."
 (declare-function libxml-parse-html-region "xml.c"
                  (start end &optional base-url))
 
-(defun eww-display-html (charset url &optional document point)
+(defun eww-display-html (charset url &optional document point buffer)
   (or (fboundp 'libxml-parse-html-region)
       (error "This function requires Emacs to be compiled with libxml2"))
-  (unless (eq charset 'utf8)
-    (condition-case nil
-       (decode-coding-region (point) (point-max) charset)
-      (coding-system-error nil)))
+  ;; There should be a better way to abort loading images
+  ;; asynchronously.
+  (setq url-queue nil)
   (let ((document
         (or document
             (list
              'base (list (cons 'href url))
-             (libxml-parse-html-region (point) (point-max))))))
-    (setq eww-current-source (buffer-substring (point) (point-max)))
-    (eww-setup-buffer)
-    (setq eww-current-dom document)
+             (progn
+               (unless (eq charset 'utf-8)
+                 (condition-case nil
+                     (decode-coding-region (point) (point-max) charset)
+                   (coding-system-error nil)))
+               (libxml-parse-html-region (point) (point-max))))))
+       (source (and (null document)
+                    (buffer-substring (point) (point-max)))))
+    (eww-setup-buffer buffer)
+    (plist-put eww-data :source source)
+    (plist-put eww-data :dom document)
     (let ((inhibit-read-only t)
          (after-change-functions nil)
          (shr-target-id (url-target (url-generic-parse-url url)))
@@ -291,9 +317,14 @@ word(s) will be searched for via `eww-search-prefix'."
          (when point
            (goto-char point))))
        (t
-       (goto-char (point-min)))))
-    (setq eww-current-url url
-         eww-history-position 0)
+       (goto-char (point-min))
+       ;; Don't leave point inside forms, because the normal eww
+       ;; commands aren't available there.
+       (while (and (not (eobp))
+                   (get-text-property (point) 'eww-form))
+         (forward-line 1)))))
+    (plist-put eww-data :url url)
+    (setq eww-history-position 0)
     (eww-update-header-line-format)))
 
 (defun eww-handle-link (cont)
@@ -302,23 +333,23 @@ word(s) will be searched for via `eww-search-prefix'."
        (where (assoc
                ;; The text associated with :rel is case-insensitive.
                (if rel (downcase (cdr rel)))
-                     '(("next" . eww-next-url)
+                     '(("next" . :next)
                        ;; Texinfo uses "previous", but HTML specifies
                        ;; "prev", so recognize both.
-                       ("previous" . eww-previous-url)
-                       ("prev" . eww-previous-url)
+                       ("previous" . :previous)
+                       ("prev" . :previous)
                        ;; HTML specifies "start" but also "contents",
                        ;; and Gtk seems to use "home".  Recognize
                        ;; them all; but store them in different
                        ;; variables so that we can readily choose the
                        ;; "best" one.
-                       ("start" . eww-start-url)
-                       ("home" . eww-home-url)
-                       ("contents" . eww-contents-url)
-                       ("up" . eww-up-url)))))
+                       ("start" . :start)
+                       ("home" . :home)
+                       ("contents" . :contents)
+                       ("up" . up)))))
     (and href
         where
-        (set (cdr where) (cdr href)))))
+        (plist-put eww-data (cdr where) (cdr href)))))
 
 (defun eww-tag-link (cont)
   (eww-handle-link cont)
@@ -338,15 +369,19 @@ word(s) will be searched for via `eww-search-prefix'."
             ;; FIXME?  Title can be blank.  Default to, eg, last component
             ;; of url?
             (format-spec eww-header-line-format
-                         `((?u . ,eww-current-url)
-                           (?t . ,eww-current-title)))))
+                         `((?u . ,(plist-get eww-data :url))
+                           (?t . ,(or (plist-get eww-data :title) ""))))))
     (setq header-line-format nil)))
 
 (defun eww-tag-title (cont)
-  (setq eww-current-title "")
-  (dolist (sub cont)
-    (when (eq (car sub) 'text)
-      (setq eww-current-title (concat eww-current-title (cdr sub)))))
+  (let ((title ""))
+    (dolist (sub cont)
+      (when (eq (car sub) 'text)
+       (setq title (concat title (cdr sub)))))
+    (plist-put eww-data :title
+              (replace-regexp-in-string
+               "^ \\| $" ""
+               (replace-regexp-in-string "[ \t\r\n]+" " " title))))
   (eww-update-header-line-format))
 
 (defun eww-tag-body (cont)
@@ -357,61 +392,122 @@ word(s) will be searched for via `eww-search-prefix'."
         (shr-stylesheet (list (cons 'color fgcolor)
                               (cons 'background-color bgcolor))))
     (shr-generic cont)
-    (eww-colorize-region start (point) fgcolor bgcolor)))
-
-(defun eww-colorize-region (start end fg &optional bg)
-  (when (or fg bg)
-    (let ((new-colors (shr-color-check fg bg)))
-      (when new-colors
-       (when fg
-         (add-face-text-property start end
-                                 (list :foreground (cadr new-colors))
-                                 t))
-       (when bg
-         (add-face-text-property start end
-                                 (list :background (car new-colors))
-                                 t))))))
-
-(defun eww-display-raw ()
+    (shr-colorize-region start (point) fgcolor bgcolor)))
+
+(defun eww-display-raw (&optional buffer)
   (let ((data (buffer-substring (point) (point-max))))
-    (eww-setup-buffer)
+    (eww-setup-buffer buffer)
     (let ((inhibit-read-only t))
       (insert data))
     (goto-char (point-min))))
 
-(defun eww-display-image ()
+(defun eww-display-image (&optional buffer)
   (let ((data (shr-parse-image-data)))
-    (eww-setup-buffer)
+    (eww-setup-buffer buffer)
     (let ((inhibit-read-only t))
       (shr-put-image data nil))
     (goto-char (point-min))))
 
-(defun eww-setup-buffer ()
-  (switch-to-buffer (get-buffer-create "*eww*"))
+(defun eww-display-pdf ()
+  (let ((data (buffer-substring (point) (point-max))))
+    (switch-to-buffer (get-buffer-create "*eww pdf*"))
+    (let ((coding-system-for-write 'raw-text)
+         (inhibit-read-only t))
+      (erase-buffer)
+      (insert data)
+      (doc-view-mode)))
+  (goto-char (point-min)))
+
+(defun eww-setup-buffer (&optional buffer)
+  (switch-to-buffer
+   (if (buffer-live-p buffer)
+       buffer
+     (get-buffer-create "*eww*")))
   (let ((inhibit-read-only t))
     (remove-overlays)
     (erase-buffer))
   (unless (eq major-mode 'eww-mode)
-    (eww-mode))
-  (setq-local eww-next-url nil)
-  (setq-local eww-previous-url nil)
-  (setq-local eww-up-url nil)
-  (setq-local eww-home-url nil)
-  (setq-local eww-start-url nil)
-  (setq-local eww-contents-url nil))
+    (eww-mode)))
 
 (defun eww-view-source ()
+  "View the HTML source code of the current page."
   (interactive)
   (let ((buf (get-buffer-create "*eww-source*"))
-        (source eww-current-source))
+        (source (plist-get eww-data :source)))
     (with-current-buffer buf
-      (delete-region (point-min) (point-max))
-      (insert (or eww-current-source "no source"))
-      (goto-char (point-min))
-      (when (fboundp 'html-mode)
-        (html-mode)))
+      (let ((inhibit-read-only t))
+       (delete-region (point-min) (point-max))
+       (insert (or source "no source"))
+       (goto-char (point-min))
+       (when (fboundp 'html-mode)
+         (html-mode))))
     (view-buffer buf)))
 
+(defun eww-readable ()
+  "View the main \"readable\" parts of the current web page.
+This command uses heuristics to find the parts of the web page that
+contains the main textual portion, leaving out navigation menus and
+the like."
+  (interactive)
+  (let* ((old-data eww-data)
+        (dom (shr-transform-dom
+              (with-temp-buffer
+                (insert (plist-get old-data :source))
+                (condition-case nil
+                    (decode-coding-region (point-min) (point-max) 'utf-8)
+                  (coding-system-error nil))
+                (libxml-parse-html-region (point-min) (point-max))))))
+    (eww-score-readability dom)
+    (eww-save-history)
+    (eww-display-html nil nil
+                     (shr-retransform-dom
+                      (eww-highest-readability dom))
+                     nil (current-buffer))
+    (dolist (elem '(:source :url :title :next :previous :up))
+      (plist-put eww-data elem (plist-get old-data elem)))
+    (eww-update-header-line-format)))
+
+(defun eww-score-readability (node)
+  (let ((score -1))
+    (cond
+     ((memq (car node) '(script head comment))
+      (setq score -2))
+     ((eq (car node) 'meta)
+      (setq score -1))
+     ((eq (car node) 'img)
+      (setq score 2))
+     ((eq (car node) 'a)
+      (setq score (- (length (split-string
+                             (or (cdr (assoc 'text (cdr node))) ""))))))
+     (t
+      (dolist (elem (cdr node))
+       (cond
+        ((and (stringp (cdr elem))
+              (eq (car elem) 'text))
+         (setq score (+ score (length (split-string (cdr elem))))))
+        ((consp (cdr elem))
+         (setq score (+ score
+                        (or (cdr (assoc :eww-readability-score (cdr elem)))
+                            (eww-score-readability elem)))))))))
+    ;; Cache the score of the node to avoid recomputing all the time.
+    (setcdr node (cons (cons :eww-readability-score score) (cdr node)))
+    score))
+
+(defun eww-highest-readability (node)
+  (let ((result node)
+       highest)
+    (dolist (elem (cdr node))
+      (when (and (consp (cdr elem))
+                (> (or (cdr (assoc
+                             :eww-readability-score
+                             (setq highest
+                                   (eww-highest-readability elem))))
+                       most-negative-fixnum)
+                   (or (cdr (assoc :eww-readability-score (cdr result)))
+                       most-negative-fixnum)))
+       (setq result highest)))
+    result))
+
 (defvar eww-mode-map
   (let ((map (make-sparse-keymap)))
     (suppress-keymap map)
@@ -434,6 +530,7 @@ word(s) will be searched for via `eww-search-prefix'."
     (define-key map "w" 'eww-copy-page-url)
     (define-key map "C" 'url-cookie-list)
     (define-key map "v" 'eww-view-source)
+    (define-key map "R" 'eww-readable)
     (define-key map "H" 'eww-list-histories)
 
     (define-key map "b" 'eww-add-bookmark)
@@ -443,7 +540,7 @@ word(s) will be searched for via `eww-search-prefix'."
 
     (easy-menu-define nil map ""
       '("Eww"
-       ["Exit" eww-quit t]
+       ["Exit" quit-window t]
        ["Close browser" quit-window t]
        ["Reload" eww-reload t]
        ["Back to previous page" eww-back-url
@@ -463,7 +560,7 @@ word(s) will be searched for via `eww-search-prefix'."
 (defvar eww-tool-bar-map
   (let ((map (make-sparse-keymap)))
     (dolist (tool-bar-item
-             '((eww-quit . "close")
+             '((quit-window . "close")
                (eww-reload . "refresh")
                (eww-back-url . "left-arrow")
                (eww-forward-url . "right-arrow")
@@ -479,11 +576,7 @@ word(s) will be searched for via `eww-search-prefix'."
   "Mode for browsing the web.
 
 \\{eww-mode-map}"
-  ;; FIXME?  This seems a strange default.
-  (setq-local eww-current-url 'author)
-  (setq-local eww-current-dom nil)
-  (setq-local eww-current-source nil)
-  (setq-local eww-current-title "")
+  (setq-local eww-data (list :title ""))
   (setq-local browse-url-browser-function 'eww-browse-url)
   (setq-local after-change-functions 'eww-process-text-input)
   (setq-local eww-history nil)
@@ -497,7 +590,7 @@ word(s) will be searched for via `eww-search-prefix'."
 ;;;###autoload
 (defun eww-browse-url (url &optional _new-window)
   (when (and (equal major-mode 'eww-mode)
-            eww-current-url)
+            (plist-get eww-data :url))
     (eww-save-history))
   (eww url))
 
@@ -522,11 +615,8 @@ word(s) will be searched for via `eww-search-prefix'."
   (let ((inhibit-read-only t))
     (erase-buffer)
     (insert (plist-get elem :text))
-    (setq eww-current-source (plist-get elem :source))
-    (setq eww-current-dom (plist-get elem :dom))
     (goto-char (plist-get elem :point))
-    (setq eww-current-url (plist-get elem :url)
-         eww-current-title (plist-get elem :title))
+    (setq eww-data elem)
     (eww-update-header-line-format)))
 
 (defun eww-next-url ()
@@ -534,8 +624,9 @@ word(s) will be searched for via `eww-search-prefix'."
 A page is marked `next' if rel=\"next\" appears in a <link>
 or <a> tag."
   (interactive)
-  (if eww-next-url
-      (eww-browse-url (shr-expand-url eww-next-url eww-current-url))
+  (if (plist-get eww-data :next)
+      (eww-browse-url (shr-expand-url (plist-get eww-data :next)
+                                     (plist-get eww-data :url)))
     (user-error "No `next' on this page")))
 
 (defun eww-previous-url ()
@@ -543,8 +634,9 @@ or <a> tag."
 A page is marked `previous' if rel=\"previous\" appears in a <link>
 or <a> tag."
   (interactive)
-  (if eww-previous-url
-      (eww-browse-url (shr-expand-url eww-previous-url eww-current-url))
+  (if (plist-get eww-data :previous)
+      (eww-browse-url (shr-expand-url (plist-get eww-data :previous)
+                                     (plist-get eww-data :url)))
     (user-error "No `previous' on this page")))
 
 (defun eww-up-url ()
@@ -552,8 +644,9 @@ or <a> tag."
 A page is marked `up' if rel=\"up\" appears in a <link>
 or <a> tag."
   (interactive)
-  (if eww-up-url
-      (eww-browse-url (shr-expand-url eww-up-url eww-current-url))
+  (if (plist-get eww-data :up)
+      (eww-browse-url (shr-expand-url (plist-get eww-data :up)
+                                     (plist-get eww-data :url)))
     (user-error "No `up' on this page")))
 
 (defun eww-top-url ()
@@ -561,18 +654,18 @@ or <a> tag."
 A page is marked `top' if rel=\"start\", rel=\"home\", or rel=\"contents\"
 appears in a <link> or <a> tag."
   (interactive)
-  (let ((best-url (or eww-start-url
-                     eww-contents-url
-                     eww-home-url)))
+  (let ((best-url (or (plist-get eww-data :start)
+                     (plist-get eww-data :contents)
+                     (plist-get eww-data :home))))
     (if best-url
-       (eww-browse-url (shr-expand-url best-url eww-current-url))
+       (eww-browse-url (shr-expand-url best-url (plist-get eww-data :url)))
       (user-error "No `top' for this page"))))
 
 (defun eww-reload ()
   "Reload the current page."
   (interactive)
-  (url-retrieve eww-current-url 'eww-render
-               (list eww-current-url (point))))
+  (let ((url (plist-get eww-data :url)))
+    (url-retrieve url 'eww-render (list url (point)))))
 
 ;; Form support.
 
@@ -584,6 +677,12 @@ appears in a <link> or <a> tag."
     (define-key map [(control c) (control c)] 'eww-submit)
     map))
 
+(defvar eww-submit-file
+  (let ((map (make-sparse-keymap)))
+    (define-key map "\r" 'eww-select-file)
+    (define-key map [(control c) (control c)] 'eww-submit)
+    map))
+
 (defvar eww-checkbox-map
   (let ((map (make-sparse-keymap)))
     (define-key map " " 'eww-toggle-checkbox)
@@ -694,6 +793,34 @@ appears in a <link> or <a> tag."
     (put-text-property start (point) 'keymap eww-checkbox-map)
     (insert " ")))
 
+(defun eww-form-file (cont)
+  (let ((start (point))
+       (value (cdr (assq :value cont))))
+    (setq value
+         (if (zerop (length value))
+             " No file selected"
+           value))
+    (insert "Browse")
+    (add-face-text-property start (point) 'eww-form-file)
+    (insert value)
+    (put-text-property start (point) 'eww-form
+                      (list :eww-form eww-form
+                            :value (cdr (assq :value cont))
+                            :type (downcase (cdr (assq :type cont)))
+                            :name (cdr (assq :name cont))))
+    (put-text-property start (point) 'keymap eww-submit-file)
+    (insert " ")))
+
+(defun eww-select-file ()
+  "Change the value of the upload file menu under point."
+  (interactive)
+  (let*  ((input (get-text-property (point) 'eww-form)))
+    (let ((filename
+          (let ((insert-default-directory t))
+            (read-file-name "filename:  "))))
+      (eww-update-field filename (length "Browse"))
+              (plist-put input :filename filename))))
+
 (defun eww-form-text (cont)
   (let ((start (point))
        (type (downcase (or (cdr (assq :type cont))
@@ -810,6 +937,8 @@ See URL `https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input'.")
      ((or (equal type "checkbox")
          (equal type "radio"))
       (eww-form-checkbox cont))
+     ((equal type "file")
+      (eww-form-file cont))
      ((equal type "submit")
       (eww-form-submit cont))
      ((equal type "hidden")
@@ -902,14 +1031,17 @@ See URL `https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input'.")
     (goto-char
      (eww-update-field display))))
 
-(defun eww-update-field (string)
+(defun eww-update-field (string &optional offset)
+  (if (not offset) (setq offset 0))
   (let ((properties (text-properties-at (point)))
-       (start (eww-beginning-of-field))
-       (end (1+ (eww-end-of-field))))
-    (delete-region start end)
+       (start (+ (eww-beginning-of-field) offset))
+       (current-end (1+ (eww-end-of-field)))
+       (new-end (1+ (+ (eww-beginning-of-field) (length string)))))
+    (delete-region start current-end)
+    (forward-char offset)
     (insert string
-           (make-string (- (- end start) (length string)) ? ))
-    (set-text-properties start end properties)
+           (make-string (- (- (+ new-end offset) start) (length string)) ? ))
+    (if (= 0 offset) (set-text-properties start new-end properties))
     start))
 
 (defun eww-toggle-checkbox ()
@@ -977,8 +1109,8 @@ See URL `https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input'.")
         (form (plist-get this-input :eww-form))
         values next-submit)
     (dolist (elem (sort (eww-inputs form)
-                        (lambda (o1 o2)
-                          (< (car o1) (car o2)))))
+                       (lambda (o1 o2)
+                         (< (car o1) (car o2)))))
       (let* ((input (cdr elem))
             (input-start (car elem))
             (name (plist-get input :name)))
@@ -988,6 +1120,16 @@ See URL `https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input'.")
            (when (plist-get input :checked)
              (push (cons name (plist-get input :value))
                    values)))
+          ((equal (plist-get input :type) "file")
+           (push (cons "file"
+                       (list (cons "filedata"
+                                   (with-temp-buffer
+                                     (insert-file-contents
+                                      (plist-get input :filename))
+                                     (buffer-string)))
+                             (cons "name" (plist-get input :name))
+                             (cons "filename" (plist-get input :filename))))
+                 values))
           ((equal (plist-get input :type) "submit")
            ;; We want the values from buttons if we hit a button if
            ;; we hit enter on it, or if it's the first button after
@@ -1006,22 +1148,43 @@ See URL `https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input'.")
       (when (and (consp elem)
                 (eq (car elem) 'hidden))
        (push (cons (plist-get (cdr elem) :name)
-                   (plist-get (cdr elem) :value))
+                   (or (plist-get (cdr elem) :value) ""))
              values)))
     (if (and (stringp (cdr (assq :method form)))
             (equal (downcase (cdr (assq :method form))) "post"))
-       (let ((url-request-method "POST")
-             (url-request-extra-headers
-              '(("Content-Type" . "application/x-www-form-urlencoded")))
-             (url-request-data (mm-url-encode-www-form-urlencoded values)))
-         (eww-browse-url (shr-expand-url (cdr (assq :action form))
-                                         eww-current-url)))
+       (let ((mtype))
+         (dolist (x values mtype)
+           (if (equal (car x) "file")
+               (progn
+                 (setq mtype "multipart/form-data"))))
+         (cond ((equal mtype "multipart/form-data")
+                (let ((boundary (mml-compute-boundary '())))
+                  (let ((url-request-method "POST")
+                        (url-request-extra-headers
+                         (list (cons "Content-Type"
+                                     (concat "multipart/form-data; boundary="
+                                             boundary))))
+                        (url-request-data
+                         (mm-url-encode-multipart-form-data values boundary)))
+                    (eww-browse-url (shr-expand-url
+                                     (cdr (assq :action form))
+                                     (plist-get eww-data :url))))))
+               (t
+                (let ((url-request-method "POST")
+                      (url-request-extra-headers
+                       '(("Content-Type" .
+                          "application/x-www-form-urlencoded")))
+                      (url-request-data
+                       (mm-url-encode-www-form-urlencoded values)))
+                  (eww-browse-url (shr-expand-url
+                                   (cdr (assq :action form))
+                                   (plist-get eww-data :url)))))))
       (eww-browse-url
        (concat
        (if (cdr (assq :action form))
            (shr-expand-url (cdr (assq :action form))
-                           eww-current-url)
-         eww-current-url)
+                           (plist-get eww-data :url))
+         (plist-get eww-data :url))
        "?"
        (mm-url-encode-www-form-urlencoded values))))))
 
@@ -1029,7 +1192,7 @@ See URL `https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input'.")
   "Browse the current URL with an external browser.
 The browser to used is specified by the `shr-external-browser' variable."
   (interactive)
-  (funcall shr-external-browser (or url eww-current-url)))
+  (funcall shr-external-browser (or url (plist-get eww-data :url))))
 
 (defun eww-follow-link (&optional external mouse-event)
   "Browse the URL under point.
@@ -1046,9 +1209,10 @@ If EXTERNAL, browse the URL using `shr-external-browser'."
       (funcall shr-external-browser url))
      ;; This is a #target url in the same page as the current one.
      ((and (url-target (url-generic-parse-url url))
-          (eww-same-page-p url eww-current-url))
+          (eww-same-page-p url (plist-get eww-data :url)))
       (eww-save-history)
-      (eww-display-html 'utf8 url eww-current-dom))
+      (eww-display-html 'utf-8 url (plist-get eww-data :url)
+                       nil (current-buffer)))
      (t
       (eww-browse-url url)))))
 
@@ -1063,8 +1227,8 @@ Differences in #targets are ignored."
 
 (defun eww-copy-page-url ()
   (interactive)
-  (message "%s" eww-current-url)
-  (kill-new eww-current-url))
+  (message "%s" (plist-get eww-data :url))
+  (kill-new (plist-get eww-data :url)))
 
 (defun eww-download ()
   "Download URL under point to `eww-download-directory'."
@@ -1080,7 +1244,9 @@ Differences in #targets are ignored."
            (path (car (url-path-and-query obj)))
            (file (eww-make-unique-file-name (file-name-nondirectory path)
                                            eww-download-directory)))
-      (write-file file)
+      (goto-char (point-min))
+      (re-search-forward "\r?\n\r?\n")
+      (write-region (point) (point-max) file)
       (message "Saved %s" file))))
 
 (defun eww-make-unique-file-name (file directory)
@@ -1108,19 +1274,20 @@ Differences in #targets are ignored."
   (interactive)
   (eww-read-bookmarks)
   (dolist (bookmark eww-bookmarks)
-    (when (equal eww-current-url
-                (plist-get bookmark :url))
+    (when (equal (plist-get eww-data :url) (plist-get bookmark :url))
       (user-error "Already bookmarked")))
   (if (y-or-n-p "bookmark this page? ")
       (progn
-       (let ((title (replace-regexp-in-string "[\n\t\r]" " " eww-current-title)))
+       (let ((title (replace-regexp-in-string "[\n\t\r]" " "
+                                              (plist-get eww-data :url))))
          (setq title (replace-regexp-in-string "\\` +\\| +\\'" "" title))
-         (push (list :url eww-current-url
+         (push (list :url (plist-get eww-data :url)
                      :title title
                      :time (current-time-string))
                eww-bookmarks))
        (eww-write-bookmarks)
-       (message "Bookmarked %s (%s)" eww-current-url eww-current-title))))
+       (message "Bookmarked %s (%s)" (plist-get eww-data :url)
+                (plist-get eww-data :title)))))
 
 (defun eww-write-bookmarks ()
   (with-temp-file (expand-file-name "eww-bookmarks" eww-bookmarks-directory)
@@ -1135,6 +1302,7 @@ Differences in #targets are ignored."
              (insert-file-contents file)
              (read (current-buffer)))))))
 
+;;;###autoload
 (defun eww-list-bookmarks ()
   "Display the bookmarks."
   (interactive)
@@ -1277,13 +1445,14 @@ Differences in #targets are ignored."
 ;;; History code
 
 (defun eww-save-history ()
-  (push (list :url eww-current-url
-              :title eww-current-title
-              :point (point)
-              :dom eww-current-dom
-              :source eww-current-source
-              :text (buffer-string))
-        eww-history))
+  (plist-put eww-data :point (point))
+  (plist-put eww-data :text (buffer-string))
+  (push eww-data eww-history)
+  (setq eww-data (list :title ""))
+  ;; Don't let the history grow infinitely.  We store quite a lot of
+  ;; data per page.
+  (when-let (tail (nthcdr 50 eww-history))
+    (setcdr tail nil)))
 
 (defun eww-list-histories ()
   "List the eww-histories."