Files
bun.sh/src/css/values/syntax.zig
Jarred Sumner 3ea6133c46 CI: Remove unused top-level decls in formatter in zig (#19879)
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-23 22:49:48 -07:00

523 lines
23 KiB
Zig

const std = @import("std");
const bun = @import("bun");
pub const css = @import("../css_parser.zig");
const Result = css.Result;
const ArrayList = std.ArrayListUnmanaged;
const Printer = css.Printer;
const PrintErr = css.PrintErr;
const CSSNumber = css.css_values.number.CSSNumber;
const CSSNumberFns = css.css_values.number.CSSNumberFns;
const LengthPercentage = css.css_values.length.LengthPercentage;
const Length = css.css_values.length.Length;
const Percentage = css.css_values.percentage.Percentage;
const CssColor = css.css_values.color.CssColor;
const Image = css.css_values.image.Image;
const Url = css.css_values.url.Url;
const CSSInteger = css.css_values.number.CSSInteger;
const CSSIntegerFns = css.css_values.number.CSSIntegerFns;
const Angle = css.css_values.angle.Angle;
const Time = css.css_values.time.Time;
const Resolution = css.css_values.resolution.Resolution;
const CustomIdent = css.css_values.ident.CustomIdent;
const CustomIdentFns = css.css_values.ident.CustomIdentFns;
const Ident = css.css_values.ident.Ident;
// https://drafts.csswg.org/css-syntax-3/#whitespace
const SPACE_CHARACTERS: []const u8 = &.{ 0x20, 0x09 };
/// A CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings)
/// used to define the grammar for a registered custom property.
pub const SyntaxString = union(enum) {
/// A list of syntax components.
components: ArrayList(SyntaxComponent),
/// The universal syntax definition.
universal,
const This = @This();
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void {
try dest.writeChar('"');
switch (this.*) {
.universal => try dest.writeChar('*'),
.components => |*components| {
var first = true;
for (components.items) |*component| {
if (first) {
first = false;
} else {
try dest.delim('|', true);
}
try component.toCss(W, dest);
}
},
}
return dest.writeChar('"');
}
pub fn parse(input: *css.Parser) Result(SyntaxString) {
const string = switch (input.expectString()) {
.result => |v| v,
.err => |e| return .{ .err = e },
};
const result = SyntaxString.parseString(input.allocator(), string);
if (result.isErr()) return .{ .err = input.newCustomError(css.ParserError.invalid_value) };
return .{ .result = result.result };
}
/// Parses a syntax string.
pub fn parseString(allocator: std.mem.Allocator, input: []const u8) css.Maybe(SyntaxString, void) {
// https://drafts.css-houdini.org/css-properties-values-api/#parsing-syntax
var trimmed_input = std.mem.trimLeft(u8, input, SPACE_CHARACTERS);
if (trimmed_input.len == 0) {
return .{ .err = {} };
}
if (bun.strings.eqlComptime(trimmed_input, "*")) {
return .{ .result = SyntaxString.universal };
}
var components = ArrayList(SyntaxComponent){};
// PERF(alloc): count first?
while (true) {
const component = switch (SyntaxComponent.parseString(&trimmed_input)) {
.result => |v| v,
.err => |e| return .{ .err = e },
};
components.append(
allocator,
component,
) catch bun.outOfMemory();
trimmed_input = std.mem.trimLeft(u8, trimmed_input, SPACE_CHARACTERS);
if (trimmed_input.len == 0) {
break;
}
if (bun.strings.startsWithChar(trimmed_input, '|')) {
trimmed_input = trimmed_input[1..];
continue;
}
return .{ .err = {} };
}
return .{ .result = SyntaxString{ .components = components } };
}
/// Parses a value according to the syntax grammar.
pub fn parseValue(this: *const SyntaxString, input: *css.Parser) Result(ParsedComponent) {
switch (this.*) {
.universal => return .{ .result = ParsedComponent{
.token_list = switch (css.css_properties.custom.TokenList.parse(
input,
&css.ParserOptions.default(input.allocator(), null),
0,
)) {
.result => |v| v,
.err => |e| return .{ .err = e },
},
} },
.components => |components| {
// Loop through each component, and return the first one that parses successfully.
for (components.items) |component| {
const state = input.state();
// PERF: deinit this on error
var parsed = ArrayList(ParsedComponent){};
while (true) {
const value_result = input.tryParse(struct {
fn parse(
i: *css.Parser,
comp: SyntaxComponent,
) Result(ParsedComponent) {
const value = switch (comp.kind) {
.length => ParsedComponent{ .length = switch (Length.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.number => ParsedComponent{ .number = switch (CSSNumberFns.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.percentage => ParsedComponent{ .percentage = switch (Percentage.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.length_percentage => ParsedComponent{ .length_percentage = switch (LengthPercentage.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.color => ParsedComponent{ .color = switch (CssColor.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.image => ParsedComponent{ .image = switch (Image.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.url => ParsedComponent{ .url = switch (Url.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.integer => ParsedComponent{ .integer = switch (CSSIntegerFns.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.angle => ParsedComponent{ .angle = switch (Angle.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.time => ParsedComponent{ .time = switch (Time.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.resolution => ParsedComponent{ .resolution = switch (Resolution.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.transform_function => ParsedComponent{ .transform_function = switch (css.css_properties.transform.Transform.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.transform_list => ParsedComponent{ .transform_list = switch (css.css_properties.transform.TransformList.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.custom_ident => ParsedComponent{ .custom_ident = switch (CustomIdentFns.parse(i)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
} },
.literal => |value| blk: {
const location = i.currentSourceLocation();
const ident = switch (i.expectIdent()) {
.result => |v| v,
.err => |e| return .{ .err = e },
};
if (!bun.strings.eql(ident, value)) {
return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) };
}
break :blk ParsedComponent{ .literal = .{ .v = ident } };
},
};
return .{ .result = value };
}
}.parse, .{component});
if (value_result.asValue()) |value| {
switch (component.multiplier) {
.none => return .{ .result = value },
.space => {
parsed.append(input.allocator(), value) catch bun.outOfMemory();
if (input.isExhausted()) {
return .{ .result = ParsedComponent{ .repeated = .{
.components = parsed,
.multiplier = component.multiplier,
} } };
}
},
.comma => {
parsed.append(input.allocator(), value) catch bun.outOfMemory();
if (input.next().asValue()) |token| {
if (token.* == .comma) continue;
break;
} else {
return .{ .result = ParsedComponent{ .repeated = .{
.components = parsed,
.multiplier = component.multiplier,
} } };
}
},
}
} else {
break;
}
}
input.reset(&state);
}
return .{ .err = input.newErrorForNextToken() };
},
}
}
};
/// A [syntax component](https://drafts.css-houdini.org/css-properties-values-api/#syntax-component)
/// within a [SyntaxString](SyntaxString).
///
/// A syntax component consists of a component kind an a multiplier, which indicates how the component
/// may repeat during parsing.
pub const SyntaxComponent = struct {
kind: SyntaxComponentKind,
multiplier: Multiplier,
pub fn parseString(input: *[]const u8) css.Maybe(SyntaxComponent, void) {
const kind = switch (SyntaxComponentKind.parseString(input)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
};
// Pre-multiplied types cannot have multipliers.
if (kind == .transform_list) {
return .{ .result = SyntaxComponent{
.kind = kind,
.multiplier = .none,
} };
}
var multiplier: Multiplier = .none;
if (bun.strings.startsWithChar(input.*, '+')) {
input.* = input.*[1..];
multiplier = .space;
} else if (bun.strings.startsWithChar(input.*, '#')) {
input.* = input.*[1..];
multiplier = .comma;
}
return .{ .result = SyntaxComponent{ .kind = kind, .multiplier = multiplier } };
}
pub fn toCss(this: *const SyntaxComponent, comptime W: type, dest: *Printer(W)) PrintErr!void {
try this.kind.toCss(W, dest);
return switch (this.multiplier) {
.none => {},
.comma => dest.writeChar('#'),
.space => dest.writeChar('+'),
};
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
};
/// A [syntax component component name](https://drafts.css-houdini.org/css-properties-values-api/#supported-names).
pub const SyntaxComponentKind = union(enum) {
/// A `<length>` component.
length,
/// A `<number>` component.
number,
/// A `<percentage>` component.
percentage,
/// A `<length-percentage>` component.
length_percentage,
/// A `<color>` component.
color,
/// An `<image>` component.
image,
/// A `<url>` component.
url,
/// An `<integer>` component.
integer,
/// An `<angle>` component.
angle,
/// A `<time>` component.
time,
/// A `<resolution>` component.
resolution,
/// A `<transform-function>` component.
transform_function,
/// A `<transform-list>` component.
transform_list,
/// A `<custom-ident>` component.
custom_ident,
/// A literal component.
literal: []const u8,
pub fn parseString(input: *[]const u8) css.Maybe(SyntaxComponentKind, void) {
// https://drafts.css-houdini.org/css-properties-values-api/#consume-syntax-component
input.* = std.mem.trimLeft(u8, input.*, SPACE_CHARACTERS);
if (bun.strings.startsWithChar(input.*, '<')) {
// https://drafts.css-houdini.org/css-properties-values-api/#consume-data-type-name
const end_idx = std.mem.indexOfScalar(u8, input.*, '>') orelse return .{ .err = {} };
const name = input.*[1..end_idx];
// todo_stuff.match_ignore_ascii_case
const component: SyntaxComponentKind = if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "length"))
.length
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "number"))
.number
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "percentage"))
.percentage
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "length-percentage"))
.length_percentage
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "color"))
.color
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "image"))
.image
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "url"))
.url
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "integer"))
.integer
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "angle"))
.angle
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "time"))
.time
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "resolution"))
.resolution
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "transform-function"))
.transform_function
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "transform-list"))
.transform_list
else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "custom-ident"))
.custom_ident
else
return .{ .err = {} };
input.* = input.*[end_idx + 1 ..];
return .{ .result = component };
} else if (input.len > 0 and isIdentStart(input.*[0])) {
// A literal.
var end_idx: usize = 0;
while (end_idx < input.len and
isNameCodePoint(input.*[end_idx])) : (end_idx +=
bun.strings.utf8ByteSequenceLengthUnsafe(input.*[end_idx]))
{}
const literal = input.*[0..end_idx];
input.* = input.*[end_idx..];
return .{ .result = SyntaxComponentKind{ .literal = literal } };
} else {
return .{ .err = {} };
}
}
pub fn toCss(this: *const SyntaxComponentKind, comptime W: type, dest: *Printer(W)) PrintErr!void {
return switch (this.*) {
.length => try dest.writeStr("<length>"),
.number => try dest.writeStr("<number>"),
.percentage => try dest.writeStr("<percentage>"),
.length_percentage => try dest.writeStr("<length-percentage>"),
.color => try dest.writeStr("<color>"),
.image => try dest.writeStr("<image>"),
.url => try dest.writeStr("<url>"),
.integer => try dest.writeStr("<integer>"),
.angle => try dest.writeStr("<angle>"),
.time => try dest.writeStr("<time>"),
.resolution => try dest.writeStr("<resolution>"),
.transform_function => try dest.writeStr("<transform-function>"),
.transform_list => try dest.writeStr("<transform-list>"),
.custom_ident => try dest.writeStr("<custom-ident>"),
.literal => |l| try dest.writeStr(l),
};
}
fn isIdentStart(c: u8) bool {
// https://drafts.csswg.org/css-syntax-3/#ident-start-code-point
return c >= 'A' and c <= 'Z' or c >= 'a' and c <= 'z' or c >= 0x80 or c == '_';
}
fn isNameCodePoint(c: u8) bool {
// https://drafts.csswg.org/css-syntax-3/#ident-code-point
return isIdentStart(c) or c >= '0' and c <= '9' or c == '-';
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
};
pub const ParsedComponent = union(enum) {
/// A `<length>` value.
length: Length,
/// A `<number>` value.
number: CSSNumber,
/// A `<percentage>` value.
percentage: Percentage,
/// A `<length-percentage>` value.
length_percentage: LengthPercentage,
/// A `<color>` value.
color: CssColor,
/// An `<image>` value.
image: Image, // Zig doesn't have lifetimes, so 'i is omitted.
/// A `<url>` value.
url: Url, // Lifetimes are omitted in Zig.
/// An `<integer>` value.
integer: CSSInteger,
/// An `<angle>` value.
angle: Angle,
/// A `<time>` value.
time: Time,
/// A `<resolution>` value.
resolution: Resolution,
/// A `<transform-function>` value.
transform_function: css.css_properties.transform.Transform,
/// A `<transform-list>` value.
transform_list: css.css_properties.transform.TransformList,
/// A `<custom-ident>` value.
custom_ident: CustomIdent,
/// A literal value.
literal: Ident,
/// A repeated component value.
repeated: struct {
/// The components to repeat.
components: ArrayList(ParsedComponent),
/// A multiplier describing how the components repeat.
multiplier: Multiplier,
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
},
/// A raw token stream.
token_list: css.css_properties.custom.TokenList,
const This = @This();
pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void {
return switch (this.*) {
.length => |*v| try v.toCss(W, dest),
.number => |*v| try CSSNumberFns.toCss(v, W, dest),
.percentage => |*v| try v.toCss(W, dest),
.length_percentage => |*v| try v.toCss(W, dest),
.color => |*v| try v.toCss(W, dest),
.image => |*v| try v.toCss(W, dest),
.url => |*v| try v.toCss(W, dest),
.integer => |*v| try CSSIntegerFns.toCss(v, W, dest),
.angle => |*v| try v.toCss(W, dest),
.time => |*v| try v.toCss(W, dest),
.resolution => |*v| try v.toCss(W, dest),
.transform_function => |*v| try v.toCss(W, dest),
.transform_list => |*v| try v.toCss(W, dest),
.custom_ident => |*v| try CustomIdentFns.toCss(v, W, dest),
.literal => |*v| css.serializer.serializeIdentifier(v.v, dest) catch return dest.addFmtError(),
.repeated => |*r| {
var first = true;
for (r.components.items) |*component| {
if (!first) {
switch (r.multiplier) {
.comma => try dest.delim(',', false),
.space => try dest.writeChar(' '),
.none => unreachable,
}
} else {
first = false;
}
try component.toCss(W, dest);
}
},
.token_list => |*t| try t.toCss(W, dest, false),
};
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
};
/// A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a
/// [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated.
pub const Multiplier = enum {
/// The component may not be repeated.
none,
/// The component may repeat one or more times, separated by spaces.
space,
/// The component may repeat one or more times, separated by commas.
comma,
};