aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRalph Amissah <ralph.amissah@gmail.com>2026-05-15 17:25:47 -0400
committerRalph Amissah <ralph.amissah@gmail.com>2026-05-15 19:40:46 -0400
commitd0cd8444fa69269803d9cda8af6277d2cdbecaee (patch)
tree6e7d34e1e0774dcc684dd93459d1b4df94fe1083
parentssp test-abstraction-ssp.sh script minor (diff)
editors syntax highlighting ...
- emacs syntax: add tree-sitter major mode for SiSU spine markup New file sisu-spine-ts-mode.el is a sibling of the existing regex mode, backed by Emacs 29+'s built-in treesit.el and the tree-sitter-sisu grammar. It replaces the long font-lock keyword list with treesit-font-lock-rules grouped into eight features (comment, header, heading, block, inline, note, link, index, misc) so users can dial verbosity via treesit-font-lock-level. It also wires up: - treesit-simple-imenu-settings for a heading outline, - treesit-thing-settings for sentence / paragraph motions, - treesit-defun-type-regexp so C-M-a / C-M-e jump heading-to-heading, - a sisu-spine-ts-install-grammar command that registers treesit-language-source-alist and runs treesit-install-language-grammar so users can install the parser from inside Emacs without leaving the editor. - the original sisu-spine-mode.el is unchanged and supported for Emacs < 29 and for users who prefer regex highlighting. - nvim drop-in: point parser fetch at tools/tree-sitter-sisu sundry/editor-syntax-etc/nvim/ The nvim-treesitter install_info now fetches the parser from https://git.sisudoc.org/projects/tree-sitter-sisu which can be cloned via git://git.sisudoc.org/tools/tree-sitter-sisu The fetched paths: files = { "src/parser.c", "src/scanner.c" } submission of the sisu parser to nvim-treesitter's parsers.lua should be a near-trivial one-liner. - the original vim regex highlighter remains as before - sundry/editor-syntax-etc/vim/syntax/sisu-spine.vim - sundry/editor-syntax-etc/vim/templates/{sst,ssm,ssi}.tpl new skeleton templates for the three SiSU markup file types sets up the YAML header (title, creator, date, rights, classify, identfier) - .gitignore - whitelist the new files (assisted by Claude-Code)
-rw-r--r--.gitignore12
-rw-r--r--makefile32
-rw-r--r--org/config_git.org12
-rw-r--r--org/config_make.org32
-rw-r--r--org/util_editors.org966
-rw-r--r--org/util_spine_markup_conversion_from_sisu.org8
-rw-r--r--sundry/editor-syntax-etc/emacs/sisu-spine-mode-autoloads.el24
-rw-r--r--sundry/editor-syntax-etc/emacs/sisu-spine-ts-mode.el296
-rw-r--r--sundry/editor-syntax-etc/nvim/README.md87
-rw-r--r--sundry/editor-syntax-etc/nvim/ftdetect/sisu.lua7
-rw-r--r--sundry/editor-syntax-etc/nvim/ftplugin/sisu.lua13
-rw-r--r--sundry/editor-syntax-etc/nvim/lua/sisu-spine/init.lua43
-rw-r--r--sundry/editor-syntax-etc/nvim/queries/sisu/folds.scm23
-rw-r--r--sundry/editor-syntax-etc/nvim/queries/sisu/highlights.scm189
-rw-r--r--sundry/editor-syntax-etc/nvim/queries/sisu/indents.scm48
-rw-r--r--sundry/editor-syntax-etc/nvim/queries/sisu/injections.scm16
-rw-r--r--sundry/editor-syntax-etc/nvim/queries/sisu/textobjects.scm140
-rw-r--r--sundry/editor-syntax-etc/vim/syntax/sisu-spine.vim14
-rw-r--r--sundry/editor-syntax-etc/vim/templates/ssi.tpl30
-rw-r--r--sundry/editor-syntax-etc/vim/templates/ssm.tpl30
-rw-r--r--sundry/editor-syntax-etc/vim/templates/sst.tpl30
21 files changed, 2004 insertions, 48 deletions
diff --git a/.gitignore b/.gitignore
index cbdcd6b..85cdddc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -76,6 +76,18 @@
!sundry/editor-syntax-etc/vim/ftplugin/*.vim
!sundry/editor-syntax-etc/vim/syntax
!sundry/editor-syntax-etc/vim/syntax/*.vim
+!sundry/editor-syntax-etc/vim/templates
+!sundry/editor-syntax-etc/vim/templates/*.tpl
+!sundry/editor-syntax-etc/nvim
+!sundry/editor-syntax-etc/nvim/*.md
+!sundry/editor-syntax-etc/nvim/ftdetect
+!sundry/editor-syntax-etc/nvim/ftdetect/*.lua
+!sundry/editor-syntax-etc/nvim/ftplugin
+!sundry/editor-syntax-etc/nvim/ftplugin/*.lua
+!sundry/editor-syntax-etc/nvim/queries
+!sundry/editor-syntax-etc/nvim/queries/sisu
+!sundry/editor-syntax-etc/nvim/queries/sisu/*.scm
+!sundry/editor-syntax-etc/nvim/**
!sundry/editor-syntax-etc/emacs
!sundry/editor-syntax-etc/emacs/*.el
!sundry/editor-syntax-etc/emacs/README
diff --git a/makefile b/makefile
index b91d722..d8703de 100644
--- a/makefile
+++ b/makefile
@@ -642,14 +642,14 @@ skel:
mkdir -p build; \
mkdir -p views; \
mkdir -p data; \
- mkdir -p sundry/misc/util/d/cgi/search/cgi-bin/src; \
- mkdir -p sundry/misc/util/d/tools/markup_conversion; \
- mkdir -p sundry/misc/editor-syntax-etc/emacs; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/syntax; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/colors; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/ftplugin; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/rc; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/templates; \
+ mkdir -p sundry/util/d/cgi/search/cgi-bin/src; \
+ mkdir -p sundry/util/d/tools/markup_conversion; \
+ mkdir -p sundry/editor-syntax-etc/emacs; \
+ mkdir -p sundry/editor-syntax-etc/vim/syntax; \
+ mkdir -p sundry/editor-syntax-etc/vim/colors; \
+ mkdir -p sundry/editor-syntax-etc/vim/ftplugin; \
+ mkdir -p sundry/editor-syntax-etc/vim/rc; \
+ mkdir -p sundry/editor-syntax-etc/vim/templates; \
mkdir -p $(PRG_SRCDIR)/$(PRG_NAME_DIR)/conf; \
mkdir -p $(PRG_SRCDIR)/$(PRG_NAME_DIR)/io_in; \
mkdir -p $(PRG_SRCDIR)/$(PRG_NAME_DIR)/io_out; \
@@ -685,14 +685,14 @@ distclean: expunge
distclean_and_init: expunge
mkdir -p views; \
- mkdir -p sundry/misc/util/d/cgi/search/cgi-bin/src; \
- mkdir -p sundry/misc/util/d/tools/markup_conversion; \
- mkdir -p sundry/misc/editor-syntax-etc/emacs; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/syntax; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/colors; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/ftplugin; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/rc; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/templates; \
+ mkdir -p sundry/util/d/cgi/search/cgi-bin/src; \
+ mkdir -p sundry/util/d/tools/markup_conversion; \
+ mkdir -p sundry/editor-syntax-etc/emacs; \
+ mkdir -p sundry/editor-syntax-etc/vim/syntax; \
+ mkdir -p sundry/editor-syntax-etc/vim/colors; \
+ mkdir -p sundry/editor-syntax-etc/vim/ftplugin; \
+ mkdir -p sundry/editor-syntax-etc/vim/rc; \
+ mkdir -p sundry/editor-syntax-etc/vim/templates; \
mkdir -p $(PRG_SRCDIR)/$(PRG_NAME_DIR); \
mkdir -p $(PRG_BINDIR);
diff --git a/org/config_git.org b/org/config_git.org
index b67d710..42959df 100644
--- a/org/config_git.org
+++ b/org/config_git.org
@@ -100,6 +100,18 @@
!sundry/editor-syntax-etc/vim/ftplugin/*.vim
!sundry/editor-syntax-etc/vim/syntax
!sundry/editor-syntax-etc/vim/syntax/*.vim
+!sundry/editor-syntax-etc/vim/templates
+!sundry/editor-syntax-etc/vim/templates/*.tpl
+!sundry/editor-syntax-etc/nvim
+!sundry/editor-syntax-etc/nvim/*.md
+!sundry/editor-syntax-etc/nvim/ftdetect
+!sundry/editor-syntax-etc/nvim/ftdetect/*.lua
+!sundry/editor-syntax-etc/nvim/ftplugin
+!sundry/editor-syntax-etc/nvim/ftplugin/*.lua
+!sundry/editor-syntax-etc/nvim/queries
+!sundry/editor-syntax-etc/nvim/queries/sisu
+!sundry/editor-syntax-etc/nvim/queries/sisu/*.scm
+!sundry/editor-syntax-etc/nvim/**
!sundry/editor-syntax-etc/emacs
!sundry/editor-syntax-etc/emacs/*.el
!sundry/editor-syntax-etc/emacs/README
diff --git a/org/config_make.org b/org/config_make.org
index 977f111..86688c3 100644
--- a/org/config_make.org
+++ b/org/config_make.org
@@ -684,14 +684,14 @@ skel:
mkdir -p build; \
mkdir -p views; \
mkdir -p data; \
- mkdir -p sundry/misc/util/d/cgi/search/cgi-bin/src; \
- mkdir -p sundry/misc/util/d/tools/markup_conversion; \
- mkdir -p sundry/misc/editor-syntax-etc/emacs; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/syntax; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/colors; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/ftplugin; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/rc; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/templates; \
+ mkdir -p sundry/util/d/cgi/search/cgi-bin/src; \
+ mkdir -p sundry/util/d/tools/markup_conversion; \
+ mkdir -p sundry/editor-syntax-etc/emacs; \
+ mkdir -p sundry/editor-syntax-etc/vim/syntax; \
+ mkdir -p sundry/editor-syntax-etc/vim/colors; \
+ mkdir -p sundry/editor-syntax-etc/vim/ftplugin; \
+ mkdir -p sundry/editor-syntax-etc/vim/rc; \
+ mkdir -p sundry/editor-syntax-etc/vim/templates; \
mkdir -p $(PRG_SRCDIR)/$(PRG_NAME_DIR)/conf; \
mkdir -p $(PRG_SRCDIR)/$(PRG_NAME_DIR)/io_in; \
mkdir -p $(PRG_SRCDIR)/$(PRG_NAME_DIR)/io_out; \
@@ -727,14 +727,14 @@ distclean: expunge
distclean_and_init: expunge
mkdir -p views; \
- mkdir -p sundry/misc/util/d/cgi/search/cgi-bin/src; \
- mkdir -p sundry/misc/util/d/tools/markup_conversion; \
- mkdir -p sundry/misc/editor-syntax-etc/emacs; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/syntax; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/colors; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/ftplugin; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/rc; \
- mkdir -p sundry/misc/editor-syntax-etc/vim/templates; \
+ mkdir -p sundry/util/d/cgi/search/cgi-bin/src; \
+ mkdir -p sundry/util/d/tools/markup_conversion; \
+ mkdir -p sundry/editor-syntax-etc/emacs; \
+ mkdir -p sundry/editor-syntax-etc/vim/syntax; \
+ mkdir -p sundry/editor-syntax-etc/vim/colors; \
+ mkdir -p sundry/editor-syntax-etc/vim/ftplugin; \
+ mkdir -p sundry/editor-syntax-etc/vim/rc; \
+ mkdir -p sundry/editor-syntax-etc/vim/templates; \
mkdir -p $(PRG_SRCDIR)/$(PRG_NAME_DIR); \
mkdir -p $(PRG_BINDIR);
diff --git a/org/util_editors.org b/org/util_editors.org
index 1f36816..f88e9d1 100644
--- a/org/util_editors.org
+++ b/org/util_editors.org
@@ -333,15 +333,25 @@ unlet s:cpo_save
#+HEADER: :tangle "../sundry/editor-syntax-etc/vim/syntax/sisu-spine.vim"
#+BEGIN_SRC text
-" SiSU Vim syntax file (sisu-spine)
+" SiSU Vim syntax file (sisu-spine) - Vim 8 fallback (regex)
" SiSU Maintainer: Ralph Amissah <ralph.amissah@gmail.com>
" SiSU Markup: SiSU (sisu-5.6.7)
" sisu-spine Markup: sisu-spine
-" Last Change: 2017-06-22, 2025-02-21
+" Last Change: 2017-06-22, 2025-02-21, 2026-05-09
" URL: <https://git.sisudoc.org/projects/sisudoc-spine/tree/sundry/editor-syntax-etc/vim/syntax/sisu-spine.vim>
" <https://git.sisudoc.org/projects/sisudoc-spine/tree/sundry/editor-syntax-etc/vim/syntax/sisu.vim>
" <https://sisudoc.org/>
"(originally looked at Ruby Vim by Mirko Nasato)
+"
+" Status: This is the regex-based Vim 8 fallback. For Neovim users, the
+" tree-sitter-sisu grammar provides structural highlighting, folding and
+" textobjects with strictly better behaviour on nested markup, multi-line
+" footnotes, block bodies, and segmented headings; see
+" sundry/editor-syntax-etc/nvim/README.md
+" Emacs 29+ users have an equivalent treesit-based mode at
+" sundry/editor-syntax-etc/emacs/sisu-spine-ts-mode.el
+" This file remains the supported path for classic Vim, where tree-sitter
+" is not available without third-party plugins.
if version < 600
syntax clear
@@ -1897,8 +1907,630 @@ make:
1~ #___#
#+END_SRC
-* Emacs Syntax highlighting
+* NVim tree-sitter Syntax highlighting
+** README.md
+
+#+HEADER: :tangle "../sundry/editor-syntax-etc/nvim/README.md"
+#+BEGIN_SRC markdown
+# Neovim integration for SiSU spine markup
+
+Tree-sitter-backed syntax highlighting, folding, and structural
+navigation for `.sst` / `.ssm` / `.ssi` files in Neovim (>= 0.9).
+
+## What is in this directory
+
+```
+nvim/
+ ftdetect/sisu.lua - register .sst/.ssm/.ssi as filetype "sisu"
+ ftplugin/sisu.lua - per-buffer settings (commentstring, conceal)
+ lua/sisu-spine/init.lua - entry point: registers parser config
+ queries/sisu/ - tree-sitter queries (mirrors tree-sitter-sisu/queries/)
+ highlights.scm
+ folds.scm
+ injections.scm
+ textobjects.scm
+ indents.scm
+```
+
+## Install (manual)
+
+1. Symlink or copy this directory into your Neovim runtime path:
+
+ ```sh
+ ln -s /path/to/sisudoc-spine/sundry/editor-syntax-etc/nvim \
+ ~/.config/nvim/pack/sisu/start/sisu-spine
+ ```
+
+2. Tell `nvim-treesitter` how to fetch the parser. Add to your config
+ (`init.lua`):
+
+ ```lua
+ require("sisu-spine").setup()
+ require("nvim-treesitter.configs").setup({
+ ensure_installed = { "sisu" },
+ highlight = { enable = true },
+ indent = { enable = true },
+ fold = { enable = true },
+ textobjects = { select = { enable = true, lookahead = true } },
+ })
+ ```
+
+3. Build the parser:
+
+ ```vim
+ :TSInstall sisu
+ ```
+
+That is it. Open a `.sst` file - highlighting, folding, and textobject
+selection should all work.
+
+## Install (lazy.nvim)
+
+```lua
+{
+ dir = "/path/to/sisudoc-spine/sundry/editor-syntax-etc/nvim",
+ name = "sisu-spine",
+ ft = { "sisu" },
+ dependencies = { "nvim-treesitter/nvim-treesitter" },
+ config = function()
+ require("sisu-spine").setup()
+ end,
+}
+```
+
+## Sync queries from upstream
+
+The query files are duplicated from `tree-sitter-sisu/queries/` so that
+this Neovim drop-in works without depending on the parser repo's
+checkout layout. To refresh them after grammar changes:
+
+```sh
+cp ../../../sisudoc-spine-tools/tree-sitter-sisu/queries/*.scm \
+ queries/sisu/
+```
+
+(Path is relative to this README.)
+
+## Upstreaming the parser
+
+When the parser is publicly hosted under a stable URL it is worth
+submitting a config to `nvim-treesitter` so users can run `:TSInstall
+sisu` without the local `setup()` call. The required fields are in
+`lua/sisu-spine/init.lua` (`install_info` table); send a PR to
+<https://github.com/nvim-treesitter/nvim-treesitter> patching
+`lua/nvim-treesitter/parsers.lua`.
+#+END_SRC
+
+** lua
+*** init
+
+#+HEADER: :tangle "../sundry/editor-syntax-etc/nvim/lua/sisu-spine/init.lua"
+#+BEGIN_SRC lua
+-- Entry point for the SiSU spine markup Neovim integration.
+--
+-- Registers a tree-sitter parser config so users can run
+-- :TSInstall sisu
+-- to fetch and build the parser via nvim-treesitter.
+--
+-- The parser source lives can be found under the
+-- `projects/` namespace on git.sisudoc.org.
+
+local M = {}
+
+--- Register the `sisu` parser with nvim-treesitter and ensure that
+--- `.sst` / `.ssm` / `.ssi` are detected as filetype "sisu".
+---
+--- Call once from your init.lua before invoking `:TSInstall sisu`.
+function M.setup()
+ local ok, parsers = pcall(require, "nvim-treesitter.parsers")
+ if not ok then
+ vim.notify(
+ "sisu-spine: nvim-treesitter is not installed; "
+ .. "syntax highlighting will not be available.",
+ vim.log.levels.WARN
+ )
+ return
+ end
+
+ local parser_config = parsers.get_parser_configs()
+ parser_config.sisu = {
+ install_info = {
+ url = "https://git.sisudoc.org/projects/tree-sitter-sisu",
+ files = {
+ "src/parser.c",
+ "src/scanner.c",
+ },
+ branch = "main",
+ generate_requires_npm = false,
+ requires_generate_from_grammar = false,
+ },
+ filetype = "sisu",
+ }
+end
+
+return M
+#+END_SRC
+
+*** ftdetect
+
+#+HEADER: :tangle "../sundry/editor-syntax-etc/nvim/ftdetect/sisu.lua"
+#+BEGIN_SRC lua
+vim.filetype.add({
+ extension = {
+ sst = "sisu",
+ ssm = "sisu",
+ ssi = "sisu",
+ },
+})
+#+END_SRC
+
+*** ftplugin
+
+#+HEADER: :tangle "../sundry/editor-syntax-etc/nvim/ftplugin/sisu.lua"
+#+BEGIN_SRC lua
+-- Buffer-local settings for SiSU spine markup.
+
+vim.bo.commentstring = "%% %s"
+vim.bo.comments = ":%"
+
+-- Soft wrap suits prose.
+vim.wo.wrap = true
+vim.wo.linebreak = true
+
+-- Conceal inline-formatting delimiters when the user opts in
+-- (`:set conceallevel=2`). See queries/sisu/highlights.scm for
+-- @conceal captures.
+vim.wo.conceallevel = vim.wo.conceallevel
+#+END_SRC
+
+** queries
+*** folds
+
+#+HEADER: :tangle "../sundry/editor-syntax-etc/nvim/queries/sisu/folds.scm"
+#+BEGIN_SRC vimrc
+; Code folding queries for SiSU Spine markup
+
+; Block elements are foldable
+(code_block_curly) @fold
+(code_block_tic) @fold
+(poem_block_curly) @fold
+(poem_block_tic) @fold
+(block_block_curly) @fold
+(block_block_tic) @fold
+(group_block_curly) @fold
+(group_block_tic) @fold
+(table_block_curly) @fold
+(table_block_tic) @fold
+(quote_block_tic) @fold
+
+; Multi-line book index entries are foldable
+(book_index) @fold
+
+; Pipe tables are foldable
+(pipe_table) @fold
+
+; Header fields with continuations are foldable
+(header_field) @fold
+#+END_SRC
+*** highlights
+
+#+HEADER: :tangle "../sundry/editor-syntax-etc/nvim/queries/sisu/highlights.scm"
+#+BEGIN_SRC vimrc
+; Syntax highlighting queries for SiSU Spine markup
+; Compatible with tree-sitter highlight capture names from
+; https://tree-sitter.github.io/tree-sitter/syntax-highlighting
+
+; =================================================================
+; Comments
+; =================================================================
+(version_comment) @comment.documentation
+(header_comment) @comment
+(body_comment) @comment
+
+; =================================================================
+; Header (document metadata)
+; =================================================================
+(header_field
+ key: (header_key) @keyword)
+
+(header_field
+ value: (header_value) @string)
+
+(header_continuation) @string
+
+; =================================================================
+; Headings
+; =================================================================
+(part_marker) @keyword.directive
+(segment_marker) @keyword.directive
+
+(heading_part
+ content: (heading_content) @markup.heading)
+
+(heading_segment
+ content: (heading_content) @markup.heading)
+
+(segment_name) @label
+(suppress_marker) @punctuation.special
+
+; Heading levels for more specific styling
+(heading_part
+ marker: (part_marker) @markup.heading.1
+ (#match? @markup.heading.1 "^:A~$"))
+
+(heading_part
+ marker: (part_marker) @markup.heading.2
+ (#match? @markup.heading.2 "^:B~$"))
+
+(heading_part
+ marker: (part_marker) @markup.heading.3
+ (#match? @markup.heading.3 "^:C~$"))
+
+(heading_part
+ marker: (part_marker) @markup.heading.4
+ (#match? @markup.heading.4 "^:D~$"))
+
+(heading_segment
+ marker: (segment_marker) @markup.heading.5
+ (#match? @markup.heading.5 "^1~$"))
+
+(heading_segment
+ marker: (segment_marker) @markup.heading.6
+ (#match? @markup.heading.6 "^2~$"))
+
+; =================================================================
+; Inline formatting
+; =================================================================
+(emphasis) @markup.italic
+(bold) @markup.bold
+(italic) @markup.italic
+(underline) @markup.underline
+(citation_mark) @markup.quote
+(superscript) @markup.superscript
+(subscript) @markup.subscript
+(inserted) @markup.underline
+(strikethrough) @markup.strikethrough
+(monospace_inline) @markup.raw
+
+; Formatting delimiters
+["*{" "}*"] @punctuation.special
+["!{" "}!"] @punctuation.special
+["/{" "}/"] @punctuation.special
+["_{" "}_"] @punctuation.special
+["\"{" "}\""] @punctuation.special
+["^{" "}^"] @punctuation.special
+[",{" "},"] @punctuation.special
+["+{" "}+"] @punctuation.special
+["-{" "}-"] @punctuation.special
+["#{" "}#"] @punctuation.special
+
+; =================================================================
+; Footnotes and editor notes
+; =================================================================
+(footnote) @markup.link
+(footnote_marker) @punctuation.special
+(editor_note) @markup.link
+
+["~{" "}~"] @punctuation.special
+; Editor-note channel selector: ~[* (asterisk set) or ~[+ (plus set).
+; A distinct capture lets themes colour the two channels separately
+; from the generic footnote delimiters above.
+(editor_note_marker) @attribute
+["]~"] @punctuation.special
+
+; =================================================================
+; Links and images
+; =================================================================
+(link
+ text: (link_text) @markup.link.label)
+
+(link
+ target: (url) @markup.link.url)
+
+(link
+ target: (anchor_ref) @markup.link.url)
+
+(link
+ target: (collection_path) @markup.link.url)
+
+(auto_footnote_marker) @punctuation.special
+
+(image
+ spec: (image_spec) @markup.link.label)
+
+(url) @markup.link.url
+
+(inline_anchor) @label
+(anchor_name) @label
+
+; =================================================================
+; Block elements
+; =================================================================
+(block_open) @keyword.directive
+(block_close) @keyword.directive
+(raw_content) @markup.raw
+
+; Code blocks get more specific highlighting
+(code_block_curly
+ open: (block_open) @keyword.directive)
+(code_block_curly
+ content: (raw_content) @markup.raw.block)
+(code_block_curly
+ close: (block_close) @keyword.directive)
+
+(code_block_tic
+ open: (block_open) @keyword.directive)
+(code_block_tic
+ content: (raw_content) @markup.raw.block)
+(code_block_tic
+ close: (block_close) @keyword.directive)
+
+; =================================================================
+; Book index
+; =================================================================
+(book_index) @markup.list
+(index_content) @string
+
+; =================================================================
+; Paragraph prefixes
+; =================================================================
+(paragraph_prefix) @punctuation.special
+
+; =================================================================
+; Special markers
+; =================================================================
+(ocn_suppress) @comment
+(ocn_suppress_open) @comment
+(ocn_suppress_close) @comment
+
+(page_break) @punctuation.special
+(horizontal_rule) @punctuation.special
+
+; =================================================================
+; Composite includes
+; =================================================================
+(composite_include) @keyword.import
+(include_path) @string.special.path
+
+; =================================================================
+; Pipe table
+; =================================================================
+(table_spec) @keyword.directive
+(table_row) @markup.raw
+
+; =================================================================
+; Text
+; =================================================================
+(text) @spell
+
+; Line break
+(line_break) @punctuation.special
+#+END_SRC
+
+*** indents
+
+#+HEADER: :tangle "../sundry/editor-syntax-etc/nvim/queries/sisu/indents.scm"
+#+BEGIN_SRC vimrc
+; Indentation queries for SiSU Spine markup.
+;
+; SiSU markup is largely flat: paragraphs and headings live at column 0,
+; block bodies preserve their author-supplied indentation verbatim, and
+; nesting is by markers rather than by indent. So indents.scm is mostly a
+; no-op - the goal is to ensure that auto-indent on <CR> stays at column 0
+; for normal lines and respects existing indentation inside header
+; continuations and blocks.
+
+; Tree-sitter indent semantics (per nvim-treesitter and treesit):
+; @indent.begin - increases indent for the following line
+; @indent.end - matches the @indent.begin and decreases indent
+; @indent.zero - resets indent to column 0
+; @indent.align - aligns following lines with this node
+; @indent.branch - same level as the parent (for else/elif-style joins)
+
+; Top-level structures live at column 0 - reset to zero on the next line.
+(heading_part) @indent.zero
+(heading_segment) @indent.zero
+(paragraph) @indent.zero
+(book_index) @indent.zero
+(composite_include) @indent.zero
+(page_break) @indent.zero
+(horizontal_rule) @indent.zero
+(ocn_suppress_open) @indent.zero
+(ocn_suppress_close) @indent.zero
+(body_comment) @indent.zero
+
+; Block elements: opening line increases indent for the body, closing
+; line returns to zero. Editors that respect this will visually indent
+; raw content one step from the delimiter line, which is conventional.
+(code_block_curly) @indent.align
+(code_block_tic) @indent.align
+(poem_block_curly) @indent.align
+(poem_block_tic) @indent.align
+(block_block_curly) @indent.align
+(block_block_tic) @indent.align
+(group_block_curly) @indent.align
+(group_block_tic) @indent.align
+(table_block_curly) @indent.align
+(table_block_tic) @indent.align
+(quote_block_tic) @indent.align
+
+; Header continuation lines are indented by two spaces from column 0;
+; mark continuations as align so a host that chooses to auto-indent the
+; next continuation line matches the previous one.
+(header_field) @indent.align
+(header_continuation) @indent.align
+#+END_SRC
+
+*** injections
+
+#+HEADER: :tangle "../sundry/editor-syntax-etc/nvim/queries/sisu/injections.scm"
+#+BEGIN_SRC vimrc
+; Language injection queries for SiSU Spine markup
+;
+; Code blocks could potentially inject language-specific highlighting,
+; but SiSU code blocks don't specify language. These queries are
+; provided as a starting point for future extension.
+
+; Code block content could be injected with a specific language
+; if the block type or context provides a hint.
+; For now, raw content in code blocks is left unhighlighted.
+
+; Example: if code blocks specified a language, e.g. code(d){
+; ((code_block_curly
+; open: (block_open) @_open
+; content: (raw_content) @injection.content)
+; (#match? @_open "code\\(d\\)")
+; (#set! injection.language "d"))
+#+END_SRC
+
+*** textobjects
+
+#+HEADER: :tangle "../sundry/editor-syntax-etc/nvim/queries/sisu/textobjects.scm"
+#+BEGIN_SRC vimrc
+; Text-object queries for SiSU Spine markup.
+;
+; Capture conventions follow nvim-treesitter/textobjects:
+; @<thing>.outer -> select including delimiters / surrounding whitespace
+; @<thing>.inner -> select content only
+;
+; Hosts that consume these (Neovim's nvim-treesitter-textobjects, Helix,
+; Emacs treesit) bind keys such as `af` / `if` to .outer / .inner.
+
+; =================================================================
+; Headings (sectioning units)
+; =================================================================
+; A whole heading line is a "section header" object. Heading sections
+; (the heading plus its body content up to the next heading of equal or
+; higher level) are not directly expressible in tree-sitter without
+; additional grammar work; hosts can synthesise that from these captures.
+
+(heading_part) @class.outer
+(heading_part
+ content: (heading_content) @class.inner)
+
+(heading_segment) @class.outer
+(heading_segment
+ content: (heading_content) @class.inner)
+
+; =================================================================
+; Block elements (code / poem / block / group / table / quote)
+; =================================================================
+; Whole block including delimiters; raw_content is the inner.
+
+(code_block_curly) @function.outer
+(code_block_curly
+ content: (raw_content) @function.inner)
+
+(code_block_tic) @function.outer
+(code_block_tic
+ content: (raw_content) @function.inner)
+
+(poem_block_curly) @function.outer
+(poem_block_curly
+ content: (raw_content) @function.inner)
+
+(poem_block_tic) @function.outer
+(poem_block_tic
+ content: (raw_content) @function.inner)
+
+(block_block_curly) @function.outer
+(block_block_curly
+ content: (raw_content) @function.inner)
+
+(block_block_tic) @function.outer
+(block_block_tic
+ content: (raw_content) @function.inner)
+
+(group_block_curly) @function.outer
+(group_block_curly
+ content: (raw_content) @function.inner)
+
+(group_block_tic) @function.outer
+(group_block_tic
+ content: (raw_content) @function.inner)
+
+(table_block_curly) @function.outer
+(table_block_curly
+ content: (raw_content) @function.inner)
+
+(table_block_tic) @function.outer
+(table_block_tic
+ content: (raw_content) @function.inner)
+
+(quote_block_tic) @function.outer
+(quote_block_tic
+ content: (raw_content) @function.inner)
+
+(pipe_table) @function.outer
+
+; =================================================================
+; Footnotes and editor notes
+; =================================================================
+; Both share the same outer/inner shape; the inner skips the markers and
+; closing delimiters.
+
+(footnote) @comment.outer
+(footnote
+ (_)+ @comment.inner)
+
+(editor_note) @comment.outer
+(editor_note
+ (_)+ @comment.inner)
+
+; =================================================================
+; Links and images
+; =================================================================
+
+(link) @parameter.outer
+(link
+ text: (link_text) @parameter.inner)
+
+(image) @parameter.outer
+(image
+ spec: (image_spec) @parameter.inner)
+
+; =================================================================
+; Paragraph / inline-formatting runs
+; =================================================================
+
+(paragraph) @block.outer
+(paragraph
+ (_)+ @block.inner)
+
+; Inline formatting pairs - useful as fine-grained text objects.
+; The same delimiter character pattern (e.g. `*{` / `}*`) opens and
+; closes each, so .inner is everything between them.
+
+(emphasis) @assignment.outer
+(bold) @assignment.outer
+(italic) @assignment.outer
+(underline) @assignment.outer
+(citation_mark) @assignment.outer
+(superscript) @assignment.outer
+(subscript) @assignment.outer
+(inserted) @assignment.outer
+(strikethrough) @assignment.outer
+(monospace_inline) @assignment.outer
+
+; =================================================================
+; Book index entries
+; =================================================================
+
+(book_index) @attribute.outer
+(book_index
+ (index_content) @attribute.inner)
+
+; =================================================================
+; Header fields
+; =================================================================
+
+(header_field) @assignment.outer
+(header_field
+ value: (header_value) @assignment.inner)
+#+END_SRC
+
+* Emacs Syntax highlighting
** README
#+HEADER: :tangle "../sundry/editor-syntax-etc/emacs/README"
@@ -1914,18 +2546,36 @@ make:
#+HEADER: :tangle "../sundry/editor-syntax-etc/emacs/sisu-spine-mode-autoloads.el"
#+BEGIN_SRC elisp
(add-to-list 'load-path (or (file-name-directory #$) (car load-path)))
+;; Regex / font-lock major mode. Fallback for Emacs < 29 and for users
+;; who have not installed the tree-sitter-sisu parser.
(autoload 'sisu-spine-mode "sisu-spine-mode" "\
Major mode for editing SiSU (spine) markup files.
SiSU (https://www.sisudoc.org/) document structuring, publishing
and search.
\(fn)" t nil)
-(add-to-list 'auto-mode-alist '("\\.sst\\'" . sisu-spine-mode))
-(add-to-list 'auto-mode-alist '("\\.ssm\\'" . sisu-spine-mode))
-(add-to-list 'auto-mode-alist '("\\.ssi\\'" . sisu-spine-mode))
+
+;; Tree-sitter major mode (Emacs 29+). When the parser is installed it
+;; is selected by `auto-mode-alist'; otherwise the regex mode is used.
+(autoload 'sisu-spine-ts-mode "sisu-spine-ts-mode" "\
+Tree-sitter major mode for editing SiSU (spine) markup files.
+
+\(fn)" t nil)
+(autoload 'sisu-spine-ts-install-grammar "sisu-spine-ts-mode" "\
+Install the tree-sitter-sisu grammar for `sisu-spine-ts-mode'.
+\(fn)" t nil)
+(defun sisu-spine-auto-mode ()
+ "Choose `sisu-spine-ts-mode' if the parser is installed, else fall back."
+ (if (and (fboundp 'treesit-ready-p) (treesit-ready-p 'sisu t))
+ (sisu-spine-ts-mode)
+ (sisu-spine-mode)))
+
+(add-to-list 'auto-mode-alist '("\\.sst\\'" . sisu-spine-auto-mode))
+(add-to-list 'auto-mode-alist '("\\.ssm\\'" . sisu-spine-auto-mode))
+(add-to-list 'auto-mode-alist '("\\.ssi\\'" . sisu-spine-auto-mode))
#+END_SRC
-** mode sisu-spine-mode.el
+** mode sisu-spine-mode.el (regex)
#+HEADER: :tangle "../sundry/editor-syntax-etc/emacs/sisu-spine-mode.el"
#+BEGIN_SRC elisp
@@ -2432,3 +3082,305 @@ URL `https://www.sisudoc.org/'"
;;; sisu-spine-mode.el ends here
#+END_SRC
+
+** mode sisu-spine-ts-mode.el (tree-sitter)
+
+#+HEADER: :tangle "../sundry/editor-syntax-etc/emacs/sisu-spine-ts-mode.el"
+#+BEGIN_SRC elisp
+;;; sisu-spine-ts-mode.el --- Tree-sitter major mode for SiSU spine markup -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Free Software Foundation, Inc.
+
+;; Author: Ralph Amissah <ralph.amissah@gmail.com>
+;; Maintainer: Ralph Amissah <ralph.amissah@gmail.com>
+;; Keywords: text, syntax, processes, tools
+;; Version: 1.0.0
+;; URL: https://git.sisudoc.org/projects/sisudoc-spine/tree/sundry/editor-syntax-etc/emacs/sisu-spine-ts-mode.el
+;; https://sisudoc.org/
+
+;; 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, or (at your option)
+;; any later version.
+
+;;; Commentary:
+
+;; Tree-sitter-backed major mode for SiSU spine markup (.sst / .ssm /
+;; .ssi). Sibling to `sisu-spine-mode' (regex / font-lock); requires
+;; Emacs 29 or newer with `treesit' built in and the tree-sitter-sisu
+;; parser installed.
+;;
+;; To install the parser inside Emacs:
+;;
+;; M-x sisu-spine-ts-install-grammar RET
+;;
+;; or, equivalently:
+;;
+;; (add-to-list 'treesit-language-source-alist
+;; '(sisu "https://git.sisudoc.org/projects"
+;; :source-dir "tree-sitter-sisu/src"))
+;; M-x treesit-install-language-grammar RET sisu RET
+;;
+;; The mode is auto-enabled for .sst / .ssm / .ssi files when the parser
+;; is available. When it is not, `sisu-spine-mode' (the regex variant)
+;; remains available as a fallback.
+
+;;; Code:
+
+(require 'treesit nil t)
+
+(defgroup sisu-spine-ts nil
+ "Tree-sitter mode for SiSU spine markup."
+ :group 'text
+ :prefix "sisu-spine-ts-")
+
+;; ---------------------------------------------------------------------
+;; Faces (mirror the structural categories the highlights.scm assigns)
+;; ---------------------------------------------------------------------
+
+(defface sisu-spine-ts-heading-1-face
+ '((t (:inherit outline-1 :weight bold)))
+ "Face for :A~ headings."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-heading-2-face
+ '((t (:inherit outline-2 :weight bold)))
+ "Face for :B~ headings."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-heading-3-face
+ '((t (:inherit outline-3 :weight bold)))
+ "Face for :C~ / :D~ headings."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-heading-segment-face
+ '((t (:inherit outline-4)))
+ "Face for 1~ / 2~ / 3~ segment headings."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-block-delimiter-face
+ '((t (:inherit font-lock-keyword-face)))
+ "Face for block opening/closing delimiters."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-raw-content-face
+ '((t (:inherit font-lock-string-face)))
+ "Face for raw block content (code, table, etc.)."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-footnote-face
+ '((t (:inherit font-lock-doc-face)))
+ "Face for footnote and editor-note bodies."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-link-face
+ '((t (:inherit link)))
+ "Face for link text and target URLs."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-book-index-face
+ '((t (:inherit font-lock-preprocessor-face)))
+ "Face for book-index entries (={...})."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-marker-face
+ '((t (:inherit font-lock-builtin-face)))
+ "Face for inline-formatting delimiters and other punctuation markers."
+ :group 'sisu-spine-ts)
+
+;; ---------------------------------------------------------------------
+;; Font-lock rules
+;; ---------------------------------------------------------------------
+
+(defvar sisu-spine-ts--font-lock-settings
+ (when (fboundp 'treesit-font-lock-rules)
+ (treesit-font-lock-rules
+ :language 'sisu
+ :feature 'comment
+ '((version_comment) @font-lock-doc-face
+ (header_comment) @font-lock-comment-face
+ (body_comment) @font-lock-comment-face)
+
+ :language 'sisu
+ :feature 'header
+ '((header_field key: (header_key) @font-lock-keyword-face)
+ (header_field value: (header_value) @font-lock-string-face)
+ (header_continuation) @font-lock-string-face)
+
+ :language 'sisu
+ :feature 'heading
+ '(((heading_part marker: (part_marker) @m
+ content: (heading_content) @sisu-spine-ts-heading-1-face)
+ (:match "^:A~$" @m))
+ ((heading_part marker: (part_marker) @m
+ content: (heading_content) @sisu-spine-ts-heading-2-face)
+ (:match "^:B~$" @m))
+ ((heading_part marker: (part_marker) @m
+ content: (heading_content) @sisu-spine-ts-heading-3-face)
+ (:match "^:[CD]~$" @m))
+ (heading_part marker: (part_marker) @sisu-spine-ts-marker-face)
+ (heading_segment marker: (segment_marker) @sisu-spine-ts-marker-face
+ content: (heading_content) @sisu-spine-ts-heading-segment-face)
+ (segment_name) @font-lock-function-name-face
+ (suppress_marker) @sisu-spine-ts-marker-face)
+
+ :language 'sisu
+ :feature 'block
+ '((block_open) @sisu-spine-ts-block-delimiter-face
+ (block_close) @sisu-spine-ts-block-delimiter-face
+ (raw_content) @sisu-spine-ts-raw-content-face
+ (table_spec) @sisu-spine-ts-block-delimiter-face)
+
+ :language 'sisu
+ :feature 'inline
+ '((emphasis) @italic
+ (bold) @bold
+ (italic) @italic
+ (underline) @underline
+ (citation_mark) @font-lock-string-face
+ (superscript) @font-lock-type-face
+ (subscript) @font-lock-type-face
+ (inserted) @underline
+ (strikethrough) @shadow
+ (monospace_inline) @font-lock-constant-face)
+
+ :language 'sisu
+ :feature 'note
+ '((footnote) @sisu-spine-ts-footnote-face
+ (footnote_marker) @sisu-spine-ts-marker-face
+ (editor_note) @sisu-spine-ts-footnote-face)
+
+ :language 'sisu
+ :feature 'link
+ '((link text: (link_text) @sisu-spine-ts-link-face)
+ (link target: (url) @sisu-spine-ts-link-face)
+ (link target: (anchor_ref) @sisu-spine-ts-link-face)
+ (link target: (collection_path) @sisu-spine-ts-link-face)
+ (image spec: (image_spec) @sisu-spine-ts-link-face)
+ (auto_footnote_marker) @sisu-spine-ts-marker-face
+ (inline_anchor) @font-lock-function-name-face
+ (anchor_name) @font-lock-function-name-face)
+
+ :language 'sisu
+ :feature 'index
+ '((book_index) @sisu-spine-ts-book-index-face
+ (index_content) @font-lock-string-face)
+
+ :language 'sisu
+ :feature 'misc
+ '((paragraph_prefix) @sisu-spine-ts-marker-face
+ (page_break) @sisu-spine-ts-marker-face
+ (horizontal_rule) @sisu-spine-ts-marker-face
+ (line_break) @sisu-spine-ts-marker-face
+ (ocn_suppress) @font-lock-comment-face
+ (ocn_suppress_open) @font-lock-comment-face
+ (ocn_suppress_close) @font-lock-comment-face
+ (composite_include) @font-lock-preprocessor-face
+ (include_path) @font-lock-string-face)))
+ "Tree-sitter font-lock rules for `sisu-spine-ts-mode'.")
+
+;; ---------------------------------------------------------------------
+;; Imenu / navigation / things
+;; ---------------------------------------------------------------------
+
+(defvar sisu-spine-ts--imenu-settings
+ '(("Part headings"
+ "\\`heading_part\\'"
+ nil
+ sisu-spine-ts--imenu-name-part)
+ ("Segment headings"
+ "\\`heading_segment\\'"
+ nil
+ sisu-spine-ts--imenu-name-segment))
+ "`treesit-simple-imenu-settings' for `sisu-spine-ts-mode'.")
+
+(defun sisu-spine-ts--imenu-name-part (node)
+ "Return display name for a heading_part NODE."
+ (let ((c (treesit-node-child-by-field-name node "content"))
+ (m (treesit-node-child-by-field-name node "marker")))
+ (concat (and m (treesit-node-text m t)) " "
+ (and c (treesit-node-text c t)))))
+
+(defun sisu-spine-ts--imenu-name-segment (node)
+ "Return display name for a heading_segment NODE."
+ (let ((c (treesit-node-child-by-field-name node "content"))
+ (m (treesit-node-child-by-field-name node "marker")))
+ (concat (and m (treesit-node-text m t)) " "
+ (and c (treesit-node-text c t)))))
+
+(defvar sisu-spine-ts--thing-settings
+ '((sisu
+ (defun "\\`heading_\\(part\\|segment\\)\\'")
+ (sentence "\\`paragraph\\'")
+ (text "\\`\\(text\\|raw_content\\|heading_content\\)\\'")))
+ "`treesit-thing-settings' for `sisu-spine-ts-mode'.")
+
+;; ---------------------------------------------------------------------
+;; Grammar install helper
+;; ---------------------------------------------------------------------
+
+;;;###autoload
+(defun sisu-spine-ts-install-grammar ()
+ "Register and install the tree-sitter-sisu grammar.
+Convenience wrapper around `treesit-install-language-grammar' with the
+upstream URL and source directory pre-filled."
+ (interactive)
+ (unless (boundp 'treesit-language-source-alist)
+ (user-error "treesit not available; Emacs 29+ required"))
+ (add-to-list 'treesit-language-source-alist
+ '(sisu "https://git.sisudoc.org/projects"
+ :source-dir "tree-sitter-sisu/src"))
+ (treesit-install-language-grammar 'sisu))
+
+;; ---------------------------------------------------------------------
+;; Major mode
+;; ---------------------------------------------------------------------
+
+;;;###autoload
+(define-derived-mode sisu-spine-ts-mode text-mode "SiSU-Spine[ts]"
+ "Major mode for SiSU spine markup, backed by tree-sitter."
+ (unless (and (fboundp 'treesit-ready-p) (treesit-ready-p 'sisu))
+ (user-error
+ "tree-sitter-sisu parser not installed; run M-x sisu-spine-ts-install-grammar"))
+ (treesit-parser-create 'sisu)
+
+ ;; Comments
+ (setq-local comment-start "% "
+ comment-end ""
+ comment-start-skip "%[ \t]+")
+
+ ;; Font-lock
+ (setq-local treesit-font-lock-settings sisu-spine-ts--font-lock-settings)
+ (setq-local treesit-font-lock-feature-list
+ '((comment header heading)
+ (block inline note link index)
+ (misc)
+ ()))
+
+ ;; Imenu / navigation
+ (setq-local treesit-simple-imenu-settings sisu-spine-ts--imenu-settings)
+ (setq-local treesit-thing-settings sisu-spine-ts--thing-settings)
+ (setq-local treesit-defun-type-regexp "\\`heading_\\(part\\|segment\\)\\'")
+ (setq-local treesit-defun-name-function
+ (lambda (node)
+ (let ((c (treesit-node-child-by-field-name node "content")))
+ (and c (treesit-node-text c t)))))
+
+ (treesit-major-mode-setup))
+
+;;;###autoload
+(when (fboundp 'treesit-ready-p)
+ (dolist (ext '("\\.sst\\'" "\\.ssm\\'" "\\.ssi\\'"))
+ ;; Prefer the ts mode iff the parser is installed; otherwise fall
+ ;; back to `sisu-spine-mode'.
+ (add-to-list 'auto-mode-alist
+ (cons ext
+ (lambda ()
+ (if (treesit-ready-p 'sisu t)
+ (sisu-spine-ts-mode)
+ (sisu-spine-mode)))))))
+
+(provide 'sisu-spine-ts-mode)
+
+;;; sisu-spine-ts-mode.el ends here
+#+END_SRC
diff --git a/org/util_spine_markup_conversion_from_sisu.org b/org/util_spine_markup_conversion_from_sisu.org
index 7c58479..a4bcb5b 100644
--- a/org/util_spine_markup_conversion_from_sisu.org
+++ b/org/util_spine_markup_conversion_from_sisu.org
@@ -21,14 +21,14 @@
** README
-#+HEADER: :tangle "../sundry/misc/util/d/tools/markup_conversion/README"
+#+HEADER: :tangle "../sundry/util/d/tools/markup_conversion/README"
#+BEGIN_SRC text
#+END_SRC
** endnotes, inline from binary
*** tangle
-#+HEADER: :tangle "../sundry/misc/util/d/tools/markup_conversion/endnotes_inline_from_binary.d"
+#+HEADER: :tangle "../sundry/util/d/tools/markup_conversion/endnotes_inline_from_binary.d"
#+HEADER: :tangle-mode (identity #o755)
#+HEADER: :shebang #!/usr/bin/env rdmd
#+BEGIN_SRC d
@@ -208,7 +208,7 @@ if (endnotes.length == endnote_ref_count) {
** conversion from sisu (sisu bespoke headers) any binary to inline notes TODO
*** tangle
-#+HEADER: :tangle "../sundry/misc/util/d/tools/markup_conversion/markup_conversion_from_sisu_ruby_to_sisu_spine.d"
+#+HEADER: :tangle "../sundry/util/d/tools/markup_conversion/markup_conversion_from_sisu_ruby_to_sisu_spine.d"
#+HEADER: :tangle-mode (identity #o755)
#+HEADER: :shebang #!/usr/bin/env rdmd
#+BEGIN_SRC d
@@ -669,7 +669,7 @@ foreach (paragraph; paragraphs) {
** conversion from sisu and multiple headers (sisu bespoke, sdlang, toml) incomplete
*** tangle
-#+HEADER: :tangle "../sundry/misc/util/d/tools/markup_conversion/markup_changes_header_and_content.d"
+#+HEADER: :tangle "../sundry/util/d/tools/markup_conversion/markup_changes_header_and_content.d"
#+HEADER: :tangle-mode (identity #o755)
#+HEADER: :shebang #!/usr/bin/env rdmd
#+BEGIN_SRC d
diff --git a/sundry/editor-syntax-etc/emacs/sisu-spine-mode-autoloads.el b/sundry/editor-syntax-etc/emacs/sisu-spine-mode-autoloads.el
index 4cc6332..098a28c 100644
--- a/sundry/editor-syntax-etc/emacs/sisu-spine-mode-autoloads.el
+++ b/sundry/editor-syntax-etc/emacs/sisu-spine-mode-autoloads.el
@@ -1,10 +1,28 @@
(add-to-list 'load-path (or (file-name-directory #$) (car load-path)))
+;; Regex / font-lock major mode. Fallback for Emacs < 29 and for users
+;; who have not installed the tree-sitter-sisu parser.
(autoload 'sisu-spine-mode "sisu-spine-mode" "\
Major mode for editing SiSU (spine) markup files.
SiSU (https://www.sisudoc.org/) document structuring, publishing
and search.
\(fn)" t nil)
-(add-to-list 'auto-mode-alist '("\\.sst\\'" . sisu-spine-mode))
-(add-to-list 'auto-mode-alist '("\\.ssm\\'" . sisu-spine-mode))
-(add-to-list 'auto-mode-alist '("\\.ssi\\'" . sisu-spine-mode))
+
+;; Tree-sitter major mode (Emacs 29+). When the parser is installed it
+;; is selected by `auto-mode-alist'; otherwise the regex mode is used.
+(autoload 'sisu-spine-ts-mode "sisu-spine-ts-mode" "\
+Tree-sitter major mode for editing SiSU (spine) markup files.
+
+\(fn)" t nil)
+(autoload 'sisu-spine-ts-install-grammar "sisu-spine-ts-mode" "\
+Install the tree-sitter-sisu grammar for `sisu-spine-ts-mode'.
+\(fn)" t nil)
+(defun sisu-spine-auto-mode ()
+ "Choose `sisu-spine-ts-mode' if the parser is installed, else fall back."
+ (if (and (fboundp 'treesit-ready-p) (treesit-ready-p 'sisu t))
+ (sisu-spine-ts-mode)
+ (sisu-spine-mode)))
+
+(add-to-list 'auto-mode-alist '("\\.sst\\'" . sisu-spine-auto-mode))
+(add-to-list 'auto-mode-alist '("\\.ssm\\'" . sisu-spine-auto-mode))
+(add-to-list 'auto-mode-alist '("\\.ssi\\'" . sisu-spine-auto-mode))
diff --git a/sundry/editor-syntax-etc/emacs/sisu-spine-ts-mode.el b/sundry/editor-syntax-etc/emacs/sisu-spine-ts-mode.el
new file mode 100644
index 0000000..ef59d14
--- /dev/null
+++ b/sundry/editor-syntax-etc/emacs/sisu-spine-ts-mode.el
@@ -0,0 +1,296 @@
+;;; sisu-spine-ts-mode.el --- Tree-sitter major mode for SiSU spine markup -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Free Software Foundation, Inc.
+
+;; Author: Ralph Amissah <ralph.amissah@gmail.com>
+;; Maintainer: Ralph Amissah <ralph.amissah@gmail.com>
+;; Keywords: text, syntax, processes, tools
+;; Version: 1.0.0
+;; URL: https://git.sisudoc.org/projects/sisudoc-spine/tree/sundry/editor-syntax-etc/emacs/sisu-spine-ts-mode.el
+;; https://sisudoc.org/
+
+;; 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, or (at your option)
+;; any later version.
+
+;;; Commentary:
+
+;; Tree-sitter-backed major mode for SiSU spine markup (.sst / .ssm /
+;; .ssi). Sibling to `sisu-spine-mode' (regex / font-lock); requires
+;; Emacs 29 or newer with `treesit' built in and the tree-sitter-sisu
+;; parser installed.
+;;
+;; To install the parser inside Emacs:
+;;
+;; M-x sisu-spine-ts-install-grammar RET
+;;
+;; or, equivalently:
+;;
+;; (add-to-list 'treesit-language-source-alist
+;; '(sisu "https://git.sisudoc.org/projects"
+;; :source-dir "tree-sitter-sisu/src"))
+;; M-x treesit-install-language-grammar RET sisu RET
+;;
+;; The mode is auto-enabled for .sst / .ssm / .ssi files when the parser
+;; is available. When it is not, `sisu-spine-mode' (the regex variant)
+;; remains available as a fallback.
+
+;;; Code:
+
+(require 'treesit nil t)
+
+(defgroup sisu-spine-ts nil
+ "Tree-sitter mode for SiSU spine markup."
+ :group 'text
+ :prefix "sisu-spine-ts-")
+
+;; ---------------------------------------------------------------------
+;; Faces (mirror the structural categories the highlights.scm assigns)
+;; ---------------------------------------------------------------------
+
+(defface sisu-spine-ts-heading-1-face
+ '((t (:inherit outline-1 :weight bold)))
+ "Face for :A~ headings."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-heading-2-face
+ '((t (:inherit outline-2 :weight bold)))
+ "Face for :B~ headings."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-heading-3-face
+ '((t (:inherit outline-3 :weight bold)))
+ "Face for :C~ / :D~ headings."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-heading-segment-face
+ '((t (:inherit outline-4)))
+ "Face for 1~ / 2~ / 3~ segment headings."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-block-delimiter-face
+ '((t (:inherit font-lock-keyword-face)))
+ "Face for block opening/closing delimiters."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-raw-content-face
+ '((t (:inherit font-lock-string-face)))
+ "Face for raw block content (code, table, etc.)."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-footnote-face
+ '((t (:inherit font-lock-doc-face)))
+ "Face for footnote and editor-note bodies."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-link-face
+ '((t (:inherit link)))
+ "Face for link text and target URLs."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-book-index-face
+ '((t (:inherit font-lock-preprocessor-face)))
+ "Face for book-index entries (={...})."
+ :group 'sisu-spine-ts)
+
+(defface sisu-spine-ts-marker-face
+ '((t (:inherit font-lock-builtin-face)))
+ "Face for inline-formatting delimiters and other punctuation markers."
+ :group 'sisu-spine-ts)
+
+;; ---------------------------------------------------------------------
+;; Font-lock rules
+;; ---------------------------------------------------------------------
+
+(defvar sisu-spine-ts--font-lock-settings
+ (when (fboundp 'treesit-font-lock-rules)
+ (treesit-font-lock-rules
+ :language 'sisu
+ :feature 'comment
+ '((version_comment) @font-lock-doc-face
+ (header_comment) @font-lock-comment-face
+ (body_comment) @font-lock-comment-face)
+
+ :language 'sisu
+ :feature 'header
+ '((header_field key: (header_key) @font-lock-keyword-face)
+ (header_field value: (header_value) @font-lock-string-face)
+ (header_continuation) @font-lock-string-face)
+
+ :language 'sisu
+ :feature 'heading
+ '(((heading_part marker: (part_marker) @m
+ content: (heading_content) @sisu-spine-ts-heading-1-face)
+ (:match "^:A~$" @m))
+ ((heading_part marker: (part_marker) @m
+ content: (heading_content) @sisu-spine-ts-heading-2-face)
+ (:match "^:B~$" @m))
+ ((heading_part marker: (part_marker) @m
+ content: (heading_content) @sisu-spine-ts-heading-3-face)
+ (:match "^:[CD]~$" @m))
+ (heading_part marker: (part_marker) @sisu-spine-ts-marker-face)
+ (heading_segment marker: (segment_marker) @sisu-spine-ts-marker-face
+ content: (heading_content) @sisu-spine-ts-heading-segment-face)
+ (segment_name) @font-lock-function-name-face
+ (suppress_marker) @sisu-spine-ts-marker-face)
+
+ :language 'sisu
+ :feature 'block
+ '((block_open) @sisu-spine-ts-block-delimiter-face
+ (block_close) @sisu-spine-ts-block-delimiter-face
+ (raw_content) @sisu-spine-ts-raw-content-face
+ (table_spec) @sisu-spine-ts-block-delimiter-face)
+
+ :language 'sisu
+ :feature 'inline
+ '((emphasis) @italic
+ (bold) @bold
+ (italic) @italic
+ (underline) @underline
+ (citation_mark) @font-lock-string-face
+ (superscript) @font-lock-type-face
+ (subscript) @font-lock-type-face
+ (inserted) @underline
+ (strikethrough) @shadow
+ (monospace_inline) @font-lock-constant-face)
+
+ :language 'sisu
+ :feature 'note
+ '((footnote) @sisu-spine-ts-footnote-face
+ (footnote_marker) @sisu-spine-ts-marker-face
+ (editor_note) @sisu-spine-ts-footnote-face)
+
+ :language 'sisu
+ :feature 'link
+ '((link text: (link_text) @sisu-spine-ts-link-face)
+ (link target: (url) @sisu-spine-ts-link-face)
+ (link target: (anchor_ref) @sisu-spine-ts-link-face)
+ (link target: (collection_path) @sisu-spine-ts-link-face)
+ (image spec: (image_spec) @sisu-spine-ts-link-face)
+ (auto_footnote_marker) @sisu-spine-ts-marker-face
+ (inline_anchor) @font-lock-function-name-face
+ (anchor_name) @font-lock-function-name-face)
+
+ :language 'sisu
+ :feature 'index
+ '((book_index) @sisu-spine-ts-book-index-face
+ (index_content) @font-lock-string-face)
+
+ :language 'sisu
+ :feature 'misc
+ '((paragraph_prefix) @sisu-spine-ts-marker-face
+ (page_break) @sisu-spine-ts-marker-face
+ (horizontal_rule) @sisu-spine-ts-marker-face
+ (line_break) @sisu-spine-ts-marker-face
+ (ocn_suppress) @font-lock-comment-face
+ (ocn_suppress_open) @font-lock-comment-face
+ (ocn_suppress_close) @font-lock-comment-face
+ (composite_include) @font-lock-preprocessor-face
+ (include_path) @font-lock-string-face)))
+ "Tree-sitter font-lock rules for `sisu-spine-ts-mode'.")
+
+;; ---------------------------------------------------------------------
+;; Imenu / navigation / things
+;; ---------------------------------------------------------------------
+
+(defvar sisu-spine-ts--imenu-settings
+ '(("Part headings"
+ "\\`heading_part\\'"
+ nil
+ sisu-spine-ts--imenu-name-part)
+ ("Segment headings"
+ "\\`heading_segment\\'"
+ nil
+ sisu-spine-ts--imenu-name-segment))
+ "`treesit-simple-imenu-settings' for `sisu-spine-ts-mode'.")
+
+(defun sisu-spine-ts--imenu-name-part (node)
+ "Return display name for a heading_part NODE."
+ (let ((c (treesit-node-child-by-field-name node "content"))
+ (m (treesit-node-child-by-field-name node "marker")))
+ (concat (and m (treesit-node-text m t)) " "
+ (and c (treesit-node-text c t)))))
+
+(defun sisu-spine-ts--imenu-name-segment (node)
+ "Return display name for a heading_segment NODE."
+ (let ((c (treesit-node-child-by-field-name node "content"))
+ (m (treesit-node-child-by-field-name node "marker")))
+ (concat (and m (treesit-node-text m t)) " "
+ (and c (treesit-node-text c t)))))
+
+(defvar sisu-spine-ts--thing-settings
+ '((sisu
+ (defun "\\`heading_\\(part\\|segment\\)\\'")
+ (sentence "\\`paragraph\\'")
+ (text "\\`\\(text\\|raw_content\\|heading_content\\)\\'")))
+ "`treesit-thing-settings' for `sisu-spine-ts-mode'.")
+
+;; ---------------------------------------------------------------------
+;; Grammar install helper
+;; ---------------------------------------------------------------------
+
+;;;###autoload
+(defun sisu-spine-ts-install-grammar ()
+ "Register and install the tree-sitter-sisu grammar.
+Convenience wrapper around `treesit-install-language-grammar' with the
+upstream URL and source directory pre-filled."
+ (interactive)
+ (unless (boundp 'treesit-language-source-alist)
+ (user-error "treesit not available; Emacs 29+ required"))
+ (add-to-list 'treesit-language-source-alist
+ '(sisu "https://git.sisudoc.org/projects"
+ :source-dir "tree-sitter-sisu/src"))
+ (treesit-install-language-grammar 'sisu))
+
+;; ---------------------------------------------------------------------
+;; Major mode
+;; ---------------------------------------------------------------------
+
+;;;###autoload
+(define-derived-mode sisu-spine-ts-mode text-mode "SiSU-Spine[ts]"
+ "Major mode for SiSU spine markup, backed by tree-sitter."
+ (unless (and (fboundp 'treesit-ready-p) (treesit-ready-p 'sisu))
+ (user-error
+ "tree-sitter-sisu parser not installed; run M-x sisu-spine-ts-install-grammar"))
+ (treesit-parser-create 'sisu)
+
+ ;; Comments
+ (setq-local comment-start "% "
+ comment-end ""
+ comment-start-skip "%[ \t]+")
+
+ ;; Font-lock
+ (setq-local treesit-font-lock-settings sisu-spine-ts--font-lock-settings)
+ (setq-local treesit-font-lock-feature-list
+ '((comment header heading)
+ (block inline note link index)
+ (misc)
+ ()))
+
+ ;; Imenu / navigation
+ (setq-local treesit-simple-imenu-settings sisu-spine-ts--imenu-settings)
+ (setq-local treesit-thing-settings sisu-spine-ts--thing-settings)
+ (setq-local treesit-defun-type-regexp "\\`heading_\\(part\\|segment\\)\\'")
+ (setq-local treesit-defun-name-function
+ (lambda (node)
+ (let ((c (treesit-node-child-by-field-name node "content")))
+ (and c (treesit-node-text c t)))))
+
+ (treesit-major-mode-setup))
+
+;;;###autoload
+(when (fboundp 'treesit-ready-p)
+ (dolist (ext '("\\.sst\\'" "\\.ssm\\'" "\\.ssi\\'"))
+ ;; Prefer the ts mode iff the parser is installed; otherwise fall
+ ;; back to `sisu-spine-mode'.
+ (add-to-list 'auto-mode-alist
+ (cons ext
+ (lambda ()
+ (if (treesit-ready-p 'sisu t)
+ (sisu-spine-ts-mode)
+ (sisu-spine-mode)))))))
+
+(provide 'sisu-spine-ts-mode)
+
+;;; sisu-spine-ts-mode.el ends here
diff --git a/sundry/editor-syntax-etc/nvim/README.md b/sundry/editor-syntax-etc/nvim/README.md
new file mode 100644
index 0000000..8e889e7
--- /dev/null
+++ b/sundry/editor-syntax-etc/nvim/README.md
@@ -0,0 +1,87 @@
+# Neovim integration for SiSU spine markup
+
+Tree-sitter-backed syntax highlighting, folding, and structural
+navigation for `.sst` / `.ssm` / `.ssi` files in Neovim (>= 0.9).
+
+## What is in this directory
+
+```
+nvim/
+ ftdetect/sisu.lua - register .sst/.ssm/.ssi as filetype "sisu"
+ ftplugin/sisu.lua - per-buffer settings (commentstring, conceal)
+ lua/sisu-spine/init.lua - entry point: registers parser config
+ queries/sisu/ - tree-sitter queries (mirrors tree-sitter-sisu/queries/)
+ highlights.scm
+ folds.scm
+ injections.scm
+ textobjects.scm
+ indents.scm
+```
+
+## Install (manual)
+
+1. Symlink or copy this directory into your Neovim runtime path:
+
+ ```sh
+ ln -s /path/to/sisudoc-spine/sundry/editor-syntax-etc/nvim \
+ ~/.config/nvim/pack/sisu/start/sisu-spine
+ ```
+
+2. Tell `nvim-treesitter` how to fetch the parser. Add to your config
+ (`init.lua`):
+
+ ```lua
+ require("sisu-spine").setup()
+ require("nvim-treesitter.configs").setup({
+ ensure_installed = { "sisu" },
+ highlight = { enable = true },
+ indent = { enable = true },
+ fold = { enable = true },
+ textobjects = { select = { enable = true, lookahead = true } },
+ })
+ ```
+
+3. Build the parser:
+
+ ```vim
+ :TSInstall sisu
+ ```
+
+That is it. Open a `.sst` file - highlighting, folding, and textobject
+selection should all work.
+
+## Install (lazy.nvim)
+
+```lua
+{
+ dir = "/path/to/sisudoc-spine/sundry/editor-syntax-etc/nvim",
+ name = "sisu-spine",
+ ft = { "sisu" },
+ dependencies = { "nvim-treesitter/nvim-treesitter" },
+ config = function()
+ require("sisu-spine").setup()
+ end,
+}
+```
+
+## Sync queries from upstream
+
+The query files are duplicated from `tree-sitter-sisu/queries/` so that
+this Neovim drop-in works without depending on the parser repo's
+checkout layout. To refresh them after grammar changes:
+
+```sh
+cp ../../../sisudoc-spine-tools/tree-sitter-sisu/queries/*.scm \
+ queries/sisu/
+```
+
+(Path is relative to this README.)
+
+## Upstreaming the parser
+
+When the parser is publicly hosted under a stable URL it is worth
+submitting a config to `nvim-treesitter` so users can run `:TSInstall
+sisu` without the local `setup()` call. The required fields are in
+`lua/sisu-spine/init.lua` (`install_info` table); send a PR to
+<https://github.com/nvim-treesitter/nvim-treesitter> patching
+`lua/nvim-treesitter/parsers.lua`.
diff --git a/sundry/editor-syntax-etc/nvim/ftdetect/sisu.lua b/sundry/editor-syntax-etc/nvim/ftdetect/sisu.lua
new file mode 100644
index 0000000..51b4f2f
--- /dev/null
+++ b/sundry/editor-syntax-etc/nvim/ftdetect/sisu.lua
@@ -0,0 +1,7 @@
+vim.filetype.add({
+ extension = {
+ sst = "sisu",
+ ssm = "sisu",
+ ssi = "sisu",
+ },
+})
diff --git a/sundry/editor-syntax-etc/nvim/ftplugin/sisu.lua b/sundry/editor-syntax-etc/nvim/ftplugin/sisu.lua
new file mode 100644
index 0000000..a531238
--- /dev/null
+++ b/sundry/editor-syntax-etc/nvim/ftplugin/sisu.lua
@@ -0,0 +1,13 @@
+-- Buffer-local settings for SiSU spine markup.
+
+vim.bo.commentstring = "%% %s"
+vim.bo.comments = ":%"
+
+-- Soft wrap suits prose.
+vim.wo.wrap = true
+vim.wo.linebreak = true
+
+-- Conceal inline-formatting delimiters when the user opts in
+-- (`:set conceallevel=2`). See queries/sisu/highlights.scm for
+-- @conceal captures.
+vim.wo.conceallevel = vim.wo.conceallevel
diff --git a/sundry/editor-syntax-etc/nvim/lua/sisu-spine/init.lua b/sundry/editor-syntax-etc/nvim/lua/sisu-spine/init.lua
new file mode 100644
index 0000000..d1ae3df
--- /dev/null
+++ b/sundry/editor-syntax-etc/nvim/lua/sisu-spine/init.lua
@@ -0,0 +1,43 @@
+-- Entry point for the SiSU spine markup Neovim integration.
+--
+-- Registers a tree-sitter parser config so users can run
+-- :TSInstall sisu
+-- to fetch and build the parser via nvim-treesitter.
+--
+-- The parser source lives can be found under the
+-- `projects/` namespace on git.sisudoc.org.
+
+local M = {}
+
+--- Register the `sisu` parser with nvim-treesitter and ensure that
+--- `.sst` / `.ssm` / `.ssi` are detected as filetype "sisu".
+---
+--- Call once from your init.lua before invoking `:TSInstall sisu`.
+function M.setup()
+ local ok, parsers = pcall(require, "nvim-treesitter.parsers")
+ if not ok then
+ vim.notify(
+ "sisu-spine: nvim-treesitter is not installed; "
+ .. "syntax highlighting will not be available.",
+ vim.log.levels.WARN
+ )
+ return
+ end
+
+ local parser_config = parsers.get_parser_configs()
+ parser_config.sisu = {
+ install_info = {
+ url = "https://git.sisudoc.org/projects/tree-sitter-sisu",
+ files = {
+ "src/parser.c",
+ "src/scanner.c",
+ },
+ branch = "main",
+ generate_requires_npm = false,
+ requires_generate_from_grammar = false,
+ },
+ filetype = "sisu",
+ }
+end
+
+return M
diff --git a/sundry/editor-syntax-etc/nvim/queries/sisu/folds.scm b/sundry/editor-syntax-etc/nvim/queries/sisu/folds.scm
new file mode 100644
index 0000000..69c44d2
--- /dev/null
+++ b/sundry/editor-syntax-etc/nvim/queries/sisu/folds.scm
@@ -0,0 +1,23 @@
+; Code folding queries for SiSU Spine markup
+
+; Block elements are foldable
+(code_block_curly) @fold
+(code_block_tic) @fold
+(poem_block_curly) @fold
+(poem_block_tic) @fold
+(block_block_curly) @fold
+(block_block_tic) @fold
+(group_block_curly) @fold
+(group_block_tic) @fold
+(table_block_curly) @fold
+(table_block_tic) @fold
+(quote_block_tic) @fold
+
+; Multi-line book index entries are foldable
+(book_index) @fold
+
+; Pipe tables are foldable
+(pipe_table) @fold
+
+; Header fields with continuations are foldable
+(header_field) @fold
diff --git a/sundry/editor-syntax-etc/nvim/queries/sisu/highlights.scm b/sundry/editor-syntax-etc/nvim/queries/sisu/highlights.scm
new file mode 100644
index 0000000..3454bd4
--- /dev/null
+++ b/sundry/editor-syntax-etc/nvim/queries/sisu/highlights.scm
@@ -0,0 +1,189 @@
+; Syntax highlighting queries for SiSU Spine markup
+; Compatible with tree-sitter highlight capture names from
+; https://tree-sitter.github.io/tree-sitter/syntax-highlighting
+
+; =================================================================
+; Comments
+; =================================================================
+(version_comment) @comment.documentation
+(header_comment) @comment
+(body_comment) @comment
+
+; =================================================================
+; Header (document metadata)
+; =================================================================
+(header_field
+ key: (header_key) @keyword)
+
+(header_field
+ value: (header_value) @string)
+
+(header_continuation) @string
+
+; =================================================================
+; Headings
+; =================================================================
+(part_marker) @keyword.directive
+(segment_marker) @keyword.directive
+
+(heading_part
+ content: (heading_content) @markup.heading)
+
+(heading_segment
+ content: (heading_content) @markup.heading)
+
+(segment_name) @label
+(suppress_marker) @punctuation.special
+
+; Heading levels for more specific styling
+(heading_part
+ marker: (part_marker) @markup.heading.1
+ (#match? @markup.heading.1 "^:A~$"))
+
+(heading_part
+ marker: (part_marker) @markup.heading.2
+ (#match? @markup.heading.2 "^:B~$"))
+
+(heading_part
+ marker: (part_marker) @markup.heading.3
+ (#match? @markup.heading.3 "^:C~$"))
+
+(heading_part
+ marker: (part_marker) @markup.heading.4
+ (#match? @markup.heading.4 "^:D~$"))
+
+(heading_segment
+ marker: (segment_marker) @markup.heading.5
+ (#match? @markup.heading.5 "^1~$"))
+
+(heading_segment
+ marker: (segment_marker) @markup.heading.6
+ (#match? @markup.heading.6 "^2~$"))
+
+; =================================================================
+; Inline formatting
+; =================================================================
+(emphasis) @markup.italic
+(bold) @markup.bold
+(italic) @markup.italic
+(underline) @markup.underline
+(citation_mark) @markup.quote
+(superscript) @markup.superscript
+(subscript) @markup.subscript
+(inserted) @markup.underline
+(strikethrough) @markup.strikethrough
+(monospace_inline) @markup.raw
+
+; Formatting delimiters
+["*{" "}*"] @punctuation.special
+["!{" "}!"] @punctuation.special
+["/{" "}/"] @punctuation.special
+["_{" "}_"] @punctuation.special
+["\"{" "}\""] @punctuation.special
+["^{" "}^"] @punctuation.special
+[",{" "},"] @punctuation.special
+["+{" "}+"] @punctuation.special
+["-{" "}-"] @punctuation.special
+["#{" "}#"] @punctuation.special
+
+; =================================================================
+; Footnotes and editor notes
+; =================================================================
+(footnote) @markup.link
+(footnote_marker) @punctuation.special
+(editor_note) @markup.link
+
+["~{" "}~"] @punctuation.special
+; Editor-note channel selector: ~[* (asterisk set) or ~[+ (plus set).
+; A distinct capture lets themes colour the two channels separately
+; from the generic footnote delimiters above.
+(editor_note_marker) @attribute
+["]~"] @punctuation.special
+
+; =================================================================
+; Links and images
+; =================================================================
+(link
+ text: (link_text) @markup.link.label)
+
+(link
+ target: (url) @markup.link.url)
+
+(link
+ target: (anchor_ref) @markup.link.url)
+
+(link
+ target: (collection_path) @markup.link.url)
+
+(auto_footnote_marker) @punctuation.special
+
+(image
+ spec: (image_spec) @markup.link.label)
+
+(url) @markup.link.url
+
+(inline_anchor) @label
+(anchor_name) @label
+
+; =================================================================
+; Block elements
+; =================================================================
+(block_open) @keyword.directive
+(block_close) @keyword.directive
+(raw_content) @markup.raw
+
+; Code blocks get more specific highlighting
+(code_block_curly
+ open: (block_open) @keyword.directive)
+(code_block_curly
+ content: (raw_content) @markup.raw.block)
+(code_block_curly
+ close: (block_close) @keyword.directive)
+
+(code_block_tic
+ open: (block_open) @keyword.directive)
+(code_block_tic
+ content: (raw_content) @markup.raw.block)
+(code_block_tic
+ close: (block_close) @keyword.directive)
+
+; =================================================================
+; Book index
+; =================================================================
+(book_index) @markup.list
+(index_content) @string
+
+; =================================================================
+; Paragraph prefixes
+; =================================================================
+(paragraph_prefix) @punctuation.special
+
+; =================================================================
+; Special markers
+; =================================================================
+(ocn_suppress) @comment
+(ocn_suppress_open) @comment
+(ocn_suppress_close) @comment
+
+(page_break) @punctuation.special
+(horizontal_rule) @punctuation.special
+
+; =================================================================
+; Composite includes
+; =================================================================
+(composite_include) @keyword.import
+(include_path) @string.special.path
+
+; =================================================================
+; Pipe table
+; =================================================================
+(table_spec) @keyword.directive
+(table_row) @markup.raw
+
+; =================================================================
+; Text
+; =================================================================
+(text) @spell
+
+; Line break
+(line_break) @punctuation.special
diff --git a/sundry/editor-syntax-etc/nvim/queries/sisu/indents.scm b/sundry/editor-syntax-etc/nvim/queries/sisu/indents.scm
new file mode 100644
index 0000000..aa73af8
--- /dev/null
+++ b/sundry/editor-syntax-etc/nvim/queries/sisu/indents.scm
@@ -0,0 +1,48 @@
+; Indentation queries for SiSU Spine markup.
+;
+; SiSU markup is largely flat: paragraphs and headings live at column 0,
+; block bodies preserve their author-supplied indentation verbatim, and
+; nesting is by markers rather than by indent. So indents.scm is mostly a
+; no-op - the goal is to ensure that auto-indent on <CR> stays at column 0
+; for normal lines and respects existing indentation inside header
+; continuations and blocks.
+
+; Tree-sitter indent semantics (per nvim-treesitter and treesit):
+; @indent.begin - increases indent for the following line
+; @indent.end - matches the @indent.begin and decreases indent
+; @indent.zero - resets indent to column 0
+; @indent.align - aligns following lines with this node
+; @indent.branch - same level as the parent (for else/elif-style joins)
+
+; Top-level structures live at column 0 - reset to zero on the next line.
+(heading_part) @indent.zero
+(heading_segment) @indent.zero
+(paragraph) @indent.zero
+(book_index) @indent.zero
+(composite_include) @indent.zero
+(page_break) @indent.zero
+(horizontal_rule) @indent.zero
+(ocn_suppress_open) @indent.zero
+(ocn_suppress_close) @indent.zero
+(body_comment) @indent.zero
+
+; Block elements: opening line increases indent for the body, closing
+; line returns to zero. Editors that respect this will visually indent
+; raw content one step from the delimiter line, which is conventional.
+(code_block_curly) @indent.align
+(code_block_tic) @indent.align
+(poem_block_curly) @indent.align
+(poem_block_tic) @indent.align
+(block_block_curly) @indent.align
+(block_block_tic) @indent.align
+(group_block_curly) @indent.align
+(group_block_tic) @indent.align
+(table_block_curly) @indent.align
+(table_block_tic) @indent.align
+(quote_block_tic) @indent.align
+
+; Header continuation lines are indented by two spaces from column 0;
+; mark continuations as align so a host that chooses to auto-indent the
+; next continuation line matches the previous one.
+(header_field) @indent.align
+(header_continuation) @indent.align
diff --git a/sundry/editor-syntax-etc/nvim/queries/sisu/injections.scm b/sundry/editor-syntax-etc/nvim/queries/sisu/injections.scm
new file mode 100644
index 0000000..27f622b
--- /dev/null
+++ b/sundry/editor-syntax-etc/nvim/queries/sisu/injections.scm
@@ -0,0 +1,16 @@
+; Language injection queries for SiSU Spine markup
+;
+; Code blocks could potentially inject language-specific highlighting,
+; but SiSU code blocks don't specify language. These queries are
+; provided as a starting point for future extension.
+
+; Code block content could be injected with a specific language
+; if the block type or context provides a hint.
+; For now, raw content in code blocks is left unhighlighted.
+
+; Example: if code blocks specified a language, e.g. code(d){
+; ((code_block_curly
+; open: (block_open) @_open
+; content: (raw_content) @injection.content)
+; (#match? @_open "code\\(d\\)")
+; (#set! injection.language "d"))
diff --git a/sundry/editor-syntax-etc/nvim/queries/sisu/textobjects.scm b/sundry/editor-syntax-etc/nvim/queries/sisu/textobjects.scm
new file mode 100644
index 0000000..0a82481
--- /dev/null
+++ b/sundry/editor-syntax-etc/nvim/queries/sisu/textobjects.scm
@@ -0,0 +1,140 @@
+; Text-object queries for SiSU Spine markup.
+;
+; Capture conventions follow nvim-treesitter/textobjects:
+; @<thing>.outer -> select including delimiters / surrounding whitespace
+; @<thing>.inner -> select content only
+;
+; Hosts that consume these (Neovim's nvim-treesitter-textobjects, Helix,
+; Emacs treesit) bind keys such as `af` / `if` to .outer / .inner.
+
+; =================================================================
+; Headings (sectioning units)
+; =================================================================
+; A whole heading line is a "section header" object. Heading sections
+; (the heading plus its body content up to the next heading of equal or
+; higher level) are not directly expressible in tree-sitter without
+; additional grammar work; hosts can synthesise that from these captures.
+
+(heading_part) @class.outer
+(heading_part
+ content: (heading_content) @class.inner)
+
+(heading_segment) @class.outer
+(heading_segment
+ content: (heading_content) @class.inner)
+
+; =================================================================
+; Block elements (code / poem / block / group / table / quote)
+; =================================================================
+; Whole block including delimiters; raw_content is the inner.
+
+(code_block_curly) @function.outer
+(code_block_curly
+ content: (raw_content) @function.inner)
+
+(code_block_tic) @function.outer
+(code_block_tic
+ content: (raw_content) @function.inner)
+
+(poem_block_curly) @function.outer
+(poem_block_curly
+ content: (raw_content) @function.inner)
+
+(poem_block_tic) @function.outer
+(poem_block_tic
+ content: (raw_content) @function.inner)
+
+(block_block_curly) @function.outer
+(block_block_curly
+ content: (raw_content) @function.inner)
+
+(block_block_tic) @function.outer
+(block_block_tic
+ content: (raw_content) @function.inner)
+
+(group_block_curly) @function.outer
+(group_block_curly
+ content: (raw_content) @function.inner)
+
+(group_block_tic) @function.outer
+(group_block_tic
+ content: (raw_content) @function.inner)
+
+(table_block_curly) @function.outer
+(table_block_curly
+ content: (raw_content) @function.inner)
+
+(table_block_tic) @function.outer
+(table_block_tic
+ content: (raw_content) @function.inner)
+
+(quote_block_tic) @function.outer
+(quote_block_tic
+ content: (raw_content) @function.inner)
+
+(pipe_table) @function.outer
+
+; =================================================================
+; Footnotes and editor notes
+; =================================================================
+; Both share the same outer/inner shape; the inner skips the markers and
+; closing delimiters.
+
+(footnote) @comment.outer
+(footnote
+ (_)+ @comment.inner)
+
+(editor_note) @comment.outer
+(editor_note
+ (_)+ @comment.inner)
+
+; =================================================================
+; Links and images
+; =================================================================
+
+(link) @parameter.outer
+(link
+ text: (link_text) @parameter.inner)
+
+(image) @parameter.outer
+(image
+ spec: (image_spec) @parameter.inner)
+
+; =================================================================
+; Paragraph / inline-formatting runs
+; =================================================================
+
+(paragraph) @block.outer
+(paragraph
+ (_)+ @block.inner)
+
+; Inline formatting pairs - useful as fine-grained text objects.
+; The same delimiter character pattern (e.g. `*{` / `}*`) opens and
+; closes each, so .inner is everything between them.
+
+(emphasis) @assignment.outer
+(bold) @assignment.outer
+(italic) @assignment.outer
+(underline) @assignment.outer
+(citation_mark) @assignment.outer
+(superscript) @assignment.outer
+(subscript) @assignment.outer
+(inserted) @assignment.outer
+(strikethrough) @assignment.outer
+(monospace_inline) @assignment.outer
+
+; =================================================================
+; Book index entries
+; =================================================================
+
+(book_index) @attribute.outer
+(book_index
+ (index_content) @attribute.inner)
+
+; =================================================================
+; Header fields
+; =================================================================
+
+(header_field) @assignment.outer
+(header_field
+ value: (header_value) @assignment.inner)
diff --git a/sundry/editor-syntax-etc/vim/syntax/sisu-spine.vim b/sundry/editor-syntax-etc/vim/syntax/sisu-spine.vim
index 2de0095..35a893e 100644
--- a/sundry/editor-syntax-etc/vim/syntax/sisu-spine.vim
+++ b/sundry/editor-syntax-etc/vim/syntax/sisu-spine.vim
@@ -1,12 +1,22 @@
-" SiSU Vim syntax file (sisu-spine)
+" SiSU Vim syntax file (sisu-spine) - Vim 8 fallback (regex)
" SiSU Maintainer: Ralph Amissah <ralph.amissah@gmail.com>
" SiSU Markup: SiSU (sisu-5.6.7)
" sisu-spine Markup: sisu-spine
-" Last Change: 2017-06-22, 2025-02-21
+" Last Change: 2017-06-22, 2025-02-21, 2026-05-09
" URL: <https://git.sisudoc.org/projects/sisudoc-spine/tree/sundry/editor-syntax-etc/vim/syntax/sisu-spine.vim>
" <https://git.sisudoc.org/projects/sisudoc-spine/tree/sundry/editor-syntax-etc/vim/syntax/sisu.vim>
" <https://sisudoc.org/>
"(originally looked at Ruby Vim by Mirko Nasato)
+"
+" Status: This is the regex-based Vim 8 fallback. For Neovim users, the
+" tree-sitter-sisu grammar provides structural highlighting, folding and
+" textobjects with strictly better behaviour on nested markup, multi-line
+" footnotes, block bodies, and segmented headings; see
+" sundry/editor-syntax-etc/nvim/README.md
+" Emacs 29+ users have an equivalent treesit-based mode at
+" sundry/editor-syntax-etc/emacs/sisu-spine-ts-mode.el
+" This file remains the supported path for classic Vim, where tree-sitter
+" is not available without third-party plugins.
if version < 600
syntax clear
diff --git a/sundry/editor-syntax-etc/vim/templates/ssi.tpl b/sundry/editor-syntax-etc/vim/templates/ssi.tpl
new file mode 100644
index 0000000..28e8101
--- /dev/null
+++ b/sundry/editor-syntax-etc/vim/templates/ssi.tpl
@@ -0,0 +1,30 @@
+# SiSU 8.0 insert
+
+title:
+ main: "#___#"
+ sub: "#___#"
+ language: "#___#"
+
+creator:
+ author: "#___#"
+
+date:
+ :published: "YYYY-MM-DD"
+
+rights:
+ copyright: "#___#"
+ license: "#___#"
+
+classify:
+ topic_register: "#___#"
+
+make:
+ breaks: "new=:B; break=1"
+# home_button_text: "#___#"
+# footer: "#___#"
+
+#% -- body ---
+
+:A~ @title @author
+
+1~ #___#
diff --git a/sundry/editor-syntax-etc/vim/templates/ssm.tpl b/sundry/editor-syntax-etc/vim/templates/ssm.tpl
new file mode 100644
index 0000000..579375f
--- /dev/null
+++ b/sundry/editor-syntax-etc/vim/templates/ssm.tpl
@@ -0,0 +1,30 @@
+# SiSU 8.0 master
+
+title:
+ main: "#___#"
+ sub: "#___#"
+ language: "#___#"
+
+creator:
+ author: "#___#"
+
+date:
+ :published: "YYYY-MM-DD"
+
+rights:
+ copyright: "#___#"
+ license: "#___#"
+
+classify:
+ topic_register: "#___#"
+
+make:
+ breaks: "new=:B; break=1"
+# home_button_text: "#___#"
+# footer: "#___#"
+
+#% -- body ---
+
+:A~ @title @author
+
+1~ #___#
diff --git a/sundry/editor-syntax-etc/vim/templates/sst.tpl b/sundry/editor-syntax-etc/vim/templates/sst.tpl
new file mode 100644
index 0000000..069d498
--- /dev/null
+++ b/sundry/editor-syntax-etc/vim/templates/sst.tpl
@@ -0,0 +1,30 @@
+# SiSU 8.0
+
+title:
+ main: "#___#"
+ sub: "#___#"
+ language: "#___#"
+
+creator:
+ author: "#___#"
+
+date:
+ :published: "YYYY-MM-DD"
+
+rights:
+ copyright: "#___#"
+ license: "#___#"
+
+classify:
+ topic_register: "#___#"
+
+make:
+ breaks: "new=:B; break=1"
+# home_button_text: "#___#"
+# footer: "#___#"
+
+#% -- body ---
+
+:A~ @title @author
+
+1~ #___#