A snapshot of the development tree. The substantial changes since the last one:
COMMAND OUTPUT POLICY. The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT. For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document. A bare command prints
its usage and succeeds rather than failing. Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.
"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded. Higher levels are the trace.
The commands no longer warn and continue: an anomaly is an error, described with
its location. Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.
@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read. Two consequences for a writer:
* a state variable reaches the predicate. "@@@state Flag :value true @@@
@cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
silently take the false branch. The document now behaves like a klammer
body, whose arguments are bound before its conditionals are decided.
* nothing in a discarded branch happens -- it is not read, not evaluated, not
expanded. An @eval in the branch not taken used to run anyway.
Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.
@eval REACHING OUTSIDE. ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported. A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".
KLAMMER SETS. Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions. "none" means none and may not be combined with other symbols. A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.
TESTS. Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers. Three suites
that could not run on macOS at all now do.
Assembled from dev commit 6c8ee6c22fca.
329 lines
13 KiB
C++
329 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_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;
|
|
}
|