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:
2026-07-18 18:48:23 +02:00
commit 2ba7ceee7a
272 changed files with 27634 additions and 0 deletions

View 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": ""
}
]
}

View 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": ""
}
]
}

View 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>

View 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" }
]
}
]

View 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)

View 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

View 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.

View 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": ""
}
]
}

View 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": ""
}
]
}

View 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": ""
}
]
}

View 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