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.
595 lines
22 KiB
C++
595 lines
22 KiB
C++
// 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;
|
|
|
|
// The interpreter spawned for the font-metric script: python.org's Windows
|
|
// build installs "python" only; Linux and macOS name it "python3".
|
|
#ifdef _WIN32
|
|
static const char* const python_interpreter = "python";
|
|
#else
|
|
static const char* const python_interpreter = "python3";
|
|
#endif
|
|
|
|
|
|
// 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().string();
|
|
for (char& c : ext) {
|
|
c = std::tolower(static_cast<unsigned char>(c));
|
|
}
|
|
return ext;
|
|
}
|
|
|
|
|
|
std::string name_to_dirname(const std::string& name)
|
|
{
|
|
std::string result {};
|
|
for (char c : name) {
|
|
if (c == ' ')
|
|
result += '-';
|
|
else
|
|
result += std::tolower(c);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
// The directories searched for installed fonts: the KLAMMERTEXT_FONTS
|
|
// environment variable (a list in the system's PATH convention: ':' or, on
|
|
// Windows, ';'), 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()
|
|
{
|
|
return directory_list("KLAMMERTEXT_FONTS", ".klammertext/fonts");
|
|
}
|
|
|
|
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);
|
|
// The family name is what the css DECLARES, not what the caller wrote:
|
|
// a typeface may be asked for by its directory name
|
|
// ("noto-sans-egyptian-hieroglyphs"), and a font-family value in
|
|
// generated html must match the @font-face or the browser substitutes
|
|
// silently (until 2026-09-12 the request was echoed back).
|
|
static const std::regex family_rgx(R"(font-family:\s*'([^']+)')");
|
|
std::smatch match;
|
|
if (std::regex_search(css, match, family_rgx))
|
|
font.family_name = match[1];
|
|
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 {};
|
|
}
|
|
|
|
std::vector<Resolved_font> fonts_in_directory(const std::string& base)
|
|
{
|
|
std::vector<Resolved_font> result {};
|
|
if (!fs::is_directory(base)) {
|
|
return result;
|
|
}
|
|
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().string());
|
|
}
|
|
}
|
|
std::sort(names.begin(), names.end());
|
|
for (const std::string& dir_name : names) {
|
|
// The directory name is the family name's fallback: classify_from_css
|
|
// overwrites it with the name the css declares, when it declares one.
|
|
result.push_back(resolve_in_directory(dir_name, dir_name, base));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Every font available for the three @document role parameters, across the
|
|
// installed directories and the default font set. The reported name is the
|
|
// family name from the CSS (what the writer types).
|
|
strings_t available_font_families()
|
|
{
|
|
strings_t result {};
|
|
strings_t bases = installed_font_dirs();
|
|
bases.push_back(default_font_dir());
|
|
for (const std::string& base : bases) {
|
|
for (const Resolved_font& font : fonts_in_directory(base)) {
|
|
result.push_back(font.family_name);
|
|
}
|
|
}
|
|
std::sort(result.begin(), result.end());
|
|
result.erase(std::unique(result.begin(), result.end()), result.end());
|
|
return result;
|
|
}
|
|
|
|
bool font_installed(const std::string& name)
|
|
{
|
|
const std::string wanted = name_to_dirname(name);
|
|
for (const std::string& family : available_font_families())
|
|
if (name_to_dirname(family) == wanted)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
|
|
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
|
|
// The path goes inside a Python literal: forward slashes, which Python
|
|
// opens on every system, where a Windows backslash would be an escape.
|
|
std::string script =
|
|
std::string(python_interpreter) + " -c \""
|
|
"import struct; "
|
|
"f = open('" + fs::path(ttf_path).generic_string() + "', '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()));
|
|
// A DERIVED value a designer cannot read off the font: the two ratios
|
|
// (or nothing, when the script failed) decide the html size normalization.
|
|
(void)K::log(3, "Font metrics of " + ttf_path + ": " + (result.empty() ? std::string("(none)") : result));
|
|
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, const Locator& loc)
|
|
{
|
|
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);
|
|
// A DERIVED value: an installed font may shadow a default one, and
|
|
// which directory won is not readable from ":serif_typeface Whatever".
|
|
// The commonest font question -- "why does it not look like the
|
|
// one I installed?" -- is answered by this line.
|
|
(void)K::log(1, "Typeface \"" + family_name + "\": " + base + "/" + dir_name);
|
|
return font;
|
|
}
|
|
}
|
|
|
|
std::stringstream ss {};
|
|
ss << "The typeface \"" << family_name << "\" is not installed.\n\n"
|
|
<< "Available typefaces:\n";
|
|
for (const std::string& name : available_font_families()) {
|
|
ss << " " << name << "\n";
|
|
}
|
|
ss << "\nTypefaces are searched in the directories of the KLAMMERTEXT_FONTS\n"
|
|
<< "environment variable (colon-separated; default $HOME/.klammertext/fonts)\n"
|
|
<< "and then in the default set. To install a typeface,\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(), loc);
|
|
}
|
|
|
|
|
|
// 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);
|
|
|
|
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());
|
|
if (entry.is_regular_file() && (ext == ".ttf" || ext == ".otf")) {
|
|
result.push_back(classify_font_file(entry.path().string()));
|
|
}
|
|
}
|
|
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 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().string());
|
|
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 "
|
|
<< "äöüß “quoted” 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());
|
|
}
|
|
}
|
|
}
|