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:
2026-08-23 20:48:26 +02:00
parent d982c0d6cc
commit 37b6ba1c4f
77 changed files with 1665 additions and 1608 deletions

View File

@@ -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 "