Verbatim-safe typography, ^-punctuation quoting, full-range ^UUUU^, polyglot html

Typographic transforms (---, quote pairs, ~) no longer touch verbatim
text: @c/@code/@source_listing content and ^'...'^ spans show exactly the
characters written. "^" before any punctuation character quotes it in
every target (the apostrophe excepted: ^' opens a literal span), with the
new :resolve option on @@@target declaring per-target renderings. The
^UUUU^ code-point form accepts 4-6 hex digits, the full Unicode range.
The html output and transform spellings are polyglot (XML-valid), in
preparation for an EPUB target. New suites: transform_test, character_test
(engine), typography_test (SKS).

(from dev 07ce5ea86a0a)
This commit is contained in:
2026-08-23 20:48:26 +02:00
parent d982c0d6cc
commit 37b6ba1c4f
77 changed files with 1665 additions and 1608 deletions

View File

@@ -48,7 +48,7 @@ std::string Argtype_registry::replace_symbols(const std::string& pattern, const
ss << "Argtype symbol " << symbol << " not defined.\n\n"
<< "Defined argtypes:\n";
ss << describe();
throw Definition_error(ss.str(), loc, false);
throw Definition_error(ss.str(), loc);
}
}
return expanded;
@@ -88,7 +88,7 @@ void Argtype_registry::add(const std::string& name, const std::string& desc,
if (pattern != expanded_pattern) {
ss << "expanded to:\n " << expanded_pattern << "\n";
}
throw Definition_error(ss.str(), loc, false);
throw Definition_error(ss.str(), loc);
}
if (!default_value.empty() &&
!std::regex_match(default_value, m_types[name].m_regex)) {
@@ -96,7 +96,7 @@ void Argtype_registry::add(const std::string& name, const std::string& desc,
ss << "The default value \"" << default_value << "\" for argument type \""
<< name << "\" does not match its own pattern:\n"
<< " " << pattern << "\n";
throw Definition_error(ss.str(), loc, false);
throw Definition_error(ss.str(), loc);
}
if (!alone_value.empty()) {
// A pattern that matches running text cannot delimit a bare option
@@ -110,21 +110,21 @@ void Argtype_registry::add(const std::string& name, const std::string& desc,
// which is equally unable to delimit.
if (std::regex_match(std::string("one two"), m_types[name].m_regex)) {
std::stringstream ss {};
ss << "The argument type \"" << name << "\" cannot declare an :alone value "
<< "because its pattern matches running text:\n"
ss << "The argument type \"" << name << "\" cannot declare an :alone value"
<< "because its pattern matches running text:"
<< " " << pattern << "\n\n"
<< "An :alone value is used when an option name is written without a "
<< "value. A type that matches running text cannot tell a bare option "
<< "name from one whose value follows it, so the text after the name "
<< "would be taken as the value instead.\n";
throw Definition_error(ss.str(), loc, false);
<< "An :alone value is used when an option name is written without a"
<< "value. A type that matches running text cannot tell a bare option"
<< "name from one whose value follows it, so the text after the name"
<< "would be taken as the value instead.";
throw Definition_error(ss.str(), loc);
}
if (!std::regex_match(alone_value, m_types[name].m_regex)) {
std::stringstream ss {};
ss << "The alone value \"" << alone_value << "\" for argument type \""
<< name << "\" does not match its own pattern:\n"
<< " " << pattern << "\n";
throw Definition_error(ss.str(), loc, false);
throw Definition_error(ss.str(), loc);
}
}
m_names.push_back(name);

View File

@@ -86,10 +86,10 @@ Parameter parse_positional_parameter(const katom_list& katoms, const Argtype_reg
{
// (void)K::log(3, katoms);
if (katoms.size() > 1) {
throw Argument_error("Multiple katoms for positional argument: " +
throw Argument_error("Multiple katoms for positional argument:\n " +
as_string(katoms.begin(), katoms.end(), true) +
"\nPositional arguments are separated by the bar (|) character.",
katoms[0].m_loc, false);
katoms[0].m_loc);
}
Katom k = katoms[0];
std::string name = k.m_text;
@@ -367,7 +367,7 @@ void Parameter_set::check_positional(const katom_lists& positional_arguments, co
ss << " " << arg.m_name << "\n";
}
//std::cout << ss.str();
throw Argument_error(ss.str(), loc, false);
throw Argument_error(ss.str(), loc);
} else if (positional_count < given_count) {
//std::cout << "DESCRIBE\n";
//describe_parameters();
@@ -391,7 +391,7 @@ Parameter_set::check_optional(const katom_lists& optional_arguments, const Locat
}
if (std::ranges::count(optional_names_used, name) > 0) {
throw Argument_error("Optional argument \":" + name + "\" already provided "
+ "with a value of:\n" + values[name], loc, false);
+ "with a value of:\n" + values[name], loc);
}
katom_list value_katoms(opt.begin()+1, opt.end());
std::string value = trim(to_string(value_katoms));
@@ -477,9 +477,12 @@ void Parameter_set::validate(
std::stringstream ss {};
ss << "The value \"" << value << "\" given for the argument \""
<< parameter.m_name << "\" does not match the \"" << argtype.m_name
<< "\" argument type:\n\n"
<< trim(argtype.m_desc) << "\n";
throw Argument_error(ss.str(), loc, false);
<< "\" argument type.\n\nThe \"" << argtype.m_name << "\" argument type describes "
// A .k description is prose: collapse its source layout (line
// breaks, continuation indentation) so it re-flows with the
// sentence -- indented .k lines would otherwise read as verbatim.
<< collapse_whitespace(argtype.m_desc) << "\n";
throw Argument_error(ss.str(), loc);
}
}
@@ -520,7 +523,7 @@ std::string replace_arguments(
}
ss << "To prevent the \"*\" character from specifying an argument, "
<< "precede it with the \"^\" character.";
throw Argument_error(ss.str(), loc, false);
throw Argument_error(ss.str(), loc);
}
return result;
}

View File

