2026-07-18 18:48:23 +02:00
|
|
|
#include "file.h"
|
|
|
|
|
#include "kutil.h"
|
|
|
|
|
#include "util.h"
|
|
|
|
|
#include "log.h"
|
|
|
|
|
#include "show.h"
|
Typed arguments, calculated tables, spans, closed-world fonts, top-level fnt/ and env/
Sync with klammertext-dev through b90b0e09:
- Argument types end to end: :python_cast values are applied (Python
@eval receives real bools/numbers/lists), argument values are
validated against their argtype patterns with the argtype's
description as the error message, argtypes can declare :default
(overridable per declaration), and parameterized type families are
supported: rest(N) casts a rest argument to an N-dimensional list
(bar-count = dimension).
- Unified indexed_range syntax (selector with parenthesized subsets,
composable mnemonic names) for table lines and spans.
- Table klammer: caption fonts fixed in both targets, :column_width /
:leading / :colsep wired, :colspan and :rowspan render (HTML
attributes; \multicolumn / \multirow), calculated cell values (:calc)
with prefix operators, display-precision semantics, :calc_format and
:decimal period|comma.
- Fonts: closed-world resolution on the Klammertext font store
(infrastructure in mac/font_store; no Google Fonts links or fetch).
Default fonts live in the top-level fnt/; additional fonts install
into KLAMMERTEXT_FONTS directories via kdesc --font (list, samples,
preview, install — classification by font metadata). CSS font family
names are quoted (digit-initial families were silently lost).
- Environment files moved from mac/env/ to the top-level env/; shell
profiles source env/runtime.env. Dead per-host variants removed.
- Container: fnt/ ships in the image; curl removed (no network use).
2026-07-22 18:17:43 +02:00
|
|
|
#include "font_store.h"
|
2026-07-18 18:48:23 +02:00
|
|
|
|
|
|
|
|
namespace latex {
|
|
|
|
|
|
|
|
|
|
std::string tex_style()
|
|
|
|
|
{
|
|
|
|
|
(void)K::log(3);
|
|
|
|
|
std::stringstream ss {};
|
|
|
|
|
for (auto filename : sks_files_of_type("sty")) {
|
|
|
|
|
//for (auto filename : find_files_with_extension(klammertext_dir() + "/sks", "sty")) {
|
|
|
|
|
// msg() << "File: " << filename << "\n";
|
|
|
|
|
ss << "\\input{" << filename << "}\n";
|
|
|
|
|
/*
|
|
|
|
|
//ss << "% " << filename << "\n\n" << read_file(filename) << "\n";
|
|
|
|
|
std::string pname = regex_split(filename, std::regex("/sks/"))[1];
|
|
|
|
|
ss << "% sks/" << pname << "\n\n" << string_from_file(filename) << "\n";
|
|
|
|
|
//ss << "\\usepackage{" << pname << "}\n";
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
std::string result = ss.str();
|
|
|
|
|
/*
|
|
|
|
|
result = std::regex_replace(result, std::regex(R"(\n\n+)"), "\n");
|
|
|
|
|
result = std::regex_replace(result, std::regex(R"(#)"), "^#");
|
|
|
|
|
//result = std::regex_replace(result, std::regex(R"(%+ *[^/].*?\n)"), "");
|
|
|
|
|
*/
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
std::string title_line(
|
|
|
|
|
std::string text, std::string font="1.3", std::string vskip="4pt",
|
|
|
|
|
bool vskip_if_missing=false)
|
|
|
|
|
{
|
|
|
|
|
std::stringstream ss {};
|
|
|
|
|
if (text.empty()) {
|
|
|
|
|
if (vskip_if_missing) {
|
|
|
|
|
ss << "\\vspace*{" << vskip << "}\n";
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ss << "{\\sfont{" << font << "}" << text << "}\\\\[" << vskip << "]\n";
|
|
|
|
|
}
|
|
|
|
|
return ss.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string default_cover(
|
|
|
|
|
std::string title, std::string subtitle,
|
|
|
|
|
std::string author, std::string date, std::string version)
|
|
|
|
|
{
|
|
|
|
|
std::string test = trim(title + subtitle + author + date + version);
|
|
|
|
|
std::stringstream ss {};
|
|
|
|
|
if (!test.empty()) {
|
|
|
|
|
ss << "\\thispagestyle{empty}\\vspace*{1in}\n";
|
|
|
|
|
ss << title_line(title, "2.0", "10pt")
|
|
|
|
|
<< title_line(subtitle, "1.7", "60pt", true)
|
|
|
|
|
<< title_line(author)
|
|
|
|
|
<< title_line(date)
|
|
|
|
|
<< title_line(version);
|
|
|
|
|
}
|
|
|
|
|
return ss.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string nonbook_title(
|
|
|
|
|
std::string title, std::string subtitle,
|
|
|
|
|
std::string author, std::string date, std::string version)
|
|
|
|
|
{
|
|
|
|
|
std::string test = trim(title + subtitle + author + date + version);
|
|
|
|
|
std::stringstream ss {};
|
|
|
|
|
if (!test.empty()) {
|
|
|
|
|
// ss << Pagestyle?
|
|
|
|
|
std::string full_title = title;
|
|
|
|
|
if (!full_title.empty() && !subtitle.empty()) {
|
|
|
|
|
full_title += " --- " + subtitle;
|
|
|
|
|
}
|
|
|
|
|
ss << title_line(full_title, "1.8", "8pt")
|
|
|
|
|
<< title_line(author)
|
|
|
|
|
<< title_line(date)
|
|
|
|
|
<< title_line(version)
|
|
|
|
|
<< "\n";
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
return ss.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string pagenumber(int n, std::string style="arabic")
|
|
|
|
|
{
|
|
|
|
|
(void)K::log(3);
|
|
|
|
|
std::stringstream ss {};
|
|
|
|
|
ss << "\\pagenumbering{" << style << "}\\setcounter{page}{" << n << "}\n";
|
|
|
|
|
return ss.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string book_verso_page(std::string copyright)
|
|
|
|
|
{
|
|
|
|
|
(void)K::log(3);
|
|
|
|
|
std::stringstream ss {};
|
|
|
|
|
ss << "\n% Verso page (page ii)\n";
|
|
|
|
|
ss << "\\newpage\\thispagestyle{empty}\n";
|
|
|
|
|
if (!copyright.empty()) {
|
|
|
|
|
ss << "\\leavevmode\\vfill\n"
|
|
|
|
|
<< copyright << "\n"
|
|
|
|
|
<< "\\vspace*{8pt}\n";
|
|
|
|
|
} else {
|
|
|
|
|
ss << "\\leavevmode\n"; // blank verso page
|
|
|
|
|
}
|
|
|
|
|
ss << "\\newpage\n";
|
|
|
|
|
return ss.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string font_command(std::string command, const Resolved_font& font, float scale)
|
|
|
|
|
{
|
|
|
|
|
std::stringstream ss {};
|
|
|
|
|
if (font.family_name.empty())
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
// Format scale option if needed
|
|
|
|
|
std::string scale_opt {};
|
|
|
|
|
if (scale > 0.0f && scale != 1.0f) {
|
|
|
|
|
char buf[16];
|
|
|
|
|
std::snprintf(buf, sizeof(buf), "%.4f", scale);
|
|
|
|
|
scale_opt = std::string(",Scale=") + buf;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!font.regular.empty()) {
|
|
|
|
|
// Use Path= to point at the .ttf files directly
|
|
|
|
|
// fontspec syntax: \setmainfont[options]{filename}
|
|
|
|
|
// Wrap in \ExplSyntaxOn to handle underscores in paths
|
|
|
|
|
ss << "\\ExplSyntaxOn\n"
|
|
|
|
|
<< "\\" << command << "[Path={" << font.font_dir << "/}"
|
|
|
|
|
<< scale_opt;
|
|
|
|
|
if (!font.bold.empty())
|
|
|
|
|
ss << ",BoldFont=" << font.bold;
|
|
|
|
|
if (!font.italic.empty())
|
|
|
|
|
ss << ",ItalicFont=" << font.italic;
|
|
|
|
|
if (!font.bold_italic.empty())
|
|
|
|
|
ss << ",BoldItalicFont=" << font.bold_italic;
|
|
|
|
|
ss << "]{" << font.regular << "}\n"
|
|
|
|
|
<< "\\ExplSyntaxOff\n";
|
|
|
|
|
} else {
|
|
|
|
|
// No .ttf files — use font name (fontspec finds by name)
|
|
|
|
|
ss << "\\" << command;
|
|
|
|
|
if (!scale_opt.empty())
|
|
|
|
|
ss << "[" << scale_opt.substr(1) << "]"; // strip leading comma
|
|
|
|
|
ss << "{" << font.family_name << "}\n";
|
|
|
|
|
}
|
|
|
|
|
return ss.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string page(std::string structure,
|
|
|
|
|
std::string title,
|
|
|
|
|
std::string subtitle,
|
|
|
|
|
std::string authors,
|
|
|
|
|
std::string date,
|
|
|
|
|
std::string version,
|
|
|
|
|
std::string copyright,
|
|
|
|
|
std::string bottom,
|
|
|
|
|
std::string prolog,
|
|
|
|
|
std::string paper_size,
|
|
|
|
|
bool two_column,
|
|
|
|
|
float leading,
|
|
|
|
|
int pointsize,
|
|
|
|
|
bool ragged_right,
|
|
|
|
|
std::string cover,
|
|
|
|
|
bool landscape,
|
|
|
|
|
std::string body,
|
|
|
|
|
Resolved_font serif_font,
|
|
|
|
|
Resolved_font sans_font,
|
|
|
|
|
Resolved_font mono_font,
|
|
|
|
|
float font_scale)
|
|
|
|
|
{
|
|
|
|
|
(void)K::log(3);
|
|
|
|
|
bool book_format = (structure == "book");
|
|
|
|
|
bool toc = (structure != "plain");
|
|
|
|
|
std::string document_class = book_format ? "book" : "article";
|
|
|
|
|
std::string page_side = book_format ? "twoside" : "oneside";
|
|
|
|
|
std::stringstream ss {};
|
|
|
|
|
ss << "\\documentclass[" << paper_size << ","
|
|
|
|
|
<< pointsize << "pt";
|
|
|
|
|
if (landscape) {
|
|
|
|
|
ss << ",landscape";
|
|
|
|
|
}
|
|
|
|
|
if (two_column) {
|
|
|
|
|
ss << ",twocolumn";
|
|
|
|
|
}
|
|
|
|
|
ss << "]{" << document_class << "}\n";
|
|
|
|
|
ss << "\\newcommand{\\documentclassname}{" << document_class << "}\n";
|
|
|
|
|
ss << "\\newcommand{\\documentside}{" << page_side << "}\n";
|
|
|
|
|
ss << "\\newcommand{\\documenttwocolumn}{" << (two_column ? "true" : "false") << "}\n";
|
|
|
|
|
ss << "\\newcommand{\\documentlandscape}{" << (landscape ? "true" : "false") << "}\n";
|
|
|
|
|
ss << "\\newcommand{\\documentisbook}{" << (book_format ? "true" : "false") << "}\n";
|
2026-07-24 21:37:58 +02:00
|
|
|
// ":bottom none" suppresses the footer entirely: no \documentbottom,
|
|
|
|
|
// and \pagestyle{empty} replaces the sks page styles below.
|
|
|
|
|
bool no_footer = (bottom == "none");
|
|
|
|
|
if (!bottom.empty() && !no_footer) {
|
2026-07-18 18:48:23 +02:00
|
|
|
ss << "\\newcommand{\\documentbottom}{" << bottom << "}\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ss << tex_style();
|
|
|
|
|
|
|
|
|
|
// Compute font scale factors: serif is the reference.
|
|
|
|
|
// Three methods (uncomment the desired one):
|
|
|
|
|
// x-height: serif_xh / other_xh (matches lowercase)
|
|
|
|
|
// cap-height: serif_ch / other_ch (matches capitals)
|
|
|
|
|
// average: mean(serif_xh,serif_ch) / mean(other_xh,other_ch)
|
|
|
|
|
float serif_xh = serif_font.xheight_ratio;
|
|
|
|
|
float serif_ch = serif_font.capheight_ratio;
|
|
|
|
|
float serif_avg = (serif_xh + serif_ch) / 2.0f;
|
|
|
|
|
auto font_scale_fn = [&](const Resolved_font& other) -> float {
|
|
|
|
|
float other_avg = (other.xheight_ratio + other.capheight_ratio) / 2.0f;
|
|
|
|
|
if (serif_avg > 0.0f && other_avg > 0.0f)
|
|
|
|
|
// return serif_xh / other.xheight_ratio; // x-height
|
|
|
|
|
// return serif_ch / other.capheight_ratio; // cap-height
|
|
|
|
|
return serif_avg / other_avg; // average
|
|
|
|
|
return 0.0f;
|
|
|
|
|
};
|
|
|
|
|
// Global font_scale multiplies all font sizes uniformly
|
|
|
|
|
float gs = (font_scale != 1.0f) ? font_scale : 0.0f;
|
|
|
|
|
float sans_s = font_scale_fn(sans_font);
|
|
|
|
|
float mono_s = font_scale_fn(mono_font);
|
|
|
|
|
// Apply global scale: multiply into per-font scale, or use alone
|
|
|
|
|
if (gs > 0.0f) {
|
|
|
|
|
sans_s = (sans_s > 0.0f) ? sans_s * font_scale : font_scale;
|
|
|
|
|
mono_s = (mono_s > 0.0f) ? mono_s * font_scale : font_scale;
|
|
|
|
|
}
|
|
|
|
|
ss << font_command("setmainfont", serif_font, gs);
|
|
|
|
|
ss << font_command("setsansfont", sans_font, sans_s);
|
|
|
|
|
ss << font_command("setmonofont", mono_font, mono_s);
|
|
|
|
|
|
|
|
|
|
// Scale leading proportionally with font_scale
|
|
|
|
|
float effective_leading = leading * ((font_scale != 1.0f) ? font_scale : 1.0f);
|
|
|
|
|
ss << "\n\\setstretch{" << effective_leading << "}\n";
|
|
|
|
|
if (ragged_right)
|
|
|
|
|
ss << "\\raggedright\n";
|
|
|
|
|
|
|
|
|
|
if (!prolog.empty()) {
|
|
|
|
|
ss << prolog << "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ss << "\\begin{document}\n";
|
|
|
|
|
|
|
|
|
|
if (book_format) {
|
|
|
|
|
// Front matter in roman numerals
|
|
|
|
|
ss << pagenumber(1, "roman");
|
|
|
|
|
|
|
|
|
|
// Cover page (page i, no displayed number)
|
|
|
|
|
if (!cover.empty()) {
|
|
|
|
|
ss << cover << "\n";
|
|
|
|
|
} else {
|
|
|
|
|
ss << default_cover(title, subtitle, authors, date, version);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verso page (page ii): copyright at bottom, or blank
|
|
|
|
|
// Ensures TOC starts on recto (page iii)
|
|
|
|
|
ss << book_verso_page(copyright);
|
|
|
|
|
|
|
|
|
|
// Table of contents (starts on page iii, recto)
|
2026-07-24 21:37:58 +02:00
|
|
|
ss << "\\pagestyle{" << (no_footer ? "empty" : "noheader") << "}\n"
|
2026-07-18 18:48:23 +02:00
|
|
|
<< "\\tableofcontents\n";
|
|
|
|
|
|
|
|
|
|
// Advance to the next recto page for the body.
|
|
|
|
|
// After the TOC (on an odd page like iii), we need a blank verso
|
|
|
|
|
// so the first chapter starts on recto. \cleardoublepage and
|
|
|
|
|
// \mainmatter fail here because \pagenumbering{arabic} resets the
|
|
|
|
|
// counter to 1 (odd), and \chapter's internal \cleardoublepage
|
|
|
|
|
// then sees an odd page and skips the blank.
|
|
|
|
|
//
|
|
|
|
|
// Solution: explicitly emit a blank verso page after the TOC,
|
|
|
|
|
// then switch to arabic numbering starting at page 1.
|
|
|
|
|
ss << "\\clearpage\n"
|
|
|
|
|
<< "\\thispagestyle{empty}\\mbox{}\\clearpage\n"
|
|
|
|
|
<< "\\pagenumbering{arabic}\n"
|
2026-07-24 21:37:58 +02:00
|
|
|
<< "\\pagestyle{" << (no_footer ? "empty" : "sksbook") << "}\n";
|
2026-07-18 18:48:23 +02:00
|
|
|
} else {
|
2026-07-24 21:37:58 +02:00
|
|
|
if (no_footer) {
|
|
|
|
|
ss << "\\pagestyle{empty}\n\n";
|
|
|
|
|
} else {
|
|
|
|
|
ss << "\\pagestyle{sks" << structure << "}\n\n";
|
|
|
|
|
}
|
2026-07-18 18:48:23 +02:00
|
|
|
// Plain or article: title block with optional copyright footnote
|
|
|
|
|
if (!copyright.empty()) {
|
|
|
|
|
ss << "\\renewcommand{\\thefootnote}{}\n";
|
|
|
|
|
ss << "\\footnotetext{" << copyright << "}\n";
|
|
|
|
|
ss << "\\renewcommand{\\thefootnote}{\\arabic{footnote}}\n";
|
|
|
|
|
}
|
|
|
|
|
ss << nonbook_title(title, subtitle, authors, date, version);
|
|
|
|
|
if (toc) {
|
|
|
|
|
ss << "\\tableofcontents\n";
|
|
|
|
|
ss << "\\vspace*{\\baselineskip}\n";
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
if (!landscape) {
|
|
|
|
|
ss << "\\vspace*{\\baselineskip}\n";
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ss << body << "\n";
|
|
|
|
|
ss << "\\end{document}\n";
|
|
|
|
|
return ss.str();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void check_for_xelatex()
|
|
|
|
|
{
|
|
|
|
|
std::string command = "which xelatex";
|
|
|
|
|
std::string xelatex_path = trim(exec(command.c_str()));
|
|
|
|
|
if (trim(xelatex_path).empty()) {
|
|
|
|
|
throw Environment_error(
|
|
|
|
|
"The \"xelatex\" command required to make PDF files is not installed",
|
|
|
|
|
Locator());
|
|
|
|
|
}
|
|
|
|
|
(void)K::log(2, "xelatex path is " + xelatex_path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> find_latex_error_lines(const std::string& log_content)
|
|
|
|
|
{
|
|
|
|
|
std::vector<std::string> errors;
|
|
|
|
|
std::istringstream stream(log_content);
|
|
|
|
|
std::string line;
|
|
|
|
|
int i = 1;
|
|
|
|
|
while (std::getline(stream, line)) {
|
|
|
|
|
if (!line.empty() && line[0] == '!') {
|
|
|
|
|
errors.push_back(std::to_string(i) + ": " + line);
|
|
|
|
|
}
|
|
|
|
|
i++;
|
|
|
|
|
}
|
|
|
|
|
return errors;
|
|
|
|
|
}
|