Something went wrong. Try again.
A Zig library that finds fonts as fontconfig does, without it: the same configuration, OpenType and bitmap fonts, and the same answers.
Something went wrong. Try again.
26 kB · 689 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2000 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! The rules a configuration is made of — `<match>` blocks of `<test>`s//! and `<edit>`s, and the `<alias>`es that become them — and the engine//! that applies them, `FcConfigSubstituteWithPat`.
const std = @import("std");const Allocator = std.mem.Allocator;const object = @import("object.zig");const value = @import("value.zig");const str = @import("str.zig");const LangSet = @import("LangSet.zig");const Pattern = @import("Pattern.zig");const Expr = @import("Expr.zig");const Object = object.Object;const Value = value.Value;const ValueList = Pattern.ValueList;const Binding = Pattern.Binding;
pub const MatchKind = Expr.MatchKind;
pub const Qual = enum { any, all, first, not_first };
pub const Test = struct { /// The pattern tested: the one being edited unless this is a font /// match and the test says `target="pattern"`. Null until the rule is /// added, when it takes the rule's own kind. kind: ?MatchKind, qual: Qual, object: Object, compare: value.Compare, ignore_blanks: bool = false, expr: *const Expr,};
pub const EditOp = enum { assign, assign_replace, prepend, prepend_first, append, append_last, delete, delete_all,};
pub const Edit = struct { object: Object, op: EditOp, expr: ?*const Expr, binding: Binding,};
pub const Item = union(enum) { @"test": *Test, edit: *Edit,};
/// One `<match>` (or `<alias>`): its tests and edits in document order.pub const Rule = struct { items: []const Item, /// Set by `RuleSets.add` when the rule opens with a test that the edited /// pattern has a family equal to one string: that string's /// `str.hashIgnoreBlanksAndCase`. A pattern with no family of the same /// hash fails that test before anything else happens, so the rule is not /// tried at all. Most rules are like this, since every family named in /// a generic family's `<alias>` becomes some. family_key: ?u32 = null, /// Whether any edit is of `family`, after which a pattern's families are /// gathered again. edits_family: bool = false,
fn familyKey(items: []const Item, kind: MatchKind) ?u32 { if (items.len == 0) return null; const t = switch (items[0]) { .@"test" => |t| t, .edit => return null, }; if (t.kind != kind or t.qual != .any or t.compare != .eq or !t.object.is(.family)) return null; // Strings equal ignoring case are equal ignoring blanks and case // too, so one hash serves either kind of test. return switch (t.expr.data) { .string => |s| str.hashIgnoreBlanksAndCase(s), else => null, }; }};
/// The rules for each kind of match, in the order they apply.pub const RuleSets = struct { pattern: std.ArrayList(Rule) = .empty, font: std.ArrayList(Rule) = .empty, scan: std.ArrayList(Rule) = .empty,
pub fn get(rs: *RuleSets, kind: MatchKind) *std.ArrayList(Rule) { return switch (kind) { .pattern => &rs.pattern, .font => &rs.font, .scan => &rs.scan, }; }
pub fn getConst(rs: *const RuleSets, kind: MatchKind) []const Rule { return switch (kind) { .pattern => rs.pattern.items, .font => rs.font.items, .scan => rs.scan.items, }; }
/// `FcRuleSetAdd`: a test with no target of its own takes the rule's. pub fn add(rs: *RuleSets, gpa: Allocator, rule: Rule, kind: MatchKind) Allocator.Error!void { for (rule.items) |item| switch (item) { .@"test" => |t| if (t.kind == null) { t.kind = kind; }, .edit => {}, }; var r = rule; r.family_key = Rule.familyKey(rule.items, kind); r.edits_family = for (rule.items) |item| switch (item) { .edit => |ed| if (ed.object.is(.family)) break true, .@"test" => {}, } else false; try rs.get(kind).append(gpa, r); }};
/// Anything a substitution needs from its configuration besides the rules.pub const Environment = struct { /// The languages of the user's locale, the first preferred, for /// `FcConfigGetDefaultLangs`. default_langs: []const []const u8 = &.{}, prgname: ?[]const u8 = null,};
const Slot = struct { elt: ?*Pattern.Elt = null, value: ?*ValueList = null, test_: ?*const Test = null,};
/// Per-object state while one rule is applied: which value a test matched,/// so that the edits after it can act there.const Slots = struct { builtin: [@typeInfo(object.Builtin).@"enum".field_names.len + 1]Slot = @splat(.{}), custom: std.ArrayList(struct { name: []const u8, slot: Slot }) = .empty,
fn get(s: *Slots, gpa: Allocator, o: Object) Allocator.Error!*Slot { switch (o) { .builtin => |b| return &s.builtin[@backingInt(b)], .custom => |n| { for (s.custom.items) |*c| if (std.mem.eql(u8, c.name, n)) return &c.slot; try s.custom.append(gpa, .{ .name = n, .slot = .{} }); return &s.custom.items[s.custom.items.len - 1].slot; }, } }};
/// `FcConfigSubstituteWithPat`: applies every rule of `kind` to `p`. For a/// font match, `p_pat` is the query, which tests and names with/// `target="pattern"` refer to.pub fn substitute( rule_sets: *const RuleSets, env: Environment, p: *Pattern, p_pat: ?*const Pattern, kind: MatchKind,) Allocator.Error!void { const arena = p.allocator();
if (kind == .pattern) { try addDefaultLangs(p, env.default_langs); if (p.findBuiltin(.prgname) == null) { if (env.prgname) |name| _ = try p.addBorrowed(.{ .builtin = .prgname }, .{ .string = name }, .strong, true); } }
var families: Families = .gather(p); for (rule_sets.getConst(kind)) |rule| { if (rule.family_key) |key| if (!families.mayHave(key)) continue; var slots: Slots = .{}; applyRule(rule, &slots, p, p_pat, kind, arena) catch |err| switch (err) { error.NoMatch => {}, error.OutOfMemory => return error.OutOfMemory, }; // Even a rule that did not match may have edited before the test // that failed. if (rule.edits_family) families = .gather(p); }}
/// The `str.hashIgnoreBlanksAndCase` of each of a pattern's families, for/// `Rule.family_key`.const Families = struct { keys: [max]u32 = undefined, len: usize = 0, /// A bit for each key, by its low six bits, which rules out most rules /// without looking through `keys`. bloom: u64 = 0, /// Set when there are more families than fit, and every rule is tried. overflow: bool = false,
const max = 32;
fn gather(p: *const Pattern) Families { var f: Families = .{}; const e = p.findBuiltin(.family) orelse return f; var it = e.iterator(); while (it.next()) |l| { const s = switch (l.value) { .string => |s| s, else => continue, }; if (f.len == max) { f.overflow = true; break; } f.keys[f.len] = str.hashIgnoreBlanksAndCase(s); f.bloom |= bit(f.keys[f.len]); f.len += 1; } return f; }
fn mayHave(f: *const Families, key: u32) bool { if (f.overflow) return true; if (f.bloom & bit(key) == 0) return false; return std.mem.findScalar(u32, f.keys[0..f.len], key) != null; }
fn bit(key: u32) u64 { return @as(u64, 1) << @as(u6, @truncate(key)); }};
/// The default languages are added to the query's `lang`, weakly, unless it/// already has one of them or `und`.fn addDefaultLangs(p: *Pattern, langs: []const []const u8) Allocator.Error!void { // "und" has no orthography, so it is held as an extra language, which // is what keeps it from being contained in every set. var und_buf: value.PromotionBuffer = .{}; const und = value.promote(.{ .string = "und" }, .{ .langset = &LangSet.empty }, &und_buf).langset; for (langs) |lang| { if (p.findBuiltin(.lang)) |e| { var it = e.iterator(); while (it.next()) |l| { switch (l.value) { .langset => |ls| { var one: value.PromotionBuffer = .{}; const want = value.promote(.{ .string = lang }, l.value, &one); if (ls.contains(want.langset)) return; if (ls.contains(und)) return; }, .string => |s| { if (str.eqlIgnoreCase(s, lang)) return; if (str.eqlIgnoreCase(s, "und")) return; }, else => {}, } } } _ = try p.addBorrowed(.{ .builtin = .lang }, .{ .string = lang }, .weak, true); }}
/// Whether any family in `values` is the string `v`. fontconfig keeps a/// hash table of the edited pattern's families for this, and consulting it/// gives a test of the families a slightly different meaning from walking/// them, which is kept: see `matchValueList`.fn familyTableHas(values: ?*ValueList, v: Value, ignore_blanks: bool) bool { const s = switch (v) { .string => |s| s, else => return false, }; var it = ValueList.iterator(values); while (it.next()) |l| { const f = switch (l.value) { .string => |f| f, else => continue, }; if (if (ignore_blanks) str.eqlIgnoreBlanksAndCase(f, s) else str.eqlIgnoreCase(f, s)) return true; } return false;}
/// `FcConfigMatchValueList`: the first value of `values` the test matches,/// or, with `qual="all"`, the first value if it matches them all./// `family_table` is set when the values are the edited pattern's own/// families.fn matchValueList(t: *const Test, values: ?*ValueList, ctx: Expr.Context, family_table: bool) Allocator.Error!?*ValueList { var ret: ?*ValueList = null; var e: ?*const Expr = t.expr; while (e) |expr| { var operand = expr; e = null; switch (expr.data) { .op => |o| if (o.op == .comma) { operand = o.left; e = o.right; }, else => {}, } const v = try operand.evaluate(ctx); if (family_table and t.object.is(.family)) { // With several values in a test, the table makes a value that // is not there undo an earlier match. if (t.compare == .eq or t.compare == .listing) { if (!familyTableHas(values, v, t.ignore_blanks)) { ret = null; continue; } } if (t.compare == .not_eq and t.qual == .all) { ret = if (familyTableHas(values, v, t.ignore_blanks)) null else values; continue; } } var it = ValueList.iterator(values); while (it.next()) |l| { if (value.compare(l.value, t.compare, v, t.ignore_blanks)) { if (ret == null) ret = l; if (t.qual != .all) break; } else if (t.qual == .all) { ret = null; break; } } } return ret;}
/// `FcConfigValues`: an edit's expressions evaluated into a list, dropping/// any that come out void.fn configValues(e: ?*const Expr, binding: Binding, ctx: Expr.Context) Allocator.Error!?*ValueList { var head: ?*ValueList = null; var tail: *?*ValueList = &head; var cur = e; while (cur) |expr| { var operand = expr; cur = null; switch (expr.data) { .op => |o| if (o.op == .comma) { operand = o.left; cur = o.right; }, else => {}, } const v = try operand.evaluate(ctx); if (v == .void) continue; const node = try ctx.arena.create(ValueList); node.* = .{ .value = v, .binding = binding }; tail.* = node; tail = &node.next; } return head;}
/// `FcConfigAdd`: links `newp` in before or after `position`, or at the/// front or back of the list. A `same` binding takes the binding of the/// value at `position`. The whole list is refused if any of it has the/// wrong type for the object.fn configAdd(head: *?*ValueList, position: ?*ValueList, append: bool, newp: ?*ValueList, o: Object) bool { const first = newp orelse return false; const same: Binding = if (position) |pos| pos.binding else .weak; var it = ValueList.iterator(first); while (it.next()) |l| { if (!o.acceptsType(l.value.typ())) return false; if (l.binding == .same) l.binding = same; }
var prev: *?*ValueList = head; if (append) { if (position) |pos| { prev = &pos.next; } else { while (prev.*) |n| prev = &n.next; } } else if (position) |pos| { while (prev.*) |n| { if (n == pos) break; prev = &n.next; } }
var last = first; while (last.next) |n| last = n; last.next = prev.*; prev.* = first; return true;}
/// `FcConfigDel`: unlinks one value.fn configDel(head: *?*ValueList, position: *ValueList) void { var prev: *?*ValueList = head; while (prev.*) |n| { if (n == position) { prev.* = position.next; position.next = null; return; } prev = &n.next; }}
fn patternAdd(p: *Pattern, o: Object, list: ?*ValueList, append: bool) Allocator.Error!void { if (list == null) return; const e = try p.insertElt(o); _ = configAdd(&e.values, null, append, list, o);}
fn patternDel(p: *Pattern, o: Object) void { const e = p.find(o) orelse return; e.values = null;}
/// `FcConfigPatternCanon`: an object left with no values goes.fn patternCanon(p: *Pattern, o: Object) void { const e = p.find(o) orelse return; if (e.values == null) _ = p.del(o);}
fn applyRule( rule: Rule, slots: *Slots, p: *Pattern, p_pat: ?*const Pattern, kind: MatchKind, arena: Allocator,) (Allocator.Error || error{NoMatch})!void { for (rule.items) |item| switch (item) { .@"test" => |t| { const slot = try slots.get(arena, t.object); const m: ?*const Pattern = if (kind == .font and t.kind == .pattern) p_pat else p; const e: ?*Pattern.Elt = if (m) |pat| pat.find(t.object) else null; // A test of the other pattern is never where an edit lands. if (slot.elt == null and kind == t.kind) { slot.elt = e; slot.test_ = t; } // An object the pattern does not have matches qual="all" and // nothing else. const elt = e orelse { if (t.qual == .all) { slot.value = null; continue; } return error.NoMatch; }; const ctx: Expr.Context = .{ .p = m.?, .p_pat = p_pat, .object = t.object, .kind = kind, .arena = arena }; const vl = try matchValueList(t, elt.values, ctx, m == @as(?*const Pattern, p)); if (slot.value == null and kind == t.kind) slot.value = vl; const matched = vl orelse return error.NoMatch; if (t.qual == .first and matched != elt.values) return error.NoMatch; if (t.qual == .not_first and matched == elt.values) return error.NoMatch; }, .edit => |ed| { const slot = try slots.get(arena, ed.object); const ctx: Expr.Context = .{ .p = p, .p_pat = p_pat, .object = ed.object, .kind = kind, .arena = arena }; const l = try configValues(ed.expr, ed.binding, ctx); if (slot.test_) |t| { if (t.kind == .font or kind == .pattern) slot.elt = p.find(t.object); }
switch (ed.op) { .assign, .assign_replace => assign: { if (ed.op == .assign) { if (slot.value) |this_value| { if (slot.elt) |elt| { // Put the new values after the matched // one, then take the matched one out. _ = configAdd(&elt.values, this_value, true, l, ed.object); configDel(&elt.values, this_value); } // Later edits land where the new values went. slot.value = l; break :assign; } } patternDel(p, ed.object); try patternAdd(p, ed.object, l, true); slot.value = null; }, .prepend, .prepend_first => prepend: { if (ed.op == .prepend) { if (slot.value) |pos| { if (slot.elt) |elt| _ = configAdd(&elt.values, pos, false, l, ed.object); break :prepend; } } try patternAdd(p, ed.object, l, false); }, .append, .append_last => append: { if (ed.op == .append) { if (slot.value) |pos| { if (slot.elt) |elt| _ = configAdd(&elt.values, pos, true, l, ed.object); break :append; } } try patternAdd(p, ed.object, l, true); }, .delete, .delete_all => delete: { if (ed.op == .delete) { if (slot.value) |pos| { if (slot.elt) |elt| configDel(&elt.values, pos); break :delete; } } patternDel(p, ed.object); }, } patternCanon(p, ed.object); }, };}
const testing = std.testing;
fn str_expr(s: []const u8) Expr { return .{ .data = .{ .string = s } };}
test "alias: prepend before the matched family" { const gpa = testing.allocator; var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); const a = arena.allocator();
// <alias><family>monospace</family><prefer><family>DejaVu Sans Mono</family></prefer></alias> const mono = try a.create(Expr); mono.* = str_expr("monospace"); const dejavu = try a.create(Expr); dejavu.* = str_expr("DejaVu Sans Mono"); const t = try a.create(Test); t.* = .{ .kind = null, .qual = .any, .object = .{ .builtin = .family }, .compare = .eq, .ignore_blanks = true, .expr = mono }; const ed = try a.create(Edit); ed.* = .{ .object = .{ .builtin = .family }, .op = .prepend, .expr = dejavu, .binding = .weak }; var rs: RuleSets = .{}; try rs.add(a, .{ .items = try a.dupe(Item, &.{ .{ .@"test" = t }, .{ .edit = ed } }) }, .pattern);
var p: Pattern = .init(gpa); defer p.deinit(); try p.addString(.family, "Foo"); try p.addString(.family, "mono space"); try substitute(&rs, .{}, &p, null, .pattern);
try testing.expectEqualStrings("Foo", p.getString(.family, 0).?); try testing.expectEqualStrings("DejaVu Sans Mono", p.getString(.family, 1).?); try testing.expectEqualStrings("mono space", p.getString(.family, 2).?); try testing.expectEqual(Binding.weak, p.getList(.{ .builtin = .family }, 1).?.binding);}
test "assign replaces the matched value, assign_replace everything" { const gpa = testing.allocator; var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); const a = arena.allocator();
const sans = try a.create(Expr); sans.* = str_expr("sans"); const sans_serif = try a.create(Expr); sans_serif.* = str_expr("sans-serif"); const t = try a.create(Test); t.* = .{ .kind = null, .qual = .any, .object = .{ .builtin = .family }, .compare = .eq, .expr = sans }; const ed = try a.create(Edit); ed.* = .{ .object = .{ .builtin = .family }, .op = .assign, .expr = sans_serif, .binding = .same }; var rs: RuleSets = .{}; try rs.add(a, .{ .items = try a.dupe(Item, &.{ .{ .@"test" = t }, .{ .edit = ed } }) }, .pattern);
var p: Pattern = .init(gpa); defer p.deinit(); try p.addString(.family, "Foo"); try p.addString(.family, "Sans"); try p.addString(.family, "Bar"); try substitute(&rs, .{}, &p, null, .pattern); try testing.expectEqualStrings("Foo", p.getString(.family, 0).?); try testing.expectEqualStrings("sans-serif", p.getString(.family, 1).?); try testing.expectEqual(Binding.strong, p.getList(.{ .builtin = .family }, 1).?.binding); try testing.expectEqualStrings("Bar", p.getString(.family, 2).?);
ed.op = .assign_replace; var q: Pattern = .init(gpa); defer q.deinit(); try q.addString(.family, "Foo"); try q.addString(.family, "Sans"); try substitute(&rs, .{}, &q, null, .pattern); try testing.expectEqual(@as(usize, 1), q.findBuiltin(.family).?.count()); try testing.expectEqualStrings("sans-serif", q.getString(.family, 0).?);}
test "a failed test stops the rule; qual=all on a missing object passes" { const gpa = testing.allocator; var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); const a = arena.allocator();
const bold = try a.create(Expr); bold.* = .{ .data = .{ .constant = "bold" } }; const t = try a.create(Test); t.* = .{ .kind = null, .qual = .any, .object = .{ .builtin = .weight }, .compare = .more_eq, .expr = bold }; const yes = try a.create(Expr); yes.* = .{ .data = .{ .bool = .true } }; const ed = try a.create(Edit); ed.* = .{ .object = .{ .builtin = .embolden }, .op = .assign, .expr = yes, .binding = .weak }; var rs: RuleSets = .{}; try rs.add(a, .{ .items = try a.dupe(Item, &.{ .{ .@"test" = t }, .{ .edit = ed } }) }, .pattern);
var p: Pattern = .init(gpa); defer p.deinit(); try substitute(&rs, .{}, &p, null, .pattern); try testing.expect(p.findBuiltin(.embolden) == null); try p.addInteger(.weight, 200); try substitute(&rs, .{}, &p, null, .pattern); try testing.expectEqual(value.Bool.true, p.getBool(.embolden, 0).?);
t.qual = .all; var q: Pattern = .init(gpa); defer q.deinit(); try substitute(&rs, .{}, &q, null, .pattern); try testing.expectEqual(value.Bool.true, q.getBool(.embolden, 0).?);}
test "a rule skipped by family still sees a family an earlier rule added" { const gpa = testing.allocator; var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); const a = arena.allocator();
// Foo gains a family "Bar Sans", and then the rule for "barsans" // (ignoring blanks) has to run although the pattern started without it. const foo = try a.create(Expr); foo.* = str_expr("Foo"); const bar = try a.create(Expr); bar.* = str_expr("Bar Sans"); const bar_test = try a.create(Expr); bar_test.* = str_expr("barsans"); const yes = try a.create(Expr); yes.* = .{ .data = .{ .bool = .true } };
const t1 = try a.create(Test); t1.* = .{ .kind = null, .qual = .any, .object = .{ .builtin = .family }, .compare = .eq, .expr = foo }; const e1 = try a.create(Edit); e1.* = .{ .object = .{ .builtin = .family }, .op = .append, .expr = bar, .binding = .weak }; const t2 = try a.create(Test); t2.* = .{ .kind = null, .qual = .any, .object = .{ .builtin = .family }, .compare = .eq, .ignore_blanks = true, .expr = bar_test }; const e2 = try a.create(Edit); e2.* = .{ .object = .{ .builtin = .embolden }, .op = .assign, .expr = yes, .binding = .weak }; var rs: RuleSets = .{}; try rs.add(a, .{ .items = try a.dupe(Item, &.{ .{ .@"test" = t1 }, .{ .edit = e1 } }) }, .scan); try rs.add(a, .{ .items = try a.dupe(Item, &.{ .{ .@"test" = t2 }, .{ .edit = e2 } }) }, .scan); try testing.expect(rs.scan.items[0].edits_family); try testing.expectEqual(str.hashIgnoreBlanksAndCase("barsans"), rs.scan.items[1].family_key.?);
var p: Pattern = .init(gpa); defer p.deinit(); try p.addString(.family, "Foo"); try substitute(&rs, .{}, &p, null, .scan); try testing.expectEqualStrings("Bar Sans", p.getString(.family, 1).?); try testing.expectEqual(value.Bool.true, p.getBool(.embolden, 0).?);
var q: Pattern = .init(gpa); defer q.deinit(); try q.addString(.family, "Qux"); try substitute(&rs, .{}, &q, null, .scan); try testing.expect(q.findBuiltin(.embolden) == null);}
test "default languages with a language set in the query" { var p = try @import("name.zig").parse(testing.allocator, ":lang=ja"); defer p.deinit(); var rs: RuleSets = .{}; try substitute(&rs, .{ .default_langs = &.{"en"} }, &p, null, .pattern); try testing.expectEqual(@as(usize, 2), p.findBuiltin(.lang).?.count()); try testing.expectEqualStrings("en", p.getString(.lang, 1).?);}
test "default languages" { var p: Pattern = .init(testing.allocator); defer p.deinit(); var rs: RuleSets = .{}; try substitute(&rs, .{ .default_langs = &.{"en"}, .prgname = "zfc" }, &p, null, .pattern); try testing.expectEqualStrings("en", p.getString(.lang, 0).?); try testing.expectEqualStrings("zfc", p.getString(.prgname, 0).?); try substitute(&rs, .{ .default_langs = &.{"en"} }, &p, null, .pattern); try testing.expectEqual(@as(usize, 1), p.findBuiltin(.lang).?.count());}