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:
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {};
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
35
mac/argv.cpp
35
mac/argv.cpp
@@ -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);
|
||||
|
||||
14
mac/argv.h
14
mac/argv.h
@@ -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
12
mac/env/lsan.supp
vendored
@@ -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
68
mac/env/makefile.env
vendored
@@ -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))
|
||||
46
mac/env/makefile.env.hollis.DISABLED
vendored
46
mac/env/makefile.env.hollis.DISABLED
vendored
@@ -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
|
||||
33
mac/env/makefile.env.jatke.DISABLED
vendored
33
mac/env/makefile.env.jatke.DISABLED
vendored
@@ -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
|
||||
33
mac/env/makefile.env.pop.DISABLED
vendored
33
mac/env/makefile.env.pop.DISABLED
vendored
@@ -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
|
||||
33
mac/env/makefile.env.ubuntu.DISABLED
vendored
33
mac/env/makefile.env.ubuntu.DISABLED
vendored
@@ -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
20
mac/env/optimize.env
vendored
@@ -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
72
mac/env/runtime.env
vendored
@@ -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
602
mac/font_store.cpp
Normal 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 "
|
||||
<< "äöüß “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());
|
||||
}
|
||||
}
|
||||
}
|
||||
59
mac/font_store.h
Normal file
59
mac/font_store.h
Normal 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 = "");
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
13
mac/state.h
13
mac/state.h
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user