Typographic transforms (---, quote pairs, ~) no longer touch verbatim text: @c/@code/@source_listing content and ^'...'^ spans show exactly the characters written. "^" before any punctuation character quotes it in every target (the apostrophe excepted: ^' opens a literal span), with the new :resolve option on @@@target declaring per-target renderings. The ^UUUU^ code-point form accepts 4-6 hex digits, the full Unicode range. The html output and transform spellings are polyglot (XML-valid), in preparation for an EPUB target. New suites: transform_test, character_test (engine), typography_test (SKS). (from dev 07ce5ea86a0a)
700 lines
24 KiB
C++
700 lines
24 KiB
C++
#include <fstream>
|
|
#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);
|
|
}
|