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:
1 parent
63956c00e5
commit
217c0f3fc5
23 files changed
+625
-290
No files matched your search
@@ -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 `9fa469dd74ed`.
|
||||
This snapshot was assembled from development commit `2e453744f5e9`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+21
-5
@@ -158,10 +158,17 @@ static std::string text_target()
|
||||
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;
|
||||
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("@diacritics_examples@");
|
||||
M.read_string("@extended_latin_examples@");
|
||||
@@ -171,20 +178,29 @@ void show_special_characters()
|
||||
|
||||
void show_unicode_examples()
|
||||
{
|
||||
Machine M;
|
||||
M.read(M.m_state.value("KLAMMERTEXT_HOME") + "/ref/ref.k");
|
||||
Machine M = ref_machine();
|
||||
M.read_string("@unicode_examples@");
|
||||
std::cout << "\n" << M.apply(text_target()) << "\n\n";
|
||||
}
|
||||
|
||||
void show_argtype_definitions()
|
||||
{
|
||||
Machine M;
|
||||
M.read(M.m_state.value("KLAMMERTEXT_HOME") + "/ref/ref.k");
|
||||
Machine M = ref_machine();
|
||||
M.read_string("@argtype_definitions@");
|
||||
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[])
|
||||
{
|
||||
try {
|
||||
|
||||
+27
-13
@@ -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. |
|
||||
| `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. |
|
||||
| `@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@
|
||||
|
||||
@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
|
||||
@@ -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) |
|
||||
| structure | `@s1`…`@s7` numbered headings, `@h1`…`@h7` unnumbered, `@part`, `@preface`, `@par`, `@newpage`, `@vspace lines`, `@vfill`, `@nl` line break |
|
||||
| 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 (`: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` |
|
||||
| lists | `@ul :bullet`, `@ol :initial`, `@define :style` term/definition, `@qa question \| answer @` — items separated by `\|` |
|
||||
| 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.
|
||||
Argument order is free for options, fixed for positionals.
|
||||
|
||||
**Coverage today** (`kdesc --coverage`; 89 klammers): html, tex and pdf cover
|
||||
nearly all. **txt and ansi cover 40**, among them `@b @i @t @u @s @r @ri @ts
|
||||
**Coverage today** (`kdesc --coverage`; 83 klammers): html, tex and pdf cover
|
||||
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
|
||||
@lines @qa`, `@ul @ol`, `@table @tbl @reference @define @left_right`,
|
||||
`@footnote @link`, `@date @datetime`, `@c`. Not for txt: `@code
|
||||
@source_listing @image @image_grid @quote @note @center @block @columns
|
||||
@multicolumn @sup @sub @color @h1`–`@h7`.
|
||||
`@footnote @link`, `@date @datetime`, `@c @code @source_listing @columns`,
|
||||
`@h1`–`@h7`. Not for txt: `@image @image_grid @quote @note @center @block
|
||||
@multicolumn @sup @sub @color`.
|
||||
`@u` has no html definition. A klammer applied under a target that lacks it is
|
||||
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.
|
||||
- Lines: default under the header and at the bottom; `:hline`/`:vline`
|
||||
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`
|
||||
makes a page-breaking table (pdf).
|
||||
- `: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 |
|
||||
| `tex` | `<name>.tex` | XeLaTeX source; fonts via fontspec from the font store |
|
||||
| `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) |
|
||||
|
||||
`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
|
||||
ships eb-garamond, crimson-pro, open-sans, source-sans-3, inconsolata,
|
||||
source-code-pro, noto-sans-egyptian-hieroglyphs, noto-sans-cuneiform (see
|
||||
`kdesc --typeface`); more are installed with `kdesc --typeface install <dir>`
|
||||
into `KLAMMERTEXT_FONTS`. An unknown typeface is an error listing the available
|
||||
ones. CJK is deliberately not shipped.
|
||||
source-code-pro, noto-sans-egyptian-hieroglyphs, noto-sans-cuneiform and
|
||||
newgardineromni (see `kdesc --typeface`); more are installed with
|
||||
`kdesc --typeface install <dir>` into `KLAMMERTEXT_FONTS`. An unknown typeface
|
||||
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
@@ -2,6 +2,7 @@
|
||||
#include <iomanip>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "locator.h"
|
||||
#include "ktype.h"
|
||||
@@ -43,127 +44,21 @@ bool is_deftype(katom_t type)
|
||||
type == katom_t::klammer_default;
|
||||
}
|
||||
|
||||
std::string pattern_display(Ktype t)
|
||||
const Ktype& type_to_katom_instance(katom_t type)
|
||||
{
|
||||
std::string pat{};
|
||||
if (t.m_type == katom_t::space) {
|
||||
pat = "<space>";
|
||||
} else if (t.m_type == katom_t::newline) {
|
||||
pat = "\\n";
|
||||
} else if (t.m_type == katom_t::ws_space) {
|
||||
pat = "#+ or #+<number>";
|
||||
} 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, "\\", "");
|
||||
auto var = std::find_if(katom_types.begin(), katom_types.end(),
|
||||
[&] (const Ktype& t) { return t.m_type == type; });
|
||||
if (var == katom_types.end()) {
|
||||
throw std::logic_error(
|
||||
"type_to_katom_instance: katom type " +
|
||||
std::to_string(static_cast<int>(type)) +
|
||||
" has no entry in katom_types. See file mac/ktype.h.");
|
||||
}
|
||||
return pat;
|
||||
}
|
||||
|
||||
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";
|
||||
return *var;
|
||||
}
|
||||
|
||||
|
||||
std::string type_to_name(katom_t type)
|
||||
{
|
||||
auto var = std::find_if(katom_types.begin(), katom_types.end(),
|
||||
[&] (Ktype t) { return t.m_type == type; });
|
||||
return var->m_name;
|
||||
return type_to_katom_instance(type).m_name;
|
||||
}
|
||||
+112
-86
@@ -69,10 +69,10 @@ enum class katom_t {
|
||||
karg,
|
||||
machine_begin,
|
||||
machine_end,
|
||||
ignore_line,
|
||||
ignore_begin,
|
||||
ignore_end,
|
||||
ignore_rest,
|
||||
ignore_line,
|
||||
ws_remove,
|
||||
ws_space,
|
||||
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;
|
||||
|
||||
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 {};
|
||||
|
||||
class Ktype {
|
||||
public:
|
||||
// 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_name(name),
|
||||
m_pattern(pattern),
|
||||
m_description(description),
|
||||
m_use_equal(use_equal),
|
||||
m_rgx(std::regex(pattern)) {
|
||||
katom_type_list.push_back(type);
|
||||
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 {
|
||||
return m_use_equal ? s == m_pattern : std::regex_match(s, m_rgx);
|
||||
}
|
||||
katom_t m_type;
|
||||
std::string m_name;
|
||||
std::string m_pattern;
|
||||
std::string m_description;
|
||||
bool m_use_equal;
|
||||
std::regex m_rgx;
|
||||
};
|
||||
|
||||
inline const
|
||||
std::vector<Ktype> katom_types {
|
||||
Ktype(katom_t::space, "space", " ", "One space character", false),
|
||||
Ktype(katom_t::word, "word", "[a-zA-Z]+", "Text only containing letters", false),
|
||||
Ktype(katom_t::newline, "newline", "\n", "One newline character", false),
|
||||
Ktype(katom_t::bar, "bar", "\\|", "Bar character used as positional argument separator", false),
|
||||
Ktype(katom_t::double_bar, "double-bar", "\\|\\|", "Separator for compound positional arguments", false),
|
||||
Ktype(katom_t::read_begin, "read-begin", at_s+read_s, "Beginning of the file input klammer", false),
|
||||
Ktype(katom_t::eval_begin, "eval-begin", at_s+eval_s, "Beginning of the evaluation klammer", false),
|
||||
Ktype(katom_t::cond_begin, "cond-begin", at_s+cond_s, "Beginning of the conditional (if/then/else) klammer", false),
|
||||
Ktype(katom_t::apply_begin, "apply-begin", at_s + definition_name, "Beginning of a klammer call"),
|
||||
Ktype(katom_t::apply_end, "apply-end", "(" + definition_name + ")?" + at_s, "End of a klammer call"),
|
||||
Ktype(katom_t::option_name, "option-name", ":" + definition_name, "Optional argument name"),
|
||||
Ktype(katom_t::define_begin, "define-begin", at2_s + definition_begin_name, "Beginning of a klammer definition"),
|
||||
Ktype(katom_t::define_end, "define-end", "(" + definition_name + ")?" + at2_s, "End of a klammer definition"),
|
||||
Ktype(katom_t::klammer_default, "klammer-default", "::::", "Define klammer default value for possible override"),
|
||||
Ktype(katom_t::klammer_override, "klammer-override", ":::", "Override existing klammer definition"),
|
||||
Ktype(katom_t::klammer_definition, "klammer-definition", ":", "Klammer definition, including parameters"),
|
||||
Ktype(katom_t::klammer_instance, "klammer-instance", "::", "Klammer definition using previously defined parameters"),
|
||||
Ktype(katom_t::space, "space", " ", false),
|
||||
Ktype(katom_t::word, "word", "[a-zA-Z]+", false),
|
||||
Ktype(katom_t::newline, "newline", "\n", false),
|
||||
Ktype(katom_t::bar, "bar", "\\|", false),
|
||||
Ktype(katom_t::double_bar, "double-bar", "\\|\\|", false),
|
||||
Ktype(katom_t::read_begin, "read-begin", at_s+read_s, false),
|
||||
Ktype(katom_t::eval_begin, "eval-begin", at_s+eval_s, false),
|
||||
Ktype(katom_t::cond_begin, "cond-begin", at_s+cond_s, false),
|
||||
Ktype(katom_t::apply_begin, "apply-begin", at_s + definition_name),
|
||||
Ktype(katom_t::apply_end, "apply-end", "(" + definition_name + ")?" + at_s),
|
||||
Ktype(katom_t::option_name, "option-name", ":" + definition_name),
|
||||
Ktype(katom_t::define_begin, "define-begin", at2_s + definition_begin_name),
|
||||
Ktype(katom_t::define_end, "define-end", "(" + definition_name + ")?" + at2_s),
|
||||
Ktype(katom_t::klammer_default, "klammer-default", "::::"),
|
||||
Ktype(katom_t::klammer_override, "klammer-override", ":::"),
|
||||
Ktype(katom_t::klammer_definition, "klammer-definition", ":"),
|
||||
Ktype(katom_t::klammer_instance, "klammer-instance", "::"),
|
||||
|
||||
Ktype(katom_t::karg, "klammer-arg", "\\*" + definition_name +"\\*", "Klammer argument in klammer body definition"),
|
||||
Ktype(katom_t::machine_begin, "machine-begin", at3_s + definition_name, "Beginning of a processor modification definition"),
|
||||
Ktype(katom_t::machine_end, "machine-end", "(" + definition_name + ")?" + at3_s, "End of a processor modification definition"),
|
||||
Ktype(katom_t::ignore_begin, "ignore-begin", ignore_begin_s, "Beginning of text to remove"),
|
||||
Ktype(katom_t::ignore_end, "ignore-end", ignore_end_s, "End of text to remove"),
|
||||
Ktype(katom_t::ignore_rest, "ignore-rest", ignore_rest_s, "Remove all text to the end of file or string"),
|
||||
Ktype(katom_t::ignore_line, "ignore-line", ignore_line_s, "Remove all text to the first newline, inclusive"),
|
||||
Ktype(katom_t::ws_remove, "ws-remove", ws_remove_s, "Remove all whitespace at this point"),
|
||||
Ktype(katom_t::ws_space, "ws-space", ws_space_s, "Remove all whitespace, leaving <number> spaces (default: 1)"),
|
||||
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::karg, "klammer-arg", "\\*" + definition_name +"\\*"),
|
||||
Ktype(katom_t::machine_begin, "machine-begin", at3_s + definition_name),
|
||||
Ktype(katom_t::machine_end, "machine-end", "(" + definition_name + ")?" + at3_s),
|
||||
Ktype(katom_t::ignore_begin, "ignore-begin", ignore_begin_s),
|
||||
Ktype(katom_t::ignore_end, "ignore-end", ignore_end_s),
|
||||
Ktype(katom_t::ignore_rest, "ignore-rest", ignore_rest_s),
|
||||
Ktype(katom_t::ignore_line, "ignore-line", ignore_line_s),
|
||||
Ktype(katom_t::ws_remove, "ws-remove", ws_remove_s),
|
||||
Ktype(katom_t::ws_space, "ws-space", ws_space_s),
|
||||
Ktype(katom_t::ws_newline, "ws-newline", ws_newline_s),
|
||||
Ktype(katom_t::special, "special-char", R"(\^[@|#^:*])"),
|
||||
//Ktype(katom_t::nonascii, "non-ascii-char", R"(\^\w(?:.|\^))", "Non-ASCII character"),
|
||||
// The second alternative is the "^...^" delimited form: a code point
|
||||
// (^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
|
||||
// recognize the shape. A shape that resolves to no character at all is
|
||||
// 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::text, "text",
|
||||
|
||||
Ktype(katom_t::literal_begin, "literal-begin", R"(\^')", "Begin unprocessed text"),
|
||||
Ktype(katom_t::literal_end, "literal-end", R"('\^)", "End unprocessed text"),
|
||||
Ktype(katom_t::text, "text", R"([^^@#|]+)", "No special characters, whitespace, or \":\" at the beginning"), // spaces, @, #, |, or ^,
|
||||
Ktype(katom_t::literal_begin, "literal-begin", R"(\^')"),
|
||||
Ktype(katom_t::literal_end, "literal-end", R"('\^)"),
|
||||
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::literal, "literal", "", "Literal katom (changed from its original type by ^'...'^)"),
|
||||
Ktype(katom_t::eval_result, "eval-result", "", "Katom produced by @eval klammer"),
|
||||
Ktype(katom_t::ws_added, "ws-added", "<space> or \\n"),
|
||||
Ktype(katom_t::literal, "literal", ""),
|
||||
Ktype(katom_t::eval_result, "eval-result", ""),
|
||||
|
||||
Ktype(katom_t::replaced, "replaced", "", "A katom replaced by definitions, applications, or file input"),
|
||||
Ktype(katom_t::ignored, "ignored", "", "A katom ignored by the action of a \"#\" katom"),
|
||||
Ktype(katom_t::replaced, "replaced", ""),
|
||||
Ktype(katom_t::ignored, "ignored", ""),
|
||||
// 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
|
||||
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_printable(katom_t type);
|
||||
|
||||
void describe_katoms(bool show_regex = false);
|
||||
|
||||
bool is_deftype(katom_t type);
|
||||
|
||||
const Ktype& type_to_katom_instance(katom_t type);
|
||||
std::string type_to_name(katom_t type);
|
||||
+11
-2
@@ -1,3 +1,4 @@
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include "machine.h"
|
||||
@@ -402,11 +403,19 @@ void Machine::mark_literal_klammer_content(katom_list& katoms)
|
||||
// Find where literal content starts.
|
||||
// Skip bars_before_literal bars (separating preceding positional args).
|
||||
// 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;
|
||||
bool has_options = false;
|
||||
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;
|
||||
}
|
||||
if (j->m_type == katom_t::bar) {
|
||||
|
||||
@@ -36,3 +36,15 @@
|
||||
|
||||
@@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 @ @@
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "error.h"
|
||||
#include "show.h"
|
||||
#include "font_store.h"
|
||||
#include "ktype.h"
|
||||
|
||||
// extern "C" is needed for dlsym name lookup. Returning std::string from
|
||||
// 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";
|
||||
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
@@ -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.
|
||||
@@
|
||||
|
||||
@@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
|
||||
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
|
||||
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
|
||||
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@
|
||||
@@ -44,4 +45,4 @@ quietly different listing.
|
||||
|
||||
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) @ @@
|
||||
+73
-7
@@ -5,11 +5,13 @@
|
||||
|
||||
import re
|
||||
import klammer_base
|
||||
import kutil
|
||||
import html_util
|
||||
import txt_util
|
||||
import phases
|
||||
from html_util import E
|
||||
import latex_util as L
|
||||
|
||||
|
||||
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.
|
||||
@@ -119,7 +121,13 @@ def tex_line(line):
|
||||
a monospace font, which LaTeX will not collapse -- so the code's
|
||||
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):
|
||||
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
|
||||
# in the same position container as the listing and moves with it.
|
||||
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(
|
||||
listing, "Listing", self.number, self.caption,
|
||||
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()
|
||||
|
||||
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):
|
||||
def __init__(self, K):
|
||||
@@ -356,10 +416,13 @@ class Code_fragment(klammer_base.Klammer_base):
|
||||
# without a beginning) and the typographically active characters
|
||||
# hidden, so a "--" in code stays two hyphens.
|
||||
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
|
||||
# context, though.
|
||||
result = "\u2039" + result + "\u203A"
|
||||
|
||||
# If *nd* ("no delimiters) is false (the default), surround code symbols
|
||||
# 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"
|
||||
return result
|
||||
|
||||
def extract_marked_region(src, marker, filename):
|
||||
@@ -424,6 +487,9 @@ class Source(Code):
|
||||
# not exist until the file has been read. File content carries no
|
||||
# whitespace markers anyway, since those come from the katomizer.
|
||||
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:
|
||||
with open(self.filename) as fp:
|
||||
text = fp.read()
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
.code_block {
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
--serif: Libre Baskerville, serif;
|
||||
--sans-serif: Open Sans, sans-serif;
|
||||
--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 {
|
||||
@@ -32,7 +40,7 @@ h1, h2, h3, h4, h5 {
|
||||
place); this file assigns only the family. */
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
line-height: var(--text_line_height);
|
||||
}
|
||||
|
||||
.plain {
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ other structures. The ^:offset is used only if the ^:hpos value is "left" or
|
||||
:number.bool true
|
||||
:caption_side.side
|
||||
:caption_style.style_code i
|
||||
:caption_size.float .9
|
||||
:caption_size.float 1
|
||||
: Arguments that define a caption for a block element.
|
||||
@@
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# the front and back matter of a book):
|
||||
|
||||
@@section_args.o :id :n.int -1 :
|
||||
Common arguments for section headings. If *n* is not the default, it is used
|
||||
as the section number.
|
||||
Common arguments for section headings. If *n* is not the default,
|
||||
it is used as the section number.
|
||||
@@
|
||||
|
||||
@@preface.k :id :name Preface : Preface text @@
|
||||
|
||||
@@ -58,8 +58,21 @@ td {
|
||||
--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 {
|
||||
border-top: var(--cell-border);
|
||||
padding-top: calc(var(--rulesep, 1) * 0.325ex);
|
||||
}
|
||||
|
||||
.Br {
|
||||
@@ -68,6 +81,7 @@ td {
|
||||
|
||||
.Bb {
|
||||
border-bottom: var(--cell-border);
|
||||
padding-bottom: calc(var(--rulesep, 1) * 0.725ex);
|
||||
}
|
||||
|
||||
.Bl {
|
||||
|
||||
+11
-1
@@ -213,7 +213,8 @@
|
||||
:calc.table_calc
|
||||
:format.table_format
|
||||
:decimal.decimal_mark
|
||||
:leading.float 1.3
|
||||
:leading.float 1.0
|
||||
:rulesep.float 1
|
||||
:colsep 4pt
|
||||
:
|
||||
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
|
||||
formats them, and ^:hpos and ^:offset place the whole table in the text
|
||||
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@ @@
|
||||
|
||||
+77
-29
@@ -575,23 +575,39 @@ class Table(klammer_base.Klammer_base):
|
||||
# would wrap it as a stray <p> inside the table.
|
||||
return E("colgroup").body(cols.strip(), newline=False).str(), layout, table_width
|
||||
|
||||
def html_style(self):
|
||||
# :leading and :colsep under html (2026-09-18; both were tex-only).
|
||||
# \arraystretch multiplies the row strut, which is \baselineskip,
|
||||
# 1.2 times the font size -- so :leading L is a 1.2 L em row, and
|
||||
# the html equivalent is that line-height on the table, inherited
|
||||
# by every cell. The cells' vertical padding went with it
|
||||
# (table.css): an html-only extra with no tex counterpart, which
|
||||
# kept a :leading 1 table 0.3rem looser per row than its pdf.
|
||||
# :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. Measured 2026-09-18: rows of :leading 1 and
|
||||
# :leading 2 in the ratio 1:2 in both targets.
|
||||
line_height = f"{1.2 * float(self.leading):.4g}"
|
||||
def html_style(self, layout=""):
|
||||
# The WHOLE style attribute of the <table>, written once: "layout" is
|
||||
# the width declarations the caller has worked out (max-width for a
|
||||
# fill table, table-layout/width for fractions). E.attr() appends,
|
||||
# so a second .attr("style", ...) made a second attribute, which a
|
||||
# browser discards -- from 2026-09-18, when this function arrived,
|
||||
# to 2026-09-20 every width declaration was lost that way.
|
||||
# :leading and :colsep under html. ":leading L" is L times the pitch
|
||||
# of a line of the surrounding TEXT, in every target. tex has that
|
||||
# by construction: \arraystretch multiplies the row strut, which is
|
||||
# the \baselineskip in force. The html counterpart of \baselineskip
|
||||
# is --text_line_height (font.css, 1.5), so the table's line-height
|
||||
# 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
|
||||
# would turn the property's "--" into an en dash (font.py's trap).
|
||||
return klammer_base.hide_typographic(
|
||||
f"line-height: {line_height}; --colsep: {self.colsep}")
|
||||
# :rulesep is a custom property too: the .Bt/.Bb rules (table.css)
|
||||
# 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):
|
||||
result = ""
|
||||
@@ -610,8 +626,10 @@ class Table(klammer_base.Klammer_base):
|
||||
# algorithm), matching the tex \ratio division. The max-width
|
||||
# cap goes on the caption wrapper when there is one (the table's
|
||||
# own percentage would be circular in a shrink-to-fit wrapper).
|
||||
result = E("table").body(result).attr("style", self.html_style())
|
||||
if self.number or self.caption:
|
||||
captioned = 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, "Table", self.number, self.caption,
|
||||
self.caption_style, hpos=self.hpos,
|
||||
@@ -619,17 +637,19 @@ class Table(klammer_base.Klammer_base):
|
||||
font_size=self.caption_size, max_width="100%",
|
||||
offset=self.offset)
|
||||
else:
|
||||
result.attr("style", "max-width: 100%")
|
||||
result = html_util.hpos_container(result, self.hpos, self.offset).str()
|
||||
return result
|
||||
colgroup, layout, table_width = self.html_colgroup()
|
||||
result = E("table").body(colgroup + result).attr("style", self.html_style())
|
||||
if self.number or self.caption:
|
||||
captioned = 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
|
||||
# shrink-to-fit wrapper itself would be circular) and 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, "Table", self.number, self.caption, self.caption_style,
|
||||
hpos=self.hpos, side=self.caption_side,
|
||||
@@ -638,8 +658,6 @@ class Table(klammer_base.Klammer_base):
|
||||
else:
|
||||
# An uncaptioned table still gets the position container, so
|
||||
# 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()
|
||||
return result
|
||||
|
||||
@@ -761,9 +779,32 @@ class Table(klammer_base.Klammer_base):
|
||||
# print("tex_column_spec:", "".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):
|
||||
# Contiguous cell borders coalesce into single \cline runs; a
|
||||
# full-width line becomes \hline.
|
||||
# Contiguous cell borders coalesce into single rule runs. The rules
|
||||
# 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
|
||||
if bottom:
|
||||
index -= 1
|
||||
@@ -773,7 +814,11 @@ class Table(klammer_base.Klammer_base):
|
||||
# No line through the interior of a merged (rowspan) cell.
|
||||
flags = [flag and (index, col_i) not in self.rowspan_covered
|
||||
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 spaced:
|
||||
return f"\\specialrule{{\\arrayrulewidth}}{{{above}}}{{{below}}}\n"
|
||||
return "\\hline\n"
|
||||
hline = ""
|
||||
start = None
|
||||
@@ -781,7 +826,10 @@ class Table(klammer_base.Klammer_base):
|
||||
if flag and start is None:
|
||||
start = i
|
||||
elif not flag and start is not None:
|
||||
hline += f"\\cline{{{start + 1}-{i}}} "
|
||||
if spaced:
|
||||
hline += f"\\cmidrule[\\arrayrulewidth]{{{start + 1}-{i}}} "
|
||||
else:
|
||||
hline += f"\\cline{{{start + 1}-{i}}} "
|
||||
start = None
|
||||
return hline.strip() + "\n"
|
||||
|
||||
@@ -1063,7 +1111,7 @@ class Table(klammer_base.Klammer_base):
|
||||
if len(rows) in self.s_hline.by_index:
|
||||
result += rule
|
||||
result = txt_util.add_caption(
|
||||
result, "Table", self.number, self.caption, self.caption_side, \
|
||||
int(self.Target_txt_indent))
|
||||
result, "Table", self.number, self.caption, self.caption_side,
|
||||
len(indent) // len(txt_util.NBSP))
|
||||
return result
|
||||
|
||||
@@ -103,6 +103,18 @@
|
||||
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 {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -238,7 +238,12 @@ def hpos_container(element, hpos, offset=None):
|
||||
|
||||
def add_caption(element, caption_label, number, caption_text,
|
||||
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)
|
||||
# Caption
|
||||
caption = ""
|
||||
@@ -276,6 +281,8 @@ def add_caption(element, caption_label, number, caption_text,
|
||||
#print("-"*80)
|
||||
|
||||
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:
|
||||
# The element's width (e.g. a table's :column_width total) lives
|
||||
# on this wrapper: the element fills it, and the caption tracks
|
||||
|
||||
+69
-20
@@ -147,6 +147,36 @@ def tex_nl_marker():
|
||||
def tex_resolve_markers(text, K):
|
||||
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):
|
||||
delim = '__DIVIDE__'
|
||||
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
|
||||
# branch that had nothing to do also did not copy the text through.
|
||||
# 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()
|
||||
if par and not txt_is_laid_out(par):
|
||||
label = re.match("(?:" + re.escape(nbsp) + r")*\[\d+\] ", par)
|
||||
par = "\n".join(txt_wrap(
|
||||
par, int(K.Target_txt_width),
|
||||
subsequent_indent=" " * len(label.group(0)) if label else ""))
|
||||
result += par + "\n\n"
|
||||
filled = []
|
||||
for laid_out, run in txt_runs(par):
|
||||
if run and not laid_out:
|
||||
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 ""))
|
||||
filled.append(run)
|
||||
result += "\n".join(filled) + "\n\n"
|
||||
result = re.sub(" *" + txt_vspace_marker() + " *", "\n", result)
|
||||
result = re.sub(r"\n? *" + txt_nl_marker() + r" *\n?", "\n", result)
|
||||
result = result.replace(txt_verbatim_marker(), "")
|
||||
@@ -192,33 +229,45 @@ def txt_justify_blocks(text, K):
|
||||
return result
|
||||
|
||||
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()
|
||||
caption_rgx = re.compile(f"{mark}(.*?){mark}(.*?){mark}", re.S)
|
||||
caption_number = 0
|
||||
nbsp = txt_nbsp_marker()
|
||||
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)
|
||||
indent = int(K.Target_txt_indent)
|
||||
indented_width = width - indent
|
||||
def replace(m):
|
||||
nonlocal caption_number
|
||||
name, text = m.groups()
|
||||
indent_run, 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
|
||||
# must NOT take a number from the sequence: a reader counting the
|
||||
# captions they can see would then find the next one a number
|
||||
# ahead. The same is true of html and tex, where an unnumbered
|
||||
# caption emits no marker and so never reaches their counter.
|
||||
if name:
|
||||
caption_number += 1
|
||||
caption = f"{name} {caption_number} - {text}"
|
||||
caption_numbers[name] = caption_numbers.get(name, 0) + 1
|
||||
caption = f"{name} {caption_numbers[name]} - {text}"
|
||||
else:
|
||||
caption = text
|
||||
# Visible widths throughout: a caption may carry style markers.
|
||||
lines = [caption]
|
||||
if txt_visible_length(caption) > indented_width:
|
||||
caption = "\n".join(txt_wrap(
|
||||
caption, indented_width,
|
||||
subsequent_indent=txt_nbsp_marker()*indent))
|
||||
line_length = max([txt_visible_length(e) for e in caption.split("\n")])
|
||||
line = txt_nbsp_marker() * indent + "-" * line_length
|
||||
return caption + "\n" + line
|
||||
lines = txt_wrap(caption, indented_width)
|
||||
line_length = max(txt_visible_length(e) for e in lines)
|
||||
lines.append("-" * line_length)
|
||||
lead = txt_verbatim_marker() + indent_run
|
||||
# The first line's marker is already in the text, before the indent
|
||||
# run this match begins with (add_caption); the others get theirs.
|
||||
return indent_run + ("\n" + lead).join(lines)
|
||||
result = caption_rgx.sub(replace, text)
|
||||
return result
|
||||
|
||||
|
||||
@@ -101,7 +101,11 @@ def add_caption(element, caption_label, number, caption_text,
|
||||
caption = kutil.caption_marker("", caption_text, None)
|
||||
else:
|
||||
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"}:
|
||||
return caption + "\n" + element
|
||||
return element + "\n" + caption
|
||||
@@ -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
|
||||
@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"
|
||||
|
||||
echo
|
||||
|
||||
@@ -107,6 +107,17 @@ echo
|
||||
echo "-- result detail is a word, not a verbosity level --"
|
||||
shows " 9. --katoms full adds the regex column" 'Regex' "$KDESC" --katoms full
|
||||
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
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user