2026-07-18 18:48:23 +02:00
|
|
|
#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"
|
|
|
|
|
|
2026-07-30 23:50:07 +02:00
|
|
|
bool strbool(const std::string& s, const Locator& loc)
|
2026-07-18 18:48:23 +02:00
|
|
|
{
|
|
|
|
|
std::vector<std::string> values = {"false", "False", "0", "true", "True", "1"};
|
|
|
|
|
if (is_not_in(s, values)) {
|
|
|
|
|
std::stringstream ss {};
|
2026-08-23 20:48:26 +02:00
|
|
|
ss << "The value \"" << s << "\" is not a Boolean value. Possible values are:\n "
|
2026-07-18 18:48:23 +02:00
|
|
|
<< join(values, ", ");
|
2026-08-23 20:48:26 +02:00
|
|
|
throw Argument_error(ss.str(), loc);
|
2026-07-18 18:48:23 +02:00
|
|
|
}
|
|
|
|
|
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");
|
2026-08-25 23:30:55 +02:00
|
|
|
// 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);
|
2026-07-18 18:48:23 +02:00
|
|
|
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");
|
2026-07-26 23:22:59 +02:00
|
|
|
// 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.
|
2026-08-23 20:48:26 +02:00
|
|
|
// 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"));
|
2026-07-18 18:48:23 +02:00
|
|
|
|
|
|
|
|
m_css_text = get("css_text");
|
2026-08-23 20:48:26 +02:00
|
|
|
m_css_filenames = resolve_filename_list(ktesc_resolve(get("css_files")), get("K_input_dir"));
|
2026-07-18 18:48:23 +02:00
|
|
|
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");
|
2026-08-23 20:48:26 +02:00
|
|
|
m_js_filenames = resolve_filename_list(ktesc_resolve(get("js_files")), get("K_input_dir"));
|
2026-07-18 18:48:23 +02:00
|
|
|
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;
|
|
|
|
|
};
|
2026-08-25 23:30:55 +02:00
|
|
|
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);
|
2026-07-18 18:48:23 +02:00
|
|
|
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);
|
2026-07-26 23:22:59 +02:00
|
|
|
m_kt_root_filename = get("K_input_filename");
|
2026-07-18 18:48:23 +02:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-07-30 23:50:07 +02:00
|
|
|
fs::path parse_input_filename(const std::string& s, const std::string& input_dir)
|
2026-07-18 18:48:23 +02:00
|
|
|
{
|
2026-08-23 20:48:26 +02:00
|
|
|
// 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.
|
2026-07-18 18:48:23 +02:00
|
|
|
fs::path p(s);
|
2026-08-23 20:48:26 +02:00
|
|
|
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.string());
|
|
|
|
|
return in_kt;
|
|
|
|
|
}
|
2026-07-18 18:48:23 +02:00
|
|
|
if (p.extension() != ".kt") {
|
|
|
|
|
p += ".kt";
|
|
|
|
|
}
|
2026-07-26 23:22:59 +02:00
|
|
|
if (p.is_absolute()) {
|
|
|
|
|
return p;
|
|
|
|
|
}
|
|
|
|
|
fs::path in_input_dir = fs::path(input_dir) / p;
|
2026-08-23 20:48:26 +02:00
|
|
|
(void)K::log(1, "Input file \"" + s + "\": " + in_input_dir.string());
|
|
|
|
|
return in_input_dir;
|
2026-07-18 18:48:23 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-30 23:50:07 +02:00
|
|
|
void Document_class::write(const std::string& filename, const std::string& contents)
|
2026-07-18 18:48:23 +02:00
|
|
|
{
|
|
|
|
|
write_file(filename, contents, write_files);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-07-30 23:50:07 +02:00
|
|
|
void write_file(const std::string& filename, const std::string& contents, bool write_p)
|
2026-07-18 18:48:23 +02:00
|
|
|
{
|
|
|
|
|
if (write_p) {
|
|
|
|
|
string_to_file(filename, contents);
|
|
|
|
|
} else {
|
2026-08-16 01:37:59 +02:00
|
|
|
// 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);
|
2026-07-18 18:48:23 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 23:30:55 +02:00
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
// 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.
|
2026-09-12 18:11:48 +02:00
|
|
|
// 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.
|
2026-08-25 23:30:55 +02:00
|
|
|
bool seen = false;
|
|
|
|
|
for (const auto& rf : m_resolved_extra)
|
2026-09-12 18:11:48 +02:00
|
|
|
if (rf.dir_name == name_to_dirname(name)) seen = true;
|
2026-08-25 23:30:55 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-12 18:11:48 +02:00
|
|
|
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)
|
2026-08-25 23:30:55 +02:00
|
|
|
{
|
|
|
|
|
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);
|
2026-09-12 18:11:48 +02:00
|
|
|
result += resolved_typeface(name).family_name;
|
2026-08-25 23:30:55 +02:00
|
|
|
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];
|
2026-09-18 20:02:44 +02:00
|
|
|
// 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).
|
2026-08-25 23:30:55 +02:00
|
|
|
const Resolved_font* rf = nullptr;
|
|
|
|
|
for (const auto& f : m_resolved_extra)
|
2026-09-18 20:02:44 +02:00
|
|
|
if (f.dir_name == name_to_dirname(name)) rf = &f;
|
2026-08-25 23:30:55 +02:00
|
|
|
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();
|
|
|
|
|
}
|