X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?p=elpher.git;a=blobdiff_plain;f=elpher.el;h=3656189260624be1314edfa73219ddce6db1cfc7;hp=e6243e524a7a6ce5417cee8c11faac960fd14d79;hb=d5116907245377fe7103153408861564cc2b0dcd;hpb=9194772213702aa5a1ab62637ad73b7f0aca3a1e diff --git a/elpher.el b/elpher.el index e6243e5..3656189 100644 --- a/elpher.el +++ b/elpher.el @@ -20,7 +20,7 @@ ;; Author: Tim Vaughan ;; Created: 11 April 2019 -;; Version: 3.1.0 +;; Version: 3.2.2 ;; Keywords: comm gopher ;; Homepage: https://thelambdalab.xyz/elpher ;; Package-Requires: ((emacs "27.1")) @@ -85,7 +85,7 @@ ;;; Global constants ;; -(defconst elpher-version "3.1.0" +(defconst elpher-version "3.2.2" "Current version of elpher.") (defconst elpher-margin-width 6 @@ -225,7 +225,7 @@ Otherwise, \\[elpher-show-bookmarks] will visit a special elpher bookmark 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:\". @@ -324,15 +324,17 @@ the start page." ;; 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)) @@ -340,6 +342,8 @@ the start page." (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 @@ -422,7 +426,7 @@ address refers to, via the table `elpher-type-map'." (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." @@ -482,9 +486,9 @@ If no address is defined, returns 0. (This is for compatibility with the URL li (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." @@ -498,6 +502,31 @@ If no address is defined, returns 0. (This is for compatibility with the URL li "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.") @@ -566,6 +595,21 @@ previously-visited pages,unless NO-HISTORY is non-nil." (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 ;; @@ -575,15 +619,16 @@ previously-visited pages,unless NO-HISTORY is non-nil." (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)))) @@ -650,8 +695,7 @@ away CRs and any terminating period." (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 @@ -773,7 +817,7 @@ the host operating system and the local network capabilities.)" (elpher-process-cleanup) (cond ; Try again with IPv4 - ((not (or force-ipv4 socks)) + ((not (or elpher-ipv4-always force-ipv4 socks)) (message "Connection timed out. Retrying with IPv4.") (elpher-get-host-response address default-port query-string @@ -794,7 +838,9 @@ the host operating system and the local network capabilities.)" (proc (if socks (socks-open-network-stream "elpher-process" nil host service) (make-network-process :name "elpher-process" :host host - :family (and force-ipv4 'ipv4) + :family (and (or force-ipv4 + elpher-ipv4-always) + 'ipv4) :service service :buffer nil :nowait t @@ -1044,9 +1090,7 @@ displayed. The _WINDOW argument is currently unused." (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. @@ -1130,8 +1174,8 @@ If ADDRESS is not supplied or nil the record is rendered as an 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))) + (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))) @@ -1422,17 +1466,20 @@ Returns the url portion in the event that the display-string portion is empty." 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. @@ -1452,6 +1499,8 @@ treatment that a separate function is warranted." (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") @@ -1465,7 +1514,8 @@ treatment that a separate function is warranted." (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 @@ -1479,10 +1529,7 @@ treatment that a separate function is warranted." 'follow-link t 'help-echo #'elpher--page-button-help)) (insert (propertize display-string 'face 'elpher-unknown))) - (insert "\n")))) - -(defvar elpher--gemini-page-headings nil - "List of headings on the page.") + (newline)))) (defun elpher-gemini-insert-header (header-line) "Insert header described by HEADER-LINE into a text/gemini document. @@ -1500,19 +1547,26 @@ by HEADER-LINE." (/ (* fill-column (font-get (font-spec :name (face-font 'default)) :size)) (font-get (font-spec :name (face-font face)) :size)) fill-column))) - (setq elpher--gemini-page-headings (cons (cons header (point)) - elpher--gemini-page-headings)) (unless (display-graphic-p) (insert (make-string level ?#) " ")) - (insert (propertize header 'face face)) + (insert (propertize header + 'face face + 'gemini-heading t + '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'." - (string-match "\\(^[ \t]*\\)\\(\\*[ \t]+\\|>[ \t]*\\)?" text-line) - (let* ((line-prefix (match-string 2 text-line)) + (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) @@ -1528,8 +1582,8 @@ width defined by `elpher-gemini-max-fill-width'." (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 2 text-line) - (replace-regexp-in-string "[>\*]" " " (match-string 0 text-line)) + (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))) @@ -1537,7 +1591,6 @@ width defined by `elpher-gemini-max-fill-width'." (defun elpher-render-gemini-map (data _parameters) "Render DATA as a gemini map file, PARAMETERS is currently unused." (elpher-with-clean-buffer - (setq elpher--gemini-page-headings nil) (let ((preformatted nil)) (auto-fill-mode 1) (setq-local fill-column (min (window-width) elpher-gemini-max-fill-width)) @@ -1551,7 +1604,6 @@ width defined by `elpher-gemini-max-fill-width'." (elpher-gemini-insert-link line)) ((string-prefix-p "#" line) (elpher-gemini-insert-header line)) (t (elpher-gemini-insert-text line))))) - (setq elpher--gemini-page-headings (nreverse elpher--gemini-page-headings)) (elpher-cache-content (elpher-page-address elpher-current-page) (buffer-string)))) @@ -1564,6 +1616,18 @@ width defined by `elpher-gemini-max-fill-width'." (elpher-page-address elpher-current-page) (buffer-string)))) +(defun elpher-build-current-imenu-index () + (save-excursion + (goto-char (point-min)) + (let ((match nil) + (headers nil)) + (while (setq match (text-property-search-forward 'gemini-heading t t)) + (push (cons + (buffer-substring-no-properties (prop-match-beginning match) + (prop-match-end match)) + (prop-match-beginning match)) + headers)) + (reverse headers)))) ;; Finger page connection @@ -1651,6 +1715,8 @@ Assumes UTF-8 encoding for all text files." (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)))) @@ -1703,14 +1769,14 @@ Assumes UTF-8 encoding for all text files." (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" @@ -1849,8 +1915,7 @@ then making that buffer the current buffer. It should not switch 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)) @@ -2082,11 +2147,12 @@ supports the old protocol elpher, where the link is self-contained." (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)) @@ -2096,11 +2162,10 @@ When run interactively HOST-OR-URL is read from the minibuffer." "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." @@ -2335,12 +2400,11 @@ When run interactively HOST-OR-URL is read from the minibuffer." This mode is automatically enabled by the interactive functions which initialize the client, namely `elpher', and `elpher-go'." - (setq-local elpher--gemini-page-headings nil) (setq-local elpher-current-page nil) (setq-local elpher-history nil) (setq-local elpher-buffer-name (buffer-name)) (setq-local bookmark-make-record-function #'elpher-bookmark-make-record) - (setq-local imenu-create-index-function (lambda () elpher--gemini-page-headings)) + (setq-local imenu-create-index-function #'elpher-build-current-imenu-index) (setq-local xterm-color-preserve-properties t)) (when (fboundp 'evil-set-initial-state)