Target coverage: a klammer states the targets it serves

kdesc gains --coverage, which reports for every klammer the set of targets it
can render to, and — the point of it — which klammers' coverage cannot be
derived and must therefore be declared.  Three rules: coverage is DERIVED
where the definitions determine it (a general body of klammer calls covers
the intersection of what those klammers cover, by a greatest fixpoint after
loading), DECLARED where the engine cannot interpret what decides it (an
@eval body, whose targets are undecidable), and UNKNOWN where nothing is
written — which never means "deliberately unavailable".

Two new spellings in a definition's name.  A comma-separated target list,
"@@table.html,tex :: ...", gives one body several targets; it is surface
syntax, expanded at registration, and each member goes through the
redefinition rules on its own.  And "@@date.* :: ..." writes the general
target out, asserting that the klammer works for EVERY target including ones
not yet defined — a stronger claim than a list of the targets defined today,
and the one target declaration that could be mechanically falsified.

The Standard Klammer Set was swept accordingly: it now has no general
definitions at all, every klammer names the targets it serves, six use ".*",
and tex and pdf are at zero undecided.

kdesc's flags are reorganised on two rules: a flag reached for often gets a
single letter (-k klammers, -t targets, -c characters, -i input), a more
specialised topic a multi-letter name (--argtypes, --katoms, --rewrite,
--optionsets, --coverage, --klammerset, --font); and -v says how much to show
about PROCESSING, never what the RESULT contains — so the katom regex column
is "--katoms full" and the coverage detail "--coverage all".  NOTE: "-k" now
lists klammers (optionally filtered by a name/description search); the katom
table moved to "--katoms".

Fixes carried along: an option written with no value crashed the command with
SIGSEGV instead of reporting the mistake; two required positional arguments
never parsed; kdesc and kdiag printed an error and exited 0; and definition
diagnostics counted registrations rather than what was written, so one line
could be reported as two definitions and then printed twice.

Four new test suites: target_list, coverage, command_option, kdesc.

(from dev 46f54080bd9a)
This commit is contained in:
2026-08-12 17:20:23 +02:00
parent 61987c8b1f
commit 59c1599bc9
49 changed files with 1960 additions and 431 deletions

View File

@@ -41,7 +41,7 @@ are regenerated on each release — patches cannot be merged directly.
Report problems (or send patches) to the author; accepted changes are
applied to the development tree and appear in a following snapshot.
This snapshot was assembled from development commit `12929fdff53b`.
This snapshot was assembled from development commit `46f54080bd9a`.
## License

View File

@@ -1,5 +1,6 @@
#include "argv.h"
#include "command.h"
#include "coverage.h"
#include "error.h"
#include "file.h"
#include "font_store.h"
@@ -33,6 +34,50 @@ static void font_usage()
"and then in the default font set.\n";
}
static void coverage_usage()
{
std::cout <<
"Coverage commands:\n"
" --coverage Report the targets each klammer covers\n"
" --coverage all Also name the file each klammer is written\n"
" in, and show every \"needs attention\"\n"
" category, including the empty ones\n"
" --coverage help This description\n"
"\n"
"Coverage is a fact about a klammer: the targets it can render to. It is\n"
"DERIVED where the definitions determine it (a general body of klammer\n"
"calls covers the intersection of what those klammers cover), must be\n"
"DECLARED where they cannot (an @eval body, whose targets are named in a\n"
"comma-separated list: @@name.html,tex :: ... @@), and is UNKNOWN where\n"
"nothing is written -- which never means \"deliberately unavailable\".\n"
"\n"
"Without \"all\", an AUTHOR sees what is available and only the problems\n"
"that exist. With it, a DESIGNER building a klammer set is reminded of\n"
"the whole set of categories.\n";
}
static void katom_usage()
{
std::cout <<
"Katom commands:\n"
" --katoms List the katom types\n"
" --katoms full The same, with the regex each type is\n"
" matched by\n"
" --katoms help This description\n";
}
// The words of a variadic option, with the empty ones dropped.
static strings_t option_words(Argv& args, const std::string& name)
{
strings_t words {};
for (const std::string& w : args.as_vector(name)) {
if (!w.empty()) {
words.push_back(w);
}
}
return words;
}
static void klammerset_usage()
{
std::cout <<
@@ -94,15 +139,23 @@ int main(int argc, char* argv[])
set_verbose_level(argc, argv);
Argv args {};
args.flag("c", "Special characters");
args.flag("a", "Argument types");
args.flag("k", "Katom types");
args.flag("r", "Katom rewrite patterns");
args.opt("input", "Input filename", "filename", "", "'text'");
args.flag("targets", "Show targets defined by the input file");
args.flag("klammers", "Show klammers defined by the input file");
args.flag("optionsets", "Show option sets declared by the input file");
// Declaration order is display order in the usage text, so these are
// ordered by how likely a user is to want them. A single letter is a
// flag one reaches for often; a multi-letter name is a more specialised
// topic. -v stays last: it says how much to show about the command's
// PROCESSING, never what its RESULT contains -- which is why the katom
// regexes and the coverage file column are words of their own options
// rather than verbosity levels.
args.var("k", "Show klammers. With <text>, only those whose name or description contains <text>.", "text");
args.flag("t", "Show the targets for Klammertext output");
args.flag("c", "Show the codes for accented vowels and other special characters");
args.opt("i", "Input filename. If given, it is used instead of the SKS.", "filename", "", "'text'");
args.var("font", "List installed fonts. Enter \"--font help\" for font maintenance commands.");
args.flag("argtypes", "Show the klammer argument types");
args.var("katoms", "Show the katom types. Enter \"--katoms help\" for details.");
args.flag("rewrite", "Show the katom rewrite patterns");
args.flag("optionsets", "Show the option sets declared by the input");
args.var("coverage", "Show the targets each klammer covers. Enter \"--coverage help\" for details.");
args.var("klammerset", "List the klammersets on the search path. Enter \"--klammerset help\" for details.");
args.opt("v", "'verbosity'", "n", "0", "'verbosity'");
@@ -123,50 +176,72 @@ int main(int argc, char* argv[])
std::cout << "\n";
}
if (p("a")) {
if (p("argtypes")) {
Argtype_registry argtypes;
std::cout << boldblack << "\nStandard klammer argument types\n" << black;
std::cout << argtypes.describe() << "\n";
}
if (p("k")) {
describe_katoms(verbose_level > 2);
if (args.given("katoms")) {
strings_t words = option_words(args, "katoms");
std::string verb = words.empty() ? "" : words[0];
if (verb == "help") {
katom_usage();
return 0;
}
if (!verb.empty() && verb != "full") {
std::cout << "Unrecognized katom command: --katoms " << join(words, " ") << "\n\n";
katom_usage();
return 1;
}
describe_katoms(verb == "full");
}
if (p("r")) {
if (p("rewrite")) {
describe_rewrite_patterns();
}
// The --font subcommands operate on the Klammertext font store
// (infrastructure) and load no klammer set.
if (args.given("font")) {
strings_t words {};
for (const std::string& w : args.as_vector("font")) {
if (!w.empty()) {
words.push_back(w);
}
}
font_command(words);
font_command(option_words(args, "font"));
return 0;
}
// The --klammerset subcommands operate on the search path
// (filesystem enumeration) and load no klammer set.
if (args.given("klammerset")) {
strings_t words {};
for (const std::string& w : args.as_vector("klammerset")) {
if (!w.empty()) {
words.push_back(w);
}
}
klammerset_command(words);
klammerset_command(option_words(args, "klammerset"));
return 0;
}
// --coverage is validated here, before a klammer set is read, so a
// mistyped word or "help" is answered at once. The report itself
// needs the loaded registry, so it runs at the end.
bool coverage_all = false;
if (args.given("coverage")) {
strings_t words = option_words(args, "coverage");
std::string verb = words.empty() ? "" : words[0];
if (verb == "help") {
coverage_usage();
return 0;
}
if (verb == "all") {
coverage_all = true;
} else if (!verb.empty()) {
std::cout << "Unrecognized coverage command: --coverage "
<< join(words, " ") << "\n\n";
coverage_usage();
return 1;
}
}
Machine M;
strings_t input_filenames = resolve_filename_list(args.get("input"));
strings_t input_filenames = resolve_filename_list(args.get("i"));
if (verbose_level > 0) {
std::cout << "input_filenames: " << input_filenames << "\n";
}
if (input_filenames.empty()) {
M.read(fs::path(M.m_state.value("KLAMMERTEXT_HOME") + "/sks/sks.k"));
} else {
@@ -178,25 +253,48 @@ int main(int argc, char* argv[])
fname = resolve_klammerset_symbol(
fname, fs::current_path().string(), Locator()).string();
}
if (verbose_level > 0) {
std::cout << "Read " << fname << "\n";
}
M.read(fs::path(absolute_pathname(fname)));
}
}
if (p("targets")) {
if (p("t")) {
std::cout << boldblack << "Targets\n" << black << M.m_targets.describe(2, true);
}
if (p("klammers")) {
std::cout << boldblack << "Klammers\n" << black << M.m_klammers.describe(2);
// "-k <text>" searches names AND descriptions, case-insensitively,
// with whitespace collapsed on both sides. An empty listing for a
// search that was actually made is reported: silence would read as a
// broken command. It is not an error -- finding nothing is a result.
if (args.given("k")) {
std::string search = join(option_words(args, "k"), " ");
std::string listing = M.m_klammers.describe(2, search);
if (listing.empty() && !search.empty()) {
std::cout << "No klammer names or descriptions contained "
<< q_(collapse_whitespace(search)) << ".\n";
} else {
std::cout << boldblack << "Klammers\n" << black << listing;
}
}
if (p("optionsets")) {
std::cout << boldblack << "Option sets\n" << black << M.m_option_sets.describe(2);
}
// Analysis only: klammer_coverage() reads the registry and modifies
// nothing, so what a document renders to is unaffected by asking.
if (args.given("coverage")) {
report_coverage(M, klammer_coverage(M), coverage_all, std::cout);
}
}
catch (Error& e) {
// Nonzero, as ktext does: a command that prints an error and exits 0
// reports success, and a script cannot tell the difference.
e.print_message();
return 1;
}
return 0;
}

View File

@@ -111,6 +111,10 @@ int main(int argc, char* argv[])
if (e.m_type == "target")
advice = "To include the Standard Klammer Set, add flag \"--sks\".";
e.print_message(advice);
// Nonzero, as ktext does: a command that prints an error and exits 0
// reports success, and a script cannot tell the difference.
std::cout << black;
return 1;
}
std::cout << black;
return 0;

View File

@@ -11,7 +11,7 @@ BASENAMES := util error locator file argv character ktype katom katom_list \
log show command argument argument_set argtype argtype_registry \
state eval eval_python eval_cpp klammer klammer_registry klammerset klammerset_registry \
option_set option_set_registry deftype \
target target_registry machine font_store check
target target_registry machine font_store check coverage
SOURCES := $(addsuffix .cpp,$(BASENAMES))
OBJECTS := $(addsuffix .o,$(BASENAMES))

View File

@@ -145,7 +145,8 @@ void Argv::opt(const std::string& name, const std::string& desc, const std::stri
update_width(arg);
}
void Argv::var(const std::string& name, const std::string& desc)
void Argv::var(const std::string& name, const std::string& desc,
const std::string& parameter)
{
(void)K::log(2, name, desc);
Arg arg {};
@@ -153,6 +154,9 @@ void Argv::var(const std::string& name, const std::string& desc)
arg.m_name = name;
arg.m_desc = get_regex_desc(desc);
arg.m_syntax = arg.symbol();
if (!parameter.empty()) {
arg.m_syntax += " [<" + parameter + ">]";
}
m_args[name] = arg;
m_names.push_back(name);
m_var_names.push_back(name);
@@ -291,18 +295,23 @@ void Argv::parse_optional(strings_t& words, string_map& named_args)
// std::cout << " Found: " << words[index] << "\n";
//std::vector<std::string> opt_args = {};
index++;
/*
// An option declared with opt() takes a value, so the word after
// the flag must exist. Written last with nothing after it -- a
// bare "kdesc -v", or "ktext doc.kt -t" -- this read past the end
// of the vector and the command died with SIGSEGV, naming
// nothing. (Two bounds checks used to sit here, commented out;
// they would have returned a HALF-PARSED option rather than
// reporting the mistake, so this reports it instead.) A valueless
// option is declared with flag(), not opt().
if (index >= words.size()) {
break;
throw Argument_error(
"The option " + flag_name(opt) + " needs a value: " +
m_args[opt].m_syntax + ". Enter \"" +
file_basename(command_name) + "\" for the list of arguments.",
Locator(), false);
}
*/
std::string opt_arg = words[index] + " ";
index++;
/*
if (index >= words.size()) {
break;
}
*/
std::regex opt_regex = m_args[opt].m_rgx;
// std::cout << "Regex match? " << std::regex_match(trim(opt_arg), opt_regex) << "\n";
@@ -346,7 +355,19 @@ void Argv::parse_positional(const std::string& command, //strings_t words,
throw Argument_error(ss.str(), Locator(), false);
}
named_args[req] = substring;
pos_args = rest;
// Drop the whitespace that separated this positional from the next.
// regex_split_prefix() requires its match at position 0 and returns
// the remainder verbatim, so without this the SECOND required
// argument is always "not found": its pattern is offered " second"
// and cannot match a leading space.
//
// Trimming here rather than at the top of the loop is deliberate: the
// first positional still receives pos_args exactly as before, so a
// command with ONE required argument -- which is every command that
// ships (ktext's `filenames`, kdiag's `input`) -- parses
// bit-identically. Only the case that never worked changes.
size_t next = rest.find_first_not_of(" \t");
pos_args = (next == std::string::npos) ? "" : rest.substr(next);
}
// std::cout << "Remaining words: " << words << "\n" << pos_args << "\n";
}
@@ -436,7 +457,13 @@ void Argv::parse(int argc, char* argv[], bool full_parse)
{
(void)K::log(2);
command_name = argv[0];
describe();
// No describe() here. A call sat at this point, before any value has
// been assigned, so it could only ever print a table of "<none>" -- and
// because set_verbose_level() parses a throwaway Argv before the real
// one, EVERY command would print that table on every run. Silencing
// Argv::describe() itself was the wrong half of the fix: it also silenced
// the callers that legitimately want it (kdesc -v, argv_test). The
// caller decides; parse() does not print.
auto input_args = classify_arguments(argc, argv, full_parse);
// std::cout << "parse() classify:\n" << input_args << "\n";
@@ -610,10 +637,11 @@ void Argv::describe()
std::stringstream ss {};
ss << " "<< std::setfill(' ') << std::setw(width)
<< m_args[name].symbol() << " : " << value;
// The line was built and then dropped, so this printed nothing at
// all -- which is why "kdesc -v 1" showed no arguments and argv_test,
// whose whole job is to display what Argv parsed, was silent. The
// callers already decide whether to call it (the commands gate it on
// verbose_level > 0), so it prints unconditionally here.
std::cout << ss.str() << "\n";
}
/*
if (verbose_level == 1) {
std::cout << "\n";
}
*/
}

