;;; klammertext-indent.el --- TAB indentation for Klammertext -*- lexical-binding: t; -*- ;; EXPERIMENTAL. This file is a separate unit, loaded from the init file: ;; ;; (require 'klammertext-indent) ;; ;; Comment that line out to disable indentation entirely; klammertext-mode ;; itself is untouched by this file. ;; ;; The convention (2026-07-26): ;; ;; @ol <- opener at its context's content column ;; Item one <- content: opener column + 2 ;; | Item two <- bar run at the OPENER's column ("| " is two ;; @ol characters, so item text aligns with "Item one") ;; Embedded item one ;; | Embedded item two ;; @ <- close at its opener's column ;; | Item four ;; @ ;; ;; Formal rule: a line indents to offset x (effective depth); a line that ;; BEGINS with a bar run (|, ||, ...) or a closing delimiter (@ or name@) ;; indents one level less, i.e. to its owner's opening column. The bar-run ;; rule is dimension-independent: | (list items), || (table rows) and any ;; longer run all drop to the opener's column. Effective depth counts every ;; enclosing span uniformly -- applications (@), definitions (@@), and ;; system commands (@@@) -- with these exceptions: ;; ;; * Klammers in `klammertext-transparent-klammers' (seeded with ;; "document") contribute no level, so ordinary paragraphs of a document ;; sit at the left margin. ;; * Lines inside a literal klammer's verbatim content (@code ... code@) ;; and inside the argument span of a klammer in ;; `klammertext-code-klammers' (seeded with "eval" -- inline Python is ;; indentation-sensitive!) are NEVER touched: TAB returns `noindent'. ;; Removed regions (#[ ... ]#, everything after ##) are likewise left ;; alone. ;; ;; Indentation happens only on explicit TAB / indent-region; nothing ;; reformats automatically, because whitespace is content in Klammertext. ;; The convention is nevertheless semantically free where it applies: list ;; and table cell content is stripped by the SKS, leading whitespace ;; collapses in the html/tex targets, and a blank line that acquires ;; indentation spaces still separates paragraphs (the SKS paragraph ;; separator is \n *\n ). ;; ;; Known limitation: a raw @ inside a ^'...'^ literal region would confuse ;; the depth scan (the same limitation as the font-lock scanner). ;; ;; SYNC: the shared Python core doc/edit/shared/klammertext_edit.py — the ;; single implementation behind the Sublime, Vim, and VS Code integrations ;; and the language server — carries this file's policy as INDENT_OFFSET / ;; TRANSPARENT_KLAMMERS / CODE_KLAMMERS (an elisp defcustom cannot be read ;; from Python, so this unit remains an independent implementation, held ;; equal by tst/editor_test.sh's byte-equality checks). When you change ;; `klammertext-indent-offset', `klammertext-transparent-klammers' or ;; `klammertext-code-klammers', mirror the change there. ;;; Code: (require 'klammertext-mode) (defgroup klammertext-indent nil "Indentation for Klammertext files." :group 'klammertext) (defcustom klammertext-indent-offset 2 "Number of columns per klammer nesting level." :type 'integer :group 'klammertext-indent) (defcustom klammertext-transparent-klammers '("document") "Klammers whose span contributes no indentation level. The @document klammer is transparent so that the ordinary paragraphs of a document sit at the left margin; a future top-level peer (e.g. @jupyter) would be registered here too." :type '(repeat string) :group 'klammertext-indent) (defcustom klammertext-code-klammers '("eval") "Klammers whose argument span holds code, never reindented. Lines inside such a span answer TAB with `noindent'. @eval is seeded because its content is Python, C++ or shell source -- Python in particular is indentation-sensitive. This mirrors the Klammermachine's own rule that @eval argument spans hold code, not writer text." :type '(repeat string) :group 'klammertext-indent) ;; --- Depth scanner ------------------------------------------------------ (defun klammertext-indent--state-at (pos) "Scan from `point-min' to POS (a line beginning). Return (STACK . OPAQUE): STACK is the list of names of the klammer applications, @@ definitions and @@@ commands open at POS, innermost first; OPAQUE is non-nil when POS lies inside content that indentation must not touch \(removed text, a literal klammer's verbatim span, or a code klammer's argument span). Reuses the mode's classification helpers, so escaped ^@ / ^#, the abbreviated @name-arg form, and #+ #/ #- are all stepped over the same way the font-lock scanner steps over them." (save-excursion (goto-char (point-min)) (let ((stack nil) (opaque nil) (done nil)) (while (and (not done) (re-search-forward "[@#]" pos t)) (let ((hit (1- (point)))) (cond ((klammertext--escaped-p hit)) ; ^@ / ^# : plain text ((eq (char-after hit) ?#) (let ((next (char-after (1+ hit)))) (cond ((eq next ?#) ; ## removes to end of buffer (setq opaque t done t)) ((eq next ?\[) ; #[ ... ]# (nestable) (let ((end (klammertext--block-end (+ hit 2)))) (if (< pos end) (setq opaque t done t) (goto-char end)))) ((memq next '(?+ ?/ ?-))) ; whitespace operators (t (goto-char (line-end-position)))))) ; # to end of line (t ; an @-run (let* ((run-end (klammertext--at-run-end hit)) (len (- run-end hit))) (cond ;; @name / @@name / @@@name : an opener (or, for a literal ;; klammer, a verbatim span to step over). ((klammertext--name-char-p (char-after run-end)) (goto-char run-end) (skip-chars-forward "A-Za-z0-9_") (let ((name (buffer-substring-no-properties run-end (point)))) (cond ((and (= len 1) (member name klammertext-literal-klammers)) ;; Verbatim interior: find the closing NAME@ by name. (if (klammertext--search-literal-close name) (when (< pos (point)) (setq opaque t done t)) (setq opaque t done t))) ; never closed ((and (= len 1) (eq (char-after) ?-))) ; @name-arg : no span (t (push name stack))))) ;; a bare @-run, or the run of a named close: a close. (t (pop stack) (goto-char run-end)))))))) ;; Inside the argument span of a code klammer (e.g. a multi-line @eval)? (unless opaque (let ((s stack)) (while s (when (member (car s) klammertext-code-klammers) (setq opaque t s nil)) (setq s (cdr s))))) (cons stack opaque)))) (defun klammertext-indent--depth (stack) "Number of indentation levels STACK contributes. Transparent klammers contribute none." (let ((d 0)) (dolist (name stack d) (unless (member name klammertext-transparent-klammers) (setq d (1+ d)))))) ;; --- Line classification ------------------------------------------------ (defun klammertext-indent--dedent-line-p () "Non-nil when the current line begins with a token that sits at its owner's opening column: a bar run (|, ||, ...), a bare close run (@, @@, @@@), or a named close (name@, name@@, name@@@). A line beginning with an opener (@name, @@name, @@@name) is content-level." (save-excursion (back-to-indentation) (let ((c (char-after))) (cond ((null c) nil) ((eq c ?|) t) ((eq c ?@) (not (klammertext--name-char-p (char-after (klammertext--at-run-end (point)))))) ((klammertext--name-char-p c) ;; A named close: name chars followed by an @-run (an unescaped @ ;; can only be a delimiter). (skip-chars-forward "A-Za-z0-9_") (eq (char-after) ?@)) (t nil))))) ;; --- The indent function ------------------------------------------------ (defun klammertext-indent--target-column () "Column for the current line, or the symbol `noindent'." (let* ((state (klammertext-indent--state-at (line-beginning-position))) (stack (car state))) (if (cdr state) 'noindent (* klammertext-indent-offset (klammertext-indent--depth (if (klammertext-indent--dedent-line-p) (cdr stack) stack)))))) (defun klammertext-indent-line () "Indent the current line per the Klammertext convention. Content indents to `klammertext-indent-offset' x depth; a line beginning with a bar run or a closing delimiter aligns with its owner's opening column. Lines inside verbatim, code, or removed content are left alone." (interactive) (let ((target (klammertext-indent--target-column))) (if (eq target 'noindent) 'noindent (if (> (current-column) (current-indentation)) (save-excursion (indent-line-to target)) (indent-line-to target))))) ;; --- Wiring ------------------------------------------------------------- (defun klammertext-indent-setup () "Enable Klammertext indentation in the current buffer." (setq-local indent-line-function #'klammertext-indent-line) ;; Klammer indentation columns are small and semantic; never use tabs. (setq-local indent-tabs-mode nil)) (add-hook 'klammertext-mode-hook #'klammertext-indent-setup) ;; Also enable in klammertext-mode buffers already open when this loads. (dolist (buf (buffer-list)) (with-current-buffer buf (when (derived-mode-p 'klammertext-mode) (klammertext-indent-setup)))) (provide 'klammertext-indent) ;;; klammertext-indent.el ends here