;; Copyright (C) 2021 Omar Polo <op@omarpolo.com>
;; Copyright (C) 2021 Noodles! <nnoodle@chiru.no>
;; Copyright (C) 2021 Abhiseck Paira <abhiseckpaira@disroot.org>
+;; Copyright (C) 2021 Daniel Semyonov <daniel@dsemy.com>
;; Copyright (C) 2020-2021 Alex Schroeder <alex@gnu.org>
;; Copyright (C) 2020 Zhiwei Chen <chenzhiwei03@kuaishou.com>
;; Copyright (C) 2020 condy0919 <condy0919@gmail.com>
;; 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 (not (string-empty-p (url-host url)))
+ (setf (url-fullness url) t)
+ (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."
"Set the address corresponding to PAGE to NEW-ADDRESS."
(setcar (cdr page) new-address))
-(defun elpher-page-from-url (url)
+(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."
- (elpher-make-page (elpher-decode (url-unhex-string url))
- (elpher-address-from-url url)))
+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
+ (host (url-host address))
+ (pass (url-password address)))
+ (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
+ (elpher-decode (url-unhex-string (url-recreate-url address))))
+ (setf (url-host address) host)
+ (setf (url-password address) pass)
+ (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
;;
(if (display-images-p)
(let* ((image (create-image
data
- nil t))
- (window (get-buffer-window elpher-buffer-name)))
- (when 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)))
+ nil t)))
+ (if (not image)
+ (error "Unsupported image format")
+ (let ((window (get-buffer-window elpher-buffer-name)))
+ (when 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))))
(elpher-render-download data))))
;; Search retrieval and rendering
(defun elpher-gemini-get-link-display-string (link-line)
"Extract the display string portion of LINK-LINE, a gemini map file link line.
-Returns the url portion in the event that the display-string portion is empty."
+Return nil if this portion is not provided."
(let* ((rest (string-trim (elt (split-string link-line "=>") 1)))
(idx (string-match "[ \t]" rest)))
- (string-trim (if idx
- (substring rest (+ idx 1))
- rest))))
+ (and idx
+ (elpher-color-filter-apply (string-trim (substring rest (+ idx 1)))))))
(defun elpher-collapse-dot-sequences (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 ((equal b "..") (cdr a))
path nil))
(path-normalized (reverse path-reversed-normalized)))
(if path-normalized
- (concat
- "/"
- (string-join (reverse path-reversed-normalized) "/")
- (if (string-match-p (rx (: (or "." ".." "/") line-end)) filename) "/") "")
+ (concat "/" (string-join path-normalized "/") (and is-directory "/"))
"/")))
(defun elpher-address-from-gemini-url (url)
(let ((address (url-generic-parse-url url))
(current-address (elpher-page-address elpher-current-page)))
(unless (and (url-type address) (not (url-fullness address))) ;avoid mangling mailto: urls
- (setf (url-fullness address) t)
(if (url-host address) ;if there is an explicit host, filenames are absolute
(if (string-empty-p (url-filename address))
(setf (url-filename address) "/")) ;ensure empty filename is marked as absolute
(setf (url-host address) (url-host current-address))
+ (setf (url-fullness address) (url-host address)) ; set fullness to t if host is set
(setf (url-portspec address) (url-portspec current-address)) ; (url-port) too slow!
(unless (string-prefix-p "/" (url-filename address)) ;deal with relative links
(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")
(defun elpher-gemini-insert-link (link-line)
"Insert link described by LINK-LINE into a text/gemini document."
- (let* ((url (elpher-gemini-get-link-url link-line))
- (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))))
- (when display-string
- (insert elpher-gemini-link-string)
- (if type-map-entry
- (let* ((face (elt type-map-entry 3))
- (filtered-display-string (elpher-color-filter-apply display-string))
- (page (elpher-make-page filtered-display-string address)))
- (insert-text-button filtered-display-string
- 'face face
- 'elpher-page page
- 'action #'elpher-click-link
- '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.")
+ (let ((url (elpher-gemini-get-link-url link-line)))
+ (when url
+ (let* ((given-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)))
+ (fill-prefix (make-string (+ 1 (length elpher-gemini-link-string)) ?\s)))
+ (insert elpher-gemini-link-string)
+ (if type-map-entry
+ (let* ((face (elt type-map-entry 3))
+ (display-string (or given-display-string
+ (elpher-address-to-iri address)))
+ (page (elpher-make-page display-string
+ address)))
+ (insert-text-button display-string
+ 'face face
+ 'elpher-page page
+ 'action #'elpher-click-link
+ 'follow-link t
+ 'help-echo #'elpher--page-button-help))
+ (insert (propertize display-string 'face 'elpher-unknown)))
+ (newline)))))
(defun elpher-gemini-insert-header (header-line)
"Insert header described by HEADER-LINE into a text/gemini document.
(/ (* 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 'rear-nonsticky t))
+ (insert (propertize header
+ 'face face
+ 'gemini-heading t
+ 'rear-nonsticky t))
(newline))))
(defun elpher-gemini-insert-text (text-line)
(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))
(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))))
(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
(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 #'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-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"
(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 ((page (elpher-page-from-url trimmed-host-or-url)))
+ (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-page-from-url url)))))
(defun elpher-info-page (page)
"Display URL of PAGE in minibuffer."
(let ((address (elpher-page-address page)))
- (message "%s" (elpher-address-to-url address))))
+ (message "%s" (elpher-address-to-iri address))))
(defun elpher-info-link ()
"Display information on page corresponding to link at point."
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)