@@ -257,7 +257,7 @@ void Argv::check_flags_and_options(const std::string& command, strings_t& words)
for (auto w : not_defined) {
ss << " " << w << "\n";
}
throw Argument_error(ss.str(), Locator(), false);
throw Argument_error(ss.str(), Locator::none());
}
}
@@ -288,7 +288,7 @@ void Argv::parse_flags(strings_t& words, string_map& named_args)
for (auto w : not_defined) {
ss << " " << w << "\n";
}
throw Argument_error(ss.str(), Locator(), false);
throw Argument_error(ss.str(), Locator::none());
}
*/
// std::cout << "Flags found: " << flag_args << "\n";
@@ -320,16 +320,29 @@ void Argv::parse_optional(strings_t& words, string_map& named_args)
"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);
Locator::none());
}
std::string opt_arg = words[index] + " ";
index++;
std::regex opt_regex = m_args[opt].m_rgx;
// std::cout << "Regex match? " << std::regex_match(trim(opt_arg), opt_regex) << "\n";
while (index < words.size() && words[index][0] != '-'
// && std::regex_match(trim(opt_arg), opt_regex)
// Only a LIST-valued option may span several argv words
// (--klammersets a b c; --katoms type index). A single-valued
// option takes exactly ONE argv element -- argv boundaries are
// authoritative -- so a positional written after it is not
// swallowed into the value. The former unconditional run
// consumed everything up to the next dash-word: "-s 'text'
// doc.kt" folded doc.kt into the -s TEXT (and rendered its
// pathname), and "--klammersets none doc.kt" consumed the input
// filename, so ktext reported that none was given. -s must be
// usable in any argv position: its role with a file argument is
// to modify the interpretation of that file, and it is
// evaluated first regardless of where it is written. A list
// value stops at the "--" delimiter (it begins with '-'), which
// remains the escape for filenames after a list option.
bool list_valued = m_args[opt].m_rgx_symbol == "'list'"
|| m_args[opt].m_rgx_symbol == "'katom_display'";
while (list_valued && index < words.size() && words[index][0] != '-'
&& std::regex_match(trim(opt_arg + words[index]), opt_regex)
) {
@@ -370,7 +383,7 @@ void Argv::parse_positional(const std::string& command, //strings_t words,
}
std::stringstream ss {};
ss << "The argument " << q_(req) << " was not found in:\n " << command;
throw Argument_error(ss.str(), Locator(), false);
throw Argument_error(ss.str(), Locator::none());
}
named_args[req] = substring;
// Drop the whitespace that separated this positional from the next.
@@ -467,7 +480,7 @@ void Argv::check_flags(const string_map& arg_map, const std::string& command)
for (const std::string& undef : undefined) {
ss << " " << flag_name(undef);
}
throw Argument_error(ss.str(), Locator());
throw Argument_error(ss.str(), Locator::none());
}
}
@@ -508,7 +521,7 @@ void Argv::parse(int argc, char* argv[], bool full_parse)
usage(file_basename(argv[0]));
throw Argument_error(
"Incorrect value for option \"" + name + "\":\n" + m_args[name].m_desc + "\n",
Locator(), false);
Locator());
}
}
}
@@ -594,7 +607,8 @@ int Argv::as_verbosity(const std::string& name)
(void)K::log(2, name);
auto value = get(name);
if (!std::regex_match(value, std::regex(regex_symbols["'verbosity'"]))) {
throw Argument_error("Argument \"" + value + "\" is not a verbosity level");
throw Argument_error("Argument \"" + value + "\" is not a verbosity level",
Locator::none());
}
return std::stoi(value);
}

View File

@@ -36,6 +36,9 @@ std::string utf8char(int cp)
c[1] = ((cp>>12)&63)+128;
c[2] = ((cp>>6)&63)+128;
c[3]=(cp&63)+128;
} else {
// Reachable since ^UUUU^ accepts 6 hex digits: FFFFFF > 10FFFF.
return "Invalid Unicode: " + std::to_string(cp);
}
return std::string(c);
}
@@ -65,7 +68,13 @@ std::string unicode_hex_to_char(std::string s, int width=4) //, std::string mark
std::string process_diacritics(std::string s)
{
(void)K::log(4);
std::regex diacritic_re("\\^([^\\s`'~@|^:*#])([" + diacritic_symbols + "])");
// The base may not be whitespace, a digit, or ASCII punctuation (the
// four ranges !-/ :-@ [-` {-~): a diacritic sits on a letter. Without
// the exclusion, ^ before a quoted punctuation character followed by a
// mark character composed nonsense -- ^-- became a hyphen with a macron
// instead of a literal hyphen before a hyphen. A multi-byte (non-ASCII)
// base is unaffected: its bytes are outside every excluded range.
std::regex diacritic_re("\\^([^\\s0-9!-/:-@\\[-`{-~])([" + diacritic_symbols + "])");
std::string result {s};
std::sregex_iterator end {};
@@ -126,9 +135,14 @@ std::string process_unicode_codepoint(std::string s)
//return std::regex_replace(s, unicode_re, hidehat + "$1" + hidehat);
std::string result {s};
std::sregex_iterator end {};
for (std::sregex_iterator p { s.begin(), s.end(), unicode_re }; p!= end; ++p) {
for (std::sregex_iterator p { s.begin(), s.end(), unicode_re }; p!= end; ++p) {
std::regex hit_re { regex_escape((*p)[0].str()) };
result = std::regex_replace(result, hit_re, unicode_hex_to_char((*p)[1].str()));
// Convert the captured hex DIRECTLY: unicode_hex_to_char() re-scans
// its argument at a fixed width, and its 4-digit default truncated a
// 5-digit code point to its first four digits (^13000^ rendered as
// U+1300 followed by a literal "0").
result = std::regex_replace(result, hit_re,
utf8char(std::stoi((*p)[1].str(), nullptr, 16)));
}
return result;

View File

@@ -11,7 +11,11 @@
#include <regex>
#include <unistd.h>
const std::regex unicode_re(R"(\^(([0-9A-Fa-f]{5})|([0-9A-Fa-f]{4})|([0-9A-Fa-f]))\^)");
// ^UUUU^ — a Unicode code point in hex, 4 to 6 digits (^263A^ is the BMP,
// ^13000^ EGYPTIAN HIEROGLYPH A001, ^10FFFD^ the top of the range), or a
// single digit. Lengths 2-3 are NOT accepted: two- and three-letter
// sequences of a-f collide with the ^s^-style mnemonic names.
const std::regex unicode_re(R"(\^(([0-9A-Fa-f]{4,6})|([0-9A-Fa-f]))\^)");
const std::regex unicode_hide_re(R"(=([0-9A-Fa-f]{2})=)");
const std::regex hex2_re(R"(([0-9A-Fa-f]{2}))");
const std::regex hex4_re(R"(([0-9A-Fa-f]{4}))");

View File

@@ -9,7 +9,16 @@ using namespace std::string_literals;
fs::path construct_command_pathname(char* command)
{
return fs::path(fs::current_path().string() + "/" + std::string(command));
// The pseudo source file for command-line string input (-s, kdiag's
// input): a file IN THE CWD named after the command, so that a relative
// @read in string input resolves against the directory the user ran the
// command from -- the string-input analog of "a document's relative
// names resolve against the document". The basename matters: the
// former "<cwd>/<argv[0]>" string concatenation anchored resolution at
// the BINARY's directory whenever argv[0] was absolute
// (/usr/local/bin/ktext, a wrapper script), so every relative @read in
// -s input searched bin/ instead of the cwd.
return fs::current_path() / fs::path(command).filename();
}
void set_verbose_level(int argc, char* argv[])
@@ -124,7 +133,7 @@ void load_klammersets(Machine& machine, const strings_t& symbols)
"The klammerset \"none\" cannot be combined with other klammersets: "
"it means that none is loaded. Give \"none\" alone, or name only the "
"klammersets to load.",
Locator());
Locator::none());
}
if (symbols.empty()) {
// Which klammerset was loaded, and from where, is a DERIVED value: the
@@ -146,7 +155,7 @@ void load_klammersets(Machine& machine, const strings_t& symbols)
continue;
}
std::string klammerset_filename = resolve_klammerset_symbol(
symbol, machine.m_state.value("K_input_dir"), Locator()).string();
symbol, machine.m_state.value("K_input_dir"), Locator::none()).string();
// Symbol -> file is the search path's answer, and the search
// path has three stages with shadowing: the file it landed on is
// exactly what a user cannot read off "--klammersets x".

View File

