mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
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>
523 lines
23 KiB
Zig
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,
|
|
};
|