Files
klammertext/sks/document/document_class.cpp
T

372 lines
14 KiB
C++
Raw Normal View History

#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.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.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];
const Resolved_font* rf = nullptr;
for (const auto& f : m_resolved_extra)
if (f.family_name == 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();
}
/*
void output_msg(const std::string& msg)
{
std::cout << red;
(void)K::log(1, msg);
std::cout << black;
}
void Document_class::create_directory(const std::string& directory)
{
if (write_files) {
fs::create_directories(directory);
} else {
output_msg("Output directory: " + directory);
}
}
*/