Factoring out transport code from rendering code.
[elpher.git] / elpher.el
index bb1bc48..c09f13e 100644 (file)
--- a/elpher.el
+++ b/elpher.el
   "Width of left-hand margin used when rendering indicies.")
 
 (defconst elpher-type-map
-  '(((gopher ?0) elpher-get-text-node "txt" elpher-text)
-    ((gopher ?1) elpher-get-index-node "/" elpher-index)
-    ((gopher ?4) elpher-get-node-download "bin" elpher-binary)
-    ((gopher ?5) elpher-get-node-download "bin" elpher-binary)
-    ((gopher ?7) elpher-get-search-node "?" elpher-search)
-    ((gopher ?8) elpher-get-telnet-node "tel" elpher-telnet)
-    ((gopher ?9) elpher-get-node-download "bin" elpher-binary)
-    ((gopher ?g) elpher-get-image-node "img" elpher-image)
-    ((gopher ?p) elpher-get-image-node "img" elpher-image)
-    ((gopher ?I) elpher-get-image-node "img" elpher-image)
-    ((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-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))
+  '(((gopher ?0) elpher-get-gopher-node elpher-render-text "txt" elpher-text)
+    ((gopher ?1) elpher-get-gopher-node elpher-render-index "/" elpher-index)
+    ((gopher ?4) elpher-get-gopher-node elpher-render-download "bin" elpher-binary)
+    ((gopher ?5) elpher-get-gopher-node elpher-render-download "bin" elpher-binary)
+    ((gopher ?7) elpher-get-gopher-query-node elpher-render-index "?" elpher-search)
+    ((gopher ?9) elpher-get-gopher-node elpher-render-node-download "bin" elpher-binary)
+    ((gopher ?g) elpher-get-gopher-node elpher-render-image "img" elpher-image)
+    ((gopher ?p) elpher-get-gopher-node elpher-render-image "img" elpher-image)
+    ((gopher ?I) elpher-get-gopher-node elpher-render-image "img" elpher-image)
+    ((gopher ?d) elpher-get-gopher-node elpher-render-download "doc" elpher-binary)
+    ((gopher ?P) elpher-get-gopher-node elpher-render-download "doc" elpher-binary)
+    ((gopher ?s) elpher-get-gopher-node elpher-render-download "snd" elpher-binary)
+    ((gopher ?h) elpher-get-gopher-node elpher-render-html "htm" elpher-html)
+    (gemini elpher-get-gemini-node elpher-render-gemini "gem" elpher-gemini)
+    (telnet elpher-get-telnet-node nil "tel" elpher-telnet)
+    (other-url elpher-get-other-url-node nil "url" elpher-other-url)
+    ((special bookmarks) elpher-get-bookmarks-node nil)
+    ((special start) elpher-get-start-node nil))
   "Association list from types to getters, margin codes and index faces.")
 
 
@@ -204,12 +204,7 @@ allows switching from an encrypted channel back to plain text without user input
                 (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))))
+                (setf (url-filename url) "/1"))))
           url)
       (set-match-data data))))
 
