Files
klammertext/sks/target/latex_util.cpp

333 lines
13 KiB
C++
Raw Normal View History

#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"
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(
const std::string& text, const std::string& font="1.3", const 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(
const std::string& title, const std::string& subtitle,
const std::string& author, const std::string& date, const 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(
const std::string& title, const std::string& subtitle,
const std::string& author, const std::string& date, const 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, const 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(const 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(const 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(const std::string& structure,
const std::string& title,
const std::string& subtitle,
const std::string& authors,
const std::string& date,
const std::string& version,
const std::string& copyright,
const std::string& bottom,
const std::string& prolog,
const std::string& paper_size,
bool two_column,
float leading,
int pointsize,
bool ragged_right,
const std::string& cover,
bool landscape,
const 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";
// ":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) {
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)
ss << "\\pagestyle{" << (no_footer ? "empty" : "noheader") << "}\n"
<< "\\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"
<< "\\pagestyle{" << (no_footer ? "empty" : "sksbook") << "}\n";
} else {
if (no_footer) {
ss << "\\pagestyle{empty}\n\n";
} else {
ss << "\\pagestyle{sks" << structure << "}\n\n";
}
// 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;
}