@@ -23,8 +23,7 @@ void Error::print_message(const std::string& epilog)
if (epilog != "") {
m_desc += "\n\n" + epilog + "\n";
}
if (m_just)
m_desc = justify(m_desc, 80, 0);
m_desc = justify(m_desc, 80, 0);
std::cerr << "\n" << red << command_name << " (" << m_type << " error)";
if (display_source(m_loc.m_filename)) {
std::cerr << ":\n " << m_loc.m_filename;

View File

@@ -11,11 +11,10 @@ inline std::string command_pathname { "Pathname of command executed on the comma
class Error : std::exception {
public:
Error(const std::string& error_type, const std::string& description,
const Locator& locator = Locator(), bool do_justify = true)
const Locator& locator = Locator())
: m_type(error_type)
, m_desc(description)
, m_loc(locator)
, m_just(do_justify)
{}
void print_message(const std::string& epilog="");
@@ -23,57 +22,56 @@ public:
std::string m_type {};
std::string m_desc {};
Locator m_loc; // {};
bool m_just { true };
};
class Parsing_error : public Error {
public:
explicit Parsing_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("parsing", description, locator, do_justify) {};
const std::string& description, const Locator& locator=Locator())
: Error("parsing", description, locator) {};
};
class File_error : public Error {
public:
explicit File_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("file", description, locator, do_justify) {};
const std::string& description, const Locator& locator=Locator())
: Error("file", description, locator) {};
};
class Target_error : public Error {
public:
explicit Target_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("target", description, locator, do_justify) {};
const std::string& description, const Locator& locator=Locator())
: Error("target", description, locator) {};
};
class Klammerset_error : public Error {
public:
explicit Klammerset_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("klammerset", description, locator, do_justify) {};
const std::string& description, const Locator& locator=Locator())
: Error("klammerset", description, locator) {};
};
class Definition_error : public Error {
public:
explicit Definition_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("definition", description, locator, do_justify) {};
const std::string& description, const Locator& locator=Locator())
: Error("definition", description, locator) {};
};
class Argument_error : public Error {
public:
explicit Argument_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("argument", description, locator, do_justify) {};
const std::string& description, const Locator& locator=Locator())
: Error("argument", description, locator) {};
};
class Environment_error : public Error {
public:
explicit Environment_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("environment", description, locator, do_justify) {};
const std::string& description, const Locator& locator=Locator())
: Error("environment", description, locator) {};
};
// Klammer application nested deeper than the engine's limit. Raised by the
@@ -83,13 +81,13 @@ public:
class Recursion_error : public Error {
public:
explicit Recursion_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("recursion", description, locator, do_justify) {};
const std::string& description, const Locator& locator=Locator())
: Error("recursion", description, locator) {};
};
class Internal_error : public Error {
public:
explicit Internal_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("internal", description, locator, do_justify) {};
const std::string& description, const Locator& locator=Locator())
: Error("internal", description, locator) {};
};

View File

@@ -42,8 +42,8 @@ std::string shell(State state, std::string command, Locator loc)
err_template.push_back('\0');
int err_fd = mkstemp(err_template.data());
if (err_fd == -1) {
throw Environment_error("Could not create a temporary file for the "
"command's error output", loc, false);
throw Environment_error(
"Could not create a temporary file for the command's error output", loc);
}
close(err_fd);
err_path = err_template.data();
@@ -55,7 +55,7 @@ std::string shell(State state, std::string command, Locator loc)
if (!pipe) {
fs::remove(err_path);
throw Environment_error(
"Could not run command:\n" + command, loc, false);
"Could not run command:\n " + command, loc);
}
char buffer[128];
std::string result = "";
@@ -87,7 +87,7 @@ std::string shell(State state, std::string command, Locator loc)
// "environment", not "parsing": the failure is OUTSIDE Klammertext,
// in the command the document invoked -- the same class as a missing
// xelatex or an unset environment variable.
throw Environment_error(ss.str(), loc, false);
throw Environment_error(ss.str(), loc);
}
if (!error_output.empty()) {
(void)K::log(1, "shell command wrote to stderr:", command,
@@ -122,8 +122,8 @@ std::string run_haskell(const std::string& hsfile, Locator loc)
err_template.push_back('\0');
int err_fd = mkstemp(err_template.data());
if (err_fd == -1) {
throw Environment_error("Could not create a temporary file for runghc's "
"error output", loc, false);
throw Environment_error(
"Could not create a temporary file for runghc's error output", loc);
}
close(err_fd);
err_path = err_template.data();
@@ -132,7 +132,7 @@ std::string run_haskell(const std::string& hsfile, Locator loc)
FILE* pipe = popen(command.c_str(), "r");
if (!pipe) {
fs::remove(err_path);
throw Environment_error("Could not run runghc.", loc, false);
throw Environment_error("Could not run runghc.", loc);
}
char buffer[128];
std::string result = "";
@@ -154,12 +154,12 @@ std::string run_haskell(const std::string& hsfile, Locator loc)
}
ss << ".";
if (!error_output.empty()) {
ss << "\nrunghc reported:\n" << error_output;
ss << "\n\nrunghc reported:\n " << error_output;
}
// A compile error arrives here, and so does a program that ran and then
// exited nonzero; the message does not guess which, it shows what
// runghc said.
throw Environment_error(ss.str(), loc, false);
throw Environment_error(ss.str(), loc);
}
if (!error_output.empty()) {
// GHC's warnings, and anything the program itself wrote to stderr.
@@ -175,9 +175,9 @@ std::string haskell(State state, std::string code, Locator loc)
// "environment", not "parsing": a missing external command is the same
// class as a missing xelatex, and "parsing error" misdescribes it.
throw Environment_error(
"@eval with the :haskell argument requires runghc, which was not found in PATH.\n"
"@eval with the :haskell argument requires runghc, which was not found in PATH.\n\n"
"Install it using GHCup; see https://www.haskell.org/ghcup/install/.",
loc, false);
loc);
}
code = state.subst(code);
@@ -193,7 +193,7 @@ std::string haskell(State state, std::string code, Locator loc)
int fd = mkstemp(tmppath.data());
if (fd < 0) {
throw Environment_error(
"Could not create temporary file for Haskell evaluation.", loc, false);
"Could not create temporary file for Haskell evaluation.", loc);
}
std::string hsfile = std::string(tmppath.data()) + ".hs";
close(fd);
@@ -202,8 +202,7 @@ std::string haskell(State state, std::string code, Locator loc)
FILE* f = fopen(hsfile.c_str(), "w");
if (!f) {
unlink(hsfile.c_str());
throw Parsing_error(
"Could not write temporary Haskell file.", loc, false);
throw Parsing_error("Could not write temporary Haskell file.", loc);
}
fprintf(f, "%s\n", code.c_str());
fclose(f);
@@ -222,7 +221,7 @@ void check_cpp_arguments(katom_list args, Locator loc)
<< "or\n"
<< " @eval :cpp <library-basename> <function-name> @\n"
<< "In the first case, the library basename is used for the function name.";
throw Argument_error(ss.str(), loc, false);
throw Argument_error(ss.str(), loc);
}
}
@@ -279,7 +278,7 @@ std::string Eval::eval_command(katom_iter begin, katom_iter end)
if (dir.empty() || !fs::is_directory(dir)) {
throw Argument_error(
"The :cwd directory does not exist: \"" + dir + "\"",
begin->m_loc, false);
begin->m_loc);
}
cwd_guard.emplace(dir);
first = resume;

View File

