kdesc --typeface from the reflection klammerset; a missing font directory is a row, not an error

Assembled from klammertext-dev daad6273483a. The font-store listing is
rendered from ref/ (one table per search directory, in search order,
with the styles each typeface holds); the search path is described,
not validated, so a KLAMMERTEXT_FONTS directory that does not exist --
the default ~/.klammertext/fonts on a fresh install -- is a row saying
so and the command exits 0. A typeface an earlier directory also holds
is marked shadowed, with the winning directory, in a column present only
when needed. The store's layout convention has one reader,
fonts_in_directory(). Six kdesc suite cases pin the listing.
This commit is contained in:
ack committed 2026-09-23 17:36:35 +02:00
1 parent c016b3e5f3
commit f9af1ff869
7 files changed
+184 -99

No files matched your search

+1 -1
View File
@@ -51,7 +51,7 @@ are regenerated on each release — patches cannot be merged directly.
Report problems (or send patches) to the author; accepted changes are
applied to the development tree and appear in a following snapshot.
This snapshot was assembled from development commit `e99530408517`.
This snapshot was assembled from development commit `daad6273483a`.
## License
+31 -25
View File
@@ -122,31 +122,6 @@ static void klammerset_command(Machine& machine, const strings_t& words)
}
}
static void font_command(const strings_t& words)
{
std::string verb = words.empty() ? "list" : words[0];
size_t n = words.size() - (words.empty() ? 0 : 1);
if (verb == "list" && n == 0) {
std::cout << boldblack << "Fonts\n" << black << describe_fonts();
} else if (verb == "samples" && n == 1) {
std::cout << "Font samples written to "
<< write_font_samples(words[1]) << "\n";
} else if (verb == "samples" && n == 2) {
std::cout << "Font samples written to "
<< write_font_samples(words[2], words[1]) << "\n";
} else if (verb == "install" && (n == 1 || n == 2)) {
std::cout << "Installing fonts from " << words[1] << ":\n"
<< install_fonts(words[1], n == 2 ? words[2] : "");
} else if (verb == "help") {
font_usage();
} else {
std::cout << "Unrecognized typeface command: --typeface " << join(words, " ")
<< "\n\n";
font_usage();
}
}
// The target for the Klammertext kdesc renders for its own output: the
// ansi target (plain text with type styles as escape sequences) when
// stdout is a terminal, the txt target otherwise. The COMMAND decides,
@@ -166,6 +141,37 @@ static Machine ref_machine()
return M;
}
void show_fonts()
{
Machine M = ref_machine();
M.read_string("@klammertext_fonts@");
std::cout << "\n" << M.apply(text_target()) << "\n\n";
}
static void font_command(const strings_t& words)
{
std::string verb = words.empty() ? "list" : words[0];
size_t n = words.size() - (words.empty() ? 0 : 1);
if (verb == "list" && n == 0) {
show_fonts();
} else if (verb == "samples" && n == 1) {
std::cout << "Font samples written to "
<< write_font_samples(words[1]) << "\n";
} else if (verb == "samples" && n == 2) {
std::cout << "Font samples written to "
<< write_font_samples(words[2], words[1]) << "\n";
} else if (verb == "install" && (n == 1 || n == 2)) {
std::cout << "Installing fonts from " << words[1] << ":\n"
<< install_fonts(words[1], n == 2 ? words[2] : "");
} else if (verb == "help") {
font_usage();
} else {
std::cout << "Unrecognized typeface command: --typeface " << join(words, " ")
<< "\n\n";
font_usage();
}
}
void show_special_characters()
{
Machine M = ref_machine();
+26 -70
View File
@@ -141,35 +141,41 @@ static Resolved_font resolve_in_directory(
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()
std::vector<Resolved_font> fonts_in_directory(const std::string& base)
{
strings_t result {};
static const std::regex family_rgx(R"(font-family:\s*'([^']+)')");
auto scan = [&](const std::string& base) {
if (!fs::exists(base)) {
return;
std::vector<Resolved_font> result {};
if (!fs::is_directory(base)) {
return result;
}
strings_t names {};
for (auto& entry : fs::directory_iterator(base)) {
if (entry.path().extension() == ".css" &&
fs::is_directory(base + "/" + entry.path().stem().string())) {
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());
names.push_back(entry.path().stem());
}
}
std::sort(names.begin(), names.end());
for (const std::string& dir_name : names) {
// The directory name is the family name's fallback: classify_from_css
// overwrites it with the name the css declares, when it declares one.
result.push_back(resolve_in_directory(dir_name, dir_name, base));
}
return result;
}
// Every font available for the three @document role parameters, across the
// installed directories and the default font set. The reported name is the
// family name from the CSS (what the writer types).
strings_t available_font_families()
{
strings_t result {};
strings_t bases = installed_font_dirs();
bases.push_back(default_font_dir());
for (const std::string& base : bases) {
for (const Resolved_font& font : fonts_in_directory(base)) {
result.push_back(font.family_name);
}
};
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;
@@ -500,56 +506,6 @@ std::string install_fonts(const std::string& source_dir, std::string dest_dir)
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);
+7 -3
View File
@@ -28,6 +28,13 @@ std::vector<std::string> installed_font_dirs();
std::string default_font_dir();
// Every installable font family found across those directories.
std::vector<std::string> available_font_families();
// Every font ONE base directory holds, in directory-name order: each
// <name>.css beside a <name>/ directory, the family name read from the css
// (the directory name when the css declares none), the styles classified.
// Empty for a directory that does not exist or holds no fonts; the caller
// asks the filesystem which. The store's layout convention lives here, so
// a listing of the store (kdesc --typeface) reads it rather than restating it.
std::vector<Resolved_font> fonts_in_directory(const std::string& base);
// Whether the store can resolve NAME -- written as the family name or as
// the store's directory name, since both resolve. The question a klammer
// asks before it names a typeface that is not shipped (a CJK face), so it
@@ -61,9 +68,6 @@ std::vector<Font_file> classify_font_files(const std::string& directory);
// 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.
+9
View File
@@ -55,3 +55,12 @@
@@katom_rewrite_patterns.* ::
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect katom_rewrite_patterns @ @@
@@klammertext_fonts.k :
List the typefaces the font store can resolve, one table per search
directory in search order -- the KLAMMERTEXT_FONTS directories (by default
$HOME/.klammertext/fonts), then the Klammertext default set -- with the
font styles each holds. A typeface found in an earlier directory shadows
the same one found later, and the later row says so. @@
@@klammertext_fonts.* ::
@eval :cpp *KLAMMERTEXT_HOME*/ref/reflect klammertext_fonts @ @@
+84
View File
@@ -1,4 +1,6 @@
#include <map>
#include <set>
#include <vector>
#include "machine.h"
#include "character.h"
@@ -335,3 +337,85 @@ std::string katom_rewrite_patterns([[maybe_unused]] Machine& machine)
kt << "table@\n";
return kt.str();
}
// A path written into generated Klammertext: the six specials and the
// txt target's nbsp marker ~ quoted with ^, which any ASCII punctuation
// accepts. A font directory is the user's to name, so it may hold any of them.
static std::string kt_literal(const std::string& text)
{
static const std::string quoted = "@|#^:*~";
std::string result {};
for (char c : text) {
if (quoted.find(c) != std::string::npos) {
result += '^';
}
result += c;
}
return result;
}
// The font store as a table per search directory, in search order. The
// search path is DESCRIBED here, not validated: a directory that does not
// exist is a row saying so, never an error, because resolve_font() skips
// one silently (a document renders fine with it) and a fresh install has no
// ~/.klammertext/fonts yet -- the default entry would fail every new user.
// kdesc --klammersets treats a missing search directory the same way.
extern "C"
std::string klammertext_fonts([[maybe_unused]] Machine& machine)
{
std::stringstream kt {};
kt << "@s2 Klammertext typefaces and font styles @\n\n";
// The directory each dir_name was FIRST seen in: the store searches in
// this order and stops at the first hit, so a later one is shadowed.
std::map<std::string, std::string> first_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);
std::string caption = is_default
? "Klammertext default typefaces: " + kt_literal(symbolic_klammertext_filename(base))
: "Installed typeface directory: " + kt_literal(base);
// Rows before the header: the "Shadowed by" column exists only in a
// table that has something to put in it.
struct Row { std::string family, styles, shadowed_by; };
std::vector<Row> rows {};
bool any_shadowed = false;
for (const Resolved_font& font : fonts_in_directory(base)) {
strings_t styles {};
if (!font.regular.empty()) styles.push_back("Regular");
if (!font.bold.empty()) styles.push_back("Bold");
if (!font.italic.empty()) styles.push_back("Italic");
if (!font.bold_italic.empty()) styles.push_back("BoldItalic");
Row row { kt_literal(font.family_name), join(styles, ", "), "" };
if (first_seen.contains(font.dir_name)) {
row.shadowed_by = kt_literal(first_seen[font.dir_name]);
any_shadowed = true;
} else {
first_seen[font.dir_name] = base;
}
rows.push_back(row);
}
kt << "\n@table :caption " << caption << " |\n"
<< "Typeface name | Font styles" << (any_shadowed ? " | Shadowed by" : "")
<< " ||\n";
if (!fs::is_directory(base)) {
kt << "[directory does not exist] ||\n";
} else if (rows.empty()) {
kt << "[no fonts] ||\n";
}
for (const Row& row : rows) {
kt << row.family << " | " << row.styles;
if (any_shadowed) {
kt << " | " << row.shadowed_by;
}
kt << " ||\n";
}
kt << "table@\n\n";
}
return kt.str();
}
+26
View File
@@ -167,6 +167,32 @@ shows "27. an unknown katom word" 'Unrecognized katom command' "$KDE
# the retired-name guard's entries did not match the bare word.
shows "27a. --typeface help names --typeface install" '--typeface install' "$KDESC" --typeface help
shows "27b. an unknown typeface word names the flag" 'Unrecognized typeface command: --typeface' "$KDESC" --typeface nonsense
# --typeface lists the font store as one @table per search directory,
# rendered from ref/ (@klammertext_fonts, 2026-09-23). The search path is
# DESCRIBED, not validated: a directory that does not exist is a row, never
# an error -- resolve_font skips one silently, and a fresh install has no
# ~/.klammertext/fonts yet, so an error there would fail every new user's
# first "kdesc --typeface". A typeface an earlier directory also holds is
# shadowed, and the LATER row says by which directory.
FONTS=$(mktemp -d /tmp/kdesc_fonts.XXXXXX)
shows "27c. --typeface lists the default set" 'Klammertext default typefaces' "$KDESC" --typeface
shows "27d. a missing search directory is a row" 'directory does not exist' \
env KLAMMERTEXT_FONTS="$FONTS/none" "$KDESC" --typeface
env KLAMMERTEXT_FONTS="$FONTS/none" "$KDESC" --typeface >/dev/null 2>&1
if [ $? -eq 0 ]; then
echo "${green}PASS${reset} 27e. ... and exits 0"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} 27e. a missing search directory exits nonzero"; FAIL=$((FAIL+1))
fi
shows "27f. an empty search directory says so" 'no fonts' env KLAMMERTEXT_FONTS="$FONTS" "$KDESC" --typeface
mkdir -p "$FONTS/crimson-pro"
printf "@font-face { font-family: 'Crimson Pro'; }\n" > "$FONTS/crimson-pro.css"
shows "27g. an installed typeface shadows the default one, and the default row says so" \
"^ *Crimson Pro .*$FONTS" env KLAMMERTEXT_FONTS="$FONTS" "$KDESC" --typeface
# Under a search path that installs nothing (the machine's own fonts may
# shadow a default one, so the default path is not a controlled case).
absent "27h. ... and a table with nothing shadowed has no such column" 'Shadowed by' \
env KLAMMERTEXT_FONTS="$FONTS/none" "$KDESC" --typeface
echo
echo "-- provenance: -i shows what the INPUT defines --"