branch: externals/hyperbole
commit 36e6dc66d39c5de82338c9220e643555bad29bc5
Merge: 61937530ea 232ed572d8
Author: Robert Weiner <r...@gnu.org>
Commit: GitHub <nore...@github.com>

    Merge pull request #521 from rswgnu/rsw
    
    Add initial version of "hywiki.el" and the hywiki ibtype
---
 ChangeLog            | 167 ++++++++++++++
 MANIFEST             |   4 +
 Makefile             |  10 +-
 hact.el              |  14 +-
 hasht.el             | 432 ++++++++++++++++++++++++++++++++++++
 hbut.el              |  24 +-
 hibtypes.el          |  35 ++-
 hpath.el             | 162 +++++++-------
 hsys-org.el          |  21 +-
 hui-em-but.el        |  81 +++++--
 hui.el               |   8 +-
 hyrolo.el            |  67 +++---
 hywiki.el            | 601 +++++++++++++++++++++++++++++++++++++++++++++++++++
 test/hyrolo-tests.el |  14 +-
 14 files changed, 1462 insertions(+), 178 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index fd0d468bee..4abf34d01b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,96 @@
+2024-05-25  Bob Weiner  <r...@gnu.org>
+
+* hbut.el (ibtype:delete): Fix interactive call.
+
+* hywiki.el (hywiki-remap-buttonize-characters): Add <tab>.
+            (hywiki-highlight-page-name): Fix to highlight page
+    name regardless of how much whitespace comes after the name.
+            (hywiki-mode): Expand doc and add/remove 'post-self-insert-hook'
+    call of 'hywiki-buttonize'.
+            (hywiki-remap-buttonize-characters, hywiki-initialize-mode-map):
+    Remove these and move keymap init into 'hywiki-mode' definition.
+            (hywiki-buttonize): Rewrite to highlight hywiki word to the left of
+    point iff last inserted char is in the set of 
'hywiki--buttonize-characters'.
+    Rename to 'hywiki-buttonize-character-commands'.
+           (hywiki-buttonize-non-character-commands): Add.
+
+* hbut.el (gbut:act): Set 'loc to current-buffer, not the gbut's
+    source buffer.
+          (hbut:key-src-set-buffer): Clarify doc.
+          (hbut:funcall): Change to use 'hbut:key-src-set-buffer' to
+    temporarily set current-buffer to button's loc attribute.
+
+* hywiki.el (hywiki-get-page-files): Check that 'hywiki-directory'
+    exists and is readable.
+           (hywiki-is-wikiword): Make case-sensitive.
+          (hywiki-at-wikiword): Remove optional 'org-link-flag' arg
+    use 'ibut:label-p' to get HyWikiWord.
+
+2024-05-23  Bob Weiner  <r...@gnu.org>
+
+* hact.el (actype:delete): Fix interactive spec, eliminating use of
+    'hui:htype-delete'.
+
+* hui.el (hui:htype-delete): Fix doc.
+
+2024-05-19  Bob Weiner  <r...@gnu.org>
+
+* hywiki.el (hywiki-remap-buttonize-characters): Move initialization of
+    'hywiki--buttonize-characters' here instead of at variable definition
+    to eliminate circular load dependency.
+           (find-file-hook): Remove add-hook on 'org-mode-hook'.
+          (hywiki-find-page): Add post 'hywiki-find-page-hook' and return
+    absolute path to the found page.
+
+* hibtypes.el (grep-msg): Fix that when no source-loc, file path was not
+    expanded.
+              (find-func): Add require of this library for native compiler
+    grep-msg library name expansion.
+
+* hywiki.el (hywiki-initialize-mode-map): Fix call of old function name.
+            (hywiki-remap-remap-buttonize-characters): Remove extra remap-.
+           (hywiki-find-page): Remove unused let of 'page-buffer'.
+
+2024-05-18  Bob Weiner  <r...@gnu.org>
+
+* test/hyrolo-tests.el (hyrolo-tests--goto-kotl-header-with-slash-match): Start
+    to try to fix this test.
+
+* hpath.el (hpath:find, hpath:to-markup-anchor): Fix for directory pathnames so
+    properly handles anchors, line numbers, column numbers and 'hpath-find' 
returns
+    the buffer found.  Also, make 'hpath:to-markup-anchor' handle any special 
buffer
+    types without attached buffers.
+
+* hywiki.el (hywiki-get-org-insertion-punctuation-keys): Allow for symbol 
syntax
+    chars and rename to 'hywiki-get-buttonize-characters'.
+            (hywiki-remap-org-insertion-punctuation-keys): Rename to
+    'hywiki-remap-buttonize-characters'.
+            (hywiki--buttonize-characters): Add as a computed string of single
+    characters that each trigger prior hywiki word highlighting when it is 
enabled.
+            (hywiki-highlight-page-name, hywiki-highlight-page-names): Don't 
skip
+     back over symbol chars.
+            (hywiki-find-page): Use this function as the 'find-file-hook' 
instead
+     of 'hywiki-highlight-page-names'; make 'page-name' optional for this use 
since
+     the current buffer will be the page, so the page name can be derived.  If 
calling
+     this function creates the page buffer, then enable hywiki-mode and call
+     'hywiki-highlight-page-names'.
+
+* hui-em-but.el (hproperty:but-clear-all-in-list, 
hproperty:but-get-all-in-region):
+    Add.
+
+* hywiki.el (org-mode-hook): Don't make 'find-file-hook' buffer local, as this
+    is not recommended.
+            (hywiki-pages-hasht): Rename to 'hywiki--pages-hasht'.
+           (hywiki-at-wikiword): Allow preceding char to be whitespace or
+    additionally any of these chars: (["'`'
+            ((hywiki-highlight-page-names, hywiki-at-wikiword,
+    hywiki-highlight-page-name): Don't limit to files within 
'hywiki-directory';
+    use whenever 'hywiki-mode' is enabled.
+            (hywiki-at-wikiword): Remove 'hywiki-allowed-modes' since now test
+    that hywiki-mode is enabled.
+            (hywiki-initialize-mode-map): Add and make punct. and 
non-square-bracket
+    and non-angle-bracket balanced expressions highlight HyWiki page name 
references.
+
 2024-05-18  Mats Lidell  <ma...@gnu.org>
 
 * hypb.el (hypb:configuration): Add info about Emacs native comp.
@@ -15,6 +108,80 @@
 
 * hact.el (actype:act): Add Emacs 30 closure support.
 
+2024-05-15  Bob Weiner  <r...@gnu.org>
+
+* hywiki.el (org-mode-hook): Remove 'post-self-insert-hook' and instead 
highlight
+    HyWikiWords via new 'hywiki-mode' minor-mode and its SPC and RET key 
bindings
+    bound to hywiki-buttonize.
+
+2024-05-12  Bob Weiner  <r...@gnu.org>
+
+* hact.el (actype:act, action:params): Add Emacs 30 closure support.
+
+2024-05-06  Bob Weiner  <r...@gnu.org>
+
+* hyrolo.el (hyrolo-name-at-p): Fix to test point is in 
'hyrolo-display-buffer'.
+
+2024-05-05  Bob Weiner  <r...@gnu.org>
+
+* hpath.el (hpath:delimited-possible-path): Rewrite to expand both delimited
+    and non-delimited shell pathnames.
+
+    For non-delimited strings, remove any leading or trailing spaces;
+    otherwise, may prepend a dir with an extra space in there when
+    extracting paths from an ls command that are indented to match the
+    column indent of filenames with quotes around them.  Also, remove
+    requirement that shell buffer filenames be tab delimited and require
+    that filenames with spaces in their names have quotes around them.
+           (hpath:at-p): If file is readable, ignore whether it has "::" in it.
+
+* hui-em-but.el: Comment out non-load when non-interactive so does not created
+    errors when used in "hywiki.el".
+
+2024-04-23  Bob Weiner  <r...@gnu.org>
+
+* hywiki.el: Remove autoload from defib and 'hywiki-open', fixing autoload 
error.
+             (hywiki-company-hasht-backend): Complete words, not symbols
+    (no punctuation).
+             (hywiki-open): Rename to 'hywiki-find-page' and automatically 
create
+    any non-existent page unless optional 'prompt-flag' is t, then prompt
+    whether to create.  If 'prompt-flag' is 'exists, return an existing page 
only,
+    else nil.
+             (hywiki-complete): Rename to 'hywiki-org-link-complete'.  Insert
+    `hywiki-org-link-type' only when 'hywiki-org-link-type-required' is 
non-nil.
+             (hywiki-store-link): Rename to 'hywiki-org-link-store'.  Insert
+    `hywiki-org-link-type' only when 'hywiki-org-link-type-required' is 
non-nil.
+            (hywiki-org-link-type): Change value from 'hy:' to 'hy'.
+            (hywiki-page-list): Rename to 'hywiki-get-page-list'.
+            (hywiki-get-pages): Rename to 'hywiki-get-page-hasht'.
+             (hywiki-word-section-regexp): Add.
+             (hywiki-get-page-files): Add missing '+' for multiple chars in 
file suffix.
+             (hywiki-at-wikiword, hywiki-highlight-page-names): Allow for 
numerics
+    in #section references.
+             (hywiki--word-face): Change foreground of HyWiki word to 'orange'
+    when on a dark background.
+
+2024-04-22  Bob Weiner  <r...@gnu.org>
+
+* hywiki.el: Add new auto-wikiword note-taking system with `hywiki' ibtype.
+  hasht.el: Temporarily add this for hywiki hash table support.
+  hibtypes.el (load "hywiki"): Add at lowest ibtype priority.
+  hsys-org.el (hsys-org-link-at-p): Support HyWiki word handling.
+  Makefile (EL_COMPILE):
+  MANIFEST: Add hywiki.el and hasht.el.
+
+* hui-em-but.el (hproperty:but-start, hproperty:but-end, hproperty:but-delete):
+    Add these functions and use in hywiki.el.
+
+2024-04-21  Bob Weiner  <r...@gnu.org>
+
+* hui-em-but.el (hproperty:but-clear): Add optional args 'property' and 
'value'.
+    When 'property' is given, clear the button only when property's value
+    matches 'value'.
+                (hproperty:but-p, hproperty:but-get): Add optional args
+    'property' and 'value', matching only when 'property' equals 'value', if
+    given.
+
 2024-04-20  Bob Weiner  <r...@gnu.org>
 
 * hui-select.el (hui-c++-defun-prompt-regexp): Add to eliminate an Emacs
diff --git a/MANIFEST b/MANIFEST
index 3fc33c06fa..f40aa462e9 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -73,6 +73,10 @@ hsmail.el            - GNU Hyperbole buttons in mail 
composer: mail
 kotl/MANIFEST        - Summary of Koutliner files
 kotl/EXAMPLE.kotl    - Sample Koutline document explaining Koutliner features
 
+* --- HYPERBOLE NOTES ---
+hywiki.el            - Hyperbole's auto-wikiword note-taking system
+hasht.el             - Create hash tables from lists and operate on them.
+
 * --- HYPERBOLE ROLO ---
 hyrolo.el            - Hierarchical, multi-file, easy-to-use record management 
system
 hyrolo.py            - Output file header and matching entries from HyRolo 
files via the command-line
diff --git a/Makefile b/Makefile
index 5c69528ac8..b72306f74e 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@
 # Author:       Bob Weiner
 #
 # Orig-Date:    15-Jun-94 at 03:42:38
-# Last-Mod:      6-May-24 at 00:25:45 by Mats Lidell
+# Last-Mod:     19-May-24 at 10:58:02 by Bob Weiner
 #
 # Copyright (C) 1994-2023  Free Software Foundation, Inc.
 # See the file HY-COPY for license information.
@@ -190,8 +190,9 @@ EL_COMPILE = hact.el hactypes.el hargs.el hbdata.el 
hbmap.el hbut.el \
              hsys-org-roam.el hsys-www.el hsys-xref.el hsys-youtube.el htz.el \
             hycontrol.el hui-jmenu.el hui-menu.el hui-mini.el hui-mouse.el 
hui-select.el \
             hui-treemacs.el hui-window.el hui.el hvar.el hversion.el hypb.el 
hyperbole.el \
-            hyrolo-demo.el hyrolo-logic.el hyrolo-menu.el hyrolo.el 
hywconfig.el set.el hypb-ert.el \
-            hui-dired-sidebar.el hypb-maintenance.el hui-em-but.el 
hui-register.el
+            hyrolo-demo.el hyrolo-logic.el hyrolo-menu.el hyrolo.el 
hywconfig.el hywiki.el \
+             hasht.el set.el hypb-ert.el hui-dired-sidebar.el 
hypb-maintenance.el hui-em-but.el \
+             hui-register.el
 
 EL_SRC = $(EL_COMPILE)
 
@@ -546,7 +547,8 @@ lint:
 # Run a build using a dockerized version of Emacs
 #
 # Usage:
-#   make dockerized version=28.1 targets='clean bin test'
+#   make dockerized version=master targets='clean bin test'
+#   make dockerized version=28.2 targets='clean bin test'
 
 # Specify version and targets to run
 ifeq ($(origin targets), command line)
diff --git a/hact.el b/hact.el
index 46fabbeb72..3e66e7d477 100644
--- a/hact.el
+++ b/hact.el
@@ -3,7 +3,7 @@
 ;; Author:       Bob Weiner
 ;;
 ;; Orig-Date:    18-Sep-91 at 02:57:09
-;; Last-Mod:     16-May-24 at 23:00:41 by Mats Lidell
+;; Last-Mod:     23-May-24 at 23:18:43 by Bob Weiner
 ;;
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;;
@@ -366,7 +366,9 @@ Autoloads action function if need be to get the parameter 
list."
             (compiled-function-arglist action)
           (action:params-emacs action)))
        ((symbolp action)
-        (car (cdr (and (fboundp action) (hypb:indirect-function action)))))))
+        (car (cdr (and (fboundp action) (hypb:indirect-function action)))))
+       ((and (fboundp #'closurep) (closurep action))
+        (aref action 0))))
 
 (defun action:param-list (action)
   "Return list of actual ACTION parameters (remove `&' special forms)."
@@ -518,8 +520,12 @@ Return symbol created when successful, else nil."
 
 (defun    actype:delete (type)
   "Delete an action TYPE (a symbol).  Return TYPE's symbol if it existed."
-  (interactive (list (hui:htype-delete 'actypes))
-  (htype:delete type 'actypes)))
+  (interactive
+   (list (intern (hargs:read-match
+                 "Delete from actypes: "
+                 (mapcar 'list (htype:names 'actypes))
+                 nil t nil 'actypes))))
+  (htype:delete type 'actypes))
 
 (defun    actype:doc (but &optional full)
   "Return first line of action doc for BUT.
diff --git a/hasht.el b/hasht.el
new file mode 100644
index 0000000000..17d0c5e51f
--- /dev/null
+++ b/hasht.el
@@ -0,0 +1,432 @@
+;;!emacs
+;;
+;; FILE:         hasht.el
+;; SUMMARY:      Create hash tables from lists and operate on them.
+;; USAGE:        GNU Emacs Lisp Library
+;; KEYWORDS:     extensions, tools
+;;
+;; AUTHOR:       Bob Weiner
+;;
+;; ORIG-DATE:    16-Mar-90 at 03:38:48
+;; LAST-MOD:     30-Jul-16 at 08:50:38 by Bob Weiner
+;;
+;; Copyright (C) 1990-1995, 1997, 2016  Free Software Foundation, Inc.
+;; See the file BR-COPY for license information.
+;;
+;; This file is part of the OO-Browser.
+
+;; The OO-Browser is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; The OO-Browser is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with the OO-Browser.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;;   Featureful set of hash table operators for use in personal programs.
+;;
+;;   `hash-make' creates a hash table from an association list, `hash-add'
+;;   adds a value-key pair to a hash table, and `hash-lookup' finds the value
+;;   associated with a given key in a hash table, if any.
+;;
+;;   `hash-map' does the same thing as `mapcar' but operates on hash tables
+;;   instead.
+;;
+;;   For a list of 300 items, these hash tables improve lookup times by a
+;;   factor of between 8 and 10 to 1 over those for an unsorted list.
+;;
+;;   Public and private function names are alphabetized for easy location.
+
+;;; Code:
+;;; ************************************************************************
+;;; Public variables
+;;; ************************************************************************
+
+(defvar hash-merge-values-function 'hash-merge-values
+  "*Function to call in hash-merge to merge the values from 2 hash tables that 
contain the same key.
+It is sent the two values as arguments.")
+
+;;; ************************************************************************
+;;; Public functions
+;;; ************************************************************************
+
+(defun hash-add (value key hash-table)
+  "Add VALUE, any lisp object, referenced by KEY, a string, to HASH-TABLE.
+Replaces any VALUE previously referenced by KEY."
+  (if (hashp hash-table)
+      (let* ((obarray (hash-obarray hash-table))
+            (sym (intern key obarray)))
+       (if sym (set sym value)))))
+
+(defun hash-copy (hash-table)
+  "Return a copy of HASH-TABLE, list and vector elements are shared across 
both tables."
+  (if (not (hashp hash-table))
+      (error "(hash-copy): Invalid hash-table: `%s'" hash-table))
+  (let ((htable-copy (hash-make (length (hash-obarray hash-table)))))
+    (hash-map
+     (lambda (elt) (hash-add (car elt) (cdr elt) htable-copy))
+     hash-table)
+    htable-copy))
+
+(defun hash-count (hash-table)
+  "Return number of elements stored in HASH-TABLE or nil if not a valid hash 
table."
+  (if (hashp hash-table)
+      (let ((obarray (hash-obarray hash-table))
+           (count 0))
+       (mapatoms (lambda (sym)
+                   (and (boundp sym) sym (setq count (1+ count))))
+                 obarray)
+       count)))
+
+(defun hash-delete (key hash-table)
+  "Delete element referenced by KEY, a string, from HASH-TABLE.
+Return nil if KEY is not in HASH-TABLE or non-nil otherwise."
+  (if (hashp hash-table)
+      (let* ((obarray (hash-obarray hash-table))
+            (sym (intern-soft key obarray)))
+       (if sym
+           (progn (makunbound sym)
+                  (unintern sym nil))))))
+
+(defun hash-deep-copy (obj)
+  "Return a copy of OBJ with new copies of all elements, except symbols."
+  (cond ((null obj) nil)
+       ((stringp obj)
+        (copy-sequence obj))
+       ((hashp obj)
+        (let ((htable-copy (hash-make (length (hash-obarray obj)))))
+          (mapc
+           (lambda (elt) (hash-add (car elt) (cdr elt) htable-copy))
+           (hash-map 'hash-deep-copy obj))
+          htable-copy))
+       ((vectorp obj)
+        ;; convert to list for mapping
+        (setq obj (append obj nil))
+        ;; Return as a vector
+        (vconcat (mapcar 'hash-deep-copy obj)))
+       ((atom obj) obj)
+       ((nlistp obj)
+        (error "(hash-deep-copy): Invalid type, `%s'" obj))
+       (t ;; list
+        (cons (hash-deep-copy (car obj)) (hash-deep-copy (cdr obj))))))
+
+(defun hash-empty-p (hash-table)
+  "Return t if HASH-TABLE is empty, else nil."
+  (and (hashp hash-table) (equal hash-table hash-empty-htable)))
+
+(defalias  'hash-get  'hash-lookup)
+
+(defun hash-key-p (key hash-table)
+  "Return non-nil iff KEY is in HASH-TABLE.  KEY's hash table symbol is 
returned."
+  (if (hashp hash-table)
+      (let* ((obarray (hash-obarray hash-table))
+            (sym (intern-soft key obarray)))
+        (if (boundp sym) sym))))
+
+(defun hash-lookup (key hash-table)
+  "Lookup KEY in HASH-TABLE and return associated value.
+If value is nil, this function does not tell you whether or not KEY is in the
+hash table.  Use `hash-key-p' instead for that function."
+  (if (hashp hash-table)
+      (let* ((obarray (hash-obarray hash-table))
+            (sym (intern-soft key obarray)))
+        (if (boundp sym) (symbol-value sym)))))
+
+(defun hash-make (initializer &optional reverse)
+  "Create a hash table from INITIALIZER.
+INITIALIZER may be an alist with elements of the form (<value> . <key>) from
+which the hash table is built.  Alternatively, it may be a non-negative
+integer which is used as the minimum size of a new, empty hash table.
+Optional non-nil second argument REVERSE means INITIALIZER has elements of
+form (<key> . <value>)."
+  (cond ((integerp initializer)
+        (if (>= initializer 0)
+            (cons 'hasht (make-vector (hash-next-prime initializer) 0))
+          (error "(hash-make): Initializer must be >= 0, not `%s'"
+                 initializer)))
+       ((numberp initializer) 
+        (error "(hash-make): Initializer must be a positive integer, not `%f'"
+               initializer))
+       (t (let* ((vlen (hash-next-prime (length initializer)))
+                 (obarray (make-vector vlen 0))
+                 key value sym)
+            (mapc
+             (lambda (cns)
+               (if (consp cns)
+                   (if reverse
+                       (setq key (car cns) value (cdr cns))
+                     (setq key (cdr cns) value (car cns))))
+               (if (setq sym (intern key))
+                   (set sym value)))
+             initializer)
+            (cons 'hasht obarray)))))
+
+(defun hash-make-prepend (initializer &optional reverse)
+  "Create a hash table from INITIALIZER.
+INITIALIZER may be an alist with elements of the form (<value> . <key>) from
+which the hash table is built.  Optional non-nil second argument REVERSE
+means INITIALIZER has elements of form (<key> . <value>).
+
+The resultant value associated with a <key> is a list of all of the <values>
+given in INITIALIZER entries which contain the <key>.  The values are listed
+in reverse order of occurrence (they are prepended to the list)."
+  (let* ((vlen (hash-next-prime (length initializer)))
+        (obarray (make-vector vlen 0))
+        key value sym)
+    (mapc
+     (lambda (cns)
+       (if (consp cns)
+          (if reverse
+              (setq key (car cns) value (cdr cns))
+            (setq key (cdr cns) value (car cns))))
+       (setq sym (intern key))
+       (if (boundp sym)
+          (set sym (cons value (symbol-value sym)))
+        (set sym (cons value nil))))
+     initializer)
+    (cons 'hasht obarray)))
+
+(defun hash-map (func hash-table)
+  "Return a list of the results of applying FUNC to each (<value> . <key>) 
element of HASH-TABLE."
+  (if (not (hashp hash-table))
+      (error "(hash-map): Invalid hash-table: `%s'" hash-table))
+  (let ((result))
+    (mapatoms (lambda (sym)
+               (and (boundp sym)
+                    sym
+                    (setq result (cons (funcall
+                                        func
+                                        (cons (symbol-value sym)
+                                              (symbol-name sym)))
+                                       result))))
+             (hash-obarray hash-table))
+    result))
+
+(defun hash-merge (&rest hash-tables)
+  "Merge any number of HASH-TABLES.  Return resultant hash table.
+A single argument consisting of a list of hash tables may also be given.
+Return an empty hash table if any argument from the merge list is other
+than nil or a hash table.
+
+Use the value of `hash-merge-values-function' to merge the values of entries
+whose keys are the same."
+  (let ((empty-ht (hash-make 1)))
+    (and (not (hashp (car hash-tables)))
+        (listp (car hash-tables))
+        ;; Handle situation where a list of hash-tables is passed in as a
+        ;; single argument, rather than as multiple arguments.
+        (setq hash-tables (car hash-tables)))
+    (if (memq nil (mapcar (lambda (ht) (or (null ht) (hashp ht)))
+                         hash-tables))
+       empty-ht
+      (setq hash-tables
+           (delq nil (mapcar (lambda (ht)
+                               (if (hash-empty-p ht) nil ht))
+                             hash-tables)))
+      (let ((len (length hash-tables)))
+       (cond ((= len 0) empty-ht)
+             ((= len 1) (car hash-tables))
+             ;; Make the merged hash-table be 20% larger than the number of
+             ;; entries filled in all hash-tables to be merged, so that
+             ;; hash misses are minimized.
+             (t (let ((htable (hash-make
+                               (ceiling
+                                (* 1.2 (apply '+ (mapcar 'hash-count
+                                                         hash-tables))))))
+                      key value)
+                  (mapc
+                    (lambda (ht)
+                      (hash-map (lambda (val-key-cons)
+                                  (setq value (car val-key-cons)
+                                        key (cdr val-key-cons))
+                                  (if (not (hash-key-p key htable))
+                                      (hash-add value key htable)
+                                    ;; Merge values
+                                    (hash-add
+                                     (funcall hash-merge-values-function
+                                              (hash-get key htable)
+                                              value)
+                                     key htable)))
+                                ht))
+                    hash-tables)
+                  htable)))))))
+
+(defun hash-merge-first-value (value1 value2)
+  "Return a copy of VALUE1 for use in a hash table merge.
+
+This is suitable for use as a value of `hash-merge-values-function'."
+  ;; Copy list so that merged result does not share structure with the
+  ;; hash tables being merged.
+  (if (listp value1) (copy-sequence value1) value1))
+
+(defun hash-merge-values (value1 value2)
+  "Return a list from merging VALUE1 and VALUE2 or creating a new list.
+Nil values are thrown away.  If both arguments are lists, their elements are
+assumed to be strings and the result is a set of ordered strings.
+
+This is suitable for use as a value of `hash-merge-values-function'."
+  ;; Copy lists so that merged result does not share structure with the
+  ;; hash tables being merged.
+  (if (listp value1) (setq value1 (copy-sequence value1)))
+  (if (listp value2) (setq value2 (copy-sequence value2)))
+  (cond ((and (listp value1) (listp value2))
+        ;; Assume desired result is a set of strings.
+        (hash-set-of-strings (sort (append value1 value2) 'string-lessp)))
+       ((null value1)
+        value2)
+       ((null value2)
+        value1)
+       ((listp value1)
+        (cons value2 value1))
+       ((listp value2)
+        (cons value1 value2))
+       (t (list value1 value2))))
+
+(make-obsolete 'hash-new 'hash-make "19.0")
+(defun hash-new (size)
+  "Return a new hash table of SIZE elements.
+This is obsolete.  Use `hash-make' instead."
+  (hash-make size))
+
+(defun hash-prepend (value key hash-table)
+  "Prepend VALUE onto the list value referenced by KEY, a string, in 
HASH-TABLE.
+If KEY is not found in HASH-TABLE, it is added with a value of (list VALUE)."
+  (if (hashp hash-table)
+      (let* ((obarray (hash-obarray hash-table))
+            (sym (intern key obarray)))
+       (if (boundp sym)
+           (if (listp (symbol-value sym))
+               (set sym (cons value (symbol-value sym)))
+             (error "(hash-prepend): `%s' key's value is not a list."
+                    key))
+         (set sym (cons value nil))))))
+
+(defun hash-prin1 (hash-table &optional stream)
+  "Output the printed representation of HASH-TABLE as a list.
+Quoting characters are printed when needed to make output that `read'
+can handle, whenever this is possible.
+Output stream is STREAM, or value of `standard-output'."
+  (if (not (hashp hash-table))
+      (progn (prin1 hash-table stream)
+            (princ "\n" stream))
+    (princ "\(\n" stream)
+    (hash-map
+     (lambda (val-key-cons)
+       (prin1 val-key-cons stream)
+       (princ "\n" stream))
+     hash-table)
+    (princ "\)\n" stream)))
+
+(defun hash-replace (value key hash-table)
+  "Replace VALUE referenced by KEY, a string, in HASH-TABLE.
+An error will occur if KEY is not found in HASH-TABLE."
+  (if (hashp hash-table)
+      (let* ((obarray (hash-obarray hash-table))
+            (sym (intern-soft key obarray)))
+       (if (and (boundp sym) sym)
+           (set sym value)
+         (error "(hash-replace): `%s' key not found in hash table." key)))))
+
+(defun hash-resize (hash-table new-size)
+  "Resize HASH-TABLE to NEW-SIZE without losing any elements and return new 
table.
+NEW-SIZE must be greater than 0.  Hashing works best if NEW-SIZE is a prime
+number.  See also `hash-next-prime'."
+  (if (< new-size 1)
+      (error "(hash-resize): Cannot resize hash table to size %d" new-size))
+  (let ((htable (hash-make new-size)))
+    (hash-map (lambda (elt)
+               (hash-add (car elt) (cdr elt) htable))
+             hash-table)
+    htable))
+
+(defun hash-resize-p (hash-table)
+  "Resizes HASH-TABLE to 1.5 times its size if above 80% full.
+Returns new hash table when resized, else nil."
+  (if (hashp hash-table)
+      (let ((count (hash-count hash-table))
+           (size (length (hash-obarray hash-table))))
+       (if (> (* count (/ count 5)) size)
+           (hash-resize hash-table (hash-next-prime (+ size (/ size 2))))))))
+
+(defun hash-size (hash-table)
+  "Return size of HASH-TABLE which is >= number of elements in the table.
+Return nil if not a valid hash table."
+  (if (hashp hash-table)
+      (length (hash-obarray hash-table))))
+(defalias 'hash-length 'hash-size)
+
+(defun hashp (object)
+  "Return non-nil if OBJECT is a hash-table."
+  (and (listp object) (eq (car object) 'hasht)
+       (vectorp (cdr object))))
+
+;;; ************************************************************************
+;;; Private functions
+;;; ************************************************************************
+
+(defun hash-next-prime (n)
+  "Return next prime number >= N."
+  (if (<= n 2)
+      2
+    (and (= (% n 2) 0) (setq n (1+ n)))
+    (while (not (hash-prime-p n))
+      (setq n (+ n 2)))
+    n))
+
+(defun hash-obarray (hash-table)
+  "Return symbol table (object array) portion of HASH-TABLE."
+  (cdr hash-table))
+
+(defun hash-prime-p (n)
+  "Return non-nil iff N is prime."
+  (if (< n 0) (setq n (- n)))
+  (let ((small-primes '(2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 
71 73 79 83 89)))
+    (cond ((< n 2) nil)
+         ((< n 91) (if (memq n small-primes) t))
+         ((< n 7921)  ;; 89, max small-prime, squared
+          (let ((prime t)
+                (pr-list small-primes))
+            (while (and (setq pr-list (cdr pr-list))
+                        (setq prime (/= (% n (car pr-list)) 0))))
+            prime))
+         ((or (= (% n 3) 0) (= (% n 2) 0)) nil)
+         ((let ((factor1 5)
+                (factor2 7)
+                (is-prime))
+            (while (and (<= (* factor1 factor1) n)
+                        (setq is-prime (and (/= (% n factor1) 0)
+                                            (/= (% n factor2) 0))))
+              (setq factor1 (+ factor1 6)
+                    factor2 (+ factor2 6)))
+            is-prime)))))
+
+(defun hash-set-of-strings (sorted-strings &optional count)
+  "Return SORTED-STRINGS list with any duplicate entries removed.
+Optional COUNT conses number of duplicates on to front of list before return."
+  (and count (setq count 0))
+  (let ((elt1) (elt2) (lst sorted-strings)
+       (test (if count
+                 (lambda (a b) (if (string-equal a b)
+                                   (setq count (1+ count))))
+               (lambda (a b) (string-equal a b)))))
+    (while (setq elt1 (car lst) elt2 (car (cdr lst)))
+      (if (funcall test elt1 elt2)
+         (setcdr lst (cdr (cdr lst)))
+       (setq lst (cdr lst)))))
+  (if count (cons count sorted-strings) sorted-strings))
+
+;;; ************************************************************************
+;;; Private variables
+;;; ************************************************************************
+
+(defvar hash-empty-htable (hash-make 1)
+  "Empty hash table used to test whether other hash tables are empty.")
+
+(provide 'hasht)
diff --git a/hbut.el b/hbut.el
index bb13b033a5..5acf3bc783 100644
--- a/hbut.el
+++ b/hbut.el
@@ -3,7 +3,7 @@
 ;; Author:       Bob Weiner
 ;;
 ;; Orig-Date:    18-Sep-91 at 02:57:09
-;; Last-Mod:     14-Apr-24 at 13:52:20 by Bob Weiner
+;; Last-Mod:     25-May-24 at 16:30:50 by Bob Weiner
 ;;
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;;
@@ -762,7 +762,11 @@ Insert INSTANCE-FLAG after END, before ending delimiter."
        (t (let* ((lbl-key (hbut:label-to-key label))
                  (but (gbut:get lbl-key)))
             (if but
-                (hbut:act but)
+                (progn
+                  ;; Ensure gbut is activated with current-buffer as
+                  ;; the context, not the gbut's source buffer.
+                  (hattr:set but 'loc (current-buffer))
+                  (hbut:act but))
               (error "(gbut:act): No global button found for label: %s" 
label))))))
 
 (defun    gbut:delete (&optional lbl-key)
@@ -1234,7 +1238,7 @@ hbut:current's 'loc attribute to KEY-SRC."
        (hattr:set 'hbut:current 'loc key-src)
       (let ((loc (hattr:get 'hbut:current 'loc)))
        (when loc
-         (set-buffer (or (get-buffer loc) (find-file-noselect loc)))))
+         (hbut:key-src-set-buffer loc)))
       (setq key-src (hbut:to-key-src 'full)
            ;; `hbut:to-key-src' sets current buffer to key-src buffer.
            buffer (or buffer (current-buffer))))
@@ -1352,9 +1356,9 @@ represent the output of particular document formatters."
            ((current-buffer))))))
 
 (defun    hbut:key-src-set-buffer (src)
-  "Set buffer to SRC, a buffer, buffer name, file, directory or symlink.
-If SRC is a directory, simply return it; otherwise, return SRC or
-nil if invalid."
+  "Temporarily set current buffer to SRC, a buffer, buffer name, or file.
+If SRC is a directory, simply return it; otherwise, return set current
+buffer to SRC and return it or return nil if SRC is invalid/unreadable."
   (cond ((null src) nil)
        ((or (bufferp src) (get-buffer src))
         (set-buffer src)
@@ -1368,7 +1372,8 @@ nil if invalid."
         (set-buffer (find-file-noselect src))
         src)
        ;; Buffer may be newly created with an attached file that has
-       ;; not yet been saved, so it can't be read.
+       ;; not yet been saved, so the file does not exist and cannot
+       ;; be read.
        ((get-file-buffer src)
         (set-buffer (get-file-buffer src))
         src)))
@@ -3170,7 +3175,10 @@ is returned."
 (defun    ibtype:delete (type)
   "Delete an implicit button TYPE (a symbol).
 Return TYPE's symbol if it existed, else nil."
-  (interactive (list (hui:htype-delete 'ibtypes)))
+  (interactive (list (intern (hargs:read-match
+                             (concat "Delete from " (symbol-name 'ibtypes) ": 
")
+                             (mapcar 'list (htype:names 'ibtypes))
+                             nil t nil 'ibtypes))))
   (htype:delete type 'ibtypes))
 
 ;; Return the full Elisp symbol for IBTYPE, which may be a string or symbol.
diff --git a/hibtypes.el b/hibtypes.el
index 8246fbd355..ed2156f614 100644
--- a/hibtypes.el
+++ b/hibtypes.el
@@ -3,7 +3,7 @@
 ;; Author:       Bob Weiner
 ;;
 ;; Orig-Date:    19-Sep-91 at 20:45:31
-;; Last-Mod:      7-Apr-24 at 15:12:46 by Bob Weiner
+;; Last-Mod:     25-May-24 at 10:11:05 by Bob Weiner
 ;;
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;;
@@ -11,6 +11,7 @@
 ;; See the "HY-COPY" file for license information.
 ;;
 ;; This file is part of GNU Hyperbole.
+
 ;;; Commentary:
 ;;
 ;;   Implicit button types (ibtypes) in this file are defined in increasing
@@ -37,6 +38,7 @@
 ;;; ************************************************************************
 
 (require 'cl-lib) ;; for cl-count
+(require 'find-func) ;; used by grep-msg ibtype
 (eval-when-compile (require 'hversion))
 (require 'hactypes)
 (require 'hypb)
@@ -88,6 +90,12 @@
 ;; them.  Use load instead to ensure are reloaded when resetting
 ;; ibtype priorities.
 
+;;; ========================================================================
+;;; Creates and display personal wiki pages with auto-wikiword links
+;;; ========================================================================
+
+(load "hywiki")
+
 ;;; ========================================================================
 ;;; Jumps to source line from Python traceback lines
 ;;; ========================================================================
@@ -358,16 +366,17 @@ attached file."
        (let ((chr (aref (buffer-name) 0)))
          (not (or (eq chr ?\ ) (eq chr ?*))))
        (not (apply #'derived-mode-p '(prog-mode c-mode objc-mode c++-mode 
java-mode markdown-mode org-mode)))
-       (let ((ref (hattr:get 'hbut:current 'lbl-key))
-            (lbl-start (hattr:get 'hbut:current 'lbl-start)))
-         (and ref
-             lbl-start
-             (eq ?w (char-syntax (aref ref 0)))
-              (not (string-match "[#@]" ref))
-             (save-excursion
-               (goto-char lbl-start)
-               (ibut:label-p t "[" "]" t))
-              (hact 'annot-bib ref)))))
+       (unless (ibut:label-p t "[[" "]]" t) ;; Org link
+        (let ((ref (hattr:get 'hbut:current 'lbl-key))
+              (lbl-start (hattr:get 'hbut:current 'lbl-start)))
+           (and ref
+               lbl-start
+               (eq ?w (char-syntax (aref ref 0)))
+               (not (string-match "[#@]" ref))
+               (save-excursion
+                 (goto-char lbl-start)
+                 (ibut:label-p t "[" "]" t))
+               (hact 'annot-bib ref))))))
 
 ;;; ========================================================================
 ;;; Follows Org links that are in non-Org mode buffers
@@ -1041,7 +1050,9 @@ in grep and shell buffers."
                              (hbut:to-key-src t))))
           (if (stringp source-loc)
               (setq file (expand-file-name file (file-name-directory 
source-loc)))
-           (setq file (or (hpath:prepend-shell-directory file) file)))
+           (setq file (or (hpath:prepend-shell-directory file)
+                          (ignore-errors (find-library-name file))
+                          (expand-file-name file))))
          (when (file-exists-p file)
             (setq line-num (string-to-number line-num))
             (ibut:label-set but-label)
diff --git a/hpath.el b/hpath.el
index 541e547750..d4abba08ae 100644
--- a/hpath.el
+++ b/hpath.el
@@ -3,7 +3,7 @@
 ;; Author:       Bob Weiner
 ;;
 ;; Orig-Date:     1-Nov-91 at 00:44:23
-;; Last-Mod:     31-Mar-24 at 00:23:02 by Bob Weiner
+;; Last-Mod:     18-May-24 at 19:06:22 by Bob Weiner
 ;;
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;;
@@ -955,43 +955,43 @@ Absolute pathnames must begin with a `/' or `~'."
     (when (and path (not non-exist) (string-match hpath:prefix-regexp path)
               (not (string-equal (match-string 0 path) path)))
       (setq non-exist t))
-    (unless (and path (or (string-empty-p path)
-                         (string-match "::" path)))
-      (cond ((and path (file-readable-p path))
-            path)
-           ((and path
-                 ;; Don't allow more than one set of grouping chars
-                 (not (string-match-p "\)\\s-*\(\\|\\]\\s-*\\[\\|\}\\s-*\{" 
path))
-                 ;; With point inside a path variable, return the path that 
point is on or to the right of.
-                 (setq subpath (or (and (setq subpath (hargs:delimited 
"[:\"\']\\|^\\s-*" "[:\"\']\\|\\s-*$" t t nil "[\t\n\r\f]\\|[;:] \\| [;:]"))
-                                        (not (string-match-p "[:;\t\n\r\f]" 
subpath))
-                                        subpath)
-                                   (and (setq subpath (hargs:delimited 
"[;\"\']\\|^\\s-*" "[;\"\']\\|\\s-*$"  t t nil "[\t\n\r\f]\\|[;:] \\| [;:]"))
-                                        (not (string-match-p 
"[;\t\n\r\f]\\|:[^:]*:" subpath))
-                                        subpath)))
-                 ;; Handle anchored or action prefix char paths in the
-                 ;; following clause; otherwise, might just be looking
-                 ;; at part of the path
-                 (and subpath (not (or (string-match-p "#" subpath)
-                                       (string-match-p hpath:prefix-regexp 
subpath))))
-                 (setq subpath
-                       (if subpath
-                           (cond ((and (string-match "\\`\\s-*\\([^; \t]+\\)" 
subpath)
-                                       (executable-find (match-string 1 
subpath)))
-                                  ;; Could be a shell command from a semicolon 
separated
-                                  ;; list; ignore if so
-                                  nil)
-                                 (t (expand-file-name subpath)))
-                         ;; Only default to current path if know are within a 
PATH value
-                         (when (string-match-p 
hpath:path-variable-value-regexp path)
-                           ".")))
-                 (hpath:is-p subpath type non-exist))
-            subpath)
-           ((hpath:is-p path type non-exist))
-           ;; Local file URLs
-           ;; ((hpath:is-p (hargs:delimited "file://" "[ \t\n\r\"\'\}]" nil 
t)))
-           ((hpath:remote-at-p))
-           ((hpath:www-at-p) nil)))))
+    (if (and path (not (string-empty-p path)) (file-readable-p path))
+       path
+      (unless (and path (or (string-empty-p path)
+                           (string-match "::" path)))
+       (cond ((and path
+                   ;; Don't allow more than one set of grouping chars
+                   (not (string-match-p "\)\\s-*\(\\|\\]\\s-*\\[\\|\}\\s-*\{" 
path))
+                   ;; With point inside a path variable, return the path that 
point is on or to the right of.
+                   (setq subpath (or (and (setq subpath (hargs:delimited 
"[:\"\']\\|^\\s-*" "[:\"\']\\|\\s-*$" t t nil "[\t\n\r\f]\\|[;:] \\| [;:]"))
+                                          (not (string-match-p "[:;\t\n\r\f]" 
subpath))
+                                          subpath)
+                                     (and (setq subpath (hargs:delimited 
"[;\"\']\\|^\\s-*" "[;\"\']\\|\\s-*$"  t t nil "[\t\n\r\f]\\|[;:] \\| [;:]"))
+                                          (not (string-match-p 
"[;\t\n\r\f]\\|:[^:]*:" subpath))
+                                          subpath)))
+                   ;; Handle anchored or action prefix char paths in the
+                   ;; following clause; otherwise, might just be looking
+                   ;; at part of the path
+                   (and subpath (not (or (string-match-p "#" subpath)
+                                         (string-match-p hpath:prefix-regexp 
subpath))))
+                   (setq subpath
+                         (if subpath
+                             (cond ((and (string-match "\\`\\s-*\\([^; 
\t]+\\)" subpath)
+                                         (executable-find (match-string 1 
subpath)))
+                                    ;; Could be a shell command from a 
semicolon separated
+                                    ;; list; ignore if so
+                                    nil)
+                                   (t (expand-file-name subpath)))
+                           ;; Only default to current path if know are within 
a PATH value
+                           (when (string-match-p 
hpath:path-variable-value-regexp path)
+                             ".")))
+                   (hpath:is-p subpath type non-exist))
+              subpath)
+             ((hpath:is-p path type non-exist))
+             ;; Local file URLs
+             ;; ((hpath:is-p (hargs:delimited "file://" "[ \t\n\r\"\'\}]" nil 
t)))
+             ((hpath:remote-at-p))
+             ((hpath:www-at-p) nil))))))
 
 (defun hpath:call (func path &optional non-exist)
   "Call FUNC with a PATH and optional NON-EXIST flag.
@@ -1125,44 +1125,47 @@ With optional INCLUDE-POSITIONS, return a triplet list 
of (path start-pos
 end-pos) or nil."
   (unless (eolp)
     ;; Prevents MSWindows to Posix path substitution
-    (let ((hyperb:microsoft-os-p t))
-      (or (hargs:delimited "file://" "\\s-" nil t include-positions)
-         ;; Filenames in HTML
-         (hargs:delimited "&quot;" "&quot;" nil nil include-positions "[`'’]")
-         ;; Embedded double quoted filenames
-         (hargs:delimited "\\\"" "\\\"" nil nil include-positions "[`'’]")
-         ;; Double quoted filenames
-         (hargs:delimited "\"" "\"" nil nil include-positions "[`'’]")
-         ;; Filenames in Info docs or Python files
-         (hargs:delimited "[`'‘]" "[`'’]" t t include-positions "\"")
-         ;; Filenames in TexInfo docs
-         (hargs:delimited "@file{" "}" nil nil include-positions)
-         ;; if `non-exist' is nil, look for any existing whitespace
-         ;; delimited filename at point.  If match consists of punctuation
-         ;; only, like . or .., don't treat it as a pathname.
-         ;; In shell modes, it must be tab delimited.
-         (unless non-exist
-           (let* ((space-delimiter (if (derived-mode-p #'shell-mode)
-                                       "\t"
-                                     "[ \t]"))
-                  (triplet (hargs:delimited (format 
"^\\|\\(%s\\|[\]\[()<>\;&,@]\\)+"
-                                                    space-delimiter)
-                                            
"\\([\]\[()<>\;&,@]\\|:*\\s-\\)+\\|$"
-                                            t t t))
-                  (p (car triplet))
-                  (punc (char-syntax ?.)))
-             ;; May have matched to a string with an embedded double
-             ;; quote or surrounded by braces; if so, don't consider it a path.
-              ;; Also ignore whitespace delimited root dirs, e.g. " / ".
-             (when (and (stringp p) (not (string-match-p 
"\\`{.*}\\'\\|\"\\|\\`[/\\]+\\'" p))
-                        (delq nil (mapcar (lambda (c) (/= punc (char-syntax 
c))) p)))
-               ;; Prepend proper directory from cd, ls *, recursive ls or dir 
file
-               ;; listings when needed.
-               (setq p (or (hpath:prepend-shell-directory p) p))
-               (setcar triplet p)
-               (if include-positions
-                   triplet
-                 p))))))))
+    (let* ((hyperb:microsoft-os-p t)
+          (triplet (or (hargs:delimited "file://" "\\s-" nil t 
include-positions)
+                       ;; Filenames in HTML
+                       (hargs:delimited "&quot;" "&quot;" nil nil 
include-positions "[`'’]")
+                       ;; Embedded double quoted filenames
+                       (hargs:delimited "\\\"" "\\\"" nil nil 
include-positions "[`'’]")
+                       ;; Filenames in TexInfo docs
+                       (hargs:delimited "@file{" "}" nil nil include-positions)
+                       ;; Double quoted filenames
+                       (hargs:delimited "\"" "\"" nil nil include-positions 
"[`'’]")
+                       ;; Filenames in Info docs, Python files or 'ls' listing 
files in
+                       ;; single quotes
+                       (hargs:delimited "[`'‘]" "[`'’]" t t include-positions 
"\"")))
+          (p (if (listp triplet) (car triplet) triplet)))
+      (if non-exist
+         ;; This may be a triplet of (path start-pos end-pos) or just path
+         triplet
+       ;; If `non-exist' and 'triplet' are nil, look for any
+       ;; existing whitespace delimited filename at point.  If
+       ;; match consists of punctuation only, like . or ..,
+       ;; don't treat it as a pathname.
+       (when (null triplet)
+         (let* ((space-delimiter "[ \t]"))
+           (setq triplet (hargs:delimited (format 
"^\\|\\(%s\\|[\]\[()<>\;&,@]\\)+"
+                                                  space-delimiter)
+                                          "\\([\]\[()<>\;&,@]\\|:*\\s-\\)+\\|$"
+                                          t t t)
+                 p (car triplet))))
+       ;; May have matched to a string with an embedded double
+       ;; quote or surrounded by braces; if so, don't consider it a path.
+       ;; Also ignore whitespace delimited root dirs, e.g. " / ".
+       (when (and (stringp p) (not (string-match-p 
"\\`{.*}\\'\\|\"\\|\\`[/\\]+\\'" p))
+                  (delq nil (mapcar (lambda (c) (/= (char-syntax ?.) 
(char-syntax c))) p)))
+         ;; Prepend proper directory from cd, ls *, recursive ls or dir file
+         ;; listings when needed.
+         (setq p (string-trim p)
+               p (or (hpath:prepend-shell-directory p) p))
+         (if include-positions
+             (progn (setcar triplet p)
+                    triplet)
+           p))))))
 
 ;;;###autoload
 (defun hpath:display-buffer (buffer &optional display-where)
@@ -1568,9 +1571,10 @@ but locational suffixes within the file are utilized."
                            ;; matching of path is likely to be wrong in
                            ;; certain cases, e.g. with mount point or os path
                            ;; alterations.
-                           (when (and buffer-file-name
+                           (when (or (null buffer-file-name)
+                                     (and buffer-file-name
                                       (equal (file-name-nondirectory path)
-                                             (file-name-nondirectory 
buffer-file-name)))
+                                             (file-name-nondirectory 
buffer-file-name))))
                              (cond ((and anchor kotl-flag)
                                     (klink:act path-with-anchor 
anchor-start-pos))
                                    ((or hash anchor)
@@ -1634,7 +1638,7 @@ of the buffer."
                                                                 (and 
buffer-file-name
                                                                      
(string-match-p "\\`[A-Z][A-Z0-9]+\\'" buffer-file-name)))
                                                             
hpath:outline-section-pattern)
-                                                           (prog-mode
+                                                           ((or prog-mode 
(null buffer-file-name))
                                                             "%s")
                                                            ((or (and 
buffer-file-name
                                                                      
(string-match-p hpath:markdown-suffix-regexp buffer-file-name))
diff --git a/hsys-org.el b/hsys-org.el
index d847ba05c5..853a88aeb6 100644
--- a/hsys-org.el
+++ b/hsys-org.el
@@ -3,7 +3,7 @@
 ;; Author:       Bob Weiner
 ;;
 ;; Orig-Date:     2-Jul-16 at 14:54:14
-;; Last-Mod:     14-Apr-24 at 11:37:50 by Bob Weiner
+;; Last-Mod:     25-May-24 at 10:22:57 by Bob Weiner
 ;;
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;;
@@ -398,10 +398,25 @@ Return the (start . end) buffer positions of the region."
 (defun hsys-org-link-at-p ()
   "Return non-nil iff point is on a square-bracketed Org mode link.
 Assume caller has already checked that the current buffer is in `org-mode'
-or are looking for an Org link in another buffer type."
+or is looking for an Org link in another buffer type."
   (unless (or (smart-eolp) (smart-eobp))
     (with-suppressed-warnings nil
-      (org-in-regexp org-link-bracket-re nil t))))
+      (let ((in-org-link (org-in-regexp org-link-bracket-re nil t)))
+       (when in-org-link
+         (save-match-data
+           ;; If this Org link matches a HyWiki word, let Org handle
+           ;; it with its normal internal link handling only if it
+           ;; has a `hywiki-org-link-type' prefix or if
+           ;; `hywiki-org-link-type-required' is non-nil.  Otherwise,
+           ;; return nil from this function and let ibtypes handle this
+           ;; as a HyWiki word.
+           (if (fboundp 'hywiki-at-wikiword)
+               (if (hywiki-at-wikiword)
+                   (when (or hywiki-org-link-type-required
+                             (hyperb:stack-frame '(hywiki-at-wikiword)))
+                     in-org-link)
+                 in-org-link)
+             in-org-link)))))))
 
 ;; Assume caller has already checked that the current buffer is in org-mode.
 (defun hsys-org-heading-at-p (&optional _)
diff --git a/hui-em-but.el b/hui-em-but.el
index 73d0aeaf88..689da05227 100644
--- a/hui-em-but.el
+++ b/hui-em-but.el
@@ -3,7 +3,7 @@
 ;; Author:       Bob Weiner
 ;;
 ;; Orig-Date:    21-Aug-92
-;; Last-Mod:     20-Jan-24 at 20:09:40 by Mats Lidell
+;; Last-Mod:     18-May-24 at 10:42:36 by Bob Weiner
 ;;
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;;
@@ -19,10 +19,10 @@
 
 ;;; Code:
 
-(when noninteractive
-  ;; Don't load this library
-  (with-current-buffer " *load*"
-    (goto-char (point-max))))
+;; (when noninteractive
+;;   ;; Don't load this library
+;;   (with-current-buffer " *load*"
+;;     (goto-char (point-max))))
 
 ;;; ************************************************************************
 ;;; Other required Elisp libraries
@@ -159,10 +159,13 @@ moves over it."
     (when hproperty:but-emphasize-flag
       (overlay-put but 'mouse-face 'highlight))))
 
-(defun hproperty:but-clear (&optional pos)
-  "Remove highlighting from any named Hyperbole button at point or POS."
-  (let ((but (hproperty:but-get pos)))
-    (when but (delete-overlay but))))
+(defun hproperty:but-clear (&optional pos property value)
+  "Remove highlighting from any named Hyperbole button at point or POS.
+If optional PROPERTY and VALUE are given, remove highlighting only if
+the PROPERTY at the position matches VALUE."
+  (let ((but (hproperty:but-get pos property value)))
+    (when but
+      (delete-overlay but))))
 
 (defun hproperty:but-clear-all (&optional regexp-match)
   "Remove highlighting from all named Hyperbole buttons in buffer.
@@ -179,6 +182,10 @@ de-highlighted."
     (remove-overlays nil nil 'face hproperty:but-face)
     (remove-overlays nil nil 'face hproperty:ibut-face)))
 
+(defun hproperty:but-clear-all-in-list (hbut-list)
+  "Delete all HBUT-LIST hproperties."
+  (mapc #'delete-overlay hbut-list))
+
 (defun hproperty:but-create (&optional regexp-match)
   "Highlight all named Hyperbole buttons in buffer.
 De-highlight buttons unless `hproperty:but-highlight-flag' is set.
@@ -217,23 +224,47 @@ moves over it."
     (narrow-to-region start end)
     (hproperty:but-create-all)))
 
+(defun hproperty:but-delete (hproperty-but)
+  "Remove HPROPERTY-BUT.  See `hproperty:but-get'."
+  (delete-overlay hproperty-but))
+
+(defun hproperty:but-end (hproperty-but)
+  "Return the start position of an HPROPERTY-BUT.
+See `hproperty:but-get'."
+  (overlay-end hproperty-but))
+
+(defun hproperty:but-get-all-in-region (start end &optional property value)
+  "Return all buttons in the current buffer between START and END.
+If optional PROPERTY and VALUE are given, return only the first button
+with that PROPERTY and VALUE."
+  (delq nil
+       (mapcar (lambda (overlay)
+                 (when (memq (overlay-get overlay (or property 'face))
+                             (if property
+                                 (list value)
+                               (list hproperty:but-face
+                                     hproperty:ibut-face
+                                     hproperty:flash-face)))
+                   overlay))
+               (overlays-in start end))))
+
+(defun hproperty:but-get (&optional pos property value)
+  "Return button at optional POS or point.
+If optional PROPERTY and VALUE are given, return only the first button
+with that PROPERTY and VALUE."
+  (car (hproperty:but-get-all-in-region pos (1+ pos) property value)))
+
+(defun hproperty:but-start (hproperty-but)
+  "Return the end position of an HPROPERTY-BUT.
+See `hproperty:but-get'."
+  (overlay-start hproperty-but))
+
 (add-to-list 'yank-handled-properties '(hproperty:but-face . 
hproperty:but-create-on-yank))
 
 ;;; ************************************************************************
 ;;; Private functions
 ;;; ************************************************************************
 
-(defun hproperty:but-get (&optional pos)
-  "Get button property at optional POS or point."
-  (car (delq nil
-            (mapcar (lambda (props)
-                      (if (memq (overlay-get props 'face)
-                                (list hproperty:but-face
-                                      hproperty:ibut-face
-                                      hproperty:flash-face))
-                          props))
-                    (overlays-at (or pos (point)))))))
-
 (defsubst hproperty:list-cycle (list-ptr list)
   "Move LIST-PTR to next element in LIST or when at end to first element."
   (or (and list-ptr (setq list-ptr (cdr list-ptr)))
@@ -277,11 +308,13 @@ hproperty:color-ptr."
     (redisplay t)
     t))
 
-(defun hproperty:but-p (&optional pos)
+(defun hproperty:but-p (&optional pos property value)
   "Return non-nil at point or optional POS iff on a highlighted Hyperbole 
button."
-  (memq t (mapcar (lambda (props)
-                   (when (memq (overlay-get props 'face)
-                               (list hproperty:but-face hproperty:ibut-face))
+  (memq t (mapcar (lambda (overlay)
+                   (when (memq (overlay-get overlay (or property 'face))
+                               (if property
+                                   (list value)
+                                 (list hproperty:but-face 
hproperty:ibut-face)))
                      t))
                  (overlays-at (or pos (point))))))
 
diff --git a/hui.el b/hui.el
index 3ba334a777..87adf7e2c6 100644
--- a/hui.el
+++ b/hui.el
@@ -3,7 +3,7 @@
 ;; Author:       Bob Weiner
 ;;
 ;; Orig-Date:    19-Sep-91 at 21:42:03
-;; Last-Mod:     21-Mar-24 at 15:30:24 by Bob Weiner
+;; Last-Mod:     25-May-24 at 11:19:06 by Bob Weiner
 ;;
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;;
@@ -1623,8 +1623,8 @@ completion of all labeled buttons within the current 
buffer."
       (hproperty:but-create))))
 
 (defun hui:htype-delete (htype-sym)
-  "Delete HTYPE-SYM from use in current Hyperbole session.
-HTYPE-SYM must be redefined for use again."
+  "Delete a prompted-for Hyperbole type from HTYPE-SYM (actypes or ibtypes).
+The type must be redefined for use again."
   (and htype-sym (symbolp htype-sym)
        (let ((type
              (intern (hargs:read-match
@@ -1753,7 +1753,7 @@ button's source file name when the button data is stored 
externally."
 If EDIT-FLAG is non-nil, edit button at point in BUT-WINDOW,
 otherwise, prompt for button label and create a button.
 LBL-KEY is internal form of button label.  BUT-LOC is the file or buffer
-in which to create button.  BUT-DIR is the directory of BUT-LOC.
+in which to create the button.  BUT-DIR is the directory of BUT-LOC.
 TYPE-AND-ARGS is the action type for the button followed by any
 arguments it requires.  Any text properties are removed from string
 arguments."
diff --git a/hyrolo.el b/hyrolo.el
index 1c8b5316eb..136a330ff6 100644
--- a/hyrolo.el
+++ b/hyrolo.el
@@ -3,7 +3,7 @@
 ;; Author:       Bob Weiner
 ;;
 ;; Orig-Date:     7-Jun-89 at 22:08:29
-;; Last-Mod:      7-Apr-24 at 18:40:47 by Bob Weiner
+;; Last-Mod:      6-May-24 at 00:20:13 by Bob Weiner
 ;;
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;;
@@ -3080,38 +3080,39 @@ Name is returned as `last, first-and-middle'."
 (defun hyrolo-name-at-p ()
   "Iff point is at or within an entry in `hyrolo-display-buffer', return 
non-nil.
 Any non-nil value returned is a cons of (<entry-name> . <entry-source>)."
-  (let ((entry-source (hbut:get-key-src t))
-       (col-num (current-column))
-       (line-start (line-beginning-position))
-       (line-end (line-end-position)))
-    (when entry-source
-      (save-excursion
-       (forward-line 0)
-       (let (case-fold-search
-             entry-line
-             entry-name)
-         (if (and (or (looking-at hyrolo-hdr-and-entry-regexp)
-                      (re-search-backward hyrolo-hdr-and-entry-regexp nil t))
-                  (save-match-data (not (looking-at hyrolo-hdr-regexp))))
-             (progn (goto-char (match-end 0))
-                    (skip-chars-forward " \t")
-                    (when (or (looking-at "[^ \t\n\r]+ ?, ?[^ \t\n\r]+")
-                              (looking-at "\\( ?[^ \t\n\r]+\\)+"))
-                      (setq entry-name (match-string-no-properties 0)
-                            entry-line (buffer-substring-no-properties 
line-start line-end))
-                      ;; Add a text-property of 'hyrolo-name-entry with
-                      ;; value of (entry-line . current-column) to entry-name.
-                      (put-text-property 0 1 'hyrolo-name-entry
-                                         (cons entry-line col-num)
-                                         entry-name)
-                      (cons entry-name entry-source)))
-           ;; If not blank, return the current line as the name with
-           ;; a text-property of 'hyrolo-line-entry with value of 
(current-column).
-           (goto-char line-start)
-           (when (not (looking-at "[ \t\f]*$"))
-             (setq entry-line (buffer-substring-no-properties line-start 
line-end))
-             (put-text-property 0 1 'hyrolo-line-entry col-num entry-line)
-             (cons entry-line entry-source))))))))
+  (when (eq (current-buffer) (get-buffer hyrolo-display-buffer))
+    (let ((entry-source (hbut:get-key-src t))
+         (col-num (current-column))
+         (line-start (line-beginning-position))
+         (line-end (line-end-position)))
+      (when entry-source
+       (save-excursion
+         (forward-line 0)
+         (let (case-fold-search
+               entry-line
+               entry-name)
+           (if (and (or (looking-at hyrolo-hdr-and-entry-regexp)
+                        (re-search-backward hyrolo-hdr-and-entry-regexp nil t))
+                    (save-match-data (not (looking-at hyrolo-hdr-regexp))))
+               (progn (goto-char (match-end 0))
+                      (skip-chars-forward " \t")
+                      (when (or (looking-at "[^ \t\n\r]+ ?, ?[^ \t\n\r]+")
+                                (looking-at "\\( ?[^ \t\n\r]+\\)+"))
+                        (setq entry-name (match-string-no-properties 0)
+                              entry-line (buffer-substring-no-properties 
line-start line-end))
+                        ;; Add a text-property of 'hyrolo-name-entry with
+                        ;; value of (entry-line . current-column) to 
entry-name.
+                        (put-text-property 0 1 'hyrolo-name-entry
+                                           (cons entry-line col-num)
+                                           entry-name)
+                        (cons entry-name entry-source)))
+             ;; If not blank, return the current line as the name with
+             ;; a text-property of 'hyrolo-line-entry with value of 
(current-column).
+             (goto-char line-start)
+             (when (not (looking-at "[ \t\f]*$"))
+               (setq entry-line (buffer-substring-no-properties line-start 
line-end))
+               (put-text-property 0 1 'hyrolo-line-entry col-num entry-line)
+               (cons entry-line entry-source)))))))))
 
 (define-derived-mode hyrolo-org-mode outline-mode "HyRoloOrg"
   "Basic Org mode for use in HyRolo display match searches."
diff --git a/hywiki.el b/hywiki.el
new file mode 100644
index 0000000000..a9bfe1f8db
--- /dev/null
+++ b/hywiki.el
@@ -0,0 +1,601 @@
+;;; hywiki.el --- Hyperbole's auto-wikiword note-taking system   -*- 
lexical-binding: t -*-
+;;
+;; Author:       Bob Weiner
+;;
+;; Orig-Date:    21-Apr-24 at 22:41:13
+;; Last-Mod:     25-May-24 at 16:54:09 by Bob Weiner
+;;
+;; SPDX-License-Identifier: GPL-3.0-or-later
+;;
+;; Copyright (C) 2024  Free Software Foundation, Inc.
+;; See the "HY-COPY" file for license information.
+;;
+;; This file is part of GNU Hyperbole.
+
+;;; Commentary:
+;;
+;;  This is Hyperbole's markup-free personal Wiki system for
+;;  note-taking and automatic WikiWord hyperlinking.  A `HyWiki word'
+;;  starts with a capitalized letter and contains only upper and
+;;  lowercase letters.  `HyWiki pages' are Org or other text mode
+;;  files with HyWiki word names (the page name) plus a file-type
+;;  suffix which are stored within `hywiki-directory'.
+;;
+;;  To create a new HyWiki page or to jump to one, simply create an
+;;  Org link in any buffer with the prefix "hy:" followed by a
+;;  capitalized alpha characters-only WikiWord, e.g. [[hy:Emacs]], and
+;;  then press the Action Key on the link to jump to the associated
+;;  page; new pages are automatically created.
+
+;;  If you set `hywiki-org-link-type-required' to `nil', then
+;;  you don't need the prefix, e.g. [[Emacs]] and existing HyWiki page
+;;  names will override Org's standard handling of such links.  To
+;;  prevent Org mode's binding of {M-RET} from splitting lines and
+;;  creating new headlines when on a HyWiki word whose page has not
+;;  yet been created, set `hsys-org-enable-smart-keys' to 't' so that
+;;  Hyperbole's Action Key does the right thing in this context.
+;;
+;;  HyWiki pages are created in `hywiki-directory'.  Within such
+;;  pages, WikiWords (the names of HyWiki pages) work without the need
+;;  for any delimiters.  Simply type them out, e.g. Emacs and if a
+;;  page exists for the word, it is automatically highlighted when:
+;;    - a HyWiki page file is read in
+;;    - a whitespace character, ')', '}', or Org-mode punctuation/symbol
+;;      character is inserted following a HyWiki word
+;;    - the Action Key is pressed to activate a HyWiki word button.
+;;
+;;  HyWiki links can also link to a section headline within a page by
+;;  simply following the page name a '#' character and then the
+;;  section headline name.  For example, if your Emacs page has a
+;;  'Major Modes section, then either [[hy:Emacs#Major Modes]] or
+;;  Emacs#Major-Modes will work as a link to that section.  Note that
+;;  without the square bracket delimiters, you must convert spaces in
+;;  section names to '-' characters.
+;;
+;;  Although HyWiki creates new pages in Org mode, you can manually
+;;  insert pages in Markdown or other text modes within
+;;  `hywiki-directory' and then link to them.  You can also change the
+;;  default `hywiki-file-suffix' to something else, like ".md" to have
+;;  HyWiki use Markdown mode for its pages.  This usage has not yet
+;;  been tested though, so use at your own risk.
+
+;;; Code:
+;;; ************************************************************************
+;;; Other required Elisp libraries
+;;; ************************************************************************
+
+(require 'hasht)
+(require 'hpath)
+(require 'hui-em-but)
+(require 'ol)
+
+(eval-and-compile
+  '(when (require 'company nil t)
+     (add-to-list 'company-backends 'hywiki-company-hasht-backend)))
+
+;;; ************************************************************************
+;;; Public variables
+;;; ************************************************************************
+
+(defcustom hywiki-word-highlight-flag t
+  "Non-nil means automatically highlight non-Org link HyWiki word 
hyperbuttons."
+  :type 'boolean
+  :initialize #'custom-initialize-default
+  :group 'hyperbole-buttons)
+
+(defvar hywiki-file-suffix ".org"
+  "File suffix (including period) to use when creating HyWiki pages.")
+
+(defvar hywiki-directory '"~/hywiki/"
+  "Directory in which to find HyWiki page files.")
+
+(defvar hywiki-non-character-commands
+  '(;; Org mode
+    org-cycle                         ;; TAB
+    org-return                        ;; RET, \r
+    org-return-and-maybe-indent       ;; C-j, \n
+    ;; Markdown mode
+    markdown-cycle                    ;; TAB
+    markdown-enter-key                ;; RET, \r
+    electric-newline-and-maybe-indent ;; C-j, \n
+    ;; Global
+    newline                           ;; RET, \r
+    newline-and-indent                ;; RET, \r
+    quoted-insert                     ;; C-q
+    )
+  "Commands that insert characters but whose input events do not
+  arrive as characters or that quote another character for input.")
+
+;; Define the keymap for hywiki-mode.
+(defvar hywiki-mode-map nil
+  "Keymap for `hywiki-mode'.")
+
+(defconst hywiki-org-link-type "hy"
+  "HyWiki string prefix type for Org links.  Excludes trailing colon.")
+
+(defvar hywiki-org-link-type-required t
+  "When non-nil, HyWiki Org links must start with `hywiki-org-link-type'.
+Otherwise, this prefix is not needed and HyWiki word Org links
+override standard Org link lookups.  See \"(org)Internal Links\".")
+
+(defconst hywiki-word-regexp
+  "\\<\\([[:upper:]][[:alpha:]]+\\)\\>"
+  "Regexp that matches a HyWiki word only.")
+
+(defconst hywiki-word-section-regexp
+  "\\(#[^][# \t\n\r\f]+\\)"
+  "Regexp that matches a HyWiki word #section extension.
+After the first # character, this may contain any non-square-bracket,
+non-# and non-whitespace characters.")
+
+(defconst hywiki-word-optional-section-regexp
+  (concat hywiki-word-regexp hywiki-word-section-regexp "?")
+  "Regexp that matches a HyWiki word with an optional #section.
+Section may not contain spaces or square brackets.  Use '-' to
+substitute for spaces in the section/headline name.  Grouping 1 is
+the HyWiki word and grouping 2 is the #section with the # included.")
+
+(defconst hywiki-word-org-link-regexp
+  (concat hywiki-word-regexp "\\(#[^][\n\r\f]+\\)?")
+  "Regexp that matches a HyWiki word with an optional #section in an Org link.
+Section may not contain spaces or square brackets.  Use '-' to
+substitute for spaces in the section/headline name.  Grouping 1 is
+the HyWiki word and grouping 2 is the #section with the # included.")
+
+(defface hywiki--word-face
+  '((((min-colors 88) (background dark)) (:foreground "orange"))
+    (((background dark)) (:background "orange" :foreground "black"))
+    (((min-colors 88)) (:foreground "orange"))
+    (t (:background "orange")))
+  "Face for HyWiki word highlighting."
+  :group 'hyperbole-buttons)
+
+(defcustom hywiki-word-face 'hywiki--word-face
+  "Hyperbole face for HyWiki word highlighting."
+  :type 'face
+  :initialize #'custom-initialize-default
+  :group 'hyperbole-buttons)
+
+;;; ************************************************************************
+;;; Private variables
+;;; ************************************************************************
+
+(defvar hywiki--buttonize-characters nil
+  "String of single character keys bound to 
`hywiki-buttonize-character-commands'.
+Each such key self-inserts before highlighting any prior HyWiki word.")
+
+(defvar hywiki--pages-hasht nil)
+
+;;; ************************************************************************
+;;; hywiki minor mode
+;;; ************************************************************************
+
+(defun hywiki-buttonize-character-commands ()
+  "Turn any HyWikiWord before point into a highlighted Hyperbole button.
+Triggered by `post-self-insert-hook' for self-inserting characters."
+  (when (and (characterp last-command-event)
+            (seq-find (lambda (c) (= c last-command-event)) 
hywiki--buttonize-characters))
+    (hywiki-highlight-page-name)))
+
+(defun hywiki-buttonize-non-character-commands ()
+  "Turn any HyWikiWord before point into a highlighted Hyperbole button.
+Triggered by `pre-command-hook' for non-character-commands, e.g. return."
+  (when (memq this-command hywiki-non-character-commands)
+    (hywiki-highlight-page-name)))
+
+(defun hywiki-get-buttonize-characters ()
+  "Return a string of Org self-insert keys that have punctuation/symbol 
syntax."
+  (let (key
+       cmd
+       key-cmds
+       result)
+    ;; Org and other text mode self-insert-command bindings are just
+    ;; remaps inherited from global-map.  Create key-cmds list of
+    ;; parsable (key . cmd) combinations where key may be a
+    ;; (start-key . end-key) range of keys.
+    (map-keymap (lambda (key cmd) (setq key-cmds (cons (cons key cmd) 
key-cmds))) (current-global-map))
+    (dolist (key-cmd key-cmds (concat (nreverse result)))
+      (setq key (car key-cmd)
+           cmd (cdr key-cmd))
+      (when (eq cmd 'self-insert-command)
+       (cond ((and (characterp key)
+                   (= (char-syntax key) ?.))
+              ;; char with punctuation/symbol syntax
+              (setq result (cons key result)))
+             ((and (consp key)
+                   (characterp (car key))
+                   (characterp (cdr key))
+                   (<= (cdr key) 256))
+              ;; ASCII char range, some of which has punctuation/symbol syntax
+              (with-syntax-table org-mode-syntax-table
+                (dolist (k (number-sequence (car key) (cdr key)))
+                  (when (memq (char-syntax k) '(?. ?_))
+                    (setq result (cons k result)))))))))))
+
+(define-minor-mode hywiki-mode
+  "Toggle HyWiki minor mode with \\[hywiki-mode].
+
+The hywiki-mode minor mode auto-highlights and creates implicit
+buttons from wiki words.  Any such button jumps to the associated
+HyWiki page or associated section when HyWikiWord#section is used.
+
+When hywiki-mode is enabled, the `hywiki-mode' variable is
+non-nil.
+
+See the Info documentation at \"(hyperbole)HyWiki\".
+
+\\{hywiki-mode-map}"
+
+  :lighter " HyWiki"
+  :keymap hywiki-mode-map
+  (if hywiki-mode
+      (progn (unless hywiki-mode-map
+               (setq hywiki-mode-map (make-sparse-keymap)))
+            ;; Self-insert punct/sym keys that trigger wiki-word
+            ;; highlighting via `hywiki-buttonize-character-commands' in 
`hywiki-mode'.
+            (unless hywiki--buttonize-characters
+              (setq hywiki--buttonize-characters
+                    (concat " \t\r\n\)\]\>\}'" 
(hywiki-get-buttonize-characters))))
+            (add-hook 'post-self-insert-hook 
'hywiki-buttonize-character-commands)
+            (add-hook 'pre-command-hook 
'hywiki-buttonize-non-character-commands 95))
+    (remove-hook 'post-self-insert-hook 'hywiki-buttonize-character-commands)
+    (remove-hook 'pre-command-hook 'hywiki-buttonize-character-commands)))
+
+;;; ************************************************************************
+;;; Public Implicit Button and Action Types
+;;; ************************************************************************
+
+(defib hywiki ()
+  "When on a HyWiki word, display its page and optional section."
+  (let* ((page-name (hywiki-at-wikiword)))
+    (when page-name
+      (ibut:label-set page-name (match-beginning 0) (match-end 0))
+      (hywiki-highlight-page-name t)
+      (hact 'hywiki-find-page page-name))))
+
+(defun hywiki-find-page (&optional page-name prompt-flag)
+  "Display HyWiki PAGE-NAME.  By default, create any non-existent page.
+With optional PROMPT-FLAG t, prompt to create if non-existent.  If
+PROMPT-FLAG is 'exists, return nil unless the page already exists."
+  (interactive (list (completing-read "Find HyWiki page: " 
(hywiki-get-page-list))))
+
+  (let ((in-page-flag (null page-name))
+       (in-hywiki-directory-flag (string-prefix-p (expand-file-name 
hywiki-directory)
+                                                  (or buffer-file-name ""))))
+    ;; If called from `find-file-hook' without a page-name and outside
+    ;; hywiki-directory, do nothing (just finding a regular file).
+    (when (or (stringp page-name) in-hywiki-directory-flag)
+      (when in-page-flag
+       ;; Current buffer must be the desired page (called from 
'find-file-hook')
+       (unless in-hywiki-directory-flag
+         (error "(hywiki-find-page): No `page-name'; buffer file must be in 
`hywiki-directory', not %s"
+                default-directory))
+       (when (null buffer-file-name)
+         (error "(hywiki-find-page): No `page-name' given in a buffer without 
an attached file"))
+       (setq page-name (file-name-sans-extension (file-name-nondirectory 
buffer-file-name))))
+
+      (let* ((section (when (string-match "#" page-name)
+                       (substring page-name (match-beginning 0))))
+            (page-name (if (string-match "#" page-name)
+                           (substring page-name 0 (match-beginning 0))
+                         page-name))
+            (page-file (or (hywiki-get-page page-name)
+                           (if prompt-flag
+                               (unless (eq prompt-flag 'exists)
+                                 (when (y-or-n-p (concat "Create new `" 
page-name "' page? "))
+                                   (hywiki-add-page page-name)))
+                             (hywiki-add-page page-name)))))
+       (when page-file
+         (unless in-page-flag (hpath:find (concat page-file section)))
+         (unless hywiki-mode (hywiki-mode 1))
+         (hywiki-highlight-page-names)
+         (run-hooks 'hywiki-find-page-hook)
+         page-file)))))
+
+;;; ************************************************************************
+;;; Public functions
+;;; ************************************************************************
+
+(defun hywiki-add-to-page (page-name text start-flag)
+  "Add to PAGE-NAME TEXT at page start with START-FLAG non-nil, else end.
+Create page if it does not exist.  If PAGE-NAME is invalid, return
+nil, else return the file name of the page."
+  (let* ((page-file (hywiki-add-page page-name))
+        (page-buf  (when page-file (find-file-noselect page-file))))
+    (when page-buf
+      (save-excursion
+       (with-current-buffer page-buf
+         (barf-if-buffer-read-only)
+         (save-restriction
+           (widen)
+           (goto-char (if start-flag (point-min) (point-max)))
+           (unless (bolp) (insert (newline)))
+           (insert text)
+           (unless (bolp) (insert (newline)))
+           (goto-char (if start-flag (point-min) (point-max)))
+           page-file))))))
+
+(defun hywiki-maybe-at-wikiword-beginning ()
+  "Return non-nil if previous character is one preceding a HyWiki word.
+Does not test whether or not a page exists for the HyWiki word.
+Use `hywiki-get-page' to determine whether a HyWiki page exists."
+  ;; Ignore wikiwords preceded by any non-whitespace character, except
+  ;; any of these: ({"'`'
+  (when (or (bolp)
+           (memq (char-before) '(?\( ?\{ ?\" ?\' ?\` ?\  ?\t ?\n ?\r ?\f)))
+    t))
+
+(defun hywiki-at-wikiword ()
+  "Return HyWiki word and optional #section at point or nil if not on one.
+Does not test whether or not a page exists for the HyWiki word.
+Use `hywiki-get-page' to determine whether a HyWiki page exists."
+  (when hywiki-mode
+    (let ((wikiword (ibut:label-p t "[[" "]]")))
+      (if wikiword
+         ;; Handle an Org link [[HyWikiWord]] [[hy:HyWikiWord]] or 
[[HyWikiWord#section]].
+         (progn
+           (setq wikiword
+                 (org-link-expand-abbrev (org-link-unescape (string-trim 
wikiword))))
+           ;; Ignore prefixed, typed hy:HyWikiWord since Org mode will display 
those.
+           (when (hywiki-is-wikiword wikiword)
+             wikiword))
+       ;; Handle a HyWiki word with optional #section; if it is an Org
+       ;; link, it may optionally have a hy: link-type prefix.
+       (save-excursion
+          (let ((case-fold-search nil))
+           (skip-chars-backward "-*#[:alnum:]")
+           ;; Ignore wikiwords preceded by any non-whitespace
+           ;; character, except any of these: (["'`'
+           (and (hywiki-maybe-at-wikiword-beginning)
+                (looking-at hywiki-word-optional-section-regexp)
+                (string-trim (match-string-no-properties 0)))))))))
+
+;; Globally set these values to avoid using 'let' with stack allocations
+;; within `hywiki-highlight-page-name' frequently.
+(defvar hywiki--any-page-regexp nil)
+(defvar hywiki--but nil)
+(defvar hywiki--but-end nil)
+(defvar hywiki--but-start nil)
+(defvar hywiki--current-page nil)
+(defvar hywiki--end nil)
+(defvar hywiki--page-name nil)
+(defvar hywiki--save-case-fold-search nil)
+(defvar hywiki--save-org-link-type-required nil)
+(defvar hywiki--start nil)
+
+(defun hywiki-highlight-page-names ()
+  "Highlight all non-Org link HyWiki page names in a HyWiki buffer.
+Use `hywiki-word-face' to highlight.  Does not highlight references to
+the current page unless they have sections attached."
+  (interactive)
+  ;; Avoid doing any lets for efficiency.
+  ;; Highlight HyWiki words in buffers where `hywiki-mode' is enabled
+  ;; or with attached files below `hywiki-directory'.
+  (when (and hywiki-word-highlight-flag
+            (or hywiki-mode
+                (string-prefix-p (expand-file-name hywiki-directory)
+                                 (or buffer-file-name ""))))
+    (save-excursion
+      (save-restriction
+       (setq hywiki--any-page-regexp (regexp-opt (hywiki-get-page-list) 'words)
+             hywiki--save-case-fold-search case-fold-search
+             case-fold-search nil
+             hywiki--save-org-link-type-required hywiki-org-link-type-required
+             hywiki-org-link-type-required t
+             hywiki--current-page (hywiki-get-buffer-page-name))
+       (widen)
+       (goto-char (point-min))
+       (while (re-search-forward hywiki--any-page-regexp nil t)
+         (setq hywiki--start (match-beginning 0)
+               hywiki--end   (match-end 0))
+         (save-excursion
+           (goto-char hywiki--start)
+           (when (hywiki-maybe-at-wikiword-beginning)
+             ;; Include any #section.
+             (skip-syntax-forward "^-\)$\>._\"\'")
+             (skip-chars-forward "-#[:alnum:]")
+             (setq hywiki--end (point))
+             ;; Don't highlight current-page matches unless they
+             ;; include a #section.
+             (unless (string-equal hywiki--current-page
+                                   (buffer-substring-no-properties 
hywiki--start hywiki--end))
+               (hproperty:but-add hywiki--start hywiki--end 
hywiki-word-face)))))))
+    (setq case-fold-search hywiki--save-case-fold-search
+         hywiki-org-link-type-required hywiki--save-org-link-type-required)))
+
+(defun hywiki-highlight-page-name (&optional on-page-name)
+  "Highlight any non-Org link HyWiki page name one character before point.
+With optional ON-PAGE-NAME non-nil, assume point is within the page or
+section name.
+
+Use `hywiki-word-face' to highlight.  Does not highlight references to
+the current page unless they have sections attached."
+  (interactive)
+  (when (and hywiki-word-highlight-flag
+            (or on-page-name
+                (and (eq (char-before) last-command-event) ; Sanity check
+                     (not (eq ?# last-command-event))
+                     (memq (char-syntax last-command-event) '(?\  ?\) ?\$ ?\> 
?. ?\" ?\'))))
+             (not executing-kbd-macro)
+             (not noninteractive))
+    (save-excursion
+      ;; (when (= (char-syntax (char-before)) ?\))
+       ;; Clear any HyWikiWord highlighting that may just be a part
+       ;; of a larger balanced delimiter text with multiple words.
+       ;; If there is just a single HyWikiWord, it will be
+       ;; re-highlighted later in this function.
+       ;; (ignore-errors
+       ;;   (let* ((sexp-end (point))
+       ;;       (sexp-start (scan-sexps sexp-end -1)))
+       ;;     (when sexp-start
+       ;;       (hproperty:but-clear-all-in-list
+       ;;        (hproperty:but-get-all-in-region sexp-start sexp-end 'face 
hywiki-word-face))))))
+
+      (unless on-page-name
+       ;; after page name
+       (skip-syntax-backward "-"))
+      ;; May be a closing delimiter that we have to skip past
+      (skip-chars-backward (regexp-quote hywiki--buttonize-characters))
+      ;; Skip pass HyWikiWord or section
+      (skip-syntax-backward "^-$()._\"\'")
+      (skip-chars-backward "#[:alpha:]")
+
+      (setq hywiki--save-case-fold-search case-fold-search
+           case-fold-search nil
+           hywiki--save-org-link-type-required hywiki-org-link-type-required
+           hywiki-org-link-type-required t)
+      (if (and (hywiki-maybe-at-wikiword-beginning)
+              (looking-at hywiki-word-optional-section-regexp)
+              (progn
+                (setq hywiki--page-name (match-string-no-properties 1)
+                      hywiki--start (match-beginning 0)
+                      hywiki--end   (match-end 0))
+                (and (hywiki-get-page hywiki--page-name)
+                     ;; Ignore wikiwords preceded by any non-whitespace 
character
+                     ;; (or (bolp) (memq (preceding-char) '(?\  ?\t)))
+                     )))
+         (progn
+           (setq hywiki--current-page (hywiki-get-buffer-page-name))
+           ;; Don't highlight current-page matches unless
+           ;; they include a #section.
+           (unless (string-equal hywiki--current-page
+                                 (buffer-substring-no-properties hywiki--start 
hywiki--end))
+             (if (setq hywiki--but (hproperty:but-get (point) 'face 
hywiki-word-face))
+                 (progn
+                   (setq hywiki--but-start (hproperty:but-start hywiki--but)
+                         hywiki--but-end   (hproperty:but-end hywiki--but))
+                   (unless (and (= hywiki--start hywiki--but-start) (= 
hywiki--end hywiki--but-end))
+                     (hproperty:but-delete hywiki--but)
+                     (hproperty:but-add hywiki--start hywiki--end 
hywiki-word-face)))
+               (hproperty:but-add hywiki--start hywiki--end 
hywiki-word-face))))
+       ;; Remove any potential earlier highlighting since the
+       ;; previous word may have changed.
+       (skip-syntax-backward "^-$()._\"\'")
+       (hproperty:but-clear (point) 'face hywiki-word-face)))))
+
+(defun hywiki-is-wikiword (word)
+  "Return non-nil if WORD is a HyWiki word and optional #section.
+The page for the word may not yet exist.  Use `hywiki-get-page'
+to determine whether a HyWiki word page exists."
+  (and (stringp word)
+       (let (case-fold-search)
+        (or (eq (string-match (concat "\\`" hywiki-word-org-link-regexp "\\'") 
word)
+                0)
+            (eq (string-match (concat "\\`" 
hywiki-word-optional-section-regexp "\\'") word)
+                0)))))
+
+(defun hywiki-get-buffer-page-name ()
+  "Extract the page name from the buffer file name or else buffer name."
+  (file-name-sans-extension (file-name-nondirectory
+                            (or buffer-file-name (buffer-name)))))
+
+(defun hywiki-get-page (page-name)
+  "Return the absolute path of HyWiki PAGE-NAME or nil if it does not exist."
+  (if (and (stringp page-name) (not (string-empty-p page-name))
+          (eq (string-match hywiki-word-org-link-regexp page-name) 0))
+      (progn
+       (when (match-string-no-properties 2 page-name)
+         ;; Remove any #section suffix in PAGE-NAME.
+         (setq page-name (match-string-no-properties 1 page-name)))
+
+       (or (hash-get page-name (hywiki-get-page-hasht))
+           ;; If page exists but not yet in lookup hash table, add it.
+           (when (file-readable-p (hywiki-get-page-file page-name))
+             (hywiki-add-page page-name))))
+    (user-error "(hywiki-get-page): Invalid page name: '%s'; must be 
capitalized, all alpha" page-name)))
+
+(defun hywiki-get-page-file (page-name)
+  "Return possibly non-existent file name for PAGE NAME.
+No validation of PAGE-NAME is done."
+  (concat (expand-file-name page-name hywiki-directory) hywiki-file-suffix))
+
+(defun hywiki-get-page-files ()
+  "Return the list of existing HyWiki page file names.
+These may have any alphanumeric file suffix, if files were added manually."
+  (when (and (stringp hywiki-directory) (file-readable-p hywiki-directory))
+    (directory-files-recursively hywiki-directory (concat "^" 
hywiki-word-regexp "\\.[A-Za-z0-9]+$"))))
+
+(defun hywiki-get-page-hasht ()
+  "Return hash table of existing HyWiki pages."
+  (or hywiki--pages-hasht (hywiki-make-pages-hasht)))
+
+(defun hywiki-get-page-list ()
+  (hash-map #'cdr (hywiki-get-page-hasht)))
+
+(defun hywiki-add-page (page-name)
+  "Add the HyWiki page for PAGE-NAME and return its file.
+If file exists already, just return it.  If PAGE-NAME is invalid,
+return nil.
+
+Use `hywiki-get-page' to determine whether a HyWiki page exists."
+  (if (and (stringp page-name) (not (string-empty-p page-name))
+          (eq (string-match hywiki-word-org-link-regexp page-name) 0))
+      (progn
+       (when (match-string-no-properties 2 page-name)
+         ;; Remove any #section suffix in PAGE-NAME.
+         (setq page-name (match-string-no-properties 1 page-name)))
+
+       (let ((page-file (hywiki-get-page-file page-name))
+             (pages-hasht (hywiki-get-page-hasht)))
+         (unless (file-readable-p page-file)
+           ;; Create any parent dirs necessary to create empty file
+           (make-empty-file page-file t))
+         (unless (hash-get page-name pages-hasht)
+           (hash-add page-file page-name pages-hasht))
+         page-file))
+    (user-error "(hywiki-add-page): Invalid page name: '%s'; must be 
capitalized, all alpha" page-name)))
+
+(defun hywiki-make-pages-hasht ()
+  (let* ((page-files (hywiki-get-page-files))
+        (page-elts (mapcar (lambda (file)
+                             (cons file (file-name-sans-extension 
(file-name-nondirectory file))))
+                           page-files)))
+    (setq hywiki--pages-hasht (hash-make page-elts))))
+
+(eval-and-compile
+'(when (featurep 'company)
+(defun hywiki-company-hasht-backend (command &optional _arg &rest ignored)
+ "A `company-mode` backend that completes from the keys of a hash table."
+ (interactive (list 'interactive))
+ (when (hywiki-at-wikiword)
+   (pcase command
+     ('interactive (company-begin-backend 'company-hash-table-backend))
+     ('prefix (company-grab-word))
+     ('candidates
+      (let ((prefix (company-grab-word)))
+       (when prefix 
+          (cl-loop for key being the hash-keys in (hywiki-get-page-list)
+                   when (string-prefix-p prefix key)
+                   collect key))))
+     ('sorted t))))))
+
+(defun hywiki-org-link-complete (&optional _arg)
+  "Complete HyWiki page names for `org-insert-link'."
+  (concat
+   (when hywiki-org-link-type-required
+     (concat hywiki-org-link-type ":"))
+   (let ((completion-ignore-case t))
+     (completing-read "HyWiki page: " (hywiki-get-page-list) nil t))))
+
+(defun hywiki-org-link-store ()
+  "Store a link to a HyWiki word at point, if any."
+  (when (hywiki-at-wikiword)
+    (let* ((page-name (hywiki-at-wikiword))
+           (link (concat
+                 (when hywiki-org-link-type-required
+                   (concat hywiki-org-link-type ":"))
+                 page-name))
+           (description (format "HyWiki page for '%s'" page-name)))
+      (org-link-store-props
+       :type hywiki-org-link-type
+       :link link
+       :description description))))
+
+(org-link-set-parameters hywiki-org-link-type
+                         :complete #'hywiki-org-link-complete
+                        :follow #'hywiki-find-page
+                        :store #'hywiki-org-link-store)
+
+(add-hook 'find-file-hook #'hywiki-find-page t)
+
+(provide 'hywiki)
diff --git a/test/hyrolo-tests.el b/test/hyrolo-tests.el
index d2655e297c..13444faf8b 100644
--- a/test/hyrolo-tests.el
+++ b/test/hyrolo-tests.el
@@ -3,7 +3,7 @@
 ;; Author:       Mats Lidell <ma...@gnu.org>
 ;;
 ;; Orig-Date:    19-Jun-21 at 22:42:00
-;; Last-Mod:     31-Mar-24 at 22:29:58 by Mats Lidell
+;; Last-Mod:     18-May-24 at 20:14:05 by Bob Weiner
 ;;
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;;
@@ -1613,21 +1613,21 @@ body
 (ert-deftest hyrolo-tests--goto-kotl-header-with-slash-match ()
   "Move from heading match to target line with a slash in kotl file."
   :expected-result :failed
-  (let* ((kotl-file1 (hyrolo-tests--gen-kotl-outline "h1 / h2" "body" 1))
+  (let* ((kotl-file1 (hyrolo-tests--gen-kotl-outline "h1" "body" 1))
          (hyrolo-file-list (list kotl-file1)))
     (unwind-protect
         (progn
-          (hyrolo-grep "h2")
-          (hyrolo-next-match)
+         (kotl-mode:beginning-of-buffer)
+          (hyrolo-grep "h1/h1 1")
           (action-key)
           (should (string= (buffer-file-name) kotl-file1))
-          (should (looking-at-p "h2$"))
+          (should (looking-at-p "h1 1$"))
           (should (string= (buffer-substring-no-properties (point-min) 
(point-max))
                            "\
-   1. h1 / h2
+   1. h1
       body
 
-     1a. h1 / h2 1
+     1a. h2
          body 1
 
 "                           )))

Reply via email to