diff options
Diffstat (limited to 'org/util_editors.org')
| -rw-r--r-- | org/util_editors.org | 976 |
1 files changed, 964 insertions, 12 deletions
diff --git a/org/util_editors.org b/org/util_editors.org index d07de15..f88e9d1 100644 --- a/org/util_editors.org +++ b/org/util_editors.org @@ -7,13 +7,13 @@ #+COPYRIGHT: Copyright (C) 2015 (continuously updated, current 2026) Ralph Amissah #+LANGUAGE: en #+STARTUP: content hideblocks hidestars noindent entitiespretty -#+PROPERTY: header-args :exports code -#+PROPERTY: header-args+ :noweb yes -#+PROPERTY: header-args+ :results silent -#+PROPERTY: header-args+ :cache no -#+PROPERTY: header-args+ :padline no +#+PROPERTY: header-args+ :eval never-export :exports code +#+PROPERTY: header-args+ :noweb yes :padline no +#+PROPERTY: header-args+ :results silent :cache no #+PROPERTY: header-args+ :mkdirp yes #+OPTIONS: H:3 num:nil toc:t \n:t ::t |:t ^:nil -:t f:t *:t +- magic single double-quote → " ← FIX changes hilighting behavior (occuring + after it) in org document. INVESTIGATE (org-mode CONFIG?) FIND & FIX - [[./doc-reform.org][doc-reform.org]] [[./][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 |
