;;; 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:     21-Aug-26 at 11:55:52 by Bob Weiner
;;
;; SPDX-License-Identifier: GPL-3.0-or-later
;;
;; Copyright (C) 2024-2026  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 wiki word highlighting and hyperlinking.  It uses Org
;;  mode for note taking and adds automatic hyperlinking of HyWikiWords
;;  within Org files in `hywiki-directory' (default = "~/hywiki"), where
;;  a HyWikiWord is a capitalized word that contains upper and lowercase
;;  letters only and has a corresponding HyWikiWord.org wiki page file
;;  below `hywiki-directory'.  HyWikiWords require no delimiters.
;;
;;  HyWikiWords are also recognized in text buffers after the global
;;  minor mode, `hywiki-mode' is enabled via {M-x hywiki-mode RET}.  To
;;  create or jump to a HyWiki page, simply type o ut a potential
;;  HyWikiWord or move point onto one and press the Action Key {M-RET}.
;;  This will create the associated page if it does not exist.  This
;;  also highlights any other instances of HyWikiWords across all
;;  visible Emacs windows.  HyWiki is built for scalability and has been
;;  tested to be performant with 10,000 HyWikiWords.
;;
;;  Once Hyperbole has been loaded and activated, HyWikiWords (with or
;;  without delimiters) are automatically highlighted and active in
;;  the following contexts:
;;    - HyWiki page buffers;
;;    - non-special text buffers, when `hywiki-mode' is enabled;
;;    - comments and strings in programming buffers, when
;;      `hywiki-mode' is enabled.
;;
;;  As HyWikiWords are typed, highlighting occurs after a trailing
;;  whitespace or punctuation character is added, or when it is
;;  surrounded by a matching pair of characters such as curly braces
;;  or single square brackets.  Since Org links use double square
;;  brackets and Org targets use double or triple angle brackets,
;;  HyWikiWords within these delimiters are ignored.
;;
;;  You can also create Org links to HyWikiWords in any non-special text
;;  buffer by surrounding them with double square brackets and the
;;  'hy:' prefix, as in: [[hy:MyWikiWord]].  If you set
;;  `hywiki-org-link-type-required' to `nil', then you don't need the
;;  prefix, e.g. [[MyWikiWord]]; existing HyWiki page names then 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 HyWikiWord 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.
;;
;;  HyWikiWord links can also link to a section headline within a page
;;  by simply following the page name with a '#' character and then the
;;  section headline name.  For example, if your Emacs page has a "Major
;;  Modes" section, then either Emacs#Major-Modes or [[hy: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.  As long as the page exists, section links are
;;  highlighted regardless of whether associated sections exist or not.
;;  When activating a link with a section reference, you will get an
;;  error if the section does not exist.
;;
;;  By default (hywiki-mode = :pages), HyWikiWords are
;;  auto-highlighted within HyWiki pages only.  Outside of such pages,
;;  `hywiki-mode' must be set to :all to enable auto-highlighting in
;;  programming and text modes.  Auto-highlighting depends on pre- and
;;  `post-command-hook' settings.  If an error occurs running one of
;;  these, the associated hook is removed.  To restore the
;;  auto-highlight hooks use {C-u C-h h h m} to  toggle `hywiki-mode';
;;  this also enables auto-highlighting when `hywiki-mode' is non-nil.

