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

@@ -4,13 +4,13 @@
K := $(KLAMMERTEXT_HOME)
KS := $(K)/sks
include $(K)/mac/env/makefile.env
include $(K)/env/makefile.env
# Source files
BASENAMES := util error locator file argv character ktype katom katom_list \
log show command argument argument_set argtype argtype_set \
state eval eval_python eval_cpp klammer klammer_set deftype \
target target_set machine
target target_set machine font_store
SOURCES := $(addsuffix .cpp,$(BASENAMES))
OBJECTS := $(addsuffix .o,$(BASENAMES))

View File

@@ -7,12 +7,13 @@
#include "util.h"
Argtype::Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
std::string python_cast, modify_string_f python_format,
std::string default_value, std::string python_cast, modify_string_f python_format,
const Locator& loc)
: m_name(name)
, m_desc(desc)
, m_symbolic_pattern(symbolic_pattern)
, m_pattern(pattern)
, m_default(default_value)
, m_python_cast(python_cast)
, m_python_format(python_format)
, m_regex(std::regex(pattern))
@@ -20,22 +21,25 @@ Argtype::Argtype(std::string name, std::string desc, std::string symbolic_patter
{
}
static bool empty_value(const strings_t& v)
{
return v.empty() || (v.size() == 1 && v[0].empty());
}
std::string pyformat_string(const strings_t& v)
{
std::string p = v[0];
std::string delim = "\"";
if (contains(p, "\"\"\"")) {
delim = "'''";
} else if (contains(p, "'''")) {
delim = "\"\"\"";
} else if (contains(p, "\"")) {
delim = "'";
}
return delim + p + delim;
std::string p = v.empty() ? "" : v[0];
p = string_replace(p, "\\", "\\\\");
p = string_replace(p, "\"", "\\\"");
p = string_replace(p, "\n", "\\n");
return "\"" + p + "\"";
}
std::string pyformat_bool(const strings_t& v)
{
if (empty_value(v)) {
return "None";
}
std::string value = v[0];
std::set<std::string> true_values { "1", "true", "True", "yes" };
std::set<std::string> false_values { "0", "false", "False", "no" };
@@ -50,6 +54,9 @@ std::string pyformat_bool(const strings_t& v)
std::string pyformat_number(const strings_t& value)
{
if (empty_value(value)) {
return "None";
}
return value[0];
}
@@ -62,12 +69,29 @@ std::string pylist(strings_t words)
std::string pyformat_list(const strings_t& value)
{
return pylist(value);
strings_t words {};
for (const std::string& v : value) {
for (const std::string& word : word_split(v)) {
if (!word.empty()) {
words.push_back(word);
}
}
}
return pylist(words);
}
std::string pyformat_dlist(const strings_t& value)
{
return pylist(value);
strings_t items {};
for (const std::string& v : value) {
if (v.empty()) {
continue;
}
for (const std::string& item : dlist_split(v)) {
items.push_back(item);
}
}
return pylist(items);
}
std::string Argtype::python_value(const std::string& var_name, std::vector<std::string> value, size_t name_size)
@@ -76,7 +100,18 @@ std::string Argtype::python_value(const std::string& var_name, std::vector<std::
ss << " " << std::left << std::setw(name_size) << var_name << " = ";
if (m_python_format) {
ss << m_python_format(value);
} else if (m_python_cast.empty() || m_python_cast == "str") {
// String-family types (and untyped variables): plain quoted string.
ss << pyformat_string(value);
} else if (!m_parameter.empty()) {
// Parameterized type: bind the type parameter as N around the
// cast, e.g. (lambda N: <cast>)(2)("A | B || C | D").
ss << "(lambda N: " << m_python_cast << ")(" << m_parameter << ")("
<< pyformat_string(value) << ")";
} else {
// User-defined :python_cast expression, applied to the raw value.
// The cast is applied to an empty value too, so e.g. a split lambda
// yields [] for an unsupplied argument.
ss << m_python_cast << "(" << pyformat_string(value) << ")";
}
return ss.str();

View File

@@ -17,23 +17,42 @@ public:
Argtype()
: m_name("default")
, m_desc("default argument type")
, m_symbolic_pattern(".+")
, m_pattern(".+")
, m_symbolic_pattern(R"((?:.|\n)*)")
, m_pattern(R"((?:.|\n)*)")
, m_python_cast("str")
, m_python_format()
, m_regex(std::regex(R"((?:.|\n)*)"))
, m_loc()
{};
Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
std::string python_cast, modify_string_f python_format,
std::string default_value, std::string python_cast, modify_string_f python_format,
const Locator& loc);
std::string python_value(const std::string& var_name, std::vector<std::string> value, size_t name_size);
// True for the match-everything pattern shared by the string family
// (string, rest, literal, and the default type). Validating against
// it is pointless, and running std::regex over a large value (e.g.
// @document's :text holding a whole document) overflows the regex
// executor's recursion stack.
bool matches_all() const { return m_pattern == R"((?:.|\n)*)"; };
std::string m_name {};
std::string m_desc {};
std::string m_symbolic_pattern {};
std::string m_pattern {};
// Default value for parameters of this type; a default given in a
// klammer's parameter declaration overrides it (see
// parse_optional_parameter). Useful for single-purpose types
// (cell_hpos, column_width); general types (bool, float) have no
// sensible universal default and leave it empty.
std::string m_default {};
// Type parameter for parameterized types like rest(2): the value N
// is bound around the python cast as (lambda N: <cast>)(2)(...).
// Empty means unparameterized; a type with a default parameter
// (rest -> "1") is specialized by writing type(N) in a declaration.
std::string m_parameter {};
std::string m_python_cast {};
modify_string_f m_python_format {};
std::regex m_regex {};

View File

@@ -13,7 +13,7 @@
Parameter_set& Argtype_set::parameters()
{
static Parameter_set instance("name | desc :pattern .* :python_cast str");
static Parameter_set instance("name | desc :pattern .* :python_cast str :default");
return instance;
}
@@ -22,8 +22,11 @@ Argtype_set::Argtype_set()
(void)(void)K::log(2);
Locator loc = current_locator();
for (auto [name, desc, pattern, python_cast, python_format] : base_argtypes) {
add(name, desc, pattern, python_cast, python_format, loc);
add(name, desc, pattern, "", python_cast, python_format, loc);
}
// rest is a parameterized type (rest(N)); an unparameterized use is
// one-dimensional.
m_types["rest"].m_parameter = "1";
}
std::string Argtype_set::replace_symbols(const std::string& pattern, const Locator& loc)
@@ -58,8 +61,8 @@ void Argtype_set::check_for_existing_definition(
}
}
void Argtype_set::add(const std::string& name, const std::string& desc,
const std::string& pattern,
void Argtype_set::add(const std::string& name, const std::string& desc,
const std::string& pattern, const std::string& default_value,
const std::string& python_cast, modify_string_f python_format,
const Locator& loc)
{
@@ -68,17 +71,35 @@ void Argtype_set::add(const std::string& name, const std::string& desc,
std::string expanded_pattern = replace_symbols(pattern, loc);
m_name_size = std::max(m_name_size, name.size()); // For display
m_pattern_size = std::max(m_pattern_size, expanded_pattern.size());
m_types[name] = Argtype(name, desc, pattern, expanded_pattern, python_cast, python_format, loc);
try {
m_types[name] = Argtype(name, desc, pattern, expanded_pattern,
default_value, python_cast, python_format, loc);
} catch (const std::regex_error& e) {
std::stringstream ss {};
ss << "The pattern for argument type \"" << name
<< "\" is not a valid regular expression (" << e.what() << "):\n"
<< " " << pattern << "\n";
if (pattern != expanded_pattern) {
ss << "expanded to:\n " << expanded_pattern << "\n";
}
throw Definition_error(ss.str(), loc, false);
}
if (!default_value.empty() &&
!std::regex_match(default_value, m_types[name].m_regex)) {
std::stringstream ss {};
ss << "The default value \"" << default_value << "\" for argument type \""
<< name << "\" does not match its own pattern:\n"
<< " " << pattern << "\n";
throw Definition_error(ss.str(), loc, false);
}
m_names.push_back(name);
}
void Argtype_set::add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms)
{
(void)K::log(3);
//Argument_set parameters("name | desc :pattern .* :python_cast str");
auto [positional, optional, rest] =
argument_split(begin + 1, end); //, Argtype_set::parameters.m_positional.size());
argument_split(begin + 1, end - 1); //, Argtype_set::parameters.m_positional.size());
check_for_existing_definition(positional[0][0].m_text, begin->m_loc);
@@ -86,11 +107,8 @@ void Argtype_set::add(std::vector<Katom>::iterator begin, std::vector<Katom>::it
// std::for_each(begin, end+1, [](Katom& k) { k.m_type = katom_t::replaced; });
modify_string_f pyformat {};
std::string pycast {};
add(values["name"], values["desc"], values["pattern"],
pycast, pyformat,
add(values["name"], values["desc"], values["pattern"], values["default"],
values["python_cast"], modify_string_f{},
begin->m_loc);
modify_type(katom_t::replaced, begin, end);

View File

@@ -17,8 +17,9 @@ public:
std::string replace_symbols(const std::string& pattern, const Locator& loc);
void check_for_existing_definition(const std::string& name, const Locator& loc);
void add(const std::string& name, const std::string& desc,
const std::string& pattern, const std::string& python_cast, modify_string_f python_format,
void add(const std::string& name, const std::string& desc,
const std::string& pattern, const std::string& default_value,
const std::string& python_cast, modify_string_f python_format,
const Locator& loc);
void add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
@@ -63,7 +64,7 @@ std::vector<std::tuple<std::string, std::string, std::string, std::string, modif
pyformat_number },
{ "float", "a floating-point number",
R"([-+]?\d+(\.\d*)?)", "float",
R"([-+]?(\d+(\.\d*)?|\.\d+))", "float",
pyformat_number },
{ "fraction", "a number in the form 'n/d'",
@@ -78,9 +79,11 @@ std::vector<std::tuple<std::string, std::string, std::string, std::string, modif
R"((?:.|\n)*)", "",
pyformat_dlist },
{ "rest", "a list of strings delimited by the bar character",
R"(.*)", "str",
pyformat_string },
{ "rest", "the remaining arguments as a list nested to N dimensions "
"(rest(N)); the delimiter for dimension n is a run of n bar "
"characters, so | separates elements and || lists of elements",
R"((?:.|\n)*)", R"((lambda s : __import__("kutil").rest_split(s, N)))",
nullptr },
{ "literal", "literal text passed without interpretation",
R"((?:.|\n)*)", "str",

View File

@@ -30,6 +30,10 @@ public:
Argtype m_argtype; // {};
bool m_optional {};
std::string m_default {};
// True when m_default was filled from the argument type's :default
// rather than declared in the klammer's parameter list (for kdesc
// provenance display).
bool m_default_from_type {};
Locator m_loc;
std::string m_target {};
};

View File

@@ -17,16 +17,44 @@ bool operator==(Parameter_set lhs, Parameter_set rhs)
std::regex parameter_regex(bool optional=false)
{
//std::string pattern = R"(([A-Za-z]\w*)(?:(\.\w*)(?:(\.\w*)))?)";
std::string pattern = R"((?:([A-Za-z]\w*))|(?:([A-Za-z]\w*)\.(\w+))|(?:([A-Za-z]\w*)\.(\w+)\.(\w+)))";
// The type component may carry a numeric type parameter, e.g.
// rows.rest(2) (see resolve_argtype below). In the optional
// three-part form the type may be empty (:paper_size..tex).
std::string type = R"(\w+(?:\(\d+\))?)";
std::string opt_type = R"(\w*(?:\(\d+\))?)";
std::string pattern = R"((?:([A-Za-z]\w*))|(?:([A-Za-z]\w*)\.()" + type +
R"())|(?:([A-Za-z]\w*)\.()" + type + R"()\.(\w+)))";
if (optional) {
pattern = R"((?::([A-Za-z]\w*))|(?::([A-Za-z]\w*)\.(\w+))|(?::([A-Za-z]\w*)\.(\w*)\.(\w+)))";
// x x
pattern = R"((?::([A-Za-z]\w*))|(?::([A-Za-z]\w*)\.()" + type +
R"())|(?::([A-Za-z]\w*)\.()" + opt_type + R"()\.(\w+)))";
}
// (void)K::log(3, pattern);
return std::regex(pattern);
}
// Look up an argument type, specializing a parameterized use such as
// rest(2): the base type is copied, its type parameter set, and its
// display name extended, so kdesc signatures show rows.rest(2).
static Argtype resolve_argtype(
const std::string& type_text, const Argtype_set& argtypes, const Locator& loc)
{
static const std::regex parameterized(R"((\w+)\((\d+)\))");
std::smatch match {};
if (std::regex_match(type_text, match, parameterized)) {
Argtype argtype = argtypes.get(match[1], loc);
argtype.m_parameter = match[2];
argtype.m_name += "(" + std::string(match[2]) + ")";
return argtype;
}
return argtypes.get(type_text, loc);
}
// True for the rest type in any parameterization (rest, rest(2), ...).
static bool is_rest(const Argtype& argtype)
{
return argtype.m_name == "rest" || argtype.m_name.rfind("rest(", 0) == 0;
}
Parameter_set::Parameter_set(const std::string parameter_string)
{
(void)K::log(3);
@@ -77,7 +105,7 @@ Parameter parse_positional_parameter(const katom_list& katoms, const Argtype_set
if (match_type.empty()) {
match_type = "string";
}
return Parameter(match_name, argtypes.get(match_type, k.m_loc), k.m_loc);
return Parameter(match_name, resolve_argtype(match_type, argtypes, k.m_loc), k.m_loc);
}
}
@@ -105,8 +133,19 @@ Parameter parse_optional_parameter(const katom_list& katoms, const Argtype_set&
if (match_type.empty()) {
match_type = "string";
}
return Parameter(match_name, argtypes.get(match_type, k.m_loc),
k.m_loc, true, default_value);
Parameter parameter(match_name, resolve_argtype(match_type, argtypes, k.m_loc),
k.m_loc, true, default_value);
// Two-level default resolution: a default declared in the
// parameter list wins; otherwise the argument type's :default
// fills in. Both are validated here, at definition time, so an
// invalid default cannot reach an application.
if (default_value.empty() && !parameter.m_argtype.m_default.empty()) {
parameter.m_default = parameter.m_argtype.m_default;
parameter.m_default_from_type = true;
} else if (!default_value.empty()) {
Parameter_set::validate(parameter, default_value, k.m_loc);
}
return parameter;
}
}
@@ -213,7 +252,7 @@ void Parameter_set::parse_parameters(const katom_list& katoms, const Argtype_set
auto [positional, optional] = parameter_split(katoms.cbegin(), katoms.cend());
for (auto req : positional) {
auto pos = parse_positional_parameter(req, argtypes);
if (pos.m_argtype.m_name == "rest") {
if (is_rest(pos.m_argtype)) {
m_rest.push_back(pos);
} else {
m_positional.push_back(pos);
@@ -295,6 +334,13 @@ argument_split(katom_list::const_iterator kbegin, katom_list::const_iterator ken
} else if (positional.size() < positional_limit) {
positional.push_back(trim_part(p));
} else {
// Parts were trimmed, so adjacent parts would abut their bar
// katoms (a row separator "||" next to an empty cell's "|"
// would serialize as "|||"). A space keeps the writer's
// bar/double-bar distinction parseable.
if (!rest.empty()) {
rest.push_back(Katom(" ", katom_t::space, p[0].m_loc));
}
rest.insert(rest.end(), p.begin(), p.end());
}
}
@@ -386,9 +432,59 @@ Parameter_set::value_map(
throw Argument_error(ss.str(), loc);
}
}
for (const auto& [name, value] : values) {
const Parameter* parameter = find(name);
if (parameter) {
validate(*parameter, value, loc);
}
}
return values;
}
// Check an argument value against its argument type's pattern. An empty
// value (an unsupplied optional argument without a default) is not checked.
// The error message includes the argument type's description from its
// @@@argtype definition, so the .k description text is what the writer
// sees when a complicated value (e.g. a table line specification) is wrong.
// The value-size limit guards against std::regex stack overflow: the
// libstdc++ executor recurses per character, so a pattern applied to a
// very large value crashes. Typed argument values are short; large
// values are content (rest, :text) whose types match everything and are
// excluded by matches_all() anyway.
const size_t validation_size_limit = 4096;
void Parameter_set::validate(
const Parameter& parameter, const std::string& value, const Locator& loc)
{
if (value.empty() || value.size() > validation_size_limit) {
return;
}
const Argtype& argtype = parameter.m_argtype;
if (argtype.matches_all()) {
return;
}
if (!std::regex_match(value, argtype.m_regex)) {
std::stringstream ss {};
ss << "The value \"" << value << "\" given for the argument \""
<< parameter.m_name << "\" does not match the \"" << argtype.m_name
<< "\" argument type:\n\n"
<< trim(argtype.m_desc) << "\n";
throw Argument_error(ss.str(), loc, false);
}
}
const Parameter* Parameter_set::find(const std::string& name) const
{
for (const auto& params : {&m_positional, &m_optional, &m_rest}) {
for (const Parameter& p : *params) {
if (p.m_name == name) {
return &p;
}
}
}
return nullptr;
}
// Parameter/argument substitution
std::string replace_arguments(

View File

@@ -34,6 +34,9 @@ public:
const std::vector<std::vector<Katom>>& optional,
const std::vector<Katom>& rest,
const Locator& loc);
const Parameter* find(const std::string& name) const;
static void validate(
const Parameter& parameter, const std::string& value, const Locator& loc);
bool empty() const { return m_katoms.size() == 0; };
std::vector<Katom> m_katoms {};

View File

@@ -145,6 +145,40 @@ void Argv::opt(const std::string& name, const std::string& desc, const std::stri
update_width(arg);
}
void Argv::var(const std::string& name, const std::string& desc)
{
(void)K::log(2, name, desc);
Arg arg {};
arg.m_type = "var";
arg.m_name = name;
arg.m_desc = get_regex_desc(desc);
arg.m_syntax = arg.symbol();
m_args[name] = arg;
m_names.push_back(name);
m_var_names.push_back(name);
m_hyphen_markers.push_back(flag_name(name));
update_width(arg);
}
void Argv::parse_vars(strings_t& words, string_map& named_args)
{
for (const std::string& name : m_var_names) {
auto it = std::ranges::find(words, flag_name(name));
named_args[name] = "";
if (it == words.end()) {
continue;
}
m_given.insert(name);
auto first = it + 1;
auto last = first;
while (last != words.end() && !(*last).empty() && (*last)[0] != '-') {
last++;
}
named_args[name] = join(strings_t(first, last), " ");
words.erase(it, last);
}
}
void Argv::usage_line(Arg arg)
{
std::cout.fill(' ');
@@ -330,6 +364,7 @@ Argv::classify_arguments(int argc, char* argv[], bool full_parse)
if (full_parse) {
check_flags_and_options(argv[0], words);
}
parse_vars(words, named_args);
parse_flags(words, named_args);
parse_optional(words, named_args);
parse_positional(argv_to_string(argc, argv), join(words, " "), named_args);

View File

@@ -3,6 +3,7 @@
// Delusions of generality, but it's really just for Klammertext commands.
#include <map>
#include <set>
#include <ranges>
#include <algorithm>
#include <regex>
@@ -52,12 +53,23 @@ public:
void flag(const std::string& name, const std::string& desc);
void req(const std::string& name, const std::string& desc, const std::string& regex_pattern="'text'");
void opt(const std::string& name, const std::string& desc="", const std::string& parameter="", const std::string& default_value="", const std::string& regex_pattern="text");
// A variadic option: --name collects every following word up to the
// next -/-- token (zero or more). get(name) returns the words
// space-joined; given(name) distinguishes "--name with no words"
// from an absent --name. Used for subcommand-style interfaces
// (kdesc --font install <dir>).
void var(const std::string& name, const std::string& desc);
void update_width(Arg arg);
void check_flags_and_options(std::string command, std::vector<std::string>& words);
void parse_flags(std::vector<std::string>& words, std::map<std::string, std::string>& named_args);
void parse_optional(std::vector<std::string>& words, std::map<std::string, std::string>& named_args);
void parse_vars(std::vector<std::string>& words, std::map<std::string, std::string>& named_args);
// True when the named variadic option appeared on the command line
// (even with no following words).
bool given(const std::string& name) { return m_given.contains(name); };
void parse_positional(
std::string command, // std::vector<std::string> words,
@@ -98,6 +110,8 @@ public:
std::vector<std::string> m_req_names {};
std::vector<std::string> m_flag_names {};
std::vector<std::string> m_opt_names {};
std::vector<std::string> m_var_names {};
std::set<std::string> m_given {};
std::vector<std::string> m_hyphen_markers {};
long unsigned int m_syntax_size = 0;
};

12
mac/env/lsan.supp vendored
View File

@@ -1,12 +0,0 @@
# LeakSanitizer suppressions for Klammertext debug builds.
#
# Suppress unactionable leaks from the embedded Python interpreter and
# OpenImageIO module initialization — one-time-init allocations that
# libraries conventionally leave for the OS to reclaim at exit. Real
# leaks in Klammertext's own C++ code are still reported.
#
# Each "leak:<pattern>" line suppresses any leak whose stack trace has
# a frame matching the substring (in a function name or library path).
leak:libpython
leak:OpenImageIO

68
mac/env/makefile.env vendored
View File

@@ -1,68 +0,0 @@
# mac/env/makefile.env — single cross-platform build environment for Klammertext.
#
# Included identically by every component Makefile:
# include $(KLAMMERTEXT_HOME)/mac/env/makefile.env
#
# The platform is auto-detected with uname; the compiler is chosen with
# make COMPILER=gcc (default)
# make COMPILER=clang
# There is no per-host/per-OS file and no DESKTOP_SESSION/HOST/SITE selector.
ifndef KLAMMERTEXT_HOME
$(error KLAMMERTEXT_HOME is not set -- source mac/env/runtime.env first)
endif
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
UNAME_S := $(shell uname -s)
CPP_VERSION := c++20
# Default compiler: clang on macOS, gcc elsewhere. gcc-built Klammertext
# binaries crash at runtime on macOS (a documented gcc/macOS codegen issue), so
# gcc there is only a compile-time conformance check (via `./dbg/rebuild.sh
# gcc`, which compiles under g++ then rebuilds with clang). Override with
# `make COMPILER=...`.
ifeq ($(UNAME_S),Darwin)
COMPILER ?= clang # gcc | clang
else
COMPILER ?= gcc # gcc | clang
endif
# Python: version- and platform-agnostic, no hardcoded paths.
PYTHON_INC := $(shell python3-config --includes)
PYTHON_LIB := $(shell python3-config --ldflags --embed)
# Flags common to all platforms.
CPPFLAGS = -I$(KLAMMERTEXT_HOME)/mac $(PYTHON_INC)
CXXFLAGS = -Wall -Wextra -Weffc++ -Wshadow -std=$(CPP_VERSION) -fPIC $(OPTIMIZE) $(SANITIZE)
ifeq ($(UNAME_S),Darwin)
# ---------- macOS (Apple Silicon) ----------
# Package-manager prefix, auto-detected: Homebrew (/opt/homebrew) or MacPorts
# (/opt/local). Override with `make MACOS_PREFIX=...`. Only the prefix differs
# between the two: the compiler is Apple Clang either way, and all Python flags
# come from python3-config (prefix-agnostic), so nothing else is PM-specific.
MACOS_PREFIX ?= $(shell test -d /opt/homebrew && echo /opt/homebrew || (test -d /opt/local && echo /opt/local || echo /opt/homebrew))
# Newest optional gcc (conformance check only): Homebrew g++-NN or MacPorts g++-mp-NN.
GCC := $(shell g=$$(ls $(MACOS_PREFIX)/bin/g++-[0-9]* $(MACOS_PREFIX)/bin/g++-mp-[0-9]* 2>/dev/null | sort -V | tail -1); echo $${g:-g++})
CLANG := clang++ # Apple clang (or brew/port llvm)
CPPFLAGS += -I$(MACOS_PREFIX)/include
LDFLAGS = -L$(MACOS_PREFIX)/lib
LDLIBS = $(if $(NOPYTHON),,$(PYTHON_LIB))
SHARED = -dynamiclib
SONAME = -install_name @rpath/$(notdir $@)
ORIGIN := @loader_path
EXPORT_DYNAMIC = -Wl,-export_dynamic
else
# ---------- Linux ----------
GCC := /usr/bin/g++
CLANG := $(shell command -v clang++-18 2>/dev/null || command -v clang++ 2>/dev/null)
LDFLAGS = -L/usr/lib/x86_64-linux-gnu
LDLIBS = -ldl $(if $(NOPYTHON),,$(PYTHON_LIB))
SHARED = -shared
SONAME = -Wl,-soname,$(notdir $@)
ORIGIN := $$ORIGIN
EXPORT_DYNAMIC = -rdynamic
endif
# Resolve the compiler choice (gcc by default; clang with COMPILER=clang).
CXX := $(if $(filter clang,$(COMPILER)),$(CLANG),$(GCC))

View File

@@ -1,46 +0,0 @@
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
CPP_VERSION = c++20
#GCC_LIB = /usr/lib/gcc/x86_64-linux-gnu/7.4.0
CXX = /usr/bin/g++
#IMPORT = -fmodules -fsearch-include-path bits/std.cc
IMPORT =
# Hack for now; to be generalized:
ifneq ("$(wildcard /usr/include/python3.14)","")
PYTHON = python3.14
else ifneq ("$(wildcard /usr/include/python3.13)","")
PYTHON = python3.13
else
PYTHON = python3.12
endif
# -no-pie?
CXXFLAGS = -Wall -Wextra -Weffc++ -fPIC $(PROFILE) -std=$(CPP_VERSION) $(IMPORT) $(OPTIMIZE) \
-I$(KLAMMERTEXT_HOME)/mac \
-I/usr/include \
-I/usr/include/$(PYTHON)
# -L$(GCC_LIB) \
LDFLAGS = \
-L/usr/lib/x86_64-linux-gnu
LDLIBS = \
-ldl
ifndef NOPYTHON
LDLIBS += -l$(PYTHON)
endif
#GCC_ROOT = /h/dev/pkg/gcc-$(GCC_VERSION)
#GCC_LIB = $(GCC_ROOT)/$(GCC_DIR)/lib/gcc/$(GCC_DIR)/$(GCC_VERSION)
#$(GCC_LIB)
LD_LIBRARY_PATH=\
/usr/lib64\
:/usr/lib/x86_64-linux-gnu

View File

@@ -1,33 +0,0 @@
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
CPP_VERSION = c++20
CXX = /usr/bin/g++
# Hack for now; to be generalized:
ifneq ("$(wildcard /usr/include/python3.14)","")
PYTHON = python3.14
else ifneq ("$(wildcard /usr/include/python3.13)","")
PYTHON = python3.13
else
PYTHON = python3.12
endif
CPPFLAGS = \
-I$(KLAMMERTEXT_HOME)/mac \
-I/usr/include \
-I/usr/include/$(PYTHON)
CXXFLAGS = -Wall -Wextra -Weffc++ -Wshadow -std=$(CPP_VERSION) -fPIC $(OPTIMIZE) $(SANITIZE)
LDFLAGS = \
-L/usr/lib/x86_64-linux-gnu
LDLIBS = \
-ldl
ifndef NOPYTHON
LDLIBS += -l$(PYTHON)
endif

View File

@@ -1,33 +0,0 @@
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
CPP_VERSION = c++20
CXX = /usr/bin/g++
# Hack for now; to be generalized:
ifneq ("$(wildcard /usr/include/python3.14)","")
PYTHON = python3.14
else ifneq ("$(wildcard /usr/include/python3.13)","")
PYTHON = python3.13
else
PYTHON = python3.12
endif
CPPFLAGS = \
-I$(KLAMMERTEXT_HOME)/mac \
-I/usr/include \
-I/usr/include/$(PYTHON)
CXXFLAGS = -Wall -Wextra -Weffc++ -Wshadow -std=$(CPP_VERSION) -fPIC $(OPTIMIZE) $(SANITIZE)
LDFLAGS = \
-L/usr/lib/x86_64-linux-gnu
LDLIBS = \
-ldl
ifndef NOPYTHON
LDLIBS += -l$(PYTHON)
endif

View File

@@ -1,33 +0,0 @@
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
CPP_VERSION = c++20
CXX = /usr/bin/g++
# Hack for now; to be generalized:
ifneq ("$(wildcard /usr/include/python3.14)","")
PYTHON = python3.14
else ifneq ("$(wildcard /usr/include/python3.13)","")
PYTHON = python3.13
else
PYTHON = python3.12
endif
CPPFLAGS = \
-I$(KLAMMERTEXT_HOME)/mac \
-I/usr/include \
-I/usr/include/$(PYTHON)
CXXFLAGS = -Wall -Wextra -Weffc++ -Wshadow -std=$(CPP_VERSION) -fPIC $(OPTIMIZE) $(SANITIZE)
LDFLAGS = \
-L/usr/lib/x86_64-linux-gnu
LDLIBS = \
-ldl
ifndef NOPYTHON
LDLIBS += -l$(PYTHON)
endif

20
mac/env/optimize.env vendored
View File

@@ -1,20 +0,0 @@
# Build mode. The DEFAULT is an optimized -O3 build (what installs and releases
# use). Opt into other modes explicitly:
# DEBUG=1 -> debug build: -O0 -g + AddressSanitizer (development)
# NOPYTHON=1 -> -O0 -g -DNOPYTHON, no ASan (build without the interpreter)
# `override` is used throughout so that a stray command-line or environment
# `OPTIMIZE=...` cannot leak into CXXFLAGS: OPTIMIZE is an internal flag string
# here, no longer a trigger. (A legacy `make OPTIMIZE=1` still yields -O3, since
# it falls through to the optimized default below.)
ifdef DEBUG
override OPTIMIZE := -O0 -g
SANITIZE := -fsanitize=address -fno-omit-frame-pointer
else ifdef NOPYTHON
override OPTIMIZE := -O0 -g -DNOPYTHON
SANITIZE :=
else
# Performance build (default).
override OPTIMIZE := -O3
SANITIZE :=
endif

72
mac/env/runtime.env vendored
View File

@@ -1,72 +0,0 @@
# mac/env/runtime.env — single self-configuring runtime environment for Klammertext.
#
# Source this from your shell profile (~/.bashrc etc.):
# source /path/to/klammertext/K/mac/env/runtime.env
#
# This is the runtime counterpart of the shared mac/env/makefile.env: one file
# for every machine, with all machine-specific values AUTO-DETECTED. There is no
# per-host runtime.env.<host> file and no HOST/OS/SITE selector.
# - KLAMMERTEXT_HOME : derived from this file's own location (self-locating)
# - KLAMMERTEXT_TEXLIVE_BIN : newest ~/external/texlive/<year>/bin/<arch>
# Machine-unique, non-committable additions (extra library paths such as NVIDIA
# iray, local tools, etc.) go in an optional, gitignored mac/env/runtime.env.local
# sourced at the end -- NOT in this shared file, so one machine's bundled
# libraries can't shadow another's system libraries.
# --- KLAMMERTEXT_HOME: self-locate (bash sets BASH_SOURCE; zsh sets $0) --------
# This file lives at $KLAMMERTEXT_HOME/mac/env/runtime.env, so go up two levels.
_kt_self="${BASH_SOURCE[0]:-$0}"
export KLAMMERTEXT_HOME="$(cd "$(dirname "$_kt_self")/../.." && pwd)"
unset _kt_self
_kt_uname="$(uname -s)"
# --- KLAMMERTEXT_TEXLIVE_BIN: newest installed TeX Live under ~/external -------
# Klammertext's TeX Live installs are named by 4-digit year (2024, 2025, 2026).
# Match only those so unrelated dirs (e.g. a "texlive-2022-min" system install)
# are never selected; sort -V then picks the newest year.
if [ "$_kt_uname" = "Darwin" ]; then
_kt_arch="universal-darwin"
else
_kt_arch="$(uname -m)-linux" # e.g. x86_64-linux
fi
# Use `find` with a -path pattern, NOT a shell glob: on macOS/zsh an unmatched
# glob (no texlive installed) is a hard "no matches found" error, whereas find
# just returns nothing. The -path pattern keeps the 4-digit-year restriction.
_kt_tl="$(find "$HOME/external/texlive" -maxdepth 3 -type d \
-path "*/[0-9][0-9][0-9][0-9]/bin/$_kt_arch" 2>/dev/null \
| sort -V | tail -1)"
[ -n "$_kt_tl" ] && export KLAMMERTEXT_TEXLIVE_BIN="$_kt_tl"
unset _kt_tl _kt_arch
# --- PATH (TeX Live appended only if one was found) ---------------------------
export PATH="$KLAMMERTEXT_HOME/bin:$KLAMMERTEXT_HOME/tst${KLAMMERTEXT_TEXLIVE_BIN:+:$KLAMMERTEXT_TEXLIVE_BIN}:$PATH"
# --- Per-platform: LSan suppressions + shared library search path -------------
# The shared library path deliberately includes ONLY Klammertext's own .so dirs,
# NOT third-party bundled-library dirs. In particular NVIDIA iray's platforms/
# ships its own libfreetype.so and Qt6; if added here they shadow the system
# libraries, and iray's freetype drags in unversioned libharfbuzz.so/libbz2.so
# (absent without -dev packages), which breaks `import OpenImageIO` (SKS @image).
# Machine-specific library paths (iray included) go in runtime.env.local.
if [ "$_kt_uname" = "Darwin" ]; then
# macOS: no LD/DYLD_LIBRARY_PATH needed — libklammertext.so is found via the
# binaries' @loader_path rpath and document.so is dlopen'd by absolute path.
# LeakSanitizer is unsupported on macOS, so LSAN_OPTIONS does not apply.
:
else
# LSan suppressions for unactionable libpython/OpenImageIO leaks (see
# lsan.supp). Real leaks in Klammertext code are still reported; no effect
# in performance builds (OPTIMIZE=1, which disables ASan).
export LSAN_OPTIONS="suppressions=$KLAMMERTEXT_HOME/mac/env/lsan.supp:print_suppressions=0"
export LD_LIBRARY_PATH="$KLAMMERTEXT_HOME/mac:$KLAMMERTEXT_HOME/sks/kutil:$KLAMMERTEXT_HOME/sks/document:$KLAMMERTEXT_HOME/doc/handbook${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
fi
unset _kt_uname
# --- Optional per-machine escape hatch (gitignored, absent by default) ---------
# Use an if-block (not `[ -f ] && .`) so that when the local file is absent this
# file's final exit status is 0 -- otherwise `source runtime.env` returns
# non-zero, breaking `source runtime.env && ...` and `set -e` callers.
if [ -f "$KLAMMERTEXT_HOME/mac/env/runtime.env.local" ]; then
. "$KLAMMERTEXT_HOME/mac/env/runtime.env.local"
fi

602
mac/font_store.cpp Normal file
View File

@@ -0,0 +1,602 @@
// 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;
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;
}
// 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(std::string family_name)
{
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);
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;
file.extension = fs::path(path).extension();
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 = entry.path().extension();
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());
}
}
}

