Files
klammertext/mac/klammerset_registry.cpp
T
ackandClaude Fable 5.1 715d939727 What a Windows editor saves, and a standard user's Windows install (from dev 9d6f66456a3d)
- CRLF line endings are read as newlines on every system (the reader used
  to delete the pair, collapsing a Notepad++ file into one line on Linux
  and macOS); a leading UTF-8 byte-order mark is ignored; a UTF-16 or
  Windows-1252 source is refused with the encoding, the place and the
  remedy named. .gitattributes pins LF in every clone.
- Windows source install: the repository path must hold no space (GNU
  Make), accented letters are fine (every linker input is relative now);
  python.org's Python is selected ahead of an MSYS2 one that poppler pulls
  in; TeX Live goes to its Windows default C:\texlive\2026.
- The Python binding needs an ASCII installation path on Windows, and its
  suite says so there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 16:17:59 +02:00

347 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).generic_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 strings_t installed =
directory_list("KLAMMERTEXT_KLAMMERSETS", ".klammertext/klammersets");
for (const std::string& dir : installed) {
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 = klammertext_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.generic_string();
if (first_hit.count(symbol)) {
line += " (shadowed by " + first_hit[symbol] + ")";
} else {
first_hit[symbol] = declaration.generic_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;
}