#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 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______ : 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 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 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(); }