;;  The custom setting, `hypb:exclude-major-modes' (default = nil), is
;;  a list of major modes to exclude from HyWikiWord auto-highlighting
;;  and recognition.
;;
;;  Within programming modes, HyWikiWords are highlighted/hyperlinked
;;  within comments and double-quoted strings only.  For programming
;;  modes in which you want HyWikiWords recognized everywhere, add
;;  them to the custom setting, `hywiki-highlight-all-in-prog-modes'
;;  (default = '(lisp-interaction-mode)).
;;
;;  HyWiki adds two implicit button types to Hyperbole:
;;    `hywiki-word'          - create and display HyWikiWord referents;
;;    `hywiki-existing-word' - display an existing HyWikiWord referent.
;;
;;  `hywiki-word' is one of the lowest priority implicit button types
;;  so that it triggers only when other types are not recognized first.
;;
;;  A HyWiki can be exported to HTML for publishing to the web via Org
;;  mode's publish a project feature.  {M-x hywiki-publish-to-html RET}
;;  will and that's it!  Add a prefix argument to force regeneration of all
;;  HyWiki pages, rather than only those that have been updated.
;;
;;  The full set of HyWiki-specific Org publish properties are set in
;;  the variable `hywiki-org-publish-project-alist'.  When the HyWiki
;;  code is loaded into Emacs, it automatically integrates these
;;  properties with Org's publishing framework, so when in a HyWiki
;;  page, you can use the standard {C-c C-e P p} current project publish
;;  command.
;;
;;  There are a few publishing settings you can customize prior to
;;  loading Hyperbole's HyWiki code.
;;
;;  HyWiki html files are saved in:
;;    (hywiki-org-get-publish-property :publishing-directory)
;;  Customize this directory with:
;;    {M-x customize-variable RET hywiki-org-publishing-directory RET}.
;;
;;  HyWiki html files are generated by the function given by:
;;    (hywiki-org-get-publish-property :publishing-function)
;;  Customize the value of this function if necessary with:
;;    {M-x customize-variable RET hywiki-org-publishing-function RET}.
;;
;; This section summarizes HyWikiWord Actions based on the
;;
;; hywiki-referent-prompt-flag      When nil                   When t
;;  -------------------------------------------------------------------------------------
;;  Action Key              hywiki-word-create-and-display
;;    or HyWiki/Create      Create Page and Display          Create Referent and Display
;;  Assist Key              hywiki-word-create-and-display
;;    or C-u HyWiki/Create  Create Referent and Display      Create Page and Display
;;  hywiki-word-create      hywiki-create-page with Msg      hywiki-create-referent with Msg
;;  C-u hywiki-word-create  hywiki-create-referent with Msg  hywiki-create-page with Msg

;;; Code:
;;; ************************************************************************
;;; Other required Elisp libraries
;;; ************************************************************************

(require 'hactypes)   ;; For `link-to-file-interactively'
(require 'hargs)
(require 'hasht)
(require 'hbut)       ;; For `hbut:syntax-table'
;; (unless (featurep 'hibtypes)
;;   (require 'hibtypes))   ;; For `pathname' and `pathname-line-and-column'
(require 'hpath)
(require 'hproperty)
(require 'hsys-consult)
(eval-when-compile (require 'consult nil t))
(require 'hui)        ;; For `hui:actype'
(require 'hui-mini)   ;; For `hui:menu-act'
(require 'hypb)       ;; Requires `seq', hypb:exclude-major-modes,
                      ;; hypb:include-major-modes
(require 'hyrolo)
(require 'outline)    ;; For `outline-mode-syntax-table'
(require 'seq)        ;; For `seq-contains-p', `seq-difference' and `seq-intersection'
(require 'subr-x)     ;; For `string-remove-prefix'
(require 'thingatpt)

;;; ************************************************************************
;;; Public declarations
;;; ************************************************************************

(defvar action-key-modeline-buffer-id-function)  ;; "hui-mouse.el"
(defvar bookmark-current-bookmark)               ;; "bookmark.el"
(defvar consult-grep-args)                       ;; "consult.el"
(defvar consult-ripgrep-args)                    ;; "consult.el"
(defvar hkey-value)                              ;; "hui-mouse.el"
(defvar hywiki-referent-menu nil)                ;; "hywiki.el"
(defvar org-agenda-buffer-tmp-name)              ;; "org-agenda.el"
(defvar org-export-with-broken-links)            ;; "ox.el"
(defvar org-publish-project-alist)               ;; "ox-publish.el"
(defvar hyperbole-mode)                          ;; "hyperbole.el"

(declare-function Info-current-filename-sans-extension "hui-mouse")
(declare-function Info-read-index-item-name "hui-mouse")
(declare-function activities-completing-read "ext:activities" (:prompt prompt :default default))
(declare-function activities-new "ext:activities" (name))
(declare-function activities-resume "ext:activities" (activity :resetp resetp))
(declare-function bookmark-completing-read "bookmark" (prompt &optional default))
(declare-function bookmark-location "bookmark" (bookmark-name-or-record))
(declare-function consult--async-command "ext:consult")
(declare-function consult--async-process "ext:consult")
(declare-function consult--async-refresh-timer "ext:consult")
(declare-function consult--async-sink "ext:consult")
(declare-function consult--async-split "ext:consult")
(declare-function consult--async-throttle "ext:consult")
(declare-function consult--lookup-member "ext:consult")
(declare-function consult--read "ext:consult")
(declare-function denote-file-prompt "ext:denote")
(declare-function denote-get-link-description "ext:denote")
(declare-function denote-get-path-by-id "ext:denote")
(declare-function denote-open-or-create "ext:denote")
(declare-function denote-retrieve-filename-identifier "ext:denote")
(declare-function hsys-denote-file-at-p "hsys-denote")
(declare-function hsys-denote-link-at-p "hsys-denote")
(declare-function hsys-org-at-tags-p "hsys-org")
(declare-function hyperbole-mode "hyperbole")
(declare-function hywiki-org-format-heading "hywiki")
(declare-function ibtypes::pathname "hpath")
(declare-function ibtypes::pathname-line-and-column "hpath")
(declare-function org-fold-core-remove-optimisation "org-fold-core")
(declare-function org-fold-core-update-optimisation "org-fold-core")
(declare-function org-fold-show-entry "org-fold")
(declare-function org-link-store-props "ol" (&rest plist))
(declare-function org-publish-property "ox-publish" (property project &optional default))
(declare-function org-roam-node-from-title-or-alias "org-roam-node" (s &optional nocase))
(declare-function org-roam-node-open "org-roam" (note &optional cmd force))
(declare-function org-roam-node-read "org-roam" (&optional initial-input filter-fn sort-fn require-match prompt))
(declare-function org-roam-node-title "org-roam-node" (node))
(declare-function smart-treemacs-edit "hui-treemacs" (&optional dir))
(declare-function smart-treemacs-modeline "hui-treemacs")

;;; ************************************************************************
;;; Private variables
;;; ************************************************************************

(defvar hywiki--prior-mode nil)

(defvar-local hywiki--buffer-modified-tick nil
  "Used to determine if a command modifies a buffer or not.
The `pre-command-hook' saves this value for a buffer and `post-command-hook'
checks it to determine if any buffer modification has occurred or not.")

;; Must be set after `hywiki-get-buttonize-characters' is defined
(defvar hywiki--buttonize-character-regexp nil
  "Regexp matching a single separating character following a HyWikiWord.
Each such key self-inserts before highlighting any prior HyWikiWord
in `hywiki-mode'.")

(defvar hywiki--buttonize-characters-cache nil
  "Single string cache of Org-mode syntax table punctuation/symbol characters.")

(defconst hywiki--close-open-hasht (hash-make '(("\"" . ?\")
					       ("'" . ?\')
					       ("}"  . ?{)
					       ("]"  . ?\[)
					       (">"  . ?<)
					       (")"  . ?\())
                                             t)
  "Delimiter htable with (close-delim-string . open-delim-char) key-value pairs.")

(defconst hywiki--open-close-hasht (hash-make '(("\"" . ?\")
					       ("'" . ?\')
					       ("{"  . ?})
					       ("["  . ?\])
					       ("<"  . ?>)
					       ("("  . ?\)))
                                             t)
  "Delimiter htable with (open-delim-string . close-delim-char) key-value pairs.")

(defvar hywiki--directory-checksum ""
  "String checksum for `hywiki-directory' page names.")

(defvar hywiki--directory-mod-time nil
  "Last mod time for `hywiki-directory' or nil if the value has not been read.
See `current-time' function for the mod time format.")

(defvar hywiki--org-heading-regexp nil
  "Cache standard `org-complex-heading-regexp' value.
Group 4 is the title.  Call `hywiki--org-set-heading-regexp'
to re-initialize.")

(defvar hywiki--org-todo-regexp nil
  "Cache regular and custom Org todo keywords for `hywiki-directory'.
Call `hywiki--org-set-heading-regexp' to re-initialize.")

;; Redefine the `org-mode-syntax-table' for use in `hywiki-get-buttonize-characters'
;; so do not have to load all of Org mode there.
(defvar hywiki--org-mode-syntax-table
  (let ((st (make-syntax-table outline-mode-syntax-table)))
    (modify-syntax-entry ?\" "\"" st)
    (modify-syntax-entry ?\\ "_" st)
    (modify-syntax-entry ?~ "_" st)
    (modify-syntax-entry ?< "(>" st)
    (modify-syntax-entry ?> ")<" st)
    st)
  "Standard syntax table for Org mode buffers with HyWiki support.")

(defvar hywiki--pages-directory nil)
(defvar hywiki--referent-alist nil
  "HyWiki alist generated from `hywiki--referent-hasht' for storage in cache.
Each element is of the form: (\"wikiword\" . (referent-type . referent-value)).")
(defvar hywiki--referent-hasht nil
  "HyWiki hash table for fast WikiWord referent lookup.")

(defvar hywiki--word-and-buttonize-character-regexp nil
  "Regexp matching HyWikiWord#section plus a valid word separating character.
Group 1 is the entire HyWikiWord#section:Lnum:Cnum expression.")

;; Globally set these values to avoid using 'let' with stack allocations
;; within `hywiki-maybe-highlight-reference' frequently.
(defvar hywiki--any-wikiword-regexp-list nil)
(defvar hywiki--current-page nil)
(defvar hywiki--highlighting-done-flag t)
(defvar hywiki--word-pre-command nil)
(defvar hywiki--word-only nil)
(defvar hywiki--save-case-fold-search nil)
(defvar hywiki--save-org-link-type-required nil)
(defvar hywiki--command-executed-flag nil
  "Prevents multiple runs of hywiki pre and post hooks.")
(defvar-local hywiki--buts nil)
(defvar-local hywiki--but-end nil)
(defvar-local hywiki--but-start nil)
(defvar-local hywiki--buttonize-end (make-marker))   ;; This must always stay a marker
(defvar-local hywiki--buttonize-start (make-marker)) ;; This must always stay a marker
(defvar-local hywiki--buttonize-range nil)
(defvar-local hywiki--char-before nil)
(defvar-local hywiki--end-pos nil)
(defvar-local hywiki--end nil)
(defvar-local hywiki--range nil)
(defvar-local hywiki--start nil)
(defvar-local hywiki--start-pos nil)

;;;
;;; ************************************************************************
;;; Public variables
;;; ************************************************************************

;; Clear the cache if any changes to the `global-map' keymap
(add-variable-watcher 'global-map
                      #'hywiki--clear-buttonize-characters-cache)

(defvar hywiki-glossary-display-buffer "*HyWiki Glossary*"
  "Buffer used to display the last match to a HyWiki glossary definition.")

(defcustom hywiki-highlight-all-in-prog-modes '(lisp-interaction-mode)
  "List of programming major modes to highlight HyWikiWords outside of comments."
  :type '(list symbol)
  :group 'hyperbole-hywiki)

(defcustom hywiki-mode-lighter  " HyWiki"
  "String to display in mode line when the HyWiki global minor mode is enabled.
Use nil for no HyWiki mode indicator."
  :type 'string
  :group 'hyperbole-hywiki)

(defconst hywiki-ignore-face-list '(button hbut-face hbut-item-face
                                    ibut-face org-link)
  "Skip highlighting of HyWikiWords in regions which have any of these faces.")

(defvar hywiki-allow-suffix-referent-types '(page path-link)
  "List of referent type symbols that support # and :L line number suffixes.")

;;;###autoload
(defun hywiki-let-directory (option value)
  (set option value)
  (hywiki-clear-referent-hasht)
  (hywiki-make-referent-hasht))

;;;###autoload
(defun hywiki-set-directory (option value)
  (unless (and (boundp 'hywiki-directory)
	       (equal hywiki-directory (file-name-as-directory value))
	       (hash-table-p hywiki--referent-hasht))
    (set-default option (file-name-as-directory value))
    (hywiki-clear-referent-hasht)
    (hywiki-make-referent-hasht))
  (hywiki-org-set-publish-project))

(defcustom hywiki-directory "~/hywiki/"
  "Directory that holds all HyWiki pages in Org format.
See `hywiki-org-publishing-directory' for exported pages in html format."
  :initialize #'custom-initialize-default
  :set #'hywiki-set-directory
  :type 'string
  :group 'hyperbole-hywiki)

(defun hywiki-directory-changed (option set-to-value operation _where)
  "Watch function for variable `hywiki-directory'.
Function is called with 4 arguments: (OPTION SET-TO-VALUE OPERATION WHERE)."
  (if (memq operation '(let unlet)) ;; not setting global value
      (hywiki-let-directory option set-to-value)
    (hywiki-set-directory option set-to-value)))

;; This next line is needed to invoke `hywiki-set-directory' when
;; `hywiki-directory' is changed via `setq' or `let' rather than
;; `customize-set-variable'.
(add-variable-watcher 'hywiki-directory #'hywiki-directory-changed)

(defvar-local hywiki-buffer-highlighted-state nil
  "State of HyWikiWords highlighting in the associated buffer.
\\='h means the buffer was already highlighted;
\\='d means the buffer was dehighlighted;
nil means no full buffer highlighting has occurred.")

(defvar hywiki-non-character-commands
  '(;; Org mode
    org-cycle                         ;; TAB
    org-open-line                     ;; C-o
    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
    open-line                         ;; C-o
    quoted-insert                     ;; C-q
    )
  "List of non-character commands.
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'.
Presently, there are no key bindings; this is for future use.")

(defconst hywiki-org-link-type "hy"
  "HyWiki string prefix type for Org links.  Excludes trailing colon.")

(defvar hywiki-org-link-type-required t
  "When t, [[hy:HyWiki Org links]] must start with `hywiki-org-link-type':.
Otherwise, this prefix is not needed and HyWikiWord Org links
override standard Org link lookups.  See \"(org)Internal Links\".")

;; Clear the cache if any changes to the `hywiki--org-mode-syntax-table'
(add-variable-watcher 'hywiki--org-mode-syntax-table
                      #'hywiki--clear-buttonize-characters-cache)

(defcustom hywiki-org-publishing-broken-links 'mark
  "HyWiki Org publish option that determines how invalid links are handled.
The default is \\='mark.

When this option is non-nil, broken HyWiki links are ignored,
without stopping the export process.  If it is set to \\='mark,
broken links are marked with a string like:

  [BROKEN LINK: path]

where PATH is the un-resolvable reference."
  :initialize #'custom-initialize-default
  :set (lambda (option value)
	 (set option value)
	 (hywiki-org-set-publish-project))
  :type 'symbol
  :group 'hyperbole-hywiki)

(defcustom hywiki-org-publishing-directory "~/public_hywiki"
  "Directory where HyWiki pages are converted into html and published."
  :initialize #'custom-initialize-default
  :set (lambda (option value)
	 (set option value)
	 (hywiki-org-set-publish-project))
  :type 'string
  :group 'hyperbole-hywiki)

(defcustom hywiki-org-publishing-function 'org-html-publish-to-html
  "HyWiki Org publish function used to export a HyWiki page to html."
  :initialize #'custom-initialize-default
  :set (lambda (option value)
	 (set option value)
	 (hywiki-org-set-publish-project))
  :type 'symbol
  :group 'hyperbole-hywiki)

(defcustom hywiki-org-publishing-sitemap-title
  (let ((dir-name (file-name-base
		   (directory-file-name
		    (file-name-as-directory hywiki-directory)))))
    (if (equal dir-name "hywiki")
	"HyWiki"
      dir-name))
  "HyWiki Org publish sitemap title."
  :initialize #'custom-initialize-default
  :set (lambda (option value)
	 (set option value)
	 (hywiki-org-set-publish-project))
  :type 'string
  :group 'hyperbole-hywiki)

(defun hywiki--export-preparation-function (_project-plist)
  "Setup export hook functions."
  (message "Hywiki export being prepared...")
  (add-hook 'org-export-before-parsing-functions #'hywiki-org-export-function))

(defun hywiki--export-completion-function (_project-plist)
  "Remove export hook function."
  (remove-hook 'org-export-before-parsing-functions #'hywiki-org-export-function)
  (message "Hywiki export completed."))

(defvar hywiki-org-publish-project-alist nil
  "HyWiki-specific export properties added to `org-publish-project-alist'.")

(defun hywiki-org-make-publish-project-alist ()
  (setq org-export-with-broken-links hywiki-org-publishing-broken-links
	hywiki-org-publish-project-alist
	(list
	 "hywiki"
         :preparation-function 'hywiki--export-preparation-function
         :completion-function 'hywiki--export-completion-function
	 :auto-sitemap t
	 :base-directory (expand-file-name hywiki-directory)
	 :html-head (format
		     "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sman/hyperbole.css\"/>"
		     hyperb:dir)
	 :html-link-home "index.html"
	 ;; :html-link-up "theindex.html"
	 ;; !! TODO: The :makeindex property is disabled for now, until a process is
	 ;; developed to force the Org publish process to regenerate the
	 ;; index after index entries are inserted into the temporary Org
	 ;; buffer prior to export to HTML.
	 :html-postamble t
	 :html-postable-format '(("en" "<p class=\"author\">Author: %a (%e)</p>
                                  <p class=\"last-mod\">Last Modified: %C</p>
                                  <p class=\"creator\">%c</p>"))
	 :html-prefer-user-labels t
	 :makeindex nil
	 :publishing-directory hywiki-org-publishing-directory
	 :publishing-function hywiki-org-publishing-function
	 :section-numbers t
	 :shell "shell-command"
	 :sitemap-filename "index.org"
	 ;; sitemap (TOC) is stored in "sitemap.html"
	 :sitemap-title hywiki-org-publishing-sitemap-title
	 :with-date t
	 :with-toc nil)))

(defvar-local hywiki-page-flag nil
  "Set to t after finding a HyWiki page file, else nil.
The file must be below `hywiki-directory'.

For reference, this is set when `window-buffer-change-functions' calls
`hywiki-maybe-highlight-references' which calls `hywiki-in-page-p'.")

(defcustom hywiki-referent-prompt-flag nil
  "Non-nil means the Action Key and HyWiki/Create always prompt for referent type.
Nil by default."
  :type 'boolean
  :initialize #'custom-initialize-default
  :group 'hyperbole-hywiki)

(defvar hywiki-file-extension ".org"
  "File suffix string (including period) to use when creating HyWiki pages.")

(defconst hywiki-word-regexp
  (format "\\<\\([[:upper:]][[:alpha:]]+\\)\\>\\(?:%s\\)?"
          (regexp-quote hywiki-file-extension))
  "Regexp that matches a HyWikiWord only.
Do not use a start or end line/string anchor in this regexp.")

(defconst hywiki-word-section-regexp
  "\\(#[^][# \t\n\r\f]+\\)"
  "Regexp that matches a non-delimited HyWikiWord #section extension.
After the first # character, this may contain any non-square-bracket,
non-# and non-whitespace characters.")

(defconst hywiki-word-line-and-column-numbers-regexp
  (concat "\\(" hpath:line-and-column-numbers "\\)")
  "Group 2 is the 1-based line number.
Group 4 is the optional 0-based column number.")

(defconst hywiki-word-with-optional-suffix-regexp
  (concat hywiki-word-regexp hywiki-word-section-regexp "??"
	  hywiki-word-line-and-column-numbers-regexp "?")
  "Regexp for a HyWikiWord with an optional #section, :Lline-num, :Ccol-num.
Section may not contain whitespace or square brackets.  Use '-' to
substitute for spaces in the section/headline name.

Group 1 is the HyWikiWord.
Group 2 is any optional #section with the # included.
Group 4 is any optional 1-based line number to jump to for any
file-based referents (relative to any section given).
Group 6 is any optional 0-based column number to jump to for any
file-based referents.")

(defconst hywiki-word-with-optional-suffix-exact-regexp
  (concat "\\`" hywiki-word-regexp "\\(#[^][#\n\r\f]+\\)??"
	  hywiki-word-line-and-column-numbers-regexp "?\\'")
  "Exact match regexp for a HyWikiWord with an optional #section.
The section may contain spaces or tabs but not square brackets;
it is preferable, however, to substitute '-' for whitespace in
the section/headline name to simplify recognition.

Group 1 is the HyWikiWord.
Group 2 is any optional #section with the # included.
Group 4 is any optional 1-based line number to jump to for any
file-based referents (relative to any section given).
Group 6 is any optional 0-based column number to jump to for any
file-based referents.")

(defconst hywiki-word-suffix-regexp "\\(#\\|::\\|:L\\)\\(.+\\)\\'"
  "Regexp matching any trailing part of a HyWikiWord reference.
It may be a section or a line number reference.  Group one is the type
of reference and group two is the rest of the suffix reference.")

(defface hywiki--word-face
  '((((min-colors 88) (background dark)) (:foreground "orange" :underline t))
    (((background dark)) (:background "orange" :foreground "black" :underline t))
    (((min-colors 88)) (:foreground "orange" :underline t))
    (t (:background "orange" :underline t)))
  "Face for HyWikiWord highlighting."
  :group 'hyperbole-hywiki)

(defcustom hywiki-word-face 'hywiki--word-face
  "Hyperbole face for HyWikiWord highlighting."
  :initialize #'custom-initialize-default
  :type 'face
  :group 'hyperbole-hywiki)

(defvar hywiki-yank-reformat-function #'ignore
  "*A function of two arguments, START and END, invoked after a `hywiki-yank'.
It should reformat the region given by the arguments to some preferred style.
Default value is to perform no reformatting.")

(defcustom hywiki-display-page-function #'hpath:find
  "Hyperbole function to display HyWiki page pathnames.
Only argument is the page's pathname."
  :initialize #'custom-initialize-default
  :type 'string
  :group 'hyperbole-hywiki)

(defcustom hywiki-allow-plurals-flag t
  "Non-nil means plural HyWikiWords have the same referent as the singular form.
Non-nil is the default."
  :initialize #'custom-initialize-default
  :set (lambda (option value)
	 (set option value)
	 (setq hywiki--any-wikiword-regexp-list nil))
  :type 'boolean
  :group 'hyperbole-hywiki)

;;; ************************************************************************
;;; hywiki minor mode and text edit command hooks
;;; ************************************************************************

(defun hywiki-ignore-command-hooks-p ()
  "Prevent duplicate runs of hywiki command hooks within a single command."
  (and hywiki--command-executed-flag
       (hyperb:stack-frame '(hypb:eval))))

(defun hywiki-word-store-around-point ()
  "Store any HyWikiWord before or after point for post-command comparison.
Markers are stored into `hywiki--buttonize-start' and `hywiki--buttonize-end'.
HyWikiWords are stored only outside of `hywiki-non-hook-context-p' contexts.
This is triggered by `pre-command-hook' for non-character commands,
including deletion commands and those in `hywiki-non-character-commands'."
  (unless (hywiki-ignore-command-hooks-p)
    ;; (when ert--running-tests
    ;;  (message "Running pre-command-hook..."))
    (setq hywiki--buffer-modified-tick (buffer-modified-tick)
	  hywiki--word-pre-command nil)

    (unless (bound-and-true-p edebug-active)
      (set-marker hywiki--buttonize-start nil)
      (set-marker hywiki--buttonize-end nil)
      (setq hywiki--buttonize-range nil))

    (when (and (current-idle-time) (hywiki-non-hook-context-p) (hywiki-word-at))
      ;; Dehighlight any previously highlighted WikiWord at point if
      ;; it is outside of a valid context.
      (save-restriction
	(narrow-to-region (line-beginning-position) (line-end-position 2))
	(hywiki-maybe-dehighlight-reference)))

    (if (and (symbolp this-command)
	     (string-match-p "\\(^\\|-?\\)\\(insert\\|undo\\)\\(-\\|$\\)\\|eval-last-sexp\\|eval-expression\\|read--expression-try-read" (symbol-name this-command)))
	;; prior to an insertion command
	(progn
	  (setq hywiki--start (point)
		hywiki--end nil)
	  ;; Use these to store any range of a delimited HyWikiWord#section
	  (set-marker hywiki--buttonize-start nil)
	  (set-marker hywiki--buttonize-end nil)
	  hywiki--start)
      (unless (hywiki-non-hook-context-p)
	;; Record the WikiWord from any WikiWord ref that point is on
	(setq hywiki--word-pre-command (hywiki-get-singular-wikiword
					(hywiki-word-at)))
	(when (or (memq this-command hywiki-non-character-commands)
		  (and (symbolp this-command)
		       (string-match-p "^\\(org-\\)?\\(delete-\\|kill-\\)\\|\\(-delete\\|-kill\\)\\(-\\|$\\)" (symbol-name this-command))))
	  ;; Test if at delimiters surrounding a single WikiWord reference
	  ;; and if so, record those for use by post hooks.
	  (cl-destructuring-bind (start end)
	      ;; Get delimited region only if before or after delimiters,
	      ;; else return (nil nil).
	      (setq hywiki--buttonize-range
                    ;; includes delimiters
		    (or (cdr hywiki--range) (hywiki-at-range-delimiter)))
	    (setq hywiki--start (point))
	    ;; Use these to store any range of a delimited HyWikiWord#section
	    (set-marker hywiki--buttonize-start start)
	    (set-marker hywiki--buttonize-end end)
	    start))))
    (setq hywiki--command-executed-flag t)))

(defun hywiki-word-highlight-post-self-insert ()
  "Turn any HyWikiWords around point into highlighted Hyperbole buttons.
Triggered by `post-self-insert-hook' after self-inserting one or
more characters while the command is still executing.  The
`post-command-hook' runs later after the command has finished."
  ;; (when ert--running-tests
  ;;   (message "Running post-self-insert-hook..."))
  (unless (or (hywiki-ignore-command-hooks-p)
	      (hywiki-non-hook-context-p))
    (setq hywiki--range nil)

    ;; Dehighlight any previously highlighted WikiWord at point
    ;; before we move to the start of any current WikiWord and
    ;; rehighlight that.
    (hywiki--maybe-dehighlight-at-point))

  (save-excursion
    (cond ((marker-position hywiki--buttonize-start)
	   ;; Point was before or after a WikiWord delimiter
	   (goto-char hywiki--buttonize-start)
	   (skip-chars-backward "-" (line-beginning-position))
	   (goto-char (1- (point))))
	  ((not (equal (setq hywiki--range (hywiki-highlight-word-get-range))
		       '(nil nil nil)))
	   (cl-destructuring-bind (_ start end)
	       hywiki--range
	     (if (and start end)
		 (progn
		   ;; On a non-delimited HyWikiWord
		   (set-marker hywiki--buttonize-start start)
		   (set-marker hywiki--buttonize-end end)
		   (goto-char start)
		   (skip-chars-backward "-" (line-beginning-position))
		   t)
	       (setq hywiki--range nil))))
	  ((not (member (setq hywiki--range (hywiki-at-range-delimiter))
			'(nil nil)))
	   ;; At delimiters surrounding a WikiWord
	   (let ((start (nth 0 hywiki--range))
		 (end   (nth 1 hywiki--range)))
	     (when (and start end)
	       ;; Use these to store any range of a delimited HyWikiWord#section
	       (set-marker hywiki--buttonize-start (1+ start))
	       (set-marker hywiki--buttonize-end (1- end))))))

    (unless (hywiki-non-hook-context-p)
      ;; This first rehighlighting is needed to ensure
      ;; any wikiword before an inserted whitespace character is
      ;; properly highlighted when separating two words or after a
      ;; closing delimiter.
      (save-excursion
	(goto-char (max (1- (point)) (point-min)))
	(hywiki--maybe-rehighlight-at-point))

      (hywiki--maybe-rehighlight-at-point))))

(defun hywiki-word-highlight-post-command ()
  "Highlight any HyWikiWord before or after point as a Hyperbole button.
Triggered by `post-command-hook' for non-character-commands, including
deletion commands and those in `hywiki-non-character-commands'."
  ;; (when ert--running-tests
  ;;   (message "Running post-command-hook..."))
  (unless (or (eq hywiki--buffer-modified-tick (buffer-modified-tick))
	      (hywiki-ignore-command-hooks-p))
    (setq hywiki--range nil)
    (cond ((and (symbolp this-command)
		(string-match-p "\\(^\\|-?\\)\\(insert\\|undo\\)\\(-\\|$\\)\\|eval-last-sexp\\|eval-expression\\|read--expression-try-read"
				(symbol-name this-command)))
	   (setq hywiki--end (point))
	   (when (and hywiki--start (not (eq hywiki--start hywiki--end)))
	     ;; Something has been inserted
	     (cl-destructuring-bind (start end)
		 (hywiki--extend-region
		  (min hywiki--start hywiki--end)
		  (max hywiki--start hywiki--end))
	       (hywiki-maybe-dehighlight-references start end)
	       (hywiki-maybe-highlight-references start end))))
          ((and (symbolp this-command)
		(string-match-p "\\(^\\|-?\\)\\(capitalize\\|downcase\\|upcase\\)\\(-\\|$\\)"
				(symbol-name this-command)))
           (hywiki--maybe-rehighlight-at-point))
	  ((when (or (memq this-command hywiki-non-character-commands)
		     (and (symbolp this-command)
			  (string-match-p "^\\(org-\\)?\\(delete-\\|kill-\\)\\|\\(-delete\\|-kill\\|eval-last-sexp\\|eval-expression\\)\\(-\\|$\\)\\|^\\(hkey-either\\|action-key\\|assist-key\\)" (symbol-name this-command))))
	     (save-excursion
	       ;; Dehighlight any previously highlighted WikiWord at point
	       ;; before we move to the start of any current WikiWord and
	       ;; rehighlight that.
	       ;; Dehighlight if point is on or between a HyWikiWord
	       (save-restriction
		 (narrow-to-region (line-beginning-position) (line-end-position 2))
		 (hywiki-maybe-dehighlight-between-references))

	       ;; Record the WikiWord from any WikiWord ref that point is on
	       (unless hywiki--word-pre-command
		 (setq hywiki--word-pre-command (hywiki-get-singular-wikiword
						 (or (unless (hywiki-non-hook-context-p)
						       (hywiki-word-at))
						     (progn (goto-char (max (point-min)
									    (1- (point))))
							    (unless (hywiki-non-hook-context-p)
							      (hywiki-word-at)))))))

	       (cond ((marker-position hywiki--buttonize-start)
		      ;; Point was before or after a WikiWord delimiter
		      (goto-char (1+ hywiki--buttonize-start))
		      (unless (hywiki-non-hook-context-p)
			(set-marker hywiki--buttonize-start nil)
			(set-marker hywiki--buttonize-end nil)))
		     ((not (equal (setq hywiki--range
					(hywiki-highlight-word-get-range))
				  '(nil nil nil)))
		      (cl-destructuring-bind (_ start end)
			  hywiki--range
			(if (and start end)
			    (progn
			      ;; On a non-delimited HyWikiWord
			      (set-marker hywiki--buttonize-start start)
			      (set-marker hywiki--buttonize-end end)
			      (goto-char start)
			      (skip-chars-backward "-" (line-beginning-position))
			      t)
			  (setq hywiki--range nil)))))

	       (unless (hywiki-non-hook-context-p)
		 ;; This first rehighlighting is needed to ensure
		 ;; any wikiword before an inserted whitespace character is
		 ;; properly highlighted when separating two words or after a
		 ;; closing delimiter.
		 (save-excursion
		   (goto-char (max (1- (point)) (point-min)))
		   (hywiki--maybe-rehighlight-at-point))

		 (hywiki--maybe-rehighlight-at-point))))))
    (setq hywiki--command-executed-flag nil)))

(defun hywiki-get-buttonize-characters ()
  "Return a string of Org self-insert keys that have punctuation/symbol syntax.
These trigger HyWiki reference highlighting.  Cache the string and
automatically invalidate it when `global-map' or `outline-mode-syntax-table'
changes."
  (if (stringp hywiki--buttonize-characters-cache)
      hywiki--buttonize-characters-cache
    (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))
      (with-syntax-table hywiki--org-mode-syntax-table
        (setq hywiki--buttonize-characters-cache
              (dolist (key-cmd key-cmds (apply #'string (seq-difference (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 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
		         (dolist (k (number-sequence (car key) (cdr key)))
		           (when (memq (char-syntax k) '(?. ?_))
		             (setq result (cons k result)))))))))))))

(defun hywiki-non-hook-context-p ()
  "Return non-nil when HyWiki command hooks should do nothing.
When used within a `post-command-hook', point must be moved back to
its location prior to the associated command run before this is called
since the command may have moved it off a HyWikiWord."
  (or (minibuffer-window-active-p (selected-window))
      ;; Can't be on an Emacs read-only pushbutton
      (button-at (point))
      ;; (and (bound-and-true-p edebug-active)
      ;;   (active-minibuffer-window))
      (and (derived-mode-p 'prog-mode)
	   (not (apply #'derived-mode-p hywiki-highlight-all-in-prog-modes))
	   ;; Not inside a comment or a string
	   (not (or (nth 4 (ignore-errors (syntax-ppss))) (hypb:in-string-p))))))

(defcustom hywiki-default-mode :pages
  "Customizable initial mode setting for HyWiki minor mode.
When Hyperbole minor mode is first activated, e.g. by pressing {C-h h},
HyWiki minor mode is initialized to the state specified by this variable.
See the documentation for the function, `hywiki-mode', for a summary of
valid state values."
  :type 'string
  :group 'hyperbole-hywiki)

(defvar hywiki-mode nil
  "Non-nil when the global hywiki minor mode is enabled.
Don't set this directly, instead call the function `hywiki-mode'
with the value you want as its argument.  See the documentation for the
customization, `hywiki-default-mode', for valid values.")

(defun hywiki-mode-normalize (to-mode)
  "Normalize `hywiki-mode' and TO-MODE values for `hywiki-mode' function.
See the documentation for the customization, `hywiki-default-mode', for
valid values."
  ;; Normalize `hywiki-default-mode' setting
  (cond
   ((or (and (integerp hywiki-default-mode) (= hywiki-default-mode 1))
	(memq hywiki-default-mode '(:all all t)))
    (setq hywiki-default-mode :all))
   ((or (null hywiki-default-mode)
        (and (integerp hywiki-default-mode) (<= hywiki-default-mode 0)))
    (setq hywiki-default-mode nil))
   (t ;; (> hywiki-default-mode 1)
    (setq hywiki-default-mode :pages)))

  ;; Normalize `hywiki-mode' setting
  (cond
   ((or (and (integerp hywiki-mode) (= hywiki-mode 1))
	(memq hywiki-mode '(:all all t)))
    ;; Enable across all editable buffers
    (setq hywiki-mode :all))
   ((or (null hywiki-mode)
        (and (integerp hywiki-mode) (<= hywiki-mode 0)))
    ;; Disable mode flag
    (setq hywiki-mode nil))
   (t ;; (> hywiki-mode 1)
    ;; Enable in HyWiki page buffers only
    (setq hywiki-mode :pages)))

  ;; Normalize `to-mode' and set mode
  (when (eq to-mode 'toggle)
    ;; Toggle across all editable buffers
    (setq to-mode (if hywiki-mode
                      nil
                    (or hywiki--prior-mode hywiki-default-mode :pages))))

  (cond
   ((or (and (integerp to-mode) (= to-mode 1))
	(memq to-mode '(:all all t)))
    ;; Enable across all editable buffers
    (setq to-mode :all))
   ((or (null to-mode)
        (and (integerp to-mode) (<= to-mode 0)))
    ;; Disable across all editable buffers
    (setq to-mode nil))
   (t ;; (> to-mode 1)
    ;; Enable in HyWiki page buffers only
    (setq to-mode :pages))))

;;;###autoload
(define-minor-mode hywiki-mode
  "Toggle HyWiki global minor mode with \\[hywiki-mode].
HyWiki minor mode automatically highlights and turns HyWikiWord references
into Hyperbole implicit buttons that either link to HyWiki pages or activate
typed referents such as bookmarks.

HyWiki minor mode has three states as tracked by the `hywiki-mode' variable,
with the default state when interactively enabled set by the value of
`hywiki-default-mode':

  - :pages - highlight HyWikiWords in HyWiki pages only (Org files in
             `hywiki-directory'); also enable `hyperbole-mode' minor mode
             if off.

  - :all   - highlight HyWikiWords in all editable buffers except those
             with a major mode in `hypb:exclude-major-modes'; also
             enable `hyperbole-mode' minor mode if off.

  - nil    - no highlighting, the `hywiki-mode' is disabled.

HyWikiWord references may also include optional suffixes:

  - a #section reference that links to a HyWiki page Org headline or
    other outline file.  Spaces in the headline must be converted
    to dash characters for proper recognition;

  - optionally followed by :L<line-number>:C<column-number>
    where the column part is also optional.  If a section is
    given, the line number is relative to the section and the
    section headline is line 1.

See the Info documentation at \"(hyperbole)HyWiki\".

\\{hywiki-mode-map}"
  :global t
  :lighter hywiki-mode-lighter
  :keymap hywiki-mode-map
  :group 'hyperbole-hywiki
  ;; Prevent definition of a custom-variable since it makes no sense to
  ;; customize this variable.
  :variable hywiki-mode
  (progn
    ;; Set mode and highlighting
    (pcase arg
     (:all (progn
             ;; Enable across all editable buffers
             ;; Need hyperbole-mode
             (unless hyperbole-mode
	       (hyperbole-mode 1))
             (hywiki-word-set-auto-highlighting hywiki--prior-mode arg)
             (setq hywiki-mode arg)))
     ('nil (progn
            ;; Disable across all editable buffers.
            ;; Dehighlight HyWikiWords in this buffer when 'hywiki-mode' is
            ;; disabled and this is not a HyWiki page buffer. If this is a
            ;; HyWiki page buffer, then dehighlight when `hywiki-mode' is nil.
            (hywiki-word-set-auto-highlighting hywiki--prior-mode arg)
            (setq hywiki-mode arg)))
     (:pages (progn
               ;; Enable in HyWiki page buffers only
               ;; Need hyperbole-mode
               (unless hyperbole-mode
	         (hyperbole-mode 1))
               (hywiki-word-set-auto-highlighting hywiki--prior-mode arg)
               (setq hywiki-mode arg))))))

(defun hywiki-mode-around-advice (hywiki-mode-fn &optional to-mode)
  (setq to-mode (hywiki-mode-normalize to-mode))
  (unless (eq hywiki-mode to-mode)
    (setq hywiki--prior-mode hywiki-mode))
  (funcall hywiki-mode-fn to-mode))

(unless (advice-member-p #'hywiki-mode-around-advice #'hywiki-mode)
  (advice-add 'hywiki-mode :around #'hywiki-mode-around-advice))

;;; ************************************************************************
;;; Public Implicit Button and Action Types
;;; ************************************************************************

(defun hywiki-display-referent-type (wikiword referent)
  "Display WIKIWORD REFERENT, a cons of (<referent-type> . <referent-value>).
Function used to display is \"hywiki-display-<referent-type>\"."
  (let ((referent-type (and (consp referent) (car referent))))
    (unless (and referent-type (symbolp referent-type))
      (error "(hywiki-display-referent-type): Referent type must be a symbol, not: referent-type = %S; referent = %S"
             referent referent-type))
    (let* ((referent-value (cdr referent))
           (display-function (intern-soft (concat "hywiki-display-"
					          (symbol-name referent-type)))))
      (when (equal (hywiki-get-singular-wikiword wikiword) (hywiki-word-at-point))
        ;; Set referent attributes of current implicit button
        (hattr:set 'hbut:current 'referent-type referent-type)
        (hattr:set 'hbut:current 'referent-value referent-value))
      (cond ((fboundp display-function)
	     (funcall display-function wikiword referent-value))
	    (t
	     (error "(hywiki-display-referent-type): No hywiki-display function for referent type '%s'" referent-type))))))

(defun hywiki-display-referent (&optional wikiword prompt-flag)
  "Display HyWiki WIKIWORD referent or a regular file with WIKIWORD nil.
Return the WIKIWORD's referent if successfully found or nil otherwise.

For further details, see documentation for `hywiki-find-referent'.
After successfully finding a referent, run `hywiki-display-referent-hook'."
  (interactive (list (hywiki-word-read)))
  (let ((in-page-flag (null wikiword))
	(in-hywiki-directory-flag (hywiki-in-page-p)))
    (if (or (stringp wikiword) in-hywiki-directory-flag)
	(progn
	  (when in-page-flag
	    ;; Current buffer must be the desired page
	    (unless in-hywiki-directory-flag
	      (error "(hywiki-display-referent): No `wikiword' given; buffer file must be in `hywiki-directory', not %s"
		     default-directory))
	    (unless (hypb:buffer-file-name)
	      (error "(hywiki-display-referent): No `wikiword' given; buffer must have an attached file"))
	    (setq wikiword (file-name-sans-extension (file-name-nondirectory (hypb:buffer-file-name)))))
	  (let* ((_suffix (when (string-match hywiki-word-suffix-regexp wikiword)
			    (substring wikiword (match-beginning 0))))
		 (referent (cond (prompt-flag
				  (hywiki-create-referent wikiword))
				 ((hywiki-get-referent wikiword))
				 (t (hywiki-add-page wikiword)))))
	    (if (not referent)
		(error "(hywiki-display-referent): Invalid `%s' referent: %s"
		       wikiword referent)
 	      ;; Ensure highlight any page name at point in case called as a
	      ;; Hyperbole action type
	      (hywiki-maybe-highlight-reference t)
	      (hywiki-display-referent-type wikiword referent)
	      (hywiki-maybe-highlight-references)
	      (run-hooks 'hywiki-display-referent-hook)
	      referent)))
      ;; When called without a wikiword and outside hywiki-directory,
      ;; just find as a regular file and use next line to highlight
      ;; HyWikiWords only if buffer was not previously highlighted.
      (hywiki-maybe-highlight-references)
      nil)))

(defun hywiki-help ()
  "Display help either for a HyWikiWord at point or HyWikiWords in general."
  (interactive)
  (if (hkey-actions)
      (hkey-help)
    (with-help-window "*Help: HyWikiWords*"
      (princ (documentation (symtable:ibtype-p "hywiki-existing-word"))))))

;;; ************************************************************************
;;; Public referent menus and utility functions
;;; ************************************************************************

(unless hywiki-referent-menu
  (makunbound 'hywiki-referent-menu))
(defcustom hywiki-referent-menu
  (delq nil
	(list
	 '("HyWiki RefType>")
         (when (fboundp #'activities-new)
	   '("Activity"   (hywiki-add-activity hkey-value)
	     "Add a HyWikiWord that activates a saved activity from the Activities package."))
	 '("Bookmark"     (hywiki-add-bookmark hkey-value)
	   "Add a HyWikiWord that jumps to an Emacs bookmark.")
	 '("Command"      (hywiki-add-command hkey-value)
	   "Add a HyWikiWord that runs an Emacs command or Hyperbole action type.")
         (when (fboundp #'denote-open-or-create)
           '("Denote"       (hywiki-add-denote hkey-value)
	     "Add a HyWikiWord that jumps to a Denote note stored by ID."))
	 '("Elisp"        (hywiki-add-elisp hkey-value)
	   "Add a HyWikiWord that evaluates an Elisp sexpression.")
	 '("Find"         (hywiki-add-find hkey-value)
	   "Add a HyWikiWord that greps through `hywiki-directory' for its matches.")
	 ;; "<(global explicit button name)>"
	 ;; "<[global implicit button name]>"
	 '("Gbut"         (hywiki-add-global-button hkey-value)
	   "Add a HyWikiWord that activates a named Hyperbole global button.")
	 '("HyRolo"       (hywiki-add-hyrolo hkey-value)
	   "Add a HyWikiWord that searches `hyrolo-file-list' for matches.")
	 ;; "(hyperbole)action implicit button"
	 '("InfoIndex"    (hywiki-add-info-index hkey-value)
	   "Add a HyWikiWord that displays an Info index item.")
	 ;; "{key series}" wikiword
	 '("Keys"         (hywiki-add-key-series hkey-value)
	   "Add a HyWikiWord that executes a key series.")
         ;; "path"
	 '("pathLink"     (hywiki-add-path-link hkey-value)
	   "Add a HyWikiWord that links to a path and possible position.")
	 ;; "(hyperbole)Smart Keys"
	 '("infoNode"     (hywiki-add-info-node hkey-value)
	   "Add a HyWikiWord that displays an Info node.")
	 ;; [[id:org-id"][Org Heading Title]
	 '("OrgID"        (hywiki-add-org-id hkey-value)
	   "Add an Org link that displays an Org section given its Org ID.")
	 ;; "pathname:line:col"
	 ;; "#in-buffer-section"
	 '("Page"         (hywiki-add-page hkey-value)
	   "Add/Reset a HyWikiWord to link to its standard HyWiki page.")
	 ;; e.g. (kbd "key sequence")
	 '("orgRoamNode"  (hywiki-add-org-roam-node hkey-value)
	   "Add a HyWikiWord that displays an Org Roam node given its title.")
	 '("Spec"         (hywiki-add-spec hkey-value)
	   "Highlight HyWikiWord; defer referent creation until activation.")
	 '("glossarY"     (hywiki-add-glossary-entry hkey-value)
           "Add a new HyWiki glossary entry.")))
  "Menu of HyWikiWord custom referent types of the form:
\(LABEL-STRING ACTION-SEXP DOC-STR)."
  :set  (lambda (var value) (set-default var value))
  :type '(cons (list string) (repeat (list string sexp string)))
  :group 'hyperbole-buttons)

(defun hywiki-add-referent (wikiword referent)
  "Add WIKIWORD (sans any suffix) that displays REFERENT to HyWiki.
Return REFERENT if WIKIWORD is of valid format, otherwise return nil.
REFERENT must be a cons of (<referent-type> . <referent-value>) or
an error is triggered."
  (hywiki-validate-referent referent)
  (when (hywiki-word-is-p wikiword)
    ;; Remove any #section suffix in PAGE-NAME.
    (setq wikiword (hywiki-word-strip-suffix wikiword))
    (unless (hash-add referent (hywiki-get-singular-wikiword wikiword)
		      (hywiki-get-referent-hasht))
      (error "(hywiki-add-referent): Failed: (hash-add %s %s %s)"
	     referent (hywiki-get-singular-wikiword wikiword)
		      (hywiki-get-referent-hasht)))
    (setq hywiki--any-wikiword-regexp-list nil)
    (unless (hyperb:stack-frame '(hywiki-maybe-highlight-wikiwords-in-frame))
      (hywiki-cache-save)
      (hywiki-maybe-highlight-wikiwords-in-frame t))
    (run-hooks 'hywiki-add-referent-hook)
    referent))

(defun hywiki-create-referent (wikiword &optional message-flag)
  "Prompt for, add to HyWiki lookups and return a WIKIWORD custom referent.
With optional prefix arg MESSAGE-FLAG non-nil or when called interactively,
display a minibuffer message with the referent."
  (interactive (list nil current-prefix-arg))
  (unless (stringp wikiword)
    (setq wikiword (hywiki-word-read-new "Create/Edit HyWikiWord: ")))
  (setq hkey-value wikiword)
  ;; Prompt for referent type and then create the referent
  (hui:menu-act 'hywiki-referent-menu
		(list (cons 'hywiki-referent-menu
			    (cons (list (format "%s RefType>"
						(if (string-match hywiki-word-suffix-regexp wikiword)
						    (substring wikiword 0 (match-beginning 0))
						  wikiword)))
				  (cdr hywiki-referent-menu)))))
  ;; Ensure the referent returned has any wikiword suffix
  (let ((referent (hywiki-get-referent wikiword)))
    (if referent
	(when (or message-flag (called-interactively-p 'interactive))
	  (message "HyWikiWord '%s' referent: %S" wikiword referent))
      (user-error "(hywiki-create-referent): Referent creation failed: '%s'; ensure HyWikiWord is capitalized and all alpha" wikiword))
    referent))

;;; ************************************************************************
;;; Public functions
;;; ************************************************************************

(defact link-to-wikiword (reference)
  "Display the HyWikiword referent matching WikiWord#section REFERENCE."
  (interactive (list (hywiki-word-read "Link to HyWiki word: ")))
  (hywiki-find-referent reference))

(defun hywiki-active-in-current-buffer-p ()
  "Return non-nil if HyWikiWord links are active in the current buffer.
Exclude the minibuffer if selected and return nil."
  (if (eq hywiki-mode :pages)
      (hywiki-in-page-p)
    (and hywiki-mode (hywiki-potential-buffer-p))))

(defun hywiki-potential-buffer-p ()
  "Return non-nil if the current buffer can support HyWikiWords.
Always exclude minibuffers.
This does not mean `hywiki-mode' is presently active in that buffer;
use `hywiki-active-in-current-buffer-p' for that."
  (and (not (minibufferp))
       ;; (not (and (boundp 'edebug-active) edebug-active))
       (or (null hypb:include-major-modes) ;; means all modes allowed
           (apply #'derived-mode-p hypb:include-major-modes)
           (and (not (eq (get major-mode 'mode-class) 'special))
                (not (apply #'derived-mode-p hypb:exclude-major-modes))))))

(defun hywiki-add-activity (wikiword)
  "Make WIKIWORD resume a prompted for, existing activity.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the activity, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (hypb:require-package 'activities)
  (let ((activity (activities-completing-read :prompt "Resume activity" :default nil)))
    (hywiki-add-referent wikiword (cons 'activity activity))))

(defun hywiki-display-activity (_wikiword activity)
  (activities-resume activity :resetp nil))

(defun hywiki-add-bookmark (wikiword)
  "Make WIKIWORD display a bookmark at point and return the action.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the bookmark, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit Bookmark HyWikiWord: "))))
  (require 'bookmark)
  (if (string-empty-p wikiword)
      (error "(hywiki-add-bookmark): No bookmark specified")
    (bookmark-set wikiword)
    (hywiki-add-referent wikiword (cons 'bookmark wikiword))))

(defun hywiki-display-bookmark (_wikiword bookmark)
  (let ((loc (bookmark-location bookmark)))
    ;; Use Hyperbole-specified display location
    (cond ((bufferp loc)
	   (hpath:display-buffer loc))
	  ((get-buffer loc)
	   (hpath:display-buffer (get-buffer loc)))
	  ((stringp loc)
	   (hywiki-display-page loc)))
    (bookmark-jump bookmark)))

(defun hywiki-add-command (wikiword)
  "Make WIKIWORD invoke a prompted for command with arguments and return it.
Interactively, use any existing HyWikiWord at point or read an existing or
valid new one.

Command is the symbol used in the definition expression, which may be an
Emacs command or a Hyperbole action type.  If it takes no more than one
string argument and that argument is the empty string or matches WIKIWORD
sans any #suffix, then WIKIWORD (including any suffix) is sent as the first
argument.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the actype, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (when (string-empty-p wikiword)
    (error "(hywiki-add-command): No HyWikiWord specified"))

  (let* ((command (hui:actype nil (format "Command for %s: " wikiword)))
	 (args (hargs:actype-get command))
         (first-arg (car args)))
    (hywiki-add-referent wikiword
                         (if (and (= (length args) 1)
                                  (stringp first-arg)
                                  (or (string-empty-p first-arg)
                                      (equal (hywiki-word-strip-suffix first-arg)
                                             (hywiki-word-strip-suffix wikiword))))
                             (cons 'command command)
                           (cons 'command (cons command args))))))

(defun hywiki-display-command (wikiword command)
  (cond ((consp command)
         ;; arbitrary list of arguments
         (apply 'actype:act command))
        ((symbolp command)
         ;; single arg of wikiword is sent to command
         (actype:act command wikiword))
        (t (error "(hywiki-display-command): Unbound referent command, '%s'"
                  command))))

(defun hywiki-add-denote (wikiword)
  "Make WIKIWORD display a denote file when the `denote' package is available.
When called interactively or with WIKIWORD nil or the empty string, then set
WIKIWORD to any wikiword at or immediately before point; otherwise, convert
the description from the denote file chosen to be the WIKIWORD and insert
that after any non-whitespace text at point.

After successfully adding the link to a denote file, run
`hywiki-add-referent-hook'.

Return the WIKIWORD referent if WIKIWORD is of valid format, otherwise
return nil.  The referent is a cons of (denote-description . denote-id).

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list nil))
  (hypb:require-package 'denote)
  (let ((at-wikiword-reference (hywiki-word-at)))
    (unless (and (stringp wikiword) (not (string-empty-p wikiword)))
      (setq wikiword at-wikiword-reference))
    (let* ((denote-file (denote-file-prompt
                         nil
                         (if (stringp wikiword)
                             (format "Link `%s' HyWikiWord to denote" wikiword)
                           ;; Will use denote file description as `wikiword'
                           "Add HyWikiWord denote link to")
                         nil t))
           (denote-desc (denote-get-link-description denote-file))
           (denote-id (denote-retrieve-filename-identifier denote-file)))
      (unless (and (stringp wikiword) (not (string-empty-p wikiword)))
        (setq wikiword (hywiki-string-to-wikiword denote-desc)))
      (unless (hyperb:stack-frame '(hywiki-create-referent-and-display))
        (unless (equal (hywiki-get-singular-wikiword wikiword)
                       (hywiki-get-singular-wikiword at-wikiword-reference))
          (if buffer-read-only
              (error "(hywiki-add-denote): Read-only buffer; call this with point on: \"%s\"" wikiword)
            (skip-syntax-forward "^-")
            (unless (or (bolp) (= (char-syntax (preceding-char)) ?\ ))
              (insert " "))
            (insert wikiword))))
      (hywiki-add-referent wikiword (cons 'denote (cons denote-desc denote-id))))))

(defun hywiki-display-denote (_wikiword denote-desc-and-id)
  (let ((denote-desc (car denote-desc-and-id))
        (denote-id (cdr denote-desc-and-id))
        denote-file)
    (cond ((not (stringp denote-id))
           (error "(hywiki-display-denote): `denote-id' must be a string, not`%s'"
                  denote-id))
          ((and (setq denote-file (denote-get-path-by-id denote-id))
                (file-readable-p denote-file))
           (hpath:find denote-file))
          ((not (stringp denote-file))
           (error "(hywiki-display-denote): denote file not found for desc: \"%s\", id: \"%s\""
                  denote-desc denote-id))
          (t ;; denote-file not readable
           (error "(hywiki-display-denote): Unreadable demote file: \"%s\""
                  denote-file)))))

(defun hywiki-add-elisp (wikiword)
  "Make WIKIWORD evaluate a prompted for Elisp sexpression and return it.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the sexpression, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (hywiki-add-referent wikiword (cons 'elisp
				      (read--expression "Elisp Sexpr: "))))

(defun hywiki-display-elisp (_wikiword elisp)
  (eval elisp))

(defun hywiki-add-find (wikiword)
  "Make WIKIWORD grep across `hywiki-directory' for matches to itself.
Return the command to invoke.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the grep, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (hywiki-add-referent wikiword (cons 'find #'hywiki-word-grep)))

(defun hywiki-display-find (wikiword func)
  (if (fboundp func)
      (actype:act func wikiword)
    (error "(hywiki-display-find): Unbound referent function, '%s'" func)))

(defun hywiki-add-global-button (wikiword)
  "Make WIKIWORD evaluate a prompted for global button.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the button link, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (let ((gbut-name (hargs:read-match "Global button: "
				     (mapcar #'list (gbut:label-list))
				     nil t nil 'gbut)))
    (hywiki-add-referent wikiword (cons 'global-button gbut-name))))

(defun hywiki-display-global-button (_wikiword gbut-name)
  (gbut:act gbut-name))

(defun hywiki-add-glossary-entry (term)
  "Add/edit TERM as a HyWiki glossary entry with a definition."
  (let ((glossary (hywiki-get-glossary-file))
        (hyrolo-date-format "")) ;; don't add a date
    (if (= 1 (hyrolo-grep (hywiki-get-term-variant-regexp term)
                          1 glossary nil t t))
        ;; Edit the existing entry; ensures use the exact term variant
        ;; matched
        (progn (hyrolo-set-buffer hyrolo-display-buffer)
               (goto-char (point-min))
               ;; Skip past file header
               (hyrolo-to-next-entry)
               (hyrolo-edit-entry))
      ;; add a new definition entry using the original term, not any
      ;; wikiword transformed version
      (hyrolo-add term glossary)))
  ;; Connect wikiword version of term to the glossary definition
  (hywiki-add-referent (hywiki-string-to-wikiword term)
                       (cons 'glossary-entry term)))

(defun hywiki-display-glossary-entry (_wikiword term)
  "Display any HyWiki glossary definition for TERM."
  ;; Don't display the entry if in the middle of a
  ;; `hywiki-create-referent-and-display' call since that displays the entry
  ;; source buffer already.
  (unless (hyperb:stack-frame '(hywiki-create-referent-and-display))
    (hywiki-get-definition term t)))

(defun hywiki-add-hyrolo (wikiword)
  "Make WIKIWORD search and display `hyrolo-file-list' matches.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the hyrolo search, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (require 'hyrolo)
  ;; !! TODO: Change PaulAllenWinter to search for "Winter, Paul Allen".
  (hywiki-add-referent wikiword (cons 'hyrolo #'hyrolo-fgrep)))

(defun hywiki-display-hyrolo (wikiword search-func)
  (funcall search-func wikiword))

(defun hywiki-add-info-index (wikiword)
  "Make WIKIWORD display an Info manual index item and return it.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the Info index item, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (let ((item (save-window-excursion
		(info)
		(Info-read-index-item-name "Info index item: "))))
    (when (stringp item)
      (unless (= (aref item 0) ?\()
	(setq item (format "(%s)%s" (Info-current-filename-sans-extension) item)))
      (hywiki-add-referent wikiword (cons 'info-index item)))))

(defun hywiki-display-info-index (_wikiword item-name)
  (hact 'link-to-Info-index-item item-name))

(defun hywiki-add-info-node (wikiword)
  "Make WIKIWORD display an Info manual node and return it.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the Info node, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (let ((node (save-window-excursion
		(info)
		(Info-read-node-name "Info node: "))))
    (when (stringp node)
      (unless (= (aref node 0) ?\()
	(setq node (format "(%s)%s" (Info-current-filename-sans-extension) node)))
      (hywiki-add-referent wikiword (cons 'info-node node)))))

(defun hywiki-display-info-node (_wikiword node)
  (hact 'link-to-Info-node node))

(defun hywiki-add-key-series (wikiword)
  "Make WIKIWORD invoke a prompted for key series and return it.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the key series, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (let ((key-series (read-string "Key series (with or without {}): ")))
    (unless (string-match-p "\\`{.+}\\'" key-series)
      (setq key-series (concat "{" (string-trim key-series) "}")))
    (hywiki-add-referent wikiword (cons 'key-series key-series))))

(defun hywiki-display-key-series (_wikiword key-series)
  (hact 'kbd-key key-series))

(defun hywiki-add-org-id (wikiword)
  "Make WIKIWORD display an Org file or headline with an Org id.
Point must be within the entry with with the id.  If no id exists, it is
created.  Return the referent created with the form: \\='(org-id
. <id-string>).

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the Org id, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (unless (hsys-org-mode-p)
    (user-error "(hywiki-add-org-id): Referent buffer <%s> must be in org-mode, not %s"
		(buffer-name)
		major-mode))
  (let ((org-id (with-suppressed-warnings ((callargs org-id-get))
                  (if (>= (action:param-count #'org-id-get) 4)
		      (org-id-get nil nil nil t)
		    (org-id-get)))))
    (when (and (null org-id) buffer-read-only)
      (user-error "(hywiki-add-org-id): Referent buffer <%s> point has no Org ID and buffer is read-only"
		  (buffer-name)))
    (unless org-id
      (setq org-id (org-id-get-create)))
    (hywiki-add-referent wikiword (cons 'org-id org-id))))

(defun hywiki-display-org-id (_wikiword org-id)
  (hact 'link-to-org-id org-id))

(defun hywiki-add-org-roam-node (wikiword)
  "Make WIKIWORD display an Org Roam Node and return the action.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the action, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (hypb:require-package 'org-roam)
  (let ((node-title (org-roam-node-title (org-roam-node-read))))
    (hywiki-add-referent wikiword (cons 'org-roam-node node-title))))

(defun hywiki-display-org-roam-node (_wikiword referent)
  (hypb:require-package 'org-roam)
  (org-roam-node-open (if (stringp (cdr referent))
			  (org-roam-node-from-title-or-alias (cdr referent))
			;; Older links were Org Roam nodes rather than titles
			(cdr referent))
		      (or (alist-get 'file org-link-frame-setup)
			  (alist-get hpath:display-where hpath:display-where-alist))))

(defun hywiki-add-page (page-name &optional force-flag debug-flag)
  "Add a new or return any existing HyWiki page path for PAGE-NAME.
Returned format is: \\='(page . \"<absolute-page-file-path>\") or nil when
none.  <absolute-page-file-path> follows the path formatting conventions
from the documentation string for `hpath:find'.  PAGE-NAME must be the
HyWikiWord reference that can link to the page (no pathname directory nor
file extension).

With optional FORCE-FLAG prefix arg non-nil, force an update to
the page's modification time.  If PAGE-NAME is invalid, trigger a
`user-error' if called interactively or return nil if not.

By default, create any non-existent page.  When not in batch or
ert test results mode, if this is the first HyWiki page in
`hywiki-directory', prompt to create it.

After successfully adding a page, run `hywiki-add-page-hook'.

Use `hywiki-get-referent' to determine whether a HyWiki page exists."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-page-read-new "Add/Edit HyWiki page: "))
		     current-prefix-arg))
  (if (hywiki-word-is-p page-name)
      (when (or noninteractive
		(hyperb:stack-frame '(ert-run-test))
		(not (hash-empty-p (hywiki-get-referent-hasht)))
		(y-or-n-p (concat "Create new HyWiki page `" page-name "'? ")))
	(let* ((case-fold-search nil)
               (page-name-with-suffix page-name)
	       ;; Remove any #section suffix in PAGE-NAME.
	       (page-name-singular (hywiki-get-singular-wikiword page-name))
               (page-file (hywiki-get-page-file page-name-singular))
	       (page-file-readable (file-readable-p page-file))
	       (referent-hasht (hywiki-get-referent-hasht))
	       (page-in-hasht (hywiki-page-exists-p page-name-singular))
               referent)
	  (unless page-file-readable
	    (if (file-writable-p page-file)
		(write-region "" nil page-file nil 0)
	      (user-error "(hywiki-add-page): No permission to write HyWikiWord page file:\n  \"%s\"" page-name-singular)))
	  (if (or force-flag (not page-in-hasht))
	      (progn
		(hash-add (cons 'page (file-name-nondirectory page-file))
			  page-name-singular referent-hasht)
		(setq hywiki--any-wikiword-regexp-list nil)
		(when (called-interactively-p 'interactive)
		  (message "Added HyWikiWord page: \"%s\"" page-file)))
	    (when (called-interactively-p 'interactive)
	      (message "HyWikiWord page exists: \"%s\"" page-file)))
	  (unless (or (hyperb:stack-frame '(hywiki-maybe-highlight-wikiwords-in-frame))
		      (and (not force-flag) page-file-readable page-in-hasht))
	    (hywiki-cache-save)
	    (hywiki-maybe-highlight-wikiwords-in-frame t))
	  (run-hooks 'hywiki-add-page-hook)
	  (when page-file
            ;; Ensure the referent returned has any wikiword suffix and the
            ;; path part of the referent is absolute
            (setq referent (hywiki-get-referent page-name-with-suffix t))
            (or referent (when debug-flag (debug))))))
    (when (called-interactively-p 'interactive)
      (user-error "(hywiki-add-page): Invalid HyWikiWord: '%s'; must be capitalized, all alpha" page-name))))

(defun hywiki-create-page (wikiword &optional message-flag)
  "Prompt for, add to HyWiki lookups and return a WIKIWORD page.
With optional prefix arg MESSAGE-FLAG non-nil, display a minibuffer message
with the page."
  (interactive (list nil current-prefix-arg))
  (unless (stringp wikiword)
    (setq wikiword (hywiki-word-read-new "Create/Edit HyWikiWord: ")))
  (setq hkey-value wikiword)
  (let ((page-file (cdr (hywiki-add-page wikiword t))))
    (if (or message-flag (called-interactively-p 'interactive))
	(if page-file
	    (message "HyWikiWord '%s' page: \"%s\"" wikiword page-file)
          (user-error "(hywiki-create-page): Invalid HyWikiWord: '%s'; must be capitalized, all alpha" wikiword)))
    page-file))

;;;###autoload
(defun hywiki-add-spec (wikiword)
  "Create a WIKIWORD spec whose action selects and displays its referent type.
The specified WIKIWORD is highlighted.  This replaces any existing referent
the WIKIWORD may have.  Use `hywiki-get-referent' to determine any referent
type associated with WIKIWORD prior to invoking this.

This differs from `hywiki-word-create-and-display' in that selection and
display of the WIKIWORD referent is deferred until the first Action Key
activation of the WIKIWORD.

When the WIKIWORD spec is activated, prompt for and choose a typed referent.
See `hywiki-referent-menu' for valid referent types.  Select Page for a
standard HyWiki page.

If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

After successfully adding the spec, run `hywiki-add-referent-hook'."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add HyWikiWord Spec: "))))
  (when (or (not (stringp wikiword)) (string-empty-p wikiword))
    (error "(hywiki-word-create-spec): No HyWikiWord specified"))
  (setq hkey-value wikiword)
  (hywiki-add-referent wikiword '(spec . t)))

;; Need unused optional second arg here because `hywiki-display-referent-type'
;; calls this with the spec value (which is always t) to conform to other
;; referent types.
(defun hywiki-at-range-delimiter ()
  "Immediately before or after a balanced delimiter, return the delimited range.
Include: (), {}, <>, [] and \"\" (double quotes).  Exclude Org links
and radio targets.

Range is a list of (start end) positions or if no such range, then \\='(nil
nil).  It is limited to the previous, current and next lines, as HyWikiWord
references are limited to two lines maximum.  The range is inclusive of the
delimiters: (), {}, <>, [] and \"\" (double quotes)."
  (save-excursion
    (save-restriction
      ;; Limit balanced pair checks to previous through next lines for
      ;; speed when no region is active.  Point must be either on the
      ;; opening or the closing line to recognize any delimiters.
      (unless (use-region-p)
	(narrow-to-region (line-beginning-position 0) (line-end-position 2)))
      (let* ((result (hywiki-get-delimited-region))
	     (start (nth 0 result))
	     (end (nth 1 result))
	     (delimited-flag (and (integerp start) (integerp end))))
	;; If there is an active region, then point can be before the
	;; start of the delimited region, within it or many characters
	;; after it ends, handle those three cases.
	(setq result
	      (cond (delimited-flag
		     (if (use-region-p)
			 (hywiki--extend-region (min start (region-beginning))
						(max end (region-end)))
		       (hywiki--extend-region start end)))
		    ((use-region-p)
		     (hywiki--extend-region (region-beginning) (region-end)))
		    (t result)))
	(if delimited-flag
	    result
	  (list nil nil))))))

(defun hywiki-display-spec (wikiword &optional _spec_value)
  ;; Prevent infinite recursion
  (unless (hyperb:stack-frame '(hywiki-create-referent-and-display))
    (hywiki-create-referent-and-display wikiword t))
  (hywiki-message-spec wikiword))

(defun hywiki-message-spec (wikiword)
  "When WIKIWORD has a referent type of `spec', print a message saying so."
  (when (eq (car (hywiki-get-referent wikiword)) 'spec)
    (message "%s is now a wikiword spec; press the Action Key on it to set its referent type" wikiword)))

(defun hywiki-clear-referent-hasht ()
  "Clear all elements from the HyWiki referent hash table and return it."
  (setq hywiki--referent-hasht nil
	hywiki--any-wikiword-regexp-list nil))

(defvar hywiki-cache-default-file ".hywiki.eld"
  "Standard file name for storing cached data for a HyWiki.")

(defvar hywiki-cache-file nil
  "Current HyWiki cache file, if any.
If nil, use: (expand-file-name hywiki-cache-default-file hywiki-directory).")

(defun hywiki-cache-default-file (&optional directory)
  "Return a HyWiki cache file for optional DIRECTORY or `hywiki-directory'.
The filename is either the string value of `hywiki-cache-file', or else the
value of `hywiki-cache-default-file'.  The filename returned is an
absolute path."
  (expand-file-name (or hywiki-cache-file hywiki-cache-default-file)
		    (or directory hywiki-directory)))

(defun hywiki-cache-edit (cache-file)
  "Read in CACHE-FILE for editing and disable undo and backups within it."
  (prog1 (set-buffer (find-file-noselect cache-file))
    (buffer-disable-undo (current-buffer))
    (make-local-variable 'make-backup-files)
    (make-local-variable 'backup-inhibited)
    (setq make-backup-files nil
	  backup-inhibited t
	  buffer-read-only nil)))

(defun hywiki-cache-save (&optional save-file)
  "Save the modified Environment to a file.
The file is given by optional SAVE-FILE or `hywiki-cache-file'.  Also
save and potentially set `hywiki--directory-mod-time' and
`hywiki--directory-checksum'."
  (when (or (not (stringp save-file)) (equal save-file ""))
    (setq save-file (hywiki-cache-default-file)))
  (setq save-file (expand-file-name save-file hywiki-directory))
  (unless (file-writable-p save-file)
    (error "(hywiki-cache-save): Non-writable Environment file, \"%s\"" save-file))
  (let ((buf (get-file-buffer save-file)))
    (when buf
      (if (buffer-modified-p buf)
          (with-current-buffer buf
	    (hypb:save-buffer-silently))
	;; (error "(hywiki-cache-save): Attempt to kill modified Environment file failed to save, \"%s\"" save-file)
	(kill-buffer buf))))
  (let ((dir (or (file-name-directory save-file)
		 default-directory)))
    (unless (file-writable-p dir)
      (error "(hywiki-cache-save): Non-writable Environment directory, \"%s\"" dir)))
  (save-excursion
    (let ((standard-output (hywiki-cache-edit save-file)))
      (with-current-buffer standard-output
	(erase-buffer)
	(princ ";; -*- mode:lisp-data; coding: utf-8-emacs; -*-\n")

	(princ (format "\n(setq\nhyperb:version %S\n" hyperb:version))

	(princ (format "\nhywiki-directory %S\n" hywiki-directory))

	;; Save last `hywiki-directory' mod time and checksum, nil if none.
	(princ (format "\nhywiki--directory-mod-time '%S\n" (hywiki-directory-set-mod-time)))

	(princ (format "\nhywiki--directory-checksum %S\n"
		       (hywiki-directory-set-checksum)))

	(princ "\nhywiki--referent-alist\n'")
	(hash-prin1 (hywiki-get-referent-hasht) nil t)
	(princ ")\n")

	(hypb:save-buffer-silently)
	(if (buffer-modified-p)
	    (error "(hywiki-cache-save): Attempt to kill modified Environment file failed to save, \"%s\"" save-file)
	  (kill-buffer standard-output))))))

(defun hywiki-completion-at-point ()
  "Complete a HyWiki reference at point.
Each candidate is an alist with keys: file, line, text, and display."
  (setq hywiki--start-pos nil
        hywiki--end-pos nil)
  (let* ((ref-start-end (and (hywiki-active-in-current-buffer-p)
			     (not (hywiki-non-hook-context-p))
			     (hywiki-word-at t t)))
         (ref (nth 0 ref-start-end))
         (start (nth 1 ref-start-end))
         (end (nth 2 ref-start-end))
         (prefix (and start end (buffer-substring-no-properties start end)))
         (existing-wikiword-prefix (when ref (hywiki-word-strip-suffix ref))))
    (when prefix
      (let* ((default-directory hywiki-directory)
             (cmd (format "grep -nEH '^([ \t]*\\*+|#\\+TITLE:) +' ./%s*%s"
                          existing-wikiword-prefix
                          hywiki-file-extension))
             (output (shell-command-to-string cmd))
             (lines (split-string output "[\n\r]" t))
             (candidates
              ;; If no matching HyWiki pages were found, return nil
              (unless (and (= 1 (length lines))
                           (string-match "No such file or directory" (car lines)))
                (delq nil
                      (nconc
                       ;; Return only candidates that start with 'existing-wikiword-prefix'
                       (seq-filter (lambda (str)
                                     (string-prefix-p existing-wikiword-prefix str))
                                   (hywiki-get-page-list))
                       (mapcar #'hywiki-format-grep-to-reference lines)))))
             (candidates-alist (when candidates (mapcar #'list candidates))))
        (when candidates-alist
          (setq hywiki--char-before (char-before start)
                hywiki--start-pos start
                hywiki--end-pos end)
          (list start end candidates-alist
                :exclusive 'no
                ;; For company, allow any non-delim chars in prefix
                ;; :company-prefix-length t
                ;; :company-prefix-dirty t
                ;; Returning the prefix as (string . t) tells Company:
                ;; 'This is the prefix, and yes, it is currently valid (dirty).'
                ;; :company-prefix-snapshot (cons ref t)

                ;; This prevents the minibuffer/Corfu/Company from
                ;; re-parsing the # as a 'function quote' trigger.
                :company-kind (lambda (_) 'keyword)
                :annotation-function (lambda (_) " [HyWiki]")
                ;; Corfu uses this
                :exit-function #'hywiki-completion-exit-function))))))

(defun hywiki-completion-exit-function (&rest _)
  "Function called when HyWiki reference completion ends."
  ;; Find possibly needed closing delimiter and insert it if not already there
  (let ((end-delim (when (characterp hywiki--char-before)
                     (hash-get (char-to-string hywiki--char-before)
                               hywiki--open-close-hasht)))
        (point-at-end (and hywiki--end-pos (>= (point) hywiki--end-pos))))
    (when point-at-end
      (cond ((and end-delim (not (eq (char-after (point)) end-delim)))
             (insert end-delim)
             (goto-char (1- (point))))
            (end-delim)
            (hywiki--start-pos
             ;; No opening or closing delim yet.
             ;; If HyWiki ref has whitespace in it, need to add double
             ;; quotes at the beginning and the end
             (when (seq-contains-p (buffer-substring-no-properties hywiki--start-pos (point))
                                   ?\  #'=)
               (save-excursion
                 (insert ?\")
                 (goto-char hywiki--start-pos)
                 (insert ?\")))))))
  (hywiki-maybe-highlight-reference))

(defun hywiki-create-referent-and-display (wikiword &optional prompt-flag)
  "Display the HyWiki referent for WIKIWORD and return the referent value.
The value is the referent linked to without its type.

If there is no existing WIKIWORD referent and PROMPT-FLAG is non-nil,
prompt for and choose a referent type; see `hywiki-referent-menu' for
valid referent types.  Otherwise, if there is no existing HyWiki page
for WIKIWORD, add a page for it.

Use `hywiki-get-referent' to determine whether a HyWikiWord referent
or page exists."
  (interactive (list nil current-prefix-arg))
  (let ((at-wikiword-reference (hywiki-word-at)))
    (unless (stringp wikiword)
      (setq wikiword (or at-wikiword-reference
		         (hywiki-word-read-new
			  (format "Add/Edit and display HyWiki %s: "
				  (if current-prefix-arg "referent" "page"))))))
    (unless (equal (hywiki-get-singular-wikiword wikiword)
                   (hywiki-get-singular-wikiword at-wikiword-reference))
      (if buffer-read-only
          (error "(hywiki-create-referent-and-display): Read-only buffer: %s; call this with point on: \"%s\""
                 (current-buffer) wikiword)
        (skip-syntax-forward "^-")
        (unless (or (bolp) (= (char-syntax (preceding-char)) ?\ ))
          (insert " "))
        (insert wikiword)))

    (when (and (not prompt-flag) hywiki-referent-prompt-flag
	       (called-interactively-p 'interactive))
      (setq prompt-flag t))
    (let* ((normalized-word (hywiki-get-singular-wikiword wikiword))
	   (referent (hywiki-find-referent wikiword prompt-flag)))
      (cond (referent)
	    ((hywiki-word-is-p normalized-word)
	     (when (hywiki-add-page normalized-word)
	       (hywiki-display-page normalized-word)))
	    (t (user-error "(hywiki-create-referent-and-display): Invalid HyWikiWord: '%s'; must be capitalized, all alpha" wikiword))))))

(defun hywiki-display-page (&optional wikiword file-name)
  "Display an optional WIKIWORD page and return the page file.
Use `hywiki-display-page-function' to display the page.
Trigger an error if the page is not found.

If FILE-NAME is provided, it includes any #section from the WIKIWORD.

If WIKIWORD is omitted or nil and `hywiki-display-page-function'
is an interactive function, it is called interactively and prompts for
an existing or new HyWikiWord."
  (if (and (null wikiword) (commandp hywiki-display-page-function))
      (call-interactively hywiki-display-page-function)
    (when (null wikiword)
      (setq wikiword (hywiki-word-read-new "Find HyWiki page: ")))
    (let ((file (hywiki-get-page-file (or file-name wikiword))))
      (funcall hywiki-display-page-function file)
      ;; Set referent attributes of current implicit button
      (hattr:set 'hbut:current 'referent-type 'page)
      (hattr:set 'hbut:current 'referent-value file)
      file)))

(defun hywiki-add-path-link (wikiword &optional file pos)
  "Set a path link anchored possible position for WIKIWORD and return it.
If WIKIWORD is invalid, trigger an error if called interactively
or return nil if not.

Interactively prompt for the file and whether to use the current
position if a buffer is visiting the file; non-interactively, you may
optionally provide the FILE and POS arguments.

After successfully adding the path link, run `hywiki-add-referent-hook'.

Use `hywiki-get-referent' to determine whether WIKIWORD exists prior to
calling this function."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new "Add/Edit HyWikiWord: "))))
  (let* ((path-args (if (and file pos)
			(list file pos)
		      (hactypes:link-to-file-interactively)))
	 (path-link (and (= (length path-args) 2)
			 (hpath:file-position-to-line-and-column
			  (car path-args) (cadr path-args)))))
    (when path-link
      (hywiki-add-referent wikiword (cons 'path-link path-link)))))

(defun hywiki-display-path-link (_wikiword path)
  (funcall hywiki-display-page-function path))

;; Leave for backward compatibility if any such WikiWords already exist.
(defun hywiki-display-sexpression (_wikiword sexpression)
  (make-obsolete 'hywiki-display-sexpression 'hywiki-display-elisp "9.0.2pre")
  (eval sexpression))

;; Presently used only in tests; maybe move it to the test/ dir
(defun hywiki-add-to-referent (wikiword text position)
  "Display WIKIWORD referent and insert TEXT at POSITION.
Create page if it does not exist.  If WIKIWORD is invalid, return
nil, else return \\='(page . \"<page-file-path>\")."
  (when-let* ((referent (hywiki-add-page wikiword)))
    (hywiki-find-referent wikiword)
    (barf-if-buffer-read-only)
    (save-excursion
      (save-restriction
	(widen)
	(when position
	  (goto-char position))
	(unless (bolp)
	  (insert (newline)))
	(insert text)
	(unless (bolp)
	  (insert (newline)))
	(when position
	  (goto-char position))))
    referent))

(defun hywiki-at-tags-p (&optional at-tag-flag)
  "Return non-nil if point is in a HyWiki buffer and at Org tags."
  (and (or at-tag-flag (hsys-org-at-tags-p))
       (or (hywiki-in-page-p) (string-prefix-p "*HyWiki Tags*" (buffer-name)))))

(defun hywiki-completing-read-glossary-term (&optional prompt)
  "Read with completion a HyWiki glossary term.
Terms are defined in the file returned by the function
`hywiki-get-glossary-file'."
  (completing-read (or prompt "HyWiki glossary term: ")
		   (hywiki-get-glossary-terms)))

;;;###autoload
(defun hywiki-consult-backlink (reference)
  "Select a HyWikiWord backlink to REFERENCE with `hywiki-consult-grep'.
REFERENCE should be a string of \"WikiWord\" and optional \"#suffix\" which
is ignored.  Unless `grep' or `rg' have Perl-style pcre2 support, matches
will include filenames that include the WikiWord."
  (interactive (list (hywiki-word-read "Select a WikiWord backlink for: "
				       (hywiki-word-at-point))))
  ;; Ensure any #suffix is stripped
  (let ((wikiword (and (stringp reference) (not (string-empty-p reference))
                       (hywiki-word-strip-suffix reference))))
    (unless wikiword
      (user-error "(hywiki-consult-backlink): Invalid HyWikiWord: '%s'; must be capitalized, all alpha" wikiword))
    (require 'consult)
    (let* ((ignored-exts "kotl|org|otl|html|md")
           (ripgrep-pcre (hypb:ripgrep-has-pcre-p))
           (grep-pcre (hypb:grep-has-pcre-p))
           (consult-ripgrep-args consult-ripgrep-args)
           (consult-grep-args consult-grep-args)
           (regexp (format "\\b%s\\b\\(?!\\.\\(%s\\)\\)" wikiword ignored-exts)))
      (cond (ripgrep-pcre
	     (setq consult-ripgrep-args
	           (if (listp consult-ripgrep-args)
                       (append consult-ripgrep-args (list "-P"))
                     (concat consult-ripgrep-args " -P"))))
            (grep-pcre
	     (setq consult-grep-args
	           (if (listp consult-grep-args)
                       (append consult-grep-args (list "-P"))
                     (concat consult-grep-args " -P"))))
            ;; No pcre support, so use a simpler grep expression that doesn't
            ;; filter out WikiWords embedded within filenames
            (t (setq regexp (format "\\b%s\\b" wikiword))))
      (minibuffer-with-setup-hook
          (lambda ()
            ;; Disable the Org cache which can cause an 'Invalid search bound' error
            (setq-local org-element-use-cache nil)
            (setq-local org-fold-core-style 'overlays))
        (hywiki-consult-grep regexp nil nil
                             (format "HyWiki Backlinks [%s]: " wikiword))))))

;;;###autoload
(defun hywiki-consult-grep (&optional regexp max-matches path-list prompt)
  "Interactively search HyWiki pages with a consult package grep command.
Search for optional REGEXP up to MAX-MATCHES in PATH-LIST or `hywiki-directory'.

Use ripgrep (rg) if found, otherwise, plain grep.  Initialize search with
optional REGEXP and interactively prompt for changes.  Limit matches
per file to the absolute value of MAX-MATCHES, if given and not 0.  If
0, match to headlines only (lines that start with a '^[*#]+' regexp).
With optional PROMPT string, use this as the first part of the grep prompt;
omit any trailing colon and space in the prompt."
  (interactive "i\nP")
  (let* ((grep-includes "--include *.org")
	 (ripgrep-globs "--glob *.org"))
    (hsys-consult-grep grep-includes ripgrep-globs
		       regexp max-matches (or path-list (list hywiki-directory))
		       (or prompt (if (eq max-matches 0)
				      "Grep HyWiki dir headlines"
				    "Grep HyWiki dir")))))

(defun hywiki-consult-page-and-headline (&optional prompt initial)
  "Return a string of HyWiki file and headline selected by `consult-grep' or nil."
  (interactive)
  (hsys-consult-require-version)
  (let* ((default-directory (expand-file-name hywiki-directory))
         (dir "./")
         (builder
          (lambda (input)
            (let* (;; Define the regexp inside the builder so it is always in scope
                   (headline-regexp (concat "^\\* .*" input))
                   (args (list "rg"
                               "--null"
                               "--line-buffered"
                               "--color=never"
                               "--with-filename"
                               "--line-number"
                               "--smart-case"
                               "--search-zip"
                               "-g" "*.org"
                               "-e" headline-regexp
                               dir)))
              (cons args dir))))
         (selected (consult--read
                    (consult--async-command builder)
                    :initial initial
                    :prompt (or prompt "HyWiki Headline: ")
                    :require-match t
                    :lookup #'consult--lookup-member
                    :category 'consult-grep)))
    selected))

(defun hywiki-references-to-org-links ()
  "Convert all highlighted HyWikiWords in current buffer to Org links.
Org publishing is then used to convert HyWiki files to other formats such
as html.

For example, the reference:
  \"WikiWord#Multi-Word Section\"
is converted to:
  \"[[hy:WikiWord#Multi-Word Section]]\".

If the reference is within the WikiWord page to which it refers, it
simplifies to:
  \"[[Multi-Word Section]]\"."
  (barf-if-buffer-read-only)
  ;; Need to be explicit about the region here so does not use markers
  ;; from a region pointing to another buffer
  (hywiki-maybe-highlight-references (point-min) (point-max))
  (let ((make-index (hywiki-org-get-publish-property :makeindex))
	org-link
	wikiword-and-section
	wikiword)
    (hywiki-map-words
     (lambda (overlay)
       (setq wikiword-and-section
	     (buffer-substring-no-properties
	      (overlay-start overlay)
	      (overlay-end overlay)))
       (goto-char (overlay-start overlay))
       (delete-region (overlay-start overlay)
		      (overlay-end overlay))
       (delete-overlay overlay)
       (if (setq org-link (hywiki-reference-to-org-link wikiword-and-section nil))
	   (insert org-link)
	 (message
	  "(hywiki-references-to-org-links): \"%s\" in \"%s\" produced nil org link output"
	  wikiword-and-section (buffer-name)))
       (when make-index
	 (when (string-match (concat hywiki-org-link-type ":")
			     wikiword-and-section)
	   (setq wikiword (substring wikiword-and-section (match-end 0))))
	 (insert "\n#+INDEX: " wikiword "\n"))))))

(defun hywiki--pathname-reference-to-org-link (pathname referent description)
  "Convert a HyWiki PATHNAME REFERENT and DESCRIPTION to an Org link."
  (let* ((path-word-suffix referent)
         (path (file-relative-name (nth 0 path-word-suffix)))
         ;; (path-stem (when path
	 ;; 		  (file-name-sans-extension path)))
         (suffix (nth 2 path-word-suffix))
         (desc description)
	 ;; suffix-no-hashmark
	 )
    (unless (and suffix (not (string-empty-p suffix)))
      (setq suffix nil))
    ;; (setq suffix-no-hashmark (when suffix (substring suffix 1)))
    ;; (when (or (not buffer-file-name)
    ;; 	  (string-equal path (file-name-nondirectory buffer-file-name)))
    ;;   (setq path nil))
    (cond (desc
	   (if path
	       ;; "[[hy:pathname]]"
	       (format "[[%s:%s]]" hywiki-org-link-type pathname)
	     ;; (if suffix
	     ;;     ;; "[[file:path-stem.org::suffix][desc]"
	     ;;     (format "[[file:%s.org::%s][%s]]"
	     ;; 	       path-stem suffix-no-hashmark desc)
	     ;;   ;; "[[file:path-stem.org][desc]]")
	     ;;   (format "[[file:%s.org][%s]]" path-stem desc))
	     (if suffix
		 ;; "[[suffix][desc]]"
		 (format "[[%s][%s]]" suffix desc)
	       ;; "[[desc]]"
	       (format "[[%s]]" desc))))
	  (path
	   ;; "[[hy:pathname]]"
	   (format "[[%s:%s]]" hywiki-org-link-type pathname)))))

(defun hywiki--referent-reference-to-org-link (reference referent _description)
  "Convert a HyWiki REFERENT REFERENCE and DESCRIPTION to an Org link."
  (format "[[hypb-msg:%s][%s]]" (format "Export of link type %s is not supported" (car referent)) reference))

(defun hywiki-reference-to-org-link (reference &optional description)
  "Convert a HyWiki REFERENCE and an optional DESCRIPTION to an Org link."
  ;; \"[[file:<hywiki-directory>/WikiWord.org::Multi-Word Section][WikiWord#Multi-Word Section]]\".
  (let ((referent (hywiki-reference-to-referent reference :full-data)))
    (when referent
      (cond ((stringp (car referent))
             (hywiki--pathname-reference-to-org-link reference referent description))
            (t
             (hywiki--referent-reference-to-org-link reference referent description))))))

(defun hywiki-maybe-at-wikiword-beginning ()
  "Return non-nil if at bol or previous character is one preceding a HyWikiWord.
When non-nil, the value returned is 0 for bol and the preceding character,
otherwise.

Do not test whether or not a page exists for the HyWikiWord.
Use `hywiki-get-referent' to determine whether a HyWiki page exists."
  ;; Ignore wikiwords preceded by any non-whitespace character, except
  ;; any of these: [({<"'`'
  (when (or (bolp)
	    (string-match (regexp-quote (char-to-string (char-before)))
			  "\[\(\{\<\"'`\t\n\r\f "))
    (or (char-before) 0)))

(defun hywiki-denote-file-to-wikiword (denote-file)
  "Return a hywikiword from the denote description associated with DENOTE-FILE."
  (interactive
   (list (denote-file-prompt nil "HyWiki denote file: "
			     nil t)))
  (if denote-file
      (hywiki-string-to-wikiword
       (denote-get-link-description denote-file))
    (error "(hywiki-denote-file-to-wikiword): Denote file not found: \"%s\""
	     denote-file)))

(defun hywiki-denote-id-to-wikiword (denote-id)
  "Return a hywikiword from the denote description associated with DENOTE-ID."
  (let ((denote-file (denote-get-path-by-id denote-id)))
    (if denote-file
	(hywiki-string-to-wikiword
	 (denote-get-link-description denote-file))
      (error "(hywiki-denote-id-to-wikiword): Denote file not found for id: \"%s\""
	     denote-id))))

(defun hywiki-directory-edit ()
  "Edit HyWiki pages in current `hywiki-directory'.
Use `dired' unless `action-key-modeline-buffer-id-function' is set to
`smart-treemacs-modeline', then use `treemacs'."
  (interactive)
  (if (eq action-key-modeline-buffer-id-function #'smart-treemacs-modeline)
      (hywiki-directory-treemacs-edit)
    (hywiki-directory-dired-edit)))

(defun hywiki-directory-dired-edit ()
  "Use `dired' to edit HyWiki pages in current `hywiki-directory'."
  (interactive)
  (let ((case-fold-search nil))
    (dired (cons hywiki-directory
		 (directory-files hywiki-directory nil
				  (format "^%s%s$"
					  hywiki-word-regexp
					  (regexp-quote hywiki-file-extension)))))))

(defun hywiki-directory-treemacs-edit ()
  "Use `treemacs' to edit HyWiki pages in current `hywiki-directory'."
  (interactive)
  (require 'hui-treemacs)
  (smart-treemacs-edit hywiki-directory))

(defun hywiki-directory-get-checksum ()
  "Compute and return the checksum for the current set of HyWiki pages."
  (let ((hywiki-page-files (hywiki-get-page-files)))
    (when hywiki-page-files
      (md5 (apply #'concat hywiki-page-files) nil nil nil t))))

(defun hywiki-directory-get-mod-time ()
  "Return the last mod time for `hywiki-directory' or nil."
  (when (file-readable-p hywiki-directory)
    (time-convert (file-attribute-modification-time
		   (file-attributes hywiki-directory))
		  'list)))

(defun hywiki-directory-modified-p ()
  "Return non-nil if any HyWiki page name change since last read."
  (or (null hywiki--directory-mod-time)
      ;; Both dir mod-time and filename checksum over HyWiki page
      ;; files must have changed for this to be an update to report.
      ;; Don't change this logic as many other dir changes can occur
      ;; that should not be reported here.
      (not (or (equal hywiki--directory-mod-time (hywiki-directory-get-mod-time))
	       (string-equal hywiki--directory-checksum (hywiki-directory-get-checksum))))))

(defun hywiki-directory-set-checksum ()
  "Store the last page name checksum for `hywiki-directory' as a string."
  (setq hywiki--directory-checksum (hywiki-directory-get-checksum)))

(defun hywiki-directory-set-mod-time ()
  "Store the last page mod time for `hywiki-directory'.
Use `time-since' to see the time in seconds since this modification time."
  (setq hywiki--directory-mod-time (hywiki-directory-get-mod-time)))

(defun hywiki-maybe-directory-updated ()
  "When a HyWiki directory is modified, reset its modified time and checksum."
  (hywiki-directory-set-mod-time)
  (hywiki-directory-set-checksum))


;;;###autoload
(defun hywiki-find-page (&optional wikiword)
  "Display optional HyWiki WIKIWORD page or if nil, use current buffer.
If called interactively, use the WIKIWORD at point or if none, prompt for
an existing or new one.

Return the absolute path to the file of the page if successfully found
or nil otherwise.

By default, create any non-existent page.  When not in batch
mode, if this is the first HyWiki page in `hywiki-directory',
prompt to create if non-existent.  After successfully finding a
page and reading it into a buffer, run
`hywiki-display-page-hook'.

After successfully finding a page, run `hywiki-find-page-hook'."
  (interactive (list (hywiki-page-read-new "Add/Edit HyWiki page: ")))
  (let ((page-file (hywiki-display-page wikiword)))
    (run-hooks 'hywiki-find-page-hook)
    page-file))

;;;###autoload
(defun hywiki-find-referent (&optional wikiword prompt-flag)
  "Display optional HyWiki WIKIWORD referent or if nil, use current buffer.
If called interactively, use the WIKIWORD at point or if none, prompt for
an existing or new one.  With a prefix arg PROMPT-FLAG, prompt for the
type of referent to link to.  See `hywiki-referent-menu' for valid
referent types.

Return the referent if successfully found or nil otherwise.
A valid referent is a cons of (<referent-type> . <referent-value>).

If the referent is a HyWiki page:
    Return a cons of the symbol \\='page and the absolute path
    to any page successfully found.  Return nil if failed or
    if displaying a regular file (read in via a `find-file' call).

    By default, create any non-existent page.  When not in batch
    mode, with optional PROMPT-FLAG t or if this is the first
    HyWiki page in `hywiki-directory', prompt to create if
    non-existent.  If PROMPT-FLAG is :existing or with a prefix
    argument when called interactively, return nil unless the
    page already exists.  After successfully finding a page and
    reading it into a buffer, run `hywiki-display-referent-hook'.

After successfully finding any kind of referent, run
`hywiki-find-referent-hook'."
  (interactive (list (hywiki-word-read-new "Add/Edit HyWikiWord: ")
		     (when current-prefix-arg t)))
  (let ((referent (hywiki-display-referent wikiword prompt-flag)))
    (run-hooks 'hywiki-find-referent-hook)
    referent))

(defun hywiki-format-grep-to-reference (page-and-headline)
  "Return a HyWikiWord#section reference from PAGE-AND-HEADLINE.
Add double quotes if the section contains any whitespace after trimming.

Return t if PAGE-AND-HEADLINE is a valid string, else nil.  If the page name
therein is invalid, trigger an error."
  (when (and page-and-headline (stringp page-and-headline))
    (if (string-match "\\`\\([^\0]+\\)[\0:]\\([0-9]+\\):\\(.+\\)"
                      page-and-headline)
        (let ((page (file-name-base (match-string 1 page-and-headline)))
              (line (match-string 3 page-and-headline)))
          (setq line (string-trim line))
          ;; Drop '* ' prefix
          (setq line (hpath:org-normalize-title
                      (hywiki-org-format-heading line t t t nil t)))
          (format "%s#%s" page line))
      (message "(hwiki-format-grep-to-reference): Parse error on: %s"
               page-and-headline)
      nil)))

(defun hywiki-format-reference-to-consult-grep (page-and-section)
  "From PAGE-AND-SECTION ref, return '|^[ \t]*\\*+.*sect|page' grep pat.

Return t if PAGE-AND-SECTION is a valid string, else nil.  If the page name
therein is invalid, trigger an error."
  (when (and page-and-section (stringp page-and-section))
    (if (string-match hywiki-word-with-optional-suffix-exact-regexp
                      page-and-section)
        (let ((page (match-string 1 page-and-section))
              (line (match-string 2 page-and-section)))
          (setq line (if line
                         ;; Remove the # prefix in line
                         (substring line 1)
                       ""))
          ;; Add '* ' prefix
          (format "|[ \t]*\\\\*+.*%s|%s"
                  (regexp-quote line)
                  (regexp-quote page)
                  ))
      (message "(hwiki-format-reference-to-consult-grep): Parse error on: %s"
               page-and-section)
      nil)))

(defun hywiki-get-term-variant-regexp (term)
  "Return a grouped regexp of the allowed glossary variants for TERM string."
  ;; Allow for term downcased, and/or with spaces between capitalized words
  ;; or the whole term capitalized.  Also allow the term to be surrounded by
  ;; Org emphasis characters.
  (let ((str)
        (case-fold-search nil))
    (format "[*/_=~]?\\(%s\\|%s\\|%s\\|%s\\)[*/_=~]?"
            (regexp-quote term)
            (regexp-quote (downcase term))
            (regexp-quote (setq str (hywiki-wikiword-to-string term " ")))
            (regexp-quote (capitalize str)))))

;;;###autoload
(defun hywiki-get-definition (term &optional display-flag file)
  "Return or display the HyWiki glossary definition string for TERM.
Return nil if TERM is not a string, if TERM is the empty string or no match
is found.

If called interactively or with optional DISPLAY-FLAG non-nil, display a
buffer with TERM's definition but don't return it.  Get definition from
HyWiki's Glossary.org or optional FILE.  If the file is not readable or no
matching TERM entry is found, return nil; othewise, return t.

If the `consult' package is installed, interactively select and complete
the entry to be inserted.

Below is a table of how the inputs determine the lookup performed.  Only
top-level headlines are searched.
|------------------+------+---------------+----------------------------------|
| TERM             | FILE | Entry to Find | Entry Target                     |
|------------------+------+---------------+----------------------------------|
| phrase           | file | phrase        | HyWiki file specified            |
| phrase           | nil  | phrase        | Glossary.org in hywiki-directory |
| WikiWord#section | nil  | section       | WikiWord.org in hywiki-directory |
| WikiWord         | nil  | WikiWord      | Glossary.org in hywiki-directory |
| #section         | nil  | section       | current buffer                   |
|------------------+------+---------------+----------------------------------|"
  ;; Add completing-read here after gathering terms from the Glossary.org file
  (interactive (list (hywiki-completing-read-glossary-term
                      "HyWiki glossary definition of: ")
                     nil t))
  (when (and (stringp term) (not (string-empty-p term)))
    (let ((glossary (hywiki-get-glossary-file file))
          entry
          index
          term-regexp
          wikiword)
      (cond ((setq index (seq-position term ?#))
             ;; WikiWord#section or #section
             (setq wikiword (substring term 0 index)
                   term (substring term (1+ index))
                   term-regexp (hywiki-get-term-variant-regexp term)
                   file (if (zerop index)
                            buffer-file-name
                          (hywiki-get-page-file wikiword))))
            ((or (hywiki-word-is-p term t) (null file))
             ;; Is a WikiWord or phrase only; match to any of several
             ;; forms: WikiWord, wikiword, Wiki Word, wiki word -- in
             ;; file when given or the Glossary file when not
             (when (file-readable-p glossary)
               (setq term-regexp (hywiki-get-term-variant-regexp term))
               (unless (and (stringp file) (not (string-empty-p file)))
                 (setq file glossary))))
            (t
             ;; `file' is non-null so try to use that or trigger an error
             (when (or (not (stringp file)) (string-empty-p file)
                       (not (file-readable-p file)))
               (error "(hywiki-get-definition): \"%s\" is invalid" file))
             (setq term-regexp (hywiki-get-term-variant-regexp term))))

      ;; Return and possibly display matching definition
      (setq entry (hywiki-get-entry term-regexp display-flag file t))

      (when (stringp entry)
        ;; Remove * or # prefix from def entry before returning, so can be
        ;; embedded in other doc
        (setq entry (substring entry (if (string-match "\\`[*#]+[ \t\n\f]*"
                                                       entry)
                                         (match-end 0)
                                       0))))
      entry)))

(defun hywiki-get-delimited-region ()
  "Immediately before or after a balanced delimiter, return the delimited range.
Include: (), {}, <>, [] and \"\" (double quotes).  Exclude Org links
and radio targets.

If no such range, return \\='(nil nil).
This includes the delimiters: (), {}, <>, [] and \"\" (double quotes)."
  (let* ((deleting-backward-flag
	  (and (symbolp this-command)
	       (string-match-p "backward" (symbol-name this-command))
	       (string-match-p "delete\\|kill" (symbol-name this-command))))
	 (result
	  (condition-case nil
              (with-syntax-table hbut:syntax-table
	        (cond
	         (deleting-backward-flag
		  ;; Since are deleting backward, consider closing
		  ;; delims first
                  (hywiki--get-delimited-range-at-closing-delimiter))
	         ;; Handle opening delimiters
	         ((memq (char-before) '(?\[ ?\<))
		  (goto-char (1- (point)))
		  (hywiki--get-delimited-range-forward))
	         ((memq (char-after) '(?\[ ?\<))
		  (hywiki--get-delimited-range-forward))
	         ((memq (char-before) '(?\( ?\{))
		  (goto-char (1- (point)))
		  (list (point) (scan-sexps (point) 1)))
	         ((memq (char-after) '(?\( ?\{))
		  (list (point) (scan-sexps (point) 1)))
	         ((and (eq (char-before) ?\")
		       (hypb:in-string-p))
		  (goto-char (1- (point)))
		  (list (point) (scan-sexps (point) 1)))
	         ((and (eq (char-after) ?\")
		       (hypb:in-string-p))
		  (goto-char (1+ (point)))
		  (list (point) (scan-sexps (point) -1)))
	         ((not deleting-backward-flag)
                  (hywiki--get-delimited-range-at-closing-delimiter))))
	    (error nil))))
    (if (and (integerp (nth 0 result)) (integerp (nth 1 result)))
	(sort result #'<)
      '(nil nil))))

;;;###autoload
(defun hywiki-get-entry (reference &optional display-flag file regexp-flag)
  "Return a string of the first HyWiki entry headline containing REFERENCE.
Return nil if REFERENCE is not a string, if REFERENCE is the empty string or
no match is found.

If called interactively or with optional DISPLAY-FLAG non-nil, display a
buffer with REFERENCE's definition but don't return it.  If the file is not
readable or no matching REFERENCE entry is found, return nil; othewise,
return t.

Get definition from HyWiki's Glossary.org, optional FILE, or the current
buffer for #in-file references.  Trigger an error if an invalid non-nil FILE
argument is sent.

With optional prefix arg, REGEXP-FLAG, treat REFERENCE as a regular
expression instead of a string.

If the `consult' package is installed, interactively select and complete
the entry to be inserted.

Below is a table of how the inputs determine the lookup performed.  Only
top-level headlines are searched.
|------------------+------+---------------+----------------------------------|
| REFERENCE        | FILE | Entry to Find | Entry Target                     |
|------------------+------+---------------+----------------------------------|
| phrase           | file | phrase        | HyWiki file specified            |
| phrase           | nil  | phrase        | Glossary.org in hywiki-directory |
| WikiWord#section | nil  | section       | WikiWord.org in hywiki-directory |
| WikiWord         | nil  | WikiWord      | Glossary.org in hywiki-directory |
| #section         | nil  | section       | current buffer                   |
|------------------+------+---------------+----------------------------------|"
  (interactive (list (hywiki-read-headline-regexp "Return")
		     current-prefix-arg))
  (when (and (stringp reference) (not (string-empty-p reference)))
    (let ((hyrolo-display-buffer hywiki-glossary-display-buffer)
          (buf-file buffer-file-name)
          (found 0)
          (word-end (hywiki-word-is-p reference t))
          (consult-flag (and (called-interactively-p 'interactive) (hsys-consult-active-p)))
          (phrase "")
          entry-to-match
          path-list)
      (save-window-excursion
        (with-current-buffer (get-buffer-create hywiki-glossary-display-buffer)
          (setq hyrolo-display-buffer hywiki-glossary-display-buffer)
          ;; (hyrolo--cache-initialize)
	  (cond ((or consult-flag
                     (not (string-match-p "#" reference))
                     (not word-end))
                 (setq phrase ""
                       entry-to-match reference))
                (word-end
                 (setq phrase (string-trim (substring reference 0 word-end))
                       entry-to-match (unless (= word-end (length reference))
                                        (string-trim (substring reference (1+ word-end)))))))
          ;; If path-list remains nil, then search will be across the whole wiki
          (cond ((stringp file)
                 ;; explicit `file' given
                 (setq path-list (list (expand-file-name file hywiki-directory))))
                ((and (not consult-flag) (string-match-p "\\`#" reference))
                 ;; in-file reference
                 (setq entry-to-match (substring reference 1)
                       path-list (if buf-file
                                     (list buf-file)
                                   (user-error "(hywiki-get-entry): #section not allowed in non-file buffer: reference = %s; buffer = %S"
                                               reference (get-buffer buf-file)))))
                ((not (string-empty-p phrase))
                 ;; The phrase is a WikiWord; use this as the file to search
                 (setq path-list (list (expand-file-name
                                        (concat phrase
                                                (unless (string-suffix-p hywiki-file-extension phrase)
                                                  hywiki-file-extension))
                                        hywiki-directory)))))

          (unless entry-to-match
            (error "(hywiki-get-entry): No `entry-to-match' derivable from \"%s\""
                   reference))

          (save-excursion
            (setq found
	          (if (and consult-flag
                           (or path-list
                               ;; Extract `name' from the line-numbered
                               ;; `consult-grep' match results
		               (string-match "\\([^ \t\n\r\"'`]*[^ \t\n\r:\"'`0-9]\\): ?\\([1-9][0-9]*\\)[ :]"
				             reference)))
		      (hyrolo-grep-file (if path-list (car path-list) (match-string-no-properties 1 reference))
				        (funcall (if regexp-flag #'identity #'regexp-quote)
                                                 (if path-list
                                                     entry-to-match
                                                   (substring reference (match-end 0))))
				        -1 nil t)
	            (hyrolo-grep (if regexp-flag entry-to-match (regexp-quote entry-to-match))
                                 -1
                                 (hyrolo-expand-path-list (or path-list (list hywiki-directory)))
                                 nil t (not display-flag)))))))
      (if display-flag
          (if (zerop found)
              (progn (beep)
                     (message "(hywiki-get-entry): No match for `%s' found in `%s'"
                              reference
                              (or path-list hywiki-directory))
                     nil)
            ;; Display even if no entries are found
            (hyrolo-display-matches)
            ;; skip past header to the entry
            (hyrolo-to-next-entry)
            t)
        (unless (zerop found)
          (with-current-buffer hyrolo-display-buffer
            (save-excursion
              (goto-char (point-min))
              ;; skip past header to the entry
              (hyrolo-to-next-entry)
              (buffer-substring (point) (point-max)))))))))

;;;###autoload
(defmacro hydef (&rest term)
  "Display TERM's definition, which may be given as multiple strings or symbols.
All of the following represent the same TERM: ActionKey, \"ActionKey\",
Action Key, action key, and \"action key\", i.e. (hydef action key).

Use this as a Hyperbole Action Button to hyperlink to an individual HyWiki
definition from ${hywiki-directory}/Glossary.org), i.e. <hydef action key>"
  `(let ((term-str (mapconcat (lambda (word)
                                (cond ((symbolp word) (symbol-name word))
                                      ((stringp word) word)
                                      (t (format "%s" word))))
                              ',term " ")))
     (when (or (null term-str) (member term-str '("" "t" "nil")))
       (error "(hydef): No term given; must give a term to lookup the definition"))
     (hywiki-get-definition term-str t)))

(defun hywiki-get-glossary-file (&optional file)
  "Return the glossary file for the current HyWiki."
  (expand-file-name (if (and (stringp file) (not (string-empty-p file)))
                        file
                      (concat "Glossary" hywiki-file-extension))
                    hywiki-directory))

(defun hywiki-get-glossary-terms ()
  "Return a completion list of terms in the HyWiki glossary file.
Call `hywiki-get-glossary-file' for the full path to this file."
  (let* ((shell-command-buffer-name "*hywiki-glossary-terms*")
	 (glossary (hywiki-get-glossary-file))
	 (glossary-terms (when (and glossary (file-readable-p glossary)
				    (not (hypb:empty-file-p glossary)))
			   (read (shell-command-to-string
			          (format "grep -E '^\\*+ *[A-Z]' %s | sed -E 's/\\*+[ \t]*([^:\;]+).*/\"\\1\"/' | echo \"\(\" $(cat -) \"\)\""
                                          (hywiki-get-glossary-file)))))))
    glossary-terms))

(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 (hypb:buffer-file-name) (buffer-name)))))

(defun hywiki-get-buffers (hywiki-mode-status)
  "Return the set of HYWIKI-MODE-STATUS buffers in any non-minibuffer window.
This goes across all live frames.

See the function documentation for `hywiki-mode' for valid HYWIKI-MODE-STATUS
values (the states of `hywiki-mode')."
  (when hywiki-mode-status
    (let ((hywiki-buf-predicate
	   (if (eq hywiki-mode-status :pages)
	       #'hywiki-in-page-p
	     #'hywiki-potential-buffer-p)))
      (seq-filter (lambda (buf)
		    (with-current-buffer buf
		      (when (funcall hywiki-buf-predicate)
			buf)))
		  (buffer-list)))))

(defun hywiki-get-buffers-in-windows (&rest frames)
  "Return the set of HyWiki buffers in all windows across all live frames.
Or include only those in optional rest of arguments FRAMES.
Always exclude minibuffer windows."
  (apply #'set:create
	 (apply #'nconc (mapcar (lambda (frame)
				  (when (frame-live-p frame)
				    (mapcar #'window-buffer
					    (window-list frame :no-minibuf))))
				(or frames (frame-list))))))

(defun hywiki-get-existing-page-file (reference)
  "Return existing `hywiki-directory' path from REFERENCE or nil.
REFERENCE should not contain a directory and may have or may omit
`hywiki-file-extension' and an optional trailing #section.

Checks only that REFERENCE is not nil, not an empty string and does
not contain a directory path or returns nil."
  (make-directory hywiki-directory t)
  (unless (or (null reference)
              (eq reference t) ;; HyWikiWord spec
              (string-empty-p reference)
              (file-name-directory reference))
    (let (file-name
          referent
	  section)
      ;; Remove any suffix from `reference' and make it singular
      (if (string-match hywiki-word-suffix-regexp reference)
	  (setq section (match-string 0 reference)
		file-name (hywiki-get-singular-wikiword
			   (substring reference 0 (match-beginning 0))))
	(setq file-name reference))
      (setq referent (hywiki-get-referent file-name))
      (when (and (eq (car referent) 'page)
                 ;; The referent replaces the page name with name.org, so can be next.
                 (setq file-name (expand-file-name (cdr referent) hywiki-directory))
                 (file-exists-p file-name))
        (concat file-name section)))))

(defun hywiki-get-page-file (reference)
  "Return possibly non-existent `hywiki-directory' path from REFERENCE.
REFERENCE may be an existing absolute file path; then, return it.
Otherwise, REFERENCE should not contain a directory and may have or may omit
`hywiki-file-extension' and an optional trailing #section, both of which are
left attached to the result returned.  So given the input, WikiWord#section,
the result might be: \"/users/me/hywiki/WikiWord.org#section\".

Check only that REFERENCE is not nil, not an empty string and does
not contain a directory path or return nil."
  (make-directory hywiki-directory t)
  (if (and (stringp reference) (file-readable-p reference))
      reference
    (unless (or (null reference) (string-empty-p reference)
                (file-name-directory reference))
      (let (file-name
	    section)
        ;; Remove any suffix from `reference' and make it singular
        (if (string-match hywiki-word-suffix-regexp reference)
	    (setq section (match-string 0 reference)
		  file-name (hywiki-get-singular-wikiword
			     (substring reference 0 (match-beginning 0))))
	  (setq file-name reference))
        (concat (expand-file-name file-name hywiki-directory)
	        (unless (string-suffix-p hywiki-file-extension file-name)
		  hywiki-file-extension)
	        section)))))

(defun hywiki-get-page-files ()
  "Return the list of existing HyWiki page file names.
These must end with `hywiki-file-extension'."
  (when (stringp hywiki-directory)
    (unless (file-directory-p hywiki-directory)
      (make-directory hywiki-directory t))
    (when (file-readable-p hywiki-directory)
      (directory-files
       hywiki-directory nil (concat "^" hywiki-word-regexp
				    (regexp-quote hywiki-file-extension) "$")))))

(defun hywiki-get-page-headings (page)
  "Return a list of all headings found in FILE.
Strip any leading '*' and space characters from the headings."
  (when (and (stringp page) (file-readable-p page))
    (let ((grep-command (format "grep -E '^\\*+ ' %s" page)))
      (with-temp-buffer
        (shell-command grep-command (current-buffer))
        (goto-char (point-min))
        (let (headings)
          (while (re-search-forward "^\\*+ +\\(.*\\)$" nil t)
            (push (match-string-no-properties 1) headings))
          (nreverse headings))))))

(defun hywiki-get-page-list ()
  "Return the list of HyWikiWords with existing pages."
  (delq nil (hash-map (lambda (referent-type)
			(when (eq (caar referent-type) 'page)
			  (cdr referent-type)))
		      (hywiki-get-referent-hasht))))

(defun hywiki-get-referent (wikiword &optional absolute-path-flag)
  "Return the referent of HyWiki WIKIWORD or nil if it does not exist.
Add any suffix from WIKIWORD to the referent value.  With optional non-nil
ABSOLUTE-PATH-FLAG, if the referent value is a pathname, expand it relative
to `hywiki-directory'."
  (when (and (stringp wikiword) (not (string-empty-p wikiword))
	     (string-match hywiki-word-with-optional-suffix-exact-regexp wikiword))
    (let* ((suffix (cond ((match-beginning 2)
			   (prog1 (substring wikiword (match-beginning 2))
			     ;; Remove any #section suffix in `wikiword'.
			     (setq wikiword (match-string-no-properties 1 wikiword))))
			  ((match-beginning 3)
			   (prog1 (substring wikiword (match-beginning 3))
			     ;; Remove any :Lnum:Cnum suffix in `wikiword'.
			     (setq wikiword (match-string-no-properties
					     1 wikiword))))))
	   (referent (hash-get (hywiki-get-singular-wikiword wikiword)
			       (hywiki-get-referent-hasht))))
      ;; If a referent type that can include a # or :L line
      ;; number suffix, append it to the referent-value.  Also, if
      ;; `absolute-path-flag' is non-nil, assume the referent value is a
      ;; filename and expand it relative to `hywiki-directory'.
      (when referent
        (hywiki--add-suffix-to-referent suffix
                                        (cons (car referent)
                                              (if absolute-path-flag
                                                  (expand-file-name
                                                   (cdr referent)
                                                   hywiki-directory)
                                                (cdr referent))))))))

(defun hywiki-get-referent-hasht ()
  "Return hash table of existing HyWiki referents.
May recreate the hash table as well as the list of
regexps of wikiwords, if the hash table is out-of-date."
  (prog1
      (if (and (hash-table-p hywiki--referent-hasht)
               (equal hywiki--pages-directory hywiki-directory)
	       ;; If page files changed, have to rebuild referent hash table
	       (not (hywiki-directory-modified-p)))
	  hywiki--referent-hasht
	;; Rebuild referent hash table
	(hywiki-make-referent-hasht))
    (when (and (null hywiki--any-wikiword-regexp-list)
               (not (hash-empty-p hywiki--referent-hasht)))
      ;; Compute these expensive regexps (matching 50
      ;; HyWikiWords at a time) only if the set of
      ;; HyWikiWords changed in `hywiki-directory'.
      (setq hywiki--any-wikiword-regexp-list
	    (mapcar (lambda (wikiword-sublist)
		      ;; Add plurals to the list
		      (setq wikiword-sublist
			    (delq nil (nconc wikiword-sublist
					     (mapcar #'hywiki-get-plural-wikiword wikiword-sublist))))
		      (concat "\\b" (regexp-opt wikiword-sublist t) "\\b"
			      "\\(" hywiki-word-section-regexp "??" hywiki-word-line-and-column-numbers-regexp "?" "\\)"
			      hywiki--buttonize-character-regexp))
		    (hypb:split-seq-into-sublists
		     (hash-map #'cdr hywiki--referent-hasht) 25)))
      ;; This may have been called after a HyWiki page is deleted.
      ;; References to it may be highlighted in any frame, so need to
      ;; walk across all frames here, rehighlighting HyWikiWords.
      (hywiki-maybe-highlight-wikiwords-in-frame t t))))

(defun hywiki-get-reference-range (reference)
  "Return a (start . end) cons cell from a highlighted HyWikiWord REFERENCE."
  (when (hproperty:but-is-p reference)
    (cons (hproperty:but-start reference)
	  (hproperty:but-end reference))))

(defun hywiki-get-references (&optional start end)
  "Return a list of all highlighted HyWikiWord references in the current buffer.
Optional START and END arguments limit the search to references that at
least partially overlap that region."
  (hywiki--get-all-references #'hproperty:but-get-all-in-region start end))

(defun hywiki-get-reference-positions (&optional start end)
  "Return a list of all highlighted HyWikiWord reference (start . end) positions.
Optional START and END arguments limit the search to references that at
least partially overlap that region."
  (hywiki--get-all-references #'hproperty:but-get-all-positions start end))

(defun hywiki-get-wikiword-list ()
  "Return the list of existing HyWikiWords."
  (hash-map #'cdr (hywiki-get-referent-hasht)))

(defun hywiki-get-plural-wikiword (wikiword)
  "Return the pluralized version of the given WIKIWORD.
`hywiki-allow-plurals-flag' must be non-nil or nil is always returned."
  ;; You add "-es" to make a noun plural when the singular noun ends
  ;; in "s", "x", "z", "sh", or "ch".  However, there are some
  ;; exceptions to this rule, such as words ending in "-ch" that are
  ;; pronounced with a hard "k", like "monarchs" and "stomachs".
  (when hywiki-allow-plurals-flag
    (cond ((let ((case-fold-search t))
	     (string-match-p "\\(es\\|.[^es]s\\)$" wikiword))
	   ;; Already plural
	   wikiword)
	  ((let ((case-fold-search t))
	     (string-match-p "\\(ch\\|sh\\|[sxz]\\)$" wikiword))
	   (concat wikiword (if (string-match-p "[[:lower:]]" wikiword)
				"es"
			      "ES")))
	  (t (concat wikiword (if (string-match-p "[[:lower:]]" wikiword)
				  "s"
				"S"))))))

(defun hywiki-get-singular-wikiword (wikiword)
  "Return the singular version of the given WIKIWORD with any suffix removed.
If `hywiki-allow-plurals-flag' is nil, return unchanged WIKIWORD name
with any suffix removed."
  (setq wikiword (hywiki-word-strip-suffix wikiword))
  (if (or (not hywiki-allow-plurals-flag)
	  (not (stringp wikiword)))
      wikiword
    (or (when (let ((case-fold-search t))
		;; Handle typical pluralized words ending in 's' (not preceded
		;; by an 's') or 'es'
		(string-match-p "\\(ch\\|sh\\|[sxz]\\)es$" wikiword))
	  (substring wikiword 0 -2))
	(when (let ((case-fold-search t))
		(and (string-match-p ".[^eEsS]s$" wikiword)
		     (not (string-match-p "emacs$" wikiword))))
	  (substring wikiword 0 -1))
	wikiword)))

(defun hywiki-kill-buffer-hook ()
  "Delete file attached to HyWiki buffer if the file is zero-sized.
If deleted, update HyWikiWord highlighting across all frames."
  (when (and buffer-file-name (hywiki-in-page-p))
    (when (hypb:empty-file-p)
      (delete-file (hypb:buffer-file-name)))
    (when (hywiki-directory-modified-p)
      ;; Rebuild lookup tables if any HyWiki page name has changed
      (hywiki-get-referent-hasht)
      t)
    nil))

(defun hywiki-highlighted-word-at (&optional range-flag)
  "Return highlighted HyWikiWord and optional #section:Lnum:Cnum at point or nil.
If the HyWikiWord is delimited, point must be within the delimiters.

With optional RANGE-FLAG, return a list of (HyWikiWord start-position
end-position); the positions include the entire
HyWikiWord#section:Lnum:Cnum string but exclude any delimiters.

This does not test whether a referent exists for the HyWikiWord; call
`hywiki-referent-exists-p' without an argument for that.

A call to `hywiki-active-in-current-buffer-p' at point must return non-nil
or this will return nil."
  (when (and (hywiki-active-in-current-buffer-p)
	     (setq hywiki--range (hywiki-word-at :range))
	     (car hywiki--range))
    (cl-destructuring-bind (wikiword start end)
	hywiki--range
      (if (and (hproperty:but-get start 'face hywiki-word-face)
	       (string-match hywiki-word-with-optional-suffix-exact-regexp wikiword))
	  (if range-flag
	      (list wikiword start end)
	    wikiword)
	(when range-flag
	  '(nil nil nil))))))

(defun hywiki-highlight-on-yank (_prop-value start end)
  "Used in `yank-handled-properties' called with START and END pos of the text."
  ;; When yank only part of a delimited pair, expand the range to
  ;; include the whole delimited pair before re-highlighting
  ;; HyWikiWords therein, so that the whole delimited expression is
  ;; included.
  (cl-destructuring-bind (start end)
      (hywiki--extend-region start end)
    (hywiki-maybe-highlight-references start (min end (point-max)))))

(defun hywiki-highlight-page ()
  "Rehighlight all HyWikiWord references when in a HyWiki page."
  (interactive)
  (setq hywiki-buffer-highlighted-state nil)
  (hywiki-maybe-highlight-references))

(defun hywiki-in-page-p ()
  "Return non-nil if the current buffer is a HyWiki page.
Note that HyWiki references can occur in non-HyWiki page buffers."
  (or hywiki-page-flag
      (and buffer-file-name
	   (string-suffix-p hywiki-file-extension buffer-file-name)
	   (string-prefix-p (expand-file-name hywiki-directory)
			    buffer-file-name)
	   (setq hywiki-page-flag t))))

;;;###autoload
(defun hywiki-insert-link (&optional arg)
  "Insert at point a HyWiki page#section reference read from the minibuffer.
With optional prefix ARG non-nil, insert a HyWikiWord instead.
Add double quotes if the section contains any whitespace after trimming."
  (interactive "*P")
  (let ((ref (if arg (hywiki-word-read) (hywiki-page-read-reference))))
    (when ref
      (when (string-match-p "\\s-" ref)
	(setq ref (concat "\"" ref "\"")))
      (insert ref)
      (skip-chars-backward "\"")
      (goto-char (1- (point)))
      (hywiki-maybe-highlight-reference))))

;;;###autoload
(defun hywiki-map-words (func)
  "Apply FUNC across highlighted HyWikiWords in the current buffer and return nil.
This temporarily expands the buffer so all HyWikiWord references are processed.
FUNC takes 1 argument, the Emacs overlay for each HyWikiWord reference,
including its optional #section."
  (save-excursion
    (save-restriction
      (widen)
      (mapc func (hproperty:but-get-all-in-region
		  (point-min) (point-max) 'face hywiki-word-face))))
  nil)

(defun hywiki-maybe-dehighlight-balanced-pairs ()
  "Before or after a balanced delimiter, dehighlight HyWikiWords within.
Include: (), {}, <>, [] and \"\" (double quotes).  Exclude Org links
and radio targets.

Range is limited to the previous, current and next lines, as HyWikiWord
references are limited to two lines maximum.

Ignore return value; it has no meaning."
  (save-excursion
    (save-restriction
      (if (hywiki--buttonized-region-p)
	  (narrow-to-region hywiki--buttonize-start hywiki--buttonize-end)
	;; Limit balanced pair checks to two lines around point for speed
	(narrow-to-region (line-beginning-position 0) (line-end-position 2)))

      ;; char-before
      (ignore-errors
	(cond ((memq (char-before) '(?\[ ?\<))
	       (goto-char (1- (point)))
	       ;; Dehighlight HyWikiWords within opening square or angle brackets
	       (hywiki-maybe-dehighlight-org-element-forward))
	      ((memq (char-before) '(?\( ?\{))
	       ;; Dehighlight HyWikiWords within opening parens or braces
	       (goto-char (1- (point)))
	       (hywiki-maybe-dehighlight-sexp 1))
	      ((and (eq (char-before) ?\")
		    (hypb:in-string-p))
	       ;; Dehighlight HyWikiWords in any string following point
	       (goto-char (1- (point)))
	       (hywiki-maybe-dehighlight-sexp 1))
	      ((memq (char-before) '(?\] ?\>))
	       ;; Dehighlight HyWikiWords within closing square or angle brackets
	       (hywiki-maybe-dehighlight-org-element-backward))
	      ((memq (char-before) '(?\) ?\}))
	       ;; Dehighlight HyWikiWords within closing parens or braces
	       (hywiki-maybe-dehighlight-sexp -1))
	      ((and (eq (char-before) ?\")
		    (not (hypb:in-string-p)))
	       ;; Dehighlight HyWikiWords in any string preceding point
	       (hywiki-maybe-dehighlight-sexp -1))))

      ;; char-after
      (ignore-errors
	(cond ((memq (char-after) '(?\[ ?\<))
	       ;; Dehighlight HyWikiWords within opening square or angle brackets
	       (hywiki-maybe-dehighlight-org-element-forward))
	      ((memq (char-after) '(?\( ?\{))
	       ;; Dehighlight HyWikiWords within opening parens or braces
	       (hywiki-maybe-dehighlight-sexp 1))
	      ((and (eq (char-after) ?\")
		    (hypb:in-string-p))
	       ;; Dehighlight HyWikiWords in any string preceding point
	       (goto-char (1+ (point)))
	       (hywiki-maybe-dehighlight-sexp -1))
	      ((memq (char-after) '(?\] ?\>))
	       (goto-char (1+ (point)))
	       ;; Dehighlight HyWikiWords within double closing square
	       ;; or angle brackets, as these may be links or targets
	       (hywiki-maybe-dehighlight-org-element-backward))
	      ((memq (char-after) '(?\) ?\}))
	       ;; Dehighlight any HyWikiWords within closing parens or braces
	       (goto-char (1+ (point)))
	       (hywiki-maybe-dehighlight-sexp -1))
	      ((and (eq (char-after) ?\")
		    (not (hypb:in-string-p)))
	       ;; Dehighlight HyWikiWords in any string following point
	       (hywiki-maybe-dehighlight-sexp 1)))))))

(defun hywiki-maybe-dehighlight-between-references ()
  "Dehighlight any non-Org link HyWiki page#section between point.
If in a programming mode, must be within a comment or string.  Use
`hywiki-word-face' to dehighlight."
  (cond ((hproperty:overlay-range (point) 'face hywiki-word-face)
	 (hproperty:but-clear-all-in-list
	  (hproperty:but-get-all-in-region (point) (1+ (point))
					   'face hywiki-word-face)))
	((and (nth 0 hywiki--buttonize-range)
	      (nth 1 hywiki--buttonize-range))
	 (hproperty:but-clear-all-in-list
	  (hproperty:but-get-all-in-region
	   (nth 0 hywiki--buttonize-range)
	   (nth 1 hywiki--buttonize-range)
	   'face hywiki-word-face))))

  (cond ((cl-destructuring-bind (start end)
	     (hywiki-at-range-delimiter)
	   (when (and start end)
	     (save-excursion
	       (goto-char (1+ start))
	       (and (hproperty:overlay-range (point) 'face hywiki-word-face)
		    (equal (hywiki-referent-exists-p :range)
			   '(nil nil nil))
		    ;; non-existing wikiword
		    (hywiki-maybe-dehighlight-on-reference)))
	     t)))
	((looking-at "[ \t\n\r\f]")
	 (hywiki-maybe-dehighlight-off-reference)
	 (hywiki-maybe-dehighlight-on-reference))))

(defun hywiki-maybe-dehighlight-off-reference ()
  "Dehighlight any non-Org link HyWiki page#section at or one char before point.
If on a whitespace character or at end of buffer, handle
dehighlighting for any previous word or punctuation.  If
in a programming mode, must be within a comment."
  ;; Dehighlight any page name at point
  (hywiki-maybe-dehighlight-reference
   ;; Flag on-page-name if on a whitespace character
   (or (= (point) (point-max))
       (= (if (char-after) (char-syntax (char-after)) 0) ? ))))

(defun hywiki-maybe-dehighlight-on-reference ()
  "Dehighlight any non-Org link HyWiki page#section at or one char before point.
If not on a whitespace character, handle dehighlighting for any
page/section name or punctuation.  If in a programming mode, must
be within a comment."
  ;; Dehighlight any page name at point
  (hywiki-maybe-dehighlight-reference
   ;; Flag on-page-name if not on a whitespace character
   (and (/= (point) (point-max))
	(/= (if (char-after) (char-syntax (char-after)) 0) ? ))))

(defun hywiki-maybe-dehighlight-org-element-backward ()
  "Dehighlight HyWikiWords within a closing double/single square/angle bracket."
  (hywiki--maybe-de/highlight-org-element-backward #'hywiki-maybe-dehighlight-sexp))

(defun hywiki-maybe-dehighlight-org-element-forward ()
  "Dehighlight HyWikiWords within an opening double/single square/angle bracket."
  (hywiki--maybe-de/highlight-org-element-forward #'hywiki-maybe-dehighlight-sexp))

;;;###autoload
(defun hywiki-maybe-dehighlight-reference (&optional on-reference)
  "Dehighlight any non-Org link HyWiki page#section at or one char before point.
A call to `hywiki-active-in-current-buffer-p' at point must return non-nil or
this function does nothing.

With optional ON-REFERENCE non-nil, assume point is within the page or
section name.  Otherwise, if `pre-command-hook' has set
`hywiki--buttonize-start' `hywiki--buttonize-end' global variables,
use these as the region in which to dehighlight.

Use `hywiki-word-face' to dehighlight."
  (interactive)
  (setq hywiki--start nil
	hywiki--end   nil)
  (when (and (hywiki-active-in-current-buffer-p)
	     (or on-reference
		 (and (characterp last-command-event)
		      (string-match (regexp-quote
				     (char-to-string (char-syntax last-command-event)))
				    " _()<>$.\"'")))
             (not executing-kbd-macro)
             (not noninteractive))
    (setq hywiki--highlighting-done-flag nil)
    (with-syntax-table hbut:syntax-table
      (save-excursion
	(save-restriction
	  (when (hywiki--buttonized-region-p)
	    (narrow-to-region hywiki--buttonize-start hywiki--buttonize-end)
	    (goto-char hywiki--buttonize-start))

	  (unless on-reference
	    ;; after page name
	    (skip-syntax-backward ">-"))

	  (hywiki-maybe-dehighlight-balanced-pairs)

	  (unless hywiki--highlighting-done-flag
	    (unless on-reference
	      ;; May be a non-delimiter but HyWikiWord ending punctuation to
	      ;; skip past
	      (skip-chars-backward (hywiki-get-buttonize-characters)))
	    ;; Skip past HyWikiWord or section
	    (skip-syntax-backward "^-$()<>._\"\'")
	    (skip-chars-backward "-_*#:[:alnum:]")

	    (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)
	    (unless (and (hywiki-maybe-at-wikiword-beginning)
			 (looking-at hywiki--word-and-buttonize-character-regexp)
			 (progn
			   (setq hywiki--word-only (match-string-no-properties 2)
				 hywiki--start (match-beginning 1)
				 hywiki--end   (match-end 1))
			   (hywiki-get-referent hywiki--word-only)))
	      ;; Remove any potential earlier highlighting since the
	      ;; previous word may have changed.
	      (skip-syntax-backward "^-$()<>._\"\'"))

	    (hproperty:but-clear-all-in-list
	     (hproperty:but-get-all-in-region (or hywiki--start (point))
					      (or hywiki--end (1+ (point)))
					      'face hywiki-word-face))))))))

;;;###autoload
(defun hywiki-maybe-dehighlight-references (&optional region-start region-end)
  "Dehighlight any highlighted HyWiki page names in a HyWiki buffer/region.
With optional REGION-START and REGION-END positions (active region
interactively), limit dehighlighting to the region.

Does nothing if either `hywiki-buffer-highlighted-state' is set to \='d
or a call to `hywiki-active-in-current-buffer-p' at point returns non-nil."
  (interactive (when (use-region-p) (list (region-beginning) (region-end))))
  (unless (or (eq hywiki-buffer-highlighted-state 'd)
	      (hywiki-active-in-current-buffer-p))
    (hproperty:but-clear-all-in-list
     (hproperty:but-get-all-in-region
      (if (markerp region-start)
	  (if (marker-position region-start)
	      region-start
	    (point-min))
	(or region-start (point-min)))
      (if (markerp region-end)
	  (if (marker-position region-end)
	      region-end
	    (point-max))
	(or region-end (point-max)))
      'face hywiki-word-face))
    (unless (or region-start region-end)
      (setq hywiki-buffer-highlighted-state 'd))))

(defun hywiki-maybe-dehighlight-sexp (direction-number)
  "Dehighlight any HyWikiWord within single square/angle bracket.
DIRECTION-NUMBER is 1 for forward scanning and -1 for backward scanning."
  ;; Enable dehighlighting in HyWiki pages
  (let ((hywiki-mode))
    (hywiki--maybe-de/highlight-sexp
     #'hywiki-maybe-dehighlight-references direction-number)))

;;;###autoload
(defun hywiki-maybe-highlight-balanced-pairs ()
  "Before or after a balanced delimiter, highlight HyWikiWords within.
Include: (), {}, <>, [] and \"\" (double quotes).  Exclude Org links
and radio targets.

Range is limited to the previous, current and next lines, as HyWikiWord
references are limited to two lines maximum.

Return t if no errors and a pair was found, else nil."
  (save-excursion
    (save-restriction
      (if (hywiki--buttonized-region-p)
	  (narrow-to-region hywiki--buttonize-start hywiki--buttonize-end)
	;; Limit balanced pair checks to two lines around point for speed
	(narrow-to-region (line-beginning-position 0) (line-end-position 2)))

      (let ((result t))
	(condition-case nil
	    ;; char-before
	    (cond ((memq (char-before) '(?\[ ?\<))
		   (goto-char (1- (point)))
		   ;; Highlight any HyWikiWords within single opening
		   ;; square or angle brackets
		   ;; Dehighlight HyWikiWords within double opening square
		   ;; or angle brackets, as these are Org links and targets
		   (hywiki-maybe-highlight-org-element-forward))
		  ((memq (char-before) '(?\( ?\{))
		   ;; Highlight any HyWikiWords within opening parens or braces
		   (goto-char (1- (point)))
		   (hywiki-maybe-highlight-sexp 1))
		  ((and (eq (char-before) ?\")
			(hypb:in-string-p))
		   (goto-char (1- (point)))
		   (hywiki-maybe-highlight-sexp 1))
		  ((memq (char-before) '(?\] ?\>))
		   ;; Dehighlight HyWikiWords within double closing square
		   ;; or angle brackets, as these are Org links and targets
		   (hywiki-maybe-highlight-org-element-backward))
		  ((memq (char-before) '(?\) ?\}))
		   ;; Highlight any HyWikiWords within closing parens or braces
		   (hywiki-maybe-highlight-sexp -1))
		  ((and (eq (char-before) ?\")
			(not (hypb:in-string-p)))
		   ;; Highlight HyWikiWords in any string preceding point
		   (hywiki-maybe-highlight-sexp -1))
		  (t (setq result nil)))
	  (error (setq result nil)))

	(when result
	  (condition-case nil
              (with-syntax-table hbut:syntax-table
	        ;; char-after
	        (cond ((memq (char-after) '(?\[ ?\<))
		       ;; Highlight any HyWikiWords within single opening
		       ;; square or angle brackets
		       ;; Dehighlight HyWikiWords within double opening square
		       ;; or angle brackets, as these are Org links and targets
		       (hywiki-maybe-highlight-org-element-forward))
		      ((memq (char-after) '(?\( ?\{))
		       ;; Highlight any HyWikiWords within opening parens or braces
		       (hywiki-maybe-highlight-sexp 1))
		      ((and (eq (char-after) ?\")
			    (hypb:in-string-p))
		       (goto-char (1+ (point)))
		       (hywiki-maybe-highlight-sexp -1))
		      ((memq (char-after) '(?\] ?\>))
		       (goto-char (1+ (point)))
		       ;; Highlight any HyWikiWords within single closing
		       ;; square or angle brackets
		       ;; Dehighlight HyWikiWords within double closing square
		       ;; or angle brackets, as these are Org links and targets
		       (hywiki-maybe-highlight-org-element-backward))
		      ((memq (char-after) '(?\) ?\}))
		       ;; Highlight any HyWikiWords within closing parens or braces
		       (goto-char (1+ (point)))
		       (hywiki-maybe-highlight-sexp -1))
		      ((and (eq (char-after) ?\")
			    (not (hypb:in-string-p)))
		       ;; Highlight HyWikiWords in any string following point
		       (hywiki-maybe-highlight-sexp 1))
		      (t (setq result nil))))
	    (error (setq result nil))))
	(when result t)))))

(defun hywiki-maybe-highlight-between-references ()
  "Highlight any non-Org link HyWiki page#section names between point.

If in a programming mode, must be within a comment.  Use
`hywiki-word-face' to highlight.  Do not highlight references to
the current page unless they have sections attached."
  (cond ((hproperty:overlay-range (point) 'face hywiki-word-face))
	((cl-destructuring-bind (word start end)
	     (hywiki-highlight-word-get-range)
	   (when (and start end)
	     (save-excursion
	       (goto-char start)
	       (when (hywiki-referent-exists-p word)
		 ;; existing wikiword
		 (hywiki-maybe-highlight-on-reference)))
	     t)))
	((cl-destructuring-bind (start end)
	     (hywiki-at-range-delimiter)
	   (when (and start end)
	     (save-excursion
	       (goto-char (1+ start))
	       (skip-syntax-forward "-" (line-end-position))
	       (unless (equal (hywiki-referent-exists-p :range)
			      '(nil nil nil))
		 ;; existing wikiword
		 (hywiki-maybe-highlight-on-reference)))
	     t)))
	((looking-at "[ \t\n\r\f]")
	 (hywiki-maybe-highlight-off-reference)
	 (hywiki-maybe-highlight-on-reference))
	(t (hywiki-maybe-highlight-on-reference))))

(defun hywiki-maybe-highlight-off-reference ()
  "Highlight any non-Org link HyWiki page#section at or one char before point.
If at bobp or any preceding char is non-whitespace and any following
character is whitespace or at eobp, handle highlighting for any previous
word or punctuation.

If in a programming mode, must be within a comment.  Use
`hywiki-word-face' to highlight.  Do not highlight references to
the current page unless they have sections attached."
  (hywiki-maybe-highlight-reference
   ;; flag on-reference if on a whitespace character
   (and (or (= (point) (point-max))
	    (= (if (char-after) (char-syntax (char-after)) 0) ?\ ))
	(or (= (point) (point-min))
	    (/= (if (char-before) (char-syntax (char-before)) 0) ?\ )))))

(defun hywiki-maybe-highlight-on-reference ()
  "Highlight any non-Org link HyWiki page#section at or one char before point.
If not on a whitespace character, handle highlighting for any page/section
name or punctuation.

If in a programming mode, must be within a comment.  Use
`hywiki-word-face' to highlight.  Do not highlight references to
the current page unless they have sections attached."
  (hywiki-maybe-highlight-reference
   ;; flag on-reference if not on a whitespace character
   (and (/= (point) (point-max))
	(/= (if (char-after) (char-syntax (char-after)) 0) ? ))))

(defun hywiki-maybe-highlight-org-element-backward ()
  "Highlight HyWikiWords with point at a single closing square/angle bracket.
Dehighlight HyWikiWords when on a double closing square/angle bracket,
since Org mode highlights those."
  (hywiki--maybe-de/highlight-org-element-backward #'hywiki-maybe-highlight-sexp))

(defun hywiki-maybe-highlight-org-element-forward ()
  "Highlight HyWikiWords with point at a single opening square/angle bracket.
Dehighlight HyWikiWords when on a double opening square/angle bracket,
since Org mode highlights those."
  (hywiki--maybe-de/highlight-org-element-forward #'hywiki-maybe-highlight-sexp))

;;;###autoload
(defun hywiki-maybe-highlight-reference (&optional on-reference)
  "Highlight any non-Org link HyWikiWord#section at or one char before point.
A call to `hywiki-active-in-current-buffer-p' at point must return non-nil or
this function does nothing.

With optional ON-REFERENCE non-nil, assume point is within the page or
section name.  Otherwise, if a HyWiki per-character hook has set
`hywiki--buttonize-start' `hywiki--buttonize-end' global variables,
use these as the region to highlight.

If in a programming mode, must be within a comment.  Use
`hywiki-word-face' to highlight.  Do not highlight references to
the current page unless they have sections attached."
  (interactive)
  (when (and (hywiki-active-in-current-buffer-p)
	     (if (and (derived-mode-p 'prog-mode)
		      (not (apply #'derived-mode-p hywiki-highlight-all-in-prog-modes)))
		 ;; Non-nil if match is inside a comment or string
		 (or (nth 4 (syntax-ppss)) (hypb:in-string-p))
	       t)
	     ;;  (or on-reference
	     ;;	 (string-match (regexp-quote (char-to-string (char-syntax last-command-event)))
	     ;;		       " _()<>$.\"'"))
             (not executing-kbd-macro)
             (not noninteractive))
      (setq hywiki--highlighting-done-flag nil)
      (with-syntax-table hbut:syntax-table
	(save-excursion
	  (when (hywiki--buttonized-region-p)
	    (goto-char hywiki--buttonize-start))

	  (unless on-reference
	    ;; after page name
	    (skip-syntax-backward ">-"))

	  (unless (or hywiki--highlighting-done-flag
 		      (hywiki-maybe-highlight-balanced-pairs))

	    (unless on-reference
	      ;; May be a non-delimiter but HyWikiWord ending punctuation to
	      ;; skip past
	      (skip-chars-backward (hywiki-get-buttonize-characters)
				   (line-beginning-position)))
	    ;; Skip past HyWikiWord or section
	    (skip-syntax-backward "^-$()<>._\"\'")
	    (skip-chars-backward "-_*#:[:alnum:]")

	    (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
		  hywiki--start nil
		  hywiki--end   nil)

	    (if (and (cl-destructuring-bind (word start end)
			 (hywiki-highlight-word-get-range)
		       (setq hywiki--word-only word
			     hywiki--start start
			     hywiki--end end))
		     hywiki--start
		     (hywiki-get-referent hywiki--word-only)
		     (goto-char hywiki--start))
		(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--buts (hproperty:but-get-all-in-region
					    hywiki--start hywiki--end
					    'face hywiki-word-face))
			(if (> (length hywiki--buts) 1)
			    (progn (hproperty:but-clear-all-in-list hywiki--buts)
				   (hywiki-maybe-highlight-references
				    hywiki--start hywiki--end))
			  ;; There is only one existing button
			  (setq hywiki--buts (car hywiki--buts)
				hywiki--but-start (hproperty:but-start hywiki--buts)
				hywiki--but-end   (hproperty:but-end hywiki--buts))
			  (unless (and (= hywiki--start hywiki--but-start)
				       (= hywiki--end hywiki--but-end))
			    (hproperty:but-delete hywiki--buts)
			    (hywiki-maybe-highlight-references
			     hywiki--start hywiki--end)))
		      (hywiki-maybe-highlight-references
		       hywiki--start hywiki--end))))
	      ;; Remove any potential earlier highlighting since the
	      ;; previous word may have changed.
	      (skip-syntax-backward "^-$()<>._\"\'")
	      (when (setq hywiki--buts (hproperty:but-get-all-in-region
					(point) (1+ (point)) 'face hywiki-word-face))
		(if (> (length hywiki--buts) 1)
		    (hproperty:but-clear-all-in-list hywiki--buts)
		  ;; There is only one existing button
		  (setq hywiki--buts (car hywiki--buts)
			hywiki--but-start (hproperty:but-start hywiki--buts)
			hywiki--but-end   (hproperty:but-end hywiki--buts))
		  (hproperty:but-delete hywiki--buts)))))))))

(defun hywiki-maybe-highlight-references (&optional region-start region-end skip-lookups-update-flag)
  "Highlight each non-Org link HyWiki page#section in the current buffer/region.
With optional REGION-START and REGION-END positions or markers (active
region interactively), limit highlight adjustment to the region.  With
optional SKIP-LOOKUPS-UPDATE-FLAG non-nil, HyWiki lookup tables should have
already been updated and this is skipped.

Called by `find-file' without a region to highlight HyWikiWords when a file
buffer is first read in to an Emacs session.

Use `hywiki-word-face' to highlight.  Do not highlight references
to the current page unless they have sections attached.

HyWiki mode must be active in the current buffer for highlighting
to occur; otherwise, highlighting is removed and disabled in the
current buffer.  Highlight/dehighlight HyWiki page buffers
whenever `hywiki-mode' is enabled/disabled."
  (interactive (when (use-region-p) (list (region-beginning) (region-end))))
  ;; Avoid doing many lets for efficiency.
  ;; Highlight HyWikiWords throughout buffers where `hywiki-mode' is enabled
  ;; or HyWiki pages below `hywiki-directory' whenever displayed in a window.
  (if (hywiki-active-in-current-buffer-p)
      (progn
	(unless skip-lookups-update-flag
	  ;; Rebuild lookup tables if any HyWiki page name has changed
	  (hywiki-get-referent-hasht))
	(unwind-protect
	    (save-excursion
	      (save-restriction
		(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
		      hywiki--current-page (hywiki-get-buffer-page-name))
		(cond ((and (markerp region-start) (markerp region-end))
		       (when (and (marker-position region-start)
				  (marker-position region-end))
			 (narrow-to-region region-start region-end)))
		      ((and region-start region-end)
		       (narrow-to-region region-start region-end)))
		;; Enable dehighlighting in HyWiki pages only when
		;; whole buffer is being processed; this prevents an
		;; error when called from `hywiki-maybe-highlight-sexp'.
		(unless (and region-start region-end)
		  (let ((hywiki-mode))
		    (hywiki-maybe-dehighlight-references)))
		(let ((highlight-in-comments-and-strings-only
		       (and (derived-mode-p 'prog-mode)
			    (not (apply #'derived-mode-p hywiki-highlight-all-in-prog-modes))))
		      hywiki--start
		      hywiki--end)
		  (dolist (hywiki-words-regexp hywiki--any-wikiword-regexp-list)
		    (goto-char (point-min))
		    (while (re-search-forward hywiki-words-regexp nil t)
		      (setq hywiki--start (match-beginning 1)
			    hywiki--end   (match-end 1))
		      (save-excursion
			(goto-char hywiki--start)
			(when (save-match-data
				(if highlight-in-comments-and-strings-only
				    ;; Non-nil if match is inside a comment or a string
				    (or (nth 4 (syntax-ppss)) (hypb:in-string-p))
				  t))
			  ;; Otherwise, highlight any HyWikiWord found, including
			  ;; any #section:Lnum:Cnum.
			  (when (hywiki-maybe-at-wikiword-beginning)
			    (or (unless (hyperb:stack-frame '(hywiki-maybe-highlight-balanced-pairs))
				  (hywiki-maybe-highlight-balanced-pairs))
				(progn (with-syntax-table hbut:syntax-table
					 (skip-syntax-forward "^-\)$\>._\"\'"))
				       (skip-chars-forward "-_*[:alnum:]")
				       (unless (zerop (skip-chars-forward "#:"))
					 (skip-chars-forward
                                          (if (save-restriction
						(widen)
                                                (and (hywiki-delimited-p)
                                                     ;; Only if delimiter is
                                                     ;; the char preceding
                                                     ;; the start of the
                                                     ;; WikiWord do we skip
                                                     ;; over spaces to find
                                                     ;; the #section.
                                                     (hash-get
						      (char-to-string
                                                       (or (char-before (or hywiki--start 0))
                                                           0))
                                                      hywiki--open-close-hasht)))
					      "-_*: \t[:alnum:]"
					    "-_*:[: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))
					 (hywiki-maybe-highlight-region-reference hywiki--start hywiki--end))))))))))

		;; Disable dehighlighting of HyWikiWords between [] and <>.
		;;
		;; (let (str-start-end)
		;;   (goto-char (point-min))
		;;   (while (search-forward "[" nil t)
		;;     (when (setq str-start-end (hargs:delimited-p "[" "]" nil nil t))
		;;       (setq hywiki--start (nth 1 str-start-end)
		;; 	    hywiki--end   (nth 2 str-start-end))
		;;       ;; Clear any HyWikiWord highlighting that may
		;;       ;; just be a part of a larger square brackets
		;;       ;; delimited text with multiple words.
		;;       (hproperty:but-clear-all-in-list
		;;        (hproperty:but-get-all-in-region hywiki--start hywiki--end
		;; 					'face hywiki-word-face))
		;;       (goto-char (min (1+ hywiki--end) (point-max)))))

		;;   (goto-char (point-min))
		;;   (while (search-forward "<" nil t)
		;;     (when (setq str-start-end (hargs:delimited-p "<" ">" nil nil t))
		;;       (setq hywiki--start (nth 1 str-start-end)
		;; 	    hywiki--end   (nth 2 str-start-end))
		;;       ;; Clear any HyWikiWord highlighting that may
		;;       ;; just be a part of a larger angle brackets
		;;       ;; delimited text with multiple words.
		;;       (hproperty:but-clear-all-in-list
		;;        (hproperty:but-get-all-in-region hywiki--start hywiki--end
		;; 					'face hywiki-word-face))
		;;       (goto-char (min (1+ hywiki--end) (point-max))))))

		(unless (and region-start region-end
			     (or (/= region-start (point-min))
				 (/= region-end   (point-max))))
		  (setq hywiki-buffer-highlighted-state 'h))))
	  (setq case-fold-search hywiki--save-case-fold-search
		hywiki-org-link-type-required hywiki--save-org-link-type-required)))

    ;; Otherwise, dehighlight HyWikiWords in this buffer when
    ;; 'hywiki-mode' is disabled or set to ':pages' and this is not a
    ;; HyWiki page buffer. If this is a HyWiki page buffer, then
    ;; dehighlight when `hywiki-mode' is disabled.
    (hywiki-maybe-dehighlight-references region-start region-end))
  (unless (hyperb:stack-frame '(hywiki-maybe-highlight-wikiwords-in-frame))
    (hywiki-maybe-directory-updated))
  nil)

(defun hywiki-maybe-highlight-region (start end)
  "Rehighlight HyWikiWord references between positions START to END."
  (hywiki-maybe-highlight-references start end t)
  (unless (hyperb:stack-frame '(hywiki-maybe-highlight-wikiwords-in-frame))
    ;; Rebuild lookup tables if any HyWiki page name has changed
    (hywiki-get-referent-hasht)
    (hywiki-maybe-directory-updated)))

(defun hywiki-maybe-highlight-region-reference (start end)
  "Conditionally highlight HyWiki reference between START and END.
Do not highlight if any face from `hywiki-ignore-face-list' appears
within the given region, e.g. ignore HyWikiWords used in Org links or
Hyperbole button names."
  (unless (hproperty:but-face-p start hywiki-ignore-face-list)
    (hproperty:but-add start end hywiki-word-face)))

(defun hywiki-maybe-highlight-sexp (direction-number)
  "Highlight any HyWikiWord within single square/angle bracket.
DIRECTION-NUMBER is 1 for forward scanning and -1 for backward scanning."
  (hywiki--maybe-de/highlight-sexp
   #'hywiki-maybe-highlight-references direction-number))

(defun hywiki-maybe-highlight-wikiwords-in-frame (frame &optional skip-lookups-update-flag)
  "Highlight all non-Org link HyWiki references displayed in FRAME.
Do not highlight references to the current page unless they have
sections attached.

If FRAME is t, then highlight in all windows across all frames, even
invisible ones.  With optional SKIP-LOOKUPS-UPDATE-FLAG non-nil, HyWiki
lookup tables should have already been updated and this is skipped.

Use `hywiki-word-face' to highlight."
  (walk-windows
   (lambda (window)
     ;; Skip child/popup/posframe windows; use only top-level frame windows
     (unless (frame-parent (window-frame window))
       (with-selected-window window
         ;; Display buffer before `normal-mode' triggers possibly
         ;; long-running font-locking
         (sit-for 0)
         (hywiki-maybe-highlight-references nil nil skip-lookups-update-flag))))
   nil frame)
  (hywiki-maybe-directory-updated))

(defun hywiki-make-referent-hasht ()
  "Rebuld referent hasht from list of HyWiki page files and non-page entries."
  (setq hywiki--any-wikiword-regexp-list nil
	hywiki--pages-directory hywiki-directory)
  ;; Try to load from a .hywiki.eld cache file if up-to-date
  (let* ((cache-file (hywiki-cache-default-file))
	 (cache-buffer (when (file-readable-p cache-file)
			 (find-file-noselect cache-file)))
	 (hywiki-loaded-flag (when cache-buffer
			       (with-current-buffer cache-buffer
				 (widen)
				 (goto-char (point-min))
				 ;; Skip past initial comments
				 (when (re-search-forward "^(" nil t)
				   (goto-char (1- (point)))
				   (condition-case ()
				       (progn (eval (read (buffer-string)))
					      t)
				     (error nil)))))))
    (if (and hywiki-loaded-flag (not (hywiki-directory-modified-p)))
	;; Rebuild from loaded data
        (prog1 (setq hywiki--referent-hasht (hash-make hywiki--referent-alist t))
	  (setq hywiki--referent-alist nil))
      ;; Read `hywiki-directory' for current page files and merge with
      ;; non-page referents
      (let* ((page-files (hywiki-get-page-files))
	     (non-page-elts (when (hash-table-p hywiki--referent-hasht)
			      (delq nil
				    (hash-map 'hywiki-non-page-elt
					      hywiki--referent-hasht))))
	     (non-page-hasht (hash-make non-page-elts))
	     (key)
	     (page-elts (delq nil (mapcar (lambda (file)
					    (setq key (file-name-sans-extension file))
					    (unless (hash-get key non-page-hasht)
					      (cons (cons 'page file) key)))
					  page-files)))
             (page-hasht (hash-make page-elts)))
	(setq hywiki--referent-hasht
	      (if non-page-elts
 		  (hash-merge non-page-hasht page-hasht)
		page-hasht))))))


(defun hywiki-non-page-elt (val-key)
  (unless (eq (caar val-key) 'page)
    val-key))

(defun hywiki-org-directory-todo-regexp (dir)
  "Scan .org files in DIR for #+TODO keyword lines; return a matching regexp.
This includes both standard and custom todo keywords.  Does not descend into
subdirectories."
  ;; Use grep, sort and uniq to find all unique #+TODO lines in the directory;
  ;; use -h to omit filenames
  (let ((grep-output
         (shell-command-to-string
          (format "grep -h '^#+TODO:' %s | sort | uniq"
		  (expand-file-name "*.org" dir))))
	keywords)
    ;; Remove #+TODO: markup
    (setq grep-output (replace-regexp-in-string "^#\\+\\(SEQ_\\|TYP_\\)?TODO:[ \t]*" "" grep-output))

    ;; Remove quick keys and other annotations, as well as alternative pipes
    (setq grep-output (replace-regexp-in-string "|\\|([^)]*)" "" grep-output))

    ;; Split into individual todo keywords
    (setq keywords (nconc (with-temp-buffer (org-mode) org-todo-keywords-1)
                          (split-string grep-output "[ \t\n\r]" t)))

    ;; Create the regexp to match to all todo keywords
    (when keywords (format "\\(%s\\)" (regexp-opt keywords)))))

(defun hywiki-org-get-titles-from-headings (headings)
  "Given a list of Org HEADINGS, return only the titles.
Works even when called from non-Org buffers."
  (delq nil (mapcar (lambda (heading)
                      (and heading
                           (string-match hywiki--org-heading-regexp heading)
                           (hpath:org-normalize-title
                            ;; raw title
                            (match-string 4 heading))))
                    headings)))

(defun hywiki-org-to-heading-instance (title &optional n)
  "To the heading whose TITLE is the optional Nth instance in an Org buffer.
If such an instance is not found, trigger an error."
  (interactive "sHeading Title: \nnInstance: ")
  (unless (wholenump n)
    (setq n 1))
  (let ((found nil)
        (exact-heading-regexp (hywiki-org-get-heading-match-regexp title)))
    (save-excursion
      (goto-char (point-min))
      ;; Search for exact heading and then extract the title
      (when (re-search-forward exact-heading-regexp nil t n)
        (setq found (line-beginning-position))))
    (if found
        (progn
          (goto-char found)
          ;; Ensure the heading is visible if folded
          (if (version< org-version "9.6")
              (with-suppressed-warnings ((obsolete org-show-entry))
                (org-show-entry))
            (org-fold-show-entry))
          ;; (message "Instance %d of '%s'" n title)
          t)
      (error "(hywiki-org-to-heading-instance): Could not find %d instance(s) of '%s' in \"%s\""
             n title (or buffer-file-name (current-buffer))))))

(defun hywiki-page-read-reference (&optional prompt initial)
  "With consult package loaded, read a \"page^@line\" string, else a page name.
May return nil if no item is selected."
  (interactive)
  (if (featurep 'consult)
      (hywiki-format-grep-to-reference (hywiki-consult-page-and-headline
                                        prompt
                                        (hywiki-format-reference-to-consult-grep
                                         initial)))
    ;; Without consult, can only complete to a HyWiki page without a section.
    (hywiki-page-read prompt initial)))

(defun hywiki--sitemap-file ()
  "Return file name for the sitemap file."
  (expand-file-name
   (org-publish-property :sitemap-filename (hywiki-org-get-publish-project))
   (org-publish-property :base-directory (hywiki-org-get-publish-project))))

(defun hywiki-org-export-function (&rest _)
  "Convert HyWikiWord links to Org links and add title if missing.
Do not convert the index file."
  (require 'org-element)
  (when (and (derived-mode-p 'org-mode)
             (not (string= (hywiki--sitemap-file) (buffer-file-name))))
    (hywiki-references-to-org-links)
    (hywiki-org-maybe-add-title)))

(if (version< org-version "9.6")
;;; For Org less than 9.6; derived from `org-get-heading' in "org.el"
;;;###autoload
(defun hywiki-org-format-heading (heading &optional no-tags no-todo
                                          no-priority no-comment no-stats)
  "Return HEADING, without the leading asterisks or a #+TITLE:.
When NO-TAGS is non-nil, don't include tags.
When NO-TODO is non-nil, don't include TODO keywords.
When NO-PRIORITY is non-nil, don't include priority cookie.
When NO-COMMENT is non-nil, don't include COMMENT string.
When NO-STATS is non-nil, don't include statistics in square brackets."
  (when (stringp heading)
    (let ((case-fold-search nil))
      (when (string-match hywiki--org-heading-regexp heading)
        (let ((todo (and (not no-todo) (match-string 2 heading)))
	      (priority (and (not no-priority) (match-string 3 heading)))
	      (headline (pcase (match-string 4 heading)
			  (`nil "")
			  ((and (guard no-comment) h)
			   (replace-regexp-in-string
			    (eval-when-compile
			      (format "\\`%s[ \t]+" org-comment-string))
			    "" h))
			  (h h)))
	      (tags (and (not no-tags) (match-string 5 heading))))

          (when no-stats
            ;; Strip trailing statistics cookies [1/2] or [50%]
            (setq headline
                  (replace-regexp-in-string
                   "\\(?: +\\[[0-9%+/]+\\]\\)+" "" headline)))

	  (mapconcat #'identity
		     (delq nil (list todo priority headline tags))
		     " "))))))

;;; Else
;;; For Org 9.6 and greater; derived from `org-get-heading' in "org.el"
;;;###autoload
(defun hywiki-org-format-heading (heading &optional no-tags no-todo
                                          no-priority no-comment no-stats)
  "Return HEADING, without the leading asterisks or a #+TITLE:.
When NO-TAGS is non-nil, don't include tags.
When NO-TODO is non-nil, don't include TODO keywords.
When NO-PRIORITY is non-nil, don't include priority cookie.
When NO-COMMENT is non-nil, don't include COMMENT string.
When NO-STATS is non-nil, don't include statistics in square brackets."
  (when (stringp heading)
    (let ((case-fold-search nil))
      (when (string-match hywiki--org-heading-regexp heading)
        ;; When using `org-fold-core--optimise-for-huge-buffers',
        ;; returned text will be invisible.  Clear it up.
        (save-match-data
          (if (fboundp 'org-fold-core-remove-optimization)
              (org-fold-core-remove-optimization (match-beginning 0) (match-end 0))
            (with-suppressed-warnings ((obsolete org-fold-core-remove-optimisation))
              (org-fold-core-remove-optimisation (match-beginning 0) (match-end 0)))))
        (let ((todo (and (not no-todo) (match-string 2 heading)))
	      (priority (and (not no-priority) (match-string 3 heading)))
	      (headline (pcase (match-string 4 heading)
			  (`nil "")
			  ((and (guard no-comment) h)
			   (replace-regexp-in-string
			    (eval-when-compile
			      (format "\\`%s[ \t]+" org-comment-string))
			    "" h))
			  (h h)))
	      (tags (and (not no-tags) (match-string 5 heading))))

          (when no-stats
            ;; Strip trailing statistics cookies [1/2] or [50%]
            (setq headline
                  (replace-regexp-in-string
                   "\\(?: +\\[[0-9%+/]+\\]\\)+" "" headline)))

          ;; Restore cleared optimization.
          (if (fboundp 'org-fold-core-remove-optimization)
              (org-fold-core-remove-optimization (match-beginning 0) (match-end 0))
            (with-suppressed-warnings ((obsolete org-fold-core-remove-optimisation))
              (org-fold-core-remove-optimisation (match-beginning 0) (match-end 0))))
	  (mapconcat #'identity
		     (delq nil (list todo priority headline tags))
        	     " "))))))
)

(defun hywiki-org-get-heading-match-regexp (title)
  "Return a regexp that matches to the TITLE and start of an Org heading."
  ;; org-complex-heading-regexp + custom todo keywords + specific title
  (format (concat "^\\(\\*+[ \t]+\\)"
                  ;; optional todo keyword
	          "\\(?:"
                  (if (hywiki-in-page-p)
                      hywiki--org-todo-regexp
                    org-todo-regexp)
                  "\\)?"
                  ;; optional priority
	          "\\(?:[ \t]*\\(\\[#.\\]\\)\\)?"
                  ;; title and optional stats
	          "\\(?:[ \t]*\\(%s\\)\\)")
          ;; exact title in diff formats
          (hywiki-get-term-variant-regexp title)))

(defun hywiki-org-get-publish-project ()
  "Return the HyWiki Org publish project, a named set of properties.
If not found, set it up and return the new project properties."
  (require 'ox-publish)
  (let ((project (assoc "hywiki" org-publish-project-alist)))
    (if (and project hywiki-org-publish-project-alist)
	project
      (hywiki-org-set-publish-project))))

(defun hywiki-org-get-publish-property (property)
  "Return the value of HyWiki Org publish PROPERTY symbol."
  (require 'ox-publish)
  (org-publish-property property (hywiki-org-get-publish-project)))

(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 ":"))
   (hywiki-word-read)))

(defun hywiki--org-link-html-format (path-stem suffix desc info)
  "Format an html link using Org ids."
  (let* ((raw-heading (and suffix (not (string-empty-p suffix)) (substring suffix 1)))
         (heading (and raw-heading (hpath:dashes-to-spaces-markup-anchor raw-heading)))
         (link-obj (org-element-create
                    'link
                    (list
                     :type "file"
                     :path (concat path-stem ".org")
                     :search-option (and heading (concat "*" heading))
                     :format 'bracket)))
         ;; Export as HTML
         (exported (org-export-data link-obj info))
         ;; Extract the href
         (href (if (string-match "href=\"\\([^\"]+\\)\"" exported)
                   (match-string 1 exported)
                 exported)))
    (format "<a href=\"%s\">%s</a>" href desc)))

;;; Next two functions derived from the denote package.
;;;###autoload
(defun hywiki-org-link-export (link description format info)
  "Export a HyWikiWord Org-format `hy:' link to various formats.
The LINK, DESCRIPTION, FORMAT and INFO are provided by the export
backend."
  (let* ((path-word-suffix (hywiki-reference-to-referent link :full-data))
         (path (when path-word-suffix
		 (file-relative-name (nth 0 path-word-suffix))))
         (path-stem (when path
		      (file-name-sans-extension path)))
         (word (nth 1 path-word-suffix))
         (suffix (nth 2 path-word-suffix))
         (desc (cond (description)
                     (suffix (when word
			       (format "%s%s" word suffix)))
                     (word)
		     (t ""))))
    (if path
	(pcase format
	  (`ascii (format "[%s] <%s:%s>" hywiki-org-link-type desc path))
	  (`html (hywiki--org-link-html-format path-stem suffix desc info))
	  (`latex (format "\\href{%s.latex}{%s}" (replace-regexp-in-string "[\\{}$%&_#~^]" "\\\\\\&" path-stem) desc))
	  (`md (format "[%s](%s.md%s)" desc path-stem
		       (hpath:spaces-to-dashes-markup-anchor
			(or suffix ""))))
	  (`texinfo (format "@uref{%s.texi,%s}" path-stem desc))
	  (_ path))
      link)))

(defun hywiki-org-link-store ()
  "Store a link to a HyWikiWord at point, if any."
  (when (hywiki-word-at)
    (let* ((page-name (hywiki-word-at))
	   (link (concat
		  (when hywiki-org-link-type-required
		    (concat hywiki-org-link-type ":"))
		  page-name)))
      (org-link-store-props
       :type hywiki-org-link-type
       :link link
       :description page-name))))

(defun hywiki-org-maybe-add-title ()
  "Add a title to an Org buffer if it doesn't have one."
  (save-excursion
    (unless (and (re-search-forward "^#\\+TITLE:[ \t]\\|^$" nil t)
		 (not (looking-at "^$")))
      (goto-char (point-min))
      (insert "#+TITLE: "
	      (if (hypb:buffer-file-name)
		  (file-name-base (hypb:buffer-file-name))
		(buffer-name))
	      "\n"))))

(defun hywiki-org-set-publish-project ()
  "Setup and return the HyWiki Org publish project, a named set of properties.
Sets the `org-publish-project-alist' and `hywiki-org-publish-project-alist'
variables."
  (require 'ox-publish)
  (prog1 (hywiki-org-make-publish-project-alist)
    ;; Remove "hywiki" entry from `org-publish-project-alist', then update it.
    (setf (alist-get "hywiki" org-publish-project-alist nil 'remove #'equal) nil)
    (add-to-list 'org-publish-project-alist hywiki-org-publish-project-alist t)))

(defun hywiki-page-exists-p (word)
  "Return HyWiki WORD iff it is an existing page reference."
  (and (stringp word) (not (file-name-directory word))
       (eq (car (hywiki-get-referent word)) 'page)
       word))

(defun hywiki-page-read (&optional prompt initial)
  "Prompt with completion for and return an existing HyWiki page name.
If point is on one, press RET immediately to use that one."
  (let* ((completion-ignore-case t)
         (wikiword (or initial (hywiki-word-at-point)))
         (page (hywiki-page-exists-p wikiword)))
    (completing-read (if (stringp prompt) prompt "HyWiki Page: ")
		     (hywiki-get-page-list)
		     nil t initial nil (when page wikiword))))

(defun hywiki-page-read-new (&optional prompt initial)
  "Prompt with completion for and return an existing/new HyWiki page name.
If point is on one, press RET immediately to use that one."
  (let ((completion-ignore-case t)
        (valid-p (lambda (input)
                   (let ((case-fold-search nil))
                     (string-match-p (concat "\\`" hywiki-word-regexp "\\'") input))))
	(collection (hywiki-get-page-list)))
    (completing-read
     (if (stringp prompt) prompt "HyWiki Page: ")
     (lambda (input pred action)
       (if (eq action 'lambda)
           ;; Check that input matches `hyrolo-word-regexp'.
           ;; If so, RET will exit the minibuffer.
           (funcall valid-p input)
         ;; Otherwise, complete over `collection'
         (complete-with-action action collection input pred)))
     nil t initial nil (hywiki-word-at-point))))

(defun hywiki-publish-to-html (&optional all-pages-flag)
  "Publish/export updated HyWiki pages to html.
With an optional prefix arg, ALL-PAGES-FLAG, regenerate all html
pages rather than only those HyWiki pages which have changed
since a prior publish.

Files are saved in:
    (hywiki-org-get-publish-property :publishing-directory)
Customize this directory with:
    {\\`M-x' `customize-variable' RET hywiki-org-publishing-directory RET}."
  (interactive "P")
  ;; Export Org to html with useful link ids.
  ;; Instead of random ids like "orga1b2c3", use heading titles with
  ;; spaces replaced with dashes, made unique when necessary.
  (org-publish-project "hywiki" all-pages-flag))

(defun hywiki-read-headline-regexp (operation-name)
  "Read (with consult if available) a headling matching regexp.
Use OPERATION-NAME in read prompt."
  (if (hsys-consult-active-p)
      ;; Assume only .org files; adjust if later support .md markdown filex
      (let ((hsys-consult-entry-regexp "^\\*+ "))
	(substring-no-properties
	 (hsys-consult-get-exit-value
	  nil
	  #'hsys-consult-grep-headlines-with-prompt
	  #'hyrolo-consult-grep
	  (format "%s HyWiki section with matching headline"
                  operation-name)
	  nil
	  (list hywiki-directory))))
    (read-regexp (format "%s HyWiki section with matching headline"
                         operation-name))))

(defun hywiki-reference-to-referent (reference &optional full-data)
  "Resolve HyWikiWord REFERENCE to its referent file or other type of referent.
File paths are made absolute.  If the referent is not a file type,
return (referent-type . referent-value).

Otherwise:
Reference may end with optional suffix of the form: (#|::)section:Lnum:Cnum.
With optional FULL-DATA non-nil, return a list in the form of (pathname
hywikiword suffix); otherwise:
  - with a section, return pathname::section;
  - with just line and optionally column numbers, return pathname:Lnum:Cnum
  - and without any suffix, return just the pathname."
  (when (stringp reference)
    (when (string-match (concat "\\`" hywiki-org-link-type ":") reference)
      ;; Remove hy: reference prefix
      (setq reference (substring reference (match-end 0))))
    (let* ((suffix-type (and (string-match hywiki-word-suffix-regexp reference)
			     (match-string 1 reference)))
	   (suffix (and suffix-type (match-string 2 reference)))
           (word (if (and suffix (not (string-empty-p suffix)))
                     (substring reference 0 (match-beginning 0))
		   reference))
           (referent (and word (hywiki-get-referent word)))
	   (referent-type (car referent))
           (pathname (when (memq referent-type '(page path-link))
		       (expand-file-name (or (cdr referent) "")
					 hywiki-directory))))
      (if (stringp pathname)
	  (cond
	   (full-data
	    (list pathname word (concat suffix-type suffix)))
	   ((and suffix (not (string-empty-p suffix)))
	    (if (equal suffix-type ":L")
		(concat pathname suffix-type suffix)
	      (concat pathname "::" suffix)))
	   (t pathname))
	referent))))

(with-eval-after-load 'org
  (org-link-set-parameters hywiki-org-link-type
                           :complete #'hywiki-org-link-complete
			   :export #'hywiki-org-link-export
			   :follow #'hywiki-find-referent
			   :htmlize-link #'hywiki-section-to-headline-reference
			   :store #'hywiki-org-link-store)
  (org-link-set-parameters "hypb-msg"
                           :follow (lambda (path) (message "Message: %s" path))
                           :export (lambda (path desc backend)
                                     (when (eq backend 'html)
                                       (format "<a href=\"#\" title=\"%s\" onclick=\"alert('%s'); return false;\">%s</a>"
                                               path
                                               (replace-regexp-in-string "'" "\\\\'" path)
                                               (or desc path))))))

(defun hywiki-referent-exists-p (&optional ref start end)
  "Return the HyWiki reference at point or optional REF, if has a referent.
If no such referent exists, return nil.

The HyWikiWord reference may be of the form:
 1. HyWikiWord#section with an optional #section.
 2. If REF is the symbol, :range, and there is a HyWikiWord at point
    with an existing referent, return the tuple of values: \='(<ref>
    <ref-start> <ref-end>) instead of the reference alone; otherwise,
    return the tuple \='(nil nil nil).

When using the reference at point, a call to
`hywiki-active-in-current-buffer-p' at point must return non-nil or this
function will return nil."
  (let ((save-input-word ref))
    (when (stringp ref)
      (setq ref (hywiki-strip-org-link ref)))
    (if (or (stringp ref)
	    (setq ref (hywiki-word-get-range)))
	(unless (hywiki-get-referent (if (stringp ref) ref (nth 0 ref)))
	  (setq ref nil))
      (setq ref nil))
    (when (and (listp ref) (= (length ref) 3))
      (setq start (nth 1 ref)
	    end   (nth 2 ref)
	    ;; `ref' must be set last so list version can be referenced
	    ;; first above
	    ref  (nth 0 ref)))
    (if (eq save-input-word :range)
	(list ref start end)
      ref)))

(defun hywiki-scan-sexps (from count)
  "Scan FROM point across COUNT sexpressions using `hbut:syntax-table'."
  (with-syntax-table hbut:syntax-table
    (scan-sexps from count)))

(defun hywiki-section-to-headline-reference ()
  "Replace file#section dashes with spaces to match to an Org headline.
Does replacement only when not in a programming mode and section
contains no spaces."
 (let ((link (get-text-property (point) 'org-link)))
   (if (and link (string-match "#" link))
       (let* ((file (substring link 0 (match-beginning 0)))
              (section (substring link (match-beginning 0))))
	 (concat file (hpath:dashes-to-spaces-markup-anchor section)))
     link)))

(defun hywiki-string-to-wikiword (str &optional separator)
  "Convert a string to a single PascalCase HyWikiWord.
Remove dashes, underscores and whitespace as part of the conversion and
capitalize each word separated by any of the removed characters.  With
optional SEPARATOR string, additionally remove those characters.  Trim the
start end end of each word using matches of the regexp value of
`split-string-default-separators'."
  (unless (stringp str)
    (error "(hywiki-string-to-wikiword): `str' must be a string, not `%s'" str))
  (when (null separator) (setq separator "-"))
  (let ((words (split-string str (if (and separator (stringp separator))
                                     (format "[-_%s\t\n\r\f]+" separator)
                                   "[-_\t\n\r\f]+")
                             t split-string-default-separators)))
    (cl-flet ((upcase-first-char (str)
                                 (concat (upcase (substring str 0 1))
                                         (substring str 1))))
      ;; Don't want to use capitalize here because if given a string that is
      ;; already a WikiWord, it will change it to 'Wikiword', losing the
      ;; middle capital 'W'; just upcase the first character.
      (apply #'concat (mapcar #'upcase-first-char words)))))

(defun hywiki-strip-org-link (link-str)
  "Return the hy:HyWikiWord#section part of an Org link string.
Strip any square bracket delimiters, description and leading or
trailing whitespace, and type prefix.  Return nil, if no match."
  (when (and (stringp link-str) (not (string-empty-p link-str)))
    (string-remove-prefix
     (concat hywiki-org-link-type ":")
     (let ((blank "[[:blank:]\r\n]+"))
       (string-trim (car (delete ""
				 (mapcar (lambda (str)
					   (string-trim (replace-regexp-in-string blank " " str t t)
							blank blank))
					 (split-string link-str "\\[\\[\\|\\]\\[\\|\\]\\]")))))))))

;;;###autoload
(defun hywiki-tags-view (&optional todo-only match view-buffer-name)
  "Prompt for colon-separated Org tags and display matching HyWiki page sections.
With optional prefix arg TODO-ONLY, limit matches to HyWiki Org
todo items only.  With optional MATCH, an Org tags match selector
string, e.g. \":tag1:tag2:tag3:\", match to sections that contain
or inherit all of these tags, regardless of tag order.  With
optional VIEW-BUFFER-NAME, use that rather than the default,
\"*HyWiki Tags*\"."
  (interactive "P")
  (require 'org-agenda)
  (let* ((org-agenda-files (list hywiki-directory))
	 (org-agenda-buffer-name (or view-buffer-name "*HyWiki Tags*"))
	 ;; `org-tags-view' is mis-written to require setting this next
	 ;; tmp-name or it will not properly name the displayed buffer.
	 (org-agenda-buffer-tmp-name org-agenda-buffer-name))
    ;; This prompts for the tags to match and uses `org-agenda-files'.
    (org-tags-view todo-only match)
    (when (equal (buffer-name) org-agenda-buffer-name)
      ;; Set up {C-u r} redo cmd
      (let (buffer-read-only)
	(put-text-property (point-min) (point-max) 'org-redo-cmd
			   `(hywiki-tags-view
			       ,todo-only
			       nil
			       ,org-agenda-buffer-name)))
      (forward-line 2))))

(defun hywiki-validate-referent (referent)
  "Return t if REFERENT is valid, otherwise trigger an error."
  (if (and (consp referent)
	   (symbolp (car referent))
	   (cdr referent))
      t
    (error (concat "(hywiki-add-referent): Invalid referent"
		   "\n  must be a cons of (<type-symbol) . <value>)"
		   "\n  not %S")
	   referent)))

(defun hywiki-wikiword-to-string (str &optional separator)
  "Convert a PascalCase HyWikiWord to a lowercase dash-separated string.
With optional SEPARATOR string, use that instead of a dash between words.

For example, hy-wiki-word.  Contiguous capital letters followed
by a lower-case letter are split before the last capital letter.
For example, BASEBall would become, base-ball but BASE would become base."
  (unless (stringp str)
    (error "(hywiki-wikiword-to-string): `str' must be a string, not `%s'" str))
  (when (null separator) (setq separator "-"))
  (let* ((sep-regexp (regexp-quote separator))
         (sep-replacement (format "\\1%s\\2" sep-regexp)))
    (unless (string-match-p sep-regexp str)
      (setq str (replace-regexp-in-string "\\([a-z]\\)\\([A-Z]\\)"
					  sep-replacement str)
	    str (replace-regexp-in-string "\\([A-Z]\\)\\([A-Z][a-z]\\)"
					  sep-replacement str)))
    (string-trim (downcase str) "[ \t\n\r]+" (format "[%s \t\n\r]+"
                                                     separator))))

(defun hywiki-word-activate (&optional arg)
  "Display HyWiki referent for wikiword at point.
If referent is a non-existent HyWiki page, create it.  When this
is the first HyWiki page, prompt before creating in case this is
not what was intended.

If found, return the referent.

If not on a HyWikiWord and optional prefix ARG is null, emulate an
Action Key press; with a prefix ARG, emulate an Assist Key press."
  (interactive "P")
  (let ((word (hywiki-word-at)))
    (if word
	(hywiki-find-referent word)
      (hkey-either arg))))

(defun hywiki-word-at (&optional range-flag hash-sign-only-flag)
  "Return potential HyWikiWord and optional #section:Lnum:Cnum at point or nil.
Point may be on or immediately after the HyWikiWord reference.
`hywiki-mode' must be enabled or this will return nil.

If the HyWikiWord is delimited, point must be within the delimiters.
This works regardless of whether the HyWikiWord has been highlighted
or not.

With optional RANGE-FLAG, return a list of (HyWikiWord start-position
end-position); the positions include the entire
HyWikiWord#section:Lnum:Cnum string but exclude any delimiters.

This does not test whether a referent exists for the HyWikiWord; call
`hywiki-referent-exists-p' without an argument for that.  Nor does it
test whether the HyWikiWord reference is within a valid context; call
`hywiki-non-hook-context-p' for that.

A call to `hywiki-active-in-current-buffer-p' at point must return
non-nil or this will return nil."
  (if (hywiki-active-in-current-buffer-p)
      (save-excursion
	;; First look for an Org-type [[hy:WikiWord]] reference.
	;; Don't use `cl-destructuring-bind' here since the `hargs:delimited' call
	;; can return nil rather than the 3 arg list that would be required
	(let* ((start-regexp (concat "\\[\\[\\(" hywiki-org-link-type ":\\)?"))
	       (opoint (point))
	       (hywiki-org-link-type-flag)
	       (wikiword-start-end
		(save-excursion
		  (skip-chars-backward (concat hywiki-org-link-type ":["))
		  (when (looking-at start-regexp)
		    (setq hywiki-org-link-type-flag (match-string 1))
		    (goto-char opoint)
		    ;; This next line drops any `hywiki-org-link-type': from the
		    ;; start of the WikiWord since it is part of the delimiter used.
		    (hargs:delimited (concat "\\[\\[\\(" hywiki-org-link-type ":\\)?")
				     "\\(\\]\\[\\|\\]\\]\\)" t t t))))
	       (wikiword (nth 0 wikiword-start-end))
	       (start    (nth 1 wikiword-start-end))
	       (end      (nth 2 wikiword-start-end)))

	  (with-syntax-table hywiki--org-mode-syntax-table
	    (if (and (cond (wikiword
			    ;; Enforce `hywiki-org-link-type-required' setting
			    (unless (and hywiki-org-link-type-required
					 (not hywiki-org-link-type-flag))
			      ;; Handle an Org link [[HyWikiWord]] [[hy:HyWikiWord]]
			      ;; or [[HyWikiWord#section][Description Text]].
			      ;; Get the HyWikiWord link reference, ignoring any
			      ;; description given in the link.
			      ;;
			      ;; Don't use next line so don't have to load all of Org
			      ;; mode just to check for HyWikiWords; however,
			      ;; ignoring this disables support for Org mode aliases.
			      ;; (setq wikiword (org-link-expand-abbrev (org-link-unescape (string-trim wikiword))))
			      (setq wikiword (hywiki-strip-org-link wikiword))
			      (when (and wikiword end)
				;; Update start and end to newly stripped
				;; string positions
				(save-excursion
				  (save-restriction
				    (narrow-to-region start end)
				    (goto-char (point-min))
				    (when (search-forward wikiword nil t)
				      (setq start (match-beginning 0)
					    end   (match-end 0))))))
			      (hywiki-word-is-p wikiword)))

			   ;; Handle a delimited HyWikiWord reference with
			   ;; multiple, possibly whitespace-separated words in
			   ;; its section, e.g. (MyWikiWord#one two three).
			   ;; Whitespace between section words is allowed only
			   ;; if the delimiters are immediately before and
			   ;; after a single HyWikiWord reference.
			   ((let ((case-fold-search nil)
				  (bol (line-beginning-position))
				  opoint)
			      ;; May be a non-delimiter but HyWikiWord ending
			      ;; punctuation to skip past
			      (skip-chars-backward (hywiki-get-buttonize-characters) bol)
			      (setq opoint (point))
			      (when (setq wikiword-start-end (hywiki-delimited-p)) ;; limited to 2 lines
				(setq start (nth 1 wikiword-start-end)
				      end   (nth 2 wikiword-start-end))
				(goto-char start)
				(if (and (save-restriction
					   (narrow-to-region (point) end)
					   (looking-at hywiki-word-with-optional-suffix-exact-regexp))
					 ;; WikiWord ref is the entirety of the string
					 (= end (match-end 0))
					 ;; Can't be followed by a # character
					 (/= (or (char-after (match-end 0)) 0)
					     ?#))
				    (setq wikiword (match-string-no-properties 0)
					  start (match-beginning 0)
					  end   (match-end 0))
				  (goto-char opoint)
				  (unless (or (progn
						(skip-chars-backward "-_*#:[:alnum:]" bol)
						(hywiki-maybe-at-wikiword-beginning))
					      (progn
						;; Skip past HyWikiWord or section with
						;; possible whitespace
						(skip-syntax-backward "^$()<>._\"\'" bol)
						(unless (= (or (char-before) 0) ?#)
						  (goto-char opoint)
						  (skip-syntax-backward "^-$()<>._\"\'" bol))
						;; Move to start of wikiword reference
						(skip-chars-backward "-_*#:[:alnum:]" bol)
						(skip-syntax-backward "-" bol)
						;; Preceding char must now be the
						;; opening delimiter or else there may
						;; be multiple non-section words within
						;; the delimiters, so reprocess and do
						;; not allow spaces in the #section part
						(memq (char-syntax (or (char-before) 0))
						      '(?\( ?\< ?\"))))
				    (goto-char opoint)
				    (skip-syntax-backward "^-$()<>._\"\'" bol)
				    ;; Move to start of wikiword reference
				    (skip-chars-backward "-_*#:[:alnum:]" bol)
				    (skip-syntax-backward "-" bol))
				  (when (and
					 ;; (or (bolp)
					 ;;     (string-match (regexp-quote
					 ;; 		     (char-to-string (char-before)))
					 ;; 		    "\[\(\{\<\""))
					 (progn
					   (skip-chars-forward " \t")
					   (hywiki-maybe-at-wikiword-beginning))
					 (looking-at (concat
						      hywiki-word-regexp
						      "\\(#[^][#()<>{}\" \t\n\r\f]*\\)?"
						      hywiki-word-line-and-column-numbers-regexp "?"))
					 ;; Can't be followed by a # character
					 (/= (or (char-after (match-end 0)) 0)
					     ?#)
					 (progn (goto-char (match-end 0))
						(skip-syntax-forward "-")))
				    (setq start (match-beginning 0)
					  end   (match-end 0)
					  ;; No following char
					  wikiword (string-trim
						    (buffer-substring-no-properties start end))))))))

			   ;; Handle a non-delimited HyWikiWord reference
			   ;; with multiple dash-separated words in its section,
			   ;; e.g. WikiWord#one-two-three.
			   ((let ((case-fold-search nil)
				  (bol (line-beginning-position))
				  opoint)
			      ;; May be a non-delimiter but HyWikiWord ending
			      ;; punctuation to skip past
			      (skip-chars-backward (hywiki-get-buttonize-characters) bol)
			      (setq opoint (point))
			      (goto-char opoint)
			      (skip-syntax-backward "^-$()<>._\"\'" bol)
			      ;; Move to start of wikiword reference
			      (skip-chars-backward "-_*#:[:alnum:]" bol)
			      (skip-syntax-backward "-" bol)
			      (when (and (or (bolp)
					     (string-match (regexp-quote
							    (char-to-string (char-before)))
							   "\[\(\{\<\""))
					 (progn
					   (skip-chars-forward " \t")
					   (hywiki-maybe-at-wikiword-beginning))
					 (looking-at (concat
						      hywiki-word-regexp
						      "\\(#[^][#()<>{}\" \t\n\r\f]+\\)?"
						      hywiki-word-line-and-column-numbers-regexp "?"))
					 ;; Can't be followed by a # character
					 (/= (or (char-after (match-end 0)) 0)
					     ?#)
					 (goto-char (match-end 0)))
				(setq start (match-beginning 0)
				      end   (match-end 0)
				      ;; No following char
				      wikiword (string-trim (match-string-no-properties 0))))))

			   ;; Handle a non-delimited HyWikiWord reference with
			   ;; optional #section:Lnum:Cnum; if it is an Org
			   ;; link, it may optionally have a hy: link-type
			   ;; prefix.  #section may not contain spaces. Ignore
			   ;; wikiwords preceded by any non-whitespace
			   ;; character, except any of these: "([\"'`'"
			   (t (let ((case-fold-search nil))
				(skip-chars-forward " \t")
				(when (hywiki-maybe-at-wikiword-beginning)
				  (when (looking-at (concat hywiki-org-link-type ":"))
				    (goto-char (match-end 0)))
				  (cond ((looking-at hywiki--word-and-buttonize-character-regexp)
					 (setq start (match-beginning 1)
					       end (match-end 1)
					       wikiword (string-trim (match-string-no-properties 1))))
					((or (and (looking-at hywiki-word-with-optional-suffix-regexp)
						  ;; Can't be followed by a # character
						  (/= (or (char-after (match-end 0)) 0)
						      ?#))
					     (and hash-sign-only-flag
						  (looking-at (concat hywiki-word-regexp "#"))))
					 (setq start (match-beginning 0)
					       end   (match-end 0)
					       ;; No following char
					       wikiword (string-trim (match-string-no-properties 0)))))))))
		     ;; If `wikiword' reference has a #section, ensure
		     ;; it stops when there are any disallowed characters
		     ;; and reset the value of 'end' to match any reduction.
		     ;; One set of \n\r characters is allowed but no
		     ;; whitespace at the end of the reference.
		     (if (and (stringp wikiword) (string-match "#" wikiword))
			 (let ((section-regexp "#[^][#()<>{}\"\f]*[^][#:;()<>{}\"\f\t\n\r ]"))
			   (when (string-match
				  (if hash-sign-only-flag
				      (concat "#\\'\\|" section-regexp)
				    section-regexp)
				  wikiword)
			     (setq end (- end (- (length wikiword)
						 (match-end 0)))
				   wikiword (substring wikiword 0 (match-end 0)))))
		       t))
		(if range-flag
		    (list wikiword start end)
		  wikiword)
	      (when range-flag
		'(nil nil nil))))))
    (when range-flag
      '(nil nil nil))))

;;;###autoload
(defun hywiki-word-create (wikiword &optional ref-type-flag)
  "Create a HyWiki referent for WIKIWORD and return it; don't display it.
This replaces any existing referent the WIKIWORD may have.

With either `hywiki-referent-prompt-flag' set or optional prefix
REF-TYPE-FLAG, prompt for and choose a typed referent, otherwise, create a
HyWiki page.  See `hywiki-referent-menu' for valid referent types.

Use `hywiki-get-referent' to test for and retrieve an existing HyWikiWord
referent."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new
			  (format "Create HyWikiWord %s: "
				  (if (or (and hywiki-referent-prompt-flag
					       (null current-prefix-arg))
					  current-prefix-arg)
				      "referent"
				    "page"))))
		     current-prefix-arg))
  (if (or ref-type-flag hywiki-referent-prompt-flag)
      (hywiki-create-referent wikiword t)
    (hywiki-create-page wikiword t)))

(defun hywiki-word-create-and-display (wikiword &optional prompt-flag)
  "Display the HyWiki referent for WIKIWORD and return it.
If there is no existing WIKIWORD referent, add one.
With either `hywiki-referent-prompt-flag' set or optional prefix ARG,
prompt for and choose a typed referent, otherwise, create and/or display
a HyWiki page.  See `hywiki-referent-menu' for valid referent types.

Use `hywiki-get-referent' to determine whether a HyWikiWord referent
exists."
  (interactive (list (or (hywiki-word-at)
			 (hywiki-word-read-new
			  (format "Add/Edit and display HyWiki %s: "
				  (if (or (and hywiki-referent-prompt-flag
					       (null current-prefix-arg))
					  current-prefix-arg)
				      "referent"
				    "page"))))
		     current-prefix-arg))
  (hywiki-create-referent-and-display
   wikiword (or (and hywiki-referent-prompt-flag
		     (null prompt-flag))
		prompt-flag)))

(defun hywiki-word-get-range ()
  "Return list of (HyWikiWord#section:Lnum:Cnum start end) around point.
Calls to `hywiki-active-in-current-buffer-p' and `hywiki-non-hook-context-p'
must return non-nil or this will return \\='(nil nil nil).

If the HyWikiWord reference is delimited, point must be within the
delimiters.  The delimiters are excluded from start and end.  If not
at a HyWikiWord, return \\='(nil nil nil).

This works regardless of whether the HyWikiWord has been highlighted
or not.  Call `hywiki-highlighted-word-at' to test for a highlighted
HyWikiWord at point.

This does not test whether a referent exists for the HyWikiWord; call
`hywiki-referent-exists-p' without an argument for that."
  (if (hywiki-non-hook-context-p)
      '(nil nil nil)
    (hywiki-word-at :range)))

(defun hywiki-highlight-word-get-range ()
  "Return list of (HyWikiWord#section:Lnum:Cnum start end) around point.
Also highlight HyWikiWord as necessary.

A call to `hywiki-active-in-current-buffer-p' at point must return
non-nil or this will return \\='(nil nil nil).

If the HyWikiWord reference is delimited, point must be within the
delimiters.  The delimiters are excluded from start and end.  If not
at a HyWikiWord, return \\='(nil nil nil).

This works regardless of whether the HyWikiWord has been highlighted
or not.  Call `hywiki-highlighted-word-at' to test for a highlighted
HyWikiWord at point.

This does not test whether a referent exists for the HyWikiWord; call
`hywiki-referent-exists-p' without an argument for that."
  (cl-destructuring-bind (wikiword start end)
      (hywiki-word-get-range)
    ;; Ensure wikiword in buffer is highlighted before
    ;; returning its non-highlighted string version.
    (when (and wikiword start end
	       (not (hproperty:but-get start 'face hywiki-word-face))
	       (hywiki-referent-exists-p wikiword))
      (hywiki-maybe-highlight-region-reference start end))
    (list wikiword start end)))

(defun hywiki-highlight-word-move-range ()
  "Ensure wikiword highlighting range matches expected range.
Return t if the highlighted range exists at point and gets moved."
  (let* ((but (hproperty:but-get (point) 'face hywiki-word-face))
	 (but-start (when but (hproperty:but-start but)))
	 (but-end (when but (hproperty:but-end but))))
    (when (and but-start but-end)
      (save-excursion
	(goto-char but-start)
	(cl-destructuring-bind (wikiword start end)
	    (hywiki-word-at :range)
	  (when (and wikiword start end but-start but-end
		     (or (/= start but-start) (/= end but-end)))
	    (hproperty:but-move but start end)
	    t))))))

(defun hywiki-word-at-point ()
  "Return singular HyWikiWord at point with its suffix stripped or nil.
Point may be on or immediately after the HyWikiWord itself.  `hywiki-mode'
must be enabled or this will return nil.  Suffix is anything after the #
symbol.

This does not test whether a referent exists for the HyWikiWord; call
`hywiki-referent-exists-p' without an argument for that.

A call to `hywiki-active-in-current-buffer-p' at point must return non-nil
or this will return nil."
  (hywiki-get-singular-wikiword (hywiki-word-strip-suffix (hywiki-word-at))))

(defun hywiki-delimited-p (&optional pos)
  "Return non-nil if optional POS or point is surrounded by delimiters.
Any non-nil value returned is a list of (hywikiword-ref start-pos end-pos)
where these positions exclude the delimiters.  The delimited range must be
two lines or less with point on the first line.

Use `hywiki-word-at', which calls this, to determine whether there is
a HyWikiWord at point."
  (save-excursion
    (when (natnump pos)
      (goto-char pos))
    (let* ((range
            (save-restriction
              ;; Limit balanced pair checks to current through next lines for speed.
              ;; Point must be on the opening line.
              (narrow-to-region (line-beginning-position) (line-end-position 2))
              (or (hypb:in-string-p nil t)
		  (hargs:delimited "[\[<\(\{]" "[\]\}\)\>]" t t t))))
           (str-start (nth 1 range))
           (str-end (nth 2 range))
           ;; Call to 'hypb:in-string-p' may have returned t as its first
           ;; element
	   (wikiword (when str-start
                       (if (stringp (car range))
                           (car range)
                         (buffer-substring-no-properties str-start str-end))))
	   range-trimmed
	   wikiword-trimmed)
      (if (and wikiword (string-match "[ \t\n\r\f]+\\'" wikiword))
	  ;; Strip any trailing whitespace
	  (setq wikiword-trimmed (substring wikiword 0 (match-beginning 0))
		range-trimmed (when (car range)
                                (list wikiword-trimmed str-start
				      (- str-end (length (match-string
                                                          0 wikiword))))))
	(setq range-trimmed (when (car range) range)))
      (and range-trimmed str-start str-end
	   ;; Ensure closing delimiter is a match for the opening one
	   (or (eq (matching-paren (or (char-before str-start)
                                       0))
		   (char-after str-end))
	       ;; May be string quotes where matching-paren returns nil.
	       (and (eq (char-before str-start)
			(char-after str-end ))
		    (eq (char-syntax (char-before str-start)) ?\")))
	   range-trimmed))))

(defun hywiki-word-face-at-p (&optional pos)
  "Non-nil if point or optional POS has the `hywiki-word-face' property.
Return any HyWikiWord reference found."
  ;; Sometimes this can return a left over button/overlay that points
  ;; to no buffer.  Ignore this case.
  (hproperty:but-get (or pos (point)) 'face hywiki-word-face))

;;;###autoload
(defun hywiki-word-consult-grep (word)
  "Use `hywiki-consult-grep' to show occurrences of a prompted for HyWikiWord.
Default to any HyWikiWord at point."
  (interactive (list (hywiki-word-read)))
  (if (and (stringp word) (not (string-empty-p word)))
      (hywiki-consult-grep (concat "\\b" (regexp-quote word) "\\b"))
    (user-error "(hywiki-word-consult-grep): Invalid HyWikiWord: '%s'; must be capitalized, all alpha" word)))

(defun hywiki-word-from-reference (ref)
  "Return the HyWikiWord part of a reference (part before the #).
This does not test whether a referent exists for the HyWikiWord; call
`hywiki-referent-exists-p' without an argument for that.  Nor does it
test whether the HyWikiWord reference is within an invalid context;
call `hywiki-non-hook-context-p' for that."
  (when (and (stringp ref)
	     (string-match hywiki-word-with-optional-suffix-exact-regexp ref))
    (match-string 1 ref)))

(defun hywiki-word-grep (wikiword)
  "Grep for occurrences of WIKIWORD with `consult-grep' or normal-grep'.
Search across `hywiki-directory'."
  (if (hsys-consult-active-p) ;; allow for autoloading
      (hywiki-consult-backlink wikiword)
    (grep (string-join (list grep-command (format "'%s'" wikiword)
			     (concat (file-name-as-directory hywiki-directory)
				     "*" hywiki-file-extension))
		       " "))))

(defun hywiki-word-is-p (word &optional regexp-flag)
  "Return non-nil if WORD is a HyWikiWord and optional #section:Lnum:Cnum.
With optional REGEXP-FLAG non-nil, #section may be a regular expression.
When non-nil, the return value is the position where either the # character
starts or the word ends.  Use it to extract the wikiword by itself with
\(substring WORD 0 <return-value>).

WORD may not yet have a referent (non-existent).  Use `hywiki-get-referent'
to determine whether a HyWikiWord referent exists.

Return nil if WORD is a prefixed, typed hy:HyWikiWord, since
these are handled by the Org mode link handler."
  (and (stringp word) (not (string-empty-p word))
       (let (case-fold-search)
	 (and (or (string-match-p hywiki-word-with-optional-suffix-exact-regexp word)
		  ;; For now this next version allows spaces and tabs in
		  ;; the suffix part
		  (eq 0 (string-match-p hywiki-word-with-optional-suffix-exact-regexp
			                word)))
	      ;; If has a #section and `regexp-flag' is nil,
              ;; ensure there are no invalid chars
	      (if regexp-flag
                  (or (string-match-p "#" word) (length word))
                (if (string-match-p "#" word)
		    (string-match "#[^][#()<>{}\"\n\r\f]+\\'" word)
		  (length word)))))))

(defun hywiki-word-read (&optional prompt initial)
  "Prompt with completion for and return an existing HyWikiWord.
If point is on one, press RET immediately to use that one."
  (let ((completion-ignore-case t))
    (completing-read (if (stringp prompt) prompt "HyWiki Word: ")
		     (hywiki-get-referent-hasht)
		     nil t initial nil (hywiki-word-at-point))))

(defun hywiki-word-read-new (&optional prompt initial)
  "Prompt with completion for and return an existing or new HyWikiWord.
If point is on one, press RET immediately to use that one.  Ensure
that any name conforms to `hywiki-word-regexp'."
  (let ((completion-ignore-case t)
        (valid-p (lambda (input)
                   (let ((case-fold-search nil))
                     (string-match-p (concat "\\`" hywiki-word-regexp "\\'") input))))
        (collection (hywiki-get-referent-hasht)))
    (completing-read
     (if (stringp prompt) prompt "HyWiki Word: ")
     (lambda (input pred action)
       (if (eq action 'lambda)
           ;; Check that input matches `hyrolo-word-regexp'.
           ;; If so, RET will exit the minibuffer.
           (funcall valid-p input)
         ;; Otherwise, complete over `collection'
         (complete-with-action action collection input pred)))
     nil t initial nil (hywiki-word-at-point))))

(defun hywiki-word-strip-suffix (wikiword)
  "Return PAGE-NAME with any optional #section:Lnum:Cnum stripped off.
If an empty string or not a string, return nil."
  (when (and (stringp wikiword) (not (string-empty-p wikiword)))
    (setq wikiword (string-trim wikiword "[# \t\n\r]+" "[# \t\n\r]+"))
    (if (and (string-match hywiki-word-with-optional-suffix-exact-regexp wikiword)
	     (or (match-beginning 2) (match-beginning 4)))
	;; Remove any #section:Lnum:Cnum suffix in PAGE-NAME.
	(match-string-no-properties 1 wikiword)
      wikiword)))

(defun hywiki-word-set-auto-highlighting (hywiki-from-mode hywiki-to-mode)
  "Set HyWikiWord auto-highlighting based on HYWIKI-FROM-MODE HYWIKI-TO-MODE.
Highlight only those buffers attached to windows.

Auto-highlighting uses pre- and post-command hooks.  If an error
occurs with one of these hooks, the problematic hook is removed."
  (cond ((null hywiki-to-mode)
         (hywiki-mode-disable)
	 ;; Ensure hooks are removed from all hywiki buffers any time
	 ;; mode is disabled
	 (let ((hywiki-mode hywiki-to-mode))
	   (hywiki-word-dehighlight-buffers (hywiki-get-buffers :all))))
	((or (null hywiki-from-mode)
	     (and (eq hywiki-from-mode :pages) (eq hywiki-to-mode :pages))
	     (and (eq hywiki-from-mode :all)   (eq hywiki-to-mode :all)))
	 ;; Don't use `hywiki-get-buffers-in-windows' when
	 ;; highlighting since if edebug this function, it will not
	 ;; highlight. -- rsw, 2026-02-08
	 (hywiki-word-highlight-buffers (hywiki-get-buffers
					 hywiki-to-mode)))
	((and (eq hywiki-from-mode :all) (eq hywiki-to-mode :pages))
	 (hywiki-word-dehighlight-buffers
	  (set:difference (hywiki-get-buffers :all)
			  (hywiki-get-buffers :pages))))
	((and (eq hywiki-from-mode :pages) (eq hywiki-to-mode :all))
	 (hywiki-word-highlight-buffers
          ;; Here the larger set must always be given first to compute any
          ;; difference
	  (set:difference (hywiki-get-buffers :all)
                          (hywiki-get-buffers :pages))))
	(t
	 (error "(hywiki-word-set-auto-highlighting): Inputs must be nil, :pages or :all, not '%s' and '%s'"
		hywiki-from-mode hywiki-to-mode))))

(defun hywiki-word-highlight-in-frame (frame)
  "Auto-highlight HyWikiWords in `hywiki-mode' buffers displayed in FRAME."
  (when hywiki-mode
    (let ((hywiki-buf-predicate
	   (if (eq hywiki-mode :pages)
	       #'hywiki-in-page-p
	     #'hywiki-potential-buffer-p)))
      (hywiki-word-highlight-in-buffers
       (seq-filter (lambda (buf)
		     (with-current-buffer buf
		       (funcall hywiki-buf-predicate)))
		   (hywiki-get-buffers-in-windows frame))))))

(defun hywiki-word-highlight-in-current-buffer ()
  "Auto-highlight HyWikiWords in the current buffer."
  (and (not (minibufferp))
       (hywiki-active-in-current-buffer-p)
       (hywiki-word-highlight-in-buffers (list (current-buffer)))))

(defun hywiki-word-highlight-in-buffers (buffers)
  "Auto-highlight HyWikiWords in BUFFERS."
  (dolist (buf buffers)
    (with-current-buffer buf
      (hywiki-word-add-completion-at-point)
      (add-hook 'pre-command-hook      'hywiki-word-store-around-point -60 :local)
      (add-hook 'post-self-insert-hook 'hywiki-word-highlight-post-self-insert -60 :local)
      (add-hook 'post-command-hook     'hywiki-word-highlight-post-command -60 :local)
      ;; Display buffer before `normal-mode' triggers possibly
      ;; long-running font-locking
      (sit-for 0)
      (hywiki-maybe-highlight-references nil nil t)))
  ;; Rebuild lookup tables if any HyWiki page name has changed
  (hywiki-get-referent-hasht)
  (hywiki-maybe-directory-updated))

(defun hywiki-word-add-completion-at-point ()
  "Add HyWiki refs in-buffer completion to `completion-at-point-functions'.
Completion requires typing at least the two first characters of the
completion or no completion candidates are returned.
If using `company-mode', you must use the `company-capf' backend for HyWiki
completion to work properly."
  ;; Make `indent-for-tab-command' by default bound to {TAB} complete HyWiki
  ;; references.
  (setq tab-always-indent 'complete)
  (add-hook 'completion-at-point-functions #'hywiki-completion-at-point -90 t)
  (cond ((bound-and-true-p corfu-mode)) ;; Uses :exit-function in hywiki-c-a-p
        ((bound-and-true-p company-mode)
         (add-hook 'company-completion-finished-hook
                   #'hywiki-completion-exit-function)
         (add-hook 'company-completion-cancelled-hook
                   #'hywiki-completion-exit-function))
        ;; Default Emacs completion
        (t (advice-add 'completion--insert :after #'hywiki-completion-exit-function))))

(defun hywiki-word-remove-completion-at-point ()
  "Remove HyWiki refs in-buffer completion from `completion-at-point-functions'."
  (remove-hook 'completion-at-point-functions #'hywiki-completion-at-point t)
  (remove-hook 'company-completion-finished-hook  #'hywiki-completion-exit-function)
  (remove-hook 'company-completion-cancelled-hook #'hywiki-completion-exit-function)
  (advice-remove 'completion--insert #'hywiki-completion-exit-function)
  ;; Restore user's customized setting of these options.
  (custom-reevaluate-setting 'tab-always-indent))

(defun hywiki-word-highlight-buffers (buffers)
  "Setup HyWikiWord auto-highlighting and highlight in BUFFERS."
  (interactive)
  (add-hook 'after-change-major-mode-hook 'hywiki-word-add-completion-at-point)
  (add-hook 'after-change-major-mode-hook 'hywiki-word-highlight-in-current-buffer)
  (add-hook 'window-buffer-change-functions 'hywiki-word-highlight-in-frame)
  (add-to-list 'yank-handled-properties
	       '(hywiki-word-face . hywiki-highlight-on-yank))
  (hywiki-word-highlight-in-buffers buffers)
  (when (called-interactively-p 'interactive)
    (message "HyWikiWord auto-highlighting enabled")))

(defun hywiki-word-dehighlight-in-buffers (buffers)
  "Dehighlight HyWikiWords in BUFFERS."
  (interactive)
  (dolist (buf buffers)
    (with-current-buffer buf
      (save-excursion
        (hywiki-word-remove-completion-at-point)
        (remove-hook 'pre-command-hook      'hywiki-word-store-around-point :local)
        (remove-hook 'post-self-insert-hook 'hywiki-word-highlight-post-self-insert :local)
        (remove-hook 'post-command-hook     'hywiki-word-highlight-post-command :local)
        ;; Display buffer before `normal-mode' triggers possibly
        ;; long-running font-locking
        (sit-for 0)
        ;; Force dehighlighting in buffer with this `let'
        (setq hywiki-buffer-highlighted-state 'h)
        (hywiki-maybe-dehighlight-references))))
  (hywiki-maybe-directory-updated))

(defun hywiki-mode-disable ()
  "Remove global `hywiki-mode' hooks when the mode is entirely disabled."
  (remove-hook 'after-change-major-mode-hook 'hywiki-word-add-completion-at-point)
  (remove-hook 'after-change-major-mode-hook 'hywiki-word-highlight-in-current-buffer)
  (remove-hook 'window-buffer-change-functions 'hywiki-word-highlight-in-frame)
  (setq yank-handled-properties
	(delete '(hywiki-word-face . hywiki-highlight-on-yank)
		yank-handled-properties)))

(defun hywiki-word-dehighlight-buffers (buffers)
  "Disable HyWikiWord auto-highlighting and dehighlight in BUFFERS."
  (interactive)
  (hywiki-word-dehighlight-in-buffers buffers)
  (when (called-interactively-p 'interactive)
    (message "HyWikiWord auto-highlighting disabled")))

;;;###autoload
(defun hywiki-yank (reference &optional regexp-flag)
  "Insert at point the first HyRolo entry with a headline containing NAME.
If the `consult' package is installed, interactively select and complete
the entry to be inserted.

With optional prefix arg, REGEXP-FLAG, treat NAME as a regular expression
instead of a string."
  (interactive (list (hywiki-read-headline-regexp "Yank")
		     current-prefix-arg))
  (push-mark)
  (insert (hywiki-get-entry reference nil nil regexp-flag))
  ;; Let user reformat the region just yanked.
  (funcall hywiki-yank-reformat-function (mark) (point))
  (exchange-point-and-mark))

;;; ************************************************************************
;;; Private functions
;;; ************************************************************************

(defun hywiki--add-suffix-to-referent (suffix referent)
  "Add SUFFIX to REFERENT's value and return REFERENT.
SUFFIX includes its type prefix, e.g. #.  Return nil if any input is
invalid.  Append only if the referent-type supports suffixes."
  (if (or (null suffix) (and (stringp suffix) (string-empty-p suffix)))
      referent
    (when (consp referent)
      (let ((referent-type (car referent))
	    (referent-value (cdr referent)))
	(when (and (symbolp referent-type) referent-value)
	  (if (and (stringp suffix)
		   (stringp referent-value)
		   (memq referent-type hywiki-allow-suffix-referent-types)
		   (not (seq-contains-p referent-value ?# #'=)))
	      ;; Need to insert #suffix into referent's value
	      (progn
		(setq referent-value
		      (if (string-match hpath:line-and-column-regexp referent-value)
			  (concat (substring 0 (match-beginning 0))
				  suffix
				  (match-string 0 referent-value))
			(concat referent-value suffix)))
		(cons referent-type referent-value))
	    referent))))))

(defun hywiki--buttonized-region-p ()
  "Return non-nil when hywiki--buttonize-start/end are in the current buffer."
  (and (marker-position hywiki--buttonize-start)
       (eq (marker-buffer hywiki--buttonize-start) (current-buffer))
       (marker-position hywiki--buttonize-end)
       (eq (marker-buffer hywiki--buttonize-end) (current-buffer))))

(defun hywiki--clear-buttonize-characters-cache (&rest _)
  "Invalidate the cached Org-mode syntax string."
  (setq hywiki--buttonize-characters-cache nil))

(defun hywiki--extend-region (start end)
  "Extend range (START END) to include delimited regions; return the new range.
Ensure START and END are in increasing order.

Used to extend a region to fully include any strings or balanced pair
delimiters."
  (unless (integer-or-marker-p start)
    (error "`start' arg must be an integer or marker, not '%s'" start))
  (unless (integer-or-marker-p end)
    (error "`end' arg must be an integer or marker, not '%s'" end))
  (let ((maximum (max start end)))
    (setq start (min start end)
	  end (max end maximum)))
  (let ((result (list start end))
	(in-string-flag (hypb:in-string-p)))

    ;; Skip past all double-quoted ranges and extend `start' and `end' as needed
    (save-excursion
      (goto-char start)
      (condition-case nil
	  (while (and (<= (point) end)
		      (skip-syntax-forward "^\"" (unless in-string-flag end))
		      (not (zerop (skip-syntax-forward
				   "\"" (unless in-string-flag end))))
		      (= ?\" (char-syntax (preceding-char))))
	    (when (or (= (1- (point)) (point-min))
		      (/= ?\\ (char-before (1- (point)))))
	      (save-excursion
		(if (hypb:in-string-p)
		    (setq end (max end (goto-char (hywiki-scan-sexps (1- (point)) 1))))
		  ;; after a string
		  (setq start (min start (goto-char (hywiki-scan-sexps (point) -1)))))
		(setq result (list start end)))))
	(error nil)))

    ;; From `start', skip past the first closing delimiter and extend
    ;; region start to include its opening delimiter, if any.
    (save-excursion
      (goto-char start)
      (condition-case nil
	  (while (and (<= (point) end)
		      (skip-syntax-forward "^\)")
		      (not (zerop (skip-syntax-forward "\)")))
		      (= ?\) (char-syntax (preceding-char))))
	    (when (or (= (1- (point)) (point-min))
		      (/= ?\\ (char-before (1- (point)))))
	      (save-excursion
		(setq start (min start (goto-char (hywiki-scan-sexps (point) -1)))
		      result (list start end)))))
	(error nil)))

    ;; From `end', skip back past the first opening delimiter and
    ;; extend region end to include its closing delimiter, if any.
    (save-excursion
      (goto-char end)
      (condition-case nil
	  (while (and (>= (point) start)
		      (skip-syntax-backward "^\(")
		      (not (zerop (skip-syntax-backward "\(")))
		      (= ?\( (char-syntax (following-char))))
	    (when (not (eq ?\\ (char-before (max (point) (point-min)))))
	      (save-excursion
		(setq end (max end (goto-char (hywiki-scan-sexps (point) 1)))
		      result (list start end)))))
	(error nil)))

    ;; Extend any highlighting as `hywiki-word-at' dictates
    (save-excursion
      (cl-destructuring-bind (_ ref-start ref-end)
	  (hywiki-word-at :range)
	(when (and ref-start ref-end)
	  (when (< ref-start start)
	    (setq start ref-start))
	  (when (> ref-end end)
	    (setq end ref-end)))))

    ;; Skip past any current word at start and end to extend if needed
    (save-excursion
      (goto-char start)
      (skip-chars-forward " \t\n\r")
      (skip-syntax-backward "w")
      (setq start (point))
      (goto-char end)
      (setq result (nth 2 (hywiki-delimited-p))
	    end (or result end))
      (unless result
	(skip-chars-backward " \t\n\r")
	(skip-syntax-forward "w")
	(setq end (point)))
      (setq result (list (min start end) (max start end))))
    result))

(defun hywiki--get-all-references (function &optional start end)
  "Apply FUNCTION to all highlighted HyWikiWord references in current buffer.
FUNCTION must take four arguments: (buffer-start-pos buffer-end-pos
\\='face hywiki-word-face).  Optional START and END are sent to the function as
the first two arguments; otherwise, the entire buffer is scanned."
  (let ((refs (funcall function
		       (or start (point-min))
		       (or end (point-max))
		       'face hywiki-word-face)))
    (if (version< emacs-version "29")
	refs
      ;; Button/overlay ordering is reversed after Emacs 28
      (nreverse refs))))

(defun hywiki--get-delimited-range-at-closing-delimiter ()
  (cond
   ;; Handle closing delimiters
   ((memq (char-before) '(?\] ?\>))
    (hywiki--get-delimited-range-backward))
   ((memq (char-after) '(?\] ?\>))
    (goto-char (1+ (point)))
    (hywiki--get-delimited-range-backward))
   ((memq (char-before) '(?\) ?\}))
    (list (point) (scan-sexps (point) -1)))
   ((memq (char-after) '(?\) ?\}))
    (goto-char (1+ (point)))
    (list (point) (scan-sexps (point) -1)))
   ((and (eq (char-before) ?\")
	 (not (hypb:in-string-p)))
    (list (point) (scan-sexps (point) -1)))
   ((and (eq (char-after) ?\")
	 (not (hypb:in-string-p)))
    (list (point) (scan-sexps (point) 1)))))

(defun hywiki--get-delimited-range-backward ()
  "Return a list of (start end) if not between/after end ]] or >>.
Delimiters are included in the range.  Point must be on or after the
closing delimiter.  Otherwise, return nil."
    (unless (or (eq (char-before) (char-before (1- (point))))
		(and (char-after)
                     (save-excursion
		       (goto-char (1+ (point)))
		       (eq (char-before) (char-before (1- (point)))))))
      (with-syntax-table hbut:syntax-table
        (nreverse (list (point) (scan-sexps (point) -1))))))

(defun hywiki--get-delimited-range-forward ()
  "Return a list of (start end) if not between/before opening [[ or <<.
Delimiters are included in the range.  Point must be on or after the
opening delimiter.  Otherwise, return nil."
  (save-excursion
    (unless (or (eq (char-after) (char-after (1+ (point))))
		(and (char-before)
		     (goto-char (1- (point)))
		     (eq (char-after) (char-after (1+ (point))))))
      (list (point) (scan-sexps (point) 1)))))

(defun hywiki--maybe-de/highlight-org-element-backward (func)
  "De/Highlight HyWikiWords with FUNC on: ], ]], >, or >> brackets.
Func must take a single numeric argument of -1 to process one
delimited grouping."
  (ignore-errors
    (unless (save-excursion
	      (when (or (eq (char-before) (char-before (1- (point))))
			(and (char-after)
			     (goto-char (1+ (point)))
			     (eq (char-before) (char-before (1- (point))))))
		;; double delimiters - dehighlight
		(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))
		    (setq hywiki--highlighting-done-flag t)))))
      ;; single delimiters - highlight
      (funcall func -1))))

(defun hywiki--maybe-de/highlight-org-element-forward (func)
  "De/Highlight HyWikiWords with FUNC on: [, [[, <, or << brackets.
Func must take a single numeric argument of 1 to process one
delimited grouping."
  (ignore-errors
    (unless (save-excursion
	      (when (or (eq (char-after) (char-after (1+ (point))))
			(and (char-before)
			     (goto-char (1- (point)))
			     (eq (char-after) (char-after (1+ (point))))))
		;; double delimiters - dehighlight
		(let* ((sexp-start (point))
		       (sexp-end (scan-sexps sexp-start 1)))
		  (when sexp-end
		    (hproperty:but-clear-all-in-list
		     (hproperty:but-get-all-in-region
		      sexp-start sexp-end 'face hywiki-word-face))
		    (setq hywiki--highlighting-done-flag t)))))
      ;; single delimiters - highlight
      (funcall func 1))))

(defun hywiki--maybe-de/highlight-sexp (func direction-number &optional sexp-start sexp-end)
  "De/highlight HyWikiWord with FUNC on a single paired delimiter char.
DIRECTION-NUMBER is 1 for forward scanning and -1 for backward scanning."
  (setq sexp-start (or sexp-start (point))
	sexp-end (or sexp-end (scan-sexps sexp-start direction-number)))
  (when (and sexp-start sexp-end)
    (cl-destructuring-bind (start end)
	;; Point may be at end of sexp, so start and end may
	;; need to be reversed.
	(list (min sexp-start sexp-end) (max sexp-start sexp-end))
      ;; When `start' is at a delimiter, increment `sexp-start' so
      ;; regexp matching excludes the delimiter and starts with the
      ;; HyWikiWord.  But include any trailing delimiter or regexp
      ;; matching will not work.
      (save-restriction
	(when (memq (char-after start) '(?< ?> ?{ ?} ?\( ?\) ?\[ ?\] ?\"))
	  (setq start (1+ start)))
	(narrow-to-region start end)
	(prog1 (funcall func start end)
	  (setq hywiki--highlighting-done-flag nil))))))

(defun hywiki--maybe-dehighlight-at-point ()
  "Dehighlight any existing HyWikiWord when needed.
That is, only if the editing command has changed the word-only part of
the HyWikiWord reference."
  (when (or hywiki--buttonize-range
	    (and hywiki--word-pre-command
		 (not (equal hywiki--word-pre-command
			     (hywiki-get-singular-wikiword
			      (or (car hywiki--range)
				  (when (hywiki--buttonized-region-p)
				    (buffer-substring hywiki--buttonize-start
						      hywiki--buttonize-end))
				  (when (and (setq hywiki--range
						   (hywiki-word-get-range))
					     (nth 1 hywiki--range))
				    (prog1 (nth 1 hywiki--range)
				      (setq hywiki--range nil)))))))))
    ;; Dehighlight if point is on or between a HyWikiWord
    (hywiki-maybe-dehighlight-between-references)))

(defun hywiki--maybe-rehighlight-at-point ()
  "Highlight any existing HyWikiWord when needed.
That is, only if the editing command has changed the word-only part of
the HyWikiWord reference.

This must be called within a `save-excursion' or it may move point."

  (hywiki--maybe-dehighlight-at-point)

  ;; Highlight wikiwords around point as needed
  (hywiki-maybe-highlight-on-reference)

  (when (hywiki--buttonized-region-p)
    (hywiki--maybe-de/highlight-sexp
     #'hywiki-maybe-highlight-references 1
     hywiki--buttonize-start hywiki--buttonize-end))

  (when (= (char-syntax (or (char-before) 0)) ?\ )
    (goto-char (1- (point))))
  (hywiki-maybe-highlight-between-references))

(defun hywiki--org-set-heading-regexp ()
  "Includes all custom todo keywords defined in `hywiki-directory' in regexp.
Initializes `hywiki--org-todo-regexp' and `hywiki--org-heading-regexp'."
  (setq hywiki--org-todo-regexp (hywiki-org-directory-todo-regexp hywiki-directory)
        hywiki--org-heading-regexp
        ;; org-complex-heading-regexp + custom todo keywords
        (concat
         ;; Make leading asterisks optional since (org-get-heading) may have
         ;; already removed them.
         "^\\(\\*+[ \t]+\\)?"
         ;; optional todo keyword
	 "\\(?:" hywiki--org-todo-regexp "\\)?"
         ;; optional priority
	 "\\(?:[ \t]*\\(\\[#.\\]\\)\\)?"
         ;; optional title and stats
	 "\\(?:[ \t]*\\(.*?\\)\\)??"
         ;; optional tags
	 "\\(?:[ \t]*\\(:[[:alnum:]_@#%:]+:\\)\\)?"
	 "[ \t]*$")))

;;; ************************************************************************
;;; Private initializations
;;; ************************************************************************

;; Must be set after `hywiki-get-buttonize-characters' is defined
(unless hywiki--buttonize-character-regexp
  (setq hywiki--buttonize-character-regexp
	(concat "\\([]["
		(regexp-quote (concat "()<>{}\"' \t\r\n"
				      (hywiki-get-buttonize-characters)))
		"]\\|$\\)")
	hywiki--word-and-buttonize-character-regexp
	(concat "\\(" hywiki-word-with-optional-suffix-regexp "\\)"
		hywiki--buttonize-character-regexp)))

(hywiki--org-set-heading-regexp)

;;; ************************************************************************
;;; Public initializations
;;; ************************************************************************

(add-hook 'kill-buffer-hook 'hywiki-kill-buffer-hook)

;; Use for its side effects, setting variables
(eval-after-load "ox-publish" '(hywiki-org-get-publish-project))

;; Ensure HyWiki referent lookup table is initialized as are HyWiki Org
;; Publish settings.
(hywiki-set-directory 'hywiki-directory hywiki-directory)

(provide 'hywiki)

;;; hywiki.el ends here
