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:
@@ -51,7 +51,7 @@ are regenerated on each release — patches cannot be merged directly.
|
||||
Report problems (or send patches) to the author; accepted changes are
|
||||
applied to the development tree and appear in a following snapshot.
|
||||
|
||||
This snapshot was assembled from development commit `f34e66a4dbfb`.
|
||||
This snapshot was assembled from development commit `07ce5ea86a0a`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ int main(int argc, char* argv[])
|
||||
|
||||
if (input_text.empty() && input_filenames.empty()) {
|
||||
throw Argument_error(
|
||||
"You must specify input filenames and/or text", Locator());
|
||||
"You must specify input filenames and/or text", Locator::none());
|
||||
}
|
||||
|
||||
auto [target, output_dir, output_basename, output_filename,
|
||||
|
||||
@@ -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)
|
||||
(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 (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"
|
||||
|
||||
@@ -48,7 +48,7 @@ std::string Argtype_registry::replace_symbols(const std::string& pattern, const
|
||||
ss << "Argtype symbol " << symbol << " not defined.\n\n"
|
||||
<< "Defined argtypes:\n";
|
||||
ss << describe();
|
||||
throw Definition_error(ss.str(), loc, false);
|
||||
throw Definition_error(ss.str(), loc);
|
||||
}
|
||||
}
|
||||
return expanded;
|
||||
@@ -88,7 +88,7 @@ void Argtype_registry::add(const std::string& name, const std::string& desc,
|
||||
if (pattern != expanded_pattern) {
|
||||
ss << "expanded to:\n " << expanded_pattern << "\n";
|
||||
}
|
||||
throw Definition_error(ss.str(), loc, false);
|
||||
throw Definition_error(ss.str(), loc);
|
||||
}
|
||||
if (!default_value.empty() &&
|
||||
!std::regex_match(default_value, m_types[name].m_regex)) {
|
||||
@@ -96,7 +96,7 @@ void Argtype_registry::add(const std::string& name, const std::string& desc,
|
||||
ss << "The default value \"" << default_value << "\" for argument type \""
|
||||
<< name << "\" does not match its own pattern:\n"
|
||||
<< " " << pattern << "\n";
|
||||
throw Definition_error(ss.str(), loc, false);
|
||||
throw Definition_error(ss.str(), loc);
|
||||
}
|
||||
if (!alone_value.empty()) {
|
||||
// A pattern that matches running text cannot delimit a bare option
|
||||
@@ -111,20 +111,20 @@ void Argtype_registry::add(const std::string& name, const std::string& desc,
|
||||
if (std::regex_match(std::string("one two"), m_types[name].m_regex)) {
|
||||
std::stringstream ss {};
|
||||
ss << "The argument type \"" << name << "\" cannot declare an :alone value"
|
||||
<< "because its pattern matches running text:\n"
|
||||
<< "because its pattern matches running text:"
|
||||
<< " " << pattern << "\n\n"
|
||||
<< "An :alone value is used when an option name is written without a"
|
||||
<< "value. A type that matches running text cannot tell a bare option"
|
||||
<< "name from one whose value follows it, so the text after the name"
|
||||
<< "would be taken as the value instead.\n";
|
||||
throw Definition_error(ss.str(), loc, false);
|
||||
<< "would be taken as the value instead.";
|
||||
throw Definition_error(ss.str(), loc);
|
||||
}
|
||||
if (!std::regex_match(alone_value, m_types[name].m_regex)) {
|
||||
std::stringstream ss {};
|
||||
ss << "The alone value \"" << alone_value << "\" for argument type \""
|
||||
<< name << "\" does not match its own pattern:\n"
|
||||
<< " " << pattern << "\n";
|
||||
throw Definition_error(ss.str(), loc, false);
|
||||
throw Definition_error(ss.str(), loc);
|
||||
}
|
||||
}
|
||||
m_names.push_back(name);
|
||||
|
||||
@@ -86,10 +86,10 @@ Parameter parse_positional_parameter(const katom_list& katoms, const Argtype_reg
|
||||
{
|
||||
// (void)K::log(3, katoms);
|
||||
if (katoms.size() > 1) {
|
||||
throw Argument_error("Multiple katoms for positional argument: " +
|
||||
throw Argument_error("Multiple katoms for positional argument:\n " +
|
||||
as_string(katoms.begin(), katoms.end(), true) +
|
||||
"\nPositional arguments are separated by the bar (|) character.",
|
||||
katoms[0].m_loc, false);
|
||||
katoms[0].m_loc);
|
||||
}
|
||||
Katom k = katoms[0];
|
||||
std::string name = k.m_text;
|
||||
@@ -367,7 +367,7 @@ void Parameter_set::check_positional(const katom_lists& positional_arguments, co
|
||||
ss << " " << arg.m_name << "\n";
|
||||
}
|
||||
//std::cout << ss.str();
|
||||
throw Argument_error(ss.str(), loc, false);
|
||||
throw Argument_error(ss.str(), loc);
|
||||
} else if (positional_count < given_count) {
|
||||
//std::cout << "DESCRIBE\n";
|
||||
//describe_parameters();
|
||||
@@ -391,7 +391,7 @@ Parameter_set::check_optional(const katom_lists& optional_arguments, const Locat
|
||||
}
|
||||
if (std::ranges::count(optional_names_used, name) > 0) {
|
||||
throw Argument_error("Optional argument \":" + name + "\" already provided "
|
||||
+ "with a value of:\n" + values[name], loc, false);
|
||||
+ "with a value of:\n" + values[name], loc);
|
||||
}
|
||||
katom_list value_katoms(opt.begin()+1, opt.end());
|
||||
std::string value = trim(to_string(value_katoms));
|
||||
@@ -477,9 +477,12 @@ void Parameter_set::validate(
|
||||
std::stringstream ss {};
|
||||
ss << "The value \"" << value << "\" given for the argument \""
|
||||
<< parameter.m_name << "\" does not match the \"" << argtype.m_name
|
||||
<< "\" argument type:\n\n"
|
||||
<< trim(argtype.m_desc) << "\n";
|
||||
throw Argument_error(ss.str(), loc, false);
|
||||
<< "\" argument type.\n\nThe \"" << argtype.m_name << "\" argument type describes "
|
||||
// A .k description is prose: collapse its source layout (line
|
||||
// breaks, continuation indentation) so it re-flows with the
|
||||
// sentence -- indented .k lines would otherwise read as verbatim.
|
||||
<< collapse_whitespace(argtype.m_desc) << "\n";
|
||||
throw Argument_error(ss.str(), loc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +523,7 @@ std::string replace_arguments(
|
||||
}
|
||||
ss << "To prevent the \"*\" character from specifying an argument, "
|
||||
<< "precede it with the \"^\" character.";
|
||||
throw Argument_error(ss.str(), loc, false);
|
||||
throw Argument_error(ss.str(), loc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
36
mac/argv.cpp
36
mac/argv.cpp
@@ -257,7 +257,7 @@ void Argv::check_flags_and_options(const std::string& command, strings_t& words)
|
||||
for (auto w : not_defined) {
|
||||
ss << " " << w << "\n";
|
||||
}
|
||||
throw Argument_error(ss.str(), Locator(), false);
|
||||
throw Argument_error(ss.str(), Locator::none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ void Argv::parse_flags(strings_t& words, string_map& named_args)
|
||||
for (auto w : not_defined) {
|
||||
ss << " " << w << "\n";
|
||||
}
|
||||
throw Argument_error(ss.str(), Locator(), false);
|
||||
throw Argument_error(ss.str(), Locator::none());
|
||||
}
|
||||
*/
|
||||
// std::cout << "Flags found: " << flag_args << "\n";
|
||||
@@ -320,16 +320,29 @@ void Argv::parse_optional(strings_t& words, string_map& named_args)
|
||||
"The option " + flag_name(opt) + " needs a value: " +
|
||||
m_args[opt].m_syntax + ". Enter \"" +
|
||||
file_basename(command_name) + "\" for the list of arguments.",
|
||||
Locator(), false);
|
||||
Locator::none());
|
||||
}
|
||||
std::string opt_arg = words[index] + " ";
|
||||
index++;
|
||||
std::regex opt_regex = m_args[opt].m_rgx;
|
||||
|
||||
// std::cout << "Regex match? " << std::regex_match(trim(opt_arg), opt_regex) << "\n";
|
||||
|
||||
while (index < words.size() && words[index][0] != '-'
|
||||
// && std::regex_match(trim(opt_arg), opt_regex)
|
||||
// Only a LIST-valued option may span several argv words
|
||||
// (--klammersets a b c; --katoms type index). A single-valued
|
||||
// option takes exactly ONE argv element -- argv boundaries are
|
||||
// authoritative -- so a positional written after it is not
|
||||
// swallowed into the value. The former unconditional run
|
||||
// consumed everything up to the next dash-word: "-s 'text'
|
||||
// doc.kt" folded doc.kt into the -s TEXT (and rendered its
|
||||
// pathname), and "--klammersets none doc.kt" consumed the input
|
||||
// filename, so ktext reported that none was given. -s must be
|
||||
// usable in any argv position: its role with a file argument is
|
||||
// to modify the interpretation of that file, and it is
|
||||
// evaluated first regardless of where it is written. A list
|
||||
// value stops at the "--" delimiter (it begins with '-'), which
|
||||
// remains the escape for filenames after a list option.
|
||||
bool list_valued = m_args[opt].m_rgx_symbol == "'list'"
|
||||
|| m_args[opt].m_rgx_symbol == "'katom_display'";
|
||||
while (list_valued && index < words.size() && words[index][0] != '-'
|
||||
&& std::regex_match(trim(opt_arg + words[index]), opt_regex)
|
||||
|
||||
) {
|
||||
@@ -370,7 +383,7 @@ void Argv::parse_positional(const std::string& command, //strings_t words,
|
||||
}
|
||||
std::stringstream ss {};
|
||||
ss << "The argument " << q_(req) << " was not found in:\n " << command;
|
||||
throw Argument_error(ss.str(), Locator(), false);
|
||||
throw Argument_error(ss.str(), Locator::none());
|
||||
}
|
||||
named_args[req] = substring;
|
||||
// Drop the whitespace that separated this positional from the next.
|
||||
@@ -467,7 +480,7 @@ void Argv::check_flags(const string_map& arg_map, const std::string& command)
|
||||
for (const std::string& undef : undefined) {
|
||||
ss << " " << flag_name(undef);
|
||||
}
|
||||
throw Argument_error(ss.str(), Locator());
|
||||
throw Argument_error(ss.str(), Locator::none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,7 +521,7 @@ void Argv::parse(int argc, char* argv[], bool full_parse)
|
||||
usage(file_basename(argv[0]));
|
||||
throw Argument_error(
|
||||
"Incorrect value for option \"" + name + "\":\n" + m_args[name].m_desc + "\n",
|
||||
Locator(), false);
|
||||
Locator());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -594,7 +607,8 @@ int Argv::as_verbosity(const std::string& name)
|
||||
(void)K::log(2, name);
|
||||
auto value = get(name);
|
||||
if (!std::regex_match(value, std::regex(regex_symbols["'verbosity'"]))) {
|
||||
throw Argument_error("Argument \"" + value + "\" is not a verbosity level");
|
||||
throw Argument_error("Argument \"" + value + "\" is not a verbosity level",
|
||||
Locator::none());
|
||||
}
|
||||
return std::stoi(value);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ std::string utf8char(int cp)
|
||||
c[1] = ((cp>>12)&63)+128;
|
||||
c[2] = ((cp>>6)&63)+128;
|
||||
c[3]=(cp&63)+128;
|
||||
} else {
|
||||
// Reachable since ^UUUU^ accepts 6 hex digits: FFFFFF > 10FFFF.
|
||||
return "Invalid Unicode: " + std::to_string(cp);
|
||||
}
|
||||
return std::string(c);
|
||||
}
|
||||
@@ -65,7 +68,13 @@ std::string unicode_hex_to_char(std::string s, int width=4) //, std::string mark
|
||||
std::string process_diacritics(std::string s)
|
||||
{
|
||||
(void)K::log(4);
|
||||
std::regex diacritic_re("\\^([^\\s`'~@|^:*#])([" + diacritic_symbols + "])");
|
||||
// The base may not be whitespace, a digit, or ASCII punctuation (the
|
||||
// four ranges !-/ :-@ [-` {-~): a diacritic sits on a letter. Without
|
||||
// the exclusion, ^ before a quoted punctuation character followed by a
|
||||
// mark character composed nonsense -- ^-- became a hyphen with a macron
|
||||
// instead of a literal hyphen before a hyphen. A multi-byte (non-ASCII)
|
||||
// base is unaffected: its bytes are outside every excluded range.
|
||||
std::regex diacritic_re("\\^([^\\s0-9!-/:-@\\[-`{-~])([" + diacritic_symbols + "])");
|
||||
|
||||
std::string result {s};
|
||||
std::sregex_iterator end {};
|
||||
@@ -128,7 +137,12 @@ std::string process_unicode_codepoint(std::string s)
|
||||
std::sregex_iterator end {};
|
||||
for (std::sregex_iterator p { s.begin(), s.end(), unicode_re }; p!= end; ++p) {
|
||||
std::regex hit_re { regex_escape((*p)[0].str()) };
|
||||
result = std::regex_replace(result, hit_re, unicode_hex_to_char((*p)[1].str()));
|
||||
// Convert the captured hex DIRECTLY: unicode_hex_to_char() re-scans
|
||||
// its argument at a fixed width, and its 4-digit default truncated a
|
||||
// 5-digit code point to its first four digits (^13000^ rendered as
|
||||
// U+1300 followed by a literal "0").
|
||||
result = std::regex_replace(result, hit_re,
|
||||
utf8char(std::stoi((*p)[1].str(), nullptr, 16)));
|
||||
}
|
||||
return result;
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
#include <regex>
|
||||
#include <unistd.h>
|
||||
|
||||
const std::regex unicode_re(R"(\^(([0-9A-Fa-f]{5})|([0-9A-Fa-f]{4})|([0-9A-Fa-f]))\^)");
|
||||
// ^UUUU^ — a Unicode code point in hex, 4 to 6 digits (^263A^ is the BMP,
|
||||
// ^13000^ EGYPTIAN HIEROGLYPH A001, ^10FFFD^ the top of the range), or a
|
||||
// single digit. Lengths 2-3 are NOT accepted: two- and three-letter
|
||||
// sequences of a-f collide with the ^s^-style mnemonic names.
|
||||
const std::regex unicode_re(R"(\^(([0-9A-Fa-f]{4,6})|([0-9A-Fa-f]))\^)");
|
||||
const std::regex unicode_hide_re(R"(=([0-9A-Fa-f]{2})=)");
|
||||
const std::regex hex2_re(R"(([0-9A-Fa-f]{2}))");
|
||||
const std::regex hex4_re(R"(([0-9A-Fa-f]{4}))");
|
||||
|
||||
@@ -9,7 +9,16 @@ using namespace std::string_literals;
|
||||
|
||||
fs::path construct_command_pathname(char* command)
|
||||
{
|
||||
return fs::path(fs::current_path().string() + "/" + std::string(command));
|
||||
// The pseudo source file for command-line string input (-s, kdiag's
|
||||
// input): a file IN THE CWD named after the command, so that a relative
|
||||
// @read in string input resolves against the directory the user ran the
|
||||
// command from -- the string-input analog of "a document's relative
|
||||
// names resolve against the document". The basename matters: the
|
||||
// former "<cwd>/<argv[0]>" string concatenation anchored resolution at
|
||||
// the BINARY's directory whenever argv[0] was absolute
|
||||
// (/usr/local/bin/ktext, a wrapper script), so every relative @read in
|
||||
// -s input searched bin/ instead of the cwd.
|
||||
return fs::current_path() / fs::path(command).filename();
|
||||
}
|
||||
|
||||
void set_verbose_level(int argc, char* argv[])
|
||||
@@ -124,7 +133,7 @@ void load_klammersets(Machine& machine, const strings_t& symbols)
|
||||
"The klammerset \"none\" cannot be combined with other klammersets: "
|
||||
"it means that none is loaded. Give \"none\" alone, or name only the "
|
||||
"klammersets to load.",
|
||||
Locator());
|
||||
Locator::none());
|
||||
}
|
||||
if (symbols.empty()) {
|
||||
// Which klammerset was loaded, and from where, is a DERIVED value: the
|
||||
@@ -146,7 +155,7 @@ void load_klammersets(Machine& machine, const strings_t& symbols)
|
||||
continue;
|
||||
}
|
||||
std::string klammerset_filename = resolve_klammerset_symbol(
|
||||
symbol, machine.m_state.value("K_input_dir"), Locator()).string();
|
||||
symbol, machine.m_state.value("K_input_dir"), Locator::none()).string();
|
||||
// Symbol -> file is the search path's answer, and the search
|
||||
// path has three stages with shadowing: the file it landed on is
|
||||
// exactly what a user cannot read off "--klammersets x".
|
||||
|
||||
@@ -23,7 +23,6 @@ void Error::print_message(const std::string& epilog)
|
||||
if (epilog != "") {
|
||||
m_desc += "\n\n" + epilog + "\n";
|
||||
}
|
||||
if (m_just)
|
||||
m_desc = justify(m_desc, 80, 0);
|
||||
std::cerr << "\n" << red << command_name << " (" << m_type << " error)";
|
||||
if (display_source(m_loc.m_filename)) {
|
||||
|
||||
40
mac/error.h
40
mac/error.h
@@ -11,11 +11,10 @@ inline std::string command_pathname { "Pathname of command executed on the comma
|
||||
class Error : std::exception {
|
||||
public:
|
||||
Error(const std::string& error_type, const std::string& description,
|
||||
const Locator& locator = Locator(), bool do_justify = true)
|
||||
const Locator& locator = Locator())
|
||||
: m_type(error_type)
|
||||
, m_desc(description)
|
||||
, m_loc(locator)
|
||||
, m_just(do_justify)
|
||||
{}
|
||||
|
||||
void print_message(const std::string& epilog="");
|
||||
@@ -23,57 +22,56 @@ public:
|
||||
std::string m_type {};
|
||||
std::string m_desc {};
|
||||
Locator m_loc; // {};
|
||||
bool m_just { true };
|
||||
};
|
||||
|
||||
|
||||
class Parsing_error : public Error {
|
||||
public:
|
||||
explicit Parsing_error(
|
||||
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
|
||||
: Error("parsing", description, locator, do_justify) {};
|
||||
const std::string& description, const Locator& locator=Locator())
|
||||
: Error("parsing", description, locator) {};
|
||||
};
|
||||
|
||||
class File_error : public Error {
|
||||
public:
|
||||
explicit File_error(
|
||||
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
|
||||
: Error("file", description, locator, do_justify) {};
|
||||
const std::string& description, const Locator& locator=Locator())
|
||||
: Error("file", description, locator) {};
|
||||
};
|
||||
|
||||
class Target_error : public Error {
|
||||
public:
|
||||
explicit Target_error(
|
||||
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
|
||||
: Error("target", description, locator, do_justify) {};
|
||||
const std::string& description, const Locator& locator=Locator())
|
||||
: Error("target", description, locator) {};
|
||||
};
|
||||
|
||||
class Klammerset_error : public Error {
|
||||
public:
|
||||
explicit Klammerset_error(
|
||||
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
|
||||
: Error("klammerset", description, locator, do_justify) {};
|
||||
const std::string& description, const Locator& locator=Locator())
|
||||
: Error("klammerset", description, locator) {};
|
||||
};
|
||||
|
||||
class Definition_error : public Error {
|
||||
public:
|
||||
explicit Definition_error(
|
||||
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
|
||||
: Error("definition", description, locator, do_justify) {};
|
||||
const std::string& description, const Locator& locator=Locator())
|
||||
: Error("definition", description, locator) {};
|
||||
};
|
||||
|
||||
class Argument_error : public Error {
|
||||
public:
|
||||
explicit Argument_error(
|
||||
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
|
||||
: Error("argument", description, locator, do_justify) {};
|
||||
const std::string& description, const Locator& locator=Locator())
|
||||
: Error("argument", description, locator) {};
|
||||
};
|
||||
|
||||
class Environment_error : public Error {
|
||||
public:
|
||||
explicit Environment_error(
|
||||
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
|
||||
: Error("environment", description, locator, do_justify) {};
|
||||
const std::string& description, const Locator& locator=Locator())
|
||||
: Error("environment", description, locator) {};
|
||||
};
|
||||
|
||||
// Klammer application nested deeper than the engine's limit. Raised by the
|
||||
@@ -83,13 +81,13 @@ public:
|
||||
class Recursion_error : public Error {
|
||||
public:
|
||||
explicit Recursion_error(
|
||||
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
|
||||
: Error("recursion", description, locator, do_justify) {};
|
||||
const std::string& description, const Locator& locator=Locator())
|
||||
: Error("recursion", description, locator) {};
|
||||
};
|
||||
|
||||
class Internal_error : public Error {
|
||||
public:
|
||||
explicit Internal_error(
|
||||
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
|
||||
: Error("internal", description, locator, do_justify) {};
|
||||
const std::string& description, const Locator& locator=Locator())
|
||||
: Error("internal", description, locator) {};
|
||||
};
|
||||
|
||||
31
mac/eval.cpp
31
mac/eval.cpp
@@ -42,8 +42,8 @@ std::string shell(State state, std::string command, Locator loc)
|
||||
err_template.push_back('\0');
|
||||
int err_fd = mkstemp(err_template.data());
|
||||
if (err_fd == -1) {
|
||||
throw Environment_error("Could not create a temporary file for the "
|
||||
"command's error output", loc, false);
|
||||
throw Environment_error(
|
||||
"Could not create a temporary file for the command's error output", loc);
|
||||
}
|
||||
close(err_fd);
|
||||
err_path = err_template.data();
|
||||
@@ -55,7 +55,7 @@ std::string shell(State state, std::string command, Locator loc)
|
||||
if (!pipe) {
|
||||
fs::remove(err_path);
|
||||
throw Environment_error(
|
||||
"Could not run command:\n" + command, loc, false);
|
||||
"Could not run command:\n " + command, loc);
|
||||
}
|
||||
char buffer[128];
|
||||
std::string result = "";
|
||||
@@ -87,7 +87,7 @@ std::string shell(State state, std::string command, Locator loc)
|
||||
// "environment", not "parsing": the failure is OUTSIDE Klammertext,
|
||||
// in the command the document invoked -- the same class as a missing
|
||||
// xelatex or an unset environment variable.
|
||||
throw Environment_error(ss.str(), loc, false);
|
||||
throw Environment_error(ss.str(), loc);
|
||||
}
|
||||
if (!error_output.empty()) {
|
||||
(void)K::log(1, "shell command wrote to stderr:", command,
|
||||
@@ -122,8 +122,8 @@ std::string run_haskell(const std::string& hsfile, Locator loc)
|
||||
err_template.push_back('\0');
|
||||
int err_fd = mkstemp(err_template.data());
|
||||
if (err_fd == -1) {
|
||||
throw Environment_error("Could not create a temporary file for runghc's "
|
||||
"error output", loc, false);
|
||||
throw Environment_error(
|
||||
"Could not create a temporary file for runghc's error output", loc);
|
||||
}
|
||||
close(err_fd);
|
||||
err_path = err_template.data();
|
||||
@@ -132,7 +132,7 @@ std::string run_haskell(const std::string& hsfile, Locator loc)
|
||||
FILE* pipe = popen(command.c_str(), "r");
|
||||
if (!pipe) {
|
||||
fs::remove(err_path);
|
||||
throw Environment_error("Could not run runghc.", loc, false);
|
||||
throw Environment_error("Could not run runghc.", loc);
|
||||
}
|
||||
char buffer[128];
|
||||
std::string result = "";
|
||||
@@ -154,12 +154,12 @@ std::string run_haskell(const std::string& hsfile, Locator loc)
|
||||
}
|
||||
ss << ".";
|
||||
if (!error_output.empty()) {
|
||||
ss << "\nrunghc reported:\n" << error_output;
|
||||
ss << "\n\nrunghc reported:\n " << error_output;
|
||||
}
|
||||
// A compile error arrives here, and so does a program that ran and then
|
||||
// exited nonzero; the message does not guess which, it shows what
|
||||
// runghc said.
|
||||
throw Environment_error(ss.str(), loc, false);
|
||||
throw Environment_error(ss.str(), loc);
|
||||
}
|
||||
if (!error_output.empty()) {
|
||||
// GHC's warnings, and anything the program itself wrote to stderr.
|
||||
@@ -175,9 +175,9 @@ std::string haskell(State state, std::string code, Locator loc)
|
||||
// "environment", not "parsing": a missing external command is the same
|
||||
// class as a missing xelatex, and "parsing error" misdescribes it.
|
||||
throw Environment_error(
|
||||
"@eval with the :haskell argument requires runghc, which was not found in PATH.\n"
|
||||
"@eval with the :haskell argument requires runghc, which was not found in PATH.\n\n"
|
||||
"Install it using GHCup; see https://www.haskell.org/ghcup/install/.",
|
||||
loc, false);
|
||||
loc);
|
||||
}
|
||||
code = state.subst(code);
|
||||
|
||||
@@ -193,7 +193,7 @@ std::string haskell(State state, std::string code, Locator loc)
|
||||
int fd = mkstemp(tmppath.data());
|
||||
if (fd < 0) {
|
||||
throw Environment_error(
|
||||
"Could not create temporary file for Haskell evaluation.", loc, false);
|
||||
"Could not create temporary file for Haskell evaluation.", loc);
|
||||
}
|
||||
std::string hsfile = std::string(tmppath.data()) + ".hs";
|
||||
close(fd);
|
||||
@@ -202,8 +202,7 @@ std::string haskell(State state, std::string code, Locator loc)
|
||||
FILE* f = fopen(hsfile.c_str(), "w");
|
||||
if (!f) {
|
||||
unlink(hsfile.c_str());
|
||||
throw Parsing_error(
|
||||
"Could not write temporary Haskell file.", loc, false);
|
||||
throw Parsing_error("Could not write temporary Haskell file.", loc);
|
||||
}
|
||||
fprintf(f, "%s\n", code.c_str());
|
||||
fclose(f);
|
||||
@@ -222,7 +221,7 @@ void check_cpp_arguments(katom_list args, Locator loc)
|
||||
<< "or\n"
|
||||
<< " @eval :cpp <library-basename> <function-name> @\n"
|
||||
<< "In the first case, the library basename is used for the function name.";
|
||||
throw Argument_error(ss.str(), loc, false);
|
||||
throw Argument_error(ss.str(), loc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +278,7 @@ std::string Eval::eval_command(katom_iter begin, katom_iter end)
|
||||
if (dir.empty() || !fs::is_directory(dir)) {
|
||||
throw Argument_error(
|
||||
"The :cwd directory does not exist: \"" + dir + "\"",
|
||||
begin->m_loc, false);
|
||||
begin->m_loc);
|
||||
}
|
||||
cwd_guard.emplace(dir);
|
||||
first = resume;
|
||||
|
||||
@@ -34,7 +34,7 @@ std::string Eval_cpp::eval(const fs::path& library_path, const std::string& func
|
||||
std::stringstream ss {};
|
||||
ss << "Cannot load symbol " << function_name
|
||||
<< " from library " << library_path << ":\n " << error_desc;
|
||||
throw File_error(ss.str(), m_loc, false);
|
||||
throw File_error(ss.str(), m_loc);
|
||||
}
|
||||
std::string result = func(m_machine);
|
||||
dlclose(handle);
|
||||
|
||||
@@ -40,6 +40,21 @@ Eval_python::Eval_python(Machine& machine, const Locator& loc)
|
||||
Py_DECREF(result_text);
|
||||
}
|
||||
import_module("inspect", false);
|
||||
// The :after_apply phase dispatcher: a phase that declares a K
|
||||
// parameter receives the state's class K; one that does not (a stdlib
|
||||
// function like string.capwords, whose second parameter is a separator)
|
||||
// is called with the text alone. Discrimination is by parameter NAME,
|
||||
// not count -- capwords has two parameters and K is not one of them.
|
||||
// Builtins whose signature inspect cannot read are treated as not
|
||||
// wanting K.
|
||||
PyRun_String(
|
||||
"def K_phase_call(f, text, K):\n"
|
||||
" try:\n"
|
||||
" wants = 'K' in inspect.signature(f).parameters\n"
|
||||
" except (ValueError, TypeError):\n"
|
||||
" wants = False\n"
|
||||
" return f(text, K=K) if wants else f(text)\n",
|
||||
Py_file_input, m_globals, m_globals);
|
||||
if (!m_machine.m_state.m_frames.empty()) {
|
||||
PyRun_String(m_machine.m_state.python_code().c_str(), Py_file_input, m_globals, m_locals);
|
||||
}
|
||||
@@ -154,7 +169,7 @@ void Eval_python::import_module(const std::string& module_name, bool verify)
|
||||
if (!detail.empty()) {
|
||||
message += ":\n\n" + detail;
|
||||
}
|
||||
throw Argument_error(message, m_loc, false);
|
||||
throw Argument_error(message, m_loc);
|
||||
}
|
||||
// PyDict_SetItemString steals a reference, so we don't need to DECREF module
|
||||
// The dictionary will own the reference
|
||||
|
||||
32
mac/file.cpp
32
mac/file.cpp
@@ -160,7 +160,6 @@ strings_t group_filename_tokens(const strings_t& tokens, const std::string& base
|
||||
// name exists. A name that never resolves is kept as given, so the
|
||||
// missing-file error downstream reports what the user wrote.
|
||||
size_t i = 0;
|
||||
bool reported = false; // at most one "tried" report per list
|
||||
while (i < tokens.size()) {
|
||||
if (filename_exists(tokens[i], base_dir)) {
|
||||
result.push_back(tokens[i]);
|
||||
@@ -188,27 +187,14 @@ strings_t group_filename_tokens(const strings_t& tokens, const std::string& base
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// Nothing resolved. The groupings that were TRIED are what
|
||||
// the user needs at verbosity 0, because the downstream error
|
||||
// names only the first token and cannot say why the others
|
||||
// were not joined to it. Recorded on the name so the caller
|
||||
// can report them with the missing-file error.
|
||||
strings_t tried {};
|
||||
std::string acc2 = tokens[i];
|
||||
tried.push_back(q_(acc2));
|
||||
for (size_t k = i + 1; k < tokens.size(); ++k) {
|
||||
acc2 += " " + tokens[k];
|
||||
tried.push_back(q_(acc2));
|
||||
}
|
||||
// Once: the first unresolved token's list already shows
|
||||
// every joining from that point on, and one report per
|
||||
// leftover word buries it.
|
||||
if (tried.size() > 1 && !reported) {
|
||||
std::cerr << command_name << ": no file matches "
|
||||
<< q_(tokens[i]) << "; tried "
|
||||
<< join(tried, ", ") << "\n";
|
||||
reported = true;
|
||||
}
|
||||
// Nothing resolved: the token passes through as written and
|
||||
// its DOWNSTREAM OWNER decides -- a bare :files word is the
|
||||
// kt/ shortcut (which resolves or errors there), a missing
|
||||
// file is a located error. A failed rescue itself says
|
||||
// NOTHING at any verbosity (2026-08-22): every outcome is
|
||||
// already either the -v 1 resolution line or an error that
|
||||
// stops the run, so a tried-list adds noise to the first and
|
||||
// nothing to the second.
|
||||
result.push_back(tokens[i]);
|
||||
++i;
|
||||
}
|
||||
@@ -358,7 +344,7 @@ std::string find_file(const std::string& basename, strings_t search_path, bool e
|
||||
std::sort(search_path.begin(), search_path.end());
|
||||
ss << "File with basename \"" << basename << "\" not found in search path:\n "
|
||||
<< join(search_path, "\n ");
|
||||
throw File_error(ss.str(), Locator(), false);
|
||||
throw File_error(ss.str(), Locator());
|
||||
}
|
||||
return pathname;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ void warn_unparsed_katoms(katom_list& katoms, bool warn)
|
||||
// commands issue has one format: it said "[warning]" where
|
||||
// everything else says "(warning)".
|
||||
warning("Word not parsed:\n " + k.m_text
|
||||
+ "\nTo include a special character (@, |, #, and ^), put "
|
||||
+ "\nTo include a punctuation character literally, put "
|
||||
"\"^\" before it.",
|
||||
k.m_loc);
|
||||
k.m_unparsed = false;
|
||||
|
||||
@@ -114,7 +114,7 @@ void missing_open(const Katom& k, bool error_exit)
|
||||
std::stringstream ss {};
|
||||
ss << "A klammer ends without a beginning: " << k;
|
||||
if (error_exit) {
|
||||
throw Parsing_error(ss.str(), k.m_loc, false);
|
||||
throw Parsing_error(ss.str(), k.m_loc);
|
||||
} else {
|
||||
std::cout << " " << ss.str() << "\n";
|
||||
}
|
||||
@@ -134,7 +134,7 @@ void missing_close(const katom_list& bounds, bool error_exit)
|
||||
ss << " " << k.m_loc << " " << k.m_src << "\n";
|
||||
}
|
||||
if (error_exit) {
|
||||
throw Parsing_error(ss.str(), bounds[0].m_loc, false);
|
||||
throw Parsing_error(ss.str(), bounds[0].m_loc);
|
||||
} else {
|
||||
std::cout << ss.str() << "\n";
|
||||
}
|
||||
@@ -148,7 +148,7 @@ void bad_close(const Katom& open, const Katom& close, bool error_exit)
|
||||
<< " " << open.m_loc << " " << open << "\n"
|
||||
<< " " << close.m_loc << " " << close;
|
||||
if (error_exit) {
|
||||
throw Parsing_error(ss.str(), open.m_loc, false);
|
||||
throw Parsing_error(ss.str(), open.m_loc);
|
||||
} else {
|
||||
std::cout << ss.str() << "\n";
|
||||
}
|
||||
@@ -174,7 +174,7 @@ void check_named_katom_span(const Katom& begin, const Katom& end)
|
||||
ss << " A named end katom does not match:\n"
|
||||
<< " " << begin.m_loc << " " << begin << "\n"
|
||||
<< " " << end.m_loc << " " << end;
|
||||
throw Parsing_error(ss.str(), end.m_loc, false);
|
||||
throw Parsing_error(ss.str(), end.m_loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,6 +370,24 @@ void hide_special_katoms(katom_list& katoms)
|
||||
definition_depth == 0 && code_depth == 0) {
|
||||
k.m_text = hide_structural_characters(k.m_text);
|
||||
}
|
||||
// "^" before ANY punctuation character quotes it, not only the six
|
||||
// Klammertext specials the katomizer knows: ^- is a literal hyphen
|
||||
// (no dash transform), ^~ a literal tilde. The pair becomes the
|
||||
// KTESC marker of the character here -- text-level, on writer-text
|
||||
// katoms only, under the same span skips as the quoted specials
|
||||
// above -- so code inside an @eval keeps its carets (grep '^-'
|
||||
// reaches the shell intact) and ^'...'^ content (katom_t::literal,
|
||||
// marked before this pass runs) stays exactly as typed. How the
|
||||
// quoted character renders is the target's decision: the :escape
|
||||
// and :resolve tables map its marker, and an unmapped marker
|
||||
// decodes to the character itself. A katom this converts is no
|
||||
// longer unparsed (the ^- warning would otherwise misfire).
|
||||
if ((k.m_type == katom_t::word || k.m_type == katom_t::text) &&
|
||||
definition_depth == 0 && code_depth == 0) {
|
||||
if (hide_quoted_punctuation(k.m_text)) {
|
||||
k.m_unparsed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +474,7 @@ void check_bar_count(katom_iter begin, katom_iter end)
|
||||
ss << "Incorrectly formatted @cond klammer. There should only be one or two bar characters:\n"
|
||||
<< " @cond <predicate> | <result-if-true @\nor:\n"
|
||||
<< " @cond <predicate> | <result-if-true> | <result-if-false> @";
|
||||
throw Argument_error(ss.str(), begin->m_loc, false);
|
||||
throw Argument_error(ss.str(), begin->m_loc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ parse_name(const Target_registry& targets, const Katom& name_katom)
|
||||
|
||||
if (!std::regex_match(name_with_target, match, Klammer::name_re)) {
|
||||
throw Parsing_error(
|
||||
"The klammer name \"" + name_with_target + "\" is not correctly defined. "
|
||||
"The klammer name \"" + name_with_target + "\" is not correctly defined.\n\n"
|
||||
"The form is \"<klammer-name>\" for general klammers or \"<klammer-name>.<target-name>\" "
|
||||
"for a specialized target. Several targets that share one body are written as a "
|
||||
"comma-separated list: \"<klammer-name>.<target-name>,<target-name>\". The klammer "
|
||||
@@ -44,7 +44,7 @@ parse_name(const Target_registry& targets, const Katom& name_katom)
|
||||
if (!targets.has(target_name)) {
|
||||
throw Target_error(
|
||||
"The target \"" + target_name + "\" in klammer definition \"" + name_with_target + "\" "
|
||||
"is not defined. Enter \"kdesc -t\" to see the targets defined by the Standard Klammer Set.",
|
||||
"is not defined.\n\nEnter \"kdesc -t\" to see the targets defined by the Standard Klammer Set.",
|
||||
name_katom.m_loc);
|
||||
}
|
||||
if (is_in(target_name, seen)) {
|
||||
@@ -105,7 +105,7 @@ parse_definition_katoms(
|
||||
" @@<name>[.<target>] :: <body> @@ instance (uses .k parameters)\n"
|
||||
" @@<name>[.<target>] ::: <body> @@ override existing definition\n"
|
||||
" @@<name>[.<target>] <parameters> :::: <body> @@ default (can be overridden)",
|
||||
begin->m_loc, false);
|
||||
begin->m_loc);
|
||||
|
||||
}
|
||||
katom_list parameter_katoms(begin, deftype);
|
||||
@@ -274,7 +274,7 @@ void Klammer::disallow_instances() //Klammer::components declaration)
|
||||
<< plural("instance", icount) << " (defined by \"::\"), but "
|
||||
<< "no declarations (defined by a \".k\" target)";
|
||||
throw Definition_error(
|
||||
error_list(ss.str(), instances), instances[0].loc, false);
|
||||
error_list(ss.str(), instances), instances[0].loc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ bool Klammer::copy_to_instances(const Target_registry& targets)
|
||||
<< plural("instance", icount) << " (defined by \"::\"), but "
|
||||
<< dcount << " "<< plural("definition", dcount) << " (defined by \":\")";
|
||||
throw Definition_error(
|
||||
error_list(ss.str(), definitions), definitions[0].loc, false);
|
||||
error_list(ss.str(), definitions), definitions[0].loc);
|
||||
} else {
|
||||
copy_components(definitions[0].parameters, m_defs, targets);
|
||||
return true;
|
||||
@@ -336,7 +336,7 @@ void Klammer::check_for_multiple_general_klammers()
|
||||
error_list(
|
||||
"There is more than one general klammer (a klammer in which no target is defined)",
|
||||
general_klammers),
|
||||
general_klammers[0].loc, false);
|
||||
general_klammers[0].loc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +362,7 @@ void Klammer::check_for_declaration_and_definitions()
|
||||
".\nWrite \"::\" instead of \":\" so the definition takes its parameters "
|
||||
"from the declaration",
|
||||
definitions),
|
||||
declares[0].loc, false);
|
||||
declares[0].loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,7 +384,7 @@ void Klammer::copy_general_klammer_to_undefined(const Target_registry& targets)
|
||||
throw Definition_error(
|
||||
error_list("The general parameters are different than the defined parameters",
|
||||
general_klammers),
|
||||
m_parameters.m_katoms[0].m_loc, false);
|
||||
m_parameters.m_katoms[0].m_loc);
|
||||
}
|
||||
}
|
||||
const auto& [target, deftype, parameters, body, varmap, loc] = general_klammers[0];
|
||||
@@ -470,19 +470,19 @@ void Klammer::no_declarations(const Target_registry& targets)
|
||||
// definitions by hand to find which one drifted.
|
||||
std::stringstream ss {};
|
||||
ss << "The parameters of klammer \"" << m_name
|
||||
<< "\" are not the same for every target,\n"
|
||||
<< "\" are not the same for every target, "
|
||||
"and there is no declaration (.k) target to define them once:\n";
|
||||
for (const auto& def : m_defs) {
|
||||
if (std::ranges::find(target_names, def.target) == target_names.end())
|
||||
continue;
|
||||
std::string target = def.target;
|
||||
target.resize(std::max(target.size(), size_t(6)), ' ');
|
||||
ss << " " << target << " " << parameter_signature(def.parameters)
|
||||
target.resize(std::max(target.size(), size_t(4)), ' ');
|
||||
ss << " Target: " << target << " Parameters: " << parameter_signature(def.parameters)
|
||||
<< "\n " << def.loc.desc() << "\n";
|
||||
}
|
||||
ss << "Use a .k target to declare the parameters and describe the klammer,\n"
|
||||
"and \"::\" with no parameters for each target's definition.";
|
||||
throw Definition_error(ss.str(), m_defs[0].loc, false);
|
||||
throw Definition_error(ss.str(), m_defs[0].loc);
|
||||
} else {
|
||||
// std::cout << " All equal\n";
|
||||
copy_components(m_defs[0].parameters, m_defs, targets);
|
||||
@@ -506,7 +506,7 @@ void Klammer::many_declarations(const std::vector<Klammer::components>& declares
|
||||
// std::cout << boldblack << "Many declarations\n" << black;
|
||||
throw Definition_error(
|
||||
error_list("More than one declaration (.k) klammer", declares),
|
||||
declares[0].loc, false);
|
||||
declares[0].loc);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -206,9 +206,9 @@ fs::path resolve_klammerset_symbol(
|
||||
}
|
||||
throw Klammerset_error(
|
||||
"The klammerset \"" + symbol + "\" was not found. A symbol x names the "
|
||||
"declaration file x/x.k in one of the search directories: "
|
||||
+ join(dirs, ", ")
|
||||
+ ". Enter \"kdesc --klammersets\" to list the available klammersets.",
|
||||
"declaration file x/x.k in one of the search directories:\n "
|
||||
+ join(dirs, "\n ")
|
||||
+ "\nEnter \"kdesc --klammersets\" to list the available klammersets.",
|
||||
loc);
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ std::vector<Ktype> katom_types {
|
||||
Ktype(katom_t::ws_newline, "ws-newline", ws_newline_s, "Remove all whitespace, leaving <number> newlines (default: 1)"),
|
||||
Ktype(katom_t::special, "special-char", R"(\^[@|#^:*])", "Klammertext special character treated as regular text"),
|
||||
//Ktype(katom_t::nonascii, "non-ascii-char", R"(\^\w(?:.|\^))", "Non-ASCII character"),
|
||||
Ktype(katom_t::nonascii, "non-ascii-char", R"((\^(\w(?:.|\^)))|(\^[A-Fa-f0-9]{1,5}\^))", "Non-ASCII character"),
|
||||
Ktype(katom_t::nonascii, "non-ascii-char", R"((\^(\w(?:.|\^)))|(\^[A-Fa-f0-9]{1,6}\^))", "Non-ASCII character"),
|
||||
|
||||
// Ktype(katom_t::word, "word", R"([a-z][a-z0-9_]*)", "Lower-case letters, numbers, or underscore"),
|
||||
// Ktype(katom_t::text, "text",
|
||||
|
||||
@@ -24,6 +24,14 @@ public:
|
||||
, m_chr(int(location.column()))
|
||||
{};
|
||||
Locator(const fs::path& filename, int line, int chr);
|
||||
// "No location": for errors with no document position -- command-line
|
||||
// argument errors, lookups made on the command's behalf. NOT the same
|
||||
// as Locator(): the default constructor captures the C++ CALL SITE via
|
||||
// std::source_location (and a defaulted Locator parameter captures the
|
||||
// CALLER), which is what leaked "argv.cpp, line 597" into user-facing
|
||||
// errors (found by the error gallery, 2026-08-22). Locator() is for
|
||||
// logging; errors either carry a real document location or this.
|
||||
static Locator none() { return Locator(fs::path{}, -1, -1); }
|
||||
std::string str(bool relative = false) const;
|
||||
std::string desc(bool relative = false) const;
|
||||
std::string abbrev(bool include_chr=true) const;
|
||||
|
||||
@@ -61,10 +61,10 @@ public:
|
||||
if (apply_depth >= max_apply_depth) {
|
||||
std::stringstream ss {};
|
||||
ss << "Klammer application nested more than " << max_apply_depth
|
||||
<< " levels deep while applying " << q_(name) << ".\n"
|
||||
<< " levels deep while applying " << q_(name) << "."
|
||||
<< "A klammer that applies itself, directly or through a cycle "
|
||||
<< "of klammers, does not terminate.";
|
||||
throw Recursion_error(ss.str(), loc, false);
|
||||
throw Recursion_error(ss.str(), loc);
|
||||
}
|
||||
++apply_depth;
|
||||
}
|
||||
@@ -132,7 +132,7 @@ void check_bar_count(katom_iter begin, std::size_t count)
|
||||
ss << "Incorrectly formatted @cond klammer. There should only be one or two bar characters:\n"
|
||||
<< " @cond <predicate> | <result-if-true @\nor:\n"
|
||||
<< " @cond <predicate> | <result-if-true> | <result-if-false> @";
|
||||
throw Argument_error(ss.str(), begin->m_loc, false);
|
||||
throw Argument_error(ss.str(), begin->m_loc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -749,24 +749,23 @@ katom_list Machine::apply_klammer(
|
||||
// remain in the klammer body substitution for final target-specific output.
|
||||
katom_list result(klammer.m_body[target].begin(), klammer.m_body[target].end());
|
||||
auto varmap = klammer.m_varmap[target];
|
||||
m_state.open_frame("Arguments for klammer " + q_(klammer.m_name));
|
||||
m_state.set(values, klammer.m_parameters);
|
||||
for (const auto& [name, indices] : varmap) {
|
||||
std::regex arg("\\*" + name + "\\*");
|
||||
for (auto i : indices) {
|
||||
result[i].m_text = std::regex_replace(result[i].m_text, arg, m_state.value(name));
|
||||
result[i].m_type = katom_t::text;
|
||||
}
|
||||
}
|
||||
// Escape target-specific characters (e.g. tex "&" -> "\&") in the writer
|
||||
// text of a GENERAL klammer's body. Runs BEFORE process_katoms/apply()
|
||||
// below expand the body, so that target-native markup pulled in by nested
|
||||
// klammers (e.g. nl.tex -> "\newline") is left untouched -- only this
|
||||
// klammer's own literal writer text is escaped here; nested klammers escape
|
||||
// theirs when they are applied in turn. Bodies from target-specific
|
||||
// definitions (m_body_generic[target] == false) are already in target form
|
||||
// and skipped. KTESC markers are idempotent, so text already escaped at the
|
||||
// top level passes through unchanged. Two kinds of body content are NOT
|
||||
// text of a GENERAL klammer's body. Runs BEFORE the *arg* substitution
|
||||
// below: an argument value is writer text already escaped at the top
|
||||
// level (KTESC markers, idempotent) plus final target markup from
|
||||
// klammers the writer nested in the argument, and neither may be escaped
|
||||
// here -- escaping after substitution swept both, so a general klammer
|
||||
// with a klammer-bearing argument emitted \textbackslash{}emph{...}
|
||||
// (fixed 2026-08-19; tst/escape_test.sh cases 26-29). A karg katom is
|
||||
// skipped by the type filter; a variable inside a mixed text katom
|
||||
// survives because no target declares '*' or identifier characters as
|
||||
// escapes. Also runs BEFORE process_katoms/apply() expand the body, so
|
||||
// that target-native markup pulled in by nested klammers (e.g. nl.tex ->
|
||||
// "\newline") is left untouched -- only this klammer's own literal writer
|
||||
// text is escaped here; nested klammers escape theirs when they are
|
||||
// applied in turn. Bodies from target-specific definitions
|
||||
// (m_body_generic[target] == false) are already in target form and
|
||||
// skipped. Two kinds of body content are NOT
|
||||
// writer text and must be skipped:
|
||||
// * ^'...'^ literal spans -- raw target markup the writer typed directly.
|
||||
// At this point they are typed literal_begin/literal_end with plain-text
|
||||
@@ -818,6 +817,16 @@ katom_list Machine::apply_klammer(
|
||||
}
|
||||
}
|
||||
}
|
||||
m_state.open_frame("Arguments for klammer " + q_(klammer.m_name));
|
||||
m_state.set(values, klammer.m_parameters);
|
||||
for (const auto& [name, indices] : varmap) {
|
||||
std::regex arg("\\*" + name + "\\*");
|
||||
for (auto i : indices) {
|
||||
result[i].m_text = std::regex_replace(
|
||||
result[i].m_text, arg, m_state.value(name, true, result[i].m_loc));
|
||||
result[i].m_type = katom_t::text;
|
||||
}
|
||||
}
|
||||
process_katoms(result, klammer.m_name);
|
||||
apply(m_klammers, result, target);
|
||||
m_state.close_frame();
|
||||
@@ -876,7 +885,18 @@ std::string Machine::run_phase_functions()
|
||||
// phase sees its predecessor's result in K_result.
|
||||
Eval E(*this, Locator());
|
||||
if (!f.empty() && f[0] != ':') {
|
||||
f += "(K_result)";
|
||||
// K reaches a phase only as a parameter: the state's
|
||||
// "class K" exists in the EVAL's globals (python_code()
|
||||
// defines it), but a phase function's body resolves names in
|
||||
// its own MODULE's globals. Every phase signature has
|
||||
// carried "K=None" for this since the calling convention was
|
||||
// created; the call never passed it, which surfaced when
|
||||
// txt_justify_blocks needed Target_txt_width (2026-08-22).
|
||||
// K_phase_call (defined in Eval_python's globals) passes K
|
||||
// only to a function that DECLARES a K parameter, so a
|
||||
// stdlib phase (string.capwords, whose second parameter is a
|
||||
// separator) keeps working.
|
||||
f = "K_phase_call(" + f + ", K_result, K)";
|
||||
}
|
||||
f = "@eval " + f + " @";
|
||||
auto katoms = katomize(line_split(f), "phase");
|
||||
@@ -916,7 +936,7 @@ std::string Machine::apply(const std::string& target_name, bool final_processing
|
||||
// Characters produced later by klammer bodies will not be escaped.
|
||||
// Skipped for sub-Machine apply() calls (e.g., from @eval), where the
|
||||
// text is already in target-specific form.
|
||||
auto target = m_targets.get(target_name, Locator());
|
||||
auto target = m_targets.get(target_name, Locator::none());
|
||||
if (escape_characters)
|
||||
escape_target_characters(target, m_katoms);
|
||||
|
||||
@@ -935,18 +955,24 @@ std::string Machine::apply(const std::string& target_name, bool final_processing
|
||||
if (++apply_count > apply_round_limit) {
|
||||
std::stringstream ss {};
|
||||
ss << "Klammer application did not reach a fixed point after "
|
||||
<< apply_round_limit << " rounds.\n"
|
||||
<< apply_round_limit << " rounds.\n\n"
|
||||
<< "Each round applies every klammer present; a klammer whose "
|
||||
<< "result contains further klammers starts another round.";
|
||||
throw Recursion_error(ss.str(), Locator(), false);
|
||||
throw Recursion_error(ss.str(), Locator());
|
||||
}
|
||||
}
|
||||
// Typographic transforms run on the katoms, not the joined string, so
|
||||
// that ^'...'^ literal content (katom_t::literal) stays verbatim --
|
||||
// Target::transform() skips literal katoms. Verbatim text produced by
|
||||
// an @eval renderer (@code, @c) is plain text by the time it is spliced
|
||||
// back; the SKS protects it with KTESC markers (hide_typographic() in
|
||||
// klammer_base.py), which are inert here and decode in resolve_escapes.
|
||||
if (final_processing) {
|
||||
target.transform(m_katoms);
|
||||
}
|
||||
m_result = to_string(m_katoms.begin(), m_katoms.end());
|
||||
|
||||
if (final_processing) {
|
||||
for (const auto& [old_str, new_str] : target.m_transforms) {
|
||||
m_result = string_replace(m_result, old_str, new_str);
|
||||
}
|
||||
m_result = target.resolve_escapes(m_result);
|
||||
m_result = run_phase_functions();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ void Option_set_registry::add(
|
||||
if (deftype.m_initial_type == katom_t::klammer_instance) {
|
||||
throw Definition_error(
|
||||
"The option set " + q_(name) + " is declared with \"::\", which takes its "
|
||||
"parameters from a \".k\" declaration. An option set IS a declaration: "
|
||||
"parameters from a \".k\" declaration. An option set is a declaration; "
|
||||
"it declares its parameters itself, after \":\".",
|
||||
begin->m_loc);
|
||||
}
|
||||
@@ -68,16 +68,16 @@ void Option_set_registry::add(
|
||||
throw Definition_error(
|
||||
"The option set " + q_(name) + " declares the positional "
|
||||
+ plural("parameter", static_cast<int>(names.size())) + " "
|
||||
+ join(names, ", ") + ".\n"
|
||||
+ join(names, ", ") + ".\n\n"
|
||||
"An option set declares only optional parameters -- names written with "
|
||||
"a leading \":\".",
|
||||
begin->m_loc, false);
|
||||
begin->m_loc);
|
||||
}
|
||||
if (parameters.m_optional.empty()) {
|
||||
throw Definition_error(
|
||||
"The option set " + q_(name) + " declares no parameters.\n"
|
||||
"The option set " + q_(name) + " declares no parameters.\n\n"
|
||||
"The form is: @@" + name + ".o :name.argtype default ... : <description> @@",
|
||||
begin->m_loc, false);
|
||||
begin->m_loc);
|
||||
}
|
||||
|
||||
// The members as written: these katoms are what is spliced into the
|
||||
@@ -201,14 +201,14 @@ std::string definition_name(const std::string& klammer_name, const std::string&
|
||||
std::stringstream ss {};
|
||||
if (option_sets.has(name) && target_name == Target_registry::optionset_name) {
|
||||
ss << "The option set " << q_(name) << " is used in the declaration of the option "
|
||||
<< "set " << q_(klammer_name) << ".\n"
|
||||
<< "set " << q_(klammer_name) << ".\n\n"
|
||||
<< "An option set is used only in the parameter list of a \".k\" declaration, "
|
||||
<< "so a set does not include another set: a klammer that needs two "
|
||||
<< "vocabularies names two sets, and each set stays a vocabulary that can "
|
||||
<< "be learned whole.";
|
||||
} else if (option_sets.has(name)) {
|
||||
ss << "The option set " << q_(name) << " is used in the parameter list of "
|
||||
<< definition_name(klammer_name, target_name) << ".\n"
|
||||
<< definition_name(klammer_name, target_name) << ".\n\n"
|
||||
<< "An option set may be used only in the parameter list of a \".k\" "
|
||||
<< "declaration, which is where a klammer's interface is declared once "
|
||||
<< "for all of its targets. Declare "
|
||||
@@ -216,15 +216,15 @@ std::string definition_name(const std::string& klammer_name, const std::string&
|
||||
<< "an instance (\"::\"), which inherits the declared parameters.";
|
||||
} else {
|
||||
ss << "The klammer " << q_(name) << " is applied in the parameter list of "
|
||||
<< definition_name(klammer_name, target_name) << ".\n"
|
||||
<< definition_name(klammer_name, target_name) << ".\n\n"
|
||||
<< "A klammer application in a parameter list is not allowed: it is "
|
||||
<< "resolved after the parameters are parsed, so the parameter list it "
|
||||
<< "was meant to contribute is not there when the list is read. An "
|
||||
<< "option set, declared with a \".o\" target, is how parameters are "
|
||||
<< "shared between klammers. Declared option sets: "
|
||||
<< "shared between klammers.\n\nDeclared option sets: "
|
||||
<< option_sets.available() << ".";
|
||||
}
|
||||
throw Definition_error(ss.str(), loc, false);
|
||||
throw Definition_error(ss.str(), loc);
|
||||
}
|
||||
|
||||
// The default written for each member at the use site:
|
||||
@@ -237,10 +237,10 @@ std::map<std::string, std::string> use_site_defaults(
|
||||
if (!positional.empty() || active(rest)) {
|
||||
throw Definition_error(
|
||||
"The use of the option set " + q_(option_set.m_name) +
|
||||
" gives a value that is not an option.\n"
|
||||
" gives a value that is not an option.\n\n"
|
||||
"A set's names and types are fixed where the set is declared; only a "
|
||||
"default may be given where it is used, written as \":name value\".",
|
||||
begin->m_loc, false);
|
||||
begin->m_loc);
|
||||
}
|
||||
std::map<std::string, std::string> defaults {};
|
||||
for (const auto& option : optional) {
|
||||
@@ -248,17 +248,17 @@ std::map<std::string, std::string> use_site_defaults(
|
||||
if (name.size() + 1 != option[0].m_text.size()) {
|
||||
throw Definition_error(
|
||||
"The use of the option set " + q_(option_set.m_name) + " gives a type for \":"
|
||||
+ name + "\".\n"
|
||||
+ name + "\".\n\n"
|
||||
"A set's names and types are declared where the set is; only a default "
|
||||
"may be given where it is used.",
|
||||
option[0].m_loc, false);
|
||||
option[0].m_loc);
|
||||
}
|
||||
const Parameter* member = option_set.find(name);
|
||||
if (member == nullptr) {
|
||||
throw Definition_error(
|
||||
"The option set " + q_(option_set.m_name) + " has no parameter \":"
|
||||
+ name + "\".\n It declares: " + option_set.member_names(),
|
||||
option[0].m_loc, false);
|
||||
+ name + "\". It declares:\n " + option_set.member_names(),
|
||||
option[0].m_loc);
|
||||
}
|
||||
if (defaults.count(name) > 0) {
|
||||
throw Definition_error(
|
||||
@@ -297,7 +297,7 @@ option_set_uses_t expand_option_sets(
|
||||
throw Definition_error(
|
||||
"The parameter \":" + name + "\" of " + q_(klammer_name) +
|
||||
" is declared twice:\n " + previous->second + "\n " + from,
|
||||
loc, false);
|
||||
loc);
|
||||
}
|
||||
origin[name] = from;
|
||||
};
|
||||
|
||||
@@ -24,16 +24,21 @@ void Target::add_transforms(const string_pairs& transforms)
|
||||
}
|
||||
}
|
||||
|
||||
// The typographic transform pass, run by Machine::apply() at final
|
||||
// processing. Per katom rather than over the joined result string, so
|
||||
// that ^'...'^ literal content (katom_t::literal) is never transformed --
|
||||
// verbatim text must show the characters the writer typed. A transform
|
||||
// source therefore cannot match across a katom boundary, which is the
|
||||
// correct reading: two hyphens separated by a klammer application were
|
||||
// separated by the writer and are not a dash.
|
||||
void Target::transform(katom_list& katoms) const
|
||||
{
|
||||
(void)K::log(3);
|
||||
std::for_each(
|
||||
katoms.begin(), katoms.end(),
|
||||
[this] (Katom& k) {
|
||||
// std::cout << "transform: " << k << "\n";
|
||||
if (k.m_type != katom_t::literal) {
|
||||
for (const auto& [a, b] : this->m_transforms) {
|
||||
// std::cout << " " << a << right_arrow << b << "\n";
|
||||
k.m_text = string_replace(k.m_text, a, b);
|
||||
}
|
||||
}
|
||||
@@ -67,6 +72,14 @@ void Target::add_escapes(const std::string& escape_spec)
|
||||
}
|
||||
}
|
||||
|
||||
void Target::add_resolves(const std::string& resolve_spec)
|
||||
{
|
||||
auto words = word_split(resolve_spec);
|
||||
for (size_t i = 0; i + 1 < words.size(); i += 2) {
|
||||
m_resolves.push_back({words[i], words[i+1]});
|
||||
}
|
||||
}
|
||||
|
||||
std::string Target::escape_marker(const std::string& ch)
|
||||
{
|
||||
std::stringstream ss {};
|
||||
@@ -94,11 +107,16 @@ std::string Target::unescape_text(std::string text) const
|
||||
std::string Target::resolve_escapes(std::string text) const
|
||||
{
|
||||
// Target-declared escapes first (marker -> declared replacement), then
|
||||
// the generic decode for the remaining markers (marker -> the character
|
||||
// itself: quoted Klammertext specials and literal-span content).
|
||||
// the resolution-only entries (:resolve -- how a QUOTED character
|
||||
// renders here), then the generic decode for the remaining markers
|
||||
// (marker -> the character itself: quoted punctuation and literal-span
|
||||
// content).
|
||||
for (const auto& [ch, repl] : m_escapes) {
|
||||
text = string_replace(text, escape_marker(ch), repl);
|
||||
}
|
||||
for (const auto& [ch, repl] : m_resolves) {
|
||||
text = string_replace(text, escape_marker(ch), repl);
|
||||
}
|
||||
return ktesc_resolve(text);
|
||||
}
|
||||
|
||||
@@ -130,6 +148,29 @@ std::string ktesc_resolve(std::string text)
|
||||
return text;
|
||||
}
|
||||
|
||||
bool hide_quoted_punctuation(std::string& s)
|
||||
{
|
||||
// ASCII punctuation, EXCEPT "'" -- ^' opens a ^'...'^ literal span, the
|
||||
// one documented exception to the rule (a literal apostrophe is ^0027^).
|
||||
// Hand-rolled scan: no std::regex, this can run over large text.
|
||||
static const std::string punct = R"pct(!"#$%&()*+,-./:;<=>?@[\]^_`{|}~)pct";
|
||||
if (s.find('^') == std::string::npos) return false;
|
||||
std::string result {};
|
||||
bool changed = false;
|
||||
for (size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i] == '^' && i + 1 < s.size()
|
||||
&& punct.find(s[i + 1]) != std::string::npos) {
|
||||
result += Target::escape_marker(std::string(1, s[i + 1]));
|
||||
++i;
|
||||
changed = true;
|
||||
} else {
|
||||
result += s[i];
|
||||
}
|
||||
}
|
||||
if (changed) s = result;
|
||||
return changed;
|
||||
}
|
||||
|
||||
std::string hide_structural_characters(const std::string& s)
|
||||
{
|
||||
std::string result {};
|
||||
@@ -145,7 +186,26 @@ std::string hide_structural_characters(const std::string& s)
|
||||
void Target::add_after_apply(const std::string& function_specs)
|
||||
{
|
||||
for (const auto& f : regex_split(function_specs, std::regex(R"(\s+;\s+)"), true)) {
|
||||
// msg() << "Add " << m_name << " after-apply: " << f << "\n";
|
||||
// A bare spec is ONE Python name (module.function): whitespace
|
||||
// inside it means two specs were written without the " ; "
|
||||
// separator, and the glued call would otherwise fail only at render
|
||||
// time, as a Python SyntaxError located at "phase" rather than at
|
||||
// this declaration (found 2026-08-22, the first time a target
|
||||
// declared two phases). Mode-tagged specs (":cpp <library>
|
||||
// <function>") are exempt: the library is a filename, and filenames
|
||||
// may contain spaces -- which is exactly why the list separator is
|
||||
// ";" rather than whitespace.
|
||||
if (!f.empty() && f[0] != ':'
|
||||
&& f.find_first_of(" \t\n") != std::string::npos) {
|
||||
throw Definition_error(
|
||||
"The :after_apply phase \"" + f + "\" contains whitespace. "
|
||||
"A bare phase is a single Python name (module.function), and "
|
||||
"several phases are separated by \" ; \":\n"
|
||||
" :after_apply first.phase ; second.phase\n"
|
||||
"(A mode-tagged phase -- \":cpp <library> <function>\" -- may "
|
||||
"contain spaces; its library is a filename.)",
|
||||
m_loc);
|
||||
}
|
||||
m_after_apply.push_back(f);
|
||||
}
|
||||
}
|
||||
|
||||
19
mac/target.h
19
mac/target.h
@@ -24,6 +24,7 @@ public:
|
||||
void transform(std::vector<Katom>& katoms) const;
|
||||
|
||||
void add_escapes(const std::string& escape_spec);
|
||||
void add_resolves(const std::string& resolve_spec);
|
||||
std::string escape_text(std::string text) const;
|
||||
std::string unescape_text(std::string text) const;
|
||||
std::string resolve_escapes(std::string text) const;
|
||||
@@ -40,6 +41,13 @@ public:
|
||||
Locator m_loc {};
|
||||
std::vector<std::pair<std::string, std::string>> m_transforms {};
|
||||
std::vector<std::pair<std::string, std::string>> m_escapes {};
|
||||
// Resolution-only entries (:resolve): how a QUOTED character renders in
|
||||
// this target. The resolve half of :escape without the escape half --
|
||||
// needed where the raw character must stay untouched in writer text
|
||||
// (tex cannot escape "-" without destroying the --- convention) but the
|
||||
// quoted character must not decode to its raw self (a decoded -- would
|
||||
// re-form TeX's dash ligature).
|
||||
std::vector<std::pair<std::string, std::string>> m_resolves {};
|
||||
// Argtype_registry m_argtypes {};
|
||||
|
||||
|
||||
@@ -60,3 +68,14 @@ std::string ktesc_resolve(std::string text);
|
||||
// sub-Machines and the @eval result read-back. Resolved by ktesc_resolve()
|
||||
// at final processing.
|
||||
std::string hide_structural_characters(const std::string& s);
|
||||
|
||||
// Replace each ^P pair in s -- "^" before any ASCII punctuation character
|
||||
// except "'" (which opens a ^'...'^ literal span) -- with the KTESC marker
|
||||
// of P: "^" before a punctuation character quotes it, uniformly, not only
|
||||
// the six Klammertext specials. A letter or digit is never special, so
|
||||
// ^<letter-or-digit> keeps its existing meanings (diacritics, mnemonics,
|
||||
// ^UUUU^ code points). Returns whether anything was replaced. Called from
|
||||
// hide_special_katoms() on writer-text katoms OUTSIDE definition and
|
||||
// @eval/@read/@cond spans -- code keeps its carets (grep '^-' must reach
|
||||
// the shell intact), the same skip set as the quoted-special hiding.
|
||||
bool hide_quoted_punctuation(std::string& s);
|
||||
|
||||
@@ -14,7 +14,7 @@ std::string Target_registry::general_name = "*";
|
||||
std::string Target_registry::optionset_name = "o";
|
||||
|
||||
Target_registry::Target_registry()
|
||||
: m_parameters(Parameter_set("name | desc :after_apply :after_write :includes :escape | transforms.rest"))
|
||||
: m_parameters(Parameter_set("name | desc :after_apply :after_write :includes :escape :resolve | transforms.rest"))
|
||||
{
|
||||
// Registration order is display order. The two that declare an interface
|
||||
// come first -- "k" a klammer's, "o" an option set's -- and then "*", the
|
||||
@@ -47,14 +47,25 @@ void Target_registry::add(std::vector<Katom>::iterator begin, std::vector<Katom>
|
||||
Target target(values["name"], values["desc"], begin->m_loc);
|
||||
target.add_transforms(values["transforms"]);
|
||||
target.add_escapes(values["escape"]);
|
||||
target.add_resolves(values["resolve"]);
|
||||
target.add_after_apply(values["after_apply"]);
|
||||
target.m_includes = word_split(values["includes"]);
|
||||
// Inherit escapes from included targets
|
||||
// Inherit escapes AND typographic transforms from included targets: an
|
||||
// including target renders through the included one's syntax (pdf
|
||||
// through tex), so both tables apply there too. Inherited entries are
|
||||
// appended after the including target's own, so a declaration can
|
||||
// override an inherited pair by declaring its source first.
|
||||
for (const auto& included : target.m_includes) {
|
||||
if (m_targets.count(included)) {
|
||||
for (const auto& esc : m_targets[included].m_escapes) {
|
||||
target.m_escapes.push_back(esc);
|
||||
}
|
||||
for (const auto& tr : m_targets[included].m_transforms) {
|
||||
target.add_transform(tr.first, tr.second);
|
||||
}
|
||||
for (const auto& res : m_targets[included].m_resolves) {
|
||||
target.m_resolves.push_back(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Registration (and the previous-definition check) goes through
|
||||
@@ -79,7 +90,7 @@ void Target_registry::check_for_previous_definition(const std::string& name, con
|
||||
if (has(name)) {
|
||||
const Target& current = m_targets.at(name);
|
||||
throw Target_error("Target \"" + name + "\" is already defined:\n " + current.m_loc.desc(),
|
||||
loc, false);
|
||||
loc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public:
|
||||
|
||||
|
||||
/*
|
||||
std::string m_parameters_spec {"name | desc :after_apply :after_write :includes | transforms.rest"};
|
||||
std::string m_parameters_spec {"name | desc :after_apply :after_write :includes :escape :resolve | transforms.rest"};
|
||||
Parameter_set m_parameters =
|
||||
katomize(line_split(m_parameters_spec), Locator().str());
|
||||
*/
|
||||
|
||||
60
mac/util.cpp
60
mac/util.cpp
@@ -226,14 +226,66 @@ std::string justify_string(const std::string& s, unsigned int width=80, bool fre
|
||||
return result;
|
||||
}
|
||||
|
||||
// The error-message formatting contract (2026-08-21): a message carries no
|
||||
// decisions about line breaks EXCEPT by indentation.
|
||||
// * a line beginning with whitespace is VERBATIM -- an example, a pattern,
|
||||
// a signature, a list entry -- emitted untouched: no folding, no
|
||||
// wrapping, no "~" substitution (a pattern may contain a literal ~);
|
||||
// * blank lines separate blocks (runs collapse to one);
|
||||
// * everything else is prose: consecutive lines fold into one paragraph
|
||||
// and are wrapped to the width.
|
||||
// So prose stays machine-wrapped however the source hand-wraps it, and the
|
||||
// one legitimate exception is marked in the one place it cannot be missed.
|
||||
// This replaced a per-call do_justify flag on the Error constructors, whose
|
||||
// decision lived apart from the text it governed. Line-based by hand: no
|
||||
// std::regex over the whole message (the ambiguous-alternation hazard).
|
||||
std::string justify(
|
||||
const std::string& input_text, unsigned int text_width, unsigned int margin_width)
|
||||
{
|
||||
std::string result {};
|
||||
text_width -= margin_width;
|
||||
for (const std::string& par : split_into_paragraphs(trim(input_text))) {
|
||||
result += justify_string(par, text_width) + "\n\n";
|
||||
strings_t lines {};
|
||||
{
|
||||
const std::string text = trim(input_text);
|
||||
std::string::size_type from = 0;
|
||||
while (from <= text.size()) {
|
||||
auto nl = text.find('\n', from);
|
||||
if (nl == std::string::npos) {
|
||||
lines.push_back(text.substr(from));
|
||||
break;
|
||||
}
|
||||
lines.push_back(text.substr(from, nl - from));
|
||||
from = nl + 1;
|
||||
}
|
||||
}
|
||||
std::string result {};
|
||||
strings_t prose {};
|
||||
bool pending_blank = false;
|
||||
auto emit = [&](const std::string& block) {
|
||||
if (!result.empty()) {
|
||||
result += pending_blank ? "\n\n" : "\n";
|
||||
}
|
||||
result += block;
|
||||
pending_blank = false;
|
||||
};
|
||||
auto flush_prose = [&]() {
|
||||
if (!prose.empty()) {
|
||||
emit(justify_string(join(prose, " "), text_width));
|
||||
prose.clear();
|
||||
}
|
||||
};
|
||||
for (const std::string& line : lines) {
|
||||
if (trim(line).empty()) { // block separator
|
||||
flush_prose();
|
||||
pending_blank = !result.empty();
|
||||
} else if (line[0] == ' ' || line[0] == '\t') { // verbatim line
|
||||
flush_prose();
|
||||
emit(trim_right(line));
|
||||
} else { // prose
|
||||
prose.push_back(trim(line));
|
||||
}
|
||||
}
|
||||
flush_prose();
|
||||
result += "\n";
|
||||
if (margin_width > 0) {
|
||||
result = add_margin(result, margin_width);
|
||||
}
|
||||
@@ -368,7 +420,7 @@ std::vector<std::pair<std::string, std::string>> environment_variables(bool allo
|
||||
if (!allow_empty_definitions && parts.size() < 2) {
|
||||
throw Internal_error(
|
||||
"Incorrect environment variable format:\n " + std::string(environ[i]),
|
||||
Locator(), false);
|
||||
Locator());
|
||||
}
|
||||
std::string name = parts[0];
|
||||
parts.erase(parts.begin());
|
||||
|
||||
@@ -9,49 +9,77 @@ paragraph pass recognizes as a block and leaves alone, and \par in
|
||||
vertical mode is a no-op.
|
||||
]#
|
||||
|
||||
@@par.k s : A paragraph of text @@
|
||||
# Paragraph
|
||||
|
||||
@@par.k s : A paragraph of text. @@
|
||||
|
||||
@@par.html :: <p>*s*</p> @@
|
||||
@@par.tex ::
|
||||
\par
|
||||
*s*
|
||||
\par
|
||||
@@
|
||||
|
||||
@@par.tex :: \par *s* \par @@
|
||||
|
||||
# In plain text a paragraph is delimited by blank lines, which
|
||||
# phases.justify_blocks then fills; #/2 inserts them without depending on
|
||||
# the definition body's own whitespace surviving extraction.
|
||||
@@par.txt :: #/2*s*#/2 @@
|
||||
|
||||
|
||||
# Non-breaking space
|
||||
|
||||
@@sp.k : Non-breaking space character @@
|
||||
@@sp.html :: &^#160; @@
|
||||
@@sp.tex :: ~ @@
|
||||
|
||||
# TODO: Easy in LaTeX; how to handle in HTML and plain text?
|
||||
# @@footnote.k s : Footnote (TBD) @@
|
||||
# @@footnote :: [*s*] @@
|
||||
# Indented block
|
||||
|
||||
@@indent.k s :w.int 3 :linebreak.bool false : Indented block @@
|
||||
@@indent.k s :left.int 8 :right.int -1 :ragged.bool false :
|
||||
Indented block. The *left* and *right* values are the number of characters for
|
||||
indentation (as roughtly defined by "ex"). By default, *right* is set to the
|
||||
*left* value (with -1 as the sentinel).
|
||||
@@
|
||||
@@indent.html,tex,txt :: @eval block.Indent(K) eval@ @@
|
||||
|
||||
@@quote.k s :w.int 1 :source : Quotation block @@
|
||||
|
||||
@@quote.html ::
|
||||
<div class="quote">
|
||||
# Right justification
|
||||
|
||||
@@right.k s :margin 0 :top_margin 2 : Right-justified text @@
|
||||
|
||||
@@right.html ::
|
||||
<div style="text-align: right; margin: *top_margin*ex *margin*ex 2ex 0;">*s*</div>
|
||||
@@
|
||||
|
||||
@@right.tex ::
|
||||
{\setlength{\topsep}{0pt}\setlength{\partopsep}{0pt}\setlength{\parskip}{*top_margin*ex}
|
||||
\begin{flushright}
|
||||
\raggedleft\rightskip=*margin*ex
|
||||
*s*
|
||||
</div>
|
||||
\end{flushright}}
|
||||
@@
|
||||
|
||||
@@quote.tex ::
|
||||
\quoteblock{*s*}{*source*}
|
||||
@@right.txt ::
|
||||
@eval import textwrap; "\n".join([line.rjust(*Target_txt_width*, "~")
|
||||
for line in textwrap.wrap("""*s*""", *Target_txt_width*)]) @
|
||||
@@
|
||||
|
||||
@@quote.txt ::
|
||||
@eval block.block_indent(K) eval@
|
||||
|
||||
# Quote with attribution
|
||||
|
||||
@@quote.k text | attribution : A block quote with attribution. @@
|
||||
@@quote.html,tex ::
|
||||
@indent *text* :left 8 @
|
||||
@right *attribution* :margin 8
|
||||
:top_margin @cond @eval "*K_target*" == "html" @ | -1 | 1 cond@ @
|
||||
@@
|
||||
|
||||
@@note.k s :label Note :color 1.0,1.0,0.9 :bordercolor 0.2,0.2,0.2 :level.int 0 :width
|
||||
: Rectangular block for a special note @@
|
||||
|
||||
@@note.html,tex :: @eval block.Note(K) eval@ @@
|
||||
# Highlighted note in text
|
||||
|
||||
@@note.k s :label Note :color 1.0,1.0,0.9 :border_color 0.2,0.2,0.2 :level.int 0 :width.float 0.5
|
||||
: Rectangular block for an editorial note @@
|
||||
|
||||
@@note.html,tex :: @eval block.Note(K) @ @@
|
||||
|
||||
|
||||
# Center
|
||||
|
||||
@@center.k s : Center text @@
|
||||
|
||||
@@ -62,42 +90,46 @@ vertical mode is a no-op.
|
||||
@@
|
||||
|
||||
@@center.html ::
|
||||
<p>
|
||||
<div class="center">
|
||||
*s*
|
||||
</div>
|
||||
</p>
|
||||
@@
|
||||
|
||||
@@right.k s : Right-justified text @@
|
||||
|
||||
@@right.html ::
|
||||
<b>TBD</b> *s*
|
||||
@@
|
||||
|
||||
@@right.tex ::
|
||||
\begin{flushright}
|
||||
*s*
|
||||
\end{flushright}
|
||||
@@
|
||||
# Newline
|
||||
|
||||
@@nl.k : Newline character @@
|
||||
@@nl.html :: <br> @@
|
||||
@@nl.html :: <br/> @@
|
||||
@@nl.tex :: \newline @@
|
||||
@@nl.txt :: \n @@
|
||||
@@nl.txt :: @eval phases.txt_nl_marker() @ @@
|
||||
|
||||
|
||||
# New page
|
||||
|
||||
@@newpage.k : Start new page @@
|
||||
@@newpage.html :: @@
|
||||
@@newpage.tex :: \newpage @@
|
||||
@@newpage.txt :: @@
|
||||
|
||||
|
||||
# Extend page
|
||||
|
||||
@@extendpage.k linecount : Extenad current page @@
|
||||
@@extendpage.html :: @@
|
||||
@@extendpage.tex :: \enlargethispage{*linecount*\baselineskip} @@
|
||||
@@extendpage.txt :: @@
|
||||
|
||||
|
||||
# Vertical space
|
||||
|
||||
@@vspace.k lines.float : Vertical space, in multiples of the current line height @@
|
||||
@@vspace.tex :: \vspace{*lines*\baselineskip} @@
|
||||
@@vspace.html :: <div style="height: *lines*lh"></div> @@
|
||||
@@vspace.txt :: @eval "__VSPACE__" * round(*lines*) @ @@
|
||||
@@vspace.txt :: @eval phases.txt_vspace_marker() * round(*lines*) @ @@
|
||||
|
||||
# Vertical fill to end of page
|
||||
|
||||
@@vfill.k :
|
||||
Fill the vertical space so that any following text is flush with the bottom
|
||||
@@ -112,6 +144,8 @@ only makes some vertical space. @@
|
||||
@@vfill.txt :: @vspace 3 @ @@
|
||||
|
||||
|
||||
# Questions and answers
|
||||
|
||||
@@qa.k question | answer : Question and answer formatting @@
|
||||
@@qa.html,tex,txt ::
|
||||
@b Q: @ *question*
|
||||
@@ -119,6 +153,9 @@ only makes some vertical space. @@
|
||||
@b A: @ *answer*
|
||||
@@
|
||||
|
||||
|
||||
# Absolute positioning
|
||||
|
||||
@@@argtype coords | x and y coordinates :pattern 'float'\s+'float' @@@
|
||||
|
||||
@@block.k : to.coords | content :width.float .5 :point.coords 0.0 0.0
|
||||
@@ -126,18 +163,51 @@ only makes some vertical space. @@
|
||||
|
||||
@@block.tex :: @eval block.Block(K) eval@ @@
|
||||
|
||||
|
||||
# Preserved line endings
|
||||
|
||||
@@lines.k s : Maintain line breaks @@
|
||||
@@lines.html,tex,txt :: @eval block.Lines(K) eval@ @@
|
||||
|
||||
@@twocolumns.k s : Format *s* in two columns @@
|
||||
@@twocolumns.tex ::
|
||||
\begin{multicols}{2}
|
||||
|
||||
# Multiple columns of text
|
||||
|
||||
@@multicolumn.k s :n 2 :gap 3 : Format *s* in *n* columns (default: 2). The *gap* is in "ex" units. @@
|
||||
|
||||
@@multicolumn.tex ::
|
||||
{\setlength{\columnsep}{*gap*ex}
|
||||
\setlength{\multicolsep}{\parskip}
|
||||
\begin{multicols}{*n*}
|
||||
*s*
|
||||
\end{multicols}
|
||||
\end{multicols}}
|
||||
@@
|
||||
|
||||
@@left_right.k left | right : Text left- and right- justified on one line @@
|
||||
|
||||
@@left_right.tex ::
|
||||
\makebox[\dimexpr\linewidth+\labelindent\relax]{*left*\hfill *right*}
|
||||
@@multicolumn.html ::
|
||||
<p>
|
||||
<div style="column-count: *n*; column-gap: *gap*ex; text-align: justify;">
|
||||
*s*
|
||||
</div>
|
||||
</p>
|
||||
@@
|
||||
|
||||
|
||||
# Text justified left and right on one line
|
||||
|
||||
@@left_right.k parts.rest(2) :
|
||||
Pairs of text left- and right- justified. First, left lines are written with a
|
||||
bar (|) between each line. Then, two bars (||), and the right lines with a bar
|
||||
betwen them.
|
||||
@@
|
||||
|
||||
@@left_right.html,tex :: @eval block.Left_right(K) @ @@
|
||||
|
||||
|
||||
# Footnote
|
||||
|
||||
@@footnote.k s : Footnote @@
|
||||
@@footnote.html :: <kt-footnote>*s*</kt-footnote> @@
|
||||
@@footnote.tex :: \unskip\footnote{*s*} @@
|
||||
@@footnote.txt :: __ #- @eval phases.txt_footnote_marker() @ *s* @eval phases.txt_footnote_marker() @ #- __ @@
|
||||
|
||||
# This doesn't work, either, so it isn't the @eval:
|
||||
# @@footnote.txt :: __ #- marker *s* marker #- __ @@
|
||||
|
||||
@@ -5,72 +5,60 @@ import pprint
|
||||
import klammer_base
|
||||
import kutil
|
||||
import latex_util
|
||||
import html_util
|
||||
from html_util import E
|
||||
import color
|
||||
|
||||
class Indent(klammer_base.Klammer_base):
|
||||
def __init__(self, K):
|
||||
super().__init__(K)
|
||||
self.s = kutil.escape(self.s)
|
||||
# Doesn't do anything now:
|
||||
# self.s = kutil.escape(self.s)
|
||||
if self.right < 0:
|
||||
self.right = self.left
|
||||
self.reduce = self.left + self.right
|
||||
|
||||
def html(self):
|
||||
return "FIX: INDENT " + self.s
|
||||
|
||||
text_align = "justify" if not self.ragged else "left"
|
||||
return E("div").body(self.s)\
|
||||
.sty("margin", f"0.5rem {self.right}ex 0.5rem {self.left}ex")\
|
||||
.sty("text-align", text_align)\
|
||||
.str()
|
||||
def tex(self):
|
||||
tab = f"\\hspace*{{{self.w}ex}}"
|
||||
result = ""
|
||||
|
||||
if self.linebreak:
|
||||
for e in self.s.split("\n"):
|
||||
result += f"{tab}{e}\\\\\n"
|
||||
result = result[:-3]
|
||||
result = re.sub(r"\t", r"\\t", result)
|
||||
else:
|
||||
result = tab + latex_util.minipage(
|
||||
"\\raggedright " + self.s, f"\\textwidth - {self.w}ex", center=False, vmargin="4pt")
|
||||
#print(result)
|
||||
return latex_util.block(result)
|
||||
if self.ragged:
|
||||
self.s = "\\raggedright " + self.s
|
||||
result = rf"\hspace*{{{self.left}ex}}" + latex_util.minipage(
|
||||
self.s, rf"\linewidth - {self.reduce}ex", center=False)
|
||||
result = latex_util.block(result)
|
||||
return result
|
||||
|
||||
def txt(self):
|
||||
indent = " " * self.w
|
||||
indent = "~" * self.left # Removed in phases.justify_blocks
|
||||
width = int(self.Target_txt_width) - self.left - self.right + 1 # Off-by one for textwrap
|
||||
result = self.s
|
||||
result = re.sub("KK0022", '"', result)
|
||||
result = indent + f"\n{indent}".join(textwrap.wrap(result, width=70, break_on_hyphens=False))
|
||||
return f"@lit\n{result}\nlit@"
|
||||
result = indent + f"\n{indent}".join(textwrap.wrap(result, width=width, break_on_hyphens=True))
|
||||
return result
|
||||
|
||||
|
||||
class Note(klammer_base.Klammer_base):
|
||||
def __init__(self, K):
|
||||
super().__init__(K)
|
||||
|
||||
def html(self):
|
||||
color = ",".join([f"{float(e)*100}%" for e in self.color.split(",")])
|
||||
result = f'''<div class="box" style="background-color: rgb({color}); border-color: rgb({self.bordercolor}) ;">
|
||||
<b>{self.label}:</b> {self.s}
|
||||
</div>'''
|
||||
result = E("div").cls("box")\
|
||||
.sty("width", f"{self.width*100}%")\
|
||||
.sty("background-color", html_util.color(self.color))\
|
||||
.sty("border-color", html_util.color(self.border_color))\
|
||||
.sty("margin-left", "auto")\
|
||||
.body(f"<b>{self.label}:</b> {self.s}")\
|
||||
.str()
|
||||
return result
|
||||
|
||||
def tex(self):
|
||||
if self.width:
|
||||
width = r'{}\\textwidth'.format(self.width)
|
||||
else:
|
||||
#width = r'\\textwidth - 16pt - {}\\leftmargin'.format(self.level)
|
||||
width = r'\\linewidth - \\leftmargin + 2pt'
|
||||
|
||||
# The \par on each side comes from latex_util.block() below: an
|
||||
# \fcolorbox is box material and must sit in vertical mode.
|
||||
result = '''
|
||||
\\begingroup
|
||||
COLOR\\setlength{\\fboxsep}{8pt}
|
||||
\\fcolorbox{bordercolor}{localcolor}{
|
||||
\\parbox{WIDTH}{\\raggedright\\setlength{\\parskip}{8pt}
|
||||
\\textbf{LABEL:} TEXT
|
||||
}}\\endgroup
|
||||
'''
|
||||
result = re.sub('LABEL', self.label, result)
|
||||
result = re.sub('TEXT', re.sub(r'\\', r'\\\\', self.s), result)
|
||||
result = re.sub('COLOR', r'\\definecolor{{localcolor}}{{rgb}}{{{}}}\nCOLOR'.format(self.color), result)
|
||||
result = re.sub('COLOR', r'\\definecolor{{bordercolor}}{{rgb}}{{{}}}\n'.format(self.bordercolor), result)
|
||||
result = re.sub('WIDTH', width, result)
|
||||
print(result)
|
||||
return latex_util.block(result)
|
||||
return latex_util.color_box(
|
||||
f"\\textbf{{{self.label}:}} {self.s}",
|
||||
self.width, self.color, self.border_color)
|
||||
|
||||
|
||||
class Block(klammer_base.Klammer_base):
|
||||
@@ -95,13 +83,12 @@ class Block(klammer_base.Klammer_base):
|
||||
class Lines(klammer_base.Klammer_base):
|
||||
def __init__(self, K):
|
||||
super().__init__(K)
|
||||
self.s = kutil.escape(self.s)
|
||||
self.lines = self.s.split("\n")
|
||||
|
||||
def html(self):
|
||||
result = ""
|
||||
for line in self.lines:
|
||||
result += line + "<br>\n"
|
||||
result += line + "<br/>\n"
|
||||
return result
|
||||
|
||||
def tex(self):
|
||||
@@ -109,3 +96,24 @@ class Lines(klammer_base.Klammer_base):
|
||||
|
||||
def txt(self):
|
||||
return self.lines
|
||||
|
||||
class Left_right(klammer_base.Klammer_base):
|
||||
def __init__(self, K):
|
||||
super().__init__(K)
|
||||
self.rows = list(zip(self.parts[0], self.parts[1]))
|
||||
|
||||
def html(self):
|
||||
result = ""
|
||||
for left, right in self.rows:
|
||||
result += E("div").body(
|
||||
E("span").cls("left_right").body(left).str() + \
|
||||
E("span").cls("left_right").sty("text-align", "right").body(right).str()).str();
|
||||
result = E("p").body(result).str()
|
||||
return result
|
||||
|
||||
def tex(self):
|
||||
result = ""
|
||||
for left, right in self.rows:
|
||||
result += rf"\parbox{{\linewidth}}{{{left} \hfill {right}}}" + " \\\\\n"
|
||||
result = result[:-3]
|
||||
return result
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
p {
|
||||
margin: .5rem 0 .5rem 0;
|
||||
margin: 1ex 0;
|
||||
}
|
||||
|
||||
.quote {
|
||||
@@ -15,10 +15,8 @@ p {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: fit-content;
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.indent {
|
||||
@@ -49,3 +47,8 @@ p {
|
||||
.vfill {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.left_right {
|
||||
display: inline-block;
|
||||
width: calc((100vw - 60px) / 2);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.path.append("../kutil")
|
||||
sys.path.append("../target")
|
||||
# if __name__ == "__main__":
|
||||
# import sys
|
||||
# sys.path.append("../kutil")
|
||||
# sys.path.append("../target")
|
||||
|
||||
import sys
|
||||
import re
|
||||
import klammer_base
|
||||
import kutil
|
||||
import html_util
|
||||
from html_util import E
|
||||
import latex_util as L
|
||||
import pprint
|
||||
import phases
|
||||
|
||||
|
||||
def escape_newlines(s):
|
||||
# A newline becomes the marker the html paragraph pass turns back into a
|
||||
# line break, so a verbatim source file keeps its lines. Used by Source.
|
||||
return re.sub("\n", " ___NL___ ", s)
|
||||
|
||||
def undash(s):
|
||||
# Verbatim text must show the hyphens the writer typed: the target's
|
||||
# "--"/"---" transforms have already run, so put them back. Used by
|
||||
# Code_fragment.
|
||||
result = re.sub("__MDASH__", "---", s)
|
||||
return re.sub("__NDASH__", "--", result)
|
||||
|
||||
def is_comment(s):
|
||||
return s.strip().startswith("//")
|
||||
|
||||
@@ -168,9 +158,17 @@ def html_line(text):
|
||||
tex_line -- a raw # in "#include" would start a text removal). The
|
||||
html entities introduce no Klammertext special, so the two passes
|
||||
cannot interfere. No target :escape entries apply here, so each
|
||||
quoted special decodes back to its own character."""
|
||||
return quote_specials(html_escape_rgx.sub(
|
||||
lambda m: html_escapes[m.group()], text))
|
||||
quoted special decodes back to its own character.
|
||||
|
||||
Last, the typographically active characters are hidden as KTESC
|
||||
markers (klammer_base.hide_typographic): the html target's transforms
|
||||
run over the final result, and without this a "--check" in a listing
|
||||
became an en-dash. The markers decode after the transform pass.
|
||||
tex_line needs no such step -- the tex target declares no typographic
|
||||
transforms (LaTeX applies its input conventions itself), and its ~ is
|
||||
this code's own markup for a preserved space."""
|
||||
return klammer_base.hide_typographic(quote_specials(html_escape_rgx.sub(
|
||||
lambda m: html_escapes[m.group()], text)))
|
||||
|
||||
def html_block(code, comment):
|
||||
r"""One block: its lines beside its comment.
|
||||
@@ -192,11 +190,25 @@ def html_block(code, comment):
|
||||
return f'<div class="code_block">{result}</div>\n'
|
||||
|
||||
|
||||
def expand_whitespace_markers(text, K=None):
|
||||
def count(count_match):
|
||||
count = int(count_match) if count_match else 1
|
||||
return count
|
||||
def replace_spaces(match):
|
||||
return " " * count(match.group(1))
|
||||
def replace_newlines(match):
|
||||
return "\n" * count(match.group(1))
|
||||
result = text
|
||||
result = re.compile(r" *#- *", re.S).sub("", result)
|
||||
result = re.compile(r" *#\+(\d*) *", re.S).sub(replace_spaces, result)
|
||||
result = re.compile(r"\s*\#\/(\d*)\s*", re.S).sub(replace_newlines, result)
|
||||
return result
|
||||
|
||||
class Code(klammer_base.Klammer_base):
|
||||
id = 0
|
||||
def __init__(self, K):
|
||||
super().__init__(K)
|
||||
self.text = phases.expand_whitespace_markers(self.text)
|
||||
self.text = expand_whitespace_markers(self.text)
|
||||
|
||||
def annotated(self, pairs):
|
||||
"""Does any block of this listing carry a comment?"""
|
||||
@@ -332,7 +344,7 @@ class Code_fragment(klammer_base.Klammer_base):
|
||||
# ordinary string and the machine did the escaping; html() got away with
|
||||
# handling "<" by hand and tex() with nothing at all.
|
||||
def html(self):
|
||||
return f'<span class="code">{html_line(undash(self.code_text.strip()))}</span>'
|
||||
return f'<span class="code">{html_line(self.code_text.strip())}</span>'
|
||||
|
||||
def tex(self):
|
||||
return f"{{\\tt {tex_line(self.code_text.strip())}}}"
|
||||
@@ -373,7 +385,7 @@ def extract_marked_region(src, marker, filename):
|
||||
|
||||
|
||||
class Source(Code):
|
||||
"""@source_listing -- a Code listing whose text comes from a FILE.
|
||||
r"""@source_listing -- a Code listing whose text comes from a FILE.
|
||||
|
||||
It IS a Code: @source_listing and @code differ only in where the text
|
||||
comes from, so they must render identically, and subclassing is what
|
||||
@@ -406,7 +418,7 @@ class Source(Code):
|
||||
except OSError as e:
|
||||
raise Exception(
|
||||
f'Cannot read the source listing "{self.filename}": {e.strerror}.\n'
|
||||
f' A relative name resolves against the DOCUMENT\'s directory.')
|
||||
f' A relative name resolves against the document\'s directory.')
|
||||
if self.marker:
|
||||
text = extract_marked_region(text, self.marker, self.filename)
|
||||
self.text = text
|
||||
|
||||
@@ -1,403 +0,0 @@
|
||||
print("DEPRECATED")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.path.append("../kutil")
|
||||
sys.path.append("../target")
|
||||
|
||||
import re
|
||||
import klammer_base
|
||||
import kutil
|
||||
import html_util
|
||||
from html_util import E
|
||||
import latex_util as L
|
||||
import pprint
|
||||
import phases
|
||||
|
||||
def escape_newlines(s):
|
||||
return re.sub("\n", " ___NL___ ", s)
|
||||
|
||||
# def literal_newline(s):
|
||||
# def replace(match):
|
||||
# before, after = match.groups()
|
||||
# return f"{before}\\n{after}"
|
||||
# backslash_pat = re.compile(r'(".*?)\n(.*?")', re.S)
|
||||
# return backslash_pat.sub(replace, s)
|
||||
|
||||
def is_comment(s):
|
||||
return s.strip().startswith("//")
|
||||
|
||||
def split_blocks(s):
|
||||
print('-'*40)
|
||||
print(s)
|
||||
print('-'*40)
|
||||
blocks = []
|
||||
in_code = True
|
||||
block = ""
|
||||
for line in s.rstrip().split("\n"):
|
||||
if is_comment(line):
|
||||
if in_code:
|
||||
blocks.append(block)
|
||||
block = line + "\n"
|
||||
in_code = False
|
||||
else:
|
||||
block += line + "\n"
|
||||
else:
|
||||
if not in_code:
|
||||
blocks.append(block)
|
||||
block = line + "\n"
|
||||
in_code = True
|
||||
else:
|
||||
block += line + "\n"
|
||||
if block:
|
||||
blocks.append(block)
|
||||
for block in blocks:
|
||||
print("B:")
|
||||
print(block)
|
||||
return blocks
|
||||
|
||||
def parse_blocks(blocks):
|
||||
box_comment_rgx = re.compile("\s*//(\d+)\s+.*", re.S)
|
||||
i = 0
|
||||
while i < len(blocks):
|
||||
match = box_comment_rgx.match(blocks[i])
|
||||
if match:
|
||||
print(match.group(1))
|
||||
i += 1
|
||||
|
||||
|
||||
|
||||
def get_blocks(s):
|
||||
comment_pat = re.compile(r"(\s*)//(\d+)\s+(.*)", re.S)
|
||||
blocks = []
|
||||
lines = s.strip("\n").split("\n")
|
||||
i = 0
|
||||
uncommented = ""
|
||||
while i < len(lines):
|
||||
match = comment_pat.match(lines[i])
|
||||
if match:
|
||||
if uncommented:
|
||||
blocks.append([uncommented.rstrip(), None])
|
||||
uncommented = ""
|
||||
count = int(match.group(2))
|
||||
comment = match.group(3)
|
||||
code = ""
|
||||
j = 0
|
||||
i += 1
|
||||
while j < count:
|
||||
line = re.sub("\n", "\\n", lines[i])
|
||||
code += line + "\n"
|
||||
j += 1
|
||||
i += 1
|
||||
blocks.append([code.strip("\n"), comment])
|
||||
else:
|
||||
uncommented += lines[i] + "\n"
|
||||
i += 1
|
||||
if uncommented:
|
||||
blocks.append([uncommented.rstrip(), None])
|
||||
return blocks
|
||||
|
||||
def latex_spaces(s):
|
||||
def replace(match):
|
||||
s = match.group(0)
|
||||
if False and len(s) == 1:
|
||||
return "~"
|
||||
else:
|
||||
result = "~" * len(s)
|
||||
result = f"\\hphantom{{{result}}}"
|
||||
return result
|
||||
space_pat = re.compile(" +", re.S)
|
||||
return space_pat.sub(replace, s)
|
||||
|
||||
def latex_unquote(s):
|
||||
quoted = "asciicircum quotesingle asciigrave asciitilde asciitilde backslash".split()
|
||||
quoted = [f"{{}}\text{e}{{}}" for e in quoted]
|
||||
result = s
|
||||
for q in quoted:
|
||||
result = re.sub(q, "X", result)
|
||||
result = re.sub(" ", "Y", result)
|
||||
return result
|
||||
|
||||
def longest_line(s):
|
||||
result = ""
|
||||
for line in latex_unquote(s).split("\n"):
|
||||
if len(line) > len(result):
|
||||
result = line
|
||||
return result
|
||||
|
||||
def literal_newline(s):
|
||||
def replace(match):
|
||||
before, after = match.groups()
|
||||
return f"{before}\\n{after}"
|
||||
backslash_pat = re.compile(r'(".*?)\n(.*?")', re.S)
|
||||
return backslash_pat.sub(replace, s)
|
||||
|
||||
class Code(klammer_base.Klammer_base):
|
||||
id = 0
|
||||
def __init__(self, K):
|
||||
super().__init__(K)
|
||||
self.text = phases.expand_whitespace_markers(self.text)
|
||||
|
||||
def html(self):
|
||||
if self.K_target == "html":
|
||||
self.text = literal_newline(self.text)
|
||||
# Escape Klammertext special characters so they survive
|
||||
# re-insertion into the katom stream after @eval
|
||||
self.text = self.text.replace("^", "^^")
|
||||
self.text = self.text.replace("#", "^#")
|
||||
self.text = self.text.replace("@", "^@")
|
||||
self.text = self.text.replace("|", "^|")
|
||||
self.blocks = get_blocks(self.text)
|
||||
result = ''
|
||||
for text, comment in self.blocks:
|
||||
border = "code_border" if comment else "code_no_border"
|
||||
body = E("div").body(text).cls(f"code_text {border}").str(None)
|
||||
if comment:
|
||||
body += "\n" + E("div").body(comment).cls("code_comment").str()
|
||||
result += E("div").body(body).cls("code_block").str()
|
||||
if self.number or self.caption:
|
||||
#result = html_util.add_caption(
|
||||
# result, "Listing", self.number, self.caption, "i", "left", "top")
|
||||
caption = kutil.caption_marker("Listing", self.caption)
|
||||
result = f'<div class="plain_caption code_caption">{caption}</div>{result}\n'
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def escape_latex(s):
|
||||
"""Escape LaTeX special characters in code text."""
|
||||
# Backslash must be first (before adding more backslashes)
|
||||
s = s.replace("\\", "\\textbackslash{}")
|
||||
s = s.replace("{", "\\{")
|
||||
s = s.replace("}", "\\}")
|
||||
s = s.replace("%", "\\%")
|
||||
s = s.replace("$", "\\$")
|
||||
s = s.replace("&", "\\&")
|
||||
s = s.replace("_", "\\_")
|
||||
s = s.replace("^", "\\textasciicircum{}")
|
||||
s = s.replace("~", "\\textasciitilde{}")
|
||||
s = s.replace("<", "\\textless{}")
|
||||
s = s.replace(">", "\\textgreater{}")
|
||||
return s
|
||||
|
||||
def code_box(self, text):
|
||||
kutil.msg(text)
|
||||
result = ""
|
||||
for line in text.rstrip().split("\n"):
|
||||
indent = len(line) - len(line.lstrip())
|
||||
eline = ("~" * indent) + line[indent:]
|
||||
print(indent, line)
|
||||
print(eline)
|
||||
result += eline + "\\\\\n"
|
||||
result = result[:-3]
|
||||
print("RESULT:")
|
||||
print(result)
|
||||
return result
|
||||
|
||||
def tex(self):
|
||||
parse_blocks(split_blocks(self.text))
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
|
||||
# Escape Klammertext special characters
|
||||
self.text = self.text.replace("^", "^^")
|
||||
self.text = self.text.replace("#", "^#")
|
||||
self.text = self.text.replace("@", "^@")
|
||||
self.text = self.text.replace("|", "^|")
|
||||
# Escape LaTeX special characters in code text
|
||||
self.text = Code.escape_latex(self.text)
|
||||
self.blocks = get_blocks(self.text)
|
||||
strutvis = "0pt"
|
||||
start_strut = f"\\rule[0pt]{{{strutvis}}}{{12pt}}"
|
||||
end_strut = f"\\rule[-6pt]{{{strutvis}}}{{12pt}}"
|
||||
caption_strut = f"\\rule[-8pt]{{{strutvis}}}{{6pt}}"
|
||||
indent = "8pt"
|
||||
comment_sep = "10pt"
|
||||
i = 0
|
||||
result = ""
|
||||
count = len(self.blocks)
|
||||
for text, comment in self.blocks:
|
||||
print("TEXT:")
|
||||
print(text)
|
||||
print("COMMENT:")
|
||||
print(comment)
|
||||
|
||||
|
||||
|
||||
|
||||
text = " " + re.sub("\n", " \n ", text) + " "
|
||||
longest = longest_line(text)
|
||||
text = latex_spaces(text)
|
||||
text = re.sub("\n", r"\\\\", text)
|
||||
text = f"{start_strut}\\ttfamily {text}{end_strut}"
|
||||
width = f"\\widthof{{\\ttfamily {longest}}}"
|
||||
code = L.environment("minipage", text, width) + "\\\\\n"
|
||||
print(code)
|
||||
|
||||
#code = "\\asymbox{" + self.code_box(text) + "}"
|
||||
#print(code)
|
||||
|
||||
code = text
|
||||
|
||||
if comment:
|
||||
#width = f"\\linewidth - {width} - {indent} - {comment_sep}"
|
||||
"""
|
||||
width = f"\\linewidth - \widestline - {indent} - {comment_sep}"
|
||||
code = f"\\fcolorbox{{Gray}}{{LightGray}}{{{code}}}"
|
||||
code += f"\\rule{{{comment_sep}}}{{{strutvis}}}" \
|
||||
+ L.environment("minipage", "\\sffamily\\small\\raggedright " + comment, width)
|
||||
"""
|
||||
code = "\\asymbox{" + self.code_box(text) + "}" + comment
|
||||
|
||||
result += f"\\rule{{{indent}}}{{{strutvis}}}{code}"
|
||||
if comment:
|
||||
if i < count - 1 and self.blocks[i+1][1]:
|
||||
result += "\\\\[4pt]"
|
||||
i += 1
|
||||
if not self.blocks[count-1][1]:
|
||||
result = result[:-4]
|
||||
if self.number or self.caption:
|
||||
caption = kutil.caption_marker("Listing", self.caption)
|
||||
if self.blocks[0][1]:
|
||||
caption += caption_strut
|
||||
strut = f"\\rule{{{indent}}}{{{strutvis}}}"
|
||||
result = f"{strut}\\emph{{\\it {caption}}}\\newline\n" + result + "\n"
|
||||
result = f"\\hypertarget{{Reference-Listing-{Code.id}}}{{}}\n{result}"
|
||||
Code.id += 1
|
||||
return result
|
||||
|
||||
def undash(s):
|
||||
result = s
|
||||
result = re.sub("__MDASH__", "---", result)
|
||||
result = re.sub("__NDASH__", "--", result)
|
||||
return result
|
||||
|
||||
class Code_fragment(klammer_base.Klammer_base):
|
||||
def __init__(self, K):
|
||||
super().__init__(K)
|
||||
|
||||
def html(self):
|
||||
#print(f"code: |{self.code_text}|")
|
||||
result = self.code_text.strip()
|
||||
result = undash(result)
|
||||
#result = re.escape(result)
|
||||
result = re.sub("<", "<", result)
|
||||
result = re.sub(" ", " ", result)
|
||||
#print(f"code: |{self.code_text}| -> |{result}|")
|
||||
return f'<span class="code">{result}</span>'
|
||||
|
||||
def tex(self):
|
||||
return f"{{\\tt {self.code_text.strip()}}}"
|
||||
|
||||
def show(s):
|
||||
print("-"*80)
|
||||
print(s)
|
||||
print("-"*80)
|
||||
|
||||
class Source(klammer_base.Klammer_base):
|
||||
def __init__(self, K):
|
||||
super().__init__(K)
|
||||
with open(self.filename) as fp:
|
||||
self.src = fp.read()
|
||||
|
||||
def tex(self):
|
||||
result = self.src
|
||||
# result = re.sub("#", "^#", result)
|
||||
# result = re.sub("\\^", "\\^", result)
|
||||
result = f"\\begin{{verbatim}}\n{result}\n\\end{{verbatim}}\n"
|
||||
return result
|
||||
|
||||
def html(self):
|
||||
result = escape_newlines(self.src.strip()) + "\n"
|
||||
result = re.sub("@", "^@", result)
|
||||
result = E("div").body(result).cls("code_text").str()
|
||||
return result
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
s = """
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
//1 One line commented
|
||||
int count = 12;
|
||||
//3 Two lines commented
|
||||
for (int i = 0; i < count; i++) {
|
||||
std::cout << "Counter: " << i << "\n";
|
||||
}
|
||||
//1 A really long comment for one line. A really long comment for one line. A really long comment for one line.
|
||||
std::cout << "End\n";
|
||||
}
|
||||
"""
|
||||
get_blocks(s);
|
||||
|
||||
|
||||
# class Code(klammer_base.Klammer_base):
|
||||
# def __init__(self, K):
|
||||
# super().__init__(K)
|
||||
# #self.show("Code")
|
||||
# if self.filename and self.text:
|
||||
# raise Exception("Both :text and :filename cannot be defined")
|
||||
# if K.filename:
|
||||
# with open(K.filename) as fp:
|
||||
# self.src = fp.read()
|
||||
# if K.pattern:
|
||||
# rgx = re.compile(f".*?({K.pattern}).*", re.S)
|
||||
# match = rgx.match(self.src)
|
||||
# if match is None:
|
||||
# raise Exception(f"Match fails for @source_code: {K.pattern}")
|
||||
# self.src = match.group(1)
|
||||
# self.src = kutil.protect_klammertext_special_characters(self.src)
|
||||
# else:
|
||||
# self.src = self.text
|
||||
|
||||
# def html(self):
|
||||
# #src = re.sub("\n", "<!-- -->", self.src) ?
|
||||
# #result = f'<pre class="code">\n{self.src}\n</pre>\n'
|
||||
# result = self.src
|
||||
# result = undash(result)
|
||||
# result = f'<pre>\n{result}\n</pre>\n'
|
||||
# return result
|
||||
|
||||
# def tex(self):
|
||||
# src = self.src
|
||||
# src = re.sub(r"\\{", "{", src)
|
||||
# src = re.sub(r"\\}", "}", src)
|
||||
# result = f"\\begin{{lstlisting}}\n{src}\n\\end{{lstlisting}}\n"
|
||||
# return result
|
||||
|
||||
# def txt(self):
|
||||
# return "x~ " + self.src
|
||||
|
||||
# class Pathname(klammer_base.Klammer_base):
|
||||
# def __init__(self, K):
|
||||
# super().__init__(K)
|
||||
|
||||
# def html(self):
|
||||
# return f'<span class="monospace">{self.s}</span>'
|
||||
|
||||
# def tex(self):
|
||||
# result = self.s
|
||||
# def replace(match):
|
||||
# return '\\{}'.format(match.group(1))
|
||||
# result = re.sub(r'\\', 'XXXBACKSLASHXXX', result)
|
||||
# result = re.compile('\s*__UNSPACE__\s*', re.S).sub('', result)
|
||||
# result = re.compile(r'([&${}%#_])').sub(replace, result)
|
||||
# result = re.sub('\^', r'\\^{}', result)
|
||||
# result = re.sub('~', r'\\~{}', result)
|
||||
# result = re.sub(r'XXXBACKSLASHXXX', r'{\\textbackslash}', result)
|
||||
# result = re.sub('\n', r'~\\\\\n', result.strip())
|
||||
# result = re.sub(' ', '$~$', result)
|
||||
# result = re.sub("'", r"{\\textquotesingle}", result)
|
||||
# result = re.sub('"', r'{\\textquotedbl}', result)
|
||||
# result = re.sub('--', '{-}{-}', result)
|
||||
# result = r'{{\normalfont\texttt{{{}}}}}'.format(result.strip())
|
||||
# result = re.sub(r'\{\\textbackslash\}\\#', '\\#', result)
|
||||
# if self.small:
|
||||
# result = '{{\\footnotesize{}}}'.format(result)
|
||||
# return result
|
||||
|
||||
# def txt(self):
|
||||
# return f"'{self.s}'"
|
||||
|
||||
@@ -25,13 +25,3 @@ class Color(klammer_base.Klammer_base):
|
||||
if self.text:
|
||||
result = '{{{} {}}}'.format(result, self.text)
|
||||
return result
|
||||
|
||||
def hex_color(name, hex):
|
||||
def c(h):
|
||||
return float(eval('0x' + h)) / 255.0
|
||||
r = c(hex[:2])
|
||||
g = c(hex[2:4])
|
||||
b = c(hex[4:])
|
||||
return '\\definecolor{{{}}}{{rgb}}{{{:.3f},{:.3f},{:.3f}}}'.format(
|
||||
name, r, g, b)
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ std::string tex_to_pdf(Machine& machine)
|
||||
ss << " " << line << "\n";
|
||||
}
|
||||
ss << "Check log file: " << outbase << ".log";
|
||||
throw Definition_error(ss.str(), Locator(), false);
|
||||
throw Definition_error(ss.str(), Locator());
|
||||
}
|
||||
if (std::regex_search(xelatex_log, std::regex("Package rerunfilecheck Warning:"))) {
|
||||
(void)K::log(1, "Rerunning xelatex because document structure has changed");
|
||||
@@ -115,7 +115,7 @@ std::string tex_to_pdf(Machine& machine)
|
||||
ss << " " << line << "\n";
|
||||
}
|
||||
ss << "Check log file: " << outbase << ".log";
|
||||
throw Definition_error(ss.str(), Locator(), false);
|
||||
throw Definition_error(ss.str(), Locator());
|
||||
}
|
||||
}
|
||||
warn_wide_tables(xelatex_log);
|
||||
|
||||
@@ -63,3 +63,20 @@
|
||||
@@
|
||||
|
||||
@@document.html,tex :: @eval :cpp *KLAMMERTEXT_HOME*/sks/document/document document @ @@
|
||||
|
||||
# The following definition of @document.txt is not adequate (it ignores :files,
|
||||
# for example), but it enables tests of other txt klammers for now. When one of
|
||||
# the arguments is empty, the justifcation postprocess should remove multiple
|
||||
# lines, but __VSPACE__ inserts a space character, which prevents the lines
|
||||
# removal from the justification function. You can also see the extra space
|
||||
# before the *subtitle* value.
|
||||
|
||||
@@document.txt ::
|
||||
@eval "*title*".upper() @ @nl@
|
||||
*subtitle*
|
||||
|
||||
*author* @nl@
|
||||
*date*
|
||||
|
||||
*text*
|
||||
@@
|
||||
|
||||
@@ -16,9 +16,9 @@ bool strbool(const std::string& s, const Locator& loc)
|
||||
std::vector<std::string> values = {"false", "False", "0", "true", "True", "1"};
|
||||
if (is_not_in(s, values)) {
|
||||
std::stringstream ss {};
|
||||
ss << "The value \"" << s << "\" is not a Boolean values. Possible values are:\n"
|
||||
ss << "The value \"" << s << "\" is not a Boolean value. Possible values are:\n "
|
||||
<< join(values, ", ");
|
||||
throw Argument_error(ss.str(), loc, false);
|
||||
throw Argument_error(ss.str(), loc);
|
||||
}
|
||||
bool result = (find(values.begin(), values.end(), s) - values.begin()) > 2;
|
||||
return result;
|
||||
@@ -51,10 +51,15 @@ Document_class::Document_class(Machine& machine) : Klammer_base(machine)
|
||||
// spaces, ~ expansion (resolve_filename_list in mac/file.cpp); the
|
||||
// existence checks resolve relative names against the input directory,
|
||||
// as parse_input_filename() will.
|
||||
m_files = resolve_filename_list(get("files"), get("K_input_dir"));
|
||||
// Filename-bearing values arrive ESCAPED for the target (the state
|
||||
// stores escaped values so they flow correctly into output); a filename
|
||||
// is programmatic use, so decode the markers first -- the C++ mirror of
|
||||
// the Python-side unescape_ktesc() rule. Found 2026-08-22: under tex,
|
||||
// ":files my_chapter" searched for "myKTESC005fKTESCchapter".
|
||||
m_files = resolve_filename_list(ktesc_resolve(get("files")), get("K_input_dir"));
|
||||
|
||||
m_css_text = get("css_text");
|
||||
m_css_filenames = resolve_filename_list(get("css_files"), get("K_input_dir"));
|
||||
m_css_filenames = resolve_filename_list(ktesc_resolve(get("css_files")), get("K_input_dir"));
|
||||
m_include_sks_css = strbool(get("include_sks_css"), loc);
|
||||
frame_background_color = get("frame_background_color");
|
||||
frame_text_color = get("frame_text_color");
|
||||
@@ -62,7 +67,7 @@ Document_class::Document_class(Machine& machine) : Klammer_base(machine)
|
||||
nav_text_color = get("nav_text_color");
|
||||
|
||||
js_text = get("js_text");
|
||||
m_js_filenames = resolve_filename_list(get("js_files"), get("K_input_dir"));
|
||||
m_js_filenames = resolve_filename_list(ktesc_resolve(get("js_files")), get("K_input_dir"));
|
||||
m_include_sks_js = strbool(get("include_sks_js"), loc);
|
||||
|
||||
// font_dirs = word_split(get("font_dirs"));
|
||||
@@ -138,34 +143,50 @@ void Document_class::save_string_input_as_file()
|
||||
|
||||
fs::path parse_input_filename(const std::string& s, const std::string& input_dir)
|
||||
{
|
||||
// The two-class rule (2026-08-22), replacing a three-stage search whose
|
||||
// second stage could quietly shadow a file beside the document:
|
||||
//
|
||||
// * a BARE WORD -- no directory separator, no extension -- is the kt/
|
||||
// SHORTCUT: ":files X" MEANS kt/X.kt in the root file's directory,
|
||||
// and nothing else. Missing is an immediate error whose message
|
||||
// teaches the convention, not a fallback. The kt/ directory is the
|
||||
// conventional home for a document's input files, and putting the
|
||||
// meaning entirely in the name lets several root documents share it.
|
||||
//
|
||||
// * anything else is a real PATHNAME, absolute or resolved against the
|
||||
// input file's directory (K_input_dir), so a document renders
|
||||
// identically wherever ktext is run from.
|
||||
//
|
||||
// Which file a name landed on is a DERIVED value, so "-v 1" reports it.
|
||||
fs::path p(s);
|
||||
bool bare = s.find('/') == std::string::npos && p.extension().empty();
|
||||
if (bare) {
|
||||
fs::path in_kt = fs::path(input_dir) / "kt" / (s + ".kt");
|
||||
if (!file_exists(in_kt.string())) {
|
||||
throw Argument_error(
|
||||
"The \":files\" name \"" + s + "\" is a bare word, which by "
|
||||
"convention means the file kt/" + s + ".kt in the root "
|
||||
"document's directory:\n"
|
||||
" " + in_kt.string() + "\n"
|
||||
"That file does not exist. Create it there, or write a real "
|
||||
"pathname (a name with a directory or the \".kt\" extension) "
|
||||
"to use a file elsewhere.",
|
||||
Locator::none());
|
||||
}
|
||||
// The resolved path itself shows kt/ -- no label needed.
|
||||
(void)K::log(1, "Input file \"" + s + "\": " + in_kt.string());
|
||||
return in_kt;
|
||||
}
|
||||
if (p.extension() != ".kt") {
|
||||
p += ".kt";
|
||||
}
|
||||
if (p.is_absolute()) {
|
||||
return p;
|
||||
}
|
||||
// A relative :files name resolves against the input file's directory
|
||||
// (K_input_dir), so a document renders identically wherever ktext is
|
||||
// run from; then the legacy kt/ subdirectory; a name found in neither
|
||||
// is returned as given (cwd-relative) and errors downstream.
|
||||
// Which of the three a name landed on is a DERIVED value -- the ".kt" may
|
||||
// have been supplied, and the directory certainly was -- so "-v 1" reports
|
||||
// it. A ":files chapter1" that quietly found kt/chapter1.kt rather than
|
||||
// the file beside the document is exactly what the author cannot see.
|
||||
fs::path in_input_dir = fs::path(input_dir) / p;
|
||||
if (file_exists(in_input_dir.string())) {
|
||||
(void)K::log(1, "Input file \"" + s + "\": " + in_input_dir.string());
|
||||
return in_input_dir;
|
||||
}
|
||||
fs::path in_kt_dir = fs::path(input_dir) / "kt" / p;
|
||||
if (file_exists(in_kt_dir.string())) {
|
||||
(void)K::log(1, "Input file \"" + s + "\": " + in_kt_dir.string()
|
||||
+ " (found in the kt/ subdirectory)");
|
||||
return in_kt_dir;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
void Document_class::write(const std::string& filename, const std::string& contents)
|
||||
{
|
||||
|
||||
@@ -113,38 +113,14 @@ std::string Document_class::font_definitions()
|
||||
ss << " --monospace: \"" << m_mono_font << "\", monospace;\n";
|
||||
ss << "}\n";
|
||||
}
|
||||
// Emit scale factors so sans and mono fonts match the serif font.
|
||||
// Three scaling methods (uncomment the desired one):
|
||||
// x-height: serif_xh / other_xh (matches lowercase, like fontspec MatchLowercase)
|
||||
// cap-height: serif_ch / other_ch (matches capitals)
|
||||
// average: mean(serif_xh,serif_ch) / mean(other_xh,other_ch) (compromise)
|
||||
float serif_xh = m_resolved_serif.xheight_ratio;
|
||||
float serif_ch = m_resolved_serif.capheight_ratio;
|
||||
float serif_avg = (serif_xh + serif_ch) / 2.0f;
|
||||
if (serif_avg > 0.0f) {
|
||||
auto scale = [&](const Resolved_font& other) -> std::string {
|
||||
float other_avg = (other.xheight_ratio + other.capheight_ratio) / 2.0f;
|
||||
if (other_avg > 0.0f && other_avg != serif_avg) {
|
||||
char buf[16];
|
||||
// float ratio = serif_xh / other.xheight_ratio; // x-height
|
||||
// float ratio = serif_ch / other.capheight_ratio; // cap-height
|
||||
float ratio = serif_avg / other_avg; // average
|
||||
std::snprintf(buf, sizeof(buf), "%.4f", ratio);
|
||||
return buf;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
std::string sans_scale = scale(m_resolved_sans);
|
||||
std::string mono_scale = scale(m_resolved_mono);
|
||||
if (!sans_scale.empty() || !mono_scale.empty()) {
|
||||
ss << ":root {\n";
|
||||
if (!sans_scale.empty())
|
||||
ss << " --sans-serif-scale: " << sans_scale << ";\n";
|
||||
if (!mono_scale.empty())
|
||||
ss << " --monospace-scale: " << mono_scale << ";\n";
|
||||
ss << "}\n";
|
||||
}
|
||||
}
|
||||
// Font-size normalization across families is no longer emitted from
|
||||
// here (2026-08-22): font.css declares "font-size-adjust: ex-height 0.5"
|
||||
// on body, and the browser renders every font at the same x-height --
|
||||
// the same computation the former --sans-serif-scale/--monospace-scale
|
||||
// factors did from build-time OS/2 metrics, but applied to EVERY family
|
||||
// switch instead of the four CSS sites that remembered to multiply.
|
||||
// The metric extraction in the font store remains (the tex path and
|
||||
// kdesc --font still use it).
|
||||
// Global font scale: applied to body font-size
|
||||
if (m_font_scale != 1.0f) {
|
||||
char buf[16];
|
||||
@@ -368,7 +344,15 @@ Document_class::insert_section_numbers(const std::string& marker, bool add_to_to
|
||||
{
|
||||
std::vector<int> levels(9, 0);
|
||||
std::smatch match {};
|
||||
std::string pattern = R"(<([\w-]+)\s*(.*?)>(.*?)MARKER\s*</span>\s*(.*?)<.*)";
|
||||
// The heading text (group 4) runs to the heading element's OWN closing
|
||||
// tag (the \1 backreference), not to the first "<": a nested element in
|
||||
// a section title -- @c's <span class="code">, an @i's <em> -- would
|
||||
// otherwise end the capture early and silently truncate the title in
|
||||
// BOTH tables of contents (article's single_page_toc and book's
|
||||
// navigation TOC read the same capture). Found 2026-08-22 via a @c in
|
||||
// an @s2 title; heading.cpp's section_rgx already used the
|
||||
// close-on-own-tag idiom.
|
||||
std::string pattern = R"(<([\w-]+)\s*(.*?)>(.*?)MARKER\s*</span>\s*(.*?)</\1>.*)";
|
||||
pattern = string_replace(pattern, "MARKER", marker);
|
||||
std::regex heading_rgx(pattern);
|
||||
std::vector<std::pair<std::string, std::string>> modified_components {};
|
||||
|
||||
@@ -38,104 +38,6 @@ std::string levels_to_section(std::vector<unsigned int> levels)
|
||||
int part_number = 1;
|
||||
int unnumbered_id = 0;
|
||||
|
||||
/*
|
||||
std::tuple<std::string, std::vector<Heading>, std::vector<unsigned int>, unsigned int>
|
||||
add_section_numbers(const std::string& s, const std::string& basename, std::vector<unsigned int> levels, unsigned int initial_id,
|
||||
std::map<std::string, std::string>& section_id_map)
|
||||
{
|
||||
(void)K::log(3);
|
||||
auto depth { levels.size() };
|
||||
std::vector<Heading> headings {};
|
||||
std::string text { s };
|
||||
std::regex section_rgx (R"((.*?)<h(\d)(.*?)>(.*?)</h\2>)");
|
||||
std::regex part_rgx (R"((.*?)<kt-part(.*?)>Part (\d+)\s*<br>\s*(.*?)\s*</kt-part>)");
|
||||
std::regex id_rgx(R"((.*?)id=\"([-\w]+)\"(.*))");
|
||||
|
||||
unsigned int id_number { initial_id };
|
||||
//std::string result {};
|
||||
std::stringstream result {};
|
||||
for (std::string line : regex_split(s, std::regex(R"(\n)"), false)) {
|
||||
std::smatch match {};
|
||||
if (std::regex_match(line, match, part_rgx)) {
|
||||
std::string pre { match[1] };
|
||||
std::string attr { match[2] };
|
||||
std::string level { match[3] };
|
||||
std::string title { match[4] };
|
||||
|
||||
std::string id_prefix { "_part_"};
|
||||
std::string id {};
|
||||
std::smatch id_match {};
|
||||
if (std::regex_match(attr, id_match, id_rgx)) {
|
||||
id = id_match[2];
|
||||
} else {
|
||||
id = id_prefix + std::to_string(part_number);
|
||||
}
|
||||
//title = "Part " + std::to_string(part_number) + " - " + title;
|
||||
//std::string section = "Part " + level;
|
||||
std::string section = "Part " + std::to_string(part_number);
|
||||
elements_t part_title
|
||||
{ html::elt("kt-part",
|
||||
{ html::elt("span", section).attr("class", "sectionnumber"),
|
||||
html::elt("span", trim(title)).attr("class", "sectiontitle") })
|
||||
.attr("id", id)
|
||||
.attr("data-level", level) };
|
||||
result << pre << part_title << "\n";
|
||||
headings.push_back(Heading(0, basename, section, id, "0", title));
|
||||
part_number += 1;
|
||||
} else if (std::regex_match(line, match, section_rgx)) {
|
||||
std::string pre { match[1] };
|
||||
int level { std::stoi(match[2]) };
|
||||
std::string attr { match[3] };
|
||||
std::string title { match[4] };
|
||||
bool numbered = attr.find("numbered") != std::string::npos;
|
||||
std::string section {};
|
||||
if (numbered) {
|
||||
levels[level-1] = levels[level-1] + 1;
|
||||
for (unsigned int li = level; li < depth; li++)
|
||||
levels[li] = 0;
|
||||
section = levels_to_section(levels);
|
||||
}
|
||||
std::string id {};
|
||||
std::smatch id_match {};
|
||||
if (std::regex_match(attr, id_match, id_rgx)) {
|
||||
id = id_match[2];
|
||||
} else {
|
||||
if (section.size() == 0) {
|
||||
id = "_su_" + std::to_string(unnumbered_id++);
|
||||
} else {
|
||||
id = "_s_" + string_replace(section, ".", "_");
|
||||
}
|
||||
}
|
||||
std::string link_target = file_basename(basename) + link_delimiter + id;
|
||||
section_id_map[title] = link_target;
|
||||
|
||||
//<h1 id="image-tests" style="clear:both;" class="headerlink">
|
||||
// <span class="sectionnumber">1</span>Image tests</h1>
|
||||
|
||||
elements_t title_parts {};
|
||||
if (numbered) {
|
||||
title_parts.push_back(html::elt("span", trim(section)).attr("class", "sectionnumber"));
|
||||
}
|
||||
title_parts.push_back(html::elt("span", trim(title)).attr("class", "sectiontitle"));
|
||||
|
||||
// msg() << "title_parts: " << title_parts << "\n";
|
||||
elements_t section_title
|
||||
{ html::elt("h"+std::to_string(level), title_parts)
|
||||
.attr("id", id)
|
||||
.attr("data-level", std::to_string(level))};
|
||||
// msg() << pre << section_title << "\n";
|
||||
result << pre << section_title << "\n";
|
||||
headings.push_back(Heading(level, basename, section, id, "0", title));
|
||||
} else {
|
||||
result << line << "\n";
|
||||
}
|
||||
}
|
||||
//std::cout << "add_section_numbers: " << result.str() << " "
|
||||
//<< headings.size() << " " << levels.size() << " " << id_number << "\n";
|
||||
return std::tuple(result.str(), headings, levels, id_number);
|
||||
}
|
||||
*/
|
||||
|
||||
std::string make_html_table_of_contents(std::vector<Heading> headings)
|
||||
{
|
||||
elements_t toc { html::elt("h1", "Contents") };
|
||||
|
||||
@@ -4,18 +4,23 @@
|
||||
--serif: Libre Baskerville, serif;
|
||||
--sans-serif: Open Sans, sans-serif;
|
||||
--monospace: Inconsolata, monospace;
|
||||
--sans-serif-scale: 1;
|
||||
--monospace-scale: 1;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--serif);
|
||||
font-size: 1rem;
|
||||
/* One normalization for every family switch: render each font so its
|
||||
x-height is 0.5em (the design's anchor -- EB Garamond comes UP ~19%,
|
||||
Open Sans down ~7%). Inherited, so headings, code, footnotes and any
|
||||
future rule are covered with no per-site scale factors; this replaced
|
||||
the --sans-serif-scale/--monospace-scale system (build-time metrics,
|
||||
applied at four sites and silently missed everywhere else). The tex
|
||||
target's equivalent is fontspec's MatchLowercase. */
|
||||
font-size-adjust: ex-height 0.5;
|
||||
}
|
||||
|
||||
tt, .tt, pre {
|
||||
font-family: var(--monospace);
|
||||
font-size: calc(1em * var(--monospace-scale));
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
@@ -23,16 +28,11 @@ h1, h2, h3, h4, h5 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.2rem * var(--sans-serif-scale));
|
||||
}
|
||||
|
||||
h2, h3, h4, h5 {
|
||||
font-size: calc(1.1rem * var(--sans-serif-scale));
|
||||
}
|
||||
/* Heading SIZES live in sks/section/css/section.css (one ladder, one
|
||||
place); this file assigns only the family. */
|
||||
|
||||
p {
|
||||
line-height: 1.3;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.plain {
|
||||
@@ -50,7 +50,6 @@ p {
|
||||
|
||||
.sansserif {
|
||||
font-family: var(--sans-serif);
|
||||
font-size: calc(1em * var(--sans-serif-scale));
|
||||
}
|
||||
|
||||
.ritalic {
|
||||
|
||||
@@ -33,7 +33,7 @@ def html_fontify(text, font_symbol, font_size):
|
||||
result = text
|
||||
if cls:
|
||||
if font_symbol == "c":
|
||||
result = re.sub(" ", " ", result)
|
||||
result = re.sub(" ", " ", result)
|
||||
cls = f'class="{cls}"'
|
||||
sty = ""
|
||||
if font_size != 1:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@@page.html body :title :
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta charset="utf-8"/>
|
||||
<title>*title*</title>
|
||||
<style>
|
||||
body { font-family:Palatino,roman; margin:2em 5em; }
|
||||
|
||||
@@ -13,15 +13,39 @@ def unescape_ktesc(s):
|
||||
return result
|
||||
return re.sub(r'KTESC([0-9a-f]+)KTESC', replace, s)
|
||||
|
||||
def escape_ktesc(s):
|
||||
"""A KTESC marker for the characters of s -- the mirror of
|
||||
Target::escape_marker() in mac/target.cpp (4 lowercase hex digits per
|
||||
byte). The marker is inert through every re-read and through the
|
||||
target's typographic transform pass, and decodes back to the characters
|
||||
at final escape resolution (ktesc_resolve)."""
|
||||
return 'KTESC' + ''.join(f'{ord(c):04x}' for c in s) + 'KTESC'
|
||||
|
||||
# The characters the SKS targets' typographic transforms act on: the
|
||||
# hyphen runs (-- and ---), the quote conventions (` ' `` ''), and ~.
|
||||
# Verbatim text (@c, @code, @source_listing) must reach the output exactly
|
||||
# as written, so the code klammers hide each of these as a KTESC marker --
|
||||
# a transform source can then never match -- and the markers decode after
|
||||
# the transform pass has run.
|
||||
# SYNC: the transform tables of the html and txt targets in
|
||||
# sks/target/target.k. A transform built from a new character needs it
|
||||
# added here, or verbatim text will show the transformed form.
|
||||
TYPOGRAPHIC_CHARS = "-'`~"
|
||||
|
||||
def hide_typographic(s):
|
||||
"""Hide the typographically active characters of s as KTESC markers so
|
||||
the target's transform pass cannot change verbatim text."""
|
||||
for c in TYPOGRAPHIC_CHARS:
|
||||
s = s.replace(c, escape_ktesc(c))
|
||||
return s
|
||||
|
||||
class Klammer_base:
|
||||
def __init__(self, K):
|
||||
#args = {k:escape(v) for k, v in K.__dict__.items()
|
||||
args = {k:v for k, v in K.__dict__.items()
|
||||
if not k.startswith('__')}
|
||||
|
||||
for key in args:
|
||||
setattr(self, key, args[key])
|
||||
|
||||
#setattr(self, "_klammer_name", klammer_name)
|
||||
#pprint.pprint(self.__dict__)
|
||||
|
||||
|
||||
@@ -21,10 +21,11 @@ def msg(text=""):
|
||||
|
||||
def escape(s):
|
||||
result = s
|
||||
|
||||
# These were commented out -- has it been replaced?
|
||||
# It's only used in kutil/klammer_base.py and block/block.py
|
||||
# and does nothing now.
|
||||
#result = re.sub(r"\b", r"\b", result)
|
||||
#result = re.sub(r"\t", r"\t", result)
|
||||
|
||||
#result = re.sub("\f", r"\\f", result)
|
||||
#result = re.sub("\v", r"\\v", result)
|
||||
#result = re.compile("\\(.)").sub(r"\\\1", result)
|
||||
|
||||
@@ -13,8 +13,8 @@ def html_link(target, link_text, is_section, no_quotation_marks):
|
||||
link_text = target if no_quotation_marks else f"“{target}”"
|
||||
else:
|
||||
# Allow long URLs to break at a slash:
|
||||
link_text = re.sub('/', '/<wbr>', target)
|
||||
link_text = re.sub('/<wbr>/<wbr>', '//', link_text)
|
||||
link_text = re.sub('/', '/<wbr/>', target)
|
||||
link_text = re.sub('/<wbr/>/<wbr/>', '//', link_text)
|
||||
link_text = f'<tt>{link_text}</tt>'
|
||||
target = f"[{target}]" if is_section else target
|
||||
#result = f"[__link__{target}__{link_text}__]"
|
||||
|
||||
331
sks/list/list.py
331
sks/list/list.py
@@ -86,7 +86,7 @@ class Define(klammer_base.Klammer_base):
|
||||
strut = r"\rule[-8pt]{0pt}{8pt}"
|
||||
result = "\n\n".join([f"\\item[{{{fontify(term)}{strut}}}] {definition}"
|
||||
for term,definition in self.items])
|
||||
result = f"\\begin{{description}}[labelindent=12pt,nosep,itemsep=6pt,parsep=.5\parskip,style=nextline]\n{result}\n\\end{{description}}"
|
||||
result = f"\\begin{{description}}[labelindent=12pt,nosep,itemsep=6pt,parsep=.5\\parskip,style=nextline]\n{result}\n\\end{{description}}"
|
||||
return result
|
||||
|
||||
# Needs to be implemented:
|
||||
@@ -99,11 +99,11 @@ class Lines(klammer_base.Klammer_base):
|
||||
super().__init__(K)
|
||||
|
||||
def html(self):
|
||||
#return " <br>".join(self.s.strip().split("\n"))
|
||||
#return " <br/>".join(self.s.strip().split("\n"))
|
||||
#return f'<pre style="font-family: var(--serif)">{self.s}</pre>'
|
||||
#return f'<pre>{self.s}</pre>'
|
||||
result = self.s
|
||||
result = re.sub("\n", "<br>\n", result)
|
||||
result = re.sub("\n", "<br/>\n", result)
|
||||
result = re.sub(" ", "&^#160;", result)
|
||||
return result
|
||||
|
||||
@@ -114,331 +114,6 @@ class Lines(klammer_base.Klammer_base):
|
||||
return self.s.strip()
|
||||
|
||||
|
||||
"""
|
||||
def lines(K):
|
||||
return {'html' : html_lines,
|
||||
'tex' : tex_lines,
|
||||
'pdf' : tex_lines}[K._target](K)
|
||||
|
||||
def tex_lines(K):
|
||||
return " \\\\\n".join(K.s.strip().split("\n"))
|
||||
|
||||
def html_lines(K):
|
||||
return " <br>".join(K.s.strip().split("\n"))
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
def definition_item(K):
|
||||
if K._format == 'html':
|
||||
return '<dt>{}</dt>\n<dd>{}</dd>'.format(
|
||||
format_for_paragraphs(K.term),
|
||||
format_for_paragraphs(K.desc))
|
||||
else:
|
||||
leading = '' if K.inlist else '\\vspace*{-16pt}'
|
||||
return '\\item[{}]\\leavevmode{}\n\n{}'.format(
|
||||
K.term, leading, K.desc)
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
def list(K, list_type):
|
||||
items = kutil.rest_args(K.list_items)
|
||||
return {'html' : html_list,
|
||||
'tex' : tex_list,
|
||||
'pdf' : tex_list}[K._target](K, list_type, items)
|
||||
|
||||
def html_list(K, list_type, items):
|
||||
body = "\n" + "".join([E("li").body(e).str()+"\n" for e in items])
|
||||
result = E(list_type).body(body).str().rstrip()
|
||||
return result
|
||||
|
||||
def tex_list(K, list_type, items):
|
||||
body = "\n\n".join([f"\\item {e}" for e in items])
|
||||
command = {'ol' : 'enumerate', 'ul' : 'itemize'}[list_type]
|
||||
topsep = '[topsep=0pt]'
|
||||
listsep = '\\setlist{nolistsep}' if K.cmp else ''
|
||||
result = f'{listsep}\n\\begin{{{command}}}{topsep}\n{body}\n\\end{{{command}}}\n'
|
||||
return result
|
||||
|
||||
def describe(K):
|
||||
items = "\n\n".join(kutil.rest_args(K.descriptions))
|
||||
return {'html' : html_describe,
|
||||
'tex' : tex_describe,
|
||||
'pdf' : tex_describe}[K._target](K, items)
|
||||
|
||||
def html_describe(K, items):
|
||||
return E("dl").body(items).str()
|
||||
|
||||
def tex_describe(K, items):
|
||||
return f'\\begin{{description}}\n{items}\n\\end{{description}}'
|
||||
|
||||
|
||||
def entry(K):
|
||||
return {'html' : html_entry,
|
||||
'tex' : tex_entry,
|
||||
'pdf' : tex_entry}[K._target](K)
|
||||
|
||||
def html_entry(K):
|
||||
#return f'<dt>{K.item}</dt>\n<dd>{K.description}</dd>'
|
||||
return E("dt").body(K.item).str(0) + E("dd").body(K.description).str(0)
|
||||
|
||||
def tex_entry(K):
|
||||
return f'\\item[{K.item}]\n{K.description}\n'
|
||||
|
||||
|
||||
def lines(K):
|
||||
return {'html' : html_lines,
|
||||
'tex' : tex_lines,
|
||||
'pdf' : tex_lines}[K._target](K)
|
||||
|
||||
def tex_lines(K):
|
||||
return " \\\\\n".join(K.s.strip().split("\n"))
|
||||
|
||||
def html_lines(K):
|
||||
return " <br>".join(K.s.strip().split("\n"))
|
||||
|
||||
"""
|
||||
|
||||
|
||||
r"""
|
||||
# List items
|
||||
|
||||
paragraph_separator_re = re.compile('\n *\n', re.S)
|
||||
|
||||
def format_for_paragraphs(s):
|
||||
result = s
|
||||
if len(paragraph_separator_re.findall(result)) > 0:
|
||||
result = '\n\n{}\n\n'.format(result)
|
||||
return result
|
||||
|
||||
def li(K):
|
||||
if K._format == 'html':
|
||||
result = K.s.strip()
|
||||
result = format_for_paragraphs(result)
|
||||
result = '<li>{}</li>'.format(result)
|
||||
else:
|
||||
#result = '\\item{{\\Kinlisttrue {}}} '.format(K.s.strip())
|
||||
#result = '\\item\\begin\\Kinlisttrue {}\\end '.format(K.s.strip())
|
||||
#result = '\\item\\parbox{{\\linewidth}}{{\\Kinlisttrue {}}} '.format(K.s.strip())
|
||||
#result = '\\item\\Kinlisttrue {} '.format(K.s.strip())
|
||||
result = '\\item {} '.format(K.s.strip())
|
||||
|
||||
if K.keep:
|
||||
result = '\\parbox{{\\linewidth}}{{{}}}'.format(result)
|
||||
return result
|
||||
|
||||
def mli(K):
|
||||
result = '@0item {} | @0ulc\n'.format(K.args[0])
|
||||
result += "\n".join(['@0li {} li@'.format(e) for e in K.args[1:]]) + '\n'
|
||||
result += ' ulc@ item@\n'
|
||||
return result
|
||||
|
||||
|
||||
# Ordered and unordered lists:
|
||||
|
||||
def list_html(K, list_type):
|
||||
result = ''
|
||||
if K.list_items[0][0:4] != '<li>':
|
||||
result = "\n".join(['<li>{}</li>'.format(e) for e in K.list_items])
|
||||
else:
|
||||
result = K.list_items[0]
|
||||
tag = 'ol'
|
||||
if list_type == 'ul':
|
||||
tag = 'ul style="list-style-type:{};"'.format(K.bullet)
|
||||
cls = 'compressed' if K.cmp else ''
|
||||
if list_type == 'ul' and K.bullet == 'none':
|
||||
cls += ' nobullet'
|
||||
cls = ' class="{}"'.format(cls.strip())
|
||||
|
||||
result = '<{}{}>\n{}\n</{}>\n'.format(tag, cls, result, list_type)
|
||||
return result
|
||||
|
||||
def list_latex(K, list_type):
|
||||
result = ''
|
||||
first_word = K.list_items[0][:5]
|
||||
if first_word not in {'\\item', '\\parb'}:
|
||||
result = " " + "\n".join(['@0li {} li@'.format(e.strip()) for e in K.list_items])
|
||||
else:
|
||||
result = K.list_items[0]
|
||||
#options = 'nosep,itemsep=1pt,topsep=0pt,partopsep=8pt,parsep=4pt'
|
||||
options = 'nosep,itemsep={}pt'.format(2 if K.cmp else 6)
|
||||
if K.cmp:
|
||||
itemsep=',nosep'
|
||||
else:
|
||||
itemsep=''
|
||||
|
||||
'''
|
||||
if list_type == 'ul' and K.bullet == 'none' and K.indent:
|
||||
left_margin = ',leftmargin=18pt'
|
||||
else:
|
||||
left_margin = ''
|
||||
'''
|
||||
|
||||
left_margin = ''
|
||||
if K.in_define:
|
||||
if list_type == 'ul':
|
||||
left_margin = ',leftmargin=4pt'
|
||||
else:
|
||||
left_margin = ',leftmargin=16pt'
|
||||
|
||||
if K.parsep:
|
||||
parsep = K.parsep
|
||||
else:
|
||||
parsep = 2 if K.cmp else 6
|
||||
|
||||
#options = '[nosep,parsep={}pt{},{}]'.format(parsep, left_margin,itemsep)
|
||||
options = '[parsep={}{}{}]'.format(parsep, left_margin, itemsep)
|
||||
|
||||
'''
|
||||
if K.cmp:
|
||||
options = '[nosep]'
|
||||
else:
|
||||
options = ''
|
||||
'''
|
||||
|
||||
#options = '[itemsep={}]'.format(K.itemsep) if K.itemsep else ''
|
||||
|
||||
#options = '[topsep=0pt,parsep={}pt]'.format(2 if K.cmp else 6)
|
||||
#options = '\\setlength{{\\topsep}{0pt}}\n\\setlength{{\parsep}}{{{}}}\n'.format(
|
||||
bullet = ''
|
||||
if list_type == 'ul' and K.bullet == 'none':
|
||||
bullet = '\\renewcommand\\labelitemi{\\hspace*{-8pt}}'
|
||||
itemsep = '\n\\setlength{{\\itemsep}}{{{}}}\n'.format(K.itemsep) if K.itemsep else ''
|
||||
#result = '''\\listvmargin%
|
||||
result = '''\\begin{{{0}}}{1}{2}
|
||||
{3}
|
||||
{4}
|
||||
\\end{{{0}}}
|
||||
'''.format('itemize' if list_type == 'ul' else 'enumerate',
|
||||
options, itemsep, bullet, result.strip())
|
||||
result = re.compile('\n\n', re.S).sub('\n', result)
|
||||
result = re.compile(r' +\\begin', re.S).sub(r'\\begin', result)
|
||||
if K.uncover:
|
||||
N = 1
|
||||
def replace(match):
|
||||
nonlocal N
|
||||
result = '{}<{}-> '.format(match.group(1), N)
|
||||
N += 1
|
||||
return result
|
||||
result = re.compile(r'(\\item(\[|\s))').sub(replace, result)
|
||||
return result
|
||||
|
||||
#\\listvmargin
|
||||
|
||||
def ul(K):
|
||||
if K._format == 'html':
|
||||
return list_html(K, 'ul')
|
||||
elif K._format == 'latex':
|
||||
return list_latex(K, 'ul')
|
||||
else:
|
||||
util.format_not_defined(K._format, 'ul')
|
||||
|
||||
def ol(K):
|
||||
if K._format == 'html':
|
||||
return list_html(K, 'ol')
|
||||
elif K._format == 'latex':
|
||||
return list_latex(K, 'ol')
|
||||
else:
|
||||
util.format_not_defined(K._format, 'ol')
|
||||
|
||||
# Definitions
|
||||
|
||||
def definition_item(K):
|
||||
if K._format == 'html':
|
||||
return '<dt>{}</dt>\n<dd>{}</dd>'.format(
|
||||
format_for_paragraphs(K.term),
|
||||
format_for_paragraphs(K.desc))
|
||||
else:
|
||||
leading = '' if K.inlist else '\\vspace*{-16pt}'
|
||||
return '\\item[{}]\\leavevmode{}\n\n{}'.format(
|
||||
K.term, leading, K.desc)
|
||||
|
||||
def dfont(K):
|
||||
if K._format == 'latex':
|
||||
result = {'r' : '\\normalfont',
|
||||
'i' : '\\normalfont\\itshape',
|
||||
'b' : '\\normalfont\\bfseries',
|
||||
't' : '\\normalfont\\ttfamily',
|
||||
's' : '\\normalfont\\sffamily\\large'
|
||||
}[K.font]
|
||||
elif K._format == 'html':
|
||||
s = K.term
|
||||
result = {'r' : s,
|
||||
'i' : '<i>{}</i>'.format(s),
|
||||
'b' : '<b>{}</b>'.format(s),
|
||||
't' : '<span class="monospace">{}</span>'.format(s),
|
||||
's' : '<span class="sansserif">{}</span>'.format(s)
|
||||
}[K.font]
|
||||
else:
|
||||
error.Klammertext_error(
|
||||
'There is not defined format named "{}".'.format(K._format))
|
||||
return result
|
||||
|
||||
def escape_code_term(s):
|
||||
def replace(match):
|
||||
left, term, right = match.groups()
|
||||
term = re.sub('\[', r'$\\lbrack$\\,', term)
|
||||
term = re.sub('\]', r'\\,$\\rbrack$', term)
|
||||
#term = re.sub('\[', '\\[', term)
|
||||
#term = re.sub('\]', '\\]', term)
|
||||
term = re.sub('--', '-{}-', term)
|
||||
result = '{}{}{}'.format(left, term, right)
|
||||
return result
|
||||
pat = re.compile(r'(\\item\[)(.*?)(\]\\)', re.S)
|
||||
return pat.sub(replace, s)
|
||||
|
||||
def define(K):
|
||||
if K._format == 'latex':
|
||||
#itemsep=1pt,topsep=2pt,partopsep=8pt,parsep=4pt,%
|
||||
options = 'nosep,parsep=4pt,leftmargin=32pt'
|
||||
fontname = {'r' : '\\normalfont',
|
||||
'i' : '\\normalfont\\itshape',
|
||||
'b' : '\\normalfont\\bfseries',
|
||||
't' : '\\normalfont\\ttfamily',
|
||||
's' : '\\normalfont\\sffamily',
|
||||
'si' : '\\normalfont\\sffamily\\itshape',
|
||||
'sb' : '\\normalfont\\sffamily\\bfseries',
|
||||
'c' : '\\normalfont',
|
||||
}[K.font]
|
||||
term = K.s
|
||||
if K.font == 'c':
|
||||
term = escape_code_term(term)
|
||||
#term = re.compile('item\[(.*?)\]', re.S).sub(r'item[\\verb+\1+]', term)
|
||||
#term = re.compile('
|
||||
return '''
|
||||
\\listvmargin \\begin{{description}}[{},
|
||||
labelindent=16pt,%
|
||||
style=nextline,font={}]
|
||||
%\\raggedright
|
||||
{}
|
||||
\\end{{description}} \\listvmargin\n'''.format(options,fontname, term)
|
||||
|
||||
elif K._format == 'html':
|
||||
term_class = {'r' : 'normal',
|
||||
'i' : 'italic',
|
||||
'b' : 'bold',
|
||||
't' : 'monospace',
|
||||
's' : 'sansserifbody',
|
||||
'si' : 'sansserifitalic',
|
||||
'sb' : 'sansserifbold',
|
||||
'c' : 'normal',
|
||||
}[K.font]
|
||||
term = K.s
|
||||
if K.font == 'c':
|
||||
#term = re.sub('–', '‐'*2, term)
|
||||
term = re.sub('–', '- __UNSPACE__ -', term)
|
||||
body = re.sub('<dt>', '<dt class="{}">'.format(term_class), term)
|
||||
return '''
|
||||
<dl>
|
||||
{}
|
||||
</dl>'''.format(body)
|
||||
"""
|
||||
|
||||
|
||||
def make_column_lists(lst, count):
|
||||
cols = math.ceil(len(lst) / count)
|
||||
|
||||
@@ -8,7 +8,7 @@ kt-part {
|
||||
padding-top: 1.5rem;
|
||||
margin-top: 0px;
|
||||
font-family: var(--sans-serif);
|
||||
font-size: 1.5rem;
|
||||
/* font-size: 2.03rem; */ /* ladder top: 1.125^6 ... */
|
||||
}
|
||||
|
||||
kt-chapter {
|
||||
@@ -16,10 +16,11 @@ kt-chapter {
|
||||
padding-top: 1.5rem;
|
||||
margin-top: 0px;
|
||||
font-family: var(--sans-serif);
|
||||
font-size: 1.5rem;
|
||||
/* font-size: 1.80rem; */ /* ... 1.125^5 */
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
h1 {
|
||||
padding-top: 1.5rem;
|
||||
margin-top: 0;
|
||||
@@ -27,10 +28,58 @@ h1 {
|
||||
|
||||
|
||||
h2, h3, h4, h5 {
|
||||
padding-top: 0.5rem;
|
||||
margin-bottom: .5rem;
|
||||
padding-top: 0.25rem;
|
||||
margin-bottom: .25rem;
|
||||
padding-bottom: 0rem;
|
||||
}
|
||||
*/
|
||||
|
||||
/* The heading ladder: a modular scale, ratio 1.125 (a "major second"),
|
||||
flattening to weight below h4 -- seven visibly distinct sizes is more
|
||||
hierarchy than an eye tracks. font-size-adjust (font.css) makes these
|
||||
steps family-independent, so the rem values are the whole truth. */
|
||||
/*
|
||||
h1 {
|
||||
font-size: 1.60rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.42rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.27rem;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.13rem;
|
||||
}
|
||||
h5, h6 {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Too big. How about: */
|
||||
|
||||
kt-part { font-size: 1.4rem; }
|
||||
kt-chapter { font-size: 1.4rem; }
|
||||
h1 { font-size: 1.3rem; }
|
||||
h2 { font-size: 1.2rem; }
|
||||
h3 { font-size: 1.15rem; }
|
||||
h4, h5, h6 { font-size: 1.125rem; }
|
||||
|
||||
|
||||
/* Vertical space not used to indicate hierarchical level: */
|
||||
|
||||
kt-part, kt-chapter, h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 1.2rem;
|
||||
margin-bottom: 0.0rem;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Table of contents: */
|
||||
|
||||
.sectiontitle {
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
@@s1.k title :id :n : Top level division, numbered @@
|
||||
@@s1.html,tex :: @eval section.Section(K, 1) eval@ @@
|
||||
|
||||
# Temporary hack for testing txt target; need to number section headings.
|
||||
@@s1.txt :: *title* @@
|
||||
|
||||
@@s2.k title :id :n : Second level division, numbered @@
|
||||
@@s2.html,tex :: @eval section.Section(K, 2) eval@ @@
|
||||
|
||||
|
||||
@@ -15,11 +15,6 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------------- */
|
||||
|
||||
.bgap {
|
||||
@@ -179,3 +174,20 @@
|
||||
margin: 0 1em 0 1em;
|
||||
}
|
||||
*/
|
||||
|
||||
.footnote_in_text {
|
||||
vertical-align: baseline;
|
||||
position: relative;
|
||||
top: -0.4em;
|
||||
font-size: 70%;
|
||||
line-height: 0;
|
||||
padding-left: 1px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.footnote_rule {
|
||||
width: 40%;
|
||||
margin: 3lh 0 .5lh 0;
|
||||
border: none;
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import kutil
|
||||
_novalue = '__no_value__'
|
||||
|
||||
class E:
|
||||
void_elements = set("area base br col embed hr img input link meta param source track wbr".split())
|
||||
void_elements = "area base br col embed hr img input link meta source track wbr".split()
|
||||
def __init__(self, tag):
|
||||
self._tag = tag
|
||||
self._sty = []
|
||||
@@ -17,6 +17,7 @@ class E:
|
||||
self._attr = []
|
||||
self.no_value = '__novalue__'
|
||||
|
||||
|
||||
def sty(self, name, value=_novalue):
|
||||
if name and value is _novalue:
|
||||
self._sty.append(name.strip(';'))
|
||||
@@ -49,6 +50,7 @@ class E:
|
||||
self._attr.append(attr)
|
||||
return self
|
||||
|
||||
|
||||
def __str__(self):
|
||||
def label(name, s):
|
||||
return ' {}="{}"'.format(name, s) if s else ''
|
||||
@@ -60,7 +62,16 @@ class E:
|
||||
b = "\n".join(self._body)
|
||||
tag = '{}{}{}{}'.format(self._tag, a, c, s).strip()
|
||||
end_tag = '</{}>'.format(self._tag) if self._tag not in E.void_elements else ""
|
||||
return '<{}>{}{}\n'.format(tag, b, end_tag)
|
||||
no_body = self._tag in E.void_elements
|
||||
if b and no_body:
|
||||
raise Exception(f"Tag {self._tag} incorrectly defines a body.")
|
||||
if no_body:
|
||||
result = f'<{tag}/>'
|
||||
else:
|
||||
result = f'<{tag}>{b}{end_tag}\n' # .format(tag, b, end_tag)
|
||||
if no_body:
|
||||
print(result)
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
@@ -80,6 +91,9 @@ class E:
|
||||
result = result.rstrip() + "\n"
|
||||
return result
|
||||
|
||||
def color(rgb):
|
||||
return "rgb(" + ",".join([f"{float(e)*100}%" for e in rgb.split(",")]) + ")"
|
||||
|
||||
def html_indent(filename):
|
||||
command = "(progn (setq make-backup-files nil) (mark-whole-buffer) "
|
||||
command += "(indent-region (point-min) (point-max) nil) (save-buffer))"
|
||||
@@ -99,10 +113,10 @@ def page(head_elt, body, js_files=[], load_jquery=True):
|
||||
|
||||
def head(title, js_files=[], js_code="", css_files=[], css_code="",
|
||||
include_fonts=True, google_font=[], favicon=None, load_jquery=True):
|
||||
result = '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
||||
result += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n'
|
||||
result = '<meta name="viewport" content="width=device-width, initial-scale=1"/>\n'
|
||||
result += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\n'
|
||||
if favicon:
|
||||
result += f'<link rel="icon" type="image/x-icon" href="{favicon}">\n'
|
||||
result += f'<link rel="icon" type="image/x-icon" href="{favicon}"/>\n'
|
||||
result += "".join([E("link").attr("href", e).attr("rel", "stylesheet").str() for e in css_files])
|
||||
fonts = None
|
||||
css = ''
|
||||
|
||||
@@ -35,7 +35,7 @@ def block(material):
|
||||
required to know that a target distinguishes horizontal from
|
||||
vertical mode. Asserted by sks/tst/paragraph_test.sh.
|
||||
"""
|
||||
return "\\par\n" + material.strip("\n") + "\n\\par\n"
|
||||
return "^'\\par'^\n" + material.strip("\n") + "\n^'\\par'^\n"
|
||||
|
||||
def environment(name, body, required=None, optional=None):
|
||||
req = f"{{{required}}}" if required else ""
|
||||
@@ -164,3 +164,35 @@ def add_caption(element, caption_label, number, caption_text, latex_width,
|
||||
element = minipage(caption + "\\rule[-0.75\\baselineskip]{0pt}{0pt}\n" + element,
|
||||
latex_width, vertical="t")
|
||||
return caption_wrapper(element, hpos, offset=offset)
|
||||
|
||||
def define_color(name, rgb_or_hex):
|
||||
if "," in rgb_or_hex:
|
||||
r, g, b = [float(e) for e in rgb_or_hex.split(",")]
|
||||
else:
|
||||
def c(h):
|
||||
return float(eval('0x' + h)) / 255.0
|
||||
r = c(rgb_or_hex[:2])
|
||||
g = c(rgb_or_hex[2:4])
|
||||
b = c(rgb_or_hex[4:])
|
||||
return r'\\definecolor{{{}}}{{rgb}}{{{:.3f},{:.3f},{:.3f}}}'.format(name, r, g, b)
|
||||
|
||||
COLOR_BOX_TEMPLATE = r'''
|
||||
^'\hfill\begingroup
|
||||
COLOR
|
||||
\setlength{\fboxsep}{8pt}
|
||||
\fcolorbox{bordercolor}{localcolor}{
|
||||
\parbox{WIDTH\linewidth}{\raggedright\setlength{\parskip}{8pt}'^
|
||||
TEXT
|
||||
^'}}\endgroup'^
|
||||
'''.strip()
|
||||
|
||||
def color_box(text, width=1, box_color="1,1,1", border_color="0,0,0"):
|
||||
color_definitions = define_color("localcolor", box_color) + "\n" \
|
||||
+ define_color("bordercolor", border_color)
|
||||
result = COLOR_BOX_TEMPLATE
|
||||
for old, new in [['TEXT', re.sub(r'\\', r'\\\\', text)],
|
||||
['COLOR', color_definitions],
|
||||
['WIDTH', str(width)]]:
|
||||
result = re.sub(old, new, result)
|
||||
result = block(result)
|
||||
return result
|
||||
|
||||
@@ -1,331 +1,93 @@
|
||||
import sys, os, re, textwrap, pprint
|
||||
|
||||
basedir = f'{os.environ["KLAMMERTEXT_HOME"]}/sks'
|
||||
sys.path = [f"{basedir}/kutil"] + sys.path
|
||||
sys.path = [f"{basedir}/target"] + sys.path
|
||||
|
||||
import kutil
|
||||
import html_util
|
||||
|
||||
def tex_to_pdf(text):
|
||||
print("in tex_to_pdf")
|
||||
|
||||
|
||||
|
||||
def expand_whitespace_markers(text, K=None):
|
||||
def count(count_match):
|
||||
count = int(count_match) if count_match else 1
|
||||
return count
|
||||
def replace_spaces(match):
|
||||
return " " * count(match.group(1))
|
||||
def replace_newlines(match):
|
||||
return "\n" * count(match.group(1))
|
||||
|
||||
result = text
|
||||
|
||||
# result = re.sub(r"\s*#-\s*", "", result)
|
||||
|
||||
# space_pat = re.compile(r" *#\+(\d*) *", re.S)
|
||||
# print("hits:", space_pat.findall(text))
|
||||
|
||||
# for p in space_pat.findall(text):
|
||||
# print("FOUND:", p)
|
||||
|
||||
|
||||
#
|
||||
#result = re.sub(" ", "SPACE", result)
|
||||
#result = re.sub("#/", "\n", result)
|
||||
#result = re.sub("X", " ", result)
|
||||
|
||||
result = re.compile(r" *#- *", re.S).sub("", result)
|
||||
result = re.compile(r" *#\+(\d*) *", re.S).sub(replace_spaces, result)
|
||||
result = re.compile(r"\s*\#\/(\d*)\s*", re.S).sub(replace_newlines, result)
|
||||
|
||||
#print(result)
|
||||
#sys.exit(0)
|
||||
#print("expand_whitespace_markers", text, result)
|
||||
return result
|
||||
|
||||
|
||||
def handle_dashes(html_text, K=None):
|
||||
def replace(match):
|
||||
tag_start, text, tag_end = match.groups()
|
||||
text = re.sub("__MDASH__", "---", text)
|
||||
text = re.sub("__NDASH__", "--", text)
|
||||
result = f"{tag_start}{text}{tag_end}"
|
||||
return result
|
||||
result = html_text
|
||||
result = re.compile('(<span class="monospace">)(.*?)(</span>)', re.S).sub(replace, result)
|
||||
result = re.sub("__MDASH__", "—", result);
|
||||
result = re.sub("__NDASH__", "–", result);
|
||||
return result
|
||||
|
||||
def add_tex_caption_numbers(tex_text, K=None):
|
||||
chapter_pat = re.compile(r"\\section\\{")
|
||||
index = {}
|
||||
result = ""
|
||||
chapter_number = 1
|
||||
for line in tex_text.split("\n"):
|
||||
if chapter_pat.search(line):
|
||||
chapter_number += 1
|
||||
reset_indices(index)
|
||||
if kutil.caption_delimiter() in line:
|
||||
before, caption_type, caption, after = line.split(kutil.caption_delimiter())
|
||||
if index.get(caption_type) is None:
|
||||
index[caption_type] = 1
|
||||
n = index[caption_type]
|
||||
number = f"{chapter_number}.{n}" if chapter_number != 0 else n
|
||||
line = f"{before}{caption_type} {number} {caption}{after}"
|
||||
index[caption_type] += 1
|
||||
result += line + "\n"
|
||||
return result
|
||||
|
||||
|
||||
def remove_redundant_vspace(tex_text, K=None):
|
||||
#print("remove_redundant_vspace")
|
||||
rgx = re.compile(r"(\\vspace\*\{-[^}]+\})\s*\\vspace\*\{-[^}]+\}", re.S)
|
||||
return rgx.sub(r"\1", tex_text)
|
||||
|
||||
def restore_backslash(tex_text, K=None):
|
||||
#print("restore backslash")
|
||||
#return re.compile(r"\^/").sub(r"\\", tex_text)
|
||||
return re.sub("\6", "", tex_text)
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
|
||||
def levels_to_section(levels):
|
||||
result = ".".join([str(e) for e in levels])
|
||||
result = re.sub(r"\.0", "", result)
|
||||
return result
|
||||
|
||||
def chapter_title_span():
|
||||
return '<span class="chapter_title">'
|
||||
|
||||
def add_html_section_numbers(html_text, K=None, start=0):
|
||||
depth = 6
|
||||
levels = [start-1] + ([0] * (depth-1))
|
||||
section_pat = re.compile(r"(.*?)<h(\d)(.*?)>(.*?)</h\2>")
|
||||
id_pat = re.compile(r'.*?id="([-\w]+)".*', re.S)
|
||||
result = ""
|
||||
id_number = 1
|
||||
id_map = []
|
||||
for line in html_text.split('\n'):
|
||||
match = section_pat.match(line)
|
||||
if match:
|
||||
pre, level, attr, text = match.groups()
|
||||
id_match = id_pat.fullmatch(attr)
|
||||
if id_match:
|
||||
id = id_match.group(1)
|
||||
else:
|
||||
id = f"id{id_number}"
|
||||
attr = " " + f'id="{id}" {attr}'.strip()
|
||||
id_number += 1
|
||||
level = int(level)
|
||||
levels[level-1] = levels[level-1] + 1
|
||||
for i in range(level, depth):
|
||||
levels[i] = 0
|
||||
section = levels_to_section(levels)
|
||||
line = f'{pre}<h{level}{attr}>{chapter_title_span()}{section}  </span>{text}</h{level}>'
|
||||
id_map.append([level, section, id, text])
|
||||
result += line + "\n"
|
||||
[print(e) for e in id_map]
|
||||
return result, id_map
|
||||
|
||||
def make_html_table_of_contents(titles):
|
||||
result = '<div id="_toc" class="toc-title">Table of contents</div>\n'
|
||||
for level, section, id, text in titles:
|
||||
result += f'<div class="level{level}"><a href="#{id}" class="level">{section} {text}</a></div>\n'
|
||||
return result
|
||||
|
||||
def process_html_sections(html_text, K=None, start=1):
|
||||
html, id_map = add_html_section_numbers(html_text)
|
||||
toc = make_html_table_of_contents(id_map)
|
||||
result = re.sub(r'(<div id="middle">)', rf"\1\n{toc}", html)
|
||||
return result
|
||||
|
||||
|
||||
def reset_indices(indices):
|
||||
for key in indices.keys():
|
||||
indices[key] = 1
|
||||
|
||||
def add_html_caption_numbers(html_text, K=None):
|
||||
chapter_pat = re.compile(rf"{chapter_title_span()}(\d+)</span>")
|
||||
index = {}
|
||||
result = ""
|
||||
chapter_number = ""
|
||||
for line in html_text.split("\n"):
|
||||
if '<span' in line:
|
||||
match = chapter_pat.search(line)
|
||||
print(match.groups())
|
||||
if match and match.group(1) != "0":
|
||||
chapter_number = f"{match.group(1)}."
|
||||
reset_indices(index)
|
||||
if kutil.caption_delimiter() in line:
|
||||
before, caption_type, caption, after = line.split(kutil.caption_delimiter())
|
||||
if index.get(caption_type) is None:
|
||||
index[caption_type] = 1
|
||||
n = index[caption_type]
|
||||
line = f"{before}{caption_type} {chapter_number}{n} {caption}{after}"
|
||||
index[caption_type] += 1
|
||||
result += line + "\n"
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def escape_pre_angle_brackets(code, K=None):
|
||||
def replace(match):
|
||||
code = re.sub(r"<", "<", match.group(1))
|
||||
return f"<pre>{code}</pre>"
|
||||
pat = re.compile("<pre>(.*?)</pre>", re.S)
|
||||
return pat.sub(replace, code)
|
||||
|
||||
def block_elements():
|
||||
return """
|
||||
address article aside blockquote details dialog dd div dl dt fieldset
|
||||
figcaption figure footer form h0 h1 h2 h3 h4 h5 h6 header hgroup hr li
|
||||
main nav ol p section table ul td tr pre
|
||||
""".strip().split()
|
||||
|
||||
|
||||
def not_a_block(par):
|
||||
if par.strip()[0] != "<":
|
||||
return True
|
||||
pat = re.compile(r'^</?([^\s>]+).*?>', re.S|re.M)
|
||||
match = pat.match(par.strip())
|
||||
return match.group(1) not in block_elements()
|
||||
|
||||
def make_paragraphs(html_text, K):
|
||||
def replace(match):
|
||||
before, content, after = match.groups()
|
||||
result = ""
|
||||
for par in [e.strip() for e in
|
||||
re.compile(r"\n *(\n *)+", re.S).split(content)]:
|
||||
if par.strip() and not_a_block(par):
|
||||
#jpar = "\n".join(textwrap.wrap(par, width=80))
|
||||
#result += f"\n<p>\n{jpar}\n</p>\n"
|
||||
jpar = "\n".join(textwrap.wrap(f"<p>{par}</p>", width=80))
|
||||
result += f"\n{jpar}\n"
|
||||
else:
|
||||
result += f"\n{par}\n"
|
||||
result = re.compile(r"\s*</(\w+)>\n<\1>").sub(r"\n</\1>\n\n<\1>", result)
|
||||
return f"{before}{result}{after}"
|
||||
pat = re.compile(r'(.*?<div id="middle">)(.*?)(</div>\s*<div id="bottom">.*)', re.S)
|
||||
match = pat.match(html_text)
|
||||
result = pat.sub(replace, html_text)
|
||||
result = re.compile(r"</td>\s+<td>", re.S).sub("</td>\n<td>", result)
|
||||
result = re.compile(r"\n *(\n *)+", re.S).sub("\n\n", result)
|
||||
return result
|
||||
|
||||
|
||||
def indent_html(filename, K):
|
||||
#print('indent_html')
|
||||
html_util.html_indent(filename)
|
||||
|
||||
def copy_html_resources(filename, K):
|
||||
output_dir = os.path.dirname(K._output_filename) or "."
|
||||
css_dir = f"{output_dir}/{html_util.css_reldir()}"
|
||||
kutil.make_dir_if_necessary(css_dir, delete_contents=True)
|
||||
for basename, source in kutil.sks_files_of_type("css"):
|
||||
command = f"cp {source} {css_dir}/{basename}.css"
|
||||
os.system(command)
|
||||
|
||||
|
||||
# def latex_escapes(latex_text):
|
||||
# return latex_text
|
||||
# result = latex_text
|
||||
# result = re.sub('#', '\\#', result)
|
||||
# result = re.sub('\^', '\\^', result)
|
||||
# result += "LATEX"
|
||||
# return result
|
||||
|
||||
# def restore_backslash(latex_text, K): # ?
|
||||
# return latex_text
|
||||
# print(latex_text)
|
||||
# print("restore_backslash")
|
||||
# result = latex_text
|
||||
# result = re.sub('\b', r'\\b', result)
|
||||
# result = re.sub(r'\\t', r'\\b', result)
|
||||
# return result
|
||||
|
||||
|
||||
def make_pdf_from_tex(filename, K, twice=True):
|
||||
#debug_mode = int(K.K_verbose_level) > 1
|
||||
verbose_level = int(K.K_verbose_level)
|
||||
pathname = os.path.abspath(filename)
|
||||
dirname, filename = os.path.split(pathname)
|
||||
basename, ext = os.path.splitext(filename)
|
||||
command = f"mv {pathname} {dirname}/{basename}.tex"
|
||||
log_file = f"{dirname}/{basename}.log"
|
||||
pdf_file = f"{dirname}/{basename}.pdf"
|
||||
result = os.system(command)
|
||||
#debug_mode = True
|
||||
if verbose_level == 3:
|
||||
remove_log = ''
|
||||
else:
|
||||
dbg_log = "/dev/null"
|
||||
remove_log = ' >{} 2>&1'.format(dbg_log)
|
||||
|
||||
env_var = "KLAMMERTEXT_TEXLIVE_BIN"
|
||||
texbin = os.environ.get(env_var)
|
||||
if texbin is None:
|
||||
msg = f"The directory of TeX Live commands must be defined by ${env_var}"
|
||||
raise Exception(msg)
|
||||
|
||||
latex_command = f"{texbin}/xelatex"
|
||||
#latex_command = f"{texbin}/pdflatex"
|
||||
|
||||
if not os.path.exists(latex_command):
|
||||
raise Exception(f"LaTeX command not found: {latex_command}")
|
||||
|
||||
flags = "--halt-on-error"
|
||||
#flags = "-file-line-error --shell-escape -halt-on-error -interaction nonstopmode -output-directory"
|
||||
|
||||
env = "export max_print_line=1000 ; export TEXINPUTS=${KLAMMERTEXT_HOME}/sks//: ;"
|
||||
command = f"{env} cd {dirname} ; {latex_command} {flags} {basename}.tex {remove_log}"
|
||||
#print(command)
|
||||
result = os.system(command)
|
||||
|
||||
if result != 0:
|
||||
os.system(f"tail -n 20 {log_file}")
|
||||
msg = f"Error in LaTeX processing. See file {os.path.relpath(log_file)}"
|
||||
#print(f"Error in LaTeX processing. See file {os.path.relpath(log_file)}")
|
||||
raise Exception(msg)
|
||||
|
||||
# No error; is repetition necessary for links, etc? Check log for this.
|
||||
# result = os.system(command)
|
||||
# print(f"Wrote {os.path.relpath(pdf_file)}")
|
||||
if twice:
|
||||
result = os.system(command)
|
||||
|
||||
aux_files = 'aux out toc'.split()
|
||||
if verbose_level < 2:
|
||||
aux_files += 'tex log'.split()
|
||||
for unused_ext in aux_files:
|
||||
os.system('rm -rf {}/{}.{}'.format(dirname, basename, unused_ext))
|
||||
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
# Emitted by @vspace.txt (sks/block/block.k), one marker per line of space.
|
||||
# An @eval result consisting only of whitespace is trimmed away by the
|
||||
# Klammermachine, so vertical space must travel as markers and become
|
||||
# newlines here, after blank-line runs have been normalized.
|
||||
vspace_marker = "__VSPACE__"
|
||||
|
||||
def justify_blocks(text, K=None):
|
||||
def txt_nl_marker():
|
||||
return "__KTEXTNEWLINE__"
|
||||
|
||||
def txt_vspace_marker():
|
||||
return "__KTEXTVSPACE__"
|
||||
|
||||
def txt_justify_blocks(text, K):
|
||||
delim = '__DIVIDE__'
|
||||
text = re.sub("\n\n+", delim, text)
|
||||
result = ""
|
||||
for par in text.split(delim):
|
||||
stripped = par.strip()
|
||||
n = stripped.count(vspace_marker)
|
||||
if n and stripped == vspace_marker * n:
|
||||
n = stripped.count(txt_vspace_marker())
|
||||
if n and stripped == txt_vspace_marker() * n:
|
||||
# A paragraph of only @vspace markers: n blank lines in addition
|
||||
# to the normal paragraph separation.
|
||||
result += "\n" * n
|
||||
continue
|
||||
if par and par[0] not in {' ', '['}:
|
||||
par = "\n".join(textwrap.wrap(par, width=80))
|
||||
|
||||
if par and par[0] != ' ':
|
||||
label = re.match(r"~*\[\d+\] ", par)
|
||||
par = "\n".join(textwrap.wrap(
|
||||
par, width=int(K.Target_txt_width), break_long_words=False,
|
||||
subsequent_indent=" " * len(label.group(0)) if label else ""))
|
||||
# if par and par[0] != ' ':
|
||||
# par = "\n".join(textwrap.wrap(
|
||||
# par, width=int(K.Target_txt_width), break_long_words=False))
|
||||
result += par + "\n\n"
|
||||
return result.replace(vspace_marker, "\n")
|
||||
|
||||
result = re.sub(" *" + txt_vspace_marker() + " *", "\n", result)
|
||||
result = re.sub(r"\n? *" + txt_nl_marker() + r" *\n?", "\n", result)
|
||||
result = re.sub("~", " ", result)
|
||||
return result
|
||||
|
||||
def txt_footnote_marker():
|
||||
return "KTEXTFOOTNOTE"
|
||||
|
||||
def txt_format_footnotes(text, K):
|
||||
footnote_number = 0
|
||||
footnotes = []
|
||||
def replace(m):
|
||||
nonlocal footnote_number, footnotes
|
||||
footnote_number += 1
|
||||
footnotes.append(m.group(1))
|
||||
return f"[{footnote_number}]"
|
||||
start_mark = "__" + txt_footnote_marker()
|
||||
end_mark = txt_footnote_marker() + "__"
|
||||
footnote_rgx = re.compile(rf"\s*{start_mark}\s+(.*?)\s+{end_mark}", re.S)
|
||||
result = footnote_rgx.sub(replace, text).rstrip() + "\n\n"
|
||||
if footnotes:
|
||||
rule_count = int(round(float(K.Target_txt_width) * 0.4))
|
||||
result += "-" * rule_count + "\n\n"
|
||||
# Calculate maximum width for right-justified numbers:
|
||||
width = len(f"[{len(footnotes)}]")
|
||||
for n, footnote in enumerate(footnotes, 1):
|
||||
label = f"[{n}]"
|
||||
result += "~" * (width - len(label)) + label + " " + footnote + "\n\n"
|
||||
return result
|
||||
|
||||
def html_footnote_marker():
|
||||
return "kt-footnote"
|
||||
|
||||
def html_format_footnotes(text, K=None):
|
||||
fnum = 0
|
||||
footnotes = []
|
||||
def replace(m):
|
||||
nonlocal fnum, footnotes
|
||||
fnum += 1
|
||||
footnotes.append(m.group(1))
|
||||
href = f'href ="#_footnote_{fnum}"'
|
||||
id = f'id="_footnote_src_{fnum}"'
|
||||
return f'<a {id} {href}><sup class="footnote_in_text">{fnum}</sup></a>'
|
||||
#return f'<a href="#_footnote_{fnum}" id="_footnote_src_{fnum}"><sup>{fnum}</sup></a>'
|
||||
start_mark = f"<{html_footnote_marker()}>"
|
||||
end_mark = f"</{html_footnote_marker()}>"
|
||||
footnote_rgx = re.compile(rf"\s*{start_mark}\s*(.*?)\s*{end_mark}", re.S)
|
||||
result = footnote_rgx.sub(replace, text).rstrip() + "\n\n"
|
||||
if footnotes:
|
||||
result += '<hr class="footnote_rule"/>\n'
|
||||
for n, footnote in enumerate(footnotes, 1):
|
||||
result += f'<a href="#_footnote_src_{n}" id="_footnote_{n}"><sup>{n} </sup></a>{footnote}<br/>\n'
|
||||
# Temporarily add room for footnotes to move to the top of the window for the test:
|
||||
result += '<div style="height: 100lh"></div>\n'
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -15,10 +15,30 @@ The LaTeX transformations supported by the SKS are:
|
||||
'' -> close double quote
|
||||
~ -> non-breaking space
|
||||
|
||||
"^" before any punctuation character quotes it (an engine rule, not an SKS
|
||||
one): the character reaches the output literally, taking no part in the
|
||||
transforms above -- ^-^- writes two plain hyphens (a --flag in prose), ^~ a
|
||||
plain tilde. The one exception is the apostrophe: "^" before it opens a
|
||||
literal span instead (a literal apostrophe is ^0027^), and writing that
|
||||
pair even in THIS comment would open one -- the span sentinels are placed
|
||||
before text removal. A target where the raw character would
|
||||
still be re-interpreted declares its rendering with :resolve -- the tex
|
||||
target maps a quoted hyphen to {-} (a bare -- in .tex re-forms TeX's dash
|
||||
ligature) and a quoted tilde to \textasciitilde{} (a raw ~ is TeX's
|
||||
non-breaking space). Verbatim text (@c, @code, ^'...'^) never needs any
|
||||
of this: neither the transforms nor the quoting reach it.
|
||||
|
||||
]#
|
||||
|
||||
|
||||
@@@target txt | Plain text with formatting | # This txt target is not full implemented in the SKS yet.
|
||||
@@@state Target_txt_width
|
||||
:desc Width of the text in the plain text target (txt)
|
||||
:value 72
|
||||
@@@
|
||||
|
||||
|
||||
# The txt target is not fully implemented in the SKS yet.
|
||||
@@@target txt | Plain text with formatting |
|
||||
-- - |
|
||||
--- -- |
|
||||
`` " |
|
||||
@@ -27,24 +47,35 @@ The LaTeX transformations supported by the SKS are:
|
||||
' ' |
|
||||
~ ^0020^
|
||||
:after_apply
|
||||
phases.justify_blocks
|
||||
phases.txt_format_footnotes ;
|
||||
phases.txt_justify_blocks
|
||||
@@@
|
||||
|
||||
@@@target html | HTML page
|
||||
# :escape < < & & |
|
||||
|
|
||||
--- — |
|
||||
-- – |
|
||||
--- &^#x2014; |
|
||||
-- &^#x2013; |
|
||||
`` “ |
|
||||
'' ” |
|
||||
` ‘ |
|
||||
' ’ |
|
||||
^^~ ˜ |
|
||||
~
|
||||
~ &^#160;
|
||||
:after_apply
|
||||
phases.html_format_footnotes
|
||||
@@@
|
||||
|
||||
# No transforms: LaTeX applies the input conventions itself. The :resolve
|
||||
# entries say how a QUOTED character renders where the raw character would
|
||||
# still be re-interpreted by TeX: ^- resolves to {-}, not a bare -, or the
|
||||
# hyphens re-form a run and the dash ligature turns ^-^- into an en-dash;
|
||||
# ^~ resolves to \textasciitilde{}, since a raw ~ is TeX's non-breaking
|
||||
# space. Other quoted punctuation either has an :escape entry (its marker
|
||||
# takes the escape replacement: ^$ renders as \$) or decodes to the raw
|
||||
# character.
|
||||
@@@target tex | LaTeX
|
||||
:escape \ \textbackslash{} & \& { \{ } \} $ \$ % \% _ \_ ^# \^# ^^ \textasciicircum{}
|
||||
:resolve - {-} ~ \textasciitilde{}
|
||||
@@@
|
||||
|
||||
@@@target pdf | PDF from LaTeX
|
||||
|
||||
@@ -570,8 +570,8 @@ def build_html(md_path, css_paths, fonts_css="", wrap=0, base=None, report=True)
|
||||
css = fonts_css + "\n".join(Path(p).read_text(encoding="utf-8") for p in css_paths)
|
||||
if base is None:
|
||||
base = md_path.resolve().parent.as_uri() + "/"
|
||||
return (f'<!doctype html>\n<html><head><meta charset="utf-8">\n'
|
||||
f'<base href="{base}">\n'
|
||||
return (f'<!doctype html>\n<html><head><meta charset="utf-8"/>\n'
|
||||
f'<base href="{base}"/>\n'
|
||||
f'<title>{md_path.stem}</title>\n'
|
||||
f'<style>\n{css}\n</style>\n</head>\n<body>\n{body}\n</body></html>\n')
|
||||
|
||||
|
||||
@@ -14,8 +14,12 @@ test:
|
||||
./check_test.sh
|
||||
./deftype_test.sh
|
||||
./escape_test.sh
|
||||
./transform_test.sh
|
||||
./character_test.sh
|
||||
./filename_test.sh
|
||||
./alone_test.sh
|
||||
./state_test.sh
|
||||
./target_test.sh
|
||||
./modulepath_test.sh
|
||||
./klammerset_test.sh
|
||||
./option_set_test.sh
|
||||
|
||||
@@ -87,7 +87,7 @@ check_error() {
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if echo "$output" | grep -qF "$pattern"; then
|
||||
if echo "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $test_name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
@@ -233,6 +233,14 @@ check_eq \
|
||||
"[]" \
|
||||
--klammersets none -s '@@s :t.word : [*t*] @@ @s :t @' -d
|
||||
|
||||
echo
|
||||
# --- The pattern itself is validated at definition time (gallery review) ---
|
||||
|
||||
check_error \
|
||||
"22. an invalid :pattern is a definition-time error" \
|
||||
"is not a valid regular expression" \
|
||||
--klammersets none -s '@@@argtype bad | d :pattern ([ @@@' -d
|
||||
|
||||
echo
|
||||
echo "================================"
|
||||
echo "Passed: $PASS Failed: $FAIL"
|
||||
|
||||
81
tst/character_test.sh
Executable file
81
tst/character_test.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# character_test.sh — Regression tests for the ^-character forms the engine
|
||||
# resolves at read time: the ^UUUU^ Unicode code point (4-6 hex digits,
|
||||
# since 2026-08-23 covering the full range to U+10FFFF), and the character
|
||||
# tables (mnemonics, diacritics) it must not disturb.
|
||||
#
|
||||
# SKS-independent: character handling is katomizer/engine machinery
|
||||
# (mac/ktype.h nonascii katom, mac/character.cpp), so everything runs with
|
||||
# --klammersets none under the default (general) target.
|
||||
#
|
||||
# The 2026-08-23 changes these cases pin:
|
||||
# - a 5-digit code point converts WHOLE: ^13000^ (EGYPTIAN HIEROGLYPH
|
||||
# A001) rendered as U+1300 followed by a literal "0" before, because
|
||||
# process_unicode_codepoint() re-scanned its capture at a fixed 4-digit
|
||||
# width;
|
||||
# - 6 digits are accepted (^10FFFD^ is the top of the range; the katom
|
||||
# regex capped at {1,5});
|
||||
# - a value above U+10FFFF renders as the "Invalid Unicode" text, the
|
||||
# same treatment as a surrogate.
|
||||
#
|
||||
# Note: expected strings use $'\xHH' UTF-8 BYTE escapes, not $'\U...' code
|
||||
# points -- macOS ships bash 3.2, which does not expand \U (it failed there
|
||||
# exactly as a naive check would).
|
||||
#
|
||||
# Usage: ./character_test.sh (needs KLAMMERTEXT_HOME set; ktext on PATH)
|
||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
KTEXT=ktext
|
||||
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
||||
ERR=$(mktemp)
|
||||
|
||||
red=$'\033[31m'
|
||||
green=$'\033[32m'
|
||||
bold=$'\033[1m'
|
||||
reset=$'\033[0m'
|
||||
|
||||
trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR && line[f]=="") f++; l=NR; while (l>=1 && line[l]=="") l--; for (i=f;i<=l;i++) print line[i] }'; }
|
||||
|
||||
# check NAME EXPECTED INPUT — render INPUT (no klammer set, default
|
||||
# target); exit 0 and stdout == EXPECTED.
|
||||
check() {
|
||||
local name="$1" expected="$2" input="$3"
|
||||
local out status
|
||||
out=$("$KTEXT" --klammersets none -s "$input" -d 2>"$ERR"); status=$?
|
||||
out=$(printf '%s' "$out" | trim)
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — ktext exited $status"
|
||||
head -3 "$ERR" | sed 's/^/ /'; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if [ "$out" = "$expected" ]; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name"
|
||||
echo " expected: [$expected]"; echo " got: [$out]"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "${bold}Character form tests (engine: ^UUUU^ code points and the tables)${reset}"
|
||||
echo "================================================================="
|
||||
echo
|
||||
|
||||
check " 1. 4-digit BMP code point" $'A ☺ B' 'A ^263A^ B'
|
||||
check " 2. 5-digit code point, whole" $'A \xf0\x93\x80\x80 B' 'A ^13000^ B'
|
||||
check " 3. 5-digit emoji" $'\xf0\x9f\x98\x80' '^1F600^'
|
||||
check " 4. 6-digit top of the range" $'\xf4\x8f\xbf\xbd' '^10FFFD^'
|
||||
check " 5. above U+10FFFF is invalid" 'Invalid Unicode: 16777215' '^FFFFFF^'
|
||||
check " 6. surrogate is invalid" 'Invalid Unicode: 55296' '^D800^'
|
||||
check " 7. mnemonic undisturbed" 'ß' '^s^'
|
||||
# The diacritic tables compose base + COMBINING mark (a U+0308), not the
|
||||
# precomposed letter -- the expected string is built the same way.
|
||||
check " 8. diacritic undisturbed" $'Ma\xcc\x88dchen' 'M^a"dchen'
|
||||
check " 9. code point beside quoted punctuation" $'☺ --' '^263A^ ^-^-'
|
||||
|
||||
echo
|
||||
echo "================================================================="
|
||||
echo "${bold}Results: $PASS passed, $FAIL failed${reset}"
|
||||
rm -f "$ERR"
|
||||
[ $FAIL -eq 0 ] && exit 0 || exit 1
|
||||
@@ -49,7 +49,7 @@ error() {
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — reported an error but exited 0"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if printf '%s' "$out" | grep -qF -- "$pattern"; then
|
||||
if printf '%s' "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF -- "$pattern"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name — expected [$pattern]"
|
||||
@@ -83,6 +83,20 @@ error " 4. ktext -t" "needs a value" ktext -s '@i-x' -t
|
||||
error " 5. the message names the option" "-v <level>" kdesc -v
|
||||
error " 6. ... and how to get the usage" "for the list of arguments" kdesc -v
|
||||
|
||||
echo
|
||||
echo "-- an option value that fails its pattern (gallery review) --"
|
||||
error "21. a non-integer verbosity is an error" "is not a verbosity level" kdesc -v abc
|
||||
# The location must be absent, not a C++ source file: the default Locator
|
||||
# captures the throw site (argv.cpp, line N) unless the throw says
|
||||
# Locator::none() -- the leak class the error gallery found 2026-08-22.
|
||||
out=$(kdesc -v abc 2>&1)
|
||||
if printf '%s' "$out" | grep -q '\.cpp, line'; then
|
||||
echo "${red}FAIL${reset} 22. the error leaks a C++ source location"
|
||||
echo " got: $(printf '%s' "$out" | grep '\.cpp, line' | head -1)"; FAIL=$((FAIL+1))
|
||||
else
|
||||
echo "${green}PASS${reset} 22. no C++ source location in the error"; PASS=$((PASS+1))
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- the same options WITH a value still work --"
|
||||
# NOT "kdesc -v 1": show_usage() treats exactly "<command> -v <n>" as a
|
||||
@@ -103,7 +117,7 @@ echo "-- two required positional arguments --"
|
||||
if [ -x "$TSTDIR/argv_test" ]; then
|
||||
out=$("$TSTDIR/argv_test" first second --bool 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
|
||||
for expect in "<input> : first" "<input2> : second"; do
|
||||
if printf '%s' "$out" | grep -qF "$expect"; then
|
||||
if printf '%s' "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF "$expect"; then
|
||||
echo "${green}PASS${reset} 13/14. positional [$expect]"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 13/14. positional [$expect] not parsed"; FAIL=$((FAIL+1))
|
||||
|
||||
@@ -79,7 +79,7 @@ check_contains() {
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if echo "$output" | grep -qF "$needle"; then
|
||||
if echo "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$needle"; then
|
||||
echo "${green}PASS${reset} $test_name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
@@ -105,7 +105,7 @@ check_error() {
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if echo "$output" | grep -qF "$pattern"; then
|
||||
if echo "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $test_name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
|
||||
@@ -106,7 +106,7 @@ check_error() {
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if echo "$out" | grep -qF "$pattern"; then
|
||||
if echo "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name — expected error to contain [$pattern]"
|
||||
|
||||
11
tst/editor/indent_literal_c.kt
Normal file
11
tst/editor/indent_literal_c.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
@document :text
|
||||
@i before @c @right c@ after @ @x@
|
||||
@note
|
||||
inside the note
|
||||
@
|
||||
@c content with basic@ inside c@ and after
|
||||
@c
|
||||
kept as-is # not a comment
|
||||
c@
|
||||
after everything
|
||||
@
|
||||
11
tst/editor/indent_literal_c_expected.kt
Normal file
11
tst/editor/indent_literal_c_expected.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
@document :text
|
||||
@i before @c @right c@ after @ @x@
|
||||
@note
|
||||
inside the note
|
||||
@
|
||||
@c content with basic@ inside c@ and after
|
||||
@c
|
||||
kept as-is # not a comment
|
||||
c@
|
||||
after everything
|
||||
@
|
||||
@@ -81,6 +81,22 @@ call s:Check('abbreviated @name-', s:Syn(9, 1) ==# 'klammertextAppOpen'
|
||||
\ && s:Syn(9, 6) ==# '' && s:Syn(9, 7) ==# '', s:Syn(9, 6))
|
||||
call s:Check('file marker ##', s:Syn(10, 1) ==# 'klammertextMarker', s:Syn(10, 1))
|
||||
call s:Check('## removes to EOF', s:Syn(11, 3) ==# 'klammertextRemovedFile', s:Syn(11, 3))
|
||||
|
||||
" @c is the single-letter literal klammer (inline @code, 2026-08-16). A
|
||||
" fresh buffer: the ## in the buffer above removes to EOF. The traps a
|
||||
" single-letter name adds: a word ending in c ("basic@") must not close the
|
||||
" region, and "@caption" must not open one.
|
||||
enew!
|
||||
call setline(1, [
|
||||
\ '@c y @ basic@ z c@ tail',
|
||||
\ '@caption arg @'])
|
||||
set filetype=klammertext
|
||||
call s:Check('c verbatim open @c', s:Syn(1, 1) ==# 'klammertextAppOpen', s:Syn(1, 1))
|
||||
call s:Check('c interior @ verbatim', s:Syn(1, 6) ==# 'klammertextVerbatim', s:Syn(1, 6))
|
||||
call s:Check('basic@ does not close @c', s:Syn(1, 13) ==# 'klammertextVerbatim', s:Syn(1, 13))
|
||||
call s:Check('real close c@', s:Syn(1, 17) ==# 'klammertextAppClose', s:Syn(1, 17))
|
||||
call s:Check('text after c@ plain', s:Syn(1, 20) ==# '', s:Syn(1, 20))
|
||||
call s:Check('@caption opens @caption, not @c', s:Syn(2, 1) ==# 'klammertextAppOpen', s:Syn(2, 1))
|
||||
else
|
||||
call add(s:results, 'SKIP syntax checks (this Vim lacks +syntax)')
|
||||
endif
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# tst/ is the SKS-independent tier, and these suites check that the
|
||||
# implementations of Klammertext's structure agree with the canonical
|
||||
# formatting conventions AND with each other. The klammer names that appear
|
||||
# in the fixtures (@ol, @table, @document, @code) are seeded configuration of
|
||||
# in the fixtures (@ol, @table, @document, @code, @c) are seeded configuration of
|
||||
# the editor tools, not SKS dependencies; nothing here runs ktext or loads a
|
||||
# klammer set.
|
||||
#
|
||||
@@ -60,7 +60,7 @@ OUT="$(mktemp -d)"
|
||||
trap 'rm -rf "$OUT"' EXIT
|
||||
|
||||
INDENT_FIXTURES="indent_list indent_document indent_table indent_untouched
|
||||
indent_defs indent_escapes indent_named_close"
|
||||
indent_defs indent_escapes indent_named_close indent_literal_c"
|
||||
ALIGN_FIXTURES="align_mixed align_empty_cells align_boundary align_colspan
|
||||
align_escapes align_too_wide"
|
||||
|
||||
|
||||
@@ -183,6 +183,33 @@ check_eq "25. a real literal klammer still works after a comment naming it" \
|
||||
|
||||
rm -f "$ERR"
|
||||
|
||||
echo
|
||||
echo "-- 26-29: a general body escapes only its OWN literal text, never a --"
|
||||
echo "-- substituted argument value (ordering defect fixed 2026-08-19) --"
|
||||
# An argument value is writer text already escaped at the top level (KTESC
|
||||
# markers, idempotent) PLUS final target markup from klammers the writer
|
||||
# nested in the argument. apply_klammer used to substitute values first and
|
||||
# escape the general body after, so that markup was swept as if it were the
|
||||
# body's literal text: "@sig @b Andy @ @" emitted BSLtextbf{Andy}. The escape
|
||||
# now runs before substitution. BOLD is a target-specific nested klammer
|
||||
# (case 7/9 pattern) whose raw output must survive untouched.
|
||||
BOLD='@@b.k s : d @@ @@b.t :: \textbf{*s*} @@'
|
||||
check_eq "26. substituted klammer output NOT re-escaped" \
|
||||
'Sincerely, \textbf{Andy}' --klammersets none -t t \
|
||||
-s "$T $BOLD @@sig name : Sincerely, *name* @@ @sig @b Andy @ @"
|
||||
check_eq "27. ... while the body's own literal & still IS escaped" \
|
||||
'Fish AMP \textbf{Chips}' --klammersets none -t t \
|
||||
-s "$T $BOLD @@sig2 name : Fish & *name* @@ @sig2 @b Chips @ @"
|
||||
check_eq "28. writer specials in the argument still escaped (markers)" \
|
||||
'Sincerely, a AMP b' --klammersets none -t t \
|
||||
-s "$T @@sig name : Sincerely, *name* @@ @sig a & b @"
|
||||
# The variable shares one text katom with literal specials on both sides:
|
||||
# the katom's literal "_"s must be escaped and the value spliced in raw
|
||||
# afterwards -- the ordering this section exists to pin.
|
||||
check_eq "29. mixed katom: literal specials escaped, spliced value raw" \
|
||||
'AUND\textbf{Q}UNDB' --klammersets none -t t \
|
||||
-s "$T $BOLD @@w s : A_*s*_B @@ @w @b Q @ @"
|
||||
|
||||
echo
|
||||
echo "============================================"
|
||||
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
||||
|
||||
@@ -219,6 +219,28 @@ main = putStrLn "partial" >> exitWith (ExitFailure 3) @'
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- the environment can be missing: the errors name what and how --"
|
||||
# Added 2026-08-22 by the error-gallery coverage review: neither message had
|
||||
# ever been printed by a test.
|
||||
run '@eval :cpp nonexistent_gallery_lib @'
|
||||
if [ $STATUS -ne 0 ] && grep -qF "Cannot open library" "$ERRF"; then
|
||||
pass "19. :cpp names the library it could not open"
|
||||
else
|
||||
fail "19. exit $STATUS" "$(plain < "$ERRF" | head -2)"
|
||||
fi
|
||||
# :haskell without runghc: strip PATH so the case is deterministic whether or
|
||||
# not GHC is installed; ktext itself is invoked by absolute path (which works
|
||||
# since the 2026-08-21 construct_command_pathname fix).
|
||||
env PATH=/nonexistent "$K/bin/ktext" --klammersets none \
|
||||
-s '@eval :haskell main = putStrLn "x" @' -d >"$OUTF" 2>"$ERRF"
|
||||
hstatus=$?
|
||||
if [ $hstatus -ne 0 ] && tr '\n' ' ' < "$ERRF" | grep -qF "requires runghc"; then
|
||||
pass "20. :haskell without runghc says what to install"
|
||||
else
|
||||
fail "20. exit $hstatus" "$(plain < "$ERRF" | head -2)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "==========="
|
||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||
|
||||
@@ -84,7 +84,7 @@ $source" -t fix -d 2>&1)
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if printf '%s' "$output" | tr '\n' ' ' | grep -qF "$needle"; then
|
||||
if printf '%s' "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$needle"; then
|
||||
echo "${green}PASS${reset} $name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
@@ -121,7 +121,7 @@ $source" -t fix -d -v 1 2>&1)
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if printf '%s' "$output" | tr '\n' ' ' | grep -qF "$needle"; then
|
||||
if printf '%s' "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$needle"; then
|
||||
echo "${green}PASS${reset} $name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
@@ -270,7 +270,7 @@ check_fails "22. a set with no parameters is rejected" \
|
||||
'@@bad.o : nothing at all @@'
|
||||
|
||||
check_fails '23. "::" has no meaning for a set' \
|
||||
'An option set IS a declaration' \
|
||||
'An option set is a declaration' \
|
||||
'@@cap.o :: nope @@'
|
||||
|
||||
# --- Collisions -----------------------------------------------------------
|
||||
|
||||
@@ -55,7 +55,7 @@ check_error() {
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if echo "$output" | grep -qF "$pattern"; then
|
||||
if echo "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $test_name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
|
||||
121
tst/state_test.sh
Executable file
121
tst/state_test.sh
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# state_test.sh — The @@@state system command and *name* substitution.
|
||||
#
|
||||
# Added 2026-08-22 by the error-gallery coverage review, which found that no
|
||||
# suite covered @@@state at all: its two error messages had never been
|
||||
# printed by any test. What is pinned here, each by outcome:
|
||||
#
|
||||
# * :value sets a variable and *name* substitutes it in a klammer BODY
|
||||
# (bodies are processed at application time). A top-level *name* of a
|
||||
# document-declared variable does NOT substitute — the same read-time
|
||||
# asymmetry @cond had before 2026-08-15; if that is ever changed, case 2
|
||||
# documents today's behaviour and should change with it.
|
||||
# * :replace replaces an existing value.
|
||||
# * Redefining WITHOUT :replace is an error that names the remedy.
|
||||
# * An undefined variable's error lists the frames it searched.
|
||||
# * A shell environment variable substitutes like any state variable.
|
||||
#
|
||||
# NOT to be confused with "state_test", the C++ diagnostic program built from
|
||||
# state_test.cpp in this directory: that one constructs engine objects and
|
||||
# prints what it gets, for a person to read, and asserts nothing (see the
|
||||
# `smoke` target in tst/Makefile). This is the regression suite. The ".sh"
|
||||
# is the only thing distinguishing them — the same collision as
|
||||
# eval_test/eval_test.sh, documented there first.
|
||||
#
|
||||
# Usage: ./state_test.sh (needs ktext on PATH)
|
||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
KTEXT=ktext
|
||||
export KT_STATE_PROBE="probe-value" # for the environment-variable case
|
||||
|
||||
red=$'\033[31m'
|
||||
green=$'\033[32m'
|
||||
bold=$'\033[1m'
|
||||
reset=$'\033[0m'
|
||||
|
||||
# check_eq NAME EXPECTED KTEXT_ARGS... — trimmed stdout equals EXPECTED.
|
||||
check_eq() {
|
||||
local name="$1" expected="$2"
|
||||
shift 2
|
||||
local out status
|
||||
out=$("$KTEXT" "$@" 2>/dev/null)
|
||||
status=$?
|
||||
out=$(printf '%s' "$out" | sed -e 's/[ \t]*$//' | grep -v '^$')
|
||||
if [ $status -eq 0 ] && [ "$out" = "$expected" ]; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name"
|
||||
echo " expected: [$expected]"; echo " got: [$out] (exit $status)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# check_error NAME PATTERN KTEXT_ARGS... — nonzero exit, PATTERN in the
|
||||
# message. Wrap-insensitive: error prose is justified to 80 columns, so a
|
||||
# phrase may wrap anywhere.
|
||||
check_error() {
|
||||
local name="$1" pattern="$2"
|
||||
shift 2
|
||||
local out status
|
||||
out=$("$KTEXT" "$@" 2>&1)
|
||||
status=$?
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"
|
||||
FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if printf '%s' "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name — expected error to contain [$pattern]"
|
||||
echo " got: $(printf '%s' "$out" | sed 's/\x1b\[[0-9;]*m//g' | tr '\n' ' ' | head -c 200)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "${bold}@@@state tests${reset}"
|
||||
echo "=============="
|
||||
echo
|
||||
|
||||
echo "-- setting and substituting --"
|
||||
check_eq " 1. :value sets; *name* substitutes in a body" \
|
||||
"[hello]" \
|
||||
--klammersets none -s '@@@state V :value hello @@@ @@f : [*V*] @@ @f@' -d
|
||||
|
||||
# Documents TODAY'S behaviour: a top-level *name* of a document-declared
|
||||
# variable is left as written (bodies substitute; the top level does not).
|
||||
check_eq " 2. a top-level *name* does not substitute (current behaviour)" \
|
||||
"[*V*]" \
|
||||
--klammersets none -s '@@@state V :value hello @@@ [*V*]' -d
|
||||
|
||||
check_eq " 3. an environment variable substitutes" \
|
||||
"[probe-value]" \
|
||||
--klammersets none -s '@@f : [*KT_STATE_PROBE*] @@ @f@' -d
|
||||
|
||||
echo
|
||||
echo "-- replacing --"
|
||||
check_eq " 4. :replace replaces an existing value" \
|
||||
"[b]" \
|
||||
--klammersets none \
|
||||
-s '@@@state V :value a @@@ @@@state V :replace b @@@ @@f : [*V*] @@ @f@' -d
|
||||
|
||||
check_error " 5. redefining without :replace is an error naming the remedy" \
|
||||
"Use ':replace <new-value>' to replace the current value" \
|
||||
--klammersets none -s '@@@state V :value a @@@ @@@state V :value b @@@' -d
|
||||
|
||||
echo
|
||||
echo "-- the undefined variable --"
|
||||
check_error " 6. an undefined *name* in a body is a located error" \
|
||||
'Variable "Missing" not defined' \
|
||||
--klammersets none -s '@@f : [*Missing*] @@ @f@' -d
|
||||
|
||||
check_error " 7. ... that lists the frames it searched" \
|
||||
"(searched:" \
|
||||
--klammersets none -s '@@f : [*Missing*] @@ @f@' -d
|
||||
|
||||
echo
|
||||
echo "=============="
|
||||
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
||||
[ $FAIL -eq 0 ]
|
||||
@@ -109,7 +109,7 @@ check_error() {
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if echo "$out" | grep -qF "$pattern"; then
|
||||
if echo "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name — expected error to contain [$pattern]"
|
||||
|
||||
133
tst/target_test.sh
Executable file
133
tst/target_test.sh
Executable file
@@ -0,0 +1,133 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# target_test.sh — The @@@target system command's :after_apply phase list.
|
||||
#
|
||||
# Added 2026-08-22, when the txt target became the first to declare TWO
|
||||
# phases and found the list separator undocumented and unguarded: two bare
|
||||
# specs written without " ; " were glued into one Python call and failed at
|
||||
# render time with a SyntaxError located at "phase, line 1" rather than at
|
||||
# the declaration. What is pinned here:
|
||||
#
|
||||
# * several phases separated by " ; " run, in order, each seeing its
|
||||
# predecessor's result;
|
||||
# * a bare spec containing whitespace is a DEFINITION-TIME error naming
|
||||
# the " ; " convention (the guard in Target::add_after_apply);
|
||||
# * a mode-tagged spec (":cpp <library> <function>") is exempt from the
|
||||
# whitespace guard -- its library is a filename, and filenames may
|
||||
# contain spaces, which is why the separator is ";" at all.
|
||||
#
|
||||
# SKS-independent: --klammersets none, an inline fixture target, and a
|
||||
# fixture Python module placed next to the input file (the module-resolution
|
||||
# rule adds the input file's directory to sys.path).
|
||||
#
|
||||
# NOT to be confused with "target_test", the C++ diagnostic program built
|
||||
# from target_test.cpp in this directory (asserts nothing; see the `smoke`
|
||||
# target in tst/Makefile). The ".sh" is the only distinguisher -- the third
|
||||
# such collision, after eval_test and state_test.
|
||||
#
|
||||
# Usage: ./target_test.sh (needs ktext on PATH)
|
||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
KTEXT=ktext
|
||||
|
||||
red=$'\033[31m'
|
||||
green=$'\033[32m'
|
||||
bold=$'\033[1m'
|
||||
reset=$'\033[0m'
|
||||
|
||||
DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$DIR"' EXIT
|
||||
|
||||
# Two phases that mark their passage; order is observable in the output.
|
||||
cat > "$DIR/phasefix.py" <<'EOF'
|
||||
def one(text, K=None):
|
||||
return text + "+ONE"
|
||||
|
||||
def two(text, K=None):
|
||||
return text + "+TWO"
|
||||
|
||||
def width(text, K=None):
|
||||
# K is the state's "class K"; a fixture state variable proves the phase
|
||||
# receives it (attributes arrive as strings unless the argtype is typed).
|
||||
return text + "|" + str(K.Fixture_width)
|
||||
EOF
|
||||
|
||||
check_eq() { # check_eq NAME EXPECTED INPUT_FILE_TEXT TARGET_DECL
|
||||
local name="$1" expected="$2" body="$3" decl="$4"
|
||||
printf '%s' "$body" > "$DIR/in.kt"
|
||||
local out status
|
||||
out=$("$KTEXT" "$DIR/in.kt" --klammersets none -s "$decl" -t t -d 2>/dev/null)
|
||||
status=$?
|
||||
out=$(printf '%s' "$out" | tr -d '\n')
|
||||
if [ $status -eq 0 ] && [ "$out" = "$expected" ]; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name"
|
||||
echo " expected: [$expected]"; echo " got: [$out] (exit $status)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
check_def_error() { # check_def_error NAME PATTERN TARGET_DECL
|
||||
local name="$1" pattern="$2" decl="$3"
|
||||
local out status
|
||||
out=$("$KTEXT" --klammersets none -s "$decl hello" -t t -d 2>&1)
|
||||
status=$?
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — expected a definition error but ktext succeeded"
|
||||
FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if printf '%s' "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name — expected error to contain [$pattern]"
|
||||
echo " got: $(printf '%s' "$out" | sed 's/\x1b\[[0-9;]*m//g' | tr '\n' ' ' | head -c 200)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "${bold}@@@target :after_apply tests${reset}"
|
||||
echo "============================"
|
||||
echo
|
||||
|
||||
echo "-- several phases: ';'-separated, run in declared order --"
|
||||
check_eq " 1. one phase runs" \
|
||||
"text+ONE" "text" \
|
||||
'@@@target t | test | :after_apply phasefix.one @@@'
|
||||
check_eq " 2. two phases run in order (ONE then TWO)" \
|
||||
"text+ONE+TWO" "text" \
|
||||
'@@@target t | test | :after_apply phasefix.one ; phasefix.two @@@'
|
||||
check_eq " 3. ... and reversing the list reverses the order" \
|
||||
"text+TWO+ONE" "text" \
|
||||
'@@@target t | test | :after_apply phasefix.two ; phasefix.one @@@'
|
||||
|
||||
echo
|
||||
echo "-- a phase receives the state as K --"
|
||||
check_eq " 4. K.<state variable> is readable inside a phase" \
|
||||
"text|42" "text" \
|
||||
'@@@state Fixture_width :value 42 @@@ @@@target t | test | :after_apply phasefix.width @@@'
|
||||
|
||||
echo
|
||||
echo "-- the missing separator is a DEFINITION-time error --"
|
||||
check_def_error " 5. two bare specs without ';' are rejected at the declaration" \
|
||||
"separated by \" ; \"" \
|
||||
'@@@target t | test | :after_apply phasefix.one phasefix.two @@@'
|
||||
check_def_error " 6. ... and the error names the offending spec" \
|
||||
"phasefix.one phasefix.two" \
|
||||
'@@@target t | test | :after_apply phasefix.one phasefix.two @@@'
|
||||
|
||||
echo
|
||||
echo "-- a mode-tagged spec may contain spaces (its library is a filename) --"
|
||||
# Declaration-time only: the :cpp library is never dlopened unless the
|
||||
# target renders, so declaring it under another name proves the exemption
|
||||
# without needing a real library.
|
||||
check_eq " 7. a ':cpp lib func' spec passes the declaration guard" \
|
||||
"text+ONE" "text" \
|
||||
'@@@target u | unused | :after_apply ^:cpp some lib func @@@ @@@target t | test | :after_apply phasefix.one @@@'
|
||||
|
||||
echo
|
||||
echo "============================"
|
||||
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
||||
[ $FAIL -eq 0 ]
|
||||
109
tst/transform_test.sh
Executable file
109
tst/transform_test.sh
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# transform_test.sh — Regression tests for the Klammermachine's typographic
|
||||
# TRANSFORM pass (the third positional of @@@target).
|
||||
#
|
||||
# Kept SKS-INDEPENDENT on purpose: a target is a Machine construct, so the
|
||||
# fixture target is declared inline and no klammer set is loaded. The SKS's
|
||||
# own transform tables (html, txt, tex) are exercised by
|
||||
# sks/tst/typography_test.sh.
|
||||
#
|
||||
# What the pass IS: each pair replaces a source character sequence in the
|
||||
# final output with its target spelling (the LaTeX input conventions carried
|
||||
# to other targets: --- to an em dash, quote pairs, ~ to a non-breaking
|
||||
# space). Since 2026-08-23 it runs PER KATOM at final processing, not over
|
||||
# the joined result string, which is what these cases pin:
|
||||
#
|
||||
# - ordinary writer text -> transformed
|
||||
# - ^'...'^ literal span (katom_t::literal) -> NEVER transformed
|
||||
# - a KTESC marker in the text -> never matched; decodes after
|
||||
# the pass (this is the contract hide_typographic() in the SKS's
|
||||
# klammer_base.py relies on to protect @c/@code verbatim text)
|
||||
# - a plain-text @eval result (a renderer) -> transformed like writer text
|
||||
# - a source split across a katom boundary -> NOT transformed (the writer
|
||||
# separated the characters structurally; they are not a dash)
|
||||
# - :includes -> transforms are inherited,
|
||||
# as escapes are
|
||||
# - the pairs run in declared order
|
||||
#
|
||||
# The transform/escape interplay is also pinned: escapes become markers
|
||||
# before klammer application, transforms run at final processing, markers
|
||||
# resolve last — so the two mechanisms cannot corrupt each other's output.
|
||||
#
|
||||
# Usage: ./transform_test.sh (needs KLAMMERTEXT_HOME set; ktext on PATH)
|
||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
KTEXT=ktext
|
||||
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
||||
ERR=/tmp/transform_test_err.$$
|
||||
|
||||
red=$'\033[31m'
|
||||
green=$'\033[32m'
|
||||
bold=$'\033[1m'
|
||||
reset=$'\033[0m'
|
||||
|
||||
# strip leading/trailing blank lines and trailing whitespace
|
||||
trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR && line[f]=="") f++; l=NR; while (l>=1 && line[l]=="") l--; for (i=f;i<=l;i++) print line[i] }'; }
|
||||
|
||||
# check_eq NAME EXPECTED KTEXT_ARGS... — exit 0 and stdout==EXPECTED.
|
||||
check_eq() {
|
||||
local name="$1" expected="$2"; shift 2
|
||||
local out status err
|
||||
out=$("$KTEXT" "$@" 2>"$ERR"); status=$?
|
||||
err=$(cat "$ERR")
|
||||
out=$(printf '%s' "$out" | trim)
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — ktext exited $status"
|
||||
echo " stderr: $(echo "$err" | head -2)"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if [ "$out" = "$expected" ]; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name"
|
||||
echo " expected: [$expected]"; echo " got: [$out]"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "${bold}Typographic transform tests (Machine mechanism, fixture target 't')${reset}"
|
||||
echo "==================================================================="
|
||||
echo
|
||||
|
||||
# The fixture target: one escape (& -> AMP) and two transforms, whose
|
||||
# replacements are distinct tokens that are easy to assert.
|
||||
T='@@@target t | test target :escape & AMP | --- MDASH | -- NDASH @@@'
|
||||
|
||||
check_eq " 1. writer text: --- transformed" 'a MDASH b' --klammersets none -t t -s "$T a --- b"
|
||||
check_eq " 2. pairs in declared order: -- after ---" 'a NDASH b' --klammersets none -t t -s "$T a -- b"
|
||||
check_eq " 3. literal span: NOT transformed" 'a -- b' --klammersets none -t t -s "$T ^'a -- b'^"
|
||||
check_eq " 4. escape and transform coexist" 'x AMP y NDASH' --klammersets none -t t -s "$T x & y --"
|
||||
# 5-6: the @eval sits in a klammer BODY, applied after the fixture target
|
||||
# is extracted — a top-level @eval runs at read time, before same-input
|
||||
# @@@ extraction (the documented pass-ordering caveat), and would not
|
||||
# find target t.
|
||||
check_eq " 5. renderer @eval result: transformed" 'aNDASHb' --klammersets none -t t -s "$T @@e.t : @eval \"a--b\" @ @@ @e@"
|
||||
check_eq " 6. KTESC marker: skipped, then decoded" '--' --klammersets none -t t -s "$T @@e.t : @eval \"KTESC002dKTESC\" * 2 @ @@ @e@"
|
||||
check_eq " 7. source across a katom boundary: NOT transformed" '- x -' --klammersets none -t t -s "$T @@g : x @@ - @g@ -"
|
||||
|
||||
# 8. :includes — an including target inherits the included target's
|
||||
# transforms (and escapes), so a target built over another renders the
|
||||
# same writer conventions.
|
||||
T2="$T @@@target t2 | over t :includes t @@@"
|
||||
check_eq " 8. :includes inherits transforms" 'a NDASH b AMP c' --klammersets none -t t2 -s "$T2 a -- b & c"
|
||||
|
||||
# 9-12: "^" before any punctuation character quotes it
|
||||
# (hide_quoted_punctuation, 2026-08-23): the pair becomes the KTESC marker
|
||||
# of the character, which the transform pass cannot match. The target
|
||||
# decides the rendering — a :resolve entry, an :escape entry, or (neither)
|
||||
# the raw character. Inside an @eval span the code keeps its carets.
|
||||
check_eq " 9. quoted hyphens: no transform, decode raw" '--' --klammersets none -t t -s "$T ^-^-"
|
||||
check_eq "10. :resolve maps a quoted character" 'LIT-' --klammersets none -t r -s '@@@target r | r :resolve - LIT- @@@ ^-'
|
||||
check_eq "11. quoted char with an :escape entry takes its replacement" 'AMP' --klammersets none -t t -s "$T ^&"
|
||||
check_eq "12. @eval code keeps its carets" '2' --klammersets none -t t -s "$T @@e.t : @eval 3 ^(1) @ @@ @e@"
|
||||
|
||||
echo
|
||||
echo "===================================================================="
|
||||
echo "${bold}Results: $PASS passed, $FAIL failed${reset}"
|
||||
rm -f "$ERR"
|
||||
[ $FAIL -eq 0 ] && exit 0 || exit 1
|
||||
Reference in New Issue
Block a user