@@ -34,7 +34,7 @@ std::string Eval_cpp::eval(const fs::path& library_path, const std::string& func
std::stringstream ss {};
ss << "Cannot load symbol " << function_name
<< " from library " << library_path << ":\n " << error_desc;
throw File_error(ss.str(), m_loc, false);
throw File_error(ss.str(), m_loc);
}
std::string result = func(m_machine);
dlclose(handle);

View File

@@ -40,6 +40,21 @@ Eval_python::Eval_python(Machine& machine, const Locator& loc)
Py_DECREF(result_text);
}
import_module("inspect", false);
// The :after_apply phase dispatcher: a phase that declares a K
// parameter receives the state's class K; one that does not (a stdlib
// function like string.capwords, whose second parameter is a separator)
// is called with the text alone. Discrimination is by parameter NAME,
// not count -- capwords has two parameters and K is not one of them.
// Builtins whose signature inspect cannot read are treated as not
// wanting K.
PyRun_String(
"def K_phase_call(f, text, K):\n"
" try:\n"
" wants = 'K' in inspect.signature(f).parameters\n"
" except (ValueError, TypeError):\n"
" wants = False\n"
" return f(text, K=K) if wants else f(text)\n",
Py_file_input, m_globals, m_globals);
if (!m_machine.m_state.m_frames.empty()) {
PyRun_String(m_machine.m_state.python_code().c_str(), Py_file_input, m_globals, m_locals);
}
@@ -154,7 +169,7 @@ void Eval_python::import_module(const std::string& module_name, bool verify)
if (!detail.empty()) {
message += ":\n\n" + detail;
}
throw Argument_error(message, m_loc, false);
throw Argument_error(message, m_loc);
}
// PyDict_SetItemString steals a reference, so we don't need to DECREF module
// The dictionary will own the reference

View File

@@ -160,7 +160,6 @@ strings_t group_filename_tokens(const strings_t& tokens, const std::string& base
// name exists. A name that never resolves is kept as given, so the
// missing-file error downstream reports what the user wrote.
size_t i = 0;
bool reported = false; // at most one "tried" report per list
while (i < tokens.size()) {
if (filename_exists(tokens[i], base_dir)) {
result.push_back(tokens[i]);
@@ -188,27 +187,14 @@ strings_t group_filename_tokens(const strings_t& tokens, const std::string& base
}
}
if (!found) {
// Nothing resolved. The groupings that were TRIED are what
// the user needs at verbosity 0, because the downstream error
// names only the first token and cannot say why the others
// were not joined to it. Recorded on the name so the caller
// can report them with the missing-file error.
strings_t tried {};
std::string acc2 = tokens[i];
tried.push_back(q_(acc2));
for (size_t k = i + 1; k < tokens.size(); ++k) {
acc2 += " " + tokens[k];
tried.push_back(q_(acc2));
}
// Once: the first unresolved token's list already shows
// every joining from that point on, and one report per
// leftover word buries it.
if (tried.size() > 1 && !reported) {
std::cerr << command_name << ": no file matches "
<< q_(tokens[i]) << "; tried "
<< join(tried, ", ") << "\n";
reported = true;
}
// Nothing resolved: the token passes through as written and
// its DOWNSTREAM OWNER decides -- a bare :files word is the
// kt/ shortcut (which resolves or errors there), a missing
// file is a located error. A failed rescue itself says
// NOTHING at any verbosity (2026-08-22): every outcome is
// already either the -v 1 resolution line or an error that
// stops the run, so a tried-list adds noise to the first and
// nothing to the second.
result.push_back(tokens[i]);
++i;
}
@@ -358,7 +344,7 @@ std::string find_file(const std::string& basename, strings_t search_path, bool e
std::sort(search_path.begin(), search_path.end());
ss << "File with basename \"" << basename << "\" not found in search path:\n "
<< join(search_path, "\n ");
throw File_error(ss.str(), Locator(), false);
throw File_error(ss.str(), Locator());
}
return pathname;
}

View File

@@ -131,7 +131,7 @@ void warn_unparsed_katoms(katom_list& katoms, bool warn)
// commands issue has one format: it said "[warning]" where
// everything else says "(warning)".
warning("Word not parsed:\n " + k.m_text
+ "\nTo include a special character (@, |, #, and ^), put "
+ "\nTo include a punctuation character literally, put "
"\"^\" before it.",
k.m_loc);
k.m_unparsed = false;

View File

@@ -114,7 +114,7 @@ void missing_open(const Katom& k, bool error_exit)
std::stringstream ss {};
ss << "A klammer ends without a beginning: " << k;
if (error_exit) {
throw Parsing_error(ss.str(), k.m_loc, false);
throw Parsing_error(ss.str(), k.m_loc);
} else {
std::cout << " " << ss.str() << "\n";
}
@@ -134,7 +134,7 @@ void missing_close(const katom_list& bounds, bool error_exit)
ss << " " << k.m_loc << " " << k.m_src << "\n";
}
if (error_exit) {
throw Parsing_error(ss.str(), bounds[0].m_loc, false);
throw Parsing_error(ss.str(), bounds[0].m_loc);
} else {
std::cout << ss.str() << "\n";
}
@@ -148,7 +148,7 @@ void bad_close(const Katom& open, const Katom& close, bool error_exit)
<< " " << open.m_loc << " " << open << "\n"
<< " " << close.m_loc << " " << close;
if (error_exit) {
throw Parsing_error(ss.str(), open.m_loc, false);
throw Parsing_error(ss.str(), open.m_loc);
} else {
std::cout << ss.str() << "\n";
}
@@ -174,7 +174,7 @@ void check_named_katom_span(const Katom& begin, const Katom& end)
ss << " A named end katom does not match:\n"
<< " " << begin.m_loc << " " << begin << "\n"
<< " " << end.m_loc << " " << end;
throw Parsing_error(ss.str(), end.m_loc, false);
throw Parsing_error(ss.str(), end.m_loc);
}
}
}
@@ -370,6 +370,24 @@ void hide_special_katoms(katom_list& katoms)
definition_depth == 0 && code_depth == 0) {
k.m_text = hide_structural_characters(k.m_text);
}
// "^" before ANY punctuation character quotes it, not only the six
// Klammertext specials the katomizer knows: ^- is a literal hyphen
// (no dash transform), ^~ a literal tilde. The pair becomes the
// KTESC marker of the character here -- text-level, on writer-text
// katoms only, under the same span skips as the quoted specials
// above -- so code inside an @eval keeps its carets (grep '^-'
// reaches the shell intact) and ^'...'^ content (katom_t::literal,
// marked before this pass runs) stays exactly as typed. How the
// quoted character renders is the target's decision: the :escape
// and :resolve tables map its marker, and an unmapped marker
// decodes to the character itself. A katom this converts is no
// longer unparsed (the ^- warning would otherwise misfire).
if ((k.m_type == katom_t::word || k.m_type == katom_t::text) &&
definition_depth == 0 && code_depth == 0) {
if (hide_quoted_punctuation(k.m_text)) {
k.m_unparsed = false;
}
}
}
}
@@ -456,7 +474,7 @@ void check_bar_count(katom_iter begin, katom_iter end)
ss << "Incorrectly formatted @cond klammer. There should only be one or two bar characters:\n"
<< " @cond <predicate> | <result-if-true @\nor:\n"
<< " @cond <predicate> | <result-if-true> | <result-if-false> @";
throw Argument_error(ss.str(), begin->m_loc, false);
throw Argument_error(ss.str(), begin->m_loc);
}
}

View File

