diff --git a/README.md b/README.md index 70d6bfc..fdd6817 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/com/kdesc.cpp b/com/kdesc.cpp index 719102f..4286498 100644 --- a/com/kdesc.cpp +++ b/com/kdesc.cpp @@ -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(); diff --git a/mac/font_store.cpp b/mac/font_store.cpp index d670187..1d9cdb3 100644 --- a/mac/font_store.cpp +++ b/mac/font_store.cpp @@ -141,35 +141,41 @@ static Resolved_font resolve_in_directory( return {}; } -// Every font available for the three @document role parameters: each -// .css with a matching / 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. +std::vector fonts_in_directory(const std::string& base) +{ + std::vector result {}; + if (!fs::is_directory(base)) { + return result; + } + strings_t names {}; + for (auto& entry : fs::directory_iterator(base)) { + if (entry.path().extension() == ".css" && + fs::is_directory(base + "/" + entry.path().stem().string())) { + names.push_back(entry.path().stem()); + } + } + 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 {}; - static const std::regex family_rgx(R"(font-family:\s*'([^']+)')"); - auto scan = [&](const std::string& base) { - if (!fs::exists(base)) { - return; + 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 (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; @@ -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 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); diff --git a/mac/font_store.h b/mac/font_store.h index 5512830..c5a6f31 100644 --- a/mac/font_store.h +++ b/mac/font_store.h @@ -28,6 +28,13 @@ std::vector installed_font_dirs(); std::string default_font_dir(); // Every installable font family found across those directories. std::vector available_font_families(); +// Every font ONE base directory holds, in directory-name order: each +// .css beside a / 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 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 classify_font_files(const std::string& directory); // generated .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. diff --git a/ref/ref.k b/ref/ref.k index 3bab904..ddfd973 100644 --- a/ref/ref.k +++ b/ref/ref.k @@ -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 @ @@ diff --git a/ref/reflect.cpp b/ref/reflect.cpp index c9ebffa..b297eaf 100644 --- a/ref/reflect.cpp +++ b/ref/reflect.cpp @@ -1,4 +1,6 @@ +#include #include +#include #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 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 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(); +} diff --git a/tst/kdesc_test.sh b/tst/kdesc_test.sh index e949058..709bdf8 100755 --- a/tst/kdesc_test.sh +++ b/tst/kdesc_test.sh @@ -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 --"