// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-FileCopyrightText: © 2000 Keith Packard // SPDX-License-Identifier: MIT AND HPND-sell-variant //! The rules a configuration is made of — `` blocks of ``s //! and ``s, and the ``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 `` (or ``): 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 `` 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(); // monospaceDejaVu Sans Mono 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()); }