@@ -20,7 +20,7 @@ parse_name(const Target_registry& targets, const Katom& name_katom)
if (!std::regex_match(name_with_target, match, Klammer::name_re)) {
throw Parsing_error(
"The klammer name \"" + name_with_target + "\" is not correctly defined. "
"The klammer name \"" + name_with_target + "\" is not correctly defined.\n\n"
"The form is \"<klammer-name>\" for general klammers or \"<klammer-name>.<target-name>\" "
"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 "
@@ -44,7 +44,7 @@ parse_name(const Target_registry& targets, const Katom& name_katom)
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.",
"is not defined.\n\nEnter \"kdesc -t\" to see the targets defined by the Standard Klammer Set.",
name_katom.m_loc);
}
if (is_in(target_name, seen)) {
@@ -105,7 +105,7 @@ parse_definition_katoms(
" @@<name>[.<target>] :: <body> @@ instance (uses .k parameters)\n"
" @@<name>[.<target>] ::: <body> @@ override existing definition\n"
" @@<name>[.<target>] <parameters> :::: <body> @@ default (can be overridden)",
begin->m_loc, false);
begin->m_loc);
}
katom_list parameter_katoms(begin, deftype);
@@ -274,7 +274,7 @@ void Klammer::disallow_instances() //Klammer::components declaration)
<< plural("instance", icount) << " (defined by \"::\"), but "
<< "no declarations (defined by a \".k\" target)";
throw Definition_error(
error_list(ss.str(), instances), instances[0].loc, false);
error_list(ss.str(), instances), instances[0].loc);
}
}
@@ -295,7 +295,7 @@ bool Klammer::copy_to_instances(const Target_registry& targets)
<< plural("instance", icount) << " (defined by \"::\"), but "
<< dcount << " "<< plural("definition", dcount) << " (defined by \":\")";
throw Definition_error(
error_list(ss.str(), definitions), definitions[0].loc, false);
error_list(ss.str(), definitions), definitions[0].loc);
} else {
copy_components(definitions[0].parameters, m_defs, targets);
return true;
@@ -336,7 +336,7 @@ void Klammer::check_for_multiple_general_klammers()
error_list(
"There is more than one general klammer (a klammer in which no target is defined)",
general_klammers),
general_klammers[0].loc, false);
general_klammers[0].loc);
}
}
@@ -362,7 +362,7 @@ void Klammer::check_for_declaration_and_definitions()
".\nWrite \"::\" instead of \":\" so the definition takes its parameters "
"from the declaration",
definitions),
declares[0].loc, false);
declares[0].loc);
}
}
}
@@ -384,7 +384,7 @@ void Klammer::copy_general_klammer_to_undefined(const Target_registry& targets)
throw Definition_error(
error_list("The general parameters are different than the defined parameters",
general_klammers),
m_parameters.m_katoms[0].m_loc, false);
m_parameters.m_katoms[0].m_loc);
}
}
const auto& [target, deftype, parameters, body, varmap, loc] = general_klammers[0];
@@ -470,19 +470,19 @@ void Klammer::no_declarations(const Target_registry& targets)
// definitions by hand to find which one drifted.
std::stringstream ss {};
ss << "The parameters of klammer \"" << m_name
<< "\" are not the same for every target,\n"
<< "\" are not the same for every target, "
"and there is no declaration (.k) target to define them once:\n";
for (const auto& def : m_defs) {
if (std::ranges::find(target_names, def.target) == target_names.end())
continue;
std::string target = def.target;
target.resize(std::max(target.size(), size_t(6)), ' ');
ss << " " << target << " " << parameter_signature(def.parameters)
<< "\n " << def.loc.desc() << "\n";
target.resize(std::max(target.size(), size_t(4)), ' ');
ss << " Target: " << target << " Parameters: " << parameter_signature(def.parameters)
<< "\n " << def.loc.desc() << "\n";
}
ss << "Use a .k target to declare the parameters and describe the klammer,\n"
"and \"::\" with no parameters for each target's definition.";
throw Definition_error(ss.str(), m_defs[0].loc, false);
throw Definition_error(ss.str(), m_defs[0].loc);
} else {
// std::cout << " All equal\n";
copy_components(m_defs[0].parameters, m_defs, targets);
@@ -506,7 +506,7 @@ void Klammer::many_declarations(const std::vector<Klammer::components>& declares
// std::cout << boldblack << "Many declarations\n" << black;
throw Definition_error(
error_list("More than one declaration (.k) klammer", declares),
declares[0].loc, false);
declares[0].loc);
}

View File

@@ -206,9 +206,9 @@ fs::path resolve_klammerset_symbol(
}
throw Klammerset_error(
"The klammerset \"" + symbol + "\" was not found. A symbol x names the "
"declaration file x/x.k in one of the search directories: "
+ join(dirs, ", ")
+ ". Enter \"kdesc --klammersets\" to list the available klammersets.",
"declaration file x/x.k in one of the search directories:\n "
+ join(dirs, "\n ")
+ "\nEnter \"kdesc --klammersets\" to list the available klammersets.",
loc);
}

View File