59
mac/font_store.h Normal file
View File

@@ -0,0 +1,59 @@
#pragma once
// The Klammertext font store (infrastructure; see font_store.cpp).
#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
// .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);
// Directories searched for installed fonts (KLAMMERTEXT_FONTS, default
// ~/.klammertext/fonts); the default font set is searched after them.
std::vector<std::string> installed_font_dirs();
// The distribution's default fonts: $KLAMMERTEXT_HOME/fnt.
std::string default_font_dir();
// Every installable font family found across those directories.
std::vector<std::string> available_font_families();
Resolved_font resolve_font(std::string family_name);
void install_resolved_font(const Resolved_font& font, std::string output_dir);
// One font file classified by its internal metadata (name table, OS/2).
struct Font_file {
std::string path {};
std::string family {}; // from name table (nameID 16, else 1)
std::string variant {}; // Regular | Bold | Italic | BoldItalic
std::string extension {}; // ".ttf" or ".otf"
bool variable = false; // has an 'fvar' table
int weight = 0; // OS/2 usWeightClass
std::string note {}; // reason when the file is not installable
};
// Recursively classify every .ttf/.otf under a directory.
std::vector<Font_file> classify_font_files(const std::string& directory);
// Install the classified families from source_dir into dest_dir (default:
// the first KLAMMERTEXT_FONTS directory, created if necessary), in the
// canonical relocatable layout <dest>/<family-kebab>/{Variant}.ttf plus a
// generated <family-kebab>.css with relative urls. Returns a report.
std::string install_fonts(const std::string& source_dir, std::string dest_dir = "");
// Describe every font in the store with provenance and variants.
std::string describe_fonts();
// Write an HTML specimen page for fonts into output_dir. With an empty
// source_dir, samples every available font in the store; otherwise
// classifies and samples the (possibly uninstalled) fonts in source_dir.
std::string write_font_samples(const std::string& output_dir, const std::string& source_dir = "");

