mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
(For internal tracking: fixes STAB-1129, STAB-1145, STAB-1146, STAB-1150, STAB-1126, STAB-1147, STAB-1148, STAB-1149, STAB-1158) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
541 lines
18 KiB
Zig
541 lines
18 KiB
Zig
pub const css = @import("../css_parser.zig");
|
|
|
|
const SmallList = css.SmallList;
|
|
const Printer = css.Printer;
|
|
const PrintErr = css.PrintErr;
|
|
|
|
const CSSNumberFns = css.css_values.number.CSSNumberFns;
|
|
const LengthPercentage = css.css_values.length.LengthPercentage;
|
|
const CustomIdent = css.css_values.ident.CustomIdent;
|
|
const CSSNumber = css.css_values.number.CSSNumber;
|
|
const CustomIdentList = css.css_values.ident.CustomIdentList;
|
|
const CSSInteger = css.css_values.number.CSSInteger;
|
|
|
|
/// A [track sizing](https://drafts.csswg.org/css-grid-2/#track-sizing) value
|
|
/// for the `grid-template-rows` and `grid-template-columns` properties.
|
|
pub const TrackSizing = union(enum) {
|
|
/// No explicit grid tracks.
|
|
none,
|
|
/// A list of grid tracks.
|
|
tracklist: TrackList,
|
|
|
|
pub const parse = css.DeriveParse(@This()).parse;
|
|
pub const toCss = css.DeriveToCss(@This()).toCss;
|
|
};
|
|
|
|
/// A [`<track-list>`](https://drafts.csswg.org/css-grid-2/#typedef-track-list) value,
|
|
/// as used in the `grid-template-rows` and `grid-template-columns` properties.
|
|
///
|
|
/// See [TrackSizing](TrackSizing).
|
|
pub const TrackList = struct {
|
|
/// A list of line names.
|
|
line_names: bun.BabyList(CustomIdentList),
|
|
/// A list of grid track items.
|
|
items: bun.BabyList(TrackListItem),
|
|
|
|
pub fn parse(input: *css.Parser) css.Result(@This()) {
|
|
var line_names = BabyList(CustomIdentList){};
|
|
var items = BabyList(TrackListItem){};
|
|
|
|
while (true) {
|
|
const line_name = input.tryParse(parseLineNames, .{}).asValue() orelse CustomIdentList{};
|
|
bun.handleOom(line_names.append(input.allocator(), line_name));
|
|
|
|
if (input.tryParse(TrackSize.parse, .{}).asValue()) |track_size| {
|
|
// TODO: error handling
|
|
bun.handleOom(items.append(.{ .track_size = track_size }));
|
|
} else if (input.tryParse(TrackRepeat.parse, .{}).asValue()) |repeat| {
|
|
// TODO: error handling
|
|
bun.handleOom(items.append(.{ .track_repeat = repeat }));
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (items.len == 0) {
|
|
return .{ .err = input.newCustomError(.invalid_declaration) };
|
|
}
|
|
|
|
return .{ .result = .{
|
|
.line_names = line_names,
|
|
.items = items,
|
|
} };
|
|
}
|
|
|
|
pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void {
|
|
var items_index = 0;
|
|
var first = true;
|
|
|
|
for (this.line_names.sliceConst()) |*names| {
|
|
if (!names.isEmpty()) try serializeLineNames(names, W, dest);
|
|
|
|
if (items_index < this.items.len) {
|
|
const item = this.items.at(items_index);
|
|
items_index += 1;
|
|
|
|
// Whitespace is required if there are no line names.
|
|
if (!names.isEmpty()) {
|
|
try dest.whitespace();
|
|
} else if (!first) {
|
|
try dest.writeChar(' ');
|
|
}
|
|
|
|
switch (item.*) {
|
|
.track_repeat => |*repeat| try repeat.toCss(W, dest),
|
|
.track_size => |*size| try size.toCss(W, dest),
|
|
}
|
|
}
|
|
|
|
first = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Either a track size or `repeat()` function.
|
|
///
|
|
/// See [TrackList](TrackList).
|
|
pub const TrackListItem = union(enum) {
|
|
/// A track size.
|
|
track_size: TrackSize,
|
|
/// A `repeat()` function.
|
|
track_repeat: TrackRepeat,
|
|
};
|
|
|
|
/// A [track size](https://drafts.csswg.org/css-grid-2/#typedef-track-size) value.
|
|
///
|
|
/// See [TrackList](TrackList).
|
|
pub const TrackSize = union(enum) {
|
|
/// An explicit track breadth.
|
|
track_breadth: TrackBreadth,
|
|
/// The `minmax()` function.
|
|
min_max: struct {
|
|
/// The minimum value.
|
|
min: TrackBreadth,
|
|
/// The maximum value.
|
|
max: TrackBreadth,
|
|
},
|
|
/// The `fit-content()` function.
|
|
fit_content: LengthPercentage,
|
|
|
|
pub fn default() @This() {
|
|
return .{ .track_breadth = TrackBreadth.auto };
|
|
}
|
|
|
|
pub fn eql(this: *const @This(), other: *const @This()) bool {
|
|
return css.implementEql(@This(), this, other);
|
|
}
|
|
|
|
pub fn parse(input: *css.Parser) css.Result(@This()) {
|
|
if (input.tryParse(TrackBreadth.parse, .{}).asValue()) |breadth| {
|
|
return .{ .result = .{ .track_breadth = breadth } };
|
|
}
|
|
|
|
if (input.tryParse(css.Parser.expectFunctionMatching, .{"minmax"}).isOk()) {
|
|
return input.parseNestedBlock(struct {
|
|
pub fn parse(i: *css.Parser) css.Result(TrackSize) {
|
|
const min = switch (TrackBreadth.parseInternal(i, false)) {
|
|
.result => |v| v,
|
|
.err => |e| return .{ .err = e },
|
|
};
|
|
if (i.expectComma().asErr()) |e| return .{ .err = e };
|
|
return .{
|
|
.result = .{ .min_max = .{ .min = min, .max = switch (TrackBreadth.parse(i)) {
|
|
.result => |v| v,
|
|
.err => |e| return .{ .err = e },
|
|
} } },
|
|
};
|
|
}
|
|
}.parseFn);
|
|
}
|
|
|
|
if (input.expectFunctionMatching("fit-content").asErr()) |e| return .{ .err = e };
|
|
|
|
const len = switch (input.parseNestedBlock(css.voidWrap(LengthPercentage, LengthPercentage.parse))) {
|
|
.result => |v| v,
|
|
.err => |e| return .{ .err = e },
|
|
};
|
|
|
|
return .{ .result = .{ .fit_content = len } };
|
|
}
|
|
|
|
pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void {
|
|
switch (this.*) {
|
|
.track_breadth => |breadth| try breadth.toCss(W, dest),
|
|
.min_max => |mm| {
|
|
try dest.writeStr("minmax(");
|
|
try mm.min.toCss(W, dest);
|
|
try dest.delim(',', false);
|
|
try mm.max.toCss(W, dest);
|
|
try dest.writeChar(')');
|
|
},
|
|
.fit_content => |len| {
|
|
try dest.writeStr("fit-content(");
|
|
try len.toCss(W, dest);
|
|
try dest.writeChar(')');
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const TrackSizeList = struct {
|
|
v: SmallList(TrackSize, 1) = .{},
|
|
|
|
pub fn parse(input: *css.Parser) css.Result(@This()) {
|
|
var res = SmallList(TrackSize, 1){};
|
|
while (input.tryParse(TrackSize.parse, .{}).asValue()) |size| {
|
|
bun.handleOom(res.append(input.allocator(), size));
|
|
}
|
|
|
|
if (res.len() == 1 and res.at(0).eql(&TrackSize.default())) {
|
|
res.clearRetainingCapacity();
|
|
}
|
|
|
|
return .{ .result = .{ .v = res } };
|
|
}
|
|
|
|
pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void {
|
|
if (this.v.len() == 0) {
|
|
try dest.writeStr("auto");
|
|
return;
|
|
}
|
|
|
|
var first = true;
|
|
for (this.v.slice()) |item| {
|
|
if (first) {
|
|
first = false;
|
|
} else {
|
|
try dest.writeChar(' ');
|
|
}
|
|
try item.toCss(W, dest);
|
|
}
|
|
}
|
|
};
|
|
|
|
/// A [track breadth](https://drafts.csswg.org/css-grid-2/#typedef-track-breadth) value.
|
|
///
|
|
/// See [TrackSize](TrackSize).
|
|
pub const TrackBreadth = union(enum) {
|
|
/// An explicit length.
|
|
length: LengthPercentage,
|
|
/// A flex factor.
|
|
flex: CSSNumber,
|
|
/// The `min-content` keyword.
|
|
min_content,
|
|
/// The `max-content` keyword.
|
|
max_content,
|
|
/// The `auto` keyword.
|
|
auto,
|
|
|
|
pub fn parse(input: *css.Parser) css.Result(@This()) {
|
|
return TrackBreadth.parseInternal(input, true);
|
|
}
|
|
|
|
fn parseInternal(input: *css.Parser, allow_flex: bool) css.Result(@This()) {
|
|
if (input.tryParse(LengthPercentage.parse, .{}).asValue()) |len| {
|
|
return .{ .result = .{ .length = len } };
|
|
}
|
|
|
|
if (allow_flex) {
|
|
if (input.tryParse(TrackBreadth.parseFlex, .{}).asValue()) |flex| {
|
|
return .{ .result = .{ .flex = flex } };
|
|
}
|
|
}
|
|
|
|
const location = input.currentSourceLocation();
|
|
const ident = switch (input.expectIdent()) {
|
|
.result => |v| v,
|
|
.err => |e| return .{ .err = e },
|
|
};
|
|
|
|
if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "auto")) {
|
|
return .{ .result = .auto };
|
|
} else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "min-content")) {
|
|
return .{ .result = .min_content };
|
|
} else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "max-content")) {
|
|
return .{ .result = .max_content };
|
|
}
|
|
|
|
return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) };
|
|
}
|
|
|
|
fn parseFlex(input: *css.Parser) css.Result(CSSNumber) {
|
|
const location = input.currentSourceLocation();
|
|
const token = switch (input.next()) {
|
|
.result => |v| v,
|
|
.err => |e| return .{ .err = e },
|
|
};
|
|
|
|
if (token == .dimension) {
|
|
if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(token.dimension.unit, "fr") and token.dimension.value >= 0.0) {
|
|
return .{ .result = token.dimension.value };
|
|
}
|
|
}
|
|
|
|
return .{ .err = location.newUnexpectedTokenError(token) };
|
|
}
|
|
|
|
pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void {
|
|
switch (this.*) {
|
|
.auto => try dest.writeStr("auto"),
|
|
.min_content => try dest.writeStr("min-content"),
|
|
.max_content => try dest.writeStr("max-content"),
|
|
.length => |len| try len.toCss(W, dest),
|
|
// .flex => |flex| try css.CSSNumberFns.serializeDimension(&flex, "fr", W, dest),
|
|
.flex => |flex| css.serializer.serializeDimension(flex, "fr", W, dest),
|
|
}
|
|
}
|
|
};
|
|
|
|
/// A `repeat()` function.
|
|
///
|
|
/// See [TrackList](TrackList).
|
|
pub const TrackRepeat = struct {
|
|
/// The repeat count.
|
|
count: RepeatCount,
|
|
/// The line names to repeat.
|
|
line_names: bun.BabyList(CustomIdentList),
|
|
/// The track sizes to repeat.
|
|
track_sizes: bun.BabyList(TrackSize),
|
|
|
|
pub fn parse(input: *css.Parser) css.Result(@This()) {
|
|
if (input.expectFunctionMatching("repeat").asErr()) |e| return .{ .err = e };
|
|
|
|
return input.parseNestedBlock(struct {
|
|
fn parse(i: *css.Parser) css.Result(TrackRepeat) {
|
|
const count = switch (@call(.auto, @field(RepeatCount, "parse"), .{i})) {
|
|
.result => |v| v,
|
|
.err => |e| return .{ .err = e },
|
|
};
|
|
|
|
if (i.expectComma().asErr()) |e| return .{ .err = e };
|
|
|
|
// TODO: this code will not compile if used
|
|
var line_names = bun.BabyList(CustomIdentList).init(i.allocator);
|
|
var track_sizes = bun.BabyList(TrackSize).init(i.allocator);
|
|
|
|
while (true) {
|
|
const line_name = i.tryParse(parseLineNames, .{}).unwrapOr(CustomIdentList{});
|
|
bun.handleOom(line_names.append(i.allocator(), line_name));
|
|
|
|
if (input.tryParse(TrackSize.parse, .{}).asValue()) |track_size| {
|
|
// TODO: error handling
|
|
bun.handleOom(track_sizes.append(i.allocator(), track_size));
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return .{ .result = .{
|
|
.count = count,
|
|
.line_names = line_names,
|
|
.track_sizes = track_sizes,
|
|
} };
|
|
}
|
|
}.parse);
|
|
}
|
|
|
|
pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void {
|
|
try dest.writeStr("repeat(");
|
|
try this.count.toCss(W, dest);
|
|
try dest.delim(',', false);
|
|
|
|
var track_sizes_index = 0;
|
|
var first = true;
|
|
for (this.line_names.sliceConst()) |*names| {
|
|
if (!names.isEmpty()) {
|
|
try serializeLineNames(names, W, dest);
|
|
}
|
|
|
|
if (track_sizes_index < this.track_sizes.len) {
|
|
const size = this.track_sizes.at(track_sizes_index);
|
|
track_sizes_index += 1;
|
|
|
|
if (!names.isEmpty()) {
|
|
try dest.whitespace();
|
|
} else if (!first) {
|
|
try dest.writeChar(' ');
|
|
}
|
|
try size.toCss(W, dest);
|
|
}
|
|
|
|
first = false;
|
|
}
|
|
|
|
try dest.writeChar(')');
|
|
}
|
|
};
|
|
|
|
fn serializeLineNames(names: []const CustomIdent, comptime W: type, dest: *Printer(W)) PrintErr!void {
|
|
try dest.writeChar('[');
|
|
var first = true;
|
|
for (names) |*name| {
|
|
if (first) {
|
|
first = false;
|
|
} else {
|
|
try dest.writeChar(' ');
|
|
}
|
|
try writeIdent(&name.value, W, dest);
|
|
}
|
|
try dest.writeChar(']');
|
|
}
|
|
|
|
fn writeIdent(name: []const u8, comptime W: type, dest: *Printer(W)) PrintErr!void {
|
|
const css_module_grid_enabled = if (dest.css_module) |*css_module| css_module.config.grid else false;
|
|
if (css_module_grid_enabled) {
|
|
if (dest.css_module) |*css_module| {
|
|
if (css_module.config.pattern.segments.last()) |last| {
|
|
if (last != css.css_modules.Segment.local) {
|
|
return try dest.addInvalidCssModulesPatternInGridError();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
try dest.writeIdent(name, css_module_grid_enabled);
|
|
}
|
|
|
|
fn parseLineNames(input: *css.Parser) css.Result(CustomIdentList) {
|
|
if (input.expectSquareBracketBlock().asErr()) |e| return .{ .err = e };
|
|
|
|
return input.parseNestedBlock(struct {
|
|
fn parse(i: *css.Parser) css.Result(CustomIdentList) {
|
|
var values = CustomIdentList{};
|
|
|
|
while (input.tryParse(CustomIdent.parse, .{}).asValue()) |ident| {
|
|
bun.handleOom(values.append(i.allocator(), ident));
|
|
}
|
|
|
|
return .{ .result = values };
|
|
}
|
|
}.parse);
|
|
}
|
|
|
|
/// A [`<repeat-count>`](https://drafts.csswg.org/css-grid-2/#typedef-track-repeat) value,
|
|
/// used in the `repeat()` function.
|
|
///
|
|
/// See [TrackRepeat](TrackRepeat).
|
|
pub const RepeatCount = union(enum) {
|
|
/// The number of times to repeat.
|
|
number: CSSInteger,
|
|
/// The `auto-fill` keyword.
|
|
@"auto-fill",
|
|
/// The `auto-fit` keyword.
|
|
@"auto-fit",
|
|
|
|
pub const parse = css.DeriveParse(@This()).parse;
|
|
pub const toCss = css.DeriveToCss(@This()).toCss;
|
|
|
|
pub fn eql(this: *const @This(), other: *const @This()) bool {
|
|
return css.implementEql(@This(), this, other);
|
|
}
|
|
};
|
|
|
|
/// A grid template areas value.
|
|
/// See https://drafts.csswg.org/css-grid-2/#propdef-grid-template-areas
|
|
pub const GridTemplateAreas = union(enum) {
|
|
/// No named grid areas.
|
|
none,
|
|
/// Defines the list of named grid areas.
|
|
areas: struct {
|
|
/// The number of columns in the grid.
|
|
columns: u32,
|
|
/// A flattened list of grid area names.
|
|
/// Unnamed areas specified by the `.` token are represented as null.
|
|
areas: SmallList(?[]const u8, 1),
|
|
},
|
|
|
|
pub fn parse(input: *css.Parser) css.Result(@This()) {
|
|
if (input.tryParse(struct {
|
|
fn parse(i: *css.Parser) css.Result(void) {
|
|
return i.expectIdentMatching("none");
|
|
}
|
|
}.parse, .{}).asValue()) |_| {
|
|
return .{ .result = .none };
|
|
}
|
|
|
|
var tokens = SmallList(?[]const u8, 1){};
|
|
var row: u32 = 0;
|
|
var columns: u32 = 0;
|
|
|
|
if (input.tryParse(css.Parser.expectString, .{}).asValue()) |s| {
|
|
const parsed_columns = switch (parseString(input.allocator(), s, &tokens)) {
|
|
.result => |v| v,
|
|
.err => return .{input.newError(.qualified_rule_invalid)},
|
|
};
|
|
|
|
if (row == 0) {
|
|
columns = parsed_columns;
|
|
} else if (parsed_columns != columns) return .{ .err = input.newCustomError(.invalid_declaration) };
|
|
|
|
row += 1;
|
|
}
|
|
|
|
return .{ .result = .{ .areas = .{
|
|
.columns = columns,
|
|
.areas = tokens,
|
|
} } };
|
|
}
|
|
|
|
const HTML_SPACE_CHARACTERS: []const u8 = &.{ 0x0020, 0x0009, 0x000a, 0x000c, 0x000d };
|
|
|
|
fn parseString(allocator: Allocator, s: []const u8, tokens: *SmallList(?[]const u8, 1)) bun.Maybe(u32, void) {
|
|
var string = s;
|
|
var column = 0;
|
|
|
|
while (true) {
|
|
const rest = bun.strings.trim(string, HTML_SPACE_CHARACTERS);
|
|
if (rest.len == 0) {
|
|
// Each string must produce a valid token.
|
|
if (column == 0) return .{ .err = {} };
|
|
break;
|
|
}
|
|
|
|
column += 1;
|
|
|
|
if (bun.strings.startsWithChar(rest, '.')) {
|
|
const idx = idx: {
|
|
for (rest, 0..) |*c, i| {
|
|
if (c.* != '.') {
|
|
break :idx i;
|
|
}
|
|
}
|
|
break :idx rest.len;
|
|
};
|
|
string = rest[idx..];
|
|
}
|
|
|
|
const starts_with_name_codepoint = brk: {
|
|
if (rest.len == 0) break :brk false;
|
|
break :brk isNameCodepoint(rest[0]);
|
|
};
|
|
|
|
if (!starts_with_name_codepoint) return .{ .err = {} };
|
|
|
|
const token_len = token_len: {
|
|
for (rest, 0..) |*c, i| {
|
|
if (!isNameCodepoint(c.*)) {
|
|
break :token_len i;
|
|
}
|
|
}
|
|
break :token_len rest.len;
|
|
};
|
|
const token = rest[0..token_len];
|
|
bun.handleOom(tokens.append(allocator, token));
|
|
string = rest[token_len..];
|
|
}
|
|
|
|
return .{ .result = column };
|
|
}
|
|
};
|
|
|
|
fn isNameCodepoint(c: u8) bool {
|
|
// alpha numeric, -, _, o
|
|
return c >= 'a' and c <= 'z' or c >= 'A' and c <= 'Z' or c == '_' or c >= '0' and c <= '9' or c == '-' or c >= 0x80; // codepoints larger than ascii;
|
|
}
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const bun = @import("bun");
|
|
const BabyList = bun.BabyList;
|