Files
klammertext/mac/font_store.cpp

623 lines
23 KiB
C++
Raw Normal View History

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
// The Klammertext font store: INFRASTRUCTURE, not part of the
// Klammermachine (the Machine class never references it) and not part of
// any klammer set. Klammer sets (the SKS's @document, a future music
// set) consume the store; the kdesc command lists, installs into, and
// samples it without loading any klammer set. The store's search runs
// over the KLAMMERTEXT_FONTS directories and ends at the distribution's
// default font set in $KLAMMERTEXT_HOME/fnt.
#include "font_store.h"
#include "file.h"
#include "locator.h"
#include "util.h"
#include "error.h"
#include "log.h"
#include "show.h"
#include <algorithm>
#include <fstream>
#include <regex>
#include <set>
#include <sstream>
#include <filesystem>
namespace fs = std::filesystem;
// A filesystem extension lowercased for case-insensitive matching, so a
// font named Foo.TTF or Foo.OTF is recognized like Foo.ttf / Foo.otf.
static std::string lower_extension(const fs::path& path)
{
std::string ext = path.extension();
for (char& c : ext) {
c = std::tolower(static_cast<unsigned char>(c));
}
return ext;
}
std::string name_to_dirname(const std::string& name)
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
{
std::string result {};
for (char c : name) {
if (c == ' ')
result += '-';
else
result += std::tolower(c);
}
return result;
}
// The directories searched for installed fonts: the colon-separated
// KLAMMERTEXT_FONTS environment variable, or ~/.klammertext/fonts when it
// is not set. Directories are searched in listed order, and the default
// fonts in $KLAMMERTEXT_HOME/fnt are searched last, so an installed font
// can deliberately shadow a default one.
strings_t installed_font_dirs()
{
strings_t result {};
std::string paths {};
const char* env = std::getenv("KLAMMERTEXT_FONTS");
if (env && *env) {
paths = env;
} else if (const char* home = std::getenv("HOME"); home && *home) {
paths = std::string(home) + "/.klammertext/fonts";
}
std::stringstream ss(paths);
std::string dir;
while (std::getline(ss, dir, ':')) {
if (!dir.empty()) {
result.push_back(dir);
}
}
return result;
}
std::string default_font_dir()
{
const char* home = std::getenv(klammertext_home_var.c_str());
if (home == nullptr || *home == '\0') {
throw Argument_error(
"The environment variable " + klammertext_home_var + " is not defined");
}
return std::string(home) + "/fnt";
}
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;
}
}
// Resolve a font within one base directory: <base>/<dir-name>/ holding the
// .ttf files and <base>/<dir-name>.css declaring the @font-face variants.
static Resolved_font resolve_in_directory(
const std::string& family_name, const std::string& dir_name, const std::string& base_dir)
{
std::string font_dir = base_dir + "/" + dir_name;
std::string css_file = font_dir + ".css";
if (fs::exists(font_dir) && fs::exists(css_file)) {
Resolved_font font {};
font.family_name = family_name;
font.dir_name = dir_name;
font.font_dir = font_dir;
font.css_file = css_file;
classify_from_css(font, css_file);
return font;
}
return {};
}
// Every font available for the three @document role parameters: each
// <name>.css with a matching <name>/ directory, across the installed
// directories and the default font set. The reported name is the family name
// from the CSS (what the writer types), falling back to the directory name.
strings_t available_font_families()
{
strings_t result {};
static const std::regex family_rgx(R"(font-family:\s*'([^']+)')");
auto scan = [&](const std::string& base) {
if (!fs::exists(base)) {
return;
}
for (auto& entry : fs::directory_iterator(base)) {
if (entry.path().extension() == ".css" &&
fs::is_directory(base + "/" + entry.path().stem().string())) {
std::string css = string_from_file(entry.path());
std::smatch match {};
if (std::regex_search(css, match, family_rgx)) {
result.push_back(match[1]);
} else {
result.push_back(entry.path().stem());
}
}
}
};
for (const std::string& dir : installed_font_dirs()) {
scan(dir);
}
scan(default_font_dir());
std::sort(result.begin(), result.end());
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}
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(const std::string& family_name)
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
{
if (family_name.empty())
return {};
std::string dir_name = name_to_dirname(family_name);
// Installed directories in listed order, then the default font set, so an
// installed font can shadow a default one.
strings_t bases = installed_font_dirs();
bases.push_back(default_font_dir());
for (const std::string& base : bases) {
Resolved_font font = resolve_in_directory(family_name, dir_name, base);
if (!font.family_name.empty()) {
extract_font_metrics(font);
An output policy for the three commands, and @cond as a true special form 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.
2026-08-16 01:37:59 +02:00
// A DERIVED value: an installed font may shadow a default one, and
// which directory won is not readable from ":serif_font Whatever".
// The commonest font question -- "why does it not look like the
// one I installed?" -- is answered by this line.
(void)K::log(1, "Font \"" + family_name + "\": " + base + "/" + dir_name);
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
return font;
}
}
std::stringstream ss {};
ss << "The font \"" << family_name << "\" is not installed.\n\n"
<< "Available fonts:\n";
for (const std::string& name : available_font_families()) {
ss << " " << name << "\n";
}
ss << "\nFonts are searched in the directories of the KLAMMERTEXT_FONTS\n"
<< "environment variable (colon-separated; default $HOME/.klammertext/fonts)\n"
<< "and then in the default font set. To install a font,\n"
<< "place its files as <fonts-dir>/" << dir_name << "/*.ttf with a\n"
<< "<fonts-dir>/" << dir_name << ".css declaring its @font-face variants.";
throw Argument_error(ss.str());
}
// Font file classification: read the family name, weight, and style from
// the font's internal tables (sfnt 'name', 'OS/2', 'fvar') rather than
// from filenames, which vary by source (Google zips, foundries, ...).
namespace {
uint16_t be16(const std::string& d, size_t off)
{
return (uint8_t(d[off]) << 8) | uint8_t(d[off + 1]);
}
uint32_t be32(const std::string& d, size_t off)
{
return (uint32_t(be16(d, off)) << 16) | be16(d, off + 2);
}
std::string read_binary_file(const std::string& path)
{
std::ifstream in(path, std::ios::binary);
std::stringstream ss {};
ss << in.rdbuf();
return ss.str();
}
// Decode a name-table string: UTF-16BE for Windows records (keep the BMP
// low bytes; family names are almost always Latin), bytes as-is otherwise.
std::string decode_name(const std::string& raw, bool utf16be)
{
std::string result {};
if (utf16be) {
for (size_t i = 0; i + 1 < raw.size(); i += 2) {
if (raw[i] == 0) {
result += raw[i + 1];
}
}
} else {
result = raw;
}
return result;
}
} // namespace
Font_file classify_font_file(const std::string& path)
{
Font_file file {};
file.path = path;
// Normalize to lowercase so the installed filename, its @font-face url,
// and the truetype/opentype format detection are uniform regardless of
// how the source file's extension was capitalized.
file.extension = lower_extension(path);
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
std::string d = read_binary_file(path);
if (d.size() < 12) {
file.note = "not a font file (too short)";
return file;
}
uint32_t tag = be32(d, 0);
if (tag == 0x74746366) { // 'ttcf'
file.note = "font collections (.ttc) are not supported; "
"use the individual font files";
return file;
}
if (tag != 0x00010000 && tag != 0x4F54544F) { // sfnt or 'OTTO'
file.note = "not a TrueType or OpenType font";
return file;
}
uint16_t num_tables = be16(d, 4);
std::map<std::string, std::pair<uint32_t, uint32_t>> tables {}; // tag -> offset,length
for (uint16_t i = 0; i < num_tables; i++) {
size_t rec = 12 + i * 16;
if (rec + 16 > d.size()) {
break;
}
tables[d.substr(rec, 4)] = { be32(d, rec + 8), be32(d, rec + 12) };
}
file.variable = tables.contains("fvar");
// Family name from the 'name' table: typographic family (16) wins
// over family (1); Windows records (platform 3) win over Macintosh.
if (auto it = tables.find("name"); it != tables.end()) {
size_t base = it->second.first;
uint16_t count = be16(d, base + 2);
uint16_t string_offset = be16(d, base + 4);
int best_rank = -1;
for (uint16_t i = 0; i < count; i++) {
size_t rec = base + 6 + i * 12;
if (rec + 12 > d.size()) {
break;
}
uint16_t platform = be16(d, rec);
uint16_t name_id = be16(d, rec + 6);
uint16_t length = be16(d, rec + 8);
uint16_t offset = be16(d, rec + 10);
if (name_id != 1 && name_id != 16) {
continue;
}
int rank = (name_id == 16 ? 2 : 0) + (platform == 3 ? 1 : 0);
size_t at = base + string_offset + offset;
if (rank > best_rank && at + length <= d.size()) {
file.family = decode_name(d.substr(at, length), platform == 3);
best_rank = rank;
}
}
}
if (file.family.empty()) {
file.note = "no family name found in the font's name table";
return file;
}
bool italic = false;
if (auto it = tables.find("OS/2"); it != tables.end()) {
size_t base = it->second.first;
file.weight = be16(d, base + 4);
italic = be16(d, base + 62) & 0x0001; // fsSelection italic bit
} else if (auto ht = tables.find("head"); ht != tables.end()) {
uint16_t mac_style = be16(d, ht->second.first + 44);
file.weight = (mac_style & 0x0001) ? 700 : 400;
italic = mac_style & 0x0002;
}
if (file.variable) {
// A variable font covers the weight axis; use it as the regular
// (or italic) face and let renderers derive weights.
file.variant = italic ? "Italic" : "Regular";
} else if (file.weight >= 380 && file.weight <= 450) {
file.variant = italic ? "Italic" : "Regular";
} else if (file.weight >= 650 && file.weight <= 760) {
file.variant = italic ? "BoldItalic" : "Bold";
} else {
std::stringstream note {};
note << "weight " << file.weight
<< " not installed (only regular 400 and bold 700 are used)";
file.note = note.str();
}
return file;
}
std::vector<Font_file> classify_font_files(const std::string& directory)
{
std::vector<Font_file> result {};
if (!fs::exists(directory)) {
throw Argument_error(
"The font directory \"" + directory + "\" does not exist");
}
for (auto& entry : fs::recursive_directory_iterator(directory)) {
std::string ext = lower_extension(entry.path());
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
if (entry.is_regular_file() && (ext == ".ttf" || ext == ".otf")) {
result.push_back(classify_font_file(entry.path()));
}
}
return result;
}
// The css is always generated, never copied, so its urls are relative and
// the installed pair stays relocatable.
static std::string font_face_css(
const std::string& family, const std::string& dir_name,
const std::map<std::string, const Font_file*>& slots)
{
std::stringstream css {};
auto emit = [&](const std::string& variant,
const std::string& style, const std::string& weight) {
auto it = slots.find(variant);
if (it == slots.end()) {
return;
}
std::string format =
it->second->extension == ".otf" ? "opentype" : "truetype";
css << "\n@font-face {\n"
<< " font-family: '" << family << "';\n"
<< " font-style: " << style << ";\n"
<< " font-weight: " << weight << ";\n"
<< " src: url('" << dir_name << "/" << variant
<< it->second->extension << "') format('" << format << "');\n"
<< "}\n";
};
emit("Regular", "normal", "400");
emit("Bold", "normal", "700");
emit("Italic", "italic", "400");
emit("BoldItalic", "italic", "700");
return css.str();
}
std::string install_fonts(const std::string& source_dir, std::string dest_dir)
{
if (dest_dir.empty()) {
strings_t dirs = installed_font_dirs();
if (dirs.empty()) {
throw Argument_error(
"No installation directory: KLAMMERTEXT_FONTS is empty and "
"HOME is not set");
}
dest_dir = dirs[0];
}
std::vector<Font_file> files = classify_font_files(source_dir);
if (files.empty()) {
throw Argument_error(
"No font files (.ttf or .otf) found under \"" + source_dir + "\"");
}
// Choose one file per (family, variant) slot; a static face wins over
// a variable font's derived face.
std::map<std::string, std::map<std::string, const Font_file*>> families {};
std::stringstream report {};
for (const Font_file& file : files) {
if (file.variant.empty()) {
report << " skipped " << fs::path(file.path).filename().string()
<< ": " << file.note << "\n";
continue;
}
auto& slots = families[file.family];
auto it = slots.find(file.variant);
if (it == slots.end() || (it->second->variable && !file.variable)) {
slots[file.variant] = &file;
}
}
for (auto& [family, slots] : families) {
std::string dir_name = name_to_dirname(family);
std::string family_dir = dest_dir + "/" + dir_name;
fs::create_directories(family_dir);
strings_t variants {};
for (auto& [variant, file] : slots) {
copy_file_stream(file->path,
family_dir + "/" + variant + file->extension);
variants.push_back(variant + (file->variable ? " (variable)" : ""));
}
string_to_file(dest_dir + "/" + dir_name + ".css",
font_face_css(family, dir_name, slots));
report << " installed \"" << family << "\" (" << join(variants, ", ")
<< ") in " << family_dir << "\n";
}
return report.str();
}
std::string describe_fonts()
{
std::stringstream ss {};
std::set<std::string> seen {};
strings_t bases = installed_font_dirs();
bases.push_back(default_font_dir());
for (size_t i = 0; i < bases.size(); i++) {
const std::string& base = bases[i];
bool is_default = (i == bases.size() - 1);
ss << base << (is_default ? " (default font set)" : "") << ":\n";
if (!fs::exists(base)) {
ss << " [directory does not exist]\n";
continue;
}
strings_t names {};
for (auto& entry : fs::directory_iterator(base)) {
if (entry.path().extension() == ".css" &&
fs::is_directory(base + "/" + entry.path().stem().string())) {
names.push_back(entry.path().stem());
}
}
std::sort(names.begin(), names.end());
if (names.empty()) {
ss << " [no fonts]\n";
}
for (const std::string& dir_name : names) {
Resolved_font font = resolve_in_directory("?", dir_name, base);
std::string css = string_from_file(font.css_file);
std::smatch match {};
std::string family = dir_name;
if (std::regex_search(css, match,
std::regex(R"(font-family:\s*'([^']+)')"))) {
family = match[1];
}
strings_t variants {};
if (!font.regular.empty()) variants.push_back("Regular");
if (!font.bold.empty()) variants.push_back("Bold");
if (!font.italic.empty()) variants.push_back("Italic");
if (!font.bold_italic.empty()) variants.push_back("BoldItalic");
ss << " " << family << " (" << join(variants, ", ") << ")";
if (seen.contains(dir_name)) {
ss << " [shadowed by an earlier directory]";
}
seen.insert(dir_name);
ss << "\n";
}
}
return ss.str();
}
std::string write_font_samples(const std::string& output_dir, const std::string& source_dir)
{
fs::create_directories(output_dir);
strings_t families {};
if (source_dir.empty()) {
for (const std::string& family : available_font_families()) {
install_resolved_font(resolve_font(family), output_dir);
families.push_back(family);
}
} else {
// Uninstalled preview: install the classified fonts directly into
// the sample page's own fonts directory.
install_fonts(source_dir, output_dir + "/fonts");
for (auto& entry : fs::directory_iterator(output_dir + "/fonts")) {
if (entry.path().extension() == ".css") {
std::string css = string_from_file(entry.path());
std::smatch match {};
if (std::regex_search(css, match,
std::regex(R"(font-family:\s*'([^']+)')"))) {
families.push_back(match[1]);
}
}
}
std::sort(families.begin(), families.end());
}
std::stringstream html {};
html << "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"
<< "<meta charset=\"UTF-8\">\n<title>Klammertext font samples</title>\n";
for (const std::string& family : families) {
html << "<link href=\"fonts/" << name_to_dirname(family)
<< ".css\" rel=\"stylesheet\">\n";
}
html << "<style>\n"
<< "body { margin: 2rem auto; max-width: 46rem; font-family: sans-serif; }\n"
<< "h2 { border-bottom: 1px solid #999; margin-top: 2.5rem; }\n"
<< ".sample { font-size: 1.3rem; margin: .3rem 0; }\n"
<< ".alphabet { font-size: 1.0rem; color: #333; margin: .3rem 0; }\n"
<< "</style>\n</head>\n<body>\n<h1>Klammertext font samples</h1>\n";
for (const std::string& family : families) {
html << "<h2>" << family << "</h2>\n"
<< "<div style=\"font-family: '" << family << "'\">\n"
<< "<p class=\"sample\">The quick brown fox jumps over the lazy dog.</p>\n"
<< "<p class=\"sample\" style=\"font-style: italic\">"
<< "The quick brown fox jumps over the lazy dog.</p>\n"
<< "<p class=\"sample\" style=\"font-weight: bold\">"
<< "The quick brown fox jumps over the lazy dog.</p>\n"
<< "<p class=\"sample\" style=\"font-weight: bold; font-style: italic\">"
<< "The quick brown fox jumps over the lazy dog.</p>\n"
<< "<p class=\"alphabet\">ABCDEFGHIJKLMNOPQRSTUVWXYZ "
<< "abcdefghijklmnopqrstuvwxyz 0123456789 "
<< "&auml;&ouml;&uuml;&szlig; &ldquo;quoted&rdquo; 3.14159</p>\n"
<< "</div>\n";
}
html << "</body>\n</html>\n";
std::string index = output_dir + "/index.html";
string_to_file(index, html.str());
return index;
}
// 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());
}
}
}