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:
@@ -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
|
||||
|
||||
|
||||
158
com/kdesc.cpp
158
com/kdesc.cpp
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
60
mac/argv.cpp
60
mac/argv.cpp
@@ -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";
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -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
481
mac/coverage.cpp
Normal 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
110
mac/coverage.h
Normal 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);
|
||||
120
mac/klammer.cpp
120
mac/klammer.cpp
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {};
|
||||
};
|
||||
|
||||
12
mac/ktype.h
12
mac/ktype.h
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
30
mac/util.cpp
30
mac/util.cpp
@@ -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();
|
||||
|
||||
13
mac/util.h
13
mac/util.h
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}}
|
||||
|
||||
@@ -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) @ @@
|
||||
|
||||
@@ -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@ @@
|
||||
|
||||
@@ -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@ @@
|
||||
|
||||
@@ -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 @ @@
|
||||
|
||||
@@ -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 :: © @@
|
||||
|
||||
@@ -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 @ @@
|
||||
|
||||
@@ -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}]"
|
||||
|
||||
@@ -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 @ @@
|
||||
|
||||
@@ -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@ @@
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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@ @@
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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@ @@
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@@
|
||||
|
||||
@@ -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 @ @@
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
120
tst/command_option_test.sh
Executable 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
29
tst/coverage/basic.k
Normal 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
8
tst/coverage/clean.k
Normal 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
22
tst/coverage/cycle.k
Normal 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
6
tst/coverage/split.k
Normal 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 @
|
||||
5
tst/coverage/split_more.k
Normal file
5
tst/coverage/split_more.k
Normal 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* @@
|
||||
10
tst/coverage/undecidable.k
Normal file
10
tst/coverage/undecidable.k
Normal 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
258
tst/coverage_test.sh
Executable 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
158
tst/kdesc_test.sh
Executable 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
|
||||
@@ -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
215
tst/target_list_test.sh
Executable 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
|
||||
Reference in New Issue
Block a user