;; 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"))
"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)
- (other-url elpher-get-url-node "url" elpher-html)
- ((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.")
'((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 url type directory records.")
+ "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 other URL type links records.")
(defface elpher-telnet
'((t :inherit font-lock-function-name-face))
(unwind-protect
(let ((url (url-generic-parse-url url-string)))
(setf (url-fullness url) t)
- (unless (url-host url)
- (setf (url-host url) (url-filename url))
- (setf (url-filename url) ""))
+ (setf (url-filename url)
+ (url-unhex-string (url-filename url)))
(unless (url-type url)
(setf (url-type url) "gopher"))
- (if (and (url-type url)
- (url-host url))
- (let ((is-gopher (or (equal "gopher" (url-type url))
- (equal "gophers" (url-type url)))))
- (setf (url-filename url)
- (url-unhex-string (url-filename url)))
- (when (or (equal (url-filename url) "")
- (equal (url-filename url) "/"))
- (if is-gopher
- (setf (url-filename url) "/1")))
- (unless (> (url-port url) 0)
- (if is-gopher
- (setf (url-port url) 70)))
- url)
- (error "Malformed URL" url)))
+ (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"))))
+ url)
(set-match-data data))))
(defun elpher-make-gopher-address (type selector host port &optional tls)
(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)))))
(defun elpher-address-gopher-p (address)
"Return non-nill if ADDRESS object is a gopher address."
- (memq (elpher-address-protocol address) '("gopher gophers")))
+ (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))
+ (if (member (url-filename address) '("" "/"))
+ ""
+ (substring (url-filename address) 2)))
;; Node
(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)
(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."
;;; Index rendering
;;
-(defun elpher-insert-index (string)
- "Insert the index corresponding to STRING into the current buffer."
- ;; Should be able to split directly on CRLF, but some non-conformant
- ;; LF-only servers sadly exist, hence the following.
- (let ((str-processed (elpher-preprocess-text-response string)))
- (dolist (line (split-string str-processed "\n"))
- (unless (= (length line) 0)
- (let* ((type (elt line 0))
- (fields (split-string (substring line 1) "\t"))
- (display-string (elt fields 0))
- (selector (elt fields 1))
- (host (elt fields 2))
- (port (if (elt fields 3)
- (string-to-number (elt fields 3))
- nil))
- (address (elpher-make-gopher-address type selector host port)))
- (elpher-insert-index-record display-string address))))))
-
-(defun elpher-insert-margin (&optional type-name)
- "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
- (if type-name
- (progn
- (insert (format (concat "%" (number-to-string (- elpher-margin-width 1)) "s")
- (concat
- (propertize "[" 'face 'elpher-margin-brackets)
- (propertize type-name 'face 'elpher-margin-key)
- (propertize "]" 'face 'elpher-margin-brackets))))
- (insert " "))
- (insert (make-string elpher-margin-width ?\s))))
-
-(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)))
- (format "mouse-1, RET: open '%s'" (elpher-address-to-url address))))
-
-(defun elpher-insert-index-record (display-string 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))
- (type-map-entry (cdr (assoc type elpher-type-map))))
- (if type-map-entry
- (let* ((margin-code (elt type-map-entry 1))
- (face (elt type-map-entry 2))
- (node (elpher-make-node display-string address)))
- (elpher-insert-margin margin-code)
- (insert-text-button display-string
- 'face face
- 'elpher-node node
- 'action #'elpher-click-link
- 'follow-link t
- 'help-echo (elpher-node-button-help node)))
- (pcase type
- ('(gopher ?i) ;; Information
- (elpher-insert-margin)
- (insert (propertize
- (if elpher-buttonify-urls-in-directories
- (elpher-buttonify-urls display-string)
- display-string)
- 'face 'elpher-info)))
- (`(gopher ,selector-type) ;; Unknown
- (elpher-insert-margin (concat (char-to-string selector-type) "?"))
- (insert (propertize display-string
- 'face 'elpher-unknown)))))
- (insert "\n")))
+;;; Network error reporting
+;;
-(defun elpher-click-link (button)
- "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
- (let ((node (button-get button 'elpher-node)))
- (elpher-visit-node node)))
+(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)
(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
(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."
+(defun elpher-get-gopher-node (renderer)
(let* ((address (elpher-node-address elpher-current-node))
(content (elpher-get-cached-content address)))
- (if content
+ (if (and content (funcall renderer nil))
(progn
- (elpher-with-clean-buffer
- (insert content)
- (elpher-restore-pos)))
+ (elpher-with-clean-buffer)
+ (insert 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)))))))
+
+;; Index rendering
-;; Text retrieval
+(defun elpher-insert-index (string)
+ "Insert the index corresponding to STRING into the current buffer."
+ ;; Should be able to split directly on CRLF, but some non-conformant
+ ;; LF-only servers sadly exist, hence the following.
+ (let ((str-processed (elpher-preprocess-text-response string)))
+ (dolist (line (split-string str-processed "\n"))
+ (unless (= (length line) 0)
+ (let* ((type (elt line 0))
+ (fields (split-string (substring line 1) "\t"))
+ (display-string (elt fields 0))
+ (selector (elt fields 1))
+ (host (elt fields 2))
+ (port (if (elt fields 3)
+ (string-to-number (elt fields 3))
+ nil))
+ (address (elpher-make-gopher-address type selector host port)))
+ (elpher-insert-index-record display-string address))))))
+
+(defun elpher-insert-margin (&optional type-name)
+ "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
+ (if type-name
+ (progn
+ (insert (format (concat "%" (number-to-string (- elpher-margin-width 1)) "s")
+ (concat
+ (propertize "[" 'face 'elpher-margin-brackets)
+ (propertize type-name 'face 'elpher-margin-key)
+ (propertize "]" 'face 'elpher-margin-brackets))))
+ (insert " "))
+ (insert (make-string elpher-margin-width ?\s))))
+
+(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)))
+ (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.
+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))
+ (face (elt type-map-entry 2))
+ (node (elpher-make-node display-string address)))
+ (elpher-insert-margin margin-code)
+ (insert-text-button display-string
+ 'face face
+ 'elpher-node node
+ 'action #'elpher-click-link
+ 'follow-link t
+ 'help-echo (elpher-node-button-help node)))
+ (pcase type
+ ((or '(gopher ?i) 'nil) ;; Information
+ (elpher-insert-margin)
+ (insert (propertize
+ (if elpher-buttonify-urls-in-directories
+ (elpher-buttonify-urls display-string)
+ display-string)
+ 'face 'elpher-info)))
+ (`(gopher ,selector-type) ;; Unknown
+ (elpher-insert-margin (concat (char-to-string selector-type) "?"))
+ (insert (propertize display-string
+ 'face 'elpher-unknown)))))
+ (insert "\n")))
+
+(defun elpher-click-link (button)
+ "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
+ (let ((node (button-get button 'elpher-node)))
+ (elpher-visit-node node)))
+
+(defun elpher-render-index (data)
+ "Render DATA as an index."
+ (elpher-with-clean-buffer
+ (if (not data)
+ t
+ (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)
'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)
+ "Render DATA as text."
+ (elpher-with-clean-buffer
+ (if (not data)
+ t
+ (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)))
+(defun elpher-render-image (data)
+ "Display DATA as image."
+ (if (not data)
+ f
(if (display-images-p)
(progn
- (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))))
-
-;; Search retrieval
-
-(defun elpher-get-search-node ()
- "Getter which submits a search query to the address of the current node."
- (let* ((address (elpher-node-address elpher-current-node))
- (content (elpher-get-cached-content address))
- (aborted t))
- (if content
- (progn
- (elpher-with-clean-buffer
- (insert content)
- (elpher-restore-pos))
- (message "Displaying cached search results. Reload to perform a new search."))
+ (let ((image (create-image
+ data
+ nil t)))
+ (elpher-with-clean-buffer
+ (insert-image image)
+ (elpher-restore-pos))))
+ (elpher-save-to-file data))))
+
+;; Search retrieval and rendering
+
+(defun elpher-get-gopher-query-node (renderer)
+ (let* ((address (elpher-node-address elpher-current-node))
+ (content (elpher-get-cached-content address))
+ (aborted t))
+ (if (and content (funcall renderer nil))
+ (elpher-with-clean-buffer
+ (insert content)
+ (elpher-restore-pos)
+ (message "Displaying cached search results. Reload to perform a new search."))
(unwind-protect
(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)
+ (equal (elpher-address-type address) "gophers"))))
(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)
+ (funcall renderer elpher-selector-string)
+ (elpher-restore-pos)))))
(if aborted
(elpher-visit-parent-node))))))
-
-;; Raw server response retrieval
-
-(defun elpher-get-node-raw ()
- "Getter which retrieves the raw server response for the current node."
- (let ((address (elpher-node-address elpher-current-node)))
- (elpher-with-clean-buffer
- (insert "LOADING RAW SERVER RESPONSE... (use 'u' to cancel)"))
- (elpher-get-selector address
- (lambda (proc event)
- (unless (string-prefix-p "deleted" event)
- (elpher-with-clean-buffer
- (insert elpher-selector-string)
- (goto-char (point-min)))))))
- (message "Displaying raw server response. Reload or redraw to return to standard view."))
-;; File export retrieval
+;; Raw server response rendering
-(defvar elpher-download-filename)
+(defun elpher-render-raw (data)
+ "Display raw DATA in buffer."
+ (if (not data)
+ f
+ (elpher-with-clean-buffer
+ (insert data)
+ (goto-char (point-min)))
+ (message "Displaying raw server response. Reload or redraw to return to standard view.")))
+
+;; File save "rendering"
+
+(defun elpher-render-download (data)
+ "Save DATA to file."
+ (if (not data)
+ f
+ (let* ((address (elpher-node-address elpher-current-node))
+ (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 "Download complete. Save file as: "
+ nil nil nil
+ (if (> (length filename-proposal) 0)
+ filename-proposal
+ "gopher.file"))))
+ (with-temp-file filename
+ (insert elpher-selector-string)
+ (message (format "Saved to file %s."
+ elpher-download-filename)))))))
+
+;; HTML rendering
+
+(defun elpher-render-html (data)
+ "Render DATA as HTML using shr."
+ (elpher-with-clean-buffer
+ (if (not data)
+ t
+ (let ((dom (with-temp-buffer
+ (insert string)
+ (libxml-parse-html-region (point-min) (point-max)))))
+ (shr-insert-document dom)))))
+
+;; Gemini node retrieval
+
+(defvar elpher-gemini-response)
+
+
+(defun elpher-get-gemini-response (address renderer)
+ "Retrieve gemini ADDRESS, then execute RENDERER on the result.
+The response is stored in the variable ‘elpher-gemini-response’."
+ (setq elpher-gemini-response "")
+ (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)
+ (if (> (elpher-address-port address) 0)
+ (elpher-address-port address)
+ 1965)
+ :type 'tls)))
+ (set-process-coding-system proc 'binary)
+ (set-process-filter proc
+ (lambda (proc string)
+ (setq elpher-gemini-response
+ (concat elpher-gemini-response string))))
+ (set-process-sentinel proc
+ (lambda (proc event)
+ (unless (string-prefix-p "deleted" event)
+ (elpher-process-gemini-response #'after))))
+ (process-send-string proc
+ (concat (elpher-address-to-url address) "\r\n")))))
+
+
+(defun elpher-process-gemini-response (renderer)
+ "Process the gemini response found in the variable elpher-gemini-response and
+pass the result to RENDERER."
+ (condition-case the-error
+ (unless (string-prefix-p "deleted" event)
+ (let* ((response-header (car (split-string elpher-gemini-response "\r\n")))
+ (response-body (substring elpher-gemini-response
+ (+ (string-match "\r\n" elpher-gemini-response) 2)))
+ (response-code (car (split-string response-header)))
+ (response-meta (string-trim
+ (substring response-header
+ (string-match "[ \t]+" response-header)))))
+ (pcase (elt response-code 0)
+ (?1 ; Input required
+ (elpher-with-clean-buffer
+ (insert "Gemini server is requesting input."))
+ (let* ((query-string (read-string (concat response-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 #'renderer)))
+ (?2 ; Normal response
+ (message response-header)
+ (funcall #'renderer elpher-gemini-response))
+ (?3 ; Redirect
+ (message "Following redirect to %s" meta)
+ (let ((redirect-address (elpher-address-from-gemini-url meta)))
+ (elpher-get-gemini redirect-address #'renderer)))
+ (?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-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-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: "
- nil nil nil
- (if (> (length filename-proposal) 0)
- filename-proposal
- "gopher.file"))))
- (message "Downloading...")
- (setq elpher-download-filename filename)
- (condition-case the-error
- (elpher-get-selector address
- (lambda (proc event)
- (let ((coding-system-for-write 'binary))
- (with-temp-file elpher-download-filename
- (insert elpher-selector-string)
- (message (format "Download complate, saved to file %s."
- elpher-download-filename)))))
- t)
- (error
- (error "Error downloading %s" elpher-download-filename))))))
-
-;; HTML node retrieval
-
-(defun elpher-insert-rendered-html (string)
- "Use shr to insert rendered view of html STRING into current buffer."
- (let ((dom (with-temp-buffer
- (insert string)
- (libxml-parse-html-region (point-min) (point-max)))))
- (shr-insert-document dom)))
-
-(defun elpher-get-html-node ()
- "Getter which retrieves and renders an HTML node."
+(defun elpher-get-gemini-node (renderer)
+ "Getter which retrieves and renders a Gemini node and renders it using RENDERER."
(let* ((address (elpher-node-address elpher-current-node))
- (selector (elpher-gopher-address-selector address)))
- (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
+ (content (elpher-get-cached-content address)))
+ (condition-case the-error
+ (if (and content (funcall renderer nil))
+ (progn
+ (insert content)
+ (elpher-restore-pos))
+ (elpher-with-clean-buffer
+ (insert "LOADING GEMINI... (use 'u' to cancel)"))
+ (elpher-get-gemini 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))))))))))
-
+ (funcall renderer elpher-gemini-response)
+ (elpher-restore-pos)))))
+ (error
+ (elpher-network-error address the-error)))))
+
+
+(defun elpher-render-gemini (data)
+ "Render gemini response DATA."
+ (if (not data)
+ t
+ (let* ((response-header (car (split-string data "\r\n")))
+ (response-body (substring data (+ (string-match "\r\n" data) 2)))
+ (mime-type-string (string-trim (substring response-header 2)))
+ (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-gemini-text/gemini response-body parameters))
+ ((pred (string-prefix-p "text/"))
+ (elpher-render-gemini-text/plain response-body parameters))
+ ((pred (string-prefix-p "image/"))
+ (elpher-render-image response-body))
+ (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)
+ "Extract address from URL with defaults as per gemini map files."
+ (let ((address (url-generic-parse-url url)))
+ (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-gemini-text/gemini (data parameters)
+ (elpher-with-clean-buffer
+ (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))
+ (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-cache-content
+ (elpher-node-address elpher-current-node)
+ (buffer-string))))
+
+(defun elpher-render-gemini-text/plain (data parameters)
+ (elpher-with-clean-buffer
+ (insert (elpher-buttonify-urls (elpher-preprocess-text-response data)))
+ (elpher-cache-content
+ (elpher-node-address elpher-current-node)
+ (buffer-string))))
-;; URL node opening
+;; Other URL node opening
-(defun elpher-get-url-node ()
- "Getter which attempts to open the URL specified by the current node."
+(defun elpher-get-other-url-node (renderer)
+ "Getter which attempts to open the URL specified by the current node (RENDERER must be nil)."
+ (when renderer
+ (elpher-visit-parent-node)
+ (error "Command not supported for general URLs"))
(let* ((address (elpher-node-address elpher-current-node))
(url (elpher-address-to-url address)))
(progn
;; Telnet node connection
-(defun elpher-get-telnet-node ()
- "Getter which opens a telnet connection to the server specified by the current node."
+(defun elpher-get-telnet-node (renderer)
+ "Opens a telnet connection to the current node address (RENDERER must be nil)."
+ (when renderer
+ (elpher-visit-parent-node)
+ (error "Command not supported for telnet URLs"))
(let* ((address (elpher-node-address elpher-current-node))
(host (elpher-address-host address))
(port (elpher-address-port address)))
;; Start page node retrieval
-(defun elpher-get-start-node ()
- "Getter which displays the start page."
+(defun elpher-get-start-node (renderer)
+ "Getter which displays the start page (RENDERER must be nil)."
+ (when renderer
+ (elpher-visit-parent-node)
+ (error "Command not supported for start page."))
(elpher-with-clean-buffer
(insert " --------------------------------------------\n"
" Elpher Gopher Client \n"
;; Bookmarks page node retrieval
-(defun elpher-get-bookmarks-node ()
- "Getter to load and display the current bookmark list."
+(defun elpher-get-bookmarks-node (renderer)
+ "Getter to load and display the current bookmark list (RENDERER must be nil)."
+ (when renderer
+ (elpher-visit-parent-node)
+ (error "Command not supported for bookmarks page."))
(elpher-with-clean-buffer
(insert "---- Bookmark list ----\n\n")
(let ((bookmarks (elpher-load-bookmarks)))
(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: ")))
+ (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*")
(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-address-to-url address))))
+ (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 ()
(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-gopher-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 (elpher-address-to-url 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 ()
(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
"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