// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-FileCopyrightText: © 2009,2010 Red Hat, Inc. // SPDX-FileCopyrightText: © 2011,2012 Google, Inc. // SPDX-License-Identifier: MIT AND MIT-Modern-Variant //! A feature the caller turns on or off, over the whole text or a range of //! its clusters, and the string syntax HarfBuzz and CSS write one in. const std = @import("std"); const Feature = @This(); /// The feature tag, as a big-endian `u32`. tag: u32, /// 0 turns the feature off, 1 on; for alternates, which one. value: u32 = 1, /// The clusters it applies to: `start` inclusive, `end` exclusive. start: u32 = global_start, end: u32 = global_end, pub const global_start: u32 = 0; pub const global_end: u32 = std.math.maxInt(u32); pub fn isGlobal(f: Feature) bool { return f.start == global_start and f.end == global_end; } pub fn init(comptime tag: *const [4]u8, value: u32) Feature { return .{ .tag = std.mem.readInt(u32, tag, .big), .value = value }; } const Parser = struct { s: []const u8, i: usize = 0, fn space(p: *Parser) void { while (p.i < p.s.len and std.ascii.isWhitespace(p.s[p.i])) p.i += 1; } fn char(p: *Parser, c: u8) bool { p.space(); if (p.i == p.s.len or p.s[p.i] != c) return false; p.i += 1; return true; } /// As `strtol`: leading space, a sign, digits. A negative number becomes /// a large unsigned one, as in HarfBuzz, so `[3:-1]` runs to the end. fn int(p: *Parser) ?u32 { var j = p.i; while (j < p.s.len and std.ascii.isWhitespace(p.s[j])) j += 1; var neg = false; if (j < p.s.len and (p.s[j] == '-' or p.s[j] == '+')) { neg = p.s[j] == '-'; j += 1; } const digits = j; var v: i64 = 0; while (j < p.s.len and std.ascii.isDigit(p.s[j])) : (j += 1) { v = @min(v * 10 + (p.s[j] - '0'), std.math.maxInt(i32) + 1); } if (j == digits) return null; p.i = j; if (neg) v = -v; const clamped: i32 = @intCast(std.math.clamp(v, std.math.minInt(i32), std.math.maxInt(i32))); return @bitCast(clamped); } fn boolean(p: *Parser) ?u32 { p.space(); const start = p.i; while (p.i < p.s.len and std.ascii.isAlphabetic(p.s[p.i])) p.i += 1; const w = p.s[start..p.i]; if (std.ascii.eqlIgnoreCase(w, "on")) return 1; if (std.ascii.eqlIgnoreCase(w, "off")) return 0; return null; } fn tag(p: *Parser) ?u32 { p.space(); var quote: u8 = 0; if (p.i < p.s.len and (p.s[p.i] == '\'' or p.s[p.i] == '"')) { quote = p.s[p.i]; p.i += 1; } const start = p.i; while (p.i < p.s.len and p.s[p.i] != ' ' and p.s[p.i] != '=' and p.s[p.i] != '[' and p.s[p.i] != quote) p.i += 1; const t = p.s[start..p.i]; if (t.len == 0 or t.len > 4) return null; if (quote != 0) { // CSS expects exactly four bytes, and quotes are only allowed for // CSS compatibility. if (t.len != 4) return null; if (p.i == p.s.len or p.s[p.i] != quote) return null; p.i += 1; } var buf: [4]u8 = .{ ' ', ' ', ' ', ' ' }; @memcpy(buf[0..t.len], t); return std.mem.readInt(u32, &buf, .big); } }; /// `hb_feature_from_string`: `kern`, `+kern`, `-kern`, `kern=0`, /// `kern[3:5]`, `aalt=2`, `"liga" off`, and so on. pub fn parse(s: []const u8) ?Feature { var p: Parser = .{ .s = s }; var f: Feature = .{ .tag = 0 }; f.value = if (p.char('-')) 0 else blk: { _ = p.char('+'); break :blk 1; }; f.tag = p.tag() orelse return null; p.space(); if (p.char('[')) { const start = p.int(); if (start) |v| f.start = v; if (p.char(':') or p.char(';')) { if (p.int()) |v| f.end = v; } else if (start != null) { f.end = f.start +% 1; } if (!p.char(']')) return null; } const had_equal = p.char('='); const value = p.int() orelse p.boolean(); if (value) |v| f.value = v; if (had_equal and value == null) return null; p.space(); if (p.i != s.len) return null; return f; } /// A position on a variable font's axis, in the axis's user units. pub const Variation = struct { tag: u32, value: f32, /// `hb_variation_from_string`: `wght=700`, or `wght 700`. pub fn parse(s: []const u8) ?Variation { var p: Parser = .{ .s = s }; const tag = p.tag() orelse return null; _ = p.char('='); p.space(); const rest = std.mem.trim(u8, s[p.i..], " \t"); const value = std.fmt.parseFloat(f32, rest) catch return null; return .{ .tag = tag, .value = value }; } }; test "feature strings" { const t = std.testing; const kern = std.mem.readInt(u32, "kern", .big); try t.expectEqual(Feature{ .tag = kern, .value = 1 }, parse("kern").?); try t.expectEqual(Feature{ .tag = kern, .value = 0 }, parse("-kern").?); try t.expectEqual(Feature{ .tag = kern, .value = 0 }, parse("kern=0").?); try t.expectEqual(Feature{ .tag = kern, .value = 1, .start = 3, .end = 5 }, parse("kern[3:5]").?); try t.expectEqual(Feature{ .tag = kern, .value = 1, .start = 3, .end = 4 }, parse("kern[3]").?); try t.expectEqual(Feature{ .tag = kern, .value = 0 }, parse("\"kern\" off").?); try t.expectEqual(Feature{ .tag = std.mem.readInt(u32, "aalt", .big), .value = 2 }, parse("aalt=2").?); try t.expectEqual(Feature{ .tag = std.mem.readInt(u32, "ss1 ", .big), .value = 1 }, parse("ss1").?); try t.expectEqual(@as(?Feature, null), parse("kern=")); try t.expectEqual(@as(?Feature, null), parse("toolong")); const v = Variation.parse("wght=700.5").?; try t.expectEqual(std.mem.readInt(u32, "wght", .big), v.tag); try t.expectEqual(@as(f32, 700.5), v.value); }