View File

@@ -405,7 +405,7 @@ katom_list Machine::apply_klammer(
katom_list result(klammer.m_body[target].begin(), klammer.m_body[target].end());
auto varmap = klammer.m_varmap[target];
m_state.open_frame("Arguments for klammer " + q_(klammer.m_name));
m_state.set(values);
m_state.set(values, klammer.m_parameters);
for (const auto& [name, indices] : varmap) {
std::regex arg("\\*" + name + "\\*");
for (auto i : indices) {

View File

@@ -25,9 +25,9 @@ std::vector<std::string> Frame::names() const
}
void Frame::set(std::string name, std::string value,
std::string delim, std::string desc, Locator loc)
std::string delim, std::string desc, Locator loc, Argtype argtype)
{
Var v(name, value, delim, desc, loc);
Var v(name, value, delim, desc, loc, argtype);
m_vars[name] = v;
}
@@ -70,7 +70,7 @@ void State::close_frame()
}
void State::set(std::string name, std::string value, bool update,
std::string delim, std::string desc, Locator loc)
std::string delim, std::string desc, Locator loc, Argtype argtype)
{
if (m_frames.empty()) {
std::stringstream ss {};
@@ -86,7 +86,7 @@ void State::set(std::string name, std::string value, bool update,
<< q_(current.m_value) << ".";
throw Argument_error(ss.str(), current.m_loc);
}
m_frames[0].set(name, value, delim, desc, loc);
m_frames[0].set(name, value, delim, desc, loc, argtype);
}
void State::set(std::map<std::string, std::string> varmap)
@@ -96,6 +96,16 @@ void State::set(std::map<std::string, std::string> varmap)
}
}
void State::set(const std::map<std::string, std::string>& varmap,
const Parameter_set& parameters)
{
for (const auto& [name, value] : varmap) {
const Parameter* parameter = parameters.find(name);
set(name, value, false, " ", "", Locator(),
parameter ? parameter->m_argtype : Argtype());
}
}
void State::replace(std::string name, std::string value, bool error_if_not_defined)
{
@@ -269,13 +279,8 @@ std::string State::python_code()
<< margin << std::left << std::setw(name_length) << "K_eval_id" << " = "
<< State::class_id++ << "\n";
for (const auto& name : names) {
// auto [var_value, argtype] = value_type(name);
// ss << argtype.python_value(name, var_value, name_length) << "\n";
std::string v = value(name);
v = string_replace(v, "\\", "\\\\");
v = string_replace(v, "\"", "\\\"");
std::string var_value = qq_(v);
ss << margin << std::left << std::setw(name_length) << name << " = " << var_value << "\n";
Var var = get(name);
ss << var.m_argtype.python_value(name, {var.m_value}, name_length) << "\n";
}
}
// msg() << ss.str() << "\n";

