diff --git a/README.md b/README.md index 54454bf..67cff52 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ are regenerated on each release — patches cannot be merged directly. Report problems (or send patches) to the author; accepted changes are applied to the development tree and appear in a following snapshot. -This snapshot was assembled from development commit `c08eb1bbfa4c`. +This snapshot was assembled from development commit `5d35f256476e`. ## License diff --git a/doc/edit/emacs/README.md b/doc/edit/emacs/README.md index ea49c39..bcc95ae 100644 --- a/doc/edit/emacs/README.md +++ b/doc/edit/emacs/README.md @@ -12,10 +12,13 @@ and load the mode. Add to `~/.emacs.d/init.el`: ```elisp (add-to-list 'load-path "full-pathname-of-the-emacs-directory") (require 'klammertext-mode) +(require 'klammertext-indent) ; optional, experimental: TAB indentation ``` Replace `full-pathname-of-the-emacs-directory` with the full path to the -directory that contains `klammertext-mode.el`. +directory that contains `klammertext-mode.el`. The second require loads the +experimental indentation support (see "Indentation" below); it is a separate +unit — comment the line out to disable indentation entirely. The mode auto-activates for `.kt` and `.k` files. (The `.k` / `.kt` distinction is a filing convention, not a lexical one — the same mode serves both.) You can @@ -153,6 +156,46 @@ opening `@name`. It uses the same matcher as the highlighting. The starting position is pushed to the mark ring, so `C-u C-SPC` jumps back. (Also available as `M-x klammertext-jump-to-match`.) +## Indentation (experimental) + +With `klammertext-indent.el` loaded (the optional require above), **TAB** +indents the current line — and `indent-region` a selection — to reflect the +klammer nesting, two spaces per level: + +``` +@ol + Item one +| Item two + @ol + Embedded item one + | Embedded item two + @ +| Item three +@ +``` + +The rule: a line indents to 2 × depth; a line *beginning* with a bar run +(`|`, `||`, …) or a closing delimiter sits one level less — at its owner's +opening column. So the bars and the close line up under the `@` of the list +they belong to, and `| ` (bar + space) puts item text exactly at the content +column. The bar rule is dimension-independent: `||` table rows drop to the +opener's column the same way. All three `@`-tiers indent uniformly. + +Exceptions, all deliberate: + +- Klammers in `klammertext-transparent-klammers` (default: `document`) + contribute no level, so a document's ordinary paragraphs stay 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` + (default: `eval` — inline Python is indentation-sensitive), are never + touched. Neither are removed regions (`#[ ... ]#`, after `##`). + +Nothing reformats automatically — whitespace is content in Klammertext, so +indentation happens only when you ask for it (TAB, `indent-region`). The +offset is `klammertext-indent-offset` (default 2); all three variables are +customizable in the `klammertext-indent` group. + ## Literal klammers Inside a `literal` argument — for example the body of `@code ... code@` — `#` diff --git a/doc/edit/emacs/klammertext-indent.el b/doc/edit/emacs/klammertext-indent.el new file mode 100644 index 0000000..ce574fb --- /dev/null +++ b/doc/edit/emacs/klammertext-indent.el @@ -0,0 +1,226 @@ +;;; 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 Sublime Text port doc/sublime/Klammertext_indent.py duplicates +;; this file's policy (a Sublime plugin cannot read these defcustoms). When +;; you change `klammertext-indent-offset', `klammertext-transparent-klammers' +;; or `klammertext-code-klammers', mirror the change in that file's +;; INDENT_OFFSET / TRANSPARENT_KLAMMERS / CODE_KLAMMERS. + +;;; 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 (re-search-forward + (concat (regexp-quote name) "@") nil t) + (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 diff --git a/doc/edit/emacs/klammertext-mode.el b/doc/edit/emacs/klammertext-mode.el index 528996f..74149b6 100644 --- a/doc/edit/emacs/klammertext-mode.el +++ b/doc/edit/emacs/klammertext-mode.el @@ -123,11 +123,12 @@ Register one with `klammertext-add-literal-klammer', e.g. in your init file: ;; SYNC: the Sublime Text port in doc/sublime/ duplicates this list statically ;; (a Sublime syntax/plugin cannot read this Emacs defcustom). When you add or -;; remove a literal klammer, mirror it in BOTH: +;; remove a literal klammer, mirror it in ALL of: ;; * LITERAL_KLAMMERS in doc/sublime/Klammertext.py +;; * LITERAL_KLAMMERS in doc/sublime/Klammertext_indent.py ;; * the @NAME literal rule + literal_NAME context in ;; doc/sublime/Klammertext.sublime-syntax -;; All three are currently seeded with just "code". +;; All four are currently seeded with just "code". (defun klammertext-add-literal-klammer (name) "Register NAME as a klammer whose literal content must not be interpreted. diff --git a/doc/edit/sublime/Default.sublime-keymap b/doc/edit/sublime/Default.sublime-keymap index dc39952..e13a656 100644 --- a/doc/edit/sublime/Default.sublime-keymap +++ b/doc/edit/sublime/Default.sublime-keymap @@ -4,8 +4,13 @@ // command) to Ctrl+M — Sublime's own "go to matching bracket" key, repurposed // for klammers, since the built-in cannot match context-dependent @ pairs. // -// The "selector" context confines the binding to Klammertext files, so Ctrl+M -// keeps its normal meaning everywhere else. +// Binds "reindent lines" (the companion Klammertext_indent.py command) to +// Ctrl+Alt+I. Sublime's own Reindent (Edit > Line > Reindent, unbound by +// default) is driven by single-line regex patterns that cannot express +// Klammertext nesting, so reindentation is a plugin command here too. +// +// The "selector" contexts confine the bindings to Klammertext files, so both +// keys keep their normal meaning everywhere else. // // macOS users may prefer "super+m"; change the "keys" value below. This file // (no platform suffix) is loaded on all platforms. @@ -16,5 +21,12 @@ "context": [ { "key": "selector", "operator": "equal", "operand": "text.klammertext" } ] + }, + { + "keys": ["ctrl+alt+i"], + "command": "klammertext_reindent", + "context": [ + { "key": "selector", "operator": "equal", "operand": "text.klammertext" } + ] } ] diff --git a/doc/edit/sublime/Klammertext.py b/doc/edit/sublime/Klammertext.py index 42d6939..490bed4 100644 --- a/doc/edit/sublime/Klammertext.py +++ b/doc/edit/sublime/Klammertext.py @@ -52,13 +52,15 @@ import sublime_plugin # Klammer names whose content is a literal argument (verbatim interior). # -# SYNC: this list is one of three copies that must agree. When you add or -# remove a literal klammer, mirror it in all three: +# SYNC: this list is one of four copies that must agree. When you add or +# remove a literal klammer, mirror it in all four: # * klammertext-literal-klammers in doc/emacs/klammertext-mode.el (the source # of truth; a Sublime syntax/plugin cannot read that Emacs defcustom) # * LITERAL_KLAMMERS here +# * LITERAL_KLAMMERS in Klammertext_indent.py (a deletable unit, so it does +# not import from this file) # * the @NAME literal rule + literal_NAME context in Klammertext.sublime-syntax -# All three are currently seeded with just "code". +# All four are currently seeded with just "code". LITERAL_KLAMMERS = set(["code"]) diff --git a/doc/edit/sublime/Klammertext_in_Sublime_Text.md b/doc/edit/sublime/Klammertext_in_Sublime_Text.md index 5b12da3..0223b4e 100644 --- a/doc/edit/sublime/Klammertext_in_Sublime_Text.md +++ b/doc/edit/sublime/Klammertext_in_Sublime_Text.md @@ -11,7 +11,8 @@ Emacs mode closely; where the two intentionally differ, the file headers say so. |------|---------| | `Klammertext.sublime-syntax` | Syntax highlighting. Colors the text-removal constructs (`#`, `##`, `#[...]#`) and the three `@`-tiers — application `@`, definition `@@`, system `@@@` — each as an opening vs. a close, plus `^`-escapes and verbatim `@code ... code@` spans. | | `Klammertext.py` | Plugin with two features that share one context-sensitive matcher: jump between an opening and its close, and live highlighting of the matching delimiter as the caret moves (mismatched or unbalanced delimiters flag in red). | -| `Default.sublime-keymap` | Binds jump-to-match to **Ctrl+M**, scoped to Klammertext files. | +| `Klammertext_indent.py` | **Experimental.** Reindentation per the Klammertext convention (see below). A separate unit: delete this one file to disable indentation; nothing else is affected. | +| `Default.sublime-keymap` | Binds jump-to-match to **Ctrl+M** and reindent to **Ctrl+Alt+I**, scoped to Klammertext files. | | `Comments.tmPreferences` | Comment toggling: **Ctrl+/** inserts `# ` (line removal), **Ctrl+Shift+/** wraps in `#[ ... ]#` (block removal). | | `Breakers` / `Celeste` / `Mariana` / `Monokai` / `Sixteen` `.sublime-color-scheme` | Color overrides for Sublime's five built-in schemes — one hue system, full intensity on the dark schemes, scaled down on the light ones. Additive: they recolor only the Klammertext delimiters and leave the rest of each scheme unchanged. | | `Klammertext_in_Sublime_Text.md` | This file. | @@ -48,11 +49,48 @@ adaptive `region.*` scopes, which were added in ST4. | caret on a klammer `@` | The matching delimiter boxes automatically; a name mismatch or unbalanced delimiter boxes in red with a status-bar message (equivalent of `show-paren-mode`) | | **Ctrl+/** | Toggle line comment (`#`) | | **Ctrl+Shift+/** | Toggle block comment (`#[ ... ]#`) | +| **Ctrl+Alt+I** | Reindent the selected lines (the current line when there is just a caret) — experimental, see "Indentation" below | Ctrl+M is Sublime's own "go to matching bracket" key, reused here because the built-in cannot match Klammertext's context-dependent `@`. macOS users who prefer `super+m` can change it in `Default.sublime-keymap`. +## Indentation (experimental) + +`Klammertext_indent.py` ports the Emacs mode's indentation +(`doc/emacs/klammertext-indent.el`): **Ctrl+Alt+I** reindents the line(s) +touched by the selection to reflect the klammer nesting, two spaces per level: + +``` +@ol + Item one +| Item two + @ol + Embedded item one + | Embedded item two + @ +| Item three +@ +``` + +The rule: a line indents to 2 × depth; a line *beginning* with a bar run +(`|`, `||`, …) or a closing delimiter sits one level less — at its owner's +opening column, so bars and closes line up under the `@` of the klammer they +belong to. All three `@`-tiers indent uniformly. Exceptions: `@document` +contributes no level (a document's paragraphs stay at the left margin); lines +inside verbatim `@code` content, inside `@eval` argument spans (inline Python +is indentation-sensitive), and inside removed regions are never touched. The +policy lists (`TRANSPARENT_KLAMMERS`, `CODE_KLAMMERS`, `INDENT_OFFSET`) are at +the top of `Klammertext_indent.py`, kept in sync with the Emacs defcustoms. + +Sublime's own Reindent (Edit → Line → Reindent) is driven by single-line +regex patterns that cannot express Klammertext nesting, so this is a plugin +command instead. Nothing reformats automatically (no on-Enter auto-indent): +whitespace is content in Klammertext, so indentation happens only when you +ask for it. To disable the feature, delete `Klammertext_indent.py` and the +Ctrl+Alt+I entry in `Default.sublime-keymap` (or just the keymap entry, to +keep the command available from plugins). + ## Colors Colors are installed automatically for all five of Sublime's built-in schemes. @@ -75,16 +113,17 @@ exact values are in each file's header comment. ## Keeping literal klammers in sync -Klammers whose content is verbatim (`@code ... code@`) are listed in three +Klammers whose content is verbatim (`@code ... code@`) are listed in four places that must agree — a Sublime syntax/plugin cannot read the Emacs defcustom, so the list is duplicated: - `klammertext-literal-klammers` in `doc/emacs/klammertext-mode.el` (the source of truth) - `LITERAL_KLAMMERS` in `Klammertext.py` +- `LITERAL_KLAMMERS` in `Klammertext_indent.py` - the `@code` rule and `literal_code` context in `Klammertext.sublime-syntax` -All three are seeded with just `code`. When you add or remove a literal -klammer, change all three. +All four are seeded with just `code`. When you add or remove a literal +klammer, change all four. ## Not included diff --git a/doc/edit/sublime/Klammertext_indent.py b/doc/edit/sublime/Klammertext_indent.py new file mode 100644 index 0000000..b122781 --- /dev/null +++ b/doc/edit/sublime/Klammertext_indent.py @@ -0,0 +1,276 @@ +# Klammertext_indent.py +# +# EXPERIMENTAL. Reindentation for Klammertext files — the Sublime Text port +# of doc/emacs/klammertext-indent.el. This file is a separate unit: delete it +# (or move it out of the package folder) to disable indentation entirely; the +# rest of the Klammertext package is unaffected. +# +# Command name (for keymaps / the command palette): klammertext_reindent +# Keybinding: Ctrl+Alt+I (in Default.sublime-keymap), scoped to Klammertext +# files. It reindents the line(s) touched by the selection — the current +# line when there is just a caret. Nothing reformats automatically (no +# on-Enter auto-indent), because whitespace is content in Klammertext. +# +# The convention (2026-07-27): +# +# @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 (a bare @-run or +# a named close) 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 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 CODE_KLAMMERS (seeded with +# "eval" -- inline Python is indentation-sensitive!) are NEVER touched. +# Removed regions (#[ ... ]#, everything after ##) are likewise left +# alone. +# +# Known limitation (shared with the Emacs scanner): a raw @ inside a ^'...'^ +# literal region would confuse the depth scan. +# +# SYNC: the policy lists below must agree with the Emacs side: +# * LITERAL_KLAMMERS with klammertext-literal-klammers (also duplicated in +# Klammertext.py and Klammertext.sublime-syntax; all seeded "code") +# * TRANSPARENT_KLAMMERS with klammertext-transparent-klammers ("document") +# * CODE_KLAMMERS with klammertext-code-klammers ("eval") +# * INDENT_OFFSET with klammertext-indent-offset (2) +# The scanning helpers (name_char_p, escaped_p, block_end, at_run_end) are +# duplicated from Klammertext.py so this file stays a deletable unit with no +# import coupling. + +try: + import sublime + import sublime_plugin + _IN_SUBLIME = True +except ImportError: # standalone testing outside Sublime Text + _IN_SUBLIME = False + +INDENT_OFFSET = 2 +LITERAL_KLAMMERS = set(["code"]) +TRANSPARENT_KLAMMERS = set(["document"]) +CODE_KLAMMERS = set(["eval"]) + + +# --- pure helpers (duplicated from Klammertext.py; see SYNC note above) ----- + +def name_char_p(ch): + """True if CH can be part of a klammer name (letter, digit or _).""" + if ch is None: + return False + return (('a' <= ch <= 'z') or ('A' <= ch <= 'Z') + or ('0' <= ch <= '9') or ch == '_') + + +def escaped_p(s, pos): + """True if the char at POS is escaped by an odd run of ^ before it.""" + n = 0 + i = pos - 1 + while i >= 0 and s[i] == '^': + n += 1 + i -= 1 + return (n % 2) == 1 + + +def block_end(s, frm): + """Index just after the ]# that closes a #[ block opened at FROM (the index + just after the opening #[). Counts nested #[ ... ]#; len(s) if unclosed.""" + depth = 1 + i = frm + n = len(s) + while depth > 0: + a = s.find('#[', i) + b = s.find(']#', i) + if a == -1 and b == -1: + return n + if b == -1 or (a != -1 and a < b): + depth += 1 + i = a + 2 + else: + depth -= 1 + i = b + 2 + return i + + +def at_run_end(s, pos): + """Index just after the run of @ that begins at POS.""" + p = pos + n = len(s) + while p < n and s[p] == '@': + p += 1 + return p + + +# --- the depth scanner (port of klammertext-indent--state-at) --------------- + +def state_at(s, pos): + """Scan s[0:POS] (POS a line beginning). Return (stack, opaque): STACK is + the list of names of the klammer applications, @@ definitions and @@@ + commands open at POS, outermost first; OPAQUE is True 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).""" + stack = [] + n = len(s) + i = 0 + while i < pos: + j = i + while j < pos and s[j] != '@' and s[j] != '#': + j += 1 + if j >= pos: + break + hit = j + i = hit + 1 + if escaped_p(s, hit): # ^@ / ^# : plain text + continue + nxt = s[hit + 1] if hit + 1 < n else None + if s[hit] == '#': + if nxt == '#': # ## removes to end of buffer + return (stack, True) + elif nxt == '[': # #[ ... ]# (nestable) + end = block_end(s, hit + 2) + if pos < end: + return (stack, True) + i = end + elif nxt in ('+', '/', '-'): # whitespace operators + pass + else: # # to end of line + eol = s.find('\n', hit) + i = n if eol == -1 else eol + continue + # an @-run + run_end = at_run_end(s, hit) + run_len = run_end - hit + after = s[run_end] if run_end < n else None + if name_char_p(after): + # @name / @@name / @@@name : an opener (or, for a literal + # klammer, a verbatim span to step over). + k = run_end + while k < n and name_char_p(s[k]): + k += 1 + name = s[run_end:k] + i = k + if run_len == 1 and name in LITERAL_KLAMMERS: + # Verbatim interior: find the closing NAME@ by name. + idx = s.find(name + '@', k) + if idx == -1: # never closed + return (stack, True) + close_end = idx + len(name) + 1 + if pos < close_end: + return (stack, True) + i = close_end + elif run_len == 1 and k < n and s[k] == '-': + pass # @name-arg : opens no span + else: + stack.append(name) + else: + # a bare @-run, or the run of a named close: a close. + if stack: + stack.pop() + i = run_end + opaque = any(name in CODE_KLAMMERS for name in stack) + return (stack, opaque) + + +def depth(stack): + """Number of indentation levels STACK contributes. + Transparent klammers contribute none.""" + return sum(1 for name in stack if name not in TRANSPARENT_KLAMMERS) + + +def dedent_line_p(s, bol): + """True when the line starting at BOL 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.""" + n = len(s) + i = bol + while i < n and s[i] in ' \t': + i += 1 + if i >= n: + return False + c = s[i] + if c == '|': + return True + if c == '@': + run_end = at_run_end(s, i) + return not name_char_p(s[run_end] if run_end < n else None) + if name_char_p(c): + # A named close: name chars followed by an @-run (an unescaped @ can + # only be a delimiter). + k = i + while k < n and name_char_p(s[k]): + k += 1 + return k < n and s[k] == '@' + return False + + +def target_column(s, bol): + """Column for the line starting at BOL, or None for lines that must not + be touched (verbatim, code, or removed content).""" + stack, opaque = state_at(s, bol) + if opaque: + return None + if dedent_line_p(s, bol) and stack: + stack = stack[:-1] + return INDENT_OFFSET * depth(stack) + + +def reindent_lines(s, bols): + """Compute the edits that reindent the lines whose beginnings are BOLS. + Return a list of (start, end, replacement) triples over S, in ascending + order, replacing each line's leading whitespace; untouchable lines and + already-correct lines produce no edit. Pure function -- also used by the + standalone tests.""" + n = len(s) + edits = [] + for bol in bols: + tgt = target_column(s, bol) + if tgt is None: + continue + i = bol + while i < n and s[i] in ' \t': + i += 1 + if s[bol:i] != ' ' * tgt: + edits.append((bol, i, ' ' * tgt)) + return edits + + +# --- the command ------------------------------------------------------------ + +if _IN_SUBLIME: + + class KlammertextReindentCommand(sublime_plugin.TextCommand): + """Reindent the line(s) touched by the selection per the Klammertext + convention. Sublime equivalent of TAB in the Emacs mode (which has no + Sublime analogue: TAB there always inserts). Bound to Ctrl+Alt+I.""" + + def run(self, edit): + view = self.view + s = view.substr(sublime.Region(0, view.size())) + bols = [] + seen = set() + for region in view.sel(): + for line in view.lines(region): + if line.a not in seen: + seen.add(line.a) + bols.append(line.a) + # Compute all edits from the original text, then apply from the + # bottom up so earlier offsets stay valid. + for a, b, new in sorted(reindent_lines(s, bols), reverse=True): + view.replace(edit, sublime.Region(a, b), new) + + def is_enabled(self): + return self.view.match_selector(0, "text.klammertext") diff --git a/doc/install/linux_container_install.md b/doc/install/linux_container_install.md index 32447c7..8541fec 100644 --- a/doc/install/linux_container_install.md +++ b/doc/install/linux_container_install.md @@ -111,6 +111,20 @@ The Standard Klammer Set is loaded by default. To load a different klammer set, pass `-k PATH` (the klammer set's `.k` file). To run with only the three primitive klammers (`@read`, `@eval`, `@cond`), use `-k none`. +## Editor support (Emacs, Sublime Text) + +Editing Klammertext is nicer with editor support: syntax highlighting, +delimiter matching, and indentation for Emacs and Sublime Text. It is not +inside the container image — it belongs on your machine, next to your editor. +Download it from either place: + +- — unpacks to `emacs/` and + `sublime/` folders +- the Klammertext source repository, + , directory `doc/edit/` + +Each package's README explains its installation. + ## If something goes wrong - **`Cannot connect to the Docker daemon`** — the Docker service isn't running: diff --git a/doc/install/macos_container_install.md b/doc/install/macos_container_install.md index 9a3b65d..ee1d764 100644 --- a/doc/install/macos_container_install.md +++ b/doc/install/macos_container_install.md @@ -124,10 +124,12 @@ That's it — you're running Klammertext. together, and run `ktext` from that folder. - **Runs natively.** On Apple Silicon, `container` runs the native arm64 image with no Rosetta translation. -- **Fonts.** The default fonts (Crimson Pro, Open Sans, Inconsolata) are built - in, so PDFs work with no internet connection. If you ask for a different font - by name, Klammertext downloads it from Google Fonts the first time, which - needs an internet connection. +- **Fonts.** The default fonts (Crimson Pro, Open Sans, Inconsolata, and + others) are built in, so PDFs work with no internet connection. Font + resolution is entirely offline: asking for a font that isn't installed + lists the available fonts, and additional fonts are installed from font + files you already have with `kdesc --font install ` (`kdesc --font + help` explains). - **Updating later.** When a new version is announced, run `klammertext-update` in Terminal. - **If you also build Klammertext from source on this Mac.** Most people don't — @@ -143,6 +145,20 @@ That's it — you're running Klammertext. run `container system stop`; start it again with `container system start` next time. +## Editor support (Emacs, Sublime Text) + +Editing Klammertext is nicer with editor support: syntax highlighting, +delimiter matching, and indentation for Emacs and Sublime Text. It is not +inside the container image — it belongs on your Mac, next to your editor. +Download it from either place: + +- — unpacks to `emacs/` and + `sublime/` folders +- the Klammertext source repository, + , directory `doc/edit/` + +Each package's README explains its installation. + ## If something goes wrong - **`command not found: ktext`** — you didn't open a new Terminal window after