Fixed up text/gemini margins, handling of mailto: URLs.
[elpher.git] / elpher.el
index 7793a73..669ed55 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -4,7 +4,7 @@
 
 ;; Author: Tim Vaughan <tgvaughan@gmail.com>
 ;; Created: 11 April 2019
-;; Version: 1.4.7
+;; Version: 2.0.0
 ;; Keywords: comm gopher
 ;; Homepage: https://github.com/tgvaughan/elpher
 ;; Package-Requires: ((emacs "26"))
     ((gopher ?d) elpher-get-node-download "doc" elpher-binary)
     ((gopher ?P) elpher-get-node-download "doc" elpher-binary)
     ((gopher ?s) elpher-get-node-download "snd" elpher-binary)
-    ((gopher ?h) elpher-get-node-html "htm" elpher-html)
+    ((gopher ?h) elpher-get-html-node "htm" elpher-html)
+    (gemini elpher-get-gemini-node "gem" elpher-gemini)
+    (other-url elpher-get-other-url-node "url" elpher-other-url)
     ((special bookmarks) elpher-get-bookmarks-node)
     ((special start) elpher-get-start-node))
   "Association list from types to getters, margin codes and index faces.")
 
+
 ;;; Customization group
 ;;
 
   '((t :inherit warning))
   "Face used for search type directory records.")
 