View File

@@ -58,7 +58,13 @@ public:
// space-joined; given(name) distinguishes "--name with no words"
// from an absent --name. Used for subcommand-style interfaces
// (kdesc --font install <dir>).
void var(const std::string& name, const std::string& desc);
// A variadic option: the words after the flag, up to the next one.
// `parameter` is shown in the usage line as "[<parameter>]" -- an option
// whose words are a free argument (a search phrase) should name it, so
// the usage says what may follow; a subcommand-style option (--font,
// --coverage) leaves it empty and documents its words in its own help.
void var(const std::string& name, const std::string& desc,
const std::string& parameter = "");
void update_width(Arg arg);

481
mac/coverage.cpp Normal file
View File

@@ -0,0 +1,481 @@
#include <algorithm>
#include <functional>
#include <iomanip>
#include <ostream>
#include <set>
#include "coverage.h"
#include "machine.h"
#include "katom.h"
#include "util.h"
namespace {
using target_set = std::set<std::string>;
// The written definitions of one klammer, split by what they tell us. A
// definition filed under the general name is the only one whose coverage has
// to be worked out; a definition written for a target IS its own statement.
struct Written
{
const Klammer::components* m_general { nullptr };
target_set m_targets {}; // targets with a definition of their own
bool m_declared { false }; // has a ".k" declaration
};
Written written_definitions(const Klammer& klammer)
{
Written w {};
for (const auto& def : klammer.m_defs) {
if (def.target == Target_registry::general_name) {
w.m_general = &def;
} else if (def.target == Target_registry::declare_name) {
w.m_declared = true;
} else if (def.target != Target_registry::optionset_name) {
w.m_targets.insert(def.target);
}
}
return w;
}
// What a general body is made of. The three findings are ordered by how
// firmly they settle the question: anything the engine cannot interpret ends
// the analysis, and only a body of plain klammer calls is derivable.
struct Body_scan
{
bool m_undecidable { false };
std::string m_reason {};
std::vector<std::string> m_calls {};
};
Body_scan scan_body(const katom_list& body)
{
Body_scan scan {};
target_set seen {};
for (const auto& k : body) {
switch (k.m_type) {
case katom_t::eval_begin:
// Which targets a Python function answers for is undecidable, so
// the analysis stops here and the targets must be declared.
scan.m_undecidable = true;
scan.m_reason = "@eval body";
return scan;
case katom_t::read_begin:
scan.m_undecidable = true;
scan.m_reason = "@read body";
return scan;
case katom_t::literal_begin:
// A ^'...'^ span exists to carry raw target markup past the
// escaping pass. A general body holding one is target-specific
// with nothing for the intersection rule to see -- the blind spot
// a body of plain text would otherwise hide.
scan.m_undecidable = true;
scan.m_reason = "^'...'^ literal span";
return scan;
case katom_t::apply_begin: {
// The body read here is the STORED body, which is the body as
// written with one exception: a general klammer that takes no
// parameters is a constant, and a constant's body is spliced into
// later definitions at definition time. So a call to a constant
// does not appear here -- what appears is whatever the constant
// expanded to. That is the right thing for coverage (the calls
// that remain are the ones that will be applied), but it makes
// the "from" list a statement about the stored body, not about
// the source text.
std::string name = trim_char(k.m_text, '@');
if (seen.insert(name).second) {
scan.m_calls.push_back("@" + name);
}
break;
}
default:
break;
}
}
return scan;
}
target_set intersect(const target_set& a, const target_set& b)
{
target_set result {};
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(),
std::inserter(result, result.begin()));
return result;
}
std::vector<std::string> as_vector(const target_set& s)
{
return { s.begin(), s.end() };
}
// Shorten a definition's pathname for the report. What identifies a
// definition to a reader is its tail -- "sks/block/block.k" -- not the
// absolute path the file happened to be read from, which is the same long
// prefix on every row. A klammer set outside $KLAMMERTEXT_HOME keeps its
// path in full rather than being shortened to something ambiguous.
std::string short_path(const std::string& path)
{
auto pos = path.rfind("/sks/");
if (pos != std::string::npos) {
return path.substr(pos + 1);
}
const char* home = std::getenv("KLAMMERTEXT_HOME");
if (home != nullptr) {
std::string prefix = std::string(home) + "/";
if (path.size() > prefix.size() && path.compare(0, prefix.size(), prefix) == 0) {
return path.substr(prefix.size());
}
}
return path;
}
} // namespace
std::vector<Klammer_coverage> klammer_coverage(const Machine& machine)
{
target_set all_targets {};
for (const auto& t : machine.m_targets.user_defined()) {
all_targets.insert(t);
}
// Pass 1: what each klammer's definitions say, without resolving anything.
std::map<std::string, Written> written {};
std::map<std::string, Body_scan> scans {};
for (const auto& [name, klammer] : machine.m_klammers.m_klammers) {
written[name] = written_definitions(klammer);
if (written[name].m_general != nullptr) {
scans[name] = scan_body(written[name].m_general->body);
}
}
// Pass 2: the greatest fixpoint. Every general klammer starts optimistic
// -- all targets -- and the intersection rule is applied until nothing
// shrinks. Starting optimistic is what makes a cycle terminate: two
// klammers calling each other simply keep each other's sets, and a set
// that only loses members cannot iterate forever.
std::map<std::string, target_set> general {};
for (const auto& [name, scan] : scans) {
general[name] = all_targets;
}
// A klammer's coverage, for use as an operand of the intersection: what
// its own definitions cover, plus whatever its general body covers.
auto coverage_of = [&](const std::string& name) -> target_set {
auto w = written.find(name);
if (w == written.end()) return {}; // not defined; contributes nothing
target_set result = w->second.m_targets;
auto g = general.find(name);
if (g != general.end()) {
result.insert(g->second.begin(), g->second.end());
}
return result;
};
bool changed = true;
while (changed) {
changed = false;
for (auto& [name, targets] : general) {
const Body_scan& scan = scans[name];
if (scan.m_undecidable || scan.m_calls.empty()) continue;
target_set next = all_targets;
for (const auto& call : scan.m_calls) {
std::string called = trim_char(call, '@');
if (called == name) continue; // self-reference constrains nothing
next = intersect(next, coverage_of(called));
}
if (next != targets) {
targets = next;
changed = true;
}
}
}
// Pass 3: classify and collect.
std::vector<Klammer_coverage> result {};
for (const auto& [name, klammer] : machine.m_klammers.m_klammers) {
const Written& w = written.at(name);
Klammer_coverage kc {};
kc.m_name = name;
kc.m_declared = w.m_declared;
kc.m_written = as_vector(w.m_targets);
// Written definitions only (m_defs), so this is where the klammer is
// WRITTEN. m_defloc would also carry the targets a general body was
// copied to, which name the same file again.
for (const auto& def : klammer.m_defs) {
std::string file = short_path(def.loc.m_filename);
if (!is_in(file, kc.m_files)) {
kc.m_files.push_back(file);
}
}
for (const auto& [target, loc] : klammer.m_defloc) {
if (target != Target_registry::declare_name &&
target != Target_registry::general_name &&
target != Target_registry::optionset_name) {
kc.m_effective.push_back(target);
}
}
if (klammer.m_general_declared) {
// The author has said "every target". That is a statement, not
// something to be re-derived from the body: the whole point of
// writing ".*" is to assert what an @eval body cannot be read to
// mean.
kc.m_kind = coverage_t::all_declared;
target_set covered = w.m_targets;
covered.insert(all_targets.begin(), all_targets.end());
kc.m_targets = as_vector(covered);
} else if (w.m_general == nullptr) {
kc.m_kind = w.m_targets.empty() ? coverage_t::none : coverage_t::declared;
kc.m_targets = kc.m_written;
} else {
const Body_scan& scan = scans.at(name);
target_set covered = w.m_targets;
if (scan.m_undecidable) {
kc.m_kind = coverage_t::undecidable;
kc.m_reason = scan.m_reason;
// What it covers is not knowable here; report what is written.
kc.m_targets = kc.m_written;
} else if (scan.m_calls.empty()) {
kc.m_kind = coverage_t::all;
covered.insert(all_targets.begin(), all_targets.end());
kc.m_targets = as_vector(covered);
} else {
kc.m_kind = coverage_t::derived;
kc.m_from = scan.m_calls;
const target_set& g = general.at(name);
covered.insert(g.begin(), g.end());
kc.m_targets = as_vector(covered);
}
}
result.push_back(kc);
}
return result;
}
namespace {
std::string list_of(const std::vector<std::string>& v)
{
return v.empty() ? "--" : join(v, " ");
}
void section(std::ostream& os, const std::string& title, size_t count,
const std::string& explanation)
{
os << "\n" << title << " (" << count << ")\n";
if (!explanation.empty()) {
os << explanation;
}
}
} // namespace
void report_coverage(const Machine& machine,
const std::vector<Klammer_coverage>& coverage,
bool full, std::ostream& os)
{
strings_t targets = machine.m_targets.user_defined();
os << "Klammer coverage\n"
<< "================\n\n"
<< coverage.size() << " klammers, " << targets.size()
<< " targets: " << join(targets, " ") << "\n";
auto of_kind = [&coverage](coverage_t kind) {
std::vector<const Klammer_coverage*> result {};
for (const auto& kc : coverage) {
if (kc.m_kind == kind) result.push_back(&kc);
}
return result;
};
size_t width = 0;
for (const auto& kc : coverage) {
width = std::max(width, kc.m_name.size());
}
// The indent of a continuation line, under the name column.
std::string continuation(2 + 1 + width, ' ');
auto name_of = [&](const Klammer_coverage& kc) {
std::stringstream ss {};
ss << " @" << std::left << std::setw(width) << kc.m_name;
return ss.str();
};
// A section's rows, held until the whole section is built so the
// source-file column can be aligned. The file goes LAST because it is
// reference information: what the row says comes first, and the reader
// looks right only when they want to go and edit it. Only a row naming
// ONE klammer carries a file -- the "All targets" section lists many
// names on a line and has nothing to attach one to.
using Row = std::pair<std::string, std::string>; // text, file
auto emit = [&os, full](const std::vector<Row>& rows) {
size_t text_width = 0;
if (full) {
for (const auto& [text, file] : rows) {
if (!file.empty()) text_width = std::max(text_width, text.size());
}
}
for (const auto& [text, file] : rows) {
if (full && !file.empty()) {
os << std::left << std::setw(text_width) << text << " " << file << "\n";
} else {
os << trim_right(text) << "\n";
}
}
};
// ---- what works, first --------------------------------------------
//
// The reporting categories come before the problems because a terminal is
// read from the BOTTOM: an eighty-klammer listing scrolls a three-line
// warning off the screen entirely, so the actionable part has to be last,
// where "| tail" finds it.
//
// The two halves of the report hide an empty category for different
// reasons, so they are two functions rather than one with a flag.
using Row_of = std::function<std::string(const Klammer_coverage&)>;
// The explanation under a heading is for a reader learning the categories,
// so it appears only under "all" -- the same argument that shows the empty
// ones. A default report is headings and rows.
auto write = [&](const std::string& title,
const std::vector<const Klammer_coverage*>& group,
const std::string& explanation, const Row_of& row) {
section(os, title, group.size(), full ? explanation : "");
std::vector<Row> rows {};
for (const auto* kc : group) {
rows.push_back({row(*kc), join(kc->m_files, ", ")});
}
emit(rows);
};
// A REPORTING category describes the shape of the klammer set, so an
// empty one still says something ("nothing here uses .*") and "all" shows
// it as a designer's checklist.
auto reporting = [&](const std::string& title,
const std::vector<const Klammer_coverage*>& group,
const std::string& explanation, const Row_of& row) {
if (group.empty() && !full) return;
write(title, group, explanation, row);
};
// A PROBLEM category is different: it sits under a banner that says
// "Needs attention", and an empty one does not. Printing "Covers no
// target (0)" there states the opposite of the heading above it, so it is
// hidden whether or not "all" was given -- "all" is for information that
// is missing, and a category with nothing in it is not missing anything.
auto problem = [&](const std::string& title,
const std::vector<const Klammer_coverage*>& group,
const std::string& explanation, const Row_of& row) {
if (group.empty()) return;
write(title, group, explanation, row);
};
auto with_targets = [&](const Klammer_coverage& kc) {
return name_of(kc) + " " + list_of(kc.m_targets);
};
reporting("Defined per target", of_kind(coverage_t::declared), "", with_targets);
reporting("All targets, declared", of_kind(coverage_t::all_declared),
" Written \".*\": the author states that these work for every target,\n"
" including targets that do not exist yet.\n", with_targets);
// Many names on one line, so no file column: there is nothing for a file
// to attach to.
auto all = of_kind(coverage_t::all);
if (!all.empty() || full) {
section(os, "All targets, derived", all.size(),
full ? " No target suffix and a body of plain text, so nothing in them is\n"
" target-specific. This is the writer's macro form -- a repeated\n"
" phrase, not a klammer set -- and needs no declaration.\n" : "");
strings_t names {};
for (const auto* kc : all) {
names.push_back("@" + kc->m_name);
}
if (!names.empty()) {
os << " " << join(names, " ") << "\n";
}
}
// A derived klammer covering NOTHING is unusable, so it is reported with
// the problems rather than here.
std::vector<const Klammer_coverage*> derived {};
std::vector<const Klammer_coverage*> uncoverable {};
for (const auto* kc : of_kind(coverage_t::derived)) {
(kc->m_targets.empty() ? uncoverable : derived).push_back(kc);
}
reporting("Derived from the klammers the body calls", derived,
" No target suffix and a body of klammer calls, so the coverage is the\n"
" intersection of what those klammers cover.\n",
[&](const Klammer_coverage& kc) {
std::stringstream ss {};
ss << name_of(kc) << " " << std::left << std::setw(22) << list_of(kc.m_targets)
<< " from " << join(kc.m_from, " ");
return ss.str();
});
// ---- then what needs doing ----------------------------------------
//
// Four categories, ordered by severity: the first two mean the klammer
// cannot be used at all, the third that its coverage is a guess, the
// fourth that it is undocumented. Counted by DISTINCT klammer -- "no .k"
// is orthogonal to the others, so a klammer can be in two categories and
// summing the counts would overstate the work.
auto undecidable = of_kind(coverage_t::undecidable);
auto none = of_kind(coverage_t::none);
std::vector<const Klammer_coverage*> undescribed {};
for (const auto& kc : coverage) {
if (!kc.m_declared) undescribed.push_back(&kc);
}
target_set needing {};
for (const auto* group : { &none, &uncoverable, &undecidable, &undescribed }) {
for (const auto* kc : *group) needing.insert(kc->m_name);
}
// Nothing to attend to, nothing said -- the same rule as the categories
// below it. "Needs attention: 0" under "all" was the banner contradicting
// itself, exactly as an empty category under it would.
if (!needing.empty()) {
os << "\nNeeds attention: " << needing.size() << " "
<< plural("klammer", static_cast<int>(needing.size())) << "\n";
}
problem("Declared but never defined", none,
" A \".k\" declaration with no definition for any target, so the klammer\n"
" can never be applied.\n",
[&](const Klammer_coverage& kc) { return name_of(kc); });
problem("Covers no target", uncoverable,
" The klammers this one calls have no target in common, so the\n"
" intersection is empty and it can never be applied. The klammers\n"
" named are the ones to look at.\n",
[&](const Klammer_coverage& kc) {
return name_of(kc) + " from " + join(kc.m_from, " ");
});
problem("Must be declared", undecidable,
" A general definition whose body the engine cannot interpret, so it is\n"
" offered to EVERY target whether or not its code answers for that\n"
" target. Name the targets it does answer for -- @@name.html,tex :: --\n"
" or, if it works for any target at all, @@name.* ::\n",
[&](const Klammer_coverage& kc) { return name_of(kc) + " " + kc.m_reason; });
problem("No \".k\" declaration", undescribed,
" These render, but nothing describes them: a klammer without a \".k\"\n"
" has no description, so kdesc can say nothing about what it does and\n"
" \"kdesc -k <text>\" can only find it by name.\n", with_targets);
// The progress number. "Undecided" is the accurate label, and the one
// that asserts no more than was measured: a klammer that does not cover a
// target has not been declared unavailable there -- no notation for that
// exists yet -- it simply has no definition. "Unsupported" or "excluded"
// would each claim a decision nobody made.
os << "\nBy target\n";
for (const auto& target : targets) {
size_t covered = 0;
size_t offered = 0;
for (const auto& kc : coverage) {
if (is_in(target, kc.m_targets)) covered++;
if (is_in(target, kc.m_effective)) offered++;
}
os << " " << std::left << std::setw(10) << target
<< std::right << std::setw(4) << covered << " covered"
<< std::setw(6) << (coverage.size() - covered) << " undecided";
if (offered > covered) {
os << " (" << offered - covered
<< " more currently offered by an underivable general body)";
}
os << "\n";
}
}

