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.
38 kB · 1051 lines
Zig
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2002 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! The configuration file format, from `fcxml.c`.//!//! fontconfig parses a configuration with two stacks: one of open elements,//! and one of the values the closed ones left behind. Each element's handler//! runs when the element closes, and takes what it wants from the values its//! children left; whatever it does not take is thrown away with it. That//! design decides what happens to every odd nesting — a value where none is//! expected, a missing operand, an element fontconfig does not know — so it//! is reproduced here rather than replaced with a grammar.//!//! What a file *does* is delegated to a `Loader`: adding directories,//! following `<include>`s, and receiving the rules. Values are allocated in//! the loader's arena, which lives as long as the configuration.
const std = @import("std");const Allocator = std.mem.Allocator;const zxml = @import("zxml");const object = @import("object.zig");const value = @import("value.zig");const str = @import("str.zig");const name_ = @import("name.zig");const CharSet = @import("CharSet.zig");const LangSet = @import("LangSet.zig");const Pattern = @import("Pattern.zig");const Expr = @import("Expr.zig");const rules = @import("rules.zig");const Loader = @import("load.zig").Loader;const Object = object.Object;const Value = value.Value;const MatchKind = Expr.MatchKind;
pub const Severity = enum { info, warning, @"error" };
const Element = enum { none, fontconfig, dir, cachedir, cache, include, config, match, alias, description, @"remap-dir", @"reset-dirs", rescan, prefer, accept, default, family, selectfont, acceptfont, rejectfont, glob, pattern, patelt, @"test", edit, int, double, string, matrix, range, bool, charset, langset, name, @"const", @"or", @"and", eq, not_eq, less, less_eq, more, more_eq, contains, not_contains, plus, minus, times, divide, not, @"if", floor, ceil, round, trunc, unknown,
fn fromName(n: []const u8) Element { if (std.meta.stringToEnum(Element, n)) |e| { if (e != .none and e != .unknown) return e; } // ITS markup, for translations, is ignored. if (std.mem.startsWith(u8, n, "its:")) return .none; return .unknown; }};
const Attr = struct { name: []const u8, value: []const u8,};
const PStack = struct { element: Element, attrs: []Attr, text: std.ArrayList(u8) = .empty,};
const VTag = union(enum) { none, string: []const u8, family: *const Expr, constant: []const u8, glob: []const u8, name: struct { kind: ?MatchKind, object: Object }, pattern: *Pattern, prefer: *const Expr, accept: *const Expr, default: *const Expr, integer: i32, double: f64, matrix: [4]*const Expr, range: value.Range, bool: value.Bool, charset: *const CharSet, langset: *const LangSet, @"test": *rules.Test, expr: *const Expr, edit: *rules.Edit, nil,};
const VStack = struct { tag: VTag, /// The depth of the element whose child pushed this: only that element /// sees it. owner: usize,};
pub const Parser = struct { loader: *Loader, arena: Allocator, filename: []const u8, src: []const u8, reader: zxml.Reader, pstack: std.ArrayList(PStack) = .empty, vstack: std.ArrayList(VStack) = .empty, /// The rules of this file not yet handed to the loader: fontconfig /// hands them over at every `<include>`, so that rules keep the order /// they are written in across files. ruleset: rules.RuleSets = .{}, @"error": bool = false,
fn message(p: *Parser, severity: Severity, comptime fmt: []const u8, args: anytype) void { if (severity == .@"error") p.@"error" = true; const line = 1 + std.mem.count(u8, p.src[0..@min(p.reader.pos, p.src.len)], "\n"); p.loader.message(severity, p.filename, line, fmt, args); }
fn depth(p: *const Parser) usize { return p.pstack.items.len; }
fn top(p: *Parser) *PStack { return &p.pstack.items[p.pstack.items.len - 1]; }
// ---- the value stack
fn push(p: *Parser, tag: VTag) Allocator.Error!void { // Owned by the parent of the element being closed. try p.vstack.append(p.arena, .{ .tag = tag, .owner = p.depth() -| 1 }); }
/// `FcVStackPeek`: the most recent value a child of the current element /// left, if any. fn peek(p: *Parser) ?*VStack { const n = p.vstack.items.len; if (n == 0) return null; const v = &p.vstack.items[n - 1]; return if (v.owner == p.depth()) v else null; }
fn pop(p: *Parser) void { if (p.peek() != null) p.vstack.items.len -= 1; }
/// `FcVStackElements`. fn elements(p: *Parser) usize { var n: usize = 0; var i = p.vstack.items.len; while (i > 0) : (i -= 1) { if (p.vstack.items[i - 1].owner != p.depth()) break; n += 1; } return n; }
fn fetch(p: *Parser, off: usize) *VStack { return &p.vstack.items[p.vstack.items.len - 1 - off]; }
fn clear(p: *Parser) void { while (p.peek() != null) p.pop(); }
// ---- attributes and text
fn attr(p: *Parser, n: []const u8) ?[]const u8 { for (p.top().attrs) |a| { if (std.mem.eql(u8, a.name, n)) return a.value; } return null; }
fn text(p: *Parser) []const u8 { return p.top().text.items; }
fn newExpr(p: *Parser, data: Expr.Data) Allocator.Error!*const Expr { const e = try p.arena.create(Expr); e.* = .{ .data = data }; return e; }
// ---- driving zxml
/// Parses one document. Returns false if it could not be read as XML at /// all, in which case, as in fontconfig, the rules after the last /// `<include>` are dropped. pub fn run(p: *Parser) Allocator.Error!bool { while (true) { const event = p.reader.next() catch |err| { p.message(.@"error", "{s}", .{@errorName(err)}); return false; }; switch (event) { .eof => break, .doctype => |d| { var it = std.mem.tokenizeAny(u8, d, " \t\r\n"); const dname = it.next() orelse ""; if (!std.mem.eql(u8, dname, "fontconfig")) p.message(.@"error", "invalid doctype \"{s}\"", .{dname}); }, .start_element => |e| { const element = Element.fromName(e.name); if (element == .unknown) p.message(.warning, "unknown element \"{s}\"", .{e.name}); const attrs = try p.arena.alloc(Attr, e.attributes.len); for (e.attributes, attrs) |a, *out| { const decoded = zxml.decodeAlloc(p.arena, a.raw_value, .lenient, .attribute) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => blk: { p.message(.@"error", "{s}", .{@errorName(err)}); break :blk try p.arena.dupe(u8, a.raw_value); }, }; out.* = .{ .name = try p.arena.dupe(u8, a.name), .value = decoded }; } try p.pstack.append(p.arena, .{ .element = element, .attrs = attrs }); }, .end_element => try p.endElement(), .text => |t| { if (p.pstack.items.len == 0) continue; const decoded = zxml.decodeAlloc(p.arena, t, .lenient, .text) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => { p.message(.@"error", "{s}", .{@errorName(err)}); return false; }, }; try p.top().text.appendSlice(p.arena, decoded); }, .cdata => |t| { if (p.pstack.items.len == 0) continue; try p.top().text.appendSlice(p.arena, t); }, else => {}, } } return true; }
/// `FcEndElement`. fn endElement(p: *Parser) Allocator.Error!void { if (p.pstack.items.len == 0) return; switch (p.top().element) { .none, .fontconfig, .config, .selectfont, .unknown, .cache => {}, .dir => try p.parseDir(), .cachedir => try p.parseCacheDir(), .include => try p.parseInclude(), .match => try p.parseMatch(), .alias => try p.parseAlias(), .description => {}, .@"remap-dir" => try p.parseRemapDir(), .@"reset-dirs" => p.loader.resetFontDirs(), .rescan => p.parseRescan(), .prefer => try p.parseFamilies(.prefer), .accept => try p.parseFamilies(.accept), .default => try p.parseFamilies(.default), .family => try p.push(.{ .family = try p.newExpr(.{ .string = try p.arena.dupe(u8, p.text()) }) }), .@"test" => try p.parseTest(), .edit => try p.parseEdit(), .int => try p.parseInt(), .double => try p.parseDouble(), .string => try p.parseString(.string), .matrix => try p.parseMatrix(), .range => try p.parseRange(), .bool => try p.parseBool(), .charset => try p.parseCharSet(), .langset => try p.parseLangSet(), .acceptfont => try p.parseAcceptRejectFont(true), .rejectfont => try p.parseAcceptRejectFont(false), .glob => try p.parseString(.glob), .pattern => try p.parsePattern(), .patelt => try p.parsePatelt(), .name => try p.parseName(), .@"const" => try p.parseString(.constant), .@"or" => try p.parseBinary(.@"or"), .@"and" => try p.parseBinary(.@"and"), .eq => try p.parseBinary(.eq), .not_eq => try p.parseBinary(.not_eq), .less => try p.parseBinary(.less), .less_eq => try p.parseBinary(.less_eq), .more => try p.parseBinary(.more), .more_eq => try p.parseBinary(.more_eq), .contains => try p.parseBinary(.contains), .not_contains => try p.parseBinary(.not_contains), .plus => try p.parseBinary(.plus), .minus => try p.parseBinary(.minus), .times => try p.parseBinary(.times), .divide => try p.parseBinary(.divide), .not => try p.parseUnary(.not), .@"if" => try p.parseBinary(.quest), .floor => try p.parseUnary(.floor), .ceil => try p.parseUnary(.ceil), .round => try p.parseUnary(.round), .trunc => try p.parseUnary(.trunc), } // `FcPStackPop`: whatever the children left and nobody took goes. p.clear(); p.pstack.items.len -= 1; }
// ---- leaves
/// `FcParseNil`: `xsi:nil="true"` makes an element a nil value. fn parseNil(p: *Parser) Allocator.Error!bool { const nil = p.attr("xsi:nil") orelse return false; if (!p.lexBool(nil)) return false; try p.push(.nil); return true; }
fn lexBool(p: *Parser, s: []const u8) bool { const b = name_.parseBool(s) orelse { p.message(.warning, "\"{s}\" is not known boolean", .{s}); return false; }; return b != .false; }
fn parseInt(p: *Parser) Allocator.Error!void { if (try p.parseNil()) return; const s = p.text(); // strtol with base 0: decimal, 0x hexadecimal or 0 octal. const parsed = parseCInt(s); if (parsed == null) { p.message(.@"error", "\"{s}\": not a valid integer", .{s}); return; } try p.push(.{ .integer = parsed.? }); }
fn parseDouble(p: *Parser) Allocator.Error!void { if (try p.parseNil()) return; const s = p.text(); const d = str.strtod(s); if (d.len != s.len or s.len == 0) { p.message(.@"error", "\"{s}\": not a valid double", .{s}); return; } try p.push(.{ .double = d.value }); }
const StringTag = enum { string, glob, constant };
fn parseString(p: *Parser, tag: StringTag) Allocator.Error!void { if (try p.parseNil()) return; const s = try p.arena.dupe(u8, p.text()); try p.push(switch (tag) { .string => .{ .string = s }, .glob => .{ .glob = s }, .constant => .{ .constant = s }, }); }
fn parseName(p: *Parser) Allocator.Error!void { var kind: ?MatchKind = null; if (p.attr("target")) |t| { if (std.mem.eql(u8, t, "pattern")) { kind = .pattern; } else if (std.mem.eql(u8, t, "font")) { kind = .font; } else if (std.mem.eql(u8, t, "default")) { kind = null; } else { p.message(.warning, "invalid name target \"{s}\"", .{t}); return; } } try p.push(.{ .name = .{ .kind = kind, .object = Object.fromName(try p.arena.dupe(u8, p.text())) } }); }
fn parseMatrix(p: *Parser) Allocator.Error!void { const yy = p.popExpr(); const yx = p.popExpr(); const xy = p.popExpr(); const xx = p.popExpr(); if (yy == null or yx == null or xy == null or xx == null) { p.message(.warning, "Missing values in matrix element", .{}); return; } if (p.popExpr() != null) { p.message(.@"error", "wrong number of matrix elements", .{}); return; } try p.push(.{ .matrix = .{ xx.?, xy.?, yx.?, yy.? } }); }
fn parseRange(p: *Parser) Allocator.Error!void { var n = [2]u32{ 0, 0 }; var d = [2]f64{ 0, 0 }; var count: i32 = 1; var dflag = false; while (p.peek()) |v| { if (count < 0) { p.message(.@"error", "too many elements in range", .{}); return; } const i: usize = @intCast(count); switch (v.tag) { .integer => |x| { if (dflag) d[i] = @floatFromInt(x) else n[i] = @bitCast(x); }, .double => |x| { if (count == 0 and !dflag) d[1] = @floatFromInt(n[1]); d[i] = x; dflag = true; }, else => { p.message(.@"error", "invalid element in range", .{}); if (dflag) d[i] = 0 else n[i] = 0; }, } count -= 1; p.pop(); } if (count >= 0) { p.message(.@"error", "invalid range", .{}); return; } if (dflag) { if (d[0] > d[1]) { p.message(.@"error", "invalid range", .{}); return; } try p.push(.{ .range = .{ .begin = d[0], .end = d[1] } }); } else { if (n[0] > n[1]) { p.message(.@"error", "invalid range", .{}); return; } try p.push(.{ .range = .{ .begin = @floatFromInt(n[0]), .end = @floatFromInt(n[1]) } }); } }
fn parseBool(p: *Parser) Allocator.Error!void { if (try p.parseNil()) return; try p.push(.{ .bool = .from(p.lexBool(p.text())) }); }
fn parseCharSet(p: *Parser) Allocator.Error!void { var b: CharSet.Builder = .{}; var n: usize = 0; if (!try p.parseNil()) { while (p.peek()) |v| { switch (v.tag) { .integer => |i| { if (i < 0) { p.message(.warning, "invalid character: 0x{x:0>4}", .{@as(u32, @bitCast(i))}); } else { try b.add(p.arena, @intCast(i)); n += 1; } }, .range => |r| { const begin = cast32(r.begin); const end = cast32(r.end); if (begin <= end) { var c = begin; while (c <= end) : (c += 1) { try b.add(p.arena, c); n += 1; if (c == std.math.maxInt(u32)) break; } } }, else => p.message(.@"error", "invalid element in charset", .{}), } p.pop(); } } if (n > 0) { const cs = try p.arena.create(CharSet); cs.* = try b.finish(p.arena); try p.push(.{ .charset = cs }); } }
fn cast32(d: f64) u32 { if (!(d >= 0)) return 0; if (d >= 0x110000) return 0x10ffff; return @intFromFloat(d); }
fn parseLangSet(p: *Parser) Allocator.Error!void { var ls: LangSet = .{}; var n: usize = 0; if (!try p.parseNil()) { while (p.peek()) |v| { switch (v.tag) { .string => |s| { try ls.addInPlace(p.arena, s); n += 1; }, else => p.message(.@"error", "invalid element in langset", .{}), } p.pop(); } } if (n > 0) { const l = try p.arena.create(LangSet); l.* = ls; try p.push(.{ .langset = l }); } }
// ---- expressions
/// `FcPopExpr`: the most recent value turned into an expression, if it /// can be. fn popExpr(p: *Parser) ?*const Expr { const v = p.peek() orelse return null; const e: ?*const Expr = switch (v.tag) { .string => |s| p.newExpr(.{ .string = s }) catch null, .family, .prefer, .accept, .default, .expr => |e| e, .name => |n| p.newExpr(.{ .field = .{ .object = n.object, .kind = n.kind } }) catch null, .constant => |s| p.newExpr(.{ .constant = s }) catch null, .integer => |i| p.newExpr(.{ .integer = i }) catch null, .double => |d| p.newExpr(.{ .double = d }) catch null, .matrix => |m| p.newExpr(.{ .matrix = m }) catch null, .range => |r| p.newExpr(.{ .range = r }) catch null, .bool => |b| p.newExpr(.{ .bool = b }) catch null, .charset => |c| p.newExpr(.{ .charset = c }) catch null, .langset => |l| p.newExpr(.{ .langset = l }) catch null, .nil => p.newExpr(.nil) catch null, .none, .glob, .pattern, .@"test", .edit => null, }; p.pop(); return e; }
/// `FcPopBinary`: every operand joined by `op`, as a right-leaning tree /// in document order. A single operand is returned alone. fn popBinary(p: *Parser, op: Expr.Op) Allocator.Error!?*const Expr { var expr: ?*const Expr = null; while (p.popExpr()) |left| { expr = if (expr) |right| try p.newExpr(.{ .op = .{ .op = op, .left = left, .right = right } }) else left; } return expr; }
fn parseBinary(p: *Parser, op: Expr.Op) Allocator.Error!void { if (try p.popBinary(op)) |e| try p.push(.{ .expr = e }); }
fn parseUnary(p: *Parser, op: Expr.Op) Allocator.Error!void { const operand = p.popExpr() orelse return; try p.push(.{ .expr = try p.newExpr(.{ .op = .{ .op = op, .left = operand, .right = null } }) }); }
// ---- rules
fn lexBinding(p: *Parser, s: ?[]const u8) ?Pattern.Binding { const b = s orelse return .weak; if (std.mem.eql(u8, b, "weak")) return .weak; if (std.mem.eql(u8, b, "strong")) return .strong; if (std.mem.eql(u8, b, "same")) return .same; p.message(.warning, "invalid binding \"{s}\"", .{b}); return null; }
fn parseTest(p: *Parser) Allocator.Error!void { var kind: ?MatchKind = null; if (p.attr("target")) |t| { if (std.mem.eql(u8, t, "pattern")) { kind = .pattern; } else if (std.mem.eql(u8, t, "font")) { kind = .font; } else if (std.mem.eql(u8, t, "scan")) { kind = .scan; } else if (std.mem.eql(u8, t, "default")) { kind = null; } else { p.message(.warning, "invalid test target \"{s}\"", .{t}); return; } } var qual: rules.Qual = .any; if (p.attr("qual")) |q| { qual = std.meta.stringToEnum(rules.Qual, q) orelse { p.message(.warning, "invalid test qual \"{s}\"", .{q}); return; }; } const test_name = p.attr("name") orelse { p.message(.warning, "missing test name", .{}); return; }; var compare: value.Compare = .eq; if (p.attr("compare")) |c| { compare = lexCompare(c) orelse { p.message(.warning, "invalid test compare \"{s}\"", .{c}); return; }; } var ignore_blanks = false; if (p.attr("ignore-blanks")) |ib| ignore_blanks = p.lexBool(ib); const expr = try p.popBinary(.comma) orelse { p.message(.warning, "missing test expression", .{}); return; }; if (isComma(expr)) p.message(.warning, "Having multiple values in <test> isn't supported and may not work as expected", .{}); const t = try p.arena.create(rules.Test); t.* = .{ .kind = kind, .qual = qual, .object = Object.fromName(test_name), .compare = compare, .ignore_blanks = ignore_blanks, .expr = expr, }; try p.push(.{ .@"test" = t }); }
fn isComma(e: *const Expr) bool { return switch (e.data) { .op => |o| o.op == .comma, else => false, }; }
fn lexCompare(c: []const u8) ?value.Compare { const map = [_]struct { []const u8, value.Compare }{ .{ "eq", .eq }, .{ "not_eq", .not_eq }, .{ "less", .less }, .{ "less_eq", .less_eq }, .{ "more", .more }, .{ "more_eq", .more_eq }, .{ "contains", .contains }, .{ "not_contains", .not_contains }, }; for (map) |m| if (std.mem.eql(u8, m[0], c)) return m[1]; return null; }
fn parseEdit(p: *Parser) Allocator.Error!void { const edit_name = p.attr("name") orelse { p.message(.warning, "missing edit name", .{}); return; }; var mode: rules.EditOp = .assign; if (p.attr("mode")) |m| { mode = std.meta.stringToEnum(rules.EditOp, m) orelse { p.message(.warning, "invalid edit mode \"{s}\"", .{m}); return; }; } const binding = p.lexBinding(p.attr("binding")) orelse return; var expr = try p.popBinary(.comma); if ((mode == .delete or mode == .delete_all) and expr != null) { p.message(.warning, "Expression doesn't take any effects for delete and delete_all", .{}); expr = null; } const ed = try p.arena.create(rules.Edit); ed.* = .{ .object = Object.fromName(edit_name), .op = mode, .expr = expr, .binding = binding }; try p.push(.{ .edit = ed }); }
fn addRule(p: *Parser, items: []const rules.Item, kind: MatchKind) Allocator.Error!void { try p.ruleset.add(p.arena, .{ .items = items }, kind); }
fn parseMatch(p: *Parser) Allocator.Error!void { var kind: MatchKind = .pattern; if (p.attr("target")) |t| { kind = std.meta.stringToEnum(MatchKind, t) orelse { p.message(.warning, "invalid match target \"{s}\"", .{t}); return; }; } // Popped from the end, so collected backwards. var items: std.ArrayList(rules.Item) = .empty; while (p.peek()) |v| { switch (v.tag) { .@"test" => |t| try items.append(p.arena, .{ .@"test" = t }), .edit => |ed| { if (kind == .scan and ed.object == .custom) { p.message(.@"error", "<match target=\"scan\"> cannot edit user-defined object \"{s}\"", .{ed.object.name()}); return; } try items.append(p.arena, .{ .edit = ed }); }, else => p.message(.warning, "invalid match element", .{}), } p.pop(); } if (items.items.len == 0) { p.message(.warning, "No <test> nor <edit> elements in <match>", .{}); return; } std.mem.reverse(rules.Item, items.items); try p.addRule(items.items, kind); }
fn parseFamilies(p: *Parser, comptime tag: enum { prefer, accept, default }) Allocator.Error!void { var expr: ?*const Expr = null; while (p.peek()) |v| { switch (v.tag) { .family => |left| { expr = if (expr) |right| try p.newExpr(.{ .op = .{ .op = .comma, .left = left, .right = right } }) else left; }, else => p.message(.warning, "non-family", .{}), } p.pop(); } if (expr) |e| try p.push(switch (tag) { .prefer => .{ .prefer = e }, .accept => .{ .accept = e }, .default => .{ .default = e }, }); }
fn parseAlias(p: *Parser) Allocator.Error!void { const binding = p.lexBinding(p.attr("binding")) orelse return; var family: ?*const Expr = null; var prefer: ?*const Expr = null; var accept: ?*const Expr = null; var def: ?*const Expr = null; var tests: std.ArrayList(rules.Item) = .empty; while (p.peek()) |v| { switch (v.tag) { .family => |f| { if (family) |existing| { p.message(.warning, "Having multiple <family> in <alias> isn't supported and may not work as expected", .{}); family = try p.newExpr(.{ .op = .{ .op = .comma, .left = f, .right = existing } }); } else family = f; }, .prefer => |e| prefer = e, .accept => |e| accept = e, .default => |e| def = e, .@"test" => |t| try tests.append(p.arena, .{ .@"test" = t }), else => p.message(.warning, "bad alias", .{}), } p.pop(); } const fam = family orelse { p.message(.@"error", "missing family in alias", .{}); return; }; if (prefer == null and accept == null and def == null) return;
std.mem.reverse(rules.Item, tests.items); const t = try p.arena.create(rules.Test); t.* = .{ .kind = .pattern, .qual = .any, .object = .{ .builtin = .family }, .compare = .eq, .ignore_blanks = true, .expr = fam }; try tests.append(p.arena, .{ .@"test" = t });
const edits = [_]struct { ?*const Expr, rules.EditOp }{ .{ prefer, .prepend }, .{ accept, .append }, .{ def, .append_last }, }; for (edits) |entry| { const expr = entry[0] orelse continue; const ed = try p.arena.create(rules.Edit); ed.* = .{ .object = .{ .builtin = .family }, .op = entry[1], .expr = expr, .binding = binding }; try tests.append(p.arena, .{ .edit = ed }); try p.aliasForGenericFamily(fam, expr, binding); } try p.addRule(tests.items, .pattern); }
/// `FcParseAliasForGenericFamily`: when the alias is for a generic family /// — `monospace`, `serif` — every family it names is marked, as it is /// scanned, as belonging to that generic family. fn aliasForGenericFamily(p: *Parser, test_expr: *const Expr, edit: *const Expr, binding: Pattern.Binding) Allocator.Error!void { const generic = switch (test_expr.data) { .string => |s| s, else => return, }; if (object.constantFor(generic, .{ .builtin = .genericfamily }) == null) return; try p.genericFamilyWalk(edit, generic, binding); }
fn genericFamilyWalk(p: *Parser, edit_in: *const Expr, generic: []const u8, binding: Pattern.Binding) Allocator.Error!void { var edit: ?*const Expr = edit_in; while (edit) |e| { switch (e.data) { .string => { try p.aliasAddScanRule(e, generic, binding); break; }, .op => |o| { if (o.op != .comma) break; try p.genericFamilyWalk(o.left, generic, binding); edit = o.right; }, else => break, } } }
fn aliasAddScanDeleteRule(p: *Parser, leaf: *const Expr, gf_value: i32) Allocator.Error!void { const t = try p.arena.create(rules.Test); t.* = .{ .kind = .scan, .qual = .any, .object = .{ .builtin = .family }, .compare = .eq, .ignore_blanks = true, .expr = leaf }; const t2 = try p.arena.create(rules.Test); t2.* = .{ .kind = .scan, .qual = .any, .object = .{ .builtin = .genericfamily }, .compare = .eq, .expr = try p.newExpr(.{ .integer = gf_value }) }; const ed = try p.arena.create(rules.Edit); ed.* = .{ .object = .{ .builtin = .genericfamily }, .op = .delete, .expr = null, .binding = .weak }; try p.addRule(try p.arena.dupe(rules.Item, &.{ .{ .@"test" = t }, .{ .@"test" = t2 }, .{ .edit = ed } }), .scan); }
fn aliasAddScanRule(p: *Parser, leaf: *const Expr, generic: []const u8, binding: Pattern.Binding) Allocator.Error!void { const c = object.constantFor(generic, .{ .builtin = .genericfamily }) orelse return; if (c.value != object.generic_family.unknown) try p.aliasAddScanDeleteRule(leaf, object.generic_family.unknown); try p.aliasAddScanDeleteRule(leaf, c.value); const t = try p.arena.create(rules.Test); t.* = .{ .kind = .scan, .qual = .any, .object = .{ .builtin = .family }, .compare = .eq, .ignore_blanks = true, .expr = leaf }; const ed = try p.arena.create(rules.Edit); ed.* = .{ .object = .{ .builtin = .genericfamily }, .op = .append, .expr = try p.newExpr(.{ .constant = generic }), .binding = binding }; try p.addRule(try p.arena.dupe(rules.Item, &.{ .{ .@"test" = t }, .{ .edit = ed } }), .scan); }
// ---- font selection
fn parseAcceptRejectFont(p: *Parser, accept: bool) Allocator.Error!void { while (p.peek()) |v| { switch (v.tag) { .glob => |g| try p.loader.addGlob(g, accept), .pattern => |pat| try p.loader.addPattern(pat, accept), else => p.message(.warning, "bad font selector", .{}), } p.pop(); } }
/// `FcPopValue`. fn popValue(p: *Parser) Value { const v = p.peek() orelse return .void; const out: Value = switch (v.tag) { .string => |s| .{ .string = s }, .constant => |s| if (object.constant(s)) |c| .{ .integer = c.value } else .void, .integer => |i| .{ .integer = i }, .double => |d| .{ .double = d }, .bool => |b| .{ .bool = b }, .charset => |c| .{ .charset = c }, .langset => |l| .{ .langset = l }, .range => |r| .{ .range = r }, else => blk: { p.message(.warning, "unknown pattern element", .{}); break :blk .void; }, }; p.pop(); return out; }
fn parsePatelt(p: *Parser) Allocator.Error!void { const pat = try p.arena.create(Pattern); pat.* = .init(p.arena); const elt_name = p.attr("name") orelse { p.message(.warning, "missing pattern element name", .{}); return; }; const o = Object.fromName(elt_name); while (true) { const v = p.popValue(); if (v == .void) break; if (!try pat.addBorrowed(o, v, .strong, true)) { p.message(.@"error", "out of memory", .{}); break; } } try p.push(.{ .pattern = pat }); }
fn parsePattern(p: *Parser) Allocator.Error!void { const pat = try p.arena.create(Pattern); pat.* = .init(p.arena); while (p.peek()) |v| { switch (v.tag) { .pattern => |elt| { // FcPatternAppend: every value of every object, appended. for (elt.elts.items) |e| { var it = e.iterator(); while (it.next()) |l| _ = try pat.addBorrowed(e.object, l.value, l.binding, true); } }, else => p.message(.warning, "unknown pattern element", .{}), } p.pop(); } try p.push(.{ .pattern = pat }); }
// ---- directories and files
fn parseRescan(p: *Parser) void { var n = p.elements(); while (n > 0) { n -= 1; const v = p.fetch(n); switch (v.tag) { .integer => |i| p.loader.setRescanInterval(i), else => p.message(.warning, "non-integer rescan", .{}), } } }
fn parseDir(p: *Parser) Allocator.Error!void { const data = p.text(); if (data.len == 0) { p.message(.warning, "empty font directory name ignored", .{}); return; } const prefix = p.attr("prefix"); const salt = p.attr("salt"); const dirs = try p.loader.pathsFromPrefix(p, data, prefix, "dir") orelse return; for (dirs) |dir| { if (dir.len == 0) continue; try p.loader.addFontDir(dir, null, salt); } }
fn parseRemapDir(p: *Parser) Allocator.Error!void { const data = p.text(); if (data.len == 0) { p.message(.warning, "empty font directory name for remap ignored", .{}); return; } const as_path = p.attr("as-path") orelse { p.message(.warning, "Missing as-path in remap-dir", .{}); return; }; const prefix = p.attr("prefix"); const salt = p.attr("salt"); const dirs = try p.loader.pathsFromPrefix(p, data, prefix, "remap-dir") orelse return; for (dirs) |dir| { if (dir.len == 0) continue; try p.loader.addFontDir(dir, as_path, salt); } }
fn parseCacheDir(p: *Parser) Allocator.Error!void { var prefix: ?[]const u8 = null; if (p.attr("prefix")) |a| { if (std.mem.eql(u8, a, "xdg")) { prefix = p.loader.xdgCacheHome() orelse return; } } const data = p.text(); if (data.len == 0) { p.message(.warning, "empty cache directory name ignored", .{}); return; } const dir = if (prefix) |pre| try std.mem.concat(p.arena, u8, &.{ pre, "/", data }) else data; try p.loader.addCacheDir(dir); }
fn parseInclude(p: *Parser) Allocator.Error!void { var s = p.text(); const ignore_missing = if (p.attr("ignore_missing")) |a| p.lexBool(a) else false; if (p.attr("prefix")) |a| { if (std.mem.eql(u8, a, "xdg")) { const home = p.loader.xdgConfigHome() orelse return; s = try std.mem.concat(p.arena, u8, &.{ home, "/", s }); } } // Hand over the rules so far, so that the included file's come after // them. try p.loader.addRules(&p.ruleset); p.ruleset = .{}; if (!try p.loader.parseFile(s, !ignore_missing)) p.@"error" = true; }};
/// strtol(s, &end, 0) consuming all of `s`.fn parseCInt(s: []const u8) ?i32 { var i: usize = 0; while (i < s.len and std.ascii.isWhitespace(s[i])) i += 1; var negative = false; if (i < s.len and (s[i] == '+' or s[i] == '-')) { negative = s[i] == '-'; i += 1; } var base: u8 = 10; if (i + 1 < s.len and s[i] == '0' and (s[i + 1] == 'x' or s[i + 1] == 'X') and i + 2 < s.len and std.ascii.isHex(s[i + 2])) { base = 16; i += 2; } else if (i < s.len and s[i] == '0') { base = 8; } const start = i; var v: i64 = 0; while (i < s.len) : (i += 1) { const digit = std.fmt.charToDigit(s[i], base) catch break; v = v * base + digit; if (v > 0x7fffffff + 1) v = 0x7fffffff + 1; } if (i == start or i != s.len) return null; if (negative) v = -v; return std.math.lossyCast(i32, v);}
test "C integers" { try std.testing.expectEqual(@as(?i32, 12), parseCInt("12")); try std.testing.expectEqual(@as(?i32, 16), parseCInt("0x10")); try std.testing.expectEqual(@as(?i32, 8), parseCInt("010")); try std.testing.expectEqual(@as(?i32, -3), parseCInt("-3")); try std.testing.expectEqual(@as(?i32, null), parseCInt("12px")); try std.testing.expectEqual(@as(?i32, null), parseCInt(""));}