From 3af1636bef8059123b0a36f44fd45214f99015ae Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:17:50 -0800 Subject: [PATCH 1/3] fix a lot of tests --- src/bundler/bundle_v2.zig | 66 +-- src/css/css_internals.zig | 99 +++- src/css/css_parser.zig | 32 +- src/css/error.zig | 31 ++ src/css/properties/flex.zig | 391 +++++++++++++++ src/css/rules/namespace.zig | 2 +- src/css/selectors/parser.zig | 72 ++- src/css/selectors/selector.zig | 11 +- src/js/internal-for-testing.ts | 4 + src/transpiler.zig | 11 +- test/js/bun/css/css.test.ts | 843 +++++++++++++++++++++++++++++---- 11 files changed, 1397 insertions(+), 165 deletions(-) diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index d396da4483cf..0d8d99528d15 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -9683,18 +9683,21 @@ pub const LinkerContext = struct { .minify = c.options.minify_whitespace, .targets = bun.css.Targets.forBundlerTarget(c.options.target), }; - _ = css.toCssWithWriter( + _ = switch (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(), - }, - }; + )) { + .result => {}, + .err => { + return CompileResult{ + .css = .{ + .result = .{ .err = error.PrintError }, + .source_index = Index.invalid.get(), + }, + }; + }, }; return CompileResult{ .css = .{ @@ -9710,18 +9713,21 @@ pub const LinkerContext = struct { .minify = c.options.minify_whitespace, .targets = bun.css.Targets.forBundlerTarget(c.options.target), }; - _ = css.toCssWithWriter( + _ = switch (css.toCssWithWriter( worker.allocator, &buffer_writer, printer_options, &import_records, - ) catch |e| { - return CompileResult{ - .css = .{ - .result = .{ .err = e }, - .source_index = Index.invalid.get(), - }, - }; + )) { + .result => {}, + .err => { + return CompileResult{ + .css = .{ + .result = .{ .err = error.PrintError }, + .source_index = Index.invalid.get(), + }, + }; + }, }; return CompileResult{ .css = .{ @@ -9737,18 +9743,21 @@ pub const LinkerContext = struct { // TODO: make this more configurable .minify = c.options.minify_whitespace or c.options.minify_syntax or c.options.minify_identifiers, }; - _ = css.toCssWithWriter( + _ = switch (css.toCssWithWriter( worker.allocator, &buffer_writer, printer_options, &c.graph.ast.items(.import_records)[idx.get()], - ) catch |e| { - return CompileResult{ - .css = .{ - .result = .{ .err = e }, - .source_index = idx.get(), - }, - }; + )) { + .result => {}, + .err => { + return CompileResult{ + .css = .{ + .result = .{ .err = error.PrintError }, + .source_index = idx.get(), + }, + }; + }, }; return CompileResult{ .css = .{ @@ -9929,9 +9938,12 @@ pub const LinkerContext = struct { .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; + const print_result = switch (ast_import.toCss(allocator, printer_options, &entry.condition_import_records)) { + .result => |v| v, + .err => |e| { + c.log.addErrorFmt(null, Loc.Empty, c.allocator, "Error generating CSS for import: {}", .{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"))); } diff --git a/src/css/css_internals.zig b/src/css/css_internals.zig index 4c0d86dc45cd..68003930e79b 100644 --- a/src/css/css_internals.zig +++ b/src/css/css_internals.zig @@ -17,19 +17,42 @@ const TestKind = enum { prefix, }; +const TestCategory = enum { + /// arg is browsers + normal, + /// arg is parser options + parser_options, +}; + +pub fn minifyErrorTestWithOptions(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return testingImpl(globalThis, callframe, .minify, .parser_options); +} + pub fn minifyTestWithOptions(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - return testingImpl(globalThis, callframe, .minify); + return testingImpl(globalThis, callframe, .minify, .parser_options); } pub fn prefixTestWithOptions(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - return testingImpl(globalThis, callframe, .prefix); + return testingImpl(globalThis, callframe, .prefix, .parser_options); } pub fn testWithOptions(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - return testingImpl(globalThis, callframe, .normal); + return testingImpl(globalThis, callframe, .normal, .parser_options); +} + +pub fn minifyTest(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return testingImpl(globalThis, callframe, .minify, .normal); } -pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, comptime test_kind: TestKind) bun.JSError!JSC.JSValue { +pub fn prefixTest(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return testingImpl(globalThis, callframe, .prefix, .normal); +} + +pub fn _test(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return testingImpl(globalThis, callframe, .normal, .normal); +} + +pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, comptime test_kind: TestKind, comptime test_category: TestCategory) bun.JSError!JSC.JSValue { var arena = arena_ orelse brk: { break :brk Arena.init() catch @panic("oopsie arena no good"); }; @@ -67,13 +90,24 @@ pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, c var browsers: ?bun.css.targets.Browsers = null; const parser_options = parser_options: { - const opts = bun.css.ParserOptions.default(alloc, &log); + var opts = bun.css.ParserOptions.default(alloc, &log); // if (test_kind == .prefix) break :parser_options opts; - if (browser_options_arg) |optargs| { - if (optargs.isObject()) { - browsers = try targetsFromJS(globalThis, optargs); - } + switch (test_category) { + .normal => { + if (browser_options_arg) |optargs| { + if (optargs.isObject()) { + browsers = try targetsFromJS(globalThis, optargs); + } + } + }, + .parser_options => { + if (browser_options_arg) |optargs| { + if (optargs.isObject()) { + try parserOptionsFromJS(globalThis, alloc, &opts, optargs); + } + } + }, } break :parser_options opts; @@ -92,7 +126,7 @@ pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, c minify_options.targets.browsers = browsers; _ = stylesheet.minify(alloc, minify_options).assert(); - const result = stylesheet.toCss(alloc, bun.css.PrinterOptions{ + const result = switch (stylesheet.toCss(alloc, bun.css.PrinterOptions{ .minify = switch (test_kind) { .minify => true, .normal => false, @@ -101,15 +135,17 @@ pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, c .targets = .{ .browsers = minify_options.targets.browsers, }, - }, &import_records) catch |e| { - bun.handleErrorReturnTrace(e, @errorReturnTrace()); - return .undefined; + }, &import_records)) { + .result => |result| result, + .err => |err| { + return err.toJSString(alloc, globalThis); + }, }; return bun.String.fromBytes(result.code).toJS(globalThis); }, .err => |err| { - if (log.hasAny()) { + if (log.hasErrors()) { return log.toJS(globalThis, bun.default_allocator, "parsing failed:"); } return globalThis.throw("parsing failed: {}", .{err.kind}); @@ -117,6 +153,41 @@ pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, c } } +fn parserOptionsFromJS(globalThis: *JSC.JSGlobalObject, allocator: Allocator, opts: *bun.css.ParserOptions, jsobj: JSValue) bun.JSError!void { + _ = allocator; // autofix + if (try jsobj.getTruthy(globalThis, "flags")) |val| { + if (val.isArray()) { + var iter = val.arrayIterator(globalThis); + while (iter.next()) |item| { + const bunstr = item.toBunString(globalThis); + defer bunstr.deref(); + const str = bunstr.toUTF8(bun.default_allocator); + defer str.deinit(); + if (std.mem.eql(u8, str.slice(), "DEEP_SELECTOR_COMBINATOR")) { + opts.flags.insert(bun.css.ParserFlags{ + .deep_selector_combinator = true, + }); + } else { + return globalThis.throw("invalid flag: {s}", .{str.slice()}); + } + } + } else { + return globalThis.throw("flags must be an array", .{}); + } + } + + // if (try jsobj.getTruthy(globalThis, "css_modules")) |val| { + // opts.css_modules = bun.css.css_modules.Config{ + + // }; + // if (val.isObject()) { + // if (try val.getTruthy(globalThis, "pure")) |pure_val| { + // opts.css_modules.pure = pure_val.toBoolean(); + // } + // } + // } +} + fn targetsFromJS(globalThis: *JSC.JSGlobalObject, jsobj: JSValue) bun.JSError!bun.css.targets.Browsers { var targets = bun.css.targets.Browsers{}; diff --git a/src/css/css_parser.zig b/src/css/css_parser.zig index 20b465579478..e1e20a705dbe 100644 --- a/src/css/css_parser.zig +++ b/src/css/css_parser.zig @@ -2849,10 +2849,23 @@ pub fn StyleSheet(comptime AtRule: type) type { return .{ .result = {} }; } - pub fn toCssWithWriter(this: *const @This(), allocator: Allocator, writer: anytype, options: css_printer.PrinterOptions, import_records: ?*const bun.BabyList(ImportRecord)) PrintErr!ToCssResultInternal { + pub fn toCssWithWriter(this: *const @This(), allocator: Allocator, writer: anytype, options: css_printer.PrinterOptions, import_records: ?*const bun.BabyList(ImportRecord)) PrintResult(ToCssResultInternal) { const W = @TypeOf(writer); - const project_root = options.project_root; + var printer = Printer(@TypeOf(writer)).new(allocator, std.ArrayList(u8).init(allocator), writer, options, import_records); + const result = this.toCssWithWriterImpl(allocator, W, &printer, options, import_records) catch { + bun.assert(printer.error_kind != null); + return .{ + .err = printer.error_kind.?, + }; + }; + + return .{ .result = result }; + } + + pub fn toCssWithWriterImpl(this: *const @This(), allocator: Allocator, comptime W: type, printer: *Printer(W), options: css_printer.PrinterOptions, import_records: ?*const bun.BabyList(ImportRecord)) PrintErr!ToCssResultInternal { + _ = import_records; // autofix + const project_root = options.project_root; // #[cfg(feature = "sourcemap")] // { @@ -2875,7 +2888,7 @@ pub fn StyleSheet(comptime AtRule: type) type { var references = CssModuleReferences{}; printer.css_module = CssModule.new(allocator, config, &this.sources, project_root, &references); - try this.rules.toCss(W, &printer); + try this.rules.toCss(W, printer); try printer.newline(); return ToCssResultInternal{ @@ -2889,7 +2902,7 @@ pub fn StyleSheet(comptime AtRule: type) type { .references = references, }; } else { - try this.rules.toCss(W, &printer); + try this.rules.toCss(W, printer); try printer.newline(); return ToCssResultInternal{ .dependencies = printer.dependencies, @@ -2900,18 +2913,21 @@ pub fn StyleSheet(comptime AtRule: type) type { } } - pub fn toCss(this: *const @This(), allocator: Allocator, options: css_printer.PrinterOptions, import_records: ?*const bun.BabyList(ImportRecord)) PrintErr!ToCssResult { + pub fn toCss(this: *const @This(), allocator: Allocator, options: css_printer.PrinterOptions, import_records: ?*const bun.BabyList(ImportRecord)) PrintResult(ToCssResult) { // TODO: this is not necessary // Make sure we always have capacity > 0: https://github.com/napi-rs/napi-rs/issues/1124. var dest = ArrayList(u8).initCapacity(allocator, 1) catch unreachable; const writer = dest.writer(allocator); - const result = try toCssWithWriter(this, allocator, writer, options, import_records); - return ToCssResult{ + const result = switch (toCssWithWriter(this, allocator, writer, options, import_records)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = ToCssResult{ .code = dest.items, .dependencies = result.dependencies, .exports = result.exports, .references = result.references, - }; + } }; } pub fn parse(allocator: Allocator, code: []const u8, options: ParserOptions, import_records: ?*bun.BabyList(ImportRecord)) Maybe(This, Err(ParserError)) { diff --git a/src/css/error.zig b/src/css/error.zig index d4338c62431a..67152e7cfd17 100644 --- a/src/css/error.zig +++ b/src/css/error.zig @@ -31,6 +31,26 @@ pub fn Err(comptime T: type) type { /// The location where the error occurred. loc: ?ErrorLocation, + pub fn fmt( + this: @This(), + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + if (@hasDecl(T, "fmt")) { + try writer.print("{}", .{this.kind}); + return; + } + @compileError("fmt not implemented for " ++ @typeName(T)); + } + + pub fn toJSString(this: @This(), allocator: Allocator, globalThis: *bun.JSC.JSGlobalObject) bun.JSC.JSValue { + var error_string = ArrayList(u8){}; + defer error_string.deinit(allocator); + error_string.writer(allocator).print("{}", .{this.kind}) catch unreachable; + return bun.String.fromBytes(error_string.items).toJS(globalThis); + } + pub fn fromParseError(err: ParseError(ParserError), filename: []const u8) Err(ParserError) { if (T != ParserError) { @compileError("Called .fromParseError() when T is not ParserError"); @@ -191,6 +211,17 @@ pub const PrinterErrorKind = union(enum) { /// The CSS modules pattern must end with `[local]` for use in CSS grid. invalid_css_modules_pattern_in_grid, no_import_records, + + pub fn format(this: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + return switch (this) { + .ambiguous_url_in_custom_property => |data| writer.print("Ambiguous relative URL '{s}' in custom property declaration", .{data.url}), + .fmt_error => writer.writeAll("Formatting error occurred"), + .invalid_composes_nesting => writer.writeAll("The 'composes' property cannot be used within nested rules"), + .invalid_composes_selector => writer.writeAll("The 'composes' property can only be used with a simple class selector"), + .invalid_css_modules_pattern_in_grid => writer.writeAll("CSS modules pattern must end with '[local]' when used in CSS grid"), + .no_import_records => writer.writeAll("No import records found"), + }; + } }; /// A parser error. diff --git a/src/css/properties/flex.zig b/src/css/properties/flex.zig index c94bfeb6375d..d9dcdfbc6a8b 100644 --- a/src/css/properties/flex.zig +++ b/src/css/properties/flex.zig @@ -29,6 +29,9 @@ const NumberOrPercentage = css.css_values.percentage.NumberOrPercentage; const CustomIdentList = css.css_values.ident.CustomIdentList; const Angle = css.css_values.angle.Angle; const Url = css.css_values.url.Url; +const CSSInteger = css.css_values.integer.CSSInteger; + +const VendorPrefix = css.VendorPrefix; /// A value for the [flex-direction](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#propdef-flex-direction) property. /// A value for the [flex-direction](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#propdef-flex-direction) property. @@ -393,3 +396,391 @@ pub const FlexLinePack = enum { pub usingnamespace css.DefineEnumProperty(@This()); }; + +// A handler for flex-related properties that manages both standard and legacy vendor prefixed values. +// pub const FlexHandler = struct { +// /// The flex-direction property value and vendor prefix +// direction: ?struct { FlexDirection, VendorPrefix } = null, +// /// The box-orient property value and vendor prefix (legacy) +// box_orient: ?struct { BoxOrient, VendorPrefix } = null, +// /// The box-direction property value and vendor prefix (legacy) +// box_direction: ?struct { BoxDirection, VendorPrefix } = null, +// /// The flex-wrap property value and vendor prefix +// wrap: ?struct { FlexWrap, VendorPrefix } = null, +// /// The box-lines property value and vendor prefix (legacy) +// box_lines: ?struct { BoxLines, VendorPrefix } = null, +// /// The flex-grow property value and vendor prefix +// grow: ?struct { CSSNumber, VendorPrefix } = null, +// /// The box-flex property value and vendor prefix (legacy) +// box_flex: ?struct { CSSNumber, VendorPrefix } = null, +// /// The flex-positive property value and vendor prefix (legacy) +// flex_positive: ?struct { CSSNumber, VendorPrefix } = null, +// /// The flex-shrink property value and vendor prefix +// shrink: ?struct { CSSNumber, VendorPrefix } = null, +// /// The flex-negative property value and vendor prefix (legacy) +// flex_negative: ?struct { CSSNumber, VendorPrefix } = null, +// /// The flex-basis property value and vendor prefix +// basis: ?struct { LengthPercentageOrAuto, VendorPrefix } = null, +// /// The preferred-size property value and vendor prefix (legacy) +// preferred_size: ?struct { LengthPercentageOrAuto, VendorPrefix } = null, +// /// The order property value and vendor prefix +// order: ?struct { CSSInteger, VendorPrefix } = null, +// /// The box-ordinal-group property value and vendor prefix (legacy) +// box_ordinal_group: ?struct { BoxOrdinalGroup, VendorPrefix } = null, +// /// The flex-order property value and vendor prefix (legacy) +// flex_order: ?struct { CSSInteger, VendorPrefix } = null, +// /// Whether any flex-related properties have been set +// has_any: bool = false, + +// pub fn handleProperty( +// this: *@This(), +// property: *const Property, +// dest: *css.DeclarationList, +// context: *css.PropertyHandlerContext, +// ) bool { +// const maybeFlush = struct { +// fn maybeFlush( +// self: *FlexHandler, +// comptime prop: []const u8, +// val: anytype, +// vp: *const 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)) |*field| { +// if (!std.meta.eql(field[0], val.*) and !field[1].contains(vp.*)) { +// self.flush(dest, context); +// } +// } +// } +// }.maybeFlush; + +// const propertyHelper = struct { +// fn propertyHelper( +// self: *FlexHandler, +// comptime prop: []const u8, +// val: anytype, +// vp: *const VendorPrefix, +// ) void { +// maybeFlush(self, prop, val, vp); + +// // Otherwise, update the value and add the prefix +// if (@field(self, prop)) |*field| { +// field[0] = val.deepClone(context.allocator); +// field[1].insert(vp.*); +// } else { +// @field(self, prop) = .{ +// val.deepClone(context.allocator), +// vp.*, +// }; +// self.has_any = true; +// } +// } +// }.propertyHelper; + +// switch (property.*) { +// .flex_direction => |*val| { +// if (context.targets.browsers != null) { +// this.box_direction = null; +// this.box_orient = null; +// } +// propertyHelper(this, "direction", &val[0], &val[1]); +// }, +// .box_orient => |*val| propertyHelper(this, "box_orient", &val[0], &val[1]), +// .box_direction => |*val| propertyHelper(this, "box_direction", &val[0], &val[1]), +// .flex_wrap => |*val| { +// if (context.targets.browsers != null) { +// this.box_lines = null; +// } +// propertyHelper(this, "wrap", &val[0], &val[1]); +// }, +// .box_lines => |*val| propertyHelper(this, "box_lines", &val[0], &val[1]), +// .flex_flow => |*val| { +// if (context.targets.browsers != null) { +// this.box_direction = null; +// this.box_orient = null; +// } +// propertyHelper(this, "direction", &val[0].direction, &val[1]); +// propertyHelper(this, "wrap", &val[0].wrap, &val[1]); +// }, +// .flex_grow => |*val| { +// if (context.targets.browsers != null) { +// this.box_flex = null; +// this.flex_positive = null; +// } +// propertyHelper(this, "grow", &val[0], &val[1]); +// }, +// .box_flex => |*val| propertyHelper(this, "box_flex", &val[0], &val[1]), +// .flex_positive => |*val| propertyHelper(this, "flex_positive", &val[0], &val[1]), +// .flex_shrink => |*val| { +// if (context.targets.browsers != null) { +// this.flex_negative = null; +// } +// propertyHelper(this, "shrink", &val[0], &val[1]); +// }, +// .flex_negative => |*val| propertyHelper(this, "flex_negative", &val[0], &val[1]), +// .flex_basis => |*val| { +// if (context.targets.browsers != null) { +// this.preferred_size = null; +// } +// propertyHelper(this, "basis", &val[0], &val[1]); +// }, +// .flex_preferred_size => |*val| propertyHelper(this, "preferred_size", &val[0], &val[1]), +// .flex => |*val| { +// if (context.targets.browsers != null) { +// this.box_flex = null; +// this.flex_positive = null; +// this.flex_negative = null; +// this.preferred_size = null; +// } +// maybeFlush(this, "grow", &val[0].grow, &val[1]); +// maybeFlush(this, "shrink", &val[0].shrink, &val[1]); +// maybeFlush(this, "basis", &val[0].basis, &val[1]); +// propertyHelper(this, "grow", &val[0].grow, &val[1]); +// propertyHelper(this, "shrink", &val[0].shrink, &val[1]); +// propertyHelper(this, "basis", &val[0].basis, &val[1]); +// }, +// .order => |*val| { +// if (context.targets.browsers != null) { +// this.box_ordinal_group = null; +// this.flex_order = null; +// } +// propertyHelper(this, "order", &val[0], &val[1]); +// }, +// .box_ordinal_group => |*val| propertyHelper(this, "box_ordinal_group", &val[0], &val[1]), +// .flex_order => |*val| propertyHelper(this, "flex_order", &val[0], &val[1]), +// .unparsed => |*val| { +// if (isFlexProperty(&val.property_id)) { +// this.flush(dest, context); +// dest.append(context.allocator, property.deepClone(context.allocator)) catch unreachable; +// } 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 direction = bun.take(&this.direction); +// var wrap = bun.take(&this.wrap); +// var grow = bun.take(&this.grow); +// var shrink = bun.take(&this.shrink); +// var basis = bun.take(&this.basis); +// const box_orient = bun.take(&this.box_orient); +// const box_direction = bun.take(&this.box_direction); +// const box_flex = bun.take(&this.box_flex); +// const box_ordinal_group = bun.take(&this.box_ordinal_group); +// const box_lines = bun.take(&this.box_lines); +// const flex_positive = bun.take(&this.flex_positive); +// const flex_negative = bun.take(&this.flex_negative); +// const preferred_size = bun.take(&this.preferred_size); +// const order = bun.take(&this.order); +// const flex_order = bun.take(&this.flex_order); + +// // Legacy properties. These are only set if the final standard properties were unset. +// if (box_orient) |val| { +// if (!val[1].isEmpty()) { +// dest.append(context.allocator, Property{ .box_orient = val }) catch unreachable; +// } +// } + +// if (box_direction) |val| { +// if (!val[1].isEmpty()) { +// dest.append(context.allocator, Property{ .box_direction = val }) catch unreachable; +// } +// } + +// if (box_ordinal_group) |val| { +// if (!val[1].isEmpty()) { +// dest.append(context.allocator, Property{ .box_ordinal_group = val }) catch unreachable; +// } +// } + +// if (box_flex) |val| { +// if (!val[1].isEmpty()) { +// dest.append(context.allocator, Property{ .box_flex = val }) catch unreachable; +// } +// } + +// if (box_lines) |val| { +// if (!val[1].isEmpty()) { +// dest.append(context.allocator, Property{ .box_lines = val }) catch unreachable; +// } +// } + +// if (flex_positive) |val| { +// if (!val[1].isEmpty()) { +// dest.append(context.allocator, Property{ .flex_positive = val }) catch unreachable; +// } +// } + +// if (flex_negative) |val| { +// if (!val[1].isEmpty()) { +// dest.append(context.allocator, Property{ .flex_negative = val }) catch unreachable; +// } +// } + +// if (preferred_size) |val| { +// if (!val[1].isEmpty()) { +// dest.append(context.allocator, Property{ .flex_preferred_size = val }) catch unreachable; +// } +// } + +// if (flex_order) |val| { +// if (!val[1].isEmpty()) { +// dest.append(context.allocator, Property{ .flex_order = val }) catch unreachable; +// } +// } + +// // Handle direction +// if (direction) |dir| { +// if (context.targets.browsers) |targets| { +// const prefixes = context.targets.prefixes(VendorPrefix{ .none = true }, .flex_direction); +// var prefixes_2009 = VendorPrefix{}; +// if (isFlexbox2009(targets)) { +// prefixes_2009.insert(.webkit); +// } +// if (prefixes.contains(.moz)) { +// prefixes_2009.insert(.moz); +// } +// if (!prefixes_2009.isEmpty()) { +// const orient_dir = dir[0].to2009(); +// dest.append(context.allocator, Property{ .box_orient = .{ orient_dir[0], prefixes_2009 } }) catch unreachable; +// dest.append(context.allocator, Property{ .box_direction = .{ orient_dir[1], prefixes_2009 } }) catch unreachable; +// } +// } +// } + +// // Handle flex-flow +// if (direction != null and wrap != null) { +// const intersection = direction.?[1].intersect(wrap.?[1]); +// if (!intersection.isEmpty()) { +// var prefix = context.targets.prefixes(intersection, .flex_flow); +// // Firefox only implemented the 2009 spec prefixed. +// prefix.remove(.moz); +// dest.append(context.allocator, Property{ .flex_flow = .{ +// .{ +// .direction = direction.?[0], +// .wrap = wrap.?[0], +// }, +// prefix, +// } }) catch unreachable; +// direction.?[1].remove(intersection); +// wrap.?[1].remove(intersection); +// } +// } + +// // Handle flex shorthand +// if (grow != null and shrink != null and basis != null) { +// const intersection = grow.?[1].intersect(shrink.?[1]).intersect(basis.?[1]); +// if (!intersection.isEmpty()) { +// var prefix = context.targets.prefixes(intersection, .flex); +// // Firefox only implemented the 2009 spec prefixed. +// prefix.remove(.moz); +// dest.append(context.allocator, Property{ .flex = .{ +// .{ +// .grow = grow.?[0], +// .shrink = shrink.?[0], +// .basis = basis.?[0], +// }, +// prefix, +// } }) catch unreachable; +// grow.?[1].remove(intersection); +// shrink.?[1].remove(intersection); +// basis.?[1].remove(intersection); +// } +// } + +// // Handle remaining individual properties +// if (direction) |dir| { +// if (!dir[1].isEmpty()) { +// var prefix = context.targets.prefixes(dir[1], .flex_direction); +// // Firefox only implemented the 2009 spec prefixed. +// prefix.remove(.moz); +// dest.append(context.allocator, Property{ .flex_direction = .{ dir[0], prefix } }) catch unreachable; +// } +// } + +// if (wrap) |w| { +// if (!w[1].isEmpty()) { +// var prefix = context.targets.prefixes(w[1], .flex_wrap); +// // Firefox only implemented the 2009 spec prefixed. +// prefix.remove(.moz); +// dest.append(context.allocator, Property{ .flex_wrap = .{ w[0], prefix } }) catch unreachable; +// } +// } + +// if (grow) |g| { +// if (!g[1].isEmpty()) { +// var prefix = context.targets.prefixes(g[1], .flex_grow); +// // Firefox only implemented the 2009 spec prefixed. +// prefix.remove(.moz); +// dest.append(context.allocator, Property{ .flex_grow = .{ g[0], prefix } }) catch unreachable; +// } +// } + +// if (shrink) |s| { +// if (!s[1].isEmpty()) { +// var prefix = context.targets.prefixes(s[1], .flex_shrink); +// // Firefox only implemented the 2009 spec prefixed. +// prefix.remove(.moz); +// dest.append(context.allocator, Property{ .flex_shrink = .{ s[0], prefix } }) catch unreachable; +// } +// } + +// if (basis) |b| { +// if (!b[1].isEmpty()) { +// var prefix = context.targets.prefixes(b[1], .flex_basis); +// // Firefox only implemented the 2009 spec prefixed. +// prefix.remove(.moz); +// dest.append(context.allocator, Property{ .flex_basis = .{ b[0], prefix } }) catch unreachable; +// } +// } + +// if (order) |o| { +// if (!o[1].isEmpty()) { +// var prefix = context.targets.prefixes(o[1], .order); +// // Firefox only implemented the 2009 spec prefixed. +// prefix.remove(.moz); +// dest.append(context.allocator, Property{ .order = .{ o[0], prefix } }) catch unreachable; +// } +// } +// } + +// fn isFlexProperty(property_id: *const PropertyId) bool { +// return switch (property_id.*) { +// .flex_direction, +// .box_orient, +// .box_direction, +// .flex_wrap, +// .box_lines, +// .flex_flow, +// .flex_grow, +// .box_flex, +// .flex_positive, +// .flex_shrink, +// .flex_negative, +// .flex_basis, +// .flex_preferred_size, +// .flex, +// .order, +// .box_ordinal_group, +// .flex_order, +// => true, +// else => false, +// }; +// } +// }; diff --git a/src/css/rules/namespace.zig b/src/css/rules/namespace.zig index 30bdade77f13..4f68a3837159 100644 --- a/src/css/rules/namespace.zig +++ b/src/css/rules/namespace.zig @@ -32,7 +32,7 @@ pub const NamespaceRule = struct { } try css.css_values.string.CSSStringFns.toCss(&this.url, W, dest); - try dest.writeChar(':'); + try dest.writeChar(';'); } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) This { diff --git a/src/css/selectors/parser.zig b/src/css/selectors/parser.zig index bcd01753cc4d..1a7fd7b7bc00 100644 --- a/src/css/selectors/parser.zig +++ b/src/css/selectors/parser.zig @@ -1049,8 +1049,58 @@ pub const SelectorParser = struct { } pub fn parseFunctionalPseudoElement(this: *SelectorParser, name: []const u8, input: *css.Parser) Result(Impl.SelectorImpl.PseudoElement) { - _ = this; // autofix - return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .unsupported_pseudo_class_or_element = name })) }; + const Enum = enum { + cue, + @"cue-region", + @"view-transition-group", + @"view-transition-image-pair", + @"view-transition-old", + @"view-transition-new", + }; + + const Map = bun.ComptimeEnumMap(Enum); + if (Map.get(name)) |v| { + return switch (v) { + .cue => .{ .result = .{ .cue_function = .{ .selector = switch (Selector.parse(this, input)) { + .result => |a| bun.create(input.allocator(), Selector, a), + .err => |e| return .{ .err = e }, + } } } }, + .@"cue-region" => .{ .result = .{ .cue_region_function = .{ .selector = switch (Selector.parse(this, input)) { + .result => |a| bun.create(input.allocator(), Selector, a), + .err => |e| return .{ .err = e }, + } } } }, + .@"view-transition-group" => .{ .result = .{ .view_transition_group = .{ .part_name = switch (ViewTransitionPartName.parse(input)) { + .result => |a| a, + .err => |e| return .{ .err = e }, + } } } }, + .@"view-transition-image-pair" => .{ .result = .{ .view_transition_image_pair = .{ .part_name = switch (ViewTransitionPartName.parse(input)) { + .result => |a| a, + .err => |e| return .{ .err = e }, + } } } }, + .@"view-transition-old" => .{ .result = .{ .view_transition_old = .{ .part_name = switch (ViewTransitionPartName.parse(input)) { + .result => |a| a, + .err => |e| return .{ .err = e }, + } } } }, + .@"view-transition-new" => .{ .result = .{ .view_transition_new = .{ .part_name = switch (ViewTransitionPartName.parse(input)) { + .result => |a| a, + .err => |e| return .{ .err = e }, + } } } }, + }; + } else { + if (!bun.strings.startsWith(name, "-")) { + this.options.warn(input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ + .unsupported_pseudo_class_or_element = name, + }))); + } + + var args = std.ArrayListUnmanaged(css.css_properties.custom.TokenOrValue){}; + if (css.TokenList.parseRaw(input, &args, this.options, 0).asErr()) |e| return .{ .err = e }; + + return .{ .result = .{ .custom_function = .{ + .name = name, + .arguments = css.TokenList{ .v = args }, + } } }; + } } fn parseIsAndWhere(this: *const SelectorParser) bool { @@ -1940,8 +1990,9 @@ pub fn GenericComponent(comptime Impl: type) type { pub fn format(this: *const This, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { switch (this.*) { .local_name => return try writer.print("local_name={s}", .{this.local_name.name.v}), - .combinator => return try writer.print("combinator={}", .{this.combinator}), + .combinator => return try writer.print("combinator='{}'", .{this.combinator}), .pseudo_element => return try writer.print("pseudo_element={}", .{this.pseudo_element}), + .class => return try writer.print("class={s}", .{this.class.v}), else => {}, } return writer.print("{s}", .{@tagName(this.*)}); @@ -2031,7 +2082,7 @@ pub const NthSelectorData = struct { .last_child => if (is_function) ":nth-last-child(" else ":last-child", .of_type => if (is_function) ":nth-of-type(" else ":first-of-type", .last_of_type => if (is_function) ":nth-last-of-type(" else ":last-of-type", - .only_child => if (is_function) ":nth-only-child(" else ":only-of-type", + .only_child => ":only-child", .only_of_type => ":only-of-type", .col => ":nth-col(", .last_col => ":nth-last-col(", @@ -2044,7 +2095,7 @@ pub const NthSelectorData = struct { fn numberSign(num: i32) []const u8 { if (num >= 0) return "+"; - return "-"; + return ""; } pub fn writeAffine(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { @@ -3631,6 +3682,17 @@ pub const ViewTransitionPartName = union(enum) { }; } + pub fn parse(input: *css.Parser) Result(ViewTransitionPartName) { + if (input.tryParse(css.Parser.expectDelim, .{'*'}).isOk()) { + return .{ .result = .all }; + } + + return .{ .result = .{ .name = switch (css.css_values.ident.CustomIdent.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } } }; + } + pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { return css.implementEql(@This(), lhs, rhs); } diff --git a/src/css/selectors/selector.zig b/src/css/selectors/selector.zig index 638863093087..945381622717 100644 --- a/src/css/selectors/selector.zig +++ b/src/css/selectors/selector.zig @@ -1523,10 +1523,13 @@ fn isTypeSelector(component: ?*const parser.Component) bool { fn isSimple(selector: *const parser.Selector) bool { var iter = selector.iterRawParseOrderFrom(0); - while (iter.next()) |component| { - if (component.isCombinator()) return true; - } - return false; + const any_is_combinator = any_is_combinator: { + while (iter.next()) |component| { + if (component.isCombinator()) break :any_is_combinator true; + } + break :any_is_combinator false; + }; + return !any_is_combinator; } const CombinatorIter = struct { diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index fb9d0391e168..95383930f3fc 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -53,8 +53,12 @@ export const iniInternals = { export const cssInternals = { minifyTestWithOptions: $newZigFunction("css_internals.zig", "minifyTestWithOptions", 3), + minifyErrorTestWithOptions: $newZigFunction("css_internals.zig", "minifyErrorTestWithOptions", 3), testWithOptions: $newZigFunction("css_internals.zig", "testWithOptions", 3), prefixTestWithOptions: $newZigFunction("css_internals.zig", "prefixTestWithOptions", 3), + minifyTest: $newZigFunction("css_internals.zig", "minifyTest", 3), + prefixTest: $newZigFunction("css_internals.zig", "prefixTest", 3), + _test: $newZigFunction("css_internals.zig", "_test", 3), attrTest: $newZigFunction("css_internals.zig", "attrTest", 3), }; diff --git a/src/transpiler.zig b/src/transpiler.zig index 6be3b668fe9a..7269d3e6a3e2 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -958,12 +958,15 @@ pub const Transpiler = struct { transpiler.log.addErrorFmt(null, logger.Loc.Empty, transpiler.allocator, "{} while minifying", .{e.kind}) catch bun.outOfMemory(); return null; } - const result = sheet.toCss(alloc, bun.css.PrinterOptions{ + const result = switch (sheet.toCss(alloc, bun.css.PrinterOptions{ .targets = bun.css.Targets.forBundlerTarget(transpiler.options.target), .minify = transpiler.options.minify_whitespace, - }, null) catch |e| { - bun.handleErrorReturnTrace(e, @errorReturnTrace()); - return null; + }, null)) { + .result => |v| v, + .err => |e| { + transpiler.log.addErrorFmt(null, logger.Loc.Empty, transpiler.allocator, "{} while printing", .{e}) catch bun.outOfMemory(); + return null; + }, }; output_file.value = .{ .buffer = .{ .allocator = alloc, .bytes = result.code } }; } else { diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 8aa51ebd346b..b1202c1f54f9 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -4,7 +4,22 @@ import { describe, test } from "bun:test"; import "harness"; -import { attrTest, cssTest, indoc, minify_test, minifyTest, prefix_test } from "./util"; +import { + attrTest, + cssTest, + indoc, + minify_test, + prefix_test, + minifyTestWithOptions as minify_test_with_options, + minify_error_test_with_options, + ParserFlags, + ParserOptions, +} from "./util"; + +function error_test(css: string, error: unknown) { + // going to ignore this test for now + test.skip(`ERROR: ${css}`, () => {}); +} describe("css tests", () => { describe("pseudo-class edge case", () => { @@ -38,7 +53,7 @@ describe("css tests", () => { }); test("calc edge case", () => { - minifyTest( + minify_test( // Problem: the value is being printed as Infinity in our restrict_prec thing but the internal thing actually wants it as 3.40282e38px `.rounded-full { border-radius: calc(infinity * 1px); @@ -48,7 +63,7 @@ describe("css tests", () => { ); }); describe("border_spacing", () => { - minifyTest( + minify_test( ` .foo { border-spacing: 0px; @@ -2392,10 +2407,10 @@ describe("css tests", () => { ); } - 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}"); + minify_test(".foo { aspect-ratio: auto }", ".foo{aspect-ratio:auto}"); + minify_test(".foo { aspect-ratio: 2 / 3 }", ".foo{aspect-ratio:2/3}"); + minify_test(".foo { aspect-ratio: auto 2 / 3 }", ".foo{aspect-ratio:auto 2/3}"); + minify_test(".foo { aspect-ratio: 2 / 3 auto }", ".foo{aspect-ratio:auto 2/3}"); }); describe("background", () => { @@ -2966,268 +2981,280 @@ describe("css tests", () => { }); describe("linear-gradient", () => { - minifyTest(".foo { background: linear-gradient(yellow, blue) }", ".foo{background:linear-gradient(#ff0,#00f)}"); - minifyTest( + minify_test(".foo { background: linear-gradient(yellow, blue) }", ".foo{background:linear-gradient(#ff0,#00f)}"); + minify_test( ".foo { background: linear-gradient(to bottom, yellow, blue); }", ".foo{background:linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(180deg, yellow, blue); }", ".foo{background:linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(0.5turn, yellow, blue); }", ".foo{background:linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(yellow 10%, blue 20%) }", ".foo{background:linear-gradient(#ff0 10%,#00f 20%)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(to top, blue, yellow); }", ".foo{background:linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(to top, blue 10%, yellow 20%); }", ".foo{background:linear-gradient(#ff0 80%,#00f 90%)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(to top, blue 10px, yellow 20px); }", ".foo{background:linear-gradient(0deg,#00f 10px,#ff0 20px)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(135deg, yellow, blue); }", ".foo{background:linear-gradient(135deg,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(yellow, blue 20%, #0f0); }", ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(to top right, red, white, blue) }", ".foo{background:linear-gradient(to top right,red,#fff,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(yellow, blue calc(10% * 2), #0f0); }", ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(yellow, 20%, blue); }", ".foo{background:linear-gradient(#ff0,20%,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(yellow, 50%, blue); }", ".foo{background:linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(yellow, 20px, blue); }", ".foo{background:linear-gradient(#ff0,20px,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(yellow, 50px, blue); }", ".foo{background:linear-gradient(#ff0,50px,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(yellow, 50px, blue); }", ".foo{background:linear-gradient(#ff0,50px,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(yellow, red 30% 40%, blue); }", ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}", ); - minifyTest( + minify_test( ".foo { background: linear-gradient(yellow, red 30%, red 40%, blue); }", ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}", ); - minifyTest(".foo { background: linear-gradient(0, yellow, blue); }", ".foo{background:linear-gradient(#00f,#ff0)}"); - minifyTest( + minify_test( + ".foo { background: linear-gradient(0, yellow, blue); }", + ".foo{background:linear-gradient(#00f,#ff0)}", + ); + minify_test( ".foo { background: -webkit-linear-gradient(yellow, blue) }", ".foo{background:-webkit-linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -webkit-linear-gradient(bottom, yellow, blue); }", ".foo{background:-webkit-linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -webkit-linear-gradient(top right, red, white, blue) }", ".foo{background:-webkit-linear-gradient(top right,red,#fff,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -moz-linear-gradient(yellow, blue) }", ".foo{background:-moz-linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -moz-linear-gradient(bottom, yellow, blue); }", ".foo{background:-moz-linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -moz-linear-gradient(top right, red, white, blue) }", ".foo{background:-moz-linear-gradient(top right,red,#fff,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -o-linear-gradient(yellow, blue) }", ".foo{background:-o-linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -o-linear-gradient(bottom, yellow, blue); }", ".foo{background:-o-linear-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -o-linear-gradient(top right, red, white, blue) }", ".foo{background:-o-linear-gradient(top right,red,#fff,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), to(yellow)) }", ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),to(#ff0))}", ); - minifyTest( + minify_test( ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), color-stop(50%, red), to(yellow)) }", ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}", ); - minifyTest( + minify_test( ".foo { background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, blue), color-stop(50%, red), color-stop(100%, yellow)) }", ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}", ); - minifyTest( + minify_test( ".foo { background: repeating-linear-gradient(yellow 10px, blue 50px) }", ".foo{background:repeating-linear-gradient(#ff0 10px,#00f 50px)}", ); - minifyTest( + minify_test( ".foo { background: -webkit-repeating-linear-gradient(yellow 10px, blue 50px) }", ".foo{background:-webkit-repeating-linear-gradient(#ff0 10px,#00f 50px)}", ); - minifyTest( + minify_test( ".foo { background: -moz-repeating-linear-gradient(yellow 10px, blue 50px) }", ".foo{background:-moz-repeating-linear-gradient(#ff0 10px,#00f 50px)}", ); - minifyTest( + minify_test( ".foo { background: -o-repeating-linear-gradient(yellow 10px, blue 50px) }", ".foo{background:-o-repeating-linear-gradient(#ff0 10px,#00f 50px)}", ); - minifyTest(".foo { background: radial-gradient(yellow, blue) }", ".foo{background:radial-gradient(#ff0,#00f)}"); - minifyTest( + minify_test(".foo { background: radial-gradient(yellow, blue) }", ".foo{background:radial-gradient(#ff0,#00f)}"); + minify_test( ".foo { background: radial-gradient(at top left, yellow, blue) }", ".foo{background:radial-gradient(at 0 0,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(5em circle at top left, yellow, blue) }", ".foo{background:radial-gradient(5em at 0 0,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(circle at 100%, #333, #333 50%, #eee 75%, #333 75%) }", ".foo{background:radial-gradient(circle at 100%,#333,#333 50%,#eee 75%,#333 75%)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(farthest-corner circle at 100% 50%, #333, #333 50%, #eee 75%, #333 75%) }", ".foo{background:radial-gradient(circle at 100%,#333,#333 50%,#eee 75%,#333 75%)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(farthest-corner circle at 50% 50%, #333, #333 50%, #eee 75%, #333 75%) }", ".foo{background:radial-gradient(circle,#333,#333 50%,#eee 75%,#333 75%)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(ellipse at top, #e66465, transparent) }", ".foo{background:radial-gradient(at top,#e66465,#0000)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(20px, yellow, blue) }", ".foo{background:radial-gradient(20px,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(circle 20px, yellow, blue) }", ".foo{background:radial-gradient(20px,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(20px 40px, yellow, blue) }", ".foo{background:radial-gradient(20px 40px,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(ellipse 20px 40px, yellow, blue) }", ".foo{background:radial-gradient(20px 40px,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(ellipse calc(20px + 10px) 40px, yellow, blue) }", ".foo{background:radial-gradient(30px 40px,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(circle farthest-side, yellow, blue) }", ".foo{background:radial-gradient(circle farthest-side,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(farthest-side circle, yellow, blue) }", ".foo{background:radial-gradient(circle farthest-side,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(ellipse farthest-side, yellow, blue) }", ".foo{background:radial-gradient(farthest-side,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: radial-gradient(farthest-side ellipse, yellow, blue) }", ".foo{background:radial-gradient(farthest-side,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -webkit-radial-gradient(yellow, blue) }", ".foo{background:-webkit-radial-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -moz-radial-gradient(yellow, blue) }", ".foo{background:-moz-radial-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -o-radial-gradient(yellow, blue) }", ".foo{background:-o-radial-gradient(#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: repeating-radial-gradient(circle 20px, yellow, blue) }", ".foo{background:repeating-radial-gradient(20px,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -webkit-repeating-radial-gradient(circle 20px, yellow, blue) }", ".foo{background:-webkit-repeating-radial-gradient(20px,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -moz-repeating-radial-gradient(circle 20px, yellow, blue) }", ".foo{background:-moz-repeating-radial-gradient(20px,#ff0,#00f)}", ); - minifyTest( + minify_test( ".foo { background: -o-repeating-radial-gradient(circle 20px, yellow, blue) }", ".foo{background:-o-repeating-radial-gradient(20px,#ff0,#00f)}", ); - minifyTest( + minify_test( ".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( + minify_test(".foo { background: conic-gradient(#f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}"); + minify_test( ".foo { background: conic-gradient(at 50% 50%, #f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}", ); - minifyTest( + minify_test( ".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( + minify_test( + ".foo { background: conic-gradient(from 0, #f06, gold) }", + ".foo{background:conic-gradient(#f06,gold)}", + ); + + minify_test( ".foo { background: conic-gradient(from 0deg at center, #f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}", ); - minifyTest( + + minify_test( ".foo { background: conic-gradient(white -50%, black 150%) }", ".foo{background:conic-gradient(#fff -50%,#000 150%)}", ); - minifyTest( + + minify_test( ".foo { background: conic-gradient(white -180deg, black 540deg) }", ".foo{background:conic-gradient(#fff -180deg,#000 540deg)}", ); - minifyTest( + + minify_test( ".foo { background: conic-gradient(from 45deg, white, black, white) }", ".foo{background:conic-gradient(from 45deg,#fff,#000,#fff)}", ); - minifyTest( + + minify_test( ".foo { background: repeating-conic-gradient(from 45deg, white, black, white) }", ".foo{background:repeating-conic-gradient(from 45deg,#fff,#000,#fff)}", ); - minifyTest( + + minify_test( ".foo { background: repeating-conic-gradient(black 0deg 25%, white 0deg 50%) }", ".foo{background:repeating-conic-gradient(#000 0deg 25%,#fff 0deg 50%)}", ); @@ -3253,7 +3280,7 @@ describe("css tests", () => { `, ); - minifyTest( + minify_test( ` .foo { font-family: "Helvetica", "Times New Roman", sans-serif; @@ -3297,7 +3324,7 @@ describe("css tests", () => { `, ); - minifyTest( + minify_test( ` .foo { font-family: "Helvetica", "Times New Roman", sans-serif; @@ -3328,29 +3355,29 @@ describe("css tests", () => { `, ); - 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"}'); + minify_test(".foo { font: normal normal 600 9px/normal Charcoal; }", ".foo{font:600 9px Charcoal}"); + minify_test(".foo { font: normal normal 500 medium/normal Charcoal; }", ".foo{font:500 medium Charcoal}"); + minify_test(".foo { font: normal normal 400 medium Charcoal; }", ".foo{font:400 medium Charcoal}"); + minify_test(".foo { font: normal normal 500 medium/10px Charcoal; }", ".foo{font:500 medium/10px Charcoal}"); + minify_test(".foo { font-family: 'sans-serif'; }", '.foo{font-family:"sans-serif"}'); + minify_test(".foo { font-family: sans-serif; }", ".foo{font-family:sans-serif}"); + minify_test(".foo { font-family: 'default'; }", '.foo{font-family:"default"}'); + minify_test(".foo { font-family: default; }", ".foo{font-family:default}"); + minify_test(".foo { font-family: 'inherit'; }", '.foo{font-family:"inherit"}'); + minify_test(".foo { font-family: inherit; }", ".foo{font-family:inherit}"); + minify_test(".foo { font-family: inherit test; }", ".foo{font-family:inherit test}"); + minify_test(".foo { font-family: 'inherit test'; }", ".foo{font-family:inherit test}"); + minify_test(".foo { font-family: revert; }", ".foo{font-family:revert}"); + minify_test(".foo { font-family: 'revert'; }", '.foo{font-family:"revert"}'); + minify_test(".foo { font-family: revert-layer; }", ".foo{font-family:revert-layer}"); + minify_test(".foo { font-family: revert-layer, serif; }", ".foo{font-family:revert-layer,serif}"); + minify_test(".foo { font-family: 'revert', sans-serif; }", '.foo{font-family:"revert",sans-serif}'); + minify_test(".foo { font-family: 'revert', foo, sans-serif; }", '.foo{font-family:"revert",foo,sans-serif}'); + minify_test(".foo { font-family: ''; }", '.foo{font-family:""}'); + + // fonTfamily in @font-face + minify_test("@font-face { font-family: 'revert'; }", '@font-face{font-family:"revert"}'); + minify_test("@font-face { font-family: 'revert-layer'; }", '@font-face{font-family:"revert-layer"}'); prefix_test( ` @@ -3628,4 +3655,616 @@ describe("css tests", () => { }, ); }); + + describe("selectors", () => { + minify_test(":nth-col(2n) {width: 20px}", ":nth-col(2n){width:20px}"); + minify_test(":nth-col(10n-1) {width: 20px}", ":nth-col(10n-1){width:20px}"); + minify_test(":nth-col(-n+2) {width: 20px}", ":nth-col(-n+2){width:20px}"); + minify_test(":nth-col(even) {width: 20px}", ":nth-col(2n){width:20px}"); + minify_test(":nth-col(odd) {width: 20px}", ":nth-col(odd){width:20px}"); + minify_test(":nth-last-col(2n) {width: 20px}", ":nth-last-col(2n){width:20px}"); + minify_test(":nth-last-col(10n-1) {width: 20px}", ":nth-last-col(10n-1){width:20px}"); + minify_test(":nth-last-col(-n+2) {width: 20px}", ":nth-last-col(-n+2){width:20px}"); + minify_test(":nth-last-col(even) {width: 20px}", ":nth-last-col(2n){width:20px}"); + minify_test(":nth-last-col(odd) {width: 20px}", ":nth-last-col(odd){width:20px}"); + minify_test(":nth-child(odd) {width: 20px}", ":nth-child(odd){width:20px}"); + minify_test(":nth-child(2n) {width: 20px}", ":nth-child(2n){width:20px}"); + minify_test(":nth-child(2n+1) {width: 20px}", ":nth-child(odd){width:20px}"); + minify_test(":first-child {width: 20px}", ":first-child{width:20px}"); + minify_test(":nth-child(1) {width: 20px}", ":first-child{width:20px}"); + minify_test(":nth-last-child(1) {width: 20px}", ":last-child{width:20px}"); + minify_test(":nth-of-type(1) {width: 20px}", ":first-of-type{width:20px}"); + minify_test(":nth-last-of-type(1) {width: 20px}", ":last-of-type{width:20px}"); + minify_test(":nth-child(even of li.important) {width: 20px}", ":nth-child(2n of li.important){width:20px}"); + minify_test(":nth-child(1 of li.important) {width: 20px}", ":nth-child(1 of li.important){width:20px}"); + minify_test( + ":nth-last-child(even of li.important) {width: 20px}", + ":nth-last-child(2n of li.important){width:20px}", + ); + minify_test(":nth-last-child(1 of li.important) {width: 20px}", ":nth-last-child(1 of li.important){width:20px}"); + minify_test(":nth-last-child(1 of.important) {width: 20px}", ":nth-last-child(1 of .important){width:20px}"); + + minify_test('[foo="baz"] {color:red}', "[foo=baz]{color:red}"); + minify_test('[foo="foo bar"] {color:red}', "[foo=foo\\ bar]{color:red}"); + minify_test('[foo="foo bar baz"] {color:red}', '[foo="foo bar baz"]{color:red}'); + minify_test('[foo=""] {color:red}', '[foo=""]{color:red}'); + minify_test('.test:not([foo="bar"]) {color:red}', ".test:not([foo=bar]){color:red}"); + minify_test(".test + .foo {color:red}", ".test+.foo{color:red}"); + minify_test(".test ~ .foo {color:red}", ".test~.foo{color:red}"); + minify_test(".test .foo {color:red}", ".test .foo{color:red}"); + minify_test( + ".custom-range::-webkit-slider-thumb:active {color:red}", + ".custom-range::-webkit-slider-thumb:active{color:red}", + ); + minify_test(".test:not(.foo, .bar) {color:red}", ".test:not(.foo,.bar){color:red}"); + minify_test(".test:is(.foo, .bar) {color:red}", ".test:is(.foo,.bar){color:red}"); + minify_test(".test:where(.foo, .bar) {color:red}", ".test:where(.foo,.bar){color:red}"); + minify_test(".test:where(.foo, .bar) {color:red}", ".test:where(.foo,.bar){color:red}"); + minify_test(":host {color:red}", ":host{color:red}"); + minify_test(":host(.foo) {color:red}", ":host(.foo){color:red}"); + minify_test("::slotted(span) {color:red", "::slotted(span){color:red}"); + minify_test("custom-element::part(foo) {color:red}", "custom-element::part(foo){color:red}"); + minify_test(".sm\\:text-5xl { font-size: 3rem }", ".sm\\:text-5xl{font-size:3rem}"); + minify_test("a:has(> img) {color:red}", "a:has(>img){color:red}"); + minify_test("dt:has(+ dt) {color:red}", "dt:has(+dt){color:red}"); + minify_test( + "section:not(:has(h1, h2, h3, h4, h5, h6)) {color:red}", + "section:not(:has(h1,h2,h3,h4,h5,h6)){color:red}", + ); + minify_test(":has(.sibling ~ .target) {color:red}", ":has(.sibling~.target){color:red}"); + minify_test(".x:has(> .a > .b) {color:red}", ".x:has(>.a>.b){color:red}"); + minify_test(".x:has(.bar, #foo) {color:red}", ".x:has(.bar,#foo){color:red}"); + minify_test(".x:has(span + span) {color:red}", ".x:has(span+span){color:red}"); + minify_test("a:has(:visited) {color:red}", "a:has(:visited){color:red}"); + + for (const element of [ + "-webkit-scrollbar", + "-webkit-scrollbar-button", + "-webkit-scrollbar-track", + "-webkit-scrollbar-track-piece", + "-webkit-scrollbar-thumb", + "-webkit-scrollbar-corner", + "-webkit-resizer", + ]) { + for (const clasz of [ + "enabled", + "disabled", + "hover", + "active", + "horizontal", + "vertical", + "decrement", + "increment", + "start", + "end", + "double-button", + "single-button", + "no-button", + "corner-present", + "window-inactive", + ]) { + minify_test(`::${element}:${clasz} {color:red}`, `::${element}:${clasz}{color:red}`); + } + } + + for (const clasz of [ + "horizontal", + "vertical", + "decrement", + "increment", + "start", + "end", + "double-button", + "single-button", + "no-button", + "corner-present", + "window-inactive", + ]) { + error_test( + `:${clasz} {color: red}`, + "ParserError::SelectorError(SelectorError::InvalidPseudoClassBeforeWebKitScrollbar)", + ); + } + + for (const element of [ + "-webkit-scrollbar", + "-webkit-scrollbar-button", + "-webkit-scrollbar-track", + "-webkit-scrollbar-track-piece", + "-webkit-scrollbar-thumb", + "-webkit-scrollbar-corner", + "-webkit-resizer", + ]) { + error_test( + `::${element}:focus {color:red}`, + "ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterWebKitScrollbar)", + ); + } + + error_test( + "a::first-letter:last-child {color:red}", + "ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement)", + ); + + minify_test("a:last-child::first-letter {color:red}", "a:last-child:first-letter{color:red}"); + + error_test( + "a::first-letter:last-child {color:red}", + "ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement)", + ); + minify_test("a:last-child::first-letter {color:red}", "a:last-child:first-letter{color:red}"); + + prefix_test( + ".test:not(.foo, .bar) {color:red}", + ` + .test:not(:-webkit-any(.foo, .bar)) { + color: red; + } + + .test:not(:is(.foo, .bar)) { + color: red; + } + `, + { + safari: 8 << 16, + }, + ); + prefix_test( + ".test:not(.foo, .bar) {color:red}", + ` + .test:not(.foo, .bar) { + color: red; + } + `, + { + safari: 11 << 16, + }, + ); + + minify_test("a:lang(en) {color:red}", "a:lang(en){color:red}"); + minify_test("a:lang(en, fr) {color:red}", "a:lang(en,fr){color:red}"); + minify_test("a:lang('en') {color:red}", "a:lang(en){color:red}"); + minify_test("a:-webkit-any(.foo, .bar) {color:red}", "a:-webkit-any(.foo,.bar){color:red}"); + minify_test("a:-moz-any(.foo, .bar) {color:red}", "a:-moz-any(.foo,.bar){color:red}"); + + prefix_test( + "a:is(.foo, .bar) {color:red}", + ` + a:-webkit-any(.foo, .bar) { + color: red; + } + + a:-moz-any(.foo, .bar) { + color: red; + } + + a:is(.foo, .bar) { + color: red; + } + `, + { + safari: 11 << 16, + firefox: 50 << 16, + }, + ); + + prefix_test( + "a:is(.foo > .bar) {color:red}", + ` + a:is(.foo > .bar) { + color: red; + } + `, + { + safari: 11 << 16, + firefox: 50 << 16, + }, + ); + + prefix_test( + "a:lang(en, fr) {color:red}", + ` + a:-webkit-any(:lang(en), :lang(fr)) { + color: red; + } + + a:-moz-any(:lang(en), :lang(fr)) { + color: red; + } + + a:is(:lang(en), :lang(fr)) { + color: red; + } + `, + { + safari: 11 << 16, + firefox: 50 << 16, + }, + ); + + prefix_test( + "a:lang(en, fr) {color:red}", + ` + a:is(:lang(en), :lang(fr)) { + color: red; + } + `, + { + safari: 14 << 16, + firefox: 88 << 16, + }, + ); + + prefix_test( + "a:lang(en, fr) {color:red}", + ` + a:lang(en, fr) { + color: red; + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + "a:dir(rtl) {color:red}", + ` + a:-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)) { + color: red; + } + + a:-moz-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)) { + color: red; + } + + a: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)) { + color: red; + } + `, + { + safari: 11 << 16, + firefox: 50 << 16, + }, + ); + + prefix_test( + "a:dir(ltr) {color:red}", + ` + a: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))) { + color: red; + } + + a:not(:-moz-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))) { + color: red; + } + + a: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))) { + color: red; + } + `, + { + safari: 11 << 16, + firefox: 50 << 16, + }, + ); + + prefix_test( + "a:dir(rtl) {color:red}", + ` + a: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)) { + color: red; + } + `, + { + safari: 14 << 16, + firefox: 88 << 16, + }, + ); + + prefix_test( + "a:dir(ltr) {color:red}", + ` + a:not(: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)) { + color: red; + } + `, + { + safari: 14 << 16, + firefox: 88 << 16, + }, + ); + + prefix_test( + "a:dir(rtl) {color:red}", + ` + a:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) { + color: red; + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + "a:dir(ltr) {color:red}", + ` + a:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) { + color: red; + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + "a:is(:dir(rtl)) {color:red}", + ` + a:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) { + color: red; + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + "a:where(:dir(rtl)) {color:red}", + ` + a:where(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) { + color: red; + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + "a:has(:dir(rtl)) {color:red}", + ` + a:has(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) { + color: red; + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + "a:not(:dir(rtl)) {color:red}", + ` + a:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) { + color: red; + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + "a:dir(rtl)::after {color:red}", + ` + a:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi):after { + color: red; + } + `, + { + safari: 14 << 16, + }, + ); + + prefix_test( + "a:dir(rtl) div {color:red}", + ` + a:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) div { + color: red; + } + `, + { + safari: 14 << 16, + }, + ); + + minify_test(".foo::cue {color: red}", ".foo::cue{color:red}"); + minify_test(".foo::cue-region {color: red}", ".foo::cue-region{color:red}"); + minify_test(".foo::cue(b) {color: red}", ".foo::cue(b){color:red}"); + minify_test(".foo::cue-region(b) {color: red}", ".foo::cue-region(b){color:red}"); + minify_test("::cue(v[voice='active']) {color: yellow;}", "::cue(v[voice=active]){color:#ff0}"); + minify_test(":foo(bar) { color: yellow }", ":foo(bar){color:#ff0}"); + minify_test("::foo(bar) { color: yellow }", "::foo(bar){color:#ff0}"); + minify_test("::foo(*) { color: yellow }", "::foo(*){color:#ff0}"); + + minify_test(":is(.foo) { color: yellow }", ".foo{color:#ff0}"); + minify_test(":is(#foo) { color: yellow }", "#foo{color:#ff0}"); + minify_test("a:is(.foo) { color: yellow }", "a.foo{color:#ff0}"); + minify_test("a:is([data-test]) { color: yellow }", "a[data-test]{color:#ff0}"); + minify_test(".foo:is(a) { color: yellow }", ".foo:is(a){color:#ff0}"); + minify_test(".foo:is(*|a) { color: yellow }", ".foo:is(*|a){color:#ff0}"); + minify_test(".foo:is(*) { color: yellow }", ".foo:is(*){color:#ff0}"); + minify_test( + "@namespace svg url(http://www.w3.org/2000/svg); .foo:is(svg|a) { color: yellow }", + '@namespace svg "http://www.w3.org/2000/svg";.foo:is(svg|a){color:#ff0}', + ); + minify_test("a:is(.foo .bar) { color: yellow }", "a:is(.foo .bar){color:#ff0}"); + minify_test(":is(.foo, .bar) { color: yellow }", ":is(.foo,.bar){color:#ff0}"); + minify_test("a:is(:not(.foo)) { color: yellow }", "a:not(.foo){color:#ff0}"); + minify_test("a:is(:first-child) { color: yellow }", "a:first-child{color:#ff0}"); + minify_test("a:is(:has(.foo)) { color: yellow }", "a:has(.foo){color:#ff0}"); + minify_test("a:is(:is(.foo)) { color: yellow }", "a.foo{color:#ff0}"); + minify_test(":host(:hover) {color: red}", ":host(:hover){color:red}"); + minify_test("::slotted(:hover) {color: red}", "::slotted(:hover){color:red}"); + + minify_test(":root::view-transition {position: fixed}", ":root::view-transition{position:fixed}"); + + for (const name of [ + "view-transition-group", + "view-transition-image-pair", + "view-transition-new", + "view-transition-old", + ]) { + minify_test(`:root::${name}(*) {position: fixed}`, `:root::${name}(*){position:fixed}`); + minify_test(`:root::${name}(foo) {position: fixed}`, `:root::${name}(foo){position:fixed}`); + minify_test(`:root::${name}(foo):only-child {position: fixed}`, `:root::${name}(foo):only-child{position:fixed}`); + error_test( + `:root::${name}(foo):first-child {position: fixed}`, + "ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement)", + ); + error_test( + `:root::${name}(foo)::before {position: fixed}`, + "ParserError::SelectorError(SelectorError::InvalidState)", + ); + } + + minify_test(".foo ::deep .bar {width: 20px}", ".foo ::deep .bar{width:20px}"); + minify_test(".foo::deep .bar {width: 20px}", ".foo::deep .bar{width:20px}"); + minify_test(".foo ::deep.bar {width: 20px}", ".foo ::deep.bar{width:20px}"); + minify_test(".foo ::unknown .bar {width: 20px}", ".foo ::unknown .bar{width:20px}"); + minify_test(".foo ::unknown(foo) .bar {width: 20px}", ".foo ::unknown(foo) .bar{width:20px}"); + minify_test(".foo ::unknown:only-child {width: 20px}", ".foo ::unknown:only-child{width:20px}"); + minify_test(".foo ::unknown(.foo) .bar {width: 20px}", ".foo ::unknown(.foo) .bar{width:20px}"); + minify_test( + ".foo ::unknown(.foo .bar / .baz) .bar {width: 20px}", + ".foo ::unknown(.foo .bar / .baz) .bar{width:20px}", + ); + minify_test(".foo ::unknown(something(foo)) .bar {width: 20px}", ".foo ::unknown(something(foo)) .bar{width:20px}"); + minify_test(".foo ::unknown([abc]) .bar {width: 20px}", ".foo ::unknown([abc]) .bar{width:20px}"); + + let deep_options: ParserOptions = { + flags: [ParserFlags.DEEP_SELECTOR_COMBINATOR], + }; + + error_test(".foo >>> .bar {width: 20px}", "ParserError::SelectorError(SelectorError::DanglingCombinator)"); + error_test(".foo /deep/ .bar {width: 20px}", "ParserError::SelectorError(SelectorError::DanglingCombinator)"); + minify_test_with_options(".foo >>> .bar {width: 20px}", ".foo>>>.bar{width:20px}", deep_options); + minify_test_with_options(".foo /deep/ .bar {width: 20px}", ".foo /deep/ .bar{width:20px}", deep_options); + + let pure_css_module_options: ParserOptions = { + css_modules: { + pure: true, + }, + }; + + minify_error_test_with_options( + "div {width: 20px}", + "MinifyErrorKind::ImpureCSSModuleSelector", + pure_css_module_options, + ); + minify_error_test_with_options( + ":global(.foo) {width: 20px}", + "MinifyErrorKind::ImpureCSSModuleSelector", + pure_css_module_options, + ); + minify_error_test_with_options( + "[foo=bar] {width: 20px}", + "MinifyErrorKind::ImpureCSSModuleSelector", + pure_css_module_options, + ); + minify_error_test_with_options( + "div, .foo {width: 20px}", + "MinifyErrorKind::ImpureCSSModuleSelector", + pure_css_module_options, + ); + minify_test_with_options(":local(.foo) {width: 20px}", "._8Z4fiW_foo{width:20px}", pure_css_module_options); + minify_test_with_options("div.my-class {color: red;}", "div._8Z4fiW_my-class{color:red}", pure_css_module_options); + minify_test_with_options("#id {color: red;}", "#_8Z4fiW_id{color:red}", pure_css_module_options); + minify_test_with_options("a .my-class{color: red;}", "a ._8Z4fiW_my-class{color:red}", pure_css_module_options); + minify_test_with_options(".my-class a {color: red;}", "._8Z4fiW_my-class a{color:red}", pure_css_module_options); + minify_test_with_options( + ".my-class:is(a) {color: red;}", + "._8Z4fiW_my-class:is(a){color:red}", + pure_css_module_options, + ); + minify_test_with_options( + "div:has(.my-class) {color: red;}", + "div:has(._8Z4fiW_my-class){color:red}", + pure_css_module_options, + ); + minify_test_with_options( + ".foo { html &:hover { a_value: some-value; } }", + "._8Z4fiW_foo{html &:hover{a_value:some-value}}", + pure_css_module_options, + ); + minify_test_with_options( + ".foo { span { color: red; } }", + "._8Z4fiW_foo{& span{color:red}}", + pure_css_module_options, + ); + minify_error_test_with_options( + "html { .foo { span { color: red; } } }", + "MinifyErrorKind::ImpureCSSModuleSelector", + pure_css_module_options, + ); + minify_test_with_options( + ".foo { div { span { color: red; } } }", + "._8Z4fiW_foo{& div{& span{color:red}}}", + pure_css_module_options, + ); + minify_error_test_with_options( + "@scope (div) { .foo { color: red } }", + "MinifyErrorKind::ImpureCSSModuleSelector", + pure_css_module_options, + ); + minify_error_test_with_options( + "@scope (.a) to (div) { .foo { color: red } }", + "MinifyErrorKind::ImpureCSSModuleSelector", + pure_css_module_options, + ); + minify_error_test_with_options( + "@scope (.a) to (.b) { div { color: red } }", + "MinifyErrorKind::ImpureCSSModuleSelector", + pure_css_module_options, + ); + minify_test_with_options( + "@scope (.a) to (.b) { .foo { color: red } }", + "@scope(._8Z4fiW_a) to (._8Z4fiW_b){._8Z4fiW_foo{color:red}}", + pure_css_module_options, + ); + + error_test( + "input.defaultCheckbox::before h1 {width: 20px}", + ` + ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Ident( + "h1".into(), + ))), +`, + ); + error_test( + "input.defaultCheckbox::before .my-class {width: 20px}", + "ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Delim('.')))", + ); + error_test( + "input.defaultCheckbox::before.my-class {width: 20px}", + "ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Delim('.')))", + ); + error_test( + "input.defaultCheckbox::before #id {width: 20px}", + `ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::IDHash( + "id".into(), + )))`, + ); + error_test( + "input.defaultCheckbox::before#id {width: 20px}", + `ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::IDHash( + "id".into(), + )))`, + ); + error_test( + "input.defaultCheckbox::before [attr] {width: 20px}", + `ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement( + Token::SquareBracketBlock, + ))`, + ); + error_test( + "input.defaultCheckbox::before[attr] {width: 20px}", + `ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement( + Token::SquareBracketBlock, + ))`, + ); + }); }); From d9b072b4ac50adfd5a2a81a40e8ba48db7818799 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:50:14 -0800 Subject: [PATCH 2/3] add media tests --- src/css/media_query.zig | 14 + src/css/rules/media.zig | 7 +- src/css/rules/rules.zig | 7 +- test/js/bun/css/css.test.ts | 599 ++++++++++++++++++++++++++++++++++++ 4 files changed, 619 insertions(+), 8 deletions(-) diff --git a/src/css/media_query.zig b/src/css/media_query.zig index dd9ce06765b7..25a5c58c619d 100644 --- a/src/css/media_query.zig +++ b/src/css/media_query.zig @@ -116,6 +116,16 @@ pub const MediaList = struct { break :brk true; }; } + + /// Returns whether the media query list never matches. + pub fn neverMatches(this: *const MediaList) bool { + return this.media_queries.items.len > 0 and brk: { + for (this.media_queries.items) |*query| { + if (!query.neverMatches()) break :brk false; + } + break :brk true; + }; + } }; /// A binary `and` or `or` operator. @@ -251,6 +261,10 @@ pub const MediaQuery = struct { return toCssWithParensIfNeeded(condition, W, dest, needs_parens); } + + pub fn neverMatches(this: *const MediaQuery) bool { + return this.qualifier == .not and this.media_type == .all and this.condition == null; + } }; /// Flags for `parse_query_condition`. diff --git a/src/css/rules/media.zig b/src/css/rules/media.zig index 7cbb5214acd4..d48d9a0938b6 100644 --- a/src/css/rules/media.zig +++ b/src/css/rules/media.zig @@ -25,10 +25,9 @@ pub fn MediaRule(comptime R: type) type { const This = @This(); pub fn minify(this: *This, context: *css.MinifyContext, parent_is_unused: bool) css.MinifyErr!bool { - _ = this; // autofix - _ = context; // autofix - _ = parent_is_unused; // autofix - return false; + try this.rules.minify(context, parent_is_unused); + + return this.rules.v.items.len == 0 or this.query.neverMatches(); } pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { diff --git a/src/css/rules/rules.zig b/src/css/rules/rules.zig index 951a302c92c8..25f7bdec6b3c 100644 --- a/src/css/rules/rules.zig +++ b/src/css/rules/rules.zig @@ -205,10 +205,9 @@ pub fn CssRuleList(comptime AtRule: type) type { _ = try last_rule.minify(context, parent_is_unused); continue; } - - if (try med.minify(context, parent_is_unused)) { - continue; - } + } + if (try med.minify(context, parent_is_unused)) { + continue; } }, .supports => |*supp| { diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index b1202c1f54f9..b15333c82f51 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -4267,4 +4267,603 @@ describe("css tests", () => { ))`, ); }); + + describe("media", () => { + minify_test( + "@media (min-width: 240px) { .foo { color: chartreuse }}", + "@media (width>=240px){.foo{color:#7fff00}}", + ); + minify_test("@media (width < 240px) { .foo { color: chartreuse }}", "@media (width<240px){.foo{color:#7fff00}}"); + minify_test("@media (width <= 240px) { .foo { color: chartreuse }}", "@media (width<=240px){.foo{color:#7fff00}}"); + minify_test("@media (width > 240px) { .foo { color: chartreuse }}", "@media (width>240px){.foo{color:#7fff00}}"); + minify_test("@media (width >= 240px) { .foo { color: chartreuse }}", "@media (width>=240px){.foo{color:#7fff00}}"); + minify_test("@media (240px < width) { .foo { color: chartreuse }}", "@media (width>240px){.foo{color:#7fff00}}"); + minify_test("@media (240px <= width) { .foo { color: chartreuse }}", "@media (width>=240px){.foo{color:#7fff00}}"); + minify_test("@media (240px > width) { .foo { color: chartreuse }}", "@media (width<240px){.foo{color:#7fff00}}"); + minify_test("@media (240px >= width) { .foo { color: chartreuse }}", "@media (width<=240px){.foo{color:#7fff00}}"); + minify_test( + "@media (100px < width < 200px) { .foo { color: chartreuse }}", + "@media (100px=30em) and (width<=50em){.foo{color:#7fff00}}", + ); + minify_test("@media screen, print { .foo { color: chartreuse }}", "@media screen,print{.foo{color:#7fff00}}"); + minify_test("@media (hover: hover) { .foo { color: chartreuse }}", "@media (hover:hover){.foo{color:#7fff00}}"); + minify_test("@media (hover) { .foo { color: chartreuse }}", "@media (hover){.foo{color:#7fff00}}"); + minify_test( + "@media (aspect-ratio: 11/5) { .foo { color: chartreuse }}", + "@media (aspect-ratio:11/5){.foo{color:#7fff00}}", + ); + minify_test( + "@media (aspect-ratio: 2/1) { .foo { color: chartreuse }}", + "@media (aspect-ratio:2){.foo{color:#7fff00}}", + ); + minify_test( + "@media (aspect-ratio: 2) { .foo { color: chartreuse }}", + "@media (aspect-ratio:2){.foo{color:#7fff00}}", + ); + minify_test( + "@media not screen and (color) { .foo { color: chartreuse }}", + "@media not screen and (color){.foo{color:#7fff00}}", + ); + minify_test( + "@media only screen and (color) { .foo { color: chartreuse }}", + "@media only screen and (color){.foo{color:#7fff00}}", + ); + minify_test( + "@media (update: slow) or (hover: none) { .foo { color: chartreuse }}", + "@media (update:slow) or (hover:none){.foo{color:#7fff00}}", + ); + minify_test( + "@media (width < 600px) and (height < 600px) { .foo { color: chartreuse }}", + "@media (width<600px) and (height<600px){.foo{color:#7fff00}}", + ); + minify_test( + "@media (not (color)) or (hover) { .foo { color: chartreuse }}", + "@media (not (color)) or (hover){.foo{color:#7fff00}}", + ); + error_test( + "@media (example, all,), speech { .foo { color: chartreuse }}", + "ParserError::UnexpectedToken(Token::Comma)", + ); + error_test("@media &test, speech { .foo { color: chartreuse }}", "ParserError::UnexpectedToken(Token::Delim('&'))"); + error_test("@media &test { .foo { color: chartreuse }}", "ParserError::UnexpectedToken(Token::Delim('&'))"); + minify_test( + "@media (min-width: calc(200px + 40px)) { .foo { color: chartreuse }}", + "@media (width>=240px){.foo{color:#7fff00}}", + ); + minify_test( + "@media (min-width: calc(1em + 5px)) { .foo { color: chartreuse }}", + "@media (width>=calc(1em + 5px)){.foo{color:#7fff00}}", + ); + minify_test("@media { .foo { color: chartreuse }}", ".foo{color:#7fff00}"); + minify_test("@media all { .foo { color: chartreuse }}", ".foo{color:#7fff00}"); + minify_test( + "@media not (((color) or (hover))) { .foo { color: chartreuse }}", + "@media not ((color) or (hover)){.foo{color:#7fff00}}", + ); + minify_test( + "@media (hover) and ((color) and (test)) { .foo { color: chartreuse }}", + "@media (hover) and (color) and (test){.foo{color:#7fff00}}", + ); + minify_test("@media (grid: 1) { .foo { color: chartreuse }}", "@media (grid:1){.foo{color:#7fff00}}"); + minify_test( + "@media (width >= calc(2px + 4px)) { .foo { color: chartreuse }}", + "@media (width>=6px){.foo{color:#7fff00}}", + ); + + prefix_test( + ` + @media (width >= 240px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (min-width: 240px) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 60 << 16, + }, + ); + + prefix_test( + ` + @media (width >= 240px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (width >= 240px) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 64 << 16, + }, + ); + + prefix_test( + ` + @media (color > 2) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (min-color: 3) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 60 << 16, + }, + ); + + prefix_test( + ` + @media (color < 2) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (max-color: 1) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 60 << 16, + }, + ); + + prefix_test( + ` + @media (width > 240px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (min-width: 240.001px) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 60 << 16, + }, + ); + + prefix_test( + ` + @media (width <= 240px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (max-width: 240px) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 60 << 16, + }, + ); + + prefix_test( + ` + @media (width <= 240px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (width <= 240px) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 64 << 16, + }, + ); + + prefix_test( + ` + @media (width < 240px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (max-width: 239.999px) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 60 << 16, + }, + ); + + prefix_test( + ` + @media (100px <= width <= 200px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (min-width: 100px) and (max-width: 200px) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 85 << 16, + }, + ); + + prefix_test( + ` + @media not (100px <= width <= 200px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media not ((min-width: 100px) and (max-width: 200px)) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 85 << 16, + }, + ); + + prefix_test( + ` + @media (hover) and (100px <= width <= 200px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (hover) and (min-width: 100px) and (max-width: 200px) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 85 << 16, + }, + ); + + prefix_test( + ` + @media (hover) or (100px <= width <= 200px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (hover) or ((min-width: 100px) and (max-width: 200px)) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 85 << 16, + }, + ); + + prefix_test( + ` + @media (100px < width < 200px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (min-width: 100.001px) and (max-width: 199.999px) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 85 << 16, + }, + ); + + prefix_test( + ` + @media (200px >= width >= 100px) { + .foo { + color: chartreuse; + } + } + `, + ` + @media (max-width: 200px) and (min-width: 100px) { + .foo { + color: #7fff00; + } + } + `, + { + firefox: 85 << 16, + }, + ); + + cssTest( + ` + @media not all { + .a { + color: green; + } + } + `, + "\n", + undefined, + false, + ); + + prefix_test( + ` + @media (width > calc(1px + 1rem)) { + .foo { color: yellow; } + } + `, + ` + @media (min-width: calc(1.001px + 1rem)) { + .foo { + color: #ff0; + } + } + `, + { + chrome: 85 << 16, + }, + ); + prefix_test( + ` + @media (width > max(10px, 1rem)) { + .foo { color: yellow; } + } + `, + ` + @media (min-width: calc(max(10px, 1rem) + .001px)) { + .foo { + color: #ff0; + } + } + `, + { + chrome: 85 << 16, + }, + ); + prefix_test( + ` + @media (width > 0) { + .foo { color: yellow; } + } + `, + ` + @media (min-width: .001px) { + .foo { + color: #ff0; + } + } + `, + { + chrome: 85 << 16, + }, + ); + prefix_test( + ` + @media (min-resolution: 2dppx) { + .foo { color: yellow; } + } + `, + ` + @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { + .foo { + color: #ff0; + } + } + `, + { + safari: 15 << 16, + }, + true, + ); + prefix_test( + ` + @media (min-resolution: 2dppx) { + .foo { color: yellow; } + } + `, + ` + @media (min--moz-device-pixel-ratio: 2), (min-resolution: 2dppx) { + .foo { + color: #ff0; + } + } + `, + { + firefox: 10 << 16, + }, + true, + ); + prefix_test( + ` + @media (resolution > 2dppx) { + .foo { color: yellow; } + } + `, + ` + @media (-webkit-min-device-pixel-ratio: 2.001), (min-resolution: 2.001dppx) { + .foo { + color: #ff0; + } + } + `, + { + safari: 15 << 16, + }, + true, + ); + prefix_test( + ` + @media (resolution >= 300dpi) { + .foo { color: yellow; } + } + `, + ` + @media (-webkit-min-device-pixel-ratio: 3.125), (min-resolution: 300dpi) { + .foo { + color: #ff0; + } + } + `, + { + safari: 15 << 16, + }, + true, + ); + prefix_test( + ` + @media (min-resolution: 113.38dpcm) { + .foo { color: yellow; } + } + `, + ` + @media (-webkit-min-device-pixel-ratio: 2.99985), (min--moz-device-pixel-ratio: 2.99985), (min-resolution: 113.38dpcm) { + .foo { + color: #ff0; + } + } + `, + { + safari: 15 << 16, + firefox: 10 << 16, + }, + true, + ); + prefix_test( + ` + @media (color) and (min-resolution: 2dppx) { + .foo { color: yellow; } + } + `, + ` + @media (color) and (-webkit-min-device-pixel-ratio: 2), (color) and (min-resolution: 2dppx) { + .foo { + color: #ff0; + } + } + `, + { + safari: 15 << 16, + }, + true, + ); + prefix_test( + ` + @media (min-resolution: 2dppx), + (min-resolution: 192dpi) { + .foo { color: yellow; } + } + `, + ` + @media (-webkit-min-device-pixel-ratio: 2), (min--moz-device-pixel-ratio: 2), (min-resolution: 2dppx), (min-resolution: 192dpi) { + .foo { + color: #ff0; + } + } + `, + { + safari: 15 << 16, + firefox: 10 << 16, + }, + true, + ); + prefix_test( + ` + @media only screen and (min-resolution: 124.8dpi) { + .foo { color: yellow; } + } + `, + ` + @media only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (min-resolution: 124.8dpi) { + .foo { + color: #ff0; + } + } + `, + { + safari: 15 << 16, + firefox: 10 << 16, + }, + true, + ); + + error_test("@media (min-width: hi) { .foo { color: chartreuse }}", "ParserError::InvalidMediaQuery"); + error_test("@media (width >= hi) { .foo { color: chartreuse }}", "ParserError::InvalidMediaQuery"); + error_test( + "@media (width >= 2/1) { .foo { color: chartreuse }}", + "ParserError::UnexpectedToken(Token::Delim('/'))", + ); + error_test("@media (600px <= min-height) { .foo { color: chartreuse }}", "ParserError::InvalidMediaQuery"); + error_test("@media (scan >= 1) { .foo { color: chartreuse }}", "ParserError::InvalidMediaQuery"); + error_test("@media (min-scan: interlace) { .foo { color: chartreuse }}", "ParserError::InvalidMediaQuery"); + error_test("@media (1px <= width <= bar) { .foo { color: chartreuse }}", "ParserError::InvalidMediaQuery"); + error_test("@media (1px <= min-width <= 2px) { .foo { color: chartreuse }}", "ParserError::InvalidMediaQuery"); + error_test("@media (1px <= scan <= 2px) { .foo { color: chartreuse }}", "ParserError::InvalidMediaQuery"); + error_test("@media (grid: 10) { .foo { color: chartreuse }}", "ParserError::InvalidMediaQuery"); + error_test("@media (prefers-color-scheme = dark) { .foo { color: chartreuse }}", "ParserError::InvalidMediaQuery"); + }); }); From 2057ea942cca014f4b449c9f12081a69bec46862 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:52:51 -0800 Subject: [PATCH 3/3] yoops --- test/js/bun/css/util.ts | 53 ++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/test/js/bun/css/util.ts b/test/js/bun/css/util.ts index 0ec5da219cb4..7b25087745e1 100644 --- a/test/js/bun/css/util.ts +++ b/test/js/bun/css/util.ts @@ -1,9 +1,18 @@ import { describe, expect, test } from "bun:test"; import { cssInternals } from "bun:internal-for-testing"; import dedent from "./dedent"; -const { minifyTestWithOptions, testWithOptions, prefixTestWithOptions, attrTest: __attrTest } = cssInternals; +const { + minifyTestWithOptions, + testWithOptions, + _test, + prefixTestWithOptions, + prefixTest, + minifyTest, + attrTest: __attrTest, + minifyErrorTestWithOptions: __minifyErrorTestWithOptions, +} = cssInternals; -type Browsers = { +export type Browsers = { android?: number; chrome?: number; edge?: number; @@ -15,27 +24,49 @@ type Browsers = { samsung?: number; }; -export function minify_test(source: string, expected: string) { - return minifyTest(source, expected); +export type ParserOptions = { + css_modules?: { + pure: boolean; + }; + flags?: ParserFlags[]; +}; + +export enum ParserFlags { + DEEP_SELECTOR_COMBINATOR = "DEEP_SELECTOR_COMBINATOR", +} + +export function minify_error_test_with_options(source: string, expectedError: string, options: ParserOptions) { + test.skip(source, () => { + let error_string: string | undefined = undefined; + try { + __minifyErrorTestWithOptions(source, expectedError, options); + } catch (err) { + error_string = err.toString(); + } + expect(error_string).toEqual(expectedError); + }); } -export function minifyTest(source: string, expected: string) { + +export function minify_test(source: string, expected: string) { test(source, () => { expect(minifyTestWithOptions(source, expected)).toEqual(expected); }); } -export function prefix_test(source: string, expected: string, targets: Browsers) { - test(source, () => { - expect(prefixTestWithOptions(source, expected, targets)).toEqualIgnoringWhitespace(expected); +export function prefix_test(source: string, expected: string, targets: Browsers, skip?: boolean) { + const testf = skip ? test.skip : test; + testf(source, () => { + expect(prefixTest(source, expected, targets)).toEqualIgnoringWhitespace(expected); }); } export function css_test(source: string, expected: string, browsers?: Browsers) { return cssTest(source, expected, browsers); } -export function cssTest(source: string, expected: string, browsers?: Browsers) { - test(source, () => { - const output = testWithOptions(source, expected, browsers); +export function cssTest(source: string, expected: string, browsers?: Browsers, skip?: boolean) { + const testf = skip ? test.skip : test; + testf(source, () => { + const output = _test(source, expected, browsers); console.log("Output", output); expect(output).toEqualIgnoringWhitespace(expected); });