110
mac/coverage.h Normal file
View File

@@ -0,0 +1,110 @@
#pragma once
#include <iosfwd>
#include <string>
#include <vector>
class Machine;
// Target coverage: which targets a klammer can actually render to.
//
// Coverage is a FACT about a klammer, distinct from a klammer set's claim
// about what it supports and from what happens when a document meets a
// target. This module computes the fact, and computes only what can be
// computed -- it changes nothing about how the Machine behaves. See
// notes/target_coverage.md for why the fact has to come first.
//
// Three rules, and the whole of the analysis is deciding which one applies:
//
// DERIVED where coverage is structurally determined. A general
// definition (no target suffix) whose body is text and nothing
// else covers every target. One whose body calls other klammers
// covers the INTERSECTION of what those klammers cover -- a
// klammer can only render where everything it is made of renders.
//
// DECLARED where coverage depends on something the engine cannot
// interpret. A general body holding an @eval is the main case:
// deciding which targets a Python function answers for is
// undecidable, so the targets have to be written down (which is
// what the comma-separated target list is for). A general body
// holding a ^'...'^ literal span is the same problem wearing
// different clothes -- the span exists precisely to carry raw
// target markup past the escaping pass, so a body containing one
// is target-specific with nothing for the intersection rule to
// see. @read is included: its content is not known statically.
//
// DECLARED-ALL a definition written ".*" asserts that the klammer works for
// EVERY target, including targets that do not exist yet. A list
// of the targets defined today cannot say that. It is the one
// target declaration a machine could later falsify: a ".*"
// klammer whose implementation branches per target is
// contradicting itself, which is a structural property.
//
// UNKNOWN where neither applies -- no definition at all. Absence means
// "not decided yet", never "deliberately unavailable": the SKS is
// incomplete on schedule rather than by design, so nothing may
// read a missing definition as a statement of intent.
//
// The intersection is computed as a GREATEST FIXPOINT rather than by
// recursion: general klammers may call each other, and a cycle would not
// terminate. Every general klammer starts at "all targets" and the rule is
// applied until nothing shrinks, which terminates because the sets only ever
// lose members. The pass runs over the whole registry AFTER loading, not at
// definition time -- definitions load in file order, so a body may call a
// klammer defined later.
enum class coverage_t {
all_declared, // written ".*": every target, including ones not yet defined
all, // general body, no klammer calls: every target
derived, // general body of klammer calls: their intersection
declared, // written per target, no general body to derive from
undecidable, // general body holding @eval, @read, or a literal span
none, // declared (.k) but never defined
};
struct Klammer_coverage
{
std::string m_name {};
coverage_t m_kind { coverage_t::none };
// The targets this klammer can render to, as computed.
std::vector<std::string> m_targets {};
// Targets the Machine currently offers it for. These differ exactly
// where a general body is copied to targets it cannot really serve, which
// is the hazard the report exists to surface.
std::vector<std::string> m_effective {};
// Targets named in a written definition (the comma-list, or one per
// definition), empty for a purely general klammer.
std::vector<std::string> m_written {};
// For `derived`: the klammers the body calls. For `undecidable`: why.
std::vector<std::string> m_from {};
std::string m_reason {};
// Whether a ".k" declaration exists. A klammer without one still works
// -- its parameters can be declared on the definition itself -- but it
// has no DESCRIPTION, so kdesc can say nothing about what it does and
// "kdesc -k <text>" can only find it by name.
bool m_declared { false };
// The file(s) the klammer is written in, in definition order. Usually
// one -- a klammer's targets are declared together -- but a klammer whose
// definitions are spread over several files lists them all. Shown by
// "--coverage -v", and only on a row that names ONE klammer.
std::vector<std::string> m_files {};
};
// Compute the coverage of every klammer the machine has loaded. Analysis
// only: nothing in the Machine is modified.
std::vector<Klammer_coverage> klammer_coverage(const Machine& machine);
// The report behind "kdesc --coverage". `full` ("--coverage all") adds the
// source-file column and shows every "Needs attention" category, including
// the empty ones; without it those categories appear only when they have
// entries, so a klammer set with nothing wrong produces a short report.
//
// It is an argument rather than a verbosity level because the two are
// independent: -v says how much to show about the command's PROCESSING, and
// this says what the command's RESULT contains.
//
// Audiences, in the project's terms: an AUTHOR runs it to see what is
// available for a target; a DESIGNER runs "--coverage all" to be reminded of
// the full set of categories while building a klammer set.
void report_coverage(const Machine& machine,
const std::vector<Klammer_coverage>& coverage,
bool full, std::ostream& os);

View File

