branch: externals/hyperbole
commit dbc6f353604876721b49a8382651b56df7d67a1c
Author: Bob Weiner <r...@gnu.org>
Commit: Bob Weiner <r...@gnu.org>

    Add initial version of "hywiki.el" and the hywiki ibtype
---
 ChangeLog     |  21 +++
 MANIFEST      |   4 +
 Makefile      |   7 +-
 hasht.el      | 432 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 hibtypes.el   |   9 +-
 hsys-org.el   |  21 ++-
 hui-em-but.el |  63 ++++++---
 hywiki.el     | 433 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 963 insertions(+), 27 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index e03a662e7d..9d7fd5ef74 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,24 @@
+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 940d0100d1..8a0a4fd8e6 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@
 # Author:       Bob Weiner
 #
 # Orig-Date:    15-Jun-94 at 03:42:38
-# Last-Mod:     14-Apr-24 at 23:00:20 by Bob Weiner
+# Last-Mod:     22-Apr-24 at 02:10:20 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)
 
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/hibtypes.el b/hibtypes.el
index 8246fbd355..d0241216ca 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:     22-Apr-24 at 02:06:09 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
@@ -88,6 +89,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
 ;;; ========================================================================
diff --git a/hsys-org.el b/hsys-org.el
index d847ba05c5..6d7f6f9882 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:     21-Apr-24 at 12:17:44 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 t)
+                   (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..9c9fa36f55 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:     22-Apr-24 at 02:06:26 by Bob Weiner
 ;;
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;;
@@ -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.
@@ -217,23 +220,41 @@ 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 (&optional pos property value)
+  "Get button at optional POS or point.
+If optional PROPERTY and VALUE are given, return only the first button
+with that PROPERTY and VALUE."
+  (car (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-at (or pos (point)))))))
+
+(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 +298,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/hywiki.el b/hywiki.el
new file mode 100644
index 0000000000..0e3798ef40
--- /dev/null
+++ b/hywiki.el
@@ -0,0 +1,433 @@
+;;; 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:     22-Apr-24 at 02:22:28 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.  You will be prompted to create the page if it does not
+;;  exist.
+
+;;  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 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 'hui-em-but)
+(require 'ol)
+
+(when (package-installed-p 'company)
+  (package-activate 'company)
+  (require 'company)
+  (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-allowed-modes '(text-mode wiki-mode)
+  "Parent modes where HyWiki words are recognized without delimiters.
+Applies only when the file is below `hywiki-directory'.")
+
+(defconst hywiki-directory '"~/hywiki/"
+  "Directory in which to find HyWiki page files.")
+
+(defconst hywiki-org-link-type "hy:"
+  "HyWiki string prefix type for Org links.")
+
+(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\".")
+
+(defvar hywiki-pages-hasht nil)
+
+(defconst hywiki-word-regexp
+  "\\<\\([[:upper:]][[:alpha:]]+\\)\\>"
+  "Regexp that matches a HyWiki word only.")
+
+(defconst hywiki-word-optional-section-regexp
+  (concat hywiki-word-regexp "\\(#[^][ \t\n\r\f]+\\)?")
+  "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 "mediumbrown"))
+    (((background dark)) (:background "orange" :foreground "black"))
+    (((min-colors 88)) (:foreground "darkbrown"))
+    (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)
+
+;;; ************************************************************************
+;;; Public Implicit Button and Action Types
+;;; ************************************************************************
+
+;;;###autoload
+(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-open page-name))))
+
+;;;###autoload
+(defun hywiki-open (page-name)
+  "Display HyWiki PAGE-NAME.  Prompt to create if non-existent."
+  (interactive (list (completing-read "HyWiki page: " (hywiki-page-list))))
+  (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)
+                       (when (y-or-n-p (concat "Create missing page, " 
page-name "? "))
+                         (hywiki-add-page page-name)))))
+    (when page-file
+      (hpath:find (concat page-file section)))))
+
+;;; ************************************************************************
+;;; Public functions
+;;; ************************************************************************
+
+(defun hywiki-at-wikiword (&optional org-link-flag)
+  "Return HyWiki word and optional #section at point or nil if not on one."
+  (let (wikiword)
+    (if (or org-link-flag (hsys-org-link-at-p))
+       ;; Handle an Org link [[HyWiki word]] [[hy:HyWiki word]] or [[HyWiki 
word#section]]
+       (progn
+         (setq wikiword
+               (org-link-expand-abbrev
+                (org-link-unescape
+                 (string-trim (match-string-no-properties 1)))))
+         ;; Ignore hy:word hywiki:word 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 or may not have a hy: link-type prefix.
+      (and (apply #'derived-mode-p hywiki-allowed-modes)
+          (string-prefix-p (expand-file-name hywiki-directory)
+                           (or buffer-file-name ""))
+          (save-excursion
+             (let ((case-fold-search nil))
+              (skip-chars-backward "-*#[:alpha:]")
+              ;; Ignore wikiwords preceded by any non-whitespace character
+              (and (or (bolp) (memq (preceding-char) '(?\[ ?\  ?\t ?\n ?\r 
?\f)))
+                   (looking-at hywiki-word-optional-section-regexp)
+                   (string-trim (match-string-no-properties 0)))))))))
+
+(defun hywiki-highlight-page-names ()
+  "Highlight all non-Org link HyWiki page names in the buffer.
+Use `hywiki-word-face' to highlight.  Does not highlight references to
+the current page unless they have sections attached.
+
+Used as a `find-file-hook'."
+  (interactive)
+  ;; Highlight HyWiki words only in files below `hywiki-directory'
+  (when (and hywiki-word-highlight-flag
+            (string-prefix-p (expand-file-name hywiki-directory)
+                             (or buffer-file-name "")))
+    (let ((any-page (string-join (hywiki-page-list) "\\|"))
+         (case-fold-search nil)
+         (hywiki-org-link-type-required t)
+         current-page
+         start
+         end)
+      (save-excursion
+       (save-restriction
+         (widen)
+         (goto-char (point-min))
+         (while (re-search-forward (format "\\<\\(%s\\)\\>" any-page) nil t)
+           (setq start (match-beginning 0)
+                 end   (match-end 0))
+           (save-excursion
+             (goto-char start)
+             ;; Ignore wikiwords preceded by any non-whitespace character
+             (when (or (bolp) (memq (preceding-char) '(?\  ?\t)))
+               ;; Include any #section
+               (skip-chars-forward "-#[:alpha:]")
+               (setq end (point)
+                     current-page (hywiki-get-buffer-page-name))
+               ;; Don't highlight current-page matches unless
+               ;; they include a #section.
+               (unless (string-equal current-page
+                                     (buffer-substring-no-properties start 
end))
+                 (hproperty:but-add start end hywiki-word-face))))))))))
+
+(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.
+
+Used as a `post-self-insert-hook'."
+  (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)
+            (string-prefix-p (expand-file-name hywiki-directory)
+                             (or buffer-file-name "")))
+    (let ((case-fold-search nil)
+         (hywiki-org-link-type-required t)
+         but
+         current-page
+         page-name
+         start
+         end)
+      (save-excursion
+       (if on-page-name
+           (progn (skip-syntax-backward "^-\)$\>.\"\'")
+                  (skip-chars-backward "#[:alpha:]"))
+         ;; after page name
+         (goto-char (max (1- (point)) (point-min)))
+         (skip-chars-backward "-#[:alpha:]"))
+       (if (and (looking-at hywiki-word-optional-section-regexp)
+                ;; Ignore wikiwords preceded by any non-whitespace character
+                (or (bolp) (memq (preceding-char) '(?\  ?\t ?\n ?\r ?\f)))
+                (progn
+                  (setq page-name (match-string-no-properties 1)
+                        start (match-beginning 0)
+                        end   (match-end 0))
+                  (and (hywiki-get-page page-name)
+                       ;; Ignore wikiwords preceded by any non-whitespace 
character
+                       (or (bolp) (memq (preceding-char) '(?\  ?\t))))))
+           (progn
+             (setq current-page (hywiki-get-buffer-page-name))
+             ;; Don't highlight current-page matches unless
+             ;; they include a #section.
+             (unless (string-equal current-page
+                                   (buffer-substring-no-properties start end))
+               (if (setq but (hproperty:but-get (point) 'face 
hywiki-word-face))
+                   (progn
+                     (setq but-start (hproperty:but-start but)
+                           but-end   (hproperty:but-end but))
+                     (unless (and (= start but-start) (= end but-end))
+                       (hproperty:but-delete but)
+                       (hproperty:but-add start end hywiki-word-face)))
+                 (hproperty:but-add start 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)
+       (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-pages ()
+  (or hywiki-pages-hasht (hywiki-make-pages-hasht)))
+
+(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."
+  (directory-files-recursively hywiki-directory (concat "^" hywiki-word-regexp 
"\\.[A-Za-z0-9]$")))
+
+(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-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))))
+
+(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-pages))
+           ;; 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-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-pages)))
+         (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-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
+       (set-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-page-list ()
+  (hash-map #'cdr (hywiki-get-pages)))
+
+(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)
+   (case command
+     ('interactive (company-begin-backend 'company-hash-table-backend))
+     ('prefix (company-grab-symbol))
+     ('candidates
+      (let ((prefix (company-grab-symbol)))
+       (when prefix 
+          (cl-loop for key being the hash-keys in (hywiki-page-list)
+                   when (string-prefix-p prefix key)
+                   collect key))))
+     ('sorted t))))
+
+;; HyWiki org link type, abbreviated as 'hy'
+(org-link-set-parameters "hy"
+                         :complete #'hywiki-complete
+                        :follow #'hywiki-open
+                        :store #'hywiki-store-link)
+
+(defun hywiki-complete (&optional _arg)
+  "Complete HyWiki page names for `org-insert-link'."
+  (concat
+   hywiki-org-link-type
+   (let ((completion-ignore-case t))
+     (completing-read "HyWiki page: " (hywiki-page-list) nil t))))
+
+(defun hywiki-store-link ()
+  "Store a link to a HyWiki word at point, if any."
+  (when (hywiki-at-wikiword)
+    (let* ((page-name (hywiki-at-wikiword))
+           (link (concat "hy:" page-name))
+           (description (format "HyWiki page for %s" page-name)))
+      (org-link-store-props
+       :type hywiki-org-link-type
+       :link link
+       :description description))))
+
+(add-hook 'find-file-hook #'hywiki-highlight-page-names t)
+(add-hook 'post-self-insert-hook #'hywiki-highlight-page-name t)
+
+(provide 'hywiki)


Reply via email to