Document language for dates; @eval pathname resolution and :cwd (from dev bc7cd62b6f68)

@date/@datetime gain :lang (German: "16. Juni 1910") over a document-wide
Language state variable, and :number for the numeric form (en 6/16/1910,
de 16.06.1910 per DIN 5008).  @eval finds Python modules next to the file
that names them regardless of the cwd, and the new :cwd option runs an
eval in a chosen working directory (@source_file uses it to resolve
against the document).  Two new test suites ship in tst/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 20:25:49 +02:00
parent f84603ee19
commit 61b21d1397
12 changed files with 309 additions and 15 deletions

View File

@@ -7,6 +7,7 @@
#include "katom.h"
#include "file.h"
#include <algorithm>
#include <optional>
#include <unistd.h>
std::string shell(State state, std::string command, Locator loc)
@@ -114,12 +115,64 @@ void check_cpp_arguments(katom_list args, Locator loc)
}
// Save the process working directory, change to DIR, and restore on
// destruction (exception-safe), so an @eval's :cwd cannot leak into the
// rest of the run. NOTE: the cwd is process-global state; if input files
// are ever processed in parallel, this needs rethinking.
class Cwd_guard
{
public:
explicit Cwd_guard(const std::string& dir)
: m_saved(fs::current_path())
{
fs::current_path(dir);
}
~Cwd_guard()
{
std::error_code ec;
fs::current_path(m_saved, ec); // never throw from a destructor
}
Cwd_guard(const Cwd_guard&) = delete;
Cwd_guard& operator=(const Cwd_guard&) = delete;
private:
fs::path m_saved;
};
std::string Eval::eval_command(katom_iter begin, katom_iter end)
{
(void)K::log(3, *begin, *(end - 1));
//msg() << "in Eval::eval:\n" << ktype << kall << kindex << std::pair(begin, end) << "\n";
katom_iter first = after_whitespace(begin + 1);
std::string eval_result = "[unevaluated]";
// :cwd DIR — run the eval (any mode) with DIR as the working directory,
// restoring the process cwd afterwards. The default is the directory
// ktext was started in (unchanged behavior). DIR may hold state
// substitutions (:cwd *K_input_dir* is the document's directory) and,
// per the filenames-with-spaces convention, whitespace-separated tokens
// are joined until they name an existing directory.
std::optional<Cwd_guard> cwd_guard {};
if (first->m_text == ":cwd") {
katom_iter tok = after_whitespace(first + 1);
std::string dir {};
katom_iter cursor = tok;
katom_iter resume = tok;
while (cursor < end - 1 && !cursor->m_text.starts_with(":")) {
dir = m_machine.m_state.subst(as_string(tok, cursor + 1, true));
resume = after_whitespace(cursor + 1);
if (fs::is_directory(dir)) break;
cursor = resume;
}
if (dir.empty() || !fs::is_directory(dir)) {
throw Argument_error(
"The :cwd directory does not exist: \"" + dir + "\"",
begin->m_loc, false);
}
cwd_guard.emplace(dir);
first = resume;
}
std::string first_word = first->m_text;
int offset = first_word[0] == ':' ? 1 : 0;
std::string command = as_string(first + offset, end - 1, true);
@@ -145,6 +198,18 @@ std::string Eval::eval_command(katom_iter begin, katom_iter end)
lib_text = string_replace(lib_text, "*KLAMMERTEXT_HOME*", khome);
}
fs::path libpath(lib_text + ".so");
// A relative library name not found from the cwd is searched in the
// directories of the files the Machine has read (same rule as the
// Python module path: the library lives next to the file using it).
if (libpath.is_relative() && !fs::exists(fs::absolute(libpath))) {
for (const auto& dir : m_machine.m_state.m_search_dirs) {
fs::path candidate = fs::path(dir) / libpath;
if (fs::exists(candidate)) {
libpath = candidate;
break;
}
}
}
libpath = fs::absolute(libpath);
std::string funcname = args.size() == 4 ? libpath.stem().string() : args[3].m_text;
Eval_cpp E_cpp(m_machine, begin->m_loc);

View File

@@ -269,6 +269,9 @@ katom_list Machine::process(
void Machine::read(const fs::path& pathname)
{
(void)K::log(3, pathname.string());
// A file's directory joins the @eval search path (Python modules and
// :cpp libraries live next to the file that uses them).
m_state.add_search_dir(fs::absolute(pathname).parent_path().string());
m_state.open_frame("Machine state: " + pathname.string());
std::string text = m_state.subst(trim_right(string_from_file(pathname)));
katom_list katoms = process(text, pathname);
@@ -328,6 +331,7 @@ void Machine::expand_read_katoms(
std::for_each(begin, end, mark_as_replaced);
input_filename = fs::canonical(input_filename);
m_state.add_search_dir(input_filename.parent_path().string());
std::string text = trim_right(string_from_file(input_filename.string()));
katom_list ks = katomize(line_split(text), input_filename);
// ks =

View File

@@ -259,6 +259,16 @@ std::vector<std::string> State::all_names()
}
void State::add_search_dir(const std::string& dir)
{
if (!dir.empty()
&& std::find(m_search_dirs.begin(), m_search_dirs.end(), dir)
== m_search_dirs.end()) {
m_search_dirs.push_back(dir);
}
}
std::string State::python_code()
{
(void)K::log(3);
@@ -267,7 +277,12 @@ std::string State::python_code()
std::stringstream ss {};
ss << "import sys\n";
for (auto d : sks_dirs()) {
strings_t python_dirs = sks_dirs();
// The directories of the files this Machine has read: a module next to
// the file whose @eval names it is found regardless of the cwd.
python_dirs.insert(python_dirs.end(),
m_search_dirs.begin(), m_search_dirs.end());
for (auto d : python_dirs) {
auto python_files = pathnames_with_extension(d, "py");
if (!python_files.empty()) {
ss << "sys.path.append('" << d << "')\n";

View File

@@ -77,10 +77,16 @@ public:
void parse_state_katoms(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, katom_list katoms);
std::vector<std::string> all_names();
void add_search_dir(const std::string& dir);
std::string python_code();
std::string describe(bool show_environment=false, int margin_size=2) const;
std::vector<Frame> m_frames {};
// Directories of the files the Machine has read (input files, klammer
// sets, @read targets), in reading order: @eval finds Python modules
// and :cpp libraries next to the file that uses them (see python_code()
// and Eval::eval_command).
std::vector<std::string> m_search_dirs {};
// @@@state Image_search_path :set :append :replace :argtype :desc
// Parameter_set m_parameters = Parameter_set("name :set :append :replace :argtype :delim :desc");
Parameter_set m_parameters = Parameter_set("name :value :append :replace :delim :desc");