@@ -10,9 +10,9 @@
using namespace std::literals;
std::regex Klammer::name_re = std::regex(R"((\w+)(?:\.(\w+))?)");
std::regex Klammer::name_re = std::regex(R"((\w+)(?:\.((?:\w+|\*)(?:,(?:\w+|\*))*))?)");
std::tuple<std::string, std::string>
std::tuple<std::string, strings_t, bool>
parse_name(const Target_registry& targets, const Katom& name_katom)
{
std::string name_with_target = trim_char(name_katom.m_text, '@');
@@ -22,22 +22,70 @@ parse_name(const Target_registry& targets, const Katom& name_katom)
throw Parsing_error(
"The klammer name \"" + name_with_target + "\" is not correctly defined. "
"The form is \"<klammer-name>\" for general klammers or \"<klammer-name>.<target-name>\" "
"for a specialized target. The klammer defined as \"<klammer-name>.k\" specifies the "
"for a specialized target. Several targets that share one body are written as a "
"comma-separated list: \"<klammer-name>.<target-name>,<target-name>\". The klammer "
"defined as \"<klammer-name>.k\" specifies the "
"arguments and contains a description of the klammer in the definition body.",
name_katom.m_loc);
}
std::string klammer_name = match[1];
std::string target_name = match[2];
if (target_name.empty()) {
target_name = Target_registry::general_name;
std::string target_part = match[2];
if (target_part.empty()) {
// No suffix at all: the general target, but not a STATEMENT about
// coverage. This is the writer's macro form -- someone defining a
// repeated phrase is not building a klammer set -- so the third
// result is false and the coverage analysis treats the body on its
// merits rather than as an assertion.
return { klammer_name, { Target_registry::general_name }, false };
}
strings_t target_names = regex_split(target_part, std::regex(","));
strings_t seen {};
for (const auto& target_name : target_names) {
if (!targets.has(target_name)) {
throw Target_error(
"The target \"" + target_name + "\" in klammer definition \"" + name_with_target + "\" "
"is not defined. Enter \"kdesc -t\" to see the targets defined by the Standard Klammer Set.",
name_katom.m_loc);
}
return { klammer_name, target_name };
if (is_in(target_name, seen)) {
throw Target_error(
"The target \"" + target_name + "\" is named more than once in klammer definition \"" +
name_with_target + "\".",
name_katom.m_loc);
}
seen.push_back(target_name);
}
// A ".k" declaration states ONE interface for every target, and a ".o"
// declares an option set; neither produces output, so neither has any
// meaning as a member of a list of output targets.
if (target_names.size() > 1) {
for (const auto& reserved :
{ Target_registry::declare_name, Target_registry::optionset_name }) {
if (is_in(reserved, target_names)) {
throw Target_error(
"The klammer definition \"" + name_with_target + "\" names \"" + reserved +
"\" in a list of targets. A \"." + reserved + "\" definition declares an "
"interface for all targets rather than producing output for one, so it must "
"be written on its own.",
name_katom.m_loc);
}
}
}
// "*" written out is an ASSERTION: this klammer works for every target,
// including targets that do not exist yet. A list of "all the targets
// defined today" cannot say that, and the difference matters the moment a
// new target is added. It may not appear IN a list -- "all targets and
// also html" is either redundant or a misunderstanding.
bool general_declared = is_in(Target_registry::general_name, target_names);
if (general_declared && target_names.size() > 1) {
throw Target_error(
"The klammer definition \"" + name_with_target + "\" names \"" +
Target_registry::general_name + "\" in a list of targets. \"" +
Target_registry::general_name + "\" already means every target, so it "
"must be written on its own.",
name_katom.m_loc);
}
return { klammer_name, target_names, general_declared };
}
std::tuple<Katom, Parameter_set, katom_list, Locator>
@@ -146,12 +194,50 @@ void Klammer::remove_target_definition(const std::string& target_name)
// Rationalize multiple definitions
// ONE definition in the source can register more than once. A target that
// "provides" another registers both (the SKS's pdf includes tex, so
// "@@fraktur.tex : ..." becomes a tex definition and a pdf one), and so does
// every member of a comma-separated target list. Counting or listing those
// registrations reports work the author did not do: "2 definitions" for a
// single line, followed by that same line printed twice -- which sends the
// reader hunting for a second definition that does not exist.
//
// These two report what was WRITTEN. Registrations are grouped by source
// location, and a location that produced several targets names them, so the
// count and the listing agree with the file.
using location_group = std::pair<std::string, strings_t>;
std::vector<location_group> group_by_location(const auto& components)
{
std::vector<location_group> groups {};
for (const auto& c : components) {
std::string loc = c.loc.desc();
auto it = std::find_if(groups.begin(), groups.end(),
[&loc](const location_group& g) { return g.first == loc; });
if (it == groups.end()) {
groups.push_back({loc, {c.target}});
} else {
it->second.push_back(c.target);
}
}
return groups;
}
int written_count(const auto& components)
{
return static_cast<int>(group_by_location(components).size());
}
std::string error_list(const std::string& label, const auto& components, const std::string& after="")
{
std::stringstream ss {};
ss << label << ":\n";
for (const auto& c : components) {
ss << " " << c.loc.desc() << "\n";
for (const auto& [loc, targets] : group_by_location(components)) {
ss << " " << loc;
if (targets.size() > 1) {
ss << " (targets " << join(targets, ", ") << ")";
}
ss << "\n";
}
ss << after;
return ss.str();
@@ -182,7 +268,7 @@ void Klammer::disallow_instances() //Klammer::components declaration)
{
auto instances = instance_defs();
if (!instances.empty()) {
int icount = instances.size();
int icount = written_count(instances);
std::stringstream ss {};
ss << "There " << to_be(icount) << " " << icount << " "
<< plural("instance", icount) << " (defined by \"::\"), but "
@@ -201,9 +287,9 @@ bool Klammer::copy_to_instances(const Target_registry& targets)
[] (const auto& def) {
return def.deftype != katom_t::klammer_instance
&& def.deftype != katom_t::klammer_override; });
int dcount = definitions.size();
int dcount = written_count(definitions);
if (dcount != 1) {
int icount = instances.size();
int icount = written_count(instances);
std::stringstream ss {};
ss << "There " << to_be(icount) << " " << icount << " "
<< plural("instance", icount) << " (defined by \"::\"), but "
@@ -268,11 +354,13 @@ void Klammer::check_for_declaration_and_definitions()
}
}
if (!definitions.empty()) {
auto defsize = definitions.size();
std::string desc = defsize == 1 ? "a definition" :
std::to_string(defsize) + " definitions";
int defsize = written_count(definitions);
std::string desc = defsize == 1 ? "a definition that declares its own parameters"
: std::to_string(defsize) + " definitions that declare their own parameters";
throw Definition_error(
error_list("A klammer has both a declaration (.k) as well as " + desc + "\n(instances are defined by \"::\")",
error_list("A klammer has both a \".k\" declaration and " + desc +
".\nWrite \"::\" instead of \":\" so the definition takes its parameters "
"from the declaration",
definitions),
declares[0].loc, false);
}

View File

@@ -18,7 +18,9 @@ public:
using variable_map_t = std::map<std::string, std::vector<int>>;
using target_variable_map_t = std::map<std::string, variable_map_t>;
static std::regex name_re; // = std::regex(R"((\w+)(?:\.(\w+))?)");
// <name>[.<target>[,<target>...]] -- the target part is a comma-separated
// list so that one body can serve several targets (see parse_name).
static std::regex name_re; // = std::regex(R"((\w+)(?:\.(\w+(?:,\w+)*))?)");
struct components {
std::string target;
@@ -93,11 +95,26 @@ public:
// target -> true if this target's body came from a general ("*") definition
// (writer content, subject to target escaping) vs a target-specific one.
std::map<std::string, bool> m_body_generic {};
// True when a definition wrote the general target out as ".*" instead of
// omitting the suffix. Identical to the engine; to a reader it is the
// difference between "I did not say" and "I say: every target, whatever
// they turn out to be". Only the second is a claim the coverage report
// can repeat.
bool m_general_declared { false };
};
std::string klammer_name_from_katom(const std::string& s, const Locator& loc);
std::tuple<std::string,std::string>
// Split "@@<name>[.<target>[,<target>...]]" into the klammer name and the
// targets the definition is for. Always at least one name: an absent suffix
// is the general target. A comma list is surface syntax only -- the caller
// registers one definition per target, so nothing downstream of registration
// knows a list was written.
// The third result is true when the general target was written out as "*"
// rather than left off. Both mean the same to the engine; they mean
// different things to a reader and to the coverage analysis -- see
// Klammer::m_general_declared.
std::tuple<std::string,strings_t,bool>
parse_name(const Target_registry& targets, const Katom& name_katom);
// Split a definition's katoms into its definition separator, parameters,

View File

@@ -18,13 +18,11 @@ void Klammer_registry::add(
{
(void)K::log(3, *begin, *(end - 1));
restore_initial_type(begin, end);
auto [klammer_name, target_name] = parse_name(targets, *begin);
if (!targets.has(target_name)) {
throw Argument_error("The target \"" + target_name + "\" is not defined", begin->m_loc);
}
if (target_name == Target_registry::optionset_name) {
auto [klammer_name, target_names, general_declared] = parse_name(targets, *begin);
if (is_in(Target_registry::optionset_name, target_names)) {
// The Machine routes an ".o" definition to the option set registry;
// reaching here means it did not.
// reaching here means it did not. parse_name has already rejected
// ".o" as a member of a list, so this is the bare ".o" form.
throw Internal_error(
"The option set declaration \"" + klammer_name + ".o\" reached the klammer registry",
begin->m_loc);
@@ -41,7 +39,20 @@ void Klammer_registry::add(
if (m_klammers.count(klammer_name) == 0) {
m_klammers[klammer_name] = Klammer(klammer_name);
} else if (m_klammers[klammer_name].m_defloc.count(target_name) > 0) {
}
// Sticky: once a definition has written ".*", the klammer carries the
// claim. A klammer with both a ".*" body and a target-specific one still
// asserts that the general body serves everything else.
if (general_declared) {
m_klammers[klammer_name].m_general_declared = true;
}
// One definition per target named. A comma-separated list is surface
// syntax: each target goes through the same registration, including the
// redefinition transition table, so a list that collides with an existing
// definition is decided per target -- one member may be silently ignored
// or rejected while the others are created.
for (const auto& target_name : target_names) {
if (m_klammers[klammer_name].m_defloc.count(target_name) > 0) {
defmode_t existing_mode = m_klammers[klammer_name].m_defmode[target_name];
const auto& result = defmode_transition(existing_mode, incoming_mode);
std::string name_target = klammer_name + "." + target_name;
@@ -49,9 +60,7 @@ void Klammer_registry::add(
if (!result.replace) {
if (result.message.empty()) {
// Silent ignore (e.g., create + default)
modify_type(katom_t::replaced, begin, end);
ignore_whitespace(end, katoms);
return;
continue;
}
std::string msg = result.message;
msg = string_replace(msg, "NAME", q_(name_target));
@@ -71,7 +80,6 @@ void Klammer_registry::add(
// This add's target:
Target target = targets.get(target_name, begin->m_loc);
if (!target.m_provides.empty()) {
for (const auto& provide_name : target.m_provides) {
if (m_klammers[klammer_name].m_defloc.count(provide_name) > 0) {
m_klammers[klammer_name].remove_target_definition(provide_name);
@@ -164,7 +172,7 @@ std::string Klammer_registry::instance_list(int margin) const
return ss.str();
}
std::string Klammer_registry::describe(int margin) const
std::string Klammer_registry::describe(int margin, const std::string& search) const
{
/*
strings_t names {};
@@ -172,7 +180,13 @@ std::string Klammer_registry::describe(int margin) const
strings_t locations {};
*/
std::string result;
std::string query = collapse_whitespace(search);
for (const auto& [name, k] : m_klammers) {
if (!query.empty() &&
!contains_fold(name, query) &&
!contains_fold(collapse_whitespace(k.description_text()), query)) {
continue;
}
result += k.describe(margin) + "\n";
/*
names.push_back(name);

View File

@@ -14,7 +14,12 @@ public:
void check_klammer(const std::string& name, const std::string& target, const Locator& loc) const;
const std::vector<Katom>* constant_body(const std::string& name) const;
std::string instance_list(int margin) const;
std::string describe(int margin=0) const;
// With a search string, only the klammers whose NAME or DESCRIPTION
// contains it, case-insensitively and with whitespace collapsed on both
// sides (a description written across several lines in a ".k" file must
// still match a phrase typed on one). Empty when nothing matched, which
// is how the caller knows to say so.
std::string describe(int margin=0, const std::string& search="") const;
std::map<std::string, Klammer> m_klammers {};
};

View File

@@ -35,6 +35,16 @@ inline const std::string ws_newline_s { R"(#/\d*)" };
// const std::string k_name = R"([^^@#|\:]+)";
inline const std::string definition_name = R"([a-zA-Z][a-zA-Z0-9_.]*)";
// The name at the START of a definition may carry a comma-separated list of
// targets ("@@table.html,tex"), so its katom runs through commas. This is
// deliberately looser than the grammar: the katomizer's job is to delimit the
// token, and parse_name() in klammer.cpp validates the list, where a
// malformed one can be reported with its location. The comma stays out of
// definition_name itself -- that pattern also delimits klammer APPLICATIONS,
// option names and "*arg*" variables, where a comma is ordinary writer text.
// "*" is here for the same reason: "@@date.*" declares a klammer for every
// target explicitly, and "*" is a Klammertext special everywhere else.
inline const std::string definition_begin_name = R"([a-zA-Z][a-zA-Z0-9_.,*]*)";
enum class katom_t {
@@ -163,7 +173,7 @@ std::vector<Ktype> katom_types {
Ktype(katom_t::apply_begin, "apply-begin", at_s + definition_name, "Beginning of a klammer call"),
Ktype(katom_t::apply_end, "apply-end", "(" + definition_name + ")?" + at_s, "End of a klammer call"),
Ktype(katom_t::option_name, "option-name", ":" + definition_name, "Optional argument name"),
Ktype(katom_t::define_begin, "define-begin", at2_s + definition_name, "Beginning of a klammer definition"),
Ktype(katom_t::define_begin, "define-begin", at2_s + definition_begin_name, "Beginning of a klammer definition"),
Ktype(katom_t::define_end, "define-end", "(" + definition_name + ")?" + at2_s, "End of a klammer definition"),
Ktype(katom_t::klammer_default, "klammer-default", "::::", "Define klammer default value for possible override"),
Ktype(katom_t::klammer_override, "klammer-override", ":::", "Override existing klammer definition"),

View File

@@ -528,8 +528,11 @@ void Machine::add_definition(katom_list& katoms, const Katom& op, const Katom& c
{
expand_constant_klammers(katoms, op, cl);
auto [begin, end] = find_span_katoms(katoms, op, cl);
auto [name, target] = parse_name(m_targets, *begin);
if (target == Target_registry::optionset_name) {
auto [name, target_names, general_declared] = parse_name(m_targets, *begin);
(void)general_declared; // routing only cares whether this is a ".o"
// parse_name rejects ".o" as a member of a target list, so an option set
// declaration is always the single-target form.
if (target_names.size() == 1 && target_names[0] == Target_registry::optionset_name) {
m_option_sets.add(m_argtypes, name, begin, end, katoms);
} else {
m_klammers.add(m_argtypes, m_targets, m_option_sets, begin, end, katoms);

View File

@@ -17,7 +17,7 @@ Target_registry::Target_registry()
: m_parameters(Parameter_set("name | desc :after_apply :after_write :includes :escape | transforms.rest"))
{
Target k(declare_name, "Description of parameters and klammer result", Locator());
Target general(general_name, "General target, used when a target is not specified", Locator());
Target general(general_name, "Every target: written \".*\" to declare a klammer works for all of them, or implied when a definition names no target", Locator());
Target option_set(optionset_name, "Declaration of an option set: parameters shared by klammers", Locator());
add(k);
add(general);

View File

@@ -134,6 +134,36 @@ strings_t word_split(const std::string& s)
return regex_split(s, std::regex("\\s+"));
}
std::string collapse_whitespace(const std::string& s)
{
std::string result {};
bool in_space = true; // leading whitespace is dropped
for (char c : s) {
if (std::isspace(static_cast<unsigned char>(c)) != 0) {
in_space = true;
} else {
if (in_space && !result.empty()) {
result += ' ';
}
in_space = false;
result += c;
}
}
return result;
}
bool contains_fold(const std::string& haystack, const std::string& needle)
{
auto fold = [](const std::string& s) {
std::string result {};
for (char c : s) {
result += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return result;
};
return fold(haystack).find(fold(needle)) != std::string::npos;
}
bool is_in(const std::string& s, const strings_t& v)
{
return find(v.begin(), v.end(), s) != v.end();

View File

@@ -38,6 +38,19 @@ bool contains(const std::string& str, const std::string& substr);
bool contains(const std::vector<std::string>& strings, const std::string& element);
std::vector<std::string> regex_split(const std::string& s, const std::regex& re, bool trim_parts=true);
std::vector<std::string> word_split(const std::string& s);
// Every run of whitespace becomes one space, and the ends are trimmed. Needed
// wherever a phrase typed on one line is matched against text that was written
// across several: a klammer's description sits on its own lines in a ".k" file
// and may wrap, so "displayed verbatim" would otherwise match the klammer
// whose description happens to fit one line and miss the identically worded
// one that does not.
std::string collapse_whitespace(const std::string& s);
// Case-insensitive substring test, ASCII folding only. The project takes no
// ICU or locale dependency (the same rule as the language tables and
// ":decimal"), so a non-ASCII query matches only exactly.
bool contains_fold(const std::string& haystack, const std::string& needle);
bool is_in(const std::string& s, const std::vector<std::string>& v);
bool is_not_in(const std::string& s, const std::vector<std::string>& v);
std::vector<std::string> find_all(const std::string& str, const std::regex& pattern, int match_group=0);

View File

@@ -25,11 +25,12 @@ vertical mode is a no-op.
@@sp.html :: &^#160; @@
@@sp.tex :: ~ @@
@@footnote.k s : Footnote (TBD) @@
@@footnote :: [*s*] @@
# TODO: Easy in LaTeX; how to handle in HTML and plain text?
# @@footnote.k s : Footnote (TBD) @@
# @@footnote :: [*s*] @@
@@indent.k s :w.int 3 :linebreak.bool false : Indented block @@
@@indent :: @eval block.Indent(K) eval@ @@
@@indent.html,tex,txt :: @eval block.Indent(K) eval@ @@
@@quote.k s :w.int 1 :source : Quotation block @@
@@ -40,18 +41,7 @@ vertical mode is a no-op.
@@
@@quote.tex ::
ANDY: QUOTE: *s*
#[
\hspace*{@{justify.length_mul("|margin|", 1, 'latex')}@}
\begin{minipage}{\textwidth- @{justify.length_mul("|margin|", 2, 'latex')}@ }
\raggedright
|text|
@? """|source|""" |?
\vspace*{6pt}
{\begin{spacing}{1.1}\footnotesize\raggedleft |source| \end{spacing}}
?@
\end{minipage}
]#
\quoteblock{*s*}{*source*}
@@
@@quote.txt ::
@@ -61,7 +51,7 @@ ANDY: QUOTE: *s*
@@note.k s :label Note :color 1.0,1.0,0.9 :bordercolor 0.2,0.2,0.2 :level.int 0 :width
: Rectangular block for a special note @@
@@note :: @eval block.Note(K) eval@ @@
@@note.html,tex :: @eval block.Note(K) eval@ @@
@@center.k s : Center text @@
@@ -94,11 +84,6 @@ ANDY: QUOTE: *s*
@@nl.tex :: \newline @@
@@nl.txt :: \n @@
@@tnl.k : Table newline (deprecated; check) @@
@@tnl.html :: <br> @@
@@tnl.tex :: \\\\ @@
@@tnl.txt :: \n @@
@@newpage.k : Start new page @@
@@newpage.html :: @@
@@newpage.tex :: \newpage @@
@@ -128,7 +113,7 @@ only makes some vertical space. @@
@@qa.k question | answer : Question and answer formatting @@
@@qa ::
@@qa.html,tex,txt ::
@b Q: @ *question*
@b A: @ *answer*
@@ -139,12 +124,13 @@ only makes some vertical space. @@
@@block.k : to.coords | content :width.float .5 :point.coords 0.0 0.0
: Absolute positioning of text block @@
@@block :: @eval block.Block(K) eval@ @@
@@block.tex :: @eval block.Block(K) eval@ @@
@@lines.k s : Maintain line breaks @@
@@lines :: @eval block.Lines(K) eval@ @@
@@lines.html,tex,txt :: @eval block.Lines(K) eval@ @@
@@twocolumns.tex s :
@@twocolumns.k s : Format *s* in two columns @@
@@twocolumns.tex ::
\begin{multicols}{2}
*s*
\end{multicols}

View File

@@ -76,10 +76,6 @@ COLOR\\setlength{\\fboxsep}{8pt}
class Block(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
#self.show()
def html(self):
return ""
def tex(self):
# NOT latex_util.block(): a textblock is absolutely positioned and
@@ -110,10 +106,6 @@ class Lines(klammer_base.Klammer_base):
def tex(self):
return "\\\\\n".join(self.lines) + "\n"
result = ""
for line in self.lines:
if line:
line += r" \\"
result += line + "\n"
result = re.sub(r"\\\\\n\n", "\n\n", result)
return result
def txt(self):
return self.lines

View File

@@ -18,3 +18,10 @@
% No space if top of page:
\newcommand{\topspace}[1]{\ifdim\pagetotal=0pt\else\vspace*{#1}\fi}
% Quote (to be improved)
\newcommand{\quoteblock}[2]{
{\hspace*{24pt}\begin{minipage}{\textwidth - 48pt}
#1
\end{minipage}}}

View File

@@ -6,15 +6,16 @@
A source file displayed verbatim
@@
@@code :: @eval code_block.Code(K) @ @@
@@code.html,tex :: @eval code_block.Code(K) @ @@
@@c.k code_text :
A word or phrase displayed verbatim in a line
@@
@@c :: @eval code_block.Code_fragment(K) eval@
@@c.html,tex :: @eval code_block.Code_fragment(K) eval@
@@
# :cwd makes the filename resolve against the DOCUMENT's directory, not
# the directory ktext happens to run in.
@@source_file filename : @eval :cwd *K_input_dir* code_block.Source(K) @ @@
@@source_file.k filename : Display the contents of the file verbatim. @@
@@source_file.html,tex :: @eval :cwd *K_input_dir* code_block.Source(K) @ @@

View File

@@ -5,4 +5,5 @@
@@@
@@color.k rgb :text : Color description as required by the target from @c <r>,<g>,<b> @ input @@
@@color :: @eval color.Color(K) eval@ @@
@@color.html,tex :: @eval color.Color(K) eval@ @@

View File

@@ -7,9 +7,8 @@
order, separator, and padding: "6/16/1910" (en, month first),
"16.06.1910" (de, day first, zero-padded per DIN 5008)
@@
@@date.html :: @eval date.date(K) eval@ @@
@@date.tex :: @eval date.date(K) eval@ @@
@@date.txt :: @eval date.date(K) eval@ @@
@@date.* :: @eval date.date(K) eval@ @@
@@datetime.k :days.int 0 :lang.language :number.bool false :
Date and time formatted as "16 June 1910, 13:10" (^:lang de:
@@ -17,21 +16,14 @@
tomorrow, ^:days -1 is yesterday). ^:lang and ^:number select the
language and the numeric form as for ^@date
@@
@@datetime.html :: @eval date.datetime(K) eval@ @@
@@datetime.tex :: @eval date.datetime(K) eval@ @@
@@datetime.txt :: @eval date.datetime(K) eval@ @@
@@datetime.* :: @eval date.datetime(K) eval@ @@
@@timestamp.k : Date and time formatted as "1910.06.16-12:34" @@
@@timestamp :: @eval time.strftime("%Y.%m.%d-%H:%M") eval@ @@
@@timestamp.* :: @eval time.strftime("%Y.%m.%d-%H:%M") eval@ @@
@@serialdate.k : Date formatted as "YYMMDD" @@
@@serialdate :: @eval time.strftime("%y%m%d") eval@ @@
@@serialdate.* :: @eval time.strftime("%y%m%d") eval@ @@
@@year.k : Current year formatted as "20XX" @@
@@year :: @eval time.strftime("%Y") eval@ @@
#[
@@@category date
date datetime timestamp serialdate
:desc Time and date formatting @@@
]#
@@year.* :: @eval time.strftime("%Y") eval@ @@

View File

@@ -62,60 +62,4 @@
: Top-level document structure
@@
@@document :: @eval :cpp *KLAMMERTEXT_HOME*/sks/document/document document @ @@
# @link https://cdn.britannica.com/09/152309-050-5E0B2A42/Sahara-Morocco.jpg :text Niagara @
@@niagara :
@link https://cdn.britannica.com/09/152309-050-5E0B2A42/Sahara-Morocco.jpg :text Niagra @
@@
#[
@@niagara.html :
<a href="https://cdn.britannica.com/09/152309-050-5E0B2A42/Sahara-Morocco.jpg">Niagara</a>
@@
@@niagara.tex :
@link https://cdn.britannica.com/09/152309-050-5E0B2A42/Sahara-Morocco.jpg :text Niagra @
@@
]#
# @@moby : Moby @@
@@moby :
But here is an artist. He desires to paint you the dreamiest,
shadiest, quietest, most enchanting bit of romantic landscape in all
the valley of the Saco. What is the chief element he employs? There
stand his trees, each with a hollow trunk, as if a hermit and a
crucifix were within; and here sleeps his meadow, and there sleep his
cattle; and up from yonder cottage goes a sleepy smoke. Deep into
distant woodlands winds a mazy way, reaching to overlapping spurs of
mountains bathed in their hill-side blue. But though the picture lies
thus tranced, and though this pine-tree shakes down its sighs like
leaves upon this shepherd's head, yet all were vain, unless the
shepherd's eye were fixed upon the magic stream before him. Go visit
the Prairies in June, when for scores on scores of miles you wade
knee-deep among Tiger-lilies---what is the one charm wanting? Water
---there is not a drop of water there! Were @niagara@ but a cataract of
sand, would you travel your thousand miles to see it?
@@
@@smoby.html :
Go visit the Prairies in June, when for scores on scores of miles you
wade knee-deep among Tiger-lilies---what is the one charm wanting?
Water --- there is not a drop of water there! Were @niagara@ but a
cataract of sand, would you travel your thousand miles to see it?
@@
@@smoby.tex :
Go visit the Prairies in June, when for scores on scores of miles you
wade knee-deep among Tiger-lilies---what is the one charm wanting?
Water --- there is not a drop of water there! Were @niagara@ but a
cataract of sand, would you travel your thousand miles to see it?
@@
@@ssmoby :
Go visit the Prairies in June, when for scores on scores of miles you
wade knee-deep among Tiger-lilies---what is the one charm wanting?
Water---there is not a drop of water there!
@@
@@document.html,tex :: @eval :cpp *KLAMMERTEXT_HOME*/sks/document/document document @ @@

View File

@@ -58,35 +58,6 @@
@@ri.tex :: \textrm{\textit{*text*}} @@
@@ri.txt :: *text* @@
#[
@@small.k s :mode span : Smaller font size @@
@@small.html :: < #- *mode* class="small"> #- *s* #- </ #- *mode* #- > @@
@@small.tex :: {\small *s*} @@
@@large.k s :mode span : Larger font size @@
@@large.html :: <*mode* class="large">*s*</*mode*> @@
@@large.tex :: {\Large *s*} @@
@@rfont.k size | leading | text : Scaled roman font @@
@@rfont.tex ::
%{\fontsize{*size*\basefontsize}{*leading* \basefontsize * \real{1.4}}\rmfamily
{\fontsize{ *size* }{ *leading* * \real{1.4}}\rmfamily
*text*
}
@@
@@sfont.k size | leading | text : Scaled sans-serif font @@
@@sfont.tex ::
{\fontsize{ *size* \basefontsize}{ *leading* \basefontsize * \real{1.4}}\sffamily
*text*}
@@
]#
@@ts.k scale.float | text : Resize *text* by *scale* @@
# @@ts.tex :: {\scalefont{*scale*}\em *text*} @@
@@ts.tex :: \scalefont{*scale*}{*text*} @@
@@ -97,37 +68,32 @@
#@@leading.tex :: \renewcommand{\baselinestretch}{*n*} @@
@@leading.tex :: \setstretch{*n*} @@
# @@default_font : @font EB Garamond @ @@
# @@default_font : @font Bitstream Charter @ @@
@@default_font :
@@default_font.k : Default font for documents @@
@@default_font.tex ::
# \setmainfont[Ligatures=TeX,Numbers=Lining]{Garamond Libre}
\setmainfont{Garamond Libre}
@@
#[
@font
# Bitstream Charter
# Liberation Serif
EB Garamond
@
]#
@@cjk.k text : Switch to font that supports CJK for *text* @@
@@cjk.html :: *cjk* @@
@@cjk.tex :: { #- @font Noto Sans CJK SC @ *cjk* #- } @@
@@cjk.txt :: *cjk* @@
@@fraktur.tex s : \textfrak{*s*} @@
@@fraktur.k s : Set text *s* in Fraktur @@
@@fraktur.tex :: \textfrak{*s*} @@
@@euro.html s : &^#x20AC;*s* @@
@@euro.tex s : €\,*s* @@
@@euro.k f : The Euro symbol, followed by the amount *f* @@
@@euro.html :: &^#x20AC;*f* @@
@@euro.tex,txt :: €\,*f* @@
@@math s :center.bool false : @i *s* @ @@ # xxx
# TODO: This needs to use MathJax.
# @@math s :center.bool false : @i *s* @ @@
@@sub.k base | script : Subscript: @sub x 2 @ @@
@@sub :: @eval font.Subsuper(K, "sub") eval@ @@
@@sub.html,tex :: @eval font.Subsuper(K, "sub") eval@ @@
@@sup.k base | script : Superscript: @sup x 2 @ @@
@@sup :: @eval font.Subsuper(K, "sup") eval@ @@
@@sup.html,tex :: @eval font.Subsuper(K, "sup") eval@ @@
@@copyright.k : Copyright symbol: © @@
@@copyright.html :: &copy; @@

View File

@@ -39,7 +39,7 @@ for in the image search path and converted to a format the target can use.
it there, and the caption arguments caption it.
@@
@@image :: @eval image.Image(K) eval@ @@
@@image.html,tex :: @eval image.Image(K) eval@ @@
@@image_grid.k
image_specs.rest(2)
@@ -56,6 +56,7 @@ it there, and the caption arguments caption it.
: A grid of images
@@
@@image_grid :: @eval image_grid.Image_grid(K) eval@ @@
@@image_grid.html,tex :: @eval image_grid.Image_grid(K) eval@ @@
@@fig spec.figure_id : @reference *spec* | Figure @ @@
@@fig.k spec.figure_id : The caption title for a figure @@
@@fig.html,tex :: @reference *spec* | Figure @ @@

View File

@@ -147,5 +147,6 @@ class Image(klammer_base.Klammer_base):
return latex_util.block(result)
def txt(self):
return f"[Image: {self.basename}\nCaption: {self.caption}]"
# Better to cause an error; there's no good text substitute for an image
# def txt(self):
# return f"[Image: {self.basename}\nCaption: {self.caption}]"

View File

@@ -172,9 +172,7 @@ other structures. The ^:offset is used only if the ^:hpos value is "left" or
# Klammers
@@reference.k spec | name : Reference marker for captioned elements @@
@@reference ::
__REF__*spec*__*name*__
@@
@@reference.* :: __REF__*spec*__*name*__ @@
@@show.k s : Show the raw Klammertext and the result @@
@@show :: @eval :cpp show show @ @@
@@show.html,tex,txt :: @eval :cpp show show @ @@

View File

@@ -1,79 +1,23 @@
@@link target
@@link.k target
:text
:footnote.bool false
:section.bool false
:nq.bool false
:basename # xxx
:basename # xxx -- Deprecated?
:code # xxx
:phrase # xxx
:dox # xxx
:doxanchor # xxx
:id # xxx
:
@eval link.Link(K) eval@
: Links within documents and to external URLs.
@@
@@target id :text :
@eval link.Target(K) eval@
@@
@@link.html,tex,txt :: @eval link.Link(K) eval@ @@
@@email name | domain :subject :body :
@eval link.Email(K) eval@
@@
@@target.k id :text : A link target within a document @@
#[
@@@category link
target link
:desc Links @@@
@@target.html,tex :: @eval link.Target(K) eval@ @@
@@target.doc id :text :
Link target in page is @i-name, with optional @i-text for target @@
@@email.k name | domain :subject :body : An email with an mangled address for HTML @@
@@target : @eval link.target(K) eval@ @@
@@link.doc url
:text
:download.bool false
:mail.bool false
:nopage.bool false
:section.bool false
:pageonly.bool false
:footnote.bool true
:in_footnote.bool false
:dox.bool false
:abs.bool false
:add_number.bool false
:
Link to @i-url using @i-text for display, or @i-url if @i-text is not
specified. If :section is true, the @i-url is the text of a section
title.
@@
@@link : @eval link.link(K) eval@ @@
@@subpage kt_basename
:pagetitle
:sectiontitle
:linktext
:index
:toc.bool true
:logo
:include
:prevlink
:nextlink
:uplink
:logo
:top_level.int
:href_base
:search.bool false
:
@eval link.subpage(K) eval@ @@
@@subpages.html s :
<!--toc-->
*s*
<!--toc-->
@@
@@subpages.latex s : *s* @@
]#
@@email.html,tex :: @eval link.Email(K) eval@ @@

View File

@@ -88,8 +88,9 @@ class Target(klammer_base.Klammer_base):
#result += '\\label{{{}-label}}'.format(id)
return result
def txt(self):
return ""
# Better to fail than return nothing; not possible to have a target in a text file.
# def txt(self):
# return ""
class Email(klammer_base.Klammer_base):

View File

@@ -4,62 +4,13 @@
@@@
@@ul.k list_items.rest :cmp.bool false :bullet : Unordered list, with list elements preceeded by the *bullet* character. @@
@@ul :: @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 :: @eval list.List(K, "ol") eval@ @@
@@ol.html,tex,txt :: @eval list.List(K, "ol") eval@ @@
@@define descriptions.rest(2) :font i : @eval list.Define(K) eval@ @@
@@define.k descriptions.rest(2) :font i : A list of words or phrases and their definitions or descriptions @@
@@define.html,tex :: @eval list.Define(K) eval@ @@
@@columns items :n.int 2 : @eval list.columns(K) eval@ @@
##
#[
@@@category list ul ulc ol li define item
:desc Ordered and unordered lists; definitions of terms @@@
]#
#[
@@describe descriptions.rest : @eval list.describe(K) eval@ @@
@@entry item | description : @eval list.entry(K) eval@ @@
]#
#[
@@ul.doc list_items.rest :cmp.bool false :bullet square :itemsep :uncover.bool :indent.bool true :parsep 6pt :margin 8pt :in_define.bool : Unordered list @@
@@ul : @eval list.ul(K) @ @@
@@ol.doc list_items.rest :cmp.bool false :itemsep :uncover.bool :parsep 6pt :margin 8pt :in_define.bool : Ordered list @@
@@ol : @eval list.ol(K) @ @@
@@li.doc s :keep.bool : List item @@
@@li : @eval list.li(K) @ @@
@@dfont :s : @eval list.dfont(K) @ @@
@@define.doc s :font r :uncover.bool : List of definitions @@
@@define : @eval list.define(K) @ @@
@@item.doc term desc :c.bool false :inlist.bool false : Definition of @i-term is @i-desc @@
@@item term desc :
@eval list.definition_item(K) @
@@
@@mli args.rest : @eval list.mli(K) @ @@
@@ncircle.latex n : \circled{*n*} @@
@@dblitem.latex a | b | desc :
\item[\parbox{\linewidth}{*a* \\ *b*}]
\leavevmode \\[-5pt]
*desc*
@@
@@dblitem.html a | b | desc :
<dt>*a*<br>
*b*</dt>
<dd>*desc*</dd>
@@
]#
@@columns.k items :n.int 2 : Create two or more columns from the text @@
@@columns.tex :: @eval list.columns(K) eval@ @@

View File

@@ -49,7 +49,6 @@ class List(klammer_base.Klammer_base):
if not self.compressed:
result += "\n"
n += 1
print(result)
return result
@@ -87,8 +86,9 @@ class Define(klammer_base.Klammer_base):
result = f"\\begin{{description}}[labelindent=12pt,nosep,itemsep=6pt]\n{result}\n\\end{{description}}"
return result
def txt(self):
return "TXT DEFINITIONS"
# Needs to be implemented:
# def txt(self):
# return "TXT DEFINITIONS"
class Lines(klammer_base.Klammer_base):

View File

@@ -2,53 +2,53 @@
# the front and back matter of a book):
@@preface.k :id :name Preface : Preface text @@
@@preface :: @eval section.Section(K, "preface") eval@ @@
@@preface.html,tex :: @eval section.Section(K, "preface") eval@ @@
# Numbered section headings:
@@part.k title :id :name Part : Part heading @@
@@part :: @eval section.Section(K, 0) eval@ @@
@@part.html,tex :: @eval section.Section(K, 0) eval@ @@
@@s1.k title :id :n : Top level division, numbered @@
@@s1 :: @eval section.Section(K, 1) eval@ @@
@@s1.html,tex :: @eval section.Section(K, 1) eval@ @@
@@s2.k title :id :n : Second level division, numbered @@
@@s2 :: @eval section.Section(K, 2) eval@ @@
@@s2.html,tex :: @eval section.Section(K, 2) eval@ @@
@@s3.k title :id :n : Third level division, numbered @@
@@s3 :: @eval section.Section(K, 3) eval@ @@
@@s3.html,tex :: @eval section.Section(K, 3) eval@ @@
@@s4.k title :id :n : Fourth level division, numbered @@
@@s4 :: @eval section.Section(K, 4) eval@ @@
@@s4.html,tex :: @eval section.Section(K, 4) eval@ @@
@@s5.k title :id :n : Fifth level division, numbered @@
@@s5 :: @eval section.Section(K, 5) eval@ @@
@@s5.html,tex :: @eval section.Section(K, 5) eval@ @@
@@s6.k title :id :n : Sixth level division, numbered @@
@@s6 :: @eval section.Section(K, 6) eval@ @@
@@s6.html,tex :: @eval section.Section(K, 6) eval@ @@
@@s7.k title :id :n : Seventh level division, numbered @@
@@s7 :: @eval section.Section(K, 7) eval@ @@
@@s7.html,tex :: @eval section.Section(K, 7) eval@ @@
# Unnumbered section headings:
@@h1.k title :id :n : Top level division, unnumbered @@
@@h1 :: @eval section.Section(K, 1, numbered=False) eval@ @@
@@h1.html,tex :: @eval section.Section(K, 1, numbered=False) eval@ @@
@@h2.k title :id : Second level division, unnumbered @@
@@h2 :: @eval section.Section(K, 2, numbered=False) eval@ @@
@@h2.html,tex :: @eval section.Section(K, 2, numbered=False) eval@ @@
@@h3.k title :id : Third level division, unnumbered @@
@@h3 :: @eval section.Section(K, 3, numbered=False) eval@ @@
@@h3.html,tex :: @eval section.Section(K, 3, numbered=False) eval@ @@
@@h4.k title :id : Fourth level division, unnumbered @@
@@h4 :: @eval section.Section(K, 4, numbered=False) eval@ @@
@@h4.html,tex :: @eval section.Section(K, 4, numbered=False) eval@ @@
@@h5.k title :id : Fifth level division, unnumbered @@
@@h5 :: @eval section.Section(K, 5, numbered=False) eval@ @@
@@h5.html,tex :: @eval section.Section(K, 5, numbered=False) eval@ @@
@@h6.k title :id : Sixth level division, unnumbered @@
@@h6 :: @eval section.Section(K, 6, numbered=False) eval@ @@
@@h6.html,tex :: @eval section.Section(K, 6, numbered=False) eval@ @@
@@h7.k title :id : Seventh level division, unnumbered @@
@@h7 :: @eval section.Section(K, 7, numbered=False) eval@ @@
@@h7.html,tex :: @eval section.Section(K, 7, numbered=False) eval@ @@

View File

@@ -72,5 +72,6 @@ class Section(klammer_base.Klammer_base):
result = f"%__sectionlink__{id}__{self.title}__" + "\n" + result
return result
def txt(self):
return self.title
# Need to implement the procedural numbering for the txt target
# def txt(self):
# return self.title

View File

@@ -6,5 +6,5 @@
:files kutil/kutil.k target/target.k font/font.k section/section.k
image/image.k code/code.k list/list.k link/link.k
table/table.k date/date.k block/block.k color/color.k
document/document.k book/book.k
document/document.k
@@@

View File

@@ -191,7 +191,8 @@
:pattern \s*([^^\s;]+\s+[^^\s;]+\s*(;\s*^|\s*$))+
@@@
@@rowcolor.tex s : \colorrow{*s*} @@
@@rowcolor.k c : Change the color of a table row to color *c* @@
@@rowcolor.tex :: \colorrow{*s*} @@
@@table.k rows.rest(2)
:id
@@ -227,8 +228,7 @@ formats them, and ^:hpos and ^:offset place the whole table in the text
column.
@@
@@table :: @eval table.Table(K) eval@ @@
@@table.html,tex :: @eval table.Table(K) eval@ @@
@@tbl spec.figure_id :
@reference *spec* | Table @
@@
@@tbl.k spec.figure_id : The caption title for a table @@
@@tbl.html,tex,txt :: @reference *spec* | Table @ @@

View File

@@ -864,6 +864,7 @@ class Table(klammer_base.Klammer_base):
# there, so the rule stays unconditional.)
return latex_util.block(result)
def txt(self):
return "Table in .txt format not implemented"
# This should signal that it has not be implemented by not being defined.
# def txt(self):
# return "Table in .txt format not implemented"

View File

@@ -19,4 +19,8 @@ test:
./klammerset_test.sh
./option_set_test.sh
./signature_test.sh
./target_list_test.sh
./coverage_test.sh
./command_option_test.sh
./kdesc_test.sh
./editor_test.sh

120
tst/command_option_test.sh Executable file
View File

@@ -0,0 +1,120 @@
#!/bin/bash
#
# command_option_test.sh — Command-line option parsing (mac/argv.cpp).
#
# The commands' own argument handling, which no suite covered: argv_test.cpp
# exercises Argv but asserts nothing, so two defects lived there unnoticed.
#
# * An option declared with opt() takes a value. Written LAST with nothing
# after it -- "kdesc -v", "ktext doc.kt -t" -- parse_optional() read one
# past the end of the word vector and the command died with SIGSEGV,
# naming nothing. Two bounds checks sat commented out at that spot; they
# would have returned a half-parsed option instead of reporting the
# mistake. It is now a located argument error.
# * kdesc and kdiag printed an error and then exited 0, so a script could
# not tell a failed run from a successful one. Both return 1 now, as
# ktext already did.
#
# Also here: two required positional arguments. parse_positional() consumes
# one per required name, but regex_split_prefix() requires its match at
# position 0 and returned the remainder with the separating space intact, so
# the second was always "not found". Latent, because no shipped command
# declares two -- tst/argv_test.cpp is the only program that does.
#
# Usage: ./command_option_test.sh (needs KLAMMERTEXT_HOME set; commands on PATH)
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
TSTDIR="$(cd "$(dirname "$0")" && pwd)"
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
# A signal death is not an error exit: 128+n, and the point of these tests is
# the difference. Report it distinctly so a crash can never read as a pass.
died_by_signal() { [ "$1" -gt 128 ]; }
# error NAME PATTERN CMD... — nonzero exit, no signal, PATTERN in the output.
error() {
local name="$1" pattern="$2"; shift 2
local out status
out=$("$@" 2>&1); status=$?
if died_by_signal $status; then
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1)); return
fi
if [ $status -eq 0 ]; then
echo "${red}FAIL${reset} $name — reported an error but exited 0"; FAIL=$((FAIL+1)); return
fi
if printf '%s' "$out" | grep -qF -- "$pattern"; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name — expected [$pattern]"
echo " got: $(printf '%s' "$out" | head -2)"; FAIL=$((FAIL+1))
fi
}
# ok NAME CMD... — exits 0 and does not die by signal.
ok() {
local name="$1"; shift
local status
"$@" >/dev/null 2>&1; status=$?
if died_by_signal $status; then
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1))
elif [ $status -ne 0 ]; then
echo "${red}FAIL${reset} $name — exit $status"; FAIL=$((FAIL+1))
else
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
fi
}
echo "${bold}Command option tests${reset}"
echo "===================="
echo
echo "-- an option written last, with no value --"
error " 1. kdesc -v" "needs a value" kdesc -v
error " 2. kdesc with a flag first" "needs a value" kdesc --katoms -v
error " 3. kdiag -v" "needs a value" kdiag -v
error " 4. ktext -t" "needs a value" ktext -s '@i-x' -t
error " 5. the message names the option" "-v <level>" kdesc -v
error " 6. ... and how to get the usage" "for the list of arguments" kdesc -v
echo
echo "-- the same options WITH a value still work --"
# NOT "kdesc -v 1": show_usage() treats exactly "<command> -v <n>" as a
# request for the usage text, which exits 1 by design. Give it another flag.
ok " 7. kdesc -c -v 1" kdesc -c -v 1
ok " 8. kdesc --katoms -v 1" kdesc --katoms -v 1
ok " 9. kdiag -v 1 '@i-x'" kdiag -v 1 '@i-x'
ok "10. ktext -t html" ktext -s '@i-x' -t html -d
echo
echo "-- an error exit is nonzero, not a printed message and exit 0 --"
error "11. kdesc reports failure" "needs a value" kdesc -v
error "12. kdiag reports failure" "needs a value" kdiag -v
echo
echo "-- two required positional arguments --"
# argv_test is the only program declaring two; it prints what it parsed.
if [ -x "$TSTDIR/argv_test" ]; then
out=$("$TSTDIR/argv_test" first second --bool 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
for expect in "<input> : first" "<input2> : second"; do
if printf '%s' "$out" | grep -qF "$expect"; then
echo "${green}PASS${reset} 13/14. positional [$expect]"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} 13/14. positional [$expect] not parsed"; FAIL=$((FAIL+1))
fi
done
else
echo "SKIP 13/14. two positionals (argv_test not built; run make -C tst)"
fi
echo
echo "===================="
echo "Results: ${PASS} passed, ${FAIL} failed"
[ "$FAIL" -eq 0 ] || exit 1
exit 0

29
tst/coverage/basic.k Normal file
View File

@@ -0,0 +1,29 @@
# Fixture for coverage_test.sh. Engine tier: it declares its own targets,
# so the suite runs with no klammer set.
@@@target ta | Target A @@@
@@@target tb | Target B @@@
@@@target tc | Target C @@@
# Defined per target: coverage is what the definitions say.
@@ab.k s : defined for ta and tb @@
@@ab.ta :: A*s* @@
@@ab.tb :: B*s* @@
# A comma list is the same statement written once (step 1).
@@cd.k s : defined for ta and tc by a list @@
@@cd.ta,tc :: C*s* @@
# General body of plain text: every target.
@@plain : just text @@
# General body calling one klammer: that klammer's coverage.
@@calls_ab s : @ab *s* @ @@
# Intersection of two klammers with different coverage: only ta is in both.
@@calls_both s : @ab *s* @ @cd *s* @ @@
# ".*" is an assertion: every target, including targets not yet defined.
# Distinct from a bare definition, which asserts nothing -- that is the
# writer's macro form.
@@universal.k s : works for any target @@
@@universal.* :: U*s* @@

8
tst/coverage/clean.k Normal file
View File

@@ -0,0 +1,8 @@
# A klammerset with nothing outstanding: every klammer declared and defined.
# Used to check that a report with no problems says nothing about problems --
# no banner, no categories.
@@@target ta | Target A @@@
@@@target tb | Target B @@@
@@x.k s : declared and defined @@
@@x.ta,tb :: [*s*] @@

22
tst/coverage/cycle.k Normal file
View File

@@ -0,0 +1,22 @@
@@@target ta | Target A @@@
@@@target tb | Target B @@@
# Mutual reference between two general klammers. A recursive analysis would
# not terminate; the greatest fixpoint does.
#
# These take a parameter deliberately. A general klammer with NO parameters
# is a CONSTANT, and a constant's body is spliced into later definitions at
# definition time -- so a parameterless pair never reaches the fixpoint as a
# cycle: the expansion has already turned the second body into a
# self-reference. Parameters keep the calls in the stored body.
@@ping s : @pong *s* @ @@
@@pong s : @ping *s* @ @@
# A cycle that reaches a constrained klammer keeps the constraint.
@@only_ta.ta s : A*s* @@
@@loop_a s : @loop_b *s* @ @only_ta *s* @ @@
@@loop_b s : @loop_a *s* @ @@
# Disjoint coverage: nothing is in both, so this can never be applied.
@@only_tb.tb s : B*s* @@
@@impossible s : @only_ta *s* @ @only_tb *s* @ @@

6
tst/coverage/split.k Normal file
View File

@@ -0,0 +1,6 @@
@@@target ta | Target A @@@
@@@target tb | Target B @@@
@@x.k s : declared here, defined in two files @@
@@x.ta :: A*s* @@
@read split_more.k @

View File

@@ -0,0 +1,5 @@
# The second half of the split fixture: one klammer's targets, written in a
# different file from its declaration. Andy's policy keeps a klammer's
# targets together, so this is the exception the file column exists to make
# visible.
@@x.tb :: B*s* @@

View File

@@ -0,0 +1,10 @@
@@@target ta | Target A @@@
@@@target tb | Target B @@@
# The three bodies whose coverage the engine cannot work out.
@@evaluated : @eval 1 + 1 @ @@
@@literally : ^'\raw{markup}'^ @@
@@included : @read /dev/null @ @@
# A declaration with no definition anywhere.
@@orphan.k s : declared and never defined @@

258
tst/coverage_test.sh Executable file
View File

@@ -0,0 +1,258 @@
#!/bin/bash
#
# coverage_test.sh — Target coverage: which targets a klammer can render to.
#
# "kdesc --coverage" computes the coverage FACT and reports it. Three rules
# (mac/coverage.{h,cpp}, notes/target_coverage.md):
#
# DERIVED a general body of plain text covers every target; a general body
# of klammer calls covers the INTERSECTION of what those klammers
# cover, computed as a greatest fixpoint after loading.
# DECLARED a general body holding @eval, @read or a ^'...'^ literal span
# cannot be interpreted, so its targets must be written down.
# UNKNOWN no definition at all. Absence never means "deliberately
# unavailable" -- the SKS is incomplete on schedule, not by design.
#
# The analysis modifies nothing. These tests therefore assert only what is
# REPORTED, and a companion case checks that rendering is unaffected.
#
# Engine tier: the fixtures in tst/coverage/ declare their own targets with
# @@@target, so no klammer set is involved.
#
# Usage: ./coverage_test.sh (needs KLAMMERTEXT_HOME set; kdesc on PATH)
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
KDESC=kdesc
KTEXT=ktext
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
DIR="$(cd "$(dirname "$0")" && pwd)/coverage"
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
# Each fixture is analysed once; the tests match against the saved report.
declare -A REPORT VREPORT
for f in basic undecidable cycle split clean; do
REPORT[$f]=$(timeout 30 "$KDESC" -i "$DIR/$f.k" --coverage 2>&1 |
sed 's/\x1b\[[0-9;]*m//g')
# "all" adds the source-file column AND the empty problem categories.
VREPORT[$f]=$(timeout 30 "$KDESC" -i "$DIR/$f.k" --coverage all 2>&1 |
sed 's/\x1b\[[0-9;]*m//g')
if [ -z "${REPORT[$f]}" ] || [ -z "${VREPORT[$f]}" ]; then
echo "${red}FAIL${reset} $f.k produced no report"; FAIL=$((FAIL+1))
fi
done
# vlacks NAME FIXTURE REGEX — the VERBOSE report does NOT match REGEX.
vlacks() {
local name="$1" fixture="$2" rgx="$3"
if printf '%s\n' "${VREPORT[$fixture]}" | grep -Eq "$rgx"; then
echo "${red}FAIL${reset} $name — unexpected match: $rgx"; FAIL=$((FAIL+1))
else
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
fi
}
# vhas NAME FIXTURE REGEX — the VERBOSE report matches REGEX.
vhas() {
local name="$1" fixture="$2" rgx="$3"
if printf '%s\n' "${VREPORT[$fixture]}" | grep -Eq "$rgx"; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name"
echo " no line matching: $rgx"; FAIL=$((FAIL+1))
fi
}
# has NAME FIXTURE REGEX — the report matches REGEX.
has() {
local name="$1" fixture="$2" rgx="$3"
if printf '%s\n' "${REPORT[$fixture]}" | grep -Eq "$rgx"; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name"
echo " no line matching: $rgx"; FAIL=$((FAIL+1))
fi
}
# section_lacks NAME FIXTURE HEADER REGEX — REGEX does not appear within the
# named section. Needed wherever the same klammer legitimately appears in a
# LATER section: a whole-report "lacks" would match there and report a failure
# that is not one. A section runs from its header to the next blank line.
section_lacks() {
local name="$1" fixture="$2" header="$3" rgx="$4"
local body
body=$(printf '%s\n' "${REPORT[$fixture]}" |
awk -v h="$header" 'index($0, h) == 1 { f = 1; next } f && /^$/ { exit } f')
if [ -z "$body" ]; then
echo "${red}FAIL${reset} $name — section [$header] not found"; FAIL=$((FAIL+1)); return
fi
if printf '%s\n' "$body" | grep -Eq "$rgx"; then
echo "${red}FAIL${reset} $name — unexpected match in [$header]: $rgx"; FAIL=$((FAIL+1))
else
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
fi
}
# lacks NAME FIXTURE REGEX — the report does NOT match REGEX.
lacks() {
local name="$1" fixture="$2" rgx="$3"
if printf '%s\n' "${REPORT[$fixture]}" | grep -Eq "$rgx"; then
echo "${red}FAIL${reset} $name — unexpected match: $rgx"; FAIL=$((FAIL+1))
else
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
fi
}
echo "${bold}Target coverage tests${reset}"
echo "====================="
echo
echo "-- derived coverage --"
has " 1. a general text body covers every target" basic '^All targets, derived \(1\)'
has " 2. ... and that body is @plain" basic '@plain'
has " 3. a body calling one klammer takes its targets" basic '@calls_ab +ta tb +from @ab'
has " 4. two klammers intersect" basic '@calls_both +ta +from @ab @cd'
lacks " 5. the intersection drops the target only one has" basic '@calls_both +ta tb'
echo
echo "-- what the definitions themselves say --"
has " 6. per-target definitions are not derived" basic 'Defined per target \(2\)'
has " 7. a comma list covers each target it names" basic '^ ta +6 covered'
has " 8. a target named by no definition is undecided" basic '^ tc +3 covered +3 undecided'
echo
echo "-- coverage that cannot be derived --"
has " 9. an @eval general body must be declared" undecidable '@evaluated +@eval body'
has "10. a @read general body must be declared" undecidable '@included +@read body'
has "11. a literal span must be declared" undecidable "@literally +\\^'\\.\\.\\.'\\^ literal span"
has "12. all three are counted together" undecidable '^Must be declared \(3\)'
has "13. the report says they are offered everywhere" undecidable 'currently offered by an underivable general body'
echo
echo "-- declared and never defined --"
has "14. a .k with no definitions is reported" undecidable '^Declared but never defined \(1\)'
has "15. ... and named" undecidable '^ @orphan$'
echo
echo "-- \".*\": every target, asserted --"
# The distinction the report exists to keep: a bare definition asserts
# nothing (the writer's macro form), while ".*" states that the klammer works
# for any target, including targets that do not exist yet. A list of the
# targets defined today cannot say that.
has "37. an explicit .* is its own category" basic '^All targets, declared \(1\)'
has "38. ... and covers every target" basic '^ @universal +ta tb tc'
lacks "39. it is not counted as derived" basic '^ @universal +ta tb tc$(.*)from'
section_lacks "40. nor as a bare general body" basic 'All targets, derived' '@universal'
# ".*" outranks the body scan: the point of writing it is to assert what an
# @eval body cannot be read to mean.
has "41. an @eval body under .* is not flagged" basic '^All targets, declared'
echo
echo "-- klammers with no \".k\" declaration --"
# The mirror image of the section above: that one has a declaration and no
# definitions, this one definitions and no declaration. Such a klammer works
# but has no DESCRIPTION, so kdesc can say nothing about it and "-k <text>"
# can only find it by name. Orthogonal to the coverage kinds -- a klammer can
# be defined for every target and still have none.
has "32. the section counts them" basic '^No "\.k" declaration \(3\)'
has "33. a general klammer has none" basic '^ @plain +ta tb tc$'
has "34. ... nor one defined without .k" basic '^ @calls_ab +ta tb$'
section_lacks "35. a declared klammer is not listed" basic 'No ".k" declaration' '^ @ab '
# @orphan has a .k and no definitions, so it belongs to the OTHER section.
lacks "36. the two sections do not overlap" undecidable '^No "\.k" declaration \(1\)\n @orphan'
echo
echo "-- the fixpoint --"
# A recursive analysis would not terminate on these; the report existing at
# all is most of the assertion.
has "16. mutual reference terminates" cycle '@ping +ta tb +from @pong'
has "17. ... in both directions" cycle '@pong +ta tb +from @ping'
has "18. a constraint propagates around a cycle" cycle '@loop_a +ta +from @loop_b @only_ta'
has "19. ... to the klammer that does not name it" cycle '@loop_b +ta +from @loop_a'
has "20. disjoint coverage is its own category" cycle '^Covers no target \(1\)'
has "21. ... naming the klammers to look at" cycle '^ @impossible +from @only_ta @only_tb'
section_lacks "42. and it is not listed as derived" cycle 'Derived from the klammers' '@impossible'
echo
echo "-- the source-file column (-v only) --"
# The file is the LAST column -- anchored, so a change of position is caught.
vhas "23. a klammer's file is shown, last" undecidable '@evaluated +@eval body +tst/coverage/undecidable\.k$'
vhas "24. ... in the derived section too" basic '@calls_ab +ta tb +from @ab +tst/coverage/basic\.k$'
vhas "25. ... and in the per-target listing" basic '@ab +ta tb +tst/coverage/basic\.k$'
vhas "26. a declaration with no definition" undecidable '^ @orphan +tst/coverage/undecidable\.k$'
lacks "27. no file column without -v" basic '@calls_ab +tst/coverage'
# Andy's policy keeps a klammer's targets together, so several files is the
# exception -- which is the case the column exists to make visible.
vhas "28. definitions in two files, comma-separated" split \
'@x +ta tb +tst/coverage/split\.k, tst/coverage/split_more\.k$'
# The "All targets" section lists many names on one line, so there is nothing
# for a file to attach to; it must not sprout a column.
vhas "29. the many-names row keeps its shape" basic '^ @plain$'
# A continuation line belongs to the row above it and takes no file.
lacks "30. the old continuation line is gone" cycle '^ +\^ covers no target'
echo
echo "-- the problems come last, and are grouped --"
# A terminal is read from the bottom: an 80-klammer listing scrolls a
# three-line warning off the screen, so the actionable part must be last.
has "43. a banner counts the distinct klammers" basic '^Needs attention: [0-9]+ klammers?$'
# An empty category is hidden, but the two halves hide for different reasons.
#
# A REPORTING category describes the shape of the klammer set, so an empty one
# still says something and "all" shows it as a designer's checklist.
lacks "44. an empty reporting category is hidden" cycle '^All targets, declared \(0\)'
vhas "45. ... and shown by \"all\"" cycle '^All targets, declared \(0\)'
# A PROBLEM category sits under a banner reading "Needs attention", and an
# empty one does not. Printing it there would state the opposite of the
# heading above it, so it stays hidden even under "all".
lacks "47. an empty problem category is hidden" basic '^Covers no target \(0\)'
vlacks "48. ... and stays hidden under \"all\"" basic '^Covers no target \(0\)'
# A category WITH entries is always shown, with or without "all".
has "49. a non-empty category needs no \"all\"" cycle '^Derived from the klammers the body calls \(4\)'
# The banner still counts, so "all" is not silent about the problems.
vhas "50. the banner survives under \"all\"" basic '^Needs attention: 3 klammers$'
# "No .k" is orthogonal to the others, so summing the counts would overstate.
has "46. the banner counts distinct klammers" basic '^Needs attention: 3 klammers$'
echo
echo "-- the category explanations --"
# The prose under a heading teaches the categories; a reader who knows them
# wants headings and rows. So it appears only under "all", with the empty
# categories and the file column.
lacks "51. no explanation by default" basic 'the author states that these work'
vhas "52. ... and one under \"all\"" basic 'the author states that these work'
has "53. the heading is always there" basic '^All targets, declared \(1\)'
echo
echo "-- a klammer set with nothing outstanding --"
# Nothing to attend to, nothing said: no banner and no problem categories,
# with or without "all". A banner reading "0" would contradict itself in the
# same way an empty category under it would.
lacks "54. no banner when nothing needs attention" clean '^Needs attention'
vlacks "55. ... not even under \"all\"" clean '^Needs attention'
vlacks "56. ... and no problem categories" clean '^No "\.k" declaration'
# The reporting categories still describe the set.
vhas "57. the reporting categories remain" clean '^Defined per target \(1\)'
echo
echo "-- the analysis changes nothing --"
# Coverage is a report, not a policy: a klammer whose general body cannot be
# interpreted is still offered to every target, exactly as before.
out=$("$KTEXT" -k none -s '@@@target ta | Target A @@@
@@e : @eval 6 * 7 @ @@ x @e@' -t ta -d 2>&1 | tr -d '\n ')
if [ "$out" = "x42" ]; then
echo "${green}PASS${reset} 22. an underivable klammer still renders"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} 22. an underivable klammer still renders — got [$out]"; FAIL=$((FAIL+1))
fi
echo
echo "====================="
echo "Results: ${PASS} passed, ${FAIL} failed"
[ "$FAIL" -eq 0 ] || exit 1
exit 0

158
tst/kdesc_test.sh Executable file
View File

@@ -0,0 +1,158 @@
#!/bin/bash
#
# kdesc_test.sh — the kdesc command's interface.
#
# The flag structure was reorganised 2026-08-11
# (notes/modifying_the_kdesc_arguments.md) on two rules:
#
# * a flag a user reaches for often gets a single letter (-k klammers,
# -t targets, -c character codes, -i input); a more specialised topic gets
# a multi-letter name (--argtypes, --katoms, --rewrite, --optionsets,
# --coverage, --klammerset, --font);
# * -v says how much to show about the command's PROCESSING and never what
# its RESULT contains. So the katom regex column is "--katoms full" and
# the coverage file column is "--coverage all", not verbosity levels.
#
# "-k" is the one rename that changes a name's MEANING rather than retiring
# it: it used to show katom types (now --katoms). A name-based guard cannot
# catch that -- the old spelling still works and does something else -- so
# these tests pin the new meaning down.
#
# Usage: ./kdesc_test.sh (needs KLAMMERTEXT_HOME set; kdesc on PATH)
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
KDESC=kdesc
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
DIR="$(cd "$(dirname "$0")" && pwd)/coverage"
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
plain() { sed 's/\x1b\[[0-9;]*m//g'; }
# shows NAME PATTERN CMD... — exits 0 and the output matches PATTERN.
shows() {
local name="$1" pattern="$2"; shift 2
local out status
out=$(timeout 60 "$@" 2>&1 | plain); status=$?
if [ $status -gt 128 ]; then
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1)); return
fi
if printf '%s' "$out" | grep -Eq -- "$pattern"; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name — no line matching [$pattern]"
echo " got: $(printf '%s' "$out" | head -2)"; FAIL=$((FAIL+1))
fi
}
# absent NAME PATTERN CMD... — the output does NOT match PATTERN.
absent() {
local name="$1" pattern="$2"; shift 2
if timeout 60 "$@" 2>&1 | plain | grep -Eq -- "$pattern"; then
echo "${red}FAIL${reset} $name — unexpected match [$pattern]"; FAIL=$((FAIL+1))
else
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
fi
}
echo "${bold}kdesc interface tests${reset}"
echo "====================="
echo
echo "-- the single-letter flags --"
shows " 1. -k lists klammers" '@table' "$KDESC" -k
shows " 2. -t lists targets" 'html' "$KDESC" -t
shows " 3. -c lists character codes" '.' "$KDESC" -c
shows " 4. -i reads the named input" '@x' "$KDESC" -i "$DIR/split.k" -k
echo
echo "-- the multi-letter topics --"
shows " 5. --katoms lists katom types" 'katom' "$KDESC" --katoms
shows " 6. --argtypes lists argtypes" 'fraction' "$KDESC" --argtypes
shows " 7. --rewrite lists rewrites" '.' "$KDESC" --rewrite
shows " 8. --optionsets lists sets" 'caption_args' "$KDESC" --optionsets
echo
echo "-- result detail is a word, not a verbosity level --"
shows " 9. --katoms full adds the regex column" 'Regex' "$KDESC" --katoms full
absent "10. --katoms alone omits it" 'Regex' "$KDESC" --katoms
shows "11. --coverage all adds the file column" 'sks/table/table\.k' "$KDESC" --coverage all
absent "12. --coverage alone omits it" 'sks/table/table\.k' "$KDESC" --coverage
# "all" is one word for both kinds of missing information: if you want some of
# it you probably want all of it. It shows an empty REPORTING category -- a
# designer's checklist of the shapes a klammer set can have -- but not an
# empty PROBLEM one, which would state the opposite of the "Needs attention"
# banner above it.
# Against a FIXTURE, not the SKS: which categories are empty there changes as
# the klammer set is worked on, and a test pinned to that state fails for a
# reason that has nothing to do with what it is testing. (It did: adopting
# ".*" in the SKS made "All targets, declared" non-empty.)
shows "12a. --coverage all shows an empty reporting category" \
'All targets, declared \(0\)' "$KDESC" -i "$DIR/cycle.k" --coverage all
absent "12b. ... hidden without it" \
'All targets, declared \(0\)' "$KDESC" -i "$DIR/cycle.k" --coverage
absent "12c. an empty problem category stays hidden" \
'Declared but never defined \(0\)' "$KDESC" -i "$DIR/cycle.k" --coverage all
absent "13. -v adds no result detail" 'Regex' "$KDESC" --katoms -v 3
echo
echo "-- -v is about processing only --"
shows "14. -v names the input it read" 'input_filenames' "$KDESC" -t -v 1
absent "15. ... and is silent without it" 'input_filenames' "$KDESC" -t
echo
echo "-- the klammer search --"
shows "16. a name matches" '@table' "$KDESC" -k table
absent "17. ... and others are excluded" '@document' "$KDESC" -k table
shows "18. matching is case-insensitive" '@table' "$KDESC" -k TABLE
shows "19. a description word matches" '@' "$KDESC" -k verbatim
# The words after -k are one phrase, and whitespace is collapsed on both
# sides -- a description written across several lines in a .k file must still
# match a phrase typed on one.
shows "20. several words are one phrase" '@' "$KDESC" -k displayed verbatim
shows "21. ... with runs of space collapsed" '@' "$KDESC" -k displayed verbatim
shows "22. no match says so" 'No klammer names or descriptions contained "zzqq"\.' "$KDESC" -k zzqq
absent "23. ... and lists nothing" '^ @' "$KDESC" -k zzqq
echo
echo "-- the subcommand words --"
shows "24. --coverage help explains" 'Coverage commands' "$KDESC" --coverage help
shows "25. --katoms help explains" 'Katom commands' "$KDESC" --katoms help
shows "26. an unknown coverage word" 'Unrecognized coverage command' "$KDESC" --coverage nonsense
shows "27. an unknown katom word" 'Unrecognized katom command' "$KDESC" --katoms nonsense
echo
echo "-- the usage text --"
shows "28. -k shows its optional argument" '\-k \[<text>\]' "$KDESC"
shows "29. -i shows its filename" '\-i <filename>' "$KDESC"
# Ordered by likely use: the single letters come before the long names.
# Line numbers, not a multi-line pattern -- grep is line-oriented.
usage=$(timeout 60 "$KDESC" 2>&1 | plain)
k_line=$(printf '%s\n' "$usage" | grep -n -- '-k \[<text>\]' | head -1 | cut -d: -f1)
katoms_line=$(printf '%s\n' "$usage" | grep -n -- '--katoms' | head -1 | cut -d: -f1)
if [ -n "$k_line" ] && [ -n "$katoms_line" ] && [ "$k_line" -lt "$katoms_line" ]; then
echo "${green}PASS${reset} 30. -k is listed before --katoms"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} 30. usage order: -k at line $k_line, --katoms at $katoms_line"
FAIL=$((FAIL+1))
fi
echo
echo "-- a no-result search is not an error --"
timeout 60 "$KDESC" -k zzqq >/dev/null 2>&1
if [ $? -eq 0 ]; then
echo "${green}PASS${reset} 31. finding nothing exits 0"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} 31. finding nothing exits nonzero"; FAIL=$((FAIL+1))
fi
echo
echo "====================="
echo "Results: ${PASS} passed, ${FAIL} failed"
[ "$FAIL" -eq 0 ] || exit 1
exit 0

View File

@@ -135,7 +135,7 @@ accepted " 8. a .k declaration with :: instances" \
rejected " 9. a .k declaration plus a parameterized definition" \
'@@k9.k s : a declaration @@
@@k9.ta s2 :other : [*s2*] @@
@k9 x @' 'both a declaration'
@k9 x @' 'both a ".k" declaration'
echo
echo "============================="

215
tst/target_list_test.sh Executable file
View File

@@ -0,0 +1,215 @@
#!/bin/bash
#
# target_list_test.sh — A klammer definition may name several targets.
#
# @@table.html,tex :: <body> @@
#
# One body, several targets. The list is SURFACE SYNTAX: the registry makes
# one definition per target named, so nothing downstream of registration knows
# a list was written. That is what these tests pin down — in particular that
# each member goes through the redefinition transition table on its own, so a
# list overlapping an existing definition is decided per target rather than
# all-or-nothing.
#
# Why the feature exists: a klammer whose body is an `@eval` cannot have its
# coverage derived (deciding which targets a Python function answers for is
# undecidable), so its targets must be DECLARED. Writing one definition per
# target would then duplicate the body. See notes/target_coverage.md.
#
# Two patterns are involved and must not be confused (mac/ktype.h):
# definition_begin_name — runs through commas; used ONLY for "@@<name>"
# definition_name — no comma; klammer applications, option names,
# "*arg*" variables, and the closing delimiters
# Case 14 guards the second: a comma next to an application is writer text.
#
# These are engine tests, so they use -k none and define their own targets
# inline: a target is a Machine construct (@@@target), not owned by any
# klammer set.
#
# Usage: ./target_list_test.sh (needs KLAMMERTEXT_HOME set; ktext on PATH)
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
KTEXT=ktext
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
ERR=/tmp/target_list_test_err.$$
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
# ta, tb, tc are independent; td INCLUDES tc, so tc "provides" td and a
# definition for tc is copied to td (mac/klammer_registry.cpp).
TARGETS='@@@target ta | Target A @@@
@@@target tb | Target B @@@
@@@target tc | Target C @@@
@@@target td | Target D :includes tc @@@
'
trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR && line[f]=="") f++; l=NR; while (l>=1 && line[l]=="") l--; for (i=f;i<=l;i++) print line[i] }'; }
# check_eq NAME TARGET EXPECTED SRC — exit 0, stdout==EXPECTED, no warning.
check_eq() {
local name="$1" target="$2" expected="$3" src="$4"
local out status err
out=$("$KTEXT" -k none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$?
err=$(cat "$ERR")
out=$(printf '%s' "$out" | trim)
if [ $status -ne 0 ]; then
echo "${red}FAIL${reset} $name — ktext exited $status"
echo " stderr: $(echo "$err" | head -2)"; FAIL=$((FAIL+1)); return
fi
if printf '%s' "$err" | grep -qiF "warning"; then
echo "${red}FAIL${reset} $name — unexpected warning"
echo " stderr: $(echo "$err" | head -2)"; FAIL=$((FAIL+1)); return
fi
if [ "$out" = "$expected" ]; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name"
echo " expected: [$expected]"; echo " got: [$out]"; FAIL=$((FAIL+1))
fi
}
# check_warn NAME TARGET EXPECTED SRC — exit 0, stdout==EXPECTED, AND a warning.
check_warn() {
local name="$1" target="$2" expected="$3" src="$4"
local out status err
out=$("$KTEXT" -k none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$?
err=$(cat "$ERR")
out=$(printf '%s' "$out" | trim)
if [ $status -ne 0 ]; then
echo "${red}FAIL${reset} $name — ktext exited $status"; FAIL=$((FAIL+1)); return
fi
if ! printf '%s' "$err" | grep -qiF "warning"; then
echo "${red}FAIL${reset} $name — expected a warning, got none"; FAIL=$((FAIL+1)); return
fi
if [ "$out" = "$expected" ]; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name"
echo " expected: [$expected]"; echo " got: [$out]"; FAIL=$((FAIL+1))
fi
}
# check_error NAME TARGET PATTERN SRC — nonzero exit and PATTERN in the message.
check_error() {
local name="$1" target="$2" pattern="$3" src="$4"
local out status
out=$("$KTEXT" -k none -s "$TARGETS$src" -t "$target" -d 2>&1); status=$?
if [ $status -eq 0 ]; then
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"; FAIL=$((FAIL+1)); return
fi
if echo "$out" | grep -qF "$pattern"; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name — expected error to contain [$pattern]"
echo " output: $(echo "$out" | head -3)"; FAIL=$((FAIL+1))
fi
}
echo "${bold}Klammer definition target-list tests${reset}"
echo "===================================="
echo
# --- One body, several targets ---
echo "-- the list defines each target named --"
check_eq " 1. first member gets the body" ta "x B y" \
'@@f.k : a test @@ @@f.ta,tb :: B @@ x @f@ y'
check_eq " 2. second member gets the body" tb "x B y" \
'@@f.k : a test @@ @@f.ta,tb :: B @@ x @f@ y'
check_error " 3. a target NOT in the list is undefined" tc "not defined for target" \
'@@f.k : a test @@ @@f.ta,tb :: B @@ x @f@ y'
check_eq " 4. three members, last one" tc "x B y" \
'@@f.k : a test @@ @@f.ta,tb,tc :: B @@ x @f@ y'
check_eq " 5. parameters are inherited from .k for every member" tb "x [q] y" \
'@@f.k s : a test @@ @@f.ta,tb :: [*s*] @@ x @f q @ y'
check_eq " 6. a list member propagates to a target that includes it" td "x B y" \
'@@f.k : a test @@ @@f.ta,tc :: B @@ x @f@ y'
# --- Malformed lists ---
echo
echo "-- malformed lists --"
check_error " 7. an unknown target in the list" ta "is not defined" \
'@@f.ta,nosuch : B @@'
check_error " 8. a target named twice" ta "more than once" \
'@@f.ta,ta : B @@'
check_error " 9. \".k\" in a list" ta "in a list of targets" \
'@@f.k,ta : a test @@'
check_error "10. \".o\" in a list" ta "in a list of targets" \
'@@f.o,ta : an option set @@'
# "*" is the general target written out: an assertion that the klammer works
# for EVERY target, including ones not yet defined. In a list it is either
# redundant or a misunderstanding.
check_error "10a. \"*\" in a list" ta 'names "*" in a list of targets' \
'@@f.*,ta : b @@'
check_eq "10b. \"*\" alone defines every target" tb "x body" \
'@@f.* : body @@ x @f@'
check_eq "10c. ... including one it does not name" ta "x body" \
'@@f.* : body @@ x @f@'
check_error "11. a trailing comma" ta "is not correctly defined" \
'@@f.ta, : B @@'
check_error "12. an empty member" ta "is not correctly defined" \
'@@f.ta,,tb : B @@'
# --- The transition table applies per member, not to the list ---
echo
echo "-- redefinition, decided per member --"
check_error "13. create + list create collides on the shared member" ta "already defined" \
'@@f.ta : one @@ @@f.ta,tb : two @@ x @f@'
# The list's default is silently ignored for ta (which already has a create)
# and creates tb. This is the case a whole-list decision would get wrong.
check_eq "14. existing create + list default: existing kept" ta "x one" \
'@@f.ta : one @@ @@f.ta,tb :::: two @@ x @f@'
check_eq "15. existing create + list default: other member defined" tb "x two" \
'@@f.ta : one @@ @@f.ta,tb :::: two @@ x @f@'
check_eq "16. list default + create for one member: replaced" ta "x real" \
'@@f.ta,tb :::: def @@ @@f.ta : real @@ x @f@'
check_eq "17. list default + create for one member: other keeps default" tb "x def" \
'@@f.ta,tb :::: def @@ @@f.ta : real @@ x @f@'
check_warn "18. list override warns and replaces (first member)" ta "x over" \
'@@f.k : a test @@ @@f.ta :: one @@ @@f.tb :: two @@ @@f.ta,tb ::: over @@ x @f@'
check_warn "19. list override warns and replaces (second member)" tb "x over" \
'@@f.k : a test @@ @@f.ta :: one @@ @@f.tb :: two @@ @@f.ta,tb ::: over @@ x @f@'
# --- The comma is a target separator ONLY after "@@" ---
echo
echo "-- a comma elsewhere is writer text --"
check_eq "20. a comma in a body is text" ta "x a,b" \
'@@f.ta : a,b @@ x @f@'
check_eq "21. a comma after an argument substitution" ta "x q,tail" \
'@@f.ta s : *s*,tail @@ x @f q @'
check_eq "22. a comma after an application" ta "x q, y" \
'@@f.ta s : *s* @@ x @f q @, y'
check_eq "23. a general definition is unaffected" ta "x B" \
'@@f : B @@ x @f@'
# --- Diagnostics count what was WRITTEN, not how often it registered ---
echo
echo "-- one definition, several registrations --"
# tc provides td, so "@@f.tc : ..." registers twice from one line. Before
# this was grouped, the message said "2 definitions" and then printed the one
# line the author wrote twice, sending them to look for a second.
check_error "24. the count is of written definitions" ta \
"and a definition that declares its own parameters" \
'@@f.k s : d @@ @@f.tc : [*s*] @@'
check_error "25. ... and the location names its targets" ta "(targets tc, td)" \
'@@f.k s : d @@ @@f.tc : [*s*] @@'
# Two definitions in two places are still two.
check_error "26. distinct places still count separately" ta \
"2 definitions that declare their own parameters" \
'@@f.k s : d @@ @@f.ta : a*s* @@ @@f.tb : b*s* @@'
# The remedy, named in the message rather than implied.
check_error "27. the message says what to write instead" ta \
'Write "::" instead of ":"' \
'@@f.k s : d @@ @@f.tc : [*s*] @@'
rm -f "$ERR"
echo
echo "===================================="
echo "Results: ${PASS} passed, ${FAIL} failed"
[ "$FAIL" -eq 0 ] || exit 1
exit 0