;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
;; Created: 11 April 2019
-;; Version: 3.2.1
+;; Version: 3.2.2
;; Keywords: comm gopher
;; Homepage: https://thelambdalab.xyz/elpher
;; Package-Requires: ((emacs "27.1"))
;;; Global constants
;;
-(defconst elpher-version "3.2.1"
+(defconst elpher-version "3.2.2"
"Current version of elpher.")
(defconst elpher-margin-width 6
page within which all of the standard elpher keybindings are active."
:type '(boolean))
-(defcustom elpher-start-page "about:welcome"
+(defcustom elpher-start-page-url "about:welcome"
"Specify the page displayed initially by elpher.
The default welcome screen is \"about:welcome\", while the bookmarks list
is \"about:bookmarks\". You can also specify local files via \"file:\".
;; dynamically for and by elpher. All others represent pages which
;; rely on content retrieved over the network.
-(defun elpher-address-from-url (url-string)
- "Create a ADDRESS object corresponding to the given URL-STRING."
+(defun elpher-address-from-url (url-string &optional default-scheme)
+ "Create a ADDRESS object corresponding to the given URL-STRING.
+If DEFAULT-SCHEME is non-nil, this sets the scheme of the URL when one
+is not explicitly given."
(let ((data (match-data))) ; Prevent parsing clobbering match data
(unwind-protect
(let ((url (url-generic-parse-url url-string)))
(unless (and (not (url-fullness url)) (url-type url))
(setf (url-fullness url) t)
(unless (url-type url)
- (setf (url-type url) elpher-default-url-type))
+ (setf (url-type url) default-scheme))
(unless (url-host url)
(let ((p (split-string (url-filename url) "/" nil nil)))
(setf (url-host url) (car p))
(if (cdr p)
(concat "/" (mapconcat #'identity (cdr p) "/"))
""))))
+ (when (url-host url)
+ (setf (url-host url) (puny-encode-domain (url-host url))))
(when (or (equal "gopher" (url-type url))
(equal "gophers" (url-type url)))
;; Gopher defaults
(defun elpher-address-gopher-p (address)
"Return non-nill if ADDRESS object is a gopher address."
- (eq 'gopher (elpher-address-type address)))
+ (pcase (elpher-address-type address) (`(gopher ,_) t)))
(defun elpher-address-protocol (address)
"Retrieve the transport protocol for ADDRESS."
(list display-string address))
(defun elpher-make-start-page ()
- "Create the welcome page."
+ "Create the start page."
(elpher-make-page "Start Page"
- (elpher-address-from-url elpher-start-page)))
+ (elpher-address-from-url elpher-start-page-url)))
(defun elpher-page-display-string (page)
"Retrieve the display string corresponding to PAGE."
"Set the address corresponding to PAGE to NEW-ADDRESS."
(setcar (cdr page) new-address))
+(defun elpher-page-from-url (url &optional default-scheme)
+ "Create a page with address and display string defined by URL.
+The URL is unhexed prior to its use as a display string to improve
+readability.
+
+If DEFAULT-SCHEME is non-nil, this scheme is applied to the URL
+in the instance that URL itself doesn't specify one."
+ (let ((address (elpher-address-from-url url default-scheme)))
+ (elpher-make-page (elpher-address-to-iri address) address)))
+
+(defun elpher-address-to-iri (address)
+ "Return an IRI for ADDRESS.
+Decode percent-escapes and handle punycode in the domain name.
+Drop the password, if any."
+ (let ((data (match-data))) ; Prevent parsing clobbering match data
+ (unwind-protect
+ (let* ((host (url-host address))
+ (pass (url-password address)))
+ (when host
+ (setf (url-host address) (puny-decode-domain host)))
+ (when pass ; RFC 3986 says we should not render
+ (setf (url-password address) nil)) ; the password as clear text
+ (url-recreate-url address))
+ (set-match-data data))))
+
(defvar elpher-current-page nil
"The current page for this Elpher buffer.")
(goto-char pos)
(goto-char (point-min)))))
+(defun elpher-get-default-url-scheme ()
+ "Suggest a default URL scheme to use for visiting addresses based on the current page."
+ (if elpher-current-page
+ (let* ((address (elpher-page-address elpher-current-page))
+ (current-type (elpher-address-type address)))
+ (pcase current-type
+ ((or (and 'file (guard (not elpher-history)))
+ `(about ,_))
+ elpher-default-url-type)
+ (`(about ,_)
+ elpher-default-url-type)
+ (_
+ (url-type address))))
+ elpher-default-url-type))
+
;;; Buffer preparation
;;
(defun elpher-update-header ()
"If `elpher-use-header' is true, display current page info in window header."
- (if elpher-use-header
+ (if (and elpher-use-header elpher-current-page)
(let* ((display-string (elpher-page-display-string elpher-current-page))
+ (sanitized-display-string (replace-regexp-in-string "%" "%%" display-string))
(address (elpher-page-address elpher-current-page))
(tls-string (if (and (not (elpher-address-about-p address))
(member (elpher-address-protocol address)
'("gophers" "gemini")))
" [TLS encryption]"
""))
- (header (concat display-string
+ (header (concat sanitized-display-string
(propertize tls-string 'face 'bold))))
(setq header-line-format header))))
(insert string)
(goto-char (point-min))
(while (re-search-forward elpher-url-regex nil t)
- (let ((page (elpher-make-page (substring-no-properties (match-string 0))
- (elpher-address-from-url (match-string 0)))))
+ (let ((page (elpher-page-from-url (substring-no-properties (match-string 0)))))
(make-text-button (match-beginning 0)
(match-end 0)
'elpher-page page
(when button
(let* ((page (button-get button 'elpher-page))
(address (elpher-page-address page)))
- (format "mouse-1, RET: open '%s'" (if (elpher-address-about-p address)
- address
- (elpher-address-to-url 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.
nil t))
(window (get-buffer-window elpher-buffer-name)))
(when window
- (setf (image-property image :max-width) (window-pixel-width window))
- (setf (image-property image :max-height) (- (window-pixel-height window)
- (window-header-line-height window)
- (window-mode-line-height window)
- (window-scroll-bar-height window)
- (window-bottom-divider-width window))))
+ (setf (image-property image :max-width) (window-body-width window t))
+ (setf (image-property image :max-height) (window-body-height window t)))
(elpher-with-clean-buffer
(insert-image image)
(elpher-restore-pos)))
rest))))
(defun elpher-collapse-dot-sequences (filename)
- "Collapse dot sequences in FILENAME.
-For instance, the filename /a/b/../c/./d will reduce to /a/c/d"
- (let* ((path (split-string filename "/"))
+ "Collapse dot sequences in the (absolute) FILENAME.
+For instance, the filename \"/a/b/../c/./d\" will reduce to \"/a/c/d\""
+ (let* ((path (split-string filename "/" t))
+ (is-directory (string-match-p (rx (: (or "." ".." "/") line-end)) filename))
(path-reversed-normalized
(seq-reduce (lambda (a b)
- (cond ((and a (equal b "..") (cdr a)))
- ((and (not a) (equal b "..")) a) ;leading .. are dropped
+ (cond ((equal b "..") (cdr a))
((equal b ".") a)
(t (cons b a))))
- path nil)))
- (string-join (reverse path-reversed-normalized) "/")))
+ path nil))
+ (path-normalized (reverse path-reversed-normalized)))
+ (if path-normalized
+ (concat "/" (string-join path-normalized "/") (and is-directory "/"))
+ "/")))
(defun elpher-address-from-gemini-url (url)
"Extract address from URL with defaults as per gemini map files.
(setf (url-filename address)
(concat (file-name-directory (url-filename current-address))
(url-filename address)))))
+ (when (url-host address)
+ (setf (url-host address) (puny-encode-domain (url-host address))))
(unless (url-type address)
(setf (url-type address) (url-type current-address)))
(when (equal (url-type address) "gemini")
(display-string (elpher-gemini-get-link-display-string link-line))
(address (elpher-address-from-gemini-url url))
(type (if address (elpher-address-type address) nil))
- (type-map-entry (cdr (assoc type elpher-type-map))))
+ (type-map-entry (cdr (assoc type elpher-type-map)))
+ (fill-prefix (make-string (+ 1 (length elpher-gemini-link-string)) ?\s)))
(when display-string
(insert elpher-gemini-link-string)
(if type-map-entry
'follow-link t
'help-echo #'elpher--page-button-help))
(insert (propertize display-string 'face 'elpher-unknown)))
- (insert "\n"))))
+ (newline))))
(defvar elpher--gemini-page-headings nil
"List of headings on the page.")
elpher--gemini-page-headings))
(unless (display-graphic-p)
(insert (make-string level ?#) " "))
- (insert (propertize header 'face face))
+ (insert (propertize header 'face face 'rear-nonsticky t))
(newline))))
(defun elpher-gemini-insert-text (text-line)
"Insert a plain non-preformatted TEXT-LINE into a text/gemini document.
This function uses Emacs' auto-fill to wrap text sensibly to a maximum
width defined by `elpher-gemini-max-fill-width'."
- (if (string-empty-p text-line)
- (insert "\n")
- (string-match
- (rx (: line-start
- (* (any " \t"))
- (optional
- (group (or (: "*" (+ (any " \t")))
- (: ">" (* (any " \t"))))))))
- text-line)
- (let* ((line-prefix (match-string 1 text-line))
- (processed-text-line
- (if line-prefix
- (cond ((string-prefix-p "*" line-prefix)
- (concat
- (replace-regexp-in-string "\\*"
- elpher-gemini-bullet-string
- (match-string 0 text-line))
- (substring text-line (match-end 0))))
- ((string-prefix-p ">" line-prefix)
- (propertize text-line 'face 'elpher-gemini-quoted))
- (t text-line))
- text-line))
- (adaptive-fill-mode t)
- ;; fill-prefix is important for adaptive-fill-mode: without
- ;; it, multi-line list items are not indented correct
- (fill-prefix (if (match-string 1 text-line)
- (make-string (length (match-string 0 text-line)) ?\s)
- nil)))
- (insert (elpher-process-text-for-display processed-text-line))
- (newline))))
+ (string-match
+ (rx (: line-start
+ (* (any " \t"))
+ (optional
+ (group (or (: "*" (+ (any " \t")))
+ (: ">" (* (any " \t"))))))))
+ text-line)
+ (let* ((line-prefix (match-string 1 text-line))
+ (processed-text-line
+ (if line-prefix
+ (cond ((string-prefix-p "*" line-prefix)
+ (concat
+ (replace-regexp-in-string "\\*"
+ elpher-gemini-bullet-string
+ (match-string 0 text-line))
+ (substring text-line (match-end 0))))
+ ((string-prefix-p ">" line-prefix)
+ (propertize text-line 'face 'elpher-gemini-quoted))
+ (t text-line))
+ text-line))
+ (adaptive-fill-mode t)
+ ;; fill-prefix is important for adaptive-fill-mode: without
+ ;; it, multi-line list items are not indented correct
+ (fill-prefix (if (match-string 1 text-line)
+ (make-string (length (match-string 0 text-line)) ?\s)
+ nil)))
+ (insert (elpher-process-text-for-display processed-text-line))
+ (newline)))
(defun elpher-render-gemini-map (data _parameters)
"Render DATA as a gemini map file, PARAMETERS is currently unused."
(elpher-render-text (decode-coding-string body 'utf-8)))
((or "jpg" "jpeg" "gif" "png" "bmp" "tif" "tiff")
(elpher-render-image body))
+ ((or "gopher" "gophermap")
+ (elpher-render-index (elpher-decode body)))
(_
(elpher-render-download body))))
(elpher-restore-pos))))
(elpher-address-from-url "gemini://geminispace.info/search"))
(insert "\n"
"Your bookmarks are stored in your ")
- (let ((help-string "RET,mouse-1: Open bookmark list"))
- (insert-text-button "bookmark list"
- 'face 'link
- 'action (lambda (_)
- (interactive)
- (call-interactively #'elpher-show-bookmarks))
- 'follow-link t
- 'help-echo help-string))
+ (insert-text-button "bookmark list"
+ 'face 'link
+ 'action #'elpher-click-link
+ 'follow-link t
+ 'help-echo #'elpher--page-button-help
+ 'elpher-page
+ (elpher-make-page "Elpher Bookmarks"
+ (elpher-make-about-address 'bookmarks)))
(insert ".\n")
(insert (propertize
"(Bookmarks from legacy elpher-bookmarks files will be automatically imported.)\n"
to the buffer."
(let* ((url (cdr (assq 'location bookmark)))
(cleaned-url (string-trim url))
- (address (elpher-address-from-url cleaned-url))
- (page (elpher-make-page cleaned-url address)))
+ (page (elpher-page-from-url cleaned-url)))
(elpher-with-clean-buffer
(elpher-visit-page page))
(set-buffer (get-buffer elpher-buffer-name))
(defun elpher-go (host-or-url)
"Go to a particular gopher site HOST-OR-URL.
When run interactively HOST-OR-URL is read from the minibuffer."
- (interactive "sGopher or Gemini URL: ")
+ (interactive (list
+ (read-string (format "Visit URL (default scheme %s): " (elpher-get-default-url-scheme)))))
(let ((trimmed-host-or-url (string-trim host-or-url)))
(unless (string-empty-p trimmed-host-or-url)
- (let* ((address (elpher-address-from-url trimmed-host-or-url))
- (page (elpher-make-page trimmed-host-or-url address)))
+ (let ((page (elpher-page-from-url trimmed-host-or-url
+ (elpher-get-default-url-scheme))))
(switch-to-buffer elpher-buffer-name)
(elpher-with-clean-buffer
(elpher-visit-page page))
"Go to a particular site read from the minibuffer, initialized with the current URL."
(interactive)
(let* ((address (elpher-page-address elpher-current-page))
- (url (read-string "Gopher or Gemini URL: "
- (unless (elpher-address-about-p address)
- (elpher-address-to-url address)))))
+ (url (read-string (format "Visit URL (default scheme %s): " (elpher-get-default-url-scheme))
+ (elpher-address-to-url address))))
(unless (string-empty-p (string-trim url))
- (elpher-visit-page (elpher-make-page url (elpher-address-from-url url))))))
+ (elpher-visit-page (elpher-page-from-url url) (elpher-get-default-url-scheme)))))
(defun elpher-redraw ()
"Redraw current page."