katom types from ref/ in groups, @table :leading in lines of text and :rulesep, @code under txt, one table style attribute, literal-klammer options (from dev 2e453744f5e9)

kdesc --katoms is Klammertext rendered from ref/, the types in titled groups,
with --katoms full for the regex column.  @table :leading L is L lines of the
surrounding text in html as in pdf (default 1), and :rulesep spaces the
horizontal rules.  The html table has one style attribute, so :column_width
fractions take effect again.  @code and @source_listing render in plain
text; captions are numbered per kind there, and a table needs no blank line
around it.  @c takes :nd, and an option-shaped word before a bar inside
literal code is code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-09-21 22:37:13 +02:00
1 parent 63956c00e5
commit 217c0f3fc5
23 files changed
+622 -287

No files matched your search

+1 -1
View File
@@ -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 Report problems (or send patches) to the author; accepted changes are
applied to the development tree and appear in a following snapshot. applied to the development tree and appear in a following snapshot.
This snapshot was assembled from development commit `9fa469dd74ed`. This snapshot was assembled from development commit `2e453744f5e9`.
## License ## License
+21 -5
View File
@@ -158,10 +158,17 @@ static std::string text_target()
return color_enabled(std::cout) ? "ansi" : "txt"; return color_enabled(std::cout) ? "ansi" : "txt";
} }
void show_special_characters() // The Machine for a description kdesc renders from the ref/ klammerset.
static Machine ref_machine()
{ {
Machine M; Machine M;
M.read(M.m_state.value("KLAMMERTEXT_HOME") + "/ref/ref.k"); M.read(M.m_state.value("KLAMMERTEXT_HOME") + "/ref/ref.k");
return M;
}
void show_special_characters()
{
Machine M = ref_machine();
M.read_string("@b Writing characters with diacritics and non-English characters @\n\n"); M.read_string("@b Writing characters with diacritics and non-English characters @\n\n");
M.read_string("@diacritics_examples@"); M.read_string("@diacritics_examples@");
M.read_string("@extended_latin_examples@"); M.read_string("@extended_latin_examples@");
@@ -171,20 +178,29 @@ void show_special_characters()
void show_unicode_examples() void show_unicode_examples()
{ {
Machine M; Machine M = ref_machine();
M.read(M.m_state.value("KLAMMERTEXT_HOME") + "/ref/ref.k");
M.read_string("@unicode_examples@"); M.read_string("@unicode_examples@");
std::cout << "\n" << M.apply(text_target()) << "\n\n"; std::cout << "\n" << M.apply(text_target()) << "\n\n";
} }
void show_argtype_definitions() void show_argtype_definitions()
{ {
Machine M; Machine M = ref_machine();
M.read(M.m_state.value("KLAMMERTEXT_HOME") + "/ref/ref.k");
M.read_string("@argtype_definitions@"); M.read_string("@argtype_definitions@");
std::cout << "\n" << M.apply(text_target()) << "\n\n"; std::cout << "\n" << M.apply(text_target()) << "\n\n";
} }
// The katom table is wider than the text: four columns, the last of them a
// sentence. The width is the klammer's argument rather than a state
// variable set here, so the paragraphs above the table keep the text width.
void describe_katoms(bool show_regex)
{
Machine M = ref_machine();
M.read_string(std::string("@katom_descriptions :txt_width ") +
(show_regex ? "160 :regex" : "120") + " @");
std::cout << "\n" << M.apply(text_target()) << "\n\n";
}
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
try { try {
+27 -13
View File
@@ -84,7 +84,8 @@ klammerset; give it `@read sks/sks.k @` in the input if it needs one.
| The document body is the first argument | For `@document` the body goes in the **`:text` option** (or `:files`). A positional body is an error. | | The document body is the first argument | For `@document` the body goes in the **`:text` option** (or `:files`). A positional body is an error. |
| `x@i` is "x, then italic" | A letter may not touch an opening `@`: `x@i` could be the close of a klammer named `x`. Write `x @i`. Brackets and sentence punctuation may abut: `(@i-x)`, `@i x @.` | | `x@i` is "x, then italic" | A letter may not touch an opening `@`: `x@i` could be the close of a klammer named `x`. Write `x @i`. Brackets and sentence punctuation may abut: `(@i-x)`, `@i x @.` |
| `:files ch1` names a file `ch1` | A bare word means **`kt/ch1.kt`** beside the root file. A name with a directory or extension is a pathname. | | `:files ch1` names a file `ch1` | A bare word means **`kt/ch1.kt`** beside the root file. A name with a directory or extension is a pathname. |
| `@font` sets the font | Retired. `@typeface name \| text @` is the temporary change; `@document :serif_typeface` etc. set the roles. | | `@font` sets the font | Retired. `@typeface name \| text @` is the temporary change; `@document :serif_typeface` etc. set the roles. `:scale N` means N times the text as it appears, the same in html and pdf; `:middle` centres a script that does not sit on the baseline (hieroglyphs); `:lift 0.15em` shifts one that draws its glyphs off it. Put those in the klammer that names the font (`@egyptian`), never on a Latin typeface. |
| `:raise`, `:class`, `:import` as option names | An option name becomes a Python attribute, so a Python keyword is a SyntaxError at the first application, reported against the `.k`. Choose another word (`:lift`). |
--- ---
@@ -139,6 +140,8 @@ while every argument is letters and digits: `@i-italic`, `@sup-2-3`, `@date@`.
code@ code@
@c a | b c@ ← inline: same rule, and the close must be "c@" @c a | b c@ ← inline: same rule, and the close must be "c@"
@c ls :w | wc c@ ← an option-shaped word and a bar are code too; only an
option the klammer HAS (@c :nd | text c@) is read as one
``` ```
`@c` and `@code` are the in-line and block forms of the same thing. Code that `@c` and `@code` are the in-line and block forms of the same thing. Code that
@@ -262,8 +265,8 @@ are the limited ones.
| | | | | |
|---|---| |---|---|
| inline | `@i` italic, `@b` bold, `@t` typewriter, `@u` underline, `@s` sans, `@r` roman, `@ri` roman italic, `@c` inline code (literal; close `c@`), `@sup base \| script @`, `@sub`, `@ts scale \| text @` resize, `@color rgb :text`, `@typeface name \| text :scale @`, `@cjk` (needs an installed CJK typeface) | | inline | `@i` italic, `@b` bold, `@t` typewriter, `@u` underline, `@s` sans, `@r` roman, `@ri` roman italic, `@c` inline code (literal; close `c@`), `@sup base \| script @`, `@sub`, `@ts scale \| text @` resize, `@color rgb :text`, `@typeface name \| text :scale :middle :lift @`, `@egyptian text @` (hieroglyphs, NewGardinerOmni, shipped), `@chinese text @` (needs Noto Serif SC installed: `kdesc --typeface install <dir>`) |
| structure | `@s1`…`@s7` numbered headings, `@h1`…`@h7` unnumbered, `@part`, `@preface`, `@par`, `@newpage`, `@vspace lines`, `@vfill`, `@nl` line break | | structure | `@s1`…`@s7` numbered headings (`:n 5` forces the number), `@h1`…`@h7` unnumbered, `@part`, `@preface`, `@par`, `@newpage`, `@vspace lines`, `@vfill`, `@nl` line break. Headings are numbered **only inside a `@document`**, in every target; a fragment's heading is its bare title |
| blocks | `@code` (inline listing), `@source_listing file :marker P`, `@quote text \| attribution @`, `@note`, `@indent :left :right`, `@center`, `@right`, `@block to \| content @` (absolute placement), `@columns items :n @`, `@multicolumn text :n :gap @`, `@lines` | | blocks | `@code` (inline listing), `@source_listing file :marker P`, `@quote text \| attribution @`, `@note`, `@indent :left :right`, `@center`, `@right`, `@block to \| content @` (absolute placement), `@columns items :n @`, `@multicolumn text :n :gap @`, `@lines` |
| lists | `@ul :bullet`, `@ol :initial`, `@define :style` term/definition, `@qa question \| answer @` — items separated by `\|` | | lists | `@ul :bullet`, `@ol :initial`, `@define :style` term/definition, `@qa question \| answer @` — items separated by `\|` |
| tables | `@table` — cells by `\|`, rows by `\|\|` (§6); `@rowcolor` | | tables | `@table` — cells by `\|`, rows by `\|\|` (§6); `@rowcolor` |
@@ -275,13 +278,13 @@ are the limited ones.
Bar depth is the dimension: one `|` separates items, two `||` separates rows. Bar depth is the dimension: one `|` separates items, two `||` separates rows.
Argument order is free for options, fixed for positionals. Argument order is free for options, fixed for positionals.
**Coverage today** (`kdesc --coverage`; 89 klammers): html, tex and pdf cover **Coverage today** (`kdesc --coverage`; 83 klammers): html, tex and pdf cover
nearly all. **txt and ansi cover 40**, among them `@b @i @t @u @s @r @ri @ts nearly all. **txt and ansi cover 60**, among them `@b @i @t @u @s @r @ri @ts
@typeface`, `@s1`–`@s7`, `@par @newpage @vspace @vfill @nl`, `@indent @right @typeface`, `@s1`–`@s7`, `@par @newpage @vspace @vfill @nl`, `@indent @right
@lines @qa`, `@ul @ol`, `@table @tbl @reference @define @left_right`, @lines @qa`, `@ul @ol`, `@table @tbl @reference @define @left_right`,
`@footnote @link`, `@date @datetime`, `@c`. Not for txt: `@code `@footnote @link`, `@date @datetime`, `@c @code @source_listing @columns`,
@source_listing @image @image_grid @quote @note @center @block @columns `@h1`–`@h7`. Not for txt: `@image @image_grid @quote @note @center @block
@multicolumn @sup @sub @color @h1`–`@h7`. @multicolumn @sup @sub @color`.
`@u` has no html definition. A klammer applied under a target that lacks it is `@u` has no html definition. A klammer applied under a target that lacks it is
an **error**, never silently dropped. an **error**, never silently dropped.
@@ -311,6 +314,11 @@ the range (here: one sum per column).
- `:cell_hpos l c r` per column; `:justify <cells> <pos>; …` per cell. - `:cell_hpos l c r` per column; `:justify <cells> <pos>; …` per cell.
- Lines: default under the header and at the bottom; `:hline`/`:vline` - Lines: default under the header and at the bottom; `:hline`/`:vline`
replace them (`all`, `none`, or indexed ranges); `:grid` for all lines. replace them (`all`, `none`, or indexed ranges); `:grid` for all lines.
- Vertical rhythm, the same in html and pdf: **`:leading L`** is the row
height in lines of the surrounding text (default `1`: as dense as a
paragraph); **`:rulesep R`** is the space between a horizontal line and
the text either side, as a multiple of the standard amount (`0` = none;
a pdf table with vertical lines gets none).
- `:colspan`/`:rowspan` with the indexed-range syntax; `:allow_break true` - `:colspan`/`:rowspan` with the indexed-range syntax; `:allow_break true`
makes a page-breaking table (pdf). makes a page-breaking table (pdf).
- `:calc` — `;`-separated `<row>(<col>) = <op> <operands>`, prefix `+ - * /` - `:calc` — `;`-separated `<row>(<col>) = <op> <operands>`, prefix `+ - * /`
@@ -331,7 +339,7 @@ the range (here: one sum per column).
| `html` | a directory `<name>/` with `index.html`, `css/`, `fonts/`, `js/` | self-contained, fonts localized; `book` structure adds navigation, search | | `html` | a directory `<name>/` with `index.html`, `css/`, `fonts/`, `js/` | self-contained, fonts localized; `book` structure adds navigation, search |
| `tex` | `<name>.tex` | XeLaTeX source; fonts via fontspec from the font store | | `tex` | `<name>.tex` | XeLaTeX source; fonts via fontspec from the font store |
| `pdf` | `<name>.pdf` | `tex` plus a xelatex run (TeX Live required) | | `pdf` | `<name>.pdf` | `tex` plus a xelatex run (TeX Live required) |
| `txt` | `<name>.txt` | plain text filled to `Target_txt_width` (72), blocks indented by `Target_txt_indent` (3); tables ruled and padded; captions numbered; **40 of 89 klammers** (§5) | | `txt` | `<name>.txt` | plain text filled to `Target_txt_width` (72), blocks indented by `Target_txt_indent` (2); tables ruled and padded (`:leading`, `:rulesep` and `:colsep` have no meaning here: a row is a line of characters); `@c` set between `‹ ›` (`@c :nd \| text c@` omits them); listings flush left under their caption; headings numbered inside a `@document`; captions numbered per kind; **60 of 83 klammers** (§5) |
| `ansi` | txt + SGR escape sequences | `@b`/`@i`/`@u` and table header styles shown by a terminal; **identical to txt once the escape sequences are stripped**; pick it with `-t ansi -d`; a terminal needs a font with an italic face (macOS Terminal: Menlo, not Monaco) | | `ansi` | txt + SGR escape sequences | `@b`/`@i`/`@u` and table header styles shown by a terminal; **identical to txt once the escape sequences are stripped**; pick it with `-t ansi -d`; a terminal needs a font with an italic face (macOS Terminal: Menlo, not Monaco) |
`k`, `o`, `*` also appear in `kdesc -t`: they are declaration pseudo-targets `k`, `o`, `*` also appear in `kdesc -t`: they are declaration pseudo-targets
@@ -344,10 +352,16 @@ quality of cached photographs, 85), `Target_txt_width`, `Target_txt_indent`,
Typefaces: the **font store** is closed-world (no network). The distribution Typefaces: the **font store** is closed-world (no network). The distribution
ships eb-garamond, crimson-pro, open-sans, source-sans-3, inconsolata, ships eb-garamond, crimson-pro, open-sans, source-sans-3, inconsolata,
source-code-pro, noto-sans-egyptian-hieroglyphs, noto-sans-cuneiform (see source-code-pro, noto-sans-egyptian-hieroglyphs, noto-sans-cuneiform and
`kdesc --typeface`); more are installed with `kdesc --typeface install <dir>` newgardineromni (see `kdesc --typeface`); more are installed with
into `KLAMMERTEXT_FONTS`. An unknown typeface is an error listing the available `kdesc --typeface install <dir>` into `KLAMMERTEXT_FONTS`. An unknown typeface
ones. CJK is deliberately not shipped. is an error listing the available ones. CJK is deliberately not shipped:
`@chinese` errors until Noto Serif SC is installed, and `kdesc -u` says so.
Two traps: a text font may carry OpenType features that rewrite plain text
(EB Garamond sets `^9053^` as a raised 9053, since a caret before figures
means superscript to it — show Klammertext codes in `@t` or `@c`, which are
monospace); and the txt/ansi targets pass a `@typeface` through unchanged,
so the terminal's own font decides what a script looks like there.
--- ---
+11 -116
View File
@@ -2,6 +2,7 @@
#include <iomanip> #include <iomanip>
#include <algorithm> #include <algorithm>
#include <utility> #include <utility>
#include <stdexcept>
#include "locator.h" #include "locator.h"
#include "ktype.h" #include "ktype.h"
@@ -43,127 +44,21 @@ bool is_deftype(katom_t type)
type == katom_t::klammer_default; type == katom_t::klammer_default;
} }
std::string pattern_display(Ktype t) const Ktype& type_to_katom_instance(katom_t type)
{ {
std::string pat{}; auto var = std::find_if(katom_types.begin(), katom_types.end(),
if (t.m_type == katom_t::space) { [&] (const Ktype& t) { return t.m_type == type; });
pat = "<space>"; if (var == katom_types.end()) {
} else if (t.m_type == katom_t::newline) { throw std::logic_error(
pat = "\\n"; "type_to_katom_instance: katom type " +
} else if (t.m_type == katom_t::ws_space) { std::to_string(static_cast<int>(type)) +
pat = "#+ or #+<number>"; " has no entry in katom_types. See file mac/ktype.h.");
} else if (t.m_type == katom_t::ws_newline) {
pat = "#/ or #/<number>";
} else if (t.m_type == katom_t::special) {
pat = "^@, ^|, ^#, ^:, or ^^";
} else if (t.m_type == katom_t::apply_end) {
pat = "@ or <name>@";
} else if (t.m_type == katom_t::define_end) {
pat = "@@ or <name>@@";
} else if (t.m_type == katom_t::machine_end) {
pat = "@@@ or <name>@@@";
} else if (t.m_type == katom_t::ws_added) {
pat = "<space> or \\n";
} else if (t.m_type == katom_t::word) {
pat = "<only-letters>";
} else if (t.m_type == katom_t::text) {
pat = "<no-special-chars>";
} else if (t.m_type == katom_t::nonascii) {
pat = "^<code> or ^<code>^";
} else {
pat = t.m_pattern;
pat = string_replace(pat, definition_name, "<name>");
pat = string_replace(pat, "\\", "");
} }
return pat; return *var;
}
std::string regex_display(Ktype t)
{
std::string rgx{};
if (t.m_type == katom_t::space) {
rgx = "<space>";
} else if (t.m_type == katom_t::newline) {
rgx = "\\n";
} else {
rgx = t.m_pattern;
}
return rgx;
}
Ktype find_katom_type(katom_t type)
{
for (auto t : katom_types) {
if (t.m_type == type) {
return t;
}
}
throw Internal_error("Unknown katom_t: " + std::to_string((int)type));
}
void describe_katoms(bool show_regex)
{
size_t name_w = 0;
size_t pattern_w = 0;
size_t regex_w = 0;
size_t description_w = 0;
std::stringstream ss {};
ss << R"(
A "katom" is an individual element in the Klammertext input text.
Each katom has a type, listed below. You can see how Klammertext
divides up text into katoms with the "kdiag" command.)" << "\n\n";
if (show_regex) {
ss << R"(The "Regex" column is the argument to the std::regex C++ function.)";
} else {
ss << R"(In the katom patterns, "<name>" is a word that begins with a letter
and only contains letters, numbers, or the underscore (_) or period (.) characters. )";
}
ss << "A <number> is an integer greater than or equal to 1. "
"A <code> is one of the non-ASCII codes that are displayed by the command "
"\"kdesc -c\".";
for (auto t : katom_types) {
name_w = std::max(name_w, t.m_name.size());
pattern_w = std::max(pattern_w, pattern_display(t).size());
regex_w = std::max(regex_w, regex_display(t).size());
description_w = std::max(description_w, t.m_description.size());
}
pattern_w++;
regex_w++;
std::cout << justify(ss.str()) << "\n\n"
<< std::setfill(' ') << boldblack << std::right << std::setw(6) << "Index"
<< std::left
<< std::setw(name_w) << " Name" << " ";
if (show_regex) {
std::cout << std::setw(regex_w) << " Regex" << " ";
} else {
std::cout << std::setw(pattern_w) << " Pattern" << " ";
}
std::cout << std::setw(description_w) << "Description"
<< "\n" << black;
for (auto typ : katom_type_display_order) {
auto t = find_katom_type(typ);
std::cout << " " << std::right << std::setw(3)
<< static_cast<int>(t.m_type) << " "
<< std::left
<< std::setw(name_w) << t.m_name << " ";
if (show_regex) {
std::cout << std::setw(regex_w) << regex_display(t) << " ";
} else {
std::cout << std::setw(pattern_w) << pattern_display(t) << " ";
}
std::cout << std::setw(description_w) << t.m_description << "\n";
}
std::cout << "\n";
} }
std::string type_to_name(katom_t type) std::string type_to_name(katom_t type)
{ {
auto var = std::find_if(katom_types.begin(), katom_types.end(), return type_to_katom_instance(type).m_name;
[&] (Ktype t) { return t.m_type == type; });
return var->m_name;
} }
+112 -86
View File
@@ -69,10 +69,10 @@ enum class katom_t {
karg, karg,
machine_begin, machine_begin,
machine_end, machine_end,
ignore_line,
ignore_begin, ignore_begin,
ignore_end, ignore_end,
ignore_rest, ignore_rest,
ignore_line,
ws_remove, ws_remove,
ws_space, ws_space,
ws_newline, ws_newline,
@@ -88,109 +88,67 @@ enum class katom_t {
}; };
inline const
std::vector<katom_t> katom_type_display_order {
katom_t::space,
katom_t::word,
katom_t::newline,
katom_t::text,
katom_t::apply_begin,
katom_t::apply_end,
katom_t::bar,
katom_t::double_bar,
katom_t::option_name,
katom_t::read_begin,
katom_t::eval_begin,
katom_t::cond_begin,
katom_t::define_begin,
katom_t::define_end,
katom_t::klammer_definition,
katom_t::klammer_instance,
katom_t::klammer_override,
katom_t::klammer_default,
katom_t::karg,
katom_t::machine_begin,
katom_t::machine_end,
katom_t::ignore_begin,
katom_t::ignore_end,
katom_t::ignore_rest,
katom_t::ignore_line,
katom_t::ws_remove,
katom_t::ws_space,
katom_t::ws_newline,
katom_t::special,
katom_t::nonascii,
katom_t::literal_begin,
katom_t::literal_end,
katom_t::ws_added,
katom_t::literal,
katom_t::eval_result,
katom_t::replaced,
katom_t::ignored
};
class Ktype; class Ktype;
inline std::map<katom_t, std::string> katom_type_names {}; inline std::map<katom_t, std::string> katom_type_names {};
inline std::map<katom_t, std::string> katom_type_descs {};
inline std::vector<katom_t> katom_type_list {}; inline std::vector<katom_t> katom_type_list {};
class Ktype { class Ktype {
public: public:
// Ktype() = default; // Ktype() = default;
Ktype(katom_t type, std::string name, std::string pattern, std::string description, bool use_equal = false) Ktype(katom_t type, std::string name, std::string pattern, bool use_equal = false)
: m_type(type), : m_type(type),
m_name(name), m_name(name),
m_pattern(pattern), m_pattern(pattern),
m_description(description),
m_use_equal(use_equal), m_use_equal(use_equal),
m_rgx(std::regex(pattern)) { m_rgx(std::regex(pattern)) {
katom_type_list.push_back(type); katom_type_list.push_back(type);
katom_type_names.insert({m_type, m_name}); katom_type_names.insert({m_type, m_name});
katom_type_descs.insert({m_type, m_description});
} }
// A description used to be the fourth argument. A string literal there
// would now convert to bool and silently set use_equal, so it is refused.
Ktype(katom_t, std::string, std::string, const char*) = delete;
bool match(const std::string& s) const { bool match(const std::string& s) const {
return m_use_equal ? s == m_pattern : std::regex_match(s, m_rgx); return m_use_equal ? s == m_pattern : std::regex_match(s, m_rgx);
} }
katom_t m_type; katom_t m_type;
std::string m_name; std::string m_name;
std::string m_pattern; std::string m_pattern;
std::string m_description;
bool m_use_equal; bool m_use_equal;
std::regex m_rgx; std::regex m_rgx;
}; };
inline const inline const
std::vector<Ktype> katom_types { std::vector<Ktype> katom_types {
Ktype(katom_t::space, "space", " ", "One space character", false), Ktype(katom_t::space, "space", " ", false),
Ktype(katom_t::word, "word", "[a-zA-Z]+", "Text only containing letters", false), Ktype(katom_t::word, "word", "[a-zA-Z]+", false),
Ktype(katom_t::newline, "newline", "\n", "One newline character", false), Ktype(katom_t::newline, "newline", "\n", false),
Ktype(katom_t::bar, "bar", "\\|", "Bar character used as positional argument separator", false), Ktype(katom_t::bar, "bar", "\\|", false),
Ktype(katom_t::double_bar, "double-bar", "\\|\\|", "Separator for compound positional arguments", false), Ktype(katom_t::double_bar, "double-bar", "\\|\\|", false),
Ktype(katom_t::read_begin, "read-begin", at_s+read_s, "Beginning of the file input klammer", false), Ktype(katom_t::read_begin, "read-begin", at_s+read_s, false),
Ktype(katom_t::eval_begin, "eval-begin", at_s+eval_s, "Beginning of the evaluation klammer", false), Ktype(katom_t::eval_begin, "eval-begin", at_s+eval_s, false),
Ktype(katom_t::cond_begin, "cond-begin", at_s+cond_s, "Beginning of the conditional (if/then/else) klammer", false), Ktype(katom_t::cond_begin, "cond-begin", at_s+cond_s, false),
Ktype(katom_t::apply_begin, "apply-begin", at_s + definition_name, "Beginning of a klammer call"), Ktype(katom_t::apply_begin, "apply-begin", at_s + definition_name),
Ktype(katom_t::apply_end, "apply-end", "(" + definition_name + ")?" + at_s, "End of a klammer call"), Ktype(katom_t::apply_end, "apply-end", "(" + definition_name + ")?" + at_s),
Ktype(katom_t::option_name, "option-name", ":" + definition_name, "Optional argument name"), Ktype(katom_t::option_name, "option-name", ":" + definition_name),
Ktype(katom_t::define_begin, "define-begin", at2_s + definition_begin_name, "Beginning of a klammer definition"), Ktype(katom_t::define_begin, "define-begin", at2_s + definition_begin_name),
Ktype(katom_t::define_end, "define-end", "(" + definition_name + ")?" + at2_s, "End of a klammer definition"), Ktype(katom_t::define_end, "define-end", "(" + definition_name + ")?" + at2_s),
Ktype(katom_t::klammer_default, "klammer-default", "::::", "Define klammer default value for possible override"), Ktype(katom_t::klammer_default, "klammer-default", "::::"),
Ktype(katom_t::klammer_override, "klammer-override", ":::", "Override existing klammer definition"), Ktype(katom_t::klammer_override, "klammer-override", ":::"),
Ktype(katom_t::klammer_definition, "klammer-definition", ":", "Klammer definition, including parameters"), Ktype(katom_t::klammer_definition, "klammer-definition", ":"),
Ktype(katom_t::klammer_instance, "klammer-instance", "::", "Klammer definition using previously defined parameters"), Ktype(katom_t::klammer_instance, "klammer-instance", "::"),
Ktype(katom_t::karg, "klammer-arg", "\\*" + definition_name +"\\*", "Klammer argument in klammer body definition"), Ktype(katom_t::karg, "klammer-arg", "\\*" + definition_name +"\\*"),
Ktype(katom_t::machine_begin, "machine-begin", at3_s + definition_name, "Beginning of a processor modification definition"), Ktype(katom_t::machine_begin, "machine-begin", at3_s + definition_name),
Ktype(katom_t::machine_end, "machine-end", "(" + definition_name + ")?" + at3_s, "End of a processor modification definition"), Ktype(katom_t::machine_end, "machine-end", "(" + definition_name + ")?" + at3_s),
Ktype(katom_t::ignore_begin, "ignore-begin", ignore_begin_s, "Beginning of text to remove"), Ktype(katom_t::ignore_begin, "ignore-begin", ignore_begin_s),
Ktype(katom_t::ignore_end, "ignore-end", ignore_end_s, "End of text to remove"), Ktype(katom_t::ignore_end, "ignore-end", ignore_end_s),
Ktype(katom_t::ignore_rest, "ignore-rest", ignore_rest_s, "Remove all text to the end of file or string"), Ktype(katom_t::ignore_rest, "ignore-rest", ignore_rest_s),
Ktype(katom_t::ignore_line, "ignore-line", ignore_line_s, "Remove all text to the first newline, inclusive"), Ktype(katom_t::ignore_line, "ignore-line", ignore_line_s),
Ktype(katom_t::ws_remove, "ws-remove", ws_remove_s, "Remove all whitespace at this point"), Ktype(katom_t::ws_remove, "ws-remove", ws_remove_s),
Ktype(katom_t::ws_space, "ws-space", ws_space_s, "Remove all whitespace, leaving <number> spaces (default: 1)"), Ktype(katom_t::ws_space, "ws-space", ws_space_s),
Ktype(katom_t::ws_newline, "ws-newline", ws_newline_s, "Remove all whitespace, leaving <number> newlines (default: 1)"), Ktype(katom_t::ws_newline, "ws-newline", ws_newline_s),
Ktype(katom_t::special, "special-char", R"(\^[@|#^:*])", "Klammertext special character treated as regular text"), Ktype(katom_t::special, "special-char", R"(\^[@|#^:*])"),
//Ktype(katom_t::nonascii, "non-ascii-char", R"(\^\w(?:.|\^))", "Non-ASCII character"), //Ktype(katom_t::nonascii, "non-ascii-char", R"(\^\w(?:.|\^))", "Non-ASCII character"),
// The second alternative is the "^...^" delimited form: a code point // The second alternative is the "^...^" delimited form: a code point
// (^263A^, 4-6 hex digits) OR a mnemonic (^s^, ^oe^). Its character // (^263A^, 4-6 hex digits) OR a mnemonic (^s^, ^oe^). Its character
@@ -207,26 +165,95 @@ std::vector<Ktype> katom_types {
// decided there, by the tables, not here -- katomization's job is only to // decided there, by the tables, not here -- katomization's job is only to
// recognize the shape. A shape that resolves to no character at all is // recognize the shape. A shape that resolves to no character at all is
// a located error, raised at the end of encode(). // a located error, raised at the end of encode().
Ktype(katom_t::nonascii, "non-ascii-char", R"((\^(\w(?:.|\^)))|(\^[A-Za-z0-9]{1,6}\^))", "Non-ASCII character"), Ktype(katom_t::nonascii, "non-ascii-char", R"((\^(\w(?:.|\^)))|(\^[A-Za-z0-9]{1,6}\^))"),
// Ktype(katom_t::word, "word", R"([a-z][a-z0-9_]*)", "Lower-case letters, numbers, or underscore"), // Ktype(katom_t::word, "word", R"([a-z][a-z0-9_]*)", "Lower-case letters, numbers, or underscore"),
// Ktype(katom_t::text, "text", // Ktype(katom_t::text, "text",
Ktype(katom_t::literal_begin, "literal-begin", R"(\^')", "Begin unprocessed text"), Ktype(katom_t::literal_begin, "literal-begin", R"(\^')"),
Ktype(katom_t::literal_end, "literal-end", R"('\^)", "End unprocessed text"), Ktype(katom_t::literal_end, "literal-end", R"('\^)"),
Ktype(katom_t::text, "text", R"([^^@#|]+)", "No special characters, whitespace, or \":\" at the beginning"), // spaces, @, #, |, or ^, Ktype(katom_t::text, "text", R"([^^@#|]+)"), // spaces, @, #, |, or ^,
Ktype(katom_t::ws_added, "ws-added", "<space> or \\n", "Added whitespace characters from #+ and #/"), Ktype(katom_t::ws_added, "ws-added", "<space> or \\n"),
Ktype(katom_t::literal, "literal", "", "Literal katom (changed from its original type by ^'...'^)"), Ktype(katom_t::literal, "literal", ""),
Ktype(katom_t::eval_result, "eval-result", "", "Katom produced by @eval klammer"), Ktype(katom_t::eval_result, "eval-result", ""),
Ktype(katom_t::replaced, "replaced", "", "A katom replaced by definitions, applications, or file input"), Ktype(katom_t::replaced, "replaced", ""),
Ktype(katom_t::ignored, "ignored", "", "A katom ignored by the action of a \"#\" katom"), Ktype(katom_t::ignored, "ignored", ""),
// Ktype(katom_t::undefined, "undefined", "", "Undefined pattern") // Ktype(katom_t::undefined, "undefined", "", "Undefined pattern")
}; };
// The human-facing description of the katom types, and the ONLY one: class
// Ktype above holds what the program needs (type, name, regex), this holds
// what a reader is told, and katom_t joins them. The pattern and the
// description are Klammertext, rendered by katom_descriptions() in
// ref/reflect.cpp (kdesc --katoms; a Handbook chapter). A title is
// Klammertext too, without styling -- the generator styles every title alike.
//
// The ORDER is the display order, from what every author meets to what only
// a programmer does, and it is also the order of the katom_t enumerators:
// the table's Index column is the enum value, which is the subscript kdiag
// prints. tst/ktype_test.cpp asserts both halves of the tie -- every type
// in katom_types described exactly once, and the indices increasing.
struct Katom_type_description {
katom_t m_type;
std::string m_pattern;
std::string m_description;
};
struct Katom_type_group {
std::string m_title;
std::vector<Katom_type_description> m_types;
};
inline const
std::vector<Katom_type_group> katom_type_groups {
{"Plain text", {
{katom_t::space, "@symb-space", "One space character"},
{katom_t::word, "@symb only-letters @", "Text only containing letters"},
{katom_t::newline, "\\n", "One newline character"},
{katom_t::text, "@symb no-special-chars @", "No special characters, whitespace, or an initial colon"}}},
{"Klammer application", {
{katom_t::apply_begin, "^@ #- @symb-name", "Beginning of a klammer call"},
{katom_t::apply_end, "^@ @ri-or @symb-name #- ^@", "End of a klammer call"},
{katom_t::bar, "^|", "Bar character used as positional argument separator"},
{katom_t::double_bar, "^|^|", "Separator for compound positional arguments"},
{katom_t::option_name, ": #- @symb-name", "Name of an optional argument"},
{katom_t::read_begin, "@t ^@read @", "Beginning of the file input klammer"},
{katom_t::eval_begin, "@t ^@eval @", "Beginning of the evaluation klammer"},
{katom_t::cond_begin, "@t ^@cond @", "Beginning of the conditional (if/then/else) klammer"}}},
{"Klammer definition", {
{katom_t::define_begin, "^@^@ #- @symb-name", "Beginning of a klammer definition"},
{katom_t::define_end, "^@^@ @ri-or @symb-name #- ^@^@", "End of a klammer definition"},
{katom_t::klammer_definition, ":", "Klammer definition, including parameters"},
{katom_t::klammer_instance, "::", "Klammer definition using previously defined parameters"},
{katom_t::klammer_override, ":::", "Override existing klammer definition"},
{katom_t::klammer_default, "::::", "Define klammer default value for possible override"},
{katom_t::karg, "* #- @symb-name #- *", "Klammer argument in klammer body definition"}}},
{"System commands", {
{katom_t::machine_begin, "^@^@^@ #- @symb-name", "Beginning of a system command"},
{katom_t::machine_end, "^@^@^@ @ri-or @symb-name #- ^@^@^@", "End of a system command"}}},
{"Text removal and whitespace", {
{katom_t::ignore_line, "^#", "Remove all text to the first newline, inclusive"},
{katom_t::ignore_begin, "^#[", "Beginning of text to remove"},
{katom_t::ignore_end, "]^#", "End of text to remove"},
{katom_t::ignore_rest, "^#^#", "Remove all text to the end of file or string"},
{katom_t::ws_remove, "^#-", "Remove all whitespace at this point"},
{katom_t::ws_space, "^#+ @ri-or ^#+ #- @symb-n", "Remove all whitespace, leaving @symb-n spaces (default: 1)"},
{katom_t::ws_newline, "^#/ @ri-or ^#/ #- @symb-n", "Remove all whitespace, leaving @symb-n newlines (default: 1)"}}},
{"Special characters and literal text", {
{katom_t::special, "^@, ^|, ^#, ^:, @ri-or ^^", "Klammertext special character treated as regular text"},
{katom_t::nonascii, "^^ #- @symb-code @ri-or ^^ #- @symb-code #- ^^", "Non-ASCII character"},
{katom_t::literal_begin, "^^ #- '", "Begin unprocessed text"},
{katom_t::literal_end, "'#- ^^", "End unprocessed text"}}},
{"Types assigned during processing", {
{katom_t::ws_added, "@symb-space @ri-or \\n", "Added whitespace characters from @t ^#+ @ and @t ^#/ @"},
{katom_t::literal, "@symb any-text @", "Literal katom (type changed by @t ^^ #- '...'#- ^^ @ #- )"},
{katom_t::eval_result, "@symb any-text @", "Katom produced by @t ^@eval @ klammer"},
{katom_t::replaced, "@symb any-text @", "A katom replaced by definitions or klammers"},
{katom_t::ignored, "@symb any-text @", "A katom ignored by the action of a text-removal katom"}}}
};
inline const inline const
std::set<katom_t> printable_katom_types { std::set<katom_t> printable_katom_types {
@@ -277,8 +304,7 @@ std::map<katom_t, katom_t> katom_spans {
bool is_active(katom_t t); bool is_active(katom_t t);
bool is_printable(katom_t type); bool is_printable(katom_t type);
void describe_katoms(bool show_regex = false);
bool is_deftype(katom_t type); bool is_deftype(katom_t type);
const Ktype& type_to_katom_instance(katom_t type);
std::string type_to_name(katom_t type); std::string type_to_name(katom_t type);
+11 -2
View File
@@ -1,3 +1,4 @@
#include <algorithm>
#include <set> #include <set>
#include <utility> #include <utility>
#include "machine.h" #include "machine.h"
@@ -402,11 +403,19 @@ void Machine::mark_literal_klammer_content(katom_list& katoms)
// Find where literal content starts. // Find where literal content starts.
// Skip bars_before_literal bars (separating preceding positional args). // Skip bars_before_literal bars (separating preceding positional args).
// If options are present, skip past the bar after them. // If options are present, skip past the bar after them.
// Options are identified by :name katoms before any bar. // Options are identified by :name katoms before any bar -- but only
// a name THE KLAMMER DECLARES. Any ":word" counted until 2026-09-20,
// so code holding one before a bar ("@c ls :w | wc c@", a regex's
// "(?:a|b)") had its text up to the bar taken for options and the
// file failed with 'Optional argument ":w" not defined' -- an error
// about an option the author never wrote, in content that is
// promised to be read as nothing but text.
const auto& optional_names = klammer.m_parameters.m_optional_names;
auto literal_start = k + 1; auto literal_start = k + 1;
bool has_options = false; bool has_options = false;
for (auto j = k + 1; j < close; ++j) { for (auto j = k + 1; j < close; ++j) {
if (j->m_type == katom_t::option_name) { if (j->m_type == katom_t::option_name &&
std::ranges::count(optional_names, std::string(j->m_text, 1)) > 0) {
has_options = true; has_options = true;
} }
if (j->m_type == katom_t::bar) { if (j->m_type == katom_t::bar) {
+12
View File
@@ -36,3 +36,15 @@
@@argtype_definitions.* :: @@argtype_definitions.* ::
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect argtype_definitions @ @@ @eval :cpp *KLAMMERTEXT_HOME*/ref/reflect argtype_definitions @ @@
@@katom_descriptions.k :regex.bool :txt_width.uint :
Describe the katom types, in groups, with the informal pattern of each.
With ^:regex a further column shows the regular expression a katom is
matched by. ^:txt_width is the width of the table in characters under the
txt and ansi targets, where a table cannot be wider than the text: it
widens the table alone, and the text around it keeps Target_txt_width. @@
@@symb s : @ri ⟨*s*⟩ @ @@
@@katom_descriptions.* ::
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect katom_descriptions @ @@
+99
View File
@@ -5,6 +5,7 @@
#include "error.h" #include "error.h"
#include "show.h" #include "show.h"
#include "font_store.h" #include "font_store.h"
#include "ktype.h"
// extern "C" is needed for dlsym name lookup. Returning std::string from // extern "C" is needed for dlsym name lookup. Returning std::string from
// C-linkage functions is technically non-standard but works correctly on // C-linkage functions is technically non-standard but works correctly on
@@ -191,3 +192,101 @@ std::string argtype_definitions(Machine& machine)
kt << "define@\n\n"; kt << "define@\n\n";
return kt.str(); return kt.str();
} }
// The Regex cell of a katom type. A type the Machine assigns is matched by
// no regex and its cell is empty -- ws_added among them, whose m_pattern is a
// description and not a regex (see the note in mac/katom_matching.cpp). The
// two whitespace types are named, since "@c c@" would show nothing.
static std::string katom_regex_cell(const Ktype& k)
{
if (k.m_pattern.empty() || k.m_type == katom_t::ws_added) {
return "";
}
if (k.m_type == katom_t::space) {
return "@symb-space";
}
if (k.m_type == katom_t::newline) {
return "@t \\n @";
}
// A literal value has its caret forms processed as it is built, so a
// caret is written quoted: bare, "[^^@#|]+" would display as "[^@#|]+"
// and "[@|#^:*]" would lose its caret to the colon.
// The exception is a caret before an apostrophe: "^'" is not a quoting
// form (it opens a literal span elsewhere) and passes through as written.
std::string quoted {};
for (size_t i = 0; i < k.m_pattern.size(); i++) {
const char c = k.m_pattern[i];
const bool before_apostrophe = i + 1 < k.m_pattern.size() && k.m_pattern[i + 1] == '\'';
quoted += (c == '^' && !before_apostrophe) ? "^^" : std::string(1, c);
}
return "@c :nd | " + quoted + " c@";
}
extern "C"
std::string katom_descriptions([[maybe_unused]] Machine& machine)
{
std::stringstream kt {};
kt << R"(
A ``katom'' is an individual element in the Klammertext input text. Each katom
has a type, listed below. You can see how Klammertext divides up text into
katoms with the @t-kdiag command.
In the katom patterns, @symb-name is a word that begins with a letter and only
contains letters, numbers, or the underscore (_) or period (.) characters. The
@symb-n symbol is an integer greater than or equal to 1. The @symb-code symbol
is one of the codes used with the @t ^^ @ character to create characters that
are not available in the English alphabet. The codes for producing those
characters can be displayed by the @t-kdesc command: @t kdesc -c -u @
)";
// ":txt_width N" widens the TABLE alone. This text is re-read by a
// Machine of its own (Eval::eval), so a @@@state here changes the width
// the @table below is laid out to and nothing else: the prose above
// leaves that Machine as plain text and is filled to the caller's width
// by the caller's phases, which skip the table because it is laid out.
const std::string txt_width = machine.m_state.value("txt_width", false);
if (!txt_width.empty()) {
kt << "@@@state Target_txt_width :replace " << txt_width << " @@@\n";
}
const bool show_regex = machine.m_state.value("regex", false) == "true";
// The rows first, so that the rules can be placed by the number of rows
// before them (the header is the first): one above each group's title
// and one below it, which is what marks the row as a title. The first
// group needs none above: "head" already rules the header off.
const std::string b = " | ";
const std::string title_cells = show_regex ? b + b + b + b : b + b + b;
std::stringstream rows {};
std::stringstream hlines {};
int row_count = 1;
for (const auto& group : katom_type_groups) {
if (row_count > 1) {
hlines << " " << row_count;
}
rows << title_cells << "@i " << group.m_title << " @ ||\n";
row_count++;
hlines << " " << row_count; // ... and one below the title
for (const auto& type : group.m_types) {
const Ktype& k = type_to_katom_instance(type.m_type);
rows << static_cast<int>(k.m_type) << b << k.m_name << b << type.m_pattern << b;
if (show_regex) {
rows << katom_regex_cell(k) << b;
}
rows << type.m_description << " ||\n";
row_count++;
}
}
kt << "@table "
<< ":number false "
<< ":hline head bottom" << hlines.str() << " :leading .95 ";
if (show_regex) {
kt << ":cell_hpos c l :style r r t t r :column_width fit fit fit fit fill |\n"
<< "Index | Name | Pattern | Regex | Description ||\n";
} else {
kt << ":cell_hpos c l :style r r t r :column_width fit fit fit fill |\n"
<< "Index | Name | Pattern | Description ||\n";
}
kt << rows.str() << "table@\n";
return kt.str();
}
+4 -3
View File
@@ -13,15 +13,16 @@ katom when no bar is written, "^@code :filename f :pattern p" DISPLAYED THOSE
WORDS as the listing rather than acting on them. WORDS as the listing rather than acting on them.
@@ @@
@@code.html,tex :: @eval code_block.Code(K) @ @@ @@code.html,tex,txt :: @eval code_block.Code(K) @ @@
@@c.k code_text.literal : @@c.k :nd.bool | code_text.literal :
A word or phrase displayed verbatim in a line. Like ^@code, its content is A word or phrase displayed verbatim in a line. Like ^@code, its content is
LITERAL -- nothing in it is interpreted as Klammertext -- so it must be closed LITERAL -- nothing in it is interpreted as Klammertext -- so it must be closed
with the named delimiter "c^@". ^@c is the in-line form and ^@code the block with the named delimiter "c^@". ^@c is the in-line form and ^@code the block
form of the same thing; before 2026-08-16 this parameter was an ordinary form of the same thing; before 2026-08-16 this parameter was an ordinary
string, so a "^#" or a bare "^@" inside it was read as Klammertext and usually string, so a "^#" or a bare "^@" inside it was read as Klammertext and usually
failed the file, which is not what "verbatim" can mean. failed the file, which is not what "verbatim" can mean.
If :nd (no delimiters) is true, then the txt target delimiters are excluded.
@@ @@
@@c.html,tex,txt :: @eval code_block.Code_fragment(K) eval@ @@c.html,tex,txt :: @eval code_block.Code_fragment(K) eval@
@@ -44,4 +45,4 @@ quietly different listing.
Named "^@source_file" until 2026-08-16. # retired-ok Named "^@source_file" until 2026-08-16. # retired-ok
@@ @@
@@source_listing.html,tex :: @eval :cwd *K_input_dir* code_block.Source(K) @ @@ @@source_listing.html,tex,txt :: @eval :cwd *K_input_dir* code_block.Source(K) @ @@
+72 -6
View File
@@ -5,11 +5,13 @@
import re import re
import klammer_base import klammer_base
import kutil
import html_util import html_util
import txt_util
import phases
from html_util import E from html_util import E
import latex_util as L import latex_util as L
def escape_newlines(s): def escape_newlines(s):
# A newline becomes the marker the html paragraph pass turns back into a # 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. # line break, so a verbatim source file keeps its lines. Used by Source.
@@ -119,7 +121,13 @@ def tex_line(line):
a monospace font, which LaTeX will not collapse -- so the code's a monospace font, which LaTeX will not collapse -- so the code's
indentation and internal alignment survive. indentation and internal alignment survive.
""" """
return quote_specials(escape_latex(line)).replace(" ", "~") # An apostrophe is hidden as a marker: after a quoted caret it would
# otherwise read back as "^" + "^'", the opening of a literal span, and
# "@c \^' c@" -- the regex of that very katom -- failed the document
# with a span that does not end. html_line hides it with the other
# typographic characters; the marker decodes to the plain character.
quoted = quote_specials(escape_latex(line)).replace("'", klammer_base.escape_ktesc("'"))
return quoted.replace(" ", "~")
def widest_line(lines): def widest_line(lines):
r"""The line to measure the block's width with. r"""The line to measure the block's width with.
@@ -325,12 +333,64 @@ class Code(klammer_base.Klammer_base):
# As in tex: the caption goes through the shared helper, so it sits # As in tex: the caption goes through the shared helper, so it sits
# in the same position container as the listing and moves with it. # in the same position container as the listing and moves with it.
if self.number or self.caption: if self.number or self.caption:
# A left-placed (or flowing) listing's caption is flush with it,
# as in tex, where such a listing is unboxed and its caption a
# paragraph on the same indent; a centred or right-placed one is
# boxed there and its caption centred over the box, which is
# the helper's default here too.
align = "left" if self.hpos in ("left", "none") else None
return html_util.add_caption( return html_util.add_caption(
listing, "Listing", self.number, self.caption, listing, "Listing", self.number, self.caption,
self.caption_style, self.hpos, self.caption_side, True, self.caption_style, self.hpos, self.caption_side, True,
self.caption_size, offset=self.offset) self.caption_size, offset=self.offset, caption_align=align)
return html_util.hpos_container(listing, self.hpos, self.offset).str() return html_util.hpos_container(listing, self.hpos, self.offset).str()
def txt_indent(self):
# The block's indent in columns, by @table's rule: a left- or
# right-placed block is inset by its :offset (0 = flush), ":hpos
# none" flows with no indent, and "center" -- not attempted in plain
# text -- keeps the block convention, Target_txt_indent.
if self.hpos in ("left", "right"):
return int(kutil.parse_width("txt", self.offset)[0])
if self.hpos == "none":
return 0
return int(self.Target_txt_indent)
def txt(self):
# The listing is LAID OUT here, line by line, and says so with the
# verbatim marker (phases.txt_lay_out) so the justification pass
# leaves it alone -- a blank line in the code included, which holds
# a marker and therefore does not end the paragraph.
#
# Each line is prepared exactly as txt()'s inline sibling prepares a
# fragment: the Klammertext specials QUOTED, because this result is
# re-read as Klammertext (raw, a "#" removed the rest of its line and
# a "code@" in a quoted source file closed the document's own span:
# "A named end katom does not match"), and the typographic characters
# hidden, so "--x" stays two hyphens. Spaces become the nbsp marker
# AFTER that, not "~": hide_typographic has already hidden the
# code's own tildes, and a "~" written here would be turned into a
# space by the txt target's transform only as long as it survived.
lines = self.text.strip("\n").split("\n")
indent = txt_util.NBSP * self.txt_indent()
def prepared(line):
line = klammer_base.hide_typographic(quote_specials(line.rstrip()))
return line.replace(" ", txt_util.NBSP)
width = max(len(line.rstrip()) for line in lines)
rule = indent + klammer_base.hide_typographic("-" * width)
body = [indent + prepared(line) for line in lines]
# A caption above the listing is ruled off by the caption pass, so
# the listing draws its own top rule only when nothing does.
captioned = self.number or self.caption
caption_above = captioned and self.caption_side in ("top", "left")
if not caption_above:
body.insert(0, rule)
body.append(rule)
result = phases.txt_lay_out("\n".join(body))
return txt_util.add_caption(
result, "Listing", self.number, self.caption, self.caption_side,
self.txt_indent())
class Code_fragment(klammer_base.Klammer_base): class Code_fragment(klammer_base.Klammer_base):
def __init__(self, K): def __init__(self, K):
@@ -356,9 +416,12 @@ class Code_fragment(klammer_base.Klammer_base):
# without a beginning) and the typographically active characters # without a beginning) and the typographically active characters
# hidden, so a "--" in code stays two hyphens. # hidden, so a "--" in code stays two hyphens.
result = klammer_base.hide_typographic(quote_specials(self.code_text.strip())) result = klammer_base.hide_typographic(quote_specials(self.code_text.strip()))
# Surround code symbols with ‹ and ›, though there are conflicts in Swiss
# German, Italian and some Scandinavian languages. Should be clear in # If *nd* ("no delimiters) is false (the default), surround code symbols
# context, though. # with the characters ‹ and ›. There are conflicts in Swiss German,
# Italian and some Scandinavian languages, but the meaning should be
# clear in context.
if not self.nd:
result = "\u2039" + result + "\u203A" result = "\u2039" + result + "\u203A"
return result return result
@@ -424,6 +487,9 @@ class Source(Code):
# not exist until the file has been read. File content carries no # not exist until the file has been read. File content carries no
# whitespace markers anyway, since those come from the katomizer. # whitespace markers anyway, since those come from the katomizer.
klammer_base.Klammer_base.__init__(self, K) klammer_base.Klammer_base.__init__(self, K)
# The filename arrives with the target's escapes as markers -- under
# tex an underscore is one -- and a marker names no file.
self.filename = klammer_base.unescape_ktesc(self.filename)
try: try:
with open(self.filename) as fp: with open(self.filename) as fp:
text = fp.read() text = fp.read()
+5 -1
View File
@@ -30,7 +30,11 @@
.code_block { .code_block {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0 0 0 1rem; /* No left margin: it was 1rem, an html-only inset with no tex
counterpart, which set the code a rem to the right of its own
flush-left caption and of the same listing's pdf. A listing's
inset is :offset, in both targets (the hpos_margin precedent). */
margin: 0;
padding: 0; padding: 0;
} }
+9 -1
View File
@@ -4,6 +4,14 @@
--serif: Libre Baskerville, serif; --serif: Libre Baskerville, serif;
--sans-serif: Open Sans, sans-serif; --sans-serif: Open Sans, sans-serif;
--monospace: Inconsolata, monospace; --monospace: Inconsolata, monospace;
/* The pitch of a line of body text, in em of the NOMINAL font size.
It is 1.5 rather than LaTeX's 1.2 because font-size-adjust (below)
draws the type larger than its nominal size -- EB Garamond by a
fifth -- and line-height is computed from the nominal one. It is
the html counterpart of \baselineskip, and a klammer whose spacing
is stated in lines of text multiplies THIS (@table :leading), so a
table and the paragraphs around it cannot drift apart. */
--text_line_height: 1.5;
} }
body { body {
@@ -32,7 +40,7 @@ h1, h2, h3, h4, h5 {
place); this file assigns only the family. */ place); this file assigns only the family. */
p { p {
line-height: 1.5; line-height: var(--text_line_height);
} }
.plain { .plain {
+1 -1
View File
@@ -195,7 +195,7 @@ other structures. The ^:offset is used only if the ^:hpos value is "left" or
:number.bool true :number.bool true
:caption_side.side :caption_side.side
:caption_style.style_code i :caption_style.style_code i
:caption_size.float .9 :caption_size.float 1
: Arguments that define a caption for a block element. : Arguments that define a caption for a block element.
@@ @@
+2 -2
View File
@@ -2,8 +2,8 @@
# the front and back matter of a book): # the front and back matter of a book):
@@section_args.o :id :n.int -1 : @@section_args.o :id :n.int -1 :
Common arguments for section headings. If *n* is not the default, it is used Common arguments for section headings. If *n* is not the default,
as the section number. it is used as the section number.
@@ @@
@@preface.k :id :name Preface : Preface text @@ @@preface.k :id :name Preface : Preface text @@
+14
View File
@@ -58,8 +58,21 @@ td {
--cell-border: 1px black solid; --cell-border: 1px black solid;
} }
/* The space between a rule and the text on either side of it: a cell is
padded on the side that CARRIES the rule, so a row with no rule is
untouched, a partial :hline pads only the cells it crosses, and the
table's outer rules are padded on the inner side alone. The amounts
began as booktabs' defaults (0.4ex above a rule, 0.65ex below one) and
were tuned by eye, 2026-09-20: half of the space below moved above, so
0.725ex above and 0.325ex below, the total unchanged. The tex target's
pair is in table.py (TEX_RULE_ABOVE/_BELOW) and differs on purpose -- a
TeX row and an html row distribute their own height differently around
the text. --rulesep is @table's :rulesep, a multiplier, set on the
<table> by table.py. "ex" is the x-height as RENDERED
(font-size-adjust), so the space tracks the visible type. */
.Bt { .Bt {
border-top: var(--cell-border); border-top: var(--cell-border);
padding-top: calc(var(--rulesep, 1) * 0.325ex);
} }
.Br { .Br {
@@ -68,6 +81,7 @@ td {
.Bb { .Bb {
border-bottom: var(--cell-border); border-bottom: var(--cell-border);
padding-bottom: calc(var(--rulesep, 1) * 0.725ex);
} }
.Bl { .Bl {
+11 -1
View File
@@ -213,7 +213,8 @@
:calc.table_calc :calc.table_calc
:format.table_format :format.table_format
:decimal.decimal_mark :decimal.decimal_mark
:leading.float 1.3 :leading.float 1.0
:rulesep.float 1
:colsep 4pt :colsep 4pt
: :
A table of rows of cells. The cells of a row are separated by "|" and the A table of rows of cells. The cells of a row are separated by "|" and the
@@ -226,6 +227,15 @@ draw lines, ^:colspan and ^:rowspan merge cells, ^:justify and ^:cell_hpos
place text within them, ^:calc computes cells from other cells and ^:format place text within them, ^:calc computes cells from other cells and ^:format
formats them, and ^:hpos and ^:offset place the whole table in the text formats them, and ^:hpos and ^:offset place the whole table in the text
column. column.
^:leading is the height of a row in lines of the surrounding text, the same
in html and pdf. ^:rulesep is the space between a horizontal line and the
text on either side of it, as a multiple of the standard amount (slightly
more below a line than above it); 0 sets the lines against the rows. The
lines at the top and the bottom of the table are spaced on their inner side
only. A table with vertical lines gets no such space in a pdf, where it
would break them. Neither argument has a meaning in plain text, where a row
is a line of characters and a horizontal line already has a line to itself.
@@ @@
@@table.html,tex,txt :: @eval table.Table(K) eval@ @@ @@table.html,tex,txt :: @eval table.Table(K) eval@ @@
+76 -28
View File
@@ -575,23 +575,39 @@ class Table(klammer_base.Klammer_base):
# would wrap it as a stray <p> inside the table. # would wrap it as a stray <p> inside the table.
return E("colgroup").body(cols.strip(), newline=False).str(), layout, table_width return E("colgroup").body(cols.strip(), newline=False).str(), layout, table_width
def html_style(self): def html_style(self, layout=""):
# :leading and :colsep under html (2026-09-18; both were tex-only). # The WHOLE style attribute of the <table>, written once: "layout" is
# \arraystretch multiplies the row strut, which is \baselineskip, # the width declarations the caller has worked out (max-width for a
# 1.2 times the font size -- so :leading L is a 1.2 L em row, and # fill table, table-layout/width for fractions). E.attr() appends,
# the html equivalent is that line-height on the table, inherited # so a second .attr("style", ...) made a second attribute, which a
# by every cell. The cells' vertical padding went with it # browser discards -- from 2026-09-18, when this function arrived,
# (table.css): an html-only extra with no tex counterpart, which # to 2026-09-20 every width declaration was lost that way.
# kept a :leading 1 table 0.3rem looser per row than its pdf. # :leading and :colsep under html. ":leading L" is L times the pitch
# :colsep is a custom property the td rule reads for its horizontal # of a line of the surrounding TEXT, in every target. tex has that
# padding (padding does not inherit); the flush-edge classes still # by construction: \arraystretch multiplies the row strut, which is
# zero the outer ones. Measured 2026-09-18: rows of :leading 1 and # the \baselineskip in force. The html counterpart of \baselineskip
# :leading 2 in the ratio 1:2 in both targets. # is --text_line_height (font.css, 1.5), so the table's line-height
line_height = f"{1.2 * float(self.leading):.4g}" # is L times that, inherited by every cell. It was 1.2 L from
# 2026-09-18 to 09-20, LaTeX's ratio of \baselineskip to font size:
# wrong in html, where font-size-adjust draws the type larger than
# the nominal size line-height is computed from, so a :leading 1
# table sat 16% tighter than the paragraphs around it (measured:
# 20.2px rows beside 24px text, where the pdf had 14.2pt beside
# 14.2pt). The fallback serves a fragment without the stylesheet.
# The cells have no vertical padding (table.css): an html-only extra
# with no tex counterpart. :colsep is a custom property the td rule
# reads for its horizontal padding (padding does not inherit); the
# flush-edge classes still zero the outer ones.
line_height = f"calc({float(self.leading):.4g} * var(--text_line_height, 1.5))"
# Hidden: the result is re-read and the html target's transforms # Hidden: the result is re-read and the html target's transforms
# would turn the property's "--" into an en dash (font.py's trap). # would turn the property's "--" into an en dash (font.py's trap).
return klammer_base.hide_typographic( # :rulesep is a custom property too: the .Bt/.Bb rules (table.css)
f"line-height: {line_height}; --colsep: {self.colsep}") # read it for the padding on the side of a cell that carries a rule.
style = (f"line-height: {line_height}; --colsep: {self.colsep}; "
f"--rulesep: {float(self.rulesep):.4g}")
if layout:
style += "; " + layout.strip().rstrip(";")
return klammer_base.hide_typographic(style)
def html(self): def html(self):
result = "" result = ""
@@ -610,8 +626,10 @@ class Table(klammer_base.Klammer_base):
# algorithm), matching the tex \ratio division. The max-width # algorithm), matching the tex \ratio division. The max-width
# cap goes on the caption wrapper when there is one (the table's # cap goes on the caption wrapper when there is one (the table's
# own percentage would be circular in a shrink-to-fit wrapper). # own percentage would be circular in a shrink-to-fit wrapper).
result = E("table").body(result).attr("style", self.html_style()) captioned = self.number or self.caption
if self.number or self.caption: result = E("table").body(result).attr(
"style", self.html_style("" if captioned else "max-width: 100%"))
if captioned:
result = html_util.add_caption( result = html_util.add_caption(
result, "Table", self.number, self.caption, result, "Table", self.number, self.caption,
self.caption_style, hpos=self.hpos, self.caption_style, hpos=self.hpos,
@@ -619,17 +637,19 @@ class Table(klammer_base.Klammer_base):
font_size=self.caption_size, max_width="100%", font_size=self.caption_size, max_width="100%",
offset=self.offset) offset=self.offset)
else: else:
result.attr("style", "max-width: 100%")
result = html_util.hpos_container(result, self.hpos, self.offset).str() result = html_util.hpos_container(result, self.hpos, self.offset).str()
return result return result
colgroup, layout, table_width = self.html_colgroup() colgroup, layout, table_width = self.html_colgroup()
result = E("table").body(colgroup + result).attr("style", self.html_style()) captioned = self.number or self.caption
if self.number or self.caption: table_layout = ""
if table_width:
table_layout = f"{layout}width: " + ("100%" if captioned else table_width)
result = E("table").body(colgroup + result).attr(
"style", self.html_style(table_layout))
if captioned:
# The caption wrapper carries the width (a percentage on the # The caption wrapper carries the width (a percentage on the
# shrink-to-fit wrapper itself would be circular) and the table # shrink-to-fit wrapper itself would be circular) and the table
# fills it -- which also makes the caption track the table. # fills it -- which also makes the caption track the table.
if table_width:
result.attr("style", f"{layout}width: 100%")
result = html_util.add_caption( result = html_util.add_caption(
result, "Table", self.number, self.caption, self.caption_style, result, "Table", self.number, self.caption, self.caption_style,
hpos=self.hpos, side=self.caption_side, hpos=self.hpos, side=self.caption_side,
@@ -638,8 +658,6 @@ class Table(klammer_base.Klammer_base):
else: else:
# An uncaptioned table still gets the position container, so # An uncaptioned table still gets the position container, so
# html and tex agree on where the table sits. # html and tex agree on where the table sits.
if table_width:
result.attr("style", f"{layout}width: {table_width}")
result = html_util.hpos_container(result, self.hpos, self.offset).str() result = html_util.hpos_container(result, self.hpos, self.offset).str()
return result return result
@@ -761,9 +779,32 @@ class Table(klammer_base.Klammer_base):
# print("tex_column_spec:", "".join(parts)) # print("tex_column_spec:", "".join(parts))
return "".join(parts) return "".join(parts)
TEX_RULE_ABOVE = 0.4 # ex; booktabs' \aboverulesep
TEX_RULE_BELOW = 0.35 # 468 # ex; booktabs' \belowrulesep (0.65) less 20%, less 10%
def tex_rule_separations(self, top, bottom):
# The space above and below a rule, as booktabs' \specialrule and
# \cmidrule take them: :rulesep times TEX_RULE_ABOVE/_BELOW. They
# began as booktabs' defaults (0.4ex above a rule, 0.65ex below) and
# were tuned by eye, per target, 2026-09-20 (Andy): the amounts that
# LOOK alike differ, because a TeX row carries its strut's depth
# under the text and an html row its half-leading. The html pair is
# in table.css (.Bt/.Bb). The table's outer rules are padded on the
# INNER side only.
rulesep = float(self.rulesep)
above = "0pt" if top else f"{self.TEX_RULE_ABOVE * rulesep:.4g}ex"
below = "0pt" if bottom else f"{self.TEX_RULE_BELOW * rulesep:.4g}ex"
return above, below
def tex_hline(self, index): def tex_hline(self, index):
# Contiguous cell borders coalesce into single \cline runs; a # Contiguous cell borders coalesce into single rule runs. The rules
# full-width line becomes \hline. # are booktabs' -- an \hline sits flush against the strut of the
# rows on either side, and the only way to open space there is a
# strut in every neighbouring row -- EXCEPT in a table with vertical
# lines: a booktabs rule is set between rows, so it breaks every
# vertical line it crosses (its author provides for none), and such
# a table keeps \hline/\cline and gets no space at its rules.
top = index == 0
bottom = index == self.row_count bottom = index == self.row_count
if bottom: if bottom:
index -= 1 index -= 1
@@ -773,7 +814,11 @@ class Table(klammer_base.Klammer_base):
# No line through the interior of a merged (rowspan) cell. # No line through the interior of a merged (rowspan) cell.
flags = [flag and (index, col_i) not in self.rowspan_covered flags = [flag and (index, col_i) not in self.rowspan_covered
for col_i, flag in enumerate(flags)] for col_i, flag in enumerate(flags)]
spaced = not self.s_vline.by_index and float(self.rulesep) > 0
above, below = self.tex_rule_separations(top, bottom)
if flags and all(flags): if flags and all(flags):
if spaced:
return f"\\specialrule{{\\arrayrulewidth}}{{{above}}}{{{below}}}\n"
return "\\hline\n" return "\\hline\n"
hline = "" hline = ""
start = None start = None
@@ -781,6 +826,9 @@ class Table(klammer_base.Klammer_base):
if flag and start is None: if flag and start is None:
start = i start = i
elif not flag and start is not None: elif not flag and start is not None:
if spaced:
hline += f"\\cmidrule[\\arrayrulewidth]{{{start + 1}-{i}}} "
else:
hline += f"\\cline{{{start + 1}-{i}}} " hline += f"\\cline{{{start + 1}-{i}}} "
start = None start = None
return hline.strip() + "\n" return hline.strip() + "\n"
@@ -1063,7 +1111,7 @@ class Table(klammer_base.Klammer_base):
if len(rows) in self.s_hline.by_index: if len(rows) in self.s_hline.by_index:
result += rule result += rule
result = txt_util.add_caption( result = txt_util.add_caption(
result, "Table", self.number, self.caption, self.caption_side, \ result, "Table", self.number, self.caption, self.caption_side,
int(self.Target_txt_indent)) len(indent) // len(txt_util.NBSP))
return result return result
+12
View File
@@ -103,6 +103,18 @@
font-style: italic; font-style: italic;
} }
/* A caption flush with its element's left edge (html_util.add_caption,
caption_align "left"): @code's caption when the listing is placed left,
as the tex target sets it. AFTER the two rules above, which it must
override at equal specificity. */
.caption_flush_left {
align-items: flex-start;
}
.caption_flush_left .caption {
text-align: left;
}
.caption_left { .caption_left {
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
+8 -1
View File
@@ -238,7 +238,12 @@ def hpos_container(element, hpos, offset=None):
def add_caption(element, caption_label, number, caption_text, def add_caption(element, caption_label, number, caption_text,
font_symbol="i", hpos="center", side="bottom", as_string=True, font_symbol="i", hpos="center", side="bottom", as_string=True,
font_size=.9, width=None, max_width=None, offset=None): font_size=.9, width=None, max_width=None, offset=None,
caption_align=None):
# caption_align "left" sets the caption flush with the element's left
# edge, where the default centres it over the element: a caption above
# a left-placed listing, which the tex target sets as a paragraph on the
# listing's own indent.
tag = element_tag(element) tag = element_tag(element)
# Caption # Caption
caption = "" caption = ""
@@ -276,6 +281,8 @@ def add_caption(element, caption_label, number, caption_text,
#print("-"*80) #print("-"*80)
element = E("div").cls("caption_" + side).attr("data-label", caption_label).body(element) element = E("div").cls("caption_" + side).attr("data-label", caption_label).body(element)
if caption_align == "left" and side in {"top", "bottom"}:
element.cls("caption_flush_left")
if width: if width:
# The element's width (e.g. a table's :column_width total) lives # The element's width (e.g. a table's :column_width total) lives
# on this wrapper: the element fills it, and the caption tracks # on this wrapper: the element fills it, and the caption tracks
+68 -19
View File
@@ -147,6 +147,36 @@ def tex_nl_marker():
def tex_resolve_markers(text, K): def tex_resolve_markers(text, K):
return text.replace(tex_nl_marker(), "\\newline") return text.replace(tex_nl_marker(), "\\newline")
def txt_runs(par):
"""A paragraph as runs of lines: [(laid_out, text), ...].
"Laid out or prose" was decided ONCE per paragraph, by its first line,
so a laid-out block written directly after prose -- no blank line
between them -- was re-flowed with it, every one of its lines marked
or not: a generator whose prose ran straight into its @table put the
table's header on the last line of the sentence (2026-09-20). A blank
line should not be what protects a block that has already said, line
by line, that it is laid out.
So a line that BEGINS WITH THE VERBATIM MARKER is laid out wherever it
stands, and the paragraph divides into runs at each change. Only the
marker divides: it is explicit and per line (txt_lay_out). A leading
space or nbsp keeps the older, whole-paragraph meaning and only on the
FIRST line, since a later line of prose may legitimately begin with
one (a footnote's hanging indent).
"""
if par.startswith((" ", txt_nbsp_marker())):
return [(True, par)]
mark = txt_verbatim_marker()
runs = []
for line in par.split("\n"):
laid_out = line.startswith(mark)
if runs and runs[-1][0] == laid_out:
runs[-1][1].append(line)
else:
runs.append((laid_out, [line]))
return [(laid_out, "\n".join(lines)) for laid_out, lines in runs]
def txt_justify_blocks(text, K): def txt_justify_blocks(text, K):
delim = '__DIVIDE__' delim = '__DIVIDE__'
text = re.sub("\n\n+", delim, text) text = re.sub("\n\n+", delim, text)
@@ -178,13 +208,20 @@ def txt_justify_blocks(text, K):
# verbatim paragraph was not left alone, it was DELETED: the one # verbatim paragraph was not left alone, it was DELETED: the one
# branch that had nothing to do also did not copy the text through. # branch that had nothing to do also did not copy the text through.
# An indented paragraph vanished from the output with no error. # An indented paragraph vanished from the output with no error.
#
# The decision is per RUN of lines, not per paragraph (txt_runs):
# prose is filled, a laid-out run is copied through, and the runs
# stay on consecutive lines as they were written.
nbsp = txt_nbsp_marker() nbsp = txt_nbsp_marker()
if par and not txt_is_laid_out(par): filled = []
label = re.match("(?:" + re.escape(nbsp) + r")*\[\d+\] ", par) for laid_out, run in txt_runs(par):
par = "\n".join(txt_wrap( if run and not laid_out:
par, int(K.Target_txt_width), label = re.match("(?:" + re.escape(nbsp) + r")*\[\d+\] ", run)
run = "\n".join(txt_wrap(
run, int(K.Target_txt_width),
subsequent_indent=" " * len(label.group(0)) if label else "")) subsequent_indent=" " * len(label.group(0)) if label else ""))
result += par + "\n\n" filled.append(run)
result += "\n".join(filled) + "\n\n"
result = re.sub(" *" + txt_vspace_marker() + " *", "\n", result) result = re.sub(" *" + txt_vspace_marker() + " *", "\n", result)
result = re.sub(r"\n? *" + txt_nl_marker() + r" *\n?", "\n", result) result = re.sub(r"\n? *" + txt_nl_marker() + r" *\n?", "\n", result)
result = result.replace(txt_verbatim_marker(), "") result = result.replace(txt_verbatim_marker(), "")
@@ -192,33 +229,45 @@ def txt_justify_blocks(text, K):
return result return result
def txt_insert_captions(text, K): def txt_insert_captions(text, K):
# The caption's INDENT is the block's, read off the run of nbsp markers
# written before the caption marker (txt_util.add_caption) -- not
# Target_txt_indent, which put the caption of a flush block two columns
# to the right of the block it captions. Every line produced here is
# marked laid-out: a flush caption begins with neither a space nor an
# nbsp, and an unmarked first line made the justification pass re-flow
# the caption, its rule and the whole block under it into one paragraph.
mark = kutil.caption_delimiter() mark = kutil.caption_delimiter()
caption_rgx = re.compile(f"{mark}(.*?){mark}(.*?){mark}", re.S) nbsp = txt_nbsp_marker()
caption_number = 0 caption_rgx = re.compile(
f"((?:{re.escape(nbsp)})*){mark}(.*?){mark}(.*?){mark}", re.S)
# One sequence PER LABEL, as in html and tex: a listing after a table is
# "Listing 1", not "Listing 2". It was one counter for every label.
caption_numbers = {}
width = int(K.Target_txt_width) width = int(K.Target_txt_width)
indent = int(K.Target_txt_indent)
indented_width = width - indent
def replace(m): def replace(m):
nonlocal caption_number indent_run, name, text = m.groups()
name, text = m.groups() indent = len(indent_run) // len(nbsp)
indented_width = max(1, width - indent)
# An empty label means an unnumbered caption (":number false"). It # An empty label means an unnumbered caption (":number false"). It
# must NOT take a number from the sequence: a reader counting the # must NOT take a number from the sequence: a reader counting the
# captions they can see would then find the next one a number # captions they can see would then find the next one a number
# ahead. The same is true of html and tex, where an unnumbered # ahead. The same is true of html and tex, where an unnumbered
# caption emits no marker and so never reaches their counter. # caption emits no marker and so never reaches their counter.
if name: if name:
caption_number += 1 caption_numbers[name] = caption_numbers.get(name, 0) + 1
caption = f"{name} {caption_number} - {text}" caption = f"{name} {caption_numbers[name]} - {text}"
else: else:
caption = text caption = text
# Visible widths throughout: a caption may carry style markers. # Visible widths throughout: a caption may carry style markers.
lines = [caption]
if txt_visible_length(caption) > indented_width: if txt_visible_length(caption) > indented_width:
caption = "\n".join(txt_wrap( lines = txt_wrap(caption, indented_width)
caption, indented_width, line_length = max(txt_visible_length(e) for e in lines)
subsequent_indent=txt_nbsp_marker()*indent)) lines.append("-" * line_length)
line_length = max([txt_visible_length(e) for e in caption.split("\n")]) lead = txt_verbatim_marker() + indent_run
line = txt_nbsp_marker() * indent + "-" * line_length # The first line's marker is already in the text, before the indent
return caption + "\n" + line # run this match begins with (add_caption); the others get theirs.
return indent_run + ("\n" + lead).join(lines)
result = caption_rgx.sub(replace, text) result = caption_rgx.sub(replace, text)
return result return result
+5 -1
View File
@@ -101,7 +101,11 @@ def add_caption(element, caption_label, number, caption_text,
caption = kutil.caption_marker("", caption_text, None) caption = kutil.caption_marker("", caption_text, None)
else: else:
return element return element
caption = indent_text(re.sub(r"\s*\n\s*", " ", caption.strip()), indent) # Marked laid-out, like the block: with no indent the caption begins
# with neither a space nor an nbsp, and the justification pass would
# re-flow it together with everything under it.
caption = phases.txt_verbatim_marker() + indent_text(
re.sub(r"\s*\n\s*", " ", caption.strip()), indent)
if side in {"top", "left"}: if side in {"top", "left"}:
return caption + "\n" + element return caption + "\n" + element
return element + "\n" + caption return element + "\n" + caption
+30
View File
@@ -181,6 +181,36 @@ check_eq "25. a real literal klammer still works after a comment naming it" \
'[x]' --klammersets none -t t -s "$T $LIT # mentions @lit here '[x]' --klammersets none -t t -s "$T $LIT # mentions @lit here
@lit x lit@" @lit x lit@"
echo
echo "-- 25a-e: only a DECLARED option name opens an option region --"
# The content of a literal klammer begins after the bar that follows its
# options. ANY ":word" before a bar counted as an option until 2026-09-20,
# declared or not, so code holding one ("ls :w | wc", a regex's "(?:a|b)")
# lost everything up to the bar to an option region and the file failed with
# 'Optional argument ":w" not defined' -- an error about an option the author
# never wrote, in content promised to be read as nothing but text.
# OPT declares one option, :flag, so both directions can be asserted.
OPT='@@opt.k :flag.bool | t.literal : A literal klammer with an option @@ @@opt.t :: [*flag*/*t*] @@'
OPTUSE=$(mktemp /tmp/escape_opt.XXXXXX).kt
opt_case() { # opt_case NAME EXPECTED USE
printf '%s\n' "$3" > "$OPTUSE"
check_eq "$1" "$2" "$OPTUSE" --klammersets none -t t -d -s "$T $OPT"
}
opt_case "25a. an UNDECLARED :name before a bar is content" \
'[/x :y | z]' '@opt x :y | z opt@'
opt_case "25b. ... as is a regex's (?:a|b)" \
'[/(?:a|b)]' '@opt (?:a|b) opt@'
opt_case "25c. a DECLARED option still opens the option region" \
'[true/x]' '@opt :flag | x opt@'
opt_case "25d. ... and after its bar an undeclared :name is content again" \
'[true/x :y | z]' '@opt :flag | x :y | z opt@'
# The one ambiguity left, and the author's way out of it: a name the klammer
# DOES declare, written inside the code before a bar, is an option -- unless
# an option region has already been written, which ends at ITS bar.
opt_case "25e. a declared name in the code is content once the options are written" \
'[false/x :flag | z]' '@opt :flag false | x :flag | z opt@'
rm -f "$OPTUSE"
rm -f "$ERR" rm -f "$ERR"
echo echo
+11
View File
@@ -107,6 +107,17 @@ echo
echo "-- result detail is a word, not a verbosity level --" echo "-- result detail is a word, not a verbosity level --"
shows " 9. --katoms full adds the regex column" 'Regex' "$KDESC" --katoms full shows " 9. --katoms full adds the regex column" 'Regex' "$KDESC" --katoms full
absent "10. --katoms alone omits it" 'Regex' "$KDESC" --katoms absent "10. --katoms alone omits it" 'Regex' "$KDESC" --katoms
# The regex goes through @c, which processes caret forms: a bare "[^^@#|]+"
# displayed as "[^@#|]+", a different regex, until the generator quoted its
# carets. These two are the patterns that carry a caret before a special.
shows " 9a. ... a regex keeps a caret before a special" '\[\^\^@#\|\]\+' "$KDESC" --katoms full
shows " 9b. ... and a caret before a colon" '\[@\|#\^:\*\]' "$KDESC" --katoms full
shows " 9c. the types are grouped under titles" '^ +Klammer definition *$' "$KDESC" --katoms
# The table is wider than the text by the klammer's own argument, so the
# paragraphs above it keep the text width (72) while a table row exceeds it.
absent "10a. the prose above the table keeps the text width" \
'^[A-Za-z].{72,}$' "$KDESC" --katoms
shows "10b. ... while the table is wider than the text" '^ +Index .{72,}$' "$KDESC" --katoms
shows "11. --coverage all adds the file column" 'sks/table/table\.k' "$KDESC" --coverage all shows "11. --coverage all adds the file column" 'sks/table/table\.k' "$KDESC" --coverage all
absent "12. --coverage alone omits it" 'sks/table/table\.k' "$KDESC" --coverage absent "12. --coverage alone omits it" 'sks/table/table\.k' "$KDESC" --coverage
# "all" is one word for both kinds of missing information: if you want some of # "all" is one word for both kinds of missing information: if you want some of