Files
klammertext/sks/target/latex_util.cpp
T
ack 830b19394d Klammertext runs natively on Windows; a Windows source-install guide; the bundled sh for @eval :shell
Assembled from klammertext-dev f344a37d772c. The commands, the library,
the Standard Klammer Set, the reflection klammerset and the Python
binding build and run natively on Windows (MSYS2 UCRT64 gcc, python.org
Python), and both test tiers pass there. doc/install/windows_source_install.md
is the fifth install guide: a source install parallel to the Linux and
macOS ones, verified by following it on a pristine Windows 11.

Behaviour on every system: paths Klammertext prints use forward slashes;
output is LF; the command line is UTF-8 on Windows; @eval :shell is a
POSIX shell command everywhere -- on Windows it runs the sh Klammertext
ships, BusyBox for Windows (win/busybox/, GPLv2, with its licence and the
source pointer). KLAMMERTEXT_FONTS and KLAMMERTEXT_KLAMMERSETS are lists in
the system's PATH convention (';' on Windows). On Linux and macOS nothing
observable changes.
2026-09-25 00:33:26 +02:00

330 lines
13 KiB
C++

#include "file.h"
#include "kutil.h"
#include "util.h"
#include "log.h"
#include "show.h"
#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 << "\\thispagestyle{empty}\n";
ss << title_line(title, "1.8", "6pt")
<< title_line(subtitle, "1.5", "16pt", true)
<< 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_typeface,
Resolved_font sans_typeface,
Resolved_font mono_typeface,
float text_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_typeface.xheight_ratio;
float serif_ch = serif_typeface.capheight_ratio;
float serif_avg = (serif_xh + serif_ch) / 2.0f;
auto text_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 text_scale multiplies all font sizes uniformly
float gs = (text_scale != 1.0f) ? text_scale : 0.0f;
float sans_s = text_scale_fn(sans_typeface);
float mono_s = text_scale_fn(mono_typeface);
// Apply global scale: multiply into per-font scale, or use alone
if (gs > 0.0f) {
sans_s = (sans_s > 0.0f) ? sans_s * text_scale : text_scale;
mono_s = (mono_s > 0.0f) ? mono_s * text_scale : text_scale;
}
ss << font_command("setmainfont", serif_typeface, gs);
ss << font_command("setsansfont", sans_typeface, sans_s);
ss << font_command("setmonofont", mono_typeface, mono_s);
// Scale leading proportionally with text_scale
float effective_leading = leading * ((text_scale != 1.0f) ? text_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()
{
// A PATH search, not "which" through a shell: on Windows popen() runs
// cmd.exe, which has no which, and the answer would always be empty.
std::string xelatex_path = find_in_path("xelatex");
if (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;
}