Initial commit: Klammertext source distribution
Curated source subset assembled by klammertext-dev's doc/make_dist.sh: the Klammermachine (mac), the Standard Klammer Set (sks), the commands (com), editor plugins and install guides (doc), a test subset (tst), and lib/bin placeholders. Builds with 'make -C com'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
183
doc/edit/emacs/README.md
Normal file
183
doc/edit/emacs/README.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# Emacs mode for Klammertext
|
||||
|
||||
`klammertext-mode.el` is an Emacs major mode for editing Klammertext files. It
|
||||
helps Klammertext authors see the structure of klammer application through
|
||||
syntax highlighting.
|
||||
|
||||
## Install
|
||||
|
||||
Put the `emacs` directory somewhere on your system, then tell Emacs where it is
|
||||
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)
|
||||
```
|
||||
|
||||
Replace `full-pathname-of-the-emacs-directory` with the full path to the
|
||||
directory that contains `klammertext-mode.el`.
|
||||
|
||||
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
|
||||
also switch to it manually with `M-x klammertext-mode`.
|
||||
|
||||
## What it highlights
|
||||
|
||||
**Text-removal ("ignore") constructs** — in two independently chosen colors,
|
||||
one for the *removed content*, one for the *marker characters*:
|
||||
|
||||
| Construct | Meaning |
|
||||
|--------------|----------------------------------|
|
||||
| `#` ... | remove to end of line |
|
||||
| `##` ... | remove to end of buffer |
|
||||
| `#[ ... ]#` | remove enclosed text (nestable) |
|
||||
|
||||
**Klammer applications** — in two independent colors: one for *opening* a
|
||||
klammer, one for *closing* it. The `@` and the name of an opening are one
|
||||
syntactic unit and share the opening color; the close (named or bare) gets the
|
||||
closing color, so you always have visual confirmation of where a klammer ends:
|
||||
|
||||
| Form | Color | Meaning |
|
||||
|-----------|---------|----------------------------------|
|
||||
| `@name` | opening | opening `@` + name (one unit) |
|
||||
| `name@` | closing | named closing delimiter |
|
||||
| `@` | closing | bare closing delimiter |
|
||||
|
||||
In the abbreviated form `@name-arg1-arg2` (equivalent to `@name arg1 | arg2 @`)
|
||||
only the name is colored — the name ends at the first hyphen, and the
|
||||
hyphen-separated arguments stay plain, just as `arg1`/`arg2` would be plain in
|
||||
the long form.
|
||||
|
||||
Because a named closing carries the closing color across its name too, long
|
||||
klammers that name their closing delimiter (`@document ... document@`) stand
|
||||
out — which is exactly where naming the closing delimiter earns its keep
|
||||
(accurate unmatched-delimiter error messages). Short bodies (`@i word @`) are
|
||||
conventionally left with a bare `@` to keep the text uncluttered.
|
||||
|
||||
**Klammer definitions** (`@@`) are highlighted the same way, in their own pair
|
||||
of colors — so definitions read as distinct from applications at a glance:
|
||||
|
||||
| Form | Color | Meaning |
|
||||
|------------|---------|-----------------------------------|
|
||||
| `@@name` | opening | opening `@@` + name (one unit) |
|
||||
| `name@@` | closing | named closing delimiter |
|
||||
| `@@` | closing | bare closing delimiter |
|
||||
|
||||
The name ends at the first non-name character, so a target suffix like
|
||||
`@@name.html` colors only `@@name` and leaves `.html` plain. A definition's
|
||||
body (between `@@name` and the closing `@@`) is highlighted like ordinary
|
||||
Klammertext — e.g. an `@i … @` inside it shows as a normal application.
|
||||
|
||||
**System/target commands** (`@@@`) — `@@@target`, `@@@argtype`, `@@@state` — get
|
||||
a third pair of colors, so the three `@`-levels (application, definition, system)
|
||||
are visually distinct:
|
||||
|
||||
| Form | Color | Meaning |
|
||||
|------------|---------|-----------------------------------|
|
||||
| `@@@name` | opening | opening `@@@` + name (one unit) |
|
||||
| `name@@@` | closing | named closing delimiter |
|
||||
| `@@@` | closing | bare closing delimiter |
|
||||
|
||||
`@@@` commands do not nest, so each delimiter is colored independently; their
|
||||
bodies (`| … |` option lists) are highlighted as ordinary Klammertext.
|
||||
|
||||
## The eight faces
|
||||
|
||||
All eight faces are defined by the `defconst klammertext--palette` at the
|
||||
beginning of `klammertext-mode.el`.
|
||||
|
||||
| Face | Applies to |
|
||||
|-----------------------------------|-------------------------------------|
|
||||
| `klammertext-ignored-face` | removed content |
|
||||
| `klammertext-marker-face` | `#`, `##`, `#[`, `]#` |
|
||||
| `klammertext-klammer-open-face` | an application opening `@name` |
|
||||
| `klammertext-klammer-close-face` | an application close `name@` or `@` |
|
||||
| `klammertext-def-open-face` | a definition opening `@@name` |
|
||||
| `klammertext-def-close-face` | a definition close `name@@` or `@@` |
|
||||
| `klammertext-system-open-face` | a system opening `@@@name` |
|
||||
| `klammertext-system-close-face` | a system close `name@@@` or `@@@` |
|
||||
|
||||
To experiment with a color (evaluate in `*scratch*`, or add to your init):
|
||||
|
||||
```elisp
|
||||
(set-face-foreground 'klammertext-marker-face "cyan")
|
||||
(set-face-foreground 'klammertext-system-close-face "chocolate4")
|
||||
```
|
||||
|
||||
or `M-x customize-face RET klammertext-marker-face RET`.
|
||||
|
||||
## Matching delimiters (show-paren)
|
||||
|
||||
With `show-paren-mode` on (the default in Emacs 28+), placing point on a klammer
|
||||
**application** delimiter highlights its partner, in both directions: on an
|
||||
opening `@name` it highlights the closing `@`/`name@`, and on a close it
|
||||
highlights the opening `@name`. Nesting is respected — in `@a @b x @ @`, the
|
||||
outer `@a` matches the last `@`, not the first.
|
||||
|
||||
Matching covers applications only (`@`), not `@@`/`@@@`, since that is where
|
||||
paired delimiters matter most. The matcher steps over `@@`/`@@@`, removed text,
|
||||
other literal spans and escaped `^@`; the abbreviated `@name-arg` form has no
|
||||
closing delimiter, so nothing is highlighted on it.
|
||||
|
||||
**Literal klammers** (those in `klammertext-literal-klammers`, e.g. `@code`) are
|
||||
closed with the full `NAME@` form because their content is verbatim. These are
|
||||
matched *by name* — `@code` ↔ `code@` — with the content treated as opaque, so a
|
||||
stray `@` inside (`@code x @ y code@`) doesn't confuse the match, in either
|
||||
direction. Register any klammer that declares a `literal` argument with
|
||||
`(klammertext-add-literal-klammer "name")` in your init file so both its
|
||||
highlighting and its delimiter matching work.
|
||||
|
||||
If a **named** close disagrees with its opening — e.g. `@doc … foo@` (should be
|
||||
`doc@`) — the mismatched delimiter is shown in **bright red** (bold), and a
|
||||
message describing the mismatch appears in the minibuffer, e.g.
|
||||
|
||||
> `Klammertext: closing foo@ does not match opening @doc`
|
||||
|
||||
An unbalanced delimiter (an opening with no close, or vice versa) is flagged the
|
||||
same way. This turns the naming convention into a live check: name a long
|
||||
klammer's closing delimiter and a typo'd or unbalanced name lights up
|
||||
immediately. The red comes from `klammertext-mismatch-face`, which is remapped
|
||||
over `show-paren-mismatch` **only in Klammertext buffers** (your global
|
||||
`show-paren-mismatch` face is left untouched); customize it to taste.
|
||||
|
||||
This is wired in automatically (`show-paren-data-function`); you only need
|
||||
`show-paren-mode` enabled. It relies on nothing in the syntax table — Klammertext
|
||||
delimiters can't be expressed there — so it does not interfere with other
|
||||
`@`/`#` characters.
|
||||
|
||||
### Jumping between matches
|
||||
|
||||
`klammertext-jump-to-match`, bound to **`C-c C-j`**, moves point to the matching
|
||||
delimiter: from an opening `@name` to its close, and from a close back to its
|
||||
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`.)
|
||||
|
||||
## Literal klammers
|
||||
|
||||
Inside a `literal` argument — for example the body of `@code ... code@` — `#`
|
||||
and `@` are literal text, not Klammertext syntax. The mode highlights the
|
||||
opening `@code` and closing `code@` but leaves the interior as normal text,
|
||||
for any klammer registered in `klammertext-literal-klammers`. `@code` is
|
||||
registered by default.
|
||||
|
||||
If you define your own klammer with a `literal` parameter (a relatively
|
||||
advanced action — see the `literal` argument type in the project
|
||||
documentation), register it in your init file:
|
||||
|
||||
```elisp
|
||||
(klammertext-add-literal-klammer "myverbatim")
|
||||
```
|
||||
|
||||
Because the list is consulted at fontification time, registering a klammer
|
||||
while a buffer is already open takes effect after `M-x font-lock-update` (or
|
||||
re-visiting the file).
|
||||
|
||||
## Known limitations (deliberate, for now)
|
||||
|
||||
- Unescaped `@` is always treated as a delimiter (as the Klammermachine does),
|
||||
so an `@` in prose that is *not* meant as a klammer — e.g. an email address
|
||||
written `foo@bar` instead of `foo^@bar` — will be highlighted. This reflects
|
||||
what the machine actually sees.
|
||||
- Very large multiline blocks or literal spans edited far from their opening
|
||||
may occasionally need `M-x font-lock-update` to re-highlight correctly.
|
||||
680
doc/edit/emacs/klammertext-mode.el
Normal file
680
doc/edit/emacs/klammertext-mode.el
Normal file
@@ -0,0 +1,680 @@
|
||||
;;; klammertext-mode.el --- Major mode for Klammertext files -*- lexical-binding: t; -*-
|
||||
|
||||
;; An Emacs major mode for editing Klammertext files. It highlights:
|
||||
;;
|
||||
;; Text-removal ("ignore") constructs:
|
||||
;; # ... remove to end of line
|
||||
;; ## ... remove to end of buffer
|
||||
;; #[ ... ]# remove enclosed text (nestable)
|
||||
;;
|
||||
;; Klammer applications:
|
||||
;; @name opening delimiter + name
|
||||
;; name@ named closing delimiter
|
||||
;; @ bare closing delimiter
|
||||
;;
|
||||
;; Klammer definitions (@@), analogous to applications:
|
||||
;; @@name opening delimiter + name
|
||||
;; name@@ named closing delimiter
|
||||
;; @@ bare closing delimiter
|
||||
;;
|
||||
;; System/target commands (@@@), analogous again:
|
||||
;; @@@name opening delimiter + name
|
||||
;; name@@@ named closing delimiter
|
||||
;; @@@ bare closing delimiter
|
||||
;;
|
||||
;; Independent faces carry each pair of colors: removed content vs. the removal
|
||||
;; marker characters; and, for applications (@name), definitions (@@name) and
|
||||
;; system commands (@@@name), each construct's opening vs. its close.
|
||||
;;
|
||||
;; The same mode serves both .kt (content) and .k (klammer definition) files:
|
||||
;; the .k/.kt split is a filing convention, not a lexical difference.
|
||||
;;
|
||||
;; Everything is driven by ONE left-to-right scanner (`klammertext--fontify').
|
||||
;; That is what makes the interactions correct: inside removed text and inside
|
||||
;; literal-klammer spans the scanner jumps over the content, so it is never
|
||||
;; re-interpreted as klammers or comments.
|
||||
;;
|
||||
;; It also matches klammer APPLICATION delimiters for `show-paren-mode' (both
|
||||
;; directions, with mismatched-name flagging); see the show-paren section below.
|
||||
;;
|
||||
;; Not handled yet (deliberately):
|
||||
;; * The bodies of definitions (@@) and system commands (@@@) are highlighted
|
||||
;; like ordinary Klammertext (an @i ... @ inside shows as a normal
|
||||
;; application), rather than being treated specially.
|
||||
;; * Inside a `literal' argument (e.g. @code ... code@) neither # nor @ is a
|
||||
;; marker. The scanner highlights the opening @code and closing code@ but
|
||||
;; leaves the interior as normal text, for any klammer registered in
|
||||
;; `klammertext-literal-klammers'.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(defgroup klammertext nil
|
||||
"Editing Klammertext files."
|
||||
:group 'text)
|
||||
|
||||
;; --- Faces: colors from a central palette table -------------------------
|
||||
;;
|
||||
;; All highlighting colors live in one table, `klammertext--palette', so they
|
||||
;; can be tuned in a single place. Each face carries a DARK-background value
|
||||
;; and a LIGHT-background value; Emacs picks automatically from the frame or
|
||||
;; terminal background (there is no "dark mode" toggle to set).
|
||||
;;
|
||||
;; The DARK column is a systematic scheme (developed for the Sublime Text port):
|
||||
;; three tier hues — application blue, definition green, system orange — each
|
||||
;; opening bright and its close the same hue at 0.80 intensity.
|
||||
;;
|
||||
;; The LIGHT column is a hybrid tuned by eye: the three delimiter opens are the
|
||||
;; original Emacs colors (RoyalBlue2 / green4 / orange3), each close = 0.60 x its
|
||||
;; open (from the open name's X11 RGB #436eee/#008b00/#cd8500); the marker and
|
||||
;; mismatch light values are the systematic ones; the ignored (gray) light value
|
||||
;; is the systematic gray reduced by 0.90 (#9a9a9a -> #8b8b8b).
|
||||
|
||||
(defconst klammertext--palette
|
||||
;; (face dark light description [extra-attrs])
|
||||
'((klammertext-ignored-face "#8a8272" "#8b8b8b" "removed (ignored) content")
|
||||
(klammertext-marker-face "#ff6b6b" "#994040" "removal markers # ## #[ ]#")
|
||||
(klammertext-klammer-open-face "#89ddff" "RoyalBlue2" "application opening @name")
|
||||
(klammertext-klammer-close-face "#6eb1cc" "#28428f" "application close name@ or bare @")
|
||||
(klammertext-def-open-face "#c3e88d" "green4" "definition opening @@name")
|
||||
(klammertext-def-close-face "#9cba71" "#005300" "definition close name@@ or bare @@")
|
||||
(klammertext-system-open-face "#ffab70" "orange3" "system opening @@@name")
|
||||
(klammertext-system-close-face "#cc895a" "#7b5000" "system close name@@@ or bare @@@")
|
||||
(klammertext-mismatch-face "#ff5555" "#c02020" "mismatched/unbalanced delimiter"
|
||||
(:weight bold)))
|
||||
"Klammertext face colors: (FACE DARK LIGHT DESCRIPTION [EXTRA-ATTRS]).
|
||||
DARK is the foreground on dark backgrounds, LIGHT on light backgrounds; each
|
||||
face is generated with both. EXTRA-ATTRS, if present, is a plist merged into
|
||||
both grounds. See the notes above the table.")
|
||||
|
||||
;; Generate the faces from the table. Using `custom-declare-face' (what
|
||||
;; `defface' expands to) keeps each face customizable via M-x customize-face.
|
||||
(dolist (entry klammertext--palette)
|
||||
(let ((face (nth 0 entry))
|
||||
(dark (nth 1 entry))
|
||||
(light (nth 2 entry))
|
||||
(desc (nth 3 entry))
|
||||
(extra (nth 4 entry)))
|
||||
(custom-declare-face
|
||||
face
|
||||
`((((background dark)) :foreground ,dark ,@extra)
|
||||
(((background light)) :foreground ,light ,@extra))
|
||||
(format "Klammertext highlighting for %s.\nColor is set from the `klammertext--palette' table." desc)
|
||||
:group 'klammertext)))
|
||||
|
||||
;; --- Klammers whose literal content must not be interpreted -------------
|
||||
|
||||
(defcustom klammertext-literal-klammers nil
|
||||
"Names of klammers whose content is a `literal' argument.
|
||||
Such a klammer must be closed with the full NAME@ form (e.g. @code ... code@),
|
||||
because its content is verbatim. This list is consulted in two places:
|
||||
|
||||
* Font-lock leaves the verbatim interior as normal text (#, @, etc. inside
|
||||
are not interpreted).
|
||||
* Delimiter matching (`show-paren-mode' and `klammertext-jump-to-match')
|
||||
pairs the opening @NAME with its closing NAME@ *by name* rather than by
|
||||
depth counting, so a literal close is matched even though its content may
|
||||
contain unbalanced @ characters.
|
||||
|
||||
Any klammer that declares a `literal' argument should be registered here.
|
||||
Register one with `klammertext-add-literal-klammer', e.g. in your init file:
|
||||
(klammertext-add-literal-klammer \"mycode\")"
|
||||
:type '(repeat string)
|
||||
:group 'klammertext)
|
||||
|
||||
;; 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:
|
||||
;; * LITERAL_KLAMMERS in doc/sublime/Klammertext.py
|
||||
;; * the @NAME literal rule + literal_NAME context in
|
||||
;; doc/sublime/Klammertext.sublime-syntax
|
||||
;; All three are currently seeded with just "code".
|
||||
|
||||
(defun klammertext-add-literal-klammer (name)
|
||||
"Register NAME as a klammer whose literal content must not be interpreted.
|
||||
NAME is the klammer name without the leading @ (e.g. \"code\")."
|
||||
(add-to-list 'klammertext-literal-klammers name))
|
||||
|
||||
;; Seed the list through the same entry point future users will use.
|
||||
(klammertext-add-literal-klammer "code")
|
||||
|
||||
;; --- Helpers -----------------------------------------------------------
|
||||
|
||||
(defun klammertext--escaped-p (pos)
|
||||
"Non-nil if the character at POS is escaped by an odd run of ^ before it.
|
||||
In Klammertext `^#' and `^@' are literal, so such a character is not a
|
||||
marker or a delimiter."
|
||||
(let ((n 0) (i (1- pos)))
|
||||
(while (and (>= i (point-min)) (eq (char-after i) ?^))
|
||||
(setq n (1+ n) i (1- i)))
|
||||
(= (mod n 2) 1)))
|
||||
|
||||
(defun klammertext--name-char-p (ch)
|
||||
"Non-nil if CH can be part of a klammer name (letter, digit or _).
|
||||
A hyphen is NOT a name character: in the abbreviated form
|
||||
@name-arg1-arg2 the hyphen separates the name from its arguments, so a
|
||||
klammer name ends at the first hyphen."
|
||||
(and ch (or (and (>= ch ?a) (<= ch ?z))
|
||||
(and (>= ch ?A) (<= ch ?Z))
|
||||
(and (>= ch ?0) (<= ch ?9))
|
||||
(eq ch ?_))))
|
||||
|
||||
(defun klammertext--block-end (from)
|
||||
"Return the position just after the ]# that closes a #[ block.
|
||||
FROM is the position just after the opening #[. Counts nested #[ ... ]#
|
||||
pairs; returns `point-max' if the block is never closed."
|
||||
(goto-char from)
|
||||
(let ((depth 1))
|
||||
(while (and (> depth 0)
|
||||
(re-search-forward "#\\[\\|]#" nil t))
|
||||
(if (string= (match-string 0) "#[")
|
||||
(setq depth (1+ depth))
|
||||
(setq depth (1- depth))))
|
||||
(if (> depth 0) (point-max) (point))))
|
||||
|
||||
(defun klammertext--set-match (wb we &rest groups)
|
||||
"Set match data covering WB..WE with up to nine GROUPS.
|
||||
Each group is a cons (BEG . END), or nil for an absent group (whose
|
||||
highlight spec must use LAXMATCH)."
|
||||
(let ((md (list wb we)))
|
||||
(dotimes (_ 9)
|
||||
(let ((g (pop groups)))
|
||||
(setq md (append md (if g (list (car g) (cdr g)) (list nil nil))))))
|
||||
(set-match-data md)))
|
||||
|
||||
;; --- Token emitters (called by the scanner) ----------------------------
|
||||
;; Each returns non-nil when it has emitted a highlight token (and set the
|
||||
;; match data + moved point past it), or nil to let the scanner keep going.
|
||||
;; Groups: 1 removal-marker 2 removed-content 3 removal-close-marker
|
||||
;; 4 app-open (@name) 5 app-close (name@ or bare @)
|
||||
;; 6 def-open (@@name) 7 def-close (name@@ or bare @@)
|
||||
;; 8 sys-open (@@@name) 9 sys-close (name@@@ or bare @@@)
|
||||
|
||||
(defun klammertext--emit-removal (pos _limit)
|
||||
"POS is at a #. Point is at POS+1 on entry."
|
||||
(let ((next (char-after (1+ pos))))
|
||||
(cond
|
||||
;; ## ... end of buffer
|
||||
((eq next ?#)
|
||||
(klammertext--set-match pos (point-max)
|
||||
(cons pos (+ pos 2))
|
||||
(cons (+ pos 2) (point-max))
|
||||
nil nil nil)
|
||||
(put-text-property pos (point-max) 'font-lock-multiline t)
|
||||
(goto-char (point-max))
|
||||
t)
|
||||
;; #[ ... ]# (nestable)
|
||||
((eq next ?\[)
|
||||
(let* ((end (klammertext--block-end (+ pos 2)))
|
||||
(close (if (and (>= end (+ pos 4))
|
||||
(eq (char-before end) ?#)
|
||||
(eq (char-before (1- end)) ?\]))
|
||||
(- end 2) end)))
|
||||
(klammertext--set-match pos end
|
||||
(cons pos (+ pos 2))
|
||||
(cons (+ pos 2) close)
|
||||
(cons close end)
|
||||
nil nil)
|
||||
(put-text-property pos end 'font-lock-multiline t)
|
||||
(goto-char end)
|
||||
t))
|
||||
;; #+ #/ #- are whitespace operators, NOT removals: keep scanning.
|
||||
((memq next '(?+ ?/ ?-))
|
||||
nil)
|
||||
;; # ... end of line
|
||||
(t
|
||||
(let ((eol (line-end-position)))
|
||||
(klammertext--set-match pos eol
|
||||
(cons pos (1+ pos))
|
||||
(cons (1+ pos) eol)
|
||||
nil nil nil)
|
||||
(goto-char eol)
|
||||
t)))))
|
||||
|
||||
(defun klammertext--emit-klammer (pos _limit)
|
||||
"POS is at an @. Point is at POS+1 on entry.
|
||||
Dispatch by the length of the @-run at POS: a single @ is a klammer
|
||||
APPLICATION (@name / name@ / @); @@ is a klammer DEFINITION (@@name /
|
||||
name@@ / @@); @@@ is a system/target command (@@@name / name@@@ / @@@)."
|
||||
(let ((before (and (> pos (point-min)) (char-before pos))))
|
||||
(cond
|
||||
;; Mid-run @ (previous char is @): the run's first @ drives everything,
|
||||
;; so skip this one (e.g. the @ after an escaped ^@).
|
||||
((eq before ?@)
|
||||
(goto-char (1+ pos))
|
||||
nil)
|
||||
;; @@@ (or longer): system/target command (@@@target, @@@argtype, ...).
|
||||
((and (eq (char-after (1+ pos)) ?@)
|
||||
(eq (char-after (+ pos 2)) ?@))
|
||||
(klammertext--emit-system pos before))
|
||||
;; @@ : klammer definition delimiter
|
||||
((eq (char-after (1+ pos)) ?@)
|
||||
(klammertext--emit-def pos before))
|
||||
;; single @ : klammer application
|
||||
(t
|
||||
(klammertext--emit-application pos before)))))
|
||||
|
||||
(defun klammertext--emit-application (pos before)
|
||||
"Emit a single-@ klammer-application token at POS (groups 4 open / 5 close).
|
||||
BEFORE is the character before POS."
|
||||
(let ((after (char-after (1+ pos))))
|
||||
(cond
|
||||
;; @name : opening application (the @ and name are one unit). The name
|
||||
;; ends at the first hyphen; any -arg1-arg2 abbreviation stays uncolored.
|
||||
((klammertext--name-char-p after)
|
||||
(goto-char (1+ pos))
|
||||
(skip-chars-forward "A-Za-z0-9_")
|
||||
(let* ((name-end (point))
|
||||
(name (buffer-substring-no-properties (1+ pos) name-end)))
|
||||
;; For a literal klammer, first locate the closing NAME@ and move the
|
||||
;; scanner to its start (skipping the verbatim interior); the closing
|
||||
;; is highlighted on the next scanner call. The search must happen
|
||||
;; BEFORE `klammertext--set-match', because `re-search-forward'
|
||||
;; clobbers the match data.
|
||||
(if (member name klammertext-literal-klammers)
|
||||
(let ((close (concat (regexp-quote name) "@")))
|
||||
(if (re-search-forward close nil t)
|
||||
(let ((close-end (point)))
|
||||
(put-text-property pos close-end 'font-lock-multiline t)
|
||||
(goto-char (- close-end (length name) 1)))
|
||||
(put-text-property pos (point-max) 'font-lock-multiline t)
|
||||
(goto-char (point-max))))
|
||||
(goto-char name-end))
|
||||
;; Set the match data for the opening LAST, so it survives to the
|
||||
;; highlight step.
|
||||
(klammertext--set-match pos name-end
|
||||
nil nil nil
|
||||
(cons pos name-end) ; 4: @name opening
|
||||
nil)
|
||||
t))
|
||||
;; A @ preceded by name characters is either a named close NAME@, or the
|
||||
;; bare close of a compact no-argument application @name@. They differ by
|
||||
;; what precedes the name run: an @ there means the name belongs to the
|
||||
;; opening (@name@), so this @ is a bare close and only it is coloured;
|
||||
;; otherwise the whole NAME@ is the closing token.
|
||||
((klammertext--name-char-p before)
|
||||
(let ((name-start (save-excursion
|
||||
(goto-char pos)
|
||||
(skip-chars-backward "A-Za-z0-9_")
|
||||
(point))))
|
||||
(if (and (> name-start (point-min))
|
||||
(eq (char-before name-start) ?@))
|
||||
(klammertext--set-match pos (1+ pos) ; @name@ -> bare @
|
||||
nil nil nil nil
|
||||
(cons pos (1+ pos)))
|
||||
(klammertext--set-match name-start (1+ pos) ; NAME@ named close
|
||||
nil nil nil nil
|
||||
(cons name-start (1+ pos))))
|
||||
(goto-char (1+ pos))
|
||||
t))
|
||||
;; bare @ : unnamed closing delimiter
|
||||
(t
|
||||
(klammertext--set-match pos (1+ pos)
|
||||
nil nil nil
|
||||
nil
|
||||
(cons pos (1+ pos))) ; 5: bare @ close
|
||||
(goto-char (1+ pos))
|
||||
t))))
|
||||
|
||||
(defun klammertext--emit-def (pos before)
|
||||
"Emit a @@ klammer-definition delimiter token at POS (groups 6 open / 7 close).
|
||||
POS and POS+1 are both @. BEFORE is the character before POS. Mirrors
|
||||
`klammertext--emit-application', with @@ in place of @."
|
||||
(let ((after (char-after (+ pos 2)))) ; char right after the @@
|
||||
(cond
|
||||
;; @@name : opening definition (the @@ and name are one unit). The name
|
||||
;; ends at the first non-name char (a space, or the .target suffix).
|
||||
((klammertext--name-char-p after)
|
||||
(goto-char (+ pos 2))
|
||||
(skip-chars-forward "A-Za-z0-9_")
|
||||
(let ((name-end (point)))
|
||||
(klammertext--set-match pos name-end
|
||||
nil nil nil nil nil
|
||||
(cons pos name-end) ; 6: @@name opening
|
||||
nil)
|
||||
(goto-char name-end)
|
||||
t))
|
||||
;; name@@ (named close) or the bare close of a compact no-body @@name@@.
|
||||
;; As with applications, an @ before the name run means the name belongs
|
||||
;; to the opening, so only the @@ is the closing token.
|
||||
((klammertext--name-char-p before)
|
||||
(let ((name-start (save-excursion
|
||||
(goto-char pos)
|
||||
(skip-chars-backward "A-Za-z0-9_")
|
||||
(point))))
|
||||
(if (and (> name-start (point-min))
|
||||
(eq (char-before name-start) ?@))
|
||||
(klammertext--set-match pos (+ pos 2) ; @@name@@ -> bare @@
|
||||
nil nil nil nil nil nil
|
||||
(cons pos (+ pos 2)))
|
||||
(klammertext--set-match name-start (+ pos 2) ; NAME@@ named close
|
||||
nil nil nil nil nil nil
|
||||
(cons name-start (+ pos 2))))
|
||||
(goto-char (+ pos 2))
|
||||
t))
|
||||
;; bare @@ : unnamed closing delimiter
|
||||
(t
|
||||
(klammertext--set-match pos (+ pos 2)
|
||||
nil nil nil nil nil nil
|
||||
(cons pos (+ pos 2))) ; 7: bare @@ close
|
||||
(goto-char (+ pos 2))
|
||||
t))))
|
||||
|
||||
(defun klammertext--emit-system (pos before)
|
||||
"Emit a @@@ system/target delimiter token at POS (groups 8 open / 9 close).
|
||||
POS, POS+1 and POS+2 are all @. BEFORE is the character before POS. These
|
||||
commands (@@@target, @@@argtype, @@@state) do not nest, so each delimiter is
|
||||
coloured independently, mirroring `klammertext--emit-def' with @@@ for @@."
|
||||
(let ((after (char-after (+ pos 3)))) ; char right after the @@@
|
||||
(cond
|
||||
;; @@@name : opening command (the @@@ and name are one unit).
|
||||
((klammertext--name-char-p after)
|
||||
(goto-char (+ pos 3))
|
||||
(skip-chars-forward "A-Za-z0-9_")
|
||||
(let ((name-end (point)))
|
||||
(klammertext--set-match pos name-end
|
||||
nil nil nil nil nil nil nil
|
||||
(cons pos name-end) ; 8: @@@name opening
|
||||
nil)
|
||||
(goto-char name-end)
|
||||
t))
|
||||
;; name@@@ (named close) or the bare close of a compact @@@name@@@.
|
||||
((klammertext--name-char-p before)
|
||||
(let ((name-start (save-excursion
|
||||
(goto-char pos)
|
||||
(skip-chars-backward "A-Za-z0-9_")
|
||||
(point))))
|
||||
(if (and (> name-start (point-min))
|
||||
(eq (char-before name-start) ?@))
|
||||
(klammertext--set-match pos (+ pos 3) ; @@@name@@@ -> bare @@@
|
||||
nil nil nil nil nil nil nil nil
|
||||
(cons pos (+ pos 3)))
|
||||
(klammertext--set-match name-start (+ pos 3) ; NAME@@@ named close
|
||||
nil nil nil nil nil nil nil nil
|
||||
(cons name-start (+ pos 3))))
|
||||
(goto-char (+ pos 3))
|
||||
t))
|
||||
;; bare @@@ : unnamed closing delimiter
|
||||
(t
|
||||
(klammertext--set-match pos (+ pos 3)
|
||||
nil nil nil nil nil nil nil nil
|
||||
(cons pos (+ pos 3))) ; 9: bare @@@ close
|
||||
(goto-char (+ pos 3))
|
||||
t))))
|
||||
|
||||
;; --- The single scanning matcher ---------------------------------------
|
||||
|
||||
(defun klammertext--fontify (limit)
|
||||
"Font-lock matcher: emit the next Klammertext token up to LIMIT.
|
||||
Removed and literal-klammer regions are jumped over, so their interiors
|
||||
are never re-interpreted."
|
||||
(let ((result nil))
|
||||
(while (and (not result)
|
||||
(re-search-forward "[#@]" limit t))
|
||||
(let* ((pos (1- (point)))
|
||||
(ch (char-after pos)))
|
||||
(setq result
|
||||
(cond
|
||||
((klammertext--escaped-p pos) nil) ; ^# or ^@
|
||||
((eq ch ?#) (klammertext--emit-removal pos limit))
|
||||
(t (klammertext--emit-klammer pos limit))))))
|
||||
result))
|
||||
|
||||
(defvar klammertext-font-lock-keywords
|
||||
'((klammertext--fontify
|
||||
(1 'klammertext-marker-face t t)
|
||||
(2 'klammertext-ignored-face t t)
|
||||
(3 'klammertext-marker-face t t)
|
||||
(4 'klammertext-klammer-open-face t t)
|
||||
(5 'klammertext-klammer-close-face t t)
|
||||
(6 'klammertext-def-open-face t t)
|
||||
(7 'klammertext-def-close-face t t)
|
||||
(8 'klammertext-system-open-face t t)
|
||||
(9 'klammertext-system-close-face t t)))
|
||||
"Font-lock keywords for `klammertext-mode'.")
|
||||
|
||||
;; --- show-paren support (klammer applications only) --------------------
|
||||
;;
|
||||
;; show-paren cannot use the syntax table for Klammertext (the same @ is both
|
||||
;; open and close, delimiters are multi-character, and open/close is decided by
|
||||
;; context), so matching is driven by `show-paren-data-function'. Only single-@
|
||||
;; APPLICATION delimiters are matched: @name <-> its closing @ or name@. The
|
||||
;; matcher steps over @@/@@@ runs, removed text, other literal spans and escaped
|
||||
;; ^@; the abbreviated @name-arg form opens no span. A LITERAL klammer (one in
|
||||
;; `klammertext-literal-klammers', e.g. @code) is matched by name — @code <->
|
||||
;; code@ — with its verbatim content opaque, since a depth scan would miscount
|
||||
;; unbalanced @ inside it. A named close name@ whose name disagrees with its
|
||||
;; opening @name is reported as a mismatch.
|
||||
|
||||
(defun klammertext--at-run-end (pos)
|
||||
"Return the position just after the run of @ that begins at POS."
|
||||
(let ((p pos)) (while (eq (char-after p) ?@) (setq p (1+ p))) p))
|
||||
|
||||
(defun klammertext--next-app-delim (limit)
|
||||
"From point, find the next single-@ application delimiter before LIMIT.
|
||||
Step over @@/@@@ runs, removed text, literal spans, escaped ^@, and the
|
||||
abbreviated @name-arg form (which opens no span). Move point past the
|
||||
delimiter (or skipped region) and return (POS . KIND) with KIND `open or
|
||||
`close, or nil when none is found."
|
||||
(catch 'found
|
||||
(while (re-search-forward "[@#]" limit t)
|
||||
(let ((hit (1- (point))))
|
||||
(cond
|
||||
((klammertext--escaped-p hit)) ; ^@ / ^# : keep going
|
||||
((eq (char-after hit) ?#) ; removal: step over it
|
||||
(let ((next (char-after (1+ hit))))
|
||||
(goto-char (cond ((eq next ?#) (point-max))
|
||||
((eq next ?\[) (klammertext--block-end (+ hit 2)))
|
||||
((memq next '(?+ ?/ ?-)) (1+ hit))
|
||||
(t (line-end-position))))))
|
||||
((eq (char-after (1+ hit)) ?@) ; @@ / @@@ : step over run
|
||||
(goto-char (klammertext--at-run-end hit)))
|
||||
((klammertext--name-char-p (char-after (1+ hit))) ; @name : opening?
|
||||
(goto-char (1+ hit))
|
||||
(skip-chars-forward "A-Za-z0-9_")
|
||||
(let ((name (buffer-substring-no-properties (1+ hit) (point))))
|
||||
(cond
|
||||
((member name klammertext-literal-klammers) ; literal span: skip
|
||||
(let ((close (concat (regexp-quote name) "@")))
|
||||
(unless (re-search-forward close nil t) (goto-char (point-max)))))
|
||||
((eq (char-after) ?-)) ; @name-arg : no span
|
||||
(t (throw 'found (cons hit 'open))))))
|
||||
(t ; name@ / bare @ : closing
|
||||
(goto-char (1+ hit))
|
||||
(throw 'found (cons hit 'close))))))
|
||||
nil))
|
||||
|
||||
(defun klammertext--match-forward (open-pos)
|
||||
"OPEN-POS is the @ of an opening application. Return the matching close @
|
||||
position, or nil if unbalanced."
|
||||
(save-excursion
|
||||
(goto-char (1+ open-pos))
|
||||
(skip-chars-forward "A-Za-z0-9_") ; past the opening name
|
||||
(let ((depth 1) (result nil) (go t))
|
||||
(while (and go (> depth 0))
|
||||
(let ((d (klammertext--next-app-delim nil)))
|
||||
(if (null d)
|
||||
(setq go nil)
|
||||
(if (eq (cdr d) 'open)
|
||||
(setq depth (1+ depth))
|
||||
(setq depth (1- depth))
|
||||
(when (= depth 0) (setq result (car d)))))))
|
||||
result)))
|
||||
|
||||
(defun klammertext--match-backward (close-pos)
|
||||
"CLOSE-POS is the @ of a closing application. Return the matching open @
|
||||
position, or nil if unbalanced. Scans forward from `point-min' with a stack."
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(let ((stack nil) (result nil) (go t))
|
||||
(while go
|
||||
(let ((d (klammertext--next-app-delim (1+ close-pos))))
|
||||
(cond
|
||||
((null d) (setq go nil))
|
||||
((eq (cdr d) 'open) (push (car d) stack))
|
||||
(t (let ((open (pop stack)))
|
||||
(when (= (car d) close-pos)
|
||||
(setq result open go nil)))))))
|
||||
result)))
|
||||
|
||||
(defun klammertext--app-delim-info (pos)
|
||||
"If the char at POS is a single-@ application delimiter, return (POS . KIND)
|
||||
with KIND `open or `close; else nil. The abbreviated @name-arg form (which
|
||||
opens no span) returns nil."
|
||||
(when (and (eq (char-after pos) ?@)
|
||||
(not (eq (char-before pos) ?@))
|
||||
(not (eq (char-after (1+ pos)) ?@))
|
||||
(not (klammertext--escaped-p pos)))
|
||||
(if (klammertext--name-char-p (char-after (1+ pos)))
|
||||
(let ((name-end (save-excursion (goto-char (1+ pos))
|
||||
(skip-chars-forward "A-Za-z0-9_")
|
||||
(point))))
|
||||
(unless (eq (char-after name-end) ?-)
|
||||
(cons pos 'open)))
|
||||
(cons pos 'close))))
|
||||
|
||||
(defun klammertext--open-name (open-pos)
|
||||
"Name of the opening @name at OPEN-POS."
|
||||
(save-excursion (goto-char (1+ open-pos))
|
||||
(buffer-substring-no-properties
|
||||
(point) (progn (skip-chars-forward "A-Za-z0-9_") (point)))))
|
||||
|
||||
(defun klammertext--close-name (close-pos)
|
||||
"Name of a named close NAME@ at CLOSE-POS, or nil for a bare @ (incl. @name@)."
|
||||
(save-excursion
|
||||
(goto-char close-pos)
|
||||
(let ((ns (progn (skip-chars-backward "A-Za-z0-9_") (point))))
|
||||
(when (and (< ns close-pos)
|
||||
(not (eq (char-before ns) ?@)))
|
||||
(buffer-substring-no-properties ns close-pos)))))
|
||||
|
||||
(defun klammertext--paren-mismatch (open-pos close-pos)
|
||||
"Non-nil if OPEN-POS/CLOSE-POS is unbalanced, or the named close disagrees
|
||||
with the opening name."
|
||||
(or (null open-pos) (null close-pos)
|
||||
(let ((cname (klammertext--close-name close-pos)))
|
||||
(and cname (not (string= cname (klammertext--open-name open-pos)))))))
|
||||
|
||||
(defun klammertext--literal-delim-name (pos kind)
|
||||
"If the application delimiter at POS (KIND `open or `close) belongs to a
|
||||
literal klammer (one in `klammertext-literal-klammers'), return its name;
|
||||
else nil. A literal klammer must be closed with the full NAME@ form because
|
||||
its content is verbatim, so its @NAME open and NAME@ close are matched by
|
||||
name, not by depth counting."
|
||||
(let ((name (if (eq kind 'open)
|
||||
(klammertext--open-name pos)
|
||||
(klammertext--close-name pos))))
|
||||
(and name (member name klammertext-literal-klammers) name)))
|
||||
|
||||
(defun klammertext--literal-match-forward (open-pos name)
|
||||
"Return the @ of the NAME@ that closes the literal @NAME at OPEN-POS, or nil.
|
||||
The verbatim content is opaque, so we search for the literal close string."
|
||||
(save-excursion
|
||||
(goto-char (+ open-pos 1 (length name)))
|
||||
(when (search-forward (concat name "@") nil t)
|
||||
(1- (point)))))
|
||||
|
||||
(defun klammertext--literal-match-backward (close-pos name)
|
||||
"Return the @ of the @NAME that opens the literal NAME@ whose @ is at
|
||||
CLOSE-POS, or nil. Literal spans do not nest, so the nearest preceding real
|
||||
@NAME is the opener."
|
||||
(save-excursion
|
||||
(goto-char close-pos)
|
||||
(let ((open-str (concat "@" name)) (result nil))
|
||||
(while (and (not result) (search-backward open-str nil t))
|
||||
(let ((op (point)))
|
||||
(unless (or (eq (char-before op) ?@) ; @@NAME = definition
|
||||
(klammertext--escaped-p op))
|
||||
(setq result op))))
|
||||
result)))
|
||||
|
||||
(defun klammertext--app-match (pos kind)
|
||||
"Return the matching application delimiter for the delimiter at POS of KIND
|
||||
\(`open or `close), or nil.
|
||||
A literal klammer (in `klammertext-literal-klammers') matches by name
|
||||
(@NAME <-> NAME@) with its content opaque; other klammers match by depth."
|
||||
(let ((lit (klammertext--literal-delim-name pos kind)))
|
||||
(cond
|
||||
((and lit (eq kind 'open)) (klammertext--literal-match-forward pos lit))
|
||||
((and lit (eq kind 'close)) (klammertext--literal-match-backward pos lit))
|
||||
((eq kind 'open) (klammertext--match-forward pos))
|
||||
(t (klammertext--match-backward pos)))))
|
||||
|
||||
(defun klammertext--report-mismatch (open-pos close-pos)
|
||||
"Show a minibuffer message describing a klammer application mismatch.
|
||||
Either position may be nil (an unbalanced delimiter)."
|
||||
(message "%s"
|
||||
(cond
|
||||
((null close-pos)
|
||||
(format "Klammertext: opening @%s has no matching close"
|
||||
(klammertext--open-name open-pos)))
|
||||
((null open-pos)
|
||||
"Klammertext: closing delimiter has no matching open")
|
||||
(t
|
||||
(format "Klammertext: closing %s@ does not match opening @%s"
|
||||
(or (klammertext--close-name close-pos) "?")
|
||||
(klammertext--open-name open-pos))))))
|
||||
|
||||
(defun klammertext--show-paren-data ()
|
||||
"`show-paren-data-function' for klammer applications, both directions.
|
||||
Returns (HERE-BEG HERE-END THERE-BEG THERE-END MISMATCH) or nil, and reports
|
||||
any mismatch in the minibuffer."
|
||||
(let* ((p (point))
|
||||
(info (or (klammertext--app-delim-info p)
|
||||
(and (> p (point-min))
|
||||
(klammertext--app-delim-info (1- p))))))
|
||||
(when info
|
||||
(let* ((dpos (car info)) (kind (cdr info))
|
||||
(match (klammertext--app-match dpos kind))
|
||||
(open (if (eq kind 'open) dpos match))
|
||||
(close (if (eq kind 'open) match dpos))
|
||||
(mism (klammertext--paren-mismatch open close)))
|
||||
(when mism (klammertext--report-mismatch open close))
|
||||
(list dpos (1+ dpos) match (and match (1+ match)) mism)))))
|
||||
|
||||
;; --- Interactive: jump to the matching application delimiter -----------
|
||||
|
||||
(defun klammertext-jump-to-match ()
|
||||
"Jump to the matching klammer application delimiter.
|
||||
On an opening @name, move to its closing @ or name@; on a close, move to the
|
||||
opening @name. Uses the same matcher as `show-paren-mode'. The starting
|
||||
position is pushed to the mark ring, so \\`C-u C-SPC' jumps back."
|
||||
(interactive)
|
||||
(let* ((p (point))
|
||||
(info (or (klammertext--app-delim-info p)
|
||||
(and (> p (point-min))
|
||||
(klammertext--app-delim-info (1- p))))))
|
||||
(unless info
|
||||
(user-error "Point is not on a klammer application delimiter (@)"))
|
||||
(let* ((dpos (car info)) (kind (cdr info))
|
||||
(match (klammertext--app-match dpos kind)))
|
||||
(unless match
|
||||
(user-error "No matching delimiter for this %s klammer"
|
||||
(if (eq kind 'open) "opening" "closing")))
|
||||
(push-mark nil t)
|
||||
(goto-char match))))
|
||||
|
||||
;; --- The mode ----------------------------------------------------------
|
||||
|
||||
;;;###autoload
|
||||
(define-derived-mode klammertext-mode text-mode "Klammertext"
|
||||
"Major mode for editing Klammertext files."
|
||||
(setq-local font-lock-multiline t)
|
||||
(setq-local font-lock-defaults '(klammertext-font-lock-keywords))
|
||||
;; Match klammer application delimiters with `show-paren-mode' (which must be
|
||||
;; enabled separately; it is on by default in Emacs 28+).
|
||||
(setq-local show-paren-data-function #'klammertext--show-paren-data)
|
||||
;; Show a mismatched delimiter in bright red rather than the default purple
|
||||
;; `show-paren-mismatch', but only in Klammertext buffers.
|
||||
(setq-local face-remapping-alist
|
||||
(cons '(show-paren-mismatch klammertext-mismatch-face)
|
||||
face-remapping-alist)))
|
||||
|
||||
(define-key klammertext-mode-map (kbd "C-c C-j") #'klammertext-jump-to-match)
|
||||
|
||||
;;;###autoload
|
||||
(add-to-list 'auto-mode-alist '("\\.kt\\'" . klammertext-mode))
|
||||
;;;###autoload
|
||||
(add-to-list 'auto-mode-alist '("\\.k\\'" . klammertext-mode))
|
||||
|
||||
(provide 'klammertext-mode)
|
||||
;;; klammertext-mode.el ends here
|
||||
60
doc/edit/sublime/Breakers.sublime-color-scheme
Normal file
60
doc/edit/sublime/Breakers.sublime-color-scheme
Normal file
@@ -0,0 +1,60 @@
|
||||
// Klammertext colors for the "Breakers" scheme (light ground).
|
||||
// One hue system across all schemes: application = blue, definition =
|
||||
// green, system = orange; each opens bright and its close is 80%% of the
|
||||
// open (a klammer "begins bright and gets dark"). Shown at full intensity
|
||||
// on dark grounds, at 60%% on light grounds for contrast. Delimiters are
|
||||
// forced to normal style. Merged onto Breakers by filename; recolors only
|
||||
// .klammertext scopes. (The highlighting was first developed as an Emacs
|
||||
// major mode; see Klammertext_in_Sublime_Text.md.)
|
||||
//
|
||||
// #999999 removed text (Breakers's comment grey)
|
||||
// #994040 removal markers
|
||||
// #528599 @name open blue
|
||||
// #426a7a name@ close darker blue
|
||||
// #758b55 @@name open green
|
||||
// #5e7044 name@@ close darker green
|
||||
// #996743 @@@name open orange
|
||||
// #7a5236 name@@@ close darker orange
|
||||
{
|
||||
"name": "Breakers",
|
||||
"rules": [
|
||||
{
|
||||
"scope": "comment.line.klammertext, comment.block.klammertext",
|
||||
"foreground": "#999999"
|
||||
},
|
||||
{
|
||||
"scope": "punctuation.definition.comment.klammertext",
|
||||
"foreground": "#994040"
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.function.begin.klammertext",
|
||||
"foreground": "#528599",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.function.end.klammertext",
|
||||
"foreground": "#426a7a",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "storage.type.begin.klammertext",
|
||||
"foreground": "#758b55",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "storage.type.end.klammertext",
|
||||
"foreground": "#5e7044",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.begin.klammertext",
|
||||
"foreground": "#996743",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.end.klammertext",
|
||||
"foreground": "#7a5236",
|
||||
"font_style": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
60
doc/edit/sublime/Celeste.sublime-color-scheme
Normal file
60
doc/edit/sublime/Celeste.sublime-color-scheme
Normal file
@@ -0,0 +1,60 @@
|
||||
// Klammertext colors for the "Celeste" scheme (light ground).
|
||||
// One hue system across all schemes: application = blue, definition =
|
||||
// green, system = orange; each opens bright and its close is 80%% of the
|
||||
// open (a klammer "begins bright and gets dark"). Shown at full intensity
|
||||
// on dark grounds, at 60%% on light grounds for contrast. Delimiters are
|
||||
// forced to normal style. Merged onto Celeste by filename; recolors only
|
||||
// .klammertext scopes. (The highlighting was first developed as an Emacs
|
||||
// major mode; see Klammertext_in_Sublime_Text.md.)
|
||||
//
|
||||
// #9a9a9a removed text (Celeste's comment grey)
|
||||
// #994040 removal markers
|
||||
// #528599 @name open blue
|
||||
// #426a7a name@ close darker blue
|
||||
// #758b55 @@name open green
|
||||
// #5e7044 name@@ close darker green
|
||||
// #996743 @@@name open orange
|
||||
// #7a5236 name@@@ close darker orange
|
||||
{
|
||||
"name": "Celeste",
|
||||
"rules": [
|
||||
{
|
||||
"scope": "comment.line.klammertext, comment.block.klammertext",
|
||||
"foreground": "#9a9a9a"
|
||||
},
|
||||
{
|
||||
"scope": "punctuation.definition.comment.klammertext",
|
||||
"foreground": "#994040"
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.function.begin.klammertext",
|
||||
"foreground": "#528599",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.function.end.klammertext",
|
||||
"foreground": "#426a7a",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "storage.type.begin.klammertext",
|
||||
"foreground": "#758b55",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "storage.type.end.klammertext",
|
||||
"foreground": "#5e7044",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.begin.klammertext",
|
||||
"foreground": "#996743",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.end.klammertext",
|
||||
"foreground": "#7a5236",
|
||||
"font_style": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
44
doc/edit/sublime/Comments.tmPreferences
Normal file
44
doc/edit/sublime/Comments.tmPreferences
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!--
|
||||
Comment toggling for Klammertext. Companion to Klammertext.sublime-syntax.
|
||||
|
||||
Ctrl+/ (toggle_comment) -> line removal: "# "
|
||||
Ctrl+Shift+/ (toggle_comment block:true) -> block removal: "#[ ... ]#"
|
||||
|
||||
These map onto Klammertext's own text-removal syntax:
|
||||
# removes to end of line (the line comment)
|
||||
#[ ... ]# removes enclosed text, nestable (the block comment)
|
||||
-->
|
||||
<key>name</key>
|
||||
<string>Comments</string>
|
||||
<key>scope</key>
|
||||
<string>text.klammertext</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>shellVariables</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>TM_COMMENT_START</string>
|
||||
<key>value</key>
|
||||
<string># </string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>TM_COMMENT_START_2</string>
|
||||
<key>value</key>
|
||||
<string>#[</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>TM_COMMENT_END_2</string>
|
||||
<key>value</key>
|
||||
<string>]#</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
20
doc/edit/sublime/Default.sublime-keymap
Normal file
20
doc/edit/sublime/Default.sublime-keymap
Normal file
@@ -0,0 +1,20 @@
|
||||
// Klammertext key bindings.
|
||||
//
|
||||
// Binds "jump to matching klammer delimiter" (the companion Klammertext.py
|
||||
// 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.
|
||||
//
|
||||
// macOS users may prefer "super+m"; change the "keys" value below. This file
|
||||
// (no platform suffix) is loaded on all platforms.
|
||||
[
|
||||
{
|
||||
"keys": ["ctrl+m"],
|
||||
"command": "klammertext_jump_to_match",
|
||||
"context": [
|
||||
{ "key": "selector", "operator": "equal", "operand": "text.klammertext" }
|
||||
]
|
||||
}
|
||||
]
|
||||
471
doc/edit/sublime/Klammertext.py
Normal file
471
doc/edit/sublime/Klammertext.py
Normal file
@@ -0,0 +1,471 @@
|
||||
# Klammertext.py
|
||||
#
|
||||
# Sublime Text plugin for klammer APPLICATION (@) delimiters. Two features,
|
||||
# both ports of doc/emacs/klammertext-mode.el, both reusing one matcher:
|
||||
#
|
||||
# 1. Jump between an opening and its close — the Sublime equivalent of the
|
||||
# Emacs mode's `klammertext-jump-to-match' (bound C-c C-j). Command name
|
||||
# klammertext_jump_to_match; keybinding in Default.sublime-keymap.
|
||||
#
|
||||
# 2. Live highlighting of the matching delimiter as the caret sits on one —
|
||||
# the equivalent of the Emacs mode's show-paren support. Implemented as a
|
||||
# ViewEventListener (see KlammertextMatchHighlighter at the bottom); no
|
||||
# language server is involved. A mismatched named close or an unbalanced
|
||||
# delimiter is highlighted in red with a status-bar message, mirroring the
|
||||
# Emacs mode's klammertext-mismatch-face + minibuffer report.
|
||||
#
|
||||
# This is the companion to Klammertext.sublime-syntax. The syntax file only
|
||||
# colors tokens; a tokenizer cannot match context-dependent delimiters, so the
|
||||
# jump is implemented here as a TextCommand. The keybinding lives in the
|
||||
# companion Default.sublime-keymap.
|
||||
#
|
||||
# Command name (for keymaps / the command palette): klammertext_jump_to_match
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# What it does (a direct port of the elisp matcher):
|
||||
# * On an opening @name, move to its closing @ or name@.
|
||||
# * On a close (bare @ or name@), move to the opening @name.
|
||||
# * Triggers when the caret is ON the @ or immediately AFTER it (the same
|
||||
# on-or-just-after rule the Emacs command uses).
|
||||
# * Only single-@ APPLICATION delimiters match. @@/@@@ runs, removed text
|
||||
# (#, ##, #[...]#), escaped ^@, and literal-klammer spans (@code ... code@)
|
||||
# are stepped over, exactly as in the Emacs mode. The abbreviated
|
||||
# @name-arg form opens no span.
|
||||
# * Works at every caret when there are multiple selections.
|
||||
#
|
||||
# Literal klammers (identical to C-c C-j): a @code ... code@ span is opaque.
|
||||
# The general depth scan still steps over such a span WHOLESALE when matching
|
||||
# some OTHER klammer, so verbatim @ inside it never miscount. A literal
|
||||
# klammer's OWN delimiters are matched BY NAME rather than by depth (see
|
||||
# app_match): @code jumps to the next code@, and code@ to the nearest preceding
|
||||
# @code — correct even when the content holds unbalanced @, e.g. @code x @ y
|
||||
# code@. LITERAL_KLAMMERS lists these names; keep it in sync with the '@code'
|
||||
# handling in Klammertext.sublime-syntax.
|
||||
#
|
||||
# LITERAL_KLAMMERS must stay in sync with the literal klammers recognized in
|
||||
# Klammertext.sublime-syntax (seeded there as @code). The Emacs mode keeps this
|
||||
# list in the `klammertext-literal-klammers' defcustom; a plugin has no access
|
||||
# to it, so it is duplicated here.
|
||||
|
||||
import sublime
|
||||
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:
|
||||
# * 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
|
||||
# * the @NAME literal rule + literal_NAME context in Klammertext.sublime-syntax
|
||||
# All three are currently seeded with just "code".
|
||||
LITERAL_KLAMMERS = set(["code"])
|
||||
|
||||
|
||||
# --- pure helpers (operate on the whole buffer as a string) ----------------
|
||||
|
||||
def name_char_p(ch):
|
||||
"""True if CH can be part of a klammer name (letter, digit or _).
|
||||
A hyphen is NOT a name char: @name-arg1 ends the name at the first hyphen."""
|
||||
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.
|
||||
In Klammertext ^# and ^@ are literal, so such a char is not a delimiter."""
|
||||
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
|
||||
|
||||
|
||||
def next_app_delim(s, i, limit):
|
||||
"""From index I, find the next single-@ application delimiter before LIMIT.
|
||||
Step over @@/@@@ runs, removed text, literal spans, escaped ^@, and the
|
||||
abbreviated @name-arg form. Return (pos, kind, next_i) with kind 'open' or
|
||||
'close' and next_i the index to resume from, or None when none is found."""
|
||||
n = len(s)
|
||||
if limit is None:
|
||||
limit = n
|
||||
while i < limit:
|
||||
# find next @ or # at or after i (emacs re-search-forward "[@#]")
|
||||
j = i
|
||||
while j < limit and s[j] != '@' and s[j] != '#':
|
||||
j += 1
|
||||
if j >= limit:
|
||||
return None
|
||||
hit = j
|
||||
i = hit + 1 # default: advance past the hit
|
||||
if escaped_p(s, hit): # ^@ / ^# : keep going
|
||||
continue
|
||||
nxt = s[hit + 1] if hit + 1 < n else None
|
||||
if s[hit] == '#': # removal: step over it
|
||||
if nxt == '#':
|
||||
i = n
|
||||
elif nxt == '[':
|
||||
i = block_end(s, hit + 2)
|
||||
elif nxt in ('+', '/', '-'):
|
||||
i = hit + 1
|
||||
else: # to end of line
|
||||
eol = s.find('\n', hit)
|
||||
i = n if eol == -1 else eol
|
||||
continue
|
||||
# s[hit] == '@'
|
||||
if nxt == '@': # @@ / @@@ : step over the run
|
||||
i = at_run_end(s, hit)
|
||||
continue
|
||||
if name_char_p(nxt): # @name : opening?
|
||||
k = hit + 1
|
||||
while k < n and name_char_p(s[k]):
|
||||
k += 1
|
||||
name = s[hit + 1:k]
|
||||
after = s[k] if k < n else None
|
||||
if name in LITERAL_KLAMMERS: # literal span: skip to its close
|
||||
close = name + '@'
|
||||
idx = s.find(close, k)
|
||||
i = n if idx == -1 else idx + len(close)
|
||||
continue
|
||||
elif after == '-': # @name-arg : opens no span
|
||||
i = k
|
||||
continue
|
||||
else:
|
||||
return (hit, 'open', k)
|
||||
else: # name@ / bare @ : closing
|
||||
return (hit, 'close', hit + 1)
|
||||
return None
|
||||
|
||||
|
||||
def match_forward(s, open_pos):
|
||||
"""OPEN_POS is the @ of an opening application. Return the matching close @
|
||||
index, or None if unbalanced."""
|
||||
n = len(s)
|
||||
i = open_pos + 1
|
||||
while i < n and name_char_p(s[i]): # past the opening name
|
||||
i += 1
|
||||
depth = 1
|
||||
while depth > 0:
|
||||
d = next_app_delim(s, i, None)
|
||||
if d is None:
|
||||
return None
|
||||
pos, kind, nxt = d
|
||||
i = nxt
|
||||
if kind == 'open':
|
||||
depth += 1
|
||||
else:
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return pos
|
||||
return None
|
||||
|
||||
|
||||
def match_backward(s, close_pos):
|
||||
"""CLOSE_POS is the @ of a closing application. Return the matching open @
|
||||
index, or None if unbalanced. Scans forward from 0 with a stack."""
|
||||
stack = []
|
||||
i = 0
|
||||
limit = close_pos + 1
|
||||
while True:
|
||||
d = next_app_delim(s, i, limit)
|
||||
if d is None:
|
||||
return None
|
||||
pos, kind, nxt = d
|
||||
i = nxt
|
||||
if kind == 'open':
|
||||
stack.append(pos)
|
||||
else:
|
||||
open_pos = stack.pop() if stack else None
|
||||
if pos == close_pos:
|
||||
return open_pos
|
||||
|
||||
|
||||
def app_delim_info(s, pos):
|
||||
"""If the char at POS is a single-@ application delimiter, return
|
||||
(pos, kind) with kind 'open' or 'close'; else None. The abbreviated
|
||||
@name-arg form (which opens no span) returns None."""
|
||||
n = len(s)
|
||||
if not (0 <= pos < n):
|
||||
return None
|
||||
if s[pos] != '@':
|
||||
return None
|
||||
if pos > 0 and s[pos - 1] == '@':
|
||||
return None
|
||||
if pos + 1 < n and s[pos + 1] == '@':
|
||||
return None
|
||||
if escaped_p(s, pos):
|
||||
return None
|
||||
nxt = s[pos + 1] if pos + 1 < n else None
|
||||
if name_char_p(nxt):
|
||||
k = pos + 1
|
||||
while k < n and name_char_p(s[k]):
|
||||
k += 1
|
||||
after = s[k] if k < n else None
|
||||
if after == '-':
|
||||
return None
|
||||
return (pos, 'open')
|
||||
return (pos, 'close')
|
||||
|
||||
|
||||
# --- name / mismatch helpers (for the live highlighter) --------------------
|
||||
|
||||
def _name_forward(s, pos):
|
||||
"""Index just past the run of name chars starting at POS."""
|
||||
n = len(s)
|
||||
k = pos
|
||||
while k < n and name_char_p(s[k]):
|
||||
k += 1
|
||||
return k
|
||||
|
||||
|
||||
def open_name(s, open_pos):
|
||||
"""Name of the opening @name whose @ is at OPEN_POS."""
|
||||
return s[open_pos + 1:_name_forward(s, open_pos + 1)]
|
||||
|
||||
|
||||
def close_name(s, close_pos):
|
||||
"""Name of a named close NAME@ whose @ is at CLOSE_POS, or None for a bare @
|
||||
(including the compact @name@ form, whose name belongs to the opening)."""
|
||||
ns = close_pos
|
||||
while ns > 0 and name_char_p(s[ns - 1]):
|
||||
ns -= 1
|
||||
if ns < close_pos and (ns == 0 or s[ns - 1] != '@'):
|
||||
return s[ns:close_pos]
|
||||
return None
|
||||
|
||||
|
||||
def paren_mismatch(s, open_pos, close_pos):
|
||||
"""True if the pair is unbalanced (either side None) or the named close
|
||||
disagrees with the opening name."""
|
||||
if open_pos is None or close_pos is None:
|
||||
return True
|
||||
cname = close_name(s, close_pos)
|
||||
return cname is not None and cname != open_name(s, open_pos)
|
||||
|
||||
|
||||
def token_region(s, pos, kind):
|
||||
"""(start, end) of the whole delimiter token whose @ is at POS.
|
||||
Opening: @ plus its name. Named close: the name plus @. Bare @: just @."""
|
||||
if kind == 'open':
|
||||
return (pos, _name_forward(s, pos + 1))
|
||||
ns = pos
|
||||
while ns > 0 and name_char_p(s[ns - 1]):
|
||||
ns -= 1
|
||||
if ns < pos and (ns == 0 or s[ns - 1] != '@'):
|
||||
return (ns, pos + 1) # named close NAME@
|
||||
return (pos, pos + 1) # bare @ (or @name@)
|
||||
|
||||
|
||||
# --- matching dispatch: literal klammers by name, others by depth ----------
|
||||
|
||||
def literal_delim_name(s, pos, kind):
|
||||
"""If the application delimiter at POS (kind 'open'/'close') belongs to a
|
||||
literal klammer (name in LITERAL_KLAMMERS), return its name; else None.
|
||||
A literal klammer's @NAME open and NAME@ close are matched by name, not by
|
||||
depth counting, because its content is verbatim."""
|
||||
name = open_name(s, pos) if kind == 'open' else close_name(s, pos)
|
||||
if name and name in LITERAL_KLAMMERS:
|
||||
return name
|
||||
return None
|
||||
|
||||
|
||||
def literal_match_forward(s, open_pos, name):
|
||||
"""Index of the @ of the NAME@ that closes the literal @NAME at OPEN_POS, or
|
||||
None. The content is opaque, so search for the literal close string."""
|
||||
start = open_pos + 1 + len(name)
|
||||
idx = s.find(name + '@', start)
|
||||
return idx + len(name) if idx != -1 else None
|
||||
|
||||
|
||||
def literal_match_backward(s, close_pos, name):
|
||||
"""Index of the @ of the @NAME that opens the literal NAME@ whose @ is at
|
||||
CLOSE_POS, or None. Literal spans do not nest, so the nearest preceding
|
||||
real @NAME is the opener (not @@NAME, and not escaped)."""
|
||||
open_str = '@' + name
|
||||
end = close_pos
|
||||
while True:
|
||||
idx = s.rfind(open_str, 0, end)
|
||||
if idx == -1:
|
||||
return None
|
||||
before = s[idx - 1] if idx > 0 else None
|
||||
if before != '@' and not escaped_p(s, idx):
|
||||
return idx
|
||||
end = idx
|
||||
|
||||
|
||||
def app_match(s, pos, kind):
|
||||
"""Matching application delimiter for the delimiter at POS of KIND
|
||||
('open'/'close'), or None. A literal klammer matches by name (@NAME <->
|
||||
NAME@) with content opaque; other klammers match by depth."""
|
||||
lit = literal_delim_name(s, pos, kind)
|
||||
if lit is not None:
|
||||
return (literal_match_forward(s, pos, lit) if kind == 'open'
|
||||
else literal_match_backward(s, pos, lit))
|
||||
return match_forward(s, pos) if kind == 'open' else match_backward(s, pos)
|
||||
|
||||
|
||||
# --- the command -----------------------------------------------------------
|
||||
|
||||
class KlammertextJumpToMatchCommand(sublime_plugin.TextCommand):
|
||||
"""Jump between a klammer application's opening and closing delimiter.
|
||||
Sublime equivalent of the Emacs mode's C-c C-j."""
|
||||
|
||||
def run(self, edit):
|
||||
view = self.view
|
||||
s = view.substr(sublime.Region(0, view.size()))
|
||||
new_regions = []
|
||||
moved = False
|
||||
message = None
|
||||
|
||||
for region in view.sel():
|
||||
p = region.b
|
||||
info = app_delim_info(s, p)
|
||||
if info is None and p > 0:
|
||||
info = app_delim_info(s, p - 1)
|
||||
if info is None:
|
||||
new_regions.append(region)
|
||||
message = "point is not on a klammer application delimiter (@)"
|
||||
continue
|
||||
dpos, kind = info
|
||||
match = app_match(s, dpos, kind)
|
||||
if match is None:
|
||||
new_regions.append(region)
|
||||
message = ("no matching delimiter for this %s klammer"
|
||||
% ("opening" if kind == 'open' else "closing"))
|
||||
continue
|
||||
new_regions.append(sublime.Region(match, match))
|
||||
moved = True
|
||||
|
||||
view.sel().clear()
|
||||
for r in new_regions:
|
||||
view.sel().add(r)
|
||||
|
||||
if moved:
|
||||
view.show(view.sel()[0].b)
|
||||
elif message:
|
||||
sublime.status_message("Klammertext: " + message)
|
||||
|
||||
def is_enabled(self):
|
||||
# Only meaningful in Klammertext buffers.
|
||||
return self.view.match_selector(0, "text.klammertext")
|
||||
|
||||
|
||||
# --- live matched-delimiter highlighting (show-paren equivalent) -----------
|
||||
|
||||
class KlammertextMatchHighlighter(sublime_plugin.ViewEventListener):
|
||||
"""Highlight the matching klammer application delimiter as the caret sits
|
||||
on one. The Sublime equivalent of the Emacs mode's show-paren support —
|
||||
driven by cursor movement, reusing the same context-sensitive matcher.
|
||||
|
||||
A matched pair is boxed (region.bluish); a mismatch or unbalanced delimiter
|
||||
is boxed in red (region.redish) with a status-bar message. Both the token
|
||||
under the caret and its match are boxed; the Emacs mode highlights only the
|
||||
single @ character, but boxing the whole @name / name@ reads better here.
|
||||
To highlight only the far delimiter, drop the first region in _update()."""
|
||||
|
||||
MATCH_KEY = 'klammertext_paren_match'
|
||||
MISMATCH_KEY = 'klammertext_paren_mismatch'
|
||||
|
||||
@classmethod
|
||||
def is_applicable(cls, settings):
|
||||
return str(settings.get('syntax', '')).endswith('Klammertext.sublime-syntax')
|
||||
|
||||
def __init__(self, view):
|
||||
super().__init__(view)
|
||||
self._change_count = -1
|
||||
self._text = ''
|
||||
|
||||
def _buffer(self):
|
||||
# Re-read the buffer only when it has actually changed, so plain cursor
|
||||
# movement over a large file does not re-copy the whole document.
|
||||
cc = self.view.change_count()
|
||||
if cc != self._change_count:
|
||||
self._text = self.view.substr(sublime.Region(0, self.view.size()))
|
||||
self._change_count = cc
|
||||
return self._text
|
||||
|
||||
def on_selection_modified_async(self):
|
||||
self._update()
|
||||
|
||||
def on_activated_async(self):
|
||||
self._update()
|
||||
|
||||
def _clear(self):
|
||||
self.view.erase_regions(self.MATCH_KEY)
|
||||
self.view.erase_regions(self.MISMATCH_KEY)
|
||||
|
||||
def _update(self):
|
||||
view = self.view
|
||||
sel = view.sel()
|
||||
if len(sel) == 0:
|
||||
self._clear()
|
||||
return
|
||||
p = sel[0].b
|
||||
s = self._buffer()
|
||||
|
||||
info = app_delim_info(s, p)
|
||||
if info is None and p > 0:
|
||||
info = app_delim_info(s, p - 1)
|
||||
if info is None:
|
||||
self._clear()
|
||||
return
|
||||
|
||||
dpos, kind = info
|
||||
match = app_match(s, dpos, kind)
|
||||
open_pos = dpos if kind == 'open' else match
|
||||
close_pos = match if kind == 'open' else dpos
|
||||
mism = paren_mismatch(s, open_pos, close_pos)
|
||||
|
||||
regions = [sublime.Region(*token_region(s, dpos, kind))]
|
||||
if match is not None:
|
||||
other_kind = 'close' if kind == 'open' else 'open'
|
||||
regions.append(sublime.Region(*token_region(s, match, other_kind)))
|
||||
|
||||
flags = sublime.DRAW_NO_FILL
|
||||
if mism:
|
||||
view.erase_regions(self.MATCH_KEY)
|
||||
view.add_regions(self.MISMATCH_KEY, regions, 'region.redish', '', flags)
|
||||
if match is None:
|
||||
if kind == 'open':
|
||||
msg = "opening @%s has no matching close" % open_name(s, open_pos)
|
||||
else:
|
||||
msg = "closing delimiter has no matching open"
|
||||
else:
|
||||
msg = ("closing %s@ does not match opening @%s"
|
||||
% (close_name(s, close_pos) or '?', open_name(s, open_pos)))
|
||||
sublime.status_message("Klammertext: " + msg)
|
||||
else:
|
||||
view.erase_regions(self.MISMATCH_KEY)
|
||||
view.add_regions(self.MATCH_KEY, regions, 'region.bluish', '', flags)
|
||||
183
doc/edit/sublime/Klammertext.sublime-syntax
Normal file
183
doc/edit/sublime/Klammertext.sublime-syntax
Normal file
@@ -0,0 +1,183 @@
|
||||
%YAML 1.2
|
||||
---
|
||||
# Klammertext.sublime-syntax
|
||||
#
|
||||
# Sublime Text syntax highlighting for Klammertext (.kt and .k files).
|
||||
# A port of the Emacs major mode doc/emacs/klammertext-mode.el.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# What it highlights (mirrors the Emacs mode's eight token classes):
|
||||
#
|
||||
# Text removal (#):
|
||||
# # ... remove to end of line (marker + removed text)
|
||||
# ## ... remove to end of file (marker + removed text)
|
||||
# #[ ... ]# remove enclosed text, nestable (markers + removed text)
|
||||
# #- #+ #/ whitespace operators: NOT removals, left unhighlighted
|
||||
# (matched only so the '#' above does not eat the line)
|
||||
#
|
||||
# Klammer applications (@), definitions (@@), system commands (@@@):
|
||||
# @name @@name @@@name opening (@ and name are one unit)
|
||||
# name@ name@@ name@@@ named closing
|
||||
# @ @@ @@@ bare closing
|
||||
#
|
||||
# Escapes: ^@ ^# ^| ^^ the caret makes the next character literal, so it
|
||||
# is consumed and NOT treated as a delimiter. Left unscoped, to
|
||||
# match the Emacs mode, which shows escaped characters as ordinary
|
||||
# text. (A run of carets pairs left-to-right: ^^ is a literal
|
||||
# caret, a leftover single ^ escapes the following character —
|
||||
# the '\^.' rule reproduces exactly that parity.)
|
||||
#
|
||||
# Literal klammers: @code ... code@ interior is verbatim (no # or @
|
||||
# interpreted). To add another literal klammer 'foo', copy the
|
||||
# '@code' rule and the 'literal_code' context below, replacing
|
||||
# code -> foo.
|
||||
#
|
||||
# SYNC: the literal-klammer set is duplicated in three places that
|
||||
# must agree (a .sublime-syntax file is static and cannot read the
|
||||
# Emacs defcustom). When you add or remove one, mirror it in all:
|
||||
# * klammertext-literal-klammers in
|
||||
# doc/emacs/klammertext-mode.el (the source of truth)
|
||||
# * LITERAL_KLAMMERS in Klammertext.py
|
||||
# * the @NAME rule + literal_NAME context here
|
||||
# All three are currently seeded with just 'code'.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# How open vs. close is decided (the same rule the Emacs scanner uses):
|
||||
# * a delimiter whose NAME follows the @-run (@name) is an OPENING;
|
||||
# * a bare @-run, or one whose NAME precedes it (name@), is a CLOSING.
|
||||
# Because this tokenizer runs left-to-right, an opening consumes "@name" as one
|
||||
# unit, so a trailing bare @ in the compact form @name@ is naturally a close.
|
||||
# The (?![A-Za-z0-9_@]) look-ahead on every closing keeps "foo@bar" correct:
|
||||
# @ is followed by a name, so it opens @bar and 'foo' stays plain text.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scope -> color. Colors live in the color scheme, not here. The package ships
|
||||
# additive .sublime-color-scheme overrides for all five of Sublime's built-in
|
||||
# schemes (Breakers, Celeste, Mariana, Monokai, Sixteen); each merges onto its
|
||||
# scheme by filename and recolors only .klammertext scopes. They use one hue
|
||||
# system — application blue, definition green, system orange, each opening bright
|
||||
# and its close the same hue darker — shown at full intensity on dark grounds and
|
||||
# scaled down on light grounds. Exact values are in each override's header.
|
||||
#
|
||||
# Without a matching override (e.g. a third-party scheme) a stock scheme still
|
||||
# gives a meaningful default from these scope names: three klammer-family colors
|
||||
# (function / storage / keyword), muted removed text (comment), plain escapes.
|
||||
# To get the full palette on another scheme, copy one of the shipped overrides
|
||||
# to <SchemeName>.sublime-color-scheme.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# Install: put this file — together with its companions Klammertext.py,
|
||||
# Default.sublime-keymap and Comments.tmPreferences — in a dedicated package
|
||||
# folder named 'Klammertext' under Packages/ (Preferences -> Browse Packages
|
||||
# opens Packages/):
|
||||
# ~/.config/sublime-text/Packages/Klammertext/ (Linux)
|
||||
# ~/Library/Application Support/Sublime Text/Packages/Klammertext/ (macOS)
|
||||
# A dedicated folder (not Packages/User/) keeps the bundled keymap from
|
||||
# merging into your personal one. Sublime picks it all up live and applies
|
||||
# the syntax to .kt and .k files. (The syntax file alone also works from
|
||||
# Packages/User/ if you only want highlighting.)
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# Known differences from the Emacs mode (deliberate, matching its own limits):
|
||||
# * @@ and @@@ definition BODIES are highlighted as ordinary Klammertext,
|
||||
# not treated specially — same as the Emacs mode.
|
||||
# * Delimiter MATCHING (jump + live highlight) is not in this syntax file —
|
||||
# Sublime's built-in bracket matching needs fixed character pairs, which @
|
||||
# (both open and close, decided by context) cannot provide. It lives in
|
||||
# the companion Klammertext.py instead: klammertext_jump_to_match (C-c C-j
|
||||
# equivalent) and a ViewEventListener that highlights the matching
|
||||
# delimiter as the caret moves (show-paren equivalent), both reusing one
|
||||
# context-sensitive matcher. This is a plugin concern, not a tokenizer one.
|
||||
# * Comment toggling is provided by the companion Comments.tmPreferences:
|
||||
# Ctrl-/ inserts '# ' (line removal), Ctrl-Shift-/ wraps in '#[ ... ]#'
|
||||
# (block removal).
|
||||
|
||||
name: Klammertext
|
||||
file_extensions:
|
||||
- kt
|
||||
- k
|
||||
scope: text.klammertext
|
||||
version: 2
|
||||
|
||||
variables:
|
||||
# A klammer name: letters, digits, underscore. A hyphen is NOT a name char
|
||||
# (the abbreviated form @name-arg1-arg2 ends the name at the first hyphen).
|
||||
name: '[A-Za-z0-9_]+'
|
||||
# A closing delimiter must not be followed by a name char (that would be an
|
||||
# opening @name) or another @ (that would be a longer @-run).
|
||||
not_delim: '(?![A-Za-z0-9_@])'
|
||||
|
||||
contexts:
|
||||
main:
|
||||
# --- escapes: ^X makes X literal; consumed so # / @ are not delimiters ---
|
||||
- match: '\^.'
|
||||
|
||||
# --- text removal (#) ---
|
||||
- match: '##'
|
||||
scope: punctuation.definition.comment.klammertext
|
||||
push: removal_file
|
||||
- match: '#\['
|
||||
scope: punctuation.definition.comment.klammertext
|
||||
push: removal_block
|
||||
# whitespace operators #- #+ #/ (with optional count): not removals.
|
||||
# Matched (and left unscoped) so the '#' line rule below does not consume
|
||||
# the rest of the line. Add a scope here if you would rather color them.
|
||||
- match: '#[-+/]\d*'
|
||||
- match: '#'
|
||||
scope: punctuation.definition.comment.klammertext
|
||||
push: removal_line
|
||||
|
||||
# --- literal klammer: interior is verbatim (seeded default: @code) ---
|
||||
- match: '@code(?![A-Za-z0-9_])'
|
||||
scope: entity.name.function.begin.klammertext
|
||||
push: literal_code
|
||||
|
||||
# --- system / target commands @@@ ---
|
||||
- match: '@@@{{name}}'
|
||||
scope: keyword.control.begin.klammertext # @@@name opening
|
||||
- match: '@@@{{not_delim}}'
|
||||
scope: keyword.control.end.klammertext # bare @@@ close
|
||||
- match: '{{name}}@@@{{not_delim}}'
|
||||
scope: keyword.control.end.klammertext # name@@@ named close
|
||||
|
||||
# --- klammer definitions @@ ---
|
||||
- match: '@@{{name}}'
|
||||
scope: storage.type.begin.klammertext # @@name opening
|
||||
- match: '@@{{not_delim}}'
|
||||
scope: storage.type.end.klammertext # bare @@ close
|
||||
- match: '{{name}}@@{{not_delim}}'
|
||||
scope: storage.type.end.klammertext # name@@ named close
|
||||
|
||||
# --- klammer applications @ ---
|
||||
- match: '@{{name}}'
|
||||
scope: entity.name.function.begin.klammertext # @name opening
|
||||
- match: '@{{not_delim}}'
|
||||
scope: entity.name.function.end.klammertext # bare @ close
|
||||
- match: '{{name}}@{{not_delim}}'
|
||||
scope: entity.name.function.end.klammertext # name@ named close
|
||||
|
||||
# rest of line is removed
|
||||
removal_line:
|
||||
- meta_scope: comment.line.klammertext
|
||||
- match: '\n'
|
||||
pop: true
|
||||
|
||||
# rest of file is removed (## never closes)
|
||||
removal_file:
|
||||
- meta_scope: comment.block.klammertext
|
||||
|
||||
# #[ ... ]# removed, nestable
|
||||
removal_block:
|
||||
- meta_scope: comment.block.klammertext
|
||||
- match: '#\['
|
||||
scope: punctuation.definition.comment.klammertext
|
||||
push: removal_block
|
||||
- match: '\]#'
|
||||
scope: punctuation.definition.comment.klammertext
|
||||
pop: true
|
||||
|
||||
# @code ... code@ — interior verbatim (unscoped), only the close ends it
|
||||
literal_code:
|
||||
- match: 'code@'
|
||||
scope: entity.name.function.end.klammertext
|
||||
pop: true
|
||||
102
doc/edit/sublime/Klammertext_in_Sublime_Text.md
Normal file
102
doc/edit/sublime/Klammertext_in_Sublime_Text.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Klammertext for Sublime Text
|
||||
|
||||
A Sublime Text port of the Emacs major mode for Klammertext
|
||||
(`doc/emacs/klammertext-mode.el`). It brings syntax highlighting, delimiter
|
||||
matching, and comment toggling to `.kt` and `.k` files. Behavior mirrors the
|
||||
Emacs mode closely; where the two intentionally differ, the file headers say so.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `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. |
|
||||
| `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. |
|
||||
|
||||
## Installation
|
||||
|
||||
Put the files into a folder named `Klammertext` under Sublime's `Packages`
|
||||
directory:
|
||||
|
||||
| Platform | Path |
|
||||
|----------|------|
|
||||
| Linux | `~/.config/sublime-text/Packages/Klammertext/` |
|
||||
| macOS | `~/Library/Application Support/Sublime Text/Packages/Klammertext/` |
|
||||
| Windows | `%AppData%\Sublime Text\Packages\Klammertext\` |
|
||||
|
||||
The quickest way to find it: **Preferences → Browse Packages…** opens the
|
||||
`Packages` directory. Create the `Klammertext` folder there and copy the files
|
||||
in. Sublime loads them live — no restart — and applies the syntax to `.kt` and
|
||||
`.k` files automatically.
|
||||
|
||||
Use a dedicated folder (not `Packages/User/`) so the bundled keymap does not
|
||||
merge into your personal one. If you want highlighting only, the
|
||||
`.sublime-syntax` file alone works from `Packages/User/`.
|
||||
|
||||
The plugin targets **Sublime Text 4**: the live-highlight colors use Sublime's
|
||||
adaptive `region.*` scopes, which were added in ST4.
|
||||
|
||||
## Features and keys
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| open a `.kt` / `.k` file | Syntax highlighting (automatic) |
|
||||
| **Ctrl+M** | Jump between a klammer application's opening and closing `@` (equivalent of the Emacs mode's `C-c C-j`) |
|
||||
| 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+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`.
|
||||
|
||||
## Colors
|
||||
|
||||
Colors are installed automatically for all five of Sublime's built-in schemes.
|
||||
Each `*.sublime-color-scheme` file (Breakers, Celeste, Mariana, Monokai,
|
||||
Sixteen) is an *additive override*: Sublime merges it onto the matching scheme
|
||||
by filename, recoloring only the Klammertext delimiters and leaving everything
|
||||
else untouched. There is nothing to set up.
|
||||
|
||||
All five share one hue system — application blue, definition green, system
|
||||
orange, each opening bright and its close the same hue darker — shown at full
|
||||
intensity on the dark schemes (Monokai, Mariana) and scaled down for contrast on
|
||||
the light schemes (Breakers, Celeste, Sixteen). Removed text uses each scheme's
|
||||
own comment grey.
|
||||
|
||||
For any other scheme — a legacy `.tmTheme` such as Solarized, or a third-party
|
||||
scheme — copy one of the included files to `<Scheme Name>.sublime-color-scheme`
|
||||
in the package folder (its name is shown at **Preferences → Settings** under
|
||||
`color_scheme`), choosing a light or dark source file to match the ground. The
|
||||
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
|
||||
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`
|
||||
- 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.
|
||||
|
||||
## Not included
|
||||
|
||||
Whole-file semantic validation — persistent error underlines when the cursor is
|
||||
elsewhere, klammer-name completion, go-to-definition — is not part of this
|
||||
package. That would need a language server (used through the Sublime LSP
|
||||
package), a separate program, and is unrelated to the highlighting and matching
|
||||
provided here.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the plugin does not seem to load, open **View → Show Console** for any error
|
||||
message. Check that the files sit directly inside `Packages/Klammertext/` (not
|
||||
a nested subfolder) and that the current file's syntax reads "Klammertext" in
|
||||
the status bar at the bottom-right of the window.
|
||||
60
doc/edit/sublime/Mariana.sublime-color-scheme
Normal file
60
doc/edit/sublime/Mariana.sublime-color-scheme
Normal file
@@ -0,0 +1,60 @@
|
||||
// Klammertext colors for the "Mariana" scheme (dark ground).
|
||||
// One hue system across all schemes: application = blue, definition =
|
||||
// green, system = orange; each opens bright and its close is 80%% of the
|
||||
// open (a klammer "begins bright and gets dark"). Shown at full intensity
|
||||
// on dark grounds, at 60%% on light grounds for contrast. Delimiters are
|
||||
// forced to normal style. Merged onto Mariana by filename; recolors only
|
||||
// .klammertext scopes. (The highlighting was first developed as an Emacs
|
||||
// major mode; see Klammertext_in_Sublime_Text.md.)
|
||||
//
|
||||
// #a6acb9 removed text (Mariana's comment grey)
|
||||
// #ff6b6b removal markers
|
||||
// #89ddff @name open blue
|
||||
// #6eb1cc name@ close darker blue
|
||||
// #c3e88d @@name open green
|
||||
// #9cba71 name@@ close darker green
|
||||
// #ffab70 @@@name open orange
|
||||
// #cc895a name@@@ close darker orange
|
||||
{
|
||||
"name": "Mariana",
|
||||
"rules": [
|
||||
{
|
||||
"scope": "comment.line.klammertext, comment.block.klammertext",
|
||||
"foreground": "#a6acb9"
|
||||
},
|
||||
{
|
||||
"scope": "punctuation.definition.comment.klammertext",
|
||||
"foreground": "#ff6b6b"
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.function.begin.klammertext",
|
||||
"foreground": "#89ddff",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.function.end.klammertext",
|
||||
"foreground": "#6eb1cc",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "storage.type.begin.klammertext",
|
||||
"foreground": "#c3e88d",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "storage.type.end.klammertext",
|
||||
"foreground": "#9cba71",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.begin.klammertext",
|
||||
"foreground": "#ffab70",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.end.klammertext",
|
||||
"foreground": "#cc895a",
|
||||
"font_style": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
60
doc/edit/sublime/Monokai.sublime-color-scheme
Normal file
60
doc/edit/sublime/Monokai.sublime-color-scheme
Normal file
@@ -0,0 +1,60 @@
|
||||
// Klammertext colors for the "Monokai" scheme (dark ground).
|
||||
// One hue system across all schemes: application = blue, definition =
|
||||
// green, system = orange; each opens bright and its close is 80%% of the
|
||||
// open (a klammer "begins bright and gets dark"). Shown at full intensity
|
||||
// on dark grounds, at 60%% on light grounds for contrast. Delimiters are
|
||||
// forced to normal style. Merged onto Monokai by filename; recolors only
|
||||
// .klammertext scopes. (The highlighting was first developed as an Emacs
|
||||
// major mode; see Klammertext_in_Sublime_Text.md.)
|
||||
//
|
||||
// #8a8272 removed text (Monokai's comment grey)
|
||||
// #ff6b6b removal markers
|
||||
// #89ddff @name open blue
|
||||
// #6eb1cc name@ close darker blue
|
||||
// #c3e88d @@name open green
|
||||
// #9cba71 name@@ close darker green
|
||||
// #ffab70 @@@name open orange
|
||||
// #cc895a name@@@ close darker orange
|
||||
{
|
||||
"name": "Monokai",
|
||||
"rules": [
|
||||
{
|
||||
"scope": "comment.line.klammertext, comment.block.klammertext",
|
||||
"foreground": "#8a8272"
|
||||
},
|
||||
{
|
||||
"scope": "punctuation.definition.comment.klammertext",
|
||||
"foreground": "#ff6b6b"
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.function.begin.klammertext",
|
||||
"foreground": "#89ddff",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.function.end.klammertext",
|
||||
"foreground": "#6eb1cc",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "storage.type.begin.klammertext",
|
||||
"foreground": "#c3e88d",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "storage.type.end.klammertext",
|
||||
"foreground": "#9cba71",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.begin.klammertext",
|
||||
"foreground": "#ffab70",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.end.klammertext",
|
||||
"foreground": "#cc895a",
|
||||
"font_style": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
60
doc/edit/sublime/Sixteen.sublime-color-scheme
Normal file
60
doc/edit/sublime/Sixteen.sublime-color-scheme
Normal file
@@ -0,0 +1,60 @@
|
||||
// Klammertext colors for the "Sixteen" scheme (light ground).
|
||||
// One hue system across all schemes: application = blue, definition =
|
||||
// green, system = orange; each opens bright and its close is 80%% of the
|
||||
// open (a klammer "begins bright and gets dark"). Shown at full intensity
|
||||
// on dark grounds, at 60%% on light grounds for contrast. Delimiters are
|
||||
// forced to normal style. Merged onto Sixteen by filename; recolors only
|
||||
// .klammertext scopes. (The highlighting was first developed as an Emacs
|
||||
// major mode; see Klammertext_in_Sublime_Text.md.)
|
||||
//
|
||||
// #b8b8b8 removed text (Sixteen's comment grey)
|
||||
// #994040 removal markers
|
||||
// #528599 @name open blue
|
||||
// #426a7a name@ close darker blue
|
||||
// #758b55 @@name open green
|
||||
// #5e7044 name@@ close darker green
|
||||
// #996743 @@@name open orange
|
||||
// #7a5236 name@@@ close darker orange
|
||||
{
|
||||
"name": "Sixteen",
|
||||
"rules": [
|
||||
{
|
||||
"scope": "comment.line.klammertext, comment.block.klammertext",
|
||||
"foreground": "#b8b8b8"
|
||||
},
|
||||
{
|
||||
"scope": "punctuation.definition.comment.klammertext",
|
||||
"foreground": "#994040"
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.function.begin.klammertext",
|
||||
"foreground": "#528599",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.function.end.klammertext",
|
||||
"foreground": "#426a7a",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "storage.type.begin.klammertext",
|
||||
"foreground": "#758b55",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "storage.type.end.klammertext",
|
||||
"foreground": "#5e7044",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.begin.klammertext",
|
||||
"foreground": "#996743",
|
||||
"font_style": ""
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.end.klammertext",
|
||||
"foreground": "#7a5236",
|
||||
"font_style": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
45
doc/edit/sublime/example.kt
Normal file
45
doc/edit/sublime/example.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
# This is a line comment — removed to end of line, in the "ignored" color.
|
||||
# The # marker is a different color from the text it removes.
|
||||
|
||||
#[ This is a block comment. It can span lines,
|
||||
and #[ nest ]# like this. ]#
|
||||
|
||||
# --- Klammer applications (@) : opening @name vs. closing name@ / bare @ ---
|
||||
|
||||
@i italic @ @b bold @ @tt monospace @
|
||||
|
||||
@sup 2 | 3 @ # positional arguments separated by |
|
||||
@sup-2-3 # the abbreviated form colors only the name
|
||||
|
||||
@link https://example.com :text a labelled link @
|
||||
|
||||
A named close is handy for long arguments: @section a long body here section@
|
||||
|
||||
# --- Klammer definitions (@@) and system commands (@@@) ---
|
||||
|
||||
@@mdlh : @i Material Definition Language Handbook @ @@
|
||||
@@heading.html : *arg* @@
|
||||
@@@target html | HTML output | options @@@
|
||||
|
||||
# --- Escapes: a caret makes the next character literal (shown as plain text) ---
|
||||
|
||||
^@ and ^# and ^^ and ^| are literal, not delimiters.
|
||||
|
||||
# --- Literal klammer: @code ... code@ interior is verbatim ---
|
||||
# The stray @ and # below are NOT delimiters inside a literal span:
|
||||
|
||||
@code
|
||||
if (a @ b) { return "# not a comment"; }
|
||||
code@
|
||||
|
||||
# --- Whitespace operators (#- #+ #/) are not removals; shown as plain text ---
|
||||
|
||||
tight#-spacing gap#+3here break#/2line
|
||||
|
||||
# --- A deliberate MISMATCH: put the cursor on @open or close@ to see it turn ---
|
||||
# --- red with a message (the names disagree); a matched pair boxes normally. ---
|
||||
|
||||
@open some content close@
|
||||
|
||||
## Everything from this line to the end of the file is removed (## = to EOF).
|
||||
this trailing line is greyed out as removed text
|
||||
38
doc/install/klammertext.zsh
Normal file
38
doc/install/klammertext.zsh
Normal file
@@ -0,0 +1,38 @@
|
||||
# Klammertext via Apple's `container` — macOS (Apple Silicon) shell wrapper
|
||||
# -----------------------------------------------------------------------------
|
||||
# Lets you run Klammertext without typing the full `container run ...` command.
|
||||
# Install: save this file (e.g. ~/klammertext.zsh) and add to your ~/.zshrc:
|
||||
#
|
||||
# source ~/klammertext.zsh
|
||||
#
|
||||
# Then open a new terminal and use `ktext`, `kdesc`, `kdiag` like normal
|
||||
# commands. Requires Apple Silicon + macOS 26 or later, with Apple's
|
||||
# `container` runtime installed and its service started (`container system
|
||||
# start`). Install `container` from the signed .pkg at
|
||||
# https://github.com/apple/container/releases (NOT Homebrew). See
|
||||
# doc/install/macos_container_install.md for the full guide.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# The published image is multi-arch; on Apple Silicon `container` pulls the
|
||||
# native arm64 build, so no --platform / --rosetta is needed.
|
||||
KLAMMERTEXT_IMAGE="${KLAMMERTEXT_IMAGE:-akopra/klammertext:latest}"
|
||||
|
||||
_klammertext_run() {
|
||||
local cmd="$1"; shift
|
||||
container run --rm \
|
||||
-v "$PWD:/work" -w /work \
|
||||
"$KLAMMERTEXT_IMAGE" "$cmd" "$@"
|
||||
}
|
||||
|
||||
# The three Klammertext commands. Files are read from and written to the
|
||||
# current directory (mounted into the container as /work).
|
||||
ktext() { _klammertext_run ktext "$@"; }
|
||||
kdesc() { _klammertext_run kdesc "$@"; }
|
||||
kdiag() { _klammertext_run kdiag "$@"; }
|
||||
|
||||
# Download or update to the latest published image (delete first so the moving
|
||||
# `latest` tag is definitely refreshed).
|
||||
klammertext-update() {
|
||||
container image delete "$KLAMMERTEXT_IMAGE" 2>/dev/null
|
||||
container image pull "$KLAMMERTEXT_IMAGE"
|
||||
}
|
||||
133
doc/install/linux_container_install.md
Normal file
133
doc/install/linux_container_install.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Running Klammertext on Linux with Docker
|
||||
|
||||
This guide runs Klammertext on a Linux system (Ubuntu or Pop!_OS — the steps are
|
||||
identical) using the prebuilt Docker container. You do **not** need to install
|
||||
TeX Live, Python, or any programming tools — everything, including the TeX Live
|
||||
system that makes PDFs, is packaged inside a single downloadable image. You
|
||||
install Docker once, then Klammertext works like a normal command.
|
||||
|
||||
The published image is multi-arch, so Docker pulls the build matching your CPU
|
||||
(`amd64` on Intel/AMD, `arm64` on ARM machines) automatically.
|
||||
|
||||
For a source build instead (full `@eval` access, no Docker), see
|
||||
`linux_source_install.md`.
|
||||
|
||||
## Step 1 — Install Docker
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install docker.io
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
Log out and back in for the group change to take effect (so you can run `docker`
|
||||
without `sudo`). You only do this once.
|
||||
|
||||
## Step 2 — Download Klammertext
|
||||
|
||||
```bash
|
||||
docker pull akopra/klammertext:latest
|
||||
```
|
||||
|
||||
This downloads Klammertext and its built-in TeX Live (a few hundred megabytes).
|
||||
You won't need to do it again unless you're updating.
|
||||
|
||||
## Step 3 — Add the Klammertext commands
|
||||
|
||||
Add these aliases to `~/.bashrc` (or `~/.zshrc`) so `ktext`, `kdesc`, and
|
||||
`kdiag` work as ordinary commands that read and write files in whatever folder
|
||||
you run them from:
|
||||
|
||||
```bash
|
||||
alias ktext='docker run --rm -u $(id -u):$(id -g) -v "$PWD:/work" -w /work akopra/klammertext ktext'
|
||||
alias kdesc='docker run --rm -v "$PWD:/work" -w /work akopra/klammertext kdesc'
|
||||
alias kdiag='docker run --rm -v "$PWD:/work" -w /work akopra/klammertext kdiag'
|
||||
```
|
||||
|
||||
The `-u $(id -u):$(id -g)` on `ktext` makes output files owned by you rather than
|
||||
root. `kdesc` and `kdiag` only read files, so they don't need it. The
|
||||
`-v "$PWD:/work"` mounts your current directory into the container as `/work`,
|
||||
which is required for the commands to see your files.
|
||||
|
||||
Reload your shell (open a new terminal, or `source ~/.bashrc`).
|
||||
|
||||
## Step 4 — Make your first document
|
||||
|
||||
In a folder you want to work in, create a test file:
|
||||
|
||||
```bash
|
||||
cat > hello.kt <<'EOF'
|
||||
@document
|
||||
:structure article
|
||||
:title Hello
|
||||
:text
|
||||
@s1 Hello, Klammertext @
|
||||
|
||||
This document was produced with no TeX Live installed — just Docker and the
|
||||
Klammertext image.
|
||||
@
|
||||
EOF
|
||||
```
|
||||
|
||||
Produce a web page and a PDF:
|
||||
|
||||
```bash
|
||||
ktext hello.kt -t html # makes hello/index.html
|
||||
ktext hello.kt -t pdf # makes hello.pdf
|
||||
```
|
||||
|
||||
That's it — you're running Klammertext.
|
||||
|
||||
## Updating
|
||||
|
||||
To update to the latest published image:
|
||||
|
||||
```bash
|
||||
docker pull akopra/klammertext:latest
|
||||
```
|
||||
|
||||
## Haskell support (`@eval :haskell`)
|
||||
|
||||
The standard image does not include Haskell. For `@eval :haskell`, pull the
|
||||
Haskell image and use it in place of the standard one:
|
||||
|
||||
```bash
|
||||
docker pull akopra/klammertext:haskell
|
||||
alias ktext='docker run --rm -u $(id -u):$(id -g) -v "$PWD:/work" -w /work akopra/klammertext:haskell ktext'
|
||||
```
|
||||
|
||||
Test:
|
||||
|
||||
```bash
|
||||
ktext -s '@eval :haskell main = putStrLn "hello" @' -d
|
||||
```
|
||||
|
||||
Alternatively, a source install gives all `@eval` modes without a separate image
|
||||
(see `linux_source_install.md`).
|
||||
|
||||
## Klammer set loading
|
||||
|
||||
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`.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
- **`Cannot connect to the Docker daemon`** — the Docker service isn't running:
|
||||
`sudo systemctl start docker`, then retry.
|
||||
- **`permission denied` running `docker`** — your user isn't in the `docker`
|
||||
group yet: `sudo usermod -aG docker $USER`, then log out and back in.
|
||||
- **`No such file or directory` for your input** — the file must be in the
|
||||
directory you run the command from (that's what gets mounted). `cd` into the
|
||||
folder with your `.kt` files first.
|
||||
- **Output files owned by root** — add `-u $(id -u):$(id -g)` to the `ktext`
|
||||
command/alias (as shown in Step 3).
|
||||
|
||||
## Freeing disk space
|
||||
|
||||
To remove the image (you can re-pull it later):
|
||||
|
||||
```bash
|
||||
docker rmi akopra/klammertext:latest
|
||||
docker system prune # optional: remove all unused Docker data
|
||||
```
|
||||
277
doc/install/linux_source_install.md
Normal file
277
doc/install/linux_source_install.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Klammertext source installation on Linux (Ubuntu / Pop!_OS)
|
||||
|
||||
This document describes how to build and install Klammertext from source on a
|
||||
Linux system — Ubuntu or Pop!_OS; the steps are identical — without using the
|
||||
container. A source installation gives full access to all `@eval` modes,
|
||||
including `:haskell` and `:shell` commands that depend on locally installed
|
||||
software.
|
||||
|
||||
For the container installation on Linux, see `linux_container_install.md`.
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The following packages are required to build Klammertext:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install g++ make python3-dev
|
||||
```
|
||||
|
||||
The C++ compiler must support C++20. GCC 11 or later is required (Ubuntu 22.04
|
||||
and later include GCC 12+).
|
||||
|
||||
The SKS `@image` klammer requires OpenImageIO Python bindings. These must match
|
||||
the Python version that ktext is built against (check with
|
||||
`python3.XX -c "import OpenImageIO"`). For example, if ktext links against
|
||||
Python 3.12:
|
||||
|
||||
```bash
|
||||
pip3.12 install OpenImageIO
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
g++ --version
|
||||
```
|
||||
|
||||
|
||||
## Clone the repository
|
||||
|
||||
```bash
|
||||
git clone https://git.andykopra.com/ack/klammertext.git
|
||||
cd klammertext
|
||||
```
|
||||
|
||||
|
||||
## Environment variables
|
||||
|
||||
Klammertext's runtime environment is provided by a single self-configuring
|
||||
file. Source it from your shell profile (e.g., `~/.bashrc` or `~/.zshrc`):
|
||||
|
||||
```bash
|
||||
source /path/to/klammertext/mac/env/runtime.env
|
||||
```
|
||||
|
||||
It self-locates `KLAMMERTEXT_HOME` from its own path, adds `bin/` and
|
||||
`tst/` to `PATH` (plus the newest `~/external/texlive/<year>/bin/<arch>` if a
|
||||
TeX Live is installed there), sets `LD_LIBRARY_PATH` so `libklammertext.so` is
|
||||
found, and sets the LSan suppressions. There is no per-host or per-OS variable
|
||||
to set. For a TeX Live or library in a non-standard location, add it to an
|
||||
optional, gitignored `mac/env/runtime.env.local` (sourced at the end).
|
||||
|
||||
After editing your shell profile, reload it:
|
||||
|
||||
```bash
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
|
||||
## Configure the build
|
||||
|
||||
No build configuration is needed. The single `mac/env/makefile.env` is
|
||||
cross-platform: it reads `KLAMMERTEXT_HOME` from the environment (set by
|
||||
`runtime.env` above), auto-detects the platform with `uname`, and auto-detects
|
||||
Python with `python3-config` — no hardcoded version and no per-host file to
|
||||
edit. Verify the Python development headers are present:
|
||||
|
||||
```bash
|
||||
python3-config --includes # prints -I.../python3.XX for your Python
|
||||
```
|
||||
|
||||
If `python3-config` is missing, install your distribution's `python3-dev`
|
||||
(Debian/Ubuntu) or `python3-devel` (Fedora/RHEL) package.
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
Build the shared library, the SKS components, and the three commands with a
|
||||
single command: `make -C com` builds its prerequisites in `mac/` and `sks/`
|
||||
first, then the commands. `OPTIMIZE=1` selects an optimized `-O3` build (what
|
||||
you want to install and run); without it you get a slower `-O0` debug build
|
||||
with AddressSanitizer, intended for development:
|
||||
|
||||
```bash
|
||||
make -C com -j OPTIMIZE=1 # lib/libklammertext.so + sks/*.so + bin/{ktext,kdesc,kdiag}
|
||||
```
|
||||
|
||||
Verify the build:
|
||||
|
||||
```bash
|
||||
ktext -s '@eval 1 + 1 @' -d
|
||||
```
|
||||
|
||||
This should print `2`. For a quick document smoke test, create a small file and
|
||||
render it to HTML:
|
||||
|
||||
```bash
|
||||
cat > hello.kt <<'EOF'
|
||||
@document
|
||||
:structure article
|
||||
:title Hello
|
||||
:text
|
||||
@s1 Hello, Klammertext @
|
||||
|
||||
This document was built from source.
|
||||
@
|
||||
EOF
|
||||
ktext hello.kt -t html # writes hello/index.html
|
||||
```
|
||||
|
||||
|
||||
## TeX Live (for PDF output)
|
||||
|
||||
The Standard Klammer Set uses **XeLaTeX** for the `pdf` target. Build a complete
|
||||
Klammertext TeX Live tree with the bundled script, giving it a destination
|
||||
directory under `~/external/texlive/<year>` — the location `runtime.env`
|
||||
auto-detects. The script needs `perl`, `xz-utils`, `fontconfig`, and either
|
||||
`wget` or `curl`:
|
||||
|
||||
```bash
|
||||
sudo apt-get install perl wget xz-utils fontconfig
|
||||
bash $KLAMMERTEXT_HOME/doc/install/texlive_additional_packages.sh ~/external/texlive/2026
|
||||
```
|
||||
|
||||
This installs `scheme-small` plus the additional packages the SKS needs and
|
||||
rebuilds all formats, fetching the binaries for your architecture. It writes a
|
||||
`KLAMMERTEXT_BUILD_INFO.txt` provenance file (mirror, release, package list,
|
||||
date) into the tree.
|
||||
|
||||
Because the tree lives under `~/external/texlive/2026`, `runtime.env` finds it
|
||||
automatically — open a new shell (or re-source `runtime.env`) and `xelatex`
|
||||
will be on `PATH`. No manual `KLAMMERTEXT_TEXLIVE_BIN` is needed.
|
||||
|
||||
If you would rather reuse a TeX Live you already have, point `runtime.env` at it
|
||||
from the gitignored escape hatch instead, and install the SKS's extra packages
|
||||
into it yourself (the package list is in `doc/install/texlive_additional_packages.sh`):
|
||||
|
||||
```bash
|
||||
cat >> "$KLAMMERTEXT_HOME/mac/env/runtime.env.local" <<'EOF'
|
||||
export KLAMMERTEXT_TEXLIVE_BIN=/path/to/texlive/bin/x86_64-linux
|
||||
export PATH="$KLAMMERTEXT_TEXLIVE_BIN:$PATH"
|
||||
EOF
|
||||
```
|
||||
|
||||
Verify and test (reusing the `hello.kt` from the Build section):
|
||||
|
||||
```bash
|
||||
xelatex --version
|
||||
ktext hello.kt -t pdf # writes hello.pdf
|
||||
```
|
||||
|
||||
|
||||
## Optional: Haskell (for @eval :haskell)
|
||||
|
||||
The `@eval :haskell` mode requires `runghc`, which is part of the Haskell
|
||||
toolchain. Alternatively, the `akopra/klammertext:haskell` container image
|
||||
includes GHC (see `linux_container_install.md`).
|
||||
|
||||
The recommended way to install Haskell on Ubuntu is via ghcup:
|
||||
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
||||
```
|
||||
|
||||
Follow the prompts to install GHC, cabal, and related tools. After
|
||||
installation, ensure the ghcup bin directory is in your `PATH`:
|
||||
|
||||
```bash
|
||||
export PATH=$HOME/.ghcup/bin:$PATH
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
runghc --version
|
||||
```
|
||||
|
||||
Test in Klammertext:
|
||||
|
||||
```bash
|
||||
ktext -s '@eval :haskell main = putStr "Hello from Haskell" @' -d
|
||||
```
|
||||
|
||||
|
||||
## Directory layout after build
|
||||
|
||||
```
|
||||
klammertext/
|
||||
├── bin/ ktext, kdesc, kdiag executables (after build)
|
||||
├── lib/ libklammertext.so shared library (after build)
|
||||
├── mac/ Klammermachine C++ source
|
||||
├── sks/ Standard Klammer Set (.k files and .so modules)
|
||||
│ ├── document/ document.so
|
||||
│ ├── kutil/ kutil.o
|
||||
│ └── target/ html_util.o, latex_util.o
|
||||
├── com/ command source (ktext, kdesc, kdiag) and Makefile
|
||||
├── doc/ installation guides (doc/install) and editor support (doc/edit)
|
||||
└── tst/ test suites
|
||||
```
|
||||
|
||||
|
||||
## Verifying the installation
|
||||
|
||||
Run the following commands to verify that everything works:
|
||||
|
||||
```bash
|
||||
# Basic evaluation (Python)
|
||||
ktext -s '@eval 1 + 1 @' -d
|
||||
|
||||
# Shell evaluation
|
||||
ktext -s '@eval :shell date @' -d
|
||||
|
||||
# Show machine state (SKS is loaded by default)
|
||||
ktext -s '' -m
|
||||
|
||||
# HTML and PDF output (uses the hello.kt from the Build section)
|
||||
ktext hello.kt -t html
|
||||
ktext hello.kt -t pdf # requires TeX Live
|
||||
|
||||
# Haskell evaluation (requires ghcup)
|
||||
ktext -s '@eval :haskell main = putStr "42" @' -d
|
||||
|
||||
# Run unit tests
|
||||
make -C $KLAMMERTEXT_HOME/tst test
|
||||
```
|
||||
|
||||
|
||||
## Updating
|
||||
|
||||
To update an existing source installation to the latest version:
|
||||
|
||||
```bash
|
||||
cd $KLAMMERTEXT_HOME
|
||||
git pull
|
||||
make -C com -j OPTIMIZE=1 # rebuild library, SKS components, and commands
|
||||
```
|
||||
|
||||
The TeX Live tree only needs rebuilding if the SKS's package requirements
|
||||
changed (rare); when they do, re-run the script from the TeX Live section
|
||||
above.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"libklammertext.so: cannot open shared object file"**
|
||||
Ensure `LD_LIBRARY_PATH` includes `$KLAMMERTEXT_HOME/lib`:
|
||||
|
||||
```bash
|
||||
export LD_LIBRARY_PATH=$KLAMMERTEXT_HOME/lib:$LD_LIBRARY_PATH
|
||||
```
|
||||
|
||||
**"KLAMMERTEXT_HOME is not set"**
|
||||
Set the environment variable as described in the Environment variables
|
||||
section above.
|
||||
|
||||
**"python3.XX/Python.h: No such file or directory"**
|
||||
Install the Python development headers:
|
||||
|
||||
```bash
|
||||
sudo apt-get install python3-dev
|
||||
```
|
||||
|
||||
**"xelatex: command not found" (when using -t pdf)**
|
||||
Install TeX Live and ensure its bin directory is in `PATH`.
|
||||
|
||||
**"@eval :haskell requires runghc"**
|
||||
Install Haskell via ghcup as described in the Optional: Haskell section.
|
||||
161
doc/install/macos_container_install.md
Normal file
161
doc/install/macos_container_install.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Running Klammertext on a Mac (Apple Silicon)
|
||||
|
||||
This guide gets Klammertext running on your Mac in a few minutes. You do
|
||||
**not** need to install TeX Live, Python, or any programming tools —
|
||||
everything, including the TeX Live system that makes PDFs, is packaged inside
|
||||
a single downloadable image. You install Apple's `container` runtime once, and
|
||||
then Klammertext works like a normal command.
|
||||
|
||||
This guide is for **Apple Silicon Macs (M1/M2/M3/M4/M5) running macOS 26 or
|
||||
later**, which is what Apple's `container` runtime requires. (Support for older
|
||||
Intel Macs can be provided separately if needed.)
|
||||
|
||||
## Step 1 — Install Apple's `container` runtime
|
||||
|
||||
`container` is Apple's own tool for running Linux container images natively on
|
||||
Apple Silicon. It's free.
|
||||
|
||||
1. Go to <https://github.com/apple/container/releases> and download the latest
|
||||
installer package (the `.pkg` file). **Do not** use Homebrew — the Homebrew
|
||||
`container` formula is a different, unrelated tool.
|
||||
2. Double-click the downloaded `.pkg` and follow the installer.
|
||||
3. Open the **Terminal** app (Applications → Utilities → Terminal) and start the
|
||||
`container` background service (accept the recommended default if prompted):
|
||||
|
||||
```sh
|
||||
container system start
|
||||
```
|
||||
|
||||
You only do this once. You can check the service any time with
|
||||
`container system status`.
|
||||
|
||||
## Step 2 — Download Klammertext
|
||||
|
||||
In Terminal, paste this and press Return:
|
||||
|
||||
```sh
|
||||
container image pull akopra/klammertext:latest
|
||||
```
|
||||
|
||||
This downloads Klammertext and its built-in TeX Live. It's a few hundred
|
||||
megabytes, so it takes a minute the first time. Because the image is multi-arch,
|
||||
`container` fetches the native Apple Silicon (arm64) build. You won't need to do
|
||||
this again unless you're updating.
|
||||
|
||||
## Step 3 — Add the Klammertext commands
|
||||
|
||||
This step makes `ktext` (and its helpers) available as ordinary commands.
|
||||
|
||||
1. In Terminal, create the wrapper file by pasting this whole block and
|
||||
pressing Return:
|
||||
|
||||
```sh
|
||||
cat > ~/klammertext.zsh <<'EOF'
|
||||
# Klammertext via Apple's `container` runtime (native arm64 image; no Rosetta).
|
||||
KLAMMERTEXT_IMAGE="${KLAMMERTEXT_IMAGE:-akopra/klammertext:latest}"
|
||||
_klammertext_run() {
|
||||
local cmd="$1"; shift
|
||||
container run --rm \
|
||||
-v "$PWD:/work" -w /work \
|
||||
"$KLAMMERTEXT_IMAGE" "$cmd" "$@"
|
||||
}
|
||||
ktext() { _klammertext_run ktext "$@"; }
|
||||
kdesc() { _klammertext_run kdesc "$@"; }
|
||||
kdiag() { _klammertext_run kdiag "$@"; }
|
||||
klammertext-update() {
|
||||
container image delete "$KLAMMERTEXT_IMAGE" 2>/dev/null
|
||||
container image pull "$KLAMMERTEXT_IMAGE"
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
2. Tell your shell to load it, by pasting this and pressing Return:
|
||||
|
||||
```sh
|
||||
echo 'source ~/klammertext.zsh' >> ~/.zshrc
|
||||
```
|
||||
|
||||
3. **Close Terminal and open a new window** so the change takes effect.
|
||||
|
||||
You now have three commands — `ktext`, `kdesc`, `kdiag` — that run Klammertext
|
||||
inside a container while reading and writing files in whatever folder you're
|
||||
working in.
|
||||
|
||||
## Step 4 — Make your first document
|
||||
|
||||
In Terminal, go to a folder you want to work in (for example your Desktop) and
|
||||
create a test file:
|
||||
|
||||
```sh
|
||||
cd ~/Desktop
|
||||
cat > hello.kt <<'EOF'
|
||||
@document
|
||||
:structure article
|
||||
:title Hello
|
||||
:text
|
||||
@s1 Hello, Klammertext @
|
||||
|
||||
This document was produced on macOS with no TeX Live installed —
|
||||
just Apple's `container` runtime and the Klammertext image.
|
||||
@
|
||||
EOF
|
||||
```
|
||||
|
||||
Now produce a web page and a PDF from it:
|
||||
|
||||
```sh
|
||||
ktext hello.kt -t html # makes hello/index.html
|
||||
ktext hello.kt -t pdf # makes hello.pdf
|
||||
```
|
||||
|
||||
Open the results:
|
||||
|
||||
```sh
|
||||
open hello.pdf
|
||||
open hello/index.html
|
||||
```
|
||||
|
||||
That's it — you're running Klammertext.
|
||||
|
||||
## Good to know
|
||||
|
||||
- **Work inside one folder.** Klammertext can only see files in (or below) the
|
||||
folder you run the command from. Keep a document and the files it uses
|
||||
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.
|
||||
- **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 —
|
||||
the whole point of the container is that you don't need a source build. But if
|
||||
this machine *also* has a native source build on its `PATH` (so `which ktext`
|
||||
shows a path like `.../K/com/ktext`), the wrapper's `ktext` function would
|
||||
shadow that native command. To keep both, give the container wrappers their
|
||||
own names by using `ktextc` / `kdescc` / `kdiagc` (trailing `c` = container)
|
||||
in place of `ktext` / `kdesc` / `kdiag` in the Step 3 file. Then plain `ktext`
|
||||
still runs your source build and `ktextc` runs the container.
|
||||
- **Quitting.** Klammertext only runs while you're using it; there's nothing
|
||||
left running afterward. If you want to stop the `container` service entirely,
|
||||
run `container system stop`; start it again with `container system start` next
|
||||
time.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
- **`command not found: ktext`** — you didn't open a new Terminal window after
|
||||
Step 3, or the `source` line didn't get added. Re-run the Step 3 commands and
|
||||
open a fresh Terminal.
|
||||
- **`container: command not found`** — the `container` runtime isn't installed
|
||||
(Step 1), or the Terminal window predates the install (open a new one).
|
||||
- **A command hangs or won't connect** — the `container` service isn't running.
|
||||
Run `container system start` (check with `container system status`), then try
|
||||
again.
|
||||
- **A run aborted and now seems stuck** — `container run --rm` can leave the
|
||||
container behind after an error. Clear leftovers with:
|
||||
|
||||
```sh
|
||||
for id in $(container list -a -q); do container kill "$id"; container delete "$id"; done
|
||||
```
|
||||
171
doc/install/macos_source_install.md
Normal file
171
doc/install/macos_source_install.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# Klammertext source installation on macOS (Apple Silicon)
|
||||
|
||||
Companion to `linux_source_install.md`. Verified on an Apple-Silicon Mac
|
||||
(arm64, macOS 26 "Tahoe"). Klammertext's core (engine, SKS, HTML/LaTeX,
|
||||
`@image`) builds and runs natively with Apple Clang; the cross-platform build
|
||||
environment (`mac/env/makefile.env`) auto-detects the OS via `uname`.
|
||||
|
||||
## 1. Toolchain prerequisites
|
||||
|
||||
```sh
|
||||
xcode-select --install # Command Line Tools (clang, headers) — if not already present
|
||||
```
|
||||
|
||||
Then install Python with a linkable `libpython` and `python3-config`, using
|
||||
whichever package manager you have — **Homebrew** or **MacPorts**. Both work;
|
||||
only the install prefix differs, and the build auto-detects it.
|
||||
|
||||
```sh
|
||||
# Homebrew (https://brew.sh):
|
||||
brew install python
|
||||
brew install gcc # OPTIONAL: a second compiler for a standards check
|
||||
|
||||
# MacPorts (https://www.macports.org) — provisional, pending testing on a
|
||||
# MacPorts system:
|
||||
sudo port install python312
|
||||
sudo port select --set python3 python312 # so python3 / python3-config resolve
|
||||
sudo port install gcc14 # OPTIONAL: a second compiler for a standards check
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The build embeds Python, which needs `python3-config` and a linkable
|
||||
`libpython`. Apple's `/usr/bin/python3` does **not** ship a usable
|
||||
`python3-config` and Apple discourages linking it — so a package-manager
|
||||
Python (Homebrew or MacPorts) is required. It coexists with Apple's;
|
||||
`python3-config` resolves to it when the manager's `bin` is early on `PATH`
|
||||
(`/opt/homebrew/bin` for Homebrew, `/opt/local/bin` for MacPorts — both set up
|
||||
by their installers). For MacPorts, `port select --set python3 python312`
|
||||
makes `python3` and `python3-config` resolve.
|
||||
- `makefile.env` gets all Python include/link flags from `python3-config` and
|
||||
auto-detects the package-manager prefix (`/opt/homebrew` or `/opt/local`, via
|
||||
`MACOS_PREFIX`), so either manager works without edits. Override with
|
||||
`make MACOS_PREFIX=...` if yours is installed elsewhere.
|
||||
|
||||
## 2. Image support (the `@image` klammer): OpenImageIO
|
||||
|
||||
```sh
|
||||
# Homebrew:
|
||||
/opt/homebrew/bin/pip3.<N> install --break-system-packages OpenImageIO
|
||||
# e.g. pip3.14 — match your Python's version
|
||||
|
||||
# MacPorts (matches the python312 installed above):
|
||||
sudo port install py312-openimageio
|
||||
```
|
||||
|
||||
This provides the OpenImageIO Python bindings the embedded interpreter uses.
|
||||
With pip, the self-contained PyPI wheel goes into that Python's site-packages;
|
||||
`--break-system-packages` is needed because the Python is PEP-668 "externally
|
||||
managed". (Homebrew alternative: `brew install openimageio`, heavier — it pulls
|
||||
ffmpeg/openexr/etc.)
|
||||
|
||||
## 3. Clone and configure the environment
|
||||
|
||||
```sh
|
||||
git clone https://git.andykopra.com/ack/klammertext.git ~/projects/klammertext
|
||||
# Set up the runtime environment (KLAMMERTEXT_HOME, PATH); add to ~/.zprofile:
|
||||
echo 'source "$HOME/projects/klammertext/mac/env/runtime.env"' >> ~/.zprofile
|
||||
```
|
||||
|
||||
The single self-configuring `runtime.env` self-locates `KLAMMERTEXT_HOME` from
|
||||
its own path. On macOS it sets no `LD_LIBRARY_PATH`/`DYLD_LIBRARY_PATH`
|
||||
(`libklammertext.so` is found via the binaries' `@loader_path` rpath, and
|
||||
`document.so` is dlopen'd by absolute path under `$KLAMMERTEXT_HOME`) and no
|
||||
`LSAN_OPTIONS` (LeakSanitizer is unsupported on macOS).
|
||||
|
||||
## 4. Build (Apple Clang)
|
||||
|
||||
Clang is the compiler you run on macOS, and it is the **default** here
|
||||
(`makefile.env` selects clang on Darwin), so no `COMPILER=` flag is needed.
|
||||
A single `make -C com` builds its prerequisites in `mac/` and `sks/` first,
|
||||
then the commands:
|
||||
|
||||
```sh
|
||||
cd "$KLAMMERTEXT_HOME"
|
||||
make -C com -j OPTIMIZE=1 # libklammertext.so + sks/*.so + bin/{ktext,kdesc,kdiag}
|
||||
ktext -s '@eval 1 + 1 @' -d # smoke test — prints 2
|
||||
```
|
||||
|
||||
For a document smoke test, create a small file and render it to HTML:
|
||||
|
||||
```sh
|
||||
cat > hello.kt <<'EOF'
|
||||
@document
|
||||
:structure article
|
||||
:title Hello
|
||||
:text
|
||||
@s1 Hello, Klammertext @
|
||||
|
||||
This document was built from source.
|
||||
@
|
||||
EOF
|
||||
ktext hello.kt -t html # writes hello/index.html
|
||||
```
|
||||
|
||||
(A PDF render needs TeX Live — see section 5.)
|
||||
|
||||
## 5. PDF target: TeX Live
|
||||
|
||||
Build a complete Klammertext TeX Live tree with the bundled script, into
|
||||
`~/external/texlive/<year>` — the location `runtime.env` auto-detects
|
||||
(`bin/universal-darwin`). macOS already has `curl`, `perl`, and `tar`, and
|
||||
`install-tl` self-provides `xz`, so nothing extra is needed:
|
||||
|
||||
```sh
|
||||
bash "$KLAMMERTEXT_HOME/doc/install/texlive_additional_packages.sh" ~/external/texlive/2026
|
||||
```
|
||||
|
||||
This installs `scheme-small` plus the SKS's additional packages and rebuilds all
|
||||
formats, fetching the `universal-darwin` binaries, and writes a
|
||||
`KLAMMERTEXT_BUILD_INFO.txt` provenance file into the tree. **This is the same
|
||||
command used on Linux**, so the TeX Live layout is identical across your
|
||||
machines. `runtime.env` then finds the tree automatically — open a new shell (or
|
||||
re-source it) and `xelatex` is on `PATH`; no manual `KLAMMERTEXT_TEXLIVE_BIN` is
|
||||
needed.
|
||||
|
||||
If you already run BasicTeX/MacTeX and prefer to reuse it, point `runtime.env`
|
||||
at its bin directory from the gitignored escape hatch instead (and install the
|
||||
SKS's extra packages into it yourself — the list is in the script). Set the
|
||||
variable **and** prepend it to `PATH`, since `runtime.env.local` is sourced
|
||||
after the main `PATH` is built:
|
||||
|
||||
```sh
|
||||
cat >> "$KLAMMERTEXT_HOME/mac/env/runtime.env.local" <<'EOF'
|
||||
export KLAMMERTEXT_TEXLIVE_BIN=/usr/local/texlive/2025basic/bin/universal-darwin
|
||||
export PATH="$KLAMMERTEXT_TEXLIVE_BIN:$PATH"
|
||||
EOF
|
||||
```
|
||||
|
||||
Verify and test (reusing `hello.kt` from section 4):
|
||||
|
||||
```sh
|
||||
xelatex --version
|
||||
ktext hello.kt -t pdf # writes hello.pdf
|
||||
```
|
||||
|
||||
## 6. Updating
|
||||
|
||||
To update an existing source installation to the latest version:
|
||||
|
||||
```sh
|
||||
cd "$KLAMMERTEXT_HOME"
|
||||
git pull
|
||||
make -C com -j OPTIMIZE=1 # rebuild library, SKS components, and commands
|
||||
```
|
||||
|
||||
Rebuild the TeX Live tree only if the SKS's package requirements changed (rare);
|
||||
re-run the script from section 5.
|
||||
|
||||
## Compiler notes (macOS)
|
||||
|
||||
- **Clang is the compiler you run, and the default here.** Apple Clang builds
|
||||
run correctly; `makefile.env` selects clang on Darwin automatically.
|
||||
- **Do not run gcc-built binaries on macOS.** GCC (Homebrew `g++-NN` or MacPorts
|
||||
`g++-mp-NN`) is useful only as an optional compile-time standards check
|
||||
(`make -C com COMPILER=gcc`); the resulting binaries **crash at runtime** on
|
||||
macOS because of a gcc/macOS codegen issue (for example `std::source_location`
|
||||
returning a bad pointer, so `Machine::Machine()` walks into `strlen` and
|
||||
SIGSEGVs). Always run the clang-built binary. (gcc-built binaries run fine on
|
||||
Linux.)
|
||||
- **Switching compilers requires a full clean** — `g++` and `clang++` objects
|
||||
must not be mixed (ABI). `make -C com redo` does a full clean rebuild across
|
||||
`mac`, `sks`, and `com`; a partial `make -C mac clean` does not.
|
||||
117
doc/install/texlive_additional_packages.sh
Normal file
117
doc/install/texlive_additional_packages.sh
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
# Build a self-contained TeX Live tree for the Klammertext SKS "tex"/"pdf"
|
||||
# targets: scheme-small plus the additional packages the SKS requires, with
|
||||
# all formats rebuilt. This is the single source of truth for constructing a
|
||||
# Klammertext TeX Live directory — used both by the Docker build (Dockerfile,
|
||||
# per-arch) and for native installs.
|
||||
#
|
||||
# Usage:
|
||||
# texlive_additional_packages.sh <texdir> [mirror]
|
||||
#
|
||||
# <texdir> Destination directory for the TeX Live tree (created by
|
||||
# install-tl), e.g. /opt/texlive or ~/external/texlive/2026.
|
||||
# Should not already exist.
|
||||
# [mirror] A CONCRETE tlnet mirror URL. Must NOT be the mirror.ctan.org
|
||||
# redirect: it resolves to a different mirror (possibly a different
|
||||
# TeX Live revision) on each call, which makes tlmgr abort partway
|
||||
# with "tlmgr itself needs to be updated". Defaults to a pinned
|
||||
# CTAN mirror. Once the current TeX Live year is frozen (the next
|
||||
# release ships), point this at the historic tlnet-final snapshot
|
||||
# for exact reproducibility, e.g.
|
||||
# https://ftp.math.utah.edu/pub/texlive/historic/2026/tlnet-final
|
||||
#
|
||||
# install-tl fetches the binaries for the ARCHITECTURE it runs on, so running
|
||||
# this under arm64 produces an arm64 tree and under x86_64 an x86_64 tree.
|
||||
#
|
||||
# Prerequisites on the host: perl, tar, gzip, xz (xz-utils), and either wget or
|
||||
# curl (macOS ships curl, not wget). fontconfig is recommended so fmtutil can
|
||||
# build all formats cleanly.
|
||||
|
||||
set -eux
|
||||
|
||||
TEXDIR="${1:?usage: $0 <texdir> [mirror]}"
|
||||
MIRROR="${2:-https://ctan.math.illinois.edu/systems/texlive/tlnet}"
|
||||
|
||||
# --- Fetch the installer into a scratch dir --------------------------------
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
cd "$WORK"
|
||||
# Fetch the installer with whichever downloader is present (macOS has curl, not
|
||||
# wget; Debian build images have wget).
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
wget -q "$MIRROR/install-tl-unx.tar.gz"
|
||||
else
|
||||
curl -fsSL -O "$MIRROR/install-tl-unx.tar.gz"
|
||||
fi
|
||||
tar --strip-components=1 -xzf install-tl-unx.tar.gz
|
||||
|
||||
# --- Base install: scheme-small into $TEXDIR -------------------------------
|
||||
cat > texlive.profile <<PROFILE
|
||||
selected_scheme scheme-small
|
||||
TEXDIR $TEXDIR
|
||||
TEXMFLOCAL $TEXDIR/texmf-local
|
||||
TEXMFSYSCONFIG $TEXDIR/texmf-config
|
||||
TEXMFSYSVAR $TEXDIR/texmf-var
|
||||
tlpdbopt_install_docfiles 0
|
||||
tlpdbopt_install_srcfiles 0
|
||||
tlpdbopt_autobackup 0
|
||||
PROFILE
|
||||
|
||||
# install-tl's in-line fmtutil can exit 3 before all packages are present;
|
||||
# that is non-fatal (install-tl continues) and the formats are rebuilt below.
|
||||
./install-tl --profile=texlive.profile --repository="$MIRROR"
|
||||
|
||||
# --- Use the tlmgr from the tree we just built (absolute path), not whatever
|
||||
# tlmgr may be on PATH. Pin its repo to the same concrete mirror and sync
|
||||
# it to that repo's revision before installing more packages. -----------
|
||||
ARCH="$(ls "$TEXDIR/bin")"
|
||||
TLMGR="$TEXDIR/bin/$ARCH/tlmgr"
|
||||
"$TLMGR" option repository "$MIRROR"
|
||||
"$TLMGR" update --self
|
||||
|
||||
# --- Additional packages required by the SKS beyond scheme-small -----------
|
||||
# Single list, used both for the install and for the provenance README below,
|
||||
# so the two cannot drift apart.
|
||||
PACKAGES="adjustbox collectbox collection-fontsrecommended enumitem fontaxes \
|
||||
footmisc inconsolata layouts mdframed multirow needspace opensans pict2e \
|
||||
textpos titlesec upquote zref"
|
||||
|
||||
# shellcheck disable=SC2086 # intentional word splitting into separate args
|
||||
"$TLMGR" install $PACKAGES
|
||||
|
||||
# --- Rebuild every format now that the full package set is installed -------
|
||||
"$TEXDIR/bin/$ARCH/fmtutil-sys" --all
|
||||
|
||||
# --- Provenance: record how this tree was constructed ----------------------
|
||||
# Written into the tree itself so it is self-documenting when found later.
|
||||
RELEASE="$(head -n1 "$TEXDIR/release-texlive.txt" 2>/dev/null || echo unknown)"
|
||||
BUILT="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
cat > "$TEXDIR/KLAMMERTEXT_BUILD_INFO.txt" <<INFO
|
||||
Klammertext TeX Live tree
|
||||
=========================
|
||||
|
||||
Built for the Klammertext Standard Klammer Set (SKS) "tex"/"pdf" targets by
|
||||
doc/install/texlive_additional_packages.sh.
|
||||
|
||||
Built: $BUILT
|
||||
Mirror: $MIRROR
|
||||
TeX Live: $RELEASE
|
||||
Architecture: bin/$ARCH
|
||||
Base scheme: scheme-small
|
||||
Docfiles/srcfiles omitted; all formats rebuilt with fmtutil-sys --all.
|
||||
|
||||
Additional packages installed beyond scheme-small:
|
||||
$(printf ' %s\n' $PACKAGES)
|
||||
|
||||
Reconstruct an equivalent tree with:
|
||||
texlive_additional_packages.sh <texdir> $MIRROR
|
||||
|
||||
Note: the mirror above serves the CURRENT TeX Live release, which receives
|
||||
package updates within its year, so a rebuild is not guaranteed byte-identical.
|
||||
For exact reproducibility, rebuild from the frozen historic tlnet-final
|
||||
snapshot once the release year is no longer current, e.g.
|
||||
https://ftp.math.utah.edu/pub/texlive/historic/<year>/tlnet-final
|
||||
INFO
|
||||
|
||||
echo "Klammertext TeX Live tree built in $TEXDIR (binaries in bin/$ARCH)"
|
||||
echo "Provenance written to $TEXDIR/KLAMMERTEXT_BUILD_INFO.txt"
|
||||
Reference in New Issue
Block a user