The ansi target, txt table styles, kdesc reference from ref/, typeface and table-width fixes (from dev fcec5e2e77ac)
Since the previous release: Machine::read_string() (the fs::path/std::string overload pair split); kdesc -c and -u rendered from the ref/ reflection klammerset; a fit column's tex width measured in TeX over every entry; @typeface resolving to the family its css declares (a store directory name now works in html) and hieroglyphs in @unicode_examples set in their typeface; the ansi target -- plain text with the type styles a terminal can show, identical to txt once the escape sequences are stripped -- with the txt @table carrying :header_style, :style and :hline; @block anchoring, @table spacing and the Image_quality state variable; and the rewritten doc/klammertext_for_llm.md.
This commit is contained in:
1 parent
760fda5470
commit
9b0d0d7897
34 files changed
+894
-393
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 `ed13dfaa5b7a`.
|
||||
This snapshot was assembled from development commit `fcec5e2e77ac`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+37
-6
@@ -147,6 +147,36 @@ static void font_command(const strings_t& words)
|
||||
}
|
||||
}
|
||||
|
||||
// The target for the Klammertext kdesc renders for its own output: the
|
||||
// ansi target (plain text with type styles as escape sequences) when
|
||||
// stdout is a terminal, the txt target otherwise. The COMMAND decides,
|
||||
// because only it knows the stream -- the same per-stream rule Color
|
||||
// follows for the listings, NO_COLOR included -- and the targets stay
|
||||
// formats a writer names with -t.
|
||||
static std::string text_target()
|
||||
{
|
||||
return color_enabled(std::cout) ? "ansi" : "txt";
|
||||
}
|
||||
|
||||
void show_special_characters()
|
||||
{
|
||||
Machine M;
|
||||
M.read(M.m_state.value("KLAMMERTEXT_HOME") + "/ref/ref.k");
|
||||
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@");
|
||||
M.read_string("@pinyin_examples@");
|
||||
std::cout << "\n" << M.apply(text_target()) << "\n";
|
||||
}
|
||||
|
||||
void show_unicode_examples()
|
||||
{
|
||||
Machine M;
|
||||
M.read(M.m_state.value("KLAMMERTEXT_HOME") + "/ref/ref.k");
|
||||
M.read_string("@unicode_examples@");
|
||||
std::cout << "\n" << M.apply(text_target()) << "\n\n";
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
try {
|
||||
@@ -189,13 +219,9 @@ int main(int argc, char* argv[])
|
||||
args.describe();
|
||||
}
|
||||
|
||||
if (p("c")) {
|
||||
show_special_characters();
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
if (p("u")) {
|
||||
unicode_examples();
|
||||
show_unicode_examples();
|
||||
}
|
||||
|
||||
if (p("argtypes")) {
|
||||
@@ -292,12 +318,17 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
strings_t own_klammers_only {};
|
||||
if (!input_filenames.empty()) {
|
||||
own_klammers_only = context_files;
|
||||
}
|
||||
|
||||
|
||||
if (p("c")) {
|
||||
show_special_characters();
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
if (p("t")) {
|
||||
std::cout << boldblack << "Targets\n" << black
|
||||
<< M.m_targets.describe(2, true, own_klammers_only);
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ int main(int argc, char* argv[])
|
||||
load_klammersets(M, word_split(args.as_string("klammersets")));
|
||||
|
||||
if (!input_text.empty()) {
|
||||
M.read(input_text + "\n");
|
||||
M.read_string(input_text + "\n");
|
||||
}
|
||||
for (auto p : input_filenames) {
|
||||
M.read(fs::path(p));
|
||||
|
||||
+333
-96
@@ -8,18 +8,23 @@ wanting to learn Klammertext should read the other documents in `doc/`.
|
||||
has not seen this language will pattern-match it to LaTeX, Markdown, or Lisp and
|
||||
produce confidently wrong syntax.
|
||||
|
||||
The klammers named here are the **Standard Klammer Set**, which is what loads by
|
||||
default. A klammer set is user-extensible, so the authority for what exists in
|
||||
any particular setup is `kdesc`, not this file — see §1.
|
||||
The klammers named here are the **Standard Klammer Set** (SKS), which is what
|
||||
loads by default. A klammer set is user-extensible, so the authority for what
|
||||
exists in any particular setup is `kdesc`, not this file — see §1. Where this
|
||||
file and `kdesc` disagree, `kdesc` is right and this file is stale.
|
||||
|
||||
Vocabulary: text is **katomized** into **katoms** (the indivisible input units)
|
||||
by **katom rewrite rules**; a **klammer** is the function-like operator; a
|
||||
**target** is an output format; a **klammerset** is a named group of klammer
|
||||
definitions. The three user modes are **author** (`@`), **designer** (`@@`),
|
||||
**programmer** (`@@@`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Do not guess — the machine will tell you
|
||||
|
||||
Klammertext is introspectable. `kdesc` **prints the live state of the machine's
|
||||
data structures in digested form**: what klammers exist, what arguments each
|
||||
takes, what the argument types accept, what targets exist. Query it instead of
|
||||
inferring from examples.
|
||||
data structures in digested form**. Query it instead of inferring from examples.
|
||||
|
||||
| ask | command |
|
||||
|---|---|
|
||||
@@ -29,12 +34,16 @@ inferring from examples.
|
||||
| argument types and what each accepts | `kdesc --argtypes` |
|
||||
| shared option groups and who uses them | `kdesc --optionsets` |
|
||||
| which targets each klammer can render to | `kdesc --coverage` |
|
||||
| what a user's own file defines | `kdesc -i FILE -k` |
|
||||
| the usage of any command | run it with no arguments |
|
||||
| what a user's own file defines (against the SKS) | `kdesc -i FILE -k` |
|
||||
| the klammersets available, with provenance | `kdesc --klammersets` |
|
||||
| how to write accented / non-English characters | `kdesc -c` |
|
||||
| how to write any Unicode character, with examples | `kdesc -u` |
|
||||
| installed typefaces; install or preview fonts | `kdesc --typeface`, `--typeface help` |
|
||||
| the usage of any command | run it with no arguments (exit 0) |
|
||||
|
||||
`kdesc -k <name>` before using an unfamiliar klammer costs one command and
|
||||
replaces a guess. The parameter list it prints is authoritative; this file is
|
||||
not.
|
||||
replaces a guess. The parameter list it prints is authoritative: a parameter
|
||||
shows as `name.type default`, an option as `:name.type default`.
|
||||
|
||||
**Check a document without rendering it:**
|
||||
|
||||
@@ -43,13 +52,15 @@ kdiag --process --check doc.kt
|
||||
```
|
||||
|
||||
reports every klammer application whose name, argument count, option names, or
|
||||
target coverage is wrong — including inside branches this render will not take.
|
||||
It applies nothing, so it is safe and fast. Use it before claiming a document is
|
||||
correct.
|
||||
target coverage is wrong — including inside `@cond` branches this render will
|
||||
not take and inside klammer bodies never applied. It applies nothing. Use it
|
||||
before claiming a document is correct.
|
||||
|
||||
**Inspect how text is being parsed:** `kdiag --type '<text>'` shows the katoms
|
||||
("katoms") and their types. This settles arguments about what a piece of syntax
|
||||
actually means.
|
||||
**Inspect how text is parsed:** `kdiag --type '<text>'` shows the katoms and
|
||||
their types; `kdiag --args '<text>'` shows how text splits into arguments;
|
||||
`kdiag --rewrite '<text>'` names the rewrite rule that fired. This settles
|
||||
arguments about what a piece of syntax means. `kdiag` deliberately loads no
|
||||
klammerset; give it `@read sks/sks.k @` in the input if it needs one.
|
||||
|
||||
---
|
||||
|
||||
@@ -57,15 +68,21 @@ actually means.
|
||||
|
||||
| assumption | reality |
|
||||
|---|---|
|
||||
| It is a macro language like TeX | No catcodes. Katom structure — the indivisible input units, which `kdiag` displays — is fixed when a file is read and cannot be changed by anything later. This is why static checking works. |
|
||||
| It is a macro language like TeX | No catcodes. Katom structure is fixed when a file is read and cannot be changed by anything later. This is why static checking works. |
|
||||
| `\command{arg}` | `@name arg @` — an `@` opens, and a **closing delimiter is required**. |
|
||||
| Arguments are `{a}{b}` | Positional arguments are separated by `|` — `@sup 2 \| 3 @`. |
|
||||
| Arguments are `{a}{b}` | Positional arguments are separated by `\|` — `@sup 2 \| 3 @`. |
|
||||
| Options are `[key=value]` | `:name value`, anywhere in the argument list — `@link URL :text Click @`. |
|
||||
| `%` starts a comment | `#` removes the rest of the line. `##` removes the rest of the file. `#[ … ]#` removes a block. |
|
||||
| `%` starts a comment | `#` removes the rest of the line. `##` removes the rest of the file. `#[ … ]#` removes a block (nestable). |
|
||||
| `*x*` is emphasis (Markdown) | `@i x @` is emphasis. `*name*` is **variable substitution** inside a klammer body; in prose write `^*` for a literal asterisk. |
|
||||
| `\\` escapes | `^` quotes: `^@ ^\| ^# ^^ ^: ^*` and **`^` before any ASCII punctuation** gives that character literally. |
|
||||
| `&`, `%`, `_`, `<`, `\` must be escaped for LaTeX/HTML | **Never.** The machine escapes the target's characters. An author quotes only Klammertext's own specials. Inside `@c`/`@code` nothing needs quoting. |
|
||||
| `--` is two hyphens | Under every target `--` is an en dash, `---` an em dash, `` `` `` / `''` curly double quotes, `` ` ``/`'` curly single quotes, `~` a non-breaking space (the LaTeX conventions). A literal `--flag` is `^-^-flag`; a literal tilde `^~`. Verbatim text (`@c`, `@code`, `^'…'^`) is exempt. |
|
||||
| Whitespace is insignificant | It is content, and it is preserved. Blank lines separate paragraphs. |
|
||||
| `\\` escapes | `^` quotes: `^@ ^\| ^# ^^ ^:` produce those characters literally. |
|
||||
| Text is nested in braces | Text lives between the opening and closing delimiter with no wrapper. |
|
||||
| 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. |
|
||||
|
||||
---
|
||||
|
||||
@@ -81,17 +98,15 @@ Three prefix tiers, distinguished by how many `@`:
|
||||
|
||||
**Closing a klammer.** The close is `@` for the application tier, `@@` for
|
||||
definitions, `@@@` for system commands. A close may be *named* by putting the
|
||||
klammer's name immediately before it: `@i text i@.
|
||||
klammer's name immediately before it: `@i text i@`.
|
||||
|
||||
> **When to name the close.** Use a bare `@` by default. Use the named form
|
||||
> `name@` when the klammer's body **contains other klammer calls**. The reason is
|
||||
> diagnostic, not stylistic: if delimiters are unbalanced, a named close lets the
|
||||
> machine say exactly which klammer was left open. With bare closes everywhere it
|
||||
> can only report that the count did not work out. Do not name closes on short,
|
||||
> klammer-free bodies — it is noise.
|
||||
> `name@` when the body **contains other klammer calls**: if delimiters are
|
||||
> unbalanced, a named close lets the machine say which klammer was left open.
|
||||
> Do not name closes on short, klammer-free bodies — it is noise.
|
||||
|
||||
```
|
||||
@i just text i@ ← unnecessary; @i just text @ is better
|
||||
@i just text @ ← bare close
|
||||
@s1 A @c fragment c@ heading s1@ ← name it: the body contains a klammer call
|
||||
```
|
||||
|
||||
@@ -103,48 +118,59 @@ klammer's name immediately before it: `@i text i@.
|
||||
|
||||
- `|` separates positional arguments.
|
||||
- `:name value` is an optional argument; it may appear anywhere in the list.
|
||||
The value runs to the next `|` or `:option`.
|
||||
- `:name` written alone takes the argument type's "alone" value — for a boolean
|
||||
that is `true`, so `:number` means `:number true`.
|
||||
- An optional argument that is not written takes its default.
|
||||
that is `true`, so `:number` means `:number true`; `@table :hline` means
|
||||
`:hline all`.
|
||||
- An optional argument that is not written takes its default (kdesc shows it).
|
||||
|
||||
**Abbreviated form.** `@name-arg1-arg2` is `@name arg1 | arg2 @`, valid only
|
||||
while every argument is letters and digits: `@i-italic`, `@sup-2-3`, `@date@`.
|
||||
|
||||
**Literal klammers** take their content uninterpreted. It is not examined for
|
||||
`@`, `|`, `#`, or `^`, so the machine cannot find the end by counting and **the
|
||||
named close is required**:
|
||||
`@`, `|`, `#`, `^` or `*`, so the machine cannot find the end by counting and
|
||||
**the named close is required**:
|
||||
|
||||
```
|
||||
@code
|
||||
int x = a[0]; // # and @ are safe in here
|
||||
code@
|
||||
```
|
||||
|
||||
```
|
||||
@c a | b c@ ← inline: same rule, and the close must be "c@"
|
||||
```
|
||||
|
||||
`@c` and `@code` are the in-line and block forms of the same thing, and both are
|
||||
literal: **nothing inside either is interpreted**, so `#`, `@`, `|`, `^` and `*`
|
||||
all pass through as written and none of them needs quoting.
|
||||
|
||||
Code that lives in a **file** is `@source_listing`, never `@code` — `@code` is
|
||||
only for a block written out inline, and has no filename argument:
|
||||
`@c` and `@code` are the in-line and block forms of the same thing. Code that
|
||||
lives in a **file** is `@source_listing`, never `@code` (which has no filename
|
||||
argument):
|
||||
|
||||
```
|
||||
@source_listing src/derivations.cpp @ ← the whole file
|
||||
@source_listing src/derivations.cpp :marker fromRdd55 @ ← one region of it
|
||||
@source_listing src/derivations.cpp @ ← the whole file
|
||||
@source_listing src/derivations.cpp :marker fromRdd55 @ ← one region of it
|
||||
```
|
||||
|
||||
`:marker P` lists what lies between two lines that are exactly `//P`, each
|
||||
beginning in the first column. The source file thus declares its own extractable
|
||||
regions and the document asks for one by name, so editing the code cannot
|
||||
silently change what the document shows. A marker that is missing, or that does
|
||||
not appear exactly twice, is an **error** — never a silent fallback to the whole
|
||||
file.
|
||||
`:marker P` lists what lies between two lines that are exactly `//P` in column
|
||||
one. A marker missing or not appearing exactly twice is an **error**, never a
|
||||
fallback. `kdesc -k <name>` shows a literal parameter as `text.literal`.
|
||||
|
||||
`kdesc -k <name>` is how you tell: a parameter shown as `text.literal` is a
|
||||
literal parameter. Do not infer it from a klammer's purpose — check.
|
||||
**Whitespace and removal (`#`).** `#` rest of line, `##` rest of file, `#[ … ]#`
|
||||
block; `#-` removes surrounding whitespace, `#+N` inserts N spaces, `#/N` inserts
|
||||
N newlines (default 1).
|
||||
|
||||
**Characters (`^`).** One mechanism, four readings, in this order:
|
||||
|
||||
| form | gives | example |
|
||||
|---|---|---|
|
||||
| `^P` (P any ASCII punctuation) | that character literally | `^-^-flag`, `^~`, `^^`, `^@` |
|
||||
| `^'…'^` | a **literal span**: raw target markup passed through untouched | `^'$\cdot$'^` |
|
||||
| `^UUUU^` (4–6 hex digits) | the code point | `^0418^` И, `^263A^` ☺, `^13000^` 𓀀 |
|
||||
| `^UUUU UUUU …^` | a run of code points, one caret pair | `^9053 53EF 9053^` 道可道 |
|
||||
| `^` + base letter + mark | a diacritic | `^e'` é, `^u"` ü, `^n~` ñ, `^cc` ç, `^oh` ô |
|
||||
| `^xx^` mnemonic | an extended-Latin letter | `^s^` ß, `^ae^` æ, `^o^` ø |
|
||||
|
||||
`kdesc -c` prints the diacritic, extended-Latin and pinyin tables; `kdesc -u`
|
||||
the code-point notation with examples. The one `^` exception: `^'` opens a
|
||||
literal span, so a literal apostrophe is `^0027^`. A caret form that names
|
||||
nothing is an error, not silence.
|
||||
|
||||
---
|
||||
|
||||
@@ -156,15 +182,17 @@ literal parameter. Do not infer it from a klammer's purpose — check.
|
||||
:subtitle Predictability and provenance
|
||||
:author Joseph Goldstone
|
||||
:date @date@
|
||||
:structure article
|
||||
:point_size 12
|
||||
:text
|
||||
|
||||
@s1 Principles s1@
|
||||
|
||||
Ordinary paragraph text. A blank line starts a new paragraph.
|
||||
Use @i emphasis i@ and @b bold @ inline, and @c code_fragment c@ for code.
|
||||
Use @i emphasis @ and @b bold @ inline, and @c code_fragment c@ for code.
|
||||
A footnote @footnote Its text. @ is written inline.
|
||||
|
||||
@link https://openexr.com/ :text the OpenEXR site link@
|
||||
@link https://openexr.com/ :text the OpenEXR site @
|
||||
|
||||
@ul
|
||||
first item
|
||||
@@ -176,86 +204,295 @@ ul@
|
||||
|
||||
@image figure_name :width .95w :caption A caption @
|
||||
|
||||
@table :caption Sizes :column_width fit fill
|
||||
| Name | Description
|
||||
|| alpha | the first
|
||||
|| beta | the second
|
||||
table@
|
||||
|
||||
@
|
||||
```
|
||||
|
||||
Render it:
|
||||
A long document names its chapters instead of holding them:
|
||||
|
||||
```
|
||||
ktext doc.kt -t pdf # also: -t html, -t tex, -t txt
|
||||
ktext doc.kt -t html # writes a DIRECTORY doc/ containing index.html
|
||||
ktext -s '@i-x' -t html -d # -d displays instead of writing; -s takes a string
|
||||
@document :title The Book :structure book :files preface one two @
|
||||
```
|
||||
|
||||
Output goes **next to the input file** unless `-o` says otherwise.
|
||||
which reads `kt/preface.kt`, `kt/one.kt`, `kt/two.kt` beside the root file.
|
||||
|
||||
**Render it:**
|
||||
|
||||
```
|
||||
ktext doc.kt -t pdf # also: -t html, -t tex
|
||||
ktext doc.kt -t html # writes a DIRECTORY doc/ containing index.html (+css, fonts, js)
|
||||
ktext doc.kt -t txt -d # -d displays on stdout instead of writing -- but this
|
||||
# example errors under txt/ansi: @c and @image have no
|
||||
# txt definition (§5), and coverage is never silently degraded
|
||||
ktext -s '@i-x' -t html -d # -s takes a string; no file needed
|
||||
ktext doc.kt -s '@@version : 3.2.1 @@' -t pdf # -s is processed BEFORE the files:
|
||||
# define a klammer the document uses
|
||||
```
|
||||
|
||||
- Output goes **next to the input file** unless `-o` names a directory and/or
|
||||
basename. String-only input writes to the current directory.
|
||||
- `-s`, `-t`, `-o` take exactly one argument (quote spaces). `--klammersets`
|
||||
takes a list; end it with `--` if filenames follow.
|
||||
- Filenames may contain spaces: quote them on the command line; in a list
|
||||
(`:files`, several input files) separate names with a standalone ` / `.
|
||||
- `@document :structure` is `plain` (default; no TOC), `article` (in-page
|
||||
TOC), `book` (cover, navigation panel in html; chapters, recto/verso in
|
||||
pdf). Headings `@s1`… are `\chapter` in a book and `\section` otherwise.
|
||||
- Role typefaces: `:serif_typeface`, `:sans_typeface`, `:mono_typeface`
|
||||
(family name or store directory name, e.g. `EB Garamond` or `eb-garamond`);
|
||||
`:text_scale` scales all text; `:point_size`, `:leading`, `:paper_size`,
|
||||
`:landscape`, `:two_column`. `kdesc -k document` lists all forty.
|
||||
- **Outside a `@document`** nothing makes paragraphs: a fragment rendered on
|
||||
its own writes `@par text @` explicitly.
|
||||
|
||||
---
|
||||
|
||||
## 5. Klammers most used when authoring
|
||||
|
||||
Confirm parameters with `kdesc -k <name>` — this list is names and intent only.
|
||||
Confirm parameters with `kdesc -k <name>` — this is names and intent only.
|
||||
Not every klammer exists for every target (`kdesc --coverage`); txt and ansi
|
||||
are the limited ones.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| inline | `@i` italic, `@b` bold, `@t` typewriter, `@u` underline, `@c` inline code (literal; close with `c@`), `@sup`, `@sub` |
|
||||
| structure | `@s1`…`@s7` numbered headings, `@h1`…`@h7` unnumbered, `@part`, `@preface`, `@par` |
|
||||
| blocks | `@code` (a listing written inline), `@source_listing` (a listing read from a file), `@quote`, `@note`, `@indent`, `@block`, `@center`, `@right`, `@columns` |
|
||||
| lists | `@ul` unordered, `@ol` ordered, `@define` term/definition — items separated by `\|` |
|
||||
| tables | `@table` — cells by `\|`, rows by `\|\|`; `@tbl`, `@rowcolor` |
|
||||
| figures | `@image`, `@image_grid`, `@fig`, `@reference` |
|
||||
| other | `@link`, `@date`, `@datetime`, `@color`, `@font`, `@newpage`, `@vspace`, `@email` |
|
||||
| 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 |
|
||||
| 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` |
|
||||
| figures & references | `@image basename :width :caption`, `@image_grid`, `@fig id`, `@tbl id`, `@reference spec \| name @`, `@target id :text` |
|
||||
| footnotes & links | `@footnote text @`, `@link target :text :footnote`, `@email name \| domain @` |
|
||||
| dates | `@date`, `@datetime` (`:days N`, `:lang de`, `:number`), `@year`, `@timestamp` |
|
||||
| symbols | `@copyright`, `@registered`, `@trademark`, `@euro amount`, `@pi`, `@rightarrow`, `@dot`, `@sp` (space) |
|
||||
|
||||
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 37**, 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`, `@footnote @link`, `@date
|
||||
@datetime`. Not for txt: `@c @code @source_listing @image @image_grid @quote
|
||||
@note @center @block @columns @multicolumn @define @sup @sub @color @h1`–`@h7`.
|
||||
`@u` has no html definition. A klammer applied under a target that lacks it is
|
||||
an **error**, never silently dropped.
|
||||
|
||||
---
|
||||
|
||||
## 6. Defining a klammer (only if asked)
|
||||
## 6. Tables
|
||||
|
||||
```
|
||||
@@name.k arg1 | arg2 :opt default : Description of what it does @@
|
||||
@table :caption Quarterly :column_width fit fit fill :cell_hpos l r r :calc 3(1-2) = + 1-2(1-2)
|
||||
| Item | Q1 | Q2
|
||||
|| Widgets | 10.5 | 12
|
||||
|| Gadgets | 3 | 4.25
|
||||
|| Total | |
|
||||
table@
|
||||
```
|
||||
|
||||
Cell indices are **zero-origin and count the header row**: `3(1-2)` is the
|
||||
fourth row, second and third columns; `1-2(1-2)` the two body rows above it;
|
||||
`-1` is the last row or column. A ranged target repeats the calculation across
|
||||
the range (here: one sum per column).
|
||||
|
||||
- First row is the header (`:header false` otherwise); header style
|
||||
`:header_style i` by default; `:style r i b` per column.
|
||||
- `:column_width` per column: `fit` (widest entry), `fill` (the remaining
|
||||
width, capped at the widest entry), a fraction of the text width (`.2`), or
|
||||
`*` (the remainder). `fill` may not combine with a fraction or `*`.
|
||||
- `: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.
|
||||
- `:colspan`/`:rowspan` with the indexed-range syntax; `:allow_break true`
|
||||
makes a page-breaking table (pdf).
|
||||
- `:calc` — `;`-separated `<row>(<col>) = <op> <operands>`, prefix `+ - * /`
|
||||
folding left over the operands, which are cell ranges or numbers;
|
||||
**display-precision** (a printed total equals the sum of the printed lines);
|
||||
`notes/calc_notation.md` is the gallery. `:format <cells> module.function`
|
||||
formats cells with user Python. `:decimal comma` for comma decimals.
|
||||
- Placement: `:hpos left|center|right|none`, `:offset 2em` (inset from that
|
||||
margin); a caption goes **above** a table (`:caption_side top` default),
|
||||
below a figure. `:number false` for an unnumbered caption.
|
||||
|
||||
---
|
||||
|
||||
## 7. Targets
|
||||
|
||||
| target | output | notes |
|
||||
|---|---|---|
|
||||
| `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; **37 of 89 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
|
||||
(interface, option set, all targets), not outputs.
|
||||
|
||||
State variables an author may set (`@@@state Name :value V @@@` in the
|
||||
document, or `-s`): `Language` (`en`/`de`, for dates), `Image_quality` (JPEG
|
||||
quality of cached photographs, 85), `Target_txt_width`, `Target_txt_indent`,
|
||||
`Image_default_width`, `Image_search_path`.
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 8. Defining klammers (only if asked)
|
||||
|
||||
```
|
||||
@@name.k arg1 | arg2 :opt.type default : Description of what it does @@
|
||||
@@name.html :: <span>*arg1*</span> @@
|
||||
@@name.tex :: \textbf{*arg1*} @@
|
||||
@@name.txt :: *arg1* @@
|
||||
```
|
||||
|
||||
- `.k` declares the interface and documents it; per-target definitions inherit
|
||||
the parameter list with `::`.
|
||||
- `*arg*` substitutes an argument into the body.
|
||||
- A definition with no target suffix applies to every target.
|
||||
the parameter list with `::`. Every target's list must agree.
|
||||
- `*arg*` substitutes an argument into the body; `*Name*` also reads a state or
|
||||
environment variable.
|
||||
- Several targets may share a body: `@@name.html,tex :: … @@`. A definition
|
||||
with **no suffix** applies to every target (the writer's macro form; its body
|
||||
is escaped for the target, so `@@corp : Smith & Jones @@` is safe). `.*`
|
||||
written out is a *claim* of target independence.
|
||||
- Separators: `:` create, `::` create inheriting parameters, `:::` override an
|
||||
existing definition, `::::` define a default that a later `:` may replace.
|
||||
existing definition, `::::` define a default a later `:` may replace.
|
||||
- Argument types: `name.type` — `string` (default), `int`, `float`, `bool`,
|
||||
`list`, `length`, `literal` (raw content, named close required), `rest`
|
||||
(everything left, bars included; `rest(2)` = list of lists), and any
|
||||
`@@@argtype` the klammerset declares (`kdesc --argtypes`). An argtype may
|
||||
carry `:default` and `:alone`; a klammer may override the default, never the
|
||||
alone value.
|
||||
- **Option sets** (`.o`): a named group of optional parameters used by several
|
||||
klammers — `@@name.k arg @caption_args :caption_side top @ : … @@` splices
|
||||
the SKS's caption options with one default overridden. Usable only in a `.k`
|
||||
parameter list. A klammer *application* inside a parameter list is an error.
|
||||
- **`@eval`** is the primitive behind every non-trivial klammer:
|
||||
`@eval python_expr @` (module.function auto-imports; `K` holds the
|
||||
arguments), `@eval :cpp lib.function @`, `@eval :shell cmd @`,
|
||||
`@eval :haskell src @`; `:cwd DIR` runs it there (`:cwd *K_input_dir*`).
|
||||
A Python module is found next to the file that names it. **The result is
|
||||
re-read as Klammertext**: a result holding klammers is *generator* output and
|
||||
its text is escaped for the target; a result with none is *renderer* output
|
||||
and left alone. A literal `@` in a result must be emitted as `^@`.
|
||||
- A klammer whose tex body is target markup with a backslash must not be
|
||||
passed through a generator raw — the SKS carries such things as markers
|
||||
(`@nl`), resolved in an `:after_apply` phase.
|
||||
- **Klammersets**: `@@@klammerset sym | description :requires sks :files a.k b.k @@@`
|
||||
in `sym/sym.k`; loaded with `ktext --klammersets sym doc.kt` (symbols only,
|
||||
resolved in the document's directory, then `KLAMMERTEXT_KLAMMERSETS`, then
|
||||
the distribution; several combine in order, later definitions deciding
|
||||
collisions via the separators above). `kdesc -i file.k -k` shows what a file
|
||||
defines against the SKS. `--klammersets ref` adds the reflection klammers
|
||||
(`@diacritics_examples@`, `@unicode_examples@`, …) that generate the
|
||||
reference tables `kdesc -c`/`-u` print.
|
||||
|
||||
---
|
||||
|
||||
## 7. Behaviour worth knowing before you debug something
|
||||
## 9. Behaviour worth knowing before you debug something
|
||||
|
||||
- **`@cond` is strict about truth.** True: `true`, `True`, `1`. False: `false`,
|
||||
`False`, `0`, and empty. **Anything else is an error**, not "probably false".
|
||||
- **`@cond` evaluates only the branch it selects.** The branch not taken is not
|
||||
read, not evaluated, not expanded — a `@read` of a missing file in the unused
|
||||
branch is harmless.
|
||||
- **A state variable reaches a `@cond` predicate.** The document behaves like a
|
||||
function body whose arguments are its `@@@state` variables.
|
||||
- **`@eval :shell` and `:haskell`:** the command's standard output becomes
|
||||
document text; its standard error does not (it appears under `-v 1`); a nonzero
|
||||
exit is an error. Append `|| true` if a nonzero exit is expected.
|
||||
- **A definition inside a `@cond` branch does not register** — in either branch.
|
||||
Define outside and select between *applications*.
|
||||
`False`, `0`, empty. **Anything else is an error**, not "probably false".
|
||||
- **`@cond` is resolved at application time and evaluates only the branch it
|
||||
selects.** A `@read`/`@eval` in the other branch does not run. A top-level
|
||||
`@cond *Flag* | … @` sees the variable's value.
|
||||
- **A state variable is visible from the moment its declaration is read** —
|
||||
in every klammer body applied afterwards and in the top-level text of later
|
||||
input units, but not in the top-level text of the same unit that declares it.
|
||||
Declare early (`-s`, a first file, a klammerset).
|
||||
- **A definition inside a `@cond` branch does not register** — in either
|
||||
branch. Define outside and select between *applications*.
|
||||
- **`@eval :shell` and `:haskell`:** stdout becomes document text; stderr does
|
||||
not (shown under `-v 1`); a nonzero exit is an error. Append `|| true` if a
|
||||
nonzero exit is expected.
|
||||
- **Arguments arrive as written**, nested klammers already rendered; a macro
|
||||
escapes only its own body text. Never escape an output format's characters
|
||||
yourself, anywhere.
|
||||
- **Typographic transforms are per katom and skip verbatim text**: a `--` split
|
||||
by a klammer is two hyphens; inside `@c`/`@code` nothing is transformed.
|
||||
- **Recursion** is guarded: a klammer reaching itself 200 deep is a named
|
||||
`Recursion_error`; a document needing more than 100 top-level passes is an
|
||||
error.
|
||||
- **`@eval` results are trimmed**; a klammer that must emit leading space or
|
||||
pure vertical space uses the txt/tex markers the SKS provides
|
||||
(`phases.txt_nbsp_marker()`, `@vspace`).
|
||||
- The html **page cache** (`/dev/shm/_klammertext_cache/` on Linux) can serve
|
||||
a stale page after a change to a klammer's html; clear it when output looks
|
||||
old (`@document :cache false` disables it).
|
||||
|
||||
---
|
||||
|
||||
## 8. Diagnostics
|
||||
## 10. Diagnostics and what the commands print
|
||||
|
||||
- **`-v 1`** reports every decision the command made that the user could not have
|
||||
read off their own input: which klammerset was loaded and from where, which
|
||||
file a font or a `:files` name resolved to, how the output path was built.
|
||||
Higher levels are an implementation trace.
|
||||
- Logging and errors go to **standard error**; the rendered document goes to
|
||||
standard output, so `ktext doc.kt -d | …` is safe.
|
||||
- An error stops the run and names a file, line, and character. There is no
|
||||
warn-and-continue except in two documented cases.
|
||||
- A bare `ktext`, `kdesc`, or `kdiag` prints its usage and exits successfully.
|
||||
- Three cases only: **logging** (stderr, `-v` > 0), **an error** (stderr, then
|
||||
exit ≠ 0), **the requested output** (stdout). So `ktext doc.kt -t txt -d | …`
|
||||
is safe; nothing else lands in the document.
|
||||
- **`-v 1`** reports every decision the user could not read off their input:
|
||||
which klammerset loaded and from where, which file a typeface or `:files`
|
||||
name resolved to, how the output path was built, an override of another
|
||||
set's definition. `-v 2`+ is an implementation trace.
|
||||
- An error stops the run and names a file, line and character; messages are
|
||||
justified to 80 columns. There is **no warn-and-continue** except two
|
||||
aggregated heuristics: unknown target coverage, and a table wider than the
|
||||
page.
|
||||
- *"Word not parsed: …"* means a piece of text matched no katom type — usually
|
||||
a letter touching a delimiter (§2) or a punctuation run needing `^`.
|
||||
- Colour on stderr only when it is a terminal; `NO_COLOR` disables it. kdesc's
|
||||
`-c`/`-u` render through `ansi` on a terminal and `txt` in a pipe.
|
||||
|
||||
**When something is wrong:** run `kdiag --process --check doc.kt` first — it
|
||||
finds name, arity, option and coverage faults without rendering. Then `kdesc -k
|
||||
<klammer>` to check the parameter list against what was written. Then `kdiag
|
||||
--type '<fragment>'` if the question is how something is being parsed.
|
||||
**When something is wrong:** `kdiag --process --check doc.kt` first (name,
|
||||
arity, option and coverage faults without rendering); then `kdesc -k <klammer>`
|
||||
against what was written; then `kdiag --type '<fragment>'` if the question is
|
||||
how something is parsed; then `ktext … -v 1` if the question is which file or
|
||||
klammerset was used.
|
||||
|
||||
---
|
||||
|
||||
## 11. Klammertext from Python
|
||||
|
||||
```python
|
||||
import klammertext # kpy/ on PYTHONPATH (env/runtime.env)
|
||||
M = klammertext.Machine(target="html", files=["doc.kt"]) # as ktext sets itself up
|
||||
M.read("@@version : 3.2.1 @@") # what "ktext -s" does
|
||||
M.read_files() # the constructor's files, in order
|
||||
html = M.apply() # the rendered text
|
||||
```
|
||||
|
||||
`Machine(target='*', klammersets=None, files=None, output=None, display=False,
|
||||
verbose=0)`; `read(text)`, `read_file(name)`, `read_files()`,
|
||||
`apply(target=None, final_processing=True, escape_characters=True)`. **One
|
||||
Machine at a time** (a second raises `RuntimeError`); `apply()` reduces in
|
||||
place, so a second target needs a second Machine. Errors are `klammertext.Error`
|
||||
subclasses under the C++ names (`Parsing_error`, `Argument_error`,
|
||||
`Definition_error`, `Environment_error`, …) with `kind`, `description`,
|
||||
`filename`, `line`, `column`; `str(e)` is ktext's report; an `@eval`'s Python
|
||||
exception is chained as `__cause__`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Adjacent tools
|
||||
|
||||
- **Markdown → Klammertext:** `sks/tns/md_to_sks.py` converts a `.md` file;
|
||||
what it cannot convert exactly is left in the output as `#[MD … ]#` markers,
|
||||
so the draft carries its own worklist (spec: `doc/markdown_to_klammertext.md`).
|
||||
`mdpdf file.md` renders Markdown straight to PDF through a headless Chromium,
|
||||
bypassing Klammertext.
|
||||
- **Editors:** `doc/edit/` holds Emacs, Sublime Text, Vim and VS Code support
|
||||
(highlighting, delimiter matching, reindent, table alignment, a language
|
||||
server); `doc/edit/README.md`.
|
||||
- **Containers:** `akopra/klammertext:latest` (and `:haskell`) hold the three
|
||||
commands, the SKS, TeX Live and the default fonts; `doc/linux_container_install.md`,
|
||||
`doc/macos_container_install.md`.
|
||||
- **Environment:** `KLAMMERTEXT_HOME` (the tree), `KLAMMERTEXT_FONTS`,
|
||||
`KLAMMERTEXT_KLAMMERSETS` (colon-separated search paths); `env/runtime.env`
|
||||
sets them when sourced.
|
||||
@@ -0,0 +1 @@
|
||||
*.d
|
||||
+1
-1
@@ -326,7 +326,7 @@ PyObject* Machine_read(PyObject* obj, PyObject* arg)
|
||||
std::string text = PyUnicode_AsUTF8(arg);
|
||||
return guarded([&]() -> PyObject* {
|
||||
set_string_source();
|
||||
machine_of(obj).read(text + "\n");
|
||||
machine_of(obj).read_string(text + "\n");
|
||||
Py_RETURN_NONE;
|
||||
});
|
||||
}
|
||||
|
||||
+1
-172
@@ -172,7 +172,7 @@ std::string process_pinyin(std::string s)
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string process_unicode_codepoint(std::string s, const Locator& loc = Locator::none())
|
||||
std::string process_unicode_codepoint(std::string s, const Locator& loc)
|
||||
{
|
||||
(void)K::log(4);
|
||||
std::string result {s};
|
||||
@@ -267,174 +267,3 @@ std::string encode(const std::string& s, const Locator& loc)
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Decode
|
||||
|
||||
|
||||
// Display
|
||||
|
||||
void write_kt_example_file(std::stringstream& kt, std::string kt_filename)
|
||||
{
|
||||
kt << "|| @line@\n";
|
||||
kt << "@\n";
|
||||
std::cout << "Writing " << kt_filename << "...";
|
||||
std::ofstream out(kt_filename);
|
||||
out << kt.str();
|
||||
out.close();
|
||||
std::cout << "done\n";
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
void diacritics_examples(const std::string& kt_filename)
|
||||
{
|
||||
if (diacritics.size() != diacritic_symbols_order.size()) {
|
||||
//throw Internal_error("Mismatch between diacritics order list and their definitions");
|
||||
std::cout << "Mismatch error\n";
|
||||
}
|
||||
|
||||
std::stringstream kt {};
|
||||
bool write_kt_file = kt_filename.size() != 0;
|
||||
if (write_kt_file)
|
||||
kt << "@table :caption Diacritics (with typical base characters) |\n"
|
||||
<< " @i-Displayed | @i-Written | @i-Name\n";
|
||||
else
|
||||
std::cout << boldblack
|
||||
<< "\nDiacritics (with typical base characters)\n\n" << black;
|
||||
|
||||
std::string line_sep = "|| @line@ ";
|
||||
for (char symbol : diacritic_symbols_order) {
|
||||
std::string sym { symbol };
|
||||
auto [code, name] = diacritics[sym];
|
||||
std::string letter = diacritic_example_letter[sym];
|
||||
std::string written = "^" + letter + sym;
|
||||
if (sym == "~") {
|
||||
written = "^" + letter + "=7e=";
|
||||
}
|
||||
if (write_kt_file) {
|
||||
std::string display = "^" + letter + sym;
|
||||
kt << line_sep << display << " | @t ^" << written << " @ | " << name << "\n";
|
||||
line_sep = "|| ";
|
||||
}
|
||||
else {
|
||||
std::cout << " " << letter << unicode_hex_to_char(code)
|
||||
<< " " << "^" << letter << symbol << " " << name << "\n";
|
||||
}
|
||||
}
|
||||
if (write_kt_file)
|
||||
write_kt_example_file(kt, kt_filename);
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
void extended_latin_examples(const std::string& kt_filename)
|
||||
{
|
||||
std::stringstream kt {};
|
||||
bool write_kt_file = kt_filename.size() != 0;
|
||||
std::string title = "Extended Latin characters and ligatures";
|
||||
if (write_kt_file)
|
||||
kt << "@table :caption " << title << " |\n"
|
||||
<< " @i-Displayed | @i-Written | @i-Name\n";
|
||||
else
|
||||
std::cout << "\n" << boldblack << title << black << "\n\n";
|
||||
|
||||
std::string line_sep = "|| @line@ ";
|
||||
for (const std::string& symbol : extended_latin_symbols) {
|
||||
auto [code, name] = extended_latin[symbol];
|
||||
if (write_kt_file) {
|
||||
std::string coded = "^" + symbol + "^";
|
||||
std::string file_literal = "^^ #- " + symbol + " #- ^^";
|
||||
kt << line_sep << coded << " | @t " << file_literal << " @ | " << name << "\n";
|
||||
line_sep = "|| ";
|
||||
}
|
||||
else {
|
||||
std::string screen_literal = "^" + symbol + "^";
|
||||
std::cout << " " << unicode_hex_to_char(code)
|
||||
<< " " << std::setw(4) << std::left << screen_literal << " " << name << "\n";
|
||||
}
|
||||
}
|
||||
if (write_kt_file)
|
||||
write_kt_example_file(kt, kt_filename);
|
||||
}
|
||||
*/
|
||||
|
||||
void pinyin_examples(const std::string& kt_filename)
|
||||
{
|
||||
std::stringstream kt {};
|
||||
bool write_kt_file = kt_filename.size() != 0;
|
||||
if (write_kt_file)
|
||||
kt << "@table :caption Mandarin pinyin tones (using vowel ``a'') |\n"
|
||||
<< " @i-Displayed | @i-Written | @i-Name\n";
|
||||
else
|
||||
std::cout << boldblack
|
||||
<< "\nMandarin pinyin tones (using vowel \"a\")\n\n" << black;
|
||||
|
||||
std::string line_sep = "|| @line@ ";
|
||||
for (auto [symbol, codename] : pinyin_tones) {
|
||||
auto [code, name] = codename;
|
||||
// std::string hat_code = "^a" + code;
|
||||
std::string literal = "^a" + symbol;
|
||||
if (write_kt_file) {
|
||||
kt << line_sep << literal << " | @t ^" << literal << " @ | " << name << "\n";
|
||||
line_sep = "|| ";
|
||||
} else
|
||||
std::cout << " a" << unicode_hex_to_char(code)
|
||||
<< " "<< std::setw(4) << std::left << literal << " " << name << "\n";
|
||||
}
|
||||
if (write_kt_file)
|
||||
write_kt_example_file(kt, kt_filename);
|
||||
}
|
||||
|
||||
/*
|
||||
Outside my window
|
||||
crows build a nest in a tree...
|
||||
slowly... carefully.
|
||||
*/
|
||||
|
||||
void unicode_examples(const std::string& kt_filename)
|
||||
{
|
||||
std::stringstream kt {};
|
||||
bool write_kt_file = kt_filename.size() != 0;
|
||||
|
||||
std::cout << boldblack << "\nIncluding Unicode in Klammertext files\n\n" << black
|
||||
<< "To specify a wide variety of different letter forms, the Unicode Standard\n"
|
||||
<< "defines a large set of \"code points\" organized by groups. To include a\n"
|
||||
<< "Unicode character in Klammertext, you surround its hexadecimal number with\n"
|
||||
<< "the \"^\" character. For example, the Cyrillic captial letter \"I\" is \"И\",\n"
|
||||
<< "included in Klammertext as ^0418^.\n\n"
|
||||
<< "To search for Unicode characters by their description, see:\n"
|
||||
<< " https://www.fileformat.info/info/unicode/char/search.htm\n";
|
||||
|
||||
std::cout << "\n⟐ Examples of Chinese characters\n\n"
|
||||
<< " Pinyin\n";
|
||||
for (auto chr : dao) {
|
||||
std::string code = "^" + chr[1] + "^";
|
||||
std::cout << " " << process_unicode_codepoint(code) << " "
|
||||
<< code << " "
|
||||
<< chr[0] << "\n";
|
||||
}
|
||||
|
||||
std::cout << "\n⟐ Examples of Egyptian hieroglyphs\n\n";
|
||||
std::cout << std::setw(16) << " " << "Sound Gardiner\n";
|
||||
for (auto glyph : ptolemy) {
|
||||
std::cout << " " << std::setw(4) << process_unicode_codepoint("^" + glyph[2] + "^") << " "
|
||||
<< "^" + glyph[2] + "^ "
|
||||
<< std::setw(4)
|
||||
<< glyph[1] << " "
|
||||
<< std::setw(4)
|
||||
<< glyph[0] << "\n";
|
||||
}
|
||||
std::cout << "\nSee: https://en.wikipedia.org/wiki/List_of_Egyptian_hieroglyphs\n"
|
||||
<< "The Unicode codepoint in the second column of the \"List of hieroglyphs\"\n"
|
||||
<< "table is the number in the ^...^ code used by Klammertext.\n\n";
|
||||
}
|
||||
|
||||
void show_special_characters()
|
||||
{
|
||||
std::cout << std::setfill(' ');
|
||||
// diacritics_examples();
|
||||
// extended_latin_examples();
|
||||
pinyin_examples();
|
||||
}
|
||||
+3
-9
@@ -59,7 +59,7 @@ inline
|
||||
std::map<std::string, std::pair<std::string, std::string>> diacritics {
|
||||
{"`", {"0300", "grave accent"}},
|
||||
{"'", {"0301", "acute accent"}},
|
||||
{"h", {"0302", "circumflex"}},
|
||||
{"h", {"0302", "circumflex (hat)"}},
|
||||
{"~", {"0303", "tilde"}},
|
||||
{"-", {"0304", "macron"}},
|
||||
{"\"", {"0308", "diaresis"}},
|
||||
@@ -138,6 +138,8 @@ const std::string italic_on() { return is_tty() ? "\033[3m" : ""; }
|
||||
inline
|
||||
const std::string italic_off() { return is_tty() ? "\033[0m" : ""; }
|
||||
|
||||
std::string process_unicode_codepoint(std::string s, const Locator& loc = Locator::none());
|
||||
|
||||
std::string encode(const std::string& s, const Locator& loc);
|
||||
|
||||
// UTF-8 encoding of one code point (to U+10FFFF; surrogates and values
|
||||
@@ -154,11 +156,3 @@ std::string utf8char(int cp);
|
||||
// "Invalid Unicode" text is no longer reachable from input).
|
||||
bool valid_codepoint(int cp);
|
||||
std::string invalid_codepoint_reason(int cp);
|
||||
|
||||
// void diacritics_examples(const std::string& kt_filename = "");
|
||||
// void extended_latin_examples(const std::string& kt_filename = "");
|
||||
|
||||
void pinyin_examples(const std::string& kt_filename = "");
|
||||
void unicode_examples(const std::string& kt_filename = "");
|
||||
|
||||
void show_special_characters();
|
||||
+1
-1
@@ -336,7 +336,7 @@ katom_list Eval::eval(katom_iter begin, katom_iter end)
|
||||
Machine M = m_machine;
|
||||
M.m_warn_unparsed = false; // Result text is machine-generated (see machine.h)
|
||||
size_t before = M.m_katoms.size();
|
||||
M.read(eval_result);
|
||||
M.read_string(eval_result);
|
||||
// If the @eval produced more Klammertext -- the read-back result still
|
||||
// holds klammers to reduce (a "generator", e.g. a Python klammer returning
|
||||
// a @table call with data) -- then its text is writer content: escape it
|
||||
|
||||
@@ -89,6 +89,15 @@ static void classify_from_css(Resolved_font& font, std::string css_path)
|
||||
{
|
||||
// Parse the @font-face blocks in the CSS to determine variant → filename mapping
|
||||
std::string css = string_from_file(css_path);
|
||||
// The family name is what the css DECLARES, not what the caller wrote:
|
||||
// a typeface may be asked for by its directory name
|
||||
// ("noto-sans-egyptian-hieroglyphs"), and a font-family value in
|
||||
// generated html must match the @font-face or the browser substitutes
|
||||
// silently (until 2026-09-12 the request was echoed back).
|
||||
static const std::regex family_rgx(R"(font-family:\s*'([^']+)')");
|
||||
std::smatch match;
|
||||
if (std::regex_search(css, match, family_rgx))
|
||||
font.family_name = match[1];
|
||||
std::regex face_rgx(
|
||||
R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\w+);[^}]*url\('([^']+\.ttf)'\)[^}]*\})",
|
||||
std::regex::multiline);
|
||||
|
||||
+11
-11
@@ -482,24 +482,24 @@ void Machine::read(const fs::path& pathname)
|
||||
// :cpp libraries live next to the file that uses them).
|
||||
m_state.add_search_dir(fs::absolute(pathname).parent_path().string());
|
||||
m_state.open_frame("Machine state: " + pathname.string());
|
||||
std::string text = m_state.subst(trim_right(string_from_file(pathname)));
|
||||
katom_list katoms = process(text, pathname);
|
||||
m_katoms.insert(m_katoms.end(), katoms.begin(), katoms.end());
|
||||
extract_machine_definitions();
|
||||
extract_klammer_definitions();
|
||||
input(string_from_file(pathname), pathname.string());
|
||||
m_sources.push_back(pathname);
|
||||
}
|
||||
|
||||
void Machine::read(const std::string& s)
|
||||
void Machine::read_string(const std::string& text)
|
||||
{
|
||||
(void)K::log(3, s);
|
||||
m_state.open_frame("Machine state: " + s);
|
||||
std::string text = m_state.subst(trim_right(s));
|
||||
katom_list katoms = process(text, command_pathname);
|
||||
(void)K::log(3, text);
|
||||
m_state.open_frame("Machine state: " + text);
|
||||
input(text, command_pathname);
|
||||
m_sources.push_back(text);
|
||||
}
|
||||
|
||||
void Machine::input(const std::string& text, const std::string& source)
|
||||
{
|
||||
katom_list katoms = process(m_state.subst(trim_right(text)), source);
|
||||
m_katoms.insert(m_katoms.end(), katoms.begin(), katoms.end());
|
||||
extract_machine_definitions();
|
||||
extract_klammer_definitions();
|
||||
m_sources.push_back(s);
|
||||
}
|
||||
|
||||
// Read
|
||||
|
||||
+9
-2
@@ -69,8 +69,15 @@ public:
|
||||
bool nonascii=true, bool literal=true, bool ignore=true, bool whitespace=true,
|
||||
bool klammers=true, bool eval=true, bool cond=true, bool read=true);
|
||||
|
||||
void read(const fs::path& pathname);
|
||||
void read(const std::string& s);
|
||||
// Two ways to give the Machine its input. They are named, not
|
||||
// overloaded: fs::path and std::string convert into each other freely
|
||||
// enough that a string literal is ambiguous between them and a
|
||||
// std::string holding a filename silently selects the text reading.
|
||||
void read(const fs::path& pathname); // the file's contents
|
||||
void read_string(const std::string& text); // the string itself
|
||||
// What both do once they have the text: substitute, katomize, extract
|
||||
// definitions. `source` names where the text came from in messages.
|
||||
void input(const std::string& text, const std::string& source);
|
||||
|
||||
void expand_read_katoms(
|
||||
std::vector<Katom>& katoms, std::string source_filename,
|
||||
|
||||
@@ -7,10 +7,26 @@
|
||||
:requires sks
|
||||
@@@
|
||||
|
||||
@@diacritics_examples.k : Show the diacritics codes and their result in the text @@
|
||||
@@diacritics_examples.k :
|
||||
Show the diacritics codes and their result in the text @@
|
||||
|
||||
@@diacritics_examples.* :: @eval :cpp *KLAMMERTEXT_HOME*/ref/reflect diacritics_examples @ @@
|
||||
@@diacritics_examples.* ::
|
||||
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect diacritics_examples @ @@
|
||||
|
||||
@@extended_latin_examples.k : Show the codes and the resulting extended Latin characters @@
|
||||
@@extended_latin_examples.k :
|
||||
Show the codes and the resulting extended Latin characters @@
|
||||
|
||||
@@extended_latin_examples.* :: @eval :cpp *KLAMMERTEXT_HOME*/ref/reflect extended_latin_examples @ @@
|
||||
@@extended_latin_examples.* ::
|
||||
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect extended_latin_examples @ @@
|
||||
|
||||
@@pinyin_examples.k :
|
||||
Show the four tone markings for Mandarin @@
|
||||
|
||||
@@pinyin_examples.* ::
|
||||
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect pinyin_examples @ @@
|
||||
|
||||
@@unicode_examples.k :
|
||||
Show examples of Unicode characters @@
|
||||
|
||||
@@unicode_examples.* ::
|
||||
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect unicode_examples @ @@
|
||||
@@ -53,3 +53,65 @@ std::string extended_latin_examples([[maybe_unused]] Machine& machine)
|
||||
return kt.str();
|
||||
}
|
||||
|
||||
|
||||
extern "C"
|
||||
std::string pinyin_examples([[maybe_unused]] Machine& machine)
|
||||
{
|
||||
std::stringstream kt {};
|
||||
kt << "@table :caption Mandarin pinyin tones (using vowel ``a'') :cell_hpos c c l |\n"
|
||||
<< " @i-Displayed | @i-Written | @i-Name ||\n";
|
||||
|
||||
for (auto [symbol, codename] : pinyin_tones) {
|
||||
auto [code, name] = codename;
|
||||
std::string literal = "^a" + symbol;
|
||||
kt << literal << " | @t ^" << literal << " @ | " << name << " ||\n";
|
||||
}
|
||||
kt << "table@";
|
||||
return kt.str();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
std::string unicode_examples([[maybe_unused]] Machine& machine)
|
||||
{
|
||||
std::stringstream kt {};
|
||||
kt << "\n@b Including Unicode in Klammertext files @\n\n"
|
||||
<< "To specify a wide variety of different letter forms, the Unicode Standard "
|
||||
<< "defines a large set of \"code points\" organized by groups. To include a "
|
||||
<< "Unicode character in Klammertext, you surround its hexadecimal number with "
|
||||
<< "the \"^^\" character. For example, the Cyrillic captial letter \"I\" is \"И\", "
|
||||
<< "included in Klammertext as ^^0418^^.\n\n"
|
||||
<< "To search for Unicode characters by their description, see: @nl@\n"
|
||||
<< "@link https://www.fileformat.info/info/unicode/char/search.htm @\n\n";
|
||||
|
||||
kt << "@table :caption Examples of Chinese characters :cell_hpos c |\n"
|
||||
<< " Character | Unicode | Pinyin ||\n";
|
||||
|
||||
for (auto chr : dao) {
|
||||
std::string code = "^" + chr[1] + "^";
|
||||
kt << process_unicode_codepoint(code) << " | "
|
||||
<< "^" << code << "^" << " | "
|
||||
<< chr[0] << " ||\n";
|
||||
}
|
||||
kt << "table@\n\n";
|
||||
|
||||
|
||||
kt << "@table :caption Examples of Egyptian hieroglyphs :cell_hpos c c c l |\n"
|
||||
<< "Hieroglyph | Unicode | Sound | Gardiner ||\n";
|
||||
|
||||
// The body font carries no hieroglyphs, so the glyph names the
|
||||
// typeface it needs (shipped in fnt/); @document declares and localizes
|
||||
// it. Under txt @typeface passes the text through unchanged.
|
||||
for (auto glyph : ptolemy) {
|
||||
kt << "@typeface noto-sans-egyptian-hieroglyphs :scale 1.5 | "
|
||||
<< process_unicode_codepoint("^" + glyph[2] + "^") << " @ | "
|
||||
<< "^^" + glyph[2] + "^^ | "
|
||||
<< glyph[1] << " | "
|
||||
<< glyph[0] << " ||\n";
|
||||
}
|
||||
kt << "table@\n\n"
|
||||
<< "\nIn @link https://en.wikipedia.org/wiki/List_of_Egyptian_hieroglyphs @, "
|
||||
<< "the Unicode codepoint in the second column of the \"List of hieroglyphs\" "
|
||||
<< "table is the number in the ^^...^^ code used by Klammertext.\n\n";
|
||||
|
||||
return kt.str();
|
||||
}
|
||||
+1
-1
@@ -161,7 +161,7 @@ only makes some vertical space. @@
|
||||
|
||||
@@@argtype coords | x and y coordinates :pattern 'float'\s+'float' @@@
|
||||
|
||||
@@block.k : to.coords | content :width.float .5 :point.coords 0.0 0.0
|
||||
@@block.k to.coords | content :width.float .5 :point.coords 0.0 0.0
|
||||
: Absolute positioning of text block @@
|
||||
|
||||
@@block.tex :: @eval block.Block(K) eval@ @@
|
||||
|
||||
@@ -70,12 +70,20 @@ class Block(klammer_base.Klammer_base):
|
||||
# NOT latex_util.block(): a textblock is absolutely positioned and
|
||||
# does not participate in the normal flow, so breaking the paragraph
|
||||
# around it would move the surrounding text.
|
||||
#
|
||||
# The trailing \par\kern0pt gives the block's vbox zero depth.
|
||||
# textpos anchors :point against the box HEIGHT alone, so content
|
||||
# whose last line has depth -- an @image, whose [c] minipage sits
|
||||
# half below the baseline -- was anchored a quarter of its height
|
||||
# too low (2in for a page-high picture) until 2026-09-08.
|
||||
to_x, to_y = [float(e) for e in self.to.split()]
|
||||
pt_x, pt_y = [float(e) for e in self.point.split()]
|
||||
result = f"""
|
||||
\\begin{{textblock}}{{{self.width}}}[{pt_x},{pt_y}]({to_x},{to_y})
|
||||
\\setlength{{\\textwidth}}{{\\linewidth}}
|
||||
\\vspace*{{-1\\parskip}}
|
||||
{self.content.strip()}
|
||||
\\par\\kern0pt
|
||||
\\end{{textblock}}
|
||||
"""
|
||||
return result
|
||||
|
||||
@@ -5,11 +5,17 @@
|
||||
|
||||
%\usepackage[absolute,showboxes]{textpos}
|
||||
\usepackage[absolute]{textpos}
|
||||
\setlength{\TPHorizModule}{\paperheight}
|
||||
\setlength{\TPVertModule}{\paperwidth}
|
||||
\setlength{\TPHorizModule}{\paperwidth}
|
||||
\setlength{\TPVertModule}{\paperheight}
|
||||
|
||||
%\setlength{\TPVertModule}{1\paperheight}
|
||||
\textblockorigin{0in}{0in}
|
||||
% The paper's top-left corner. textpos's absolute mode subtracts TeX's
|
||||
% 1in origin itself (\textblockorigin stores x-1in, y-1in), and
|
||||
% document.sty already sets \hoffset and \voffset to -1in for the same
|
||||
% reason, so {0in}{0in} here shifted every block 1in left and 1in up
|
||||
% (until 2026-09-08). Cancelling the offsets is right under either
|
||||
% convention.
|
||||
\textblockorigin{-\hoffset}{-\voffset}
|
||||
|
||||
\newlength{\fwboxwidth}
|
||||
\newcommand{\fwboxn}[1]{\setlength{\fwboxwidth}{#1}}
|
||||
|
||||
@@ -276,9 +276,12 @@ void Document_class::collect_typefaces(const std::string& text)
|
||||
// located error naming the available typefaces if the store
|
||||
// does not have it, which is the whole error path for a
|
||||
// misspelled or uninstalled typeface.
|
||||
// Compared by directory name: a @typeface may write either
|
||||
// the family name or the store's directory name, and both
|
||||
// resolve to the same directory.
|
||||
bool seen = false;
|
||||
for (const auto& rf : m_resolved_extra)
|
||||
if (rf.family_name == name) seen = true;
|
||||
if (rf.dir_name == name_to_dirname(name)) seen = true;
|
||||
if (!seen) {
|
||||
// What a @typeface klammer asked for, and what the store
|
||||
// resolved it to: a decision the user cannot read off
|
||||
@@ -293,13 +296,22 @@ void Document_class::collect_typefaces(const std::string& text)
|
||||
}
|
||||
}
|
||||
|
||||
std::string Document_class::strip_typeface_markers(const std::string& text)
|
||||
const Resolved_font& Document_class::resolved_typeface(const std::string& name) const
|
||||
{
|
||||
for (const auto& rf : m_resolved_extra)
|
||||
if (rf.dir_name == name_to_dirname(name)) return rf;
|
||||
// collect_typefaces() resolves every marker before this is asked.
|
||||
throw Internal_error("Typeface \"" + name + "\" was not resolved");
|
||||
}
|
||||
|
||||
std::string Document_class::resolve_typeface_families(const std::string& text)
|
||||
{
|
||||
std::string result {};
|
||||
size_t pos = 0, start = 0, end = 0;
|
||||
std::string name {}, scale {};
|
||||
while (next_typeface_marker(text, pos, start, end, name, scale)) {
|
||||
result += text.substr(pos, start - pos);
|
||||
result += resolved_typeface(name).family_name;
|
||||
pos = end;
|
||||
}
|
||||
result += text.substr(pos);
|
||||
|
||||
@@ -144,7 +144,12 @@ public:
|
||||
void collect_typefaces(const std::string& text);
|
||||
// html: the marker carries no inline switch (the span holds the family
|
||||
// name), so each is replaced by nothing.
|
||||
std::string strip_typeface_markers(const std::string& text);
|
||||
// html: each marker is replaced by the family name the store resolved
|
||||
// its typeface to (the name as written may be the store's directory
|
||||
// name, which no @font-face declares).
|
||||
std::string resolve_typeface_families(const std::string& text);
|
||||
// The resolved font for a typeface name as a @typeface wrote it.
|
||||
const Resolved_font& resolved_typeface(const std::string& name) const;
|
||||
// tex: each distinct (typeface, scale) gets a \newfontfamily command in
|
||||
// the preamble; the marker is replaced by that command.
|
||||
std::string resolve_typeface_markers(const std::string& text);
|
||||
|
||||
@@ -109,12 +109,15 @@ std::string Document_class::font_definitions()
|
||||
// var() with such a value computes to inherit, silently losing
|
||||
// the font.
|
||||
ss << ":root {\n";
|
||||
// The family the store RESOLVED, which is what the localized css
|
||||
// declares -- the name as written may be the store's directory
|
||||
// name ("crimson-pro"), which matches no @font-face.
|
||||
if (!m_serif_typeface.empty())
|
||||
ss << " --serif: \"" << m_serif_typeface << "\", serif;\n";
|
||||
ss << " --serif: \"" << m_resolved_serif.family_name << "\", serif;\n";
|
||||
if (!m_sans_typeface.empty())
|
||||
ss << " --sans-serif: \"" << m_sans_typeface << "\", sans-serif;\n";
|
||||
ss << " --sans-serif: \"" << m_resolved_sans.family_name << "\", sans-serif;\n";
|
||||
if (!m_mono_typeface.empty())
|
||||
ss << " --monospace: \"" << m_mono_typeface << "\", monospace;\n";
|
||||
ss << " --monospace: \"" << m_resolved_mono.family_name << "\", monospace;\n";
|
||||
ss << "}\n";
|
||||
}
|
||||
// Font-size normalization across families is no longer emitted from
|
||||
@@ -761,7 +764,7 @@ std::string Document_class::html()
|
||||
install_resolved_font(rf, output_directory);
|
||||
}
|
||||
for (auto& [basename, text] : m_file_components) {
|
||||
text = strip_typeface_markers(text);
|
||||
text = resolve_typeface_families(text);
|
||||
}
|
||||
auto headings = insert_section_numbers();
|
||||
insert_section_numbers("___NUM2___", false);
|
||||
|
||||
+8
-3
@@ -49,12 +49,16 @@ unchanged.
|
||||
@@b.k text : Use a bold weight of the current font for *text* @@
|
||||
@@b.html :: <b>*text*</b> @@
|
||||
@@b.tex :: \textbf{*text*} @@
|
||||
@@b.txt :: *text* @@
|
||||
# The txt bodies of @b, @i, @u and @ri emit STYLE MARKERS around the text
|
||||
# (phases.txt_style_marker), which the txt target strips and the ansi
|
||||
# target turns into escape sequences -- see the ansi target in
|
||||
# sks/target/target.k. "#-" glues the marker to the text.
|
||||
@@b.txt :: @eval phases.txt_style_marker("bold") @ #- *text* #- @eval phases.txt_style_marker("bold", False) @ @@
|
||||
|
||||
@@i.k text : Use an italic style of the current font for *text* @@
|
||||
@@i.html :: <i>*text*</i> @@
|
||||
@@i.tex :: \emph{*text*} @@ # \begin{emph}{*text*}\end{emph} @@
|
||||
@@i.txt :: *text* @@
|
||||
@@i.txt :: @eval phases.txt_style_marker("italic") @ #- *text* #- @eval phases.txt_style_marker("italic", False) @ @@
|
||||
|
||||
@@t.k text : Use the currently defined monospace font for *text* @@
|
||||
@@t.html :: <span class="tt">*text*</span> @@
|
||||
@@ -68,11 +72,12 @@ unchanged.
|
||||
|
||||
@@u.k text : Underline *text* in the current font @@
|
||||
@@u.tex :: \underline{\botstrut{2pt}*text*} @@
|
||||
@@u.txt :: @eval phases.txt_style_marker("underline") @ #- *text* #- @eval phases.txt_style_marker("underline", False) @ @@
|
||||
|
||||
@@ri.k text : Use a roman italic font for *text* (to override monospace environment) @@
|
||||
@@ri.html :: <span class="ritalic">*text*</span> @@
|
||||
@@ri.tex :: \textrm{\textit{*text*}} @@
|
||||
@@ri.txt :: *text* @@
|
||||
@@ri.txt :: @eval phases.txt_style_marker("italic") @ #- *text* #- @eval phases.txt_style_marker("italic", False) @ @@
|
||||
|
||||
@@ts.k scale.float | text : Resize *text* by *scale* @@
|
||||
# @@ts.tex :: {\scalefont{*scale*}\em *text*} @@
|
||||
|
||||
+19
-10
@@ -31,8 +31,12 @@ class Subsuper(klammer_base.Klammer_base):
|
||||
# this marker where the typeface is needed, and @document collects the
|
||||
# markers, resolves and localizes each typeface, declares it, and replaces
|
||||
# the marker with whatever inline switch the target needs (a font-switch
|
||||
# command for tex, nothing for html, which carries the family name in the
|
||||
# span's own style). Same shape as the __CAPTION__ markers.
|
||||
# command for tex; for html the marker sits IN the span's font-family value
|
||||
# and becomes the family name the store resolved the typeface to, which
|
||||
# is what the localized css declares -- the name as WRITTEN may be the
|
||||
# store's directory name, "noto-sans-egyptian-hieroglyphs", and a span
|
||||
# carrying that matched no @font-face until 2026-09-12). Same shape as
|
||||
# the __CAPTION__ markers.
|
||||
#
|
||||
# SYNC: typeface_marker_rgx in sks/document/document_class.h -- a C++/Python
|
||||
# pair, so the constant cannot be single-sourced the way a .k/.py pair can
|
||||
@@ -53,18 +57,23 @@ class Typeface(klammer_base.Klammer_base):
|
||||
# hieroglyphs, cuneiform -- and would resize them unpredictably.
|
||||
# The family name is quoted -- an unquoted digit-initial word
|
||||
# ("Source Sans 3") is invalid CSS -- with SINGLE quotes, because
|
||||
# this sits inside a double-quoted html attribute.
|
||||
style = (f"font-family: '{self.name}', serif; "
|
||||
f"font-size-adjust: none")
|
||||
# this sits inside a double-quoted html attribute. What is quoted
|
||||
# here is the MARKER: @document replaces it with the resolved
|
||||
# family name (see the marker note above).
|
||||
style = "', serif; font-size-adjust: none"
|
||||
if float(self.scale) != 1.0:
|
||||
style += f"; font-size: {float(self.scale)}em"
|
||||
# The STYLE is markup, so its typographically active characters are
|
||||
# hidden: the html target's transforms turn a single quote into a
|
||||
# curly one, which broke the css font-family value. The TEXT is
|
||||
# writer content and is deliberately left alone -- its quotes and
|
||||
# dashes should be transformed.
|
||||
return (typeface_marker(self.name, self.scale)
|
||||
+ f'<span style="{klammer_base.hide_typographic(style)}">'
|
||||
# curly one, which broke the css font-family value. The MARKER is
|
||||
# not hidden: a hyphen in the typeface name would become a KTESC
|
||||
# sequence, which on the :files path is still there when @document
|
||||
# looks for the marker. The TEXT is writer content and is
|
||||
# deliberately left alone -- its quotes and dashes should be
|
||||
# transformed.
|
||||
hide = klammer_base.hide_typographic
|
||||
return (f'<span style="{hide("font-family: " + chr(39))}'
|
||||
+ typeface_marker(self.name, self.scale) + hide(style) + '">'
|
||||
+ f'{self.text}</span>')
|
||||
|
||||
def tex(self):
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
|
||||
@@@state Image_default_width :desc Default width for images :value .667w @@@
|
||||
|
||||
@@@state Image_quality
|
||||
:desc JPEG quality, 1 to 100, at which a photograph -- a raster with
|
||||
no transparency, whether it arrives as JPEG, PNG or TIFF -- is
|
||||
written to the image cache for the html and pdf targets. The
|
||||
default keeps print quality at a fraction of the size; 100 copies
|
||||
or converts the source unchanged, as before 2026-09-09.
|
||||
:value 85
|
||||
@@@
|
||||
|
||||
#
|
||||
# @@@argtype pixels | a pixel count :pattern \d+px @@@
|
||||
# @@image.k basename : Image read from a file @@
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@ class Image(klammer_base.Klammer_base):
|
||||
if not os.path.isdir(cache_dir):
|
||||
import tempfile
|
||||
cache_dir = tempfile.gettempdir()
|
||||
self.cache = image_cache.Image_cache(cache_dir, self.Image_search_path, verbose=False)
|
||||
self.cache = image_cache.Image_cache(cache_dir, self.Image_search_path, verbose=False,
|
||||
quality=self.Image_quality)
|
||||
self.basename = klammer_base.unescape_ktesc(self.basename)
|
||||
self.source, self.pwidth, self.pheight, self.file_error = self.cache.get(self.K_target, self.basename)
|
||||
if self.file_error:
|
||||
|
||||
@@ -40,14 +40,49 @@ def image_dimensions(filename):
|
||||
return width, height, resize
|
||||
|
||||
|
||||
# Raster formats a photograph arrives in. Vector sources (pdf, eps) are
|
||||
# never re-encoded, nor is a gif or webp (they may be animated, and only
|
||||
# html takes them).
|
||||
photograph_formats = {"jpg", "jpeg", "png", "tif", "tiff"}
|
||||
|
||||
def is_photograph(filename):
|
||||
"""True if the file is a raster with no alpha channel: a photograph
|
||||
(or a grayscale one) that JPEG stores well. A png with transparency,
|
||||
or a palette image, is not one -- JPEG would flatten or posterize it."""
|
||||
ext = os.path.splitext(filename)[1][1:].lower()
|
||||
if ext not in photograph_formats:
|
||||
return False
|
||||
inp = oiio.ImageInput.open(filename)
|
||||
if not inp:
|
||||
return False
|
||||
spec = inp.spec()
|
||||
inp.close()
|
||||
return spec.alpha_channel < 0 and spec.nchannels in (1, 3)
|
||||
|
||||
|
||||
class Image_cache:
|
||||
max_width = 1600 # Should be a state variable
|
||||
cache_basedir = "_klammertext_imgsrc" # Should be a state variable
|
||||
error_marker_filename = "_error_marker.png"
|
||||
verbose_label = " [klammertext]" # Should parameterize verbosity
|
||||
def __init__(self, cache_dir, search_path, verbose=False):
|
||||
def __init__(self, cache_dir, search_path, verbose=False, quality=100):
|
||||
#self.cache_dir = kutil.cache_directory(
|
||||
self.dir = kutil.cache_directory(cache_dir, "_image_cache")
|
||||
# JPEG quality (1-100) at which a photograph is written to the cache.
|
||||
# 100 means the source is copied or converted as it is (the
|
||||
# behaviour before 2026-09-09); below it, every photograph -- a
|
||||
# JPEG source included -- is re-encoded at that quality, which is
|
||||
# where a document's size mostly lives: a source JPEG is often
|
||||
# near-lossless (quality 95+), and a png photograph is stored
|
||||
# losslessly, at three to six times the size of a quality-85 JPEG
|
||||
# with no visible difference at print resolution. The Image_quality
|
||||
# state variable (sks/image/image.k) sets it.
|
||||
try:
|
||||
self.quality = int(quality)
|
||||
except (TypeError, ValueError):
|
||||
raise Exception(f'Image_quality must be an integer from 1 to 100, not "{quality}"')
|
||||
if not 1 <= self.quality <= 100:
|
||||
raise Exception(f"Image_quality must be from 1 to 100, not {self.quality}")
|
||||
self.search = search_path.split(" ")
|
||||
self.verbose = verbose or os.environ.get("KLAMMERTEXT_IMAGE_CACHE_DISPLAY")
|
||||
self.required_format = { "html" : ["jpg", "jpeg", "JPEG", "png", "gif", "webp"],
|
||||
@@ -75,6 +110,18 @@ class Image_cache:
|
||||
return f"{self.dir}/{filename}.{cache_ext}"
|
||||
|
||||
|
||||
def photograph_cache_filename(self, basename):
|
||||
# The quality is part of the name, so changing it re-encodes and
|
||||
# two documents at different qualities do not fight over one file.
|
||||
return f"{self.dir}/{basename}.q{self.quality}.jpg"
|
||||
|
||||
def reencode(self, target, source_filename):
|
||||
"""Whether this source is written to the cache as a quality-N JPEG."""
|
||||
return self.quality < 100 \
|
||||
and target in self.required_format \
|
||||
and source_filename != self.error_image_filename \
|
||||
and is_photograph(source_filename)
|
||||
|
||||
def find_file(self, target, basename):
|
||||
result = None
|
||||
candidates = []
|
||||
@@ -131,6 +178,8 @@ class Image_cache:
|
||||
if src[:2] == "..":
|
||||
src = source_filename
|
||||
dest = os.path.splitext(cached_filename)[1]
|
||||
if cached_filename.endswith(f".q{self.quality}.jpg"):
|
||||
dest = f"{dest} (quality {self.quality})"
|
||||
dim = "" if width == new_width else f" [{new_width}x{new_height}]"
|
||||
desc = "(copied)" if copied else f"-> {dest}{dim}"
|
||||
#print(f"{Image_cache.verbose_label} To cache: {src} [{width}x{height}] {desc}")
|
||||
@@ -148,7 +197,20 @@ class Image_cache:
|
||||
cached_filename = f"{os.path.dirname(cached_filename)}/{os.path.basename(source_filename)}"
|
||||
|
||||
copied = False
|
||||
if resize and width > Image_cache.max_width:
|
||||
if self.reencode(target, source_filename):
|
||||
cached_filename = self.photograph_cache_filename(basename)
|
||||
buf = oiio.ImageBuf(source_filename)
|
||||
if buf.has_error:
|
||||
raise Exception(f'Cannot read image "{source_filename}": {buf.geterror()}')
|
||||
if width > Image_cache.max_width:
|
||||
new_width = Image_cache.max_width
|
||||
new_height = int(round(new_width * float(height) / float(width)))
|
||||
roi = oiio.ROI(0, new_width, 0, new_height, 0, 1, 0, buf.nchannels)
|
||||
buf = oiio.ImageBufAlgo.resize(buf, roi=roi)
|
||||
buf.specmod().attribute("CompressionQuality", self.quality)
|
||||
if not buf.write(cached_filename):
|
||||
raise Exception(f'Cannot write "{cached_filename}": {buf.geterror()}')
|
||||
elif resize and width > Image_cache.max_width:
|
||||
new_width = Image_cache.max_width
|
||||
new_height = int(round(new_width * float(height) / float(width)))
|
||||
if source_filename.endswith(".pdf"):
|
||||
@@ -190,6 +252,11 @@ class Image_cache:
|
||||
def get(self, target, basename):
|
||||
cached_filename = self.find_cached_file(target, basename)
|
||||
source_filename = self.find_file(target, basename)
|
||||
if self.reencode(target, source_filename):
|
||||
# A copy cached at another quality (or untouched, by an earlier
|
||||
# run at 100) does not serve: the wanted file is the quality-N
|
||||
# one, and cache_update_required() makes it if it is missing.
|
||||
cached_filename = self.photograph_cache_filename(basename)
|
||||
if cache_update_required(source_filename, cached_filename):
|
||||
cached_filename, width, height = self.update_cache(target, basename, source_filename)
|
||||
else:
|
||||
|
||||
@@ -65,7 +65,8 @@ class Image_grid(klammer_base.Klammer_base):
|
||||
self.cache = image_cache.Image_cache(
|
||||
self.K_input_dir,
|
||||
self.Image_search_path,
|
||||
verbose=False)
|
||||
verbose=False,
|
||||
quality=self.Image_quality)
|
||||
|
||||
self.margin = 0.01
|
||||
|
||||
|
||||
@@ -67,16 +67,25 @@ class Klammer_base:
|
||||
print(label)
|
||||
pprint.pprint(self.__dict__)
|
||||
|
||||
# A target that :includes another renders through the included one's
|
||||
# method: pdf is tex compiled, ansi is txt with escape sequences. The
|
||||
# Python side cannot see :includes, so the relation is restated here.
|
||||
# SYNC: the @@@target declarations in sks/target/target.k.
|
||||
TARGET_METHOD = {"html": "html",
|
||||
"tex": "tex", "pdf": "tex",
|
||||
"txt": "txt", "ansi": "txt"}
|
||||
|
||||
def __str__(self):
|
||||
result = None
|
||||
if self.K_target == 'html':
|
||||
method = Klammer_base.TARGET_METHOD.get(self.K_target)
|
||||
if method == 'html':
|
||||
result = self.html()
|
||||
elif self.K_target in {'tex', 'pdf'}:
|
||||
elif method == 'tex':
|
||||
result = kutil.escape(self.tex())
|
||||
elif self.K_target == 'txt':
|
||||
elif method == 'txt':
|
||||
result = self.txt()
|
||||
if result is None:
|
||||
#print(self.__dict__)
|
||||
raise Exception(
|
||||
f'Target "{self.K_target}" is undefined for klammer "@{self.K_klammer}"')
|
||||
f'Target "{self.K_target}" is undefined for klammer class '
|
||||
f'"{self.__class__.__name__}"')
|
||||
return result
|
||||
+1
-1
@@ -70,7 +70,7 @@ class Link(klammer_base.Klammer_base):
|
||||
|
||||
def txt(self):
|
||||
text = f'{self.text} ' if self.text else ''
|
||||
return f'{text}[{self.url}]'
|
||||
return f'{text}[{self.target}]'
|
||||
|
||||
class Target(klammer_base.Klammer_base):
|
||||
def __init__(self, K):
|
||||
|
||||
@@ -36,3 +36,8 @@
|
||||
\newlength{\klfillb}
|
||||
\newlength{\klfillc}
|
||||
\newlength{\klfilld}
|
||||
|
||||
% Scratch register in which a 'fit' or 'fill' column's width is measured
|
||||
% (\settowidth, then \maxof over each further entry) before being frozen
|
||||
% into the per-column macro \klfit<X> the column spec reads.
|
||||
\newlength{\klfitwidth}
|
||||
+129
-41
@@ -440,28 +440,33 @@ class Table(klammer_base.Klammer_base):
|
||||
self.rowspan_covered.add((row_i, col_i))
|
||||
self.covered = self.colspan_covered | self.rowspan_covered
|
||||
|
||||
def column_width_text(self):
|
||||
# For each column, the text used to measure a 'fit' width in the
|
||||
# tex target. The longest cell's font is applied, so a bold or
|
||||
# italic cell is measured in the font it will be set in.
|
||||
self.column_widths = []
|
||||
def column_width_candidates(self):
|
||||
# For each column, the texts whose widths a 'fit' or 'fill' column's
|
||||
# tex width is the maximum of: every cell of the column, each line
|
||||
# of a multi-line cell separately, set in its own cell's font. The
|
||||
# maximum is taken in TeX (tex_column_widths), not here: until
|
||||
# 2026-09-12 one "longest" entry was chosen by len() of the tex
|
||||
# MARKUP, so \texttt{\textasciicircum{}e'} (26 characters, 3
|
||||
# visible) outranked a header "Written" and the header overflowed
|
||||
# into the next column (TODO #59).
|
||||
self.column_candidates = []
|
||||
for col_i in range(len(self.cells[0])):
|
||||
longest = ""
|
||||
longest_style = "r"
|
||||
candidates = []
|
||||
for row_i in range(len(self.cells)):
|
||||
cell = self.cells[row_i][col_i]
|
||||
# A colspan anchor's text spans several columns and must
|
||||
# not set the width of its own column.
|
||||
if cell is not None and cell.colspan <= 1:
|
||||
lines = [e.strip() for e in cell.text.split("\\newline")]
|
||||
lines = sorted(lines, key=len)
|
||||
longest_in_line = lines[-1]
|
||||
if len(longest_in_line) > len(longest):
|
||||
longest = longest_in_line
|
||||
longest_style = cell.font
|
||||
if longest_style != "r":
|
||||
longest = font.tex_fontify(longest, longest_style, 1.0)
|
||||
self.column_widths.append(longest)
|
||||
if cell is None or cell.colspan > 1:
|
||||
continue
|
||||
for line in cell.text.split("\\newline"):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if cell.font != "r":
|
||||
line = font.tex_fontify(line, cell.font, 1.0)
|
||||
if line not in candidates:
|
||||
candidates.append(line)
|
||||
self.column_candidates.append(candidates)
|
||||
|
||||
# ---- :hpos -------------------------------------------------------------
|
||||
#
|
||||
@@ -530,7 +535,7 @@ class Table(klammer_base.Klammer_base):
|
||||
cell_i + max(cspan, 1) == self.row_size)))
|
||||
cells.append(row_cells)
|
||||
self.cells = cells
|
||||
self.column_width_text()
|
||||
self.column_width_candidates()
|
||||
|
||||
# HTML
|
||||
|
||||
@@ -626,6 +631,41 @@ class Table(klammer_base.Klammer_base):
|
||||
# in table.sty; one per fill column, in column order.
|
||||
fill_registers = ["\\klfilla", "\\klfillb", "\\klfillc", "\\klfilld"]
|
||||
|
||||
@staticmethod
|
||||
def width_macro(col_i):
|
||||
# The macro holding a column's measured width: \klfitA ... \klfitZ,
|
||||
# \klfitAA, ... (a control-sequence name holds letters only).
|
||||
letters = ""
|
||||
i = col_i
|
||||
while i >= 0:
|
||||
letters = chr(ord("A") + i % 26) + letters
|
||||
i = i // 26 - 1
|
||||
return "\\klfit" + letters
|
||||
|
||||
def tex_column_widths(self):
|
||||
# Set each 'fit' or 'fill' column's width macro to the width of its
|
||||
# widest entry, measured in TeX: \settowidth of the first candidate,
|
||||
# then calc's \maxof against \widthof of each further one, in the
|
||||
# scratch register \klfitwidth (table.sty); the result is frozen
|
||||
# into \klfit<X> by \edef, so the column spec -- which array's p{}
|
||||
# evaluates once per CELL -- reads a number instead of re-measuring
|
||||
# every candidate. Flat, one line per candidate: a nested \maxof
|
||||
# would recurse as deep as the column is long.
|
||||
result = ""
|
||||
for i, w in enumerate(extend(self.column_width, self.row_size)):
|
||||
if w not in ("fit", "f", "fill"):
|
||||
continue
|
||||
candidates = self.column_candidates[i]
|
||||
if not candidates:
|
||||
result += "\\setlength{\\klfitwidth}{0pt}\n"
|
||||
else:
|
||||
result += f"\\settowidth{{\\klfitwidth}}{{{candidates[0]}}}\n"
|
||||
for c in candidates[1:]:
|
||||
result += (f"\\setlength{{\\klfitwidth}}"
|
||||
f"{{\\maxof{{\\klfitwidth}}{{\\widthof{{{c}}}}}}}\n")
|
||||
result += f"\\edef{Table.width_macro(i)}{{\\the\\klfitwidth}}\n"
|
||||
return result
|
||||
|
||||
def tex_fill_widths(self):
|
||||
# Set each 'fill' column's register to min(its share of the
|
||||
# remaining width, its widest entry) -- the same rule the html auto
|
||||
@@ -637,11 +677,10 @@ class Table(klammer_base.Klammer_base):
|
||||
if not self.fill_columns:
|
||||
return ""
|
||||
widths = extend(self.column_width, self.row_size)
|
||||
fit = [f"\\widthof{{{self.column_widths[i]}}}"
|
||||
fit = [Table.width_macro(i)
|
||||
for i, w in enumerate(widths) if w in ("fit", "f")]
|
||||
remaining = "\\tablewidth" + "".join(f" - {e}" for e in fit)
|
||||
widest = {i: f"\\widthof{{{self.column_widths[i]}}}"
|
||||
for i in self.fill_columns}
|
||||
widest = {i: Table.width_macro(i) for i in self.fill_columns}
|
||||
total = " + ".join(widest[i] for i in self.fill_columns)
|
||||
result = ""
|
||||
for k, i in enumerate(self.fill_columns):
|
||||
@@ -658,7 +697,7 @@ class Table(klammer_base.Klammer_base):
|
||||
|
||||
def tex_hpos(self):
|
||||
# One column specification per column: the width comes from
|
||||
# :column_width ('fit', 'fill' via its precomputed register, a
|
||||
# :column_width ('fit' and 'fill' via their precomputed macros, a
|
||||
# fraction of \tablewidth, or '*' for the remaining width), the
|
||||
# justification from :cell_hpos.
|
||||
def par_format(s, justification):
|
||||
@@ -671,7 +710,7 @@ class Table(klammer_base.Klammer_base):
|
||||
fill_ordinal = 0
|
||||
for i, w in enumerate(extend(self.column_width, self.row_size)):
|
||||
if w in ("fit", "f"):
|
||||
widths.append(f"\\widthof{{{self.column_widths[i]}}}")
|
||||
widths.append(Table.width_macro(i))
|
||||
elif w == "fill":
|
||||
widths.append(Table.fill_registers[fill_ordinal])
|
||||
fill_ordinal += 1
|
||||
@@ -804,9 +843,9 @@ class Table(klammer_base.Klammer_base):
|
||||
# marker and prints the console warning with the :column_width
|
||||
# primer -- the widths are only known at LaTeX run time, and prose
|
||||
# kept out of TeX avoids the log's 79-column line wrapping.
|
||||
return ("\\ifdim\\tableboxwidth>\\dimexpr\\textwidth+2pt\\relax\n"
|
||||
return ("\\ifdim\\tableboxwidth>\\dimexpr\\linewidth+2pt\\relax\n"
|
||||
f"\\message{{^^JKT-WIDE-TABLE {name} overfull by "
|
||||
"\\the\\dimexpr\\tableboxwidth-\\textwidth\\relax^^J}\n"
|
||||
"\\the\\dimexpr\\tableboxwidth-\\linewidth\\relax^^J}\n"
|
||||
"\\fi\n")
|
||||
|
||||
def tex(self):
|
||||
@@ -816,15 +855,25 @@ class Table(klammer_base.Klammer_base):
|
||||
# minipage below: it computes the width the minipage consumes.
|
||||
measure = self.get_width() + self.tex_width_check(name)
|
||||
result = f"\\renewcommand*{{\\arraystretch}}{{{self.leading}}}\n"
|
||||
if self.allow_break:
|
||||
result += "\\vspace*{12pt}\n"
|
||||
result += "\\begin{longtable}{"
|
||||
# A page-breaking table is a longtable. A boxed one is a plain
|
||||
# tabular: inside its minipage a longtable could not break anyway,
|
||||
# and longtable refuses to run in a multicols or two-column page,
|
||||
# which is where a boxed table is wanted (until 2026-09-08 both
|
||||
# paths emitted longtable, so @table could not be used in
|
||||
# @multicolumn at all).
|
||||
env = "longtable" if self.allow_break else "tabular"
|
||||
result += f"\\begin{{{env}}}{{"
|
||||
result += self.tex_column_spec()
|
||||
result += "}\n"
|
||||
if self.allow_break:
|
||||
if self.allow_break and (self.number or self.caption):
|
||||
# The caption rides in longtable's last foot so it tracks the
|
||||
# table across page breaks. Without a caption there is no foot:
|
||||
# an empty one is still a full row (\vspace*{8pt} plus an empty
|
||||
# \small line), which put ~20pt of blank under every uncaptioned
|
||||
# breaking table until 2026-09-08.
|
||||
result += self.make_caption()
|
||||
result += self.tex_rows()
|
||||
result += "\\end{longtable}\n"
|
||||
result += f"\\end{{{env}}}\n"
|
||||
|
||||
if not self.allow_break:
|
||||
# Box the table at its measured width so the caption tracks it
|
||||
@@ -845,18 +894,27 @@ class Table(klammer_base.Klammer_base):
|
||||
result = measure + result
|
||||
|
||||
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
|
||||
# The wrapper (add_caption/caption_wrapper) owns all vertical space
|
||||
# around the table; longtable's own glue is zeroed.
|
||||
# A boxed table's wrapper (add_caption/caption_wrapper) owns all
|
||||
# vertical space around it, so longtable's own glue is zeroed. A
|
||||
# page-breaking table has no wrapper and is set by longtable itself,
|
||||
# so its glue is \parskip on both sides: it then sits in the text
|
||||
# with the spacing of a paragraph (it was a fixed \vspace*{12pt}
|
||||
# above and nothing below until 2026-09-08).
|
||||
lt_glue = "\\parskip" if self.allow_break else "0pt"
|
||||
result = (f"\\setlength{{\\tabcolsep}}{{{self.colsep}}}\n"
|
||||
"\\setlength{\\LTpre}{0pt}\n"
|
||||
"\\setlength{\\LTpost}{0pt}\n"
|
||||
f"\\setlength{{\\LTpre}}{{{lt_glue}}}\n"
|
||||
f"\\setlength{{\\LTpost}}{{{lt_glue}}}\n"
|
||||
+ self.tex_position() +
|
||||
# 2 \tabcolsep per column, minus the ones @{} removes at
|
||||
# flush (lineless) edges.
|
||||
f"\\setlength{{\\tablewidth}}{{\\textwidth - "
|
||||
# flush (lineless) edges. \linewidth, not \textwidth: in
|
||||
# a multicols column or a minipage the table must fit the
|
||||
# column it is set in (the two are equal in plain text).
|
||||
f"\\setlength{{\\tablewidth}}{{\\linewidth - "
|
||||
f"{2 * self.row_size - self.flush_l - self.flush_r}\\tabcolsep}}\n"
|
||||
# Fill widths need \tablewidth and must precede the
|
||||
# measuring \savebox, whose column spec reads them.
|
||||
# Column widths precede the fill widths, which read
|
||||
# them; both precede the measuring \savebox, whose
|
||||
# column spec reads them.
|
||||
+ self.tex_column_widths()
|
||||
+ self.tex_fill_widths()
|
||||
+ result)
|
||||
result = re.sub(r"\newline", r"\\\\", result)
|
||||
@@ -907,16 +965,46 @@ class Table(klammer_base.Klammer_base):
|
||||
left = space // 2
|
||||
return txt_util.NBSP * left + cell + txt_util.NBSP * (space - left)
|
||||
return cell + txt_util.NBSP * space
|
||||
# A cell's type style, as make_cells() assigns it for html and tex:
|
||||
# the column's :style code, overridden by :header_style in the
|
||||
# header row. Plain text has one face, so only the styles a
|
||||
# terminal can show are carried -- italic and bold, as the markers
|
||||
# the ansi target resolves and the txt target strips (phases.py);
|
||||
# the other codes (roman, sans, typewriter, code) style nothing.
|
||||
STYLE = {"i": "italic", "b": "bold"}
|
||||
def styled(cell, row_i, cell_i):
|
||||
code = self.header_style if row_i == 0 and self.header else self.style[cell_i]
|
||||
if cell and code in STYLE:
|
||||
return (phases.txt_style_marker(STYLE[code]) + cell
|
||||
+ phases.txt_style_marker(STYLE[code], False))
|
||||
return cell
|
||||
widths = self.txt_column_widths()
|
||||
pos = [hpos(p) for p in self.cell_hpos]
|
||||
result = ""
|
||||
indent = txt_util.NBSP * int(self.Target_txt_indent)
|
||||
colsep = " "
|
||||
for row in self.rows:
|
||||
# Horizontal lines are the same rule as the other targets (:hline,
|
||||
# defaulting to under the header row and at the bottom when the
|
||||
# table has a header): a rule of hyphens the width of the table at
|
||||
# each boundary that has a line. A line over part of the columns
|
||||
# is drawn full width here -- a partial rule in a plain-text table
|
||||
# reads as an accident. Vertical lines have no plain-text form.
|
||||
# The hyphens are HIDDEN (KTESC markers): a klammer's result is
|
||||
# re-read as Klammertext and the txt target's dash transforms turn
|
||||
# "--" into "-", so a bare run of hyphens came out a fraction of
|
||||
# its length. (The caption rule escapes this by being drawn in a
|
||||
# phase, after the transforms.)
|
||||
rule = (indent + klammer_base.hide_typographic(
|
||||
"-" * (sum(widths) + len(colsep) * (len(widths) - 1))) + "\n")
|
||||
for row_i, row in enumerate(self.rows):
|
||||
if row_i in self.s_hline.by_index:
|
||||
result += rule
|
||||
result += indent + colsep.join(
|
||||
[pad(elt, width, align) for elt, width, align in
|
||||
list(zip(row, widths, pos))])
|
||||
[pad(styled(elt, row_i, cell_i), width, align)
|
||||
for cell_i, (elt, width, align) in enumerate(zip(row, widths, pos))])
|
||||
result += "\n"
|
||||
if len(self.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))
|
||||
|
||||
+77
-11
@@ -1,7 +1,7 @@
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
import kutil
|
||||
import klammer_base
|
||||
|
||||
# Emitted by @vspace.txt (sks/block/block.k), one marker per line of space.
|
||||
# An @eval result consisting only of whitespace is trimmed away by the
|
||||
@@ -25,6 +25,75 @@ def txt_vspace_marker():
|
||||
def txt_nbsp_marker():
|
||||
return "__KTEXTNBSP__"
|
||||
|
||||
# The type-style markers, emitted by @b.txt, @i.txt, @u.txt and @ri.txt
|
||||
# (sks/font/font.k) around their text. Plain text has no bold, but a
|
||||
# terminal does: the txt target STRIPS these in its last phase
|
||||
# (txt_strip_styles) and the ansi target, which :includes txt, RESOLVES
|
||||
# them to SGR escape sequences in its last phase (ansi_resolve_styles).
|
||||
# So the two targets differ in nothing but that phase, and stripping the
|
||||
# escape sequences from an ansi render gives the txt render byte for byte
|
||||
# (sks/tst/ansi_test.sh pins it). Every phase before the last measures
|
||||
# them as ZERO width -- txt_visible_length below, which txt_wrap and the
|
||||
# table layout use -- or a bold word would cost thirteen columns of line
|
||||
# that the reader never sees.
|
||||
#
|
||||
# The OFF sequences are the specific resets (22 ends bold, 23 italic, 24
|
||||
# underline), never "\033[0m": @b @i x @ y @ must leave y bold, and a
|
||||
# blanket reset would cancel the outer style with the inner one.
|
||||
TXT_STYLES = {
|
||||
"bold": ("\033[1m", "\033[22m"),
|
||||
"italic": ("\033[3m", "\033[23m"),
|
||||
"underline": ("\033[4m", "\033[24m"),
|
||||
}
|
||||
|
||||
def txt_style_marker(style, on=True):
|
||||
if style not in TXT_STYLES:
|
||||
raise ValueError(f"unknown type style {style!r}; one of {sorted(TXT_STYLES)}")
|
||||
return f"__KTEXT{style.upper()}{'' if on else 'OFF'}__"
|
||||
|
||||
_STYLE_MARKER_RGX = re.compile(
|
||||
"|".join(re.escape(txt_style_marker(s, on))
|
||||
for s in TXT_STYLES for on in (True, False)))
|
||||
|
||||
def txt_strip_styles(text, K):
|
||||
return _STYLE_MARKER_RGX.sub("", text)
|
||||
|
||||
def ansi_resolve_styles(text, K):
|
||||
for style, (on, off) in TXT_STYLES.items():
|
||||
text = text.replace(txt_style_marker(style), on)
|
||||
text = text.replace(txt_style_marker(style, False), off)
|
||||
return text
|
||||
|
||||
def txt_visible_length(s):
|
||||
"""How many columns s occupies once its markers resolve: a KTESC
|
||||
marker is one glyph, an nbsp marker one space, a style marker nothing.
|
||||
len() is the wrong measure for anything laid out (see txt_util)."""
|
||||
s = _STYLE_MARKER_RGX.sub("", s)
|
||||
return len(klammer_base.unescape_ktesc(s).replace(txt_nbsp_marker(), " "))
|
||||
|
||||
def txt_wrap(text, width, subsequent_indent=""):
|
||||
"""textwrap.wrap(text, width, break_long_words=False) measured by
|
||||
VISIBLE width. textwrap counts characters, so a marker inside a word
|
||||
-- a bold word carries a thirteen-character marker at each end -- made
|
||||
it break lines a dozen columns early. Whitespace runs are kept as
|
||||
written (sentence spacing survives), a word longer than the measure
|
||||
stands on its own line, and no word is broken at a hyphen."""
|
||||
chunks = re.split(r"(\s+)", text.strip())
|
||||
lines, line, sep = [], "", ""
|
||||
for i in range(0, len(chunks), 2):
|
||||
word = chunks[i]
|
||||
if word:
|
||||
if line and (txt_visible_length(line) + len(sep)
|
||||
+ txt_visible_length(word) > width):
|
||||
lines.append(line)
|
||||
line = subsequent_indent + word
|
||||
else:
|
||||
line += sep + word
|
||||
sep = " " * len(chunks[i + 1]) if i + 1 < len(chunks) else ""
|
||||
if line:
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
# The tex line break, carried as a marker.
|
||||
#
|
||||
# @nl.tex used to emit "\\newline" directly, and that is destroyed the moment
|
||||
@@ -92,8 +161,8 @@ def txt_justify_blocks(text, K):
|
||||
nbsp = txt_nbsp_marker()
|
||||
if par and par[0] != ' ' and not par.startswith(nbsp):
|
||||
label = re.match("(?:" + re.escape(nbsp) + r")*\[\d+\] ", par)
|
||||
par = "\n".join(textwrap.wrap(
|
||||
par, width=int(K.Target_txt_width), break_long_words=False,
|
||||
par = "\n".join(txt_wrap(
|
||||
par, int(K.Target_txt_width),
|
||||
subsequent_indent=" " * len(label.group(0)) if label else ""))
|
||||
result += par + "\n\n"
|
||||
result = re.sub(" *" + txt_vspace_marker() + " *", "\n", result)
|
||||
@@ -121,15 +190,12 @@ def txt_insert_captions(text, K):
|
||||
caption = f"{name} {caption_number} - {text}"
|
||||
else:
|
||||
caption = text
|
||||
caption_width = len(caption) + indent
|
||||
if len(caption) > indented_width:
|
||||
caption = textwrap.wrap(
|
||||
# Visible widths throughout: a caption may carry style markers.
|
||||
if txt_visible_length(caption) > indented_width:
|
||||
caption = "\n".join(txt_wrap(
|
||||
caption, indented_width,
|
||||
#initial_indent ="~"*indent,
|
||||
subsequent_indent=txt_nbsp_marker()*indent)
|
||||
caption = "\n".join(caption)
|
||||
caption_width = indented_width
|
||||
line_length = max([len(e) for e in caption.split("\n")])
|
||||
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
|
||||
result = caption_rgx.sub(replace, text)
|
||||
|
||||
+22
-1
@@ -65,7 +65,28 @@ of this: neither the transforms nor the quoting reach it.
|
||||
:after_apply
|
||||
phases.txt_format_footnotes ;
|
||||
phases.txt_insert_captions ;
|
||||
phases.txt_justify_blocks
|
||||
phases.txt_justify_blocks ;
|
||||
phases.txt_strip_styles
|
||||
@@@
|
||||
|
||||
# Plain text for a TERMINAL: the txt target's output with the type styles
|
||||
# a terminal can show (@b, @i, @u) as SGR escape sequences. Named for its
|
||||
# FORMAT, not its destination -- a file of it is what "less -R" shows, and
|
||||
# "ktext -t txt -d | less" is a stream that must stay plain -- so the
|
||||
# writer picks it with -t, as they pick pdf over tex; kdesc picks it for
|
||||
# its own output when stdout is a terminal. It includes txt and defines no
|
||||
# klammers: @b.txt and its siblings emit style markers, which the txt
|
||||
# target's last phase strips and this target's last phase resolves. So
|
||||
# the two renders are equal once the escape sequences are removed
|
||||
# (sks/tst/ansi_test.sh), and only the last phase differs. Phases are
|
||||
# not inherited through :includes (see pdf), hence the restated list.
|
||||
@@@target ansi | Plain text with ANSI escape sequences for type styles
|
||||
:includes txt
|
||||
:after_apply
|
||||
phases.txt_format_footnotes ;
|
||||
phases.txt_insert_captions ;
|
||||
phases.txt_justify_blocks ;
|
||||
phases.ansi_resolve_styles
|
||||
@@@
|
||||
|
||||
@@@target html | HTML page
|
||||
|
||||
@@ -44,7 +44,7 @@ def visible_length(s):
|
||||
escape sequences as visible width, and the reason escape resolution
|
||||
runs BEFORE the txt phases.)
|
||||
"""
|
||||
return len(klammer_base.unescape_ktesc(s).replace(NBSP, " "))
|
||||
return phases.txt_visible_length(s)
|
||||
|
||||
def indent_text(text, indent):
|
||||
"""Every line of text prefixed with `indent` columns of NBSP."""
|
||||
|
||||
+1
-1
@@ -169,7 +169,7 @@ shows "34. without -i the klammerset is shown" '@table' "$KDESC" -k
|
||||
DERIVE=$(mktemp /tmp/kdesc_derive.XXXXXX.k)
|
||||
printf '@@wave.k s : Wave @@\n@@wave :: @i *s* @ @@\n' > "$DERIVE"
|
||||
shows "35. coverage is derived through the unlisted klammerset" \
|
||||
'@wave +html pdf tex txt +from @i' "$KDESC" -i "$DERIVE" --coverage
|
||||
'@wave +ansi html pdf tex txt +from @i' "$KDESC" -i "$DERIVE" --coverage
|
||||
absent "36. ... and the klammerset is not reported" '@table' "$KDESC" -i "$DERIVE" --coverage
|
||||
# A klammer the input REDEFINES came from the klammerset, but the input
|
||||
# changed it -- which is exactly what a reader needs to know.
|
||||
|
||||
Reference in new issue
Block a user