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).
This commit is contained in:
2026-07-22 18:17:43 +02:00
parent 6b75aa0c54
commit 8a2699a253
100 changed files with 1726 additions and 1152 deletions

View File

@@ -5,12 +5,12 @@ K := $(KLAMMERTEXT_HOME)
KS := $(K)/sks
KM := $(K)/mac
include $(KM)/env/makefile.env
include $(K)/env/makefile.env
# Source files
SOURCES := html_util.cpp latex_util.cpp font_resolve.cpp
OBJECTS := html_util.o latex_util.o font_resolve.o
DEPFILES := html_util.d latex_util.d font_resolve.d
SOURCES := html_util.cpp latex_util.cpp
OBJECTS := html_util.o latex_util.o
DEPFILES := html_util.d latex_util.d
# Additional include paths
LOCAL_CPPFLAGS := -I$(KM) -I$(KS)/kutil

View File

@@ -1,329 +0,0 @@
#include "font_resolve.h"
#include "file.h"
#include "util.h"
#include "error.h"
#include "log.h"
#include "show.h"
#include "kutil.h"
#include <algorithm>
#include <regex>
#include <sstream>
#include <filesystem>
namespace fs = std::filesystem;
std::string name_to_dirname(std::string name)
{
std::string result {};
for (char c : name) {
if (c == ' ')
result += '-';
else
result += std::tolower(c);
}
return result;
}
std::string name_to_google_query(std::string name)
{
std::string result {};
for (char c : name) {
if (c == ' ')
result += '+';
else
result += c;
}
return result;
}
static void classify_from_css(Resolved_font& font, std::string css_path)
{
// Parse the @font-face blocks in the CSS to determine variant → filename mapping
std::string css = string_from_file(css_path);
std::regex face_rgx(
R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\w+);[^}]*url\('([^']+\.ttf)'\)[^}]*\})",
std::regex::multiline);
auto begin = std::sregex_iterator(css.begin(), css.end(), face_rgx);
auto end = std::sregex_iterator();
for (auto it = begin; it != end; ++it) {
std::string style = (*it)[1];
std::string weight = (*it)[2];
std::string url_path = (*it)[3];
// URL is like 'dir-name/Filename.ttf' — extract just the filename
std::string filename = url_path.substr(url_path.rfind('/') + 1);
bool is_bold = (weight == "700" || weight == "bold");
bool is_italic = (style == "italic" || style == "oblique");
if (is_bold && is_italic)
font.bold_italic = filename;
else if (is_bold)
font.bold = filename;
else if (is_italic)
font.italic = filename;
else
font.regular = filename;
}
}
static Resolved_font resolve_bundled(std::string family_name, std::string dir_name)
{
std::string bundled_dir = klammertext_dir() + "/sks/font/fonts/" + dir_name;
std::string bundled_css = bundled_dir + ".css";
if (fs::exists(bundled_dir) && fs::exists(bundled_css)) {
Resolved_font font {};
font.family_name = family_name;
font.dir_name = dir_name;
font.font_dir = bundled_dir;
font.css_file = bundled_css;
font.from_cache = false;
classify_from_css(font, bundled_css);
return font;
}
return {};
}
static Resolved_font resolve_cached(std::string family_name, std::string dir_name)
{
std::string cache_base = cache_directory("_fonts");
std::string cache_dir = cache_base + "/" + dir_name;
std::string cache_css = cache_base + "/" + dir_name + ".css";
if (fs::exists(cache_dir) && fs::exists(cache_css)) {
Resolved_font font {};
font.family_name = family_name;
font.dir_name = dir_name;
font.font_dir = cache_dir;
font.css_file = cache_css;
font.from_cache = true;
classify_from_css(font, cache_css);
return font;
}
return {};
}
static Resolved_font fetch_google_font(std::string family_name, std::string dir_name)
{
(void)K::log(1, "Fetching font \"" + family_name + "\" from Google Fonts");
std::string query = name_to_google_query(family_name);
std::string url =
"https://fonts.googleapis.com/css2?family=" + query +
":ital,wght@0,400;0,700;1,400;1,700&display=swap";
std::string cmd = "curl -s -H 'User-Agent: Mozilla/4.0' '" + url + "'";
std::string css_response = exec(cmd.c_str());
if (css_response.empty() || css_response.find("@font-face") == std::string::npos) {
return {};
}
// Parse @font-face blocks to extract style, weight, and .ttf URL
struct Font_variant {
std::string style; // "normal" or "italic"
std::string weight; // "400" or "700"
std::string url;
};
std::vector<Font_variant> variants {};
std::regex face_rgx(
R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\d+);[^}]*src:\s*url\((https?://[^)]+\.ttf)\)[^}]*\})",
std::regex::multiline);
auto begin = std::sregex_iterator(css_response.begin(), css_response.end(), face_rgx);
auto end = std::sregex_iterator();
for (auto it = begin; it != end; ++it) {
Font_variant v {};
v.style = (*it)[1];
v.weight = (*it)[2];
v.url = (*it)[3];
variants.push_back(v);
}
if (variants.empty()) {
return {};
}
// Create cache directory
std::string cache_base = cache_directory("_fonts");
if (!fs::exists(cache_base))
fs::create_directories(cache_base);
std::string cache_dir = cache_base + "/" + dir_name;
if (!fs::exists(cache_dir))
fs::create_directory(cache_dir);
Resolved_font font {};
font.family_name = family_name;
font.dir_name = dir_name;
font.font_dir = cache_dir;
font.from_cache = true;
// Download each .ttf file with descriptive names
for (auto& v : variants) {
std::string local_name;
if (v.style == "normal" && v.weight == "400")
local_name = "Regular.ttf";
else if (v.style == "normal" && v.weight == "700")
local_name = "Bold.ttf";
else if (v.style == "italic" && v.weight == "400")
local_name = "Italic.ttf";
else if (v.style == "italic" && v.weight == "700")
local_name = "BoldItalic.ttf";
else
continue;
std::string ttf_path = cache_dir + "/" + local_name;
if (!fs::exists(ttf_path)) {
std::string dl_cmd = "curl -s -o '" + ttf_path + "' '" + v.url + "'";
(void)exec(dl_cmd.c_str());
if (!fs::exists(ttf_path)) {
(void)K::log(1, "Failed to download font file: " + v.url);
continue;
}
}
if (local_name == "Regular.ttf")
font.regular = local_name;
else if (local_name == "Bold.ttf")
font.bold = local_name;
else if (local_name == "Italic.ttf")
font.italic = local_name;
else if (local_name == "BoldItalic.ttf")
font.bold_italic = local_name;
}
// Generate @font-face CSS file
std::string css_file = cache_base + "/" + dir_name + ".css";
std::stringstream css {};
auto emit_face = [&](std::string style, std::string weight, std::string filename) {
if (filename.empty())
return;
css << "\n@font-face {\n"
<< " font-family: '" << family_name << "';\n"
<< " font-style: " << style << ";\n"
<< " font-weight: " << weight << ";\n"
<< " src: url('" << dir_name << "/" << filename << "') format('truetype');\n"
<< "}\n";
};
emit_face("normal", "400", font.regular);
emit_face("normal", "700", font.bold);
emit_face("italic", "400", font.italic);
emit_face("italic", "700", font.bold_italic);
string_to_file(css_file, css.str());
font.css_file = css_file;
return font;
}
static void extract_font_metrics(Resolved_font& font)
{
if (font.regular.empty() || font.font_dir.empty())
return;
std::string ttf_path = font.font_dir + "/" + font.regular;
if (!fs::exists(ttf_path))
return;
// Extract both x-height and cap-height ratios from OS/2 table
std::string script =
"python3 -c \""
"import struct; "
"f = open('" + ttf_path + "', 'rb'); "
"_, n = struct.unpack('>IH', f.read(6)); "
"f.read(6); "
"t = {};\n"
"for _ in range(n):\n"
" tag = f.read(4).decode('latin-1').strip('\\\\x00'); "
" _, o, l = struct.unpack('>III', f.read(12)); "
" t[tag] = o\n"
"f.seek(t['head'] + 18); "
"upm = struct.unpack('>H', f.read(2))[0]; "
"f.seek(t['OS/2']); "
"ver = struct.unpack('>H', f.read(2))[0]; "
"f.seek(t['OS/2'] + 86); "
"xh, ch = struct.unpack('>hh', f.read(4)); "
"print(f'{xh/upm:.4f} {ch/upm:.4f}') if ver >= 2 else None; "
"f.close()\"";
std::string result = trim(exec(script.c_str()));
if (!result.empty()) {
try {
auto pos = result.find(' ');
if (pos != std::string::npos) {
font.xheight_ratio = std::stof(result.substr(0, pos));
font.capheight_ratio = std::stof(result.substr(pos + 1));
}
} catch (...) {}
}
}
Resolved_font resolve_font(std::string family_name)
{
if (family_name.empty())
return {};
std::string dir_name = name_to_dirname(family_name);
// 1. Check bundled fonts
Resolved_font font = resolve_bundled(family_name, dir_name);
if (!font.family_name.empty()) {
extract_font_metrics(font);
return font;
}
// 2. Check font cache
font = resolve_cached(family_name, dir_name);
if (!font.family_name.empty()) {
extract_font_metrics(font);
return font;
}
// 3. Fetch from Google Fonts
font = fetch_google_font(family_name, dir_name);
if (!font.family_name.empty()) {
extract_font_metrics(font);
return font;
}
// 4. Error
throw Argument_error(
"Font \"" + family_name + "\" not found.\n"
" Not bundled in sks/font/fonts/" + dir_name + "/,\n"
" not cached, and not available from Google Fonts.\n"
" Check the font name or install it locally.");
}
// Font assets are copied with copy_file_stream() (mac/file.h) rather than
// std::filesystem::copy_file, which fails on Apple `container` virtiofs mounts
// — see the note on copy_file_stream() in file.cpp for the full rationale.
void install_resolved_font(const Resolved_font& font, std::string output_dir)
{
if (font.family_name.empty())
return;
std::string output_font_dir = output_dir + "/fonts";
if (!fs::exists(output_font_dir))
fs::create_directory(output_font_dir);
// Copy .css file and font directory to output
std::string dest_css = output_font_dir + "/" + font.dir_name + ".css";
std::string dest_dir = output_font_dir + "/" + font.dir_name;
copy_file_stream(font.css_file, dest_css);
if (!fs::exists(dest_dir)) {
fs::create_directory(dest_dir);
for (auto& entry : fs::directory_iterator(font.font_dir)) {
copy_file_stream(entry.path(),
dest_dir + "/" + entry.path().filename().string());
}
}
}

