Files
klammertext/mac/file.cpp
T
ack e4f1307aa9 Characters that killed a render, what may sit next to a klammer, and ref/
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)
2026-09-07 22:37:05 +02:00

729 lines
24 KiB
C++

#include <fstream>
#include <sstream>
#include <algorithm>
#include <cstring>
#include "file.h"
#include "error.h"
#include "log.h"
#include "util.h"
#include "show.h"
std::string file_basename(const std::string& filename)
{
fs::path p(filename);
return p.stem().string();
}
std::string extension(const std::string& filename)
{
auto pos = filename.find_last_of(".");
if (pos != std::string::npos)
return filename.substr(pos + 1);
return "";
}
std::string file_directory(const std::string& filename)
{
fs::path p(filename);
return p.parent_path().string();
}
std::string absolute_pathname(const std::string& filename, const std::string& base)
{
fs::path p(filename);
if (filename.empty()) {
return fs::current_path().string();
}
if (!base.empty()) {
// Resolve filename relative to base's directory
fs::path b(base);
fs::path base_dir = fs::is_directory(b) ? b : b.parent_path();
p = base_dir / p;
}
return fs::absolute(p).string();
}
std::string relative_pathname(const std::string& filename)
{
/*
fs::path relative_to_current(const fs::path& input,
const fs::path& currentFile) {
const auto base = fs::absolute(currentFile).parent_path();
return fs::relative(fs::absolute(input), base);
*/
fs::path p(absolute_pathname(filename)); // + "/" + filename);
auto relpath = fs::relative(p, fs::current_path());
std::cout << "Absolute: " << fs::current_path() << " " << p << "->" << relpath << "\n";
return relpath.string();
}
size_t count_substrings(const std::string& text, const std::string& substring) {
size_t count = 0;
size_t pos = 0;
while ((pos = text.find(substring, pos)) != std::string::npos) {
count++;
pos += substring.length();
}
return count;
}
fs::path relative_to_cwd(const fs::path& input)
{
const auto base = fs::current_path();
std::error_code ec;
auto abs_input = fs::weakly_canonical(input, ec);
if (ec) abs_input = fs::absolute(input);
auto rel = fs::relative(abs_input, base, ec);
if (ec) rel = abs_input.lexically_relative(base);
auto result = rel.empty() ? abs_input : rel;
if (count_substrings(result.string(), "../") > 3) {
result = input;
}
return result;
}
std::string relative_pathname(const std::string& filename, std::string base)
{
fs::path p(absolute_pathname(filename));
auto relpath = relative(p, base);
relpath = relpath.lexically_normal();
return relpath.string();
}
bool file_exists(const std::string& pathname, bool error_if_not, bool is_directory)
{
fs::path p(pathname);
bool exists = fs::exists(p);
bool regular = fs::is_regular_file(p);
bool directory = fs::is_directory(p);
bool valid = exists and (regular or directory);
std::string filetype = is_directory ? "Directory " : "File";
Locator no_source("", -1, -1);
if (error_if_not and not valid) {
if (not exists)
throw File_error(filetype + " '" + pathname + "' does not exist", no_source);
else if (not is_directory and not regular)
throw File_error(filetype + " '" + pathname + "' is not a regular text file", no_source);
else if (is_directory and regular)
throw File_error(filetype + " '" + pathname + "' is not a directory", no_source);
}
return valid;
}
// Filename lists (see file.h): standalone-"/" separation, existence-rescue
// grouping, and tilde expansion.
std::string expand_tilde(const std::string& path)
{
if (path == "~" || (path.size() > 1 && path[0] == '~' && path[1] == '/')) {
std::string home = get_env_var("HOME");
if (!home.empty()) {
return home + path.substr(1);
}
}
return path;
}
static bool filename_exists(const std::string& name, const std::string& base_dir)
{
std::string resolved = expand_tilde(name);
if (!base_dir.empty() && !fs::path(resolved).is_absolute()) {
resolved = base_dir + "/" + resolved;
}
return fs::is_regular_file(resolved);
}
strings_t group_filename_tokens(const strings_t& tokens, const std::string& base_dir)
{
strings_t result {};
if (std::find(tokens.begin(), tokens.end(), "/") != tokens.end()) {
// Deterministic form: standalone "/" separates the filenames; the
// tokens between separators form one name. No existence checks.
strings_t group {};
for (const std::string& t : tokens) {
if (t == "/") {
if (!group.empty()) result.push_back(join(group));
group.clear();
} else {
group.push_back(t);
}
}
if (!group.empty()) result.push_back(join(group));
} else {
// Rescue: a token naming an existing file stands alone; one that
// does not is joined with following tokens until the accumulated
// 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;
while (i < tokens.size()) {
if (filename_exists(tokens[i], base_dir)) {
result.push_back(tokens[i]);
++i;
continue;
}
std::string acc = tokens[i];
size_t j = i + 1;
bool found = false;
while (j < tokens.size()) {
acc += " " + tokens[j];
++j;
if (filename_exists(acc, base_dir)) {
// A successful regrouping is a DERIVED value -- the
// command completed a name the input left ambiguous -- so
// it is reported at "-v 1" and not at every run. Silence
// is safe here because a rescue only succeeds when the
// joined name names an existing file: a mistyped name
// fails rather than resolving to something else.
K::log(1, "interpreting", q_(acc), "as one filename");
result.push_back(acc);
i = j;
found = true;
break;
}
}
if (!found) {
// 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;
}
}
}
for (std::string& name : result) {
name = expand_tilde(name);
}
return result;
}
strings_t resolve_filename_list(const std::string& text, const std::string& base_dir)
{
// Split on standalone "/" at the string level first, so a name's inner
// spacing survives exactly; without a separator, fall back to
// whitespace tokens and the rescue in group_filename_tokens().
auto standalone_slash = [&](size_t i) {
return text[i] == '/'
&& (i == 0 || std::isspace(static_cast<unsigned char>(text[i-1])))
&& (i + 1 == text.size() || std::isspace(static_cast<unsigned char>(text[i+1])));
};
bool has_separator = false;
for (size_t i = 0; i < text.size(); ++i) {
if (standalone_slash(i)) {
has_separator = true;
break;
}
}
if (has_separator) {
strings_t result {};
std::string part {};
for (size_t i = 0; i < text.size(); ++i) {
if (standalone_slash(i)) {
part = trim(part);
if (!part.empty()) result.push_back(expand_tilde(part));
part.clear();
} else {
part += text[i];
}
}
part = trim(part);
if (!part.empty()) result.push_back(expand_tilde(part));
return result;
}
return group_filename_tokens(word_split(text), base_dir);
}
std::string string_from_file(const std::string& pathname, bool strip_surrounding_whitespace)
{
std::regex klammertext_filename_re { R"(.*\.kt?)" };
fs::path p(pathname);
std::string result {};
if (fs::exists(p)) {
if (fs::is_regular_file(p)) {
std::ifstream stream { pathname };
if (!stream.is_open()) {
throw File_error("Error opening file \"" + pathname + "\"", Locator("", -1, -1));
} else {
std::ostringstream buffer {};
stream >> std::noskipws >> buffer.rdbuf();
if (stream.fail() && !stream.eof()) {
throw File_error("Error reading file \"" + pathname + "\"", Locator("", -1, -1));
} else {
result = buffer.str();
result = string_replace(result, "\r\n", ""); // Urgh.
if (std::regex_match(pathname, klammertext_filename_re)) {
//std::cout << "Read Klammertext source file: " << pathname << "\n";
// result = encode(result);
} else {
//std::cout << "Read file: " << pathname << "\n";
}
if (strip_surrounding_whitespace)
result = trim(result);
return result;
}
}
} else {
throw File_error("File \"" + pathname + "\" is not a regular text file", Locator("", -1, -1));
}
} else {
throw File_error("File '" + pathname + "' does not exist", Locator("", -1, -1));
}
return result;
}
void string_to_file(const std::string& pathname, std::string contents)
{
fs::create_directories(file_directory(fs::absolute(pathname)));
std::ofstream out(pathname);
if (!out) {
throw File_error("Could not write file " + pathname);
}
out << contents;
out.close();
}
strings_t get_subdirectories(const std::string& s, std::regex name_match_re)
{
strings_t result {};
for (auto& p : fs::recursive_directory_iterator(s)) {
std::smatch match {};
//std::cout << "Check dir: " << p.path().string() << "\n";
std::string base = file_basename(p.path().string());
if (fs::is_directory(p) and std::regex_match(base, match, name_match_re))
result.push_back(p.path().string());
}
return result;
}
strings_t get_files_in_directory(const std::string& dir)
{
strings_t result {};
try {
for (const auto& entry : fs::directory_iterator(dir)) {
if (entry.is_regular_file()) {
// std::cout << entry.path().filename() << std::endl;
result.push_back(entry.path().filename());
}
}
} catch (const fs::filesystem_error& ex) {
std::cerr << "Error: " << ex.what() << std::endl;
}
return result;
}
std::string find_file(const std::string& basename, strings_t search_path, bool error_if_not_found)
{
bool found = false;
std::string pathname { "" };
for (const std::string& s : search_path) {
pathname = s + "/" + basename;
if (file_exists(pathname)) {
found = true;
break;
}
}
if (error_if_not_found and not found) {
std::stringstream ss {};
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());
}
return pathname;
}
std::vector<fs::path>
find_file_recursive(const fs::path& root, const std::string& filename, bool only_one) //, Locator loc)
{
std::vector<fs::path> result {};
if (!fs::exists(root) || !fs::is_directory(root)) {
return result;
}
for (const auto& entry : fs::recursive_directory_iterator(root)) {
// msg() << "Entry: " << entry.path().string() << "\n";
if (entry.is_regular_file() && entry.path().filename() == filename) {
result.push_back(entry.path());
}
}
if (only_one && result.size() > 1) {
std::stringstream ss {};
ss << "More than one file named " + q_(filename) + " found:\n";
for (auto f : result) {
ss << " " << f << "\n";
}
std::cerr << ss.str();
throw File_error("More than one file named " + q_(filename) + " found");
}
return result;
}
std::vector<fs::path>
find_file_from_roots(const std::vector<std::string>& roots, const std::string& filename, bool only_one)
{
(void)K::log(3, filename);
std::vector<fs::path> result {};
for (std::string root : roots) {
std::vector<fs::path> filenames = find_file_recursive(root, filename, only_one);
for (auto f : filenames) {
if (std::ranges::count(result, f) == 0) {
result.push_back(f);
}
}
// result.insert(result.end(), filenames.begin(), filenames.end());
}
if (result.empty()) {
throw File_error("File \"" + filename + "\" not found");
}
if (only_one && result.size() > 1) {
std::stringstream ss {};
ss << "More than one file named " + q_(filename) + " found:\n";
for (auto f : result) {
ss << " " << f << "\n";
}
std::cerr << ss.str();
throw File_error("More than one file named " + q_(filename) + " found");
}
for (auto f : result) {
if (!fs::exists(f)) {
throw File_error("File \"" + filename + "\" does not exist");
}
}
return result;
}
fs::path klammertext_filename(const std::string& basename, bool error_if_missing, bool make_directory_if_missing)
{
std::string home { get_env_var("KLAMMERTEXT_HOME") };
std::string result = home + "/" + basename;
if (make_directory_if_missing)
fs::create_directory(file_directory(result));
if (error_if_missing and !file_exists(result, false, true)) {
throw File_error(
"Klammertext file does not exist: " + result);
}
return fs::path(result);
}
strings_t sks_dirs()
{
std::string home { get_env_var("KLAMMERTEXT_HOME") };
strings_t result = get_subdirectories(home + "/sks", std::regex(R"([a-zA-Z]\w*)"));
result.emplace(result.begin(), home + "/sks/kutil");
// result.push_back(home + "/doc/handbook"); // Not included in container yet
return result;
}
strings_t get_sks_directories(const std::string& s, bool include_argument)
{
strings_t result;
if (include_argument)
result.push_back(s);
for (auto& p : fs::recursive_directory_iterator(s)) {
auto basename = file_basename(p.path().string());
auto parent = p.path().parent_path();
if (fs::is_directory(p)
and basename[0] != '_'
and basename != "css"
and basename != "sty"
and basename != "js"
//and basename != "font"
and parent != "font"
and parent != "fonts")
result.push_back(p.path().string());
}
return result;
}
std::string cache_directory(const std::string& subdirectory, std::string parent_directory)
{
if (parent_directory.empty()) {
// /dev/shm is a fast RAM-backed tmpfs on Linux; it does not exist on
// macOS, so fall back to the platform temp directory there.
if (fs::is_directory("/dev/shm")) {
parent_directory = "/dev/shm";
} else {
parent_directory = fs::temp_directory_path().string();
}
}
std::string result = parent_directory + "/_klammertext_cache/" + subdirectory;
// msg() << "Cache directory: " << result << "\n";
return result;
}
std::time_t to_time_t(const fs::file_time_type& ftime)
{
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
return std::chrono::system_clock::to_time_t(sctp);
}
bool in_modification_order(std::string filename1, std::string filename2)
{
if ((!file_exists(filename1)) || (!file_exists(filename2))) {
return false;
} else {
auto time1 = fs::last_write_time(fs::path(filename1));
auto time2 = fs::last_write_time(fs::path(filename2));
return time1 < time2;
}
}
void write_to_cache(const std::string& cache_dir, const std::string& basename, const std::string& text)
{
if (!file_exists(cache_dir)) {
//std::cout << "Creating cache directory: " << cache_dir << "\n";
fs::create_directories(cache_dir);
}
// msg() << "Writing file to cache: " << basename << "\n";
std::string output_filename = cache_dir + "/" + basename;
string_to_file(output_filename, text);
}
std::string read_from_cache(const std::string& cache_dir, const std::string& basename)
{
std::string input_filename = cache_dir + "/" + basename;
// msg() << "Reading file from cache: " << input_filename << "\n";
return string_from_file(input_filename);
}
bool cache_requires_update(const std::string& cache_dir, const std::string& file_to_cache, const std::string& basename)
{
std::string cache_filename = cache_dir + "/" + basename;
return !in_modification_order(file_to_cache, cache_filename);
}
std::vector<fs::path> pathnames_with_extension(
const fs::path& dir, const std::string extension)
{
std::vector<fs::path> files;
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
if (!entry.is_regular_file()) continue;
auto ext = entry.path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (ext == "." + extension) files.push_back(entry.path());
}
return files;
}
/*
std::string find_file(const fs::path& root, const std::string& name)
{
//std::vector<fs::path> matches;
strings_t matches {};
auto normalize = [&](std::string s) {
if (!case_insensitive) return s;
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
return s;
};
const std::string norm_ext = normalize("." + ext);
for (const auto& entry : fs::recursive_directory_iterator(root)) {
if (!entry.is_regular_file())
continue;
//std::string entry_ext = normalize(entry.path().extension().string());
if (entry == name) {
matches.push_back(entry.path().string());
}
}
return matches[0];
}
*/
// std::vector<fs::path> find_files_with_extension(
strings_t find_files_with_extension(
const fs::path& root, const std::string& ext, bool case_insensitive)
{
//std::vector<fs::path> matches;
strings_t matches {};
auto normalize = [&](std::string s) {
if (!case_insensitive) return s;
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
return s;
};
const std::string norm_ext = normalize("." + ext);
for (const auto& entry : fs::recursive_directory_iterator(root)) {
if (!entry.is_regular_file())
continue;
std::string entry_ext = normalize(entry.path().extension().string());
if (entry_ext == norm_ext)
matches.push_back(entry.path().string());
}
return matches;
}
std::string combine_files(
const std::vector<std::string>& filenames,
const std::string& prolog, const std::string& epilog,
const std::function <std::string(std::string)>& processor)
{
std::string result = prolog;
for (const std::string& f : filenames) {
result += "\n/* " + file_basename(f) + " */\n";
result += string_from_file(f);
}
result += "\n" + epilog + "\n";
if (processor) {
result = processor(result);
}
return result;
}
bool files_differ(const fs::path& p1,
const fs::path& p2,
std::size_t buffer_size) // 64 KiB
{
// 1. Check existence and type
if (!fs::exists(p1) || !fs::exists(p2)) return true;
if (!fs::is_regular_file(p1) || !fs::is_regular_file(p2)) return true;
// 2. Compare sizes
auto s1 = fs::file_size(p1);
auto s2 = fs::file_size(p2);
if (s1 != s2) return true;
// 3. Open both files in binary mode
std::ifstream f1(p1, std::ios::binary);
std::ifstream f2(p2, std::ios::binary);
if (!f1 || !f2) return true; // treat I/O error as "different"
// 4. Compare in chunks
std::vector<char> buf1(buffer_size);
std::vector<char> buf2(buffer_size);
while (f1 && f2) {
f1.read(buf1.data(), buffer_size);
f2.read(buf2.data(), buffer_size);
std::streamsize r1 = f1.gcount();
std::streamsize r2 = f2.gcount();
if (r1 != r2) return true; // should not happen if sizes equal
if (r1 == 0) break; // EOF both
if (std::memcmp(buf1.data(), buf2.data(), static_cast<std::size_t>(r1)) != 0)
return true;
}
return false; // no difference found
}
// Copy a single file with an explicit binary read/write stream, forcing the
// destination to be world-readable. Deliberately NOT std::filesystem::copy_file
// or fs::copy: under Apple's `container` runtime the HTML output directory is a
// virtiofs bind mount, and libstdc++'s copy_file/copy create the destination
// with openat(O_WRONLY|O_CREAT|O_TRUNC, 0200) — a mode lacking the owner-read
// bit, which virtiofs rejects with EACCES (apple/container #1344, an OS-level
// Virtualization.framework bug), leaving a 0-byte --w------- file and aborting
// output. A stream copy creates the destination owner-readable and works
// identically on virtiofs, on in-VM filesystems, and under Docker. Used for
// every file Klammertext writes into the output tree (fonts, CSS, JS, ...).
void copy_file_stream(const fs::path& src, const fs::path& dst)
{
{
std::ifstream in(src, std::ios::binary);
if (!in)
throw File_error("Cannot read file for copy:\n " + src.string());
std::ofstream out(dst, std::ios::binary | std::ios::trunc);
if (!out)
throw File_error("Cannot create output file:\n " + dst.string());
// Guard against the empty-source failbit quirk of rdbuf insertion.
if (in.peek() != std::ifstream::traits_type::eof())
out << in.rdbuf();
out.flush();
if (!out || in.bad())
throw File_error("Failed to copy file:\n " + src.string()
+ "\n -> " + dst.string());
}
fs::permissions(dst,
fs::perms::owner_read | fs::perms::owner_write |
fs::perms::group_read | fs::perms::others_read,
fs::perm_options::replace);
}
void copy_preserving_basename(
const strings_t& filenames, const std::string& output_directory,
const std::string& link_directory)
{
fs::path outdir(output_directory + "/" + link_directory);
fs::create_directories(outdir);
for (const std::string& filename : filenames) {
fs::path pname(filename);
auto out_path = outdir / pname.filename();
// Preserve the previous copy_options::update_existing behavior: skip
// when the destination already exists and is no older than the source.
if (fs::exists(out_path) &&
fs::last_write_time(out_path) >= fs::last_write_time(pname))
continue;
copy_file_stream(pname, out_path);
}
}
fs::path resolve_relative_to(const fs::path& relative, const fs::path& base)
{
fs::path base_dir = is_directory(base) ? base : base.parent_path();
return fs::weakly_canonical(base_dir / relative);
}
strings_t line_split(const std::string& s)
{
std::istringstream is(s);
strings_t result {};
std::string line;
while (std::getline(is, line)) {
result.push_back(line);
}
result.push_back("\n");
return result;
}
std::pair<std::string, strings_t> line_split(const fs::path& pathname)
{
std::string source {};
strings_t lines {};
std::ifstream infile(pathname);
std::string line;
while (std::getline(infile, line)) {
source += line + "\n";
lines.push_back(line);
}
if (!source.empty()) {
source.pop_back();
}
return {source, lines};
}