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.
11 kB · 299 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2000 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! An expression from a configuration file, `FcExpr`, and its evaluation,//! `FcConfigEvaluate`.
const std = @import("std");const Allocator = std.mem.Allocator;const object = @import("object.zig");const value = @import("value.zig");const CharSet = @import("CharSet.zig");const LangSet = @import("LangSet.zig");const Pattern = @import("Pattern.zig");const Object = object.Object;const Value = value.Value;
const Expr = @This();
data: Data,
/// Which pattern a `<test>`, an `<edit>` or a `<name>` refers to.pub const MatchKind = enum { /// The query, before matching. pattern, /// A font, while a match is being prepared. font, /// A font, as it is scanned. scan,};
pub const Op = enum { quest, @"or", @"and", eq, not_eq, contains, not_contains, less, less_eq, more, more_eq, plus, minus, times, divide, not, comma, floor, ceil, round, trunc,
pub fn compare(op: Op) ?value.Compare { return switch (op) { .eq => .eq, .not_eq => .not_eq, .contains => .contains, .not_contains => .not_contains, .less => .less, .less_eq => .less_eq, .more => .more, .more_eq => .more_eq, else => null, }; }};
pub const Data = union(enum) { integer: i32, double: f64, string: []const u8, matrix: [4]*const Expr, range: value.Range, bool: value.Bool, charset: *const CharSet, langset: *const LangSet, nil, /// `<name>`: the first value of an object, from the pattern being /// edited or, with `target="pattern"` in a font match, from the query. field: struct { object: Object, kind: ?MatchKind }, /// `<const>`: a named constant, looked up for the object being tested /// or edited. constant: []const u8, op: struct { op: Op, left: *const Expr, right: ?*const Expr },};
/// What an expression is evaluated against.pub const Context = struct { /// The pattern being edited. p: *const Pattern, /// The query, while a font is being prepared for it. p_pat: ?*const Pattern, /// The object of the test or edit the expression belongs to, which is /// what a `<const>` is looked up for. object: Object, kind: MatchKind, /// Where any value the evaluation creates is allocated: the edited /// pattern's arena, so that it lives as long as the pattern. arena: Allocator,};
fn floorInt(d: f64) i32 { // fontconfig's FcDoubleFloor and friends truncate through an int. return lossyInt(@floor(d));}
fn lossyInt(d: f64) i32 { if (std.math.isNan(d)) return 0; return std.math.lossyCast(i32, d);}
/// `FcConfigEvaluate`.pub fn evaluate(e: *const Expr, ctx: Context) Allocator.Error!Value { switch (e.data) { .integer => |i| return .{ .integer = i }, .double => |d| return .{ .double = d }, .string => |s| return .{ .string = s }, .matrix => |m| { const mtype: Value = .{ .matrix = .identity }; var d: [4]f64 = undefined; for (m, &d) |sub, *out| { const v = value.promote(try sub.evaluate(ctx), mtype, null); if (v != .double) return .void; out.* = v.double; } return .{ .matrix = .{ .xx = d[0], .xy = d[1], .yx = d[2], .yy = d[3] } }; }, .charset => |c| return .{ .charset = c }, .langset => |l| return .{ .langset = l }, .range => |r| return .{ .range = r }, .bool => |b| return .{ .bool = b }, .nil => return .void, .field => |f| { const want = f.kind; if (ctx.kind == .font and want == .pattern) { const pat = ctx.p_pat orelse return .void; return pat.get(f.object, 0) orelse .void; } else if (ctx.kind == .pattern and want == .font) { // fontconfig warns: a <name target="font"> in a pattern // match has nothing to refer to. return .void; } return ctx.p.get(f.object, 0) orelse .void; }, .constant => |name| { if (object.constantFor(name, ctx.object)) |c| return .{ .integer = c.value }; return .void; }, .op => |o| return evaluateOp(o.op, o.left, o.right, ctx), }}
fn evaluateOp(op: Op, left: *const Expr, right: ?*const Expr, ctx: Context) Allocator.Error!Value { switch (op) { .quest => { const cond = try left.evaluate(ctx); if (cond != .bool) return .void; // The branches are the two sides of the right operand, whatever // operator joins them. const r = right orelse return .void; const branches = switch (r.data) { .op => |b| b, else => return .void, }; if (cond.bool != .false) return branches.left.evaluate(ctx); const else_branch = branches.right orelse return .void; return else_branch.evaluate(ctx); }, .eq, .not_eq, .less, .less_eq, .more, .more_eq, .contains, .not_contains => { const vl = try left.evaluate(ctx); const vr = if (right) |r| try r.evaluate(ctx) else Value.void; return .{ .bool = .from(value.compare(vl, op.compare().?, vr, false)) }; }, .@"or", .@"and", .plus, .minus, .times, .divide => { const vl = try left.evaluate(ctx); const vr = if (right) |r| try r.evaluate(ctx) else Value.void; var buf1: value.PromotionBuffer = .{}; var buf2: value.PromotionBuffer = .{}; const a = value.promote(vl, vr, &buf1); const b = value.promote(vr, a, &buf2); if (a.typ() != b.typ()) return .void; switch (a) { .double => |x| { const y = b.double; const d: f64 = switch (op) { .plus => x + y, .minus => x - y, .times => x * y, .divide => x / y, else => return .void, }; // A whole result becomes an integer again. if (std.math.isFinite(d) and d == @trunc(d) and d >= std.math.minInt(i32) and d <= std.math.maxInt(i32)) return .{ .integer = @intFromFloat(d) }; return .{ .double = d }; }, .bool => |x| { const y = b.bool; return switch (op) { .@"or" => .{ .bool = .from(x != .false or y != .false) }, .@"and" => .{ .bool = .from(x != .false and y != .false) }, else => .void, }; }, .string => |x| { if (op != .plus) return .void; return .{ .string = try std.mem.concat(ctx.arena, u8, &.{ x, b.string }) }; }, .matrix => |x| { if (op != .times) return .void; return .{ .matrix = x.multiply(b.matrix) }; }, .charset => |x| { const p = try ctx.arena.create(CharSet); p.* = switch (op) { .plus => try CharSet.unionWith(ctx.arena, x.*, b.charset.*), .minus => try CharSet.subtract(ctx.arena, x.*, b.charset.*), else => return .void, }; return .{ .charset = p }; }, .langset => |x| { const p = try ctx.arena.create(LangSet); p.* = switch (op) { .plus => try LangSet.unionWith(ctx.arena, x, b.langset), .minus => try LangSet.subtract(ctx.arena, x, b.langset), else => return .void, }; return .{ .langset = p }; }, else => return .void, } }, .not => { const v = try left.evaluate(ctx); if (v != .bool) return .void; return .{ .bool = .from(v.bool == .false) }; }, .floor, .ceil, .round, .trunc => { const v = try left.evaluate(ctx); return switch (v) { .integer => v, .double => |d| .{ .integer = switch (op) { .floor => floorInt(d), .ceil => lossyInt(@ceil(d)), .round => floorInt(d + 0.5), .trunc => lossyInt(@trunc(d)), else => unreachable, } }, else => .void, }; }, // A comma is only ever taken apart by whoever holds the list. .comma => return .void, }}
const testing = std.testing;
test "arithmetic and promotion" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); var p: Pattern = .init(testing.allocator); defer p.deinit(); try p.addDouble(.pixelsize, 10.5);
const two: Expr = .{ .data = .{ .integer = 2 } }; const field: Expr = .{ .data = .{ .field = .{ .object = .{ .builtin = .pixelsize }, .kind = null } } }; const times: Expr = .{ .data = .{ .op = .{ .op = .times, .left = &field, .right = &two } } }; const ctx: Context = .{ .p = &p, .p_pat = null, .object = .{ .builtin = .pixelsize }, .kind = .pattern, .arena = arena.allocator() }; try testing.expectEqual(Value{ .integer = 21 }, try times.evaluate(ctx));
const half: Expr = .{ .data = .{ .double = 0.25 } }; const plus: Expr = .{ .data = .{ .op = .{ .op = .plus, .left = &field, .right = &half } } }; try testing.expectEqual(Value{ .double = 10.75 }, try plus.evaluate(ctx));
const round: Expr = .{ .data = .{ .op = .{ .op = .round, .left = &plus, .right = null } } }; try testing.expectEqual(Value{ .integer = 11 }, try round.evaluate(ctx));
const bold: Expr = .{ .data = .{ .constant = "bold" } }; const wctx: Context = .{ .p = &p, .p_pat = null, .object = .{ .builtin = .weight }, .kind = .pattern, .arena = arena.allocator() }; try testing.expectEqual(Value{ .integer = 200 }, try bold.evaluate(wctx)); try testing.expectEqual(Value.void, try bold.evaluate(ctx));}
test "if" { var p: Pattern = .init(testing.allocator); defer p.deinit(); const t: Expr = .{ .data = .{ .bool = .true } }; const a: Expr = .{ .data = .{ .integer = 1 } }; const b: Expr = .{ .data = .{ .integer = 2 } }; const branches: Expr = .{ .data = .{ .op = .{ .op = .quest, .left = &a, .right = &b } } }; const quest: Expr = .{ .data = .{ .op = .{ .op = .quest, .left = &t, .right = &branches } } }; const ctx: Context = .{ .p = &p, .p_pat = null, .object = .{ .builtin = .size }, .kind = .pattern, .arena = p.allocator() }; try testing.expectEqual(Value{ .integer = 1 }, try quest.evaluate(ctx));}