diff --git a/src/baby_list.zig b/src/baby_list.zig index fea5af650da4ef..4db8655638a2b1 100644 --- a/src/baby_list.zig +++ b/src/baby_list.zig @@ -111,8 +111,9 @@ pub fn BabyList(comptime Type: type) type { } fn assertValidDeepClone(comptime T: type) void { + if (@hasDecl(T, "deepClone")) return; return switch (T) { - bun.JSAst.Expr, bun.JSAst.G.Property, bun.css.ImportConditions => {}, + bun.JSAst.Expr, bun.JSAst.G.Property, bun.css.ImportConditions, bun.css.LayerName => {}, else => { @compileError("Unsupported type for BabyList.deepClone(): " ++ @typeName(Type)); }, @@ -130,11 +131,12 @@ pub fn BabyList(comptime Type: type) type { } /// Same as `deepClone` but doesn't return an error - pub fn deepClone2(this: @This(), allocator: std.mem.Allocator) @This() { + pub fn deepClone2(this: *const @This(), allocator: std.mem.Allocator) @This() { assertValidDeepClone(Type); var list_ = initCapacity(allocator, this.len) catch bun.outOfMemory(); - for (this.slice()) |item| { - list_.appendAssumeCapacity(item.deepClone(allocator)); + list_.len = this.len; + for (this.sliceConst(), list_.slice()) |*old, *new| { + new.* = old.deepClone(allocator); } return list_; @@ -308,6 +310,12 @@ pub fn BabyList(comptime Type: type) type { this.update(list__); } + pub fn insert(this: *@This(), allocator: std.mem.Allocator, index: usize, val: Type) !void { + var list__ = this.listManaged(allocator); + try list__.insert(index, val); + this.update(list__); + } + pub fn append(this: *@This(), allocator: std.mem.Allocator, value: []const Type) !void { var list__ = this.listManaged(allocator); try list__.appendSlice(value); diff --git a/src/bitflags.zig b/src/bitflags.zig index d70a018fa8f619..90fdbfc737a760 100644 --- a/src/bitflags.zig +++ b/src/bitflags.zig @@ -37,6 +37,10 @@ pub fn Bitflags(comptime T: type) type { return @bitCast(@as(IntType, asBits(lhs) & asBits(rhs))); } + pub inline fn intersect(lhs: T, rhs: T) T { + return bitwiseAnd(lhs, rhs); + } + pub inline fn insert(this: *T, other: T) void { this.* = bitwiseOr(this.*, other); } diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index de2350021f7db2..8aca57c4b66020 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -42,8 +42,10 @@ pub const TimeLike = if (Environment.isWindows) f64 else std.posix.timespec; /// - "path" /// - "errno" pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { - const hasRetry = ErrorTypeT != void and @hasDecl(ErrorTypeT, "retry"); - const hasTodo = ErrorTypeT != void and @hasDecl(ErrorTypeT, "todo"); + // can't call @hasDecl on void, anyerror, etc + const has_any_decls = ErrorTypeT != void and ErrorTypeT != anyerror; + const hasRetry = has_any_decls and @hasDecl(ErrorTypeT, "retry"); + const hasTodo = has_any_decls and @hasDecl(ErrorTypeT, "todo"); return union(Tag) { pub const ErrorType = ErrorTypeT; diff --git a/src/bun.zig b/src/bun.zig index 806cf42ae73bce..3f6b8f8ee7ba45 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -4147,6 +4147,16 @@ pub inline fn take(val: anytype) ?bun.meta.OptionalChild(@TypeOf(val)) { return null; } +/// `val` must be a pointer to an optional type (e.g. `*?T`) +/// +/// This function deinitializes the value and sets the optional to null. +pub inline fn clear(val: anytype, allocator: std.mem.Allocator) void { + if (val.*) |*v| { + v.deinit(allocator); + val.* = null; + } +} + pub inline fn wrappingNegation(val: anytype) @TypeOf(val) { return 0 -% val; } @@ -4175,6 +4185,82 @@ pub inline fn itemOrNull(comptime T: type, slice: []const T, index: usize) ?T { return if (index < slice.len) slice[index] else null; } +pub const Maybe = bun.JSC.Node.Maybe; + +/// Type which could be borrowed or owned +/// The name is from the Rust std's `Cow` type +/// Can't think of a better name +pub fn Cow(comptime T: type, comptime VTable: type) type { + const Handler = struct { + fn copy(this: *const T, allocator: std.mem.Allocator) T { + if (!@hasDecl(VTable, "copy")) @compileError(@typeName(VTable) ++ " needs `copy()` function"); + return VTable.copy(this, allocator); + } + + fn deinit(this: *T, allocator: std.mem.Allocator) void { + if (!@hasDecl(VTable, "deinit")) @compileError(@typeName(VTable) ++ " needs `deinit()` function"); + return VTable.deinit(this, allocator); + } + }; + + return union(enum) { + borrowed: *const T, + owned: T, + + pub fn borrow(val: *const T) @This() { + return .{ + .borrowed = val, + }; + } + + pub fn own(val: T) @This() { + return .{ + .owned = val, + }; + } + + pub fn replace(this: *@This(), allocator: std.mem.Allocator, newval: T) void { + if (this.* == .owned) { + this.deinit(allocator); + } + this.* = .{ .owned = newval }; + } + + /// Get the underlying value. + pub inline fn inner(this: *const @This()) *const T { + return switch (this.*) { + .borrowed => this.borrowed, + .owned => &this.owned, + }; + } + + pub inline fn innerMut(this: *@This()) ?*T { + return switch (this.*) { + .borrowed => null, + .owned => &this.owned, + }; + } + + pub fn toOwned(this: *@This(), allocator: std.mem.Allocator) *T { + switch (this.*) { + .borrowed => { + this.* = .{ + .owned = Handler.copy(this.borrowed, allocator), + }; + }, + .owned => {}, + } + return &this.owned; + } + + pub fn deinit(this: *@This(), allocator: std.mem.Allocator) void { + if (this.* == .owned) { + Handler.deinit(&this.owned, allocator); + } + } + }; +} + /// To handle stack overflows: /// 1. StackCheck.init() /// 2. .isSafeToRecurse() diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 54abb91627b8cf..d396da4483cf42 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -131,6 +131,7 @@ const Loc = Logger.Loc; const bake = bun.bake; const lol = bun.LOLHTML; const debug_deferred = bun.Output.scoped(.BUNDLER_DEFERRED, true); +const DataURL = @import("../resolver/resolver.zig").DataURL; const logPartDependencyTree = Output.scoped(.part_dep_tree, false); @@ -471,6 +472,8 @@ pub const BundleV2 = struct { reachable: std.ArrayList(Index), visited: bun.bit_set.DynamicBitSet, all_import_records: []ImportRecord.List, + all_loaders: []const Loader, + all_urls_for_css: []const []const u8, redirects: []u32, redirect_map: PathToSourceIndexMap, dynamic_import_entry_points: *std.AutoArrayHashMap(Index.Int, void), @@ -478,6 +481,11 @@ pub const BundleV2 = struct { scb_bitset: ?bun.bit_set.DynamicBitSetUnmanaged, scb_list: ServerComponentBoundary.List.Slice, + /// Files which are imported by JS and inlined in CSS + additional_files_imported_by_js_and_inlined_in_css: *bun.bit_set.DynamicBitSetUnmanaged, + /// Files which are imported by CSS and inlined in CSS + additional_files_imported_by_css_and_inlined: *bun.bit_set.DynamicBitSetUnmanaged, + const MAX_REDIRECTS: usize = 64; // Find all files reachable from all entry points. This order should be @@ -505,6 +513,9 @@ pub const BundleV2 = struct { } } + const is_js = v.all_loaders[source_index.get()].isJavaScriptLike(); + const is_css = v.all_loaders[source_index.get()] == .css; + const import_record_list_id = source_index; // when there are no import records, v index will be invalid if (import_record_list_id.get() < v.all_import_records.len) { @@ -532,6 +543,14 @@ pub const BundleV2 = struct { } } + // Mark if the file is imported by JS and its URL is inlined for CSS + const is_inlined = import_record.source_index.isValid() and v.all_urls_for_css[import_record.source_index.get()].len > 0; + if (is_js and is_inlined) { + v.additional_files_imported_by_js_and_inlined_in_css.set(import_record.source_index.get()); + } else if (is_css and is_inlined) { + v.additional_files_imported_by_css_and_inlined.set(import_record.source_index.get()); + } + v.visit(import_record.source_index, check_dynamic_imports and import_record.kind == .dynamic, check_dynamic_imports); } } @@ -568,13 +587,24 @@ pub const BundleV2 = struct { null; defer if (scb_bitset) |*b| b.deinit(stack_alloc); + var additional_files_imported_by_js_and_inlined_in_css = try bun.bit_set.DynamicBitSetUnmanaged.initEmpty(stack_alloc, this.graph.input_files.len); + var additional_files_imported_by_css_and_inlined = try bun.bit_set.DynamicBitSetUnmanaged.initEmpty(stack_alloc, this.graph.input_files.len); + defer { + additional_files_imported_by_js_and_inlined_in_css.deinit(stack_alloc); + additional_files_imported_by_css_and_inlined.deinit(stack_alloc); + } + this.dynamic_import_entry_points = std.AutoArrayHashMap(Index.Int, void).init(this.graph.allocator); + const all_urls_for_css = this.graph.ast.items(.url_for_css); + var visitor = ReachableFileVisitor{ .reachable = try std.ArrayList(Index).initCapacity(this.graph.allocator, this.graph.entry_points.items.len + 1), .visited = try bun.bit_set.DynamicBitSet.initEmpty(this.graph.allocator, this.graph.input_files.len), .redirects = this.graph.ast.items(.redirect_import_record_index), .all_import_records = this.graph.ast.items(.import_records), + .all_loaders = this.graph.input_files.items(.loader), + .all_urls_for_css = all_urls_for_css, .redirect_map = this.graph.path_to_source_index_map, .dynamic_import_entry_points = &this.dynamic_import_entry_points, .scb_bitset = scb_bitset, @@ -582,6 +612,8 @@ pub const BundleV2 = struct { this.graph.server_component_boundaries.slice() else undefined, // will never be read since the above bitset is `null` + .additional_files_imported_by_js_and_inlined_in_css = &additional_files_imported_by_js_and_inlined_in_css, + .additional_files_imported_by_css_and_inlined = &additional_files_imported_by_css_and_inlined, }; defer visitor.visited.deinit(); @@ -613,6 +645,21 @@ pub const BundleV2 = struct { } } + const additional_files = this.graph.input_files.items(.additional_files); + const unique_keys = this.graph.input_files.items(.unique_key_for_additional_file); + const content_hashes = this.graph.input_files.items(.content_hash_for_additional_file); + for (all_urls_for_css, 0..) |url_for_css, index| { + if (url_for_css.len > 0) { + // We like to inline additional files in CSS if they fit a size threshold + // If we do inline a file in CSS, and it is not imported by JS, then we don't need to copy the additional file into the output directory + if (additional_files_imported_by_css_and_inlined.isSet(index) and !additional_files_imported_by_js_and_inlined_in_css.isSet(index)) { + additional_files[index].clearRetainingCapacity(); + unique_keys[index] = ""; + content_hashes[index] = 0; + } + } + } + return visitor.reachable.toOwnedSlice(); } @@ -2431,6 +2478,7 @@ pub const BundleV2 = struct { .range = import_record.range, .original_target = original_target, }); + resolve.dispatch(); return true; } @@ -2440,6 +2488,29 @@ pub const BundleV2 = struct { } pub fn enqueueOnLoadPluginIfNeeded(this: *BundleV2, parse: *ParseTask) bool { + const had_matches = enqueueOnLoadPluginIfNeededImpl(this, parse); + if (had_matches) return true; + + if (bun.strings.eqlComptime(parse.path.namespace, "dataurl")) { + const maybe_data_url = DataURL.parse(parse.path.text) catch return false; + const data_url = maybe_data_url orelse return false; + const maybe_decoded = data_url.decodeData(bun.default_allocator) catch return false; + this.free_list.append(maybe_decoded) catch bun.outOfMemory(); + parse.contents_or_fd = .{ + .contents = maybe_decoded, + }; + parse.loader = switch (data_url.decodeMimeType().category) { + .javascript => .js, + .css => .css, + .json => .json, + else => parse.loader, + }; + } + + return false; + } + + pub fn enqueueOnLoadPluginIfNeededImpl(this: *BundleV2, parse: *ParseTask) bool { if (this.plugins) |plugins| { if (plugins.hasAnyMatches(&parse.path, true)) { if (parse.is_entry_point and parse.loader != null and parse.loader.?.shouldCopyForBundling(this.transpiler.options.experimental)) { @@ -3909,7 +3980,6 @@ pub const ParseTask = struct { }, Logger.Loc{ .start = 0 }); unique_key_for_additional_file.* = unique_key; var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); - ast.url_for_css = unique_key; ast.addUrlForCss(allocator, transpiler.options.experimental, &source, null, unique_key); return ast; } @@ -6559,7 +6629,7 @@ pub const LinkerContext = struct { visitor: *@This(), source_index: Index, wrapping_conditions: *BabyList(bun.css.ImportConditions), - wrapping_import_records: *BabyList(*const ImportRecord), + wrapping_import_records: *BabyList(ImportRecord), ) void { // The CSS specification strangely does not describe what to do when there // is a cycle. So we are left with reverse-engineering the behavior from a @@ -6608,17 +6678,15 @@ pub const LinkerContext = struct { // Follow internal dependencies if (record.source_index.isValid()) { - // TODO: conditions // If this import has conditions, fork our state so that the entire // imported stylesheet subtree is wrapped in all of the conditions if (rule.import.hasConditions()) { // Fork our state var nested_conditions = wrapping_conditions.deepClone2(visitor.allocator); - // var nested_import_records = wrapping_import_records.deepClone(visitor.allocator) catch bun.outOfMemory(); - // _ = nested_import_records; // autofix + var nested_import_records = wrapping_import_records.clone(visitor.allocator) catch bun.outOfMemory(); // Clone these import conditions and append them to the state - nested_conditions.push(visitor.allocator, rule.import.conditionsOwned(visitor.allocator)) catch bun.outOfMemory(); + nested_conditions.push(visitor.allocator, rule.import.conditionsWithImportRecords(visitor.allocator, &nested_import_records)) catch bun.outOfMemory(); visitor.visit(record.source_index, &nested_conditions, wrapping_import_records); continue; } @@ -6626,18 +6694,17 @@ pub const LinkerContext = struct { continue; } - // TODO // Record external depednencies if (!record.is_internal) { - + var all_conditions = wrapping_conditions.deepClone2(visitor.allocator); + var all_import_records = wrapping_import_records.clone(visitor.allocator) catch bun.outOfMemory(); // If this import has conditions, append it to the list of overall // conditions for this external import. Note that an external import // may actually have multiple sets of conditions that can't be // merged. When this happens we need to generate a nested imported // CSS file using a data URL. if (rule.import.hasConditions()) { - var all_conditions = wrapping_conditions.deepClone2(visitor.allocator); - all_conditions.push(visitor.allocator, rule.import.conditionsOwned(visitor.allocator)) catch bun.outOfMemory(); + all_conditions.push(visitor.allocator, rule.import.conditionsWithImportRecords(visitor.allocator, &all_import_records)) catch bun.outOfMemory(); visitor.order.push( visitor.allocator, Chunk.CssImportOrder{ @@ -6645,7 +6712,7 @@ pub const LinkerContext = struct { .external_path = record.path, }, .conditions = all_conditions, - // .condition_import_records = wrapping_import_records.*, + .condition_import_records = all_import_records, }, ) catch bun.outOfMemory(); } else { @@ -6656,7 +6723,7 @@ pub const LinkerContext = struct { .external_path = record.path, }, .conditions = wrapping_conditions.*, - // .condition_import_records = visitor.all, + .condition_import_records = wrapping_import_records.*, }, ) catch bun.outOfMemory(); } @@ -6665,7 +6732,7 @@ pub const LinkerContext = struct { } } - // TODO: composes? + // TODO: composes from css modules if (comptime bun.Environment.isDebug) { debug( @@ -6697,7 +6764,7 @@ pub const LinkerContext = struct { .all_import_records = this.graph.ast.items(.import_records), }; var wrapping_conditions: BabyList(bun.css.ImportConditions) = .{}; - var wrapping_import_records: BabyList(*const ImportRecord) = .{}; + var wrapping_import_records: BabyList(ImportRecord) = .{}; // Include all files reachable from any entry point for (entry_points) |entry_point| { visitor.visit(entry_point, &wrapping_conditions, &wrapping_import_records); @@ -6706,6 +6773,8 @@ pub const LinkerContext = struct { var order = visitor.order; var wip_order = BabyList(Chunk.CssImportOrder).initCapacity(temp_allocator, order.len) catch bun.outOfMemory(); + const css_asts: []const ?*bun.css.BundlerStyleSheet = this.graph.ast.items(.css); + // CSS syntax unfortunately only allows "@import" rules at the top of the // file. This means we must hoist all external "@import" rules to the top of // the file when bundling, even though doing so will change the order of CSS @@ -6737,6 +6806,7 @@ pub const LinkerContext = struct { @memcpy(order.slice(), wip_order.slice()); wip_order.clearRetainingCapacity(); } + debugCssOrder(this, &order, .AFTER_HOISTING); // Next, optimize import order. If there are duplicate copies of an imported // file, replace all but the last copy with just the layers that are in that @@ -6757,10 +6827,12 @@ pub const LinkerContext = struct { gop.value_ptr.* = BabyList(u32){}; } for (gop.value_ptr.slice()) |j| { - // TODO: check conditions are redundant if (isConditionalImportRedundant(&entry.conditions, &order.at(j).conditions)) { + // This import is redundant, but it might have @layer rules. + // So we should keep the @layer rules so that the cascade ordering of layers + // is preserved order.mut(i).kind = .{ - .layers = &.{}, + .layers = Chunk.CssImportOrder.Layers.borrow(&css_asts[idx.get()].?.layer_names), }; continue :next_backward; } @@ -6773,13 +6845,12 @@ pub const LinkerContext = struct { gop.value_ptr.* = BabyList(u32){}; } for (gop.value_ptr.slice()) |j| { - // TODO: check conditions are redundant if (isConditionalImportRedundant(&entry.conditions, &order.at(j).conditions)) { // Don't remove duplicates entirely. The import conditions may // still introduce layers to the layer order. Represent this as a // file with an empty layer list. order.mut(i).kind = .{ - .layers = &.{}, + .layers = .{ .owned = .{} }, }; continue :next_backward; } @@ -6790,54 +6861,313 @@ pub const LinkerContext = struct { } } } + debugCssOrder(this, &order, .AFTER_REMOVING_DUPLICATES); - // TODO: layers // Then optimize "@layer" rules by removing redundant ones. This loop goes // forward instead of backward because "@layer" takes effect at the first // copy instead of the last copy like other things in CSS. + { + const DuplicateEntry = struct { + layers: []const bun.css.LayerName, + indices: bun.BabyList(u32) = .{}, + }; + var layer_duplicates = bun.BabyList(DuplicateEntry){}; + + next_forward: for (order.slice()) |*entry| { + debugCssOrder(this, &wip_order, .WHILE_OPTIMIZING_REDUNDANT_LAYER_RULES); + switch (entry.kind) { + // Simplify the conditions since we know they only wrap "@layer" + .layers => |*layers| { + // Truncate the conditions at the first anonymous layer + for (entry.conditions.slice(), 0..) |*condition_, i| { + const conditions: *bun.css.ImportConditions = condition_; + // The layer is anonymous if it's a "layer" token without any + // children instead of a "layer(...)" token with children: + // + // /* entry.css */ + // @import "foo.css" layer; + // + // /* foo.css */ + // @layer foo; + // + // We don't need to generate this (as far as I can tell): + // + // @layer { + // @layer foo; + // } + // + if (conditions.hasAnonymousLayer()) { + entry.conditions.len = @intCast(i); + layers.replace(temp_allocator, .{}); + break; + } + } + + // If there are no layer names for this file, trim all conditions + // without layers because we know they have no effect. + // + // (They have no effect because this is a `.layer` import with no rules + // and only layer declarations.) + // + // /* entry.css */ + // @import "foo.css" layer(foo) supports(display: flex); + // + // /* foo.css */ + // @import "empty.css" supports(display: grid); + // + // That would result in this: + // + // @supports (display: flex) { + // @layer foo { + // @supports (display: grid) {} + // } + // } + // + // Here we can trim "supports(display: grid)" to generate this: + // + // @supports (display: flex) { + // @layer foo; + // } + // + if (layers.inner().len == 0) { + var i: u32 = entry.conditions.len; + while (i != 0) { + i -= 1; + const condition = entry.conditions.at(i); + if (condition.layer != null) { + break; + } + entry.conditions.len = i; + } + } + + // Remove unnecessary entries entirely + if (entry.conditions.len == 0 and layers.inner().len == 0) { + continue; + } + }, + else => {}, + } + + // Omit redundant "@layer" rules with the same set of layer names. Note + // that this tests all import order entries (not just layer ones) because + // sometimes non-layer ones can make following layer ones redundant. + // layers_post_import + const layers_key: []const bun.css.LayerName = switch (entry.kind) { + .source_index => css_asts[entry.kind.source_index.get()].?.layer_names.sliceConst(), + .layers => entry.kind.layers.inner().sliceConst(), + .external_path => &.{}, + }; + var index: usize = 0; + while (index < layer_duplicates.len) : (index += 1) { + const both_equal = both_equal: { + if (layers_key.len != layer_duplicates.at(index).layers.len) { + break :both_equal false; + } + + for (layers_key, layer_duplicates.at(index).layers) |*a, *b| { + if (!a.eql(b)) { + break :both_equal false; + } + } + + break :both_equal true; + }; + + if (both_equal) { + break; + } + } + if (index == layer_duplicates.len) { + // This is the first time we've seen this combination of layer names. + // Allocate a new set of duplicate indices to track this combination. + layer_duplicates.push(temp_allocator, DuplicateEntry{ + .layers = layers_key, + }) catch bun.outOfMemory(); + } + var duplicates = layer_duplicates.at(index).indices.slice(); + var j = duplicates.len; + while (j != 0) { + j -= 1; + const duplicate_index = duplicates[j]; + if (isConditionalImportRedundant(&entry.conditions, &wip_order.at(duplicate_index).conditions)) { + if (entry.kind != .layers) { + // If an empty layer is followed immediately by a full layer and + // everything else is identical, then we don't need to emit the + // empty layer. For example: + // + // @media screen { + // @supports (display: grid) { + // @layer foo; + // } + // } + // @media screen { + // @supports (display: grid) { + // @layer foo { + // div { + // color: red; + // } + // } + // } + // } + // + // This can be improved by dropping the empty layer. But we can + // only do this if there's nothing in between these two rules. + if (j == duplicates.len - 1 and duplicate_index == wip_order.len - 1) { + const other = wip_order.at(duplicate_index); + if (other.kind == .layers and importConditionsAreEqual(entry.conditions.sliceConst(), other.conditions.sliceConst())) { + // Remove the previous entry and then overwrite it below + duplicates = duplicates[0..j]; + wip_order.len = duplicate_index; + break; + } + } + + // Non-layer entries still need to be present because they have + // other side effects beside inserting things in the layer order + wip_order.push(temp_allocator, entry.*) catch bun.outOfMemory(); + } + + // Don't add this to the duplicate list below because it's redundant + continue :next_forward; + } + } + + layer_duplicates.mut(index).indices.push( + temp_allocator, + wip_order.len, + ) catch bun.outOfMemory(); + wip_order.push(temp_allocator, entry.*) catch bun.outOfMemory(); + } + + debugCssOrder(this, &wip_order, .WHILE_OPTIMIZING_REDUNDANT_LAYER_RULES); + + order.len = wip_order.len; + @memcpy(order.slice(), wip_order.slice()); + wip_order.clearRetainingCapacity(); + } + debugCssOrder(this, &order, .AFTER_OPTIMIZING_REDUNDANT_LAYER_RULES); - // TODO: layers // Finally, merge adjacent "@layer" rules with identical conditions together. + { + var did_clone: i32 = -1; + for (order.slice()) |*entry| { + if (entry.kind == .layers and wip_order.len > 0) { + const prev_index = wip_order.len - 1; + const prev = wip_order.at(prev_index); + if (prev.kind == .layers and importConditionsAreEqual(prev.conditions.sliceConst(), entry.conditions.sliceConst())) { + if (did_clone != prev_index) { + did_clone = @intCast(prev_index); + } + // need to clone the layers here as they could be references to css ast + wip_order.mut(prev_index).kind.layers.toOwned(temp_allocator).append( + temp_allocator, + entry.kind.layers.inner().sliceConst(), + ) catch bun.outOfMemory(); + } + } + } + } + debugCssOrder(this, &order, .AFTER_MERGING_ADJACENT_LAYER_RULES); + + return order; + } + + const CssOrderDebugStep = enum { + AFTER_HOISTING, + AFTER_REMOVING_DUPLICATES, + WHILE_OPTIMIZING_REDUNDANT_LAYER_RULES, + AFTER_OPTIMIZING_REDUNDANT_LAYER_RULES, + AFTER_MERGING_ADJACENT_LAYER_RULES, + }; - if (bun.Environment.isDebug) { - debug("CSS order:\n", .{}); + fn debugCssOrder(this: *LinkerContext, order: *const BabyList(Chunk.CssImportOrder), comptime step: CssOrderDebugStep) void { + if (comptime bun.Environment.isDebug) { + const env_var = "BUN_DEBUG_CSS_ORDER_" ++ @tagName(step); + const enable_all = bun.getenvTruthy("BUN_DEBUG_CSS_ORDER"); + if (enable_all or bun.getenvTruthy(env_var)) { + debugCssOrderImpl(this, order, step); + } + } + } + + fn debugCssOrderImpl(this: *LinkerContext, order: *const BabyList(Chunk.CssImportOrder), comptime step: CssOrderDebugStep) void { + if (comptime bun.Environment.isDebug) { + debug("CSS order {s}:\n", .{@tagName(step)}); + var arena = bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); for (order.slice(), 0..) |entry, i| { - debug(" {d}: {}\n", .{ i, entry }); + const conditions_str = if (entry.conditions.len > 0) conditions_str: { + var arrlist = std.ArrayListUnmanaged(u8){}; + const writer = arrlist.writer(arena.allocator()); + const W = @TypeOf(writer); + arrlist.appendSlice(arena.allocator(), "[") catch unreachable; + for (entry.conditions.sliceConst(), 0..) |*condition_, j| { + const condition: *const bun.css.ImportConditions = condition_; + const scratchbuf = std.ArrayList(u8).init(arena.allocator()); + var printer = bun.css.Printer(W).new( + arena.allocator(), + scratchbuf, + writer, + bun.css.PrinterOptions.default(), + &entry.condition_import_records, + ); + + condition.toCss(W, &printer) catch unreachable; + if (j != entry.conditions.len - 1) { + arrlist.appendSlice(arena.allocator(), ", ") catch unreachable; + } + } + arrlist.appendSlice(arena.allocator(), " ]") catch unreachable; + break :conditions_str arrlist.items; + } else "[]"; + + debug(" {d}: {} {s}\n", .{ i, entry.fmt(this), conditions_str }); } } + } + + fn importConditionsAreEqual(a: []const bun.css.ImportConditions, b: []const bun.css.ImportConditions) bool { + if (a.len != b.len) { + return false; + } - return order; + for (a, b) |*ai, *bi| { + if (!ai.layersEql(bi) or !ai.supportsEql(bi) or !ai.media.eql(&bi.media)) return false; + } + + return true; } - // Given two "@import" rules for the same source index (an earlier one and a - // later one), the earlier one is masked by the later one if the later one's - // condition list is a prefix of the earlier one's condition list. - // - // For example: - // - // // entry.css - // @import "foo.css" supports(display: flex); - // @import "bar.css" supports(display: flex); - // - // // foo.css - // @import "lib.css" screen; - // - // // bar.css - // @import "lib.css"; - // - // When we bundle this code we'll get an import order as follows: - // - // 1. lib.css [supports(display: flex), screen] - // 2. foo.css [supports(display: flex)] - // 3. lib.css [supports(display: flex)] - // 4. bar.css [supports(display: flex)] - // 5. entry.css [] - // - // For "lib.css", the entry with the conditions [supports(display: flex)] should - // make the entry with the conditions [supports(display: flex), screen] redundant. - // - // Note that all of this deliberately ignores the existence of "@layer" because - // that is handled separately. All of this is only for handling unlayered styles. + /// Given two "@import" rules for the same source index (an earlier one and a + /// later one), the earlier one is masked by the later one if the later one's + /// condition list is a prefix of the earlier one's condition list. + /// + /// For example: + /// + /// // entry.css + /// @import "foo.css" supports(display: flex); + /// @import "bar.css" supports(display: flex); + /// + /// // foo.css + /// @import "lib.css" screen; + /// + /// // bar.css + /// @import "lib.css"; + /// + /// When we bundle this code we'll get an import order as follows: + /// + /// 1. lib.css [supports(display: flex), screen] + /// 2. foo.css [supports(display: flex)] + /// 3. lib.css [supports(display: flex)] + /// 4. bar.css [supports(display: flex)] + /// 5. entry.css [] + /// + /// For "lib.css", the entry with the conditions [supports(display: flex)] should + /// make the entry with the conditions [supports(display: flex), screen] redundant. + /// + /// Note that all of this deliberately ignores the existence of "@layer" because + /// that is handled separately. All of this is only for handling unlayered styles. pub fn isConditionalImportRedundant(earlier: *const BabyList(bun.css.ImportConditions), later: *const BabyList(bun.css.ImportConditions)) bool { if (later.len > earlier.len) return false; @@ -6847,10 +7177,8 @@ pub const LinkerContext = struct { // Only compare "@supports" and "@media" if "@layers" is equal if (a.layersEql(b)) { - // TODO: supports - // TODO: media - const same_supports = true; - const same_media = true; + const same_supports = a.supportsEql(b); + const same_media = a.media.eql(&b.media); // If the import conditions are exactly equal, then only keep // the later one. The earlier one is redundant. Example: @@ -7165,6 +7493,8 @@ pub const LinkerContext = struct { const tla_keywords = this.parse_graph.ast.items(.top_level_await_keyword); const tla_checks = this.parse_graph.ast.items(.tla_check); const input_files = this.parse_graph.input_files.items(.source); + const loaders: []const Loader = this.parse_graph.input_files.items(.loader); + const unique_key_for_additional_file = this.parse_graph.input_files.items(.unique_key_for_additional_file); const export_star_import_records: [][]u32 = this.graph.ast.items(.export_star_import_records); const exports_refs: []Ref = this.graph.ast.items(.exports_ref); @@ -7195,9 +7525,37 @@ pub const LinkerContext = struct { if (record.source_index.isValid()) { // Other file is not CSS if (css_asts[record.source_index.get()] == null) { - const url = urls_for_css[record.source_index.get()]; - if (url.len > 0) { - record.path.text = url; + const source = &input_files[id]; + const loader = loaders[record.source_index.get()]; + switch (loader) { + .jsx, .js, .ts, .tsx, .napi, .sqlite, .json, .html => { + this.log.addErrorFmt( + source, + record.range.loc, + this.allocator, + "Cannot import a \".{s}\" file into a CSS file", + .{@tagName(loader)}, + ) catch bun.outOfMemory(); + }, + .sqlite_embedded => { + this.log.addErrorFmt( + source, + record.range.loc, + this.allocator, + "Cannot import a \"sqlite_embedded\" file into a CSS file", + .{}, + ) catch bun.outOfMemory(); + }, + .css, .file, .toml, .wasm, .base64, .dataurl, .text, .bunsh => {}, + } + + // It has an inlined url for CSS + if (urls_for_css[record.source_index.get()].len > 0) { + record.path.text = urls_for_css[record.source_index.get()]; + } + // It is some external URL + else if (unique_key_for_additional_file[record.source_index.get()].len > 0) { + record.path.text = unique_key_for_additional_file[record.source_index.get()]; } } } @@ -9315,29 +9673,38 @@ pub const LinkerContext = struct { defer _ = arena.reset(.retain_capacity); const css_import = chunk.content.css.imports_in_chunk_in_order.at(imports_in_chunk_index); + const css: *const bun.css.BundlerStyleSheet = &chunk.content.css.asts[imports_in_chunk_index]; switch (css_import.kind) { .layers => |layers| { - if (layers.len > 0) { - @panic("TODO: layer only import"); - } + _ = layers; // autofix + const printer_options = bun.css.PrinterOptions{ + // TODO: make this more configurable + .minify = c.options.minify_whitespace, + .targets = bun.css.Targets.forBundlerTarget(c.options.target), + }; + _ = css.toCssWithWriter( + worker.allocator, + &buffer_writer, + printer_options, + &css_import.condition_import_records, + ) catch |e| { + return CompileResult{ + .css = .{ + .result = .{ .err = e }, + .source_index = Index.invalid.get(), + }, + }; + }; return CompileResult{ .css = .{ - .code = &.{}, + .result = .{ .result = buffer_writer.getWritten() }, .source_index = Index.invalid.get(), }, }; }, - .external_path => |p| { - const import_records_ = [_]ImportRecord{ - ImportRecord{ - .kind = .at, - .path = p, - .range = Logger.Range.None, - }, - }; - var import_records = BabyList(ImportRecord).init(&import_records_); - const css: *const bun.css.BundlerStyleSheet = &chunk.content.css.asts[imports_in_chunk_index]; + .external_path => { + var import_records = BabyList(ImportRecord).init(css_import.condition_import_records.sliceConst()); const printer_options = bun.css.PrinterOptions{ // TODO: make this more configurable .minify = c.options.minify_whitespace, @@ -9348,18 +9715,23 @@ pub const LinkerContext = struct { &buffer_writer, printer_options, &import_records, - ) catch { - @panic("TODO: HANDLE THIS ERROR!"); + ) catch |e| { + return CompileResult{ + .css = .{ + .result = .{ .err = e }, + .source_index = Index.invalid.get(), + }, + }; }; return CompileResult{ .css = .{ - .code = buffer_writer.getWritten(), + .result = .{ .result = buffer_writer.getWritten() }, + .source_index = Index.invalid.get(), }, }; }, .source_index => |idx| { - const css: *const bun.css.BundlerStyleSheet = &chunk.content.css.asts[imports_in_chunk_index]; const printer_options = bun.css.PrinterOptions{ .targets = bun.css.Targets.forBundlerTarget(c.options.target), // TODO: make this more configurable @@ -9370,12 +9742,18 @@ pub const LinkerContext = struct { &buffer_writer, printer_options, &c.graph.ast.items(.import_records)[idx.get()], - ) catch { - @panic("TODO: HANDLE THIS ERROR!"); + ) catch |e| { + return CompileResult{ + .css = .{ + .result = .{ .err = e }, + .source_index = idx.get(), + }, + }; }; return CompileResult{ .css = .{ - .code = buffer_writer.getWritten(), + .result = .{ .result = buffer_writer.getWritten() }, + .source_index = idx.get(), }, }; @@ -9473,6 +9851,7 @@ pub const LinkerContext = struct { const import_records: []const BabyList(ImportRecord) = c.graph.ast.items(.import_records); _ = import_records; // autofix const asts: []const ?*bun.css.BundlerStyleSheet = c.graph.ast.items(.css); + // Prepare CSS asts // Remove duplicate rules across files. This must be done in serial, not // in parallel, and must be done from the last rule to the first rule. @@ -9480,17 +9859,38 @@ pub const LinkerContext = struct { var i: usize = chunk.content.css.imports_in_chunk_in_order.len; while (i != 0) { i -= 1; - const entry = chunk.content.css.imports_in_chunk_in_order.at(i); + const entry = chunk.content.css.imports_in_chunk_in_order.mut(i); switch (entry.kind) { .layers => |layers| { - if (layers.len > 0) { - @panic("TODO: external path"); + const len = layers.inner().len; + var rules = bun.css.BundlerCssRuleList{}; + if (len > 0) { + rules.v.append(allocator, bun.css.BundlerCssRule{ + .layer_statement = bun.css.LayerStatementRule{ + .names = bun.css.SmallList(bun.css.LayerName, 1).fromBabyListNoDeinit(layers.inner().*), + .loc = bun.css.Location.dummy(), + }, + }) catch bun.outOfMemory(); } - // asts[entry.source_index.get()].?.rules.v.len = 0; + var ast = bun.css.BundlerStyleSheet{ + .rules = rules, + .sources = .{}, + .source_map_urls = .{}, + .license_comments = .{}, + .options = bun.css.ParserOptions.default(allocator, null), + }; + wrapRulesWithConditions(&ast, allocator, &entry.conditions, &entry.condition_import_records); + chunk.content.css.asts[i] = ast; }, - .external_path => |p| { + .external_path => |*p| { + var conditions: ?*bun.css.ImportConditions = null; if (entry.conditions.len > 0) { - @panic("TODO: external path with conditions"); + conditions = entry.conditions.mut(0); + entry.condition_import_records.push( + allocator, + bun.ImportRecord{ .kind = .at, .path = p.*, .range = Logger.Range{} }, + ) catch bun.outOfMemory(); + // Handling a chain of nested conditions is complicated. We can't // necessarily join them together because a) there may be multiple // layer names and b) layer names are only supposed to be inserted @@ -9499,18 +9899,60 @@ pub const LinkerContext = struct { // Instead we handle them by preserving the "@import" nesting using // imports of data URL stylesheets. This may seem strange but I think // this is the only way to do this in CSS. - // var i: usize = entry.conditions.len; - // while (i != 0) { - // i -= 1; + var j: usize = entry.conditions.len; + while (j != 1) { + j -= 1; + + const ast_import = bun.css.BundlerStyleSheet{ + .options = bun.css.ParserOptions.default(allocator, null), + .license_comments = .{}, + .sources = .{}, + .source_map_urls = .{}, + .rules = rules: { + var rules = bun.css.BundlerCssRuleList{}; + var import_rule = bun.css.ImportRule{ + .url = p.pretty, + .import_record_idx = entry.condition_import_records.len, + .loc = bun.css.Location.dummy(), + }; + import_rule.conditionsMut().* = entry.conditions.at(j).*; + rules.v.append(allocator, bun.css.BundlerCssRule{ + .import = import_rule, + }) catch bun.outOfMemory(); + break :rules rules; + }, + }; + + const printer_options = bun.css.PrinterOptions{ + .targets = bun.css.Targets.forBundlerTarget(c.options.target), + // TODO: make this more configurable + .minify = c.options.minify_whitespace or c.options.minify_syntax or c.options.minify_identifiers, + }; - // } + const print_result = ast_import.toCss(allocator, printer_options, &entry.condition_import_records) catch |e| { + c.log.addErrorFmt(null, Loc.Empty, c.allocator, "Error generating CSS for import: {s}", .{@errorName(e)}) catch bun.outOfMemory(); + continue; + }; + p.* = bun.fs.Path.init(DataURL.encodeStringAsShortestDataURL(allocator, "text/css", std.mem.trim(u8, print_result.code, " \n\r\t"))); + } } + var empty_conditions = bun.css.ImportConditions{}; + const actual_conditions = if (conditions) |cc| cc else &empty_conditions; + + entry.condition_import_records.push(allocator, bun.ImportRecord{ + .kind = .at, + .path = p.*, + .range = Logger.Range.none, + }) catch bun.outOfMemory(); + chunk.content.css.asts[i] = bun.css.BundlerStyleSheet{ .rules = rules: { var rules = bun.css.BundlerCssRuleList{}; + var import_rule = bun.css.ImportRule.fromUrlAndImportRecordIdx(p.pretty, entry.condition_import_records.len); + import_rule.conditionsMut().* = actual_conditions.*; rules.v.append(allocator, bun.css.BundlerCssRule{ - .import = bun.css.ImportRule.fromUrl(p.pretty), + .import = import_rule, }) catch bun.outOfMemory(); break :rules rules; }, @@ -9549,7 +9991,6 @@ pub const LinkerContext = struct { ast.rules.v.items.len = 0; } - // TODO: wrapRulesWithConditions wrapRulesWithConditions(ast, allocator, &entry.conditions, &entry.condition_import_records); // TODO: Remove top-level duplicate rules across files }, @@ -9565,6 +10006,11 @@ pub const LinkerContext = struct { condition_import_records: *const BabyList(ImportRecord), ) void { _ = condition_import_records; // autofix + var dummy_import_records = bun.BabyList(bun.ImportRecord){}; + defer { + bun.debugAssert(dummy_import_records.len == 0); + } + var i: usize = conditions.len; while (i > 0) { i -= 1; @@ -9573,36 +10019,75 @@ pub const LinkerContext = struct { // Generate "@layer" wrappers. Note that empty "@layer" rules still have // a side effect (they set the layer order) so they cannot be removed. if (item.layer) |l| { - if (l.v) |layer| { - if (ast.rules.v.items.len == 0) { - if (layer.v.isEmpty()) { - // Omit an empty "@layer {}" entirely - continue; - } else { - // Generate "@layer foo;" instead of "@layer foo {}" - ast.rules.v = .{}; - } + const layer = l.v; + var do_block_rule = true; + if (ast.rules.v.items.len == 0) { + if (l.v == null) { + // Omit an empty "@layer {}" entirely + continue; + } else { + // Generate "@layer foo;" instead of "@layer foo {}" + ast.rules.v = .{}; + do_block_rule = false; } + } + ast.rules = brk: { + var new_rules = bun.css.BundlerCssRuleList{}; + new_rules.v.append( + temp_allocator, + if (do_block_rule) .{ .layer_block = bun.css.BundlerLayerBlockRule{ + .name = layer, + .rules = ast.rules, + .loc = bun.css.Location.dummy(), + } } else .{ + .layer_statement = .{ + .names = if (layer) |ly| bun.css.SmallList(bun.css.LayerName, 1).withOne(ly) else .{}, + .loc = bun.css.Location.dummy(), + }, + }, + ) catch bun.outOfMemory(); + + break :brk new_rules; + }; + } + + // Generate "@supports" wrappers. This is not done if the rule block is + // empty because empty "@supports" rules have no effect. + if (ast.rules.v.items.len > 0) { + if (item.supports) |*supports| { ast.rules = brk: { var new_rules = bun.css.BundlerCssRuleList{}; - new_rules.v.append( - temp_allocator, - .{ .layer_block = bun.css.BundlerLayerBlockRule{ - .name = layer, + new_rules.v.append(temp_allocator, .{ + .supports = bun.css.BundlerSupportsRule{ + .condition = supports.cloneWithImportRecords( + temp_allocator, + &dummy_import_records, + ), .rules = ast.rules, .loc = bun.css.Location.dummy(), - } }, - ) catch bun.outOfMemory(); - + }, + }) catch bun.outOfMemory(); break :brk new_rules; }; } } - // TODO: @supports wrappers - - // TODO: @media wrappers + // Generate "@media" wrappers. This is not done if the rule block is + // empty because empty "@media" rules have no effect. + if (ast.rules.v.items.len > 0 and item.media.media_queries.items.len > 0) { + ast.rules = brk: { + var new_rules = bun.css.BundlerCssRuleList{}; + new_rules.v.append(temp_allocator, .{ + .media = bun.css.BundlerMediaRule{ + .query = item.media.cloneWithImportRecords(temp_allocator, &dummy_import_records), + .rules = ast.rules, + .loc = bun.css.Location.dummy(), + }, + }) catch bun.outOfMemory(); + break :brk new_rules; + }; + } } } @@ -15443,21 +15928,45 @@ pub const Chunk = struct { condition_import_records: BabyList(ImportRecord) = .{}, kind: union(enum) { - // kind == .import_layers - layers: [][]const u8, - // kind == .external_path + /// Represents earlier imports that have been made redundant by later ones (see `isConditionalImportRedundant`) + /// We don't want to redundantly print the rules of these redundant imports + /// BUT, the imports may include layers. + /// We'll just print layer name declarations so that the original ordering is preserved. + layers: Layers, external_path: bun.fs.Path, - // kind == .source_idnex source_index: Index, }, + const Layers = bun.Cow(bun.BabyList(bun.css.LayerName), struct { + const Self = bun.BabyList(bun.css.LayerName); + pub fn copy(self: *const Self, allocator: std.mem.Allocator) Self { + return self.deepClone2(allocator); + } + + pub fn deinit(self: *Self, a: std.mem.Allocator) void { + // do shallow deinit since `LayerName` has + // allocations in arena + self.deinitWithAllocator(a); + } + }); + pub fn hash(this: *const CssImportOrder, hasher: anytype) void { // TODO: conditions, condition_import_records bun.writeAnyToHasher(hasher, std.meta.activeTag(this.kind)); switch (this.kind) { .layers => |layers| { - for (layers) |layer| hasher.update(layer); + for (layers.inner().sliceConst()) |layer| { + for (layer.v.slice(), 0..) |layer_name, i| { + const is_last = i == layers.inner().len - 1; + if (is_last) { + hasher.update(layer_name); + } else { + hasher.update(layer_name); + hasher.update("."); + } + } + } hasher.update("\x00"); }, .external_path => |path| hasher.update(path.text), @@ -15465,25 +15974,40 @@ pub const Chunk = struct { } } - pub fn format(this: *const CssImportOrder, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - try writer.print("{s} = ", .{@tagName(this.kind)}); - switch (this.kind) { - .layers => |layers| { - try writer.print("[", .{}); - for (layers, 0..) |layer, i| { - if (i > 0) try writer.print(", ", .{}); - try writer.print("\"{s}\"", .{layer}); - } - try writer.print("]", .{}); - }, - .external_path => |path| { - try writer.print("\"{s}\"", .{path.pretty}); - }, - .source_index => |source_index| { - try writer.print("{d}", .{source_index.get()}); - }, - } + pub fn fmt(this: *const CssImportOrder, ctx: *LinkerContext) CssImportOrderDebug { + return .{ + .inner = this, + .ctx = ctx, + }; } + + const CssImportOrderDebug = struct { + inner: *const CssImportOrder, + ctx: *LinkerContext, + + pub fn format(this: *const CssImportOrderDebug, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.print("{s} = ", .{@tagName(this.inner.kind)}); + switch (this.inner.kind) { + .layers => |layers| { + try writer.print("[", .{}); + const l = layers.inner(); + for (l.sliceConst(), 0..) |*layer, i| { + if (i > 0) try writer.print(", ", .{}); + try writer.print("\"{}\"", .{layer}); + } + + try writer.print("]", .{}); + }, + .external_path => |path| { + try writer.print("\"{s}\"", .{path.pretty}); + }, + .source_index => |source_index| { + const source = this.ctx.parse_graph.input_files.items(.source)[source_index.get()]; + try writer.print("{d} ({s})", .{ source_index.get(), source.path.text }); + }, + } + } + }; }; pub const ImportsFromOtherChunks = std.AutoArrayHashMapUnmanaged(Index.Int, CrossChunkImport.Item.List); @@ -15599,9 +16123,8 @@ pub const CompileResult = union(enum) { result: js_printer.PrintResult, }, css: struct { + result: bun.Maybe([]const u8, anyerror), source_index: Index.Int, - code: []const u8, - // TODO: we need to do this source_map: ?bun.sourcemap.Chunk = null, }, html: struct { @@ -15626,7 +16149,11 @@ pub const CompileResult = union(enum) { .result => |r2| r2.code, else => "", }, - inline .html, .css => |*c| c.code, + .css => |*c| switch (c.result) { + .result => |v| v, + .err => "", + }, + .html => |*c| c.code, }; } diff --git a/src/css/css_parser.zig b/src/css/css_parser.zig index 83c1b0750f570d..20b4655794782f 100644 --- a/src/css/css_parser.zig +++ b/src/css/css_parser.zig @@ -24,6 +24,7 @@ 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 LayerStatementRule = css_rules.layer.LayerStatementRule; pub const SupportsCondition = css_rules.supports.SupportsCondition; pub const CustomMedia = css_rules.custom_media.CustomMediaRule; pub const NamespaceRule = css_rules.namespace.NamespaceRule; @@ -169,6 +170,7 @@ pub const VendorPrefix = packed struct(u8) { pub const NONE = VendorPrefix{ .none = true }; pub const WEBKIT = VendorPrefix{ .webkit = true }; pub const MOZ = VendorPrefix{ .moz = true }; + pub const O = VendorPrefix{ .o = true }; /// Fields listed here so we can iterate them in the order we want pub const FIELDS: []const []const u8 = &.{ "webkit", "moz", "ms", "o", "none" }; @@ -1290,6 +1292,20 @@ pub const DefaultAtRuleParser = struct { } pub fn onImportRule(_: *This, _: *ImportRule, _: u32, _: u32) void {} + + pub fn onLayerRule(_: *This, _: *const bun.css.SmallList(LayerName, 1)) void {} + + pub fn enclosingLayerLength(_: *This) u32 { + return 0; + } + + pub fn setEnclosingLayer(_: *This, _: LayerName) void {} + + pub fn pushToEnclosingLayer(_: *This, _: LayerName) void {} + + pub fn resetEnclosingLayer(_: *This, _: u32) void {} + + pub fn bumpAnonLayerCount(_: *This, _: i32) void {} }; }; @@ -1301,8 +1317,23 @@ pub const BundlerAtRuleParser = struct { const This = @This(); allocator: Allocator, import_records: *bun.BabyList(ImportRecord), + layer_names: bun.BabyList(LayerName) = .{}, options: *const ParserOptions, + /// Having _named_ layers nested inside of an _anonymous_ layer + /// has no effect: + /// + /// ```css + /// @layer { + /// @layer foo { /* layer 1 */ } + /// @layer foo { /* also layer 1 */ } + /// } + /// ``` + /// + /// See: https://drafts.csswg.org/css-cascade-5/#example-787042b6 + anon_layer_count: u32 = 0, + enclosing_layer: LayerName = .{}, + pub const CustomAtRuleParser = struct { pub const Prelude = if (ENABLE_TAILWIND_PARSING) union(enum) { tailwind: TailwindAtRule, @@ -1364,6 +1395,50 @@ pub const BundlerAtRuleParser = struct { }, }) catch bun.outOfMemory(); } + + pub fn onLayerRule(this: *This, layers: *const bun.css.SmallList(LayerName, 1)) void { + if (this.anon_layer_count > 0) return; + + this.layer_names.ensureUnusedCapacity(this.allocator, layers.len()) catch bun.outOfMemory(); + + for (layers.slice()) |*layer| { + if (this.enclosing_layer.v.len() > 0) { + var cloned = LayerName{ + .v = SmallList([]const u8, 1){}, + }; + cloned.v.ensureTotalCapacity(this.allocator, this.enclosing_layer.v.len() + layer.v.len()); + cloned.v.appendSliceAssumeCapacity(this.enclosing_layer.v.slice()); + cloned.v.appendSliceAssumeCapacity(layer.v.slice()); + this.layer_names.push(this.allocator, cloned) catch bun.outOfMemory(); + } else { + this.layer_names.push(this.allocator, layer.deepClone(this.allocator)) catch bun.outOfMemory(); + } + } + } + + pub fn enclosingLayerLength(this: *This) u32 { + return this.enclosing_layer.v.len(); + } + + pub fn setEnclosingLayer(this: *This, layer: LayerName) void { + this.enclosing_layer = layer; + } + + pub fn pushToEnclosingLayer(this: *This, name: LayerName) void { + this.enclosing_layer.v.appendSlice(this.allocator, name.v.slice()); + } + + pub fn resetEnclosingLayer(this: *This, len: u32) void { + this.enclosing_layer.v.setLen(len); + } + + pub fn bumpAnonLayerCount(this: *This, amount: i32) void { + if (amount > 0) { + this.anon_layer_count += @intCast(amount); + } else { + this.anon_layer_count -= @intCast(@abs(amount)); + } + } }; }; @@ -1420,6 +1495,14 @@ pub fn ValidCustomAtRuleParser(comptime T: type) void { _ = T.CustomAtRuleParser.parseBlock; _ = T.CustomAtRuleParser.onImportRule; + + _ = T.CustomAtRuleParser.onLayerRule; + + _ = T.CustomAtRuleParser.enclosingLayerLength; + _ = T.CustomAtRuleParser.setEnclosingLayer; + _ = T.CustomAtRuleParser.pushToEnclosingLayer; + _ = T.CustomAtRuleParser.resetEnclosingLayer; + _ = T.CustomAtRuleParser.bumpAnonLayerCount; } pub fn ValidAtRuleParser(comptime T: type) void { @@ -1504,7 +1587,7 @@ pub fn AtRulePrelude(comptime T: type) type { }, page: ArrayList(css_rules.page.PageSelector), moz_document, - layer: ArrayList(LayerName), + layer: bun.css.SmallList(LayerName, 1), container: struct { name: ?css_rules.container.ContainerName, condition: css_rules.container.ContainerCondition, @@ -1745,7 +1828,8 @@ pub fn TopLevelRuleParser(comptime AtRuleParserT: type) type { this.state = .body; } var nested_parser = this.nested(); - return NestedRuleParser(AtRuleParserT).AtRuleParser.ruleWithoutBlock(&nested_parser, prelude, start); + const result = NestedRuleParser(AtRuleParserT).AtRuleParser.ruleWithoutBlock(&nested_parser, prelude, start); + return result; }, .charset => return .{ .result = {} }, .unknown => { @@ -1956,11 +2040,11 @@ pub fn NestedRuleParser(comptime T: type) type { break :brk .moz_document; }, .layer => { - const names = switch (input.parseList(LayerName, LayerName.parse)) { + const names = switch (bun.css.SmallList(LayerName, 1).parse(input)) { .result => |vv| vv, .err => |e| names: { if (e.kind == .basic and e.kind.basic == .end_of_input) { - break :names ArrayList(LayerName){}; + break :names bun.css.SmallList(LayerName, 1){}; } return .{ .err = e }; }, @@ -2245,17 +2329,28 @@ pub fn NestedRuleParser(comptime T: type) type { return .{ .result = {} }; }, .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; + const name = if (prelude.layer.len() == 0) null else if (prelude.layer.len() == 1) names: { + break :names prelude.layer.at(0).*; } else return .{ .err = input.newError(.at_rule_body_invalid) }; + T.CustomAtRuleParser.onLayerRule(this.at_rule_parser, &prelude.layer); + const old_len = T.CustomAtRuleParser.enclosingLayerLength(this.at_rule_parser); + if (name != null) { + T.CustomAtRuleParser.pushToEnclosingLayer(this.at_rule_parser, name.?); + } else { + T.CustomAtRuleParser.bumpAnonLayerCount(this.at_rule_parser, 1); + } + const rules = switch (this.parseStyleBlock(input)) { .err => |e| return .{ .err = e }, .result => |v| v, }; + if (name == null) { + T.CustomAtRuleParser.bumpAnonLayerCount(this.at_rule_parser, -1); + } + T.CustomAtRuleParser.resetEnclosingLayer(this.at_rule_parser, old_len); + this.rules.v.append(input.allocator(), .{ .layer_block = css_rules.layer.LayerBlockRule(T.CustomAtRuleParser.AtRule){ .name = name, .rules = rules, .loc = loc }, }) catch bun.outOfMemory(); @@ -2353,10 +2448,12 @@ pub fn NestedRuleParser(comptime T: type) type { const loc = this.getLoc(start); switch (prelude) { .layer => { - if (this.is_in_style_rule or prelude.layer.items.len == 0) { + if (this.is_in_style_rule or prelude.layer.len() == 0) { return .{ .err = {} }; } + T.CustomAtRuleParser.onLayerRule(this.at_rule_parser, &prelude.layer); + this.rules.v.append( this.allocator, .{ @@ -2681,6 +2778,9 @@ pub const BundlerStyleSheet = StyleSheet(BundlerAtRule); pub const BundlerCssRuleList = CssRuleList(BundlerAtRule); pub const BundlerCssRule = CssRule(BundlerAtRule); pub const BundlerLayerBlockRule = css_rules.layer.LayerBlockRule(BundlerAtRule); +pub const BundlerSupportsRule = css_rules.supports.SupportsRule(BundlerAtRule); +pub const BundlerMediaRule = css_rules.media.MediaRule(BundlerAtRule); +pub const BundlerPrintResult = bun.css.PrintResult(BundlerAtRule); pub const BundlerTailwindState = struct { source: []const u8, index: bun.bundle_v2.Index, @@ -2696,6 +2796,7 @@ pub fn StyleSheet(comptime AtRule: type) type { license_comments: ArrayList([]const u8), options: ParserOptions, tailwind: if (AtRule == BundlerAtRule) ?*BundlerTailwindState else u0 = if (AtRule == BundlerAtRule) null else 0, + layer_names: bun.BabyList(LayerName) = .{}, const This = @This(); @@ -2823,6 +2924,7 @@ pub fn StyleSheet(comptime AtRule: type) type { .import_records = import_records, .allocator = allocator, .options = &options, + .layer_names = .{}, }; return parseWith(allocator, code, options, BundlerAtRuleParser, &at_rule_parser, import_records); } @@ -2886,10 +2988,29 @@ pub fn StyleSheet(comptime AtRule: type) type { .source_map_urls = source_map_urls, .license_comments = license_comments, .options = options, + .layer_names = if (comptime P == BundlerAtRuleParser) at_rule_parser.layer_names else .{}, }, }; } + pub fn debugLayerRuleSanityCheck(this: *const @This()) void { + if (comptime !bun.Environment.isDebug) return; + + const layer_names_field_len = this.layer_names.len; + _ = layer_names_field_len; // autofix + var actual_layer_rules_len: usize = 0; + + for (this.rules.v.items) |*rule| { + switch (rule.*) { + .layer_block => { + actual_layer_rules_len += 1; + }, + } + } + + // bun.debugAssert() + } + pub fn containsTailwindDirectives(this: *const @This()) bool { if (comptime AtRule != BundlerAtRule) @compileError("Expected BundlerAtRule for this function."); var found_import: bool = false; @@ -6474,6 +6595,18 @@ pub fn implementHash(comptime T: type, this: *const T, hasher: *std.hash.Wyhash) 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| { + generic.hash(tyinfo.Array.child, item, hasher); + } + return; + } if (comptime T == []const u8) { return hasher.update(this.*); } @@ -6484,11 +6617,21 @@ pub fn implementHash(comptime T: type, this: *const T, hasher: *std.hash.Wyhash) @compileError("Invalid type for implementHash(): " ++ @typeName(T)); } return switch (tyinfo) { - .Optional => unreachable, - .Pointer => unreachable, + .Optional => { + if (this.* == null) { + bun.writeAnyToHasher(hasher, "null"); + } else { + bun.writeAnyToHasher(hasher, "some"); + generic.hash(tyinfo.Optional.child, &this.*.?, hasher); + } + }, + .Pointer => { + generic.hash(tyinfo.Pointer.child, &this.*, hasher); + }, .Array => { - if (comptime @typeInfo(T) == .Optional) { - @compileError("Invalid type for implementHash(): " ++ @typeName(T)); + bun.writeAnyToHasher(hasher, this.len); + for (this.*[0..]) |*item| { + generic.hash(tyinfo.Array.child, item, hasher); } }, .Struct => { @@ -6503,8 +6646,12 @@ pub fn implementHash(comptime T: type, this: *const T, hasher: *std.hash.Wyhash) } 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.*)) { @@ -6576,9 +6723,9 @@ pub const to_css = struct { return s.items; } - 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| { + pub fn fromList(comptime T: type, this: []const T, comptime W: type, dest: *Printer(W)) PrintErr!void { + const len = this.len; + for (this, 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 index a7ec7e1182eb67..f0c0604b631438 100644 --- a/src/css/declaration.zig +++ b/src/css/declaration.zig @@ -15,10 +15,12 @@ const ArrayList = std.ArrayListUnmanaged; pub const DeclarationList = ArrayList(css.Property); const BackgroundHandler = css.css_properties.background.BackgroundHandler; +const BorderHandler = css.css_properties.border.BorderHandler; const FallbackHandler = css.css_properties.prefix_handler.FallbackHandler; const MarginHandler = css.css_properties.margin_padding.MarginHandler; const PaddingHandler = css.css_properties.margin_padding.PaddingHandler; const ScrollMarginHandler = css.css_properties.margin_padding.ScrollMarginHandler; +const FontHandler = css.css_properties.font.FontHandler; const InsetHandler = css.css_properties.margin_padding.InsetHandler; const SizeHandler = css.css_properties.size.SizeHandler; @@ -329,10 +331,12 @@ pub fn parse_declaration( pub const DeclarationHandler = struct { background: BackgroundHandler = .{}, + border: BorderHandler = .{}, size: SizeHandler = .{}, margin: MarginHandler = .{}, padding: PaddingHandler = .{}, scroll_margin: ScrollMarginHandler = .{}, + font: FontHandler = .{}, inset: InsetHandler = .{}, fallback: FallbackHandler = .{}, direction: ?css.css_properties.text.Direction, @@ -351,10 +355,12 @@ pub const DeclarationHandler = struct { // } this.background.finalize(&this.decls, context); + this.border.finalize(&this.decls, context); this.size.finalize(&this.decls, context); this.margin.finalize(&this.decls, context); this.padding.finalize(&this.decls, context); this.scroll_margin.finalize(&this.decls, context); + this.font.finalize(&this.decls, context); this.inset.finalize(&this.decls, context); this.fallback.finalize(&this.decls, context); } @@ -362,10 +368,12 @@ pub const DeclarationHandler = struct { pub fn handleProperty(this: *DeclarationHandler, property: *const css.Property, context: *css.PropertyHandlerContext) bool { // return this.background.handleProperty(property, &this.decls, context); return this.background.handleProperty(property, &this.decls, context) or + this.border.handleProperty(property, &this.decls, context) or this.size.handleProperty(property, &this.decls, context) or this.margin.handleProperty(property, &this.decls, context) or this.padding.handleProperty(property, &this.decls, context) or this.scroll_margin.handleProperty(property, &this.decls, context) or + this.font.handleProperty(property, &this.decls, context) or this.inset.handleProperty(property, &this.decls, context) or this.fallback.handleProperty(property, &this.decls, context); } diff --git a/src/css/error.zig b/src/css/error.zig index b70281f28e82cc..d4338c62431a8c 100644 --- a/src/css/error.zig +++ b/src/css/error.zig @@ -244,7 +244,7 @@ pub const ParserError = union(enum) { .selector_error => |err| writer.print("Invalid selector. {s}", .{err}), .unexpected_import_rule => writer.writeAll("@import rules must come before any other rules except @charset and @layer"), .unexpected_namespace_rule => writer.writeAll("@namespace rules must come before any other rules except @charset, @import, and @layer"), - .unexpected_token => |token| writer.print("Unexpected token. {}", .{token}), + .unexpected_token => |token| writer.print("Unexpected token: {}", .{token}), .maximum_nesting_depth => writer.writeAll("Maximum CSS nesting depth exceeded"), }; } diff --git a/src/css/generics.zig b/src/css/generics.zig index 7dc3a088787a17..8d90ca52878869 100644 --- a/src/css/generics.zig +++ b/src/css/generics.zig @@ -25,6 +25,38 @@ const DashedIdentFns = css.DashedIdentFns; const Ident = css.Ident; const IdentFns = css.IdentFns; +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| { @@ -125,10 +157,10 @@ pub inline fn toCss(comptime T: type, this: *const T, comptime W: type, dest: *P if (comptime bun.meta.looksLikeListContainerType(T)) |result| { switch (result.list) { .array_list => { - return css.to_css.fromList(result.child, this, W, dest); + return css.to_css.fromList(result.child, this.items, W, dest); }, - .baby_list => {}, - .small_list => {}, + .baby_list => @compileError("TODO"), + .small_list => @compileError("TODO"), } } return switch (T) { @@ -143,8 +175,8 @@ pub inline fn toCss(comptime T: type, this: *const T, comptime W: type, dest: *P 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, 0..) |*item, i| { - if (!eql(T, item, &rhs.items[i])) return false; + for (lhs.items, rhs.items) |*left, *right| { + if (!eql(T, left, right)) return false; } return true; } @@ -175,6 +207,7 @@ pub inline fn eql(comptime T: type, lhs: *const T, rhs: *const T) bool { } 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; } @@ -213,15 +246,15 @@ pub inline fn deepClone(comptime T: type, this: *const T, allocator: Allocator) return bun.create(allocator, TT, deepClone(TT, this.*, allocator)); } if (comptime tyinfo.Pointer.size == .Slice) { - var slice = allocator.alloc(tyinfo.Pointer.child, this.len) catch bun.outOfMemory(); + 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(slice, this.*); + @memcpy(slc, this.*); } else { for (this.*, 0..) |*e, i| { - slice[i] = deepClone(tyinfo.Pointer.child, e, allocator); + slc[i] = deepClone(tyinfo.Pointer.child, e, allocator); } } - return slice; + return slc; } @compileError("Deep clone not supported for this kind of pointer: " ++ @tagName(tyinfo.Pointer.size) ++ " (" ++ @typeName(T) ++ ")"); } @@ -237,15 +270,7 @@ pub inline fn deepClone(comptime T: type, this: *const T, allocator: Allocator) return switch (result.list) { .array_list => css.deepClone(result.child, allocator, this), .baby_list => { - var ret = bun.BabyList(result.child){ - .ptr = (allocator.alloc(result.child, this.len) catch bun.outOfMemory()).ptr, - .len = this.len, - .cap = this.len, - }; - for (this.sliceConst(), ret.ptr[0..this.len]) |*old, *new| { - new.* = bun.css.generic.deepClone(result.child, old, allocator); - } - return ret; + return bun.BabyList(result.child).deepClone2(this, allocator); }, .small_list => this.deepClone(allocator), }; diff --git a/src/css/media_query.zig b/src/css/media_query.zig index 1f56da703ec1e8..dd9ce06765b752 100644 --- a/src/css/media_query.zig +++ b/src/css/media_query.zig @@ -84,6 +84,10 @@ pub const MediaList = struct { return; } + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } + pub fn eql(lhs: *const MediaList, rhs: *const MediaList) bool { return css.implementEql(@This(), lhs, rhs); } @@ -94,6 +98,14 @@ pub const MediaList = struct { }; } + pub fn cloneWithImportRecords( + this: *const @This(), + allocator: std.mem.Allocator, + _: *bun.BabyList(bun.ImportRecord), + ) @This() { + return deepClone(this, allocator); + } + /// Returns whether the media query list always matches. pub fn alwaysMatches(this: *const MediaList) bool { // If the media list is empty, it always matches. @@ -146,6 +158,10 @@ pub const MediaQuery = struct { }; } + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } + pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } @@ -282,6 +298,10 @@ pub const Qualifier = enum { pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { return css.enum_property_util.toCss(@This(), this, W, dest); } + + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } }; /// A [media type](https://drafts.csswg.org/mediaqueries/#media-types) within a media query. @@ -318,6 +338,10 @@ pub const MediaType = union(enum) { pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } + + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } }; pub fn operationToCss(comptime QueryCondition: type, operator: Operator, conditions: *const ArrayList(QueryCondition), comptime W: type, dest: *Printer(W)) PrintErr!void { @@ -351,23 +375,6 @@ pub const MediaCondition = union(enum) { const This = @This(); - pub fn deepClone(this: *const MediaCondition, allocator: std.mem.Allocator) MediaCondition { - return switch (this.*) { - .feature => |*f| MediaCondition{ .feature = f.deepClone(allocator) }, - .not => |c| MediaCondition{ .not = bun.create(allocator, MediaCondition, c.deepClone(allocator)) }, - .operation => |op| MediaCondition{ - .operation = .{ - .operator = op.operator, - .conditions = css.deepClone(MediaCondition, allocator, &op.conditions), - }, - }, - }; - } - - pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { - return css.implementEql(@This(), lhs, rhs); - } - pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { switch (this.*) { .feature => |*f| { @@ -426,6 +433,27 @@ pub const MediaCondition = union(enum) { pub fn parseWithFlags(input: *css.Parser, flags: QueryConditionFlags) Result(MediaCondition) { return parseQueryCondition(MediaCondition, input, flags); } + + pub fn deepClone(this: *const MediaCondition, allocator: std.mem.Allocator) MediaCondition { + return switch (this.*) { + .feature => |*f| MediaCondition{ .feature = f.deepClone(allocator) }, + .not => |c| MediaCondition{ .not = bun.create(allocator, MediaCondition, c.deepClone(allocator)) }, + .operation => |op| MediaCondition{ + .operation = .{ + .operator = op.operator, + .conditions = css.deepClone(MediaCondition, allocator, &op.conditions), + }, + }, + }; + } + + pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { + return css.implementEql(@This(), lhs, rhs); + } + + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } }; /// Parse a single query condition. @@ -738,6 +766,10 @@ pub const MediaFeatureId = enum { pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { return css.enum_property_util.toCss(@This(), this, W, dest); } + + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } }; pub fn QueryFeature(comptime FeatureId: type) type { @@ -752,6 +784,8 @@ pub fn QueryFeature(comptime FeatureId: type) type { pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } + + pub fn __generateHash() void {} }, /// A boolean feature, e.g. `(hover)`. @@ -762,6 +796,8 @@ pub fn QueryFeature(comptime FeatureId: type) type { pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } + + pub fn __generateHash() void {} }, /// A range, e.g. `(width > 240px)`. @@ -776,6 +812,8 @@ pub fn QueryFeature(comptime FeatureId: type) type { pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } + + pub fn __generateHash() void {} }, /// An interval, e.g. `(120px < width < 240px)`. @@ -794,6 +832,8 @@ pub fn QueryFeature(comptime FeatureId: type) type { pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } + + pub fn __generateHash() void {} }, const This = @This(); @@ -834,6 +874,10 @@ pub fn QueryFeature(comptime FeatureId: type) type { return css.implementEql(@This(), lhs, rhs); } + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } + pub fn needsParens(this: *const This, parent_operator: ?Operator, targets: *const css.Targets) bool { return parent_operator != .@"and" and this.* == .interval and @@ -1476,6 +1520,10 @@ pub fn MediaFeatureName(comptime FeatureId: type) type { null, } }; } + + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } }; } diff --git a/src/css/properties/border.zig b/src/css/properties/border.zig index 6f89d00d28af5e..3cb48e135497f6 100644 --- a/src/css/properties/border.zig +++ b/src/css/properties/border.zig @@ -21,6 +21,11 @@ const CssColor = css.css_values.color.CssColor; const Ratio = css.css_values.ratio.Ratio; const Length = css.css_values.length.Length; +const PropertyCategory = css.PropertyCategory; +const BorderImageHandler = @import("./border_image.zig").BorderImageHandler; +const BorderRadiusHandler = @import("./border_radius.zig").BorderRadiusHandler; +const UnparsedProperty = css.css_properties.custom.UnparsedProperty; + /// 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. @@ -131,6 +136,23 @@ pub fn GenericBorder(comptime S: type, comptime P: u8) type { return; } + fn getFallbacks(this: *@This(), allocator: Allocator, targets: css.targets.Targets) css.SmallList(@This(), 2) { + var fallbacks = this.color.getFallbacks(allocator, targets); + defer fallbacks.deinit(allocator); + var out = css.SmallList(@This(), 2).initCapacity(allocator, fallbacks.len()); + out.setLen(fallbacks.len()); + + for (fallbacks.slice(), out.slice_mut()) |color, *o| { + o.* = .{ + .color = color, + .width = this.width.deepClone(allocator), + .style = this.style.deepClone(allocator), + }; + } + + return out; + } + pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { return css.implementDeepClone(@This(), this, allocator); } @@ -174,6 +196,12 @@ pub const LineStyle = enum { pub usingnamespace css.DefineEnumProperty(@This()); + pub fn isCompatible(_: *const @This(), _: bun.css.targets.Browsers) bool { + return true; + } + + pub fn deinit(_: *const @This(), _: std.mem.Allocator) void {} + pub fn default() LineStyle { return .none; } @@ -193,6 +221,13 @@ pub const BorderSideWidth = union(enum) { pub usingnamespace css.DeriveParse(@This()); pub usingnamespace css.DeriveToCss(@This()); + pub fn isCompatible(this: *const @This(), browsers: bun.css.targets.Browsers) bool { + return switch (this.*) { + .length => |len| len.isCompatible(browsers), + else => true, + }; + } + pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { return css.implementDeepClone(@This(), this, allocator); } @@ -201,6 +236,8 @@ pub const BorderSideWidth = union(enum) { return .medium; } + pub fn deinit(_: *@This(), _: std.mem.Allocator) void {} + pub fn eql(this: *const @This(), other: *const @This()) bool { return switch (this.*) { .thin => switch (other.*) { @@ -234,6 +271,7 @@ pub const BorderColor = struct { // TODO: bring this back // pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-color"); pub usingnamespace css.DefineRectShorthand(@This(), CssColor); + pub usingnamespace ImplFallbacks(@This()); pub const PropertyFieldMap = .{ .top = css.PropertyIdTag.@"border-top-color", @@ -316,6 +354,7 @@ pub const BorderBlockColor = struct { // TODO: bring this back // pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-block-color"); pub usingnamespace css.DefineSizeShorthand(@This(), CssColor); + pub usingnamespace ImplFallbacks(@This()); pub const PropertyFieldMap = .{ .start = css.PropertyIdTag.@"border-block-start-color", @@ -392,6 +431,7 @@ pub const BorderInlineColor = struct { // TODO: bring this back // pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-inline-color"); pub usingnamespace css.DefineSizeShorthand(@This(), CssColor); + pub usingnamespace ImplFallbacks(@This()); pub const PropertyFieldMap = .{ .start = css.PropertyIdTag.@"border-inline-start-color", @@ -456,3 +496,998 @@ pub const BorderInlineWidth = struct { return css.implementEql(@This(), lhs, rhs); } }; + +pub fn ImplFallbacks(comptime T: type) type { + const field_names = bun.meta.fieldNames(T); + return struct { + pub fn getFallbacks(this: *T, allocator: std.mem.Allocator, targets: css.Targets) css.SmallList(T, 2) { + const ColorFallbackKind = css.css_values.color.ColorFallbackKind; + var fallbacks = ColorFallbackKind.empty(); + inline for (field_names) |name| { + fallbacks.insert(@field(this, name).getNecessaryFallbacks(targets)); + } + + var res = css.SmallList(T, 2){}; + if (fallbacks.contains(ColorFallbackKind{ .rgb = true })) { + var out: T = undefined; + inline for (field_names) |name| { + @field(out, name) = @field(this, name).getFallback(allocator, ColorFallbackKind{ .rgb = true }); + } + res.append(allocator, out); + } + + if (fallbacks.contains(ColorFallbackKind{ .p3 = true })) { + var out: T = undefined; + inline for (field_names) |name| { + @field(out, name) = @field(this, name).getFallback(allocator, ColorFallbackKind{ .p3 = true }); + } + res.append(allocator, out); + } + + if (fallbacks.contains(ColorFallbackKind{ .lab = true })) { + inline for (field_names) |name| { + @field(this, name) = @field(this, name).getFallback(allocator, ColorFallbackKind{ .lab = true }); + } + } + + return res; + } + }; +} + +const BorderShorthand = struct { + width: ?BorderSideWidth = null, + style: ?LineStyle = null, + color: ?CssColor = null, + + pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { + return css.implementEql(@This(), lhs, rhs); + } + + pub fn setBorder(this: *@This(), allocator: std.mem.Allocator, border: anytype) void { + this.width = border.width.deepClone(allocator); + this.style = border.style.deepClone(allocator); + this.color = border.color.deepClone(allocator); + } + + fn reset(this: *@This(), allocator: std.mem.Allocator) void { + bun.clear(&this.width, allocator); + bun.clear(&this.style, allocator); + bun.clear(&this.color, allocator); + } + + fn isValid(this: *const @This()) bool { + return this.width != null and this.style != null and this.color != null; + } + + fn toBorder(this: *const @This(), allocator: std.mem.Allocator) Border { + return .{ + .width = css.generic.deepClone(@TypeOf(this.width), &this.width, allocator).?, + .style = css.generic.deepClone(@TypeOf(this.style), &this.style, allocator).?, + .color = css.generic.deepClone(@TypeOf(this.color), &this.color, allocator).?, + }; + } +}; + +const BorderProperty = packed struct(u32) { + @"border-top-color": bool = false, + @"border-bottom-color": bool = false, + @"border-left-color": bool = false, + @"border-right-color": bool = false, + @"border-block-start-color": bool = false, + @"border-block-end-color": bool = false, + @"border-inline-start-color": bool = false, + @"border-inline-end-color": bool = false, + @"border-top-width": bool = false, + @"border-bottom-width": bool = false, + @"border-left-width": bool = false, + @"border-right-width": bool = false, + @"border-block-start-width": bool = false, + @"border-block-end-width": bool = false, + @"border-inline-start-width": bool = false, + @"border-inline-end-width": bool = false, + @"border-top-style": bool = false, + @"border-bottom-style": bool = false, + @"border-left-style": bool = false, + @"border-right-style": bool = false, + @"border-block-start-style": bool = false, + @"border-block-end-style": bool = false, + @"border-inline-start-style": bool = false, + @"border-inline-end-style": bool = false, + __unused: u8 = 0, + + pub usingnamespace css.Bitflags(@This()); + + const @"border-top-color" = BorderProperty{ .@"border-top-color" = true }; + const @"border-bottom-color" = BorderProperty{ .@"border-bottom-color" = true }; + const @"border-left-color" = BorderProperty{ .@"border-left-color" = true }; + const @"border-right-color" = BorderProperty{ .@"border-right-color" = true }; + const @"border-block-start-color" = BorderProperty{ .@"border-block-start-color" = true }; + const @"border-block-end-color" = BorderProperty{ .@"border-block-end-color" = true }; + const @"border-inline-start-color" = BorderProperty{ .@"border-inline-start-color" = true }; + const @"border-inline-end-color" = BorderProperty{ .@"border-inline-end-color" = true }; + const @"border-top-width" = BorderProperty{ .@"border-top-width" = true }; + const @"border-bottom-width" = BorderProperty{ .@"border-bottom-width" = true }; + const @"border-left-width" = BorderProperty{ .@"border-left-width" = true }; + const @"border-right-width" = BorderProperty{ .@"border-right-width" = true }; + const @"border-block-start-width" = BorderProperty{ .@"border-block-start-width" = true }; + const @"border-block-end-width" = BorderProperty{ .@"border-block-end-width" = true }; + const @"border-inline-start-width" = BorderProperty{ .@"border-inline-start-width" = true }; + const @"border-inline-end-width" = BorderProperty{ .@"border-inline-end-width" = true }; + const @"border-top-style" = BorderProperty{ .@"border-top-style" = true }; + const @"border-bottom-style" = BorderProperty{ .@"border-bottom-style" = true }; + const @"border-left-style" = BorderProperty{ .@"border-left-style" = true }; + const @"border-right-style" = BorderProperty{ .@"border-right-style" = true }; + const @"border-block-start-style" = BorderProperty{ .@"border-block-start-style" = true }; + const @"border-block-end-style" = BorderProperty{ .@"border-block-end-style" = true }; + const @"border-inline-start-style" = BorderProperty{ .@"border-inline-start-style" = true }; + const @"border-inline-end-style" = BorderProperty{ .@"border-inline-end-style" = true }; + + const @"border-block-color" = BorderProperty{ .@"border-block-start-color" = true, .@"border-block-end-color" = true }; + const @"border-inline-color" = BorderProperty{ .@"border-inline-start-color" = true, .@"border-inline-end-color" = true }; + const @"border-block-width" = BorderProperty{ .@"border-block-start-width" = true, .@"border-block-end-width" = true }; + const @"border-inline-width" = BorderProperty{ .@"border-inline-start-width" = true, .@"border-inline-end-width" = true }; + const @"border-block-style" = BorderProperty{ .@"border-block-start-style" = true, .@"border-block-end-style" = true }; + const @"border-inline-style" = BorderProperty{ .@"border-inline-start-style" = true, .@"border-inline-end-style" = true }; + const @"border-top" = BorderProperty{ .@"border-top-color" = true, .@"border-top-width" = true, .@"border-top-style" = true }; + const @"border-bottom" = BorderProperty{ .@"border-bottom-color" = true, .@"border-bottom-width" = true, .@"border-bottom-style" = true }; + const @"border-left" = BorderProperty{ .@"border-left-color" = true, .@"border-left-width" = true, .@"border-left-style" = true }; + const @"border-right" = BorderProperty{ .@"border-right-color" = true, .@"border-right-width" = true, .@"border-right-style" = true }; + const @"border-block-start" = BorderProperty{ .@"border-block-start-color" = true, .@"border-block-start-width" = true, .@"border-block-start-style" = true }; + const @"border-block-end" = BorderProperty{ .@"border-block-end-color" = true, .@"border-block-end-width" = true, .@"border-block-end-style" = true }; + const @"border-inline-start" = BorderProperty{ .@"border-inline-start-color" = true, .@"border-inline-start-width" = true, .@"border-inline-start-style" = true }; + const @"border-inline-end" = BorderProperty{ .@"border-inline-end-color" = true, .@"border-inline-end-width" = true, .@"border-inline-end-style" = true }; + const @"border-block" = BorderProperty{ .@"border-block-start-color" = true, .@"border-block-end-color" = true, .@"border-block-start-width" = true, .@"border-block-end-width" = true, .@"border-block-start-style" = true, .@"border-block-end-style" = true }; + const @"border-inline" = BorderProperty{ .@"border-inline-start-color" = true, .@"border-inline-end-color" = true, .@"border-inline-start-width" = true, .@"border-inline-end-width" = true, .@"border-inline-start-style" = true, .@"border-inline-end-style" = true }; + const @"border-width" = BorderProperty{ .@"border-left-width" = true, .@"border-right-width" = true, .@"border-top-width" = true, .@"border-bottom-width" = true }; + const @"border-style" = BorderProperty{ .@"border-left-style" = true, .@"border-right-style" = true, .@"border-top-style" = true, .@"border-bottom-style" = true }; + const @"border-color" = BorderProperty{ .@"border-left-color" = true, .@"border-right-color" = true, .@"border-top-color" = true, .@"border-bottom-color" = true }; + const border = BorderProperty{ .@"border-left-width" = true, .@"border-right-width" = true, .@"border-top-width" = true, .@"border-bottom-width" = true, .@"border-left-style" = true, .@"border-right-style" = true, .@"border-top-style" = true, .@"border-bottom-style" = true, .@"border-left-color" = true, .@"border-right-color" = true, .@"border-top-color" = true, .@"border-bottom-color" = true }; + + pub fn tryFromPropertyId(property_id: css.PropertyIdTag) ?@This() { + @setEvalBranchQuota(10000); + const fields = bun.meta.EnumFields(css.PropertyIdTag); + inline for (fields) |field| { + if (field.value == @intFromEnum(property_id)) { + if (comptime std.mem.startsWith(u8, field.name, "border") and @hasDecl(@This(), field.name)) { + return @field(@This(), field.name); + } + } + } + + return null; + } +}; + +pub const BorderHandler = struct { + border_top: BorderShorthand = .{}, + border_bottom: BorderShorthand = .{}, + border_left: BorderShorthand = .{}, + border_right: BorderShorthand = .{}, + border_block_start: BorderShorthand = .{}, + border_block_end: BorderShorthand = .{}, + border_inline_start: BorderShorthand = .{}, + border_inline_end: BorderShorthand = .{}, + category: PropertyCategory = PropertyCategory.default(), + border_image_handler: BorderImageHandler = .{}, + border_radius_handler: BorderRadiusHandler = .{}, + flushed_properties: BorderProperty = .{}, + has_any: bool = false, + + pub fn handleProperty( + this: *@This(), + property: *const css.Property, + dest: *css.DeclarationList, + context: *css.PropertyHandlerContext, + ) bool { + const allocator = context.allocator; + + const flushHelper = struct { + inline fn flushHelper(self: *BorderHandler, d: *css.DeclarationList, c: *css.PropertyHandlerContext, comptime key: []const u8, comptime prop: []const u8, val: anytype, category: PropertyCategory) void { + if (category != self.category) { + self.flush(d, c); + } + + if (@field(@field(self, key), prop) != null and !@field(@field(self, key), prop).?.eql(val) and c.targets.browsers != null and !css.generic.isCompatible(@TypeOf(val.*), val, c.targets.browsers.?)) { + self.flush(d, c); + } + } + }.flushHelper; + + const propertyHelper = struct { + inline fn propertyHelper(self: *BorderHandler, d: *css.DeclarationList, c: *css.PropertyHandlerContext, comptime key: []const u8, comptime prop: []const u8, val: anytype, category: PropertyCategory) void { + flushHelper(self, d, c, key, prop, val, category); + @field(@field(self, key), prop) = val.deepClone(c.allocator); + self.category = category; + self.has_any = true; + } + }.propertyHelper; + + const setBorderHelper = struct { + inline fn setBorderHelper(self: *BorderHandler, d: *css.DeclarationList, c: *css.PropertyHandlerContext, comptime key: []const u8, val: anytype, category: PropertyCategory) void { + if (category != self.category) { + self.flush(d, c); + } + + @field(self, key).setBorder(c.allocator, val); + self.category = category; + self.has_any = true; + } + }.setBorderHelper; + + switch (property.*) { + .@"border-top-color" => |*val| propertyHelper(this, dest, context, "border_top", "color", val, .physical), + .@"border-bottom-color" => |*val| propertyHelper(this, dest, context, "border_bottom", "color", val, .physical), + .@"border-left-color" => |*val| propertyHelper(this, dest, context, "border_left", "color", val, .physical), + .@"border-right-color" => |*val| propertyHelper(this, dest, context, "border_right", "color", val, .physical), + .@"border-block-start-color" => |*val| propertyHelper(this, dest, context, "border_block_start", "color", val, .logical), + .@"border-block-end-color" => |*val| propertyHelper(this, dest, context, "border_block_end", "color", val, .logical), + .@"border-block-color" => |val| { + propertyHelper(this, dest, context, "border_block_start", "color", &val.start, .logical); + propertyHelper(this, dest, context, "border_block_end", "color", &val.end, .logical); + }, + .@"border-inline-start-color" => |*val| propertyHelper(this, dest, context, "border_inline_start", "color", val, .logical), + .@"border-inline-end-color" => |*val| propertyHelper(this, dest, context, "border_inline_end", "color", val, .logical), + .@"border-inline-color" => |val| { + propertyHelper(this, dest, context, "border_inline_start", "color", &val.start, .logical); + propertyHelper(this, dest, context, "border_inline_end", "color", &val.end, .logical); + }, + .@"border-top-width" => |*val| propertyHelper(this, dest, context, "border_top", "width", val, .physical), + .@"border-bottom-width" => |*val| propertyHelper(this, dest, context, "border_bottom", "width", val, .physical), + .@"border-left-width" => |*val| propertyHelper(this, dest, context, "border_left", "width", val, .physical), + .@"border-right-width" => |*val| propertyHelper(this, dest, context, "border_right", "width", val, .physical), + .@"border-block-start-width" => |*val| propertyHelper(this, dest, context, "border_block_start", "width", val, .logical), + .@"border-block-end-width" => |*val| propertyHelper(this, dest, context, "border_block_end", "width", val, .logical), + .@"border-block-width" => |val| { + propertyHelper(this, dest, context, "border_block_start", "width", &val.start, .logical); + propertyHelper(this, dest, context, "border_block_end", "width", &val.end, .logical); + }, + .@"border-inline-start-width" => |*val| propertyHelper(this, dest, context, "border_inline_start", "width", val, .logical), + .@"border-inline-end-width" => |*val| propertyHelper(this, dest, context, "border_inline_end", "width", val, .logical), + .@"border-inline-width" => |val| { + propertyHelper(this, dest, context, "border_inline_start", "width", &val.start, .logical); + propertyHelper(this, dest, context, "border_inline_end", "width", &val.end, .logical); + }, + .@"border-top-style" => |*val| propertyHelper(this, dest, context, "border_top", "style", val, .physical), + .@"border-bottom-style" => |*val| propertyHelper(this, dest, context, "border_bottom", "style", val, .physical), + .@"border-left-style" => |*val| propertyHelper(this, dest, context, "border_left", "style", val, .physical), + .@"border-right-style" => |*val| propertyHelper(this, dest, context, "border_right", "style", val, .physical), + .@"border-block-start-style" => |*val| propertyHelper(this, dest, context, "border_block_start", "style", val, .logical), + .@"border-block-end-style" => |*val| propertyHelper(this, dest, context, "border_block_end", "style", val, .logical), + .@"border-block-style" => |val| { + propertyHelper(this, dest, context, "border_block_start", "style", &val.start, .logical); + propertyHelper(this, dest, context, "border_block_end", "style", &val.end, .logical); + }, + .@"border-inline-start-style" => |*val| propertyHelper(this, dest, context, "border_inline_start", "style", val, .logical), + .@"border-inline-end-style" => |*val| propertyHelper(this, dest, context, "border_inline_end", "style", val, .logical), + .@"border-inline-style" => |val| { + propertyHelper(this, dest, context, "border_inline_start", "style", &val.start, .logical); + propertyHelper(this, dest, context, "border_inline_end", "style", &val.end, .logical); + }, + .@"border-top" => |*val| setBorderHelper(this, dest, context, "border_top", val, .physical), + .@"border-bottom" => |*val| setBorderHelper(this, dest, context, "border_bottom", val, .physical), + .@"border-left" => |*val| setBorderHelper(this, dest, context, "border_left", val, .physical), + .@"border-right" => |*val| setBorderHelper(this, dest, context, "border_right", val, .physical), + .@"border-block-start" => |*val| setBorderHelper(this, dest, context, "border_block_start", val, .logical), + .@"border-block-end" => |*val| setBorderHelper(this, dest, context, "border_block_end", val, .logical), + .@"border-inline-start" => |*val| setBorderHelper(this, dest, context, "border_inline_start", val, .logical), + .@"border-inline-end" => |*val| setBorderHelper(this, dest, context, "border_inline_end", val, .logical), + .@"border-block" => |*val| { + setBorderHelper(this, dest, context, "border_block_start", val, .logical); + setBorderHelper(this, dest, context, "border_block_end", val, .logical); + }, + .@"border-inline" => |*val| { + setBorderHelper(this, dest, context, "border_inline_start", val, .logical); + setBorderHelper(this, dest, context, "border_inline_end", val, .logical); + }, + .@"border-width" => |*val| { + propertyHelper(this, dest, context, "border_top", "width", &val.top, .physical); + propertyHelper(this, dest, context, "border_right", "width", &val.right, .physical); + propertyHelper(this, dest, context, "border_bottom", "width", &val.bottom, .physical); + propertyHelper(this, dest, context, "border_left", "width", &val.left, .physical); + + bun.clear(&this.border_block_start.width, context.allocator); + bun.clear(&this.border_block_end.width, context.allocator); + bun.clear(&this.border_inline_start.width, context.allocator); + bun.clear(&this.border_inline_end.width, context.allocator); + this.has_any = true; + }, + .@"border-style" => |*val| { + propertyHelper(this, dest, context, "border_top", "style", &val.top, .physical); + propertyHelper(this, dest, context, "border_right", "style", &val.right, .physical); + propertyHelper(this, dest, context, "border_bottom", "style", &val.bottom, .physical); + propertyHelper(this, dest, context, "border_left", "style", &val.left, .physical); + + bun.clear(&this.border_block_start.style, context.allocator); + bun.clear(&this.border_block_end.style, context.allocator); + bun.clear(&this.border_inline_start.style, context.allocator); + bun.clear(&this.border_inline_end.style, context.allocator); + this.has_any = true; + }, + .@"border-color" => |*val| { + propertyHelper(this, dest, context, "border_top", "color", &val.top, .physical); + propertyHelper(this, dest, context, "border_right", "color", &val.right, .physical); + propertyHelper(this, dest, context, "border_bottom", "color", &val.bottom, .physical); + propertyHelper(this, dest, context, "border_left", "color", &val.left, .physical); + + bun.clear(&this.border_block_start.color, context.allocator); + bun.clear(&this.border_block_end.color, context.allocator); + bun.clear(&this.border_inline_start.color, context.allocator); + bun.clear(&this.border_inline_end.color, context.allocator); + this.has_any = true; + }, + .border => |*val| { + this.border_top.setBorder(context.allocator, val); + this.border_bottom.setBorder(context.allocator, val); + this.border_left.setBorder(context.allocator, val); + this.border_right.setBorder(context.allocator, val); + + this.border_block_start.reset(allocator); + this.border_block_end.reset(allocator); + this.border_inline_start.reset(allocator); + this.border_inline_end.reset(allocator); + + // Setting the `border` property resets `border-image` + this.border_image_handler.reset(allocator); + this.has_any = true; + }, + .unparsed => |*val| { + if (isBorderProperty(val.property_id)) { + this.flush(dest, context); + this.flushUnparsed(val, dest, context); + } else { + if (this.border_image_handler.willFlush(property)) { + this.flush(dest, context); + } + return this.border_image_handler.handleProperty(property, dest, context) or this.border_radius_handler.handleProperty(property, dest, context); + } + }, + else => { + if (this.border_image_handler.willFlush(property)) { + this.flush(dest, context); + } + return this.border_image_handler.handleProperty(property, dest, context) or this.border_radius_handler.handleProperty(property, dest, context); + }, + } + + return true; + } + + pub fn finalize(this: *@This(), dest: *css.DeclarationList, context: *css.PropertyHandlerContext) void { + this.flush(dest, context); + this.flushed_properties = .{}; + this.border_image_handler.finalize(dest, context); + this.border_radius_handler.finalize(dest, context); + } + + const FlushContext = struct { + self: *BorderHandler, + dest: *css.DeclarationList, + ctx: *css.PropertyHandlerContext, + logical_supported: bool, + logical_shorthand_supported: bool, + + inline fn logicalProp(f: *FlushContext, comptime ltr: []const u8, comptime ltr_key: []const u8, comptime rtl: []const u8, comptime rtl_key: []const u8, val: anytype) void { + _ = ltr_key; // autofix + _ = rtl_key; // autofix + f.ctx.addLogicalRule(f.ctx.allocator, @unionInit(css.Property, ltr, val.deepClone(f.ctx.allocator)), @unionInit(css.Property, rtl, val.deepClone(f.ctx.allocator))); + } + + inline fn push(f: *FlushContext, comptime p: []const u8, val: anytype) void { + f.self.flushed_properties.insert(@field(BorderProperty, p)); + f.dest.append(f.ctx.allocator, @unionInit(css.Property, p, val.deepClone(f.ctx.allocator))) catch bun.outOfMemory(); + } + + inline fn fallbacks(f: *FlushContext, comptime p: []const u8, _val: anytype) void { + var val = _val; + if (!f.self.flushed_properties.contains(@field(BorderProperty, p))) { + const fbs = val.getFallbacks(f.ctx.allocator, f.ctx.targets); + for (css.generic.slice(@TypeOf(fbs), &fbs)) |fallback| { + f.dest.append(f.ctx.allocator, @unionInit(css.Property, p, fallback)) catch bun.outOfMemory(); + } + } + push(f, p, val); + } + + inline fn prop(f: *FlushContext, comptime prop_name: []const u8, val: anytype) void { + @setEvalBranchQuota(10000); + if (comptime std.mem.eql(u8, prop_name, "border-inline-start")) { + if (f.logical_supported) { + fallbacks(f, "border-inline-start", val); + } else { + logicalProp(f, "border-left", "border_left", "border-right", "border_right", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-inline-start-width")) { + if (f.logical_supported) { + push(f, "border-inline-start-width", val); + } else { + logicalProp(f, "border-left-width", "border_left_width", "border-right-width", "border_right_width", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-inline-start-color")) { + if (f.logical_supported) { + fallbacks(f, "border-inline-start-color", val); + } else { + logicalProp(f, "border-left-color", "border_left_color", "border-right-color", "border_right_color", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-inline-start-style")) { + if (f.logical_supported) { + push(f, "border-inline-start-style", val); + } else { + logicalProp(f, "border-left-style", "border_left_style", "border-right-style", "border_right_style", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-inline-end")) { + if (f.logical_supported) { + fallbacks(f, "border-inline-end", val); + } else { + logicalProp(f, "border-right", "border_right", "border-left", "border_left", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-inline-end-width")) { + if (f.logical_supported) { + push(f, "border-inline-end-width", val); + } else { + logicalProp(f, "border-right-width", "border_right_width", "border-left-width", "border_left_width", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-inline-end-color")) { + if (f.logical_supported) { + fallbacks(f, "border-inline-end-color", val); + } else { + logicalProp(f, "border-right-color", "border_right_color", "border-left-color", "border_left_color", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-inline-end-style")) { + if (f.logical_supported) { + push(f, "border-inline-end-style", val); + } else { + logicalProp(f, "border-right-style", "border_right_style", "border-left-style", "border_left_style", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-block-start")) { + if (f.logical_supported) { + fallbacks(f, "border-block-start", val); + } else { + fallbacks(f, "border-top", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-block-start-width")) { + if (f.logical_supported) { + push(f, "border-block-start-width", val); + } else { + push(f, "border-top-width", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-block-start-color")) { + if (f.logical_supported) { + fallbacks(f, "border-block-start-color", val); + } else { + fallbacks(f, "border-top-color", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-block-start-style")) { + if (f.logical_supported) { + push(f, "border-block-start-style", val); + } else { + push(f, "border-top-style", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-block-end")) { + if (f.logical_supported) { + fallbacks(f, "border-block-end", val); + } else { + fallbacks(f, "border-bottom", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-block-end-width")) { + if (f.logical_supported) { + push(f, "border-block-end-width", val); + } else { + push(f, "border-bottom-width", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-block-end-color")) { + if (f.logical_supported) { + fallbacks(f, "border-block-end-color", val); + } else { + fallbacks(f, "border-bottom-color", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-block-end-style")) { + if (f.logical_supported) { + push(f, "border-block-end-style", val); + } else { + push(f, "border-bottom-style", val); + } + } else if (comptime std.mem.eql(u8, prop_name, "border-left-color") or + std.mem.eql(u8, prop_name, "border-right-color") or + std.mem.eql(u8, prop_name, "border-top-color") or + std.mem.eql(u8, prop_name, "border-bottom-color") or + std.mem.eql(u8, prop_name, "border-color") or + std.mem.eql(u8, prop_name, "border-block-color") or + std.mem.eql(u8, prop_name, "border-inline-color") or + std.mem.eql(u8, prop_name, "border-left") or + std.mem.eql(u8, prop_name, "border-right") or + std.mem.eql(u8, prop_name, "border-top") or + std.mem.eql(u8, prop_name, "border-bottom") or + std.mem.eql(u8, prop_name, "border-block-start") or + std.mem.eql(u8, prop_name, "border-block-end") or + std.mem.eql(u8, prop_name, "border-inline-start") or + std.mem.eql(u8, prop_name, "border-inline-end") or + std.mem.eql(u8, prop_name, "border-inline") or + std.mem.eql(u8, prop_name, "border-block") or + std.mem.eql(u8, prop_name, "border")) + { + fallbacks(f, prop_name, val); + } else { + push(f, prop_name, val); + } + } + + fn flushCategory( + f: *FlushContext, + comptime block_start_prop: []const u8, + comptime block_start_width: []const u8, + comptime block_start_style: []const u8, + comptime block_start_color: []const u8, + block_start: anytype, + comptime block_end_prop: []const u8, + comptime block_end_width: []const u8, + comptime block_end_style: []const u8, + comptime block_end_color: []const u8, + block_end: anytype, + comptime inline_start_prop: []const u8, + comptime inline_start_width: []const u8, + comptime inline_start_style: []const u8, + comptime inline_start_color: []const u8, + inline_start: anytype, + comptime inline_end_prop: []const u8, + comptime inline_end_width: []const u8, + comptime inline_end_style: []const u8, + comptime inline_end_color: []const u8, + inline_end: anytype, + comptime is_logical: bool, + ) void { + const State = struct { + f: *FlushContext, + block_start: @TypeOf(block_start), + block_end: @TypeOf(block_end), + inline_start: @TypeOf(inline_start), + inline_end: @TypeOf(inline_end), + + inline fn shorthand(s: *@This(), comptime p: type, comptime prop_name: []const u8, comptime key: []const u8) void { + const has_prop = @field(s.block_start, key) != null and @field(s.block_end, key) != null and @field(s.inline_start, key) != null and @field(s.inline_end, key) != null; + if (has_prop) { + if (!is_logical or css.generic.eql(@TypeOf(@field(s.block_start, key)), &@field(s.block_start, key), &@field(s.block_end, key)) and + css.generic.eql(@TypeOf(@field(s.block_end, key)), &@field(s.block_end, key), &@field(s.inline_start, key)) and + css.generic.eql(@TypeOf(@field(s.inline_start, key)), &@field(s.inline_start, key), &@field(s.inline_end, key))) + { + const rect = p{ + .top = bun.take(&@field(s.block_start, key)).?, + .right = bun.take(&@field(s.inline_end, key)).?, + .bottom = bun.take(&@field(s.block_end, key)).?, + .left = bun.take(&@field(s.inline_start, key)).?, + }; + prop(s.f, prop_name, rect); + } + } + } + + inline fn logicalShorthand( + s: *@This(), + comptime P: type, + comptime prop_name: []const u8, + comptime key: []const u8, + start: anytype, + end: anytype, + ) void { + const has_prop = @field(start, key) != null and @field(end, key) != null; + if (has_prop) { + prop(s.f, prop_name, P{ + .start = bun.take(&@field(start, key)).?, + .end = bun.take(&@field(end, key)).?, + }); + bun.clear(&@field(end, key), s.f.ctx.allocator); + } + } + + inline fn is_eq(s: *@This(), comptime key: []const u8) bool { + return css.generic.eql(@TypeOf(@field(s.block_start, key)), &@field(s.block_start, key), &@field(s.block_end, key)) and + css.generic.eql(@TypeOf(@field(s.inline_start, key)), &@field(s.inline_start, key), &@field(s.inline_end, key)) and + css.generic.eql(@TypeOf(@field(s.inline_start, key)), &@field(s.inline_start, key), &@field(s.block_start, key)); + } + + inline fn prop_diff(s: *@This(), border: anytype, fallback: anytype, border_fallback: anytype) void { + if (!is_logical and + s.is_eq("color") and + s.is_eq("style")) + { + prop(s.f, "border", border.toBorder(s.f.ctx.allocator)); + shorthand(s, BorderWidth, "border-width", "width"); + } else if (!is_logical and + s.is_eq("width") and + s.is_eq("style")) + { + prop(s.f, "border", border.toBorder(s.f.ctx.allocator)); + shorthand(s, BorderColor, "border-color", "color"); + } else if (!is_logical and + s.is_eq("width") and + s.is_eq("color")) + { + prop(s.f, "border", border.toBorder(s.f.ctx.allocator)); + shorthand(s, BorderStyle, "border-style", "style"); + } else { + if (border_fallback) { + prop(s.f, "border", border.toBorder(s.f.ctx.allocator)); + } + fallback(s); + } + } + + inline fn side_diff(s: *@This(), border: anytype, other: anytype, comptime prop_name: []const u8, width: anytype, style: anytype, comptime color: []const u8) void { + const eq_width = css.generic.eql(@TypeOf(border.width), &border.width, &other.width); + const eq_style = css.generic.eql(@TypeOf(border.style), &border.style, &other.style); + const eq_color = css.generic.eql(@TypeOf(border.color), &border.color, &other.color); + + // If only one of the sub-properties is different, only emit that. + // Otherwise, emit the full border value. + if (eq_width and eq_style) { + s.f.prop(color, css.generic.deepClone(@TypeOf(other.color), &other.color, s.f.ctx.allocator).?); + } else if (eq_width and eq_color) { + s.f.prop(style, css.generic.deepClone(@TypeOf(other.style), &other.style, s.f.ctx.allocator).?); + } else if (eq_style and eq_color) { + s.f.prop(width, css.generic.deepClone(@TypeOf(other.width), &other.width, s.f.ctx.allocator).?); + } else { + s.f.prop(prop_name, other.toBorder(s.f.ctx.allocator)); + } + } + + inline fn side(s: *@This(), val: anytype, comptime short: []const u8, comptime width: []const u8, comptime style: []const u8, comptime color: []const u8) void { + if (val.isValid()) { + s.f.prop(short, val.toBorder(s.f.ctx.allocator)); + } else { + if (val.style) |*sty| { + s.f.prop(style, sty.deepClone(s.f.ctx.allocator)); + } + + if (val.width) |*w| { + s.f.prop(width, w.deepClone(s.f.ctx.allocator)); + } + + if (val.color) |*c| { + s.f.prop(color, c.deepClone(s.f.ctx.allocator)); + } + } + } + + // If both values of an inline logical property are equal, then we can just convert them to physical properties. + inline fn inlineProp(s: *@This(), comptime key: []const u8, comptime left: []const u8, comptime right: []const u8) void { + if (@field(s.inline_start, key) != null and css.generic.eql(@TypeOf(@field(s.inline_start, key)), &@field(s.inline_start, key), &@field(s.inline_end, key))) { + s.f.prop(left, bun.take(&@field(s.inline_start, key)).?); + s.f.prop(right, bun.take(&@field(s.inline_end, key)).?); + } + } + }; + + var state = State{ + .f = f, + .block_start = block_start, + .block_end = block_end, + .inline_start = inline_start, + .inline_end = inline_end, + }; + + if (block_start.isValid() and block_end.isValid() and inline_start.isValid() and inline_end.isValid()) { + const top_eq_bottom = block_start.eql(block_end); + const left_eq_right = inline_start.eql(inline_end); + const top_eq_left = block_start.eql(inline_start); + const top_eq_right = block_start.eql(inline_end); + const bottom_eq_left = block_end.eql(inline_start); + const bottom_eq_right = block_end.eql(inline_end); + + if (top_eq_bottom and top_eq_left and top_eq_right) { + state.f.prop("border", block_start.toBorder(f.ctx.allocator)); + } else if (top_eq_bottom and top_eq_left) { + state.f.prop("border", block_start.toBorder(f.ctx.allocator)); + state.side_diff(block_start, inline_end, inline_end_prop, inline_end_width, inline_end_style, inline_end_color); + } else if (top_eq_bottom and top_eq_right) { + state.f.prop("border", block_start.toBorder(f.ctx.allocator)); + state.side_diff(block_start, inline_start, inline_start_prop, inline_start_width, inline_start_style, inline_start_color); + } else if (left_eq_right and bottom_eq_left) { + state.f.prop("border", inline_start.toBorder(f.ctx.allocator)); + state.side_diff(inline_start, block_start, block_start_prop, block_start_width, block_start_style, block_start_color); + } else if (left_eq_right and top_eq_left) { + state.f.prop("border", inline_start.toBorder(f.ctx.allocator)); + state.side_diff(inline_start, block_end, block_end_prop, block_end_width, block_end_style, block_end_color); + } else if (top_eq_bottom) { + state.prop_diff(block_start, struct { + fn fallback(s: *State) void { + // Try to use border-inline shorthands for the opposite direction if possible + var handled = false; + if (is_logical) { + var diff: u32 = 0; + if (!css.generic.eql(@TypeOf(s.inline_start.width), &s.inline_start.width, &s.block_start.width) or + !css.generic.eql(@TypeOf(s.inline_end.width), &s.inline_end.width, &s.block_start.width)) + { + diff += 1; + } + if (!css.generic.eql(@TypeOf(s.inline_start.style), &s.inline_start.style, &s.block_start.style) or + !css.generic.eql(@TypeOf(s.inline_end.style), &s.inline_end.style, &s.block_start.style)) + { + diff += 1; + } + if (!css.generic.eql(@TypeOf(s.inline_start.color), &s.inline_start.color, &s.block_start.color) or + !css.generic.eql(@TypeOf(s.inline_end.color), &s.inline_end.color, &s.block_start.color)) + { + diff += 1; + } + + if (diff == 1) { + if (!css.generic.eql(@TypeOf(s.inline_start.width), &s.inline_start.width, &s.block_start.width)) { + s.f.prop("border-inline-width", BorderInlineWidth{ + .start = s.inline_start.width.?.deepClone(s.f.ctx.allocator), + .end = s.inline_end.width.?.deepClone(s.f.ctx.allocator), + }); + handled = true; + } else if (!css.generic.eql(@TypeOf(s.inline_start.style), &s.inline_start.style, &s.block_start.style)) { + s.f.prop("border-inline-style", BorderInlineStyle{ + .start = s.inline_start.style.?.deepClone(s.f.ctx.allocator), + .end = s.inline_end.style.?.deepClone(s.f.ctx.allocator), + }); + handled = true; + } else if (!css.generic.eql(@TypeOf(s.inline_start.color), &s.inline_start.color, &s.block_start.color)) { + s.f.prop("border-inline-color", BorderInlineColor{ + .start = s.inline_start.color.?.deepClone(s.f.ctx.allocator), + .end = s.inline_end.color.?.deepClone(s.f.ctx.allocator), + }); + handled = true; + } + } else if (diff > 1 and + css.generic.eql(@TypeOf(s.inline_start.width), &s.inline_start.width, &s.inline_end.width) and + css.generic.eql(@TypeOf(s.inline_start.style), &s.inline_start.style, &s.inline_end.style) and + css.generic.eql(@TypeOf(s.inline_start.color), &s.inline_start.color, &s.inline_end.color)) + { + s.f.prop("border-inline", s.inline_start.toBorder(s.f.ctx.allocator)); + handled = true; + } + } + + if (!handled) { + s.side_diff(s.block_start, s.inline_start, inline_start_prop, inline_start_width, inline_start_style, inline_start_color); + s.side_diff(s.block_start, s.inline_end, inline_end_prop, inline_end_width, inline_end_style, inline_end_color); + } + } + }.fallback, true); + } else if (left_eq_right) { + state.prop_diff(inline_start, struct { + fn fallback(s: *State) void { + // We know already that top != bottom, so no need to try to use border-block. + s.side_diff(s.inline_start, s.block_start, block_start_prop, block_start_width, block_start_style, block_start_color); + s.side_diff(s.inline_start, s.block_end, block_end_prop, block_end_width, block_end_style, block_end_color); + } + }.fallback, true); + } else if (bottom_eq_right) { + state.prop_diff(block_end, struct { + fn fallback(s: *State) void { + s.side_diff(s.block_end, s.block_start, block_start_prop, block_start_width, block_start_style, block_start_color); + s.side_diff(s.block_end, s.inline_start, inline_start_prop, inline_start_width, inline_start_style, inline_start_color); + } + }.fallback, true); + } else { + state.prop_diff(block_start, struct { + fn fallback(s: *State) void { + s.f.prop(block_start_prop, s.block_start.toBorder(s.f.ctx.allocator)); + s.f.prop(block_end_prop, s.block_end.toBorder(s.f.ctx.allocator)); + s.f.prop(inline_start_prop, s.inline_start.toBorder(s.f.ctx.allocator)); + s.f.prop(inline_end_prop, s.inline_end.toBorder(s.f.ctx.allocator)); + } + }.fallback, false); + } + } else { + state.shorthand(BorderStyle, "border-style", "style"); + state.shorthand(BorderWidth, "border-width", "width"); + state.shorthand(BorderColor, "border-color", "color"); + + if (is_logical and block_start.eql(block_end) and block_start.isValid()) { + if (f.logical_supported) { + if (f.logical_shorthand_supported) { + state.f.prop("border-block", block_start.toBorder(f.ctx.allocator)); + } else { + state.f.prop("border-block-start", block_start.toBorder(f.ctx.allocator)); + state.f.prop("border-block-end", block_start.toBorder(f.ctx.allocator)); + } + } else { + state.f.prop("border-top", block_start.toBorder(f.ctx.allocator)); + state.f.prop("border-bottom", block_start.toBorder(f.ctx.allocator)); + } + } else { + if (is_logical and f.logical_shorthand_supported and !block_start.isValid() and !block_end.isValid()) { + state.logicalShorthand(BorderBlockStyle, "border-block-style", "style", block_start, block_end); + state.logicalShorthand(BorderBlockWidth, "border-block-width", "width", block_start, block_end); + state.logicalShorthand(BorderBlockColor, "border-block-color", "color", block_start, block_end); + } + + state.side(block_start, block_start_prop, block_start_width, block_start_style, block_start_color); + state.side(block_end, block_end_prop, block_end_width, block_end_style, block_end_color); + } + + if (is_logical and inline_start.eql(inline_end) and inline_start.isValid()) { + if (f.logical_supported) { + if (f.logical_shorthand_supported) { + state.f.prop("border-inline", inline_start.toBorder(f.ctx.allocator)); + } else { + state.f.prop("border-inline-start", inline_start.toBorder(f.ctx.allocator)); + state.f.prop("border-inline-end", inline_start.toBorder(f.ctx.allocator)); + } + } else { + state.f.prop("border-left", inline_start.toBorder(f.ctx.allocator)); + state.f.prop("border-right", inline_start.toBorder(f.ctx.allocator)); + } + } else { + if (is_logical and !inline_start.isValid() and !inline_end.isValid()) { + if (f.logical_shorthand_supported) { + state.logicalShorthand(BorderInlineStyle, "border-inline-style", "style", inline_start, inline_end); + state.logicalShorthand(BorderInlineWidth, "border-inline-width", "width", inline_start, inline_end); + state.logicalShorthand(BorderInlineColor, "border-inline-color", "color", inline_start, inline_end); + } else { + // If both values of an inline logical property are equal, then we can just convert them to physical properties. + state.inlineProp("style", "border-left-style", "border-right-style"); + state.inlineProp("width", "border-left-width", "border-right-width"); + state.inlineProp("color", "border-left-color", "border-right-color"); + } + } + + state.side(inline_start, inline_start_prop, inline_start_width, inline_start_style, inline_start_color); + state.side(inline_end, inline_end_prop, inline_end_width, inline_end_style, inline_end_color); + } + } + } + }; + + fn flush(this: *@This(), dest: *css.DeclarationList, context: *css.PropertyHandlerContext) void { + if (!this.has_any) return; + + this.has_any = false; + + const logical_supported = !context.shouldCompileLogical(css.Feature.logical_borders); + const logical_shorthand_supported = !context.shouldCompileLogical(css.Feature.logical_border_shorthand); + + var flctx = FlushContext{ + .self = this, + .dest = dest, + .ctx = context, + .logical_supported = logical_supported, + .logical_shorthand_supported = logical_shorthand_supported, + }; + + flctx.flushCategory( + "border-top", + "border-top-width", + "border-top-style", + "border-top-color", + &this.border_top, + + "border-bottom", + "border-bottom-width", + "border-bottom-style", + "border-bottom-color", + &this.border_bottom, + + "border-left", + "border-left-width", + "border-left-style", + "border-left-color", + &this.border_left, + + "border-right", + "border-right-width", + "border-right-style", + "border-right-color", + &this.border_right, + + false, + ); + + flctx.flushCategory( + "border-block-start", + "border-block-start-width", + "border-block-start-style", + "border-block-start-color", + &this.border_block_start, + + "border-block-end", + "border-block-end-width", + "border-block-end-style", + "border-block-end-color", + &this.border_block_end, + + "border-inline-start", + "border-inline-start-width", + "border-inline-start-style", + "border-inline-start-color", + &this.border_inline_start, + + "border-inline-end", + "border-inline-end-width", + "border-inline-end-style", + "border-inline-end-color", + &this.border_inline_end, + + true, + ); + + this.border_top.reset(context.allocator); + this.border_bottom.reset(context.allocator); + this.border_left.reset(context.allocator); + this.border_right.reset(context.allocator); + this.border_block_start.reset(context.allocator); + this.border_block_end.reset(context.allocator); + this.border_inline_start.reset(context.allocator); + this.border_inline_end.reset(context.allocator); + } + + fn flushUnparsed(this: *@This(), unparsed: *const UnparsedProperty, dest: *css.DeclarationList, context: *css.PropertyHandlerContext) void { + const logical_supported = !context.shouldCompileLogical(css.Feature.logical_borders); + if (logical_supported) { + var up = unparsed.deepClone(context.allocator); + context.addUnparsedFallbacks(&up); + this.flushed_properties.insert(BorderProperty.tryFromPropertyId(up.property_id).?); + dest.append(context.allocator, .{ .unparsed = up }) catch bun.outOfMemory(); + return; + } + + const prop = struct { + inline fn prop(self: *BorderHandler, d: *css.DeclarationList, c: *css.PropertyHandlerContext, up: *const UnparsedProperty, comptime id: []const u8) void { + _ = d; // autofix + var upppppppppp = up.withPropertyId(c.allocator, @unionInit(css.PropertyId, id, {})); + c.addUnparsedFallbacks(&upppppppppp); + self.flushed_properties.insert(@field(BorderProperty, id)); + } + }.prop; + + const logical_prop = struct { + inline fn logical_prop( + c: *css.PropertyHandlerContext, + up: *const UnparsedProperty, + comptime ltr: []const u8, + comptime rtl: []const u8, + ) void { + c.addLogicalRule( + c.allocator, + css.Property{ + .unparsed = up.withPropertyId( + c.allocator, + @unionInit(css.PropertyId, ltr, {}), + ), + }, + css.Property{ + .unparsed = up.withPropertyId( + c.allocator, + @unionInit(css.PropertyId, rtl, {}), + ), + }, + ); + } + }.logical_prop; + + switch (unparsed.property_id) { + .@"border-inline-start" => logical_prop(context, unparsed, "border-left", "border-right"), + .@"border-inline-start-width" => logical_prop(context, unparsed, "border-left-width", "border-right-width"), + .@"border-inline-start-color" => logical_prop(context, unparsed, "border-left-color", "border-right-color"), + .@"border-inline-start-style" => logical_prop(context, unparsed, "border-left-style", "border-right-style"), + .@"border-inline-end" => logical_prop(context, unparsed, "border-right", "border-left"), + .@"border-inline-end-width" => logical_prop(context, unparsed, "border-right-width", "border-left-width"), + .@"border-inline-end-color" => logical_prop(context, unparsed, "border-right-color", "border-left-color"), + .@"border-inline-end-style" => logical_prop(context, unparsed, "border-right-style", "border-left-style"), + .@"border-block-start" => prop(this, dest, context, unparsed, "border-top"), + .@"border-block-start-width" => prop(this, dest, context, unparsed, "border-top-width"), + .@"border-block-start-color" => prop(this, dest, context, unparsed, "border-top-color"), + .@"border-block-start-style" => prop(this, dest, context, unparsed, "border-top-style"), + .@"border-block-end" => prop(this, dest, context, unparsed, "border-bottom"), + .@"border-block-end-width" => prop(this, dest, context, unparsed, "border-bottom-width"), + .@"border-block-end-color" => prop(this, dest, context, unparsed, "border-bottom-color"), + .@"border-block-end-style" => prop(this, dest, context, unparsed, "border-bottom-style"), + else => { + var up = unparsed.deepClone(context.allocator); + context.addUnparsedFallbacks(&up); + this.flushed_properties.insert(BorderProperty.tryFromPropertyId(up.property_id).?); + dest.append(context.allocator, .{ .unparsed = up }) catch bun.outOfMemory(); + }, + } + } +}; + +fn isBorderProperty(property_id: css.PropertyIdTag) bool { + return switch (property_id) { + .@"border-top-color", .@"border-bottom-color", .@"border-left-color", .@"border-right-color", .@"border-block-start-color", .@"border-block-end-color", .@"border-block-color", .@"border-inline-start-color", .@"border-inline-end-color", .@"border-inline-color", .@"border-top-width", .@"border-bottom-width", .@"border-left-width", .@"border-right-width", .@"border-block-start-width", .@"border-block-end-width", .@"border-block-width", .@"border-inline-start-width", .@"border-inline-end-width", .@"border-inline-width", .@"border-top-style", .@"border-bottom-style", .@"border-left-style", .@"border-right-style", .@"border-block-start-style", .@"border-block-end-style", .@"border-block-style", .@"border-inline-start-style", .@"border-inline-end-style", .@"border-inline-style", .@"border-top", .@"border-bottom", .@"border-left", .@"border-right", .@"border-block-start", .@"border-block-end", .@"border-inline-start", .@"border-inline-end", .@"border-block", .@"border-inline", .@"border-width", .@"border-style", .@"border-color", .border => true, + else => false, + }; +} diff --git a/src/css/properties/border_image.zig b/src/css/properties/border_image.zig index bde899c8ee91f1..5054509d40c4c5 100644 --- a/src/css/properties/border_image.zig +++ b/src/css/properties/border_image.zig @@ -24,6 +24,8 @@ const Length = css.css_values.length.LengthValue; const Rect = css.css_values.rect.Rect; const NumberOrPercentage = css.css_values.percentage.NumberOrPercentage; const Percentage = css.css_values.percentage.Percentage; +const Property = css.Property; +const VendorPrefix = css.VendorPrefix; /// A value for the [border-image](https://www.w3.org/TR/css-backgrounds-3/#border-image) shorthand property. pub const BorderImage = struct { @@ -180,6 +182,21 @@ pub const BorderImage = struct { return; } + pub fn getFallbacks(this: *@This(), allocator: Allocator, targets: css.targets.Targets) css.SmallList(BorderImage, 6) { + var fallbacks = this.source.getFallbacks(allocator, targets); + defer fallbacks.deinit(allocator); + var res = css.SmallList(BorderImage, 6).initCapacity(allocator, fallbacks.len()); + res.setLen(fallbacks.len()); + for (fallbacks.slice(), res.slice_mut()) |fallback, *out| { + out.* = this.*; + out.source = Image.default(); + out.* = out.deepClone(allocator); + out.source = fallback; + } + + return res; + } + pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { return css.implementDeepClone(@This(), this, allocator); } @@ -230,6 +247,10 @@ pub const BorderImageRepeat = struct { } } + pub fn isCompatible(this: *const @This(), browsers: css.targets.Browsers) bool { + return this.horizontal.isCompatible(browsers) and this.vertical.isCompatible(browsers); + } + pub fn default() BorderImageRepeat { return BorderImageRepeat{ .horizontal = BorderImageRepeatKeyword.stretch, @@ -258,6 +279,14 @@ pub const BorderImageSideWidth = union(enum) { pub usingnamespace css.DeriveParse(@This()); pub usingnamespace css.DeriveToCss(@This()); + pub fn deinit(this: *const BorderImageSideWidth, allocator: std.mem.Allocator) void { + switch (this.*) { + .length_percentage => |*l| l.deinit(allocator), + .number => {}, + .auto => {}, + } + } + pub fn default() BorderImageSideWidth { return .{ .number = 1.0 }; } @@ -282,6 +311,13 @@ pub const BorderImageSideWidth = union(enum) { }, }; } + + pub fn isCompatible(this: *const @This(), browsers: css.targets.Browsers) bool { + return switch (this.*) { + .length_percentage => |*l| l.isCompatible(browsers), + else => true, + }; + } }; /// A single [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) keyword. @@ -296,6 +332,14 @@ pub const BorderImageRepeatKeyword = enum { space, pub usingnamespace css.DefineEnumProperty(@This()); + + pub fn isCompatible(this: *const @This(), browsers: css.targets.Browsers) bool { + return switch (this.*) { + .round => css.compat.Feature.border_image_repeat_round.isCompatible(browsers), + .space => css.compat.Feature.border_image_repeat_space.isCompatible(browsers), + .stretch, .repeat => true, + }; + } }; /// A value for the [border-image-slice](https://www.w3.org/TR/css-backgrounds-3/#border-image-slice) property. @@ -330,6 +374,10 @@ pub const BorderImageSlice = struct { } } + pub fn isCompatible(_: *const BorderImageSlice, _: css.targets.Browsers) bool { + return true; + } + pub fn eql(this: *const BorderImageSlice, other: *const BorderImageSlice) bool { return this.offsets.eql(&other.offsets) and this.fill == other.fill; } @@ -345,3 +393,252 @@ pub const BorderImageSlice = struct { return css.implementDeepClone(@This(), this, allocator); } }; + +pub const BorderImageProperty = packed struct(u8) { + @"border-image-source": bool = false, + @"border-image-slice": bool = false, + @"border-image-width": bool = false, + @"border-image-outset": bool = false, + @"border-image-repeat": bool = false, + __unused: u3 = 0, + + pub const @"border-image-source" = BorderImageProperty{ .@"border-image-source" = true }; + pub const @"border-image-slice" = BorderImageProperty{ .@"border-image-slice" = true }; + pub const @"border-image-width" = BorderImageProperty{ .@"border-image-width" = true }; + pub const @"border-image-outset" = BorderImageProperty{ .@"border-image-outset" = true }; + pub const @"border-image-repeat" = BorderImageProperty{ .@"border-image-repeat" = true }; + + pub usingnamespace css.Bitflags(@This()); + + pub const @"border-image" = BorderImageProperty{ + .@"border-image-source" = true, + .@"border-image-slice" = true, + .@"border-image-width" = true, + .@"border-image-outset" = true, + .@"border-image-repeat" = true, + }; + + pub fn tryFromPropertyId(property_id: css.PropertyIdTag) ?BorderImageProperty { + inline for (std.meta.fields(BorderImageProperty)) |field| { + if (comptime std.mem.eql(u8, field.name, "__unused")) continue; + const desired = comptime @field(css.PropertyIdTag, field.name); + if (desired == property_id) { + var result: BorderImageProperty = .{}; + @field(result, field.name) = true; + return result; + } + } + if (property_id == .@"border-image") { + return BorderImageProperty.@"border-image"; + } + return null; + } +}; + +pub const BorderImageHandler = struct { + source: ?Image = null, + slice: ?BorderImageSlice = null, + width: ?Rect(BorderImageSideWidth) = null, + outset: ?Rect(LengthOrNumber) = null, + repeat: ?BorderImageRepeat = null, + vendor_prefix: css.VendorPrefix = css.VendorPrefix.empty(), + flushed_properties: BorderImageProperty = BorderImageProperty.empty(), + has_any: bool = false, + + pub fn handleProperty(this: *@This(), property: *const css.Property, dest: *css.DeclarationList, context: *css.PropertyHandlerContext) bool { + const allocator = context.allocator; + + const flushHelper = struct { + inline fn flushHelper( + self: *BorderImageHandler, + d: *css.DeclarationList, + ctx: *css.PropertyHandlerContext, + comptime name: []const u8, + val: anytype, + ) void { + if (@field(self, name) != null and !@field(self, name).?.eql(val) and ctx.targets.browsers != null and css.generic.isCompatible(@TypeOf(@field(self, name).?), val, ctx.targets.browsers.?)) { + self.flush(d, ctx); + } + } + }.flushHelper; + + const propertyHelper = struct { + inline fn propertyHelper(self: *BorderImageHandler, comptime field: []const u8, comptime T: type, val: *const T, d: *css.DeclarationList, ctx: *css.PropertyHandlerContext) void { + if (!self.vendor_prefix.eql(VendorPrefix{ .none = true })) { + self.flush(d, ctx); + } + + flushHelper(self, d, ctx, field, val); + + self.vendor_prefix = VendorPrefix{ .none = true }; + @field(self, field) = val.deepClone(ctx.allocator); + self.has_any = true; + } + }.propertyHelper; + + switch (property.*) { + .@"border-image-source" => |*val| propertyHelper(this, "source", Image, val, dest, context), + .@"border-image-slice" => |*val| propertyHelper(this, "slice", BorderImageSlice, val, dest, context), + .@"border-image-width" => |*val| propertyHelper(this, "width", Rect(BorderImageSideWidth), val, dest, context), + .@"border-image-outset" => |*val| propertyHelper(this, "outset", Rect(LengthOrNumber), val, dest, context), + .@"border-image-repeat" => |*val| propertyHelper(this, "repeat", BorderImageRepeat, val, dest, context), + .@"border-image" => |_val| { + const val = &_val[0]; + const vp = _val[1]; + + flushHelper(this, dest, context, "source", &val.source); + flushHelper(this, dest, context, "slice", &val.slice); + flushHelper(this, dest, context, "width", &val.width); + flushHelper(this, dest, context, "outset", &val.outset); + flushHelper(this, dest, context, "repeat", &val.repeat); + + this.source = val.source.deepClone(allocator); + this.slice = val.slice.deepClone(allocator); + this.width = val.width.deepClone(allocator); + this.outset = val.outset.deepClone(allocator); + this.repeat = val.repeat.deepClone(allocator); + this.vendor_prefix = this.vendor_prefix.bitwiseOr(vp); + this.has_any = true; + }, + .unparsed => |unparsed| { + if (isBorderImageProperty(unparsed.property_id)) { + this.flush(dest, context); + + // Even if we weren't able to parse the value (e.g. due to var() references), + // we can still add vendor prefixes to the property itself. + var unparsed_clone = if (unparsed.property_id == .@"border-image") + unparsed.getPrefixed(allocator, context.targets, css.prefixes.Feature.border_image) + else + unparsed.deepClone(allocator); + + context.addUnparsedFallbacks(&unparsed_clone); + this.flushed_properties.insert(BorderImageProperty.tryFromPropertyId(unparsed_clone.property_id).?); + dest.append(allocator, Property{ .unparsed = unparsed_clone }) catch bun.outOfMemory(); + } else return false; + }, + else => return false, + } + + return true; + } + + pub fn finalize(this: *@This(), dest: *css.DeclarationList, context: *css.PropertyHandlerContext) void { + this.flush(dest, context); + this.flushed_properties = BorderImageProperty.empty(); + } + + pub fn reset(this: *@This(), allocator: std.mem.Allocator) void { + if (this.source) |*s| s.deinit(allocator); + // if (this.slice) |*s| s.deinit(allocator); + if (this.width) |*w| w.deinit(allocator); + if (this.outset) |*o| o.deinit(allocator); + // if (this.repeat) |*r| r.deinit(allocator); + this.source = null; + this.slice = null; + this.width = null; + this.outset = null; + this.repeat = null; + } + + pub fn willFlush(this: *const @This(), property: *const Property) bool { + return switch (property.*) { + .@"border-image-source", + .@"border-image-slice", + .@"border-image-width", + .@"border-image-outset", + .@"border-image-repeat", + => !this.vendor_prefix.eql(VendorPrefix{ .none = true }), + .unparsed => |val| isBorderImageProperty(val.property_id), + else => false, + }; + } + + fn flush(this: *@This(), dest: *css.DeclarationList, context: *css.PropertyHandlerContext) void { + if (!this.has_any) return; + const allocator = context.allocator; + + this.has_any = false; + + var source = bun.take(&this.source); + const slice = bun.take(&this.slice); + const width = bun.take(&this.width); + const outset = bun.take(&this.outset); + const repeat = bun.take(&this.repeat); + + if (source != null and slice != null and width != null and outset != null and repeat != null) { + var border_image = BorderImage{ + .source = source.?, + .slice = slice.?, + .width = width.?, + .outset = outset.?, + .repeat = repeat.?, + }; + + var prefix = this.vendor_prefix; + if (prefix.contains(css.VendorPrefix{ .none = true }) and !border_image.slice.fill) { + prefix = context.targets.prefixes(this.vendor_prefix, css.prefixes.Feature.border_image); + if (!this.flushed_properties.intersects(BorderImageProperty.@"border-image")) { + const fallbacks = border_image.getFallbacks(allocator, context.targets).slice(); + for (fallbacks) |fallback| { + // Match prefix of fallback. e.g. -webkit-linear-gradient + // can only be used in -webkit-border-image, not -moz-border-image. + // However, if border-image is unprefixed, gradients can still be. + var p = fallback.source.getVendorPrefix().intersect(prefix); + if (p.isEmpty()) { + p = prefix; + } + dest.append(allocator, css.Property{ .@"border-image" = .{ fallback, p } }) catch bun.outOfMemory(); + } + } + } + + const p = border_image.source.getVendorPrefix().intersect(prefix); + if (!p.isEmpty()) { + prefix = p; + } + + dest.append(allocator, Property{ .@"border-image" = .{ border_image, prefix } }) catch bun.outOfMemory(); + this.flushed_properties.insert(BorderImageProperty.@"border-image"); + } else { + if (source) |*mut_source| { + if (!this.flushed_properties.contains(BorderImageProperty{ .@"border-image-source" = true })) { + for (mut_source.getFallbacks(allocator, context.targets).slice()) |fallback| { + dest.append(allocator, Property{ .@"border-image-source" = fallback }) catch bun.outOfMemory(); + } + } + + dest.append(allocator, Property{ .@"border-image-source" = mut_source.* }) catch bun.outOfMemory(); + this.flushed_properties.insert(BorderImageProperty.@"border-image-source"); + } + + if (slice) |s| { + dest.append(allocator, Property{ .@"border-image-slice" = s }) catch bun.outOfMemory(); + this.flushed_properties.insert(BorderImageProperty.@"border-image-slice"); + } + + if (width) |w| { + dest.append(allocator, Property{ .@"border-image-width" = w }) catch bun.outOfMemory(); + this.flushed_properties.insert(BorderImageProperty.@"border-image-width"); + } + + if (outset) |o| { + dest.append(allocator, Property{ .@"border-image-outset" = o }) catch bun.outOfMemory(); + this.flushed_properties.insert(BorderImageProperty.@"border-image-outset"); + } + + if (repeat) |r| { + dest.append(allocator, Property{ .@"border-image-repeat" = r }) catch bun.outOfMemory(); + this.flushed_properties.insert(BorderImageProperty.@"border-image-repeat"); + } + } + + this.vendor_prefix = VendorPrefix.empty(); + } +}; + +pub fn isBorderImageProperty(property_id: css.PropertyId) bool { + return switch (property_id) { + .@"border-image-source", .@"border-image-slice", .@"border-image-width", .@"border-image-outset", .@"border-image-repeat", .@"border-image" => true, + else => false, + }; +} diff --git a/src/css/properties/border_radius.zig b/src/css/properties/border_radius.zig index befd591f75d64a..d2c9843c24309b 100644 --- a/src/css/properties/border_radius.zig +++ b/src/css/properties/border_radius.zig @@ -22,6 +22,10 @@ const Ratio = css.css_values.ratio.Ratio; const Length = css.css_values.length.LengthValue; const Rect = css.css_values.rect.Rect; const NumberOrPercentage = css.css_values.percentage.NumberOrPercentage; +const Property = css.Property; +const PropertyId = css.PropertyId; +const VendorPrefix = css.VendorPrefix; +const PropertyIdTag = css.PropertyIdTag; /// A value for the [border-radius](https://www.w3.org/TR/css-backgrounds-3/#border-radius) property. pub const BorderRadius = struct { @@ -107,3 +111,224 @@ pub const BorderRadius = struct { return css.implementEql(@This(), lhs, rhs); } }; + +pub const BorderRadiusHandler = struct { + top_left: ?struct { Size2D(LengthPercentage), css.VendorPrefix } = null, + top_right: ?struct { Size2D(LengthPercentage), css.VendorPrefix } = null, + bottom_right: ?struct { Size2D(LengthPercentage), css.VendorPrefix } = null, + bottom_left: ?struct { Size2D(LengthPercentage), css.VendorPrefix } = null, + start_start: ?css.Property = null, + start_end: ?css.Property = null, + end_end: ?css.Property = null, + end_start: ?css.Property = null, + category: css.PropertyCategory = .physical, + has_any: bool = false, + + pub fn handleProperty(this: *@This(), property: *const css.Property, dest: *css.DeclarationList, context: *css.PropertyHandlerContext) bool { + switch (property.*) { + .@"border-top-left-radius" => |val| propertyHelper(this, dest, context, "top_left", &val[0], val[1]), + .@"border-top-right-radius" => |val| propertyHelper(this, dest, context, "top_right", &val[0], val[1]), + .@"border-bottom-right-radius" => |val| propertyHelper(this, dest, context, "bottom_right", &val[0], val[1]), + .@"border-bottom-left-radius" => |val| propertyHelper(this, dest, context, "bottom_left", &val[0], val[1]), + .@"border-start-start-radius" => logicalPropertyHelper(this, dest, context, "start_start", property), + .@"border-start-end-radius" => logicalPropertyHelper(this, dest, context, "start_end", property), + .@"border-end-end-radius" => logicalPropertyHelper(this, dest, context, "end_end", property), + .@"border-end-start-radius" => logicalPropertyHelper(this, dest, context, "end_start", property), + .@"border-radius" => |val| { + this.start_start = null; + this.start_end = null; + this.end_end = null; + this.end_start = null; + + maybeFlush(this, dest, context, "top_left", &val[0].top_left, val[1]); + maybeFlush(this, dest, context, "top_right", &val[0].top_right, val[1]); + maybeFlush(this, dest, context, "bottom_right", &val[0].bottom_right, val[1]); + maybeFlush(this, dest, context, "bottom_left", &val[0].bottom_left, val[1]); + + propertyHelper(this, dest, context, "top_left", &val[0].top_left, val[1]); + propertyHelper(this, dest, context, "top_right", &val[0].top_right, val[1]); + propertyHelper(this, dest, context, "bottom_right", &val[0].bottom_right, val[1]); + propertyHelper(this, dest, context, "bottom_left", &val[0].bottom_left, val[1]); + }, + .unparsed => |unparsed| { + if (isBorderRadiusProperty(unparsed.property_id)) { + // Even if we weren't able to parse the value (e.g. due to var() references), + // we can still add vendor prefixes to the property itself. + switch (unparsed.property_id) { + .@"border-start-start-radius" => logicalPropertyHelper(this, dest, context, "start_start", property), + .@"border-start-end-radius" => logicalPropertyHelper(this, dest, context, "start_end", property), + .@"border-end-end-radius" => logicalPropertyHelper(this, dest, context, "end_end", property), + .@"border-end-start-radius" => logicalPropertyHelper(this, dest, context, "end_start", property), + else => { + this.flush(dest, context); + dest.append(context.allocator, Property{ .unparsed = unparsed.getPrefixed(context.allocator, context.targets, css.prefixes.Feature.border_radius) }) catch bun.outOfMemory(); + }, + } + } else return false; + }, + else => return false, + } + + return true; + } + + pub fn finalize(this: *@This(), dest: *css.DeclarationList, context: *css.PropertyHandlerContext) void { + this.flush(dest, context); + } + + fn flush(this: *@This(), dest: *css.DeclarationList, context: *css.PropertyHandlerContext) void { + if (!this.has_any) return; + + this.has_any = false; + + var top_left = bun.take(&this.top_left); + var top_right = bun.take(&this.top_right); + var bottom_right = bun.take(&this.bottom_right); + var bottom_left = bun.take(&this.bottom_left); + const start_start = bun.take(&this.start_start); + const start_end = bun.take(&this.start_end); + const end_end = bun.take(&this.end_end); + const end_start = bun.take(&this.end_start); + + if (top_left != null and top_right != null and bottom_right != null and bottom_left != null) { + const intersection = top_left.?[1].bitwiseAnd(top_right.?[1]).bitwiseAnd(bottom_right.?[1]).bitwiseAnd(bottom_left.?[1]); + if (!intersection.isEmpty()) { + const prefix = context.targets.prefixes(intersection, css.prefixes.Feature.border_radius); + dest.append(context.allocator, Property{ .@"border-radius" = .{ + BorderRadius{ + .top_left = top_left.?[0].deepClone(context.allocator), + .top_right = top_right.?[0].deepClone(context.allocator), + .bottom_right = bottom_right.?[0].deepClone(context.allocator), + .bottom_left = bottom_left.?[0].deepClone(context.allocator), + }, + prefix, + } }) catch bun.outOfMemory(); + top_left.?[1].remove(intersection); + top_right.?[1].remove(intersection); + bottom_right.?[1].remove(intersection); + bottom_left.?[1].remove(intersection); + } + } + + const logical_supported = !context.shouldCompileLogical(.logical_border_radius); + + singleProperty(dest, context, "border-top-left-radius", top_left); + singleProperty(dest, context, "border-top-right-radius", top_right); + singleProperty(dest, context, "border-bottom-right-radius", bottom_right); + singleProperty(dest, context, "border-bottom-left-radius", bottom_left); + + logicalProperty(dest, context, start_start, "border-top-left-radius", "border-top-right-radius", logical_supported); + logicalProperty(dest, context, start_end, "border-top-right-radius", "border-top-left-radius", logical_supported); + logicalProperty(dest, context, end_end, "border-bottom-right-radius", "border-bottom-left-radius", logical_supported); + logicalProperty(dest, context, end_start, "border-bottom-left-radius", "border-bottom-right-radius", logical_supported); + } + + fn singleProperty(d: *css.DeclarationList, ctx: *css.PropertyHandlerContext, comptime prop: []const u8, val: ?struct { Size2D(LengthPercentage), css.VendorPrefix }) void { + if (val) |v| { + if (!v[1].isEmpty()) { + const prefix = ctx.targets.prefixes(v[1], css.prefixes.Feature.border_radius); + d.append(ctx.allocator, @unionInit(css.Property, prop, .{ v[0], prefix })) catch bun.outOfMemory(); + } + } + } + + fn logicalProperty(d: *css.DeclarationList, ctx: *css.PropertyHandlerContext, val: ?css.Property, comptime ltr: []const u8, comptime rtl: []const u8, logical_supported: bool) void { + if (val) |v| { + if (logical_supported) { + d.append(ctx.allocator, v) catch bun.outOfMemory(); + } else { + const prefix = ctx.targets.prefixes(css.VendorPrefix.empty(), css.prefixes.Feature.border_radius); + switch (v) { + .@"border-start-start-radius", + .@"border-start-end-radius", + .@"border-end-end-radius", + .@"border-end-start-radius", + => |radius| { + ctx.addLogicalRule( + ctx.allocator, + @unionInit(css.Property, ltr, .{ radius, prefix }), + @unionInit(css.Property, rtl, .{ radius, prefix }), + ); + }, + .unparsed => |unparsed| { + ctx.addLogicalRule( + ctx.allocator, + Property{ .unparsed = unparsed.withPropertyId(ctx.allocator, .{ .@"border-top-left-radius" = prefix }) }, + Property{ .unparsed = unparsed.withPropertyId(ctx.allocator, .{ .@"border-top-right-radius" = prefix }) }, + ); + }, + else => {}, + } + } + } + } + + fn maybeFlush(self: *BorderRadiusHandler, d: *css.DeclarationList, ctx: *css.PropertyHandlerContext, comptime prop: []const u8, val: anytype, vp: css.VendorPrefix) void { + // If two vendor prefixes for the same property have different + // values, we need to flush what we have immediately to preserve order. + if (@field(self, prop)) |*existing| { + if (!existing.*[0].eql(val) and !existing.*[1].contains(vp)) { + self.flush(d, ctx); + } + } + + if (@field(self, prop) != null and ctx.targets.browsers != null and !css.generic.isCompatible(Size2D(LengthPercentage), val, ctx.targets.browsers.?)) { + self.flush(d, ctx); + } + } + + fn propertyHelper(self: *BorderRadiusHandler, d: *css.DeclarationList, ctx: *css.PropertyHandlerContext, comptime prop: []const u8, val: *const Size2D(LengthPercentage), vp: css.VendorPrefix) void { + if (self.category != .physical) { + self.flush(d, ctx); + } + + maybeFlush(self, d, ctx, prop, val, vp); + + // Otherwise, update the value and add the prefix. + if (@field(self, prop)) |*existing| { + existing.* = .{ val.deepClone(ctx.allocator), vp }; + } else { + @field(self, prop) = .{ val.deepClone(ctx.allocator), vp }; + self.has_any = true; + } + + self.category = .physical; + } + + fn logicalPropertyHelper(self: *BorderRadiusHandler, d: *css.DeclarationList, ctx: *css.PropertyHandlerContext, comptime prop: []const u8, val: *const css.Property) void { + if (self.category != .logical) { + self.flush(d, ctx); + } + + @field(self, prop) = val.deepClone(ctx.allocator); + self.category = .logical; + self.has_any = true; + } +}; + +pub fn isBorderRadiusProperty(property_id: PropertyIdTag) bool { + if (isLogicalBorderRadiusProperty(property_id)) { + return true; + } + + return switch (property_id) { + .@"border-top-left-radius", + .@"border-top-right-radius", + .@"border-bottom-right-radius", + .@"border-bottom-left-radius", + .@"border-radius", + => true, + else => false, + }; +} + +pub fn isLogicalBorderRadiusProperty(property_id: PropertyIdTag) bool { + return switch (property_id) { + .@"border-start-start-radius", + .@"border-start-end-radius", + .@"border-end-end-radius", + .@"border-end-start-radius", + => true, + else => false, + }; +} diff --git a/src/css/properties/custom.zig b/src/css/properties/custom.zig index cfde458ae0618d..c94ed570fdd6d7 100644 --- a/src/css/properties/custom.zig +++ b/src/css/properties/custom.zig @@ -604,6 +604,7 @@ pub const TokenList = struct { pub fn getFallback(this: *const TokenList, allocator: Allocator, kind: ColorFallbackKind) @This() { var tokens = TokenList{}; tokens.v.ensureTotalCapacity(allocator, this.v.items.len) catch bun.outOfMemory(); + tokens.v.items.len = this.v.items.len; for (this.v.items, tokens.v.items[0..this.v.items.len]) |*old, *new| { new.* = switch (old.*) { .color => |*color| TokenOrValue{ .color = color.getFallback(allocator, kind) }, @@ -1425,6 +1426,13 @@ pub const UnparsedProperty = struct { return .{ .result = .{ .property_id = property_id, .value = value } }; } + pub fn getPrefixed(this: *const @This(), allocator: Allocator, targets: css.Targets, feature: css.prefixes.Feature) UnparsedProperty { + var clone = this.deepClone(allocator); + const prefix = this.property_id.prefix(); + clone.property_id = clone.property_id.withPrefix(targets.prefixes(prefix.orNone(), feature)); + return clone; + } + /// Returns a new UnparsedProperty with the same value and the given property id. pub fn withPropertyId(this: *const @This(), allocator: Allocator, property_id: css.PropertyId) UnparsedProperty { return UnparsedProperty{ .property_id = property_id, .value = this.value.deepClone(allocator) }; diff --git a/src/css/properties/font.zig b/src/css/properties/font.zig index b5e8f12effca4e..80af832a245af5 100644 --- a/src/css/properties/font.zig +++ b/src/css/properties/font.zig @@ -55,6 +55,13 @@ pub const FontWeight = union(enum) { return .{ .absolute = AbsoluteFontWeight.default() }; } + pub fn isCompatible(this: *const FontWeight, browsers: bun.css.targets.Browsers) bool { + return switch (this.*) { + .absolute => |*a| a.isCompatible(browsers), + .bolder, .lighter => true, + }; + } + pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } @@ -86,6 +93,17 @@ pub const AbsoluteFontWeight = union(enum) { }; } + pub fn isCompatible(this: *const AbsoluteFontWeight, browsers: bun.css.targets.Browsers) bool { + return switch (this.*) { + // Older browsers only supported 100, 200, 300, ...900 rather than arbitrary values. + .weight => |*val| if (!((val.* >= 100.0 and val.* <= 900.0) and @mod(val.*, 100.0) == 0.0)) + css.Feature.font_weight_number.isCompatible(browsers) + else + true, + else => true, + }; + } + pub inline fn default() AbsoluteFontWeight { return .normal; } @@ -107,6 +125,20 @@ pub const FontSize = union(enum) { pub usingnamespace css.DeriveParse(@This()); pub usingnamespace css.DeriveToCss(@This()); + pub fn isCompatible(this: *const FontSize, browsers: bun.css.targets.Browsers) bool { + return switch (this.*) { + .length => |*l| switch (l.*) { + .dimension => |*d| switch (d.*) { + .rem => css.Feature.font_size_rem.isCompatible(browsers), + else => l.isCompatible(browsers), + }, + else => l.isCompatible(browsers), + }, + .absolute => |*a| a.isCompatible(browsers), + .relative => true, + }; + } + pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } @@ -139,6 +171,13 @@ pub const AbsoluteFontSize = enum { @"xxx-large", pub usingnamespace css.DefineEnumProperty(@This()); + + pub fn isCompatible(this: *const AbsoluteFontSize, browsers: bun.css.targets.Browsers) bool { + return switch (this.*) { + .@"xxx-large" => css.Feature.font_size_x_x_x_large.isCompatible(browsers), + else => true, + }; + } }; /// A [relative font size](https://www.w3.org/TR/css-fonts-3/#relative-size-value), @@ -181,6 +220,13 @@ pub const FontStretch = union(enum) { }; } + pub fn isCompatible(this: *const FontStretch, browsers: bun.css.targets.Browsers) bool { + return switch (this.*) { + .percentage => css.Feature.font_stretch_percentage.isCompatible(browsers), + .keyword => true, + }; + } + pub fn eql(lhs: *const FontStretch, rhs: *const FontStretch) bool { return css.implementEql(@This(), lhs, rhs); } @@ -247,6 +293,20 @@ pub const FontFamily = union(enum) { /// A custom family name. family_name: []const u8, + pub fn HashMap(comptime V: type) type { + return std.ArrayHashMapUnmanaged(FontFamily, V, struct { + pub fn hash(_: @This(), key: FontFamily) u32 { + var hasher = std.hash.Wyhash.init(0); + key.hash(&hasher); + return @truncate(hasher.final()); + } + + pub fn eql(_: @This(), a: FontFamily, b: FontFamily, _: usize) bool { + return a.eql(&b); + } + }, false); + } + pub fn parse(input: *css.Parser) css.Result(@This()) { if (input.tryParse(css.Parser.expectString, .{}).asValue()) |value| { return .{ .result = .{ .family_name = value } }; @@ -306,7 +366,8 @@ pub const FontFamily = union(enum) { } else { id.append(dest.allocator, ' ') catch bun.outOfMemory(); } - css.serializer.serializeIdentifier(slice, dest) catch return dest.addFmtError(); + const dest_id = id.writer(dest.allocator); + css.serializer.serializeIdentifier(slice, dest_id) catch return dest.addFmtError(); } if (id.items.len < val.len + 2) { return dest.writeStr(id.items); @@ -317,6 +378,13 @@ pub const FontFamily = union(enum) { } } + pub fn isCompatible(this: *const FontFamily, browsers: bun.css.targets.Browsers) bool { + return switch (this.*) { + .generic => |g| g.isCompatible(browsers), + .family_name => true, + }; + } + pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } @@ -324,6 +392,10 @@ pub const FontFamily = union(enum) { pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { return css.implementDeepClone(@This(), this, allocator); } + + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } }; /// A [generic font family](https://www.w3.org/TR/css-fonts-4/#generic-font-families) name, @@ -361,6 +433,14 @@ pub const GenericFontFamily = enum { @"revert-layer", pub usingnamespace css.DefineEnumProperty(@This()); + + pub fn isCompatible(this: *const GenericFontFamily, browsers: bun.css.targets.Browsers) bool { + return switch (this.*) { + .@"system-ui" => css.Feature.font_family_system_ui.isCompatible(browsers), + .@"ui-serif", .@"ui-sans-serif", .@"ui-monospace", .@"ui-rounded" => css.Feature.extended_system_fonts.isCompatible(browsers), + else => true, + }; + } }; /// A value for the [font-style](https://www.w3.org/TR/css-fonts-4/#font-style-prop) property. @@ -402,7 +482,7 @@ pub const FontStyle = union(enum) { .italic => try dest.writeStr("italic"), .oblique => |angle| { try dest.writeStr("oblique"); - if (angle.eql(&FontStyle.defaultObliqueAngle())) { + if (!angle.eql(&FontStyle.defaultObliqueAngle())) { try dest.writeChar(' '); try angle.toCss(W, dest); } @@ -410,6 +490,16 @@ pub const FontStyle = union(enum) { } } + pub fn isCompatible(this: *const FontStyle, browsers: bun.css.targets.Browsers) bool { + return switch (this.*) { + .oblique => |*angle| if (!angle.eql(&FontStyle.defaultObliqueAngle())) + css.Feature.font_style_oblique_angle.isCompatible(browsers) + else + true, + .normal, .italic => true, + }; + } + pub fn defaultObliqueAngle() Angle { return Angle{ .deg = 14.0 }; } @@ -463,6 +553,10 @@ pub const FontVariantCaps = enum { } return .{ .result = value }; } + + pub fn isCompatible(_: *const FontVariantCaps, _: bun.css.targets.Browsers) bool { + return true; + } }; /// A value for the [line-height](https://www.w3.org/TR/2020/WD-css-inline-3-20200827/#propdef-line-height) property. @@ -477,6 +571,13 @@ pub const LineHeight = union(enum) { pub usingnamespace @call(.auto, css.DeriveParse, .{@This()}); pub usingnamespace @call(.auto, css.DeriveToCss, .{@This()}); + pub fn isCompatible(this: *const LineHeight, browsers: bun.css.targets.Browsers) bool { + return switch (this.*) { + .length => |*l| l.isCompatible(browsers), + .normal, .number => true, + }; + } + pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } @@ -676,3 +777,279 @@ pub const VerticalAlignKeyword = enum { pub usingnamespace css.DefineEnumProperty(@This()); }; + +pub const FontProperty = packed struct(u8) { + @"font-family": bool = false, + @"font-size": bool = false, + @"font-style": bool = false, + @"font-weight": bool = false, + @"font-stretch": bool = false, + @"line-height": bool = false, + @"font-variant-caps": bool = false, + __unused: u1 = 0, + + pub usingnamespace css.Bitflags(@This()); + + const FONT = FontProperty{ + .@"font-family" = true, + .@"font-size" = true, + .@"font-style" = true, + .@"font-weight" = true, + .@"font-stretch" = true, + .@"line-height" = true, + .@"font-variant-caps" = true, + }; + + pub fn tryFromPropertyId(property_id: css.PropertyIdTag) ?FontProperty { + inline for (std.meta.fields(FontProperty)) |field| { + if (comptime std.mem.eql(u8, field.name, "__unused")) continue; + const desired = comptime @field(css.PropertyIdTag, field.name); + if (desired == property_id) { + var result: FontProperty = .{}; + @field(result, field.name) = true; + return result; + } + } + if (property_id == .font) { + return FontProperty.FONT; + } + return null; + } +}; + +pub const FontHandler = struct { + family: ?bun.BabyList(FontFamily) = null, + size: ?FontSize = null, + style: ?FontStyle = null, + weight: ?FontWeight = null, + stretch: ?FontStretch = null, + line_height: ?LineHeight = null, + variant_caps: ?FontVariantCaps = null, + flushed_properties: FontProperty = .{}, + has_any: bool = false, + + pub fn handleProperty( + this: *FontHandler, + property: *const css.Property, + dest: *css.DeclarationList, + context: *css.PropertyHandlerContext, + ) bool { + switch (property.*) { + .@"font-family" => |*val| this.propertyHelper(dest, context, "family", val), + .@"font-size" => |*val| this.propertyHelper(dest, context, "size", val), + .@"font-style" => |*val| this.propertyHelper(dest, context, "style", val), + .@"font-weight" => |*val| this.propertyHelper(dest, context, "weight", val), + .@"font-stretch" => |*val| this.propertyHelper(dest, context, "stretch", val), + .@"font-variant-caps" => |*val| this.propertyHelper(dest, context, "variant_caps", val), + .@"line-height" => |*val| this.propertyHelper(dest, context, "line_height", val), + .font => |*val| { + this.flushHelper(dest, context, "family", &val.family); + this.flushHelper(dest, context, "size", &val.size); + this.flushHelper(dest, context, "style", &val.style); + this.flushHelper(dest, context, "weight", &val.weight); + this.flushHelper(dest, context, "stretch", &val.stretch); + this.flushHelper(dest, context, "line_height", &val.line_height); + this.flushHelper(dest, context, "variant_caps", &val.variant_caps); + + this.family = css.generic.deepClone(bun.BabyList(FontFamily), &val.family, context.allocator); + this.size = val.size.deepClone(context.allocator); + this.style = val.style.deepClone(context.allocator); + this.weight = val.weight.deepClone(context.allocator); + this.stretch = val.stretch.deepClone(context.allocator); + this.line_height = val.line_height.deepClone(context.allocator); + this.variant_caps = val.variant_caps.deepClone(context.allocator); + this.has_any = true; + // TODO: reset other properties + }, + .unparsed => |*val| { + if (isFontProperty(val.property_id)) { + this.flush(dest, context); + this.flushed_properties.insert(FontProperty.tryFromPropertyId(val.property_id).?); + dest.append(context.allocator, property.*) catch bun.outOfMemory(); + } else { + return false; + } + }, + else => return false, + } + + return true; + } + + inline fn propertyHelper(this: *FontHandler, dest: *css.DeclarationList, context: *css.PropertyHandlerContext, comptime prop: []const u8, val: anytype) void { + this.flushHelper(dest, context, prop, val); + @field(this, prop) = css.generic.deepClone(@TypeOf(val.*), val, context.allocator); + this.has_any = true; + } + + inline fn flushHelper( + this: *FontHandler, + dest: *css.DeclarationList, + context: *css.PropertyHandlerContext, + comptime prop: []const u8, + val: anytype, + ) void { + if (@field(this, prop) != null and + !css.generic.eql(@TypeOf(@field(this, prop).?), &@field(this, prop).?, val) and + context.targets.browsers != null and + !css.generic.isCompatible(@TypeOf(@field(this, prop).?), val, context.targets.browsers.?)) + { + this.flush(dest, context); + } + } + + pub fn finalize(this: *FontHandler, decls: *css.DeclarationList, context: *css.PropertyHandlerContext) void { + this.flush(decls, context); + this.flushed_properties = .{}; + } + + fn flush(this: *FontHandler, decls: *css.DeclarationList, context: *css.PropertyHandlerContext) void { + const push = struct { + fn push(self: *FontHandler, d: *css.DeclarationList, ctx: *css.PropertyHandlerContext, comptime prop: []const u8, val: anytype) void { + d.append(ctx.allocator, @unionInit(css.Property, prop, val)) catch bun.outOfMemory(); + var insertion: FontProperty = .{}; + if (comptime std.mem.eql(u8, prop, "font")) { + insertion = FontProperty.FONT; + } else { + @field(insertion, prop) = true; + } + self.flushed_properties.insert(insertion); + } + }.push; + + if (!this.has_any) { + return; + } + + this.has_any = false; + + var family: ?bun.BabyList(FontFamily) = bun.take(&this.family); + if (!this.flushed_properties.contains(FontProperty{ .@"font-family" = true })) { + family = compatibleFontFamily(context.allocator, family, !context.targets.shouldCompileSame(.font_family_system_ui)); + } + + const size: ?FontSize = bun.take(&this.size); + const style: ?FontStyle = bun.take(&this.style); + const weight: ?FontWeight = bun.take(&this.weight); + const stretch: ?FontStretch = bun.take(&this.stretch); + const line_height: ?LineHeight = bun.take(&this.line_height); + const variant_caps: ?FontVariantCaps = bun.take(&this.variant_caps); + + if (family) |*f| { + if (f.len > 1) { + // Dedupe + var sfb = std.heap.stackFallback(664, bun.default_allocator); + const alloc = sfb.get(); + var seen = FontFamily.HashMap(void){}; + defer seen.deinit(alloc); + + var i: usize = 0; + while (i < f.len) { + const gop = seen.getOrPut(alloc, f.at(i).*) catch bun.outOfMemory(); + if (gop.found_existing) { + _ = f.orderedRemove(i); + } else { + i += 1; + } + } + } + } + + if (family != null and size != null and style != null and weight != null and stretch != null and line_height != null and variant_caps != null) { + const caps = variant_caps.?; + push(this, decls, context, "font", Font{ + .family = family.?, + .size = size.?, + .style = style.?, + .weight = weight.?, + .stretch = stretch.?, + .line_height = line_height.?, + .variant_caps = if (caps.isCss2()) caps else FontVariantCaps.default(), + }); + + // The `font` property only accepts CSS 2.1 values for font-variant caps. + // If we have a CSS 3+ value, we need to add a separate property. + if (!caps.isCss2()) { + push(this, decls, context, "font-variant-caps", caps); + } + } else { + if (family) |val| { + push(this, decls, context, "font-family", val); + } + + if (size) |val| { + push(this, decls, context, "font-size", val); + } + + if (style) |val| { + push(this, decls, context, "font-style", val); + } + + if (variant_caps) |val| { + push(this, decls, context, "font-variant-caps", val); + } + + if (weight) |val| { + push(this, decls, context, "font-weight", val); + } + + if (stretch) |val| { + push(this, decls, context, "font-stretch", val); + } + + if (line_height) |val| { + push(this, decls, context, "line-height", val); + } + } + } +}; + +const SYSTEM_UI: FontFamily = FontFamily{ .generic = .@"system-ui" }; + +const DEFAULT_SYSTEM_FONTS: []const []const u8 = &.{ + // #1: Supported as the '-apple-system' value (macOS, Safari >= 9.2 < 11, Firefox >= 43) + "-apple-system", + // #2: Supported as the 'BlinkMacSystemFont' value (macOS, Chrome < 56) + "BlinkMacSystemFont", + "Segoe UI", // Windows >= Vista + "Roboto", // Android >= 4 + "Noto Sans", // Plasma >= 5.5 + "Ubuntu", // Ubuntu >= 10.10 + "Cantarell", // GNOME >= 3 + "Helvetica Neue", +}; + +inline fn compatibleFontFamily(allocator: std.mem.Allocator, _family: ?bun.BabyList(FontFamily), is_supported: bool) ?bun.BabyList(FontFamily) { + var family = _family; + if (is_supported) { + return family; + } + + if (family) |*families| { + for (families.sliceConst(), 0..) |v, i| { + if (v.eql(&SYSTEM_UI)) { + for (DEFAULT_SYSTEM_FONTS, 0..) |name, j| { + families.insert(allocator, i + j + 1, .{ .family_name = name }) catch bun.outOfMemory(); + } + break; + } + } + } + + return family; +} + +inline fn isFontProperty(property_id: css.PropertyId) bool { + return switch (property_id) { + .@"font-family", + .@"font-size", + .@"font-style", + .@"font-weight", + .@"font-stretch", + .@"font-variant-caps", + .@"line-height", + .font, + => true, + else => false, + }; +} diff --git a/src/css/properties/generate_properties.ts b/src/css/properties/generate_properties.ts index f55f04d022fba2..453032e9161a38 100644 --- a/src/css/properties/generate_properties.ts +++ b/src/css/properties/generate_properties.ts @@ -358,6 +358,7 @@ function generatePropertyIdImpl(property_defs: Record): str return ` /// Returns the property name, without any vendor prefixes. pub inline fn name(this: *const PropertyId) []const u8 { + if (this.* == .custom) return this.custom.asStr(); return @tagName(this.*); } diff --git a/src/css/properties/properties_generated.zig b/src/css/properties/properties_generated.zig index 46615f071a42f8..8df0d795a9b008 100644 --- a/src/css/properties/properties_generated.zig +++ b/src/css/properties/properties_generated.zig @@ -5086,6 +5086,7 @@ pub const Property = union(PropertyIdTag) { } }, .@"margin-right" => { + @setEvalBranchQuota(5000); if (css.generic.parseWithOptions(LengthPercentageOrAuto, input, options).asValue()) |c| { if (input.expectExhausted().isOk()) { return .{ .result = .{ .@"margin-right" = c } }; @@ -7196,6 +7197,7 @@ pub const PropertyId = union(PropertyIdTag) { /// Returns the property name, without any vendor prefixes. pub inline fn name(this: *const PropertyId) []const u8 { + if (this.* == .custom) return this.custom.asStr(); return @tagName(this.*); } diff --git a/src/css/rules/font_face.zig b/src/css/rules/font_face.zig index 2867b2ca641615..21b5cf31386035 100644 --- a/src/css/rules/font_face.zig +++ b/src/css/rules/font_face.zig @@ -591,7 +591,7 @@ pub const UrlSource = struct { if (this.tech.items.len != 0) { try dest.whitespace(); try dest.writeStr("tech("); - try css.to_css.fromList(FontTechnology, &this.tech, W, dest); + try css.to_css.fromList(FontTechnology, this.tech.items, W, dest); try dest.writeChar(')'); } } diff --git a/src/css/rules/font_palette_values.zig b/src/css/rules/font_palette_values.zig index d5f1eb0c1b7dcb..fe59e11632f9b2 100644 --- a/src/css/rules/font_palette_values.zig +++ b/src/css/rules/font_palette_values.zig @@ -114,7 +114,7 @@ pub const FontPaletteValuesProperty = union(enum) { .override_colors => |*o| { try dest.writeStr("override-colors"); try dest.delim(':', false); - try css.to_css.fromList(OverrideColors, o, W, dest); + try css.to_css.fromList(OverrideColors, o.items, W, dest); }, .custom => |*custom| { try dest.writeStr(custom.name.asStr()); diff --git a/src/css/rules/import.zig b/src/css/rules/import.zig index 7c42e67834b504..2dbf45c508bdab 100644 --- a/src/css/rules/import.zig +++ b/src/css/rules/import.zig @@ -18,18 +18,35 @@ const LayerName = css.css_rules.layer.LayerName; const SupportsCondition = css.css_rules.supports.SupportsCondition; const Location = css.css_rules.Location; +/// TODO: change this to be field on ImportRule +/// The fields of this struct need to match the fields of ImportRule +/// because we cast between them pub const ImportConditions = struct { /// An optional cascade layer name, or `None` for an anonymous layer. layer: ?struct { /// PERF: null pointer optimizaiton, nullable v: ?LayerName, - }, + + pub fn eql(this: *const @This(), other: *const @This()) bool { + if (this.v == null and other.v == null) return true; + if (this.v == null or other.v == null) return false; + return this.v.?.eql(&other.v.?); + } + } = null, /// An optional `supports()` condition. - supports: ?SupportsCondition, + supports: ?SupportsCondition = null, /// A media query. - media: css.MediaList, + media: css.MediaList = .{}, + + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } + + pub fn hasAnonymousLayer(this: *const @This()) bool { + return this.layer != null and this.layer.?.v == null; + } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) ImportConditions { return ImportConditions{ @@ -39,20 +56,78 @@ pub const ImportConditions = struct { }; } - pub fn layersEql(lhs: *const @This(), rhs: *const @This()) bool { - if (lhs.layer) |ll| { - if (rhs.layer) |rl| { - if (ll.v) |lv| { - if (rl.v) |rv| { - return lv.eql(&rv); - } - return false; - } - return true; + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + 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(')'); } - return false; } - return false; + + if (this.media.media_queries.items.len > 0) { + try dest.writeChar(' '); + try this.media.toCss(W, dest); + } + } + + /// This code does the same thing as `deepClone` right now, but might change in the future so keeping this separate. + /// + /// So this code is used when we wrap a CSS file in import conditions in the final output chunk: + /// ```css + /// @layer foo { + /// /* css file contents */ + /// } + /// ``` + /// + /// However, the *prelude* of the condition /could/ contain a URL token: + /// ```css + /// @supports (background-image: url('example.png')) { + /// /* css file contents */ + /// } + /// ``` + /// + /// In this case, the URL token's import record actually belongs to the /parent/ of the current CSS file (the one who imported it). + /// Therefore, we need to copy this import record from the parent into the import record list of this current CSS file. + /// + /// In actuality, the css parser doesn't create an import record for URL tokens in `@supports` because that's pointless in the context of hte + /// @supports rule. + /// + /// Furthermore, a URL token is not valid in `@media` or `@layer` rules. + /// + /// But this could change in the future, so still keeping this function. + /// + pub fn cloneWithImportRecords(this: *const @This(), allocator: std.mem.Allocator, import_records: *bun.BabyList(bun.ImportRecord)) ImportConditions { + return ImportConditions{ + .layer = if (this.layer) |layer| if (layer.v) |l| .{ .v = l.cloneWithImportRecords(allocator, import_records) } else .{ .v = null } else null, + .supports = if (this.supports) |*supp| supp.cloneWithImportRecords(allocator, import_records) else null, + .media = this.media.cloneWithImportRecords(allocator, import_records), + }; + } + + pub fn layersEql(lhs: *const @This(), rhs: *const @This()) bool { + if (lhs.layer == null and rhs.layer == null) return true; + if (lhs.layer == null or rhs.layer == null) return false; + return lhs.layer.?.eql(&rhs.layer.?); + } + + pub fn supportsEql(lhs: *const @This(), rhs: *const @This()) bool { + if (lhs.supports == null and rhs.supports == null) return true; + if (lhs.supports == null or rhs.supports == null) return false; + + return lhs.supports.?.eql(&rhs.supports.?); } }; @@ -69,13 +144,13 @@ pub const ImportRule = struct { pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { return css.implementDeepClone(@This(), this, allocator); } - }, + } = null, /// An optional `supports()` condition. - supports: ?SupportsCondition, + supports: ?SupportsCondition = null, /// A media query. - media: css.MediaList, + media: css.MediaList = .{}, /// This is default initialized to 2^32 - 1 when parsing. /// If we are bundling, this will be set to the index of the corresponding ImportRecord @@ -98,22 +173,42 @@ pub const ImportRule = struct { }; } - pub fn fromConditionsAndUrl(url: []const u8, conditions: ImportConditions) This { + pub fn fromUrlAndImportRecordIdx(url: []const u8, import_record_idx: u32) This { + return .{ + .url = url, + .layer = null, + .supports = null, + .media = MediaList{ .media_queries = .{} }, + .import_record_idx = import_record_idx, + .loc = Location.dummy(), + }; + } + + pub fn fromConditionsAndUrl(url: []const u8, conds: ImportConditions) This { return ImportRule{ .url = url, - .layer = if (conditions.layer) |layer| if (layer.v) |ly| .{ .v = ly } else .{ .v = null } else null, - .supports = conditions.supports, - .media = conditions.media, + .layer = if (conds.layer) |layer| if (layer.v) |ly| .{ .v = ly } else .{ .v = null } else null, + .supports = conds.supports, + .media = conds.media, .import_record_idx = std.math.maxInt(u32), .loc = Location.dummy(), }; } - pub fn conditionsOwned(this: *const This, allocator: std.mem.Allocator) ImportConditions { + pub fn conditions(this: *const @This()) *const ImportConditions { + return @ptrCast(&this.layer); + } + + pub fn conditionsMut(this: *@This()) *ImportConditions { + return @ptrCast(&this.layer); + } + + /// The `import_records` here is preserved from esbuild in the case that we do need it, it doesn't seem necessary now + pub fn conditionsWithImportRecords(this: *const This, allocator: std.mem.Allocator, import_records: *bun.BabyList(bun.ImportRecord)) ImportConditions { return ImportConditions{ - .layer = if (this.layer) |*l| if (l.v) |layer| .{ .v = layer.deepClone(allocator) } else .{ .v = null } else null, - .supports = if (this.supports) |*s| s.deepClone(allocator) else null, - .media = this.media.deepClone(allocator), + .layer = if (this.layer) |layer| if (layer.v) |l| .{ .v = l.cloneWithImportRecords(allocator, import_records) } else .{ .v = null } else null, + .supports = if (this.supports) |*supp| supp.cloneWithImportRecords(allocator, import_records) else null, + .media = this.media.cloneWithImportRecords(allocator, import_records), }; } diff --git a/src/css/rules/layer.zig b/src/css/rules/layer.zig index 05c2d692a879a4..657d4da8d87544 100644 --- a/src/css/rules/layer.zig +++ b/src/css/rules/layer.zig @@ -12,7 +12,8 @@ const SupportsCondition = css.css_rules.supports.SupportsCondition; const Location = css.css_rules.Location; const Result = css.Result; -// TODO: make this equivalent of SmallVec<[CowArcStr<'i>; 1] +/// Stored as a list of strings as dot notation can be used +/// to create sublayers pub const LayerName = struct { v: css.SmallList([]const u8, 1) = .{}, @@ -36,6 +37,18 @@ pub const LayerName = struct { }, false); } + pub fn hash(this: *const LayerName, hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } + + pub fn cloneWithImportRecords( + this: *const @This(), + allocator: std.mem.Allocator, + _: *bun.BabyList(bun.ImportRecord), + ) @This() { + return LayerName{ .v = this.v.deepClone(allocator) }; + } + pub fn deepClone(this: *const LayerName, allocator: std.mem.Allocator) LayerName { return LayerName{ .v = this.v.clone(allocator), @@ -44,8 +57,8 @@ pub const LayerName = struct { pub fn eql(lhs: *const LayerName, rhs: *const LayerName) bool { if (lhs.v.len() != rhs.v.len()) return false; - for (lhs.v.slice(), 0..) |part, i| { - if (!bun.strings.eql(part, rhs.v.at(@intCast(i)).*)) return false; + for (lhs.v.slice(), rhs.v.slice()) |l, r| { + if (!bun.strings.eql(l, r)) return false; } return true; } @@ -120,6 +133,18 @@ pub const LayerName = struct { css.serializer.serializeIdentifier(name, dest) catch return dest.addFmtError(); } } + + pub fn format(this: *const LayerName, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + var first = true; + for (this.v.slice()) |name| { + if (first) { + first = false; + } else { + try writer.writeAll("."); + } + try writer.writeAll(name); + } + } }; /// A [@layer block](https://drafts.csswg.org/css-cascade-5/#layer-block) rule. @@ -166,7 +191,7 @@ pub fn LayerBlockRule(comptime R: type) type { /// See also [LayerBlockRule](LayerBlockRule). pub const LayerStatementRule = struct { /// The layer names to declare. - names: ArrayList(LayerName), + names: bun.css.SmallList(LayerName, 1), /// The location of the rule in the source file. loc: Location, @@ -175,9 +200,13 @@ pub const LayerStatementRule = struct { 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 "); - try css.to_css.fromList(LayerName, &this.names, W, dest); - try dest.writeChar(';'); + if (this.names.len() > 0) { + try dest.writeStr("@layer "); + try css.to_css.fromList(LayerName, this.names.slice(), W, dest); + try dest.writeChar(';'); + } else { + try dest.writeStr("@layer;"); + } } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) This { diff --git a/src/css/rules/supports.zig b/src/css/rules/supports.zig index 126720002bd301..e33f6115a84273 100644 --- a/src/css/rules/supports.zig +++ b/src/css/rules/supports.zig @@ -42,6 +42,9 @@ pub const SupportsCondition = union(enum) { /// The property id for the declaration. property_id: css.PropertyId, /// The raw value of the declaration. + /// + /// What happens if the value is a URL? A URL in this context does nothing + /// e.g. `@supports (background-image: url('example.png'))` value: []const u8, pub fn eql(this: *const @This(), other: *const @This()) bool { @@ -74,6 +77,18 @@ pub const SupportsCondition = union(enum) { } } + pub fn cloneWithImportRecords( + this: *const @This(), + allocator: std.mem.Allocator, + _: *bun.BabyList(bun.ImportRecord), + ) @This() { + return deepClone(this, allocator); + } + + pub fn hash(this: *const @This(), hasher: anytype) void { + return css.implementHash(@This(), this, hasher); + } + pub fn eql(this: *const SupportsCondition, other: *const SupportsCondition) bool { return css.implementEql(SupportsCondition, this, other); } diff --git a/src/css/small_list.zig b/src/css/small_list.zig index bb48ca8593953a..cf05e490a3054c 100644 --- a/src/css/small_list.zig +++ b/src/css/small_list.zig @@ -80,6 +80,70 @@ pub fn SmallList(comptime T: type, comptime N: comptime_int) type { } } + /// NOTE: This will deinit the list + pub fn fromList(allocator: Allocator, list: std.ArrayListUnmanaged(T)) @This() { + if (list.cap > N) { + return .{ + .capacity = list.cap, + .data = .{ .heap = .{ .len = list.len, .ptr = list.ptr } }, + }; + } + defer list.deinit(allocator); + var this: @This() = .{ + .capacity = list.len, + .data = .{ .inlined = undefined }, + }; + @memcpy(this.data.inlined[0..list.len], list.items[0..list.len]); + return this; + } + + pub fn fromListNoDeinit(list: std.ArrayListUnmanaged(T)) @This() { + if (list.cap > N) { + return .{ + .capacity = list.cap, + .data = .{ .heap = .{ .len = list.len, .ptr = list.ptr } }, + }; + } + var this: @This() = .{ + .capacity = list.len, + .data = .{ .inlined = undefined }, + }; + @memcpy(this.data.inlined[0..list.len], list.items[0..list.len]); + return this; + } + + /// NOTE: This will deinit the list + pub fn fromBabyList(allocator: Allocator, list: bun.BabyList(T)) @This() { + if (list.cap > N) { + return .{ + .capacity = list.cap, + .data = .{ .heap = .{ .len = list.len, .ptr = list.ptr } }, + }; + } + defer list.deinitWithAllocator(allocator); + var this: @This() = .{ + .capacity = list.len, + .data = .{ .inlined = undefined }, + }; + @memcpy(this.data.inlined[0..list.len], list.items[0..list.len]); + return this; + } + + pub fn fromBabyListNoDeinit(list: bun.BabyList(T)) @This() { + if (list.cap > N) { + return .{ + .capacity = list.cap, + .data = .{ .heap = .{ .len = list.len, .ptr = list.ptr } }, + }; + } + var this: @This() = .{ + .capacity = list.len, + .data = .{ .inlined = undefined }, + }; + @memcpy(this.data.inlined[0..list.len], list.ptr[0..list.len]); + return this; + } + pub fn withOne(val: T) @This() { var ret = This{}; ret.capacity = 1; @@ -461,9 +525,17 @@ pub fn SmallList(comptime T: type, comptime N: comptime_int) type { this.insertSlice(allocator, this.len(), items); } - pub fn insertSlice(this: *@This(), allocator: Allocator, index: u32, items: []const T) void { + pub fn appendSliceAssumeCapacity(this: *@This(), items: []const T) void { + bun.assert(this.len() + items.len <= this.capacity); + this.insertSliceAssumeCapacity(this.len(), items); + } + + pub inline fn insertSlice(this: *@This(), allocator: Allocator, index: u32, items: []const T) void { this.reserve(allocator, @intCast(items.len)); + this.insertSliceAssumeCapacity(index, items); + } + pub inline fn insertSliceAssumeCapacity(this: *@This(), index: u32, items: []const T) void { const length = this.len(); bun.assert(index <= length); const ptr: [*]T = this.as_ptr()[index..]; diff --git a/src/css/targets.zig b/src/css/targets.zig index 26f744bc39103d..f429e6f2c0fe5d 100644 --- a/src/css/targets.zig +++ b/src/css/targets.zig @@ -162,6 +162,8 @@ pub fn BrowsersImpl(comptime T: type) type { // .opera = 67 << 16, // }; + /// Ported from here: + /// https://github.com/vitejs/vite/blob/ac329685bba229e1ff43e3d96324f817d48abe48/packages/vite/src/node/plugins/css.ts#L3335 pub fn convertFromString(esbuild_target: []const []const u8) anyerror!T { var browsers: T = .{}; diff --git a/src/css/values/image.zig b/src/css/values/image.zig index b19fe98efecd69..fc82ef938fb2bf 100644 --- a/src/css/values/image.zig +++ b/src/css/values/image.zig @@ -129,6 +129,70 @@ pub const Image = union(enum) { }; } + pub fn getFallbacks(this: *@This(), allocator: Allocator, targets: css.targets.Targets) css.SmallList(Image, 6) { + const ColorFallbackKind = css.ColorFallbackKind; + // Determine which prefixes and color fallbacks are needed. + const prefixes = this.getNecessaryPrefixes(targets); + const fallbacks = this.getNecessaryFallbacks(targets); + var res = css.SmallList(Image, 6){}; + + // Get RGB fallbacks if needed. + const rgb = if (fallbacks.contains(ColorFallbackKind.RGB)) + this.getFallback(allocator, ColorFallbackKind.RGB) + else + null; + + // Prefixed properties only support RGB. + const prefix_image = if (rgb) |r| &r else this; + + // Legacy -webkit-gradient() + if (prefixes.contains(VendorPrefix.WEBKIT) and + if (targets.browsers) |browsers| css.prefixes.Feature.isWebkitGradient(browsers) else false and + prefix_image.* == .gradient) + { + if (prefix_image.getLegacyWebkit(allocator)) |legacy| { + res.append(allocator, legacy); + } + } + + // Standard syntax, with prefixes. + if (prefixes.contains(VendorPrefix.WEBKIT)) { + res.append(allocator, prefix_image.getPrefixed(allocator, css.VendorPrefix.WEBKIT)); + } + + if (prefixes.contains(VendorPrefix.MOZ)) { + res.append(allocator, prefix_image.getPrefixed(allocator, css.VendorPrefix.MOZ)); + } + + if (prefixes.contains(VendorPrefix.O)) { + res.append(allocator, prefix_image.getPrefixed(allocator, css.VendorPrefix.O)); + } + + if (prefixes.contains(VendorPrefix.NONE)) { + // Unprefixed, rgb fallback. + if (rgb) |r| { + res.append(allocator, r); + } + + // P3 fallback. + if (fallbacks.contains(ColorFallbackKind.P3)) { + res.append(allocator, this.getFallback(allocator, ColorFallbackKind.P3)); + } + + // Convert original to lab if needed (e.g. if oklab is not supported but lab is). + if (fallbacks.contains(ColorFallbackKind.LAB)) { + this.* = this.getFallback(allocator, ColorFallbackKind.LAB); + } + } else if (res.pop()) |last| { + // Prefixed property with no unprefixed version. + // Replace self with the last prefixed version so that it doesn't + // get duplicated when the caller pushes the original value. + this.* = last; + } + + return res; + } + pub fn getFallback(this: *const @This(), allocator: Allocator, kind: css.ColorFallbackKind) Image { return switch (this.*) { .gradient => |grad| .{ .gradient = bun.create(allocator, Gradient, grad.getFallback(allocator, kind)) }, diff --git a/src/css/values/length.zig b/src/css/values/length.zig index 3cd1fc1b44f875..b3a9fe12e4d488 100644 --- a/src/css/values/length.zig +++ b/src/css/values/length.zig @@ -22,6 +22,13 @@ pub const LengthOrNumber = union(enum) { pub usingnamespace css.DeriveParse(@This()); pub usingnamespace css.DeriveToCss(@This()); + pub fn deinit(this: *const LengthOrNumber, allocator: std.mem.Allocator) void { + switch (this.*) { + .number => {}, + .length => |*l| l.deinit(allocator), + } + } + pub fn default() LengthOrNumber { return .{ .number = 0.0 }; } @@ -36,6 +43,13 @@ pub const LengthOrNumber = union(enum) { pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { return css.implementDeepClone(@This(), this, allocator); } + + pub fn isCompatible(this: *const @This(), browsers: css.targets.Browsers) bool { + return switch (this.*) { + .length => |*l| l.isCompatible(browsers), + .number => true, + }; + } }; pub const LengthPercentage = DimensionPercentage(LengthValue); diff --git a/src/css/values/rect.zig b/src/css/values/rect.zig index 28b281c6d72fe0..dde9986ca851ce 100644 --- a/src/css/values/rect.zig +++ b/src/css/values/rect.zig @@ -151,5 +151,9 @@ pub fn Rect(comptime T: type) type { pub fn valParse(i: *css.Parser) Result(T) { return css.generic.parse(T, i); } + + pub fn isCompatible(this: *const @This(), browsers: css.targets.Browsers) bool { + return this.top.isCompatible(browsers) and this.right.isCompatible(browsers) and this.bottom.isCompatible(browsers) and this.left.isCompatible(browsers); + } }; } diff --git a/src/css/values/size.zig b/src/css/values/size.zig index 07aceaa9aa0909..51634e799712cc 100644 --- a/src/css/values/size.zig +++ b/src/css/values/size.zig @@ -67,6 +67,10 @@ pub fn Size2D(comptime T: type) type { }; } + pub fn isCompatible(this: *const @This(), browsers: bun.css.targets.Browsers) bool { + return this.a.isCompatible(browsers) and this.b.isCompatible(browsers); + } + pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { return css.implementDeepClone(@This(), this, allocator); } diff --git a/src/js_ast.zig b/src/js_ast.zig index 6ce22d81da6dc2..9babd2c2e645d3 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -7181,13 +7181,11 @@ pub const BundledAst = struct { // TODO: make this configurable const COPY_THRESHOLD = 128 * 1024; // 128kb const should_copy = contents.len >= COPY_THRESHOLD and unique_key != null; + if (should_copy) return; this.url_for_css = url_for_css: { - // Copy it - if (should_copy) break :url_for_css unique_key.?; // Encode as base64 const encode_len = bun.base64.encodeLen(contents); - if (encode_len == 0) return; const data_url_prefix_len = "data:".len + mime_type.len + ";base64,".len; const total_buffer_len = data_url_prefix_len + encode_len; var encoded = allocator.alloc(u8, total_buffer_len) catch bun.outOfMemory(); diff --git a/src/resolver/data_url.zig b/src/resolver/data_url.zig index 5757cebfe8360d..4c536d36878d17 100644 --- a/src/resolver/data_url.zig +++ b/src/resolver/data_url.zig @@ -115,7 +115,8 @@ pub const DataURL = struct { return bun.http.MimeType.init(d.mime_type, null, null); } - pub fn decodeData(url: DataURL, allocator: std.mem.Allocator) ![]u8 { + /// Decodes the data from the data URL. Always returns an owned slice. + pub fn decodeData(url: DataURL, allocator: Allocator) ![]u8 { const percent_decoded = PercentEncoding.decodeUnstrict(allocator, url.data) catch url.data orelse url.data; if (url.is_base64) { const len = bun.base64.decodeLen(percent_decoded); @@ -127,6 +128,110 @@ pub const DataURL = struct { return buf; } - return allocator.dupe(u8, percent_decoded); + return try allocator.dupe(u8, percent_decoded); + } + + /// Returns the shorter of either a base64-encoded or percent-escaped data URL + pub fn encodeStringAsShortestDataURL(allocator: Allocator, mime_type: []const u8, text: []const u8) []u8 { + // Calculate base64 version + const base64_encode_len = bun.base64.encodeLen(text); + const total_base64_encode_len = "data:".len + mime_type.len + ";base64,".len + base64_encode_len; + + use_base64: { + var counter = CountingBuf{}; + const success = encodeStringAsPercentEscapedDataURL(&counter, mime_type, text) catch unreachable; + if (!success) { + break :use_base64; + } + + if (counter.len > total_base64_encode_len) { + break :use_base64; + } + + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + const success2 = encodeStringAsPercentEscapedDataURL(&buf, mime_type, text) catch unreachable; + if (!success2) { + break :use_base64; + } + return buf.items; + } + + const base64buf = allocator.alloc(u8, total_base64_encode_len) catch bun.outOfMemory(); + return std.fmt.bufPrint(base64buf, "data:{s};base64,{s}", .{ mime_type, text }) catch unreachable; + } + + const CountingBuf = struct { + len: usize = 0, + + pub fn appendSlice(self: *CountingBuf, slice: []const u8) Allocator.Error!void { + self.len += slice.len; + } + + pub fn append(self: *CountingBuf, _: u8) Allocator.Error!void { + self.len += 1; + } + + pub fn toOwnedSlice(_: *CountingBuf) Allocator.Error![]u8 { + return ""; + } + }; + + pub fn encodeStringAsPercentEscapedDataURL(buf: anytype, mime_type: []const u8, text: []const u8) !bool { + const hex = "0123456789ABCDEF"; + + try buf.appendSlice("data:"); + try buf.appendSlice(mime_type); + try buf.append(','); + + // Scan for trailing characters that need to be escaped + var trailing_start = text.len; + while (trailing_start > 0) { + const c = text[trailing_start - 1]; + if (c > 0x20 or c == '\t' or c == '\n' or c == '\r') { + break; + } + trailing_start -= 1; + } + + if (!bun.simdutf.validate.utf8(text)) { + return false; + } + + var i: usize = 0; + var run_start: usize = 0; + + // TODO: vectorize this + while (i < text.len) { + const first_byte = text[i]; + + // Check if we need to escape this character + const needs_escape = first_byte == '\t' or + first_byte == '\n' or + first_byte == '\r' or + first_byte == '#' or + i >= trailing_start or + (first_byte == '%' and i + 2 < text.len and + PercentEncoding.isHex(text[i + 1]) and + PercentEncoding.isHex(text[i + 2])); + + if (needs_escape) { + if (run_start < i) { + try buf.appendSlice(text[run_start..i]); + } + try buf.append('%'); + try buf.append(hex[first_byte >> 4]); + try buf.append(hex[first_byte & 15]); + run_start = i + 1; + } + + i += bun.strings.utf8ByteSequenceLength(first_byte); + } + + if (run_start < text.len) { + try buf.appendSlice(text[run_start..]); + } + + return true; } }; diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 0e4250b2715039..9b2cff96b9a699 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -24,7 +24,7 @@ const MacroRemap = @import("./package_json.zig").MacroMap; const ESModule = @import("./package_json.zig").ESModule; const BrowserMap = @import("./package_json.zig").BrowserMap; const CacheSet = cache.Set; -const DataURL = @import("./data_url.zig").DataURL; +pub const DataURL = @import("./data_url.zig").DataURL; pub const DirInfo = @import("./dir_info.zig"); const ResolvePath = @import("./resolve_path.zig"); const NodeFallbackModules = @import("../node_fallbacks.zig"); @@ -878,6 +878,23 @@ pub const Resolver = struct { errdefer (r.flushDebugLogs(.fail) catch {}); var tmp = r.resolveWithoutSymlinks(source_dir_normalized, import_path, kind, global_cache); + + // Fragments in URLs in CSS imports are technically expected to work + if (tmp == .not_found and kind.isFromCSS()) try_without_suffix: { + // If resolution failed, try again with the URL query and/or hash removed + const maybe_suffix = std.mem.indexOfAny(u8, import_path, "?#"); + if (maybe_suffix == null or maybe_suffix.? < 1) + break :try_without_suffix; + + const suffix = maybe_suffix.?; + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Retrying resolution after removing the suffix {s}", .{import_path[suffix..]}); + } + const result2 = r.resolveWithoutSymlinks(source_dir_normalized, import_path[0..suffix], kind, global_cache); + if (result2 == .not_found) break :try_without_suffix; + tmp = result2; + } + switch (tmp) { .success => |*result| { if (!strings.eqlComptime(result.path_pair.primary.namespace, "node") and !result.is_standalone_module) @@ -1156,7 +1173,7 @@ pub const Resolver = struct { // Check both relative and package paths for CSS URL tokens, with relative // paths taking precedence over package paths to match Webpack behavior. const is_package_path = kind != .entry_point_run and isPackagePathNotAbsolute(import_path); - var check_relative = !is_package_path or kind == .url; + var check_relative = !is_package_path or kind.isFromCSS(); var check_package = is_package_path; if (check_relative) { diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 8ed314fe94af49..f515e175c67036 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -110,61 +110,6 @@ pub const StateKind = enum(u8) { subshell, }; -/// Copy-on-write -pub fn Cow(comptime T: type, comptime VTable: type) type { - const Handler = struct { - fn copy(this: *T) T { - if (@hasDecl(VTable, "copy")) @compileError(@typeName(VTable) ++ " needs `copy()` function"); - return VTable.copy(this); - } - - fn deinit(this: *T) void { - if (@hasDecl(VTable, "deinit")) @compileError(@typeName(VTable) ++ " needs `deinit()` function"); - return VTable.deinit(this); - } - }; - - return union(enum) { - borrowed: *T, - owned: T, - - pub fn borrow(val: *T) @This() { - return .{ - .borrowed = val, - }; - } - - pub fn own(val: T) @This() { - return .{ - .owned = val, - }; - } - - /// Get the underlying value. - pub inline fn inner(this: *@This()) *T { - return switch (this.*) { - .borrowed => this.borrowed, - .owned => &this.owned, - }; - } - - pub fn copy(this: *@This()) void { - switch (this.*) { - .borrowed => { - this.* = .{ - .owned = Handler.copy(this.borrowed), - }; - }, - .owned => {}, - } - } - - pub fn deinit(this: *@This()) void { - Handler.deinit(this.inner()); - } - }; -} - /// Copy-on-write file descriptor. This is to avoid having multiple non-blocking /// writers to the same file descriptor, which breaks epoll/kqueue /// @@ -517,8 +462,8 @@ pub const RefCountedStr = struct { /// A) or B) won't even mutate the environment anyway. /// /// A way to reduce copying is to only do it when the env is mutated: copy-on-write. -pub const CowEnvMap = Cow(EnvMap, struct { - pub fn copy(val: *EnvMap) EnvMap { +pub const CowEnvMap = bun.Cow(EnvMap, struct { + pub fn copy(val: *const EnvMap) EnvMap { return val.clone(); } diff --git a/test/bundler/esbuild/__snapshots__/css.test.ts.snap b/test/bundler/esbuild/__snapshots__/css.test.ts.snap new file mode 100644 index 00000000000000..9c03a0015152d9 --- /dev/null +++ b/test/bundler/esbuild/__snapshots__/css.test.ts.snap @@ -0,0 +1,1436 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/001/default/style.css 1`] = ` +"/* 001/default/a.css */ +.box { + background-color: green; +} + +/* 001/default/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/001/relative-url/style.css 1`] = ` +"/* 001/relative-url/a.css */ +.box { + background-color: green; +} + +/* 001/relative-url/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-charset/001/style.css 1`] = ` +"@charset "UTF-8"; + +/* at-charset/001/a.css */ +.box { + background-color: red; +} + +/* at-charset/001/b.css */ +.box { + background-color: green; +} + +/* at-charset/001/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-keyframes/001/style.css 1`] = ` +"/* at-keyframes/001/a.css */ +@media screen { + .box { + animation: BOX; + animation-duration: 0s; + animation-fill-mode: both; + } + @keyframes BOX { + 0%, 100% { + background-color: green; + } + } +} + +/* at-keyframes/001/b.css */ +@media print { + .box { + animation: BOX; + animation-duration: 0s; + animation-fill-mode: both; + } + @keyframes BOX { + 0%, 100% { + background-color: red; + } + } +} + +/* at-keyframes/001/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-layer/001/style.css 1`] = ` +"@layer a; + +/* at-layer/001/b.css */ +@layer b { + .box { + background-color: green; + } +} + +/* at-layer/001/a.css */ +@layer a { + .box { + background-color: red; + } +} + +/* at-layer/001/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-layer/002/style.css 1`] = ` +"@media print { + @layer a; +} + +/* at-layer/002/b.css */ +@layer b { + .box { + background-color: red; + } +} + +/* at-layer/002/a.css */ +@layer a { + .box { + background-color: green; + } +} + +/* at-layer/002/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-layer/003/style.css 1`] = ` +"@layer a; + +/* at-layer/003/b.css */ +@layer b { + .box { + background-color: green; + } +} + +/* at-layer/003/a.css */ +@layer a { + .box { + background-color: red; + } +} + +/* at-layer/003/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-layer/004/style.css 1`] = ` +"/* at-layer/004/b.css */ +@layer { + .box { + background-color: red; + } +} + +/* at-layer/004/a.css */ +@layer { + .box { + background-color: green; + } +} + +/* at-layer/004/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-layer/005/style.css 1`] = ` +"/* at-layer/005/b.css */ +@media (min-width: 1px) { + @layer a { + @media (width: 1px) { + @layer b { + .box { + background-color: red; + } + } + } + } +} + +/* at-layer/005/a.css */ +@media (min-width: 1px) { + @layer a; +} + +/* at-layer/005/style.css */ +@layer a.c { + .box { + background-color: red; + } +} +@layer a.b { + .box { + background-color: green; + } +} +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-layer/006/style.css 1`] = ` +"/* at-layer/006/b.css */ +@media (min-width: 1px) { + @layer a { + @media (min-width: 1px) { + @layer b { + .box { + background-color: red; + } + } + } + } +} + +/* at-layer/006/a.css */ +@media (min-width: 1px) { + @layer a; +} + +/* at-layer/006/style.css */ +@layer a.c { + .box { + background-color: green; + } +} +@layer a.b { + .box { + background-color: red; + } +} +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-layer/007/style.css 1`] = ` +"/* at-layer/007/style.css */ +@layer foo { +} +@layer bar { +} +@layer bar { + .box { + background-color: green; + } +} +@layer foo { + .box { + background-color: red; + } +} +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-layer/008/style.css 1`] = ` +"/* at-layer/008/b.css */ +@layer { + @layer { + .box { + background-color: red; + } + } +} + +/* at-layer/008/a.css */ +@layer { + .box { + background-color: green; + } +} + +/* at-layer/008/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-media/001/default/style.css 1`] = ` +"/* at-media/001/default/a.css */ +@media screen { + .box { + background-color: green; + } +} + +/* at-media/001/default/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-media/002/style.css 1`] = ` +"/* at-media/002/a.css */ +@media screen { + .box { + background-color: green; + } +} + +/* at-media/002/b.css */ +@media print { + .box { + background-color: red; + } +} + +/* at-media/002/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-media/003/style.css 1`] = ` +"/* at-media/003/b.css */ +@media screen { + @media (min-width: 1px) { + .box { + background-color: green; + } + } +} + +/* at-media/003/a.css */ + +/* at-media/003/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-media/004/style.css 1`] = ` +"/* at-media/004/c.css */ +.box { + background-color: green; +} + +/* at-media/004/b.css */ +@media print { + @media print { + .box { + background-color: red; + } + } +} + +/* at-media/004/a.css */ + +/* at-media/004/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-media/005/style.css 1`] = ` +"/* at-media/005/c.css */ +.box { + background-color: green; +} + +/* at-media/005/b.css */ +@media (max-width: 1px) { + @media (max-width: 1px) { + .box { + background-color: red; + } + } +} + +/* at-media/005/a.css */ + +/* at-media/005/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-media/006/style.css 1`] = ` +"/* at-media/006/b.css */ +@media (min-height: 1px) { + @media (min-width: 1px) { + .box { + background-color: green; + } + } +} + +/* at-media/006/a.css */ + +/* at-media/006/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-media/007/style.css 1`] = ` +"/* at-media/007/b.css */ +@media all { + @media screen { + .box { + background-color: green; + } + } +} + +/* at-media/007/a.css */ + +/* at-media/007/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-media/008/style.css 1`] = ` +"/* at-media/008/green.css */ +@media all { + @layer alpha { + @media print { + @layer alpha { + .box { + background-color: green; + } + } + } + } +} + +/* at-media/008/a.css */ +@media all { + @layer alpha; +} + +/* at-media/008/red.css */ +@media all { + @layer beta { + @media print { + @layer beta { + .box { + background-color: red; + } + } + } + } +} + +/* at-media/008/b.css */ +@media all { + @layer beta; +} + +/* at-media/008/style.css */ +@layer beta { + .box { + background-color: green; + } +} +@layer alpha { + .box { + background-color: red; + } +} +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-supports/001/style.css 1`] = ` +"/* at-supports/001/a.css */ +@supports (display: block) { + .box { + background-color: green; + } +} + +/* at-supports/001/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-supports/002/style.css 1`] = ` +"/* at-supports/002/b.css */ +@supports (display: block) { + @supports (width: 10px) { + .box { + background-color: green; + } + } +} + +/* at-supports/002/a.css */ + +/* at-supports/002/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-supports/003/style.css 1`] = ` +"/* at-supports/003/b.css */ +@supports ((display: block) or (display: inline)) { + @supports (width: 10px) { + .box { + background-color: green; + } + } +} + +/* at-supports/003/a.css */ + +/* at-supports/003/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-supports/004/style.css 1`] = ` +"/* at-supports/004/b.css */ +@supports (display: block) { + @layer a { + @supports (width: 10px) { + @layer b { + .box { + background-color: green; + } + } + } + } +} + +/* at-supports/004/a.css */ +@supports (display: block) { + @layer a; +} + +/* at-supports/004/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/at-supports/005/style.css 1`] = ` +"/* at-supports/005/green.css */ +@supports (display: block) { + @layer alpha { + @supports (foo: bar) { + @layer alpha { + .box { + background-color: green; + } + } + } + } +} + +/* at-supports/005/a.css */ +@supports (display: block) { + @layer alpha; +} + +/* at-supports/005/red.css */ +@supports (display: block) { + @layer beta { + @supports (foo: bar) { + @layer beta { + .box { + background-color: red; + } + } + } + } +} + +/* at-supports/005/b.css */ +@supports (display: block) { + @layer beta; +} + +/* at-supports/005/style.css */ +@layer beta { + .box { + background-color: green; + } +} +@layer alpha { + .box { + background-color: red; + } +} +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/cycles/001/style.css 1`] = ` +"/* cycles/001/style.css */ +.box { + background-color: green; +} +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/cycles/002/style.css 1`] = ` +"/* cycles/002/red.css */ +.box { + background-color: red; +} + +/* cycles/002/green.css */ +.box { + background-color: green; +} + +/* cycles/002/b.css */ + +/* cycles/002/a.css */ + +/* cycles/002/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/cycles/003/style.css 1`] = ` +"/* cycles/003/b.css */ +.box { + background-color: red; +} + +/* cycles/003/a.css */ +.box { + background-color: green; +} + +/* cycles/003/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/cycles/004/style.css 1`] = ` +"/* cycles/004/a.css */ +.box { + background-color: red; +} + +/* cycles/004/b.css */ +.box { + background-color: green; +} + +/* cycles/004/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/cycles/005/style.css 1`] = ` +"/* cycles/005/b.css */ +.box { + background-color: red; +} + +/* cycles/005/a.css */ +.box { + background-color: green; +} + +/* cycles/005/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/cycles/006/style.css 1`] = ` +"/* cycles/006/red.css */ +.box { + background-color: red; +} + +/* cycles/006/green.css */ +.box { + background-color: green; +} + +/* cycles/006/b.css */ + +/* cycles/006/a.css */ + +/* cycles/006/c.css */ + +/* cycles/006/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/cycles/007/style.css 1`] = ` +"/* cycles/007/green.css */ +.box { + background-color: green; +} + +/* cycles/007/red.css */ +@media all { + .box { + background-color: red; + } +} + +/* cycles/007/a.css */ + +/* cycles/007/b.css */ + +/* cycles/007/red.css */ +@media not print { + .box { + background-color: red; + } +} + +/* cycles/007/green.css */ +@media not print { + @media screen { + .box { + background-color: green; + } + } +} + +/* cycles/007/b.css */ + +/* cycles/007/a.css */ + +/* cycles/007/c.css */ + +/* cycles/007/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/cycles/008/style.css 1`] = ` +"/* cycles/008/green.css */ +@layer { + .box { + background-color: green; + } +} + +/* cycles/008/red.css */ +@layer { + .box { + background-color: red; + } +} + +/* cycles/008/a.css */ + +/* cycles/008/b.css */ + +/* cycles/008/red.css */ +@layer { + @layer { + .box { + background-color: red; + } + } +} + +/* cycles/008/green.css */ +@layer { + @layer { + .box { + background-color: green; + } + } +} + +/* cycles/008/b.css */ + +/* cycles/008/a.css */ + +/* cycles/008/c.css */ + +/* cycles/008/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/data-urls/002/style.css 1`] = ` +"/* */ +.box { + background-color: green; +} + +/* data-urls/002/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/data-urls/003/style.css 1`] = ` +"/* */ +.box { + background-color: green; +} + +/* data-urls/003/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/duplicates/001/style.css 1`] = ` +"/* duplicates/001/b.css */ +.box { + background-color: red; +} + +/* duplicates/001/a.css */ +.box { + background-color: green; +} + +/* duplicates/001/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/duplicates/002/style.css 1`] = ` +"/* duplicates/002/b.css */ +.box { + background-color: red; +} + +/* duplicates/002/a.css */ +.box { + background-color: green; +} + +/* duplicates/002/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/empty/001/style.css 1`] = ` +"/* empty/001/empty.css */ + +/* empty/001/style.css */ +.box { + background-color: green; +} +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/relative-paths/001/style.css 1`] = ` +"/* relative-paths/001/b/b.css */ +.box { + background-color: green; +} + +/* relative-paths/001/a/a.css */ + +/* relative-paths/001/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/relative-paths/002/style.css 1`] = ` +"/* relative-paths/002/b/b.css */ +.box { + background-color: green; +} + +/* relative-paths/002/a/a.css */ + +/* relative-paths/002/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/subresource/001/style.css 1`] = ` +"/* subresource/001/something/styles/green.css */ +.box { + background-image: url(data:image/png;base64,Li4u); +} + +/* subresource/001/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/subresource/002/style.css 1`] = ` +"/* subresource/002/styles/green.css */ +.box { + background-image: url(data:image/png;base64,Li4u); +} + +/* subresource/002/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/subresource/004/style.css 1`] = ` +"/* subresource/004/styles/green.css */ +.box { + background-image: url(data:image/png;base64,Li4u); +} + +/* subresource/004/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/subresource/005/style.css 1`] = ` +"/* subresource/005/styles/green.css */ +.box { + background-image: url(data:image/png;base64,Li4u); +} + +/* subresource/005/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/subresource/007/style.css 1`] = ` +"/* subresource/007/style.css */ +.box { + background-image: url(data:image/png;base64,Li4u); +} +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/url-format/001/default/style.css 1`] = ` +"/* url-format/001/default/a.css */ +.box { + background-color: green; +} + +/* url-format/001/default/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/url-format/001/relative-url/style.css 1`] = ` +"/* url-format/001/relative-url/a.css */ +.box { + background-color: green; +} + +/* url-format/001/relative-url/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/url-format/002/default/style.css 1`] = ` +"/* url-format/002/default/a.css */ +.box { + background-color: green; +} + +/* url-format/002/default/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/url-format/002/relative-url/style.css 1`] = ` +"/* url-format/002/relative-url/a.css */ +.box { + background-color: green; +} + +/* url-format/002/relative-url/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/url-format/003/default/style.css 1`] = ` +"/* url-format/003/default/a.css */ +.box { + background-color: green; +} + +/* url-format/003/default/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/url-format/003/relative-url/style.css 1`] = ` +"/* url-format/003/relative-url/a.css */ +.box { + background-color: green; +} + +/* url-format/003/relative-url/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/url-fragments/001/style.css 1`] = ` +"/* url-fragments/001/a.css#foo */ +.box { + background-color: green; +} + +/* url-fragments/001/style.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/url-fragments/002/style.css 1`] = ` +"/* url-fragments/002/a.css#1 */ +.box { + background-color: green; +} + +/* url-fragments/002/b.css#2 */ +.box { + background-color: red; +} + +/* url-fragments/002/a.css#3 */ +.box { + background-color: green; +} + +/* url-fragments/002/style.css */ + +" +`; +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /001/default/style.css 1`] = `"@import url("a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /001/relative-url/style.css 1`] = `"@import url("./a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-charset/001/style.css 1`] = `"@charset "utf-8"; @import url("a.css"); @import url("b.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-keyframes/001/style.css 1`] = `"@import url("a.css") screen; @import url("b.css") print;"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-layer/001/style.css 1`] = ` +"@import url("a.css") layer(a); +@import url("b.css") layer(b); +@import url("a.css") layer(a);" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-layer/002/style.css 1`] = ` +"@import url("a.css") layer(a) print; +@import url("b.css") layer(b); +@import url("a.css") layer(a);" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-layer/003/style.css 1`] = `"@import url("a.css"); @import url("b.css"); @import url("a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-layer/004/style.css 1`] = `"@import url("a.css"); @import url("b.css"); @import url("a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-layer/005/style.css 1`] = ` +"@import url("a.css") layer(a) (min-width: 1px); +@layer a.c { .box { background-color: red; } } +@layer a.b { .box { background-color: green; } }" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-layer/006/style.css 1`] = ` +"@import url("a.css") layer(a) (min-width: 1px); +@layer a.c { .box { background-color: green; } } +@layer a.b { .box { background-color: red; } }" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-layer/007/style.css 1`] = ` +"@layer foo {} +@layer bar {} +@layer bar { .box { background-color: green; } } +@layer foo { .box { background-color: red; } }" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-layer/008/style.css 1`] = `"@import url("a.css") layer;"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-media/001/default/style.css 1`] = `"@import url("a.css") screen;"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-media/002/style.css 1`] = `"@import url("a.css") screen; @import url("b.css") print;"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-media/003/style.css 1`] = `"@import url("a.css") screen;"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-media/004/style.css 1`] = `"@import url("c.css"); @import url("a.css") print;"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-media/005/style.css 1`] = `"@import url("c.css"); @import url("a.css") (max-width: 1px);"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-media/006/style.css 1`] = `"@import url("a.css") (min-height: 1px);"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-media/007/style.css 1`] = `"@import url("a.css") all;"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-media/008/style.css 1`] = ` +"@import url("a.css") layer(alpha) all; +@import url("b.css") layer(beta) all; +@layer beta { .box { background-color: green; } } +@layer alpha { .box { background-color: red; } }" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-supports/001/style.css 1`] = `"@import url("a.css") supports(display: block);"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-supports/002/style.css 1`] = `"@import url("a.css") supports(display: block);"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-supports/003/style.css 1`] = `"@import url("a.css") supports((display: block) or (display: inline));"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-supports/004/style.css 1`] = `"@import url("a.css") layer(a) supports(display: block);"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /at-supports/005/style.css 1`] = ` +"@import url("a.css") layer(alpha) supports(display: block); +@import url("b.css") layer(beta) supports(display: block); +@layer beta { .box { background-color: green; } } +@layer alpha { .box { background-color: red; } }" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /cycles/001/style.css 1`] = `"@import url("style.css"); .box { background-color: green; }"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /cycles/002/style.css 1`] = `"@import url("a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /cycles/003/style.css 1`] = `"@import url("a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /cycles/004/style.css 1`] = `"@import url("a.css"); @import url("b.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /cycles/005/style.css 1`] = `"@import url("a.css"); @import url("b.css"); @import url("a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /cycles/006/style.css 1`] = `"@import url("b.css"); @import url("c.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /cycles/007/style.css 1`] = `"@import url("b.css"); @import url("c.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /cycles/008/style.css 1`] = `"@import url("b.css"); @import url("c.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /data-urls/002/style.css 1`] = `"@import url('data:text/css;plain,.box%20%7B%0A%09background-color%3A%20green%3B%0A%7D%0A');"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /data-urls/003/style.css 1`] = `"@import url('data:text/css,.box%20%7B%0A%09background-color%3A%20green%3B%0A%7D%0A');"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /duplicates/001/style.css 1`] = `"@import url("a.css"); @import url("b.css"); @import url("a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /duplicates/002/style.css 1`] = `"@import url("a.css"); @import url("b.css"); @import url("a.css"); @import url("b.css"); @import url("a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /empty/001/style.css 1`] = `"@import url("./empty.css"); .box { background-color: green; }"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /relative-paths/001/style.css 1`] = `"@import url("./a/a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /relative-paths/002/style.css 1`] = `"@import url("./a/a.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /subresource/001/style.css 1`] = `"@import url("./something/styles/green.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /subresource/002/style.css 1`] = `"@import url("./styles/green.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /subresource/004/style.css 1`] = `"@import url("./styles/green.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /subresource/005/style.css 1`] = `"@import url("./styles/green.css");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /subresource/007/style.css 1`] = `".box { background-image: url("./green.png"); }"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /url-format/001/default/style.css 1`] = `"@import url(a.css);"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /url-format/001/relative-url/style.css 1`] = `"@import url(./a.css);"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /url-format/002/default/style.css 1`] = `"@import "a.css";"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /url-format/002/relative-url/style.css 1`] = `"@import "./a.css";"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /url-format/003/default/style.css 1`] = `"@import url("a.css""`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /url-format/003/relative-url/style.css 1`] = `"@import url("./a.css""`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /url-fragments/001/style.css 1`] = `"@import url("./a.css#foo");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /url-fragments/002/style.css 1`] = `"@import url("./a.css#1"); @import url("./b.css#2"); @import url("./a.css#3");"`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundle: case1.css 1`] = ` +"@layer first.one; + +/* case1-foo.css */ +@layer last.one { + body { + color: red; + } +} + +/* case1-foo.css */ +@layer first.one { + body { + color: red; + } +} + +/* case1.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundle: case2.css 1`] = ` +"@layer first.one; + +/* case2-bar.css */ +@layer last.one { + body { + color: green; + } +} + +/* case2-foo.css */ +@layer first.one { + body { + color: red; + } +} + +/* case2.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundle: case3.css 1`] = ` +"/* case3-bar.css */ +@layer only.one { + body { + color: green; + } +} + +/* case3-foo.css */ +@layer { + body { + color: red; + } +} + +/* case3.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundle: case4.css 1`] = ` +"@layer first { + @layer one, one.two, one.three.four; +} + +/* case4-foo.css */ +@layer last { + @layer one { + @layer two, three.four; + + body { + color: red; + } + } +} + +/* case4-foo.css */ +@layer first { + @layer one { + @layer two, three.four; + + body { + color: red; + } + } +} + +/* case4.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundle: case5.css 1`] = ` +"/* case5-foo.css */ +@layer middle { + @layer one { + @layer two, three.four; + + body { + color: red; + } + } +} + +/* case5-foo.css */ +@layer { + @layer one { + @layer two, three.four; + + body { + color: red; + } + } +} + +/* case5.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundle: case6.css 1`] = ` +"@layer first; + +/* case6-foo.css */ +@layer last { + @layer { + @layer two, three.four; + + body { + color: red; + } + } +} + +/* case6-foo.css */ +@layer first { + @layer { + @layer two, three.four; + + body { + color: red; + } + } +} + +/* case6.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundleAlternatingLayerInFile: case1.css 1`] = ` +"/* a.css */ +@layer first { + body { + color: red; + } +} + +/* case1.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundleAlternatingLayerInFile: case2.css 1`] = ` +"@layer first; + +/* b.css */ +@layer last { + body { + color: green; + } +} + +/* a.css */ +@layer first { + body { + color: red; + } +} + +/* case2.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundleAlternatingLayerInFile: case3.css 1`] = ` +"@layer first; +@layer last; + +/* a.css */ +@layer first { + body { + color: red; + } +} + +/* b.css */ +@layer last { + body { + color: green; + } +} + +/* case3.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundleAlternatingLayerInFile: case4.css 1`] = ` +"@layer first; + +/* b.css */ +@layer last { + body { + color: green; + } +} + +/* a.css */ +@layer first { + body { + color: red; + } +} + +/* case4.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundleAlternatingLayerInFile: case5.css 1`] = ` +"@layer first; +@layer last; + +/* a.css */ +@layer first { + body { + color: red; + } +} + +/* b.css */ +@layer last { + body { + color: green; + } +} + +/* case5.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAtImportConditionsAtLayerBundleAlternatingLayerInFile: case6.css 1`] = ` +"@layer first; + +/* b.css */ +@layer last { + body { + color: green; + } +} + +/* a.css */ +@layer first { + body { + color: red; + } +} + +/* case6.css */ + +" +`; + +exports[`esbuild-bundler css/CSSAndJavaScriptCodeSplittingESBuildIssue1064: /a.js 1`] = ` +"import shared from './shared.js' +console.log(shared() + 1)" +`; + +exports[`esbuild-bundler css/CSSAndJavaScriptCodeSplittingESBuildIssue1064: /b.js 1`] = ` +"import shared from './shared.js' +console.log(shared() + 2)" +`; + +exports[`esbuild-bundler css/CSSAndJavaScriptCodeSplittingESBuildIssue1064: /c.css 1`] = ` +"@import "./shared.css"; +body { color: red }" +`; + +exports[`esbuild-bundler css/CSSAndJavaScriptCodeSplittingESBuildIssue1064: /d.css 1`] = ` +"@import "./shared.css"; +body { color: blue }" +`; diff --git a/test/bundler/esbuild/css.test.ts b/test/bundler/esbuild/css.test.ts index db049477532486..eb7bf1b4631dee 100644 --- a/test/bundler/esbuild/css.test.ts +++ b/test/bundler/esbuild/css.test.ts @@ -1,5 +1,7 @@ import { describe } from "bun:test"; import { itBundled } from "../expectBundled"; +import { readdirSync } from "node:fs"; +import { join } from "node:path"; // Tests ported from: // https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_css_test.go @@ -185,8 +187,9 @@ body { }); }); -describe.todo("bundler", () => { +describe("esbuild-bundler", () => { itBundled("css/CSSEntryPoint", { + experimentalCss: true, // GENERATED files: { "/entry.css": /* css */ ` @@ -197,14 +200,17 @@ describe.todo("bundler", () => { }, }); itBundled("css/CSSAtImportMissing", { + experimentalCss: true, files: { "/entry.css": `@import "./missing.css";`, }, bundleErrors: { - "/entry.css": ['Could not resolve "./missing.css"'], + "/entry.css": ['Could not resolve: "./missing.css"'], }, }); itBundled("css/CSSAtImportExternal", { + experimentalCss: true, + external: ["./external1.css", "./external2.css", "./external3.css", "./external4.css", "./external5.css"], // GENERATED files: { "/entry.css": /* css */ ` @@ -238,8 +244,39 @@ describe.todo("bundler", () => { .after { color: blue } `, }, + outfile: "/out/out.css", + onAfterBundle(api) { + api.expectFile("/out/out.css").toEqualIgnoringWhitespace(/* css */ `@import "./external1.css"; +@import "./external2.css"; +@import "./external4.css"; +@import "./external5.css"; +@import "https://www.example.com/style2.css"; +@import "./external3.css"; +@import "https://www.example.com/style1.css"; +@import "https://www.example.com/style3.css"; +@import "./external5.css" screen; + +/* internal.css */ +.before { + color: red; +} + +/* charset1.css */ +.middle { + color: green; +} + +/* charset2.css */ +.after { + color: #00f; +} + +/* entry.css */`); + }, }); itBundled("css/CSSAtImport", { + experimentalCss: true, + // GENERATED files: { "/entry.css": /* css */ ` @@ -259,6 +296,8 @@ describe.todo("bundler", () => { }, }); itBundled("css/CSSFromJSMissingImport", { + experimentalCss: true, + // GENERATED files: { "/entry.js": /* js */ ` @@ -267,11 +306,13 @@ describe.todo("bundler", () => { `, "/a.css": `.a { color: red }`, }, - /* TODO FIX expectedCompileLog: `entry.js: ERROR: No matching export in "a.css" for import "missing" - `, */ + bundleErrors: { + "/entry.js": ['No matching export in "a.css" for import "missing"'], + }, }); itBundled("css/CSSFromJSMissingStarImport", { - // GENERATED + experimentalCss: true, + outdir: "/out", files: { "/entry.js": /* js */ ` import * as ns from "./a.css" @@ -279,9 +320,19 @@ describe.todo("bundler", () => { `, "/a.css": `.a { color: red }`, }, + bundleWarnings: { + "/entry.js": ['Import "missing" will always be undefined because there is no matching export in "a.css"'], + }, + onAfterBundle(api) { + api.expectFile("/out/entry.css").toEqualIgnoringWhitespace(/* css */ `/* a.css */ + .a{ + color: red; + }`); + }, }); itBundled("css/ImportCSSFromJS", { - // GENERATED + experimentalCss: true, + outdir: "/out", files: { "/entry.js": /* js */ ` import "./a.js" @@ -299,38 +350,44 @@ describe.todo("bundler", () => { "/b.css": `.b { color: blue }`, }, }); - itBundled("css/ImportCSSFromJSWriteToStdout", { - // GENERATED - files: { - "/entry.js": `import "./entry.css"`, - "/entry.css": `.entry { color: red }`, - }, - /* TODO FIX expectedScanLog: `entry.js: ERROR: Cannot import "entry.css" into a JavaScript file without an output path configured - `, */ - }); + // itBundled("css/ImportCSSFromJSWriteToStdout", { + // experimentalCss: true, + // files: { + // "/entry.js": `import "./entry.css"`, + // "/entry.css": `.entry { color: red }`, + // }, + // bundleErrors: { + // "/entry.js": ['Cannot import "entry.css" into a JavaScript file without an output path configured'], + // }, + // }); itBundled("css/ImportJSFromCSS", { - // GENERATED + experimentalCss: true, + outdir: "/out", files: { - "/entry.js": `export default 123`, - "/entry.css": `@import "./entry.js";`, + "/entry.ts": `export default 123`, + "/entry.css": `@import "./entry.ts";`, }, entryPoints: ["/entry.css"], - /* TODO FIX expectedScanLog: `entry.css: ERROR: Cannot import "entry.js" into a CSS file - NOTE: An "@import" rule can only be used to import another CSS file, and "entry.js" is not a CSS file (it was loaded with the "js" loader). - `, */ + bundleErrors: { + "/entry.css": ['Cannot import a ".ts" file into a CSS file'], + }, }); itBundled("css/ImportJSONFromCSS", { + experimentalCss: true, + // GENERATED files: { "/entry.json": `{}`, "/entry.css": `@import "./entry.json";`, }, entryPoints: ["/entry.css"], - /* TODO FIX expectedScanLog: `entry.css: ERROR: Cannot import "entry.json" into a CSS file - NOTE: An "@import" rule can only be used to import another CSS file, and "entry.json" is not a CSS file (it was loaded with the "json" loader). - `, */ + bundleErrors: { + "/entry.css": ['Cannot import a ".json" file into a CSS file'], + }, }); itBundled("css/MissingImportURLInCSS", { + experimentalCss: true, + // GENERATED files: { "/src/entry.css": /* css */ ` @@ -338,12 +395,14 @@ describe.todo("bundler", () => { b { background: url("./two.png"); } `, }, - /* TODO FIX expectedScanLog: `src/entry.css: ERROR: Could not resolve "./one.png" - src/entry.css: ERROR: Could not resolve "./two.png" - `, */ + bundleErrors: { + "/src/entry.css": ['Could not resolve: "./one.png"', 'Could not resolve: "./two.png"'], + }, }); + + // Skipping for now itBundled("css/ExternalImportURLInCSS", { - // GENERATED + experimentalCss: true, files: { "/src/entry.css": /* css */ ` div:after { @@ -359,8 +418,12 @@ describe.todo("bundler", () => { path { fill: url(#filter) } `, }, + external: ["./src/external.png"], }); + itBundled("css/InvalidImportURLInCSS", { + experimentalCss: true, + // GENERATED files: { "/entry.css": /* css */ ` @@ -380,6 +443,15 @@ describe.todo("bundler", () => { "/json.json": `{ "test": true }`, "/css.css": `a { color: red }`, }, + bundleErrors: { + "/entry.css": [ + 'Cannot import a ".jsx" file into a CSS file', + 'Cannot import a ".jsx" file into a CSS file', + 'Cannot import a ".ts" file into a CSS file', + 'Cannot import a ".tsx" file into a CSS file', + 'Cannot import a ".json" file into a CSS file', + ], + }, /* TODO FIX expectedScanLog: `entry.css: ERROR: Cannot use "js.js" as a URL NOTE: You can't use a "url()" token to reference the file "js.js" because it was loaded with the "js" loader, which doesn't provide a URL to embed in the resulting CSS. entry.css: ERROR: Cannot use "jsx.jsx" as a URL @@ -395,7 +467,8 @@ describe.todo("bundler", () => { `, */ }); itBundled("css/TextImportURLInCSSText", { - // GENERATED + experimentalCss: true, + outfile: "/out.css", files: { "/entry.css": /* css */ ` a { @@ -404,19 +477,18 @@ describe.todo("bundler", () => { `, "/example.txt": `This is some text.`, }, - }); - itBundled("css/DataURLImportURLInCSS", { - // GENERATED - files: { - "/entry.css": /* css */ ` - a { - background: url(./example.png); - } - `, - "/example.png": `\x89\x50\x4E\x47\x0D\x0A\x1A\x0A`, + onAfterBundle(api) { + api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ ` +/* entry.css */ +a { + background: url("data:text/plain;base64,VGhpcyBpcyBzb21lIHRleHQu"); +} +`); }, }); - itBundled("css/BinaryImportURLInCSS", { + itBundled("css/Png", { + experimentalCss: true, + outfile: "/out.css", // GENERATED files: { "/entry.css": /* css */ ` @@ -424,22 +496,84 @@ describe.todo("bundler", () => { background: url(./example.png); } `, - "/example.png": `\x89\x50\x4E\x47\x0D\x0A\x1A\x0A`, + "/example.png": new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), }, - }); - itBundled("css/Base64ImportURLInCSS", { - // GENERATED - files: { - "/entry.css": /* css */ ` - a { - background: url(./example.png); - } - `, - "/example.png": `\x89\x50\x4E\x47\x0D\x0A\x1A\x0A`, + onAfterBundle(api) { + api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ ` +/* entry.css */ +a { + background: url("data:image/png;base64,iVBORw0KGgo="); +} +`); }, }); + + // We don't support dataurl rn + // itBundled("css/DataURLImportURLInCSS", { + // experimentalCss: true, + // outfile: "/out.css", + // // GENERATED + // files: { + // "/entry.css": /* css */ ` + // a { + // background: url(./example.png); + // } + // `, + // "/example.png": new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + // }, + // loader: { + // ".png": "dataurl", + // }, + // onAfterBundle(api) { + // api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ ` + // /* entry.css */ + // a { + // background: url("data:image/png;base64,iVBORw0KGgo="); + // } + // `); + // }, + // }); + + // We don't support binary loader rn + // itBundled("css/BinaryImportURLInCSS", { + // experimentalCss: true, + + // // GENERATED + // files: { + // "/entry.css": /* css */ ` + // a { + // background: url(./example.png); + // } + // `, + // "/example.png": new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + // }, + // onAfterBundle(api) { + // api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ ` + // /* entry.css */ + // a { + // background: url("data:image/png;base64,iVBORw0KGgo="); + // } + // `); + // }, + // }); + + // We don't support base64 loader rn + // itBundled("css/Base64ImportURLInCSS", { + // experimentalCss: true, + + // // GENERATED + // files: { + // "/entry.css": /* css */ ` + // a { + // background: url(./example.png); + // } + // `, + // "/example.png": `\x89\x50\x4E\x47\x0D\x0A\x1A\x0A`, + // }, + // }); + itBundled("css/FileImportURLInCSS", { - // GENERATED + experimentalCss: true, files: { "/entry.css": /* css */ ` @import "./one.css"; @@ -447,10 +581,34 @@ describe.todo("bundler", () => { `, "/one.css": `a { background: url(./example.data) }`, "/two.css": `b { background: url(./example.data) }`, - "/example.data": `This is some data.`, + "/example.data": new Array(128 * 1024 + 1).fill("Z".charCodeAt(0)).join(""), + }, + loader: { + ".data": "file", + }, + outdir: "/out", + async onAfterBundle(api) { + api.expectFile("/out/example-ra0pdz4b.data").toEqual(new Array(128 * 1024 + 1).fill("Z".charCodeAt(0)).join("")); + + api.expectFile("/out/entry.css").toEqualIgnoringWhitespace(/* css */ ` +/* one.css */ +a { + background: url("./example-ra0pdz4b.data"); +} + +/* two.css */ +b { + background: url("./example-ra0pdz4b.data"); +} + +/* entry.css */ +`); }, }); + itBundled("css/IgnoreURLsInAtRulePrelude", { + experimentalCss: true, + // GENERATED files: { "/entry.css": /* css */ ` @@ -461,11 +619,12 @@ describe.todo("bundler", () => { `, }, }); + itBundled("css/PackageURLsInCSS", { - // GENERATED + experimentalCss: true, files: { "/entry.css": /* css */ ` - @import "test.css"; + @import "./test.css"; a { background: url(a/1.png); } b { background: url(b/2.png); } @@ -477,18 +636,54 @@ describe.todo("bundler", () => { "/c/3.png": `c-3`, "/node_modules/c/3.png": `c-3-node_modules`, }, + outfile: "/out.css", + onAfterBundle(api) { + api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ ` +/* test.css */ +.css { + color: red; +} + +/* entry.css */ +a { + background: url("data:image/png;base64,YS0x"); +} +b { + background: url("data:image/png;base64,Yi0yLW5vZGVfbW9kdWxlcw=="); +} +c { + background: url("data:image/png;base64,Yy0z"); +} +`); + }, }); + itBundled("css/CSSAtImportExtensionOrderCollision", { - // GENERATED + experimentalCss: true, files: { + // This should avoid picking ".js" because it's explicitly configured as non-CSS "/entry.css": `@import "./test";`, "/test.js": `console.log('js')`, "/test.css": `.css { color: red }`, }, outfile: "/out.css", - extensionOrder: [".js", ".css"], + // extensionOrder: [".js", ".css"], + onAfterBundle(api) { + api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ ` +/* test.css */ +.css { + color: red; +} + +/* entry.css */ +`); + }, }); + + /* We don't support `extensionOrder`/`--resolve-extensions` rn itBundled("css/CSSAtImportExtensionOrderCollisionUnsupported", { + experimentalCss: true, + // GENERATED files: { "/entry.css": `@import "./test";`, @@ -501,36 +696,794 @@ describe.todo("bundler", () => { "/entry.css": ['ERROR: No loader is configured for ".sass" files: test.sass'], }, }); - itBundled("css/CSSAtImportConditionsNoBundle", { - // GENERATED + */ + + // itBundled("css/CSSAtImportConditionsNoBundle", { + // experimentalCss: true, + // files: { + // "/entry.css": `@import "./print.css" print;`, + // }, + // }); + + itBundled("css/CSSAtImportConditionsBundleExternal", { + experimentalCss: true, files: { - "/entry.css": `@import "./print.css" print;`, + "/entry.css": /* css */ `@import "https://example.com/print.css" print;`, + }, + outfile: "/out.css", + onAfterBundle(api) { + api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ ` +@import "https://example.com/print.css" print; + +/* entry.css */ +`); }, - mode: "passthrough", }); - itBundled("css/CSSAtImportConditionsBundleExternal", { - // GENERATED + + itBundled("css/CSSAtImportConditionsBundleExternalConditionWithURL", { + experimentalCss: true, files: { - "/entry.css": `@import "https://example.com/print.css" print;`, + "/entry.css": /* css */ `@import "https://example.com/foo.css" supports(background: url("foo.png"));`, }, }); - itBundled("css/CSSAtImportConditionsBundleExternalConditionWithURL", { - // GENERATED + + itBundled("css/CSSAtImportConditionsBundleLOL", { + experimentalCss: true, + outfile: "/out.css", + files: { + "/entry.css": /* css */ ` +@import url(http://example.com/foo.css); +@import url(http://example.com/foo.css) layer; +@import url(http://example.com/foo.css) layer(layer-name); +@import url(http://example.com/foo.css) layer(layer-name) supports(display: flex); +@import url(http://example.com/foo.css) layer(layer-name) supports(display: flex) (min-width: 768px) and + (max-width: 1024px); +@import url(http://example.com/foo.css) layer(layer-name) (min-width: 768px) and (max-width: 1024px); +@import url(http://example.com/foo.css) supports(display: flex); +@import url(http://example.com/foo.css) supports(display: flex) (min-width: 768px) and (max-width: 1024px); +@import url(http://example.com/foo.css) (min-width: 768px) and (max-width: 1024px); + +@import url(./foo.css); +@import url(./foo.css) layer; +@import url(./foo.css) layer(layer-name); +@import url(./foo.css) layer(layer-name) supports(display: flex); +@import url(./foo.css) layer(layer-name) supports(display: flex) (min-width: 768px) and (max-width: 1024px); +@import url(./foo.css) layer(layer-name) (min-width: 768px) and (max-width: 1024px); +@import url(./foo.css) supports(display: flex); +@import url(./foo.css) supports(display: flex) (min-width: 768px) and (max-width: 1024px); +@import url(./foo.css) (min-width: 768px) and (max-width: 1024px); + +@import url(./empty-1.css) layer(empty-1); +@import url(./empty-2.css) supports(empty: 2); +@import url(./empty-3.css) (empty: 3); + +@import "./nested-layer.css" layer(outer); +@import "./nested-layer.css" supports(outer: true); +@import "./nested-layer.css" (outer: true); +@import "./nested-supports.css" layer(outer); +@import "./nested-supports.css" supports(outer: true); +@import "./nested-supports.css" (outer: true); +@import "./nested-media.css" layer(outer); +@import "./nested-media.css" supports(outer: true); +@import "./nested-media.css" (outer: true); + `, + + "/foo.css": /* css */ `body { color: red }`, + + "/empty-1.css": ``, + "/empty-2.css": ``, + "/empty-3.css": ``, + + "/nested-layer.css": /* css */ `@import "./foo.css" layer(inner);`, + "/nested-supports.css": /* css */ `@import "./foo.css" supports(inner: true);`, + "/nested-media.css": /* css */ `@import "./foo.css" (inner: true);`, + }, + onAfterBundle(api) { + // api.expectFile("/out.css").toMatchSnapshot(); + api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ `@import "http://example.com/foo.css"; + @import "http://example.com/foo.css" layer; + @import "http://example.com/foo.css" layer(layer-name); + @import "http://example.com/foo.css" layer(layer-name) supports(display: flex); + @import "http://example.com/foo.css" layer(layer-name) (min-width: 768px) and (max-width: 1024px); + @import "http://example.com/foo.css" supports(display: flex); + @import "http://example.com/foo.css" (min-width: 768px) and (max-width: 1024px); + + /* foo.css */ + body { + color: red; + } + + /* foo.css */ + @layer { + body { + color: red; + } + } + + /* foo.css */ + @layer layer-name { + body { + color: red; + } + } + + /* foo.css */ + @supports (display: flex) { + @layer layer-name { + body { + color: red; + } + } + } + + /* foo.css */ + @media (min-width: 768px) and (max-width: 1024px) { + @layer layer-name { + body { + color: red; + } + } + } + + /* foo.css */ + @supports (display: flex) { + body { + color: red; + } + } + + /* foo.css */ + @media (min-width: 768px) and (max-width: 1024px) { + body { + color: red; + } + } + + /* empty-1.css */ + @layer empty-1; + + /* empty-2.css */ + + + /* empty-3.css */ + + + /* foo.css */ + @layer outer { + @layer inner { + body { + color: red; + } + } + } + + /* nested-layer.css */ + @layer outer; + + /* foo.css */ + @supports (outer: true) { + @layer inner { + body { + color: red; + } + } + } + + /* nested-layer.css */ + + + /* foo.css */ + @media (outer: true) { + @layer inner { + body { + color: red; + } + } + } + + /* nested-layer.css */ + + + /* foo.css */ + @layer outer { + @supports (inner: true) { + body { + color: red; + } + } + } + + /* nested-supports.css */ + @layer outer; + + /* foo.css */ + @supports (outer: true) { + @supports (inner: true) { + body { + color: red; + } + } + } + + /* nested-supports.css */ + + + /* foo.css */ + @media (outer: true) { + @supports (inner: true) { + body { + color: red; + } + } + } + + /* nested-supports.css */ + + + /* foo.css */ + @layer outer { + @media (inner: true) { + body { + color: red; + } + } + } + + /* nested-media.css */ + @layer outer; + + /* foo.css */ + @supports (outer: true) { + @media (inner: true) { + body { + color: red; + } + } + } + + /* nested-media.css */ + + + /* foo.css */ + @media (outer: true) { + @media (inner: true) { + body { + color: red; + } + } + } + + /* nested-media.css */ + + + /* entry.css */ + `); + }, + }); + + // This tests that bun correctly clones the import records for all import + // condition tokens. If they aren't cloned correctly, then something will + // likely crash with an out-of-bounds error. + itBundled("css/CSSAtImportConditionsWithImportRecordsBundle", { + experimentalCss: true, files: { - "/entry.css": `@import "https://example.com/foo.css" (foo: url("foo.png")) and (bar: url("bar.png"));`, + "/entry.css": /* css */ ` + @import url(./foo.css) supports(background: url(./a.png)); + @import url(./foo.css) supports(background: url(./b.png)) list-of-media-queries; + @import url(./foo.css) layer(layer-name) supports(background: url(./a.png)); + @import url(./foo.css) layer(layer-name) supports(background: url(./b.png)) list-of-media-queries; + `, + "/foo.css": /* css */ `body { color: red }`, + "/a.png": `A`, + "/b.png": `B`, + }, + outfile: "/out.css", + onAfterBundle(api) { + api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ ` +/* foo.css */ +@supports (background: url(./a.png)) { + body { + color: red; + } +} + +/* foo.css */ +@media list-of-media-queries { + @supports (background: url(./b.png)) { + body { + color: red; + } + } +} + +/* foo.css */ +@supports (background: url(./a.png)) { + @layer layer-name { + body { + color: red; + } + } +} + +/* foo.css */ +@media list-of-media-queries { + @supports (background: url(./b.png)) { + @layer layer-name { + body { + color: red; + } + } + } +} + +/* entry.css */ +`); }, }); - itBundled("css/CSSAtImportConditionsBundle", { - // GENERATED + + const files = [ + "/001/default/style.css", + "/001/relative-url/style.css", + "/at-charset/001/style.css", + "/at-keyframes/001/style.css", + "/at-layer/001/style.css", + "/at-layer/002/style.css", + "/at-layer/003/style.css", + "/at-layer/004/style.css", + "/at-layer/005/style.css", + "/at-layer/006/style.css", + "/at-layer/007/style.css", + "/at-layer/008/style.css", + "/at-media/001/default/style.css", + "/at-media/002/style.css", + "/at-media/003/style.css", + "/at-media/004/style.css", + "/at-media/005/style.css", + "/at-media/006/style.css", + "/at-media/007/style.css", + "/at-media/008/style.css", + "/at-supports/001/style.css", + "/at-supports/002/style.css", + "/at-supports/003/style.css", + "/at-supports/004/style.css", + "/at-supports/005/style.css", + "/cycles/001/style.css", + "/cycles/002/style.css", + "/cycles/003/style.css", + "/cycles/004/style.css", + "/cycles/005/style.css", + "/cycles/006/style.css", + "/cycles/007/style.css", + "/cycles/008/style.css", + "/data-urls/002/style.css", + "/data-urls/003/style.css", + "/duplicates/001/style.css", + "/duplicates/002/style.css", + "/empty/001/style.css", + "/relative-paths/001/style.css", + "/relative-paths/002/style.css", + "/subresource/001/style.css", + "/subresource/002/style.css", + "/subresource/004/style.css", + "/subresource/005/style.css", + "/subresource/007/style.css", + "/url-format/001/default/style.css", + "/url-format/001/relative-url/style.css", + "/url-format/002/default/style.css", + "/url-format/002/relative-url/style.css", + "/url-format/003/default/style.css", + "/url-format/003/relative-url/style.css", + "/url-fragments/001/style.css", + "/url-fragments/002/style.css", + ]; + + // From: https://github.com/romainmenke/css-import-tests. These test cases just + // serve to document any changes in bun's behavior. Any changes in behavior + // should be tested to ensure they don't cause any regressions. The easiest way + // to test the changes is to bundle https://github.com/evanw/css-import-tests + // and visually inspect a browser's rendering of the resulting CSS file. + itBundled("css/CSSAtImportConditionsFromExternalRepo", { + experimentalCss: true, files: { - "/entry.css": `@import "./print.css" print;`, - "/print.css": `body { color: red }`, + "/001/default/a.css": `.box { background-color: green; }`, + "/001/default/style.css": `@import url("a.css");`, + + "/001/relative-url/a.css": `.box { background-color: green; }`, + "/001/relative-url/style.css": `@import url("./a.css");`, + + "/at-charset/001/a.css": `@charset "utf-8"; .box { background-color: red; }`, + "/at-charset/001/b.css": `@charset "utf-8"; .box { background-color: green; }`, + "/at-charset/001/style.css": `@charset "utf-8"; @import url("a.css"); @import url("b.css");`, + + "/at-keyframes/001/a.css": ` + .box { animation: BOX; animation-duration: 0s; animation-fill-mode: both; } + @keyframes BOX { 0%, 100% { background-color: green; } } + `, + "/at-keyframes/001/b.css": ` + .box { animation: BOX; animation-duration: 0s; animation-fill-mode: both; } + @keyframes BOX { 0%, 100% { background-color: red; } } + `, + "/at-keyframes/001/style.css": `@import url("a.css") screen; @import url("b.css") print;`, + + "/at-layer/001/a.css": `.box { background-color: red; }`, + "/at-layer/001/b.css": `.box { background-color: green; }`, + "/at-layer/001/style.css": ` + @import url("a.css") layer(a); + @import url("b.css") layer(b); + @import url("a.css") layer(a); + `, + + "/at-layer/002/a.css": `.box { background-color: green; }`, + "/at-layer/002/b.css": `.box { background-color: red; }`, + "/at-layer/002/style.css": ` + @import url("a.css") layer(a) print; + @import url("b.css") layer(b); + @import url("a.css") layer(a); + `, + + "/at-layer/003/a.css": `@layer a { .box { background-color: red; } }`, + "/at-layer/003/b.css": `@layer b { .box { background-color: green; } }`, + "/at-layer/003/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`, + + "/at-layer/004/a.css": `@layer { .box { background-color: green; } }`, + "/at-layer/004/b.css": `@layer { .box { background-color: red; } }`, + "/at-layer/004/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`, + + "/at-layer/005/a.css": `@import url("b.css") layer(b) (width: 1px);`, + "/at-layer/005/b.css": `.box { background-color: red; }`, + "/at-layer/005/style.css": ` + @import url("a.css") layer(a) (min-width: 1px); + @layer a.c { .box { background-color: red; } } + @layer a.b { .box { background-color: green; } } + `, + + "/at-layer/006/a.css": `@import url("b.css") layer(b) (min-width: 1px);`, + "/at-layer/006/b.css": `.box { background-color: red; }`, + "/at-layer/006/style.css": ` + @import url("a.css") layer(a) (min-width: 1px); + @layer a.c { .box { background-color: green; } } + @layer a.b { .box { background-color: red; } } + `, + + "/at-layer/007/style.css": ` + @layer foo {} + @layer bar {} + @layer bar { .box { background-color: green; } } + @layer foo { .box { background-color: red; } } + `, + + "/at-layer/008/a.css": `@import "b.css" layer; .box { background-color: green; }`, + "/at-layer/008/b.css": `.box { background-color: red; }`, + "/at-layer/008/style.css": `@import url("a.css") layer;`, + + "/at-media/001/default/a.css": `.box { background-color: green; }`, + "/at-media/001/default/style.css": `@import url("a.css") screen;`, + + "/at-media/002/a.css": `.box { background-color: green; }`, + "/at-media/002/b.css": `.box { background-color: red; }`, + "/at-media/002/style.css": `@import url("a.css") screen; @import url("b.css") print;`, + + "/at-media/003/a.css": `@import url("b.css") (min-width: 1px);`, + "/at-media/003/b.css": `.box { background-color: green; }`, + "/at-media/003/style.css": `@import url("a.css") screen;`, + + "/at-media/004/a.css": `@import url("b.css") print;`, + "/at-media/004/b.css": `.box { background-color: red; }`, + "/at-media/004/c.css": `.box { background-color: green; }`, + "/at-media/004/style.css": `@import url("c.css"); @import url("a.css") print;`, + + "/at-media/005/a.css": `@import url("b.css") (max-width: 1px);`, + "/at-media/005/b.css": `.box { background-color: red; }`, + "/at-media/005/c.css": `.box { background-color: green; }`, + "/at-media/005/style.css": `@import url("c.css"); @import url("a.css") (max-width: 1px);`, + + "/at-media/006/a.css": `@import url("b.css") (min-width: 1px);`, + "/at-media/006/b.css": `.box { background-color: green; }`, + "/at-media/006/style.css": `@import url("a.css") (min-height: 1px);`, + + "/at-media/007/a.css": `@import url("b.css") screen;`, + "/at-media/007/b.css": `.box { background-color: green; }`, + "/at-media/007/style.css": `@import url("a.css") all;`, + + "/at-media/008/a.css": `@import url("green.css") layer(alpha) print;`, + "/at-media/008/b.css": `@import url("red.css") layer(beta) print;`, + "/at-media/008/green.css": `.box { background-color: green; }`, + "/at-media/008/red.css": `.box { background-color: red; }`, + "/at-media/008/style.css": ` + @import url("a.css") layer(alpha) all; + @import url("b.css") layer(beta) all; + @layer beta { .box { background-color: green; } } + @layer alpha { .box { background-color: red; } } + `, + + "/at-supports/001/a.css": `.box { background-color: green; }`, + "/at-supports/001/style.css": `@import url("a.css") supports(display: block);`, + + "/at-supports/002/a.css": `@import url("b.css") supports(width: 10px);`, + "/at-supports/002/b.css": `.box { background-color: green; }`, + "/at-supports/002/style.css": `@import url("a.css") supports(display: block);`, + + "/at-supports/003/a.css": `@import url("b.css") supports(width: 10px);`, + "/at-supports/003/b.css": `.box { background-color: green; }`, + "/at-supports/003/style.css": `@import url("a.css") supports((display: block) or (display: inline));`, + + "/at-supports/004/a.css": `@import url("b.css") layer(b) supports(width: 10px);`, + "/at-supports/004/b.css": `.box { background-color: green; }`, + "/at-supports/004/style.css": `@import url("a.css") layer(a) supports(display: block);`, + + "/at-supports/005/a.css": `@import url("green.css") layer(alpha) supports(foo: bar);`, + "/at-supports/005/b.css": `@import url("red.css") layer(beta) supports(foo: bar);`, + "/at-supports/005/green.css": `.box { background-color: green; }`, + "/at-supports/005/red.css": `.box { background-color: red; }`, + "/at-supports/005/style.css": ` + @import url("a.css") layer(alpha) supports(display: block); + @import url("b.css") layer(beta) supports(display: block); + @layer beta { .box { background-color: green; } } + @layer alpha { .box { background-color: red; } } + `, + + "/cycles/001/style.css": `@import url("style.css"); .box { background-color: green; }`, + + "/cycles/002/a.css": `@import url("red.css"); @import url("b.css");`, + "/cycles/002/b.css": `@import url("green.css"); @import url("a.css");`, + "/cycles/002/green.css": `.box { background-color: green; }`, + "/cycles/002/red.css": `.box { background-color: red; }`, + "/cycles/002/style.css": `@import url("a.css");`, + + "/cycles/003/a.css": `@import url("b.css"); .box { background-color: green; }`, + "/cycles/003/b.css": `@import url("a.css"); .box { background-color: red; }`, + "/cycles/003/style.css": `@import url("a.css");`, + + "/cycles/004/a.css": `@import url("b.css"); .box { background-color: red; }`, + "/cycles/004/b.css": `@import url("a.css"); .box { background-color: green; }`, + "/cycles/004/style.css": `@import url("a.css"); @import url("b.css");`, + + "/cycles/005/a.css": `@import url("b.css"); .box { background-color: green; }`, + "/cycles/005/b.css": `@import url("a.css"); .box { background-color: red; }`, + "/cycles/005/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`, + + "/cycles/006/a.css": `@import url("red.css"); @import url("b.css");`, + "/cycles/006/b.css": `@import url("green.css"); @import url("a.css");`, + "/cycles/006/c.css": `@import url("a.css");`, + "/cycles/006/green.css": `.box { background-color: green; }`, + "/cycles/006/red.css": `.box { background-color: red; }`, + "/cycles/006/style.css": `@import url("b.css"); @import url("c.css");`, + + "/cycles/007/a.css": `@import url("red.css"); @import url("b.css") screen;`, + "/cycles/007/b.css": `@import url("green.css"); @import url("a.css") all;`, + "/cycles/007/c.css": `@import url("a.css") not print;`, + "/cycles/007/green.css": `.box { background-color: green; }`, + "/cycles/007/red.css": `.box { background-color: red; }`, + "/cycles/007/style.css": `@import url("b.css"); @import url("c.css");`, + + "/cycles/008/a.css": `@import url("red.css") layer; @import url("b.css");`, + "/cycles/008/b.css": `@import url("green.css") layer; @import url("a.css");`, + "/cycles/008/c.css": `@import url("a.css") layer;`, + "/cycles/008/green.css": `.box { background-color: green; }`, + "/cycles/008/red.css": `.box { background-color: red; }`, + "/cycles/008/style.css": `@import url("b.css"); @import url("c.css");`, + + "/data-urls/002/style.css": `@import url('data:text/css;plain,.box%20%7B%0A%09background-color%3A%20green%3B%0A%7D%0A');`, + + "/data-urls/003/style.css": `@import url('data:text/css,.box%20%7B%0A%09background-color%3A%20green%3B%0A%7D%0A');`, + + "/duplicates/001/a.css": `.box { background-color: green; }`, + "/duplicates/001/b.css": `.box { background-color: red; }`, + "/duplicates/001/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`, + + "/duplicates/002/a.css": `.box { background-color: green; }`, + "/duplicates/002/b.css": `.box { background-color: red; }`, + "/duplicates/002/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css"); @import url("b.css"); @import url("a.css");`, + + "/empty/001/empty.css": ``, + "/empty/001/style.css": `@import url("./empty.css"); .box { background-color: green; }`, + + "/relative-paths/001/a/a.css": `@import url("../b/b.css")`, + "/relative-paths/001/b/b.css": `.box { background-color: green; }`, + "/relative-paths/001/style.css": `@import url("./a/a.css");`, + + "/relative-paths/002/a/a.css": `@import url("./../b/b.css")`, + "/relative-paths/002/b/b.css": `.box { background-color: green; }`, + "/relative-paths/002/style.css": `@import url("./a/a.css");`, + + "/subresource/001/something/images/green.png": `...`, + "/subresource/001/something/styles/green.css": `.box { background-image: url("../images/green.png"); }`, + "/subresource/001/style.css": `@import url("./something/styles/green.css");`, + + "/subresource/002/green.png": `...`, + "/subresource/002/style.css": `@import url("./styles/green.css");`, + "/subresource/002/styles/green.css": `.box { background-image: url("../green.png"); }`, + + "/subresource/004/style.css": `@import url("./styles/green.css");`, + "/subresource/004/styles/green.css": `.box { background-image: url("green.png"); }`, + "/subresource/004/styles/green.png": `...`, + + "/subresource/005/style.css": `@import url("./styles/green.css");`, + "/subresource/005/styles/green.css": `.box { background-image: url("./green.png"); }`, + "/subresource/005/styles/green.png": `...`, + + "/subresource/007/green.png": `...`, + "/subresource/007/style.css": `.box { background-image: url("./green.png"); }`, + + "/url-format/001/default/a.css": `.box { background-color: green; }`, + "/url-format/001/default/style.css": `@import url(a.css);`, + + "/url-format/001/relative-url/a.css": `.box { background-color: green; }`, + "/url-format/001/relative-url/style.css": `@import url(./a.css);`, + + "/url-format/002/default/a.css": `.box { background-color: green; }`, + "/url-format/002/default/style.css": `@import "a.css";`, + + "/url-format/002/relative-url/a.css": `.box { background-color: green; }`, + "/url-format/002/relative-url/style.css": `@import "./a.css";`, + + "/url-format/003/default/a.css": `.box { background-color: green; }`, + "/url-format/003/default/style.css": `@import url("a.css"`, + + "/url-format/003/relative-url/a.css": `.box { background-color: green; }`, + "/url-format/003/relative-url/style.css": `@import url("./a.css"`, + + "/url-fragments/001/a.css": `.box { background-color: green; }`, + "/url-fragments/001/style.css": `@import url("./a.css#foo");`, + + "/url-fragments/002/a.css": `.box { background-color: green; }`, + "/url-fragments/002/b.css": `.box { background-color: red; }`, + "/url-fragments/002/style.css": `@import url("./a.css#1"); @import url("./b.css#2"); @import url("./a.css#3");`, + }, + entryPoints: files, + outputPaths: files, + outdir: "/out", + onAfterBundle(api) { + for (const file of files) { + console.log("Checking snapshot:", file); + api.expectFile(join(file)).toMatchSnapshot(file); + } + }, + }); + + itBundled("css/CSSAtImportConditionsAtLayerBundle", { + experimentalCss: true, + files: { + "/case1.css": /* css */ ` + @import url(case1-foo.css) layer(first.one); + @import url(case1-foo.css) layer(last.one); + @import url(case1-foo.css) layer(first.one); + `, + "/case1-foo.css": `body { color: red }`, + + "/case2.css": /* css */ ` + @import url(case2-foo.css); + @import url(case2-bar.css); + @import url(case2-foo.css); + `, + "/case2-foo.css": `@layer first.one { body { color: red } }`, + "/case2-bar.css": `@layer last.one { body { color: green } }`, + + "/case3.css": /* css */ ` + @import url(case3-foo.css); + @import url(case3-bar.css); + @import url(case3-foo.css); + `, + "/case3-foo.css": `@layer { body { color: red } }`, + "/case3-bar.css": `@layer only.one { body { color: green } }`, + + "/case4.css": /* css */ ` + @import url(case4-foo.css) layer(first); + @import url(case4-foo.css) layer(last); + @import url(case4-foo.css) layer(first); + `, + "/case4-foo.css": `@layer one { @layer two, three.four; body { color: red } }`, + + "/case5.css": /* css */ ` + @import url(case5-foo.css) layer; + @import url(case5-foo.css) layer(middle); + @import url(case5-foo.css) layer; + `, + "/case5-foo.css": `@layer one { @layer two, three.four; body { color: red } }`, + + // Note: There was a bug that only showed up in this case. We need at least this many cases. + "/case6.css": /* css */ ` + @import url(case6-foo.css) layer(first); + @import url(case6-foo.css) layer(last); + @import url(case6-foo.css) layer(first); + `, + "/case6-foo.css": `@layer { @layer two, three.four; body { color: red } }`, + }, + entryPoints: ["/case1.css", "/case2.css", "/case3.css", "/case4.css", "/case5.css", "/case6.css"], + outdir: "/out", + onAfterBundle(api) { + const snapshotFiles = ["case1.css", "case2.css", "case3.css", "case4.css", "case5.css", "case6.css"]; + for (const file of snapshotFiles) { + console.log("Checking snapshot:", file); + api.expectFile(join("/out", file)).toMatchSnapshot(file); + } }, - /* TODO FIX expectedScanLog: `entry.css: ERROR: Bundling with conditional "@import" rules is not currently supported - `, */ }); + + itBundled("css/CSSAtImportConditionsAtLayerBundleAlternatingLayerInFile", { + experimentalCss: true, + files: { + "/a.css": `@layer first { body { color: red } }`, + "/b.css": `@layer last { body { color: green } }`, + + "/case1.css": /* css */ ` + @import url(a.css); + @import url(a.css); + `, + + "/case2.css": /* css */ ` + @import url(a.css); + @import url(b.css); + @import url(a.css); + `, + + "/case3.css": /* css */ ` + @import url(a.css); + @import url(b.css); + @import url(a.css); + @import url(b.css); + `, + + "/case4.css": /* css */ ` + @import url(a.css); + @import url(b.css); + @import url(a.css); + @import url(b.css); + @import url(a.css); + `, + + "/case5.css": /* css */ ` + @import url(a.css); + @import url(b.css); + @import url(a.css); + @import url(b.css); + @import url(a.css); + @import url(b.css); + `, + + "/case6.css": /* css */ ` + @import url(a.css); + @import url(b.css); + @import url(a.css); + @import url(b.css); + @import url(a.css); + @import url(b.css); + @import url(a.css); + `, + }, + entryPoints: ["/case1.css", "/case2.css", "/case3.css", "/case4.css", "/case5.css", "/case6.css"], + outdir: "/out", + onAfterBundle(api) { + const snapshotFiles = ["case1.css", "case2.css", "case3.css", "case4.css", "case5.css", "case6.css"]; + for (const file of snapshotFiles) { + console.log("Checking snapshot:", file); + api.expectFile(join("/out", file)).toMatchSnapshot(file); + } + }, + }); + + itBundled("css/CSSAtImportConditionsChainExternal", { + experimentalCss: true, + files: { + "/entry.css": /* css */ ` + @import "a.css" layer(a) not print; + `, + "/a.css": /* css */ ` + @import "http://example.com/external1.css"; + @import "b.css" layer(b) not tv; + @import "http://example.com/external2.css" layer(a2); + `, + "/b.css": /* css */ ` + @import "http://example.com/external3.css"; + @import "http://example.com/external4.css" layer(b2); + `, + }, + outfile: "/out.css", + }); + + // This test mainly just makes sure that this scenario doesn't crash itBundled("css/CSSAndJavaScriptCodeSplittingESBuildIssue1064", { - // GENERATED + experimentalCss: true, files: { "/a.js": /* js */ ` import shared from './shared.js' @@ -554,9 +1507,16 @@ describe.todo("bundler", () => { entryPoints: ["/a.js", "/b.js", "/c.css", "/d.css"], format: "esm", splitting: true, + onAfterBundle(api) { + const files = ["/a.js", "/b.js", "/c.css", "/d.css"]; + for (const file of files) { + api.expectFile(file).toMatchSnapshot(file); + } + }, }); + itBundled("css/CSSExternalQueryAndHashNoMatchESBuildIssue1822", { - // GENERATED + experimentalCss: true, files: { "/entry.css": /* css */ ` a { background: url(foo/bar.png?baz) } @@ -564,23 +1524,16 @@ describe.todo("bundler", () => { `, }, outfile: "/out.css", - /* TODO FIX expectedScanLog: `entry.css: ERROR: Could not resolve "foo/bar.png?baz" - NOTE: You can mark the path "foo/bar.png?baz" as external to exclude it from the bundle, which will remove this error. - entry.css: ERROR: Could not resolve "foo/bar.png#baz" - NOTE: You can mark the path "foo/bar.png#baz" as external to exclude it from the bundle, which will remove this error. - `, */ - }); - itBundled("css/CSSExternalQueryAndHashMatchESBuildIssue1822", { - // GENERATED - files: { - "/entry.css": /* css */ ` - a { background: url(foo/bar.png?baz) } - b { background: url(foo/bar.png#baz) } - `, + bundleErrors: { + "/entry.css": [ + `Could not resolve: "foo/bar.png?baz". Maybe you need to "bun install"?`, + `Could not resolve: "foo/bar.png#baz". Maybe you need to "bun install"?`, + ], }, - outfile: "/out.css", }); itBundled("css/CSSNestingOldBrowser", { + experimentalCss: true, + // GENERATED files: { "/nested-@layer.css": `a { @layer base { color: red; } }`, @@ -650,6 +1603,8 @@ describe.todo("bundler", () => { `, */ }); itBundled("css/MetafileCSSBundleTwoToOne", { + experimentalCss: true, + files: { "/foo/entry.js": /* js */ ` import '../common.css' @@ -667,6 +1622,8 @@ describe.todo("bundler", () => { outdir: "/", }); itBundled("css/DeduplicateRules", { + experimentalCss: true, + // GENERATED files: { "/yes0.css": `a { color: red; color: green; color: red }`, diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index f229a603670b3f..7796810e9203e1 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -1,7 +1,7 @@ /** * See `./expectBundled.md` for how this works. */ -import { BuildConfig, BuildOutput, BunPlugin, fileURLToPath, PluginBuilder } from "bun"; +import { BuildConfig, BuildOutput, BunPlugin, fileURLToPath, PluginBuilder, Loader } from "bun"; import { callerSourceOrigin } from "bun:jsc"; import type { Matchers } from "bun:test"; import * as esbuild from "esbuild"; @@ -186,7 +186,7 @@ export interface BundlerTestInput { publicPath?: string; keepNames?: boolean; legalComments?: "none" | "inline" | "eof" | "linked" | "external"; - loader?: Record; + loader?: Record<`.${string}`, Loader>; mangleProps?: RegExp; mangleQuoted?: boolean; mainFields?: string[]; @@ -543,7 +543,7 @@ function expectBundled( const loaderValues = [...new Set(Object.values(loader))]; const supportedLoaderTypes = ["js", "jsx", "ts", "tsx", "css", "json", "text", "file", "wtf", "toml"]; const unsupportedLoaderTypes = loaderValues.filter(x => !supportedLoaderTypes.includes(x)); - if (unsupportedLoaderTypes.length) { + if (unsupportedLoaderTypes.length > 0) { throw new Error(`loader '${unsupportedLoaderTypes.join("', '")}' not implemented in bun build`); } } diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 5c85ae7b07d998..8aa51ebd346bff 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -108,2117 +108,355 @@ describe("css tests", () => { }); describe("border", () => { - // cssTest( - // ` - // .foo { - // border-left: 2px solid red; - // border-right: 2px solid red; - // border-bottom: 2px solid red; - // border-top: 2px solid red; - // } - // `, - // indoc` - // .foo { - // border: 2px solid red; - // } - // `, - // ); - // TODO: this - // cssTest( - // ` - // .foo { - // border-left-color: red; - // border-right-color: red; - // border-bottom-color: red; - // border-top-color: red; - // } - // `, - // indoc` - // .foo { - // border-color: red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-left-width: thin; - // border-right-width: thin; - // border-bottom-width: thin; - // border-top-width: thin; - // } - // `, - // indoc` - // .foo { - // border-width: thin; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-left-style: dotted; - // border-right-style: dotted; - // border-bottom-style: dotted; - // border-top-style: dotted; - // } - // `, - // indoc` - // .foo { - // border-style: dotted; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-left-width: thin; - // border-left-style: dotted; - // border-left-color: red; - // } - // `, - // indoc` - // .foo { - // border-left: thin dotted red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-left-width: thick; - // border-left: thin dotted red; - // } - // `, - // indoc` - // .foo { - // border-left: thin dotted red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-left-width: thick; - // border: thin dotted red; - // } - // `, - // indoc` - // .foo { - // border: thin dotted red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border: thin dotted red; - // border-right-width: thick; - // } - // `, - // indoc` - // .foo { - // border: thin dotted red; - // border-right-width: thick; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border: thin dotted red; - // border-right: thick dotted red; - // } - // `, - // indoc` - // .foo { - // border: thin dotted red; - // border-right-width: thick; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border: thin dotted red; - // border-right-width: thick; - // border-right-style: solid; - // } - // `, - // indoc` - // .foo { - // border: thin dotted red; - // border-right: thick solid red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-top: thin dotted red; - // border-block-start: thick solid green; - // } - // `, - // indoc` - // .foo { - // border-top: thin dotted red; - // border-block-start: thick solid green; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border: thin dotted red; - // border-block-start-width: thick; - // border-left-width: medium; - // } - // `, - // indoc` - // .foo { - // border: thin dotted red; - // border-block-start-width: thick; - // border-left-width: medium; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start: thin dotted red; - // border-inline-end: thin dotted red; - // } - // `, - // indoc` - // .foo { - // border-block-start: thin dotted red; - // border-inline-end: thin dotted red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start-width: thin; - // border-block-start-style: dotted; - // border-block-start-color: red; - // border-inline-end: thin dotted red; - // } - // `, - // indoc` - // .foo { - // border-block-start: thin dotted red; - // border-inline-end: thin dotted red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start: thin dotted red; - // border-block-end: thin dotted red; - // } - // `, - // indoc` - // .foo { - // border-block: thin dotted red; - // } - // `, - // ); - // minifyTest( - // ` - // .foo { - // border: none; - // } - // `, - // `.foo{border:none}`, - // ); - // minifyTest(".foo { border-width: 0 0 1px; }", ".foo{border-width:0 0 1px}"); - // cssTest( - // ` - // .foo { - // border-block-width: 1px; - // border-inline-width: 1px; - // } - // `, - // indoc` - // .foo { - // border-width: 1px; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start-width: 1px; - // border-block-end-width: 1px; - // border-inline-start-width: 1px; - // border-inline-end-width: 1px; - // } - // `, - // indoc` - // .foo { - // border-width: 1px; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start-width: 1px; - // border-block-end-width: 1px; - // border-inline-start-width: 2px; - // border-inline-end-width: 2px; - // } - // `, - // indoc` - // .foo { - // border-block-width: 1px; - // border-inline-width: 2px; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start-width: 1px; - // border-block-end-width: 1px; - // border-inline-start-width: 2px; - // border-inline-end-width: 3px; - // } - // `, - // indoc` - // .foo { - // border-block-width: 1px; - // border-inline-width: 2px 3px; - // } - // `, - // ); - // minifyTest( - // ".foo { border-bottom: 1px solid var(--spectrum-global-color-gray-200)}", - // ".foo{border-bottom:1px solid var(--spectrum-global-color-gray-200)}", - // ); - // cssTest( - // ` - // .foo { - // border-width: 0; - // border-bottom: var(--test, 1px) solid; - // } - // `, - // indoc` - // .foo { - // border-width: 0; - // border-bottom: var(--test, 1px) solid; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border: 1px solid black; - // border-width: 1px 1px 0 0; - // } - // `, - // indoc` - // .foo { - // border: 1px solid #000; - // border-width: 1px 1px 0 0; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-top: 1px solid black; - // border-bottom: 1px solid black; - // border-left: 2px solid black; - // border-right: 2px solid black; - // } - // `, - // indoc` - // .foo { - // border: 1px solid #000; - // border-width: 1px 2px; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-top: 1px solid black; - // border-bottom: 1px solid black; - // border-left: 2px solid black; - // border-right: 1px solid black; - // } - // `, - // indoc` - // .foo { - // border: 1px solid #000; - // border-left-width: 2px; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-top: 1px solid black; - // border-bottom: 1px solid black; - // border-left: 1px solid red; - // border-right: 1px solid red; - // } - // `, - // indoc` - // .foo { - // border: 1px solid #000; - // border-color: #000 red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start: 1px solid black; - // border-block-end: 1px solid black; - // border-inline-start: 1px solid red; - // border-inline-end: 1px solid red; - // } - // `, - // indoc` - // .foo { - // border: 1px solid #000; - // border-inline-color: red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start: 1px solid black; - // border-block-end: 1px solid black; - // border-inline-start: 2px solid black; - // border-inline-end: 2px solid black; - // } - // `, - // indoc` - // .foo { - // border: 1px solid #000; - // border-inline-width: 2px; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start: 1px solid black; - // border-block-end: 1px solid black; - // border-inline-start: 2px solid red; - // border-inline-end: 2px solid red; - // } - // `, - // indoc` - // .foo { - // border: 1px solid #000; - // border-inline: 2px solid red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start: 1px solid black; - // border-block-end: 1px solid black; - // border-inline-start: 2px solid red; - // border-inline-end: 3px solid red; - // } - // `, - // indoc` - // .foo { - // border: 1px solid #000; - // border-inline-start: 2px solid red; - // border-inline-end: 3px solid red; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start: 2px solid black; - // border-block-end: 1px solid black; - // border-inline-start: 2px solid red; - // border-inline-end: 2px solid red; - // } - // `, - // indoc` - // .foo { - // border: 2px solid red; - // border-block-start-color: #000; - // border-block-end: 1px solid #000; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start: 2px solid red; - // border-block-end: 1px solid red; - // border-inline-start: 2px solid red; - // border-inline-end: 2px solid red; - // } - // `, - // indoc` - // .foo { - // border: 2px solid red; - // border-block-end-width: 1px; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border-block-start: 2px solid red; - // border-block-end: 2px solid red; - // border-inline-start: 2px solid red; - // border-inline-end: 1px solid red; - // } - // `, - // indoc` - // .foo { - // border: 2px solid red; - // border-inline-end-width: 1px; - // } - // `, - // ); - // cssTest( - // ` - // .foo { - // border: 1px solid currentColor; - // } - // `, - // indoc` - // .foo { - // border: 1px solid; - // } - // `, - // ); - // minifyTest( - // ` - // .foo { - // border: 1px solid currentColor; - // } - // `, - // ".foo{border:1px solid}", - // ); - // prefix_test( - // ` - // .foo { - // border-block: 2px solid red; - // } - // `, - // indoc` - // .foo { - // border-top: 2px solid red; - // border-bottom: 2px solid red; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-block-start: 2px solid red; - // } - // `, - // indoc` - // .foo { - // border-top: 2px solid red; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-block-end: 2px solid red; - // } - // `, - // indoc` - // .foo { - // border-bottom: 2px solid red; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline: 2px solid red; - // } - // `, - // indoc` - // .foo { - // border-left: 2px solid red; - // border-right: 2px solid red; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-block-width: 2px; - // } - // `, - // indoc` - // .foo { - // border-block-start-width: 2px; - // border-block-end-width: 2px; - // } - // `, - // { - // safari: 13 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-block-width: 2px; - // } - // `, - // indoc` - // .foo { - // border-block-width: 2px; - // } - // `, - // { - // safari: 15 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-start: 2px solid red; - // } - // `, - // indoc` - // .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left: 2px solid red; - // } - // .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left: 2px solid red; - // } - // .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-right: 2px solid red; - // } - // .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-right: 2px solid red; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-start-width: 2px; - // } - // `, - // indoc` - // .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left-width: 2px; - // } - // .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left-width: 2px; - // } - // .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-right-width: 2px; - // } - // .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-right-width: 2px; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-end: 2px solid red; - // } - // `, - // indoc` - // .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-right: 2px solid red; - // } - // .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-right: 2px solid red; - // } - // .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left: 2px solid red; - // } - // .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left: 2px solid red; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-start: 2px solid red; - // border-inline-end: 5px solid green; - // } - // `, - // indoc` - // .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left: 2px solid red; - // border-right: 5px solid green; - // } - // .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left: 2px solid red; - // border-right: 5px solid green; - // } - // .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left: 5px solid green; - // border-right: 2px solid red; - // } - // .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left: 5px solid green; - // border-right: 2px solid red; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-start: 2px solid red; - // border-inline-end: 5px solid green; - // } - // .bar { - // border-inline-start: 1px dotted gray; - // border-inline-end: 1px solid black; - // } - // `, - // indoc` - // .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left: 2px solid red; - // border-right: 5px solid green; - // } - // .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left: 2px solid red; - // border-right: 5px solid green; - // } - // .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left: 5px solid green; - // border-right: 2px solid red; - // } - // .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left: 5px solid green; - // border-right: 2px solid red; - // } - // .bar:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left: 1px dotted gray; - // border-right: 1px solid #000; - // } - // .bar:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left: 1px dotted gray; - // border-right: 1px solid #000; - // } - // .bar:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left: 1px solid #000; - // border-right: 1px dotted gray; - // } - // .bar:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left: 1px solid #000; - // border-right: 1px dotted gray; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-width: 2px; - // } - // `, - // indoc` - // .foo { - // border-left-width: 2px; - // border-right-width: 2px; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-width: 2px; - // } - // `, - // indoc` - // .foo { - // border-left-width: 2px; - // border-right-width: 2px; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-style: solid; - // } - // `, - // indoc` - // .foo { - // border-left-style: solid; - // border-right-style: solid; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-color: red; - // } - // `, - // indoc` - // .foo { - // border-left-color: red; - // border-right-color: red; - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-end: var(--test); - // } - // `, - // indoc` - // .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-right: var(--test); - // } - // .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-right: var(--test); - // } - // .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left: var(--test); - // } - // .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left: var(--test); - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-start: var(--start); - // border-inline-end: var(--end); - // } - // `, - // indoc` - // .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left: var(--start); - // border-right: var(--end); - // } - // .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left: var(--start); - // border-right: var(--end); - // } - // .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-right: var(--start); - // border-left: var(--end); - // } - // .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-right: var(--start); - // border-left: var(--end); - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // for (const prop of [ - // "border-inline-start-color", - // "border-inline-end-color", - // "border-block-start-color", - // "border-block-end-color", - // "border-top-color", - // "border-bottom-color", - // "border-left-color", - // "border-right-color", - // "border-color", - // "border-block-color", - // "border-inline-color", - // ]) { - // prefix_test( - // ` - // .foo { - // ${prop}: lab(40% 56.6 39); - // } - // `, - // indoc` - // .foo { - // ${prop}: #b32323; - // ${prop}: lab(40% 56.6 39); - // } - // `, - // { - // chrome: 90 << 16, - // }, - // ); - // } - // for (const prop of [ - // "border", - // "border-inline", - // "border-block", - // "border-left", - // "border-right", - // "border-top", - // "border-bottom", - // "border-block-start", - // "border-block-end", - // "border-inline-start", - // "border-inline-end", - // ]) { - // prefix_test( - // ` - // .foo { - // ${prop}: 2px solid lab(40% 56.6 39); - // } - // `, - // indoc` - // .foo { - // ${prop}: 2px solid #b32323; - // ${prop}: 2px solid lab(40% 56.6 39); - // } - // `, - // { - // chrome: 90 << 16, - // }, - // ); - // } - // for (const prop of [ - // "border", - // "border-inline", - // "border-block", - // "border-left", - // "border-right", - // "border-top", - // "border-bottom", - // "border-block-start", - // "border-block-end", - // "border-inline-start", - // "border-inline-end", - // ]) { - // prefix_test( - // ` - // .foo { - // ${prop}: var(--border-width) solid lab(40% 56.6 39); - // } - // `, - // indoc` - // .foo { - // ${prop}: var(--border-width) solid #b32323; - // } - // @supports (color: lab(0% 0 0)) { - // .foo { - // ${prop}: var(--border-width) solid lab(40% 56.6 39); - // } - // } - // `, - // { - // chrome: 90 << 16, - // }, - // ); - // } - // prefix_test( - // ` - // .foo { - // border-inline-start-color: lab(40% 56.6 39); - // } - // `, - // indoc` - // .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left-color: #b32323; - // border-left-color: lab(40% 56.6 39); - // } - // .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left-color: #b32323; - // border-left-color: lab(40% 56.6 39); - // } - // .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-right-color: #b32323; - // border-right-color: lab(40% 56.6 39); - // } - // .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-right-color: #b32323; - // border-right-color: lab(40% 56.6 39); - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-end-color: lab(40% 56.6 39); - // } - // `, - // indoc` - // .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-right-color: #b32323; - // border-right-color: lab(40% 56.6 39); - // } - // .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-right-color: #b32323; - // border-right-color: lab(40% 56.6 39); - // } - // .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left-color: #b32323; - // border-left-color: lab(40% 56.6 39); - // } - // .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - // border-left-color: #b32323; - // border-left-color: lab(40% 56.6 39); - // } - // `, - // { - // safari: 8 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // border-inline-start-color: lab(40% 56.6 39); - // border-inline-end-color: lch(50.998% 135.363 338); - // } - // `, - // indoc` - // .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - // border-left-color: #b32323; - // border-left-color: lab(40% 56.6 39); - // border-right-color: #ee00be; - // border-right-color: lch(50.998% 135.363 338); - // }`, - // { - // chrome: 8 << 16, - // safari: 14 << 16, - // }, - // ); - }); - - describe("color", () => { - minifyTest(".foo { color: yellow }", ".foo{color:#ff0}"); - minifyTest(".foo { color: rgb(255, 255, 0) }", ".foo{color:#ff0}"); - minifyTest(".foo { color: rgba(255, 255, 0, 1) }", ".foo{color:#ff0}"); - minifyTest(".foo { color: rgba(255, 255, 0, 0.8) }", ".foo{color:#ff0c}"); - minifyTest(".foo { color: rgb(128, 128, 128) }", ".foo{color:gray}"); - minifyTest(".foo { color: rgb(123, 255, 255) }", ".foo{color:#7bffff}"); - minifyTest(".foo { color: rgba(123, 255, 255, 0.5) }", ".foo{color:#7bffff80}"); - minifyTest(".foo { color: rgb(123 255 255) }", ".foo{color:#7bffff}"); - minifyTest(".foo { color: rgb(123 255 255 / .5) }", ".foo{color:#7bffff80}"); - minifyTest(".foo { color: rgb(123 255 255 / 50%) }", ".foo{color:#7bffff80}"); - minifyTest(".foo { color: rgb(48% 100% 100% / 50%) }", ".foo{color:#7affff80}"); - minifyTest(".foo { color: hsl(100deg, 100%, 50%) }", ".foo{color:#5f0}"); - minifyTest(".foo { color: hsl(100, 100%, 50%) }", ".foo{color:#5f0}"); - minifyTest(".foo { color: hsl(100 100% 50%) }", ".foo{color:#5f0}"); - minifyTest(".foo { color: hsl(100, 100%, 50%, .8) }", ".foo{color:#5f0c}"); - minifyTest(".foo { color: hsl(100 100% 50% / .8) }", ".foo{color:#5f0c}"); - minifyTest(".foo { color: hsla(100, 100%, 50%, .8) }", ".foo{color:#5f0c}"); - minifyTest(".foo { color: hsla(100 100% 50% / .8) }", ".foo{color:#5f0c}"); - minifyTest(".foo { color: transparent }", ".foo{color:#0000}"); - minifyTest(".foo { color: currentColor }", ".foo{color:currentColor}"); - minifyTest(".foo { color: ButtonBorder }", ".foo{color:buttonborder}"); - minifyTest(".foo { color: hwb(194 0% 0%) }", ".foo{color:#00c4ff}"); - minifyTest(".foo { color: hwb(194 0% 0% / 50%) }", ".foo{color:#00c4ff80}"); - minifyTest(".foo { color: hwb(194 0% 50%) }", ".foo{color:#006280}"); - minifyTest(".foo { color: hwb(194 50% 0%) }", ".foo{color:#80e1ff}"); - minifyTest(".foo { color: hwb(194 50% 50%) }", ".foo{color:gray}"); - minifyTest(".foo { color: lab(29.2345% 39.3825 20.0664); }", ".foo{color:lab(29.2345% 39.3825 20.0664)}"); - minifyTest(".foo { color: lab(29.2345% 39.3825 20.0664 / 100%); }", ".foo{color:lab(29.2345% 39.3825 20.0664)}"); - minifyTest(".foo { color: lab(29.2345% 39.3825 20.0664 / 50%); }", ".foo{color:lab(29.2345% 39.3825 20.0664/.5)}"); - minifyTest(".foo { color: lch(29.2345% 44.2 27); }", ".foo{color:lch(29.2345% 44.2 27)}"); - minifyTest(".foo { color: lch(29.2345% 44.2 45deg); }", ".foo{color:lch(29.2345% 44.2 45)}"); - minifyTest(".foo { color: lch(29.2345% 44.2 .5turn); }", ".foo{color:lch(29.2345% 44.2 180)}"); - minifyTest(".foo { color: lch(29.2345% 44.2 27 / 100%); }", ".foo{color:lch(29.2345% 44.2 27)}"); - minifyTest(".foo { color: lch(29.2345% 44.2 27 / 50%); }", ".foo{color:lch(29.2345% 44.2 27/.5)}"); - minifyTest(".foo { color: oklab(40.101% 0.1147 0.0453); }", ".foo{color:oklab(40.101% .1147 .0453)}"); - minifyTest(".foo { color: oklch(40.101% 0.12332 21.555); }", ".foo{color:oklch(40.101% .12332 21.555)}"); - minifyTest(".foo { color: oklch(40.101% 0.12332 .5turn); }", ".foo{color:oklch(40.101% .12332 180)}"); - minifyTest(".foo { color: color(display-p3 1 0.5 0); }", ".foo{color:color(display-p3 1 .5 0)}"); - minifyTest(".foo { color: color(display-p3 100% 50% 0%); }", ".foo{color:color(display-p3 1 .5 0)}"); - minifyTest( - ".foo { color: color(xyz-d50 0.2005 0.14089 0.4472); }", - ".foo{color:color(xyz-d50 .2005 .14089 .4472)}", - ); - minifyTest( - ".foo { color: color(xyz-d50 20.05% 14.089% 44.72%); }", - ".foo{color:color(xyz-d50 .2005 .14089 .4472)}", - ); - minifyTest(".foo { color: color(xyz-d65 0.2005 0.14089 0.4472); }", ".foo{color:color(xyz .2005 .14089 .4472)}"); - minifyTest(".foo { color: color(xyz-d65 20.05% 14.089% 44.72%); }", ".foo{color:color(xyz .2005 .14089 .4472)}"); - minifyTest(".foo { color: color(xyz 0.2005 0.14089 0.4472); }", ".foo{color:color(xyz .2005 .14089 .4472)}"); - minifyTest(".foo { color: color(xyz 20.05% 14.089% 44.72%); }", ".foo{color:color(xyz .2005 .14089 .4472)}"); - minifyTest(".foo { color: color(xyz 0.2005 0 0); }", ".foo{color:color(xyz .2005 0 0)}"); - minifyTest(".foo { color: color(xyz 0 0 0); }", ".foo{color:color(xyz 0 0 0)}"); - minifyTest(".foo { color: color(xyz 0 1 0); }", ".foo{color:color(xyz 0 1 0)}"); - minifyTest(".foo { color: color(xyz 0 1 0 / 20%); }", ".foo{color:color(xyz 0 1 0/.2)}"); - minifyTest(".foo { color: color(xyz 0 0 0 / 20%); }", ".foo{color:color(xyz 0 0 0/.2)}"); - minifyTest(".foo { color: color(display-p3 100% 50% 0 / 20%); }", ".foo{color:color(display-p3 1 .5 0/.2)}"); - minifyTest(".foo { color: color(display-p3 100% 0 0 / 20%); }", ".foo{color:color(display-p3 1 0 0/.2)}"); - minifyTest(".foo { color: hsl(none none none) }", ".foo{color:#000}"); - minifyTest(".foo { color: hwb(none none none) }", ".foo{color:red}"); - minifyTest(".foo { color: rgb(none none none) }", ".foo{color:#000}"); - - // If the browser doesn't support `#rrggbbaa` color syntax, it is converted to `transparent`. - attrTest("color: rgba(0, 0, 0, 0)", indoc`color:transparent`, true, { - chrome: 61 << 16, - }); - - // prefix_test( - // ".foo { color: #0000 }", - // indoc` - // .foo { - // color: transparent; - // }`, - // { - // chrome: 61 << 16, - // }, - // ); - - // prefix_test( - // ".foo { color: transparent }", - // indoc` - // .foo { - // color: transparent; - // }`, - // { - // chrome: 61 << 16, - // }, - // ); - - // prefix_test( - // ".foo { color: rgba(0, 0, 0, 0) }", - // indoc` - // .foo { - // color: rgba(0, 0, 0, 0); - // }`, - // { - // chrome: 61 << 16, - // }, - // ); - - // prefix_test( - // ".foo { color: rgba(255, 0, 0, 0) }", - // indoc` - // .foo { - // color: rgba(255,0,0,0); - // }`, - // { - // chrome: 61 << 16, - // }, - // ); - - // prefix_test( - // ".foo { color: rgba(255, 0, 0, 0) }", - // indoc` - // .foo { - // color: #f000; - // }`, - // { - // chrome: 62 << 16, - // }, - // ); - - // prefix_test( - // ".foo { color: rgba(123, 456, 789, 0.5) }", - // indoc` - // .foo { - // color: #7bffff80; - // }`, - // { - // chrome: 95 << 16, - // }, - // ); - - // prefix_test( - // ".foo { color: rgba(123, 255, 255, 0.5) }", - // indoc` - // .foo { - // color: rgba(123, 255, 255, .5); - // }`, - // { - // ie: 11 << 16, - // }, - // ); - - // prefix_test( - // ".foo { color: #7bffff80 }", - // indoc` - // .foo { - // color: rgba(123, 255, 255, .5); - // }`, - // { - // ie: 11 << 16, - // }, - // ); - - // prefix_test( - // ".foo { color: rgba(123, 456, 789, 0.5) }", - // indoc` - // .foo { - // color: rgba(123, 255, 255, .5); - // }`, - // { - // firefox: 48 << 16, - // safari: 10 << 16, - // ios_saf: 9 << 16, - // }, - // ); - - // prefix_test( - // ".foo { color: rgba(123, 456, 789, 0.5) }", - // indoc` - // .foo { - // color: #7bffff80; - // }`, - // { - // firefox: 49 << 16, - // safari: 10 << 16, - // ios_saf: 10 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: lab(40% 56.6 39) }", - // indoc` - // .foo { - // background-color: #b32323; - // background-color: lab(40% 56.6 39); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: lch(40% 68.735435 34.568626) }", - // indoc` - // .foo { - // background-color: #b32323; - // background-color: lch(40% 68.7354 34.5686); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: oklab(59.686% 0.1009 0.1192); }", - // indoc` - // .foo { - // background-color: #c65d07; - // background-color: lab(52.2319% 40.1449 59.9171); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: oklch(40% 0.1268735435 34.568626) }", - // indoc` - // .foo { - // background-color: #7e250f; - // background-color: lab(29.2661% 38.2437 35.3889); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: lab(40% 56.6 39) }", - // indoc` - // .foo { - // background-color: lab(40% 56.6 39); - // }`, - // { - // safari: 15 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: oklab(59.686% 0.1009 0.1192); }", - // indoc` - // .foo { - // background-color: #c65d07; - // background-color: lab(52.2319% 40.1449 59.9171); - // }`, - // { - // chrome: 90 << 16, - // safari: 15 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: oklab(59.686% 0.1009 0.1192); }", - // indoc` - // .foo { - // background-color: #c65d07; - // background-color: color(display-p3 .724144 .386777 .148795); - // background-color: lab(52.2319% 40.1449 59.9171); - // }`, - // { - // chrome: 90 << 16, - // safari: 14 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: lab(40% 56.6 39) }", - // indoc` - // .foo { - // background-color: #b32323; - // background-color: color(display-p3 .643308 .192455 .167712); - // background-color: lab(40% 56.6 39); - // }`, - // { - // chrome: 90 << 16, - // safari: 14 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: oklch(59.686% 0.15619 49.7694); }", - // indoc` - // .foo { - // background-color: #c65d06; - // background-color: lab(52.2321% 40.1417 59.9527); - // }`, - // { - // chrome: 90 << 16, - // safari: 15 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(sRGB 0.41587 0.503670 0.36664); }", - // indoc` - // .foo { - // background-color: #6a805d; - // background-color: color(srgb .41587 .50367 .36664); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }", - // indoc` - // .foo { - // background-color: #6a805d; - // background-color: color(display-p3 .43313 .50108 .3795); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }", - // indoc` - // .foo { - // background-color: #6a805d; - // background-color: color(display-p3 .43313 .50108 .3795); - // }`, - // { - // chrome: 90 << 16, - // safari: 14 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }", - // indoc` - // .foo { - // background-color: color(display-p3 .43313 .50108 .3795); - // }`, - // { - // safari: 14 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }", - // indoc` - // .foo { - // background-color: #6a805d; - // background-color: color(display-p3 .43313 .50108 .3795); - // }`, - // { - // chrome: 90 << 16, - // safari: 15 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }", - // indoc` - // .foo { - // background-color: #6a805d; - // background-color: color(display-p3 .43313 .50108 .3795); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(a98-rgb 0.44091 0.49971 0.37408); }", - // indoc` - // .foo { - // background-color: #6a805d; - // background-color: color(a98-rgb .44091 .49971 .37408); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(a98-rgb 0.44091 0.49971 0.37408); }", - // indoc` - // .foo { - // background-color: color(a98-rgb .44091 .49971 .37408); - // }`, - // { - // safari: 15 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(prophoto-rgb 0.36589 0.41717 0.31333); }", - // indoc` - // .foo { - // background-color: #6a805d; - // background-color: color(prophoto-rgb .36589 .41717 .31333); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(rec2020 0.42210 0.47580 0.35605); }", - // indoc` - // .foo { - // background-color: #728765; - // background-color: color(rec2020 .4221 .4758 .35605); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(xyz-d50 0.2005 0.14089 0.4472); }", - // indoc` - // .foo { - // background-color: #7654cd; - // background-color: color(xyz-d50 .2005 .14089 .4472); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: color(xyz-d65 0.21661 0.14602 0.59452); }", - // indoc` - // .foo { - // background-color: #7654cd; - // background-color: color(xyz .21661 .14602 .59452); - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background-color: lch(50.998% 135.363 338) }", - // indoc` - // .foo { - // background-color: #ee00be; - // background-color: color(display-p3 .972962 -.362078 .804206); - // background-color: lch(50.998% 135.363 338); - // }`, - // { - // chrome: 90 << 16, - // safari: 14 << 16, - // }, - // ); - - // prefix_test( - // ".foo { color: lch(50.998% 135.363 338) }", - // indoc` - // .foo { - // color: #ee00be; - // color: color(display-p3 .972962 -.362078 .804206); - // color: lch(50.998% 135.363 338); - // }`, - // { - // chrome: 90 << 16, - // safari: 14 << 16, - // }, - // ); - - // prefix_test( - // ".foo { background: var(--image) lch(40% 68.735435 34.568626) }", - // indoc` - // .foo { - // background: var(--image) #b32323; - // } - - // @supports (color: lab(0% 0 0)) { - // .foo { - // background: var(--image) lab(40% 56.6 39); - // } - // }`, - // { - // chrome: 90 << 16, - // }, - // ); - - // prefix_test( - // ` - // .foo { - // color: red; - // color: lab(40% 56.6 39); - // } - // `, - // indoc` - // .foo { - // color: red; - // color: lab(40% 56.6 39); - // }`, - // { - // safari: 14 << 16, - // }, - // ); - // prefix_test( - // ` - // .foo { - // color: red; - // color: lab(40% 56.6 39); - // } - // `, - // indoc` - // .foo { - // color: lab(40% 56.6 39); - // }`, - // { - // safari: 16 << 16, - // }, - // ); - - // prefix_test( - // ` - // .foo { - // color: var(--fallback); - // color: lab(40% 56.6 39); - // } - // `, - // indoc` - // .foo { - // color: var(--fallback); - // color: lab(40% 56.6 39); - // }`, - // { - // safari: 14 << 16, - // }, - // ); - - // prefix_test( - // ` - // .foo { - // color: var(--fallback); - // color: lab(40% 56.6 39); - // } - // `, - // indoc` - // .foo { - // color: lab(40% 56.6 39); - // }`, - // { - // safari: 16 << 16, - // }, - // ); - - // prefix_test( - // ` - // .foo { - // color: red; - // color: var(--foo, lab(40% 56.6 39)); - // } - // `, - // indoc` - // .foo { - // color: var(--foo, color(display-p3 .643308 .192455 .167712)); - // } - - // @supports (color: lab(0% 0 0)) { - // .foo { - // color: var(--foo, lab(40% 56.6 39)); - // } - // }`, - // { - // safari: 14 << 16, - // }, - // ); - - // prefix_test( - // ` - // .foo { - // --a: rgb(0 0 0 / var(--alpha)); - // --b: rgb(50% 50% 50% / var(--alpha)); - // --c: rgb(var(--x) 0 0); - // --d: rgb(0 var(--x) 0); - // --e: rgb(0 0 var(--x)); - // --f: rgb(var(--x) 0 0 / var(--alpha)); - // --g: rgb(0 var(--x) 0 / var(--alpha)); - // --h: rgb(0 0 var(--x) / var(--alpha)); - // --i: rgb(none 0 0 / var(--alpha)); - // --j: rgb(from yellow r g b / var(--alpha)); - // } - // `, - // indoc` - // .foo { - // --a: rgba(0, 0, 0, var(--alpha)); - // --b: rgba(128, 128, 128, var(--alpha)); - // --c: rgb(var(--x) 0 0); - // --d: rgb(0 var(--x) 0); - // --e: rgb(0 0 var(--x)); - // --f: rgb(var(--x) 0 0 / var(--alpha)); - // --g: rgb(0 var(--x) 0 / var(--alpha)); - // --h: rgb(0 0 var(--x) / var(--alpha)); - // --i: rgb(none 0 0 / var(--alpha)); - // --j: rgba(255, 255, 0, var(--alpha)); - // }`, - // { - // safari: 11 << 16, - // }, - // ); - - // prefix_test( - // ` - // .foo { - // --a: rgb(0 0 0 / var(--alpha)); - // --b: rgb(50% 50% 50% / var(--alpha)); - // --c: rgb(var(--x) 0 0); - // --d: rgb(0 var(--x) 0); - // --e: rgb(0 0 var(--x)); - // --f: rgb(var(--x) 0 0 / var(--alpha)); - // --g: rgb(0 var(--x) 0 / var(--alpha)); - // --h: rgb(0 0 var(--x) / var(--alpha)); - // --i: rgb(none 0 0 / var(--alpha)); - // --j: rgb(from yellow r g b / var(--alpha)); - // } - // `, - // indoc` - // .foo { - // --a: rgb(0 0 0 / var(--alpha)); - // --b: rgb(128 128 128 / var(--alpha)); - // --c: rgb(var(--x) 0 0); - // --d: rgb(0 var(--x) 0); - // --e: rgb(0 0 var(--x)); - // --f: rgb(var(--x) 0 0 / var(--alpha)); - // --g: rgb(0 var(--x) 0 / var(--alpha)); - // --h: rgb(0 0 var(--x) / var(--alpha)); - // --i: rgb(none 0 0 / var(--alpha)); - // --j: rgb(255 255 0 / var(--alpha)); - // }`, - // { - // safari: 13 << 16, - // }, - // ); - - // prefix_test( - // ` - // .foo { - // --a: hsl(270 100% 50% / var(--alpha)); - // --b: hsl(var(--x) 0 0); - // --c: hsl(0 var(--x) 0); - // --d: hsl(0 0 var(--x)); - // --e: hsl(var(--x) 0 0 / var(--alpha)); - // --f: hsl(0 var(--x) 0 / var(--alpha)); - // --g: hsl(0 0 var(--x) / var(--alpha)); - // --h: hsl(270 100% 50% / calc(var(--alpha) / 2)); - // --i: hsl(none 100% 50% / var(--alpha)); - // --j: hsl(from yellow h s l / var(--alpha)); - // } - // `, - // indoc` - // .foo { - // --a: hsla(270, 100%, 50%, var(--alpha)); - // --b: hsl(var(--x) 0 0); - // --c: hsl(0 var(--x) 0); - // --d: hsl(0 0 var(--x)); - // --e: hsl(var(--x) 0 0 / var(--alpha)); - // --f: hsl(0 var(--x) 0 / var(--alpha)); - // --g: hsl(0 0 var(--x) / var(--alpha)); - // --h: hsla(270, 100%, 50%, calc(var(--alpha) / 2)); - // --i: hsl(none 100% 50% / var(--alpha)); - // --j: hsla(60, 100%, 50%, var(--alpha)); - // }`, - // { - // safari: 11 << 16, - // }, - // ); - - // prefix_test( - // ` - // .foo { - // --a: hsl(270 100% 50% / var(--alpha)); - // --b: hsl(var(--x) 0 0); - // --c: hsl(0 var(--x) 0); - // --d: hsl(0 0 var(--x)); - // --e: hsl(var(--x) 0 0 / var(--alpha)); - // --f: hsl(0 var(--x) 0 / var(--alpha)); - // --g: hsl(0 0 var(--x) / var(--alpha)); - // --h: hsl(270 100% 50% / calc(var(--alpha) / 2)); - // --i: hsl(none 100% 50% / var(--alpha)); - // } - // `, - // indoc` - // .foo { - // --a: hsl(270 100% 50% / var(--alpha)); - // --b: hsl(var(--x) 0 0); - // --c: hsl(0 var(--x) 0); - // --d: hsl(0 0 var(--x)); - // --e: hsl(var(--x) 0 0 / var(--alpha)); - // --f: hsl(0 var(--x) 0 / var(--alpha)); - // --g: hsl(0 0 var(--x) / var(--alpha)); - // --h: hsl(270 100% 50% / calc(var(--alpha) / 2)); - // --i: hsl(none 100% 50% / var(--alpha)); - // } - // `, - // { - // safari: 13 << 16, - // }, - // ); - - // minifyTest( - // ` - // .foo { - // --a: rgb(50% 50% 50% / calc(100% / 2)); - // --b: hsl(calc(360deg / 2) 50% 50%); - // --c: oklab(40.101% calc(0.1 + 0.2) 0.0453); - // --d: color(display-p3 0.43313 0.50108 calc(0.1 + 0.2)); - // --e: rgb(calc(255 / 2), calc(255 / 2), calc(255 / 2)); - // } - // `, - // indoc` - // .foo { - // --a: #80808080; - // --b: #40bfbf; - // --c: oklab(40.101% .3 .0453); - // --d: color(display-p3 .43313 .50108 .3); - // --e: gray; - // } - // `, - // ); - }); - - describe("margin", () => { cssTest( ` .foo { - margin-left: 10px; - margin-right: 10px; - margin-top: 20px; - margin-bottom: 20px; - }`, - indoc` + border-left: 2px solid red; + border-right: 2px solid red; + border-bottom: 2px solid red; + border-top: 2px solid red; + } + `, + ` .foo { - margin: 20px 10px; + border: 2px solid red; } -`, + `, ); cssTest( ` .foo { - margin-block-start: 15px; - margin-block-end: 15px; - }`, - indoc` + border-left-color: red; + border-right-color: red; + border-bottom-color: red; + border-top-color: red; + } + `, + ` .foo { - margin-block: 15px; + border-color: red; } -`, + `, ); cssTest( ` .foo { - margin-left: 10px; - margin-right: 10px; - margin-inline-start: 15px; - margin-inline-end: 15px; - margin-top: 20px; - margin-bottom: 20px; - }`, - indoc` + border-left-width: thin; + border-right-width: thin; + border-bottom-width: thin; + border-top-width: thin; + } + `, + ` .foo { - margin-left: 10px; - margin-right: 10px; - margin-inline: 15px; - margin-top: 20px; - margin-bottom: 20px; + border-width: thin; } -`, + `, ); cssTest( ` .foo { - margin: 10px; - margin-top: 20px; - }`, - indoc` + border-left-style: dotted; + border-right-style: dotted; + border-bottom-style: dotted; + border-top-style: dotted; + } + `, + ` .foo { - margin: 20px 10px 10px; + border-style: dotted; } -`, + `, ); cssTest( ` .foo { - margin: 10px; - margin-top: var(--top); - }`, - indoc` + border-left-width: thin; + border-left-style: dotted; + border-left-color: red; + } + `, + ` .foo { - margin: 10px; - margin-top: var(--top); + border-left: thin dotted red; } -`, + `, ); - prefix_test( + cssTest( ` .foo { - margin-inline-start: 2px; + border-left-width: thick; + border-left: thin dotted red; } `, - indoc` - .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - margin-left: 2px; + ` + .foo { + border-left: thin dotted red; } + `, + ); - .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - margin-left: 2px; + cssTest( + ` + .foo { + border-left-width: thick; + border: thin dotted red; } - - .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - margin-right: 2px; + `, + ` + .foo { + border: thin dotted red; } + `, + ); - .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - margin-right: 2px; + cssTest( + ` + .foo { + border: thin dotted red; + border-right-width: thick; + } + `, + ` + .foo { + border: thin dotted red; + border-right-width: thick; } `, - { - safari: 8 << 16, - }, ); - prefix_test( + cssTest( ` .foo { - margin-inline-start: 2px; - margin-inline-end: 4px; + border: thin dotted red; + border-right: thick dotted red; } `, - indoc` - .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - margin-left: 2px; - margin-right: 4px; + ` + .foo { + border: thin dotted red; + border-right-width: thick; } + `, + ); - .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - margin-left: 2px; - margin-right: 4px; + cssTest( + ` + .foo { + border: thin dotted red; + border-right-width: thick; + border-right-style: solid; } - - .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - margin-left: 4px; - margin-right: 2px; + `, + ` + .foo { + border: thin dotted red; + border-right: thick solid red; } + `, + ); - .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - margin-left: 4px; - margin-right: 2px; + cssTest( + ` + .foo { + border-top: thin dotted red; + border-block-start: thick solid green; + } + `, + ` + .foo { + border-top: thin dotted red; + border-block-start: thick solid green; } `, - { - safari: 8 << 16, - }, ); - prefix_test( + cssTest( ` .foo { - margin-inline: 2px; + border: thin dotted red; + border-block-start-width: thick; + border-left-width: medium; } `, - indoc` + ` .foo { - margin-left: 2px; - margin-right: 2px; + border: thin dotted red; + border-block-start-width: thick; + border-left-width: medium; } `, - { - safari: 8 << 16, - }, ); - prefix_test( + cssTest( ` .foo { - margin-block-start: 2px; + border-block-start: thin dotted red; + border-inline-end: thin dotted red; } `, - indoc` + ` .foo { - margin-top: 2px; + border-block-start: thin dotted red; + border-inline-end: thin dotted red; } `, - { - safari: 8 << 16, - }, ); - prefix_test( + cssTest( ` .foo { - margin-block-end: 2px; + border-block-start-width: thin; + border-block-start-style: dotted; + border-block-start-color: red; + border-inline-end: thin dotted red; } `, - indoc` + ` .foo { - margin-bottom: 2px; + border-block-start: thin dotted red; + border-inline-end: thin dotted red; } `, - { - safari: 8 << 16, - }, ); - prefix_test( + cssTest( ` .foo { - margin-inline-start: 2px; - margin-inline-end: 2px; + border-block-start: thin dotted red; + border-block-end: thin dotted red; } `, - indoc` + ` .foo { - margin-inline-start: 2px; - margin-inline-end: 2px; + border-block: thin dotted red; } `, - { - safari: 13 << 16, - }, ); - prefix_test( + minify_test( ` .foo { - margin-inline: 2px; + border: none; } `, - indoc` + ".foo{border:none}", + ); + + minify_test(".foo { border-width: 0 0 1px; }", ".foo{border-width:0 0 1px}"); + cssTest( + ` .foo { - margin-inline-start: 2px; - margin-inline-end: 2px; + border-block-width: 1px; + border-inline-width: 1px; + } + `, + ` + .foo { + border-width: 1px; + } + `, + ); + cssTest( + ` + .foo { + border-block-start-width: 1px; + border-block-end-width: 1px; + border-inline-start-width: 1px; + border-inline-end-width: 1px; + } + `, + ` + .foo { + border-width: 1px; + } + `, + ); + cssTest( + ` + .foo { + border-block-start-width: 1px; + border-block-end-width: 1px; + border-inline-start-width: 2px; + border-inline-end-width: 2px; + } + `, + ` + .foo { + border-block-width: 1px; + border-inline-width: 2px; + } + `, + ); + cssTest( + ` + .foo { + border-block-start-width: 1px; + border-block-end-width: 1px; + border-inline-start-width: 2px; + border-inline-end-width: 3px; + } + `, + ` + .foo { + border-block-width: 1px; + border-inline-width: 2px 3px; } `, - { - safari: 13 << 16, - }, ); - prefix_test( + minify_test( + ".foo { border-bottom: 1px solid var(--spectrum-global-color-gray-200)}", + ".foo{border-bottom:1px solid var(--spectrum-global-color-gray-200)}", + ); + cssTest( ` .foo { - margin-inline-start: 2px; - margin-inline-end: 2px; + border-width: 0; + border-bottom: var(--test, 1px) solid; } `, - indoc` + ` .foo { - margin-inline: 2px; + border-width: 0; + border-bottom: var(--test, 1px) solid; } `, - { - safari: 15 << 16, - }, ); - prefix_test( + cssTest( ` .foo { - margin-inline: 2px; + border: 1px solid black; + border-width: 1px 1px 0 0; } `, - indoc` + ` .foo { - margin-inline: 2px; + border: 1px solid #000; + border-width: 1px 1px 0 0; } `, - { - safari: 15 << 16, - }, ); - }); - describe("length", () => { - const properties = [ - "margin-right", - "margin", - "padding-right", - "padding", - "width", - "height", - "min-height", - "max-height", - // "line-height", - // "border-radius", - ]; - - for (const prop of properties) { - prefix_test( - ` - .foo { - ${prop}: 22px; - ${prop}: max(4%, 22px); - } - `, - indoc` - .foo { - ${prop}: 22px; - ${prop}: max(4%, 22px); - } - `, - { - safari: 10 << 16, - }, - ); - - prefix_test( - ` - .foo { - ${prop}: 22px; - ${prop}: max(4%, 22px); - } - `, - indoc` - .foo { - ${prop}: max(4%, 22px); - } - `, - { - safari: 14 << 16, - }, - ); - - prefix_test( - ` - .foo { - ${prop}: 22px; - ${prop}: max(2cqw, 22px); - } - `, - indoc` - .foo { - ${prop}: 22px; - ${prop}: max(2cqw, 22px); - } - `, - { - safari: 14 << 16, - }, - ); - - prefix_test( - ` - .foo { - ${prop}: 22px; - ${prop}: max(2cqw, 22px); - } - `, - indoc` - .foo { - ${prop}: max(2cqw, 22px); - } - `, - { - safari: 16 << 16, - }, - ); - } - }); - - describe("padding", () => { cssTest( ` .foo { - padding-left: 10px; - padding-right: 10px; - padding-top: 20px; - padding-bottom: 20px; + border-top: 1px solid black; + border-bottom: 1px solid black; + border-left: 2px solid black; + border-right: 2px solid black; } `, - indoc` + ` .foo { - padding: 20px 10px; + border: 1px solid #000; + border-width: 1px 2px; } `, ); @@ -2226,13 +464,16 @@ describe("css tests", () => { cssTest( ` .foo { - padding-block-start: 15px; - padding-block-end: 15px; + border-top: 1px solid black; + border-bottom: 1px solid black; + border-left: 2px solid black; + border-right: 1px solid black; } `, - indoc` + ` .foo { - padding-block: 15px; + border: 1px solid #000; + border-left-width: 2px; } `, ); @@ -2240,124 +481,173 @@ describe("css tests", () => { cssTest( ` .foo { - padding-left: 10px; - padding-right: 10px; - padding-inline-start: 15px; - padding-inline-end: 15px; - padding-top: 20px; - padding-bottom: 20px; + border-top: 1px solid black; + border-bottom: 1px solid black; + border-left: 1px solid red; + border-right: 1px solid red; } `, - indoc` + ` .foo { - padding-left: 10px; - padding-right: 10px; - padding-inline: 15px; - padding-top: 20px; - padding-bottom: 20px; + border: 1px solid #000; + border-color: #000 red; } `, ); - prefix_test( + cssTest( ` .foo { - padding-inline-start: 2px; + border-block-start: 1px solid black; + border-block-end: 1px solid black; + border-inline-start: 1px solid red; + border-inline-end: 1px solid red; } `, - indoc` - .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - padding-left: 2px; + ` + .foo { + border: 1px solid #000; + border-inline-color: red; } + `, + ); - .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - padding-left: 2px; + cssTest( + ` + .foo { + border-block-start: 1px solid black; + border-block-end: 1px solid black; + border-inline-start: 2px solid black; + border-inline-end: 2px solid black; } - - .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - padding-right: 2px; + `, + ` + .foo { + border: 1px solid #000; + border-inline-width: 2px; } + `, + ); - .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - padding-right: 2px; + cssTest( + ` + .foo { + border-block-start: 1px solid black; + border-block-end: 1px solid black; + border-inline-start: 2px solid red; + border-inline-end: 2px solid red; + } + `, + ` + .foo { + border: 1px solid #000; + border-inline: 2px solid red; } `, - { - safari: 8 << 16, - }, ); - prefix_test( + cssTest( ` .foo { - padding-inline-start: 2px; - padding-inline-end: 4px; + border-block-start: 1px solid black; + border-block-end: 1px solid black; + border-inline-start: 2px solid red; + border-inline-end: 3px solid red; } `, - indoc` - .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - padding-left: 2px; - padding-right: 4px; + ` + .foo { + border: 1px solid #000; + border-inline-start: 2px solid red; + border-inline-end: 3px solid red; } + `, + ); - .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - padding-left: 2px; - padding-right: 4px; + cssTest( + ` + .foo { + border-block-start: 2px solid black; + border-block-end: 1px solid black; + border-inline-start: 2px solid red; + border-inline-end: 2px solid red; } - - .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - padding-left: 4px; - padding-right: 2px; + `, + ` + .foo { + border: 2px solid red; + border-block-start-color: #000; + border-block-end: 1px solid #000; } + `, + ); - .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - padding-left: 4px; - padding-right: 2px; + cssTest( + ` + .foo { + border-block-start: 2px solid red; + border-block-end: 1px solid red; + border-inline-start: 2px solid red; + border-inline-end: 2px solid red; + } + `, + ` + .foo { + border: 2px solid red; + border-block-end-width: 1px; } `, - { - safari: 8 << 16, - }, ); - prefix_test( + cssTest( ` .foo { - padding-inline-start: var(--padding); + border-block-start: 2px solid red; + border-block-end: 2px solid red; + border-inline-start: 2px solid red; + border-inline-end: 1px solid red; } `, - indoc` - .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - padding-left: var(--padding); + ` + .foo { + border: 2px solid red; + border-inline-end-width: 1px; } + `, + ); - .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { - padding-left: var(--padding); + cssTest( + ` + .foo { + border: 1px solid currentColor; } - - .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - padding-right: var(--padding); + `, + ` + .foo { + border: 1px solid; } + `, + ); - .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { - padding-right: var(--padding); + minify_test( + ` + .foo { + border: 1px solid currentColor; } `, - { - safari: 8 << 16, - }, + ".foo{border:1px solid}", ); prefix_test( ` .foo { - padding-inline: 2px; + border-block: 2px solid red; } `, - indoc` + ` .foo { - padding-left: 2px; - padding-right: 2px; + border-top: 2px solid red; + border-bottom: 2px solid red; } `, { @@ -2368,12 +658,12 @@ describe("css tests", () => { prefix_test( ` .foo { - padding-block-start: 2px; + border-block-start: 2px solid red; } `, - indoc` + ` .foo { - padding-top: 2px; + border-top: 2px solid red; } `, { @@ -2384,12 +674,12 @@ describe("css tests", () => { prefix_test( ` .foo { - padding-block-end: 2px; + border-block-end: 2px solid red; } `, - indoc` + ` .foo { - padding-bottom: 2px; + border-bottom: 2px solid red; } `, { @@ -2400,15 +690,13 @@ describe("css tests", () => { prefix_test( ` .foo { - padding-top: 1px; - padding-left: 2px; - padding-bottom: 3px; - padding-right: 4px; + border-inline: 2px solid red; } `, - indoc` + ` .foo { - padding: 1px 4px 3px 2px; + border-left: 2px solid red; + border-right: 2px solid red; } `, { @@ -2419,14 +707,13 @@ describe("css tests", () => { prefix_test( ` .foo { - padding-inline-start: 2px; - padding-inline-end: 2px; + border-block-width: 2px; } `, - indoc` + ` .foo { - padding-inline-start: 2px; - padding-inline-end: 2px; + border-block-start-width: 2px; + border-block-end-width: 2px; } `, { @@ -2437,55 +724,96 @@ describe("css tests", () => { prefix_test( ` .foo { - padding-inline-start: 2px; - padding-inline-end: 2px; + border-block-width: 2px; } `, - indoc` + ` .foo { - padding-inline: 2px; + border-block-width: 2px; } `, { safari: 15 << 16, }, ); - }); - describe("scroll-paddding", () => { prefix_test( ` .foo { - scroll-padding-inline: 2px; + border-inline-start: 2px solid red; } `, - indoc` - .foo { - scroll-padding-inline: 2px; + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid red; } - `, - { - safari: 8 << 16, - }, - ); - }); - describe("size", () => { + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid red; + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-right: 2px solid red; + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-right: 2px solid red; + } + `, + { + safari: 8 << 16, + }, + ); + prefix_test( ` .foo { - block-size: 25px; - inline-size: 25px; - min-block-size: 25px; - min-inline-size: 25px; + border-inline-start-width: 2px; } `, - indoc` + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left-width: 2px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left-width: 2px; + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-right-width: 2px; + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-right-width: 2px; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` .foo { - height: 25px; - min-height: 25px; - width: 25px; - min-width: 25px; + border-inline-end: 2px solid red; + } + `, + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: 2px solid red; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: 2px solid red; + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: 2px solid red; + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: 2px solid red; } `, { @@ -2496,40 +824,104 @@ describe("css tests", () => { prefix_test( ` .foo { - block-size: 25px; - min-block-size: 25px; - inline-size: 25px; - min-inline-size: 25px; + border-inline-start: 2px solid red; + border-inline-end: 5px solid green; } `, - indoc` + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid red; + border-right: 5px solid green; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid red; + border-right: 5px solid green; + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: 5px solid green; + border-right: 2px solid red; + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: 5px solid green; + border-right: 2px solid red; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` .foo { - block-size: 25px; - min-block-size: 25px; - inline-size: 25px; - min-inline-size: 25px; + border-inline-start: 2px solid red; + border-inline-end: 5px solid green; + } + + .bar { + border-inline-start: 1px dotted gray; + border-inline-end: 1px solid black; + } + `, + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid red; + border-right: 5px solid green; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid red; + border-right: 5px solid green; + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: 5px solid green; + border-right: 2px solid red; + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: 5px solid green; + border-right: 2px solid red; + } + + .bar:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 1px dotted gray; + border-right: 1px solid #000; + } + + .bar:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 1px dotted gray; + border-right: 1px solid #000; + } + + .bar:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: 1px solid #000; + border-right: 1px dotted gray; + } + + .bar:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: 1px solid #000; + border-right: 1px dotted gray; } `, { - safari: 14 << 16, + safari: 8 << 16, }, ); prefix_test( ` .foo { - block-size: var(--size); - min-block-size: var(--size); - inline-size: var(--size); - min-inline-size: var(--size); + border-inline-width: 2px; } `, - indoc` + ` .foo { - height: var(--size); - min-height: var(--size); - width: var(--size); - min-width: var(--size); + border-left-width: 2px; + border-right-width: 2px; } `, { @@ -2537,379 +929,452 @@ describe("css tests", () => { }, ); - const sizeProps = [ - ["width", "width"], - ["height", "height"], - ["block-size", "height"], - ["inline-size", "width"], - ["min-width", "min-width"], - ["min-height", "min-height"], - ["min-block-size", "min-height"], - ["min-inline-size", "min-width"], - ["max-width", "max-width"], - ["max-height", "max-height"], - ["max-block-size", "max-height"], - ["max-inline-size", "max-width"], - ]; + prefix_test( + ` + .foo { + border-inline-width: 2px; + } + `, + ` + .foo { + border-left-width: 2px; + border-right-width: 2px; + } + `, + { + safari: 8 << 16, + }, + ); - for (const [inProp, outProp] of sizeProps) { + prefix_test( + ` + .foo { + border-inline-style: solid; + } + `, + ` + .foo { + border-left-style: solid; + border-right-style: solid; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + border-inline-color: red; + } + `, + ` + .foo { + border-left-color: red; + border-right-color: red; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + border-inline-end: var(--test); + } + `, + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: var(--test); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: var(--test); + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: var(--test); + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: var(--test); + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + border-inline-start: var(--start); + border-inline-end: var(--end); + } + `, + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: var(--start); + border-right: var(--end); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: var(--start); + border-right: var(--end); + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-right: var(--start); + border-left: var(--end); + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-right: var(--start); + border-left: var(--end); + } + `, + { + safari: 8 << 16, + }, + ); + + for (const prop of [ + "border-inline-start-color", + "border-inline-end-color", + "border-block-start-color", + "border-block-end-color", + "border-top-color", + "border-bottom-color", + "border-left-color", + "border-right-color", + "border-color", + "border-block-color", + "border-inline-color", + ]) { prefix_test( ` .foo { - ${inProp}: stretch; + ${prop}: lab(40% 56.6 39); } `, - indoc` + ` .foo { - ${outProp}: -webkit-fill-available; - ${outProp}: -moz-available; - ${outProp}: stretch; + ${prop}: #b32323; + ${prop}: lab(40% 56.6 39); } `, { - safari: 8 << 16, - firefox: 4 << 16, + chrome: 90 << 16, }, ); + } + for (const prop of [ + "border", + "border-inline", + "border-block", + "border-left", + "border-right", + "border-top", + "border-bottom", + "border-block-start", + "border-block-end", + "border-inline-start", + "border-inline-end", + ]) { prefix_test( ` .foo { - ${inProp}: -webkit-fill-available; + ${prop}: 2px solid lab(40% 56.6 39); } `, - indoc` + ` .foo { - ${outProp}: -webkit-fill-available; + ${prop}: 2px solid #b32323; + ${prop}: 2px solid lab(40% 56.6 39); } `, { - safari: 8 << 16, - firefox: 4 << 16, + chrome: 90 << 16, }, ); + } + for (const prop of [ + "border", + "border-inline", + "border-block", + "border-left", + "border-right", + "border-top", + "border-bottom", + "border-block-start", + "border-block-end", + "border-inline-start", + "border-inline-end", + ]) { prefix_test( ` .foo { - ${inProp}: 100vw; - ${inProp}: -webkit-fill-available; + ${prop}: var(--border-width) solid lab(40% 56.6 39); } `, - indoc` + ` .foo { - ${outProp}: 100vw; - ${outProp}: -webkit-fill-available; + ${prop}: var(--border-width) solid #b32323; + } + + @supports (color: lab(0% 0 0)) { + .foo { + ${prop}: var(--border-width) solid lab(40% 56.6 39); + } } `, { - safari: 8 << 16, - firefox: 4 << 16, + chrome: 90 << 16, }, ); + } - prefix_test( - ` - .foo { - ${inProp}: fit-content; - } - `, - indoc` - .foo { - ${outProp}: -webkit-fit-content; - ${outProp}: -moz-fit-content; - ${outProp}: fit-content; - } - `, - { - safari: 8 << 16, - firefox: 4 << 16, - }, - ); - - prefix_test( - ` - .foo { - ${inProp}: fit-content(50%); - } - `, - indoc` - .foo { - ${outProp}: fit-content(50%); - } - `, - { - safari: 8 << 16, - firefox: 4 << 16, - }, - ); - - prefix_test( - ` - .foo { - ${inProp}: min-content; - } - `, - indoc` - .foo { - ${outProp}: -webkit-min-content; - ${outProp}: -moz-min-content; - ${outProp}: min-content; - } - `, - { - safari: 8 << 16, - firefox: 4 << 16, - }, - ); - - prefix_test( - ` - .foo { - ${inProp}: max-content; - } - `, - indoc` - .foo { - ${outProp}: -webkit-max-content; - ${outProp}: -moz-max-content; - ${outProp}: max-content; - } - `, - { - safari: 8 << 16, - firefox: 4 << 16, - }, - ); - - prefix_test( - ` - .foo { - ${inProp}: 100%; - ${inProp}: max-content; - } - `, - indoc` - .foo { - ${outProp}: 100%; - ${outProp}: max-content; - } - `, - { - safari: 8 << 16, - firefox: 4 << 16, - }, - ); - - prefix_test( - ` - .foo { - ${inProp}: var(--fallback); - ${inProp}: max-content; - } - `, - indoc` - .foo { - ${outProp}: var(--fallback); - ${outProp}: max-content; - } - `, - { - safari: 8 << 16, - firefox: 4 << 16, - }, - ); - } - - minifyTest(".foo { aspect-ratio: auto }", ".foo{aspect-ratio:auto}"); - minifyTest(".foo { aspect-ratio: 2 / 3 }", ".foo{aspect-ratio:2/3}"); - minifyTest(".foo { aspect-ratio: auto 2 / 3 }", ".foo{aspect-ratio:auto 2/3}"); - minifyTest(".foo { aspect-ratio: 2 / 3 auto }", ".foo{aspect-ratio:auto 2/3}"); - }); - - describe("background", () => { - cssTest( + prefix_test( ` .foo { - background: url(img.png); - background-position-x: 20px; - background-position-y: 10px; - background-size: 50px 100px; - background-repeat: repeat no-repeat; + border-inline-start-color: lab(40% 56.6 39); } `, - indoc` - .foo { - background: url("img.png") 20px 10px / 50px 100px repeat-x; + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left-color: #b32323; + border-left-color: lab(40% 56.6 39); } - `, - ); - cssTest( - ` - .foo { - background-color: red; - background-position: 0% 0%; - background-size: auto; - background-repeat: repeat; - background-clip: border-box; - background-origin: padding-box; - background-attachment: scroll; - background-image: none + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left-color: #b32323; + border-left-color: lab(40% 56.6 39); } - `, - indoc` - .foo { - background: red; + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-right-color: #b32323; + border-right-color: lab(40% 56.6 39); + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-right-color: #b32323; + border-right-color: lab(40% 56.6 39); } `, + { + safari: 8 << 16, + }, ); - cssTest( + prefix_test( ` .foo { - background-color: gray; - background-position: 40% 50%; - background-size: 10em auto; - background-repeat: round; - background-clip: border-box; - background-origin: border-box; - background-attachment: fixed; - background-image: url('chess.png'); + border-inline-end-color: lab(40% 56.6 39); } `, - indoc` - .foo { - background: gray url("chess.png") 40% / 10em round fixed border-box; + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right-color: #b32323; + border-right-color: lab(40% 56.6 39); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right-color: #b32323; + border-right-color: lab(40% 56.6 39); + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left-color: #b32323; + border-left-color: lab(40% 56.6 39); + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left-color: #b32323; + border-left-color: lab(40% 56.6 39); } `, + { + safari: 8 << 16, + }, ); - cssTest( + prefix_test( ` .foo { - background: url(img.png), url(test.jpg) gray; - background-position-x: right 20px, 10px; - background-position-y: top 20px, 15px; - background-size: 50px 50px, auto; - background-repeat: repeat no-repeat, no-repeat; + border-inline-start-color: lab(40% 56.6 39); + border-inline-end-color: lch(50.998% 135.363 338); } `, - indoc` - .foo { - background: url("img.png") right 20px top 20px / 50px 50px repeat-x, gray url("test.jpg") 10px 15px no-repeat; + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left-color: #b32323; + border-left-color: lab(40% 56.6 39); + border-right-color: #ee00be; + border-right-color: lch(50.998% 135.363 338); } - `, - ); - minify_test( - ` - .foo { - background-position: center center; + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left-color: #b32323; + border-left-color: lab(40% 56.6 39); + border-right-color: #ee00be; + border-right-color: lch(50.998% 135.363 338); + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left-color: #ee00be; + border-left-color: lch(50.998% 135.363 338); + border-right-color: #b32323; + border-right-color: lab(40% 56.6 39); + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left-color: #ee00be; + border-left-color: lch(50.998% 135.363 338); + border-right-color: #b32323; + border-right-color: lab(40% 56.6 39); } `, - indoc`.foo{background-position:50%}`, + { + safari: 8 << 16, + }, ); - cssTest( + prefix_test( ` .foo { - background: url(img.png) gray; - background-clip: content-box; - -webkit-background-clip: text; + border-inline-start-color: lab(40% 56.6 39); + border-inline-end-color: lch(50.998% 135.363 338); } `, - indoc` - .foo { - background: gray url("img.png") padding-box content-box; - -webkit-background-clip: text; + ` + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left-color: #b32323; + border-left-color: color(display-p3 .6433075 .19245467 .1677117); + border-left-color: lab(40% 56.6 39); + border-right-color: #ee00be; + border-right-color: color(display-p3 .9729615 -.36207756 .80420625); + border-right-color: lch(50.998% 135.363 338); + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left-color: #ee00be; + border-left-color: color(display-p3 .9729615 -.36207756 .80420625); + border-left-color: lch(50.998% 135.363 338); + border-right-color: #b32323; + border-right-color: color(display-p3 .6433075 .19245467 .1677117); + border-right-color: lab(40% 56.6 39); } `, + { + chrome: 8 << 16, + safari: 14 << 16, + }, ); - cssTest( + prefix_test( ` .foo { - background: url(img.png) gray; - -webkit-background-clip: text; - background-clip: content-box; + border-inline-start: 2px solid lab(40% 56.6 39); } `, - indoc` - .foo { - background: gray url("img.png"); - -webkit-background-clip: text; - background-clip: content-box; + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid #b32323; + border-left: 2px solid lab(40% 56.6 39); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-left: 2px solid #b32323; + border-left: 2px solid lab(40% 56.6 39); + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-right: 2px solid #b32323; + border-right: 2px solid lab(40% 56.6 39); + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-right: 2px solid #b32323; + border-right: 2px solid lab(40% 56.6 39); } `, + { + safari: 8 << 16, + }, ); - cssTest( + prefix_test( ` .foo { - background: url(img.png) gray; - background-position: var(--pos); + border-inline-end: 2px solid lab(40% 56.6 39); } `, - indoc` - .foo { - background: gray url("img.png"); - background-position: var(--pos); + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: 2px solid #b32323; + border-right: 2px solid lab(40% 56.6 39); } - `, - ); - minify_test(".foo { background-position: bottom left }", ".foo{background-position:0 100%}"); - minify_test(".foo { background-position: left 10px center }", ".foo{background-position:10px 50%}"); - minify_test(".foo { background-position: right 10px center }", ".foo{background-position:right 10px center}"); - minify_test(".foo { background-position: right 10px top 20px }", ".foo{background-position:right 10px top 20px}"); - minify_test(".foo { background-position: left 10px top 20px }", ".foo{background-position:10px 20px}"); - minify_test( - ".foo { background-position: left 10px bottom 20px }", - ".foo{background-position:left 10px bottom 20px}", - ); - minify_test(".foo { background-position: left 10px top }", ".foo{background-position:10px 0}"); - minify_test(".foo { background-position: bottom right }", ".foo{background-position:100% 100%}"); + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: 2px solid #b32323; + border-right: 2px solid lab(40% 56.6 39); + } - minify_test( - ".foo { background: url('img-sprite.png') no-repeat bottom right }", - ".foo{background:url(img-sprite.png) 100% 100% no-repeat}", - ); - minify_test(".foo { background: transparent }", ".foo{background:0 0}"); + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: 2px solid #b32323; + border-left: 2px solid lab(40% 56.6 39); + } - minify_test( - ".foo { background: url(\"data:image/svg+xml,%3Csvg width='168' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\") }", - ".foo{background:url(\"data:image/svg+xml,%3Csvg width='168' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\")}", + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: 2px solid #b32323; + border-left: 2px solid lab(40% 56.6 39); + } + `, + { + safari: 8 << 16, + }, ); - cssTest( + prefix_test( ` .foo { - background: url(img.png); - background-clip: text; + border-inline-end: var(--border-width) solid lab(40% 56.6 39); } `, - indoc` - .foo { - background: url("img.png") text; + ` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: var(--border-width) solid #b32323; } - `, - ); - prefix_test( - ` - .foo { - background: url(img.png); - background-clip: text; + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: var(--border-width) solid #b32323; + } + + @supports (color: lab(0% 0 0)) { + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + border-right: var(--border-width) solid lab(40% 56.6 39); } - `, - indoc` - .foo { - background: url("img.png"); - -webkit-background-clip: text; - background-clip: text; + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: var(--border-width) solid #b32323; + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: var(--border-width) solid #b32323; + } + + @supports (color: lab(0% 0 0)) { + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + border-left: var(--border-width) solid lab(40% 56.6 39); } - `, + } + `, { safari: 8 << 16, }, @@ -2917,416 +1382,1622 @@ describe("css tests", () => { prefix_test( ` - .foo { - background: url(img.png); - background-clip: text; - } - `, - indoc` - .foo { - background: url("img.png") text; - } - `, - { - safari: 14 << 16, - }, - ); - - prefix_test( + .foo { + border-inline-start: 2px solid red; + border-inline-end: 2px solid red; + } + `, ` - .foo { - background: url(img.png) text; - } - `, - indoc` - .foo { - background: url("img.png"); - -webkit-background-clip: text; - background-clip: text; - } - `, + .foo { + border-inline-start: 2px solid red; + border-inline-end: 2px solid red; + } + `, { - chrome: 45 << 16, + safari: 13 << 16, }, ); prefix_test( ` - .foo { - background: url(img.png); - -webkit-background-clip: text; - } - `, - indoc` - .foo { - background: url("img.png"); - -webkit-background-clip: text; - } - `, + .foo { + border-inline-start: 2px solid red; + border-inline-end: 2px solid red; + } + `, + ` + .foo { + border-inline: 2px solid red; + } + `, { - chrome: 45 << 16, + safari: 15 << 16, }, ); prefix_test( ` - .foo { - background: url(img.png); - background-clip: text; - } - `, - indoc` - .foo { - background: url("img.png"); - -webkit-background-clip: text; - background-clip: text; - } - `, + .foo { + border-width: 22px; + border-width: max(2cqw, 22px); + } + `, + ` + .foo { + border-width: 22px; + border-width: max(2cqw, 22px); + } + `, { safari: 14 << 16, - chrome: 95 << 16, }, ); prefix_test( ` - .foo { - background-image: url(img.png); - background-clip: text; - } - `, - indoc` - .foo { - background-image: url("img.png"); - -webkit-background-clip: text; - background-clip: text; - } - `, + .foo { + border-width: 22px; + border-width: max(2cqw, 22px); + } + `, + ` + .foo { + border-width: max(2cqw, 22px); + } + `, { - safari: 8 << 16, + safari: 16 << 16, }, ); - prefix_test( ` - .foo { - -webkit-background-clip: text; - background-clip: text; - } - `, - indoc` - .foo { - -webkit-background-clip: text; - background-clip: text; - } - `, + .foo { + border-color: #4263eb; + border-color: color(display-p3 0 .5 1); + } + `, + ` + .foo { + border-color: #4263eb; + border-color: color(display-p3 0 .5 1); + } + `, { - chrome: 45 << 16, + chrome: 99 << 16, }, ); - prefix_test( ` - .foo { - background-image: url(img.png); - background-clip: text; - } - `, - indoc` - .foo { - background-image: url("img.png"); - background-clip: text; - } - `, + .foo { + border-color: #4263eb; + border-color: color(display-p3 0 .5 1); + } + `, + ` + .foo { + border-color: color(display-p3 0 .5 1); + } + `, { - safari: 14 << 16, + safari: 16 << 16, }, ); - - minify_test(".foo { background: none center }", ".foo{background:50%}"); - minify_test(".foo { background: none }", ".foo{background:0 0}"); - prefix_test( ` - .foo { - background: lab(51.5117% 43.3777 -29.0443); - } - `, - indoc` - .foo { - background: #af5cae; - background: lab(51.5117% 43.3777 -29.0443); - } - `, + .foo { + border: 1px solid #4263eb; + border-color: color(display-p3 0 .5 1); + } + `, + ` + .foo { + border: 1px solid #4263eb; + border-color: color(display-p3 0 .5 1); + } + `, { - chrome: 95 << 16, - safari: 15 << 16, + chrome: 99 << 16, }, ); - prefix_test( ` - .foo { - background: lab(51.5117% 43.3777 -29.0443) url(foo.png); - } - `, - indoc` - .foo { - background: #af5cae url("foo.png"); - background: lab(51.5117% 43.3777 -29.0443) url("foo.png"); - } - `, + .foo { + border: 1px solid #4263eb; + border-color: color(display-p3 0 .5 1); + } + `, + ` + .foo { + border: 1px solid color(display-p3 0 .5 1); + } + `, { - chrome: 95 << 16, - safari: 15 << 16, + safari: 16 << 16, }, ); - prefix_test( ` - .foo { - background: lab(51.5117% 43.3777 -29.0443) linear-gradient(lab(52.2319% 40.1449 59.9171), lab(47.7776% -34.2947 -7.65904)); - } - `, - indoc` - .foo { - background: #af5cae linear-gradient(#c65d07, #00807c); - background: lab(51.5117% 43.3777 -29.0443) linear-gradient(lab(52.2319% 40.1449 59.9171), lab(47.7776% -34.2947 -7.65904)); - } - `, + .foo { + border-color: var(--fallback); + border-color: color(display-p3 0 .5 1); + } + `, + ` + .foo { + border-color: var(--fallback); + border-color: color(display-p3 0 .5 1); + } + `, { - chrome: 95 << 16, - safari: 15 << 16, + chrome: 99 << 16, }, ); + }); + describe("margin", () => { cssTest( - ".foo { background: calc(var(--v) / 0.3)", + ` + .foo { + margin-left: 10px; + margin-right: 10px; + margin-top: 20px; + margin-bottom: 20px; + }`, indoc` - .foo { - background: calc(var(--v) / .3); - } - `, + .foo { + margin: 20px 10px; + } +`, ); - prefix_test( + cssTest( ` - .foo { - background-color: #4263eb; - background-color: color(display-p3 0 .5 1); - } - `, + .foo { + margin-block-start: 15px; + margin-block-end: 15px; + }`, indoc` - .foo { - background-color: #4263eb; - background-color: color(display-p3 0 .5 1); - } - `, - { - chrome: 99 << 16, - }, + .foo { + margin-block: 15px; + } +`, ); - prefix_test( + + cssTest( ` - .foo { - background-color: #4263eb; - background-color: color(display-p3 0 .5 1); - } - `, + .foo { + margin-left: 10px; + margin-right: 10px; + margin-inline-start: 15px; + margin-inline-end: 15px; + margin-top: 20px; + margin-bottom: 20px; + }`, indoc` - .foo { - background-color: color(display-p3 0 .5 1); - } - `, - { - safari: 16 << 16, - }, + .foo { + margin-left: 10px; + margin-right: 10px; + margin-inline: 15px; + margin-top: 20px; + margin-bottom: 20px; + } +`, ); - prefix_test( + + cssTest( ` - .foo { - background-image: linear-gradient(red, green); - background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, + .foo { + margin: 10px; + margin-top: 20px; + }`, indoc` - .foo { - background-image: linear-gradient(red, green); - background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, - { - chrome: 99 << 16, - }, + .foo { + margin: 20px 10px 10px; + } +`, ); - prefix_test( + + cssTest( ` - .foo { - background-image: linear-gradient(red, green); - background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, + .foo { + margin: 10px; + margin-top: var(--top); + }`, indoc` - .foo { - background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, - { - safari: 16 << 16, - }, + .foo { + margin: 10px; + margin-top: var(--top); + } +`, ); + prefix_test( ` - .foo { - background: #4263eb; - background: color(display-p3 0 .5 1); - } - `, + .foo { + margin-inline-start: 2px; + } + `, indoc` - .foo { - background: #4263eb; - background: color(display-p3 0 .5 1); - } - `, + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + margin-left: 2px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + margin-left: 2px; + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + margin-right: 2px; + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + margin-right: 2px; + } + `, { - chrome: 99 << 16, + safari: 8 << 16, }, ); + prefix_test( ` - .foo { - background: #4263eb; - background: color(display-p3 0 .5 1); - } - `, + .foo { + margin-inline-start: 2px; + margin-inline-end: 4px; + } + `, indoc` - .foo { - background: color(display-p3 0 .5 1); - } - `, + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + margin-left: 2px; + margin-right: 4px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + margin-left: 2px; + margin-right: 4px; + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + margin-left: 4px; + margin-right: 2px; + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + margin-left: 4px; + margin-right: 2px; + } + `, { - safari: 16 << 16, + safari: 8 << 16, }, ); + prefix_test( ` - .foo { - background: linear-gradient(red, green); - background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, + .foo { + margin-inline: 2px; + } + `, indoc` - .foo { - background: linear-gradient(red, green); - background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, + .foo { + margin-left: 2px; + margin-right: 2px; + } + `, { - chrome: 99 << 16, + safari: 8 << 16, }, ); + prefix_test( ` - .foo { - background: red; - background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, + .foo { + margin-block-start: 2px; + } + `, indoc` - .foo { - background: red; - background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, + .foo { + margin-top: 2px; + } + `, { - chrome: 99 << 16, + safari: 8 << 16, }, ); + prefix_test( ` - .foo { - background: linear-gradient(red, green); - background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, + .foo { + margin-block-end: 2px; + } + `, indoc` - .foo { - background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, + .foo { + margin-bottom: 2px; + } + `, { - safari: 16 << 16, + safari: 8 << 16, }, ); + prefix_test( ` - .foo { - background: var(--fallback); - background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, + .foo { + margin-inline-start: 2px; + margin-inline-end: 2px; + } + `, indoc` - .foo { - background: var(--fallback); - background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); - } - `, + .foo { + margin-inline-start: 2px; + margin-inline-end: 2px; + } + `, { - chrome: 99 << 16, + safari: 13 << 16, + }, + ); + + prefix_test( + ` + .foo { + margin-inline: 2px; + } + `, + indoc` + .foo { + margin-inline-start: 2px; + margin-inline-end: 2px; + } + `, + { + safari: 13 << 16, + }, + ); + + prefix_test( + ` + .foo { + margin-inline-start: 2px; + margin-inline-end: 2px; + } + `, + indoc` + .foo { + margin-inline: 2px; + } + `, + { + safari: 15 << 16, }, ); + prefix_test( ` + .foo { + margin-inline: 2px; + } + `, + indoc` + .foo { + margin-inline: 2px; + } + `, + { + safari: 15 << 16, + }, + ); + }); + + describe("length", () => { + const properties = [ + "margin-right", + "margin", + "padding-right", + "padding", + "width", + "height", + "min-height", + "max-height", + // "line-height", + // "border-radius", + ]; + + for (const prop of properties) { + prefix_test( + ` .foo { - background: red url(foo.png); - background: lch(50% 132 50) url(foo.png); + ${prop}: 22px; + ${prop}: max(4%, 22px); } `, - indoc` + indoc` .foo { - background: red url("foo.png"); - background: lch(50% 132 50) url("foo.png"); + ${prop}: 22px; + ${prop}: max(4%, 22px); } `, - { - chrome: 99 << 16, - }, - ); + { + safari: 10 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${prop}: 22px; + ${prop}: max(4%, 22px); + } + `, + indoc` + .foo { + ${prop}: max(4%, 22px); + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${prop}: 22px; + ${prop}: max(2cqw, 22px); + } + `, + indoc` + .foo { + ${prop}: 22px; + ${prop}: max(2cqw, 22px); + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${prop}: 22px; + ${prop}: max(2cqw, 22px); + } + `, + indoc` + .foo { + ${prop}: max(2cqw, 22px); + } + `, + { + safari: 16 << 16, + }, + ); + } }); - describe("linear-gradient", () => { - minifyTest(".foo { background: linear-gradient(yellow, blue) }", ".foo{background:linear-gradient(#ff0,#00f)}"); - minifyTest( - ".foo { background: linear-gradient(to bottom, yellow, blue); }", - ".foo{background:linear-gradient(#ff0,#00f)}", - ); - minifyTest( - ".foo { background: linear-gradient(180deg, yellow, blue); }", - ".foo{background:linear-gradient(#ff0,#00f)}", - ); - minifyTest( - ".foo { background: linear-gradient(0.5turn, yellow, blue); }", - ".foo{background:linear-gradient(#ff0,#00f)}", - ); - minifyTest( - ".foo { background: linear-gradient(yellow 10%, blue 20%) }", - ".foo{background:linear-gradient(#ff0 10%,#00f 20%)}", - ); - minifyTest( - ".foo { background: linear-gradient(to top, blue, yellow); }", - ".foo{background:linear-gradient(#ff0,#00f)}", + describe("padding", () => { + cssTest( + ` + .foo { + padding-left: 10px; + padding-right: 10px; + padding-top: 20px; + padding-bottom: 20px; + } + `, + indoc` + .foo { + padding: 20px 10px; + } + `, ); - minifyTest( - ".foo { background: linear-gradient(to top, blue 10%, yellow 20%); }", - ".foo{background:linear-gradient(#ff0 80%,#00f 90%)}", + + cssTest( + ` + .foo { + padding-block-start: 15px; + padding-block-end: 15px; + } + `, + indoc` + .foo { + padding-block: 15px; + } + `, ); - minifyTest( - ".foo { background: linear-gradient(to top, blue 10px, yellow 20px); }", - ".foo{background:linear-gradient(0deg,#00f 10px,#ff0 20px)}", + + cssTest( + ` + .foo { + padding-left: 10px; + padding-right: 10px; + padding-inline-start: 15px; + padding-inline-end: 15px; + padding-top: 20px; + padding-bottom: 20px; + } + `, + indoc` + .foo { + padding-left: 10px; + padding-right: 10px; + padding-inline: 15px; + padding-top: 20px; + padding-bottom: 20px; + } + `, ); - minifyTest( - ".foo { background: linear-gradient(135deg, yellow, blue); }", - ".foo{background:linear-gradient(135deg,#ff0,#00f)}", + + prefix_test( + ` + .foo { + padding-inline-start: 2px; + } + `, + indoc` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + padding-left: 2px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + padding-left: 2px; + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + padding-right: 2px; + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + padding-right: 2px; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + padding-inline-start: 2px; + padding-inline-end: 4px; + } + `, + indoc` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + padding-left: 2px; + padding-right: 4px; + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + padding-left: 2px; + padding-right: 4px; + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + padding-left: 4px; + padding-right: 2px; + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + padding-left: 4px; + padding-right: 2px; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + padding-inline-start: var(--padding); + } + `, + indoc` + .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + padding-left: var(--padding); + } + + .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) { + padding-left: var(--padding); + } + + .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + padding-right: var(--padding); + } + + .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) { + padding-right: var(--padding); + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + padding-inline: 2px; + } + `, + indoc` + .foo { + padding-left: 2px; + padding-right: 2px; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + padding-block-start: 2px; + } + `, + indoc` + .foo { + padding-top: 2px; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + padding-block-end: 2px; + } + `, + indoc` + .foo { + padding-bottom: 2px; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + padding-top: 1px; + padding-left: 2px; + padding-bottom: 3px; + padding-right: 4px; + } + `, + indoc` + .foo { + padding: 1px 4px 3px 2px; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + padding-inline-start: 2px; + padding-inline-end: 2px; + } + `, + indoc` + .foo { + padding-inline-start: 2px; + padding-inline-end: 2px; + } + `, + { + safari: 13 << 16, + }, + ); + + prefix_test( + ` + .foo { + padding-inline-start: 2px; + padding-inline-end: 2px; + } + `, + indoc` + .foo { + padding-inline: 2px; + } + `, + { + safari: 15 << 16, + }, + ); + }); + + describe("scroll-paddding", () => { + prefix_test( + ` + .foo { + scroll-padding-inline: 2px; + } + `, + indoc` + .foo { + scroll-padding-inline: 2px; + } + `, + { + safari: 8 << 16, + }, + ); + }); + + describe("size", () => { + prefix_test( + ` + .foo { + block-size: 25px; + inline-size: 25px; + min-block-size: 25px; + min-inline-size: 25px; + } + `, + indoc` + .foo { + height: 25px; + min-height: 25px; + width: 25px; + min-width: 25px; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + block-size: 25px; + min-block-size: 25px; + inline-size: 25px; + min-inline-size: 25px; + } + `, + indoc` + .foo { + block-size: 25px; + min-block-size: 25px; + inline-size: 25px; + min-inline-size: 25px; + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + ` + .foo { + block-size: var(--size); + min-block-size: var(--size); + inline-size: var(--size); + min-inline-size: var(--size); + } + `, + indoc` + .foo { + height: var(--size); + min-height: var(--size); + width: var(--size); + min-width: var(--size); + } + `, + { + safari: 8 << 16, + }, + ); + + const sizeProps = [ + ["width", "width"], + ["height", "height"], + ["block-size", "height"], + ["inline-size", "width"], + ["min-width", "min-width"], + ["min-height", "min-height"], + ["min-block-size", "min-height"], + ["min-inline-size", "min-width"], + ["max-width", "max-width"], + ["max-height", "max-height"], + ["max-block-size", "max-height"], + ["max-inline-size", "max-width"], + ]; + + for (const [inProp, outProp] of sizeProps) { + prefix_test( + ` + .foo { + ${inProp}: stretch; + } + `, + indoc` + .foo { + ${outProp}: -webkit-fill-available; + ${outProp}: -moz-available; + ${outProp}: stretch; + } + `, + { + safari: 8 << 16, + firefox: 4 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${inProp}: -webkit-fill-available; + } + `, + indoc` + .foo { + ${outProp}: -webkit-fill-available; + } + `, + { + safari: 8 << 16, + firefox: 4 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${inProp}: 100vw; + ${inProp}: -webkit-fill-available; + } + `, + indoc` + .foo { + ${outProp}: 100vw; + ${outProp}: -webkit-fill-available; + } + `, + { + safari: 8 << 16, + firefox: 4 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${inProp}: fit-content; + } + `, + indoc` + .foo { + ${outProp}: -webkit-fit-content; + ${outProp}: -moz-fit-content; + ${outProp}: fit-content; + } + `, + { + safari: 8 << 16, + firefox: 4 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${inProp}: fit-content(50%); + } + `, + indoc` + .foo { + ${outProp}: fit-content(50%); + } + `, + { + safari: 8 << 16, + firefox: 4 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${inProp}: min-content; + } + `, + indoc` + .foo { + ${outProp}: -webkit-min-content; + ${outProp}: -moz-min-content; + ${outProp}: min-content; + } + `, + { + safari: 8 << 16, + firefox: 4 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${inProp}: max-content; + } + `, + indoc` + .foo { + ${outProp}: -webkit-max-content; + ${outProp}: -moz-max-content; + ${outProp}: max-content; + } + `, + { + safari: 8 << 16, + firefox: 4 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${inProp}: 100%; + ${inProp}: max-content; + } + `, + indoc` + .foo { + ${outProp}: 100%; + ${outProp}: max-content; + } + `, + { + safari: 8 << 16, + firefox: 4 << 16, + }, + ); + + prefix_test( + ` + .foo { + ${inProp}: var(--fallback); + ${inProp}: max-content; + } + `, + indoc` + .foo { + ${outProp}: var(--fallback); + ${outProp}: max-content; + } + `, + { + safari: 8 << 16, + firefox: 4 << 16, + }, + ); + } + + minifyTest(".foo { aspect-ratio: auto }", ".foo{aspect-ratio:auto}"); + minifyTest(".foo { aspect-ratio: 2 / 3 }", ".foo{aspect-ratio:2/3}"); + minifyTest(".foo { aspect-ratio: auto 2 / 3 }", ".foo{aspect-ratio:auto 2/3}"); + minifyTest(".foo { aspect-ratio: 2 / 3 auto }", ".foo{aspect-ratio:auto 2/3}"); + }); + + describe("background", () => { + cssTest( + ` + .foo { + background: url(img.png); + background-position-x: 20px; + background-position-y: 10px; + background-size: 50px 100px; + background-repeat: repeat no-repeat; + } + `, + indoc` + .foo { + background: url("img.png") 20px 10px / 50px 100px repeat-x; + } + `, + ); + + cssTest( + ` + .foo { + background-color: red; + background-position: 0% 0%; + background-size: auto; + background-repeat: repeat; + background-clip: border-box; + background-origin: padding-box; + background-attachment: scroll; + background-image: none + } + `, + indoc` + .foo { + background: red; + } + `, + ); + + cssTest( + ` + .foo { + background-color: gray; + background-position: 40% 50%; + background-size: 10em auto; + background-repeat: round; + background-clip: border-box; + background-origin: border-box; + background-attachment: fixed; + background-image: url('chess.png'); + } + `, + indoc` + .foo { + background: gray url("chess.png") 40% / 10em round fixed border-box; + } + `, + ); + + cssTest( + ` + .foo { + background: url(img.png), url(test.jpg) gray; + background-position-x: right 20px, 10px; + background-position-y: top 20px, 15px; + background-size: 50px 50px, auto; + background-repeat: repeat no-repeat, no-repeat; + } + `, + indoc` + .foo { + background: url("img.png") right 20px top 20px / 50px 50px repeat-x, gray url("test.jpg") 10px 15px no-repeat; + } + `, + ); + + minify_test( + ` + .foo { + background-position: center center; + } + `, + indoc`.foo{background-position:50%}`, + ); + + cssTest( + ` + .foo { + background: url(img.png) gray; + background-clip: content-box; + -webkit-background-clip: text; + } + `, + indoc` + .foo { + background: gray url("img.png") padding-box content-box; + -webkit-background-clip: text; + } + `, + ); + + cssTest( + ` + .foo { + background: url(img.png) gray; + -webkit-background-clip: text; + background-clip: content-box; + } + `, + indoc` + .foo { + background: gray url("img.png"); + -webkit-background-clip: text; + background-clip: content-box; + } + `, + ); + + cssTest( + ` + .foo { + background: url(img.png) gray; + background-position: var(--pos); + } + `, + indoc` + .foo { + background: gray url("img.png"); + background-position: var(--pos); + } + `, + ); + + minify_test(".foo { background-position: bottom left }", ".foo{background-position:0 100%}"); + minify_test(".foo { background-position: left 10px center }", ".foo{background-position:10px 50%}"); + minify_test(".foo { background-position: right 10px center }", ".foo{background-position:right 10px center}"); + minify_test(".foo { background-position: right 10px top 20px }", ".foo{background-position:right 10px top 20px}"); + minify_test(".foo { background-position: left 10px top 20px }", ".foo{background-position:10px 20px}"); + minify_test( + ".foo { background-position: left 10px bottom 20px }", + ".foo{background-position:left 10px bottom 20px}", + ); + minify_test(".foo { background-position: left 10px top }", ".foo{background-position:10px 0}"); + minify_test(".foo { background-position: bottom right }", ".foo{background-position:100% 100%}"); + + minify_test( + ".foo { background: url('img-sprite.png') no-repeat bottom right }", + ".foo{background:url(img-sprite.png) 100% 100% no-repeat}", + ); + minify_test(".foo { background: transparent }", ".foo{background:0 0}"); + + minify_test( + ".foo { background: url(\"data:image/svg+xml,%3Csvg width='168' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\") }", + ".foo{background:url(\"data:image/svg+xml,%3Csvg width='168' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\")}", + ); + + cssTest( + ` + .foo { + background: url(img.png); + background-clip: text; + } + `, + indoc` + .foo { + background: url("img.png") text; + } + `, + ); + + prefix_test( + ` + .foo { + background: url(img.png); + background-clip: text; + } + `, + indoc` + .foo { + background: url("img.png"); + -webkit-background-clip: text; + background-clip: text; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + background: url(img.png); + background-clip: text; + } + `, + indoc` + .foo { + background: url("img.png") text; + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + ` + .foo { + background: url(img.png) text; + } + `, + indoc` + .foo { + background: url("img.png"); + -webkit-background-clip: text; + background-clip: text; + } + `, + { + chrome: 45 << 16, + }, + ); + + prefix_test( + ` + .foo { + background: url(img.png); + -webkit-background-clip: text; + } + `, + indoc` + .foo { + background: url("img.png"); + -webkit-background-clip: text; + } + `, + { + chrome: 45 << 16, + }, + ); + + prefix_test( + ` + .foo { + background: url(img.png); + background-clip: text; + } + `, + indoc` + .foo { + background: url("img.png"); + -webkit-background-clip: text; + background-clip: text; + } + `, + { + safari: 14 << 16, + chrome: 95 << 16, + }, + ); + + prefix_test( + ` + .foo { + background-image: url(img.png); + background-clip: text; + } + `, + indoc` + .foo { + background-image: url("img.png"); + -webkit-background-clip: text; + background-clip: text; + } + `, + { + safari: 8 << 16, + }, + ); + + prefix_test( + ` + .foo { + -webkit-background-clip: text; + background-clip: text; + } + `, + indoc` + .foo { + -webkit-background-clip: text; + background-clip: text; + } + `, + { + chrome: 45 << 16, + }, + ); + + prefix_test( + ` + .foo { + background-image: url(img.png); + background-clip: text; + } + `, + indoc` + .foo { + background-image: url("img.png"); + background-clip: text; + } + `, + { + safari: 14 << 16, + }, + ); + + minify_test(".foo { background: none center }", ".foo{background:50%}"); + minify_test(".foo { background: none }", ".foo{background:0 0}"); + + prefix_test( + ` + .foo { + background: lab(51.5117% 43.3777 -29.0443); + } + `, + indoc` + .foo { + background: #af5cae; + background: lab(51.5117% 43.3777 -29.0443); + } + `, + { + chrome: 95 << 16, + safari: 15 << 16, + }, + ); + + prefix_test( + ` + .foo { + background: lab(51.5117% 43.3777 -29.0443) url(foo.png); + } + `, + indoc` + .foo { + background: #af5cae url("foo.png"); + background: lab(51.5117% 43.3777 -29.0443) url("foo.png"); + } + `, + { + chrome: 95 << 16, + safari: 15 << 16, + }, + ); + + prefix_test( + ` + .foo { + background: lab(51.5117% 43.3777 -29.0443) linear-gradient(lab(52.2319% 40.1449 59.9171), lab(47.7776% -34.2947 -7.65904)); + } + `, + indoc` + .foo { + background: #af5cae linear-gradient(#c65d07, #00807c); + background: lab(51.5117% 43.3777 -29.0443) linear-gradient(lab(52.2319% 40.1449 59.9171), lab(47.7776% -34.2947 -7.65904)); + } + `, + { + chrome: 95 << 16, + safari: 15 << 16, + }, + ); + + cssTest( + ".foo { background: calc(var(--v) / 0.3)", + indoc` + .foo { + background: calc(var(--v) / .3); + } + `, + ); + + prefix_test( + ` + .foo { + background-color: #4263eb; + background-color: color(display-p3 0 .5 1); + } + `, + indoc` + .foo { + background-color: #4263eb; + background-color: color(display-p3 0 .5 1); + } + `, + { + chrome: 99 << 16, + }, + ); + prefix_test( + ` + .foo { + background-color: #4263eb; + background-color: color(display-p3 0 .5 1); + } + `, + indoc` + .foo { + background-color: color(display-p3 0 .5 1); + } + `, + { + safari: 16 << 16, + }, + ); + prefix_test( + ` + .foo { + background-image: linear-gradient(red, green); + background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + indoc` + .foo { + background-image: linear-gradient(red, green); + background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + { + chrome: 99 << 16, + }, + ); + prefix_test( + ` + .foo { + background-image: linear-gradient(red, green); + background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + indoc` + .foo { + background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + { + safari: 16 << 16, + }, + ); + prefix_test( + ` + .foo { + background: #4263eb; + background: color(display-p3 0 .5 1); + } + `, + indoc` + .foo { + background: #4263eb; + background: color(display-p3 0 .5 1); + } + `, + { + chrome: 99 << 16, + }, + ); + prefix_test( + ` + .foo { + background: #4263eb; + background: color(display-p3 0 .5 1); + } + `, + indoc` + .foo { + background: color(display-p3 0 .5 1); + } + `, + { + safari: 16 << 16, + }, + ); + prefix_test( + ` + .foo { + background: linear-gradient(red, green); + background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + indoc` + .foo { + background: linear-gradient(red, green); + background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + { + chrome: 99 << 16, + }, + ); + prefix_test( + ` + .foo { + background: red; + background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + indoc` + .foo { + background: red; + background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + { + chrome: 99 << 16, + }, + ); + prefix_test( + ` + .foo { + background: linear-gradient(red, green); + background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + indoc` + .foo { + background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + { + safari: 16 << 16, + }, + ); + prefix_test( + ` + .foo { + background: var(--fallback); + background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + indoc` + .foo { + background: var(--fallback); + background: linear-gradient(lch(50% 132 50), lch(50% 130 150)); + } + `, + { + chrome: 99 << 16, + }, + ); + prefix_test( + ` + .foo { + background: red url(foo.png); + background: lch(50% 132 50) url(foo.png); + } + `, + indoc` + .foo { + background: red url("foo.png"); + background: lch(50% 132 50) url("foo.png"); + } + `, + { + chrome: 99 << 16, + }, + ); + }); + + describe("linear-gradient", () => { + minifyTest(".foo { background: linear-gradient(yellow, blue) }", ".foo{background:linear-gradient(#ff0,#00f)}"); + minifyTest( + ".foo { background: linear-gradient(to bottom, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(180deg, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(0.5turn, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow 10%, blue 20%) }", + ".foo{background:linear-gradient(#ff0 10%,#00f 20%)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top, blue, yellow); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top, blue 10%, yellow 20%); }", + ".foo{background:linear-gradient(#ff0 80%,#00f 90%)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top, blue 10px, yellow 20px); }", + ".foo{background:linear-gradient(0deg,#00f 10px,#ff0 20px)}", + ); + minifyTest( + ".foo { background: linear-gradient(135deg, yellow, blue); }", + ".foo{background:linear-gradient(135deg,#ff0,#00f)}", ); minifyTest( ".foo { background: linear-gradient(yellow, blue 20%, #0f0); }", @@ -3494,71 +3165,467 @@ describe("css tests", () => { ".foo { background: radial-gradient(farthest-side ellipse, yellow, blue) }", ".foo{background:radial-gradient(farthest-side,#ff0,#00f)}", ); - minifyTest( - ".foo { background: -webkit-radial-gradient(yellow, blue) }", - ".foo{background:-webkit-radial-gradient(#ff0,#00f)}", + minifyTest( + ".foo { background: -webkit-radial-gradient(yellow, blue) }", + ".foo{background:-webkit-radial-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -moz-radial-gradient(yellow, blue) }", + ".foo{background:-moz-radial-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -o-radial-gradient(yellow, blue) }", + ".foo{background:-o-radial-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:-webkit-repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -moz-repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:-moz-repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -o-repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:-o-repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-gradient(radial, center center, 0, center center, 100, from(blue), to(yellow)) }", + ".foo{background:-webkit-gradient(radial,50% 50%,0,50% 50%,100,from(#00f),to(#ff0))}", + ); + minifyTest(".foo { background: conic-gradient(#f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}"); + minifyTest( + ".foo { background: conic-gradient(at 50% 50%, #f06, gold) }", + ".foo{background:conic-gradient(#f06,gold)}", + ); + minifyTest( + ".foo { background: conic-gradient(from 0deg, #f06, gold) }", + ".foo{background:conic-gradient(#f06,gold)}", + ); + minifyTest(".foo { background: conic-gradient(from 0, #f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}"); + minifyTest( + ".foo { background: conic-gradient(from 0deg at center, #f06, gold) }", + ".foo{background:conic-gradient(#f06,gold)}", + ); + minifyTest( + ".foo { background: conic-gradient(white -50%, black 150%) }", + ".foo{background:conic-gradient(#fff -50%,#000 150%)}", + ); + minifyTest( + ".foo { background: conic-gradient(white -180deg, black 540deg) }", + ".foo{background:conic-gradient(#fff -180deg,#000 540deg)}", + ); + minifyTest( + ".foo { background: conic-gradient(from 45deg, white, black, white) }", + ".foo{background:conic-gradient(from 45deg,#fff,#000,#fff)}", + ); + minifyTest( + ".foo { background: repeating-conic-gradient(from 45deg, white, black, white) }", + ".foo{background:repeating-conic-gradient(from 45deg,#fff,#000,#fff)}", + ); + minifyTest( + ".foo { background: repeating-conic-gradient(black 0deg 25%, white 0deg 50%) }", + ".foo{background:repeating-conic-gradient(#000 0deg 25%,#fff 0deg 50%)}", + ); + }); + + describe("font", () => { + cssTest( + ` + .foo { + font-family: "Helvetica", "Times New Roman", sans-serif; + font-size: 12px; + font-weight: bold; + font-style: italic; + font-stretch: expanded; + font-variant-caps: small-caps; + line-height: 1.2em; + } + `, + indoc` + .foo { + font: italic small-caps bold expanded 12px / 1.2em Helvetica, Times New Roman, sans-serif; + } +`, + ); + + minifyTest( + ` + .foo { + font-family: "Helvetica", "Times New Roman", sans-serif; + font-size: 12px; + font-weight: bold; + font-style: italic; + font-stretch: expanded; + font-variant-caps: small-caps; + line-height: 1.2em; + } + `, + indoc`.foo{font:italic small-caps 700 125% 12px/1.2em Helvetica,Times New Roman,sans-serif}`, + ); + + cssTest( + ` + .foo { + font: 12px "Helvetica", "Times New Roman", sans-serif; + line-height: 1.2em; + } + `, + indoc` + .foo { + font: 12px / 1.2em Helvetica, Times New Roman, sans-serif; + } +`, + ); + + cssTest( + ` + .foo { + font: 12px "Helvetica", "Times New Roman", sans-serif; + line-height: var(--lh); + } + `, + indoc` + .foo { + font: 12px Helvetica, Times New Roman, sans-serif; + line-height: var(--lh); + } +`, + ); + + minifyTest( + ` + .foo { + font-family: "Helvetica", "Times New Roman", sans-serif; + font-size: 12px; + font-stretch: expanded; + } + `, + indoc`.foo{font-family:Helvetica,Times New Roman,sans-serif;font-size:12px;font-stretch:125%}`, + ); + + cssTest( + ` + .foo { + font-family: "Helvetica", "Times New Roman", sans-serif; + font-size: 12px; + font-weight: bold; + font-style: italic; + font-stretch: expanded; + font-variant-caps: all-small-caps; + line-height: 1.2em; + } + `, + indoc` + .foo { + font: italic bold expanded 12px / 1.2em Helvetica, Times New Roman, sans-serif; + font-variant-caps: all-small-caps; + } +`, + ); + + minifyTest(".foo { font: normal normal 600 9px/normal Charcoal; }", ".foo{font:600 9px Charcoal}"); + minifyTest(".foo { font: normal normal 500 medium/normal Charcoal; }", ".foo{font:500 medium Charcoal}"); + minifyTest(".foo { font: normal normal 400 medium Charcoal; }", ".foo{font:400 medium Charcoal}"); + minifyTest(".foo { font: normal normal 500 medium/10px Charcoal; }", ".foo{font:500 medium/10px Charcoal}"); + minifyTest(".foo { font-family: 'sans-serif'; }", '.foo{font-family:"sans-serif"}'); + minifyTest(".foo { font-family: sans-serif; }", ".foo{font-family:sans-serif}"); + minifyTest(".foo { font-family: 'default'; }", '.foo{font-family:"default"}'); + minifyTest(".foo { font-family: default; }", ".foo{font-family:default}"); + minifyTest(".foo { font-family: 'inherit'; }", '.foo{font-family:"inherit"}'); + minifyTest(".foo { font-family: inherit; }", ".foo{font-family:inherit}"); + minifyTest(".foo { font-family: inherit test; }", ".foo{font-family:inherit test}"); + minifyTest(".foo { font-family: 'inherit test'; }", ".foo{font-family:inherit test}"); + minifyTest(".foo { font-family: revert; }", ".foo{font-family:revert}"); + minifyTest(".foo { font-family: 'revert'; }", '.foo{font-family:"revert"}'); + minifyTest(".foo { font-family: revert-layer; }", ".foo{font-family:revert-layer}"); + minifyTest(".foo { font-family: revert-layer, serif; }", ".foo{font-family:revert-layer,serif}"); + minifyTest(".foo { font-family: 'revert', sans-serif; }", '.foo{font-family:"revert",sans-serif}'); + minifyTest(".foo { font-family: 'revert', foo, sans-serif; }", '.foo{font-family:"revert",foo,sans-serif}'); + minifyTest(".foo { font-family: ''; }", '.foo{font-family:""}'); + + // font-family in @font-face + minifyTest("@font-face { font-family: 'revert'; }", '@font-face{font-family:"revert"}'); + minifyTest("@font-face { font-family: 'revert-layer'; }", '@font-face{font-family:"revert-layer"}'); + + prefix_test( + ` + .foo { + font-family: Helvetica, system-ui, sans-serif; + } + `, + indoc` + .foo { + font-family: Helvetica, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif; + } +`, + { + safari: 8 << 16, + }, ); - minifyTest( - ".foo { background: -moz-radial-gradient(yellow, blue) }", - ".foo{background:-moz-radial-gradient(#ff0,#00f)}", + + prefix_test( + ` + .foo { + font: 100%/1.5 Helvetica, system-ui, sans-serif; + } + `, + indoc` + .foo { + font: 100% / 1.5 Helvetica, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif; + } +`, + { + safari: 8 << 16, + }, ); - minifyTest( - ".foo { background: -o-radial-gradient(yellow, blue) }", - ".foo{background:-o-radial-gradient(#ff0,#00f)}", + + prefix_test( + ` + .foo { + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + } + `, + indoc` + .foo { + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + } +`, + { + firefox: 91 << 16, + }, ); - minifyTest( - ".foo { background: repeating-radial-gradient(circle 20px, yellow, blue) }", - ".foo{background:repeating-radial-gradient(20px,#ff0,#00f)}", + + prefix_test( + ` + .foo { + font-size: 22px; + font-size: max(2cqw, 22px); + } + `, + indoc` + .foo { + font-size: 22px; + font-size: max(2cqw, 22px); + } +`, + { + safari: 14 << 16, + }, ); - minifyTest( - ".foo { background: -webkit-repeating-radial-gradient(circle 20px, yellow, blue) }", - ".foo{background:-webkit-repeating-radial-gradient(20px,#ff0,#00f)}", + + prefix_test( + ` + .foo { + font-size: 22px; + font-size: max(2cqw, 22px); + } + `, + indoc` + .foo { + font-size: max(2cqw, 22px); + } +`, + { + safari: 16 << 16, + }, ); - minifyTest( - ".foo { background: -moz-repeating-radial-gradient(circle 20px, yellow, blue) }", - ".foo{background:-moz-repeating-radial-gradient(20px,#ff0,#00f)}", + + prefix_test( + ` + .foo { + font-size: 22px; + font-size: xxx-large; + } + `, + indoc` + .foo { + font-size: 22px; + font-size: xxx-large; + } +`, + { + chrome: 70 << 16, + }, ); - minifyTest( - ".foo { background: -o-repeating-radial-gradient(circle 20px, yellow, blue) }", - ".foo{background:-o-repeating-radial-gradient(20px,#ff0,#00f)}", + + prefix_test( + ` + .foo { + font-size: 22px; + font-size: xxx-large; + } + `, + indoc` + .foo { + font-size: xxx-large; + } +`, + { + chrome: 80 << 16, + }, ); - minifyTest( - ".foo { background: -webkit-gradient(radial, center center, 0, center center, 100, from(blue), to(yellow)) }", - ".foo{background:-webkit-gradient(radial,50% 50%,0,50% 50%,100,from(#00f),to(#ff0))}", + + prefix_test( + ` + .foo { + font-weight: 700; + font-weight: 789; + } + `, + indoc` + .foo { + font-weight: 700; + font-weight: 789; + } +`, + { + chrome: 60 << 16, + }, ); - minifyTest(".foo { background: conic-gradient(#f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}"); - minifyTest( - ".foo { background: conic-gradient(at 50% 50%, #f06, gold) }", - ".foo{background:conic-gradient(#f06,gold)}", + + prefix_test( + ` + .foo { + font-weight: 700; + font-weight: 789; + } + `, + indoc` + .foo { + font-weight: 789; + } +`, + { + chrome: 80 << 16, + }, ); - minifyTest( - ".foo { background: conic-gradient(from 0deg, #f06, gold) }", - ".foo{background:conic-gradient(#f06,gold)}", + + prefix_test( + ` + .foo { + font-family: Helvetica; + font-family: system-ui; + } + `, + indoc` + .foo { + font-family: Helvetica; + font-family: system-ui; + } +`, + { + chrome: 50 << 16, + }, ); - minifyTest(".foo { background: conic-gradient(from 0, #f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}"); - minifyTest( - ".foo { background: conic-gradient(from 0deg at center, #f06, gold) }", - ".foo{background:conic-gradient(#f06,gold)}", + + prefix_test( + ` + .foo { + font-family: Helvetica; + font-family: system-ui; + } + `, + indoc` + .foo { + font-family: system-ui; + } +`, + { + chrome: 80 << 16, + }, ); - minifyTest( - ".foo { background: conic-gradient(white -50%, black 150%) }", - ".foo{background:conic-gradient(#fff -50%,#000 150%)}", + + prefix_test( + ` + .foo { + font-style: oblique; + font-style: oblique 40deg; + } + `, + indoc` + .foo { + font-style: oblique; + font-style: oblique 40deg; + } +`, + { + firefox: 50 << 16, + }, ); - minifyTest( - ".foo { background: conic-gradient(white -180deg, black 540deg) }", - ".foo{background:conic-gradient(#fff -180deg,#000 540deg)}", + + prefix_test( + ` + .foo { + font-style: oblique; + font-style: oblique 40deg; + } + `, + indoc` + .foo { + font-style: oblique 40deg; + } +`, + { + firefox: 80 << 16, + }, ); - minifyTest( - ".foo { background: conic-gradient(from 45deg, white, black, white) }", - ".foo{background:conic-gradient(from 45deg,#fff,#000,#fff)}", + + prefix_test( + ` + .foo { + font: 22px Helvetica; + font: xxx-large system-ui; + } + `, + indoc` + .foo { + font: 22px Helvetica; + font: xxx-large system-ui; + } +`, + { + chrome: 70 << 16, + }, ); - minifyTest( - ".foo { background: repeating-conic-gradient(from 45deg, white, black, white) }", - ".foo{background:repeating-conic-gradient(from 45deg,#fff,#000,#fff)}", + + prefix_test( + ` + .foo { + font: 22px Helvetica; + font: xxx-large system-ui; + } + `, + indoc` + .foo { + font: xxx-large system-ui; + } +`, + { + chrome: 80 << 16, + }, ); - minifyTest( - ".foo { background: repeating-conic-gradient(black 0deg 25%, white 0deg 50%) }", - ".foo{background:repeating-conic-gradient(#000 0deg 25%,#fff 0deg 50%)}", + + prefix_test( + ` + .foo { + font: var(--fallback); + font: xxx-large system-ui; + } + `, + indoc` + .foo { + font: var(--fallback); + font: xxx-large system-ui; + } +`, + { + chrome: 50 << 16, + }, ); }); }); diff --git a/test/js/bun/http/bun-serve-html.test.ts b/test/js/bun/http/bun-serve-html.test.ts index 87bc43095bda20..63ad42dae96a7a 100644 --- a/test/js/bun/http/bun-serve-html.test.ts +++ b/test/js/bun/http/bun-serve-html.test.ts @@ -211,19 +211,19 @@ console.log("How...dashing?"); "/* styles.css */ .container { text-align: center; - font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; + font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif; } button { - font-size: 1.25rem; - border-radius: .25rem; - border: 2px solid #000; cursor: pointer; transition: all .2s; background: #fff; + border: 2px solid #000; + border-radius: .25rem; padding: .5rem 1rem; + font-size: 1.25rem; } button:hover { @@ -234,7 +234,9 @@ button:hover { `); } - expect(await (await fetch(`http://${hostname}:${port}/a-different-url`)).text()).toMatchInlineSnapshot(`"Hello World"`); + expect(await (await fetch(`http://${hostname}:${port}/a-different-url`)).text()).toMatchInlineSnapshot( + `"Hello World"`, + ); subprocess.kill(); });