const std = @import("std"); const Allocator = std.mem.Allocator; const bun = @import("bun"); const logger = bun.logger; const ArrayList = std.ArrayListUnmanaged; const css = @import("./css_parser.zig"); const css_values = css.css_values; const Parser = css.Parser; const ParserOptions = css.ParserOptions; const Result = css.Result; const Printer = css.Printer; const PrintErr = css.PrintErr; const CSSNumber = css.CSSNumber; const CSSNumberFns = css.CSSNumberFns; const CSSInteger = css.CSSInteger; const CSSIntegerFns = css.CSSIntegerFns; const CustomIdent = css.CustomIdent; const CustomIdentFns = css.CustomIdentFns; const DashedIdent = css.DashedIdent; const DashedIdentFns = css.DashedIdentFns; const Ident = css.Ident; const IdentFns = css.IdentFns; const VendorPrefix = css.VendorPrefix; pub inline fn implementDeepClone(comptime T: type, this: *const T, allocator: Allocator) T { const tyinfo = @typeInfo(T); if (comptime bun.meta.isSimpleCopyType(T)) { return this.*; } if (comptime bun.meta.looksLikeListContainerType(T)) |result| { return switch (result) { .array_list => deepClone(result.child, allocator, this), .baby_list => @panic("Not implemented."), .small_list => this.deepClone(allocator), }; } if (comptime T == []const u8) { return this.*; } if (comptime T == bun.logger.Loc) { return this.*; } if (comptime @typeInfo(T) == .pointer) { const TT = std.meta.Child(T); return implementEql(TT, this.*); } return switch (tyinfo) { .@"struct" => { var strct: T = undefined; inline for (tyinfo.@"struct".fields) |field| { if (comptime canTransitivelyImplementDeepClone(field.type) and @hasDecl(field.type, "__generateDeepClone")) { @field(strct, field.name) = implementDeepClone(field.type, &field(this, field.name, allocator)); } else { @field(strct, field.name) = deepClone(field.type, &@field(this, field.name), allocator); } } return strct; }, .@"union" => { inline for (bun.meta.EnumFields(T), tyinfo.@"union".fields) |enum_field, union_field| { if (@intFromEnum(this.*) == enum_field.value) { if (comptime canTransitivelyImplementDeepClone(union_field.type) and @hasDecl(union_field.type, "__generateDeepClone")) { return @unionInit(T, enum_field.name, implementDeepClone(union_field.type, &@field(this, enum_field.name), allocator)); } return @unionInit(T, enum_field.name, deepClone(union_field.type, &@field(this, enum_field.name), allocator)); } } unreachable; }, else => @compileError("Unhandled type " ++ @typeName(T)), }; } /// A function to implement `lhs.eql(&rhs)` for the many types in the CSS parser that needs this. /// /// This is the equivalent of doing `#[derive(PartialEq])` in Rust. /// /// This function only works on simple types like: /// - Simple equality types (e.g. integers, floats, strings, enums, etc.) /// - Types which implement a `.eql(lhs: *const @This(), rhs: *const @This()) bool` function /// /// Or compound types composed of simple types such as: /// - Pointers to simple types /// - Optional simple types /// - Structs, Arrays, and Unions pub fn implementEql(comptime T: type, this: *const T, other: *const T) bool { const tyinfo = @typeInfo(T); if (comptime bun.meta.isSimpleEqlType(T)) { return this.* == other.*; } if (comptime T == []const u8) { return bun.strings.eql(this.*, other.*); } if (comptime @typeInfo(T) == .pointer) { const TT = std.meta.Child(T); return implementEql(TT, this.*, other.*); } if (comptime @typeInfo(T) == .optional) { const TT = std.meta.Child(T); if (this.* != null and other.* != null) return implementEql(TT, &this.*.?, &other.*.?); return false; } if (comptime T == VendorPrefix) { return VendorPrefix.eql(this.*, other.*); } if (comptime T == bun.logger.Loc) { return this.*.start == other.*.start; } return switch (tyinfo) { .optional => @compileError("Handled above, this means Zack wrote a bug."), .pointer => @compileError("Handled above, this means Zack wrote a bug."), .array => { const Child = std.meta.Child(T); if (comptime bun.meta.isSimpleEqlType(Child)) { return std.mem.eql(Child, &this.*, &other.*); } if (this.len != other.len) return false; if (comptime canTransitivelyImplementEql(Child) and @hasDecl(Child, "__generateEql")) { for (this.*, other.*) |*a, *b| { if (!implementEql(Child, &a, &b)) return false; } } else { for (this.*, other.*) |*a, *b| { if (!eql(Child, a, b)) return false; } } return true; }, .@"struct" => { inline for (tyinfo.@"struct".fields) |field| { if (!eql(field.type, &@field(this, field.name), &@field(other, field.name))) return false; } return true; }, .@"union" => { if (tyinfo.@"union".tag_type == null) @compileError("Unions must have a tag type"); if (@intFromEnum(this.*) != @intFromEnum(other.*)) return false; const enum_fields = bun.meta.EnumFields(T); inline for (enum_fields, std.meta.fields(T)) |enum_field, union_field| { if (enum_field.value == @intFromEnum(this.*)) { if (union_field.type != void) { if (comptime canTransitivelyImplementEql(union_field.type) and @hasDecl(union_field.type, "__generateEql")) { return implementEql(union_field.type, &@field(this, enum_field.name), &@field(other, enum_field.name)); } return eql(union_field.type, &@field(this, enum_field.name), &@field(other, enum_field.name)); } else { return true; } } } unreachable; }, else => @compileError("Unsupported type: " ++ @typeName(T)), }; } pub fn implementHash(comptime T: type, this: *const T, hasher: *std.hash.Wyhash) void { const tyinfo = @typeInfo(T); if (comptime T == void) return; if (comptime bun.meta.isSimpleEqlType(T)) { return hasher.update(std.mem.asBytes(&this)); } if (comptime bun.meta.looksLikeListContainerType(T)) |result| { const list = switch (result) { .array_list => this.items[0..], .baby_list => this.sliceConst(), .small_list => this.slice(), }; bun.writeAnyToHasher(hasher, list.len); for (list) |*item| { hash(tyinfo.array.child, item, hasher); } return; } if (comptime T == []const u8) { return hasher.update(this.*); } if (comptime @typeInfo(T) == .pointer) { @compileError("Invalid type for implementHash(): " ++ @typeName(T)); } if (comptime @typeInfo(T) == .optional) { @compileError("Invalid type for implementHash(): " ++ @typeName(T)); } return switch (tyinfo) { .optional => { if (this.* == null) { bun.writeAnyToHasher(hasher, "null"); } else { bun.writeAnyToHasher(hasher, "some"); hash(tyinfo.optional.child, &this.*.?, hasher); } }, .pointer => { hash(tyinfo.pointer.child, &this.*, hasher); }, .array => { bun.writeAnyToHasher(hasher, this.len); for (this.*[0..]) |*item| { hash(tyinfo.array.child, item, hasher); } }, .@"struct" => { inline for (tyinfo.@"struct".fields) |field| { if (comptime hasHash(field.type)) { hash(field.type, &@field(this, field.name), hasher); } else if (@hasDecl(field.type, "__generateHash") and @typeInfo(field.type) == .@"struct") { implementHash(field.type, &@field(this, field.name), hasher); } else { @compileError("Can't hash these fields: " ++ @typeName(field.type) ++ ". On " ++ @typeName(T)); } } return; }, .@"enum" => { bun.writeAnyToHasher(hasher, @intFromEnum(this.*)); }, .@"union" => { if (tyinfo.@"union".tag_type == null) @compileError("Unions must have a tag type"); bun.writeAnyToHasher(hasher, @intFromEnum(this.*)); const enum_fields = bun.meta.EnumFields(T); inline for (enum_fields, std.meta.fields(T)) |enum_field, union_field| { if (enum_field.value == @intFromEnum(this.*)) { const field = union_field; if (comptime hasHash(field.type)) { hash(field.type, &@field(this, field.name), hasher); } else if (@hasDecl(field.type, "__generateHash") and @typeInfo(field.type) == .@"struct") { implementHash(field.type, &@field(this, field.name), hasher); } else { @compileError("Can't hash these fields: " ++ @typeName(field.type) ++ ". On " ++ @typeName(T)); } } } return; }, else => @compileError("Unsupported type: " ++ @typeName(T)), }; } pub fn slice(comptime T: type, val: *const T) []const bun.meta.looksLikeListContainerType(T).?.child { if (comptime bun.meta.looksLikeListContainerType(T)) |result| { return switch (result.list) { .array_list => val.items, .baby_list => val.sliceConst(), .small_list => val.slice(), }; } @compileError("Unsupported type for `slice`: " ++ @typeName(T)); } pub fn isCompatible(comptime T: type, val: *const T, browsers: bun.css.targets.Browsers) bool { if (@hasDecl(T, "isCompatible")) return T.isCompatible(val, browsers); const tyinfo = @typeInfo(T); if (tyinfo == .pointer) { const TT = std.meta.Child(T); return isCompatible(TT, val.*, browsers); } if (comptime bun.meta.looksLikeListContainerType(T)) |result| { const slc = switch (result.list) { .array_list => val.items, .baby_list => val.sliceConst(), .small_list => val.sliceConst(), }; for (slc) |*item| { if (!isCompatible(result.child, item, browsers)) return false; } return true; } @compileError("Unsupported type for `isCompatible`: " ++ @typeName(T)); } pub inline fn parseWithOptions(comptime T: type, input: *Parser, options: *const ParserOptions) Result(T) { if (T != f32 and T != i32 and @hasDecl(T, "parseWithOptions")) return T.parseWithOptions(input, options); if (comptime bun.meta.looksLikeListContainerType(T)) |result| { switch (result.list) { .array_list => return input.parseCommaSeparated(result.child, parseFor(result.child)), .baby_list => {}, .small_list => {}, } } return switch (T) { f32 => CSSNumberFns.parse(input), CSSInteger => CSSIntegerFns.parse(input), CustomIdent => CustomIdentFns.parse(input), DashedIdent => DashedIdentFns.parse(input), Ident => IdentFns.parse(input), else => T.parse(input), }; } pub inline fn parse(comptime T: type, input: *Parser) Result(T) { if (comptime @typeInfo(T) == .pointer) { const TT = std.meta.Child(T); return switch (parse(TT, input)) { .result => |v| .{ .result = bun.create(input.allocator(), TT, v) }, .err => |e| .{ .err = e }, }; } if (comptime @typeInfo(T) == .optional) { const TT = std.meta.Child(T); return .{ .result = input.tryParse(parseFor(TT), .{}).asValue() }; } if (comptime bun.meta.looksLikeListContainerType(T)) |result| { switch (result.list) { .array_list => return input.parseCommaSeparated(result.child, parseFor(result.child)), .baby_list => {}, .small_list => {}, } } return switch (T) { f32 => CSSNumberFns.parse(input), CSSInteger => CSSIntegerFns.parse(input), CustomIdent => CustomIdentFns.parse(input), DashedIdent => DashedIdentFns.parse(input), Ident => IdentFns.parse(input), else => T.parse(input), }; } pub inline fn parseFor(comptime T: type) @TypeOf(struct { fn parsefn(input: *Parser) Result(T) { return parse(T, input); } }.parsefn) { return struct { fn parsefn(input: *Parser) Result(T) { return parse(T, input); } }.parsefn; } pub fn hasToCss(comptime T: type) bool { const tyinfo = @typeInfo(T); if (comptime T == []const u8) return false; if (tyinfo == .pointer) { const TT = std.meta.Child(T); return hasToCss(TT); } if (tyinfo == .optional) { const TT = std.meta.Child(T); return hasToCss(TT); } if (comptime bun.meta.looksLikeListContainerType(T)) |result| { switch (result.list) { .array_list => return true, .baby_list => return true, .small_list => return true, } } return switch (T) { f32 => true, else => @hasDecl(T, "toCss"), }; } pub inline fn toCss(comptime T: type, this: *const T, comptime W: type, dest: *Printer(W)) PrintErr!void { if (@typeInfo(T) == .pointer) { const TT = std.meta.Child(T); return toCss(TT, this.*, W, dest); } if (@typeInfo(T) == .optional) { const TT = std.meta.Child(T); if (this.*) |*val| { return toCss(TT, val, W, dest); } return; } if (comptime bun.meta.looksLikeListContainerType(T)) |result| { switch (result.list) { .array_list => { return css.to_css.fromList(result.child, this.items, W, dest); }, .baby_list => @compileError("TODO"), .small_list => @compileError("TODO"), } } return switch (T) { f32 => CSSNumberFns.toCss(this, W, dest), CSSInteger => CSSIntegerFns.toCss(this, W, dest), CustomIdent => CustomIdentFns.toCss(this, W, dest), DashedIdent => DashedIdentFns.toCss(this, W, dest), Ident => IdentFns.toCss(this, W, dest), else => T.toCss(this, W, dest), }; } pub fn eqlList(comptime T: type, lhs: *const ArrayList(T), rhs: *const ArrayList(T)) bool { if (lhs.items.len != rhs.items.len) return false; for (lhs.items, rhs.items) |*left, *right| { if (!eql(T, left, right)) return false; } return true; } pub fn canTransitivelyImplementEql(comptime T: type) bool { return switch (@typeInfo(T)) { .@"struct", .@"union" => true, else => false, }; } pub inline fn eql(comptime T: type, lhs: *const T, rhs: *const T) bool { const tyinfo = comptime @typeInfo(T); @setEvalBranchQuota(10_000); if (comptime tyinfo == .pointer) { if (comptime T == []const u8) return bun.strings.eql(lhs.*, rhs.*); if (comptime tyinfo.pointer.size == .one) { const TT = std.meta.Child(T); return eql(TT, lhs.*, rhs.*); } else if (comptime tyinfo.pointer.size == .slice) { if (lhs.*.len != rhs.*.len) return false; for (lhs.*[0..], rhs.*[0..]) |*a, *b| { if (!eql(tyinfo.pointer.child, a, b)) return false; } return true; } else { @compileError("Unsupported pointer size: " ++ @tagName(tyinfo.pointer.size) ++ " (" ++ @typeName(T) ++ ")"); } } if (comptime tyinfo == .optional) { const TT = std.meta.Child(T); if (lhs.* == null and rhs.* == null) return true; if (lhs.* != null and rhs.* != null) return eql(TT, &lhs.*.?, &rhs.*.?); return false; } if (comptime bun.meta.isSimpleEqlType(T)) { return lhs.* == rhs.*; } if (comptime bun.meta.looksLikeListContainerType(T)) |result| { return switch (result.list) { .array_list => eqlList(result.child, lhs, rhs), .baby_list => return lhs.eql(rhs), .small_list => lhs.eql(rhs), }; } if (@hasDecl(T, "IMPL_BITFLAGS")) { return T.eql(lhs.*, rhs.*); } return switch (T) { f32 => lhs.* == rhs.*, CSSInteger => lhs.* == rhs.*, CustomIdent, DashedIdent, Ident => bun.strings.eql(lhs.v, rhs.v), []const u8 => bun.strings.eql(lhs.*, rhs.*), bun.logger.Loc => lhs.eql(rhs.*), else => if (@typeInfo(T) == .@"struct" and @typeInfo(T).@"struct".layout == .@"packed") lhs.* == rhs.* else T.eql(lhs, rhs), }; } pub fn canTransitivelyImplementDeepClone(comptime T: type) bool { return switch (@typeInfo(T)) { .@"struct", .@"union" => true, else => false, }; } pub inline fn deepClone(comptime T: type, this: *const T, allocator: Allocator) T { const tyinfo = comptime @typeInfo(T); if (comptime tyinfo == .pointer) { if (comptime tyinfo.pointer.size == .one) { const TT = std.meta.Child(T); return bun.create(allocator, TT, deepClone(TT, this.*, allocator)); } if (comptime tyinfo.pointer.size == .slice) { var slc = allocator.alloc(tyinfo.pointer.child, this.len) catch bun.outOfMemory(); if (comptime bun.meta.isSimpleCopyType(tyinfo.pointer.child) or tyinfo.pointer.child == []const u8) { @memcpy(slc, this.*); } else { for (this.*, 0..) |*e, i| { slc[i] = deepClone(tyinfo.pointer.child, e, allocator); } } return slc; } @compileError("Deep clone not supported for this kind of pointer: " ++ @tagName(tyinfo.pointer.size) ++ " (" ++ @typeName(T) ++ ")"); } if (comptime tyinfo == .optional) { const TT = std.meta.Child(T); if (this.* != null) return deepClone(TT, &this.*.?, allocator); return null; } if (comptime bun.meta.isSimpleCopyType(T)) { return this.*; } if (comptime bun.meta.looksLikeListContainerType(T)) |result| { return switch (result.list) { .array_list => css.deepClone(result.child, allocator, this), .baby_list => { return bun.BabyList(result.child).deepClone2(this, allocator); }, .small_list => this.deepClone(allocator), }; } // Strings in the CSS parser are always arena allocated // So it is safe to skip const strings as they will never be mutated if (comptime T == []const u8) { return this.*; } if (!@hasDecl(T, "deepClone")) { @compileError(@typeName(T) ++ " does not have a deepClone() function"); } return T.deepClone(this, allocator); } const Angle = css_values.angle.Angle; pub inline fn tryFromAngle(comptime T: type, angle: Angle) ?T { return switch (T) { CSSNumber => CSSNumberFns.tryFromAngle(angle), Angle => return Angle.tryFromAngle(angle), else => T.tryFromAngle(angle), }; } pub inline fn trySign(comptime T: type, val: *const T) ?f32 { return switch (T) { CSSNumber => CSSNumberFns.sign(val), else => { if (@hasDecl(T, "sign")) return T.sign(val); return T.trySign(val); }, }; } pub inline fn tryMap( comptime T: type, val: *const T, comptime map_fn: *const fn (a: f32) f32, ) ?T { return switch (T) { CSSNumber => map_fn(val.*), else => { if (@hasDecl(T, "map")) return T.map(val, map_fn); return T.tryMap(val, map_fn); }, }; } pub inline fn tryOpTo( comptime T: type, comptime R: type, lhs: *const T, rhs: *const T, ctx: anytype, comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) R, ) ?R { return switch (T) { CSSNumber => op_fn(ctx, lhs.*, rhs.*), else => { if (@hasDecl(T, "opTo")) return T.opTo(lhs, rhs, R, ctx, op_fn); return T.tryOpTo(lhs, rhs, R, ctx, op_fn); }, }; } pub inline fn tryOp( comptime T: type, lhs: *const T, rhs: *const T, ctx: anytype, comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) f32, ) ?T { return switch (T) { Angle => Angle.tryOp(lhs, rhs, ctx, op_fn), CSSNumber => op_fn(ctx, lhs.*, rhs.*), else => { if (@hasDecl(T, "op")) return T.op(lhs, rhs, ctx, op_fn); return T.tryOp(lhs, rhs, ctx, op_fn); }, }; } pub inline fn partialCmp(comptime T: type, lhs: *const T, rhs: *const T) ?std.math.Order { return switch (T) { f32 => partialCmpF32(lhs, rhs), CSSInteger => std.math.order(lhs.*, rhs.*), css_values.angle.Angle => css_values.angle.Angle.partialCmp(lhs, rhs), else => T.partialCmp(lhs, rhs), }; } pub inline fn partialCmpF32(lhs: *const f32, rhs: *const f32) ?std.math.Order { const lte = lhs.* <= rhs.*; const rte = lhs.* >= rhs.*; if (!lte and !rte) return null; if (!lte and rte) return .gt; if (lte and !rte) return .lt; return .eq; } pub const HASH_SEED: u64 = 0; pub fn hashArrayList(comptime V: type, this: *const ArrayList(V), hasher: *std.hash.Wyhash) void { for (this.items) |*item| { hash(V, item, hasher); } } pub fn hashBabyList(comptime V: type, this: *const bun.BabyList(V), hasher: *std.hash.Wyhash) void { for (this.sliceConst()) |*item| { hash(V, item, hasher); } } pub fn hasHash(comptime T: type) bool { const tyinfo = @typeInfo(T); if (comptime T == []const u8) return true; if (comptime bun.meta.isSimpleEqlType(T)) return true; if (tyinfo == .pointer) { const TT = std.meta.Child(T); return hasHash(TT); } if (tyinfo == .optional) { const TT = std.meta.Child(T); return hasHash(TT); } if (comptime bun.meta.looksLikeListContainerType(T)) |result| { switch (result.list) { .array_list => return true, .baby_list => return true, .small_list => return true, } } return switch (T) { else => @hasDecl(T, "hash"), }; } pub fn hash(comptime T: type, this: *const T, hasher: *std.hash.Wyhash) void { if (comptime T == void) return; const tyinfo = @typeInfo(T); if (comptime tyinfo == .pointer and T != []const u8) { const TT = std.meta.Child(T); if (tyinfo.pointer.size == .one) { return hash(TT, this.*, hasher); } else if (tyinfo.pointer.size == .slice) { for (this.*) |*item| { hash(TT, item, hasher); } return; } else { @compileError("Can't hash this pointer type: " ++ @typeName(T)); } } if (comptime @typeInfo(T) == .optional) { const TT = std.meta.Child(T); if (this.* != null) return hash(TT, &this.*.?, hasher); return; } if (comptime bun.meta.looksLikeListContainerType(T)) |result| { switch (result.list) { .array_list => return hashArrayList(result.child, this, hasher), .baby_list => return hashBabyList(result.child, this, hasher), .small_list => return this.hash(hasher), } } if (comptime bun.meta.isSimpleEqlType(T)) { const bytes = std.mem.asBytes(&this); hasher.update(bytes); return; } return switch (T) { []const u8 => hasher.update(this.*), else => T.hash(this, hasher), }; }