This file defines elot-mode, a minor mode that activates when editing
an ELOT ontology document in Org-mode. It replaces the old
elot-defaults.el approach, where every ELOT file required a first-line
file-local variable:
# -*- eval: (load-library "elot-defaults") -*-
That mechanism had several problems:
- Every file open triggered a security prompt (“Allow eval in local variables?”)
- The setup was an imperative bag of side-effects with no teardown path
- Hooks were added locally but never removed
- Keybindings were set via
local-set-keywith no proper keymap
A define-minor-mode solves all of these: it provides a toggle, a
dedicated keymap, a modeline lighter, and clean enable/disable
functions.
elot-mode.org -> elot-mode.el (this file: minor mode definition) elot-tangling.org -> elot-tangle.el (AST parsing, OMN generation) elot-label-display.org -> elot-label-display.el (fontification, overlays, lookup) elot-defs.org -> elot.el (legacy: gradually being emptied)
elot-mode.el is thin orchestration glue. It require’s the modules,
defines the minor mode, and wires up hooks and keybindings.
Users can activate elot-mode in several ways:
- File-local variable (replaces the old
eval:cookie):# -*- eval: (elot-mode 1) -*- - A
.dir-locals.elin the project root:((org-mode . ((eval . (elot-mode 1))))) - Automatic detection on
org-mode-hook(see Auto-detection).
;;; elot-mode.el --- ELOT minor mode for ontology authoring -*- lexical-binding: t; -*-
;; Copyright (C) 2024, 2025, 2026 Johan W. Klüwer
;; Author: Johan W. Klüwer <johan.w.kluwer@gmail.com>
;; URL: https://github.com/johanwk/elot
;; Version: 2.0.0
;; Keywords: languages outlines tools org ontology
;; This file is not part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; `elot-mode' is a buffer-local minor mode for authoring OWL
;; ontologies in Org-mode using the ELOT (Emacs Literate Ontology
;; Tool) conventions.
;;
;; Enabling the mode sets up:
;; - Syntax table tweaks (`:' and `_' as word-constituent)
;; - Buffer-local hooks for tangling, saving, and display
;; - Label-display overlays (toggled with F5)
;; - Org babel Library-of-Babel ingestion
;;
;; Disabling the mode cleanly tears down all of the above.
;;
;; See `elot-mode-map' for keybindings.
;;; Code:Load the core ELOT modules (zero external dependencies). Then
attempt to load the full elot package, which brings in SPARQL
execution, LaTeX export filters, OWL import, etc.
The (require 'elot nil t) call is soft: if elot.el (or any of
its external dependencies) is not installed, it silently falls back
to the core. Every menu item that depends on the full package is
already guarded with :active (fboundp ...), so the menu degrades
gracefully.
elot-lint is autoloaded – it is only pulled in when the user
explicitly calls elot-org-lint.
(require 'elot-tangle)
(require 'elot-db)
(require 'elot-sources)
(require 'elot-label-display)
(require 'xref)
(require 'easymenu)
(require 'tempo)
(require 'org-tempo)
(require 'ob-lob)
;; Load the full ELOT package (SPARQL, LaTeX export, OWL
;; import, etc.) when available. The `nil t' arguments mean "don't
;; signal an error if elot.el is not on `load-path'", so users who
;; only have the zero-dependency core still get a working `elot-mode'.
;; Features guarded by `fboundp' in the ELOT menu degrade gracefully.
(require 'elot nil t)
;; Lint is optional -- only loaded when the user invokes it
(autoload 'elot-org-lint "elot-lint" "Refresh `elot-slurp', then run `org-lint'." t)The elot customization group collects all user-facing options. It
must be defined early, before any defcustom that references :group
'elot.
(defgroup elot
nil
"Customization group for Emacs Literate Ontology Tool (ELOT)."
:prefix "elot-"
:group 'org)
(defcustom elot-label-display-size-threshold (* 500 1024)
"Buffer size (bytes) above which `elot-mode' asks before enabling label-display.
When a file is larger than this threshold, the user is prompted
with a yes/no question. Files at or below the threshold get
label-display automatically. Set to 0 to always ask, or nil to
never ask."
:type '(choice (const :tag "Never ask -- always enable" nil)
(integer :tag "Byte threshold"))
:group 'elot)These are the non-LaTeX settings formerly set inside the progn block of
elot-defaults.el. They are applied when the mode is enabled and have
no teardown needed (they are buffer-local and vanish with the buffer).
(defun elot-mode--set-buffer-locals ()
"Set buffer-local variables for an ELOT buffer.
These correspond to the `setq-local' block that was in `elot-defaults.el',
minus the LaTeX-specific settings."
(setq-local
org-confirm-babel-evaluate nil
org-export-allow-bind-keywords t
org-babel-default-inline-header-args '((:exports . "code"))
tempo-interactive t
time-stamp-line-limit 100
time-stamp-format "%Y-%m-%d %H:%M"
time-stamp-active t
time-stamp-start "(version of "
time-stamp-end ")"
org-startup-folded 'show2levels
org-export-with-sub-superscripts nil ; preserve "_" in exports
org-export-headline-levels 8 ; deep numbering
org-export-with-section-numbers 8)) ; deep numberingELOT treats : and _ as word constituents so that CURIEs like
ex:MyClass and identifiers like BFO_0000001 are recognised as
single words by movement commands and thing-at-point.
We derive a syntax table from org-mode-syntax-table so we can swap it
in on enable and restore the original on disable.
(defun elot--in-elot-buffer-p ()
"Return non-nil when the current buffer is an active ELOT Org buffer.
Used as an `:enable' guard in the ELOT menu to grey out entries that
only make sense inside an ELOT Org file (tangle, lint, templates,
xref navigation, etc.) when the menu is visible via
`elot-global-label-display-mode' in a non-ELOT buffer."
(and (derived-mode-p 'org-mode)
(bound-and-true-p elot-mode)))(defvar elot-mode-syntax-table
(let ((st (make-syntax-table org-mode-syntax-table)))
(modify-syntax-entry ?: "w" st)
(modify-syntax-entry ?_ "w" st)
st)
"Syntax table for `elot-mode'.
Treats `:' and `_' as word-constituent characters.")ELOT uses `tempo-template` to provide skeleton structures for ontologies and resources.
Insert a document header with <oh.
(tempo-define-template "elot-doc-header"
'("#+title: " (p "Document title: " doctitle) > n
"#+subtitle: An OWL ontology" > n
"#+author: " (p "Author name: " authname) > n
"#+date: Created " (format-time-string "%Y-%m-%d %H:%M") > n
"#+call: theme-elot()" n n)
"<odh"
"ELOT document header"
'org-tempo-tags)Insert a skeleton with <ods.
(tempo-define-template "elot-ont-skeleton"
'(n > "* " (p "Ontology identifier localname: " ontlocalname) > n
":PROPERTIES:" > n
":ID: " (s ontlocalname) > n
":ELOT-context-type: ontology" > n
":ELOT-context-localname: " (s ontlocalname) > n
":ELOT-default-prefix: " (p "Namespace prefix for resources in this ontology (without the \":\") " resprefix) > n
":header-args:omn: :tangle ./" (s ontlocalname) ".omn :noweb yes" > n
":header-args:emacs-lisp: :tangle no :exports results" > n
":header-args: :padline yes" > n
":END:" > n
"** Prefixes
:PROPERTIES:
:prefixdefs: yes
:END:
#+caption: OWL ontology prefixes
| prefix | uri |
|-----------+--------------------------------------------------------------------------------|
| owl: | http://www.w3.org/2002/07/owl# |
| rdf: | http://www.w3.org/1999/02/22-rdf-syntax-ns# |
| xml: | http://www.w3.org/XML/1998/namespace |
| xsd: | http://www.w3.org/2001/XMLSchema# |
| rdfs: | http://www.w3.org/2000/01/rdf-schema# |
| skos: | http://www.w3.org/2004/02/skos/core# |
| dc: | http://purl.org/dc/elements/1.1/ |
| dcterms: | http://purl.org/dc/terms/ |" > n
"| " (s resprefix)
": | " (p "Resource namespace in full (\"http ...\") " resns) " |" > n
"| " (p "Namespace prefix for the ontology itself (without the \":\") " ontprefix)
": | " (p "Ontology namespace in full (\"http ...\") " ontns) " |" > n
"
** " (s ontlocalname) " ontology (" (s ontprefix) ":" (s ontlocalname) " " (s ontprefix) ":" (s ontlocalname) "/0.0)
:PROPERTIES:
:ID: " (s ontlocalname) "-ontology-declaration
:custom_id: " (s ontlocalname) "-ontology-declaration
:resourcedefs: yes
:END:
- owl:versionInfo :: 0.0 start of " (s ontlocalname) "
- dcterms:title :: \"" (s ontlocalname) " ontology\"@en
- dcterms:license :: [[https://creativecommons.org/licenses/by-sa/4.0/]]
- dcterms:creator :: {{{author}}}
- dcterms:modified :: {{{time(\"%Y-%m-%d\",t)}}}^^xsd:date
- dcterms:publisher :: https://example.org/thepublisher
- dc:rights :: Copyright info here
- dcterms:description :: The " (s ontlocalname) " ontology is ...
- rdfs:comment :: The " (s ontlocalname) " ontology is ...
** Datatypes
:PROPERTIES:
:ID: " (s ontlocalname) "-datatypes
:custom_id: " (s ontlocalname) "-datatypes
:resourcedefs: yes
:END:
** Classes
:PROPERTIES:
:ID: " (s ontlocalname) "-class-hierarchy
:custom_id: " (s ontlocalname) "-class-hierarchy
:resourcedefs: yes
:END:
*** My class (" (s resprefix) ":MyClass)
- rdfs:comment :: Leave a comment here
** Object properties
:PROPERTIES:
:ID: " (s ontlocalname) "-object-property-hierarchy
:custom_id: " (s ontlocalname) "-object-property-hierarchy
:resourcedefs: yes
:END:
** Data properties
:PROPERTIES:
:ID: " (s ontlocalname) "-data-property-hierarchy
:custom_id: " (s ontlocalname) "-data-property-hierarchy
:resourcedefs: yes
:END:
** Annotation properties
:PROPERTIES:
:ID: " (s ontlocalname) "-annotation-property-hierarchy
:custom_id: " (s ontlocalname) "-annotation-property-hierarchy
:resourcedefs: yes
:END:
*** owl:versionInfo
*** dcterms:title
- rdfs:isDefinedBy :: http://purl.org/dc/terms/
*** dcterms:license
- rdfs:isDefinedBy :: http://purl.org/dc/terms/
*** dcterms:creator
- rdfs:isDefinedBy :: http://purl.org/dc/terms/
*** dcterms:modified
- rdfs:isDefinedBy :: http://purl.org/dc/terms/
*** dcterms:publisher
- rdfs:isDefinedBy :: http://purl.org/dc/terms/
*** dcterms:description
- rdfs:isDefinedBy :: http://purl.org/dc/terms/
*** dc:rights
- rdfs:isDefinedBy :: http://purl.org/dc/elements/1.1/
*** skos:example
- rdfs:isDefinedBy :: http://www.w3.org/2004/02/skos/core
*** skos:prefLabel
- rdfs:isDefinedBy :: http://www.w3.org/2004/02/skos/core
*** skos:altLabel
- rdfs:isDefinedBy :: http://www.w3.org/2004/02/skos/core
*** skos:definition
- rdfs:isDefinedBy :: http://www.w3.org/2004/02/skos/core
** Individuals
:PROPERTIES:
:ID: " (s ontlocalname) "-individuals
:custom_id: " (s ontlocalname) "-individuals
:resourcedefs: yes
:END:
"
(progn (elot-update-link-abbrev)
(save-buffer) (org-macro-initialize-templates)
(org-cycle-set-startup-visibility)
(goto-char (point-min))
(search-forward "dcterms:description :: ") (outline-show-entry) ""))
"<ods"
"ELOT ontology sections skeleton"
'org-tempo-tags)
;;
;; end of template 'elot-ont-skeleton'
;;Insert a class heading with SKOS annotation properties and completion cookies.
(tempo-define-template "elot-class-skos"
'(
(org-open-line 1)
(make-string (max 3 (org-current-level)) ?*) " "
(p "Class label: ") " ("
(elot-default-prefix) ":" (p "localname: ") ")" > n
" - [ ] skos:definition :: " > n
" - [ ] skos:example :: " > )
"<ocp"
"ELOT class heading with SKOS annotations"
'org-tempo-tags)
(tempo-define-template "elot-property-skos"
'((org-open-line 1)
(make-string (max 3 (org-current-level)) ?*) " "
(p "Property label: ") " ("
(elot-default-prefix) ":" (p "localname: ") ")" > n
" - [ ] skos:definition :: " > n
" - [ ] skos:example :: " > )
"<op"
"ELOT property heading with SKOS annotations"
'org-tempo-tags)(tempo-define-template "elot-block-robot-metrics"
'(
(org-open-line 1) p
"#+call: robot-metrics(omnfile=\"" (elot-context-localname) ".omn\") :eval never-export" >
(progn (message "Execute blocks with C-c C-c") ""))
"<obm"
"ELOT ontology metrics from ROBOT"
'org-tempo-tags)
(tempo-define-template "elot-block-sparql-select"
'(
(org-open-line 1)
"#+name: " (p "Select query name: ") > n
"#+begin_src sparql :url \"" (elot-context-localname) ".omn\" :eval never-export :exports results
select
{
}
#+end_src" n
(progn (message "Execute blocks with C-c C-c") ""))
"<obs"
"ELOT SPARQL SELECT from OMN "
'org-tempo-tags)
(tempo-define-template "elot-block-sparql-construct"
'(
(org-open-line 1)
"#+name: " (p "Construct query name: ") > n
"#+begin_src sparql :url \"" (elot-context-localname) ".omn\" :eval never-export :exports results"
" :format ttl :wrap \"src ttl\" :cache yes :post kill-prefixes(data=*this*) :eval never-export
construct {
} {
}
#+end_src" n
(progn (message "Execute blocks with C-c C-c") ""))
"<obc"
"ELOT SPARQL CONSTRUCT from OMN "
'org-tempo-tags)
(tempo-define-template "elot-block-rdfpuml-diagram"
'(
(org-open-line 1)
"#+name: rdfpuml:" (p "Name of Turtle source block for diagram: " ttl-source) > n
"#+call: rdfpuml-block(ttlblock=\"" (s ttl-source) "\") :eval never-export" > n
"#+caption: " (p "Caption: ") > n
"#+results: rdfpuml:" (s ttl-source) > n
(progn (message "Execute blocks with C-c C-c") ""))
"<obm"
"ELOT ontology metrics from ROBOT"
'org-tempo-tags)(tempo-define-template "elot-table-of-resources"
'(
(org-open-line 1)
"#+name: tbl:" (p "Name of resource table: " tblname) > n
"| id | super | rdfs:label |" > n
"|-----+-------+------------|" > n
"| " (elot-default-prefix) ": | " (elot-default-prefix) ": | |" > n
(progn (previous-line) (forward-word)
(message "Insert outline with M-x elot-headings-from-table") ""))
"<otr"
"ELOT table of resources"
'org-tempo-tags)(defun elot--coerce-literal (header value)
"Return VALUE coerced per HEADER decoration.
If HEADER has no decoration the original VALUE is returned
unchanged (so numbers stay numbers, symbols stay symbols, etc.)."
(cond
;; language tag
((string-match "@\\([[:alnum:]-]+\\)\\'" header)
(let ((lang (match-string 0 header))) ; includes leading @
(if (or (null value) (and (stringp value) (string= value "")))
value ; keep empty cell empty
(format "\"%s\"%s" (format "%s" value) lang))))
;; datatype
((string-match "\\^\\^\\(.+\\)\\'" header)
(let ((dtype (match-string 0 header))) ; includes leading ^^
(if (or (null value) (and (stringp value) (string= value "")))
value
(format "\"%s\"%s" (format "%s" value) dtype))))
(t value))) ; no decoration -> untouched
(defun elot--strip-decoration (header)
"Return HEADER without trailing @lang or ^^dtype part."
(cond ((string-match "@[[:alnum:]-]+\\'" header)
(substring header 0 (match-beginning 0)))
((string-match "\\^\\^.+\\'" header)
(substring header 0 (match-beginning 0)))
(t header)))
(defun elot--table->forest (mini-table)
"Convert MINI-TABLE (Org-babel list) to a forest of plists.
- Header suffixes `@lang' / `^^datatype' decorate the cell value
but are removed from the stored key.
- Warns if a row's SUPER value never appears as an ID.
- Skips `hline` markers that `org-table-to-lisp` inserts."
(let* ((headers (car mini-table))
(rows (cdr mini-table))
(id-col (cl-position "id" headers :test #'string=))
(sup-col (cl-position "super" headers :test #'string=))
(id->obj (make-hash-table :test 'equal))
triples)
;; -- pass 1: build node plists and register them ---------------
(dolist (row rows)
(when (listp row) ; skip `hline`
(let* ((id (nth id-col row))
(sup (nth sup-col row))
(pl (append
;; copy columns: stripped key / decorated value
(apply #'append
(cl-mapcar (lambda (hdr val)
(list (elot--strip-decoration hdr)
(elot--coerce-literal hdr val)))
headers row))
(list :subs nil))))
(push (list id sup pl) triples)
(puthash id pl id->obj))))
(setq triples (nreverse triples))
;; -- pass 2: link children to parents, warn on missing parents -
(dolist (tr triples)
(cl-destructuring-bind (id sup child) tr
(unless (string= sup "")
(let ((parent (gethash sup id->obj)))
(if parent
(setf (plist-get parent :subs)
(append (plist-get parent :subs) (list child)))
(message "elot--table->forest: WARNING -- parent id \"%s\" referenced by \"%s\" not found"
sup id)
(setf (nth 1 tr) "")))))) ; promote to root
;; -- pass 3: collect root nodes in original order --------------
(let (forest)
(dolist (tr triples)
(cl-destructuring-bind (_id sup pl) tr
(when (string= sup "") (push pl forest))))
(nreverse forest))))
(defun elot--prop (plist key)
"Return KEY's value in PLIST, comparing keys with `string=`."
(cl-loop for (k v) on plist by #'cddr
when (and (stringp k) (string= k key))
return v))
(defun elot-forest->org (forest &optional level)
"Render FOREST (from `elot--table->forest`) as Org headlines.
LEVEL is the asterisk depth for root nodes (default 4)."
(setq level (or level 4))
(let ((lines '()))
(cl-labels
((emit (node depth)
(let* ((stars (make-string (+ level depth) ?*))
(id (elot--prop node "id"))
(label (elot--prop node "rdfs:label"))
(name (if (and label (not (string= label "")))
(format "%s (%s)" label id)
id)))
;; headline
(push (concat stars " " name) lines)
;; description list (skip empty values and certain keys)
(cl-loop for (k v) on node by #'cddr
when (and (stringp k)
(not (member k '("id" "super")))
v
(not (string= v "")))
do (push (format "- %s :: %s" k v) lines))
;; children
(dolist (child (plist-get node :subs))
(emit child (1+ depth))))))
(dolist (root forest) (emit root 0)))
(mapconcat #'identity (nreverse lines) "\n")))
(defun elot-headings-from-table ()
"Convert the Org table at point to an ELOT outline.
The table must contain column headers `id' and `super'.
Entities in `id' with optional `rdfs:label' produce
an outline indented under the current headline.
Additional columns are output as description lists entries.
Column headers for these should be annotation
properties (`skos:synonym', etc.) or OWL Manchester Syntax
keywords (`EquivalentTo', `Domain', etc). Suffixes like `@en'
or `xsd:integer' on a column header will be applied to values."
(interactive)
(unless (org-at-table-p)
(user-error "Point is not inside an Org table"))
;; 1. read the table as lisp
(let* ((mini-table (org-table-to-lisp))
(headers (car mini-table)))
;; 2. must contain \"id\"
(unless (member "id" headers)
(user-error "Table lacks required \"id\" column -- cannot create headings"))
;; 3. outline depth = one deeper than containing headline
(let ((target-level (save-excursion
(org-back-to-heading t)
(1+ (org-outline-level)))))
;; 4. position after table & any TBLFM lines
(let ((insert-pos
(save-excursion
(goto-char (org-table-end)) ; end of last | row
;; skip following #+tblfm lines
(while (and (not (eobp))
(progn (beginning-of-line)
(looking-at "^[ \t]*#\\+tblfm:")))
(forward-line 1))
(point))))
;; 5. build forest -> org text and insert
(let* ((forest (elot--table->forest mini-table))
(org-text (elot-forest->org forest target-level)))
(goto-char insert-pos)
(insert "\n" org-text "\n"))))))A proper keymap that activates and deactivates with the mode. This
replaces the ad-hoc local-set-key calls in elot-setup-org-keybindings.
(defvar elot-mode-map
(let ((map (make-sparse-keymap)))
;; The optional binding for `elot-toggle-label-display' is
;; installed at mode-enable time (see `elot-mode--apply-toggle-key'
;; called from `elot-mode--enable'), not here, because the
;; user's customized value of `elot-toggle-labels-key' is not
;; necessarily available when this `defvar' runs at load time.
(define-key map (kbd "C-c C-x r") #'elot-label-lookup)
;; S-<f5> previously bound to a Shift-F5 menu; that menu has
;; been retired in favour of the ELOT easymenu ("ELOT" in the
;; menu bar).
map)
"Keymap active when `elot-mode' is enabled.")(easy-menu-define elot-menu elot-mode-map
"ELOT Ontology Authoring Menu."
'("ELOT"
["Check for common problems" elot-org-lint
:active (fboundp 'elot-org-lint)
:enable (elot--in-elot-buffer-p)]
["Export to OWL (Tangle)" elot-tangle-buffer-to-omn
:enable (elot--in-elot-buffer-p)]
["Export to HTML" (lambda () (interactive) (browse-url-of-file (expand-file-name (org-html-export-to-html))))
:active (fboundp 'org-html-export-to-html)
:enable (elot--in-elot-buffer-p)]
["Import OWL ontology" elot-open-owl
:active (fboundp 'elot-open-owl)
:enable (elot--in-elot-buffer-p)]
"---"
["Insert Existing Resource ID" elot-label-lookup :active (fboundp 'elot-label-lookup)]
["... (local only)" elot-label-lookup-local
:active (fboundp 'elot-label-lookup-local)
:enable (and (bound-and-true-p elot-attriblist-ht)
(hash-table-p elot-attriblist-ht)
(> (hash-table-count elot-attriblist-ht) 0))]
["... (external only)" elot-label-lookup-external
:active (fboundp 'elot-label-lookup-external)
:enable (bound-and-true-p elot-active-label-sources)]
["Insert Class heading" (lambda () (interactive) (outline-next-heading) (tempo-template-elot-class-skos))
:active (fboundp 'tempo-template-elot-class-skos)
:enable (elot--in-elot-buffer-p)]
["Insert Property heading" (lambda () (interactive) (outline-next-heading) (tempo-template-elot-property-skos))
:active (fboundp 'tempo-template-elot-property-skos)
:enable (elot--in-elot-buffer-p)]
"---"
["Jump to Resource Headline (M-.)" xref-find-definitions
:enable (elot--in-elot-buffer-p)]
["Find References to Resource (M-?)" xref-find-references
:enable (elot--in-elot-buffer-p)]
["Quick-Describe Resource" elot-describe-curie-at-point
:enable (elot--in-elot-buffer-p)]
["Toggle Label-Display" elot-toggle-label-display :active (fboundp 'elot-toggle-label-display)]
["Refresh Label-Display Index" elot-label-display-setup
:active (fboundp 'elot-label-display-setup)
:enable (elot--in-elot-buffer-p)]
"---"
["Insert Table declaring Resources" tempo-template-elot-table-of-resources
:active (fboundp 'tempo-template-elot-table-of-resources)
:enable (elot--in-elot-buffer-p)]
["Generate Outline from Table" elot-headings-from-table
:active (fboundp 'elot-headings-from-table)
:enable (elot--in-elot-buffer-p)]
"---"
["Insert SPARQL Select Block" tempo-template-elot-block-sparql-select
:active (fboundp 'tempo-template-elot-block-sparql-select)
:enable (elot--in-elot-buffer-p)]
["Insert SPARQL Construct Block" tempo-template-elot-block-sparql-construct
:active (fboundp 'tempo-template-elot-block-sparql-construct)
:enable (elot--in-elot-buffer-p)]
["Insert RDFPUML Diagram Block" tempo-template-elot-block-rdfpuml-diagram
:active (fboundp 'tempo-template-elot-block-rdfpuml-diagram)
:enable (elot--in-elot-buffer-p)]
"---"
["Insert New Document Header" tempo-template-elot-doc-header
:active (fboundp 'tempo-template-elot-doc-header)
:enable (elot--in-elot-buffer-p)]
["Insert New Ontology Skeleton" tempo-template-elot-ont-skeleton
:active (fboundp 'tempo-template-elot-ont-skeleton)
:enable (elot--in-elot-buffer-p)]
"---"
("Labels & sources"
("Source registration"
["Register current buffer as label source"
elot-label-register-current-buffer
:active (fboundp 'elot-label-register-current-buffer)]
["Register label source..."
elot-label-register-source
:active (fboundp 'elot-label-register-source)]
["Unregister label source..."
elot-label-unregister-source
:active (fboundp 'elot-label-unregister-source)
:enable (and (fboundp 'elot-db-list-sources)
(consp (ignore-errors (elot-db-list-sources))))]
["Refresh label source..."
elot-label-refresh-source
:active (fboundp 'elot-label-refresh-source)
:enable (and (fboundp 'elot-db-list-sources)
(consp (ignore-errors (elot-db-list-sources))))]
["Refresh all label sources"
elot-label-refresh-all-sources
:active (fboundp 'elot-label-refresh-all-sources)
:enable (and (fboundp 'elot-db-list-sources)
(consp (ignore-errors (elot-db-list-sources))))]
["List registered label sources"
elot-label-list-sources
:active (fboundp 'elot-label-list-sources)])
("Active sources (per buffer / project)"
["Activate label source in buffer..."
elot-label-activate-source
:active (fboundp 'elot-label-activate-source)]
["Deactivate label source in buffer..."
elot-label-deactivate-source
:active (fboundp 'elot-label-deactivate-source)
:enable (bound-and-true-p elot-active-label-sources)]
["List/reorder active label sources"
elot-label-list-active-sources
:active (fboundp 'elot-label-list-active-sources)]
["Raise active source priority..."
elot-label-move-source-up
:active (fboundp 'elot-label-move-source-up)
:enable (bound-and-true-p elot-active-label-sources)]
["Lower active source priority..."
elot-label-move-source-down
:active (fboundp 'elot-label-move-source-down)
:enable (bound-and-true-p elot-active-label-sources)])
("Display"
["Toggle global label display (this buffer)"
elot-global-label-display-mode
:active (fboundp 'elot-global-label-display-mode)
:style toggle
:selected (bound-and-true-p elot-global-label-display-mode)]
["Rebuild label-display matcher"
elot-global-label-display-setup
:active (fboundp 'elot-global-label-display-setup)]
["Toggle label display"
elot-toggle-label-display
:active (fboundp 'elot-toggle-label-display)
:style toggle
:selected (and (boundp 'elot-label-display)
(eq elot-label-display 'on))]))))
; Also surface the ELOT menu when `elot-global-label-display-mode'
;; is active in a non-ELOT buffer. We bind the same menu into the
;; global mode's keymap, but mark it `:visible' only when `elot-mode'
;; is NOT active -- that prevents a duplicate ELOT menu when both
;; modes are enabled in the same buffer (elot-mode-map already
;; provides the entry in that case).
;; `with-eval-after-load' guards the load order: if elot-label-display.el
;; loads later, the sibling snippet there handles the other direction.
;; NOTE: `package-lint' warns about `with-eval-after-load' in packages.
;; The warning is informational (configuration code belongs in user
;; init); here it is intentional -- a cross-module load-order bridge
;; between two modules of the same package, not user-config injection.
(with-eval-after-load 'elot-label-display
(when (and (boundp 'elot-global-label-display-mode-map)
(boundp 'elot-menu))
(define-key elot-global-label-display-mode-map
[menu-bar ELOT]
`(menu-item "ELOT" ,elot-menu
:visible (not (bound-and-true-p elot-mode))))))All buffer-local hooks are added on enable and removed on disable.
This is the key improvement over elot-defaults.el, which only added
hooks and never removed them.
(defun elot-mode--refresh-inline-images ()
"Refresh inline image previews after a Babel block executes.
Use `org-link-preview-refresh' on Org 9.8+, falling back to
the older `org-redisplay-inline-images' on earlier versions."
(if (fboundp 'org-link-preview-refresh)
(org-link-preview-refresh)
(when (fboundp 'org-redisplay-inline-images)
(org-redisplay-inline-images))))
(defun elot-mode--add-hooks ()
"Add buffer-local hooks for an ELOT buffer."
;; Pre-tangle: remember source file
(add-hook 'org-babel-pre-tangle-hook #'elot--remember-org-source nil t)
(add-hook 'org-babel-after-execute-hook #'elot-mode--refresh-inline-images nil t)
;; After save: refresh link abbreviations and slurp data
(add-hook 'after-save-hook #'elot-update-link-abbrev nil t)
(add-hook 'after-save-hook #'elot-slurp-to-vars nil t)
;; xref: register the ELOT backend for M-. / M-?
(add-hook 'xref-backend-functions #'elot-xref-backend nil t))
(defun elot-mode--remove-hooks ()
"Remove buffer-local hooks added by `elot-mode--add-hooks'."
(remove-hook 'org-babel-pre-tangle-hook #'elot--remember-org-source t)
(remove-hook 'org-babel-after-execute-hook #'elot-mode--refresh-inline-images t)
(remove-hook 'after-save-hook #'elot-update-link-abbrev t)
(remove-hook 'after-save-hook #'elot-slurp-to-vars t)
(remove-hook 'xref-backend-functions #'elot-xref-backend t))The ELOT-specific around-advice on org-babel-execute:sparql (defined
in elot-defs.org block src-sparql-exec-patch) is not installed at
require time – that would mutate global state for every Emacs session
that merely loads ELOT, which MELPA discourages. Instead it is
installed by elot-mode--enable and removed by elot-mode--disable.
A single boolean, elot--sparql-advice-installed-p, tracks whether
the advice is currently in place so that multiple ELOT buffers share
a single installation: enabling elot-mode in additional buffers is a
no-op for the advice, and disabling elot-mode in the last such
buffer fully uninstalls it. The advice body itself still guards on
elot--is-elot-buffer as defence in depth.
(defvar elot--sparql-advice-installed-p nil
"Non-nil when ELOT's around-advice on `org-babel-execute:sparql' is active.
Managed by `elot-mode--install-sparql-advice' and
`elot-mode--uninstall-sparql-advice'.")
(defvar elot--sparql-advice-buffer-count 0
"Number of live buffers in which `elot-mode' is currently enabled.
When this drops to zero, `elot-mode--uninstall-sparql-advice' removes
the global advice on `org-babel-execute:sparql'.")
(defun elot-mode--install-sparql-advice ()
"Install ELOT's around-advice on `org-babel-execute:sparql'.
Idempotent: safe to call from every `elot-mode--enable'."
(cl-incf elot--sparql-advice-buffer-count)
(unless elot--sparql-advice-installed-p
(when (fboundp 'elot--custom-org-babel-execute-sparql)
(advice-add 'org-babel-execute:sparql :around
#'elot--custom-org-babel-execute-sparql)
(setq elot--sparql-advice-installed-p t))))
(defun elot-mode--uninstall-sparql-advice ()
"Remove ELOT's around-advice on `org-babel-execute:sparql'.
Uninstalled when no ELOT buffer is left. Idempotent."
(when (> elot--sparql-advice-buffer-count 0)
(cl-decf elot--sparql-advice-buffer-count))
(when (and elot--sparql-advice-installed-p
(<= elot--sparql-advice-buffer-count 0))
(advice-remove 'org-babel-execute:sparql
#'elot--custom-org-babel-execute-sparql)
(setq elot--sparql-advice-installed-p nil
elot--sparql-advice-buffer-count 0)))In addition to the SPARQL advice handled above, elot-mode.el also
installs a small set of global xref customisations: an :before
advice on xref-find-references (elot--capture-slurp, which copies
the current buffer’s elot-slurp into elot-slurp-global so the
*xref* buffer can render label overlays), and two entries on
xref-after-update-hook (elot--xref-label-overlay-setup and
elot--xref-buffer-enable-backend).
These were originally installed at top level when elot-mode.el
loaded, which mutated global state for every Emacs session that
merely required ELOT. Following the same pattern as the SPARQL
advice, installation is now driven by the elot-mode lifecycle and
reference-counted across ELOT buffers: the first elot-mode--enable
installs; subsequent calls only bump the count; elot-mode--disable
in the last ELOT buffer fully uninstalls.
The install helper guards each advice-add / add-hook with
fboundp because the backing functions are defined further down in
the file (after the xref backend), so during require time the
helpers exist before their targets do. Installation runs from
elot-mode--enable which executes after the file is fully loaded,
so the guards are belt-and-braces rather than strictly required.
(defvar elot--xref-globals-installed-p nil
"Non-nil when ELOT's global xref advice and hooks are installed.
Managed by `elot-mode--install-xref-globals' and
`elot-mode--uninstall-xref-globals'.")
(defvar elot--xref-globals-buffer-count 0
"Reference count of live ELOT buffers for global xref management.
Counts buffers with `elot-mode' enabled, for the purpose of managing
ELOT's global xref advice and hooks. When this drops to zero,
`elot-mode--uninstall-xref-globals' removes the global advice on
`xref-find-references' and the entries on `xref-after-update-hook'.")
(defun elot-mode--install-xref-globals ()
"Install ELOT's global xref advice and `xref-after-update-hook' entries.
Idempotent: safe to call from every `elot-mode--enable'. The
backing helpers are defined later in this file, so each install
is guarded with `fboundp' so that loading order does not matter."
(cl-incf elot--xref-globals-buffer-count)
(unless elot--xref-globals-installed-p
(when (fboundp 'elot--capture-slurp)
(advice-add 'xref-find-references :before #'elot--capture-slurp))
(when (fboundp 'elot--xref-label-overlay-setup)
(add-hook 'xref-after-update-hook #'elot--xref-label-overlay-setup))
(when (fboundp 'elot--xref-buffer-enable-backend)
(add-hook 'xref-after-update-hook #'elot--xref-buffer-enable-backend))
(setq elot--xref-globals-installed-p t)))
(defun elot-mode--uninstall-xref-globals ()
"Remove ELOT's global xref advice and hooks.
Uninstalled when no ELOT buffer is left. Idempotent."
(when (> elot--xref-globals-buffer-count 0)
(cl-decf elot--xref-globals-buffer-count))
(when (and elot--xref-globals-installed-p
(<= elot--xref-globals-buffer-count 0))
(advice-remove 'xref-find-references #'elot--capture-slurp)
(remove-hook 'xref-after-update-hook #'elot--xref-label-overlay-setup)
(remove-hook 'xref-after-update-hook #'elot--xref-buffer-enable-backend)
(setq elot--xref-globals-installed-p nil
elot--xref-globals-buffer-count 0)))elot-package/elot-lob.org contains supported, user-facing
#+call:-style helpers (rdfpuml-block, kill-prefixes,
theme/setupfile helpers, robot-metrics, robot-sparql-select, the
date helpers, etc.). These named blocks are made available to every
ELOT Org buffer by ingesting the file via org-babel-lob-ingest.
Ingestion is a global, idempotent side effect on Org’s Library of
Babel: once any ELOT buffer has ingested elot-lob.org, every other
Org buffer in the same Emacs session can resolve #+call:
rdfpuml-block(...) and friends. The elot--lob-ingested-p flag
ensures we ingest only once per Emacs session, so re-enabling
elot-mode in another buffer (or in the same buffer after a toggle)
does not pay the parse cost again.
locate-library "elot-mode" resolves to the directory of the loaded
elot-mode.el, which sits next to elot-lob.org in both a Git
checkout and an installed MELPA package; that gives us a portable
way to find the file without baking in any path.
There is no corresponding teardown in elot-mode--disable: ingested
LOB blocks are harmless in non-ELOT buffers, and Org provides no
public API to remove individual entries from the Library of Babel.
(defvar elot--lob-ingested-p nil
"Non-nil once ELOT's `elot-lob.org' has been ingested in this Emacs.
Guards `elot-mode--ingest-lob' against redundant re-ingestion when
`elot-mode' is enabled in additional buffers.")
(defun elot-mode--ingest-lob ()
"Ingest ELOT's `elot-lob.org' into Org's Library of Babel.
No-op after the first successful call in an Emacs session. Locates
`elot-lob.org' relative to the loaded `elot-mode.el' so it works
both from a Git checkout and from an installed MELPA package."
(unless elot--lob-ingested-p
(let* ((mode-lib (locate-library "elot-mode"))
(lob-file (and mode-lib
(expand-file-name
"elot-lob.org"
(file-name-directory mode-lib)))))
(when (and lob-file (file-readable-p lob-file))
(org-babel-lob-ingest lob-file)
(setq elot--lob-ingested-p t)))))The defvar elot-mode-map above runs at load time, before the
user’s custom-file has been read. Reading
elot-toggle-labels-key there would always see the default nil
and never install the user’s preferred binding. Instead the
binding is applied (and refreshed) at every elot-mode--enable
via the helper below.
A small state variable elot-mode--applied-toggle-key tracks the
last key we installed so we can unbind it cleanly if the user
later changes elot-toggle-labels-key via Customize.
(defvar elot-mode--applied-toggle-key nil
"Key currently bound to `elot-toggle-label-display' in `elot-mode-map'.
Tracked so `elot-mode--apply-toggle-key' can rebind cleanly when
the user changes `elot-toggle-labels-key' between activations.")
(defun elot-mode--apply-toggle-key ()
"Install the user's `elot-toggle-labels-key' binding in `elot-mode-map'.
Reads the current value via `elot--toggle-labels-key' (which also
emits the one-time hint when no value is set), unbinds any
previously-installed key, and binds the new one if any. Safe to
call from every `elot-mode--enable'."
(when (boundp 'elot-mode-map)
;; Unbind the previously-applied key, if any, so customize-driven
;; changes between activations don't leave stale bindings behind.
(when elot-mode--applied-toggle-key
(define-key elot-mode-map
(kbd elot-mode--applied-toggle-key) nil)
(setq elot-mode--applied-toggle-key nil))
(let ((key (and (fboundp 'elot--toggle-labels-key)
(elot--toggle-labels-key))))
(when key
(define-key elot-mode-map (kbd key)
#'elot-toggle-label-display)
(setq elot-mode--applied-toggle-key key)))))The enable function does what the progn block in elot-defaults.el used
to do, but in a structured and reversible way.
Label-display is set up synchronously via elot-mode--maybe-setup-labels.
For files larger than elot-label-display-size-threshold (default 500 KB),
the user is prompted first. The user can always run
M-x elot-label-display-setup later.
(defun elot-mode--enable ()
"Set up ELOT in the current Org buffer."
;; Guard: elot-mode is only meaningful inside Org-mode buffers.
;; Silently refuse when invoked in a non-Org file so that visiting
;; a .ttl (or any non-Org) file does not accidentally spawn a
;; second ELOT menu via this mode's keymap.
(unless (derived-mode-p 'org-mode)
(elot-mode -1)
(user-error "elot-mode is only supported in Org-mode buffers"))
;; 1. Buffer-local variables
(elot-mode--set-buffer-locals)
;; 2. Syntax table
(set-syntax-table elot-mode-syntax-table)
;; 3. Ingest the Library of Babel (once per Emacs session)
(elot-mode--ingest-lob)
;; 3b. Apply the user's toggle-labels keybinding (if any) into
;; `elot-mode-map' now that customize has had a chance to load.
(elot-mode--apply-toggle-key)
;; 4. Parse the headline hierarchy (populates elot-headline-hierarchy,
;; which is needed by link-abbrev refresh and label-display)
(elot-update-headline-hierarchy)
;; 5. Hooks
(elot-mode--add-hooks)
;; 6. SPARQL advice (global, but reference-counted across ELOT buffers)
(elot-mode--install-sparql-advice)
;; 7. Xref globals (advice on `xref-find-references' and entries on
;; `xref-after-update-hook'; reference-counted across ELOT buffers)
(elot-mode--install-xref-globals)
;; 8. LaTeX export filter for OMN in description lists
(when (fboundp 'elot-latex-filter-omn-item)
(add-to-list 'org-export-filter-item-functions
'elot-latex-filter-omn-item))
;; 9. Export pre-processing hook (linkify, CUSTOM_IDs, prefix resolution)
(when (fboundp 'elot--prepare-export-buffer)
(add-hook 'org-export-before-processing-functions
#'elot--prepare-export-buffer))
;; 10. Label-display: set up immediately (with size-gated prompt)
(elot-mode--maybe-setup-labels)
;; 11. Set initial fold visibility
(org-cycle-set-startup-visibility))
(defun elot-mode--maybe-setup-labels ()
"Set up label-display, prompting the user for large files.
If `elot-label-display-size-threshold' is nil, always set up.
If the buffer is larger than the threshold, ask the user first.
In batch mode (`noninteractive'), skip label-display entirely."
(unless noninteractive
(let ((threshold elot-label-display-size-threshold))
(when (or (null threshold)
(<= (buffer-size) threshold)
(y-or-n-p
(format "Buffer is %s KB -- enable ELOT label-display? "
(/ (buffer-size) 1024))))
(elot-label-display-setup)))))
(defun elot-mode--disable ()
"Tear down ELOT in the current Org buffer."
;; 1. Remove hooks
(elot-mode--remove-hooks)
;; 2. SPARQL advice (uninstall when this is the last ELOT buffer)
(elot-mode--uninstall-sparql-advice)
;; 3. Xref globals (uninstall when this is the last ELOT buffer)
(elot-mode--uninstall-xref-globals)
;; 4. LaTeX export filter
(setq org-export-filter-item-functions
(delq 'elot-latex-filter-omn-item
org-export-filter-item-functions))
;; 5. Export pre-processing hook
(remove-hook 'org-export-before-processing-functions
#'elot--prepare-export-buffer)
;; 6. Remove label-display overlays
(when (fboundp 'elot-remove-prop-display)
(with-silent-modifications
(elot-remove-prop-display)))
;; 7. Restore syntax table
(set-syntax-table org-mode-syntax-table))This is the central define-minor-mode form.
;;;###autoload
(define-minor-mode elot-mode
"Minor mode for authoring OWL ontologies in Org-mode with ELOT.
When enabled, sets up:
- Tangling hooks for OMN export and ROBOT conversion
- Label-display overlays (toggle with \\[elot-toggle-label-display])
- Syntax table tweaks for CURIE navigation
- Library of Babel ingestion
When disabled, all hooks, timers, and overlays are cleanly removed."
:lighter " ELOT"
:keymap elot-mode-map
(if elot-mode
(elot-mode--enable)
(elot-mode--disable)))Optionally, ELOT mode can be activated automatically when an Org buffer
contains the property :ELOT-context-type: ontology. This eliminates
the need for any file-local variable at all.
The detection function is added to org-mode-hook. It searches for the
ELOT marker property in the first few thousand characters of the
buffer – fast enough for interactive use.
(defcustom elot-mode-auto-detect t
"When non-nil, automatically enable `elot-mode' in ELOT buffers.
Detection looks for `:ELOT-context-type: ontology' in the buffer."
:type 'boolean
:group 'elot)
(defun elot-mode--maybe-enable ()
"Enable `elot-mode' if the current Org buffer looks like an ELOT file."
(when (and elot-mode-auto-detect
(derived-mode-p 'org-mode)
(not (bound-and-true-p elot-mode))
(save-excursion
(goto-char (point-min))
(re-search-forward
":ELOT-context-type:.*ontology" nil t)))
(elot-mode 1)))
;; NOTE: This `add-hook' is intentional and is the standard idiom
;; for a minor mode that opts users in based on Org-file content (the
;; :ELOT-context-type: property) rather than on a global preference.
;; The predicate `elot-mode--maybe-enable' is a cheap no-op for
;; non-ELOT Org buffers.
(add-hook 'org-mode-hook #'elot-mode--maybe-enable)This section defines xref backend support for ELOT buffers. It enables
cross-referencing identifiers such as CURIEs (e.g., :BFO_0000015) within
Org-mode ontology documents.
References are collected from description list entries, formatted with
their preceding Org heading, and shown in the *xref* buffer. Multi-line
entries are flattened to improve readability.
Definitions are resolved by searching open buffers for headings that contain
the identifier. This allows users to press M-. on any CURIE–even inside the
*xref* buffer–to jump to its defining section.
Label overlays are automatically enabled in the *xref* buffer, using context
from the originating ELOT buffer. Hooks and advice ensure the xref backend is
registered and the correct context is preserved across jumps.
(defun elot-xref-backend ()
"Return the ELOT xref backend identifier."
'elot)
(cl-defmethod xref-backend-identifier-at-point ((_backend (eql elot)))
"Return a CURIE at point, like :BFO_0000015, or nil if not found."
(let ((curie-regex "\\(?:\\sw\\|\\s_\\|:\\)+"))
(save-excursion
(skip-chars-backward "-_A-Za-z0-9:")
(when (looking-at curie-regex)
(match-string-no-properties 0)))))
(cl-defun elot--xref-find-matches (identifier &key find-definition)
"Return xref matches for IDENTIFIER in all ELOT buffers.
If FIND-DEFINITION is non-nil, restrict matches to headlines;
otherwise return every reference."
(let ((matches nil))
(dolist (buf (buffer-list))
(with-current-buffer buf
(when (and (derived-mode-p 'org-mode)
(boundp 'elot-slurp)) ; use whatever predicate marks an ELOT buffer
(save-excursion
(goto-char (point-min))
(let ((pattern (if find-definition
(concat "^\\*+ .*\\b" (regexp-quote identifier) "\\b")
(concat "\\b" (regexp-quote identifier) "\\b")))
(case-fold-search nil))
(while (re-search-forward pattern nil t)
(let ((loc (xref-make-buffer-location
buf (line-beginning-position))))
(push
(xref-make
(cond
(find-definition
(org-get-heading t t t t)) ; headline text only
;; reference context ---------
((not (org-at-heading-p))
(let* ((heading (save-excursion
(or (outline-previous-heading)
(goto-char (point-min)))
(org-get-heading t t t t)))
(item (org-element-lineage
(org-element-context) '(item) t))
(entry-text (if item
(buffer-substring-no-properties
(org-element-property :begin item)
(org-element-property :end item))
(thing-at-point 'line t)))
(one-line (replace-regexp-in-string
"\n\\s-*" " " entry-text)))
(format "%s\n %s\n" heading (string-trim one-line))))
(t
(thing-at-point 'line t))) ; fallback
loc)
matches))))))))
(nreverse matches)))
(cl-defmethod xref-backend-references ((_backend (eql elot)) identifier)
"Return every reference to IDENTIFIER in ELOT buffers."
(elot--xref-find-matches identifier))
(cl-defmethod xref-backend-identifier-completion-table ((_backend (eql elot)))
"Disable identifier completion for ELOT xref backends.
This prevents Emacs from prompting with completions in xref commands
like `xref-find-references'."
nil)
(defvar elot--xref-pending-active-sources nil
"Transient: snapshot of `elot-active-label-sources' from the caller of xref.
Captured by `elot--capture-slurp' (the :before advice on
`xref-find-references') and consumed by
`elot--xref-label-overlay-setup' to seed the *xref* buffer so that
label display works there too. Cleared after use.")
(defvar elot--xref-pending-source-buffer nil
"Transient: the buffer that invoked `xref-find-references'.
Captured alongside `elot--xref-pending-active-sources' so the
calling buffer's file can be added as a DB-backed label source in
the *xref* buffer. Cleared after use.")
(defun elot--capture-slurp (&rest _args)
"Stage label-display state for transfer into the *xref* buffer.
Runs as :before advice on `xref-find-references' in the originating
ELOT buffer. Captures:
* `elot-slurp' -> `elot-slurp-global' (legacy staging path);
* `elot-active-label-sources' + the current buffer ->
`elot--xref-pending-active-sources' /
`elot--xref-pending-source-buffer', consumed by
`elot--xref-label-overlay-setup' to enable
`elot-global-label-display-mode' in the *xref* buffer with the
caller's sources (and the caller's file) seeded."
(when (boundp 'elot-slurp)
(setq elot-slurp-global elot-slurp))
(setq elot--xref-pending-active-sources
(and (boundp 'elot-active-label-sources)
(copy-sequence elot-active-label-sources)))
(setq elot--xref-pending-source-buffer (current-buffer)))
;; The advice on `xref-find-references' that runs `elot--capture-slurp'
;; before xref is installed by `elot-mode--install-xref-globals' and
;; removed by `elot-mode--uninstall-xref-globals'.
(defun elot--xref-label-overlay-setup ()
"Enable label display in the *xref* buffer using DB-backed sources.
Reads the transient values stashed by `elot--capture-slurp':
* sets `elot-active-label-sources' buffer-locally in *xref* to the
caller's list, augmented with an entry naming the caller's file
so identifiers defined there are looked up;
* enables `elot-global-label-display-mode' in *xref* (DB-driven
matcher, works in any major mode);
* binds the opt-in toggle key (if customized) locally.
Transients are cleared after use."
(when (equal (buffer-name) "*xref*")
;; 1. Carry over active sources from the calling buffer.
(let* ((sources (copy-sequence elot--xref-pending-active-sources))
(src-buf elot--xref-pending-source-buffer)
(src-file (and (buffer-live-p src-buf)
(buffer-local-value 'buffer-file-name src-buf))))
(when (and src-file
(not (cl-find-if (lambda (e) (equal (nth 0 e) src-file))
sources)))
(setq sources (append sources (list (list src-file nil)))))
(setq-local elot-active-label-sources sources))
;; 2. Bind opt-in toggle key (if customized).
(let ((key (and (fboundp 'elot--toggle-labels-key)
(elot--toggle-labels-key))))
(when key
(local-set-key (kbd key) #'elot-toggle-label-display)))
;; 3. Enable label display via the DB-driven global mode.
(when (fboundp 'elot-global-label-display-mode)
(elot-global-label-display-mode 1))
;; 4. Clear transients defensively.
(setq elot--xref-pending-active-sources nil
elot--xref-pending-source-buffer nil)))
;; Installed/removed via `elot-mode--{install,uninstall}-xref-globals'.
(cl-defmethod xref-backend-definitions ((_backend (eql elot)) identifier)
"Return Org headlines that *define* IDENTIFIER."
(elot--xref-find-matches identifier :find-definition t))
;; NOTE: The xref backend is registered/unregistered in
;; elot-mode--add-hooks / elot-mode--remove-hooks, not globally.
(defun elot--xref-buffer-enable-backend ()
"Enable the ELOT xref backend in the `*xref*` buffer.
This ensures `xref-find-definitions` works on CURIEs inside the xref buffer."
(when (equal (buffer-name) "*xref*")
(with-current-buffer (current-buffer)
(make-local-variable 'xref-backend-functions)
(add-hook 'xref-backend-functions #'elot-xref-backend nil t))))
;; Installed/removed via `elot-mode--{install,uninstall}-xref-globals'.
(cl-defun elot-describe-curie-at-point (&optional curie)
"Pop up a *Help* buffer describing CURIE at point (or prompt).
Shows the defining headline and a few reference examples, each as
a clickable button. Label overlays are rendered inside the help
buffer exactly like they are in the *xref* buffer."
(interactive
(list
(or (cl-letf* ((backend 'elot)
(id-fn (symbol-function
'xref-backend-identifier-at-point)))
(funcall id-fn backend))
(read-string "ELOT CURIE: "))))
(unless (and curie (stringp curie) (not (string-empty-p curie)))
(user-error "No CURIE given"))
;; ------------------------------------------------------------------
;; 1. Capture caller-side label-display state for transfer into the
;; *ELOT Describe* buffer. Mirrors the xref path: snapshot
;; `elot-active-label-sources' and the calling buffer's file so the
;; Help buffer can use `elot-global-label-display-mode' (DB-backed)
;; instead of the older buffer-local `elot-slurp' path.
;; ------------------------------------------------------------------
(let* ((caller-sources (and (boundp 'elot-active-label-sources)
(copy-sequence elot-active-label-sources)))
(caller-file buffer-file-name))
;; ----------------------------------------------------------------
;; 2. Gather xref data
;; ----------------------------------------------------------------
(let* ((defns (elot--xref-find-matches curie :find-definition t))
(refs (elot--xref-find-matches curie))
(buffer (get-buffer-create "*ELOT Describe*"))
(max-ref 10))
;; ----------------------------------------------------------------
;; 3. Build the Help buffer
;; ----------------------------------------------------------------
(with-help-window buffer
(with-current-buffer buffer
(help-mode)
(setq truncate-lines t)
;; ------------------------------------
;; 3a. Header & definition
;; ------------------------------------
(princ (format "%s\n\n" curie))
(if defns
(progn
(princ (propertize "Defined in:\n" 'face 'bold))
(elot--describe--insert-xref-button (car defns) 2))
(princ (propertize "No definition found.\n" 'face 'warning)))
(princ "\n")
;; ------------------------------------
;; 3b. References (first N)
;; ------------------------------------
(princ (propertize "Selected references:\n" 'face 'bold))
(if refs
(let ((count 0))
(dolist (xref refs)
(when (< count max-ref)
(elot--describe--insert-xref-button xref 4)
(setq count (1+ count))))
(when (> (length refs) max-ref)
(princ (format " ...and %d more\n"
(- (length refs) max-ref)))))
(princ " (none)\n"))
(princ
"\n----\nType q to quit, RET to visit location.\n")
;; ------------------------------------
;; 3c. Seed DB-backed label sources, bind opt-in toggle key, and
;; enable label display via `elot-global-label-display-mode'
;; (same path as the *xref* buffer).
;; ------------------------------------
(let ((sources (copy-sequence caller-sources)))
(when (and caller-file
(not (cl-find-if (lambda (e) (equal (nth 0 e) caller-file))
sources)))
(setq sources (append sources (list (list caller-file nil)))))
(setq-local elot-active-label-sources sources))
(let ((key (and (fboundp 'elot--toggle-labels-key)
(elot--toggle-labels-key))))
(when key
(local-set-key (kbd key) #'elot-toggle-label-display)))
(when (fboundp 'elot-global-label-display-mode)
(elot-global-label-display-mode 1)))))))
(defun elot--describe--insert-xref-button (xref indent)
"Insert XREF as a bullet indented by INDENT, with filename and a clickable link."
(let* ((summary (xref-item-summary xref))
(loc (xref-item-location xref))
(marker (xref-location-marker loc))
(buf (marker-buffer marker))
(file (or (buffer-file-name buf) (buffer-name buf)))
(short (file-name-nondirectory file))
(line (with-current-buffer buf
(line-number-at-pos marker))))
;; bullet + file prefix
(insert (make-string indent ?\s) "- "
(propertize (concat short ": ") 'face 'font-lock-keyword-face))
;; clickable summary
(insert-text-button
summary
'follow-link t
'elot-target-buffer buf
'elot-target-pos (marker-position marker)
'action (lambda (btn)
(let ((target-buf (button-get btn 'elot-target-buffer))
(pos (button-get btn 'elot-target-pos)))
(pop-to-buffer target-buf)
(goto-char pos)
(xref-pulse-momentarily)))
'help-echo (format "%s:%d" short line))
(insert "\n")))Existing ELOT files that use the old eval: cookie can be migrated
gradually. During the transition, elot-defaults.el can be reduced to:
(require 'elot-mode) (elot-mode 1)
Or users can replace:
# -*- eval: (load-library "elot-defaults") -*-
with:
# -*- eval: (elot-mode 1) -*-
Once auto-detection is relied upon, even that line can be removed.
(provide 'elot-mode)
;;; elot-mode.el ends hereFor reference, here are the LaTeX-specific settings that were in
elot-defaults.el and are deliberately excluded from elot-mode.el
for now. They need a complete rework before being re-included:
org-latex-src-block-backendorg-latex-prefer-user-labelsorg-latex-image-default-scaleorg-latex-default-classorg-latex-packages-alist(extensive KOMA-Script configuration)org-latex-classes(the"elot-scrreprt"class definition)elot-ontology-resource-section(LaTeX section-level function)
These will be addressed in a future elot-latex.org module.