From 45c8c60554791136433d1a7617483bd3579f4c16 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 15 Aug 2024 17:12:45 -0400 Subject: [PATCH 001/125] HOLY --- src/css/compat.zig | 15 + src/css/css_modules.zig | 140 + src/css/css_parser.zig | 4440 +++++++++++++++++++++++++++++ src/css/declaration.zig | 183 ++ src/css/dependencies.zig | 121 + src/css/error.zig | 54 + src/css/logical.zig | 30 + src/css/media_query.zig | 976 +++++++ src/css/printer.zig | 310 ++ src/css/properties/custom.zig | 925 ++++++ src/css/properties/properties.zig | 3643 +++++++++++++++++++++++ src/css/rules/custom_media.zig | 33 + src/css/rules/layer.zig | 12 + src/css/rules/namespace.zig | 37 + src/css/rules/rules.zig | 1560 ++++++++++ src/css/rules/supports.zig | 18 + src/css/rules/unknown.zig | 51 + src/css/selector.zig | 2885 +++++++++++++++++++ src/css/selectors/parser.zig | 8 + src/css/sourcemap.zig | 36 + src/css/targets.zig | 0 src/css/values/color.zig | 1110 ++++++++ src/css/values/ident.zig | 109 + src/css/values/image.zig | 3 + src/css/values/string.zig | 22 + src/css/values/values.zig | 627 ++++ src/meta.zig | 56 + src/string_immutable.zig | 35 + 28 files changed, 17439 insertions(+) create mode 100644 src/css/compat.zig create mode 100644 src/css/css_modules.zig create mode 100644 src/css/css_parser.zig create mode 100644 src/css/declaration.zig create mode 100644 src/css/dependencies.zig create mode 100644 src/css/error.zig create mode 100644 src/css/logical.zig create mode 100644 src/css/media_query.zig create mode 100644 src/css/printer.zig create mode 100644 src/css/properties/custom.zig create mode 100644 src/css/properties/properties.zig create mode 100644 src/css/rules/custom_media.zig create mode 100644 src/css/rules/layer.zig create mode 100644 src/css/rules/namespace.zig create mode 100644 src/css/rules/rules.zig create mode 100644 src/css/rules/supports.zig create mode 100644 src/css/rules/unknown.zig create mode 100644 src/css/selector.zig create mode 100644 src/css/selectors/parser.zig create mode 100644 src/css/sourcemap.zig create mode 100644 src/css/targets.zig create mode 100644 src/css/values/color.zig create mode 100644 src/css/values/ident.zig create mode 100644 src/css/values/image.zig create mode 100644 src/css/values/string.zig create mode 100644 src/css/values/values.zig diff --git a/src/css/compat.zig b/src/css/compat.zig new file mode 100644 index 0000000000000..235c40176eb10 --- /dev/null +++ b/src/css/compat.zig @@ -0,0 +1,15 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); + +// TODO: this should be generated +pub const Feature = enum { + comptime { + @compileError(css.todo_stuff.depth); + } +}; diff --git a/src/css/css_modules.zig b/src/css/css_modules.zig new file mode 100644 index 0000000000000..aca7dd90cfeb7 --- /dev/null +++ b/src/css/css_modules.zig @@ -0,0 +1,140 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); +const DashedIdent = css_values.ident.DashedIdent; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; + +const ArrayList = std.ArrayListUnmanaged; + +const CssModule = struct { + config: *const Config, + sources: ArrayList(*const bun.PathString), + hashes: ArrayList([]const u8), + exports_by_source_index: ArrayList(CssModuleExports), + references: *std.HashMap([]const u8, CssModuleReference), + + pub fn new( + config: *const Config, + sources: *const ArrayList([]const u8), + project_root: ?[]const u8, + references: *std.StringArrayHashMap(CssModuleReference), + ) CssModule { + _ = config; // autofix + _ = sources; // autofix + _ = project_root; // autofix + _ = references; // autofix + @compileError(css.todo_stuff.errors); + } + + pub fn handleComposes( + this: *CssModule, + selectors: *const css.selector.api.SelectorList, + composes: *const css.css_properties.css_modules.Composes, + source_index: u32, + ) css.PrintErr!void { + _ = this; // autofix + _ = selectors; // autofix + _ = composes; // autofix + _ = source_index; // autofix + @compileError(css.todo_stuff.errors); + } +}; + +/// Configuration for CSS modules. +pub const Config = struct { + /// The name pattern to use when renaming class names and other identifiers. + /// Default is `[hash]_[local]`. + pattern: Pattern, + + /// Whether to rename dashed identifiers, e.g. custom properties. + dashed_idents: bool, + + /// Whether to scope animation names. + /// Default is `true`. + animation: bool, + + /// Whether to scope grid names. + /// Default is `true`. + grid: bool, + + /// Whether to scope custom identifiers + /// Default is `true`. + custom_idents: bool, +}; + +/// A CSS modules class name pattern. +pub const Pattern = struct { + /// The list of segments in the pattern. + segments: css.SmallList(Segment, 2), +}; + +/// A segment in a CSS modules class name pattern. +/// +/// See [Pattern](Pattern). +pub const Segment = union(enum) { + /// A literal string segment. + literal: []const u8, + + /// The base file name. + name, + + /// The original class name. + local, + + /// A hash of the file name. + hash, +}; + +/// A map of exported names to values. +pub const CssModuleExports = std.StringArrayHashMap(CssModuleExport); + +/// A map of placeholders to references. +pub const CssModuleReferences = std.StringArrayHashMap(CssModuleReference); + +/// An exported value from a CSS module. +pub const CssModuleExport = struct { + /// The local (compiled) name for this export. + name: []const u8, + /// Other names that are composed by this export. + composes: ArrayList(CssModuleReference), + /// Whether the export is referenced in this file. + is_referenced: bool, +}; + +/// A referenced name within a CSS module, e.g. via the `composes` property. +/// +/// See [CssModuleExport](CssModuleExport). +pub const CssModuleReference = union(enum) { + /// A local reference. + local: struct { + /// The local (compiled) name for the reference. + name: []const u8, + }, + /// A global reference. + global: struct { + /// The referenced global name. + name: []const u8, + }, + /// A reference to an export in a different file. + dependency: struct { + /// The name to reference within the dependency. + name: []const u8, + /// The dependency specifier for the referenced file. + specifier: []const u8, + }, +}; + +// TODO: replace with bun's hash +pub fn hash(allocator: Allocator, comptime fmt: []const u8, args: anytype, at_start: bool) []const u8 { + _ = fmt; // autofix + _ = args; // autofix + _ = allocator; // autofix + _ = at_start; // autofix + @compileError(css.todo_stuff.depth); +} diff --git a/src/css/css_parser.zig b/src/css/css_parser.zig new file mode 100644 index 0000000000000..7abe58696ab52 --- /dev/null +++ b/src/css/css_parser.zig @@ -0,0 +1,4440 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +const ArrayList = std.ArrayListUnmanaged; + +pub const dependencies = @import("./dependencies.zig"); +pub const Dependency = dependencies.Dependency; + +pub const css_modules = @import("./css_modules.zig"); +pub const CssModuleExports = css_modules.CssModuleExports; +pub const CssModule = css_modules.CssModule; +pub const CssModuleReferences = css_modules.CssModuleReferences; + +pub const css_rules = @import("./rules/rules.zig"); +pub const CssRule = css_rules.CssRule; +pub const CssRuleList = css_rules.CssRuleList; +pub const LayerName = css_rules.layer.LayerName; +pub const SupportsCondition = css_rules.supports.SupportsCondition; +pub const CustomMedia = css_rules.custom_media.CustomMediaRule; +pub const NamespaceRule = css_rules.namespace.NamespaceRule; +pub const UnknownAtRule = css_rules.unknown.UnknownAtRule; +pub const ImportRule = css_rules.import.ImportRule; +pub const StyleRule = css_rules.style.StyleRule; + +const media_query = @import("./media_query.zig"); +pub const MediaList = media_query.MediaList; + +pub const css_values = @import("./values/values.zig"); +pub const DashedIdent = css_values.ident.DashedIdent; +pub const DashedIdentFns = css_values.ident.DashedIdentFns; +pub const CSSString = css_values.string.CSSString; +pub const Ident = css_values.ident.Ident; +pub const CustomIdent = css_values.ident.CustomIdent; +pub const CustomIdentFns = css_values.ident.CustomIdentFns; + +pub const css_properties = @import("./properties/properties.zig"); +pub const Property = css_properties.Property; +pub const PropertyId = Property.Id; +pub const TokenList = css_properties.custom.TokenList; +pub const TokenListFns = css_properties.custom.TokenListFns; + +const css_decls = @import("./declaration.zig"); +pub const DeclarationList = css_decls.DeclarationList; +pub const DeclarationBlock = css_decls.DeclarationBlock; + +pub const selector = @import("./selector.zig"); +pub const SelectorList = selector.api.SelectorList; + +pub const logical = @import("./logical.zig"); +pub const PropertyCategory = logical.PropertyCategory; +pub const LogicalGroup = logical.LogicalGroup; + +pub const css_printer = @import("./printer.zig"); +pub const Printer = css_printer.Printer; +pub const PrinterOptions = css_printer.PrinterOptions; +pub const Targets = css_printer.Targets; +pub const Features = css_printer.Features; + +pub const Maybe = bun.JSC.Node.Maybe; +// TODO: Remove existing Error defined here and replace it with these +const errors_ = @import("./error.zig"); +pub const Err = errors_.Error; +pub const PrinterErrorKind = errors_.PrinterErrorKind; +pub const PrinterError = errors_.PrinterError; + +const compat = @import("./compat.zig"); + +pub const PrintErr = error{}; + +pub fn SmallList(comptime T: type, comptime N: comptime_int) type { + _ = N; // autofix + { + @compileError(todo_stuff.smallvec); + } + return ArrayList(T); +} + +pub fn Bitflags(comptime T: type) type { + const tyinfo = @typeInfo(T); + const IntType = tyinfo.Struct.backing_integer.?; + + return struct { + pub inline fn empty() T { + return @bitCast(0); + } + + pub inline fn fromName(comptime name: []const u8) T { + var this: T = .{}; + @field(this, name) = true; + return this; + } + + pub fn bitwiseOr(lhs: T, rhs: T) T { + return @bitCast(@as(IntType, @bitCast(lhs)) | @as(IntType, @bitCast(rhs))); + } + + pub fn insert(this: T, other: T) T { + return bitwiseOr(this, other); + } + + pub fn contains(lhs: T, rhs: T) bool { + return @as(IntType, @bitCast(lhs)) & @as(IntType, @bitCast(rhs)) != 0; + } + + pub inline fn asBits(this: T) IntType { + return @as(IntType, @bitCast(this)); + } + + pub fn isEmpty(this: T) bool { + return asBits(this) == 0; + } + + pub fn eq(lhs: T, rhs: T) bool { + return asBits(lhs) == asBits(rhs); + } + + pub fn neq(lhs: T, rhs: T) bool { + return asBits(lhs) != asBits(rhs); + } + }; +} + +pub const todo_stuff = struct { + pub const think_about_allocator = "TODO: think about how to pass allocator"; + + pub const think_mem_mgmt = "TODO: think about memory management"; + + pub const depth = "TODO: we need to go deeper"; + + pub const errors = "TODO: think about errors"; + + pub const smallvec = "TODO: implement smallvec"; + + pub const match_ignore_ascii_case = "TODO: implement match_ignore_ascii_case"; + + pub const enum_property = "TODO: implement enum_property!"; + + pub const match_byte = "TODO: implement match_byte!"; +}; + +pub const VendorPrefix = packed struct(u8) { + /// No vendor prefixes. + /// 0b00000001 + none: bool = false, + /// The `-webkit` vendor prefix. + /// 0b00000010 + webkit: bool = false, + /// The `-moz` vendor prefix. + /// 0b00000100 + moz: bool = false, + /// The `-ms` vendor prefix. + /// 0b00001000 + ms: bool = false, + /// The `-o` vendor prefix. + /// 0b00010000 + o: bool = false, + __unused: u3 = 0, + + pub usingnamespace Bitflags(@This()); + + pub fn toCss(this: *const VendorPrefix, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(todo_stuff.depth); + } + + /// Returns VendorPrefix::None if empty. + pub fn orNone(this: VendorPrefix) VendorPrefix { + return this.@"or"(VendorPrefix{ .none = true }); + } + + pub fn @"or"(this: VendorPrefix, other: VendorPrefix) VendorPrefix { + if (this.isEmpty()) return other; + return this; + } + + pub fn bitwiseOr(lhs: VendorPrefix, rhs: VendorPrefix) VendorPrefix { + return @bitCast(@as(u8, @bitCast(lhs)) | @as(u8, @bitCast(rhs))); + } +}; + +pub const SourceLocation = struct { + line: u32, + column: u32, +}; +pub const Location = css_rules.Location; + +/// do not add any more errors +pub const Error = error{ + ParsingError, +}; + +/// Details about a `BasicParseError` +pub const BasicParseErrorKind = union(enum) { + /// An unexpected token was encountered. + unexpected_token: Token, + + /// The end of the input was encountered unexpectedly. + end_of_input, + + /// An `@` rule was encountered that was invalid. + at_rule_invalid: []const u8, + + /// The body of an '@' rule was invalid. + at_rule_body_invalid, + + /// A qualified rule was encountered that was invalid. + qualified_rule_invalid, +}; + +pub fn todo(comptime fmt: []const u8, args: anytype) noreturn { + std.debug.panic("TODO: " ++ fmt, args); +} + +pub fn todo2(comptime fmt: []const u8) void { + std.debug.panic("TODO: " ++ fmt); +} + +pub fn voidWrap(comptime T: type, comptime parsefn: *const fn (*Parser) Error!T) *const fn (void, *Parser) Error!T { + const Wrapper = struct { + fn wrapped(_: void, p: *Parser) Error!T { + parsefn(p); + } + }; + return Wrapper.wrapped; +} + +pub fn DefineLengthUnits(comptime T: type) type { + return struct { + pub fn parse(input: *Parser) Error!T { + _ = input; // autofix + @compileError(todo_stuff.depth); + } + }; +} + +pub fn DefineEnumProperty(comptime T: type) type { + const fields: []const std.builtin.Type.EnumField = std.meta.fields(T); + + return struct { + pub fn asStr(this: *const T) []const u8 { + const tag = @intFromEnum(this); + inline for (fields) |field| { + if (tag == field.tag) return field.name; + } + unreachable; + } + + pub fn parse(input: *Parser) Error!T { + const location = input.currentSourceLocation(); + const ident = try input.expectIdent(); + + // todo_stuff.match_ignore_ascii_case + inline for (fields) |field| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, field.name)) return @enumFromInt(field.value); + } + + return location.newUnexpectedTokenError(.{ .ident = ident }); + } + + pub fn toCss(this: *const T, comptime W: type, dest: *Printer(W)) PrintErr!void { + try dest.writeStr(asStr(this)); + } + }; +} + +pub fn DefineListShorthand(comptime T: type) type { + _ = T; // autofix + @compileError(todo_stuff.depth); +} + +fn consume_until_end_of_block(block_type: BlockType, tokenizer: *Tokenizer) void { + const StackCount = 16; + var sfb = std.heap.stackFallback(@sizeOf(BlockType) * StackCount, @compileError(todo_stuff.think_about_allocator)); + const alloc = sfb.get(); + var stack = std.ArrayList(BlockType).initCapacity(alloc, StackCount) catch unreachable; + defer stack.deinit(); + + stack.appendAssumeCapacity(block_type); + + while (tokenizer.next()) |tok| { + if (tok == .eof) break; + if (BlockType.closing(&tok)) |b| { + if (stack.getLast() == b) { + stack.pop(); + if (stack.items.len == 0) return; + } + } + + if (BlockType.opening(&tok)) stack.append(tok) catch unreachable; + } +} + +fn parse_at_rule( + allocator: Allocator, + start: *const ParserState, + name: []const u8, + input: *Parser, + comptime P: type, + parser: *P, +) Error!P.AtRuleParser.AtRule { + ValidAtRuleParser(P); + const delimiters = Delimiters{ .semicolon = true, .curly_bracket = true }; + const Closure = struct { + name: []const u8, + parser: *P, + + pub fn parsefn(this: *@This(), input2: *Parser) Error!P.AtRuleParser.Prelude { + return this.parser.AtRuleParser.parsePrelude(this.name, input2); + } + }; + var closure = Closure{ .name = name, .parser = parser }; + const prelude: P.AtRuleParser.Prelude = input.parseUntilBefore(delimiters, P.AtRuleParser.Prelude, &closure, closure.parsefn) catch |e| { + const end_position = input.position(); + _ = end_position; // autofix + out: { + const tok = input.next() catch break :out; + if (tok.* != .open_curly and tok != .semicolon) unreachable; + } + return e; + }; + const next = input.next() catch { + return P.AtRuleParser.ruleWithoutBlock(allocator, parser, prelude, start); + }; + switch (next.*) { + .semicolon => return P.AtRuleParser.ruleWithoutBlock(allocator, parser, prelude, start), + .open_curly => { + const AnotherClosure = struct { + prelude: *P.AtRuleParser.Prelude, + start: *const ParserState, + parser: *P, + pub fn parsefn(this: *@This(), input2: *Parser) Error!P.AtRuleParser.AtRule { + return P.AtRuleParser.parseBlock(this.parser, this.prelude, this.start, input2); + } + }; + var another_closure = AnotherClosure{ + .prelude = &prelude, + .start = start, + .parser = parser, + }; + return parse_nested_block(input, P.AtRuleParser.AtRule, &another_closure, AnotherClosure.parsefn); + }, + } +} + +fn parse_custom_at_rule_prelude(name: []const u8, input: *Parser, options: *ParserOptions, comptime T: type, at_rule_parser: *T) Error!AtRulePrelude(T.AtRuleParser.AtRule) { + ValidCustomAtRuleParser(T); + if (at_rule_parser.CustomAtRuleParser.parsePrelude(at_rule_parser, name, input, options)) |prelude| { + return .{ .custom = prelude }; + } else { + // } else |e| unknown: { + // TODO: error does not exist but should exist + // if (e == Error.at_rule_invalid) break :brk unknown; + return input.newCustomError(.at_rule_prelude_invalid); + } + + options.warn(input.newError(.{ .at_rule_invalid = name })); + input.skipWhitespace(); + const tokens = try TokenListFns.parse(input, options, 0); + return .{ .unknown = .{ + .name = name, + .tokens = tokens, + } }; +} + +fn parse_custom_at_rule_body( + comptime T: type, + prelude: T.CustomAtRuleParser.Prelude, + input: *Parser, + start: *const ParserState, + options: *ParserOptions, + at_rule_parser: *T, + is_nested: bool, +) Error!T.CustomAtRuleParser.AtRule { + const result = T.CustomAtRuleParser.parseBlock(at_rule_parser, prelude, start, input, options, is_nested) catch |e| { + _ = e; // autofix + // match &err.kind { + // ParseErrorKind::Basic(kind) => ParseError { + // kind: ParseErrorKind::Basic(kind.clone()), + // location: err.location, + // }, + // _ => input.new_error(BasicParseErrorKind::AtRuleBodyInvalid), + // } + todo("This part here", .{}); + }; + return result; +} + +fn parse_qualified_rule( + start: *const ParserState, + input: *Parser, + comptime P: type, + parser: *P, + delimiters: Delimiters, +) Error!P.QualifiedRuleParser.QualifiedRule { + ValidQualifiedRuleParser(P); + const prelude_result = brk: { + const prelude = input.parseUntilBefore(delimiters, P.QualifiedRuleParser.Prelude, parser, parser.QualifiedRuleParser.parsePrelude); + break :brk prelude; + }; + try input.expectCurlyBracketBlock(); + const prelude = try prelude_result; + const Closure = struct { + start: *const ParserState, + prelude: P.QualifiedRuleParser.Prelude, + parser: *P, + + pub fn parsefn(this: *@This(), input2: *Parser) Error!P.QualifiedRuleParser.QualifiedRule { + P.QualifiedRuleParser.parseBlock(this.parser, this.prelude, this.start, input2); + } + }; + var closure = Closure{ + .start = start, + .prelude = prelude, + .parser = parser, + }; + return parse_nested_block(input, P.QualifiedRuleParser.QualifiedRule, &closure, Closure.parsefn); +} + +fn parse_until_before( + parser: *Parser, + delimiters_: Delimiters, + error_behavior: ParseUntilErrorBehavior, + comptime T: type, + closure: anytype, + comptime parse_fn: *const fn (@TypeOf(closure), *Parser) Error!T, +) Error!T { + const delimiters = parser.stop_before.bitwiseOr(delimiters_); + const result = result: { + var delimited_parser = Parser{ + .input = parser.input, + .at_start_of = if (parser.at_start_of) |block_type| brk: { + parser.at_start_of = null; + break :brk block_type; + } else null, + }; + const result = delimited_parser.parseEntirely(T, closure, parse_fn); + const is_result = if (result) |_| false else true; + if (error_behavior == .stop and is_result) { + return result; + } + if (delimited_parser.at_start_of) |block_type| { + consume_until_end_of_block(block_type, &delimited_parser.input.tokenizer); + } + break :result result; + }; + + // FIXME: have a special-purpose tokenizer method for this that does less work. + while (true) { + if (delimiters.contains(Delimiters.fromByte(parser.input.tokenizer.nextByte()))) break; + + if (parser.input.tokenizer.next()) |token| { + if (BlockType.opening(&token)) |block_type| { + consume_until_end_of_block(block_type, &parser.input.tokenizer); + } + } else { + break; + } + } + + return result; +} + +// fn parse_until_before_impl(parser: *Parser, delimiters: Delimiters, error_behavior: Parse + +pub fn parse_until_after( + parser: *Parser, + delimiters: Delimiters, + error_behavior: ParseUntilErrorBehavior, + comptime T: type, + closure: anytype, + comptime parsefn: *const fn (@TypeOf(closure), *Parser) Error!T, +) Error!T { + const result = parse_until_before(parser, delimiters, error_behavior, T, closure, parsefn); + const is_err = if (result) |_| false else true; + if (error_behavior == .stop and is_err) { + return result; + } + const next_byte = parser.input.tokenizer.nextByte(); + if (next_byte != null and !parser.stop_before.contains(Delimiters.fromByte(next_byte))) { + bun.debugAssert(delimiters.contains(Delimiters.from_byte(next_byte))); + // We know this byte is ASCII. + parser.input.tokenizer.advance(1); + if (next_byte == '{') { + consume_until_end_of_block(BlockType.curly_bracket, &parser.input.tokenizer); + } + } + return result; +} + +fn parse_nested_block(parser: *Parser, comptime T: type, closure: anytype, comptime parsefn: *const fn (@TypeOf(closure), *Parser) Error!T) Error!T { + const block_type: BlockType = if (parser.at_start_of) |block_type| brk: { + parser.at_start_of = null; + break :brk block_type; + } else @panic( + \\ + \\A nested parser can only be created when a Function, + \\ParenthisisBlock, SquareBracketBlock, or CurlyBracketBlock + \\token was just consumed. + ); + + const closing_delimiter = switch (block_type) { + .curly_bracket => Delimiters{ .close_curly_bracket = true }, + .square_bracket => Delimiters{ .close_square_bracket = true }, + .parenthesis => Delimiters{ .close_parenthesis = true }, + }; + const nested_parser = Parser{ + .input = parser.input, + .stop_before = closing_delimiter, + }; + const result = nested_parser.parseEntirely(T, closure, parsefn); + if (nested_parser.at_start_of) |block_type2| { + consume_until_end_of_block(block_type2, &nested_parser.input.tokenizer); + } + consume_until_end_of_block(block_type, &parser.input.tokenizer); + return result; +} + +pub fn ValidQualifiedRuleParser(comptime T: type) void { + // The intermediate representation of a qualified rule prelude. + _ = T.QualifiedRuleParser.Prelude; + + // The finished representation of a qualified rule. + _ = T.QualifiedRuleParser.QualifiedRule; + + // Parse the prelude of a qualified rule. For style rules, this is as Selector list. + // + // Return the representation of the prelude, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // The prelude is the part before the `{ /* ... */ }` block. + // + // The given `input` is a "delimited" parser + // that ends where the prelude should end (before the next `{`). + // + // fn parsePrelude(this: *T, input: *Parser) Error!T.QualifiedRuleParser.Prelude; + _ = T.QualifiedRuleParser.parsePrelude; + + // Parse the content of a `{ /* ... */ }` block for the body of the qualified rule. + // + // The location passed in is source location of the start of the prelude. + // + // Return the finished representation of the qualified rule + // as returned by `RuleListParser::next`, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // fn parseBlock(this: *T, prelude: P.QualifiedRuleParser.Prelude, start: *const ParserState, input: *Parser) Error!P.QualifiedRuleParser.QualifiedRule; + _ = T.QualifiedRuleParser.parseBlock; +} + +pub const DefaultAtRule = struct {}; + +/// Same as `ValidAtRuleParser` but modified to provide parser options +pub fn ValidCustomAtRuleParser(comptime T: type) void { + // The intermediate representation of prelude of an at-rule. + _ = T.CustomAtRuleParser.Prelude; + + // The finished representation of an at-rule. + _ = T.CustomAtRuleParser.AtRule; + + // Parse the prelude of an at-rule with the given `name`. + // + // Return the representation of the prelude and the type of at-rule, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // The prelude is the part after the at-keyword + // and before the `;` semicolon or `{ /* ... */ }` block. + // + // At-rule name matching should be case-insensitive in the ASCII range. + // This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + // or with the `match_ignore_ascii_case!` macro. + // + // The given `input` is a "delimited" parser + // that ends wherever the prelude should end. + // (Before the next semicolon, the next `{`, or the end of the current block.) + // + // pub fn parsePrelude(this: *T, allocator: Allocator, name: []const u8, *Parser, options: *ParserOptions) Error!T.CustomAtRuleParser.Prelude {} + _ = T.CustomAtRuleParser.parsePrelude; + + // End an at-rule which doesn't have block. Return the finished + // representation of the at-rule. + // + // The location passed in is source location of the start of the prelude. + // `is_nested` indicates whether the rule is nested inside a style rule. + // + // This is only called when either the `;` semicolon indeed follows the prelude, + // or parser is at the end of the input. + _ = T.CustomAtRuleParser.ruleWithoutBlock; + + // Parse the content of a `{ /* ... */ }` block for the body of the at-rule. + // + // The location passed in is source location of the start of the prelude. + // `is_nested` indicates whether the rule is nested inside a style rule. + // + // Return the finished representation of the at-rule + // as returned by `RuleListParser::next` or `DeclarationListParser::next`, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // This is only called when a block was found following the prelude. + _ = T.CustomAtRuleParser.parseBlock; +} + +pub fn ValidAtRuleParser(comptime T: type) void { + _ = T.AtRuleParser.AtRule; + _ = T.AtRuleParser.Prelude; + + // Parse the prelude of an at-rule with the given `name`. + // + // Return the representation of the prelude and the type of at-rule, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // The prelude is the part after the at-keyword + // and before the `;` semicolon or `{ /* ... */ }` block. + // + // At-rule name matching should be case-insensitive in the ASCII range. + // This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + // or with the `match_ignore_ascii_case!` macro. + // + // The given `input` is a "delimited" parser + // that ends wherever the prelude should end. + // (Before the next semicolon, the next `{`, or the end of the current block.) + // + // pub fn parsePrelude(this: *T, allocator: Allocator, name: []const u8, *Parser) Error!T.AtRuleParser.Prelude {} + _ = T.AtRuleParser.parsePrelude; + + // End an at-rule which doesn't have block. Return the finished + // representation of the at-rule. + // + // The location passed in is source location of the start of the prelude. + // + // This is only called when `parse_prelude` returned `WithoutBlock`, and + // either the `;` semicolon indeed follows the prelude, or parser is at + // the end of the input. + // fn ruleWithoutBlock(this: *T, allocator: Allocator, prelude: T.AtRuleParser.Prelude, state: *const ParserState) Error!T.AtRuleParser.AtRule + _ = T.AtRuleParser.ruleWithoutBlock; + + // Parse the content of a `{ /* ... */ }` block for the body of the at-rule. + // + // The location passed in is source location of the start of the prelude. + // + // Return the finished representation of the at-rule + // as returned by `RuleListParser::next` or `DeclarationListParser::next`, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // This is only called when `parse_prelude` returned `WithBlock`, and a block + // was indeed found following the prelude. + // + // fn parseBlock(this: *T, prelude: T.AtRuleParser.Prelude, start: *const ParserState, input: *Parser) Error!T.AtRuleParser.AtRule + _ = T.AtRuleParser.parseBlock; +} + +pub fn AtRulePrelude(comptime T: type) type { + return union(enum) { + // TODO put the comments here + font_face, + font_feature_values, + font_palette_values: DashedIdent, + import: struct { + []const u8, + MediaList, + ?SupportsCondition, + ?struct { value: ?LayerName }, + }, + namespace: struct { + ?[]const u8, + []const u8, + }, + charset, + custom_media: struct { + DashedIdent, + MediaList, + }, + property: struct { + DashedIdent, + }, + media: MediaList, + supports: SupportsCondition, + viewport: VendorPrefix, + keyframes: struct { + name: css_rules.keyframes.KeyframesName, + prefix: VendorPrefix, + }, + page: ArrayList(css_rules.page.PageSelector), + moz_document, + layer: ArrayList(LayerName), + container: struct { + name: ?css_rules.container.ContainerName, + condition: css_rules.container.ContainerCondition, + }, + starting_style, + nest: selector.api.SelectorList, + scope: struct { + scope_start: ?selector.api.SelectorList, + scope_end: ?selector.api.SelectorList, + }, + unknown: struct { + name: []const u8, + /// The tokens of the prelude + tokens: TokenList, + }, + custom: T, + // ZACK YOU ARE IN AT RULE PRELUDE I REPEAT AT RULE PRELUDE + // TODO + + pub fn allowedInStyleRule(this: *const @This()) bool { + return switch (this.*) { + .media, .supports, .container, .moz_document, .layer, .starting_style, .scope, .nest, .unknown, .custom => true, + .namespace, .font_face, .font_feature_values, .font_palette_values, .counter_style, .keyframes, .page, .property, .import, .custom_media, .viewport, .charset => false, + }; + } + }; +} + +pub fn TopLevelRuleParser(comptime AtRuleParserT: type) type { + ValidAtRuleParser(AtRuleParserT); + const AtRuleT = AtRuleParserT.AtRuleParser.AtRule; + const AtRulePreludeT = AtRulePrelude(AtRuleParserT.AtRuleParser.Prelude); + + return struct { + allocator: Allocator, + options: *ParserOptions, + state: State, + at_rule_parser: *AtRuleParserT, + // TODO: think about memory management + rules: *CssRuleList(AtRuleT), + + const State = enum(u8) { + start = 1, + layers = 2, + imports = 3, + namespaces = 4, + body = 5, + }; + + const This = @This(); + + pub const AtRuleParser = struct { + pub const Prelude = AtRulePreludeT; + pub const AtRule = void; + + pub fn parsePrelude(this: *This, name: []const u8, input: *Parser) Error!Prelude { + // TODO: optimize string switch + // So rust does the strategy of: + // 1. switch (or if branches) on the length of the input string + // 2. then do string comparison by word size (or smaller sometimes) + // rust sometimes makes jump table https://godbolt.org/z/63d5vYnsP + // sometimes it doesn't make a jump table and just does branching on lengths: https://godbolt.org/z/d8jGPEd56 + // it looks like it will only make a jump table when it knows it won't be too sparse? If I add a "h" case (to make it go 1, 2, 4, 5) or a "hzz" case (so it goes 2, 3, 4, 5) it works: + // - https://godbolt.org/z/WGTMPxafs (change "hzz" to "h" and it works too, remove it and jump table is gone) + // + // I tried recreating the jump table (first link) by hand: https://godbolt.org/z/WPM5c5K4b + // it worked fairly well. Well I actually just made it match on the length, compiler made the jump table, + // so we should let the compiler make the jump table. + // Another recreation with some more nuances: https://godbolt.org/z/9Y1eKdY3r + // Another recreation where hand written is faster than the Rust compiler: https://godbolt.org/z/sTarKe4Yx + // specifically we can make the compiler generate a jump table instead of brancing + // + // Our ExactSizeMatcher is decent + // or comptime string map that calls eqlcomptime function thingy, or std.StaticStringMap + // rust-cssparser does a thing where it allocates stack buffer with maximum possible size and + // then uses that to do ASCII to lowercase conversion: + // https://github.com/servo/rust-cssparser/blob/b75ce6a8df2dbd712fac9d49ba38ee09b96d0d52/src/macros.rs#L168 + // we could probably do something similar, looks like the max length never goes above 20 bytes + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "import")) { + if (@intFromEnum(this.state) > @intFromEnum(State.imports)) { + input.newCustomError(.unexpected_import_rule); + return Error.ParsingError; + } + + const url_str = try input.expectUrlOrString(); + + const layer: ?struct { value: ?LayerName } = + if (input.tryParse(Parser.expectIdentMatching, .{"layer"}) != Error.ParsingError) + .{ .value = null } + else if (input.tryParse(Parser.expectFunctionMatching, .{"layer"}) != Error.ParsingError) brk: { + break :brk .{ .value = try input.parseNestedBlock(LayerName, void, voidWrap(LayerName, LayerName.parse)) }; + } else null; + + const supports = if (input.tryParse(Parser.expectFunctionMatching, .{"supports"}) != Error.ParsingError) brk: { + const Func = struct { + pub fn do(p: *Parser) Error!SupportsCondition { + return p.tryParse(SupportsCondition.parse, .{}) catch { + return SupportsCondition.parseDeclaration(p); + }; + } + }; + break :brk try input.parseNestedBlock(SupportsCondition, void, voidWrap(SupportsCondition, Func.do)); + } else null; + + const media = try MediaList.parse(input); + + return .{ + .import = .{ + url_str, + media, + supports, + layer, + }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "namespace")) { + if (@intFromEnum(this.state) > @intFromEnum(State.namespaces)) { + input.newCustomError(.unexpected_namespace_rule); + return Error.ParsingError; + } + + const prefix = input.tryParse(Parser.expectIdent, .{}) catch null; + const namespace = try input.expectUrlOrString(); + return .{ .namespace = .{ prefix, namespace } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "charset")) { + // @charset is removed by rust-cssparser if it’s the first rule in the stylesheet. + // Anything left is technically invalid, however, users often concatenate CSS files + // together, so we are more lenient and simply ignore @charset rules in the middle of a file. + try input.expectString(); + return .charset; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "custom-media")) { + const custom_media_name = try DashedIdent.parse(input); + const media = try MediaList.parse(input); + return .{ + .custom_media = .{ + .name = custom_media_name, + .media = media, + }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "property")) { + const property_name = try DashedIdent.parse(input); + return .{ .property = property_name }; + } else { + const Nested = NestedRuleParser(AtRuleParserT); + const nested_rule_parser: Nested = this.nested(); + return Nested.AtRuleParser.parsePrelude(&nested_rule_parser, name, input); + } + } + + pub fn parseBlock(this: *This, prelude: AtRuleParser.Prelude, start: *const ParserState, input: *Parser) Error!AtRuleParser.AtRule { + this.state = .body; + const nested_parser = this.nested(); + return NestedRuleParser(AtRuleParserT).AtRuleParser.parseBlock(nested_parser, prelude, start, input); + } + + pub fn ruleWithoutBlock(this: *This, prelude: AtRuleParser.Prelude, start: *const ParserState) Error!AtRuleParser.AtRule { + const loc_ = start.sourceLocation(); + const loc = css_rules.Location{ + .source_index = this.options.source_index, + .line = loc_.line, + .column = loc_.column, + }; + + switch (prelude) { + .import => { + this.state = State.imports; + this.rules.v.append(this.allocator, .{ + .import = ImportRule{ + .url = prelude.import[0], + .media = prelude.import[1], + .supports = prelude.import[2], + .layer = prelude.import[3], + }, + }); + return; + }, + .namespace => { + this.state = State.namespaces; + + const prefix = prelude.namespace[0]; + const url = prelude.namespace[1]; + + this.rules.v.append(this.allocator, .{ + .namespace = NamespaceRule{ + .prefix = prefix, + .url = url, + .loc = loc, + }, + }); + + return; + }, + .custom_media => { + this.state = State.body; + this.rules.v.append( + this.allocator, + .{ + .custom_media = css_rules.custom_media.CustomMediaRule{ + .name = prelude.custom_media.name, + .query = prelude.custom_media.query, + .loc = prelude.custom_media.loc, + }, + }, + ); + }, + .layer => { + if (@intFromEnum(this.state) <= @intFromEnum(State.layers)) { + this.state = .layers; + } else { + this.state = .body; + } + const nested_parser = this.nested(); + return NestedRuleParser(AtRuleParserT).AtRuleParser.parseBlock(nested_parser, prelude, start); + }, + .charset => {}, + .unknown => { + const name = prelude.unknown[0]; + const prelude2 = prelude.unknown[1]; + this.rules.v.append(this.allocator, .{ .unknown = UnknownAtRule{ + .name = name, + .prelude = prelude2, + .block = null, + .loc = loc, + } }); + }, + .custom => { + this.state = .body; + const nested_parser = this.nested(); + return NestedRuleParser(AtRuleParserT).AtRuleParser.parseBlock(nested_parser, prelude, start); + }, + else => error.ParsingError, + } + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = selector.api.SelectorList; + pub const QualifiedRule = void; + + pub fn parsePrelude(this: *This, input: *Parser) Error!Prelude { + this.state = .body; + var nested_parser = this.nested(); + return nested_parser.QualifiedRuleParser.parsePrelude(&nested_parser, input); + } + + pub fn parseBlock(this: *This, prelude: Prelude, start: *const ParserState, input: *Parser) Error!QualifiedRule { + var nested_parser = this.nested(); + return nested_parser.QualifiedRuleParser.parseBlock(&nested_parser, prelude, start, input); + } + }; + + pub fn new(options: *ParserOptions, at_rule_parser: *AtRuleParser, rules: *CssRuleList(AtRuleT)) @This() { + return .{ + .options = options, + .state = .start, + .at_rule_parser = at_rule_parser, + .rules = rules, + }; + } + + pub fn nested(this: *This) NestedRuleParser(AtRuleParserT) { + return NestedRuleParser(AtRuleParserT){ + .options = this.options, + .at_rule_parser = this.at_rule_parser, + .declarations = DeclarationList{}, + .important_declarations = DeclarationList{}, + .rules = &this.rules, + .is_in_style_rule = false, + .allow_declarations = false, + }; + } + }; +} + +pub fn NestedRuleParser(comptime T: type) type { + ValidCustomAtRuleParser(T); + + return struct { + options: *const ParserOptions, + at_rule_parser: *T, + // todo_stuff.think_mem_mgmt + declarations: DeclarationList, + // todo_stuff.think_mem_mgmt + important_declarations: DeclarationList, + // todo_stuff.think_mem_mgmt + rules: *CssRuleList(T.CustomAtRuleParser.AtRule), + is_in_style_rule: bool, + allow_declarations: bool, + + const This = @This(); + + pub fn getLoc(this: *This, start: *ParserState) Location { + const loc = start.sourceLocation(); + return Location{ + .source_index = this.options.source_index, + .line = loc.line, + .column = loc.column, + }; + } + + pub const AtRuleParser = struct { + pub const Prelude = AtRulePrelude(T.CustomAtRuleParser.Prelude); + pub const AtRule = void; + + pub fn parsePrelude(this: *This, name: []const u8, input: *Parser) Error!Prelude { + const result: Prelude = brk: { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "media")) { + const media = try MediaList.parse(input); + break :brk .{ .media = media }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "supports")) { + const cond = try SupportsCondition.parse(input); + break :brk .{ .supports = cond }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "font-face")) { + break :brk .font_face; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "font-palette-values")) { + const dashed_ident_name = try DashedIdentFns.parse(input); + break :brk .{ .font_palette_values = dashed_ident_name }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "counter-style")) { + const custom_name = try CustomIdentFns.parse(input); + break :brk .{ .counter_style = custom_name }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "viewport") or bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-viewport")) { + const prefix: VendorPrefix = if (bun.strings.startsWithCaseInsensitiveAscii(name, "-ms")) .ms else .none; + break :brk .{ .viewport = prefix }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "keyframes") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-viewport") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-keyframes") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-o-keyframes") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-keyframes")) + { + const prefix: VendorPrefix = if (bun.strings.startsWithCaseInsensitiveAscii(name, "-webkit")) + .webkit + else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-moz-")) + .moz + else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-o-")) + .o + else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-ms-")) .ms else .none; + + const keyframes_name = try input.tryParse(css_rules.keyframes.KeyframesName.parse, .{}); + break :brk .{ .keyframes = .{ .name = keyframes_name, .prefix = prefix } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "page")) { + const Fn = struct { + pub fn parsefn(input2: *Parser) Error!css_rules.page.PageSelector { + return input2.parseCommaSeparated(css_rules.page.PageSelector.parse); + } + }; + const selectors = input.tryParse(Fn.parsefn, .{}); + break :brk .{ .page = selectors }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-document")) { + // Firefox only supports the url-prefix() function with no arguments as a legacy CSS hack. + // See https://css-tricks.com/snippets/css/css-hacks-targeting-firefox/ + try input.expectFunctionMatching("url-prefix"); + const Fn = struct { + pub fn parsefn(_: void, input2: *Parser) Error!void { + // Firefox also allows an empty string as an argument... + // https://github.com/mozilla/gecko-dev/blob/0077f2248712a1b45bf02f0f866449f663538164/servo/components/style/stylesheets/document_rule.rs#L303 + _ = input2.tryParse(parseInner, .{}); + try input2.expectExhausted(); + } + fn parseInner(input2: *Parser) Error!void { + const s = try input2.expectString(); + if (s.len > 0) { + input2.newCustomError(.invalid_value); + return error.ParsingError; + } + return; + } + }; + try input.parseNestedBlock(void, void, Fn.parsefn); + break :brk .moz_document; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "layer")) { + const names = input.parseList(LayerName) catch |e| { + // TODO: error does not exist + // but it should exist + // if (e == Error.EndOfInput) {} + return e; + }; + break :brk .{ .layer = names }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "container")) { + const container_name = input.tryParse(css_rules.container.ContainerName.parse, .{}) catch null; + const condition = try css_rules.container.ContainerCondition.parse(input); + break :brk .{ .container = .{ .name = container_name, .condition = condition } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "starting-style")) { + break :brk .starting_style; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "scope")) { + var selector_parser = selector.api.SelectorParser{ + .is_nesting_allowed = true, + .options = this.options, + }; + const Closure = struct { + selector_parser: *selector.api.SelectorParser, + pub fn parsefn(_: void, input2: *Parser) Error!selector.api.SelectorList { + return selector.api.SelectorList.parseRelative(&this.selector_parser, input2, .ignore_invalid_selector, .none); + } + }; + var closure = Closure{ + .selector_parser = &selector_parser, + }; + + const scope_start = if (input.tryParse(Parser.expectParenthesisBlock, .{})) scope_start: { + break :scope_start try input.parseNestedBlock(selector.api.SelectorList, &closure, Closure.parsefn); + } else null; + + const scope_end = if (input.tryParse(Parser.expectIdentMatching, .{"to"})) scope_end: { + try input.expectParenthesisBlock(); + break :scope_end try input.parseNestedBlock(selector.api.SelectorList, &closure, Closure.parsefn); + } else null; + + break :brk .{ + .scope = .{ + .scope_start = scope_start, + .scope_end = scope_end, + }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nest") and this.is_in_style_rule) { + this.options.warn(input.newCustomError(.deprecated_nest_rule)); + var selector_parser = selector.api.SelectorParser{ + .is_nesting_allowed = true, + .options = this.options, + }; + const selectors = try selector.api.SelectorList.parse(&selector_parser, input, .discard_list, .contained); + break :brk .{ .nest = selectors }; + } else { + break :brk try parse_custom_at_rule_prelude(name, input, this.options, this.at_rule_parser); + } + }; + + if (this.is_in_style_rule and !result.allowedInStyleRule()) { + input.newError(.{ .at_rule_invalid = name }); + return error.ParsingError; + } + + return result; + } + + pub fn parseBlock(this: *This, prelude: AtRuleParser.Prelude, start: *const ParserState, input: *Parser) Error!AtRuleParser.AtRule { + defer { + // how should we think about deinitializing this? + // do it like this defer thing going on here? + prelude.deinit(); + @compileError(todo_stuff.think_mem_mgmt); + } + // TODO: finish + const loc = this.getLoc(start); + switch (prelude) { + .font_face => { + var decl_parser = css_rules.font_face.FontFaceDeclarationParser{}; + var parser = RuleBodyParser(css_rules.font_face.FontFaceDeclarationParser).new(input, &decl_parser); + // todo_stuff.think_mem_mgmt + var properties: ArrayList(css_rules.font_face.FontFaceProperty) = .{}; + + while (parser.next()) |result| { + if (result) |decl| { + properties.append( + @compileError(todo_stuff.think_about_allocator), + decl, + ) catch bun.outOfMemory(); + } + } + + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ .font_face = css_rules.font_face.FontFaceRule{ + .properties = properties, + .loc = loc, + } }, + ) catch bun.outOfMemory(); + }, + .font_palette_values => { + const name = prelude.font_palette_values; + const rule = try css_rules.font_face.FontPaletteValuesRule.parse(name, input, loc); + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ .font_palette_values = rule }, + ) catch bun.outOfMemory(); + }, + .counter_style => { + const name = prelude.counter_style; + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .counter_style = css_rules.counter_style.CounterStyleRule{ + .name = name, + .declarations = try DeclarationBlock.parse(input, this.options), + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + }, + .media => { + const query = prelude.media; + const rules = try this.parseStyleBlock(input); + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .media = css_rules.media.MediaRule(T.CustomAtRuleParser.AtRule){ + .query = query, + .rules = rules, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + }, + .supports => { + const condition = prelude.supports; + const rules = try this.parseStyleBlock(input); + this.rules.v.append(@compileError(todo_stuff.think_about_allocator), .{ + .supports = css_rules.supports.SupportsRule{ + .condition = condition, + .rules = rules, + .loc = loc, + }, + }) catch bun.outOfMemory(); + }, + .container => { + const rules = try this.parseStyleBlock(input); + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .container = css_rules.container.ContainerRule(T.CustomAtRuleParser.AtRule){ + .name = prelude.container.name, + .condition = prelude.container.condition, + .rules = rules, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + }, + .scope => { + const rules = try this.parseStyleBlock(input); + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .scope = css_rules.scope.ScopeRule(T.CustomAtRuleParser.AtRule){ + .scope_start = prelude.scope.scope_start, + .scope_end = prelude.scope.scope_end, + .rules = rules, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + }, + .viewport => { + this.rules.v.append(@compileError(todo_stuff.think_about_allocator), .{ + .viewport = css_rules.viewport.ViewportRule{ + .vendor_prefix = prelude.viewport, + .declarations = try DeclarationBlock.parse(input, this.options), + .loc = loc, + }, + }) catch bun.outOfMemory(); + }, + .keyframes => { + var parser = css_rules.keyframes.KeyframeListParser; + var iter = RuleBodyParser(css_rules.keyframes.KeyframeListParser).new(input, &parser); + // todo_stuff.think_mem_mgmt + var keyframes = ArrayList(css_rules.keyframes.Keyframe){}; + + while (iter.next()) |result| { + if (result) |keyframe| { + keyframes.append( + @compileError(todo_stuff.think_about_allocator), + keyframe, + ) catch bun.outOfMemory(); + } + } + + this.rules.v.append(@compileError(todo_stuff.think_about_allocator), .{ + .keyframes = css_rules.keyframes.KeyframesRule{ + .name = prelude.keyframes.name, + .keyframes = keyframes, + .vendor_prefix = prelude.keyframes.prefix, + .loc = loc, + }, + }) catch bun.outOfMemory(); + }, + .page => { + const selectors = prelude.page; + const rule = try css_rules.page.PageRule.parse(selectors, input, loc, this.options); + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ .page = rule }, + ) catch bun.outOfMemory(); + }, + .moz_document => { + const rules = try this.parseStyleBlock(input); + this.rules.v.append(@compileError(todo_stuff.think_about_allocator), .{ + .moz_document = css_rules.document.MozDocumentRule(T.CustomAtRuleParser.AtRule){ + .rules = rules, + .loc = loc, + }, + }) catch bun.outOfMemory(); + }, + .layer => { + const name = if (prelude.layer.items.len == 0) null else if (prelude.layer.items.len == 1) names: { + var out: LayerName = .{}; + std.mem.swap(LayerName, &out, &prelude.layer.items[0]); + break :names out; + } else return input.newError(.at_rule_body_invalid); + + const rules = try this.parseStyleBlock(input); + + this.rules.v.append(@compileError(todo_stuff.think_about_allocator), .{ + .layer_block = css_rules.layer.LayerBlockRule{ .name = name, .rules = rules, .loc = loc }, + }); + }, + .property => { + const name = prelude.property[0]; + this.rules.v.append(@compileError(todo_stuff.think_about_allocator), .{ + .property = try css_rules.property.PropertyRule.parse(name, input, loc), + }); + }, + .import, .namespace, .custom_media, .charset => { + // These rules don't have blocks + return input.newUnexpectedTokenError(.curly_bracket_block); + }, + .starting_style => { + const rules = try this.parseStyleBlock(input); + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .starting_style = css_rules.starting_style.StartingStyleRule{ + .rules = rules, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + }, + .nest => { + const selectors = prelude.nest; + const result = try this.parseNested(input, true); + const declarations = result[0]; + const rules = result[1]; + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .nesting = css_rules.nesting.NestingRule{ + .style = css_rules.style.StyleRule(T.CustomAtRuleParser.AtRule){ + .selectors = selectors, + .declarations = declarations, + .vendor_prefix = VendorPrefix.empty(), + .rules = rules, + .loc = loc, + }, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + }, + .font_feature_values => bun.unreachablePanic("", .{}), + .unknown => { + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .unknown = css_rules.unknown.UnknownAtRule{ + .name = prelude.unknown.name, + .prelude = prelude.unknown.tokens, + .block = try TokenListFns.parse(input, this.options, 0), + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + }, + .custom => { + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .custom = try parse_custom_at_rule_body( + T, + prelude, + input, + start, + this.options, + this.at_rule_parser, + this.is_in_style_rule, + ), + }, + ) catch bun.outOfMemory(); + }, + } + } + + pub fn ruleWithoutBlock(this: *This, prelude: AtRuleParser.Prelude, start: *const ParserState) Error!AtRuleParser.AtRule { + // TODO: finish + const loc = this.getLoc(start); + switch (prelude) { + .layer => { + if (this.is_in_style_rule or prelude.layer.names.len == 0) { + // TODO: the source actually has the return like: Result for AtRuleParser + // maybe we should make an empty error type? (EmptyError) or make it return nullable type + // return Err(()); + todo("this", .{}); + } + + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .layer_statement = css_rules.layer.LayerStatementRule{ + .names = prelude.layer.names, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + }, + .unknown => { + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .unknown = css_rules.unknown.UnknownAtRule{ + .name = prelude.unknown.name, + .prelude = prelude.unknown.tokens, + .block = null, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + }, + .custom => { + this.rules.v.append( + @compileError(todo_stuff.think_about_allocator), + .{ + .custom = try parse_custom_at_rule_body( + T, + prelude, + null, + start, + this.options, + this.at_rule_parser, + this.is_in_style_rule, + ), + }, + ) catch bun.outOfMemory(); + }, + else => { + // TODO: the source actually has the return like: Result for AtRuleParser + // maybe we should make an empty error type? (EmptyError) or make it return nullable type + // return Err(()); + todo("this", .{}); + }, + } + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = selector.api.SelectorList; + pub const QualifiedRule = void; + + pub fn parsePrelude(this: *This, input: *Parser) Error!Prelude { + var selector_parser = selector.SelectorParser{ + .is_nesting_allowed = true, + .options = this.options, + }; + + if (this.is_in_style_rule) { + return selector.api.SelectorList.parseRelative(&selector_parser, input, .discard_list, .implicit); + } else { + return selector.api.SelectorList.parse(&selector_parser, input, .discard_list, .none); + } + } + + pub fn parseBlock(this: *This, selectors: Prelude, start: *const ParserState, input: *Parser) Error!QualifiedRule { + const loc = this.getLoc(start); + const result = try this.parseNested(input, true); + const declarations = result[0]; + const rules = result[1]; + + this.rules.v.append(this.allocator(), .{ + .style = StyleRule{ + .selectors = selectors, + .vendor_prefix = VendorPrefix{}, + .declarations = declarations, + .rules = rules, + .loc = loc, + }, + }) catch bun.outOfMemory(); + } + }; + + pub const RuleBodyItemParser = struct { + pub fn parseQualified(this: *This) bool { + _ = this; // autofix + return true; + } + + pub fn parseDeclarations(this: *This) bool { + return this.allow_declarations; + } + }; + + pub const DeclarationParser = struct { + pub const Declaration = void; + + fn parseValue(this: *This, name: []const u8, input: *Parser) Error!Declaration { + return css_decls.parse_declaration( + name, + input, + &this.declarations, + &this.important_declarations, + this.options, + ); + } + }; + + pub fn parseNested(this: *This, input: *Parser, is_style_rule: bool) Error!struct { DeclarationBlock, CssRuleList(T.CustomAtRuleParser.AtRule) } { + // TODO: think about memory management in error cases + var rules = CssRuleList(T.CustomAtRuleParser.AtRule){}; + var nested_parser = This{ + .options = this.options, + .at_rule_parser = this.at_rule_parser, + .declarations = DeclarationList{}, + .important_declarations = DeclarationList{}, + .rules = &rules, + .is_in_style_rule = this.is_in_style_rule or is_style_rule, + .allow_declarations = this.allow_declarations or this.is_in_style_rule or is_style_rule, + }; + + const parse_declarations = This.RuleBodyItemParser.parseDeclarations(nested_parser); + // TODO: think about memory management + var errors = ArrayList(Error){}; + var iter = RuleBodyParser(This).new(input, &nested_parser); + + while (iter.next()) |result| { + if (result) {} else |e| { + if (parse_declarations) { + iter.parser.declarations.clearRetainingCapacity(); + iter.parser.important_declarations.clearRetainingCapacity(); + errors.append( + @compileError(todo_stuff.think_about_allocator), + e, + ) catch bun.outOfMemory(); + } else { + if (iter.parser.options.error_recovery) { + iter.parser.options.warn(e); + continue; + } + return e; + } + } + } + + if (parse_declarations) { + if (errors.items.len > 0) { + if (this.options.error_recovery) { + for (errors.items) |e| { + this.options.warn(e); + } + } else { + return errors.orderedRemove(0); + } + } + } + + return .{ + DeclarationBlock{ + .declarations = nested_parser.declarations, + .important_declarations = nested_parser.important_declarations, + }, + rules, + }; + } + + pub fn parseStyleBlock(this: *This, input: *Parser) Error!CssRuleList(T.CustomAtRuleParser.AtRule) { + const srcloc = input.currentSourceLocation(); + const loc = Location{ + .source_index = this.options.source_index, + .line = srcloc.line, + .column = srcloc.column, + }; + + // Declarations can be immediately within @media and @supports blocks that are nested within a parent style rule. + // These act the same way as if they were nested within a `& { ... }` block. + const declarations, var rules = try this.parseNested(input, false); + + if (declarations.len() > 0) { + rules.v.insert( + @compileError(todo_stuff.think_about_allocator), + 0, + .{ + .style = StyleRule(T.CustomAtRuleParser.AtRule){ + .selectors = selector.api.SelectorList.fromSelector( + @compileError(todo_stuff.think_about_allocator), + selector.api.Selector.fromComponent(.nesting), + ), + .declarations = declarations, + .vendor_prefix = VendorPrefix.empty(), + .rules = .{}, + .loc = loc, + }, + }, + ) catch unreachable; + } + + return rules; + } + }; +} + +pub fn StyleSheetParser(comptime P: type) type { + ValidAtRuleParser(P); + ValidQualifiedRuleParser(P); + + if (P.QualifiedRuleParser.QualifiedRule != P.AtRuleParser.AtRule) { + @compileError("StyleSheetParser: P.QualifiedRuleParser.QualifiedRule != P.AtRuleParser.AtRule"); + } + + const Item = P.AtRuleParser.AtRule; + + return struct { + input: *Parser, + parser: *P, + any_rule_so_far: bool = false, + + pub fn new(input: *Parser, parser: *P) @This() { + return .{ + .input = input, + .parser = parser, + }; + } + + pub fn next(this: *@This(), allocator: Allocator) ?(Error!Item) { + _ = allocator; // autofix + while (true) { + this.input.@"skip cdc and cdo"(); + + const start = this.input.state(); + const at_keyword: ?[]const u8 = switch (this.input.nextByte()) { + '@' => brk: { + const at_keyword: *Token = this.input.nextIncludingWhitespaceAndComments() catch { + this.input.reset(&start); + break :brk null; + }; + if (at_keyword.* == .at_keyword) break :brk at_keyword.*; + this.input.reset(&start); + break :brk null; + }, + else => null, + }; + + if (at_keyword) |name| { + const first_stylesheet_rule = !this.any_rule_so_far; + this.any_rule_so_far = true; + + if (first_stylesheet_rule and bun.strings.eqlCaseInsensitiveASCII(name, "charset", true)) { + const delimiters = Delimiters{ + .semicolon = true, + .close_curly_bracket = true, + }; + _ = this.input.parseUntilAfter(delimiters, Parser.parseEmpty); + } else { + return parse_at_rule(&start, name, this.input, this.parser); + } + } else { + this.any_rule_so_far = true; + const result = parse_qualified_rule(&start, this.input, *this.parser, Delimiters{ .curly_bracket = true }); + return result; + } + } + } + }; +} + +/// A result returned from `to_css`, including the serialized CSS +/// and other metadata depending on the input options. +pub const ToCssResult = struct { + /// Serialized CSS code. + code: []const u8, + /// A map of CSS module exports, if the `css_modules` option was + /// enabled during parsing. + exports: ?CssModuleExports, + /// A map of CSS module references, if the `css_modules` config + /// had `dashed_idents` enabled. + references: ?CssModuleReferences, + /// A list of dependencies (e.g. `@import` or `url()`) found in + /// the style sheet, if the `analyze_dependencies` option is enabled. + dependencies: ?ArrayList(Dependency), +}; + +pub fn StyleSheet(comptime AtRule: type) type { + return struct { + /// A list of top-level rules within the style sheet. + rules: CssRuleList(AtRule) = .{}, + sources: ArrayList([]const u8) = .{}, + source_map_urls: ArrayList(?[]const u8) = .{}, + license_comments: ArrayList([]const u8) = .{}, + options: ParserOptions, + + const This = @This(); + + pub fn toCss(this: *const @This(), allocator: Allocator, options: css_printer.PrinterOptions) Maybe(ToCssResult, Error(PrinterErrorKind)) { + // TODO: this is not necessary + // Make sure we always have capacity > 0: https://github.com/napi-rs/napi-rs/issues/1124. + var dest = ArrayList(u8).initCapacity(allocator, 1) catch unreachable; + const writer = dest.writer(allocator); + const project_root = options.project_root; + var printer = Printer(@TypeOf(writer)).new(writer, options); + + // #[cfg(feature = "sourcemap")] + // { + // printer.sources = Some(&self.sources); + // } + + // #[cfg(feature = "sourcemap")] + // if printer.source_map.is_some() { + // printer.source_maps = self.sources.iter().enumerate().map(|(i, _)| self.source_map(i)).collect(); + // } + + for (this.license_comments.items) |comment| { + printer.writeStr("/*"); + printer.writeStr(comment); + printer.writeStr("*/\n"); + } + + if (this.options.css_modules) |*config| { + var references = std.StringArrayHashMap(CssModuleReferences).init(allocator); + printer.css_module = CssModule.new(config, &this.sources, project_root, &references); + + try this.rules.toCss(&printer); + try printer.newline(); + + return ToCssResult{ + .dependencies = printer.dependencies, + .exports = exports: { + const val = printer.css_module.?.exports_by_source_index.items[0]; + printer.css_module.?.exports_by_source_index.items[0] = .{}; + break :exports val; + }, + .code = dest, + .references = references, + }; + } else { + try this.rules.toCss(&printer); + return ToCssResult{ + .dependencies = printer.dependencies, + .code = dest, + .exports = null, + .references = null, + }; + } + } + + pub fn parseWith( + allocator: Allocator, + code: []const u8, + options: ParserOptions, + comptime P: type, + at_rule_parser: *P, + ) Error!This { + var input = ParserInput.new(allocator, code); + var parser = Parser.new(allocator, &input); + + var license_comments = ArrayList([]const u8){}; + var state = parser.state(); + while (parser.nextIncludingWhitespaceAndComments() catch null) |token| { + switch (token.*) { + .whitespace => {}, + .comment => |comment| { + if (bun.strings.startsWithChar(comment, '!')) { + license_comments.append(allocator, comment) catch bun.outOfMemory(); + } + }, + else => break, + } + state = parser.state(); + } + parser.reset(&state); + + var rules = CssRuleList(AtRule){}; + var rule_parser = TopLevelRuleParser(AtRule).new(&options, at_rule_parser, &rules); + var rule_list_parser = StyleSheetParser(TopLevelRuleParser(AtRule)).new(&parser, &rule_parser); + + while (rule_list_parser.next()) |result| { + _ = result catch |e| { + const result_options = rule_list_parser.parser.options; + if (result_options.error_recovery) { + // TODO this + // options.logger.addWarningFmt(source: ?*const Source, l: Loc, allocator: std.mem.Allocator, comptime text: string, args: anytype) + continue; + } + + return e; + }; + } + + // TODO finish these + const sources = ArrayList([]const u8){}; + // sources.append(allocator, options.filename) catch bun.outOfMemory(); + const source_map_urls = ArrayList([]const u8){}; + + return This{ + .sources = sources, + .source_map_urls = source_map_urls, + .license_comments = license_comments, + .options = options, + }; + } + }; +} + +pub fn ValidDeclarationParser(comptime P: type) void { + // The finished representation of a declaration. + _ = P.DeclarationParser.Declaration; + + // Parse the value of a declaration with the given `name`. + // + // Return the finished representation for the declaration + // as returned by `DeclarationListParser::next`, + // or `Err(())` to ignore the entire declaration as invalid. + // + // Declaration name matching should be case-insensitive in the ASCII range. + // This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + // or with the `match_ignore_ascii_case!` macro. + // + // The given `input` is a "delimited" parser + // that ends wherever the declaration value should end. + // (In declaration lists, before the next semicolon or end of the current block.) + // + // If `!important` can be used in a given context, + // `input.try_parse(parse_important).is_ok()` should be used at the end + // of the implementation of this method and the result should be part of the return value. + // + // fn parseValue(this: *T, name: []const u8, input: *Parser) Error!T.DeclarationParser.Declaration + _ = P.DeclarationParser.parseValue; +} + +/// Also checks that P is: +/// - ValidDeclarationParser(P) +/// - ValidQualifiedRuleParser(P) +/// - ValidAtRuleParser(P) +pub fn ValidRuleBodyItemParser(comptime P: type) void { + ValidDeclarationParser(P); + ValidQualifiedRuleParser(P); + ValidAtRuleParser(P); + + // Whether we should attempt to parse declarations. If you know you won't, returning false + // here is slightly faster. + _ = P.RuleBodyItemParser.parseDeclarations; + + // Whether we should attempt to parse qualified rules. If you know you won't, returning false + // would be slightly faster. + _ = P.RuleBodyItemParser.parseQualified; + + // We should have: + // P.DeclarationParser.Declaration == P.QualifiedRuleParser.QualifiedRule == P.AtRuleParser.AtRule + if (P.DeclarationParser.Declaration != P.QualifiedRuleParser.QualifiedRule or + P.DeclarationParser.Declaration != P.AtRuleParser.AtRule) + { + @compileError("ValidRuleBodyItemParser: P.DeclarationParser.Declaration != P.QualifiedRuleParser.QualifiedRule or\n P.DeclarationParser.Declaration != P.AtRuleParser.AtRule"); + } +} + +pub fn RuleBodyParser(comptime P: type) type { + ValidRuleBodyItemParser(P); + // Same as P.AtRuleParser.AtRule and P.DeclarationParser.Declaration + const I = P.QualifiedRuleParser.QualifiedRule; + + return struct { + input: *Parser, + parser: *P, + + const This = @This(); + + pub fn new(input: *Parser, parser: *P) This { + return .{ + .input = input, + .parser = parser, + }; + } + + /// TODO: result is actually: + /// type Item = Result, &'i str)>; + /// + /// but nowhere in the source do i actually see it using the string part of the tuple + pub fn next(this: *This) ?(Error!I) { + while (true) { + this.input.skipWhitespace(); + const start = this.input.state(); + + const tok: *Token = this.input.nextIncludingWhitespaceAndComments() catch return null; + + switch (tok.*) { + .close_curly_bracket, .whitespace, .semicolon, .comment => continue, + .at_keyword => { + const name = tok.at_keyword; + return parse_at_rule( + @compileError(todo_stuff.think_about_allocator), + &start, + name, + this.input, + P, + this.parser, + ); + }, + .ident => { + if (P.RuleBodyItemParser.parseDeclarations(this.parser)) { + const name = tok.ident; + const parse_qualified = P.RuleBodyItemParser.parseQualified(this.parser); + const result: Error!I = result: { + const error_behavior: ParseUntilErrorBehavior = if (parse_qualified) .stop else .consume; + const Closure = struct { + parser: *P, + pub fn parsefn(self: *@This(), input: *Parser) Error!I { + try input.expectColon(); + return P.DeclarationParser.parseValue(self.parser, name, input); + } + }; + var closure = Closure{ + .parser = this.parser, + }; + break :result parse_until_after(this.input, Delimiters{ .semicolon = true }, error_behavior, I, &closure, Closure.parsefn); + }; + const is_err = if (result) true else false; + if (is_err and parse_qualified) { + this.input.reset(&start); + if (parse_qualified_rule( + &start, + this.input, + P, + this.parser, + Delimiters{ .semicolon = true, .curly_bracket_block = true }, + )) |qual| { + return qual; + } + } + + return result; + } + }, + else => {}, + } + + const result: Error!I = if (P.RuleBodyItemParser.parseQualified(this.parser)) result: { + this.input.reset(&start); + const delimiters = if (P.RuleBodyItemParser.parseDeclarations(this.parser)) Delimiters{ + .semicolon = true, + .curly_bracket_block = true, + } else Delimiters{ .curly_bracket = true }; + break :result parse_qualified_rule(&start, this.input, P, this.parser, delimiters); + } else result: { + const token = tok.*; + _ = token; // autofix + const Fn = struct { + token: Token, + fn parsefn(input: *Parser) Error!I { + _ = input; // autofix + // TODO: implement this + // Err(start.source_location().new_unexpected_token_error(token)) + // return input.newCustomError(.unexpected_token); + @panic("TODO"); + } + }; + break :result this.input.parseUntilAfter(Delimiters{ .semicolon = true }, I, Fn.parsefn); + }; + + return result; + } + } + }; +} + +pub const ParserOptions = struct { + /// Filename to use in error messages. + filename: []const u8, + /// Whether the enable [CSS modules](https://github.com/css-modules/css-modules). + css_modules: ?css_modules.Config, + /// The source index to assign to all parsed rules. Impacts the source map when + /// the style sheet is serialized. + source_index: u32, + /// Whether to ignore invalid rules and declarations rather than erroring. + error_recovery: bool, + /// A list that will be appended to when a warning occurs. + logger: Log, + /// Feature flags to enable. + flags: ParserFlags, + + pub fn default(allocator: std.mem.Allocator) ParserOptions { + return ParserOptions{ + .filename = "", + .css_modules = null, + .source_index = 0, + .error_recovery = false, + .logger = Log.init(allocator), + .flags = ParserFlags{}, + }; + } +}; + +/// Parser feature flags to enable. +pub const ParserFlags = packed struct(u8) { + /// Whether the enable the [CSS nesting](https://www.w3.org/TR/css-nesting-1/) draft syntax. + nesting: bool = false, + /// Whether to enable the [custom media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) draft syntax. + custom_media: bool = false, + /// Whether to enable the non-standard >>> and /deep/ selector combinators used by Vue and Angular. + deep_selector_combinator: bool = false, +}; + +const ParseUntilErrorBehavior = enum { + consume, + stop, +}; + +pub const Parser = struct { + input: *ParserInput, + at_start_of: ?BlockType = null, + stop_before: Delimiters = Delimiters.NONE, + + pub fn new(input: *ParserInput) Parser { + return Parser{ + .input = input, + }; + } + + pub fn currentSourceLocation(this: *const Parser) SourceLocation { + return this.input.tokenizer.currentSourceLocation(); + } + + pub fn allocator(this: *Parser) Allocator { + return this.input.tokenizer.allocator; + } + + /// Implementation of Vec::::parse + pub fn parseList(this: *Parser, comptime T: type, comptime parse_one: *const fn (*Parser) Error!T) Error!ArrayList(T) { + return this.parseCommaSeparated(T, parse_one); + } + + /// Parse a list of comma-separated values, all with the same syntax. + /// + /// The given closure is called repeatedly with a "delimited" parser + /// (see the `Parser::parse_until_before` method) so that it can over + /// consume the input past a comma at this block/function nesting level. + /// + /// Successful results are accumulated in a vector. + /// + /// This method returns `Err(())` the first time that a closure call does, + /// or if a closure call leaves some input before the next comma or the end + /// of the input. + pub fn parseCommaSeparated( + this: *Parser, + comptime T: type, + comptime parse_one: *const fn (*Parser) Error!T, + ) Error!ArrayList(T) { + return this.parseCommaSeparatedInternal(T, parse_one, false); + } + + fn parseCommaSeparatedInternal( + this: *Parser, + comptime T: type, + comptime parse_one: *const fn (*Parser) Error!T, + ignore_errors: bool, + ) Error!ArrayList(T) { + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the somewhat common case of only one item we don't + // way overallocate. Note that we always push at least one item if + // parsing succeeds. + // + // TODO(zack): might be faster to use stack fallback here + // in the common case we may have just 1, but I feel like it is also very common to have >1 + // which means every time we have >1 items we will always incur 1 more additional allocation + var values = ArrayList(T){}; + values.initCapacity(@compileError(todo_stuff.think_about_allocator), 1) catch unreachable; + + while (true) { + this.skipWhitespace(); // Unnecessary for correctness, but may help try() in parse_one rewind less. + if (this.parseUntilBefore(Delimiters{ .comma = true }, {}, voidWrap(T, parse_one))) |v| { + values.append(@compileError(todo_stuff.think_about_allocator), v) catch unreachable; + } else |e| { + if (!ignore_errors) return e; + } + const tok = this.next() catch return values; + if (tok != .comma) bun.unreachablePanic("", .{}); + } + } + + /// Execute the given closure, passing it the parser. + /// If the result (returned unchanged) is `Err`, + /// the internal state of the parser (including position within the input) + /// is restored to what it was before the call. + /// + /// func needs to be a funtion like this: `fn func(*ParserInput, ...@TypeOf(args_)) T` + pub fn tryParse(this: *Parser, comptime func: anytype, args_: anytype) Error!bun.meta.ReturnOf(func) { + const start = this.state(); + const result = result: { + const args = brk: { + var args: std.meta.ArgsTuple(@TypeOf(func)) = undefined; + args[0] = this.input; + + inline for (args_, 1..) |a, i| { + args[i] = a; + } + + break :brk args; + }; + + break :result @call(.auto, func, args); + }; + result catch { + this.reset(start); + }; + return result; + } + + pub fn parseNestedBlock(this: *Parser, comptime T: type, closure: anytype, comptime parsefn: *const fn (@TypeOf(closure), *Parser) Error!T) Error!T { + return parse_nested_block(this, T, closure, parsefn); + } + + pub fn isExhausted(this: *Parser) bool { + return if (this.expectExhausted()) |_| true else false; + } + + pub fn expectPercentage(this: *Parser) Error!f32 { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + if (tok.* == .percentage) return tok.percentage.unit_value; + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + pub fn expectComma(this: *Parser) Error!void { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + switch (tok.*) { + .semicolon => return, + else => {}, + } + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + /// Parse a that does not have a fractional part, and return the integer value. + pub fn expectInteger(this: *Parser) Error!i32 { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + if (tok.* == .number and tok.number.int_value != null) return tok.number.int_value.?; + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + /// Parse a and return the integer value. + pub fn expectNumber(this: *Parser) Error!f32 { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + if (tok.* == .number) return tok.number.value; + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + pub fn expectDelim(this: *Parser, delim: u8) Error!void { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + if (tok.* == .delim and tok.delim == delim) return; + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + pub fn expectParenthesisBlock(this: *Parser) Error!void { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + if (tok.* == .open_paren) return; + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + pub fn expectColon(this: *Parser) Error!void { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + if (tok.* == .colon) return; + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + pub fn expectString(this: *Parser) Error![]const u8 { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + if (tok.* == .string) return tok.string; + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + pub fn expectIdent(this: *Parser) Error![]const u8 { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + if (tok.* == .ident) return tok.ident; + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + /// Parse either a or a , and return the unescaped value. + pub fn expectIdentOrString(this: *Parser) Error![]const u8 { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + switch (tok.*) { + .ident => |i| return i, + .string => |s| return s, + else => {}, + } + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + pub fn expectIdentMatching(this: *Parser, name: []const u8) Error!void { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + switch (tok.*) { + .ident => |i| if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, i)) return, + else => {}, + } + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + pub fn expectFunctionMatching(this: *Parser, name: []const u8) Error!void { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + switch (tok.*) { + .function => |fn_name| if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, fn_name)) return, + else => {}, + } + return start_location.newBasicUnexpectedTokenError(tok.*); + } + + pub fn expectCurlyBracketBlock(this: *Parser) Error!void { + const start_location = this.currentSourceLocation(); + const tok = try this.next(); + switch (tok.*) { + .open_curly => return, + else => return start_location.newBasicUnexpectedTokenError(tok.*), + } + } + + pub fn position(this: *Parser) usize { + bun.debugAssert(bun.strings.isOnCharBoundary(this.input.tokenizer.src, this.input.tokenizer.position)); + return this.input.tokenizer.position; + } + + fn parseEmpty(_: *Parser) Error!void {} + + /// Like `parse_until_before`, but also consume the delimiter token. + /// + /// This can be useful when you don’t need to know which delimiter it was + /// (e.g. if these is only one in the given set) + /// or if it was there at all (as opposed to reaching the end of the input). + pub fn parseUntilAfter( + this: *Parser, + delimiters: Delimiters, + comptime T: type, + comptime parse_fn: *const fn (*Parser) Error!T, + ) Error!T { + const Fn = struct { + pub fn parsefn(_: void, p: *Parser) Error!T { + return parse_fn(p); + } + }; + return parse_until_after( + this, + delimiters, + ParserState.none, + T, + {}, + Fn.parsefn, + ); + } + + pub fn parseUntilBefore(this: *Parser, delimiters: Delimiters, comptime T: type, closure: anytype, comptime parse_fn: *const fn (@TypeOf(closure), *Parser) Error!T) Error!T { + return parse_until_before(this, delimiters, .consume, T, closure, parse_fn); + } + + pub fn parseEntirely(this: *Parser, comptime T: type, closure: anytype, comptime parsefn: *const fn (@TypeOf(closure), *Parser) Error!T) Error!T { + const result = try parsefn(closure, this); + try this.expectExhausted(); + return result; + } + + /// Check whether the input is exhausted. That is, if `.next()` would return a token. + /// Return a `Result` so that the `?` operator can be used: `input.expect_exhausted()?` + /// + /// This ignores whitespace and comments. + pub fn expectExhausted(this: *Parser) Error!void { + const start = this.state(); + const result = result: { + if (this.next()) |t| { + break :result start.sourceLocation().newBasicUnexpectedTokenError(t.*); + } else |e| { + if (e == .end_of_input) break :result; + bun.unreachablePanic("Unexpected error encountered: {s}", .{@errorName(e)}); + } + }; + this.reset(&start); + return result; + } + + pub fn @"skip cdc and cdo"(this: *@This()) void { + if (this.at_start_of) |block_type| { + this.at_start_of = null; + consume_until_end_of_block(block_type, &this.input.tokenizer); + } + + this.input.tokenizer.@"skip cdc and cdo"(); + } + + pub fn skipWhitespace(this: *@This()) void { + if (this.at_start_of) |block_type| { + this.at_start_of = null; + consume_until_end_of_block(block_type, &this.input.tokenizer); + } + + this.input.tokenizer.skipWhitespace(); + } + + pub fn next(this: *@This()) Error!*Token { + this.skipWhitespace(); + this.nextIncludingWhitespaceAndComments(); + } + + /// Same as `Parser::next`, but does not skip whitespace tokens. + pub fn nextIncludingWhitespace(this: *@This()) Error!*Token { + while (true) { + if (this.nextIncludingWhitespaceAndComments()) |tok| { + if (tok.* == .comment) {} else break; + } else |e| return e; + } + return this.input.cached_token.?; + } + + pub fn nextByte(this: *@This()) ?u8 { + const byte = this.input.tokenizer.nextByte(); + if (this.stop_before.contains(Delimiters.fromByte(byte))) { + return null; + } + return byte; + } + + pub fn reset(this: *Parser, state_: *const ParserState) void { + this.input.tokenizer.reset(state_); + this.at_start_of = state_.at_start_of; + } + + pub fn state(this: *Parser) ParserState { + return ParserState{ + .position = this.input.tokenizer.getPosition(), + .current_line_start_position = this.input.tokenizer.current_line_start_position, + .current_line_number = this.input.tokenizer.current_line_number, + .at_start_of = this.at_start_of, + }; + } + + /// Same as `Parser::next`, but does not skip whitespace or comment tokens. + /// + /// **Note**: This should only be used in contexts like a CSS pre-processor + /// where comments are preserved. + /// When parsing higher-level values, per the CSS Syntax specification, + /// comments should always be ignored between tokens. + pub fn nextIncludingWhitespaceAndComments(this: *Parser) error.ParseError!*Token { + if (this.at_start_of) |block_type| { + this.at_start_of = null; + consume_until_end_of_block(block_type, *this.input.tokenizer); + } + + const byte = this.input.tokenizer.nextByte(); + if (this.stop_before.contains(Delimiters.fromByte(byte))) { + return this.newBasicError(BasicParseErrorKind{ .end_of_input = true }); + } + + const token_start_position = this.input.tokenizer.getPosition(); + const using_cached_token = this.input.cached_token != null and this.input.cached_token.?.start_position == token_start_position; + + const token = if (using_cached_token) token: { + const cached_token = &this.input.cached_token.?; + this.input.tokenizer.reset(&cached_token.end_state); + if (cached_token.token == .function) { + this.input.tokenizer.seeFunction(cached_token.token.function); + } + break :token cached_token.token; + } else token: { + const new_token = try (this.input.tokenizer.next() catch this.newBasicError(BasicParseErrorKind{ .end_of_input = true })); + this.input.cached_token = CachedToken{ + .token = new_token, + .start_position = token_start_position, + .end_state = this.input.tokenizer.state(), + }; + break :token &this.input.cached_token; + }; + + if (BlockType.opening(token)) |block_type| { + this.at_start_of = block_type; + } + + return token; + } + + const ParseError = struct { + comptime { + @compileError(todo_stuff.errors); + } + }; + + /// Create a new unexpected token or EOF ParseError at the current location + pub fn newErrorForNextToken(this: *Parser) ParseError { + _ = this; // autofix + @compileError(todo_stuff.errors); + // let token = match self.next() { + // Ok(token) => token.clone(), + // Err(e) => return e.into(), + // }; + // self.new_error(BasicParseErrorKind::UnexpectedToken(token)) + } +}; + +/// A set of characters, to be used with the `Parser::parse_until*` methods. +/// +/// The union of two sets can be obtained with the `|` operator. Example: +/// +/// ```{rust,ignore} +/// input.parse_until_before(Delimiter::CurlyBracketBlock | Delimiter::Semicolon) +/// ``` +pub const Delimiters = packed struct(u8) { + /// The delimiter set with only the `{` opening curly bracket + curly_bracket: bool = false, + /// The delimiter set with only the `;` semicolon + semicolon: bool = false, + /// The delimiter set with only the `!` exclamation point + bang: bool = false, + /// The delimiter set with only the `,` comma + comma: bool = false, + close_curly_bracket: bool = false, + close_square_bracket: bool = false, + close_parenthesis: bool = false, + __unused: u1 = 0, + + pub usingnamespace Bitflags(Delimiters); + + const NONE: Delimiters = .{}; + + pub fn getDelimiter(comptime tag: @TypeOf(.EnumLiteral)) Delimiters { + var empty = Delimiters{}; + @field(empty, @tagName(tag)) = true; + return empty; + } + + const TABLE: [256]Delimiters = brk: { + var table: [256]Delimiters = [_]Delimiters{.{}} ** 256; + table[';'] = getDelimiter(.semicolon); + table['!'] = getDelimiter(.bang); + table[','] = getDelimiter(.comma); + table['{'] = getDelimiter(.curly_bracket_block); + table['}'] = getDelimiter(.close_curly_bracket); + table[']'] = getDelimiter(.close_square_bracket); + table[')'] = getDelimiter(.close_parenthesis); + break :brk table; + }; + + // pub fn bitwiseOr(lhs: Delimiters, rhs: Delimiters) Delimiters { + // return @bitCast(@as(u8, @bitCast(lhs)) | @as(u8, @bitCast(rhs))); + // } + + // pub fn contains(lhs: Delimiters, rhs: Delimiters) bool { + // return @as(u8, @bitCast(lhs)) & @as(u8, @bitCast(rhs)) != 0; + // } + + pub fn fromByte(byte: ?u8) Delimiters { + if (byte) |b| return TABLE[b]; + return .{}; + } +}; + +const ParserInput = struct { + tokenizer: Tokenizer, + cached_token: ?CachedToken = null, + + pub fn new(allocator: Allocator, code: []const u8) ParserInput { + return ParserInput{ + .tokenizer = Tokenizer.init(allocator, code), + }; + } +}; + +/// A capture of the internal state of a `Parser` (including the position within the input), +/// obtained from the `Parser::position` method. +/// +/// Can be used with the `Parser::reset` method to restore that state. +/// Should only be used with the `Parser` instance it came from. +pub const ParserState = struct { + position: usize, + current_line_start_position: usize, + current_line_number: u32, + at_start_of: BlockType, + + pub fn sourceLocation(this: *const ParserState) SourceLocation { + return .{ + .line = this.current_line_number, + .column = @intCast(this.position - this.current_line_start_position + 1), + }; + } +}; + +const BlockType = enum { + parenthesis, + square_bracket, + curly_bracket, + + fn opening(token: *const Token) ?BlockType { + return switch (token.*) { + .function, .open_paren => .parenthesis, + .open_square => .square_bracket, + .open_curly => .curly_bracket, + else => null, + }; + } + + fn closing(token: *const Token) ?BlockType { + return switch (token.*) { + .close_paren => .parenthesis, + .close_square => .square_bracket, + .close_curly => .curly_bracket, + else => null, + }; + } +}; + +pub const nth = struct { + /// Parse the *An+B* notation, as found in the `:nth-child()` selector. + /// The input is typically the arguments of a function, + /// in which case the caller needs to check if the arguments’ parser is exhausted. + /// Return `Ok((A, B))`, or `Err(())` for a syntax error. + pub fn parse_nth(input: *Parser) Error!struct { i32, i32 } { + const tok = try input.next(); + switch (tok.*) { + .number => { + if (tok.number.int_value) |b| return .{ 0, b }; + }, + .dimension => { + if (tok.dimension.num.int_value) |a| { + // @compileError(todo_stuff.match_ignore_ascii_case); + const unit = tok.dimension.unit; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "n")) { + return try parse_b(input, a); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "n-")) { + return try parse_signless_b(input, a); + } else { + if (parse_n_dash_digits(unit)) |b| { + return .{ a, b }; + } else { + return input.newBasicUnexpectedTokenError(.{ .ident = unit }); + } + } + } + }, + .ident => { + const value = tok.ident; + // @compileError(todo_stuff.match_ignore_ascii_case); + if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "even")) { + return .{ 2, 0 }; + } else if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "odd")) { + return .{ 2, 1 }; + } else if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "n")) { + return try parse_b(input, 1); + } else if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "-n")) { + return try parse_b(input, -1); + } else if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "n-")) { + return try parse_signless_b(input, 1, -1); + } else if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "-n-")) { + return try parse_signless_b(input, -1, -1); + } else { + const slice, const a = if (bun.strings.startsWithChar(value, '-')) .{ value[1..], -1 } else .{ value, 1 }; + if (parse_n_dash_digits(slice)) |b| return .{ a, b }; + return input.newBasicUnexpectedTokenError(.{ .ident = value }); + } + }, + .delim => { + const next_tok = try input.nextIncludingWhitespace(); + if (next_tok.* == .ident) { + const value = next_tok.ident; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(value, "n")) { + return try parse_b(input, 1); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(value, "-n")) { + return try parse_signless_b(input, 1, -1); + } else { + if (parse_n_dash_digits(value)) |b| { + return .{ 1, b }; + } else { + return input.newBasicUnexpectedTokenError(.{ .ident = value }); + } + } + } else { + return input.newBasicUnexpectedTokenError(next_tok.*); + } + }, + else => {}, + } + return input.newBasicUnexpectedTokenError(tok.*); + } + + fn parse_b(input: *Parser, a: i23) Error!struct { i32, i32 } { + const start = input.state(); + const tok = input.next() catch { + input.reset(&start); + return .{ a, 0 }; + }; + + if (tok.* == .delim and tok.delim == '+') return parse_signless_b(input, a, 1); + if (tok.* == .delim and tok.delim == '-') return parse_signless_b(input, a, -1); + if (tok.* == .number and tok.number.has_sign and tok.number.int_value != null) return parse_signless_b(input, a, tok.number.int_value.?); + input.reset(&start); + return .{ a, 0 }; + } + + fn parse_signless_b(input: *Parser, a: i32, b_sign: i32) Error!struct { i32, i32 } { + const tok = try input.next(); + if (tok.* == .number and !tok.number.has_sign and tok.number.int_value != null) { + const b = tok.number.int_value.?; + return .{ a, b_sign * b }; + } + return input.newBasicUnexpectedTokenError(tok.*); + } + + fn parse_n_dash_digits(str: []const u8) Error!i32 { + const bytes = str; + if (bytes.len >= 3 and + bun.strings.eqlCaseInsensitiveASCIIICheckLength(bytes[0..2], "n-") and + brk: { + for (bytes[2..]) |b| { + if (b < '0' or b > '9') break :brk false; + } + break :brk true; + }) { + return parse_number_saturate(str[1..]); // Include the minus sign + } else { + // return Err(()); + @compileError(todo_stuff.errors); + } + } + + fn parse_number_saturate(string: []const u8) Error!i32 { + var input = ParserInput.new(@compileError(todo_stuff.think_about_allocator), string); + var parser = Parser.new(&input); + const tok = parser.nextIncludingWhitespaceAndComments() catch { + // return Err(()); + @compileError(todo_stuff.errors); + }; + const int = if (tok.* == .number and tok.number.int_value != null) tok.number.int_value.? else { + // return Err(()); + @compileError(todo_stuff.errors); + }; + if (!parser.isExhausted()) { + // return Err(()); + @compileError(todo_stuff.errors); + } + return int; + } +}; + +const CachedToken = struct { + token: Token, + start_position: usize, + end_state: ParserState, +}; + +const Tokenizer = struct { + src: []const u8, + position: usize = 0, + source_map_url: ?[]const u8 = null, + current_line_start_position: usize = 0, + current_line_number: usize = 0, + allocator: Allocator, + var_or_env_functions: SeenStatus = .dont_care, + current: Token = undefined, + previous: Token = undefined, + + const SeenStatus = enum { + dont_care, + looking_for_them, + seen_at_least_one, + }; + + const FORM_FEED_BYTE = 0x0C; + const REPLACEMENT_CHAR = 0xFFFD; + const REPLACEMENT_CHAR_UNICODE: [3]u8 = [3]u8{ 0xEF, 0xBF, 0xBD }; + const MAX_ONE_B: u32 = 0x80; + const MAX_TWO_B: u32 = 0x800; + const MAX_THREE_B: u32 = 0x10000; + + pub fn init(allocator: Allocator, src: []const u8) Tokenizer { + var lexer = Tokenizer{ + .src = src, + .allocator = allocator, + .position = 0, + }; + + // make current point to the first token + _ = lexer.next(); + lexer.position = 0; + + return lexer; + } + + pub fn getPosition(this: *const Tokenizer) usize { + bun.debugAssert!(bun.strings.isOnCharBoundary(this.src, this.position)); + return this.position; + } + + pub fn state(this: *const Tokenizer) ParserState { + return ParserState{ + .position = this.position, + .current_line_start_position = this.current_line_start_position, + .current_line_number = this.current_line_number, + .at_start_of = null, + }; + } + + pub fn skipWhitespace(this: *Tokenizer) void { + while (!this.isEof()) { + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + ' ' | '\t' => this.advance(1), + '\n', 0x0C, '\r' => this.consumeNewline(), + '/' => { + if (this.startsWith("/*")) { + _ = this.consumeComment(); + } else return; + }, + else => return, + } + } + } + + pub fn currentSourceLocation(this: *const Tokenizer) SourceLocation { + return SourceLocation{ + .line = this.current_line_number, + .column = @intCast(this.position - this.current_line_start_position + 1), + }; + } + + pub fn prev(this: *Tokenizer) Token { + bun.assert(this.position > 0); + return this.previous; + } + + pub inline fn isEof(this: *Tokenizer) bool { + return this.position >= this.src.len; + } + + pub fn seeFunction(this: *Tokenizer, name: []const u8) void { + if (this.var_or_env_functions == .looking_for_them) { + if (std.ascii.eqlIgnoreCase(name, "var") and std.ascii.eqlIgnoreCase(name, "env")) { + this.var_or_env_functions = .seen_at_least_one; + } + } + } + + /// TODO: fix this, remove the additional shit I added + /// return error if it is eof + pub fn next(this: *Tokenizer) Token { + this.previous = this.current; + const ret = this.nextImpl(); + this.current = ret; + return ret; + } + + pub fn nextImpl(this: *Tokenizer) Token { + if (this.isEof()) return .eof; + + // todo_stuff.match_byte; + const b = this.byteAt(0); + switch (b) { + ' ', '\t' => return this.consumeWhitespace(false), + '\n', FORM_FEED_BYTE, '\r' => return this.consumeWhitespace(true), + '"' => return this.consumeString(false), + '#' => { + this.advance(1); + if (this.isIdentStart()) return .{ .idhash = this.consumeName() }; + if (!this.isEof() and switch (this.nextByteUnchecked()) { + // Any other valid case here already resulted in IDHash. + '0'...'9', '-' => true, + else => false, + }) return .{ .hash = this.consumeName() }; + return .{ .delim = '#' }; + }, + '$' => { + if (this.startsWith("$=")) { + this.advance(2); + return .suffix_match; + } + this.advance(1); + return .{ .delim = '$' }; + }, + '\'' => return this.consumeString(true), + '(' => { + this.advance(1); + return .open_paren; + }, + ')' => { + this.advance(1); + return .close_paren; + }, + '*' => { + if (this.startsWith("*=")) { + this.advance(2); + return .substring_match; + } + this.advance(1); + return .{ .delim = '*' }; + }, + '+' => { + if ((this.hasAtLeast(1) and switch (this.byteAt(1)) { + '0'...'9' => true, + else => false, + }) or (this.hasAtLeast(2) and + this.byteAt(1) == '.' and switch (this.byteAt(2)) { + '0'...'9' => true, + else => false, + })) { + return this.consumeNumeric(); + } + + this.advance(1); + return .{ .delim = '+' }; + }, + ',' => { + this.advance(1); + return .comma; + }, + '-' => { + if ((this.hasAtLeast(1) and switch (this.byteAt(1)) { + '0'...'9' => true, + else => false, + }) or (this.hasAtLeast(2) and this.byteAt(1) == '.' and switch (this.byteAt(2)) { + '0'...'9' => true, + else => false, + })) return this.consumeNumeric(); + + if (this.startsWith("-->")) { + this.advance(3); + return .cdc; + } + + if (this.isIdentStart()) return this.consumeIdentLike(); + + this.advance(1); + return .{ .delim = '-' }; + }, + '.' => { + if (this.hasAtLeast(1) and switch (this.byteAt(1)) { + '0'...'9' => true, + else => false, + }) { + return this.consumeNumeric(); + } + this.advance(1); + return .{ .delim = '.' }; + }, + '/' => { + if (this.startsWith("/*")) return .{ .comment = this.consumeComment() }; + this.advance(1); + return .{ .delim = '/' }; + }, + '0'...'9' => return this.consumeNumeric(), + ':' => { + this.advance(1); + return .colon; + }, + ';' => { + this.advance(1); + return .semicolon; + }, + '<' => { + if (this.startsWith("")) this.advance(3) else return, + else => return, + } + } + } + + pub fn consumeNumeric(this: *Tokenizer) Token { + // Parse [+-]?\d*(\.\d+)?([eE][+-]?\d+)? + // But this is always called so that there is at least one digit in \d*(\.\d+)? + + // Do all the math in f64 so that large numbers overflow to +/-inf + // and i32::{MIN, MAX} are within range. + const has_sign: bool, const sign: f64 = brk: { + switch (this.nextByteUnchecked()) { + '-' => break :brk .{ true, -1.0 }, + '+' => break :brk .{ true, 1.0 }, + else => break :brk .{ false, 1.0 }, + } + }; + + if (has_sign) this.advance(1); + + var integral_part: f64 = 0.0; + while (byteToDecimalDigit(this.nextByteUnchecked())) |digit| { + integral_part = integral_part * 10.0 + @as(f64, @floatFromInt(digit)); + this.advance(1); + if (this.isEof()) break; + } + + var is_integer = true; + + var fractional_part: f64 = 0.0; + if (this.hasAtLeast(1) and this.nextByteUnchecked() == '.' and switch (this.byteAt(1)) { + '0'...'9' => true, + else => false, + }) { + is_integer = false; + this.advance(1); + var factor: f64 = 0.1; + while (byteToDecimalDigit(this.nextByteUnchecked())) |digit| { + fractional_part += @as(f64, @floatFromInt(digit)) * factor; + factor *= 0.1; + this.advance(1); + if (this.isEof()) break; + } + } + + var value: f64 = sign * (integral_part + fractional_part); + + if (this.hasAtLeast(1) and switch (this.nextByteUnchecked()) { + 'e', 'E' => true, + else => false, + }) { + if (switch (this.byteAt(1)) { + '0'...'9' => true, + else => false, + } or (this.hasAtLeast(2) and switch (this.byteAt(1)) { + '+', '-' => true, + else => false, + } and switch (this.byteAt(2)) { + '0'...'9' => true, + else => false, + })) { + is_integer = false; + this.advance(1); + const has_sign2: bool, const sign2: f64 = brk: { + switch (this.nextByteUnchecked()) { + '-' => break :brk .{ true, -1.0 }, + '+' => break :brk .{ true, 1.0 }, + else => break :brk .{ false, 1.0 }, + } + }; + + if (has_sign2) this.advance(1); + + var exponent: f64 = 0.0; + while (byteToDecimalDigit(this.nextByteUnchecked())) |digit| { + exponent = exponent * 10.0 + @as(f64, @floatFromInt(digit)); + this.advance(1); + if (this.isEof()) break; + } + value *= std.math.pow(f64, 10, sign2 * exponent); + } + } + + const int_value: ?i32 = brk: { + const i32_max = comptime std.math.maxInt(i32); + const i32_min = comptime std.math.minInt(i32); + if (is_integer) { + if (value >= @as(f64, @floatFromInt(i32_max))) { + break :brk i32_max; + } else if (value <= @as(f64, @floatFromInt(i32_min))) { + break :brk i32_min; + } else { + break :brk @intFromFloat(value); + } + } + + break :brk null; + }; + + if (!this.isEof() and this.nextByteUnchecked() == '%') { + this.advance(1); + return .{ .percentage = .{ .unit_value = @floatCast(value / 100), .int_value = int_value, .has_sign = has_sign } }; + } + + if (this.isIdentStart()) { + const unit = this.consumeName(); + return .{ + .dimension = .{ + .num = .{ .value = @floatCast(value), .int_value = int_value, .has_sign = has_sign }, + .unit = unit, + }, + }; + } + + return .{ + .number = .{ .value = @floatCast(value), .int_value = int_value, .has_sign = has_sign }, + }; + } + + pub fn consumeWhitespace(this: *Tokenizer, comptime newline: bool) Token { + const start_position = this.position; + if (newline) { + this.consumeNewline(); + } else { + this.advance(1); + } + + while (!this.isEof()) { + // todo_stuff.match_byte + const b = this.nextByteUnchecked(); + switch (b) { + ' ', '\t' => this.advance(1), + '\n', FORM_FEED_BYTE, '\r' => this.consumeNewline(), + else => break, + } + } + + return .{ .whitespace = this.sliceFrom(start_position) }; + } + + pub fn consumeString(this: *Tokenizer, comptime single_quote: bool) Token { + const quoted_string = this.consumeQuotedString(single_quote); + if (quoted_string.bad) return .{ .bad_string = quoted_string.str }; + return .{ .string = quoted_string.str }; + } + + pub fn consumeIdentLike(this: *Tokenizer) Token { + const value = this.consumeName(); + if (!this.isEof() and this.nextByteUnchecked() == '(') { + this.advance(1); + if (std.ascii.eqlIgnoreCase(value, "url")) return if (this.consumeUnquotedUrl()) |tok| return tok else .{ .function = value }; + this.seeFunction(value); + return .{ .function = value }; + } + return .{ .ident = value }; + } + + pub fn consumeName(this: *Tokenizer) []const u8 { + const start_pos = this.position; + var value_bytes: CopyOnWriteStr = undefined; + + while (true) { + if (this.isEof()) return this.sliceFrom(start_pos); + + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => this.advance(1), + '\\', 0 => { + // * The tokenizer’s input is UTF-8 since it’s `&str`. + // * start_pos is at a code point boundary + // * so is the current position (which is before '\\' or '\0' + // + // So `value_bytes` is well-formed UTF-8. + value_bytes = .{ .borrowed = this.sliceFrom(start_pos) }; + break; + }, + 0x80...0xBF => this.consumeContinuationByte(), + // This is the range of the leading byte of a 2-3 byte character + // encoding + 0xC0...0xEF => this.advance(1), + 0xF0...0xFF => this.consume4byteIntro(), + else => return this.sliceFrom(start_pos), + } + } + + while (!this.isEof()) { + const b = this.nextByteUnchecked(); + // todo_stuff.match_byte + switch (b) { + 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => { + this.advance(1); + value_bytes.append(this.allocator, &[_]u8{b}); + }, + '\\' => { + if (this.hasNewlineAt(1)) break; + this.advance(1); + this.consumeEscapeAndWrite(&value_bytes); + }, + 0 => { + this.advance(1); + value_bytes.append(this.allocator, REPLACEMENT_CHAR_UNICODE[0..]); + }, + 0x80...0xBF => { + // This byte *is* part of a multi-byte code point, + // we’ll end up copying the whole code point before this loop does something else. + this.consumeContinuationByte(); + value_bytes.append(this.allocator, &[_]u8{b}); + }, + 0xC0...0xEF => { + // This byte *is* part of a multi-byte code point, + // we’ll end up copying the whole code point before this loop does something else. + this.advance(1); + value_bytes.append(this.allocator, &[_]u8{b}); + }, + 0xF0...0xFF => { + this.consume4byteIntro(); + value_bytes.append(this.allocator, &[_]u8{b}); + }, + else => { + // ASCII + break; + }, + } + } + + return value_bytes.toSlice(); + } + + pub fn consumeQuotedString(this: *Tokenizer, comptime single_quote: bool) struct { str: []const u8, bad: bool = false } { + const start_pos = this.position; + var string_bytes: CopyOnWriteStr = undefined; + + while (true) { + if (this.isEof()) return .{ .str = this.sliceFrom(start_pos) }; + + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + '"' => { + if (!single_quote) { + const value = this.sliceFrom(start_pos); + this.advance(1); + return .{ .str = value }; + } + this.advance(1); + }, + '\'' => { + if (single_quote) { + const value = this.sliceFrom(start_pos); + this.advance(1); + return .{ .str = value }; + } + this.advance(1); + }, + // The CSS spec says NULL bytes ('\0') should be turned into replacement characters: 0xFFFD + '\\', 0 => { + // * The tokenizer’s input is UTF-8 since it’s `&str`. + // * start_pos is at a code point boundary + // * so is the current position (which is before '\\' or '\0' + // + // So `string_bytes` is well-formed UTF-8. + string_bytes = .{ .borrowed = this.sliceFrom(start_pos) }; + break; + }, + '\n', '\r', FORM_FEED_BYTE => return .{ .str = this.sliceFrom(start_pos), .bad = true }, + 0x80...0xBF => this.consumeContinuationByte(), + 0xF0...0xFF => this.consume4byteIntro(), + else => { + this.advance(1); + }, + } + } + + while (!this.isEof()) { + const b = this.nextByteUnchecked(); + // todo_stuff.match_byte + switch (b) { + // string_bytes is well-formed UTF-8, see other comments + '\n', '\r', FORM_FEED_BYTE => return .{ .str = string_bytes.toSlice(), .bad = true }, + '"' => { + this.advance(1); + if (!single_quote) break; + }, + '\'' => { + this.advance(1); + if (single_quote) break; + }, + '\\' => { + this.advance(1); + if (!this.isEof()) { + switch (this.nextByteUnchecked()) { + // Escaped newline + '\n', FORM_FEED_BYTE, '\r' => this.consumeNewline(), + else => this.consumeEscapeAndWrite(&string_bytes), + } + } + // else: escaped EOF, do nothing. + // continue; + }, + 0 => { + this.advance(1); + string_bytes.append(this.allocator, REPLACEMENT_CHAR_UNICODE[0..]); + continue; + }, + 0x80...0xBF => this.consumeContinuationByte(), + 0xF0...0xFF => this.consume4byteIntro(), + else => { + this.advance(1); + }, + } + + string_bytes.append(this.allocator, &[_]u8{b}); + } + + return .{ .str = string_bytes.toSlice() }; + } + + pub fn consumeUnquotedUrl(this: *Tokenizer) ?Token { + // This is only called after "url(", so the current position is a code point boundary. + const start_position = this.position; + const from_start = this.src[this.position..]; + var newlines: u32 = 0; + var last_newline: usize = 0; + var found_printable_char = false; + + var offset: usize = 0; + var b: u8 = undefined; + while (true) { + defer offset += 1; + + if (offset < from_start.len) { + b = from_start[offset]; + } else { + this.position = this.src.len; + break; + } + + // todo_stuff.match_byte + switch (b) { + ' ', '\t' => {}, + '\n', FORM_FEED_BYTE => { + newlines += 1; + last_newline = offset; + }, + '\r' => { + if (offset + 1 < from_start.len and from_start[offset + 1] != '\n') { + newlines += 1; + last_newline = offset; + } + }, + '"', '\'' => return null, // Do not advance + ')' => { + // Don't use advance, because we may be skipping + // newlines here, and we want to avoid the assert. + this.position += offset + 1; + break; + }, + else => { + // Don't use advance, because we may be skipping + // newlines here, and we want to avoid the assert. + this.position += offset; + found_printable_char = true; + break; + }, + } + } + + if (newlines > 0) { + this.current_line_number += newlines; + // No need for wrapping_add here, because there's no possible + // way to wrap. + this.current_line_start_position = start_position + last_newline + 1; + } + + if (found_printable_char) { + // This function only consumed ASCII (whitespace) bytes, + // so the current position is a code point boundary. + return this.consumeUnquotedUrlInternal(); + } + return .{ .unquoted_url = "" }; + } + + pub fn consumeUnquotedUrlInternal(this: *Tokenizer) Token { + // This function is only called with start_pos at a code point boundary.; + const start_pos = this.position; + var string_bytes: CopyOnWriteStr = undefined; + + while (true) { + if (this.isEof()) return .{ .unquoted_url = this.sliceFrom(start_pos) }; + + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + ' ', '\t', '\n', '\r', FORM_FEED_BYTE => { + var value = .{ .borrowed = this.sliceFrom(start_pos) }; + return this.consumeUrlEnd(start_pos, &value); + }, + ')' => { + const value = this.sliceFrom(start_pos); + this.advance(1); + return .{ .unquoted_url = value }; + }, + // non-printable + 0x01...0x08, + 0x0B, + 0x0E...0x1F, + 0x7F, + + // not valid in this context + '"', + '\'', + '(', + => { + this.advance(1); + return this.consumeBadUrl(start_pos); + }, + '\\', 0 => { + // * The tokenizer’s input is UTF-8 since it’s `&str`. + // * start_pos is at a code point boundary + // * so is the current position (which is before '\\' or '\0' + // + // So `string_bytes` is well-formed UTF-8. + string_bytes = .{ .borrowed = this.sliceFrom(start_pos) }; + break; + }, + 0x80...0xBF => this.consumeContinuationByte(), + 0xF0...0xFF => this.consume4byteIntro(), + else => { + // ASCII or other leading byte. + this.advance(1); + }, + } + } + + while (!this.isEof()) { + const b = this.nextByteUnchecked(); + // todo_stuff.match_byte + switch (b) { + ' ', '\t', '\n', '\r', FORM_FEED_BYTE => { + // string_bytes is well-formed UTF-8, see other comments. + // const string = string_bytes.toSlice(); + // return this.consumeUrlEnd(start_pos, &string); + return this.consumeUrlEnd(start_pos, &string_bytes); + }, + ')' => { + this.advance(1); + break; + }, + // non-printable + 0x01...0x08, + 0x0B, + 0x0E...0x1F, + 0x7F, + + // invalid in this context + '"', + '\'', + '(', + => { + this.advance(1); + return this.consumeBadUrl(start_pos); + }, + '\\' => { + this.advance(1); + if (this.hasNewlineAt(0)) return this.consumeBadUrl(start_pos); + + // This pushes one well-formed code point to string_bytes + this.consumeEscapeAndWrite(&string_bytes); + }, + 0 => { + this.advance(1); + string_bytes.append(this.allocator, REPLACEMENT_CHAR_UNICODE[0..]); + }, + 0x80...0xBF => { + // We’ll end up copying the whole code point + // before this loop does something else. + this.consumeContinuationByte(); + string_bytes.append(this.allocator, &[_]u8{b}); + }, + 0xF0...0xFF => { + // We’ll end up copying the whole code point + // before this loop does something else. + this.consume4byteIntro(); + string_bytes.append(this.allocator, &[_]u8{b}); + }, + // If this byte is part of a multi-byte code point, + // we’ll end up copying the whole code point before this loop does something else. + else => { + // ASCII or other leading byte. + this.advance(1); + string_bytes.append(this.allocator, &[_]u8{b}); + }, + } + } + + // string_bytes is well-formed UTF-8, see other comments. + return .{ .unquoted_url = string_bytes.toSlice() }; + } + + pub fn consumeUrlEnd(this: *Tokenizer, start_pos: usize, string: *CopyOnWriteStr) Token { + while (!this.isEof()) { + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + ')' => { + this.advance(1); + break; + }, + ' ', '\t' => this.advance(1), + '\n', FORM_FEED_BYTE, '\r' => this.consumeNewline(), + else => |b| { + this.consumeKnownByte(b); + return this.consumeBadUrl(start_pos); + }, + } + } + + return .{ .unquoted_url = string.toSlice() }; + } + + pub fn consumeBadUrl(this: *Tokenizer, start_pos: usize) Token { + // Consume up to the closing ) + while (!this.isEof()) { + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + ')' => { + const contents = this.sliceFrom(start_pos); + this.advance(1); + return .{ .bad_url = contents }; + }, + '\\' => { + this.advance(1); + if (this.nextByte()) |b| { + if (b == ')' or b == '\\') this.advance(1); // Skip an escaped ')' or '\' + } + }, + '\n', FORM_FEED_BYTE, '\r' => this.consumeNewline(), + else => |b| this.consumeKnownByte(b), + } + } + return .{ .bad_url = this.sliceFrom(start_pos) }; + } + + pub fn consumeEscapeAndWrite(this: *Tokenizer, bytes: *CopyOnWriteStr) void { + const val = this.consumeEscape(); + var utf8bytes: [4]u8 = undefined; + const len = std.unicode.utf8Encode(@truncate(val), utf8bytes[0..]) catch @panic("Invalid"); + bytes.append(this.allocator, utf8bytes[0..len]); + } + + pub fn consumeEscape(this: *Tokenizer) u32 { + if (this.isEof()) return 0xFFFD; // Unicode replacement character + + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + '0'...'9', 'A'...'F', 'a'...'f' => { + const c = this.consumeHexDigits().value; + if (!this.isEof()) { + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + ' ', '\t' => this.advance(1), + '\n', FORM_FEED_BYTE, '\r' => this.consumeNewline(), + else => {}, + } + } + + if (c != 0 and std.unicode.utf8ValidCodepoint(@truncate(c))) return c; + return REPLACEMENT_CHAR; + }, + 0 => { + this.advance(1); + return REPLACEMENT_CHAR; + }, + else => return this.consumeChar(), + } + } + + pub fn consumeHexDigits(this: *Tokenizer) struct { value: u32, num_digits: u32 } { + var value: u32 = 0; + var digits: u32 = 0; + while (digits < 6 and !this.isEof()) { + if (byteToHexDigit(this.nextByteUnchecked())) |digit| { + value = value * 16 + digit; + digits += 1; + this.advance(1); + } else break; + } + + return .{ .value = value, .num_digits = digits }; + } + + pub fn consumeChar(this: *Tokenizer) u32 { + const c = this.nextChar(); + const len_utf8 = lenUtf8(c); + this.position += len_utf8; + // Note that due to the special case for the 4-byte sequence + // intro, we must use wrapping add here. + this.current_line_start_position +%= len_utf8 - lenUtf16(c); + return c; + } + + fn lenUtf8(code: u32) usize { + if (code < MAX_ONE_B) { + return 1; + } else if (code < MAX_TWO_B) { + return 2; + } else if (code < MAX_THREE_B) { + return 3; + } else { + return 4; + } + } + + fn lenUtf16(ch: u32) usize { + if ((ch & 0xFFFF) == ch) { + return 1; + } else { + return 2; + } + } + + fn byteToHexDigit(b: u8) ?u32 { + + // todo_stuff.match_byte + return switch (b) { + '0'...'9' => b - '0', + 'a'...'f' => b - 'a' + 10, + 'A'...'F' => b - 'A' + 10, + else => null, + }; + } + + fn byteToDecimalDigit(b: u8) ?u32 { + if (b >= '0' and b <= '9') { + return b - '0'; + } + return null; + } + + pub fn consumeComment(this: *Tokenizer) []const u8 { + this.advance(2); + const start_position = this.position; + while (!this.isEof()) { + const b = this.nextByteUnchecked(); + // todo_stuff.match_byte + switch (b) { + '*' => { + const end_position = this.position; + this.advance(1); + if (this.nextByte() == '/') { + this.advance(1); + const contents = this.src[start_position..end_position]; + this.checkForSourceMap(contents); + return contents; + } + }, + '\n', FORM_FEED_BYTE, '\r' => { + this.consumeNewline(); + }, + 0x80...0xBF => this.consumeContinuationByte(), + 0xF0...0xFF => this.consume4byteIntro(), + else => { + // ASCII or other leading byte + this.advance(1); + }, + } + } + const contents = this.sliceFrom(start_position); + this.checkForSourceMap(contents); + return contents; + } + + pub fn checkForSourceMap(this: *Tokenizer, contents: []const u8) void { + { + const directive = "# sourceMappingURL="; + const directive_old = "@ sourceMappingURL="; + if (std.mem.startsWith(u8, contents, directive) or std.mem.startsWith(u8, contents, directive_old)) { + this.source_map_url = splitSourceMap(contents[directive.len..]); + } + } + + { + const directive = "# sourceURL="; + const directive_old = "@ sourceURL="; + if (std.mem.startsWith(u8, contents, directive) or std.mem.startsWith(u8, contents, directive_old)) { + this.source_map_url = splitSourceMap(contents[directive.len..]); + } + } + } + + pub fn splitSourceMap(contents: []const u8) ?[]const u8 { + // FIXME: Use bun CodepointIterator + var iter = std.unicode.Utf8Iterator{ .bytes = contents, .i = 0 }; + while (iter.nextCodepoint()) |c| { + switch (c) { + ' ', '\t', FORM_FEED_BYTE, '\r', '\n' => { + const start = 0; + const end = iter.i; + return contents[start..end]; + }, + else => {}, + } + } + return null; + } + + pub fn consumeNewline(this: *Tokenizer) void { + const byte = this.nextByteUnchecked(); + if (bun.Environment.allow_assert) { + std.debug.assert(byte == '\r' or byte == '\n' or byte == FORM_FEED_BYTE); + } + this.position += 1; + if (byte == '\r' and this.nextByte() == '\n') { + this.position += 1; + } + this.current_line_start_position = this.position; + this.current_line_number += 1; + } + + /// Advance over a single byte; the byte must be a UTF-8 + /// continuation byte. + /// + /// Binary Hex Comments + /// 0xxxxxxx 0x00..0x7F Only byte of a 1-byte character encoding + /// 110xxxxx 0xC0..0xDF First byte of a 2-byte character encoding + /// 1110xxxx 0xE0..0xEF First byte of a 3-byte character encoding + /// 11110xxx 0xF0..0xF7 First byte of a 4-byte character encoding + /// 10xxxxxx 0x80..0xBF Continuation byte: one of 1-3 bytes following the first <-- + pub fn consumeContinuationByte(this: *Tokenizer) void { + if (bun.Environment.allow_assert) std.debug.assert(this.nextByteUnchecked() & 0xC0 == 0x80); + // Continuation bytes contribute to column overcount. Note + // that due to the special case for the 4-byte sequence intro, + // we must use wrapping add here. + this.current_line_start_position +%= 1; + this.position += 1; + } + + /// Advance over a single byte; the byte must be a UTF-8 sequence + /// leader for a 4-byte sequence. + /// + /// Binary Hex Comments + /// 0xxxxxxx 0x00..0x7F Only byte of a 1-byte character encoding + /// 110xxxxx 0xC0..0xDF First byte of a 2-byte character encoding + /// 1110xxxx 0xE0..0xEF First byte of a 3-byte character encoding + /// 11110xxx 0xF0..0xF7 First byte of a 4-byte character encoding <-- + /// 10xxxxxx 0x80..0xBF Continuation byte: one of 1-3 bytes following the first + pub fn consume4byteIntro(this: *Tokenizer) void { + if (bun.Environment.allow_assert) std.debug.assert(this.nextByteUnchecked() & 0xF0 == 0xF0); + // This takes two UTF-16 characters to represent, so we + // actually have an undercount. + // this.current_line_start_position = self.current_line_start_position.wrapping_sub(1); + this.current_line_start_position -%= 1; + this.position += 1; + } + + pub fn isIdentStart(this: *Tokenizer) bool { + + // todo_stuff.match_byte + return !this.isEof() and switch (this.nextByteUnchecked()) { + 'a'...'z', 'A'...'Z', '_', 0 => true, + + // todo_stuff.match_byte + '-' => this.hasAtLeast(1) and switch (this.byteAt(1)) { + 'a'...'z', 'A'...'Z', '-', '_', 0 => true, + '\\' => !this.hasNewlineAt(1), + else => |b| !std.ascii.isASCII(b), + }, + '\\' => !this.hasNewlineAt(1), + else => |b| !std.ascii.isASCII(b), + }; + } + + /// If true, the input has at least `n` bytes left *after* the current one. + /// That is, `tokenizer.char_at(n)` will not panic. + fn hasAtLeast(this: *Tokenizer, n: usize) bool { + return this.position + n < this.src.len; + } + + fn hasNewlineAt(this: *Tokenizer, offset: usize) bool { + return this.position + offset < this.src.len and switch (this.byteAt(offset)) { + '\n', '\r', FORM_FEED_BYTE => true, + else => false, + }; + } + + pub fn startsWith(this: *Tokenizer, comptime needle: []const u8) bool { + return std.mem.eql(u8, this.src[this.position .. this.position + needle.len], needle); + } + + /// Advance over N bytes in the input. This function can advance + /// over ASCII bytes (excluding newlines), or UTF-8 sequence + /// leaders (excluding leaders for 4-byte sequences). + pub fn advance(this: *Tokenizer, n: usize) void { + if (bun.Environment.allow_assert) { + // Each byte must either be an ASCII byte or a sequence + // leader, but not a 4-byte leader; also newlines are + // rejected. + for (0..n) |i| { + const b = this.byteAt(i); + std.debug.assert(std.ascii.isASCII(b) or (b & 0xF0 != 0xF0 and b & 0xC0 != 0x80)); + std.debug.assert(b != '\r' and b != '\n' and b != '\x0C'); + } + } + this.position += n; + } + + /// Advance over any kind of byte, excluding newlines. + pub fn consumeKnownByte(this: *Tokenizer, byte: u8) void { + if (bun.Environment.allow_assert) std.debug.assert(byte != '\r' and byte != '\n' and byte != FORM_FEED_BYTE); + this.position += 1; + // Continuation bytes contribute to column overcount. + if (byte & 0xF0 == 0xF0) { + // This takes two UTF-16 characters to represent, so we + // actually have an undercount. + this.current_line_start_position -%= 1; + } else if (byte & 0xC0 == 0x80) { + // Note that due to the special case for the 4-byte + // sequence intro, we must use wrapping add here. + this.current_line_start_position +%= 1; + } + } + + pub inline fn byteAt(this: *Tokenizer, n: usize) u8 { + return this.src[this.position + n]; + } + + pub inline fn nextByte(this: *Tokenizer) ?u8 { + if (this.isEof()) return null; + return this.src[this.position]; + } + + pub inline fn nextChar(this: *Tokenizer) u32 { + const len = bun.strings.utf8ByteSequenceLength(this.src[this.position]); + return bun.strings.decodeWTF8RuneT(this.src[this.position].ptr[0..4], len, u32, bun.strings.unicode_replacement); + } + + pub inline fn nextByteUnchecked(this: *Tokenizer) u8 { + return this.src[this.position]; + } + + pub inline fn sliceFrom(this: *Tokenizer, start: usize) []const u8 { + return this.src[start..this.position]; + } +}; + +const TokenKind = enum { + /// An [](https://drafts.csswg.org/css-syntax/#typedef-ident-token) + ident, + + /// Value is the ident + function, + + /// Value is the ident + at_keyword, + + /// A [``](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "unrestricted" + /// + /// The value does not include the `#` marker. + hash, + + /// A [``](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "id" + /// + /// The value does not include the `#` marker. + idhash, + + string, + + bad_string, + + /// `url()` is represented by a `.function` token + unquoted_url, + + bad_url, + + /// Value of a single codepoint + delim, + + /// A can be fractional or an integer, and can contain an optional + or - sign + number, + + percentage, + + dimension, + + /// [](https://drafts.csswg.org/css-syntax/#typedef-unicode-range-token) + /// FIXME: this is not complete + unicode_range, + + whitespace, + + /// `` + cdc, + + /// `~=` (https://www.w3.org/TR/selectors-4/#attribute-representation) + include_match, + + /// `|=` (https://www.w3.org/TR/selectors-4/#attribute-representation) + dash_match, + + /// `^=` (https://www.w3.org/TR/selectors-4/#attribute-substrings) + prefix_match, + + /// `$=`(https://www.w3.org/TR/selectors-4/#attribute-substrings) + suffix_match, + + /// `*=` (https://www.w3.org/TR/selectors-4/#attribute-substrings) + substring_match, + + colon, + semicolon, + comma, + open_square, + close_square, + open_paren, + close_paren, + open_curly, + close_curly, + + /// Not an actual token in the spec, but we keep it anyway + comment, + + eof, + + pub fn toString(this: TokenKind) []const u8 { + return switch (this) { + .eof => "end of file", + .at_keyword => "@-keyword", + .bad_string => "bad string token", + .bad_url => "bad URL token", + .cdc => "\"-->\"", + .cdo => "\"` + cdc, + + /// `~=` (https://www.w3.org/TR/selectors-4/#attribute-representation) + include_match, + + /// `|=` (https://www.w3.org/TR/selectors-4/#attribute-representation) + dash_match, + + /// `^=` (https://www.w3.org/TR/selectors-4/#attribute-substrings) + prefix_match, + + /// `$=`(https://www.w3.org/TR/selectors-4/#attribute-substrings) + suffix_match, + + /// `*=` (https://www.w3.org/TR/selectors-4/#attribute-substrings) + substring_match, + + colon, + semicolon, + comma, + open_square, + close_square, + open_paren, + close_paren, + open_curly, + close_curly, + + /// Not an actual token in the spec, but we keep it anyway + comment: []const u8, + + eof, + + /// Return whether this token represents a parse error. + /// + /// `BadUrl` and `BadString` are tokenizer-level parse errors. + /// + /// `CloseParenthesis`, `CloseSquareBracket`, and `CloseCurlyBracket` are *unmatched* + /// and therefore parse errors when returned by one of the `Parser::next*` methods. + pub fn isParseError(this: *const Token) bool { + return switch (this.*) { + .bad_url, .bad_string, .close_paren, .close_square, .close_curly => true, + else => false, + }; + } + + pub fn raw(this: Token) []const u8 { + return switch (this) { + .ident => this.ident, + // .function => + }; + } + + pub inline fn kind(this: Token) TokenKind { + return @as(TokenKind, this); + } + + pub inline fn kindString(this: Token) []const u8 { + return this.kind.toString(); + } + + // ~toCssImpl + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(todo_stuff.depth); + } +}; + +const Num = struct { + has_sign: bool, + value: f32, + int_value: ?i32, +}; + +const Dimension = struct { + num: Num, + /// e.g. "px" + unit: []const u8, +}; + +const CopyOnWriteStr = union(enum) { + borrowed: []const u8, + owned: std.ArrayList(u8), + + pub fn append(this: *@This(), allocator: Allocator, slice: []const u8) void { + switch (this.*) { + .borrowed => { + var list = std.ArrayList(u8).initCapacity(allocator, this.borrowed.len + slice.len) catch bun.outOfMemory(); + list.appendSliceAssumeCapacity(this.borrowed); + list.appendSliceAssumeCapacity(slice); + this.* = .{ .owned = list }; + }, + .owned => { + this.owned.appendSlice(slice) catch bun.outOfMemory(); + }, + } + } + + pub fn toSlice(this: *@This()) []const u8 { + return switch (this.*) { + .borrowed => this.borrowed, + .owned => this.owned.items[0..], + }; + } +}; + +pub const color = struct { + /// The opaque alpha value of 1.0. + pub const OPAQUE: f32 = 1.0; + + const ColorError = error{ + parse, + }; + + /// Either an angle or a number. + pub const AngleOrNumber = union(enum) { + /// ``. + number: struct { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `` + angle: struct { + /// The value as a number of degrees. + degrees: f32, + }, + }; + + pub const named_colors = named_colors: { + { + break :named_colors; + } + const defined_colors = .{ + "black", .{ 0, 0, 0 }, + "silver", .{ 192, 192, 192 }, + "gray", .{ 128, 128, 128 }, + "white", .{ 255, 255, 255 }, + "maroon", .{ 128, 0, 0 }, + "red", .{ 255, 0, 0 }, + "purple", .{ 128, 0, 128 }, + "fuchsia", .{ 255, 0, 255 }, + "green", .{ 0, 128, 0 }, + "lime", .{ 0, 255, 0 }, + "olive", .{ 128, 128, 0 }, + "yellow", .{ 255, 255, 0 }, + "navy", .{ 0, 0, 128 }, + "blue", .{ 0, 0, 255 }, + "teal", .{ 0, 128, 128 }, + "aqua", .{ 0, 255, 255 }, + + "aliceblue", .{ 240, 248, 255 }, + "antiquewhite", .{ 250, 235, 215 }, + "aquamarine", .{ 127, 255, 212 }, + "azure", .{ 240, 255, 255 }, + "beige", .{ 245, 245, 220 }, + "bisque", .{ 255, 228, 196 }, + "blanchedalmond", .{ 255, 235, 205 }, + "blueviolet", .{ 138, 43, 226 }, + "brown", .{ 165, 42, 42 }, + "burlywood", .{ 222, 184, 135 }, + "cadetblue", .{ 95, 158, 160 }, + "chartreuse", .{ 127, 255, 0 }, + "chocolate", .{ 210, 105, 30 }, + "coral", .{ 255, 127, 80 }, + "cornflowerblue", .{ 100, 149, 237 }, + "cornsilk", .{ 255, 248, 220 }, + "crimson", .{ 220, 20, 60 }, + "cyan", .{ 0, 255, 255 }, + "darkblue", .{ 0, 0, 139 }, + "darkcyan", .{ 0, 139, 139 }, + "darkgoldenrod", .{ 184, 134, 11 }, + "darkgray", .{ 169, 169, 169 }, + "darkgreen", .{ 0, 100, 0 }, + "darkgrey", .{ 169, 169, 169 }, + "darkkhaki", .{ 189, 183, 107 }, + "darkmagenta", .{ 139, 0, 139 }, + "darkolivegreen", .{ 85, 107, 47 }, + "darkorange", .{ 255, 140, 0 }, + "darkorchid", .{ 153, 50, 204 }, + "darkred", .{ 139, 0, 0 }, + "darksalmon", .{ 233, 150, 122 }, + "darkseagreen", .{ 143, 188, 143 }, + "darkslateblue", .{ 72, 61, 139 }, + "darkslategray", .{ 47, 79, 79 }, + "darkslategrey", .{ 47, 79, 79 }, + "darkturquoise", .{ 0, 206, 209 }, + "darkviolet", .{ 148, 0, 211 }, + "deeppink", .{ 255, 20, 147 }, + "deepskyblue", .{ 0, 191, 255 }, + "dimgray", .{ 105, 105, 105 }, + "dimgrey", .{ 105, 105, 105 }, + "dodgerblue", .{ 30, 144, 255 }, + "firebrick", .{ 178, 34, 34 }, + "floralwhite", .{ 255, 250, 240 }, + "forestgreen", .{ 34, 139, 34 }, + "gainsboro", .{ 220, 220, 220 }, + "ghostwhite", .{ 248, 248, 255 }, + "gold", .{ 255, 215, 0 }, + "goldenrod", .{ 218, 165, 32 }, + "greenyellow", .{ 173, 255, 47 }, + "grey", .{ 128, 128, 128 }, + "honeydew", .{ 240, 255, 240 }, + "hotpink", .{ 255, 105, 180 }, + "indianred", .{ 205, 92, 92 }, + "indigo", .{ 75, 0, 130 }, + "ivory", .{ 255, 255, 240 }, + "khaki", .{ 240, 230, 140 }, + "lavender", .{ 230, 230, 250 }, + "lavenderblush", .{ 255, 240, 245 }, + "lawngreen", .{ 124, 252, 0 }, + "lemonchiffon", .{ 255, 250, 205 }, + "lightblue", .{ 173, 216, 230 }, + "lightcoral", .{ 240, 128, 128 }, + "lightcyan", .{ 224, 255, 255 }, + "lightgoldenrodyellow", .{ 250, 250, 210 }, + "lightgray", .{ 211, 211, 211 }, + "lightgreen", .{ 144, 238, 144 }, + "lightgrey", .{ 211, 211, 211 }, + "lightpink", .{ 255, 182, 193 }, + "lightsalmon", .{ 255, 160, 122 }, + "lightseagreen", .{ 32, 178, 170 }, + "lightskyblue", .{ 135, 206, 250 }, + "lightslategray", .{ 119, 136, 153 }, + "lightslategrey", .{ 119, 136, 153 }, + "lightsteelblue", .{ 176, 196, 222 }, + "lightyellow", .{ 255, 255, 224 }, + "limegreen", .{ 50, 205, 50 }, + "linen", .{ 250, 240, 230 }, + "magenta", .{ 255, 0, 255 }, + "mediumaquamarine", .{ 102, 205, 170 }, + "mediumblue", .{ 0, 0, 205 }, + "mediumorchid", .{ 186, 85, 211 }, + "mediumpurple", .{ 147, 112, 219 }, + "mediumseagreen", .{ 60, 179, 113 }, + "mediumslateblue", .{ 123, 104, 238 }, + "mediumspringgreen", .{ 0, 250, 154 }, + "mediumturquoise", .{ 72, 209, 204 }, + "mediumvioletred", .{ 199, 21, 133 }, + "midnightblue", .{ 25, 25, 112 }, + "mintcream", .{ 245, 255, 250 }, + "mistyrose", .{ 255, 228, 225 }, + "moccasin", .{ 255, 228, 181 }, + "navajowhite", .{ 255, 222, 173 }, + "oldlace", .{ 253, 245, 230 }, + "olivedrab", .{ 107, 142, 35 }, + "orange", .{ 255, 165, 0 }, + "orangered", .{ 255, 69, 0 }, + "orchid", .{ 218, 112, 214 }, + "palegoldenrod", .{ 238, 232, 170 }, + "palegreen", .{ 152, 251, 152 }, + "paleturquoise", .{ 175, 238, 238 }, + "palevioletred", .{ 219, 112, 147 }, + "papayawhip", .{ 255, 239, 213 }, + "peachpuff", .{ 255, 218, 185 }, + "peru", .{ 205, 133, 63 }, + "pink", .{ 255, 192, 203 }, + "plum", .{ 221, 160, 221 }, + "powderblue", .{ 176, 224, 230 }, + "rebeccapurple", .{ 102, 51, 153 }, + "rosybrown", .{ 188, 143, 143 }, + "royalblue", .{ 65, 105, 225 }, + "saddlebrown", .{ 139, 69, 19 }, + "salmon", .{ 250, 128, 114 }, + "sandybrown", .{ 244, 164, 96 }, + "seagreen", .{ 46, 139, 87 }, + "seashell", .{ 255, 245, 238 }, + "sienna", .{ 160, 82, 45 }, + "skyblue", .{ 135, 206, 235 }, + "slateblue", .{ 106, 90, 205 }, + "slategray", .{ 112, 128, 144 }, + "slategrey", .{ 112, 128, 144 }, + "snow", .{ 255, 250, 250 }, + "springgreen", .{ 0, 255, 127 }, + "steelblue", .{ 70, 130, 180 }, + "tan", .{ 210, 180, 140 }, + "thistle", .{ 216, 191, 216 }, + "tomato", .{ 255, 99, 71 }, + "turquoise", .{ 64, 224, 208 }, + "violet", .{ 238, 130, 238 }, + "wheat", .{ 245, 222, 179 }, + "whitesmoke", .{ 245, 245, 245 }, + "yellowgreen", .{ 154, 205, 50 }, + }; + @compileLog(defined_colors); + @compileError(todo_stuff.depth); + }; + + /// Returns the named color with the given name. + /// + pub fn parseNamedColor(ident: []const u8) ?struct { u8, u8, u8 } { + _ = ident; // autofix + @compileError(todo_stuff.depth); + } + + /// Parse a color hash, without the leading '#' character. + pub fn parseHashColor(value: []const u8) ?struct { u8, u8, u8, f32 } { + return parseHashColorImpl(value) catch return null; + } + + pub fn parseHashColorImpl(value: []const u8) ColorError!struct { u8, u8, u8, f32 } { + return switch (value.len) { + 8 => .{ + (try fromHex(value[0])) * 16 + (try fromHex(value[1])), + (try fromHex(value[2])) * 16 + (try fromHex(value[3])), + (try fromHex(value[4])) * 16 + (try fromHex(value[5])), + @as(f32, (try fromHex(value[6])) * 16 + (try fromHex(value[7]))) / 255.0, + }, + 6 => .{ + (try fromHex(value[0])) * 16 + (try fromHex(value[1])), + (try fromHex(value[2])) * 16 + (try fromHex(value[3])), + (try fromHex(value[4])) * 16 + (try fromHex(value[5])), + OPAQUE, + }, + 4 => .{ + (try fromHex(value[0])) * 17, + (try fromHex(value[1])) * 17, + (try fromHex(value[2])) * 17, + @as(f32, @intCast((try fromHex(value[3])) * 17)) / 255.0, + }, + 3 => .{ + (try fromHex(value[0])) * 17, + (try fromHex(value[1])) * 17, + (try fromHex(value[2])) * 17, + OPAQUE, + }, + else => ColorError.parse, + }; + } + + pub fn fromHex(c: u8) ColorError!u8 { + return switch (c) { + '0'...'9' => c - '0', + 'a'...'f' => c - 'a' + 10, + 'A'...'F' => c - 'A' + 10, + else => ColorError.parse, + }; + } +}; + +pub const enum_property = struct { + pub fn as_str(comptime T: type, val: T) []const u8 { + _ = val; // autofix + @compileError(todo_stuff.depth); + } +}; + +pub const comptime_parse = struct { + pub fn parse(comptime T: type, input: *Parser) Error!T { + _ = input; // autofix + @compileError(todo_stuff.depth); + } +}; + +/// A parser error. +pub const ParserError = union(enum) { + /// An at rule body was invalid. + at_rule_body_invalid, + /// An at rule prelude was invalid. + at_rule_prelude_invalid, + /// An unknown or unsupported at rule was encountered. + at_rule_invalid: []const u8, + /// Unexpectedly encountered the end of input data. + end_of_input, + /// A declaration was invalid. + invalid_declaration, + /// A media query was invalid. + invalid_media_query, + /// Invalid CSS nesting. + invalid_nesting, + /// The @nest rule is deprecated. + deprecated_nest_rule, + /// An invalid selector in an `@page` rule. + invalid_page_selector, + /// An invalid value was encountered. + invalid_value, + /// Invalid qualified rule. + qualified_rule_invalid, + /// A selector was invalid. + selector_error: SelectorError, + /// An `@import` rule was encountered after any rule besides `@charset` or `@layer`. + unexpected_import_rule, + /// A `@namespace` rule was encountered after any rules besides `@charset`, `@import`, or `@layer`. + unexpected_namespace_rule, + /// An unexpected token was encountered. + unexpected_token: Token, + /// Maximum nesting depth was reached. + maximum_nesting_depth, +}; + +/// A selector parsing error. +pub const SelectorError = union(enum) { + /// An unexpected token was found in an attribute selector. + bad_value_in_attr: Token, + /// An unexpected token was found in a class selector. + class_needs_ident: Token, + /// A dangling combinator was found. + dangling_combinator, + /// An empty selector. + empty_selector, + /// A `|` was expected in an attribute selector. + expected_bar_in_attr: Token, + /// A namespace was expected. + expected_namespace: []const u8, + /// An unexpected token was encountered in a namespace. + explicit_namespace_unexpected_token: Token, + /// An invalid pseudo class was encountered after a pseudo element. + invalid_pseudo_class_after_pseudo_element, + /// An invalid pseudo class was encountered after a `-webkit-scrollbar` pseudo element. + invalid_pseudo_class_after_webkit_scrollbar, + /// A `-webkit-scrollbar` state was encountered before a `-webkit-scrollbar` pseudo element. + invalid_pseudo_class_before_webkit_scrollbar, + /// Invalid qualified name in attribute selector. + invalid_qual_name_in_attr: Token, + /// The current token is not allowed in this state. + invalid_state, + /// The selector is required to have the `&` nesting selector at the start. + missing_nesting_prefix, + /// The selector is missing a `&` nesting selector. + missing_nesting_selector, + /// No qualified name in attribute selector. + no_qualified_name_in_attribute_selector: Token, + /// An invalid token was encountered in a pseudo element. + pseudo_element_expected_ident: Token, + /// An unexpected identifier was encountered. + unexpected_ident: []const u8, + /// An unexpected token was encountered inside an attribute selector. + unexpected_token_in_attribute_selector: Token, + /// An unsupported pseudo class or pseudo element was encountered. + unsupported_pseudo_class_or_element: []const u8, +}; + +// pub const Bitflags + +pub const serializer = struct { + /// Write a CSS name, like a custom property name. + /// + /// You should only use this when you know what you're doing, when in doubt, + /// consider using `serialize_identifier`. + pub fn serializeName(value: []const u8, comptime W: type, dest: *W) !void { + _ = value; // autofix + _ = dest; // autofix + @compileError(todo_stuff.depth); + } + + /// Write a double-quoted CSS string token, escaping content as necessary. + pub fn serializeString(value: []const u8, comptime W: type, dest: *W) !void { + _ = value; // autofix + _ = dest; // autofix + @compileError(todo_stuff.depth); + } + + pub fn serializeDimension(value: f32, unit: []const u8, comptime W: type, dest: *W) PrintErr!void { + _ = value; // autofix + _ = unit; // autofix + _ = dest; // autofix + @compileError(todo_stuff.depth); + } +}; + +pub const to_css = struct { + /// Serialize `self` in CSS syntax and return a string. + /// + /// (This is a convenience wrapper for `to_css` and probably should not be overridden.) + pub fn string(allocator: Allocator, comptime T: type, this: *T, options: PrinterOptions) PrintErr![]const u8 { + var s = ArrayList(u8){}; + const writer = s.writer(allocator); + var printer = Printer(@TypeOf(writer)).new(allocator, writer, options); + defer printer.deinit(); + switch (T) { + else => try this.toCss(printer), + } + return s; + } + + pub fn fromList(comptime T: type, this: *const ArrayList(T), comptime W: type, dest: *Printer(W)) PrintErr!void { + const len = this.items.len; + for (this.items, 0..) |*val, idx| { + try val.toCss(W, dest); + if (idx < len - 1) { + try dest.delim(',', false); + } + } + } +}; diff --git a/src/css/declaration.zig b/src/css/declaration.zig new file mode 100644 index 0000000000000..cd6888c872e2c --- /dev/null +++ b/src/css/declaration.zig @@ -0,0 +1,183 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const Error = css.Error; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const ArrayList = std.ArrayListUnmanaged; +pub const DeclarationList = ArrayList(css.Property); + +/// A CSS declaration block. +/// +/// Properties are separated into a list of `!important` declararations, +/// and a list of normal declarations. This reduces memory usage compared +/// with storing a boolean along with each property. +/// +/// TODO: multiarraylist will probably be faster here, as it makes one allocation +/// instead of two. +pub const DeclarationBlock = struct { + /// A list of `!important` declarations in the block. + important_declarations: ArrayList(css.Property), + /// A list of normal declarations in the block. + declarations: ArrayList(css.Property), + + const This = @This(); + + pub fn parse(input: *css.Parser, options: *css.ParserOptions) Error!DeclarationBlock { + var important_declarations = DeclarationList{}; + var declarations = DeclarationList{}; + var decl_parser = PropertyDeclarationParser{ + .important_declarations = &important_declarations, + .declarations = &declarations, + }; + errdefer decl_parser.deinit(); + + var parser = css.RuleBodyParser(PropertyDeclarationParser).new(input, &decl_parser); + + while (parser.next()) |res| { + _ = res catch |e| { + if (options.error_recovery) { + options.warn(e); + continue; + } + return e; + }; + } + + return DeclarationBlock{ + .important_declarations = important_declarations, + .declarations = declarations, + }; + } + + pub fn len(this: *const DeclarationBlock) usize { + return this.declarations.len + this.important_declarations.len; + } + + /// Writes the declarations to a CSS block, including starting and ending braces. + pub fn toCssBlock(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } +}; + +pub const PropertyDeclarationParser = struct { + important_declarations: *ArrayList(css.Property), + declarations: *ArrayList(css.Property), + options: *css.ParserOptions, + + const This = @This(); + + pub const AtRuleParser = struct { + pub const Prelude = void; + pub const AtRule = void; + + pub fn parsePrelude(this: *This, name: []const u8, input: *css.Parser) Error!Prelude { + _ = input; // autofix + _ = this; // autofix + _ = name; // autofix + @compileError(css.todo_stuff.errors); + } + + pub fn parseBlock(this: *This, prelude: Prelude, start: *const css.ParserState, input: *css.Parser) Error!AtRule { + _ = this; // autofix + _ = prelude; // autofix + _ = start; // autofix + return input.newError(css.BasicParseErrorKind.at_rule_invalid); + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = void; + pub const QualifiedRule = void; + + pub fn parsePrelude(this: *This, input: *css.Parser) Error!Prelude { + _ = this; // autofix + return input.newError(css.BasicParseErrorKind.qualified_rule_invalid); + } + + pub fn parseBlock(this: *This, prelude: Prelude, start: *const css.ParserState, input: *css.Parser) Error!QualifiedRule { + _ = this; // autofix + _ = prelude; // autofix + _ = start; // autofix + return input.newError(css.BasicParseErrorKind.qualified_rule_invalid); + } + }; + + pub const DeclarationParser = struct { + pub const Declaration = void; + + fn parseValue(this: *This, name: []const u8, input: *css.Parser) Error!Declaration { + parse_declaration( + name, + input, + this.declarations, + this.important_declarations, + this.options, + ); + } + }; + + pub const RuleBodyItemParser = struct { + pub fn parseQualified(this: *This) bool { + _ = this; // autofix + return false; + } + + pub fn parseDeclarations(this: *This) bool { + _ = this; // autofix + return true; + } + }; +}; + +pub fn parse_declaration( + name: []const u8, + input: *css.Parser, + declarations: *DeclarationList, + important_declarations: *DeclarationList, + options: *css.ParserOptions, +) Error!void { + const property_id = css.PropertyId.fromStr(name); + var delimiters = css.Delimiters{ .bang = true }; + if (property_id != .custom and property_id.custom != .custom) { + delimiters.curly_bracket = true; + } + const Closure = struct { + property_id: css.PropertyId, + options: *css.ParserOptions, + + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!css.Property { + return css.Property.parse(this.property_id, input2, this.options); + } + }; + var closure = Closure{ + .property_id = property_id, + .options = options, + }; + const property = try input.parseUntilBefore(delimiters, css.Property, &closure, closure.parsefn); + const Fn = struct { + pub fn parsefn(input2: *css.Parser) Error!void { + try input2.expectDelim('?'); + try input2.expectIdentMatching("important"); + } + }; + const important = if (input.tryParse(Fn.parsefn, .{})) true else false; + try input.expectExhausted(); + if (important) { + important_declarations.append(comptime { + @compileError(css.todo_stuff.think_about_allocator); + }, property) catch bun.outOfMemory(); + } else { + declarations.append(comptime { + @compileError(css.todo_stuff.think_about_allocator); + }, property); + } + return; +} diff --git a/src/css/dependencies.zig b/src/css/dependencies.zig new file mode 100644 index 0000000000000..4c4aff049ed17 --- /dev/null +++ b/src/css/dependencies.zig @@ -0,0 +1,121 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); +const DashedIdent = css_values.ident.DashedIdent; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; +// const Location = css.Location; + +const ArrayList = std.ArrayListUnmanaged; + +/// Options for `analyze_dependencies` in `PrinterOptions`. +pub const DependencyOptions = struct { + /// Whether to remove `@import` rules. + remove_imports: bool, +}; + +/// A dependency. +pub const Dependency = union(enum) { + /// An `@import` dependency. + import: ImportDependency, + /// A `url()` dependency. + url: UrlDependency, +}; + +/// A line and column position within a source file. +pub const Location = struct { + /// The line number, starting from 1. + line: u32, + /// The column number, starting from 1. + column: u32, +}; + +/// An `@import` dependency. +pub const ImportDependency = struct { + /// The url to import. + url: []const u8, + /// The placeholder that the URL was replaced with. + placeholder: []const u8, + /// An optional `supports()` condition. + supports: ?[]const u8, + /// A media query. + media: ?[]const u8, + /// The location of the dependency in the source file. + loc: SourceRange, + + pub fn new(allocator: Allocator, rule: *const css.css_rules.import.ImportRule, filename: []const u8) ImportDependency { + const supports = if (rule.supports) |*supports| brk: { + const s = css.to_css.string( + allocator, + css.css_rules.supports.SupportsCondition, + supports, + css.PrinterOptions{}, + ) catch bun.Output.panic( + "Unreachable code: failed to stringify SupportsCondition.\n\nThis is a bug in Bun's CSS printer. Please file a bug report at https://github.com/oven-sh/bun/issues/new/choose", + ); + break :brk s; + } else null; + + const media = if (rule.media.media_queries.items.len > 0) media: { + const s = css.to_css.string(allocator, css.MediaList, &rule.media, css.PrinterOptions{}) catch bun.Output.panic( + "Unreachable code: failed to stringify MediaList.\n\nThis is a bug in Bun's CSS printer. Please file a bug report at https://github.com/oven-sh/bun/issues/new/choose", + ); + break :media s; + } else null; + + const placeholder = css.css_modules.hash(allocator, "{s}_{s}", .{ filename, rule.url }, false); + + return ImportDependency{ + // TODO(zack): should we clone this? lightningcss does that + .url = rule.url, + .placeholder = placeholder, + .supports = supports, + .media = media, + .loc = SourceRange.new( + filename, + css.Location{ .line = rule.loc.line + 1, .column = rule.loc.column }, + 8, + rule.url.len + 2, + ), // TODO: what about @import url(...)? + }; + } +}; + +/// A `url()` dependency. +pub const UrlDependency = struct { + /// The url of the dependency. + url: []const u8, + /// The placeholder that the URL was replaced with. + placeholder: []const u8, + /// The location of the dependency in the source file. + loc: SourceRange, +}; + +/// Represents the range of source code where a dependency was found. +pub const SourceRange = struct { + /// The filename in which the dependency was found. + file_path: []const u8, + /// The starting line and column position of the dependency. + start: Location, + /// The ending line and column position of the dependency. + end: Location, + + pub fn new(filename: []const u8, loc: Location, offset: u32, len: usize) SourceRange { + return SourceRange{ + .file_path = filename, + .start = css.Location{ + .line = loc.line, + .column = loc.column + offset, + }, + .end = css.Location{ + .line = loc.line, + .column = loc.column + offset + @as(u32, @intCast(len)) - 1, + }, + }; + } +}; diff --git a/src/css/error.zig b/src/css/error.zig new file mode 100644 index 0000000000000..143ed337622d7 --- /dev/null +++ b/src/css/error.zig @@ -0,0 +1,54 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); +const DashedIdent = css_values.ident.DashedIdent; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; +const Location = css.Location; + +const ArrayList = std.ArrayListUnmanaged; + +/// A printer error. +pub const PrinterError = Err(PrinterErrorKind); + +/// An error with a source location. +pub fn Err(comptime T: type) type { + return struct { + /// The type of error that occurred. + kind: T, + /// The location where the error occurred. + loc: ?ErrorLocation, + }; +} + +/// A line and column location within a source file. +pub const ErrorLocation = struct { + /// The filename in which the error occurred. + filename: []const u8, + /// The line number, starting from 0. + line: u32, + /// The column number, starting from 1. + column: u32, +}; + +/// A printer error type. +pub const PrinterErrorKind = union(enum) { + /// An ambiguous relative `url()` was encountered in a custom property declaration. + ambiguous_url_in_custom_property: struct { + /// The ambiguous URL. + url: []const u8, + }, + /// A [std::fmt::Error](std::fmt::Error) was encountered in the underlying destination. + fmt_error, + /// The CSS modules `composes` property cannot be used within nested rules. + invalid_composes_nesting, + /// The CSS modules `composes` property cannot be used with a simple class selector. + invalid_composes_selector, + /// The CSS modules pattern must end with `[local]` for use in CSS grid. + invalid_css_modules_pattern_in_grid, +}; diff --git a/src/css/logical.zig b/src/css/logical.zig new file mode 100644 index 0000000000000..ed0c945fc233c --- /dev/null +++ b/src/css/logical.zig @@ -0,0 +1,30 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const Error = css.Error; + +const ArrayList = std.ArrayListUnmanaged; + +pub const PropertyCategory = enum { + logical, + physical, +}; + +pub const LogicalGroup = enum { + border_color, + border_style, + border_width, + border_radius, + margin, + scroll_margin, + padding, + scroll_padding, + inset, + size, + min_size, + max_size, +}; diff --git a/src/css/media_query.zig b/src/css/media_query.zig new file mode 100644 index 0000000000000..3c5f36b384d2b --- /dev/null +++ b/src/css/media_query.zig @@ -0,0 +1,976 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; + +const Length = css.css_values.length.Length; +const CSSNumber = css.css_values.number.CSSNumber; +const Integer = css.css_values.number.Integer; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const CSSInteger = css.css_values.number.CSSInteger; +const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +const Resolution = css.css_values.resolution.Resolution; +const Ratio = css.css_values.ratio.Ratio; +const Ident = css.css_values.ident.Ident; +const IdentFns = css.css_values.ident.IdentFns; +const EnvironmentVariable = css.css_properties.custom.EnvironmentVariable; +const DashedIdent = css.css_values.ident.DashedIdent; + +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +/// A [media query list](https://drafts.csswg.org/mediaqueries/#mq-list). +pub const MediaList = struct { + /// The list of media queries. + media_queries: ArrayList(MediaQuery), + + /// Parse a media query list from CSS. + pub fn parse(input: *css.Parser) Error!MediaList { + var media_queries = ArrayList(MediaList){}; + while (true) { + const mq = input.parseUntilBefore( + css.Delimiters{ .comma = true }, + MediaQuery, + {}, + css.voidWrap(MediaQuery, MediaQuery.parse), + ) catch |e| { + _ = e; // autofix + @compileError(css.todo_stuff.errors); + }; + media_queries.append(@compileError(css.todo_stuff.think_about_allocator), mq) catch bun.outOfMemory(); + + if (input.next()) |tok| { + if (tok.* != .comma) { + bun.Output.panic("Unreachable code: expected a comma after parsing a MediaQuery.\n\nThis is a bug in Bun's CSS parser. Please file a bug report at https://github.com/oven-sh/bun/issues/new/choose", .{}); + } + } else break; + } + + return MediaList{ .media_queries = media_queries }; + } + + pub fn toCss(this: *const MediaList, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + if (this.media_queries.items.len == 0) { + try dest.writeStr("not all"); + return; + } + + var first = true; + for (this.media_queries.items) |*query| { + if (!first) { + try dest.delim(',', false); + } + first = false; + try query.toCss(W, dest); + } + } + + /// Returns whether the media query list always matches. + pub fn alwaysMatches(this: *const MediaList) bool { + // If the media list is empty, it always matches. + return this.media_queries.items.len == 0 or brk: { + for (this.media_queries.items) |*query| { + if (!query.alwaysMatches()) break :brk false; + } + break :brk true; + }; + } +}; + +/// A binary `and` or `or` operator. +pub const Operator = enum { + /// The `and` operator. + @"and", + /// The `or` operator. + @"or", + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A [media query](https://drafts.csswg.org/mediaqueries/#media). +pub const MediaQuery = struct { + /// The qualifier for this query. + qualifier: ?Qualifier, + /// The media type for this query, that can be known, unknown, or "all". + media_type: MediaType, + /// The condition that this media query contains. This cannot have `or` + /// in the first level. + condition: ?MediaCondition, + // ~toCssImpl + const This = @This(); + + /// Returns whether the media query is guaranteed to always match. + pub fn alwaysMatches(this: *const MediaQuery) bool { + return this.qualifier == null and this.media_type == .all and this.condition == null; + } + + pub fn parse(input: *css.Parser) Error!MediaQuery { + const Fn = struct { + pub fn tryParseFn(i: *css.Parser) Error!struct { ?Qualifier, ?MediaType } { + const qualifier = i.tryParse(Qualifier.parse, .{}) catch null; + const media_type = try MediaType.parse(i); + return .{ qualifier, media_type }; + } + }; + const qualifier, const explicit_media_type = (try input.tryParse(Fn.tryParseFn, .{})) catch .{ null, null }; + + const condition = if (explicit_media_type == null) + MediaCondition.parseWithFlags(input, QueryConditionFlags{ .allow_or = true }) + else if (input.tryParse(css.Parser.expectIdentMatching, .{"and"})) + MediaCondition.parseWithFlags(input, QueryConditionFlags.empty()) + else + null; + + const media_type = explicit_media_type orelse MediaType.all; + + return MediaQuery{ + .qualifier = qualifier, + .media_type = media_type, + .condition = condition, + }; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (this.qualifier) |qual| { + try qual.toCss(W, dest); + try dest.writeChar(' '); + } + + switch (this.media_type) { + .all => { + // We need to print "all" if there's a qualifier, or there's + // just an empty list of expressions. + // + // Otherwise, we'd serialize media queries like "(min-width: + // 40px)" in "all (min-width: 40px)", which is unexpected. + if (this.qualifier.is_some() or this.condition.is_none()) { + try dest.writeStr("all"); + } + }, + .print => { + try dest.writeStr("print"); + }, + .screen => { + try dest.writeStr("screen"); + }, + .custom => |desc| { + try dest.writeStr(desc); + }, + } + + const condition = if (this.condition) |*cond| cond else return; + + const needs_parens = if (this.media_type != .all or this.qualifier != null) needs_parens: { + break :needs_parens condition.* == .operation and condition.operation.operator != .@"and"; + } else false; + + return toCssWithParensIfNeeded(W, condition, dest, needs_parens); + } +}; + +/// Flags for `parse_query_condition`. +pub const QueryConditionFlags = packed struct(u8) { + /// Whether to allow top-level "or" boolean logic. + allow_or: bool = false, + /// Whether to allow style container queries. + allow_style: bool = false, + + pub usingnamespace css.Bitflags(@This()); +}; + +pub fn toCssWithParensIfNeeded( + v: anytype, + comptime W: type, + dest: *Printer(W), + needs_parens: bool, +) PrintErr!void { + if (needs_parens) { + try dest.writeChar('('); + } + try v.toCss(W, dest); + if (needs_parens) { + try dest.writeChar(')'); + } +} + +/// A [media query qualifier](https://drafts.csswg.org/mediaqueries/#mq-prefix). +pub const Qualifier = enum { + /// Prevents older browsers from matching the media query. + only, + /// Negates a media query. + not, + + pub usingnamespace css.DefineEnumProperty(@This()); + + // ~toCssImpl + const This = @This(); +}; + +/// A [media type](https://drafts.csswg.org/mediaqueries/#media-types) within a media query. +pub const MediaType = union(enum) { + /// Matches all devices. + all, + /// Matches printers, and devices intended to reproduce a printed + /// display, such as a web browser showing a document in “Print Preview”. + print, + /// Matches all devices that aren’t matched by print. + screen, + /// An unknown media type. + custom: []const u8, + + pub fn parse(input: *css.Parser) Error!MediaType { + const name = try input.expectIdent(); + return MediaType.fromStr(name); + } + + pub fn fromStr(name: []const u8) MediaType { + // css.todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "all")) return .all; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "print")) return .print; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "screen")) return .print; + return .{ .custom = name }; + } +}; + +/// Represents a media condition. +/// +/// Implements QueryCondition interface. +pub const MediaCondition = struct { + feature: MediaFeature, + not: *MediaCondition, + operation: struct { + operator: Operator, + conditions: ArrayList(MediaCondition), + }, + + /// QueryCondition.parseFeature + pub fn parseFeature(input: *css.Parser) Error!MediaCondition { + const feature = try MediaFeature.parse(input); + return MediaCondition{ .feature = feature }; + } + + /// QueryCondition.createNegation + pub fn createNegation(condition: *MediaCondition) MediaCondition { + return MediaCondition{ .not = condition }; + } + + /// QueryCondition.createOperation + pub fn createOperation(operator: Operator, conditions: ArrayList(MediaCondition)) MediaCondition { + return MediaCondition{ + .operation = .{ + .operator = operator, + .conditions = conditions, + }, + }; + } + + /// QueryCondition.parseStyleQuery + pub fn parseStyleQuery(input: *css.Parser) Error!MediaCondition { + return try input.newErrorForNextToken(); + } + + /// QueryCondition.needsParens + pub fn needsParens(this: *const MediaCondition, parent_operator: ?Operator, targets: *const css.Targets) bool { + return switch (this.*) { + .not => true, + .operation => |operation| operation.operator != parent_operator, + .feature => |f| f.needsParens(parent_operator, targets), + }; + } + + pub fn parseWithFlags(input: *css.Parser, flags: QueryConditionFlags) Error!MediaCondition { + return parseQueryCondition(MediaCondition, input, flags); + } +}; + +/// Parse a single query condition. +pub fn parseQueryCondition( + comptime QueryCondition: type, + input: *css.Parser, + flags: QueryConditionFlags, +) Error!QueryCondition { + const location = input.currentSourceLocation(); + const is_negation, const is_style = brk: { + const tok = try input.next(); + switch (tok.*) { + .open_paren => break :brk .{ false, false }, + .ident => |ident| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "not")) break :brk .{ true, false }; + }, + .function => |f| { + if (flags.contains(QueryConditionFlags{ .allow_style = true }) and + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "style")) + { + break :brk .{ false, true }; + } + }, + else => {}, + } + return location.newUnexpectedTokenError(tok.*); + }; + + const alloc: Allocator = { + @compileError(css.todo_stuff.think_about_allocator); + }; + + const first_condition: QueryCondition = first_condition: { + const val: u8 = @as(u8, @intFromBool(is_negation)) << 1 | @as(u8, @intFromBool(is_style)); + // (is_negation, is_style) + switch (val) { + // (true, false) + 0b10 => { + const inner_condition = try parseParensOrFunction(QueryCondition, input, flags); + return QueryCondition.createNegation(bun.create(alloc, QueryCondition, inner_condition)); + }, + // (true, true) + 0b11 => { + const inner_condition = try QueryCondition.parseStyleQuery(input); + return QueryCondition.createNegation(bun.create(alloc, QueryCondition, inner_condition)); + }, + 0b00 => break :first_condition try parseParenBlock(QueryCondition, input, flags), + 0b01 => break :first_condition try QueryCondition.parseStyleQuery(input), + else => unreachable, + } + }; + + const operator: Operator = if (input.tryParse(Operator.parse, .{})) |op| + op + else + return first_condition; + + if (!flags.contains(QueryConditionFlags{ .allow_or = true }) and operator == .@"or") { + return location.newUnexpectedTokenError(css.Token{ .ident = "or" }); + } + + var conditions = ArrayList(QueryCondition){}; + conditions.append( + @compileError(css.todo_stuff.think_about_allocator), + first_condition, + ) catch unreachable; + conditions.append( + @compileError(css.todo_stuff.think_about_allocator), + try parseParensOrFunction(QueryCondition, input, flags), + ) catch unreachable; + + const delim = switch (operator) { + .@"and" => "and", + .@"or" => "or", + }; + + while (true) { + input.tryParse(css.Parser.expectIdentMatching, .{delim}) catch { + return QueryCondition.createOperation(operator, conditions); + }; + + conditions.append( + @compileError(css.todo_stuff.think_about_allocator), + try parseParensOrFunction(QueryCondition, input, flags), + ) catch unreachable; + } +} + +/// Parse a media condition in parentheses, or a style() function. +pub fn parseParensOrFunction( + comptime QueryCondition: type, + input: *css.Parser, + flags: QueryConditionFlags, +) Error!QueryCondition { + const location = input.currentSourceLocation(); + const t = try input.next(); + switch (t.*) { + .open_paren => return parseParenBlock(QueryCondition, input, flags), + .function => |f| { + if (flags.contains(QueryConditionFlags{ .allow_style = true }) and + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "style")) + { + return QueryCondition.parseStyleQuery(input); + } + }, + else => {}, + } + return location.newUnexpectedTokenError(t.*); +} + +fn parseParenBlock( + comptime QueryCondition: type, + input: *css.Parser, + flags: QueryConditionFlags, +) Error!QueryCondition { + const Closure = struct { + flags: QueryConditionFlags, + pub fn parseNestedBlockFn(this: *@This(), i: *css.Parser) Error!QueryCondition { + if (i.tryParse(@This().tryParseFn, .{this})) |inner| { + return inner; + } + + return QueryCondition.parseFeature(i); + } + + pub fn tryParseFn(i: *css.Parser, this: *@This()) Error!QueryCondition { + return parseQueryCondition(QueryCondition, i, this.flags); + } + }; + + var closure = Closure{ + .flags = flags, + }; + return try input.parseNestedBlock(QueryCondition, &closure, Closure.parseNestedBlockFn); +} + +/// A [media feature](https://drafts.csswg.org/mediaqueries/#typedef-media-feature) +pub const MediaFeature = QueryFeature(MediaFeatureId); + +const MediaFeatureId = union(enum) { + /// The [width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#width) media feature. + width, + /// The [height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#height) media feature. + height, + /// The [aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#aspect-ratio) media feature. + @"aspect-ratio", + /// The [orientation](https://w3c.github.io/csswg-drafts/mediaqueries-5/#orientation) media feature. + orientation, + /// The [overflow-block](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-block) media feature. + @"overflow-block", + /// The [overflow-inline](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-inline) media feature. + @"overflow-inline", + /// The [horizontal-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#horizontal-viewport-segments) media feature. + @"horizontal-viewport-segments", + /// The [vertical-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#vertical-viewport-segments) media feature. + @"vertical-viewport-segments", + /// The [display-mode](https://w3c.github.io/csswg-drafts/mediaqueries-5/#display-mode) media feature. + @"display-mode", + /// The [resolution](https://w3c.github.io/csswg-drafts/mediaqueries-5/#resolution) media feature. + resolution, + /// The [scan](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scan) media feature. + scan, + /// The [grid](https://w3c.github.io/csswg-drafts/mediaqueries-5/#grid) media feature. + grid, + /// The [update](https://w3c.github.io/csswg-drafts/mediaqueries-5/#update) media feature. + update, + /// The [environment-blending](https://w3c.github.io/csswg-drafts/mediaqueries-5/#environment-blending) media feature. + @"environment-blending", + /// The [color](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color) media feature. + color, + /// The [color-index](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-index) media feature. + @"color-index", + /// The [monochrome](https://w3c.github.io/csswg-drafts/mediaqueries-5/#monochrome) media feature. + monochrome, + /// The [color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-gamut) media feature. + @"color-gamut", + /// The [dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#dynamic-range) media feature. + @"dynamic-range", + /// The [inverted-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#inverted-colors) media feature. + @"inverted-colors", + /// The [pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#pointer) media feature. + pointer, + /// The [hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#hover) media feature. + hover, + /// The [any-pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-pointer) media feature. + @"any-pointer", + /// The [any-hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-hover) media feature. + @"any-hover", + /// The [nav-controls](https://w3c.github.io/csswg-drafts/mediaqueries-5/#nav-controls) media feature. + @"nav-controls", + /// The [video-color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-color-gamut) media feature. + @"video-color-gamut", + /// The [video-dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-dynamic-range) media feature. + @"video-dynamic-range", + /// The [scripting](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scripting) media feature. + scripting, + /// The [prefers-reduced-motion](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-motion) media feature. + @"prefers-reduced-motion", + /// The [prefers-reduced-transparency](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-transparency) media feature. + @"prefers-reduced-transparency", + /// The [prefers-contrast](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-contrast) media feature. + @"prefers-contrast", + /// The [forced-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#forced-colors) media feature. + @"forced-colors", + /// The [prefers-color-scheme](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-color-scheme) media feature. + @"prefers-color-scheme", + /// The [prefers-reduced-data](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-data) media feature. + @"prefers-reduced-data", + /// The [device-width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-width) media feature. + @"device-width", + /// The [device-height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-height) media feature. + @"device-height", + /// The [device-aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-aspect-ratio) media feature. + @"device-aspect-ratio", + + /// The non-standard -webkit-device-pixel-ratio media feature. + @"-webkit-device-pixel-ratio", + /// The non-standard -moz-device-pixel-ratio media feature. + @"-moz-device-pixel-ratio", + + pub usingnamespace css.DefineEnumProperty(@This()); + + const meta = .{ + .width = MediaFeatureType.length, + .height = MediaFeatureType.length, + .@"aspect-ratio" = MediaFeatureType.ratio, + .orientation = MediaFeatureType.ident, + .@"overflow-block" = MediaFeatureType.ident, + .@"overflow-inline" = MediaFeatureType.ident, + .@"horizontal-viewport-segments" = MediaFeatureType.integer, + .@"vertical-viewport-segments" = MediaFeatureType.integer, + .@"display-mode" = MediaFeatureType.ident, + .resolution = MediaFeatureType.resolution, + .scan = MediaFeatureType.ident, + .grid = MediaFeatureType.boolean, + .update = MediaFeatureType.ident, + .@"environment-blending" = MediaFeatureType.ident, + .color = MediaFeatureType.integer, + .@"color-index" = MediaFeatureType.integer, + .monochrome = MediaFeatureType.integer, + .@"color-gamut" = MediaFeatureType.ident, + .@"dynamic-range" = MediaFeatureType.ident, + .@"inverted-colors" = MediaFeatureType.ident, + .pointer = MediaFeatureType.ident, + .hover = MediaFeatureType.ident, + .@"any-pointer" = MediaFeatureType.ident, + .@"any-hover" = MediaFeatureType.ident, + .@"nav-controls" = MediaFeatureType.ident, + .@"video-color-gamut" = MediaFeatureType.ident, + .@"video-dynamic-range" = MediaFeatureType.ident, + .scripting = MediaFeatureType.ident, + .@"prefers-reduced-motion" = MediaFeatureType.ident, + .@"prefers-reduced-transparency" = MediaFeatureType.ident, + .@"prefers-contrast" = MediaFeatureType.ident, + .@"forced-colors" = MediaFeatureType.ident, + .@"prefers-color-scheme" = MediaFeatureType.ident, + .@"prefers-reduced-data" = MediaFeatureType.ident, + .@"device-width" = MediaFeatureType.length, + .@"device-height" = MediaFeatureType.length, + .@"device-aspect-ratio" = MediaFeatureType.ratio, + .@"-webkit-device-pixel-ratio" = MediaFeatureType.number, + .@"-moz-device-pixel-ratio" = MediaFeatureType.number, + }; + + // Make sure we defined ecah field + comptime { + const fields = std.meta.fields(@This()); + for (fields) |field| { + _ = @field(meta, field.name); + } + } + + pub fn valueType(this: *const MediaFeatureId) MediaFeatureType { + return @field(meta, @tagName(this.*)); + } +}; + +pub fn QueryFeature(comptime FeatureId: type) type { + return union(enum) { + /// A plain media feature, e.g. `(min-width: 240px)`. + plain: struct { + /// The name of the feature. + name: MediaFeatureName(FeatureId), + /// The feature value. + value: MediaFeatureValue, + }, + + /// A boolean feature, e.g. `(hover)`. + boolean: struct { + /// The name of the feature. + name: MediaFeatureName(FeatureId), + }, + + /// A range, e.g. `(width > 240px)`. + range: struct { + /// The name of the feature. + name: MediaFeatureName(FeatureId), + /// A comparator. + operator: MediaFeatureComparison, + /// The feature value. + value: MediaFeatureValue, + }, + + /// An interval, e.g. `(120px < width < 240px)`. + interval: struct { + /// The name of the feature. + name: MediaFeatureName(FeatureId), + /// A start value. + start: MediaFeatureValue, + /// A comparator for the start value. + start_operator: MediaFeatureComparison, + /// The end value. + end: MediaFeatureValue, + /// A comparator for the end value. + end_operator: MediaFeatureComparison, + }, + + const This = @This(); + + pub fn needsParens(this: *const This, parent_operator: ?Operator, targets: *const css.Targets) bool { + return parent_operator != .@"and" and + this.* == .interval and + targets.shouldCompile(css.Features{ .media_interval_syntax = true }); + } + + pub fn parse(input: *css.Parser) Error!This { + if (input.tryParse(parseNameFirst, .{})) |res| { + return res; + } else |e| { + if (e == css.ParserError.invalid_media_query) { + @compileError(css.todo_stuff.errors); + } + return parseValueFirst(input); + } + } + + pub fn parseNameFirst(input: *css.Parser) Error!This { + const name, const legacy_op = try MediaFeatureName(FeatureId).parse(input); + + const operator = if (input.tryParse(consumeOperationOrColon, .{true})) |operator| operator else return .{ + .boolean = .{ .name = name }, + }; + + if (operator != null and legacy_op != null) { + return try input.newCustomError(css.ParserError.invalid_media_query); + } + + const value = try MediaFeatureValue.parse(input, name.valueType()); + if (!value.checkType(name.valueType())) { + return try input.newCustomError(css.ParserError.invalid_media_query); + } + + if (operator orelse legacy_op) |op| { + if (!name.valueType().allowsRanges()) { + return try input.newCustomError(css.ParserError.invalid_media_query); + } + + return .{ + .range = .{ + .name = name, + .operator = op, + .value = value, + }, + }; + } else { + return .{ + .plain = .{ + .name = name, + .value = value, + }, + }; + } + } + + pub fn parseValueFirst(input: *css.Parser) Error!This { + // We need to find the feature name first so we know the type. + const start = input.state(); + const name = name: { + while (true) { + if (MediaFeatureName(FeatureId).parse(input)) |result| { + const name: MediaFeatureName(FeatureId) = result[0]; + const legacy_op: ?MediaFeatureComparison = result[1]; + if (legacy_op != null) { + return input.newCustomError(css.ParserError.invalid_media_query); + } + break :name name; + } + if (input.isExhausted()) { + return input.newCustomError(css.ParserError.invalid_media_query); + } + } + }; + + input.reset(&start); + + // Now we can parse the first value. + const value = try MediaFeatureValue.parse(input, name.valueType()); + const operator = try consumeOperationOrColon(input, false); + + // Skip over the feature name again. + { + const feature_name, const blah = try MediaFeatureName(FeatureId).parse(input); + _ = blah; + bun.debugAssert(bun.strings.eql(feature_name, name)); + } + + if (!name.valueType().allowsRanges() or !value.checkType(name.valueType())) { + return input.newCustomError(css.ParserError.invalid_media_query); + } + + if (input.tryParse(consumeOperationOrColon, .{ input, false })) |end_operator_| { + const start_operator = operator.?; + const end_operator = end_operator_.?; + // Start and end operators must be matching. + const GT: u8 = comptime @intFromEnum(MediaFeatureComparison.@"greater-than"); + const GTE: u8 = comptime @intFromEnum(MediaFeatureComparison.@"greater-than-equal"); + const LT: u8 = comptime @intFromEnum(MediaFeatureComparison.@"less-than"); + const LTE: u8 = comptime @intFromEnum(MediaFeatureComparison.@"less-than-equal"); + const check_val: u8 = @intFromEnum(start_operator) | @intFromEnum(end_operator); + switch (check_val) { + GT | GT, + GT | GTE, + GTE | GTE, + LT | LT, + LT | LTE, + LTE | LTE, + => {}, + else => return input.newCustomError(css.ParserError.invalid_media_query), + } + + const end_value = try MediaFeatureValue.parse(input, name.valueType()); + if (!end_value.checkType(name.valueType())) { + return input.newCustomError(css.ParserError.invalid_media_query); + } + + return .{ + .interval = .{ + .name = name, + .start = value, + .start_operator = start_operator, + .end = end_value, + .end_operator = end_operator, + }, + }; + } else { + const final_operator = operator.?.opposite(); + _ = final_operator; // autofix + return .{ + .range = .{ + .name = name, + .operator = operator, + .value = value, + }, + }; + } + } + }; +} + +/// Consumes an operation or a colon, or returns an error. +fn consumeOperationOrColon(input: *css.Parser, allow_colon: bool) Error!(?MediaFeatureComparison) { + const location = input.currentSourceLocation(); + const first_delim = first_delim: { + const loc = input.currentSourceLocation(); + const next_token = try input.next(); + switch (next_token.*) { + .colon => if (allow_colon) return null, + .delim => |oper| break :first_delim oper, + else => {}, + } + return loc.newUnexpectedTokenError(next_token.*); + }; + + switch (first_delim) { + '=' => return .equal, + '>' => { + if (input.tryParse(css.Parser.expectDelim, .{'='})) { + return .@"greater-than-equal"; + } + return .@"greater-than"; + }, + '<' => { + if (input.tryParse(css.Parser.expectDelim, .{'='})) { + return .@"less-than-equal"; + } + return .@"less-than"; + }, + else => return location.newUnexpectedTokenError(.{ .delim = first_delim }), + } +} + +pub const MediaFeatureComparison = enum(u8) { + /// `=` + equal = 1, + /// `>` + @"greater-than" = 2, + /// `>=` + @"greater-than-equal" = 4, + /// `<` + @"less-than" = 8, + /// `<=` + @"less-than-equal" = 16, + + pub usingnamespace css.DefineEnumProperty(@This()); + + pub fn opposite(this: MediaFeatureComparison) MediaFeatureComparison { + return switch (this) { + .equal => .equal, + .@"greater-than" => .@"less-than", + .@"greater-than-equal" => .@"less-than-equal", + .@"less-than" => .@"greater-than", + .@"less-than-equal" => .@"greater-than-equal", + }; + } +}; + +/// [media feature value](https://drafts.csswg.org/mediaqueries/#typedef-mf-value) within a media query. +/// +/// See [MediaFeature](MediaFeature). +pub const MediaFeatureValue = union(enum) { + /// A length value. + length: Length, + /// A number value. + number: CSSNumber, + /// An integer value. + integer: CSSInteger, + /// A boolean value. + boolean: bool, + /// A resolution. + resolution: Resolution, + /// A ratio. + ratio: Ratio, + /// An identifier. + ident: Ident, + /// An environment variable reference. + env: EnvironmentVariable, + + pub fn checkType(this: *const @This(), expected_type: MediaFeatureType) bool { + const vt = this.valueType(); + if (expected_type == .unknown or vt == .unknown) return true; + return expected_type == vt; + } + + /// Parses a single media query feature value, with an expected type. + /// If the type is unknown, pass MediaFeatureType::Unknown instead. + pub fn parse(input: *css.Parser, expected_type: MediaFeatureType) Error!MediaFeatureValue { + if (input.tryParse(parseKnown, .{expected_type})) |value| { + return value; + } + + return parseUnknown(input); + } + + pub fn parseKnown(input: *css.Parser, expected_type: MediaFeatureType) Error!MediaFeatureValue { + return switch (expected_type) { + .bool => { + const value = try CSSIntegerFns.parse(input); + if (value != 0 and value != 1) return input.newCustomError(css.ParserError.invalid_value); + return .{ .boolean = value == 1 }; + }, + .number => .{ .number = try CSSNumberFns.parse(input) }, + .integer => .{ .integer = try CSSIntegerFns.parse(input) }, + .length => .{ .integer = try Length.parse(input) }, + }; + } + + pub fn parseUnknown(input: *css.Parser) Error!MediaFeatureValue { + // Ratios are ambiguous with numbers because the second param is optional (e.g. 2/1 == 2). + // We require the / delimiter when parsing ratios so that 2/1 ends up as a ratio and 2 is + // parsed as a number. + if (input.tryParse(Ratio.parseRequired, .{})) |ratio| return .{ .ratio = ratio }; + + // Parse number next so that unitless values are not parsed as lengths. + if (input.tryParse(CSSNumberFns.parse, .{})) |num| return .{ .number = num }; + + if (input.tryParse(Length.parse, .{})) |res| return .{ .length = res }; + + if (input.tryParse(Resolution.parse, .{})) |res| return .{ .resolution = res }; + + if (input.tryParse(EnvironmentVariable.parse, .{})) |env| return .{ .env = env }; + + const ident = try IdentFns.parse(input); + return .{ .ident = ident }; + } +}; + +/// The type of a media feature. +pub const MediaFeatureType = enum { + /// A length value. + length, + /// A number value. + number, + /// An integer value. + integer, + /// A boolean value, either 0 or 1. + boolean, + /// A resolution. + resolution, + /// A ratio. + ratio, + /// An identifier. + ident, + /// An unknown type. + unknown, + + pub fn allowsRanges(this: MediaFeatureType) bool { + return switch (this) { + .length, .number, .integer, .resolution, .ratio, .unknown => true, + .boolean, .ident => false, + }; + } +}; + +pub fn MediaFeatureName(comptime FeatureId: type) type { + return union(enum) { + /// A standard media query feature identifier. + standard: FeatureId, + + /// A custom author-defined environment variable. + custom: DashedIdent, + + /// An unknown environment variable. + unknown: Ident, + + const This = @This(); + + pub fn valueType(this: *const This) MediaFeatureType { + return switch (this) { + .standard => |standard| standard.valueType(), + _ => .unknown, + }; + } + + /// Parses a media feature name. + pub fn parse(input: *css.Parser) Error!struct { This, ?MediaFeatureComparison } { + const alloc: Allocator = { + @compileError(css.todo_stuff.think_about_allocator); + }; + const ident = try input.expectIdent(); + + if (bun.strings.startsWith(ident, "--")) { + return .{ + .{ + .custom = .ident, + }, + null, + }; + } + + var name = ident; + + // Webkit places its prefixes before "min" and "max". Remove it first, and + // re-add after removing min/max. + const is_webkit = bun.strings.startsWithCaseInsensitiveAscii(name, "-webkit-"); + if (is_webkit) { + name = name[8..]; + } + + const comparator = comparator: { + if (bun.strings.startsWithCaseInsensitiveAscii(name, "min-")) { + name = name[4..]; + break :comparator .@"greater-than-equal"; + } else if (bun.strings.startsWithCaseInsensitiveAscii(name, "max-")) { + name = name[4..]; + break :comparator .@"less-than-equal"; + } else break :comparator null; + }; + + const final_name = if (is_webkit) name: { + // PERF: stack buffer here? + break :name std.fmt.allocPrint(alloc, "-webkit-{s}", .{}) catch bun.outOfMemory(); + } else name; + + if (FeatureId.parseString(final_name)) |standard| { + return .{ + .{ .standard = standard }, + comparator, + }; + } + + return .{ + .{ + .unknown = ident, + }, + null, + }; + } + }; +} diff --git a/src/css/printer.zig b/src/css/printer.zig new file mode 100644 index 0000000000000..968580c8e6167 --- /dev/null +++ b/src/css/printer.zig @@ -0,0 +1,310 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); +const DashedIdent = css_values.ident.DashedIdent; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; +const Location = css.Location; + +const ArrayList = std.ArrayListUnmanaged; + +const sourcemap = @import("./sourcemap.zig"); + +/// Options that control how CSS is serialized to a string. +pub const PrinterOptions = struct { + /// Whether to minify the CSS, i.e. remove white space. + minify: bool = false, + /// An optional reference to a source map to write mappings into. + /// (Available when the `sourcemap` feature is enabled.) + source_map: ?*sourcemap.SourceMap = null, + /// An optional project root path, used to generate relative paths for sources used in CSS module hashes. + project_root: ?[]const u8 = null, + /// Targets to output the CSS for. + targets: Targets = .{}, + /// Whether to analyze dependencies (i.e. `@import` and `url()`). + /// If true, the dependencies are returned as part of the + /// [ToCssResult](super::stylesheet::ToCssResult). + /// + /// When enabled, `@import` and `url()` dependencies + /// are replaced with hashed placeholders that can be replaced with the final + /// urls later (after bundling). + analyze_dependencies: ?css.dependencies.DependencyOptions = null, + /// A mapping of pseudo classes to replace with class names that can be applied + /// from JavaScript. Useful for polyfills, for example. + pseudo_classes: ?PseudoClasses = null, +}; + +/// A mapping of user action pseudo classes to replace with class names. +/// +/// See [PrinterOptions](PrinterOptions). +const PseudoClasses = struct { + /// The class name to replace `:hover` with. + hover: ?[]const u8 = null, + /// The class name to replace `:active` with. + active: ?[]const u8 = null, + /// The class name to replace `:focus` with. + focus: ?[]const u8 = null, + /// The class name to replace `:focus-visible` with. + focus_visible: ?[]const u8 = null, + /// The class name to replace `:focus-within` with. + focus_within: ?[]const u8 = null, +}; + +/// Target browsers and features to compile. +pub const Targets = struct { + /// Browser targets to compile the CSS for. + browsers: ?Browsers = null, + /// Features that should always be compiled, even when supported by targets. + include: Features = 0, + /// Features that should never be compiled, even when unsupported by targets. + exclude: Features = 0, + + pub fn shouldCompile(this: *const Targets, feature: css.compat.Feature, flag: Features) bool { + _ = this; // autofix + _ = feature; // autofix + _ = flag; // autofix + @compileError(css.todo_stuff.depth); + } +}; + +pub const Features = packed struct(u32) { + pub usingnamespace css.Bitflags(@This()); + comptime { + @compileError(css.todo_stuff.depth); + } +}; + +/// Browser versions to compile CSS for. +/// +/// Versions are represented as a single 24-bit integer, with one byte +/// per `major.minor.patch` component. +/// +/// # Example +/// +/// This example represents a target of Safari 13.2.0. +/// +/// ``` +/// const Browsers = struct { +/// safari: ?u32 = (13 << 16) | (2 << 8), +/// ..Browsers{} +/// }; +/// ``` +const Browsers = struct { + android: ?u32 = null, + chrome: ?u32 = null, + edge: ?u32 = null, + firefox: ?u32 = null, + ie: ?u32 = null, + ios_saf: ?u32 = null, + opera: ?u32 = null, + safari: ?u32 = null, + samsung: ?u32 = null, +}; + +/// A `Printer` represents a destination to output serialized CSS, as used in +/// the [ToCss](super::traits::ToCss) trait. It can wrap any destination that +/// implements [std::fmt::Write](std::fmt::Write), such as a [String](String). +/// +/// A `Printer` keeps track of the current line and column position, and uses +/// this to generate a source map if provided in the options. +/// +/// `Printer` also includes helper functions that assist with writing output +/// that respects options such as `minify`, and `css_modules`. +pub fn Printer(comptime Writer: type) type { + return struct { + // #[cfg(feature = "sourcemap")] + sources: ?*ArrayList([]const u8), + dest: Writer, + loc: Location, + indent_amt: u8, + line: u32 = 0, + col: u32 = 0, + minify: bool, + targets: Targets, + css_module: ?css.CssModule, + dependencies: ?ArrayList(css.Dependency), + remove_imports: bool, + PseudoClasses: ?PseudoClasses, + indentation_buf: std.ArrayList(u8), + // TODO: finish the fields + + const This = @This(); + + /// Returns the current source filename that is being printed. + pub fn filename(this: *const This) []const u8 { + if (this.sources) |sources| { + if (this.loc.source_index < sources.items.len) return sources.items[this.loc.source_index]; + } + return "unknown.css"; + } + + /// Returns whether the indent level is greater than one. + pub fn isNested(this: *const This) bool { + return this.ident > 2; + } + + /// Returns an error of the given kind at the provided location in the current source file. + pub fn newError( + this: *const This, + kind: css.PrinterErrorKind, + loc: css.dependencies.Location, + ) css.Err(css.PrinterErrorKind) { + _ = this; // autofix + _ = kind; // autofix + _ = loc; // autofix + @compileError(css.todo_stuff.errors); + } + + pub fn deinit(this: *This) void { + _ = this; // autofix + @compileError(css.todo_stuff.depth); + } + + /// Increases the current indent level. + pub fn indent(this: *This) void { + this.indent_amt += 2; + } + + /// Decreases the current indent level. + pub fn dedent(this: *This) void { + this.indent_amt -= 2; + } + + const INDENTS: []const []const u8 = indents: { + const levels = 32; + var indents: [levels][]const u8 = undefined; + for (0..levels) |i| { + const n = i * 2; + var str: [n]u8 = undefined; + for (0..n) |j| { + str[j] = ' '; + } + indents[i] = str; + } + break :indents indents; + }; + + fn getIndent(this: *This, idnt: u8) []const u8 { + // divide by 2 to get index into table + const i = idnt >> 1; + // PERF: may be faster to just do `i < (IDENTS.len - 1) * 2` (e.g. 62 if IDENTS.len == 32) here + if (i < INDENTS.len) { + return INDENTS[i]; + } + if (this.indentation_buf.items.len < idnt) { + this.indentation_buf.appendNTimes(' ', this.indentation_buf.items.len - idnt) catch unreachable; + } else { + this.indentation_buf.items = this.indentation_buf.items[0..idnt]; + } + return this.indentation_buf.items; + } + + fn writeIndent(this: *This) !void { + bun.debugAssert(!this.minify); + if (this.ident > 0) { + // try this.writeStr(this.getIndent(this.ident)); + try this.dest.writeByteNTimes(' ', this.ident); + } + } + + pub fn new(allocator: Allocator, dest: Writer, options: PrinterOptions) This { + return .{ + .sources = null, + .dest = dest, + .minify = options.minify, + .targets = options.targets, + .dependencies = if (options.analyze_dependencies != null) ArrayList(css.Dependency){} else null, + .remove_imports = options.analyze_dependencies != null and options.analyze_dependencies.?.remove_imports, + .pseudo_classes = options.pseudo_classes, + .indentation_buf = std.ArrayList(u8).init(allocator), + }; + } + + /// Writes a raw string to the underlying destination. + /// + /// NOTE: Is is assumed that the string does not contain any newline characters. + /// If such a string is written, it will break source maps. + pub fn writeStr(this: *This, s: []const u8) void { + this.col += s.len; + this.dest.writeAll(s) catch bun.outOfMemory(); + } + + pub fn writeFmt(this: *This, comptime fmt: []const u8, args: anytype) void { + _ = this; // autofix + _ = fmt; // autofix + _ = args; // autofix + @compileError(css.todo_stuff.depth); + } + + /// Write a single character to the underlying destination. + pub fn writeChar(this: *This, char: u8) void { + if (char == '\n') { + this.line += 1; + this.col = 0; + } else { + this.col += 1; + } + try this.dest.writeByte(char); + // var p: [4]u8 = undefined; + // const len = bun.strings.encodeWTF8RuneT(&p, u32, char); + // try this.dest.writeAll(p[0..len]) catch bun.outOfMemory(); + } + + /// Writes a newline character followed by indentation. + /// If the `minify` option is enabled, then nothing is printed. + pub fn newline(this: *This) !void { + if (this.minify) { + return; + } + + try this.writeChar('\n'); + try this.writeIndent(); + } + + /// Writes a delimiter character, followed by whitespace (depending on the `minify` option). + /// If `ws_before` is true, then whitespace is also written before the delimiter. + pub fn delim(this: *This, delim_: u8, ws_before: bool) !void { + if (ws_before) { + try this.whitespace(); + } + try this.writeChar(delim_); + return this.whitespace(); + } + + /// Writes a single whitespace character, unless the `minify` option is enabled. + /// + /// Use `write_char` instead if you wish to force a space character to be written, + /// regardless of the `minify` option. + pub fn whitespace(this: *This) !void { + if (this.minify) return; + return try this.writeChar(' '); + } + + pub fn withContext( + this: *This, + selectors: *css.SelectorList, + comptime func: anytype, + args: anytype, + ) bun.meta.ReturnOfType(@TypeOf(func)) { + _ = this; // autofix + _ = selectors; // autofix + _ = args; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn withClearedContext( + this: *This, + comptime func: anytype, + args: anytype, + ) bun.meta.ReturnOfType(@TypeOf(func)) { + _ = this; // autofix + _ = args; // autofix + @compileError(css.todo_stuff.depth); + } + }; +} diff --git a/src/css/properties/custom.zig b/src/css/properties/custom.zig new file mode 100644 index 0000000000000..8be1bd30cf8ad --- /dev/null +++ b/src/css/properties/custom.zig @@ -0,0 +1,925 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const css_values = @import("../values/values.zig"); +pub const Printer = css.Printer; +pub const PrintErr = css.PrintErr; +const DashedIdent = css_values.ident.DashedIdent; +const DashedIdentFns = css_values.ident.DashedIdentFns; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; + +pub const CssColor = css.css_values.color.CssColor; +pub const RGBA = css.css_values.color.RGBA; +pub const SRGB = css.css_values.color.SRGB; +pub const HSL = css.css_values.color.HSL; +pub const CSSInteger = css.css_values.number.CSSInteger; +pub const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +pub const Url = css.css_values.url.Url; +pub const DashedIdentReference = css.css_values.ident.DashedIdentReference; +pub const CustomIdent = css.css_values.ident.CustomIdent; +pub const CustomIdentFns = css.css_values.ident.CustomIdentFns; +pub const LengthValue = css.css_values.length.LengthValue; +pub const Angle = css.css_values.angle.Angle; +pub const Time = css.css_values.time.Time; +pub const Resolution = css.css_values.resolution.Resolution; +pub const AnimationName = css.css_properties.animation.AnimationName; +const ComponentParser = css.css_values.color.ComponentParser; + +const ArrayList = std.ArrayListUnmanaged; + +/// PERF: nullable optimization +pub const TokenList = struct { + v: std.ArrayListUnmanaged(TokenOrValue), + + const This = @This(); + + pub fn toCss( + this: *const This, + comptime W: type, + dest: *Printer(W), + is_custom_property: bool, + ) PrintErr!void { + if (!dest.minify and this.v.items.len == 1 and this.v.items[0].isWhitespace()) { + return; + } + + var has_whitespace = false; + for (this.v.items, 0..) |*token_or_value, i| { + switch (token_or_value.*) { + .color => |color| { + try color.toCss(W, dest); + has_whitespace = false; + }, + .unresolved_color => |color| { + try color.toCss(W, dest, is_custom_property); + has_whitespace = false; + }, + .url => |url| { + if (dest.dependencies != null and is_custom_property and !url.isAbsolute()) { + @compileError(css.todo_stuff.errors); + } + try url.toCss(W, dest); + has_whitespace = false; + }, + .@"var" => |@"var"| { + try @"var".toCss(W, dest, is_custom_property); + has_whitespace = try this.writeWhitespaceIfNeeded(i, dest); + }, + .env => |env| { + try env.toCss(W, dest, is_custom_property); + has_whitespace = try this.writeWhitespaceIfNeeded(i, W, dest); + }, + .function => |f| { + try f.toCss(W, dest, is_custom_property); + has_whitespace = try this.writeWhitespaceIfNeeded(i, W, dest); + }, + .length => |v| { + // Do not serialize unitless zero lengths in custom properties as it may break calc(). + const value, const unit = v.toUnitValue(); + try try css.serializer.serializeDimension(value, unit, W, dest); + has_whitespace = false; + }, + .angle => |v| { + try v.toCss(W, dest); + has_whitespace = false; + }, + .resolution => |v| { + try v.toCss(W, dest); + has_whitespace = false; + }, + .dashed_ident => |v| { + try DashedIdentFns.toCss(v, W, dest); + has_whitespace = false; + }, + .animation_name => |v| { + try v.toCss(W, dest); + has_whitespace = false; + }, + .token => |token| switch (token) { + .delim => |d| { + if (d == '+' or d == '-') { + try dest.writeChar(' '); + try dest.writeChar(d); + try dest.writeChar(' '); + } else { + const ws_before = !has_whitespace and (d == '/' or d == '*'); + try dest.delim(d, ws_before); + } + has_whitespace = true; + }, + .comma => { + try dest.delim(',', false); + has_whitespace = true; + }, + .close_paren, .close_square, .close_curly => { + try token.toCss(W, dest); + has_whitespace = try this.writeWhitespaceIfNeeded(i, W, dest); + }, + .dimension => { + try css.serializer.serializeDimension(token.dimension.value, token.dimension.unit, W, dest); + has_whitespace = false; + }, + .number => { + try css.css_values.number.CSSNumberFns.toCss(W, dest); + has_whitespace = false; + }, + else => { + try token.toCss(W, dest); + has_whitespace = token == .whitespace; + }, + }, + } + } + } + + pub fn writeWhitespaceIfNeeded( + this: *const This, + i: usize, + comptime W: type, + dest: *Printer(W), + ) PrintErr!bool { + _ = this; // autofix + _ = i; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn parse(input: *css.Parser, options: *css.ParserOptions, depth: usize) Error!TokenList { + var tokens = ArrayList(TokenOrValue){}; + try TokenListFns.parseInto(input, &tokens, options, depth); + + // Slice off leading and trailing whitespace if there are at least two tokens. + // If there is only one token, we must preserve it. e.g. `--foo: ;` is valid. + // TODO(zack): this feels like a common codepath, idk how I feel about reallocating a new array just to slice off whitespace. + if (tokens.items.len >= 2) { + var slice = tokens.items[0..]; + if (tokens.items.len > 0 and tokens.items[0].isWhitespace()) { + slice = slice[1..]; + } + if (tokens.items.len > 0 and tokens.items[tokens.items.len - 1].isWhitespace()) { + slice = slice[0 .. slice.len - 1]; + } + var newlist = ArrayList(TokenOrValue){}; + newlist.insertSlice(@compileError(css.todo_stuff.think_about_allocator), 0, slice) catch unreachable; + tokens.deinit(@compileError(css.todo_stuff.think_about_allocator)); + return newlist; + } + + return .{ .v = tokens }; + } + + pub fn parseRaw( + input: *css.Parser, + tokens: *ArrayList(TokenOrValue), + options: *const css.ParserOptions, + depth: usize, + ) Error!void { + if (depth > 500) { + // return input.newCustomError(ParseError.maximum_nesting_depth); + @compileError(css.todo_stuff.errors); + } + + while (true) { + const state = input.state(); + _ = state; // autofix + const token = input.nextIncludingWhitespace() catch break; + switch (token.*) { + .open_paren, .open_square, .open_curly => { + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .token = token.* }, + ) catch unreachable; + const closing_delimiter = switch (token.*) { + .open_paren => .close_paren, + .open_square => .close_square, + .open_curly => .close_curly, + else => unreachable, + }; + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + tokens: *ArrayList(TokenOrValue), + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!void { + return TokenListFns.parseRaw( + input2, + this.tokens, + this.options, + this.depth + 1, + ); + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + .tokens = tokens, + }; + try input.parseNestedBlock(void, &closure, closure.parsefn); + tokens.append( + @compileError(css.todo_stuff.thinknk_about_allocator), + .{ .token = closing_delimiter }, + ) catch unreachable; + }, + .function => { + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .token = token.* }, + ) catch unreachable; + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + tokens: *ArrayList(TokenOrValue), + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!void { + return TokenListFns.parseRaw( + input2, + this.tokens, + this.options, + this.depth + 1, + ); + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + .tokens = tokens, + }; + try input.parseNestedBlock(void, &closure, closure.parsefn); + tokens.append( + @compileError(css.todo_stuff.thinknk_about_allocator), + .{ .token = .close_paren }, + ) catch unreachable; + }, + else => { + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .token = token.* }, + ) catch unreachable; + }, + } + } + } + + pub fn parseInto( + input: *css.Parser, + tokens: *ArrayList(TokenOrValue), + options: *const css.ParserOptions, + depth: usize, + ) Error!void { + if (depth > 500) { + // return input.newCustomError(ParseError.maximum_nesting_depth); + @compileError(css.todo_stuff.errors); + } + + var last_is_delim = false; + var last_is_whitespace = false; + + while (true) { + const state = input.state(); + const tok = input.nextIncludingWhitespace() catch break; + switch (tok.*) { + .whitespace, .comment => { + // Skip whitespace if the last token was a delimiter. + // Otherwise, replace all whitespace and comments with a single space character. + if (!last_is_delim) { + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .token = .{ .whitespace = " " } }, + ) catch unreachable; + last_is_whitespace = true; + } + }, + .function => |f| { + // Attempt to parse embedded color values into hex tokens. + if (tryParseColorToken(f, &state, input)) |color| { + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .color = color }, + ) catch unreachable; + last_is_delim = false; + last_is_whitespace = true; + } else if (input.tryParse(UnresolvedColor.parse, .{ f, options })) |color| { + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .unresolved_color = color }, + ) catch unreachable; + last_is_delim = false; + last_is_whitespace = true; + } else if (bun.strings.eql(f, "url")) { + input.reset(&state); + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .url = try Url.parse(input) }, + ) catch unreachable; + last_is_delim = false; + last_is_whitespace = false; + } else if (bun.strings.eql(f, "var")) { + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + tokens: *ArrayList(TokenOrValue), + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!TokenList { + const thevar = try TokenListFns.parse(input2, this.options, this.depth + 1); + return TokenOrValue{ .@"var" = thevar }; + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + .tokens = tokens, + }; + const @"var" = try input.parseNestedBlock(TokenOrValue, &closure, Closure.parsefn); + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + @"var", + ) catch unreachable; + last_is_delim = true; + last_is_whitespace = false; + } else if (bun.strings.eql(f, "env")) { + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!EnvironmentVariable { + const env = try EnvironmentVariable.parseNested(input2, this.options, depth + 1); + return TokenOrValue{ .env = env }; + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + }; + const env = try input.parseNestedBlock(TokenOrValue, &closure, Closure.parsefn); + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + env, + ) catch unreachable; + last_is_delim = true; + last_is_whitespace = false; + } else { + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!Function { + const args = try TokenListFns.parse(input2, this.options, this.depth + 1); + return args; + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + }; + const arguments = try input.parseNestedBlock(TokenList, &closure, Closure.parsefn); + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ + .function = .{ + .name = f, + .arguments = arguments, + }, + }, + ) catch unreachable; + last_is_delim = true; // Whitespace is not required after any of these chars. + last_is_whitespace = false; + } + }, + .hash, .idhash => { + const h = switch (tok.*) { + .hash => |h| h, + .idhash => |h| h, + else => unreachable, + }; + brk: { + const r, const g, const b, const a = css.color.parseHashColor(h) orelse { + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .token = .{ .hash = h } }, + ) catch unreachable; + break :brk; + }; + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ + .color = CssColor{ .rgba = RGBA.new(r, g, b, a) }, + }, + ) catch unreachable; + } + last_is_delim = false; + last_is_whitespace = false; + }, + .unquoted_url => { + input.reset(&state); + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .url = try Url.parse(input) }, + ) catch unreachable; + last_is_delim = false; + last_is_whitespace = false; + }, + .ident => |name| { + if (bun.strings.startsWith(name, "--")) { + tokens.append(@compileError(css.todo_stuff.think_about_allocator), .{ .dashed_ident = name }) catch unreachable; + last_is_delim = false; + last_is_whitespace = false; + } + }, + .open_paren, .open_square, .open_curly => { + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .token = tok.* }, + ) catch unreachable; + const closing_delimiter = switch (tok.*) { + .open_paren => .close_paren, + .open_square => .close_square, + .open_curly => .close_curly, + else => unreachable, + }; + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + tokens: *ArrayList(TokenOrValue), + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!void { + return TokenListFns.parseInto( + input2, + this.tokens, + this.options, + this.depth + 1, + ); + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + .tokens = tokens, + }; + try input.parseNestedBlock(void, &closure, closure.parsefn); + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .token = closing_delimiter }, + ) catch unreachable; + last_is_delim = true; // Whitespace is not required after any of these chars. + last_is_whitespace = false; + }, + .dimension => { + const value = if (LengthValue.tryFromToken(tok)) |length| + TokenOrValue{ .length = length } + else if (Angle.tryFromToken(tok)) |angle| + TokenOrValue{ .angle = angle } + else if (Time.tryFromToken(tok)) |time| + TokenOrValue{ .time = time } + else if (Resolution.tryFromToken(tok)) |resolution| + TokenOrValue{ .resolution = resolution } + else + TokenOrValue{ .token = tok.* }; + + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + value, + ) catch unreachable; + + last_is_delim = false; + last_is_whitespace = false; + }, + else => {}, + } + + if (tok.isParseError()) { + @compileError(css.todo_stuff.errors); + } + last_is_delim = switch (tok.*) { + .delim, .comma => true, + else => false, + }; + + // If this is a delimiter, and the last token was whitespace, + // replace the whitespace with the delimiter since both are not required. + if (last_is_delim and last_is_whitespace) { + const last = &tokens.items[tokens.items.len - 1]; + last.* = .{ .token = tok.* }; + } else { + tokens.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .token = tok.* }, + ) catch unreachable; + } + + last_is_whitespace = false; + } + } +}; +pub const TokenListFns = TokenList; + +/// A color value with an unresolved alpha value (e.g. a variable). +/// These can be converted from the modern slash syntax to older comma syntax. +/// This can only be done when the only unresolved component is the alpha +/// since variables can resolve to multiple tokens. +pub const UnresolvedColor = union(enum) { + /// An rgb() color. + RGB: struct { + /// The red component. + r: f32, + /// The green component. + g: f32, + /// The blue component. + b: f32, + /// The unresolved alpha component. + alpha: TokenList, + }, + /// An hsl() color. + HSL: struct { + /// The hue component. + h: f32, + /// The saturation component. + s: f32, + /// The lightness component. + l: f32, + /// The unresolved alpha component. + alpha: TokenList, + }, + /// The light-dark() function. + light_dark: struct { + /// The light value. + light: TokenList, + /// The dark value. + dark: TokenList, + }, + const This = @This(); + + pub fn toCss( + this: *const This, + comptime W: type, + dest: *Printer(W), + is_custom_property: bool, + ) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + _ = is_custom_property; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn parse( + input: *css.Parser, + f: []const u8, + options: *const css.ParserOptions, + ) Error!UnresolvedColor { + var parser = ComponentParser.new(false); + // css.todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "rgb")) { + const Closure = struct { + options: *const css.ParserOptions, + parser: *ComponentParser, + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!UnresolvedColor { + return this.parser.parseRelative(input2, SRGB, UnresolvedColor, @This().innerParseFn, .{this.options}); + } + pub fn innerParseFn(i: *css.Parser, p: *ComponentParser, opts: *const css.ParserOptions) Error!UnresolvedColor { + const r, const g, const b, const is_legacy = try css.css_values.color.parseRGBComponents(i, p); + if (is_legacy) { + @compileError(css.todo_stuff.errors); + } + try i.expectDelim('/'); + const alpha = try TokenListFns.parse(i, opts, 0); + return UnresolvedColor{ + .RGB = .{ + .r = r, + .g = g, + .b = b, + .alpha = alpha, + }, + }; + } + }; + var closure = Closure{ + .options = options, + .parser = &parser, + }; + return try input.parseNestedBlock(UnresolvedColor, &closure, Closure.parsefn); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "hsl")) { + const Closure = struct { + options: *const css.ParserOptions, + parser: *ComponentParser, + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!UnresolvedColor { + return this.parser.parseRelative(input2, HSL, UnresolvedColor, @This().innerParseFn, .{this.options}); + } + pub fn innerParseFn(i: *css.Parser, p: *ComponentParser, opts: *const css.ParserOptions) Error!UnresolvedColor { + const h, const s, const l, const is_legacy = try css.css_values.color.parseHSLHWBComponents(HSL, i, p, false); + if (is_legacy) { + @compileError(css.todo_stuff.errors); + } + try i.expectDelim('/'); + const alpha = try TokenListFns.parse(i, opts, 0); + return UnresolvedColor{ + .HSL = .{ + .h = h, + .s = s, + .l = l, + .alpha = alpha, + }, + }; + } + }; + var closure = Closure{ + .options = options, + .parser = &parser, + }; + return try input.parseNestedBlock(UnresolvedColor, &closure, Closure.parsefn); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "light-dark")) { + const Closure = struct { + options: *const css.ParserOptions, + parser: *ComponentParser, + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!UnresolvedColor { + const light = try input2.parseUntilBefore(css.Delimiters{ .comma = true }, TokenList, this, @This().parsefn2); + errdefer light.deinit(); + try input2.expectComma(); + const dark = try TokenListFns.parse(input2, this.options, 0); + errdefer dark.deinit(); + return UnresolvedColor{ + .light_dark = .{ + .light = light, + .dark = dark, + }, + }; + } + + pub fn parsefn2(this: *@This(), input2: *css.Parser) Error!TokenList { + return TokenListFns.parse(input2, this.options, 1); + } + }; + var closure = Closure{ + .options = options, + .parser = &parser, + }; + return try input.parseNestedBlock(UnresolvedColor, &closure, Closure.parsefn); + } else { + // return input.newCustomError(); + @compileError(css.todo_stuff.errors); + } + } +}; + +/// A CSS variable reference. +pub const Variable = struct { + /// The variable name. + name: DashedIdentReference, + /// A fallback value in case the variable is not defined. + fallback: ?TokenList, + + const This = @This(); + + pub fn toCss( + this: *const This, + comptime W: type, + dest: *Printer(W), + ) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } +}; + +/// A CSS environment variable reference. +pub const EnvironmentVariable = struct { + /// The environment variable name. + name: EnvironmentVariableName, + /// Optional indices into the dimensions of the environment variable. + indices: ArrayList(CSSInteger) = ArrayList(CSSInteger).init(), + /// A fallback value in case the variable is not defined. + fallback: ?TokenList, + + pub fn parse(input: *css.Parser, options: *const css.ParserOptions, depth: usize) Error!EnvironmentVariable { + try input.expectFunctionMatching("env"); + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + pub fn parsefn(this: *@This(), i: *css.Parser) Error!EnvironmentVariableName { + return EnvironmentVariable.parseNested(i, this.options, this.depth); + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + }; + return input.parseNestedBlock(EnvironmentVariable, &closure, Closure.parsefn); + } + + pub fn parseNested(input: *css.Parser, options: *const css.ParserOptions, depth: usize) Error!EnvironmentVariable { + const name = try EnvironmentVariableName.parse(); + var indices = ArrayList(i32){}; + errdefer indices.deinit(@compileError(css.todo_stuff.think_about_allocator)); + while (input.tryParse(CSSIntegerFns.parse, .{}) catch null) |idx| { + indices.append( + @compileError(css.todo_stuff.think_about_allocator), + idx, + ) catch unreachable; + } + + const fallback = if (input.tryParse(css.Parser.expectComma, .{})) |_| try TokenListFns.parse(input, options, depth + 1) else null; + + return EnvironmentVariable{ + .name = name, + .indices = indices, + .fallback = fallback, + }; + } + + pub fn toCss( + this: *const EnvironmentVariable, + comptime W: type, + dest: *Printer(W), + is_custom_property: bool, + ) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + _ = is_custom_property; // autofix + @compileError(css.todo_stuff.depth); + } +}; + +/// A CSS environment variable name. +pub const EnvironmentVariableName = union(enum) { + /// A UA-defined environment variable. + ua: UAEnvironmentVariable, + /// A custom author-defined environment variable. + custom: DashedIdentReference, + /// An unknown environment variable. + unknown: CustomIdent, + + pub fn parse(input: *css.Parser) Error!EnvironmentVariableName { + if (input.tryParse(UAEnvironmentVariable.parse, .{})) |ua| { + return .{ .ua = ua }; + } + + if (input.tryParse(DashedIdentReference.parseWithOptions, .{ + css.ParserOptions.default( + @compileError(css.todo_stuff.think_about_allocator), + ), + })) |dashed| { + return .{ .custom = dashed }; + } + + const ident = try CustomIdentFns.parse(input); + return .{ .unknown = ident }; + } +}; + +/// A UA-defined environment variable name. +pub const UAEnvironmentVariable = enum { + /// The safe area inset from the top of the viewport. + safe_area_inset_top, + /// The safe area inset from the right of the viewport. + safe_area_inset_right, + /// The safe area inset from the bottom of the viewport. + safe_area_inset_bottom, + /// The safe area inset from the left of the viewport. + safe_area_inset_left, + /// The viewport segment width. + viewport_segment_width, + /// The viewport segment height. + viewport_segment_height, + /// The viewport segment top position. + viewport_segment_top, + /// The viewport segment left position. + viewport_segment_left, + /// The viewport segment bottom position. + viewport_segment_bottom, + /// The viewport segment right position. + viewport_segment_right, + + pub fn parse(input: *css.Parser) Error!UAEnvironmentVariable { + return css.comptime_parse(UAEnvironmentVariable, input); + } +}; + +/// A custom CSS function. +pub const Function = struct { + /// The function name. + name: Ident, + /// The function arguments. + arguments: TokenList, + + const This = @This(); + + pub fn toCss( + this: *const This, + comptime W: type, + dest: *Printer(W), + ) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } +}; + +/// A raw CSS token, or a parsed value. +pub const TokenOrValue = union(enum) { + /// A token. + token: css.Token, + /// A parsed CSS color. + color: CssColor, + /// A color with unresolved components. + unresolved_color: UnresolvedColor, + /// A parsed CSS url. + url: Url, + /// A CSS variable reference. + @"var": Variable, + /// A CSS environment variable reference. + env: EnvironmentVariable, + /// A custom CSS function. + function: Function, + /// A length. + length: LengthValue, + /// An angle. + angle: Angle, + /// A time. + time: Time, + /// A resolution. + resolution: Resolution, + /// A dashed ident. + dashed_ident: DashedIdent, + /// An animation name. + animation_name: AnimationName, + + pub fn isWhitespace(self: *const TokenOrValue) bool { + switch (self.*) { + .token => |tok| return tok == .whitespace, + else => return false, + } + } +}; + +/// A CSS custom property, representing any unknown property. +pub const CustomProperty = struct { + /// The name of the property. + name: CustomPropertyName, + /// The property value, stored as a raw token list. + value: TokenList, + + pub fn parse(name: CustomPropertyName, input: *css.Parser, options: *const css.ParserOptions) Error!CustomProperty { + const Closure = struct { + options: *const css.ParserOptions, + + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!TokenList { + return TokenListFns.parse(input2, this.options, 0); + } + }; + + var closure = Closure{ + .options = options, + }; + + const value = try input.parseUntilBefore( + css.Delimiters{ + .bang = true, + .semicolon = true, + }, + TokenList, + &closure, + Closure.parsefn, + ); + + return CustomProperty{ + .name = name, + .value = value, + }; + } +}; + +/// A CSS custom property name. +pub const CustomPropertyName = union(enum) { + /// An author-defined CSS custom property. + custom: DashedIdent, + /// An unknown CSS property. + unknown: Ident, + + pub fn fromStr(name: []const u8) CustomPropertyName { + if (bun.strings.startsWith(name, "--")) return .{ .custom = name }; + return .{ .unknown = name }; + } +}; + +pub fn tryParseColorToken(f: []const u8, state: *const css.ParserState, input: *css.Parser) ?CssColor { + // css.todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "rgb") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "rgba") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "hsl") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "hsla") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "hwb") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "lab") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "lch") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "oklab") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "oklch") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "color") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "color-mix") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "light-dark")) + { + const s = input.state(); + input.reset(&state); + if (CssColor.parse(input)) |color| { + return color; + } + input.reset(&s); + } + + return null; +} diff --git a/src/css/properties/properties.zig b/src/css/properties/properties.zig new file mode 100644 index 0000000000000..ebb919c6154e4 --- /dev/null +++ b/src/css/properties/properties.zig @@ -0,0 +1,3643 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; + +pub const css = @import("../css_parser.zig"); + +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const css_values = css.css_values; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const Length = css.css_values.length.LengthValue; +const LengthPercentage = css_values.length.LengthPercentage; +const LengthPercentageOrAuto = css_values.length.LengthPercentageOrAuto; +const PropertyCategory = css.PropertyCategory; +const LogicalGroup = css.LogicalGroup; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSInteger = css.css_values.number.CSSInteger; +const NumberOrPercentage = css.css_values.percentage.NumberOrPercentage; +const Percentage = css.css_values.percentage.Percentage; +const Angle = css.css_values.angle.Angle; +const DashedIdentReference = css.css_values.ident.DashedIdentReference; +const Time = css.css_values.time.Time; +const EasingFunction = css.css_values.easing.EasingFunction; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const DashedIdent = css.css_values.ident.DashedIdent; +const Url = css.css_values.url.Url; +const CustomIdentList = css.css_values.ident.CustomIdentList; +const Location = css.Location; +const HorizontalPosition = css.css_values.position.HorizontalPosition; +const VerticalPosition = css.css_values.position.VerticalPosition; +const ContainerName = css.css_rules.container.ContainerName; + +const BorderSideWidth = border.BorderSideWith; +const Size2D = css_values.size.Size2D; +const BorderRadius = border_radius.BorderRadius; +const Rect = css_values.rect.Rect; +const LengthOrNumber = css_values.length.LengthOrNumber; +const BorderImageRepeat = border_image.BorderImageRepeat; +const BorderImageSideWidth = border_image.BorderImageSideWidth; +const BorderImageSlice = border_image.BorderImageSlice; +const BorderImage = border_image.BorderImage; +const BorderColor = border.BorderColor; +const BorderStyle = border.BorderStyle; +const BorderWidth = border.BorderWidth; +const BorderBlockColor = border.BorderBlockColor; +const BorderBlockStyle = border.BorderBlockStyle; +const BorderBlockWidth = border.BorderBlockWidth; +const BorderInlineColor = border.BorderInlineColor; +const BorderInlineStyle = border.BorderInlineStyle; +const BorderInlineWidth = border.BorderInlineWidth; +const Border = border.Border; +const BorderTop = border.BorderTop; +const BorderRight = border.BorderRight; +const BorderLeft = border.BorderLeft; +const BorderBottom = border.BorderBottom; +const BorderBlockStart = border.BorderBlockStart; +const BorderBlockEnd = border.BorderBlockEnd; +const BorderInlineStart = border.BorderInlineStart; +const BorderInlineEnd = border.BorderInlineEnd; +const BorderBlock = border.BorderBlock; +const BorderInline = border.BorderInline; +const Outline = outline.Outline; +const OutlineStyle = outline.OutlineStyle; +const FlexDirection = flex.FlexDirection; +const FlexWrap = flex.FlexWrap; +const FlexFlow = flex.FlexFlow; +const Flex = flex.Flex; +const BoxOrient = flex.BoxOrient; +const BoxDirection = flex.BoxDirection; +const BoxAlign = flex.BoxAlign; +const BoxPack = flex.BoxPack; +const BoxLines = flex.BoxLines; +const FlexPack = flex.FlexPack; +const FlexItemAlign = flex.FlexItemAlign; +const FlexLinePack = flex.FlexLinePack; +const AlignContent = @"align".AlignContent; +const JustifyContent = @"align".JustifyContent; +const PlaceContent = @"align".PlaceContent; +const AlignSelf = @"align".AlignSelf; +const JustifySelf = @"align".JustifySelf; +const PlaceSelf = @"align".PlaceSelf; +const AlignItems = @"align".AlignItems; +const JustifyItems = @"align".JustifyItems; +const PlaceItems = @"align".PlaceItems; +const GapValue = @"align".GapValue; +const Gap = @"align".Gap; +const MarginBlock = margin_padding.MarginBlock; +const Margin = margin_padding.Margin; +const MarginInline = margin_padding.MarginInline; +const PaddingBlock = margin_padding.PaddingBlock; +const PaddingInline = margin_padding.PaddingInline; +const Padding = margin_padding.Padding; +const ScrollMarginBlock = margin_padding.ScrollMarginBlock; +const ScrollMarginInline = margin_padding.ScrollMarginInline; +const ScrollMargin = margin_padding.ScrollMargin; +const ScrollPaddingBlock = margin_padding.ScrollPaddingBlock; +const ScrollPaddingInline = margin_padding.ScrollPaddingInline; +const ScrollPadding = margin_padding.ScrollPadding; +const FontWeight = font.FontWeight; +const FontSize = font.FontSize; +const FontStretch = font.FontStretch; +const FontFamily = font.FontFamily; +const FontStyle = font.FontStyle; +const FontVariantCaps = font.FontVariantCaps; +const LineHeight = font.LineHeight; +const Font = font.Font; +const VerticalAlign = font.VerticalAlign; +const Transition = transition.Transition; +const AnimationNameList = animation.AnimationNameList; +const AnimationList = animation.AnimationList; +const AnimationIterationCount = animation.AnimationIterationCount; +const AnimationDirection = animation.AnimationDirection; +const AnimationPlayState = animation.AnimationPlayState; +const AnimationFillMode = animation.AnimationFillMode; +const AnimationComposition = animation.AnimationComposition; +const AnimationTimeline = animation.AnimationTimeline; +const AnimationRangeStart = animation.AnimationRangeStart; +const AnimationRangeEnd = animation.AnimationRangeEnd; +const AnimationRange = animation.AnimationRange; +const TransformList = transform.TransformList; +const TransformStyle = transform.TransformStyle; +const TransformBox = transform.TransformBox; +const BackfaceVisibility = transform.BackfaceVisibility; +const Perspective = transform.Perspective; +const Translate = transform.Translate; +const Rotate = transform.Rotate; +const Scale = transform.Scale; +const TextTransform = text.TextTransform; +const WhiteSpace = text.WhiteSpace; +const WordBreak = text.WordBreak; +const LineBreak = text.LineBreak; +const Hyphens = text.Hyphens; +const OverflowWrap = text.OverflowWrap; +const TextAlign = text.TextAlign; +const TextIndent = text.TextIndent; +const Spacing = text.Spacing; +const TextJustify = text.TextJustify; +const TextAlignLast = text.TextAlignLast; +const TextDecorationLine = text.TextDecorationLine; +const TextDecorationStyle = text.TextDecorationStyle; +const TextDecorationThickness = text.TextDecorationThickness; +const TextDecoration = text.TextDecoration; +const TextDecorationSkipInk = text.TextDecorationSkipInk; +const TextEmphasisStyle = text.TextEmphasisStyle; +const TextEmphasis = text.TextEmphasis; +const TextEmphasisPositionVertical = text.TextEmphasisPositionVertical; +const TextEmphasisPositionHorizontal = text.TextEmphasisPositionHorizontal; +const TextEmphasisPosition = text.TextEmphasisPosition; +const TextShadow = text.TextShadow; +const TextSizeAdjust = text.TextSizeAdjust; +const Direction = text.Direction; +const UnicodeBidi = text.UnicodeBidi; +const BoxDecorationBreak = text.BoxDecorationBreak; +const Resize = ui.Resize; +const Cursor = ui.Cursor; +const ColorOrAuto = ui.ColorOrAuto; +const CaretShape = ui.CaretShape; +const Caret = ui.Caret; +const UserSelect = ui.UserSelect; +const Appearance = ui.Appearance; +const ColorScheme = ui.ColorScheme; +const ListStyleType = list.ListStyleType; +const ListStylePosition = list.ListStylePosition; +const ListStyle = list.ListStyle; +const MarkerSide = list.MarkerSide; +const Composes = css_modules.Composes; +const SVGPaint = svg.SVGPaint; +const FillRule = shape.FillRule; +const AlphaValue = shape.AlphaValue; +const StrokeLinecap = svg.StrokeLinecap; +const StrokeLinejoin = svg.StrokeLinejoin; +const StrokeDasharray = svg.StrokeDasharray; +const Marker = svg.Marker; +const ColorInterpolation = svg.ColorInterpolation; +const ColorRendering = svg.ColorRendering; +const ShapeRendering = svg.ShapeRendering; +const TextRendering = svg.TextRendering; +const ImageRendering = svg.ImageRendering; +const ClipPath = masking.ClipPath; +const MaskMode = masking.MaskMode; +const MaskClip = masking.MaskClip; +const GeometryBox = masking.GeometryBox; +const MaskComposite = masking.MaskComposite; +const MaskType = masking.MaskType; +const Mask = masking.Mask; +const MaskBorderMode = masking.MaskBorderMode; +const MaskBorder = masking.MaskBorder; +const WebKitMaskComposite = masking.WebKitMaskComposite; +const WebKitMaskSourceType = masking.WebKitMaskSourceType; +const BackgroundRepeat = background.BackgroundRepeat; +const BackgroundSize = background.BackgroundSize; +const FilterList = effects.FilterList; +const ContainerType = contain.ContainerType; +const Container = contain.Container; +const ContainerNameList = contain.ContainerNameList; +const CustomPropertyName = custom.CustomPropertyName; + +const Position = position.Position; + +const Error = css.Error; + +const ArrayList = std.ArrayListUnmanaged; +const SmallList = css.SmallList; + +pub const custom = struct { + pub usingnamespace @import("./custom.zig"); +}; + +pub const @"align" = struct { + /// A value for the [align-content](https://www.w3.org/TR/css-align-3/#propdef-align-content) property. + pub const AlignContent = union(enum) { + /// Default alignment. + normal: void, + /// A baseline position. + baseline_position: BaselinePosition, + /// A content distribution keyword. + content_distribution: ContentDistribution, + /// A content position keyword. + content_position: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + /// A content position keyword. + value: ContentPosition, + }, + }; + + /// A [``](https://www.w3.org/TR/css-align-3/#typedef-baseline-position) value, + /// as used in the alignment properties. + pub const BaselinePosition = enum { + /// The first baseline. + first, + /// The last baseline. + last, + }; + + /// A value for the [justify-content](https://www.w3.org/TR/css-align-3/#propdef-justify-content) property. + pub const JustifyContent = union(enum) { + /// Default justification. + normal, + /// A content distribution keyword. + content_distribution: ContentDistribution, + /// A content position keyword. + content_position: struct { + /// A content position keyword. + value: ContentPosition, + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Justify to the left. + left: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Justify to the right. + right: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + }; + + /// A value for the [align-self](https://www.w3.org/TR/css-align-3/#align-self-property) property. + pub const AlignSelf = union(enum) { + /// Automatic alignment. + auto, + /// Default alignment. + normal, + /// Item is stretched. + stretch, + /// A baseline position keyword. + baseline_position: BaselinePosition, + /// A self position keyword. + self_position: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + /// A self position keyword. + value: SelfPosition, + }, + }; + + /// A value for the [justify-self](https://www.w3.org/TR/css-align-3/#justify-self-property) property. + pub const JustifySelf = union(enum) { + /// Automatic justification. + auto, + /// Default justification. + normal, + /// Item is stretched. + stretch, + /// A baseline position keyword. + baseline_position: BaselinePosition, + /// A self position keyword. + self_position: struct { + /// A self position keyword. + value: SelfPosition, + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Item is justified to the left. + left: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Item is justified to the right. + right: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + }; + + /// A value for the [align-items](https://www.w3.org/TR/css-align-3/#align-items-property) property. + pub const AlignItems = union(enum) { + /// Default alignment. + normal, + /// Items are stretched. + stretch, + /// A baseline position keyword. + baseline_position: BaselinePosition, + /// A self position keyword. + self_position: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + /// A self position keyword. + value: SelfPosition, + }, + }; + + /// A value for the [justify-items](https://www.w3.org/TR/css-align-3/#justify-items-property) property. + pub const JustifyItems = union(enum) { + /// Default justification. + normal, + /// Items are stretched. + stretch, + /// A baseline position keyword. + baseline_position: BaselinePosition, + /// A self position keyword, with optional overflow position. + self_position: struct { + /// A self position keyword. + value: SelfPosition, + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Items are justified to the left, with an optional overflow position. + left: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Items are justified to the right, with an optional overflow position. + right: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// A legacy justification keyword. + legacy: LegacyJustify, + }; + + /// A legacy justification keyword, as used in the `justify-items` property. + pub const LegacyJustify = enum { + /// Left justify. + left, + /// Right justify. + right, + /// Centered. + center, + }; + + /// A [gap](https://www.w3.org/TR/css-align-3/#column-row-gap) value, as used in the + /// `column-gap` and `row-gap` properties. + pub const GapValue = union(enum) { + /// Equal to `1em` for multi-column containers, and zero otherwise. + normal, + /// An explicit length. + length_percentage: LengthPercentage, + }; + + /// A value for the [gap](https://www.w3.org/TR/css-align-3/#gap-shorthand) shorthand property. + pub const Gap = @compileError(css.todo_stuff.depth); + + /// A value for the [place-items](https://www.w3.org/TR/css-align-3/#place-items-property) shorthand property. + pub const PlaceItems = @compileError(css.todo_stuff.depth); + + /// A value for the [place-self](https://www.w3.org/TR/css-align-3/#place-self-property) shorthand property. + pub const PlaceSelf = @compileError(css.todo_stuff.depth); + + /// A [``](https://www.w3.org/TR/css-align-3/#typedef-self-position) value. + pub const SelfPosition = @compileError(css.todo_stuff.depth); + + /// A value for the [place-content](https://www.w3.org/TR/css-align-3/#place-content) shorthand property. + pub const PlaceContent = @compileError(css.todo_stuff.depth); + + /// A [``](https://www.w3.org/TR/css-align-3/#typedef-content-distribution) value. + pub const ContentDistribution = css.DefineEnumProperty(@compileError(css.todo_stuff.errors)); + + /// An [``](https://www.w3.org/TR/css-align-3/#typedef-overflow-position) value. + pub const OverflowPosition = css.DefineEnumProperty(@compileError(css.todo_stuff.errors)); + + /// A [``](https://www.w3.org/TR/css-align-3/#typedef-content-position) value. + pub const ContentPosition = css.DefineEnumProperty(@compileError(css.todo_stuff.errors)); +}; + +pub const animation = struct { + /// A list of animations. + pub const AnimationList = SmallList(Animation, 1); + + /// A list of animation names. + pub const AnimationNameList = SmallList(AnimationName, 1); + + /// A value for the [animation](https://drafts.csswg.org/css-animations/#animation) shorthand property. + pub const Animation = @compileError(css.todo_stuff.depth); + + /// A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property. + pub const AnimationName = union(enum) { + /// The `none` keyword. + none, + /// An identifier of a `@keyframes` rule. + ident: CustomIdent, + /// A `` name of a `@keyframes` rule. + string: CSSString, + + // ~toCssImpl + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + /// A value for the [animation-iteration-count](https://drafts.csswg.org/css-animations/#animation-iteration-count) property. + pub const AnimationIterationCount = union(enum) { + /// The animation will repeat the specified number of times. + number: CSSNumber, + /// The animation will repeat forever. + infinite, + }; + + /// A value for the [animation-direction](https://drafts.csswg.org/css-animations/#animation-direction) property. + pub const AnimationDirection = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [animation-play-state](https://drafts.csswg.org/css-animations/#animation-play-state) property. + pub const AnimationPlayState = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [animation-fill-mode](https://drafts.csswg.org/css-animations/#animation-fill-mode) property. + pub const AnimationFillMode = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [animation-composition](https://drafts.csswg.org/css-animations-2/#animation-composition) property. + pub const AnimationComposition = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [animation-timeline](https://drafts.csswg.org/css-animations-2/#animation-timeline) property. + pub const AnimationTimeline = union(enum) { + /// The animation's timeline is a DocumentTimeline, more specifically the default document timeline. + auto, + /// The animation is not associated with a timeline. + none, + /// A timeline referenced by name. + dashed_ident: DashedIdent, + /// The scroll() function. + scroll: ScrollTimeline, + /// The view() function. + view: ViewTimeline, + }; + + /// The [scroll()](https://drafts.csswg.org/scroll-animations-1/#scroll-notation) function. + pub const ScrollTimeline = struct { + /// Specifies which element to use as the scroll container. + scroller: Scroller, + /// Specifies which axis of the scroll container to use as the progress for the timeline. + axis: ScrollAxis, + }; + + /// The [view()](https://drafts.csswg.org/scroll-animations-1/#view-notation) function. + pub const ViewTimeline = struct { + /// Specifies which axis of the scroll container to use as the progress for the timeline. + axis: ScrollAxis, + /// Provides an adjustment of the view progress visibility range. + inset: Size2D(LengthPercentageOrAuto), + }; + + /// A scroller, used in the `scroll()` function. + pub const Scroller = @compileError(css.todo_stuff.depth); + + /// A scroll axis, used in the `scroll()` function. + pub const ScrollAxis = @compileError(css.todo_stuff.depth); + + /// A value for the animation-range shorthand property. + pub const AnimationRange = struct { + /// The start of the animation's attachment range. + start: animation.AnimationRangeStart, + /// The end of the animation's attachment range. + end: animation.AnimationRangeEnd, + }; + + /// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) property. + pub const AnimationRangeStart = struct { + v: AnimationAttachmentRange, + }; + + /// A value for the [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-start) property. + pub const AnimationRangeEnd = struct { + v: AnimationAttachmentRange, + }; + + /// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) + /// or [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property. + pub const AnimationAttachmentRange = union(enum) { + /// The start of the animation's attachment range is the start of its associated timeline. + normal, + /// The animation attachment range starts at the specified point on the timeline measuring from the start of the timeline. + length_percentage: LengthPercentage, + /// The animation attachment range starts at the specified point on the timeline measuring from the start of the specified named timeline range. + timeline_range: struct { + /// The name of the timeline range. + name: TimelineRangeName, + /// The offset from the start of the named timeline range. + offset: LengthPercentage, + }, + }; + + /// A [view progress timeline range](https://drafts.csswg.org/scroll-animations/#view-timelines-ranges) + pub const TimelineRangeName = enum { + /// Represents the full range of the view progress timeline. + cover, + /// Represents the range during which the principal box is either fully contained by, + /// or fully covers, its view progress visibility range within the scrollport. + contain, + /// Represents the range during which the principal box is entering the view progress visibility range. + entry, + /// Represents the range during which the principal box is exiting the view progress visibility range. + exit, + /// Represents the range during which the principal box crosses the end border edge. + entry_crossing, + /// Represents the range during which the principal box crosses the start border edge. + exit_crossing, + }; +}; + +pub const background = struct { + /// A value for the [background](https://www.w3.org/TR/css-backgrounds-3/#background) shorthand property. + pub const Background = struct { + /// The background image. + image: Image, + /// The background color. + color: CssColor, + /// The background position. + position: BackgroundPosition, + /// How the background image should repeat. + repeat: background.BackgroundRepeat, + /// The size of the background image. + size: background.BackgroundSize, + /// The background attachment. + attachment: BackgroundAttachment, + /// The background origin. + origin: BackgroundOrigin, + /// How the background should be clipped. + clip: BackgroundClip, + }; + + /// A value for the [background-size](https://www.w3.org/TR/css-backgrounds-3/#background-size) property. + pub const BackgroundSize = union(enum) { + /// An explicit background size. + explicit: struct { + /// The width of the background. + width: css.css_values.length.LengthPercentage, + /// The height of the background. + height: css.css_values.length.LengthPercentageOrAuto, + }, + /// The `cover` keyword. Scales the background image to cover both the width and height of the element. + cover, + /// The `contain` keyword. Scales the background image so that it fits within the element. + contain, + }; + + /// A value for the [background-position](https://drafts.csswg.org/css-backgrounds/#background-position) shorthand property. + pub const BackgroundPosition = css.DefineListShorthand(struct { + comptime { + @compileError(css.todo_stuff.depth); + } + }); + + /// A value for the [background-repeat](https://www.w3.org/TR/css-backgrounds-3/#background-repeat) property. + pub const BackgroundRepeat = struct { + /// A repeat style for the x direction. + x: BackgroundRepeatKeyword, + /// A repeat style for the y direction. + y: BackgroundRepeatKeyword, + }; + + /// A [``](https://www.w3.org/TR/css-backgrounds-3/#typedef-repeat-style) value, + /// used within the `background-repeat` property to represent how a background image is repeated + /// in a single direction. + /// + /// See [BackgroundRepeat](BackgroundRepeat). + pub const BackgroundRepeatKeyword = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [background-attachment](https://www.w3.org/TR/css-backgrounds-3/#background-attachment) property. + pub const BackgroundAttachment = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [background-origin](https://www.w3.org/TR/css-backgrounds-3/#background-origin) property. + pub const BackgroundOrigin = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [background-clip](https://drafts.csswg.org/css-backgrounds-4/#background-clip) property. + pub const BackgroundClip = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + pub const BoxSizing = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [aspect-ratio](https://drafts.csswg.org/css-sizing-4/#aspect-ratio) property. + pub const AspectRatio = struct { + /// The `auto` keyword. + auto: bool, + /// A preferred aspect ratio for the box, specified as width / height. + ratio: ?css_values.ratio.Ratio, + }; +}; + +pub const border = struct { + /// A value for the [border-top](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-top) shorthand property. + pub const BorderTop = GenericBorder(LineStyle, 0); + /// A value for the [border-right](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-right) shorthand property. + pub const BorderRight = GenericBorder(LineStyle, 1); + /// A value for the [border-bottom](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-bottom) shorthand property. + pub const BorderBottom = GenericBorder(LineStyle, 2); + /// A value for the [border-left](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-left) shorthand property. + pub const BorderLeft = GenericBorder(LineStyle, 3); + /// A value for the [border-block-start](https://drafts.csswg.org/css-logical/#propdef-border-block-start) shorthand property. + pub const BorderBlockStart = GenericBorder(LineStyle, 4); + /// A value for the [border-block-end](https://drafts.csswg.org/css-logical/#propdef-border-block-end) shorthand property. + pub const BorderBlockEnd = GenericBorder(LineStyle, 5); + /// A value for the [border-inline-start](https://drafts.csswg.org/css-logical/#propdef-border-inline-start) shorthand property. + pub const BorderInlineStart = GenericBorder(LineStyle, 6); + /// A value for the [border-inline-end](https://drafts.csswg.org/css-logical/#propdef-border-inline-end) shorthand property. + pub const BorderInlineEnd = GenericBorder(LineStyle, 7); + /// A value for the [border-block](https://drafts.csswg.org/css-logical/#propdef-border-block) shorthand property. + pub const BorderBlock = GenericBorder(LineStyle, 8); + /// A value for the [border-inline](https://drafts.csswg.org/css-logical/#propdef-border-inline) shorthand property. + pub const BorderInline = GenericBorder(LineStyle, 9); + /// A value for the [border](https://www.w3.org/TR/css-backgrounds-3/#propdef-border) shorthand property. + pub const Border = GenericBorder(LineStyle, 10); + + /// A generic type that represents the `border` and `outline` shorthand properties. + pub fn GenericBorder(comptime S: type, comptime P: u8) type { + _ = P; // autofix + return struct { + /// The width of the border. + width: BorderSideWidth, + /// The border style. + style: S, + /// The border color. + color: CssColor, + }; + } + /// A [``](https://drafts.csswg.org/css-backgrounds/#typedef-line-style) value, used in the `border-style` property. + pub const LineStyle = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [border-width](https://www.w3.org/TR/css-backgrounds-3/#border-width) property. + pub const BorderSideWith = union(enum) { + /// A UA defined `thin` value. + thin, + /// A UA defined `medium` value. + medium, + /// A UA defined `thick` value. + thick, + /// An explicit width. + length: Length, + }; + + /// A value for the [border-color](https://drafts.csswg.org/css-backgrounds/#propdef-border-color) shorthand property. + pub const BorderColor = @compileError(css.todo_stuff.depth); + + /// A value for the [border-style](https://drafts.csswg.org/css-backgrounds/#propdef-border-style) shorthand property. + pub const BorderStyle = @compileError(css.todo_stuff.depth); + + /// A value for the [border-width](https://drafts.csswg.org/css-backgrounds/#propdef-border-width) shorthand property. + pub const BorderWidth = @compileError(css.todo_stuff.depth); + + /// A value for the [border-block-color](https://drafts.csswg.org/css-logical/#propdef-border-block-color) shorthand property. + pub const BorderBlockColor = @compileError(css.todo_stuff.depth); + + /// A value for the [border-block-width](https://drafts.csswg.org/css-logical/#propdef-border-block-width) shorthand property. + pub const BorderBlockWidth = @compileError(css.todo_stuff.depth); + + /// A value for the [border-inline-color](https://drafts.csswg.org/css-logical/#propdef-border-inline-color) shorthand property. + pub const BorderInlineColor = @compileError(css.todo_stuff.depth); + + /// A value for the [border-inline-style](https://drafts.csswg.org/css-logical/#propdef-border-inline-style) shorthand property. + pub const BorderInlineStyle = @compileError(css.todo_stuff.depth); + + /// A value for the [border-inline-width](https://drafts.csswg.org/css-logical/#propdef-border-inline-width) shorthand property. + pub const BorderInlineWidth = @compileError(css.todo_stuff.depth); +}; + +pub const border_image = struct { + /// A value for the [border-image](https://www.w3.org/TR/css-backgrounds-3/#border-image) shorthand property. + pub const BorderImage = @compileError(css.todo_stuff.depth); + + /// A value for the [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) property. + const BorderImageRepeat = struct { + /// The horizontal repeat value. + horizontal: BorderImageRepeatKeyword, + /// The vertical repeat value. + vertical: BorderImageRepeatKeyword, + }; + + /// A value for the [border-image-width](https://www.w3.org/TR/css-backgrounds-3/#border-image-width) property. + pub const BorderImageSideWidth = union(enum) { + /// A number representing a multiple of the border width. + number: CSSNumber, + /// An explicit length or percentage. + length_percentage: LengthPercentage, + /// The `auto` keyword, representing the natural width of the image slice. + auto: void, + }; + + const BorderImageRepeatKeyword = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [border-image-slice](https://www.w3.org/TR/css-backgrounds-3/#border-image-slice) property. + const BorderImageSlice = struct { + /// The offsets from the edges of the image. + offsets: Rect(NumberOrPercentage), + /// Whether the middle of the border image should be preserved. + fill: bool, + }; +}; + +pub const border_radius = struct { + /// A value for the [border-radius](https://www.w3.org/TR/css-backgrounds-3/#border-radius) property. + pub const BorderRadius = @compileError(css.todo_stuff.depth); +}; + +pub const box_shadow = struct { + /// A value for the [box-shadow](https://drafts.csswg.org/css-backgrounds/#box-shadow) property. + pub const BoxShadow = struct { + /// The color of the box shadow. + color: CssColor, + /// The x offset of the shadow. + x_offset: Length, + /// The y offset of the shadow. + y_offset: Length, + /// The blur radius of the shadow. + blur: Length, + /// The spread distance of the shadow. + spread: Length, + /// Whether the shadow is inset within the box. + inset: bool, + }; +}; + +pub const contain = struct { + const ContainerIdent = ContainerName; + /// A value for the [container-type](https://drafts.csswg.org/css-contain-3/#container-type) property. + /// Establishes the element as a query container for the purpose of container queries. + pub const ContainerType = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [container-name](https://drafts.csswg.org/css-contain-3/#container-name) property. + pub const ContainerNameList = union(enum) { + /// The `none` keyword. + none, + /// A list of container names. + names: SmallList(ContainerIdent, 1), + }; + + /// A value for the [container](https://drafts.csswg.org/css-contain-3/#container-shorthand) shorthand property. + pub const Container = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); +}; + +pub const css_modules = struct { + /// A value for the [composes](https://github.com/css-modules/css-modules/#dependencies) property from CSS modules. + pub const Composes = struct { + /// A list of class names to compose. + names: CustomIdentList, + /// Where the class names are composed from. + from: ?Specifier, + /// The source location of the `composes` property. + loc: Location, + }; + + /// Defines where the class names referenced in the `composes` property are located. + /// + /// See [Composes](Composes). + const Specifier = union(enum) { + /// The referenced name is global. + global, + /// The referenced name comes from the specified file. + file: []const u8, + /// The referenced name comes from a source index (used during bundling). + source_index: u32, + + pub fn parse(input: *css.Parser) Error!Specifier { + if (input.tryParse(css.Parser.expectString, .{})) |file| { + return .{ .file = file }; + } + try input.expectIdentMatching("global"); + return .global; + } + }; +}; + +pub const display = struct { + /// A value for the [display](https://drafts.csswg.org/css-display-3/#the-display-properties) property. + pub const Display = union(enum) { + /// A display keyword. + keyword: DisplayKeyword, + /// The inside and outside display values. + pair: DisplayPair, + }; + + /// A value for the [visibility](https://drafts.csswg.org/css-display-3/#visibility) property. + pub const Visibility = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + pub const DisplayKeyword = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A pair of inside and outside display values, as used in the `display` property. + /// + /// See [Display](Display). + pub const DisplayPair = struct { + /// The outside display value. + outside: DisplayOutside, + /// The inside display value. + inside: DisplayInside, + /// Whether this is a list item. + is_list_item: bool, + }; + + /// A [``](https://drafts.csswg.org/css-display-3/#typedef-display-outside) value. + pub const DisplayOutside = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + /// A [``](https://drafts.csswg.org/css-display-3/#typedef-display-inside) value. + pub const DisplayInside = union(enum) { + flow, + flow_root, + table, + flex: css.VendorPrefix, + box: css.VendorPrefix, + grid, + ruby, + }; +}; + +pub const effects = struct { + /// A value for the [filter](https://drafts.fxtf.org/filter-effects-1/#FilterProperty) and + /// [backdrop-filter](https://drafts.fxtf.org/filter-effects-2/#BackdropFilterProperty) properties. + pub const FilterList = union(enum) { + /// The `none` keyword. + none, + /// A list of filter functions. + filters: SmallList(Filter, 1), + }; + + /// A [filter](https://drafts.fxtf.org/filter-effects-1/#filter-functions) function. + pub const Filter = union(enum) { + /// A `blur()` filter. + blur: Length, + /// A `brightness()` filter. + brightness: NumberOrPercentage, + /// A `contrast()` filter. + contrast: NumberOrPercentage, + /// A `grayscale()` filter. + grayscale: NumberOrPercentage, + /// A `hue-rotate()` filter. + hue_rotate: Angle, + /// An `invert()` filter. + invert: NumberOrPercentage, + /// An `opacity()` filter. + opacity: NumberOrPercentage, + /// A `saturate()` filter. + saturate: NumberOrPercentage, + /// A `sepia()` filter. + sepia: NumberOrPercentage, + /// A `drop-shadow()` filter. + drop_shadow: DropShadow, + /// A `url()` reference to an SVG filter. + url: Url, + }; + + /// A [`drop-shadow()`](https://drafts.fxtf.org/filter-effects-1/#funcdef-filter-drop-shadow) filter function. + pub const DropShadow = struct { + /// The color of the drop shadow. + color: CssColor, + /// The x offset of the drop shadow. + x_offset: Length, + /// The y offset of the drop shadow. + y_offset: Length, + /// The blur radius of the drop shadow. + blur: Length, + }; +}; + +pub const flex = struct { + /// A value for the [flex-direction](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#propdef-flex-direction) property. + pub const FlexDirection = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [flex-wrap](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-wrap-property) property. + pub const FlexWrap = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [flex-flow](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-flow-property) shorthand property. + pub const FlexFlow = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [flex](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-property) shorthand property. + pub const Flex = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the legacy (prefixed) [box-orient](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#orientation) property. + /// Partially equivalent to `flex-direction` in the standard syntax. + pub const BoxOrient = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the legacy (prefixed) [box-orient](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#orientation) property. + /// Partially equivalent to `flex-direction` in the standard syntax. + pub const BoxDirection = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the legacy (prefixed) [box-align](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#alignment) property. + /// Equivalent to the `align-items` property in the standard syntax. + pub const BoxAlign = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the legacy (prefixed) [box-pack](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#packing) property. + /// Equivalent to the `justify-content` property in the standard syntax. + pub const BoxPack = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the legacy (prefixed) [box-lines](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#multiple) property. + /// Equivalent to the `flex-wrap` property in the standard syntax. + pub const BoxLines = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + // Old flex (2012): https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/ + /// A value for the legacy (prefixed) [flex-pack](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-pack) property. + /// Equivalent to the `justify-content` property in the standard syntax. + pub const FlexPack = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the legacy (prefixed) [flex-item-align](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-align) property. + /// Equivalent to the `align-self` property in the standard syntax. + pub const FlexItemAlign = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the legacy (prefixed) [flex-line-pack](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-line-pack) property. + /// Equivalent to the `align-content` property in the standard syntax. + pub const FlexLinePack = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); +}; + +pub const font = struct { + /// A value for the [font-weight](https://www.w3.org/TR/css-fonts-4/#font-weight-prop) property. + pub const FontWeight = union(enum) { + /// An absolute font weight. + absolute: AbsoluteFontWeight, + /// The `bolder` keyword. + bolder, + /// The `lighter` keyword. + lighter, + }; + + /// An [absolute font weight](https://www.w3.org/TR/css-fonts-4/#font-weight-absolute-values), + /// as used in the `font-weight` property. + /// + /// See [FontWeight](FontWeight). + pub const AbsoluteFontWeight = union(enum) { + /// An explicit weight. + weight: CSSNumber, + /// Same as `400`. + normal, + /// Same as `700`. + bold, + }; + + /// A value for the [font-size](https://www.w3.org/TR/css-fonts-4/#font-size-prop) property. + pub const FontSize = union(enum) { + /// An explicit size. + length: LengthPercentage, + /// An absolute font size keyword. + absolute: AbsoluteFontSize, + /// A relative font size keyword. + relative: RelativeFontSize, + }; + + /// An [absolute font size](https://www.w3.org/TR/css-fonts-3/#absolute-size-value), + /// as used in the `font-size` property. + /// + /// See [FontSize](FontSize). + pub const AbsoluteFontSize = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A [relative font size](https://www.w3.org/TR/css-fonts-3/#relative-size-value), + /// as used in the `font-size` property. + /// + /// See [FontSize](FontSize). + pub const RelativeFontSize = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [font-stretch](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop) property. + pub const FontStretch = union(enum) { + /// A font stretch keyword. + keyword: FontStretchKeyword, + /// A percentage. + percentage: Percentage, + }; + + pub const FontStretchKeyword = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [font-family](https://www.w3.org/TR/css-fonts-4/#font-family-prop) property. + pub const FontFamily = union(enum) { + /// A generic family name. + generic: GenericFontFamily, + /// A custom family name. + family_name: []const u8, + }; + + /// A [generic font family](https://www.w3.org/TR/css-fonts-4/#generic-font-families) name, + /// as used in the `font-family` property. + /// + /// See [FontFamily](FontFamily). + pub const GenericFontFamily = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [font-style](https://www.w3.org/TR/css-fonts-4/#font-style-prop) property. + pub const FontStyle = union(enum) { + /// Normal font style. + normal, + /// Italic font style. + italic, + /// Oblique font style, with a custom angle. + oblique: Angle, + }; + + /// A value for the [font-variant-caps](https://www.w3.org/TR/css-fonts-4/#font-variant-caps-prop) property. + pub const FontVariantCaps = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [line-height](https://www.w3.org/TR/2020/WD-css-inline-3-20200827/#propdef-line-height) property. + pub const LineHeight = union(enum) { + /// The UA sets the line height based on the font. + normal, + /// A multiple of the element's font size. + number: CSSNumber, + /// An explicit height. + length: LengthPercentage, + }; + + /// A value for the [font](https://www.w3.org/TR/css-fonts-4/#font-prop) shorthand property. + pub const Font = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property. + // TODO: there is a more extensive spec in CSS3 but it doesn't seem any browser implements it? https://www.w3.org/TR/css-inline-3/#transverse-alignment + pub const VerticalAlign = union(enum) { + /// A vertical align keyword. + keyword: VerticalAlignKeyword, + /// An explicit length. + length: LengthPercentage, + }; + + /// A keyword for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property. + pub const VerticalAlignKeyword = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); +}; + +pub const list = struct { + /// A value for the [list-style-type](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#text-markers) property. + pub const ListStyleType = union(enum) { + /// No marker. + none, + /// An explicit marker string. + string: CSSString, + /// A named counter style. + counter_style: CounterStyle, + }; + + /// A [counter-style](https://www.w3.org/TR/css-counter-styles-3/#typedef-counter-style) name. + pub const CounterStyle = union(enum) { + /// A predefined counter style name. + predefined: PredefinedCounterStyle, + /// A custom counter style name. + name: CustomIdent, + /// An inline `symbols()` definition. + symbols: Symbols, + + const Symbols = struct { + /// The counter system. + system: SymbolsType, + /// The symbols. + symbols: ArrayList(Symbol), + }; + }; + + /// A single [symbol](https://www.w3.org/TR/css-counter-styles-3/#funcdef-symbols) as used in the + /// `symbols()` function. + /// + /// See [CounterStyle](CounterStyle). + const Symbol = union(enum) { + /// A string. + string: CSSString, + /// An image. + image: Image, + }; + + /// A [predefined counter](https://www.w3.org/TR/css-counter-styles-3/#predefined-counters) style. + pub const PredefinedCounterStyle = @compileError(css.todo_stuff.depth); + + /// A [``](https://www.w3.org/TR/css-counter-styles-3/#typedef-symbols-type) value, + /// as used in the `symbols()` function. + /// + /// See [CounterStyle](CounterStyle). + pub const SymbolsType = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [list-style-position](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#list-style-position-property) property. + pub const ListStylePosition = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [list-style](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#list-style-property) shorthand property. + pub const ListStyle = @compileError(css.todo_stuff.depth); + + /// A value for the [marker-side](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#marker-side) property. + pub const MarkerSide = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); +}; + +pub const margin_padding = struct { + /// A value for the [inset-block](https://drafts.csswg.org/css-logical/#propdef-inset-block) shorthand property. + pub const InsetBlock = @compileError(css.todo_stuff.depth); + /// A value for the [inset-inline](https://drafts.csswg.org/css-logical/#propdef-inset-inline) shorthand property. + pub const InsetInline = @compileError(css.todo_stuff.depth); + /// A value for the [inset](https://drafts.csswg.org/css-logical/#propdef-inset) shorthand property. + pub const Inline = @compileError(css.todo_stuff.depth); + + /// A value for the [margin-block](https://drafts.csswg.org/css-logical/#propdef-margin-block) shorthand property. + pub const MarginBlock = @compileError(css.todo_stuff.depth); + + /// A value for the [margin-inline](https://drafts.csswg.org/css-logical/#propdef-margin-inline) shorthand property. + pub const MarginInline = @compileError(css.todo_stuff.depth); + + /// A value for the [margin](https://drafts.csswg.org/css-box-4/#propdef-margin) shorthand property. + pub const Margin = @compileError(css.todo_stuff.depth); + + /// A value for the [padding-block](https://drafts.csswg.org/css-logical/#propdef-padding-block) shorthand property. + pub const PaddingBlock = @compileError(css.todo_stuff.depth); + + /// A value for the [padding-inline](https://drafts.csswg.org/css-logical/#propdef-padding-inline) shorthand property. + pub const PaddingInline = @compileError(css.todo_stuff.depth); + + /// A value for the [padding](https://drafts.csswg.org/css-box-4/#propdef-padding) shorthand property. + pub const Padding = @compileError(css.todo_stuff.depth); + + /// A value for the [scroll-margin-block](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-margin-block) shorthand property. + pub const ScrollMarginBlock = @compileError(css.todo_stuff.depth); + + /// A value for the [scroll-margin-inline](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-margin-inline) shorthand property. + pub const ScrollMarginInline = @compileError(css.todo_stuff.depth); + + /// A value for the [scroll-margin](https://drafts.csswg.org/css-scroll-snap/#scroll-margin) shorthand property. + pub const ScrollMargin = @compileError(css.todo_stuff.depth); + + /// A value for the [scroll-padding-block](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-padding-block) shorthand property. + pub const ScrollPaddingBlock = @compileError(css.todo_stuff.depth); + + /// A value for the [scroll-padding-inline](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-padding-inline) shorthand property. + pub const ScrollPaddingInline = @compileError(css.todo_stuff.depth); + + /// A value for the [scroll-padding](https://drafts.csswg.org/css-scroll-snap/#scroll-padding) shorthand property. + pub const ScrollPadding = @compileError(css.todo_stuff.depth); +}; + +pub const masking = struct { + /// A value for the [clip-path](https://www.w3.org/TR/css-masking-1/#the-clip-path) property. + const ClipPath = union(enum) { + /// No clip path. + None, + /// A url reference to an SVG path element. + Url: Url, + /// A basic shape, positioned according to the reference box. + Shape: struct { + /// A basic shape. + // todo_stuff.think_about_mem_mgmt + shape: *BasicShape, + /// A reference box that the shape is positioned according to. + reference_box: masking.GeometryBox, + }, + /// A reference box. + Box: masking.GeometryBox, + }; + + /// A [``](https://www.w3.org/TR/css-masking-1/#typedef-geometry-box) value + /// as used in the `mask-clip` and `clip-path` properties. + const GeometryBox = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A CSS [``](https://www.w3.org/TR/css-shapes-1/#basic-shape-functions) value. + const BasicShape = union(enum) { + /// An inset rectangle. + Inset: InsetRect, + /// A circle. + Circle: Circle, + /// An ellipse. + Ellipse: Ellipse, + /// A polygon. + Polygon: Polygon, + }; + + /// An [`inset()`](https://www.w3.org/TR/css-shapes-1/#funcdef-inset) rectangle shape. + const InsetRect = struct { + /// The rectangle. + rect: Rect(LengthPercentage), + /// A corner radius for the rectangle. + radius: BorderRadius, + }; + + /// A [`circle()`](https://www.w3.org/TR/css-shapes-1/#funcdef-circle) shape. + pub const Circle = struct { + /// The radius of the circle. + radius: ShapeRadius, + /// The position of the center of the circle. + position: Position, + }; + + /// An [`ellipse()`](https://www.w3.org/TR/css-shapes-1/#funcdef-ellipse) shape. + pub const Ellipse = struct { + /// The x-radius of the ellipse. + radius_x: ShapeRadius, + /// The y-radius of the ellipse. + radius_y: ShapeRadius, + /// The position of the center of the ellipse. + position: Position, + }; + + /// A [`polygon()`](https://www.w3.org/TR/css-shapes-1/#funcdef-polygon) shape. + pub const Polygon = struct { + /// The fill rule used to determine the interior of the polygon. + fill_rule: FillRule, + /// The points of each vertex of the polygon. + points: ArrayList(Point), + }; + + /// A [``](https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius) value + /// that defines the radius of a `circle()` or `ellipse()` shape. + pub const ShapeRadius = union(enum) { + /// An explicit length or percentage. + LengthPercentage: LengthPercentage, + /// The length from the center to the closest side of the box. + ClosestSide, + /// The length from the center to the farthest side of the box. + FarthestSide, + }; + + /// A point within a `polygon()` shape. + /// + /// See [Polygon](Polygon). + pub const Point = struct { + /// The x position of the point. + x: LengthPercentage, + /// The y position of the point. + y: LengthPercentage, + }; + + /// A value for the [mask-mode](https://www.w3.org/TR/css-masking-1/#the-mask-mode) property. + const MaskMode = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [mask-clip](https://www.w3.org/TR/css-masking-1/#the-mask-clip) property. + const MaskClip = union(enum) { + /// A geometry box. + GeometryBox: masking.GeometryBox, + /// The painted content is not clipped. + NoClip, + }; + + /// A value for the [mask-composite](https://www.w3.org/TR/css-masking-1/#the-mask-composite) property. + pub const MaskComposite = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [mask-type](https://www.w3.org/TR/css-masking-1/#the-mask-type) property. + pub const MaskType = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [mask](https://www.w3.org/TR/css-masking-1/#the-mask) shorthand property. + pub const Mask = @compileError(css.todo_stuff.depth); + + /// A value for the [mask-border-mode](https://www.w3.org/TR/css-masking-1/#the-mask-border-mode) property. + pub const MaskBorderMode = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [mask-border](https://www.w3.org/TR/css-masking-1/#the-mask-border) shorthand property. + pub const MaskBorder = @compileError(css.todo_stuff.depth); + + /// A value for the [-webkit-mask-composite](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-mask-composite) + /// property. + /// + /// See also [MaskComposite](MaskComposite). + pub const WebKitMaskComposite = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [-webkit-mask-source-type](https://github.com/WebKit/WebKit/blob/6eece09a1c31e47489811edd003d1e36910e9fd3/Source/WebCore/css/CSSProperties.json#L6578-L6587) + /// property. + /// + /// See also [MaskMode](MaskMode). + pub const WebKitMaskSourceType = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); +}; + +pub const outline = struct { + /// A value for the [outline](https://drafts.csswg.org/css-ui/#outline) shorthand property. + pub const Outline = border.GenericBorder(outline.OutlineStyle, 11); + + /// A value for the [outline-style](https://drafts.csswg.org/css-ui/#outline-style) property. + pub const OutlineStyle = union(enum) { + /// The `auto` keyword. + auto: void, + /// A value equivalent to the `border-style` property. + line_style: border.LineStyle, + }; +}; + +pub const overflow = struct { + /// A value for the [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) shorthand property. + pub const Overflow = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + /// An [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) keyword + /// as used in the `overflow-x`, `overflow-y`, and `overflow` properties. + pub const OverflowKeyword = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + /// A value for the [text-overflow](https://www.w3.org/TR/css-overflow-3/#text-overflow) property. + pub const TextOverflow = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); +}; + +pub const position = struct { + /// A value for the [position](https://www.w3.org/TR/css-position-3/#position-property) property. + pub const Position = union(enum) { + /// The box is laid in the document flow. + static, + /// The box is laid out in the document flow and offset from the resulting position. + relative, + /// The box is taken out of document flow and positioned in reference to its relative ancestor. + absolute, + /// Similar to relative but adjusted according to the ancestor scrollable element. + sticky: css.VendorPrefix, + /// The box is taken out of the document flow and positioned in reference to the page viewport. + fixed, + }; +}; + +pub const shape = struct { + /// A [``](https://www.w3.org/TR/css-shapes-1/#typedef-fill-rule) used to + /// determine the interior of a `polygon()` shape. + /// + /// See [Polygon](Polygon). + pub const FillRule = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A CSS [``](https://www.w3.org/TR/css-color-4/#typedef-alpha-value), + /// used to represent opacity. + /// + /// Parses either a `` or ``, but is always stored and serialized as a number. + pub const AlphaValue = struct { + v: f32, + }; +}; + +pub const size = struct { + pub const Size = union(enum) { + /// The `auto` keyworda + auto, + /// An explicit length or percentage. + length_percentage: css_values.length.LengthPercentage, + /// The `min-content` keyword. + min_content: css.VendorPrefix, + /// The `max-content` keyword. + max_content: css.VendorPrefix, + /// The `fit-content` keyword. + fit_content: css.VendorPrefix, + /// The `fit-content()` function. + fit_content_function: css_values.length.LengthPercentage, + /// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords. + stretch: css.VendorPrefix, + /// The `contain` keyword. + contain, + }; + + /// A value for the [minimum](https://drafts.csswg.org/css-sizing-3/#min-size-properties) + /// and [maximum](https://drafts.csswg.org/css-sizing-3/#max-size-properties) size properties, + /// e.g. `min-width` and `max-height`. + pub const MaxSize = union(enum) { + /// The `none` keyword. + none, + /// An explicit length or percentage. + length_percentage: LengthPercentage, + /// The `min-content` keyword. + min_content: css.VendorPrefix, + /// The `max-content` keyword. + max_content: css.VendorPrefix, + /// The `fit-content` keyword. + fit_content: css.VendorPrefix, + /// The `fit-content()` function. + fit_content_function: LengthPercentage, + /// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords. + stretch: css.VendorPrefix, + /// The `contain` keyword. + contain, + }; +}; + +pub const svg = struct { + /// An SVG [``](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) value + /// used in the `fill` and `stroke` properties. + const SVGPaint = union(enum) { + /// A URL reference to a paint server element, e.g. `linearGradient`, `radialGradient`, and `pattern`. + Url: struct { + /// The url of the paint server. + url: Url, + /// A fallback to be used in case the paint server cannot be resolved. + fallback: ?SVGPaintFallback, + }, + /// A solid color paint. + Color: CssColor, + /// Use the paint value of fill from a context element. + ContextFill, + /// Use the paint value of stroke from a context element. + ContextStroke, + /// No paint. + None, + }; + + /// A fallback for an SVG paint in case a paint server `url()` cannot be resolved. + /// + /// See [SVGPaint](SVGPaint). + const SVGPaintFallback = union(enum) { + /// No fallback. + None, + /// A solid color. + Color: CssColor, + }; + + /// A value for the [stroke-linecap](https://www.w3.org/TR/SVG2/painting.html#LineCaps) property. + pub const StrokeLinecap = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [stroke-linejoin](https://www.w3.org/TR/SVG2/painting.html#LineJoin) property. + pub const StrokeLinejoin = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [stroke-dasharray](https://www.w3.org/TR/SVG2/painting.html#StrokeDashing) property. + const StrokeDasharray = union(enum) { + /// No dashing is used. + None, + /// Specifies a dashing pattern to use. + Values: ArrayList(LengthPercentage), + }; + + /// A value for the [marker](https://www.w3.org/TR/SVG2/painting.html#VertexMarkerProperties) properties. + const Marker = union(enum) { + /// No marker. + None, + /// A url reference to a `` element. + Url: Url, + }; + + /// A value for the [color-interpolation](https://www.w3.org/TR/SVG2/painting.html#ColorInterpolation) property. + pub const ColorInterpolation = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [color-rendering](https://www.w3.org/TR/SVG2/painting.html#ColorRendering) property. + pub const ColorRendering = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [shape-rendering](https://www.w3.org/TR/SVG2/painting.html#ShapeRendering) property. + pub const ShapeRendering = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [text-rendering](https://www.w3.org/TR/SVG2/painting.html#TextRendering) property. + pub const TextRendering = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [image-rendering](https://www.w3.org/TR/SVG2/painting.html#ImageRendering) property. + pub const ImageRendering = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); +}; + +pub const text = struct { + /// A value for the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. + pub const TextTransform = struct { + /// How case should be transformed. + case: TextTransformCase, + /// How ideographic characters should be transformed. + other: TextTransformOther, + }; + + pub const TextTransformOther = packed struct(u8) { + /// Puts all typographic character units in full-width form. + full_width: bool = false, + /// Converts all small Kana characters to the equivalent full-size Kana. + full_size_kana: bool = false, + }; + + /// Defines how text case should be transformed in the + /// [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. + const TextTransformCase = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [white-space](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#white-space-property) property. + pub const WhiteSpace = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [word-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-break-property) property. + pub const WordBreak = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [line-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#line-break-property) property. + pub const LineBreak = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [hyphens](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#hyphenation) property. + pub const Hyphens = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [overflow-wrap](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#overflow-wrap-property) property. + pub const OverflowWrap = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [text-align](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-property) property. + pub const TextAlign = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [text-align-last](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-last-property) property. + pub const TextAlignLast = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [text-justify](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-justify-property) property. + pub const TextJustify = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [word-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-spacing-property) + /// and [letter-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#letter-spacing-property) properties. + pub const Spacing = union(enum) { + /// No additional spacing is applied. + normal, + /// Additional spacing between each word or letter. + length: Length, + }; + + /// A value for the [text-indent](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-indent-property) property. + pub const TextIndent = struct { + /// The amount to indent. + value: LengthPercentage, + /// Inverts which lines are affected. + hanging: bool, + /// Affects the first line after each hard break. + each_line: bool, + }; + + /// A value for the [text-decoration-line](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-line-property) property. + /// + /// Multiple lines may be specified by combining the flags. + pub const TextDecorationLine = packed struct(u8) { + /// Each line of text is underlined. + underline: bool = false, + /// Each line of text has a line over it. + overline: bool = false, + /// Each line of text has a line through the middle. + line_through: bool = false, + /// The text blinks. + blink: bool = false, + /// The text is decorated as a spelling error. + spelling_error: bool = false, + /// The text is decorated as a grammar error. + grammar_error: bool = false, + }; + + /// A value for the [text-decoration-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-style-property) property. + pub const TextDecorationStyle = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [text-decoration-thickness](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-width-property) property. + pub const TextDecorationThickness = union(enum) { + /// The UA chooses an appropriate thickness for text decoration lines. + auto, + /// Use the thickness defined in the current font. + from_font, + /// An explicit length. + length_percentage: LengthPercentage, + }; + + /// A value for the [text-decoration](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-property) shorthand property. + pub const TextDecoration = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [text-decoration-skip-ink](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-skip-ink-property) property. + pub const TextDecorationSkipInk = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A text emphasis shape for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property. + /// + /// See [TextEmphasisStyle](TextEmphasisStyle). + pub const TextEmphasisStyle = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [text-emphasis](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-property) shorthand property. + pub const TextEmphasis = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property. + pub const TextEmphasisPosition = struct { + /// The vertical position. + vertical: text.TextEmphasisPositionVertical, + /// The horizontal position. + horizontal: text.TextEmphasisPositionHorizontal, + }; + + /// A vertical position keyword for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property. + /// + /// See [TextEmphasisPosition](TextEmphasisPosition). + pub const TextEmphasisPositionVertical = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A horizontal position keyword for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property. + /// + /// See [TextEmphasisPosition](TextEmphasisPosition). + pub const TextEmphasisPositionHorizontal = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [text-shadow](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-shadow-property) property. + pub const TextShadow = struct { + /// The color of the text shadow. + color: CssColor, + /// The x offset of the text shadow. + x_offset: Length, + /// The y offset of the text shadow. + y_offset: Length, + /// The blur radius of the text shadow. + blur: Length, + /// The spread distance of the text shadow. + spread: Length, // added in Level 4 spec + }; + + /// A value for the [text-size-adjust](https://w3c.github.io/csswg-drafts/css-size-adjust/#adjustment-control) property. + pub const TextSizeAdjust = union(enum) { + /// Use the default size adjustment when displaying on a small device. + auto, + /// No size adjustment when displaying on a small device. + none, + /// When displaying on a small device, the font size is multiplied by this percentage. + percentage: Percentage, + }; + + /// A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property. + pub const Direction = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property. + pub const UnicodeBidi = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [box-decoration-break](https://www.w3.org/TR/css-break-3/#break-decoration) property. + pub const BoxDecorationBreak = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); +}; + +pub const transform = struct { + /// A value for the [transform](https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#propdef-transform) property. + pub const TransformList = struct { + v: ArrayList(Transform), + }; + + /// An individual transform function (https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#two-d-transform-functions). + pub const Transform = union(enum) { + /// A 2D translation. + translate: struct { x: LengthPercentage, y: LengthPercentage }, + /// A translation in the X direction. + translate_x: LengthPercentage, + /// A translation in the Y direction. + translate_y: LengthPercentage, + /// A translation in the Z direction. + translate_z: Length, + /// A 3D translation. + translate_3d: struct { x: LengthPercentage, y: LengthPercentage, z: Length }, + /// A 2D scale. + scale: struct { x: NumberOrPercentage, y: NumberOrPercentage }, + /// A scale in the X direction. + scale_x: NumberOrPercentage, + /// A scale in the Y direction. + scale_y: NumberOrPercentage, + /// A scale in the Z direction. + scale_z: NumberOrPercentage, + /// A 3D scale. + scale_3d: struct { x: NumberOrPercentage, y: NumberOrPercentage, z: NumberOrPercentage }, + /// A 2D rotation. + rotate: Angle, + /// A rotation around the X axis. + rotate_x: Angle, + /// A rotation around the Y axis. + rotate_y: Angle, + /// A rotation around the Z axis. + rotate_z: Angle, + /// A 3D rotation. + rotate_3d: struct { x: f32, y: f32, z: f32, angle: Angle }, + /// A 2D skew. + skew: struct { x: Angle, y: Angle }, + /// A skew along the X axis. + skew_x: Angle, + /// A skew along the Y axis. + skew_y: Angle, + /// A perspective transform. + perspective: Length, + /// A 2D matrix transform. + matrix: Matrix(f32), + /// A 3D matrix transform. + matrix_3d: Matrix3d(f32), + }; + + /// A 2D matrix. + pub fn Matrix(comptime T: type) type { + return struct { + a: T, + b: T, + c: T, + d: T, + e: T, + f: T, + }; + } + + /// A 3D matrix. + pub fn Matrix3d(comptime T: type) type { + return struct { + m11: T, + m12: T, + m13: T, + m14: T, + m21: T, + m22: T, + m23: T, + m24: T, + m31: T, + m32: T, + m33: T, + m34: T, + m41: T, + m42: T, + m43: T, + m44: T, + }; + } + + /// A value for the [transform-style](https://drafts.csswg.org/css-transforms-2/#transform-style-property) property. + pub const TransformStyle = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [transform-box](https://drafts.csswg.org/css-transforms-1/#transform-box) property. + pub const TransformBox = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [backface-visibility](https://drafts.csswg.org/css-transforms-2/#backface-visibility-property) property. + pub const BackfaceVisibility = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the perspective property. + pub const Perspective = union(enum) { + /// No perspective transform is applied. + none, + /// Distance to the center of projection. + length: Length, + }; + + /// A value for the [translate](https://drafts.csswg.org/css-transforms-2/#propdef-translate) property. + pub const Translate = union(enum) { + /// The "none" keyword. + none, + + /// The x, y, and z translations. + xyz: struct { + /// The x translation. + x: LengthPercentage, + /// The y translation. + y: LengthPercentage, + /// The z translation. + z: Length, + }, + }; + + /// A value for the [rotate](https://drafts.csswg.org/css-transforms-2/#propdef-rotate) property. + pub const Rotate = struct { + /// Rotation around the x axis. + x: f32, + /// Rotation around the y axis. + y: f32, + /// Rotation around the z axis. + z: f32, + /// The angle of rotation. + angle: Angle, + }; + + /// A value for the [scale](https://drafts.csswg.org/css-transforms-2/#propdef-scale) property. + pub const Scale = union(enum) { + /// The "none" keyword. + none, + + /// Scale on the x, y, and z axis. + xyz: struct { + /// Scale on the x axis. + x: NumberOrPercentage, + /// Scale on the y axis. + y: NumberOrPercentage, + /// Scale on the z axis. + z: NumberOrPercentage, + }, + }; +}; + +pub const transition = struct { + /// A value for the [transition](https://www.w3.org/TR/2018/WD-css-transitions-1-20181011/#transition-shorthand-property) property. + pub const Transition = @compileError(css.todo_stuff.depth); +}; + +pub const ui = struct { + /// A value for the [color-scheme](https://drafts.csswg.org/css-color-adjust/#color-scheme-prop) property. + pub const ColorScheme = packed struct(u8) { + /// Indicates that the element supports a light color scheme. + light: bool = false, + /// Indicates that the element supports a dark color scheme. + dark: bool = false, + /// Forbids the user agent from overriding the color scheme for the element. + only: bool = false, + }; + + /// A value for the [resize](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#resize) property. + pub const Resize = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [cursor](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) property. + pub const Cursor = struct { + /// A list of cursor images. + images: SmallList(CursorImage), + /// A pre-defined cursor. + keyword: CursorKeyword, + }; + + /// A [cursor image](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) value, used in the `cursor` property. + /// + /// See [Cursor](Cursor). + pub const CursorImage = struct { + /// A url to the cursor image. + url: Url, + /// The location in the image where the mouse pointer appears. + hotspot: ?[2]CSSNumber, + }; + + /// A pre-defined [cursor](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) value, + /// used in the `cursor` property. + /// + /// See [Cursor](Cursor). + pub const CursorKeyword = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [caret-color](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-color) property. + pub const ColorOrAuto = union(enum) { + /// The `currentColor`, adjusted by the UA to ensure contrast against the background. + auto, + /// A color. + color: CssColor, + }; + + /// A value for the [caret-shape](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-shape) property. + pub const CaretShape = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [caret](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret) shorthand property. + pub const Caret = @compileError(css.todo_stuff.depth); + + /// A value for the [user-select](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#content-selection) property. + pub const UserSelect = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + + /// A value for the [appearance](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#appearance-switching) property. + pub const Appearance = union(enum) { + none, + auto, + textfield, + menulist_button, + button, + checkbox, + listbox, + menulist, + meter, + progress_bar, + push_button, + radio, + searchfield, + slider_horizontal, + square_button, + textarea, + non_standard: []const u8, + }; +}; + +pub fn DefineProperties(comptime properties: anytype) type { + const input_fields: []const std.builtin.Type.StructField = std.meta.fields(@TypeOf(properties)); + const total_fields_len = input_fields.len + 2; // +2 for the custom property and the `all` property + const TagSize = u16; + const PropertyIdT, const max_enum_name_length: usize = brk: { + var max: usize = 0; + var property_id_type = std.builtin.Type.Enum{ + .tag_type = TagSize, + .is_exhaustive = true, + .decls = &.{}, + .fields = undefined, + }; + var enum_fields: [total_fields_len]std.builtin.Type.EnumField = undefined; + for (input_fields, 0..) |field, i| { + enum_fields[i] = .{ + .name = field.name, + .value = i, + }; + max = @max(max, field.name.len); + } + enum_fields[input_fields.len] = std.builtin.Type.EnumField{ + .name = "all", + .value = input_fields.len, + }; + enum_fields[input_fields.len + 1] = std.builtin.Type.EnumField{ + .name = "custom", + .value = input_fields.len + 1, + }; + property_id_type.fields = &enum_fields; + break :brk .{ property_id_type, max }; + }; + + const types: []const type = types: { + var types: [total_fields_len]type = undefined; + inline for (input_fields, 0..) |field, i| { + types[i] = @field(properties, field.name).ty; + + if (std.mem.eql(u8, field.name, "transition-property")) { + types[i] = struct { SmallList(PropertyIdT, 1), css.VendorPrefix }; + } + + // Validate it + + const value = @field(properties, field.name); + const ValueT = @TypeOf(value); + const value_ty = value.ty; + const ValueTy = @TypeOf(value_ty); + const value_ty_info = @typeInfo(ValueTy); + // If `valid_prefixes` is defined, the `ty` should be a two item tuple where + // the second item is of type `VendorPrefix` + if (@hasField(ValueT, "valid_prefixes")) { + if (!value_ty_info.Struct.is_tuple) { + @compileError("Expected a tuple type for `ty` when `valid_prefixes` is defined"); + } + if (value_ty_info.Struct.fields[1].type != css.VendorPrefix) { + @compileError("Expected the second item in the tuple to be of type `VendorPrefix`"); + } + } + } + types[input_fields.len] = void; + types[input_fields.len + 1] = CustomPropertyName; + break :types &types; + }; + const PropertyT = PropertyT: { + var union_fields: [total_fields_len]std.builtin.Type.UnionField = undefined; + inline for (input_fields, 0..) |input_field, i| { + const Ty = types[i]; + union_fields[i] = std.builtin.Type.UnionField{ + .alignment = @alignOf(Ty), + .type = type, + .name = input_field.name, + }; + } + union_fields[input_fields.len] = std.builtin.Type.UnionField{ + .alignment = 0, + .type = void, + .name = "all", + }; + union_fields[input_fields.len + 1] = std.builtin.Type.UnionField{ + .alignment = @alignOf(CustomPropertyName), + .type = CustomPropertyName, + .name = "custom", + }; + break :PropertyT std.builtin.Type.Union{ + .layout = .auto, + .tag_type = PropertyIdT, + .decls = &.{}, + .fields = union_fields, + }; + }; + _ = PropertyT; // autofix + return struct { + pub const PropertyId = PropertyIdT; + + pub fn propertyIdIsShorthand(id: PropertyId) bool { + inline for (std.meta.fields(PropertyId)) |field| { + if (field.value == @intFromEnum(id)) { + const is_shorthand = if (@hasField(@TypeOf(@field(properties, field.name)), "shorthand")) + @field(@field(properties, field.name), "shorthand") + else + false; + return is_shorthand; + } + } + return false; + } + + /// PropertyId.prefix() + pub fn propertyIdPrefix(id: PropertyId) css.VendorPrefix { + _ = id; // autofix + @compileError(css.todo_stuff.depth); + } + + /// PropertyId.name() + pub fn propertyIdName(id: PropertyId) []const u8 { + _ = id; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn propertyIdFromStr(name: []const u8) PropertyId { + const prefix, const name_ref = if (bun.strings.startsWithCaseInsensitiveAscii(name, "-webkit-")) + .{ css.VendorPrefix.webkit, name[8..] } + else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-moz-")) + .{ css.VendorPrefix.moz, name[5..] } + else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-o-")) + .{ css.VendorPrefix.moz, name[3..] } + else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-ms-")) + .{ css.VendorPrefix.moz, name[4..] } + else + .{ css.VendorPrefix.none, name }; + + return parsePropertyIdFromNameAndPrefix(name_ref, prefix) catch .{ + .custom = CustomPropertyName.fromStr(name), + }; + } + + pub fn parsePropertyIdFromNameAndPrefix(name: []const u8, prefix: css.VendorPrefix) Error!PropertyId { + var buffer: [max_enum_name_length]u8 = undefined; + if (name.len > buffer.len) { + // TODO: actual source just returns empty Err(()) + return Error.InvalidPropertyName; + } + const lower = bun.strings.copyLowercase(name, buffer[0..name.len]); + inline for (std.meta.fields(PropertyIdT)) |field_| { + const field: std.builtin.Type.EnumField = field_; + // skip custom + if (bun.strings.eql(field.name, "custom")) continue; + + if (bun.strings.eql(lower, field.name)) { + const prop = @field(properties, field.name); + const allowed_prefixes = allowed_prefixes: { + var prefixes: css.VendorPrefix = if (@hasField(@TypeOf(prop), "unprefixed") and !prop.unprefixed) + css.VendorPrefix.empty() + else + css.VendorPrefix{ .none = true }; + + if (@hasField(@TypeOf(prop), "valid_prefixes")) { + prefixes = css.VendorPrefix.bitwiseOr(prefixes, prop.valid_prefixes); + } + + break :allowed_prefixes prefixes; + }; + + if (allowed_prefixes.contains(prefix)) return @enumFromInt(field.value); + } + } + return Error.InvalidPropertyName; + } + }; +} + +/// SmallList(PropertyId) +const SmallListPropertyIdPlaceholder = struct {}; + +pub const Property = DefineProperties(.{ + .@"background-color" = .{ + .ty = CssColor, + }, + .@"background-image" = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = SmallList(Image, 1), + }, + .@"background-position-x" = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = SmallList(css_values.position.HorizontalPosition, 1), + }, + .@"background-position-y" = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = SmallList(css_values.position.HorizontalPosition, 1), + }, + .@"background-position" = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = SmallList(background.BackgroundPosition, 1), + .shorthand = true, + }, + .@"background-size" = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = SmallList(background.BackgroundSize, 1), + }, + .@"background-repeat" = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = SmallList(background.BackgroundSize, 1), + }, + .@"background-attachment" = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = SmallList(background.BackgroundAttachment, 1), + }, + .@"background-clip" = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = struct { + SmallList(background.BackgroundAttachment, 1), + css.VendorPrefix, + }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + }, + .@"background-origin" = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = SmallList(background.BackgroundOrigin, 1), + }, + .background = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = SmallList(background.Background, 1), + }, + + .@"box-shadow" = .{ + // PERF: make this equivalent to SmallVec<[_; 1]> + .ty = struct { SmallList(box_shadow.BoxShadow, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + }, + .opacity = .{ + .ty = css.css_values.alpha.AlphaValue, + }, + .color = .{ + .ty = CssColor, + }, + .display = .{ + .ty = display.Display, + }, + .visibility = .{ + .ty = display.Visibility, + }, + + .width = .{ + .ty = size.Size, + .logical_group = .{ .ty = LogicalGroup.size, .category = PropertyCategory.physical }, + }, + .height = .{ + .ty = size.Size, + .logical_group = .{ .ty = LogicalGroup.size, .category = PropertyCategory.physical }, + }, + .@"min-width" = .{ + .ty = size.Size, + .logical_group = .{ .ty = LogicalGroup.min_size, .category = PropertyCategory.physical }, + }, + .@"min-height" = .{ + .ty = size.Size, + .logical_group = .{ .ty = LogicalGroup.min_size, .category = PropertyCategory.physical }, + }, + .@"max-width" = .{ + .ty = size.MaxSize, + .logical_group = .{ .ty = LogicalGroup.max_size, .category = PropertyCategory.physical }, + }, + .@"max-height" = .{ + .ty = size.MaxSize, + .logical_group = .{ .ty = LogicalGroup.max_size, .category = PropertyCategory.physical }, + }, + .@"block-size" = .{ + .ty = size.Size, + .logical_group = .{ .ty = LogicalGroup.size, .category = PropertyCategory.logical }, + }, + .@"inline-size" = .{ + .ty = size.Size, + .logical_group = .{ .ty = LogicalGroup.size, .category = PropertyCategory.logical }, + }, + .min_block_size = .{ + .ty = size.Size, + .logical_group = .{ .ty = LogicalGroup.min_size, .category = PropertyCategory.logical }, + }, + .@"min-inline-size" = .{ + .ty = size.Size, + .logical_group = .{ .ty = LogicalGroup.min_size, .category = PropertyCategory.logical }, + }, + .@"max-block-size" = .{ + .ty = size.MaxSize, + .logical_group = .{ .ty = LogicalGroup.max_size, .category = PropertyCategory.logical }, + }, + .@"max-inline-size" = .{ + .ty = size.MaxSize, + .logical_group = .{ .ty = LogicalGroup.max_size, .category = PropertyCategory.logical }, + }, + .@"box-sizing" = .{ + .ty = struct { size.BoxSizing, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + }, + .@"aspect-ratio" = .{ + .ty = size.AspectRatio, + }, + + .overflow = .{ + .ty = overflow.Overflow, + .shorthand = true, + }, + .@"overflow-x" = .{ + .ty = overflow.OverflowKeyword, + }, + .@"overflow-y" = .{ + .ty = overflow.OverflowKeyword, + }, + .@"text-overflow" = .{ + .ty = struct { overflow.TextOverflow, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .o = true, + }, + }, + + // https://www.w3.org/TR/2020/WD-css-position-3-20200519 + .position = .{ + .ty = position.Position, + }, + .top = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.physical }, + }, + .bottom = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.physical }, + }, + .left = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.physical }, + }, + .right = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.physical }, + }, + .@"inset-block-start" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.logical }, + }, + .@"inset-block-end" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.logical }, + }, + .@"inset-inline-start" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.logical }, + }, + .@"inset-inline-end" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.logical }, + }, + .@"inset-block" = .{ + .ty = margin_padding.InsetBlock, + .shorthand = true, + }, + .@"inset-inline" = .{ + .ty = margin_padding.InsetInline, + .shorthand = true, + }, + .inset = .{ + .ty = margin_padding.Inset, + .shorthand = true, + }, + + .@"border-spacing" = .{ + .ty = css.css_values.size.Size(Length), + }, + + .@"border-top-color" = .{ + .ty = CssColor, + .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.physical }, + }, + .@"border-bottom-color" = .{ + .ty = CssColor, + .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.physical }, + }, + .@"border-left-color" = .{ + .ty = CssColor, + .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.physical }, + }, + .@"border-right-color" = .{ + .ty = CssColor, + .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.physical }, + }, + .@"border-block-start-color" = .{ + .ty = CssColor, + .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.logical }, + }, + .@"border-block-end-color" = .{ + .ty = CssColor, + .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.logical }, + }, + .@"border-inline-start-color" = .{ + .ty = CssColor, + .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.logical }, + }, + .@"border-inline-end-color" = .{ + .ty = CssColor, + .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.logical }, + }, + + .@"border-top-style" = .{ + .ty = border.LineStyle, + .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.physical }, + }, + .@"border-bottom-style" = .{ + .ty = border.LineStyle, + .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.physical }, + }, + .@"border-left-style" = .{ + .ty = border.LineStyle, + .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.physical }, + }, + .@"border-right-style" = .{ + .ty = border.LineStyle, + .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.physical }, + }, + .@"border-block-start-style" = .{ + .ty = border.LineStyle, + .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.logical }, + }, + .@"border-block-end-style" = .{ + .ty = border.LineStyle, + .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.logical }, + }, + .@"border-inline-start-style" = .{ + .ty = border.LineStyle, + .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.logical }, + }, + .@"border-inline-end-style" = .{ + .ty = border.LineStyle, + .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.logical }, + }, + + .@"border-top-width" = .{ + .ty = BorderSideWidth, + .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.physical }, + }, + .@"border-bottom-width" = .{ + .ty = BorderSideWidth, + .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.physical }, + }, + .@"border-left-width" = .{ + .ty = BorderSideWidth, + .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.physical }, + }, + .@"border-right-width" = .{ + .ty = BorderSideWidth, + .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.physical }, + }, + .@"border-block-start-width" = .{ + .ty = BorderSideWidth, + .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.logical }, + }, + .@"border-block-end-width" = .{ + .ty = BorderSideWidth, + .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.logical }, + }, + .@"border-inline-start-width" = .{ + .ty = BorderSideWidth, + .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.logical }, + }, + .@"border-inline-end-width" = .{ + .ty = BorderSideWidth, + .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.logical }, + }, + + .@"border-top-left-radius" = .{ + .ty = struct { Size2D(LengthPercentage), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.physical }, + }, + .@"border-top-right-radius" = .{ + .ty = struct { Size2D(LengthPercentage), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.physical }, + }, + .@"border-bottom-left-radius" = .{ + .ty = struct { Size2D(LengthPercentage), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.physical }, + }, + .@"border-bottom-right-radius" = .{ + .ty = struct { Size2D(LengthPercentage), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.physical }, + }, + .@"border-start-start-radius" = .{ + .ty = Size2D(LengthPercentage), + .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.logical }, + }, + .@"border-start-end-radius" = .{ + .ty = Size2D(LengthPercentage), + .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.logical }, + }, + .@"border-end-start-radius" = .{ + .ty = Size2D(LengthPercentage), + .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.logical }, + }, + .@"border-end-end-radius" = .{ + .ty = Size2D(LengthPercentage), + .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.logical }, + }, + .@"border-radius" = .{ + .ty = struct { BorderRadius, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .shorthand = true, + }, + + .@"border-image-source" = .{ + .ty = Image, + }, + .@"border-image-outset" = .{ + .ty = Rect(LengthOrNumber), + }, + .@"border-image-repeat" = .{ + .ty = BorderImageRepeat, + }, + .@"border-image-width" = .{ + .ty = Rect(BorderImageSideWidth), + }, + .@"border-image-slice" = .{ + .ty = BorderImageSlice, + }, + .@"border-image" = .{ + .ty = struct { BorderImage, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .o = true, + }, + .shorthand = true, + }, + + .@"border-color" = .{ + .ty = BorderColor, + .shorthand = true, + }, + .@"border-style" = .{ + .ty = BorderStyle, + .shorthand = true, + }, + .@"border-width" = .{ + .ty = BorderWidth, + .shorthand = true, + }, + + .@"border-block-color" = .{ + .ty = BorderBlockColor, + .shorthand = true, + }, + .@"border-block-style" = .{ + .ty = BorderBlockStyle, + .shorthand = true, + }, + .@"border-block-width" = .{ + .ty = BorderBlockWidth, + .shorthand = true, + }, + + .@"border-inline-color" = .{ + .ty = BorderInlineColor, + .shorthand = true, + }, + .@"border-inline-style" = .{ + .ty = BorderInlineStyle, + .shorthand = true, + }, + .@"border-inline-width" = .{ + .ty = BorderInlineWidth, + .shorthand = true, + }, + + .border = .{ + .ty = Border, + .shorthand = true, + }, + .@"border-top" = .{ + .ty = BorderTop, + .shorthand = true, + }, + .@"border-bottom" = .{ + .ty = BorderBottom, + .shorthand = true, + }, + .@"border-left" = .{ + .ty = BorderLeft, + .shorthand = true, + }, + .@"border-right" = .{ + .ty = BorderRight, + .shorthand = true, + }, + .@"border-block" = .{ + .ty = BorderBlock, + .shorthand = true, + }, + .@"border-block-start" = .{ + .ty = BorderBlockStart, + .shorthand = true, + }, + .@"border-block-end" = .{ + .ty = BorderBlockEnd, + .shorthand = true, + }, + .@"border-inline" = .{ + .ty = BorderInline, + .shorthand = true, + }, + .@"border-inline-start" = .{ + .ty = BorderInlineStart, + .shorthand = true, + }, + .@"border-inline-end" = .{ + .ty = BorderInlineEnd, + .shorthand = true, + }, + + .outline = .{ + .ty = Outline, + .shorthand = true, + }, + .@"outline-color" = .{ + .ty = CssColor, + }, + .@"outline-style" = .{ + .ty = OutlineStyle, + }, + .@"outline-width" = .{ + .ty = BorderSideWidth, + }, + + // Flex properties: https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119 + .@"flex-direction" = .{ + .ty = struct { FlexDirection, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .ms = true, + }, + }, + .@"flex-wrap" = .{ + .ty = struct { FlexWrap, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .ms = true, + }, + }, + .@"flex-flow" = .{ + .ty = struct { FlexFlow, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .ms = true, + }, + .shorthand = true, + }, + .@"flex-grow" = .{ + .ty = struct { CSSNumber, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"flex-shrink" = .{ + .ty = struct { CSSNumber, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"flex-basis" = .{ + .ty = struct { LengthPercentageOrAuto, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .flex = .{ + .ty = struct { Flex, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .ms = true, + }, + .shorthand = true, + }, + .order = .{ + .ty = struct { CSSInteger, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + + // Align properties: https://www.w3.org/TR/2020/WD-css-align-3-20200421 + .@"align-content" = .{ + .ty = struct { AlignContent, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"justify-content" = .{ + .ty = struct { JustifyContent, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"place-content" = .{ + .ty = PlaceContent, + .shorthand = true, + }, + .@"align-self" = .{ + .ty = struct { AlignSelf, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"justify-self" = .{ + .ty = JustifySelf, + }, + .@"place-self" = .{ + .ty = PlaceSelf, + .shorthand = true, + }, + .@"align-items" = .{ + .ty = struct { AlignItems, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"justify-items" = .{ + .ty = JustifyItems, + }, + .@"place-items" = .{ + .ty = PlaceItems, + .shorthand = true, + }, + .@"row-gap" = .{ + .ty = GapValue, + }, + .@"column-gap" = .{ + .ty = GapValue, + }, + .gap = .{ + .ty = Gap, + .shorthand = true, + }, + + // Old flex (2009): https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/ + .@"box-orient" = .{ + .ty = struct { BoxOrient, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .unprefixed = false, + }, + .@"box-direction" = .{ + .ty = struct { BoxDirection, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .unprefixed = false, + }, + .@"box-ordinal-group" = .{ + .ty = struct { CSSInteger, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .unprefixed = false, + }, + .@"box-align" = .{ + .ty = struct { BoxAlign, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .unprefixed = false, + }, + .@"box-flex" = .{ + .ty = struct { CSSNumber, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .unprefixed = false, + }, + .@"box-flex-group" = .{ + .ty = struct { CSSInteger, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + .unprefixed = false, + }, + .@"box-pack" = .{ + .ty = struct { BoxPack, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .unprefixed = false, + }, + .@"box-lines" = .{ + .ty = struct { BoxLines, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .unprefixed = false, + }, + + // Old flex (2012): https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/ + .@"flex-pack" = .{ + .ty = struct { FlexPack, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .ms = true, + }, + .unprefixed = false, + }, + .@"flex-order" = .{ + .ty = struct { CSSInteger, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .ms = true, + }, + .unprefixed = false, + }, + .@"flex-align" = .{ + .ty = struct { BoxAlign, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .ms = true, + }, + .unprefixed = false, + }, + .@"flex-item-align" = .{ + .ty = struct { FlexItemAlign, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .ms = true, + }, + .unprefixed = false, + }, + .@"flex-line-pack" = .{ + .ty = struct { FlexLinePack, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .ms = true, + }, + .unprefixed = false, + }, + + // Microsoft extensions + .@"flex-positive" = .{ + .ty = struct { CSSNumber, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .ms = true, + }, + .unprefixed = false, + }, + .@"flex-negative" = .{ + .ty = struct { CSSNumber, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .ms = true, + }, + .unprefixed = false, + }, + .@"flex-preferred-size" = .{ + .ty = struct { LengthPercentageOrAuto, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .ms = true, + }, + .unprefixed = false, + }, + + // TODO: the following is enabled with #[cfg(feature = "grid")] + // .@"grid-template-columns" = .{ + // .ty = TrackSizing, + // }, + // .@"grid-template-rows" = .{ + // .ty = TrackSizing, + // }, + // .@"grid-auto-columns" = .{ + // .ty = TrackSizeList, + // }, + // .@"grid-auto-rows" = .{ + // .ty = TrackSizeList, + // }, + // .@"grid-auto-flow" = .{ + // .ty = GridAutoFlow, + // }, + // .@"grid-template-areas" = .{ + // .ty = GridTemplateAreas, + // }, + // .@"grid-template" = .{ + // .ty = GridTemplate, + // .shorthand = true, + // }, + // .grid = .{ + // .ty = Grid, + // .shorthand = true, + // }, + // .@"grid-row-start" = .{ + // .ty = GridLine, + // }, + // .@"grid-row-end" = .{ + // .ty = GridLine, + // }, + // .@"grid-column-start" = .{ + // .ty = GridLine, + // }, + // .@"grid-column-end" = .{ + // .ty = GridLine, + // }, + // .@"grid-row" = .{ + // .ty = GridRow, + // .shorthand = true, + // }, + // .@"grid-column" = .{ + // .ty = GridColumn, + // .shorthand = true, + // }, + // .@"grid-area" = .{ + // .ty = GridArea, + // .shorthand = true, + // }, + + .@"margin-top" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.physical }, + }, + .@"margin-bottom" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.physical }, + }, + .@"margin-left" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.physical }, + }, + .@"margin-right" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.physical }, + }, + .@"margin-block-start" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.logical }, + }, + .@"margin-block-end" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.logical }, + }, + .@"margin-inline-start" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.logical }, + }, + .@"margin-inline-end" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.logical }, + }, + .@"margin-block" = .{ + .ty = MarginBlock, + .shorthand = true, + }, + .@"margin-inline" = .{ + .ty = MarginInline, + .shorthand = true, + }, + .margin = .{ + .ty = Margin, + .shorthand = true, + }, + + .@"padding-top" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.physical }, + }, + .@"padding-bottom" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.physical }, + }, + .@"padding-left" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.physical }, + }, + .@"padding-right" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.physical }, + }, + .@"padding-block-start" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.logical }, + }, + .@"padding-block-end" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.logical }, + }, + .@"padding-inline-start" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.logical }, + }, + .@"padding-inline-end" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.logical }, + }, + .@"padding-block" = .{ + .ty = PaddingBlock, + .shorthand = true, + }, + .@"padding-inline" = .{ + .ty = PaddingInline, + .shorthand = true, + }, + .padding = .{ + .ty = Padding, + .shorthand = true, + }, + + .@"scroll-margin-top" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.physical }, + }, + .@"scroll-margin-bottom" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.physical }, + }, + .@"scroll-margin-left" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.physical }, + }, + .@"scroll-margin-right" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.physical }, + }, + .@"scroll-margin-block-start" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.logical }, + }, + .@"scroll-margin-block-end" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.logical }, + }, + .@"scroll-margin-inline-start" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.logical }, + }, + .@"scroll-margin-inline-end" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.logical }, + }, + .@"scroll-margin-block" = .{ + .ty = ScrollMarginBlock, + .shorthand = true, + }, + .@"scroll-margin-inline" = .{ + .ty = ScrollMarginInline, + .shorthand = true, + }, + .@"scroll-margin" = .{ + .ty = ScrollMargin, + .shorthand = true, + }, + + .@"scroll-padding-top" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.physical }, + }, + .@"scroll-padding-bottom" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.physical }, + }, + .@"scroll-padding-left" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.physical }, + }, + .@"scroll-padding-right" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.physical }, + }, + .@"scroll-padding-block-start" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.logical }, + }, + .@"scroll-padding-block-end" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.logical }, + }, + .@"scroll-padding-inline-start" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.logical }, + }, + .@"scroll-padding-inline-end" = .{ + .ty = LengthPercentageOrAuto, + .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.logical }, + }, + .@"scroll-padding-block" = .{ + .ty = ScrollPaddingBlock, + .shorthand = true, + }, + .@"scroll-padding-inline" = .{ + .ty = ScrollPaddingInline, + .shorthand = true, + }, + .@"scroll-padding" = .{ + .ty = ScrollPadding, + .shorthand = true, + }, + + .@"font-weight" = .{ + .ty = FontWeight, + }, + .@"font-size" = .{ + .ty = FontSize, + }, + .@"font-stretch" = .{ + .ty = FontStretch, + }, + .@"font-family" = .{ + .ty = ArrayList(FontFamily), + }, + .@"font-style" = .{ + .ty = FontStyle, + }, + .@"font-variant-caps" = .{ + .ty = FontVariantCaps, + }, + .@"line-height" = .{ + .ty = LineHeight, + }, + .font = .{ + .ty = Font, + .shorthand = true, + }, + .@"vertical-align" = .{ + .ty = VerticalAlign, + }, + .@"font-palette" = .{ + .ty = DashedIdentReference, + }, + + .@"transition-property" = .{ + .ty = struct { SmallListPropertyIdPlaceholder, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + }, + }, + .@"transition-duration" = .{ + .ty = struct { SmallList(Time, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + }, + }, + .@"transition-delay" = .{ + .ty = struct { SmallList(Time, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + }, + }, + .@"transition-timing-function" = .{ + .ty = struct { SmallList(EasingFunction, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + }, + }, + .transition = .{ + .ty = struct { SmallList(Transition, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + }, + .shorthand = true, + }, + + .@"animation-name" = .{ + .ty = struct { AnimationNameList, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .o = true, + }, + }, + .@"animation-duration" = .{ + .ty = struct { SmallList(Time, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .o = true, + }, + }, + .@"animation-timing-function" = .{ + .ty = struct { SmallList(EasingFunction, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .o = true, + }, + }, + .@"animation-iteration-count" = .{ + .ty = struct { SmallList(AnimationIterationCount, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .o = true, + }, + }, + .@"animation-direction" = .{ + .ty = struct { SmallList(AnimationDirection, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .o = true, + }, + }, + .@"animation-play-state" = .{ + .ty = struct { SmallList(AnimationPlayState, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .o = true, + }, + }, + .@"animation-delay" = .{ + .ty = struct { SmallList(Time, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .o = true, + }, + }, + .@"animation-fill-mode" = .{ + .ty = struct { SmallList(AnimationFillMode, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .o = true, + }, + }, + .@"animation-composition" = .{ + .ty = SmallList(AnimationComposition, 1), + }, + .@"animation-timeline" = .{ + .ty = SmallList(AnimationTimeline, 1), + }, + .@"animation-range-start" = .{ + .ty = SmallList(AnimationRangeStart, 1), + }, + .@"animation-range-end" = .{ + .ty = SmallList(AnimationRangeEnd, 1), + }, + .@"animation-range" = .{ + .ty = SmallList(AnimationRange, 1), + }, + .animation = .{ + .ty = struct { AnimationList, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .o = true, + }, + .shorthand = true, + }, + + // https://drafts.csswg.org/css-transforms-2/ + .transform = .{ + .ty = struct { TransformList, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + .o = true, + }, + }, + .@"transform-origin" = .{ + .ty = struct { Position, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + .o = true, + }, + // TODO: handle z offset syntax + }, + .@"transform-style" = .{ + .ty = struct { TransformStyle, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + }, + .@"transform-box" = .{ + .ty = TransformBox, + }, + .@"backface-visibility" = .{ + .ty = struct { BackfaceVisibility, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + }, + .perspective = .{ + .ty = struct { Perspective, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + }, + .@"perspective-origin" = .{ + .ty = struct { Position, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + }, + .translate = .{ + .ty = Translate, + }, + .rotate = .{ + .ty = Rotate, + }, + .scale = .{ + .ty = Scale, + }, + + // https://www.w3.org/TR/2021/CRD-css-text-3-20210422 + .@"text-transform" = .{ + .ty = TextTransform, + }, + .@"white-space" = .{ + .ty = WhiteSpace, + }, + .@"tab-size" = .{ + .ty = struct { LengthOrNumber, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .moz = true, + .o = true, + }, + }, + .@"word-break" = .{ + .ty = WordBreak, + }, + .@"line-break" = .{ + .ty = LineBreak, + }, + .hyphens = .{ + .ty = struct { Hyphens, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + }, + }, + .@"overflow-wrap" = .{ + .ty = OverflowWrap, + }, + .@"word-wrap" = .{ + .ty = OverflowWrap, + }, + .@"text-align" = .{ + .ty = TextAlign, + }, + .@"text-align-last" = .{ + .ty = struct { TextAlignLast, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .moz = true, + }, + }, + .@"text-justify" = .{ + .ty = TextJustify, + }, + .@"word-spacing" = .{ + .ty = Spacing, + }, + .@"letter-spacing" = .{ + .ty = Spacing, + }, + .@"text-indent" = .{ + .ty = TextIndent, + }, + + // https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506 + .@"text-decoration-line" = .{ + .ty = struct { TextDecorationLine, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + }, + .@"text-decoration-style" = .{ + .ty = struct { TextDecorationStyle, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + }, + .@"text-decoration-color" = .{ + .ty = struct { CssColor, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + }, + .@"text-decoration-thickness" = .{ + .ty = TextDecorationThickness, + }, + .@"text-decoration" = .{ + .ty = struct { TextDecoration, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + }, + .shorthand = true, + }, + .@"text-decoration-skip-ink" = .{ + .ty = struct { TextDecorationSkipInk, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"text-emphasis-style" = .{ + .ty = struct { TextEmphasisStyle, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"text-emphasis-color" = .{ + .ty = struct { CssColor, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"text-emphasis" = .{ + .ty = struct { TextEmphasis, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + .shorthand = true, + }, + .@"text-emphasis-position" = .{ + .ty = struct { TextEmphasisPosition, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"text-shadow" = .{ + .ty = SmallList(TextShadow, 1), + }, + + // https://w3c.github.io/csswg-drafts/css-size-adjust/ + .@"text-size-adjust" = .{ + .ty = struct { TextSizeAdjust, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + }, + }, + + // https://drafts.csswg.org/css-writing-modes-3/ + .direction = .{ + .ty = Direction, + }, + .@"unicode-bidi" = .{ + .ty = UnicodeBidi, + }, + + // https://www.w3.org/TR/css-break-3/ + .@"box-decoration-break" = .{ + .ty = struct { BoxDecorationBreak, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + + // https://www.w3.org/TR/2021/WD-css-ui-4-20210316 + .resize = .{ + .ty = Resize, + }, + .cursor = .{ + .ty = Cursor, + }, + .@"caret-color" = .{ + .ty = ColorOrAuto, + }, + .@"caret-shape" = .{ + .ty = CaretShape, + }, + .caret = .{ + .ty = Caret, + .shorthand = true, + }, + .@"user-select" = .{ + .ty = struct { UserSelect, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + }, + }, + .@"accent-color" = .{ + .ty = ColorOrAuto, + }, + .appearance = .{ + .ty = struct { Appearance, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + .moz = true, + .ms = true, + }, + }, + + // https://www.w3.org/TR/2020/WD-css-lists-3-20201117 + .@"list-style-type" = .{ + .ty = ListStyleType, + }, + .@"list-style-image" = .{ + .ty = Image, + }, + .@"list-style-position" = .{ + .ty = ListStylePosition, + }, + .@"list-style" = .{ + .ty = ListStyle, + .shorthand = true, + }, + .@"marker-side" = .{ + .ty = MarkerSide, + }, + + // CSS modules + .composes = .{ + .ty = Composes, + .conditional = .{ + .css_modules = true, + }, + }, + + // https://www.w3.org/TR/SVG2/painting.html + .fill = .{ + .ty = SVGPaint, + }, + .@"fill-rule" = .{ + .ty = FillRule, + }, + .@"fill-opacity" = .{ + .ty = AlphaValue, + }, + .stroke = .{ + .ty = SVGPaint, + }, + .@"stroke-opacity" = .{ + .ty = AlphaValue, + }, + .@"stroke-width" = .{ + .ty = LengthPercentage, + }, + .@"stroke-linecap" = .{ + .ty = StrokeLinecap, + }, + .@"stroke-linejoin" = .{ + .ty = StrokeLinejoin, + }, + .@"stroke-miterlimit" = .{ + .ty = CSSNumber, + }, + .@"stroke-dasharray" = .{ + .ty = StrokeDasharray, + }, + .@"stroke-dashoffset" = .{ + .ty = LengthPercentage, + }, + .@"marker-start" = .{ + .ty = Marker, + }, + .@"marker-mid" = .{ + .ty = Marker, + }, + .@"marker-end" = .{ + .ty = Marker, + }, + .marker = .{ + .ty = Marker, + }, + .@"color-interpolation" = .{ + .ty = ColorInterpolation, + }, + .@"color-interpolation-filters" = .{ + .ty = ColorInterpolation, + }, + .@"color-rendering" = .{ + .ty = ColorRendering, + }, + .@"shape-rendering" = .{ + .ty = ShapeRendering, + }, + .@"text-rendering" = .{ + .ty = TextRendering, + }, + .@"image-rendering" = .{ + .ty = ImageRendering, + }, + + // https://www.w3.org/TR/css-masking-1/ + .@"clip-path" = .{ + .ty = struct { ClipPath, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"clip-rule" = .{ + .ty = FillRule, + }, + .@"mask-image" = .{ + .ty = struct { SmallList(Image, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"mask-mode" = .{ + .ty = SmallList(MaskMode, 1), + }, + .@"mask-repeat" = .{ + .ty = struct { SmallList(BackgroundRepeat, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"mask-position-x" = .{ + .ty = SmallList(HorizontalPosition, 1), + }, + .@"mask-position-y" = .{ + .ty = SmallList(VerticalPosition, 1), + }, + .@"mask-position" = .{ + .ty = struct { SmallList(Position, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"mask-clip" = .{ + .ty = struct { SmallList(MaskClip, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"mask-origin" = .{ + .ty = struct { SmallList(GeometryBox, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"mask-size" = .{ + .ty = struct { SmallList(BackgroundSize, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"mask-composite" = .{ + .ty = SmallList(MaskComposite, 1), + }, + .@"mask-type" = .{ + .ty = MaskType, + }, + .mask = .{ + .ty = struct { SmallList(Mask, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + .shorthand = true, + }, + .@"mask-border-source" = .{ + .ty = Image, + }, + .@"mask-border-mode" = .{ + .ty = MaskBorderMode, + }, + .@"mask-border-slice" = .{ + .ty = BorderImageSlice, + }, + .@"mask-border-width" = .{ + .ty = Rect(BorderImageSideWidth), + }, + .@"mask-border-outset" = .{ + .ty = Rect(LengthOrNumber), + }, + .@"mask-border-repeat" = .{ + .ty = BorderImageRepeat, + }, + .@"mask-border" = .{ + .ty = MaskBorder, + .shorthand = true, + }, + + // WebKit additions + .@"-webkit-mask-composite" = .{ + .ty = SmallList(WebKitMaskComposite, 1), + }, + .@"mask-source-type" = .{ + .ty = struct { SmallList(WebKitMaskSourceType, 1), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + .unprefixed = false, + }, + .@"mask-box-image" = .{ + .ty = struct { BorderImage, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + .unprefixed = false, + }, + .@"mask-box-image-source" = .{ + .ty = struct { Image, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + .unprefixed = false, + }, + .@"mask-box-image-slice" = .{ + .ty = struct { BorderImageSlice, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + .unprefixed = false, + }, + .@"mask-box-image-width" = .{ + .ty = struct { Rect(BorderImageSideWidth), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + .unprefixed = false, + }, + .@"mask-box-image-outset" = .{ + .ty = struct { Rect(LengthOrNumber), css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + .unprefixed = false, + }, + .@"mask-box-image-repeat" = .{ + .ty = struct { BorderImageRepeat, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + .unprefixed = false, + }, + + // https://drafts.fxtf.org/filter-effects-1/ + .filter = .{ + .ty = struct { FilterList, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + .@"backdrop-filter" = .{ + .ty = struct { FilterList, css.VendorPrefix }, + .valid_prefixes = css.VendorPrefix{ + .webkit = true, + }, + }, + + // https://drafts.csswg.org/css2/ + .@"z-index" = .{ + .ty = position.ZIndex, + }, + + // https://drafts.csswg.org/css-contain-3/ + .@"container-type" = .{ + .ty = ContainerType, + }, + .@"container-name" = .{ + .ty = ContainerNameList, + }, + .container = .{ + .ty = Container, + .shorthand = true, + }, + + // https://w3c.github.io/csswg-drafts/css-view-transitions-1/ + .@"view-transition-name" = .{ + .ty = CustomIdent, + }, + + // https://drafts.csswg.org/css-color-adjust/ + .@"color-scheme" = .{ + .ty = ColorScheme, + }, +}); diff --git a/src/css/rules/custom_media.zig b/src/css/rules/custom_media.zig new file mode 100644 index 0000000000000..de50f682941bc --- /dev/null +++ b/src/css/rules/custom_media.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const css_values = @import("../values/values.zig"); +pub const Error = css.Error; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +/// A [@custom-media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) rule. +pub const CustomMediaRule = struct { + /// The name of the declared media query. + name: css_values.ident.DashedIdent, + /// The media query to declare. + query: css.MediaList, + /// The location of the rule in the source file. + loc: css.Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + try dest.writeStr("@custom-media "); + try css_values.ident.DashedIdentFns.toCss(this, W, dest); + try dest.writeChar(' '); + try this.query.toCss(W, dest); + try dest.writeChar(';'); + } +}; diff --git a/src/css/rules/layer.zig b/src/css/rules/layer.zig new file mode 100644 index 0000000000000..2aa3367579a7c --- /dev/null +++ b/src/css/rules/layer.zig @@ -0,0 +1,12 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const Error = css.Error; + +const ArrayList = std.ArrayListUnmanaged; + +// TODO: make this equivalent of SmallVec<[CowArcStr<'i>; 1] diff --git a/src/css/rules/namespace.zig b/src/css/rules/namespace.zig new file mode 100644 index 0000000000000..7f7d7dd75fcd7 --- /dev/null +++ b/src/css/rules/namespace.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const css_values = @import("../values/values.zig"); +pub const Error = css.Error; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +/// A [@namespace](https://drafts.csswg.org/css-namespaces/#declaration) rule. +pub const NamespaceRule = struct { + /// An optional namespace prefix to declare, or `None` to declare the default namespace. + prefix: ?css.Ident, + /// The url of the namespace. + url: css.CSSString, + /// The location of the rule in the source file. + loc: css.Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@namespace "); + if (this.prefix) |*prefix| { + try css.css_values.ident.IdentFns.toCss(prefix, W, dest); + try dest.writeChar(' '); + } + + try css.css_values.string.CSSStringFns.toCss(this, W, dest); + try dest.writeChar(':'); + } +}; diff --git a/src/css/rules/rules.zig b/src/css/rules/rules.zig new file mode 100644 index 0000000000000..bcce6a1786546 --- /dev/null +++ b/src/css/rules/rules.zig @@ -0,0 +1,1560 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; + +pub fn CssRule(comptime Rule: type) type { + return union(enum) { + /// A `@media` rule. + media: media.MediaRule(Rule), + /// An `@import` rule. + import: import.ImportRule, + /// A style rule. + style: style.StyleRule(Rule), + /// A `@keyframes` rule. + keyframes: keyframes.KeyframesRule, + /// A `@font-face` rule. + font_face: font_face.FontFaceRule, + /// A `@font-palette-values` rule. + font_palette_values: font_face.FontPaletteValuesRule, + /// A `@page` rule. + page: page.PageRule, + /// A `@supports` rule. + supports: supports.SupportsRule(Rule), + /// A `@counter-style` rule. + counter_style: counter_style.CounterStyleRule, + /// A `@namespace` rule. + namespace: namespace.NamespaceRule, + /// A `@-moz-document` rule. + moz_document: document.MozDocumentRule(Rule), + /// A `@nest` rule. + nesting: nesting.NestingRule(Rule), + /// A `@viewport` rule. + viewport: viewport.ViewportRule, + /// A `@custom-media` rule. + custom_media: CustomMedia, + /// A `@layer` statement rule. + layer_statement: layer.LayerStatementRule, + /// A `@layer` block rule. + layer_block: layer.LayerBlockRule(Rule), + /// A `@property` rule. + property: property.PropertyRule, + /// A `@container` rule. + container: container.ContainerRule(Rule), + /// A `@scope` rule. + scope: scope.ScopeRule(Rule), + /// A `@starting-style` rule. + starting_style: starting_style.StartingStyleRule(Rule), + /// A placeholder for a rule that was removed. + ignored, + /// An unknown at-rule. + unknown: unknown.UnknownAtRule(Rule), + /// A custom at-rule. + custom: Rule, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.*) { + .media => |x| x.toCss(W, dest), + .import => |x| x.toCss(W, dest), + .style => |x| x.toCss(W, dest), + .keyframes => |x| x.toCss(W, dest), + .font_face => |x| x.toCss(W, dest), + .font_palette_values => |x| x.toCss(W, dest), + .page => |x| x.toCss(W, dest), + .counter_style => |x| x.toCss(W, dest), + .namespace => |x| x.toCss(W, dest), + .moz_document => |x| x.toCss(W, dest), + .nesting => |x| x.toCss(W, dest), + .viewport => |x| x.toCss(W, dest), + .custom_media => |x| x.toCss(W, dest), + .layer_statement => |x| x.toCss(W, dest), + .layer_block => |x| x.toCss(W, dest), + .property => |x| x.toCss(W, dest), + .starting_style => |x| x.toCss(W, dest), + .container => |x| x.toCss(W, dest), + .scope => |x| x.toCss(W, dest), + .unknown => |x| x.toCss(W, dest), + .custom => |x| x.toCss(W, dest) catch return PrinterError{ + .kind = css.PrinterErrorKind.fmt_error, + .loc = null, + }, + .ignored => {}, + }; + } + }; +} + +pub fn CssRuleList(comptime AtRule: type) type { + return struct { + v: ArrayList(CssRule(AtRule)) = .{}, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) Maybe(void, PrinterError) { + var first = true; + var last_without_block = false; + + for (this.v.items) |*rule| { + if (rule.* == .ignored) continue; + + // Skip @import rules if collecting dependencies. + if (rule == .import) { + if (dest.remove_imports) { + const dep = if (dest.dependencies != null) Dependency{ + .import = dependencies.ImportDependency.new(&rule.import, dest.filename()), + } else null; + + if (dest.dependencies) |*deps| { + deps.append(@compileError(css.todo_stuff.think_about_allocator), dep.?) catch unreachable; + continue; + } + } + } + + if (first) { + first = false; + } else { + if (!dest.minify and + !(last_without_block and + (rule.* == .import or rule.* == .namespace or rule.* == .layer_statement))) + { + try dest.writeChar('\n'); + } + try dest.newline(); + } + try rule.toCss(dest); + last_without_block = rule.* == .import or rule.* == .namespace or rule.* == .layer_statement; + } + } + }; +} + +pub const layer = struct { + pub const LayerName = struct { + v: ArrayList([]const u8) = .{}, + + pub fn parse(input: *css.Parser) Error!LayerName { + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn toCss(this: *const LayerName, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + /// A [@layer block](https://drafts.csswg.org/css-cascade-5/#layer-block) rule. + pub fn LayerBlockRule(comptime R: type) type { + return struct { + /// PERF: null pointer optimizaiton, nullable + /// The name of the layer to declare, or `None` to declare an anonymous layer. + name: ?LayerName, + /// The rules within the `@layer` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + } + + /// A [@layer statement](https://drafts.csswg.org/css-cascade-5/#layer-empty) rule. + /// + /// See also [LayerBlockRule](LayerBlockRule). + pub const LayerStatementRule = struct { + /// The layer names to declare. + names: ArrayList(LayerName), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + try dest.writeStr("@layer "); + css.to_css.fromList(LayerName, &this.names, W, dest); + try dest.writeChar(';'); + } + }; +}; + +pub const supports = struct { + /// A [``](https://drafts.csswg.org/css-conditional-3/#typedef-supports-condition), + /// as used in the `@supports` and `@import` rules. + pub const SupportsCondition = union(enum) { + /// A `not` expression. + not: *SupportsCondition, + + /// An `and` expression. + @"and": ArrayList(SupportsCondition), + + /// An `or` expression. + @"or": ArrayList(SupportsCondition), + + /// A declaration to evaluate. + declaration: struct { + /// The property id for the declaration. + property_id: css.PropertyId, + /// The raw value of the declaration. + value: []const u8, + }, + + /// A selector to evaluate. + selector: []const u8, + + /// An unknown condition. + unknown: []const u8, + + fn needsParens(this: *const SupportsCondition, parent: *const SupportsCondition) bool { + return switch (this.*) { + .not => true, + .@"and" => parent.* != .@"and", + .@"or" => parent.* != .@"or", + _ => false, + }; + } + + pub fn parse(input: *css.Parser) Error!SupportsCondition { + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn parseDeclaration(input: *css.Parser) Error!SupportsCondition { + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn toCss(this: *const SupportsCondition, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + switch (this.*) { + .not => |condition| { + try dest.writeStr(" not "); + condition.toCssWithParensIfNeeded(dest, condition.needsParens(this)); + }, + .@"and" => |conditions| { + var first = true; + for (conditions.items) |*cond| { + if (first) { + first = false; + } else { + try dest.writeStr(" and "); + } + try cond.toCssWithParensIfNeeded(dest, cond.needsParens(this)); + } + }, + .@"or" => |conditions| { + var first = true; + for (conditions.items) |*cond| { + if (first) { + first = false; + } else { + try dest.writeStr(" or "); + } + try cond.toCssWithParensIfNeeded(dest, cond.needsParens(this)); + } + }, + .declaration => |decl| { + const property_id = decl.property_id; + const value = decl.value; + + try dest.writeChar('('); + + const prefix: css.VendorPrefix = property_id.prefix().orNone(); + if (!prefix.eq(css.VendorPrefix{ .none = true })) { + try dest.writeChar('('); + } + + const name = property_id.name(); + var first = true; + inline for (std.meta.fields(css.VendorPrefix)) |field_| { + const field: std.builtin.Type.StructField = field_; + if (!@field(prefix, field.name)) continue; + + if (first) { + first = false; + } else { + try dest.writeStr(") or ("); + } + + var p = css.VendorPrefix{}; + @field(p, field.name) = true; + try css.serializer.serializeName(name, dest); + try dest.delim(':', false); + try dest.writeStr(value); + } + + if (!prefix.eq(css.VendorPrefix{ .none = true })) { + try dest.writeChar(')'); + } + try dest.writeChar(')'); + }, + .selector => |sel| { + try dest.writeStr("selector("); + try dest.writeStr(sel); + try dest.writeChar(')'); + }, + .unknown => |unk| { + try dest.writeStr(unk); + }, + } + } + + pub fn toCssWithParensIfNeeded( + this: *const SupportsCondition, + comptime W: type, + dest: *css.Printer( + W, + ), + needs_parens: bool, + ) css.PrintErr!void { + if (needs_parens) try dest.writeStr("("); + try this.toCss(W, dest); + if (needs_parens) try dest.writeStr(")"); + } + }; + + /// A [@supports](https://drafts.csswg.org/css-conditional-3/#at-supports) rule. + pub fn SupportsRule(comptime R: type) type { + return struct { + /// The supports condition. + condition: SupportsCondition, + /// The rules within the `@supports` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@supports "); + try this.condition.toCss(W, dest); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; + } +}; + +pub const custom_media = struct { + pub usingnamespace @import("./custom_media.zig"); +}; + +pub const namespace = struct { + pub usingnamespace @import("./namespace.zig"); +}; + +pub const unknown = struct { + pub usingnamespace @import("./unknown.zig"); +}; + +pub const media = struct { + pub fn MediaRule(comptime R: type) type { + return struct { + /// The media query list. + query: css.MediaList, + /// The rules within the `@media` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (dest.minify and this.query.alwaysMatches()) { + try this.rules.toCss(W, dest); + return; + } + + // #[cfg(feature = "sourcemap")] + // dest.addMapping(this.loc); + + try dest.writeStr("@media "); + try this.query.toCss(W, dest); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + dest.writeChar('}'); + } + }; + } +}; + +pub const keyframes = struct { + pub const KeyframesListParser = struct { + const This = @This(); + + pub const DeclarationParser = struct { + pub const Declaration = Keyframe; + + fn parseValue(this: *This, name: []const u8, input: *css.Parser) Error!Declaration { + _ = this; // autofix + _ = name; // autofix + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + }; + }; + pub const KeyframesName = union(enum) { + ident: css.css_values.ident.CustomIdent, + custom: []const u8, + + const This = @This(); + + pub fn parse(input: *css.Parser) Error!KeyframesName { + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + pub const KeyframeSelector = union(enum) { + /// An explicit percentage. + percentage: css.css_values.percentage.Percentage, + /// The `from` keyword. Equivalent to 0%. + from, + /// The `to` keyword. Equivalent to 100%. + to, + }; + + /// An individual keyframe within an `@keyframes` rule. + /// + /// See [KeyframesRule](KeyframesRule). + pub const Keyframe = struct { + /// A list of keyframe selectors to associate with the declarations in this keyframe. + selectors: ArrayList(KeyframeSelector), + /// The declarations for this keyframe. + declarations: css.DeclarationBlock, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + pub const KeyframesRule = struct { + /// The animation name. + /// = | + name: KeyframesName, + /// A list of keyframes in the animation. + keyframes: ArrayList(Keyframe), + /// A vendor prefix for the rule, e.g. `@-webkit-keyframes`. + vendor_prefix: css.VendorPrefix, + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + var first_rule = true; + + const PREFIXES = .{ "webkit", "moz", "ms", "o", "none" }; + + inline for (PREFIXES) |prefix_name| { + const prefix = css.VendorPrefix.fromName(prefix_name); + + if (this.vendor_prefix.contains(prefix)) { + if (first_rule) { + first_rule = false; + } else { + if (!dest.minify) { + try dest.writeChar('\n'); // no indent + } + try dest.newline(); + } + + try dest.writeChar('@'); + try prefix.toCss(W, dest); + try dest.writeStr("keyframes "); + try this.name.toCss(W, dest); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + + var first = true; + for (this.keyframes.items) |*keyframe| { + if (first) { + first = false; + } else if (!dest.minify) { + try dest.writeChar('\n'); // no indent + } + try dest.newline(); + try keyframe.toCss(W, dest); + } + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + } + } + }; +}; + +pub const page = struct { + /// A [page selector](https://www.w3.org/TR/css-page-3/#typedef-page-selector) + /// within a `@page` rule. + /// + /// Either a name or at least one pseudo class is required. + pub const PageSelector = struct { + /// An optional named page type. + name: ?[]const u8, + /// A list of page pseudo classes. + psuedo_classes: ArrayList(PagePseudoClass), + + pub fn parse(input: *css.Parser) Error!PageSelector { + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + pub const PageMarginRule = struct { + /// The margin box identifier for this rule. + margin_box: PageMarginBox, + /// The declarations within the rule. + declarations: css.DeclarationBlock, + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + /// A [@page](https://www.w3.org/TR/css-page-3/#at-page-rule) rule. + pub const PageRule = struct { + /// A list of page selectors. + selectors: ArrayList(PageSelector), + /// The declarations within the `@page` rule. + declarations: css.DeclarationBlock, + /// The nested margin rules. + rules: ArrayList(PageMarginRule), + /// The location of the rule in the source file. + loc: Location, + + pub fn parse(selectors: ArrayList(PageSelector), input: *css.Parser, loc: Location, options: *css.ParserOptions) Error!PageRule { + _ = selectors; // autofix + _ = input; // autofix + _ = loc; // autofix + _ = options; // autofix + @compileError(css.todo_stuff.depth); + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + try dest.writeStr("@page"); + if (this.selectors.items.len >= 1) { + const firstsel = &this.selectors.items[0]; + // Space is only required if the first selector has a name. + if (!dest.minify and firstsel.name != null) { + try dest.writeChar(' '); + } + var first = true; + for (this.selectors.items) |selector| { + if (first) { + first = false; + } else { + try dest.delim(',', false); + } + try selector.toCss(W, dest); + } + } + + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + + var i = 0; + const len = this.declarations.len() + this.rules.len(); + + const DECLS = .{ "declarations", "important_declarations" }; + inline for (DECLS) |decl_field_name| { + const decls: *const ArrayList(css.Property) = &@field(this.declarations, decl_field_name); + const important = comptime std.mem.eql(u8, decl_field_name, "important_declarations"); + for (decls.items) |*decl| { + try dest.newline(); + try decl.toCss(W, dest, important); + if (i != len - 1 or !dest.minify) { + try dest.writeChar(';'); + } + i += 1; + } + } + + if (this.rules.items.len > 0) { + if (!dest.minify and this.declarations.items.len > 0) { + try dest.writeChar('\n'); + } + try dest.newline(); + + var first = true; + for (this.rules.items) |*rule| { + if (first) { + first = false; + } else { + if (!dest.minify) { + try dest.writeChar('\n'); + } + try dest.newline(); + } + try rule.toCss(W, dest); + } + } + + dest.dedent(); + try dest.newline(); + dest.writeChar('}'); + } + }; + + pub const PagePseudoClass = css.DefineEnumProperty(struct { + comptime { + @compileError(css.todo_stuff.depth); + } + }); + + pub const PageMarginBox = css.DefineEnumProperty(struct { + comptime { + @compileError(css.todo_stuff.depth); + } + }); +}; + +pub const container = struct { + pub const ContainerName = struct { + v: css.css_values.ident.CustomIdent, + pub fn parse(input: *css.Parser) Error!ContainerName { + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + pub const ContainerNameFns = ContainerName; + pub const ContainerSizeFeature = struct { + comptime { + @compileError(css.todo_stuff.depth); + } + }; + + /// Represents a style query within a container condition. + pub const StyleQuery = union(enum) { + /// A style feature, implicitly parenthesized. + feature: css.Property, + + /// A negation of a condition. + not: *StyleQuery, + + /// A set of joint operations. + Operation: struct { + /// The operator for the conditions. + operator: css.media_query.Operator, + /// The conditions for the operator. + conditions: ArrayList(StyleQuery), + }, + }; + + pub const ContainerCondition = union(enum) { + /// A size container feature, implicitly parenthesized. + feature: ContainerSizeFeature, + /// A negation of a condition. + not: *ContainerCondition, + /// A set of joint operations. + operation: struct { + /// The operator for the conditions. + operator: css.media_query.Operator, + /// The conditions for the operator. + conditions: ArrayList(ContainerCondition), + }, + /// A style query. + style: StyleQuery, + + pub fn parse(input: *css.Parser) Error!ContainerCondition { + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + /// A [@container](https://drafts.csswg.org/css-contain-3/#container-rule) rule. + pub fn ContainerRule(comptime R: type) type { + return struct { + /// The name of the container. + name: ?ContainerName, + /// The container condition. + condition: ContainerCondition, + /// The rules within the `@container` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@container "); + if (this.name) |*name| { + try name.toCss(W, dest); + try dest.writeChar(' '); + } + + // Don't downlevel range syntax in container queries. + const exclude = dest.targets.exclude; + dest.targets.exclude.insert(css.Features{ .media_queries = true }); + try this.condition.toCss(W, dest); + dest.targets.exclude = exclude; + + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; + } +}; + +pub const scope = struct { + /// A [@scope](https://drafts.csswg.org/css-cascade-6/#scope-atrule) rule. + /// + /// @scope () [to ()]? { + /// + /// } + pub fn ScopeRule(comptime R: type) type { + return struct { + /// A selector list used to identify the scoping root(s). + scope_start: ?css.selector.api.SelectorList, + /// A selector list used to identify any scoping limits. + scope_end: ?css.selector.api.SelectorList, + /// Nested rules within the `@scope` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@scope"); + try dest.whitespace(); + if (this.scope_start) |*scope_start| { + try dest.writeChar('('); + try scope_start.toCss(W, dest); + try dest.writeChar(')'); + try dest.whitespace(); + } + if (this.scope_end) |*scope_end| { + if (dest.minify) { + try dest.writeChar(' '); + } + try dest.writeStr("to ("); + // is treated as an ancestor of scope end. + // https://drafts.csswg.org/css-nesting/#nesting-at-scope + if (this.scope_start) |*scope_start| { + try dest.withContext(scope_start, css.SelectorList.toCss, .{scope_start}); + } else { + try scope_end.toCss(W, dest); + } + try dest.writeChar(')'); + try dest.whitespace(); + } + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + // Nested style rules within @scope are implicitly relative to the + // so clear our style context while printing them to avoid replacing & ourselves. + // https://drafts.csswg.org/css-cascade-6/#scoped-rules + try dest.withClearedContext(CssRuleList(R).toCss, .{&this.rules}); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; + } +}; + +pub const style = struct { + pub fn StyleRule(comptime R: type) type { + return struct { + /// The selectors for the style rule. + selectors: css.selector.api.SelectorList, + /// A vendor prefix override, used during selector printing. + vendor_prefix: css.VendorPrefix, + /// The declarations within the style rule. + declarations: css.DeclarationBlock, + /// Nested rules within the style rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: css.Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (this.vendor_prefix.isEmpty()) { + try this.toCssBase(W, dest); + } else { + var first_rule = true; + inline for (std.meta.fields(css.VendorPrefix)) |field| { + if (!@field(this.vendor_prefix, field.name)) continue; + + if (first_rule) { + first_rule = false; + } else { + if (!dest.minify) { + try dest.writeChar('\n'); // no indent + } + try dest.newline(); + } + + const prefix = css.VendorPrefix.fromName(field.name); + dest.vendor_prefix = prefix; + this.toCssBase(W, dest); + } + + dest.vendor_prefix = css.VendorPrefix.empty(); + } + } + + fn toCssBase(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // If supported, or there are no targets, preserve nesting. Otherwise, write nested rules after parent. + const supports_nesting = this.rules.v.items.len == 0 or + css.Targets.shouldCompile( + dest.targets, + css.Targets.nestin, + ); + + const len = this.declarations.declarations.items.len + this.declarations.important_declarations.items.len; + const has_declarations = supports_nesting or len > 0 or this.rules.v.items.len == 0; + + if (has_declarations) { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try this.selectors.toCss(W, dest); + try dest.whitespace(); + try dest.writeChar('{'); + try dest.indent(); + + var i = 0; + const DECLS = .{ "declarations", "important_declarations" }; + inline for (DECLS) |decl_field_name| { + const important = comptime std.mem.eql(u8, decl_field_name, "important_declarations"); + const decls: *const ArrayList(css.Property) = &@field(this.declarations, decl_field_name); + + for (decls.items) |*decl| { + // The CSS modules `composes` property is handled specially, and omitted during printing. + // We need to add the classes it references to the list for the selectors in this rule. + if (decl.* == .composes) { + const composes = &decl.composes; + if (dest.isNested() and dest.css_module != null) { + return try dest.newError(css.PrinterErrorKind.invalid_composes_nesting, composes.loc); + } + + if (dest.css_module) |*css_module| { + css_module.handleComposes( + &this.selectors, + composes, + this.loc.source_index, + ) catch |e| { + return try dest.newError(e, composes.loc); + }; + continue; + } + } + + try dest.newline(); + try decl.toCss(dest, important); + if (i != len - 1 or !dest.minify or (supports_nesting and this.rules.v.items.len > 0)) { + try dest.writeChar(';'); + } + + i += 1; + } + } + } + + const Helpers = struct { + pub fn newline( + self: *const This, + comptime W2: type, + d: *Printer(W2), + supports_nesting2: bool, + len1: usize, + ) PrintErr!void { + if (!d.minify and (supports_nesting2 or len1 > 0) and self.rules.v.items.len > 0) { + if (len1 > 0) { + try d.writeChar('\n'); + } + try d.newline(); + } + } + + pub fn end(comptime W2: type, d: *Printer(W2), has_decls: bool) PrintErr!void { + if (has_decls) { + d.dedent(); + try d.newline(); + try d.writeChar('}'); + } + } + }; + + // Write nested rules after the parent. + if (supports_nesting) { + try Helpers.newline(this, W, dest, supports_nesting, len); + try this.rules.toCss(W, dest); + Helpers.end(W, dest, has_declarations); + } else { + Helpers.end(W, dest, has_declarations); + try Helpers.newline(this, W, dest, supports_nesting, len); + try dest.withContext(&this.selectors, This.toCss, .{this}); + } + } + }; + } +}; + +pub const font_face = struct { + /// A [@font-palette-values](https://drafts.csswg.org/css-fonts-4/#font-palette-values) rule. + pub const FontPaletteValuesRule = struct { + /// The name of the font palette. + name: css.css_values.ident.DashedIdent, + /// Declarations in the `@font-palette-values` rule. + properties: ArrayList(FontPaletteValuesProperty), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@font-palette-values "); + try css.css_values.ident.DashedIdentFns.toCss(&this.name, dest); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + const len = this.properties.items.len; + for (this.properties.items, 0..) |*prop, i| { + try dest.newline(); + try prop.toCss(dest); + if (i != len - 1 or !dest.minify) { + try dest.writeChar(';'); + } + } + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; + + pub const FontPaletteValuesProperty = struct { + /// The `font-family` property. + font_family: fontprops.FontFamily, + + /// The `base-palette` property. + base_palette: BasePalette, + + /// The `override-colors` property. + override_colors: ArrayList(OverrideColors), + + /// An unknown or unsupported property. + custom: css.css_properties.custom.CustomProperty, + + /// A property within an `@font-palette-values` rule. + /// + /// See [FontPaletteValuesRule](FontPaletteValuesRule). + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + /// A value for the [override-colors](https://drafts.csswg.org/css-fonts-4/#override-color) + /// property in an `@font-palette-values` rule. + pub const OverrideColors = struct { + /// The index of the color within the palette to override. + index: u16, + + /// The replacement color. + color: css.css_values.color.CssColor, + }; + + /// A value for the [base-palette](https://drafts.csswg.org/css-fonts-4/#base-palette-desc) + /// property in an `@font-palette-values` rule. + pub const BasePalette = union(enum) { + /// A light color palette as defined within the font. + light, + + /// A dark color palette as defined within the font. + dark, + + /// A palette index within the font. + integer: u16, + }; + + /// A property within an `@font-face` rule. + /// + /// See [FontFaceRule](FontFaceRule). + pub const FontFaceProperty = union(enum) { + /// The `src` property. + source: ArrayList(Source), + + /// The `font-family` property. + font_family: fontprops.FontFamily, + + /// The `font-style` property. + font_style: FontStyle, + + /// The `font-weight` property. + font_weight: Size2D(fontprops.FontWeight), + + /// The `font-stretch` property. + font_stretch: Size2D(fontprops.FontStretch), + + /// The `unicode-range` property. + unicode_range: ArrayList(UnicodeRange), + + /// An unknown or unsupported property. + custom: css.css_properties.custom.CustomProperty, + + comptime { + @compileError(css.todo_stuff.depth); + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + + @compileError(css.todo_stuff.depth); + } + }; + + /// A contiguous range of Unicode code points. + /// + /// Cannot be empty. Can represent a single code point when start == end. + pub const UnicodeRange = struct { + /// Inclusive start of the range. In [0, end]. + start: u32, + + /// Inclusive end of the range. In [0, 0x10FFFF]. + end: u32, + }; + + pub const FontStyle = union(enum) { + /// Normal font style. + normal, + + /// Italic font style. + italic, + + /// Oblique font style, with a custom angle. + oblique: css.css_values.angle.Angle, + }; + + /// A font format keyword in the `format()` function of the + /// [src](https://drafts.csswg.org/css-fonts/#src-desc) + /// property of an `@font-face` rule. + pub const FontFormat = union(enum) { + /// A WOFF 1.0 font. + woff, + + /// A WOFF 2.0 font. + woff2, + + /// A TrueType font. + truetype, + + /// An OpenType font. + opentype, + + /// An Embedded OpenType (.eot) font. + embedded_opentype, + + /// OpenType Collection. + collection, + + /// An SVG font. + svg, + + /// An unknown format. + string: []const u8, + }; + + /// A value for the [src](https://drafts.csswg.org/css-fonts/#src-desc) + /// property in an `@font-face` rule. + pub const Source = union(enum) { + /// A `url()` with optional format metadata. + url: UrlSource, + + /// The `local()` function. + local: fontprops.FontFamily, + }; + + pub const FontTechnology = enum { + /// A font format keyword in the `format()` function of the + /// [src](https://drafts.csswg.org/css-fonts/#src-desc) + /// property of an `@font-face` rule. + /// A font features tech descriptor in the `tech()`function of the + /// [src](https://drafts.csswg.org/css-fonts/#font-features-tech-values) + /// property of an `@font-face` rule. + /// Supports OpenType Features. + /// https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + @"features-opentype", + + /// Supports Apple Advanced Typography Font Features. + /// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM09/AppendixF.html + @"features-aat", + + /// Supports Graphite Table Format. + /// https://scripts.sil.org/cms/scripts/render_download.php?site_id=nrsi&format=file&media_id=GraphiteBinaryFormat_3_0&filename=GraphiteBinaryFormat_3_0.pdf + @"features-graphite", + + /// A color font tech descriptor in the `tech()`function of the + /// [src](https://drafts.csswg.org/css-fonts/#src-desc) + /// property of an `@font-face` rule. + /// Supports the `COLR` v0 table. + @"color-colrv0", + + /// Supports the `COLR` v1 table. + @"color-colrv1", + + /// Supports the `SVG` table. + @"color-svg", + + /// Supports the `sbix` table. + @"color-sbix", + + /// Supports the `CBDT` table. + @"color-cbdt", + + /// Supports Variations + /// The variations tech refers to the support of font variations + variations, + + /// Supports Palettes + /// The palettes tech refers to support for font palettes + palettes, + + /// Supports Incremental + /// The incremental tech refers to client support for incremental font loading, using either the range-request or the patch-subset method + incremental, + + pub usingnamespace css.DefineEnumProperty(@This()); + }; + + /// A `url()` value for the [src](https://drafts.csswg.org/css-fonts/#src-desc) + /// property in an `@font-face` rule. + pub const UrlSource = struct { + /// The URL. + url: Url, + + /// Optional `format()` function. + format: ?FontFormat, + + /// Optional `tech()` function. + tech: ArrayList(FontTechnology), + }; + + /// A [@font-face](https://drafts.csswg.org/css-fonts/#font-face-rule) rule. + pub const FontFaceRule = struct { + /// Declarations in the `@font-face` rule. + proeprties: ArrayList(FontFaceProperty), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@font-face"); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + const len = this.proeprties.items.len; + for (this.proeprties.items, 0..) |*prop, i| { + try dest.newline(); + prop.toCss(dest); + if (i != len - 1 or !dest.minify) { + try dest.writeChar(';'); + } + } + dest.dedent(); + try dest.newline(); + dest.writeChar('}'); + } + }; + + pub const FontFaceDeclarationParser = struct { + const This = @This(); + + pub const DeclarationParser = struct { + pub const Declaration = FontFaceProperty; + + fn parseValue(this: *This, name: []const u8, input: *css.Parser) Error!Declaration { + _ = this; // autofix + _ = name; // autofix + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + }; + }; +}; + +pub const viewport = struct { + /// A [@viewport](https://drafts.csswg.org/css-device-adapt/#atviewport-rule) rule. + pub const ViewportRule = struct { + /// The vendor prefix for this rule, e.g. `@-ms-viewport`. + vendor_prefix: css.VendorPrefix, + /// The declarations within the `@viewport` rule. + declarations: css.DeclarationBlock, + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + try dest.write_char('@'); + try this.vendor_prefix.toCss(W, dest); + try dest.write_str("viewport"); + try this.declarations.toCssBlock(W, dest); + } + }; +}; + +pub const counter_style = struct { + /// A [@counter-style](https://drafts.csswg.org/css-counter-styles/#the-counter-style-rule) rule. + pub const CounterStyleRule = struct { + /// The name of the counter style to declare. + name: css.css_values.ident.CustomIdent, + /// Declarations in the `@counter-style` rule. + declarations: css.DeclarationBlock, + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@counter-style"); + css.css_values.ident.CustomIdentFns.toCss(W, dest); + try this.declarations.toCssBlock(W, dest); + } + }; +}; + +pub const document = struct { + /// A [@-moz-document](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document) rule. + /// + /// Note that only the `url-prefix()` function with no arguments is supported, and only the `-moz` prefix + /// is allowed since Firefox was the only browser that ever implemented this rule. + pub fn MozDocumentRule(comptime R: type) type { + return struct { + /// Nested rules within the `@-moz-document` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + try dest.writeStr("@-moz-document url-prefix()"); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; + } +}; + +pub const property = struct { + pub const PropertyRule = struct { + name: css.css_values.ident.DashedIdent, + syntax: css.css_values.syntax.SyntaxString, + inherits: bool, + initial_vlaue: ?css.css_values.syntax.ParsedComponent, + loc: Location, + + pub fn parse(name: css.css_values.ident.DashedIdent, input: *css.Parser, loc: Location) Error!PropertyRule { + _ = name; // autofix + _ = input; // autofix + _ = loc; // autofix + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@property "); + try css.css_values.ident.DashedIdentFns.toCss(&this.name, W, dest); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + + try dest.writeStr("syntax:"); + try dest.whitespace(); + try this.syntax.toCss(W, dest); + try dest.writeChar(';'); + try dest.newline(); + + try dest.writeStr("inherits:"); + try dest.whitespace(); + if (this.inherits) { + try dest.writeStr("true"); + } else { + try dest.writeStr("false"); + } + + if (this.initial_vlaue) |*initial_value| { + try dest.writeChar(';'); + try dest.newline(); + + try dest.writeStr("initial-value:"); + try dest.whitespace(); + try initial_value.toCss(W, dest); + + if (!dest.minify) { + try dest.writeChar(';'); + } + } + + dest.dedent(); + try dest.newline(); + try dest.writeChar(';'); + } + }; +}; + +pub const starting_style = struct { + /// A [@starting-style](https://drafts.csswg.org/css-transitions-2/#defining-before-change-style-the-starting-style-rule) rule. + pub fn StartingStyleRule(comptime R: type) type { + return struct { + /// Nested rules within the `@starting-style` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@starting-style"); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; + } +}; + +pub const nesting = struct { + /// A [@nest](https://www.w3.org/TR/css-nesting-1/#at-nest) rule. + pub fn NestingRule(comptime R: type) type { + return struct { + /// The style rule that defines the selector and declarations for the `@nest` rule. + style: style.StyleRule(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + } +}; + +pub const Location = struct { + /// The index of the source file within the source map. + source_index: u32, + /// The line number, starting at 0. + line: u32, + /// The column number within a line, starting at 1 for first the character of the line. + /// Column numbers are counted in UTF-16 code units. + column: u32, +}; + +pub const import = struct { + /// A [@import](https://drafts.csswg.org/css-cascade/#at-import) rule. + pub const ImportRule = struct { + /// The url to import. + url: []const u8, + + /// An optional cascade layer name, or `None` for an anonymous layer. + layer: ?struct { + /// PERF: null pointer optimizaiton, nullable + v: ?layer.LayerName, + }, + + /// An optional `supports()` condition. + supports: ?supports.SupportsCondition, + + /// A media query. + media: css.MediaList, + + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + const dep = if (dest.dependencies != null) dependencies.ImportDependency.new( + @compileError(css.todo_stuff.think_about_allocator), + dest.filename(), + ) else null; + + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@import "); + if (dep) |d| { + try css.serializer.serializeString(dep.placeholder, W, dest); + + if (dest.dependencies) |*deps| { + deps.append( + @compileError(css.todo_stuff.think_about_allocator), + Dependency{ .import = d }, + ) catch unreachable; + } + } else { + try css.serializer.serializeString(this.url, W, dest); + } + + if (this.layer) |*lyr| { + try dest.writeStr(" layer"); + if (lyr.v) |l| { + try dest.writeChar('('); + try l.toCss(W, dest); + try dest.writeChar(')'); + } + } + + if (this.supports) |*sup| { + try dest.writeStr(" supports"); + if (sup.* == .declaration) { + try sup.toCss(W, dest); + } else { + try dest.writeChar('('); + try sup.toCss(W, dest); + try dest.writeChar(')'); + } + } + + if (this.media.media_queries.items.len > 0) { + try dest.writeChar(' '); + try this.media.toCss(W, dest); + } + try dest.writeStr(";"); + } + }; +}; diff --git a/src/css/rules/supports.zig b/src/css/rules/supports.zig new file mode 100644 index 0000000000000..40c1cc0f399e9 --- /dev/null +++ b/src/css/rules/supports.zig @@ -0,0 +1,18 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const Error = css.Error; + +pub const SupportsCondition = union(enum) { + pub fn parse(input: *css.Parser) Error!SupportsCondition { + _ = input; // autofix + } + + pub fn parseDeclaration(input: *css.Parser) Error!SupportsCondition { + _ = input; // autofix + } +}; diff --git a/src/css/rules/unknown.zig b/src/css/rules/unknown.zig new file mode 100644 index 0000000000000..c6c9b06c638e8 --- /dev/null +++ b/src/css/rules/unknown.zig @@ -0,0 +1,51 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const css_values = @import("../values/values.zig"); +pub const Error = css.Error; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +/// An unknown at-rule, stored as raw tokens. +pub const UnknownAtRule = struct { + /// The name of the at-rule (without the @). + name: []const u8, + /// The prelude of the rule. + prelude: css.TokenList, + /// The contents of the block, if any. + block: ?css.TokenList, + /// The location of the rule in the source file. + loc: css.Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeChar('@'); + try dest.writeStr(this.name); + + if (this.prelude.items.len > 0) { + try dest.writeChar(' '); + try this.prelude.toCss(W, dest, false); + } + + if (this.block) |*block| { + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + block.toCss(W, dest, false); + dest.dedent(); + try dest.newline(); + dest.writeChar('}'); + } else { + try dest.writeChar(';'); + } + } +}; diff --git a/src/css/selector.zig b/src/css/selector.zig new file mode 100644 index 0000000000000..7e0b8cf18e438 --- /dev/null +++ b/src/css/selector.zig @@ -0,0 +1,2885 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const Error = css.Error; + +pub const Printer = css.Printer; +pub const PrintErr = css.PrintErr; + +const ArrayList = std.ArrayListUnmanaged; + +pub const impl = struct { + pub const Selectors = struct { + pub const SelectorImpl = struct { + pub const AttrValue = css.css_values.string.CSSString; + pub const Identifier = css.css_values.ident.Ident; + pub const LocalName = css.css_values.ident.Ident; + pub const NamespacePrefix = css.css_values.ident.Ident; + pub const NamespaceUrl = []const u8; + pub const BorrowedNamespaceUrl = []const u8; + pub const BorrowedLocalName = css.css_values.ident.Ident; + + pub const NonTSPseudoClass = api.PseudoClass; + pub const PseudoElement = api.PseudoElement; + pub const VendorPrefix = css.VendorPrefix; + pub const ExtraMatchingData = void; + }; + }; +}; + +pub const api = struct { + pub const Selector = GenericSelector(impl.Selectors); + pub const SelectorList = GenericSelectorList(impl.Selectors); + + /// The definition of whitespace per CSS Selectors Level 3 § 4. + pub const SELECTOR_WHITESPACE: []const u8 = &u8{ ' ', '\t', '\n', '\r', 0x0C }; + + pub fn ValidSelectorImpl(comptime T: type) void { + _ = T.SelectorImpl.ExtraMatchingData; + _ = T.SelectorImpl.AttrValue; + _ = T.SelectorImpl.Identifier; + _ = T.SelectorImpl.LocalName; + _ = T.SelectorImpl.NamespaceUrl; + _ = T.SelectorImpl.NamespacePrefix; + _ = T.SelectorImpl.BorrowedNamespaceUrl; + _ = T.SelectorImpl.BorrowedLocalName; + + _ = T.SelectorImpl.NonTSsSeudoClass; + _ = T.SelectorImpl.VendorPrefix; + _ = T.SelectorImpl.PseudoElement; + } + + const selector_builder = struct { + /// Top-level SelectorBuilder struct. This should be stack-allocated by the + /// consumer and never moved (because it contains a lot of inline data that + /// would be slow to memmov). + /// + /// After instantiation, callers may call the push_simple_selector() and + /// push_combinator() methods to append selector data as it is encountered + /// (from left to right). Once the process is complete, callers should invoke + /// build(), which transforms the contents of the SelectorBuilder into a heap- + /// allocated Selector and leaves the builder in a drained state. + pub fn SelectorBuilder(comptime Impl: type) type { + ValidSelectorImpl(Impl); + + return struct { + /// The entire sequence of simple selectors, from left to right, without combinators. + /// + /// We make this large because the result of parsing a selector is fed into a new + /// Arc-ed allocation, so any spilled vec would be a wasted allocation. Also, + /// Components are large enough that we don't have much cache locality benefit + /// from reserving stack space for fewer of them. + /// + /// todo_stuff.smallvec + simple_selectors: ArrayList(GenericComponent(Impl)), + + /// The combinators, and the length of the compound selector to their left. + /// + /// todo_stuff.smallvec + combinators: ArrayList(struct { Combinator, usize }), + + /// The length of the current compound selector. + current_len: usize, + + const This = @This(); + + pub fn default() This {} + + /// Returns true if combinators have ever been pushed to this builder. + pub inline fn hasCombinators(this: *This) bool { + return this.combinators.items.len > 0; + } + + /// Completes the current compound selector and starts a new one, delimited + /// by the given combinator. + pub inline fn pushCombinator(this: *This, combinator: Combinator) void { + this.combinators.append(@compileError(css.todo_stuff.think_about_allocator), .{ combinator, this.current_len }) catch unreachable; + this.current_len = 0; + } + + /// Pushes a simple selector onto the current compound selector. + pub fn pushSimpleSelector(this: *This, ss: GenericComponent(Impl)) void { + bun.assert(!ss.isCombinator()); + this.simple_selectors.append(@compileError(css.todo_stuff.think_about_allocator), ss) catch unreachable; + this.current_len += 1; + } + + pub fn addNestingPrefix(this: *This) void { + this.combinators.insert(@compileError(css.todo_stuff.think_about_allocator), 0, struct { Combinator.descendant, 1 }) catch unreachable; + this.simple_selectors.insert(@compileError(css.todo_stuff.think_about_allocator), 0, .nesting); + } + + /// Consumes the builder, producing a Selector. + /// + /// *NOTE*: This will free all allocated memory in the builder + /// TODO: deallocate unused memory after calling this + pub fn build( + this: *This, + parsed_pseudo: bool, + parsed_slotted: bool, + parsed_part: bool, + ) struct { + specifity_and_flags: SpecifityAndFlags, + components: ArrayList(GenericComponent(Impl)), + } { + { + @compileError(css.todo_stuff.think_mem_mgmt); + } + const specifity = compute_specifity(this.simple_selectors.items); + var flags = SelectorFlags.empty(); + // PERF: is it faster to do these ORs all at once + if (parsed_pseudo) { + flags.has_pseudo = true; + } + if (parsed_slotted) { + flags.has_slotted = true; + } + if (parsed_part) { + flags.has_part = true; + } + return this.buildWithSpecificityAndFlags(SpecifityAndFlags{ .specificity = specifity, .flags = flags }); + } + + // TODO: make sure this is correct transliteration of the unsafe Rust original + pub fn buildWithSpecificityAndFlags(this: *This, spec: SpecifityAndFlags) struct { + specifity_and_flags: SpecifityAndFlags, + components: ArrayList(GenericComponent(Impl)), + } { + const T = GenericComponent(Impl); + const rest: []const T, const current: []const T = splitFromEnd(T, this.simple_selectors.items, this.current_len); + const combinators = this.combinators.items; + defer { + // set len from end for this.simple_selectors here + this.simple_selectors.items.len = 0; + // clear retaining combinators + this.combinators.items.len = 0; + } + + var components = ArrayList(T){}; + + var current_simple_selectors_i: usize = 0; + var combinator_i: i64 = @intCast(this.combinators.items.len - 1); + var rest_of_simple_selectors = rest; + var current_simple_selectors = current; + + while (true) { + if (current_simple_selectors_i < current.len) { + components.append( + @compileError(css.todo_stuff.think_about_allocator), + current_simple_selectors[current_simple_selectors_i], + ) catch unreachable; + current_simple_selectors_i += 1; + } else { + if (combinator_i >= 0) { + const combo: Combinator, const len: usize = combinators[combinator_i]; + const rest2, const current2 = splitFromEnd(GenericComponent(Impl), rest_of_simple_selectors, len); + rest_of_simple_selectors = rest2; + current_simple_selectors_i = 0; + current_simple_selectors = current2; + combinator_i -= 1; + components.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .combinator = combo }, + ) catch unreachable; + } + break; + } + } + + return .{ .specifity_and_flags = spec, .components = components }; + } + + pub fn splitFromEnd(comptime T: type, s: []const T, at: usize) struct { []const T, []const T } { + const midpoint = s.len - at; + return .{ + s[0..midpoint], + s[midpoint..], + }; + } + }; + } + }; + + pub const attrs = struct { + pub fn AttrSelectorWithOptionalNamespace(comptime Impl: type) type { + return struct { + namespace: ?NamespaceConstraint(struct { + prefix: Impl.SelectorImpl.NamespacePrefix, + url: Impl.SelectorImpl.NamespaceUrl, + }), + local_name: Impl.SelectorImpl.LocalName, + local_name_lower: Impl.SelectorImpl.LocalName, + operation: ParsedAttrSelectorOperation(Impl.SelectorImpl.AttrValue), + never_matches: bool, + }; + } + + pub fn NamespaceConstraint(comptime NamespaceUrl: type) type { + return union(enum) { + any, + /// Empty string for no namespace + specific: NamespaceUrl, + }; + } + + pub fn ParsedAttrSelectorOperation(comptime AttrValue: type) type { + return union(enum) { + exists, + with_value: struct { + operator: AttrSelectorOperator, + case_sensitivity: ParsedCaseSensitivity, + expected_value: AttrValue, + }, + }; + } + + pub const AttrSelectorOperator = enum { + equal, + includes, + dash_match, + prefix, + substring, + suffix, + }; + + pub const AttrSelectorOperation = enum { + equal, + includes, + dash_match, + prefix, + substring, + suffix, + }; + + pub const ParsedCaseSensitivity = enum { + // 's' was specified. + explicit_case_sensitive, + // 'i' was specified. + ascii_case_insensitive, + // No flags were specified and HTML says this is a case-sensitive attribute. + case_sensitive, + // No flags were specified and HTML says this is a case-insensitive attribute. + ascii_case_insensitive_if_in_html_element, + }; + }; + + pub const Specifity = struct { + id_selectors: u32 = 0, + class_like_selectors: u32 = 0, + element_selectors: u32 = 0, + + const MAX_10BIT: u32 = (1 << 10) - 1; + + pub fn toU32(this: Specifity) u32 { + return (@min(this.id_selectors, MAX_10BIT) << 20) | + (@min(this.class_like_selectors, MAX_10BIT) << 10) | + @min(this.element_selectors, MAX_10BIT); + } + + pub fn fromU32(value: u32) Specifity { + bun.assert(value <= MAX_10BIT << 20 | MAX_10BIT << 10 | MAX_10BIT); + return Specifity{ + .id_selectors = value >> 20, + .class_like_selectors = (value >> 10) & MAX_10BIT, + .element_selectors = value & MAX_10BIT, + }; + } + + pub fn add(lhs: *Specifity, rhs: Specifity) void { + lhs.id_selectors += rhs.id_selectors; + lhs.element_selectors += rhs.element_selectors; + lhs.class_like_selectors += rhs.class_like_selectors; + } + }; + + fn compute_specifity(comptime Impl: type, iter: []const GenericComponent(Impl)) u32 { + const spec = compute_complex_selector_specifity(Impl, iter); + return spec.toU32(); + } + + fn compute_complex_selector_specifity(comptime Impl: type, iter: []const GenericComponent(Impl)) Specifity { + var specifity: Specifity = .{}; + + for (iter) |*simple_selector| { + compute_simple_selector_specifity(Impl, simple_selector, &specifity); + } + } + + fn compute_simple_selector_specifity( + comptime Impl: type, + simple_selector: *const GenericComponent(Impl), + specifity: *Specifity, + ) void { + switch (simple_selector.*) { + .combinator => { + bun.unreachablePanic("Found combinator in simple selectors vector?", .{}); + }, + .part, .pseudo_element, .local_name => { + specifity.element_selectors += 1; + }, + .slotted => |selector| { + specifity.element_selectors += 1; + // Note that due to the way ::slotted works we only compete with + // other ::slotted rules, so the above rule doesn't really + // matter, but we do it still for consistency with other + // pseudo-elements. + // + // See: https://github.com/w3c/csswg-drafts/issues/1915 + specifity.add(selector.specifity()); + }, + .host => |maybe_selector| { + specifity.class_like_selectors += 1; + if (maybe_selector) |*selector| { + // See: https://github.com/w3c/csswg-drafts/issues/1915 + specifity.add(selector.specifity()); + } + }, + .id => { + specifity.id_selectors += 1; + }, + .class, + .attribute_in_no_namespace, + .attribute_in_no_namespace_exists, + .attribute_other, + .root, + .empty, + .scope, + .nth, + .non_ts_pseudo_class, + => { + specifity.class_like_selectors += 1; + }, + .nth_of => |nth_of_data| { + // https://drafts.csswg.org/selectors/#specificity-rules: + // + // The specificity of the :nth-last-child() pseudo-class, + // like the :nth-child() pseudo-class, combines the + // specificity of a regular pseudo-class with that of its + // selector argument S. + specifity.class_like_selectors += 1; + var max: u32 = 0; + for (nth_of_data.selectors) |*selector| { + max = @max(selector.specifity(), max); + } + specifity.add(Specifity.fromU32(max)); + }, + .negation, .is, .any => { + // https://drafts.csswg.org/selectors/#specificity-rules: + // + // The specificity of an :is() pseudo-class is replaced by the + // specificity of the most specific complex selector in its + // selector list argument. + const list: []GenericSelector(Impl) = switch (simple_selector.*) { + .negation => |list| list, + .is => |list| list, + .any => |a| a.selectors, + else => unreachable, + }; + var max: u32 = 0; + for (list) |*selector| { + max = @max(selector.specifity(), max); + } + specifity.add(Specifity.fromU32(max)); + }, + .where, + .has, + .explicit_universal_type, + .explicit, + .any_namespace, + .explicit_no_namespace, + .default_namespace, + .namespace, + => { + // Does not affect specifity + }, + .nesting => { + // TODO + }, + } + } + + const SelectorBuilder = selector_builder.SelectorBuilder; + + /// Build up a Selector. + /// selector : simple_selector_sequence [ combinator simple_selector_sequence ]* ; + /// + /// `Err` means invalid selector. + fn parse_selector( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + nesting_requirement: NestingRequirement, + ) Error!GenericSelector(Impl) { + if (nesting_requirement == .prefixed) { + const parser_state = input.state(); + if (!(if (input.expectDelim('&')) |_| true else false)) { + // todo_stuff.errors + return input.newCustomError(.missing_nesting_prefix); + } + input.reset(&parser_state); + } + + var builder = selector_builder.SelectorBuilder(Impl).default(); + errdefer { + @compileError(css.todo_stuff.think_mem_mgmt); + } + + outer_loop: while (true) { + // Parse a sequence of simple selectors. + const empty = try parse_compound_selector(parser, state, input, &builder); + if (empty) { + const kind: SelectorParseErrorKind = if (builder.hasCombinators()) + .dangling_combinator + else + .empty_selector; + + // todo_stuff.errors + return input.newCustomError(kind); + } + + if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { + break; + } + + // Parse a combinator + var combinator: Combinator = undefined; + var any_whitespace = false; + while (true) { + const before_this_token = input.state(); + const tok: *css.Token = input.nextIncludingWhitespace() catch break :outer_loop; + switch (tok.*) { + .whitespace => { + any_whitespace = true; + continue; + }, + .delim => |d| { + switch (d) { + '>' => { + continue; + }, + '+' => { + continue; + }, + '~' => { + continue; + }, + '/' => { + if (parser.deepCombinatorEnabled()) { + continue; + } + }, + } + }, + else => {}, + } + + input.reset(&before_this_token); + if (any_whitespace) { + combinator = .descendant; + break; + } else { + break :outer_loop; + } + } + + if (!state.allowsCombinators()) { + return input.newCustomError(.invalid_state); + } + + builder.pushCombinator(combinator); + } + + if (!state.contains(SelectorParsingState{ .after_nesting = true })) { + switch (nesting_requirement) { + .implicit => { + builder.addNestingPrefix(); + }, + .contained, .prefixed => { + // todo_stuff.errors + return input.newCustomError(SelectorParseErrorKind.missing_nesting_selector); + }, + else => {}, + } + } + + const has_pseudo_element = state.intersects(SelectorParsingState{ + .after_pseudo_element = true, + .after_unknown_pseudo_element = true, + }); + const slotted = state.intersects(SelectorParsingState{ .after_slotted = true }); + const part = state.intersects(SelectorParsingState{ .after_part = true }); + const result = builder.build(has_pseudo_element, slotted, part); + return Selector{ + .specifity_and_flags = result.specifity_and_flags, + .components = result.components, + }; + } + + /// simple_selector_sequence + /// : [ type_selector | universal ] [ HASH | class | attrib | pseudo | negation ]* + /// | [ HASH | class | attrib | pseudo | negation ]+ + /// + /// `Err(())` means invalid selector. + /// `Ok(true)` is an empty selector + fn parse_compound_selector( + comptime Impl: type, + parser: *SelectorParser, + state: *SelectorParsingState, + input: *css.Parser, + builder: *SelectorBuilder(Impl), + ) Error!bool { + input.skipWhitespace(); + + var empty: bool = true; + if (parser.isNestingAllowed() and if (input.tryParse(css.Parser.expectDelim, .{'&'})) |_| true else false) { + state.insert(SelectorParsingState{ .after_nesting = true }); + builder.pushSimpleSelector(.nesting); + empty = false; + } + + if (try parse_type_selector(Impl, parser, input, state.*, builder)) { + empty = false; + } + + while (true) { + const result: SimpleSelectorParseResult(Impl) = if (try parse_one_simple_selector(Impl, parser, input, state)) |result| result else break; + + if (empty) { + if (parser.defaultNamespace()) |url| { + // If there was no explicit type selector, but there is a + // default namespace, there is an implicit "|*" type + // selector. Except for :host() or :not() / :is() / :where(), + // where we ignore it. + // + // https://drafts.csswg.org/css-scoping/#host-element-in-tree: + // + // When considered within its own shadow trees, the shadow + // host is featureless. Only the :host, :host(), and + // :host-context() pseudo-classes are allowed to match it. + // + // https://drafts.csswg.org/selectors-4/#featureless: + // + // A featureless element does not match any selector at all, + // except those it is explicitly defined to match. If a + // given selector is allowed to match a featureless element, + // it must do so while ignoring the default namespace. + // + // https://drafts.csswg.org/selectors-4/#matches + // + // Default namespace declarations do not affect the compound + // selector representing the subject of any selector within + // a :is() pseudo-class, unless that compound selector + // contains an explicit universal selector or type selector. + // + // (Similar quotes for :where() / :not()) + // + const ignore_default_ns = state.intersects(SelectorParsingState{ .skip_default_namespace = true }) or + (result == .simple_selector and result.simple_selector == .host); + if (!ignore_default_ns) { + builder.pushSimpleSelector(.{ .default_namespace = url }); + } + } + } + + empty = false; + + switch (result) { + .simple_selector => { + builder.pushSimpleSelector(result.simple_selector); + }, + .part_pseudo => { + const selector = result.part_pseudo; + state.insert(SelectorParsingState{ .after_part = true }); + builder.pushCombinator(.part); + builder.pushSimpleSelector(.{ .slotted = selector }); + }, + .slotted_pseudo => |selector| { + state.insert(.{ .after_slotted = true }); + builder.pushCombinator(.slot_assignment); + builder.pushSimpleSelector(.{ .slotted = selector }); + }, + .pseudo_element => |p| { + if (!p.isUnknown()) { + state.insert(SelectorParsingState{ .after_pseudo_element = true }); + builder.pushCombinator(.pseudo_element); + } else { + state.insert(.{ .after_unknown_pseudo_element = true }); + } + + if (!p.acceptsStatePseudoClasses()) { + state.insert(.{ .after_non_stateful_pseudo_element = true }); + } + + if (p.isWebkitScrollbar()) { + state.insert(.{ .after_webkit_scrollbar = true }); + } + + if (p.isViewTransition()) { + state.insert(.{ .after_view_transition = true }); + } + + builder.pushSimpleSelector(.{ .pseudo_element = p }); + }, + } + } + + return empty; + } + + fn parse_relative_selector( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + nesting_requirement_: NestingRequirement, + ) Error!GenericSelector(Impl) { + // https://www.w3.org/TR/selectors-4/#parse-relative-selector + var nesting_requirement = nesting_requirement_; + const s = input.state(); + + const combinator: ?Combinator = combinator: { + switch ((try input.next()).*) { + .delim => |c| { + switch (c) { + '>' => break :combinator Combinator.child, + '+' => break :combinator Combinator.next_sibling, + '~' => break :combinator Combinator.later_sibling, + else => {}, + } + }, + } + input.reset(&s); + break :combinator null; + }; + + const scope: GenericComponent(Impl) = if (nesting_requirement == .implicit) .nesting else .scope; + + if (combinator != null) { + nesting_requirement = .none; + } + + var selector = try parse_selector(Impl, parser, input, state, nesting_requirement); + if (combinator) |wombo_combo| { + // https://www.w3.org/TR/selectors/#absolutizing + selector.components.append( + @compileError(css.todo_stuff.think_about_allocator), + .{ .combinator = wombo_combo }, + ) catch unreachable; + selector.components.append( + @compileError(css.todo_stuff.think_about_allocator), + scope, + ) catch unreachable; + } + + return selector; + } + + pub fn ValidSelectorParser(comptime T: type) type { + ValidSelectorImpl(T.SelectorParser.Impl); + + // Whether to parse the `::slotted()` pseudo-element. + _ = T.SelectorParser.parseSlotted; + + _ = T.SelectorParser.parsePart; + + _ = T.SelectorParser.parseIsAndWhere; + + _ = T.SelectorParser.isAndWhereErrorRecovery; + + _ = T.SelectorParser.parseAnyPrefix; + + _ = T.SelectorParser.parseHost; + + _ = T.SelectorParser.parseNonTsPseudoClass; + + _ = T.SelectorParser.parseNonTsFunctionalPseudoClass; + + _ = T.SelectorParser.parsePseudoElement; + + _ = T.SelectorParser.parseFunctionalPseudoElement; + + _ = T.SelectorParser.defaultNamespace; + + _ = T.SelectorParser.namespaceForPrefix; + + _ = T.SelectorParser.isNestingAllowed; + + _ = T.SelectorParser.deepCombinatorEnabled; + } + + pub const Direction = css.DefineEnumProperty(struct { + comptime { + @compileError(css.todo_stuff.enum_property); + } + }); + + /// A pseudo class. + pub const PseudoClass = union(enum) { + /// https://drafts.csswg.org/selectors-4/#linguistic-pseudos + /// The [:lang()](https://drafts.csswg.org/selectors-4/#the-lang-pseudo) pseudo class. + lang: struct { + /// A list of language codes. + languages: ArrayList([]const u8), + }, + /// The [:dir()](https://drafts.csswg.org/selectors-4/#the-dir-pseudo) pseudo class. + dir: struct { + /// A direction. + direction: Direction, + }, + + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + /// The [:hover](https://drafts.csswg.org/selectors-4/#the-hover-pseudo) pseudo class. + hover, + /// The [:active](https://drafts.csswg.org/selectors-4/#the-active-pseudo) pseudo class. + active, + /// The [:focus](https://drafts.csswg.org/selectors-4/#the-focus-pseudo) pseudo class. + focus, + /// The [:focus-visible](https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo) pseudo class. + focus_visible, + /// The [:focus-within](https://drafts.csswg.org/selectors-4/#the-focus-within-pseudo) pseudo class. + focus_within, + + /// https://drafts.csswg.org/selectors-4/#time-pseudos + /// The [:current](https://drafts.csswg.org/selectors-4/#the-current-pseudo) pseudo class. + current, + /// The [:past](https://drafts.csswg.org/selectors-4/#the-past-pseudo) pseudo class. + past, + /// The [:future](https://drafts.csswg.org/selectors-4/#the-future-pseudo) pseudo class. + future, + + /// https://drafts.csswg.org/selectors-4/#resource-pseudos + /// The [:playing](https://drafts.csswg.org/selectors-4/#selectordef-playing) pseudo class. + playing, + /// The [:paused](https://drafts.csswg.org/selectors-4/#selectordef-paused) pseudo class. + paused, + /// The [:seeking](https://drafts.csswg.org/selectors-4/#selectordef-seeking) pseudo class. + seeking, + /// The [:buffering](https://drafts.csswg.org/selectors-4/#selectordef-buffering) pseudo class. + buffering, + /// The [:stalled](https://drafts.csswg.org/selectors-4/#selectordef-stalled) pseudo class. + stalled, + /// The [:muted](https://drafts.csswg.org/selectors-4/#selectordef-muted) pseudo class. + muted, + /// The [:volume-locked](https://drafts.csswg.org/selectors-4/#selectordef-volume-locked) pseudo class. + volume_locked, + + /// The [:fullscreen](https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class) pseudo class. + fullscreen: css.VendorPrefix, + + /// https://drafts.csswg.org/selectors/#display-state-pseudos + /// The [:open](https://drafts.csswg.org/selectors/#selectordef-open) pseudo class. + open, + /// The [:closed](https://drafts.csswg.org/selectors/#selectordef-closed) pseudo class. + closed, + /// The [:modal](https://drafts.csswg.org/selectors/#modal-state) pseudo class. + modal, + /// The [:picture-in-picture](https://drafts.csswg.org/selectors/#pip-state) pseudo class. + picture_in_picture, + + /// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open + /// The [:popover-open](https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open) pseudo class. + popover_open, + + /// The [:defined](https://drafts.csswg.org/selectors-4/#the-defined-pseudo) pseudo class. + defined, + + /// https://drafts.csswg.org/selectors-4/#location + /// The [:any-link](https://drafts.csswg.org/selectors-4/#the-any-link-pseudo) pseudo class. + any_link: css.VendorPrefix, + /// The [:link](https://drafts.csswg.org/selectors-4/#link-pseudo) pseudo class. + link, + /// The [:local-link](https://drafts.csswg.org/selectors-4/#the-local-link-pseudo) pseudo class. + local_link, + /// The [:target](https://drafts.csswg.org/selectors-4/#the-target-pseudo) pseudo class. + target, + /// The [:target-within](https://drafts.csswg.org/selectors-4/#the-target-within-pseudo) pseudo class. + taget_within, + /// The [:visited](https://drafts.csswg.org/selectors-4/#visited-pseudo) pseudo class. + visited, + + /// https://drafts.csswg.org/selectors-4/#input-pseudos + /// The [:enabled](https://drafts.csswg.org/selectors-4/#enabled-pseudo) pseudo class. + enabled, + /// The [:disabled](https://drafts.csswg.org/selectors-4/#disabled-pseudo) pseudo class. + disabled, + /// The [:read-only](https://drafts.csswg.org/selectors-4/#read-only-pseudo) pseudo class. + read_only: css.VendorPrefix, + /// The [:read-write](https://drafts.csswg.org/selectors-4/#read-write-pseudo) pseudo class. + read_write: css.VendorPrefix, + /// The [:placeholder-shown](https://drafts.csswg.org/selectors-4/#placeholder) pseudo class. + placeholder_shown: css.VendorPrefix, + /// The [:default](https://drafts.csswg.org/selectors-4/#the-default-pseudo) pseudo class. + default, + /// The [:checked](https://drafts.csswg.org/selectors-4/#checked) pseudo class. + checked, + /// The [:indeterminate](https://drafts.csswg.org/selectors-4/#indeterminate) pseudo class. + indeterminate, + /// The [:blank](https://drafts.csswg.org/selectors-4/#blank) pseudo class. + blank, + /// The [:valid](https://drafts.csswg.org/selectors-4/#valid-pseudo) pseudo class. + valid, + /// The [:invalid](https://drafts.csswg.org/selectors-4/#invalid-pseudo) pseudo class. + invalid, + /// The [:in-range](https://drafts.csswg.org/selectors-4/#in-range-pseudo) pseudo class. + in_range, + /// The [:out-of-range](https://drafts.csswg.org/selectors-4/#out-of-range-pseudo) pseudo class. + out_of_range, + /// The [:required](https://drafts.csswg.org/selectors-4/#required-pseudo) pseudo class. + required, + /// The [:optional](https://drafts.csswg.org/selectors-4/#optional-pseudo) pseudo class. + optional, + /// The [:user-valid](https://drafts.csswg.org/selectors-4/#user-valid-pseudo) pseudo class. + user_valid, + /// The [:used-invalid](https://drafts.csswg.org/selectors-4/#user-invalid-pseudo) pseudo class. + user_invalid, + + /// The [:autofill](https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill) pseudo class. + autofill: css.VendorPrefix, + + // CSS modules + /// The CSS modules :local() pseudo class. + local: struct { + /// A local selector. + selector: *Selector, + }, + /// The CSS modules :global() pseudo class. + global: struct { + /// A global selector. + selector: *Selector, + }, + + /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo class. + // https://webkit.org/blog/363/styling-scrollbars/ + webkit_scrollbar: WebKitScrollbarPseudoClass, + /// An unknown pseudo class. + custom: struct { + /// The pseudo class name. + name: []const u8, + }, + /// An unknown functional pseudo class. + custom_function: struct { + /// The pseudo class name. + name: []const u8, + /// The arguments of the pseudo class function. + arguments: css.TokenList, + }, + }; + + /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo class. + pub const WebKitScrollbarPseudoClass = enum { + /// :horizontal + horizontal, + /// :vertical + vertical, + /// :decrement + decrement, + /// :increment + increment, + /// :start + start, + /// :end + end, + /// :double-button + double_button, + /// :single-button + single_button, + /// :no-button + no_button, + /// :corner-present + corner_present, + /// :window-inactive + window_inactive, + }; + + /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo element. + pub const WebKitScrollbarPseudoElement = enum { + /// ::-webkit-scrollbar + scrollbar, + /// ::-webkit-scrollbar-button + button, + /// ::-webkit-scrollbar-track + track, + /// ::-webkit-scrollbar-track-piece + track_piece, + /// ::-webkit-scrollbar-thumb + thumb, + /// ::-webkit-scrollbar-corner + corner, + /// ::-webkit-resizer + resizer, + }; + + pub const SelectorParser = struct { + is_nesting_allowed: bool, + options: *const css.ParserOptions, + + pub const Impl = impl.Selectors; + + pub fn namespaceForPrefix(this: *SelectorParser, prefix: css.css_values.ident.Ident) ?[]const u8 { + _ = this; // autofix + return prefix; + } + + pub fn parseNonTsPseudoClass( + this: *SelectorParser, + loc: css.SourceLocation, + name: []const u8, + ) Error!PseudoClass { + // @compileError(css.todo_stuff.match_ignore_ascii_case); + const pseudo_class: PseudoClass = pseudo_class: { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "hover")) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + break :pseudo_class .hover; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "active")) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + break :pseudo_class .active; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "focus")) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + break :pseudo_class .focus; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "focus-visible")) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + break :pseudo_class .focus_visible; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "focus-within")) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + break :pseudo_class .focus_within; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "current")) { + // https://drafts.csswg.org/selectors-4/#time-pseudos + break :pseudo_class .current; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "past")) { + // https://drafts.csswg.org/selectors-4/#time-pseudos + break :pseudo_class .past; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "future")) { + // https://drafts.csswg.org/selectors-4/#time-pseudos + break :pseudo_class .future; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "playing")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .playing; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "paused")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .paused; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "seeking")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .seeking; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "buffering")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .buffering; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "stalled")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .stalled; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "muted")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .muted; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "volume-locked")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .volume_locked; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "fullscreen")) { + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + break :pseudo_class .{ .fullscreen = .none }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-full-screen")) { + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + break :pseudo_class .{ .fullscreen = .webkit }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-full-screen")) { + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + break :pseudo_class .{ .fullscreen = .moz_document }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-fullscreen")) { + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + break :pseudo_class .{ .fullscreen = .ms }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "open")) { + // https://drafts.csswg.org/selectors/#display-state-pseudos + break :pseudo_class .open; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "closed")) { + // https://drafts.csswg.org/selectors/#display-state-pseudos + break :pseudo_class .closed; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "modal")) { + // https://drafts.csswg.org/selectors/#display-state-pseudos + break :pseudo_class .modal; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "picture-in-picture")) { + // https://drafts.csswg.org/selectors/#display-state-pseudos + break :pseudo_class .picture_in_picture; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "popover-open")) { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open + break :pseudo_class .popover_open; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "defined")) { + // https://drafts.csswg.org/selectors-4/#the-defined-pseudo + break :pseudo_class .defined; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "any-link")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .{ .any_link = .none }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-any-link")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .{ .any_link = .webkit }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-any-link")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .{ .any_link = .moz }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "link")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .link; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "local-link")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .local_link; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "target")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .target; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "target-within")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .target_within; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "visited")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .visited; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "enabled")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .enabled; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "disabled")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .disabled; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "read-only")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .read_only = .none }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-read-only")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .read_only = .moz }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "read-write")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .read_write = .none }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-read-write")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .read_write = .moz }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "placeholder-shown")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .placeholder_shown = .none }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-placeholder-shown")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .placeholder_shown = .moz }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-placeholder-shown")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .placeholder_shown = .ms }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "default")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .default; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "checked")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .checked; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "indeterminate")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .indeterminate; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "blank")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .blank; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "valid")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .valid; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "invalid")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .invalid; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "in-range")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .in_range; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "out-of-range")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .out_of_range; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "required")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .required; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "optional")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .optional; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "user-valid")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .user_valid; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "user-invalid")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .user_invalid; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "autofill")) { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + break :pseudo_class .{ .autofill = .none }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-autofill")) { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + break :pseudo_class .{ .autofill = .webkit }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-o-autofill")) { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + break :pseudo_class .{ .autofill = .o }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "horizontal")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .horizontal }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "vertical")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .vertical }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "decrement")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .decrement }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "increment")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .increment }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "start")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .start }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "end")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .end }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "double-button")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .double_button }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "single-button")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .single_button }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "no-button")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .no_button }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "corner-present")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .corner_present }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "window-inactive")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .window_inactive }; + } else { + if (bun.strings.startsWithChar(name, '_')) { + this.options.warn(loc.newCustomError(SelectorParseErrorKind{ .unsupported_pseudo_class_or_element = name })); + } + return PseudoClass{ .custom = name }; + } + }; + + return pseudo_class; + } + + pub fn parseNonTsFunctionalPseudoClass( + this: *SelectorParser, + name: []const u8, + parser: *css.Parser, + ) Error!PseudoClass { + + // todo_stuff.match_ignore_ascii_case + const pseudo_class = pseudo_class: { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "lang")) { + const languages = try parser.parseCommaSeparated([]const u8, css.Parser.expectIdentOrString); + return PseudoClass{ + .lang = .{ .languages = languages }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "dir")) { + break :pseudo_class PseudoClass{ + .dir = .{ + .direction = try Direction.parse(parser), + }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "local") and this.options.css_modules != null) { + break :pseudo_class PseudoClass{ + .local = .{ + .selector = brk: { + const selector = try Selector.parse(); + const alloc: Allocator = { + @compileError(css.todo_stuff.think_about_allocator); + }; + + const sel = alloc.create(Selector) catch unreachable; + sel.* = selector; + break :brk sel; + }, + }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "global") and this.options.css_modules != null) { + break :pseudo_class PseudoClass{ + .global = .{ + .selector = brk: { + const selector = try Selector.parse(); + const alloc: Allocator = { + @compileError(css.todo_stuff.think_about_allocator); + }; + + const sel = alloc.create(Selector) catch unreachable; + sel.* = selector; + break :brk sel; + }, + }, + }; + } else { + if (!bun.strings.startsWithChar(name, '-')) { + this.options.warn(parser.newCustomError(SelectorParseErrorKind{ .unsupported_pseudo_class_or_element = name })); + } + var args = ArrayList(css.css_properties.custom.TokenOrValue){}; + _ = try css.TokenListFns.parseRaw(parser, &args, this.options, 0); + break :pseudo_class PseudoClass{ + .custom_function = .{ + .name = name, + .arguments = args, + }, + }; + } + }; + + return pseudo_class; + } + + pub fn isNestingAllowed(this: *SelectorParser) bool { + return this.is_nesting_allowed; + } + + pub fn deepCombinatorEnabled(this: *SelectorParser) bool { + return this.options.flags.contains(css.ParserFlags{ .deep_selector_combinator = true }); + } + + pub fn defaultNamespace(this: *SelectorParser) ?impl.Selectors.SelectorImpl.NamespaceUrl { + _ = this; // autofix + return null; + } + + pub fn parsePart(this: *SelectorParser) bool { + _ = this; // autofix + return true; + } + + pub fn parseSlotted(this: *SelectorParser) bool { + _ = this; // autofix + return true; + } + + /// The error recovery that selector lists inside :is() and :where() have. + fn isAndWhereErrorRecovery(this: *SelectorParser) ParseErrorRecovery { + _ = this; // autofix + return .ignore_invalid_selector; + } + + pub fn parsePseudoElement(this: *SelectorParser, loc: css.SourceLocation, name: []const u8) Error!PseudoElement { + const pseudo_element: PseudoElement = pseudo_element: { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "before")) { + break :pseudo_element .before; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "after")) { + break :pseudo_element .after; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-line")) { + break :pseudo_element .first_line; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-letter")) { + break :pseudo_element .first_letter; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "cue")) { + break :pseudo_element .cue; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "cue-region")) { + break :pseudo_element .cue_region; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "selection")) { + break :pseudo_element .{ .selection = .none }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-selection")) { + break :pseudo_element .{ .selection = .moz }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "placeholder")) { + break :pseudo_element .{ .placeholder = .none }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-input-placeholder")) { + break :pseudo_element .{ .placeholder = .webkit }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-placeholder")) { + break :pseudo_element .{ .placeholder = .moz }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-input-placeholder")) { + // this is a bugin hte source + break :pseudo_element .{ .placeholder = .ms }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "marker")) { + break :pseudo_element .maker; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "backdrop")) { + break :pseudo_element .{ .backdrop = .none }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-backdrop")) { + break :pseudo_element .{ .backdrop = .webkit }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "file-selector-button")) { + break :pseudo_element .{ .file_selector_button = .none }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-file-upload-button")) { + break :pseudo_element .{ .file_selector_button = .webkit }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-browse")) { + break :pseudo_element .{ .file_selector_button = .ms }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-scrollbar")) { + break :pseudo_element .{ .webkit_scrollbar = .scrollbar }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-scrollbar-button")) { + break :pseudo_element .{ .webkit_scrollbar = .button }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-scrollbar-track")) { + break :pseudo_element .{ .webkit_scrollbar = .track }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-scrollbar-track-piece")) { + break :pseudo_element .{ .webkit_scrollbar = .track_piece }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-scrollbar-thumb")) { + break :pseudo_element .{ .webkit_scrollbar = .thumb }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-scrollbar-corner")) { + break :pseudo_element .{ .webkit_scrollbar = .corner }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-resizer")) { + break :pseudo_element .{ .webkit_scrollbar = .resizer }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "view-transition")) { + break :pseudo_element .view_transition; + } else { + if (bun.strings.startsWith(name, '-')) { + this.options.warn(loc.newCustomError(SelectorParseErrorKind{ .unsupported_pseudo_class_or_element = name })); + } + return PseudoElement{ .custom = name }; + } + }; + + return pseudo_element; + } + }; + + pub fn GenericSelectorList(comptime Impl: type) type { + ValidSelectorImpl(Impl); + + const SelectorT = GenericSelector(Impl); + return struct { + // PERF: make this equivalent to SmallVec<[Selector; 1]> + v: ArrayList(SelectorT) = .{}, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn parse( + parser: *SelectorParser, + input: *css.Parser, + error_recovery: ParseErrorRecovery, + nesting_requirement: NestingRequirement, + ) Error!This { + var state = SelectorParsingState.empty(); + return parseWithState(parser, input, &state, error_recovery, nesting_requirement); + } + + pub fn parseRelative( + parser: *SelectorParser, + input: *css.Parser, + error_recovery: ParseErrorRecovery, + nesting_requirement: NestingRequirement, + ) Error!This { + var state = SelectorParsingState.empty(); + return parseRelativeWithState(parser, input, &state, error_recovery, nesting_requirement); + } + + pub fn parseWithState( + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + recovery: ParseErrorRecovery, + nesting_requirement: NestingRequirement, + ) Error!This { + const original_state = state.*; + // TODO: Think about deinitialization in error cases + var values = ArrayList(SelectorT){}; + + while (true) { + const Closure = struct { + outer_state: *SelectorParsingState, + original_state: SelectorParsingState, + nesting_requirement: NestingRequirement, + + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!SelectorT { + var selector_state = this.original_state; + const result = parse_selector(Impl, parser, input2, &selector_state, this.nesting_requirement); + if (selector_state.after_nesting) { + this.outer_state.after_nesting = true; + } + return result; + } + }; + var closure = Closure{ + .outer_state = state, + .original_state = original_state, + .nesting_requirement = nesting_requirement, + }; + const selector = input.parseUntilBefore(css.Delimiters{ .comma = true }, SelectorT, &closure, Closure.parsefn); + + const was_ok = if (selector) true else false; + if (selector) |sel| { + values.append(comptime { + @compileError("TODO: Think about where Allocator comes from"); + }, sel) catch bun.outOfMemory(); + } else |e| { + switch (recovery) { + .discard_list => return e, + .ignore_invalid_selector => {}, + } + } + + while (true) { + if (input.next()) |tok| { + if (tok == .comma) break; + // Shouldn't have got a selector if getting here. + bun.debugAssert(!was_ok); + } + return .{ .v = values }; + } + } + } + + // TODO: this looks exactly the same as `parseWithState()` except it uses `parse_relative_selector()` instead of `parse_selector()` + pub fn parseRelativeWithState( + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + recovery: ParseErrorRecovery, + nesting_requirement: NestingRequirement, + ) Error!This { + const original_state = state.*; + // TODO: Think about deinitialization in error cases + var values = ArrayList(SelectorT){}; + + while (true) { + const Closure = struct { + outer_state: *SelectorParsingState, + original_state: SelectorParsingState, + nesting_requirement: NestingRequirement, + + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!SelectorT { + var selector_state = this.original_state; + const result = parse_relative_selector(Impl, parser, input2, &selector_state, this.nesting_requirement); + if (selector_state.after_nesting) { + this.outer_state.after_nesting = true; + } + return result; + } + }; + var closure = Closure{ + .outer_state = state, + .original_state = original_state, + .nesting_requirement = nesting_requirement, + }; + const selector = input.parseUntilBefore(css.Delimiters{ .comma = true }, SelectorT, &closure, Closure.parsefn); + + const was_ok = if (selector) true else false; + if (selector) |sel| { + values.append(comptime { + @compileError("TODO: Think about where Allocator comes from"); + }, sel) catch bun.outOfMemory(); + } else |e| { + switch (recovery) { + .discard_list => return e, + .ignore_invalid_selector => {}, + } + } + + while (true) { + if (input.next()) |tok| { + if (tok == .comma) break; + // Shouldn't have got a selector if getting here. + bun.debugAssert(!was_ok); + } + return .{ .v = values }; + } + } + } + + pub fn fromSelector(allocator: Allocator, selector: GenericSelector(Impl)) This { + var result = This{}; + result.v.append(allocator, selector) catch unreachable; + return result; + } + }; + } + + pub fn GenericSelector(comptime Impl: type) type { + ValidSelectorImpl(Impl); + + return struct { + specifity_and_flags: SpecifityAndFlags, + components: ArrayList(GenericComponent(Impl)), + + const This = @This(); + + pub fn fromComponent(component: GenericComponent(Impl)) This { + var builder = SelectorBuilder(Impl).default(); + if (component.asCombinator()) |combinator| { + builder.pushCombinator(combinator); + } else { + builder.pushSimpleSelector(component); + } + const result = builder.build(false, false, false); + return This{ + .specifity_and_flags = result.specifity_and_flags, + .components = result.components, + }; + } + + pub fn specifity(this: *const This) u32 { + this.specifity_and_flags.specificity; + } + + /// Parse a selector, without any pseudo-element. + pub fn parse(parser: *SelectorParser, input: *css.Parser) Error!This { + var state = SelectorParsingState.empty(); + return parse_selector(Impl, parser, input, &state, .none); + } + }; + } + + /// A CSS simple selector or combinator. We store both in the same enum for + /// optimal packing and cache performance, see [1]. + /// + /// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1357973 + pub fn GenericComponent(comptime Impl: type) type { + ValidSelectorImpl(Impl); + + return union(enum) { + combinator: Combinator, + + explicit_any_namespace, + explicit_no_namespace, + default_namespace: Impl.SelectorImpl.NamespaceUrl, + namespace: struct { + prefix: Impl.SelectorImpl.NamespacePrefix, + url: Impl.SelectorImpl.NamespaceUrl, + }, + + explicit_universal_type, + local_name: LocalName(Impl), + + id: Impl.SelectorImpl.Identifier, + class: Impl.SelectorImpl.Identifier, + + attribute_in_no_namespace_exists: struct { + local_name: Impl.SelectorImpl.LocalName, + local_name_lower: Impl.SelectorImpl.LocalName, + }, + /// Used only when local_name is already lowercase. + attribute_in_no_namespace: struct { + local_name: Impl.SelectorImpl.LocalName, + operator: attrs.AttrSelectorOperator, + value: Impl.SelectorImpl.AttrValue, + case_sensitivity: attrs.ParsedCaseSensitivity, + never_matches: bool, + }, + /// Use a Box in the less common cases with more data to keep size_of::() small. + attribute_other: *attrs.AttrSelectorWithOptionalNamespace(Impl), + + /// Pseudo-classes + negation: []GenericSelector(Impl), + root, + empty, + scope, + nth: NthSelectorData, + nth_of: NthOfSelectorData(Impl), + non_ts_pseudo_class: Impl.SelectorImpl.NonTSPseudoClass, + /// The ::slotted() pseudo-element: + /// + /// https://drafts.csswg.org/css-scoping/#slotted-pseudo + /// + /// The selector here is a compound selector, that is, no combinators. + /// + /// NOTE(emilio): This should support a list of selectors, but as of this + /// writing no other browser does, and that allows them to put ::slotted() + /// in the rule hash, so we do that too. + /// + /// See https://github.com/w3c/csswg-drafts/issues/2158 + slotted: GenericSelector(Impl), + /// The `::part` pseudo-element. + /// https://drafts.csswg.org/css-shadow-parts/#part + part: []GenericSelector(Impl.SelectorImpl.Identifier), + /// The `:host` pseudo-class: + /// + /// https://drafts.csswg.org/css-scoping/#host-selector + /// + /// NOTE(emilio): This should support a list of selectors, but as of this + /// writing no other browser does, and that allows them to put :host() + /// in the rule hash, so we do that too. + /// + /// See https://github.com/w3c/csswg-drafts/issues/2158 + host: ?GenericSelector(Impl.SelectorImpl.Identifier), + /// The `:where` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#zero-matches + /// + /// The inner argument is conceptually a SelectorList, but we move the + /// selectors to the heap to keep Component small. + where: []GenericSelector(Impl), + /// The `:is` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#matches-pseudo + /// + /// Same comment as above re. the argument. + is: []GenericSelector(Impl), + any: struct { + vendor_prefix: Impl.SelectorImpl.VendorPrefix, + selectors: []GenericSelector(Impl), + }, + /// The `:has` pseudo-class. + /// + /// https://www.w3.org/TR/selectors/#relational + has: []GenericSelector(Impl), + /// An implementation-dependent pseudo-element selector. + pseudo_element: Impl.SelectorImpl.PseudoElement, + /// A nesting selector: + /// + /// https://drafts.csswg.org/css-nesting-1/#nest-selector + /// + /// NOTE: This is a lightningcss addition. + nesting, + + const This = @This(); + + pub fn asCombinator(this: *const This) ?Combinator { + if (this.* == .combinator) return this.combinator; + return null; + } + + pub fn convertHelper_is(s: []GenericSelector(Impl)) This { + return .{ .is = s }; + } + + pub fn convertHelper_where(s: []GenericSelector(Impl)) This { + return .{ .where = s }; + } + + pub fn convertHelper_any(s: []GenericSelector(Impl), prefix: Impl.SelectorImpl.VendorPrefix) This { + return .{ + .any = .{ + .vendor_prefix = prefix, + .selectors = s, + }, + }; + } + + /// Returns true if this is a combinator. + pub fn isCombinator(this: *This) bool { + return this.* == .combinator; + } + }; + } + + /// The properties that comprise an :nth- pseudoclass as of Selectors 3 (e.g., + /// nth-child(An+B)). + /// https://www.w3.org/TR/selectors-3/#nth-child-pseudo + pub const NthSelectorData = struct { + ty: NthType, + is_function: bool, + a: i32, + b: i32, + + /// Returns selector data for :only-{child,of-type} + pub fn only(of_type: bool) NthSelectorData { + return NthSelectorData{ + .ty = if (of_type) NthType.only_of_type else NthType.only_child, + .is_function = false, + .a = 0, + .b = 1, + }; + } + + /// Returns selector data for :first-{child,of-type} + pub fn first(of_type: bool) NthSelectorData { + return NthSelectorData{ + .ty = if (of_type) NthType.of_type else NthType.child, + .is_function = false, + .a = 0, + .b = 1, + }; + } + + /// Returns selector data for :last-{child,of-type} + pub fn last(of_type: bool) NthSelectorData { + return NthSelectorData{ + .ty = if (of_type) NthType.last_of_type else NthType.last_child, + .is_function = false, + .a = 0, + .b = 1, + }; + } + }; + + /// The properties that comprise an :nth- pseudoclass as of Selectors 4 (e.g., + /// nth-child(An+B [of S]?)). + /// https://www.w3.org/TR/selectors-4/#nth-child-pseudo + pub fn NthOfSelectorData(comptime Impl: type) type { + return struct { + data: NthSelectorData, + selectors: []GenericSelector(Impl), + }; + } + + pub const SelectorParsingState = packed struct(u16) { + /// Whether we should avoid adding default namespaces to selectors that + /// aren't type or universal selectors. + skip_default_namespace: bool = false, + + /// Whether we've parsed a ::slotted() pseudo-element already. + /// + /// If so, then we can only parse a subset of pseudo-elements, and + /// whatever comes after them if so. + after_slotted: bool = false, + + /// Whether we've parsed a ::part() pseudo-element already. + /// + /// If so, then we can only parse a subset of pseudo-elements, and + /// whatever comes after them if so. + after_part: bool = false, + + /// Whether we've parsed a pseudo-element (as in, an + /// `Impl::PseudoElement` thus not accounting for `::slotted` or + /// `::part`) already. + /// + /// If so, then other pseudo-elements and most other selectors are + /// disallowed. + after_pseudo_element: bool = false, + + /// Whether we've parsed a non-stateful pseudo-element (again, as-in + /// `Impl::PseudoElement`) already. If so, then other pseudo-classes are + /// disallowed. If this flag is set, `AFTER_PSEUDO_ELEMENT` must be set + /// as well. + after_non_stateful_pseudo_element: bool = false, + + /// Whether we explicitly disallow combinators. + disallow_combinators: bool = false, + + /// Whether we explicitly disallow pseudo-element-like things. + disallow_pseudos: bool = false, + + /// Whether we have seen a nesting selector. + after_nesting: bool = false, + + after_webkit_scrollbar: bool = false, + after_view_transition: bool = false, + after_unknown_pseudo_element: bool = false, + + /// Whether we are after any of the pseudo-like things. + pub const AFTER_PSEUDO = css.Bitflags.bitwiseOr(.{ + SelectorParsingState{ .after_part = true }, + SelectorParsingState{ .after_slotted = true }, + SelectorParsingState{ .after_pseudo_element = true }, + }); + + pub inline fn empty() SelectorParsingState { + return .{}; + } + + pub fn intersects(self: SelectorParsingState, other: SelectorParsingState) bool { + _ = other; // autofix + _ = self; // autofix + css.todo("SelectorParsingState.intersects", .{}); + } + + pub fn insert(self: *SelectorParsingState, other: SelectorParsingState) void { + _ = self; // autofix + _ = other; // autofix + css.todo("SelectorParsingState.insert", .{}); + } + + pub fn allowsPseudos(this: SelectorParsingState) bool { + _ = this; // autofix + css.todo("SelectorParsingState.allowsPseudos", .{}); + } + + pub fn allowsPart(this: SelectorParsingState) bool { + _ = this; // autofix + css.todo("SelectorParsingState.allowsPart", .{}); + } + + pub fn allowsSlotted(this: SelectorParsingState) bool { + _ = this; // autofix + css.todo("SelectorParsingState.allowsSlotted", .{}); + } + + pub fn allowsTreeStructuralPseudoClasses(this: SelectorParsingState) bool { + return !this.intersects(SelectorParsingState.AFTER_PSEUDO); + } + + pub fn allowsNonFunctionalPseudoClasses(this: SelectorParsingState) bool { + return !this.intersects(SelectorParsingState{ .after_slotted = true, .after_non_stateful_pseudo_element = true }); + } + }; + + pub const SpecifityAndFlags = struct { + /// There are two free bits here, since we use ten bits for each specificity + /// kind (id, class, element). + specificity: u32, + /// There's padding after this field due to the size of the flags. + flags: SelectorFlags, + }; + + pub const SelectorFlags = packed struct(u8) { + has_pseudo: bool = false, + has_slotted: bool = false, + has_part: bool = false, + __unused: u5 = 0, + }; + + /// How to treat invalid selectors in a selector list. + pub const ParseErrorRecovery = enum { + /// Discard the entire selector list, this is the default behavior for + /// almost all of CSS. + discard_list, + /// Ignore invalid selectors, potentially creating an empty selector list. + /// + /// This is the error recovery mode of :is() and :where() + ignore_invalid_selector, + }; + + pub const NestingRequirement = enum { + none, + prefixed, + contained, + implicit, + }; + + pub const Combinator = enum { + child, // > + descendant, // space + next_sibling, // + + later_sibling, // ~ + /// A dummy combinator we use to the left of pseudo-elements. + /// + /// It serializes as the empty string, and acts effectively as a child + /// combinator in most cases. If we ever actually start using a child + /// combinator for this, we will need to fix up the way hashes are computed + /// for revalidation selectors. + pseudo_element, + /// Another combinator used for ::slotted(), which represent the jump from + /// a node to its assigned slot. + slot_assignment, + /// Another combinator used for `::part()`, which represents the jump from + /// the part to the containing shadow host. + part, + + /// Non-standard Vue >>> combinator. + /// https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors + deep_descendant, + /// Non-standard /deep/ combinator. + /// Appeared in early versions of the css-scoping-1 specification: + /// https://www.w3.org/TR/2014/WD-css-scoping-1-20140403/#deep-combinator + /// And still supported as an alias for >>> by Vue. + deep, + }; + + pub const SelectorParseErrorKind = union(enum) { + invalid_state, + class_needs_ident: css.Token, + pseudo_element_expected_ident: css.Token, + unsupported_pseudo_class_or_element: []const u8, + no_qualified_name_in_attribute_selector: css.Token, + unexpected_token_in_attribute_selector: css.Token, + invalid_qual_name_in_attr: css.Token, + expected_bar_in_attr: css.Token, + empty_selector, + dangling_combinator, + invalid_pseudo_class_before_webkit_scrollbar, + invalid_pseudo_class_after_webkit_scrollbar, + invalid_pseudo_class_after_pseudo_element, + missing_nesting_selector, + missing_nesting_prefix, + expected_namespace: []const u8, + bad_value_in_attr: css.Token, + explicit_namespace_unexpected_token: css.Token, + unexpected_ident: []const u8, + }; + + pub fn SimpleSelectorParseResult(comptime Impl: type) type { + ValidSelectorImpl(Impl); + + return union(enum) { + simple_selector: GenericComponent(Impl), + pseudo_element: Impl.PseudoElement, + slotted_pseudo: GenericSelector(Impl), + // todo_stuff.think_mem_mgmt + part_pseudo: []Impl.Identifier, + }; + } + + /// A pseudo element. + pub const PseudoElement = union(enum) { + /// The [::after](https://drafts.csswg.org/css-pseudo-4/#selectordef-after) pseudo element. + after, + /// The [::before](https://drafts.csswg.org/css-pseudo-4/#selectordef-before) pseudo element. + before, + /// The [::first-line](https://drafts.csswg.org/css-pseudo-4/#first-line-pseudo) pseudo element. + first_line, + /// The [::first-letter](https://drafts.csswg.org/css-pseudo-4/#first-letter-pseudo) pseudo element. + first_letter, + /// The [::selection](https://drafts.csswg.org/css-pseudo-4/#selectordef-selection) pseudo element. + selection: css.VendorPrefix, + /// The [::placeholder](https://drafts.csswg.org/css-pseudo-4/#placeholder-pseudo) pseudo element. + placeholder: css.VendorPrefix, + /// The [::marker](https://drafts.csswg.org/css-pseudo-4/#marker-pseudo) pseudo element. + marker, + /// The [::backdrop](https://fullscreen.spec.whatwg.org/#::backdrop-pseudo-element) pseudo element. + backdrop: css.VendorPrefix, + /// The [::file-selector-button](https://drafts.csswg.org/css-pseudo-4/#file-selector-button-pseudo) pseudo element. + file_selector_button: css.VendorPrefix, + /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo element. + webkit_scrollbar: WebKitScrollbarPseudoElement, + /// The [::cue](https://w3c.github.io/webvtt/#the-cue-pseudo-element) pseudo element. + cue, + /// The [::cue-region](https://w3c.github.io/webvtt/#the-cue-region-pseudo-element) pseudo element. + cue_region, + /// The [::cue()](https://w3c.github.io/webvtt/#cue-selector) functional pseudo element. + cue_function: struct { + /// The selector argument. + selector: *Selector, + }, + /// The [::cue-region()](https://w3c.github.io/webvtt/#cue-region-selector) functional pseudo element. + cue_region_function: struct { + /// The selector argument. + selector: *Selector, + }, + /// The [::view-transition](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition) pseudo element. + view_transition, + /// The [::view-transition-group()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-group-pt-name-selector) functional pseudo element. + view_transition_group: struct { + /// A part name selector. + part_name: ViewTransitionPartName, + }, + /// The [::view-transition-image-pair()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-image-pair-pt-name-selector) functional pseudo element. + view_transition_image_pair: struct { + /// A part name selector. + part_name: ViewTransitionPartName, + }, + /// The [::view-transition-old()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-old-pt-name-selector) functional pseudo element. + view_transition_old: struct { + /// A part name selector. + part_name: ViewTransitionPartName, + }, + /// The [::view-transition-new()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-new-pt-name-selector) functional pseudo element. + view_transition_new: struct { + /// A part name selector. + part_name: ViewTransitionPartName, + }, + /// An unknown pseudo element. + custom: struct { + /// The name of the pseudo element. + name: []const u8, + }, + /// An unknown functional pseudo element. + custom_function: struct { + /// The name of the pseudo element. + name: []const u8, + /// The arguments of the pseudo element function. + arguments: css.TokenList, + }, + + pub fn acceptsStatePseudoClasses(this: *const PseudoElement) bool { + _ = this; // autofix + // Be lienient. + return true; + } + }; + + /// An enum for the different types of :nth- pseudoclasses + pub const NthType = enum { + child, + last_child, + only_child, + of_type, + last_of_type, + only_of_type, + col, + last_col, + + pub fn isOnly(self: NthType) bool { + return self == NthType.only_child or self == NthType.only_of_type; + } + + pub fn isOfType(self: NthType) bool { + return self == NthType.of_type or self == NthType.last_of_type or self == NthType.only_of_type; + } + + pub fn isFromEnd(self: NthType) bool { + return self == NthType.last_child or self == NthType.last_of_type or self == NthType.last_col; + } + + pub fn allowsOfSelector(self: NthType) bool { + return self == NthType.child or self == NthType.last_child; + } + }; + + /// * `Err(())`: Invalid selector, abort + /// * `Ok(false)`: Not a type selector, could be something else. `input` was not consumed. + /// * `Ok(true)`: Length 0 (`*|*`), 1 (`*|E` or `ns|*`) or 2 (`|E` or `ns|E`) + pub fn parse_type_selector( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: SelectorParsingState, + sink: *SelectorBuilder(Impl), + ) Error!bool { + const result = parse_qualified_name( + Impl, + parser, + input, + false, + ) catch |e| { + _ = e; // autofix + + // TODO: error does not exist + // but it should exist + // todo_stuff.errors + // if (e == Error.EndOfInput) + // this is not complete + // needs to check if error is EndOfInput and return false + // otherwise return error + return false; + }; + + if (result == .none) return false; + + const namespace: QNamePrefix(Impl) = result.some[0]; + const local_name: ?[]const u8 = result.some[1]; + if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { + return input.newCustomError(SelectorParseErrorKind.invalid_state); + } + + switch (namespace) { + .implicit_any_namespace => {}, + .implicit_default_namespace => |url| { + sink.pushSimpleSelector(.{ .default_namespace = url }); + }, + .explicit_namespace => { + const prefix = namespace.explicit_namespace[0]; + const url = namespace.explicit_namespace[1]; + const component = component: { + if (parser.defaultNamespace()) |default_url| { + if (bun.strings.eql(url, default_url)) { + break :component .{ .default_namespace = url }; + } + } + break :component .{ + .namespace = .{ + .prefix = prefix, + .url = url, + }, + }; + }; + sink.pushSimpleSelector(component); + }, + .explicit_no_namespace => { + sink.pushSimpleSelector(.explicit_no_namespace); + }, + .explicit_any_namespace => { + // Element type selectors that have no namespace + // component (no namespace separator) represent elements + // without regard to the element's namespace (equivalent + // to "*|") unless a default namespace has been declared + // for namespaced selectors (e.g. in CSS, in the style + // sheet). If a default namespace has been declared, + // such selectors will represent only elements in the + // default namespace. + // -- Selectors § 6.1.1 + // So we'll have this act the same as the + // QNamePrefix::ImplicitAnyNamespace case. + // For lightning css this logic was removed, should be handled when matching. + sink.pushSimpleSelector(.explicit_any_namespace); + }, + .implicit_no_namespace => { + bun.unreachablePanic("Should not be returned with in_attr_selector = false", .{}); + }, + } + + if (local_name) |name| { + sink.pushSimpleSelector(.{ + .local_name = LocalName{ + .lower_name = brk: { + const alloc: std.mem.Allocator = { + @compileError(css.todo_stuff.think_about_allocator); + }; + var lowercase = alloc.alloc(u8, name.len) catch unreachable; + bun.strings.copyLowercase(name, lowercase[0..]); + break :brk lowercase; + }, + .name = name, + }, + }); + } else { + sink.pushSimpleSelector(.explicit_universal_type); + } + + return true; + } + + /// Parse a simple selector other than a type selector. + /// + /// * `Err(())`: Invalid selector, abort + /// * `Ok(None)`: Not a simple selector, could be something else. `input` was not consumed. + /// * `Ok(Some(_))`: Parsed a simple selector or pseudo-element + pub fn parse_one_simple_selector( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + ) Error!(?SimpleSelectorParseResult(Impl)) { + const S = SimpleSelectorParseResult(Impl); + + const start = input.state(); + const token = (input.nextIncludingWhitespace() catch { + input.reset(start); + return null; + }).*; + + switch (token) { + .idhash => |id| { + if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { + return input.newCustomError(SelectorParseErrorKind.invalid_state); + } + const component: GenericComponent(Impl) = .{ .id = id }; + return S{ + .simple_selector = component, + }; + }, + .open_square => { + if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { + return input.newCustomError(SelectorParseErrorKind.invalid_state); + } + const Closure = struct { + parser: *SelectorParser, + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!GenericComponent(Impl) { + return try parse_attribute_selector(Impl, this.parser, input2); + } + }; + var closure = Closure{ + .parser = parser, + }; + const attr = try input.parseNestedBlock(GenericComponent(Impl), &closure, Closure.parsefn); + return .{ .simple_selector = attr }; + }, + .colon => { + const location = input.currentSourceLocation(); + const is_single_colon: bool, const next_token: css.Token = switch ((try input.nextIncludingWhitespace()).*) { + .colon => .{ false, (try input.nextIncludingWhitespace()).* }, + else => |t| .{ true, t }, + }; + const name: []const u8, const is_functional = switch (next_token) { + .ident => |name| .{ name, false }, + .function => |name| .{ name, true }, + else => |t| { + const e = SelectorParseErrorKind{ .pseudo_element_expected_ident = t }; + return input.newCustomError(e); + }, + }; + const is_pseudo_element = !is_single_colon or is_css2_pseudo_element(name); + if (is_pseudo_element) { + if (!state.allowsPseudos()) { + return input.newCustomError(SelectorParseErrorKind.invalid_state); + } + const pseudo_element: Impl.SelectorImpl.PseudoElement = if (is_functional) pseudo_element: { + if (parser.parsePart() and bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "part")) { + if (!state.allowsPart()) { + return input.newCustomError(SelectorParseErrorKind.invalid_state); + } + + const Fn = struct { + pub fn parsefn(_: void, input2: *css.Parser) Error![]Impl.SelectorImpl.Identifier { + // todo_stuff.think_about_mem_mgmt + var result = ArrayList(Impl.SelectorImpl.Identifier).initCapacity( + @compileError(css.todo_stuff.think_about_allocator), + // TODO: source does this, should see if initializing to 1 is actually better + // when appending empty std.ArrayList(T), it will usually initially reserve 8 elements, + // maybe that's unnecessary, or maybe smallvec is gud here + 1, + ) catch unreachable; + + result.append( + @compileError(css.todo_stuff.think_about_allocator), + try input2.expectIdent(), + ) catch unreachable; + + while (!input.isExhausted()) { + result.append( + @compileError(css.todo_stuff.think_about_allocator), + try input.expectIdent(), + ) catch unreachable; + } + + return result.items; + } + }; + + const names = try input.parseNestedBlock([]Impl.SelectorImpl.Identifier, {}, Fn.parsefn); + + break :pseudo_element .{ .part_pseudo = names }; + } + + if (parser.parseSlotted() and bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "slotted")) { + if (!state.allowsSlotted()) { + return input.newCustomError(SelectorParseErrorKind.invalid_state); + } + const Closure = struct { + parser: *SelectorParser, + state: *SelectorParsingState, + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!GenericSelector(Impl) { + return parse_inner_compound_selector(this.parser, input2, this.state); + } + }; + var closure = Closure{ + .parser = parser, + .state = state, + }; + const selector = try input.parseNestedBlock(GenericSelector(Impl), &closure, Closure.parsefn); + return .{ .slotted_pseudo = selector }; + } + } else pseudo_element: { + break :pseudo_element try parser.parsePseudoElement(location, name); + }; + + if (state.intersects(.{ .after_slotted = true }) and pseudo_element.validAfterSlotted()) { + return .{ .pseudo_element = pseudo_element }; + } + } else { + const pseudo_class: GenericComponent(Impl) = if (is_functional) pseudo_class: { + const Closure = struct { + parser: *SelectorParser, + name: []const u8, + state: *SelectorParsingState, + pub fn parsefn(this: *@This(), input2: *css.Parser) Error!GenericComponent(Impl) { + return try parse_functional_pseudo_class(Impl, this.parser, input2, this.name, this.state); + } + }; + var closure = Closure{ + .parser = parser, + .name = name, + .state = state, + }; + + break :pseudo_class try input.parseNestedBlock(GenericComponent(Impl), &closure, Closure.parsefn); + } else try parse_simple_pseudo_class(Impl, parser, location, name, state.*); + return .{ .simple_selector = pseudo_class }; + } + }, + .delim => |d| { + switch (d) { + '.' => { + if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { + return input.newCustomError(SelectorParseErrorKind.invalid_state); + } + const location = input.currentSourceLocation(); + const class = switch ((try input.nextIncludingWhitespace()).*) { + .ident => |class| class, + else => |t| { + const e = SelectorParseErrorKind{ .class_needs_ident = t }; + return location.newCustomError(e); + }, + }; + const component_class = .{ .class = class }; + return .{ .simple_selector = component_class }; + }, + '&' => { + if (parser.isNestingAllowed()) { + state.insert(SelectorParsingState{ .after_nesting = true }); + return S{ + .simple_selector = .nesting, + }; + } + }, + } + }, + else => {}, + } + + input.reset(&start); + return null; + } + + pub fn parse_attribute_selector(comptime Impl: type, parser: *SelectorParser, input: *css.Parser) Error!GenericComponent(Impl) { + const alloc: std.mem.Allocator = { + @compileError(css.todo_stuff.think_about_allocator); + }; + + const N = attrs.NamespaceConstraint(struct { + prefix: Impl.SelectorImpl.NamespacePrefix, + url: Impl.SelectorImpl.NamespaceUrl, + }); + + const namespace: ?N, const local_name: []const u8 = brk: { + input.skipWhitespace(); + + switch (try parse_qualified_name(Impl, parser, input, true)) { + .none => |t| return input.newCustomError(SelectorParseErrorKind{ .no_qualified_name_in_attribute_selector = t }), + .some => |qname| { + if (qname[1] == null) { + bun.unreachablePanic("", .{}); + } + const ns: QNamePrefix(Impl) = qname[0]; + const ln = qname[1].?; + break :brk .{ + switch (ns) { + .implicit_no_namespace, .explicit_no_namespace => null, + .explicit_namespace => |x| .{ .specific = .{ .prefix = x[0], .url = x[1] } }, + .explicit_any_namespace => .any, + .implicit_any_namespace, .implicit_default_namespace => { + bun.unreachablePanic("Not returned with in_attr_selector = true", .{}); + }, + }, + ln, + }; + }, + } + }; + + const location = input.currentSourceLocation(); + const operator = operator: { + const tok = input.next() catch |e| { + _ = e; // autofix + const local_name_lower = local_name_lower: { + const lower = alloc.alloc(u8, local_name.len) catch unreachable; + _ = bun.strings.copyLowercase(local_name, lower); + break :local_name_lower lower; + }; + if (namespace) |ns| { + return brk: { + const x = attrs.AttrSelectorWithOptionalNamespace(Impl){ + .namespace = ns, + .local_name = local_name, + .local_name_lower = local_name_lower, + .never_matches = false, + .operation = .exists, + }; + const v = alloc.create(@TypeOf(x)) catch unreachable; + v.* = x; + break :brk v; + }; + } else { + return .{ + .attribute_in_no_namespace_exists = .{ + .local_name = local_name, + .local_name_lower = local_name_lower, + }, + }; + } + }; + switch (tok.*) { + // [foo=bar] + .delim => |d| { + if (d == '=') break :operator .equal; + }, + // [foo~=bar] + .include_match => break :operator .includes, + // [foo|=bar] + .dash_match => break :operator .dash_match, + // [foo^=bar] + .prefix_match => break :operator .prefix, + // [foo*=bar] + .substring_match => break :operator .substring, + // [foo$=bar] + .suffix_match => break :operator .suffix, + else => {}, + } + return location.newCustomError(SelectorParseErrorKind{ .unexpected_token_in_attribute_selector = tok.* }); + }; + + const value_str: []const u8 = (input.expectIdentOrString() catch |e| { + _ = e; // autofix + @compileError(css.todo_stuff.errors); + }).*; + const never_matches = switch (operator) { + .equal, .dash_match => false, + .includes => value_str.len == 0 or std.mem.indexOfAny(u8, value_str, SELECTOR_WHITESPACE), + .prefix, .substring, .suffix => value_str.len == 0, + }; + + const attribute_flags = try parse_attribute_flags(input); + + const value: Impl.SelectorImpl.AttrValue = value_str; + const local_name_lower: Impl.SelectorImpl.LocalName, const local_name_is_ascii_lowercase: bool = brk: { + if (a: { + for (local_name, 0..) |b, i| { + if (b >= 'A' and b <= 'Z') break :a i; + } + break :a null; + }) |first_uppercase| { + const str = local_name[first_uppercase..]; + const lower = alloc.alloc(u8, str.len) catch unreachable; + break :brk .{ bun.strings.copyLowercase(str, lower), false }; + } else { + break :brk .{ local_name, true }; + } + }; + const case_sensitivity: attrs.ParsedCaseSensitivity = attribute_flags.toCaseSensitivity(local_name_lower, namespace != null); + if (namespace != null and !local_name_is_ascii_lowercase) { + return .{ + .attribute_other = brk: { + const x = attrs.AttrSelectorWithOptionalNamespace(Impl){ + .namespace = namespace, + .local_name = local_name, + .local_name_lower = local_name_lower, + .never_matches = never_matches, + .operation = .{ + .with_value = .{ + .operator = operator, + .case_sensitivity = case_sensitivity, + .expected_value = value, + }, + }, + }; + const v = alloc.create(@TypeOf(x)) catch unreachable; + v.* = x; + break :brk v; + }, + }; + } else { + return .{ + .attribute_in_no_namespace = .{ + .local_name = local_name, + .operator = operator, + .value = value, + .case_sensitivity = case_sensitivity, + .never_matches = never_matches, + }, + }; + } + } + + /// Returns whether the name corresponds to a CSS2 pseudo-element that + /// can be specified with the single colon syntax (in addition to the + /// double-colon syntax, which can be used for all pseudo-elements). + pub fn is_css2_pseudo_element(name: []const u8) bool { + // ** Do not add to this list! ** + // TODO: todo_stuff.match_ignore_ascii_case + return bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "before") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "after") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-line") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-letter"); + } + + /// Parses one compound selector suitable for nested stuff like :-moz-any, etc. + pub fn parse_inner_compound_selector( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + ) Error!GenericSelector(Impl) { + var child_state = brk: { + var child_state = state.*; + child_state.disallow_pseudos = true; + child_state.disallow_combinators = true; + break :brk child_state; + }; + const result = try parse_selector(Impl, parser, input, &child_state, NestingRequirement.none); + if (child_state.after_nesting) { + state.after_nesting = true; + } + return result; + } + + pub fn parse_functional_pseudo_class( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + name: []const u8, + state: *SelectorParsingState, + ) Error!GenericComponent(Impl) { + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-child")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .child); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-of-type")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .of_type); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-last-child")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .last_child); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-last-of-type")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .last_of_type); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-col")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .col); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-last-col")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .last_col); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "is") and parser.parseIsAndWhere()) { + return parse_is_or_where(Impl, parser, input, state.*, GenericComponent(Impl).convertHelper_is, .{}); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "where") and parser.parseIsAndWhere()) { + return parse_is_or_where(Impl, parser, input, state.*, GenericComponent(Impl).convertHelper_where, .{}); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "has")) { + return parse_has(Impl, parser, input, state); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "host")) { + if (!state.allowsTreeStructuralPseudoClasses()) { + return input.newCustomError(SelectorParseErrorKind.invalid_state); + } + return .{ + .host = try parse_inner_compound_selector(Impl, parser, input, state), + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "not")) { + return parse_negation(Impl, parser, input, state); + } else { + // + } + + if (parser.parseAnyPrefix(name)) |prefix| { + return parse_is_or_where(Impl, parser, input, state, GenericComponent(Impl).convertHelper_any, .{prefix}); + } + + if (!state.allowsCustomFunctionalPseudoClasses()) { + return input.newCustomError(SelectorParseErrorKind.invalid_state); + } + + const result = try parser.parseNonTsFunctionalPseudoClass(Impl, name, input); + return .{ .non_ts_pseudo_class = result }; + } + + pub fn parse_simple_pseudo_class( + comptime Impl: type, + parser: *SelectorParser, + location: css.SourceLocation, + name: []const u8, + state: SelectorParsingState, + ) Error!GenericComponent(Impl) { + if (state.allowsNonFunctionalPseudoClasses()) { + return location.newCustomError(SelectorParseErrorKind.invalid_state); + } + + if (state.allowsTreeStructuralPseudoClasses()) { + // css.todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-child")) { + return .{ .nth = NthSelectorData.first(false) }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "last-child")) { + return .{ .nth = NthSelectorData.last(false) }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "only-child")) { + return .{ .nth = NthSelectorData.only(false) }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "root")) { + return .root; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "empty")) { + return .empty; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "scope")) { + return .scope; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "host")) { + return .{ .host = null }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-of-type")) { + return .{ .nth = NthSelectorData.first(true) }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "last-of-type")) { + return .{ .nth = NthSelectorData.last(true) }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "only-of-type")) { + return .{ .nth = NthSelectorData.only(true) }; + } else {} + } + + // The view-transition pseudo elements accept the :only-child pseudo class. + // https://w3c.github.io/csswg-drafts/css-view-transitions-1/#pseudo-root + if (state.intersects(SelectorParsingState{ .after_view_transition = true })) { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "only-child")) { + return .{ .nth = NthSelectorData.only(false) }; + } + } + + const pseudo_class = try parser.parseNonTsPseudoClass(location, name); + if (state.intersects(SelectorParsingState{ .after_webkit_scrollbar = true })) { + if (!pseudo_class.isValidAterWebkitScrollbar()) { + return location.newCustomError(SelectorParseErrorKind{ .invalid_pseudo_class_after_webkit_scrollbar = true }); + } + } else if (state.intersects(SelectorParsingState{ .after_pseudo_element = true })) { + if (!pseudo_class.isUserActionState()) { + return location.newCustomError(SelectorParseErrorKind{ .invalid_pseudo_class_after_pseudo_element = true }); + } + } else if (!pseudo_class.isValidBeforeWebkitScrollbar()) { + return location.newCustomError(SelectorParseErrorKind{ .invalid_pseudo_class_before_webkit_scrollbar = true }); + } + + return .{ .non_ts_pseudo_class = pseudo_class }; + } + + pub fn parse_nth_pseudo_class( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: SelectorParsingState, + ty: NthType, + ) Error!GenericComponent(Impl) { + if (!state.allowsTreeStructuralPseudoClasses()) { + return input.newCustomError(SelectorParseErrorKind.invalid_state); + } + + const a, const b = try css.nth.parse_nth(input); + const nth_data = NthSelectorData{ + .ty = ty, + .is_function = true, + .a = a, + .b = b, + }; + + if (!ty.allowsOfSelector()) { + return .{ .nth = nth_data }; + } + + // Try to parse "of ". + input.tryParse(css.Parser.expectIdentMatching, .{"of"}) catch { + return .{ .nth = nth_data }; + }; + + // Whitespace between "of" and the selector list is optional + // https://github.com/w3c/csswg-drafts/issues/8285 + var child_state = child_state: { + var s = state; + s.skip_default_namespace = true; + s.disallow_pseudos = true; + break :child_state s; + }; + + const selectors = try SelectorList.parseWithState( + parser, + input, + &child_state, + .ignore_invalid_selector, + .none, + ); + + return .{ + .nth_of = NthOfSelectorData(Impl){ + .data = nth_data, + .selectors = selectors.v.items, + }, + }; + } + + /// `func` must be of the type: fn([]GenericSelector(Impl), ...@TypeOf(args_)) GenericComponent(Impl) + pub fn parse_is_or_where( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + comptime func: anytype, + args_: anytype, + ) Error!GenericComponent(Impl) { + bun.debugAssert(parser.parseIsAndWhere()); + // https://drafts.csswg.org/selectors/#matches-pseudo: + // + // Pseudo-elements cannot be represented by the matches-any + // pseudo-class; they are not valid within :is(). + // + var child_state = brk: { + var child_state = state.*; + child_state.skip_default_namespace = true; + child_state.disallow_pseudos = true; + break :brk child_state; + }; + + const inner = try SelectorList.parseWithState(parser, input, &child_state, parser.isAndWhereRecovery(), NestingRequirement.none); + if (child_state.after_nesting) { + state.after_nesting = true; + } + + const selector_slice = inner.v.items; + + const result = result: { + const args = brk: { + var args: std.meta.ArgsTuple(@TypeOf(func)) = undefined; + args[0] = selector_slice; + + inline for (args_, 1..) |a, i| { + args[i] = a; + } + + break :brk args; + }; + + break :result @call(.auto, func, args); + }; + + return result; + } + + pub fn parse_has( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + ) Error!GenericComponent(Impl) { + var child_state = state.*; + const inner = try SelectorList.parseRelativeWithState( + parser, + input, + &child_state, + parser.isAndWhereErrorRecovery(), + .none, + ); + + if (child_state.after_nesting) { + state.after_nesting = true; + } + return .{ .has = inner.v.items }; + } + + /// Level 3: Parse **one** simple_selector. (Though we might insert a second + /// implied "|*" type selector.) + pub fn parse_negation( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + ) Error!GenericComponent(Impl) { + var child_state = state.*; + child_state.skip_default_namespace = true; + child_state.disallow_pseudos = true; + + const list = try SelectorList.parseWithState(parser, input, &child_state, .discard_list, .none); + + if (child_state.after_nesting) { + state.after_nesting = true; + } + + return .{ .negation = list.v.items }; + } + + pub fn OptionalQName(comptime Impl: type) type { + return struct { + some: struct { QNamePrefix(Impl), ?[]const u8 }, + none: css.Token, + }; + } + + pub fn QNamePrefix(comptime Impl: type) type { + return union(enum) { + implicit_no_namespace, // `foo` in attr selectors + implicit_any_namespace, // `foo` in type selectors, without a default ns + implicit_default_namespace: Impl.SelectorImpl.NamespaceUrl, // `foo` in type selectors, with a default ns + explicit_no_namespace, // `|foo` + explicit_any_namespace, // `*|foo` + explicit_namespace: struct { Impl.SelectorImpl.NamespacePrefix, Impl.SelectorImpl.NamespaceUrl }, // `prefix|foo` + }; + } + + /// * `Err(())`: Invalid selector, abort + /// * `Ok(None(token))`: Not a simple selector, could be something else. `input` was not consumed, + /// but the token is still returned. + /// * `Ok(Some(namespace, local_name))`: `None` for the local name means a `*` universal selector + pub fn parse_qualified_name( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + in_attr_selector: bool, + ) Error!OptionalQName(Impl) { + const start = input.state(); + + const tok = input.nextIncludingWhitespace() catch |e| { + input.reset(&start); + return e; + }; + switch (tok.*) { + .ident => |value| { + const after_ident = input.state(); + const n = if (input.nextIncludingWhitespace()) |t| t == .delim and t.delim == '|' else false; + if (n) { + const prefix: Impl.SelectorImpl.NamespacePrefix = value; + const result: ?Impl.SelectorImpl.NamespaceUrl = parser.namespaceForPrefix(prefix); + const url: Impl.SelectorImpl.NamespaceUrl = try brk: { + if (result) break :brk result.*; + return input.newCustomError(SelectorParseErrorKind{ .unsupported_pseudo_class_or_element = value }); + }; + return parse_qualified_name_eplicit_namespace_helper( + Impl, + input, + .{ .explicit_namespace = .{ prefix, url } }, + in_attr_selector, + ); + } else { + input.reset(&after_ident); + if (in_attr_selector) return .{ .some = .{ .implicit_no_namespace, value } }; + return parse_qualified_name_default_namespace_helper(Impl, parser, value); + } + }, + .delim => |c| { + switch (c) { + '*' => { + const after_star = input.state(); + const result = input.nextIncludingWhitespace(); + if (result) |t| if (t == .delim and t.delim == '|') + return parse_qualified_name_eplicit_namespace_helper( + Impl, + input, + .explicit_any_namespace, + in_attr_selector, + ); + input.reset(after_star); + if (in_attr_selector) { + if (result) |t| { + return after_star.sourceLocation().newCustomError(SelectorParseErrorKind{ + .expected_bar_in_attr = t.*, + }); + } else |e| { + return e; + } + } else { + return parse_qualified_name_default_namespace_helper(Impl, parser, null); + } + }, + '|' => return parse_qualified_name_eplicit_namespace_helper(Impl, input, .explicit_no_namespace, in_attr_selector), + else => {}, + } + }, + else => {}, + } + input.reset(&start); + return .{ .none = tok.* }; + } + + fn parse_qualified_name_default_namespace_helper( + comptime Impl: type, + parser: *SelectorParser, + local_name: ?[]const u8, + ) OptionalQName(Impl) { + const namespace = if (parser.defaultNamespace()) |url| .{ .implicit_default_namespace = url } else .implicit_any_namespace; + return .{ + .some = .{ + namespace, + local_name, + }, + }; + } + + fn parse_qualified_name_eplicit_namespace_helper( + comptime Impl: type, + input: *css.Parser, + namespace: QNamePrefix(Impl), + in_attr_selector: bool, + ) Error!OptionalQName(Impl) { + const location = input.currentSourceLocation(); + const t = input.nextIncludingWhitespace() catch |e| return e; + switch (t) { + .ident => |local_name| return .{ .some = .{ namespace, local_name } }, + .delim => |c| { + if (c == '*') { + return .{ .some = .{ namespace, null } }; + } + }, + else => {}, + } + if (in_attr_selector) { + const e = SelectorParseErrorKind{ .invalid_qual_name_in_attr = t.* }; + return location.newCustomError(e); + } + return location.newCustomError(SelectorParseErrorKind{ .explicit_namespace_expected_token = t.* }); + } + + pub fn LocalName(comptime Impl: type) type { + return struct { + name: Impl.SelectorImpl.LocalName, + lower_name: Impl.SelectorImpl.LocalName, + }; + } + + /// An attribute selector can have 's' or 'i' as flags, or no flags at all. + pub const AttributeFlags = enum { + // Matching should be case-sensitive ('s' flag). + case_sensitive, + // Matching should be case-insensitive ('i' flag). + ascii_case_insensitive, + // No flags. Matching behavior depends on the name of the attribute. + case_sensitivity_depends_on_name, + + pub fn toCaseSensitivity(this: AttributeFlags, local_name: []const u8, have_namespace: bool) attrs.ParsedCaseSensitivity { + _ = local_name; // autofix + _ = have_namespace; // autofix + return switch (this) { + .case_sensitive => .explicit_case_sensitive, + .ascii_case_insensitive => .ascii_case_insensitive, + .case_sensitivity_depends_on_name => { + @compileError(css.todo_stuff.depth); + }, + }; + } + }; + + /// A [view transition part name](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector). + pub const ViewTransitionPartName = union(enum) { + /// * + all, + /// + name: css.css_values.ident.CustomIdent, + }; + + pub fn parse_attribute_flags(input: *css.Parser) Error!AttributeFlags { + const location = input.currentSourceLocation(); + const token = input.next() catch { + // Selectors spec says language-defined; HTML says it depends on the + // exact attribute name. + return AttributeFlags.case_sensitivity_depends_on_name; + }; + + const ident = if (token.* == .ident) token.ident else return location.newBasicUnexpectedTokenError(token.*); + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "i")) { + return AttributeFlags.ascii_case_insensitive; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "s")) { + return AttributeFlags.case_sensitive; + } else { + return location.newBasicUnexpectedTokenError(token.*); + } + } +}; diff --git a/src/css/selectors/parser.zig b/src/css/selectors/parser.zig new file mode 100644 index 0000000000000..a15cdfc96a785 --- /dev/null +++ b/src/css/selectors/parser.zig @@ -0,0 +1,8 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; + +pub fn ValidSelectorParser(comptime T: type) type { + _ = T.SelectorParser.Impl; +} diff --git a/src/css/sourcemap.zig b/src/css/sourcemap.zig new file mode 100644 index 0000000000000..57f5cab30ff9a --- /dev/null +++ b/src/css/sourcemap.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); +const DashedIdent = css_values.ident.DashedIdent; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; +const Location = css.Location; +const ArrayList = std.ArrayListUnmanaged; + +pub const SourceMap = struct { + project_root: []const u8, + inner: SourceMapInner, +}; + +pub const SourceMapInner = struct { + sources: ArrayList([]const u8), + sources_content: ArrayList([]const u8), + names: ArrayList([]const u8), + mapping_lines: ArrayList(MappingLine), +}; + +pub const MappingLine = struct { mappings: ArrayList(LineMapping), last_column: u32, is_sorted: bool }; + +pub const LineMapping = struct { generated_column: u32, original: ?OriginalLocation }; + +pub const OriginalLocation = struct { + original_line: u32, + original_column: u32, + source: u32, + name: ?u32, +}; diff --git a/src/css/targets.zig b/src/css/targets.zig new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/css/values/color.zig b/src/css/values/color.zig new file mode 100644 index 0000000000000..321549f08d62f --- /dev/null +++ b/src/css/values/color.zig @@ -0,0 +1,1110 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const Error = css.Error; + +const Percentage = css.css_values.percentage.Percentage; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const Angle = css.css_values.angle.Angle; + +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +/// A CSS `` value. +/// +/// CSS supports many different color spaces to represent colors. The most common values +/// are stored as RGBA using a single byte per component. Less common values are stored +/// using a `Box` to reduce the amount of memory used per color. +/// +/// Each color space is represented as a struct that implements the `From` and `Into` traits +/// for all other color spaces, so it is possible to convert between color spaces easily. +/// In addition, colors support interpolation as in the `color-mix()` function. +pub const CssColor = union(enum) { + /// The `currentColor` keyword. + current_color, + /// A value in the RGB color space, including values parsed as hex colors, or the `rgb()`, `hsl()`, and `hwb()` functions. + rgba: RGBA, + /// A value in a LAB color space, including the `lab()`, `lch()`, `oklab()`, and `oklch()` functions. + lab: *LABColor, + /// A value in a predefined color space, e.g. `display-p3`. + predefined: *PredefinedColor, + /// A floating point representation of an RGB, HSL, or HWB color when it contains `none` components. + float: *FloatColor, + /// The `light-dark()` function. + light_dark: struct { + // TODO: why box the two fields separately? why not one allocation? + light: *CssColor, + dark: *CssColor, + }, + /// A system color keyword. + system: SystemColor, + + const This = @This(); + + pub fn toCss( + this: *const This, + comptime W: type, + dest: *Printer(W), + ) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn parse(input: *css.Parser) Error!CssColor { + const location = input.currentSourceLocation(); + const token = try input.next(); + + switch (token.*) { + .hash, .idhash => |v| { + const r, const g, const b, const a = css.color.parseHashColor(v) orelse return location.newUnexpectedTokenError(token.*); + return .{ + .rgba = RGBA.new(r, g, b, a), + }; + }, + .ident => |value| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(value, "currentcolor")) { + return .current_color; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(value, "transparent")) { + return .{ + .rgba = RGBA.transparent(), + }; + } else { + if (css.color.parseNamedColor(value)) |named| { + const r, const g, const b = named; + return .{ .rgba = RGBA.new(r, g, b, 255.0) }; + } else if (SystemColor.parseString(value)) |system_color| { + return .{ .system = system_color }; + } else return location.newUnexpectedTokenError(token.*); + } + }, + .function => |name| css.color.parseColorFunction(location, name, input), + } + } + + pub fn deinit(this: CssColor) void { + _ = this; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn clone(this: *const CssColor, allocator: Allocator) CssColor { + _ = this; // autofix + _ = allocator; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn toLightDark(this: *const CssColor, allocator: Allocator) CssColor { + return switch (this.*) { + .light_dark => this.clone(allocator), + else => .{ + .light_dark = .{ + .light = bun.create(allocator, CssColor, this.clone(allocator)), + .dark = bun.create(allocator, CssColor, this.clone(allocator)), + }, + }, + }; + } + + /// Mixes this color with another color, including the specified amount of each. + /// Implemented according to the [`color-mix()`](https://www.w3.org/TR/css-color-5/#color-mix) function. + // PERF: these little allocations feel bad + pub fn interpolate( + this: *const CssColor, + allocator: Allocator, + comptime T: type, + p1_: f32, + other: *const CssColor, + p2_: f32, + method: HueInterpolationMethod, + ) ?CssColor { + var p1 = p1_; + var p2 = p2_; + + if (this.* == .current_color or other.* == .current_color) { + return null; + } + + if (this.* == .light_dark or other.* == .light_dark) { + const this_light_dark = this.toLightDark(allocator); + const other_light_dark = this.toLightDark(allocator); + + const al = this_light_dark.light_dark.light; + const ad = this_light_dark.light_dark.dark; + + const bl = other_light_dark.light_dark.light; + const bd = other_light_dark.light_dark.dark; + + return .{ + .light_dark = .{ + .light = bun.create( + allocator, + CssColor, + al.interpolate(allocator, T, p1, &bl, p2, method) orelse return null, + ), + .dark = bun.create( + allocator, + CssColor, + ad.interpolate(allocator, T, p1, &bd, p2, method) orelse return null, + ), + }, + }; + } + + const check_converted = struct { + fn run(color: *CssColor) bool { + bun.debugAssert(color.* != .light_dark and color.* != .current_color and color.* != .system); + return switch (color.*) { + .rgba => T == RGBA, + .lab => |lab| switch (lab.*) { + .lab => T == LAB, + .lch => T == LCH, + .oklab => T == OKLAB, + .oklch => T == OKLCH, + }, + .predefined => |pre| switch (pre.*) { + .srgb => T == SRGB, + .srgb_linear => T == SRGBLinear, + .display_p3 => T == P3, + .a98 => T == A98, + .prophoto => T == ProPhoto, + .rec2020 => T == Rec2020, + .xyz_d50 => T == XYZd50, + .xyz_d65 => T == XYZd65, + }, + .float => |f| switch (f.*) { + .rgb => T == SRGB, + .hsl => T == HSL, + .hwb => T == HWB, + }, + .system => bun.Output.panic("Unreachable code: system colors cannot be converted to a color.\n\nThis is a bug in Bun's CSS color parser. Please file a bug report at https://github.com/oven-sh/bun/issues/new/choose", .{}), + // We checked these above + .light_dark, .current_color => unreachable, + }; + } + }; + + const converted_first = check_converted.run(this); + const converted_second = check_converted.run(other); + + // https://drafts.csswg.org/css-color-5/#color-mix-result + var first_color = T.tryFromCssColor(this) catch return null; + var second_color = T.tryFromCssColor(other) catch return null; + + if (converted_first and !first_color.inGamut()) { + first_color = mapGamut(first_color); + } + + if (converted_second and !second_color.inGamut()) { + second_color = mapGamut(second_color); + } + + // https://www.w3.org/TR/css-color-4/#powerless + if (converted_first) { + first_color.adjustPowerlessComponents(); + } + + if (converted_second) { + second_color.adjustPowerlessComponents(); + } + + // https://drafts.csswg.org/css-color-4/#interpolation-missing + first_color.fillMissingComponents(&second_color); + second_color.fillMissingComponents(&first_color); + + // https://www.w3.org/TR/css-color-4/#hue-interpolation + first_color.adjustJue(&second_color, method); + + // https://www.w3.org/TR/css-color-4/#interpolation-alpha + first_color.premultiply(); + second_color.premultiply(); + + // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm + var alpha_multiplier = p1 + p2; + if (alpha_multiplier != 1.0) { + p1 = p1 / alpha_multiplier; + p2 = p2 / alpha_multiplier; + if (alpha_multiplier > 1.0) { + alpha_multiplier = 1.0; + } + } + + var result_color = first_color.interpolate(p1, &second_color, p2); + + result_color.unpremultiply(alpha_multiplier); + + return result_color.toCssColor(); + } +}; + +pub fn mapGamut(comptime T: type, color: T) T { + _ = color; // autofix + @compileError(css.todo_stuff.depth); +} + +pub fn parseLab( + comptime T: type, + input: *css.Parser, + parser: *ComponentParser, + comptime func: *const fn (f32, f32, f32, f32) LABColor, +) Error!CssColor { + const Closure = struct { + parser: *ComponentParser, + + pub fn parsefn(this: *@This(), i: *css.Parser) Error!CssColor { + return this.parser.parseRelative(i, T, CssColor, @This().innerfn, .{}); + } + + pub fn innerfn(i: *css.Parser, p: *ComponentParser) Error!CssColor { + // f32::max() does not propagate NaN, so use clamp for now until f32::maximum() is stable. + const l = std.math.clamp(try p.parsePercentage(input), 0.0, std.math.floatMax(f32)); + const a = try p.parseNumber(i); + const b = try p.parseNumber(i); + const alpha = try parseAlpha(i, p); + const lab = func(l, a, b, alpha); + const allocator: Allocator = { + @compileError(css.todo_stuff.think_about_allocator); + }; + const heap_lab = bun.create(allocator, LABColor, lab) catch unreachable; + heap_lab.* = lab; + return CssColor{ .lab = heap_lab }; + } + }; + var closure = Closure{ + .parser = parser, + }; + // https://www.w3.org/TR/css-color-4/#funcdef-lab + return input.parseNestedBlock( + T, + &closure, + ); +} + +pub fn parseLch( + comptime T: type, + input: *css.Parser, + parser: *ComponentParser, + comptime func: *const fn ( + f32, + f32, + f32, + f32, + ) LABColor, +) Error!CssColor { + const Closure = struct { + parser: *ComponentParser, + + pub fn parseNestedBlockFn(this: *@This(), i: *css.Parser) Error!CssColor { + return this.parser.parseRelative(i, T, CssColor, @This().parseRelativeFn, .{this}); + } + + pub fn parseRelativeFn(i: *css.Parser, p: *ComponentParser, this: *@This()) Error!CssColor { + _ = this; // autofix + if (p.from) |from| { + // Relative angles should be normalized. + // https://www.w3.org/TR/css-color-5/#relative-LCH + from.components[2] %= 360.0; + if (from.components[2] < 0.0) { + from.components[2] += 360.0; + } + } + + const l = std.math.clamp(try parser.parsePercentage(i), 0.0, std.math.floatMax(f32)); + const c = std.math.clamp(try parser.parseNumber(i), 0.0, std.math.floatMax(f32)); + const h = try parseAngleOrNumber(i, p); + const alpha = try parseAlpha(i, p); + const lab = func(l, c, h, alpha); + return .{ + .lab = bun.create(@compileError(css.todo_stuff.think_about_allocator), LABColor, lab), + }; + } + }; + + var closure = Closure{ + .parser = parser, + }; + + return input.parseNestedBlock(T, &closure, Closure.parseNestedBlockFn); +} + +/// Parses the hsl() and hwb() functions. +/// The results of this function are stored as floating point if there are any `none` components. +/// https://drafts.csswg.org/css-color-4/#the-hsl-notation +pub fn parseHslHwb( + comptime T: type, + input: *css.Parser, + parser: *ComponentParser, + allows_legacy: bool, + comptime func: *const fn ( + f32, + f32, + f32, + f32, + ) CssColor, +) Error!CssColor { + const Closure = struct { + parser: *ComponentParser, + allows_legacy: bool, + + pub fn parseNestedBlockFn(this: *@This(), i: *css.Parser) Error!CssColor { + return this.parser.parseRelative(i, T, CssColor, @This().parseRelativeFn, .{this}); + } + + pub fn parseRelativeFn(i: *css.Parser, p: *ComponentParser, this: *@This()) Error!CssColor { + const h, const a, const b, const is_legacy = try parseHslHwbComponents(T, i, p, this.allows_legacy); + const alpha = if (is_legacy) try parseLegacyAlpha(i, p) else try parseAlpha(i, p); + + return func(h, a, b, alpha); + } + }; + + var closure = Closure{ + .parser = parser, + .allows_legacy = allows_legacy, + }; + + return input.parseNestedBlock(T, &closure, Closure.parseNestedBlockFn); +} + +pub fn parseHslHwbComponents( + comptime T: type, + input: *css.Parser, + parser: *ComponentParser, + allows_legacy: bool, +) Error!struct { f32, f32, f32, bool } { + _ = T; // autofix + const h = try parseAngleOrNumber(input, parser); + const is_legacy_syntax = allows_legacy and + parser.from == null and + !std.math.isNan(h) and + (if (input.tryParse(css.Parser.expectComma, .{})) |_| true else false); + + const a = std.math.clamp(try parser.parsePercentage(input), 0.0, 1.0); + + if (is_legacy_syntax) { + try input.expectComma(); + } + + const b = std.math.clamp(try parser.parsePercentage(input), 0.0, 1.0); + + if (is_legacy_syntax and (std.math.isNan(a) or std.math.isNan(b))) { + return try input.newCustomError(css.ParserError.invalid_value); + } + + return .{ h, a, b, is_legacy_syntax }; +} + +pub fn parseAngleOrNumber(input: *css.Parser, parser: *const ComponentParser) Error!f32 { + // zack is here + return switch (try parser.parseAngleOrNumber(input)) { + .number => |v| v.value, + .angle => |v| v.degrees, + }; +} + +fn parseRgb(input: *css.Parser, parser: *ComponentParser) Error!CssColor { + // https://drafts.csswg.org/css-color-4/#rgb-functions + + const Closure = struct { + p: *ComponentParser, + + pub fn parseNestedBlockFn(this: *@This(), i: *css.Parser) Error!CssColor { + this.p.parseRelative(i, SRGB, CssColor, @This().parseRelativeFn, .{this}); + } + + pub fn parseRelativeFn(i: *css.Parser, p: *css.Parser, this: *@This()) Error!CssColor { + _ = i; // autofix + _ = this; // autofix + const r, const g, const b, const is_legacy = try parseRgbComponents(input, p); + const alpha = if (is_legacy) try parseLegacyAlpha(input, p) else try parseAlpha(input, p); + + if (!std.math.isNan(r) and + !std.math.isNan(g) and + !std.math.isNan(b) and + !std.math.isNan(alpha)) + { + if (is_legacy) return .{ + .rgba = RGBA.new( + @intCast(r), + @intCast(g), + @intCast(b), + @intCast(alpha), + ), + }; + + return .{ + .rgba = RGBA.fromFloats( + r, + g, + b, + alpha, + ), + }; + } else { + return .{ + .float = bun.create( + @compileError(css.todo_stuff.think_about_allocator), + FloatColor, + .{ + .srgb = .{ + .r = r, + .g = g, + .b = b, + .alpha = alpha, + }, + }, + ), + }; + } + } + }; + var closure = Closure{ + .p = parser, + }; + return input.parseNestedBlock(CssColor, &closure, Closure.parseNestedBlockFn); +} + +pub fn parseRgbComponents(input: *css.Parser, parser: *ComponentParser) Error!struct { + f32, + f32, + f32, + bool, +} { + const red = try parser.parseNumberOrPercentage(input); + const is_legacy_syntax = parser.from == null and !std.math.isNan(red.value) and (if (input.tryParse(css.Parser.expectComma)) |_| true else false); + + const r, const g, const b = if (is_legacy_syntax) switch (red) { + .number => |num| brk: { + const r = std.math.clamp(@round(num.value), 0.0, 255.0); + const g = std.math.clamp(@round(try parser.parseNumber(input)), 0.0, 255.0); + try input.expectComma(); + const b = std.math.clamp(@round(try parser.parseNumber(input)), 0.0, 255.0); + break :brk .{ r, g, b }; + }, + .percentage => |per| brk: { + const unit_value = per.unit_value; + const r = std.math.clamp(@round(unit_value * 255.0), 0.0, 255.0); + const g = std.math.clamp(@round(try parser.parsePercentage(input) * 255.0), 0.0, 255.0); + try input.expectComma(); + const b = std.math.clamp(@round(try parser.parsePercentage(input) * 255.0), 0.0, 255.0); + break :brk .{ r, g, b }; + }, + } else brk: { + const get = struct { + pub fn component(value: NumberOrPercentage) f32 { + return switch (value) { + .number => |num| { + const v = num.value; + if (std.math.isNan(v)) return v; + return std.math.clamp(@round(v), 0.0, 255.0) / 255.0; + }, + .percentage => |per| std.math.clamp(per.unit_value, 0.0, 1.0), + }; + } + }; + const r = get.component(red); + const g = get.component(try parser.parseNumberOrPercentage(input)); + const b = get.component(try parser.parseNumberOrPercentage(input)); + break :brk .{ r, g, b }; + }; + + if (is_legacy_syntax and (std.math.isNan(g) or std.math.isNan(b))) { + return input.newCustomError(css.ParserError.invalid_value); + } + + return .{ r, g, b, is_legacy_syntax }; +} + +fn parseLegacyAlpha(input: *css.Parser, parser: *const ComponentParser) Error!f32 { + if (!input.isExhausted()) { + try input.expectComma(); + return std.math.clamp(try parseNumberOrPercentage(input, parser), 0.0, 1.0); + } + return 1.0; +} + +fn parseAlpha(input: *css.Parser, parser: *const ComponentParser) Error!f32 { + const res = if (input.tryParse(css.Parser.expectDelim, .{'/'})) + std.math.clamp(try parseNumberOrPercentage(input, parser), 0.0, 1.0) + else + 1.0; + + return res; +} + +pub fn parseNumberOrPercentage(input: *css.Parser, parser: *const ComponentParser) Error!f32 { + return switch (try parser.parseNumberOrPercentage(input)) { + .number => |value| value.value, + .percentage => |value| value.unit_value, + }; +} + +pub fn parseeColorFunction(location: css.SourceLocation, function: []const u8, input: *css.Parser) Error!CssColor { + var parser = ComponentParser.new(true); + + // css.todo_stuff.match_ignore_ascii_case; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "lab")) { + return parseLab(LAB, input, &parser, LABColor.newLAB, .{}); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "oklab")) { + return parseLab(OKLAB, input, &parser, LABColor.newOKLAB, .{}); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "lch")) { + return parseLch(LCH, input, &parser, LABColor.newLCH, .{}); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "oklch")) { + return parseLch(OKLCH, input, &parser, LABColor.newOKLCH, .{}); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "color")) { + const predefined = try parsePredefined(input, &parser); + return predefined; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "hsl") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "hsla")) + { + const Fn = struct { + pub fn parsefn(h: f32, s: f32, l: f32, a: f32) CssColor { + const hsl = HSL{ .h = h, .s = s, .l = l, .alpha = a }; + + if (!std.math.isNan(h) and !std.math.isNan(s) and !std.math.isNan(l) and !std.math.isNan(a)) { + return .{ .rgba = hsl.intoRgba() }; + } + + return .{ + .float = bun.create( + @compileError(css.todo_stuff.think_about_allocator), + FloatColor, + .{ .hsl = hsl }, + ), + }; + } + }; + return parseHslHwb(HSL, input, &parser, true, Fn.parsefn); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "hwb")) { + const Fn = struct { + pub fn parsefn() void {} + }; + return parseHslHwb(HWB, input, &parser, true, Fn.parsefn); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "rgb") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "rgba")) + { + return parseRgb(input, &parser); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "color-mix")) { + return input.parseNestedBlock(CssColor, void, css.voidWrap(CssColor, parseColorMix)); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "light-dark")) { + const Fn = struct { + pub fn parsefn(_: void, i: *css.Parser) Error!CssColor { + const light = switch (try CssColor.parse(i)) { + .light_dark => |c| c.light, + else => |light| bun.create( + @compileError(css.todo_stuff.think_about_allocator), + CssColor, + light, + ), + }; + try i.expectComma(); + const dark = switch (try CssColor.parse(i)) { + .light_dark => |c| c.dark, + else => |dark| bun.create( + @compileError(css.todo_stuff.think_about_allocator), + CssColor, + dark, + ), + }; + return .{ + .light_dark = .{ + .light = light, + .dark = dark, + }, + }; + } + }; + return input.parseNestedBlock(CssColor, {}, Fn.parsefn); + } else { + return location.newUnexpectedTokenError(.{ + .ident = function, + }); + } +} + +// Copied from an older version of cssparser. +/// A color with red, green, blue, and alpha components, in a byte each. +pub const RGBA = struct { + /// The red component. + red: u8, + /// The green component. + green: u8, + /// The blue component. + blue: u8, + /// The alpha component. + alpha: u8, + + pub fn new(red: u8, green: u8, blue: u8, alpha: f32) RGBA { + return RGBA{ + .red = red, + .green = green, + .blue = blue, + .alpha = alpha, + }; + } + + pub fn transparent() RGBA { + return RGBA.new(0, 0, 0, 0.0); + } +}; + +fn clamp_unit_f32(val: f32) u8 { + // Whilst scaling by 256 and flooring would provide + // an equal distribution of integers to percentage inputs, + // this is not what Gecko does so we instead multiply by 255 + // and round (adding 0.5 and flooring is equivalent to rounding) + // + // Chrome does something similar for the alpha value, but not + // the rgb values. + // + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1340484 + // + // Clamping to 256 and rounding after would let 1.0 map to 256, and + // `256.0_f32 as u8` is undefined behavior: + // + // https://github.com/rust-lang/rust/issues/10184 + return clamp_floor_256_f32(val * 255.0); +} + +fn clamp_floor_256_f32(val: f32) u8 { + return @intCast(@min(255.0, @max(0.0, @round(val)))); + // val.round().max(0.).min(255.) as u8 +} + +/// A color in a LAB color space, including the `lab()`, `lch()`, `oklab()`, and `oklch()` functions. +pub const LABColor = union(enum) { + /// A `lab()` color. + lab: LAB, + /// An `lch()` color. + lch: LCH, + /// An `oklab()` color. + oklab: OKLAB, + /// An `oklch()` color. + oklch: OKLCH, + + pub fn newLAB(l: f32, a: f32, b: f32, alpha: f32) LABColor { + return LABColor{ + .lab = LAB.new(l, a, b, alpha), + }; + } + + pub fn newOKLAB(l: f32, a: f32, b: f32, alpha: f32) LABColor { + return LABColor{ + .lab = OKLAB.new(l, a, b, alpha), + }; + } + + pub fn newLCH(l: f32, a: f32, b: f32, alpha: f32) LABColor { + return LABColor{ + .lab = LCH.new(l, a, b, alpha), + }; + } + + pub fn newOKLCH(l: f32, a: f32, b: f32, alpha: f32) LABColor { + return LABColor{ + .lab = LCH.new(l, a, b, alpha), + }; + } +}; + +/// A color in a predefined color space, e.g. `display-p3`. +pub const PredefinedColor = union(enum) { + /// A color in the `srgb` color space. + srgb: SRGB, + /// A color in the `srgb-linear` color space. + srgb_linear: SRGBLinear, + /// A color in the `display-p3` color space. + display_p3: P3, + /// A color in the `a98-rgb` color space. + a98: A98, + /// A color in the `prophoto-rgb` color space. + prophoto: ProPhoto, + /// A color in the `rec2020` color space. + rec2020: Rec2020, + /// A color in the `xyz-d50` color space. + xyz_d50: XYZd50, + /// A color in the `xyz-d65` color space. + xyz_d65: XYZd65, +}; + +/// A floating point representation of color types that +/// are usually stored as RGBA. These are used when there +/// are any `none` components, which are represented as NaN. +pub const FloatColor = union(enum) { + /// An RGB color. + rgb: SRGB, + /// An HSL color. + hsl: HSL, + /// An HWB color. + hwb: HWB, +}; + +/// A CSS [system color](https://drafts.csswg.org/css-color/#css-system-colors) keyword. +pub const SystemColor = css.DefineEnumProperty(@compileError(css.todo_stuff.enum_property)); + +/// A color in the [CIE Lab](https://www.w3.org/TR/css-color-4/#cie-lab) color space. +pub const LAB = @compileError(css.todo_stuff.depth); + +/// A color in the [`sRGB`](https://www.w3.org/TR/css-color-4/#predefined-sRGB) color space. +pub const SRGB = @compileError(css.todo_stuff.depth); + +/// A color in the [`hsl`](https://www.w3.org/TR/css-color-4/#the-hsl-notation) color space. +pub const HSL = @compileError(css.todo_stuff.depth); + +/// A color in the [`hwb`](https://www.w3.org/TR/css-color-4/#the-hwb-notation) color space. +pub const HWB = @compileError(css.todo_stuff.depth); + +/// A color in the [`sRGB-linear`](https://www.w3.org/TR/css-color-4/#predefined-sRGB-linear) color space. +pub const SRGBLinear = @compileError(css.todo_stuff.depth); + +/// A color in the [`display-p3`](https://www.w3.org/TR/css-color-4/#predefined-display-p3) color space. +pub const P3 = @compileError(css.todo_stuff.depth); + +/// A color in the [`a98-rgb`](https://www.w3.org/TR/css-color-4/#predefined-a98-rgb) color space. +pub const A98 = @compileError(css.todo_stuff.depth); + +/// A color in the [`prophoto-rgb`](https://www.w3.org/TR/css-color-4/#predefined-prophoto-rgb) color space. +pub const ProPhoto = @compileError(css.todo_stuff.depth); + +/// A color in the [`rec2020`](https://www.w3.org/TR/css-color-4/#predefined-rec2020) color space. +pub const Rec2020 = @compileError(css.todo_stuff.depth); + +/// A color in the [`xyz-d50`](https://www.w3.org/TR/css-color-4/#predefined-xyz) color space. +pub const XYZd50 = @compileError(css.todo_stuff.depth); + +/// A color in the [`xyz-d65`](https://www.w3.org/TR/css-color-4/#predefined-xyz) color space. +pub const XYZd65 = @compileError(css.todo_stuff.depth); + +/// A color in the [CIE LCH](https://www.w3.org/TR/css-color-4/#cie-lab) color space. +pub const LCH = @compileError(css.todo_stuff.depth); + +/// A color in the [OKLab](https://www.w3.org/TR/css-color-4/#ok-lab) color space. +pub const OKLAB = @compileError(css.todo_stuff.depth); + +/// A color in the [OKLCH](https://www.w3.org/TR/css-color-4/#ok-lab) color space. +pub const OKLCH = @compileError(css.todo_stuff.depth); + +pub const ComponentParser = struct { + allow_none: bool, + from: ?RelativeComponentParser, + + pub fn new(allow_none: bool) ComponentParser { + return ComponentParser{ + .allow_none = allow_none, + .from = null, + }; + } + + pub fn parseRelative( + this: *ComponentParser, + input: *css.Parser, + comptime T: type, + comptime C: type, + comptime func: anytype, + args_: anytype, + ) Error!C { + if (input.tryParse(css.Parser.expectIdentMatching, .{"from"})) { + const from = try CssColor.parse(input); + return this.parseFrom(from, input, T, C, func, args_); + } + + const args = bun.meta.ConcatArgs2(func, input, this, args_); + return @call(.auto, func, args); + } + + pub fn parseFrom( + this: *ComponentParser, + from: CssColor, + input: *css.Parser, + comptime T: type, + comptime C: type, + comptime func: anytype, + args_: anytype, + ) Error!C { + if (from == .light_dark) { + const state = input.state(); + const light = try this.parseFrom(from.light_dark.light.*, input, T, C, func, args_); + input.reset(&state); + const dark = try this.parseFrom(from.light_dark.dark.*, input, T, C, func, args_); + return C.LightDarkColor.lightDark(light, dark); + } + + const new_from = (T.tryFromCssColor(from) catch { + @compileError(css.todo_stuff.errors); + }).resolve(); + + this.from = RelativeComponentParser.new(&new_from); + + const args = bun.meta.ConcatArgs2(func, input, this, args_); + return @call(.auto, func, args); + } + + pub fn parseNumberOrPercentage(this: *const ComponentParser, input: *css.Parser) Error!NumberOrPercentage { + if (this.from) |*from| { + if (input.tryParse(RelativeComponentParser.parseNumberOrPercentage, .{from})) |res| { + return res; + } + } + + if (input.tryParse(CSSNumberFns.parse, .{})) |value| { + return NumberOrPercentage{ .number = value }; + } else if (input.tryParse(Percentage.parse, .{})) |value| { + return NumberOrPercentage{ + .percentage = .{ .unit_value = value.v }, + }; + } else if (this.allow_none) { + try input.expectIdentMatching("none"); + return NumberOrPercentage{ + .number = .{ + .value = std.math.nan(f32), + }, + }; + } else { + return try input.newCustomError(css.ParserError.invalid_value); + } + } + + pub fn parseAngleOrNumber(this: *ComponentParser, input: *css.Parser) Error!css.color.AngleOrNumber { + if (this.from) |from| { + if (input.tryParse(RelativeComponentParser.parseAngleOrNumber, .{from})) |res| { + return res; + } + } + + if (input.tryParse(Angle.parse, .{})) |angle| { + return .{ + .angle = .{ + .degrees = angle.toDegrees(), + }, + }; + } else if (input.tryParse(CSSNumberFns.parse, .{})) |value| { + return .{ + .number = .{ + .value = value, + }, + }; + } else if (this.allow_none) { + try input.expectIdentMatching("none"); + return .{ .number = .{ + .value = std.math.nan(f32), + } }; + } else { + return try input.newCustomError(css.ParserError.invalid_value); + } + } +}; + +/// Either a number or a percentage. +pub const NumberOrPercentage = union(enum) { + /// ``. + number: struct { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `` + percentage: struct { + /// The value as a float, divided by 100 so that the nominal range is + /// 0.0 to 1.0. + unit_value: f32, + }, +}; + +const RelativeComponentParser = struct { + names: struct { []const u8, []const u8, []const u8 }, + components: struct { f32, f32, f32, f32 }, + types: struct { ChannelType, ChannelType, ChannelType, ChannelType }, + + pub fn parseAngleOrNumber(input: *css.Parser, this: *const RelativeComponentParser) Error!css.color.AngleOrNumber { + _ = input; // autofix + _ = this; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn parseNumberOrPercentage(input: *css.Parser, this: *const RelativeComponentParser) Error!NumberOrPercentage { + if (input.tryParse(RelativeComponentParser.parseIdent, .{ this, ChannelType{ .percentage = true, .number = true } })) |value| { + return NumberOrPercentage{ .percentage = .{ .unit_value = value } }; + } + + if (input.tryParse(RelativeComponentParser.parseCalc, .{ this, ChannelType{ .percentage = true, .number = true } })) |value| { + return NumberOrPercentage{ + .percentage = .{ + .unit_value = value, + }, + }; + } + + { + const Closure = struct { + parser: *const RelativeComponentParser, + percentage: Percentage = 0, + + pub fn parsefn(i: *css.Parser, self: *@This()) Error!Percentage { + if (Calc(Percentage).parseWith(i, self, @This().calcparseident)) |calc_value| { + if (calc_value == .value) return calc_value.value.*; + } + return i.newCustomError(css.ParserError.invalid_value); + } + + pub fn calcparseident(self: *@This(), ident: []const u8) ?Calc(Percentage) { + const v = self.parser.getIdent(ident, ChannelType{ .percentage = true, .number = true }) orelse return null; + self.percentage = v; + // value variant is a *Percentage + // but we immediately dereference it and discard the pointer + // so using a field on this closure struct instead of making a gratuitous allocation + return .{ + .value = &self.percentage, + }; + } + }; + var closure = Closure{ + .parser = this, + }; + if (input.tryParse(Closure.parsefn, .{ + &closure, + })) |value| { + return NumberOrPercentage{ + .percentage = .{ + .unit_value = value, + }, + }; + } + } + + return input.newErrorForNextToken(); + } + + pub fn getIdent( + this: *const RelativeComponentParser, + ident: []const u8, + allowed_types: ChannelType, + ) ?f32 { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, this.names[0]) and allowed_types.intersects(this.types[0])) { + return this.components[0]; + } + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, this.names[1]) and allowed_types.intersects(this.types[1])) { + return this.components[1]; + } + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, this.names[2]) and allowed_types.intersects(this.types[2])) { + return this.components[2]; + } + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "alpha") and allowed_types.intersects(ChannelType{ .percentage = true })) { + return this.components[3]; + } + + return null; + } +}; + +/// A channel type for a color space. +pub const ChannelType = packed struct(u8) { + /// Channel represents a percentage. + percentage: bool = false, + /// Channel represents an angle. + angle: bool = false, + /// Channel represents a number. + number: bool = false, +}; + +pub fn parsePredefined(input: *css.Parser, parser: *ComponentParser) Error!CssColor { + _ = input; // autofix + _ = parser; // autofix + @compileError(css.todo_stuff.depth); +} + +/// A [color space](https://www.w3.org/TR/css-color-4/#interpolation-space) keyword +/// used in interpolation functions such as `color-mix()`. +pub const ColorSpaceName = union(enum) { + srgb, + @"srgb-linear", + lab, + oklab, + xyz, + @"xyz-d50", + @"xyz-d65", + hsl, + hwb, + lch, + oklch, + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +pub fn parseColorMix(input: *css.Parser) Error!CssColor { + try input.expectIdentMatching("in"); + const method = try ColorSpaceName.parse(input); + + const hue_method_ = if (switch (method) { + .hsl, .hwb, .lch, .oklch => true, + else => false, + }) brk: { + const hue_method = input.tryParse(HueInterpolationMethod.parse, .{}); + if (hue_method) |_| { + try input.expectIdentMatching("hue"); + } + break :brk hue_method; + } else HueInterpolationMethod.shorter; + + const hue_method = hue_method_ orelse HueInterpolationMethod.shorter; + + const first_percent_ = input.tryParse(css.Parser.expectPercentage, .{}); + const first_color = try CssColor.parse(input); + const first_percent = first_percent_ catch first_percent: { + break :first_percent input.tryParse(css.Parser.expectPercentage, .{}) catch null; + }; + try input.expectComma(); + + const second_percent_ = input.tryParse(css.Parser.expectPercentage, .{}); + const second_color = try CssColor.parse(input); + const second_percent = second_percent_ catch first_percent: { + break :first_percent input.tryParse(css.Parser.expectPercentage, .{}) catch null; + }; + + // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm + const p1, const p2 = if (first_percent == null and second_percent == null) .{ 0.5, 0.5 } else brk: { + const p2 = second_percent orelse (1.0 - first_percent.?); + const p1 = first_percent orelse (1.0 - second_percent.?); + break :brk .{ p1, p2 }; + }; + + if ((p1 + p2) == 0.0) return input.newCustomError(css.ParserError.invalid_value); + + return (switch (method) { + .srgb => first_color.interpolate(SRGB, p1, &second_color, p2, hue_method), + .@"srgb-linear" => first_color.interpolate(SRGBLinear, p1, &second_color, p2, hue_method), + .hsl => first_color.interpolate(HSL, p1, &second_color, p2, hue_method), + .hwb => first_color.interpolate(HWB, p1, &second_color, p2, hue_method), + .lab => first_color.interpolate(LAB, p1, &second_color, p2, hue_method), + .lch => first_color.interpolate(LCH, p1, &second_color, p2, hue_method), + .oklab => first_color.interpolate(OKLAB, p1, &second_color, p2, hue_method), + .oklch => first_color.interpolate(OKLCH, p1, &second_color, p2, hue_method), + .xyz, .@"xyz-d65" => first_color.interpolate(XYZd65, p1, &second_color, p2, hue_method), + .@"xyz-d50" => first_color.interpolate(XYZd65, p1, &second_color, p2, hue_method), + }) orelse { + return try input.newCustomError(css.ParserError.invalid_value); + }; +} + +/// A hue [interpolation method](https://www.w3.org/TR/css-color-4/#typedef-hue-interpolation-method) +/// used in interpolation functions such as `color-mix()`. +pub const HueInterpolationMethod = enum { + /// Angles are adjusted so that θ₂ - θ₁ ∈ [-180, 180]. + shorter, + /// Angles are adjusted so that θ₂ - θ₁ ∈ {0, [180, 360)}. + longer, + /// Angles are adjusted so that θ₂ - θ₁ ∈ [0, 360). + increasing, + /// Angles are adjusted so that θ₂ - θ₁ ∈ (-360, 0]. + decreasing, + /// No fixup is performed. Angles are interpolated in the same way as every other component. + specified, + @"converts-to-kebab", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; diff --git a/src/css/values/ident.zig b/src/css/values/ident.zig new file mode 100644 index 0000000000000..e43bf6a97159f --- /dev/null +++ b/src/css/values/ident.zig @@ -0,0 +1,109 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const Error = css.Error; +pub const Printer = css.Printer; +pub const PrintErr = css.PrintErr; + +const Specifier = css.css_properties.css_modules.Specifier; + +/// A CSS [``](https://www.w3.org/TR/css-values-4/#dashed-idents) reference. +/// +/// Dashed idents are used in cases where an identifier can be either author defined _or_ CSS-defined. +/// Author defined idents must start with two dash characters ("--") or parsing will fail. +/// +/// In CSS modules, when the `dashed_idents` option is enabled, the identifier may be followed by the +/// `from` keyword and an argument indicating where the referenced identifier is declared (e.g. a filename). +pub const DashedIdentReference = struct { + /// The referenced identifier. + ident: DashedIdent, + /// CSS modules extension: the filename where the variable is defined. + /// Only enabled when the CSS modules `dashed_idents` option is turned on. + from: ?Specifier, + + pub fn parseWithOptions(input: *css.Parser, options: *const css.ParserOptions) Error!DashedIdentReference { + const ident = try DashedIdentFns.parse(input); + + const from = if (options.css_modules.config != null and options.css_modules.config.dashed_idents) + if (input.tryParse(css.Parser.expectIdentMatching, .{"from"})) try Specifier.parse(input) else null + else + null; + + return DashedIdentReference{ .ident = ident, .from = from }; + } +}; + +/// A CSS [``](https://www.w3.org/TR/css-values-4/#dashed-idents) declaration. +/// +/// Dashed idents are used in cases where an identifier can be either author defined _or_ CSS-defined. +/// Author defined idents must start with two dash characters ("--") or parsing will fail. +pub const DashedIdent = []const u8; +pub const DashedIdentFns = struct { + pub fn parse(input: *css.Parser) Error!DashedIdent { + const location = input.currentSourceLocation(); + const ident = try input.expectIdent(); + if (bun.strings.startsWith(ident, "--")) return location.newUnexpectedTokenError(.{ .ident = ident }); + + return ident; + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } +}; + +/// A CSS [``](https://www.w3.org/TR/css-values-4/#css-css-identifier). +pub const Ident = []const u8; + +pub const IdentFns = struct { + pub fn parse(input: *css.Parser) Error!This { + _ = input; // autofix + + @compileError(css.todo_stuff.depth); + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } +}; + +pub const CustomIdent = []const u8; +pub const CustomIdentFns = struct { + pub fn parse(input: *css.Parser) Error!CustomIdent { + const location = input.currentSourceLocation(); + const ident = try input.expectIdent(); + // css.todo_stuff.match_ignore_ascii_case + const valid = !(bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "initial") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "inherit") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "unset") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "default") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "revert") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "revert-layer")); + + if (!valid) return location.newUnexpectedTokenError(.{ .ident = ident }); + return ident; + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } +}; + +/// A list of CSS [``](https://www.w3.org/TR/css-values-4/#custom-idents) values. +pub const CustomIdentList = css.SmallList(CustomIdent, 1); diff --git a/src/css/values/image.zig b/src/css/values/image.zig new file mode 100644 index 0000000000000..406e75c75d819 --- /dev/null +++ b/src/css/values/image.zig @@ -0,0 +1,3 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +pub const Image = union(enum) {}; diff --git a/src/css/values/string.zig b/src/css/values/string.zig new file mode 100644 index 0000000000000..f1cc01019e26e --- /dev/null +++ b/src/css/values/string.zig @@ -0,0 +1,22 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const Error = css.Error; +pub const Printer = css.Printer; +pub const PrintErr = css.PrintErr; + +/// A quoted CSS string. +pub const CSSString = []const u8; +pub const CSSStringFns = struct { + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } +}; diff --git a/src/css/values/values.zig b/src/css/values/values.zig new file mode 100644 index 0000000000000..14ecedcba7033 --- /dev/null +++ b/src/css/values/values.zig @@ -0,0 +1,627 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +pub const css_modules = struct { + /// Defines where the class names referenced in the `composes` property are located. + /// + /// See [Composes](Composes). + pub const Specifier = union(enum) { + /// The referenced name is global. + global, + /// The referenced name comes from the specified file. + file: []const u8, + /// The referenced name comes from a source index (used during bundling). + source_index: u32, + }; +}; + +pub const angle = struct { + const CSSNumber = number.CSSNumber; + /// A CSS [``](https://www.w3.org/TR/css-values-4/#angles) value. + /// + /// Angles may be explicit or computed by `calc()`, but are always stored and serialized + /// as their computed value. + pub const Angle = union(enum) { + /// An angle in degrees. There are 360 degrees in a full circle. + deg: CSSNumber, + /// An angle in radians. There are 2π radians in a full circle. + rad: CSSNumber, + /// An angle in gradians. There are 400 gradians in a full circle. + grad: CSSNumber, + /// An angle in turns. There is 1 turn in a full circle. + turn: CSSNumber, + + pub fn parse(input: *css.Parser) Error!Angle { + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + + // ~toCssImpl + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn tryFromToken(token: *const css.Token) Error!Angle { + if (token.* == .dimension) { + const value = token.dimension.num; + const unit = token.dimension.unit; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "deg")) { + return .{ .deg = value }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "grad")) { + return .{ .grad = value }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "turn")) { + return .{ .turn = value }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "rad")) { + return .{ .rad = value }; + } + } + @compileError(css.todo_stuff.errors); + } + }; +}; + +pub const ident = struct { + pub usingnamespace @import("./ident.zig"); +}; + +pub const string = struct { + pub usingnamespace @import("./string.zig"); +}; + +pub const color = struct { + pub usingnamespace @import("./color.zig"); +}; + +pub const image = struct { + pub usingnamespace @import("./image.zig"); +}; + +pub const number = struct { + pub const CSSNumber = f32; + pub const CSSNumberFns = struct { + pub fn parse(input: *css.Parser) Error!CSSNumber { + if (input.tryParse(calc.Calc(f32).parse, .{})) |calc_value| { + switch (calc_value) { + .value => |v| return v.*, + .number => |n| return n, + // Numbers are always compatible, so they will always compute to a value. + else => return input.newCustomError(css.ParserError.invalid_value), + } + } + + const num = try input.expectNumber(); + return num; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + /// A CSS [``](https://www.w3.org/TR/css-values-4/#integers) value. + pub const CSSInteger = i32; + pub const CSSIntegerFns = struct { + pub fn parse(input: *css.Parser) Error!CSSInteger { + // TODO: calc?? + const integer = try input.expectInteger(); + return integer; + } + }; +}; + +pub const calc = struct { + const CSSNumber = css.css_values.number.CSSNumber; + /// A mathematical expression used within the `calc()` function. + /// + /// This type supports generic value types. Values such as `Length`, `Percentage`, + /// `Time`, and `Angle` support `calc()` expressions. + pub fn Calc(comptime V: type) type { + return union(enum) { + /// A literal value. + value: *V, + /// A literal number. + number: CSSNumber, + /// A sum of two calc expressions. + sum: struct { + left: *Calc(V), + right: *Calc(V), + }, + /// A product of a number and another calc expression. + product: struct { + number: CSSNumber, + expression: *Calc(V), + }, + /// A math function, such as `calc()`, `min()`, or `max()`. + function: *MathFunction(V), + + const This = @This(); + + // TODO: users of this and `parseWith` don't need the pointer and often throwaway heap allocated values immediately + // use temp allocator or something? + pub fn parse(input: *css.Parser) Error!This { + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn parseWith( + input: *css.Parser, + closure: anytype, + comptime parse_ident: *const fn (@TypeOf(closure), []const u8) Error!This, + ) Error!This { + _ = parse_ident; // autofix + _ = input; // autofix + @compileError(css.todo_stuff.depth); + } + }; + } + + /// A CSS math function. + /// + /// Math functions may be used in most properties and values that accept numeric + /// values, including lengths, percentages, angles, times, etc. + pub fn MathFunction(comptime V: type) type { + return union(enum) { + /// The `calc()` function. + calc: Calc(V), + /// The `min()` function. + min: ArrayList(Calc(V)), + /// The `max()` function. + max: ArrayList(Calc(V)), + /// The `clamp()` function. + clamp: struct { + min: Calc(V), + center: Calc(V), + max: Calc(V), + }, + /// The `round()` function. + round: struct { + strategy: RoundingStrategy, + value: Calc(V), + interval: Calc(V), + }, + /// The `rem()` function. + rem: struct { + dividend: Calc(V), + divisor: Calc(V), + }, + /// The `mod()` function. + mod_: struct { + dividend: Calc(V), + divisor: Calc(V), + }, + /// The `abs()` function. + abs: Calc(V), + /// The `sign()` function. + sign: Calc(V), + /// The `hypot()` function. + hypot: ArrayList(Calc(V)), + }; + } + + /// A [rounding strategy](https://www.w3.org/TR/css-values-4/#typedef-rounding-strategy), + /// as used in the `round()` function. + pub const RoundingStrategy = css.DefineEnumProperty(@compileError(css.todo_stuff.enum_property)); +}; + +pub const percentage = struct { + pub const Percentage = struct { + v: number.CSSNumber, + + pub fn parse(input: *css.Parser) Error!Percentage { + if (input.tryParse(calc.Calc(Percentage), .{})) |calc_value| { + if (calc_value == .value) |v| return v.*; + // Percentages are always compatible, so they will always compute to a value. + bun.unreachablePanic("Percentages are always compatible, so they will always compute to a value.", .{}); + } + + const percent = try input.expectPercentage(); + return Percentage{ .v = percent }; + } + }; + + pub fn DimensionPercentage(comptime D: type) type { + return union(enum) { + dimension: D, + percentage: Percentage, + calc: *calc.Calc(DimensionPercentage(D)), + }; + } + + /// Either a `` or ``. + pub const NumberOrPercentage = union(enum) { + /// A number. + number: number.CSSNumber, + /// A percentage. + percentage: Percentage, + }; +}; + +pub const length = struct { + /// Either a [``](https://www.w3.org/TR/css-values-4/#lengths) or a [``](https://www.w3.org/TR/css-values-4/#numbers). + pub const LengthOrNumber = union(enum) { + /// A number. + number: number.CSSNumber, + /// A length. + length: Length, + }; + + pub const LengthPercentage = percentage.DimensionPercentage(LengthValue); + /// Either a [``](https://www.w3.org/TR/css-values-4/#typedef-length-percentage), or the `auto` keyword. + pub const LengthPercentageOrAuto = union(enum) { + /// The `auto` keyword. + auto, + /// A [``](https://www.w3.org/TR/css-values-4/#typedef-length-percentage). + length: LengthPercentage, + }; + + pub const LengthValue = struct { + pub usingnamespace css.DefineLengthUnits(@This()); + + pub fn tryFromToken(token: *const css.Token) Error!@This() { + _ = token; // autofix + @compileError(css.todo_stuff.depth); + } + + pub fn toUnitValue(this: *const @This()) struct { number.CSSNumber, []const u8 } { + _ = this; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + /// A CSS [``](https://www.w3.org/TR/css-values-4/#lengths) value, with support for `calc()`. + pub const Length = union(enum) { + /// An explicitly specified length value. + value: LengthValue, + /// A computed length value using `calc()`. + calc: *calc.Calc(Length), + + pub fn parse(input: *css.Parser) Error!Length { + if (input.tryParse(calc.Calc(Length).parse, .{})) |calc_value| { + // PERF: I don't like this redundant allocation + if (calc_value == .value) return .{ .calc = calc_value.value.* }; + return .{ + .calc = bun.create( + @compileError(css.todo_stuff.think_about_allocator), + calc.Calc(Length), + calc_value, + ), + }; + } + + const len = try LengthValue.parse(input); + return .{ .value = len }; + } + }; +}; + +pub const position = struct { + pub fn PositionComponent(comptime S: type) type { + return union(enum) { + center, + length, + side: struct { + side: S, + offset: ?length.LengthPercentage, + }, + }; + } + + pub const HorizontalPositionKeyword = css.DefineEnumProperty(struct { + comptime { + @compileError(css.todo_stuff.depth); + } + }); + + pub const VerticalPositionKeyword = css.DefineEnumProperty(struct { + comptime { + @compileError(css.todo_stuff.depth); + } + }); + + pub const HorizontalPosition = PositionComponent(HorizontalPositionKeyword); + pub const VerticalPosition = PositionComponent(VerticalPositionKeyword); +}; + +pub const syntax = struct { + /// 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 toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError(css.todo_stuff.depth); + } + }; + + /// 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 const SyntaxComponentKind = union(enum) { + comptime { + @compileError(css.todo_stuff.depth); + } + }; + + pub const ParsedComponent = union(enum) { + /// A `` value. + length: length.Length, + /// A `` value. + number: number.CSSNumber, + /// A `` value. + percentage: percentage.Percentage, + /// A `` value. + length_percentage: length.LengthPercentage, + /// A `` value. + color: color.CssColor, + /// An `` value. + image: image.Image, // Zig doesn't have lifetimes, so 'i is omitted. + /// A `` value. + url: url.Url, // Lifetimes are omitted in Zig. + /// An `` value. + integer: number.CSSInteger, + /// An `` value. + angle: angle.Angle, + /// A `