A character written in the ^...^ mnemonic form -- ^oe^, ^zz^, ^hello^ --
used to kill ktext outright: no message, no output, and an exit status
that looked like a crash because it was one. Only the forms that happen
to spell hexadecimal digits escaped, and they were right by luck rather
than by reading. Any caret form that names no character is now a located
error that quotes what was written.
A klammer may now sit inside square brackets and braces as it already
could inside parentheses, and may be followed by the punctuation that
ends a sentence: [@i word@], {@b word@}, @i word@'s. The rule is a fixed
list of characters, because none of them can occur inside a klammer name
and so none of them is ambiguous next to a delimiter. A letter still
cannot: "x@" is the named closing delimiter, so a letter before "@" has
two readings and no rule can separate them.
The abbreviations an author writes -- the hyphen form @i-word, a klammer
glued to its neighbouring punctuation -- are now recognised as author
conveniences only. They are not applied to @@ definitions or @@@ system
commands, which are written by someone who needs no such shorthand and
whose forms are already whole.
Also fixed, each of them silent: an exported shell function in the
environment broke every @eval in the document; a line break written with
@nl was destroyed when it passed through a klammer that generates a
table, printing as \textbackslash{}newline in the finished PDF; and
"kdesc --coverage" shortened its file column by comparing the spelling of
a path rather than the path, so a klammer set reached by a symbolic link
showed a full path where its neighbours showed a short one.
New in the distribution: ref/, a small klammerset that describes the
Klammermachine's own tables in Klammertext. It currently provides the
character tables -- the diacritics and the extended Latin letters -- as
klammers that render to any target, so the documentation of what can be
written cannot drift from what the machine accepts. It is early, and
will grow.
The Klammertext taxonomy now has a section on targets, describes the
@@@klammerset system command alongside the other three, and states the
precedence of "^" -- what a caret means before a letter, a digit, a
punctuation character, and a quote, in one ordered list.
(from dev 6594c7ff8c1f)
359 lines
14 KiB
C++
359 lines
14 KiB
C++
#include <cctype>
|
|
#include <cstdlib>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
|
|
#include "klammerset_registry.h"
|
|
#include "error.h"
|
|
#include "file.h"
|
|
#include "log.h"
|
|
#include "util.h"
|
|
#include "show.h"
|
|
#include "katom.h"
|
|
|
|
Klammerset_registry::Klammerset_registry()
|
|
: m_parameters(Parameter_set("symbol | desc :name :author :date :requires :files"))
|
|
{
|
|
}
|
|
|
|
void Klammerset_registry::add(Klammerset klammerset)
|
|
{
|
|
(void)K::log(3, klammerset.m_symbol);
|
|
m_klammersets[klammerset.m_symbol] = klammerset;
|
|
m_symbols.push_back(klammerset.m_symbol);
|
|
}
|
|
|
|
std::optional<Klammerset> Klammerset_registry::add(
|
|
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms)
|
|
{
|
|
(void)K::log(3, *begin, *(end - 1));
|
|
auto [positional, optional, rest] =
|
|
argument_split(begin + 1, end - 1, m_parameters.m_positional.size());
|
|
auto values = m_parameters.value_map(positional, optional, rest, begin->m_loc);
|
|
|
|
modify_type(katom_t::replaced, begin, end);
|
|
auto next_iter = end;
|
|
ignore_whitespace(next_iter, katoms);
|
|
|
|
std::string symbol = values["symbol"];
|
|
check_symbol(symbol, begin->m_loc);
|
|
check_declaring_file(symbol, begin->m_loc);
|
|
if (has(symbol)) {
|
|
(void)K::log(2, "Klammerset \"" + symbol + "\" is already loaded; declaration skipped");
|
|
return std::nullopt;
|
|
}
|
|
|
|
// The filename lists follow the standard conventions (spaces allowed,
|
|
// standalone "/" separator). The existence rescue tests names against
|
|
// the declaring file's directory, where relative names are later
|
|
// resolved by Machine::load_klammerset_files.
|
|
fs::path declaring(begin->m_loc.m_filename);
|
|
std::string base_dir =
|
|
fs::exists(declaring) ? declaring.parent_path().string() : "";
|
|
|
|
Klammerset klammerset(symbol, values["desc"], begin->m_loc);
|
|
klammerset.m_name = values["name"];
|
|
klammerset.m_author = values["author"];
|
|
klammerset.m_date = values["date"];
|
|
klammerset.m_requires = resolve_filename_list(values["requires"], base_dir);
|
|
klammerset.m_files = resolve_filename_list(values["files"], base_dir);
|
|
add(klammerset);
|
|
return klammerset;
|
|
}
|
|
|
|
void Klammerset_registry::check_symbol(const std::string& symbol, const Locator& loc) const
|
|
{
|
|
bool valid = !symbol.empty() && std::isalpha(static_cast<unsigned char>(symbol[0]));
|
|
for (char c : symbol) {
|
|
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_') {
|
|
valid = false;
|
|
}
|
|
}
|
|
if (!valid) {
|
|
throw Klammerset_error(
|
|
"The klammerset symbol \"" + symbol + "\" is not valid. A symbol begins "
|
|
"with a letter and contains only letters, digits, and underscores.",
|
|
loc);
|
|
}
|
|
}
|
|
|
|
// A klammerset that can be NAMED must live where its name says: symbol X is
|
|
// declared in X/X.k (Andy, 2026-08-15). The convention already governed
|
|
// symbol RESOLUTION; making it a requirement of the declaration too is what
|
|
// turns the symbol into a function of the path -- and that is what lets the
|
|
// already-loaded guard run BEFORE a file is read rather than after, which is
|
|
// the whole of the diamond-":requires" fix. Without it the symbol is known
|
|
// only once the file has been parsed, and by then a second read has already
|
|
// re-executed the declaring file's own definitions.
|
|
//
|
|
// It also continues the reasoning behind symbols-only on the command line
|
|
// (2026-08-14): if every klammerset is reachable by symbol the set of all of
|
|
// them is ENUMERABLE; if every declaration is in X/X.k each one is also
|
|
// IDENTIFIABLE from where it sits.
|
|
//
|
|
// EXEMPT: a declaration that is not in a ".k" file at all. A document may
|
|
// declare a klammerset -- that is how a designer writes one, and kdesc's
|
|
// provenance filter depends on it -- and such a set is local to the document:
|
|
// nothing can ":requires" it, so it has no identity to protect.
|
|
void Klammerset_registry::check_declaring_file(
|
|
const std::string& symbol, const Locator& loc) const
|
|
{
|
|
fs::path declaring(loc.m_filename);
|
|
if (declaring.extension() != ".k") {
|
|
return;
|
|
}
|
|
if (declaring.stem() == symbol && declaring.parent_path().filename() == symbol) {
|
|
return;
|
|
}
|
|
throw Klammerset_error(
|
|
"The klammerset \"" + symbol + "\" must be declared in a file named \""
|
|
+ symbol + "/" + symbol + ".k\", but this declaration is in \""
|
|
+ declaring.filename().string() + "\" (in a directory named \""
|
|
+ declaring.parent_path().filename().string() + "\"). A klammerset that "
|
|
"can be named is identified by where it is, so that a symbol names one "
|
|
"file and one file declares one symbol. A klammerset local to a document "
|
|
"may be declared in the document itself.",
|
|
loc);
|
|
}
|
|
|
|
bool Klammerset_registry::has(const std::string& symbol) const
|
|
{
|
|
return m_klammersets.count(symbol) > 0;
|
|
}
|
|
|
|
Klammerset Klammerset_registry::get(const std::string& symbol, const Locator& loc) const
|
|
{
|
|
if (has(symbol)) {
|
|
return m_klammersets.at(symbol);
|
|
} else {
|
|
throw Klammerset_error("Klammerset " + symbol + " does not exist", loc);
|
|
}
|
|
}
|
|
|
|
// --- The klammerset search path ---
|
|
|
|
bool is_klammerset_symbol(const std::string& name)
|
|
{
|
|
if (name.empty() || !std::isalpha(static_cast<unsigned char>(name[0]))) {
|
|
return false;
|
|
}
|
|
for (char c : name) {
|
|
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::vector<std::string> klammerset_search_dirs(const std::string& local_dir)
|
|
{
|
|
std::vector<std::string> result {};
|
|
// A repeated directory (e.g. the local stage already IS
|
|
// $KLAMMERTEXT_HOME) adds nothing and clutters error messages.
|
|
auto push_unique = [&result](const std::string& dir) {
|
|
std::string canonical = fs::weakly_canonical(dir).string();
|
|
if (!is_in(canonical, result)) {
|
|
result.push_back(canonical);
|
|
}
|
|
};
|
|
if (!local_dir.empty()) {
|
|
push_unique(local_dir);
|
|
}
|
|
// Read ONCE per process, not once per resolution. A search path decides
|
|
// WHICH FILE a symbol means -- an identity question, not a value one -- so
|
|
// it must not move while a document is being processed: if it did, "which
|
|
// klammerset is X" would depend on evaluation order and "kdesc
|
|
// --klammersets" could not be a complete answer, which is the reason the
|
|
// command line takes symbols only.
|
|
//
|
|
// It could move. The embedded Python shares the process, so an @eval
|
|
// doing os.environ[...] = ... changed what a later getenv here returned;
|
|
// a document could extend its own search path between one declaration and
|
|
// the next. Nothing intended that -- it fell out of a fresh getenv and an
|
|
// in-process interpreter.
|
|
static const std::string paths = [] {
|
|
const char* env = std::getenv("KLAMMERTEXT_KLAMMERSETS");
|
|
if (env && *env) {
|
|
return std::string(env);
|
|
}
|
|
if (const char* home = std::getenv("HOME"); home && *home) {
|
|
return std::string(home) + "/.klammertext/klammersets";
|
|
}
|
|
return std::string();
|
|
}();
|
|
std::stringstream ss(paths);
|
|
std::string dir;
|
|
while (std::getline(ss, dir, ':')) {
|
|
if (!dir.empty()) {
|
|
push_unique(dir);
|
|
}
|
|
}
|
|
if (const char* kthome = std::getenv(klammertext_home_var.c_str()); kthome && *kthome) {
|
|
push_unique(kthome);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
fs::path resolve_klammerset_symbol(
|
|
const std::string& symbol, const std::string& local_dir, const Locator& loc)
|
|
{
|
|
std::vector<std::string> dirs = klammerset_search_dirs(local_dir);
|
|
for (const std::string& dir : dirs) {
|
|
fs::path candidate = fs::path(dir) / symbol / (symbol + ".k");
|
|
if (fs::exists(candidate) && fs::is_regular_file(candidate)) {
|
|
return fs::weakly_canonical(candidate);
|
|
}
|
|
}
|
|
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:\n "
|
|
+ join(dirs, "\n ")
|
|
+ "\nEnter \"kdesc --klammersets\" to list the available klammersets.",
|
|
loc);
|
|
}
|
|
|
|
// The description out of a klammerset's declaration, WITHOUT loading it.
|
|
//
|
|
// The listing below enumerates the sets a user could load, and loading is
|
|
// not an option for finding out what they are: a "@@@klammerset"
|
|
// declaration is OPERATIVE -- processing one reads its ":requires" and
|
|
// ":files" -- so describing five available sets by loading them would load
|
|
// all five. So this reads the declaring file as text and takes the second
|
|
// positional, following the declaration's own parameter spec
|
|
// ("symbol | desc :name :author :date :requires :files"): the description
|
|
// runs from the bar after the symbol to whichever comes first of an option
|
|
// (" :" and a letter -- the same boundary the real parser sees, which is
|
|
// why a description may not contain a bare ":name"), another bar, or the
|
|
// closing "@@@".
|
|
//
|
|
// It is a DISPLAY convenience and says so by failing quietly: anything it
|
|
// cannot read as a description yields an empty string and the listing
|
|
// simply shows the symbol and its file, as it did before.
|
|
static std::string klammerset_description(const fs::path& declaration)
|
|
{
|
|
std::string text {};
|
|
try {
|
|
text = string_from_file(declaration.string());
|
|
} catch (...) {
|
|
return "";
|
|
}
|
|
static const std::string tag = "@@@klammerset";
|
|
size_t at = text.find(tag);
|
|
if (at == std::string::npos) return "";
|
|
size_t bar = text.find('|', at + tag.size());
|
|
if (bar == std::string::npos) return "";
|
|
size_t pos = bar + 1;
|
|
size_t end = text.size();
|
|
for (size_t i = pos; i + 1 < text.size(); i++) {
|
|
if (text[i] == '|' || (text.compare(i, 3, "@@@") == 0)) {
|
|
end = i;
|
|
break;
|
|
}
|
|
// An option: whitespace, ":", a letter. "Document production:
|
|
// formatting" is NOT one -- its colon ends a word and is followed
|
|
// by a space -- which is the distinction the parser makes too.
|
|
if (text[i] == ':' && i > pos
|
|
&& std::isspace(static_cast<unsigned char>(text[i - 1]))
|
|
&& std::isalpha(static_cast<unsigned char>(text[i + 1]))) {
|
|
end = i;
|
|
break;
|
|
}
|
|
}
|
|
return collapse_whitespace(text.substr(pos, end - pos));
|
|
}
|
|
|
|
std::string describe_klammerset_search(const std::string& local_dir, int margin)
|
|
{
|
|
std::string tab(margin, ' ');
|
|
std::stringstream ss {};
|
|
std::map<std::string, std::string> first_hit {};
|
|
for (const std::string& dir : klammerset_search_dirs(local_dir)) {
|
|
if (!fs::is_directory(dir)) {
|
|
continue;
|
|
}
|
|
for (const auto& entry : fs::directory_iterator(dir)) {
|
|
if (!entry.is_directory()) {
|
|
continue;
|
|
}
|
|
std::string symbol = entry.path().filename().string();
|
|
fs::path declaration = entry.path() / (symbol + ".k");
|
|
if (!is_klammerset_symbol(symbol) || !fs::is_regular_file(declaration)) {
|
|
continue;
|
|
}
|
|
std::string line = tab + symbol + sp_arrow + declaration.string();
|
|
if (first_hit.count(symbol)) {
|
|
line += " (shadowed by " + first_hit[symbol] + ")";
|
|
} else {
|
|
first_hit[symbol] = declaration.string();
|
|
}
|
|
// What a set IS, under where it lives: the symbol and the path
|
|
// say which file would load, and nothing at all about whether
|
|
// this is a set to load or (say) the commands' own reference
|
|
// documentation. Read from the declaration without loading it
|
|
// -- see klammerset_description() above.
|
|
std::string desc = klammerset_description(declaration);
|
|
if (!desc.empty()) {
|
|
line += "\n" + tab + std::string(symbol.size(), ' ')
|
|
+ " " + desc;
|
|
}
|
|
ss << line << "\n";
|
|
}
|
|
}
|
|
if (first_hit.empty()) {
|
|
ss << tab << "(no klammersets found)\n";
|
|
}
|
|
return ss.str();
|
|
}
|
|
|
|
std::string Klammerset_registry::describe(int margin, bool long_format) const
|
|
{
|
|
std::string tab(margin, ' ');
|
|
std::stringstream ss {};
|
|
std::vector<std::string> descs {};
|
|
for (const std::string& symbol : m_symbols) {
|
|
descs.push_back(m_klammersets.at(symbol).m_desc);
|
|
}
|
|
auto symbol_width = max_length(m_symbols);
|
|
auto desc_width = max_length(descs);
|
|
for (const std::string& symbol : m_symbols) {
|
|
const Klammerset& ks = m_klammersets.at(symbol);
|
|
if (long_format) {
|
|
ss << tab << std::setfill(' ') << std::setw(symbol_width) << std::left << symbol << " "
|
|
<< std::setw(desc_width) << std::left << ks.m_desc << " "
|
|
<< ks.m_loc.str() << "\n";
|
|
if (!ks.m_name.empty())
|
|
ss << tab << std::string(symbol_width, ' ') << " name: " << ks.m_name << "\n";
|
|
if (!ks.m_author.empty())
|
|
ss << tab << std::string(symbol_width, ' ') << " author: " << ks.m_author << "\n";
|
|
if (!ks.m_date.empty())
|
|
ss << tab << std::string(symbol_width, ' ') << " date: " << ks.m_date << "\n";
|
|
if (!ks.m_requires.empty())
|
|
ss << tab << std::string(symbol_width, ' ') << " requires: " << join(ks.m_requires, " / ") << "\n";
|
|
if (!ks.m_files.empty())
|
|
ss << tab << std::string(symbol_width, ' ') << " files: " << join(ks.m_files, " / ") << "\n";
|
|
} else {
|
|
ss << tab << std::setfill(' ') << std::setw(symbol_width) << std::left << symbol
|
|
<< sp_arrow << ks << "\n";
|
|
}
|
|
}
|
|
return ss.str();
|
|
}
|
|
|
|
void Klammerset_registry::set_loaded_files(const std::string& symbol, const strings_t& files)
|
|
{
|
|
auto it = m_klammersets.find(symbol);
|
|
if (it != m_klammersets.end()) {
|
|
it->second.m_loaded_files = files;
|
|
}
|
|
}
|
|
|
|
strings_t Klammerset_registry::loaded_files() const
|
|
{
|
|
strings_t result {};
|
|
for (const auto& [symbol, klammerset] : m_klammersets) {
|
|
result.insert(result.end(),
|
|
klammerset.m_loaded_files.begin(), klammerset.m_loaded_files.end());
|
|
}
|
|
return result;
|
|
}
|