View File

@@ -1,24 +0,0 @@
#pragma once
#include <string>
#include <vector>
struct Resolved_font {
std::string family_name {}; // "Crimson Pro"
std::string dir_name {}; // "crimson-pro"
std::string font_dir {}; // Full path to font directory
std::string css_file {}; // Full path to .css file
bool from_cache = false;
// .ttf filenames for each variant (empty if variant not available):
std::string regular {};
std::string bold {};
std::string italic {};
std::string bold_italic {};
float xheight_ratio = 0.0f; // x-height / unitsPerEm from OS/2 table
float capheight_ratio = 0.0f; // cap-height / unitsPerEm from OS/2 table
};
std::string name_to_dirname(std::string name);
std::string name_to_google_query(std::string name);
Resolved_font resolve_font(std::string family_name);
void install_resolved_font(const Resolved_font& font, std::string output_dir);

View File

@@ -254,25 +254,6 @@ namespace html {
return result;
}
elements_t google_font_elements(strings_t fontnames)
{
elements_t result {};
if (fontnames.size() > 0) {
result.push_back(elt("link")
.attr("rel", "preconnect")
.attr("href", "https://fonts.googleapis.com"));
result.push_back(elt("link")
.attr("rel", "preconnect")
.attr("href", "https://fonts.gstatic.com"));
}
for (auto name : fontnames)
result.push_back(
elt("link")
.attr("href", "https://fonts.googleapis.com/css2?family=" + name + "&display=swap")
.attr("rel", "stylesheet"));
return result;
}
elements_t javascript(std::string output_dir, strings_t js_filenames)
{
//elements_t result { jquery_elements() };
@@ -298,15 +279,12 @@ namespace html {
HTML head(std::string title,
std::string css,
strings_t css_filenames,
strings_t local_fonts, strings_t google_fonts)
strings_t local_fonts)
{
elements_t elts = meta_elements();
// msg() << "ELTS sks: " << elts << "\n";
for (auto e : local_font_elements(local_fonts))
elts.push_back(e);
for (auto e : google_font_elements(google_fonts))
elts.push_back(e);
//elts += google_font_prolog();
//std::string css_dir = output_dir + "/css/";
// No; a single file at the top level...?
@@ -471,55 +449,6 @@ namespace html {
return result;
}
void install_local_fonts(strings_t font_dirs, strings_t names, std::string output_dir)
{
std::string output_font_dir = output_dir + "/fonts";
//std::cout << "Font directory for website: " << output_font_dir << "\n";
if (!file_exists(output_font_dir)) {
fs::create_directory(output_font_dir);
//std::cout << " Font directory created: " << output_font_dir << "\n";
}
for (auto name : names) {
//std::cout << "Font search for " << name << ": " << klammertext_dir() << "/sks/font/fonts/\n";
std::string src_font_dir = klammertext_dir() + "/sks/font/fonts/" + name;
if (!file_exists(src_font_dir)) {
bool found = false;
for (std::string font_dir : font_dirs) {
src_font_dir = font_dir + "/" + name;
//std::cout << "Font search for " << name << ": " << font_dir << "/\n";
if (file_exists(src_font_dir)) {
found = true;
break;
}
}
if (!found) {
throw Argument_error(
"Local font directory \"" + src_font_dir + "\" does not exist");
}
}
//std::cout << "Font " << name << ": " << src_font_dir << "\n";
std::string src_font_css = src_font_dir + ".css";
if (!fs::exists(src_font_css)) {
throw Argument_error(
"Local font CSS file \"" + src_font_css + "\" does not exist");
}
std::string font_copy =
"cp -r " + src_font_css + " " + src_font_dir + " " + output_font_dir;
//std::string files_copy = "cp -r " + basename + " " + font_dir;
//cout << css_copy << "\n" << files_copy << "\n";
//std::cout << system(css_copy.c_str()) << "\n";
//std::cout << system(files_copy.c_str()) << "\n";
if (system(font_copy.c_str()) != 0) {
std::cout << "Font copy command: " << font_copy << "\n";
throw Argument_error("Local font \"" + name + "\" not found");
}
}
}
elements_t status(std::string date, std::string version, std::string copyright)
{
elements_t result {};
@@ -550,7 +479,6 @@ namespace html {
strings_t css_filenames,
strings_t js_filenames,
strings_t local_fonts,
strings_t google_fonts,
std::string logo)
{
if (page_title.empty())
@@ -601,7 +529,7 @@ namespace html {
page_title = title;
result.push_back(
elt("html",
{ head(page_title, css, css_filenames, local_fonts, google_fonts),
{ head(page_title, css, css_filenames, local_fonts),
elt("body", body)
}).attr("lang", "en"));
return result;
@@ -666,12 +594,12 @@ namespace html {
elements_t make_page(
elements_t body,
std::string page_title, std::string css,
strings_t css_filenames, strings_t local_fonts, strings_t google_fonts)
strings_t css_filenames, strings_t local_fonts)
{
elements_t page { preamble() };
page.push_back(
elt("html",
{ head(page_title, css, css_filenames, local_fonts, google_fonts),
{ head(page_title, css, css_filenames, local_fonts),
elt("body", body)
}).attr("lang", "en"));
return page;

View File

@@ -76,8 +76,6 @@ namespace html {
//HTML preamble();
//HTML head(strings_t css_filenames, strings_t js_filenames);
void install_local_fonts(strings_t font_dirs, strings_t names, std::string output_dir);
elements_t page(
std::string title,
std::string page_title,
@@ -93,7 +91,6 @@ namespace html {
strings_t css_filenames = {},
strings_t js_filenames = {},
strings_t local_fonts = {},
strings_t google_fonts = {},
std::string logo = {});
void add_title(elements_t& body, std::string title, std::string logo="");
@@ -108,8 +105,7 @@ namespace html {
elements_t body,
std::string page_title, std::string css,
std::vector<std::string> css_filenames,
std::vector<std::string> local_fonts,
std::vector<std::string> google_fonts);
std::vector<std::string> local_fonts);
bool tag_is_block_element(std::string tag);
std::string make_paragraphs(std::string html_text);

View File

@@ -198,7 +198,7 @@ def add_caption(element, caption_label, number, caption_text,
tag = element_tag(element)
# Caption
caption = ""
if number == "true":
if number:
caption = kutil.caption_marker(caption_label, caption_text)
elif caption_text:
caption = caption_text

View File

@@ -3,7 +3,7 @@
#include "util.h"
#include "log.h"
#include "show.h"
#include "font_resolve.h"
#include "font_store.h"
namespace latex {

View File

@@ -2,7 +2,7 @@
#include <string>
#include <vector>
#include "font_resolve.h"
#include "font_store.h"
namespace latex {

View File

@@ -73,9 +73,8 @@ def make_caption_text(number, label, text, font_symbol, font_size):
# caption = text # f"{{\\small \\it \\par {caption_text}}}"
caption = text
font_symbol = "r"
font_size = 1.2
if caption is None:
return ""
caption = font.tex_fontify(caption, font_symbol, font_size)
return caption
@@ -108,9 +107,9 @@ def add_caption(element, caption_label, number, caption_text, latex_width,
# minipage(caption),
latex_width, vertical="t")
elif side == "top":
element = minipage(caption + "\n" + element,
# minipage(caption, vertical="t") +
# caption_margin + "\n" +
# minipage(element),
# A depth strut: the bottom-caption case uses \vstrut (height)
# above the caption; a top caption needs the mirror image,
# space below its line.
element = minipage(caption + "\\rule[-0.75\\baselineskip]{0pt}{0pt}\n" + element,
latex_width, vertical="t")
return caption_wrapper(element, hpos)