Files
klammertext/sks/document/document_class.cpp
T
ack 830b19394d Klammertext runs natively on Windows; a Windows source-install guide; the bundled sh for @eval :shell
Assembled from klammertext-dev f344a37d772c. 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.
2026-09-25 00:33:26 +02:00

359 lines
14 KiB
C++

#include "util.h"
#include "machine.h"
#include "error.h"
#include "kutil.h"
#include "html_util.h"
#include "latex_util.h"
#include "locator.h"
// #include "state.h"
#include "document_class.h"
#include "show.h"
#include "log.h"
#include "file.h"
bool strbool(const std::string& s, const Locator& loc)
{
std::vector<std::string> values = {"false", "False", "0", "true", "True", "1"};
if (is_not_in(s, values)) {
std::stringstream ss {};
ss << "The value \"" << s << "\" is not a Boolean value. Possible values are:\n "
<< join(values, ", ");
throw Argument_error(ss.str(), loc);
}
bool result = (find(values.begin(), values.end(), s) - values.begin()) > 2;
return result;
}
Document_class::Document_class(Machine& machine) : Klammer_base(machine)
{
(void)K::log(3);
// show("document in main SKS");
m_machine = machine;
sloc = get("K_loc");
// The klammer's own position, so an argument error points at the
// @document application rather than at document_class.cpp: a
// default-constructed Locator captures the C++ source location.
loc = Locator::from_string(sloc);
m_title = get("title");
m_subtitle = get("subtitle");
m_page_title = get("page_title");
m_author = get("author");
m_date = get("date");
// m_nav = strbool(get("nav"), loc);
// m_toc = strbool(get("toc"), loc);
m_structure = docstruct(get("structure"));
m_date = get("date");
m_version = get("version");
m_logo = get("logo");
m_text = get("text");
// Filename lists: standalone-"/" separation, existence rescue for
// spaces, ~ expansion (resolve_filename_list in mac/file.cpp); the
// existence checks resolve relative names against the input directory,
// as parse_input_filename() will.
// Filename-bearing values arrive ESCAPED for the target (the state
// stores escaped values so they flow correctly into output); a filename
// is programmatic use, so decode the markers first -- the C++ mirror of
// the Python-side unescape_ktesc() rule. Found 2026-08-22: under tex,
// ":files my_chapter" searched for "myKTESC005fKTESCchapter".
m_files = resolve_filename_list(ktesc_resolve(get("files")), get("K_input_dir"));
m_css_text = get("css_text");
m_css_filenames = resolve_filename_list(ktesc_resolve(get("css_files")), get("K_input_dir"));
m_include_sks_css = strbool(get("include_sks_css"), loc);
frame_background_color = get("frame_background_color");
frame_text_color = get("frame_text_color");
nav_background_color = get("nav_background_color");
nav_text_color = get("nav_text_color");
js_text = get("js_text");
m_js_filenames = resolve_filename_list(ktesc_resolve(get("js_files")), get("K_input_dir"));
m_include_sks_js = strbool(get("include_sks_js"), loc);
// font_dirs = word_split(get("font_dirs"));
auto strip_quotes = [](std::string s) {
if (s.size() >= 2 && s.front() == '"' && s.back() == '"')
s = s.substr(1, s.size() - 2);
return s;
};
m_serif_typeface = strip_quotes(get("serif_typeface"));
m_sans_typeface = strip_quotes(get("sans_typeface"));
m_mono_typeface = strip_quotes(get("mono_typeface"));
m_text_scale = stof(get("text_scale"));
if (!m_serif_typeface.empty())
m_resolved_serif = resolve_font(m_serif_typeface, loc);
if (!m_sans_typeface.empty())
m_resolved_sans = resolve_font(m_sans_typeface, loc);
if (!m_mono_typeface.empty())
m_resolved_mono = resolve_font(m_mono_typeface, loc);
paper_size = get("paper_size");
landscape = strbool(get("landscape"), loc);
leading = stof(get("leading"));
point_size = stoi(get("point_size"));
ragged_right = strbool(get("ragged_right"), loc);
cover = get("cover");
prolog = get("prolog");
m_two_column = strbool(get("two_column"), loc);
m_copyright = get("copyright");
m_bottom = get("bottom");
create_output_directory = strbool(get("create_output_directory"), loc);
output_directory_name = get("output_directory_name");
resources_in_file = strbool(get("resources_in_file"), loc);
//use_pages_dir = strbool(get("use_pages_dir"), loc);
// m_toc_only = strbool(get("K_toc_only"), loc);
m_kt_root_filename = get("K_input_filename");
m_no_cache = !strbool(get("cache"), loc);
write_files = !strbool(get("K_stdout_only"), loc);
m_input_dir = get("K_input_dir");
if (m_text.size() == 0 and m_files.size() == 0) {
throw Argument_error(
"Neither the :text or :files options have values"); //,
//get("K_loc"));
}
/*
for (std::string file : m_files) {
sources.push_back(string_from_file(find_kt_file(file)));
}
*/
// std::cout << "IN DOCUMENT CLASS:\n" << m_state << "\n";
}
void Document_class::save_string_input_as_file()
{
if (!m_text.empty()) {
std::string input_text_filename = m_cache_dir + "/_text.kt";
// msg() << "Write string input to cache file: "
// << input_text_filename << "\n";
string_to_file(input_text_filename, m_text);
m_files.insert(m_files.begin(), input_text_filename);
}
}
fs::path parse_input_filename(const std::string& s, const std::string& input_dir)
{
// The two-class rule (2026-08-22), replacing a three-stage search whose
// second stage could quietly shadow a file beside the document:
//
// * a BARE WORD -- no directory separator, no extension -- is the kt/
// SHORTCUT: ":files X" MEANS kt/X.kt in the root file's directory,
// and nothing else. Missing is an immediate error whose message
// teaches the convention, not a fallback. The kt/ directory is the
// conventional home for a document's input files, and putting the
// meaning entirely in the name lets several root documents share it.
//
// * anything else is a real PATHNAME, absolute or resolved against the
// input file's directory (K_input_dir), so a document renders
// identically wherever ktext is run from.
//
// Which file a name landed on is a DERIVED value, so "-v 1" reports it.
fs::path p(s);
bool bare = s.find('/') == std::string::npos && p.extension().empty();
if (bare) {
fs::path in_kt = fs::path(input_dir) / "kt" / (s + ".kt");
if (!file_exists(in_kt.string())) {
throw Argument_error(
"The \":files\" name \"" + s + "\" is a bare word, which by "
"convention means the file kt/" + s + ".kt in the root "
"document's directory:\n"
" " + in_kt.string() + "\n"
"That file does not exist. Create it there, or write a real "
"pathname (a name with a directory or the \".kt\" extension) "
"to use a file elsewhere.",
Locator::none());
}
// The resolved path itself shows kt/ -- no label needed.
(void)K::log(1, "Input file \"" + s + "\": " + in_kt.generic_string());
return in_kt;
}
if (p.extension() != ".kt") {
p += ".kt";
}
if (p.is_absolute()) {
return p;
}
fs::path in_input_dir = fs::path(input_dir) / p;
(void)K::log(1, "Input file \"" + s + "\": " + in_input_dir.generic_string());
return in_input_dir;
}
void Document_class::write(const std::string& filename, const std::string& contents)
{
write_file(filename, contents, write_files);
}
void write_file(const std::string& filename, const std::string& contents, bool write_p)
{
if (write_p) {
string_to_file(filename, contents);
} else {
// Not writing: name the file that would have been written. A
// derived value, so "-v 1" -- and never on stdout, which carries
// the document itself.
(void)K::log(1, "Output filename:", filename);
}
}
// ---------------------------------------------------------------------
// Typeface markers (@typeface; see the marker note in sks/font/font.py)
//
// Hand-rolled scanning, no std::regex: these run over the whole assembled
// document and libstdc++'s regex executor recurses per character (the
// documented SIGSEGV hazard).
// ---------------------------------------------------------------------
namespace {
const std::string typeface_tag { "__TYPEFACE__" };
// The next marker at or after `from`. Returns false when there is none.
// A marker is __TYPEFACE__<name>__<scale>__ : the two fields are taken up
// to the next "__", so neither may contain one (a typeface name with a
// double underscore is not a thing, and the scale is a number).
bool next_typeface_marker(const std::string& text, size_t from,
size_t& start, size_t& end,
std::string& name, std::string& scale)
{
start = text.find(typeface_tag, from);
if (start == std::string::npos) return false;
size_t name_at = start + typeface_tag.size();
size_t name_end = text.find("__", name_at);
if (name_end == std::string::npos) return false;
size_t scale_at = name_end + 2;
size_t scale_end = text.find("__", scale_at);
if (scale_end == std::string::npos) return false;
name = text.substr(name_at, name_end - name_at);
scale = text.substr(scale_at, scale_end - scale_at);
end = scale_end + 2;
return true;
}
// The fontspec command name for the nth distinct (typeface, scale) pair.
// A TeX command name may hold only letters, and a typeface name may hold
// anything -- so the pairs are NUMBERED rather than transliterated, and
// the number is turned into letters. Nothing outside this file needs to
// know the rule: the klammer leaves a marker and this replaces it, so
// there is no second implementation to keep in step.
std::string typeface_command(size_t index)
{
std::string suffix {};
do {
suffix = char('a' + (index % 26)) + suffix;
index /= 26;
} while (index > 0);
return "\\ktface" + suffix;
}
} // namespace
void Document_class::collect_typefaces(const std::string& text)
{
size_t pos = 0, start = 0, end = 0;
std::string name {}, scale {};
while (next_typeface_marker(text, pos, start, end, name, scale)) {
std::pair<std::string, std::string> entry { name, scale };
if (std::find(m_extra_typefaces.begin(), m_extra_typefaces.end(), entry)
== m_extra_typefaces.end()) {
m_extra_typefaces.push_back(entry);
// Resolve ONCE per distinct typeface: resolve_font() throws a
// located error naming the available typefaces if the store
// does not have it, which is the whole error path for a
// misspelled or uninstalled typeface.
// Compared by directory name: a @typeface may write either
// the family name or the store's directory name, and both
// resolve to the same directory.
bool seen = false;
for (const auto& rf : m_resolved_extra)
if (rf.dir_name == name_to_dirname(name)) seen = true;
if (!seen) {
// What a @typeface klammer asked for, and what the store
// resolved it to: a decision the user cannot read off
// their own input, so "-v 1".
Resolved_font rf = resolve_font(name, loc);
(void)K::log(1, "Typeface \"" + name + "\" resolved to:",
rf.font_dir);
m_resolved_extra.push_back(rf);
}
}
pos = end;
}
}
const Resolved_font& Document_class::resolved_typeface(const std::string& name) const
{
for (const auto& rf : m_resolved_extra)
if (rf.dir_name == name_to_dirname(name)) return rf;
// collect_typefaces() resolves every marker before this is asked.
throw Internal_error("Typeface \"" + name + "\" was not resolved");
}
std::string Document_class::resolve_typeface_families(const std::string& text)
{
std::string result {};
size_t pos = 0, start = 0, end = 0;
std::string name {}, scale {};
while (next_typeface_marker(text, pos, start, end, name, scale)) {
result += text.substr(pos, start - pos);
result += resolved_typeface(name).family_name;
pos = end;
}
result += text.substr(pos);
return result;
}
std::string Document_class::resolve_typeface_markers(const std::string& text)
{
std::string result {};
size_t pos = 0, start = 0, end = 0;
std::string name {}, scale {};
while (next_typeface_marker(text, pos, start, end, name, scale)) {
result += text.substr(pos, start - pos);
auto entry = std::make_pair(name, scale);
auto it = std::find(m_extra_typefaces.begin(), m_extra_typefaces.end(), entry);
if (it != m_extra_typefaces.end())
result += typeface_command(it - m_extra_typefaces.begin());
pos = end;
}
result += text.substr(pos);
return result;
}
std::string Document_class::typeface_preamble()
{
std::stringstream ss {};
for (size_t i = 0; i < m_extra_typefaces.size(); i++) {
const auto& [name, scale] = m_extra_typefaces[i];
// Compared by directory name, as collect_typefaces() compares: a
// @typeface may write the family name or the store's directory
// name, and family_name holds what the css declares (2026-09-12),
// so a name-for-name comparison missed the directory spelling and
// left \ktface<n> undefined in the .tex (found 2026-09-17).
const Resolved_font* rf = nullptr;
for (const auto& f : m_resolved_extra)
if (f.dir_name == name_to_dirname(name)) rf = &f;
if (!rf) continue;
// \newfontfamily declares a switch command, unlike \setmainfont,
// which changes the document's font permanently.
ss << latex::font_command("newfontfamily" + typeface_command(i),
*rf, stof(scale));
}
return ss.str();
}