@@ -243,7 +238,10 @@ attributes: TYPE, SELECTOR, HOST and PORT."
     (let ((protocol (url-type address)))
       (cond ((or (equal protocol "gopher")
                  (equal protocol "gophers"))
-             (list 'gopher (string-to-char (substring (url-filename address) 1))))
+             (list 'gopher
+                   (if (member (url-filename address) '("" "/"))
+                       ?1
+                     (string-to-char (substring (url-filename address) 1)))))
             ((equal protocol "gemini")
              'gemini)
             (t 'other-url)))))
@@ -277,7 +275,9 @@ attributes: TYPE, SELECTOR, HOST and PORT."
 
 (defun elpher-gopher-address-selector (address)
   "Retrieve gopher selector from ADDRESS object."
-  (substring (url-filename address) 2))
+  (if (member (url-filename address) '("" "/"))
+      ""
+    (substring (url-filename address) 2)))
 
 ;; Node
 
@@ -335,8 +335,8 @@ initially."
 
 (defvar elpher-current-node nil)
 
-(defun elpher-visit-node (node &optional getter preserve-parent)
-  "Visit NODE using its own getter or GETTER, if non-nil.
+(defun elpher-visit-node (node &optional renderer preserve-parent)
+  "Visit NODE using its own renderer or RENDERER, if non-nil.
 Additionally, set the parent of NODE to `elpher-current-node',
 unless PRESERVE-PARENT is non-nil."
   (elpher-save-pos)
@@ -348,21 +348,22 @@ unless PRESERVE-PARENT is non-nil."
         (elpher-set-node-parent node (elpher-node-parent elpher-current-node))
       (elpher-set-node-parent node elpher-current-node)))
   (setq elpher-current-node node)
-  (if getter
-      (funcall getter)
-    (let* ((address (elpher-node-address node))
-           (type (elpher-address-type address))
-           (type-record (cdr (assoc type elpher-type-map))))
-      (if type-record
-          (funcall (car type-record))
-        (elpher-visit-parent-node)
-        (pcase type
-          (`(gopher ,type-char)
-           (error "Unsupported gopher selector type '%c' for '%s'"
-                  type-char (elpher-address-to-url address)))
-          (else
-           (error "Unsupported address type '%S' for '%s'"
-                  type (elpher-address-to-url address))))))))
+  (let* ((address (elpher-node-address node))
+         (type (elpher-address-type address))
+         (type-record (cdr (assoc type elpher-type-map))))
+    (if type-record
+        (funcall (car type-record)
+                 (if renderer
+                     renderer
+                   (cadr type-record)))
+      (elpher-visit-parent-node)
+      (pcase type
+        (`(gopher ,type-char)
+         (error "Unsupported gopher selector type '%c' for '%s'"
+                type-char (elpher-address-to-url address)))
+        (else
+         (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."
@@ -465,10 +466,12 @@ away CRs and any terminating period."
   (let ((address (elpher-node-address node)))
     (format "mouse-1, RET: open '%s'" (elpher-address-to-url address))))
 
-(defun elpher-insert-index-record (display-string 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))
@@ -482,7 +485,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
@@ -546,7 +549,9 @@ up to the calling function."
              (proc (open-network-stream "elpher-process"
                                        nil
                                        (elpher-address-host address)
-                                       (elpher-address-port address)
+                                       (if (> (elpher-address-port address) 0)
+                                           (elpher-address-port address)
+                                         70)
                                        :type (if elpher-use-tls 'tls 'plain))))
         (set-process-coding-system proc 'binary)
         (set-process-filter proc
@@ -575,33 +580,37 @@ up to the calling function."
                   (propertize "\n----------------\n\n" 'face 'error)
                   "Press 'u' to return to the previous page.")))))))
 
-;; Index retrieval
-
-(defun elpher-get-index-node ()
-  "Getter which retrieves the current node contents as an index."
-  (let* ((address (elpher-node-address elpher-current-node))
-         (content (elpher-get-cached-content address)))
+(defun elpher-get-gopher-node (renderer)
+   (let* ((address (elpher-node-address elpher-current-node)))
+         (content (elpher-get-cached-content address))
     (if content
         (progn
-          (elpher-with-clean-buffer
-           (insert content)
-           (elpher-restore-pos)))
+          (funcall renderer nil content)
+          (elpher-restore-pos))
       (elpher-with-clean-buffer
-       (insert "LOADING DIRECTORY... (use 'u' to cancel)"))
+       (insert "LOADING... (use 'u' to cancel)"))
       (elpher-get-selector address
                            (lambda (proc event)
                              (unless (string-prefix-p "deleted" event)
-                               (elpher-with-clean-buffer
-                                (elpher-insert-index elpher-selector-string)
-                                (elpher-restore-pos)
-                                (elpher-cache-content
-                                 (elpher-node-address elpher-current-node)
-                                 (buffer-string)))))))))
+                               (funcall renderer elpher-selector-string)
+                               (elpher-restore-pos)))))))
+                                
 
-;; Text retrieval
+;; Index node rendering
+
+(defun elpher-render-index (data &optional cached-data)
+  "Render DATA as an index, using CACHED-DATA instead if supplied."
+  (elpher-with-clean-buffer
+   (if cached-data
+       (insert cached-data)
+     (elpher-insert-index data)
+     (elpher-cache-content (elpher-node-address elpher-current-node)
+                           (buffer-string)))))
+
+;; Text rendering
 
 (defconst elpher-url-regex
-  "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]+\\|\[[a-zA-Z0-9:]+\]\\)\\(?3::[0-9]+\\)?\\(?4:/[^ \r\n\t(),]*\\)?"
+  "\\([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-buttonify-urls (string)
@@ -620,49 +629,32 @@ up to the calling function."
                             'help-echo (elpher-node-button-help node))))
     (buffer-string)))
 
-(defun elpher-get-text-node ()
-  "Getter which retrieves the current node contents as a text document."
-  (let* ((address (elpher-node-address elpher-current-node))
-         (content (elpher-get-cached-content address)))
-    (if content
-        (progn
-          (elpher-with-clean-buffer
-           (insert content)
-           (elpher-restore-pos)))
-      (progn
-        (elpher-with-clean-buffer
-         (insert "LOADING TEXT... (use 'u' to cancel)"))
-        (elpher-get-selector address
-                              (lambda (proc event)
-                                (unless (string-prefix-p "deleted" event)
-                                  (elpher-with-clean-buffer
-                                   (insert (elpher-buttonify-urls
-                                            (elpher-preprocess-text-response
-                                             elpher-selector-string)))
-                                   (elpher-restore-pos)
-                                   (elpher-cache-content
-                                    (elpher-node-address elpher-current-node)
-                                    (buffer-string))))))))))
+(defun elpher-render-text (data &optional cached-data)
+  "Render DATA as text, using CACHED-DATA instead if supplied."
+  (elpher-with-clean-buffer
+   (if cached-data
+       (insert cached-data)
+     (insert (elpher-buttonify-urls
+              (elpher-preprocess-text-response)
+              elpher-selector-string))
+     (elpher-cache-content
+      (elpher-node-address elpher-current-node)
+      (buffer-string)))))
 
 ;; Image retrieval
 
-(defun elpher-get-image-node ()
-  "Getter which retrieves the current node contents as an image to view."
-  (let* ((address (elpher-node-address elpher-current-node)))
-    (if (display-images-p)
-        (progn
+(defun elpher-render-image (data)
+  "Display DATA as image, using CACHED-DATA if supplied.
+If image display is unsupported, offer to save the image to a file."
+  (if (display-images-p)
+      (progn
+        (let ((image (create-image
+                      data
+                      nil t)))
           (elpher-with-clean-buffer
-           (insert "LOADING IMAGE... (use 'u' to cancel)"))
-          (elpher-get-selector address
-                               (lambda (proc event)
-                                 (unless (string-prefix-p "deleted" event)
-                                   (let ((image (create-image
-                                                 elpher-selector-string
-                                                 nil t)))
-                                     (elpher-with-clean-buffer
-                                      (insert-image image)
-                                      (elpher-restore-pos)))))))
-      (elpher-get-node-download))))
+           (insert-image image)
+           (elpher-restore-pos))))
+    (elpher-save-to-file data)))
 
 ;; Search retrieval
 
@@ -681,21 +673,21 @@ up to the calling function."
           (let* ((query-string (read-string "Query: "))
                  (query-selector (concat (elpher-gopher-address-selector address) "\t" query-string))
                  (search-address (elpher-make-gopher-address ?1
-                                                      query-selector
-                                                      (elpher-address-host address)
-                                                      (elpher-address-port address))))
+                                                             query-selector
+                                                             (elpher-address-host address)
+                                                             (elpher-address-port address))))
             (setq aborted nil)
             (elpher-with-clean-buffer
              (insert "LOADING RESULTS... (use 'u' to cancel)"))
             (elpher-get-selector search-address
-                                  (lambda (proc event)
-                                    (unless (string-prefix-p "deleted" event)
-                                      (elpher-with-clean-buffer
-                                       (elpher-insert-index elpher-selector-string))
-                                      (goto-char (point-min))
-                                      (elpher-cache-content
-                                       (elpher-node-address elpher-current-node)
-                                       (buffer-string))))))
+                                 (lambda (proc event)
+                                   (unless (string-prefix-p "deleted" event)
+                                     (elpher-with-clean-buffer
+                                      (elpher-insert-index elpher-selector-string))
+                                     (goto-char (point-min))
+                                     (elpher-cache-content
+                                      (elpher-node-address elpher-current-node)
+                                      (buffer-string))))))
         (if aborted
             (elpher-visit-parent-node))))))
 
@@ -780,13 +772,6 @@ up to the calling function."
 (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’.
@@ -805,7 +790,9 @@ up to the calling function."
            (proc (open-network-stream "elpher-process"
                                       nil
                                       (elpher-address-host address)
-                                      (elpher-address-port address)
+                                      (if (> (elpher-address-port address) 0)
+                                          (elpher-address-port address)
+                                        1965)
                                       :type 'tls)))
       (set-process-coding-system proc 'binary)
       (set-process-filter proc
@@ -825,16 +812,28 @@ up to the calling function."
       (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" mime-type)
+
+(defun elpher-render-gemini-response (mime-type-string)
+  (let* ((mime-type-string* (if (string-empty-p mime-type-string)
+                                "text/gemini; charset=utf-8"
+                              mime-type-string))
+         (mime-type-split (split-string mime-type-string* ";"))
+         (mime-type (string-trim (car mime-type-split)))
+         (parameters (mapcar (lambda (s)
+                               (let ((key-val (split-string s "=")))
+                                 (list (downcase (string-trim (car key-val)))
+                                       (downcase (string-trim (cadr key-val))))))
+                             (cdr mime-type-split))))
+    (if (and (equal "text/gemini" mime-type)
+             (not (assoc "charset" parameters)))
+        (setq parameters (cons (list "charset" "utf-8") parameters)))
+    (when (string-prefix-p "text/" mime-type)
+      (if (assoc "charset" parameters)
+          (setq elpher-gemini-response
+                (decode-coding-string elpher-gemini-response
+                                      (intern (cadr (assoc "charset" parameters))))))
+      (setq elpher-gemini-response
+            (replace-regexp-in-string "\r" "" elpher-gemini-response)))
     (pcase mime-type
       ((or "text/gemini" "")
        (elpher-render--mimetype-text/gemini elpher-gemini-response parameters))
@@ -856,26 +855,24 @@ up to the calling function."
       "")))
 
 (defun elpher-address-from-gemini-url (url)
+  "Extract address from URL with defaults as per gemini map files."
   (let ((address (url-generic-parse-url url)))
-    (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))))
+    (unless (and (url-type address) (not (url-fullness address))) ;avoid mangling mailto: urls
+      (setf (url-fullness address) t)
+      (unless (url-host address) ;if there is an explicit host, filenames are explicit
+        (setf (url-host address) (url-host (elpher-node-address elpher-current-node)))
+        (unless (string-prefix-p "/" (url-filename address)) ;deal with relative links
+          (setf (url-filename address)
+                (concat (file-name-directory 
+                         (url-filename (elpher-node-address elpher-current-node)))
+                        (url-filename address)))))
+      (unless (url-type address)
+        (setf (url-type address) "gemini")))
     address))
 
 (defun elpher-render--mimetype-text/gemini (data parameters)
   (elpher-with-clean-buffer
-   (dolist (line (split-string (elpher-preprocess-text-response data) "\n"))
+   (dolist (line (split-string data "\n"))
      (if (string-prefix-p "=>" line)
          (let* ((url (elpher-gemini-get-link-url line))
                 (display-string (elpher-gemini-get-link-display-string line))
@@ -883,7 +880,7 @@ up to the calling function."
            (if (> (length display-string) 0)
                (elpher-insert-index-record display-string address)
              (elpher-insert-index-record url address)))
-       (insert (elpher-buttonify-urls line) "\n")))
+       (elpher-insert-index-record line)))
    (elpher-restore-pos)
    (elpher-cache-content
     (elpher-node-address elpher-current-node)
@@ -906,8 +903,11 @@ up to the calling function."
 (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)))
+        (let ((response-code (car (split-string elpher-gemini-response-header)))
+              (meta (string-trim
+                     (substring elpher-gemini-response-header
+                                (string-match "[ \t]+"
+                                              elpher-gemini-response-header)))))
           (pcase (elt response-code 0)
             (?1 ; Input required
              (elpher-with-clean-buffer
@@ -917,6 +917,7 @@ up to the calling function."
                     (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)
@@ -951,8 +952,6 @@ up to the calling function."
       (error
        (elpher-network-error address the-error)))))
 
-       
-
 
 ;; Other URL node opening