View File

@@ -15,12 +15,14 @@ class Var
public:
Var() = default;
Var(std::string name, std::string value=klammerstate::no_value,
std::string delim=":", std::string desc="", Locator loc=Locator())
std::string delim=":", std::string desc="", Locator loc=Locator(),
Argtype argtype=Argtype())
: m_name(name)
, m_value(value)
, m_delim(delim)
, m_desc(desc)
, m_loc(loc)
, m_argtype(argtype)
{};
bool defined();
@@ -29,6 +31,7 @@ public:
std::string m_delim {};
std::string m_desc {};
Locator m_loc {};
Argtype m_argtype {};
};
namespace klammerstate {
@@ -44,7 +47,8 @@ public:
std::vector<std::string> names() const;
void set(std::string name, std::string value,
std::string delim=":", std::string desc="", Locator loc=Locator());
std::string delim=":", std::string desc="", Locator loc=Locator(),
Argtype argtype=Argtype());
std::pair<Var, bool> get(std::string name);
std::string m_name {};
@@ -58,8 +62,11 @@ public:
void open_frame(std::string name);
void close_frame();
void set(std::string name, std::string value, bool update=false,
std::string delim=" ", std::string desc="", Locator loc=Locator());
std::string delim=" ", std::string desc="", Locator loc=Locator(),
Argtype argtype=Argtype());
void set(std::map<std::string, std::string> varmap);
void set(const std::map<std::string, std::string>& varmap,
const Parameter_set& parameters);
void replace(std::string name, std::string value, bool error_if_not_defined=true);
void add_environment_frame();
Var get(std::string name, bool error_if_not_defined=false, Locator loc=Locator());