@@ -192,7 +192,7 @@ std::vector<Ktype> katom_types {
Ktype(katom_t::ws_newline, "ws-newline", ws_newline_s, "Remove all whitespace, leaving <number> newlines (default: 1)"),
Ktype(katom_t::special, "special-char", R"(\^[@|#^:*])", "Klammertext special character treated as regular text"),
//Ktype(katom_t::nonascii, "non-ascii-char", R"(\^\w(?:.|\^))", "Non-ASCII character"),
Ktype(katom_t::nonascii, "non-ascii-char", R"((\^(\w(?:.|\^)))|(\^[A-Fa-f0-9]{1,5}\^))", "Non-ASCII character"),
Ktype(katom_t::nonascii, "non-ascii-char", R"((\^(\w(?:.|\^)))|(\^[A-Fa-f0-9]{1,6}\^))", "Non-ASCII character"),
// Ktype(katom_t::word, "word", R"([a-z][a-z0-9_]*)", "Lower-case letters, numbers, or underscore"),
// Ktype(katom_t::text, "text",

View File

@@ -24,6 +24,14 @@ public:
, m_chr(int(location.column()))
{};
Locator(const fs::path& filename, int line, int chr);
// "No location": for errors with no document position -- command-line
// argument errors, lookups made on the command's behalf. NOT the same
// as Locator(): the default constructor captures the C++ CALL SITE via
// std::source_location (and a defaulted Locator parameter captures the
// CALLER), which is what leaked "argv.cpp, line 597" into user-facing
// errors (found by the error gallery, 2026-08-22). Locator() is for
// logging; errors either carry a real document location or this.
static Locator none() { return Locator(fs::path{}, -1, -1); }
std::string str(bool relative = false) const;
std::string desc(bool relative = false) const;
std::string abbrev(bool include_chr=true) const;

View File

@@ -61,10 +61,10 @@ public:
if (apply_depth >= max_apply_depth) {
std::stringstream ss {};
ss << "Klammer application nested more than " << max_apply_depth
<< " levels deep while applying " << q_(name) << ".\n"
<< " levels deep while applying " << q_(name) << "."
<< "A klammer that applies itself, directly or through a cycle "
<< "of klammers, does not terminate.";
throw Recursion_error(ss.str(), loc, false);
throw Recursion_error(ss.str(), loc);
}
++apply_depth;
}
@@ -132,7 +132,7 @@ void check_bar_count(katom_iter begin, std::size_t count)
ss << "Incorrectly formatted @cond klammer. There should only be one or two bar characters:\n"
<< " @cond <predicate> | <result-if-true @\nor:\n"
<< " @cond <predicate> | <result-if-true> | <result-if-false> @";
throw Argument_error(ss.str(), begin->m_loc, false);
throw Argument_error(ss.str(), begin->m_loc);
}
}
@@ -749,24 +749,23 @@ katom_list Machine::apply_klammer(
// remain in the klammer body substitution for final target-specific output.
katom_list result(klammer.m_body[target].begin(), klammer.m_body[target].end());
auto varmap = klammer.m_varmap[target];
m_state.open_frame("Arguments for klammer " + q_(klammer.m_name));
m_state.set(values, klammer.m_parameters);
for (const auto& [name, indices] : varmap) {
std::regex arg("\\*" + name + "\\*");
for (auto i : indices) {
result[i].m_text = std::regex_replace(result[i].m_text, arg, m_state.value(name));
result[i].m_type = katom_t::text;
}
}
// Escape target-specific characters (e.g. tex "&" -> "\&") in the writer
// text of a GENERAL klammer's body. Runs BEFORE process_katoms/apply()
// below expand the body, so that target-native markup pulled in by nested
// klammers (e.g. nl.tex -> "\newline") is left untouched -- only this
// klammer's own literal writer text is escaped here; nested klammers escape
// theirs when they are applied in turn. Bodies from target-specific
// definitions (m_body_generic[target] == false) are already in target form
// and skipped. KTESC markers are idempotent, so text already escaped at the
// top level passes through unchanged. Two kinds of body content are NOT
// text of a GENERAL klammer's body. Runs BEFORE the *arg* substitution
// below: an argument value is writer text already escaped at the top
// level (KTESC markers, idempotent) plus final target markup from
// klammers the writer nested in the argument, and neither may be escaped
// here -- escaping after substitution swept both, so a general klammer
// with a klammer-bearing argument emitted \textbackslash{}emph{...}
// (fixed 2026-08-19; tst/escape_test.sh cases 26-29). A karg katom is
// skipped by the type filter; a variable inside a mixed text katom
// survives because no target declares '*' or identifier characters as
// escapes. Also runs BEFORE process_katoms/apply() expand the body, so
// that target-native markup pulled in by nested klammers (e.g. nl.tex ->
// "\newline") is left untouched -- only this klammer's own literal writer
// text is escaped here; nested klammers escape theirs when they are
// applied in turn. Bodies from target-specific definitions
// (m_body_generic[target] == false) are already in target form and
// skipped. Two kinds of body content are NOT
// writer text and must be skipped:
// * ^'...'^ literal spans -- raw target markup the writer typed directly.
// At this point they are typed literal_begin/literal_end with plain-text
@@ -818,6 +817,16 @@ katom_list Machine::apply_klammer(
}
}
}
m_state.open_frame("Arguments for klammer " + q_(klammer.m_name));
m_state.set(values, klammer.m_parameters);
for (const auto& [name, indices] : varmap) {
std::regex arg("\\*" + name + "\\*");
for (auto i : indices) {
result[i].m_text = std::regex_replace(
result[i].m_text, arg, m_state.value(name, true, result[i].m_loc));
result[i].m_type = katom_t::text;
}
}
process_katoms(result, klammer.m_name);
apply(m_klammers, result, target);
m_state.close_frame();
@@ -876,7 +885,18 @@ std::string Machine::run_phase_functions()
// phase sees its predecessor's result in K_result.
Eval E(*this, Locator());
if (!f.empty() && f[0] != ':') {
f += "(K_result)";
// K reaches a phase only as a parameter: the state's
// "class K" exists in the EVAL's globals (python_code()
// defines it), but a phase function's body resolves names in
// its own MODULE's globals. Every phase signature has
// carried "K=None" for this since the calling convention was
// created; the call never passed it, which surfaced when
// txt_justify_blocks needed Target_txt_width (2026-08-22).
// K_phase_call (defined in Eval_python's globals) passes K
// only to a function that DECLARES a K parameter, so a
// stdlib phase (string.capwords, whose second parameter is a
// separator) keeps working.
f = "K_phase_call(" + f + ", K_result, K)";
}
f = "@eval " + f + " @";
auto katoms = katomize(line_split(f), "phase");
@@ -916,7 +936,7 @@ std::string Machine::apply(const std::string& target_name, bool final_processing
// Characters produced later by klammer bodies will not be escaped.
// Skipped for sub-Machine apply() calls (e.g., from @eval), where the
// text is already in target-specific form.
auto target = m_targets.get(target_name, Locator());
auto target = m_targets.get(target_name, Locator::none());
if (escape_characters)
escape_target_characters(target, m_katoms);
@@ -935,18 +955,24 @@ std::string Machine::apply(const std::string& target_name, bool final_processing
if (++apply_count > apply_round_limit) {
std::stringstream ss {};
ss << "Klammer application did not reach a fixed point after "
<< apply_round_limit << " rounds.\n"
<< apply_round_limit << " rounds.\n\n"
<< "Each round applies every klammer present; a klammer whose "
<< "result contains further klammers starts another round.";
throw Recursion_error(ss.str(), Locator(), false);
throw Recursion_error(ss.str(), Locator());
}
}
// Typographic transforms run on the katoms, not the joined string, so
// that ^'...'^ literal content (katom_t::literal) stays verbatim --
// Target::transform() skips literal katoms. Verbatim text produced by
// an @eval renderer (@code, @c) is plain text by the time it is spliced
// back; the SKS protects it with KTESC markers (hide_typographic() in
// klammer_base.py), which are inert here and decode in resolve_escapes.
if (final_processing) {
target.transform(m_katoms);
}
m_result = to_string(m_katoms.begin(), m_katoms.end());
if (final_processing) {
for (const auto& [old_str, new_str] : target.m_transforms) {
m_result = string_replace(m_result, old_str, new_str);
}
m_result = target.resolve_escapes(m_result);
m_result = run_phase_functions();
}

View File

@@ -27,7 +27,7 @@ void Option_set_registry::add(
if (deftype.m_initial_type == katom_t::klammer_instance) {
throw Definition_error(
"The option set " + q_(name) + " is declared with \"::\", which takes its "
"parameters from a \".k\" declaration. An option set IS a declaration: "
"parameters from a \".k\" declaration. An option set is a declaration; "
"it declares its parameters itself, after \":\".",
begin->m_loc);
}
@@ -68,16 +68,16 @@ void Option_set_registry::add(
throw Definition_error(
"The option set " + q_(name) + " declares the positional "
+ plural("parameter", static_cast<int>(names.size())) + " "
+ join(names, ", ") + ".\n"
+ join(names, ", ") + ".\n\n"
"An option set declares only optional parameters -- names written with "
"a leading \":\".",
begin->m_loc, false);
begin->m_loc);
}
if (parameters.m_optional.empty()) {
throw Definition_error(
"The option set " + q_(name) + " declares no parameters.\n"
"The option set " + q_(name) + " declares no parameters.\n\n"
"The form is: @@" + name + ".o :name.argtype default ... : <description> @@",
begin->m_loc, false);
begin->m_loc);
}
// The members as written: these katoms are what is spliced into the
@@ -201,14 +201,14 @@ std::string definition_name(const std::string& klammer_name, const std::string&
std::stringstream ss {};
if (option_sets.has(name) && target_name == Target_registry::optionset_name) {
ss << "The option set " << q_(name) << " is used in the declaration of the option "
<< "set " << q_(klammer_name) << ".\n"
<< "set " << q_(klammer_name) << ".\n\n"
<< "An option set is used only in the parameter list of a \".k\" declaration, "
<< "so a set does not include another set: a klammer that needs two "
<< "vocabularies names two sets, and each set stays a vocabulary that can "
<< "be learned whole.";
} else if (option_sets.has(name)) {
ss << "The option set " << q_(name) << " is used in the parameter list of "
<< definition_name(klammer_name, target_name) << ".\n"
<< definition_name(klammer_name, target_name) << ".\n\n"
<< "An option set may be used only in the parameter list of a \".k\" "
<< "declaration, which is where a klammer's interface is declared once "
<< "for all of its targets. Declare "
@@ -216,15 +216,15 @@ std::string definition_name(const std::string& klammer_name, const std::string&
<< "an instance (\"::\"), which inherits the declared parameters.";
} else {
ss << "The klammer " << q_(name) << " is applied in the parameter list of "
<< definition_name(klammer_name, target_name) << ".\n"
<< definition_name(klammer_name, target_name) << ".\n\n"
<< "A klammer application in a parameter list is not allowed: it is "
<< "resolved after the parameters are parsed, so the parameter list it "
<< "was meant to contribute is not there when the list is read. An "
<< "option set, declared with a \".o\" target, is how parameters are "
<< "shared between klammers. Declared option sets: "
<< "shared between klammers.\n\nDeclared option sets: "
<< option_sets.available() << ".";
}
throw Definition_error(ss.str(), loc, false);
throw Definition_error(ss.str(), loc);
}
// The default written for each member at the use site:
@@ -237,10 +237,10 @@ std::map<std::string, std::string> use_site_defaults(
if (!positional.empty() || active(rest)) {
throw Definition_error(
"The use of the option set " + q_(option_set.m_name) +
" gives a value that is not an option.\n"
" gives a value that is not an option.\n\n"
"A set's names and types are fixed where the set is declared; only a "
"default may be given where it is used, written as \":name value\".",
begin->m_loc, false);
begin->m_loc);
}
std::map<std::string, std::string> defaults {};
for (const auto& option : optional) {
@@ -248,17 +248,17 @@ std::map<std::string, std::string> use_site_defaults(
if (name.size() + 1 != option[0].m_text.size()) {
throw Definition_error(
"The use of the option set " + q_(option_set.m_name) + " gives a type for \":"
+ name + "\".\n"
+ name + "\".\n\n"
"A set's names and types are declared where the set is; only a default "
"may be given where it is used.",
option[0].m_loc, false);
option[0].m_loc);
}
const Parameter* member = option_set.find(name);
if (member == nullptr) {
throw Definition_error(
"The option set " + q_(option_set.m_name) + " has no parameter \":"
+ name + "\".\n It declares: " + option_set.member_names(),
option[0].m_loc, false);
+ name + "\". It declares:\n " + option_set.member_names(),
option[0].m_loc);
}
if (defaults.count(name) > 0) {
throw Definition_error(
@@ -297,7 +297,7 @@ option_set_uses_t expand_option_sets(
throw Definition_error(
"The parameter \":" + name + "\" of " + q_(klammer_name) +
" is declared twice:\n " + previous->second + "\n " + from,
loc, false);
loc);
}
origin[name] = from;
};

View File

@@ -24,16 +24,21 @@ void Target::add_transforms(const string_pairs& transforms)
}
}
// The typographic transform pass, run by Machine::apply() at final
// processing. Per katom rather than over the joined result string, so
// that ^'...'^ literal content (katom_t::literal) is never transformed --
// verbatim text must show the characters the writer typed. A transform
// source therefore cannot match across a katom boundary, which is the
// correct reading: two hyphens separated by a klammer application were
// separated by the writer and are not a dash.
void Target::transform(katom_list& katoms) const
{
(void)K::log(3);
std::for_each(
katoms.begin(), katoms.end(),
[this] (Katom& k) {
// std::cout << "transform: " << k << "\n";
if (k.m_type != katom_t::literal) {
for (const auto& [a, b] : this->m_transforms) {
// std::cout << " " << a << right_arrow << b << "\n";
k.m_text = string_replace(k.m_text, a, b);
}
}
@@ -67,6 +72,14 @@ void Target::add_escapes(const std::string& escape_spec)
}
}
void Target::add_resolves(const std::string& resolve_spec)
{
auto words = word_split(resolve_spec);
for (size_t i = 0; i + 1 < words.size(); i += 2) {
m_resolves.push_back({words[i], words[i+1]});
}
}
std::string Target::escape_marker(const std::string& ch)
{
std::stringstream ss {};
@@ -94,11 +107,16 @@ std::string Target::unescape_text(std::string text) const
std::string Target::resolve_escapes(std::string text) const
{
// Target-declared escapes first (marker -> declared replacement), then
// the generic decode for the remaining markers (marker -> the character
// itself: quoted Klammertext specials and literal-span content).
// the resolution-only entries (:resolve -- how a QUOTED character
// renders here), then the generic decode for the remaining markers
// (marker -> the character itself: quoted punctuation and literal-span
// content).
for (const auto& [ch, repl] : m_escapes) {
text = string_replace(text, escape_marker(ch), repl);
}
for (const auto& [ch, repl] : m_resolves) {
text = string_replace(text, escape_marker(ch), repl);
}
return ktesc_resolve(text);
}
@@ -130,6 +148,29 @@ std::string ktesc_resolve(std::string text)
return text;
}
bool hide_quoted_punctuation(std::string& s)
{
// ASCII punctuation, EXCEPT "'" -- ^' opens a ^'...'^ literal span, the
// one documented exception to the rule (a literal apostrophe is ^0027^).
// Hand-rolled scan: no std::regex, this can run over large text.
static const std::string punct = R"pct(!"#$%&()*+,-./:;<=>?@[\]^_`{|}~)pct";
if (s.find('^') == std::string::npos) return false;
std::string result {};
bool changed = false;
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] == '^' && i + 1 < s.size()
&& punct.find(s[i + 1]) != std::string::npos) {
result += Target::escape_marker(std::string(1, s[i + 1]));
++i;
changed = true;
} else {
result += s[i];
}
}
if (changed) s = result;
return changed;
}
std::string hide_structural_characters(const std::string& s)
{
std::string result {};
@@ -145,7 +186,26 @@ std::string hide_structural_characters(const std::string& s)
void Target::add_after_apply(const std::string& function_specs)
{
for (const auto& f : regex_split(function_specs, std::regex(R"(\s+;\s+)"), true)) {
// msg() << "Add " << m_name << " after-apply: " << f << "\n";
// A bare spec is ONE Python name (module.function): whitespace
// inside it means two specs were written without the " ; "
// separator, and the glued call would otherwise fail only at render
// time, as a Python SyntaxError located at "phase" rather than at
// this declaration (found 2026-08-22, the first time a target
// declared two phases). Mode-tagged specs (":cpp <library>
// <function>") are exempt: the library is a filename, and filenames
// may contain spaces -- which is exactly why the list separator is
// ";" rather than whitespace.
if (!f.empty() && f[0] != ':'
&& f.find_first_of(" \t\n") != std::string::npos) {
throw Definition_error(
"The :after_apply phase \"" + f + "\" contains whitespace. "
"A bare phase is a single Python name (module.function), and "
"several phases are separated by \" ; \":\n"
" :after_apply first.phase ; second.phase\n"
"(A mode-tagged phase -- \":cpp <library> <function>\" -- may "
"contain spaces; its library is a filename.)",
m_loc);
}
m_after_apply.push_back(f);
}
}

View File

@@ -24,6 +24,7 @@ public:
void transform(std::vector<Katom>& katoms) const;
void add_escapes(const std::string& escape_spec);
void add_resolves(const std::string& resolve_spec);
std::string escape_text(std::string text) const;
std::string unescape_text(std::string text) const;
std::string resolve_escapes(std::string text) const;
@@ -40,6 +41,13 @@ public:
Locator m_loc {};
std::vector<std::pair<std::string, std::string>> m_transforms {};
std::vector<std::pair<std::string, std::string>> m_escapes {};
// Resolution-only entries (:resolve): how a QUOTED character renders in
// this target. The resolve half of :escape without the escape half --
// needed where the raw character must stay untouched in writer text
// (tex cannot escape "-" without destroying the --- convention) but the
// quoted character must not decode to its raw self (a decoded -- would
// re-form TeX's dash ligature).
std::vector<std::pair<std::string, std::string>> m_resolves {};
// Argtype_registry m_argtypes {};
@@ -60,3 +68,14 @@ std::string ktesc_resolve(std::string text);
// sub-Machines and the @eval result read-back. Resolved by ktesc_resolve()
// at final processing.
std::string hide_structural_characters(const std::string& s);
// Replace each ^P pair in s -- "^" before any ASCII punctuation character
// except "'" (which opens a ^'...'^ literal span) -- with the KTESC marker
// of P: "^" before a punctuation character quotes it, uniformly, not only
// the six Klammertext specials. A letter or digit is never special, so
// ^<letter-or-digit> keeps its existing meanings (diacritics, mnemonics,
// ^UUUU^ code points). Returns whether anything was replaced. Called from
// hide_special_katoms() on writer-text katoms OUTSIDE definition and
// @eval/@read/@cond spans -- code keeps its carets (grep '^-' must reach
// the shell intact), the same skip set as the quoted-special hiding.
bool hide_quoted_punctuation(std::string& s);

View File

@@ -14,7 +14,7 @@ std::string Target_registry::general_name = "*";
std::string Target_registry::optionset_name = "o";
Target_registry::Target_registry()
: m_parameters(Parameter_set("name | desc :after_apply :after_write :includes :escape | transforms.rest"))
: m_parameters(Parameter_set("name | desc :after_apply :after_write :includes :escape :resolve | transforms.rest"))
{
// Registration order is display order. The two that declare an interface
// come first -- "k" a klammer's, "o" an option set's -- and then "*", the
@@ -47,14 +47,25 @@ void Target_registry::add(std::vector<Katom>::iterator begin, std::vector<Katom>
Target target(values["name"], values["desc"], begin->m_loc);
target.add_transforms(values["transforms"]);
target.add_escapes(values["escape"]);
target.add_resolves(values["resolve"]);
target.add_after_apply(values["after_apply"]);
target.m_includes = word_split(values["includes"]);
// Inherit escapes from included targets
// Inherit escapes AND typographic transforms from included targets: an
// including target renders through the included one's syntax (pdf
// through tex), so both tables apply there too. Inherited entries are
// appended after the including target's own, so a declaration can
// override an inherited pair by declaring its source first.
for (const auto& included : target.m_includes) {
if (m_targets.count(included)) {
for (const auto& esc : m_targets[included].m_escapes) {
target.m_escapes.push_back(esc);
}
for (const auto& tr : m_targets[included].m_transforms) {
target.add_transform(tr.first, tr.second);
}
for (const auto& res : m_targets[included].m_resolves) {
target.m_resolves.push_back(res);
}
}
}
// Registration (and the previous-definition check) goes through
@@ -79,7 +90,7 @@ void Target_registry::check_for_previous_definition(const std::string& name, con
if (has(name)) {
const Target& current = m_targets.at(name);
throw Target_error("Target \"" + name + "\" is already defined:\n " + current.m_loc.desc(),
loc, false);
loc);
}
}

View File

@@ -51,7 +51,7 @@ public:
/*
std::string m_parameters_spec {"name | desc :after_apply :after_write :includes | transforms.rest"};
std::string m_parameters_spec {"name | desc :after_apply :after_write :includes :escape :resolve | transforms.rest"};
Parameter_set m_parameters =
katomize(line_split(m_parameters_spec), Locator().str());
*/

View File

@@ -226,14 +226,66 @@ std::string justify_string(const std::string& s, unsigned int width=80, bool fre
return result;
}
// The error-message formatting contract (2026-08-21): a message carries no
// decisions about line breaks EXCEPT by indentation.
// * a line beginning with whitespace is VERBATIM -- an example, a pattern,
// a signature, a list entry -- emitted untouched: no folding, no
// wrapping, no "~" substitution (a pattern may contain a literal ~);
// * blank lines separate blocks (runs collapse to one);
// * everything else is prose: consecutive lines fold into one paragraph
// and are wrapped to the width.
// So prose stays machine-wrapped however the source hand-wraps it, and the
// one legitimate exception is marked in the one place it cannot be missed.
// This replaced a per-call do_justify flag on the Error constructors, whose
// decision lived apart from the text it governed. Line-based by hand: no
// std::regex over the whole message (the ambiguous-alternation hazard).
std::string justify(
const std::string& input_text, unsigned int text_width, unsigned int margin_width)
{
std::string result {};
text_width -= margin_width;
for (const std::string& par : split_into_paragraphs(trim(input_text))) {
result += justify_string(par, text_width) + "\n\n";
strings_t lines {};
{
const std::string text = trim(input_text);
std::string::size_type from = 0;
while (from <= text.size()) {
auto nl = text.find('\n', from);
if (nl == std::string::npos) {
lines.push_back(text.substr(from));
break;
}
lines.push_back(text.substr(from, nl - from));
from = nl + 1;
}
}
std::string result {};
strings_t prose {};
bool pending_blank = false;
auto emit = [&](const std::string& block) {
if (!result.empty()) {
result += pending_blank ? "\n\n" : "\n";
}
result += block;
pending_blank = false;
};
auto flush_prose = [&]() {
if (!prose.empty()) {
emit(justify_string(join(prose, " "), text_width));
prose.clear();
}
};
for (const std::string& line : lines) {
if (trim(line).empty()) { // block separator
flush_prose();
pending_blank = !result.empty();
} else if (line[0] == ' ' || line[0] == '\t') { // verbatim line
flush_prose();
emit(trim_right(line));
} else { // prose
prose.push_back(trim(line));
}
}
flush_prose();
result += "\n";
if (margin_width > 0) {
result = add_margin(result, margin_width);
}
@@ -367,8 +419,8 @@ std::vector<std::pair<std::string, std::string>> environment_variables(bool allo
auto parts = regex_split(environ[i], std::regex("="), true);
if (!allow_empty_definitions && parts.size() < 2) {
throw Internal_error(
"Incorrect environment variable format:\n" + std::string(environ[i]),
Locator(), false);
"Incorrect environment variable format:\n " + std::string(environ[i]),
Locator());
}
std::string name = parts[0];
parts.erase(parts.begin());