Assembled from klammertext-dev f344a37d0f6e. The commands, the library,
the Standard Klammer Set, the reflection klammerset and the Python
binding build and run natively on Windows (MSYS2 UCRT64 gcc, python.org
Python), and both test tiers pass there. doc/install/windows_source_install.md
is the fifth install guide: a source install parallel to the Linux and
macOS ones, verified by following it on a pristine Windows 11.
Behaviour on every system: paths Klammertext prints use forward slashes;
output is LF; the command line is UTF-8 on Windows; @eval :shell is a
POSIX shell command everywhere -- on Windows it runs the sh Klammertext
ships, BusyBox for Windows (win/busybox/, GPLv2, with its licence and the
source pointer). KLAMMERTEXT_FONTS and KLAMMERTEXT_KLAMMERSETS are lists in
the system's PATH convention (';' on Windows). On Linux and macOS nothing
observable changes.
801 lines
26 KiB
C++
801 lines
26 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().generic_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).generic_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;
|
|
}
|
|
|
|
std::string home_directory()
|
|
{
|
|
if (const char* home = std::getenv("HOME"); home && *home) {
|
|
return home;
|
|
}
|
|
#ifdef _WIN32
|
|
if (const char* profile = std::getenv("USERPROFILE"); profile && *profile) {
|
|
return profile;
|
|
}
|
|
#endif
|
|
return {};
|
|
}
|
|
|
|
std::string find_in_path(const std::string& program)
|
|
{
|
|
const char* path = std::getenv("PATH");
|
|
if (path == nullptr) {
|
|
return {};
|
|
}
|
|
std::stringstream ss(path);
|
|
std::string dir;
|
|
while (std::getline(ss, dir, path_list_separator)) {
|
|
if (dir.empty()) {
|
|
continue;
|
|
}
|
|
fs::path candidate = fs::path(dir) / program;
|
|
std::error_code ec {};
|
|
if (fs::is_regular_file(candidate, ec)) {
|
|
return candidate.generic_string();
|
|
}
|
|
#ifdef _WIN32
|
|
fs::path exe = candidate;
|
|
exe += ".exe";
|
|
if (fs::is_regular_file(exe, ec)) {
|
|
return exe.generic_string();
|
|
}
|
|
#endif
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::vector<std::string> directory_list(const char* variable, const std::string& default_subdir)
|
|
{
|
|
std::string paths {};
|
|
if (const char* env = std::getenv(variable); env && *env) {
|
|
paths = env;
|
|
} else if (std::string home = home_directory(); !home.empty()) {
|
|
paths = home + "/" + default_subdir;
|
|
}
|
|
std::vector<std::string> result {};
|
|
std::stringstream ss(paths);
|
|
std::string dir;
|
|
while (std::getline(ss, dir, path_list_separator)) {
|
|
if (!dir.empty()) {
|
|
result.push_back(dir);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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).string()));
|
|
// Binary: the text Klammertext writes ends its lines with LF on every
|
|
// system; Windows' text mode would turn each into CRLF.
|
|
std::ofstream out(pathname, std::ios::binary);
|
|
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().string());
|
|
}
|
|
}
|
|
} 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 symbolic_klammertext_filename(std::string filename)
|
|
{
|
|
// Both in the generic spelling: a Windows KLAMMERTEXT_HOME may be written
|
|
// with either separator, and a path Klammertext prints uses '/'.
|
|
return string_replace(
|
|
fs::path(filename).generic_string(),
|
|
fs::path(get_env_var("KLAMMERTEXT_HOME")).generic_string(), "$KLAMMERTEXT_HOME");
|
|
}
|
|
|
|
|
|
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};
|
|
}
|