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);