Width and height argtypes, kdesc --argtypes from ref/, txt @define/@left_right/@columns, kdiag --check value validation (from dev d6993110e9b3)
Also: the klammertext argtype (descriptions kept as source), txt table wrapping and :hpos/:offset, @c between ‹ ›, txt section headings, the ktext newline-only trim, capitalize(), and the argtype test suite. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
9b0d0d7897
commit
0ead40f272
39 files changed
+734
-158
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
|
Report problems (or send patches) to the author; accepted changes are
|
||||||
applied to the development tree and appear in a following snapshot.
|
applied to the development tree and appear in a following snapshot.
|
||||||
|
|
||||||
This snapshot was assembled from development commit `fcec5e2e77ac`.
|
This snapshot was assembled from development commit `d6993110e9b3`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -177,6 +177,14 @@ void show_unicode_examples()
|
|||||||
std::cout << "\n" << M.apply(text_target()) << "\n\n";
|
std::cout << "\n" << M.apply(text_target()) << "\n\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void show_argtype_definitions()
|
||||||
|
{
|
||||||
|
Machine M;
|
||||||
|
M.read(M.m_state.value("KLAMMERTEXT_HOME") + "/ref/ref.k");
|
||||||
|
M.read_string("@argtype_definitions@");
|
||||||
|
std::cout << "\n" << M.apply(text_target()) << "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -224,10 +232,9 @@ int main(int argc, char* argv[])
|
|||||||
show_unicode_examples();
|
show_unicode_examples();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (p("argtypes")) {
|
if (p("argtypes")) {
|
||||||
Argtype_registry argtypes;
|
show_argtype_definitions();
|
||||||
std::cout << boldblack << "\nStandard klammer argument types\n" << black;
|
|
||||||
std::cout << argtypes.describe() << "\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.given("katoms")) {
|
if (args.given("katoms")) {
|
||||||
|
|||||||
+6
-1
@@ -70,7 +70,12 @@ int main(int argc, char* argv[])
|
|||||||
M.read(fs::path(p));
|
M.read(fs::path(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string result = trim(M.apply(target));
|
// Trim NEWLINES only, as Machine::apply itself does: a leading space
|
||||||
|
// is content under txt -- the indent of a block that opens the
|
||||||
|
// document (a table, a definition list) -- and trim() took it off
|
||||||
|
// the first line alone, every other line keeping its indent
|
||||||
|
// (TODO #60, 2026-09-13).
|
||||||
|
std::string result = trim_char(M.apply(target), '\n');
|
||||||
if (display_only && !result.empty()) {
|
if (display_only && !result.empty()) {
|
||||||
std::cout << result << "\n";
|
std::cout << result << "\n";
|
||||||
} else if (!result.empty()) {
|
} else if (!result.empty()) {
|
||||||
|
|||||||
+18
-13
@@ -51,10 +51,12 @@ shows as `name.type default`, an option as `:name.type default`.
|
|||||||
kdiag --process --check doc.kt
|
kdiag --process --check doc.kt
|
||||||
```
|
```
|
||||||
|
|
||||||
reports every klammer application whose name, argument count, option names, or
|
reports every klammer application whose name, argument count, option names,
|
||||||
target coverage is wrong — including inside `@cond` branches this render will
|
target coverage, or **literal argument value** is wrong (a value that fails
|
||||||
not take and inside klammer bodies never applied. It applies nothing. Use it
|
its argument type, such as `:width .5h`) — including inside `@cond` branches
|
||||||
before claiming a document is correct.
|
this render will not take and inside klammer bodies never applied. A value
|
||||||
|
holding a nested klammer or a `*variable*` is computed at run time and is not
|
||||||
|
judged. It applies nothing. Use it before claiming a document is correct.
|
||||||
|
|
||||||
**Inspect how text is parsed:** `kdiag --type '<text>'` shows the katoms and
|
**Inspect how text is parsed:** `kdiag --type '<text>'` shows the katoms and
|
||||||
their types; `kdiag --args '<text>'` shows how text splits into arguments;
|
their types; `kdiag --args '<text>'` shows how text splits into arguments;
|
||||||
@@ -227,8 +229,8 @@ which reads `kt/preface.kt`, `kt/one.kt`, `kt/two.kt` beside the root file.
|
|||||||
ktext doc.kt -t pdf # also: -t html, -t tex
|
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 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
|
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
|
# example errors under txt/ansi: @image has no txt
|
||||||
# txt definition (§5), and coverage is never silently degraded
|
# definition (§5), and coverage is never silently degraded
|
||||||
ktext -s '@i-x' -t html -d # -s takes a string; no file needed
|
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:
|
ktext doc.kt -s '@@version : 3.2.1 @@' -t pdf # -s is processed BEFORE the files:
|
||||||
# define a klammer the document uses
|
# define a klammer the document uses
|
||||||
@@ -274,11 +276,12 @@ Bar depth is the dimension: one `|` separates items, two `||` separates rows.
|
|||||||
Argument order is free for options, fixed for positionals.
|
Argument order is free for options, fixed for positionals.
|
||||||
|
|
||||||
**Coverage today** (`kdesc --coverage`; 89 klammers): html, tex and pdf cover
|
**Coverage today** (`kdesc --coverage`; 89 klammers): html, tex and pdf cover
|
||||||
nearly all. **txt and ansi cover 37**, among them `@b @i @t @u @s @r @ri @ts
|
nearly all. **txt and ansi cover 40**, among them `@b @i @t @u @s @r @ri @ts
|
||||||
@typeface`, `@s1`–`@s7`, `@par @newpage @vspace @vfill @nl`, `@indent @right
|
@typeface`, `@s1`–`@s7`, `@par @newpage @vspace @vfill @nl`, `@indent @right
|
||||||
@lines @qa`, `@ul @ol`, `@table @tbl @reference`, `@footnote @link`, `@date
|
@lines @qa`, `@ul @ol`, `@table @tbl @reference @define @left_right`,
|
||||||
@datetime`. Not for txt: `@c @code @source_listing @image @image_grid @quote
|
`@footnote @link`, `@date @datetime`, `@c`. Not for txt: `@code
|
||||||
@note @center @block @columns @multicolumn @define @sup @sub @color @h1`–`@h7`.
|
@source_listing @image @image_grid @quote @note @center @block @columns
|
||||||
|
@multicolumn @sup @sub @color @h1`–`@h7`.
|
||||||
`@u` has no html definition. A klammer applied under a target that lacks it is
|
`@u` has no html definition. A klammer applied under a target that lacks it is
|
||||||
an **error**, never silently dropped.
|
an **error**, never silently dropped.
|
||||||
|
|
||||||
@@ -328,7 +331,7 @@ the range (here: one sum per column).
|
|||||||
| `html` | a directory `<name>/` with `index.html`, `css/`, `fonts/`, `js/` | self-contained, fonts localized; `book` structure adds navigation, search |
|
| `html` | a directory `<name>/` with `index.html`, `css/`, `fonts/`, `js/` | self-contained, fonts localized; `book` structure adds navigation, search |
|
||||||
| `tex` | `<name>.tex` | XeLaTeX source; fonts via fontspec from the font store |
|
| `tex` | `<name>.tex` | XeLaTeX source; fonts via fontspec from the font store |
|
||||||
| `pdf` | `<name>.pdf` | `tex` plus a xelatex run (TeX Live required) |
|
| `pdf` | `<name>.pdf` | `tex` plus a xelatex run (TeX Live required) |
|
||||||
| `txt` | `<name>.txt` | plain text filled to `Target_txt_width` (72), blocks indented by `Target_txt_indent` (3); tables ruled and padded; captions numbered; **37 of 89 klammers** (§5) |
|
| `txt` | `<name>.txt` | plain text filled to `Target_txt_width` (72), blocks indented by `Target_txt_indent` (3); tables ruled and padded; captions numbered; **40 of 89 klammers** (§5) |
|
||||||
| `ansi` | txt + SGR escape sequences | `@b`/`@i`/`@u` and table header styles shown by a terminal; **identical to txt once the escape sequences are stripped**; pick it with `-t ansi -d`; a terminal needs a font with an italic face (macOS Terminal: Menlo, not Monaco) |
|
| `ansi` | txt + SGR escape sequences | `@b`/`@i`/`@u` and table header styles shown by a terminal; **identical to txt once the escape sequences are stripped**; pick it with `-t ansi -d`; a terminal needs a font with an italic face (macOS Terminal: Menlo, not Monaco) |
|
||||||
|
|
||||||
`k`, `o`, `*` also appear in `kdesc -t`: they are declaration pseudo-targets
|
`k`, `o`, `*` also appear in `kdesc -t`: they are declaration pseudo-targets
|
||||||
@@ -368,7 +371,9 @@ ones. CJK is deliberately not shipped.
|
|||||||
- Separators: `:` create, `::` create inheriting parameters, `:::` override an
|
- Separators: `:` create, `::` create inheriting parameters, `:::` override an
|
||||||
existing definition, `::::` define a default a later `:` may replace.
|
existing definition, `::::` define a default a later `:` may replace.
|
||||||
- Argument types: `name.type` — `string` (default), `int`, `float`, `bool`,
|
- Argument types: `name.type` — `string` (default), `int`, `float`, `bool`,
|
||||||
`list`, `length`, `literal` (raw content, named close required), `rest`
|
`list`, `width`/`height` (a bare number = characters / lines in every
|
||||||
|
target; units `em pt px`, `w pw` for widths, `h ph` for heights; the axes
|
||||||
|
are checked), `literal` (raw content, named close required), `rest`
|
||||||
(everything left, bars included; `rest(2)` = list of lists), and any
|
(everything left, bars included; `rest(2)` = list of lists), and any
|
||||||
`@@@argtype` the klammerset declares (`kdesc --argtypes`). An argtype may
|
`@@@argtype` the klammerset declares (`kdesc --argtypes`). An argtype may
|
||||||
carry `:default` and `:alone`; a klammer may override the default, never the
|
carry `:default` and `:alone`; a klammer may override the default, never the
|
||||||
@@ -451,7 +456,7 @@ ones. CJK is deliberately not shipped.
|
|||||||
`-c`/`-u` render through `ansi` on a terminal and `txt` in a pipe.
|
`-c`/`-u` render through `ansi` on a terminal and `txt` in a pipe.
|
||||||
|
|
||||||
**When something is wrong:** `kdiag --process --check doc.kt` first (name,
|
**When something is wrong:** `kdiag --process --check doc.kt` first (name,
|
||||||
arity, option and coverage faults without rendering); then `kdesc -k <klammer>`
|
arity, option, literal-value and coverage faults without rendering); then `kdesc -k <klammer>`
|
||||||
against what was written; then `kdiag --type '<fragment>'` if the question is
|
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
|
how something is parsed; then `ktext … -v 1` if the question is which file or
|
||||||
klammerset was used.
|
klammerset was used.
|
||||||
|
|||||||
@@ -13,7 +13,10 @@
|
|||||||
|
|
||||||
Parameter_set& Argtype_registry::parameters()
|
Parameter_set& Argtype_registry::parameters()
|
||||||
{
|
{
|
||||||
static Parameter_set instance("name | desc :pattern .* :python_cast str :default :alone");
|
// desc.klammertext: the description is Klammertext, kept exactly as
|
||||||
|
// written (Argtype::m_desc) for a Machine to render -- kdesc's listing,
|
||||||
|
// the Handbook. Plain-text displays (an argument error) unquote it.
|
||||||
|
static Parameter_set instance("name | desc.klammertext :pattern .* :python_cast str :default :alone");
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +208,8 @@ std::string Argtype_registry::describe(bool long_form, int indent_width) const
|
|||||||
int pat_width = name_width + 2 + indent_width + label.size();
|
int pat_width = name_width + 2 + indent_width + label.size();
|
||||||
result << indent << std::setw(name_width) << name << sp_arrow;
|
result << indent << std::setw(name_width) << name << sp_arrow;
|
||||||
if (long_form) {
|
if (long_form) {
|
||||||
|
// The stored text, as written: this is the programmer's dump
|
||||||
|
// (kdiag --machine), and a quoted "^@" in it is the fact.
|
||||||
result << m_types.at(name).m_desc << "\n";
|
result << m_types.at(name).m_desc << "\n";
|
||||||
result << std::setw(pat_width) << "regex: " << m_types.at(name).m_symbolic_pattern;
|
result << std::setw(pat_width) << "regex: " << m_types.at(name).m_symbolic_pattern;
|
||||||
if (m_types.at(name).m_symbolic_pattern != m_types.at(name).m_pattern)
|
if (m_types.at(name).m_symbolic_pattern != m_types.at(name).m_pattern)
|
||||||
|
|||||||
+13
-2
@@ -27,6 +27,7 @@ public:
|
|||||||
Argtype get(const std::string& name, const Locator& loc) const;
|
Argtype get(const std::string& name, const Locator& loc) const;
|
||||||
|
|
||||||
std::string eval(const std::string& value, const std::string& type_name, const Locator& loc);
|
std::string eval(const std::string& value, const std::string& type_name, const Locator& loc);
|
||||||
|
|
||||||
std::string describe(bool long_form=false, int indent_width=4) const;
|
std::string describe(bool long_form=false, int indent_width=4) const;
|
||||||
|
|
||||||
inline bool is_defined(const std::string& name) const {
|
inline bool is_defined(const std::string& name) const {
|
||||||
@@ -51,7 +52,7 @@ std::vector<std::tuple<std::string, std::string, std::string, std::string, modif
|
|||||||
R"([^\s]+)", "str",
|
R"([^\s]+)", "str",
|
||||||
pyformat_string },
|
pyformat_string },
|
||||||
|
|
||||||
{ "bool", "a Boolean value of 'false', 'False', '0', 'true', 'True', or '1'",
|
{ "bool", "a Boolean value of @c false c@, @c False c@, @c 0 c@, @c true c@, @c True c@, or @c 1 c@",
|
||||||
R"(0|1|true|false|True|False)",
|
R"(0|1|true|false|True|False)",
|
||||||
"(lambda b : True if b not in {'0','false','False'} else False)",
|
"(lambda b : True if b not in {'0','false','False'} else False)",
|
||||||
pyformat_bool },
|
pyformat_bool },
|
||||||
@@ -82,11 +83,21 @@ std::vector<std::tuple<std::string, std::string, std::string, std::string, modif
|
|||||||
|
|
||||||
{ "rest", "the remaining arguments as a list nested to N dimensions "
|
{ "rest", "the remaining arguments as a list nested to N dimensions "
|
||||||
"(rest(N)); the delimiter for dimension n is a run of n bar "
|
"(rest(N)); the delimiter for dimension n is a run of n bar "
|
||||||
"characters, so | separates elements and || lists of elements",
|
"characters, so ^| separates elements and ^|^| lists of elements",
|
||||||
R"((?:.|\n)*)", R"((lambda s : __import__("kutil").rest_split(s, N)))",
|
R"((?:.|\n)*)", R"((lambda s : __import__("kutil").rest_split(s, N)))",
|
||||||
nullptr },
|
nullptr },
|
||||||
|
|
||||||
{ "literal", "literal text passed without interpretation",
|
{ "literal", "literal text passed without interpretation",
|
||||||
R"((?:.|\n)*)", "str",
|
R"((?:.|\n)*)", "str",
|
||||||
pyformat_string },
|
pyformat_string },
|
||||||
|
|
||||||
|
// Klammertext SOURCE, exactly as written -- quoting carets included --
|
||||||
|
// for a parameter whose value will be handed to a Machine to render
|
||||||
|
// later (an @@@argtype's description). Unlike "literal", whose content
|
||||||
|
// has its "^" forms processed as the value is built, this keeps them:
|
||||||
|
// "^@date" stays "^@date", so it re-reads as the text "@date" rather
|
||||||
|
// than as a klammer application. (2026-09-13)
|
||||||
|
{ "klammertext", "Klammertext source, kept exactly as written",
|
||||||
|
R"((?:.|\n)*)", "str",
|
||||||
|
pyformat_string },
|
||||||
};
|
};
|
||||||
+11
-2
@@ -414,7 +414,16 @@ Parameter_set::value_map(
|
|||||||
check_positional(positional, loc);
|
check_positional(positional, loc);
|
||||||
for (size_t i = 0; i < m_positional.size(); ++i) {
|
for (size_t i = 0; i < m_positional.size(); ++i) {
|
||||||
auto param = m_positional[i];
|
auto param = m_positional[i];
|
||||||
std::string arg = as_string(positional[i].begin(), positional[i].end(), true);
|
// A "klammertext" parameter holds Klammertext SOURCE -- the text as
|
||||||
|
// written, quoting carets included -- so that whoever displays it can
|
||||||
|
// hand it to a Machine (an @@@argtype's description, 2026-09-13).
|
||||||
|
// The katomizer strips the caret of a quoted special into m_text, so
|
||||||
|
// the plain join would turn "^@date" into "@date", which re-reads as
|
||||||
|
// a klammer application. A "literal" parameter keeps the plain join:
|
||||||
|
// its "^" forms ARE processed (a "^@" in @c prints "@").
|
||||||
|
std::string arg = param.m_argtype.m_name == "klammertext"
|
||||||
|
? as_source(positional[i].begin(), positional[i].end(), true)
|
||||||
|
: as_string(positional[i].begin(), positional[i].end(), true);
|
||||||
values[param.m_name] = arg;
|
values[param.m_name] = arg;
|
||||||
}
|
}
|
||||||
auto optional_values = check_optional(optional, loc);
|
auto optional_values = check_optional(optional, loc);
|
||||||
@@ -483,7 +492,7 @@ void Parameter_set::validate(
|
|||||||
// A .k description is prose: collapse its source layout (line
|
// A .k description is prose: collapse its source layout (line
|
||||||
// breaks, continuation indentation) so it re-flows with the
|
// breaks, continuation indentation) so it re-flows with the
|
||||||
// sentence -- indented .k lines would otherwise read as verbatim.
|
// sentence -- indented .k lines would otherwise read as verbatim.
|
||||||
<< collapse_whitespace(argtype.m_desc) << "\n";
|
<< collapse_whitespace(unquote_punctuation(argtype.m_desc)) << "\n";
|
||||||
throw Argument_error(ss.str(), loc);
|
throw Argument_error(ss.str(), loc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#include "check.h"
|
#include "check.h"
|
||||||
|
#include "error.h"
|
||||||
#include "machine.h"
|
#include "machine.h"
|
||||||
#include "katom.h"
|
#include "katom.h"
|
||||||
#include "katom_process.h"
|
#include "katom_process.h"
|
||||||
@@ -23,12 +24,32 @@ namespace {
|
|||||||
// check time nothing has been reduced, so the nesting is still present and
|
// check time nothing has been reduced, so the nesting is still present and
|
||||||
// must be tracked. (The depth-0 rule is the same one cond_separator_bars()
|
// must be tracked. (The depth-0 rule is the same one cond_separator_bars()
|
||||||
// applies for @cond; see doc/cond_evaluation_order.md.)
|
// applies for @cond; see doc/cond_evaluation_order.md.)
|
||||||
|
// A value as written, and whether it is LITERAL: made of nothing but text
|
||||||
|
// and whitespace at depth 0, so that what the writer typed is what the
|
||||||
|
// argument type will see. A value holding a nested klammer, a *variable*,
|
||||||
|
// a quoted special or a caret form is computed at run time and cannot be
|
||||||
|
// judged here -- the static-structure rule: check what is fixed, skip what
|
||||||
|
// is computed.
|
||||||
|
struct Argument_value
|
||||||
|
{
|
||||||
|
std::string m_text {};
|
||||||
|
bool m_literal { true };
|
||||||
|
};
|
||||||
|
|
||||||
struct Application_shape
|
struct Application_shape
|
||||||
{
|
{
|
||||||
int m_positional { 0 };
|
int m_positional { 0 };
|
||||||
std::vector<std::string> m_options {};
|
std::vector<std::string> m_options {};
|
||||||
|
std::vector<Argument_value> m_positional_values {};
|
||||||
|
std::vector<std::pair<std::string, Argument_value>> m_option_values {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bool is_literal_katom(const Katom& k)
|
||||||
|
{
|
||||||
|
return k.m_type == katom_t::text || k.m_type == katom_t::word
|
||||||
|
|| k.m_type == katom_t::space || k.m_type == katom_t::newline;
|
||||||
|
}
|
||||||
|
|
||||||
bool is_boundary_katom(const Katom& k)
|
bool is_boundary_katom(const Katom& k)
|
||||||
{
|
{
|
||||||
return k.m_type == katom_t::bar || k.m_type == katom_t::option_name;
|
return k.m_type == katom_t::bar || k.m_type == katom_t::option_name;
|
||||||
@@ -49,14 +70,36 @@ Application_shape application_shape(katom_list::const_iterator begin, katom_list
|
|||||||
bool in_positional = first->m_type != katom_t::option_name;
|
bool in_positional = first->m_type != katom_t::option_name;
|
||||||
if (in_positional) shape.m_positional = 1;
|
if (in_positional) shape.m_positional = 1;
|
||||||
|
|
||||||
|
// The value being accumulated: a positional part, or the option named
|
||||||
|
// last. Closed at each depth-0 boundary and at the end.
|
||||||
|
Argument_value current {};
|
||||||
|
bool current_is_option = !in_positional;
|
||||||
|
std::string current_option {};
|
||||||
|
auto close = [&]() {
|
||||||
|
current.m_text = trim(current.m_text);
|
||||||
|
if (current_is_option) {
|
||||||
|
shape.m_option_values.emplace_back(current_option, current);
|
||||||
|
} else {
|
||||||
|
shape.m_positional_values.push_back(current);
|
||||||
|
}
|
||||||
|
current = Argument_value {};
|
||||||
|
};
|
||||||
|
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
for (auto k = first; k != end; ++k) {
|
for (auto k = first; k != end; ++k) {
|
||||||
if (depth == 0 && is_boundary_katom(*k)) {
|
if (depth == 0 && is_boundary_katom(*k)) {
|
||||||
|
close();
|
||||||
if (k->m_type == katom_t::bar) {
|
if (k->m_type == katom_t::bar) {
|
||||||
++shape.m_positional;
|
++shape.m_positional;
|
||||||
|
current_is_option = false;
|
||||||
} else {
|
} else {
|
||||||
shape.m_options.push_back(k->m_text.substr(1));
|
shape.m_options.push_back(k->m_text.substr(1));
|
||||||
|
current_is_option = true;
|
||||||
|
current_option = k->m_text.substr(1);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (depth > 0 || !is_literal_katom(*k)) current.m_literal = false;
|
||||||
|
current.m_text += k->m_text;
|
||||||
}
|
}
|
||||||
if (level_increase(*k)) {
|
if (level_increase(*k)) {
|
||||||
++depth;
|
++depth;
|
||||||
@@ -64,6 +107,7 @@ Application_shape application_shape(katom_list::const_iterator begin, katom_list
|
|||||||
--depth;
|
--depth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
close();
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +203,28 @@ void Checker::check_application(
|
|||||||
error(ss.str(), context, begin->m_loc);
|
error(ss.str(), context, begin->m_loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Argument VALUES against their argument types -- the check that finds
|
||||||
|
// a height unit on a width, a word where a number is wanted -- for every
|
||||||
|
// value that is literal (see Argument_value). The same test the engine
|
||||||
|
// applies when the klammer is applied, Parameter_set::validate(), so the
|
||||||
|
// two can never disagree; its exception becomes a diagnostic here.
|
||||||
|
auto validate = [&](const Parameter& parameter, const Argument_value& value) {
|
||||||
|
if (!value.m_literal || value.m_text.empty()) return;
|
||||||
|
try {
|
||||||
|
Parameter_set::validate(parameter, value.m_text, begin->m_loc);
|
||||||
|
} catch (const Argument_error& e) {
|
||||||
|
error("@" + name + ": " + collapse_whitespace(e.m_desc), context, begin->m_loc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < shape.m_positional_values.size() && i < parameters.m_positional.size(); i++) {
|
||||||
|
validate(parameters.m_positional[i], shape.m_positional_values[i]);
|
||||||
|
}
|
||||||
|
for (const auto& [option, value] : shape.m_option_values) {
|
||||||
|
if (const Parameter* parameter = parameters.find(option)) {
|
||||||
|
validate(*parameter, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> seen {};
|
std::vector<std::string> seen {};
|
||||||
for (const auto& option : shape.m_options) {
|
for (const auto& option : shape.m_options) {
|
||||||
if (std::ranges::count(parameters.m_optional_names, option) == 0) {
|
if (std::ranges::count(parameters.m_optional_names, option) == 0) {
|
||||||
|
|||||||
@@ -454,6 +454,13 @@ strings_t get_sks_directories(const std::string& s, bool include_argument)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string symbolic_klammertext_filename(std::string filename)
|
||||||
|
{
|
||||||
|
return string_replace(
|
||||||
|
filename, get_env_var("KLAMMERTEXT_HOME"), "$KLAMMERTEXT_HOME");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
std::string cache_directory(const std::string& subdirectory, std::string parent_directory)
|
std::string cache_directory(const std::string& subdirectory, std::string parent_directory)
|
||||||
{
|
{
|
||||||
if (parent_directory.empty()) {
|
if (parent_directory.empty()) {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ fs::path klammertext_filename(
|
|||||||
const std::string& basename, bool error_if_missing=true, bool make_directory_if_missing=false);
|
const std::string& basename, bool error_if_missing=true, bool make_directory_if_missing=false);
|
||||||
std::vector<std::string> sks_dirs();
|
std::vector<std::string> sks_dirs();
|
||||||
std::vector<std::string> get_sks_directories(const std::string& s, bool include_argument=true);
|
std::vector<std::string> get_sks_directories(const std::string& s, bool include_argument=true);
|
||||||
|
std::string symbolic_klammertext_filename(std::string filename);
|
||||||
std::string cache_directory(const std::string& subdirectory, std::string parent_directory="");
|
std::string cache_directory(const std::string& subdirectory, std::string parent_directory="");
|
||||||
std::time_t to_time_t(const fs::file_time_type& ftime);
|
std::time_t to_time_t(const fs::file_time_type& ftime);
|
||||||
bool in_modification_order(std::string filename1, std::string filename2);
|
bool in_modification_order(std::string filename1, std::string filename2);
|
||||||
|
|||||||
@@ -140,6 +140,18 @@ std::string as_string(const katom_list& ks, bool strip_whitespace)
|
|||||||
return as_string(ks.begin(), ks.end(), strip_whitespace);
|
return as_string(ks.begin(), ks.end(), strip_whitespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string as_source(std::vector<Katom>::const_iterator begin, std::vector<Katom>::const_iterator end, bool strip_whitespace)
|
||||||
|
{
|
||||||
|
std::string result {};
|
||||||
|
for (auto ki = begin; ki < end; ki++) {
|
||||||
|
if (ki->m_type != katom_t::replaced && ki->m_type != katom_t::ignored)
|
||||||
|
result += ki->m_src;
|
||||||
|
}
|
||||||
|
if (strip_whitespace)
|
||||||
|
result = trim(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
katom_list trim(katom_list& katoms, std::set<katom_t> trim_types)
|
katom_list trim(katom_list& katoms, std::set<katom_t> trim_types)
|
||||||
{
|
{
|
||||||
katom_iter begin =
|
katom_iter begin =
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ std::vector<Katom> text_katoms(
|
|||||||
std::string as_string(std::vector<Katom>::const_iterator begin, std::vector<Katom>::const_iterator end, bool strip_whitespace);
|
std::string as_string(std::vector<Katom>::const_iterator begin, std::vector<Katom>::const_iterator end, bool strip_whitespace);
|
||||||
|
|
||||||
std::string as_string(const std::vector<Katom>& katoms, bool strip_whitespace);
|
std::string as_string(const std::vector<Katom>& katoms, bool strip_whitespace);
|
||||||
|
// The text of a katom range EXACTLY AS WRITTEN (Katom::m_src): a quoted
|
||||||
|
// special keeps its caret, so the result is Klammertext source that can be
|
||||||
|
// re-read. What a literal parameter of a definition holds.
|
||||||
|
std::string as_source(std::vector<Katom>::const_iterator begin, std::vector<Katom>::const_iterator end, bool strip_whitespace);
|
||||||
|
|
||||||
|
|
||||||
std::vector<Katom> trim(std::vector<Katom>& katoms, std::set<katom_t> trim_types = {katom_t::space, katom_t::newline});
|
std::vector<Katom> trim(std::vector<Katom>& katoms, std::set<katom_t> trim_types = {katom_t::space, katom_t::newline});
|
||||||
|
|||||||
@@ -134,6 +134,29 @@ strings_t word_split(const std::string& s)
|
|||||||
return regex_split(s, std::regex("\\s+"));
|
return regex_split(s, std::regex("\\s+"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string capitalize(std::string s)
|
||||||
|
{
|
||||||
|
// The cast through unsigned char is what makes std::toupper safe on a
|
||||||
|
// byte above 127 (negative as a char, and undefined behaviour to pass).
|
||||||
|
if (!s.empty())
|
||||||
|
s[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(s[0])));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string unquote_punctuation(const std::string& s)
|
||||||
|
{
|
||||||
|
std::string result {};
|
||||||
|
result.reserve(s.size());
|
||||||
|
for (size_t i = 0; i < s.size(); i++) {
|
||||||
|
if (s[i] == '^' && i + 1 < s.size() && std::ispunct(static_cast<unsigned char>(s[i + 1]))) {
|
||||||
|
result += s[++i];
|
||||||
|
} else {
|
||||||
|
result += s[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
std::string collapse_whitespace(const std::string& s)
|
std::string collapse_whitespace(const std::string& s)
|
||||||
{
|
{
|
||||||
std::string result {};
|
std::string result {};
|
||||||
|
|||||||
+11
@@ -46,6 +46,17 @@ std::vector<std::string> word_split(const std::string& s);
|
|||||||
// whose description happens to fit one line and miss the identically worded
|
// whose description happens to fit one line and miss the identically worded
|
||||||
// one that does not.
|
// one that does not.
|
||||||
std::string collapse_whitespace(const std::string& s);
|
std::string collapse_whitespace(const std::string& s);
|
||||||
|
// Klammertext source shown as PLAIN text: "^" before an ASCII punctuation
|
||||||
|
// character is removed ("^@date" -> "@date"), nothing else changes. For an
|
||||||
|
// error message or a log line that quotes a literal description; a display
|
||||||
|
// that can render should hand the source to a Machine instead.
|
||||||
|
std::string unquote_punctuation(const std::string& s);
|
||||||
|
|
||||||
|
// The string with its first character in upper case ("table" -> "Table").
|
||||||
|
// ASCII only: a non-ASCII first byte is left as it is, since a std::string
|
||||||
|
// is bytes and the standard library has no Unicode case mapping. Unlike
|
||||||
|
// Python's str.capitalize(), the rest of the string is untouched.
|
||||||
|
std::string capitalize(std::string s);
|
||||||
|
|
||||||
// Case-insensitive substring test, ASCII folding only. The project takes no
|
// Case-insensitive substring test, ASCII folding only. The project takes no
|
||||||
// ICU or locale dependency (the same rule as the language tables and
|
// ICU or locale dependency (the same rule as the language tables and
|
||||||
|
|||||||
@@ -30,3 +30,9 @@
|
|||||||
|
|
||||||
@@unicode_examples.* ::
|
@@unicode_examples.* ::
|
||||||
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect unicode_examples @ @@
|
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect unicode_examples @ @@
|
||||||
|
|
||||||
|
@@argtype_definitions.k :
|
||||||
|
Show the defined klammer argument types @@
|
||||||
|
|
||||||
|
@@argtype_definitions.* ::
|
||||||
|
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect argtype_definitions @ @@
|
||||||
+54
-5
@@ -1,3 +1,5 @@
|
|||||||
|
#include <set>
|
||||||
|
|
||||||
#include "machine.h"
|
#include "machine.h"
|
||||||
#include "character.h"
|
#include "character.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
@@ -53,7 +55,6 @@ std::string extended_latin_examples([[maybe_unused]] Machine& machine)
|
|||||||
return kt.str();
|
return kt.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
std::string pinyin_examples([[maybe_unused]] Machine& machine)
|
std::string pinyin_examples([[maybe_unused]] Machine& machine)
|
||||||
{
|
{
|
||||||
@@ -74,7 +75,7 @@ extern "C"
|
|||||||
std::string unicode_examples([[maybe_unused]] Machine& machine)
|
std::string unicode_examples([[maybe_unused]] Machine& machine)
|
||||||
{
|
{
|
||||||
std::stringstream kt {};
|
std::stringstream kt {};
|
||||||
kt << "\n@b Including Unicode in Klammertext files @\n\n"
|
kt << "\n@s2 Including Unicode in Klammertext files @\n\n"
|
||||||
<< "To specify a wide variety of different letter forms, the Unicode Standard "
|
<< "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 "
|
<< "defines a large set of \"code points\" organized by groups. To include a "
|
||||||
<< "Unicode character in Klammertext, you surround its hexadecimal number with "
|
<< "Unicode character in Klammertext, you surround its hexadecimal number with "
|
||||||
@@ -83,8 +84,8 @@ std::string unicode_examples([[maybe_unused]] Machine& machine)
|
|||||||
<< "To search for Unicode characters by their description, see: @nl@\n"
|
<< "To search for Unicode characters by their description, see: @nl@\n"
|
||||||
<< "@link https://www.fileformat.info/info/unicode/char/search.htm @\n\n";
|
<< "@link https://www.fileformat.info/info/unicode/char/search.htm @\n\n";
|
||||||
|
|
||||||
kt << "@table :caption Examples of Chinese characters :cell_hpos c |\n"
|
kt << "@table :caption Examples of Chinese characters :cell_hpos c c l |\n"
|
||||||
<< " Character | Unicode | Pinyin ||\n";
|
<< " Character | Written | Pinyin ||\n";
|
||||||
|
|
||||||
for (auto chr : dao) {
|
for (auto chr : dao) {
|
||||||
std::string code = "^" + chr[1] + "^";
|
std::string code = "^" + chr[1] + "^";
|
||||||
@@ -96,7 +97,7 @@ std::string unicode_examples([[maybe_unused]] Machine& machine)
|
|||||||
|
|
||||||
|
|
||||||
kt << "@table :caption Examples of Egyptian hieroglyphs :cell_hpos c c c l |\n"
|
kt << "@table :caption Examples of Egyptian hieroglyphs :cell_hpos c c c l |\n"
|
||||||
<< "Hieroglyph | Unicode | Sound | Gardiner ||\n";
|
<< "Hieroglyph | Written | Sound | Gardiner ||\n";
|
||||||
|
|
||||||
// The body font carries no hieroglyphs, so the glyph names the
|
// The body font carries no hieroglyphs, so the glyph names the
|
||||||
// typeface it needs (shipped in fnt/); @document declares and localizes
|
// typeface it needs (shipped in fnt/); @document declares and localizes
|
||||||
@@ -115,3 +116,51 @@ std::string unicode_examples([[maybe_unused]] Machine& machine)
|
|||||||
|
|
||||||
return kt.str();
|
return kt.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
std::string argtype_definitions(Machine& machine)
|
||||||
|
{
|
||||||
|
auto argtypes = machine.m_argtypes;
|
||||||
|
std::stringstream kt {};
|
||||||
|
std::set<std::string> custom {};
|
||||||
|
|
||||||
|
std::map<std::string, std::vector<std::string>> custom_types {};
|
||||||
|
|
||||||
|
kt << "@s2 Types of klammer arguments @\n\n"
|
||||||
|
<< "@define :style r | \n";
|
||||||
|
// Hack to make the "Pattern:" label line up vertically;
|
||||||
|
// there should be a klammer that aligns text at "tab stops".
|
||||||
|
auto name_width = std::string("klammertext").size() + 1;
|
||||||
|
for (const auto& name : argtypes.m_names) {
|
||||||
|
auto argtype = argtypes.m_types.at(name);
|
||||||
|
auto filename = argtype.m_loc.m_filename;
|
||||||
|
if (contains(filename, "argtype_registry")) {
|
||||||
|
std::string fill(name_width - name.size(), '~');
|
||||||
|
kt << "\n@c " << name << " c@ @t " << fill << " t@ @i Pattern: i@ @c "
|
||||||
|
<< argtype.m_symbolic_pattern << " c@ |\n"
|
||||||
|
<< capitalize(trim(argtype.m_desc)) << " ||\n";
|
||||||
|
} else {
|
||||||
|
custom.insert(filename);
|
||||||
|
custom_types[filename].push_back(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kt << "\ndefine@\n\n"
|
||||||
|
<< "@vspace .5 @\n@s3 Reusing argtype symbols @\n\n"
|
||||||
|
<< "A previously defined type can be included in the definition of a new type "
|
||||||
|
<< "by surrounding the name of the existing type in single quotation marks. "
|
||||||
|
<< "For example, the @c uint c@ type (unsigned integer) is used to define an integer with a "
|
||||||
|
<< "`+' or `-' character before it.\n\n"
|
||||||
|
<< "@vspace .5 @\n@s3 Custom argument types @\n\n"
|
||||||
|
<< "Other argtypes are defined for special purposes for klammers defined in the following files:\n\n";
|
||||||
|
|
||||||
|
kt << "@define :style t |\n";
|
||||||
|
for (auto [filename, types] : custom_types) {
|
||||||
|
auto type_count = types.size();
|
||||||
|
int column_count = type_count < 4 ? 1 : int(float(type_count)/3);
|
||||||
|
// auto count = int(float(types.size()) / 3);
|
||||||
|
kt << symbolic_klammertext_filename(filename) << " |\n"
|
||||||
|
<< "@columns :style c :n " << column_count << " | " << join(types, " ") << " @ ||\n\n";
|
||||||
|
}
|
||||||
|
kt << "define@\n\n";
|
||||||
|
return kt.str();
|
||||||
|
}
|
||||||
+13
-10
@@ -31,10 +31,10 @@ vertical mode is a no-op.
|
|||||||
|
|
||||||
# Indented block
|
# Indented block
|
||||||
|
|
||||||
@@indent.k s :left.int 8 :right.int -1 :ragged.bool false :
|
@@indent.k s :left.width 8 :right.width :ragged.bool false :
|
||||||
Indented block. The *left* and *right* values are the number of characters for
|
Indented block. *left* and *right* are widths: a bare number is that many
|
||||||
indentation (as roughtly defined by "ex"). By default, *right* is set to the
|
characters (the width of a digit in a proportional typeface), or a width
|
||||||
*left* value (with -1 as the sentinel).
|
with a unit such as 2em. *right* defaults to the *left* value.
|
||||||
@@
|
@@
|
||||||
@@indent.html,tex,txt :: @eval block.Indent(K) eval@ @@
|
@@indent.html,tex,txt :: @eval block.Indent(K) eval@ @@
|
||||||
|
|
||||||
@@ -124,13 +124,12 @@ indentation (as roughtly defined by "ex"). By default, *right* is set to the
|
|||||||
@@extendpage.tex :: \enlargethispage{*linecount*\baselineskip} @@
|
@@extendpage.tex :: \enlargethispage{*linecount*\baselineskip} @@
|
||||||
@@extendpage.txt :: @@
|
@@extendpage.txt :: @@
|
||||||
|
|
||||||
|
|
||||||
# Vertical space
|
# Vertical space
|
||||||
|
|
||||||
@@vspace.k lines.float : Vertical space, in multiples of the current line height @@
|
# The first parameter of the height type: a bare number is lines in every
|
||||||
@@vspace.tex :: \vspace{*lines*\baselineskip} @@
|
# target (as it always was here), and a unit may now be given.
|
||||||
@@vspace.html :: <div style="height: *lines*lh"></div> @@
|
@@vspace.k lines.height : Vertical space: a number of lines, or a height such as 2em @@
|
||||||
@@vspace.txt :: @eval phases.txt_vspace_marker() * round(*lines*) @ @@
|
@@vspace.html,tex,txt :: @eval block.Vspace(K) @ @@
|
||||||
|
|
||||||
# Vertical fill to end of page
|
# Vertical fill to end of page
|
||||||
|
|
||||||
@@ -146,6 +145,10 @@ only makes some vertical space. @@
|
|||||||
@@vfill.html :: <div class="vfill"></div> @@
|
@@vfill.html :: <div class="vfill"></div> @@
|
||||||
@@vfill.txt :: @vspace 3 @ @@
|
@@vfill.txt :: @vspace 3 @ @@
|
||||||
|
|
||||||
|
# Horizontal space
|
||||||
|
@@hspace.k width.width : Horizontal space of the given *width* (a bare number is characters) @@
|
||||||
|
@@hspace.html,tex,txt :: @eval block.Hspace(K) @ @@
|
||||||
|
|
||||||
|
|
||||||
# Questions and answers
|
# Questions and answers
|
||||||
|
|
||||||
@@ -202,7 +205,7 @@ bar (|) between each line. Then, two bars (||), and the right lines with a bar
|
|||||||
betwen them.
|
betwen them.
|
||||||
@@
|
@@
|
||||||
|
|
||||||
@@left_right.html,tex :: @eval block.Left_right(K) @ @@
|
@@left_right.html,tex,txt :: @eval block.Left_right(K) @ @@
|
||||||
|
|
||||||
|
|
||||||
# Footnote
|
# Footnote
|
||||||
|
|||||||
+74
-13
@@ -11,35 +11,56 @@ from html_util import E
|
|||||||
import color
|
import color
|
||||||
|
|
||||||
class Indent(klammer_base.Klammer_base):
|
class Indent(klammer_base.Klammer_base):
|
||||||
|
"""An indented block (@indent). :left and :right are widths: a bare
|
||||||
|
number is characters in every target (2026-09-16); :right defaults to
|
||||||
|
:left."""
|
||||||
def __init__(self, K):
|
def __init__(self, K):
|
||||||
super().__init__(K)
|
super().__init__(K)
|
||||||
# Doesn't do anything now:
|
if not self.right:
|
||||||
# self.s = kutil.escape(self.s)
|
|
||||||
if self.right < 0:
|
|
||||||
self.right = self.left
|
self.right = self.left
|
||||||
self.reduce = self.left + self.right
|
|
||||||
|
|
||||||
def html(self):
|
def html(self):
|
||||||
text_align = "justify" if not self.ragged else "left"
|
text_align = "justify" if not self.ragged else "left"
|
||||||
|
left = kutil.parse_width("html", self.left)[0]
|
||||||
|
right = kutil.parse_width("html", self.right)[0]
|
||||||
return E("div").body(self.s)\
|
return E("div").body(self.s)\
|
||||||
.sty("margin", f"0.5rem {self.right}ex 0.5rem {self.left}ex")\
|
.sty("margin", f"0.5rem {right} 0.5rem {left}")\
|
||||||
.sty("text-align", text_align)\
|
.sty("text-align", text_align)\
|
||||||
.str()
|
.str()
|
||||||
def tex(self):
|
def tex(self):
|
||||||
if self.ragged:
|
if self.ragged:
|
||||||
self.s = "\\raggedright " + self.s
|
self.s = "\\raggedright " + self.s
|
||||||
result = rf"\hspace*{{{self.left}ex}}" + latex_util.minipage(
|
left = kutil.parse_width("tex", self.left)[0]
|
||||||
self.s, rf"\linewidth - {self.reduce}ex", center=False)
|
right = kutil.parse_width("tex", self.right)[0]
|
||||||
|
result = rf"\hspace*{{{left}}}" + latex_util.minipage(
|
||||||
|
self.s, rf"\linewidth - {left} - {right}", center=False)
|
||||||
result = latex_util.block(result)
|
result = latex_util.block(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def txt(self):
|
def txt(self):
|
||||||
indent = phases.txt_nbsp_marker() * self.left # a space that survives
|
left = int(kutil.parse_width("txt", self.left)[0])
|
||||||
width = int(self.Target_txt_width) - self.left - self.right + 1 # Off-by one for textwrap
|
right = int(kutil.parse_width("txt", self.right)[0])
|
||||||
result = self.s
|
indent = phases.txt_nbsp_marker() * left # a space that survives
|
||||||
result = re.sub("KK0022", '"', result)
|
width = int(self.Target_txt_width) - left - right
|
||||||
result = indent + f"\n{indent}".join(textwrap.wrap(result, width=width, break_on_hyphens=True))
|
result = re.sub("KK0022", '"', self.s)
|
||||||
return result
|
return phases.txt_lay_out(indent + f"\n{indent}".join(phases.txt_wrap(result, width)))
|
||||||
|
|
||||||
|
class Hspace(klammer_base.Klammer_base):
|
||||||
|
"""Horizontal space (@hspace): a width, so a bare number is characters
|
||||||
|
in every target."""
|
||||||
|
def __init__(self, K):
|
||||||
|
super().__init__(K)
|
||||||
|
|
||||||
|
def html(self):
|
||||||
|
return f'<span style="display: inline-block; width: {kutil.parse_width("html", self.width)[0]}"></span>'
|
||||||
|
|
||||||
|
def tex(self):
|
||||||
|
return f"\\hspace{{{kutil.parse_width('tex', self.width)[0]}}}"
|
||||||
|
|
||||||
|
def txt(self):
|
||||||
|
# The nbsp marker, not spaces: an @eval result of only whitespace
|
||||||
|
# is trimmed away entirely.
|
||||||
|
return phases.txt_nbsp_marker() * int(kutil.parse_width("txt", self.width)[0])
|
||||||
|
|
||||||
|
|
||||||
class Note(klammer_base.Klammer_base):
|
class Note(klammer_base.Klammer_base):
|
||||||
@@ -106,6 +127,26 @@ class Lines(klammer_base.Klammer_base):
|
|||||||
def txt(self):
|
def txt(self):
|
||||||
return self.lines
|
return self.lines
|
||||||
|
|
||||||
|
class Vspace(klammer_base.Klammer_base):
|
||||||
|
"""Vertical space (@vspace): the first parameter of the height type. A
|
||||||
|
bare number is lines in every target, as it always was here; a unit
|
||||||
|
(2em, 12pt) is now accepted as well, under html and tex."""
|
||||||
|
def __init__(self, K):
|
||||||
|
super().__init__(K)
|
||||||
|
|
||||||
|
def html(self):
|
||||||
|
height = kutil.parse_height("html", self.lines)[0]
|
||||||
|
return f'<div style="{kutil.css_height("height", height)}"></div>'
|
||||||
|
|
||||||
|
def tex(self):
|
||||||
|
return f"\\vspace{{{kutil.parse_height('tex', self.lines)[0]}}}"
|
||||||
|
|
||||||
|
def txt(self):
|
||||||
|
# One marker per line; the justification phase turns them into
|
||||||
|
# blank lines after paragraph normalization (a whitespace-only
|
||||||
|
# @eval result would be trimmed away, hence the markers).
|
||||||
|
return phases.txt_vspace_marker() * int(kutil.parse_height("txt", self.lines)[0])
|
||||||
|
|
||||||
class Left_right(klammer_base.Klammer_base):
|
class Left_right(klammer_base.Klammer_base):
|
||||||
def __init__(self, K):
|
def __init__(self, K):
|
||||||
super().__init__(K)
|
super().__init__(K)
|
||||||
@@ -126,3 +167,23 @@ class Left_right(klammer_base.Klammer_base):
|
|||||||
result += rf"\parbox{{\linewidth}}{{{left} \hfill {right}}}" + " \\\\\n"
|
result += rf"\parbox{{\linewidth}}{{{left} \hfill {right}}}" + " \\\\\n"
|
||||||
result = result[:-3]
|
result = result[:-3]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def txt(self):
|
||||||
|
# Plain text: the left text at the block indent, the right text ending
|
||||||
|
# at Target_txt_width, the gap filled with the nbsp marker (the only
|
||||||
|
# space that survives to the output, and what marks the block as laid
|
||||||
|
# out). A pair that cannot share a line -- less than two columns
|
||||||
|
# between them -- goes on two lines, the right text still flush right.
|
||||||
|
nbsp = phases.txt_nbsp_marker()
|
||||||
|
indent = int(self.Target_txt_indent)
|
||||||
|
width = int(self.Target_txt_width)
|
||||||
|
lines = []
|
||||||
|
for left, right in self.rows:
|
||||||
|
left, right = left.strip(), right.strip()
|
||||||
|
gap = width - indent - phases.txt_visible_length(left) - phases.txt_visible_length(right)
|
||||||
|
if gap >= 2:
|
||||||
|
lines.append(nbsp * indent + left + nbsp * gap + right)
|
||||||
|
else:
|
||||||
|
lines.append(nbsp * indent + left)
|
||||||
|
lines.append(nbsp * max(indent, width - phases.txt_visible_length(right)) + right)
|
||||||
|
return phases.txt_lay_out("\n".join(lines))
|
||||||
+1
-1
@@ -24,7 +24,7 @@ string, so a "^#" or a bare "^@" inside it was read as Klammertext and usually
|
|||||||
failed the file, which is not what "verbatim" can mean.
|
failed the file, which is not what "verbatim" can mean.
|
||||||
@@
|
@@
|
||||||
|
|
||||||
@@c.html,tex :: @eval code_block.Code_fragment(K) eval@
|
@@c.html,tex,txt :: @eval code_block.Code_fragment(K) eval@
|
||||||
@@
|
@@
|
||||||
|
|
||||||
# :cwd makes the filename resolve against the DOCUMENT's directory, not
|
# :cwd makes the filename resolve against the DOCUMENT's directory, not
|
||||||
|
|||||||
@@ -349,6 +349,18 @@ class Code_fragment(klammer_base.Klammer_base):
|
|||||||
def tex(self):
|
def tex(self):
|
||||||
return f"{{\\tt {tex_line(self.code_text.strip())}}}"
|
return f"{{\\tt {tex_line(self.code_text.strip())}}}"
|
||||||
|
|
||||||
|
def txt(self):
|
||||||
|
# Plain text has no face to switch to: the fragment is its own text.
|
||||||
|
# As in html_line/tex_line, the Klammertext specials are quoted for
|
||||||
|
# the @eval read-back (a bare "@" in the result would be a klammer
|
||||||
|
# without a beginning) and the typographically active characters
|
||||||
|
# hidden, so a "--" in code stays two hyphens.
|
||||||
|
result = klammer_base.hide_typographic(quote_specials(self.code_text.strip()))
|
||||||
|
# Surround code symbols with ‹ and ›, though there are conflicts in Swiss
|
||||||
|
# German, Italian and some Scandinavian languages. Should be clear in
|
||||||
|
# context, though.
|
||||||
|
result = "\u2039" + result + "\u203A"
|
||||||
|
return result
|
||||||
|
|
||||||
def extract_marked_region(src, marker, filename):
|
def extract_marked_region(src, marker, filename):
|
||||||
"""The region of `src` between two lines that are exactly "//<marker>".
|
"""The region of `src` between two lines that are exactly "//<marker>".
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@
|
|||||||
@@image.k
|
@@image.k
|
||||||
basename
|
basename
|
||||||
:id
|
:id
|
||||||
:width.length .5w
|
:width.width .5w
|
||||||
@caption_args@
|
@caption_args@
|
||||||
:vmargin.bool true
|
:vmargin.bool true
|
||||||
@hpos_args@
|
@hpos_args@
|
||||||
|
|||||||
+7
-3
@@ -76,12 +76,16 @@ class Image(klammer_base.Klammer_base):
|
|||||||
|
|
||||||
# kutil.msg("rel: " + str(self.rel_fraction))
|
# kutil.msg("rel: " + str(self.rel_fraction))
|
||||||
|
|
||||||
length, value, ltype = kutil.parse_length("html", self.width, self.rel_fraction)
|
length, value, ltype = kutil.parse_width("html", self.width, self.rel_fraction)
|
||||||
if ltype == "w":
|
if ltype == "w":
|
||||||
percent = float(value) * 100
|
percent = float(value) * 100
|
||||||
result.sty(f"width: {percent}%")
|
result.sty(f"width: {percent}%")
|
||||||
else:
|
elif ltype == "px":
|
||||||
|
# The html width ATTRIBUTE takes pixels only; every other unit
|
||||||
|
# is a style (an attribute of "2em" was silently ignored).
|
||||||
result.attr("width", length)
|
result.attr("width", length)
|
||||||
|
else:
|
||||||
|
result.sty(f"width: {length}")
|
||||||
|
|
||||||
if self.border:
|
if self.border:
|
||||||
result.cls("image_border")
|
result.cls("image_border")
|
||||||
@@ -124,7 +128,7 @@ class Image(klammer_base.Klammer_base):
|
|||||||
#if self.caption or self.number:
|
#if self.caption or self.number:
|
||||||
# width = "\\textwidth"
|
# width = "\\textwidth"
|
||||||
#else:
|
#else:
|
||||||
width = kutil.parse_length("tex", self.width, self.rel_fraction)[0]
|
width = kutil.parse_width("tex", self.width, self.rel_fraction)[0]
|
||||||
width = re.sub("px", "pt", width)
|
width = re.sub("px", "pt", width)
|
||||||
#result = f'\\includegraphics[width={width}]{{{source}}}'
|
#result = f'\\includegraphics[width={width}]{{{source}}}'
|
||||||
if self.number or self.caption:
|
if self.number or self.caption:
|
||||||
|
|||||||
+37
-6
@@ -14,6 +14,34 @@
|
|||||||
# :python_cast (lambda s : __import__("kutil").parse_length("tex", s))
|
# :python_cast (lambda s : __import__("kutil").parse_length("tex", s))
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
|
# Lengths on an axis (2026-09-16). A width and a height are different
|
||||||
|
# types so that a value can only name a unit of its own axis: "w" and "pw"
|
||||||
|
# belong to widths, "h" and "ph" to heights, and a mistake is an argument
|
||||||
|
# error rather than a silent misreading. A BARE NUMBER is the natural
|
||||||
|
# unit of the axis in every target -- characters for a width (a column in
|
||||||
|
# txt, "ch" in html, the width of a digit in tex), lines for a height (a
|
||||||
|
# newline in txt, "lh" in html, \baselineskip in tex) -- which is what lets
|
||||||
|
# a klammer written for the page keep its meaning in a terminal. "length"
|
||||||
|
# stays for the quantities with no axis (a rule thickness).
|
||||||
|
@@@argtype width |
|
||||||
|
a horizontal length. A bare number is that many characters (one column
|
||||||
|
of plain text; in a proportional typeface, the width of a digit, so an
|
||||||
|
indent or a column lines up across lines even where the letters do not).
|
||||||
|
Otherwise a number with a unit: ^:width 2em, 12pt, 100px, .5w (that
|
||||||
|
fraction of the text width), .5pw (of the page width); f or none for an
|
||||||
|
element's natural width; a quoted string, whose set width is used where
|
||||||
|
the target can measure it.
|
||||||
|
:pattern 'float'^|'float'em^|'int'pt^|'int'px^|'float'w^|'float'pw^|f^|none^|"[^^"]+"
|
||||||
|
@@@
|
||||||
|
|
||||||
|
@@@argtype height |
|
||||||
|
a vertical length. A bare number is that many lines (blank lines in
|
||||||
|
plain text; the line height of the surrounding text otherwise).
|
||||||
|
Otherwise a number with a unit: ^:height 2em, 12pt, 100px, .5h (that
|
||||||
|
fraction of the text height), .5ph (of the page height).
|
||||||
|
:pattern 'float'^|'float'em^|'int'pt^|'int'px^|'float'h^|'float'ph
|
||||||
|
@@@
|
||||||
|
|
||||||
@@@argtype lengths | a list of lengths
|
@@@argtype lengths | a list of lengths
|
||||||
:pattern ('length'^|\s+)*
|
:pattern ('length'^|\s+)*
|
||||||
#:python_cast (lambda s : [__import__("kutil").parse_length("tex", e) for e in s.split()])
|
#:python_cast (lambda s : [__import__("kutil").parse_length("tex", e) for e in s.split()])
|
||||||
@@ -37,15 +65,18 @@
|
|||||||
An offset has no meaning for a centered element and is ignored there.
|
An offset has no meaning for a centered element and is ignored there.
|
||||||
Written alone, ^:offset is the standard indentation (e.g. ^:hpos left
|
Written alone, ^:offset is the standard indentation (e.g. ^:hpos left
|
||||||
^:offset), and a value overrides it (^:hpos right ^:offset 4em).
|
^:offset), and a value overrides it (^:hpos right ^:offset 4em).
|
||||||
# A single-purpose type, not a use of "length", because the two values
|
# A single-purpose type, not a use of "width", because the two values
|
||||||
# below are what the writer relies on and only a type can carry them: an
|
# below are what the writer relies on and only a type can carry them: an
|
||||||
# element is flush unless an offset is asked for (:default), and asking
|
# element is flush unless an offset is asked for (:default), and asking
|
||||||
# without saying how much is the standard indentation (:alone). A general
|
# without saying how much is the standard indentation (:alone). A general
|
||||||
# length type must not declare :alone -- a bare option name has to read the
|
# width type must not declare :alone -- a bare option name has to read the
|
||||||
# same way wherever it appears, and "2em" is meaningless for :width.
|
# same way wherever it appears, and "4" is meaningless for :width.
|
||||||
:pattern 'length'
|
# Both values are BARE numbers -- characters -- so that they mean the same
|
||||||
:default 0pt
|
# thing under txt as on the page (2026-09-16): four characters is about
|
||||||
:alone 2em
|
# the 2em the alone value used to be, and 0 is flush everywhere.
|
||||||
|
:pattern 'width'
|
||||||
|
:default 0
|
||||||
|
:alone 4
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+49
-4
@@ -112,10 +112,27 @@ def _rest_split_level(s, dimensions):
|
|||||||
def rest_args(s, dimensions=1):
|
def rest_args(s, dimensions=1):
|
||||||
return rest_split(s, dimensions)
|
return rest_split(s, dimensions)
|
||||||
|
|
||||||
def parse_length(target, s, rel_fraction):
|
def parse_length(target, s, rel_fraction=1, axis="width"):
|
||||||
def choose(html_value, tex_value):
|
"""A length argument as the target's own length.
|
||||||
|
|
||||||
|
Returns (text, number, unit). `axis` is "width" or "height" and gives
|
||||||
|
a BARE number its meaning -- characters or lines, the natural unit of
|
||||||
|
the axis in every target (sks/kutil/kutil.k, the width and height
|
||||||
|
argtypes): under txt the integer count itself; under html "ch" and
|
||||||
|
"lh" (a consumer of "lh" writes the em fallback first, see
|
||||||
|
css_height); under tex the width of a digit in the current font
|
||||||
|
(\\fontcharwd) and \\baselineskip. `target` "txt" is accepted for the
|
||||||
|
bare units only, since plain text has no other.
|
||||||
|
"""
|
||||||
|
def choose(html_value, tex_value, txt_value=None):
|
||||||
|
if target == "txt" or target == "ansi":
|
||||||
|
if txt_value is None:
|
||||||
|
unit = "characters" if axis == "width" else "lines"
|
||||||
|
raise Exception(f'The {axis} "{s}" has no meaning in plain text: '
|
||||||
|
f'give a bare number of {unit}')
|
||||||
|
return txt_value
|
||||||
return html_value if target == "html" else tex_value
|
return html_value if target == "html" else tex_value
|
||||||
pat = re.compile(r'(?:([0-9.]+)([a-z]+))|(none|f)|(?:"([^"]+)")')
|
pat = re.compile(r'(?:([0-9.]+)([a-z]*))|(none|f)|(?:"([^"]+)")')
|
||||||
#pat = re.compile(r'(?:([0-9.]+)([a-z]+))|(none|f)|(?:{([^}]+)})')
|
#pat = re.compile(r'(?:([0-9.]+)([a-z]+))|(none|f)|(?:{([^}]+)})')
|
||||||
|
|
||||||
|
|
||||||
@@ -138,6 +155,14 @@ def parse_length(target, s, rel_fraction):
|
|||||||
result = choose("", f"\\textwidth - \\widthof{{ {text[1:]}}}")
|
result = choose("", f"\\textwidth - \\widthof{{ {text[1:]}}}")
|
||||||
else:
|
else:
|
||||||
result = choose("", f"\\widthof{{ {text}}}")
|
result = choose("", f"\\widthof{{ {text}}}")
|
||||||
|
elif units == "":
|
||||||
|
# The natural unit of the axis.
|
||||||
|
if axis == "height":
|
||||||
|
result = choose(f"{num}lh", f"{num}\\baselineskip", str(round(num)))
|
||||||
|
units = "lines"
|
||||||
|
else:
|
||||||
|
result = choose(f"{num}ch", f"{num}\\fontcharwd\\font`0 ", str(round(num)))
|
||||||
|
units = "ch"
|
||||||
elif units == "w":
|
elif units == "w":
|
||||||
result = choose(f"{100 * num}vw", f"{num}\\textwidth")
|
result = choose(f"{100 * num}vw", f"{num}\\textwidth")
|
||||||
elif units == "h":
|
elif units == "h":
|
||||||
@@ -149,11 +174,31 @@ def parse_length(target, s, rel_fraction):
|
|||||||
elif units == "px":
|
elif units == "px":
|
||||||
result = choose(f"{round(num)}", f"{num}px")
|
result = choose(f"{round(num)}", f"{num}px")
|
||||||
else:
|
else:
|
||||||
result = f"{num}{units}"
|
# em, pt: the same text in html and tex; none in plain text.
|
||||||
|
result = choose(f"{num}{units}", f"{num}{units}")
|
||||||
result = re.sub("\t", "\\t", result)
|
result = re.sub("\t", "\\t", result)
|
||||||
return result, num, units
|
return result, num, units
|
||||||
|
|
||||||
|
|
||||||
|
def parse_width(target, s, rel_fraction=1):
|
||||||
|
return parse_length(target, s, rel_fraction, "width")
|
||||||
|
|
||||||
|
def parse_height(target, s, rel_fraction=1):
|
||||||
|
return parse_length(target, s, rel_fraction, "height")
|
||||||
|
|
||||||
|
def css_height(prop, value):
|
||||||
|
"""A CSS declaration for a height from parse_height's html text. The
|
||||||
|
"lh" unit is recent (2023 in every major browser); an older browser
|
||||||
|
ignores a declaration it cannot parse and keeps the previous one, so
|
||||||
|
the em equivalent is written FIRST and lh second -- the standard
|
||||||
|
fallback idiom, no detection needed."""
|
||||||
|
if value.endswith("lh"):
|
||||||
|
# A short decimal, not the float's full expansion (1.5 * 1.2 is
|
||||||
|
# 1.7999999999999998 in binary).
|
||||||
|
em = f"{float(value[:-2]) * 1.2:.4g}"
|
||||||
|
return f"{prop}: {em}em; {prop}: {value}"
|
||||||
|
return f"{prop}: {value}"
|
||||||
|
|
||||||
def old_parse_length(target, s):
|
def old_parse_length(target, s):
|
||||||
pat = re.compile(r'(?:([0-9.]+)([a-z]+))|(none|f)|("([^"]+)")')
|
pat = re.compile(r'(?:([0-9.]+)([a-z]+))|(none|f)|("([^"]+)")')
|
||||||
match = pat.match(s)
|
match = pat.match(s)
|
||||||
|
|||||||
+16
-6
@@ -3,14 +3,24 @@
|
|||||||
:value 7
|
:value 7
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@ul.k list_items.rest :cmp.bool false :bullet : Unordered list, with list elements preceeded by the *bullet* character. @@
|
@@ul.k list_items.rest :cmp.bool false :bullet :
|
||||||
|
Unordered list, with list elements preceeded by the *bullet* character. @@
|
||||||
|
|
||||||
@@ul.html,tex,txt :: @eval list.List(K, "ul") eval@ @@
|
@@ul.html,tex,txt :: @eval list.List(K, "ul") eval@ @@
|
||||||
|
|
||||||
@@ol.k list_items.rest :cmp.bool false :initial.int 1 : Ordered list, with numbered list elements. @@
|
@@ol.k list_items.rest :cmp.bool false :initial.int 1 :
|
||||||
|
Ordered list, with numbered list elements. @@
|
||||||
|
|
||||||
@@ol.html,tex,txt :: @eval list.List(K, "ol") eval@ @@
|
@@ol.html,tex,txt :: @eval list.List(K, "ol") eval@ @@
|
||||||
|
|
||||||
@@define.k descriptions.rest(2) :style.style_code i : A list of words or phrases and their definitions or descriptions @@
|
@@define.k descriptions.rest(2) :style.style_code i :
|
||||||
@@define.html,tex :: @eval list.Define(K) eval@ @@
|
A list of words or phrases and their definitions or descriptions @@
|
||||||
|
|
||||||
@@columns.k items :n.int 2 : Create two or more columns from the text @@
|
@@define.html,tex,txt :: @eval list.Define(K) eval@ @@
|
||||||
@@columns.tex :: @eval list.columns(K) eval@ @@
|
|
||||||
|
@@columns.k items :n.int 2 :style.style_code r @hpos_args :hpos left @ :
|
||||||
|
Create two or more columns from the text. Placed like a table: flush at
|
||||||
|
the left by default, ^:offset inset from that margin (a bare number is
|
||||||
|
characters). @@
|
||||||
|
|
||||||
|
@@columns.html,tex,txt :: @eval list.Columns(K) eval@ @@
|
||||||
+59
-31
@@ -3,6 +3,7 @@ import re
|
|||||||
import textwrap
|
import textwrap
|
||||||
import kutil
|
import kutil
|
||||||
import klammer_base
|
import klammer_base
|
||||||
|
import phases
|
||||||
import itertools
|
import itertools
|
||||||
import math
|
import math
|
||||||
from html_util import E
|
from html_util import E
|
||||||
@@ -81,17 +82,42 @@ class Define(klammer_base.Klammer_base):
|
|||||||
'c' : r'\normalfont\ttfamily',
|
'c' : r'\normalfont\ttfamily',
|
||||||
's' : r'\normalfont\sffamily'}[self.style]
|
's' : r'\normalfont\sffamily'}[self.style]
|
||||||
return f"{font} {s}"
|
return f"{font} {s}"
|
||||||
# The "\hfill\newline" was to force the description to the next line even if the item was small:
|
strut = r"\rule[-4pt]{0pt}{4pt}"
|
||||||
# result = "\n\n".join([f"\\item[{{{fontify(term)}}}]\\hfill\n\\newline {definition}"
|
|
||||||
strut = r"\rule[-8pt]{0pt}{8pt}"
|
|
||||||
result = "\n\n".join([f"\\item[{{{fontify(term)}{strut}}}] {definition}"
|
result = "\n\n".join([f"\\item[{{{fontify(term)}{strut}}}] {definition}"
|
||||||
for term,definition in self.items])
|
for term,definition in self.items])
|
||||||
result = f"\\begin{{description}}[labelindent=12pt,nosep,itemsep=6pt,parsep=.5\\parskip,style=nextline]\n{result}\n\\end{{description}}"
|
options = "style=nextline" # See list/list.sty
|
||||||
|
result = f"\\begin{{description}}[{options}]\n{result}\n\\end{{description}}"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Needs to be implemented:
|
def txt(self):
|
||||||
# def txt(self):
|
# Plain text: each term on its own line at the block indent, its
|
||||||
# return "TXT DEFINITIONS"
|
# definition beneath it indented once more, items separated by a
|
||||||
|
# blank line. Every line begins with the nbsp marker, which is what
|
||||||
|
# tells the justification pass the block is already laid out
|
||||||
|
# (phases.txt_justify_blocks); a definition's own paragraphs are
|
||||||
|
# wrapped to the remaining measure, and a paragraph that is already
|
||||||
|
# laid out (it begins with the marker -- a @table inside the
|
||||||
|
# definition) is only indented further. The term takes :style as
|
||||||
|
# italic or bold, the styles a terminal can show.
|
||||||
|
nbsp = phases.txt_nbsp_marker()
|
||||||
|
indent = int(self.Target_txt_indent)
|
||||||
|
term_indent, def_indent = nbsp * indent, nbsp * (2 * indent)
|
||||||
|
measure = int(self.Target_txt_width) - 2 * indent
|
||||||
|
style = {"i": "italic", "b": "bold"}.get(self.style)
|
||||||
|
items = []
|
||||||
|
for term, definition in self.items:
|
||||||
|
term = term.strip()
|
||||||
|
if style and term:
|
||||||
|
term = phases.txt_style_marker(style) + term + phases.txt_style_marker(style, False)
|
||||||
|
paragraphs = []
|
||||||
|
for par in re.split(r"\n\s*\n", definition.strip()):
|
||||||
|
if phases.txt_is_laid_out(par):
|
||||||
|
paragraphs.append("\n".join(def_indent + line for line in par.split("\n")))
|
||||||
|
else:
|
||||||
|
paragraphs.append("\n".join(def_indent + line
|
||||||
|
for line in phases.txt_wrap(par, measure)))
|
||||||
|
items.append(term_indent + term + "\n" + "\n\n".join(paragraphs))
|
||||||
|
return phases.txt_lay_out("\n\n".join(items))
|
||||||
|
|
||||||
|
|
||||||
class Lines(klammer_base.Klammer_base):
|
class Lines(klammer_base.Klammer_base):
|
||||||
@@ -114,30 +140,32 @@ class Lines(klammer_base.Klammer_base):
|
|||||||
return self.s.strip()
|
return self.s.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class Columns(klammer_base.Klammer_base):
|
||||||
|
def __init__(self, K):
|
||||||
|
super().__init__(K)
|
||||||
|
|
||||||
def make_column_lists(lst, count):
|
def make_column_lists(self):
|
||||||
cols = math.ceil(len(lst) / count)
|
lst = [f"@{self.style} {e} {self.style}@" for e in self.items.split()]
|
||||||
result = []
|
cols = math.ceil(len(lst) / self.n)
|
||||||
for i in range(count):
|
result = []
|
||||||
#print(i * cols, (i + 1) * cols)
|
for i in range(self.n):
|
||||||
result.append(lst[i * cols : (i+1) * cols])
|
result.append(lst[i * cols : (i+1) * cols])
|
||||||
return list(itertools.zip_longest(*result, fillvalue=""))
|
return list(itertools.zip_longest(*result, fillvalue=""))
|
||||||
|
|
||||||
def xmake_column_lists(lst, column_count):
|
def columns(self):
|
||||||
count = len(lst)
|
rows = self.make_column_lists()
|
||||||
row_count = math.ceil(count / column_count)
|
result = (f"@table :header false :number false :leading 1.0 "
|
||||||
print(count, column_count, row_count)
|
f":hpos {self.hpos} :offset {self.offset} |\n")
|
||||||
i = 0
|
for row in rows:
|
||||||
while i < count:
|
result += " | ".join(row) + " ||\n"
|
||||||
i += 1
|
result += "table@\n"
|
||||||
|
return result
|
||||||
|
|
||||||
def columns(K):
|
def html(self):
|
||||||
rows = make_column_lists(K.items.split(), K.n)
|
return self.columns()
|
||||||
result = "@table\n" # :lines false :header_line false :leading 1.1 :vmargin false |\n"
|
|
||||||
for row in rows:
|
def tex(self):
|
||||||
#print("row:", row)
|
return self.columns()
|
||||||
result += " | ".join(row) + " ||\n"
|
|
||||||
result += "table@\n"
|
def txt(self):
|
||||||
if K.K_target == "latex":
|
return self.columns()
|
||||||
result = f"\\vspace*{{-12pt}}\n{result}"
|
|
||||||
return result
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
\setlistdepth{9}
|
\setlistdepth{9}
|
||||||
\renewlist{enumerate}{enumerate}{9}
|
\renewlist{enumerate}{enumerate}{9}
|
||||||
\renewlist{itemize}{itemize}{9}
|
\renewlist{itemize}{itemize}{9}
|
||||||
\setlist{topsep=0pt, partopsep=0pt, parsep=2pt, itemsep=0pt}
|
\setlist{topsep=0pt, partopsep=0pt, parsep=.5\parskip, itemsep=0pt, labelindent=8pt, leftmargin=16pt}
|
||||||
\setlist[enumerate]{label*=\arabic*.}
|
\setlist[enumerate]{label*=\arabic*.}
|
||||||
\setlist[itemize,1]{label=\textbullet}
|
\setlist[itemize,1]{label=\textbullet}
|
||||||
\setlist[itemize,2]{label=\normalfont\bfseries\textendash}
|
\setlist[itemize,2]{label=\normalfont\bfseries\textendash}
|
||||||
|
|||||||
+16
-19
@@ -2,56 +2,53 @@
|
|||||||
# the front and back matter of a book):
|
# the front and back matter of a book):
|
||||||
|
|
||||||
@@preface.k :id :name Preface : Preface text @@
|
@@preface.k :id :name Preface : Preface text @@
|
||||||
@@preface.html,tex :: @eval section.Section(K, "preface") eval@ @@
|
@@preface.html,tex,txt :: @eval section.Section(K, "preface") eval@ @@
|
||||||
|
|
||||||
# Numbered section headings:
|
# Numbered section headings:
|
||||||
|
|
||||||
@@part.k title :id :name Part : Part heading @@
|
@@part.k title :id :name Part : Part heading @@
|
||||||
@@part.html,tex :: @eval section.Section(K, 0) eval@ @@
|
@@part.html,tex,txt :: @eval section.Section(K, 0) eval@ @@
|
||||||
|
|
||||||
@@s1.k title :id :n : Top level division, numbered @@
|
@@s1.k title :id :n : Top level division, numbered @@
|
||||||
@@s1.html,tex :: @eval section.Section(K, 1) eval@ @@
|
@@s1.html,tex,txt :: @eval section.Section(K, 1) eval@ @@
|
||||||
|
|
||||||
# Temporary hack for testing txt target; need to number section headings.
|
|
||||||
@@s1.txt :: *title* @@
|
|
||||||
|
|
||||||
@@s2.k title :id :n : Second level division, numbered @@
|
@@s2.k title :id :n : Second level division, numbered @@
|
||||||
@@s2.html,tex :: @eval section.Section(K, 2) eval@ @@
|
@@s2.html,tex,txt :: @eval section.Section(K, 2) eval@ @@
|
||||||
|
|
||||||
@@s3.k title :id :n : Third level division, numbered @@
|
@@s3.k title :id :n : Third level division, numbered @@
|
||||||
@@s3.html,tex :: @eval section.Section(K, 3) eval@ @@
|
@@s3.html,tex,txt :: @eval section.Section(K, 3) eval@ @@
|
||||||
|
|
||||||
@@s4.k title :id :n : Fourth level division, numbered @@
|
@@s4.k title :id :n : Fourth level division, numbered @@
|
||||||
@@s4.html,tex :: @eval section.Section(K, 4) eval@ @@
|
@@s4.html,tex,txt :: @eval section.Section(K, 4) eval@ @@
|
||||||
|
|
||||||
@@s5.k title :id :n : Fifth level division, numbered @@
|
@@s5.k title :id :n : Fifth level division, numbered @@
|
||||||
@@s5.html,tex :: @eval section.Section(K, 5) eval@ @@
|
@@s5.html,tex,txt :: @eval section.Section(K, 5) eval@ @@
|
||||||
|
|
||||||
@@s6.k title :id :n : Sixth level division, numbered @@
|
@@s6.k title :id :n : Sixth level division, numbered @@
|
||||||
@@s6.html,tex :: @eval section.Section(K, 6) eval@ @@
|
@@s6.html,tex,txt :: @eval section.Section(K, 6) eval@ @@
|
||||||
|
|
||||||
@@s7.k title :id :n : Seventh level division, numbered @@
|
@@s7.k title :id :n : Seventh level division, numbered @@
|
||||||
@@s7.html,tex :: @eval section.Section(K, 7) eval@ @@
|
@@s7.html,tex,txt :: @eval section.Section(K, 7) eval@ @@
|
||||||
|
|
||||||
# Unnumbered section headings:
|
# Unnumbered section headings:
|
||||||
|
|
||||||
@@h1.k title :id :n : Top level division, unnumbered @@
|
@@h1.k title :id :n : Top level division, unnumbered @@
|
||||||
@@h1.html,tex :: @eval section.Section(K, 1, numbered=False) eval@ @@
|
@@h1.html,tex,txt :: @eval section.Section(K, 1, numbered=False) eval@ @@
|
||||||
|
|
||||||
@@h2.k title :id : Second level division, unnumbered @@
|
@@h2.k title :id : Second level division, unnumbered @@
|
||||||
@@h2.html,tex :: @eval section.Section(K, 2, numbered=False) eval@ @@
|
@@h2.html,tex,txt :: @eval section.Section(K, 2, numbered=False) eval@ @@
|
||||||
|
|
||||||
@@h3.k title :id : Third level division, unnumbered @@
|
@@h3.k title :id : Third level division, unnumbered @@
|
||||||
@@h3.html,tex :: @eval section.Section(K, 3, numbered=False) eval@ @@
|
@@h3.html,tex,txt :: @eval section.Section(K, 3, numbered=False) eval@ @@
|
||||||
|
|
||||||
@@h4.k title :id : Fourth level division, unnumbered @@
|
@@h4.k title :id : Fourth level division, unnumbered @@
|
||||||
@@h4.html,tex :: @eval section.Section(K, 4, numbered=False) eval@ @@
|
@@h4.html,tex,txt :: @eval section.Section(K, 4, numbered=False) eval@ @@
|
||||||
|
|
||||||
@@h5.k title :id : Fifth level division, unnumbered @@
|
@@h5.k title :id : Fifth level division, unnumbered @@
|
||||||
@@h5.html,tex :: @eval section.Section(K, 5, numbered=False) eval@ @@
|
@@h5.html,tex,txt :: @eval section.Section(K, 5, numbered=False) eval@ @@
|
||||||
|
|
||||||
@@h6.k title :id : Sixth level division, unnumbered @@
|
@@h6.k title :id : Sixth level division, unnumbered @@
|
||||||
@@h6.html,tex :: @eval section.Section(K, 6, numbered=False) eval@ @@
|
@@h6.html,tex,txt :: @eval section.Section(K, 6, numbered=False) eval@ @@
|
||||||
|
|
||||||
@@h7.k title :id : Seventh level division, unnumbered @@
|
@@h7.k title :id : Seventh level division, unnumbered @@
|
||||||
@@h7.html,tex :: @eval section.Section(K, 7, numbered=False) eval@ @@
|
@@h7.html,tex,txt :: @eval section.Section(K, 7, numbered=False) eval@ @@
|
||||||
@@ -72,6 +72,5 @@ class Section(klammer_base.Klammer_base):
|
|||||||
result = f"%__sectionlink__{id}__{self.title}__" + "\n" + result
|
result = f"%__sectionlink__{id}__{self.title}__" + "\n" + result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Need to implement the procedural numbering for the txt target
|
def txt(self):
|
||||||
# def txt(self):
|
return f"@b {self.title} @"
|
||||||
# return self.title
|
|
||||||
@@ -232,3 +232,10 @@ column.
|
|||||||
|
|
||||||
@@tbl.k spec.figure_id : The caption title for a table @@
|
@@tbl.k spec.figure_id : The caption title for a table @@
|
||||||
@@tbl.html,tex,txt :: @reference *spec* | Table @ @@
|
@@tbl.html,tex,txt :: @reference *spec* | Table @ @@
|
||||||
|
|
||||||
|
#[
|
||||||
|
@@columns.k rows.rest : A minimal table without a caption, header, or lines @@
|
||||||
|
@@columns.html,tex,txt ::
|
||||||
|
@table rows :number false :header false @
|
||||||
|
@@
|
||||||
|
]#
|
||||||
+61
-22
@@ -978,32 +978,71 @@ class Table(klammer_base.Klammer_base):
|
|||||||
return (phases.txt_style_marker(STYLE[code]) + cell
|
return (phases.txt_style_marker(STYLE[code]) + cell
|
||||||
+ phases.txt_style_marker(STYLE[code], False))
|
+ phases.txt_style_marker(STYLE[code], False))
|
||||||
return cell
|
return cell
|
||||||
widths = self.txt_column_widths()
|
# A cell is one line of text: a newline written inside it is
|
||||||
|
# whitespace, as it is in html and tex.
|
||||||
|
rows = [[re.sub(r"\s*\n\s*", " ", cell) for cell in row] for row in self.rows]
|
||||||
|
widest = self.txt_column_widths()
|
||||||
pos = [hpos(p) for p in self.cell_hpos]
|
pos = [hpos(p) for p in self.cell_hpos]
|
||||||
result = ""
|
# Placement under txt (2026-09-16): an explicit :hpos left takes its
|
||||||
indent = txt_util.NBSP * int(self.Target_txt_indent)
|
# indent from :offset (characters; 0 = flush), :hpos none flows with
|
||||||
colsep = " "
|
# no indent, and anything else -- including the default, center --
|
||||||
# Horizontal lines are the same rule as the other targets (:hline,
|
# keeps the block convention, Target_txt_indent. (Centering and
|
||||||
# defaulting to under the header row and at the bottom when the
|
# right-alignment are not attempted: a block that shifts with the
|
||||||
# table has a header): a rule of hyphens the width of the table at
|
# width of its widest row reads as an accident in plain text.)
|
||||||
# each boundary that has a line. A line over part of the columns
|
if self.hpos == "left":
|
||||||
# is drawn full width here -- a partial rule in a plain-text table
|
indent = txt_util.NBSP * int(kutil.parse_width("txt", self.offset)[0])
|
||||||
# reads as an accident. Vertical lines have no plain-text form.
|
elif self.hpos == "none":
|
||||||
# The hyphens are HIDDEN (KTESC markers): a klammer's result is
|
indent = ""
|
||||||
# re-read as Klammertext and the txt target's dash transforms turn
|
else:
|
||||||
# "--" into "-", so a bare run of hyphens came out a fraction of
|
indent = txt_util.NBSP * int(self.Target_txt_indent)
|
||||||
# its length. (The caption rule escapes this by being drawn in a
|
colsep = " "
|
||||||
# phase, after the transforms.)
|
# Column widths, by the same rules as the other targets
|
||||||
rule = (indent + klammer_base.hide_typographic(
|
# (:column_width): 'fit' is the widest entry; a fraction is that part
|
||||||
|
# of the text width; '*' takes the remaining width; 'fill' takes the
|
||||||
|
# remaining width but no more than its widest entry -- and, unlike a
|
||||||
|
# 'fit' column, its cells WRAP to that width (2026-09-13; until then
|
||||||
|
# the txt table never wrapped a cell, so a description column ran off
|
||||||
|
# the page). Several fill/'*' columns share the remainder in
|
||||||
|
# proportion to their widest entries.
|
||||||
|
text_width = (int(self.Target_txt_width) - int(self.Target_txt_indent)
|
||||||
|
- len(colsep) * (self.row_size - 1))
|
||||||
|
specs = extend(self.column_width, self.row_size)
|
||||||
|
widths = [0] * self.row_size
|
||||||
|
for i, w in enumerate(specs):
|
||||||
|
if w in ("fit", "f"):
|
||||||
|
widths[i] = widest[i]
|
||||||
|
elif w not in ("fill", "*"):
|
||||||
|
widths[i] = max(1, int(float(w) * text_width))
|
||||||
|
flexible = [i for i, w in enumerate(specs) if w in ("fill", "*")]
|
||||||
|
if flexible:
|
||||||
|
remaining = max(len(flexible), text_width - sum(widths))
|
||||||
|
total = sum(max(1, widest[i]) for i in flexible)
|
||||||
|
for i in flexible:
|
||||||
|
share = max(1, remaining * max(1, widest[i]) // total)
|
||||||
|
widths[i] = min(share, widest[i]) if specs[i] == "fill" else share
|
||||||
|
wrapped = {i for i, w in enumerate(specs) if w not in ("fit", "f")}
|
||||||
|
mark = phases.txt_verbatim_marker() # each line: already laid out
|
||||||
|
rule = (mark + indent + klammer_base.hide_typographic(
|
||||||
"-" * (sum(widths) + len(colsep) * (len(widths) - 1))) + "\n")
|
"-" * (sum(widths) + len(colsep) * (len(widths) - 1))) + "\n")
|
||||||
for row_i, row in enumerate(self.rows):
|
result = ""
|
||||||
|
for row_i, row in enumerate(rows):
|
||||||
if row_i in self.s_hline.by_index:
|
if row_i in self.s_hline.by_index:
|
||||||
result += rule
|
result += rule
|
||||||
result += indent + colsep.join(
|
# Each cell becomes its lines: wrapped to the column's width in a
|
||||||
[pad(styled(elt, row_i, cell_i), width, align)
|
# wrapping column, one line otherwise; the row is as tall as its
|
||||||
for cell_i, (elt, width, align) in enumerate(zip(row, widths, pos))])
|
# tallest cell, short cells padded with empty lines.
|
||||||
result += "\n"
|
cell_lines = []
|
||||||
if len(self.rows) in self.s_hline.by_index:
|
for cell_i, (elt, width) in enumerate(zip(row, widths)):
|
||||||
|
text = styled(elt, row_i, cell_i)
|
||||||
|
lines = phases.txt_wrap(text, width) if cell_i in wrapped and text else [text]
|
||||||
|
cell_lines.append(lines or [""])
|
||||||
|
height = max(len(lines) for lines in cell_lines)
|
||||||
|
for n in range(height):
|
||||||
|
result += mark + indent + colsep.join(
|
||||||
|
[pad(lines[n] if n < len(lines) else "", width, align)
|
||||||
|
for lines, width, align in zip(cell_lines, widths, pos)])
|
||||||
|
result += "\n"
|
||||||
|
if len(rows) in self.s_hline.by_index:
|
||||||
result += rule
|
result += rule
|
||||||
result = txt_util.add_caption(
|
result = txt_util.add_caption(
|
||||||
result, "Table", self.number, self.caption, self.caption_side, \
|
result, "Table", self.number, self.caption, self.caption_side, \
|
||||||
|
|||||||
@@ -223,11 +223,11 @@ def hpos_container(element, hpos, offset=None):
|
|||||||
# coded, and only in html.)
|
# coded, and only in html.)
|
||||||
styles = []
|
styles = []
|
||||||
if hpos not in ("left", "center", "right", "none"):
|
if hpos not in ("left", "center", "right", "none"):
|
||||||
length, _, _ = kutil.parse_length("html", hpos, 1)
|
length, _, _ = kutil.parse_width("html", hpos)
|
||||||
styles.append(f"margin-left: {length}")
|
styles.append(f"margin-left: {length}")
|
||||||
hpos = "indent"
|
hpos = "indent"
|
||||||
if offset and offset not in ("0", "0pt") and hpos in ("left", "right", "indent"):
|
if offset and offset not in ("0", "0pt") and hpos in ("left", "right", "indent"):
|
||||||
length, _, _ = kutil.parse_length("html", offset, 1)
|
length, _, _ = kutil.parse_width("html", offset)
|
||||||
side = "right" if hpos == "right" else "left"
|
side = "right" if hpos == "right" else "left"
|
||||||
styles.append(f"padding-{side}: {length}")
|
styles.append(f"padding-{side}: {length}")
|
||||||
result = E("div").cls("hpos_" + hpos).body(element)
|
result = E("div").cls("hpos_" + hpos).body(element)
|
||||||
|
|||||||
@@ -81,9 +81,9 @@ def minipage(content, width="\\textwidth", vertical="c", center=True, vmargin=""
|
|||||||
|
|
||||||
def offset_length(offset):
|
def offset_length(offset):
|
||||||
r"""An :offset as a LaTeX length, or "0pt" when there is none."""
|
r"""An :offset as a LaTeX length, or "0pt" when there is none."""
|
||||||
if not offset or offset in ("0", "0pt"):
|
if not offset or offset in ("0", "0pt", "0.0"):
|
||||||
return "0pt"
|
return "0pt"
|
||||||
return kutil.parse_length("tex", offset, 1)[0]
|
return kutil.parse_width("tex", offset)[0]
|
||||||
|
|
||||||
def offset_space(offset):
|
def offset_space(offset):
|
||||||
r"""An :offset as an \hspace*, or "" when there is none."""
|
r"""An :offset as an \hspace*, or "" when there is none."""
|
||||||
@@ -111,7 +111,7 @@ def caption_wrapper(element, hpos, bottom_margin=0, offset=None):
|
|||||||
return minipage("\\hfill" + element + inset, vmargin=vmargin, center=False)
|
return minipage("\\hfill" + element + inset, vmargin=vmargin, center=False)
|
||||||
if hpos == "none":
|
if hpos == "none":
|
||||||
return element
|
return element
|
||||||
length, _, _ = kutil.parse_length("tex", hpos, 1)
|
length, _, _ = kutil.parse_width("tex", hpos)
|
||||||
return minipage(f"\\hspace*{{{length}}}" + element + "\\hfill",
|
return minipage(f"\\hspace*{{{length}}}" + element + "\\hfill",
|
||||||
vmargin=vmargin, center=False)
|
vmargin=vmargin, center=False)
|
||||||
|
|
||||||
|
|||||||
+23
-2
@@ -25,6 +25,26 @@ def txt_vspace_marker():
|
|||||||
def txt_nbsp_marker():
|
def txt_nbsp_marker():
|
||||||
return "__KTEXTNBSP__"
|
return "__KTEXTNBSP__"
|
||||||
|
|
||||||
|
# A ZERO-WIDTH mark that says "this line is already laid out" (2026-09-16).
|
||||||
|
# The justification pass takes a paragraph beginning with a space or the
|
||||||
|
# nbsp marker as verbatim, and until now that was the only way a block
|
||||||
|
# could say so -- which broke the moment a block was flush left: a @table
|
||||||
|
# with :hpos left :offset 0 (what @columns emits) began with its first
|
||||||
|
# cell, so its rows were re-flowed into one line. A block that lays itself
|
||||||
|
# out puts this at the start of every line (txt_lay_out); it is nothing to
|
||||||
|
# txt_visible_length and is removed at the end of txt_justify_blocks.
|
||||||
|
def txt_verbatim_marker():
|
||||||
|
return "__KTEXTVERBATIM__"
|
||||||
|
|
||||||
|
def txt_lay_out(text):
|
||||||
|
"""text with every line marked as already laid out."""
|
||||||
|
mark = txt_verbatim_marker()
|
||||||
|
return "\n".join(mark + line for line in text.split("\n"))
|
||||||
|
|
||||||
|
def txt_is_laid_out(par):
|
||||||
|
"""True for a paragraph the justification pass must leave alone."""
|
||||||
|
return par.startswith((" ", txt_nbsp_marker(), txt_verbatim_marker()))
|
||||||
|
|
||||||
# The type-style markers, emitted by @b.txt, @i.txt, @u.txt and @ri.txt
|
# 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
|
# (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
|
# terminal does: the txt target STRIPS these in its last phase
|
||||||
@@ -68,7 +88,7 @@ def txt_visible_length(s):
|
|||||||
"""How many columns s occupies once its markers resolve: a KTESC
|
"""How many columns s occupies once its markers resolve: a KTESC
|
||||||
marker is one glyph, an nbsp marker one space, a style marker nothing.
|
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)."""
|
len() is the wrong measure for anything laid out (see txt_util)."""
|
||||||
s = _STYLE_MARKER_RGX.sub("", s)
|
s = _STYLE_MARKER_RGX.sub("", s).replace(txt_verbatim_marker(), "")
|
||||||
return len(klammer_base.unescape_ktesc(s).replace(txt_nbsp_marker(), " "))
|
return len(klammer_base.unescape_ktesc(s).replace(txt_nbsp_marker(), " "))
|
||||||
|
|
||||||
def txt_wrap(text, width, subsequent_indent=""):
|
def txt_wrap(text, width, subsequent_indent=""):
|
||||||
@@ -159,7 +179,7 @@ def txt_justify_blocks(text, K):
|
|||||||
# branch that had nothing to do also did not copy the text through.
|
# branch that had nothing to do also did not copy the text through.
|
||||||
# An indented paragraph vanished from the output with no error.
|
# An indented paragraph vanished from the output with no error.
|
||||||
nbsp = txt_nbsp_marker()
|
nbsp = txt_nbsp_marker()
|
||||||
if par and par[0] != ' ' and not par.startswith(nbsp):
|
if par and not txt_is_laid_out(par):
|
||||||
label = re.match("(?:" + re.escape(nbsp) + r")*\[\d+\] ", par)
|
label = re.match("(?:" + re.escape(nbsp) + r")*\[\d+\] ", par)
|
||||||
par = "\n".join(txt_wrap(
|
par = "\n".join(txt_wrap(
|
||||||
par, int(K.Target_txt_width),
|
par, int(K.Target_txt_width),
|
||||||
@@ -167,6 +187,7 @@ def txt_justify_blocks(text, K):
|
|||||||
result += par + "\n\n"
|
result += par + "\n\n"
|
||||||
result = re.sub(" *" + txt_vspace_marker() + " *", "\n", result)
|
result = re.sub(" *" + txt_vspace_marker() + " *", "\n", result)
|
||||||
result = re.sub(r"\n? *" + txt_nl_marker() + r" *\n?", "\n", result)
|
result = re.sub(r"\n? *" + txt_nl_marker() + r" *\n?", "\n", result)
|
||||||
|
result = result.replace(txt_verbatim_marker(), "")
|
||||||
result = result.replace(txt_nbsp_marker(), " ")
|
result = result.replace(txt_nbsp_marker(), " ")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -38,7 +38,7 @@ of this: neither the transforms nor the quoting reach it.
|
|||||||
|
|
||||||
@@@state Target_txt_indent
|
@@@state Target_txt_indent
|
||||||
:desc Width of indentation in the plain text target (txt)
|
:desc Width of indentation in the plain text target (txt)
|
||||||
:value 3
|
:value 2
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
# The txt target is not fully implemented in the SKS yet.
|
# The txt target is not fully implemented in the SKS yet.
|
||||||
@@ -59,7 +59,7 @@ of this: neither the transforms nor the quoting reach it.
|
|||||||
--- -- |
|
--- -- |
|
||||||
`` " |
|
`` " |
|
||||||
'' " |
|
'' " |
|
||||||
` ` |
|
` ' |
|
||||||
' ' |
|
' ' |
|
||||||
~ __KTEXTNBSP__
|
~ __KTEXTNBSP__
|
||||||
:after_apply
|
:after_apply
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ test:
|
|||||||
./katomize_test.sh
|
./katomize_test.sh
|
||||||
./filename_test.sh
|
./filename_test.sh
|
||||||
./alone_test.sh
|
./alone_test.sh
|
||||||
|
./argtype_test.sh
|
||||||
./state_test.sh
|
./state_test.sh
|
||||||
./target_test.sh
|
./target_test.sh
|
||||||
./modulepath_test.sh
|
./modulepath_test.sh
|
||||||
|
|||||||
Executable
+71
@@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# argtype_test.sh -- the @@@argtype description is Klammertext source
|
||||||
|
# (engine tier: -k none, every definition inline).
|
||||||
|
#
|
||||||
|
# Decided 2026-09-13 (Andy): the second argument of @@@argtype has the
|
||||||
|
# "klammertext" type, so Argtype::m_desc holds the description EXACTLY AS
|
||||||
|
# WRITTEN --
|
||||||
|
# a quoted "^@date" keeps its caret -- and whoever displays it hands the
|
||||||
|
# text to a Machine (kdesc's listing through ref/, the Handbook). Before,
|
||||||
|
# the description was assembled from katom text, where the katomizer has
|
||||||
|
# already stripped the caret of a quoted special, so "(^@date and
|
||||||
|
# ^@datetime)" reached the generator as "(@date and @datetime)" and re-read
|
||||||
|
# as two klammer applications. A display that cannot render -- an argument
|
||||||
|
# error -- shows the source unquoted instead.
|
||||||
|
#
|
||||||
|
# Note the neighbour: tst/argtype_test (no .sh) is the C++ diagnostic
|
||||||
|
# program, the fourth such name collision after eval, state and target.
|
||||||
|
#
|
||||||
|
# Usage: ./argtype_test.sh (needs KLAMMERTEXT_HOME set; ktext/kdiag on PATH)
|
||||||
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||||
|
|
||||||
|
KTEXT="${KTEXT:-ktext}"; KDIAG="${KDIAG:-kdiag}"
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
red=$'\033[31m'; green=$'\033[32m'; bold=$'\033[1m'; reset=$'\033[0m'
|
||||||
|
pass() { echo "${green}PASS${reset} $1"; PASS=$((PASS+1)); }
|
||||||
|
fail() { echo "${red}FAIL${reset} $1"; [ -n "$2" ] && echo " $2"; FAIL=$((FAIL+1)); }
|
||||||
|
flat() { tr '\n' ' ' | tr -s ' '; } # error text is justified: match wrap-insensitively
|
||||||
|
|
||||||
|
echo "${bold}@@@argtype description tests${reset}"
|
||||||
|
echo "============================"
|
||||||
|
echo
|
||||||
|
|
||||||
|
DEF='@@@target tt | T @@@ @@@argtype lang | see ^@date and ^: here, ^| bar too :pattern [a-z][a-z] @@@'
|
||||||
|
T=$(mktemp -d)
|
||||||
|
|
||||||
|
# 1. The registry holds the source as written (kdiag --machine is the
|
||||||
|
# programmer's dump and shows the stored text).
|
||||||
|
out=$("$KDIAG" --system --machine "$DEF" 2>&1 | flat)
|
||||||
|
if echo "$out" | grep -qF 'see ^@date and ^: here, ^| bar too'; then pass " 1. the description is stored exactly as written (carets kept)"
|
||||||
|
else fail " 1. the description is stored exactly as written (carets kept)" "$(echo "$out" | grep -o 'lang.\{0,60\}' | head -1)"; fi
|
||||||
|
|
||||||
|
# 2. A quoted ":" inside the description does not start an option: the
|
||||||
|
# :pattern that follows is still the pattern (a valid value passes).
|
||||||
|
out=$("$KTEXT" --klammersets none -s "$DEF @@w.k c.lang : W @@ @@w.tt :: [*c*] @@ @w de @" -t tt -d 2>&1 | flat)
|
||||||
|
if echo "$out" | grep -qF '[de]'; then pass " 2. ^: in the description does not end it; :pattern still applies"
|
||||||
|
else fail " 2. ^: in the description does not end it; :pattern still applies" "$out"; fi
|
||||||
|
|
||||||
|
# 3. An invalid value's error shows the description UNQUOTED -- plain text
|
||||||
|
# for a reader who cannot render it.
|
||||||
|
out=$("$KTEXT" --klammersets none -s "$DEF @@w.k c.lang : W @@ @@w.tt :: [*c*] @@ @w 123 @" -t tt -d 2>&1 | flat)
|
||||||
|
if echo "$out" | grep -qF 'describes see @date and : here, | bar too'; then pass " 3. an argument error shows the description unquoted"
|
||||||
|
else fail " 3. an argument error shows the description unquoted" "$out"; fi
|
||||||
|
if ! echo "$out" | grep -qF '^@'; then pass " 4. ... and no caret leaks into the error"
|
||||||
|
else fail " 4. ... and no caret leaks into the error"; fi
|
||||||
|
|
||||||
|
# 5. A klammer's LITERAL parameter is a different type and is untouched:
|
||||||
|
# its "^" forms are processed ("^@" prints "@"), as they always were. The application is in a
|
||||||
|
# FILE after the -s definitions: the literal-marking pass needs the
|
||||||
|
# klammer registered before the unit holding the application is read,
|
||||||
|
# and "--" ends the --klammersets list before the filename.
|
||||||
|
printf '@lit ^@ and # and | x lit@\n' > "$T/lit.kt"
|
||||||
|
out=$("$KTEXT" --klammersets none -s '@@@target tt | T @@@ @@lit.k t.literal : L @@ @@lit.tt :: [*t*] @@' -- "$T/lit.kt" -t tt -d 2>&1 | flat)
|
||||||
|
if echo "$out" | grep -qF '[@ and # and | x]'; then pass " 5. the literal parameter of a klammer still holds its content as written"
|
||||||
|
else fail " 5. the literal parameter of a klammer still holds its content as written" "$out"; fi
|
||||||
|
|
||||||
|
rm -rf "$T"
|
||||||
|
echo
|
||||||
|
echo "============================"
|
||||||
|
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
||||||
|
[ $FAIL -eq 0 ]
|
||||||
@@ -209,6 +209,28 @@ check_clean "17. a correct document checks clean" \
|
|||||||
check_clean "18. the Standard Klammer Set checks clean" \
|
check_clean "18. the Standard Klammer Set checks clean" \
|
||||||
"@read $K/sks/sks.k @"
|
"@read $K/sks/sks.k @"
|
||||||
|
|
||||||
|
# Argument VALUES are checked against their argument types (2026-09-16):
|
||||||
|
# the same test the engine applies at application time, run here on every
|
||||||
|
# value that is LITERAL -- nothing but text -- so a height unit on a width,
|
||||||
|
# or a word where a number is wanted, is found without rendering. A value
|
||||||
|
# holding a nested klammer, a *variable*, or a caret form is computed at run
|
||||||
|
# time and is skipped: check what is fixed, not what is computed.
|
||||||
|
W='@@@target tt | T @@@ @@@argtype em | a length in em :pattern \d+em @@@ @@f.k x.em :opt.em : F @@ @@f.tt :: [*x*] @@'
|
||||||
|
check_finds "19. a literal positional value that fails its argument type" \
|
||||||
|
'does not match the "em" argument type' "$W @f 3px @"
|
||||||
|
check_finds "20. ... and the argtype description is quoted" \
|
||||||
|
'a length in em' "$W @f 3px @"
|
||||||
|
check_clean "21. a literal value that matches is clean" "$W @f 3em @"
|
||||||
|
check_finds "22. a literal option value that fails its argument type" \
|
||||||
|
'does not match the "em" argument type' "$W @f 3em :opt 4 @"
|
||||||
|
check_clean "23. an option written alone is not a value to check" "$W @f 3em :opt @"
|
||||||
|
check_clean "24. a value holding a nested klammer is computed: skipped" \
|
||||||
|
"$W @@n : 3em @@ @f @n@ @"
|
||||||
|
check_clean "25. a value holding a *variable* in a body is computed: skipped" \
|
||||||
|
"$W @@g.k v : G @@ @@g.tt :: @f *v* @ @@"
|
||||||
|
check_clean "26. a value with a caret form is computed: skipped" \
|
||||||
|
"$W @f 3^:em @"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "============================="
|
echo "============================="
|
||||||
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
||||||
|
|||||||
@@ -99,9 +99,13 @@ printf '@eval :cwd *K_input_dir* os.getcwd() @\n' > "$T/setb/pycwd.kt"
|
|||||||
check " 5. :cwd changes the Python working directory" \
|
check " 5. :cwd changes the Python working directory" \
|
||||||
"$T/setb" "$T/setb/pycwd.kt"
|
"$T/setb" "$T/setb/pycwd.kt"
|
||||||
|
|
||||||
|
# The first @eval renders nothing, so the document begins with the space
|
||||||
|
# the author wrote before the "/": ktext keeps it (it trims only newlines
|
||||||
|
# from the result, as Machine::apply and the Python binding do -- a leading
|
||||||
|
# space is the indent of a txt block that opens a document; TODO #60).
|
||||||
printf '@eval :cwd *K_input_dir* :shell true @ / @eval :shell pwd @\n' > "$T/setb/restore.kt"
|
printf '@eval :cwd *K_input_dir* :shell true @ / @eval :shell pwd @\n' > "$T/setb/restore.kt"
|
||||||
check " 6. the cwd is restored after the eval" \
|
check " 6. the cwd is restored after the eval" \
|
||||||
"/ $T/elsewhere" "$T/setb/restore.kt"
|
" / $T/elsewhere" "$T/setb/restore.kt"
|
||||||
|
|
||||||
mkdir -p "$T/sp ace"
|
mkdir -p "$T/sp ace"
|
||||||
printf 'spaced\n' > "$T/sp ace/f.txt"
|
printf 'spaced\n' > "$T/sp ace/f.txt"
|
||||||
|
|||||||
Reference in new issue
Block a user