;; - (m)enu key support, similar to Emacs' info browser,
;; - clickable web and gopher links in plain text.
-;; The caching mechanism works by maintaining a hierarchy of visited
-;; pages rather than a linear history, meaning that it is quick and
-;; easy to navigate this history.
+;; Visited pages are stored as a hierarchy rather than a linear history,
+;; meaning that navigation between these pages is quick and easy.
;; To launch Elpher, simply use 'M-x elpher'. This will open a start
;; page containing information on key bindings and suggested starting
;;; Global constants
;;
-(defconst elpher-version "1.0.0"
+(defconst elpher-version "1.1.0"
"Current version of elpher.")
(defconst elpher-margin-width 6
(mapconcat
'identity
(list "i\tfake\tfake\t1"
- "i--------------------------------------------\tfake\tfake\t1"
- "i Elpher Gopher Client \tfake\tfake\t1"
- (format "i version %s\tfake\tfake\t1" elpher-version)
- "i--------------------------------------------\tfake\tfake\t1"
+ "i --------------------------------------------\tfake\tfake\t1"
+ "i Elpher Gopher Client \tfake\tfake\t1"
+ (format "i version %s\tfake\tfake\t1" elpher-version)
+ "i --------------------------------------------\tfake\tfake\t1"
"i\tfake\tfake\t1"
"iUsage:\tfake\tfake\t1"
"i\tfake\tfake\t1"
"isearch terms:\tfake\tfake\t1"
"i\tfake\tfake\t1"
"7Veronica-2 Gopher Search Engine\t/v2/vs\tgopher.floodgap.com\t70"
- ".")
+ ".\r\n")
"\r\n")
"Source for elpher start page.")
Otherwise, use the system browser via the BROWSE-URL function."
:type '(boolean))
+(defcustom elpher-buttonify-urls-in-directories nil
+ "If non-nil, turns URLs matched in directories into clickable buttons."
+ :type '(boolean))
+
(defcustom elpher-cache-images nil
"If non-nil, cache images in memory in the same way as other content."
:type '(boolean))
use as the start page."
:type '(list string string integer))
-
;;; Model
;;
;; Node
+(defvar elpher-seen-nodes (make-hash-table :test 'equal)
+ "Table mapping addresses to existing (seen) node objects.")
+
(defun elpher-make-node (parent address getter &optional content pos)
"Create a node in the gopher page hierarchy.
page.
The optional arguments CONTENT and POS can be used to fill the cached
-content and cursor position fields of the node."
- (list parent address getter content pos))
+content and cursor position fields of the node.
+
+If the hash table `elpher-seen-nodes' contains a key equal to ADDRESS,
+the node contained as its value will be returned instead."
+ (let ((existing-node (gethash address elpher-seen-nodes)))
+ (if existing-node
+ existing-node
+ (let ((new-node (list parent address getter content pos)))
+ (puthash address new-node elpher-seen-nodes)
+ new-node))))
(defun elpher-node-parent (node)
"Retrieve the parent node of NODE."
;; Node graph traversal
-(defvar elpher-current-node)
+(defvar elpher-current-node nil)
(defun elpher-visit-node (node &optional getter)
"Visit NODE using its own getter or GETTER, if non-nil."
"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.
- (dolist (line (split-string (replace-regexp-in-string "\r" "" string) "\n"))
- (unless (= (length line) 0)
- (elpher-insert-index-record line))))
+ (let* ((str-no-period (replace-regexp-in-string "\r\n\.\r\n$" "\r\n" string))
+ (str-no-cr (replace-regexp-in-string "\r" "" str-no-period)))
+ (dolist (line (split-string str-no-cr "\n"))
+ (unless (= (length line) 0)
+ (elpher-insert-index-record line)))))
(defun elpher-insert-margin (&optional type-name)
"Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
selector host port)))
(pcase type
(?i (elpher-insert-margin) ;; Information
- (insert (propertize display-string
- 'face 'elpher-info)))
+ (insert (propertize
+ (if elpher-buttonify-urls-in-directories
+ (elpher-buttonify-urls display-string)
+ display-string)
+ 'face 'elpher-info)))
(?h (elpher-insert-margin "W") ;; Web link
(let ((url (elt (split-string selector "URL:") 1)))
(insert-text-button display-string
'action #'elpher-click-url
'follow-link t
'help-echo (format "mouse-1, RET: open url %s" url))))
- (?.) ;; Occurs at end of index, can safely ignore.
(tp (elpher-insert-margin (concat (char-to-string tp) "?"))
(insert (propertize display-string
'face 'elpher-unknown-face)))))
(defun elpher-redraw ()
"Redraw current page."
(interactive)
- (elpher-visit-node elpher-current-node))
+ (if elpher-current-node
+ (elpher-visit-node elpher-current-node)
+ (message "No current site.")))
(defun elpher-reload ()
"Reload current page."
(interactive)
- (elpher-reload-current-node))
+ (if elpher-current-node
+ (elpher-reload-current-node)
+ (message "No current site.")))
(defun elpher-view-raw ()
"View current page as plain text."
(interactive)
- (elpher-visit-node elpher-current-node
- #'elpher-get-node-raw))
+ (if elpher-current-node
+ (elpher-visit-node elpher-current-node
+ #'elpher-get-node-raw)
+ (message "No current site.")))
(defun elpher-back ()
"Go to previous site."
(defun elpher ()
"Start elpher with default landing page."
(interactive)
- (switch-to-buffer "*elpher*")
- (setq elpher-current-node nil)
- (let ((start-node (elpher-make-node nil
- elpher-start-address
- #'elpher-get-index-node)))
- (elpher-visit-node start-node))
+ (if (get-buffer "*elpher*")
+ (switch-to-buffer "*elpher*")
+ (switch-to-buffer "*elpher*")
+ (setq elpher-current-node nil)
+ (let ((start-node (elpher-make-node nil
+ elpher-start-address
+ #'elpher-get-index-node)))
+ (elpher-visit-node start-node)))
"Started Elpher.") ; Otherwise (elpher) evaluates to start page string.
;;; elpher.el ends here