Verbatim-safe typography, ^-punctuation quoting, full-range ^UUUU^, polyglot html
Typographic transforms (---, quote pairs, ~) no longer touch verbatim text: @c/@code/@source_listing content and ^'...'^ spans show exactly the characters written. "^" before any punctuation character quotes it in every target (the apostrophe excepted: ^' opens a literal span), with the new :resolve option on @@@target declaring per-target renderings. The ^UUUU^ code-point form accepts 4-6 hex digits, the full Unicode range. The html output and transform spellings are polyglot (XML-valid), in preparation for an EPUB target. New suites: transform_test, character_test (engine), typography_test (SKS). (from dev 07ce5ea86a0a)
This commit is contained in:
@@ -133,8 +133,7 @@ same way the font-lock scanner steps over them."
|
||||
((and (= len 1)
|
||||
(member name klammertext-literal-klammers))
|
||||
;; Verbatim interior: find the closing NAME@ by name.
|
||||
(if (re-search-forward
|
||||
(concat (regexp-quote name) "@") nil t)
|
||||
(if (klammertext--search-literal-close name)
|
||||
(when (< pos (point))
|
||||
(setq opaque t done t))
|
||||
(setq opaque t done t))) ; never closed
|
||||
|
||||
@@ -131,15 +131,32 @@ Register one with `klammertext-add-literal-klammer', e.g. in your init file:
|
||||
;; doc/edit/sublime/Klammertext.sublime-syntax
|
||||
;; * the @NAME verbatim region in doc/edit/vim/syntax/klammertext.vim
|
||||
;; * the @NAME rule in doc/edit/vscode/syntaxes/klammertext.tmLanguage.json
|
||||
;; All are currently seeded with just "code".
|
||||
;; All are currently seeded with "code" and "c" (@c is the inline form of
|
||||
;; @code and took a literal argument 2026-08-16; the miscounted stack from an
|
||||
;; unrecognized "@c ... c@" shifted a whole document's indentation by one).
|
||||
|
||||
(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))
|
||||
|
||||
(defun klammertext--search-literal-close (name &optional bound)
|
||||
"Move point past the exact close token NAME@ at or after point.
|
||||
Return the position after the close, or nil if there is none before BOUND.
|
||||
NAME@ preceded by a name character is verbatim content, not a close --
|
||||
\"basic@\" does not close @c, and \"barcode@\" does not close @code
|
||||
\(the engine's close is a whole katom). Mirrors the shared core's
|
||||
find_literal_close."
|
||||
(let ((close (concat (regexp-quote name) "@")) (found nil))
|
||||
(while (and (setq found (re-search-forward close bound t))
|
||||
(let ((b (match-beginning 0)))
|
||||
(and (> b (point-min))
|
||||
(klammertext--name-char-p (char-before b))))))
|
||||
found))
|
||||
|
||||
;; Seed the list through the same entry point future users will use.
|
||||
(klammertext-add-literal-klammer "code")
|
||||
(klammertext-add-literal-klammer "c")
|
||||
|
||||
;; --- Helpers -----------------------------------------------------------
|
||||
|
||||
@@ -275,13 +292,12 @@ BEFORE is the character before POS."
|
||||
;; 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))))
|
||||
(if (klammertext--search-literal-close name)
|
||||
(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.
|
||||
@@ -478,8 +494,8 @@ delimiter (or skipped region) and return (POS . KIND) with KIND `open or
|
||||
(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)))))
|
||||
(unless (klammertext--search-literal-close name)
|
||||
(goto-char (point-max))))
|
||||
((eq (char-after) ?-)) ; @name-arg : no span
|
||||
(t (throw 'found (cons hit 'open))))))
|
||||
(t ; name@ / bare @ : closing
|
||||
@@ -574,7 +590,7 @@ name, not by depth counting."
|
||||
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)
|
||||
(when (klammertext--search-literal-close name)
|
||||
(1- (point)))))
|
||||
|
||||
(defun klammertext--literal-match-backward (close-pos name)
|
||||
@@ -586,7 +602,11 @@ CLOSE-POS, or nil. Literal spans do not nest, so the nearest preceding real
|
||||
(let ((open-str (concat "@" name)) (result nil))
|
||||
(while (and (not result) (search-backward open-str nil t))
|
||||
(let ((op (point)))
|
||||
;; The name must end where the token ends: "@c" found inside
|
||||
;; "@caption" is not an opener of @c.
|
||||
(unless (or (eq (char-before op) ?@) ; @@NAME = definition
|
||||
(klammertext--name-char-p
|
||||
(char-after (+ op 1 (length name))))
|
||||
(klammertext--escaped-p op))
|
||||
(setq result op))))
|
||||
result)))
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
# * the Emacs defcustoms (klammertext-literal-klammers, -transparent-,
|
||||
# -code-, -align-klammers, -indent-offset, -align-cell-max, -align-row-max)
|
||||
# in doc/edit/emacs/klammertext-mode.el / -indent.el / -align.el
|
||||
# * the '@code' rule + literal_code context in
|
||||
# * the '@code'/'@c' rules + literal_code/literal_c contexts in
|
||||
# doc/edit/sublime/Klammertext.sublime-syntax
|
||||
# * the '@code' verbatim region in doc/edit/vim/syntax/klammertext.vim
|
||||
# * the '@code' rule in doc/edit/vscode/syntaxes/klammertext.tmLanguage.json
|
||||
# * the '@code'/'@c' verbatim regions in doc/edit/vim/syntax/klammertext.vim
|
||||
# * the '@code'/'@c' rules in doc/edit/vscode/syntaxes/klammertext.tmLanguage.json
|
||||
#
|
||||
# Installation note: editors locate this file either next to their own plugin
|
||||
# files (a vendored copy, placed there by doc/make_editing_zip.sh), as
|
||||
@@ -57,7 +57,7 @@ import sys
|
||||
|
||||
# Klammer names whose content is a literal argument (verbatim interior,
|
||||
# closed by a named NAME@ delimiter).
|
||||
LITERAL_KLAMMERS = set(["code"])
|
||||
LITERAL_KLAMMERS = set(["code", "c"])
|
||||
|
||||
# Klammers that contribute no indentation level (a @document's paragraphs
|
||||
# stay at the left margin).
|
||||
@@ -91,6 +91,19 @@ def name_char_p(ch):
|
||||
or ('0' <= ch <= '9') or ch == '_')
|
||||
|
||||
|
||||
def find_literal_close(s, name, start):
|
||||
"""Index of the exact close token NAME@ at or after START, or -1.
|
||||
The engine's close is a whole katom, so NAME@ preceded by a name
|
||||
character is content, not a close -- "basic@" does not close @c, and
|
||||
"barcode@" does not close @code. Single-letter literal names (@c) make
|
||||
this guard essential rather than theoretical."""
|
||||
close = name + '@'
|
||||
idx = s.find(close, start)
|
||||
while idx > 0 and name_char_p(s[idx - 1]):
|
||||
idx = s.find(close, idx + 1)
|
||||
return idx
|
||||
|
||||
|
||||
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."""
|
||||
@@ -188,9 +201,8 @@ def next_app_delim(s, i, limit):
|
||||
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)
|
||||
idx = find_literal_close(s, name, k)
|
||||
i = n if idx == -1 else idx + len(name) + 1
|
||||
continue
|
||||
elif after == '-': # @name-arg : opens no span
|
||||
i = k
|
||||
@@ -318,7 +330,7 @@ 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)
|
||||
idx = find_literal_close(s, name, start)
|
||||
return idx + len(name) if idx != -1 else None
|
||||
|
||||
|
||||
@@ -333,7 +345,10 @@ def literal_match_backward(s, close_pos, name):
|
||||
if idx == -1:
|
||||
return None
|
||||
before = s[idx - 1] if idx > 0 else None
|
||||
if before != '@' and not escaped_p(s, idx):
|
||||
# The name must end where the token ends: "@c" found inside
|
||||
# "@caption" is not an opener of @c.
|
||||
follower = s[idx + len(open_str)] if idx + len(open_str) < len(s) else None
|
||||
if before != '@' and not name_char_p(follower) and not escaped_p(s, idx):
|
||||
return idx
|
||||
end = idx
|
||||
|
||||
@@ -446,7 +461,7 @@ def state_at(s, pos):
|
||||
i = k
|
||||
if run_len == 1 and name in LITERAL_KLAMMERS:
|
||||
# Verbatim interior: find the closing NAME@ by name.
|
||||
idx = s.find(name + '@', k)
|
||||
idx = find_literal_close(s, name, k)
|
||||
if idx == -1: # never closed
|
||||
return (stack, True)
|
||||
close_end = idx + len(name) + 1
|
||||
@@ -584,7 +599,7 @@ def enclosing_span(s, pos, names):
|
||||
name = s[run_end:k]
|
||||
i = k
|
||||
if run_len == 1 and name in LITERAL_KLAMMERS:
|
||||
idx = s.find(name + '@', k)
|
||||
idx = find_literal_close(s, name, k)
|
||||
if idx == -1:
|
||||
break
|
||||
i = idx + len(name) + 1
|
||||
@@ -692,7 +707,7 @@ def scan_lines(content):
|
||||
name = content[run_end:k]
|
||||
i = k
|
||||
if run_len == 1 and name in LITERAL_KLAMMERS:
|
||||
idx = content.find(name + '@', k)
|
||||
idx = find_literal_close(content, name, k)
|
||||
e = n if idx == -1 else idx + len(name) + 1
|
||||
if line_index(max(hit, e - 1)) != line_index(hit):
|
||||
block_range(hit, e)
|
||||
@@ -886,7 +901,7 @@ def diagnostics(s):
|
||||
name = s[run_end:k]
|
||||
i = k
|
||||
if run_len == 1 and name in LITERAL_KLAMMERS:
|
||||
idx = s.find(name + '@', k)
|
||||
idx = find_literal_close(s, name, k)
|
||||
if idx == -1:
|
||||
probs.append({'start': hit, 'end': k,
|
||||
'message': ("literal klammer @%s has no "
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
# caret, a leftover single ^ escapes the following character —
|
||||
# the '\^.' rule reproduces exactly that parity.)
|
||||
#
|
||||
# Literal klammers: @code ... code@ interior is verbatim (no # or @
|
||||
# Literal klammers: @code ... code@ and @c ... c@ 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.
|
||||
@@ -43,7 +43,7 @@
|
||||
# * the @NAME verbatim region in doc/edit/vim/syntax/klammertext.vim
|
||||
# * the @NAME rule in
|
||||
# doc/edit/vscode/syntaxes/klammertext.tmLanguage.json
|
||||
# All are currently seeded with just 'code'.
|
||||
# All are currently seeded with 'code' and 'c'.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# How open vs. close is decided (the same rule the Emacs scanner uses):
|
||||
@@ -131,10 +131,13 @@ contexts:
|
||||
scope: punctuation.definition.comment.klammertext
|
||||
push: removal_line
|
||||
|
||||
# --- literal klammer: interior is verbatim (seeded default: @code) ---
|
||||
# --- literal klammers: interior is verbatim (seeded: @code and @c) ---
|
||||
- match: '@code(?![A-Za-z0-9_])'
|
||||
scope: entity.name.function.begin.klammertext
|
||||
push: literal_code
|
||||
- match: '@c(?![A-Za-z0-9_])'
|
||||
scope: entity.name.function.begin.klammertext
|
||||
push: literal_c
|
||||
|
||||
# --- system / target commands @@@ ---
|
||||
- match: '@@@{{name}}'
|
||||
@@ -185,3 +188,11 @@ contexts:
|
||||
- match: 'code@'
|
||||
scope: entity.name.function.end.klammertext
|
||||
pop: true
|
||||
|
||||
# @c ... c@ — the inline form of @code, same verbatim interior. The
|
||||
# lookbehind keeps a word ending in c ("basic@") from closing the span —
|
||||
# essential for a single-letter name.
|
||||
literal_c:
|
||||
- match: '(?<![A-Za-z0-9_])c@'
|
||||
scope: entity.name.function.end.klammertext
|
||||
pop: true
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
" character is never read as a delimiter. A run of carets pairs
|
||||
" left-to-right, reproducing the language's parity rule.
|
||||
"
|
||||
" Literal klammers: @code ... code@ — the interior is verbatim (no # or @
|
||||
" Literal klammers: @code ... code@ and @c ... c@ — the interior is verbatim (no # or @
|
||||
" interpreted). SYNC: the literal-klammer set's source of truth is
|
||||
" LITERAL_KLAMMERS in doc/edit/shared/klammertext_edit.py; a static
|
||||
" syntax file cannot read it, so when you add a literal klammer 'foo',
|
||||
" copy the klammertextVerbatim region below with code -> foo (and mirror
|
||||
" it in the Emacs, Sublime, and VS Code artifacts; all are seeded with
|
||||
" just 'code').
|
||||
" 'code' and 'c').
|
||||
"
|
||||
" How open vs. close is decided (the same rule as every other integration):
|
||||
" a delimiter whose NAME follows the @-run (@name) is an OPENING; a bare
|
||||
@@ -76,12 +76,16 @@ syn match klammertextAppOpen /@\@1<!@\w\+/
|
||||
syn match klammertextAppClose /@\@1<!@\%(\w\|@\)\@!/
|
||||
syn match klammertextAppClose /@\@1<!\w\+@\%(\w\|@\)\@!/
|
||||
|
||||
" --- literal klammer: interior verbatim (seeded default: @code) -----------
|
||||
" --- literal klammers: interior verbatim (seeded: @code and @c) -----------
|
||||
" Defined AFTER the @-tier matches: in Vim, when several items match at the
|
||||
" same position the LAST defined wins, and this region must beat the plain
|
||||
" klammertextAppOpen match at '@code'. (Sublime's tokenizer picks the FIRST
|
||||
" listed rule — the opposite convention; don't copy that ordering here.)
|
||||
syn region klammertextVerbatim matchgroup=klammertextAppOpen start=/@\@1<!@code\%(\w\)\@!/ matchgroup=klammertextAppClose end=/code@/
|
||||
" @c is the inline form of @code (literal since 2026-08-16). The \w\@1<!
|
||||
" guard on the close keeps a word ending in c ("basic@") from ending the
|
||||
" region — essential for a single-letter name.
|
||||
syn region klammertextVerbatim matchgroup=klammertextAppOpen start=/@\@1<!@c\%(\w\)\@!/ matchgroup=klammertextAppClose end=/\w\@1<!c@/
|
||||
|
||||
" --- colors ---------------------------------------------------------------
|
||||
" The shared palette, dark and light values (see the header). cterm values
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"whitespace operators left unscoped), the three @-tiers — application",
|
||||
"(@), definition (@@), system (@@@) — each as an opening (@name, one",
|
||||
"unit) or a close (name@, bare @), ^-escapes (consumed, unscoped),",
|
||||
"and verbatim @code ... code@ interiors.",
|
||||
"and verbatim @code ... code@ / @c ... c@ interiors.",
|
||||
"",
|
||||
"How open vs. close is decided (the same rule as every integration):",
|
||||
"a delimiter whose NAME follows the @-run is an OPENING; a bare",
|
||||
@@ -19,7 +19,7 @@
|
||||
"in doc/edit/shared/klammertext_edit.py. A static grammar cannot",
|
||||
"read it: to add a literal klammer 'foo', copy the @code begin/end",
|
||||
"rule below with code -> foo (and mirror it in the Emacs, Sublime,",
|
||||
"and Vim artifacts; all are seeded with just 'code').",
|
||||
"and Vim artifacts; all are seeded with 'code' and 'c').",
|
||||
"",
|
||||
"Delimiter matching, indentation, alignment, and diagnostics are not",
|
||||
"tokenizer concerns — they come from the Klammertext language server",
|
||||
@@ -77,6 +77,16 @@
|
||||
"0": { "name": "entity.name.function.end.klammertext" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"begin": "@c(?![A-Za-z0-9_])",
|
||||
"beginCaptures": {
|
||||
"0": { "name": "entity.name.function.begin.klammertext" }
|
||||
},
|
||||
"end": "(?<![A-Za-z0-9_])c@",
|
||||
"endCaptures": {
|
||||
"0": { "name": "entity.name.function.end.klammertext" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "@@@[A-Za-z0-9_]+",
|
||||
"name": "keyword.control.begin.klammertext"
|
||||
|
||||
Reference in New Issue
Block a user