-(defface elpher-url
+(defface elpher-html
+  '((t :inherit font-lock-comment-face))
+  "Face used for html type directory records.")
+
+(defface elpher-gemini
+  '((t :inherit font-lock-function-name-face))
+  "Face used for html type directory records.")
+
+(defface elpher-other-url
   '((t :inherit font-lock-comment-face))
-  "Face used for url type directory records.")
+  "Face used for other URL type links records.")
 
 (defface elpher-telnet
   '((t :inherit font-lock-function-name-face))
@@ -175,39 +186,54 @@ allows switching from an encrypted channel back to plain text without user input
 
 (defun elpher-address-from-url (url-string)
   "Create a ADDRESS object corresponding to the given URL-STRING."
-  (let ((url (url-generic-parse-url url-string)))
-    (if (and (url-type url)
-             (url-host url))
-        (let ((is-gopher (or (equal "gopher" (url-type url))
-                             (equal "gophers" (url-type url)))))
+  (let ((data (match-data))) ; Prevent parsing clobbering match data
+    (unwind-protect
+        (let ((url (url-generic-parse-url url-string)))
+          (setf (url-fullness url) t)
           (setf (url-filename url)
                 (url-unhex-string (url-filename url)))
-          (when (string-empty-p (url-filename url))
-            (if is-gopher
-                (setf (url-filename url) "1")))
-          (unless (> (url-port url) 0)
-            (if is-gopher
+          (unless (url-type url)
+            (setf (url-type url) "gopher"))
+          (let ((is-gopher (or (equal "gopher" (url-type url))
+                               (equal "gophers" (url-type url))))
+                (is-gemini (equal "gemini" (url-type url))))
+            (when is-gopher
+              ;; Gopher defaults
+              (unless (url-host url)
+                (setf (url-host url) (url-filename url))
+                (setf (url-filename url) ""))
+              (when (or (equal (url-filename url) "")
+                        (equal (url-filename url) "/"))
+                (setf (url-filename url) "/1"))
+              (unless (> (url-port url) 0)
                 (setf (url-port url) 70)))
+            (when is-gemini
+              (unless (> (url-port url) 0)
+                (setf (url-port url) 1965))))
           url)
-      (error "Malformed URL" url))))
+      (set-match-data data))))
 
-(defun elpher-make-gopher-address (type selector host port)
+(defun elpher-make-gopher-address (type selector host port &optional tls)
   "Create an ADDRESS object corresponding to the given gopher directory record
 attributes: TYPE, SELECTOR, HOST and PORT."
-  (elpher-address-from-url
-   (concat "gopher://" host
-           ":" (number-to-string port)
-           "/" (string type)
-           selector)))
+  (if (and (equal type ?h)
+           (string-prefix-p "URL:" selector))
+      (elpher-address-from-url (elt (split-string selector "URL:") 1))
+    (elpher-address-from-url
+     (concat "gopher" (if tls "s" "")
+             "://" host
+             ":" (number-to-string port)
+             "/" (string type)
+             selector))))
 
 (defun elpher-make-special-address (type)
   "Create an ADDRESS object corresponding to the given special page symbol TYPE."
   type)
 
-(defun elpher-address-to-url-string (address)
+(defun elpher-address-to-url (address)
   "Get string representation of ADDRESS, or nil if ADDRESS is special."
   (if (not (elpher-address-special-p address))
-      (url-encode-url (url-recreate address))
+      (url-encode-url (url-recreate-url address))
     nil))
 
 (defun elpher-address-type (address)
@@ -217,21 +243,20 @@ attributes: TYPE, SELECTOR, HOST and PORT."
     (let ((protocol (url-type address)))
       (cond ((or (equal protocol "gopher")
                  (equal protocol "gophers"))
-             (list 'gopher (string-to-char (url-filename address)) ?1))
-            ((string-equal protocol "gemini")
-             'gemini)))))
+             (list 'gopher (string-to-char (substring (url-filename address) 1))))
+            ((equal protocol "gemini")
+             'gemini)
+            (t 'other-url)))))
 
 (defun elpher-address-protocol (address)
   (if (symbolp address)
       nil
     (url-type address)))
 
-(defun elpher-gopher-address-selector (address)
-  "Retrieve selector from ADDRESS object."
-  (let ((filename (url-filename address)))
-    (if (> (length filename) 0)
-        (substring filename 1)
-      "")))
+(defun elpher-address-filename (address)
+  (if (symbolp address)
+      nil
+    (url-filename address)))
 
 (defun elpher-address-host (address)
   "Retrieve host from ADDRESS object."
@@ -245,6 +270,15 @@ attributes: TYPE, SELECTOR, HOST and PORT."
   "Return non-nil if ADDRESS object is special (e.g. start page, bookmarks page)."
   (symbolp address))
 
+(defun elpher-address-gopher-p (address)
+  "Return non-nill if ADDRESS object is a gopher address."
+  (and (not (elpher-address-special-p address))
+       (member (elpher-address-protocol address) '("gopher gophers"))))
+
+(defun elpher-gopher-address-selector (address)
+  "Retrieve gopher selector from ADDRESS object."
+  (substring (url-filename address) 2))
+
 ;; Node
 
 (defun elpher-make-node (display-string address &optional parent)
@@ -324,9 +358,11 @@ unless PRESERVE-PARENT is non-nil."
         (elpher-visit-parent-node)
         (pcase type
           (`(gopher ,type-char)
-           (error "Unsupported gopher selector type '%c'" type-char))
+           (error "Unsupported gopher selector type '%c' for '%s'"
+                  type-char (elpher-address-to-url address)))
           (else
-           (error "Unsupported address type '%S'" type)))))))
+           (error "Unsupported address type '%S' for '%s'"
+                  type (elpher-address-to-url address))))))))
 
 (defun elpher-visit-parent-node ()
   "Visit the parent of the current node."
@@ -427,18 +463,14 @@ away CRs and any terminating period."
 (defun elpher-node-button-help (node)
   "Return a string containing the help text for a button corresponding to NODE."
   (let ((address (elpher-node-address node)))
-    (if (equal (elpher-address-type address) '(gopher ?h))
-        (let ((url (cadr (split-string (elpher-address-selector address) "URL:"))))
-          (format "mouse-1, RET: open url '%s'" url))
-      (format "mouse-1, RET: open '%s' on %s port %s"
-              (elpher-gopher-address-selector address)
-              (elpher-address-host address)
-              (elpher-address-port address)))))
-
-(defun elpher-insert-index-record (display-string address)
+    (format "mouse-1, RET: open '%s'" (elpher-address-to-url address))))
+
+(defun elpher-insert-index-record (display-string &optional address)
   "Function to insert an index record into the current buffer.
-The contents of the record are dictated by DISPLAY-STRING and ADDRESS."
-  (let* ((type (elpher-address-type address))
+The contents of the record are dictated by DISPLAY-STRING and ADDRESS.
+If ADDRESS is not supplied or nil the record is rendered as an
+'information' line."
+  (let* ((type (if address (elpher-address-type address) nil))
          (type-map-entry (cdr (assoc type elpher-type-map))))
     (if type-map-entry
         (let* ((margin-code (elt type-map-entry 1))
@@ -452,7 +484,7 @@ The contents of the record are dictated by DISPLAY-STRING and ADDRESS."
                               'follow-link t
                               'help-echo (elpher-node-button-help node)))
       (pcase type
-        ('(gopher ?i) ;; Information
+        ((or '(gopher ?i) 'nil) ;; Information
          (elpher-insert-margin)
          (insert (propertize
                   (if elpher-buttonify-urls-in-directories
@@ -471,7 +503,19 @@ The contents of the record are dictated by DISPLAY-STRING and ADDRESS."
     (elpher-visit-node node)))
 
 
-;;; Selector retrieval (all kinds)
+;;; Network error reporting
+;;
+
+(defun elpher-network-error (address error)
+  (elpher-with-clean-buffer
+   (insert (propertize "\n---- ERROR -----\n\n" 'face 'error)
+           "When attempting to retrieve " (elpher-address-to-url address) ":\n"
+           (error-message-string the-error) ".\n"
+           (propertize "\n----------------\n\n" 'face 'error)
+           "Press 'u' to return to the previous page.")))
+
+
+;;; Gopher selector retrieval (all kinds)
 ;;
 
 (defun elpher-process-cleanup ()
@@ -529,7 +573,7 @@ up to the calling function."
            (error the-error)
          (elpher-with-clean-buffer
           (insert (propertize "\n---- ERROR -----\n\n" 'face 'error)
-                  "Failed to connect to " (elpher-get-address-url address) ".\n"
+                  "Failed to connect to " (elpher-address-to-url address) ".\n"
                   (propertize "\n----------------\n\n" 'face 'error)
                   "Press 'u' to return to the previous page.")))))))
 
@@ -562,50 +606,14 @@ up to the calling function."
   "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]+\\|\[[a-zA-Z0-9:]+\]\\)\\(?3::[0-9]+\\)?\\(?4:/[^ \r\n\t(),]*\\)?"
   "Regexp used to locate and buttinofy URLs in text files loaded by elpher.")
 
-(defun elpher-make-node-from-matched-url (&optional string)
-  "Convert most recent `elpher-url-regex' match to a node.
-
-If STRING is non-nil, this is given as an argument to all `match-string'
-calls, as is necessary if the match is performed by `string-match'."
-  (let ((url (match-string 0 string))
-        (protocol (downcase (match-string 1 string))))
-    (if (or (string= protocol "gopher")
-            (string= protocol "gophers"))
-        (let* ((bare-host (match-string 2 string))
-               (host (if (string-prefix-p "[" bare-host)
-                         (substring bare-host 1 (- (length bare-host) 1))
-                       bare-host))
-               (port (if (> (length (match-string 3 string))  1)
-                         (string-to-number (substring (match-string 3 string) 1))
-                       70))
-               (type-and-selector (match-string 4 string))
-               (type (if (> (length type-and-selector) 1)
-                         (elt type-and-selector 1)
-                       ?1))
-               (selector (decode-coding-string
-                          (url-unhex-string
-                           (if (> (length type-and-selector) 1)
-                               (substring type-and-selector 2)
-                             "")) 'utf-8))
-               (use-tls (string= protocol "gophers"))
-               (address (elpher-make-gopher-address type selector host port use-tls)))
-          (elpher-make-node url address))
-      (let* ((host (match-string 2 string))
-             (port (if (> (length (match-string 3 string)) 1)
-                       (string-to-number (substring (match-string 3 string) 1))
-                     70))
-             (selector (concat "URL:" url))
-             (address (elpher-make-gopher-address ?h selector host port)))
-        (elpher-make-node url address)))))
-
-
 (defun elpher-buttonify-urls (string)
   "Turn substrings which look like urls in STRING into clickable buttons."
   (with-temp-buffer
     (insert string)
     (goto-char (point-min))
     (while (re-search-forward elpher-url-regex nil t)
-        (let ((node (elpher-make-node-from-matched-url)))
+      (let ((node (elpher-make-node (match-string 0)
+                                    (elpher-address-from-url (match-string 0)))))
           (make-text-button (match-beginning 0)
                             (match-end 0)
                             'elpher-node  node
@@ -673,7 +681,7 @@ calls, as is necessary if the match is performed by `string-match'."
           (message "Displaying cached search results.  Reload to perform a new search."))
       (unwind-protect
           (let* ((query-string (read-string "Query: "))
-                 (query-selector (concat (elpher-address-selector address) "\t" query-string))
+                 (query-selector (concat (elpher-gopher-address-selector address) "\t" query-string))
                  (search-address (elpher-make-gopher-address ?1
                                                       query-selector
                                                       (elpher-address-host address)
@@ -715,7 +723,7 @@ calls, as is necessary if the match is performed by `string-match'."
 (defun elpher-get-node-download ()
   "Getter which retrieves the current node and writes the result to a file."
   (let* ((address (elpher-node-address elpher-current-node))
-         (selector (elpher-address-selector address)))
+         (selector (elpher-gopher-address-selector address)))
     (elpher-visit-parent-node) ; Do first in case of non-local exits.
     (let* ((filename-proposal (file-name-nondirectory selector))
            (filename (read-file-name "Save file as: "
@@ -737,7 +745,7 @@ calls, as is necessary if the match is performed by `string-match'."
         (error
          (error "Error downloading %s" elpher-download-filename))))))
 
-;; URL retrieval
+;; HTML node retrieval
 
 (defun elpher-insert-rendered-html (string)
   "Use shr to insert rendered view of html STRING into current buffer."
@@ -746,35 +754,222 @@ calls, as is necessary if the match is performed by `string-match'."
                (libxml-parse-html-region (point-min) (point-max)))))
     (shr-insert-document dom)))
 
-(defun elpher-get-url-node ()
-  "Getter which attempts to open the URL specified by the current node."
+(defun elpher-get-html-node ()
+  "Getter which retrieves and renders an HTML node."
   (let* ((address (elpher-node-address elpher-current-node))
-         (selector (elpher-address-selector address)))
-    (let ((url (elt (split-string selector "URL:") 1)))
-      (if url
+         (selector (elpher-gopher-address-selector address)))
+    (let ((content (elpher-get-cached-content address)))
+      (if content
           (progn
-            (elpher-visit-parent-node) ; Do first in case of non-local exits.
-            (message "Opening URL...")
-            (if elpher-open-urls-with-eww
-                (browse-web url)
-              (browse-url url)))
-        (let ((content (elpher-get-cached-content address)))
-          (if content
-              (progn
-                (elpher-with-clean-buffer
-                 (insert content)
-                 (elpher-restore-pos)))
             (elpher-with-clean-buffer
-             (insert "LOADING HTML... (use 'u' to cancel)"))
-            (elpher-get-selector address
-                                 (lambda (proc event)
-                                   (unless (string-prefix-p "deleted" event)
-                                     (elpher-with-clean-buffer
-                                      (elpher-insert-rendered-html elpher-selector-string)
-                                      (goto-char (point-min))
-                                      (elpher-cache-content
-                                       (elpher-node-address elpher-current-node)
-                                       (buffer-string))))))))))))
+             (insert content)
+             (elpher-restore-pos)))
+        (elpher-with-clean-buffer
+         (insert "LOADING HTML... (use 'u' to cancel)"))
+        (elpher-get-selector address
+                             (lambda (proc event)
+                               (unless (string-prefix-p "deleted" event)
+                                 (elpher-with-clean-buffer
+                                  (elpher-insert-rendered-html elpher-selector-string)
+                                  (goto-char (point-min))
+                                  (elpher-cache-content
+                                   (elpher-node-address elpher-current-node)
+                                   (buffer-string))))))))))
+
+;; Gemini node retrieval
+
+(defvar elpher-gemini-response)
+(defvar elpher-gemini-response-header)
+(defvar elpher-gemini-in-header)
+
+(defun elpher-gemini-response-code ()
+  (elt (split-string elpher-gemini-response-header) 0))
+
+(defun elpher-gemini-response-meta ()
+  (string-trim (substring elpher-gemini-response-header
+                          (string-match "[ \t]+" elpher-gemini-response-header))))
+
+(defun elpher-get-gemini (address after)
+  "Retrieve gemini ADDRESS, then execute AFTER.
+The response header is stored in the variable ‘elpher-gemini-response-header’.
+If available, the response is stored in the variable ‘elpher-gemini-response’.
+
+Usually errors result in an error page being displayed.  This is only
+appropriate if the selector is to be directly viewed.  If PROPAGATE-ERROR
+is non-nil, this message is not displayed.  Instead, the error propagates
+up to the calling function."
+  (setq elpher-gemini-response "")
+  (setq elpher-gemini-response-header "")
+  (setq elpher-gemini-in-header t)
+  (if (not (gnutls-available-p))
+      (error "Cannot retrieve TLS selector: GnuTLS not available")
+    (let* ((kill-buffer-query-functions nil)
+           (proc (open-network-stream "elpher-process"
+                                      nil
+                                      (elpher-address-host address)
+                                      (elpher-address-port address)
+                                      :type 'tls)))
+      (set-process-coding-system proc 'binary)
+      (set-process-filter proc
+                          (lambda (proc string)
+                            (if elpher-gemini-in-header
+                                (progn
+                                  (setq elpher-gemini-response-header
+                                        (concat elpher-gemini-response-header
+                                                (elt (split-string string "\r\n") 0)))
+                                  (let ((idx (string-match "\r\n" string)))
+                                    (setq elpher-gemini-response
+                                          (substring string (+ idx 2)))
+                                    (setq elpher-gemini-in-header nil)))
+                              (setq elpher-gemini-response
+                                    (concat elpher-gemini-response string)))))
+      (set-process-sentinel proc after)
+      (process-send-string proc
+                           (concat (elpher-address-to-url address) "\r\n")))))
+
+(defun elpher-render-gemini-response (mime-type-raw)
+  (let* ((mime-type-full (if (string-empty-p mime-type-raw)
+                             "text/gemini; charset=utf-8"
+                           mime-type-raw))
+         (mime-type-split (split-string mime-type-full ";"))
+         (mime-type (string-trim (elt mime-type-split 0)))
+         (parameters (if (> (length mime-type-split) 1)
+                         (string-trim (elt mime-type-split 1))
+                       "")))
+    ;; (message "MIME type %S with parameters %S" mime-type parameters)
+    (pcase mime-type
+      ((or "text/gemini" "")
+       (elpher-render--mimetype-text/gemini elpher-gemini-response parameters))
+      ((pred (string-prefix-p "text/"))
+       (elpher-render--mimetype-text/plain elpher-gemini-response parameters))
+      ((pred (string-prefix-p "image/"))
+       (elpher-render--mimetype-image/* elpher-gemini-response parameters))
+      (other
+       (error "Unsupported MIME type %S" mime-type)))))
+
+(defun elpher-gemini-get-link-url (line)
+  (string-trim (elt (split-string (substring line 2)) 0)))
+
+(defun elpher-gemini-get-link-display-string (line)
+  (let* ((rest (string-trim (elt (split-string line "=>") 1)))
+         (idx (string-match "[ \t]" rest)))
+    (if idx
+        (string-trim (substring rest (+ idx 1)))
+      "")))
+
+(defun elpher-address-from-gemini-url (url)
+  (let ((address (url-generic-parse-url url)))
+    (unless (equal (url-type address) "mailto")
+      (setf (url-fullness address) t)
+      (unless (url-host address)
+        (setf (url-host address) (url-host (elpher-node-address elpher-current-node)))
+        (unless (string-prefix-p "/" (url-filename address))
+          (setf (url-filename address)
+                (concat (file-name-as-directory 
+                         (url-filename (elpher-node-address elpher-current-node)))
+                        (url-filename address)))))
+      (unless (url-type address)
+        (setf (url-type address) "gemini"))
+      (unless (> (url-port address) 0)
+        (pcase (url-type address)
+          ("gemini" (setf (url-port address) 1965))
+          ("gopher" (setf (url-port address) 70)))))
+    address))
+
+(defun elpher-render--mimetype-text/gemini (data parameters)
+  (elpher-with-clean-buffer
+   (dolist (line (split-string (elpher-preprocess-text-response data) "\n"))
+     (if (string-prefix-p "=>" line)
+         (let* ((url (elpher-gemini-get-link-url line))
+                (display-string (elpher-gemini-get-link-display-string line))
+                (address (elpher-address-from-gemini-url url)))
+           (if (> (length display-string) 0)
+               (elpher-insert-index-record display-string address)
+             (elpher-insert-index-record url address)))
+       (elpher-insert-index-record line)))
+   (elpher-restore-pos)
+   (elpher-cache-content
+    (elpher-node-address elpher-current-node)
+    (buffer-string))))
+
+(defun elpher-render--mimetype-text/plain (data parameters)
+  (elpher-with-clean-buffer
+   (insert (elpher-buttonify-urls (elpher-preprocess-text-response data)))
+   (elpher-restore-pos)
+   (elpher-cache-content
+    (elpher-node-address elpher-current-node)
+    (buffer-string))))
+
+(defun elpher-render--mimetype-image/* (data parameters)
+  (let ((image (create-image data nil t)))
+    (elpher-with-clean-buffer
+     (insert-image image)
+     (elpher-restore-pos))))
+
+(defun elpher-process-gemini-response (proc event)
+  (condition-case the-error
+      (unless (string-prefix-p "deleted" event)
+        (let ((response-code (elpher-gemini-response-code))
+              (meta (elpher-gemini-response-meta)))
+          (pcase (elt response-code 0)
+            (?1 ; Input required
+             (elpher-with-clean-buffer
+              (insert "Gemini server is requesting input."))
+             (let* ((query-string (read-string (concat meta ": ")))
+                    (url (elpher-address-to-url (elpher-node-address elpher-current-node)))
+                    (query-address (elpher-address-from-url (concat url "?" query-string))))
+               (elpher-get-gemini query-address #'elpher-process-gemini-response)))
+            (?2 ; Normal response
+             (message elpher-gemini-response-header)
+             (elpher-render-gemini-response meta))
+            (?3 ; Redirect
+             (message "Following redirect to %s" meta)
+             (let ((redirect-address (elpher-address-from-gemini-url meta)))
+               (elpher-get-gemini redirect-address #'elpher-process-gemini-response)))
+            (?4 ; Temporary failure
+             (error "Gemini server reports TEMPORARY FAILURE for this request"))
+            (?5 ; Permanent failure
+             (error "Gemini server reports PERMANENT FAILURE for this request"))
+            (?6 ; Client certificate required
+             (error "Gemini server requires client certificate (unsupported at this time)"))
+            (other
+             (error "Gemini server responded with unknown response code %S"
+                    response-code)))))
+    (error
+     (elpher-network-error (elpher-node-address elpher-current-node) the-error))))
+
+
+(defun elpher-get-gemini-node ()
+  "Getter which retrieves and renders a Gemini node."
+  (let* ((address (elpher-node-address elpher-current-node))
+         (content (elpher-get-cached-content address)))
+    (condition-case the-error
+        (if content
+            (progn
+              (elpher-with-clean-buffer
+               (insert content)
+               (elpher-restore-pos)))
+          (elpher-with-clean-buffer
+           (insert "LOADING GEMINI... (use 'u' to cancel)"))
+          (elpher-get-gemini address #'elpher-process-gemini-response))
+      (error
+       (elpher-network-error address the-error)))))
+
+       
+
+
+;; Other URL node opening
+
+(defun elpher-get-other-url-node ()
+  "Getter which attempts to open the URL specified by the current node."
+  (let* ((address (elpher-node-address elpher-current-node))
+         (url (elpher-address-to-url address)))
+    (progn
+      (elpher-visit-parent-node) ; Do first in case of non-local exits.
+      (message "Opening URL...")
+      (if elpher-open-urls-with-eww
+          (browse-web url)
+        (browse-url url)))))
 
 ;; Telnet node connection
 
@@ -850,7 +1045,7 @@ calls, as is necessary if the match is performed by `string-match'."
      (if bookmarks
          (dolist (bookmark bookmarks)
            (let ((display-string (elpher-bookmark-display-string bookmark))
-                 (address (elpher-bookmark-address bookmark)))
+                 (address (elpher-address-from-url (elpher-bookmark-url bookmark))))
              (elpher-insert-index-record display-string address)))
        (insert "No bookmarks found.\n")))
    (insert "\n-----------------------\n\n"
@@ -865,11 +1060,11 @@ calls, as is necessary if the match is performed by `string-match'."
 ;;; Bookmarks
 ;;
 
-(defun elpher-make-bookmark (display-string address)
+(defun elpher-make-bookmark (display-string url)
   "Make an elpher bookmark.
 DISPLAY-STRING determines how the bookmark will appear in the
 bookmark list, while ADDRESS is the address of the entry."
-  (list display-string address))
+  (list display-string (elpher-address-to-url address)))
   
 (defun elpher-bookmark-display-string (bookmark)
   "Get the display string of BOOKMARK."
@@ -879,46 +1074,48 @@ bookmark list, while ADDRESS is the address of the entry."
   "Set the display string of BOOKMARK to DISPLAY-STRING."
   (setcar bookmark display-string))
 
-(defun elpher-bookmark-address (bookmark)
+(defun elpher-bookmark-url (bookmark)
   "Get the address for BOOKMARK."
   (elt bookmark 1))
 
+
 (defun elpher-save-bookmarks (bookmarks)
   "Record the bookmark list BOOKMARKS to the user's bookmark file.
 Beware that this completely replaces the existing contents of the file."
-  (with-temp-file (locate-user-emacs-file "elpher-bookmarks")
+  (with-temp-file (locate-user-emacs-file "elpher2-bookmarks")
     (erase-buffer)
-    (insert "; Elpher gopher bookmarks file\n\n"
-            "; Bookmarks are stored as a list of (label (type selector host port))\n"
-            "; s-expressions, where type is stored as a character (i.e. 49 = ?1).\n"
-            "; Feel free to edit by hand, but ensure this structure remains intact.\n\n")
+    (insert "; Elpher bookmarks file\n\n"
+            "; Bookmarks are stored as a list of (label URL) items.\n"
+            "; Feel free to edit by hand, but take care to ensure\n"
+            "; the list structure remains intact.\n\n")
     (pp bookmarks (current-buffer))))
 
 (defun elpher-load-bookmarks ()
   "Get the list of bookmarks from the users's bookmark file."
   (with-temp-buffer
     (ignore-errors
-      (insert-file-contents (locate-user-emacs-file "elpher-bookmarks"))
+      (insert-file-contents (locate-user-emacs-file "elpher2-bookmarks"))
       (goto-char (point-min))
       (read (current-buffer)))))
 
 (defun elpher-add-address-bookmark (address display-string)
-  "Save a bookmark for ADDRESS with label DISPLAY-STRING.
+  "Save a bookmark for ADDRESS with label DISPLAY-STRING.)))
 If ADDRESS is already bookmarked, update the label only."
-  (let ((bookmarks (elpher-load-bookmarks)))
-    (let ((existing-bookmark (rassoc (list address) bookmarks)))
+  (let ((bookmarks (elpher-load-bookmarks))
+        (url (elpher-address-to-url address)))
+    (let ((existing-bookmark (rassoc (list url) bookmarks)))
       (if existing-bookmark
           (elpher-set-bookmark-display-string existing-bookmark display-string)
-        (add-to-list 'bookmarks (elpher-make-bookmark display-string address))))
+        (add-to-list 'bookmarks (elpher-make-bookmark display-string url))))
     (elpher-save-bookmarks bookmarks)))
 
 (defun elpher-remove-address-bookmark (address)
   "Remove any bookmark to ADDRESS."
+  (let ((url (elpher-address-to-url address)))
     (elpher-save-bookmarks
      (seq-filter (lambda (bookmark)
-                   (not (equal (elpher-bookmark-address bookmark) address)))
-                 (elpher-load-bookmarks))))
-
+                   (not (equal (elpher-bookmark-url bookmark) url)))
+                 (elpher-load-bookmarks)))))
 
 ;;; Interactive procedures
 ;;
@@ -939,21 +1136,12 @@ If ADDRESS is already bookmarked, update the label only."
   (push-button))
 
 (defun elpher-go ()
-  "Go to a particular gopher site read from the minibuffer.
-The site may be specified via a URL or explicitly in terms of
-host, selector and port."
+  "Go to a particular gopher site read from the minibuffer."
   (interactive)
   (let ((node
-         (let ((host-or-url (read-string "Gopher host or URL: ")))
-           (if (string-match elpher-url-regex host-or-url)
-               (elpher-make-node-from-matched-url host-or-url)
-             (let ((selector (read-string "Selector (default none): " nil nil ""))
-                   (port-string (read-string "Port (default 70): " nil nil "70")))
-               (elpher-make-node (concat "gopher://" host-or-url
-                                         ":" port-string
-                                         "/1" selector)
-                                 (elpher-make-gopher-address ?1 selector host-or-url
-                                                             (string-to-number port-string))))))))
+         (let ((host-or-url (read-string "Gopher or Gemini URL: ")))
+           (elpher-make-node host-or-url
+                             (elpher-address-from-url host-or-url)))))
     (switch-to-buffer "*elpher*")
     (elpher-visit-node node)))
 
@@ -963,12 +1151,8 @@ host, selector and port."
   (let ((address (elpher-node-address elpher-current-node)))
     (if (elpher-address-special-p address)
         (error "Command not valid for this page")
-      (let ((url (read-string "URL: " (elpher-get-address-url address))))
-        (if (string-match elpher-url-regex url)
-            (let ((new-node (elpher-make-node-from-matched-url url)))
-              (unless (equal (elpher-node-address new-node) address)
-                (elpher-visit-node new-node)))
-          (error "Could not parse URL %s" url))))))
+      (let ((url (read-string "Gopher or Gemini URL: " (elpher-address-to-url address))))
+        (elpher-visit-node (elpher-make-node url (elpher-address-from-url url)))))))
 
 (defun elpher-redraw ()
   "Redraw current page."
@@ -1061,25 +1245,24 @@ host, selector and port."
 (defun elpher-root-dir ()
   "Visit root of current server."
   (interactive)
-  (let* ((address (elpher-node-address elpher-current-node))
-         (host (elpher-address-host address)))
-    (if host
-        (let ((host (elpher-address-host address))
-              (selector (elpher-address-selector address))
-              (port (elpher-address-port address)))
-          (if (> (length selector) 0)
-              (let ((root-address (elpher-make-gopher-address ?1 "" host port)))
-                (elpher-visit-node
-                 (elpher-make-node (concat "gopher://" host
-                                           ":" (number-to-string port)
-                                           "/1/")
-                                   root-address)))
-            (error "Already at root directory of current server")))
+  (let ((address (elpher-node-address elpher-current-node)))
+    (if (not (elpher-address-special-p address))
+        (if (or (member (url-filename address) '("/" ""))
+                (and (elpher-address-gopher-p address)
+                     (= (length (elpher-gopher-address-selector address)) 0)))
+            (error "Already at root directory of current server")
+          (let ((address-copy (elpher-address-from-url
+                               (elpher-address-to-url address))))
+            (setf (url-filename address-copy) "")
+            (elpher-visit-node
+             (elpher-make-node (elpher-address-to-url address-copy)
+                               address-copy))))
       (error "Command invalid for this page"))))
 
 (defun elpher-bookmarks-current-p ()
   "Return non-nil if current node is a bookmarks page."
-  (eq (elpher-address-type (elpher-node-address elpher-current-node)) 'bookmarks))
+  (equal (elpher-address-type (elpher-node-address elpher-current-node))
+         '(special bookmarks)))
 
 (defun elpher-reload-bookmarks ()
   "Reload bookmarks if current node is a bookmarks page."
@@ -1145,12 +1328,9 @@ host, selector and port."
   "Display information on NODE."
   (let ((display-string (elpher-node-display-string node))
         (address (elpher-node-address node)))
-    (if (not (elpher-address-special-p address))
-        (message "`%s' on %s port %s"
-                (elpher-address-selector address)
-                (elpher-address-host address)
-                (elpher-address-port address))
-      (message "%s" display-string))))
+    (if (elpher-address-special-p address)
+        (message "Special page: %s" display-string)
+      (message (elpher-address-to-url address)))))
 
 (defun elpher-info-link ()
   "Display information on node corresponding to link at point."
@@ -1170,7 +1350,7 @@ host, selector and port."
   (let ((address (elpher-node-address node)))
     (if (elpher-address-special-p address)
         (error (format "Cannot represent %s as URL" (elpher-node-display-string node)))
-      (let ((url (elpher-get-address-url address)))
+      (let ((url (elpher-address-to-url address)))
         (message "Copied \"%s\" to kill-ring/clipboard." url)
         (kill-new url)))))
 
@@ -1228,10 +1408,9 @@ host, selector and port."
     (when (fboundp 'evil-define-key)
       (evil-define-key 'motion map
         (kbd "TAB") 'elpher-next-link
-        (kbd "C-]") 'elpher-follow-current-link
+        (kbd "C-") 'elpher-follow-current-link
         (kbd "C-t") 'elpher-back
         (kbd "u") 'elpher-back
-        (kbd "O") 'elpher-root-dir
         (kbd "g") 'elpher-go
         (kbd "o") 'elpher-go-current
         (kbd "r") 'elpher-redraw
@@ -1255,7 +1434,7 @@ host, selector and port."
   "Keymap for gopher client.")
 
 (define-derived-mode elpher-mode special-mode "elpher"
-  "Major mode for elpher, an elisp gopher client.
+  "Major mode for elpher, an elisp gopher client.)
 
 This mode is automatically enabled by the interactive
 functions which initialize the gopher client, namely