From af12ff104afdcf6a5e22f241fc2d27978d598114 Mon Sep 17 00:00:00 2001 From: snwy Date: Wed, 25 Sep 2024 17:50:11 -0700 Subject: [PATCH 01/10] fix utf8 handling when importing json (#14168) --- src/StandaloneModuleGraph.zig | 2 +- src/bundler.zig | 4 +-- src/bundler/bundle_v2.zig | 2 +- src/bunfig.zig | 2 +- src/cache.zig | 14 ++++---- src/json_parser.zig | 7 ++-- src/resolver/package_json.zig | 2 +- src/resolver/resolver.zig | 2 +- src/sourcemap/sourcemap.zig | 2 +- test/bundler/bundler_edgecase.test.ts | 52 +++++++++++++++++++++++++++ 10 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 9e9827aa00e6e..8a4200f3cf536 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -999,7 +999,7 @@ pub const StandaloneModuleGraph = struct { bun.JSAst.Expr.Data.Store.reset(); bun.JSAst.Stmt.Data.Store.reset(); } - var json = bun.JSON.ParseJSON(&json_src, &log, arena) catch + var json = bun.JSON.ParseJSON(&json_src, &log, arena, false) catch return error.InvalidSourceMap; const mappings_str = json.get("mappings") orelse diff --git a/src/bundler.zig b/src/bundler.zig index 6d79465e43f5c..6f2d8abda8072 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -1473,9 +1473,9 @@ pub const Bundler = struct { // We allow importing tsconfig.*.json or jsconfig.*.json with comments // These files implicitly become JSONC files, which aligns with the behavior of text editors. if (source.path.isJSONCFile()) - json_parser.ParseTSConfig(&source, bundler.log, allocator) catch return null + json_parser.ParseTSConfig(&source, bundler.log, allocator, false) catch return null else - json_parser.ParseJSON(&source, bundler.log, allocator) catch return null + json_parser.ParseJSON(&source, bundler.log, allocator, false) catch return null else if (kind == .toml) TOML.parse(&source, bundler.log, allocator) catch return null else diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 66b3ec532ac6e..877031490772f 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -2736,7 +2736,7 @@ pub const ParseTask = struct { .json => { const trace = tracer(@src(), "ParseJSON"); defer trace.end(); - const root = (try resolver.caches.json.parsePackageJSON(log, source, allocator)) orelse Expr.init(E.Object, E.Object{}, Logger.Loc.Empty); + const root = (try resolver.caches.json.parsePackageJSON(log, source, allocator, false)) orelse Expr.init(E.Object, E.Object{}, Logger.Loc.Empty); return JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?); }, .toml => { diff --git a/src/bunfig.zig b/src/bunfig.zig index ede8389cde3c3..bccc9aa750b35 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -806,7 +806,7 @@ pub const Bunfig = struct { ctx.log.addErrorFmt(&source, logger.Loc.Empty, allocator, "Failed to parse", .{}) catch unreachable; } return err; - } else JSONParser.ParseTSConfig(&source, ctx.log, allocator) catch |err| { + } else JSONParser.ParseTSConfig(&source, ctx.log, allocator, true) catch |err| { if (ctx.log.errors + ctx.log.warnings == log_count) { ctx.log.addErrorFmt(&source, logger.Loc.Empty, allocator, "Failed to parse", .{}) catch unreachable; } diff --git a/src/cache.zig b/src/cache.zig index b82e2e10b7d36..6934a8aa6efd8 100644 --- a/src/cache.zig +++ b/src/cache.zig @@ -294,12 +294,12 @@ pub const Json = struct { pub fn init(_: std.mem.Allocator) Json { return Json{}; } - fn parse(_: *@This(), log: *logger.Log, source: logger.Source, allocator: std.mem.Allocator, comptime func: anytype) anyerror!?js_ast.Expr { + fn parse(_: *@This(), log: *logger.Log, source: logger.Source, allocator: std.mem.Allocator, comptime func: anytype, comptime force_utf8: bool) anyerror!?js_ast.Expr { var temp_log = logger.Log.init(allocator); defer { temp_log.appendToMaybeRecycled(log, &source) catch {}; } - return func(&source, &temp_log, allocator) catch handler: { + return func(&source, &temp_log, allocator, force_utf8) catch handler: { break :handler null; }; } @@ -308,17 +308,17 @@ pub const Json = struct { // They are JSON files with comments and trailing commas. // Sometimes tooling expects this to work. if (source.path.isJSONCFile()) { - return try parse(cache, log, source, allocator, json_parser.ParseTSConfig); + return try parse(cache, log, source, allocator, json_parser.ParseTSConfig, true); } - return try parse(cache, log, source, allocator, json_parser.ParseJSON); + return try parse(cache, log, source, allocator, json_parser.ParseJSON, false); } - pub fn parsePackageJSON(cache: *@This(), log: *logger.Log, source: logger.Source, allocator: std.mem.Allocator) anyerror!?js_ast.Expr { - return try parse(cache, log, source, allocator, json_parser.ParseTSConfig); + pub fn parsePackageJSON(cache: *@This(), log: *logger.Log, source: logger.Source, allocator: std.mem.Allocator, comptime force_utf8: bool) anyerror!?js_ast.Expr { + return try parse(cache, log, source, allocator, json_parser.ParseTSConfig, force_utf8); } pub fn parseTSConfig(cache: *@This(), log: *logger.Log, source: logger.Source, allocator: std.mem.Allocator) anyerror!?js_ast.Expr { - return try parse(cache, log, source, allocator, json_parser.ParseTSConfig); + return try parse(cache, log, source, allocator, json_parser.ParseTSConfig, true); } }; diff --git a/src/json_parser.zig b/src/json_parser.zig index bf46ea15edfe0..56a5e6ac889bc 100644 --- a/src/json_parser.zig +++ b/src/json_parser.zig @@ -714,6 +714,7 @@ pub fn ParseJSON( source: *const logger.Source, log: *logger.Log, allocator: std.mem.Allocator, + comptime force_utf8: bool, ) !Expr { var parser = try JSONParser.init(allocator, source.*, log); switch (source.contents.len) { @@ -734,7 +735,7 @@ pub fn ParseJSON( else => {}, } - return try parser.parseExpr(false, false); + return try parser.parseExpr(false, force_utf8); } /// Parse Package JSON @@ -1023,7 +1024,7 @@ pub fn ParseEnvJSON(source: *const logger.Source, log: *logger.Log, allocator: s } } -pub fn ParseTSConfig(source: *const logger.Source, log: *logger.Log, allocator: std.mem.Allocator) !Expr { +pub fn ParseTSConfig(source: *const logger.Source, log: *logger.Log, allocator: std.mem.Allocator, comptime force_utf8: bool) !Expr { switch (source.contents.len) { // This is to be consisntent with how disabled JS files are handled 0 => { @@ -1044,7 +1045,7 @@ pub fn ParseTSConfig(source: *const logger.Source, log: *logger.Log, allocator: var parser = try TSConfigParser.init(allocator, source.*, log); - return parser.parseExpr(false, true); + return parser.parseExpr(false, force_utf8); } const duplicateKeyJson = "{ \"name\": \"valid\", \"name\": \"invalid\" }"; diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index baa90d2f52410..95c64776f14ce 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -641,7 +641,7 @@ pub const PackageJSON = struct { var json_source = logger.Source.initPathString(key_path.text, entry.contents); json_source.path.pretty = r.prettyPath(json_source.path); - const json: js_ast.Expr = (r.caches.json.parsePackageJSON(r.log, json_source, allocator) catch |err| { + const json: js_ast.Expr = (r.caches.json.parsePackageJSON(r.log, json_source, allocator, true) catch |err| { if (Environment.isDebug) { Output.printError("{s}: JSON parse error: {s}", .{ package_json_path, @errorName(err) }); } diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 11052982e723a..6f444080e56d8 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -734,7 +734,7 @@ pub const Resolver = struct { // support passing a package.json or path to a package const pkg: *const PackageJSON = result.package_json orelse r.packageJSONForResolvedNodeModuleWithIgnoreMissingName(&result, true) orelse return error.MissingPackageJSON; - const json = (try r.caches.json.parsePackageJSON(r.log, pkg.source, r.allocator)) orelse return error.JSONParseError; + const json = (try r.caches.json.parsePackageJSON(r.log, pkg.source, r.allocator, true)) orelse return error.JSONParseError; pkg.loadFrameworkWithPreference(pair, json, r.allocator, load_defines, preference); const dir = pkg.source.path.sourceDir(); diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index 8277ae948ee64..87cdd0558eb0c 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -132,7 +132,7 @@ pub fn parseJSON( bun.JSAst.Stmt.Data.Store.reset(); } debug("parse (JSON, {d} bytes)", .{source.len}); - var json = bun.JSON.ParseJSON(&json_src, &log, arena) catch { + var json = bun.JSON.ParseJSON(&json_src, &log, arena, false) catch { return error.InvalidJSON; }; diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index 866c98d0ea4f7..d1357d9d87b08 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -1831,6 +1831,58 @@ describe("bundler", () => { }, run: { stdout: "1\n2" }, }); + itBundled("edgecase/Latin1StringInImportedJSON", { + files: { + "/entry.ts": ` + import x from './second.json'; + console.log(x + 'a'); + `, + "/second.json": ` + "测试" + `, + }, + target: "bun", + run: { stdout: `测试a` }, + }); + itBundled("edgecase/Latin1StringInImportedJSONBrowser", { + files: { + "/entry.ts": ` + import x from './second.json'; + console.log(x + 'a'); + `, + "/second.json": ` + "测试" + `, + }, + target: "browser", + run: { stdout: `测试a` }, + }); + itBundled("edgecase/Latin1StringKey", { + files: { + "/entry.ts": ` + import x from './second.json'; + console.log(x["测试" + "a"]); + `, + "/second.json": ` + {"测试a" : 123} + `, + }, + target: "bun", + run: { stdout: `123` }, + }); + itBundled("edgecase/Latin1StringKeyBrowser", { + files: { + "/entry.ts": ` + import x from './second.json'; + console.log(x["测试" + "a"]); + `, + "/second.json": ` + {"测试a" : 123} + `, + }, + target: "browser", + run: { stdout: `123` }, + }); // TODO(@paperdave): test every case of this. I had already tested it manually, but it may break later const requireTranspilationListESM = [ From ec7078a00647714b1d7548a13fc944f45c226cbc Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Wed, 25 Sep 2024 18:12:32 -0700 Subject: [PATCH 02/10] dont leak the address string in UDPSocket.addressToString (#14127) --- src/bun.js/api/bun/udp_socket.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bun.js/api/bun/udp_socket.zig b/src/bun.js/api/bun/udp_socket.zig index b26d327ecf03e..c911be57766b7 100644 --- a/src/bun.js/api/bun/udp_socket.zig +++ b/src/bun.js/api/bun/udp_socket.zig @@ -631,7 +631,8 @@ pub const UDPSocket = struct { }; const slice = bun.fmt.formatIp(address, &text_buf) catch unreachable; - return bun.String.createLatin1(slice).toJS(globalThis); + var str = bun.String.createLatin1(slice); + return str.transferToJS(globalThis); } pub fn getAddress(this: *This, globalThis: *JSGlobalObject) JSValue { From 7b058e24ff8d06c5782fc0461c32d19fa8e0c811 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Wed, 25 Sep 2024 18:13:28 -0700 Subject: [PATCH 03/10] fix memory leak in Bun.shellEscape return value (#14130) --- src/bun.js/api/BunObject.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 12aff1d40d562..488b9dc9f0a5b 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -435,7 +435,8 @@ pub fn shellEscape( globalThis.throw("String has invalid utf-16: {s}", .{bunstr.byteSlice()}); return .undefined; } - return bun.String.createUTF8(outbuf.items[0..]).toJS(globalThis); + var str = bun.String.createUTF8(outbuf.items[0..]); + return str.transferToJS(globalThis); } return jsval; } @@ -445,7 +446,8 @@ pub fn shellEscape( globalThis.throwOutOfMemory(); return .undefined; }; - return bun.String.createUTF8(outbuf.items[0..]).toJS(globalThis); + var str = bun.String.createUTF8(outbuf.items[0..]); + return str.transferToJS(globalThis); } return jsval; From 18822b9f45df07310b9edc40c7888acfc054b94f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 26 Sep 2024 10:54:54 -0700 Subject: [PATCH 04/10] Support `AbortSignal` in `Bun.spawn` (#14180) --- packages/bun-types/bun.d.ts | 26 +++++++ src/bun.js/api/bun/subprocess.zig | 53 ++++++++++++++- test/js/bun/spawn/spawn-signal.test.ts | 93 ++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 test/js/bun/spawn/spawn-signal.test.ts diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index bed018c9a5043..5b5a316fa4e88 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -4615,6 +4615,32 @@ declare module "bun" { * @default cmds[0] */ argv0?: string; + + /** + * An {@link AbortSignal} that can be used to abort the subprocess. + * + * This is useful for aborting a subprocess when some other part of the + * program is aborted, such as a `fetch` response. + * + * Internally, this works by calling `subprocess.kill(1)`. + * + * @example + * ```ts + * const controller = new AbortController(); + * const { signal } = controller; + * const start = performance.now(); + * const subprocess = Bun.spawn({ + * cmd: ["sleep", "100"], + * signal, + * }); + * await Bun.sleep(1); + * controller.abort(); + * await subprocess.exited; + * const end = performance.now(); + * console.log(end - start); // 1ms instead of 101ms + * ``` + */ + signal?: AbortSignal; } type OptionsToSubprocess = diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index f51669b5948db..e5b211830c9e7 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -203,6 +203,7 @@ pub const Subprocess = struct { weak_file_sink_stdin_ptr: ?*JSC.WebCore.FileSink = null, ref_count: u32 = 1, + abort_signal: ?*JSC.AbortSignal = null, usingnamespace bun.NewRefCounted(@This(), Subprocess.deinit); @@ -226,6 +227,12 @@ pub const Subprocess = struct { poll_ref: Async.KeepAlive = .{}, }; + pub fn onAbortSignal(subprocess_ctx: ?*anyopaque, _: JSC.JSValue) callconv(.C) void { + var this: *Subprocess = @ptrCast(@alignCast(subprocess_ctx.?)); + this.clearAbortSignal(); + _ = this.tryKill(SignalCode.default); + } + pub fn resourceUsage( this: *Subprocess, globalObject: *JSGlobalObject, @@ -1437,6 +1444,7 @@ pub const Subprocess = struct { this_jsvalue.ensureStillAlive(); this.pid_rusage = rusage.*; const is_sync = this.flags.is_sync; + this.clearAbortSignal(); defer this.deref(); defer this.disconnectIPC(true); @@ -1587,6 +1595,15 @@ pub const Subprocess = struct { this.destroy(); } + fn clearAbortSignal(this: *Subprocess) void { + if (this.abort_signal) |signal| { + this.abort_signal = null; + signal.pendingActivityUnref(); + signal.cleanNativeBindings(this); + signal.unref(); + } + } + pub fn finalize(this: *Subprocess) callconv(.C) void { log("finalize", .{}); // Ensure any code which references the "this" value doesn't attempt to @@ -1594,6 +1611,8 @@ pub const Subprocess = struct { // access GC'd values during the finalizer this.this_jsvalue = .zero; + this.clearAbortSignal(); + bun.assert(!this.hasPendingActivity() or JSC.VirtualMachine.get().isShuttingDown()); this.finalizeStreams(); @@ -1702,6 +1721,13 @@ pub const Subprocess = struct { var windows_hide: bool = false; var windows_verbatim_arguments: bool = false; + var abort_signal: ?*JSC.WebCore.AbortSignal = null; + defer { + // Ensure we clean it up on error. + if (abort_signal) |signal| { + signal.unref(); + } + } { if (args.isEmptyOrUndefinedOrNull()) { @@ -1846,6 +1872,14 @@ pub const Subprocess = struct { } } + if (args.getOwnTruthy(globalThis, "signal")) |signal_val| { + if (signal_val.as(JSC.WebCore.AbortSignal)) |signal| { + abort_signal = signal.ref(); + } else { + return globalThis.throwInvalidArgumentTypeValue("signal", "AbortSignal", signal_val); + } + } + if (args.getOwnTruthy(globalThis, "onDisconnect")) |onDisconnect_| { if (!onDisconnect_.isCell() or !onDisconnect_.isCallable(globalThis.vm())) { globalThis.throwInvalidArguments("onDisconnect must be a function or undefined", .{}); @@ -2280,12 +2314,29 @@ pub const Subprocess = struct { should_close_memfd = false; if (comptime !is_sync) { + // Once everything is set up, we can add the abort listener + // Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted + // Therefore, we must do this at the very end. + if (abort_signal) |signal| { + signal.pendingActivityRef(); + subprocess.abort_signal = signal.addListener(subprocess, onAbortSignal); + abort_signal = null; + } return out; } if (comptime is_sync) { switch (subprocess.process.watchOrReap()) { - .result => {}, + .result => { + // Once everything is set up, we can add the abort listener + // Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted + // Therefore, we must do this at the very end. + if (abort_signal) |signal| { + signal.pendingActivityRef(); + subprocess.abort_signal = signal.addListener(subprocess, onAbortSignal); + abort_signal = null; + } + }, .err => { subprocess.process.wait(true); }, diff --git a/test/js/bun/spawn/spawn-signal.test.ts b/test/js/bun/spawn/spawn-signal.test.ts new file mode 100644 index 0000000000000..535bd23964bac --- /dev/null +++ b/test/js/bun/spawn/spawn-signal.test.ts @@ -0,0 +1,93 @@ +import { test, expect } from "bun:test"; +import { spawn, sleep } from "bun"; +import { bunEnv, bunExe } from "harness"; + +test("spawn AbortSignal works after spawning", async () => { + const controller = new AbortController(); + const { signal } = controller; + const start = performance.now(); + const subprocess = Bun.spawn({ + cmd: [bunExe(), "--eval", "await Bun.sleep(100000)"], + env: bunEnv, + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + signal, + }); + await Bun.sleep(1); + controller.abort(); + expect(await subprocess.exited).not.toBe(0); + const end = performance.now(); + expect(end - start).toBeLessThan(100); +}); + +test("spawn AbortSignal works if already aborted", async () => { + const controller = new AbortController(); + const { signal } = controller; + const start = performance.now(); + const subprocess = Bun.spawn({ + cmd: [bunExe(), "--eval", "await Bun.sleep(100000)"], + env: bunEnv, + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + signal, + }); + await Bun.sleep(1); + controller.abort(); + expect(await subprocess.exited).not.toBe(0); + const end = performance.now(); + expect(end - start).toBeLessThan(100); +}); + +test("spawn AbortSignal args validation", async () => { + expect(() => + Bun.spawn({ + cmd: [bunExe(), "--eval", "await Bun.sleep(100000)"], + env: bunEnv, + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + signal: 123, + }), + ).toThrow(); +}); + +test("spawnSync AbortSignal works as timeout", async () => { + const start = performance.now(); + const subprocess = Bun.spawnSync({ + cmd: [bunExe(), "--eval", "await Bun.sleep(100000)"], + env: bunEnv, + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + signal: AbortSignal.timeout(10), + }); + + expect(subprocess.success).toBeFalse(); + const end = performance.now(); + expect(end - start).toBeLessThan(100); +}); + +// TODO: this test should fail. +// It passes because we are ticking the event loop incorrectly in spawnSync. +// it should be ticking a different event loop. +test("spawnSync AbortSignal...executes javascript?", async () => { + const start = performance.now(); + var signal = AbortSignal.timeout(10); + signal.addEventListener("abort", () => { + console.log("abort", performance.now()); + }); + const subprocess = Bun.spawnSync({ + cmd: [bunExe(), "--eval", "await Bun.sleep(100000)"], + env: bunEnv, + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + signal, + }); + console.log("after", performance.now()); + expect(subprocess.success).toBeFalse(); + const end = performance.now(); + expect(end - start).toBeLessThan(100); +}); From 274e5a202223ae8b809b556565fae2979d59fcd5 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:39:26 -0700 Subject: [PATCH 05/10] CSS Parser (#14122) Co-authored-by: Jarred Sumner --- bench/bun.lockb | Bin 69762 -> 72327 bytes bench/package.json | 2 + bench/snippets/color.mjs | 25 + build.zig | 1 + bun.lockb | Bin 184722 -> 189738 bytes docs/api/color.md | 263 + docs/nav.ts | 4 + package.json | 5 +- packages/bun-types/bun.d.ts | 81 + src/bitflags.zig | 62 + src/bun.js/api/BunObject.zig | 12 +- src/bun.js/bindings/BunObject+exports.h | 1 + src/bun.js/bindings/BunObject.cpp | 1 + src/bun.js/bindings/bindings.zig | 6 +- src/bun.js/javascript.zig | 2 + src/bun.js/node/types.zig | 60 +- src/bun.zig | 32 + src/bundler.zig | 148 +- src/comptime_string_map.zig | 58 + src/css/README.md | 3 + src/css/build-prefixes.js | 745 ++ src/css/compat.zig | 5407 ++++++++++++++ src/css/context.zig | 52 + src/css/css_internals.zig | 279 + src/css/css_modules.zig | 406 ++ src/css/css_parser.zig | 6279 +++++++++++++++++ src/css/declaration.zig | 236 + src/css/dependencies.zig | 145 + src/css/error.zig | 324 + src/css/logical.zig | 30 + src/css/media_query.zig | 1401 ++++ src/css/prefixes.zig | 2201 ++++++ src/css/printer.zig | 412 ++ src/css/properties/align.zig | 310 + src/css/properties/animation.zig | 153 + src/css/properties/background.zig | 136 + src/css/properties/border.zig | 246 + src/css/properties/border_image.zig | 55 + src/css/properties/border_radius.zig | 27 + src/css/properties/box_shadow.zig | 40 + src/css/properties/contain.zig | 43 + src/css/properties/css_modules.zig | 91 + src/css/properties/custom.zig | 1329 ++++ src/css/properties/display.zig | 102 + src/css/properties/effects.zig | 77 + src/css/properties/flex.zig | 75 + src/css/properties/font.zig | 616 ++ src/css/properties/generate_properties.ts | 1803 +++++ src/css/properties/list.zig | 86 + src/css/properties/margin_padding.zig | 280 + src/css/properties/masking.zig | 161 + src/css/properties/outline.zig | 44 + src/css/properties/overflow.zig | 85 + src/css/properties/position.zig | 47 + src/css/properties/properties.zig | 1879 +++++ src/css/properties/properties_generated.zig | 679 ++ src/css/properties/properties_impl.zig | 122 + src/css/properties/shape.zig | 47 + src/css/properties/size.zig | 121 + src/css/properties/svg.zig | 100 + src/css/properties/text.zig | 192 + src/css/properties/transform.zig | 202 + src/css/properties/transition.zig | 37 + src/css/properties/ui.zig | 109 + src/css/rules/container.zig | 331 + src/css/rules/counter_style.zig | 47 + src/css/rules/custom_media.zig | 33 + src/css/rules/document.zig | 55 + src/css/rules/font_face.zig | 723 ++ src/css/rules/font_palette_values.zig | 286 + src/css/rules/import.zig | 92 + src/css/rules/keyframes.zig | 299 + src/css/rules/layer.zig | 164 + src/css/rules/media.zig | 55 + src/css/rules/namespace.zig | 37 + src/css/rules/nesting.zig | 34 + src/css/rules/page.zig | 384 + src/css/rules/property.zig | 225 + src/css/rules/rules.zig | 364 + src/css/rules/scope.zig | 78 + src/css/rules/starting_style.zig | 41 + src/css/rules/style.zig | 170 + src/css/rules/supports.zig | 389 + src/css/rules/unknown.zig | 51 + src/css/rules/viewport.zig | 34 + src/css/selectors/builder.zig | 215 + src/css/selectors/parser.zig | 3239 +++++++++ src/css/selectors/selector.zig | 1167 +++ src/css/sourcemap.zig | 36 + src/css/targets.zig | 126 + src/css/values/alpha.zig | 48 + src/css/values/angle.zig | 290 + src/css/values/calc.zig | 1792 +++++ src/css/values/color.zig | 4294 +++++++++++ src/css/values/color_generated.zig | 945 +++ src/css/values/color_js.zig | 478 ++ src/css/values/color_via.ts | 231 + src/css/values/css_string.zig | 22 + src/css/values/easing.zig | 257 + src/css/values/gradient.zig | 1077 +++ src/css/values/ident.zig | 149 + src/css/values/image.zig | 195 + src/css/values/length.zig | 500 ++ src/css/values/number.zig | 69 + src/css/values/percentage.zig | 297 + src/css/values/position.zig | 372 + src/css/values/ratio.zig | 71 + src/css/values/rect.zig | 98 + src/css/values/resolution.zig | 98 + src/css/values/size.zig | 84 + src/css/values/syntax.zig | 505 ++ src/css/values/time.zig | 201 + src/css/values/url.zig | 139 + src/css/values/values.zig | 42 + src/css/writer.zig | 68 + src/feature_flags.zig | 2 + src/js/internal-for-testing.ts | 7 + src/logger.zig | 20 + src/meta.zig | 120 +- src/output.zig | 176 +- src/string_immutable.zig | 117 + .../bun/css/__snapshots__/color.test.ts.snap | Bin 0 -> 28817 bytes test/js/bun/css/color.test.ts | 210 + test/js/bun/css/css.test.ts | 1916 +++++ test/js/bun/css/dedent.ts | 85 + test/js/bun/css/util.ts | 51 + test/js/bun/shell/exec.test.ts | 9 +- 127 files changed, 51593 insertions(+), 94 deletions(-) create mode 100644 bench/snippets/color.mjs create mode 100644 docs/api/color.md create mode 100644 src/bitflags.zig create mode 100644 src/css/README.md create mode 100644 src/css/build-prefixes.js create mode 100644 src/css/compat.zig create mode 100644 src/css/context.zig create mode 100644 src/css/css_internals.zig create mode 100644 src/css/css_modules.zig create mode 100644 src/css/css_parser.zig create mode 100644 src/css/declaration.zig create mode 100644 src/css/dependencies.zig create mode 100644 src/css/error.zig create mode 100644 src/css/logical.zig create mode 100644 src/css/media_query.zig create mode 100644 src/css/prefixes.zig create mode 100644 src/css/printer.zig create mode 100644 src/css/properties/align.zig create mode 100644 src/css/properties/animation.zig create mode 100644 src/css/properties/background.zig create mode 100644 src/css/properties/border.zig create mode 100644 src/css/properties/border_image.zig create mode 100644 src/css/properties/border_radius.zig create mode 100644 src/css/properties/box_shadow.zig create mode 100644 src/css/properties/contain.zig create mode 100644 src/css/properties/css_modules.zig create mode 100644 src/css/properties/custom.zig create mode 100644 src/css/properties/display.zig create mode 100644 src/css/properties/effects.zig create mode 100644 src/css/properties/flex.zig create mode 100644 src/css/properties/font.zig create mode 100644 src/css/properties/generate_properties.ts create mode 100644 src/css/properties/list.zig create mode 100644 src/css/properties/margin_padding.zig create mode 100644 src/css/properties/masking.zig create mode 100644 src/css/properties/outline.zig create mode 100644 src/css/properties/overflow.zig create mode 100644 src/css/properties/position.zig create mode 100644 src/css/properties/properties.zig create mode 100644 src/css/properties/properties_generated.zig create mode 100644 src/css/properties/properties_impl.zig create mode 100644 src/css/properties/shape.zig create mode 100644 src/css/properties/size.zig create mode 100644 src/css/properties/svg.zig create mode 100644 src/css/properties/text.zig create mode 100644 src/css/properties/transform.zig create mode 100644 src/css/properties/transition.zig create mode 100644 src/css/properties/ui.zig create mode 100644 src/css/rules/container.zig create mode 100644 src/css/rules/counter_style.zig create mode 100644 src/css/rules/custom_media.zig create mode 100644 src/css/rules/document.zig create mode 100644 src/css/rules/font_face.zig create mode 100644 src/css/rules/font_palette_values.zig create mode 100644 src/css/rules/import.zig create mode 100644 src/css/rules/keyframes.zig create mode 100644 src/css/rules/layer.zig create mode 100644 src/css/rules/media.zig create mode 100644 src/css/rules/namespace.zig create mode 100644 src/css/rules/nesting.zig create mode 100644 src/css/rules/page.zig create mode 100644 src/css/rules/property.zig create mode 100644 src/css/rules/rules.zig create mode 100644 src/css/rules/scope.zig create mode 100644 src/css/rules/starting_style.zig create mode 100644 src/css/rules/style.zig create mode 100644 src/css/rules/supports.zig create mode 100644 src/css/rules/unknown.zig create mode 100644 src/css/rules/viewport.zig create mode 100644 src/css/selectors/builder.zig create mode 100644 src/css/selectors/parser.zig create mode 100644 src/css/selectors/selector.zig create mode 100644 src/css/sourcemap.zig create mode 100644 src/css/targets.zig create mode 100644 src/css/values/alpha.zig create mode 100644 src/css/values/angle.zig create mode 100644 src/css/values/calc.zig create mode 100644 src/css/values/color.zig create mode 100644 src/css/values/color_generated.zig create mode 100644 src/css/values/color_js.zig create mode 100644 src/css/values/color_via.ts create mode 100644 src/css/values/css_string.zig create mode 100644 src/css/values/easing.zig create mode 100644 src/css/values/gradient.zig create mode 100644 src/css/values/ident.zig create mode 100644 src/css/values/image.zig create mode 100644 src/css/values/length.zig create mode 100644 src/css/values/number.zig create mode 100644 src/css/values/percentage.zig create mode 100644 src/css/values/position.zig create mode 100644 src/css/values/ratio.zig create mode 100644 src/css/values/rect.zig create mode 100644 src/css/values/resolution.zig create mode 100644 src/css/values/size.zig create mode 100644 src/css/values/syntax.zig create mode 100644 src/css/values/time.zig create mode 100644 src/css/values/url.zig create mode 100644 src/css/values/values.zig create mode 100644 src/css/writer.zig create mode 100644 test/js/bun/css/__snapshots__/color.test.ts.snap create mode 100644 test/js/bun/css/color.test.ts create mode 100644 test/js/bun/css/css.test.ts create mode 100644 test/js/bun/css/dedent.ts create mode 100644 test/js/bun/css/util.ts diff --git a/bench/bun.lockb b/bench/bun.lockb index 679ce8aba1293d97e0f346c0c8f47166adddff39..6704d645425760bf1f9bd0007a1c72648610365c 100755 GIT binary patch delta 12206 zcmeHNcU)B0wmxTMkU}|vn%S2;C1Q`UWQLwWm zM8&A6v3H|lj2aW8Mx$Q!MvPvADLyqZ?_1~05$?O@-uu`4EBnW{zrFTayRBW$nZusC z)@jBrrv>`pvbj-_Lp-KTY%q3SeYXSSp7sd7>{;0MT>2-Qw9P)~_u;&!I#J+ou-NLS z>b}1?znvyrB?xYUV1K{(s9*b;4xte2ip-Rp%nY`F3a~uSNf7FQ-vP61R11O^I1tSF zITRooyRWP%Ywv(H$Q_xJn3S9=2!mbZ{59a($bSN6{X*E)0^4#!N2h0w6iSfEwNItB zV#6XmlJy`x26IQ_VCn(RK`Br0C|izHtR|FPTS`9nSP)WjLzDCKauW9=pUb7?W{yp< zr6+HLz^x^vCZ>;p-46ikZ-BX1UxHab90Kw?*pDNF8{FyOmJp;4K;~|yfVl!M^oHfZ z4(8smbAF+0m0wZ4y7Dkwz+8Ri*yKE0MzWA$%djPdri}d!{^MG*67y0+$0Qg0=#ak$ z^RTXg-N6^Y+{#HX*E2rVmXwMfe1WF8SIPBdJEfjJ(4ic8)X)^&Zzu>*+b@IB4f_cP z?*yZ>_75F$F&Nd`3&HGq8<@-Wcd!ZE0CG!*-W$yJu3)a`Ih4%zz+CPU80`(TA4Y;* zvK7n)mO2b($OiUoF#haGndzB1==j0LvQz5$$#ato=0R(~?1XL5yVJoMO?%u85Cm`J zlskAQm}^=LZUim_>%hqlP5^5mM}yI8dlNAC%r$^QoVpZk36edu2F$avF<6$TgPTAe z1NH$IL(g94b(@%;E4&SvXK7eVK?nr52Xpg*V78y?uy+Qt19pbWo^sa9v+P)uMZz*< z@M=s=%neORPaHiRGWTyHm^~nk9ISXkjRr6TdqY&ru20KN&P{>`zX_Aax6UAse4JXp-8YFjgSpDfPJ9|f`S|HjFNj=keHrs%gqy$$_QvCl>Z25Y|tL!Q}1 zv2uZ-$cNYL@nG&y_c(bb>jsuJasEzpxw5Fe&#(D&dhBq?62n&9@So9lhkDCC_1g7L zXFlFu(`&fb#EK4|{5p2gR4><$nqGW!(}nIouf8`jyII?IwLD_VvNU^C;!Fy5Gpj_A z(%ej9S1NHciv?7Hcn<};o5kPA=5E%Qa7MO6`!y)r%_y#;3TP@QxVBjgARFR-RDyUG zRUj^>U=Op(*@@CTOky;Zc$mf2RN-OPJitL3FIVDj6fI=)G;5aQFztankt*GcntPCX zLUN+*XvRRnb#EMbxch`XhWqQapK2RQrE0` z$TCb-wB5s~3BIx zf#ggE52I+NV69nQh`ewLtsAG=h7^~?ta%wRUlm%j7)Zf7vp9xqh__IQ&aC+XTY+n= zCH21z1$&#_3$V}GPa<}eGu^J$lKkCV)Vq+|j!L~diZ!`C%_wX>wsPjasMkR1BaQh- zq@4Z-L*jAE{_@AU%_C4t9vT7Zl}^2cb@T{6zJe6{SgyS;_GbsFOeOr-9}b!-FZsI*zU6Hed$ zl;Izz?yJR6rPS(IsYgilkn)T;y=CpYNcEKRE+Pf@qHKV-R7=x%Cpm$vGe`}Tlzuqy zWV1}93zZbtVrka!w+(P%Jf(xTA>;PJ(qOrY#KMoktZb}b}NY(PlyT!IYVM$H3A>`-}W zxAIrEF?6aFe+mgSsaE+@8t5y3+7xI~zr>OFCY1)psRuSuQd^PgBjvqBYM7K7(v-c! zb~|3BT$=H~IBy72Z%C=FNcEIb&lRnzS93}WHmMdir%j+ef%G8Qq`8LYLnL~NZHEP} zZSiV>xEWPFTF|BtlV*k^(}_<^&0a{Yq@m$7QQd1nX)R4ELojV>X;SaQhQmD=~kb{|)^f-|P6@Yn>!oOoYqXjre z3a?>rfeqktsY2{^DKLwa+{RDpUI&%{IA03P+R%1u||El=EhW~|(7f)dk-~nuK*uRdsqaQi+ zMM{F1=d#41cpdWy${hOY%oXkgn0Eo}flmRhZy&(<`yKKjFb8Jn1BIhVu)#5aLv`l- zh5rCJFms0<1FWwExI<4J@-qj&0LuZ#^j{~l!xvq^?6tp6 zX4xT04!=}qUTrA=ud>k&e%*--mB9Iboy_csS10seC-YIbgBQ;+_6tIF=KSNpUneu1 zPU^&1^?$!>ZpKe%M1&j9emcjlh&Ge z`r(UR#{LN}D#QI-{jGe$?|<)grKjm*xKtEgdb|Y3(cU+$lsd?r8V$0DZOJysO4`Bh zbPkf4bc3yQ8q(y!7JLIb11bM4cM5&WB6g&Lx2)7+h&$bY)R}^ZSm_F+xkD`YwssZL z%%SeocBn<{PP2wuDQ1{EJ%VJR*kM+>4{7Bvi#!TiAfN_I+BQSwMNCiJ{&b-_K9y>9W2_R zS)up*`1qG?Y%t>qcaMQzfrB?^y(EC^iOmBB{_Njw(Du1|1hC-RMU3( z?mfOK@$UNMV$vVuj`ytWxcZl*c%KhCKmR^;&#?1-kBIBu`eyRk4Zl6PSvyrVd{DJ| zGnDrnuHPx|;i2ob7TqY_7PY%;lRLT%M*sK5%<1=HO6wQPvx_Ikjyt~Y)XiIu!w0Tv zU(|KAL*MYj=WQ>@J@=1?)>T&h5vNA1(B;@h+o@i?kPJf6Cbv5NV$7I6VRLp*`{rd!2{ zRDyUCi5XULG7ZYWj?2RTJ2EU{5oyL^$3e;%YZ0f=UP$@b*qfOaaT?h&vEy>EHzB=C zx-9HCNRzWH;tVT)GNr$=leQxfXFg z&C11&8;89KshDE(u;U=D%(I9@6_7TL$KHI~B7Q(i-?q}5`PiF~7E||eR&pu8-W+EU zm(nvxdms%RZxNSM$#|S76Wqx?-y*K0LHQWpM2rv8YSI*7e2_8hTiZDJ%n<;oQ#s_KcWQ$lz zS0OF2V|;}c@e`U=i1AIq_#kbk*dmM%(#j$WzHwJT+Bg;Cvs=VnwA5}DcT*+ePpSJ9 zEB-667V%zshIk+KooW^LQwib&Bu>l3>}5~$^E6;};HQc`4(tZT2V^gNKz7oMPac_- zIZ6;V)6r@5TpmL033XZJ^d!~786U7o*_G*&RP>|6Ua9B?CC*B6(KnF)9VFTBnBgvR zfo8eM8RL?31mSFu{NE{lE9HNGMuBU$kUK(^(@(phHQ@k0h~PDgo9rePC%5@7F8{#i z4-NOTa{7;4X9MN`JW1&`eOj*krt2MD7Pug$*^U`j(RBg;m_qP9K&d<9C#$hkOd1^rU?WX*u-0Hcn2^6{HdUjp6%+bo`}>WO4#k!d^l1g03#3s;ENc4dlc{sO8Rut zA3T7HcN>cuBjpG1ue$pHkL)vme;$4dTnG3k4y1Acg#O1d{eiKpDgp`to+|7VAroi>*nob(8vysq z8)y%-1LDdO_tX;yn33dJjtAf`f!BBfm|euKVwdr7l;N>&*~eV~?rC3uUE2$A0@&55 zbaHRMM*KWV12TXyK)NHH4bGzCef5g+ks1$-1KtMm0BlBSH*E*=g6G}AyJi=_!z=@e zffc~Vzz^-UX%sQvseLJMa!L3z!Ma;Odl$mLp?6z)OEFz?E?&Tmf$l zPBXXxW-h1LEJT_sd>Aj?=}1I`K>T;h@gv2CQns zs^j&>ZnxLf@L(&wK_8AULgaU7x%ia!BMv9);n89t9X_nXf5fjIju77`_amcya-8J< z*c*?|*}bUa?ei!di6+9ZT4??eosV)!$Xy@V;r1_``a1N6FiGFQ3C#r3jUzhINKcO} z_fhWg%o+9m_5;_>+=O{Js)N;3Dm|(b7t*Pt5qLXzf%p!!KBkLO?h&>9>B#nS&!|+C zFiK+-l$%9PZ-)0j=e+48G*PnV2~9p0f%k#!$8@5e&K^sM`qy^3*(kV1xoI?`X7H+n zbvr&rE$p6H{tc9HT<4?QG}?N7|HFXXR^HG%9I%b19FM@$|NL>C97k<)lONDAJ<9(P zzT7+CiERz~XhCq?FS2Vg<3D@QciDcGSQw=bkJd*DEveB7o~pjD;$LXY37s#$8F*pZ z`hT+eVk^&E_oaSGj`#AUH7B%D%FUy};)-v(JYAgygJ^m9%2liZBOdIm{yD zYO-nWXF4C{Hc;8>x!#5eqm{9~CJqrK6we#I5u#tdzS%k*yZJXxqBvX_eI_ z@;If7zSls0ues3vNn?w8QvE8uay@Co=4Mmg$Zh+mO5@s)hMm$zEBBdd9O-83mX^|| z%0RjClxqv>Kj&xRQkBNkkhYxC`Y2bV%D!z=-{pEt-6{hc-8`j>Qm#}z7$J^V&G}+f zm0r1YHSg?<1mogcTdOo5Qs`-&k8(+C?e3ciu9>d6Rr(W@cv>f3rirI@udaBx|B~Ch zSDnn&RaVN4A?2!zWAY4Dx2%*)GSFbP^Ry|qi zgPDt)F5Gyo%0RizwW{>H^yec6Y^c&C2T;#5THiv{t;5!7#tL2GAF0w5n^(Y0FnyU*$s651W%Wtom~G zZVcO~H^gA8*9|1!a;>j&VQJF)G1)aUmfo&17#c`@%C)N5fs|GruBr^ARgj#6=)KRi zzRHEJAJQgH*R+Yogz|$TOm7r|g6JWtm2Yeb_CXWYAN-~McUU*Ffx$33i2ToLqm?^p z9v!=U>(Ta6jACFEl-p<>dJpKGFqaNhX^Mkr>{+d9O%OG{s1t+e!?T@Khl1$-+5W0A z!DRYetC}867cOXhm5Xuhy*AstKN&a?RXC1>|JZ+ZOFHtoHd?tFSKi=xeOHglucb~H zc=;)p=i2O@)Bi>NQ9EP}dvsV!s(nuD`)4CFM4;onE@)}+xkw(vf^*VPK)$aXGG@sT zCfRv8bHk3V|5^1$L9X6cxxIHdJLuboMLoQ#ItcHK_#)g_xz=}o<7|)1mz}d<;CNC@ z)YH~4w7$xp5v5N{gjo4TsZJ^1S*g7YB!3{ZZd*jrS}x=-3ZOBWMM>EKj1ckp5%! zJV88CiTEu-nlP>ENhGCT{CC^WDT>xy)T%tAXy3&YU*-N`#KNQrM^~x!6;j4Owo&d``rU$PzKL5_O9%GRjz^DE@^%Le3ll)QoBoz z!zNr=)bW9|@lv=-*8LlgJ35 zB0`@VbpNY%uF@@Oh>vv?{`C^qJlog;>0LFvto&=OhfkH+3Q%*9f_~(pJet3m*CAfs1n3I!OV9QOVqu({w zDgugBQ-y}n?Q2b?UM*fwLu^3dSNvIOjJM{)mz$Pt{%MQ{ow_XbJBW;TC4^;cHb{e3cT*6+K{nIrsu*ZSk$?q9d_neSd}?Y;Ke zYp=b}KKsnsOZK@etae#t3>k2x__TL#)8%9DzWKJt*Tecx7}YtYN;Ci4`nkyo`J;*l z{NN!9eC=Ot_m?K@U0s-<4VDDKT@cEatv>GGx5OzFfVv?wJuf$#_3r>IKjR_@UZ8V9 zSq{?(f;XriDCcKTSJ6C#S}LV=pjzZk&U2)uq=E>C_AfK(J7qZ8Kke(lwR#=ed z*n>)3Z(4qCPI_im+ExhMT55(PYdZ8A0WSXpl)b7288aL0s7VcR|@~ZcbW3W_Fs8otd4P8kU~3ANg!$hNB=O zYXo-{N zfO4?LJ7pUv43))#vUdTXT(6Cj-s>O;_?6v6!sVZWvi|#^Y-g{NR)KQ86`*LZdsz_} z?tBI)R~YS7=&LA{^#tue|7Z~~^m~6n@PWo9Cp`v=!m=%(oj{j?8bD_{X%eU&az9Xb zS!M*~mOA*;WS4=Z2LhEIzYfar+#96GYd{}^JO|Vlv=ZeUFP>?RtbAbsWRCB&5V#MT z2+GZyKw1BJr+!;d?$EKGN(9=5D&ymvMy0}5WbpjSaO8)jXE~;>fz19b0p$UECroKb zXH*8#3k`HdegF(}NT=ne<)>mm@1vai_hB!k|2sk15o5TLzYa2oJRkMAy)mHvLa9*p zWrU(w3+ji0NEC1bk&%k0MUJej%=`i&2(ml!gQA=P2j!WC<{ep%DQP~CIU=s0-k|rL z`a(BB=nT0%WS(0OAhRQ~!_i>YAp_2reHEaL>_M|qArtxNeA!@7b||a2GLoIU)g-%K z6KfJ$%@AvDXb(tY301gTrJn?Yz z>`QgXTdfg` z*J~-e(o-u47P+jtjakg3I&Z7Eoeb@*nwu!GQl(d%Xr_wxR_!zlWDGRX1!%rRh7MNE zUF0QDgDy^+fO$AjF0FPqYoCQ=g#_Q)nKd6kilt7_a>eA}wq4}56Cm;6`O8u^B#d5J zYifkgMLIaT8aWm|?V6QoMnOuT6hw0kIF6=b{WPS0avcXE)&{p1&j7`bp^&)cmK-7N zHb^E&63-P)9i)M>y-?h8Y(cS?1BpFqL0#LMwOgHK8unKDu|4^8wP@bLVlsw0b&b=s z#_~B_=8_(A+rSN_PJ!_*53{UTb%)7n%fXG1xzE5UMtb?E+#+z}I8HP9A@?>oOcUg}VF{Wba~a^q$=o{+Iq%1~ zan{QKH%jK-1~*LRT(PprrBZSLZR}~$yoij!R2d#Gb|arqi}ow5xl!;A_WEnh){F8peYzwGF0ZqeUtV zri~!ygQ*^5RS5a?vPcafl-$dr@#!H5In>ZIPE!tUJf%DyFZLjxaEm6sC(p%(usBUI zxB;@xd2s!h)B2)yj;B1QrLm!u9AT06hSJ6ei}oJMVo|2dwQwv0BOr;qUPxtOlx(s{ zr^09>NJk^pgN!zkPozb9%Sg!}KD}sTq(wUn53Hf6Cs3oO+3Px_Ct1SP0`Q`d?m@K5 zy_NJtI3-6}v>R}rvN{$?+<>1$axPhI%$i_4+N{)IjFYBDP;#_I`YnPsMq4y?L^)aR z*dB0GxU<^$NTp*~P;qO_hQv3re4|LOM^b%^MLQfrH5QpzEwMW9hZL_!xHo@*r1YJu z_lOpR{wR}JQ_7AepID1@G@6oQEgDzY#e_oSr+^zoDUos7o#5DQNnRW7LQ*0CTbejL z7E5IP%izXQWptb-3{5FEmV+ynOIu>OOhaemUBF-_qJc}`W-})Zdz_Nv9#6tz$h#RC zxH065sR&?yBwx)b=XU|{SQ0S1WW8WezL;`3_9cQqXW}}RGK0w^j}r!8F2w_0kN}K} zeEku{6I4Lp{7mrN&V#@gl zx|}~q&c$#vqik)kQ*KUq8rq!sOxaGNQ+@=M!y+rNK|DF+t2t!@BLFTR32*~roHFK{ zeEkvSdgA~*+=YpQ6NI-|R^gX6ab!hWsT?xBo9_OW1xM;Crmn*-kU6Oa;#1IKSvrXij+?s-5|d zqTGR<&hqAz?Y#~#-39QJ+6}P1zX6q$?t5BBT1tdyI=tx-!o~5lIodejyc_+O9>Iz(T%2z=7V(J211=!McfUo}vZk$IAPSjer{GCCS~1Cn|8eRdU4hicVH0h%)M2M` zhmP(*N~GSC?G!s%N0pOp;*)e6(k)1dsWx#WRixT!L#mD*KpIU$rr56yGXNX zNUmMXp=zYL^fS^KG;)SroJre}=8-tlF6L7b(gNCpbQWpz?BZ;4Ae}=8krt9yzFjP$ zOr&$^IMR7!D6osgG!yB3I#Gae$wRbf+3=rO(JYKhKB5h20fo%QxD+7Tvu)xcs)JMu zsm~mnxR{pC!MMyqv>`2_-h~*K*@$+bO=@z7vc{Xu1ZJ&p6DbkUy*d`Js729d>Tpb;Ow3f8rrm@Xy+9NLL^wK4TMiQpGdq z-$L{c(k>dZ82wv>{w=nNyXj|0w;-iFYZLd-_Gi(*XVAYTHnE11mY{!&(LYH0NL!w( z$-`>wPL1Uq@xJS#4Z?l`+rP31sxA*|UkypeZ;a5Hksnr&mzE}cUei!ss`(e?^)&za zRLw1H*IU;#mj5Qv_%*Ezm+`LyHZpQu{>%{#8$R7(9c`U*9c7BybI@K%A` zAK7RJ^`GF>tW4vyeC5g8C#vS{wXxmYUfPRlxLo#;XM;CQJrrE}>u<`%*?Rysyov=} zs?6^5s|#Q1K9zR}Ps=&HXI1L)rjX0|;+EJ}_&%1<y@ekzS#sGMC=RXMePfhtHZxrZgfOnLhwC9Mq^g6g3 z0Qd79-~#Xo@G0;SPz#&@P6DTZSAbW69l(pgO8~!H@Lu*Cpby{xCIb_JzCb^~3d8|p z0Nz0H?hEg?rNR&}LxFI>1VjNnfSv&U+L1T6yy0yGJ_p_fcno;gUI)|zp8>q9w*YCt z6F`3;0T=`r0Zb~P3(ysK3AIiwVE190HC2slX&) zFA(mW1-~4k?uQ?XV8051IlydSI*!~`+!Lz*;z;nQAfN#0yft9Si1`GjQaXqjO z*aTDn8-R^YzPb18P!;eZzz%T(&CA&qV<*6o+5%Jq+)0ko%fNQvHCEaN@C~yScoksf zT>v+(I`SrX4m(%oIvjT9Ill_vvKoNPIA7IOY4~Ae;=1;!Lfzt@HpQgOilwag=CzWP zJb}J@?@4;<{r%B%*#xe8E(^cV92nJG5+|6ACS#=184nE3Ag_a6UfkWr7bOu9#&BbV zaF9|yF!d9Xdy0fH7IUU{Ys{>eu*>0V82Didzg9SjFJ0 zezDIRdhpxOD`7jiNtB1>KIMfy zhwD#VmqZgPL?akC>F7yAwECq#&VTTPxod{noUNM#LH*YM#??dLbj=U-K?(N`-ftz{ zsYpDE`<^l=sjvFIKX&NJZ%vzg3SC$?!iXh&EUi3c@KrzOZ|`0;^u(9@wn<_S9z|TW z)Br1T8nYYY$3yhZl;p7u@y7AG5paLa67BTe6q3FK0a{^t9erJr|HH zZvJfGPgOIZ5TgX9jwYTqM5|x)N0ek8zPhL8b(C}9FmKdv{=s*zeV5y?dBD2GQJh8@&++92+y|2iEhz3WBcXY|q;FB*C#T`Z?PXA(t! z`t6J%TK*W1PVF3+nUZG>+AN8fAyLNYD4}l$z_ z<~P_ut!tXfi)g`F1NMQNprxJ;IN$e&&NfYP`zEcubndJnMmJTQQ@MKH zpvAGlnS-aLk7z2n*?~Hr)BCCi6b7Bmd$!Gwt4f*_+R(^z1~Gu9pELNX#~c1WNt`1s zKRcyK%SKh_4AJUwh=4vr9&2rhd9A5jJup$d|9YZZu3LUnNeSJ7W%YnX)+{gK=Ip(V zP313B*YgItbUsSL$prTcdO!7`!uH;4XN)rZP$h{Gm^hJ`=l%RC;etL!J=yT$u4`HM zCyjlzNkKjPFxLI{;gPKZ~`P1{Ly2W2Pt+fxTdTUXW!ZDO!5X*vYpE|Rs zUE;E)l1u({6>h2LFZOOadim;CTQ)W+d=)^RAL}Kd8@*Ad_v_qE-ZYfujrr!@k_Rzk z<>`s#DMlFFjn03j_ftRbuise~;(e(1y(Wcq-Du6nCO`F{fRO&q`*&8&yb1*q?m$e_ zwt;j5Ed>P9*-!O;>Vbu*;uRZyDE`|u*^nvRXcoo<3!6t|D=Q$6M3b1(bOcH3j-KN(A>CI2{(t$ua`N;t3WFUEdI>t{u z91{4!*3m(}(_Ccrh~CC1VNDQ~!8P?XNnh`snLfM6&4U83RsY@v$t#3bUD5lgheu9! zxZmE*oN(xKf~!-C5PA6IH}w12+@ zeg5X6-tW;0z2T%Y%nyU)jKQ&WP*MPwhBAv2YoBPM`JaE4wPSgFZWXr0rPK@6lx zb^1RT3iSlchkosTt-S4_SB>MAR_+<~#VU8hIggU^Ve6p~Wi)Y2A|vVZy7Um2Sae)Mr#-OqBb>k9+cd$q2`wcy+_T6J15%IeDq+thb}cYD>pAJH8*E=T3!JyxaPZl er4A=ZmOs$1pRN { + if (typeof Bun !== "undefined") { + bench("Bun.color()", () => { + Bun.color(input, "css"); + }); + } + + bench("color", () => { + Color(input).hex(); + }); + + bench("'tinycolor2'", () => { + tinycolor(input).toHexString(); + }); + }); +} + +await run(); diff --git a/build.zig b/build.zig index 064f47852e660..776585f3e896d 100644 --- a/build.zig +++ b/build.zig @@ -368,6 +368,7 @@ pub fn addBunObject(b: *Build, opts: *BunBuildOptions) *Compile { .root_source_file = switch (opts.os) { .wasm => b.path("root_wasm.zig"), else => b.path("root.zig"), + // else => b.path("root_css.zig"), }, .target = opts.target, .optimize = opts.optimize, diff --git a/bun.lockb b/bun.lockb index 9e6b62e3f28c6ceb980936c823974d488d13fec7..4ccfae2715a8e4f8b3965953513a6d47f1dc6aab 100755 GIT binary patch delta 36616 zcmeHwcU)E1*6rC_k8)5H3knJX7OeCxctAxI5PMB*h^Qz{z(Oowj0KD>TeH-}L}TnF zY7$Fgj3q|FZelMniN+E&8cWnfePiqbT(9Qd_ulvYm-AyV*Bq+tNHiP~eW~h9kNr=VSFUiV=eWD07G9Q3{9KsR(>3n; zDM?jKrhy~V)KEPTwkA_v4M;mkJ4g#;W>P|8Hd6cvy(07iltTV@J^t35T{UxWl`)wr zfXvQHNgjyAv&w3P4NA}UOiaj1N=sLTX{1@>-$Xnsc?#VgG6y;fO@m}PgY^8pbv+W2 zmA8jvzQXcaIYC;w+`QD}?1bzDlj#&HuYm+fXh<5GosTXxfIb|u4rIJ8ub{SS&;y`5 zLApU!hpec}M~J8H3dx3jgjy>@maC*y{0KUNd4rM$jvQjjHJMVgR4bagBEkU?Mg=F3 zn>{>?+$cM5cv8+Nri=o~o(@h)$j(k0WZJ8z8=R1pJp%EWNh7n82j`gNDq2IogQNvq zt7;9|1D)l5&U^s~JVyk3G%U+ACm}V(l#n?jBOx;@=^l7yY>e(uIz!TgTa0M-NR-9E#eLvy#RnL2f}j>nW(MwKXFld#LB|q}-fn zWMIZ{UFJJ$x}UB$a?z}kk~E6FG1Y<2^c8gJ0Ewg+wxkTtTHuv_YODGfZ}_ zT7zX>W_YHgCnjVgaY}M(a@lN??55l(Qz!Snn`VHa30a;P!6Db7v!mPHwFXzHtJRy7 zHE?8d${9GA(MV^)M|V zEj@XV$+QAGTRanzLpKSs0^~4A7O>pYSeS_kDG7s;7C`4ToD9i)AD{wCFK^8Plc95A z8Vh;3kI97DkXO~$Wa1Rgn+1XLyLeU(BrePCunViNqsWa#7%Kyn$mG}q!&lQM@Sp##Qp8I+uIapgDnXe{f9Y~`BV=!K24$Ta?*fjK(=`AX5P_x$BbaRE|s5gn!reRc+ zHrii6+Jj#MSsBs?l7sB5%ir5*gIx(aE!I0in@!c*YUA@~G-gXKBGNKaQ__c+Oj3-N z;ch#v;IE;x;8w9(0Rg(qL_B-$2AvfqXC-82Cghl^L#HJ+BOizI3er&@k}z@zt@2rW zts^TT$#;r3nL2PfeuV&g*jkq_da6apzzW~dw86w;;M}JsHOX_(KvOn! z`h#T55bX4+L!h(yy&+-fu)M6Sp&7$b(C8joMQM;UNK#^==di4SX#;hhVOBUKDLW$} zE6a4cyVAts5xb?ARgL2@3!f2WT| zyOsW0x1V4>v3a*3X@;}`TJ!Yj6PIVqn?yt~p|NtVVFI!rnia>GVg_pQEg)IpzyKUh z@bQPvboUaJH*G!S=t0VATaVo5Sf^aK6$WeT?hd9Ho!uemY-SENa}H=KW;WMzKS*}7 z3nWWVOi3P@jQEK|wQlG5hNy#)otnW)P3KS^x0Ufo#8wZ>N>AhALZ9uqrffve{BwtC z)4?)aYfuXGs^F(W(jNAhT`ZtHB-2j-PdOw#L!FKfpmQ1RLxq$b(F}Ir1SA`J5R&Or zlhd%DW@WP>fjmBCC8dr^$~2i;AptAugod++5s>V00QgFf{v)&o9Dq52&UHFYm&5e5wyf{y~6#Fw$R^$WcullY;Z0lO-t*sf?kl!w|RobcYw}(A&|`H4#|dB zg=BiKJlLN-GJ&WH`72Dv7N6ARw~(x89waB_cu4lFE{25ZD??U;)Q-X?)BRK}U1m~N zddjFI)8$Fp-gy?14H=~Gtrw7v4e_6h(pW$t2u_-{kl4=hK1fw=*hS~s73ler2V*&y zOgE-zLzg%-AqCUabl*)I5#uc8hDun>b?_Xy7tdtpke?kt0LgwC7c?&`FCi_fN3v1S zU93_X%5j#~zNa&_j$Vdz0)G&ahWSjFiy+y-sgSh%2weuF(T<-!t&~-EsQ){OZ|qvM zc>F`p4SAh@xiGpC5sL*K5+@BkOoSFW24G z%G7PUv!592&@?lB{*lTPrnMKHep|BQwf*Dz{FSojY5ePB`fqXEd;aLrXP18sOGqg6 z>tOjbqncZ}Pv<*bD`V5*UHdK@?k~wz@?6@LrFSpm^pnc*5H9a3UkBP=#$OeXl5yt2xi`oU73Qs^2XZB;C85tc`Yv1&1K zAxgemgmgeDbVHG5lPN?w|UuEe9h zI3=cjgjAs9GSj7xzK}Z5yq4j%|m?jajLlvcDX< zQnpgqG{W*J)-$bHhC?e?t)|2_vs%)yHd(Sd-j7u*aMLtZ7dlS$A5q1meQYN=A_6(O5yD7M~K$zO@_ zju4QNJ15VtqZHM*S`O+Ytbt}Yx}qcMljnllt2lHHp;$Ft0}uKh z=1naQeO?@TS{!QA&`39o#os3EQJSZ%LA8(-Sbg`+@sHtWnTSS!wsw)#+8Y+=vE%u1uGPBVm8mke= zV*VX5;asM6;c!J~O&%oW4Rd*jTEBe7OR;TfHMjOQ znc@_?mSOTtZ>6B6RqpJg6hZ9rQEXdTEndD_UUh<*bD?#lXR{nes2$4GR!R*lJFbE< zYG1lQQ~Rqfu~bwge}Ct&eCwrhHFmnqT40GfUUtRRe=9`=GHr zq;wB9{|OCiuTGe`DM+})Mq%>s0HvUfRbCsQ6hT}KP;8^Ea_vAR7UK0l_QyOM?hiX3 znqGtutI}M7jOEi$nb9)LoPtm{HMA3sDo7=Z&hqNTID@fB^IJ=gi-*}B2p>pXf%=vIxoz#S95K()v^7ptW= zEY?NsB=`5CD8;s`Rj$@XiS3HGHrg75k9H51SG7@!x?0T^3@Yq^gF=6VXm5$5A}@_r z3c6Y4khV$@#DcbpZFj4ABNjn`>TDfjkWEdD^(d#sC`H|^^5Gc8wujZyDb^^6Tie`X zjl0=tXeC2cBTj3P=8OkJ!(8Od#}K~{O`D9cgXKIlT1AVi(O!$g9K)dWXs^WfvRZb5 zgmK_vSW%gHBm1%B5F03ZYq1d(LDP;D6+38IP%6Ai2WXsrSl2iP73i9_3x5lZ?Z!Ng z3AU8&Xv}e@Yz2+^B&7-#iUN)8#&!ekA~e>lEW7 zlmd{?KysyOB?R@qb{-|9EID3}^Oe_;+Y@R+t0u=zE}rD)D{8Aa zo_(~^(GJ*u2sDlcrUKe=0vhVkr;m4EtrAQ$EQuk|m{L1UZ-&M;s%I+m9cbO?Q_X4p zOr`3X-kn{{1-zd7fO15ZjF9k4vPV`YW-+tQPkH8mZY~3^Y!3 zv=kfFXV6$bHes~p2{hdtu(UP77$FumEnSK zMTx!y}Mr$1n%cG0NTW!VmmvmY8tsXNLT5o#-- z>GKnw?8g!frPLm+9VIb$p(R4&Ji$O9C<6lS)410$`51|rl&Ig}A*`3xbgBUL(w*g&B*(4ICS zHe*XQP98T_vCXrZeRAZ2LM|Ncn+D6r#woTFt(F?&wbiZ}s0%dB zl$?VNoZ8tx!82s3+4KwDI2m5mUK)XcOSn&uDac;sB8;DQYA3N28vb+mZ$v1n~LoetHoxLHeWH1y9CS4Cn*I}tn$)HN)h79yk#;CR?EV& zvLr*JE7ZET3>vMjd87-_v_XY;vqVoemMUk~Txi;wqUXB|ZJ=@}C`^u?q7+TD%I{85 zZ1b)1hN((yzSTT$nhE#c>=MH)#}NuZ1=!q11k2^}725)ROI_BM z6li+WF$&wES=GXLmU%K=v7KSHguh)}olmf33^Z+Gaue9`wo-&71Yu^#zzKfV$%Hj%z4mTj*@0E?Yw~H7sUg zJWQ>AD9PjIfT^xBIgL#zC|o0S$d|fzf3Yd3t+wQ zvbk!3aH8s`v}A%Y05ibHsUJ%6a8oJ|*Q9<*OA8n*9rZ)W{8KblmCTP7qkg9GJ0&K- z`cOYFlk7QWtC|iIP5n@o1u!Yp&&wpseMgUfrG%eQdo)*1NXd-zbe)p=ySfxQPgzDu zom(@PvG42b%Ov9$>G721fTg-lNqZ~TPr_XVKpAxCnUy3M6z8|z93ZfNJ#vcTJhJ*WiG$#H+4Y*wy`@T z3wT}6K*{MpP}g53nJ)?PltcA&r6oI-qVxYQ;SjjsM<4+!9u3K!jL~HdB;zMQ^7Asu z^gPzF0dGQ9g`5k?is$S3UM87ufqEWfKri5ZNJcEu^~Jhe3W<9brelz7&`C(9JEhCh zko-`RKcnlEY{&&j=KD#H|5=y0eEOUju0Z0)bXC`{QNf3jmbtC-zv=n|NEY}6l0AQ_ zODfdM;1??{uj{suNMfp}^Y)s|HC5LMM@W7snbBF-DcL|5UDnZgO7gCdEZ7~AQT6eQ z`5Wnc6P@?ed2XEeF$LfkeVHjxCqi@?rc0|XTSC&~#z3;7I7n8|5t0q)q|45b_%U_U zWlt*jl$K223q14pf#il{>Z=n8kTmrWU8X`-f<6|K6_1DHr?g~&6ZH5zJ)V;ML|uQG zWYk+sCn;ZjsGSa{=-H;~*-A@N1v*d3k)NgOl&o&Hu2a%p^L72dkhH?9Gcb-j3BgZk z$vXc3FZ@`_KUaPn#DA{*{<-qQG}sUD^FJW9wf#R(&?_7C&z0Z5UIB6)|8wQ1Zl(WR z`TcX{_s^9d4v+K+|6KW%z7oW~#8d1)SAPFo`TcX{r#XnySA0BeQRk)6|KgQj4c7DT zmx`a}tUoVDURgFLyOH;=Ge;z^AJ@OI!@i)Q(=r-lm)Sj~*ROwlb3gO*6QP67JZ^gB zr>ZqX#ESg4mW8eSxZ1p$voFM~tGz#B_`0K?&f9p@Gq+FD>~5|;4t6{4pUfE2x7^+! z`4^`Ehfh}D{pRCO;s$Kr-|MYiN1mE?^bTn{=3W`|(08+*bpNy0$M*&_IM?QI)3oR; zxK7FRtnxERe4SbRDcg6(wgc~X`TDQ2AFbU}IPze%JoAL;c1NFXIkF_>!Zg46p+z2% zQ<~>ga5`T9uH}=^&dnFbmOsCy;UAqU?a10*{cG0;HrwPt6hYl`S{z8;|ih^z2?o_hlo_pG~j7@ZlfX z{VqIpIuqZtN6ORXlNOe1;pP0!rlMK7Un~fyv(bL6vhI>&ZchE=<-wKR*K~H<`FYo6 z4fCGYbU65zuX#$#b0d9yI^8^(xA4K93HL^%J(%}{%cg4$qdM<@Gj``byK=8jo-*;S z)QC5m4cjTjmP6~l|E|@iYu2xO?2vlp+aJ9x4iTxR&TPG$w<7Gycb;!Z-TdseysW8Uib{I=hoo;SGiCS}6Aw_ACg4|)53 zEWiIST4#L!r`m4t&L6(HJ#y!Ono-fASJqz{e*E}|VHsb4KcentQ@L&FA)S(3?=P{w zam#7r==GhZo@%soUjCjDt#5=k2rAqWu+{%a=2*q;)>ZjFa%jbm&+ILGs?3L{L(5JL9~N3+Z}Yces(=4Q;LZH+I51w^9RW0+)tt&G=xVt~H?%?Yoac4f9dPw=|Cr7cjt`rx)p!ArGKGCnQwQ9r7 z{hBWk*Xu<2$8X;K&a<4vYl}Bouf+EFaOm!iJyvdvDm%H%!&7gMzL4y*FSC>1?;cN% zH|lnEXm+=)$CcEd9ox^_&}YYS%cT$2#!Y|yir@7UJ1P|3S>)Hg>XyJG`)wR121IVW zF(&QFPN&5kA~wf-UdGO2?(7Fmru_KD$>v|!C07`Ht5m1oQ+of0${A=YuiGgtuEa^}l>93_m0maO zlv~g~Q9^&|so4B#r!4vkBTo8K>3swJ`wjhr_LXA!75#&j`fHr@wXzM` zv^(hE%{Xb7l6({WyNmuo+oL$$LjUeoRL0$EFYQ$dq0Rpt{kt6}?Nf4Yqks3%KWK$Y z9eV2mO11{y{sUgx*E}9-@DD5Q`KK05dq9efZcol|;0 zKnI_ogU~K0mWSvdwA6=j(htfuXw&{g2mgqZepHhGKnMRq2ci9}I6XoKp^bkOCtXnr zq0N7a4nB^Pt|~c?(ZOfvAhhdB<0t4Kv>8w0@b_b9psjq44*nS@-BR-Z>^ZNOgbqUc zO$q(0r?8QrF8V7@x~p7=x|M4D(>Up#vfyb?F;s^76zT&d=2=hSSO)64XK~UW%44Ye zsrG#yCp}hHJ?|;Tl$Gq}*-3HIpYwW4Jw+ok)UTjE6&4BlY3L$V0`XjIBQdQU2p3t3 zlO;(ElcgRaq&%2IWXedQRv9qY$c!%o#w>{P9f+qS97K!-gkvQT>ntE@ipM1Oljv&;!bz;M1u>>F2)ha(YKz_#Ks2%k@f8Ud zVW|k>G>O!TAY8>Z64R=HaG@F9MY0`;kg6aKk*Ft}DuK90VtgeK4MZV{`PD#pRtC{f zQ`)EFP2CPol2_h)}W00mPVEAna;@ z2ot?)fM`@3#8)I*2un>6r%9yN1Q8*&k(lNT!o?9pOOfmdBE$v6Arh^HlM{$*B*r^| zXd?&%0nt?~r~_hXJrGYxbQdwMAROz1Smz3&r+7?aKZ(9> zAbN{cZXm`q0Ac42;&sv69YiA!5MPn#D=c+EoFRaGKm>Y#NEQ=4Ks+LGg~V{-+YrRc zrXc1u1d%F!B+;uGh&GKtq>DL?K-hSKctB!=XxSLVRuan^gUAwhNeuM@(Y*` z5RTp;%uPXz7M+`d*iYhf5;;O@24aj4h@@s9#)^$38u@~7@B}elBzS^2O=2&JTv5df z#56w;BfUUO6uU@-_=9NR4PufQ;SJ&%i4!Cy3wIw7^8-Lk_5m?f93#;#5JaFah-2*|)6H5X?I5r1i4gw)W=O7UKNqkOXfslehj0ppg6b#}$v5`cha1ah5AQp*) z5D=$H>?QG`s1gcdS_=>(LqRMNyGVpsK{RL%VyPI>9K_3k@gs>|tw6M~g7`$tv4XH^4dMZb zjiO})h^-`+MS%EJ+$AwI3Pkrv5TA=Bksut~fH1cNu|;%l31UBq&q-_(QY#Q+qCq6J z0`aBTNTN|&5Du+Dd?gZEgE&oMFNv>3l_(I?+JP7u1!9-jMIs~yM1wXU_J|Q}KwKkn zg2Z0o9t~oCEQra`Aohu4Af)}Gaa)K&F_EH3oS`@%eA_`B6!{c~#E%q*MQ9Af5iy72 zsJKpXOtg%JI4%}Yd?)Twd@o|)7o-zn3B^f)y}PG$N_1`yaaydRI3uKZh_j+M#W}Gt zUh0Ue8|}jDJkEv2r)>3;wFl&d_iFpY^Go5=cCGz;Pq4nw--Kq`>l`lTcJny(PdPJZVf0|iSRPP{lll~OR9Z*@umR%jB zEXm~*s=x(lULL-_w1woCuEsw)y11otCuz7W4G>>-k@lJYaKOVRTkdv4Zv07RS;4SF_5JmL>F-yk+yZ!FcM4RkoFdeY!O5KmW>O zId;bXtl8ZKT@c8BZ-rx`RMyVu(x^lUPFIsF*(qCS<)!)iU2tmu$=hQ}ni|=JPd* z-9N3Qs*?7VaKrR}zBXWsB}1AenLTG>^w`y_TQV}Gw;H_4RDJ(G`oA#o|4fojwmy8m z3!iT1MXdu;%iL$rwGX)!>O~X&Nu0h&sPj1jPMm={w+@mzZ|8K>xlbS&$ zkNV8fQ%Lscu+CLMm`_~ra|9eK=A%66;MmDy;22#E$l_yDB)``)^O1xcJu{!k!w>&i zi)k85!RMsT@pf0f&YjXZKE7Z)AZR?r$7k3%WV9Necj8_I)_L7RL&JV>!5WnXD+alYXGi=ZmkjKU(4ESDkE#Fee&6H+8NN!m|+OWB=N11(T_<&ap?g^>j@TrWM>Ef73ZQX%nqL z?vBniL-=hpke|CWFMdp(zzmS=CI9m#IWJ%`xO$NHbj};$uNjFCAB1G5_!kDdbnc;^ z&KDdfB-8W3NJjes$90YmL8_cTNG=Z+@)(j03jpkNbN!_!%+q(y?8{=x5ovHPhk<72 zEFOR~^cnOKgMcKUFVGKY2gCreKpep5m*asBKu4exz=xpE0rVYwaC$#b1RMYk0*8RZ zz(!yb@TtipIv$l`9KMCJ4`>Ut17d(UvF@l8W_}&PKH}Cf`fp;zz|?4z$wf1 z!u7`IjOGCE017Y{m2T zxDyB*0cHTRfZ4!O;A3Ddunx!v3V=1ha$p6(CrHwO4}cGWj{yFqfB@zLyzBN3z)3t2 zcoP@}j0Q4+;Xoae`2CpVk;_@kSxbk&ry=_QeSv;Je_#NR0Ng}8XNC;Wi=_b>zz84{ z7zvC5=%o0N;w0cLU@|}_MW@97xHKJ@3Csd!^AXi|5Eu$11H*vf0KL^(;1htG!@Gb0 z`1odTU@^i=fRBKW0p4g00)hehhfrV++WQHx9#{#a12|Qf-UAi_Q-G;7(PRYpRMHy& z|2`!P$Oa;SRsfgVAXLhUPS55B*Z`G)%78sk6{rT#ZS(25?m!PO9DuSo4sR5Y>xd_F60Prc$kAY>t3ScF$O5}Vm)i!^Q;AXJ| z!Q34P?gVxLyMb?jy}-A?K43pk2owPafP=sx;4p9mI12FjP=6o*2m~4eje#bBE8qq= z0hiFiAAz3$y8O!$+{+aNegUom^zYZjnGPyMYKG5?GDi^BGn@ z^x7Zr0qOzOfQR7E0_TBdfG1EJa0l=>wfbl_AJeW1&{5JaJ^|=RAMjDnLkJuJjso1y zzXSNBeM{g1GF=3I04e|#0XkDYRsIy<*FVnz6F3Qwfil2JaC?FMKr7%h(3(J83=j*% z0Ufw!bwVH+k;4FQpec}yGA{$yf!~0Kz@GrmC(i($KeB+)z!+c(%9skw0oEb^dSC;< z9Xc1t1I7T{?cV^nw{b69$Vl$ket-qwuAC2WCw>#)?#o9>`M4>cW$g$wMXNo@Qde+e=7tD&Vc6wsNDRH1K$ATP67LX zLZAq!j4~EulBu72+|%m19D~Bue-x5?4)>r%0A0x}vFeo6am!z)q|fABI_8~zS~QNEgnK8TLmaN3Mxeme3dz-Hicpb+>BptI+GHV~j6V;cImRsek)HiSCr z7RFtpI}i?FM^tx456F$;%xNqVx`U2D2Ot)R2e{ez10MnIKs{gyP#5?Ra06U{Isofs zaf^YafFIxkaMM}_aHFETq0ePHZdl6!@~e3^z%R8UEQlRgCuW?HYD(+Hp>vXJ?w4S` z0N8`Az!tzT1)IjO1Z)Eu1N#A*;2Ypu;45G|5DKtMdjZDn0Coaj1G|9Tz#f40>;v>V za@E2%fnde74G$W$!vVk;4~_*V0K33+Ok<1@3vvTWkJ}OGhXGC``h=dq34|fk)4*|E ze$V~rI+U}(MSvAC(T~6>;3U9G*<)I%bVHe*4ZQ%I0nP)=&wR|oG7bJT!i*zd+T!O> zzuF@su{(5zX>Nlvdd&)rg1$ib67U1SbZ!6}V8k;I({QF<1AYOn0Gw$*0c;GNRPN9C zWhAZZzx# z8|ekm6C8FPR&pcos|`T+!ELUU9*%SFMvys z6OS_+s@f~2B{vWR3uI3j_ZogNPXf>%m=8{!6A1SO`T(j9egMK*T>l0!1jJw<2^a(n z1JVFmXgDww$N)-n>Clq_rezuqyl*RPhfUUq5;PYJk`UK$las#j)U?z?m zU8M)G4&kxD7+^Gz3DAV2AV&h(K$ae6C*A;Z05*mVW&_zFBW@hRYk@UD?nFIeHRLK_ zC1A9cg{%OU0UrbG$x`4W!037Do-PLeA&>`r04xIL0`CJ;fVYA7fQ0}Xw18*0cM+He zD8M_w9AGvu3z!Ma0Hy@! z`ez0z%*3!!Fnh)pv(T4SY{b7T4>_8j`588b4?;*9eVc&p2rg0SAG-z&F4ifQPTIA-P0%K(aAk0o#Eu0sN?8N-on~ zz)oN{PzdY?zU_-&`+y?g0B{6gFAhT<2bky>z}@aB4>p2{x7kOG$GI{(918x)KUC-MRKa$8(AtMK`n;{ zRVLQ!deBDd=NsbX@8xew65hW^PD3XnffEu8J`=I2yJyH1BnbBM_4V>my-{_obnp8jdl`Qo`r@Z`dI1+1qMQ!cHt!t7$zA1OVF8S9s-m|me zz}mA-e!zVdsh__W?0~@*>DMKvK;y+cTQ6^3IIFJr$&v(bmeqRvWYq21Z9g90tR!ZZ z`0Bd!9=@LLeM6dv=Ty$!kd8=d^+PnxNi@3&OBRZRn`nww)0Xu&r4UJWwTPW}BuA-{ zkZz;3exf!-xbUqmJ4w-^BehOq$zKq!i!Uh#i$x%78*lP>Z$bSX@yd$(Xif-vf{u?D z$CxW${7NB2i>DCF#T>KjRNHtzNYaGsqceN|xI*oOYS+EuZW*~o*$_-$k@_2QToDKE zK|B&mpve_&#rEGM$J%ZfTF$VUOEz_XI{pX^7=UUqFahE>W{VdspFs>4uT$iU7Iz@7 zh>4P1T@1e?)wJxZp#2x|u44KfOeoy|wT%}J_5S*{`TdKx8lhlc3}i4)uEM>H>?D_~ zBr4s-z#kSZ@8S#1C^73U+9g*JtM5wfYa6cznm%am#_eYde~=|iMov1@ccRMgNa$Ks zH2YmzDBYWPKrXHo&k#A>ll%jXcLc3*@Ac7>$w1a~-n&0?D%w*WDY$e|%Y6~?hve_P-CbL814qn+g%vo8j zX4^kX(w!5PA4yI@w~(M33i4^Su>Y2FmF1EIyi#M#N2?|byKU3pcu7pPdZOPWG{Qqq zVN=E0bgMM?U`dKlF$aZXNvwgWoq%*)I=fA`+j?A=BTLea6h9zAp#HL>ye3r}`AgG( zUQ&`^k)Cy6jlV3FU8_GViP8A^IXS$++8V2a7H8`fplApg>W|BzhqZ4 z>iP%kN9`*~SF66*@K|!pHQv~iGT@`cPS<9q`ko@%VfN2z@Cm$oo2=}0>(=RkjFn48!ejf>WW_6_|zt-8{ez@YP|Ew zcsqfb2b&^J-kzE@jQ1DFx%wOr(Vk4ebTM9apk`9ros^ZRzEa9~`2pMgpX~GE5ZO!i zUfP~(ynrg|qEn4T4`KU6@{mt@3wyKdDBt!Li)Go-*?7%STAh1epEzAVRF-hy2%_CR zeYWI1k=~NzDL$efvl#x|c*9f;b8nXpE|cIxLc9WlIUxV_9*0N5e&de!HyawB`5D3O zP20WdaQA}iwTIdKQa-&`bY$od7>Aw-O;tCc8X>qj_UcW{c*)i#C)c=L)#{a3jjoPv zfUv`cR(m-*P#eX!=#Y`)GI@765}G=p#xq@ zeYqD(vrg;$(c}Ip;}Am+hnap@TzDoO(XHv=iso+yOP6cDIxi?~{`!h))6_j`m!J6Q zxpeJcw>D?v{dgVM9UFb?lvAKutGWj*4G;m6>{qgNVRPn2UwY#ahb8$Ei>|n-B z9S|f^Fcn^$8rs%g!wjn$lLjZW+erDc9bI?d>W)6q*?4W3Z^Xw9#!aeHOKqW_S14|T zgoqtwP{hCQ>V|#P0q2>hLRoC*`$I%zS=rCoc=1^G0XLsNnVs-ZwI|jFE|EgT%(9rt z#w*Cm_VVj?sNCwkNTKhRF3rVmmSMbnZ2kE!Z@OnSW2^KNj?qDVRfvE;aVS+wHzvPs zB>l7{Hw_nSu_h$-Xyj0igVj1*Tq?D)WIG>Gy&T43vS?cl9Wkt|=afBt1eTYdVRh|v zRoz|Pi~nv>s=36V@^UTNr-jHXkA4|%bNT4~l$9g)c&u+k-x&xWZF=5994;?A1{rTb zn-i$~+IEUE3MtTJTp|R&c&0}Tt%eyHXcf*juxq4M^Cn9V%+L97*zx4L(gdt`FJB*~ z=!6tO#=F)m>EYkr=)CYt94c_t69g}4yxi>DJinTc9vq$~V;4uYAt9VnA0RIcxz$GY z3w*I1_ELI;dLy^^1#8K}>%?UA{(dUFe1JA=lZC4V(>qCoTI3@)i25O=4{!sC%LXK8X3f+2HXLrgVh#|#8CFKm76## zk(vj3@iNU`S{f-nu$A37nmd_8&-oJfYLt!fuT(*9Vphu(OKfEq@mdABgY&<;lu(!q zW65>Oj=fliwoaW4+W95NZGCaR&^Xb>f=OhQih-b=I4{{QS&0;t?9e0E*22dQ)4+J| z-TvzVYe#OlcoX)6&-e26<505C|6vBd&~KK1(N69rSBesKDx+TKDAA!3#>seJUil^8 z#C5njmj`Ul8BEAm`}U|P@qQ(_iLEg{Yof$alqG)wzP9Wr?{*ZKneuH~%5LbZam*;rDHpAod7T6nqkJi@0pV8tr8(qDv80QA#7%y2n z)Yo7B`J*FOPzG&|DfFbR2)CCVy^Pnl{n@pj)0v9}7iG!ZPJPfO&v*e`c(XS*J*?$D zRh9zVi8t+Gn#^{1`X56vzMUvrMQ(3>A2}SbPZ)3RTUt13xcq%)4%VBWS1{+u6{O%5 zf5m?9yZzFvhpQT+w&QL)F|!KHYrIWx){5MKxBSLlLkeDup{2(A0b|y9bsDv4yc_xe zo-tp?i1WxRSC17B(GF+h4TA^5qQk`4WgSauL5urUNKB=`} zv9;1;KJq`=;lpK*gVh*JBiwUsC)QSlzYG^YRF$2AjJFBe^?leVd(g3gYID?v8t)ri z@r9JQsNC95N{qdzz3{4r>Ww!LK24D}WYmp1t9J#J7%w~Ql7Di2onEoFh|#Z1@Z|qE z_59*?wBJICfcVy2~@f7Cr z@luaxt-FX$4(O}#`oSqv-veS(kyZnJ+uTJ=LrPBP?KQCQPIngroUkgxYswvpm5*!60rJ2e z;%rT%OzR;kIihUiO^p@hO{X(oU%sKlzDUsrDVSrnBkIu0ESBIef)6^~U0cE)b$|MJ z=kgYB$&x;&O~&gS1AfX{Z65U~i7`04!O!mMC7L;*Y`sk9>X@=zRGX(R+1e(3+H$Q7 zbP#2TxlH-AH~x|Z5~KDPdgeS8qlUXFdER)kzu$ob-of#MP2el{ye52Vp=ZY19bap> zD`?{EZpV>=dm3iw_C8_)QpoH2icPgJ6QcUP3l|pj>I}vUA>X@GZOxwZ3+zx7&!8yk za6i$3d5u?-Hvgo3->dO0v-K3{>#Jil*XbuK{l!s~Atw$HH)^9<@C9|8p{ETHA=^lNJL-xC?5H87O+V$k*iPK_aq_{Mp}}g!d&0dsp<`cwgkZPx@855p|YlKF$Et z_8-qCwT*YsmhUijbVlRbt&kT>k3ML!nB$5YS9g-cT2xZoc-iH{uLkVi-H3NYxMlf< z`ry9q4@{`<6JDOMd3{WbRW29ypV-ufr&ztjmz{q^q?_#K{OWdUr4Dv}F)vER`HOu# zG*~>t{v=lzEF9gj^j5YPdG4@^@wUwtLyis_H+2~Ais8h=O9(1Z z<7J-ps_$6z;i+RhTWGB@r3@1{kx`y7Ojzqf8q;y=FmW^xRT^*kyt8H6>;*Pgcq557 zz%cC|3=?VfUY%2XUk_&j%W!driHvIWVqdJz*?38)i}TamIjgVYl2@HYsK!1;WHgbz zxnCgjPPWk~OmDNO*g&oo^g*h&BRV)PYF0x&WJL))W#EQ{@s3E_@D>w4k!G*by8s7X zm@4`-K!F!h#i$0@ca7IkCcRxP?41Lja!bU?kXKce)5OIFSYuKBL{$$+{Zd`7nv=Inr)P?aP0^0`GDT!lv}&oG-`yB}`sZ35c@)&W`?xHz7I~eGm)lPKcu&={WoPiNj-Fz5 zmbkdRIE8{=8lxXjT78@`0y^*lq z+#-AV2|Xo-di*GnhZI4^D}c*%A6@XvyUyHa*lXPEQQs1bk*fav+pTvjKX6Nrfq(yC zlsJsMdUqVmXcBtZ$s3d*_12b(a(+dt(H4D~se!|~#N*mRum2|x>sEzqbOGr=fSpLhS!b*n8lTB>^$ zqd3eWt?dqGGy=`+5QyWi@kVF68joJP^Y$ein2h;p6r*=Y);sR}YVV}CRyYThG9h~L zLJ6gsML64nx4m` zbT(dv-XPw6@i!6Yt7&H8Btlp0LosRe?MUBJN{zpR8RZ$VM)QsNWNhqM1eZc_8pUa4 zTytCq4rq>%(-r5lleCL~NgI-mGzsmvO}8g@xXY8ohUUdb5w+6)JdpmAMW=9_GuwvA zF8_B^K+nmO-K#ocOfh3sYgvjGwRL9jrlQARoo!HrUe|O@=%(RU)9=?Y@<8~P@ z)z3{4ZY|K~8&gEL7BCqOcH3LX3#-3)5scX{X$QFi}G+{M-Im^d467Qgb?fB5BkW$<;auNNMX!NvB}nPPn;mZI@4 z^2+zbyROzFi%U`%Z!3@RTxB!tY@OdqVh+v}e<824@qY8K*0zgq?iuo7Ns6a4g-=V_ zvA*#}^zO4F-&`O2Q@xTDclehG_#EvlQd-JUVtq^5FV_=~#PHt0TOol5r7@MB{oGxf zz0)W3icsn;%e)1>65^k~f7t7{*-m%=)Z@QS+TdR9z~wvU9$Ng*ml9@fvku-^4LZyF z`Bb;NR8J(1sk&u~{WIEW&`o*7ZUk}?kB410ezhKi}dpdPq zJ`p-^J^Y;gX0rXDm~+#iS7QOgU!Oj>ZA)kM|Hh*imdj#cE4g3I)In+912fY{XC-BN zCZ?xmBxHLIO2|$Sf3%Vl8zhX(PS40p8k{^PDO2@#{ysiFzCnIIi3w@RBeRk`Qh$(!r6gx%muhA}(BRC3#O&nsH2h6?T6$(`LQ3)*NuHSrX+x4S zk`vPt(^Jwjv-B^KJx3*^j7;)Wt(!SCJ3Aw*xwp6I{hD08MtW9uVpbOFHHOswI)Av|*8o-}|#q zNhwK**_r8Sp4sW1i9<8fQ*?pvm% z%E_Ty8n2M!8jFiZWaofaER_;Fm0m!eeDW_KRAd~LtJ}7_thtU(m&MGZa$N_kzp*!m U9GX`3*SdLn)WXZfPZF;FKev@Zp#T5? delta 33377 zcmeHwd3;UR`|jRb4mpUK3^E!bhC~ucP9mI`Vv592V+<7pi3}t$4+%QB-SI+WS0vubud+_V>HP=iWbVcRlA_@3ZE2t@W<8&)Lm> zbAN?Jt12uAY1ri9&aYQEyuVsEvqn_&U&r`7y=vdyxx;2AZmlvnz@uZu?b=aI;dNwT zAHReu2UJ5*lp$lYjL-#qmRFR56Oe9@MSLQEO$ypM^x0g0CPeK@LkDGIoTLuPB+hs2NuXB5ESS zs$e4uVh`6LH#3e(oj8u^{6Mm&!!uIy@=}K>D`dLiDYq}J%2iqvhrb&?0sKImf1zhQy$Vf zke~`$2)KzBAD2>8uLO5mO5O%0h;Rg99tDT4uJWv_UtV=2W==>4tQ_ z#F|s8LMwAhOoyZ=%7Nsfy%T3nnR8NZLytHkjzOoz{E?q);A9(f$UcG0rw`5=otcq6 zLQ(d$Gc#<3WWlrJ&4TUi%>sUd&VphPUjgz*=rthIa#M10QYI=Vq0Vud48^G2uS<|@m(>x=Ax z`O|uu^;%||1D(CBF6-IT%Zx7%o#kG`%wl`z_BQKJ$(@)rbW~d2L-1@@>V&-1tYM16 zeLo{LH#cQ`F4mBuoJTzKH^Oi*|IEI|)|+2I7nzrqm752G5qbU0gcsVH6U8&xbS;Y^ zIbIbY*>O1E?D6PPa(}bpT}aP{Z-!(;A_tfax8{#EZyJGTI&0 zn8R#-dTw?WP6+wfHO+aSiwI`a;bu8uT(Oqeuu;&fLC%JxT`G?>3n&Z8^i#o8j>sO3 zsukrXbgrlQsE{%#-R#KMkZkY)NT$zB%fhCZpPR>)zzr&vQ*Dg|tf*^-*;6MZU0E|( zP6xx#j%kS^NIPBEMr2@% z=7FHcm;%WPbD02Ag=B&2P(U@vZAf1UauIa8rpZ#K$@s32Uf>;&v|t@bn!Y+@Wk{T3 za#Ba6PSBv|v!W{~kS#qt-mJLM1k-fif~O^p$_hV&EDyaeX@^0wfc4PlV2V4oKGH56OmC&BE5f48c>(9{mB6v4@u-+2UhT?uBGUFF|s0PJv|40x%>@ zUkQ?P#yrm|%8fKLT~2ClcE-3=reutQei+46LUQXsWu3?Ofal0Pey}P=e)fAm zB>QFEl{~4ul&svOG^?PkSgowc{du!}59XR3JqO86=l~=Qvq{QjkhJt{NLqe0<)+li zxh|Vp)$lLt`XFQX!^Hj*p58Rr>tY$bn{P+$fua}sI@M_Xgm0WWN%!=Nv+q|Gr5Q3; z(*veNtEyh;=d}0M6s0*h7d@b(LtUVI`p4N1fWxAcTz`G3f1Il8C-C`|?imoL=IQ45`U_zdCWYm!Y>YVlE=oQ6E?*))rb8wUM)@BZQs2 zpo|_6;Lz5V)e8fh_8%mv=_>;q_Byz`YK0^!HQNwqMseEP<@Dq_PTOS=Ep#8$7pQyI zkF%$`DoRfzx9chX4)s%gY5h3$IsHWaINNXK6{VMcx_+#^8wRn5(X*9)4t0mVG%!xB zpq~hgv-iZj?`Fi%4y*Nk4dS%(74^afPJ3H7(?mv7?Jq#I!Zqg6lX62*94r#j;f#Q{TjyEt7Fb2fisM5Pl47Enk$FEz6lyjbYY3w z&z1G$MoxPpOfJ(}tY##%C?r9H(2G}f&&F}u^(uODW2e0?+yJ8to2mWu6OH3+AA)P8 zpZ1Ma?Rvi^acWn6X_Gkne5^NGw+zjtU9F~j1v_n>u=t|%f&Q`fe1xJAtr;EL0!=1v z;ILQ0%A#g8-_{LU8^gMy7>x@TydeH11+_D59D1fS_kL*rC;{RCnLVi~Yl7ky=@!@dxj*-TVo`y5(( zy~r=tUKxvrBT+^_J3m_Y43D$xVAyC^9b;p^0F73*=_%+?9V|I>5l|ZdjmB2=l!gv_ zq14J6`Hn-QeK9&=4!eh!SqjEB*kSJpjrqzk&bA#|I48aRDnsCr3ghbszt_G*%YoJg z8iul?!?qb(wC)oeYrle!oL9aMdyu!Op$*SPyGq5zkx}2&`$fg6)%B%Oakl>ObfJ1t zXsrEtgv{)m=b!2Q9C2zHeW@eP-qP3XfYCWE*HyVb0Dl_*-ga zjks)Rw6~e^4QS>>#3

%9JP?TA;rfhY1wsPzUIq&Esr~z=d`;&q@RwBwarGTixK*=B;;G);?he( zA0X7(NT&qSH8F3;lF)*Z(D9Oxe*-Js$db^;lF;3fP^%!$K%*@sp@Su%Y7MQ}z6iB5 z(k&|qeOD5y*T{+;X@#`)jdZVgr>%TrMM=W_FNT=YME7d%wBLfO z>26vJbFh7|qIBiTw=YJBKBla(oKHie6|p*Cea{ecMPb=uWh6oiMO-;OrHR9~1RCs9 zKUVuLMEC0GwEH%-793r4Z>hoEL^b(qaOya0pFqQG#A5svAuc^E=9$rIs9xB~ zspW?1#Sm|Y>Rz3l+V`P)GDMv)_QlpG%-AK6b0I>UZszj%9NHkgC_dKK4C|?<5qcIO zEcy6Y`yqr_3u?!-acgESX}i9%iNoFjT6?1f^q+;$*iLj2efRcpz_temX4H-WwrWwVdG%RzTwnH-%$vviNRhsL?J)By*=DOEYPHk>; zJsILabG;D4rG;Mnl+#v#&S5S*6>Hmv&>$le*pdT5ZW2P6`A@~#cOt|J?Ro%)yQPu0 z8#>s{NVftZBb{c8)4h5-wS+i58De&vUfA1d_s0;FND^~&{no{XvMAc(=aCva(^m?ou=Nr+LuWUaa~=hy8%mOuuSRu>9)T#twTjG*;|FA7Ebt zjrK97o&7gxEX6pj*kZaEvmYKN6CwHySL57L)J0Do?6e;P8D$iNX|GjC)Qds3OtkEl z;&5n_6ZPa2ID$mIFvV%taV%mfSa3Kr?1sj6D0)D1hpkLkX!^iDv0B@%df^bKeGCZr zJ2Sz112b_OM5G&7;m z7C0*Qb7-rcqC3^jLX2=~UOn|>h|WFrLI}O5UOd8S-`CR`6FM}PUXRpnZwHN|fbPLg zGoW!oz}H~(zJSKA8#BvZ9z(~~h%+^2ce~ztVVcvn1Y|p7E}cTiY(ERC(Z?(ZE$$5O z4vlWg*cfbcp~dI}Q)0Ec!bk z`!tZWC)&^ta}t^Z8j4PJ*lQ0mGr+nd9QJxPVg%Ga|(TgE24AH&DI_>fJRtOG)MF%@hf||yZt~JbT08EKWlAy6eh6k`e2aRKE?s&VQab4T=m1#I(LSv`k!Qo#5 zQcaf+qhpyo3ym||)b>J)GE!nYu>D5OXh+y^%artGuS4U4hRHB({YNMYCLA@m%?Z%D zaH?y2M(AD>op!I0Rs}r5_l3r08dKB0s6^vzI0%i6#(0g7c7*~@!BQJKv@U6S;UuSh zcDka(gM_=vbZFnD>t2&l!6>sxbDQo74QKksvG$h{qJ7MP-3N^Wgt?C`>cJ?z*w<+r zltDw&`>aN&vp&!fYrlq2J0si5X3?&hiV|liXWKfoli7OVRHr?1v^k^Af+s`ctiZCw z9N7fTH0R3R4*M_A;8ob$M2GF^F}TSxyufyZxF*aJ0&=VY;imUGG}A&WG05MQXf(W6 zu6e99%CirF#z|wW9{YM|rTH#EGuy#3>*ZlkjFdPxYNPY?)PHbN-~_0Uh}#o9L^WcGt&dk>oFc6cKfHvW;;X_Lq6 z#WS6@??@V7O*kj;gfb9wd_F?-D7eRK?9h%((2JjO+G|cUoq%bf?$D$iQO_D^9gzY4 z2wVJBXt>?3A8QMq#L?up9k~dZPKs9B1+A6Udz(6${-G$hg)0JPq3q~FXfVLwSlf>X z!Czu3#7-%h3RpM!&}bO5+5^yf7*oe*s$mW^AR8g$^lsmR5d07a9!;z{ji#e#)Ytw2$R8*^qPLUfDf*sq30Tbt+R zQ_y-FarAWY(@T~o#$Jck&5E;~gEmAja>Q!!GxWk2aPXR;7el-=Q}>$Zw7v9`0V=dc9L6c2~{kKEolty7V z$J~2XFJ9oZ$IdCK4@dj)(Ap!3;mouhbM(T6PJ7wsEdRiz&glzzI89&4M2P%k6&Jwn}$P_q|!3&_|RC847T zB^sRXJmYQ>v1u~YOdss)l2%nQzCgz3f66k@YXB-xQ_5P9yeP@L11>;qfEOisZ=fO& z1n{EN^wX=|JsH~+VCgW0k-8bArn{~2>qHjaHey@LSga*Q17IrSqGT!X#Y!CjKHs<= zm6|^FWq19BmtBc`mYM-D7ksKwS~euFMz*?lJRh!Mt-jEwLYMn?Dr<9bxu!OWCAB`bVJ>Xa-PQ`WdB8IOr+Tu+kh_*~Y{gqSAA zMd<>(XsS?}=Y|0YJEQRDn$rOcDaQ%nW6PLK> zmj%1hh%+VYTLm!JYRUhdWct@-I!fkyL&~*Mu7l+DsARe~`GX7#SPzic0Pvzz^@ne` z=X3ghN($Hi18GjI|DB52tPp@-t z&E_?bv6MFGEQPs3Bu~j;Qz=8G3?r$xe8tZ&doyadD49DFvI1l)NR}EeWoH@R1(MgJ zl2jr-*s7->srQt!7ZqHT?At&{H^_7qW@m=cGGYQG8!!oy7bSy}B|jCC4VnSTg61;v zF-hy|GW}v1|0K!LSfZNK4gnTJ!Yp`&)G6uv*F)0JY?M4D8~Pq(Imm62r)2O$ssEQU z|B;03(I--;WDEB~lKNEgk4jRXN&ZQaQTy=0a;)bSAD1=~Z~$*L&FB7jRu)Ie;P?1o zKYx(?-$^DpFVp>Qz^L#hS^Z&=yD>nwWU|{*-ht$B{07P5 zeuw1sB+2F*MM}x^Wk52EixK&^(u>)in6QjKbwks9mQV#Bl-2OTVyjD8gCwp;C7bOb zc}g~?woLCW`9~$={lGI{T^Ucwdg^IBoST6^NhYW-Gf+|wgyec_BK2TMO)q-eU0?sU zYkn2P#DQUzZDh)hkh~~4(Ys3h?p$$u(!N;YIaB=a4X@kcn7n2^sIQ+^4Ff6CWVKSl)?B`tGO@~5Q! zJtPbK5t0qREag=xuS2rpn^M06iGRvH$>%?m0TsEK(8f}vq;8iwB^y{?$_kRFBwrDd z1y_P(R8@R1e@)4|OWsTJzNVh91R%fy>c|B3r3{j?v6LZ@be2(&tSB0i6|{h4{+3cY zA@NUXEoD0@xE_^E9}nJzCjg$=Npv;{-26e()IFt4hO7jABqS?NhvfCBWd2byK2ye1 zlFyR*lVlmr|1mOQj!gKdWC6J{K2OF!DoKr(JSFGC6sc1(|5T|{(t@+3PO1OvJ=Z6} zT$f1vf9>W9bu|KL;U;L;Ppj?!+-m)Et7Y6`VM_dSt3@Mx$`4%s3({QB{|gyQDx@p< z=T^(~Q2*R&8IJl%w^*DnaAHCxRH~kQ_#k%KDee}D~rv8+m3jK3vYkzdpgRdm0OZACY`sjTx zy6LB(y`nd~+DCW2TyfJMLVH8+^h+N-=&GB(;g1nsn zzhBTlXz%Nux6wanQ*S4zTlCMNt^F1KyOW@9(TdlKw0*ZLiU$MK*805PDyVz)rN5z* zchJe-67V#0rw8cdU3Bt60v@Qp2kkhtfe#bZ1Nyp$=;S?g^7jPwklycibn-qr3GJ|M z{{x+bmib45dQ{&AZOLzLi+$7t^{d5cDmwYVZSf&!$AqT}_3i^VF;#{Bjrg4U+J|l; zSc85-Ow^$F{oPHRrhZa1ECb#34>vKd4D{3D1of@ZTe?91Ry^mT_E|hqbrV;qpA|7S zwU6*nK@{0QoD-Kw>?4s_7Q}h6v@D1T8i5Iz;u1nq_*(krOFMaObr z4w1Q~2(OA@?vj~W5zHM$>?gC<6-=-jn0tzt@`7LB#~JY zL{+hk#FDBYd}@KHF4Ag&=vWQJArdu(r#pzdB&NE9a2KDGSX&)Lum=cFG0_7=-x?rJ zlc+5kdV+AR31Xfn2p@5R#8wh5y+HVh=e$6StOepKi2xB(8-#~Dh@#pc>WWJw_K`^R z22o!u^#(D)1H?lT4MZm&5J8?GHgIA$6!%CRCo#|$L}Rhe7sMPd5N>`Tf<-?+5Ye?k z>>$xp*!@9VB$4S4B1~)}vBVpMPXLH!A}s(!M;{P}NJI+HIw0w zQ4|QGt++&DABn^UAmYW+1|TNX1@VwX2hk}AL{L2t8-hS|68A_PCo!-ghy<~&A&5Ek zLAW&nktq5#0udbuVh4$?!rmCfMG~2fL39_}NGxffy6Nx#n4mr-sy2b%F$f99HbH`3 zVi$?KBB>V-S%|K@1ern}XO%;yj6` zMOY|^kxf7>4h4}S&XVv51`!_yVyIXc24WwHTO?9N>u?YgLO`qu2QfnYLL#Ush+fS= zq=}WyKpZDwivTf7bdLZrClti{Br=5>2_iZSL~106Z1FCMizI4BffyrFqChMO2eFq# zuBhq&(Xknbu?`Sp#V!(eNz{)9FRML-OQzL6ki#ekSBj*@VV0uk99 z#8ff8If$(!&XdR&VX+`aIzTLr1u;PgP ziCZMrh}IoJOlSvUO-B%Kh+jwq#e?Y83B)?FvJ;5oBy62Qyd}DK1~I2Si1$fs5cq&S zI)F&Ua#G(B?~=GkqGlHm?~0TzAeM9lv6sXqQ8f`n$4(%|CW3fh>>_cOMExWXTf~?o z5NkVwI7VWd2!(0W=n7)ZQy@MQzkpEpiB3HsJ{Kz~_KSNI2SoQ? z5C_FNibF!}4e^EOM{!uZ+Z+Fz-BZ3bL`EOAr}~lD)dzIK7SFzFuIkHYTJW?~L4W*o zP#%(B&|7~gW}EAlYpLoWO<&;aA}S70U2U<|@neZNI{>FGaCYlYqNEBEsRnJSGF%;8mY>^Q%J-GO)=xPsZhWJ*&R2f_!@OX3r4V1N z@q5@Le&C|c->Nt$T_v{yk~+VDYazMUAQ|KjW(NRXt0c#tjD{Hu9!ZiM%M3*!d{#qQ z1Cl=j6JabFm-TD}e@|*AIX)l3O8Faqq~tb94uh^Zi~<$q16c{5Hi?$p7I4hM=PqI; z$LA`HAISLFNJ~lnOD5zma(!g@BPlC`^FWxr+zE+){2!f_%K&?{OLF{8i?^D*_{;?> z=Fi0gz_F8iz%d$gS;-$PNj_|Wf5sDq!)0bZXhDuo7mj5qxIUL0){HVva{PapnU0TJ zSx=o9&tO3J062`a+CiDl6X91(E?+q$Nq$rIEW$M97n0-SiWvygl!qn9M;C`9Oj90_ zoDafz2-A{BCFhIqc*%V!IX`f$gVz1Z;4uFFQ07STYe;q~0H}{JuYW@_SO?&5lQi!) zAUF(l0sinw^PZIH>LEN@a;GF$A6z48t<#bV1lQPj)&hYul5BwRn+WszR&qfIPeGU# z`c85UCC46F54JQym{#zITCtb+# zQ2_p0TNmSLSZ3S zAla}8;6B2LoI;9Tx;e%Q96Z8w+fgV6-AOVOJXTMde*KdwsEI>EW z68H?+_W^Vfp91dy8%5-4wNK4>1lt20fKEUH&_%2~t;X60AUIH*J*~Ru(~qPA!vW4s zK3>G>z@MI{0eq6R0GJNU0A>Qu08L>fx|VQ&?j!=B3yA@m11*4LG_WU-jq++j)&ibK zcpfkxSc#wHmDdn>6L<@F7MKI916BiTfR}&_U>Q&ZtN;pu#lXve02Tmv0-df;DFE^S zJjic6S(^sQ1?D4uIioht=fdYxGM#|VKmyPONCc9A6NtYG{0z|J^aln4gMg=jA;3_8 z4uub`j0VO4IRG6B9msfK0x%hv0!#(CDtc*XA^##mAD}Nl&$1L)2D}Q)1fBu-SWkOk z9>Vj17lDO908kgG2XHM10xzPW8-Vq|>p&)u1uO;l^y@RgEMO)uo)(&bz&KzGkOQ;? zS^-=`-BBssA>E%Fa0?utv3vkL1pWZHjxx}DK26aEXbbcLdINoczCb^qJMa|1$E^5R z&rIMMnt{&_4Fm##AfO>YPtq9RGgUr-FR%g?tpvCga_d_IybinptOMv&-U0-`M+sj7 z!hy!X+i2$-z-(Y5kPF-cQt2hiAOWBCn2SoD2l$N8LSPZ_60jI}87Kr^0hR%;0?UCF zz-z!NU=8p(@CL9JSO>fbyd^rFRcpI#M(};$17M3t1C@USL4P0sr~}jhY60#*MZgUx z3vdVFjzI^(9f3OlJw81=9r`hVPMl8rIB)_e2I#Qqpy_+*Yrg~N+BN`iXTz0u5NHAf z18ZRqKKA4XQ}_cuKox-g>KypbfX@L>zzc8%DgotyG5{UrA1H^umA;g|lfL(RarQg4 zdj3uXdB6w(_9N*5;2>}p`aOW&kPjHrmC|+6Q(gtG0Y3xRf!)A+!23W`U@s6#pqwax z2NWJicmUyfqc`IE0Ny+Xc_1(X%{&VH8#o1=11~PtO^<;%*TZ$0Nm6j1Kh~?C{i%WXaXFN`B=`R)>7d!MV}(fXOg&k zakt{;L~akT8TbI$0z3ega8BV`f1aCCVAVYc?*?`PJAegZ#}8`PEw1O)ty(ah5BF#~ zY)+?t0bF(qfeyeBU^KuquZggWYIV;Q2=)S60Q6V%@qxfmRzcn639 z*rlxiUiPKs$T{SR+8|b4z3wn2u?zQDH&; zz@sDe3G`h6hx#+Lo@b7I2tyc$ioH^P#_i)2ltTbl6)R$*uYvu*=Kw2Zk7=bx8_M)- z=n>!`a2Q~I=3^d~Y4HaTW*qrPE&c`SH+p0x4uH-u&24ea^d`Uxt%5cn{1xyez;ylq z8(_sV57T4=CxH{daey=J7{JDS1LV_>Sc!@eZinu-1-RNcADTnP0A~;hdfQmo` zpgiCTlmp5FHh?|jCaVFe)LkIEfVV^PFi7uQ32>J>4~I2?YCv_M3Q!fOEW_lg<>P~y z2_~fU0^sizPpQ|ITnAL0m@{veWpen4Mf5HJLwg`Nfm0KHjn{~0)_)LVLD_QFcKIc!|X&B zkO{CcY%m+h4q0)d5ncf-2lB_th*u$tfMtNyS{CvOuoPGVuqOg|8L)c(Xir}PuLF6& zB48mf2Uq~`2v7jL2+RlApm{vh%thci;8|ccFbjAFmYxHX;Y0 zsafebfW4dskedKZ1*QO#0XAYHz&Pg1;reF=D$K;NRWN(T7PHVNRcysSDGxcCpZOWK zhU8CS_Kf2}Q(Kv6GKSeGN}8Q5=H#>rVByaL)`WZkVUDvE&-B)qld~GavY3~1Y%!3} zND_?TxUi)(>!Z9?C>zR(XzD_Mreq_Tmy(5A`4;_6c!i8ZUHL}utp;%NaI&xvh99k% z7Fq=~#>}S-1=dLU2IT8N7UCYw^Cmc}QS2ByK&~_m#-Ek5@c)c3IrfZ`W*4v%*amC` zwg5bQZGz+yeGih2c^B9SyaV8$5vJraeIM8id;shK{snxzOoaVq z8}3L5oqp3*9TXN47J@TdYmt9L^^8nJ7JnqzckqkLUfl+TAb}$!EG#6{xC8e@%hc^!%ZphpC*s7ePquv`} zR2z;$vFQu{TdGHG+Qp6(JZKv?^w!itp&{YTLc*1g#P(Y#J>!<@g`Wf%uFY>X znE2w_xt0I@@3Nycm3K*1z?cc5nw2rW&n)pSWzYBzEEjyhdEE}p)t9yY5VVQQXY<2@LCwK#tdP3g47 z^S;_$)$Y44F3>z`TYql5maU&plF!V~H8m#0L7OXXVqpc%6TiP_*VR00TYs0m*NL-@ ztNA5NLj5om6E+s#B3*6k&tktidwbip4=cTn1W~jmCr!Wun4*h_g{W=)Mft3IF?SQw zd(=j{h>$3z8zxdJqQV?8nPR#)Tmu5PT+F;eG_9`Ha*4(|5xT2{Lch< zRx97Kl4dk3j2-yVE?PZQJ+$BKVy6!}YP|>G?3|N%?>!zLkFxE2s95% z=;<~6+x19*866VFhV6Q2RC)5D+SvXY#=SO%K15Xh9kbW8BYs33_&d^kC7!OPd1`0f z#G>ETaQw)(_jfhX+j=|U%5Nv^wZT{Ba9A) za)cHMRn?l+w%!D=+S=BO0Zz;b{N34q zjvdJ{RoUw9;wZ}V{>sB#U@4yj?Cr3`lXEr#<)Ok;9>QPKJTxgqURBLYtt2uvt(m$= ztkkqb{DA(mhVirhynnK~>}2~p%dtq&Cc`uryv5EknulmnM)UHv{;L1ZNrDaCF9*LAu)7*&`ZV?j z&6O#%)4rmT3z~h=R|L6eUA#~GnTxXN8_t~)PhRuf zzq!(z9K)+|y4m_BXDn0hT+Rj3>-n*}3YKn$Ji(Y|jU>2!1Xc{GPk7QP@$ zG`7M11-N6Zh6HKbTC9yIn|Z-S#cB#;pBIUzksxxlOt9T>Xd zm5Vk{DvjAEHZZI8vVhmtO-sM#TK~(^1lN$YI%=KcvVP>#Rhv&JjZwvKY?k%@fqmt= ziS`#WKPgQRB$}2**)8fC-=r6OqFn0`cu8wjnxLDEIXQE9`>>abca_Er6H}43w)GN( zA+;XZEBn>BU7Fwp@h%d;4IE&)%_yiQ8u@DN&=0@5G&H$1-RI&Dw&7bOsD%V>?(HtU z`u4cbN)z0YF>kd`8<-QMa_<_SKmy(Xf)99w2#e2Y>U=NSTQ)UG^qwxWxd~~ z-^bT%%g$U4lJ-EWjXi6+cNw@Io|nSnR@a|0v#OWTt#Ix=6(ht07Gb@S=J{bSz5B1? z&%c#LU;{BGiKiMRI#tJp^kRgVRvkXsdb38a!B_sczaZtdk-ixX)g@9Ktd5RZ@7QqZ z8{YF!**Ethg>?TvM7~@DMOZJ_c>C1$D*?Gp*eJMc&LgXTaute#8k(22#A^Sw!OUYt zl=Uu-a!HfNk8XIaP3f9_@;QN3mX8*`HA~mM_H|>?qox*)=eTCo)S5;8b)A1*nRF1A zOHqSFY%NTW|H`9u(e6ZxkKkFntv`1D+OmwbV|E9=9mI8pT^+XsF`}Y7Ol-X^WMQQK zQ-|65IHW*lLc(Gkm>I2*BFcKd$ik+{^8(**8B<4{7LEkrA<@B3~R;*|WYhj#DA*J@KL)>=P!Xx8zcYu-`Gg^}%ElP`duxqsMw#MUPt8ZPs;V^=pLuD1 z-hcLAF&G>SpCWtrctz$4_q1s5zh2glwNM4rN_Si^=y5t>soRP;q9aK)Q-i80lqNWxj69x&g9?;><4ZiWac;t)$bCes&`RuVq}3EZ`)D=GTceWGN>pMo(_4ukUu?)$vD;dUalYDc z={9S|z>M#-F=t!C;@PJTsmJJG;DC56e0NcdM1N@|oXyS*~orI3VKta%S_iuLn zdIo!#v2dcpmCbF%V5I11y^kci^Zsk$uXs%`yi+*Oa2MK|2aw;M8svH6%-l1YdJv2k zO0!;$(xS0_+-3@2*6p_dd1Fx*!Hnv@*3QtSe2kz%UFi>a+U30UKp}AcQYDpRDyM7Z!eA@ z1v_v#01nYz_|(xnqpX+bxDC7=lsD|?5LC(&E?Q;1UgwPu)uBaY*S{m1gP36*MMfP| zX1#FdVTQVKbe(p^MqW%ld^y)qtU!vWKld4>8&f=-AY~7vqI@^$!X9AR7zLU6y zypebxknzfg_1R&YGp-GNRhEtv)(e8}&6ztZFD;IK2CzcnHJd#>H@0 zNn(CI^woM%&+KPkS)yOaUXB!SXmk_)Nyg((1uG&KZrFUd;xuDWjB{2awS_7ENo zkTSA|=tAl2AqpCx1n7m-5&s2sSwe{f?~M|@mv=Yq8(8Q2)jMBr`HZH@VOOU06y1ST9_e+goHIuebGXqvQs=qNXqC`6cpl^TLds*;o7vDYU$P;(9}jdxd1- zY=jZ{>m1hmkY2f7{jJ@nmb#%V&R~?aI9a@aywWe0O1Q$}MyMmXzxXa3@r?$EpvKT= z$BO}tp+^o7Rszmt})x#y65M-0aF49og(ps3aaJ^gT?=_hCY zyLaHZvF)lFDUI(D?hh1$n_!Hr*D6)*)#vrs1KfE~lq<@Cj`slrItEia?I~40GyQBCBdX(o- z9Lv$*mLrY~oAPXWJ>|k{q5=~c+JEDl8qGA}1@qczs?pZx6E0v)b%{SLE)Vw z=H<=dVm;^jU*r_kn&H6XIzogr``e0TMSm)$>`1W@Ikn0o#psrBWuL}q?(7%R|Ni$S z$_9zf5%}t=D9!ZHH9d-&)Y1+)(SDvl9Qai)O%z0+Y1Rvs(nof*-XQ z*c^e)*?Lz~>I>Cl7aiEdofL;e-kMcP7v7OroA4>!D9!H{w19N+Y9xxb-pADrK zah=h!i{93o#EyCIJoVJ(J*!JAfZ?4Eq?9B7ctPQrqE!N;GsR9M^R`}Fb-my~w?={Y z3S~h&!exq#7Dy>u615kH@+z3%``12O`M9ma>C!5#S79BAx}qi>sM)zR=5nSOfXVgW zn0~T@-qvfjDz$p~a_z}a^+j1cF`+$Gv&0#eWxb(mP||l#UFsgx+NjMqT3R#8ktLeM zpyi2KqJ0c(Dd(bwnN=kY{3A2V%iDS(SmPesmUj!B)JZlT^JG|-cz|NOt(T@%IB|1v z-w!H`F;awcCQZu{q0Ld?+$_<#IVvj564Ri2TW?LP@%0OBKYIPZ)zUJzWeI0He@Nw?Sms_J7CN@h~YPRYk!^aY~Q5;TZ|avO9w}` zScbgb)|=Wq0)GrRv!Jl9%!^yOv}|#NrCaZItI;~L&Dc*4pOh)!A*{E*O<%pcTCvMK ze#s>Fj&<3>7n91{dg0vlitgznzr3GVnqq&p7!-$}93mf|fa28_IK~8y7Ok9GO&fYC zhcW7j<71rCN?va_;d1xm?H|U;T1uUyABwSFVW-609QRs>=O>po#j2oUj%lo#^CtxS ze&W!X(iGNf@GhskZEKnL`Y{;;6HLhwAHjP?Suf5j(`)?P3om)Mp4u*&Jm@jx@F_xqF z+epiSY1Tsc3qSYYqfKX4%OzB?{G*w#rVU3&Yd7#}gYC%TG2WIPttu?9VR?lQ9!-yO zqzATkyTjv!e_O2J$9KU-6GRqy%h^~pVmxIDvVzAvvoU~HTmI83MwHpSnl=og)uV_= z^yvLb0yjCRMIE#VmHW$M+vreWgRES^m{BkJZHgf7-Sdx!?CGYFX*hF7_eeb+^OnDlZ``;Wp=DVYag&)E$K09-_nozt~)3D z2m4q}M($nIV_4$glc}x;cdJG z6Z`V{d(B4uP`8Zcf_vs@<9o^3bH$aeSY_60Gb`T^FZ($UuP9BiX|C|^hArp7T=V&g zqfwjMeEh|)gGy6auj1UXzGIwspXlYKF%Ra7Eaa_cy}Yy6i>+q7-TC{vr71SH)^O*J zhkx3LH@j)=#Px1kX#RB&yd8QbI`W`8q0;Yfb~nHFdwyEqIKA$goXyZHA^wkLUxfU; z!1I?+Wc)fYww+H%wFUj5yjoLL84m^ZHV|NS*x|>Y7cFS=EvL5yb+UH;YpOH87WyK#-@x& z4IZ5_c0^iM@aVB4wv6tjO= |r| r, .failure => |e| { + { + } this.log.addErrorFmt( null, logger.Loc.Empty, diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 3b4daaa54fe16..7cf06de46bf3d 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -59,8 +59,8 @@ pub const Flavor = enum { /// - "path" /// - "errno" pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { - const hasRetry = @hasDecl(ErrorTypeT, "retry"); - const hasTodo = @hasDecl(ErrorTypeT, "todo"); + const hasRetry = ErrorTypeT != void and @hasDecl(ErrorTypeT, "retry"); + const hasTodo = ErrorTypeT != void and @hasDecl(ErrorTypeT, "todo"); return union(Tag) { pub const ErrorType = ErrorTypeT; @@ -114,6 +114,23 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }; } + /// Unwrap the value if it is `result` or use the provided `default_value` + /// + /// `default_value` must be comptime known so the optimizer can optimize this branch out + pub inline fn unwrapOr(this: @This(), comptime default_value: ReturnType) ReturnType { + return switch (this) { + .result => |v| v, + .err => default_value, + }; + } + + pub inline fn unwrapOrNoOptmizations(this: @This(), default_value: ReturnType) ReturnType { + return switch (this) { + .result => |v| v, + .err => default_value, + }; + } + pub inline fn initErr(e: ErrorType) Maybe(ReturnType, ErrorType) { return .{ .err = e }; } @@ -131,10 +148,49 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { return null; } + pub inline fn asValue(this: *const @This()) ?ReturnType { + if (this.* == .result) return this.result; + return null; + } + + pub inline fn isOk(this: *const @This()) bool { + return switch (this.*) { + .result => true, + .err => false, + }; + } + + pub inline fn isErr(this: *const @This()) bool { + return switch (this.*) { + .result => false, + .err => true, + }; + } + pub inline fn initResult(result: ReturnType) Maybe(ReturnType, ErrorType) { return .{ .result = result }; } + pub inline fn mapErr(this: @This(), comptime E: type, err_fn: *const fn (ErrorTypeT) E) Maybe(ReturnType, E) { + return switch (this) { + .result => |v| .{ .result = v }, + .err => |e| .{ .err = err_fn(e) }, + }; + } + + pub inline fn toCssResult(this: @This()) Maybe(ReturnType, bun.css.ParseError(bun.css.ParserError)) { + return switch (ErrorTypeT) { + bun.css.BasicParseError => { + return switch (this) { + .result => |v| return .{ .result = v }, + .err => |e| return .{ .err = e.intoDefaultParseError() }, + }; + }, + bun.css.ParseError(bun.css.ParserError) => @compileError("Already a ParseError(ParserError)"), + else => @compileError("Bad!"), + }; + } + pub fn toJS(this: @This(), globalObject: *JSC.JSGlobalObject) JSC.JSValue { return switch (this) { .result => |r| switch (ReturnType) { diff --git a/src/bun.zig b/src/bun.zig index c15cd38740bd0..ca1f8415f46f1 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -39,6 +39,36 @@ pub const huge_allocator_threshold: comptime_int = @import("./memory_allocator.z pub const callmod_inline: std.builtin.CallModifier = if (builtin.mode == .Debug) .auto else .always_inline; pub const callconv_inline: std.builtin.CallingConvention = if (builtin.mode == .Debug) .Unspecified else .Inline; +/// Restrict a value to a certain interval unless it is a float and NaN. +pub inline fn clamp(self: anytype, min: @TypeOf(self), max: @TypeOf(self)) @TypeOf(self) { + bun.debugAssert(min <= max); + if (comptime (@TypeOf(self) == f32 or @TypeOf(self) == f64)) { + return clampFloat(self, min, max); + } + return std.math.clamp(self, min, max); +} + +/// Restrict a value to a certain interval unless it is NaN. +/// +/// Returns `max` if `self` is greater than `max`, and `min` if `self` is +/// less than `min`. Otherwise this returns `self`. +/// +/// Note that this function returns NaN if the initial value was NaN as +/// well. +pub inline fn clampFloat(_self: anytype, min: @TypeOf(_self), max: @TypeOf(_self)) @TypeOf(_self) { + if (comptime !(@TypeOf(_self) == f32 or @TypeOf(_self) == f64)) { + @compileError("Only call this on floats."); + } + var self = _self; + if (self < min) { + self = min; + } + if (self > max) { + self = max; + } + return self; +} + /// We cannot use a threadlocal memory allocator for FileSystem-related things /// FileSystem is a singleton. pub const fs_allocator = default_allocator; @@ -93,6 +123,8 @@ pub const ComptimeStringMapWithKeyType = comptime_string_map.ComptimeStringMapWi pub const glob = @import("./glob.zig"); pub const patch = @import("./patch.zig"); pub const ini = @import("./ini.zig"); +pub const Bitflags = @import("./bitflags.zig").Bitflags; +pub const css = @import("./css/css_parser.zig"); pub const shell = struct { pub usingnamespace @import("./shell/shell.zig"); diff --git a/src/bundler.zig b/src/bundler.zig index 6f2d8abda8072..540f879bd79d5 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -1003,75 +1003,111 @@ pub const Bundler = struct { Output.panic("TODO: dataurl, base64", .{}); // TODO }, .css => { - var file: bun.sys.File = undefined; + if (comptime bun.FeatureFlags.css) { + const Arena = @import("../src/mimalloc_arena.zig").Arena; + + var arena = Arena.init() catch @panic("oopsie arena no good"); + const alloc = arena.allocator(); + + const entry = bundler.resolver.caches.fs.readFileWithAllocator( + bundler.allocator, + bundler.fs, + file_path.text, + resolve_result.dirname_fd, + false, + null, + ) catch |err| { + bundler.log.addErrorFmt(null, logger.Loc.Empty, bundler.allocator, "{s} reading \"{s}\"", .{ @errorName(err), file_path.pretty }) catch {}; + return null; + }; + const source = logger.Source.initRecycledFile(.{ .path = file_path, .contents = entry.contents }, bundler.allocator) catch return null; + _ = source; // + switch (bun.css.StyleSheet(bun.css.DefaultAtRule).parse(alloc, entry.contents, bun.css.ParserOptions.default(alloc, bundler.log))) { + .result => |v| { + const result = v.toCss(alloc, bun.css.PrinterOptions{ + .minify = bun.getenvTruthy("BUN_CSS_MINIFY"), + }) catch |e| { + bun.handleErrorReturnTrace(e, @errorReturnTrace()); + return null; + }; + output_file.value = .{ .buffer = .{ .allocator = alloc, .bytes = result.code } }; + }, + .err => |e| { + bundler.log.addErrorFmt(null, logger.Loc.Empty, bundler.allocator, "{} parsing", .{e}) catch unreachable; + return null; + }, + } + } else { + var file: bun.sys.File = undefined; - if (Outstream == std.fs.Dir) { - const output_dir = outstream; + if (Outstream == std.fs.Dir) { + const output_dir = outstream; - if (std.fs.path.dirname(file_path.pretty)) |dirname| { - try output_dir.makePath(dirname); + if (std.fs.path.dirname(file_path.pretty)) |dirname| { + try output_dir.makePath(dirname); + } + file = bun.sys.File.from(try output_dir.createFile(file_path.pretty, .{})); + } else { + file = bun.sys.File.from(outstream); } - file = bun.sys.File.from(try output_dir.createFile(file_path.pretty, .{})); - } else { - file = bun.sys.File.from(outstream); - } - const CSSBuildContext = struct { - origin: URL, - }; - const build_ctx = CSSBuildContext{ .origin = bundler.options.origin }; + const CSSBuildContext = struct { + origin: URL, + }; + const build_ctx = CSSBuildContext{ .origin = bundler.options.origin }; - const BufferedWriter = std.io.CountingWriter(std.io.BufferedWriter(8192, bun.sys.File.Writer)); - const CSSWriter = Css.NewWriter( - BufferedWriter.Writer, - @TypeOf(&bundler.linker), - import_path_format, - CSSBuildContext, - ); - var buffered_writer = BufferedWriter{ - .child_stream = .{ .unbuffered_writer = file.writer() }, - .bytes_written = 0, - }; - const entry = bundler.resolver.caches.fs.readFile( - bundler.fs, - file_path.text, - resolve_result.dirname_fd, - !cache_files, - null, - ) catch return null; - - const _file = Fs.PathContentsPair{ .path = file_path, .contents = entry.contents }; - var source = try logger.Source.initFile(_file, bundler.allocator); - source.contents_is_recycled = !cache_files; - - var css_writer = CSSWriter.init( - &source, - buffered_writer.writer(), - &bundler.linker, - bundler.log, - ); + const BufferedWriter = std.io.CountingWriter(std.io.BufferedWriter(8192, bun.sys.File.Writer)); + const CSSWriter = Css.NewWriter( + BufferedWriter.Writer, + @TypeOf(&bundler.linker), + import_path_format, + CSSBuildContext, + ); + var buffered_writer = BufferedWriter{ + .child_stream = .{ .unbuffered_writer = file.writer() }, + .bytes_written = 0, + }; + const entry = bundler.resolver.caches.fs.readFile( + bundler.fs, + file_path.text, + resolve_result.dirname_fd, + !cache_files, + null, + ) catch return null; + + const _file = Fs.PathContentsPair{ .path = file_path, .contents = entry.contents }; + var source = try logger.Source.initFile(_file, bundler.allocator); + source.contents_is_recycled = !cache_files; + + var css_writer = CSSWriter.init( + &source, + buffered_writer.writer(), + &bundler.linker, + bundler.log, + ); - css_writer.buildCtx = build_ctx; + css_writer.buildCtx = build_ctx; - try css_writer.run(bundler.log, bundler.allocator); - try css_writer.ctx.context.child_stream.flush(); - output_file.size = css_writer.ctx.context.bytes_written; - var file_op = options.OutputFile.FileOperation.fromFile(file.handle, file_path.pretty); + try css_writer.run(bundler.log, bundler.allocator); + try css_writer.ctx.context.child_stream.flush(); + output_file.size = css_writer.ctx.context.bytes_written; + var file_op = options.OutputFile.FileOperation.fromFile(file.handle, file_path.pretty); - file_op.fd = bun.toFD(file.handle); + file_op.fd = bun.toFD(file.handle); - file_op.is_tmpdir = false; + file_op.is_tmpdir = false; - if (Outstream == std.fs.Dir) { - file_op.dir = bun.toFD(outstream.fd); + if (Outstream == std.fs.Dir) { + file_op.dir = bun.toFD(outstream.fd); - if (bundler.fs.fs.needToCloseFiles()) { - file.close(); - file_op.fd = .zero; + if (bundler.fs.fs.needToCloseFiles()) { + file.close(); + file_op.fd = .zero; + } } - } - output_file.value = .{ .move = file_op }; + output_file.value = .{ .move = file_op }; + } }, .bunsh, .sqlite_embedded, .sqlite, .wasm, .file, .napi => { diff --git a/src/comptime_string_map.zig b/src/comptime_string_map.zig index 0dad921d9fa76..6c781e2bee6e7 100644 --- a/src/comptime_string_map.zig +++ b/src/comptime_string_map.zig @@ -191,6 +191,34 @@ pub fn ComptimeStringMapWithKeyType(comptime KeyType: type, comptime V: type, co return str.inMapCaseInsensitive(@This()); } + pub fn getASCIIICaseInsensitive(input: anytype) ?V { + return getWithEqlLowercase(input, bun.strings.eqlComptime); + } + + pub fn getWithEqlLowercase(input: anytype, comptime eql: anytype) ?V { + const Input = @TypeOf(input); + const length = if (@hasField(Input, "len")) input.len else input.length(); + if (length < precomputed.min_len or length > precomputed.max_len) + return null; + + comptime var i: usize = precomputed.min_len; + inline while (i <= precomputed.max_len) : (i += 1) { + if (length == i) { + const lowerbuf: [length]u8 = brk: { + var buf: [length]u8 = undefined; + for (input[0..length].*, &buf) |c, *j| { + j.* = std.ascii.toLower(c); + } + break :brk buf; + }; + + return getWithLengthAndEql(&lowerbuf, i, eql); + } + } + + return null; + } + pub fn getWithEql(input: anytype, comptime eql: anytype) ?V { const Input = @TypeOf(input); const length = if (@hasField(Input, "len")) input.len else input.length(); @@ -207,6 +235,36 @@ pub fn ComptimeStringMapWithKeyType(comptime KeyType: type, comptime V: type, co return null; } + pub fn getAnyCase(input: anytype) ?V { + return getCaseInsensitiveWithEql(input, bun.strings.eqlComptimeIgnoreLen); + } + + pub fn getCaseInsensitiveWithEql(input: anytype, comptime eql: anytype) ?V { + const Input = @TypeOf(input); + const length = if (@hasField(Input, "len")) input.len else input.length(); + if (length < precomputed.min_len or length > precomputed.max_len) + return null; + + comptime var i: usize = precomputed.min_len; + inline while (i <= precomputed.max_len) : (i += 1) { + if (length == i) { + const lowercased: [i]u8 = brk: { + var buf: [i]u8 = undefined; + for (input[0..i], &buf) |c, *b| { + b.* = switch (c) { + 'A'...'Z' => c + 32, + else => c, + }; + } + break :brk buf; + }; + return getWithLengthAndEql(&lowercased, i, eql); + } + } + + return null; + } + pub fn getWithEqlList(input: anytype, comptime eql: anytype) ?V { const Input = @TypeOf(input); const length = if (@hasField(Input, "len")) input.len else input.length(); diff --git a/src/css/README.md b/src/css/README.md new file mode 100644 index 0000000000000..75b7dead60266 --- /dev/null +++ b/src/css/README.md @@ -0,0 +1,3 @@ +# CSS + +This is the code for Bun's experimental CSS parser. This code is derived from the [Lightning CSS](https://github.com/parcel-bundler/lightningcss) (huge, huge thanks to Devon Govett and contributors) project and the [Servo](https://github.com/servo/servo) project. diff --git a/src/css/build-prefixes.js b/src/css/build-prefixes.js new file mode 100644 index 0000000000000..bb36e786692b9 --- /dev/null +++ b/src/css/build-prefixes.js @@ -0,0 +1,745 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// const { execSync } = require("child_process"); +const prefixes = require("autoprefixer/data/prefixes"); +const browsers = require("caniuse-lite").agents; +const unpack = require("caniuse-lite").feature; +const features = require("caniuse-lite").features; +const mdn = require("@mdn/browser-compat-data"); +const fs = require("fs"); + +const BROWSER_MAPPING = { + and_chr: "chrome", + and_ff: "firefox", + ie_mob: "ie", + op_mob: "opera", + and_qq: null, + and_uc: null, + baidu: null, + bb: null, + kaios: null, + op_mini: null, + oculus: null, +}; + +const MDN_BROWSER_MAPPING = { + chrome_android: "chrome", + firefox_android: "firefox", + opera_android: "opera", + safari_ios: "ios_saf", + samsunginternet_android: "samsung", + webview_android: "android", + oculus: null, +}; + +const latestBrowserVersions = {}; +for (let b in browsers) { + let versions = browsers[b].versions.slice(-10); + for (let i = versions.length - 1; i >= 0; i--) { + if (versions[i] != null && versions[i] != "all" && versions[i] != "TP") { + latestBrowserVersions[b] = versions[i]; + break; + } + } +} + +// Caniuse data for clip-path is incorrect. +// https://github.com/Fyrd/caniuse/issues/6209 +prefixes["clip-path"].browsers = prefixes["clip-path"].browsers.filter(b => { + let [name, version] = b.split(" "); + return !( + (name === "safari" && parseVersion(version) >= ((9 << 16) | (1 << 8))) || + (name === "ios_saf" && parseVersion(version) >= ((9 << 16) | (3 << 8))) + ); +}); + +prefixes["any-pseudo"] = { + browsers: Object.entries(mdn.css.selectors.is.__compat.support).flatMap(([key, value]) => { + if (Array.isArray(value)) { + key = MDN_BROWSER_MAPPING[key] || key; + let any = value.find(v => v.alternative_name?.includes("-any"))?.version_added; + let supported = value.find(x => x.version_added && !x.alternative_name)?.version_added; + if (any && supported) { + let parts = supported.split("."); + parts[0]--; + supported = parts.join("."); + return [`${key} ${any}}`, `${key} ${supported}`]; + } + } + + return []; + }), +}; + +let flexSpec = {}; +let oldGradient = {}; +let p = new Map(); +for (let prop in prefixes) { + let browserMap = {}; + for (let b of prefixes[prop].browsers) { + let [name, version, variant] = b.split(" "); + if (BROWSER_MAPPING[name] === null) { + continue; + } + let prefix = browsers[name].prefix_exceptions?.[version] || browsers[name].prefix; + + // https://github.com/postcss/autoprefixer/blob/main/lib/hacks/backdrop-filter.js#L11 + if (prefix === "ms" && prop === "backdrop-filter") { + prefix = "webkit"; + } + + let origName = name; + let isCurrentVersion = version === latestBrowserVersions[name]; + name = BROWSER_MAPPING[name] || name; + let v = parseVersion(version); + if (v == null) { + console.log("BAD VERSION", prop, name, version); + continue; + } + if (browserMap[name]?.[prefix] == null) { + browserMap[name] = browserMap[name] || {}; + browserMap[name][prefix] = + prefixes[prop].browsers.filter(b => b.startsWith(origName) || b.startsWith(name)).length === 1 + ? isCurrentVersion + ? [null, null] + : [null, v] + : isCurrentVersion + ? [v, null] + : [v, v]; + } else { + if (v < browserMap[name][prefix][0]) { + browserMap[name][prefix][0] = v; + } + + if (isCurrentVersion && browserMap[name][prefix][0] != null) { + browserMap[name][prefix][1] = null; + } else if (v > browserMap[name][prefix][1] && browserMap[name][prefix][1] != null) { + browserMap[name][prefix][1] = v; + } + } + + if (variant === "2009") { + if (flexSpec[name] == null) { + flexSpec[name] = [v, v]; + } else { + if (v < flexSpec[name][0]) { + flexSpec[name][0] = v; + } + + if (v > flexSpec[name][1]) { + flexSpec[name][1] = v; + } + } + } else if (variant === "old" && prop.includes("gradient")) { + if (oldGradient[name] == null) { + oldGradient[name] = [v, v]; + } else { + if (v < oldGradient[name][0]) { + oldGradient[name][0] = v; + } + + if (v > oldGradient[name][1]) { + oldGradient[name][1] = v; + } + } + } + } + addValue(p, browserMap, prop); +} + +function addValue(map, value, prop) { + let s = JSON.stringify(value); + let found = false; + for (let [key, val] of map) { + if (JSON.stringify(val) === s) { + key.push(prop); + found = true; + break; + } + } + if (!found) { + map.set([prop], value); + } +} + +let cssFeatures = [ + "css-sel2", + "css-sel3", + "css-gencontent", + "css-first-letter", + "css-first-line", + "css-in-out-of-range", + "form-validation", + "css-any-link", + "css-default-pseudo", + "css-dir-pseudo", + "css-focus-within", + "css-focus-visible", + "css-indeterminate-pseudo", + "css-matches-pseudo", + "css-optional-pseudo", + "css-placeholder-shown", + "dialog", + "fullscreen", + "css-marker-pseudo", + "css-placeholder", + "css-selection", + "css-case-insensitive", + "css-read-only-write", + "css-autofill", + "css-namespaces", + "shadowdomv1", + "css-rrggbbaa", + "css-nesting", + "css-not-sel-list", + "css-has", + "font-family-system-ui", + "extended-system-fonts", + "calc", +]; + +let cssFeatureMappings = { + "css-dir-pseudo": "DirSelector", + "css-rrggbbaa": "HexAlphaColors", + "css-not-sel-list": "NotSelectorList", + "css-has": "HasSelector", + "css-matches-pseudo": "IsSelector", + "css-sel2": "Selectors2", + "css-sel3": "Selectors3", + "calc": "CalcFunction", +}; + +let cssFeatureOverrides = { + // Safari supports the ::marker pseudo element, but only supports styling some properties. + // However this does not break using the selector itself, so ignore for our purposes. + // https://bugs.webkit.org/show_bug.cgi?id=204163 + // https://github.com/parcel-bundler/lightningcss/issues/508 + "css-marker-pseudo": { + safari: { + "y #1": "y", + }, + }, +}; + +let compat = new Map(); +for (let feature of cssFeatures) { + let data = unpack(features[feature]); + let overrides = cssFeatureOverrides[feature]; + let browserMap = {}; + for (let name in data.stats) { + if (BROWSER_MAPPING[name] === null) { + continue; + } + + name = BROWSER_MAPPING[name] || name; + let browserOverrides = overrides?.[name]; + for (let version in data.stats[name]) { + let value = data.stats[name][version]; + value = browserOverrides?.[value] || value; + if (value === "y") { + let v = parseVersion(version); + if (v == null) { + console.log("BAD VERSION", feature, name, version); + continue; + } + + if (browserMap[name] == null || v < browserMap[name]) { + browserMap[name] = v; + } + } + } + } + + let name = (cssFeatureMappings[feature] || feature).replace(/^css-/, ""); + addValue(compat, browserMap, name); +} + +// No browser supports custom media queries yet. +addValue(compat, {}, "custom-media-queries"); + +let mdnFeatures = { + doublePositionGradients: mdn.css.types.image.gradient["radial-gradient"].doubleposition.__compat.support, + clampFunction: mdn.css.types.clamp.__compat.support, + placeSelf: mdn.css.properties["place-self"].__compat.support, + placeContent: mdn.css.properties["place-content"].__compat.support, + placeItems: mdn.css.properties["place-items"].__compat.support, + overflowShorthand: mdn.css.properties["overflow"].multiple_keywords.__compat.support, + mediaRangeSyntax: mdn.css["at-rules"].media.range_syntax.__compat.support, + mediaIntervalSyntax: Object.fromEntries( + Object.entries(mdn.css["at-rules"].media.range_syntax.__compat.support).map(([browser, value]) => { + // Firefox supported only ranges and not intervals for a while. + if (Array.isArray(value)) { + value = value.filter(v => !v.partial_implementation); + } else if (value.partial_implementation) { + value = undefined; + } + + return [browser, value]; + }), + ), + logicalBorders: mdn.css.properties["border-inline-start"].__compat.support, + logicalBorderShorthand: mdn.css.properties["border-inline"].__compat.support, + logicalBorderRadius: mdn.css.properties["border-start-start-radius"].__compat.support, + logicalMargin: mdn.css.properties["margin-inline-start"].__compat.support, + logicalMarginShorthand: mdn.css.properties["margin-inline"].__compat.support, + logicalPadding: mdn.css.properties["padding-inline-start"].__compat.support, + logicalPaddingShorthand: mdn.css.properties["padding-inline"].__compat.support, + logicalInset: mdn.css.properties["inset-inline-start"].__compat.support, + logicalSize: mdn.css.properties["inline-size"].__compat.support, + logicalTextAlign: mdn.css.properties["text-align"].start.__compat.support, + labColors: mdn.css.types.color.lab.__compat.support, + oklabColors: mdn.css.types.color.oklab.__compat.support, + colorFunction: mdn.css.types.color.color.__compat.support, + spaceSeparatedColorNotation: mdn.css.types.color.rgb.space_separated_parameters.__compat.support, + textDecorationThicknessPercent: mdn.css.properties["text-decoration-thickness"].percentage.__compat.support, + textDecorationThicknessShorthand: mdn.css.properties["text-decoration"].includes_thickness.__compat.support, + cue: mdn.css.selectors.cue.__compat.support, + cueFunction: mdn.css.selectors.cue.selector_argument.__compat.support, + anyPseudo: Object.fromEntries( + Object.entries(mdn.css.selectors.is.__compat.support).map(([key, value]) => { + if (Array.isArray(value)) { + value = value.filter(v => v.alternative_name?.includes("-any")).map(({ alternative_name, ...other }) => other); + } + + if (value && value.length) { + return [key, value]; + } else { + return [key, { version_added: false }]; + } + }), + ), + partPseudo: mdn.css.selectors.part.__compat.support, + imageSet: mdn.css.types.image["image-set"].__compat.support, + xResolutionUnit: mdn.css.types.resolution.x.__compat.support, + nthChildOf: mdn.css.selectors["nth-child"].of_syntax.__compat.support, + minFunction: mdn.css.types.min.__compat.support, + maxFunction: mdn.css.types.max.__compat.support, + roundFunction: mdn.css.types.round.__compat.support, + remFunction: mdn.css.types.rem.__compat.support, + modFunction: mdn.css.types.mod.__compat.support, + absFunction: mdn.css.types.abs.__compat.support, + signFunction: mdn.css.types.sign.__compat.support, + hypotFunction: mdn.css.types.hypot.__compat.support, + gradientInterpolationHints: mdn.css.types.image.gradient["linear-gradient"].interpolation_hints.__compat.support, + borderImageRepeatRound: mdn.css.properties["border-image-repeat"].round.__compat.support, + borderImageRepeatSpace: mdn.css.properties["border-image-repeat"].space.__compat.support, + fontSizeRem: mdn.css.properties["font-size"].rem_values.__compat.support, + fontSizeXXXLarge: mdn.css.properties["font-size"]["xxx-large"].__compat.support, + fontStyleObliqueAngle: mdn.css.properties["font-style"]["oblique-angle"].__compat.support, + fontWeightNumber: mdn.css.properties["font-weight"].number.__compat.support, + fontStretchPercentage: mdn.css.properties["font-stretch"].percentage.__compat.support, + lightDark: mdn.css.types.color["light-dark"].__compat.support, + accentSystemColor: mdn.css.types.color["system-color"].accentcolor_accentcolortext.__compat.support, + animationTimelineShorthand: mdn.css.properties.animation["animation-timeline_included"].__compat.support, +}; + +for (let key in mdn.css.types.length) { + if (key === "__compat") { + continue; + } + + let feat = key.includes("_") ? key.replace(/_([a-z])/g, (_, l) => l.toUpperCase()) : key + "Unit"; + + mdnFeatures[feat] = mdn.css.types.length[key].__compat.support; +} + +for (let key in mdn.css.types.image.gradient) { + if (key === "__compat") { + continue; + } + + let feat = key.replace(/-([a-z])/g, (_, l) => l.toUpperCase()); + mdnFeatures[feat] = mdn.css.types.image.gradient[key].__compat.support; +} + +const nonStandardListStyleType = new Set([ + // https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type#non-standard_extensions + "ethiopic-halehame", + "ethiopic-halehame-am", + "ethiopic-halehame-ti-er", + "ethiopic-halehame-ti-et", + "hangul", + "hangul-consonant", + "urdu", + "cjk-ideographic", + // https://github.com/w3c/csswg-drafts/issues/135 + "upper-greek", +]); + +for (let key in mdn.css.properties["list-style-type"]) { + if ( + key === "__compat" || + nonStandardListStyleType.has(key) || + mdn.css.properties["list-style-type"][key].__compat.support.chrome.version_removed + ) { + continue; + } + + let feat = key[0].toUpperCase() + key.slice(1).replace(/-([a-z])/g, (_, l) => l.toUpperCase()) + "ListStyleType"; + mdnFeatures[feat] = mdn.css.properties["list-style-type"][key].__compat.support; +} + +for (let key in mdn.css.properties["width"]) { + if (key === "__compat" || key === "animatable") { + continue; + } + + let feat = key[0].toUpperCase() + key.slice(1).replace(/[-_]([a-z])/g, (_, l) => l.toUpperCase()) + "Size"; + mdnFeatures[feat] = mdn.css.properties["width"][key].__compat.support; +} + +Object.entries(mdn.css.properties.width.stretch.__compat.support) + .filter(([, v]) => v.alternative_name) + .forEach(([k, v]) => { + let name = v.alternative_name.slice(1).replace(/[-_]([a-z])/g, (_, l) => l.toUpperCase()) + "Size"; + mdnFeatures[name] ??= {}; + mdnFeatures[name][k] = { version_added: v.version_added }; + }); + +for (let feature in mdnFeatures) { + let browserMap = {}; + for (let name in mdnFeatures[feature]) { + if (MDN_BROWSER_MAPPING[name] === null) { + continue; + } + + let feat = mdnFeatures[feature][name]; + let version; + if (Array.isArray(feat)) { + version = feat + .filter(x => x.version_added && !x.alternative_name && !x.flags) + .sort((a, b) => (parseVersion(a.version_added) < parseVersion(b.version_added) ? -1 : 1))[0].version_added; + } else if (!feat.alternative_name && !feat.flags) { + version = feat.version_added; + } + + if (!version) { + continue; + } + + let v = parseVersion(version); + if (v == null) { + console.log("BAD VERSION", feature, name, version); + continue; + } + + name = MDN_BROWSER_MAPPING[name] || name; + browserMap[name] = v; + } + + addValue(compat, browserMap, feature); +} + +addValue( + compat, + { + safari: parseVersion("10.1"), + ios_saf: parseVersion("10.3"), + }, + "p3Colors", +); + +addValue( + compat, + { + // https://github.com/WebKit/WebKit/commit/baed0d8b0abf366e1d9a6105dc378c59a5f21575 + safari: parseVersion("10.1"), + ios_saf: parseVersion("10.3"), + }, + "LangSelectorList", +); + +let prefixMapping = { + webkit: "webkit", + moz: "moz", + ms: "ms", + o: "o", +}; + +let flags = [ + "nesting", + "not_selector_list", + "dir_selector", + "lang_selector_list", + "is_selector", + "text_decoration_thickness_percent", + "media_interval_syntax", + "media_range_syntax", + "custom_media_queries", + "clamp_function", + "color_function", + "oklab_colors", + "lab_colors", + "p3_colors", + "hex_alpha_colors", + "space_separated_color_notation", + "font_family_system_ui", + "double_position_gradients", + "vendor_prefixes", + "logical_properties", + ["selectors", ["nesting", "not_selector_list", "dir_selector", "lang_selector_list", "is_selector"]], + ["media_queries", ["media_interval_syntax", "media_range_syntax", "custom_media_queries"]], + [ + "colors", + ["color_function", "oklab_colors", "lab_colors", "p3_colors", "hex_alpha_colors", "space_separated_color_notation"], + ], +]; + +function snakecase(str) { + let s = ""; + for (let i = 0; i < str.length; i++) { + let c = str[i].charCodeAt(0); + if (c === "-") { + s += "_"; + } else { + if (i > 0 && c >= 65 && c <= 90) { + s += "_"; + } + s += str[i].toLowerCase(); + } + } + return s; +} + +let enumify = f => + snakecase( + f + .replace(/^@([a-z])/, (_, x) => "at_" + x) + .replace(/^::([a-z])/, (_, x) => "pseudo_element_" + x) + .replace(/^:([a-z])/, (_, x) => "pseudo_class_" + x) + // .replace(/(^|-)([a-z])/g, (_, a, x) => (a === "-" ? "_" + x : x)); + .replace(/(^|-)([a-z])/g, (_, a, x) => (a === "-" ? "_" + x : x)), + ); + +let allBrowsers = Object.keys(browsers) + .filter(b => !(b in BROWSER_MAPPING)) + .sort(); +let browsersZig = `pub const Browsers = struct { + ${allBrowsers.join(": ?u32 = null,\n")}: ?u32 = null, + pub usingnamespace BrowsersImpl(@This()); +}`; +let flagsZig = `pub const Features = packed struct(u32) { + ${flags + .map((flag, i) => { + if (Array.isArray(flag)) { + // return `const ${flag[0]} = ${flag[1].map(f => `Self::${f}.bits()`).join(" | ")};`; + return `const ${flag[0]} = Features.fromNames(${flag[1].map(f => `"${f}"`).join(", ")});`; + } else { + return `${flag}: bool = 1 << ${i},`; + } + }) + .join("\n ")} + + pub usingnamespace css.Bitflags(@This()); + pub usingnamespace FeaturesImpl(@This()); + }`; +let targets = fs + .readFileSync("src/css/targets.zig", "utf8") + .replace(/pub const Browsers = struct \{((?:.|\n)+?)\}/, browsersZig) + .replace(/pub const Features = packed struct\(u32\) \{((?:.|\n)+?)\}/, flagsZig); + +console.log("TARGETS", targets); +fs.writeFileSync("src/css/targets.zig", targets); +await Bun.$`zig fmt src/css/targets.zig`; + +let targets_dts = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT! + +export interface Targets { + ${allBrowsers.join("?: number,\n ")}?: number +} + +export const Features: { + ${flags + .map((flag, i) => { + if (Array.isArray(flag)) { + return `${flag[0]}: ${flag[1].reduce((p, f) => p | (1 << flags.indexOf(f)), 0)},`; + } else { + return `${flag}: ${1 << i},`; + } + }) + .join("\n ")} +}; +`; + +// fs.writeFileSync("node/targets.d.ts", targets_dts); + +let flagsJs = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT! + +exports.Features = { + ${flags + .map((flag, i) => { + if (Array.isArray(flag)) { + return `${flag[0]}: ${flag[1].reduce((p, f) => p | (1 << flags.indexOf(f)), 0)},`; + } else { + return `${flag}: ${1 << i},`; + } + }) + .join("\n ")} +}; +`; + +// fs.writeFileSync("node/flags.js", flagsJs); + +let s = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT! + +const css = @import("./css_parser.zig"); +const VendorPrefix = css.VendorPrefix; +const Browsers = css.targets.Browsers; + +pub const Feature = enum { + ${[...p.keys()].flat().map(enumify).sort().join(",\n ")}, + + pub fn prefixesFor(this: *const Feature, browsers: Browsers) VendorPrefix { + var prefixes = VendorPrefix{ .none = true }; + switch (this.*) { + ${[...p] + .map(([features, versions]) => { + return `${features.map(name => `.${enumify(name)}`).join(" ,\n ")} => { + ${Object.entries(versions) + .map(([name, prefixes]) => { + let needsVersion = !Object.values(prefixes).every(([min, max]) => min == null && max == null); + return `if ${needsVersion ? `(browsers.${name}) |version|` : `(browsers.${name} != null)`} { + ${Object.entries(prefixes) + .map(([prefix, [min, max]]) => { + if (!prefixMapping[prefix]) { + throw new Error("Missing prefix " + prefix); + } + let addPrefix = `prefixes = prefixes.bitwiseOr(VendorPrefix{.${prefixMapping[prefix]} = true });`; + let condition; + if (min == null && max == null) { + return addPrefix; + } else if (min == null) { + condition = `version <= ${max}`; + } else if (max == null) { + condition = `version >= ${min}`; + } else if (min == max) { + condition = `version == ${min}`; + } else { + condition = `version >= ${min} and version <= ${max}`; + } + + return `if (${condition}) { + ${addPrefix} + }`; + }) + .join("\n ")} + }`; + }) + .join("\n ")} + }`; + }) + .join(",\n ")} + } + return prefixes; + } + +pub fn isFlex2009(browsers: Browsers) bool { + ${Object.entries(flexSpec) + .map(([name, [min, max]]) => { + return `if (browsers.${name}) |version| { + if (version >= ${min} and version <= ${max}) { + return true; + } + }`; + }) + .join("\n ")} + return false; +} + +pub fn isWebkitGradient(browsers: Browsers) bool { + ${Object.entries(oldGradient) + .map(([name, [min, max]]) => { + return `if (browsers.${name}) |version| { + if (version >= ${min} and version <= ${max}) { + return true; + } + }`; + }) + .join("\n ")} + return false; +} +}; +`; + +fs.writeFileSync("src/css/prefixes.zig", s); +await Bun.$`zig fmt src/css/prefixes.zig`; + +let c = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT! + +const Browsers = @import("./targets.zig").Browsers; + +pub const Feature = enum { + ${[...compat.keys()].flat().map(enumify).sort().join(",\n ")}, + + pub fn isCompatible(this: *const Feature, browsers: Browsers) bool { + switch (this.*) { + ${[...compat] + .map( + ([features, supportedBrowsers]) => + `${features.map(name => `.${enumify(name)}`).join(" ,\n ")} => {` + + (Object.entries(supportedBrowsers).length === 0 + ? "\n return false;\n }," + : ` + ${Object.entries(supportedBrowsers) + .map( + ([browser, min]) => + `if (browsers.${browser}) |version| { + if (version < ${min}) { + return false; + } + }`, + ) + .join("\n ")}${ + Object.keys(supportedBrowsers).length === allBrowsers.length + ? "" + : `\n if (${allBrowsers + .filter(b => !supportedBrowsers[b]) + .map(browser => `browsers.${browser} != null`) + .join(" or ")}) { + return false; + }` + } + },`), + ) + .join("\n ")} + } + return true; + } + + +pub fn isPartiallyCompatible(this: *const Feature, targets: Browsers) bool { + var browsers = Browsers{}; + ${allBrowsers + .map( + browser => `if (targets.${browser} != null) { + browsers.${browser} = targets.${browser}; + if (this.isCompatible(browsers)) { + return true; + } + browsers.${browser} = null; + }\n`, + ) + .join(" ")} + return false; +} +}; +`; + +fs.writeFileSync("src/css/compat.zig", c); +await Bun.$`zig fmt src/css/compat.zig`; + +function parseVersion(version) { + version = version.replace("≤", ""); + let [major, minor = "0", patch = "0"] = version + .split("-")[0] + .split(".") + .map(v => parseInt(v, 10)); + + if (isNaN(major) || isNaN(minor) || isNaN(patch)) { + return null; + } + + return (major << 16) | (minor << 8) | patch; +} diff --git a/src/css/compat.zig b/src/css/compat.zig new file mode 100644 index 0000000000000..01189df7cc54b --- /dev/null +++ b/src/css/compat.zig @@ -0,0 +1,5407 @@ +// This file is autogenerated by build-prefixes.js. DO NOT EDIT! + +const Browsers = @import("./targets.zig").Browsers; + +pub const Feature = enum { + abs_function, + accent_system_color, + afar_list_style_type, + amharic_abegede_list_style_type, + amharic_list_style_type, + anchor_size_size, + animation_timeline_shorthand, + any_link, + any_pseudo, + arabic_indic_list_style_type, + armenian_list_style_type, + asterisks_list_style_type, + auto_size, + autofill, + bengali_list_style_type, + binary_list_style_type, + border_image_repeat_round, + border_image_repeat_space, + calc_function, + cambodian_list_style_type, + cap_unit, + case_insensitive, + ch_unit, + circle_list_style_type, + cjk_decimal_list_style_type, + cjk_earthly_branch_list_style_type, + cjk_heavenly_stem_list_style_type, + clamp_function, + color_function, + conic_gradient, + container_query_length_units, + cue, + cue_function, + custom_media_queries, + decimal_leading_zero_list_style_type, + decimal_list_style_type, + default_pseudo, + devanagari_list_style_type, + dialog, + dir_selector, + disc_list_style_type, + disclosure_closed_list_style_type, + disclosure_open_list_style_type, + double_position_gradients, + em_unit, + ethiopic_abegede_am_et_list_style_type, + ethiopic_abegede_gez_list_style_type, + ethiopic_abegede_list_style_type, + ethiopic_abegede_ti_er_list_style_type, + ethiopic_abegede_ti_et_list_style_type, + ethiopic_halehame_aa_er_list_style_type, + ethiopic_halehame_aa_et_list_style_type, + ethiopic_halehame_am_et_list_style_type, + ethiopic_halehame_gez_list_style_type, + ethiopic_halehame_om_et_list_style_type, + ethiopic_halehame_sid_et_list_style_type, + ethiopic_halehame_so_et_list_style_type, + ethiopic_halehame_tig_list_style_type, + ethiopic_list_style_type, + ethiopic_numeric_list_style_type, + ex_unit, + extended_system_fonts, + first_letter, + first_line, + fit_content_function_size, + fit_content_size, + focus_visible, + focus_within, + font_family_system_ui, + font_size_rem, + font_size_x_x_x_large, + font_stretch_percentage, + font_style_oblique_angle, + font_weight_number, + footnotes_list_style_type, + form_validation, + fullscreen, + gencontent, + georgian_list_style_type, + gradient_interpolation_hints, + gujarati_list_style_type, + gurmukhi_list_style_type, + has_selector, + hebrew_list_style_type, + hex_alpha_colors, + hiragana_iroha_list_style_type, + hiragana_list_style_type, + hypot_function, + ic_unit, + image_set, + in_out_of_range, + indeterminate_pseudo, + is_animatable_size, + is_selector, + japanese_formal_list_style_type, + japanese_informal_list_style_type, + kannada_list_style_type, + katakana_iroha_list_style_type, + katakana_list_style_type, + khmer_list_style_type, + korean_hangul_formal_list_style_type, + korean_hanja_formal_list_style_type, + korean_hanja_informal_list_style_type, + lab_colors, + lang_selector_list, + lao_list_style_type, + lh_unit, + light_dark, + linear_gradient, + logical_border_radius, + logical_border_shorthand, + logical_borders, + logical_inset, + logical_margin, + logical_margin_shorthand, + logical_padding, + logical_padding_shorthand, + logical_size, + logical_text_align, + lower_alpha_list_style_type, + lower_armenian_list_style_type, + lower_greek_list_style_type, + lower_hexadecimal_list_style_type, + lower_latin_list_style_type, + lower_norwegian_list_style_type, + lower_roman_list_style_type, + malayalam_list_style_type, + marker_pseudo, + max_content_size, + max_function, + media_interval_syntax, + media_range_syntax, + min_content_size, + min_function, + mod_function, + mongolian_list_style_type, + moz_available_size, + myanmar_list_style_type, + namespaces, + nesting, + none_list_style_type, + not_selector_list, + nth_child_of, + octal_list_style_type, + oklab_colors, + optional_pseudo, + oriya_list_style_type, + oromo_list_style_type, + overflow_shorthand, + p3_colors, + part_pseudo, + persian_list_style_type, + place_content, + place_items, + place_self, + placeholder, + placeholder_shown, + q_unit, + radial_gradient, + rcap_unit, + rch_unit, + read_only_write, + rem_function, + rem_unit, + repeating_conic_gradient, + repeating_linear_gradient, + repeating_radial_gradient, + rex_unit, + ric_unit, + rlh_unit, + round_function, + selection, + selectors2, + selectors3, + shadowdomv1, + sidama_list_style_type, + sign_function, + simp_chinese_formal_list_style_type, + simp_chinese_informal_list_style_type, + somali_list_style_type, + space_separated_color_notation, + square_list_style_type, + stretch_size, + string_list_style_type, + symbols_list_style_type, + tamil_list_style_type, + telugu_list_style_type, + text_decoration_thickness_percent, + text_decoration_thickness_shorthand, + thai_list_style_type, + tibetan_list_style_type, + tigre_list_style_type, + tigrinya_er_abegede_list_style_type, + tigrinya_er_list_style_type, + tigrinya_et_abegede_list_style_type, + tigrinya_et_list_style_type, + trad_chinese_formal_list_style_type, + trad_chinese_informal_list_style_type, + upper_alpha_list_style_type, + upper_armenian_list_style_type, + upper_hexadecimal_list_style_type, + upper_latin_list_style_type, + upper_norwegian_list_style_type, + upper_roman_list_style_type, + vb_unit, + vh_unit, + vi_unit, + viewport_percentage_units_dynamic, + viewport_percentage_units_large, + viewport_percentage_units_small, + vmax_unit, + vmin_unit, + vw_unit, + webkit_fill_available_size, + x_resolution_unit, + + pub fn isCompatible(this: *const Feature, browsers: Browsers) bool { + switch (this.*) { + .selectors2 => { + if (browsers.ie) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 131072) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 196864) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 197120) { + return false; + } + } + if (browsers.android) |version| { + if (version < 131328) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + }, + .selectors3 => { + if (browsers.ie) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 197888) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 197120) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 591104) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 197120) { + return false; + } + } + if (browsers.android) |version| { + if (version < 131328) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + }, + .gencontent, .first_line => { + if (browsers.ie) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 131072) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 196864) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 197120) { + return false; + } + } + if (browsers.android) |version| { + if (version < 131328) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + }, + .first_letter => { + if (browsers.ie) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 197888) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327936) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 722432) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.android) |version| { + if (version < 196608) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + }, + .in_out_of_range => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3276800) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 3473408) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2621440) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 656128) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .form_validation => { + if (browsers.ie) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 656128) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263171) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + }, + .any_link => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3276800) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 4259840) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3407872) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 590336) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .default_pseudo => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 3342336) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2490368) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 656128) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .dir_selector => { + if (browsers.edge) |version| { + if (version < 7864320) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3211264) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 7864320) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 6946816) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1638400) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .focus_within => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3407872) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 3932160) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3080192) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 656128) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 524800) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .focus_visible => { + if (browsers.edge) |version| { + if (version < 5636096) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5570560) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 5636096) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4718592) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .indeterminate_pseudo => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3342336) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 2555904) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 1703936) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 656128) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .is_selector => { + if (browsers.edge) |version| { + if (version < 5767168) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5111808) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 5767168) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4915200) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .optional_pseudo => { + if (browsers.ie) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.android) |version| { + if (version < 131840) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + }, + .placeholder_shown => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3342336) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 3080192) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2228224) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .dialog => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 6422528) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 2424832) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 1572864) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .fullscreen => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 4194304) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 4653056) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 786688) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.android != null or browsers.ie != null or browsers.ios_saf != null) { + return false; + } + }, + .marker_pseudo => { + if (browsers.edge) |version| { + if (version < 5636096) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 4456448) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 5636096) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 721152) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4718592) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 721664) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .placeholder => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3342336) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 3735552) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2883584) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 656128) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 459264) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .selection => { + if (browsers.ie) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 196864) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 591104) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ios_saf != null) { + return false; + } + }, + .case_insensitive => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3080192) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 3211264) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2359296) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .read_only_write => { + if (browsers.edge) |version| { + if (version < 851968) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5111808) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 2359296) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 1507328) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .autofill => { + if (browsers.chrome) |version| { + if (version < 7208960) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7208960) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5636096) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 6291456) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1376256) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .namespaces => { + if (browsers.ie) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 131072) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 262656) { + return false; + } + } + if (browsers.android) |version| { + if (version < 131328) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + }, + .shadowdomv1 => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 4128768) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 3473408) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2621440) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 393728) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .hex_alpha_colors => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3211264) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3407872) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 524800) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .nesting => { + if (browsers.edge) |version| { + if (version < 7864320) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7667712) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 7864320) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1114624) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 6946816) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1114624) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.ie != null or browsers.samsung != null) { + return false; + } + }, + .not_selector_list => { + if (browsers.edge) |version| { + if (version < 5767168) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5505024) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 5767168) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4915200) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .has_selector => { + if (browsers.edge) |version| { + if (version < 6881280) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7929856) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 6881280) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 5963776) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1310720) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .font_family_system_ui => { + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 6029312) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 3670016) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2818048) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 393728) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .extended_system_fonts => { + if (browsers.safari) |version| { + if (version < 852224) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 852992) { + return false; + } + } + if (browsers.android != null or browsers.chrome != null or browsers.edge != null or browsers.firefox != null or browsers.ie != null or browsers.opera != null or browsers.samsung != null) { + return false; + } + }, + .calc_function => { + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 1048576) { + return false; + } + } + if (browsers.chrome) |version| { + if (version < 1703936) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 393472) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8323072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .custom_media_queries, .fit_content_function_size, .stretch_size => { + return false; + }, + .double_position_gradients => { + if (browsers.chrome) |version| { + if (version < 4653056) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 4194304) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3276800) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 786688) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 786944) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4653056) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .clamp_function => { + if (browsers.chrome) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3735552) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 852224) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 852992) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .place_self, .place_items => { + if (browsers.chrome) |version| { + if (version < 3866624) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2949120) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2818048) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.android) |version| { + if (version < 3866624) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .place_content => { + if (browsers.chrome) |version| { + if (version < 3866624) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2949120) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2818048) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.android) |version| { + if (version < 3866624) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .overflow_shorthand => { + if (browsers.chrome) |version| { + if (version < 4456448) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3997696) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3145728) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 852224) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 852992) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4456448) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .media_range_syntax => { + if (browsers.chrome) |version| { + if (version < 6815744) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 6815744) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 4128768) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4653056) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1310720) { + return false; + } + } + if (browsers.android) |version| { + if (version < 6815744) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .media_interval_syntax => { + if (browsers.chrome) |version| { + if (version < 6815744) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 6815744) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 6684672) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4653056) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1310720) { + return false; + } + } + if (browsers.android) |version| { + if (version < 6815744) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .logical_borders => { + if (browsers.chrome) |version| { + if (version < 4521984) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2686976) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3145728) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 786688) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 786944) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4521984) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .logical_border_shorthand, .logical_margin_shorthand, .logical_padding_shorthand => { + if (browsers.chrome) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 4325376) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 917760) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 918784) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .logical_border_radius => { + if (browsers.chrome) |version| { + if (version < 5832704) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5832704) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 4325376) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4128768) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5832704) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .logical_margin, .logical_padding => { + if (browsers.chrome) |version| { + if (version < 4521984) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2686976) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3145728) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 786688) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 786944) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .logical_inset => { + if (browsers.chrome) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 4128768) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 917760) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 918784) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .logical_size => { + if (browsers.chrome) |version| { + if (version < 3735552) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2686976) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2818048) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 786688) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 786944) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.android) |version| { + if (version < 3735552) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .logical_text_align => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 196864) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 131072) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 2424832) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .lab_colors => { + if (browsers.chrome) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7405568) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4915200) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1441792) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .oklab_colors => { + if (browsers.chrome) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7405568) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4915200) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1441792) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .color_function => { + if (browsers.chrome) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7405568) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4915200) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 656128) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1441792) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .space_separated_color_notation => { + if (browsers.chrome) |version| { + if (version < 4259840) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3407872) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3080192) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 786688) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 786944) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4259840) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .text_decoration_thickness_percent => { + if (browsers.chrome) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1115136) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1115136) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .text_decoration_thickness_shorthand => { + if (browsers.chrome) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5701632) { + return false; + } + } + if (browsers.ie != null or browsers.ios_saf != null or browsers.safari != null) { + return false; + } + }, + .cue => { + if (browsers.chrome) |version| { + if (version < 1703936) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3604480) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .cue_function => { + if (browsers.chrome) |version| { + if (version < 1703936) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.firefox != null or browsers.ie != null) { + return false; + } + }, + .any_pseudo => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 2424832) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .part_pseudo => { + if (browsers.chrome) |version| { + if (version < 4784128) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3407872) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 852224) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 852992) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4784128) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .image_set => { + if (browsers.chrome) |version| { + if (version < 1638400) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5767168) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 393216) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 393216) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .x_resolution_unit => { + if (browsers.chrome) |version| { + if (version < 4456448) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3145728) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4456448) { + return false; + } + } + if (browsers.ie != null or browsers.ios_saf != null or browsers.safari != null) { + return false; + } + }, + .nth_child_of => { + if (browsers.chrome) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7405568) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4915200) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1441792) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .min_function, .max_function => { + if (browsers.chrome) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3735552) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 721152) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 721664) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .round_function, .rem_function, .mod_function => { + if (browsers.chrome) |version| { + if (version < 8192000) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 8192000) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7733248) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 5439488) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8192000) { + return false; + } + } + if (browsers.ie != null or browsers.samsung != null) { + return false; + } + }, + .abs_function, .sign_function => { + if (browsers.firefox) |version| { + if (version < 7733248) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.android != null or browsers.chrome != null or browsers.edge != null or browsers.ie != null or browsers.opera != null or browsers.samsung != null) { + return false; + } + }, + .hypot_function => { + if (browsers.chrome) |version| { + if (version < 7864320) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7864320) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7733248) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 5242880) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1638400) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7864320) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .gradient_interpolation_hints => { + if (browsers.chrome) |version| { + if (version < 2621440) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2359296) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 1769472) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.android) |version| { + if (version < 2621440) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .border_image_repeat_round => { + if (browsers.chrome) |version| { + if (version < 1966080) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 590080) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 590592) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 131072) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .border_image_repeat_space => { + if (browsers.chrome) |version| { + if (version < 3670016) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3276800) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2818048) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 590080) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 590592) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 393216) { + return false; + } + } + if (browsers.android) |version| { + if (version < 3670016) { + return false; + } + } + }, + .font_size_rem => { + if (browsers.chrome) |version| { + if (version < 2752512) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2031616) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 1835008) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.android) |version| { + if (version < 2752512) { + return false; + } + } + }, + .font_size_x_x_x_large => { + if (browsers.chrome) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3735552) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .font_style_oblique_angle => { + if (browsers.chrome) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3997696) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3014656) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 721152) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 721664) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 524288) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .font_weight_number => { + if (browsers.chrome) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 1114112) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3997696) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3014656) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 524288) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .font_stretch_percentage => { + if (browsers.chrome) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3997696) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3014656) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 721152) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 721664) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 524288) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4063232) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .light_dark => { + if (browsers.chrome) |version| { + if (version < 8060928) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 8060928) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7864320) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 5373952) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1115392) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1115392) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8060928) { + return false; + } + } + if (browsers.ie != null or browsers.samsung != null) { + return false; + } + }, + .accent_system_color => { + if (browsers.firefox) |version| { + if (version < 6750208) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1049856) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1049856) { + return false; + } + } + if (browsers.android != null or browsers.chrome != null or browsers.edge != null or browsers.ie != null or browsers.opera != null or browsers.samsung != null) { + return false; + } + }, + .animation_timeline_shorthand => { + if (browsers.chrome) |version| { + if (version < 7536640) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7536640) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 5046272) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1507328) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7536640) { + return false; + } + } + if (browsers.firefox != null or browsers.ie != null or browsers.ios_saf != null or browsers.safari != null) { + return false; + } + }, + .q_unit => { + if (browsers.chrome) |version| { + if (version < 4128768) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 3211264) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3014656) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 852224) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 852992) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 524288) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4128768) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .cap_unit => { + if (browsers.chrome) |version| { + if (version < 7733248) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7733248) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 6356992) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1114624) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1114624) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1638400) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7733248) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .ch_unit => { + if (browsers.chrome) |version| { + if (version < 1769472) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .container_query_length_units => { + if (browsers.chrome) |version| { + if (version < 6881280) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 6881280) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7208960) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4718592) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1048576) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1048576) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1310720) { + return false; + } + } + if (browsers.android) |version| { + if (version < 6881280) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .em_unit => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 196608) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 65536) { + return false; + } + } + }, + .ex_unit, .circle_list_style_type, .decimal_list_style_type, .disc_list_style_type, .square_list_style_type => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .ic_unit => { + if (browsers.chrome) |version| { + if (version < 6946816) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 6946816) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 6356992) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4718592) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1310720) { + return false; + } + } + if (browsers.android) |version| { + if (version < 6946816) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .lh_unit => { + if (browsers.chrome) |version| { + if (version < 7143424) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7143424) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7864320) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4849664) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1376256) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7143424) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .rcap_unit => { + if (browsers.chrome) |version| { + if (version < 7733248) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7733248) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1114624) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1114624) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1638400) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7733248) { + return false; + } + } + if (browsers.firefox != null or browsers.ie != null) { + return false; + } + }, + .rch_unit, .rex_unit, .ric_unit => { + if (browsers.chrome) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4915200) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1114624) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1114624) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1441792) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.firefox != null or browsers.ie != null) { + return false; + } + }, + .rem_unit => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 131072) { + return false; + } + } + }, + .rlh_unit => { + if (browsers.chrome) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 7864320) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4915200) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 1049600) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1441792) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7274496) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .vb_unit, .vi_unit, .viewport_percentage_units_dynamic, .viewport_percentage_units_large, .viewport_percentage_units_small => { + if (browsers.chrome) |version| { + if (version < 7077888) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 7077888) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 6619136) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4784128) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 984064) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1376256) { + return false; + } + } + if (browsers.android) |version| { + if (version < 7077888) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .vh_unit, .vw_unit => { + if (browsers.chrome) |version| { + if (version < 1638400) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 1245184) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 589824) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 393216) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 393216) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .vmax_unit => { + if (browsers.chrome) |version| { + if (version < 1703936) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 1048576) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 1245184) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.android) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .vmin_unit => { + if (browsers.chrome) |version| { + if (version < 1703936) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 1245184) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .conic_gradient, .repeating_conic_gradient => { + if (browsers.chrome) |version| { + if (version < 4521984) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5439488) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3145728) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 786688) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 786944) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.android) |version| { + if (version < 4521984) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .linear_gradient, .repeating_linear_gradient => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327936) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 2424832) { + return false; + } + } + }, + .radial_gradient => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327936) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 2424832) { + return false; + } + } + }, + .repeating_radial_gradient => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 655360) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327936) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .afar_list_style_type, .amharic_list_style_type, .amharic_abegede_list_style_type, .ethiopic_list_style_type, .ethiopic_abegede_list_style_type, .ethiopic_abegede_am_et_list_style_type, .ethiopic_abegede_gez_list_style_type, .ethiopic_abegede_ti_er_list_style_type, .ethiopic_abegede_ti_et_list_style_type, .ethiopic_halehame_aa_er_list_style_type, .ethiopic_halehame_aa_et_list_style_type, .ethiopic_halehame_am_et_list_style_type, .ethiopic_halehame_gez_list_style_type, .ethiopic_halehame_om_et_list_style_type, .ethiopic_halehame_sid_et_list_style_type, .ethiopic_halehame_so_et_list_style_type, .ethiopic_halehame_tig_list_style_type, .lower_hexadecimal_list_style_type, .lower_norwegian_list_style_type, .upper_hexadecimal_list_style_type, .upper_norwegian_list_style_type => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5963776) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 262656) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 196608) { + return false; + } + } + if (browsers.firefox != null or browsers.ie != null) { + return false; + } + }, + .arabic_indic_list_style_type, .bengali_list_style_type, .cjk_earthly_branch_list_style_type, .cjk_heavenly_stem_list_style_type, .devanagari_list_style_type, .gujarati_list_style_type, .gurmukhi_list_style_type, .kannada_list_style_type, .khmer_list_style_type, .lao_list_style_type, .malayalam_list_style_type, .myanmar_list_style_type, .oriya_list_style_type, .persian_list_style_type, .telugu_list_style_type, .thai_list_style_type => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 262656) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .armenian_list_style_type, .decimal_leading_zero_list_style_type, .georgian_list_style_type, .lower_alpha_list_style_type, .lower_greek_list_style_type, .lower_roman_list_style_type, .upper_alpha_list_style_type, .upper_latin_list_style_type, .upper_roman_list_style_type => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 524288) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .asterisks_list_style_type, .footnotes_list_style_type => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5963776) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327936) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.firefox != null or browsers.ie != null) { + return false; + } + }, + .binary_list_style_type, .octal_list_style_type, .oromo_list_style_type, .sidama_list_style_type, .somali_list_style_type, .tigre_list_style_type, .tigrinya_er_list_style_type, .tigrinya_er_abegede_list_style_type, .tigrinya_et_list_style_type, .tigrinya_et_abegede_list_style_type => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5963776) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 262656) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.firefox != null or browsers.ie != null) { + return false; + } + }, + .cambodian_list_style_type, .mongolian_list_style_type, .tibetan_list_style_type => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2162688) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 262656) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .cjk_decimal_list_style_type => { + if (browsers.chrome) |version| { + if (version < 5963776) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5963776) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 1835008) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4194304) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1048576) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5963776) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .disclosure_closed_list_style_type, .disclosure_open_list_style_type => { + if (browsers.chrome) |version| { + if (version < 5832704) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5832704) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2162688) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4128768) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5832704) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .ethiopic_numeric_list_style_type, .japanese_formal_list_style_type, .japanese_informal_list_style_type, .tamil_list_style_type => { + if (browsers.chrome) |version| { + if (version < 5963776) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5963776) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 4194304) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 1048576) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5963776) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .hebrew_list_style_type, .hiragana_list_style_type, .hiragana_iroha_list_style_type, .katakana_list_style_type, .katakana_iroha_list_style_type, .auto_size => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .korean_hangul_formal_list_style_type, .korean_hanja_formal_list_style_type, .korean_hanja_informal_list_style_type => { + if (browsers.chrome) |version| { + if (version < 2949120) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 1835008) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2097152) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.android) |version| { + if (version < 2949120) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .lower_armenian_list_style_type, .upper_armenian_list_style_type => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2162688) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 327936) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .lower_latin_list_style_type => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 524288) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .none_list_style_type => { + if (browsers.chrome) |version| { + if (version < 1179648) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 65536) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .simp_chinese_formal_list_style_type, .simp_chinese_informal_list_style_type, .trad_chinese_formal_list_style_type, .trad_chinese_informal_list_style_type => { + if (browsers.chrome) |version| { + if (version < 2949120) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2097152) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 983040) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.android) |version| { + if (version < 2949120) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .string_list_style_type => { + if (browsers.chrome) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 2555904) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 3735552) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 917760) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 918784) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.android) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .symbols_list_style_type => { + if (browsers.firefox) |version| { + if (version < 2293760) { + return false; + } + } + if (browsers.android != null or browsers.chrome != null or browsers.edge != null or browsers.ie != null or browsers.ios_saf != null or browsers.opera != null or browsers.safari != null or browsers.samsung != null) { + return false; + } + }, + .anchor_size_size => { + if (browsers.chrome) |version| { + if (version < 8192000) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 8192000) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 5439488) { + return false; + } + } + if (browsers.android) |version| { + if (version < 8192000) { + return false; + } + } + if (browsers.firefox != null or browsers.ie != null or browsers.ios_saf != null or browsers.safari != null or browsers.samsung != null) { + return false; + } + }, + .fit_content_size => { + if (browsers.chrome) |version| { + if (version < 1638400) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .is_animatable_size => { + if (browsers.chrome) |version| { + if (version < 1703936) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 786432) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 1048576) { + return false; + } + } + if (browsers.ie) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + }, + .max_content_size => { + if (browsers.chrome) |version| { + if (version < 1638400) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2818048) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 66816) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .min_content_size => { + if (browsers.chrome) |version| { + if (version < 3014656) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 2162688) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 720896) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.android) |version| { + if (version < 3014656) { + return false; + } + } + if (browsers.ie != null) { + return false; + } + }, + .webkit_fill_available_size => { + if (browsers.chrome) |version| { + if (version < 1638400) { + return false; + } + } + if (browsers.edge) |version| { + if (version < 5177344) { + return false; + } + } + if (browsers.opera) |version| { + if (version < 917504) { + return false; + } + } + if (browsers.safari) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 458752) { + return false; + } + } + if (browsers.samsung) |version| { + if (version < 327680) { + return false; + } + } + if (browsers.android) |version| { + if (version < 263168) { + return false; + } + } + if (browsers.firefox != null or browsers.ie != null) { + return false; + } + }, + .moz_available_size => { + if (browsers.firefox) |version| { + if (version < 262144) { + return false; + } + } + if (browsers.android != null or browsers.chrome != null or browsers.edge != null or browsers.ie != null or browsers.ios_saf != null or browsers.opera != null or browsers.safari != null or browsers.samsung != null) { + return false; + } + }, + .p3_colors, .lang_selector_list => { + if (browsers.safari) |version| { + if (version < 655616) { + return false; + } + } + if (browsers.ios_saf) |version| { + if (version < 656128) { + return false; + } + } + if (browsers.android != null or browsers.chrome != null or browsers.edge != null or browsers.firefox != null or browsers.ie != null or browsers.opera != null or browsers.samsung != null) { + return false; + } + }, + } + return true; + } + + pub fn isPartiallyCompatible(this: *const Feature, targets: Browsers) bool { + var browsers = Browsers{}; + if (targets.android != null) { + browsers.android = targets.android; + if (this.isCompatible(browsers)) { + return true; + } + browsers.android = null; + } + if (targets.chrome != null) { + browsers.chrome = targets.chrome; + if (this.isCompatible(browsers)) { + return true; + } + browsers.chrome = null; + } + if (targets.edge != null) { + browsers.edge = targets.edge; + if (this.isCompatible(browsers)) { + return true; + } + browsers.edge = null; + } + if (targets.firefox != null) { + browsers.firefox = targets.firefox; + if (this.isCompatible(browsers)) { + return true; + } + browsers.firefox = null; + } + if (targets.ie != null) { + browsers.ie = targets.ie; + if (this.isCompatible(browsers)) { + return true; + } + browsers.ie = null; + } + if (targets.ios_saf != null) { + browsers.ios_saf = targets.ios_saf; + if (this.isCompatible(browsers)) { + return true; + } + browsers.ios_saf = null; + } + if (targets.opera != null) { + browsers.opera = targets.opera; + if (this.isCompatible(browsers)) { + return true; + } + browsers.opera = null; + } + if (targets.safari != null) { + browsers.safari = targets.safari; + if (this.isCompatible(browsers)) { + return true; + } + browsers.safari = null; + } + if (targets.samsung != null) { + browsers.samsung = targets.samsung; + if (this.isCompatible(browsers)) { + return true; + } + browsers.samsung = null; + } + + return false; + } +}; diff --git a/src/css/context.zig b/src/css/context.zig new file mode 100644 index 0000000000000..a0d89d6d5a53f --- /dev/null +++ b/src/css/context.zig @@ -0,0 +1,52 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); + +const ArrayList = std.ArrayListUnmanaged; + +pub const SupportsEntry = struct { + condition: css.SupportsCondition, + declarations: ArrayList(css.Property), + important_declarations: ArrayList(css.Property), +}; + +pub const DeclarationContext = enum { + none, + style_rule, + keyframes, + style_attribute, +}; + +pub const PropertyHandlerContext = struct { + allocator: Allocator, + targets: css.targets.Targets, + is_important: bool, + supports: ArrayList(SupportsEntry), + ltr: ArrayList(css.Property), + rtl: ArrayList(css.Property), + dark: ArrayList(css.Property), + context: DeclarationContext, + unused_symbols: *const std.StringArrayHashMapUnmanaged(void), + + pub fn new( + allocator: Allocator, + targets: css.targets.Targets, + unused_symbols: *const std.StringArrayHashMapUnmanaged(void), + ) PropertyHandlerContext { + return PropertyHandlerContext{ + .allocator = allocator, + .targets = targets, + .is_important = false, + .supports = ArrayList(SupportsEntry){}, + .ltr = ArrayList(css.Property){}, + .rtl = ArrayList(css.Property){}, + .dark = ArrayList(css.Property){}, + .context = DeclarationContext.none, + .unused_symbols = unused_symbols, + }; + } +}; diff --git a/src/css/css_internals.zig b/src/css/css_internals.zig new file mode 100644 index 0000000000000..debca956dbcd1 --- /dev/null +++ b/src/css/css_internals.zig @@ -0,0 +1,279 @@ +const bun = @import("root").bun; +const std = @import("std"); +const builtin = @import("builtin"); +const Arena = @import("../mimalloc_arena.zig").Arena; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const JSC = bun.JSC; +const JSValue = bun.JSC.JSValue; +const JSPromise = bun.JSC.JSPromise; +const JSGlobalObject = bun.JSC.JSGlobalObject; + +threadlocal var arena_: ?Arena = null; + +const TestKind = enum { + normal, + minify, + prefix, +}; + +pub fn minifyTestWithOptions(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + return testingImpl(globalThis, callframe, .minify); +} + +pub fn prefixTestWithOptions(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + return testingImpl(globalThis, callframe, .prefix); +} + +pub fn testWithOptions(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + return testingImpl(globalThis, callframe, .normal); +} + +pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, comptime test_kind: TestKind) JSC.JSValue { + var arena = arena_ orelse brk: { + break :brk Arena.init() catch @panic("oopsie arena no good"); + }; + defer arena.reset(); + const alloc = arena.allocator(); + + const arguments_ = callframe.arguments(2); + var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); + const source_arg: JSC.JSValue = arguments.nextEat() orelse { + globalThis.throw("minifyTestWithOptions: expected 2 arguments, got 0", .{}); + return .undefined; + }; + if (!source_arg.isString()) { + globalThis.throw("minifyTestWithOptions: expected source to be a string", .{}); + return .undefined; + } + const source_bunstr = source_arg.toBunString(globalThis); + defer source_bunstr.deref(); + const source = source_bunstr.toUTF8(bun.default_allocator); + defer source.deinit(); + + const expected_arg = arguments.nextEat() orelse { + globalThis.throw("minifyTestWithOptions: expected 2 arguments, got 1", .{}); + return .undefined; + }; + if (!expected_arg.isString()) { + globalThis.throw("minifyTestWithOptions: expected `expected` arg to be a string", .{}); + return .undefined; + } + const expected_bunstr = expected_arg.toBunString(globalThis); + defer expected_bunstr.deref(); + const expected = expected_bunstr.toUTF8(bun.default_allocator); + defer expected.deinit(); + + const options_arg = arguments.nextEat(); + + var log = bun.logger.Log.init(alloc); + defer log.deinit(); + + const parser_options = parser_options: { + const opts = bun.css.ParserOptions.default(alloc, &log); + if (test_kind == .prefix) break :parser_options opts; + + if (options_arg) |optargs| { + _ = optargs; // autofix + // if (optargs.isObject()) { + // if (optargs.getStr + // } + std.debug.panic("ZACK: suppor this lol", .{}); + } + + break :parser_options opts; + }; + + switch (bun.css.StyleSheet(bun.css.DefaultAtRule).parse( + alloc, + source.slice(), + parser_options, + )) { + .result => |stylesheet_| { + var stylesheet = stylesheet_; + var minify_options: bun.css.MinifyOptions = bun.css.MinifyOptions.default(); + switch (test_kind) { + .minify => {}, + .normal => {}, + .prefix => { + if (options_arg) |optarg| { + if (optarg.isObject()) { + minify_options.targets.browsers = targetsFromJS(globalThis, optarg); + } + } + }, + } + _ = stylesheet.minify(alloc, minify_options).assert(); + + const result = stylesheet.toCss(alloc, bun.css.PrinterOptions{ + .minify = switch (test_kind) { + .minify => true, + .normal => false, + .prefix => false, + }, + }) catch |e| { + bun.handleErrorReturnTrace(e, @errorReturnTrace()); + return .undefined; + }; + + return bun.String.fromBytes(result.code).toJS(globalThis); + }, + .err => |err| { + if (log.hasAny()) { + return log.toJS(globalThis, bun.default_allocator, "parsing failed:"); + } + globalThis.throw("parsing failed: {}", .{err.kind}); + return .undefined; + }, + } +} + +fn targetsFromJS(globalThis: *JSC.JSGlobalObject, jsobj: JSValue) bun.css.targets.Browsers { + var targets = bun.css.targets.Browsers{}; + + if (jsobj.getTruthy(globalThis, "android")) |val| { + if (val.isInt32()) { + if (val.getNumber()) |value| { + targets.android = @intFromFloat(value); + } + } + } + if (jsobj.getTruthy(globalThis, "chrome")) |val| { + if (val.isInt32()) { + if (val.getNumber()) |value| { + targets.chrome = @intFromFloat(value); + } + } + } + if (jsobj.getTruthy(globalThis, "edge")) |val| { + if (val.isInt32()) { + if (val.getNumber()) |value| { + targets.edge = @intFromFloat(value); + } + } + } + if (jsobj.getTruthy(globalThis, "firefox")) |val| { + if (val.isInt32()) { + if (val.getNumber()) |value| { + targets.firefox = @intFromFloat(value); + } + } + } + if (jsobj.getTruthy(globalThis, "ie")) |val| { + if (val.isInt32()) { + if (val.getNumber()) |value| { + targets.ie = @intFromFloat(value); + } + } + } + if (jsobj.getTruthy(globalThis, "ios_saf")) |val| { + if (val.isInt32()) { + if (val.getNumber()) |value| { + targets.ios_saf = @intFromFloat(value); + } + } + } + if (jsobj.getTruthy(globalThis, "opera")) |val| { + if (val.isInt32()) { + if (val.getNumber()) |value| { + targets.opera = @intFromFloat(value); + } + } + } + if (jsobj.getTruthy(globalThis, "safari")) |val| { + if (val.isInt32()) { + if (val.getNumber()) |value| { + targets.safari = @intFromFloat(value); + } + } + } + if (jsobj.getTruthy(globalThis, "samsung")) |val| { + if (val.isInt32()) { + if (val.getNumber()) |value| { + targets.samsung = @intFromFloat(value); + } + } + } + + return targets; +} + +pub fn attrTest(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + var arena = arena_ orelse brk: { + break :brk Arena.init() catch @panic("oopsie arena no good"); + }; + defer arena.reset(); + const alloc = arena.allocator(); + + const arguments_ = callframe.arguments(4); + var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); + const source_arg: JSC.JSValue = arguments.nextEat() orelse { + globalThis.throw("attrTest: expected 3 arguments, got 0", .{}); + return .undefined; + }; + if (!source_arg.isString()) { + globalThis.throw("attrTest: expected source to be a string", .{}); + return .undefined; + } + const source_bunstr = source_arg.toBunString(globalThis); + defer source_bunstr.deref(); + const source = source_bunstr.toUTF8(bun.default_allocator); + defer source.deinit(); + + const expected_arg = arguments.nextEat() orelse { + globalThis.throw("attrTest: expected 3 arguments, got 1", .{}); + return .undefined; + }; + if (!expected_arg.isString()) { + globalThis.throw("attrTest: expected `expected` arg to be a string", .{}); + return .undefined; + } + const expected_bunstr = expected_arg.toBunString(globalThis); + defer expected_bunstr.deref(); + const expected = expected_bunstr.toUTF8(bun.default_allocator); + defer expected.deinit(); + + const minify_arg: JSC.JSValue = arguments.nextEat() orelse { + globalThis.throw("attrTest: expected 3 arguments, got 2", .{}); + return .undefined; + }; + const minify = minify_arg.isBoolean() and minify_arg.toBoolean(); + + var targets: bun.css.targets.Targets = .{}; + if (arguments.nextEat()) |arg| { + if (arg.isObject()) { + targets.browsers = targetsFromJS(globalThis, arg); + } + } + + var log = bun.logger.Log.init(alloc); + defer log.deinit(); + + const parser_options = bun.css.ParserOptions.default(alloc, &log); + + switch (bun.css.StyleAttribute.parse(alloc, source.slice(), parser_options)) { + .result => |stylesheet_| { + var stylesheet = stylesheet_; + var minify_options: bun.css.MinifyOptions = bun.css.MinifyOptions.default(); + minify_options.targets = targets; + stylesheet.minify(alloc, minify_options); + + const result = stylesheet.toCss(alloc, bun.css.PrinterOptions{ + .minify = minify, + .targets = targets, + }) catch |e| { + bun.handleErrorReturnTrace(e, @errorReturnTrace()); + return .undefined; + }; + + return bun.String.fromBytes(result.code).toJS(globalThis); + }, + .err => |err| { + if (log.hasAny()) { + return log.toJS(globalThis, bun.default_allocator, "parsing failed:"); + } + globalThis.throw("parsing failed: {}", .{err.kind}); + return .undefined; + }, + } +} diff --git a/src/css/css_modules.zig b/src/css/css_modules.zig new file mode 100644 index 0000000000000..941d698092e01 --- /dev/null +++ b/src/css/css_modules.zig @@ -0,0 +1,406 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); +const DashedIdent = css_values.ident.DashedIdent; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; +const PrintErr = css.PrintErr; + +const ArrayList = std.ArrayListUnmanaged; + +pub const CssModule = struct { + config: *const Config, + sources: *const ArrayList([]const u8), + hashes: ArrayList([]const u8), + exports_by_source_index: ArrayList(CssModuleExports), + references: *CssModuleReferences, + + pub fn new( + allocator: Allocator, + config: *const Config, + sources: *const ArrayList([]const u8), + project_root: ?[]const u8, + references: *CssModuleReferences, + ) CssModule { + const hashes = hashes: { + var hashes = ArrayList([]const u8).initCapacity(allocator, sources.items.len) catch bun.outOfMemory(); + for (sources.items) |path| { + var alloced = false; + const source = source: { + if (project_root) |root| { + if (bun.path.Platform.auto.isAbsolute(root)) { + alloced = true; + // TODO: should we use this allocator or something else + break :source allocator.dupe(u8, bun.path.relative(root, path)) catch bun.outOfMemory(); + } + } + break :source path; + }; + defer if (alloced) allocator.free(source); + hashes.appendAssumeCapacity(hash( + allocator, + "{s}", + .{source}, + config.pattern.segments.items[0] == .hash, + )); + } + break :hashes hashes; + }; + const exports_by_source_index = exports_by_source_index: { + var exports_by_source_index = ArrayList(CssModuleExports).initCapacity(allocator, sources.items.len) catch bun.outOfMemory(); + exports_by_source_index.appendNTimesAssumeCapacity(CssModuleExports{}, sources.items.len); + break :exports_by_source_index exports_by_source_index; + }; + return CssModule{ + .config = config, + .sources = sources, + .references = references, + .hashes = hashes, + .exports_by_source_index = exports_by_source_index, + }; + } + + pub fn deinit(this: *CssModule) void { + _ = this; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn referenceDashed( + this: *CssModule, + name: []const u8, + from: *const ?css.css_properties.css_modules.Specifier, + source_index: u32, + ) ?[]const u8 { + _ = this; // autofix + _ = name; // autofix + _ = from; // autofix + _ = source_index; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn handleComposes( + this: *CssModule, + allocator: Allocator, + selectors: *const css.selector.parser.SelectorList, + composes: *const css.css_properties.css_modules.Composes, + source_index: u32, + ) css.Maybe(void, css.PrinterErrorKind) { + for (selectors.v.items) |*sel| { + if (sel.len() == 1) { + const component: *const css.selector.parser.Component = &sel.components.items[0]; + switch (component.*) { + .class => |id| { + for (composes.names.items) |name| { + const reference: CssModuleReference = if (composes.from) |*specifier| + switch (specifier.*) { + .source_index => |dep_source_index| { + if (this.exports_by_source_index.items[dep_source_index].get(name.v)) |entry| { + const entry_name = entry.name; + const composes2 = &entry.composes; + const @"export" = this.exports_by_source_index.items[source_index].getPtr(id.v).?; + + @"export".composes.append(allocator, .{ .local = .{ .name = entry_name } }) catch bun.outOfMemory(); + @"export".composes.appendSlice(allocator, composes2.items) catch bun.outOfMemory(); + } + continue; + }, + .global => CssModuleReference{ .global = .{ .name = name.v } }, + .file => |file| CssModuleReference{ + .dependency = .{ + .name = name.v, + .specifier = file, + }, + }, + } + else + CssModuleReference{ + .local = .{ + .name = this.config.pattern.writeToString( + allocator, + ArrayList(u8){}, + this.hashes.items[source_index], + this.sources.items[source_index], + name.v, + ), + }, + }; + + const export_value = this.exports_by_source_index.items[source_index].getPtr(id.v) orelse unreachable; + export_value.composes.append(allocator, reference) catch bun.outOfMemory(); + + const contains_reference = brk: { + for (export_value.composes.items) |*compose_| { + const compose: *const CssModuleReference = compose_; + if (compose.eql(&reference)) { + break :brk true; + } + } + break :brk false; + }; + if (!contains_reference) { + export_value.composes.append(allocator, reference) catch bun.outOfMemory(); + } + } + }, + else => {}, + } + } + + // The composes property can only be used within a simple class selector. + return .{ .err = css.PrinterErrorKind.invalid_composes_selector }; + } + + return .{ .result = {} }; + } + + pub fn addDashed(this: *CssModule, allocator: Allocator, local: []const u8, source_index: u32) void { + const gop = this.exports_by_source_index.items[source_index].getOrPut(allocator, local) catch bun.outOfMemory(); + if (!gop.found_existing) { + gop.value_ptr.* = CssModuleExport{ + // todo_stuff.depth + .name = this.config.pattern.writeToStringWithPrefix( + allocator, + "--", + this.hashes.items[source_index], + this.sources.items[source_index], + local[2..], + ), + .composes = .{}, + .is_referenced = false, + }; + } + } + + pub fn addLocal(this: *CssModule, allocator: Allocator, exported: []const u8, local: []const u8, source_index: u32) void { + const gop = this.exports_by_source_index.items[source_index].getOrPut(allocator, exported) catch bun.outOfMemory(); + if (!gop.found_existing) { + gop.value_ptr.* = CssModuleExport{ + // todo_stuff.depth + .name = this.config.pattern.writeToString( + allocator, + .{}, + this.hashes.items[source_index], + this.sources.items[source_index], + local, + ), + .composes = .{}, + .is_referenced = false, + }; + } + } +}; + +/// Configuration for CSS modules. +pub const Config = struct { + /// The name pattern to use when renaming class names and other identifiers. + /// Default is `[hash]_[local]`. + pattern: Pattern, + + /// Whether to rename dashed identifiers, e.g. custom properties. + dashed_idents: bool, + + /// Whether to scope animation names. + /// Default is `true`. + animation: bool, + + /// Whether to scope grid names. + /// Default is `true`. + grid: bool, + + /// Whether to scope custom identifiers + /// Default is `true`. + custom_idents: bool, +}; + +/// A CSS modules class name pattern. +pub const Pattern = struct { + /// The list of segments in the pattern. + segments: css.SmallList(Segment, 2), + + /// Write the substituted pattern to a destination. + pub fn write( + this: *const Pattern, + hash_: []const u8, + path: []const u8, + local: []const u8, + closure: anytype, + comptime writefn: *const fn (@TypeOf(closure), []const u8, replace_dots: bool) void, + ) void { + for (this.segments.items) |*segment| { + switch (segment.*) { + .literal => |s| { + writefn(closure, s, false); + }, + .name => { + const stem = std.fs.path.stem(path); + if (std.mem.indexOf(u8, stem, ".")) |_| { + writefn(closure, stem, true); + } else { + writefn(closure, stem, false); + } + }, + .local => { + writefn(closure, local, false); + }, + .hash => { + writefn(closure, hash_, false); + }, + } + } + } + + pub fn writeToStringWithPrefix( + this: *const Pattern, + allocator: Allocator, + comptime prefix: []const u8, + hash_: []const u8, + path: []const u8, + local: []const u8, + ) []const u8 { + const Closure = struct { res: ArrayList(u8), allocator: Allocator }; + var closure = Closure{ .res = .{}, .allocator = allocator }; + this.write( + hash_, + path, + local, + &closure, + struct { + pub fn writefn(self: *Closure, slice: []const u8, replace_dots: bool) void { + self.res.appendSlice(self.allocator, prefix) catch bun.outOfMemory(); + if (replace_dots) { + const start = self.res.items.len; + self.res.appendSlice(self.allocator, slice) catch bun.outOfMemory(); + const end = self.res.items.len; + for (self.res.items[start..end]) |*c| { + if (c.* == '.') { + c.* = '-'; + } + } + return; + } + self.res.appendSlice(self.allocator, slice) catch bun.outOfMemory(); + } + }.writefn, + ); + return closure.res.items; + } + + pub fn writeToString( + this: *const Pattern, + allocator: Allocator, + res_: ArrayList(u8), + hash_: []const u8, + path: []const u8, + local: []const u8, + ) []const u8 { + var res = res_; + const Closure = struct { res: *ArrayList(u8), allocator: Allocator }; + var closure = Closure{ .res = &res, .allocator = allocator }; + this.write( + hash_, + path, + local, + &closure, + struct { + pub fn writefn(self: *Closure, slice: []const u8, replace_dots: bool) void { + if (replace_dots) { + const start = self.res.items.len; + self.res.appendSlice(self.allocator, slice) catch bun.outOfMemory(); + const end = self.res.items.len; + for (self.res.items[start..end]) |*c| { + if (c.* == '.') { + c.* = '-'; + } + } + return; + } + self.res.appendSlice(self.allocator, slice) catch bun.outOfMemory(); + return; + } + }.writefn, + ); + + return res.items; + } +}; + +/// A segment in a CSS modules class name pattern. +/// +/// See [Pattern](Pattern). +pub const Segment = union(enum) { + /// A literal string segment. + literal: []const u8, + + /// The base file name. + name, + + /// The original class name. + local, + + /// A hash of the file name. + hash, +}; + +/// A map of exported names to values. +pub const CssModuleExports = std.StringArrayHashMapUnmanaged(CssModuleExport); + +/// A map of placeholders to references. +pub const CssModuleReferences = std.StringArrayHashMapUnmanaged(CssModuleReference); + +/// An exported value from a CSS module. +pub const CssModuleExport = struct { + /// The local (compiled) name for this export. + name: []const u8, + /// Other names that are composed by this export. + composes: ArrayList(CssModuleReference), + /// Whether the export is referenced in this file. + is_referenced: bool, +}; + +/// A referenced name within a CSS module, e.g. via the `composes` property. +/// +/// See [CssModuleExport](CssModuleExport). +pub const CssModuleReference = union(enum) { + /// A local reference. + local: struct { + /// The local (compiled) name for the reference. + name: []const u8, + }, + /// A global reference. + global: struct { + /// The referenced global name. + name: []const u8, + }, + /// A reference to an export in a different file. + dependency: struct { + /// The name to reference within the dependency. + name: []const u8, + /// The dependency specifier for the referenced file. + specifier: []const u8, + }, + + pub fn eql(this: *const @This(), other: *const @This()) bool { + if (@intFromEnum(this.*) != @intFromEnum(other.*)) return false; + + return switch (this.*) { + .local => |v| bun.strings.eql(v.name, other.local.name), + .global => |v| bun.strings.eql(v.name, other.global.name), + .dependency => |v| bun.strings.eql(v.name, other.dependency.name) and bun.strings.eql(v.specifier, other.dependency.specifier), + }; + } +}; + +// TODO: replace with bun's hash +pub fn hash(allocator: Allocator, comptime fmt: []const u8, args: anytype, at_start: bool) []const u8 { + _ = fmt; // autofix + _ = args; // autofix + _ = allocator; // autofix + _ = at_start; // autofix + // @compileError(css.todo_stuff.depth); + @panic(css.todo_stuff.depth); +} diff --git a/src/css/css_parser.zig b/src/css/css_parser.zig new file mode 100644 index 0000000000000..6febe2123aa6b --- /dev/null +++ b/src/css/css_parser.zig @@ -0,0 +1,6279 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +const ArrayList = std.ArrayListUnmanaged; + +pub const prefixes = @import("./prefixes.zig"); + +pub const dependencies = @import("./dependencies.zig"); +pub const Dependency = dependencies.Dependency; + +pub const css_modules = @import("./css_modules.zig"); +pub const CssModuleExports = css_modules.CssModuleExports; +pub const CssModule = css_modules.CssModule; +pub const CssModuleReferences = css_modules.CssModuleReferences; +pub const CssModuleReference = css_modules.CssModuleReference; + +pub const css_rules = @import("./rules/rules.zig"); +pub const CssRule = css_rules.CssRule; +pub const CssRuleList = css_rules.CssRuleList; +pub const LayerName = css_rules.layer.LayerName; +pub const SupportsCondition = css_rules.supports.SupportsCondition; +pub const CustomMedia = css_rules.custom_media.CustomMediaRule; +pub const NamespaceRule = css_rules.namespace.NamespaceRule; +pub const UnknownAtRule = css_rules.unknown.UnknownAtRule; +pub const ImportRule = css_rules.import.ImportRule; +pub const StyleRule = css_rules.style.StyleRule; +pub const StyleContext = css_rules.StyleContext; + +pub const MinifyContext = css_rules.MinifyContext; + +pub const media_query = @import("./media_query.zig"); +pub const MediaList = media_query.MediaList; +pub const MediaFeatureType = media_query.MediaFeatureType; + +pub const css_values = @import("./values/values.zig"); +pub const DashedIdent = css_values.ident.DashedIdent; +pub const DashedIdentFns = css_values.ident.DashedIdentFns; +pub const CssColor = css_values.color.CssColor; +pub const CSSString = css_values.string.CSSString; +pub const CSSStringFns = css_values.string.CSSStringFns; +pub const CSSInteger = css_values.number.CSSInteger; +pub const CSSIntegerFns = css_values.number.CSSIntegerFns; +pub const CSSNumber = css_values.number.CSSNumber; +pub const CSSNumberFns = css_values.number.CSSNumberFns; +pub const Ident = css_values.ident.Ident; +pub const IdentFns = css_values.ident.IdentFns; +pub const CustomIdent = css_values.ident.CustomIdent; +pub const CustomIdentFns = css_values.ident.CustomIdentFns; +pub const Url = css_values.url.Url; + +pub const declaration = @import("./declaration.zig"); + +pub const css_properties = @import("./properties/properties.zig"); +pub const Property = css_properties.Property; +pub const PropertyId = css_properties.PropertyId; +pub const PropertyIdTag = css_properties.PropertyIdTag; +pub const TokenList = css_properties.custom.TokenList; +pub const TokenListFns = css_properties.custom.TokenListFns; + +const css_decls = @import("./declaration.zig"); +pub const DeclarationList = css_decls.DeclarationList; +pub const DeclarationBlock = css_decls.DeclarationBlock; + +pub const selector = @import("./selectors/selector.zig"); +pub const SelectorList = selector.parser.SelectorList; + +pub const logical = @import("./logical.zig"); +pub const PropertyCategory = logical.PropertyCategory; +pub const LogicalGroup = logical.LogicalGroup; + +pub const css_printer = @import("./printer.zig"); +pub const Printer = css_printer.Printer; +pub const PrinterOptions = css_printer.PrinterOptions; +pub const targets = @import("./targets.zig"); +pub const Targets = css_printer.Targets; +// pub const Features = css_printer.Features; + +const context = @import("./context.zig"); +pub const PropertyHandlerContext = context.PropertyHandlerContext; +pub const DeclarationHandler = declaration.DeclarationHandler; + +pub const Maybe = bun.JSC.Node.Maybe; +// TODO: Remove existing Error defined here and replace it with these +const errors_ = @import("./error.zig"); +pub const Err = errors_.Err; +pub const PrinterErrorKind = errors_.PrinterErrorKind; +pub const PrinterError = errors_.PrinterError; +pub const ErrorLocation = errors_.ErrorLocation; +pub const ParseError = errors_.ParseError; +pub const ParserError = errors_.ParserError; +pub const BasicParseError = errors_.BasicParseError; +pub const BasicParseErrorKind = errors_.BasicParseErrorKind; +pub const SelectorError = errors_.SelectorError; +pub const MinifyErrorKind = errors_.MinifyErrorKind; +pub const MinifyError = errors_.MinifyError; + +pub const compat = @import("./compat.zig"); + +pub const fmtPrinterError = errors_.fmtPrinterError; + +pub const PrintErr = error{ + lol, +}; + +pub fn OOM(e: anyerror) noreturn { + if (comptime bun.Environment.isDebug) { + std.debug.assert(e == std.mem.Allocator.Error.OutOfMemory); + } + bun.outOfMemory(); +} + +// TODO: smallvec +pub fn SmallList(comptime T: type, comptime N: comptime_int) type { + _ = N; // autofix + return ArrayList(T); +} + +pub const Bitflags = bun.Bitflags; + +pub const todo_stuff = struct { + pub const think_mem_mgmt = "TODO: think about memory management"; + + pub const depth = "TODO: we need to go deeper"; + + pub const match_ignore_ascii_case = "TODO: implement match_ignore_ascii_case"; + + pub const enum_property = "TODO: implement enum_property!"; + + pub const match_byte = "TODO: implement match_byte!"; + + pub const warn = "TODO: implement warning"; +}; + +pub const VendorPrefix = packed struct(u8) { + /// No vendor prefixes. + /// 0b00000001 + none: bool = false, + /// The `-webkit` vendor prefix. + /// 0b00000010 + webkit: bool = false, + /// The `-moz` vendor prefix. + /// 0b00000100 + moz: bool = false, + /// The `-ms` vendor prefix. + /// 0b00001000 + ms: bool = false, + /// The `-o` vendor prefix. + /// 0b00010000 + o: bool = false, + __unused: u3 = 0, + + pub usingnamespace Bitflags(@This()); + + pub fn all() VendorPrefix { + return VendorPrefix{ .webkit = true, .moz = true, .ms = true, .o = true, .none = true }; + } + + pub fn toCss(this: *const VendorPrefix, comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.asBits()) { + VendorPrefix.asBits(.{ .webkit = true }) => dest.writeStr("-webkit"), + VendorPrefix.asBits(.{ .moz = true }) => dest.writeStr("-moz-"), + VendorPrefix.asBits(.{ .ms = true }) => dest.writeStr("-ms-"), + VendorPrefix.asBits(.{ .o = true }) => dest.writeStr("-o-"), + else => {}, + }; + } + + /// Returns VendorPrefix::None if empty. + pub fn orNone(this: VendorPrefix) VendorPrefix { + return this.bitwiseOr(VendorPrefix{ .none = true }); + } +}; + +pub const SourceLocation = struct { + line: u32, + column: u32, + + /// Create a new BasicParseError at this location for an unexpected token + pub fn newBasicUnexpectedTokenError(this: SourceLocation, token: Token) ParseError(ParserError) { + return BasicParseError.intoDefaultParseError(.{ + .kind = .{ .unexpected_token = token }, + .location = this, + }); + } + + /// Create a new ParseError at this location for an unexpected token + pub fn newUnexpectedTokenError(this: SourceLocation, token: Token) ParseError(ParserError) { + return ParseError(ParserError){ + .kind = .{ .basic = .{ .unexpected_token = token } }, + .location = this, + }; + } + + pub fn newCustomError(this: SourceLocation, err: anytype) ParseError(ParserError) { + return switch (@TypeOf(err)) { + ParserError => .{ + .kind = .{ .custom = err }, + .location = this, + }, + BasicParseError => .{ + .kind = .{ .custom = BasicParseError.intoDefaultParseError(err) }, + .location = this, + }, + selector.parser.SelectorParseErrorKind => .{ + .kind = .{ .custom = selector.parser.SelectorParseErrorKind.intoDefaultParserError(err) }, + .location = this, + }, + else => @compileError("TODO implement this for: " ++ @typeName(@TypeOf(err))), + }; + } +}; +pub const Location = css_rules.Location; + +pub const Error = Err(ParserError); + +pub fn Result(comptime T: type) type { + return Maybe(T, ParseError(ParserError)); +} + +pub fn PrintResult(comptime T: type) type { + return Maybe(T, PrinterError); +} + +pub fn todo(comptime fmt: []const u8, args: anytype) noreturn { + std.debug.panic("TODO: " ++ fmt, args); +} + +pub fn todo2(comptime fmt: []const u8) void { + std.debug.panic("TODO: " ++ fmt); +} + +pub fn voidWrap(comptime T: type, comptime parsefn: *const fn (*Parser) Result(T)) *const fn (void, *Parser) Result(T) { + const Wrapper = struct { + fn wrapped(_: void, p: *Parser) Result(T) { + return parsefn(p); + } + }; + return Wrapper.wrapped; +} + +pub fn DefineListShorthand(comptime T: type) type { + _ = T; // autofix + // TODO: implement this when we implement visit? + // does nothing now + return struct {}; +} + +pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) type { + // TODO: validate map, make sure each field is set + // make sure each field is same index as in T + _ = T.PropertyFieldMap; + + return struct { + /// Returns a shorthand from the longhand properties defined in the given declaration block. + pub fn fromLonghands(allocator: Allocator, decls: *const DeclarationBlock, vendor_prefix: VendorPrefix) ?struct { T, bool } { + var count: usize = 0; + var important_count: usize = 0; + var this: T = undefined; + var set_fields = std.StaticBitSet(std.meta.fields(T).len).initEmpty(); + const all_fields_set = std.StaticBitSet(std.meta.fields(T).len).initFull(); + + // Loop through each property in `decls.declarations` and then `decls.important_declarations` + // The inline for loop is so we can share the code for both + const DECL_FIELDS = &.{ "declarations", "important_declarations" }; + inline for (DECL_FIELDS) |decl_field_name| { + const decl_list: *const ArrayList(css_properties.Property) = &@field(decls, decl_field_name); + const important = comptime std.mem.eql(u8, decl_field_name, "important_declarations"); + + // Now loop through each property in the list + main_loop: for (decl_list.items) |*property| { + // The property field map maps each field in `T` to a tag of `Property` + // Here we do `inline for` to basically switch on the tag of `property` to see + // if it matches a field in `T` which maps to the same tag + // + // Basically, check that `@as(PropertyIdTag, property.*)` equals `T.PropertyFieldMap[field.name]` + inline for (std.meta.fields(@TypeOf(T.PropertyFieldMap))) |field| { + const tag: PropertyIdTag = @as(?*const PropertyIdTag, field.default_value).?.*; + + if (@intFromEnum(@as(PropertyIdTag, property.*)) == tag) { + if (@hasField(T.VendorPrefixMap, field.name)) { + if (@hasField(T.VendorPrefixMap, field.name) and + !VendorPrefix.eq(@field(property, field.name)[1], vendor_prefix)) + { + return null; + } + + @field(this, field.name) = if (@hasDecl(@TypeOf(@field(property, field.name)[0]), "clone")) + @field(property, field.name)[0].deepClone(allocator) + else + @field(property, field.name)[0]; + } else { + @field(this, field.name) = if (@hasDecl(@TypeOf(@field(property, field.name)), "clone")) + @field(property, field.name).deepClone(allocator) + else + @field(property, field.name); + } + + set_fields.set(std.meta.fieldIndex(T, field.name)); + count += 1; + if (important) { + important_count += 1; + } + + continue :main_loop; + } + } + + // If `property` matches none of the tags in `T.PropertyFieldMap` then let's try + // if it matches the tag specified by `property_name` + if (@as(PropertyIdTag, property.*) == property_name) { + inline for (std.meta.fields(@TypeOf(T.PropertyFieldMap))) |field| { + if (@hasField(T.VendorPrefixMap, field.name)) { + @field(this, field.name) = if (@hasDecl(@TypeOf(@field(property, field.name)[0]), "clone")) + @field(property, field.name)[0].deepClone(allocator) + else + @field(property, field.name)[0]; + } else { + @field(this, field.name) = if (@hasDecl(@TypeOf(@field(property, field.name)), "clone")) + @field(property, field.name).deepClone(allocator) + else + @field(property, field.name); + } + + set_fields.set(std.meta.fieldIndex(T, field.name)); + count += 1; + if (important) { + important_count += 1; + } + } + continue :main_loop; + } + + // Otherwise, try to convert to te fields using `.longhand()` + inline for (std.meta.fields(@TypeOf(T.PropertyFieldMap))) |field| { + const property_id = @unionInit( + PropertyId, + field.name, + if (@hasDecl(T.VendorPrefixMap, field.name)) vendor_prefix else {}, + ); + const value = property.longhand(&property_id); + if (@as(PropertyIdTag, value) == @as(PropertyIdTag, property_id)) { + @field(this, field.name) = if (@hasDecl(T.VendorPrefixMap, field.name)) + @field(value, field.name)[0] + else + @field(value, field.name); + set_fields.set(std.meta.fieldIndex(T, field.name)); + count += 1; + if (important) { + important_count += 1; + } + } + } + } + } + + if (important_count > 0 and important_count != count) { + return null; + } + + // All properties in the group must have a matching value to produce a shorthand. + if (set_fields.eql(all_fields_set)) { + return .{ this, important_count > 0 }; + } + + return null; + } + + /// Returns a shorthand from the longhand properties defined in the given declaration block. + pub fn longhands(vendor_prefix: VendorPrefix) []const PropertyId { + const out: []const PropertyId = comptime out: { + var out: [std.meta.fields(@TypeOf(T.PropertyFieldMap)).len]PropertyId = undefined; + + for (std.meta.fields(@TypeOf(T.PropertyFieldMap)), 0..) |field, i| { + out[i] = @unionInit( + PropertyId, + field.name, + if (@hasField(T.VendorPrefixMap, field.name)) vendor_prefix else {}, + ); + } + + break :out out; + }; + return out; + } + + /// Returns a longhand property for this shorthand. + pub fn longhand(this: *const T, allocator: Allocator, property_id: *const PropertyId) ?Property { + inline for (std.meta.fields(@TypeOf(T.PropertyFieldMap))) |field| { + if (@as(PropertyIdTag, property_id.*) == @field(T.PropertyFieldMap, field.name)) { + const val = if (@hasDecl(@TypeOf(@field(T, field.namee)), "clone")) + @field(this, field.name).deepClone(allocator) + else + @field(this, field.name); + return @unionInit( + Property, + field.name, + if (@field(T.VendorPrefixMap, field.name)) + .{ val, @field(property_id, field.name)[1] } + else + val, + ); + } + } + return null; + } + + /// Updates this shorthand from a longhand property. + pub fn setLonghand(this: *T, allocator: Allocator, property: *const Property) bool { + inline for (std.meta.fields(T.PropertyFieldMap)) |field| { + if (@as(PropertyIdTag, property.*) == @field(T.PropertyFieldMap, field.name)) { + const val = if (@hasDecl(@TypeOf(@field(T, field.name)), "clone")) + @field(this, field.name).deepClone(allocator) + else + @field(this, field.name); + + @field(this, field.name) = val; + + return true; + } + } + return false; + } + }; +} + +pub fn DefineRectShorthand(comptime T: type, comptime V: type) type { + return struct { + pub fn parse(input: *Parser) Result(T) { + const rect = switch (css_values.rect.Rect(V).parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + return .{ + .result = .{ + .top = rect.top, + .right = rect.right, + .bottom = rect.bottom, + .left = rect.left, + }, + }; + } + + pub fn toCss(this: *const T, comptime W: type, dest: *Printer(W)) PrintErr!void { + const rect = css_values.rect.Rect(V){ + .top = this.top, + .right = this.right, + .bottom = this.bottom, + .left = this.left, + }; + return rect.toCss(W, dest); + } + }; +} + +pub fn DefineSizeShorthand(comptime T: type, comptime V: type) type { + const fields = std.meta.fields(T); + if (fields.len != 2) @compileError("DefineSizeShorthand must be used on a struct with 2 fields"); + return struct { + pub fn parse(input: *Parser) Result(T) { + const size = switch (css_values.size.Size2D(V).parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + var this: T = undefined; + @field(this, fields[0].name) = size.a; + @field(this, fields[1].name) = size.b; + + return .{ .result = this }; + } + + pub fn toCss(this: *const T, comptime W: type, dest: *Printer(W)) PrintErr!void { + const size: css_values.size.Size2D(V) = .{ + .a = @field(this, fields[0].name), + .b = @field(this, fields[1].name), + }; + return size.toCss(W, dest); + } + }; +} + +pub fn DeriveParse(comptime T: type) type { + const tyinfo = @typeInfo(T); + const is_union_enum = tyinfo == .Union; + const enum_type = if (comptime is_union_enum) @typeInfo(tyinfo.Union.tag_type.?) else tyinfo; + const enum_actual_type = if (comptime is_union_enum) tyinfo.Union.tag_type.? else T; + + const Map = bun.ComptimeEnumMap(enum_actual_type); + + // TODO: this has to work for enums and union(enums) + return struct { + inline fn gnerateCode( + input: *Parser, + comptime first_payload_index: usize, + comptime maybe_first_void_index: ?usize, + comptime void_count: usize, + comptime payload_count: usize, + ) Result(T) { + const last_payload_index = first_payload_index + payload_count - 1; + if (comptime maybe_first_void_index == null) { + inline for (tyinfo.Union.fields[first_payload_index .. first_payload_index + payload_count], first_payload_index..) |field, i| { + if (comptime (i == last_payload_index)) { + return generic.parseFor(field.type)(input); + } + if (input.tryParse(generic.parseFor(field.type), .{}).asValue()) |v| { + return .{ .result = @unionInit(T, field.name, v) }; + } + } + } + + const first_void_index = maybe_first_void_index.?; + + const void_fields = bun.meta.EnumFields(T)[first_void_index .. first_void_index + void_count]; + + if (comptime void_count == 1) { + const void_field = enum_type.Enum.fields[first_void_index]; + // The field is declared before the payload fields. + // So try to parse an ident matching the name of the field, then fallthrough + // to parsing the payload fields. + if (comptime first_void_index < first_payload_index) { + if (input.tryParse(Parser.expectIdentMatching, .{void_field.name}).isOk()) { + if (comptime is_union_enum) return .{ .result = @unionInit(T, void_field.name, {}) }; + return .{ .result = @enumFromInt(void_field.value) }; + } + + inline for (tyinfo.Union.fields[first_payload_index .. first_payload_index + payload_count], first_payload_index..) |field, i| { + if (comptime (i == last_payload_index and last_payload_index > first_void_index)) { + return generic.parseFor(field.type)(input); + } + if (input.tryParse(generic.parseFor(field.type), .{}).asValue()) |v| { + return .{ .result = @unionInit(T, field.name, v) }; + } + } + } else { + inline for (tyinfo.Union.fields[first_payload_index .. first_payload_index + payload_count], first_payload_index..) |field, i| { + if (comptime (i == last_payload_index and last_payload_index > first_void_index)) { + return generic.parseFor(field.type)(input); + } + if (input.tryParse(generic.parseFor(field.type), .{}).asValue()) |v| { + return .{ .result = @unionInit(T, field.name, v) }; + } + } + + // We can generate this as the last statements of the function, avoiding the `input.tryParse` routine above + if (input.expectIdentMatching(void_field.name).asErr()) |e| return .{ .err = e }; + if (comptime is_union_enum) return .{ .result = @unionInit(T, void_field.name, {}) }; + return .{ .result = @enumFromInt(void_field.value) }; + } + } else if (comptime first_void_index < first_payload_index) { + // Multiple fields declared before the payload fields, use tryParse + const state = input.state(); + if (input.tryParse(Parser.expectIdent, .{}).asValue()) |ident| { + if (Map.getCaseInsensitiveWithEql(ident, bun.strings.eqlComptimeIgnoreLen)) |matched| { + inline for (void_fields) |field| { + if (field.value == @intFromEnum(matched)) { + if (comptime is_union_enum) return .{ .result = @unionInit(T, field.name, {}) }; + return .{ .result = @enumFromInt(field.value) }; + } + } + unreachable; + } + input.reset(&state); + } + + inline for (tyinfo.Union.fields[first_payload_index .. first_payload_index + payload_count], first_payload_index..) |field, i| { + if (comptime (i == last_payload_index and last_payload_index > first_void_index)) { + return generic.parseFor(field.type)(input); + } + if (input.tryParse(generic.parseFor(field.type), .{}).asValue()) |v| { + return .{ .result = @unionInit(T, field.name, v) }; + } + } + } else if (comptime first_void_index > first_payload_index) { + inline for (tyinfo.Union.fields[first_payload_index .. first_payload_index + payload_count], first_payload_index..) |field, i| { + if (comptime (i == last_payload_index and last_payload_index > first_void_index)) { + return generic.parseFor(field.type)(input); + } + if (input.tryParse(generic.parseFor(field.type), .{}).asValue()) |v| { + return .{ .result = @unionInit(T, field.name, v) }; + } + } + + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (Map.getCaseInsensitiveWithEql(ident, bun.strings.eqlComptimeIgnoreLen)) |matched| { + inline for (void_fields) |field| { + if (field.value == @intFromEnum(matched)) { + if (comptime is_union_enum) return .{ .result = @unionInit(T, field.name, {}) }; + return .{ .result = @enumFromInt(field.value) }; + } + } + unreachable; + } + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + } + @compileError("SHOULD BE UNREACHABLE!"); + } + + // inline fn generatePayloadBranches( + // input: *Parser, + // comptime first_payload_index: usize, + // comptime first_void_index: usize, + // comptime payload_count: usize, + // ) Result(T) { + // const last_payload_index = first_payload_index + payload_count - 1; + // inline for (tyinfo.Union.fields[first_payload_index..], first_payload_index..) |field, i| { + // if (comptime (i == last_payload_index and last_payload_index > first_void_index)) { + // return generic.parseFor(field.type)(input); + // } + // if (input.tryParse(generic.parseFor(field.type), .{}).asValue()) |v| { + // return .{ .result = @unionInit(T, field.name, v) }; + // } + // } + // // The last field will return so this is never reachable + // unreachable; + // } + + pub fn parse(input: *Parser) Result(T) { + if (comptime is_union_enum) { + const payload_count, const first_payload_index, const void_count, const first_void_index = comptime counts: { + var first_void_index: ?usize = null; + var first_payload_index: ?usize = null; + var payload_count: usize = 0; + var void_count: usize = 0; + for (tyinfo.Union.fields, 0..) |field, i| { + if (field.type == void) { + void_count += 1; + if (first_void_index == null) first_void_index = i; + } else { + payload_count += 1; + if (first_payload_index == null) first_payload_index = i; + } + } + if (first_payload_index == null) { + @compileError("Type defined as `union(enum)` but no variant carries a payload. Make it an `enum` instead."); + } + if (first_void_index) |void_index| { + // Check if they overlap + if (first_payload_index.? < void_index and void_index < first_payload_index.? + payload_count) @compileError("Please put all the fields with data together and all the fields with no data together."); + if (first_payload_index.? > void_index and first_payload_index.? < void_index + void_count) @compileError("Please put all the fields with data together and all the fields with no data together."); + } + break :counts .{ payload_count, first_payload_index.?, void_count, first_void_index }; + }; + + return gnerateCode(input, first_payload_index, first_void_index, void_count, payload_count); + } + + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (Map.getCaseInsensitiveWithEql(ident, bun.strings.eqlComptimeIgnoreLen)) |matched| { + inline for (bun.meta.EnumFields(enum_type)) |field| { + if (field.value == @intFromEnum(matched)) { + if (comptime is_union_enum) return .{ .result = @unionInit(T, field.name, void) }; + return .{ .result = @enumFromInt(field.value) }; + } + } + unreachable; + } + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + } + + // pub fn parse(this: *const T, comptime W: type, dest: *Printer(W)) PrintErr!void { + // // to implement this, we need to cargo expand the derive macro + // _ = this; // autofix + // _ = dest; // autofix + // @compileError(todo_stuff.depth); + // } + }; +} + +pub fn DeriveToCss(comptime T: type) type { + // TODO: this has to work for enums and union(enums) + return struct { + pub fn toCss(this: *const T, comptime W: type, dest: *Printer(W)) PrintErr!void { + // to implement this, we need to cargo expand the derive macro + _ = this; // autofix + _ = dest; // autofix + @compileError(todo_stuff.depth); + } + }; +} + +pub const enum_property_util = struct { + pub fn asStr(comptime T: type, this: *const T) []const u8 { + const tag = @intFromEnum(this.*); + inline for (bun.meta.EnumFields(T)) |field| { + if (tag == field.value) return field.name; + } + unreachable; + } + + pub inline fn parse(comptime T: type, input: *Parser) Result(T) { + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + // todo_stuff.match_ignore_ascii_case + inline for (std.meta.fields(T)) |field| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, field.name)) return .{ .result = @enumFromInt(field.value) }; + } + + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + } + + pub fn toCss(comptime T: type, this: *const T, comptime W: type, dest: *Printer(W)) PrintErr!void { + return dest.writeStr(asStr(T, this)); + } +}; + +pub fn DefineEnumProperty(comptime T: type) type { + const fields: []const std.builtin.Type.EnumField = std.meta.fields(T); + + return struct { + pub fn asStr(this: *const T) []const u8 { + const tag = @intFromEnum(this.*); + inline for (fields) |field| { + if (tag == field.value) return field.name; + } + unreachable; + } + + pub fn parse(input: *Parser) Result(T) { + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + // todo_stuff.match_ignore_ascii_case + inline for (fields) |field| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, field.name)) return .{ .result = @enumFromInt(field.value) }; + } + + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + // @panic("TODO renable this"); + } + + pub fn toCss(this: *const T, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + // return dest.writeStr(asStr(this)); + } + }; +} + +pub fn DeriveValueType(comptime T: type) type { + _ = @typeInfo(T).Enum; + + const ValueTypeMap = T.ValueTypeMap; + const field_values: []const MediaFeatureType = field_values: { + const fields = std.meta.fields(T); + var mapping: [fields.len]MediaFeatureType = undefined; + for (fields, 0..) |field, i| { + // Check that it exists in the type map + mapping[i] = @field(ValueTypeMap, field.name); + } + const mapping_final = mapping; + break :field_values mapping_final[0..]; + }; + + return struct { + pub fn valueType(this: *const T) MediaFeatureType { + inline for (std.meta.fields(T), 0..) |field, i| { + if (field.value == @intFromEnum(this.*)) { + return field_values[i]; + } + } + unreachable; + } + }; +} + +fn consume_until_end_of_block(block_type: BlockType, tokenizer: *Tokenizer) void { + const StackCount = 16; + var sfb = std.heap.stackFallback(@sizeOf(BlockType) * StackCount, tokenizer.allocator); + const alloc = sfb.get(); + var stack = std.ArrayList(BlockType).initCapacity(alloc, StackCount) catch unreachable; + defer stack.deinit(); + + stack.appendAssumeCapacity(block_type); + + while (switch (tokenizer.next()) { + .result => |v| v, + .err => null, + }) |tok| { + if (BlockType.closing(&tok)) |b| { + if (stack.getLast() == b) { + _ = stack.pop(); + if (stack.items.len == 0) return; + } + } + + if (BlockType.opening(&tok)) |bt| stack.append(bt) catch unreachable; + } +} + +fn parse_at_rule( + allocator: Allocator, + start: *const ParserState, + name: []const u8, + input: *Parser, + comptime P: type, + parser: *P, +) Result(P.AtRuleParser.AtRule) { + _ = allocator; // autofix + ValidAtRuleParser(P); + const delimiters = Delimiters{ .semicolon = true, .curly_bracket = true }; + const Closure = struct { + name: []const u8, + parser: *P, + + pub fn parsefn(this: *@This(), input2: *Parser) Result(P.AtRuleParser.Prelude) { + return P.AtRuleParser.parsePrelude(this.parser, this.name, input2); + } + }; + var closure = Closure{ .name = name, .parser = parser }; + const prelude: P.AtRuleParser.Prelude = switch (input.parseUntilBefore(delimiters, P.AtRuleParser.Prelude, &closure, Closure.parsefn)) { + .result => |vvv| vvv, + .err => |e| { + // const end_position = input.position(); + // _ = end_position; k + out: { + const tok = switch (input.next()) { + .result => |v| v, + .err => break :out, + }; + if (tok.* != .open_curly and tok.* != .semicolon) unreachable; + break :out; + } + return .{ .err = e }; + }, + }; + const next = switch (input.next()) { + .result => |v| v, + .err => { + return switch (P.AtRuleParser.ruleWithoutBlock(parser, prelude, start)) { + .result => |v| .{ .result = v }, + .err => return .{ .err = input.newUnexpectedTokenError(.semicolon) }, + }; + }, + }; + switch (next.*) { + .semicolon => return switch (P.AtRuleParser.ruleWithoutBlock(parser, prelude, start)) { + .result => |v| .{ .result = v }, + .err => return .{ .err = input.newUnexpectedTokenError(.semicolon) }, + }, + .open_curly => { + const AnotherClosure = struct { + prelude: P.AtRuleParser.Prelude, + start: *const ParserState, + parser: *P, + pub fn parsefn(this: *@This(), input2: *Parser) Result(P.AtRuleParser.AtRule) { + return P.AtRuleParser.parseBlock(this.parser, this.prelude, this.start, input2); + } + }; + var another_closure = AnotherClosure{ + .prelude = prelude, + .start = start, + .parser = parser, + }; + return parse_nested_block(input, P.AtRuleParser.AtRule, &another_closure, AnotherClosure.parsefn); + }, + else => { + bun.unreachablePanic("", .{}); + }, + } +} + +fn parse_custom_at_rule_prelude(name: []const u8, input: *Parser, options: *const ParserOptions, comptime T: type, at_rule_parser: *T) Result(AtRulePrelude(T.CustomAtRuleParser.Prelude)) { + ValidCustomAtRuleParser(T); + switch (T.CustomAtRuleParser.parsePrelude(at_rule_parser, name, input, options)) { + .result => |prelude| { + return .{ .result = .{ .custom = prelude } }; + }, + .err => |e| { + if (e.kind == .basic and e.kind.basic == .at_rule_invalid) { + // do nothing + } else return .{ + .err = input.newCustomError( + ParserError{ .at_rule_prelude_invalid = {} }, + ), + }; + }, + } + + options.warn(input.newError(.{ .at_rule_invalid = name })); + input.skipWhitespace(); + const tokens = switch (TokenListFns.parse(input, options, 0)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = .{ .unknown = .{ + .name = name, + .tokens = tokens, + } } }; +} + +fn parse_custom_at_rule_without_block( + comptime T: type, + prelude: T.CustomAtRuleParser.Prelude, + start: *const ParserState, + options: *const ParserOptions, + at_rule_parser: *T, + is_nested: bool, +) Maybe(CssRule(T.CustomAtRuleParser.AtRule), void) { + return switch (T.CustomAtRuleParser.ruleWithoutBlock(at_rule_parser, prelude, start, options, is_nested)) { + .result => |v| .{ .result = CssRule(T.CustomAtRuleParser.AtRule){ .custom = v } }, + .err => |e| .{ .err = e }, + }; +} + +fn parse_custom_at_rule_body( + comptime T: type, + prelude: T.CustomAtRuleParser.Prelude, + input: *Parser, + start: *const ParserState, + options: *const ParserOptions, + at_rule_parser: *T, + is_nested: bool, +) Result(T.CustomAtRuleParser.AtRule) { + const result = switch (T.CustomAtRuleParser.parseBlock(at_rule_parser, prelude, start, input, options, is_nested)) { + .result => |vv| vv, + .err => |e| { + _ = e; // autofix + // match &err.kind { + // ParseErrorKind::Basic(kind) => ParseError { + // kind: ParseErrorKind::Basic(kind.clone()), + // location: err.location, + // }, + // _ => input.new_error(BasicParseErrorKind::AtRuleBodyInvalid), + // } + todo("This part here", .{}); + }, + }; + return .{ .result = result }; +} + +fn parse_qualified_rule( + start: *const ParserState, + input: *Parser, + comptime P: type, + parser: *P, + delimiters: Delimiters, +) Result(P.QualifiedRuleParser.QualifiedRule) { + ValidQualifiedRuleParser(P); + const prelude_result = brk: { + const prelude = input.parseUntilBefore(delimiters, P.QualifiedRuleParser.Prelude, parser, P.QualifiedRuleParser.parsePrelude); + break :brk prelude; + }; + if (input.expectCurlyBracketBlock().asErr()) |e| return .{ .err = e }; + const prelude = switch (prelude_result) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + const Closure = struct { + start: *const ParserState, + prelude: P.QualifiedRuleParser.Prelude, + parser: *P, + + pub fn parsefn(this: *@This(), input2: *Parser) Result(P.QualifiedRuleParser.QualifiedRule) { + return P.QualifiedRuleParser.parseBlock(this.parser, this.prelude, this.start, input2); + } + }; + var closure = Closure{ + .start = start, + .prelude = prelude, + .parser = parser, + }; + return parse_nested_block(input, P.QualifiedRuleParser.QualifiedRule, &closure, Closure.parsefn); +} + +fn parse_until_before( + parser: *Parser, + delimiters_: Delimiters, + error_behavior: ParseUntilErrorBehavior, + comptime T: type, + closure: anytype, + comptime parse_fn: *const fn (@TypeOf(closure), *Parser) Result(T), +) Result(T) { + const delimiters = parser.stop_before.bitwiseOr(delimiters_); + const result = result: { + var delimited_parser = Parser{ + .input = parser.input, + .at_start_of = if (parser.at_start_of) |block_type| brk: { + parser.at_start_of = null; + break :brk block_type; + } else null, + .stop_before = delimiters, + }; + const result = delimited_parser.parseEntirely(T, closure, parse_fn); + if (error_behavior == .stop and result.isErr()) { + return result; + } + if (delimited_parser.at_start_of) |block_type| { + consume_until_end_of_block(block_type, &delimited_parser.input.tokenizer); + } + break :result result; + }; + + // FIXME: have a special-purpose tokenizer method for this that does less work. + while (true) { + if (delimiters.contains(Delimiters.fromByte(parser.input.tokenizer.nextByte()))) break; + + switch (parser.input.tokenizer.next()) { + .result => |token| { + if (BlockType.opening(&token)) |block_type| { + consume_until_end_of_block(block_type, &parser.input.tokenizer); + } + }, + else => break, + } + } + + return result; +} + +// fn parse_until_before_impl(parser: *Parser, delimiters: Delimiters, error_behavior: Parse + +pub fn parse_until_after( + parser: *Parser, + delimiters: Delimiters, + error_behavior: ParseUntilErrorBehavior, + comptime T: type, + closure: anytype, + comptime parsefn: *const fn (@TypeOf(closure), *Parser) Result(T), +) Result(T) { + const result = parse_until_before(parser, delimiters, error_behavior, T, closure, parsefn); + const is_err = result.isErr(); + if (error_behavior == .stop and is_err) { + return result; + } + const next_byte = parser.input.tokenizer.nextByte(); + if (next_byte != null and !parser.stop_before.contains(Delimiters.fromByte(next_byte))) { + bun.debugAssert(delimiters.contains(Delimiters.fromByte(next_byte))); + // We know this byte is ASCII. + parser.input.tokenizer.advance(1); + if (next_byte == '{') { + consume_until_end_of_block(BlockType.curly_bracket, &parser.input.tokenizer); + } + } + return result; +} + +fn parse_nested_block(parser: *Parser, comptime T: type, closure: anytype, comptime parsefn: *const fn (@TypeOf(closure), *Parser) Result(T)) Result(T) { + const block_type: BlockType = if (parser.at_start_of) |block_type| brk: { + parser.at_start_of = null; + break :brk block_type; + } else @panic( + \\ + \\A nested parser can only be created when a Function, + \\ParenthisisBlock, SquareBracketBlock, or CurlyBracketBlock + \\token was just consumed. + ); + + const closing_delimiter = switch (block_type) { + .curly_bracket => Delimiters{ .close_curly_bracket = true }, + .square_bracket => Delimiters{ .close_square_bracket = true }, + .parenthesis => Delimiters{ .close_parenthesis = true }, + }; + var nested_parser = Parser{ + .input = parser.input, + .stop_before = closing_delimiter, + }; + const result = nested_parser.parseEntirely(T, closure, parsefn); + if (nested_parser.at_start_of) |block_type2| { + consume_until_end_of_block(block_type2, &nested_parser.input.tokenizer); + } + consume_until_end_of_block(block_type, &parser.input.tokenizer); + return result; +} + +pub fn ValidQualifiedRuleParser(comptime T: type) void { + // The intermediate representation of a qualified rule prelude. + _ = T.QualifiedRuleParser.Prelude; + + // The finished representation of a qualified rule. + _ = T.QualifiedRuleParser.QualifiedRule; + + // Parse the prelude of a qualified rule. For style rules, this is as Selector list. + // + // Return the representation of the prelude, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // The prelude is the part before the `{ /* ... */ }` block. + // + // The given `input` is a "delimited" parser + // that ends where the prelude should end (before the next `{`). + // + // fn parsePrelude(this: *T, input: *Parser) Error!T.QualifiedRuleParser.Prelude; + _ = T.QualifiedRuleParser.parsePrelude; + + // Parse the content of a `{ /* ... */ }` block for the body of the qualified rule. + // + // The location passed in is source location of the start of the prelude. + // + // Return the finished representation of the qualified rule + // as returned by `RuleListParser::next`, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // fn parseBlock(this: *T, prelude: P.QualifiedRuleParser.Prelude, start: *const ParserState, input: *Parser) Error!P.QualifiedRuleParser.QualifiedRule; + _ = T.QualifiedRuleParser.parseBlock; +} + +pub const DefaultAtRule = struct { + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + return dest.newError(.fmt_error, null); + } +}; + +pub const DefaultAtRuleParser = struct { + const This = @This(); + + pub const CustomAtRuleParser = struct { + pub const Prelude = void; + pub const AtRule = DefaultAtRule; + + pub fn parsePrelude(_: *This, name: []const u8, input: *Parser, options: *const ParserOptions) Result(Prelude) { + _ = options; // autofix + return .{ .err = input.newError(BasicParseErrorKind{ .at_rule_invalid = name }) }; + } + + pub fn parseBlock(_: *This, _: CustomAtRuleParser.Prelude, _: *const ParserState, input: *Parser, options: *const ParserOptions, is_nested: bool) Result(CustomAtRuleParser.AtRule) { + _ = options; // autofix + _ = is_nested; // autofix + return .{ .err = input.newError(BasicParseErrorKind.at_rule_body_invalid) }; + } + + pub fn ruleWithoutBlock(_: *This, _: CustomAtRuleParser.Prelude, _: *const ParserState, options: *const ParserOptions, is_nested: bool) Maybe(CustomAtRuleParser.AtRule, void) { + _ = options; // autofix + _ = is_nested; // autofix + return .{ .err = {} }; + } + }; +}; + +/// Same as `ValidAtRuleParser` but modified to provide parser options +pub fn ValidCustomAtRuleParser(comptime T: type) void { + // The intermediate representation of prelude of an at-rule. + _ = T.CustomAtRuleParser.Prelude; + + // The finished representation of an at-rule. + _ = T.CustomAtRuleParser.AtRule; + + // Parse the prelude of an at-rule with the given `name`. + // + // Return the representation of the prelude and the type of at-rule, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // The prelude is the part after the at-keyword + // and before the `;` semicolon or `{ /* ... */ }` block. + // + // At-rule name matching should be case-insensitive in the ASCII range. + // This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + // or with the `match_ignore_ascii_case!` macro. + // + // The given `input` is a "delimited" parser + // that ends wherever the prelude should end. + // (Before the next semicolon, the next `{`, or the end of the current block.) + // + // pub fn parsePrelude(this: *T, allocator: Allocator, name: []const u8, *Parser, options: *ParserOptions) Result(T.CustomAtRuleParser.Prelude) {} + _ = T.CustomAtRuleParser.parsePrelude; + + // End an at-rule which doesn't have block. Return the finished + // representation of the at-rule. + // + // The location passed in is source location of the start of the prelude. + // `is_nested` indicates whether the rule is nested inside a style rule. + // + // This is only called when either the `;` semicolon indeed follows the prelude, + // or parser is at the end of the input. + _ = T.CustomAtRuleParser.ruleWithoutBlock; + + // Parse the content of a `{ /* ... */ }` block for the body of the at-rule. + // + // The location passed in is source location of the start of the prelude. + // `is_nested` indicates whether the rule is nested inside a style rule. + // + // Return the finished representation of the at-rule + // as returned by `RuleListParser::next` or `DeclarationListParser::next`, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // This is only called when a block was found following the prelude. + _ = T.CustomAtRuleParser.parseBlock; +} + +pub fn ValidAtRuleParser(comptime T: type) void { + _ = T.AtRuleParser.AtRule; + _ = T.AtRuleParser.Prelude; + + // Parse the prelude of an at-rule with the given `name`. + // + // Return the representation of the prelude and the type of at-rule, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // The prelude is the part after the at-keyword + // and before the `;` semicolon or `{ /* ... */ }` block. + // + // At-rule name matching should be case-insensitive in the ASCII range. + // This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + // or with the `match_ignore_ascii_case!` macro. + // + // The given `input` is a "delimited" parser + // that ends wherever the prelude should end. + // (Before the next semicolon, the next `{`, or the end of the current block.) + // + // pub fn parsePrelude(this: *T, allocator: Allocator, name: []const u8, *Parser) Result(T.AtRuleParser.Prelude) {} + _ = T.AtRuleParser.parsePrelude; + + // End an at-rule which doesn't have block. Return the finished + // representation of the at-rule. + // + // The location passed in is source location of the start of the prelude. + // + // This is only called when `parse_prelude` returned `WithoutBlock`, and + // either the `;` semicolon indeed follows the prelude, or parser is at + // the end of the input. + // fn ruleWithoutBlock(this: *T, allocator: Allocator, prelude: T.AtRuleParser.Prelude, state: *const ParserState) Maybe(T.AtRuleParser.AtRule, void) + _ = T.AtRuleParser.ruleWithoutBlock; + + // Parse the content of a `{ /* ... */ }` block for the body of the at-rule. + // + // The location passed in is source location of the start of the prelude. + // + // Return the finished representation of the at-rule + // as returned by `RuleListParser::next` or `DeclarationListParser::next`, + // or `Err(())` to ignore the entire at-rule as invalid. + // + // This is only called when `parse_prelude` returned `WithBlock`, and a block + // was indeed found following the prelude. + // + // fn parseBlock(this: *T, prelude: T.AtRuleParser.Prelude, start: *const ParserState, input: *Parser) Error!T.AtRuleParser.AtRule + _ = T.AtRuleParser.parseBlock; +} + +pub fn AtRulePrelude(comptime T: type) type { + return union(enum) { + font_face, + font_feature_values, + font_palette_values: DashedIdent, + counter_style: CustomIdent, + import: struct { + []const u8, + MediaList, + ?SupportsCondition, + ?struct { value: ?LayerName }, + }, + namespace: struct { + ?[]const u8, + []const u8, + }, + charset, + custom_media: struct { + DashedIdent, + MediaList, + }, + property: struct { + DashedIdent, + }, + media: MediaList, + supports: SupportsCondition, + viewport: VendorPrefix, + keyframes: struct { + name: css_rules.keyframes.KeyframesName, + prefix: VendorPrefix, + }, + page: ArrayList(css_rules.page.PageSelector), + moz_document, + layer: ArrayList(LayerName), + container: struct { + name: ?css_rules.container.ContainerName, + condition: css_rules.container.ContainerCondition, + }, + starting_style, + nest: selector.parser.SelectorList, + scope: struct { + scope_start: ?selector.parser.SelectorList, + scope_end: ?selector.parser.SelectorList, + }, + unknown: struct { + name: []const u8, + /// The tokens of the prelude + tokens: TokenList, + }, + custom: T, + + pub fn allowedInStyleRule(this: *const @This()) bool { + return switch (this.*) { + .media, .supports, .container, .moz_document, .layer, .starting_style, .scope, .nest, .unknown, .custom => true, + .namespace, .font_face, .font_feature_values, .font_palette_values, .counter_style, .keyframes, .page, .property, .import, .custom_media, .viewport, .charset => false, + }; + } + }; +} + +pub fn TopLevelRuleParser(comptime AtRuleParserT: type) type { + ValidCustomAtRuleParser(AtRuleParserT); + const AtRuleT = AtRuleParserT.CustomAtRuleParser.AtRule; + const AtRulePreludeT = AtRulePrelude(AtRuleParserT.CustomAtRuleParser.Prelude); + + return struct { + allocator: Allocator, + options: *const ParserOptions, + state: State, + at_rule_parser: *AtRuleParserT, + // TODO: think about memory management + rules: *CssRuleList(AtRuleT), + + const State = enum(u8) { + start = 1, + layers = 2, + imports = 3, + namespaces = 4, + body = 5, + }; + + const This = @This(); + + pub const AtRuleParser = struct { + pub const Prelude = AtRulePreludeT; + pub const AtRule = void; + + pub fn parsePrelude(this: *This, name: []const u8, input: *Parser) Result(Prelude) { + // TODO: optimize string switch + // So rust does the strategy of: + // 1. switch (or if branches) on the length of the input string + // 2. then do string comparison by word size (or smaller sometimes) + // rust sometimes makes jump table https://godbolt.org/z/63d5vYnsP + // sometimes it doesn't make a jump table and just does branching on lengths: https://godbolt.org/z/d8jGPEd56 + // it looks like it will only make a jump table when it knows it won't be too sparse? If I add a "h" case (to make it go 1, 2, 4, 5) or a "hzz" case (so it goes 2, 3, 4, 5) it works: + // - https://godbolt.org/z/WGTMPxafs (change "hzz" to "h" and it works too, remove it and jump table is gone) + // + // I tried recreating the jump table (first link) by hand: https://godbolt.org/z/WPM5c5K4b + // it worked fairly well. Well I actually just made it match on the length, compiler made the jump table, + // so we should let the compiler make the jump table. + // Another recreation with some more nuances: https://godbolt.org/z/9Y1eKdY3r + // Another recreation where hand written is faster than the Rust compiler: https://godbolt.org/z/sTarKe4Yx + // specifically we can make the compiler generate a jump table instead of brancing + // + // Our ExactSizeMatcher is decent + // or comptime string map that calls eqlcomptime function thingy, or std.StaticStringMap + // rust-cssparser does a thing where it allocates stack buffer with maximum possible size and + // then uses that to do ASCII to lowercase conversion: + // https://github.com/servo/rust-cssparser/blob/b75ce6a8df2dbd712fac9d49ba38ee09b96d0d52/src/macros.rs#L168 + // we could probably do something similar, looks like the max length never goes above 20 bytes + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "import")) { + if (@intFromEnum(this.state) > @intFromEnum(State.imports)) { + return .{ .err = input.newCustomError(@as(ParserError, ParserError.unexpected_import_rule)) }; + } + const url_str = switch (input.expectUrlOrString()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + const layer: ?struct { value: ?LayerName } = + if (input.tryParse(Parser.expectIdentMatching, .{"layer"}) == .result) + .{ .value = null } + else if (input.tryParse(Parser.expectFunctionMatching, .{"layer"}) == .result) brk: { + break :brk .{ + .value = switch (input.parseNestedBlock(LayerName, {}, voidWrap(LayerName, LayerName.parse))) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }, + }; + } else null; + + const supports = if (input.tryParse(Parser.expectFunctionMatching, .{"supports"}) == .result) brk: { + const Func = struct { + pub fn do(_: void, p: *Parser) Result(SupportsCondition) { + const result = p.tryParse(SupportsCondition.parse, .{}); + if (result == .err) return SupportsCondition.parseDeclaration(p); + return result; + } + }; + break :brk switch (input.parseNestedBlock(SupportsCondition, {}, Func.do)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + } else null; + + const media = switch (MediaList.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + return .{ + .result = .{ + .import = .{ + url_str, + media, + supports, + if (layer) |l| .{ .value = if (l.value) |ll| ll else null } else null, + }, + }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "namespace")) { + if (@intFromEnum(this.state) > @intFromEnum(State.namespaces)) { + return .{ .err = input.newCustomError(ParserError{ .unexpected_namespace_rule = {} }) }; + } + + const prefix = switch (input.tryParse(Parser.expectIdent, .{})) { + .result => |v| v, + .err => null, + }; + const namespace = switch (input.expectUrlOrString()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = .{ .namespace = .{ prefix, namespace } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "charset")) { + // @charset is removed by rust-cssparser if it’s the first rule in the stylesheet. + // Anything left is technically invalid, however, users often concatenate CSS files + // together, so we are more lenient and simply ignore @charset rules in the middle of a file. + if (input.expectString().asErr()) |e| return .{ .err = e }; + return .{ .result = .charset }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "custom-media")) { + const custom_media_name = switch (DashedIdentFns.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + const media = switch (MediaList.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ + .result = .{ + .custom_media = .{ + custom_media_name, + media, + }, + }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "property")) { + const property_name = switch (DashedIdentFns.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = .{ .property = .{property_name} } }; + } else { + const Nested = NestedRuleParser(AtRuleParserT); + var nested_rule_parser: Nested = this.nested(); + return Nested.AtRuleParser.parsePrelude(&nested_rule_parser, name, input); + } + } + + pub fn parseBlock(this: *This, prelude: AtRuleParser.Prelude, start: *const ParserState, input: *Parser) Result(AtRuleParser.AtRule) { + this.state = .body; + var nested_parser = this.nested(); + return NestedRuleParser(AtRuleParserT).AtRuleParser.parseBlock(&nested_parser, prelude, start, input); + } + + pub fn ruleWithoutBlock(this: *This, prelude: AtRuleParser.Prelude, start: *const ParserState) Maybe(AtRuleParser.AtRule, void) { + const loc_ = start.sourceLocation(); + const loc = css_rules.Location{ + .source_index = this.options.source_index, + .line = loc_.line, + .column = loc_.column, + }; + + switch (prelude) { + .import => { + this.state = State.imports; + this.rules.v.append(this.allocator, .{ + .import = ImportRule{ + .url = prelude.import[0], + .media = prelude.import[1], + .supports = prelude.import[2], + .layer = if (prelude.import[3]) |v| .{ .v = v.value } else null, + .loc = loc, + }, + }) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .namespace => { + this.state = State.namespaces; + + const prefix = prelude.namespace[0]; + const url = prelude.namespace[1]; + + this.rules.v.append(this.allocator, .{ + .namespace = NamespaceRule{ + .prefix = if (prefix) |p| .{ .v = p } else null, + .url = url, + .loc = loc, + }, + }) catch bun.outOfMemory(); + + return .{ .result = {} }; + }, + .custom_media => { + const name = prelude.custom_media[0]; + const query = prelude.custom_media[1]; + this.state = State.body; + this.rules.v.append( + this.allocator, + .{ + .custom_media = css_rules.custom_media.CustomMediaRule{ + .name = name, + .query = query, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .layer => { + if (@intFromEnum(this.state) <= @intFromEnum(State.layers)) { + this.state = .layers; + } else { + this.state = .body; + } + var nested_parser = this.nested(); + return NestedRuleParser(AtRuleParserT).AtRuleParser.ruleWithoutBlock(&nested_parser, prelude, start); + }, + .charset => return .{ .result = {} }, + .unknown => { + const name = prelude.unknown.name; + const prelude2 = prelude.unknown.tokens; + this.rules.v.append(this.allocator, .{ .unknown = UnknownAtRule{ + .name = name, + .prelude = prelude2, + .block = null, + .loc = loc, + } }) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .custom => { + this.state = .body; + var nested_parser = this.nested(); + return NestedRuleParser(AtRuleParserT).AtRuleParser.ruleWithoutBlock(&nested_parser, prelude, start); + }, + else => return .{ .err = {} }, + } + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = selector.parser.SelectorList; + pub const QualifiedRule = void; + + pub fn parsePrelude(this: *This, input: *Parser) Result(Prelude) { + this.state = .body; + var nested_parser = this.nested(); + const N = @TypeOf(nested_parser); + return N.QualifiedRuleParser.parsePrelude(&nested_parser, input); + } + + pub fn parseBlock(this: *This, prelude: Prelude, start: *const ParserState, input: *Parser) Result(QualifiedRule) { + var nested_parser = this.nested(); + const N = @TypeOf(nested_parser); + return N.QualifiedRuleParser.parseBlock(&nested_parser, prelude, start, input); + } + }; + + pub fn new(allocator: Allocator, options: *const ParserOptions, at_rule_parser: *AtRuleParserT, rules: *CssRuleList(AtRuleT)) @This() { + return .{ + .options = options, + .state = .start, + .at_rule_parser = at_rule_parser, + .rules = rules, + .allocator = allocator, + }; + } + + pub fn nested(this: *This) NestedRuleParser(AtRuleParserT) { + return NestedRuleParser(AtRuleParserT){ + .options = this.options, + .at_rule_parser = this.at_rule_parser, + .declarations = DeclarationList{}, + .important_declarations = DeclarationList{}, + .rules = this.rules, + .is_in_style_rule = false, + .allow_declarations = false, + .allocator = this.allocator, + }; + } + }; +} + +pub fn NestedRuleParser(comptime T: type) type { + ValidCustomAtRuleParser(T); + + const AtRuleT = T.CustomAtRuleParser.AtRule; + + return struct { + options: *const ParserOptions, + at_rule_parser: *T, + // todo_stuff.think_mem_mgmt + declarations: DeclarationList, + // todo_stuff.think_mem_mgmt + important_declarations: DeclarationList, + // todo_stuff.think_mem_mgmt + rules: *CssRuleList(T.CustomAtRuleParser.AtRule), + is_in_style_rule: bool, + allow_declarations: bool, + allocator: Allocator, + + const This = @This(); + + pub fn getLoc(this: *This, start: *const ParserState) Location { + const loc = start.sourceLocation(); + return Location{ + .source_index = this.options.source_index, + .line = loc.line, + .column = loc.column, + }; + } + + pub const AtRuleParser = struct { + pub const Prelude = AtRulePrelude(T.CustomAtRuleParser.Prelude); + pub const AtRule = void; + + pub fn parsePrelude(this: *This, name: []const u8, input: *Parser) Result(Prelude) { + const result: Prelude = brk: { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "media")) { + const media = switch (MediaList.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + break :brk .{ .media = media }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "supports")) { + const cond = switch (SupportsCondition.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + break :brk .{ .supports = cond }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "font-face")) { + break :brk .font_face; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "font-palette-values")) { + const dashed_ident_name = switch (DashedIdentFns.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + break :brk .{ .font_palette_values = dashed_ident_name }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "counter-style")) { + const custom_name = switch (CustomIdentFns.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + break :brk .{ .counter_style = custom_name }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "viewport") or bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-viewport")) { + const prefix: VendorPrefix = if (bun.strings.startsWithCaseInsensitiveAscii(name, "-ms")) VendorPrefix{ .ms = true } else VendorPrefix{ .none = true }; + break :brk .{ .viewport = prefix }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "keyframes") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-viewport") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-keyframes") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-o-keyframes") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-keyframes")) + { + const prefix: VendorPrefix = if (bun.strings.startsWithCaseInsensitiveAscii(name, "-webkit")) + VendorPrefix{ .webkit = true } + else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-moz-")) + VendorPrefix{ .moz = true } + else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-o-")) + VendorPrefix{ .o = true } + else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-ms-")) VendorPrefix{ .ms = true } else VendorPrefix{ .none = true }; + + const keyframes_name = switch (input.tryParse(css_rules.keyframes.KeyframesName.parse, .{})) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + break :brk .{ .keyframes = .{ .name = keyframes_name, .prefix = prefix } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "page")) { + const Fn = struct { + pub fn parsefn(input2: *Parser) Result(ArrayList(css_rules.page.PageSelector)) { + return input2.parseCommaSeparated(css_rules.page.PageSelector, css_rules.page.PageSelector.parse); + } + }; + const selectors = switch (input.tryParse(Fn.parsefn, .{})) { + .result => |v| v, + .err => ArrayList(css_rules.page.PageSelector){}, + }; + break :brk .{ .page = selectors }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-document")) { + // Firefox only supports the url-prefix() function with no arguments as a legacy CSS hack. + // See https://css-tricks.com/snippets/css/css-hacks-targeting-firefox/ + if (input.expectFunctionMatching("url-prefix").asErr()) |e| return .{ .err = e }; + const Fn = struct { + pub fn parsefn(_: void, input2: *Parser) Result(void) { + // Firefox also allows an empty string as an argument... + // https://github.com/mozilla/gecko-dev/blob/0077f2248712a1b45bf02f0f866449f663538164/servo/components/style/stylesheets/document_rule.rs#L303 + _ = input2.tryParse(parseInner, .{}); + if (input2.expectExhausted().asErr()) |e| return .{ .err = e }; + return .{ .result = {} }; + } + fn parseInner(input2: *Parser) Result(void) { + const s = switch (input2.expectString()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (s.len > 0) { + return .{ .err = input2.newCustomError(ParserError.invalid_value) }; + } + return .{ .result = {} }; + } + }; + if (input.parseNestedBlock(void, {}, Fn.parsefn).asErr()) |e| return .{ .err = e }; + break :brk .moz_document; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "layer")) { + const names = switch (input.parseList(LayerName, LayerName.parse)) { + .result => |vv| vv, + .err => |e| names: { + if (e.kind == .basic and e.kind.basic == .end_of_input) { + break :names ArrayList(LayerName){}; + } + return .{ .err = e }; + }, + }; + + break :brk .{ .layer = names }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "container")) { + const container_name = switch (input.tryParse(css_rules.container.ContainerName.parse, .{})) { + .result => |vv| vv, + .err => null, + }; + const condition = switch (css_rules.container.ContainerCondition.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + break :brk .{ .container = .{ .name = container_name, .condition = condition } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "starting-style")) { + break :brk .starting_style; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "scope")) { + var selector_parser = selector.parser.SelectorParser{ + .is_nesting_allowed = true, + .options = this.options, + .allocator = input.allocator(), + }; + const Closure = struct { + selector_parser: *selector.parser.SelectorParser, + pub fn parsefn(self: *@This(), input2: *Parser) Result(selector.parser.SelectorList) { + return selector.parser.SelectorList.parseRelative(self.selector_parser, input2, .ignore_invalid_selector, .none); + } + }; + var closure = Closure{ + .selector_parser = &selector_parser, + }; + + const scope_start = if (input.tryParse(Parser.expectParenthesisBlock, .{}).isOk()) scope_start: { + break :scope_start switch (input.parseNestedBlock(selector.parser.SelectorList, &closure, Closure.parsefn)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + } else null; + + const scope_end = if (input.tryParse(Parser.expectIdentMatching, .{"to"}).isOk()) scope_end: { + if (input.expectParenthesisBlock().asErr()) |e| return .{ .err = e }; + break :scope_end switch (input.parseNestedBlock(selector.parser.SelectorList, &closure, Closure.parsefn)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + } else null; + + break :brk .{ + .scope = .{ + .scope_start = scope_start, + .scope_end = scope_end, + }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nest") and this.is_in_style_rule) { + this.options.warn(input.newCustomError(ParserError{ .deprecated_nest_rule = {} })); + var selector_parser = selector.parser.SelectorParser{ + .is_nesting_allowed = true, + .options = this.options, + .allocator = input.allocator(), + }; + const selectors = switch (selector.parser.SelectorList.parse(&selector_parser, input, .discard_list, .contained)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + break :brk .{ .nest = selectors }; + } else { + break :brk switch (parse_custom_at_rule_prelude( + name, + input, + this.options, + T, + this.at_rule_parser, + )) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + } + }; + + if (this.is_in_style_rule and !result.allowedInStyleRule()) { + return .{ .err = input.newError(BasicParseErrorKind{ .at_rule_invalid = name }) }; + } + + return .{ .result = result }; + } + + pub fn parseBlock(this: *This, prelude: AtRuleParser.Prelude, start: *const ParserState, input: *Parser) Result(AtRuleParser.AtRule) { + const loc = this.getLoc(start); + switch (prelude) { + .font_face => { + var decl_parser = css_rules.font_face.FontFaceDeclarationParser{}; + var parser = RuleBodyParser(css_rules.font_face.FontFaceDeclarationParser).new(input, &decl_parser); + // todo_stuff.think_mem_mgmt + var properties: ArrayList(css_rules.font_face.FontFaceProperty) = .{}; + + while (parser.next()) |result| { + if (result.asValue()) |decl| { + properties.append( + input.allocator(), + decl, + ) catch bun.outOfMemory(); + } + } + + this.rules.v.append( + input.allocator(), + .{ + .font_face = css_rules.font_face.FontFaceRule{ + .properties = properties, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .font_palette_values => { + const name = prelude.font_palette_values; + const rule = switch (css_rules.font_palette_values.FontPaletteValuesRule.parse(name, input, loc)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + this.rules.v.append( + input.allocator(), + .{ .font_palette_values = rule }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .counter_style => { + const name = prelude.counter_style; + this.rules.v.append( + input.allocator(), + .{ + .counter_style = css_rules.counter_style.CounterStyleRule{ + .name = name, + .declarations = switch (DeclarationBlock.parse(input, this.options)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .media => { + const query = prelude.media; + const rules = switch (this.parseStyleBlock(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + this.rules.v.append( + input.allocator(), + .{ + .media = css_rules.media.MediaRule(T.CustomAtRuleParser.AtRule){ + .query = query, + .rules = rules, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .supports => { + const condition = prelude.supports; + const rules = switch (this.parseStyleBlock(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + this.rules.v.append(input.allocator(), .{ + .supports = css_rules.supports.SupportsRule(T.CustomAtRuleParser.AtRule){ + .condition = condition, + .rules = rules, + .loc = loc, + }, + }) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .container => { + const rules = switch (this.parseStyleBlock(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + this.rules.v.append( + input.allocator(), + .{ + .container = css_rules.container.ContainerRule(T.CustomAtRuleParser.AtRule){ + .name = prelude.container.name, + .condition = prelude.container.condition, + .rules = rules, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .scope => { + const rules = switch (this.parseStyleBlock(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + this.rules.v.append( + input.allocator(), + .{ + .scope = css_rules.scope.ScopeRule(T.CustomAtRuleParser.AtRule){ + .scope_start = prelude.scope.scope_start, + .scope_end = prelude.scope.scope_end, + .rules = rules, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .viewport => { + this.rules.v.append(input.allocator(), .{ + .viewport = css_rules.viewport.ViewportRule{ + .vendor_prefix = prelude.viewport, + .declarations = switch (DeclarationBlock.parse(input, this.options)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + .loc = loc, + }, + }) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .keyframes => { + var parser = css_rules.keyframes.KeyframesListParser{}; + var iter = RuleBodyParser(css_rules.keyframes.KeyframesListParser).new(input, &parser); + // todo_stuff.think_mem_mgmt + var keyframes = ArrayList(css_rules.keyframes.Keyframe){}; + + while (iter.next()) |result| { + if (result.asValue()) |keyframe| { + keyframes.append( + input.allocator(), + keyframe, + ) catch bun.outOfMemory(); + } + } + + this.rules.v.append(input.allocator(), .{ + .keyframes = css_rules.keyframes.KeyframesRule{ + .name = prelude.keyframes.name, + .keyframes = keyframes, + .vendor_prefix = prelude.keyframes.prefix, + .loc = loc, + }, + }) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .page => { + const selectors = prelude.page; + const rule = switch (css_rules.page.PageRule.parse(selectors, input, loc, this.options)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + this.rules.v.append( + input.allocator(), + .{ .page = rule }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .moz_document => { + const rules = switch (this.parseStyleBlock(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + this.rules.v.append(input.allocator(), .{ + .moz_document = css_rules.document.MozDocumentRule(T.CustomAtRuleParser.AtRule){ + .rules = rules, + .loc = loc, + }, + }) catch bun.outOfMemory(); + 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; + } else return .{ .err = input.newError(.at_rule_body_invalid) }; + + const rules = switch (this.parseStyleBlock(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + this.rules.v.append(input.allocator(), .{ + .layer_block = css_rules.layer.LayerBlockRule(T.CustomAtRuleParser.AtRule){ .name = name, .rules = rules, .loc = loc }, + }) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .property => { + const name = prelude.property[0]; + this.rules.v.append(input.allocator(), .{ + .property = switch (css_rules.property.PropertyRule.parse(name, input, loc)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + }) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .import, .namespace, .custom_media, .charset => { + // These rules don't have blocks + return .{ .err = input.newUnexpectedTokenError(.open_curly) }; + }, + .starting_style => { + const rules = switch (this.parseStyleBlock(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + this.rules.v.append( + input.allocator(), + .{ + .starting_style = css_rules.starting_style.StartingStyleRule(T.CustomAtRuleParser.AtRule){ + .rules = rules, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .nest => { + const selectors = prelude.nest; + const result = switch (this.parseNested(input, true)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + const declarations = result[0]; + const rules = result[1]; + this.rules.v.append( + input.allocator(), + .{ + .nesting = css_rules.nesting.NestingRule(T.CustomAtRuleParser.AtRule){ + .style = css_rules.style.StyleRule(T.CustomAtRuleParser.AtRule){ + .selectors = selectors, + .declarations = declarations, + .vendor_prefix = VendorPrefix.empty(), + .rules = rules, + .loc = loc, + }, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .font_feature_values => bun.unreachablePanic("", .{}), + .unknown => { + this.rules.v.append( + input.allocator(), + .{ + .unknown = css_rules.unknown.UnknownAtRule{ + .name = prelude.unknown.name, + .prelude = prelude.unknown.tokens, + .block = switch (TokenListFns.parse(input, this.options, 0)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .custom => { + this.rules.v.append( + input.allocator(), + .{ + .custom = switch (parse_custom_at_rule_body(T, prelude.custom, input, start, this.options, this.at_rule_parser, this.is_in_style_rule)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + } + } + + pub fn ruleWithoutBlock(this: *This, prelude: AtRuleParser.Prelude, start: *const ParserState) Maybe(AtRuleParser.AtRule, void) { + const loc = this.getLoc(start); + switch (prelude) { + .layer => { + if (this.is_in_style_rule or prelude.layer.items.len == 0) { + return .{ .err = {} }; + } + + this.rules.v.append( + this.allocator, + .{ + .layer_statement = css_rules.layer.LayerStatementRule{ + .names = prelude.layer, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .unknown => { + this.rules.v.append( + this.allocator, + .{ + .unknown = css_rules.unknown.UnknownAtRule{ + .name = prelude.unknown.name, + .prelude = prelude.unknown.tokens, + .block = null, + .loc = loc, + }, + }, + ) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + .custom => { + this.rules.v.append(this.allocator, switch (parse_custom_at_rule_without_block( + T, + prelude.custom, + start, + this.options, + this.at_rule_parser, + this.is_in_style_rule, + )) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }) catch bun.outOfMemory(); + return .{ .result = {} }; + }, + else => return .{ .err = {} }, + } + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = selector.parser.SelectorList; + pub const QualifiedRule = void; + + pub fn parsePrelude(this: *This, input: *Parser) Result(Prelude) { + var selector_parser = selector.parser.SelectorParser{ + .is_nesting_allowed = true, + .options = this.options, + .allocator = input.allocator(), + }; + + if (this.is_in_style_rule) { + return selector.parser.SelectorList.parseRelative(&selector_parser, input, .discard_list, .implicit); + } else { + return selector.parser.SelectorList.parse(&selector_parser, input, .discard_list, .none); + } + } + + pub fn parseBlock(this: *This, selectors: Prelude, start: *const ParserState, input: *Parser) Result(QualifiedRule) { + const loc = this.getLoc(start); + const result = switch (this.parseNested(input, true)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + const declarations = result[0]; + const rules = result[1]; + + this.rules.v.append(this.allocator, .{ + .style = StyleRule(AtRuleT){ + .selectors = selectors, + .vendor_prefix = VendorPrefix{}, + .declarations = declarations, + .rules = rules, + .loc = loc, + }, + }) catch bun.outOfMemory(); + + return Result(QualifiedRule).success; + } + }; + + pub const RuleBodyItemParser = struct { + pub fn parseQualified(this: *This) bool { + _ = this; // autofix + return true; + } + + pub fn parseDeclarations(this: *This) bool { + return this.allow_declarations; + } + }; + + pub const DeclarationParser = struct { + pub const Declaration = void; + + pub fn parseValue(this: *This, name: []const u8, input: *Parser) Result(Declaration) { + return css_decls.parse_declaration( + name, + input, + &this.declarations, + &this.important_declarations, + this.options, + ); + } + }; + + pub fn parseNested(this: *This, input: *Parser, is_style_rule: bool) Result(struct { DeclarationBlock, CssRuleList(T.CustomAtRuleParser.AtRule) }) { + // TODO: think about memory management in error cases + var rules = CssRuleList(T.CustomAtRuleParser.AtRule){}; + var nested_parser = This{ + .allocator = input.allocator(), + .options = this.options, + .at_rule_parser = this.at_rule_parser, + .declarations = DeclarationList{}, + .important_declarations = DeclarationList{}, + .rules = &rules, + .is_in_style_rule = this.is_in_style_rule or is_style_rule, + .allow_declarations = this.allow_declarations or this.is_in_style_rule or is_style_rule, + }; + + const parse_declarations = This.RuleBodyItemParser.parseDeclarations(&nested_parser); + // TODO: think about memory management + var errors = ArrayList(ParseError(ParserError)){}; + var iter = RuleBodyParser(This).new(input, &nested_parser); + + while (iter.next()) |result| { + if (result.asErr()) |e| { + if (parse_declarations) { + iter.parser.declarations.clearRetainingCapacity(); + iter.parser.important_declarations.clearRetainingCapacity(); + errors.append( + this.allocator, + e, + ) catch bun.outOfMemory(); + } else { + if (iter.parser.options.error_recovery) { + iter.parser.options.warn(e); + continue; + } + return .{ .err = e }; + } + } + } + + if (parse_declarations) { + if (errors.items.len > 0) { + if (this.options.error_recovery) { + for (errors.items) |e| { + this.options.warn(e); + } + } else { + return .{ .err = errors.orderedRemove(0) }; + } + } + } + + return .{ + .result = .{ + DeclarationBlock{ + .declarations = nested_parser.declarations, + .important_declarations = nested_parser.important_declarations, + }, + rules, + }, + }; + } + + pub fn parseStyleBlock(this: *This, input: *Parser) Result(CssRuleList(T.CustomAtRuleParser.AtRule)) { + const srcloc = input.currentSourceLocation(); + const loc = Location{ + .source_index = this.options.source_index, + .line = srcloc.line, + .column = srcloc.column, + }; + + // Declarations can be immediately within @media and @supports blocks that are nested within a parent style rule. + // These act the same way as if they were nested within a `& { ... }` block. + const declarations, var rules = switch (this.parseNested(input, false)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + if (declarations.len() > 0) { + rules.v.insert( + input.allocator(), + 0, + .{ + .style = StyleRule(T.CustomAtRuleParser.AtRule){ + .selectors = selector.parser.SelectorList.fromSelector( + input.allocator(), + selector.parser.Selector.fromComponent(input.allocator(), .nesting), + ), + .declarations = declarations, + .vendor_prefix = VendorPrefix.empty(), + .rules = .{}, + .loc = loc, + }, + }, + ) catch unreachable; + } + + return .{ .result = rules }; + } + }; +} + +pub fn StyleSheetParser(comptime P: type) type { + ValidAtRuleParser(P); + ValidQualifiedRuleParser(P); + + if (P.QualifiedRuleParser.QualifiedRule != P.AtRuleParser.AtRule) { + @compileError("StyleSheetParser: P.QualifiedRuleParser.QualifiedRule != P.AtRuleParser.AtRule"); + } + + const Item = P.AtRuleParser.AtRule; + + return struct { + input: *Parser, + parser: *P, + any_rule_so_far: bool = false, + + pub fn new(input: *Parser, parser: *P) @This() { + return .{ + .input = input, + .parser = parser, + }; + } + + pub fn next(this: *@This(), allocator: Allocator) ?(Result(Item)) { + while (true) { + this.input.@"skip cdc and cdo"(); + + const start = this.input.state(); + const at_keyword: ?[]const u8 = switch (this.input.nextByte() orelse return null) { + '@' => brk: { + const at_keyword: *Token = switch (this.input.nextIncludingWhitespaceAndComments()) { + .result => |vv| vv, + .err => { + this.input.reset(&start); + break :brk null; + }, + }; + + if (at_keyword.* == .at_keyword) break :brk at_keyword.at_keyword; + this.input.reset(&start); + break :brk null; + }, + else => null, + }; + + if (at_keyword) |name| { + const first_stylesheet_rule = !this.any_rule_so_far; + this.any_rule_so_far = true; + + if (first_stylesheet_rule and bun.strings.eqlCaseInsensitiveASCII(name, "charset", true)) { + const delimiters = Delimiters{ + .semicolon = true, + .close_curly_bracket = true, + }; + _ = this.input.parseUntilAfter(delimiters, void, {}, voidWrap(void, Parser.parseEmpty)); + } else { + return parse_at_rule(allocator, &start, name, this.input, P, this.parser); + } + } else { + this.any_rule_so_far = true; + const result = parse_qualified_rule(&start, this.input, P, this.parser, Delimiters{ .curly_bracket = true }); + return result; + } + } + } + }; +} + +/// A result returned from `to_css`, including the serialized CSS +/// and other metadata depending on the input options. +pub const ToCssResult = struct { + /// Serialized CSS code. + code: []const u8, + /// A map of CSS module exports, if the `css_modules` option was + /// enabled during parsing. + exports: ?CssModuleExports, + /// A map of CSS module references, if the `css_modules` config + /// had `dashed_idents` enabled. + references: ?CssModuleReferences, + /// A list of dependencies (e.g. `@import` or `url()`) found in + /// the style sheet, if the `analyze_dependencies` option is enabled. + dependencies: ?ArrayList(Dependency), +}; + +pub const MinifyOptions = struct { + /// Targets to compile the CSS for. + targets: targets.Targets, + /// A list of known unused symbols, including CSS class names, + /// ids, and `@keyframe` names. The declarations of these will be removed. + unused_symbols: std.StringArrayHashMapUnmanaged(void), + + pub fn default() MinifyOptions { + return MinifyOptions{ + .targets = .{}, + .unused_symbols = .{}, + }; + } +}; + +pub fn StyleSheet(comptime AtRule: type) type { + return struct { + /// A list of top-level rules within the style sheet. + rules: CssRuleList(AtRule), + sources: ArrayList([]const u8), + source_map_urls: ArrayList(?[]const u8), + license_comments: ArrayList([]const u8), + options: ParserOptions, + + const This = @This(); + + /// Minify and transform the style sheet for the provided browser targets. + pub fn minify(this: *@This(), allocator: Allocator, options: MinifyOptions) Maybe(void, Err(MinifyErrorKind)) { + _ = this; // autofix + _ = allocator; // autofix + _ = options; // autofix + // TODO + return .{ .result = {} }; + + // const ctx = PropertyHandlerContext.new(allocator, options.targets, &options.unused_symbols); + // var handler = declaration.DeclarationHandler.default(); + // var important_handler = declaration.DeclarationHandler.default(); + + // // @custom-media rules may be defined after they are referenced, but may only be defined at the top level + // // of a stylesheet. Do a pre-scan here and create a lookup table by name. + // const custom_media: ?std.StringArrayHashMapUnmanaged(css_rules.custom_media.CustomMediaRule) = if (this.options.flags.contains(ParserFlags{ .custom_media = true }) and options.targets.shouldCompileSame(.custom_media_queries)) brk: { + // var custom_media = std.StringArrayHashMapUnmanaged(css_rules.custom_media.CustomMediaRule){}; + + // for (this.rules.v.items) |*rule| { + // if (rule.* == .custom_media) { + // custom_media.put(allocator, rule.custom_media.name, rule.deepClone(allocator)) catch bun.outOfMemory(); + // } + // } + + // break :brk custom_media; + // } else null; + // defer if (custom_media) |media| media.deinit(allocator); + + // var minify_ctx = MinifyContext{ + // .targets = &options.targets, + // .handler = &handler, + // .important_handler = &important_handler, + // .handler_context = ctx, + // .unused_symbols = &options.unused_symbols, + // .custom_media = custom_media, + // .css_modules = this.options.css_modules != null, + // }; + + // switch (this.rules.minify(&minify_ctx, false)) { + // .result => return .{ .result = {} }, + // .err => |e| { + // _ = e; // autofix + // @panic("TODO: here"); + // // return .{ .err = .{ .kind = e, .loc = } }; + // }, + // } + } + + pub fn toCss(this: *const @This(), allocator: Allocator, options: css_printer.PrinterOptions) PrintErr!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 W = @TypeOf(writer); + const project_root = options.project_root; + var printer = Printer(@TypeOf(writer)).new(allocator, std.ArrayList(u8).init(allocator), writer, options); + + // #[cfg(feature = "sourcemap")] + // { + // printer.sources = Some(&self.sources); + // } + + // #[cfg(feature = "sourcemap")] + // if printer.source_map.is_some() { + // printer.source_maps = self.sources.iter().enumerate().map(|(i, _)| self.source_map(i)).collect(); + // } + + for (this.license_comments.items) |comment| { + try printer.writeStr("/*"); + try printer.writeStr(comment); + try printer.writeStr("*/\n"); + } + + if (this.options.css_modules) |*config| { + var references = CssModuleReferences{}; + printer.css_module = CssModule.new(allocator, config, &this.sources, project_root, &references); + + try this.rules.toCss(W, &printer); + try printer.newline(); + + return ToCssResult{ + .dependencies = printer.dependencies, + .exports = exports: { + const val = printer.css_module.?.exports_by_source_index.items[0]; + printer.css_module.?.exports_by_source_index.items[0] = .{}; + break :exports val; + }, + .code = dest.items, + .references = references, + }; + } else { + try this.rules.toCss(W, &printer); + try printer.newline(); + return ToCssResult{ + .dependencies = printer.dependencies, + .code = dest.items, + .exports = null, + .references = null, + }; + } + } + + pub fn parse(allocator: Allocator, code: []const u8, options: ParserOptions) Maybe(This, Err(ParserError)) { + var default_at_rule_parser = DefaultAtRuleParser{}; + return parseWith(allocator, code, options, DefaultAtRuleParser, &default_at_rule_parser); + } + + /// Parse a style sheet from a string. + pub fn parseWith( + allocator: Allocator, + code: []const u8, + options: ParserOptions, + comptime P: type, + at_rule_parser: *P, + ) Maybe(This, Err(ParserError)) { + var input = ParserInput.new(allocator, code); + var parser = Parser.new(&input); + + var license_comments = ArrayList([]const u8){}; + var state = parser.state(); + while (switch (parser.nextIncludingWhitespaceAndComments()) { + .result => |v| v, + .err => null, + }) |token| { + switch (token.*) { + .whitespace => {}, + .comment => |comment| { + if (bun.strings.startsWithChar(comment, '!')) { + license_comments.append(allocator, comment) catch bun.outOfMemory(); + } + }, + else => break, + } + state = parser.state(); + } + parser.reset(&state); + + var rules = CssRuleList(AtRule){}; + var rule_parser = TopLevelRuleParser(P).new(allocator, &options, at_rule_parser, &rules); + var rule_list_parser = StyleSheetParser(TopLevelRuleParser(P)).new(&parser, &rule_parser); + + while (rule_list_parser.next(allocator)) |result| { + if (result.asErr()) |e| { + const result_options = rule_list_parser.parser.options; + if (result_options.error_recovery) { + // todo_stuff.warn + continue; + } + + return .{ .err = Err(ParserError).fromParseError(e, options.filename) }; + } + } + + var sources = ArrayList([]const u8){}; + sources.append(allocator, options.filename) catch bun.outOfMemory(); + var source_map_urls = ArrayList(?[]const u8){}; + source_map_urls.append(allocator, parser.currentSourceMapUrl()) catch bun.outOfMemory(); + + return .{ + .result = This{ + .rules = rules, + .sources = sources, + .source_map_urls = source_map_urls, + .license_comments = license_comments, + .options = options, + }, + }; + } + }; +} + +pub const StyleAttribute = struct { + declarations: DeclarationBlock, + sources: ArrayList([]const u8), + + pub fn parse(allocator: Allocator, code: []const u8, options: ParserOptions) Maybe(StyleAttribute, Err(ParserError)) { + var input = ParserInput.new(allocator, code); + var parser = Parser.new(&input); + const sources = sources: { + var s = ArrayList([]const u8).initCapacity(allocator, 1) catch bun.outOfMemory(); + s.appendAssumeCapacity(options.filename); + break :sources s; + }; + return .{ .result = StyleAttribute{ + .declarations = switch (DeclarationBlock.parse(&parser, &options)) { + .result => |v| v, + .err => |e| return .{ .err = Err(ParserError).fromParseError(e, "") }, + }, + .sources = sources, + } }; + } + + pub fn toCss(this: *const StyleAttribute, allocator: Allocator, options: PrinterOptions) PrintErr!ToCssResult { + // #[cfg(feature = "sourcemap")] + // assert!( + // options.source_map.is_none(), + // "Source maps are not supported for style attributes" + // ); + + var dest = ArrayList(u8){}; + const writer = dest.writer(allocator); + var printer = Printer(@TypeOf(writer)).new(allocator, std.ArrayList(u8).init(allocator), writer, options); + printer.sources = &this.sources; + + try this.declarations.toCss(@TypeOf(writer), &printer); + + return ToCssResult{ + .dependencies = printer.dependencies, + .code = dest.items, + .exports = null, + .references = null, + }; + } + + pub fn minify(this: *@This(), allocator: Allocator, options: MinifyOptions) void { + _ = allocator; // autofix + _ = this; // autofix + _ = options; // autofix + // TODO: IMPLEMENT THIS! + } +}; + +pub fn ValidDeclarationParser(comptime P: type) void { + // The finished representation of a declaration. + _ = P.DeclarationParser.Declaration; + + // Parse the value of a declaration with the given `name`. + // + // Return the finished representation for the declaration + // as returned by `DeclarationListParser::next`, + // or `Err(())` to ignore the entire declaration as invalid. + // + // Declaration name matching should be case-insensitive in the ASCII range. + // This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + // or with the `match_ignore_ascii_case!` macro. + // + // The given `input` is a "delimited" parser + // that ends wherever the declaration value should end. + // (In declaration lists, before the next semicolon or end of the current block.) + // + // If `!important` can be used in a given context, + // `input.try_parse(parse_important).is_ok()` should be used at the end + // of the implementation of this method and the result should be part of the return value. + // + // fn parseValue(this: *T, name: []const u8, input: *Parser) Error!T.DeclarationParser.Declaration + _ = P.DeclarationParser.parseValue; +} + +/// Also checks that P is: +/// - ValidDeclarationParser(P) +/// - ValidQualifiedRuleParser(P) +/// - ValidAtRuleParser(P) +pub fn ValidRuleBodyItemParser(comptime P: type) void { + ValidDeclarationParser(P); + ValidQualifiedRuleParser(P); + ValidAtRuleParser(P); + + // Whether we should attempt to parse declarations. If you know you won't, returning false + // here is slightly faster. + _ = P.RuleBodyItemParser.parseDeclarations; + + // Whether we should attempt to parse qualified rules. If you know you won't, returning false + // would be slightly faster. + _ = P.RuleBodyItemParser.parseQualified; + + // We should have: + // P.DeclarationParser.Declaration == P.QualifiedRuleParser.QualifiedRule == P.AtRuleParser.AtRule + if (P.DeclarationParser.Declaration != P.QualifiedRuleParser.QualifiedRule or + P.DeclarationParser.Declaration != P.AtRuleParser.AtRule) + { + @compileError("ValidRuleBodyItemParser: P.DeclarationParser.Declaration != P.QualifiedRuleParser.QualifiedRule or\n P.DeclarationParser.Declaration != P.AtRuleParser.AtRule"); + } +} + +pub fn RuleBodyParser(comptime P: type) type { + ValidRuleBodyItemParser(P); + // Same as P.AtRuleParser.AtRule and P.DeclarationParser.Declaration + const I = P.QualifiedRuleParser.QualifiedRule; + + return struct { + input: *Parser, + parser: *P, + + const This = @This(); + + pub fn new(input: *Parser, parser: *P) This { + return .{ + .input = input, + .parser = parser, + }; + } + + /// TODO: result is actually: + /// type Item = Result, &'i str)>; + /// + /// but nowhere in the source do i actually see it using the string part of the tuple + pub fn next(this: *This) ?(Result(I)) { + while (true) { + this.input.skipWhitespace(); + const start = this.input.state(); + + const tok: *Token = switch (this.input.nextIncludingWhitespaceAndComments()) { + .err => |_| return null, + .result => |vvv| vvv, + }; + + switch (tok.*) { + .close_curly, .whitespace, .semicolon, .comment => continue, + .at_keyword => { + const name = tok.at_keyword; + return parse_at_rule( + this.input.allocator(), + &start, + name, + this.input, + P, + this.parser, + ); + }, + .ident => { + if (P.RuleBodyItemParser.parseDeclarations(this.parser)) { + const name = tok.ident; + const parse_qualified = P.RuleBodyItemParser.parseQualified(this.parser); + const result: Result(I) = result: { + const error_behavior: ParseUntilErrorBehavior = if (parse_qualified) .stop else .consume; + const Closure = struct { + parser: *P, + name: []const u8, + pub fn parsefn(self: *@This(), input: *Parser) Result(I) { + if (input.expectColon().asErr()) |e| return .{ .err = e }; + return P.DeclarationParser.parseValue(self.parser, self.name, input); + } + }; + var closure = Closure{ + .parser = this.parser, + .name = name, + }; + break :result parse_until_after(this.input, Delimiters{ .semicolon = true }, error_behavior, I, &closure, Closure.parsefn); + }; + if (result.isErr() and parse_qualified) { + this.input.reset(&start); + if (parse_qualified_rule( + &start, + this.input, + P, + this.parser, + Delimiters{ .semicolon = true, .curly_bracket = true }, + ).asValue()) |qual| { + return .{ .result = qual }; + } + } + + return result; + } + }, + else => {}, + } + + const result: Result(I) = if (P.RuleBodyItemParser.parseQualified(this.parser)) result: { + this.input.reset(&start); + const delimiters = if (P.RuleBodyItemParser.parseDeclarations(this.parser)) Delimiters{ + .semicolon = true, + .curly_bracket = true, + } else Delimiters{ .curly_bracket = true }; + break :result parse_qualified_rule(&start, this.input, P, this.parser, delimiters); + } else result: { + const token = tok.*; + + const Closure = struct { token: Token, start: ParserState }; + break :result this.input.parseUntilAfter(Delimiters{ .semicolon = true }, I, &Closure{ .token = token, .start = start }, struct { + pub fn parseFn(closure: *const Closure, i: *Parser) Result(I) { + _ = i; // autofix + return .{ .err = closure.start.sourceLocation().newUnexpectedTokenError(closure.token) }; + } + }.parseFn); + }; + + return result; + } + } + }; +} + +pub const ParserOptions = struct { + /// Filename to use in error messages. + filename: []const u8, + /// Whether the enable [CSS modules](https://github.com/css-modules/css-modules). + css_modules: ?css_modules.Config, + /// The source index to assign to all parsed rules. Impacts the source map when + /// the style sheet is serialized. + source_index: u32, + /// Whether to ignore invalid rules and declarations rather than erroring. + error_recovery: bool, + /// A list that will be appended to when a warning occurs. + logger: ?*Log = null, + /// Feature flags to enable. + flags: ParserFlags, + allocator: Allocator, + + pub fn warn(this: *const ParserOptions, warning: ParseError(ParserError)) void { + if (this.logger) |lg| { + lg.addWarningFmtLineCol( + this.filename, + warning.location.line, + warning.location.column, + this.allocator, + "{}", + .{warning.kind}, + ) catch unreachable; + } + } + + pub fn default(allocator: std.mem.Allocator, log: ?*Log) ParserOptions { + return ParserOptions{ + .filename = "", + .css_modules = null, + .source_index = 0, + .error_recovery = false, + .logger = log, + .flags = ParserFlags{}, + .allocator = allocator, + }; + } +}; + +/// Parser feature flags to enable. +pub const ParserFlags = packed struct(u8) { + /// Whether the enable the [CSS nesting](https://www.w3.org/TR/css-nesting-1/) draft syntax. + nesting: bool = false, + /// Whether to enable the [custom media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) draft syntax. + custom_media: bool = false, + /// Whether to enable the non-standard >>> and /deep/ selector combinators used by Vue and Angular. + deep_selector_combinator: bool = false, + __unused: u5 = 0, + + pub usingnamespace Bitflags(@This()); +}; + +const ParseUntilErrorBehavior = enum { + consume, + stop, +}; + +pub const Parser = struct { + input: *ParserInput, + at_start_of: ?BlockType = null, + stop_before: Delimiters = Delimiters.NONE, + + pub inline fn allocator(self: *Parser) Allocator { + return self.input.tokenizer.allocator; + } + + pub fn new(input: *ParserInput) Parser { + return Parser{ + .input = input, + }; + } + + pub fn newCustomError(this: *const Parser, err: ParserError) ParseError(ParserError) { + return this.currentSourceLocation().newCustomError(err); + } + + pub fn newBasicError(this: *const Parser, kind: BasicParseErrorKind) BasicParseError { + return BasicParseError{ + .kind = kind, + .location = this.currentSourceLocation(), + }; + } + + pub fn newError(this: *const Parser, kind: BasicParseErrorKind) ParseError(ParserError) { + return .{ + .kind = .{ .basic = kind }, + .location = this.currentSourceLocation(), + }; + } + + pub fn newUnexpectedTokenError(this: *const Parser, token: Token) ParseError(ParserError) { + return this.newError(.{ .unexpected_token = token }); + } + + pub fn newBasicUnexpectedTokenError(this: *const Parser, token: Token) ParseError(ParserError) { + return this.newBasicError(.{ .unexpected_token = token }).intoDefaultParseError(); + } + + pub fn currentSourceLocation(this: *const Parser) SourceLocation { + return this.input.tokenizer.currentSourceLocation(); + } + + pub fn currentSourceMapUrl(this: *const Parser) ?[]const u8 { + return this.input.tokenizer.currentSourceMapUrl(); + } + + /// Return a slice of the CSS input, from the given position to the current one. + pub fn sliceFrom(this: *const Parser, start_position: usize) []const u8 { + return this.input.tokenizer.sliceFrom(start_position); + } + + /// Implementation of Vec::::parse + pub fn parseList(this: *Parser, comptime T: type, comptime parse_one: *const fn (*Parser) Result(T)) Result(ArrayList(T)) { + return this.parseCommaSeparated(T, parse_one); + } + + /// Parse a list of comma-separated values, all with the same syntax. + /// + /// The given closure is called repeatedly with a "delimited" parser + /// (see the `Parser::parse_until_before` method) so that it can over + /// consume the input past a comma at this block/function nesting level. + /// + /// Successful results are accumulated in a vector. + /// + /// This method returns `Err(())` the first time that a closure call does, + /// or if a closure call leaves some input before the next comma or the end + /// of the input. + pub fn parseCommaSeparated( + this: *Parser, + comptime T: type, + comptime parse_one: *const fn (*Parser) Result(T), + ) Result(ArrayList(T)) { + return this.parseCommaSeparatedInternal(T, {}, voidWrap(T, parse_one), false); + } + + pub fn parseCommaSeparatedWithCtx( + this: *Parser, + comptime T: type, + closure: anytype, + comptime parse_one: *const fn (@TypeOf(closure), *Parser) Result(T), + ) Result(ArrayList(T)) { + return this.parseCommaSeparatedInternal(T, closure, parse_one, false); + } + + fn parseCommaSeparatedInternal( + this: *Parser, + comptime T: type, + closure: anytype, + comptime parse_one: *const fn (@TypeOf(closure), *Parser) Result(T), + ignore_errors: bool, + ) Result(ArrayList(T)) { + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the somewhat common case of only one item we don't + // way overallocate. Note that we always push at least one item if + // parsing succeeds. + // + // TODO(zack): might be faster to use stack fallback here + // in the common case we may have just 1, but I feel like it is also very common to have >1 + // which means every time we have >1 items we will always incur 1 more additional allocation + var sfb = std.heap.stackFallback(@sizeOf(T), this.allocator()); + const alloc = sfb.get(); + var values = ArrayList(T).initCapacity(alloc, 1) catch unreachable; + + while (true) { + this.skipWhitespace(); // Unnecessary for correctness, but may help try() in parse_one rewind less. + switch (this.parseUntilBefore(Delimiters{ .comma = true }, T, closure, parse_one)) { + .result => |v| { + values.append(alloc, v) catch unreachable; + }, + .err => |e| { + if (!ignore_errors) return .{ .err = e }; + }, + } + + const tok = switch (this.next()) { + .result => |v| v, + .err => { + // need to clone off the stack + const needs_clone = values.items.len == 1; + if (needs_clone) return .{ .result = values.clone(this.allocator()) catch bun.outOfMemory() }; + return .{ .result = values }; + }, + }; + if (tok.* != .comma) bun.unreachablePanic("", .{}); + } + } + + /// Execute the given closure, passing it the parser. + /// If the result (returned unchanged) is `Err`, + /// the internal state of the parser (including position within the input) + /// is restored to what it was before the call. + /// + /// func needs to be a funtion like this: `fn func(*Parser, ...@TypeOf(args_)) T` + pub inline fn tryParse(this: *Parser, comptime func: anytype, args_: anytype) bun.meta.ReturnOf(func) { + const start = this.state(); + const result = result: { + const args = brk: { + var args: std.meta.ArgsTuple(@TypeOf(func)) = undefined; + args[0] = this; + + inline for (args_, 1..) |a, i| { + args[i] = a; + } + + break :brk args; + }; + + break :result @call(.auto, func, args); + }; + if (result == .err) { + this.reset(&start); + } + return result; + } + + pub inline fn tryParseImpl(this: *Parser, comptime Ret: type, comptime func: anytype, args: anytype) Ret { + const start = this.state(); + const result = result: { + break :result @call(.auto, func, args); + }; + if (result == .err) { + this.reset(&start); + } + return result; + } + + pub inline fn parseNestedBlock(this: *Parser, comptime T: type, closure: anytype, comptime parsefn: *const fn (@TypeOf(closure), *Parser) Result(T)) Result(T) { + return parse_nested_block(this, T, closure, parsefn); + } + + pub fn isExhausted(this: *Parser) bool { + return this.expectExhausted().isOk(); + } + + /// Parse the input until exhaustion and check that it contains no “error” token. + /// + /// See `Token::is_parse_error`. This also checks nested blocks and functions recursively. + pub fn expectNoErrorToken(this: *Parser) Result(void) { + while (true) { + const tok = switch (this.nextIncludingWhitespaceAndComments()) { + .err => return .{ .result = {} }, + .result => |v| v, + }; + switch (tok.*) { + .function, .open_paren, .open_square, .open_curly => { + if (this.parseNestedBlock(void, {}, struct { + pub fn parse(_: void, i: *Parser) Result(void) { + if (i.expectNoErrorToken().asErr()) |e| { + return .{ .err = e }; + } + return .{ .result = {} }; + } + }.parse).asErr()) |err| { + return .{ .err = err }; + } + return .{ .result = {} }; + }, + else => { + if (tok.isParseError()) { + return .{ .err = this.newUnexpectedTokenError(tok.*) }; + } + }, + } + } + } + + pub fn expectPercentage(this: *Parser) Result(f32) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (tok.* == .percentage) return .{ .result = tok.percentage.unit_value }; + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn expectComma(this: *Parser) Result(void) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .comma => return .{ .result = {} }, + else => {}, + } + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + /// Parse a that does not have a fractional part, and return the integer value. + pub fn expectInteger(this: *Parser) Result(i32) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (tok.* == .number and tok.number.int_value != null) return .{ .result = tok.number.int_value.? }; + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + /// Parse a and return the integer value. + pub fn expectNumber(this: *Parser) Result(f32) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (tok.* == .number) return .{ .result = tok.number.value }; + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn expectDelim(this: *Parser, delim: u8) Result(void) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (tok.* == .delim and tok.delim == delim) return .{ .result = {} }; + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn expectParenthesisBlock(this: *Parser) Result(void) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (tok.* == .open_paren) return .{ .result = {} }; + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn expectColon(this: *Parser) Result(void) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (tok.* == .colon) return .{ .result = {} }; + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn expectString(this: *Parser) Result([]const u8) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (tok.* == .quoted_string) return .{ .result = tok.quoted_string }; + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn expectIdent(this: *Parser) Result([]const u8) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (tok.* == .ident) return .{ .result = tok.ident }; + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + /// Parse either a or a , and return the unescaped value. + pub fn expectIdentOrString(this: *Parser) Result([]const u8) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .ident => |i| return .{ .result = i }, + .quoted_string => |s| return .{ .result = s }, + else => {}, + } + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn expectIdentMatching(this: *Parser, name: []const u8) Result(void) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .ident => |i| if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, i)) return .{ .result = {} }, + else => {}, + } + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn expectFunction(this: *Parser) Result([]const u8) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .function => |fn_name| return .{ .result = fn_name }, + else => {}, + } + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn expectFunctionMatching(this: *Parser, name: []const u8) Result(void) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .function => |fn_name| if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, fn_name)) return .{ .result = {} }, + else => {}, + } + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn expectCurlyBracketBlock(this: *Parser) Result(void) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .open_curly => return .{ .result = {} }, + else => return .{ .err = start_location.newUnexpectedTokenError(tok.*) }, + } + } + + /// Parse a and return the unescaped value. + pub fn expectUrl(this: *Parser) Result([]const u8) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .unquoted_url => |value| return .{ .result = value }, + .function => |name| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("url", name)) { + const result = this.parseNestedBlock([]const u8, {}, struct { + fn parse(_: void, parser: *Parser) Result([]const u8) { + return switch (parser.expectString()) { + .result => |v| .{ .result = v }, + .err => |e| .{ .err = e }, + }; + } + }.parse); + return switch (result) { + .result => |v| .{ .result = v }, + .err => |e| .{ .err = e }, + }; + } + }, + else => {}, + } + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + /// Parse either a or a , and return the unescaped value. + pub fn expectUrlOrString(this: *Parser) Result([]const u8) { + const start_location = this.currentSourceLocation(); + const tok = switch (this.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .unquoted_url => |value| return .{ .result = value }, + .quoted_string => |value| return .{ .result = value }, + .function => |name| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("url", name)) { + const result = this.parseNestedBlock([]const u8, {}, struct { + fn parse(_: void, parser: *Parser) Result([]const u8) { + return switch (parser.expectString()) { + .result => |v| .{ .result = v }, + .err => |e| .{ .err = e }, + }; + } + }.parse); + return switch (result) { + .result => |v| .{ .result = v }, + .err => |e| .{ .err = e }, + }; + } + }, + else => {}, + } + return .{ .err = start_location.newUnexpectedTokenError(tok.*) }; + } + + pub fn position(this: *Parser) usize { + bun.debugAssert(bun.strings.isOnCharBoundary(this.input.tokenizer.src, this.input.tokenizer.position)); + return this.input.tokenizer.position; + } + + fn parseEmpty(_: *Parser) Result(void) { + return .{ .result = {} }; + } + + /// Like `parse_until_before`, but also consume the delimiter token. + /// + /// This can be useful when you don’t need to know which delimiter it was + /// (e.g. if these is only one in the given set) + /// or if it was there at all (as opposed to reaching the end of the input). + pub fn parseUntilAfter( + this: *Parser, + delimiters: Delimiters, + comptime T: type, + closure: anytype, + comptime parse_fn: *const fn (@TypeOf(closure), *Parser) Result(T), + ) Result(T) { + return parse_until_after( + this, + delimiters, + ParseUntilErrorBehavior.consume, + T, + closure, + parse_fn, + ); + } + + pub fn parseUntilBefore(this: *Parser, delimiters: Delimiters, comptime T: type, closure: anytype, comptime parse_fn: *const fn (@TypeOf(closure), *Parser) Result(T)) Result(T) { + return parse_until_before(this, delimiters, .consume, T, closure, parse_fn); + } + + pub fn parseEntirely(this: *Parser, comptime T: type, closure: anytype, comptime parsefn: *const fn (@TypeOf(closure), *Parser) Result(T)) Result(T) { + const result = switch (parsefn(closure, this)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (this.expectExhausted().asErr()) |e| return .{ .err = e }; + return .{ .result = result }; + } + + /// Check whether the input is exhausted. That is, if `.next()` would return a token. + /// Return a `Result` so that the `?` operator can be used: `input.expect_exhausted()?` + /// + /// This ignores whitespace and comments. + pub fn expectExhausted(this: *Parser) Result(void) { + const start = this.state(); + const result: Result(void) = switch (this.next()) { + .result => |t| .{ .err = start.sourceLocation().newUnexpectedTokenError(t.*) }, + .err => |e| brk: { + if (e.kind == .basic and e.kind.basic == .end_of_input) break :brk .{ .result = {} }; + bun.unreachablePanic("Unexpected error encountered: {}", .{e.kind}); + }, + }; + this.reset(&start); + return result; + } + + pub fn @"skip cdc and cdo"(this: *@This()) void { + if (this.at_start_of) |block_type| { + this.at_start_of = null; + consume_until_end_of_block(block_type, &this.input.tokenizer); + } + + this.input.tokenizer.@"skip cdc and cdo"(); + } + + pub fn skipWhitespace(this: *@This()) void { + if (this.at_start_of) |block_type| { + this.at_start_of = null; + consume_until_end_of_block(block_type, &this.input.tokenizer); + } + + this.input.tokenizer.skipWhitespace(); + } + + pub fn next(this: *@This()) Result(*Token) { + this.skipWhitespace(); + return this.nextIncludingWhitespaceAndComments(); + } + + /// Same as `Parser::next`, but does not skip whitespace tokens. + pub fn nextIncludingWhitespace(this: *@This()) Result(*Token) { + while (true) { + switch (this.nextIncludingWhitespaceAndComments()) { + .result => |tok| if (tok.* == .comment) {} else break, + .err => |e| return .{ .err = e }, + } + } + return .{ .result = &this.input.cached_token.?.token }; + } + + pub fn nextByte(this: *@This()) ?u8 { + const byte = this.input.tokenizer.nextByte(); + if (this.stop_before.contains(Delimiters.fromByte(byte))) { + return null; + } + return byte; + } + + pub fn reset(this: *Parser, state_: *const ParserState) void { + this.input.tokenizer.reset(state_); + this.at_start_of = state_.at_start_of; + } + + pub fn state(this: *Parser) ParserState { + return ParserState{ + .position = this.input.tokenizer.getPosition(), + .current_line_start_position = this.input.tokenizer.current_line_start_position, + .current_line_number = @intCast(this.input.tokenizer.current_line_number), + .at_start_of = this.at_start_of, + }; + } + + /// Same as `Parser::next`, but does not skip whitespace or comment tokens. + /// + /// **Note**: This should only be used in contexts like a CSS pre-processor + /// where comments are preserved. + /// When parsing higher-level values, per the CSS Syntax specification, + /// comments should always be ignored between tokens. + pub fn nextIncludingWhitespaceAndComments(this: *Parser) Result(*Token) { + if (this.at_start_of) |block_type| { + this.at_start_of = null; + consume_until_end_of_block(block_type, &this.input.tokenizer); + } + + const byte = this.input.tokenizer.nextByte(); + if (this.stop_before.contains(Delimiters.fromByte(byte))) { + return .{ .err = this.newError(BasicParseErrorKind.end_of_input) }; + } + + const token_start_position = this.input.tokenizer.getPosition(); + const using_cached_token = this.input.cached_token != null and this.input.cached_token.?.start_position == token_start_position; + + const token = if (using_cached_token) token: { + const cached_token = &this.input.cached_token.?; + this.input.tokenizer.reset(&cached_token.end_state); + if (cached_token.token == .function) { + this.input.tokenizer.seeFunction(cached_token.token.function); + } + break :token &cached_token.token; + } else token: { + const new_token = switch (this.input.tokenizer.next()) { + .result => |v| v, + .err => return .{ .err = this.newError(BasicParseErrorKind.end_of_input) }, + }; + this.input.cached_token = CachedToken{ + .token = new_token, + .start_position = token_start_position, + .end_state = this.input.tokenizer.state(), + }; + break :token &this.input.cached_token.?.token; + }; + + if (BlockType.opening(token)) |block_type| { + this.at_start_of = block_type; + } + + return .{ .result = token }; + } + + /// Create a new unexpected token or EOF ParseError at the current location + pub fn newErrorForNextToken(this: *Parser) ParseError(ParserError) { + const token = switch (this.next()) { + .result => |t| t.*, + .err => |e| return e, + }; + return this.newError(BasicParseErrorKind{ .unexpected_token = token }); + } +}; + +/// A set of characters, to be used with the `Parser::parse_until*` methods. +/// +/// The union of two sets can be obtained with the `|` operator. Example: +/// +/// ```{rust,ignore} +/// input.parse_until_before(Delimiter::CurlyBracketBlock | Delimiter::Semicolon) +/// ``` +pub const Delimiters = packed struct(u8) { + /// The delimiter set with only the `{` opening curly bracket + curly_bracket: bool = false, + /// The delimiter set with only the `;` semicolon + semicolon: bool = false, + /// The delimiter set with only the `!` exclamation point + bang: bool = false, + /// The delimiter set with only the `,` comma + comma: bool = false, + close_curly_bracket: bool = false, + close_square_bracket: bool = false, + close_parenthesis: bool = false, + __unused: u1 = 0, + + pub usingnamespace Bitflags(Delimiters); + + const NONE: Delimiters = .{}; + + pub fn getDelimiter(comptime tag: @TypeOf(.EnumLiteral)) Delimiters { + var empty = Delimiters{}; + @field(empty, @tagName(tag)) = true; + return empty; + } + + const TABLE: [256]Delimiters = brk: { + var table: [256]Delimiters = [_]Delimiters{.{}} ** 256; + table[';'] = getDelimiter(.semicolon); + table['!'] = getDelimiter(.bang); + table[','] = getDelimiter(.comma); + table['{'] = getDelimiter(.curly_bracket); + table['}'] = getDelimiter(.close_curly_bracket); + table[']'] = getDelimiter(.close_square_bracket); + table[')'] = getDelimiter(.close_parenthesis); + break :brk table; + }; + + // pub fn bitwiseOr(lhs: Delimiters, rhs: Delimiters) Delimiters { + // return @bitCast(@as(u8, @bitCast(lhs)) | @as(u8, @bitCast(rhs))); + // } + + // pub fn contains(lhs: Delimiters, rhs: Delimiters) bool { + // return @as(u8, @bitCast(lhs)) & @as(u8, @bitCast(rhs)) != 0; + // } + + pub fn fromByte(byte: ?u8) Delimiters { + if (byte) |b| return TABLE[b]; + return .{}; + } +}; + +pub const ParserInput = struct { + tokenizer: Tokenizer, + cached_token: ?CachedToken = null, + + pub fn new(allocator: Allocator, code: []const u8) ParserInput { + return ParserInput{ + .tokenizer = Tokenizer.init(allocator, code), + }; + } +}; + +/// A capture of the internal state of a `Parser` (including the position within the input), +/// obtained from the `Parser::position` method. +/// +/// Can be used with the `Parser::reset` method to restore that state. +/// Should only be used with the `Parser` instance it came from. +pub const ParserState = struct { + position: usize, + current_line_start_position: usize, + current_line_number: u32, + at_start_of: ?BlockType, + + pub fn sourceLocation(this: *const ParserState) SourceLocation { + return .{ + .line = this.current_line_number, + .column = @intCast(this.position - this.current_line_start_position + 1), + }; + } +}; + +const BlockType = enum { + parenthesis, + square_bracket, + curly_bracket, + + fn opening(token: *const Token) ?BlockType { + return switch (token.*) { + .function, .open_paren => .parenthesis, + .open_square => .square_bracket, + .open_curly => .curly_bracket, + else => null, + }; + } + + fn closing(token: *const Token) ?BlockType { + return switch (token.*) { + .close_paren => .parenthesis, + .close_square => .square_bracket, + .close_curly => .curly_bracket, + else => null, + }; + } +}; + +pub const nth = struct { + const NthResult = struct { i32, i32 }; + /// Parse the *An+B* notation, as found in the `:nth-child()` selector. + /// The input is typically the arguments of a function, + /// in which case the caller needs to check if the arguments’ parser is exhausted. + /// Return `Ok((A, B))`, or `Err(())` for a syntax error. + pub fn parse_nth(input: *Parser) Result(NthResult) { + const tok = switch (input.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .number => { + if (tok.number.int_value) |b| return .{ .result = .{ 0, b } }; + }, + .dimension => { + if (tok.dimension.num.int_value) |a| { + // @compileError(todo_stuff.match_ignore_ascii_case); + const unit = tok.dimension.unit; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "n")) { + return parse_b(input, a); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "n-")) { + return parse_signless_b(input, a, -1); + } else { + if (parse_n_dash_digits(input.allocator(), unit).asValue()) |b| { + return .{ .result = .{ a, b } }; + } else { + return .{ .err = input.newUnexpectedTokenError(.{ .ident = unit }) }; + } + } + } + }, + .ident => { + const value = tok.ident; + // @compileError(todo_stuff.match_ignore_ascii_case); + if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "even")) { + return .{ .result = .{ 2, 0 } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "odd")) { + return .{ .result = .{ 2, 1 } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "n")) { + return parse_b(input, 1); + } else if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "-n")) { + return parse_b(input, -1); + } else if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "n-")) { + return parse_signless_b(input, 1, -1); + } else if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(value, "-n-")) { + return parse_signless_b(input, -1, -1); + } else { + const slice, const a: i32 = if (bun.strings.startsWithChar(value, '-')) .{ value[1..], -1 } else .{ value, 1 }; + if (parse_n_dash_digits(input.allocator(), slice).asValue()) |b| return .{ .result = .{ a, b } }; + return .{ .err = input.newUnexpectedTokenError(.{ .ident = value }) }; + } + }, + .delim => { + const next_tok = switch (input.nextIncludingWhitespace()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (next_tok.* == .ident) { + const value = next_tok.ident; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(value, "n")) { + return parse_b(input, 1); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(value, "-n")) { + return parse_signless_b(input, 1, -1); + } else { + if (parse_n_dash_digits(input.allocator(), value).asValue()) |b| { + return .{ .result = .{ 1, b } }; + } else { + return .{ .err = input.newUnexpectedTokenError(.{ .ident = value }) }; + } + } + } else { + return .{ .err = input.newUnexpectedTokenError(next_tok.*) }; + } + }, + else => {}, + } + return .{ .err = input.newUnexpectedTokenError(tok.*) }; + } + + fn parse_b(input: *Parser, a: i32) Result(NthResult) { + const start = input.state(); + const tok = switch (input.next()) { + .result => |v| v, + .err => { + input.reset(&start); + return .{ .result = .{ a, 0 } }; + }, + }; + + if (tok.* == .delim and tok.delim == '+') return parse_signless_b(input, a, 1); + if (tok.* == .delim and tok.delim == '-') return parse_signless_b(input, a, -1); + if (tok.* == .number and tok.number.has_sign and tok.number.int_value != null) return parse_signless_b(input, a, tok.number.int_value.?); + input.reset(&start); + return .{ .result = .{ a, 0 } }; + } + + fn parse_signless_b(input: *Parser, a: i32, b_sign: i32) Result(NthResult) { + const tok = switch (input.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (tok.* == .number and !tok.number.has_sign and tok.number.int_value != null) { + const b = tok.number.int_value.?; + return .{ .result = .{ a, b_sign * b } }; + } + return .{ .err = input.newUnexpectedTokenError(tok.*) }; + } + + fn parse_n_dash_digits(allocator: Allocator, str: []const u8) Maybe(i32, void) { + const bytes = str; + if (bytes.len >= 3 and + bun.strings.eqlCaseInsensitiveASCIIICheckLength(bytes[0..2], "n-") and + brk: { + for (bytes[2..]) |b| { + if (b < '0' or b > '9') break :brk false; + } + break :brk true; + }) { + return parse_number_saturate(allocator, str[1..]); // Include the minus sign + } else { + return .{ .err = {} }; + } + } + + fn parse_number_saturate(allocator: Allocator, string: []const u8) Maybe(i32, void) { + var input = ParserInput.new(allocator, string); + var parser = Parser.new(&input); + const tok = switch (parser.nextIncludingWhitespaceAndComments()) { + .result => |v| v, + .err => { + return .{ .err = {} }; + }, + }; + const int = if (tok.* == .number and tok.number.int_value != null) tok.number.int_value.? else { + return .{ .err = {} }; + }; + if (!parser.isExhausted()) { + return .{ .err = {} }; + } + return .{ .result = int }; + } +}; + +const CachedToken = struct { + token: Token, + start_position: usize, + end_state: ParserState, +}; + +const Tokenizer = struct { + src: []const u8, + position: usize = 0, + source_map_url: ?[]const u8 = null, + current_line_start_position: usize = 0, + current_line_number: u32 = 0, + allocator: Allocator, + var_or_env_functions: SeenStatus = .dont_care, + current: Token = undefined, + previous: Token = undefined, + + const SeenStatus = enum { + dont_care, + looking_for_them, + seen_at_least_one, + }; + + const FORM_FEED_BYTE = 0x0C; + const REPLACEMENT_CHAR = 0xFFFD; + const REPLACEMENT_CHAR_UNICODE: [3]u8 = [3]u8{ 0xEF, 0xBF, 0xBD }; + const MAX_ONE_B: u32 = 0x80; + const MAX_TWO_B: u32 = 0x800; + const MAX_THREE_B: u32 = 0x10000; + + pub fn init(allocator: Allocator, src: []const u8) Tokenizer { + var lexer = Tokenizer{ + .src = src, + .allocator = allocator, + .position = 0, + }; + + // make current point to the first token + _ = lexer.next(); + lexer.position = 0; + + return lexer; + } + + pub fn currentSourceMapUrl(this: *const Tokenizer) ?[]const u8 { + return this.source_map_url; + } + + pub fn getPosition(this: *const Tokenizer) usize { + bun.debugAssert(bun.strings.isOnCharBoundary(this.src, this.position)); + return this.position; + } + + pub fn state(this: *const Tokenizer) ParserState { + return ParserState{ + .position = this.position, + .current_line_start_position = this.current_line_start_position, + .current_line_number = this.current_line_number, + .at_start_of = null, + }; + } + + pub fn skipWhitespace(this: *Tokenizer) void { + while (!this.isEof()) { + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + ' ', '\t' => this.advance(1), + '\n', 0x0C, '\r' => this.consumeNewline(), + '/' => { + if (this.startsWith("/*")) { + _ = this.consumeComment(); + } else return; + }, + else => return, + } + } + } + + pub fn currentSourceLocation(this: *const Tokenizer) SourceLocation { + return SourceLocation{ + .line = this.current_line_number, + .column = @intCast(this.position - this.current_line_start_position + 1), + }; + } + + pub fn prev(this: *Tokenizer) Token { + bun.assert(this.position > 0); + return this.previous; + } + + pub inline fn isEof(this: *Tokenizer) bool { + return this.position >= this.src.len; + } + + pub fn seeFunction(this: *Tokenizer, name: []const u8) void { + if (this.var_or_env_functions == .looking_for_them) { + if (std.ascii.eqlIgnoreCase(name, "var") and std.ascii.eqlIgnoreCase(name, "env")) { + this.var_or_env_functions = .seen_at_least_one; + } + } + } + + /// TODO: fix this, remove the additional shit I added + /// return error if it is eof + pub inline fn next(this: *Tokenizer) Maybe(Token, void) { + return this.nextImpl(); + } + + pub fn nextImpl(this: *Tokenizer) Maybe(Token, void) { + if (this.isEof()) return .{ .err = {} }; + + // todo_stuff.match_byte; + const b = this.byteAt(0); + const token: Token = switch (b) { + ' ', '\t' => this.consumeWhitespace(false), + '\n', FORM_FEED_BYTE, '\r' => this.consumeWhitespace(true), + '"' => this.consumeString(false), + '#' => brk: { + this.advance(1); + if (this.isIdentStart()) break :brk .{ .idhash = this.consumeName() }; + if (!this.isEof() and switch (this.nextByteUnchecked()) { + // Any other valid case here already resulted in IDHash. + '0'...'9', '-' => true, + else => false, + }) break :brk .{ .hash = this.consumeName() }; + break :brk .{ .delim = '#' }; + }, + '$' => brk: { + if (this.startsWith("$=")) { + this.advance(2); + break :brk .suffix_match; + } + this.advance(1); + break :brk .{ .delim = '$' }; + }, + '\'' => this.consumeString(true), + '(' => brk: { + this.advance(1); + break :brk .open_paren; + }, + ')' => brk: { + this.advance(1); + break :brk .close_paren; + }, + '*' => brk: { + if (this.startsWith("*=")) { + this.advance(2); + break :brk .substring_match; + } + this.advance(1); + break :brk .{ .delim = '*' }; + }, + '+' => brk: { + if ((this.hasAtLeast(1) and switch (this.byteAt(1)) { + '0'...'9' => true, + else => false, + }) or (this.hasAtLeast(2) and + this.byteAt(1) == '.' and switch (this.byteAt(2)) { + '0'...'9' => true, + else => false, + })) { + break :brk this.consumeNumeric(); + } + + this.advance(1); + break :brk .{ .delim = '+' }; + }, + ',' => brk: { + this.advance(1); + break :brk .comma; + }, + '-' => brk: { + if ((this.hasAtLeast(1) and switch (this.byteAt(1)) { + '0'...'9' => true, + else => false, + }) or (this.hasAtLeast(2) and this.byteAt(1) == '.' and switch (this.byteAt(2)) { + '0'...'9' => true, + else => false, + })) break :brk this.consumeNumeric(); + + if (this.startsWith("-->")) { + this.advance(3); + break :brk .cdc; + } + + if (this.isIdentStart()) break :brk this.consumeIdentLike(); + + this.advance(1); + break :brk .{ .delim = '-' }; + }, + '.' => brk: { + if (this.hasAtLeast(1) and switch (this.byteAt(1)) { + '0'...'9' => true, + else => false, + }) { + break :brk this.consumeNumeric(); + } + this.advance(1); + break :brk .{ .delim = '.' }; + }, + '/' => brk: { + if (this.startsWith("/*")) break :brk .{ .comment = this.consumeComment() }; + this.advance(1); + break :brk .{ .delim = '/' }; + }, + '0'...'9' => this.consumeNumeric(), + ':' => brk: { + this.advance(1); + break :brk .colon; + }, + ';' => brk: { + this.advance(1); + break :brk .semicolon; + }, + '<' => brk: { + if (this.startsWith("")) this.advance(3) else return, + else => return, + } + } + } + + pub fn consumeNumeric(this: *Tokenizer) Token { + // Parse [+-]?\d*(\.\d+)?([eE][+-]?\d+)? + // But this is always called so that there is at least one digit in \d*(\.\d+)? + + // Do all the math in f64 so that large numbers overflow to +/-inf + // and i32::{MIN, MAX} are within range. + const has_sign: bool, const sign: f64 = brk: { + switch (this.nextByteUnchecked()) { + '-' => break :brk .{ true, -1.0 }, + '+' => break :brk .{ true, 1.0 }, + else => break :brk .{ false, 1.0 }, + } + }; + + if (has_sign) this.advance(1); + + var integral_part: f64 = 0.0; + while (byteToDecimalDigit(this.nextByteUnchecked())) |digit| { + integral_part = integral_part * 10.0 + @as(f64, @floatFromInt(digit)); + this.advance(1); + if (this.isEof()) break; + } + + var is_integer = true; + + var fractional_part: f64 = 0.0; + if (this.hasAtLeast(1) and this.nextByteUnchecked() == '.' and switch (this.byteAt(1)) { + '0'...'9' => true, + else => false, + }) { + is_integer = false; + this.advance(1); // Consume '.' + var factor: f64 = 0.1; + while (byteToDecimalDigit(this.nextByteUnchecked())) |digit| { + fractional_part += @as(f64, @floatFromInt(digit)) * factor; + factor *= 0.1; + this.advance(1); + if (this.isEof()) break; + } + } + + var value: f64 = sign * (integral_part + fractional_part); + + if (this.hasAtLeast(1) and switch (this.nextByteUnchecked()) { + 'e', 'E' => true, + else => false, + }) { + if (switch (this.byteAt(1)) { + '0'...'9' => true, + else => false, + } or (this.hasAtLeast(2) and switch (this.byteAt(1)) { + '+', '-' => true, + else => false, + } and switch (this.byteAt(2)) { + '0'...'9' => true, + else => false, + })) { + is_integer = false; + this.advance(1); + const has_sign2: bool, const sign2: f64 = brk: { + switch (this.nextByteUnchecked()) { + '-' => break :brk .{ true, -1.0 }, + '+' => break :brk .{ true, 1.0 }, + else => break :brk .{ false, 1.0 }, + } + }; + + if (has_sign2) this.advance(1); + + var exponent: f64 = 0.0; + while (byteToDecimalDigit(this.nextByteUnchecked())) |digit| { + exponent = exponent * 10.0 + @as(f64, @floatFromInt(digit)); + this.advance(1); + if (this.isEof()) break; + } + value *= std.math.pow(f64, 10, sign2 * exponent); + } + } + + const int_value: ?i32 = brk: { + const i32_max = comptime std.math.maxInt(i32); + const i32_min = comptime std.math.minInt(i32); + if (is_integer) { + if (value >= @as(f64, @floatFromInt(i32_max))) { + break :brk i32_max; + } else if (value <= @as(f64, @floatFromInt(i32_min))) { + break :brk i32_min; + } else { + break :brk @intFromFloat(value); + } + } + + break :brk null; + }; + + if (!this.isEof() and this.nextByteUnchecked() == '%') { + this.advance(1); + return .{ .percentage = .{ .unit_value = @floatCast(value / 100), .int_value = int_value, .has_sign = has_sign } }; + } + + if (this.isIdentStart()) { + const unit = this.consumeName(); + return .{ + .dimension = .{ + .num = .{ .value = @floatCast(value), .int_value = int_value, .has_sign = has_sign }, + .unit = unit, + }, + }; + } + + return .{ + .number = .{ .value = @floatCast(value), .int_value = int_value, .has_sign = has_sign }, + }; + } + + pub fn consumeWhitespace(this: *Tokenizer, comptime newline: bool) Token { + const start_position = this.position; + if (newline) { + this.consumeNewline(); + } else { + this.advance(1); + } + + while (!this.isEof()) { + // todo_stuff.match_byte + const b = this.nextByteUnchecked(); + switch (b) { + ' ', '\t' => this.advance(1), + '\n', FORM_FEED_BYTE, '\r' => this.consumeNewline(), + else => break, + } + } + + return .{ .whitespace = this.sliceFrom(start_position) }; + } + + pub fn consumeString(this: *Tokenizer, comptime single_quote: bool) Token { + const quoted_string = this.consumeQuotedString(single_quote); + if (quoted_string.bad) return .{ .bad_string = quoted_string.str }; + return .{ .quoted_string = quoted_string.str }; + } + + pub fn consumeIdentLike(this: *Tokenizer) Token { + const value = this.consumeName(); + if (!this.isEof() and this.nextByteUnchecked() == '(') { + this.advance(1); + if (std.ascii.eqlIgnoreCase(value, "url")) return if (this.consumeUnquotedUrl()) |tok| return tok else .{ .function = value }; + this.seeFunction(value); + return .{ .function = value }; + } + return .{ .ident = value }; + } + + pub fn consumeName(this: *Tokenizer) []const u8 { + const start_pos = this.position; + var value_bytes: CopyOnWriteStr = undefined; + + while (true) { + if (this.isEof()) return this.sliceFrom(start_pos); + + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => this.advance(1), + '\\', 0 => { + // * The tokenizer’s input is UTF-8 since it’s `&str`. + // * start_pos is at a code point boundary + // * so is the current position (which is before '\\' or '\0' + // + // So `value_bytes` is well-formed UTF-8. + value_bytes = .{ .borrowed = this.sliceFrom(start_pos) }; + break; + }, + 0x80...0xBF => this.consumeContinuationByte(), + // This is the range of the leading byte of a 2-3 byte character + // encoding + 0xC0...0xEF => this.advance(1), + 0xF0...0xFF => this.consume4byteIntro(), + else => return this.sliceFrom(start_pos), + } + } + + while (!this.isEof()) { + const b = this.nextByteUnchecked(); + // todo_stuff.match_byte + switch (b) { + 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => { + this.advance(1); + value_bytes.append(this.allocator, &[_]u8{b}); + }, + '\\' => { + if (this.hasNewlineAt(1)) break; + this.advance(1); + this.consumeEscapeAndWrite(&value_bytes); + }, + 0 => { + this.advance(1); + value_bytes.append(this.allocator, REPLACEMENT_CHAR_UNICODE[0..]); + }, + 0x80...0xBF => { + // This byte *is* part of a multi-byte code point, + // we’ll end up copying the whole code point before this loop does something else. + this.consumeContinuationByte(); + value_bytes.append(this.allocator, &[_]u8{b}); + }, + 0xC0...0xEF => { + // This byte *is* part of a multi-byte code point, + // we’ll end up copying the whole code point before this loop does something else. + this.advance(1); + value_bytes.append(this.allocator, &[_]u8{b}); + }, + 0xF0...0xFF => { + this.consume4byteIntro(); + value_bytes.append(this.allocator, &[_]u8{b}); + }, + else => { + // ASCII + break; + }, + } + } + + return value_bytes.toSlice(); + } + + pub fn consumeQuotedString(this: *Tokenizer, comptime single_quote: bool) struct { str: []const u8, bad: bool = false } { + this.advance(1); // Skip the initial quote + const start_pos = this.position; + var string_bytes: CopyOnWriteStr = undefined; + + while (true) { + if (this.isEof()) return .{ .str = this.sliceFrom(start_pos) }; + + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + '"' => { + if (!single_quote) { + const value = this.sliceFrom(start_pos); + this.advance(1); + return .{ .str = value }; + } + this.advance(1); + }, + '\'' => { + if (single_quote) { + const value = this.sliceFrom(start_pos); + this.advance(1); + return .{ .str = value }; + } + this.advance(1); + }, + // The CSS spec says NULL bytes ('\0') should be turned into replacement characters: 0xFFFD + '\\', 0 => { + // * The tokenizer’s input is UTF-8 since it’s `&str`. + // * start_pos is at a code point boundary + // * so is the current position (which is before '\\' or '\0' + // + // So `string_bytes` is well-formed UTF-8. + string_bytes = .{ .borrowed = this.sliceFrom(start_pos) }; + break; + }, + '\n', '\r', FORM_FEED_BYTE => return .{ .str = this.sliceFrom(start_pos), .bad = true }, + 0x80...0xBF => this.consumeContinuationByte(), + 0xF0...0xFF => this.consume4byteIntro(), + else => { + this.advance(1); + }, + } + } + + while (!this.isEof()) { + const b = this.nextByteUnchecked(); + // todo_stuff.match_byte + switch (b) { + // string_bytes is well-formed UTF-8, see other comments + '\n', '\r', FORM_FEED_BYTE => return .{ .str = string_bytes.toSlice(), .bad = true }, + '"' => { + this.advance(1); + if (!single_quote) break; + }, + '\'' => { + this.advance(1); + if (single_quote) break; + }, + '\\' => { + this.advance(1); + if (!this.isEof()) { + switch (this.nextByteUnchecked()) { + // Escaped newline + '\n', FORM_FEED_BYTE, '\r' => this.consumeNewline(), + else => this.consumeEscapeAndWrite(&string_bytes), + } + } + // else: escaped EOF, do nothing. + // continue; + }, + 0 => { + this.advance(1); + string_bytes.append(this.allocator, REPLACEMENT_CHAR_UNICODE[0..]); + continue; + }, + 0x80...0xBF => this.consumeContinuationByte(), + 0xF0...0xFF => this.consume4byteIntro(), + else => { + this.advance(1); + }, + } + + string_bytes.append(this.allocator, &[_]u8{b}); + } + + return .{ .str = string_bytes.toSlice() }; + } + + pub fn consumeUnquotedUrl(this: *Tokenizer) ?Token { + // This is only called after "url(", so the current position is a code point boundary. + const start_position = this.position; + const from_start = this.src[this.position..]; + var newlines: u32 = 0; + var last_newline: usize = 0; + var found_printable_char = false; + + var offset: usize = 0; + var b: u8 = undefined; + while (true) { + defer offset += 1; + + if (offset < from_start.len) { + b = from_start[offset]; + } else { + this.position = this.src.len; + break; + } + + // todo_stuff.match_byte + switch (b) { + ' ', '\t' => {}, + '\n', FORM_FEED_BYTE => { + newlines += 1; + last_newline = offset; + }, + '\r' => { + if (offset + 1 < from_start.len and from_start[offset + 1] != '\n') { + newlines += 1; + last_newline = offset; + } + }, + '"', '\'' => return null, // Do not advance + ')' => { + // Don't use advance, because we may be skipping + // newlines here, and we want to avoid the assert. + this.position += offset + 1; + break; + }, + else => { + // Don't use advance, because we may be skipping + // newlines here, and we want to avoid the assert. + this.position += offset; + found_printable_char = true; + break; + }, + } + } + + if (newlines > 0) { + this.current_line_number += newlines; + // No need for wrapping_add here, because there's no possible + // way to wrap. + this.current_line_start_position = start_position + last_newline + 1; + } + + if (found_printable_char) { + // This function only consumed ASCII (whitespace) bytes, + // so the current position is a code point boundary. + return this.consumeUnquotedUrlInternal(); + } + return .{ .unquoted_url = "" }; + } + + pub fn consumeUnquotedUrlInternal(this: *Tokenizer) Token { + // This function is only called with start_pos at a code point boundary.; + const start_pos = this.position; + var string_bytes: CopyOnWriteStr = undefined; + + while (true) { + if (this.isEof()) return .{ .unquoted_url = this.sliceFrom(start_pos) }; + + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + ' ', '\t', '\n', '\r', FORM_FEED_BYTE => { + var value = .{ .borrowed = this.sliceFrom(start_pos) }; + return this.consumeUrlEnd(start_pos, &value); + }, + ')' => { + const value = this.sliceFrom(start_pos); + this.advance(1); + return .{ .unquoted_url = value }; + }, + // non-printable + 0x01...0x08, + 0x0B, + 0x0E...0x1F, + 0x7F, + + // not valid in this context + '"', + '\'', + '(', + => { + this.advance(1); + return this.consumeBadUrl(start_pos); + }, + '\\', 0 => { + // * The tokenizer’s input is UTF-8 since it’s `&str`. + // * start_pos is at a code point boundary + // * so is the current position (which is before '\\' or '\0' + // + // So `string_bytes` is well-formed UTF-8. + string_bytes = .{ .borrowed = this.sliceFrom(start_pos) }; + break; + }, + 0x80...0xBF => this.consumeContinuationByte(), + 0xF0...0xFF => this.consume4byteIntro(), + else => { + // ASCII or other leading byte. + this.advance(1); + }, + } + } + + while (!this.isEof()) { + const b = this.nextByteUnchecked(); + // todo_stuff.match_byte + switch (b) { + ' ', '\t', '\n', '\r', FORM_FEED_BYTE => { + // string_bytes is well-formed UTF-8, see other comments. + // const string = string_bytes.toSlice(); + // return this.consumeUrlEnd(start_pos, &string); + return this.consumeUrlEnd(start_pos, &string_bytes); + }, + ')' => { + this.advance(1); + break; + }, + // non-printable + 0x01...0x08, + 0x0B, + 0x0E...0x1F, + 0x7F, + + // invalid in this context + '"', + '\'', + '(', + => { + this.advance(1); + return this.consumeBadUrl(start_pos); + }, + '\\' => { + this.advance(1); + if (this.hasNewlineAt(0)) return this.consumeBadUrl(start_pos); + + // This pushes one well-formed code point to string_bytes + this.consumeEscapeAndWrite(&string_bytes); + }, + 0 => { + this.advance(1); + string_bytes.append(this.allocator, REPLACEMENT_CHAR_UNICODE[0..]); + }, + 0x80...0xBF => { + // We’ll end up copying the whole code point + // before this loop does something else. + this.consumeContinuationByte(); + string_bytes.append(this.allocator, &[_]u8{b}); + }, + 0xF0...0xFF => { + // We’ll end up copying the whole code point + // before this loop does something else. + this.consume4byteIntro(); + string_bytes.append(this.allocator, &[_]u8{b}); + }, + // If this byte is part of a multi-byte code point, + // we’ll end up copying the whole code point before this loop does something else. + else => { + // ASCII or other leading byte. + this.advance(1); + string_bytes.append(this.allocator, &[_]u8{b}); + }, + } + } + + // string_bytes is well-formed UTF-8, see other comments. + return .{ .unquoted_url = string_bytes.toSlice() }; + } + + pub fn consumeUrlEnd(this: *Tokenizer, start_pos: usize, string: *CopyOnWriteStr) Token { + while (!this.isEof()) { + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + ')' => { + this.advance(1); + break; + }, + ' ', '\t' => this.advance(1), + '\n', FORM_FEED_BYTE, '\r' => this.consumeNewline(), + else => |b| { + this.consumeKnownByte(b); + return this.consumeBadUrl(start_pos); + }, + } + } + + return .{ .unquoted_url = string.toSlice() }; + } + + pub fn consumeBadUrl(this: *Tokenizer, start_pos: usize) Token { + // Consume up to the closing ) + while (!this.isEof()) { + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + ')' => { + const contents = this.sliceFrom(start_pos); + this.advance(1); + return .{ .bad_url = contents }; + }, + '\\' => { + this.advance(1); + if (this.nextByte()) |b| { + if (b == ')' or b == '\\') this.advance(1); // Skip an escaped ')' or '\' + } + }, + '\n', FORM_FEED_BYTE, '\r' => this.consumeNewline(), + else => |b| this.consumeKnownByte(b), + } + } + return .{ .bad_url = this.sliceFrom(start_pos) }; + } + + pub fn consumeEscapeAndWrite(this: *Tokenizer, bytes: *CopyOnWriteStr) void { + const val = this.consumeEscape(); + var utf8bytes: [4]u8 = undefined; + const len = std.unicode.utf8Encode(@truncate(val), utf8bytes[0..]) catch @panic("Invalid"); + bytes.append(this.allocator, utf8bytes[0..len]); + } + + pub fn consumeEscape(this: *Tokenizer) u32 { + if (this.isEof()) return 0xFFFD; // Unicode replacement character + + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + '0'...'9', 'A'...'F', 'a'...'f' => { + const c = this.consumeHexDigits().value; + if (!this.isEof()) { + // todo_stuff.match_byte + switch (this.nextByteUnchecked()) { + ' ', '\t' => this.advance(1), + '\n', FORM_FEED_BYTE, '\r' => this.consumeNewline(), + else => {}, + } + } + + if (c != 0 and std.unicode.utf8ValidCodepoint(@truncate(c))) return c; + return REPLACEMENT_CHAR; + }, + 0 => { + this.advance(1); + return REPLACEMENT_CHAR; + }, + else => return this.consumeChar(), + } + } + + pub fn consumeHexDigits(this: *Tokenizer) struct { value: u32, num_digits: u32 } { + var value: u32 = 0; + var digits: u32 = 0; + while (digits < 6 and !this.isEof()) { + if (byteToHexDigit(this.nextByteUnchecked())) |digit| { + value = value * 16 + digit; + digits += 1; + this.advance(1); + } else break; + } + + return .{ .value = value, .num_digits = digits }; + } + + pub fn consumeChar(this: *Tokenizer) u32 { + const c = this.nextChar(); + const len_utf8 = lenUtf8(c); + this.position += len_utf8; + // Note that due to the special case for the 4-byte sequence + // intro, we must use wrapping add here. + this.current_line_start_position +%= len_utf8 - lenUtf16(c); + return c; + } + + fn lenUtf8(code: u32) usize { + if (code < MAX_ONE_B) { + return 1; + } else if (code < MAX_TWO_B) { + return 2; + } else if (code < MAX_THREE_B) { + return 3; + } else { + return 4; + } + } + + fn lenUtf16(ch: u32) usize { + if ((ch & 0xFFFF) == ch) { + return 1; + } else { + return 2; + } + } + + fn byteToHexDigit(b: u8) ?u32 { + + // todo_stuff.match_byte + return switch (b) { + '0'...'9' => b - '0', + 'a'...'f' => b - 'a' + 10, + 'A'...'F' => b - 'A' + 10, + else => null, + }; + } + + fn byteToDecimalDigit(b: u8) ?u32 { + if (b >= '0' and b <= '9') { + return b - '0'; + } + return null; + } + + pub fn consumeComment(this: *Tokenizer) []const u8 { + this.advance(2); + const start_position = this.position; + while (!this.isEof()) { + const b = this.nextByteUnchecked(); + // todo_stuff.match_byte + switch (b) { + '*' => { + const end_position = this.position; + this.advance(1); + if (this.nextByte() == '/') { + this.advance(1); + const contents = this.src[start_position..end_position]; + this.checkForSourceMap(contents); + return contents; + } + }, + '\n', FORM_FEED_BYTE, '\r' => { + this.consumeNewline(); + }, + 0x80...0xBF => this.consumeContinuationByte(), + 0xF0...0xFF => this.consume4byteIntro(), + else => { + // ASCII or other leading byte + this.advance(1); + }, + } + } + const contents = this.sliceFrom(start_position); + this.checkForSourceMap(contents); + return contents; + } + + pub fn checkForSourceMap(this: *Tokenizer, contents: []const u8) void { + { + const directive = "# sourceMappingURL="; + const directive_old = "@ sourceMappingURL="; + if (std.mem.startsWith(u8, contents, directive) or std.mem.startsWith(u8, contents, directive_old)) { + this.source_map_url = splitSourceMap(contents[directive.len..]); + } + } + + { + const directive = "# sourceURL="; + const directive_old = "@ sourceURL="; + if (std.mem.startsWith(u8, contents, directive) or std.mem.startsWith(u8, contents, directive_old)) { + this.source_map_url = splitSourceMap(contents[directive.len..]); + } + } + } + + pub fn splitSourceMap(contents: []const u8) ?[]const u8 { + // FIXME: Use bun CodepointIterator + var iter = std.unicode.Utf8Iterator{ .bytes = contents, .i = 0 }; + while (iter.nextCodepoint()) |c| { + switch (c) { + ' ', '\t', FORM_FEED_BYTE, '\r', '\n' => { + const start = 0; + const end = iter.i; + return contents[start..end]; + }, + else => {}, + } + } + return null; + } + + pub fn consumeNewline(this: *Tokenizer) void { + const byte = this.nextByteUnchecked(); + if (bun.Environment.allow_assert) { + std.debug.assert(byte == '\r' or byte == '\n' or byte == FORM_FEED_BYTE); + } + this.position += 1; + if (byte == '\r' and this.nextByte() == '\n') { + this.position += 1; + } + this.current_line_start_position = this.position; + this.current_line_number += 1; + } + + /// Advance over a single byte; the byte must be a UTF-8 + /// continuation byte. + /// + /// Binary Hex Comments + /// 0xxxxxxx 0x00..0x7F Only byte of a 1-byte character encoding + /// 110xxxxx 0xC0..0xDF First byte of a 2-byte character encoding + /// 1110xxxx 0xE0..0xEF First byte of a 3-byte character encoding + /// 11110xxx 0xF0..0xF7 First byte of a 4-byte character encoding + /// 10xxxxxx 0x80..0xBF Continuation byte: one of 1-3 bytes following the first <-- + pub fn consumeContinuationByte(this: *Tokenizer) void { + if (bun.Environment.allow_assert) std.debug.assert(this.nextByteUnchecked() & 0xC0 == 0x80); + // Continuation bytes contribute to column overcount. Note + // that due to the special case for the 4-byte sequence intro, + // we must use wrapping add here. + this.current_line_start_position +%= 1; + this.position += 1; + } + + /// Advance over a single byte; the byte must be a UTF-8 sequence + /// leader for a 4-byte sequence. + /// + /// Binary Hex Comments + /// 0xxxxxxx 0x00..0x7F Only byte of a 1-byte character encoding + /// 110xxxxx 0xC0..0xDF First byte of a 2-byte character encoding + /// 1110xxxx 0xE0..0xEF First byte of a 3-byte character encoding + /// 11110xxx 0xF0..0xF7 First byte of a 4-byte character encoding <-- + /// 10xxxxxx 0x80..0xBF Continuation byte: one of 1-3 bytes following the first + pub fn consume4byteIntro(this: *Tokenizer) void { + if (bun.Environment.allow_assert) std.debug.assert(this.nextByteUnchecked() & 0xF0 == 0xF0); + // This takes two UTF-16 characters to represent, so we + // actually have an undercount. + // this.current_line_start_position = self.current_line_start_position.wrapping_sub(1); + this.current_line_start_position -%= 1; + this.position += 1; + } + + pub fn isIdentStart(this: *Tokenizer) bool { + + // todo_stuff.match_byte + return !this.isEof() and switch (this.nextByteUnchecked()) { + 'a'...'z', 'A'...'Z', '_', 0 => true, + + // todo_stuff.match_byte + '-' => this.hasAtLeast(1) and switch (this.byteAt(1)) { + 'a'...'z', 'A'...'Z', '-', '_', 0 => true, + '\\' => !this.hasNewlineAt(1), + else => |b| !std.ascii.isASCII(b), + }, + '\\' => !this.hasNewlineAt(1), + else => |b| !std.ascii.isASCII(b), + }; + } + + /// If true, the input has at least `n` bytes left *after* the current one. + /// That is, `tokenizer.char_at(n)` will not panic. + fn hasAtLeast(this: *Tokenizer, n: usize) bool { + return this.position + n < this.src.len; + } + + fn hasNewlineAt(this: *Tokenizer, offset: usize) bool { + return this.position + offset < this.src.len and switch (this.byteAt(offset)) { + '\n', '\r', FORM_FEED_BYTE => true, + else => false, + }; + } + + pub fn startsWith(this: *Tokenizer, comptime needle: []const u8) bool { + return std.mem.eql(u8, this.src[this.position .. this.position + needle.len], needle); + } + + /// Advance over N bytes in the input. This function can advance + /// over ASCII bytes (excluding newlines), or UTF-8 sequence + /// leaders (excluding leaders for 4-byte sequences). + pub fn advance(this: *Tokenizer, n: usize) void { + if (bun.Environment.allow_assert) { + // Each byte must either be an ASCII byte or a sequence + // leader, but not a 4-byte leader; also newlines are + // rejected. + for (0..n) |i| { + const b = this.byteAt(i); + std.debug.assert(std.ascii.isASCII(b) or (b & 0xF0 != 0xF0 and b & 0xC0 != 0x80)); + std.debug.assert(b != '\r' and b != '\n' and b != '\x0C'); + } + } + this.position += n; + } + + /// Advance over any kind of byte, excluding newlines. + pub fn consumeKnownByte(this: *Tokenizer, byte: u8) void { + if (bun.Environment.allow_assert) std.debug.assert(byte != '\r' and byte != '\n' and byte != FORM_FEED_BYTE); + this.position += 1; + // Continuation bytes contribute to column overcount. + if (byte & 0xF0 == 0xF0) { + // This takes two UTF-16 characters to represent, so we + // actually have an undercount. + this.current_line_start_position -%= 1; + } else if (byte & 0xC0 == 0x80) { + // Note that due to the special case for the 4-byte + // sequence intro, we must use wrapping add here. + this.current_line_start_position +%= 1; + } + } + + pub inline fn byteAt(this: *Tokenizer, n: usize) u8 { + return this.src[this.position + n]; + } + + pub inline fn nextByte(this: *Tokenizer) ?u8 { + if (this.isEof()) return null; + return this.src[this.position]; + } + + pub inline fn nextChar(this: *Tokenizer) u32 { + const len = bun.strings.utf8ByteSequenceLength(this.src[this.position]); + return bun.strings.decodeWTF8RuneT(this.src[this.position..].ptr[0..4], len, u32, bun.strings.unicode_replacement); + } + + pub inline fn nextByteUnchecked(this: *Tokenizer) u8 { + return this.src[this.position]; + } + + pub inline fn sliceFrom(this: *Tokenizer, start: usize) []const u8 { + return this.src[start..this.position]; + } +}; + +const TokenKind = enum { + /// An [](https://drafts.csswg.org/css-syntax/#typedef-ident-token) + ident, + + /// Value is the ident + function, + + /// Value is the ident + at_keyword, + + /// A [``](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "unrestricted" + /// + /// The value does not include the `#` marker. + hash, + + /// A [``](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "id" + /// + /// The value does not include the `#` marker. + idhash, + + quoted_string, + + bad_string, + + /// `url()` is represented by a `.function` token + unquoted_url, + + bad_url, + + /// Value of a single codepoint + delim, + + /// A can be fractional or an integer, and can contain an optional + or - sign + number, + + percentage, + + dimension, + + whitespace, + + /// `` + cdc, + + /// `~=` (https://www.w3.org/TR/selectors-4/#attribute-representation) + include_match, + + /// `|=` (https://www.w3.org/TR/selectors-4/#attribute-representation) + dash_match, + + /// `^=` (https://www.w3.org/TR/selectors-4/#attribute-substrings) + prefix_match, + + /// `$=`(https://www.w3.org/TR/selectors-4/#attribute-substrings) + suffix_match, + + /// `*=` (https://www.w3.org/TR/selectors-4/#attribute-substrings) + substring_match, + + colon, + semicolon, + comma, + open_square, + close_square, + open_paren, + close_paren, + open_curly, + close_curly, + + /// Not an actual token in the spec, but we keep it anyway + comment, + + pub fn toString(this: TokenKind) []const u8 { + return switch (this) { + .at_keyword => "@-keyword", + .bad_string => "bad string token", + .bad_url => "bad URL token", + .cdc => "\"-->\"", + .cdo => "\"` + cdc, + + /// `~=` (https://www.w3.org/TR/selectors-4/#attribute-representation) + include_match, + + /// `|=` (https://www.w3.org/TR/selectors-4/#attribute-representation) + dash_match, + + /// `^=` (https://www.w3.org/TR/selectors-4/#attribute-substrings) + prefix_match, + + /// `$=`(https://www.w3.org/TR/selectors-4/#attribute-substrings) + suffix_match, + + /// `*=` (https://www.w3.org/TR/selectors-4/#attribute-substrings) + substring_match, + + colon, + semicolon, + comma, + open_square, + close_square, + open_paren, + close_paren, + open_curly, + close_curly, + + /// Not an actual token in the spec, but we keep it anyway + comment: []const u8, + + /// Return whether this token represents a parse error. + /// + /// `BadUrl` and `BadString` are tokenizer-level parse errors. + /// + /// `CloseParenthesis`, `CloseSquareBracket`, and `CloseCurlyBracket` are *unmatched* + /// and therefore parse errors when returned by one of the `Parser::next*` methods. + pub fn isParseError(this: *const Token) bool { + return switch (this.*) { + .bad_url, .bad_string, .close_paren, .close_square, .close_curly => true, + else => false, + }; + } + + pub fn raw(this: Token) []const u8 { + return switch (this) { + .ident => this.ident, + // .function => + }; + } + + pub inline fn kind(this: Token) TokenKind { + return @as(TokenKind, this); + } + + pub inline fn kindString(this: Token) []const u8 { + return this.kind.toString(); + } + + // ~toCssImpl + const This = @This(); + + pub fn toCssGeneric(this: *const This, writer: anytype) !void { + return switch (this.*) { + .ident => { + try serializer.serializeIdentifier(this.ident, writer); + }, + .at_keyword => { + try writer.writeAll("@"); + try serializer.serializeIdentifier(this.at_keyword, writer); + }, + .hash => { + try writer.writeAll("#"); + try serializer.serializeName(this.hash, writer); + }, + .idhash => { + try writer.writeAll("#"); + try serializer.serializeName(this.idhash, writer); + }, + .quoted_string => |x| { + try serializer.serializeName(x, writer); + }, + .unquoted_url => |x| { + try writer.writeAll("url("); + try serializer.serializeUnquotedUrl(x, writer); + try writer.writeAll(")"); + }, + .delim => |x| { + bun.assert(x <= 0x7F); + try writer.writeByte(@intCast(x)); + }, + .number => |n| { + try serializer.writeNumeric(n.value, n.int_value, n.has_sign, writer); + }, + .percentage => |p| { + try serializer.writeNumeric(p.unit_value * 100.0, p.int_value, p.has_sign, writer); + }, + .dimension => |d| { + try serializer.writeNumeric(d.num.value, d.num.int_value, d.num.has_sign, writer); + // Disambiguate with scientific notation. + const unit = d.unit; + // TODO(emilio): This doesn't handle e.g. 100E1m, which gets us + // an unit of "E1m"... + if ((unit.len == 1 and unit[0] == 'e') or + (unit.len == 1 and unit[0] == 'E') or + bun.strings.startsWith(unit, "e-") or + bun.strings.startsWith(unit, "E-")) + { + try writer.writeAll("\\65 "); + try serializer.serializeName(unit[1..], writer); + } else { + try serializer.serializeIdentifier(unit, writer); + } + }, + .whitespace => |content| { + try writer.writeAll(content); + }, + .comment => |content| { + try writer.writeAll("/*"); + try writer.writeAll(content); + try writer.writeAll("*/"); + }, + .colon => try writer.writeAll(":"), + .semicolon => try writer.writeAll(";"), + .comma => try writer.writeAll(","), + .include_match => try writer.writeAll("~="), + .dash_match => try writer.writeAll("|="), + .prefix_match => try writer.writeAll("^="), + .suffix_match => try writer.writeAll("$="), + .substring_match => try writer.writeAll("*="), + .cdo => try writer.writeAll(""), + + .function => |name| { + try serializer.serializeIdentifier(name, writer); + try writer.writeAll("("); + }, + .open_paren => try writer.writeAll("("), + .open_square => try writer.writeAll("["), + .open_curly => try writer.writeAll("{"), + + .bad_url => |contents| { + try writer.writeAll("url("); + try writer.writeAll(contents); + try writer.writeByte(')'); + }, + .bad_string => |value| { + // During tokenization, an unescaped newline after a quote causes + // the token to be a BadString instead of a QuotedString. + // The BadString token ends just before the newline + // (which is in a separate WhiteSpace token), + // and therefore does not have a closing quote. + try writer.writeByte('"'); + var string_writer = serializer.CssStringWriter(@TypeOf(writer)).new(writer); + try string_writer.writeStr(value); + }, + .close_paren => try writer.writeAll(")"), + .close_square => try writer.writeAll("]"), + .close_curly => try writer.writeAll("}"), + }; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // zack is here: verify this is correct + return switch (this.*) { + .ident => |value| serializer.serializeIdentifier(value, dest) catch return dest.addFmtError(), + .at_keyword => |value| { + try dest.writeStr("@"); + return serializer.serializeIdentifier(value, dest) catch return dest.addFmtError(); + }, + .hash => |value| { + try dest.writeStr("#"); + return serializer.serializeName(value, dest) catch return dest.addFmtError(); + }, + .idhash => |value| { + try dest.writeStr("#"); + return serializer.serializeIdentifier(value, dest) catch return dest.addFmtError(); + }, + .quoted_string => |value| serializer.serializeString(value, dest) catch return dest.addFmtError(), + .unquoted_url => |value| { + try dest.writeStr("url("); + serializer.serializeUnquotedUrl(value, dest) catch return dest.addFmtError(); + return dest.writeStr(")"); + }, + .delim => |value| { + // See comment for this variant in declaration of Token + // The value of delim is only ever ascii + bun.debugAssert(value <= 0x7F); + return dest.writeChar(@truncate(value)); + }, + .number => |num| serializer.writeNumeric(num.value, num.int_value, num.has_sign, dest) catch return dest.addFmtError(), + .percentage => |num| { + serializer.writeNumeric(num.unit_value * 100, num.int_value, num.has_sign, dest) catch return dest.addFmtError(); + return dest.writeStr("%"); + }, + .dimension => |dim| { + serializer.writeNumeric(dim.num.value, dim.num.int_value, dim.num.has_sign, dest) catch return dest.addFmtError(); + // Disambiguate with scientific notation. + const unit = dim.unit; + if (std.mem.eql(u8, unit, "e") or std.mem.eql(u8, unit, "E") or + std.mem.startsWith(u8, unit, "e-") or std.mem.startsWith(u8, unit, "E-")) + { + try dest.writeStr("\\65 "); + serializer.serializeName(unit[1..], dest) catch return dest.addFmtError(); + } else { + serializer.serializeIdentifier(unit, dest) catch return dest.addFmtError(); + } + return; + }, + .whitespace => |content| dest.writeStr(content), + .comment => |content| { + try dest.writeStr("/*"); + try dest.writeStr(content); + return dest.writeStr("*/"); + }, + .colon => dest.writeStr(":"), + .semicolon => dest.writeStr(";"), + .comma => dest.writeStr(","), + .include_match => dest.writeStr("~="), + .dash_match => dest.writeStr("|="), + .prefix_match => dest.writeStr("^="), + .suffix_match => dest.writeStr("$="), + .substring_match => dest.writeStr("*="), + .cdo => dest.writeStr(""), + .function => |name| { + serializer.serializeIdentifier(name, dest) catch return dest.addFmtError(); + return dest.writeStr("("); + }, + .open_paren => dest.writeStr("("), + .open_square => dest.writeStr("["), + .open_curly => dest.writeStr("{"), + .bad_url => |contents| { + try dest.writeStr("url("); + try dest.writeStr(contents); + return dest.writeChar(')'); + }, + .bad_string => |value| { + try dest.writeChar('"'); + var writer = serializer.CssStringWriter(*Printer(W)).new(dest); + return writer.writeStr(value) catch return dest.addFmtError(); + }, + .close_paren => dest.writeStr(")"), + .close_square => dest.writeStr("]"), + .close_curly => dest.writeStr("}"), + }; + } +}; + +const Num = struct { + has_sign: bool, + value: f32, + int_value: ?i32, +}; + +const Dimension = struct { + num: Num, + /// e.g. "px" + unit: []const u8, +}; + +const CopyOnWriteStr = union(enum) { + borrowed: []const u8, + owned: std.ArrayList(u8), + + pub fn append(this: *@This(), allocator: Allocator, slice: []const u8) void { + switch (this.*) { + .borrowed => { + var list = std.ArrayList(u8).initCapacity(allocator, this.borrowed.len + slice.len) catch bun.outOfMemory(); + list.appendSliceAssumeCapacity(this.borrowed); + list.appendSliceAssumeCapacity(slice); + this.* = .{ .owned = list }; + }, + .owned => { + this.owned.appendSlice(slice) catch bun.outOfMemory(); + }, + } + } + + pub fn toSlice(this: *@This()) []const u8 { + return switch (this.*) { + .borrowed => this.borrowed, + .owned => this.owned.items[0..], + }; + } +}; + +pub const color = struct { + /// The opaque alpha value of 1.0. + pub const OPAQUE: f32 = 1.0; + + const ColorError = error{ + parse, + }; + + /// Either an angle or a number. + pub const AngleOrNumber = union(enum) { + /// ``. + number: struct { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `` + angle: struct { + /// The value as a number of degrees. + degrees: f32, + }, + }; + + const RGB = struct { u8, u8, u8 }; + pub const named_colors = bun.ComptimeStringMap(RGB, .{ + .{ "aliceblue", .{ 240, 248, 255 } }, + .{ "antiquewhite", .{ 250, 235, 215 } }, + .{ "aqua", .{ 0, 255, 255 } }, + .{ "aquamarine", .{ 127, 255, 212 } }, + .{ "azure", .{ 240, 255, 255 } }, + .{ "beige", .{ 245, 245, 220 } }, + .{ "bisque", .{ 255, 228, 196 } }, + .{ "black", .{ 0, 0, 0 } }, + .{ "blanchedalmond", .{ 255, 235, 205 } }, + .{ "blue", .{ 0, 0, 255 } }, + .{ "blueviolet", .{ 138, 43, 226 } }, + .{ "brown", .{ 165, 42, 42 } }, + .{ "burlywood", .{ 222, 184, 135 } }, + .{ "cadetblue", .{ 95, 158, 160 } }, + .{ "chartreuse", .{ 127, 255, 0 } }, + .{ "chocolate", .{ 210, 105, 30 } }, + .{ "coral", .{ 255, 127, 80 } }, + .{ "cornflowerblue", .{ 100, 149, 237 } }, + .{ "cornsilk", .{ 255, 248, 220 } }, + .{ "crimson", .{ 220, 20, 60 } }, + .{ "cyan", .{ 0, 255, 255 } }, + .{ "darkblue", .{ 0, 0, 139 } }, + .{ "darkcyan", .{ 0, 139, 139 } }, + .{ "darkgoldenrod", .{ 184, 134, 11 } }, + .{ "darkgray", .{ 169, 169, 169 } }, + .{ "darkgreen", .{ 0, 100, 0 } }, + .{ "darkgrey", .{ 169, 169, 169 } }, + .{ "darkkhaki", .{ 189, 183, 107 } }, + .{ "darkmagenta", .{ 139, 0, 139 } }, + .{ "darkolivegreen", .{ 85, 107, 47 } }, + .{ "darkorange", .{ 255, 140, 0 } }, + .{ "darkorchid", .{ 153, 50, 204 } }, + .{ "darkred", .{ 139, 0, 0 } }, + .{ "darksalmon", .{ 233, 150, 122 } }, + .{ "darkseagreen", .{ 143, 188, 143 } }, + .{ "darkslateblue", .{ 72, 61, 139 } }, + .{ "darkslategray", .{ 47, 79, 79 } }, + .{ "darkslategrey", .{ 47, 79, 79 } }, + .{ "darkturquoise", .{ 0, 206, 209 } }, + .{ "darkviolet", .{ 148, 0, 211 } }, + .{ "deeppink", .{ 255, 20, 147 } }, + .{ "deepskyblue", .{ 0, 191, 255 } }, + .{ "dimgray", .{ 105, 105, 105 } }, + .{ "dimgrey", .{ 105, 105, 105 } }, + .{ "dodgerblue", .{ 30, 144, 255 } }, + .{ "firebrick", .{ 178, 34, 34 } }, + .{ "floralwhite", .{ 255, 250, 240 } }, + .{ "forestgreen", .{ 34, 139, 34 } }, + .{ "fuchsia", .{ 255, 0, 255 } }, + .{ "gainsboro", .{ 220, 220, 220 } }, + .{ "ghostwhite", .{ 248, 248, 255 } }, + .{ "gold", .{ 255, 215, 0 } }, + .{ "goldenrod", .{ 218, 165, 32 } }, + .{ "gray", .{ 128, 128, 128 } }, + .{ "green", .{ 0, 128, 0 } }, + .{ "greenyellow", .{ 173, 255, 47 } }, + .{ "grey", .{ 128, 128, 128 } }, + .{ "honeydew", .{ 240, 255, 240 } }, + .{ "hotpink", .{ 255, 105, 180 } }, + .{ "indianred", .{ 205, 92, 92 } }, + .{ "indigo", .{ 75, 0, 130 } }, + .{ "ivory", .{ 255, 255, 240 } }, + .{ "khaki", .{ 240, 230, 140 } }, + .{ "lavender", .{ 230, 230, 250 } }, + .{ "lavenderblush", .{ 255, 240, 245 } }, + .{ "lawngreen", .{ 124, 252, 0 } }, + .{ "lemonchiffon", .{ 255, 250, 205 } }, + .{ "lightblue", .{ 173, 216, 230 } }, + .{ "lightcoral", .{ 240, 128, 128 } }, + .{ "lightcyan", .{ 224, 255, 255 } }, + .{ "lightgoldenrodyellow", .{ 250, 250, 210 } }, + .{ "lightgray", .{ 211, 211, 211 } }, + .{ "lightgreen", .{ 144, 238, 144 } }, + .{ "lightgrey", .{ 211, 211, 211 } }, + .{ "lightpink", .{ 255, 182, 193 } }, + .{ "lightsalmon", .{ 255, 160, 122 } }, + .{ "lightseagreen", .{ 32, 178, 170 } }, + .{ "lightskyblue", .{ 135, 206, 250 } }, + .{ "lightslategray", .{ 119, 136, 153 } }, + .{ "lightslategrey", .{ 119, 136, 153 } }, + .{ "lightsteelblue", .{ 176, 196, 222 } }, + .{ "lightyellow", .{ 255, 255, 224 } }, + .{ "lime", .{ 0, 255, 0 } }, + .{ "limegreen", .{ 50, 205, 50 } }, + .{ "linen", .{ 250, 240, 230 } }, + .{ "magenta", .{ 255, 0, 255 } }, + .{ "maroon", .{ 128, 0, 0 } }, + .{ "mediumaquamarine", .{ 102, 205, 170 } }, + .{ "mediumblue", .{ 0, 0, 205 } }, + .{ "mediumorchid", .{ 186, 85, 211 } }, + .{ "mediumpurple", .{ 147, 112, 219 } }, + .{ "mediumseagreen", .{ 60, 179, 113 } }, + .{ "mediumslateblue", .{ 123, 104, 238 } }, + .{ "mediumspringgreen", .{ 0, 250, 154 } }, + .{ "mediumturquoise", .{ 72, 209, 204 } }, + .{ "mediumvioletred", .{ 199, 21, 133 } }, + .{ "midnightblue", .{ 25, 25, 112 } }, + .{ "mintcream", .{ 245, 255, 250 } }, + .{ "mistyrose", .{ 255, 228, 225 } }, + .{ "moccasin", .{ 255, 228, 181 } }, + .{ "navajowhite", .{ 255, 222, 173 } }, + .{ "navy", .{ 0, 0, 128 } }, + .{ "oldlace", .{ 253, 245, 230 } }, + .{ "olive", .{ 128, 128, 0 } }, + .{ "olivedrab", .{ 107, 142, 35 } }, + .{ "orange", .{ 255, 165, 0 } }, + .{ "orangered", .{ 255, 69, 0 } }, + .{ "orchid", .{ 218, 112, 214 } }, + .{ "palegoldenrod", .{ 238, 232, 170 } }, + .{ "palegreen", .{ 152, 251, 152 } }, + .{ "paleturquoise", .{ 175, 238, 238 } }, + .{ "palevioletred", .{ 219, 112, 147 } }, + .{ "papayawhip", .{ 255, 239, 213 } }, + .{ "peachpuff", .{ 255, 218, 185 } }, + .{ "peru", .{ 205, 133, 63 } }, + .{ "pink", .{ 255, 192, 203 } }, + .{ "plum", .{ 221, 160, 221 } }, + .{ "powderblue", .{ 176, 224, 230 } }, + .{ "purple", .{ 128, 0, 128 } }, + .{ "rebeccapurple", .{ 102, 51, 153 } }, + .{ "red", .{ 255, 0, 0 } }, + .{ "rosybrown", .{ 188, 143, 143 } }, + .{ "royalblue", .{ 65, 105, 225 } }, + .{ "saddlebrown", .{ 139, 69, 19 } }, + .{ "salmon", .{ 250, 128, 114 } }, + .{ "sandybrown", .{ 244, 164, 96 } }, + .{ "seagreen", .{ 46, 139, 87 } }, + .{ "seashell", .{ 255, 245, 238 } }, + .{ "sienna", .{ 160, 82, 45 } }, + .{ "silver", .{ 192, 192, 192 } }, + .{ "skyblue", .{ 135, 206, 235 } }, + .{ "slateblue", .{ 106, 90, 205 } }, + .{ "slategray", .{ 112, 128, 144 } }, + .{ "slategrey", .{ 112, 128, 144 } }, + .{ "snow", .{ 255, 250, 250 } }, + .{ "springgreen", .{ 0, 255, 127 } }, + .{ "steelblue", .{ 70, 130, 180 } }, + .{ "tan", .{ 210, 180, 140 } }, + .{ "teal", .{ 0, 128, 128 } }, + .{ "thistle", .{ 216, 191, 216 } }, + .{ "tomato", .{ 255, 99, 71 } }, + .{ "turquoise", .{ 64, 224, 208 } }, + .{ "violet", .{ 238, 130, 238 } }, + .{ "wheat", .{ 245, 222, 179 } }, + .{ "white", .{ 255, 255, 255 } }, + .{ "whitesmoke", .{ 245, 245, 245 } }, + .{ "yellow", .{ 255, 255, 0 } }, + .{ "yellowgreen", .{ 154, 205, 50 } }, + }); + + /// Returns the named color with the given name. + /// + pub fn parseNamedColor(ident: []const u8) ?struct { u8, u8, u8 } { + return named_colors.get(ident); + } + + /// Parse a color hash, without the leading '#' character. + pub fn parseHashColor(value: []const u8) ?struct { u8, u8, u8, f32 } { + return parseHashColorImpl(value) catch return null; + } + + pub fn parseHashColorImpl(value: []const u8) ColorError!struct { u8, u8, u8, f32 } { + return switch (value.len) { + 8 => .{ + (try fromHex(value[0])) * 16 + (try fromHex(value[1])), + (try fromHex(value[2])) * 16 + (try fromHex(value[3])), + (try fromHex(value[4])) * 16 + (try fromHex(value[5])), + @as(f32, @floatFromInt((try fromHex(value[6])) * 16 + (try fromHex(value[7])))) / 255.0, + }, + 6 => { + const r = (try fromHex(value[0])) * 16 + (try fromHex(value[1])); + const g = (try fromHex(value[2])) * 16 + (try fromHex(value[3])); + const b = (try fromHex(value[4])) * 16 + (try fromHex(value[5])); + return .{ + r, g, b, + + OPAQUE, + }; + }, + 4 => .{ + (try fromHex(value[0])) * 17, + (try fromHex(value[1])) * 17, + (try fromHex(value[2])) * 17, + @as(f32, @floatFromInt((try fromHex(value[3])) * 17)) / 255.0, + }, + 3 => .{ + (try fromHex(value[0])) * 17, + (try fromHex(value[1])) * 17, + (try fromHex(value[2])) * 17, + OPAQUE, + }, + else => ColorError.parse, + }; + } + + pub fn fromHex(c: u8) ColorError!u8 { + return switch (c) { + '0'...'9' => c - '0', + 'a'...'f' => c - 'a' + 10, + 'A'...'F' => c - 'A' + 10, + else => ColorError.parse, + }; + } + + /// + /// except with h pre-multiplied by 3, to avoid some rounding errors. + pub fn hslToRgb(hue: f32, saturation: f32, lightness: f32) struct { f32, f32, f32 } { + bun.debugAssert(saturation >= 0.0 and saturation <= 1.0); + const Helpers = struct { + pub fn hueToRgb(m1: f32, m2: f32, _h3: f32) f32 { + var h3 = _h3; + if (h3 < 0.0) { + h3 += 3.0; + } + if (h3 > 3.0) { + h3 -= 3.0; + } + if (h3 * 2.0 < 1.0) { + return m1 + (m2 - m1) * h3 * 2.0; + } else if (h3 * 2.0 < 3.0) { + return m2; + } else if (h3 < 2.0) { + return m1 + (m2 - m1) * (2.0 - h3) * 2.0; + } else { + return m1; + } + } + }; + const m2 = if (lightness <= 0.5) + lightness * (saturation + 1.0) + else + lightness + saturation - lightness * saturation; + const m1 = lightness * 2.0 - m2; + const hue_times_3 = hue * 3.0; + const red = Helpers.hueToRgb(m1, m2, hue_times_3 + 1.0); + const green = Helpers.hueToRgb(m1, m2, hue_times_3); + const blue = Helpers.hueToRgb(m1, m2, hue_times_3 - 1.0); + return .{ red, green, blue }; + } +}; + +// pub const Bitflags + +pub const serializer = struct { + /// Write a CSS name, like a custom property name. + /// + /// You should only use this when you know what you're doing, when in doubt, + /// consider using `serialize_identifier`. + pub fn serializeName(value: []const u8, writer: anytype) !void { + var chunk_start: usize = 0; + for (value, 0..) |b, i| { + const escaped: ?[]const u8 = switch (b) { + '0'...'9', 'A'...'Z', 'a'...'z', '_', '-' => continue, + // the unicode replacement character + 0 => bun.strings.encodeUTF8Comptime(0xFFD), + else => if (!std.ascii.isASCII(b)) continue else null, + }; + + try writer.writeAll(value[chunk_start..i]); + if (escaped) |esc| { + try writer.writeAll(esc); + } else if ((b >= 0x01 and b <= 0x1F) or b == 0x7F) { + try hexEscape(b, writer); + } else { + try charEscape(b, writer); + } + chunk_start = i + 1; + } + return writer.writeAll(value[chunk_start..]); + } + + /// Write a double-quoted CSS string token, escaping content as necessary. + pub fn serializeString(value: []const u8, writer: anytype) !void { + try writer.writeAll("\""); + var string_writer = CssStringWriter(@TypeOf(writer)).new(writer); + try string_writer.writeStr(value); + return writer.writeAll("\""); + } + + pub fn serializeDimension(value: f32, unit: []const u8, comptime W: type, dest: *Printer(W)) PrintErr!void { + const int_value: ?i32 = if (fract(value) == 0.0) @intFromFloat(value) else null; + const token = Token{ .dimension = .{ + .num = .{ + .has_sign = value < 0.0, + .value = value, + .int_value = int_value, + }, + .unit = unit, + } }; + if (value != 0.0 and @abs(value) < 1.0) { + // TODO: calculate the actual number of chars here + var buf: [64]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + token.toCssGeneric(fbs.writer()) catch return dest.addFmtError(); + const s = fbs.getWritten(); + if (value < 0.0) { + try dest.writeStr("-"); + return dest.writeStr(bun.strings.trimLeadingPattern2(s, '-', '0')); + } else { + return dest.writeStr(bun.strings.trimLeadingChar(s, '0')); + } + } else { + return token.toCssGeneric(dest) catch return dest.addFmtError(); + } + } + + /// Write a CSS identifier, escaping characters as necessary. + pub fn serializeIdentifier(value: []const u8, writer: anytype) !void { + if (value.len == 0) { + return; + } + + if (bun.strings.startsWith(value, "--")) { + try writer.writeAll("--"); + return serializeName(value[2..], writer); + } else if (bun.strings.eql(value, "-")) { + return writer.writeAll("\\-"); + } else { + var slice = value; + if (slice[0] == '-') { + try writer.writeAll("-"); + slice = slice[1..]; + } + if (slice.len > 0 and slice[0] >= '0' and slice[0] <= '9') { + try hexEscape(slice[0], writer); + slice = slice[1..]; + } + return serializeName(slice, writer); + } + } + + pub fn serializeUnquotedUrl(value: []const u8, writer: anytype) !void { + var chunk_start: usize = 0; + for (value, 0..) |b, i| { + const hex = switch (b) { + 0...' ', 0x7F => true, + '(', ')', '"', '\'', '\\' => false, + else => continue, + }; + try writer.writeAll(value[chunk_start..i]); + if (hex) { + try hexEscape(b, writer); + } else { + try charEscape(b, writer); + } + chunk_start = i + 1; + } + return writer.writeAll(value[chunk_start..]); + } + + // pub fn writeNumeric(value: f32, int_value: ?i32, has_sign: bool, writer: anytype) !void { + // // `value >= 0` is true for negative 0. + // if (has_sign and !std.math.signbit(value)) { + // try writer.writeAll("+"); + // } + + // if (value == 0.0 and signfns.isSignNegative(value)) { + // // Negative zero. Work around #20596. + // try writer.writeAll("-0"); + // if (int_value == null and @mod(value, 1) == 0) { + // try writer.writeAll(".0"); + // } + // } else { + // var buf: [124]u8 = undefined; + // const bytes = bun.fmt.FormatDouble.dtoa(&buf, @floatCast(value)); + // try writer.writeAll(bytes); + // } + // } + + pub fn writeNumeric(value: f32, int_value: ?i32, has_sign: bool, writer: anytype) !void { + // `value >= 0` is true for negative 0. + if (has_sign and !std.math.signbit(value)) { + try writer.writeAll("+"); + } + + const notation: Notation = if (value == 0.0 and std.math.signbit(value)) notation: { + // Negative zero. Work around #20596. + try writer.writeAll("-0"); + break :notation Notation{ + .decimal_point = false, + .scientific = false, + }; + } else notation: { + var buf: [129]u8 = undefined; + const str, const notation = dtoa_short(&buf, value, 6); + try writer.writeAll(str); + break :notation notation; + }; + + if (int_value == null and fract(value) == 0) { + if (!notation.decimal_point and !notation.scientific) { + try writer.writeAll(".0"); + } + } + + return; + } + + pub fn hexEscape(ascii_byte: u8, writer: anytype) !void { + const HEX_DIGITS = "0123456789abcdef"; + var bytes: [4]u8 = undefined; + const slice: []const u8 = if (ascii_byte > 0x0F) slice: { + const high: usize = @intCast(ascii_byte >> 4); + const low: usize = @intCast(ascii_byte & 0x0F); + bytes[0] = '\\'; + bytes[1] = HEX_DIGITS[high]; + bytes[2] = HEX_DIGITS[low]; + bytes[3] = ' '; + break :slice bytes[0..4]; + } else slice: { + bytes[0] = '\\'; + bytes[1] = HEX_DIGITS[ascii_byte]; + bytes[2] = ' '; + break :slice bytes[0..3]; + }; + return writer.writeAll(slice); + } + + pub fn charEscape(ascii_byte: u8, writer: anytype) !void { + const bytes = [_]u8{ '\\', ascii_byte }; + return writer.writeAll(&bytes); + } + + pub fn CssStringWriter(comptime W: type) type { + return struct { + inner: W, + + /// Wrap a text writer to create a `CssStringWriter`. + pub fn new(inner: W) @This() { + return .{ .inner = inner }; + } + + pub fn writeStr(this: *@This(), str: []const u8) !void { + var chunk_start: usize = 0; + for (str, 0..) |b, i| { + const escaped = switch (b) { + '"' => "\\\"", + '\\' => "\\\\", + // replacement character + 0 => bun.strings.encodeUTF8Comptime(0xFFD), + 0x01...0x1F, 0x7F => null, + else => continue, + }; + try this.inner.writeAll(str[chunk_start..i]); + if (escaped) |e| { + try this.inner.writeAll(e); + } else { + try serializer.hexEscape(b, this.inner); + } + chunk_start = i + 1; + } + return this.inner.writeAll(str[chunk_start..]); + } + }; + } +}; + +pub const generic = struct { + pub inline fn parseWithOptions(comptime T: type, input: *Parser, options: *const ParserOptions) Result(T) { + if (@hasDecl(T, "parseWithOptions")) return T.parseWithOptions(input, options); + return switch (T) { + f32 => CSSNumberFns.parse(input), + CSSInteger => CSSIntegerFns.parse(input), + CustomIdent => CustomIdentFns.parse(input), + DashedIdent => DashedIdentFns.parse(input), + Ident => IdentFns.parse(input), + else => T.parse(input), + }; + } + + pub inline fn parse(comptime T: type, input: *Parser) Result(T) { + return switch (T) { + f32 => CSSNumberFns.parse(input), + CSSInteger => CSSIntegerFns.parse(input), + CustomIdent => CustomIdentFns.parse(input), + DashedIdent => DashedIdentFns.parse(input), + Ident => IdentFns.parse(input), + else => T.parse(input), + }; + } + + pub inline fn parseFor(comptime T: type) @TypeOf(struct { + fn parsefn(input: *Parser) Result(T) { + return generic.parse(T, input); + } + }.parsefn) { + return struct { + fn parsefn(input: *Parser) Result(T) { + return generic.parse(T, input); + } + }.parsefn; + } + + pub inline fn toCss(comptime T: type, this: *const T, comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (T) { + f32 => CSSNumberFns.toCss(this, W, dest), + CSSInteger => CSSIntegerFns.toCss(this, W, dest), + CustomIdent => CustomIdentFns.toCss(this, W, dest), + DashedIdent => DashedIdentFns.toCss(this, W, dest), + Ident => IdentFns.toCss(this, W, dest), + else => T.toCss(this, W, dest), + }; + } + + pub fn eqlList(comptime T: type, lhs: *const ArrayList(T), rhs: *const ArrayList(T)) bool { + if (lhs.items.len != rhs.items.len) return false; + for (lhs.items, 0..) |*item, i| { + if (!eql(T, item, &rhs.items[i])) return false; + } + return true; + } + + pub inline fn eql(comptime T: type, lhs: *const T, rhs: *const T) bool { + return switch (T) { + f32 => lhs.* == rhs.*, + CSSInteger => lhs.* == rhs.*, + CustomIdent, DashedIdent, Ident => bun.strings.eql(lhs.*, rhs.*), + else => T.eql(lhs, rhs), + }; + } + + const Angle = css_values.angle.Angle; + pub inline fn tryFromAngle(comptime T: type, angle: Angle) ?T { + return switch (T) { + CSSNumber => CSSNumberFns.tryFromAngle(angle), + Angle => return Angle.tryFromAngle(angle), + else => T.tryFromAngle(angle), + }; + } + + pub inline fn trySign(comptime T: type, val: *const T) ?f32 { + return switch (T) { + CSSNumber => CSSNumberFns.sign(val), + else => { + if (@hasDecl(T, "sign")) return T.sign(val); + return T.trySign(val); + }, + }; + } + + pub inline fn tryMap( + comptime T: type, + val: *const T, + comptime map_fn: *const fn (a: f32) f32, + ) ?T { + return switch (T) { + CSSNumber => map_fn(val.*), + else => { + if (@hasDecl(T, "map")) return T.map(val, map_fn); + return T.tryMap(val, map_fn); + }, + }; + } + + pub inline fn tryOpTo( + comptime T: type, + comptime R: type, + lhs: *const T, + rhs: *const T, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) R, + ) ?R { + return switch (T) { + CSSNumber => op_fn(ctx, lhs.*, rhs.*), + else => { + if (@hasDecl(T, "opTo")) return T.opTo(lhs, rhs, R, ctx, op_fn); + return T.tryOpTo(lhs, rhs, R, ctx, op_fn); + }, + }; + } + + pub inline fn tryOp( + comptime T: type, + lhs: *const T, + rhs: *const T, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) f32, + ) ?T { + return switch (T) { + Angle => Angle.tryOp(lhs, rhs, ctx, op_fn), + CSSNumber => op_fn(ctx, lhs.*, rhs.*), + else => { + if (@hasDecl(T, "op")) return T.op(lhs, rhs, ctx, op_fn); + return T.tryOp(lhs, rhs, ctx, op_fn); + }, + }; + } + + pub inline fn partialCmp(comptime T: type, lhs: *const T, rhs: *const T) ?std.math.Order { + return switch (T) { + f32 => partialCmpF32(lhs, rhs), + CSSInteger => std.math.order(lhs.*, rhs.*), + css_values.angle.Angle => css_values.angle.Angle.partialCmp(lhs, rhs), + else => T.partialCmp(lhs, rhs), + }; + } + + pub inline fn partialCmpF32(lhs: *const f32, rhs: *const f32) ?std.math.Order { + const lte = lhs.* <= rhs.*; + const rte = lhs.* >= rhs.*; + if (!lte and !rte) return null; + if (!lte and rte) return .gt; + if (lte and !rte) return .lt; + return .eq; + } +}; + +pub const parse_utility = struct { + /// Parse a value from a string. + /// + /// (This is a convenience wrapper for `parse` and probably should not be overridden.) + /// + /// NOTE: `input` should live as long as the returned value. Otherwise, strings in the + /// returned parsed value will point to undefined memory. + pub fn parseString( + allocator: Allocator, + comptime T: type, + input: []const u8, + comptime parse_one: *const fn (*Parser) Result(T), + ) Result(T) { + var i = ParserInput.new(allocator, input); + var parser = Parser.new(&i); + const result = switch (parse_one(&parser)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (parser.expectExhausted().asErr()) |e| return .{ .err = e }; + return .{ .result = result }; + } +}; + +pub const to_css = struct { + /// Serialize `self` in CSS syntax and return a string. + /// + /// (This is a convenience wrapper for `to_css` and probably should not be overridden.) + pub fn string(allocator: Allocator, comptime T: type, this: *const T, options: PrinterOptions) PrintErr![]const u8 { + var s = ArrayList(u8){}; + errdefer s.deinit(allocator); + const writer = s.writer(allocator); + const W = @TypeOf(writer); + // PERF: think about how cheap this is to create + var printer = Printer(W).new(allocator, std.ArrayList(u8).init(allocator), writer, options); + defer printer.deinit(); + switch (T) { + CSSString => try CSSStringFns.toCss(this, W, &printer), + else => try this.toCss(W, &printer), + } + 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| { + try val.toCss(W, dest); + if (idx < len - 1) { + try dest.delim(',', false); + } + } + return; + } + + pub fn integer(comptime T: type, this: T, comptime W: type, dest: *Printer(W)) PrintErr!void { + const MAX_LEN = comptime maxDigits(T); + var buf: [MAX_LEN]u8 = undefined; + const str = std.fmt.bufPrint(buf[0..], "{d}", .{this}) catch unreachable; + return dest.writeStr(str); + } + + pub fn float32(this: f32, writer: anytype) !void { + var scratch: [64]u8 = undefined; + // PERF/TODO: Compare this to Rust dtoa-short crate + const floats = std.fmt.formatFloat(scratch[0..], this, .{ + .mode = .decimal, + }) catch unreachable; + return writer.writeAll(floats); + } + + fn maxDigits(comptime T: type) usize { + const max_val = std.math.maxInt(T); + return std.fmt.count("{d}", .{max_val}); + } +}; + +/// Parse `!important`. +/// +/// Typical usage is `input.try_parse(parse_important).is_ok()` +/// at the end of a `DeclarationParser::parse_value` implementation. +pub fn parseImportant(input: *Parser) Result(void) { + if (input.expectDelim('!').asErr()) |e| return .{ .err = e }; + return switch (input.expectIdentMatching("important")) { + .result => |v| .{ .result = v }, + .err => |e| .{ .err = e }, + }; +} + +pub const signfns = struct { + pub inline fn isSignPositive(x: f32) bool { + return !isSignNegative(x); + } + pub inline fn isSignNegative(x: f32) bool { + // IEEE754 says: isSignMinus(x) is true if and only if x has negative sign. isSignMinus + // applies to zeros and NaNs as well. + // SAFETY: This is just transmuting to get the sign bit, it's fine. + return @as(u32, @bitCast(x)) & 0x8000_0000 != 0; + } + /// Returns a number that represents the sign of `self`. + /// + /// - `1.0` if the number is positive, `+0.0` or `INFINITY` + /// - `-1.0` if the number is negative, `-0.0` or `NEG_INFINITY` + /// - NaN if the number is NaN + pub fn signum(x: f32) f32 { + if (std.math.isNan(x)) return std.math.nan(f32); + return copysign(1, x); + } + + pub inline fn signF32(x: f32) f32 { + if (x == 0.0) return if (isSignNegative(x)) 0.0 else -0.0; + return signum(x); + } +}; + +/// TODO(zack) is this correct +/// Copies the sign of `sign` to `self`, returning a new f32 value +pub inline fn copysign(self: f32, sign: f32) f32 { + // Convert both floats to their bit representations + const self_bits = @as(u32, @bitCast(self)); + const sign_bits = @as(u32, @bitCast(sign)); + + // Clear the sign bit of self and combine with the sign bit of sign + const result_bits = (self_bits & 0x7FFFFFFF) | (sign_bits & 0x80000000); + + // Convert the result back to f32 + return @as(f32, @bitCast(result_bits)); +} + +pub fn deepClone(comptime V: type, allocator: Allocator, list: *const ArrayList(V)) ArrayList(V) { + var newlist = ArrayList(V).initCapacity(allocator, list.items.len) catch bun.outOfMemory(); + + for (list.items) |item| { + newlist.appendAssumeCapacity(switch (V) { + i32, i64, u32, u64, f32, f64 => item, + else => item.deepClone(allocator), + }); + } + + return newlist; +} + +pub fn deepDeinit(comptime V: type, allocator: Allocator, list: *ArrayList(V)) void { + if (comptime !@hasDecl(V, "deinit")) return; + for (list.items) |*item| { + item.deinit(allocator); + } + + list.deinit(allocator); +} + +const Notation = struct { + decimal_point: bool, + scientific: bool, + + pub fn integer() Notation { + return .{ + .decimal_point = false, + .scientific = false, + }; + } +}; + +pub fn dtoa_short(buf: *[129]u8, value: f32, comptime precision: u8) struct { []u8, Notation } { + buf[0] = '0'; + const buf_len = bun.fmt.FormatDouble.dtoa(@ptrCast(buf[1..].ptr), @floatCast(value)).len; + return restrict_prec(buf[0 .. buf_len + 1], precision); +} + +fn restrict_prec(buf: []u8, comptime prec: u8) struct { []u8, Notation } { + const len: u8 = @intCast(buf.len); + + // Put a leading zero to capture any carry. + // Caller must prepare an empty byte for us; + bun.debugAssert(buf[0] == '0'); + buf[0] = '0'; + // Remove the sign for now. We will put it back at the end. + const sign = switch (buf[1]) { + '+', '-' => brk: { + const s = buf[1]; + buf[1] = '0'; + break :brk s; + }, + else => null, + }; + + // Locate dot, exponent, and the first significant digit. + var _pos_dot: ?u8 = null; + var pos_exp: ?u8 = null; + var _prec_start: ?u8 = null; + for (1..len) |i| { + if (buf[i] == '.') { + bun.debugAssert(_pos_dot == null); + _pos_dot = @intCast(i); + } else if (buf[i] == 'e') { + pos_exp = @intCast(i); + // We don't change exponent part, so stop here. + break; + } else if (_prec_start == null and buf[i] != '0') { + bun.debugAssert(buf[i] >= '1' and buf[i] <= '9'); + _prec_start = @intCast(i); + } + } + + const prec_start = if (_prec_start) |i| + i + else + // If there is no non-zero digit at all, it is just zero. + return .{ + buf[0..1], + Notation.integer(), + }; + + // Coefficient part ends at 'e' or the length. + const coeff_end = pos_exp orelse len; + // Decimal dot is effectively at the end of coefficient part if no + // dot presents before that. + const had_pos_dot = _pos_dot != null; + const pos_dot = _pos_dot orelse coeff_end; + // Find the end position of the number within the given precision. + const prec_end: u8 = brk: { + const end = prec_start + prec; + break :brk if (pos_dot > prec_start and pos_dot <= end) end + 1 else end; + }; + var new_coeff_end = coeff_end; + if (prec_end < coeff_end) { + // Round to the given precision. + const next_char = buf[prec_end]; + new_coeff_end = prec_end; + if (next_char >= '5') { + var i = prec_end; + while (i != 0) { + i -= 1; + if (buf[i] == '.') { + continue; + } + if (buf[i] != '9') { + buf[i] += 1; + new_coeff_end = i + 1; + break; + } + buf[i] = '0'; + } + } + } + if (new_coeff_end < pos_dot) { + // If the precision isn't enough to reach the dot, set all digits + // in-between to zero and keep the number until the dot. + for (new_coeff_end..pos_dot) |i| { + buf[i] = '0'; + } + new_coeff_end = pos_dot; + } else if (had_pos_dot) { + // Strip any trailing zeros. + var i = new_coeff_end; + while (i != 0) { + i -= 1; + if (buf[i] != '0') { + if (buf[i] == '.') { + new_coeff_end = i; + } + break; + } + new_coeff_end = i; + } + } + // Move exponent part if necessary. + const real_end = if (pos_exp) |posexp| brk: { + const exp_len = len - posexp; + if (new_coeff_end != posexp) { + for (0..exp_len) |i| { + buf[new_coeff_end + i] = buf[posexp + i]; + } + } + break :brk new_coeff_end + exp_len; + } else new_coeff_end; + // Add back the sign and strip the leading zero. + const result = if (sign) |sgn| brk: { + if (buf[1] == '0' and buf[2] != '.') { + buf[1] = sgn; + break :brk buf[1..real_end]; + } + bun.debugAssert(buf[0] == '0'); + buf[0] = sgn; + break :brk buf[0..real_end]; + } else brk: { + if (buf[0] == '0' and buf[1] != '.') { + break :brk buf[1..real_end]; + } + break :brk buf[0..real_end]; + }; + // Generate the notation info. + const notation = Notation{ + .decimal_point = pos_dot < new_coeff_end, + .scientific = pos_exp != null, + }; + return .{ result, notation }; +} + +pub inline fn fract(val: f32) f32 { + return val - @trunc(val); +} diff --git a/src/css/declaration.zig b/src/css/declaration.zig new file mode 100644 index 0000000000000..86ec09d67ed82 --- /dev/null +++ b/src/css/declaration.zig @@ -0,0 +1,236 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const Error = css.Error; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const PrintResult = css.PrintResult; +const Result = css.Result; + +const ArrayList = std.ArrayListUnmanaged; +pub const DeclarationList = ArrayList(css.Property); + +/// A CSS declaration block. +/// +/// Properties are separated into a list of `!important` declararations, +/// and a list of normal declarations. This reduces memory usage compared +/// with storing a boolean along with each property. +/// +/// TODO: multiarraylist will probably be faster here, as it makes one allocation +/// instead of two. +pub const DeclarationBlock = struct { + /// A list of `!important` declarations in the block. + important_declarations: ArrayList(css.Property) = .{}, + /// A list of normal declarations in the block. + declarations: ArrayList(css.Property) = .{}, + + const This = @This(); + + pub fn parse(input: *css.Parser, options: *const css.ParserOptions) Result(DeclarationBlock) { + var important_declarations = DeclarationList{}; + var declarations = DeclarationList{}; + var decl_parser = PropertyDeclarationParser{ + .important_declarations = &important_declarations, + .declarations = &declarations, + .options = options, + }; + errdefer decl_parser.deinit(); + + var parser = css.RuleBodyParser(PropertyDeclarationParser).new(input, &decl_parser); + + while (parser.next()) |res| { + if (res.asErr()) |e| { + if (options.error_recovery) { + options.warn(e); + continue; + } + return .{ .err = e }; + } + } + + return .{ .result = DeclarationBlock{ + .important_declarations = important_declarations, + .declarations = declarations, + } }; + } + + pub fn len(this: *const DeclarationBlock) usize { + return this.declarations.items.len + this.important_declarations.items.len; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + const length = this.len(); + var i: usize = 0; + + const DECLS: []const []const u8 = &[_][]const u8{ "declarations", "important_declarations" }; + + inline for (DECLS) |decl_field_name| { + const decls = &@field(this, decl_field_name); + const is_important = comptime std.mem.eql(u8, decl_field_name, "important_declarations"); + + for (decls.items) |*decl| { + try decl.toCss(W, dest, is_important); + if (i != length - 1) { + try dest.writeChar(';'); + try dest.whitespace(); + } + i += 1; + } + } + + return; + } + + /// Writes the declarations to a CSS block, including starting and ending braces. + pub fn toCssBlock(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + + var i: usize = 0; + const length = this.len(); + + const DECLS: []const []const u8 = &[_][]const u8{ "declarations", "important_declarations" }; + + inline for (DECLS) |decl_field_name| { + const decls = &@field(this, decl_field_name); + const is_important = comptime std.mem.eql(u8, decl_field_name, "important_declarations"); + for (decls.items) |*decl| { + try dest.newline(); + try decl.toCss(W, dest, is_important); + if (i != length - 1 or !dest.minify) { + try dest.writeChar(';'); + } + i += 1; + } + } + + dest.dedent(); + try dest.newline(); + return dest.writeChar('}'); + } +}; + +pub const PropertyDeclarationParser = struct { + important_declarations: *ArrayList(css.Property), + declarations: *ArrayList(css.Property), + options: *const css.ParserOptions, + + const This = @This(); + + pub const AtRuleParser = struct { + pub const Prelude = void; + pub const AtRule = void; + + pub fn parsePrelude(_: *This, name: []const u8, input: *css.Parser) Result(Prelude) { + return .{ + .err = input.newError(css.BasicParseErrorKind{ .at_rule_invalid = name }), + }; + } + + pub fn parseBlock(_: *This, _: Prelude, _: *const css.ParserState, input: *css.Parser) Result(AtRule) { + return .{ .err = input.newError(css.BasicParseErrorKind.at_rule_body_invalid) }; + } + + pub fn ruleWithoutBlock(_: *This, _: Prelude, _: *const css.ParserState) css.Maybe(AtRule, void) { + return .{ .err = {} }; + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = void; + pub const QualifiedRule = void; + + pub fn parsePrelude(this: *This, input: *css.Parser) Result(Prelude) { + _ = this; // autofix + return .{ .err = input.newError(css.BasicParseErrorKind.qualified_rule_invalid) }; + } + + pub fn parseBlock(this: *This, prelude: Prelude, start: *const css.ParserState, input: *css.Parser) Result(QualifiedRule) { + _ = this; // autofix + _ = prelude; // autofix + _ = start; // autofix + return .{ .err = input.newError(css.BasicParseErrorKind.qualified_rule_invalid) }; + } + }; + + pub const DeclarationParser = struct { + pub const Declaration = void; + + pub fn parseValue(this: *This, name: []const u8, input: *css.Parser) Result(Declaration) { + return parse_declaration( + name, + input, + this.declarations, + this.important_declarations, + this.options, + ); + } + }; + + pub const RuleBodyItemParser = struct { + pub fn parseQualified(this: *This) bool { + _ = this; // autofix + return false; + } + + pub fn parseDeclarations(this: *This) bool { + _ = this; // autofix + return true; + } + }; +}; + +pub fn parse_declaration( + name: []const u8, + input: *css.Parser, + declarations: *DeclarationList, + important_declarations: *DeclarationList, + options: *const css.ParserOptions, +) Result(void) { + const property_id = css.PropertyId.fromStr(name); + var delimiters = css.Delimiters{ .bang = true }; + if (property_id != .custom or property_id.custom != .custom) { + delimiters.curly_bracket = true; + } + const Closure = struct { + property_id: css.PropertyId, + options: *const css.ParserOptions, + + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(css.Property) { + return css.Property.parse(this.property_id, input2, this.options); + } + }; + var closure = Closure{ + .property_id = property_id, + .options = options, + }; + const property = switch (input.parseUntilBefore(delimiters, css.Property, &closure, Closure.parsefn)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + const important = input.tryParse(struct { + pub fn parsefn(i: *css.Parser) Result(void) { + if (i.expectDelim('!').asErr()) |e| return .{ .err = e }; + return i.expectIdentMatching("important"); + } + }.parsefn, .{}).isOk(); + if (input.expectExhausted().asErr()) |e| return .{ .err = e }; + if (important) { + important_declarations.append(input.allocator(), property) catch bun.outOfMemory(); + } else { + declarations.append(input.allocator(), property) catch bun.outOfMemory(); + } + + return .{ .result = {} }; +} + +pub const DeclarationHandler = struct { + pub fn default() DeclarationHandler { + return .{}; + } +}; diff --git a/src/css/dependencies.zig b/src/css/dependencies.zig new file mode 100644 index 0000000000000..1079a6e0fac33 --- /dev/null +++ b/src/css/dependencies.zig @@ -0,0 +1,145 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); +const DashedIdent = css_values.ident.DashedIdent; +const Url = css_values.url.Url; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; +// const Location = css.Location; + +const ArrayList = std.ArrayListUnmanaged; + +/// Options for `analyze_dependencies` in `PrinterOptions`. +pub const DependencyOptions = struct { + /// Whether to remove `@import` rules. + remove_imports: bool, +}; + +/// A dependency. +pub const Dependency = union(enum) { + /// An `@import` dependency. + import: ImportDependency, + /// A `url()` dependency. + url: UrlDependency, +}; + +/// A line and column position within a source file. +pub const Location = struct { + /// The line number, starting from 1. + line: u32, + /// The column number, starting from 1. + column: u32, + + pub fn fromSourceLocation(loc: css.SourceLocation) Location { + return Location{ + .line = loc.line + 1, + .column = loc.column, + }; + } +}; + +/// An `@import` dependency. +pub const ImportDependency = struct { + /// The url to import. + url: []const u8, + /// The placeholder that the URL was replaced with. + placeholder: []const u8, + /// An optional `supports()` condition. + supports: ?[]const u8, + /// A media query. + media: ?[]const u8, + /// The location of the dependency in the source file. + loc: SourceRange, + + pub fn new(allocator: Allocator, rule: *const css.css_rules.import.ImportRule, filename: []const u8) ImportDependency { + const supports = if (rule.supports) |*supports| brk: { + const s = css.to_css.string( + allocator, + css.css_rules.supports.SupportsCondition, + supports, + css.PrinterOptions{}, + ) catch bun.Output.panic( + "Unreachable code: failed to stringify SupportsCondition.\n\nThis is a bug in Bun's CSS printer. Please file a bug report at https://github.com/oven-sh/bun/issues/new/choose", + .{}, + ); + break :brk s; + } else null; + + const media = if (rule.media.media_queries.items.len > 0) media: { + const s = css.to_css.string(allocator, css.MediaList, &rule.media, css.PrinterOptions{}) catch bun.Output.panic( + "Unreachable code: failed to stringify MediaList.\n\nThis is a bug in Bun's CSS printer. Please file a bug report at https://github.com/oven-sh/bun/issues/new/choose", + .{}, + ); + break :media s; + } else null; + + const placeholder = css.css_modules.hash(allocator, "{s}_{s}", .{ filename, rule.url }, false); + + return ImportDependency{ + // TODO(zack): should we clone this? lightningcss does that + .url = rule.url, + .placeholder = placeholder, + .supports = supports, + .media = media, + .loc = SourceRange.new( + filename, + css.dependencies.Location{ .line = rule.loc.line + 1, .column = rule.loc.column }, + 8, + rule.url.len + 2, + ), // TODO: what about @import url(...)? + }; + } +}; + +/// A `url()` dependency. +pub const UrlDependency = struct { + /// The url of the dependency. + url: []const u8, + /// The placeholder that the URL was replaced with. + placeholder: []const u8, + /// The location of the dependency in the source file. + loc: SourceRange, + + pub fn new(allocator: Allocator, url: *const Url, filename: []const u8) UrlDependency { + const placeholder = css.css_modules.hash( + allocator, + "{s}_{s}", + .{ filename, url.url }, + false, + ); + return UrlDependency{ + .url = url.url, + .placeholder = placeholder, + .loc = SourceRange.new(filename, url.loc, 4, url.url.len), + }; + } +}; + +/// Represents the range of source code where a dependency was found. +pub const SourceRange = struct { + /// The filename in which the dependency was found. + file_path: []const u8, + /// The starting line and column position of the dependency. + start: Location, + /// The ending line and column position of the dependency. + end: Location, + + pub fn new(filename: []const u8, loc: Location, offset: u32, len: usize) SourceRange { + return SourceRange{ + .file_path = filename, + .start = Location{ + .line = loc.line, + .column = loc.column + offset, + }, + .end = Location{ + .line = loc.line, + .column = loc.column + offset + @as(u32, @intCast(len)) - 1, + }, + }; + } +}; diff --git a/src/css/error.zig b/src/css/error.zig new file mode 100644 index 0000000000000..8a1561d70553e --- /dev/null +++ b/src/css/error.zig @@ -0,0 +1,324 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); +const DashedIdent = css_values.ident.DashedIdent; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; +const Location = css.Location; + +const ArrayList = std.ArrayListUnmanaged; + +/// A printer error. +pub const PrinterError = Err(PrinterErrorKind); + +pub fn fmtPrinterError() PrinterError { + return .{ + .kind = .fmt_error, + .loc = null, + }; +} + +/// An error with a source location. +pub fn Err(comptime T: type) type { + return struct { + /// The type of error that occurred. + kind: T, + /// The location where the error occurred. + loc: ?ErrorLocation, + + pub fn fromParseError(err: ParseError(ParserError), filename: []const u8) Err(ParserError) { + if (T != ParserError) { + @compileError("Called .fromParseError() when T is not ParserError"); + } + + const kind = switch (err.kind) { + .basic => |b| switch (b) { + .unexpected_token => |t| ParserError{ .unexpected_token = t }, + .end_of_input => ParserError.end_of_input, + .at_rule_invalid => |a| ParserError{ .at_rule_invalid = a }, + .at_rule_body_invalid => ParserError.at_rule_body_invalid, + .qualified_rule_invalid => ParserError.qualified_rule_invalid, + }, + .custom => |c| c, + }; + + return .{ + .kind = kind, + .loc = ErrorLocation{ + .filename = filename, + .line = err.location.line, + .column = err.location.column, + }, + }; + } + }; +} + +/// Extensible parse errors that can be encountered by client parsing implementations. +pub fn ParseError(comptime T: type) type { + return struct { + /// Details of this error + kind: ParserErrorKind(T), + /// Location where this error occurred + location: css.SourceLocation, + + pub fn basic(this: @This()) BasicParseError { + return switch (this.kind) { + .basic => |kind| BasicParseError{ + .kind = kind, + .location = this.location, + }, + .custom => @panic("Not a basic parse error. This is a bug in Bun's css parser."), + }; + } + }; +} + +pub fn ParserErrorKind(comptime T: type) type { + return union(enum) { + /// A fundamental parse error from a built-in parsing routine. + basic: BasicParseErrorKind, + /// A parse error reported by downstream consumer code. + custom: T, + + pub fn format(this: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + return switch (this) { + .basic => |basic| writer.print("basic: {}", .{basic}), + .custom => |custom| writer.print("custom: {}", .{custom}), + }; + } + }; +} + +/// Details about a `BasicParseError` +pub const BasicParseErrorKind = union(enum) { + /// An unexpected token was encountered. + unexpected_token: css.Token, + /// The end of the input was encountered unexpectedly. + end_of_input, + /// An `@` rule was encountered that was invalid. + at_rule_invalid: []const u8, + /// The body of an '@' rule was invalid. + at_rule_body_invalid, + /// A qualified rule was encountered that was invalid. + qualified_rule_invalid, + + pub fn format(this: *const BasicParseErrorKind, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { + _ = fmt; // autofix + _ = opts; // autofix + return switch (this.*) { + .unexpected_token => |token| { + try writer.print("unexpected token: {any}", .{token}); + }, + .end_of_input => { + try writer.print("unexpected end of input", .{}); + }, + .at_rule_invalid => |rule| { + try writer.print("invalid @ rule encountered: '@{s}'", .{rule}); + }, + .at_rule_body_invalid => { + // try writer.print("invalid @ body rule encountered: '@{s}'", .{}); + try writer.print("invalid @ body rule encountered", .{}); + }, + .qualified_rule_invalid => { + try writer.print("invalid qualified rule encountered", .{}); + }, + }; + } +}; + +/// A line and column location within a source file. +pub const ErrorLocation = struct { + /// The filename in which the error occurred. + filename: []const u8, + /// The line number, starting from 0. + line: u32, + /// The column number, starting from 1. + column: u32, +}; + +/// A printer error type. +pub const PrinterErrorKind = union(enum) { + /// An ambiguous relative `url()` was encountered in a custom property declaration. + ambiguous_url_in_custom_property: struct { + /// The ambiguous URL. + url: []const u8, + }, + /// A [std::fmt::Error](std::fmt::Error) was encountered in the underlying destination. + fmt_error, + /// The CSS modules `composes` property cannot be used within nested rules. + invalid_composes_nesting, + /// The CSS modules `composes` property cannot be used with a simple class selector. + invalid_composes_selector, + /// The CSS modules pattern must end with `[local]` for use in CSS grid. + invalid_css_modules_pattern_in_grid, +}; + +/// A parser error. +pub const ParserError = union(enum) { + /// An at rule body was invalid. + at_rule_body_invalid, + /// An at rule prelude was invalid. + at_rule_prelude_invalid, + /// An unknown or unsupported at rule was encountered. + at_rule_invalid: []const u8, + /// Unexpectedly encountered the end of input data. + end_of_input, + /// A declaration was invalid. + invalid_declaration, + /// A media query was invalid. + invalid_media_query, + /// Invalid CSS nesting. + invalid_nesting, + /// The @nest rule is deprecated. + deprecated_nest_rule, + /// An invalid selector in an `@page` rule. + invalid_page_selector, + /// An invalid value was encountered. + invalid_value, + /// Invalid qualified rule. + qualified_rule_invalid, + /// A selector was invalid. + selector_error: SelectorError, + /// An `@import` rule was encountered after any rule besides `@charset` or `@layer`. + unexpected_import_rule, + /// A `@namespace` rule was encountered after any rules besides `@charset`, `@import`, or `@layer`. + unexpected_namespace_rule, + /// An unexpected token was encountered. + unexpected_token: css.Token, + /// Maximum nesting depth was reached. + maximum_nesting_depth, + + pub fn format(this: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + return switch (this) { + .at_rule_invalid => |name| writer.print("at_rule_invalid: {s}", .{name}), + .unexpected_token => |token| writer.print("unexpected_token: {s}", .{@tagName(token)}), + .selector_error => |err| writer.print("selector_error: {}", .{err}), + else => writer.print("{s}", .{@tagName(this)}), + }; + } +}; + +/// The fundamental parsing errors that can be triggered by built-in parsing routines. +pub const BasicParseError = struct { + /// Details of this error + kind: BasicParseErrorKind, + /// Location where this error occurred + location: css.SourceLocation, + + pub fn intoParseError( + this: @This(), + comptime T: type, + ) ParseError(T) { + return ParseError(T){ + .kind = .{ .basic = this.kind }, + .location = this.location, + }; + } + + pub inline fn intoDefaultParseError( + this: @This(), + ) ParseError(ParserError) { + return ParseError(ParserError){ + .kind = .{ .basic = this.kind }, + .location = this.location, + }; + } +}; + +/// A selector parsing error. +pub const SelectorError = union(enum) { + /// An unexpected token was found in an attribute selector. + bad_value_in_attr: css.Token, + /// An unexpected token was found in a class selector. + class_needs_ident: css.Token, + /// A dangling combinator was found. + dangling_combinator, + /// An empty selector. + empty_selector, + /// A `|` was expected in an attribute selector. + expected_bar_in_attr: css.Token, + /// A namespace was expected. + expected_namespace: []const u8, + /// An unexpected token was encountered in a namespace. + explicit_namespace_unexpected_token: css.Token, + /// An invalid pseudo class was encountered after a pseudo element. + invalid_pseudo_class_after_pseudo_element, + /// An invalid pseudo class was encountered after a `-webkit-scrollbar` pseudo element. + invalid_pseudo_class_after_webkit_scrollbar, + /// A `-webkit-scrollbar` state was encountered before a `-webkit-scrollbar` pseudo element. + invalid_pseudo_class_before_webkit_scrollbar, + /// Invalid qualified name in attribute selector. + invalid_qual_name_in_attr: css.Token, + /// The current token is not allowed in this state. + invalid_state, + /// The selector is required to have the `&` nesting selector at the start. + missing_nesting_prefix, + /// The selector is missing a `&` nesting selector. + missing_nesting_selector, + /// No qualified name in attribute selector. + no_qualified_name_in_attribute_selector: css.Token, + /// An invalid token was encountered in a pseudo element. + pseudo_element_expected_ident: css.Token, + /// An unexpected identifier was encountered. + unexpected_ident: []const u8, + /// An unexpected token was encountered inside an attribute selector. + unexpected_token_in_attribute_selector: css.Token, + /// An unsupported pseudo class or pseudo element was encountered. + unsupported_pseudo_class_or_element: []const u8, + + pub fn format(this: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + return switch (this) { + .dangling_combinator, .empty_selector, .invalid_state, .missing_nesting_prefix, .missing_nesting_selector => { + try writer.print("{s}", .{@tagName(this)}); + }, + inline .expected_namespace, .unexpected_ident, .unsupported_pseudo_class_or_element => |str| { + try writer.print("{s}: {s}", .{ @tagName(this), str }); + }, + inline .bad_value_in_attr, + .class_needs_ident, + .expected_bar_in_attr, + .explicit_namespace_unexpected_token, + .invalid_qual_name_in_attr, + .no_qualified_name_in_attribute_selector, + .pseudo_element_expected_ident, + .unexpected_token_in_attribute_selector, + => |tok| { + try writer.print("{s}: {s}", .{ @tagName(this), @tagName(tok) }); + }, + else => try writer.print("{s}", .{@tagName(this)}), + }; + } +}; + +pub fn ErrorWithLocation(comptime T: type) type { + return struct { + kind: T, + loc: css.Location, + }; +} + +pub const MinifyError = ErrorWithLocation(MinifyErrorKind); +/// A transformation error. +pub const MinifyErrorKind = union(enum) { + /// A circular `@custom-media` rule was detected. + circular_custom_media: struct { + /// The name of the `@custom-media` rule that was referenced circularly. + name: []const u8, + }, + /// Attempted to reference a custom media rule that doesn't exist. + custom_media_not_defined: struct { + /// The name of the `@custom-media` rule that was not defined. + name: []const u8, + }, + /// Boolean logic with media types in @custom-media rules is not supported. + unsupported_custom_media_boolean_logic: struct { + /// The source location of the `@custom-media` rule with unsupported boolean logic. + custom_media_loc: Location, + }, +}; diff --git a/src/css/logical.zig b/src/css/logical.zig new file mode 100644 index 0000000000000..ed0c945fc233c --- /dev/null +++ b/src/css/logical.zig @@ -0,0 +1,30 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const Error = css.Error; + +const ArrayList = std.ArrayListUnmanaged; + +pub const PropertyCategory = enum { + logical, + physical, +}; + +pub const LogicalGroup = enum { + border_color, + border_style, + border_width, + border_radius, + margin, + scroll_margin, + padding, + scroll_padding, + inset, + size, + min_size, + max_size, +}; diff --git a/src/css/media_query.zig b/src/css/media_query.zig new file mode 100644 index 0000000000000..32c3b8df4ee23 --- /dev/null +++ b/src/css/media_query.zig @@ -0,0 +1,1401 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; + +const Length = css.css_values.length.Length; +const CSSNumber = css.css_values.number.CSSNumber; +const Integer = css.css_values.number.Integer; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const CSSInteger = css.css_values.number.CSSInteger; +const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +const Resolution = css.css_values.resolution.Resolution; +const Ratio = css.css_values.ratio.Ratio; +const Ident = css.css_values.ident.Ident; +const IdentFns = css.css_values.ident.IdentFns; +const EnvironmentVariable = css.css_properties.custom.EnvironmentVariable; +const DashedIdent = css.css_values.ident.DashedIdent; +const DashedIdentFns = css.css_values.ident.DashedIdentFns; + +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const PrintResult = css.PrintResult; +const Result = css.Result; + +pub fn ValidQueryCondition(comptime T: type) void { + // fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result>>; + _ = T.parseFeature; + // fn create_negation(condition: Box) -> Self; + _ = T.createNegation; + // fn create_operation(operator: Operator, conditions: Vec) -> Self; + _ = T.createOperation; + // fn parse_style_query<'t>(input: &mut Parser<'i, 't>) -> Result>> { + _ = T.parseStyleQuery; + // fn needs_parens(&self, parent_operator: Option, targets: &Targets) -> bool; + _ = T.needsParens; +} + +/// A [media query list](https://drafts.csswg.org/mediaqueries/#mq-list). +pub const MediaList = struct { + /// The list of media queries. + media_queries: ArrayList(MediaQuery), + + /// Parse a media query list from CSS. + pub fn parse(input: *css.Parser) Result(MediaList) { + var media_queries = ArrayList(MediaQuery){}; + while (true) { + const mq = switch (input.parseUntilBefore(css.Delimiters{ .comma = true }, MediaQuery, {}, css.voidWrap(MediaQuery, MediaQuery.parse))) { + .result => |v| v, + .err => |e| { + if (e.kind == .basic and e.kind.basic == .end_of_input) break; + return .{ .err = e }; + }, + }; + media_queries.append(input.allocator(), mq) catch bun.outOfMemory(); + + if (input.next().asValue()) |tok| { + if (tok.* != .comma) { + bun.Output.panic("Unreachable code: expected a comma after parsing a MediaQuery.\n\nThis is a bug in Bun's CSS parser. Please file a bug report at https://github.com/oven-sh/bun/issues/new/choose", .{}); + } + } else break; + } + + return .{ .result = MediaList{ .media_queries = media_queries } }; + } + + pub fn toCss(this: *const MediaList, comptime W: type, dest: *css.Printer(W)) PrintErr!void { + if (this.media_queries.items.len == 0) { + return dest.writeStr("not all"); + } + + var first = true; + for (this.media_queries.items) |*query| { + if (!first) { + try dest.delim(',', false); + } + first = false; + try query.toCss(W, dest); + } + return; + } + + pub fn eql(lhs: *const MediaList, rhs: *const MediaList) bool { + _ = lhs; // autofix + _ = rhs; // autofix + @panic(css.todo_stuff.depth); + } + + /// Returns whether the media query list always matches. + pub fn alwaysMatches(this: *const MediaList) bool { + // If the media list is empty, it always matches. + return this.media_queries.items.len == 0 or brk: { + for (this.media_queries.items) |*query| { + if (!query.alwaysMatches()) break :brk false; + } + break :brk true; + }; + } +}; + +/// A binary `and` or `or` operator. +pub const Operator = enum { + /// The `and` operator. + @"and", + /// The `or` operator. + @"or", + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return css.enum_property_util.toCss(@This(), this, W, dest); + } +}; + +/// A [media query](https://drafts.csswg.org/mediaqueries/#media). +pub const MediaQuery = struct { + /// The qualifier for this query. + qualifier: ?Qualifier, + /// The media type for this query, that can be known, unknown, or "all". + media_type: MediaType, + /// The condition that this media query contains. This cannot have `or` + /// in the first level. + condition: ?MediaCondition, + // ~toCssImpl + const This = @This(); + + /// Returns whether the media query is guaranteed to always match. + pub fn alwaysMatches(this: *const MediaQuery) bool { + return this.qualifier == null and this.media_type == .all and this.condition == null; + } + + pub fn parse(input: *css.Parser) Result(MediaQuery) { + const Fn = struct { + pub fn tryParseFn(i: *css.Parser) Result(struct { ?Qualifier, ?MediaType }) { + const qualifier = switch (i.tryParse(Qualifier.parse, .{})) { + .result => |vv| vv, + .err => null, + }; + const media_type = switch (MediaType.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ qualifier, media_type } }; + } + }; + const qualifier, const explicit_media_type = switch (input.tryParse(Fn.tryParseFn, .{})) { + .result => |v| v, + .err => .{ null, null }, + }; + + const condition = if (explicit_media_type == null) + switch (MediaCondition.parseWithFlags(input, QueryConditionFlags{ .allow_or = true })) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } + else if (input.tryParse(css.Parser.expectIdentMatching, .{"and"}).isOk()) + switch (MediaCondition.parseWithFlags(input, QueryConditionFlags.empty())) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } + else + null; + + const media_type = explicit_media_type orelse MediaType.all; + + return .{ + .result = MediaQuery{ + .qualifier = qualifier, + .media_type = media_type, + .condition = condition, + }, + }; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (this.qualifier) |qual| { + try qual.toCss(W, dest); + try dest.writeChar(' '); + } + + switch (this.media_type) { + .all => { + // We need to print "all" if there's a qualifier, or there's + // just an empty list of expressions. + // + // Otherwise, we'd serialize media queries like "(min-width: + // 40px)" in "all (min-width: 40px)", which is unexpected. + if (this.qualifier != null or this.condition != null) { + try dest.writeStr("all"); + } + }, + .print => { + try dest.writeStr("print"); + }, + .screen => { + try dest.writeStr("screen"); + }, + .custom => |desc| { + try dest.writeStr(desc); + }, + } + + const condition = if (this.condition) |*cond| cond else return; + + const needs_parens = if (this.media_type != .all or this.qualifier != null) needs_parens: { + break :needs_parens condition.* == .operation and condition.operation.operator != .@"and"; + } else false; + + return toCssWithParensIfNeeded(condition, W, dest, needs_parens); + } +}; + +/// Flags for `parse_query_condition`. +pub const QueryConditionFlags = packed struct(u8) { + /// Whether to allow top-level "or" boolean logic. + allow_or: bool = false, + /// Whether to allow style container queries. + allow_style: bool = false, + __unused: u6 = 0, + + pub usingnamespace css.Bitflags(@This()); +}; + +pub fn toCssWithParensIfNeeded( + v: anytype, + comptime W: type, + dest: *Printer(W), + needs_parens: bool, +) PrintErr!void { + if (needs_parens) { + try dest.writeChar('('); + } + try v.toCss(W, dest); + if (needs_parens) { + try dest.writeChar(')'); + } + return; +} + +/// A [media query qualifier](https://drafts.csswg.org/mediaqueries/#mq-prefix). +pub const Qualifier = enum { + /// Prevents older browsers from matching the media query. + only, + /// Negates a media query. + not, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return css.enum_property_util.toCss(@This(), this, W, dest); + } +}; + +/// A [media type](https://drafts.csswg.org/mediaqueries/#media-types) within a media query. +pub const MediaType = union(enum) { + /// Matches all devices. + all, + /// Matches printers, and devices intended to reproduce a printed + /// display, such as a web browser showing a document in “Print Preview”. + print, + /// Matches all devices that aren’t matched by print. + screen, + /// An unknown media type. + custom: []const u8, + + pub fn parse(input: *css.Parser) Result(MediaType) { + const name = switch (input.expectIdent()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = MediaType.fromStr(name) }; + } + + pub fn fromStr(name: []const u8) MediaType { + // css.todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "all")) return .all; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "print")) return .print; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "screen")) return .print; + return .{ .custom = name }; + } +}; + +pub fn operationToCss(comptime QueryCondition: type, operator: Operator, conditions: *const ArrayList(QueryCondition), comptime W: type, dest: *Printer(W)) PrintErr!void { + ValidQueryCondition(QueryCondition); + const first = &conditions.items[0]; + try toCssWithParensIfNeeded(first, W, dest, first.needsParens(operator, &dest.targets)); + if (conditions.items.len == 1) return; + for (conditions.items[1..]) |*item| { + try dest.writeChar(' '); + try operator.toCss(W, dest); + try dest.writeChar(' '); + try toCssWithParensIfNeeded(item, W, dest, item.needsParens(operator, &dest.targets)); + } + return; +} + +/// Represents a media condition. +/// +/// Implements QueryCondition interface. +pub const MediaCondition = union(enum) { + feature: MediaFeature, + not: *MediaCondition, + operation: struct { + operator: Operator, + conditions: ArrayList(MediaCondition), + }, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .feature => |*f| { + try f.toCss(W, dest); + }, + .not => |c| { + try dest.writeStr("not "); + try toCssWithParensIfNeeded(c, W, dest, c.needsParens(null, &dest.targets)); + }, + .operation => |operation| { + try operationToCss(MediaCondition, operation.operator, &operation.conditions, W, dest); + }, + } + + return; + } + + /// QueryCondition.parseFeature + pub fn parseFeature(input: *css.Parser) Result(MediaCondition) { + const feature = switch (MediaFeature.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = MediaCondition{ .feature = feature } }; + } + + /// QueryCondition.createNegation + pub fn createNegation(condition: *MediaCondition) MediaCondition { + return MediaCondition{ .not = condition }; + } + + /// QueryCondition.createOperation + pub fn createOperation(operator: Operator, conditions: ArrayList(MediaCondition)) MediaCondition { + return MediaCondition{ + .operation = .{ + .operator = operator, + .conditions = conditions, + }, + }; + } + + /// QueryCondition.parseStyleQuery + pub fn parseStyleQuery(input: *css.Parser) Result(MediaCondition) { + return .{ .err = input.newErrorForNextToken() }; + } + + /// QueryCondition.needsParens + pub fn needsParens(this: *const MediaCondition, parent_operator: ?Operator, targets: *const css.targets.Targets) bool { + return switch (this.*) { + .not => true, + .operation => |operation| operation.operator != parent_operator, + .feature => |f| f.needsParens(parent_operator, targets), + }; + } + + pub fn parseWithFlags(input: *css.Parser, flags: QueryConditionFlags) Result(MediaCondition) { + return parseQueryCondition(MediaCondition, input, flags); + } +}; + +/// Parse a single query condition. +pub fn parseQueryCondition( + comptime QueryCondition: type, + input: *css.Parser, + flags: QueryConditionFlags, +) Result(QueryCondition) { + const location = input.currentSourceLocation(); + const is_negation, const is_style = brk: { + const tok = switch (input.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .open_paren => break :brk .{ false, false }, + .ident => |ident| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "not")) break :brk .{ true, false }; + }, + .function => |f| { + if (flags.contains(QueryConditionFlags{ .allow_style = true }) and + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "style")) + { + break :brk .{ false, true }; + } + }, + else => {}, + } + return .{ .err = location.newUnexpectedTokenError(tok.*) }; + }; + + const first_condition: QueryCondition = first_condition: { + const val: u8 = @as(u8, @intFromBool(is_negation)) << 1 | @as(u8, @intFromBool(is_style)); + // (is_negation, is_style) + switch (val) { + // (true, false) + 0b10 => { + const inner_condition = switch (parseParensOrFunction(QueryCondition, input, flags)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = QueryCondition.createNegation(bun.create(input.allocator(), QueryCondition, inner_condition)) }; + }, + // (true, true) + 0b11 => { + const inner_condition = switch (QueryCondition.parseStyleQuery(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = QueryCondition.createNegation(bun.create(input.allocator(), QueryCondition, inner_condition)) }; + }, + 0b00 => break :first_condition switch (parseParenBlock(QueryCondition, input, flags)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + 0b01 => break :first_condition switch (QueryCondition.parseStyleQuery(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + else => unreachable, + } + }; + + const operator: Operator = if (input.tryParse(Operator.parse, .{}).asValue()) |op| + op + else + return .{ .result = first_condition }; + + if (!flags.contains(QueryConditionFlags{ .allow_or = true }) and operator == .@"or") { + return .{ .err = location.newUnexpectedTokenError(css.Token{ .ident = "or" }) }; + } + + var conditions = ArrayList(QueryCondition){}; + conditions.append( + input.allocator(), + first_condition, + ) catch unreachable; + conditions.append( + input.allocator(), + switch (parseParensOrFunction(QueryCondition, input, flags)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + ) catch unreachable; + + const delim = switch (operator) { + .@"and" => "and", + .@"or" => "or", + }; + + while (true) { + if (input.tryParse(css.Parser.expectIdentMatching, .{delim}).isErr()) { + return .{ .result = QueryCondition.createOperation(operator, conditions) }; + } + + conditions.append( + input.allocator(), + switch (parseParensOrFunction(QueryCondition, input, flags)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + ) catch unreachable; + } +} + +/// Parse a media condition in parentheses, or a style() function. +pub fn parseParensOrFunction( + comptime QueryCondition: type, + input: *css.Parser, + flags: QueryConditionFlags, +) Result(QueryCondition) { + const location = input.currentSourceLocation(); + const t = switch (input.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (t.*) { + .open_paren => return parseParenBlock(QueryCondition, input, flags), + .function => |f| { + if (flags.contains(QueryConditionFlags{ .allow_style = true }) and + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "style")) + { + return QueryCondition.parseStyleQuery(input); + } + }, + else => {}, + } + return .{ .err = location.newUnexpectedTokenError(t.*) }; +} + +fn parseParenBlock( + comptime QueryCondition: type, + input: *css.Parser, + flags: QueryConditionFlags, +) Result(QueryCondition) { + const Closure = struct { + flags: QueryConditionFlags, + pub fn parseNestedBlockFn(this: *@This(), i: *css.Parser) Result(QueryCondition) { + if (i.tryParse(@This().tryParseFn, .{this}).asValue()) |inner| { + return .{ .result = inner }; + } + + return QueryCondition.parseFeature(i); + } + + pub fn tryParseFn(i: *css.Parser, this: *@This()) Result(QueryCondition) { + return parseQueryCondition(QueryCondition, i, this.flags); + } + }; + + var closure = Closure{ + .flags = flags, + }; + return input.parseNestedBlock(QueryCondition, &closure, Closure.parseNestedBlockFn); +} + +/// A [media feature](https://drafts.csswg.org/mediaqueries/#typedef-media-feature) +pub const MediaFeature = QueryFeature(MediaFeatureId); + +const MediaFeatureId = enum { + /// The [width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#width) media feature. + width, + /// The [height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#height) media feature. + height, + /// The [aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#aspect-ratio) media feature. + @"aspect-ratio", + /// The [orientation](https://w3c.github.io/csswg-drafts/mediaqueries-5/#orientation) media feature. + orientation, + /// The [overflow-block](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-block) media feature. + @"overflow-block", + /// The [overflow-inline](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-inline) media feature. + @"overflow-inline", + /// The [horizontal-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#horizontal-viewport-segments) media feature. + @"horizontal-viewport-segments", + /// The [vertical-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#vertical-viewport-segments) media feature. + @"vertical-viewport-segments", + /// The [display-mode](https://w3c.github.io/csswg-drafts/mediaqueries-5/#display-mode) media feature. + @"display-mode", + /// The [resolution](https://w3c.github.io/csswg-drafts/mediaqueries-5/#resolution) media feature. + resolution, + /// The [scan](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scan) media feature. + scan, + /// The [grid](https://w3c.github.io/csswg-drafts/mediaqueries-5/#grid) media feature. + grid, + /// The [update](https://w3c.github.io/csswg-drafts/mediaqueries-5/#update) media feature. + update, + /// The [environment-blending](https://w3c.github.io/csswg-drafts/mediaqueries-5/#environment-blending) media feature. + @"environment-blending", + /// The [color](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color) media feature. + color, + /// The [color-index](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-index) media feature. + @"color-index", + /// The [monochrome](https://w3c.github.io/csswg-drafts/mediaqueries-5/#monochrome) media feature. + monochrome, + /// The [color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-gamut) media feature. + @"color-gamut", + /// The [dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#dynamic-range) media feature. + @"dynamic-range", + /// The [inverted-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#inverted-colors) media feature. + @"inverted-colors", + /// The [pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#pointer) media feature. + pointer, + /// The [hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#hover) media feature. + hover, + /// The [any-pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-pointer) media feature. + @"any-pointer", + /// The [any-hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-hover) media feature. + @"any-hover", + /// The [nav-controls](https://w3c.github.io/csswg-drafts/mediaqueries-5/#nav-controls) media feature. + @"nav-controls", + /// The [video-color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-color-gamut) media feature. + @"video-color-gamut", + /// The [video-dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-dynamic-range) media feature. + @"video-dynamic-range", + /// The [scripting](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scripting) media feature. + scripting, + /// The [prefers-reduced-motion](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-motion) media feature. + @"prefers-reduced-motion", + /// The [prefers-reduced-transparency](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-transparency) media feature. + @"prefers-reduced-transparency", + /// The [prefers-contrast](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-contrast) media feature. + @"prefers-contrast", + /// The [forced-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#forced-colors) media feature. + @"forced-colors", + /// The [prefers-color-scheme](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-color-scheme) media feature. + @"prefers-color-scheme", + /// The [prefers-reduced-data](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-data) media feature. + @"prefers-reduced-data", + /// The [device-width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-width) media feature. + @"device-width", + /// The [device-height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-height) media feature. + @"device-height", + /// The [device-aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-aspect-ratio) media feature. + @"device-aspect-ratio", + + /// The non-standard -webkit-device-pixel-ratio media feature. + @"-webkit-device-pixel-ratio", + /// The non-standard -moz-device-pixel-ratio media feature. + @"-moz-device-pixel-ratio", + + pub usingnamespace css.DeriveValueType(@This()); + + pub const ValueTypeMap = .{ + .width = MediaFeatureType.length, + .height = MediaFeatureType.length, + .@"aspect-ratio" = MediaFeatureType.ratio, + .orientation = MediaFeatureType.ident, + .@"overflow-block" = MediaFeatureType.ident, + .@"overflow-inline" = MediaFeatureType.ident, + .@"horizontal-viewport-segments" = MediaFeatureType.integer, + .@"vertical-viewport-segments" = MediaFeatureType.integer, + .@"display-mode" = MediaFeatureType.ident, + .resolution = MediaFeatureType.resolution, + .scan = MediaFeatureType.ident, + .grid = MediaFeatureType.boolean, + .update = MediaFeatureType.ident, + .@"environment-blending" = MediaFeatureType.ident, + .color = MediaFeatureType.integer, + .@"color-index" = MediaFeatureType.integer, + .monochrome = MediaFeatureType.integer, + .@"color-gamut" = MediaFeatureType.ident, + .@"dynamic-range" = MediaFeatureType.ident, + .@"inverted-colors" = MediaFeatureType.ident, + .pointer = MediaFeatureType.ident, + .hover = MediaFeatureType.ident, + .@"any-pointer" = MediaFeatureType.ident, + .@"any-hover" = MediaFeatureType.ident, + .@"nav-controls" = MediaFeatureType.ident, + .@"video-color-gamut" = MediaFeatureType.ident, + .@"video-dynamic-range" = MediaFeatureType.ident, + .scripting = MediaFeatureType.ident, + .@"prefers-reduced-motion" = MediaFeatureType.ident, + .@"prefers-reduced-transparency" = MediaFeatureType.ident, + .@"prefers-contrast" = MediaFeatureType.ident, + .@"forced-colors" = MediaFeatureType.ident, + .@"prefers-color-scheme" = MediaFeatureType.ident, + .@"prefers-reduced-data" = MediaFeatureType.ident, + .@"device-width" = MediaFeatureType.length, + .@"device-height" = MediaFeatureType.length, + .@"device-aspect-ratio" = MediaFeatureType.ratio, + .@"-webkit-device-pixel-ratio" = MediaFeatureType.number, + .@"-moz-device-pixel-ratio" = MediaFeatureType.number, + }; + + pub fn toCssWithPrefix( + this: *const MediaFeatureId, + prefix: []const u8, + comptime W: type, + dest: *Printer(W), + ) PrintErr!void { + switch (this.*) { + .@"-webkit-device-pixel-ratio" => { + return dest.writeFmt("-webkit-{s}device-pixel-ratio", .{prefix}); + }, + else => { + try dest.writeStr(prefix); + return this.toCss(W, dest); + }, + } + } + + pub inline fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + 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 QueryFeature(comptime FeatureId: type) type { + return union(enum) { + /// A plain media feature, e.g. `(min-width: 240px)`. + plain: struct { + /// The name of the feature. + name: MediaFeatureName(FeatureId), + /// The feature value. + value: MediaFeatureValue, + }, + + /// A boolean feature, e.g. `(hover)`. + boolean: struct { + /// The name of the feature. + name: MediaFeatureName(FeatureId), + }, + + /// A range, e.g. `(width > 240px)`. + range: struct { + /// The name of the feature. + name: MediaFeatureName(FeatureId), + /// A comparator. + operator: MediaFeatureComparison, + /// The feature value. + value: MediaFeatureValue, + }, + + /// An interval, e.g. `(120px < width < 240px)`. + interval: struct { + /// The name of the feature. + name: MediaFeatureName(FeatureId), + /// A start value. + start: MediaFeatureValue, + /// A comparator for the start value. + start_operator: MediaFeatureComparison, + /// The end value. + end: MediaFeatureValue, + /// A comparator for the end value. + end_operator: MediaFeatureComparison, + }, + + const This = @This(); + + pub fn needsParens(this: *const This, parent_operator: ?Operator, targets: *const css.Targets) bool { + return parent_operator != .@"and" and + this.* == .interval and + targets.shouldCompileSame(.media_interval_syntax); + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + try dest.writeChar('('); + + switch (this.*) { + .boolean => { + try this.boolean.name.toCss(W, dest); + }, + .plain => { + try this.plain.name.toCss(W, dest); + try dest.delim(':', false); + try this.plain.value.toCss(W, dest); + }, + .range => { + // If range syntax is unsupported, use min/max prefix if possible. + if (dest.targets.shouldCompileSame(.media_range_syntax)) { + return writeMinMax( + &this.range.operator, + FeatureId, + &this.range.name, + &this.range.value, + W, + dest, + ); + } + try this.range.name.toCss(W, dest); + try this.range.operator.toCss(W, dest); + try this.range.value.toCss(W, dest); + }, + .interval => |interval| { + if (dest.targets.shouldCompileSame(.media_interval_syntax)) { + try writeMinMax( + &interval.start_operator.opposite(), + FeatureId, + &interval.name, + &interval.start, + W, + dest, + ); + try dest.writeStr(" and ("); + return writeMinMax( + &interval.end_operator, + FeatureId, + &interval.name, + &interval.end, + W, + dest, + ); + } + + try interval.start.toCss(W, dest); + try interval.start_operator.toCss(W, dest); + try interval.name.toCss(W, dest); + try interval.end_operator.toCss(W, dest); + try interval.end.toCss(W, dest); + }, + } + + return dest.writeChar(')'); + } + + pub fn parse(input: *css.Parser) Result(This) { + switch (input.tryParse(parseNameFirst, .{})) { + .result => |res| { + return .{ .result = res }; + }, + .err => |e| { + if (e.kind == .custom and e.kind.custom == .invalid_media_query) { + return .{ .err = e }; + } + return parseValueFirst(input); + }, + } + return Result(This).success; + } + + pub fn parseNameFirst(input: *css.Parser) Result(This) { + const name, const legacy_op = switch (MediaFeatureName(FeatureId).parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + const operator = if (input.tryParse(consumeOperationOrColon, .{true}).asValue()) |operator| operator else return .{ + .result = .{ + .boolean = .{ .name = name }, + }, + }; + + if (operator != null and legacy_op != null) { + return .{ .err = input.newCustomError(css.ParserError.invalid_media_query) }; + } + + const value = switch (MediaFeatureValue.parse(input, name.valueType())) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (!value.checkType(name.valueType())) { + return .{ .err = input.newCustomError(css.ParserError.invalid_media_query) }; + } + + if (operator orelse legacy_op) |op| { + if (!name.valueType().allowsRanges()) { + return .{ .err = input.newCustomError(css.ParserError.invalid_media_query) }; + } + + return .{ .result = .{ + .range = .{ + .name = name, + .operator = op, + .value = value, + }, + } }; + } else { + return .{ .result = .{ + .plain = .{ + .name = name, + .value = value, + }, + } }; + } + } + + pub fn parseValueFirst(input: *css.Parser) Result(This) { + // We need to find the feature name first so we know the type. + const start = input.state(); + const name = name: { + while (true) { + if (MediaFeatureName(FeatureId).parse(input).asValue()) |result| { + const name: MediaFeatureName(FeatureId) = result[0]; + const legacy_op: ?MediaFeatureComparison = result[1]; + if (legacy_op != null) { + return .{ .err = input.newCustomError(css.ParserError.invalid_media_query) }; + } + break :name name; + } + if (input.isExhausted()) { + return .{ .err = input.newCustomError(css.ParserError.invalid_media_query) }; + } + } + }; + + input.reset(&start); + + // Now we can parse the first value. + const value = switch (MediaFeatureValue.parse(input, name.valueType())) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + const operator = switch (consumeOperationOrColon(input, false)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + // Skip over the feature name again. + { + const feature_name, const blah = switch (MediaFeatureName(FeatureId).parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + _ = blah; + bun.debugAssert(feature_name.eql(&name)); + } + + if (!name.valueType().allowsRanges() or !value.checkType(name.valueType())) { + return .{ .err = input.newCustomError(css.ParserError.invalid_media_query) }; + } + + if (input.tryParse(consumeOperationOrColon, .{false}).asValue()) |end_operator_| { + const start_operator = operator.?; + const end_operator = end_operator_.?; + // Start and end operators must be matching. + const GT: u8 = comptime @intFromEnum(MediaFeatureComparison.@"greater-than"); + const GTE: u8 = comptime @intFromEnum(MediaFeatureComparison.@"greater-than-equal"); + const LT: u8 = comptime @intFromEnum(MediaFeatureComparison.@"less-than"); + const LTE: u8 = comptime @intFromEnum(MediaFeatureComparison.@"less-than-equal"); + const check_val: u8 = @intFromEnum(start_operator) | @intFromEnum(end_operator); + switch (check_val) { + GT | GT, + GT | GTE, + GTE | GTE, + LT | LT, + LT | LTE, + LTE | LTE, + => {}, + else => return .{ .err = input.newCustomError(css.ParserError.invalid_media_query) }, + } + + const end_value = switch (MediaFeatureValue.parse(input, name.valueType())) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (!end_value.checkType(name.valueType())) { + return .{ .err = input.newCustomError(css.ParserError.invalid_media_query) }; + } + + return .{ .result = .{ + .interval = .{ + .name = name, + .start = value, + .start_operator = start_operator, + .end = end_value, + .end_operator = end_operator, + }, + } }; + } else { + const final_operator = operator.?.opposite(); + return .{ .result = .{ + .range = .{ + .name = name, + .operator = final_operator, + .value = value, + }, + } }; + } + } + }; +} + +/// Consumes an operation or a colon, or returns an error. +fn consumeOperationOrColon(input: *css.Parser, allow_colon: bool) Result(?MediaFeatureComparison) { + const location = input.currentSourceLocation(); + const first_delim = first_delim: { + const loc = input.currentSourceLocation(); + const next_token = switch (input.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (next_token.*) { + .colon => if (allow_colon) return .{ .result = null }, + .delim => |oper| break :first_delim oper, + else => {}, + } + return .{ .err = loc.newUnexpectedTokenError(next_token.*) }; + }; + + switch (first_delim) { + '=' => return .{ .result = .equal }, + '>' => { + if (input.tryParse(css.Parser.expectDelim, .{'='}).isOk()) { + return .{ .result = .@"greater-than-equal" }; + } + return .{ .result = .@"greater-than" }; + }, + '<' => { + if (input.tryParse(css.Parser.expectDelim, .{'='}).isOk()) { + return .{ .result = .@"less-than-equal" }; + } + return .{ .result = .@"less-than" }; + }, + else => return .{ .err = location.newUnexpectedTokenError(.{ .delim = first_delim }) }, + } +} + +pub const MediaFeatureComparison = enum(u8) { + /// `=` + equal = 1, + /// `>` + @"greater-than" = 2, + /// `>=` + @"greater-than-equal" = 4, + /// `<` + @"less-than" = 8, + /// `<=` + @"less-than-equal" = 16, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + 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 opposite(self: @This()) @This() { + return switch (self) { + .@"greater-than" => .@"less-than", + .@"greater-than-equal" => .@"less-than-equal", + .@"less-than" => .@"greater-than", + .@"less-than-equal" => .@"greater-than-equal", + .equal => .equal, + }; + } +}; + +/// [media feature value](https://drafts.csswg.org/mediaqueries/#typedef-mf-value) within a media query. +/// +/// See [MediaFeature](MediaFeature). +pub const MediaFeatureValue = union(enum) { + /// A length value. + length: Length, + /// A number value. + number: CSSNumber, + /// An integer value. + integer: CSSInteger, + /// A boolean value. + boolean: bool, + /// A resolution. + resolution: Resolution, + /// A ratio. + ratio: Ratio, + /// An identifier. + ident: Ident, + /// An environment variable reference. + env: EnvironmentVariable, + + pub fn deepClone(this: *const MediaFeatureValue, allocator: std.mem.Allocator) MediaFeatureValue { + return switch (this.*) { + .length => |*l| .{ .length = l.deepClone(allocator) }, + .number => |n| .{ .number = n }, + .integer => |i| .{ .integer = i }, + .boolean => |b| .{ .boolean = b }, + .resolution => |r| .{ .resolution = r }, + .ratio => |r| .{ .ratio = r }, + .ident => |i| .{ .ident = i }, + .env => |*e| .{ .env = e.deepClone(allocator) }, + }; + } + + pub fn deinit(this: *MediaFeatureValue, allocator: std.mem.Allocator) void { + return switch (this.*) { + .length => |l| l.deinit(allocator), + .number => {}, + .integer => {}, + .boolean => {}, + .resolution => {}, + .ratio => {}, + .ident => {}, + .env => |*env| env.deinit(allocator), + }; + } + + pub fn toCss( + this: *const MediaFeatureValue, + comptime W: type, + dest: *Printer(W), + ) PrintErr!void { + switch (this.*) { + .length => |len| return len.toCss(W, dest), + .number => |num| return CSSNumberFns.toCss(&num, W, dest), + .integer => |int| return CSSIntegerFns.toCss(&int, W, dest), + .boolean => |b| { + if (b) { + return dest.writeChar('1'); + } else { + return dest.writeChar('0'); + } + }, + .resolution => |res| return res.toCss(W, dest), + .ratio => |ratio| return ratio.toCss(W, dest), + .ident => |id| return IdentFns.toCss(&id, W, dest), + .env => |*env| return EnvironmentVariable.toCss(env, W, dest, false), + } + } + + pub fn checkType(this: *const @This(), expected_type: MediaFeatureType) bool { + const vt = this.valueType(); + if (expected_type == .unknown or vt == .unknown) return true; + return expected_type == vt; + } + + /// Parses a single media query feature value, with an expected type. + /// If the type is unknown, pass MediaFeatureType::Unknown instead. + pub fn parse(input: *css.Parser, expected_type: MediaFeatureType) Result(MediaFeatureValue) { + if (input.tryParse(parseKnown, .{expected_type}).asValue()) |value| { + return .{ .result = value }; + } + + return parseUnknown(input); + } + + pub fn parseKnown(input: *css.Parser, expected_type: MediaFeatureType) Result(MediaFeatureValue) { + return .{ + .result = switch (expected_type) { + .boolean => { + const value = switch (CSSIntegerFns.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (value != 0 and value != 1) return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + return .{ .result = .{ .boolean = value == 1 } }; + }, + .number => .{ .number = switch (CSSNumberFns.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } }, + .integer => .{ .integer = switch (CSSIntegerFns.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } }, + .length => .{ .length = switch (Length.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } }, + .resolution => .{ .resolution = switch (Resolution.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } }, + .ratio => .{ .ratio = switch (Ratio.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } }, + .ident => .{ .ident = switch (IdentFns.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } }, + .unknown => return .{ .err = input.newCustomError(.invalid_value) }, + }, + }; + } + + pub fn parseUnknown(input: *css.Parser) Result(MediaFeatureValue) { + // Ratios are ambiguous with numbers because the second param is optional (e.g. 2/1 == 2). + // We require the / delimiter when parsing ratios so that 2/1 ends up as a ratio and 2 is + // parsed as a number. + if (input.tryParse(Ratio.parseRequired, .{}).asValue()) |ratio| return .{ .result = .{ .ratio = ratio } }; + + // Parse number next so that unitless values are not parsed as lengths. + if (input.tryParse(CSSNumberFns.parse, .{}).asValue()) |num| return .{ .result = .{ .number = num } }; + + if (input.tryParse(Length.parse, .{}).asValue()) |res| return .{ .result = .{ .length = res } }; + + if (input.tryParse(Resolution.parse, .{}).asValue()) |res| return .{ .result = .{ .resolution = res } }; + + if (input.tryParse(EnvironmentVariable.parse, .{}).asValue()) |env| return .{ .result = .{ .env = env } }; + + const ident = switch (IdentFns.parse(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = .{ .ident = ident } }; + } + + pub fn addF32(this: MediaFeatureValue, allocator: Allocator, other: f32) MediaFeatureValue { + return switch (this) { + .length => |len| .{ .length = len.add(allocator, Length.px(other)) }, + // .length => |len| .{ + // .length = .{ + // .value = .{ .px = other }, + // }, + // }, + .number => |num| .{ .number = num + other }, + .integer => |num| .{ .integer = num + if (css.signfns.isSignPositive(other)) @as(i32, 1) else @as(i32, -1) }, + .boolean => |v| .{ .boolean = v }, + .resolution => |res| .{ .resolution = res.addF32(allocator, other) }, + .ratio => |ratio| .{ .ratio = ratio.addF32(allocator, other) }, + .ident => |id| .{ .ident = id }, + .env => |env| .{ .env = env }, // TODO: calc support + }; + } + + pub fn valueType(this: *const MediaFeatureValue) MediaFeatureType { + return switch (this.*) { + .length => .length, + .number => .number, + .integer => .integer, + .boolean => .boolean, + .resolution => .resolution, + .ratio => .ratio, + .ident => .ident, + .env => .unknown, + }; + } +}; + +/// The type of a media feature. +pub const MediaFeatureType = enum { + /// A length value. + length, + /// A number value. + number, + /// An integer value. + integer, + /// A boolean value, either 0 or 1. + boolean, + /// A resolution. + resolution, + /// A ratio. + ratio, + /// An identifier. + ident, + /// An unknown type. + unknown, + + pub fn allowsRanges(this: MediaFeatureType) bool { + return switch (this) { + .length, .number, .integer, .resolution, .ratio, .unknown => true, + .boolean, .ident => false, + }; + } +}; + +pub fn MediaFeatureName(comptime FeatureId: type) type { + return union(enum) { + /// A standard media query feature identifier. + standard: FeatureId, + + /// A custom author-defined environment variable. + custom: DashedIdent, + + /// An unknown environment variable. + unknown: Ident, + + const This = @This(); + + pub fn eql(lhs: *const This, rhs: *const This) bool { + if (@intFromEnum(lhs.*) != @intFromEnum(rhs.*)) return false; + return switch (lhs.*) { + .standard => |fid| fid == rhs.standard, + .custom => |ident| bun.strings.eql(ident.v, rhs.custom.v), + .unknown => |ident| bun.strings.eql(ident.v, rhs.unknown.v), + }; + } + + pub fn valueType(this: *const This) MediaFeatureType { + return switch (this.*) { + .standard => |standard| standard.valueType(), + else => .unknown, + }; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.*) { + .standard => |v| v.toCss(W, dest), + .custom => |d| DashedIdentFns.toCss(&d, W, dest), + .unknown => |v| IdentFns.toCss(&v, W, dest), + }; + } + + pub fn toCssWithPrefix(this: *const This, prefix: []const u8, comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.*) { + .standard => |v| v.toCssWithPrefix(prefix, W, dest), + .custom => |d| { + try dest.writeStr(prefix); + return DashedIdentFns.toCss(&d, W, dest); + }, + .unknown => |v| { + try dest.writeStr(prefix); + return IdentFns.toCss(&v, W, dest); + }, + }; + } + + /// Parses a media feature name. + pub fn parse(input: *css.Parser) Result(struct { This, ?MediaFeatureComparison }) { + const ident = switch (input.expectIdent()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + if (bun.strings.startsWith(ident, "--")) { + return .{ .result = .{ + .{ + .custom = .{ .v = ident }, + }, + null, + } }; + } + + var name = ident; + + // Webkit places its prefixes before "min" and "max". Remove it first, and + // re-add after removing min/max. + const is_webkit = bun.strings.startsWithCaseInsensitiveAscii(name, "-webkit-"); + if (is_webkit) { + name = name[8..]; + } + + const comparator: ?MediaFeatureComparison = comparator: { + if (bun.strings.startsWithCaseInsensitiveAscii(name, "min-")) { + name = name[4..]; + break :comparator .@"greater-than-equal"; + } else if (bun.strings.startsWithCaseInsensitiveAscii(name, "max-")) { + name = name[4..]; + break :comparator .@"less-than-equal"; + } else break :comparator null; + }; + + var free_str = false; + const final_name = if (is_webkit) name: { + // PERF: stack buffer here? + free_str = true; + break :name std.fmt.allocPrint(input.allocator(), "-webkit-{s}", .{name}) catch bun.outOfMemory(); + } else name; + + defer if (is_webkit) { + // If we made an allocation let's try to free it, + // this only works if FeatureId doesn't hold any references to the input string. + // i.e. it is an enum + comptime { + std.debug.assert(@typeInfo(FeatureId) == .Enum); + } + input.allocator().free(final_name); + }; + + if (css.parse_utility.parseString( + input.allocator(), + FeatureId, + final_name, + FeatureId.parse, + ).asValue()) |standard| { + return .{ .result = .{ + .{ .standard = standard }, + comparator, + } }; + } + + return .{ .result = .{ + .{ + .unknown = .{ .v = ident }, + }, + null, + } }; + } + }; +} + +fn writeMinMax( + operator: *const MediaFeatureComparison, + comptime FeatureId: type, + name: *const MediaFeatureName(FeatureId), + value: *const MediaFeatureValue, + comptime W: type, + dest: *Printer(W), +) PrintErr!void { + const prefix = switch (operator.*) { + .@"greater-than", .@"greater-than-equal" => "min-", + .@"less-than", .@"less-than-equal" => "max-", + .equal => null, + }; + + if (prefix) |p| { + try name.toCssWithPrefix(p, W, dest); + } else { + try name.toCss(W, dest); + } + + try dest.delim(':', false); + + var adjusted: ?MediaFeatureValue = switch (operator.*) { + .@"greater-than" => value.deepClone(dest.allocator).addF32(dest.allocator, 0.001), + .@"less-than" => value.deepClone(dest.allocator).addF32(dest.allocator, -0.001), + else => null, + }; + + if (adjusted) |*val| { + defer val.deinit(dest.allocator); + try val.toCss(W, dest); + } else { + try value.toCss(W, dest); + } + + return dest.writeChar(')'); +} diff --git a/src/css/prefixes.zig b/src/css/prefixes.zig new file mode 100644 index 0000000000000..2a9b29edafc83 --- /dev/null +++ b/src/css/prefixes.zig @@ -0,0 +1,2201 @@ +// This file is autogenerated by build-prefixes.js. DO NOT EDIT! + +const css = @import("./css_parser.zig"); +const VendorPrefix = css.VendorPrefix; +const Browsers = css.targets.Browsers; + +pub const Feature = enum { + align_content, + align_items, + align_self, + animation, + animation_delay, + animation_direction, + animation_duration, + animation_fill_mode, + animation_iteration_count, + animation_name, + animation_play_state, + animation_timing_function, + any_pseudo, + appearance, + at_keyframes, + at_resolution, + at_viewport, + backdrop_filter, + backface_visibility, + background_clip, + background_origin, + background_size, + border_block_end, + border_block_start, + border_bottom_left_radius, + border_bottom_right_radius, + border_image, + border_inline_end, + border_inline_start, + border_radius, + border_top_left_radius, + border_top_right_radius, + box_decoration_break, + box_shadow, + box_sizing, + break_after, + break_before, + break_inside, + calc, + clip_path, + color_adjust, + column_count, + column_fill, + column_gap, + column_rule, + column_rule_color, + column_rule_style, + column_rule_width, + column_span, + column_width, + columns, + cross_fade, + display_flex, + display_grid, + element, + fill, + fill_available, + filter, + filter_function, + fit_content, + flex, + flex_basis, + flex_direction, + flex_flow, + flex_grow, + flex_shrink, + flex_wrap, + flow_from, + flow_into, + font_feature_settings, + font_kerning, + font_language_override, + font_variant_ligatures, + grab, + grabbing, + grid_area, + grid_column, + grid_column_align, + grid_column_end, + grid_column_start, + grid_row, + grid_row_align, + grid_row_end, + grid_row_start, + grid_template, + grid_template_areas, + grid_template_columns, + grid_template_rows, + hyphens, + image_rendering, + image_set, + inline_flex, + inline_grid, + isolate, + isolate_override, + justify_content, + linear_gradient, + margin_block_end, + margin_block_start, + margin_inline_end, + margin_inline_start, + mask, + mask_border, + mask_border_outset, + mask_border_repeat, + mask_border_slice, + mask_border_source, + mask_border_width, + mask_clip, + mask_composite, + mask_image, + mask_origin, + mask_position, + mask_repeat, + mask_size, + max_content, + min_content, + object_fit, + object_position, + order, + overscroll_behavior, + padding_block_end, + padding_block_start, + padding_inline_end, + padding_inline_start, + perspective, + perspective_origin, + pixelated, + place_self, + plaintext, + print_color_adjust, + pseudo_class_any_link, + pseudo_class_autofill, + pseudo_class_fullscreen, + pseudo_class_placeholder_shown, + pseudo_class_read_only, + pseudo_class_read_write, + pseudo_element_backdrop, + pseudo_element_file_selector_button, + pseudo_element_placeholder, + pseudo_element_selection, + radial_gradient, + region_fragment, + repeating_linear_gradient, + repeating_radial_gradient, + scroll_snap_coordinate, + scroll_snap_destination, + scroll_snap_points_x, + scroll_snap_points_y, + scroll_snap_type, + shape_image_threshold, + shape_margin, + shape_outside, + sticky, + stretch, + tab_size, + text_align_last, + text_decoration, + text_decoration_color, + text_decoration_line, + text_decoration_skip, + text_decoration_skip_ink, + text_decoration_style, + text_emphasis, + text_emphasis_color, + text_emphasis_position, + text_emphasis_style, + text_orientation, + text_overflow, + text_size_adjust, + text_spacing, + touch_action, + transform, + transform_origin, + transform_style, + transition, + transition_delay, + transition_duration, + transition_property, + transition_timing_function, + user_select, + writing_mode, + zoom_in, + zoom_out, + + pub fn prefixesFor(this: *const Feature, browsers: Browsers) VendorPrefix { + var prefixes = VendorPrefix{ .none = true }; + switch (this.*) { + .border_radius, .border_top_left_radius, .border_top_right_radius, .border_bottom_right_radius, .border_bottom_left_radius => { + if (browsers.android) |version| { + if (version <= 131328) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version <= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 131072 and version <= 198144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version <= 197120) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .box_shadow => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 196608) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 589824) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 197888 and version <= 198144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 262656) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 327680) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .animation, .animation_name, .animation_duration, .animation_delay, .animation_direction, .animation_fill_mode, .animation_iteration_count, .animation_play_state, .animation_timing_function, .at_keyframes => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 2752512) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 327680 and version <= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 524544) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version == 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + if (version >= 983040 and version <= 1900544) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 262144 and version <= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .transition, .transition_property, .transition_duration, .transition_delay, .transition_timing_function => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 262656) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 1638400) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 262144 and version <= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 393216) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 655360 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 393216) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .transform, .transform_origin => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 2293760) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 197888 and version <= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ie) |version| { + if (version <= 589824) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 524544) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 656640 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + if (version >= 983040 and version <= 1441792) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .perspective, .perspective_origin, .transform_style => { + if (browsers.android) |version| { + if (version >= 196608 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 786432 and version <= 2293760) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 655360 and version <= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 524544) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 1441792) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 262144 and version <= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .backface_visibility => { + if (browsers.android) |version| { + if (version >= 196608 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 786432 and version <= 2293760) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 655360 and version <= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 983552) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 1441792) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 262144 and version <= 983552) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .linear_gradient, .repeating_linear_gradient, .radial_gradient, .repeating_radial_gradient => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 262656) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 1638400) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 198144 and version <= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 393216) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 721152 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + } + if (browsers.safari) |version| { + if (version >= 262144 and version <= 393216) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .box_sizing => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 196608) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 589824) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 131072 and version <= 1835008) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 262656) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 327680) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .filter => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 1179648 and version <= 3407872) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 393216 and version <= 589824) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2555904) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393216 and version <= 589824) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 393728) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .filter_function => { + if (browsers.ios_saf) |version| { + if (version >= 589824 and version <= 590592) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version <= 589824) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .backdrop_filter => { + if (browsers.edge) |version| { + if (version >= 1114112 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 589824 and version <= 1115648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 589824 and version <= 1115648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .element => { + if (browsers.firefox) |version| { + if (version >= 131072) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + }, + .columns, .column_width, .column_gap, .column_rule, .column_rule_color, .column_rule_width, .column_count, .column_rule_style, .column_span, .column_fill => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 3211264) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 131072 and version <= 3342336) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 524544) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2359296) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version <= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .break_before, .break_after, .break_inside => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 3211264) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 524544) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2359296) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version <= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .user_select => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 3473408) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 131072 and version <= 4456448) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2621440) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 327680) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .display_flex, .inline_flex, .flex, .flex_grow, .flex_shrink, .flex_basis, .flex_direction, .flex_wrap, .flex_flow, .justify_content, .order, .align_items, .align_self, .align_content => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 262656) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 1835008) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 131072 and version <= 1376256) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ie) |version| { + if (version == 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 524544) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 1048576) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .calc => { + if (browsers.chrome) |version| { + if (version >= 1245184 and version <= 1638400) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 262144 and version <= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version <= 393216) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version <= 393216) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .background_origin, .background_size => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 131840) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version <= 198144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.opera) |version| { + if (version <= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + } + }, + .background_clip => { + if (browsers.android) |version| { + if (version >= 262144 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 7798784) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 786432 and version <= 917504) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + if (version >= 5177344 and version <= 7798784) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 393216 and version <= 852992) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 6881280) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 262144 and version <= 852224) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 1572864) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .font_feature_settings, .font_variant_ligatures, .font_language_override => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 1048576 and version <= 3080192) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 262144 and version <= 2162688) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2228224) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version <= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .font_kerning => { + if (browsers.android) |version| { + if (version <= 263168) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 1900544 and version <= 2097152) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 524288 and version <= 721664) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 1048576 and version <= 1245184) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 458752 and version <= 589824) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .border_image => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 262656) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 917504) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 197888 and version <= 917504) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 327680) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 720896 and version <= 786688) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 327936) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .pseudo_element_selection => { + if (browsers.firefox) |version| { + if (version >= 131072 and version <= 3997696) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + }, + .pseudo_element_placeholder => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 3670016) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 1179648 and version <= 3276800) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 262656 and version <= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2818048) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 327680 and version <= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 393728) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .pseudo_class_placeholder_shown => { + if (browsers.firefox) |version| { + if (version >= 262144 and version <= 3276800) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + }, + .hyphens => { + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 393216 and version <= 2752512) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 262656 and version <= 1050112) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 327936 and version <= 1050112) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .pseudo_class_fullscreen => { + if (browsers.chrome) |version| { + if (version >= 983040 and version <= 4587520) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 655360 and version <= 4128768) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ie) |version| { + if (version >= 720896) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 4128768) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 327936 and version <= 1049344) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 590336) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .pseudo_element_backdrop => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 2097152 and version <= 2359296) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ie != null) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + if (browsers.opera) |version| { + if (version >= 1245184 and version <= 1507328) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .pseudo_element_file_selector_button => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 5767168) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + if (version >= 5177344 and version <= 5767168) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 917504) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 4849664) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 917504) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 917504) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .pseudo_class_autofill => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 7143424) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 5177344 and version <= 7143424) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 918784) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 6225920) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 917760) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 1310720) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .tab_size => { + if (browsers.firefox) |version| { + if (version >= 262144 and version <= 5898240) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.opera) |version| { + if (version >= 656896 and version <= 786688) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + } + }, + .max_content, .min_content => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 1441792 and version <= 2949120) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 196608 and version <= 4259840) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 458752 and version <= 852992) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2097152) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393472 and version <= 655616) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version <= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .fill, .fill_available => { + if (browsers.chrome) |version| { + if (version >= 1441792) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.android) |version| { + if (version >= 263168) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 5177344) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 196608 and version <= 4259840) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 458752 and version <= 852992) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393472 and version <= 655616) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .fit_content => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 1441792 and version <= 2949120) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 196608 and version <= 6094848) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 458752 and version <= 852992) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2097152) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393472 and version <= 655616) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version <= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .stretch => { + if (browsers.chrome) |version| { + if (version >= 1441792) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 196608) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.android) |version| { + if (version >= 263168) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 5177344) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 458752) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 458752) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 327680) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .zoom_in, .zoom_out => { + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 2359296) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 131072 and version <= 1507328) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 1507328) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .grab, .grabbing => { + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 4390912) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 131072 and version <= 1703936) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 3538944) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 655616) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .sticky => { + if (browsers.ios_saf) |version| { + if (version >= 393216 and version <= 786944) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393472 and version <= 786688) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .touch_action => { + if (browsers.ie) |version| { + if (version == 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + }, + .text_decoration_skip, .text_decoration_skip_ink => { + if (browsers.ios_saf) |version| { + if (version >= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 459008 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .text_decoration => { + if (browsers.ios_saf) |version| { + if (version >= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .text_decoration_color, .text_decoration_line, .text_decoration_style => { + if (browsers.firefox) |version| { + if (version >= 393216 and version <= 2293760) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 524288 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 524288 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .text_size_adjust => { + if (browsers.firefox != null) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 327680) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .mask_clip, .mask_composite, .mask_image, .mask_origin, .mask_repeat, .mask_border_repeat, .mask_border_source, .mask, .mask_position, .mask_size, .mask_border, .mask_border_outset, .mask_border_width, .mask_border_slice => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 7798784) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 5177344 and version <= 7798784) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 983552) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 6881280) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 262144 and version <= 983552) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 1572864) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .clip_path => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 1572864 and version <= 3538944) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 458752 and version <= 589824) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2686976) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 458752 and version <= 589824) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 327680) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .box_decoration_break => { + if (browsers.chrome) |version| { + if (version >= 1441792) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.android) |version| { + if (version >= 263168) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 5177344) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 458752) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393472) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .object_fit, .object_position => { + if (browsers.opera) |version| { + if (version >= 656896 and version <= 786688) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + } + }, + .shape_margin, .shape_outside, .shape_image_threshold => { + if (browsers.ios_saf) |version| { + if (version >= 524288 and version <= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 459008 and version <= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .text_overflow => { + if (browsers.opera) |version| { + if (version >= 589824 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + } + }, + .at_viewport => { + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.opera) |version| { + if (version >= 720896 and version <= 786688) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + } + }, + .at_resolution => { + if (browsers.android) |version| { + if (version >= 131840 and version <= 262656) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 1835008) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 197888 and version <= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 262144 and version <= 984576) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 591104 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + } + if (browsers.safari) |version| { + if (version >= 262144 and version <= 984576) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .text_align_last => { + if (browsers.firefox) |version| { + if (version >= 786432 and version <= 3145728) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + }, + .pixelated => { + if (browsers.firefox) |version| { + if (version >= 198144 and version <= 4194304) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 327680 and version <= 393216) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 722432 and version <= 786688) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .o = true }); + } + } + if (browsers.safari) |version| { + if (version <= 393216) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .image_rendering => { + if (browsers.ie) |version| { + if (version >= 458752) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + }, + .border_inline_start, .border_inline_end, .margin_inline_start, .margin_inline_end, .padding_inline_start, .padding_inline_end => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 4456448) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 196608 and version <= 2621440) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 3604480) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 590336) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .border_block_start, .border_block_end, .margin_block_start, .margin_block_end, .padding_block_start, .padding_block_end => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 4456448) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 3604480) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 786432) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 590336) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .appearance => { + if (browsers.android) |version| { + if (version >= 131328 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 5439488) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + if (version >= 5177344 and version <= 5439488) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 131072 and version <= 5177344) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ie != null) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 983552) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 4718592) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 983552) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 851968) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .scroll_snap_type, .scroll_snap_coordinate, .scroll_snap_destination, .scroll_snap_points_x, .scroll_snap_points_y => { + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 589824 and version <= 656128) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 589824 and version <= 655616) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .flow_into, .flow_from, .region_fragment => { + if (browsers.chrome) |version| { + if (version >= 983040 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 458752 and version <= 720896) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393472 and version <= 720896) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .image_set => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 1376256 and version <= 7340032) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 5177344 and version <= 7340032) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 393216 and version <= 590592) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 6422528) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393216 and version <= 590080) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 1441792) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .writing_mode => { + if (browsers.android) |version| { + if (version >= 196608 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 524288 and version <= 3080192) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ie) |version| { + if (version >= 328960) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 327680 and version <= 656128) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2228224) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 327936 and version <= 655616) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version <= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .cross_fade => { + if (browsers.chrome) |version| { + if (version >= 1114112) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.android) |version| { + if (version >= 263168) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 5177344) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 327680 and version <= 590592) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 327936 and version <= 590080) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .pseudo_class_read_only, .pseudo_class_read_write => { + if (browsers.firefox) |version| { + if (version >= 196608 and version <= 5046272) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + }, + .text_emphasis, .text_emphasis_position, .text_emphasis_style, .text_emphasis_color => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 1638400 and version <= 6422528) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 5177344 and version <= 6422528) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 5570560) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393472 and version <= 458752) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144 and version <= 1114112) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .display_grid, .inline_grid, .grid_template_columns, .grid_template_rows, .grid_row_start, .grid_column_start, .grid_row_end, .grid_column_end, .grid_row, .grid_column, .grid_area, .grid_template, .grid_template_areas, .place_self, .grid_column_align, .grid_row_align => { + if (browsers.edge) |version| { + if (version >= 786432 and version <= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + }, + .text_spacing => { + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1179648) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ie) |version| { + if (version >= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + }, + .pseudo_class_any_link => { + if (browsers.android) |version| { + if (version >= 263168 and version <= 263171) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.chrome) |version| { + if (version >= 983040 and version <= 4194304) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 196608 and version <= 3211264) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 393216 and version <= 524544) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 3342336) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393472 and version <= 524288) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 327680 and version <= 524800) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .isolate => { + if (browsers.chrome) |version| { + if (version >= 1048576 and version <= 3080192) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 655360 and version <= 3211264) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 393216 and version <= 656128) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040 and version <= 2228224) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393216 and version <= 655616) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .plaintext => { + if (browsers.firefox) |version| { + if (version >= 655360 and version <= 3211264) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 393216 and version <= 656128) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393216 and version <= 655616) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .isolate_override => { + if (browsers.firefox) |version| { + if (version >= 1114112 and version <= 3211264) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 458752 and version <= 656128) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 458752 and version <= 655616) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .overscroll_behavior => { + if (browsers.edge) |version| { + if (version >= 786432 and version <= 1114112) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + if (browsers.ie) |version| { + if (version >= 655360) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .ms = true }); + } + } + }, + .text_orientation => { + if (browsers.safari) |version| { + if (version >= 655616 and version <= 852224) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .print_color_adjust, .color_adjust => { + if (browsers.chrome) |version| { + if (version >= 1114112) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.android) |version| { + if (version >= 263168) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 5177344) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 3145728 and version <= 6291456) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 393216 and version <= 983552) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.opera) |version| { + if (version >= 983040) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 393216 and version <= 983552) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 262144) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + .any_pseudo => { + if (browsers.chrome) |version| { + if (version >= 786432 and version <= 5701632) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.edge) |version| { + if (version >= 5177344 and version <= 5701632) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.firefox) |version| { + if (version >= 262144 and version <= 5111808) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .moz = true }); + } + } + if (browsers.opera) |version| { + if (version >= 917504 and version <= 4784128) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.safari) |version| { + if (version >= 327680 and version <= 851968) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.ios_saf) |version| { + if (version >= 327680 and version <= 851968) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.samsung) |version| { + if (version >= 65536 and version <= 917504) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + if (browsers.android) |version| { + if (version >= 2424832 and version <= 5701632) { + prefixes = prefixes.bitwiseOr(VendorPrefix{ .webkit = true }); + } + } + }, + } + return prefixes; + } + + pub fn isFlex2009(browsers: Browsers) bool { + if (browsers.android) |version| { + if (version >= 131328 and version <= 262656) { + return true; + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 1310720) { + return true; + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 393216) { + return true; + } + } + if (browsers.safari) |version| { + if (version >= 196864 and version <= 393216) { + return true; + } + } + return false; + } + + pub fn isWebkitGradient(browsers: Browsers) bool { + if (browsers.android) |version| { + if (version >= 131328 and version <= 196608) { + return true; + } + } + if (browsers.chrome) |version| { + if (version >= 262144 and version <= 589824) { + return true; + } + } + if (browsers.ios_saf) |version| { + if (version >= 197120 and version <= 393216) { + return true; + } + } + if (browsers.safari) |version| { + if (version >= 262144 and version <= 393216) { + return true; + } + } + return false; + } +}; diff --git a/src/css/printer.zig b/src/css/printer.zig new file mode 100644 index 0000000000000..3b6b078b76027 --- /dev/null +++ b/src/css/printer.zig @@ -0,0 +1,412 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); +const DashedIdent = css_values.ident.DashedIdent; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; +const Location = css.Location; +const PrintErr = css.PrintErr; + +const ArrayList = std.ArrayListUnmanaged; + +const sourcemap = @import("./sourcemap.zig"); + +/// Options that control how CSS is serialized to a string. +pub const PrinterOptions = struct { + /// Whether to minify the CSS, i.e. remove white space. + minify: bool = false, + /// An optional reference to a source map to write mappings into. + /// (Available when the `sourcemap` feature is enabled.) + source_map: ?*sourcemap.SourceMap = null, + /// An optional project root path, used to generate relative paths for sources used in CSS module hashes. + project_root: ?[]const u8 = null, + /// Targets to output the CSS for. + targets: Targets = .{}, + /// Whether to analyze dependencies (i.e. `@import` and `url()`). + /// If true, the dependencies are returned as part of the + /// [ToCssResult](super::stylesheet::ToCssResult). + /// + /// When enabled, `@import` and `url()` dependencies + /// are replaced with hashed placeholders that can be replaced with the final + /// urls later (after bundling). + analyze_dependencies: ?css.dependencies.DependencyOptions = null, + /// A mapping of pseudo classes to replace with class names that can be applied + /// from JavaScript. Useful for polyfills, for example. + pseudo_classes: ?PseudoClasses = null, +}; + +/// A mapping of user action pseudo classes to replace with class names. +/// +/// See [PrinterOptions](PrinterOptions). +const PseudoClasses = struct { + /// The class name to replace `:hover` with. + hover: ?[]const u8 = null, + /// The class name to replace `:active` with. + active: ?[]const u8 = null, + /// The class name to replace `:focus` with. + focus: ?[]const u8 = null, + /// The class name to replace `:focus-visible` with. + focus_visible: ?[]const u8 = null, + /// The class name to replace `:focus-within` with. + focus_within: ?[]const u8 = null, +}; + +pub const Targets = css.targets.Targets; + +pub const Features = css.targets.Features; + +const Browsers = css.targets.Browsers; + +/// A `Printer` represents a destination to output serialized CSS, as used in +/// the [ToCss](super::traits::ToCss) trait. It can wrap any destination that +/// implements [std::fmt::Write](std::fmt::Write), such as a [String](String). +/// +/// A `Printer` keeps track of the current line and column position, and uses +/// this to generate a source map if provided in the options. +/// +/// `Printer` also includes helper functions that assist with writing output +/// that respects options such as `minify`, and `css_modules`. +pub fn Printer(comptime Writer: type) type { + return struct { + // #[cfg(feature = "sourcemap")] + sources: ?*const ArrayList([]const u8), + dest: Writer, + loc: Location = Location{ + .source_index = 0, + .line = 0, + .column = 1, + }, + indent_amt: u8 = 0, + line: u32 = 0, + col: u32 = 0, + minify: bool, + targets: Targets, + vendor_prefix: css.VendorPrefix = css.VendorPrefix.empty(), + in_calc: bool = false, + css_module: ?css.CssModule = null, + dependencies: ?ArrayList(css.Dependency) = null, + remove_imports: bool, + pseudo_classes: ?PseudoClasses = null, + indentation_buf: std.ArrayList(u8), + ctx: ?*const css.StyleContext = null, + scratchbuf: std.ArrayList(u8), + error_kind: ?css.PrinterError = null, + /// NOTE This should be the same mimalloc heap arena allocator + allocator: Allocator, + // TODO: finish the fields + + const This = @This(); + + /// Returns the current source filename that is being printed. + pub fn filename(this: *const This) []const u8 { + if (this.sources) |sources| { + if (this.loc.source_index < sources.items.len) return sources.items[this.loc.source_index]; + } + return "unknown.css"; + } + + /// Returns whether the indent level is greater than one. + pub fn isNested(this: *const This) bool { + return this.indent_amt > 2; + } + + /// Add an error related to std lib fmt errors + pub fn addFmtError(this: *This) PrintErr!void { + this.error_kind = css.PrinterError{ + .kind = .fmt_error, + .loc = null, + }; + } + + /// Returns an error of the given kind at the provided location in the current source file. + pub fn newError( + this: *This, + kind: css.PrinterErrorKind, + maybe_loc: ?css.dependencies.Location, + ) PrintErr!void { + bun.debugAssert(this.error_kind == null); + this.error_kind = css.PrinterError{ + .kind = kind, + .loc = if (maybe_loc) |loc| css.ErrorLocation{ + .filename = this.filename(), + .line = loc.line - 1, + .column = loc.column, + } else null, + }; + return PrintErr.lol; + } + + pub fn deinit(this: *This) void { + this.scratchbuf.deinit(); + this.indentation_buf.deinit(); + if (this.dependencies) |*dependencies| { + dependencies.deinit(this.allocator); + } + } + + pub fn new(allocator: Allocator, scratchbuf: std.ArrayList(u8), dest: Writer, options: PrinterOptions) This { + return .{ + .sources = null, + .dest = dest, + .minify = options.minify, + .targets = options.targets, + .dependencies = if (options.analyze_dependencies != null) ArrayList(css.Dependency){} else null, + .remove_imports = options.analyze_dependencies != null and options.analyze_dependencies.?.remove_imports, + .pseudo_classes = options.pseudo_classes, + .indentation_buf = std.ArrayList(u8).init(allocator), + .scratchbuf = scratchbuf, + .allocator = allocator, + .loc = Location{ + .source_index = 0, + .line = 0, + .column = 1, + }, + }; + } + + pub fn context(this: *const Printer(Writer)) ?*const css.StyleContext { + return this.ctx; + } + + /// To satisfy io.Writer interface + /// + /// NOTE: Same constraints as `writeStr`, the `str` param is assumted to not + /// contain any newline characters + pub fn writeAll(this: *This, str: []const u8) !void { + return this.writeStr(str) catch std.mem.Allocator.Error.OutOfMemory; + } + + /// Writes a raw string to the underlying destination. + /// + /// NOTE: Is is assumed that the string does not contain any newline characters. + /// If such a string is written, it will break source maps. + pub fn writeStr(this: *This, s: []const u8) PrintErr!void { + if (comptime bun.Environment.isDebug) { + bun.assert(std.mem.indexOfScalar(u8, s, '\n') == null); + } + this.col += @intCast(s.len); + this.dest.writeAll(s) catch { + return this.addFmtError(); + }; + return; + } + + /// Writes a formatted string to the underlying destination. + /// + /// NOTE: Is is assumed that the formatted string does not contain any newline characters. + /// If such a string is written, it will break source maps. + pub fn writeFmt(this: *This, comptime fmt: []const u8, args: anytype) PrintErr!void { + // assuming the writer comes from an ArrayList + const start: usize = this.dest.context.self.items.len; + this.dest.print(fmt, args) catch bun.outOfMemory(); + const written = this.dest.context.self.items.len - start; + this.col += @intCast(written); + } + + fn replaceDots(allocator: Allocator, s: []const u8) []const u8 { + var str = allocator.dupe(u8, s) catch bun.outOfMemory(); + std.mem.replaceScalar(u8, str[0..], '.', '-'); + return str; + } + + /// Writes a CSS identifier to the underlying destination, escaping it + /// as appropriate. If the `css_modules` option was enabled, then a hash + /// is added, and the mapping is added to the CSS module. + pub fn writeIdent(this: *This, ident: []const u8, handle_css_module: bool) PrintErr!void { + if (handle_css_module) { + if (this.css_module) |*css_module| { + const Closure = struct { first: bool, printer: *This }; + var closure = Closure{ .first = true, .printer = this }; + css_module.config.pattern.write( + css_module.hashes.items[this.loc.source_index], + css_module.sources.items[this.loc.source_index], + ident, + &closure, + struct { + pub fn writeFn(self: *Closure, s1: []const u8, replace_dots: bool) void { + // PERF: stack fallback? + const s = if (!replace_dots) s1 else replaceDots(self.printer.allocator, s1); + defer if (replace_dots) self.printer.allocator.free(s); + self.printer.col += @intCast(s.len); + if (self.first) { + self.first = false; + return css.serializer.serializeIdentifier(s, self.printer) catch |e| css.OOM(e); + } else { + return css.serializer.serializeName(s, self.printer) catch |e| css.OOM(e); + } + } + }.writeFn, + ); + + css_module.addLocal(this.allocator, ident, ident, this.loc.source_index); + return; + } + } + + return css.serializer.serializeIdentifier(ident, this) catch return this.addFmtError(); + } + + pub fn writeDashedIdent(this: *This, ident: *const DashedIdent, is_declaration: bool) !void { + try this.writeStr("--"); + + if (this.css_module) |*css_module| { + if (css_module.config.dashed_idents) { + const Fn = struct { + pub fn writeFn(self: *This, s1: []const u8, replace_dots: bool) void { + const s = if (!replace_dots) s1 else replaceDots(self.allocator, s1); + defer if (replace_dots) self.allocator.free(s); + self.col += @intCast(s.len); + return css.serializer.serializeName(s, self) catch |e| css.OOM(e); + } + }; + css_module.config.pattern.write( + css_module.hashes.items[this.loc.source_index], + css_module.sources.items[this.loc.source_index], + ident.v[2..], + this, + Fn.writeFn, + ); + + if (is_declaration) { + css_module.addDashed(this.allocator, ident.v, this.loc.source_index); + } + } + } + + return css.serializer.serializeName(ident.v[2..], this) catch return this.addFmtError(); + } + + pub fn writeByte(this: *This, char: u8) !void { + return this.writeChar(char) catch return Allocator.Error.OutOfMemory; + } + + /// Write a single character to the underlying destination. + pub fn writeChar(this: *This, char: u8) PrintErr!void { + if (char == '\n') { + this.line += 1; + this.col = 0; + } else { + this.col += 1; + } + return this.dest.writeByte(char) catch { + return this.addFmtError(); + }; + } + + /// Writes a newline character followed by indentation. + /// If the `minify` option is enabled, then nothing is printed. + pub fn newline(this: *This) PrintErr!void { + if (this.minify) { + return; + } + + try this.writeChar('\n'); + return this.writeIndent(); + } + + /// Writes a delimiter character, followed by whitespace (depending on the `minify` option). + /// If `ws_before` is true, then whitespace is also written before the delimiter. + pub fn delim(this: *This, delim_: u8, ws_before: bool) PrintErr!void { + if (ws_before) { + try this.whitespace(); + } + try this.writeChar(delim_); + return this.whitespace(); + } + + /// Writes a single whitespace character, unless the `minify` option is enabled. + /// + /// Use `write_char` instead if you wish to force a space character to be written, + /// regardless of the `minify` option. + pub fn whitespace(this: *This) PrintErr!void { + if (this.minify) return; + return this.writeChar(' '); + } + + pub fn withContext( + this: *This, + selectors: *const css.SelectorList, + closure: anytype, + comptime func: anytype, + ) PrintErr!void { + const parent = if (this.ctx) |ctx| parent: { + this.ctx = null; + break :parent ctx; + } else null; + + const ctx = css.StyleContext{ .selectors = selectors, .parent = parent }; + + this.ctx = &ctx; + const res = func(closure, Writer, this); + this.ctx = parent; + + return res; + } + + pub fn withClearedContext( + this: *This, + closure: anytype, + comptime func: anytype, + ) PrintErr!void { + const parent = if (this.ctx) |ctx| parent: { + this.ctx = null; + break :parent ctx; + } else null; + const res = func(closure, Writer, this); + this.ctx = parent; + return res; + } + + /// Increases the current indent level. + pub fn indent(this: *This) void { + this.indent_amt += 2; + } + + /// Decreases the current indent level. + pub fn dedent(this: *This) void { + this.indent_amt -= 2; + } + + const INDENTS: []const []const u8 = indents: { + const levels = 32; + var indents: [levels][]const u8 = undefined; + for (0..levels) |i| { + const n = i * 2; + var str: [n]u8 = undefined; + for (0..n) |j| { + str[j] = ' '; + } + indents[i] = str; + } + break :indents indents; + }; + + fn getIndent(this: *This, idnt: u8) []const u8 { + // divide by 2 to get index into table + const i = idnt >> 1; + // PERF: may be faster to just do `i < (IDENTS.len - 1) * 2` (e.g. 62 if IDENTS.len == 32) here + if (i < INDENTS.len) { + return INDENTS[i]; + } + if (this.indentation_buf.items.len < idnt) { + this.indentation_buf.appendNTimes(' ', this.indentation_buf.items.len - idnt) catch unreachable; + } else { + this.indentation_buf.items = this.indentation_buf.items[0..idnt]; + } + return this.indentation_buf.items; + } + + fn writeIndent(this: *This) PrintErr!void { + bun.debugAssert(!this.minify); + if (this.indent_amt > 0) { + // try this.writeStr(this.getIndent(this.ident)); + this.dest.writeByteNTimes(' ', this.indent_amt) catch return this.addFmtError(); + } + } + }; +} diff --git a/src/css/properties/align.zig b/src/css/properties/align.zig new file mode 100644 index 0000000000000..964ad7907f9c0 --- /dev/null +++ b/src/css/properties/align.zig @@ -0,0 +1,310 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const LengthPercentage = css.css_values.length.LengthPercentage; + +/// A value for the [align-content](https://www.w3.org/TR/css-align-3/#propdef-align-content) property. +pub const AlignContent = union(enum) { + /// Default alignment. + normal: void, + /// A baseline position. + baseline_position: BaselinePosition, + /// A content distribution keyword. + content_distribution: ContentDistribution, + /// A content position keyword. + content_position: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + /// A content position keyword. + value: ContentPosition, + }, +}; + +/// A [``](https://www.w3.org/TR/css-align-3/#typedef-baseline-position) value, +/// as used in the alignment properties. +pub const BaselinePosition = enum { + /// The first baseline. + first, + /// The last baseline. + last, +}; + +/// A value for the [justify-content](https://www.w3.org/TR/css-align-3/#propdef-justify-content) property. +pub const JustifyContent = union(enum) { + /// Default justification. + normal, + /// A content distribution keyword. + content_distribution: ContentDistribution, + /// A content position keyword. + content_position: struct { + /// A content position keyword. + value: ContentPosition, + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Justify to the left. + left: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Justify to the right. + right: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, +}; + +/// A value for the [align-self](https://www.w3.org/TR/css-align-3/#align-self-property) property. +pub const AlignSelf = union(enum) { + /// Automatic alignment. + auto, + /// Default alignment. + normal, + /// Item is stretched. + stretch, + /// A baseline position keyword. + baseline_position: BaselinePosition, + /// A self position keyword. + self_position: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + /// A self position keyword. + value: SelfPosition, + }, +}; + +/// A value for the [justify-self](https://www.w3.org/TR/css-align-3/#justify-self-property) property. +pub const JustifySelf = union(enum) { + /// Automatic justification. + auto, + /// Default justification. + normal, + /// Item is stretched. + stretch, + /// A baseline position keyword. + baseline_position: BaselinePosition, + /// A self position keyword. + self_position: struct { + /// A self position keyword. + value: SelfPosition, + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Item is justified to the left. + left: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Item is justified to the right. + right: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, +}; + +/// A value for the [align-items](https://www.w3.org/TR/css-align-3/#align-items-property) property. +pub const AlignItems = union(enum) { + /// Default alignment. + normal, + /// Items are stretched. + stretch, + /// A baseline position keyword. + baseline_position: BaselinePosition, + /// A self position keyword. + self_position: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + /// A self position keyword. + value: SelfPosition, + }, +}; + +/// A value for the [justify-items](https://www.w3.org/TR/css-align-3/#justify-items-property) property. +pub const JustifyItems = union(enum) { + /// Default justification. + normal, + /// Items are stretched. + stretch, + /// A baseline position keyword. + baseline_position: BaselinePosition, + /// A self position keyword, with optional overflow position. + self_position: struct { + /// A self position keyword. + value: SelfPosition, + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Items are justified to the left, with an optional overflow position. + left: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// Items are justified to the right, with an optional overflow position. + right: struct { + /// An overflow alignment mode. + overflow: ?OverflowPosition, + }, + /// A legacy justification keyword. + legacy: LegacyJustify, +}; + +/// A legacy justification keyword, as used in the `justify-items` property. +pub const LegacyJustify = enum { + /// Left justify. + left, + /// Right justify. + right, + /// Centered. + center, +}; + +/// A [gap](https://www.w3.org/TR/css-align-3/#column-row-gap) value, as used in the +/// `column-gap` and `row-gap` properties. +pub const GapValue = union(enum) { + /// Equal to `1em` for multi-column containers, and zero otherwise. + normal, + /// An explicit length. + length_percentage: LengthPercentage, +}; + +/// A value for the [gap](https://www.w3.org/TR/css-align-3/#gap-shorthand) shorthand property. +pub const Gap = struct { + /// The row gap. + row: GapValue, + /// The column gap. + column: GapValue, + + pub usingnamespace css.DefineShorthand(@This()); + + const PropertyFieldMap = .{ + .row = "row-gap", + .column = "column-gap", + }; +}; + +/// A value for the [place-items](https://www.w3.org/TR/css-align-3/#place-items-property) shorthand property. +pub const PlaceItems = struct { + /// The item alignment. + @"align": AlignItems, + /// The item justification. + justify: JustifyItems, + + pub usingnamespace css.DefineShorthand(@This()); + + const PropertyFieldMap = .{ + .@"align" = "align-items", + .justify = "justify-items", + }; + + const VendorPrefixMap = .{ + .@"align" = true, + }; +}; + +/// A value for the [place-self](https://www.w3.org/TR/css-align-3/#place-self-property) shorthand property. +pub const PlaceSelf = struct { + /// The item alignment. + @"align": AlignSelf, + /// The item justification. + justify: JustifySelf, + + pub usingnamespace css.DefineShorthand(@This()); + + const PropertyFieldMap = .{ + .@"align" = "align-self", + .justify = "justify-self", + }; + + const VendorPrefixMap = .{ + .@"align" = true, + }; +}; + +/// A [``](https://www.w3.org/TR/css-align-3/#typedef-self-position) value. +pub const SelfPosition = enum { + /// Item is centered within the container. + center, + /// Item is aligned to the start of the container. + start, + /// Item is aligned to the end of the container. + end, + /// Item is aligned to the edge of the container corresponding to the start side of the item. + @"self-start", + /// Item is aligned to the edge of the container corresponding to the end side of the item. + @"self-end", + /// Item is aligned to the start of the container, within flexbox layouts. + @"flex-start", + /// Item is aligned to the end of the container, within flexbox layouts. + @"flex-end", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A value for the [place-content](https://www.w3.org/TR/css-align-3/#place-content) shorthand property. +pub const PlaceContent = struct { + /// The content alignment. + @"align": AlignContent, + /// The content justification. + justify: JustifyContent, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"place-content"); + + const PropertyFieldMap = .{ + .@"align" = css.PropertyIdTag.@"align-content", + .justify = css.PropertyIdTag.@"justify-content", + }; + + const VendorPrefixMap = .{ + .@"align" = true, + .justify = true, + }; +}; + +/// A [``](https://www.w3.org/TR/css-align-3/#typedef-content-distribution) value. +pub const ContentDistribution = enum { + /// Items are spaced evenly, with the first and last items against the edge of the container. + @"space-between", + /// Items are spaced evenly, with half-size spaces at the start and end. + @"space-around", + /// Items are spaced evenly, with full-size spaces at the start and end. + @"space-evenly", + /// Items are stretched evenly to fill free space. + stretch, + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// An [``](https://www.w3.org/TR/css-align-3/#typedef-overflow-position) value. +pub const OverflowPosition = enum { + /// If the size of the alignment subject overflows the alignment container, + /// the alignment subject is instead aligned as if the alignment mode were start. + safe, + /// Regardless of the relative sizes of the alignment subject and alignment + /// container, the given alignment value is honored. + unsafe, + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A [``](https://www.w3.org/TR/css-align-3/#typedef-content-position) value. +pub const ContentPosition = enum { + /// Content is centered within the container. + center, + /// Content is aligned to the start of the container. + start, + /// Content is aligned to the end of the container. + end, + /// Same as `start` when within a flexbox container. + @"flex-start", + /// Same as `end` when within a flexbox container. + @"flex-end", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; diff --git a/src/css/properties/animation.zig b/src/css/properties/animation.zig new file mode 100644 index 0000000000000..92a52ac642396 --- /dev/null +++ b/src/css/properties/animation.zig @@ -0,0 +1,153 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; + +/// A list of animations. +pub const AnimationList = SmallList(Animation, 1); + +/// A list of animation names. +pub const AnimationNameList = SmallList(AnimationName, 1); + +/// A value for the [animation](https://drafts.csswg.org/css-animations/#animation) shorthand property. +pub const Animation = @compileError(css.todo_stuff.depth); + +/// A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property. +pub const AnimationName = union(enum) { + /// The `none` keyword. + none, + /// An identifier of a `@keyframes` rule. + ident: CustomIdent, + /// A `` name of a `@keyframes` rule. + string: CSSString, + + // ~toCssImpl + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @panic(css.todo_stuff.depth); + } +}; + +/// A value for the [animation-iteration-count](https://drafts.csswg.org/css-animations/#animation-iteration-count) property. +pub const AnimationIterationCount = union(enum) { + /// The animation will repeat the specified number of times. + number: CSSNumber, + /// The animation will repeat forever. + infinite, +}; + +/// A value for the [animation-direction](https://drafts.csswg.org/css-animations/#animation-direction) property. +pub const AnimationDirection = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [animation-play-state](https://drafts.csswg.org/css-animations/#animation-play-state) property. +pub const AnimationPlayState = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [animation-fill-mode](https://drafts.csswg.org/css-animations/#animation-fill-mode) property. +pub const AnimationFillMode = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [animation-composition](https://drafts.csswg.org/css-animations-2/#animation-composition) property. +pub const AnimationComposition = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [animation-timeline](https://drafts.csswg.org/css-animations-2/#animation-timeline) property. +pub const AnimationTimeline = union(enum) { + /// The animation's timeline is a DocumentTimeline, more specifically the default document timeline. + auto, + /// The animation is not associated with a timeline. + none, + /// A timeline referenced by name. + dashed_ident: DashedIdent, + /// The scroll() function. + scroll: ScrollTimeline, + /// The view() function. + view: ViewTimeline, +}; + +/// The [scroll()](https://drafts.csswg.org/scroll-animations-1/#scroll-notation) function. +pub const ScrollTimeline = struct { + /// Specifies which element to use as the scroll container. + scroller: Scroller, + /// Specifies which axis of the scroll container to use as the progress for the timeline. + axis: ScrollAxis, +}; + +/// The [view()](https://drafts.csswg.org/scroll-animations-1/#view-notation) function. +pub const ViewTimeline = struct { + /// Specifies which axis of the scroll container to use as the progress for the timeline. + axis: ScrollAxis, + /// Provides an adjustment of the view progress visibility range. + inset: Size2D(LengthPercentageOrAuto), +}; + +/// A scroller, used in the `scroll()` function. +pub const Scroller = @compileError(css.todo_stuff.depth); + +/// A scroll axis, used in the `scroll()` function. +pub const ScrollAxis = @compileError(css.todo_stuff.depth); + +/// A value for the animation-range shorthand property. +pub const AnimationRange = struct { + /// The start of the animation's attachment range. + start: AnimationRangeStart, + /// The end of the animation's attachment range. + end: AnimationRangeEnd, +}; + +/// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) property. +pub const AnimationRangeStart = struct { + v: AnimationAttachmentRange, +}; + +/// A value for the [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-start) property. +pub const AnimationRangeEnd = struct { + v: AnimationAttachmentRange, +}; + +/// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) +/// or [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property. +pub const AnimationAttachmentRange = union(enum) { + /// The start of the animation's attachment range is the start of its associated timeline. + normal, + /// The animation attachment range starts at the specified point on the timeline measuring from the start of the timeline. + length_percentage: LengthPercentage, + /// The animation attachment range starts at the specified point on the timeline measuring from the start of the specified named timeline range. + timeline_range: struct { + /// The name of the timeline range. + name: TimelineRangeName, + /// The offset from the start of the named timeline range. + offset: LengthPercentage, + }, +}; + +/// A [view progress timeline range](https://drafts.csswg.org/scroll-animations/#view-timelines-ranges) +pub const TimelineRangeName = enum { + /// Represents the full range of the view progress timeline. + cover, + /// Represents the range during which the principal box is either fully contained by, + /// or fully covers, its view progress visibility range within the scrollport. + contain, + /// Represents the range during which the principal box is entering the view progress visibility range. + entry, + /// Represents the range during which the principal box is exiting the view progress visibility range. + exit, + /// Represents the range during which the principal box crosses the end border edge. + entry_crossing, + /// Represents the range during which the principal box crosses the start border edge. + exit_crossing, +}; diff --git a/src/css/properties/background.zig b/src/css/properties/background.zig new file mode 100644 index 0000000000000..8dbe02d9c07e7 --- /dev/null +++ b/src/css/properties/background.zig @@ -0,0 +1,136 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +const Ratio = css.css_values.ratio.Ratio; +const HorizontalPosition = css.css_values.position.HorizontalPosition; +const VerticalPosition = css.css_values.position.HorizontalPosition; + +/// A value for the [background](https://www.w3.org/TR/css-backgrounds-3/#background) shorthand property. +pub const Background = struct { + /// The background image. + image: Image, + /// The background color. + color: CssColor, + /// The background position. + position: BackgroundPosition, + /// How the background image should repeat. + repeat: BackgroundRepeat, + /// The size of the background image. + size: BackgroundSize, + /// The background attachment. + attachment: BackgroundAttachment, + /// The background origin. + origin: BackgroundOrigin, + /// How the background should be clipped. + clip: BackgroundClip, +}; + +/// A value for the [background-size](https://www.w3.org/TR/css-backgrounds-3/#background-size) property. +pub const BackgroundSize = union(enum) { + /// An explicit background size. + explicit: struct { + /// The width of the background. + width: css.css_values.length.LengthPercentage, + /// The height of the background. + height: css.css_values.length.LengthPercentageOrAuto, + }, + /// The `cover` keyword. Scales the background image to cover both the width and height of the element. + cover, + /// The `contain` keyword. Scales the background image so that it fits within the element. + contain, +}; + +/// A value for the [background-position](https://drafts.csswg.org/css-backgrounds/#background-position) shorthand property. +pub const BackgroundPosition = struct { + /// The x-position. + x: HorizontalPosition, + /// The y-position. + y: VerticalPosition, + + pub usingnamespace css.DefineListShorthand(@This()); + + const PropertyFieldMap = .{ + .x = css.PropertyIdTag.@"background-position-x", + .y = css.PropertyIdTag.@"background-position-y", + }; +}; + +/// A value for the [background-repeat](https://www.w3.org/TR/css-backgrounds-3/#background-repeat) property. +pub const BackgroundRepeat = struct { + /// A repeat style for the x direction. + x: BackgroundRepeatKeyword, + /// A repeat style for the y direction. + y: BackgroundRepeatKeyword, +}; + +/// A [``](https://www.w3.org/TR/css-backgrounds-3/#typedef-repeat-style) value, +/// used within the `background-repeat` property to represent how a background image is repeated +/// in a single direction. +/// +/// See [BackgroundRepeat](BackgroundRepeat). +pub const BackgroundRepeatKeyword = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [background-attachment](https://www.w3.org/TR/css-backgrounds-3/#background-attachment) property. +pub const BackgroundAttachment = enum { + /// The background scrolls with the container. + scroll, + /// The background is fixed to the viewport. + fixed, + /// The background is fixed with regard to the element's contents. + local, + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A value for the [background-origin](https://www.w3.org/TR/css-backgrounds-3/#background-origin) property. +pub const BackgroundOrigin = enum { + /// The position is relative to the border box. + @"border-box", + /// The position is relative to the padding box. + @"padding-box", + /// The position is relative to the content box. + @"content-box", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A value for the [background-clip](https://drafts.csswg.org/css-backgrounds-4/#background-clip) property. +pub const BackgroundClip = enum { + /// The background is clipped to the border box. + @"border-box", + /// The background is clipped to the padding box. + @"padding-box", + /// The background is clipped to the content box. + @"content-box", + /// The background is clipped to the area painted by the border. + border, + /// The background is clipped to the text content of the element. + text, + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A value for the [aspect-ratio](https://drafts.csswg.org/css-sizing-4/#aspect-ratio) property. +pub const AspectRatio = struct { + /// The `auto` keyword. + auto: bool, + /// A preferred aspect ratio for the box, specified as width / height. + ratio: ?Ratio, +}; diff --git a/src/css/properties/border.zig b/src/css/properties/border.zig new file mode 100644 index 0000000000000..ba49b98321422 --- /dev/null +++ b/src/css/properties/border.zig @@ -0,0 +1,246 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +const Ratio = css.css_values.ratio.Ratio; +const Length = css.css_values.length.LengthValue; + +/// A value for the [border-top](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-top) shorthand property. +pub const BorderTop = GenericBorder(LineStyle, 0); +/// A value for the [border-right](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-right) shorthand property. +pub const BorderRight = GenericBorder(LineStyle, 1); +/// A value for the [border-bottom](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-bottom) shorthand property. +pub const BorderBottom = GenericBorder(LineStyle, 2); +/// A value for the [border-left](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-left) shorthand property. +pub const BorderLeft = GenericBorder(LineStyle, 3); +/// A value for the [border-block-start](https://drafts.csswg.org/css-logical/#propdef-border-block-start) shorthand property. +pub const BorderBlockStart = GenericBorder(LineStyle, 4); +/// A value for the [border-block-end](https://drafts.csswg.org/css-logical/#propdef-border-block-end) shorthand property. +pub const BorderBlockEnd = GenericBorder(LineStyle, 5); +/// A value for the [border-inline-start](https://drafts.csswg.org/css-logical/#propdef-border-inline-start) shorthand property. +pub const BorderInlineStart = GenericBorder(LineStyle, 6); +/// A value for the [border-inline-end](https://drafts.csswg.org/css-logical/#propdef-border-inline-end) shorthand property. +pub const BorderInlineEnd = GenericBorder(LineStyle, 7); +/// A value for the [border-block](https://drafts.csswg.org/css-logical/#propdef-border-block) shorthand property. +pub const BorderBlock = GenericBorder(LineStyle, 8); +/// A value for the [border-inline](https://drafts.csswg.org/css-logical/#propdef-border-inline) shorthand property. +pub const BorderInline = GenericBorder(LineStyle, 9); +/// A value for the [border](https://www.w3.org/TR/css-backgrounds-3/#propdef-border) shorthand property. +pub const Border = GenericBorder(LineStyle, 10); + +/// A generic type that represents the `border` and `outline` shorthand properties. +pub fn GenericBorder(comptime S: type, comptime P: u8) type { + _ = P; // autofix + return struct { + /// The width of the border. + width: BorderSideWidth, + /// The border style. + style: S, + /// The border color. + color: CssColor, + }; +} +/// A [``](https://drafts.csswg.org/css-backgrounds/#typedef-line-style) value, used in the `border-style` property. +/// A [``](https://drafts.csswg.org/css-backgrounds/#typedef-line-style) value, used in the `border-style` property. +pub const LineStyle = enum { + /// No border. + none, + /// Similar to `none` but with different rules for tables. + hidden, + /// Looks as if the content on the inside of the border is sunken into the canvas. + inset, + /// Looks as if it were carved in the canvas. + groove, + /// Looks as if the content on the inside of the border is coming out of the canvas. + outset, + /// Looks as if it were coming out of the canvas. + ridge, + /// A series of round dots. + dotted, + /// A series of square-ended dashes. + dashed, + /// A single line segment. + solid, + /// Two parallel solid lines with some space between them. + double, + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A value for the [border-width](https://www.w3.org/TR/css-backgrounds-3/#border-width) property. +pub const BorderSideWidth = union(enum) { + /// A UA defined `thin` value. + thin, + /// A UA defined `medium` value. + medium, + /// A UA defined `thick` value. + thick, + /// An explicit width. + length: Length, +}; + +/// A value for the [border-color](https://drafts.csswg.org/css-backgrounds/#propdef-border-color) shorthand property. +pub const BorderColor = struct { + top: CssColor, + right: CssColor, + bottom: CssColor, + left: CssColor, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-color"); + pub usingnamespace css.DefineRectShorthand(@This(), CssColor); + + pub const PropertyFieldMap = .{ + .top = css.PropertyIdTag.@"border-top-color", + .right = css.PropertyIdTag.@"border-right-color", + .bottom = css.PropertyIdTag.@"border-bottom-color", + .left = css.PropertyIdTag.@"border-left-color", + }; +}; + +/// A value for the [border-style](https://drafts.csswg.org/css-backgrounds/#propdef-border-style) shorthand property. +pub const BorderStyle = struct { + top: LineStyle, + right: LineStyle, + bottom: LineStyle, + left: LineStyle, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-style"); + pub usingnamespace css.DefineRectShorthand(@This(), LineStyle); + + pub const PropertyFieldMap = .{ + .top = css.PropertyIdTag.@"border-top-style", + .right = css.PropertyIdTag.@"border-right-style", + .bottom = css.PropertyIdTag.@"border-bottom-style", + .left = css.PropertyIdTag.@"border-left-style", + }; +}; + +/// A value for the [border-width](https://drafts.csswg.org/css-backgrounds/#propdef-border-width) shorthand property. +pub const BorderWidth = struct { + top: BorderSideWidth, + right: BorderSideWidth, + bottom: BorderSideWidth, + left: BorderSideWidth, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-width"); + pub usingnamespace css.DefineRectShorthand(@This(), BorderSideWidth); + + pub const PropertyFieldMap = .{ + .top = css.PropertyIdTag.@"border-top-width", + .right = css.PropertyIdTag.@"border-right-width", + .bottom = css.PropertyIdTag.@"border-bottom-width", + .left = css.PropertyIdTag.@"border-left-width", + }; +}; + +/// A value for the [border-block-color](https://drafts.csswg.org/css-logical/#propdef-border-block-color) shorthand property. +pub const BorderBlockColor = struct { + /// The block start value. + start: CssColor, + /// The block end value. + end: CssColor, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-block-color"); + pub usingnamespace css.DefineSizeShorthand(@This(), CssColor); + + pub const PropertyFieldMap = .{ + .start = css.PropertyIdTag.@"border-block-start-color", + .end = css.PropertyIdTag.@"border-block-end-color", + }; +}; + +/// A value for the [border-block-style](https://drafts.csswg.org/css-logical/#propdef-border-block-style) shorthand property. +pub const BorderBlockStyle = struct { + /// The block start value. + start: LineStyle, + /// The block end value. + end: LineStyle, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-block-style"); + pub usingnamespace css.DefineSizeShorthand(@This(), LineStyle); + + pub const PropertyFieldMap = .{ + .start = css.PropertyIdTag.@"border-block-start-style", + .end = css.PropertyIdTag.@"border-block-end-style", + }; +}; + +/// A value for the [border-block-width](https://drafts.csswg.org/css-logical/#propdef-border-block-width) shorthand property. +pub const BorderBlockWidth = struct { + /// The block start value. + start: BorderSideWidth, + /// The block end value. + end: BorderSideWidth, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-block-width"); + pub usingnamespace css.DefineSizeShorthand(@This(), BorderSideWidth); + + pub const PropertyFieldMap = .{ + .start = css.PropertyIdTag.@"border-block-start-width", + .end = css.PropertyIdTag.@"border-block-end-width", + }; +}; + +/// A value for the [border-inline-color](https://drafts.csswg.org/css-logical/#propdef-border-inline-color) shorthand property. +pub const BorderInlineColor = struct { + /// The inline start value. + start: CssColor, + /// The inline end value. + end: CssColor, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-inline-color"); + pub usingnamespace css.DefineSizeShorthand(@This(), CssColor); + + pub const PropertyFieldMap = .{ + .start = css.PropertyIdTag.@"border-inline-start-color", + .end = css.PropertyIdTag.@"border-inline-end-color", + }; +}; + +/// A value for the [border-inline-style](https://drafts.csswg.org/css-logical/#propdef-border-inline-style) shorthand property. +pub const BorderInlineStyle = struct { + /// The inline start value. + start: LineStyle, + /// The inline end value. + end: LineStyle, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-inline-style"); + pub usingnamespace css.DefineSizeShorthand(@This(), LineStyle); + + pub const PropertyFieldMap = .{ + .start = css.PropertyIdTag.@"border-inline-start-style", + .end = css.PropertyIdTag.@"border-inline-end-style", + }; +}; + +/// A value for the [border-inline-width](https://drafts.csswg.org/css-logical/#propdef-border-inline-width) shorthand property. +pub const BorderInlineWidth = struct { + /// The inline start value. + start: BorderSideWidth, + /// The inline end value. + end: BorderSideWidth, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"border-inline-width"); + pub usingnamespace css.DefineSizeShorthand(@This(), BorderSideWidth); + + pub const PropertyFieldMap = .{ + .start = css.PropertyIdTag.@"border-inline-start-width", + .end = css.PropertyIdTag.@"border-inline-end-width", + }; +}; diff --git a/src/css/properties/border_image.zig b/src/css/properties/border_image.zig new file mode 100644 index 0000000000000..c993308d1999c --- /dev/null +++ b/src/css/properties/border_image.zig @@ -0,0 +1,55 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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; + +/// A value for the [border-image](https://www.w3.org/TR/css-backgrounds-3/#border-image) shorthand property. +pub const BorderImage = @compileError(css.todo_stuff.depth); + +/// A value for the [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) property. +pub const BorderImageRepeat = struct { + /// The horizontal repeat value. + horizontal: BorderImageRepeatKeyword, + /// The vertical repeat value. + vertical: BorderImageRepeatKeyword, +}; + +/// A value for the [border-image-width](https://www.w3.org/TR/css-backgrounds-3/#border-image-width) property. +pub const BorderImageSideWidth = union(enum) { + /// A number representing a multiple of the border width. + number: CSSNumber, + /// An explicit length or percentage. + length_percentage: LengthPercentage, + /// The `auto` keyword, representing the natural width of the image slice. + auto: void, +}; + +pub const BorderImageRepeatKeyword = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [border-image-slice](https://www.w3.org/TR/css-backgrounds-3/#border-image-slice) property. +pub const BorderImageSlice = struct { + /// The offsets from the edges of the image. + offsets: Rect(NumberOrPercentage), + /// Whether the middle of the border image should be preserved. + fill: bool, +}; diff --git a/src/css/properties/border_radius.zig b/src/css/properties/border_radius.zig new file mode 100644 index 0000000000000..448f3778f354d --- /dev/null +++ b/src/css/properties/border_radius.zig @@ -0,0 +1,27 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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; + +/// A value for the [border-radius](https://www.w3.org/TR/css-backgrounds-3/#border-radius) property. +pub const BorderRadius = @compileError(css.todo_stuff.depth); diff --git a/src/css/properties/box_shadow.zig b/src/css/properties/box_shadow.zig new file mode 100644 index 0000000000000..d1255f6d3ab20 --- /dev/null +++ b/src/css/properties/box_shadow.zig @@ -0,0 +1,40 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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; + +/// A value for the [box-shadow](https://drafts.csswg.org/css-backgrounds/#box-shadow) property. +pub const BoxShadow = struct { + /// The color of the box shadow. + color: CssColor, + /// The x offset of the shadow. + x_offset: Length, + /// The y offset of the shadow. + y_offset: Length, + /// The blur radius of the shadow. + blur: Length, + /// The spread distance of the shadow. + spread: Length, + /// Whether the shadow is inset within the box. + inset: bool, +}; diff --git a/src/css/properties/contain.zig b/src/css/properties/contain.zig new file mode 100644 index 0000000000000..a095c5228c7f8 --- /dev/null +++ b/src/css/properties/contain.zig @@ -0,0 +1,43 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 ContainerIdent = ContainerName; + +/// A value for the [container-type](https://drafts.csswg.org/css-contain-3/#container-type) property. +/// Establishes the element as a query container for the purpose of container queries. +pub const ContainerType = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [container-name](https://drafts.csswg.org/css-contain-3/#container-name) property. +pub const ContainerNameList = union(enum) { + /// The `none` keyword. + none, + /// A list of container names. + names: SmallList(ContainerIdent, 1), +}; + +/// A value for the [container](https://drafts.csswg.org/css-contain-3/#container-shorthand) shorthand property. +pub const Container = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); diff --git a/src/css/properties/css_modules.zig b/src/css/properties/css_modules.zig new file mode 100644 index 0000000000000..037ab90f7363e --- /dev/null +++ b/src/css/properties/css_modules.zig @@ -0,0 +1,91 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const CustomIdentFns = css.css_values.ident.CustomIdentFns; + +const Location = css.dependencies.Location; + +/// A value for the [composes](https://github.com/css-modules/css-modules/#dependencies) property from CSS modules. +pub const Composes = struct { + /// A list of class names to compose. + names: CustomIdentList, + /// Where the class names are composed from. + from: ?Specifier, + /// The source location of the `composes` property. + loc: Location, + + pub fn parse(input: *css.Parser) css.Result(Composes) { + _ = input; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + var first = true; + for (this.names.items) |name| { + if (first) { + first = false; + } else { + try dest.writeChar(' '); + } + try CustomIdentFns.toCss(&name, W, dest); + } + + if (this.from) |*from| { + try dest.writeStr(" from "); + try from.toCss(W, dest); + } + } +}; + +/// Defines where the class names referenced in the `composes` property are located. +/// +/// See [Composes](Composes). +pub const Specifier = union(enum) { + /// The referenced name is global. + global, + /// The referenced name comes from the specified file. + file: []const u8, + /// The referenced name comes from a source index (used during bundling). + source_index: u32, + + pub fn parse(input: *css.Parser) css.Result(Specifier) { + if (input.tryParse(css.Parser.expectString, .{}).asValue()) |file| { + return .{ .result = .{ .file = file } }; + } + if (input.expectIdentMatching("global").asErr()) |e| return .{ .err = e }; + return .{ .result = .global }; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.*) { + .global => dest.writeStr("global"), + .file => |file| css.serializer.serializeString(file, dest) catch return dest.addFmtError(), + .source_index => {}, + }; + } +}; diff --git a/src/css/properties/custom.zig b/src/css/properties/custom.zig new file mode 100644 index 0000000000000..748152418c99e --- /dev/null +++ b/src/css/properties/custom.zig @@ -0,0 +1,1329 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const css_values = @import("../values/values.zig"); +pub const Printer = css.Printer; +pub const PrintErr = css.PrintErr; +const DashedIdent = css_values.ident.DashedIdent; +const DashedIdentFns = css_values.ident.DashedIdentFns; +const Ident = css_values.ident.Ident; +const IdentFns = css_values.ident.IdentFns; +pub const Result = css.Result; + +pub const CssColor = css.css_values.color.CssColor; +pub const RGBA = css.css_values.color.RGBA; +pub const SRGB = css.css_values.color.SRGB; +pub const HSL = css.css_values.color.HSL; +pub const CSSInteger = css.css_values.number.CSSInteger; +pub const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +pub const CSSNumberFns = css.css_values.number.CSSNumberFns; +pub const Percentage = css.css_values.percentage.Percentage; +pub const Url = css.css_values.url.Url; +pub const DashedIdentReference = css.css_values.ident.DashedIdentReference; +pub const CustomIdent = css.css_values.ident.CustomIdent; +pub const CustomIdentFns = css.css_values.ident.CustomIdentFns; +pub const LengthValue = css.css_values.length.LengthValue; +pub const Angle = css.css_values.angle.Angle; +pub const Time = css.css_values.time.Time; +pub const Resolution = css.css_values.resolution.Resolution; +pub const AnimationName = css.css_properties.animation.AnimationName; +const ComponentParser = css.css_values.color.ComponentParser; + +const ArrayList = std.ArrayListUnmanaged; + +/// PERF: nullable optimization +pub const TokenList = struct { + v: std.ArrayListUnmanaged(TokenOrValue), + + const This = @This(); + + pub fn deepClone(this: *const TokenList, allocator: Allocator) TokenList { + return .{ + .v = css.deepClone(TokenOrValue, allocator, &this.v), + }; + } + + pub fn deinit(this: *TokenList, allocator: Allocator) void { + for (this.v.items) |*token_or_value| { + token_or_value.deinit(allocator); + } + this.v.deinit(allocator); + } + + pub fn toCss( + this: *const This, + comptime W: type, + dest: *Printer(W), + is_custom_property: bool, + ) PrintErr!void { + if (!dest.minify and this.v.items.len == 1 and this.v.items[0].isWhitespace()) { + return; + } + + var has_whitespace = false; + for (this.v.items, 0..) |*token_or_value, i| { + switch (token_or_value.*) { + .color => |color| { + try color.toCss(W, dest); + has_whitespace = false; + }, + .unresolved_color => |color| { + try color.toCss(W, dest, is_custom_property); + has_whitespace = false; + }, + .url => |url| { + if (dest.dependencies != null and is_custom_property and !url.isAbsolute()) { + return dest.newError(css.PrinterErrorKind{ + .ambiguous_url_in_custom_property = .{ .url = url.url }, + }, url.loc); + } + try url.toCss(W, dest); + has_whitespace = false; + }, + .@"var" => |@"var"| { + try @"var".toCss(W, dest, is_custom_property); + has_whitespace = try this.writeWhitespaceIfNeeded(i, W, dest); + }, + .env => |env| { + try env.toCss(W, dest, is_custom_property); + has_whitespace = try this.writeWhitespaceIfNeeded(i, W, dest); + }, + .function => |f| { + try f.toCss(W, dest, is_custom_property); + has_whitespace = try this.writeWhitespaceIfNeeded(i, W, dest); + }, + .length => |v| { + // Do not serialize unitless zero lengths in custom properties as it may break calc(). + const value, const unit = v.toUnitValue(); + try css.serializer.serializeDimension(value, unit, W, dest); + has_whitespace = false; + }, + .angle => |v| { + try v.toCss(W, dest); + has_whitespace = false; + }, + .time => |v| { + try v.toCss(W, dest); + has_whitespace = false; + }, + .resolution => |v| { + try v.toCss(W, dest); + has_whitespace = false; + }, + .dashed_ident => |v| { + try DashedIdentFns.toCss(&v, W, dest); + has_whitespace = false; + }, + .animation_name => |v| { + try v.toCss(W, dest); + has_whitespace = false; + }, + .token => |token| switch (token) { + .delim => |d| { + if (d == '+' or d == '-') { + try dest.writeChar(' '); + bun.assert(d <= 0x7F); + try dest.writeChar(@intCast(d)); + try dest.writeChar(' '); + } else { + const ws_before = !has_whitespace and (d == '/' or d == '*'); + bun.assert(d <= 0x7F); + try dest.delim(@intCast(d), ws_before); + } + has_whitespace = true; + }, + .comma => { + try dest.delim(',', false); + has_whitespace = true; + }, + .close_paren, .close_square, .close_curly => { + try token.toCss(W, dest); + has_whitespace = try this.writeWhitespaceIfNeeded(i, W, dest); + }, + .dimension => { + try css.serializer.serializeDimension(token.dimension.num.value, token.dimension.unit, W, dest); + has_whitespace = false; + }, + .number => |v| { + try css.css_values.number.CSSNumberFns.toCss(&v.value, W, dest); + has_whitespace = false; + }, + else => { + try token.toCss(W, dest); + has_whitespace = token == .whitespace; + }, + }, + } + } + } + + pub fn toCssRaw(this: *const TokenList, comptime W: type, dest: *Printer(W)) PrintErr!void { + for (this.v.items) |*token_or_value| { + if (token_or_value.* == .token) { + try token_or_value.token.toCss(W, dest); + } else { + return dest.addFmtError(); + } + } + } + + pub fn writeWhitespaceIfNeeded( + this: *const This, + i: usize, + comptime W: type, + dest: *Printer(W), + ) PrintErr!bool { + if (!dest.minify and + i != this.v.items.len - 1 and + this.v.items[i + 1] == .token and switch (this.v.items[i + 1].token) { + .comma, .close_paren => true, + else => false, + }) { + // Whitespace is removed during parsing, so add it back if we aren't minifying. + try dest.writeChar(' '); + return true; + } else return false; + } + + pub fn parse(input: *css.Parser, options: *const css.ParserOptions, depth: usize) Result(TokenList) { + var tokens = ArrayList(TokenOrValue){}; // PERF: deinit on error + if (TokenListFns.parseInto(input, &tokens, options, depth).asErr()) |e| return .{ .err = e }; + + // Slice off leading and trailing whitespace if there are at least two tokens. + // If there is only one token, we must preserve it. e.g. `--foo: ;` is valid. + // PERF(alloc): this feels like a common codepath, idk how I feel about reallocating a new array just to slice off whitespace. + if (tokens.items.len >= 2) { + var slice = tokens.items[0..]; + if (tokens.items.len > 0 and tokens.items[0].isWhitespace()) { + slice = slice[1..]; + } + if (tokens.items.len > 0 and tokens.items[tokens.items.len - 1].isWhitespace()) { + slice = slice[0 .. slice.len - 1]; + } + var newlist = ArrayList(TokenOrValue){}; + newlist.insertSlice(input.allocator(), 0, slice) catch unreachable; + tokens.deinit(input.allocator()); + return .{ .result = TokenList{ .v = newlist } }; + } + + return .{ .result = .{ .v = tokens } }; + } + + pub fn parseWithOptions(input: *css.Parser, options: *const css.ParserOptions) Result(TokenList) { + return parse(input, options, 0); + } + + pub fn parseRaw( + input: *css.Parser, + tokens: *ArrayList(TokenOrValue), + options: *const css.ParserOptions, + depth: usize, + ) Result(void) { + if (depth > 500) { + return .{ .err = input.newCustomError(css.ParserError.maximum_nesting_depth) }; + } + + while (true) { + const state = input.state(); + const token = switch (input.nextIncludingWhitespace()) { + .result => |vv| vv, + .err => break, + }; + switch (token.*) { + .open_paren, .open_square, .open_curly => { + tokens.append( + input.allocator(), + .{ .token = token.* }, + ) catch unreachable; + const closing_delimiter: css.Token = switch (token.*) { + .open_paren => .close_paren, + .open_square => .close_square, + .open_curly => .close_curly, + else => unreachable, + }; + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + tokens: *ArrayList(TokenOrValue), + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(void) { + return TokenListFns.parseRaw( + input2, + this.tokens, + this.options, + this.depth + 1, + ); + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + .tokens = tokens, + }; + if (input.parseNestedBlock(void, &closure, Closure.parsefn).asErr()) |e| return .{ .err = e }; + tokens.append( + input.allocator(), + .{ .token = closing_delimiter }, + ) catch unreachable; + }, + .function => { + tokens.append( + input.allocator(), + .{ .token = token.* }, + ) catch unreachable; + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + tokens: *ArrayList(TokenOrValue), + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(void) { + return TokenListFns.parseRaw( + input2, + this.tokens, + this.options, + this.depth + 1, + ); + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + .tokens = tokens, + }; + if (input.parseNestedBlock(void, &closure, Closure.parsefn).asErr()) |e| return .{ .err = e }; + tokens.append( + input.allocator(), + .{ .token = .close_paren }, + ) catch unreachable; + }, + else => { + if (token.isParseError()) { + return .{ + .err = css.ParseError(css.ParserError){ + .kind = .{ .basic = .{ .unexpected_token = token.* } }, + .location = state.sourceLocation(), + }, + }; + } + tokens.append( + input.allocator(), + .{ .token = token.* }, + ) catch unreachable; + }, + } + } + + return .{ .result = {} }; + } + + pub fn parseInto( + input: *css.Parser, + tokens: *ArrayList(TokenOrValue), + options: *const css.ParserOptions, + depth: usize, + ) Result(void) { + if (depth > 500) { + return .{ .err = input.newCustomError(css.ParserError.maximum_nesting_depth) }; + } + + var last_is_delim = false; + var last_is_whitespace = false; + + while (true) { + const state = input.state(); + const tok = switch (input.nextIncludingWhitespace()) { + .result => |vv| vv, + .err => break, + }; + switch (tok.*) { + .whitespace, .comment => { + // Skip whitespace if the last token was a delimiter. + // Otherwise, replace all whitespace and comments with a single space character. + if (!last_is_delim) { + tokens.append( + input.allocator(), + .{ .token = .{ .whitespace = " " } }, + ) catch unreachable; + last_is_whitespace = true; + } + continue; + }, + .function => |f| { + // Attempt to parse embedded color values into hex tokens. + if (tryParseColorToken(f, &state, input)) |color| { + tokens.append( + input.allocator(), + .{ .color = color }, + ) catch unreachable; + last_is_delim = false; + last_is_whitespace = true; + } else if (input.tryParse(UnresolvedColor.parse, .{ f, options }).asValue()) |color| { + tokens.append( + input.allocator(), + .{ .unresolved_color = color }, + ) catch unreachable; + last_is_delim = false; + last_is_whitespace = true; + } else if (bun.strings.eql(f, "url")) { + input.reset(&state); + tokens.append( + input.allocator(), + .{ .url = switch (Url.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + ) catch unreachable; + last_is_delim = false; + last_is_whitespace = false; + } else if (bun.strings.eql(f, "var")) { + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + tokens: *ArrayList(TokenOrValue), + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(TokenOrValue) { + const thevar = switch (Variable.parse(input2, this.options, this.depth + 1)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = TokenOrValue{ .@"var" = thevar } }; + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + .tokens = tokens, + }; + const @"var" = switch (input.parseNestedBlock(TokenOrValue, &closure, Closure.parsefn)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + tokens.append( + input.allocator(), + @"var", + ) catch unreachable; + last_is_delim = true; + last_is_whitespace = false; + } else if (bun.strings.eql(f, "env")) { + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(TokenOrValue) { + const env = switch (EnvironmentVariable.parseNested(input2, this.options, this.depth + 1)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = TokenOrValue{ .env = env } }; + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + }; + const env = switch (input.parseNestedBlock(TokenOrValue, &closure, Closure.parsefn)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + tokens.append( + input.allocator(), + env, + ) catch unreachable; + last_is_delim = true; + last_is_whitespace = false; + } else { + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(TokenList) { + const args = switch (TokenListFns.parse(input2, this.options, this.depth + 1)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = args }; + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + }; + const arguments = switch (input.parseNestedBlock(TokenList, &closure, Closure.parsefn)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + tokens.append( + input.allocator(), + .{ + .function = .{ + .name = .{ .v = f }, + .arguments = arguments, + }, + }, + ) catch unreachable; + last_is_delim = true; // Whitespace is not required after any of these chars. + last_is_whitespace = false; + } + continue; + }, + .hash, .idhash => { + const h = switch (tok.*) { + .hash => |h| h, + .idhash => |h| h, + else => unreachable, + }; + brk: { + const r, const g, const b, const a = css.color.parseHashColor(h) orelse { + tokens.append( + input.allocator(), + .{ .token = .{ .hash = h } }, + ) catch unreachable; + break :brk; + }; + tokens.append( + input.allocator(), + .{ + .color = CssColor{ .rgba = RGBA.new(r, g, b, a) }, + }, + ) catch unreachable; + } + last_is_delim = false; + last_is_whitespace = false; + continue; + }, + .unquoted_url => { + input.reset(&state); + tokens.append( + input.allocator(), + .{ .url = switch (Url.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + ) catch unreachable; + last_is_delim = false; + last_is_whitespace = false; + continue; + }, + .ident => |name| { + if (bun.strings.startsWith(name, "--")) { + tokens.append(input.allocator(), .{ .dashed_ident = .{ .v = name } }) catch unreachable; + last_is_delim = false; + last_is_whitespace = false; + continue; + } + }, + .open_paren, .open_square, .open_curly => { + tokens.append( + input.allocator(), + .{ .token = tok.* }, + ) catch unreachable; + const closing_delimiter: css.Token = switch (tok.*) { + .open_paren => .close_paren, + .open_square => .close_square, + .open_curly => .close_curly, + else => unreachable, + }; + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + tokens: *ArrayList(TokenOrValue), + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(void) { + return TokenListFns.parseInto( + input2, + this.tokens, + this.options, + this.depth + 1, + ); + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + .tokens = tokens, + }; + if (input.parseNestedBlock(void, &closure, Closure.parsefn).asErr()) |e| return .{ .err = e }; + tokens.append( + input.allocator(), + .{ .token = closing_delimiter }, + ) catch unreachable; + last_is_delim = true; // Whitespace is not required after any of these chars. + last_is_whitespace = false; + continue; + }, + .dimension => { + const value = if (LengthValue.tryFromToken(tok).asValue()) |length| + TokenOrValue{ .length = length } + else if (Angle.tryFromToken(tok).asValue()) |angle| + TokenOrValue{ .angle = angle } + else if (Time.tryFromToken(tok).asValue()) |time| + TokenOrValue{ .time = time } + else if (Resolution.tryFromToken(tok).asValue()) |resolution| + TokenOrValue{ .resolution = resolution } + else + TokenOrValue{ .token = tok.* }; + + tokens.append( + input.allocator(), + value, + ) catch unreachable; + + last_is_delim = false; + last_is_whitespace = false; + continue; + }, + else => {}, + } + + if (tok.isParseError()) { + return .{ + .err = .{ + .kind = .{ .basic = .{ .unexpected_token = tok.* } }, + .location = state.sourceLocation(), + }, + }; + } + last_is_delim = switch (tok.*) { + .delim, .comma => true, + else => false, + }; + + // If this is a delimiter, and the last token was whitespace, + // replace the whitespace with the delimiter since both are not required. + if (last_is_delim and last_is_whitespace) { + const last = &tokens.items[tokens.items.len - 1]; + last.* = .{ .token = tok.* }; + } else { + tokens.append( + input.allocator(), + .{ .token = tok.* }, + ) catch unreachable; + } + + last_is_whitespace = false; + } + + return .{ .result = {} }; + } +}; +pub const TokenListFns = TokenList; + +/// A color value with an unresolved alpha value (e.g. a variable). +/// These can be converted from the modern slash syntax to older comma syntax. +/// This can only be done when the only unresolved component is the alpha +/// since variables can resolve to multiple tokens. +pub const UnresolvedColor = union(enum) { + /// An rgb() color. + RGB: struct { + /// The red component. + r: f32, + /// The green component. + g: f32, + /// The blue component. + b: f32, + /// The unresolved alpha component. + alpha: TokenList, + }, + /// An hsl() color. + HSL: struct { + /// The hue component. + h: f32, + /// The saturation component. + s: f32, + /// The lightness component. + l: f32, + /// The unresolved alpha component. + alpha: TokenList, + }, + /// The light-dark() function. + light_dark: struct { + /// The light value. + light: TokenList, + /// The dark value. + dark: TokenList, + }, + const This = @This(); + + pub fn deepClone(this: *const This, allocator: Allocator) This { + return switch (this.*) { + .RGB => |*rgb| .{ .RGB = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b, .alpha = rgb.alpha.deepClone(allocator) } }, + .HSL => |*hsl| .{ .HSL = .{ .h = hsl.h, .s = hsl.s, .l = hsl.l, .alpha = hsl.alpha.deepClone(allocator) } }, + .light_dark => |*light_dark| .{ + .light_dark = .{ + .light = light_dark.light.deepClone(allocator), + .dark = light_dark.dark.deepClone(allocator), + }, + }, + }; + } + + pub fn deinit(this: *This, allocator: Allocator) void { + return switch (this.*) { + .RGB => |*rgb| rgb.alpha.deinit(allocator), + .HSL => |*hsl| hsl.alpha.deinit(allocator), + .light_dark => |*light_dark| { + light_dark.light.deinit(allocator); + light_dark.dark.deinit(allocator); + }, + }; + } + + pub fn toCss( + this: *const This, + comptime W: type, + dest: *Printer(W), + is_custom_property: bool, + ) PrintErr!void { + const Helper = struct { + pub fn conv(c: f32) i32 { + return @intFromFloat(bun.clamp(@round(c * 255.0), 0.0, 255.0)); + } + }; + + switch (this.*) { + .RGB => |rgb| { + if (dest.targets.shouldCompileSame(.space_separated_color_notation)) { + try dest.writeStr("rgba("); + try css.to_css.integer(i32, Helper.conv(rgb.r), W, dest); + try dest.delim(',', false); + try css.to_css.integer(i32, Helper.conv(rgb.g), W, dest); + try dest.delim(',', false); + try css.to_css.integer(i32, Helper.conv(rgb.b), W, dest); + try rgb.alpha.toCss(W, dest, is_custom_property); + try dest.writeChar(')'); + return; + } + + try dest.writeStr("rgb("); + try css.to_css.integer(i32, Helper.conv(rgb.r), W, dest); + try dest.writeChar(' '); + try css.to_css.integer(i32, Helper.conv(rgb.g), W, dest); + try dest.writeChar(' '); + try css.to_css.integer(i32, Helper.conv(rgb.b), W, dest); + try dest.delim('/', true); + try rgb.alpha.toCss(W, dest, is_custom_property); + try dest.writeChar(')'); + }, + .HSL => |hsl| { + if (dest.targets.shouldCompileSame(.space_separated_color_notation)) { + try dest.writeStr("hsla("); + try CSSNumberFns.toCss(&hsl.h, W, dest); + try dest.delim(',', false); + try (Percentage{ .v = hsl.s }).toCss(W, dest); + try dest.delim(',', false); + try (Percentage{ .v = hsl.l }).toCss(W, dest); + try dest.delim(',', false); + try hsl.alpha.toCss(W, dest, is_custom_property); + try dest.writeChar(')'); + return; + } + + try dest.writeStr("hsl("); + try CSSNumberFns.toCss(&hsl.h, W, dest); + try dest.writeChar(' '); + try (Percentage{ .v = hsl.s }).toCss(W, dest); + try dest.writeChar(' '); + try (Percentage{ .v = hsl.l }).toCss(W, dest); + try dest.delim('/', true); + try hsl.alpha.toCss(W, dest, is_custom_property); + try dest.writeChar(')'); + return; + }, + .light_dark => |*ld| { + const light: *const TokenList = &ld.light; + const dark: *const TokenList = &ld.dark; + + if (!dest.targets.isCompatible(.light_dark)) { + // TODO(zack): lightningcss -> buncss + try dest.writeStr("var(--lightningcss-light)"); + try dest.delim(',', false); + try light.toCss(W, dest, is_custom_property); + try dest.writeChar(')'); + try dest.whitespace(); + try dest.writeStr("var(--lightningcss-dark"); + try dest.delim(',', false); + try dark.toCss(W, dest, is_custom_property); + return dest.writeChar(')'); + } + + try dest.writeStr("light-dark("); + try light.toCss(W, dest, is_custom_property); + try dest.delim(',', false); + try dark.toCss(W, dest, is_custom_property); + try dest.writeChar(')'); + }, + } + } + + pub fn parse( + input: *css.Parser, + f: []const u8, + options: *const css.ParserOptions, + ) Result(UnresolvedColor) { + var parser = ComponentParser.new(false); + // css.todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "rgb")) { + const Closure = struct { + options: *const css.ParserOptions, + parser: *ComponentParser, + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(UnresolvedColor) { + return this.parser.parseRelative(input2, SRGB, UnresolvedColor, @This().innerParseFn, .{this.options}); + } + pub fn innerParseFn(i: *css.Parser, p: *ComponentParser, opts: *const css.ParserOptions) Result(UnresolvedColor) { + const r, const g, const b, const is_legacy = switch (css.css_values.color.parseRGBComponents(i, p)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (is_legacy) { + return .{ .err = i.newCustomError(css.ParserError.invalid_value) }; + } + if (i.expectDelim('/').asErr()) |e| return .{ .err = e }; + const alpha = switch (TokenListFns.parse(i, opts, 0)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = UnresolvedColor{ + .RGB = .{ + .r = r, + .g = g, + .b = b, + .alpha = alpha, + }, + } }; + } + }; + var closure = Closure{ + .options = options, + .parser = &parser, + }; + return input.parseNestedBlock(UnresolvedColor, &closure, Closure.parsefn); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "hsl")) { + const Closure = struct { + options: *const css.ParserOptions, + parser: *ComponentParser, + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(UnresolvedColor) { + return this.parser.parseRelative(input2, HSL, UnresolvedColor, @This().innerParseFn, .{this.options}); + } + pub fn innerParseFn(i: *css.Parser, p: *ComponentParser, opts: *const css.ParserOptions) Result(UnresolvedColor) { + const h, const s, const l, const is_legacy = switch (css.css_values.color.parseHSLHWBComponents(HSL, i, p, false)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (is_legacy) { + return .{ .err = i.newCustomError(css.ParserError.invalid_value) }; + } + if (i.expectDelim('/').asErr()) |e| return .{ .err = e }; + const alpha = switch (TokenListFns.parse(i, opts, 0)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = UnresolvedColor{ + .HSL = .{ + .h = h, + .s = s, + .l = l, + .alpha = alpha, + }, + } }; + } + }; + var closure = Closure{ + .options = options, + .parser = &parser, + }; + return input.parseNestedBlock(UnresolvedColor, &closure, Closure.parsefn); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "light-dark")) { + const Closure = struct { + options: *const css.ParserOptions, + parser: *ComponentParser, + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(UnresolvedColor) { + const light = switch (input2.parseUntilBefore(css.Delimiters{ .comma = true }, TokenList, this, @This().parsefn2)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + // TODO: fix this + errdefer light.deinit(); + if (input2.expectComma().asErr()) |e| return .{ .err = e }; + const dark = switch (TokenListFns.parse(input2, this.options, 0)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + // TODO: fix this + errdefer dark.deinit(); + return .{ .result = UnresolvedColor{ + .light_dark = .{ + .light = light, + .dark = dark, + }, + } }; + } + + pub fn parsefn2(this: *@This(), input2: *css.Parser) Result(TokenList) { + return TokenListFns.parse(input2, this.options, 1); + } + }; + var closure = Closure{ + .options = options, + .parser = &parser, + }; + return input.parseNestedBlock(UnresolvedColor, &closure, Closure.parsefn); + } else { + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + } + + pub fn lightDarkOwned(allocator: Allocator, light: UnresolvedColor, dark: UnresolvedColor) UnresolvedColor { + var lightlist = ArrayList(TokenOrValue).initCapacity(allocator, 1) catch bun.outOfMemory(); + lightlist.append(allocator, TokenOrValue{ .unresolved_color = light }) catch bun.outOfMemory(); + var darklist = ArrayList(TokenOrValue).initCapacity(allocator, 1) catch bun.outOfMemory(); + darklist.append(allocator, TokenOrValue{ .unresolved_color = dark }) catch bun.outOfMemory(); + return UnresolvedColor{ + .light_dark = .{ + .light = css.TokenList{ .v = lightlist }, + .dark = css.TokenList{ .v = darklist }, + }, + }; + } +}; + +/// A CSS variable reference. +pub const Variable = struct { + /// The variable name. + name: DashedIdentReference, + /// A fallback value in case the variable is not defined. + fallback: ?TokenList, + + const This = @This(); + + pub fn deepClone(this: *const Variable, allocator: Allocator) Variable { + return .{ + .name = this.name, + .fallback = if (this.fallback) |*fallback| fallback.deepClone(allocator) else null, + }; + } + + pub fn deinit(this: *Variable, allocator: Allocator) void { + if (this.fallback) |*fallback| { + fallback.deinit(allocator); + } + } + + pub fn parse( + input: *css.Parser, + options: *const css.ParserOptions, + depth: usize, + ) Result(This) { + const name = switch (DashedIdentReference.parseWithOptions(input, options)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + const fallback = if (input.tryParse(css.Parser.expectComma, .{}).isOk()) + switch (TokenList.parse(input, options, depth)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } + else + null; + + return .{ .result = Variable{ .name = name, .fallback = fallback } }; + } + + pub fn toCss( + this: *const This, + comptime W: type, + dest: *Printer(W), + is_custom_property: bool, + ) PrintErr!void { + try dest.writeStr("var("); + try this.name.toCss(W, dest); + if (this.fallback) |*fallback| { + try dest.delim(',', false); + try fallback.toCss(W, dest, is_custom_property); + } + return try dest.writeChar(')'); + } +}; + +/// A CSS environment variable reference. +pub const EnvironmentVariable = struct { + /// The environment variable name. + name: EnvironmentVariableName, + /// Optional indices into the dimensions of the environment variable. + /// TODO(zack): this could totally be a smallvec, why isn't it? + indices: ArrayList(CSSInteger) = ArrayList(CSSInteger){}, + /// A fallback value in case the variable is not defined. + fallback: ?TokenList, + + pub fn deepClone(this: *const EnvironmentVariable, allocator: Allocator) EnvironmentVariable { + return .{ + .name = this.name, + .indices = this.indices.clone(allocator) catch bun.outOfMemory(), + .fallback = if (this.fallback) |*fallback| fallback.deepClone(allocator) else null, + }; + } + + pub fn deinit(this: *EnvironmentVariable, allocator: Allocator) void { + this.indices.deinit(allocator); + if (this.fallback) |*fallback| { + fallback.deinit(allocator); + } + } + + pub fn parse(input: *css.Parser, options: *const css.ParserOptions, depth: usize) Result(EnvironmentVariable) { + if (input.expectFunctionMatching("env").asErr()) |e| return .{ .err = e }; + const Closure = struct { + options: *const css.ParserOptions, + depth: usize, + pub fn parsefn(this: *@This(), i: *css.Parser) Result(EnvironmentVariable) { + return EnvironmentVariable.parseNested(i, this.options, this.depth); + } + }; + var closure = Closure{ + .options = options, + .depth = depth, + }; + return input.parseNestedBlock(EnvironmentVariable, &closure, Closure.parsefn); + } + + pub fn parseNested(input: *css.Parser, options: *const css.ParserOptions, depth: usize) Result(EnvironmentVariable) { + const name = switch (EnvironmentVariableName.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + var indices = ArrayList(i32){}; + while (switch (input.tryParse(CSSIntegerFns.parse, .{})) { + .result => |v| v, + .err => null, + }) |idx| { + indices.append( + input.allocator(), + idx, + ) catch unreachable; + } + + const fallback = if (input.tryParse(css.Parser.expectComma, .{}).isOk()) + switch (TokenListFns.parse(input, options, depth + 1)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } + else + null; + + return .{ .result = EnvironmentVariable{ + .name = name, + .indices = indices, + .fallback = fallback, + } }; + } + + pub fn toCss( + this: *const EnvironmentVariable, + comptime W: type, + dest: *Printer(W), + is_custom_property: bool, + ) PrintErr!void { + try dest.writeStr("env("); + try this.name.toCss(W, dest); + + for (this.indices.items) |index| { + try dest.writeChar(' '); + try css.to_css.integer(i32, index, W, dest); + } + + if (this.fallback) |*fallback| { + try dest.delim(',', false); + try fallback.toCss(W, dest, is_custom_property); + } + + return try dest.writeChar(')'); + } +}; + +/// A CSS environment variable name. +pub const EnvironmentVariableName = union(enum) { + /// A UA-defined environment variable. + ua: UAEnvironmentVariable, + /// A custom author-defined environment variable. + custom: DashedIdentReference, + /// An unknown environment variable. + unknown: CustomIdent, + + pub fn parse(input: *css.Parser) Result(EnvironmentVariableName) { + if (input.tryParse(UAEnvironmentVariable.parse, .{}).asValue()) |ua| { + return .{ .result = .{ .ua = ua } }; + } + + if (input.tryParse(DashedIdentReference.parseWithOptions, .{ + &css.ParserOptions.default( + input.allocator(), + null, + ), + }).asValue()) |dashed| { + return .{ .result = .{ .custom = dashed } }; + } + + const ident = switch (CustomIdentFns.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .unknown = ident } }; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.*) { + .ua => |ua| ua.toCss(W, dest), + .custom => |custom| custom.toCss(W, dest), + .unknown => |unknown| CustomIdentFns.toCss(&unknown, W, dest), + }; + } +}; + +/// A UA-defined environment variable name. +pub const UAEnvironmentVariable = enum { + /// The safe area inset from the top of the viewport. + @"safe-area-inset-top", + /// The safe area inset from the right of the viewport. + @"safe-area-inset-right", + /// The safe area inset from the bottom of the viewport. + @"safe-area-inset-bottom", + /// The safe area inset from the left of the viewport. + @"safe-area-inset-left", + /// The viewport segment width. + @"viewport-segment-width", + /// The viewport segment height. + @"viewport-segment-height", + /// The viewport segment top position. + @"viewport-segment-top", + /// The viewport segment left position. + @"viewport-segment-left", + /// The viewport segment bottom position. + @"viewport-segment-bottom", + /// The viewport segment right position. + @"viewport-segment-right", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A custom CSS function. +pub const Function = struct { + /// The function name. + name: Ident, + /// The function arguments. + arguments: TokenList, + + const This = @This(); + + pub fn deepClone(this: *const Function, allocator: Allocator) Function { + return .{ + .name = this.name, + .arguments = this.arguments.deepClone(allocator), + }; + } + + pub fn deinit(this: *Function, allocator: Allocator) void { + this.arguments.deinit(allocator); + } + + pub fn toCss( + this: *const This, + comptime W: type, + dest: *Printer(W), + is_custom_property: bool, + ) PrintErr!void { + try IdentFns.toCss(&this.name, W, dest); + try dest.writeChar('('); + try this.arguments.toCss(W, dest, is_custom_property); + return try dest.writeChar(')'); + } +}; + +/// A raw CSS token, or a parsed value. +pub const TokenOrValue = union(enum) { + /// A token. + token: css.Token, + /// A parsed CSS color. + color: CssColor, + /// A color with unresolved components. + unresolved_color: UnresolvedColor, + /// A parsed CSS url. + url: Url, + /// A CSS variable reference. + @"var": Variable, + /// A CSS environment variable reference. + env: EnvironmentVariable, + /// A custom CSS function. + function: Function, + /// A length. + length: LengthValue, + /// An angle. + angle: Angle, + /// A time. + time: Time, + /// A resolution. + resolution: Resolution, + /// A dashed ident. + dashed_ident: DashedIdent, + /// An animation name. + animation_name: AnimationName, + + pub fn deepClone(this: *const TokenOrValue, allocator: Allocator) TokenOrValue { + return switch (this.*) { + .token => this.*, + .color => |*color| .{ .color = color.deepClone(allocator) }, + .unresolved_color => |*color| .{ .unresolved_color = color.deepClone(allocator) }, + .url => this.*, + .@"var" => |*@"var"| .{ .@"var" = @"var".deepClone(allocator) }, + .env => |*env| .{ .env = env.deepClone(allocator) }, + .function => |*f| .{ .function = f.deepClone(allocator) }, + .length => this.*, + .angle => this.*, + .time => this.*, + .resolution => this.*, + .dashed_ident => this.*, + .animation_name => this.*, + }; + } + + pub fn deinit(this: *TokenOrValue, allocator: Allocator) void { + return switch (this.*) { + .token => {}, + .color => |*color| color.deinit(allocator), + .unresolved_color => |*color| color.deinit(allocator), + .url => {}, + .@"var" => |*@"var"| @"var".deinit(allocator), + .env => |*env| env.deinit(allocator), + .function => |*f| f.deinit(allocator), + .length => {}, + .angle => {}, + .time => {}, + .resolution => {}, + .dashed_ident => {}, + .animation_name => {}, + }; + } + + pub fn isWhitespace(self: *const TokenOrValue) bool { + switch (self.*) { + .token => |tok| return tok == .whitespace, + else => return false, + } + } +}; + +/// A known property with an unparsed value. +/// +/// This type is used when the value of a known property could not +/// be parsed, e.g. in the case css `var()` references are encountered. +/// In this case, the raw tokens are stored instead. +pub const UnparsedProperty = struct { + /// The id of the property. + property_id: css.PropertyId, + /// The property value, stored as a raw token list. + value: TokenList, + + pub fn parse(property_id: css.PropertyId, input: *css.Parser, options: *const css.ParserOptions) Result(UnparsedProperty) { + const Closure = struct { options: *const css.ParserOptions }; + const value = switch (input.parseUntilBefore(css.Delimiters{ .bang = true, .semicolon = true }, css.TokenList, &Closure{ .options = options }, struct { + pub fn parseFn(self: *const Closure, i: *css.Parser) Result(TokenList) { + return TokenList.parse(i, self.options, 0); + } + }.parseFn)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + return .{ .result = .{ .property_id = property_id, .value = value } }; + } +}; + +/// A CSS custom property, representing any unknown property. +pub const CustomProperty = struct { + /// The name of the property. + name: CustomPropertyName, + /// The property value, stored as a raw token list. + value: TokenList, + + pub fn parse(name: CustomPropertyName, input: *css.Parser, options: *const css.ParserOptions) Result(CustomProperty) { + const Closure = struct { + options: *const css.ParserOptions, + + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(TokenList) { + return TokenListFns.parse(input2, this.options, 0); + } + }; + + var closure = Closure{ + .options = options, + }; + + const value = switch (input.parseUntilBefore( + css.Delimiters{ + .bang = true, + .semicolon = true, + }, + TokenList, + &closure, + Closure.parsefn, + )) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + return .{ .result = CustomProperty{ + .name = name, + .value = value, + } }; + } +}; + +/// A CSS custom property name. +pub const CustomPropertyName = union(enum) { + /// An author-defined CSS custom property. + custom: DashedIdent, + /// An unknown CSS property. + unknown: Ident, + + pub fn toCss(this: *const CustomPropertyName, comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.*) { + .custom => |custom| try custom.toCss(W, dest), + .unknown => |unknown| css.serializer.serializeIdentifier(unknown.v, dest) catch return dest.addFmtError(), + }; + } + + pub fn fromStr(name: []const u8) CustomPropertyName { + if (bun.strings.startsWith(name, "--")) return .{ .custom = .{ .v = name } }; + return .{ .unknown = .{ .v = name } }; + } + + pub fn asStr(self: *const CustomPropertyName) []const u8 { + switch (self.*) { + .custom => |custom| return custom.v, + .unknown => |unknown| return unknown.v, + } + } +}; + +pub fn tryParseColorToken(f: []const u8, state: *const css.ParserState, input: *css.Parser) ?CssColor { + // css.todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "rgb") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "rgba") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "hsl") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "hsla") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "hwb") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "lab") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "lch") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "oklab") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "oklch") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "color") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "color-mix") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(f, "light-dark")) + { + const s = input.state(); + input.reset(state); + if (CssColor.parse(input).asValue()) |color| { + return color; + } + input.reset(&s); + } + + return null; +} diff --git a/src/css/properties/display.zig b/src/css/properties/display.zig new file mode 100644 index 0000000000000..a469a74a9a765 --- /dev/null +++ b/src/css/properties/display.zig @@ -0,0 +1,102 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; + +/// A value for the [display](https://drafts.csswg.org/css-display-3/#the-display-properties) property. +pub const Display = union(enum) { + /// A display keyword. + keyword: DisplayKeyword, + /// The inside and outside display values. + pair: DisplayPair, +}; + +/// A value for the [visibility](https://drafts.csswg.org/css-display-3/#visibility) property. +pub const Visibility = enum { + /// The element is visible. + visible, + /// The element is hidden. + hidden, + /// The element is collapsed. + collapse, + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A `display` keyword. +/// +/// See [Display](Display). +pub const DisplayKeyword = enum { + none, + contents, + @"table-row-group", + @"table-header-group", + @"table-footer-group", + @"table-row", + @"table-cell", + @"table-column-group", + @"table-column", + @"table-caption", + @"ruby-base", + @"ruby-text", + @"ruby-base-container", + @"ruby-text-container", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A pair of inside and outside display values, as used in the `display` property. +/// +/// See [Display](Display). +pub const DisplayPair = struct { + /// The outside display value. + outside: DisplayOutside, + /// The inside display value. + inside: DisplayInside, + /// Whether this is a list item. + is_list_item: bool, +}; + +/// A [``](https://drafts.csswg.org/css-display-3/#typedef-display-outside) value. +pub const DisplayOutside = enum { + block, + @"inline", + @"run-in", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A [``](https://drafts.csswg.org/css-display-3/#typedef-display-inside) value. +pub const DisplayInside = union(enum) { + flow, + flow_root, + table, + flex: css.VendorPrefix, + box: css.VendorPrefix, + grid, + ruby, +}; diff --git a/src/css/properties/effects.zig b/src/css/properties/effects.zig new file mode 100644 index 0000000000000..34da8b7759bfe --- /dev/null +++ b/src/css/properties/effects.zig @@ -0,0 +1,77 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +/// A value for the [filter](https://drafts.fxtf.org/filter-effects-1/#FilterProperty) and +/// [backdrop-filter](https://drafts.fxtf.org/filter-effects-2/#BackdropFilterProperty) properties. +pub const FilterList = union(enum) { + /// The `none` keyword. + none, + /// A list of filter functions. + filters: SmallList(Filter, 1), +}; + +/// A [filter](https://drafts.fxtf.org/filter-effects-1/#filter-functions) function. +pub const Filter = union(enum) { + /// A `blur()` filter. + blur: Length, + /// A `brightness()` filter. + brightness: NumberOrPercentage, + /// A `contrast()` filter. + contrast: NumberOrPercentage, + /// A `grayscale()` filter. + grayscale: NumberOrPercentage, + /// A `hue-rotate()` filter. + hue_rotate: Angle, + /// An `invert()` filter. + invert: NumberOrPercentage, + /// An `opacity()` filter. + opacity: NumberOrPercentage, + /// A `saturate()` filter. + saturate: NumberOrPercentage, + /// A `sepia()` filter. + sepia: NumberOrPercentage, + /// A `drop-shadow()` filter. + drop_shadow: DropShadow, + /// A `url()` reference to an SVG filter. + url: Url, +}; + +/// A [`drop-shadow()`](https://drafts.fxtf.org/filter-effects-1/#funcdef-filter-drop-shadow) filter function. +pub const DropShadow = struct { + /// The color of the drop shadow. + color: CssColor, + /// The x offset of the drop shadow. + x_offset: Length, + /// The y offset of the drop shadow. + y_offset: Length, + /// The blur radius of the drop shadow. + blur: Length, +}; diff --git a/src/css/properties/flex.zig b/src/css/properties/flex.zig new file mode 100644 index 0000000000000..ffd283a68051b --- /dev/null +++ b/src/css/properties/flex.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +/// A value for the [flex-direction](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#propdef-flex-direction) property. +pub const FlexDirection = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [flex-wrap](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-wrap-property) property. +pub const FlexWrap = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [flex-flow](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-flow-property) shorthand property. +pub const FlexFlow = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [flex](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-property) shorthand property. +pub const Flex = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the legacy (prefixed) [box-orient](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#orientation) property. +/// Partially equivalent to `flex-direction` in the standard syntax. +pub const BoxOrient = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the legacy (prefixed) [box-orient](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#orientation) property. +/// Partially equivalent to `flex-direction` in the standard syntax. +pub const BoxDirection = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the legacy (prefixed) [box-align](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#alignment) property. +/// Equivalent to the `align-items` property in the standard syntax. +pub const BoxAlign = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the legacy (prefixed) [box-pack](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#packing) property. +/// Equivalent to the `justify-content` property in the standard syntax. +pub const BoxPack = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the legacy (prefixed) [box-lines](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#multiple) property. +/// Equivalent to the `flex-wrap` property in the standard syntax. +pub const BoxLines = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +// Old flex (2012): https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/ +/// A value for the legacy (prefixed) [flex-pack](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-pack) property. +/// Equivalent to the `justify-content` property in the standard syntax. +pub const FlexPack = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the legacy (prefixed) [flex-item-align](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-align) property. +/// Equivalent to the `align-self` property in the standard syntax. +pub const FlexItemAlign = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the legacy (prefixed) [flex-line-pack](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-line-pack) property. +/// Equivalent to the `align-content` property in the standard syntax. +pub const FlexLinePack = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); diff --git a/src/css/properties/font.zig b/src/css/properties/font.zig new file mode 100644 index 0000000000000..f6ef2943b10c3 --- /dev/null +++ b/src/css/properties/font.zig @@ -0,0 +1,616 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; + +pub const css = @import("../css_parser.zig"); +const Error = css.Error; + +const ArrayList = std.ArrayListUnmanaged; +const SmallList = css.SmallList; + +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +const css_values = css.css_values; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const Length = css.css_values.length.LengthValue; +const LengthPercentage = css_values.length.LengthPercentage; +const LengthPercentageOrAuto = css_values.length.LengthPercentageOrAuto; +const PropertyCategory = css.PropertyCategory; +const LogicalGroup = css.LogicalGroup; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSInteger = css.css_values.number.CSSInteger; +const NumberOrPercentage = css.css_values.percentage.NumberOrPercentage; +const Percentage = css.css_values.percentage.Percentage; +const Angle = css.css_values.angle.Angle; +const DashedIdentReference = css.css_values.ident.DashedIdentReference; +const Time = css.css_values.time.Time; +const EasingFunction = css.css_values.easing.EasingFunction; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const DashedIdent = css.css_values.ident.DashedIdent; +const Url = css.css_values.url.Url; +const CustomIdentList = css.css_values.ident.CustomIdentList; +const Location = css.Location; +const HorizontalPosition = css.css_values.position.HorizontalPosition; +const VerticalPosition = css.css_values.position.VerticalPosition; +const ContainerName = css.css_rules.container.ContainerName; + +/// A value for the [font-weight](https://www.w3.org/TR/css-fonts-4/#font-weight-prop) property. +pub const FontWeight = union(enum) { + /// An absolute font weight. + absolute: AbsoluteFontWeight, + /// The `bolder` keyword. + bolder, + /// The `lighter` keyword. + lighter, + + // TODO: implement this + // pub usingnamespace css.DeriveParse(@This()); + // pub usingnamespace css.DeriveToCss(@This()); + + pub inline fn default() FontWeight { + return .{ .absolute = AbsoluteFontWeight.default() }; + } + + pub fn parse(input: *css.Parser) css.Result(FontWeight) { + _ = input; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn toCss(this: *const FontWeight, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn eql(lhs: *const FontWeight, rhs: *const FontWeight) bool { + return switch (lhs.*) { + .absolute => rhs.* == .absolute and lhs.absolute.eql(&rhs.absolute), + .bolder => rhs.* == .bolder, + .lighter => rhs.* == .lighter, + }; + } +}; + +/// An [absolute font weight](https://www.w3.org/TR/css-fonts-4/#font-weight-absolute-values), +/// as used in the `font-weight` property. +/// +/// See [FontWeight](FontWeight). +pub const AbsoluteFontWeight = union(enum) { + /// An explicit weight. + weight: CSSNumber, + /// Same as `400`. + normal, + /// Same as `700`. + bold, + + pub inline fn default() AbsoluteFontWeight { + return .normal; + } + + pub fn eql(lhs: *const AbsoluteFontWeight, rhs: *const AbsoluteFontWeight) bool { + return switch (lhs.*) { + .weight => lhs.weight == rhs.weight, + .normal => rhs.* == .normal, + .bold => rhs.* == .bold, + }; + } +}; + +/// A value for the [font-size](https://www.w3.org/TR/css-fonts-4/#font-size-prop) property. +pub const FontSize = union(enum) { + /// An explicit size. + length: LengthPercentage, + /// An absolute font size keyword. + absolute: AbsoluteFontSize, + /// A relative font size keyword. + relative: RelativeFontSize, + + // TODO: implement this + // pub usingnamespace css.DeriveParse(@This()); + // pub usingnamespace css.DeriveToCss(@This()); + + pub fn parse(input: *css.Parser) css.Result(FontSize) { + _ = input; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn toCss(this: *const FontSize, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @panic(css.todo_stuff.depth); + } +}; + +/// An [absolute font size](https://www.w3.org/TR/css-fonts-3/#absolute-size-value), +/// as used in the `font-size` property. +/// +/// See [FontSize](FontSize). +pub const AbsoluteFontSize = enum { + /// "xx-small" + @"xx-small", + /// "x-small" + @"x-small", + /// "small" + small, + /// "medium" + medium, + /// "large" + large, + /// "x-large" + @"x-large", + /// "xx-large" + @"xx-large", + /// "xxx-large" + @"xxx-large", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A [relative font size](https://www.w3.org/TR/css-fonts-3/#relative-size-value), +/// as used in the `font-size` property. +/// +/// See [FontSize](FontSize). +pub const RelativeFontSize = enum { + smaller, + larger, + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A value for the [font-stretch](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop) property. +pub const FontStretch = union(enum) { + /// A font stretch keyword. + keyword: FontStretchKeyword, + /// A percentage. + percentage: Percentage, + + // TODO: implement this + // pub usingnamespace css.DeriveParse(@This()); + + pub fn parse(input: *css.Parser) css.Result(FontStretch) { + _ = input; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn toCss(this: *const FontStretch, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn eql(lhs: *const FontStretch, rhs: *const FontStretch) bool { + return lhs.keyword == rhs.keyword and lhs.percentage.v == rhs.percentage.v; + } + + pub inline fn default() FontStretch { + return .{ .keyword = FontStretchKeyword.default() }; + } +}; + +/// A [font stretch keyword](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop), +/// as used in the `font-stretch` property. +/// +/// See [FontStretch](FontStretch). +pub const FontStretchKeyword = enum { + /// 100% + normal, + /// 50% + @"ultra-condensed", + /// 62.5% + @"extra-condensed", + /// 75% + condensed, + /// 87.5% + @"semi-condensed", + /// 112.5% + @"semi-expanded", + /// 125% + expanded, + /// 150% + @"extra-expanded", + /// 200% + @"ultra-expanded", + + pub usingnamespace css.DefineEnumProperty(@This()); + + pub inline fn default() FontStretchKeyword { + return .normal; + } +}; + +/// A value for the [font-family](https://www.w3.org/TR/css-fonts-4/#font-family-prop) property. +pub const FontFamily = union(enum) { + /// A generic family name. + generic: GenericFontFamily, + /// A custom family name. + family_name: []const u8, + + pub fn parse(input: *css.Parser) css.Result(@This()) { + if (input.tryParse(css.Parser.expectString, .{}).asValue()) |value| { + return .{ .result = .{ .family_name = value } }; + } + + if (input.tryParse(GenericFontFamily.parse, .{}).asValue()) |value| { + return .{ .result = .{ .generic = value } }; + } + + const stralloc = input.allocator(); + const value = switch (input.expectIdent()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + var string: ?ArrayList(u8) = null; + while (input.tryParse(css.Parser.expectIdent, .{}).asValue()) |ident| { + if (string == null) { + string = ArrayList(u8){}; + string.?.appendSlice(stralloc, value) catch bun.outOfMemory(); + } + + if (string) |*s| { + s.append(stralloc, ' ') catch bun.outOfMemory(); + s.appendSlice(stralloc, ident) catch bun.outOfMemory(); + } + } + + const final_value = if (string) |s| s.items else value; + + return .{ .result = .{ .family_name = final_value } }; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .generic => |val| { + try val.toCss(W, dest); + }, + .family_name => |val| { + // Generic family names such as sans-serif must be quoted if parsed as a string. + // CSS wide keywords, as well as "default", must also be quoted. + // https://www.w3.org/TR/css-fonts-4/#family-name-syntax + + if (val.len > 0 and + !css.parse_utility.parseString( + dest.allocator, + GenericFontFamily, + val, + GenericFontFamily.parse, + ).isOk()) { + var id = ArrayList(u8){}; + defer id.deinit(dest.allocator); + var first = true; + var split_iter = std.mem.splitScalar(u8, val, ' '); + while (split_iter.next()) |slice| { + if (first) { + first = false; + } else { + id.append(dest.allocator, ' ') catch bun.outOfMemory(); + } + css.serializer.serializeIdentifier(slice, dest) catch return dest.addFmtError(); + } + if (id.items.len < val.len + 2) { + return dest.writeStr(id.items); + } + } + return css.serializer.serializeString(val, dest) catch return dest.addFmtError(); + }, + } + } +}; + +/// A [generic font family](https://www.w3.org/TR/css-fonts-4/#generic-font-families) name, +/// as used in the `font-family` property. +/// +/// See [FontFamily](FontFamily). +pub const GenericFontFamily = enum { + serif, + @"sans-serif", + cursive, + fantasy, + monospace, + @"system-ui", + emoji, + math, + fangsong, + @"ui-serif", + @"ui-sans-serif", + @"ui-monospace", + @"ui-rounded", + + // CSS wide keywords. These must be parsed as identifiers so they + // don't get serialized as strings. + // https://www.w3.org/TR/css-values-4/#common-keywords + initial, + inherit, + unset, + // Default is also reserved by the type. + // https://www.w3.org/TR/css-values-4/#custom-idents + default, + + // CSS defaulting keywords + // https://drafts.csswg.org/css-cascade-5/#defaulting-keywords + revert, + @"revert-layer", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A value for the [font-style](https://www.w3.org/TR/css-fonts-4/#font-style-prop) property. +pub const FontStyle = union(enum) { + /// Normal font style. + normal, + /// Italic font style. + italic, + /// Oblique font style, with a custom angle. + oblique: Angle, + + pub fn default() FontStyle { + return .normal; + } + + pub fn parse(input: *css.Parser) css.Result(FontStyle) { + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("normal", ident)) { + return .{ .result = .normal }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("italic", ident)) { + return .{ .result = .italic }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("oblique", ident)) { + const angle = input.tryParse(Angle.parse, .{}).unwrapOr(FontStyle.defaultObliqueAngle()); + return .{ .result = .{ .oblique = angle } }; + } else { + // + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + } + } + + pub fn toCss(this: *const FontStyle, comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this) { + .normal => try dest.writeStr("normal"), + .italic => try dest.writeStr("italic"), + .oblique => |angle| { + try dest.writeStr("oblique"); + if (angle != FontStyle.defaultObliqueAngle()) { + try dest.writeChar(' '); + try angle.toCss(dest); + } + }, + } + } + + pub fn defaultObliqueAngle() Angle { + return Angle{ .deg = 14.0 }; + } +}; + +/// A value for the [font-variant-caps](https://www.w3.org/TR/css-fonts-4/#font-variant-caps-prop) property. +pub const FontVariantCaps = enum { + /// No special capitalization features are applied. + normal, + /// The small capitals feature is used for lower case letters. + @"small-caps", + /// Small capitals are used for both upper and lower case letters. + @"all-small-caps", + /// Petite capitals are used. + @"petite-caps", + /// Petite capitals are used for both upper and lower case letters. + @"all-petite-caps", + /// Enables display of mixture of small capitals for uppercase letters with normal lowercase letters. + unicase, + /// Uses titling capitals. + @"titling-caps", + + pub usingnamespace css.DefineEnumProperty(@This()); + + pub fn default() FontVariantCaps { + return .normal; + } + + fn isCss2(this: *const FontVariantCaps) bool { + return switch (this.*) { + .normal, .@"small-caps" => true, + else => false, + }; + } + + pub fn parseCss2(input: *css.Parser) css.Result(FontVariantCaps) { + const value = try FontVariantCaps.parse(input); + if (!value.isCss2()) { + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + return value; + } +}; + +/// A value for the [line-height](https://www.w3.org/TR/2020/WD-css-inline-3-20200827/#propdef-line-height) property. +pub const LineHeight = union(enum) { + /// The UA sets the line height based on the font. + normal, + /// A multiple of the element's font size. + number: CSSNumber, + /// An explicit height. + length: LengthPercentage, + + // pub usingnamespace css.DeriveParse(@This()); + // pub usingnamespace css.DeriveToCss(@This()); + + pub fn parse(input: *css.Parser) css.Result(LineHeight) { + _ = input; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn toCss(this: *const LineHeight, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn default() LineHeight { + return .normal; + } +}; + +/// A value for the [font](https://www.w3.org/TR/css-fonts-4/#font-prop) shorthand property. +pub const Font = struct { + /// The font family. + family: ArrayList(FontFamily), + /// The font size. + size: FontSize, + /// The font style. + style: FontStyle, + /// The font weight. + weight: FontWeight, + /// The font stretch. + stretch: FontStretch, + /// The line height. + line_height: LineHeight, + /// How the text should be capitalized. Only CSS 2.1 values are supported. + variant_caps: FontVariantCaps, + + pub usingnamespace css.DefineShorthand(@This()); + + pub fn parse(input: *css.Parser) css.Result(Font) { + var style: ?FontStyle = null; + var weight: ?FontWeight = null; + var stretch: ?FontStretch = null; + var size: ?FontSize = null; + var variant_caps: ?FontVariantCaps = null; + var count: i32 = 0; + + while (true) { + // Skip "normal" since it is valid for several properties, but we don't know which ones it will be used for yet. + if (input.tryParse(css.Parser.expectIdentMatching, .{"normal"}).isOk()) { + count += 1; + continue; + } + + if (style == null) { + if (input.tryParse(FontStyle.parse, .{})) |value| { + style = value; + count += 1; + continue; + } + } + + if (weight == null) { + if (input.tryParse(FontWeight.parse, .{})) |value| { + weight = value; + count += 1; + continue; + } + } + + if (variant_caps != null) { + if (input.tryParse(FontVariantCaps.parseCss2, .{})) |value| { + variant_caps = value; + count += 1; + continue; + } + } + + if (stretch == null) { + if (input.tryParse(FontStretchKeyword.parse, .{})) |value| { + stretch = value; + count += 1; + continue; + } + } + + size = try FontSize.parse(input); + break; + } + + if (count > 4) return .{ .err = input.newCustomError(css.ParserError.invalid_declaration) }; + + const final_size = size orelse return .{ .err = input.newCustomError(css.ParserError.invalid_declaration) }; + + const line_height = if (input.tryParse(css.Parser.expectDelim, .{'/'}).isOk()) try LineHeight.parse(input) else null; + + const family = input.parseCommaSeparated(FontFamily, FontFamily.parse); + + return Font{ + .family = family, + .size = final_size, + .style = style orelse FontStyle.default(), + .weight = weight orelse FontWeight.default(), + .stretch = stretch orelse FontStretch.default(), + .line_height = line_height orelse LineHeight.default(), + .variant_caps = variant_caps orelse FontVariantCaps.default(), + }; + } + + pub fn toCss(this: *const Font, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (this.style != FontStyle.default()) { + try this.style.toCss(W, dest); + try dest.writeChar(' '); + } + + if (this.variant_caps != FontVariantCaps.default()) { + try this.variant_caps.toCss(W, dest); + try dest.writeChar(' '); + } + + if (this.weight != FontWeight.default()) { + try this.weight.toCss(W, dest); + try dest.writeChar(' '); + } + + if (this.stretch != FontStretch.default()) { + try this.stretch.toCss(W, dest); + try dest.writeChar(' '); + } + + try this.size.toCss(W, dest); + + if (this.line_height != LineHeight.default()) { + try dest.delim('/', true); + try this.line_height.toCss(W, dest); + } + + try dest.writeChar(' '); + + const len = this.family.items.len; + for (this.family.items, 0..) |*val, idx| { + try val.toCss(W, dest); + if (idx < len - 1) { + try dest.delim(',', false); + } + } + } +}; + +/// A value for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property. +// TODO: there is a more extensive spec in CSS3 but it doesn't seem any browser implements it? https://www.w3.org/TR/css-inline-3/#transverse-alignment +pub const VerticalAlign = union(enum) { + /// A vertical align keyword. + keyword: VerticalAlignKeyword, + /// An explicit length. + length: LengthPercentage, +}; + +/// A keyword for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property. +pub const VerticalAlignKeyword = enum { + /// Align the baseline of the box with the baseline of the parent box. + baseline, + /// Lower the baseline of the box to the proper position for subscripts of the parent’s box. + sub, + /// Raise the baseline of the box to the proper position for superscripts of the parent’s box. + super, + /// Align the top of the aligned subtree with the top of the line box. + top, + /// Align the top of the box with the top of the parent’s content area. + @"text-top", + /// Align the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent. + middle, + /// Align the bottom of the aligned subtree with the bottom of the line box. + bottom, + /// Align the bottom of the box with the bottom of the parent’s content area. + @"text-bottom", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; diff --git a/src/css/properties/generate_properties.ts b/src/css/properties/generate_properties.ts new file mode 100644 index 0000000000000..4c501b561d80f --- /dev/null +++ b/src/css/properties/generate_properties.ts @@ -0,0 +1,1803 @@ +type VendorPrefixes = "none" | "webkit" | "moz" | "ms" | "o"; + +type LogicalGroup = + | "border_color" + | "border_style" + | "border_width" + | "border_radius" + | "margin" + | "scroll_margin" + | "padding" + | "scroll_padding" + | "inset" + | "size" + | "min_size" + | "max_size"; + +type PropertyCategory = "logical" | "physical"; + +type PropertyDef = { + ty: string; + shorthand?: boolean; + valid_prefixes?: VendorPrefixes[]; + logical_group?: { + ty: LogicalGroup; + category: PropertyCategory; + }; + /// By default true + unprefixed?: boolean; + conditional?: { + css_modules: boolean; + }; +}; + +const OUTPUT_FILE = "src/css/properties/properties_generated.zig"; + +async function generateCode(property_defs: Record) { + await Bun.$`echo ${prelude()} > ${OUTPUT_FILE}`; + await Bun.$`echo ${generateProperty(property_defs)} >> ${OUTPUT_FILE}`; + await Bun.$`echo ${generatePropertyId(property_defs)} >> ${OUTPUT_FILE}`; + await Bun.$`echo ${generatePropertyIdTag(property_defs)} >> ${OUTPUT_FILE}`; + await Bun.$`vendor/zig/zig.exe fmt ${OUTPUT_FILE}`; +} + +function generatePropertyIdTag(property_defs: Record): string { + return `pub const PropertyIdTag = enum(u16) { + ${Object.keys(property_defs) + .map(key => `${escapeIdent(key)},`) + .join("\n")} + all, + unparsed, + custom, +};`; +} + +function generateProperty(property_defs: Record): string { + return `pub const Property = union(PropertyIdTag) { +${Object.entries(property_defs) + .map(([name, meta]) => generatePropertyField(name, meta)) + .join("\n")} + all: CSSWideKeyword, + unparsed: UnparsedProperty, + custom: CustomProperty, + + ${generatePropertyImpl(property_defs)} +};`; +} + +function generatePropertyImpl(property_defs: Record): string { + return ` + pub usingnamespace PropertyImpl(); + /// Parses a CSS property by name. + pub fn parse(property_id: PropertyId, input: *css.Parser, options: *const css.ParserOptions) Result(Property) { + const state = input.state(); + + switch (property_id) { + ${generatePropertyImplParseCases(property_defs)} + .all => return .{ .result = .{ .all = switch (CSSWideKeyword.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } } }, + .custom => |name| return .{ .result = .{ .custom = switch (CustomProperty.parse(name, input, options)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } } }, + else => {}, + } + + // If a value was unable to be parsed, treat as an unparsed property. + // This is different from a custom property, handled below, in that the property name is known + // and stored as an enum rather than a string. This lets property handlers more easily deal with it. + // Ideally we'd only do this if var() or env() references were seen, but err on the safe side for now. + input.reset(&state); + return .{ .result = .{ .unparsed = switch (UnparsedProperty.parse(property_id, input, options)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } } }; + } + + pub inline fn __toCssHelper(this: *const Property) struct{[]const u8, VendorPrefix} { + return switch (this.*) { + ${generatePropertyImplToCssHelper(property_defs)} + .all => .{ "all", VendorPrefix{ .none = true } }, + .unparsed => |*unparsed| brk: { + var prefix = unparsed.property_id.prefix(); + if (prefix.isEmpty()) { + prefix = VendorPrefix{ .none = true }; + } + break :brk .{ unparsed.property_id.name(), prefix }; + }, + .custom => unreachable, + }; + } + + /// Serializes the value of a CSS property without its name or \`!important\` flag. + pub fn valueToCss(this: *const Property, comptime W: type, dest: *css.Printer(W)) PrintErr!void { + return switch(this.*) { + ${Object.entries(property_defs) + .map(([name, meta]) => { + const value = meta.valid_prefixes === undefined ? "value" : "value[0]"; + return `.${escapeIdent(name)} => |*value| ${value}.toCss(W, dest),`; + }) + .join("\n")} + .all => |*keyword| keyword.toCss(W, dest), + .unparsed => |*unparsed| unparsed.value.toCss(W, dest, false), + .custom => |*c| c.value.toCss(W, dest, c.name == .custom), + }; + } + + /// Returns the given longhand property for a shorthand. + pub fn longhand(this: *const Property, property_id: *const PropertyId) ?Property { + switch (this.*) { + ${Object.entries(property_defs) + .filter(([_, meta]) => meta.shorthand) + .map(([name, meta]) => { + if (meta.valid_prefixes !== undefined) { + return `.${escapeIdent(name)} => |*v| { + if (!v[1].eq(property_id.prefix())) return null; + return v[0].longhand(property_id); + },`; + } + + return `.${escapeIdent(name)} => |*v| return v.longhand(property_id),`; + }) + .join("\n")} + else => {}, + } + return null; + } +`; +} + +function generatePropertyImplToCssHelper(property_defs: Record): string { + return Object.entries(property_defs) + .map(([name, meta]) => { + const capture = meta.valid_prefixes === undefined ? "" : "|pre|"; + const prefix = meta.valid_prefixes === undefined ? "VendorPrefix{ .none = true }" : "pre"; + return `.${escapeIdent(name)} => ${capture} .{"${name}", ${prefix}},`; + }) + .join("\n"); +} + +function generatePropertyImplParseCases(property_defs: Record): string { + return Object.entries(property_defs) + .map(([name, meta]) => { + const capture = meta.valid_prefixes === undefined ? "" : "|pre|"; + const ret = + meta.valid_prefixes === undefined + ? `.{ .${escapeIdent(name)} = c }` + : `.{ .${escapeIdent(name)} = .{ c, pre } }`; + return `.${escapeIdent(name)} => ${capture} { + if (css.generic.parseWithOptions(${meta.ty}, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = ${ret} }; + } + } +},`; + }) + .join("\n"); +} + +function generatePropertyField(name: string, meta: PropertyDef): string { + if (meta.valid_prefixes !== undefined) { + return ` ${escapeIdent(name)}: struct{ ${meta.ty}, VendorPrefix },`; + } + return ` ${escapeIdent(name)}: ${meta.ty},`; +} + +function generatePropertyId(property_defs: Record): string { + return `pub const PropertyId = union(PropertyIdTag) { +${Object.entries(property_defs) + .map(([name, meta]) => generatePropertyIdField(name, meta)) + .join("\n")} + all, + unparsed, + custom: CustomPropertyName, + +pub usingnamespace PropertyIdImpl(); + +${generatePropertyIdImpl(property_defs)} +};`; +} + +function generatePropertyIdField(name: string, meta: PropertyDef): string { + if (meta.valid_prefixes !== undefined) { + return ` ${escapeIdent(name)}: VendorPrefix,`; + } + return ` ${escapeIdent(name)},`; +} + +function generatePropertyIdImpl(property_defs: Record): string { + return ` + /// Returns the property name, without any vendor prefixes. + pub inline fn name(this: *const PropertyId) []const u8 { + return @tagName(this.*); + } + + /// Returns the vendor prefix for this property id. + pub fn prefix(this: *const PropertyId) VendorPrefix { + return switch (this.*) { + ${generatePropertyIdImplPrefix(property_defs)} + .all, .custom, .unparsed => VendorPrefix.empty(), + }; + } + + pub fn fromNameAndPrefix(name1: []const u8, pre: VendorPrefix) ?PropertyId { + // TODO: todo_stuff.match_ignore_ascii_case + ${generatePropertyIdImplFromNameAndPrefix(property_defs)} + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "all")) { + } else { + return null; + } + + return null; + } + + + pub fn withPrefix(this: *const PropertyId, pre: VendorPrefix) PropertyId { + return switch (this.*) { + ${Object.entries(property_defs) + .map(([prop_name, def]) => { + if (def.valid_prefixes === undefined) return `.${escapeIdent(prop_name)} => .${escapeIdent(prop_name)},`; + return `.${escapeIdent(prop_name)} => .{ .${escapeIdent(prop_name)} = pre },`; + }) + .join("\n")} + else => this.*, + }; + } + + pub fn addPrefix(this: *const PropertyId, pre: VendorPrefix) void { + return switch (this.*) { + ${Object.entries(property_defs) + .map(([prop_name, def]) => { + if (def.valid_prefixes === undefined) return `.${escapeIdent(prop_name)} => {},`; + return `.${escapeIdent(prop_name)} => |*p| { p.insert(pre); },`; + }) + .join("\n")} + else => {}, + }; + } +`; +} + +function generatePropertyIdImplPrefix(property_defs: Record): string { + return Object.entries(property_defs) + .map(([name, meta]) => { + if (meta.valid_prefixes === undefined) return `.${escapeIdent(name)} => VendorPrefix.empty(),`; + return `.${escapeIdent(name)} => |p| p,`; + }) + .join("\n"); +} + +// TODO: todo_stuff.match_ignore_ascii_case +function generatePropertyIdImplFromNameAndPrefix(property_defs: Record): string { + return Object.entries(property_defs) + .map(([name, meta]) => { + if (name === "unparsed") return ""; + return `if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "${name}")) { + const allowed_prefixes = ${constructVendorPrefix(meta.valid_prefixes)}; + if (allowed_prefixes.contains(pre)) return ${meta.valid_prefixes === undefined ? `.${escapeIdent(name)}` : `.{ .${escapeIdent(name)} = pre }`}; +} else `; + }) + .join("\n"); +} + +function constructVendorPrefix(prefixes: VendorPrefixes[] | undefined): string { + if (prefixes === undefined) return `VendorPrefix{ .none = true }`; + return `VendorPrefix{ ${prefixes.map(prefix => `.${prefix} = true`).join(", ")} }`; +} + +function needsEscaping(name: string): boolean { + switch (name) { + case "align": + return true; + case "var": + default: { + return ["-", "(", ")", " ", ":", ";", ","].some(c => name.includes(c)); + } + } +} + +function escapeIdent(name: string): string { + if (needsEscaping(name)) { + return `@"${name}"`; + } + return name; +} + +generateCode({ + "background-color": { + ty: "CssColor", + }, + // "background-image": { + // ty: "SmallList(Image, 1)", + // }, + // "background-position-x": { + // ty: "SmallList(css_values.position.HorizontalPosition, 1)", + // }, + // "background-position-y": { + // ty: "SmallList(css_values.position.HorizontalPosition, 1)", + // }, + // "background-position": { + // ty: "SmallList(background.BackgroundPosition, 1)", + // shorthand: true, + // }, + // "background-size": { + // ty: "SmallList(background.BackgroundSize, 1)", + // }, + // "background-repeat": { + // ty: "SmallList(background.BackgroundSize, 1)", + // }, + // "background-attachment": { + // ty: "SmallList(background.BackgroundAttachment, 1)", + // }, + // "background-clip": { + // ty: "SmallList(background.BackgroundAttachment, 1)", + // valid_prefixes: ["webkit", "moz"], + // }, + // "background-origin": { + // ty: "SmallList(background.BackgroundOrigin, 1)", + // }, + // background: { + // ty: "SmallList(background.Background, 1)", + // }, + // "box-shadow": { + // ty: "SmallList(box_shadow.BoxShadow, 1)", + // valid_prefixes: ["webkit", "moz"], + // }, + // opacity: { + // ty: "css.css_values.alpha.AlphaValue", + // }, + color: { + ty: "CssColor", + }, + // display: { + // ty: "display.Display", + // }, + // visibility: { + // ty: "display.Visibility", + // }, + // width: { + // ty: "size.Size", + // logical_group: { ty: "size", category: "physical" }, + // }, + // height: { + // ty: "size.Size", + // logical_group: { ty: "size", category: "physical" }, + // }, + // "min-width": { + // ty: "size.Size", + // logical_group: { ty: "min_size", category: "physical" }, + // }, + // "min-height": { + // ty: "size.Size", + // logical_group: { ty: "min_size", category: "physical" }, + // }, + // "max-width": { + // ty: "size.MaxSize", + // logical_group: { ty: "max_size", category: "physical" }, + // }, + // "max-height": { + // ty: "size.MaxSize", + // logical_group: { ty: "max_size", category: "physical" }, + // }, + // "block-size": { + // ty: "size.Size", + // logical_group: { ty: "size", category: "logical" }, + // }, + // "inline-size": { + // ty: "size.Size", + // logical_group: { ty: "size", category: "logical" }, + // }, + // "min-block-size": { + // ty: "size.Size", + // logical_group: { ty: "min_size", category: "logical" }, + // }, + // "min-inline-size": { + // ty: "size.Size", + // logical_group: { ty: "min_size", category: "logical" }, + // }, + // "max-block-size": { + // ty: "size.MaxSize", + // logical_group: { ty: "max_size", category: "logical" }, + // }, + // "max-inline-size": { + // ty: "size.MaxSize", + // logical_group: { ty: "max_size", category: "logical" }, + // }, + // "box-sizing": { + // ty: "size.BoxSizing", + // valid_prefixes: ["webkit", "moz"], + // }, + // "aspect-ratio": { + // ty: "size.AspectRatio", + // }, + // overflow: { + // ty: "overflow.Overflow", + // shorthand: true, + // }, + // "overflow-x": { + // ty: "overflow.OverflowKeyword", + // }, + // "overflow-y": { + // ty: "overflow.OverflowKeyword", + // }, + // "text-overflow": { + // ty: "overflow.TextOverflow", + // valid_prefixes: ["o"], + // }, + // position: { + // ty: "position.Position", + // }, + // top: { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "inset", category: "physical" }, + // }, + // bottom: { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "inset", category: "physical" }, + // }, + // left: { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "inset", category: "physical" }, + // }, + // right: { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "inset", category: "physical" }, + // }, + // "inset-block-start": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "inset", category: "logical" }, + // }, + // "inset-block-end": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "inset", category: "logical" }, + // }, + // "inset-inline-start": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "inset", category: "logical" }, + // }, + // "inset-inline-end": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "inset", category: "logical" }, + // }, + // "inset-block": { + // ty: "margin_padding.InsetBlock", + // shorthand: true, + // }, + // "inset-inline": { + // ty: "margin_padding.InsetInline", + // shorthand: true, + // }, + // inset: { + // ty: "margin_padding.Inset", + // shorthand: true, + // }, + "border-spacing": { + ty: "css.css_values.size.Size2D(Length)", + }, + "border-top-color": { + ty: "CssColor", + logical_group: { ty: "border_color", category: "physical" }, + }, + "border-bottom-color": { + ty: "CssColor", + logical_group: { ty: "border_color", category: "physical" }, + }, + "border-left-color": { + ty: "CssColor", + logical_group: { ty: "border_color", category: "physical" }, + }, + "border-right-color": { + ty: "CssColor", + logical_group: { ty: "border_color", category: "physical" }, + }, + "border-block-start-color": { + ty: "CssColor", + logical_group: { ty: "border_color", category: "logical" }, + }, + "border-block-end-color": { + ty: "CssColor", + logical_group: { ty: "border_color", category: "logical" }, + }, + "border-inline-start-color": { + ty: "CssColor", + logical_group: { ty: "border_color", category: "logical" }, + }, + "border-inline-end-color": { + ty: "CssColor", + logical_group: { ty: "border_color", category: "logical" }, + }, + "border-top-style": { + ty: "border.LineStyle", + logical_group: { ty: "border_style", category: "physical" }, + }, + "border-bottom-style": { + ty: "border.LineStyle", + logical_group: { ty: "border_style", category: "physical" }, + }, + "border-left-style": { + ty: "border.LineStyle", + logical_group: { ty: "border_style", category: "physical" }, + }, + "border-right-style": { + ty: "border.LineStyle", + logical_group: { ty: "border_style", category: "physical" }, + }, + "border-block-start-style": { + ty: "border.LineStyle", + logical_group: { ty: "border_style", category: "logical" }, + }, + "border-block-end-style": { + ty: "border.LineStyle", + logical_group: { ty: "border_style", category: "logical" }, + }, + // "border-inline-start-style": { + // ty: "border.LineStyle", + // logical_group: { ty: "border_style", category: "logical" }, + // }, + // "border-inline-end-style": { + // ty: "border.LineStyle", + // logical_group: { ty: "border_style", category: "logical" }, + // }, + // "border-top-width": { + // ty: "BorderSideWidth", + // logical_group: { ty: "border_width", category: "physical" }, + // }, + // "border-bottom-width": { + // ty: "BorderSideWidth", + // logical_group: { ty: "border_width", category: "physical" }, + // }, + // "border-left-width": { + // ty: "BorderSideWidth", + // logical_group: { ty: "border_width", category: "physical" }, + // }, + // "border-right-width": { + // ty: "BorderSideWidth", + // logical_group: { ty: "border_width", category: "physical" }, + // }, + // "border-block-start-width": { + // ty: "BorderSideWidth", + // logical_group: { ty: "border_width", category: "logical" }, + // }, + // "border-block-end-width": { + // ty: "BorderSideWidth", + // logical_group: { ty: "border_width", category: "logical" }, + // }, + // "border-inline-start-width": { + // ty: "BorderSideWidth", + // logical_group: { ty: "border_width", category: "logical" }, + // }, + // "border-inline-end-width": { + // ty: "BorderSideWidth", + // logical_group: { ty: "border_width", category: "logical" }, + // }, + // "border-top-left-radius": { + // ty: "Size2D(LengthPercentage)", + // valid_prefixes: ["webkit", "moz"], + // logical_group: { ty: "border_radius", category: "physical" }, + // }, + // "border-top-right-radius": { + // ty: "Size2D(LengthPercentage)", + // valid_prefixes: ["webkit", "moz"], + // logical_group: { ty: "border_radius", category: "physical" }, + // }, + // "border-bottom-left-radius": { + // ty: "Size2D(LengthPercentage)", + // valid_prefixes: ["webkit", "moz"], + // logical_group: { ty: "border_radius", category: "physical" }, + // }, + // "border-bottom-right-radius": { + // ty: "Size2D(LengthPercentage)", + // valid_prefixes: ["webkit", "moz"], + // logical_group: { ty: "border_radius", category: "physical" }, + // }, + // "border-start-start-radius": { + // ty: "Size2D(LengthPercentage)", + // logical_group: { ty: "border_radius", category: "logical" }, + // }, + // "border-start-end-radius": { + // ty: "Size2D(LengthPercentage)", + // logical_group: { ty: "border_radius", category: "logical" }, + // }, + // "border-end-start-radius": { + // ty: "Size2D(LengthPercentage)", + // logical_group: { ty: "border_radius", category: "logical" }, + // }, + // "border-end-end-radius": { + // ty: "Size2D(LengthPercentage)", + // logical_group: { ty: "border_radius", category: "logical" }, + // }, + // "border-radius": { + // ty: "BorderRadius", + // valid_prefixes: ["webkit", "moz"], + // shorthand: true, + // }, + // "border-image-source": { + // ty: "Image", + // }, + // "border-image-outset": { + // ty: "Rect(LengthOrNumber)", + // }, + // "border-image-repeat": { + // ty: "BorderImageRepeat", + // }, + // "border-image-width": { + // ty: "Rect(BorderImageSideWidth)", + // }, + // "border-image-slice": { + // ty: "BorderImageSlice", + // }, + // "border-image": { + // ty: "BorderImage", + // valid_prefixes: ["webkit", "moz", "o"], + // shorthand: true, + // }, + // "border-color": { + // ty: "BorderColor", + // shorthand: true, + // }, + // "border-style": { + // ty: "BorderStyle", + // shorthand: true, + // }, + // "border-width": { + // ty: "BorderWidth", + // shorthand: true, + // }, + // "border-block-color": { + // ty: "BorderBlockColor", + // shorthand: true, + // }, + // "border-block-style": { + // ty: "BorderBlockStyle", + // shorthand: true, + // }, + // "border-block-width": { + // ty: "BorderBlockWidth", + // shorthand: true, + // }, + // "border-inline-color": { + // ty: "BorderInlineColor", + // shorthand: true, + // }, + // "border-inline-style": { + // ty: "BorderInlineStyle", + // shorthand: true, + // }, + // "border-inline-width": { + // ty: "BorderInlineWidth", + // shorthand: true, + // }, + // border: { + // ty: "Border", + // shorthand: true, + // }, + // "border-top": { + // ty: "BorderTop", + // shorthand: true, + // }, + // "border-bottom": { + // ty: "BorderBottom", + // shorthand: true, + // }, + // "border-left": { + // ty: "BorderLeft", + // shorthand: true, + // }, + // "border-right": { + // ty: "BorderRight", + // shorthand: true, + // }, + // "border-block": { + // ty: "BorderBlock", + // shorthand: true, + // }, + // "border-block-start": { + // ty: "BorderBlockStart", + // shorthand: true, + // }, + // "border-block-end": { + // ty: "BorderBlockEnd", + // shorthand: true, + // }, + // "border-inline": { + // ty: "BorderInline", + // shorthand: true, + // }, + // "border-inline-start": { + // ty: "BorderInlineStart", + // shorthand: true, + // }, + // "border-inline-end": { + // ty: "BorderInlineEnd", + // shorthand: true, + // }, + // outline: { + // ty: "Outline", + // shorthand: true, + // }, + // "outline-color": { + // ty: "CssColor", + // }, + // "outline-style": { + // ty: "OutlineStyle", + // }, + // "outline-width": { + // ty: "BorderSideWidth", + // }, + // "flex-direction": { + // ty: "FlexDirection", + // valid_prefixes: ["webkit", "ms"], + // }, + // "flex-wrap": { + // ty: "FlexWrap", + // valid_prefixes: ["webkit", "ms"], + // }, + // "flex-flow": { + // ty: "FlexFlow", + // valid_prefixes: ["webkit", "ms"], + // shorthand: true, + // }, + // "flex-grow": { + // ty: "CSSNumber", + // valid_prefixes: ["webkit"], + // }, + // "flex-shrink": { + // ty: "CSSNumber", + // valid_prefixes: ["webkit"], + // }, + // "flex-basis": { + // ty: "LengthPercentageOrAuto", + // valid_prefixes: ["webkit"], + // }, + // flex: { + // ty: "Flex", + // valid_prefixes: ["webkit", "ms"], + // shorthand: true, + // }, + // order: { + // ty: "CSSInteger", + // valid_prefixes: ["webkit"], + // }, + // "align-content": { + // ty: "AlignContent", + // valid_prefixes: ["webkit"], + // }, + // "justify-content": { + // ty: "JustifyContent", + // valid_prefixes: ["webkit"], + // }, + // "place-content": { + // ty: "PlaceContent", + // shorthand: true, + // }, + // "align-self": { + // ty: "AlignSelf", + // valid_prefixes: ["webkit"], + // }, + // "justify-self": { + // ty: "JustifySelf", + // }, + // "place-self": { + // ty: "PlaceSelf", + // shorthand: true, + // }, + // "align-items": { + // ty: "AlignItems", + // valid_prefixes: ["webkit"], + // }, + // "justify-items": { + // ty: "JustifyItems", + // }, + // "place-items": { + // ty: "PlaceItems", + // shorthand: true, + // }, + // "row-gap": { + // ty: "GapValue", + // }, + // "column-gap": { + // ty: "GapValue", + // }, + // gap: { + // ty: "Gap", + // shorthand: true, + // }, + // "box-orient": { + // ty: "BoxOrient", + // valid_prefixes: ["webkit", "moz"], + // unprefixed: false, + // }, + // "box-direction": { + // ty: "BoxDirection", + // valid_prefixes: ["webkit", "moz"], + // unprefixed: false, + // }, + // "box-ordinal-group": { + // ty: "CSSInteger", + // valid_prefixes: ["webkit", "moz"], + // unprefixed: false, + // }, + // "box-align": { + // ty: "BoxAlign", + // valid_prefixes: ["webkit", "moz"], + // unprefixed: false, + // }, + // "box-flex": { + // ty: "CSSNumber", + // valid_prefixes: ["webkit", "moz"], + // unprefixed: false, + // }, + // "box-flex-group": { + // ty: "CSSInteger", + // valid_prefixes: ["webkit"], + // unprefixed: false, + // }, + // "box-pack": { + // ty: "BoxPack", + // valid_prefixes: ["webkit", "moz"], + // unprefixed: false, + // }, + // "box-lines": { + // ty: "BoxLines", + // valid_prefixes: ["webkit", "moz"], + // unprefixed: false, + // }, + // "flex-pack": { + // ty: "FlexPack", + // valid_prefixes: ["ms"], + // unprefixed: false, + // }, + // "flex-order": { + // ty: "CSSInteger", + // valid_prefixes: ["ms"], + // unprefixed: false, + // }, + // "flex-align": { + // ty: "BoxAlign", + // valid_prefixes: ["ms"], + // unprefixed: false, + // }, + // "flex-item-align": { + // ty: "FlexItemAlign", + // valid_prefixes: ["ms"], + // unprefixed: false, + // }, + // "flex-line-pack": { + // ty: "FlexLinePack", + // valid_prefixes: ["ms"], + // unprefixed: false, + // }, + // "flex-positive": { + // ty: "CSSNumber", + // valid_prefixes: ["ms"], + // unprefixed: false, + // }, + // "flex-negative": { + // ty: "CSSNumber", + // valid_prefixes: ["ms"], + // unprefixed: false, + // }, + // "flex-preferred-size": { + // ty: "LengthPercentageOrAuto", + // valid_prefixes: ["ms"], + // unprefixed: false, + // }, + // "margin-top": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "margin", category: "physical" }, + // }, + // "margin-bottom": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "margin", category: "physical" }, + // }, + // "margin-left": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "margin", category: "physical" }, + // }, + // "margin-right": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "margin", category: "physical" }, + // }, + // "margin-block-start": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "margin", category: "logical" }, + // }, + // "margin-block-end": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "margin", category: "logical" }, + // }, + // "margin-inline-start": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "margin", category: "logical" }, + // }, + // "margin-inline-end": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "margin", category: "logical" }, + // }, + // "margin-block": { + // ty: "MarginBlock", + // shorthand: true, + // }, + // "margin-inline": { + // ty: "MarginInline", + // shorthand: true, + // }, + // margin: { + // ty: "Margin", + // shorthand: true, + // }, + // "padding-top": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "padding", category: "physical" }, + // }, + // "padding-bottom": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "padding", category: "physical" }, + // }, + // "padding-left": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "padding", category: "physical" }, + // }, + // "padding-right": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "padding", category: "physical" }, + // }, + // "padding-block-start": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "padding", category: "logical" }, + // }, + // "padding-block-end": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "padding", category: "logical" }, + // }, + // "padding-inline-start": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "padding", category: "logical" }, + // }, + // "padding-inline-end": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "padding", category: "logical" }, + // }, + // "padding-block": { + // ty: "PaddingBlock", + // shorthand: true, + // }, + // "padding-inline": { + // ty: "PaddingInline", + // shorthand: true, + // }, + // padding: { + // ty: "Padding", + // shorthand: true, + // }, + // "scroll-margin-top": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_margin", category: "physical" }, + // }, + // "scroll-margin-bottom": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_margin", category: "physical" }, + // }, + // "scroll-margin-left": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_margin", category: "physical" }, + // }, + // "scroll-margin-right": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_margin", category: "physical" }, + // }, + // "scroll-margin-block-start": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_margin", category: "logical" }, + // }, + // "scroll-margin-block-end": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_margin", category: "logical" }, + // }, + // "scroll-margin-inline-start": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_margin", category: "logical" }, + // }, + // "scroll-margin-inline-end": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_margin", category: "logical" }, + // }, + // "scroll-margin-block": { + // ty: "ScrollMarginBlock", + // shorthand: true, + // }, + // "scroll-margin-inline": { + // ty: "ScrollMarginInline", + // shorthand: true, + // }, + // "scroll-margin": { + // ty: "ScrollMargin", + // shorthand: true, + // }, + // "scroll-padding-top": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_padding", category: "physical" }, + // }, + // "scroll-padding-bottom": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_padding", category: "physical" }, + // }, + // "scroll-padding-left": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_padding", category: "physical" }, + // }, + // "scroll-padding-right": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_padding", category: "physical" }, + // }, + // "scroll-padding-block-start": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_padding", category: "logical" }, + // }, + // "scroll-padding-block-end": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_padding", category: "logical" }, + // }, + // "scroll-padding-inline-start": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_padding", category: "logical" }, + // }, + // "scroll-padding-inline-end": { + // ty: "LengthPercentageOrAuto", + // logical_group: { ty: "scroll_padding", category: "logical" }, + // }, + // "scroll-padding-block": { + // ty: "ScrollPaddingBlock", + // shorthand: true, + // }, + // "scroll-padding-inline": { + // ty: "ScrollPaddingInline", + // shorthand: true, + // }, + // "scroll-padding": { + // ty: "ScrollPadding", + // shorthand: true, + // }, + // "font-weight": { + // ty: "FontWeight", + // }, + // "font-size": { + // ty: "FontSize", + // }, + // "font-stretch": { + // ty: "FontStretch", + // }, + // "font-family": { + // ty: "ArrayList(FontFamily)", + // }, + // "font-style": { + // ty: "FontStyle", + // }, + // "font-variant-caps": { + // ty: "FontVariantCaps", + // }, + // "line-height": { + // ty: "LineHeight", + // }, + // font: { + // ty: "Font", + // shorthand: true, + // }, + // "vertical-align": { + // ty: "VerticalAlign", + // }, + // "font-palette": { + // ty: "DashedIdentReference", + // }, + // "transition-property": { + // ty: "SmallList(PropertyId, 1)", + // valid_prefixes: ["webkit", "moz", "ms"], + // }, + // "transition-duration": { + // ty: "SmallList(Time, 1)", + // valid_prefixes: ["webkit", "moz", "ms"], + // }, + // "transition-delay": { + // ty: "SmallList(Time, 1)", + // valid_prefixes: ["webkit", "moz", "ms"], + // }, + // "transition-timing-function": { + // ty: "SmallList(EasingFunction, 1)", + // valid_prefixes: ["webkit", "moz", "ms"], + // }, + // transition: { + // ty: "SmallList(Transition, 1)", + // valid_prefixes: ["webkit", "moz", "ms"], + // shorthand: true, + // }, + // "animation-name": { + // ty: "AnimationNameList", + // valid_prefixes: ["webkit", "moz", "o"], + // }, + // "animation-duration": { + // ty: "SmallList(Time, 1)", + // valid_prefixes: ["webkit", "moz", "o"], + // }, + // "animation-timing-function": { + // ty: "SmallList(EasingFunction, 1)", + // valid_prefixes: ["webkit", "moz", "o"], + // }, + // "animation-iteration-count": { + // ty: "SmallList(AnimationIterationCount, 1)", + // valid_prefixes: ["webkit", "moz", "o"], + // }, + // "animation-direction": { + // ty: "SmallList(AnimationDirection, 1)", + // valid_prefixes: ["webkit", "moz", "o"], + // }, + // "animation-play-state": { + // ty: "SmallList(AnimationPlayState, 1)", + // valid_prefixes: ["webkit", "moz", "o"], + // }, + // "animation-delay": { + // ty: "SmallList(Time, 1)", + // valid_prefixes: ["webkit", "moz", "o"], + // }, + // "animation-fill-mode": { + // ty: "SmallList(AnimationFillMode, 1)", + // valid_prefixes: ["webkit", "moz", "o"], + // }, + // "animation-composition": { + // ty: "SmallList(AnimationComposition, 1)", + // }, + // "animation-timeline": { + // ty: "SmallList(AnimationTimeline, 1)", + // }, + // "animation-range-start": { + // ty: "SmallList(AnimationRangeStart, 1)", + // }, + // "animation-range-end": { + // ty: "SmallList(AnimationRangeEnd, 1)", + // }, + // "animation-range": { + // ty: "SmallList(AnimationRange, 1)", + // }, + // animation: { + // ty: "AnimationList", + // valid_prefixes: ["webkit", "moz", "o"], + // shorthand: true, + // }, + // transform: { + // ty: "TransformList", + // valid_prefixes: ["webkit", "moz", "ms", "o"], + // }, + // "transform-origin": { + // ty: "Position", + // valid_prefixes: ["webkit", "moz", "ms", "o"], + // }, + // "transform-style": { + // ty: "TransformStyle", + // valid_prefixes: ["webkit", "moz"], + // }, + // "transform-box": { + // ty: "TransformBox", + // }, + // "backface-visibility": { + // ty: "BackfaceVisibility", + // valid_prefixes: ["webkit", "moz"], + // }, + // perspective: { + // ty: "Perspective", + // valid_prefixes: ["webkit", "moz"], + // }, + // "perspective-origin": { + // ty: "Position", + // valid_prefixes: ["webkit", "moz"], + // }, + // translate: { + // ty: "Translate", + // }, + // rotate: { + // ty: "Rotate", + // }, + // scale: { + // ty: "Scale", + // }, + // "text-transform": { + // ty: "TextTransform", + // }, + // "white-space": { + // ty: "WhiteSpace", + // }, + // "tab-size": { + // ty: "LengthOrNumber", + // valid_prefixes: ["moz", "o"], + // }, + // "word-break": { + // ty: "WordBreak", + // }, + // "line-break": { + // ty: "LineBreak", + // }, + // hyphens: { + // ty: "Hyphens", + // valid_prefixes: ["webkit", "moz", "ms"], + // }, + // "overflow-wrap": { + // ty: "OverflowWrap", + // }, + // "word-wrap": { + // ty: "OverflowWrap", + // }, + // "text-align": { + // ty: "TextAlign", + // }, + // "text-align-last": { + // ty: "TextAlignLast", + // valid_prefixes: ["moz"], + // }, + // "text-justify": { + // ty: "TextJustify", + // }, + // "word-spacing": { + // ty: "Spacing", + // }, + // "letter-spacing": { + // ty: "Spacing", + // }, + // "text-indent": { + // ty: "TextIndent", + // }, + // "text-decoration-line": { + // ty: "TextDecorationLine", + // valid_prefixes: ["webkit", "moz"], + // }, + // "text-decoration-style": { + // ty: "TextDecorationStyle", + // valid_prefixes: ["webkit", "moz"], + // }, + // "text-decoration-color": { + // ty: "CssColor", + // valid_prefixes: ["webkit", "moz"], + // }, + // "text-decoration-thickness": { + // ty: "TextDecorationThickness", + // }, + // "text-decoration": { + // ty: "TextDecoration", + // valid_prefixes: ["webkit", "moz"], + // shorthand: true, + // }, + // "text-decoration-skip-ink": { + // ty: "TextDecorationSkipInk", + // valid_prefixes: ["webkit"], + // }, + // "text-emphasis-style": { + // ty: "TextEmphasisStyle", + // valid_prefixes: ["webkit"], + // }, + // "text-emphasis-color": { + // ty: "CssColor", + // valid_prefixes: ["webkit"], + // }, + // "text-emphasis": { + // ty: "TextEmphasis", + // valid_prefixes: ["webkit"], + // shorthand: true, + // }, + // "text-emphasis-position": { + // ty: "TextEmphasisPosition", + // valid_prefixes: ["webkit"], + // }, + // "text-shadow": { + // ty: "SmallList(TextShadow, 1)", + // }, + // "text-size-adjust": { + // ty: "TextSizeAdjust", + // valid_prefixes: ["webkit", "moz", "ms"], + // }, + // direction: { + // ty: "Direction", + // }, + // "unicode-bidi": { + // ty: "UnicodeBidi", + // }, + // "box-decoration-break": { + // ty: "BoxDecorationBreak", + // valid_prefixes: ["webkit"], + // }, + // resize: { + // ty: "Resize", + // }, + // cursor: { + // ty: "Cursor", + // }, + // "caret-color": { + // ty: "ColorOrAuto", + // }, + // "caret-shape": { + // ty: "CaretShape", + // }, + // caret: { + // ty: "Caret", + // shorthand: true, + // }, + // "user-select": { + // ty: "UserSelect", + // valid_prefixes: ["webkit", "moz", "ms"], + // }, + // "accent-color": { + // ty: "ColorOrAuto", + // }, + // appearance: { + // ty: "Appearance", + // valid_prefixes: ["webkit", "moz", "ms"], + // }, + // "list-style-type": { + // ty: "ListStyleType", + // }, + // "list-style-image": { + // ty: "Image", + // }, + // "list-style-position": { + // ty: "ListStylePosition", + // }, + // "list-style": { + // ty: "ListStyle", + // shorthand: true, + // }, + // "marker-side": { + // ty: "MarkerSide", + // }, + composes: { + ty: "Composes", + conditional: { css_modules: true }, + }, + // fill: { + // ty: "SVGPaint", + // }, + // "fill-rule": { + // ty: "FillRule", + // }, + // "fill-opacity": { + // ty: "AlphaValue", + // }, + // stroke: { + // ty: "SVGPaint", + // }, + // "stroke-opacity": { + // ty: "AlphaValue", + // }, + // "stroke-width": { + // ty: "LengthPercentage", + // }, + // "stroke-linecap": { + // ty: "StrokeLinecap", + // }, + // "stroke-linejoin": { + // ty: "StrokeLinejoin", + // }, + // "stroke-miterlimit": { + // ty: "CSSNumber", + // }, + // "stroke-dasharray": { + // ty: "StrokeDasharray", + // }, + // "stroke-dashoffset": { + // ty: "LengthPercentage", + // }, + // "marker-start": { + // ty: "Marker", + // }, + // "marker-mid": { + // ty: "Marker", + // }, + // "marker-end": { + // ty: "Marker", + // }, + // marker: { + // ty: "Marker", + // }, + // "color-interpolation": { + // ty: "ColorInterpolation", + // }, + // "color-interpolation-filters": { + // ty: "ColorInterpolation", + // }, + // "color-rendering": { + // ty: "ColorRendering", + // }, + // "shape-rendering": { + // ty: "ShapeRendering", + // }, + // "text-rendering": { + // ty: "TextRendering", + // }, + // "image-rendering": { + // ty: "ImageRendering", + // }, + // "clip-path": { + // ty: "ClipPath", + // valid_prefixes: ["webkit"], + // }, + // "clip-rule": { + // ty: "FillRule", + // }, + // "mask-image": { + // ty: "SmallList(Image, 1)", + // valid_prefixes: ["webkit"], + // }, + // "mask-mode": { + // ty: "SmallList(MaskMode, 1)", + // }, + // "mask-repeat": { + // ty: "SmallList(BackgroundRepeat, 1)", + // valid_prefixes: ["webkit"], + // }, + // "mask-position-x": { + // ty: "SmallList(HorizontalPosition, 1)", + // }, + // "mask-position-y": { + // ty: "SmallList(VerticalPosition, 1)", + // }, + // "mask-position": { + // ty: "SmallList(Position, 1)", + // valid_prefixes: ["webkit"], + // }, + // "mask-clip": { + // ty: "SmallList(MaskClip, 1)", + // valid_prefixes: ["webkit"], + // }, + // "mask-origin": { + // ty: "SmallList(GeometryBox, 1)", + // valid_prefixes: ["webkit"], + // }, + // "mask-size": { + // ty: "SmallList(BackgroundSize, 1)", + // valid_prefixes: ["webkit"], + // }, + // "mask-composite": { + // ty: "SmallList(MaskComposite, 1)", + // }, + // "mask-type": { + // ty: "MaskType", + // }, + // mask: { + // ty: "SmallList(Mask, 1)", + // valid_prefixes: ["webkit"], + // shorthand: true, + // }, + // "mask-border-source": { + // ty: "Image", + // }, + // "mask-border-mode": { + // ty: "MaskBorderMode", + // }, + // "mask-border-slice": { + // ty: "BorderImageSlice", + // }, + // "mask-border-width": { + // ty: "Rect(BorderImageSideWidth)", + // }, + // "mask-border-outset": { + // ty: "Rect(LengthOrNumber)", + // }, + // "mask-border-repeat": { + // ty: "BorderImageRepeat", + // }, + // "mask-border": { + // ty: "MaskBorder", + // shorthand: true, + // }, + // "-webkit-mask-composite": { + // ty: "SmallList(WebKitMaskComposite, 1)", + // }, + // "mask-source-type": { + // ty: "SmallList(WebKitMaskSourceType, 1)", + // valid_prefixes: ["webkit"], + // unprefixed: false, + // }, + // "mask-box-image": { + // ty: "BorderImage", + // valid_prefixes: ["webkit"], + // unprefixed: false, + // }, + // "mask-box-image-source": { + // ty: "Image", + // valid_prefixes: ["webkit"], + // unprefixed: false, + // }, + // "mask-box-image-slice": { + // ty: "BorderImageSlice", + // valid_prefixes: ["webkit"], + // unprefixed: false, + // }, + // "mask-box-image-width": { + // ty: "Rect(BorderImageSideWidth)", + // valid_prefixes: ["webkit"], + // unprefixed: false, + // }, + // "mask-box-image-outset": { + // ty: "Rect(LengthOrNumber)", + // valid_prefixes: ["webkit"], + // unprefixed: false, + // }, + // "mask-box-image-repeat": { + // ty: "BorderImageRepeat", + // valid_prefixes: ["webkit"], + // unprefixed: false, + // }, + // filter: { + // ty: "FilterList", + // valid_prefixes: ["webkit"], + // }, + // "backdrop-filter": { + // ty: "FilterList", + // valid_prefixes: ["webkit"], + // }, + // "z-index": { + // ty: "position.ZIndex", + // }, + // "container-type": { + // ty: "ContainerType", + // }, + // "container-name": { + // ty: "ContainerNameList", + // }, + // container: { + // ty: "Container", + // shorthand: true, + // }, + // "view-transition-name": { + // ty: "CustomIdent", + // }, + // "color-scheme": { + // ty: "ColorScheme", + // }, +}); + +function prelude() { + return /* zig */ `const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; + +pub const css = @import("../css_parser.zig"); + +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const VendorPrefix = css.VendorPrefix; + + +const PropertyImpl = @import("./properties_impl.zig").PropertyImpl; +const PropertyIdImpl = @import("./properties_impl.zig").PropertyIdImpl; + +const CSSWideKeyword = css.css_properties.CSSWideKeyword; +const UnparsedProperty = css.css_properties.custom.UnparsedProperty; +const CustomProperty = css.css_properties.custom.CustomProperty; + +const css_values = css.css_values; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const Length = css.css_values.length.Length; +const LengthValue = css.css_values.length.LengthValue; +const LengthPercentage = css_values.length.LengthPercentage; +const LengthPercentageOrAuto = css_values.length.LengthPercentageOrAuto; +const PropertyCategory = css.PropertyCategory; +const LogicalGroup = css.LogicalGroup; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSInteger = css.css_values.number.CSSInteger; +const NumberOrPercentage = css.css_values.percentage.NumberOrPercentage; +const Percentage = css.css_values.percentage.Percentage; +const Angle = css.css_values.angle.Angle; +const DashedIdentReference = css.css_values.ident.DashedIdentReference; +const Time = css.css_values.time.Time; +const EasingFunction = css.css_values.easing.EasingFunction; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const DashedIdent = css.css_values.ident.DashedIdent; +const Url = css.css_values.url.Url; +const CustomIdentList = css.css_values.ident.CustomIdentList; +const Location = css.Location; +const HorizontalPosition = css.css_values.position.HorizontalPosition; +const VerticalPosition = css.css_values.position.VerticalPosition; +const ContainerName = css.css_rules.container.ContainerName; + +pub const font = css.css_properties.font; +const border = css.css_properties.border; +const border_radius = css.css_properties.border_radius; +const border_image = css.css_properties.border_image; +const outline = css.css_properties.outline; +const flex = css.css_properties.flex; +const @"align" = css.css_properties.@"align"; +const margin_padding = css.css_properties.margin_padding; +const transition = css.css_properties.transition; +const animation = css.css_properties.animation; +const transform = css.css_properties.transform; +const text = css.css_properties.text; +const ui = css.css_properties.ui; +const list = css.css_properties.list; +const css_modules = css.css_properties.css_modules; +const svg = css.css_properties.svg; +const shape = css.css_properties.shape; +const masking = css.css_properties.masking; +const background = css.css_properties.background; +const effects = css.css_properties.effects; +const contain = css.css_properties.contain; +const custom = css.css_properties.custom; +const position = css.css_properties.position; +const box_shadow = css.css_properties.box_shadow; +const size = css.css_properties.size; +const overflow = css.css_properties.overflow; + +// const BorderSideWidth = border.BorderSideWith; +// const Size2D = css_values.size.Size2D; +// const BorderRadius = border_radius.BorderRadius; +// const Rect = css_values.rect.Rect; +// const LengthOrNumber = css_values.length.LengthOrNumber; +// const BorderImageRepeat = border_image.BorderImageRepeat; +// const BorderImageSideWidth = border_image.BorderImageSideWidth; +// const BorderImageSlice = border_image.BorderImageSlice; +// const BorderImage = border_image.BorderImage; +// const BorderColor = border.BorderColor; +// const BorderStyle = border.BorderStyle; +// const BorderWidth = border.BorderWidth; +// const BorderBlockColor = border.BorderBlockColor; +// const BorderBlockStyle = border.BorderBlockStyle; +// const BorderBlockWidth = border.BorderBlockWidth; +// const BorderInlineColor = border.BorderInlineColor; +// const BorderInlineStyle = border.BorderInlineStyle; +// const BorderInlineWidth = border.BorderInlineWidth; +// const Border = border.Border; +// const BorderTop = border.BorderTop; +// const BorderRight = border.BorderRight; +// const BorderLeft = border.BorderLeft; +// const BorderBottom = border.BorderBottom; +// const BorderBlockStart = border.BorderBlockStart; +// const BorderBlockEnd = border.BorderBlockEnd; +// const BorderInlineStart = border.BorderInlineStart; +// const BorderInlineEnd = border.BorderInlineEnd; +// const BorderBlock = border.BorderBlock; +// const BorderInline = border.BorderInline; +// const Outline = outline.Outline; +// const OutlineStyle = outline.OutlineStyle; +// const FlexDirection = flex.FlexDirection; +// const FlexWrap = flex.FlexWrap; +// const FlexFlow = flex.FlexFlow; +// const Flex = flex.Flex; +// const BoxOrient = flex.BoxOrient; +// const BoxDirection = flex.BoxDirection; +// const BoxAlign = flex.BoxAlign; +// const BoxPack = flex.BoxPack; +// const BoxLines = flex.BoxLines; +// const FlexPack = flex.FlexPack; +// const FlexItemAlign = flex.FlexItemAlign; +// const FlexLinePack = flex.FlexLinePack; +// const AlignContent = @"align".AlignContent; +// const JustifyContent = @"align".JustifyContent; +// const PlaceContent = @"align".PlaceContent; +// const AlignSelf = @"align".AlignSelf; +// const JustifySelf = @"align".JustifySelf; +// const PlaceSelf = @"align".PlaceSelf; +// const AlignItems = @"align".AlignItems; +// const JustifyItems = @"align".JustifyItems; +// const PlaceItems = @"align".PlaceItems; +// const GapValue = @"align".GapValue; +// const Gap = @"align".Gap; +// const MarginBlock = margin_padding.MarginBlock; +// const Margin = margin_padding.Margin; +// const MarginInline = margin_padding.MarginInline; +// const PaddingBlock = margin_padding.PaddingBlock; +// const PaddingInline = margin_padding.PaddingInline; +// const Padding = margin_padding.Padding; +// const ScrollMarginBlock = margin_padding.ScrollMarginBlock; +// const ScrollMarginInline = margin_padding.ScrollMarginInline; +// const ScrollMargin = margin_padding.ScrollMargin; +// const ScrollPaddingBlock = margin_padding.ScrollPaddingBlock; +// const ScrollPaddingInline = margin_padding.ScrollPaddingInline; +// const ScrollPadding = margin_padding.ScrollPadding; +// const FontWeight = font.FontWeight; +// const FontSize = font.FontSize; +// const FontStretch = font.FontStretch; +// const FontFamily = font.FontFamily; +// const FontStyle = font.FontStyle; +// const FontVariantCaps = font.FontVariantCaps; +// const LineHeight = font.LineHeight; +// const Font = font.Font; +// const VerticalAlign = font.VerticalAlign; +// const Transition = transition.Transition; +// const AnimationNameList = animation.AnimationNameList; +// const AnimationList = animation.AnimationList; +// const AnimationIterationCount = animation.AnimationIterationCount; +// const AnimationDirection = animation.AnimationDirection; +// const AnimationPlayState = animation.AnimationPlayState; +// const AnimationFillMode = animation.AnimationFillMode; +// const AnimationComposition = animation.AnimationComposition; +// const AnimationTimeline = animation.AnimationTimeline; +// const AnimationRangeStart = animation.AnimationRangeStart; +// const AnimationRangeEnd = animation.AnimationRangeEnd; +// const AnimationRange = animation.AnimationRange; +// const TransformList = transform.TransformList; +// const TransformStyle = transform.TransformStyle; +// const TransformBox = transform.TransformBox; +// const BackfaceVisibility = transform.BackfaceVisibility; +// const Perspective = transform.Perspective; +// const Translate = transform.Translate; +// const Rotate = transform.Rotate; +// const Scale = transform.Scale; +// const TextTransform = text.TextTransform; +// const WhiteSpace = text.WhiteSpace; +// const WordBreak = text.WordBreak; +// const LineBreak = text.LineBreak; +// const Hyphens = text.Hyphens; +// const OverflowWrap = text.OverflowWrap; +// const TextAlign = text.TextAlign; +// const TextIndent = text.TextIndent; +// const Spacing = text.Spacing; +// const TextJustify = text.TextJustify; +// const TextAlignLast = text.TextAlignLast; +// const TextDecorationLine = text.TextDecorationLine; +// const TextDecorationStyle = text.TextDecorationStyle; +// const TextDecorationThickness = text.TextDecorationThickness; +// const TextDecoration = text.TextDecoration; +// const TextDecorationSkipInk = text.TextDecorationSkipInk; +// const TextEmphasisStyle = text.TextEmphasisStyle; +// const TextEmphasis = text.TextEmphasis; +// const TextEmphasisPositionVertical = text.TextEmphasisPositionVertical; +// const TextEmphasisPositionHorizontal = text.TextEmphasisPositionHorizontal; +// const TextEmphasisPosition = text.TextEmphasisPosition; +// const TextShadow = text.TextShadow; +// const TextSizeAdjust = text.TextSizeAdjust; +// const Direction = text.Direction; +// const UnicodeBidi = text.UnicodeBidi; +// const BoxDecorationBreak = text.BoxDecorationBreak; +// const Resize = ui.Resize; +// const Cursor = ui.Cursor; +// const ColorOrAuto = ui.ColorOrAuto; +// const CaretShape = ui.CaretShape; +// const Caret = ui.Caret; +// const UserSelect = ui.UserSelect; +// const Appearance = ui.Appearance; +// const ColorScheme = ui.ColorScheme; +// const ListStyleType = list.ListStyleType; +// const ListStylePosition = list.ListStylePosition; +// const ListStyle = list.ListStyle; +// const MarkerSide = list.MarkerSide; +const Composes = css_modules.Composes; +// const SVGPaint = svg.SVGPaint; +// const FillRule = shape.FillRule; +// const AlphaValue = shape.AlphaValue; +// const StrokeLinecap = svg.StrokeLinecap; +// const StrokeLinejoin = svg.StrokeLinejoin; +// const StrokeDasharray = svg.StrokeDasharray; +// const Marker = svg.Marker; +// const ColorInterpolation = svg.ColorInterpolation; +// const ColorRendering = svg.ColorRendering; +// const ShapeRendering = svg.ShapeRendering; +// const TextRendering = svg.TextRendering; +// const ImageRendering = svg.ImageRendering; +// const ClipPath = masking.ClipPath; +// const MaskMode = masking.MaskMode; +// const MaskClip = masking.MaskClip; +// const GeometryBox = masking.GeometryBox; +// const MaskComposite = masking.MaskComposite; +// const MaskType = masking.MaskType; +// const Mask = masking.Mask; +// const MaskBorderMode = masking.MaskBorderMode; +// const MaskBorder = masking.MaskBorder; +// const WebKitMaskComposite = masking.WebKitMaskComposite; +// const WebKitMaskSourceType = masking.WebKitMaskSourceType; +// const BackgroundRepeat = background.BackgroundRepeat; +// const BackgroundSize = background.BackgroundSize; +// const FilterList = effects.FilterList; +// const ContainerType = contain.ContainerType; +// const Container = contain.Container; +// const ContainerNameList = contain.ContainerNameList; +const CustomPropertyName = custom.CustomPropertyName; +// const display = css.css_properties.display; + +const Position = position.Position; + +const Result = css.Result; + +const ArrayList = std.ArrayListUnmanaged; +const SmallList = css.SmallList; + +`; +} diff --git a/src/css/properties/list.zig b/src/css/properties/list.zig new file mode 100644 index 0000000000000..9c6e488a4fa5f --- /dev/null +++ b/src/css/properties/list.zig @@ -0,0 +1,86 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +/// A value for the [list-style-type](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#text-markers) property. +pub const ListStyleType = union(enum) { + /// No marker. + none, + /// An explicit marker string. + string: CSSString, + /// A named counter style. + counter_style: CounterStyle, +}; + +/// A [counter-style](https://www.w3.org/TR/css-counter-styles-3/#typedef-counter-style) name. +pub const CounterStyle = union(enum) { + /// A predefined counter style name. + predefined: PredefinedCounterStyle, + /// A custom counter style name. + name: CustomIdent, + /// An inline `symbols()` definition. + symbols: Symbols, + + const Symbols = struct { + /// The counter system. + system: SymbolsType, + /// The symbols. + symbols: ArrayList(Symbol), + }; +}; + +/// A single [symbol](https://www.w3.org/TR/css-counter-styles-3/#funcdef-symbols) as used in the +/// `symbols()` function. +/// +/// See [CounterStyle](CounterStyle). +const Symbol = union(enum) { + /// A string. + string: CSSString, + /// An image. + image: Image, +}; + +/// A [predefined counter](https://www.w3.org/TR/css-counter-styles-3/#predefined-counters) style. +pub const PredefinedCounterStyle = @compileError(css.todo_stuff.depth); + +/// A [``](https://www.w3.org/TR/css-counter-styles-3/#typedef-symbols-type) value, +/// as used in the `symbols()` function. +/// +/// See [CounterStyle](CounterStyle). +pub const SymbolsType = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [list-style-position](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#list-style-position-property) property. +pub const ListStylePosition = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [list-style](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#list-style-property) shorthand property. +pub const ListStyle = @compileError(css.todo_stuff.depth); + +/// A value for the [marker-side](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#marker-side) property. +pub const MarkerSide = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); diff --git a/src/css/properties/margin_padding.zig b/src/css/properties/margin_padding.zig new file mode 100644 index 0000000000000..ff77a06207f14 --- /dev/null +++ b/src/css/properties/margin_padding.zig @@ -0,0 +1,280 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +/// A value for the [inset](https://drafts.csswg.org/css-logical/#propdef-inset) shorthand property. +pub const Inset = struct { + top: LengthPercentageOrAuto, + right: LengthPercentageOrAuto, + bottom: LengthPercentageOrAuto, + left: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.inset); + pub usingnamespace css.DefineRectShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .top = css.PropertyIdTag.top, + .right = css.PropertyIdTag.right, + .bottom = css.PropertyIdTag.bottom, + .left = css.PropertyIdTag.left, + }; +}; + +/// A value for the [inset-block](https://drafts.csswg.org/css-logical/#propdef-inset-block) shorthand property. +pub const InsetBlock = struct { + /// The block start value. + block_start: LengthPercentageOrAuto, + /// The block end value. + block_end: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"inset-block"); + pub usingnamespace css.DefineSizeShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .block_start = css.PropertyIdTag.@"inset-block-start", + .block_end = css.PropertyIdTag.@"inset-block-end", + }; +}; + +/// A value for the [inset-inline](https://drafts.csswg.org/css-logical/#propdef-inset-inline) shorthand property. +pub const InsetInline = struct { + /// The inline start value. + inline_start: LengthPercentageOrAuto, + /// The inline end value. + inline_end: LengthPercentageOrAuto, + + pub const PropertyFieldMap = .{ + .inline_start = css.PropertyIdTag.@"inset-inline-start", + .inline_end = css.PropertyIdTag.@"inset-inline-end", + }; + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"inset-inline"); + pub usingnamespace css.DefineSizeShorthand(@This(), LengthPercentageOrAuto); +}; + +/// A value for the [margin-block](https://drafts.csswg.org/css-logical/#propdef-margin-block) shorthand property. +pub const MarginBlock = struct { + /// The block start value. + block_start: LengthPercentageOrAuto, + /// The block end value. + block_end: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"margin-block"); + pub usingnamespace css.DefineSizeShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .block_start = css.PropertyIdTag.@"margin-block-start", + .block_end = css.PropertyIdTag.@"margin-block-end", + }; +}; + +/// A value for the [margin-inline](https://drafts.csswg.org/css-logical/#propdef-margin-inline) shorthand property. +pub const MarginInline = struct { + /// The inline start value. + inline_start: LengthPercentageOrAuto, + /// The inline end value. + inline_end: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"margin-inline"); + pub usingnamespace css.DefineSizeShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .inline_start = css.PropertyIdTag.@"margin-inline-start", + .inline_end = css.PropertyIdTag.@"margin-inline-end", + }; +}; + +/// A value for the [margin](https://drafts.csswg.org/css-box-4/#propdef-margin) shorthand property. +pub const Margin = struct { + top: LengthPercentageOrAuto, + right: LengthPercentageOrAuto, + bottom: LengthPercentageOrAuto, + left: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.margin); + pub usingnamespace css.DefineRectShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .top = css.PropertyIdTag.@"margin-top", + .right = css.PropertyIdTag.@"margin-right", + .bottom = css.PropertyIdTag.@"margin-bottom", + .left = css.PropertyIdTag.@"margin-left", + }; +}; + +/// A value for the [padding-block](https://drafts.csswg.org/css-logical/#propdef-padding-block) shorthand property. +pub const PaddingBlock = struct { + /// The block start value. + block_start: LengthPercentageOrAuto, + /// The block end value. + block_end: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"padding-block"); + pub usingnamespace css.DefineSizeShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .block_start = css.PropertyIdTag.@"padding-block-start", + .block_end = css.PropertyIdTag.@"padding-block-end", + }; +}; + +/// A value for the [padding-inline](https://drafts.csswg.org/css-logical/#propdef-padding-inline) shorthand property. +pub const PaddingInline = struct { + /// The inline start value. + inline_start: LengthPercentageOrAuto, + /// The inline end value. + inline_end: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"padding-inline"); + pub usingnamespace css.DefineSizeShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .inline_start = css.PropertyIdTag.@"padding-inline-start", + .inline_end = css.PropertyIdTag.@"padding-inline-end", + }; +}; + +/// A value for the [padding](https://drafts.csswg.org/css-box-4/#propdef-padding) shorthand property. +pub const Padding = struct { + top: LengthPercentageOrAuto, + right: LengthPercentageOrAuto, + bottom: LengthPercentageOrAuto, + left: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.padding); + pub usingnamespace css.DefineRectShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .top = css.PropertyIdTag.@"padding-top", + .right = css.PropertyIdTag.@"padding-right", + .bottom = css.PropertyIdTag.@"padding-bottom", + .left = css.PropertyIdTag.@"padding-left", + }; +}; + +/// A value for the [scroll-margin-block](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-margin-block) shorthand property. +pub const ScrollMarginBlock = struct { + /// The block start value. + block_start: LengthPercentageOrAuto, + /// The block end value. + block_end: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"scroll-margin-block"); + pub usingnamespace css.DefineSizeShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .block_start = css.PropertyIdTag.@"scroll-margin-block-start", + .block_end = css.PropertyIdTag.@"scroll-margin-block-end", + }; +}; + +/// A value for the [scroll-margin-inline](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-margin-inline) shorthand property. +pub const ScrollMarginInline = struct { + /// The inline start value. + inline_start: LengthPercentageOrAuto, + /// The inline end value. + inline_end: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"scroll-margin-inline"); + pub usingnamespace css.DefineSizeShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .inline_start = css.PropertyIdTag.@"scroll-margin-inline-start", + .inline_end = css.PropertyIdTag.@"scroll-margin-inline-end", + }; +}; + +/// A value for the [scroll-margin](https://drafts.csswg.org/css-scroll-snap/#scroll-margin) shorthand property. +pub const ScrollMargin = struct { + top: LengthPercentageOrAuto, + right: LengthPercentageOrAuto, + bottom: LengthPercentageOrAuto, + left: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"scroll-margin"); + pub usingnamespace css.DefineRectShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .top = css.PropertyIdTag.@"scroll-margin-top", + .right = css.PropertyIdTag.@"scroll-margin-right", + .bottom = css.PropertyIdTag.@"scroll-margin-bottom", + .left = css.PropertyIdTag.@"scroll-margin-left", + }; +}; + +/// A value for the [scroll-padding-block](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-padding-block) shorthand property. +pub const ScrollPaddingBlock = struct { + /// The block start value. + block_start: LengthPercentageOrAuto, + /// The block end value. + block_end: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"scroll-padding-block"); + pub usingnamespace css.DefineSizeShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .block_start = css.PropertyIdTag.@"scroll-padding-block-start", + .block_end = css.PropertyIdTag.@"scroll-padding-block-end", + }; +}; + +/// A value for the [scroll-padding-inline](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-padding-inline) shorthand property. +pub const ScrollPaddingInline = struct { + /// The inline start value. + inline_start: LengthPercentageOrAuto, + /// The inline end value. + inline_end: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"scroll-padding-inline"); + pub usingnamespace css.DefineSizeShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .inline_start = css.PropertyIdTag.@"scroll-padding-inline-start", + .inline_end = css.PropertyIdTag.@"scroll-padding-inline-end", + }; +}; + +/// A value for the [scroll-padding](https://drafts.csswg.org/css-scroll-snap/#scroll-padding) shorthand property. +pub const ScrollPadding = struct { + top: LengthPercentageOrAuto, + right: LengthPercentageOrAuto, + bottom: LengthPercentageOrAuto, + left: LengthPercentageOrAuto, + + pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.@"scroll-padding"); + pub usingnamespace css.DefineRectShorthand(@This(), LengthPercentageOrAuto); + + pub const PropertyFieldMap = .{ + .top = css.PropertyIdTag.@"scroll-padding-top", + .right = css.PropertyIdTag.@"scroll-padding-right", + .bottom = css.PropertyIdTag.@"scroll-padding-bottom", + .left = css.PropertyIdTag.@"scroll-padding-left", + }; +}; diff --git a/src/css/properties/masking.zig b/src/css/properties/masking.zig new file mode 100644 index 0000000000000..8511c1a37e6f4 --- /dev/null +++ b/src/css/properties/masking.zig @@ -0,0 +1,161 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +const Position = css.css_properties.position.Position; +const BorderRadius = css.css_properties.border_radius.BorderRadius; +const FillRule = css.css_properties.shape.FillRule; + +/// A value for the [clip-path](https://www.w3.org/TR/css-masking-1/#the-clip-path) property. +const ClipPath = union(enum) { + /// No clip path. + None, + /// A url reference to an SVG path element. + Url: Url, + /// A basic shape, positioned according to the reference box. + Shape: struct { + /// A basic shape. + // todo_stuff.think_about_mem_mgmt + shape: *BasicShape, + /// A reference box that the shape is positioned according to. + reference_box: GeometryBox, + }, + /// A reference box. + Box: GeometryBox, +}; + +/// A [``](https://www.w3.org/TR/css-masking-1/#typedef-geometry-box) value +/// as used in the `mask-clip` and `clip-path` properties. +const GeometryBox = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A CSS [``](https://www.w3.org/TR/css-shapes-1/#basic-shape-functions) value. +const BasicShape = union(enum) { + /// An inset rectangle. + Inset: InsetRect, + /// A circle. + Circle: Circle, + /// An ellipse. + Ellipse: Ellipse, + /// A polygon. + Polygon: Polygon, +}; + +/// An [`inset()`](https://www.w3.org/TR/css-shapes-1/#funcdef-inset) rectangle shape. +const InsetRect = struct { + /// The rectangle. + rect: Rect(LengthPercentage), + /// A corner radius for the rectangle. + radius: BorderRadius, +}; + +/// A [`circle()`](https://www.w3.org/TR/css-shapes-1/#funcdef-circle) shape. +pub const Circle = struct { + /// The radius of the circle. + radius: ShapeRadius, + /// The position of the center of the circle. + position: Position, +}; + +/// An [`ellipse()`](https://www.w3.org/TR/css-shapes-1/#funcdef-ellipse) shape. +pub const Ellipse = struct { + /// The x-radius of the ellipse. + radius_x: ShapeRadius, + /// The y-radius of the ellipse. + radius_y: ShapeRadius, + /// The position of the center of the ellipse. + position: Position, +}; + +/// A [`polygon()`](https://www.w3.org/TR/css-shapes-1/#funcdef-polygon) shape. +pub const Polygon = struct { + /// The fill rule used to determine the interior of the polygon. + fill_rule: FillRule, + /// The points of each vertex of the polygon. + points: ArrayList(Point), +}; + +/// A [``](https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius) value +/// that defines the radius of a `circle()` or `ellipse()` shape. +pub const ShapeRadius = union(enum) { + /// An explicit length or percentage. + LengthPercentage: LengthPercentage, + /// The length from the center to the closest side of the box. + ClosestSide, + /// The length from the center to the farthest side of the box. + FarthestSide, +}; + +/// A point within a `polygon()` shape. +/// +/// See [Polygon](Polygon). +pub const Point = struct { + /// The x position of the point. + x: LengthPercentage, + /// The y position of the point. + y: LengthPercentage, +}; + +/// A value for the [mask-mode](https://www.w3.org/TR/css-masking-1/#the-mask-mode) property. +const MaskMode = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [mask-clip](https://www.w3.org/TR/css-masking-1/#the-mask-clip) property. +const MaskClip = union(enum) { + /// A geometry box. + GeometryBox: GeometryBox, + /// The painted content is not clipped. + NoClip, +}; + +/// A value for the [mask-composite](https://www.w3.org/TR/css-masking-1/#the-mask-composite) property. +pub const MaskComposite = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [mask-type](https://www.w3.org/TR/css-masking-1/#the-mask-type) property. +pub const MaskType = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [mask](https://www.w3.org/TR/css-masking-1/#the-mask) shorthand property. +pub const Mask = @compileError(css.todo_stuff.depth); + +/// A value for the [mask-border-mode](https://www.w3.org/TR/css-masking-1/#the-mask-border-mode) property. +pub const MaskBorderMode = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [mask-border](https://www.w3.org/TR/css-masking-1/#the-mask-border) shorthand property. +pub const MaskBorder = @compileError(css.todo_stuff.depth); + +/// A value for the [-webkit-mask-composite](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-mask-composite) +/// property. +/// +/// See also [MaskComposite](MaskComposite). +pub const WebKitMaskComposite = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [-webkit-mask-source-type](https://github.com/WebKit/WebKit/blob/6eece09a1c31e47489811edd003d1e36910e9fd3/Source/WebCore/css/CSSProperties.json#L6578-L6587) +/// property. +/// +/// See also [MaskMode](MaskMode). +pub const WebKitMaskSourceType = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); diff --git a/src/css/properties/outline.zig b/src/css/properties/outline.zig new file mode 100644 index 0000000000000..d19b7cb70dd01 --- /dev/null +++ b/src/css/properties/outline.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +const GenericBorder = css.css_properties.border.GenericBorder; +const LineStyle = css.css_properties.border.LineStyle; + +/// A value for the [outline](https://drafts.csswg.org/css-ui/#outline) shorthand property. +pub const Outline = GenericBorder(OutlineStyle, 11); + +/// A value for the [outline-style](https://drafts.csswg.org/css-ui/#outline-style) property. +pub const OutlineStyle = union(enum) { + /// The `auto` keyword. + auto: void, + /// A value equivalent to the `border-style` property. + line_style: LineStyle, +}; diff --git a/src/css/properties/overflow.zig b/src/css/properties/overflow.zig new file mode 100644 index 0000000000000..fc56a7e479efc --- /dev/null +++ b/src/css/properties/overflow.zig @@ -0,0 +1,85 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +const GenericBorder = css.css_properties.border.GenericBorder; +const LineStyle = css.css_properties.border.LineStyle; + +/// A value for the [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) shorthand property. +pub const Overflow = struct { + /// A value for the [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) shorthand property. + x: OverflowKeyword, + /// The overflow mode for the y direction. + y: OverflowKeyword, + + pub fn parse(input: *css.Parser) css.Result(Overflow) { + const x = try OverflowKeyword.parse(input); + const y = switch (input.tryParse(OverflowKeyword.parse, .{})) { + .result => |v| v, + else => x, + }; + return .{ .result = Overflow{ .x = x, .y = y } }; + } + + pub fn toCss(this: *const Overflow, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + try this.x.toCss(W, dest); + if (this.y != this.x) { + try dest.writeChar(' '); + try this.y.toCss(W, dest); + } + } +}; + +/// An [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) keyword +/// as used in the `overflow-x`, `overflow-y`, and `overflow` properties. +pub const OverflowKeyword = enum { + /// Overflowing content is visible. + visible, + /// Overflowing content is hidden. Programmatic scrolling is allowed. + hidden, + /// Overflowing content is clipped. Programmatic scrolling is not allowed. + clip, + /// The element is scrollable. + scroll, + /// Overflowing content scrolls if needed. + auto, + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +/// A value for the [text-overflow](https://www.w3.org/TR/css-overflow-3/#text-overflow) property. +pub const TextOverflow = enum { + /// Overflowing text is clipped. + clip, + /// Overflowing text is truncated with an ellipsis. + ellipsis, + + pub usingnamespace css.DefineEnumProperty(@This()); +}; diff --git a/src/css/properties/position.zig b/src/css/properties/position.zig new file mode 100644 index 0000000000000..8c311b64c23b7 --- /dev/null +++ b/src/css/properties/position.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +const GenericBorder = css.css_properties.border.GenericBorder; +const LineStyle = css.css_properties.border.LineStyle; + +/// A value for the [position](https://www.w3.org/TR/css-position-3/#position-property) property. +pub const Position = union(enum) { + /// The box is laid in the document flow. + static, + /// The box is laid out in the document flow and offset from the resulting position. + relative, + /// The box is taken out of document flow and positioned in reference to its relative ancestor. + absolute, + /// Similar to relative but adjusted according to the ancestor scrollable element. + sticky: css.VendorPrefix, + /// The box is taken out of the document flow and positioned in reference to the page viewport. + fixed, +}; diff --git a/src/css/properties/properties.zig b/src/css/properties/properties.zig new file mode 100644 index 0000000000000..f0efc330636ed --- /dev/null +++ b/src/css/properties/properties.zig @@ -0,0 +1,1879 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const css = @import("../css_parser.zig"); +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Position = position.Position; +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const SmallList = css.SmallList; + +pub const CustomPropertyName = @import("./custom.zig").CustomPropertyName; + +pub const @"align" = @import("./align.zig"); +pub const animation = @import("./animation.zig"); +pub const background = @import("./background.zig"); +pub const border = @import("./border.zig"); +pub const border_image = @import("./border_image.zig"); +pub const border_radius = @import("./border_radius.zig"); +pub const box_shadow = @import("./box_shadow.zig"); +pub const contain = @import("./contain.zig"); +pub const css_modules = @import("./css_modules.zig"); +pub const custom = @import("./custom.zig"); +pub const display = @import("./display.zig"); +pub const effects = @import("./effects.zig"); +pub const flex = @import("./flex.zig"); +pub const font = @import("./font.zig"); +pub const list = @import("./list.zig"); +pub const margin_padding = @import("./margin_padding.zig"); +pub const masking = @import("./masking.zig"); +pub const outline = @import("./outline.zig"); +pub const overflow = @import("./overflow.zig"); +pub const position = @import("./position.zig"); +pub const shape = @import("./shape.zig"); +pub const size = @import("./size.zig"); +pub const svg = @import("./svg.zig"); +pub const text = @import("./text.zig"); +pub const transform = @import("./transform.zig"); +pub const transition = @import("./transition.zig"); +pub const ui = @import("./ui.zig"); + +const generated = @import("./properties_generated.zig"); +pub const PropertyId = generated.PropertyId; +pub const Property = generated.Property; +pub const PropertyIdTag = generated.PropertyIdTag; + +/// A [CSS-wide keyword](https://drafts.csswg.org/css-cascade-5/#defaulting-keywords). +pub const CSSWideKeyword = enum { + /// The property's initial value. + initial, + /// The property's computed value on the parent element. + inherit, + /// Either inherit or initial depending on whether the property is inherited. + unset, + /// Rolls back the cascade to the cascaded value of the earlier origin. + revert, + /// Rolls back the cascade to the value of the previous cascade layer. + @"revert-layer", + + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +// pub fn DefineProperties(comptime properties: anytype) type { +// const input_fields: []const std.builtin.Type.StructField = std.meta.fields(@TypeOf(properties)); +// const total_fields_len = input_fields.len + 2; // +2 for the custom property and the `all` property +// const TagSize = u16; +// const PropertyIdT, const max_enum_name_length: usize = brk: { +// var max: usize = 0; +// var property_id_type = std.builtin.Type.Enum{ +// .tag_type = TagSize, +// .is_exhaustive = true, +// .decls = &.{}, +// .fields = undefined, +// }; +// var enum_fields: [total_fields_len]std.builtin.Type.EnumField = undefined; +// for (input_fields, 0..) |field, i| { +// enum_fields[i] = .{ +// .name = field.name, +// .value = i, +// }; +// max = @max(max, field.name.len); +// } +// enum_fields[input_fields.len] = std.builtin.Type.EnumField{ +// .name = "all", +// .value = input_fields.len, +// // }; +// // enum_fields[input_fields.len + 1] = std.builtin.Type.EnumField{ +// .name = "custom", +// .value = input_fields.len + 1, +// }; +// property_id_type.fields = &enum_fields; +// break :brk .{ property_id_type, max }; +// }; + +// const types: []const type = types: { +// var types: [total_fields_len]type = undefined; +// inline for (input_fields, 0..) |field, i| { +// types[i] = @field(properties, field.name).ty; + +// if (std.mem.eql(u8, field.name, "transition-property")) { +// types[i] = struct { SmallList(PropertyIdT, 1), css.VendorPrefix }; +// } + +// // Validate it + +// const value = @field(properties, field.name); +// const ValueT = @TypeOf(value); +// const value_ty = value.ty; +// const ValueTy = @TypeOf(value_ty); +// const value_ty_info = @typeInfo(ValueTy); +// // If `valid_prefixes` is defined, the `ty` should be a two item tuple where +// // the second item is of type `VendorPrefix` +// if (@hasField(ValueT, "valid_prefixes")) { +// if (!value_ty_info.Struct.is_tuple) { +// @compileError("Expected a tuple type for `ty` when `valid_prefixes` is defined"); +// } +// if (value_ty_info.Struct.fields[1].type != css.VendorPrefix) { +// @compileError("Expected the second item in the tuple to be of type `VendorPrefix`"); +// } +// } +// } +// types[input_fields.len] = void; +// types[input_fields.len + 1] = CustomPropertyName; +// break :types &types; +// }; +// const PropertyT = PropertyT: { +// var union_fields: [total_fields_len]std.builtin.Type.UnionField = undefined; +// inline for (input_fields, 0..) |input_field, i| { +// const Ty = types[i]; +// union_fields[i] = std.builtin.Type.UnionField{ +// .alignment = @alignOf(Ty), +// .type = type, +// .name = input_field.name, +// }; +// } +// union_fields[input_fields.len] = std.builtin.Type.UnionField{ +// .alignment = 0, +// .type = void, +// .name = "all", +// }; +// union_fields[input_fields.len + 1] = std.builtin.Type.UnionField{ +// .alignment = @alignOf(CustomPropertyName), +// .type = CustomPropertyName, +// .name = "custom", +// }; +// break :PropertyT std.builtin.Type.Union{ +// .layout = .auto, +// .tag_type = PropertyIdT, +// .decls = &.{}, +// .fields = union_fields, +// }; +// }; +// _ = PropertyT; // autofix +// return struct { +// pub const PropertyId = PropertyIdT; + +// pub fn propertyIdEq(lhs: PropertyId, rhs: PropertyId) bool { +// _ = lhs; // autofix +// _ = rhs; // autofix +// @compileError(css.todo_stuff.depth); +// } + +// pub fn propertyIdIsShorthand(id: PropertyId) bool { +// inline for (std.meta.fields(PropertyId)) |field| { +// if (field.value == @intFromEnum(id)) { +// const is_shorthand = if (@hasField(@TypeOf(@field(properties, field.name)), "shorthand")) +// @field(@field(properties, field.name), "shorthand") +// else +// false; +// return is_shorthand; +// } +// } +// return false; +// } + +// /// PropertyId.prefix() +// pub fn propertyIdPrefix(id: PropertyId) css.VendorPrefix { +// _ = id; // autofix +// @compileError(css.todo_stuff.depth); +// } + +// /// PropertyId.name() +// pub fn propertyIdName(id: PropertyId) []const u8 { +// _ = id; // autofix +// @compileError(css.todo_stuff.depth); +// } + +// pub fn propertyIdFromStr(name: []const u8) PropertyId { +// const prefix, const name_ref = if (bun.strings.startsWithCaseInsensitiveAscii(name, "-webkit-")) +// .{ css.VendorPrefix.webkit, name[8..] } +// else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-moz-")) +// .{ css.VendorPrefix.moz, name[5..] } +// else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-o-")) +// .{ css.VendorPrefix.moz, name[3..] } +// else if (bun.strings.startsWithCaseInsensitiveAscii(name, "-ms-")) +// .{ css.VendorPrefix.moz, name[4..] } +// else +// .{ css.VendorPrefix.none, name }; + +// return parsePropertyIdFromNameAndPrefix(name_ref, prefix) catch .{ +// .custom = CustomPropertyName.fromStr(name), +// }; +// } + +// pub fn parsePropertyIdFromNameAndPrefix(name: []const u8, prefix: css.VendorPrefix) Error!PropertyId { +// var buffer: [max_enum_name_length]u8 = undefined; +// if (name.len > buffer.len) { +// // TODO: actual source just returns empty Err(()) +// return Error.InvalidPropertyName; +// } +// const lower = bun.strings.copyLowercase(name, buffer[0..name.len]); +// inline for (std.meta.fields(PropertyIdT)) |field_| { +// const field: std.builtin.Type.EnumField = field_; +// // skip custom +// if (bun.strings.eql(field.name, "custom")) continue; + +// if (bun.strings.eql(lower, field.name)) { +// const prop = @field(properties, field.name); +// const allowed_prefixes = allowed_prefixes: { +// var prefixes: css.VendorPrefix = if (@hasField(@TypeOf(prop), "unprefixed") and !prop.unprefixed) +// css.VendorPrefix.empty() +// else +// css.VendorPrefix{ .none = true }; + +// if (@hasField(@TypeOf(prop), "valid_prefixes")) { +// prefixes = css.VendorPrefix.bitwiseOr(prefixes, prop.valid_prefixes); +// } + +// break :allowed_prefixes prefixes; +// }; + +// if (allowed_prefixes.contains(prefix)) return @enumFromInt(field.value); +// } +// } +// return Error.InvalidPropertyName; +// } +// }; +// } + +// /// SmallList(PropertyId) +// const SmallListPropertyIdPlaceholder = struct {}; + +// pub const Property = DefineProperties(.{ +// .@"background-color" = .{ +// .ty = CssColor, +// }, +// .@"background-image" = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = SmallList(Image, 1), +// }, +// .@"background-position-x" = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = SmallList(css_values.position.HorizontalPosition, 1), +// }, +// .@"background-position-y" = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = SmallList(css_values.position.HorizontalPosition, 1), +// }, +// .@"background-position" = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = SmallList(background.BackgroundPosition, 1), +// .shorthand = true, +// }, +// .@"background-size" = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = SmallList(background.BackgroundSize, 1), +// }, +// .@"background-repeat" = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = SmallList(background.BackgroundSize, 1), +// }, +// .@"background-attachment" = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = SmallList(background.BackgroundAttachment, 1), +// }, +// .@"background-clip" = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = struct { +// SmallList(background.BackgroundAttachment, 1), +// css.VendorPrefix, +// }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// }, +// .@"background-origin" = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = SmallList(background.BackgroundOrigin, 1), +// }, +// .background = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = SmallList(background.Background, 1), +// }, + +// .@"box-shadow" = .{ +// // PERF: make this equivalent to SmallVec<[_; 1]> +// .ty = struct { SmallList(box_shadow.BoxShadow, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// }, +// .opacity = .{ +// .ty = css.css_values.alpha.AlphaValue, +// }, +// .color = .{ +// .ty = CssColor, +// }, +// .display = .{ +// .ty = display.Display, +// }, +// .visibility = .{ +// .ty = display.Visibility, +// }, + +// .width = .{ +// .ty = size.Size, +// .logical_group = .{ .ty = LogicalGroup.size, .category = PropertyCategory.physical }, +// }, +// .height = .{ +// .ty = size.Size, +// .logical_group = .{ .ty = LogicalGroup.size, .category = PropertyCategory.physical }, +// }, +// .@"min-width" = .{ +// .ty = size.Size, +// .logical_group = .{ .ty = LogicalGroup.min_size, .category = PropertyCategory.physical }, +// }, +// .@"min-height" = .{ +// .ty = size.Size, +// .logical_group = .{ .ty = LogicalGroup.min_size, .category = PropertyCategory.physical }, +// }, +// .@"max-width" = .{ +// .ty = size.MaxSize, +// .logical_group = .{ .ty = LogicalGroup.max_size, .category = PropertyCategory.physical }, +// }, +// .@"max-height" = .{ +// .ty = size.MaxSize, +// .logical_group = .{ .ty = LogicalGroup.max_size, .category = PropertyCategory.physical }, +// }, +// .@"block-size" = .{ +// .ty = size.Size, +// .logical_group = .{ .ty = LogicalGroup.size, .category = PropertyCategory.logical }, +// }, +// .@"inline-size" = .{ +// .ty = size.Size, +// .logical_group = .{ .ty = LogicalGroup.size, .category = PropertyCategory.logical }, +// }, +// .min_block_size = .{ +// .ty = size.Size, +// .logical_group = .{ .ty = LogicalGroup.min_size, .category = PropertyCategory.logical }, +// }, +// .@"min-inline-size" = .{ +// .ty = size.Size, +// .logical_group = .{ .ty = LogicalGroup.min_size, .category = PropertyCategory.logical }, +// }, +// .@"max-block-size" = .{ +// .ty = size.MaxSize, +// .logical_group = .{ .ty = LogicalGroup.max_size, .category = PropertyCategory.logical }, +// }, +// .@"max-inline-size" = .{ +// .ty = size.MaxSize, +// .logical_group = .{ .ty = LogicalGroup.max_size, .category = PropertyCategory.logical }, +// }, +// .@"box-sizing" = .{ +// .ty = struct { size.BoxSizing, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// }, +// .@"aspect-ratio" = .{ +// .ty = size.AspectRatio, +// }, + +// .overflow = .{ +// .ty = overflow.Overflow, +// .shorthand = true, +// }, +// .@"overflow-x" = .{ +// .ty = overflow.OverflowKeyword, +// }, +// .@"overflow-y" = .{ +// .ty = overflow.OverflowKeyword, +// }, +// .@"text-overflow" = .{ +// .ty = struct { overflow.TextOverflow, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .o = true, +// }, +// }, + +// // https://www.w3.org/TR/2020/WD-css-position-3-20200519 +// .position = .{ +// .ty = position.Position, +// }, +// .top = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.physical }, +// }, +// .bottom = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.physical }, +// }, +// .left = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.physical }, +// }, +// .right = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.physical }, +// }, +// .@"inset-block-start" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.logical }, +// }, +// .@"inset-block-end" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.logical }, +// }, +// .@"inset-inline-start" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.logical }, +// }, +// .@"inset-inline-end" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.inset, .category = PropertyCategory.logical }, +// }, +// .@"inset-block" = .{ +// .ty = margin_padding.InsetBlock, +// .shorthand = true, +// }, +// .@"inset-inline" = .{ +// .ty = margin_padding.InsetInline, +// .shorthand = true, +// }, +// .inset = .{ +// .ty = margin_padding.Inset, +// .shorthand = true, +// }, + +// .@"border-spacing" = .{ +// .ty = css.css_values.size.Size(Length), +// }, + +// .@"border-top-color" = .{ +// .ty = CssColor, +// .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.physical }, +// }, +// .@"border-bottom-color" = .{ +// .ty = CssColor, +// .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.physical }, +// }, +// .@"border-left-color" = .{ +// .ty = CssColor, +// .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.physical }, +// }, +// .@"border-right-color" = .{ +// .ty = CssColor, +// .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.physical }, +// }, +// .@"border-block-start-color" = .{ +// .ty = CssColor, +// .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.logical }, +// }, +// .@"border-block-end-color" = .{ +// .ty = CssColor, +// .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.logical }, +// }, +// .@"border-inline-start-color" = .{ +// .ty = CssColor, +// .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.logical }, +// }, +// .@"border-inline-end-color" = .{ +// .ty = CssColor, +// .logical_group = .{ .ty = LogicalGroup.border_color, .category = PropertyCategory.logical }, +// }, + +// .@"border-top-style" = .{ +// .ty = border.LineStyle, +// .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.physical }, +// }, +// .@"border-bottom-style" = .{ +// .ty = border.LineStyle, +// .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.physical }, +// }, +// .@"border-left-style" = .{ +// .ty = border.LineStyle, +// .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.physical }, +// }, +// .@"border-right-style" = .{ +// .ty = border.LineStyle, +// .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.physical }, +// }, +// .@"border-block-start-style" = .{ +// .ty = border.LineStyle, +// .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.logical }, +// }, +// .@"border-block-end-style" = .{ +// .ty = border.LineStyle, +// .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.logical }, +// }, +// .@"border-inline-start-style" = .{ +// .ty = border.LineStyle, +// .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.logical }, +// }, +// .@"border-inline-end-style" = .{ +// .ty = border.LineStyle, +// .logical_group = .{ .ty = LogicalGroup.border_style, .category = PropertyCategory.logical }, +// }, + +// .@"border-top-width" = .{ +// .ty = BorderSideWidth, +// .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.physical }, +// }, +// .@"border-bottom-width" = .{ +// .ty = BorderSideWidth, +// .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.physical }, +// }, +// .@"border-left-width" = .{ +// .ty = BorderSideWidth, +// .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.physical }, +// }, +// .@"border-right-width" = .{ +// .ty = BorderSideWidth, +// .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.physical }, +// }, +// .@"border-block-start-width" = .{ +// .ty = BorderSideWidth, +// .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.logical }, +// }, +// .@"border-block-end-width" = .{ +// .ty = BorderSideWidth, +// .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.logical }, +// }, +// .@"border-inline-start-width" = .{ +// .ty = BorderSideWidth, +// .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.logical }, +// }, +// .@"border-inline-end-width" = .{ +// .ty = BorderSideWidth, +// .logical_group = .{ .ty = LogicalGroup.border_width, .category = PropertyCategory.logical }, +// }, + +// .@"border-top-left-radius" = .{ +// .ty = struct { Size2D(LengthPercentage), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.physical }, +// }, +// .@"border-top-right-radius" = .{ +// .ty = struct { Size2D(LengthPercentage), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.physical }, +// }, +// .@"border-bottom-left-radius" = .{ +// .ty = struct { Size2D(LengthPercentage), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.physical }, +// }, +// .@"border-bottom-right-radius" = .{ +// .ty = struct { Size2D(LengthPercentage), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.physical }, +// }, +// .@"border-start-start-radius" = .{ +// .ty = Size2D(LengthPercentage), +// .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.logical }, +// }, +// .@"border-start-end-radius" = .{ +// .ty = Size2D(LengthPercentage), +// .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.logical }, +// }, +// .@"border-end-start-radius" = .{ +// .ty = Size2D(LengthPercentage), +// .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.logical }, +// }, +// .@"border-end-end-radius" = .{ +// .ty = Size2D(LengthPercentage), +// .logical_group = .{ .ty = LogicalGroup.border_radius, .category = PropertyCategory.logical }, +// }, +// .@"border-radius" = .{ +// .ty = struct { BorderRadius, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .shorthand = true, +// }, + +// .@"border-image-source" = .{ +// .ty = Image, +// }, +// .@"border-image-outset" = .{ +// .ty = Rect(LengthOrNumber), +// }, +// .@"border-image-repeat" = .{ +// .ty = BorderImageRepeat, +// }, +// .@"border-image-width" = .{ +// .ty = Rect(BorderImageSideWidth), +// }, +// .@"border-image-slice" = .{ +// .ty = BorderImageSlice, +// }, +// .@"border-image" = .{ +// .ty = struct { BorderImage, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .o = true, +// }, +// .shorthand = true, +// }, + +// .@"border-color" = .{ +// .ty = BorderColor, +// .shorthand = true, +// }, +// .@"border-style" = .{ +// .ty = BorderStyle, +// .shorthand = true, +// }, +// .@"border-width" = .{ +// .ty = BorderWidth, +// .shorthand = true, +// }, + +// .@"border-block-color" = .{ +// .ty = BorderBlockColor, +// .shorthand = true, +// }, +// .@"border-block-style" = .{ +// .ty = BorderBlockStyle, +// .shorthand = true, +// }, +// .@"border-block-width" = .{ +// .ty = BorderBlockWidth, +// .shorthand = true, +// }, + +// .@"border-inline-color" = .{ +// .ty = BorderInlineColor, +// .shorthand = true, +// }, +// .@"border-inline-style" = .{ +// .ty = BorderInlineStyle, +// .shorthand = true, +// }, +// .@"border-inline-width" = .{ +// .ty = BorderInlineWidth, +// .shorthand = true, +// }, + +// .border = .{ +// .ty = Border, +// .shorthand = true, +// }, +// .@"border-top" = .{ +// .ty = BorderTop, +// .shorthand = true, +// }, +// .@"border-bottom" = .{ +// .ty = BorderBottom, +// .shorthand = true, +// }, +// .@"border-left" = .{ +// .ty = BorderLeft, +// .shorthand = true, +// }, +// .@"border-right" = .{ +// .ty = BorderRight, +// .shorthand = true, +// }, +// .@"border-block" = .{ +// .ty = BorderBlock, +// .shorthand = true, +// }, +// .@"border-block-start" = .{ +// .ty = BorderBlockStart, +// .shorthand = true, +// }, +// .@"border-block-end" = .{ +// .ty = BorderBlockEnd, +// .shorthand = true, +// }, +// .@"border-inline" = .{ +// .ty = BorderInline, +// .shorthand = true, +// }, +// .@"border-inline-start" = .{ +// .ty = BorderInlineStart, +// .shorthand = true, +// }, +// .@"border-inline-end" = .{ +// .ty = BorderInlineEnd, +// .shorthand = true, +// }, + +// .outline = .{ +// .ty = Outline, +// .shorthand = true, +// }, +// .@"outline-color" = .{ +// .ty = CssColor, +// }, +// .@"outline-style" = .{ +// .ty = OutlineStyle, +// }, +// .@"outline-width" = .{ +// .ty = BorderSideWidth, +// }, + +// // Flex properties: https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119 +// .@"flex-direction" = .{ +// .ty = struct { FlexDirection, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .ms = true, +// }, +// }, +// .@"flex-wrap" = .{ +// .ty = struct { FlexWrap, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .ms = true, +// }, +// }, +// .@"flex-flow" = .{ +// .ty = struct { FlexFlow, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .ms = true, +// }, +// .shorthand = true, +// }, +// .@"flex-grow" = .{ +// .ty = struct { CSSNumber, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"flex-shrink" = .{ +// .ty = struct { CSSNumber, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"flex-basis" = .{ +// .ty = struct { LengthPercentageOrAuto, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .flex = .{ +// .ty = struct { Flex, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .ms = true, +// }, +// .shorthand = true, +// }, +// .order = .{ +// .ty = struct { CSSInteger, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, + +// // Align properties: https://www.w3.org/TR/2020/WD-css-align-3-20200421 +// .@"align-content" = .{ +// .ty = struct { AlignContent, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"justify-content" = .{ +// .ty = struct { JustifyContent, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"place-content" = .{ +// .ty = PlaceContent, +// .shorthand = true, +// }, +// .@"align-self" = .{ +// .ty = struct { AlignSelf, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"justify-self" = .{ +// .ty = JustifySelf, +// }, +// .@"place-self" = .{ +// .ty = PlaceSelf, +// .shorthand = true, +// }, +// .@"align-items" = .{ +// .ty = struct { AlignItems, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"justify-items" = .{ +// .ty = JustifyItems, +// }, +// .@"place-items" = .{ +// .ty = PlaceItems, +// .shorthand = true, +// }, +// .@"row-gap" = .{ +// .ty = GapValue, +// }, +// .@"column-gap" = .{ +// .ty = GapValue, +// }, +// .gap = .{ +// .ty = Gap, +// .shorthand = true, +// }, + +// // Old flex (2009): https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/ +// .@"box-orient" = .{ +// .ty = struct { BoxOrient, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .unprefixed = false, +// }, +// .@"box-direction" = .{ +// .ty = struct { BoxDirection, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .unprefixed = false, +// }, +// .@"box-ordinal-group" = .{ +// .ty = struct { CSSInteger, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .unprefixed = false, +// }, +// .@"box-align" = .{ +// .ty = struct { BoxAlign, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .unprefixed = false, +// }, +// .@"box-flex" = .{ +// .ty = struct { CSSNumber, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .unprefixed = false, +// }, +// .@"box-flex-group" = .{ +// .ty = struct { CSSInteger, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// .unprefixed = false, +// }, +// .@"box-pack" = .{ +// .ty = struct { BoxPack, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .unprefixed = false, +// }, +// .@"box-lines" = .{ +// .ty = struct { BoxLines, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .unprefixed = false, +// }, + +// // Old flex (2012): https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/ +// .@"flex-pack" = .{ +// .ty = struct { FlexPack, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .ms = true, +// }, +// .unprefixed = false, +// }, +// .@"flex-order" = .{ +// .ty = struct { CSSInteger, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .ms = true, +// }, +// .unprefixed = false, +// }, +// .@"flex-align" = .{ +// .ty = struct { BoxAlign, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .ms = true, +// }, +// .unprefixed = false, +// }, +// .@"flex-item-align" = .{ +// .ty = struct { FlexItemAlign, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .ms = true, +// }, +// .unprefixed = false, +// }, +// .@"flex-line-pack" = .{ +// .ty = struct { FlexLinePack, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .ms = true, +// }, +// .unprefixed = false, +// }, + +// // Microsoft extensions +// .@"flex-positive" = .{ +// .ty = struct { CSSNumber, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .ms = true, +// }, +// .unprefixed = false, +// }, +// .@"flex-negative" = .{ +// .ty = struct { CSSNumber, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .ms = true, +// }, +// .unprefixed = false, +// }, +// .@"flex-preferred-size" = .{ +// .ty = struct { LengthPercentageOrAuto, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .ms = true, +// }, +// .unprefixed = false, +// }, + +// // TODO: the following is enabled with #[cfg(feature = "grid")] +// // .@"grid-template-columns" = .{ +// // .ty = TrackSizing, +// // }, +// // .@"grid-template-rows" = .{ +// // .ty = TrackSizing, +// // }, +// // .@"grid-auto-columns" = .{ +// // .ty = TrackSizeList, +// // }, +// // .@"grid-auto-rows" = .{ +// // .ty = TrackSizeList, +// // }, +// // .@"grid-auto-flow" = .{ +// // .ty = GridAutoFlow, +// // }, +// // .@"grid-template-areas" = .{ +// // .ty = GridTemplateAreas, +// // }, +// // .@"grid-template" = .{ +// // .ty = GridTemplate, +// // .shorthand = true, +// // }, +// // .grid = .{ +// // .ty = Grid, +// // .shorthand = true, +// // }, +// // .@"grid-row-start" = .{ +// // .ty = GridLine, +// // }, +// // .@"grid-row-end" = .{ +// // .ty = GridLine, +// // }, +// // .@"grid-column-start" = .{ +// // .ty = GridLine, +// // }, +// // .@"grid-column-end" = .{ +// // .ty = GridLine, +// // }, +// // .@"grid-row" = .{ +// // .ty = GridRow, +// // .shorthand = true, +// // }, +// // .@"grid-column" = .{ +// // .ty = GridColumn, +// // .shorthand = true, +// // }, +// // .@"grid-area" = .{ +// // .ty = GridArea, +// // .shorthand = true, +// // }, + +// .@"margin-top" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.physical }, +// }, +// .@"margin-bottom" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.physical }, +// }, +// .@"margin-left" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.physical }, +// }, +// .@"margin-right" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.physical }, +// }, +// .@"margin-block-start" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.logical }, +// }, +// .@"margin-block-end" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.logical }, +// }, +// .@"margin-inline-start" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.logical }, +// }, +// .@"margin-inline-end" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.margin, .category = PropertyCategory.logical }, +// }, +// .@"margin-block" = .{ +// .ty = MarginBlock, +// .shorthand = true, +// }, +// .@"margin-inline" = .{ +// .ty = MarginInline, +// .shorthand = true, +// }, +// .margin = .{ +// .ty = Margin, +// .shorthand = true, +// }, + +// .@"padding-top" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.physical }, +// }, +// .@"padding-bottom" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.physical }, +// }, +// .@"padding-left" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.physical }, +// }, +// .@"padding-right" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.physical }, +// }, +// .@"padding-block-start" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.logical }, +// }, +// .@"padding-block-end" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.logical }, +// }, +// .@"padding-inline-start" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.logical }, +// }, +// .@"padding-inline-end" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.padding, .category = PropertyCategory.logical }, +// }, +// .@"padding-block" = .{ +// .ty = PaddingBlock, +// .shorthand = true, +// }, +// .@"padding-inline" = .{ +// .ty = PaddingInline, +// .shorthand = true, +// }, +// .padding = .{ +// .ty = Padding, +// .shorthand = true, +// }, + +// .@"scroll-margin-top" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.physical }, +// }, +// .@"scroll-margin-bottom" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.physical }, +// }, +// .@"scroll-margin-left" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.physical }, +// }, +// .@"scroll-margin-right" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.physical }, +// }, +// .@"scroll-margin-block-start" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.logical }, +// }, +// .@"scroll-margin-block-end" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.logical }, +// }, +// .@"scroll-margin-inline-start" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.logical }, +// }, +// .@"scroll-margin-inline-end" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_margin, .category = PropertyCategory.logical }, +// }, +// .@"scroll-margin-block" = .{ +// .ty = ScrollMarginBlock, +// .shorthand = true, +// }, +// .@"scroll-margin-inline" = .{ +// .ty = ScrollMarginInline, +// .shorthand = true, +// }, +// .@"scroll-margin" = .{ +// .ty = ScrollMargin, +// .shorthand = true, +// }, + +// .@"scroll-padding-top" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.physical }, +// }, +// .@"scroll-padding-bottom" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.physical }, +// }, +// .@"scroll-padding-left" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.physical }, +// }, +// .@"scroll-padding-right" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.physical }, +// }, +// .@"scroll-padding-block-start" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.logical }, +// }, +// .@"scroll-padding-block-end" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.logical }, +// }, +// .@"scroll-padding-inline-start" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.logical }, +// }, +// .@"scroll-padding-inline-end" = .{ +// .ty = LengthPercentageOrAuto, +// .logical_group = .{ .ty = LogicalGroup.scroll_padding, .category = PropertyCategory.logical }, +// }, +// .@"scroll-padding-block" = .{ +// .ty = ScrollPaddingBlock, +// .shorthand = true, +// }, +// .@"scroll-padding-inline" = .{ +// .ty = ScrollPaddingInline, +// .shorthand = true, +// }, +// .@"scroll-padding" = .{ +// .ty = ScrollPadding, +// .shorthand = true, +// }, + +// .@"font-weight" = .{ +// .ty = FontWeight, +// }, +// .@"font-size" = .{ +// .ty = FontSize, +// }, +// .@"font-stretch" = .{ +// .ty = FontStretch, +// }, +// .@"font-family" = .{ +// .ty = ArrayList(FontFamily), +// }, +// .@"font-style" = .{ +// .ty = FontStyle, +// }, +// .@"font-variant-caps" = .{ +// .ty = FontVariantCaps, +// }, +// .@"line-height" = .{ +// .ty = LineHeight, +// }, +// .font = .{ +// .ty = Font, +// .shorthand = true, +// }, +// .@"vertical-align" = .{ +// .ty = VerticalAlign, +// }, +// .@"font-palette" = .{ +// .ty = DashedIdentReference, +// }, + +// .@"transition-property" = .{ +// .ty = struct { SmallListPropertyIdPlaceholder, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// }, +// }, +// .@"transition-duration" = .{ +// .ty = struct { SmallList(Time, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// }, +// }, +// .@"transition-delay" = .{ +// .ty = struct { SmallList(Time, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// }, +// }, +// .@"transition-timing-function" = .{ +// .ty = struct { SmallList(EasingFunction, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// }, +// }, +// .transition = .{ +// .ty = struct { SmallList(Transition, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// }, +// .shorthand = true, +// }, + +// .@"animation-name" = .{ +// .ty = struct { AnimationNameList, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .o = true, +// }, +// }, +// .@"animation-duration" = .{ +// .ty = struct { SmallList(Time, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .o = true, +// }, +// }, +// .@"animation-timing-function" = .{ +// .ty = struct { SmallList(EasingFunction, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .o = true, +// }, +// }, +// .@"animation-iteration-count" = .{ +// .ty = struct { SmallList(AnimationIterationCount, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .o = true, +// }, +// }, +// .@"animation-direction" = .{ +// .ty = struct { SmallList(AnimationDirection, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .o = true, +// }, +// }, +// .@"animation-play-state" = .{ +// .ty = struct { SmallList(AnimationPlayState, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .o = true, +// }, +// }, +// .@"animation-delay" = .{ +// .ty = struct { SmallList(Time, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .o = true, +// }, +// }, +// .@"animation-fill-mode" = .{ +// .ty = struct { SmallList(AnimationFillMode, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .o = true, +// }, +// }, +// .@"animation-composition" = .{ +// .ty = SmallList(AnimationComposition, 1), +// }, +// .@"animation-timeline" = .{ +// .ty = SmallList(AnimationTimeline, 1), +// }, +// .@"animation-range-start" = .{ +// .ty = SmallList(AnimationRangeStart, 1), +// }, +// .@"animation-range-end" = .{ +// .ty = SmallList(AnimationRangeEnd, 1), +// }, +// .@"animation-range" = .{ +// .ty = SmallList(AnimationRange, 1), +// }, +// .animation = .{ +// .ty = struct { AnimationList, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .o = true, +// }, +// .shorthand = true, +// }, + +// // https://drafts.csswg.org/css-transforms-2/ +// .transform = .{ +// .ty = struct { TransformList, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// .o = true, +// }, +// }, +// .@"transform-origin" = .{ +// .ty = struct { Position, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// .o = true, +// }, +// // TODO: handle z offset syntax +// }, +// .@"transform-style" = .{ +// .ty = struct { TransformStyle, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// }, +// .@"transform-box" = .{ +// .ty = TransformBox, +// }, +// .@"backface-visibility" = .{ +// .ty = struct { BackfaceVisibility, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// }, +// .perspective = .{ +// .ty = struct { Perspective, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// }, +// .@"perspective-origin" = .{ +// .ty = struct { Position, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// }, +// .translate = .{ +// .ty = Translate, +// }, +// .rotate = .{ +// .ty = Rotate, +// }, +// .scale = .{ +// .ty = Scale, +// }, + +// // https://www.w3.org/TR/2021/CRD-css-text-3-20210422 +// .@"text-transform" = .{ +// .ty = TextTransform, +// }, +// .@"white-space" = .{ +// .ty = WhiteSpace, +// }, +// .@"tab-size" = .{ +// .ty = struct { LengthOrNumber, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .moz = true, +// .o = true, +// }, +// }, +// .@"word-break" = .{ +// .ty = WordBreak, +// }, +// .@"line-break" = .{ +// .ty = LineBreak, +// }, +// .hyphens = .{ +// .ty = struct { Hyphens, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// }, +// }, +// .@"overflow-wrap" = .{ +// .ty = OverflowWrap, +// }, +// .@"word-wrap" = .{ +// .ty = OverflowWrap, +// }, +// .@"text-align" = .{ +// .ty = TextAlign, +// }, +// .@"text-align-last" = .{ +// .ty = struct { TextAlignLast, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .moz = true, +// }, +// }, +// .@"text-justify" = .{ +// .ty = TextJustify, +// }, +// .@"word-spacing" = .{ +// .ty = Spacing, +// }, +// .@"letter-spacing" = .{ +// .ty = Spacing, +// }, +// .@"text-indent" = .{ +// .ty = TextIndent, +// }, + +// // https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506 +// .@"text-decoration-line" = .{ +// .ty = struct { TextDecorationLine, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// }, +// .@"text-decoration-style" = .{ +// .ty = struct { TextDecorationStyle, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// }, +// .@"text-decoration-color" = .{ +// .ty = struct { CssColor, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// }, +// .@"text-decoration-thickness" = .{ +// .ty = TextDecorationThickness, +// }, +// .@"text-decoration" = .{ +// .ty = struct { TextDecoration, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// }, +// .shorthand = true, +// }, +// .@"text-decoration-skip-ink" = .{ +// .ty = struct { TextDecorationSkipInk, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"text-emphasis-style" = .{ +// .ty = struct { TextEmphasisStyle, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"text-emphasis-color" = .{ +// .ty = struct { CssColor, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"text-emphasis" = .{ +// .ty = struct { TextEmphasis, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// .shorthand = true, +// }, +// .@"text-emphasis-position" = .{ +// .ty = struct { TextEmphasisPosition, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"text-shadow" = .{ +// .ty = SmallList(TextShadow, 1), +// }, + +// // https://w3c.github.io/csswg-drafts/css-size-adjust/ +// .@"text-size-adjust" = .{ +// .ty = struct { TextSizeAdjust, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// }, +// }, + +// // https://drafts.csswg.org/css-writing-modes-3/ +// .direction = .{ +// .ty = Direction, +// }, +// .@"unicode-bidi" = .{ +// .ty = UnicodeBidi, +// }, + +// // https://www.w3.org/TR/css-break-3/ +// .@"box-decoration-break" = .{ +// .ty = struct { BoxDecorationBreak, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, + +// // https://www.w3.org/TR/2021/WD-css-ui-4-20210316 +// .resize = .{ +// .ty = Resize, +// }, +// .cursor = .{ +// .ty = Cursor, +// }, +// .@"caret-color" = .{ +// .ty = ColorOrAuto, +// }, +// .@"caret-shape" = .{ +// .ty = CaretShape, +// }, +// .caret = .{ +// .ty = Caret, +// .shorthand = true, +// }, +// .@"user-select" = .{ +// .ty = struct { UserSelect, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// }, +// }, +// .@"accent-color" = .{ +// .ty = ColorOrAuto, +// }, +// .appearance = .{ +// .ty = struct { Appearance, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// .moz = true, +// .ms = true, +// }, +// }, + +// // https://www.w3.org/TR/2020/WD-css-lists-3-20201117 +// .@"list-style-type" = .{ +// .ty = ListStyleType, +// }, +// .@"list-style-image" = .{ +// .ty = Image, +// }, +// .@"list-style-position" = .{ +// .ty = ListStylePosition, +// }, +// .@"list-style" = .{ +// .ty = ListStyle, +// .shorthand = true, +// }, +// .@"marker-side" = .{ +// .ty = MarkerSide, +// }, + +// // CSS modules +// .composes = .{ +// .ty = Composes, +// .conditional = .{ +// .css_modules = true, +// }, +// }, + +// // https://www.w3.org/TR/SVG2/painting.html +// .fill = .{ +// .ty = SVGPaint, +// }, +// .@"fill-rule" = .{ +// .ty = FillRule, +// }, +// .@"fill-opacity" = .{ +// .ty = AlphaValue, +// }, +// .stroke = .{ +// .ty = SVGPaint, +// }, +// .@"stroke-opacity" = .{ +// .ty = AlphaValue, +// }, +// .@"stroke-width" = .{ +// .ty = LengthPercentage, +// }, +// .@"stroke-linecap" = .{ +// .ty = StrokeLinecap, +// }, +// .@"stroke-linejoin" = .{ +// .ty = StrokeLinejoin, +// }, +// .@"stroke-miterlimit" = .{ +// .ty = CSSNumber, +// }, +// .@"stroke-dasharray" = .{ +// .ty = StrokeDasharray, +// }, +// .@"stroke-dashoffset" = .{ +// .ty = LengthPercentage, +// }, +// .@"marker-start" = .{ +// .ty = Marker, +// }, +// .@"marker-mid" = .{ +// .ty = Marker, +// }, +// .@"marker-end" = .{ +// .ty = Marker, +// }, +// .marker = .{ +// .ty = Marker, +// }, +// .@"color-interpolation" = .{ +// .ty = ColorInterpolation, +// }, +// .@"color-interpolation-filters" = .{ +// .ty = ColorInterpolation, +// }, +// .@"color-rendering" = .{ +// .ty = ColorRendering, +// }, +// .@"shape-rendering" = .{ +// .ty = ShapeRendering, +// }, +// .@"text-rendering" = .{ +// .ty = TextRendering, +// }, +// .@"image-rendering" = .{ +// .ty = ImageRendering, +// }, + +// // https://www.w3.org/TR/css-masking-1/ +// .@"clip-path" = .{ +// .ty = struct { ClipPath, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"clip-rule" = .{ +// .ty = FillRule, +// }, +// .@"mask-image" = .{ +// .ty = struct { SmallList(Image, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"mask-mode" = .{ +// .ty = SmallList(MaskMode, 1), +// }, +// .@"mask-repeat" = .{ +// .ty = struct { SmallList(BackgroundRepeat, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"mask-position-x" = .{ +// .ty = SmallList(HorizontalPosition, 1), +// }, +// .@"mask-position-y" = .{ +// .ty = SmallList(VerticalPosition, 1), +// }, +// .@"mask-position" = .{ +// .ty = struct { SmallList(Position, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"mask-clip" = .{ +// .ty = struct { SmallList(MaskClip, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"mask-origin" = .{ +// .ty = struct { SmallList(GeometryBox, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"mask-size" = .{ +// .ty = struct { SmallList(BackgroundSize, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"mask-composite" = .{ +// .ty = SmallList(MaskComposite, 1), +// }, +// .@"mask-type" = .{ +// .ty = MaskType, +// }, +// .mask = .{ +// .ty = struct { SmallList(Mask, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// .shorthand = true, +// }, +// .@"mask-border-source" = .{ +// .ty = Image, +// }, +// .@"mask-border-mode" = .{ +// .ty = MaskBorderMode, +// }, +// .@"mask-border-slice" = .{ +// .ty = BorderImageSlice, +// }, +// .@"mask-border-width" = .{ +// .ty = Rect(BorderImageSideWidth), +// }, +// .@"mask-border-outset" = .{ +// .ty = Rect(LengthOrNumber), +// }, +// .@"mask-border-repeat" = .{ +// .ty = BorderImageRepeat, +// }, +// .@"mask-border" = .{ +// .ty = MaskBorder, +// .shorthand = true, +// }, + +// // WebKit additions +// .@"-webkit-mask-composite" = .{ +// .ty = SmallList(WebKitMaskComposite, 1), +// }, +// .@"mask-source-type" = .{ +// .ty = struct { SmallList(WebKitMaskSourceType, 1), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// .unprefixed = false, +// }, +// .@"mask-box-image" = .{ +// .ty = struct { BorderImage, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// .unprefixed = false, +// }, +// .@"mask-box-image-source" = .{ +// .ty = struct { Image, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// .unprefixed = false, +// }, +// .@"mask-box-image-slice" = .{ +// .ty = struct { BorderImageSlice, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// .unprefixed = false, +// }, +// .@"mask-box-image-width" = .{ +// .ty = struct { Rect(BorderImageSideWidth), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// .unprefixed = false, +// }, +// .@"mask-box-image-outset" = .{ +// .ty = struct { Rect(LengthOrNumber), css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// .unprefixed = false, +// }, +// .@"mask-box-image-repeat" = .{ +// .ty = struct { BorderImageRepeat, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// .unprefixed = false, +// }, + +// // https://drafts.fxtf.org/filter-effects-1/ +// .filter = .{ +// .ty = struct { FilterList, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, +// .@"backdrop-filter" = .{ +// .ty = struct { FilterList, css.VendorPrefix }, +// .valid_prefixes = css.VendorPrefix{ +// .webkit = true, +// }, +// }, + +// // https://drafts.csswg.org/css2/ +// .@"z-index" = .{ +// .ty = position.ZIndex, +// }, + +// // https://drafts.csswg.org/css-contain-3/ +// .@"container-type" = .{ +// .ty = ContainerType, +// }, +// .@"container-name" = .{ +// .ty = ContainerNameList, +// }, +// .container = .{ +// .ty = Container, +// .shorthand = true, +// }, + +// // https://w3c.github.io/csswg-drafts/css-view-transitions-1/ +// .@"view-transition-name" = .{ +// .ty = CustomIdent, +// }, + +// // https://drafts.csswg.org/css-color-adjust/ +// .@"color-scheme" = .{ +// .ty = ColorScheme, +// }, +// }); diff --git a/src/css/properties/properties_generated.zig b/src/css/properties/properties_generated.zig new file mode 100644 index 0000000000000..3e3640fd414b2 --- /dev/null +++ b/src/css/properties/properties_generated.zig @@ -0,0 +1,679 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; + +pub const css = @import("../css_parser.zig"); + +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const VendorPrefix = css.VendorPrefix; + +const PropertyImpl = @import("./properties_impl.zig").PropertyImpl; +const PropertyIdImpl = @import("./properties_impl.zig").PropertyIdImpl; + +const CSSWideKeyword = css.css_properties.CSSWideKeyword; +const UnparsedProperty = css.css_properties.custom.UnparsedProperty; +const CustomProperty = css.css_properties.custom.CustomProperty; + +const css_values = css.css_values; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const Length = css.css_values.length.Length; +const LengthValue = css.css_values.length.LengthValue; +const LengthPercentage = css_values.length.LengthPercentage; +const LengthPercentageOrAuto = css_values.length.LengthPercentageOrAuto; +const PropertyCategory = css.PropertyCategory; +const LogicalGroup = css.LogicalGroup; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSInteger = css.css_values.number.CSSInteger; +const NumberOrPercentage = css.css_values.percentage.NumberOrPercentage; +const Percentage = css.css_values.percentage.Percentage; +const Angle = css.css_values.angle.Angle; +const DashedIdentReference = css.css_values.ident.DashedIdentReference; +const Time = css.css_values.time.Time; +const EasingFunction = css.css_values.easing.EasingFunction; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const DashedIdent = css.css_values.ident.DashedIdent; +const Url = css.css_values.url.Url; +const CustomIdentList = css.css_values.ident.CustomIdentList; +const Location = css.Location; +const HorizontalPosition = css.css_values.position.HorizontalPosition; +const VerticalPosition = css.css_values.position.VerticalPosition; +const ContainerName = css.css_rules.container.ContainerName; + +pub const font = css.css_properties.font; +const border = css.css_properties.border; +const border_radius = css.css_properties.border_radius; +const border_image = css.css_properties.border_image; +const outline = css.css_properties.outline; +const flex = css.css_properties.flex; +const @"align" = css.css_properties.@"align"; +const margin_padding = css.css_properties.margin_padding; +const transition = css.css_properties.transition; +const animation = css.css_properties.animation; +const transform = css.css_properties.transform; +const text = css.css_properties.text; +const ui = css.css_properties.ui; +const list = css.css_properties.list; +const css_modules = css.css_properties.css_modules; +const svg = css.css_properties.svg; +const shape = css.css_properties.shape; +const masking = css.css_properties.masking; +const background = css.css_properties.background; +const effects = css.css_properties.effects; +const contain = css.css_properties.contain; +const custom = css.css_properties.custom; +const position = css.css_properties.position; +const box_shadow = css.css_properties.box_shadow; +const size = css.css_properties.size; +const overflow = css.css_properties.overflow; + +// const BorderSideWidth = border.BorderSideWith; +// const Size2D = css_values.size.Size2D; +// const BorderRadius = border_radius.BorderRadius; +// const Rect = css_values.rect.Rect; +// const LengthOrNumber = css_values.length.LengthOrNumber; +// const BorderImageRepeat = border_image.BorderImageRepeat; +// const BorderImageSideWidth = border_image.BorderImageSideWidth; +// const BorderImageSlice = border_image.BorderImageSlice; +// const BorderImage = border_image.BorderImage; +// const BorderColor = border.BorderColor; +// const BorderStyle = border.BorderStyle; +// const BorderWidth = border.BorderWidth; +// const BorderBlockColor = border.BorderBlockColor; +// const BorderBlockStyle = border.BorderBlockStyle; +// const BorderBlockWidth = border.BorderBlockWidth; +// const BorderInlineColor = border.BorderInlineColor; +// const BorderInlineStyle = border.BorderInlineStyle; +// const BorderInlineWidth = border.BorderInlineWidth; +// const Border = border.Border; +// const BorderTop = border.BorderTop; +// const BorderRight = border.BorderRight; +// const BorderLeft = border.BorderLeft; +// const BorderBottom = border.BorderBottom; +// const BorderBlockStart = border.BorderBlockStart; +// const BorderBlockEnd = border.BorderBlockEnd; +// const BorderInlineStart = border.BorderInlineStart; +// const BorderInlineEnd = border.BorderInlineEnd; +// const BorderBlock = border.BorderBlock; +// const BorderInline = border.BorderInline; +// const Outline = outline.Outline; +// const OutlineStyle = outline.OutlineStyle; +// const FlexDirection = flex.FlexDirection; +// const FlexWrap = flex.FlexWrap; +// const FlexFlow = flex.FlexFlow; +// const Flex = flex.Flex; +// const BoxOrient = flex.BoxOrient; +// const BoxDirection = flex.BoxDirection; +// const BoxAlign = flex.BoxAlign; +// const BoxPack = flex.BoxPack; +// const BoxLines = flex.BoxLines; +// const FlexPack = flex.FlexPack; +// const FlexItemAlign = flex.FlexItemAlign; +// const FlexLinePack = flex.FlexLinePack; +// const AlignContent = @"align".AlignContent; +// const JustifyContent = @"align".JustifyContent; +// const PlaceContent = @"align".PlaceContent; +// const AlignSelf = @"align".AlignSelf; +// const JustifySelf = @"align".JustifySelf; +// const PlaceSelf = @"align".PlaceSelf; +// const AlignItems = @"align".AlignItems; +// const JustifyItems = @"align".JustifyItems; +// const PlaceItems = @"align".PlaceItems; +// const GapValue = @"align".GapValue; +// const Gap = @"align".Gap; +// const MarginBlock = margin_padding.MarginBlock; +// const Margin = margin_padding.Margin; +// const MarginInline = margin_padding.MarginInline; +// const PaddingBlock = margin_padding.PaddingBlock; +// const PaddingInline = margin_padding.PaddingInline; +// const Padding = margin_padding.Padding; +// const ScrollMarginBlock = margin_padding.ScrollMarginBlock; +// const ScrollMarginInline = margin_padding.ScrollMarginInline; +// const ScrollMargin = margin_padding.ScrollMargin; +// const ScrollPaddingBlock = margin_padding.ScrollPaddingBlock; +// const ScrollPaddingInline = margin_padding.ScrollPaddingInline; +// const ScrollPadding = margin_padding.ScrollPadding; +// const FontWeight = font.FontWeight; +// const FontSize = font.FontSize; +// const FontStretch = font.FontStretch; +// const FontFamily = font.FontFamily; +// const FontStyle = font.FontStyle; +// const FontVariantCaps = font.FontVariantCaps; +// const LineHeight = font.LineHeight; +// const Font = font.Font; +// const VerticalAlign = font.VerticalAlign; +// const Transition = transition.Transition; +// const AnimationNameList = animation.AnimationNameList; +// const AnimationList = animation.AnimationList; +// const AnimationIterationCount = animation.AnimationIterationCount; +// const AnimationDirection = animation.AnimationDirection; +// const AnimationPlayState = animation.AnimationPlayState; +// const AnimationFillMode = animation.AnimationFillMode; +// const AnimationComposition = animation.AnimationComposition; +// const AnimationTimeline = animation.AnimationTimeline; +// const AnimationRangeStart = animation.AnimationRangeStart; +// const AnimationRangeEnd = animation.AnimationRangeEnd; +// const AnimationRange = animation.AnimationRange; +// const TransformList = transform.TransformList; +// const TransformStyle = transform.TransformStyle; +// const TransformBox = transform.TransformBox; +// const BackfaceVisibility = transform.BackfaceVisibility; +// const Perspective = transform.Perspective; +// const Translate = transform.Translate; +// const Rotate = transform.Rotate; +// const Scale = transform.Scale; +// const TextTransform = text.TextTransform; +// const WhiteSpace = text.WhiteSpace; +// const WordBreak = text.WordBreak; +// const LineBreak = text.LineBreak; +// const Hyphens = text.Hyphens; +// const OverflowWrap = text.OverflowWrap; +// const TextAlign = text.TextAlign; +// const TextIndent = text.TextIndent; +// const Spacing = text.Spacing; +// const TextJustify = text.TextJustify; +// const TextAlignLast = text.TextAlignLast; +// const TextDecorationLine = text.TextDecorationLine; +// const TextDecorationStyle = text.TextDecorationStyle; +// const TextDecorationThickness = text.TextDecorationThickness; +// const TextDecoration = text.TextDecoration; +// const TextDecorationSkipInk = text.TextDecorationSkipInk; +// const TextEmphasisStyle = text.TextEmphasisStyle; +// const TextEmphasis = text.TextEmphasis; +// const TextEmphasisPositionVertical = text.TextEmphasisPositionVertical; +// const TextEmphasisPositionHorizontal = text.TextEmphasisPositionHorizontal; +// const TextEmphasisPosition = text.TextEmphasisPosition; +// const TextShadow = text.TextShadow; +// const TextSizeAdjust = text.TextSizeAdjust; +// const Direction = text.Direction; +// const UnicodeBidi = text.UnicodeBidi; +// const BoxDecorationBreak = text.BoxDecorationBreak; +// const Resize = ui.Resize; +// const Cursor = ui.Cursor; +// const ColorOrAuto = ui.ColorOrAuto; +// const CaretShape = ui.CaretShape; +// const Caret = ui.Caret; +// const UserSelect = ui.UserSelect; +// const Appearance = ui.Appearance; +// const ColorScheme = ui.ColorScheme; +// const ListStyleType = list.ListStyleType; +// const ListStylePosition = list.ListStylePosition; +// const ListStyle = list.ListStyle; +// const MarkerSide = list.MarkerSide; +const Composes = css_modules.Composes; +// const SVGPaint = svg.SVGPaint; +// const FillRule = shape.FillRule; +// const AlphaValue = shape.AlphaValue; +// const StrokeLinecap = svg.StrokeLinecap; +// const StrokeLinejoin = svg.StrokeLinejoin; +// const StrokeDasharray = svg.StrokeDasharray; +// const Marker = svg.Marker; +// const ColorInterpolation = svg.ColorInterpolation; +// const ColorRendering = svg.ColorRendering; +// const ShapeRendering = svg.ShapeRendering; +// const TextRendering = svg.TextRendering; +// const ImageRendering = svg.ImageRendering; +// const ClipPath = masking.ClipPath; +// const MaskMode = masking.MaskMode; +// const MaskClip = masking.MaskClip; +// const GeometryBox = masking.GeometryBox; +// const MaskComposite = masking.MaskComposite; +// const MaskType = masking.MaskType; +// const Mask = masking.Mask; +// const MaskBorderMode = masking.MaskBorderMode; +// const MaskBorder = masking.MaskBorder; +// const WebKitMaskComposite = masking.WebKitMaskComposite; +// const WebKitMaskSourceType = masking.WebKitMaskSourceType; +// const BackgroundRepeat = background.BackgroundRepeat; +// const BackgroundSize = background.BackgroundSize; +// const FilterList = effects.FilterList; +// const ContainerType = contain.ContainerType; +// const Container = contain.Container; +// const ContainerNameList = contain.ContainerNameList; +const CustomPropertyName = custom.CustomPropertyName; +// const display = css.css_properties.display; + +const Position = position.Position; + +const Result = css.Result; + +const ArrayList = std.ArrayListUnmanaged; +const SmallList = css.SmallList; +pub const Property = union(PropertyIdTag) { + @"background-color": CssColor, + color: CssColor, + @"border-spacing": css.css_values.size.Size2D(Length), + @"border-top-color": CssColor, + @"border-bottom-color": CssColor, + @"border-left-color": CssColor, + @"border-right-color": CssColor, + @"border-block-start-color": CssColor, + @"border-block-end-color": CssColor, + @"border-inline-start-color": CssColor, + @"border-inline-end-color": CssColor, + @"border-top-style": border.LineStyle, + @"border-bottom-style": border.LineStyle, + @"border-left-style": border.LineStyle, + @"border-right-style": border.LineStyle, + @"border-block-start-style": border.LineStyle, + @"border-block-end-style": border.LineStyle, + composes: Composes, + all: CSSWideKeyword, + unparsed: UnparsedProperty, + custom: CustomProperty, + + pub usingnamespace PropertyImpl(); + /// Parses a CSS property by name. + pub fn parse(property_id: PropertyId, input: *css.Parser, options: *const css.ParserOptions) Result(Property) { + const state = input.state(); + + switch (property_id) { + .@"background-color" => { + if (css.generic.parseWithOptions(CssColor, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"background-color" = c } }; + } + } + }, + .color => { + if (css.generic.parseWithOptions(CssColor, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .color = c } }; + } + } + }, + .@"border-spacing" => { + if (css.generic.parseWithOptions(css.css_values.size.Size2D(Length), input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-spacing" = c } }; + } + } + }, + .@"border-top-color" => { + if (css.generic.parseWithOptions(CssColor, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-top-color" = c } }; + } + } + }, + .@"border-bottom-color" => { + if (css.generic.parseWithOptions(CssColor, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-bottom-color" = c } }; + } + } + }, + .@"border-left-color" => { + if (css.generic.parseWithOptions(CssColor, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-left-color" = c } }; + } + } + }, + .@"border-right-color" => { + if (css.generic.parseWithOptions(CssColor, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-right-color" = c } }; + } + } + }, + .@"border-block-start-color" => { + if (css.generic.parseWithOptions(CssColor, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-block-start-color" = c } }; + } + } + }, + .@"border-block-end-color" => { + if (css.generic.parseWithOptions(CssColor, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-block-end-color" = c } }; + } + } + }, + .@"border-inline-start-color" => { + if (css.generic.parseWithOptions(CssColor, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-inline-start-color" = c } }; + } + } + }, + .@"border-inline-end-color" => { + if (css.generic.parseWithOptions(CssColor, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-inline-end-color" = c } }; + } + } + }, + .@"border-top-style" => { + if (css.generic.parseWithOptions(border.LineStyle, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-top-style" = c } }; + } + } + }, + .@"border-bottom-style" => { + if (css.generic.parseWithOptions(border.LineStyle, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-bottom-style" = c } }; + } + } + }, + .@"border-left-style" => { + if (css.generic.parseWithOptions(border.LineStyle, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-left-style" = c } }; + } + } + }, + .@"border-right-style" => { + if (css.generic.parseWithOptions(border.LineStyle, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-right-style" = c } }; + } + } + }, + .@"border-block-start-style" => { + if (css.generic.parseWithOptions(border.LineStyle, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-block-start-style" = c } }; + } + } + }, + .@"border-block-end-style" => { + if (css.generic.parseWithOptions(border.LineStyle, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .@"border-block-end-style" = c } }; + } + } + }, + .composes => { + if (css.generic.parseWithOptions(Composes, input, options).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .composes = c } }; + } + } + }, + .all => return .{ .result = .{ .all = switch (CSSWideKeyword.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } } }, + .custom => |name| return .{ .result = .{ .custom = switch (CustomProperty.parse(name, input, options)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } } }, + else => {}, + } + + // If a value was unable to be parsed, treat as an unparsed property. + // This is different from a custom property, handled below, in that the property name is known + // and stored as an enum rather than a string. This lets property handlers more easily deal with it. + // Ideally we'd only do this if var() or env() references were seen, but err on the safe side for now. + input.reset(&state); + return .{ .result = .{ .unparsed = switch (UnparsedProperty.parse(property_id, input, options)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } } }; + } + + pub inline fn __toCssHelper(this: *const Property) struct { []const u8, VendorPrefix } { + return switch (this.*) { + .@"background-color" => .{ "background-color", VendorPrefix{ .none = true } }, + .color => .{ "color", VendorPrefix{ .none = true } }, + .@"border-spacing" => .{ "border-spacing", VendorPrefix{ .none = true } }, + .@"border-top-color" => .{ "border-top-color", VendorPrefix{ .none = true } }, + .@"border-bottom-color" => .{ "border-bottom-color", VendorPrefix{ .none = true } }, + .@"border-left-color" => .{ "border-left-color", VendorPrefix{ .none = true } }, + .@"border-right-color" => .{ "border-right-color", VendorPrefix{ .none = true } }, + .@"border-block-start-color" => .{ "border-block-start-color", VendorPrefix{ .none = true } }, + .@"border-block-end-color" => .{ "border-block-end-color", VendorPrefix{ .none = true } }, + .@"border-inline-start-color" => .{ "border-inline-start-color", VendorPrefix{ .none = true } }, + .@"border-inline-end-color" => .{ "border-inline-end-color", VendorPrefix{ .none = true } }, + .@"border-top-style" => .{ "border-top-style", VendorPrefix{ .none = true } }, + .@"border-bottom-style" => .{ "border-bottom-style", VendorPrefix{ .none = true } }, + .@"border-left-style" => .{ "border-left-style", VendorPrefix{ .none = true } }, + .@"border-right-style" => .{ "border-right-style", VendorPrefix{ .none = true } }, + .@"border-block-start-style" => .{ "border-block-start-style", VendorPrefix{ .none = true } }, + .@"border-block-end-style" => .{ "border-block-end-style", VendorPrefix{ .none = true } }, + .composes => .{ "composes", VendorPrefix{ .none = true } }, + .all => .{ "all", VendorPrefix{ .none = true } }, + .unparsed => |*unparsed| brk: { + var prefix = unparsed.property_id.prefix(); + if (prefix.isEmpty()) { + prefix = VendorPrefix{ .none = true }; + } + break :brk .{ unparsed.property_id.name(), prefix }; + }, + .custom => unreachable, + }; + } + + /// Serializes the value of a CSS property without its name or `!important` flag. + pub fn valueToCss(this: *const Property, comptime W: type, dest: *css.Printer(W)) PrintErr!void { + return switch (this.*) { + .@"background-color" => |*value| value.toCss(W, dest), + .color => |*value| value.toCss(W, dest), + .@"border-spacing" => |*value| value.toCss(W, dest), + .@"border-top-color" => |*value| value.toCss(W, dest), + .@"border-bottom-color" => |*value| value.toCss(W, dest), + .@"border-left-color" => |*value| value.toCss(W, dest), + .@"border-right-color" => |*value| value.toCss(W, dest), + .@"border-block-start-color" => |*value| value.toCss(W, dest), + .@"border-block-end-color" => |*value| value.toCss(W, dest), + .@"border-inline-start-color" => |*value| value.toCss(W, dest), + .@"border-inline-end-color" => |*value| value.toCss(W, dest), + .@"border-top-style" => |*value| value.toCss(W, dest), + .@"border-bottom-style" => |*value| value.toCss(W, dest), + .@"border-left-style" => |*value| value.toCss(W, dest), + .@"border-right-style" => |*value| value.toCss(W, dest), + .@"border-block-start-style" => |*value| value.toCss(W, dest), + .@"border-block-end-style" => |*value| value.toCss(W, dest), + .composes => |*value| value.toCss(W, dest), + .all => |*keyword| keyword.toCss(W, dest), + .unparsed => |*unparsed| unparsed.value.toCss(W, dest, false), + .custom => |*c| c.value.toCss(W, dest, c.name == .custom), + }; + } + + /// Returns the given longhand property for a shorthand. + pub fn longhand(this: *const Property, property_id: *const PropertyId) ?Property { + _ = property_id; // autofix + switch (this.*) { + else => {}, + } + return null; + } +}; +pub const PropertyId = union(PropertyIdTag) { + @"background-color", + color, + @"border-spacing", + @"border-top-color", + @"border-bottom-color", + @"border-left-color", + @"border-right-color", + @"border-block-start-color", + @"border-block-end-color", + @"border-inline-start-color", + @"border-inline-end-color", + @"border-top-style", + @"border-bottom-style", + @"border-left-style", + @"border-right-style", + @"border-block-start-style", + @"border-block-end-style", + composes, + all, + unparsed, + custom: CustomPropertyName, + + pub usingnamespace PropertyIdImpl(); + + /// Returns the property name, without any vendor prefixes. + pub inline fn name(this: *const PropertyId) []const u8 { + return @tagName(this.*); + } + + /// Returns the vendor prefix for this property id. + pub fn prefix(this: *const PropertyId) VendorPrefix { + return switch (this.*) { + .@"background-color" => VendorPrefix.empty(), + .color => VendorPrefix.empty(), + .@"border-spacing" => VendorPrefix.empty(), + .@"border-top-color" => VendorPrefix.empty(), + .@"border-bottom-color" => VendorPrefix.empty(), + .@"border-left-color" => VendorPrefix.empty(), + .@"border-right-color" => VendorPrefix.empty(), + .@"border-block-start-color" => VendorPrefix.empty(), + .@"border-block-end-color" => VendorPrefix.empty(), + .@"border-inline-start-color" => VendorPrefix.empty(), + .@"border-inline-end-color" => VendorPrefix.empty(), + .@"border-top-style" => VendorPrefix.empty(), + .@"border-bottom-style" => VendorPrefix.empty(), + .@"border-left-style" => VendorPrefix.empty(), + .@"border-right-style" => VendorPrefix.empty(), + .@"border-block-start-style" => VendorPrefix.empty(), + .@"border-block-end-style" => VendorPrefix.empty(), + .composes => VendorPrefix.empty(), + .all, .custom, .unparsed => VendorPrefix.empty(), + }; + } + + pub fn fromNameAndPrefix(name1: []const u8, pre: VendorPrefix) ?PropertyId { + // TODO: todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "background-color")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"background-color"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "color")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .color; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-spacing")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-spacing"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-top-color")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-top-color"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-bottom-color")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-bottom-color"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-left-color")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-left-color"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-right-color")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-right-color"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-block-start-color")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-block-start-color"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-block-end-color")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-block-end-color"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-inline-start-color")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-inline-start-color"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-inline-end-color")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-inline-end-color"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-top-style")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-top-style"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-bottom-style")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-bottom-style"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-left-style")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-left-style"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-right-style")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-right-style"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-block-start-style")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-block-start-style"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "border-block-end-style")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .@"border-block-end-style"; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "composes")) { + const allowed_prefixes = VendorPrefix{ .none = true }; + if (allowed_prefixes.contains(pre)) return .composes; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name1, "all")) {} else { + return null; + } + + return null; + } + + pub fn withPrefix(this: *const PropertyId, pre: VendorPrefix) PropertyId { + _ = pre; // autofix + return switch (this.*) { + .@"background-color" => .@"background-color", + .color => .color, + .@"border-spacing" => .@"border-spacing", + .@"border-top-color" => .@"border-top-color", + .@"border-bottom-color" => .@"border-bottom-color", + .@"border-left-color" => .@"border-left-color", + .@"border-right-color" => .@"border-right-color", + .@"border-block-start-color" => .@"border-block-start-color", + .@"border-block-end-color" => .@"border-block-end-color", + .@"border-inline-start-color" => .@"border-inline-start-color", + .@"border-inline-end-color" => .@"border-inline-end-color", + .@"border-top-style" => .@"border-top-style", + .@"border-bottom-style" => .@"border-bottom-style", + .@"border-left-style" => .@"border-left-style", + .@"border-right-style" => .@"border-right-style", + .@"border-block-start-style" => .@"border-block-start-style", + .@"border-block-end-style" => .@"border-block-end-style", + .composes => .composes, + else => this.*, + }; + } + + pub fn addPrefix(this: *const PropertyId, pre: VendorPrefix) void { + _ = pre; // autofix + return switch (this.*) { + .@"background-color" => {}, + .color => {}, + .@"border-spacing" => {}, + .@"border-top-color" => {}, + .@"border-bottom-color" => {}, + .@"border-left-color" => {}, + .@"border-right-color" => {}, + .@"border-block-start-color" => {}, + .@"border-block-end-color" => {}, + .@"border-inline-start-color" => {}, + .@"border-inline-end-color" => {}, + .@"border-top-style" => {}, + .@"border-bottom-style" => {}, + .@"border-left-style" => {}, + .@"border-right-style" => {}, + .@"border-block-start-style" => {}, + .@"border-block-end-style" => {}, + .composes => {}, + else => {}, + }; + } +}; +pub const PropertyIdTag = enum(u16) { + @"background-color", + color, + @"border-spacing", + @"border-top-color", + @"border-bottom-color", + @"border-left-color", + @"border-right-color", + @"border-block-start-color", + @"border-block-end-color", + @"border-inline-start-color", + @"border-inline-end-color", + @"border-top-style", + @"border-bottom-style", + @"border-left-style", + @"border-right-style", + @"border-block-start-style", + @"border-block-end-style", + composes, + all, + unparsed, + custom, +}; diff --git a/src/css/properties/properties_impl.zig b/src/css/properties/properties_impl.zig new file mode 100644 index 0000000000000..12bff6885855c --- /dev/null +++ b/src/css/properties/properties_impl.zig @@ -0,0 +1,122 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; + +pub const css = @import("../css_parser.zig"); + +const CustomPropertyName = css.css_properties.CustomPropertyName; + +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const VendorPrefix = css.VendorPrefix; +const Error = css.Error; + +const PropertyId = css.PropertyId; +const Property = css.Property; + +pub fn PropertyIdImpl() type { + return struct { + pub fn toCss(this: *const PropertyId, comptime W: type, dest: *Printer(W)) PrintErr!void { + var first = true; + const name = this.name(this); + const prefix_value = this.prefix().orNone(); + inline for (std.meta.fields(VendorPrefix)) |field| { + if (@field(prefix_value, field.name)) { + var prefix: VendorPrefix = .{}; + @field(prefix, field.name) = true; + + if (first) { + first = false; + } else { + try dest.delim(',', false); + } + try prefix.toCss(W, dest); + try dest.writeStr(name); + } + } + } + + pub fn parse(input: *css.Parser) css.Result(PropertyId) { + const name = switch (input.expectIdent()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = fromString(name) }; + } + + pub fn fromStr(name: []const u8) PropertyId { + return fromString(name); + } + + pub fn fromString(name_: []const u8) PropertyId { + const name_ref = name_; + var prefix: VendorPrefix = undefined; + var trimmed_name: []const u8 = undefined; + + // TODO: todo_stuff.match_ignore_ascii_case + if (bun.strings.startsWithCaseInsensitiveAscii(name_ref, "-webkit-")) { + prefix = VendorPrefix{ .webkit = true }; + trimmed_name = name_ref[8..]; + } else if (bun.strings.startsWithCaseInsensitiveAscii(name_ref, "-moz-")) { + prefix = VendorPrefix{ .moz = true }; + trimmed_name = name_ref[5..]; + } else if (bun.strings.startsWithCaseInsensitiveAscii(name_ref, "-o-")) { + prefix = VendorPrefix{ .o = true }; + trimmed_name = name_ref[3..]; + } else if (bun.strings.startsWithCaseInsensitiveAscii(name_ref, "-ms-")) { + prefix = VendorPrefix{ .ms = true }; + trimmed_name = name_ref[4..]; + } else { + prefix = VendorPrefix{ .none = true }; + trimmed_name = name_ref; + } + + return PropertyId.fromNameAndPrefix(trimmed_name, prefix) orelse .{ .custom = CustomPropertyName.fromStr(name_) }; + } + }; +} + +pub fn PropertyImpl() type { + return struct { + /// Serializes the CSS property, with an optional `!important` flag. + pub fn toCss(this: *const Property, comptime W: type, dest: *Printer(W), important: bool) PrintErr!void { + if (this.* == .custom) { + try this.custom.name.toCss(W, dest); + try dest.delim(':', false); + try this.valueToCss(W, dest); + if (important) { + try dest.whitespace(); + try dest.writeStr("!important"); + } + return; + } + const name, const prefix = this.__toCssHelper(); + var first = true; + + inline for (std.meta.fields(VendorPrefix)) |field| { + if (comptime !std.mem.eql(u8, field.name, "__unused")) { + if (@field(prefix, field.name)) { + var p: VendorPrefix = .{}; + @field(p, field.name) = true; + + if (first) { + first = false; + } else { + try dest.writeChar(';'); + try dest.newline(); + } + try p.toCss(W, dest); + try dest.writeStr(name); + try dest.delim(':', false); + try this.valueToCss(W, dest); + if (important) { + try dest.whitespace(); + try dest.writeStr("!important"); + } + return; + } + } + } + } + }; +} diff --git a/src/css/properties/shape.zig b/src/css/properties/shape.zig new file mode 100644 index 0000000000000..5d815259dc56f --- /dev/null +++ b/src/css/properties/shape.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +const GenericBorder = css.css_properties.border.GenericBorder; +const LineStyle = css.css_properties.border.LineStyle; + +/// A [``](https://www.w3.org/TR/css-shapes-1/#typedef-fill-rule) used to +/// determine the interior of a `polygon()` shape. +/// +/// See [Polygon](Polygon). +pub const FillRule = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A CSS [``](https://www.w3.org/TR/css-color-4/#typedef-alpha-value), +/// used to represent opacity. +/// +/// Parses either a `` or ``, but is always stored and serialized as a number. +pub const AlphaValue = struct { + v: f32, +}; diff --git a/src/css/properties/size.zig b/src/css/properties/size.zig new file mode 100644 index 0000000000000..9c3e535412d66 --- /dev/null +++ b/src/css/properties/size.zig @@ -0,0 +1,121 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +const GenericBorder = css.css_properties.border.GenericBorder; +const LineStyle = css.css_properties.border.LineStyle; + +pub const BoxSizing = enum { + /// Exclude the margin/border/padding from the width and height. + @"content-box", + /// Include the padding and border (but not the margin) in the width and height. + @"border-box", + pub usingnamespace css.DefineEnumProperty(@This()); +}; + +pub const Size = union(enum) { + /// The `auto` keyworda + auto, + /// An explicit length or percentage. + length_percentage: LengthPercentage, + /// The `min-content` keyword. + min_content: css.VendorPrefix, + /// The `max-content` keyword. + max_content: css.VendorPrefix, + /// The `fit-content` keyword. + fit_content: css.VendorPrefix, + /// The `fit-content()` function. + fit_content_function: LengthPercentage, + /// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords. + stretch: css.VendorPrefix, + /// The `contain` keyword. + contain, +}; + +/// A value for the [minimum](https://drafts.csswg.org/css-sizing-3/#min-size-properties) +/// and [maximum](https://drafts.csswg.org/css-sizing-3/#max-size-properties) size properties, +/// e.g. `min-width` and `max-height`. +pub const MaxSize = union(enum) { + /// The `none` keyword. + none, + /// An explicit length or percentage. + length_percentage: LengthPercentage, + /// The `min-content` keyword. + min_content: css.VendorPrefix, + /// The `max-content` keyword. + max_content: css.VendorPrefix, + /// The `fit-content` keyword. + fit_content: css.VendorPrefix, + /// The `fit-content()` function. + fit_content_function: LengthPercentage, + /// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords. + stretch: css.VendorPrefix, + /// The `contain` keyword. + contain, +}; + +/// A value for the [aspect-ratio](https://drafts.csswg.org/css-sizing-4/#aspect-ratio) property. +pub const AspectRatio = struct { + /// The `auto` keyword. + auto: bool, + /// A preferred aspect ratio for the box, specified as width / height. + ratio: ?Ratio, + + pub fn parse(input: *css.Parser) css.Result(AspectRatio) { + const location = input.currentSourceLocation(); + var auto = input.tryParse(css.Parser.expectIdentMatching, .{"auto"}); + + const ratio = input.tryParse(Ratio.parse, .{}); + if (auto.isErr()) { + auto = input.tryParse(css.Parser.expectIdentMatching, .{"auto"}); + } + if (auto.isErr() and ratio.isErr()) { + return .{ .err = location.newCustomError(css.ParserError.invalid_value) }; + } + + return .{ + .result = AspectRatio{ + .auto = auto.isOk(), + .ratio = ratio.asValue(), + }, + }; + } + + pub fn toCss(this: *const AspectRatio, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + if (this.auto) { + try dest.writeStr("auto"); + } + + if (this.ratio) |*ratio| { + if (this.auto) try dest.writeChar(' '); + try ratio.toCss(W, dest); + } + } +}; diff --git a/src/css/properties/svg.zig b/src/css/properties/svg.zig new file mode 100644 index 0000000000000..b46734d7cb137 --- /dev/null +++ b/src/css/properties/svg.zig @@ -0,0 +1,100 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; + +const GenericBorder = css.css_properties.border.GenericBorder; +const LineStyle = css.css_properties.border.LineStyle; + +/// An SVG [``](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) value +/// used in the `fill` and `stroke` properties. +const SVGPaint = union(enum) { + /// A URL reference to a paint server element, e.g. `linearGradient`, `radialGradient`, and `pattern`. + Url: struct { + /// The url of the paint server. + url: Url, + /// A fallback to be used in case the paint server cannot be resolved. + fallback: ?SVGPaintFallback, + }, + /// A solid color paint. + Color: CssColor, + /// Use the paint value of fill from a context element. + ContextFill, + /// Use the paint value of stroke from a context element. + ContextStroke, + /// No paint. + None, +}; + +/// A fallback for an SVG paint in case a paint server `url()` cannot be resolved. +/// +/// See [SVGPaint](SVGPaint). +const SVGPaintFallback = union(enum) { + /// No fallback. + None, + /// A solid color. + Color: CssColor, +}; + +/// A value for the [stroke-linecap](https://www.w3.org/TR/SVG2/painting.html#LineCaps) property. +pub const StrokeLinecap = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [stroke-linejoin](https://www.w3.org/TR/SVG2/painting.html#LineJoin) property. +pub const StrokeLinejoin = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [stroke-dasharray](https://www.w3.org/TR/SVG2/painting.html#StrokeDashing) property. +const StrokeDasharray = union(enum) { + /// No dashing is used. + None, + /// Specifies a dashing pattern to use. + Values: ArrayList(LengthPercentage), +}; + +/// A value for the [marker](https://www.w3.org/TR/SVG2/painting.html#VertexMarkerProperties) properties. +const Marker = union(enum) { + /// No marker. + None, + /// A url reference to a `` element. + Url: Url, +}; + +/// A value for the [color-interpolation](https://www.w3.org/TR/SVG2/painting.html#ColorInterpolation) property. +pub const ColorInterpolation = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [color-rendering](https://www.w3.org/TR/SVG2/painting.html#ColorRendering) property. +pub const ColorRendering = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [shape-rendering](https://www.w3.org/TR/SVG2/painting.html#ShapeRendering) property. +pub const ShapeRendering = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [text-rendering](https://www.w3.org/TR/SVG2/painting.html#TextRendering) property. +pub const TextRendering = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [image-rendering](https://www.w3.org/TR/SVG2/painting.html#ImageRendering) property. +pub const ImageRendering = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); diff --git a/src/css/properties/text.zig b/src/css/properties/text.zig new file mode 100644 index 0000000000000..03bdc3d3aa032 --- /dev/null +++ b/src/css/properties/text.zig @@ -0,0 +1,192 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; +const Percentage = css.css_values.percentage.Percentage; + +const GenericBorder = css.css_properties.border.GenericBorder; +const LineStyle = css.css_properties.border.LineStyle; + +/// A value for the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. +pub const TextTransform = struct { + /// How case should be transformed. + case: TextTransformCase, + /// How ideographic characters should be transformed. + other: TextTransformOther, +}; + +pub const TextTransformOther = packed struct(u8) { + /// Puts all typographic character units in full-width form. + full_width: bool = false, + /// Converts all small Kana characters to the equivalent full-size Kana. + full_size_kana: bool = false, +}; + +/// Defines how text case should be transformed in the +/// [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. +const TextTransformCase = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [white-space](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#white-space-property) property. +pub const WhiteSpace = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [word-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-break-property) property. +pub const WordBreak = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [line-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#line-break-property) property. +pub const LineBreak = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [hyphens](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#hyphenation) property. +pub const Hyphens = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [overflow-wrap](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#overflow-wrap-property) property. +pub const OverflowWrap = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [text-align](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-property) property. +pub const TextAlign = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [text-align-last](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-last-property) property. +pub const TextAlignLast = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [text-justify](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-justify-property) property. +pub const TextJustify = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [word-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-spacing-property) +/// and [letter-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#letter-spacing-property) properties. +pub const Spacing = union(enum) { + /// No additional spacing is applied. + normal, + /// Additional spacing between each word or letter. + length: Length, +}; + +/// A value for the [text-indent](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-indent-property) property. +pub const TextIndent = struct { + /// The amount to indent. + value: LengthPercentage, + /// Inverts which lines are affected. + hanging: bool, + /// Affects the first line after each hard break. + each_line: bool, +}; + +/// A value for the [text-decoration-line](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-line-property) property. +/// +/// Multiple lines may be specified by combining the flags. +pub const TextDecorationLine = packed struct(u8) { + /// Each line of text is underlined. + underline: bool = false, + /// Each line of text has a line over it. + overline: bool = false, + /// Each line of text has a line through the middle. + line_through: bool = false, + /// The text blinks. + blink: bool = false, + /// The text is decorated as a spelling error. + spelling_error: bool = false, + /// The text is decorated as a grammar error. + grammar_error: bool = false, +}; + +/// A value for the [text-decoration-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-style-property) property. +pub const TextDecorationStyle = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [text-decoration-thickness](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-width-property) property. +pub const TextDecorationThickness = union(enum) { + /// The UA chooses an appropriate thickness for text decoration lines. + auto, + /// Use the thickness defined in the current font. + from_font, + /// An explicit length. + length_percentage: LengthPercentage, +}; + +/// A value for the [text-decoration](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-property) shorthand property. +pub const TextDecoration = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [text-decoration-skip-ink](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-skip-ink-property) property. +pub const TextDecorationSkipInk = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A text emphasis shape for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property. +/// +/// See [TextEmphasisStyle](TextEmphasisStyle). +pub const TextEmphasisStyle = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [text-emphasis](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-property) shorthand property. +pub const TextEmphasis = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property. +pub const TextEmphasisPosition = struct { + /// The vertical position. + vertical: TextEmphasisPositionVertical, + /// The horizontal position. + horizontal: TextEmphasisPositionHorizontal, +}; + +/// A vertical position keyword for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property. +/// +/// See [TextEmphasisPosition](TextEmphasisPosition). +pub const TextEmphasisPositionVertical = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A horizontal position keyword for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property. +/// +/// See [TextEmphasisPosition](TextEmphasisPosition). +pub const TextEmphasisPositionHorizontal = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [text-shadow](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-shadow-property) property. +pub const TextShadow = struct { + /// The color of the text shadow. + color: CssColor, + /// The x offset of the text shadow. + x_offset: Length, + /// The y offset of the text shadow. + y_offset: Length, + /// The blur radius of the text shadow. + blur: Length, + /// The spread distance of the text shadow. + spread: Length, // added in Level 4 spec +}; + +/// A value for the [text-size-adjust](https://w3c.github.io/csswg-drafts/css-size-adjust/#adjustment-control) property. +pub const TextSizeAdjust = union(enum) { + /// Use the default size adjustment when displaying on a small device. + auto, + /// No size adjustment when displaying on a small device. + none, + /// When displaying on a small device, the font size is multiplied by this percentage. + percentage: Percentage, +}; + +/// A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property. +pub const Direction = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property. +pub const UnicodeBidi = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [box-decoration-break](https://www.w3.org/TR/css-break-3/#break-decoration) property. +pub const BoxDecorationBreak = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); diff --git a/src/css/properties/transform.zig b/src/css/properties/transform.zig new file mode 100644 index 0000000000000..b549831dd136b --- /dev/null +++ b/src/css/properties/transform.zig @@ -0,0 +1,202 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Result = css.Result; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; +const Percentage = css.css_values.percentage.Percentage; + +const GenericBorder = css.css_properties.border.GenericBorder; +const LineStyle = css.css_properties.border.LineStyle; + +/// A value for the [transform](https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#propdef-transform) property. +pub const TransformList = struct { + v: ArrayList(Transform), + + pub fn parse(input: *css.Parser) Result(@This()) { + _ = input; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @panic(css.todo_stuff.depth); + } +}; + +/// An individual transform function (https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#two-d-transform-functions). +pub const Transform = union(enum) { + /// A 2D translation. + translate: struct { x: LengthPercentage, y: LengthPercentage }, + /// A translation in the X direction. + translate_x: LengthPercentage, + /// A translation in the Y direction. + translate_y: LengthPercentage, + /// A translation in the Z direction. + translate_z: Length, + /// A 3D translation. + translate_3d: struct { x: LengthPercentage, y: LengthPercentage, z: Length }, + /// A 2D scale. + scale: struct { x: NumberOrPercentage, y: NumberOrPercentage }, + /// A scale in the X direction. + scale_x: NumberOrPercentage, + /// A scale in the Y direction. + scale_y: NumberOrPercentage, + /// A scale in the Z direction. + scale_z: NumberOrPercentage, + /// A 3D scale. + scale_3d: struct { x: NumberOrPercentage, y: NumberOrPercentage, z: NumberOrPercentage }, + /// A 2D rotation. + rotate: Angle, + /// A rotation around the X axis. + rotate_x: Angle, + /// A rotation around the Y axis. + rotate_y: Angle, + /// A rotation around the Z axis. + rotate_z: Angle, + /// A 3D rotation. + rotate_3d: struct { x: f32, y: f32, z: f32, angle: Angle }, + /// A 2D skew. + skew: struct { x: Angle, y: Angle }, + /// A skew along the X axis. + skew_x: Angle, + /// A skew along the Y axis. + skew_y: Angle, + /// A perspective transform. + perspective: Length, + /// A 2D matrix transform. + matrix: Matrix(f32), + /// A 3D matrix transform. + matrix_3d: Matrix3d(f32), + + pub fn parse(input: *css.Parser) Result(Transform) { + _ = input; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @panic(css.todo_stuff.depth); + } +}; + +/// A 2D matrix. +pub fn Matrix(comptime T: type) type { + return struct { + a: T, + b: T, + c: T, + d: T, + e: T, + f: T, + }; +} + +/// A 3D matrix. +pub fn Matrix3d(comptime T: type) type { + return struct { + m11: T, + m12: T, + m13: T, + m14: T, + m21: T, + m22: T, + m23: T, + m24: T, + m31: T, + m32: T, + m33: T, + m34: T, + m41: T, + m42: T, + m43: T, + m44: T, + }; +} + +/// A value for the [transform-style](https://drafts.csswg.org/css-transforms-2/#transform-style-property) property. +pub const TransformStyle = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [transform-box](https://drafts.csswg.org/css-transforms-1/#transform-box) property. +pub const TransformBox = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [backface-visibility](https://drafts.csswg.org/css-transforms-2/#backface-visibility-property) property. +pub const BackfaceVisibility = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the perspective property. +pub const Perspective = union(enum) { + /// No perspective transform is applied. + none, + /// Distance to the center of projection. + length: Length, +}; + +/// A value for the [translate](https://drafts.csswg.org/css-transforms-2/#propdef-translate) property. +pub const Translate = union(enum) { + /// The "none" keyword. + none, + + /// The x, y, and z translations. + xyz: struct { + /// The x translation. + x: LengthPercentage, + /// The y translation. + y: LengthPercentage, + /// The z translation. + z: Length, + }, +}; + +/// A value for the [rotate](https://drafts.csswg.org/css-transforms-2/#propdef-rotate) property. +pub const Rotate = struct { + /// Rotation around the x axis. + x: f32, + /// Rotation around the y axis. + y: f32, + /// Rotation around the z axis. + z: f32, + /// The angle of rotation. + angle: Angle, +}; + +/// A value for the [scale](https://drafts.csswg.org/css-transforms-2/#propdef-scale) property. +pub const Scale = union(enum) { + /// The "none" keyword. + none, + + /// Scale on the x, y, and z axis. + xyz: struct { + /// Scale on the x axis. + x: NumberOrPercentage, + /// Scale on the y axis. + y: NumberOrPercentage, + /// Scale on the z axis. + z: NumberOrPercentage, + }, +}; diff --git a/src/css/properties/transition.zig b/src/css/properties/transition.zig new file mode 100644 index 0000000000000..a44aa5cc11d6d --- /dev/null +++ b/src/css/properties/transition.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; +const Percentage = css.css_values.percentage.Percentage; + +const GenericBorder = css.css_properties.border.GenericBorder; +const LineStyle = css.css_properties.border.LineStyle; + +/// A value for the [transition](https://www.w3.org/TR/2018/WD-css-transitions-1-20181011/#transition-shorthand-property) property. +pub const Transition = @compileError(css.todo_stuff.depth); diff --git a/src/css/properties/ui.zig b/src/css/properties/ui.zig new file mode 100644 index 0000000000000..58b7cec84d52c --- /dev/null +++ b/src/css/properties/ui.zig @@ -0,0 +1,109 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; + +pub const css = @import("../css_parser.zig"); + +const SmallList = css.SmallList; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Error = css.Error; + +const ContainerName = css.css_rules.container.ContainerName; + +const LengthPercentage = css.css_values.length.LengthPercentage; +const CustomIdent = css.css_values.ident.CustomIdent; +const CSSString = css.css_values.string.CSSString; +const CSSNumber = css.css_values.number.CSSNumber; +const LengthPercentageOrAuto = css.css_values.length.LengthPercentageOrAuto; +const Size2D = css.css_values.size.Size2D; +const DashedIdent = css.css_values.ident.DashedIdent; +const Image = css.css_values.image.Image; +const CssColor = css.css_values.color.CssColor; +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 CustomIdentList = css.css_values.ident.CustomIdentList; +const Angle = css.css_values.angle.Angle; +const Url = css.css_values.url.Url; +const Percentage = css.css_values.percentage.Percentage; + +const GenericBorder = css.css_properties.border.GenericBorder; +const LineStyle = css.css_properties.border.LineStyle; + +/// A value for the [color-scheme](https://drafts.csswg.org/css-color-adjust/#color-scheme-prop) property. +pub const ColorScheme = packed struct(u8) { + /// Indicates that the element supports a light color scheme. + light: bool = false, + /// Indicates that the element supports a dark color scheme. + dark: bool = false, + /// Forbids the user agent from overriding the color scheme for the element. + only: bool = false, +}; + +/// A value for the [resize](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#resize) property. +pub const Resize = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [cursor](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) property. +pub const Cursor = struct { + /// A list of cursor images. + images: SmallList(CursorImage), + /// A pre-defined cursor. + keyword: CursorKeyword, +}; + +/// A [cursor image](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) value, used in the `cursor` property. +/// +/// See [Cursor](Cursor). +pub const CursorImage = struct { + /// A url to the cursor image. + url: Url, + /// The location in the image where the mouse pointer appears. + hotspot: ?[2]CSSNumber, +}; + +/// A pre-defined [cursor](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) value, +/// used in the `cursor` property. +/// +/// See [Cursor](Cursor). +pub const CursorKeyword = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [caret-color](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-color) property. +pub const ColorOrAuto = union(enum) { + /// The `currentColor`, adjusted by the UA to ensure contrast against the background. + auto, + /// A color. + color: CssColor, +}; + +/// A value for the [caret-shape](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-shape) property. +pub const CaretShape = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [caret](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret) shorthand property. +pub const Caret = @compileError(css.todo_stuff.depth); + +/// A value for the [user-select](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#content-selection) property. +pub const UserSelect = css.DefineEnumProperty(@compileError(css.todo_stuff.depth)); + +/// A value for the [appearance](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#appearance-switching) property. +pub const Appearance = union(enum) { + none, + auto, + textfield, + menulist_button, + button, + checkbox, + listbox, + menulist, + meter, + progress_bar, + push_button, + radio, + searchfield, + slider_horizontal, + square_button, + textarea, + non_standard: []const u8, +}; diff --git a/src/css/rules/container.zig b/src/css/rules/container.zig new file mode 100644 index 0000000000000..f158ea1ae60c6 --- /dev/null +++ b/src/css/rules/container.zig @@ -0,0 +1,331 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Location = css.css_rules.Location; +const CustomIdent = css.css_values.ident.CustomIdent; +const CustomIdentFns = css.css_values.ident.CustomIdentFns; +const QueryFeature = css.media_query.QueryFeature; +const QueryConditionFlags = css.media_query.QueryConditionFlags; +const Operator = css.media_query.Operator; + +pub const ContainerName = struct { + v: css.css_values.ident.CustomIdent, + pub fn parse(input: *css.Parser) Result(ContainerName) { + const ident = switch (CustomIdentFns.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + // todo_stuff.match_ignore_ascii_case; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("none", ident.v) or + bun.strings.eqlCaseInsensitiveASCIIICheckLength("and", ident.v) or + bun.strings.eqlCaseInsensitiveASCIIICheckLength("not", ident.v) or + bun.strings.eqlCaseInsensitiveASCIIICheckLength("or", ident.v)) + return .{ .err = input.newUnexpectedTokenError(.{ .ident = ident.v }) }; + + return .{ .result = ContainerName{ .v = ident } }; + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + return try CustomIdentFns.toCss(&this.v, W, dest); + } +}; + +pub const ContainerNameFns = ContainerName; +pub const ContainerSizeFeature = QueryFeature(ContainerSizeFeatureId); + +pub const ContainerSizeFeatureId = enum { + /// The [width](https://w3c.github.io/csswg-drafts/css-contain-3/#width) size container feature. + width, + /// The [height](https://w3c.github.io/csswg-drafts/css-contain-3/#height) size container feature. + height, + /// The [inline-size](https://w3c.github.io/csswg-drafts/css-contain-3/#inline-size) size container feature. + @"inline-size", + /// The [block-size](https://w3c.github.io/csswg-drafts/css-contain-3/#block-size) size container feature. + @"block-size", + /// The [aspect-ratio](https://w3c.github.io/csswg-drafts/css-contain-3/#aspect-ratio) size container feature. + @"aspect-ratio", + /// The [orientation](https://w3c.github.io/csswg-drafts/css-contain-3/#orientation) size container feature. + orientation, + + pub usingnamespace css.DeriveValueType(@This()); + + pub const ValueTypeMap = .{ + .width = css.MediaFeatureType.length, + .height = css.MediaFeatureType.length, + .@"inline-size" = css.MediaFeatureType.length, + .@"block-size" = css.MediaFeatureType.length, + .@"aspect-ratio" = css.MediaFeatureType.ratio, + .orientation = css.MediaFeatureType.ident, + }; + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + 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 toCssWithPrefix(this: *const @This(), prefix: []const u8, comptime W: type, dest: *Printer(W)) PrintErr!void { + try dest.writeStr(prefix); + try this.toCss(W, dest); + } +}; + +/// Represents a style query within a container condition. +pub const StyleQuery = union(enum) { + /// A style feature, implicitly parenthesized. + feature: css.Property, + + /// A negation of a condition. + not: *StyleQuery, + + /// A set of joint operations. + operation: struct { + /// The operator for the conditions. + operator: css.media_query.Operator, + /// The conditions for the operator. + conditions: ArrayList(StyleQuery), + }, + + pub fn toCss(this: *const StyleQuery, comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .feature => |f| try f.toCss(W, dest, false), + .not => |c| { + try dest.writeStr("not "); + return try css.media_query.toCssWithParensIfNeeded( + c, + W, + dest, + c.needsParens(null, &dest.targets), + ); + }, + .operation => |op| return css.media_query.operationToCss( + StyleQuery, + op.operator, + &op.conditions, + W, + dest, + ), + } + } + + pub fn parseFeature(input: *css.Parser) Result(StyleQuery) { + const property_id = switch (css.PropertyId.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectColon().asErr()) |e| return .{ .err = e }; + input.skipWhitespace(); + const opts = css.ParserOptions.default(input.allocator(), null); + const feature = .{ + .feature = switch (css.Property.parse( + property_id, + input, + &opts, + )) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }, + }; + _ = input.tryParse(css.parseImportant, .{}); + return .{ .result = feature }; + } + + pub fn createNegation(condition: *StyleQuery) StyleQuery { + return .{ .not = condition }; + } + + pub fn createOperation(operator: Operator, conditions: ArrayList(StyleQuery)) StyleQuery { + return .{ + .operation = .{ + .operator = operator, + .conditions = conditions, + }, + }; + } + + pub fn needsParens( + this: *const StyleQuery, + parent_operator: ?Operator, + _: *const css.Targets, + ) bool { + return switch (this.*) { + .not => true, + .operation => |op| op.operator == parent_operator, + .feature => true, + }; + } + + pub fn parseStyleQuery(input: *css.Parser) Result(@This()) { + return .{ .err = input.newErrorForNextToken() }; + } +}; + +pub const ContainerCondition = union(enum) { + /// A size container feature, implicitly parenthesized. + feature: ContainerSizeFeature, + /// A negation of a condition. + not: *ContainerCondition, + /// A set of joint operations. + operation: struct { + /// The operator for the conditions. + operator: css.media_query.Operator, + /// The conditions for the operator. + conditions: ArrayList(ContainerCondition), + }, + /// A style query. + style: StyleQuery, + + const This = @This(); + + pub fn parse(input: *css.Parser) Result(ContainerCondition) { + return css.media_query.parseQueryCondition( + ContainerCondition, + input, + QueryConditionFlags{ + .allow_or = true, + .allow_style = true, + }, + ); + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .feature => |f| try f.toCss(W, dest), + .not => |c| { + try dest.writeStr("not "); + return try css.media_query.toCssWithParensIfNeeded( + c, + W, + dest, + c.needsParens(null, &dest.targets), + ); + }, + .operation => |op| try css.media_query.operationToCss(ContainerCondition, op.operator, &op.conditions, W, dest), + .style => |query| { + try dest.writeStr("style("); + try query.toCss(W, dest); + try dest.writeChar(')'); + }, + } + } + + pub fn parseFeature(input: *css.Parser) Result(ContainerCondition) { + const feature = switch (QueryFeature(ContainerSizeFeatureId).parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .feature = feature } }; + } + + pub fn createNegation(condition: *ContainerCondition) ContainerCondition { + return .{ .not = condition }; + } + + pub fn createOperation(operator: Operator, conditions: ArrayList(ContainerCondition)) ContainerCondition { + return .{ + .operation = .{ + .operator = operator, + .conditions = conditions, + }, + }; + } + + pub fn parseStyleQuery(input: *css.Parser) Result(ContainerCondition) { + const Fns = struct { + pub inline fn adaptedParseQueryCondition(i: *css.Parser, flags: QueryConditionFlags) Result(StyleQuery) { + return css.media_query.parseQueryCondition(StyleQuery, i, flags); + } + + pub fn parseNestedBlockFn(_: void, i: *css.Parser) Result(ContainerCondition) { + if (i.tryParse( + @This().adaptedParseQueryCondition, + .{ + QueryConditionFlags{ .allow_or = true }, + }, + ).asValue()) |res| { + return .{ .result = .{ .style = res } }; + } + + return .{ .result = .{ + .style = switch (StyleQuery.parseFeature(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }, + } }; + } + }; + return input.parseNestedBlock(ContainerCondition, {}, Fns.parseNestedBlockFn); + } + + pub fn needsParens( + this: *const ContainerCondition, + parent_operator: ?Operator, + targets: *const css.Targets, + ) bool { + return switch (this.*) { + .not => true, + .operation => |op| op.operator == parent_operator, + .feature => |f| f.needsParens(parent_operator, targets), + .style => false, + }; + } +}; + +/// A [@container](https://drafts.csswg.org/css-contain-3/#container-rule) rule. +pub fn ContainerRule(comptime R: type) type { + return struct { + /// The name of the container. + name: ?ContainerName, + /// The container condition. + condition: ContainerCondition, + /// The rules within the `@container` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@container "); + if (this.name) |*name| { + try name.toCss(W, dest); + try dest.writeChar(' '); + } + + // Don't downlevel range syntax in container queries. + const exclude = dest.targets.exclude; + dest.targets.exclude.insert(css.targets.Features.media_queries); + try this.condition.toCss(W, dest); + dest.targets.exclude = exclude; + + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; +} diff --git a/src/css/rules/counter_style.zig b/src/css/rules/counter_style.zig new file mode 100644 index 0000000000000..a8a3e10d8d932 --- /dev/null +++ b/src/css/rules/counter_style.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; +const LayerName = css.css_rules.layer.LayerName; +const Location = css.css_rules.Location; +const Angle = css.css_values.angle.Angle; +const FontStyleProperty = css.css_properties.font.FontStyle; +const FontFamily = css.css_properties.font.FontFamily; +const FontWeight = css.css_properties.font.FontWeight; +const FontStretch = css.css_properties.font.FontStretch; +const CustomProperty = css.css_properties.custom.CustomProperty; +const CustomPropertyName = css.css_properties.custom.CustomPropertyName; +const DashedIdent = css.css_values.ident.DashedIdent; + +/// A [@counter-style](https://drafts.csswg.org/css-counter-styles/#the-counter-style-rule) rule. +pub const CounterStyleRule = struct { + /// The name of the counter style to declare. + name: css.css_values.ident.CustomIdent, + /// Declarations in the `@counter-style` rule. + declarations: css.DeclarationBlock, + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@counter-style"); + try css.css_values.ident.CustomIdentFns.toCss(&this.name, W, dest); + try this.declarations.toCssBlock(W, dest); + } +}; diff --git a/src/css/rules/custom_media.zig b/src/css/rules/custom_media.zig new file mode 100644 index 0000000000000..854abb28071bb --- /dev/null +++ b/src/css/rules/custom_media.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const css_values = @import("../values/values.zig"); +pub const Error = css.Error; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +/// A [@custom-media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) rule. +pub const CustomMediaRule = struct { + /// The name of the declared media query. + name: css_values.ident.DashedIdent, + /// The media query to declare. + query: css.MediaList, + /// The location of the rule in the source file. + loc: css.Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + try dest.writeStr("@custom-media "); + try css_values.ident.DashedIdentFns.toCss(&this.name, W, dest); + try dest.writeChar(' '); + try this.query.toCss(W, dest); + try dest.writeChar(';'); + } +}; diff --git a/src/css/rules/document.zig b/src/css/rules/document.zig new file mode 100644 index 0000000000000..2ace5662ed029 --- /dev/null +++ b/src/css/rules/document.zig @@ -0,0 +1,55 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; +const LayerName = css.css_rules.layer.LayerName; +const Location = css.css_rules.Location; +const Angle = css.css_values.angle.Angle; +const FontStyleProperty = css.css_properties.font.FontStyle; +const FontFamily = css.css_properties.font.FontFamily; +const FontWeight = css.css_properties.font.FontWeight; +const FontStretch = css.css_properties.font.FontStretch; +const CustomProperty = css.css_properties.custom.CustomProperty; +const CustomPropertyName = css.css_properties.custom.CustomPropertyName; +const DashedIdent = css.css_values.ident.DashedIdent; + +/// A [@-moz-document](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document) rule. +/// +/// Note that only the `url-prefix()` function with no arguments is supported, and only the `-moz` prefix +/// is allowed since Firefox was the only browser that ever implemented this rule. +pub fn MozDocumentRule(comptime R: type) type { + return struct { + /// Nested rules within the `@-moz-document` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + try dest.writeStr("@-moz-document url-prefix()"); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; +} diff --git a/src/css/rules/font_face.zig b/src/css/rules/font_face.zig new file mode 100644 index 0000000000000..e0a24080252fd --- /dev/null +++ b/src/css/rules/font_face.zig @@ -0,0 +1,723 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; +const LayerName = css.css_rules.layer.LayerName; +const SupportsCondition = css.css_rules.supports.SupportsCondition; +const Location = css.css_rules.Location; +const Angle = css.css_values.angle.Angle; +const FontStyleProperty = css.css_properties.font.FontStyle; +const FontFamily = css.css_properties.font.FontFamily; +const FontWeight = css.css_properties.font.FontWeight; +const FontStretch = css.css_properties.font.FontStretch; +const CustomProperty = css.css_properties.custom.CustomProperty; +const CustomPropertyName = css.css_properties.custom.CustomPropertyName; +const Result = css.Result; + +/// A property within an `@font-face` rule. +/// +/// See [FontFaceRule](FontFaceRule). +pub const FontFaceProperty = union(enum) { + /// The `src` property. + source: ArrayList(Source), + + /// The `font-family` property. + font_family: fontprops.FontFamily, + + /// The `font-style` property. + font_style: FontStyle, + + /// The `font-weight` property. + font_weight: Size2D(fontprops.FontWeight), + + /// The `font-stretch` property. + font_stretch: Size2D(fontprops.FontStretch), + + /// The `unicode-range` property. + unicode_range: ArrayList(UnicodeRange), + + /// An unknown or unsupported property. + custom: css.css_properties.custom.CustomProperty, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + const Helpers = struct { + pub fn writeProperty( + d: *Printer(W), + comptime prop: []const u8, + value: anytype, + comptime multi: bool, + ) PrintErr!void { + try d.writeStr(prop); + try d.delim(':', false); + if (comptime multi) { + const len = value.items.len; + for (value.items, 0..) |*val, idx| { + try val.toCss(W, d); + if (idx < len - 1) { + try d.delim(',', false); + } + } + } else { + try value.toCss(W, d); + } + } + }; + return switch (this.*) { + .source => |value| Helpers.writeProperty(dest, "src", value, true), + .font_family => |value| Helpers.writeProperty(dest, "font-family", value, false), + .font_style => |value| Helpers.writeProperty(dest, "font-style", value, false), + .font_weight => |value| Helpers.writeProperty(dest, "font-weight", value, false), + .font_stretch => |value| Helpers.writeProperty(dest, "font-stretch", value, false), + .unicode_range => |value| Helpers.writeProperty(dest, "unicode-range", value, true), + .custom => |custom| { + try dest.writeStr(this.custom.name.asStr()); + try dest.delim(':', false); + return custom.value.toCss(W, dest, true); + }, + }; + } +}; + +/// A contiguous range of Unicode code points. +/// +/// Cannot be empty. Can represent a single code point when start == end. +pub const UnicodeRange = struct { + /// Inclusive start of the range. In [0, end]. + start: u32, + + /// Inclusive end of the range. In [0, 0x10FFFF]. + end: u32, + + pub fn toCss(this: *const UnicodeRange, comptime W: type, dest: *Printer(W)) PrintErr!void { + // Attempt to optimize the range to use question mark syntax. + if (this.start != this.end) { + // Find the first hex digit that differs between the start and end values. + var shift: u5 = 24; + var mask: u32 = @as(u32, 0xf) << shift; + while (shift > 0) { + const c1 = this.start & mask; + const c2 = this.end & mask; + if (c1 != c2) { + break; + } + + mask = mask >> 4; + shift -= 4; + } + + // Get the remainder of the value. This must be 0x0 to 0xf for the rest + // of the value to use the question mark syntax. + shift += 4; + const remainder_mask: u32 = (@as(u32, 1) << shift) - @as(u32, 1); + const start_remainder = this.start & remainder_mask; + const end_remainder = this.end & remainder_mask; + + if (start_remainder == 0 and end_remainder == remainder_mask) { + const start = (this.start & ~remainder_mask) >> shift; + if (start != 0) { + try dest.writeFmt("U+{x}", .{start}); + } else { + try dest.writeStr("U+"); + } + + while (shift > 0) { + try dest.writeChar('?'); + shift -= 4; + } + + return; + } + } + + try dest.writeFmt("U+{x}", .{this.start}); + if (this.end != this.start) { + try dest.writeFmt("-{x}", .{this.end}); + } + } + + /// https://drafts.csswg.org/css-syntax/#urange-syntax + pub fn parse(input: *css.Parser) Result(UnicodeRange) { + // = + // u '+' '?'* | + // u '?'* | + // u '?'* | + // u | + // u | + // u '+' '?'+ + + if (input.expectIdentMatching("u").asErr()) |e| return .{ .err = e }; + const after_u = input.position(); + if (parseTokens(input).asErr()) |e| return .{ .err = e }; + + // This deviates from the spec in case there are CSS comments + // between tokens in the middle of one , + // but oh well… + const concatenated_tokens = input.sliceFrom(after_u); + + const range = if (parseConcatenated(concatenated_tokens).asValue()) |range| + range + else + return .{ .err = input.newBasicUnexpectedTokenError(.{ .ident = concatenated_tokens }) }; + + if (range.end > 0x10FFFF or range.start > range.end) { + return .{ .err = input.newBasicUnexpectedTokenError(.{ .ident = concatenated_tokens }) }; + } + + return .{ .result = range }; + } + + fn parseTokens(input: *css.Parser) Result(void) { + const tok = switch (input.nextIncludingWhitespace()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + switch (tok.*) { + .dimension => return parseQuestionMarks(input), + .number => { + const after_number = input.state(); + const token = switch (input.nextIncludingWhitespace()) { + .result => |vv| vv, + .err => { + input.reset(&after_number); + return .{ .result = {} }; + }, + }; + + if (token.* == .delim and token.delim == '?') return parseQuestionMarks(input); + if (token.* == .delim or token.* == .number) return .{ .result = {} }; + return .{ .result = {} }; + }, + .delim => |c| { + if (c == '+') { + const next = switch (input.nextIncludingWhitespace()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (!(next.* == .ident or (next.* == .delim and next.delim == '?'))) { + return .{ .err = input.newBasicUnexpectedTokenError(next.*) }; + } + return parseQuestionMarks(input); + } + }, + else => {}, + } + return .{ .err = input.newBasicUnexpectedTokenError(tok.*) }; + } + + /// Consume as many '?' as possible + fn parseQuestionMarks(input: *css.Parser) Result(void) { + while (true) { + const start = input.state(); + if (input.nextIncludingWhitespace().asValue()) |tok| if (tok.* == .delim and tok.delim == '?') continue; + input.reset(&start); + return .{ .result = {} }; + } + } + + fn parseConcatenated(_text: []const u8) css.Maybe(UnicodeRange, void) { + var text = if (_text.len > 0 and _text[0] == '+') _text[1..] else { + return .{ .err = {} }; + }; + const first_hex_value, const hex_digit_count = consumeHex(&text); + const question_marks = consumeQuestionMarks(&text); + const consumed = hex_digit_count + question_marks; + + if (consumed == 0 or consumed > 6) { + return .{ .err = {} }; + } + + if (question_marks > 0) { + if (text.len == 0) return .{ .result = UnicodeRange{ + .start = first_hex_value << @intCast(question_marks * 4), + .end = ((first_hex_value + 1) << @intCast(question_marks * 4)) - 1, + } }; + } else if (text.len == 0) { + return .{ .result = UnicodeRange{ + .start = first_hex_value, + .end = first_hex_value, + } }; + } else { + if (text.len > 0 and text[0] == '-') { + text = text[1..]; + const second_hex_value, const hex_digit_count2 = consumeHex(&text); + if (hex_digit_count2 > 0 and hex_digit_count2 <= 6 and text.len == 0) { + return .{ .result = UnicodeRange{ + .start = first_hex_value, + .end = second_hex_value, + } }; + } + } + } + return .{ .err = {} }; + } + + fn consumeQuestionMarks(text: *[]const u8) usize { + var question_marks: usize = 0; + while (bun.strings.splitFirstWithExpected(text.*, '?')) |rest| { + question_marks += 1; + text.* = rest; + } + return question_marks; + } + + fn consumeHex(text: *[]const u8) struct { u32, usize } { + var value: u32 = 0; + var digits: usize = 0; + while (bun.strings.splitFirst(text.*)) |result| { + if (toHexDigit(result.first)) |digit_value| { + value = value * 0x10 + digit_value; + digits += 1; + text.* = result.rest; + } else { + break; + } + } + return .{ value, digits }; + } + + fn toHexDigit(b: u8) ?u32 { + var digit = @as(u32, b) -% @as(u32, '0'); + if (digit < 10) return digit; + // Force the 6th bit to be set to ensure ascii is lower case. + // digit = (@as(u32, b) | 0b10_0000).wrapping_sub('a' as u32).saturating_add(10); + digit = (@as(u32, b) | 0b10_0000) -% (@as(u32, 'a') +% 10); + return if (digit < 16) digit else null; + } +}; + +pub const FontStyle = union(enum) { + /// Normal font style. + normal, + + /// Italic font style. + italic, + + /// Oblique font style, with a custom angle. + oblique: Size2D(css.css_values.angle.Angle), + + pub fn parse(input: *css.Parser) Result(FontStyle) { + const property = switch (FontStyleProperty.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ + .result = switch (property) { + .normal => .normal, + .italic => .italic, + .oblique => |angle| { + const second_angle = if (input.tryParse(css.css_values.angle.Angle.parse, .{}).asValue()) |a| a else angle; + return .{ .result = .{ + .oblique = .{ .a = angle, .b = second_angle }, + } }; + }, + }, + }; + } + + pub fn toCss(this: *const FontStyle, comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .normal => try dest.writeStr("normal"), + .italic => try dest.writeStr("italic"), + .oblique => |angle| { + try dest.writeStr("oblique"); + if (!angle.eql(&FontStyle.defaultObliqueAngle())) { + try dest.writeChar(' '); + try angle.toCss(W, dest); + } + }, + } + } + + fn defaultObliqueAngle() Size2D(Angle) { + return Size2D(Angle){ + .a = FontStyleProperty.defaultObliqueAngle(), + .b = FontStyleProperty.defaultObliqueAngle(), + }; + } +}; + +/// A font format keyword in the `format()` function of the +/// [src](https://drafts.csswg.org/css-fonts/#src-desc) +/// property of an `@font-face` rule. +pub const FontFormat = union(enum) { + /// A WOFF 1.0 font. + woff, + + /// A WOFF 2.0 font. + woff2, + + /// A TrueType font. + truetype, + + /// An OpenType font. + opentype, + + /// An Embedded OpenType (.eot) font. + embedded_opentype, + + /// OpenType Collection. + collection, + + /// An SVG font. + svg, + + /// An unknown format. + string: []const u8, + + pub fn parse(input: *css.Parser) Result(FontFormat) { + const s = switch (input.expectIdentOrString()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("woff", s)) { + return .{ .result = .woff }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("woff2", s)) { + return .{ .result = .woff2 }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("truetype", s)) { + return .{ .result = .truetype }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("opentype", s)) { + return .{ .result = .opentype }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("embedded-opentype", s)) { + return .{ .result = .embedded_opentype }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("collection", s)) { + return .{ .result = .collection }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("svg", s)) { + return .{ .result = .svg }; + } else { + return .{ .result = .{ .string = s } }; + } + } + + pub fn toCss(this: *const FontFormat, comptime W: type, dest: *Printer(W)) PrintErr!void { + // Browser support for keywords rather than strings is very limited. + // https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src + switch (this.*) { + .woff => try dest.writeStr("woff"), + .woff2 => try dest.writeStr("woff2"), + .truetype => try dest.writeStr("truetype"), + .opentype => try dest.writeStr("opentype"), + .embedded_opentype => try dest.writeStr("embedded-opentype"), + .collection => try dest.writeStr("collection"), + .svg => try dest.writeStr("svg"), + .string => try dest.writeStr(this.string), + } + } +}; + +/// A value for the [src](https://drafts.csswg.org/css-fonts/#src-desc) +/// property in an `@font-face` rule. +pub const Source = union(enum) { + /// A `url()` with optional format metadata. + url: UrlSource, + + /// The `local()` function. + local: fontprops.FontFamily, + + pub fn parse(input: *css.Parser) Result(Source) { + switch (input.tryParse(UrlSource.parse, .{})) { + .result => |url| .{ .result = return .{ .result = .{ .url = url } } }, + .err => |e| { + if (e.kind == .basic and e.kind.basic == .at_rule_body_invalid) { + return .{ .err = e }; + } + }, + } + + if (input.expectFunctionMatching("local").asErr()) |e| return .{ .err = e }; + + const Fn = struct { + pub fn parseNestedBlock(_: void, i: *css.Parser) Result(fontprops.FontFamily) { + return fontprops.FontFamily.parse(i); + } + }; + const local = switch (input.parseNestedBlock(fontprops.FontFamily, {}, Fn.parseNestedBlock)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .local = local } }; + } + + pub fn toCss(this: *const Source, comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .url => try this.url.toCss(W, dest), + .local => { + try dest.writeStr("local("); + try this.local.toCss(W, dest); + try dest.writeChar(')'); + }, + } + } +}; + +pub const FontTechnology = enum { + /// A font format keyword in the `format()` function of the + /// [src](https://drafts.csswg.org/css-fonts/#src-desc) + /// property of an `@font-face` rule. + /// A font features tech descriptor in the `tech()`function of the + /// [src](https://drafts.csswg.org/css-fonts/#font-features-tech-values) + /// property of an `@font-face` rule. + /// Supports OpenType Features. + /// https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + @"features-opentype", + + /// Supports Apple Advanced Typography Font Features. + /// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM09/AppendixF.html + @"features-aat", + + /// Supports Graphite Table Format. + /// https://scripts.sil.org/cms/scripts/render_download.php?site_id=nrsi&format=file&media_id=GraphiteBinaryFormat_3_0&filename=GraphiteBinaryFormat_3_0.pdf + @"features-graphite", + + /// A color font tech descriptor in the `tech()`function of the + /// [src](https://drafts.csswg.org/css-fonts/#src-desc) + /// property of an `@font-face` rule. + /// Supports the `COLR` v0 table. + @"color-colrv0", + + /// Supports the `COLR` v1 table. + @"color-colrv1", + + /// Supports the `SVG` table. + @"color-svg", + + /// Supports the `sbix` table. + @"color-sbix", + + /// Supports the `CBDT` table. + @"color-cbdt", + + /// Supports Variations + /// The variations tech refers to the support of font variations + variations, + + /// Supports Palettes + /// The palettes tech refers to support for font palettes + palettes, + + /// Supports Incremental + /// The incremental tech refers to client support for incremental font loading, using either the range-request or the patch-subset method + incremental, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return css.enum_property_util.toCss(@This(), this, W, dest); + } +}; + +/// A `url()` value for the [src](https://drafts.csswg.org/css-fonts/#src-desc) +/// property in an `@font-face` rule. +pub const UrlSource = struct { + /// The URL. + url: Url, + + /// Optional `format()` function. + format: ?FontFormat, + + /// Optional `tech()` function. + tech: ArrayList(FontTechnology), + + pub fn parse(input: *css.Parser) Result(UrlSource) { + const url = switch (Url.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + const format = if (input.tryParse(css.Parser.expectFunctionMatching, .{"format"}).isOk()) format: { + switch (input.parseNestedBlock(FontFormat, {}, css.voidWrap(FontFormat, FontFormat.parse))) { + .result => |vv| break :format vv, + .err => |e| return .{ .err = e }, + } + } else null; + + const tech = if (input.tryParse(css.Parser.expectFunctionMatching, .{"tech"}).isOk()) tech: { + const Fn = struct { + pub fn parseNestedBlockFn(_: void, i: *css.Parser) Result(ArrayList(FontTechnology)) { + return i.parseList(FontTechnology, FontTechnology.parse); + } + }; + break :tech switch (input.parseNestedBlock(ArrayList(FontTechnology), {}, Fn.parseNestedBlockFn)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + } else ArrayList(FontTechnology){}; + + return .{ + .result = UrlSource{ .url = url, .format = format, .tech = tech }, + }; + } + + pub fn toCss(this: *const UrlSource, comptime W: type, dest: *Printer(W)) PrintErr!void { + try this.url.toCss(W, dest); + if (this.format) |*format| { + try dest.whitespace(); + try dest.writeStr("format("); + try format.toCss(W, dest); + try dest.writeChar(')'); + } + + if (this.tech.items.len != 0) { + try dest.whitespace(); + try dest.writeStr("tech("); + try css.to_css.fromList(FontTechnology, &this.tech, W, dest); + try dest.writeChar(')'); + } + } +}; + +/// A [@font-face](https://drafts.csswg.org/css-fonts/#font-face-rule) rule. +pub const FontFaceRule = struct { + /// Declarations in the `@font-face` rule. + properties: ArrayList(FontFaceProperty), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@font-face"); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + const len = this.properties.items.len; + for (this.properties.items, 0..) |*prop, i| { + try dest.newline(); + try prop.toCss(W, dest); + if (i != len - 1 or !dest.minify) { + try dest.writeChar(';'); + } + } + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } +}; + +pub const FontFaceDeclarationParser = struct { + const This = @This(); + + pub const AtRuleParser = struct { + pub const Prelude = void; + pub const AtRule = FontFaceProperty; + + pub fn parsePrelude(_: *This, name: []const u8, input: *css.Parser) Result(Prelude) { + return .{ + .err = input.newError(css.BasicParseErrorKind{ .at_rule_invalid = name }), + }; + } + + pub fn parseBlock(_: *This, _: Prelude, _: *const css.ParserState, input: *css.Parser) Result(AtRule) { + return .{ .err = input.newError(css.BasicParseErrorKind{ .at_rule_body_invalid = {} }) }; + } + + pub fn ruleWithoutBlock(_: *This, _: Prelude, _: *const css.ParserState) css.Maybe(AtRule, void) { + return .{ .err = {} }; + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = void; + pub const QualifiedRule = FontFaceProperty; + + pub fn parsePrelude(_: *This, input: *css.Parser) Result(Prelude) { + return .{ .err = input.newError(css.BasicParseErrorKind{ .qualified_rule_invalid = {} }) }; + } + + pub fn parseBlock(_: *This, _: Prelude, _: *const css.ParserState, input: *css.Parser) Result(QualifiedRule) { + return .{ .err = input.newError(css.BasicParseErrorKind.qualified_rule_invalid) }; + } + }; + + pub const DeclarationParser = struct { + pub const Declaration = FontFaceProperty; + + pub fn parseValue(this: *This, name: []const u8, input: *css.Parser) Result(Declaration) { + _ = this; // autofix + const state = input.state(); + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "src")) { + if (input.parseCommaSeparated(Source, Source.parse).asValue()) |sources| { + return .{ .result = .{ .source = sources } }; + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "font-family")) { + if (FontFamily.parse(input).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .font_family = c } }; + } + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "font-weight")) { + if (Size2D(FontWeight).parse(input).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .font_weight = c } }; + } + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "font-style")) { + if (FontStyle.parse(input).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .font_style = c } }; + } + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "font-stretch")) { + if (Size2D(FontStretch).parse(input).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .font_stretch = c } }; + } + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "unicode-renage")) { + if (input.parseList(UnicodeRange, UnicodeRange.parse).asValue()) |c| { + if (input.expectExhausted().isOk()) { + return .{ .result = .{ .unicode_range = c } }; + } + } + } else { + // + } + + input.reset(&state); + const opts = css.ParserOptions.default(input.allocator(), null); + return .{ + .result = .{ + .custom = switch (CustomProperty.parse(CustomPropertyName.fromStr(name), input, &opts)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }, + }, + }; + } + }; + + pub const RuleBodyItemParser = struct { + pub fn parseQualified(this: *This) bool { + _ = this; // autofix + return false; + } + + pub fn parseDeclarations(this: *This) bool { + _ = this; // autofix + return true; + } + }; +}; diff --git a/src/css/rules/font_palette_values.zig b/src/css/rules/font_palette_values.zig new file mode 100644 index 0000000000000..1f33c44e758de --- /dev/null +++ b/src/css/rules/font_palette_values.zig @@ -0,0 +1,286 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; +const LayerName = css.css_rules.layer.LayerName; +const SupportsCondition = css.css_rules.supports.SupportsCondition; +const Location = css.css_rules.Location; +const Angle = css.css_values.angle.Angle; +const CustomProperty = css.css_properties.custom.CustomProperty; +const CustomPropertyName = css.css_properties.custom.CustomPropertyName; +const DashedIdent = css.css_values.ident.DashedIdent; +const FontFamily = css.css_properties.font.FontFamily; + +/// A [@font-palette-values](https://drafts.csswg.org/css-fonts-4/#font-palette-values) rule. +pub const FontPaletteValuesRule = struct { + /// The name of the font palette. + name: css.css_values.ident.DashedIdent, + /// Declarations in the `@font-palette-values` rule. + properties: ArrayList(FontPaletteValuesProperty), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn parse(name: DashedIdent, input: *css.Parser, loc: Location) Result(FontPaletteValuesRule) { + var decl_parser = FontPaletteValuesDeclarationParser{}; + var parser = css.RuleBodyParser(FontPaletteValuesDeclarationParser).new(input, &decl_parser); + var properties = ArrayList(FontPaletteValuesProperty){}; + while (parser.next()) |result| { + if (result.asValue()) |decl| { + properties.append( + input.allocator(), + decl, + ) catch unreachable; + } + } + + return .{ .result = FontPaletteValuesRule{ + .name = name, + .properties = properties, + .loc = loc, + } }; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@font-palette-values "); + try css.css_values.ident.DashedIdentFns.toCss(&this.name, W, dest); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + const len = this.properties.items.len; + for (this.properties.items, 0..) |*prop, i| { + try dest.newline(); + try prop.toCss(W, dest); + if (i != len - 1 or !dest.minify) { + try dest.writeChar(';'); + } + } + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } +}; + +pub const FontPaletteValuesProperty = union(enum) { + /// The `font-family` property. + font_family: fontprops.FontFamily, + + /// The `base-palette` property. + base_palette: BasePalette, + + /// The `override-colors` property. + override_colors: ArrayList(OverrideColors), + + /// An unknown or unsupported property. + custom: css.css_properties.custom.CustomProperty, + + /// A property within an `@font-palette-values` rule. + /// + /// See [FontPaletteValuesRule](FontPaletteValuesRule). + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .font_family => |*f| { + try dest.writeStr("font-family"); + try dest.delim(':', false); + try f.toCss(W, dest); + }, + .base_palette => |*b| { + try dest.writeStr("base-palette"); + try dest.delim(':', false); + try b.toCss(W, dest); + }, + .override_colors => |*o| { + try dest.writeStr("override-colors"); + try dest.delim(':', false); + try css.to_css.fromList(OverrideColors, o, W, dest); + }, + .custom => |*custom| { + try dest.writeStr(custom.name.asStr()); + try dest.delim(':', false); + try custom.value.toCss(W, dest, true); + }, + } + } +}; + +/// A value for the [override-colors](https://drafts.csswg.org/css-fonts-4/#override-color) +/// property in an `@font-palette-values` rule. +pub const OverrideColors = struct { + /// The index of the color within the palette to override. + index: u16, + + /// The replacement color. + color: css.css_values.color.CssColor, + + pub fn parse(input: *css.Parser) Result(OverrideColors) { + const index = switch (css.CSSIntegerFns.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (index < 0) return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + + const color = switch (css.CssColor.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (color == .current_color) return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + + return .{ + .result = OverrideColors{ + .index = @intCast(index), + .color = color, + }, + }; + } + + pub fn toCss(this: *const OverrideColors, comptime W: type, dest: *Printer(W)) PrintErr!void { + try css.CSSIntegerFns.toCss(&@as(i32, @intCast(this.index)), W, dest); + try dest.writeChar(' '); + try this.color.toCss(W, dest); + } +}; + +/// A value for the [base-palette](https://drafts.csswg.org/css-fonts-4/#base-palette-desc) +/// property in an `@font-palette-values` rule. +pub const BasePalette = union(enum) { + /// A light color palette as defined within the font. + light, + + /// A dark color palette as defined within the font. + dark, + + /// A palette index within the font. + integer: u16, + + pub fn parse(input: *css.Parser) Result(BasePalette) { + if (input.tryParse(css.CSSIntegerFns.parse, .{}).asValue()) |i| { + if (i < 0) return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + return .{ .result = .{ .integer = @intCast(i) } }; + } + + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("light", ident)) { + return .{ .result = .light }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("dark", ident)) { + return .{ .result = .dark }; + } else return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + } + + pub fn toCss(this: *const BasePalette, comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .light => try dest.writeStr("light"), + .dark => try dest.writeStr("dark"), + .integer => try css.CSSIntegerFns.toCss(&@as(i32, @intCast(this.integer)), W, dest), + } + } +}; + +pub const FontPaletteValuesDeclarationParser = struct { + const This = @This(); + + pub const DeclarationParser = struct { + pub const Declaration = FontPaletteValuesProperty; + + pub fn parseValue(this: *This, name: []const u8, input: *css.Parser) Result(Declaration) { + _ = this; // autofix + const state = input.state(); + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("font-family", name)) { + // https://drafts.csswg.org/css-fonts-4/#font-family-2-desc + if (FontFamily.parse(input).asValue()) |font_family| { + if (font_family == .generic) { + return .{ .err = input.newCustomError(css.ParserError.invalid_declaration) }; + } + return .{ .result = .{ .font_family = font_family } }; + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("base-palette", name)) { + // https://drafts.csswg.org/css-fonts-4/#base-palette-desc + if (BasePalette.parse(input).asValue()) |base_palette| { + return .{ .result = .{ .base_palette = base_palette } }; + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("override-colors", name)) { + // https://drafts.csswg.org/css-fonts-4/#override-color + if (input.parseCommaSeparated(OverrideColors, OverrideColors.parse).asValue()) |override_colors| { + return .{ .result = .{ .override_colors = override_colors } }; + } + } else { + return .{ .err = input.newCustomError(css.ParserError.invalid_declaration) }; + } + + input.reset(&state); + const opts = css.ParserOptions.default(input.allocator(), null); + return .{ .result = .{ + .custom = switch (CustomProperty.parse( + CustomPropertyName.fromStr(name), + input, + &opts, + )) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }, + } }; + } + }; + + pub const RuleBodyItemParser = struct { + pub fn parseQualified(_: *This) bool { + return false; + } + + pub fn parseDeclarations(_: *This) bool { + return true; + } + }; + + pub const AtRuleParser = struct { + pub const Prelude = void; + pub const AtRule = FontPaletteValuesProperty; + + pub fn parsePrelude(_: *This, name: []const u8, input: *css.Parser) Result(Prelude) { + return .{ .err = input.newError(css.BasicParseErrorKind{ .at_rule_invalid = name }) }; + } + + pub fn parseBlock(_: *This, _: AtRuleParser.Prelude, _: *const css.ParserState, input: *css.Parser) Result(AtRuleParser.AtRule) { + return .{ .err = input.newError(css.BasicParseErrorKind.at_rule_body_invalid) }; + } + + pub fn ruleWithoutBlock(_: *This, _: AtRuleParser.Prelude, _: *const css.ParserState) css.Maybe(AtRuleParser.AtRule, void) { + return .{ .err = {} }; + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = void; + pub const QualifiedRule = FontPaletteValuesProperty; + + pub fn parsePrelude(_: *This, input: *css.Parser) Result(Prelude) { + return .{ .err = input.newError(css.BasicParseErrorKind.qualified_rule_invalid) }; + } + + pub fn parseBlock(_: *This, _: Prelude, _: *const css.ParserState, input: *css.Parser) Result(QualifiedRule) { + return .{ .err = input.newError(css.BasicParseErrorKind.qualified_rule_invalid) }; + } + }; +}; diff --git a/src/css/rules/import.zig b/src/css/rules/import.zig new file mode 100644 index 0000000000000..0954ab6c8381b --- /dev/null +++ b/src/css/rules/import.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; +const LayerName = css.css_rules.layer.LayerName; +const SupportsCondition = css.css_rules.supports.SupportsCondition; +const Location = css.css_rules.Location; + +/// A [@import](https://drafts.csswg.org/css-cascade/#at-import) rule. +pub const ImportRule = struct { + /// The url to import. + url: []const u8, + + /// An optional cascade layer name, or `None` for an anonymous layer. + layer: ?struct { + /// PERF: null pointer optimizaiton, nullable + v: ?LayerName, + }, + + /// An optional `supports()` condition. + supports: ?SupportsCondition, + + /// A media query. + media: css.MediaList, + + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + const dep = if (dest.dependencies != null) dependencies.ImportDependency.new( + dest.allocator, + this, + dest.filename(), + ) else null; + + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@import "); + if (dep) |d| { + css.serializer.serializeString(d.placeholder, dest) catch return dest.addFmtError(); + + if (dest.dependencies) |*deps| { + deps.append( + dest.allocator, + Dependency{ .import = d }, + ) catch unreachable; + } + } else { + css.serializer.serializeString(this.url, dest) catch return dest.addFmtError(); + } + + if (this.layer) |*lyr| { + try dest.writeStr(" layer"); + if (lyr.v) |l| { + try dest.writeChar('('); + try l.toCss(W, dest); + try dest.writeChar(')'); + } + } + + if (this.supports) |*sup| { + try dest.writeStr(" supports"); + if (sup.* == .declaration) { + try sup.toCss(W, dest); + } else { + try dest.writeChar('('); + try sup.toCss(W, dest); + try dest.writeChar(')'); + } + } + + if (this.media.media_queries.items.len > 0) { + try dest.writeChar(' '); + try this.media.toCss(W, dest); + } + try dest.writeStr(";"); + } +}; diff --git a/src/css/rules/keyframes.zig b/src/css/rules/keyframes.zig new file mode 100644 index 0000000000000..e4ad00a57b32c --- /dev/null +++ b/src/css/rules/keyframes.zig @@ -0,0 +1,299 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; +const LayerName = css.css_rules.layer.LayerName; +const SupportsCondition = css.css_rules.supports.SupportsCondition; +const Location = css.css_rules.Location; +const Result = css.Result; + +pub const KeyframesListParser = struct { + const This = @This(); + + pub const DeclarationParser = struct { + pub const Declaration = Keyframe; + + pub fn parseValue(_: *This, name: []const u8, input: *css.Parser) Result(Declaration) { + return .{ .err = input.newError(css.BasicParseErrorKind{ .unexpected_token = .{ .ident = name } }) }; + } + }; + + pub const RuleBodyItemParser = struct { + pub fn parseQualified(_: *This) bool { + return true; + } + + pub fn parseDeclarations(_: *This) bool { + return false; + } + }; + + pub const AtRuleParser = struct { + pub const Prelude = void; + pub const AtRule = Keyframe; + + pub fn parsePrelude(_: *This, name: []const u8, input: *css.Parser) Result(Prelude) { + return .{ .err = input.newError(css.BasicParseErrorKind{ .at_rule_invalid = name }) }; + } + + pub fn parseBlock(_: *This, _: AtRuleParser.Prelude, _: *const css.ParserState, input: *css.Parser) Result(AtRuleParser.AtRule) { + return .{ .err = input.newError(css.BasicParseErrorKind.at_rule_body_invalid) }; + } + + pub fn ruleWithoutBlock(_: *This, _: AtRuleParser.Prelude, _: *const css.ParserState) css.Maybe(AtRuleParser.AtRule, void) { + return .{ .err = {} }; + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = ArrayList(KeyframeSelector); + pub const QualifiedRule = Keyframe; + + pub fn parsePrelude(_: *This, input: *css.Parser) Result(Prelude) { + return input.parseCommaSeparated(KeyframeSelector, KeyframeSelector.parse); + } + + pub fn parseBlock(_: *This, prelude: Prelude, _: *const css.ParserState, input: *css.Parser) Result(QualifiedRule) { + // For now there are no options that apply within @keyframes + const options = css.ParserOptions.default(input.allocator(), null); + return .{ + .result = Keyframe{ + .selectors = prelude, + .declarations = switch (css.DeclarationBlock.parse(input, &options)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }, + }, + }; + } + }; +}; + +/// KeyframesName +pub const KeyframesName = union(enum) { + /// `` of a `@keyframes` name. + ident: css.css_values.ident.CustomIdent, + /// `` of a `@keyframes` name. + custom: []const u8, + + const This = @This(); + + pub fn HashMap(comptime V: type) type { + return std.ArrayHashMapUnmanaged(KeyframesName, V, struct { + pub fn hash(_: @This(), key: KeyframesName) u32 { + return switch (key) { + .ident => std.array_hash_map.hashString(key.ident.v), + .custom => std.array_hash_map.hashString(key.custom), + }; + } + + pub fn eql(_: @This(), a: KeyframesName, b: KeyframesName, _: usize) bool { + return switch (a) { + .ident => switch (b) { + .ident => bun.strings.eql(a.ident.v, b.ident.v), + .custom => false, + }, + .custom => switch (b) { + .ident => false, + .custom => bun.strings.eql(a.custom, b.custom), + }, + }; + } + }, false); + } + + pub fn parse(input: *css.Parser) Result(KeyframesName) { + switch (switch (input.next()) { + .result => |v| v.*, + .err => |e| return .{ .err = e }, + }) { + .ident => |s| { + // todo_stuff.match_ignore_ascii_case + // CSS-wide keywords without quotes throws an error. + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "none") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "initial") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "inherit") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "unset") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "default") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "revert") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "revert-layer")) + { + return .{ .err = input.newUnexpectedTokenError(.{ .ident = s }) }; + } else { + return .{ .result = .{ .ident = .{ .v = s } } }; + } + }, + .quoted_string => |s| return .{ .result = .{ .custom = s } }, + else => |t| { + return .{ .err = input.newUnexpectedTokenError(t) }; + }, + } + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + const css_module_aimation_enabled = if (dest.css_module) |css_module| css_module.config.animation else false; + + switch (this.*) { + .ident => |ident| { + try dest.writeIdent(ident.v, css_module_aimation_enabled); + }, + .custom => |s| { + // todo_stuff.match_ignore_ascii_case + // CSS-wide keywords and `none` cannot remove quotes. + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "none") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "initial") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "inherit") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "unset") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "default") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "revert") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "revert-layer")) + { + css.serializer.serializeString(s, dest) catch return dest.addFmtError(); + } else { + try dest.writeIdent(s, css_module_aimation_enabled); + } + }, + } + } +}; + +pub const KeyframeSelector = union(enum) { + /// An explicit percentage. + percentage: css.css_values.percentage.Percentage, + /// The `from` keyword. Equivalent to 0%. + from, + /// The `to` keyword. Equivalent to 100%. + to, + + // TODO: implement this + pub usingnamespace css.DeriveParse(@This()); + + // pub fn parse(input: *css.Parser) Result(KeyframeSelector) { + // _ = input; // autofix + // @panic(css.todo_stuff.depth); + // } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .percentage => |p| { + if (dest.minify and p.v == 1.0) { + try dest.writeStr("to"); + } else { + try p.toCss(W, dest); + } + }, + .from => { + if (dest.minify) { + try dest.writeStr("0%"); + } else { + try dest.writeStr("from"); + } + }, + .to => { + try dest.writeStr("to"); + }, + } + } +}; + +/// An individual keyframe within an `@keyframes` rule. +/// +/// See [KeyframesRule](KeyframesRule). +pub const Keyframe = struct { + /// A list of keyframe selectors to associate with the declarations in this keyframe. + selectors: ArrayList(KeyframeSelector), + /// The declarations for this keyframe. + declarations: css.DeclarationBlock, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + var first = true; + for (this.selectors.items) |sel| { + if (!first) { + try dest.delim(',', false); + } + first = false; + try sel.toCss(W, dest); + } + + try this.declarations.toCssBlock(W, dest); + } +}; + +pub const KeyframesRule = struct { + /// The animation name. + /// = | + name: KeyframesName, + /// A list of keyframes in the animation. + keyframes: ArrayList(Keyframe), + /// A vendor prefix for the rule, e.g. `@-webkit-keyframes`. + vendor_prefix: css.VendorPrefix, + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + var first_rule = true; + + const PREFIXES = .{ "webkit", "moz", "ms", "o", "none" }; + + inline for (PREFIXES) |prefix_name| { + const prefix = css.VendorPrefix.fromName(prefix_name); + + if (this.vendor_prefix.contains(prefix)) { + if (first_rule) { + first_rule = false; + } else { + if (!dest.minify) { + try dest.writeChar('\n'); // no indent + } + try dest.newline(); + } + + try dest.writeChar('@'); + try prefix.toCss(W, dest); + try dest.writeStr("keyframes "); + try this.name.toCss(W, dest); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + + var first = true; + for (this.keyframes.items) |*keyframe| { + if (first) { + first = false; + } else if (!dest.minify) { + try dest.writeChar('\n'); // no indent + } + try dest.newline(); + try keyframe.toCss(W, dest); + } + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + } + } + + pub fn getFallbacks(this: *This, comptime T: type, targets: *const css.targets.Targets) []css.CssRule(T) { + _ = this; // autofix + _ = targets; // autofix + @panic(css.todo_stuff.depth); + } +}; diff --git a/src/css/rules/layer.zig b/src/css/rules/layer.zig new file mode 100644 index 0000000000000..3352a800a8067 --- /dev/null +++ b/src/css/rules/layer.zig @@ -0,0 +1,164 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const 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] +pub const LayerName = struct { + v: css.SmallList([]const u8, 1) = .{}, + + pub fn HashMap(comptime V: type) type { + return std.ArrayHashMapUnmanaged(LayerName, V, struct { + pub fn hash(_: @This(), key: LayerName) u32 { + var hasher = std.hash.Wyhash.init(0); + for (key.v.items) |part| { + hasher.update(part); + } + return hasher.final(); + } + + pub fn eql(_: @This(), a: LayerName, b: LayerName, _: usize) bool { + if (a.v.len != b.v.len) return false; + for (a.v.items, 0..) |part, i| { + if (!bun.strings.eql(part, b.v.items[i])) return false; + } + return true; + } + }, false); + } + + pub fn parse(input: *css.Parser) Result(LayerName) { + var parts: css.SmallList([]const u8, 1) = .{}; + const ident = switch (input.expectIdent()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + parts.append( + input.allocator(), + ident, + ) catch bun.outOfMemory(); + + while (true) { + const Fn = struct { + pub fn tryParseFn( + i: *css.Parser, + ) Result([]const u8) { + const name = name: { + out: { + const start_location = i.currentSourceLocation(); + const tok = switch (i.nextIncludingWhitespace()) { + .err => |e| return .{ .err = e }, + .result => |vvv| vvv, + }; + if (tok.* == .delim or tok.delim == '.') { + break :out; + } + return .{ .err = start_location.newBasicUnexpectedTokenError(tok.*) }; + } + + const start_location = i.currentSourceLocation(); + const tok = switch (i.nextIncludingWhitespace()) { + .err => |e| return .{ .err = e }, + .result => |vvv| vvv, + }; + if (tok.* == .ident) { + break :name tok.ident; + } + return .{ .err = start_location.newBasicUnexpectedTokenError(tok.*) }; + }; + return .{ .result = name }; + } + }; + + while (true) { + const name = switch (input.tryParse(Fn.tryParseFn, .{})) { + .err => break, + .result => |vvv| vvv, + }; + parts.append( + input.allocator(), + name, + ) catch bun.outOfMemory(); + } + + return .{ .result = LayerName{ .v = parts } }; + } + } + + pub fn toCss(this: *const LayerName, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + var first = true; + for (this.v.items) |name| { + if (first) { + first = false; + } else { + try dest.writeChar('.'); + } + + css.serializer.serializeIdentifier(name, dest) catch return dest.addFmtError(); + } + } +}; + +/// A [@layer block](https://drafts.csswg.org/css-cascade-5/#layer-block) rule. +pub fn LayerBlockRule(comptime R: type) type { + return struct { + /// PERF: null pointer optimizaiton, nullable + /// The name of the layer to declare, or `None` to declare an anonymous layer. + name: ?LayerName, + /// The rules within the `@layer` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@layer"); + if (this.name) |*name| { + try dest.writeChar(' '); + try name.toCss(W, dest); + } + + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; +} + +/// A [@layer statement](https://drafts.csswg.org/css-cascade-5/#layer-empty) rule. +/// +/// See also [LayerBlockRule](LayerBlockRule). +pub const LayerStatementRule = struct { + /// The layer names to declare. + names: ArrayList(LayerName), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + try dest.writeStr("@layer "); + try css.to_css.fromList(LayerName, &this.names, W, dest); + try dest.writeChar(';'); + } +}; diff --git a/src/css/rules/media.zig b/src/css/rules/media.zig new file mode 100644 index 0000000000000..93e655231d510 --- /dev/null +++ b/src/css/rules/media.zig @@ -0,0 +1,55 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Location = css.css_rules.Location; +const style = css.css_rules.style; +const CssRuleList = css.CssRuleList; + +pub fn MediaRule(comptime R: type) type { + return struct { + /// The media query list. + query: css.MediaList, + /// The rules within the `@media` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn minify(this: *This, context: *css.MinifyContext, parent_is_unused: bool) Maybe(bool, css.MinifyError) { + _ = this; // autofix + _ = context; // autofix + _ = parent_is_unused; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (dest.minify and this.query.alwaysMatches()) { + try this.rules.toCss(W, dest); + return; + } + + // #[cfg(feature = "sourcemap")] + // dest.addMapping(this.loc); + + try dest.writeStr("@media "); + try this.query.toCss(W, dest); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + return dest.writeChar('}'); + } + }; +} diff --git a/src/css/rules/namespace.zig b/src/css/rules/namespace.zig new file mode 100644 index 0000000000000..b3caf037ed0ee --- /dev/null +++ b/src/css/rules/namespace.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const css_values = @import("../values/values.zig"); +pub const Error = css.Error; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +/// A [@namespace](https://drafts.csswg.org/css-namespaces/#declaration) rule. +pub const NamespaceRule = struct { + /// An optional namespace prefix to declare, or `None` to declare the default namespace. + prefix: ?css.Ident, + /// The url of the namespace. + url: css.CSSString, + /// The location of the rule in the source file. + loc: css.Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@namespace "); + if (this.prefix) |*prefix| { + try css.css_values.ident.IdentFns.toCss(prefix, W, dest); + try dest.writeChar(' '); + } + + try css.css_values.string.CSSStringFns.toCss(&this.url, W, dest); + try dest.writeChar(':'); + } +}; diff --git a/src/css/rules/nesting.zig b/src/css/rules/nesting.zig new file mode 100644 index 0000000000000..90db3b8c91799 --- /dev/null +++ b/src/css/rules/nesting.zig @@ -0,0 +1,34 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Location = css.css_rules.Location; +const style = css.css_rules.style; + +/// A [@nest](https://www.w3.org/TR/css-nesting-1/#at-nest) rule. +pub fn NestingRule(comptime R: type) type { + return struct { + /// The style rule that defines the selector and declarations for the `@nest` rule. + style: style.StyleRule(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + if (dest.context() == null) { + try dest.writeStr("@nest "); + } + return try this.style.toCss(W, dest); + } + }; +} diff --git a/src/css/rules/page.zig b/src/css/rules/page.zig new file mode 100644 index 0000000000000..ec8806c88d55c --- /dev/null +++ b/src/css/rules/page.zig @@ -0,0 +1,384 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; +const LayerName = css.css_rules.layer.LayerName; +const SupportsCondition = css.css_rules.supports.SupportsCondition; +const Location = css.css_rules.Location; +const Angle = css.css_values.angle.Angle; +const FontStyleProperty = css.css_properties.font.FontStyle; +const FontFamily = css.css_properties.font.FontFamily; +const FontWeight = css.css_properties.font.FontWeight; +const FontStretch = css.css_properties.font.FontStretch; +const CustomProperty = css.css_properties.custom.CustomProperty; +const CustomPropertyName = css.css_properties.custom.CustomPropertyName; +const DashedIdent = css.css_values.ident.DashedIdent; + +/// A [page selector](https://www.w3.org/TR/css-page-3/#typedef-page-selector) +/// within a `@page` rule. +/// +/// Either a name or at least one pseudo class is required. +pub const PageSelector = struct { + /// An optional named page type. + name: ?[]const u8, + /// A list of page pseudo classes. + pseudo_classes: ArrayList(PagePseudoClass), + + pub fn parse(input: *css.Parser) Result(PageSelector) { + const name = if (input.tryParse(css.Parser.expectIdent, .{}).asValue()) |name| name else null; + var pseudo_classes = ArrayList(PagePseudoClass){}; + + while (true) { + // Whitespace is not allowed between pseudo classes + const state = input.state(); + if (switch (input.nextIncludingWhitespace()) { + .result => |tok| tok.* == .colon, + .err => |e| return .{ .err = e }, + }) { + pseudo_classes.append( + input.allocator(), + switch (PagePseudoClass.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }, + ) catch bun.outOfMemory(); + } else { + input.reset(&state); + break; + } + } + + if (name == null and pseudo_classes.items.len == 0) { + return .{ .err = input.newCustomError(css.ParserError.invalid_page_selector) }; + } + + return .{ + .result = PageSelector{ + .name = name, + .pseudo_classes = pseudo_classes, + }, + }; + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (this.name) |name| { + try dest.writeStr(name); + } + + for (this.pseudo_classes.items) |*pseudo| { + try dest.writeChar(':'); + try pseudo.toCss(W, dest); + } + } +}; + +pub const PageMarginRule = struct { + /// The margin box identifier for this rule. + margin_box: PageMarginBox, + /// The declarations within the rule. + declarations: css.DeclarationBlock, + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeChar('@'); + try this.margin_box.toCss(W, dest); + try this.declarations.toCssBlock(W, dest); + } +}; + +/// A [@page](https://www.w3.org/TR/css-page-3/#at-page-rule) rule. +pub const PageRule = struct { + /// A list of page selectors. + selectors: ArrayList(PageSelector), + /// The declarations within the `@page` rule. + declarations: css.DeclarationBlock, + /// The nested margin rules. + rules: ArrayList(PageMarginRule), + /// The location of the rule in the source file. + loc: Location, + + pub fn parse(selectors: ArrayList(PageSelector), input: *css.Parser, loc: Location, options: *const css.ParserOptions) Result(PageRule) { + var declarations = css.DeclarationBlock{}; + var rules = ArrayList(PageMarginRule){}; + var rule_parser = PageRuleParser{ + .declarations = &declarations, + .rules = &rules, + .options = options, + }; + var parser = css.RuleBodyParser(PageRuleParser).new(input, &rule_parser); + + while (parser.next()) |decl| { + if (decl.asErr()) |e| { + if (parser.parser.options.error_recovery) { + parser.parser.options.warn(e); + continue; + } + + return .{ .err = e }; + } + } + + return .{ .result = PageRule{ + .selectors = selectors, + .declarations = declarations, + .rules = rules, + .loc = loc, + } }; + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + try dest.writeStr("@page"); + if (this.selectors.items.len >= 1) { + const firstsel = &this.selectors.items[0]; + // Space is only required if the first selector has a name. + if (!dest.minify and firstsel.name != null) { + try dest.writeChar(' '); + } + var first = true; + for (this.selectors.items) |selector| { + if (first) { + first = false; + } else { + try dest.delim(',', false); + } + try selector.toCss(W, dest); + } + } + + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + + var i: usize = 0; + const len = this.declarations.len() + this.rules.items.len; + + const DECLS = .{ "declarations", "important_declarations" }; + inline for (DECLS) |decl_field_name| { + const decls: *const ArrayList(css.Property) = &@field(this.declarations, decl_field_name); + const important = comptime std.mem.eql(u8, decl_field_name, "important_declarations"); + for (decls.items) |*decl| { + try dest.newline(); + try decl.toCss(W, dest, important); + if (i != len - 1 or !dest.minify) { + try dest.writeChar(';'); + } + i += 1; + } + } + + if (this.rules.items.len > 0) { + if (!dest.minify and this.declarations.len() > 0) { + try dest.writeChar('\n'); + } + try dest.newline(); + + var first = true; + for (this.rules.items) |*rule| { + if (first) { + first = false; + } else { + if (!dest.minify) { + try dest.writeChar('\n'); + } + try dest.newline(); + } + try rule.toCss(W, dest); + } + } + + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } +}; + +/// A page pseudo class within an `@page` selector. +/// +/// See [PageSelector](PageSelector). +pub const PagePseudoClass = enum { + /// The `:left` pseudo class. + left, + /// The `:right` pseudo class. + right, + /// The `:first` pseudo class. + first, + /// The `:last` pseudo class. + last, + /// The `:blank` pseudo class. + blank, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return css.enum_property_util.toCss(@This(), this, W, dest); + } +}; + +/// A [page margin box](https://www.w3.org/TR/css-page-3/#margin-boxes). +pub const PageMarginBox = enum { + /// A fixed-size box defined by the intersection of the top and left margins of the page box. + @"top-left-corner", + /// A variable-width box filling the top page margin between the top-left-corner and top-center page-margin boxes. + @"top-left", + /// A variable-width box centered horizontally between the page’s left and right border edges and filling the + /// page top margin between the top-left and top-right page-margin boxes. + @"top-center", + /// A variable-width box filling the top page margin between the top-center and top-right-corner page-margin boxes. + @"top-right", + /// A fixed-size box defined by the intersection of the top and right margins of the page box. + @"top-right-corner", + /// A variable-height box filling the left page margin between the top-left-corner and left-middle page-margin boxes. + @"left-top", + /// A variable-height box centered vertically between the page’s top and bottom border edges and filling the + /// left page margin between the left-top and left-bottom page-margin boxes. + @"left-middle", + /// A variable-height box filling the left page margin between the left-middle and bottom-left-corner page-margin boxes. + @"left-bottom", + /// A variable-height box filling the right page margin between the top-right-corner and right-middle page-margin boxes. + @"right-top", + /// A variable-height box centered vertically between the page’s top and bottom border edges and filling the right + /// page margin between the right-top and right-bottom page-margin boxes. + @"right-middle", + /// A variable-height box filling the right page margin between the right-middle and bottom-right-corner page-margin boxes. + @"right-bottom", + /// A fixed-size box defined by the intersection of the bottom and left margins of the page box. + @"bottom-left-corner", + /// A variable-width box filling the bottom page margin between the bottom-left-corner and bottom-center page-margin boxes. + @"bottom-left", + /// A variable-width box centered horizontally between the page’s left and right border edges and filling the bottom + /// page margin between the bottom-left and bottom-right page-margin boxes. + @"bottom-center", + /// A variable-width box filling the bottom page margin between the bottom-center and bottom-right-corner page-margin boxes. + @"bottom-right", + /// A fixed-size box defined by the intersection of the bottom and right margins of the page box. + @"bottom-right-corner", + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + 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 const PageRuleParser = struct { + declarations: *css.DeclarationBlock, + rules: *ArrayList(PageMarginRule), + options: *const css.ParserOptions, + + const This = @This(); + + pub const DeclarationParser = struct { + pub const Declaration = void; + + pub fn parseValue(this: *This, name: []const u8, input: *css.Parser) Result(Declaration) { + return css.declaration.parse_declaration( + name, + input, + &this.declarations.declarations, + &this.declarations.important_declarations, + this.options, + ); + } + }; + + pub const RuleBodyItemParser = struct { + pub fn parseQualified(_: *This) bool { + return false; + } + + pub fn parseDeclarations(_: *This) bool { + return true; + } + }; + + pub const AtRuleParser = struct { + pub const Prelude = PageMarginBox; + pub const AtRule = void; + + pub fn parsePrelude(_: *This, name: []const u8, input: *css.Parser) Result(Prelude) { + const loc = input.currentSourceLocation(); + return switch (css.parse_utility.parseString( + input.allocator(), + PageMarginBox, + name, + PageMarginBox.parse, + )) { + .result => |v| return .{ .result = v }, + .err => { + return .{ .err = loc.newCustomError(css.ParserError{ .at_rule_invalid = name }) }; + }, + }; + } + + pub fn parseBlock(this: *This, prelude: AtRuleParser.Prelude, start: *const css.ParserState, input: *css.Parser) Result(AtRuleParser.AtRule) { + const loc = start.sourceLocation(); + const declarations = switch (css.DeclarationBlock.parse(input, this.options)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + this.rules.append(input.allocator(), PageMarginRule{ + .margin_box = prelude, + .declarations = declarations, + .loc = Location{ + .source_index = this.options.source_index, + .line = loc.line, + .column = loc.column, + }, + }) catch bun.outOfMemory(); + return Result(AtRuleParser.AtRule).success; + } + + pub fn ruleWithoutBlock(_: *This, _: AtRuleParser.Prelude, _: *const css.ParserState) css.Maybe(AtRuleParser.AtRule, void) { + return .{ .err = {} }; + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = void; + pub const QualifiedRule = void; + + pub fn parsePrelude(_: *This, input: *css.Parser) Result(Prelude) { + return .{ .err = input.newError(css.BasicParseErrorKind.qualified_rule_invalid) }; + } + + pub fn parseBlock(_: *This, _: Prelude, _: *const css.ParserState, input: *css.Parser) Result(QualifiedRule) { + return .{ .err = input.newError(css.BasicParseErrorKind.qualified_rule_invalid) }; + } + }; +}; diff --git a/src/css/rules/property.zig b/src/css/rules/property.zig new file mode 100644 index 0000000000000..71d1c5aca17b2 --- /dev/null +++ b/src/css/rules/property.zig @@ -0,0 +1,225 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Location = css.css_rules.Location; +const style = css.css_rules.style; +const SyntaxString = css.css_values.syntax.SyntaxString; +const ParsedComponent = css.css_values.syntax.ParsedComponent; + +pub const PropertyRule = struct { + name: css.css_values.ident.DashedIdent, + syntax: SyntaxString, + inherits: bool, + initial_value: ?css.css_values.syntax.ParsedComponent, + loc: Location, + + pub fn parse(name: css.css_values.ident.DashedIdent, input: *css.Parser, loc: Location) Result(PropertyRule) { + var p = PropertyRuleDeclarationParser{ + .syntax = null, + .inherits = null, + .initial_value = null, + }; + + var decl_parser = css.RuleBodyParser(PropertyRuleDeclarationParser).new(input, &p); + while (decl_parser.next()) |decl| { + if (decl.asErr()) |e| { + return .{ .err = e }; + } + } + + // `syntax` and `inherits` are always required. + const parser = decl_parser.parser; + // TODO(zack): source clones these two, but I omitted here becaues it seems 100% unnecessary + const syntax: SyntaxString = parser.syntax orelse return .{ .err = decl_parser.input.newCustomError(css.ParserError.at_rule_body_invalid) }; + const inherits: bool = parser.inherits orelse return .{ .err = decl_parser.input.newCustomError(css.ParserError.at_rule_body_invalid) }; + + // `initial-value` is required unless the syntax is a universal definition. + const initial_value = switch (syntax) { + .universal => if (parser.initial_value) |val| brk: { + var i = css.ParserInput.new(input.allocator(), val); + var p2 = css.Parser.new(&i); + + if (p2.isExhausted()) { + break :brk ParsedComponent{ + .token_list = css.TokenList{ + .v = .{}, + }, + }; + } + break :brk switch (syntax.parseValue(&p2)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + } else null, + else => brk: { + const val = parser.initial_value orelse return .{ .err = input.newCustomError(css.ParserError.at_rule_body_invalid) }; + var i = css.ParserInput.new(input.allocator(), val); + var p2 = css.Parser.new(&i); + break :brk switch (syntax.parseValue(&p2)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + }, + }; + + return .{ + .result = PropertyRule{ + .name = name, + .syntax = syntax, + .inherits = inherits, + .initial_value = initial_value, + .loc = loc, + }, + }; + } + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@property "); + try css.css_values.ident.DashedIdentFns.toCss(&this.name, W, dest); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + + try dest.writeStr("syntax:"); + try dest.whitespace(); + try this.syntax.toCss(W, dest); + try dest.writeChar(';'); + try dest.newline(); + + try dest.writeStr("inherits:"); + try dest.whitespace(); + if (this.inherits) { + try dest.writeStr("true"); + } else { + try dest.writeStr("false"); + } + + if (this.initial_value) |*initial_value| { + try dest.writeChar(';'); + try dest.newline(); + + try dest.writeStr("initial-value:"); + try dest.whitespace(); + try initial_value.toCss(W, dest); + + if (!dest.minify) { + try dest.writeChar(';'); + } + } + + dest.dedent(); + try dest.newline(); + try dest.writeChar(';'); + } +}; + +pub const PropertyRuleDeclarationParser = struct { + syntax: ?SyntaxString, + inherits: ?bool, + initial_value: ?[]const u8, + + const This = @This(); + + pub const DeclarationParser = struct { + pub const Declaration = void; + const Map = bun.ComptimeStringMap(std.meta.FieldEnum(PropertyRuleDeclarationParser), .{ + .{ "syntax", .syntax }, + .{ "inherits", .inherits }, + .{ "initial-value", .initial_value }, + }); + + pub fn parseValue(this: *This, name: []const u8, input: *css.Parser) Result(Declaration) { + // todo_stuff.match_ignore_ascii_case + + // if (Map.getASCIIICaseInsensitive( + // name)) |field| { + // return switch (field) { + // .syntax => |syntax| { + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("syntax", name)) { + const syntax = switch (SyntaxString.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + this.syntax = syntax; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("inherits", name)) { + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const inherits = if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("true", ident)) + true + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("false", ident)) + false + else + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + this.inherits = inherits; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("initial-value", name)) { + // Buffer the value into a string. We will parse it later. + const start = input.position(); + while (input.next().isOk()) {} + const initial_value = input.sliceFrom(start); + this.initial_value = initial_value; + } else { + return .{ .err = input.newCustomError(css.ParserError.invalid_declaration) }; + } + + return .{ .result = {} }; + } + }; + + pub const RuleBodyItemParser = struct { + pub fn parseQualified(_: *This) bool { + return false; + } + + pub fn parseDeclarations(_: *This) bool { + return true; + } + }; + + pub const AtRuleParser = struct { + pub const Prelude = void; + pub const AtRule = void; + + pub fn parsePrelude(_: *This, name: []const u8, input: *css.Parser) Result(Prelude) { + return .{ .err = input.newError(css.BasicParseErrorKind{ .at_rule_invalid = name }) }; + } + + pub fn parseBlock(_: *This, _: AtRuleParser.Prelude, _: *const css.ParserState, input: *css.Parser) Result(AtRuleParser.AtRule) { + return .{ .err = input.newError(css.BasicParseErrorKind.at_rule_body_invalid) }; + } + + pub fn ruleWithoutBlock(_: *This, _: AtRuleParser.Prelude, _: *const css.ParserState) css.Maybe(AtRuleParser.AtRule, void) { + return .{ .err = {} }; + } + }; + + pub const QualifiedRuleParser = struct { + pub const Prelude = void; + pub const QualifiedRule = void; + + pub fn parsePrelude(_: *This, input: *css.Parser) Result(Prelude) { + return .{ .err = input.newError(css.BasicParseErrorKind.qualified_rule_invalid) }; + } + + pub fn parseBlock(_: *This, _: Prelude, _: *const css.ParserState, input: *css.Parser) Result(QualifiedRule) { + return .{ .err = input.newError(css.BasicParseErrorKind.qualified_rule_invalid) }; + } + }; +}; diff --git a/src/css/rules/rules.zig b/src/css/rules/rules.zig new file mode 100644 index 0000000000000..099ffbb4a881f --- /dev/null +++ b/src/css/rules/rules.zig @@ -0,0 +1,364 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; + +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; + +pub const import = @import("./import.zig"); +pub const layer = @import("./layer.zig"); +pub const style = @import("./style.zig"); +pub const keyframes = @import("./keyframes.zig"); +pub const font_face = @import("./font_face.zig"); +pub const font_palette_values = @import("./font_palette_values.zig"); +pub const page = @import("./page.zig"); +pub const supports = @import("./supports.zig"); +pub const counter_style = @import("./counter_style.zig"); +pub const custom_media = @import("./custom_media.zig"); +pub const namespace = @import("./namespace.zig"); +pub const unknown = @import("./unknown.zig"); +pub const document = @import("./document.zig"); +pub const nesting = @import("./nesting.zig"); +pub const viewport = @import("./viewport.zig"); +pub const property = @import("./property.zig"); +pub const container = @import("./container.zig"); +pub const scope = @import("./scope.zig"); +pub const media = @import("./media.zig"); +pub const starting_style = @import("./starting_style.zig"); + +pub fn CssRule(comptime Rule: type) type { + return union(enum) { + /// A `@media` rule. + media: media.MediaRule(Rule), + /// An `@import` rule. + import: import.ImportRule, + /// A style rule. + style: style.StyleRule(Rule), + /// A `@keyframes` rule. + keyframes: keyframes.KeyframesRule, + /// A `@font-face` rule. + font_face: font_face.FontFaceRule, + /// A `@font-palette-values` rule. + font_palette_values: font_palette_values.FontPaletteValuesRule, + /// A `@page` rule. + page: page.PageRule, + /// A `@supports` rule. + supports: supports.SupportsRule(Rule), + /// A `@counter-style` rule. + counter_style: counter_style.CounterStyleRule, + /// A `@namespace` rule. + namespace: namespace.NamespaceRule, + /// A `@-moz-document` rule. + moz_document: document.MozDocumentRule(Rule), + /// A `@nest` rule. + nesting: nesting.NestingRule(Rule), + /// A `@viewport` rule. + viewport: viewport.ViewportRule, + /// A `@custom-media` rule. + custom_media: CustomMedia, + /// A `@layer` statement rule. + layer_statement: layer.LayerStatementRule, + /// A `@layer` block rule. + layer_block: layer.LayerBlockRule(Rule), + /// A `@property` rule. + property: property.PropertyRule, + /// A `@container` rule. + container: container.ContainerRule(Rule), + /// A `@scope` rule. + scope: scope.ScopeRule(Rule), + /// A `@starting-style` rule. + starting_style: starting_style.StartingStyleRule(Rule), + /// A placeholder for a rule that was removed. + ignored, + /// An unknown at-rule. + unknown: unknown.UnknownAtRule, + /// A custom at-rule. + custom: Rule, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.*) { + .media => |x| x.toCss(W, dest), + .import => |x| x.toCss(W, dest), + .style => |x| x.toCss(W, dest), + .keyframes => |x| x.toCss(W, dest), + .font_face => |x| x.toCss(W, dest), + .font_palette_values => |x| x.toCss(W, dest), + .page => |x| x.toCss(W, dest), + .supports => |x| x.toCss(W, dest), + .counter_style => |x| x.toCss(W, dest), + .namespace => |x| x.toCss(W, dest), + .moz_document => |x| x.toCss(W, dest), + .nesting => |x| x.toCss(W, dest), + .viewport => |x| x.toCss(W, dest), + .custom_media => |x| x.toCss(W, dest), + .layer_statement => |x| x.toCss(W, dest), + .layer_block => |x| x.toCss(W, dest), + .property => |x| x.toCss(W, dest), + .starting_style => |x| x.toCss(W, dest), + .container => |x| x.toCss(W, dest), + .scope => |x| x.toCss(W, dest), + .unknown => |x| x.toCss(W, dest), + .custom => |x| x.toCss(W, dest) catch return dest.addFmtError(), + .ignored => {}, + }; + } + }; +} + +pub fn CssRuleList(comptime AtRule: type) type { + return struct { + v: ArrayList(CssRule(AtRule)) = .{}, + + const This = @This(); + + pub fn minify(this: *This, context: *MinifyContext, parent_is_unused: bool) Maybe(void, css.MinifyError) { + var keyframe_rules: keyframes.KeyframesName.HashMap(usize) = .{}; + const layer_rules: layer.LayerName.HashMap(usize) = .{}; + _ = layer_rules; // autofix + const property_rules: css.css_values.ident.DashedIdent.HashMap(usize) = .{}; + _ = property_rules; // autofix + // const style_rules = void; + // _ = style_rules; // autofix + var rules = ArrayList(CssRule(AtRule)){}; + + for (this.v.items) |*rule| { + // NOTE Anytime you append to `rules` with this `rule`, you must set `moved_rule` to true. + var moved_rule = false; + defer if (moved_rule) { + rule.* = .ignored; + }; + + switch (rule.*) { + .keyframes => |*keyframez| { + if (context.unused_symbols.contains(switch (keyframez.name) { + .ident => |ident| ident, + .custom => |custom| custom, + })) { + continue; + } + + keyframez.minify(context); + + // Merge @keyframes rules with the same name. + if (keyframe_rules.get(keyframez.name)) |existing_idx| { + if (existing_idx < rules.items.len and rules.items[existing_idx] == .keyframes) { + var existing = &rules.items[existing_idx].keyframes; + // If the existing rule has the same vendor prefixes, replace it with this rule. + if (existing.vendor_prefix.eq(keyframez.vendor_prefix)) { + existing.* = keyframez.clone(context.allocator); + continue; + } + // Otherwise, if the keyframes are identical, merge the prefixes. + if (existing.keyframes == keyframez.keyframes) { + existing.vendor_prefix |= keyframez.vendor_prefix; + existing.vendor_prefix = context.targets.prefixes(existing.vendor_prefix, css.prefixes.Feature.at_keyframes); + continue; + } + } + } + + keyframez.vendor_prefix = context.targets.prefixes(keyframez.vendor_prefix, css.prefixes.Feature.at_keyframes); + keyframe_rules.put(context.allocator, keyframez.name, rules.items.len) catch bun.outOfMemory(); + + const fallbacks = keyframez.getFallbacks(AtRule, context.targets); + moved_rule = true; + rules.append(context.allocator, rule.*) catch bun.outOfMemory(); + rules.appendSlice(context.allocator, fallbacks) catch bun.outOfMemory(); + continue; + }, + .custom_media => { + if (context.custom_media != null) { + continue; + } + }, + .media => |*med| { + if (rules.items[rules.items.len - 1] == .media) { + var last_rule = &rules.items[rules.items.len - 1].media; + if (last_rule.query.eql(&med.query)) { + last_rule.rules.v.appendSlice(context.allocator, med.rules.v.items) catch bun.outOfMemory(); + if (last_rule.minify(context, parent_is_unused).asErr()) |e| { + return .{ .err = e }; + } + continue; + } + + switch (med.minify(context, parent_is_unused)) { + .result => continue, + .err => |e| return .{ .err = e }, + } + } + }, + .supports => |*supp| { + if (rules.items[rules.items.len - 1] == .supports) { + var last_rule = &rules.items[rules.items.len - 1].supports; + if (last_rule.condition.eql(&supp.condition)) { + continue; + } + } + + if (supp.minify(context, parent_is_unused).asErr()) |e| return .{ .err = e }; + if (supp.rules.v.items.len == 0) continue; + }, + .container => |*cont| { + _ = cont; // autofix + }, + .layer_block => |*lay| { + _ = lay; // autofix + }, + .layer_statement => |*lay| { + _ = lay; // autofix + }, + .moz_document => |*doc| { + _ = doc; // autofix + }, + .style => |*sty| { + _ = sty; // autofix + }, + .counter_style => |*cntr| { + _ = cntr; // autofix + }, + .scope => |*scpe| { + _ = scpe; // autofix + }, + .nesting => |*nst| { + _ = nst; // autofix + }, + .starting_style => |*rl| { + _ = rl; // autofix + }, + .font_palette_values => |*f| { + _ = f; // autofix + }, + .property => |*prop| { + _ = prop; // autofix + }, + else => {}, + } + + rules.append(context.allocator, rule.*) catch bun.outOfMemory(); + } + + // MISSING SHIT HERE + + css.deepDeinit(CssRule(AtRule), context.allocator, &this.v); + this.v = rules; + return .{ .result = {} }; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + var first = true; + var last_without_block = false; + + for (this.v.items) |*rule| { + if (rule.* == .ignored) continue; + + // Skip @import rules if collecting dependencies. + if (rule.* == .import) { + if (dest.remove_imports) { + const dep = if (dest.dependencies != null) Dependency{ + .import = dependencies.ImportDependency.new(dest.allocator, &rule.import, dest.filename()), + } else null; + + if (dest.dependencies) |*deps| { + deps.append(dest.allocator, dep.?) catch unreachable; + continue; + } + } + } + + if (first) { + first = false; + } else { + if (!dest.minify and + !(last_without_block and + (rule.* == .import or rule.* == .namespace or rule.* == .layer_statement))) + { + try dest.writeChar('\n'); + } + try dest.newline(); + } + try rule.toCss(W, dest); + last_without_block = rule.* == .import or rule.* == .namespace or rule.* == .layer_statement; + } + } + }; +} + +pub const MinifyContext = struct { + allocator: std.mem.Allocator, + targets: *const css.targets.Targets, + handler: *css.DeclarationHandler, + important_handler: *css.DeclarationHandler, + handler_context: css.PropertyHandlerContext, + unused_symbols: *const std.StringArrayHashMapUnmanaged(void), + custom_media: ?std.StringArrayHashMapUnmanaged(custom_media.CustomMediaRule), + css_modules: bool, +}; + +pub const Location = struct { + /// The index of the source file within the source map. + source_index: u32, + /// The line number, starting at 0. + line: u32, + /// The column number within a line, starting at 1 for first the character of the line. + /// Column numbers are counted in UTF-16 code units. + column: u32, +}; + +pub const StyleContext = struct { + selectors: *const css.SelectorList, + parent: ?*const StyleContext, +}; + +/// A key to a StyleRule meant for use in a HashMap for quickly detecting duplicates. +/// It stores a reference to a list and an index so it can access items without cloning +/// even when the list is reallocated. A hash is also pre-computed for fast lookups. +pub fn StyleRuleKey(comptime R: type) type { + return struct { + list: *const ArrayList(CssRule(R)), + index: usize, + hash: u64, + + const This = @This(); + + pub fn HashMap(comptime V: type) type { + return std.ArrayHashMapUnmanaged(StyleRuleKey(R), V, struct { + pub fn hash(_: @This(), key: This) u32 { + _ = key; // autofix + @panic("TODO"); + } + + pub fn eql(_: @This(), a: This, b: This, _: usize) bool { + return a.eql(&b); + } + }); + } + + pub fn eql(this: *const This, other: *const This) bool { + const rule = if (this.index < this.list.items.len and this.list.items[this.index] == .style) + &this.list.items[this.index].style + else + return false; + + const other_rule = if (other.index < other.list.items.len and other.list.items[other.index] == .style) + &other.list.items[other.index].style + else + return false; + + return rule.isDuplicate(other_rule); + } + }; +} diff --git a/src/css/rules/scope.zig b/src/css/rules/scope.zig new file mode 100644 index 0000000000000..93f69a7885443 --- /dev/null +++ b/src/css/rules/scope.zig @@ -0,0 +1,78 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Location = css.css_rules.Location; +const style = css.css_rules.style; +const CssRuleList = css.CssRuleList; + +/// A [@scope](https://drafts.csswg.org/css-cascade-6/#scope-atrule) rule. +/// +/// @scope () [to ()]? { +/// +/// } +pub fn ScopeRule(comptime R: type) type { + return struct { + /// A selector list used to identify the scoping root(s). + scope_start: ?css.selector.parser.SelectorList, + /// A selector list used to identify any scoping limits. + scope_end: ?css.selector.parser.SelectorList, + /// Nested rules within the `@scope` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@scope"); + try dest.whitespace(); + if (this.scope_start) |*scope_start| { + try dest.writeChar('('); + // try scope_start.toCss(W, dest); + try css.selector.serialize.serializeSelectorList(scope_start.v.items, W, dest, dest.context(), false); + try dest.writeChar(')'); + try dest.whitespace(); + } + if (this.scope_end) |*scope_end| { + if (dest.minify) { + try dest.writeChar(' '); + } + try dest.writeStr("to ("); + // is treated as an ancestor of scope end. + // https://drafts.csswg.org/css-nesting/#nesting-at-scope + if (this.scope_start) |*scope_start| { + try dest.withContext(scope_start, scope_end, struct { + pub fn toCssFn(scope_end_: *const css.selector.parser.SelectorList, comptime WW: type, d: *Printer(WW)) PrintErr!void { + return css.selector.serialize.serializeSelectorList(scope_end_.v.items, WW, d, d.context(), false); + } + }.toCssFn); + } else { + return css.selector.serialize.serializeSelectorList(scope_end.v.items, W, dest, dest.context(), false); + } + try dest.writeChar(')'); + try dest.whitespace(); + } + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + // Nested style rules within @scope are implicitly relative to the + // so clear our style context while printing them to avoid replacing & ourselves. + // https://drafts.csswg.org/css-cascade-6/#scoped-rules + try dest.withClearedContext(&this.rules, CssRuleList(R).toCss); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; +} diff --git a/src/css/rules/starting_style.zig b/src/css/rules/starting_style.zig new file mode 100644 index 0000000000000..54a74092132ea --- /dev/null +++ b/src/css/rules/starting_style.zig @@ -0,0 +1,41 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Location = css.css_rules.Location; +const style = css.css_rules.style; +const CssRuleList = css.CssRuleList; + +/// A [@starting-style](https://drafts.csswg.org/css-transitions-2/#defining-before-change-style-the-starting-style-rule) rule. +pub fn StartingStyleRule(comptime R: type) type { + return struct { + /// Nested rules within the `@starting-style` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@starting-style"); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + }; +} diff --git a/src/css/rules/style.zig b/src/css/rules/style.zig new file mode 100644 index 0000000000000..fe91f5fd563a5 --- /dev/null +++ b/src/css/rules/style.zig @@ -0,0 +1,170 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; +const LayerName = css.css_rules.layer.LayerName; +const SupportsCondition = css.css_rules.supports.SupportsCondition; +const Location = css.css_rules.Location; + +pub fn StyleRule(comptime R: type) type { + return struct { + /// The selectors for the style rule. + selectors: css.selector.parser.SelectorList, + /// A vendor prefix override, used during selector printing. + vendor_prefix: css.VendorPrefix, + /// The declarations within the style rule. + declarations: css.DeclarationBlock, + /// Nested rules within the style rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: css.Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (this.vendor_prefix.isEmpty()) { + try this.toCssBase(W, dest); + } else { + var first_rule = true; + inline for (std.meta.fields(css.VendorPrefix)) |field| { + if (field.type == bool and @field(this.vendor_prefix, field.name)) { + if (first_rule) { + first_rule = false; + } else { + if (!dest.minify) { + try dest.writeChar('\n'); // no indent + } + try dest.newline(); + } + + const prefix = css.VendorPrefix.fromName(field.name); + dest.vendor_prefix = prefix; + try this.toCssBase(W, dest); + } + } + + dest.vendor_prefix = css.VendorPrefix.empty(); + } + } + + fn toCssBase(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // If supported, or there are no targets, preserve nesting. Otherwise, write nested rules after parent. + const supports_nesting = this.rules.v.items.len == 0 or + css.Targets.shouldCompileSame( + &dest.targets, + .nesting, + ); + + const len = this.declarations.declarations.items.len + this.declarations.important_declarations.items.len; + const has_declarations = supports_nesting or len > 0 or this.rules.v.items.len == 0; + + if (has_declarations) { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try css.selector.serialize.serializeSelectorList(this.selectors.v.items, W, dest, dest.context(), false); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + + var i: usize = 0; + const DECLS = .{ "declarations", "important_declarations" }; + inline for (DECLS) |decl_field_name| { + const important = comptime std.mem.eql(u8, decl_field_name, "important_declarations"); + const decls: *const ArrayList(css.Property) = &@field(this.declarations, decl_field_name); + + for (decls.items) |*decl| { + // The CSS modules `composes` property is handled specially, and omitted during printing. + // We need to add the classes it references to the list for the selectors in this rule. + if (decl.* == .composes) { + const composes = &decl.composes; + if (dest.isNested() and dest.css_module != null) { + return dest.newError(css.PrinterErrorKind.invalid_composes_nesting, composes.loc); + } + + if (dest.css_module) |*css_module| { + if (css_module.handleComposes( + dest.allocator, + &this.selectors, + composes, + this.loc.source_index, + ).asErr()) |error_kind| { + return dest.newError(error_kind, composes.loc); + } + continue; + } + } + + try dest.newline(); + try decl.toCss(W, dest, important); + if (i != len - 1 or !dest.minify or (supports_nesting and this.rules.v.items.len > 0)) { + try dest.writeChar(';'); + } + + i += 1; + } + } + } + + const Helpers = struct { + pub fn newline( + self: *const This, + comptime W2: type, + d: *Printer(W2), + supports_nesting2: bool, + len1: usize, + ) PrintErr!void { + if (!d.minify and (supports_nesting2 or len1 > 0) and self.rules.v.items.len > 0) { + if (len1 > 0) { + try d.writeChar('\n'); + } + try d.newline(); + } + } + + pub fn end(comptime W2: type, d: *Printer(W2), has_decls: bool) PrintErr!void { + if (has_decls) { + d.dedent(); + try d.newline(); + try d.writeChar('}'); + } + } + }; + + // Write nested rules after the parent. + if (supports_nesting) { + try Helpers.newline(this, W, dest, supports_nesting, len); + try this.rules.toCss(W, dest); + try Helpers.end(W, dest, has_declarations); + } else { + try Helpers.end(W, dest, has_declarations); + try Helpers.newline(this, W, dest, supports_nesting, len); + try dest.withContext(&this.selectors, this, This.toCss); + } + } + + /// Returns whether this rule is a duplicate of another rule. + /// This means it has the same selectors and properties. + pub inline fn isDuplicate(this: *const This, other: *const This) bool { + return this.declarations.len() == other.declarations.len() and + this.selectors.eql(&other.selectors) and + brk: { + const len = @min(this.declarations.len(), other.declarations.len()); + for (this.declarations[0..len], other.declarations[0..len]) |*a, *b| { + if (!a.eql(b)) break :brk false; + } + break :brk true; + }; + } + }; +} diff --git a/src/css/rules/supports.zig b/src/css/rules/supports.zig new file mode 100644 index 0000000000000..4be232ebdc82a --- /dev/null +++ b/src/css/rules/supports.zig @@ -0,0 +1,389 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Dependency = css.Dependency; +const dependencies = css.dependencies; +const Url = css.css_values.url.Url; +const Size2D = css.css_values.size.Size2D; +const fontprops = css.css_properties.font; +const LayerName = css.css_rules.layer.LayerName; +const Location = css.css_rules.Location; +const Angle = css.css_values.angle.Angle; +const FontStyleProperty = css.css_properties.font.FontStyle; +const FontFamily = css.css_properties.font.FontFamily; +const FontWeight = css.css_properties.font.FontWeight; +const FontStretch = css.css_properties.font.FontStretch; +const CustomProperty = css.css_properties.custom.CustomProperty; +const CustomPropertyName = css.css_properties.custom.CustomPropertyName; +const DashedIdent = css.css_values.ident.DashedIdent; + +/// A [``](https://drafts.csswg.org/css-conditional-3/#typedef-supports-condition), +/// as used in the `@supports` and `@import` rules. +pub const SupportsCondition = union(enum) { + /// A `not` expression. + not: *SupportsCondition, + + /// An `and` expression. + @"and": ArrayList(SupportsCondition), + + /// An `or` expression. + @"or": ArrayList(SupportsCondition), + + /// A declaration to evaluate. + declaration: struct { + /// The property id for the declaration. + property_id: css.PropertyId, + /// The raw value of the declaration. + value: []const u8, + }, + + /// A selector to evaluate. + selector: []const u8, + + /// An unknown condition. + unknown: []const u8, + + pub fn deepClone(this: *const SupportsCondition, allocator: std.mem.Allocator) SupportsCondition { + _ = allocator; // autofix + _ = this; // autofix + @panic(css.todo_stuff.depth); + } + + fn needsParens(this: *const SupportsCondition, parent: *const SupportsCondition) bool { + return switch (this.*) { + .not => true, + .@"and" => parent.* != .@"and", + .@"or" => parent.* != .@"or", + else => false, + }; + } + + const SeenDeclKey = struct { + css.PropertyId, + []const u8, + }; + + pub fn parse(input: *css.Parser) Result(SupportsCondition) { + if (input.tryParse(css.Parser.expectIdentMatching, .{"not"}).isOk()) { + const in_parens = switch (SupportsCondition.parseInParens(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ + .result = .{ + .not = bun.create( + input.allocator(), + SupportsCondition, + in_parens, + ), + }, + }; + } + + const in_parens: SupportsCondition = switch (SupportsCondition.parseInParens(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + var expected_type: ?i32 = null; + var conditions = ArrayList(SupportsCondition){}; + const mapalloc: std.mem.Allocator = input.allocator(); + var seen_declarations = std.ArrayHashMap( + SeenDeclKey, + usize, + struct { + pub fn hash(self: @This(), s: SeenDeclKey) u32 { + _ = self; // autofix + return std.array_hash_map.hashString(s[1]) +% @intFromEnum(s[0]); + } + pub fn eql(self: @This(), a: SeenDeclKey, b: SeenDeclKey, b_index: usize) bool { + _ = self; // autofix + _ = b_index; // autofix + return seenDeclKeyEql(a, b); + } + + pub inline fn seenDeclKeyEql(this: SeenDeclKey, that: SeenDeclKey) bool { + return @intFromEnum(this[0]) == @intFromEnum(that[0]) and bun.strings.eql(this[1], that[1]); + } + }, + false, + ).init(mapalloc); + defer seen_declarations.deinit(); + + while (true) { + const Closure = struct { + expected_type: *?i32, + pub fn tryParseFn(i: *css.Parser, this: *@This()) Result(SupportsCondition) { + const location = i.currentSourceLocation(); + const s = switch (i.expectIdent()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const found_type: i32 = found_type: { + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("and", s)) break :found_type 1; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("or", s)) break :found_type 2; + return .{ .err = location.newUnexpectedTokenError(.{ .ident = s }) }; + }; + + if (this.expected_type.*) |expected| { + if (found_type != expected) { + return .{ .err = location.newUnexpectedTokenError(.{ .ident = s }) }; + } + } else { + this.expected_type.* = found_type; + } + + return SupportsCondition.parseInParens(i); + } + }; + var closure = Closure{ + .expected_type = &expected_type, + }; + const _condition = input.tryParse(Closure.tryParseFn, .{&closure}); + + switch (_condition) { + .result => |condition| { + if (conditions.items.len == 0) { + conditions.append(input.allocator(), in_parens.deepClone(input.allocator())) catch bun.outOfMemory(); + if (in_parens == .declaration) { + const property_id = in_parens.declaration.property_id; + const value = in_parens.declaration.value; + seen_declarations.put( + .{ property_id.withPrefix(css.VendorPrefix{ .none = true }), value }, + 0, + ) catch bun.outOfMemory(); + } + } + + if (condition == .declaration) { + // Merge multiple declarations with the same property id (minus prefix) and value together. + const property_id_ = condition.declaration.property_id; + const value = condition.declaration.value; + + const property_id = property_id_.withPrefix(css.VendorPrefix{ .none = true }); + const key = SeenDeclKey{ property_id, value }; + if (seen_declarations.get(key)) |index| { + const cond = &conditions.items[index]; + if (cond.* == .declaration) { + cond.declaration.property_id.addPrefix(property_id.prefix()); + } + } else { + seen_declarations.put(key, conditions.items.len) catch bun.outOfMemory(); + conditions.append(input.allocator(), SupportsCondition{ .declaration = .{ + .property_id = property_id, + .value = value, + } }) catch bun.outOfMemory(); + } + } else { + conditions.append( + input.allocator(), + condition, + ) catch bun.outOfMemory(); + } + }, + else => break, + } + } + + if (conditions.items.len == 1) { + const ret = conditions.pop(); + defer conditions.deinit(input.allocator()); + return .{ .result = ret }; + } + + if (expected_type == 1) return .{ .result = .{ .@"and" = conditions } }; + if (expected_type == 2) return .{ .result = .{ .@"or" = conditions } }; + return .{ .result = in_parens }; + } + + pub fn parseDeclaration(input: *css.Parser) Result(SupportsCondition) { + const property_id = switch (css.PropertyId.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (input.expectColon().asErr()) |e| return .{ .err = e }; + input.skipWhitespace(); + const pos = input.position(); + if (input.expectNoErrorToken().asErr()) |e| return .{ .err = e }; + return .{ .result = SupportsCondition{ + .declaration = .{ + .property_id = property_id, + .value = input.sliceFrom(pos), + }, + } }; + } + + fn parseInParens(input: *css.Parser) Result(SupportsCondition) { + input.skipWhitespace(); + const location = input.currentSourceLocation(); + const pos = input.position(); + const tok = switch (input.next()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + switch (tok.*) { + .function => |f| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("selector", f)) { + const Fn = struct { + pub fn tryParseFn(i: *css.Parser) Result(SupportsCondition) { + return i.parseNestedBlock(SupportsCondition, {}, @This().parseNestedBlockFn); + } + pub fn parseNestedBlockFn(_: void, i: *css.Parser) Result(SupportsCondition) { + const p = i.position(); + if (i.expectNoErrorToken().asErr()) |e| return .{ .err = e }; + return .{ .result = SupportsCondition{ .selector = i.sliceFrom(p) } }; + } + }; + const res = input.tryParse(Fn.tryParseFn, .{}); + if (res.isOk()) return res; + } + }, + .open_curly => {}, + else => return .{ .err = location.newUnexpectedTokenError(tok.*) }, + } + + if (input.parseNestedBlock(void, {}, struct { + pub fn parseFn(_: void, i: *css.Parser) Result(void) { + return i.expectNoErrorToken(); + } + }.parseFn).asErr()) |err| { + return .{ .err = err }; + } + + return .{ .result = SupportsCondition{ .unknown = input.sliceFrom(pos) } }; + } + + pub fn toCss(this: *const SupportsCondition, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + switch (this.*) { + .not => |condition| { + try dest.writeStr(" not "); + try condition.toCssWithParensIfNeeded(W, dest, condition.needsParens(this)); + }, + .@"and" => |conditions| { + var first = true; + for (conditions.items) |*cond| { + if (first) { + first = false; + } else { + try dest.writeStr(" and "); + } + try cond.toCssWithParensIfNeeded(W, dest, cond.needsParens(this)); + } + }, + .@"or" => |conditions| { + var first = true; + for (conditions.items) |*cond| { + if (first) { + first = false; + } else { + try dest.writeStr(" or "); + } + try cond.toCssWithParensIfNeeded(W, dest, cond.needsParens(this)); + } + }, + .declaration => |decl| { + const property_id = decl.property_id; + const value = decl.value; + + try dest.writeChar('('); + + const prefix: css.VendorPrefix = property_id.prefix().orNone(); + if (!prefix.eq(css.VendorPrefix{ .none = true })) { + try dest.writeChar('('); + } + + const name = property_id.name(); + var first = true; + inline for (std.meta.fields(css.VendorPrefix)) |field_| { + const field: std.builtin.Type.StructField = field_; + if (!(comptime std.mem.eql(u8, field.name, "__unused"))) { + if (@field(prefix, field.name)) { + if (first) { + first = false; + } else { + try dest.writeStr(") or ("); + } + + var p = css.VendorPrefix{}; + @field(p, field.name) = true; + css.serializer.serializeName(name, dest) catch return dest.addFmtError(); + try dest.delim(':', false); + try dest.writeStr(value); + } + } + } + + if (!prefix.eq(css.VendorPrefix{ .none = true })) { + try dest.writeChar(')'); + } + try dest.writeChar(')'); + }, + .selector => |sel| { + try dest.writeStr("selector("); + try dest.writeStr(sel); + try dest.writeChar(')'); + }, + .unknown => |unk| { + try dest.writeStr(unk); + }, + } + } + + pub fn toCssWithParensIfNeeded( + this: *const SupportsCondition, + comptime W: type, + dest: *css.Printer( + W, + ), + needs_parens: bool, + ) css.PrintErr!void { + if (needs_parens) try dest.writeStr("("); + try this.toCss(W, dest); + if (needs_parens) try dest.writeStr(")"); + } +}; + +/// A [@supports](https://drafts.csswg.org/css-conditional-3/#at-supports) rule. +pub fn SupportsRule(comptime R: type) type { + return struct { + /// The supports condition. + condition: SupportsCondition, + /// The rules within the `@supports` rule. + rules: css.CssRuleList(R), + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeStr("@supports "); + try this.condition.toCss(W, dest); + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try this.rules.toCss(W, dest); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } + + pub fn minify(this: *This, context: *css.MinifyContext, parent_is_unused: bool) Maybe(void, css.MinifyError) { + _ = this; // autofix + _ = context; // autofix + _ = parent_is_unused; // autofix + @panic(css.todo_stuff.depth); + } + }; +} diff --git a/src/css/rules/unknown.zig b/src/css/rules/unknown.zig new file mode 100644 index 0000000000000..91da16a587771 --- /dev/null +++ b/src/css/rules/unknown.zig @@ -0,0 +1,51 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const css_values = @import("../values/values.zig"); +pub const Error = css.Error; +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +/// An unknown at-rule, stored as raw tokens. +pub const UnknownAtRule = struct { + /// The name of the at-rule (without the @). + name: []const u8, + /// The prelude of the rule. + prelude: css.TokenList, + /// The contents of the block, if any. + block: ?css.TokenList, + /// The location of the rule in the source file. + loc: css.Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + + try dest.writeChar('@'); + try dest.writeStr(this.name); + + if (this.prelude.v.items.len > 0) { + try dest.writeChar(' '); + try this.prelude.toCss(W, dest, false); + } + + if (this.block) |*block| { + try dest.whitespace(); + try dest.writeChar('{'); + dest.indent(); + try dest.newline(); + try block.toCss(W, dest, false); + dest.dedent(); + try dest.newline(); + try dest.writeChar('}'); + } else { + try dest.writeChar(';'); + } + } +}; diff --git a/src/css/rules/viewport.zig b/src/css/rules/viewport.zig new file mode 100644 index 0000000000000..23c9e8e381a2c --- /dev/null +++ b/src/css/rules/viewport.zig @@ -0,0 +1,34 @@ +const std = @import("std"); +pub const css = @import("../css_parser.zig"); +const bun = @import("root").bun; +const Error = css.Error; +const ArrayList = std.ArrayListUnmanaged; +const MediaList = css.MediaList; +const CustomMedia = css.CustomMedia; +const Printer = css.Printer; +const Maybe = css.Maybe; +const PrinterError = css.PrinterError; +const PrintErr = css.PrintErr; +const Location = css.css_rules.Location; +const style = css.css_rules.style; + +/// A [@viewport](https://drafts.csswg.org/css-device-adapt/#atviewport-rule) rule. +pub const ViewportRule = struct { + /// The vendor prefix for this rule, e.g. `@-ms-viewport`. + vendor_prefix: css.VendorPrefix, + /// The declarations within the `@viewport` rule. + declarations: css.DeclarationBlock, + /// The location of the rule in the source file. + loc: Location, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // #[cfg(feature = "sourcemap")] + // dest.add_mapping(self.loc); + try dest.writeChar('@'); + try this.vendor_prefix.toCss(W, dest); + try dest.writeStr("viewport"); + try this.declarations.toCssBlock(W, dest); + } +}; diff --git a/src/css/selectors/builder.zig b/src/css/selectors/builder.zig new file mode 100644 index 0000000000000..fb96b46fb14bd --- /dev/null +++ b/src/css/selectors/builder.zig @@ -0,0 +1,215 @@ +//! This is the selector builder module ported from the copypasted implementation from +//! servo in lightningcss. +//! +//! -- original comment from servo -- +//! Helper module to build up a selector safely and efficiently. +//! +//! Our selector representation is designed to optimize matching, and has +//! several requirements: +//! * All simple selectors and combinators are stored inline in the same buffer +//! as Component instances. +//! * We store the top-level compound selectors from right to left, i.e. in +//! matching order. +//! * We store the simple selectors for each combinator from left to right, so +//! that we match the cheaper simple selectors first. +//! +//! Meeting all these constraints without extra memmove traffic during parsing +//! is non-trivial. This module encapsulates those details and presents an +//! easy-to-use API for the parser. +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +const CSSString = css.CSSString; +const CSSStringFns = css.CSSStringFns; + +pub const Printer = css.Printer; +pub const PrintErr = css.PrintErr; + +const Result = css.Result; +const PrintResult = css.PrintResult; + +const ArrayList = std.ArrayListUnmanaged; + +const parser = css.selector.parser; + +const ValidSelectorImpl = parser.ValidSelectorImpl; +const GenericComponent = parser.GenericComponent; +const Combinator = parser.Combinator; +const SpecifityAndFlags = parser.SpecifityAndFlags; +const compute_specifity = parser.compute_specifity; +const SelectorFlags = parser.SelectorFlags; + +/// Top-level SelectorBuilder struct. This should be stack-allocated by the +/// consumer and never moved (because it contains a lot of inline data that +/// would be slow to memmov). +/// +/// After instantiation, callers may call the push_simple_selector() and +/// push_combinator() methods to append selector data as it is encountered +/// (from left to right). Once the process is complete, callers should invoke +/// build(), which transforms the contents of the SelectorBuilder into a heap- +/// allocated Selector and leaves the builder in a drained state. +pub fn SelectorBuilder(comptime Impl: type) type { + ValidSelectorImpl(Impl); + + return struct { + /// The entire sequence of simple selectors, from left to right, without combinators. + /// + /// We make this large because the result of parsing a selector is fed into a new + /// Arc-ed allocation, so any spilled vec would be a wasted allocation. Also, + /// Components are large enough that we don't have much cache locality benefit + /// from reserving stack space for fewer of them. + /// + simple_selectors: css.SmallList(GenericComponent(Impl), 32) = .{}, + + /// The combinators, and the length of the compound selector to their left. + /// + combinators: css.SmallList(struct { Combinator, usize }, 32) = .{}, + + /// The length of the current compound selector. + current_len: usize = 0, + + allocator: Allocator, + + const This = @This(); + + const BuildResult = struct { + specifity_and_flags: SpecifityAndFlags, + components: ArrayList(GenericComponent(Impl)), + }; + + pub inline fn init(allocator: Allocator) This { + return This{ + .allocator = allocator, + }; + } + + /// Returns true if combinators have ever been pushed to this builder. + pub inline fn hasCombinators(this: *This) bool { + return this.combinators.items.len > 0; + } + + /// Completes the current compound selector and starts a new one, delimited + /// by the given combinator. + pub inline fn pushCombinator(this: *This, combinator: Combinator) void { + this.combinators.append(this.allocator, .{ combinator, this.current_len }) catch unreachable; + this.current_len = 0; + } + + /// Pushes a simple selector onto the current compound selector. + pub fn pushSimpleSelector(this: *This, ss: GenericComponent(Impl)) void { + bun.assert(!ss.isCombinator()); + this.simple_selectors.append(this.allocator, ss) catch unreachable; + this.current_len += 1; + } + + pub fn addNestingPrefix(this: *This) void { + this.combinators.insert(this.allocator, 0, .{ Combinator.descendant, 1 }) catch unreachable; + this.simple_selectors.insert(this.allocator, 0, .nesting) catch bun.outOfMemory(); + } + + pub fn deinit(this: *This) void { + this.simple_selectors.deinit(this.allocator); + this.combinators.deinit(this.allocator); + } + + /// Consumes the builder, producing a Selector. + /// + /// *NOTE*: This will free all allocated memory in the builder + pub fn build( + this: *This, + parsed_pseudo: bool, + parsed_slotted: bool, + parsed_part: bool, + ) BuildResult { + const specifity = compute_specifity(Impl, this.simple_selectors.items); + var flags = SelectorFlags.empty(); + // PERF: is it faster to do these ORs all at once + if (parsed_pseudo) { + flags.has_pseudo = true; + } + if (parsed_slotted) { + flags.has_slotted = true; + } + if (parsed_part) { + flags.has_part = true; + } + // `buildWithSpecificityAndFlags()` will + defer this.deinit(); + return this.buildWithSpecificityAndFlags(SpecifityAndFlags{ .specificity = specifity, .flags = flags }); + } + + /// Builds a selector with the given specifity and flags. + /// + /// PERF: + /// Recall that this code is ported from servo, which optimizes for matching speed, so + /// the final AST has the components of the selector stored in reverse order, which is + /// optimized for matching. + /// + /// We don't really care about matching selectors, and storing the components in reverse + /// order requires additional allocations, and undoing the reversal when serializing the + /// selector. So we could just change this code to store the components in the same order + /// as the source. + pub fn buildWithSpecificityAndFlags(this: *This, spec: SpecifityAndFlags) BuildResult { + const T = GenericComponent(Impl); + const rest: []const T, const current: []const T = splitFromEnd(T, this.simple_selectors.items, this.current_len); + const combinators = this.combinators.items; + defer { + // This function should take every component from `this.simple_selectors` + // and place it into `components` and return it. + // + // This means that we shouldn't leak any `GenericComponent(Impl)`, so + // it is safe to just set the length to 0. + // + // Combinators don't need to be deinitialized because they are simple enums. + this.simple_selectors.items.len = 0; + this.combinators.items.len = 0; + } + + var components = ArrayList(T){}; + + var current_simple_selectors_i: usize = 0; + var combinator_i: i64 = @as(i64, @intCast(this.combinators.items.len)) - 1; + var rest_of_simple_selectors = rest; + var current_simple_selectors = current; + + while (true) { + if (current_simple_selectors_i < current_simple_selectors.len) { + components.append( + this.allocator, + current_simple_selectors[current_simple_selectors_i], + ) catch unreachable; + current_simple_selectors_i += 1; + } else { + if (combinator_i >= 0) { + const combo: Combinator, const len: usize = combinators[@intCast(combinator_i)]; + const rest2, const current2 = splitFromEnd(GenericComponent(Impl), rest_of_simple_selectors, len); + rest_of_simple_selectors = rest2; + current_simple_selectors_i = 0; + current_simple_selectors = current2; + combinator_i -= 1; + components.append( + this.allocator, + .{ .combinator = combo }, + ) catch unreachable; + continue; + } + break; + } + } + + return .{ .specifity_and_flags = spec, .components = components }; + } + + pub fn splitFromEnd(comptime T: type, s: []const T, at: usize) struct { []const T, []const T } { + const midpoint = s.len - at; + return .{ + s[0..midpoint], + s[midpoint..], + }; + } + }; +} diff --git a/src/css/selectors/parser.zig b/src/css/selectors/parser.zig new file mode 100644 index 0000000000000..34e6982f73b40 --- /dev/null +++ b/src/css/selectors/parser.zig @@ -0,0 +1,3239 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +const CSSString = css.CSSString; +const CSSStringFns = css.CSSStringFns; + +pub const Printer = css.Printer; +pub const PrintErr = css.PrintErr; + +const Result = css.Result; +const PrintResult = css.PrintResult; +const ArrayList = std.ArrayListUnmanaged; + +const impl = css.selector.impl; +const serialize = css.selector.serialize; + +/// Instantiation of generic selector structs using our implementation of the `SelectorImpl` trait. +pub const Component = GenericComponent(impl.Selectors); +pub const Selector = GenericSelector(impl.Selectors); +pub const SelectorList = GenericSelectorList(impl.Selectors); + +pub const ToCssCtx = enum { + lightning, + servo, +}; + +/// The definition of whitespace per CSS Selectors Level 3 § 4. +pub const SELECTOR_WHITESPACE: []const u8 = &[_]u8{ ' ', '\t', '\n', '\r', 0x0C }; + +pub fn ValidSelectorImpl(comptime T: type) void { + _ = T.SelectorImpl.ExtraMatchingData; + _ = T.SelectorImpl.AttrValue; + _ = T.SelectorImpl.Identifier; + _ = T.SelectorImpl.LocalName; + _ = T.SelectorImpl.NamespaceUrl; + _ = T.SelectorImpl.NamespacePrefix; + _ = T.SelectorImpl.BorrowedNamespaceUrl; + _ = T.SelectorImpl.BorrowedLocalName; + + _ = T.SelectorImpl.NonTSPseudoClass; + _ = T.SelectorImpl.VendorPrefix; + _ = T.SelectorImpl.PseudoElement; +} + +const selector_builder = @import("./builder.zig"); + +pub const attrs = struct { + pub fn NamespaceUrl(comptime Impl: type) type { + return struct { + prefix: Impl.SelectorImpl.NamespacePrefix, + url: Impl.SelectorImpl.NamespaceUrl, + }; + } + + pub fn AttrSelectorWithOptionalNamespace(comptime Impl: type) type { + return struct { + namespace: ?NamespaceConstraint(NamespaceUrl(Impl)), + local_name: Impl.SelectorImpl.LocalName, + local_name_lower: Impl.SelectorImpl.LocalName, + operation: ParsedAttrSelectorOperation(Impl.SelectorImpl.AttrValue), + never_matches: bool, + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + try dest.writeChar('['); + if (this.namespace) |nsp| switch (nsp) { + .specific => |v| { + try css.IdentFns.toCss(&v.prefix, W, dest); + try dest.writeChar('|'); + }, + .any => { + try dest.writeStr("*|"); + }, + }; + try css.IdentFns.toCss(&this.local_name, W, dest); + switch (this.operation) { + .exists => {}, + .with_value => |v| { + try v.operator.toCss(W, dest); + // try v.expected_value.toCss(dest); + try CSSStringFns.toCss(&v.expected_value, W, dest); + switch (v.case_sensitivity) { + .case_sensitive, .ascii_case_insensitive_if_in_html_element_in_html_document => {}, + .ascii_case_insensitive => { + try dest.writeStr(" i"); + }, + .explicit_case_sensitive => { + try dest.writeStr(" s"); + }, + } + }, + } + return dest.writeChar(']'); + } + }; + } + + pub fn NamespaceConstraint(comptime NamespaceUrl_: type) type { + return union(enum) { + any, + /// Empty string for no namespace + specific: NamespaceUrl_, + }; + } + + pub fn ParsedAttrSelectorOperation(comptime AttrValue: type) type { + return union(enum) { + exists, + with_value: struct { + operator: AttrSelectorOperator, + case_sensitivity: ParsedCaseSensitivity, + expected_value: AttrValue, + }, + }; + } + + pub const AttrSelectorOperator = enum { + equal, + includes, + dash_match, + prefix, + substring, + suffix, + + const This = @This(); + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + // https://drafts.csswg.org/cssom/#serializing-selectors + // See "attribute selector". + return dest.writeStr(switch (this.*) { + .equal => "=", + .includes => "~=", + .dash_match => "|=", + .prefix => "^=", + .substring => "*=", + .suffix => "$=", + }); + } + }; + + pub const AttrSelectorOperation = enum { + equal, + includes, + dash_match, + prefix, + substring, + suffix, + }; + + pub const ParsedCaseSensitivity = enum { + // 's' was specified. + explicit_case_sensitive, + // 'i' was specified. + ascii_case_insensitive, + // No flags were specified and HTML says this is a case-sensitive attribute. + case_sensitive, + // No flags were specified and HTML says this is a case-insensitive attribute. + ascii_case_insensitive_if_in_html_element_in_html_document, + }; +}; + +pub const Specifity = struct { + id_selectors: u32 = 0, + class_like_selectors: u32 = 0, + element_selectors: u32 = 0, + + const MAX_10BIT: u32 = (1 << 10) - 1; + + pub fn toU32(this: Specifity) u32 { + return @as(u32, @as(u32, @min(this.id_selectors, MAX_10BIT)) << @as(u32, 20)) | + @as(u32, @as(u32, @min(this.class_like_selectors, MAX_10BIT)) << @as(u32, 10)) | + @min(this.element_selectors, MAX_10BIT); + } + + pub fn fromU32(value: u32) Specifity { + bun.assert(value <= MAX_10BIT << 20 | MAX_10BIT << 10 | MAX_10BIT); + return Specifity{ + .id_selectors = value >> 20, + .class_like_selectors = (value >> 10) & MAX_10BIT, + .element_selectors = value & MAX_10BIT, + }; + } + + pub fn add(lhs: *Specifity, rhs: Specifity) void { + lhs.id_selectors += rhs.id_selectors; + lhs.element_selectors += rhs.element_selectors; + lhs.class_like_selectors += rhs.class_like_selectors; + } +}; + +pub fn compute_specifity(comptime Impl: type, iter: []const GenericComponent(Impl)) u32 { + const spec = compute_complex_selector_specifity(Impl, iter); + return spec.toU32(); +} + +fn compute_complex_selector_specifity(comptime Impl: type, iter: []const GenericComponent(Impl)) Specifity { + var specifity: Specifity = .{}; + + for (iter) |*simple_selector| { + compute_simple_selector_specifity(Impl, simple_selector, &specifity); + } + + return specifity; +} + +fn compute_simple_selector_specifity( + comptime Impl: type, + simple_selector: *const GenericComponent(Impl), + specifity: *Specifity, +) void { + switch (simple_selector.*) { + .combinator => { + bun.unreachablePanic("Found combinator in simple selectors vector?", .{}); + }, + .part, .pseudo_element, .local_name => { + specifity.element_selectors += 1; + }, + .slotted => |selector| { + specifity.element_selectors += 1; + // Note that due to the way ::slotted works we only compete with + // other ::slotted rules, so the above rule doesn't really + // matter, but we do it still for consistency with other + // pseudo-elements. + // + // See: https://github.com/w3c/csswg-drafts/issues/1915 + specifity.add(Specifity.fromU32(selector.specifity())); + }, + .host => |maybe_selector| { + specifity.class_like_selectors += 1; + if (maybe_selector) |*selector| { + // See: https://github.com/w3c/csswg-drafts/issues/1915 + specifity.add(Specifity.fromU32(selector.specifity())); + } + }, + .id => { + specifity.id_selectors += 1; + }, + .class, + .attribute_in_no_namespace, + .attribute_in_no_namespace_exists, + .attribute_other, + .root, + .empty, + .scope, + .nth, + .non_ts_pseudo_class, + => { + specifity.class_like_selectors += 1; + }, + .nth_of => |nth_of_data| { + // https://drafts.csswg.org/selectors/#specificity-rules: + // + // The specificity of the :nth-last-child() pseudo-class, + // like the :nth-child() pseudo-class, combines the + // specificity of a regular pseudo-class with that of its + // selector argument S. + specifity.class_like_selectors += 1; + var max: u32 = 0; + for (nth_of_data.selectors) |*selector| { + max = @max(selector.specifity(), max); + } + specifity.add(Specifity.fromU32(max)); + }, + .negation, .is, .any => { + // https://drafts.csswg.org/selectors/#specificity-rules: + // + // The specificity of an :is() pseudo-class is replaced by the + // specificity of the most specific complex selector in its + // selector list argument. + const list: []GenericSelector(Impl) = switch (simple_selector.*) { + .negation => |list| list, + .is => |list| list, + .any => |a| a.selectors, + else => unreachable, + }; + var max: u32 = 0; + for (list) |*selector| { + max = @max(selector.specifity(), max); + } + specifity.add(Specifity.fromU32(max)); + }, + .where, + .has, + .explicit_universal_type, + .explicit_any_namespace, + .explicit_no_namespace, + .default_namespace, + .namespace, + => { + // Does not affect specifity + }, + .nesting => { + // TODO + }, + } +} + +const SelectorBuilder = selector_builder.SelectorBuilder; + +/// Build up a Selector. +/// selector : simple_selector_sequence [ combinator simple_selector_sequence ]* ; +/// +/// `Err` means invalid selector. +fn parse_selector( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + nesting_requirement: NestingRequirement, +) Result(GenericSelector(Impl)) { + if (nesting_requirement == .prefixed) { + const parser_state = input.state(); + if (!input.expectDelim('&').isOk()) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.missing_nesting_prefix)) }; + } + input.reset(&parser_state); + } + + // PERF: allocations here + var builder = selector_builder.SelectorBuilder(Impl){ + .allocator = input.allocator(), + }; + + outer_loop: while (true) { + // Parse a sequence of simple selectors. + const empty = switch (parse_compound_selector(Impl, parser, state, input, &builder)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (empty) { + const kind: SelectorParseErrorKind = if (builder.hasCombinators()) + .dangling_combinator + else + .empty_selector; + + return .{ .err = input.newCustomError(kind.intoDefaultParserError()) }; + } + + if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { + break; + } + + // Parse a combinator + var combinator: Combinator = undefined; + var any_whitespace = false; + while (true) { + const before_this_token = input.state(); + const tok: *css.Token = switch (input.nextIncludingWhitespace()) { + .result => |vv| vv, + .err => break :outer_loop, + }; + switch (tok.*) { + .whitespace => { + any_whitespace = true; + continue; + }, + .delim => |d| { + switch (d) { + '>' => { + if (parser.deepCombinatorEnabled() and input.tryParse(struct { + pub fn parseFn(i: *css.Parser) Result(void) { + if (i.expectDelim('>').asErr()) |e| return .{ .err = e }; + return i.expectDelim('>'); + } + }.parseFn, .{}).isOk()) { + combinator = Combinator.deep_descendant; + } else { + combinator = Combinator.child; + } + break; + }, + '+' => { + combinator = .next_sibling; + break; + }, + '~' => { + combinator = .later_sibling; + break; + }, + '/' => { + if (parser.deepCombinatorEnabled()) { + if (input.tryParse(struct { + pub fn parseFn(i: *css.Parser) Result(void) { + if (i.expectIdentMatching("deep").asErr()) |e| return .{ .err = e }; + return i.expectDelim('/'); + } + }.parseFn, .{}).isOk()) { + combinator = .deep; + break; + } else { + break :outer_loop; + } + } + }, + else => {}, + } + }, + else => {}, + } + + input.reset(&before_this_token); + + if (any_whitespace) { + combinator = .descendant; + break; + } else { + break :outer_loop; + } + } + + if (!state.allowsCombinators()) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + + builder.pushCombinator(combinator); + } + + if (!state.contains(SelectorParsingState{ .after_nesting = true })) { + switch (nesting_requirement) { + .implicit => { + builder.addNestingPrefix(); + }, + .contained, .prefixed => { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.missing_nesting_selector)) }; + }, + else => {}, + } + } + + const has_pseudo_element = state.intersects(SelectorParsingState{ + .after_pseudo_element = true, + .after_unknown_pseudo_element = true, + }); + const slotted = state.intersects(SelectorParsingState{ .after_slotted = true }); + const part = state.intersects(SelectorParsingState{ .after_part = true }); + const result = builder.build(has_pseudo_element, slotted, part); + return .{ .result = Selector{ + .specifity_and_flags = result.specifity_and_flags, + .components = result.components, + } }; +} + +/// simple_selector_sequence +/// : [ type_selector | universal ] [ HASH | class | attrib | pseudo | negation ]* +/// | [ HASH | class | attrib | pseudo | negation ]+ +/// +/// `Err(())` means invalid selector. +/// `Ok(true)` is an empty selector +fn parse_compound_selector( + comptime Impl: type, + parser: *SelectorParser, + state: *SelectorParsingState, + input: *css.Parser, + builder: *SelectorBuilder(Impl), +) Result(bool) { + input.skipWhitespace(); + + var empty: bool = true; + if (parser.isNestingAllowed() and if (input.tryParse(css.Parser.expectDelim, .{'&'}).isOk()) true else false) { + state.insert(SelectorParsingState{ .after_nesting = true }); + builder.pushSimpleSelector(.nesting); + empty = false; + } + + if (parse_type_selector(Impl, parser, input, state.*, builder).asValue()) |_| { + empty = false; + } + + while (true) { + const result: SimpleSelectorParseResult(Impl) = result: { + const ret = switch (parse_one_simple_selector(Impl, parser, input, state)) { + .result => |r| r, + .err => |e| return .{ .err = e }, + }; + if (ret) |result| { + break :result result; + } + break; + }; + + if (empty) { + if (parser.defaultNamespace()) |url| { + // If there was no explicit type selector, but there is a + // default namespace, there is an implicit "|*" type + // selector. Except for :host() or :not() / :is() / :where(), + // where we ignore it. + // + // https://drafts.csswg.org/css-scoping/#host-element-in-tree: + // + // When considered within its own shadow trees, the shadow + // host is featureless. Only the :host, :host(), and + // :host-context() pseudo-classes are allowed to match it. + // + // https://drafts.csswg.org/selectors-4/#featureless: + // + // A featureless element does not match any selector at all, + // except those it is explicitly defined to match. If a + // given selector is allowed to match a featureless element, + // it must do so while ignoring the default namespace. + // + // https://drafts.csswg.org/selectors-4/#matches + // + // Default namespace declarations do not affect the compound + // selector representing the subject of any selector within + // a :is() pseudo-class, unless that compound selector + // contains an explicit universal selector or type selector. + // + // (Similar quotes for :where() / :not()) + // + const ignore_default_ns = state.intersects(SelectorParsingState{ .skip_default_namespace = true }) or + (result == .simple_selector and result.simple_selector == .host); + if (!ignore_default_ns) { + builder.pushSimpleSelector(.{ .default_namespace = url }); + } + } + } + + empty = false; + + switch (result) { + .simple_selector => { + builder.pushSimpleSelector(result.simple_selector); + }, + .part_pseudo => { + const selector = result.part_pseudo; + state.insert(SelectorParsingState{ .after_part = true }); + builder.pushCombinator(.part); + builder.pushSimpleSelector(.{ .part = selector }); + }, + .slotted_pseudo => |selector| { + state.insert(.{ .after_slotted = true }); + builder.pushCombinator(.slot_assignment); + builder.pushSimpleSelector(.{ .slotted = selector }); + }, + .pseudo_element => |p| { + if (!p.isUnknown()) { + state.insert(SelectorParsingState{ .after_pseudo_element = true }); + builder.pushCombinator(.pseudo_element); + } else { + state.insert(.{ .after_unknown_pseudo_element = true }); + } + + if (!p.acceptsStatePseudoClasses()) { + state.insert(.{ .after_non_stateful_pseudo_element = true }); + } + + if (p.isWebkitScrollbar()) { + state.insert(.{ .after_webkit_scrollbar = true }); + } + + if (p.isViewTransition()) { + state.insert(.{ .after_view_transition = true }); + } + + builder.pushSimpleSelector(.{ .pseudo_element = p }); + }, + } + } + + return .{ .result = empty }; +} + +fn parse_relative_selector( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + nesting_requirement_: NestingRequirement, +) Result(GenericSelector(Impl)) { + // https://www.w3.org/TR/selectors-4/#parse-relative-selector + var nesting_requirement = nesting_requirement_; + const s = input.state(); + + const combinator: ?Combinator = combinator: { + const tok = switch (input.next()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (tok.*) { + .delim => |c| { + switch (c) { + '>' => break :combinator Combinator.child, + '+' => break :combinator Combinator.next_sibling, + '~' => break :combinator Combinator.later_sibling, + else => {}, + } + }, + else => {}, + } + input.reset(&s); + break :combinator null; + }; + + const scope: GenericComponent(Impl) = if (nesting_requirement == .implicit) .nesting else .scope; + + if (combinator != null) { + nesting_requirement = .none; + } + + var selector = switch (parse_selector(Impl, parser, input, state, nesting_requirement)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (combinator) |wombo_combo| { + // https://www.w3.org/TR/selectors/#absolutizing + selector.components.append( + parser.allocator, + .{ .combinator = wombo_combo }, + ) catch unreachable; + selector.components.append( + parser.allocator, + scope, + ) catch unreachable; + } + + return .{ .result = selector }; +} + +pub fn ValidSelectorParser(comptime T: type) type { + ValidSelectorImpl(T.SelectorParser.Impl); + + // Whether to parse the `::slotted()` pseudo-element. + _ = T.SelectorParser.parseSlotted; + + _ = T.SelectorParser.parsePart; + + _ = T.SelectorParser.parseIsAndWhere; + + _ = T.SelectorParser.isAndWhereErrorRecovery; + + _ = T.SelectorParser.parseAnyPrefix; + + _ = T.SelectorParser.parseHost; + + _ = T.SelectorParser.parseNonTsPseudoClass; + + _ = T.SelectorParser.parseNonTsFunctionalPseudoClass; + + _ = T.SelectorParser.parsePseudoElement; + + _ = T.SelectorParser.parseFunctionalPseudoElement; + + _ = T.SelectorParser.defaultNamespace; + + _ = T.SelectorParser.namespaceForPrefix; + + _ = T.SelectorParser.isNestingAllowed; + + _ = T.SelectorParser.deepCombinatorEnabled; +} + +/// The [:dir()](https://drafts.csswg.org/selectors-4/#the-dir-pseudo) pseudo class. +pub const Direction = enum { + /// Left to right + ltr, + /// Right to left + rtl, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return css.enum_property_util.toCss(@This(), this, W, dest); + } +}; + +/// A pseudo class. +pub const PseudoClass = union(enum) { + /// https://drafts.csswg.org/selectors-4/#linguistic-pseudos + /// The [:lang()](https://drafts.csswg.org/selectors-4/#the-lang-pseudo) pseudo class. + lang: struct { + /// A list of language codes. + languages: ArrayList([]const u8), + }, + /// The [:dir()](https://drafts.csswg.org/selectors-4/#the-dir-pseudo) pseudo class. + dir: struct { + /// A direction. + direction: Direction, + }, + + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + /// The [:hover](https://drafts.csswg.org/selectors-4/#the-hover-pseudo) pseudo class. + hover, + /// The [:active](https://drafts.csswg.org/selectors-4/#the-active-pseudo) pseudo class. + active, + /// The [:focus](https://drafts.csswg.org/selectors-4/#the-focus-pseudo) pseudo class. + focus, + /// The [:focus-visible](https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo) pseudo class. + focus_visible, + /// The [:focus-within](https://drafts.csswg.org/selectors-4/#the-focus-within-pseudo) pseudo class. + focus_within, + + /// https://drafts.csswg.org/selectors-4/#time-pseudos + /// The [:current](https://drafts.csswg.org/selectors-4/#the-current-pseudo) pseudo class. + current, + /// The [:past](https://drafts.csswg.org/selectors-4/#the-past-pseudo) pseudo class. + past, + /// The [:future](https://drafts.csswg.org/selectors-4/#the-future-pseudo) pseudo class. + future, + + /// https://drafts.csswg.org/selectors-4/#resource-pseudos + /// The [:playing](https://drafts.csswg.org/selectors-4/#selectordef-playing) pseudo class. + playing, + /// The [:paused](https://drafts.csswg.org/selectors-4/#selectordef-paused) pseudo class. + paused, + /// The [:seeking](https://drafts.csswg.org/selectors-4/#selectordef-seeking) pseudo class. + seeking, + /// The [:buffering](https://drafts.csswg.org/selectors-4/#selectordef-buffering) pseudo class. + buffering, + /// The [:stalled](https://drafts.csswg.org/selectors-4/#selectordef-stalled) pseudo class. + stalled, + /// The [:muted](https://drafts.csswg.org/selectors-4/#selectordef-muted) pseudo class. + muted, + /// The [:volume-locked](https://drafts.csswg.org/selectors-4/#selectordef-volume-locked) pseudo class. + volume_locked, + + /// The [:fullscreen](https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class) pseudo class. + fullscreen: css.VendorPrefix, + + /// https://drafts.csswg.org/selectors/#display-state-pseudos + /// The [:open](https://drafts.csswg.org/selectors/#selectordef-open) pseudo class. + open, + /// The [:closed](https://drafts.csswg.org/selectors/#selectordef-closed) pseudo class. + closed, + /// The [:modal](https://drafts.csswg.org/selectors/#modal-state) pseudo class. + modal, + /// The [:picture-in-picture](https://drafts.csswg.org/selectors/#pip-state) pseudo class. + picture_in_picture, + + /// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open + /// The [:popover-open](https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open) pseudo class. + popover_open, + + /// The [:defined](https://drafts.csswg.org/selectors-4/#the-defined-pseudo) pseudo class. + defined, + + /// https://drafts.csswg.org/selectors-4/#location + /// The [:any-link](https://drafts.csswg.org/selectors-4/#the-any-link-pseudo) pseudo class. + any_link: css.VendorPrefix, + /// The [:link](https://drafts.csswg.org/selectors-4/#link-pseudo) pseudo class. + link, + /// The [:local-link](https://drafts.csswg.org/selectors-4/#the-local-link-pseudo) pseudo class. + local_link, + /// The [:target](https://drafts.csswg.org/selectors-4/#the-target-pseudo) pseudo class. + target, + /// The [:target-within](https://drafts.csswg.org/selectors-4/#the-target-within-pseudo) pseudo class. + target_within, + /// The [:visited](https://drafts.csswg.org/selectors-4/#visited-pseudo) pseudo class. + visited, + + /// https://drafts.csswg.org/selectors-4/#input-pseudos + /// The [:enabled](https://drafts.csswg.org/selectors-4/#enabled-pseudo) pseudo class. + enabled, + /// The [:disabled](https://drafts.csswg.org/selectors-4/#disabled-pseudo) pseudo class. + disabled, + /// The [:read-only](https://drafts.csswg.org/selectors-4/#read-only-pseudo) pseudo class. + read_only: css.VendorPrefix, + /// The [:read-write](https://drafts.csswg.org/selectors-4/#read-write-pseudo) pseudo class. + read_write: css.VendorPrefix, + /// The [:placeholder-shown](https://drafts.csswg.org/selectors-4/#placeholder) pseudo class. + placeholder_shown: css.VendorPrefix, + /// The [:default](https://drafts.csswg.org/selectors-4/#the-default-pseudo) pseudo class. + default, + /// The [:checked](https://drafts.csswg.org/selectors-4/#checked) pseudo class. + checked, + /// The [:indeterminate](https://drafts.csswg.org/selectors-4/#indeterminate) pseudo class. + indeterminate, + /// The [:blank](https://drafts.csswg.org/selectors-4/#blank) pseudo class. + blank, + /// The [:valid](https://drafts.csswg.org/selectors-4/#valid-pseudo) pseudo class. + valid, + /// The [:invalid](https://drafts.csswg.org/selectors-4/#invalid-pseudo) pseudo class. + invalid, + /// The [:in-range](https://drafts.csswg.org/selectors-4/#in-range-pseudo) pseudo class. + in_range, + /// The [:out-of-range](https://drafts.csswg.org/selectors-4/#out-of-range-pseudo) pseudo class. + out_of_range, + /// The [:required](https://drafts.csswg.org/selectors-4/#required-pseudo) pseudo class. + required, + /// The [:optional](https://drafts.csswg.org/selectors-4/#optional-pseudo) pseudo class. + optional, + /// The [:user-valid](https://drafts.csswg.org/selectors-4/#user-valid-pseudo) pseudo class. + user_valid, + /// The [:used-invalid](https://drafts.csswg.org/selectors-4/#user-invalid-pseudo) pseudo class. + user_invalid, + + /// The [:autofill](https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill) pseudo class. + autofill: css.VendorPrefix, + + // CSS modules + /// The CSS modules :local() pseudo class. + local: struct { + /// A local selector. + selector: *Selector, + }, + /// The CSS modules :global() pseudo class. + global: struct { + /// A global selector. + selector: *Selector, + }, + + /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo class. + // https://webkit.org/blog/363/styling-scrollbars/ + webkit_scrollbar: WebKitScrollbarPseudoClass, + /// An unknown pseudo class. + custom: struct { + /// The pseudo class name. + name: []const u8, + }, + /// An unknown functional pseudo class. + custom_function: struct { + /// The pseudo class name. + name: []const u8, + /// The arguments of the pseudo class function. + arguments: css.TokenList, + }, + + pub fn toCss(this: *const PseudoClass, comptime W: type, dest: *Printer(W)) PrintErr!void { + var s = ArrayList(u8){}; + // PERF(alloc): I don't like making these little allocations + const writer = s.writer(dest.allocator); + const W2 = @TypeOf(writer); + const scratchbuf = std.ArrayList(u8).init(dest.allocator); + var printer = Printer(W2).new(dest.allocator, scratchbuf, writer, css.PrinterOptions{}); + try serialize.serializePseudoClass(this, W2, &printer, null); + return dest.writeStr(s.items); + } + + pub fn isUserActionState(this: *const PseudoClass) bool { + return switch (this.*) { + .active, .hover => true, + else => false, + }; + } + + pub fn isValidBeforeWebkitScrollbar(this: *const PseudoClass) bool { + return !switch (this.*) { + .webkit_scrollbar => true, + else => false, + }; + } + + pub fn isValidAfterWebkitScrollbar(this: *const PseudoClass) bool { + return switch (this.*) { + .webkit_scrollbar, .enabled, .disabled, .hover, .active => true, + else => false, + }; + } +}; + +/// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo class. +pub const WebKitScrollbarPseudoClass = enum { + /// :horizontal + horizontal, + /// :vertical + vertical, + /// :decrement + decrement, + /// :increment + increment, + /// :start + start, + /// :end + end, + /// :double-button + double_button, + /// :single-button + single_button, + /// :no-button + no_button, + /// :corner-present + corner_present, + /// :window-inactive + window_inactive, +}; + +/// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo element. +pub const WebKitScrollbarPseudoElement = enum { + /// ::-webkit-scrollbar + scrollbar, + /// ::-webkit-scrollbar-button + button, + /// ::-webkit-scrollbar-track + track, + /// ::-webkit-scrollbar-track-piece + track_piece, + /// ::-webkit-scrollbar-thumb + thumb, + /// ::-webkit-scrollbar-corner + corner, + /// ::-webkit-resizer + resizer, +}; + +pub const SelectorParser = struct { + is_nesting_allowed: bool, + options: *const css.ParserOptions, + allocator: Allocator, + + pub const Impl = impl.Selectors; + + pub fn namespaceForPrefix(this: *SelectorParser, prefix: css.css_values.ident.Ident) ?[]const u8 { + _ = this; // autofix + return prefix.v; + } + + pub fn parseFunctionalPseudoElement(this: *SelectorParser, name: []const u8, input: *css.Parser) Result(Impl.SelectorImpl.PseudoElement) { + _ = this; // autofix + _ = name; // autofix + _ = input; // autofix + @panic(css.todo_stuff.depth); + } + + fn parseIsAndWhere(this: *const SelectorParser) bool { + _ = this; // autofix + return true; + } + + /// Whether the given function name is an alias for the `:is()` function. + fn parseAnyPrefix(this: *const SelectorParser, name: []const u8) ?css.VendorPrefix { + _ = this; // autofix + _ = name; // autofix + return null; + } + + pub fn parseNonTsPseudoClass( + this: *SelectorParser, + loc: css.SourceLocation, + name: []const u8, + ) Result(PseudoClass) { + // @compileError(css.todo_stuff.match_ignore_ascii_case); + const pseudo_class: PseudoClass = pseudo_class: { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "hover")) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + break :pseudo_class .hover; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "active")) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + break :pseudo_class .active; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "focus")) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + break :pseudo_class .focus; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "focus-visible")) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + break :pseudo_class .focus_visible; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "focus-within")) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + break :pseudo_class .focus_within; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "current")) { + // https://drafts.csswg.org/selectors-4/#time-pseudos + break :pseudo_class .current; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "past")) { + // https://drafts.csswg.org/selectors-4/#time-pseudos + break :pseudo_class .past; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "future")) { + // https://drafts.csswg.org/selectors-4/#time-pseudos + break :pseudo_class .future; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "playing")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .playing; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "paused")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .paused; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "seeking")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .seeking; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "buffering")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .buffering; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "stalled")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .stalled; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "muted")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .muted; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "volume-locked")) { + // https://drafts.csswg.org/selectors-4/#resource-pseudos + break :pseudo_class .volume_locked; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "fullscreen")) { + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + break :pseudo_class .{ .fullscreen = css.VendorPrefix{ .none = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-full-screen")) { + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + break :pseudo_class .{ .fullscreen = css.VendorPrefix{ .webkit = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-full-screen")) { + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + break :pseudo_class .{ .fullscreen = css.VendorPrefix{ .moz = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-fullscreen")) { + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + break :pseudo_class .{ .fullscreen = css.VendorPrefix{ .ms = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "open")) { + // https://drafts.csswg.org/selectors/#display-state-pseudos + break :pseudo_class .open; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "closed")) { + // https://drafts.csswg.org/selectors/#display-state-pseudos + break :pseudo_class .closed; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "modal")) { + // https://drafts.csswg.org/selectors/#display-state-pseudos + break :pseudo_class .modal; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "picture-in-picture")) { + // https://drafts.csswg.org/selectors/#display-state-pseudos + break :pseudo_class .picture_in_picture; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "popover-open")) { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open + break :pseudo_class .popover_open; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "defined")) { + // https://drafts.csswg.org/selectors-4/#the-defined-pseudo + break :pseudo_class .defined; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "any-link")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .{ .any_link = css.VendorPrefix{ .none = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-any-link")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .{ .any_link = css.VendorPrefix{ .webkit = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-any-link")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .{ .any_link = css.VendorPrefix{ .moz = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "link")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .link; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "local-link")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .local_link; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "target")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .target; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "target-within")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .target_within; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "visited")) { + // https://drafts.csswg.org/selectors-4/#location + break :pseudo_class .visited; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "enabled")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .enabled; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "disabled")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .disabled; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "read-only")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .read_only = css.VendorPrefix{ .none = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-read-only")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .read_only = css.VendorPrefix{ .moz = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "read-write")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .read_write = css.VendorPrefix{ .none = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-read-write")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .read_write = css.VendorPrefix{ .moz = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "placeholder-shown")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .placeholder_shown = css.VendorPrefix{ .none = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-moz-placeholder-shown")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .placeholder_shown = css.VendorPrefix{ .moz = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-ms-placeholder-shown")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .{ .placeholder_shown = css.VendorPrefix{ .ms = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "default")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .default; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "checked")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .checked; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "indeterminate")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .indeterminate; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "blank")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .blank; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "valid")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .valid; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "invalid")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .invalid; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "in-range")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .in_range; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "out-of-range")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .out_of_range; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "required")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .required; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "optional")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .optional; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "user-valid")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .user_valid; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "user-invalid")) { + // https://drafts.csswg.org/selectors-4/#input-pseudos + break :pseudo_class .user_invalid; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "autofill")) { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + break :pseudo_class .{ .autofill = css.VendorPrefix{ .none = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-webkit-autofill")) { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + break :pseudo_class .{ .autofill = css.VendorPrefix{ .webkit = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "-o-autofill")) { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + break :pseudo_class .{ .autofill = css.VendorPrefix{ .o = true } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "horizontal")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .horizontal }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "vertical")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .vertical }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "decrement")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .decrement }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "increment")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .increment }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "start")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .start }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "end")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .end }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "double-button")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .double_button }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "single-button")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .single_button }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "no-button")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .no_button }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "corner-present")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .corner_present }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "window-inactive")) { + // https://webkit.org/blog/363/styling-scrollbars/ + break :pseudo_class .{ .webkit_scrollbar = .window_inactive }; + } else { + if (bun.strings.startsWithChar(name, '_')) { + this.options.warn(loc.newCustomError(SelectorParseErrorKind{ .unsupported_pseudo_class_or_element = name })); + } + return .{ .result = PseudoClass{ .custom = .{ .name = name } } }; + } + }; + + return .{ .result = pseudo_class }; + } + + pub fn parseNonTsFunctionalPseudoClass( + this: *SelectorParser, + name: []const u8, + parser: *css.Parser, + ) Result(PseudoClass) { + + // todo_stuff.match_ignore_ascii_case + const pseudo_class = pseudo_class: { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "lang")) { + const languages = switch (parser.parseCommaSeparated([]const u8, css.Parser.expectIdentOrString)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = PseudoClass{ + .lang = .{ .languages = languages }, + } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "dir")) { + break :pseudo_class PseudoClass{ + .dir = .{ + .direction = switch (Direction.parse(parser)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "local") and this.options.css_modules != null) { + break :pseudo_class PseudoClass{ + .local = .{ + .selector = brk: { + const selector = switch (Selector.parse(this, parser)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + break :brk bun.create(this.allocator, Selector, selector); + }, + }, + }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "global") and this.options.css_modules != null) { + break :pseudo_class PseudoClass{ + .global = .{ + .selector = brk: { + const selector = switch (Selector.parse(this, parser)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + break :brk bun.create(this.allocator, Selector, selector); + }, + }, + }; + } else { + if (!bun.strings.startsWithChar(name, '-')) { + this.options.warn(parser.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .unsupported_pseudo_class_or_element = name }))); + } + var args = ArrayList(css.css_properties.custom.TokenOrValue){}; + _ = switch (css.TokenListFns.parseRaw(parser, &args, this.options, 0)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + break :pseudo_class PseudoClass{ + .custom_function = .{ + .name = name, + .arguments = css.TokenList{ .v = args }, + }, + }; + } + }; + + return .{ .result = pseudo_class }; + } + + pub fn isNestingAllowed(this: *SelectorParser) bool { + return this.is_nesting_allowed; + } + + pub fn deepCombinatorEnabled(this: *SelectorParser) bool { + return this.options.flags.contains(css.ParserFlags{ .deep_selector_combinator = true }); + } + + pub fn defaultNamespace(this: *SelectorParser) ?impl.Selectors.SelectorImpl.NamespaceUrl { + _ = this; // autofix + return null; + } + + pub fn parsePart(this: *SelectorParser) bool { + _ = this; // autofix + return true; + } + + pub fn parseSlotted(this: *SelectorParser) bool { + _ = this; // autofix + return true; + } + + /// The error recovery that selector lists inside :is() and :where() have. + fn isAndWhereErrorRecovery(this: *SelectorParser) ParseErrorRecovery { + _ = this; // autofix + return .ignore_invalid_selector; + } + + pub fn parsePseudoElement(this: *SelectorParser, loc: css.SourceLocation, name: []const u8) Result(PseudoElement) { + const Map = comptime bun.ComptimeStringMap(PseudoElement, .{ + .{ "before", PseudoElement.before }, + .{ "after", PseudoElement.after }, + .{ "first-line", PseudoElement.first_line }, + .{ "first-letter", PseudoElement.first_letter }, + .{ "cue", PseudoElement.cue }, + .{ "cue-region", PseudoElement.cue_region }, + .{ "selection", PseudoElement{ .selection = css.VendorPrefix{ .none = true } } }, + .{ "-moz-selection", PseudoElement{ .selection = css.VendorPrefix{ .moz = true } } }, + .{ "placeholder", PseudoElement{ .placeholder = css.VendorPrefix{ .none = true } } }, + .{ "-webkit-input-placeholder", PseudoElement{ .placeholder = css.VendorPrefix{ .webkit = true } } }, + .{ "-moz-placeholder", PseudoElement{ .placeholder = css.VendorPrefix{ .moz = true } } }, + .{ "-ms-input-placeholder", PseudoElement{ .placeholder = css.VendorPrefix{ .ms = true } } }, + .{ "marker", PseudoElement.marker }, + .{ "backdrop", PseudoElement{ .backdrop = css.VendorPrefix{ .none = true } } }, + .{ "-webkit-backdrop", PseudoElement{ .backdrop = css.VendorPrefix{ .webkit = true } } }, + .{ "file-selector-button", PseudoElement{ .file_selector_button = css.VendorPrefix{ .none = true } } }, + .{ "-webkit-file-upload-button", PseudoElement{ .file_selector_button = css.VendorPrefix{ .webkit = true } } }, + .{ "-ms-browse", PseudoElement{ .file_selector_button = css.VendorPrefix{ .ms = true } } }, + .{ "-webkit-scrollbar", PseudoElement{ .webkit_scrollbar = .scrollbar } }, + .{ "-webkit-scrollbar-button", PseudoElement{ .webkit_scrollbar = .button } }, + .{ "-webkit-scrollbar-track", PseudoElement{ .webkit_scrollbar = .track } }, + .{ "-webkit-scrollbar-track-piece", PseudoElement{ .webkit_scrollbar = .track_piece } }, + .{ "-webkit-scrollbar-thumb", PseudoElement{ .webkit_scrollbar = .thumb } }, + .{ "-webkit-scrollbar-corner", PseudoElement{ .webkit_scrollbar = .corner } }, + .{ "-webkit-resizer", PseudoElement{ .webkit_scrollbar = .resizer } }, + .{ "view-transition", PseudoElement.view_transition }, + }); + + const pseudo_element = Map.getCaseInsensitiveWithEql(name, bun.strings.eqlComptimeIgnoreLen) orelse brk: { + if (!bun.strings.startsWithChar(name, '-')) { + this.options.warn(loc.newCustomError(SelectorParseErrorKind{ .unsupported_pseudo_class_or_element = name })); + } + break :brk PseudoElement{ .custom = .{ .name = name } }; + }; + + return .{ .result = pseudo_element }; + } +}; + +pub fn GenericSelectorList(comptime Impl: type) type { + ValidSelectorImpl(Impl); + + const SelectorT = GenericSelector(Impl); + return struct { + // PERF: make this equivalent to SmallVec<[Selector; 1]> + v: ArrayList(SelectorT) = .{}, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError("Do not call this! Use `serializer.serializeSelectorList()` or `tocss_servo.toCss_SelectorList()` instead."); + } + + pub fn parseWithOptions(input: *css.Parser, options: *const css.ParserOptions) Result(This) { + var parser = SelectorParser{ + .options = options, + .is_nesting_allowed = true, + }; + return parse(&parser, input, .discard_list, .none); + } + + pub fn parse( + parser: *SelectorParser, + input: *css.Parser, + error_recovery: ParseErrorRecovery, + nesting_requirement: NestingRequirement, + ) Result(This) { + var state = SelectorParsingState.empty(); + return parseWithState(parser, input, &state, error_recovery, nesting_requirement); + } + + pub fn parseRelative( + parser: *SelectorParser, + input: *css.Parser, + error_recovery: ParseErrorRecovery, + nesting_requirement: NestingRequirement, + ) Result(This) { + var state = SelectorParsingState.empty(); + return parseRelativeWithState(parser, input, &state, error_recovery, nesting_requirement); + } + + pub fn parseWithState( + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + recovery: ParseErrorRecovery, + nesting_requirement: NestingRequirement, + ) Result(This) { + const original_state = state.*; + // TODO: Think about deinitialization in error cases + var values = ArrayList(SelectorT){}; + + while (true) { + const Closure = struct { + outer_state: *SelectorParsingState, + original_state: SelectorParsingState, + nesting_requirement: NestingRequirement, + parser: *SelectorParser, + + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(SelectorT) { + var selector_state = this.original_state; + const result = parse_selector(Impl, this.parser, input2, &selector_state, this.nesting_requirement); + if (selector_state.after_nesting) { + this.outer_state.after_nesting = true; + } + return result; + } + }; + var closure = Closure{ + .outer_state = state, + .original_state = original_state, + .nesting_requirement = nesting_requirement, + .parser = parser, + }; + const selector = input.parseUntilBefore(css.Delimiters{ .comma = true }, SelectorT, &closure, Closure.parsefn); + + const was_ok = selector.isOk(); + switch (selector) { + .result => |sel| { + values.append(input.allocator(), sel) catch bun.outOfMemory(); + }, + .err => |e| { + switch (recovery) { + .discard_list => return .{ .err = e }, + .ignore_invalid_selector => {}, + } + }, + } + + while (true) { + if (input.next().asValue()) |tok| { + if (tok.* == .comma) break; + // Shouldn't have got a selector if getting here. + bun.debugAssert(!was_ok); + } + return .{ .result = .{ .v = values } }; + } + } + } + + // TODO: this looks exactly the same as `parseWithState()` except it uses `parse_relative_selector()` instead of `parse_selector()` + pub fn parseRelativeWithState( + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + recovery: ParseErrorRecovery, + nesting_requirement: NestingRequirement, + ) Result(This) { + const original_state = state.*; + // TODO: Think about deinitialization in error cases + var values = ArrayList(SelectorT){}; + + while (true) { + const Closure = struct { + outer_state: *SelectorParsingState, + original_state: SelectorParsingState, + nesting_requirement: NestingRequirement, + parser: *SelectorParser, + + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(SelectorT) { + var selector_state = this.original_state; + const result = parse_relative_selector(Impl, this.parser, input2, &selector_state, this.nesting_requirement); + if (selector_state.after_nesting) { + this.outer_state.after_nesting = true; + } + return result; + } + }; + var closure = Closure{ + .outer_state = state, + .original_state = original_state, + .nesting_requirement = nesting_requirement, + .parser = parser, + }; + const selector = input.parseUntilBefore(css.Delimiters{ .comma = true }, SelectorT, &closure, Closure.parsefn); + + const was_ok = selector.isOk(); + switch (selector) { + .result => |sel| { + values.append(input.allocator(), sel) catch bun.outOfMemory(); + }, + .err => |e| { + switch (recovery) { + .discard_list => return .{ .err = e }, + .ignore_invalid_selector => {}, + } + }, + } + + while (true) { + if (input.next().asValue()) |tok| { + if (tok.* == .comma) break; + // Shouldn't have got a selector if getting here. + bun.debugAssert(!was_ok); + } + return .{ .result = .{ .v = values } }; + } + } + } + + pub fn fromSelector(allocator: Allocator, selector: GenericSelector(Impl)) This { + var result = This{}; + result.v.append(allocator, selector) catch unreachable; + return result; + } + }; +} + +/// -- original comment from servo -- +/// A Selector stores a sequence of simple selectors and combinators. The +/// iterator classes allow callers to iterate at either the raw sequence level or +/// at the level of sequences of simple selectors separated by combinators. Most +/// callers want the higher-level iterator. +/// +/// We store compound selectors internally right-to-left (in matching order). +/// Additionally, we invert the order of top-level compound selectors so that +/// each one matches left-to-right. This is because matching namespace, local name, +/// id, and class are all relatively cheap, whereas matching pseudo-classes might +/// be expensive (depending on the pseudo-class). Since authors tend to put the +/// pseudo-classes on the right, it's faster to start matching on the left. +/// +/// This reordering doesn't change the semantics of selector matching, and we +/// handle it in to_css to make it invisible to serialization. +pub fn GenericSelector(comptime Impl: type) type { + ValidSelectorImpl(Impl); + + return struct { + specifity_and_flags: SpecifityAndFlags, + components: ArrayList(GenericComponent(Impl)), + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError("Do not call this! Use `serializer.serializeSelector()` or `tocss_servo.toCss_Selector()` instead."); + } + + /// Returns count of simple selectors and combinators in the Selector. + pub fn len(this: *const This) usize { + return this.components.items.len; + } + + pub fn fromComponent(allocator: Allocator, component: GenericComponent(Impl)) This { + var builder = SelectorBuilder(Impl).init(allocator); + if (component.asCombinator()) |combinator| { + builder.pushCombinator(combinator); + } else { + builder.pushSimpleSelector(component); + } + const result = builder.build(false, false, false); + return This{ + .specifity_and_flags = result.specifity_and_flags, + .components = result.components, + }; + } + + pub fn specifity(this: *const This) u32 { + return this.specifity_and_flags.specificity; + } + + /// Parse a selector, without any pseudo-element. + pub fn parse(parser: *SelectorParser, input: *css.Parser) Result(This) { + var state = SelectorParsingState.empty(); + return parse_selector(Impl, parser, input, &state, .none); + } + + pub fn parseWithOptions(input: *css.Parser, options: *const css.ParserOptions) Result(This) { + var selector_parser = SelectorParser{ + .is_nesting_allowed = true, + .options = options, + }; + return parse(&selector_parser, input); + } + + /// Returns an iterator over the sequence of simple selectors and + /// combinators, in parse order (from left to right), starting from + /// `offset`. + pub fn iterRawParseOrderFrom(this: *const This, offset: usize) RawParseOrderFromIter { + return RawParseOrderFromIter{ + .slice = this.components.items[0 .. this.components.items.len - offset], + }; + } + + const RawParseOrderFromIter = struct { + slice: []const GenericComponent(Impl), + i: usize = 0, + + pub fn next(this: *@This()) ?GenericComponent(Impl) { + if (!(this.i < this.slice.len)) return null; + const result = this.slice[this.slice.len - 1 - this.i]; + this.i += 1; + return result; + } + }; + }; +} + +/// A CSS simple selector or combinator. We store both in the same enum for +/// optimal packing and cache performance, see [1]. +/// +/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1357973 +pub fn GenericComponent(comptime Impl: type) type { + ValidSelectorImpl(Impl); + + return union(enum) { + combinator: Combinator, + + explicit_any_namespace, + explicit_no_namespace, + default_namespace: Impl.SelectorImpl.NamespaceUrl, + namespace: struct { + prefix: Impl.SelectorImpl.NamespacePrefix, + url: Impl.SelectorImpl.NamespaceUrl, + }, + + explicit_universal_type, + local_name: LocalName(Impl), + + id: Impl.SelectorImpl.Identifier, + class: Impl.SelectorImpl.Identifier, + + attribute_in_no_namespace_exists: struct { + local_name: Impl.SelectorImpl.LocalName, + local_name_lower: Impl.SelectorImpl.LocalName, + }, + /// Used only when local_name is already lowercase. + attribute_in_no_namespace: struct { + local_name: Impl.SelectorImpl.LocalName, + operator: attrs.AttrSelectorOperator, + value: Impl.SelectorImpl.AttrValue, + case_sensitivity: attrs.ParsedCaseSensitivity, + never_matches: bool, + }, + /// Use a Box in the less common cases with more data to keep size_of::() small. + attribute_other: *attrs.AttrSelectorWithOptionalNamespace(Impl), + + /// Pseudo-classes + negation: []GenericSelector(Impl), + root, + empty, + scope, + nth: NthSelectorData, + nth_of: NthOfSelectorData(Impl), + non_ts_pseudo_class: Impl.SelectorImpl.NonTSPseudoClass, + /// The ::slotted() pseudo-element: + /// + /// https://drafts.csswg.org/css-scoping/#slotted-pseudo + /// + /// The selector here is a compound selector, that is, no combinators. + /// + /// NOTE(emilio): This should support a list of selectors, but as of this + /// writing no other browser does, and that allows them to put ::slotted() + /// in the rule hash, so we do that too. + /// + /// See https://github.com/w3c/csswg-drafts/issues/2158 + slotted: GenericSelector(Impl), + /// The `::part` pseudo-element. + /// https://drafts.csswg.org/css-shadow-parts/#part + part: []Impl.SelectorImpl.Identifier, + /// The `:host` pseudo-class: + /// + /// https://drafts.csswg.org/css-scoping/#host-selector + /// + /// NOTE(emilio): This should support a list of selectors, but as of this + /// writing no other browser does, and that allows them to put :host() + /// in the rule hash, so we do that too. + /// + /// See https://github.com/w3c/csswg-drafts/issues/2158 + host: ?GenericSelector(Impl), + /// The `:where` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#zero-matches + /// + /// The inner argument is conceptually a SelectorList, but we move the + /// selectors to the heap to keep Component small. + where: []GenericSelector(Impl), + /// The `:is` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#matches-pseudo + /// + /// Same comment as above re. the argument. + is: []GenericSelector(Impl), + any: struct { + vendor_prefix: Impl.SelectorImpl.VendorPrefix, + selectors: []GenericSelector(Impl), + }, + /// The `:has` pseudo-class. + /// + /// https://www.w3.org/TR/selectors/#relational + has: []GenericSelector(Impl), + /// An implementation-dependent pseudo-element selector. + pseudo_element: Impl.SelectorImpl.PseudoElement, + /// A nesting selector: + /// + /// https://drafts.csswg.org/css-nesting-1/#nest-selector + /// + /// NOTE: This is a lightningcss addition. + nesting, + + const This = @This(); + + pub fn 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}), + else => {}, + } + return writer.print("{s}", .{@tagName(this.*)}); + } + + pub fn asCombinator(this: *const This) ?Combinator { + if (this.* == .combinator) return this.combinator; + return null; + } + + pub fn convertHelper_is(s: []GenericSelector(Impl)) This { + return .{ .is = s }; + } + + pub fn convertHelper_where(s: []GenericSelector(Impl)) This { + return .{ .where = s }; + } + + pub fn convertHelper_any(s: []GenericSelector(Impl), prefix: Impl.SelectorImpl.VendorPrefix) This { + return .{ + .any = .{ + .vendor_prefix = prefix, + .selectors = s, + }, + }; + } + + /// Returns true if this is a combinator. + pub fn isCombinator(this: *const This) bool { + return this.* == .combinator; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError("Do not call this! Use `serializer.serializeComponent()` or `tocss_servo.toCss_Component()` instead."); + } + }; +} + +/// The properties that comprise an :nth- pseudoclass as of Selectors 3 (e.g., +/// nth-child(An+B)). +/// https://www.w3.org/TR/selectors-3/#nth-child-pseudo +pub const NthSelectorData = struct { + ty: NthType, + is_function: bool, + a: i32, + b: i32, + + /// Returns selector data for :only-{child,of-type} + pub fn only(of_type: bool) NthSelectorData { + return NthSelectorData{ + .ty = if (of_type) NthType.only_of_type else NthType.only_child, + .is_function = false, + .a = 0, + .b = 1, + }; + } + + /// Returns selector data for :first-{child,of-type} + pub fn first(of_type: bool) NthSelectorData { + return NthSelectorData{ + .ty = if (of_type) NthType.of_type else NthType.child, + .is_function = false, + .a = 0, + .b = 1, + }; + } + + /// Returns selector data for :last-{child,of-type} + pub fn last(of_type: bool) NthSelectorData { + return NthSelectorData{ + .ty = if (of_type) NthType.last_of_type else NthType.last_child, + .is_function = false, + .a = 0, + .b = 1, + }; + } + + pub fn writeStart(this: *const @This(), comptime W: type, dest: *Printer(W), is_function: bool) PrintErr!void { + try dest.writeStr(switch (this.ty) { + .child => if (is_function) ":nth-child(" else ":first-child", + .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_of_type => ":only-of-type", + .col => ":nth-col(", + .last_col => ":nth-last-col(", + }); + } + + pub fn isFunction(this: *const @This()) bool { + return this.a != 0 or this.b != 1; + } + + fn numberSign(num: i32) []const u8 { + if (num >= 0) return "+"; + return "-"; + } + + pub fn writeAffine(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + // PERF: this could be made faster + if (this.a == 0 and this.b == 0) { + try dest.writeChar('0'); + } else if (this.a == 1 and this.b == 0) { + try dest.writeChar('n'); + } else if (this.a == -1 and this.b == 0) { + try dest.writeStr("-n"); + } else if (this.b == 0) { + try dest.writeFmt("{d}n", .{this.a}); + } else if (this.a == 2 and this.b == 1) { + try dest.writeStr("odd"); + } else if (this.a == 0) { + try dest.writeFmt("{d}", .{this.b}); + } else if (this.a == 1) { + try dest.writeFmt("n{s}{d}", .{ numberSign(this.b), this.b }); + } else if (this.a == -1) { + try dest.writeFmt("-n{s}{d}", .{ numberSign(this.b), this.b }); + } else { + try dest.writeFmt("{}n{s}{d}", .{ this.a, numberSign(this.b), this.b }); + } + } +}; + +/// The properties that comprise an :nth- pseudoclass as of Selectors 4 (e.g., +/// nth-child(An+B [of S]?)). +/// https://www.w3.org/TR/selectors-4/#nth-child-pseudo +pub fn NthOfSelectorData(comptime Impl: type) type { + return struct { + data: NthSelectorData, + selectors: []GenericSelector(Impl), + + pub fn nthData(this: *const @This()) NthSelectorData { + return this.data; + } + + pub fn selectors(this: *const @This()) []GenericSelector(Impl) { + return this.selectors; + } + }; +} + +pub const SelectorParsingState = packed struct(u16) { + /// Whether we should avoid adding default namespaces to selectors that + /// aren't type or universal selectors. + skip_default_namespace: bool = false, + + /// Whether we've parsed a ::slotted() pseudo-element already. + /// + /// If so, then we can only parse a subset of pseudo-elements, and + /// whatever comes after them if so. + after_slotted: bool = false, + + /// Whether we've parsed a ::part() pseudo-element already. + /// + /// If so, then we can only parse a subset of pseudo-elements, and + /// whatever comes after them if so. + after_part: bool = false, + + /// Whether we've parsed a pseudo-element (as in, an + /// `Impl::PseudoElement` thus not accounting for `::slotted` or + /// `::part`) already. + /// + /// If so, then other pseudo-elements and most other selectors are + /// disallowed. + after_pseudo_element: bool = false, + + /// Whether we've parsed a non-stateful pseudo-element (again, as-in + /// `Impl::PseudoElement`) already. If so, then other pseudo-classes are + /// disallowed. If this flag is set, `AFTER_PSEUDO_ELEMENT` must be set + /// as well. + after_non_stateful_pseudo_element: bool = false, + + /// Whether we explicitly disallow combinators. + disallow_combinators: bool = false, + + /// Whether we explicitly disallow pseudo-element-like things. + disallow_pseudos: bool = false, + + /// Whether we have seen a nesting selector. + after_nesting: bool = false, + + after_webkit_scrollbar: bool = false, + after_view_transition: bool = false, + after_unknown_pseudo_element: bool = false, + __unused: u5 = 0, + + /// Whether we are after any of the pseudo-like things. + pub const AFTER_PSEUDO = SelectorParsingState{ .after_part = true, .after_slotted = true, .after_pseudo_element = true }; + + pub usingnamespace css.Bitflags(@This()); + + pub fn allowsPseudos(this: SelectorParsingState) bool { + return !this.intersects(SelectorParsingState{ + .after_pseudo_element = true, + .disallow_pseudos = true, + }); + } + + pub fn allowsPart(this: SelectorParsingState) bool { + return !this.intersects(SelectorParsingState.AFTER_PSEUDO.bitwiseOr(SelectorParsingState{ .disallow_pseudos = true })); + } + + pub fn allowsSlotted(this: SelectorParsingState) bool { + return !this.intersects(SelectorParsingState.AFTER_PSEUDO.bitwiseOr(.{ .disallow_pseudos = true })); + } + + pub fn allowsTreeStructuralPseudoClasses(this: SelectorParsingState) bool { + return !this.intersects(SelectorParsingState.AFTER_PSEUDO); + } + + pub fn allowsNonFunctionalPseudoClasses(this: SelectorParsingState) bool { + return !this.intersects(SelectorParsingState{ .after_slotted = true, .after_non_stateful_pseudo_element = true }); + } + + pub fn allowsCombinators(this: SelectorParsingState) bool { + return !this.intersects(SelectorParsingState{ .disallow_combinators = true }); + } + + pub fn allowsCustomFunctionalPseudoClasses(this: SelectorParsingState) bool { + return !this.intersects(SelectorParsingState.AFTER_PSEUDO); + } +}; + +pub const SpecifityAndFlags = struct { + /// There are two free bits here, since we use ten bits for each specificity + /// kind (id, class, element). + specificity: u32, + /// There's padding after this field due to the size of the flags. + flags: SelectorFlags, +}; + +pub const SelectorFlags = packed struct(u8) { + has_pseudo: bool = false, + has_slotted: bool = false, + has_part: bool = false, + __unused: u5 = 0, + + pub usingnamespace css.Bitflags(@This()); +}; + +/// How to treat invalid selectors in a selector list. +pub const ParseErrorRecovery = enum { + /// Discard the entire selector list, this is the default behavior for + /// almost all of CSS. + discard_list, + /// Ignore invalid selectors, potentially creating an empty selector list. + /// + /// This is the error recovery mode of :is() and :where() + ignore_invalid_selector, +}; + +pub const NestingRequirement = enum { + none, + prefixed, + contained, + implicit, +}; + +pub const Combinator = enum { + child, // > + descendant, // space + next_sibling, // + + later_sibling, // ~ + /// A dummy combinator we use to the left of pseudo-elements. + /// + /// It serializes as the empty string, and acts effectively as a child + /// combinator in most cases. If we ever actually start using a child + /// combinator for this, we will need to fix up the way hashes are computed + /// for revalidation selectors. + pseudo_element, + /// Another combinator used for ::slotted(), which represent the jump from + /// a node to its assigned slot. + slot_assignment, + + /// Another combinator used for `::part()`, which represents the jump from + /// the part to the containing shadow host. + part, + + /// Non-standard Vue >>> combinator. + /// https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors + deep_descendant, + /// Non-standard /deep/ combinator. + /// Appeared in early versions of the css-scoping-1 specification: + /// https://www.w3.org/TR/2014/WD-css-scoping-1-20140403/#deep-combinator + /// And still supported as an alias for >>> by Vue. + deep, + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @compileError("Do not call this! Use `serializer.serializeCombinator()` or `tocss_servo.toCss_Combinator()` instead."); + } + + pub fn format(this: *const Combinator, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + return switch (this.*) { + .child => writer.print(">", .{}), + .descendant => writer.print("`descendant` (space)", .{}), + .next_sibling => writer.print("+", .{}), + .later_sibling => writer.print("~", .{}), + else => writer.print("{s}", .{@tagName(this.*)}), + }; + } +}; + +pub const SelectorParseErrorKind = union(enum) { + invalid_state, + class_needs_ident: css.Token, + pseudo_element_expected_ident: css.Token, + unsupported_pseudo_class_or_element: []const u8, + no_qualified_name_in_attribute_selector: css.Token, + unexpected_token_in_attribute_selector: css.Token, + invalid_qual_name_in_attr: css.Token, + expected_bar_in_attr: css.Token, + empty_selector, + dangling_combinator, + invalid_pseudo_class_before_webkit_scrollbar, + invalid_pseudo_class_after_webkit_scrollbar, + invalid_pseudo_class_after_pseudo_element, + missing_nesting_selector, + missing_nesting_prefix, + expected_namespace: []const u8, + bad_value_in_attr: css.Token, + explicit_namespace_unexpected_token: css.Token, + unexpected_ident: []const u8, + + pub fn intoDefaultParserError(this: SelectorParseErrorKind) css.ParserError { + return css.ParserError{ + .selector_error = this.intoSelectorError(), + }; + } + + pub fn intoSelectorError(this: SelectorParseErrorKind) css.SelectorError { + return switch (this) { + .invalid_state => .invalid_state, + .class_needs_ident => |token| .{ .class_needs_ident = token }, + .pseudo_element_expected_ident => |token| .{ .pseudo_element_expected_ident = token }, + .unsupported_pseudo_class_or_element => |name| .{ .unsupported_pseudo_class_or_element = name }, + .no_qualified_name_in_attribute_selector => |token| .{ .no_qualified_name_in_attribute_selector = token }, + .unexpected_token_in_attribute_selector => |token| .{ .unexpected_token_in_attribute_selector = token }, + .invalid_qual_name_in_attr => |token| .{ .invalid_qual_name_in_attr = token }, + .expected_bar_in_attr => |token| .{ .expected_bar_in_attr = token }, + .empty_selector => .empty_selector, + .dangling_combinator => .dangling_combinator, + .invalid_pseudo_class_before_webkit_scrollbar => .invalid_pseudo_class_before_webkit_scrollbar, + .invalid_pseudo_class_after_webkit_scrollbar => .invalid_pseudo_class_after_webkit_scrollbar, + .invalid_pseudo_class_after_pseudo_element => .invalid_pseudo_class_after_pseudo_element, + .missing_nesting_selector => .missing_nesting_selector, + .missing_nesting_prefix => .missing_nesting_prefix, + .expected_namespace => |name| .{ .expected_namespace = name }, + .bad_value_in_attr => |token| .{ .bad_value_in_attr = token }, + .explicit_namespace_unexpected_token => |token| .{ .explicit_namespace_unexpected_token = token }, + .unexpected_ident => |ident| .{ .unexpected_ident = ident }, + }; + } +}; + +pub fn SimpleSelectorParseResult(comptime Impl: type) type { + ValidSelectorImpl(Impl); + + return union(enum) { + simple_selector: GenericComponent(Impl), + pseudo_element: Impl.SelectorImpl.PseudoElement, + slotted_pseudo: GenericSelector(Impl), + // todo_stuff.think_mem_mgmt + part_pseudo: []Impl.SelectorImpl.Identifier, + }; +} + +/// A pseudo element. +pub const PseudoElement = union(enum) { + /// The [::after](https://drafts.csswg.org/css-pseudo-4/#selectordef-after) pseudo element. + after, + /// The [::before](https://drafts.csswg.org/css-pseudo-4/#selectordef-before) pseudo element. + before, + /// The [::first-line](https://drafts.csswg.org/css-pseudo-4/#first-line-pseudo) pseudo element. + first_line, + /// The [::first-letter](https://drafts.csswg.org/css-pseudo-4/#first-letter-pseudo) pseudo element. + first_letter, + /// The [::selection](https://drafts.csswg.org/css-pseudo-4/#selectordef-selection) pseudo element. + selection: css.VendorPrefix, + /// The [::placeholder](https://drafts.csswg.org/css-pseudo-4/#placeholder-pseudo) pseudo element. + placeholder: css.VendorPrefix, + /// The [::marker](https://drafts.csswg.org/css-pseudo-4/#marker-pseudo) pseudo element. + marker, + /// The [::backdrop](https://fullscreen.spec.whatwg.org/#::backdrop-pseudo-element) pseudo element. + backdrop: css.VendorPrefix, + /// The [::file-selector-button](https://drafts.csswg.org/css-pseudo-4/#file-selector-button-pseudo) pseudo element. + file_selector_button: css.VendorPrefix, + /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo element. + webkit_scrollbar: WebKitScrollbarPseudoElement, + /// The [::cue](https://w3c.github.io/webvtt/#the-cue-pseudo-element) pseudo element. + cue, + /// The [::cue-region](https://w3c.github.io/webvtt/#the-cue-region-pseudo-element) pseudo element. + cue_region, + /// The [::cue()](https://w3c.github.io/webvtt/#cue-selector) functional pseudo element. + cue_function: struct { + /// The selector argument. + selector: *Selector, + }, + /// The [::cue-region()](https://w3c.github.io/webvtt/#cue-region-selector) functional pseudo element. + cue_region_function: struct { + /// The selector argument. + selector: *Selector, + }, + /// The [::view-transition](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition) pseudo element. + view_transition, + /// The [::view-transition-group()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-group-pt-name-selector) functional pseudo element. + view_transition_group: struct { + /// A part name selector. + part_name: ViewTransitionPartName, + }, + /// The [::view-transition-image-pair()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-image-pair-pt-name-selector) functional pseudo element. + view_transition_image_pair: struct { + /// A part name selector. + part_name: ViewTransitionPartName, + }, + /// The [::view-transition-old()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-old-pt-name-selector) functional pseudo element. + view_transition_old: struct { + /// A part name selector. + part_name: ViewTransitionPartName, + }, + /// The [::view-transition-new()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-new-pt-name-selector) functional pseudo element. + view_transition_new: struct { + /// A part name selector. + part_name: ViewTransitionPartName, + }, + /// An unknown pseudo element. + custom: struct { + /// The name of the pseudo element. + name: []const u8, + }, + /// An unknown functional pseudo element. + custom_function: struct { + /// The name of the pseudo element. + name: []const u8, + /// The arguments of the pseudo element function. + arguments: css.TokenList, + }, + + pub fn validAfterSlotted(this: *const PseudoElement) bool { + return switch (this.*) { + .before, .after, .marker, .placeholder, .file_selector_button => true, + else => false, + }; + } + + pub fn isUnknown(this: *const PseudoElement) bool { + return switch (this.*) { + .custom, .custom_function => true, + else => false, + }; + } + + pub fn acceptsStatePseudoClasses(this: *const PseudoElement) bool { + _ = this; // autofix + // Be lienient. + return true; + } + + pub fn isWebkitScrollbar(this: *const PseudoElement) bool { + return this.* == .webkit_scrollbar; + } + + pub fn isViewTransition(this: *const PseudoElement) bool { + return switch (this.*) { + .view_transition_group, .view_transition_image_pair, .view_transition_new, .view_transition_old => true, + else => false, + }; + } + + pub fn toCss(this: *const PseudoElement, comptime W: type, dest: *Printer(W)) PrintErr!void { + var s = ArrayList(u8){}; + // PERF(alloc): I don't like making small allocations here for the string. + const writer = s.writer(dest.allocator); + const W2 = @TypeOf(writer); + const scratchbuf = std.ArrayList(u8).init(dest.allocator); + var printer = Printer(W2).new(dest.allocator, scratchbuf, writer, css.PrinterOptions{}); + try serialize.serializePseudoElement(this, W2, &printer, null); + return dest.writeStr(s.items); + } +}; + +/// An enum for the different types of :nth- pseudoclasses +pub const NthType = enum { + child, + last_child, + only_child, + of_type, + last_of_type, + only_of_type, + col, + last_col, + + pub fn isOnly(self: NthType) bool { + return self == NthType.only_child or self == NthType.only_of_type; + } + + pub fn isOfType(self: NthType) bool { + return self == NthType.of_type or self == NthType.last_of_type or self == NthType.only_of_type; + } + + pub fn isFromEnd(self: NthType) bool { + return self == NthType.last_child or self == NthType.last_of_type or self == NthType.last_col; + } + + pub fn allowsOfSelector(self: NthType) bool { + return self == NthType.child or self == NthType.last_child; + } +}; + +/// * `Err(())`: Invalid selector, abort +/// * `Ok(false)`: Not a type selector, could be something else. `input` was not consumed. +/// * `Ok(true)`: Length 0 (`*|*`), 1 (`*|E` or `ns|*`) or 2 (`|E` or `ns|E`) +pub fn parse_type_selector( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: SelectorParsingState, + sink: *SelectorBuilder(Impl), +) Result(bool) { + const result = switch (parse_qualified_name( + Impl, + parser, + input, + false, + )) { + .result => |v| v, + .err => |e| { + if (e.kind == .basic and e.kind.basic == .end_of_input) { + return .{ .result = false }; + } + + return .{ .err = e }; + }, + }; + + if (result == .none) return .{ .result = false }; + + const namespace: QNamePrefix(Impl) = result.some[0]; + const local_name: ?[]const u8 = result.some[1]; + if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + + switch (namespace) { + .implicit_any_namespace => {}, + .implicit_default_namespace => |url| { + sink.pushSimpleSelector(.{ .default_namespace = url }); + }, + .explicit_namespace => { + const prefix = namespace.explicit_namespace[0]; + const url = namespace.explicit_namespace[1]; + const component: GenericComponent(Impl) = component: { + if (parser.defaultNamespace()) |default_url| { + if (bun.strings.eql(url, default_url)) { + break :component .{ .default_namespace = url }; + } + } + break :component .{ + .namespace = .{ + .prefix = prefix, + .url = url, + }, + }; + }; + sink.pushSimpleSelector(component); + }, + .explicit_no_namespace => { + sink.pushSimpleSelector(.explicit_no_namespace); + }, + .explicit_any_namespace => { + // Element type selectors that have no namespace + // component (no namespace separator) represent elements + // without regard to the element's namespace (equivalent + // to "*|") unless a default namespace has been declared + // for namespaced selectors (e.g. in CSS, in the style + // sheet). If a default namespace has been declared, + // such selectors will represent only elements in the + // default namespace. + // -- Selectors § 6.1.1 + // So we'll have this act the same as the + // QNamePrefix::ImplicitAnyNamespace case. + // For lightning css this logic was removed, should be handled when matching. + sink.pushSimpleSelector(.explicit_any_namespace); + }, + .implicit_no_namespace => { + bun.unreachablePanic("Should not be returned with in_attr_selector = false", .{}); + }, + } + + if (local_name) |name| { + sink.pushSimpleSelector(.{ + .local_name = LocalName(Impl){ + .lower_name = brk: { + var lowercase = parser.allocator.alloc(u8, name.len) catch unreachable; // PERF: check if it's already lowercase + break :brk .{ .v = bun.strings.copyLowercase(name, lowercase[0..]) }; + }, + .name = .{ .v = name }, + }, + }); + } else { + sink.pushSimpleSelector(.explicit_universal_type); + } + + return .{ .result = true }; +} + +/// Parse a simple selector other than a type selector. +/// +/// * `Err(())`: Invalid selector, abort +/// * `Ok(None)`: Not a simple selector, could be something else. `input` was not consumed. +/// * `Ok(Some(_))`: Parsed a simple selector or pseudo-element +pub fn parse_one_simple_selector( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, +) Result(?SimpleSelectorParseResult(Impl)) { + const S = SimpleSelectorParseResult(Impl); + + const start = input.state(); + const token = switch (input.nextIncludingWhitespace()) { + .result => |v| v.*, + .err => { + input.reset(&start); + return .{ .result = null }; + }, + }; + + switch (token) { + .idhash => |id| { + if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + const component: GenericComponent(Impl) = .{ .id = .{ .v = id } }; + return .{ .result = S{ + .simple_selector = component, + } }; + }, + .open_square => { + if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + const Closure = struct { + parser: *SelectorParser, + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(GenericComponent(Impl)) { + return parse_attribute_selector(Impl, this.parser, input2); + } + }; + var closure = Closure{ + .parser = parser, + }; + const attr = switch (input.parseNestedBlock(GenericComponent(Impl), &closure, Closure.parsefn)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = .{ .simple_selector = attr } }; + }, + .colon => { + const location = input.currentSourceLocation(); + const is_single_colon: bool, const next_token: css.Token = switch ((switch (input.nextIncludingWhitespace()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }).*) { + .colon => .{ false, (switch (input.nextIncludingWhitespace()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }).* }, + else => |t| .{ true, t }, + }; + const name: []const u8, const is_functional = switch (next_token) { + .ident => |name| .{ name, false }, + .function => |name| .{ name, true }, + else => |t| { + const e = SelectorParseErrorKind{ .pseudo_element_expected_ident = t }; + return .{ .err = input.newCustomError(e.intoDefaultParserError()) }; + }, + }; + const is_pseudo_element = !is_single_colon or is_css2_pseudo_element(name); + if (is_pseudo_element) { + if (!state.allowsPseudos()) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + const pseudo_element: Impl.SelectorImpl.PseudoElement = if (is_functional) pseudo_element: { + if (parser.parsePart() and bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "part")) { + if (!state.allowsPart()) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + + const Closure = struct { + parser: *SelectorParser, + + pub fn parsefn(self: *const @This(), input2: *css.Parser) Result([]Impl.SelectorImpl.Identifier) { + // todo_stuff.think_about_mem_mgmt + var result = ArrayList(Impl.SelectorImpl.Identifier).initCapacity( + self.parser.allocator, + // TODO: source does this, should see if initializing to 1 is actually better + // when appending empty std.ArrayList(T), it will usually initially reserve 8 elements, + // maybe that's unnecessary, or maybe smallvec is gud here + 1, + ) catch unreachable; + + result.append( + self.parser.allocator, + switch (input2.expectIdent()) { + .err => |e| return .{ .err = e }, + .result => |v| .{ .v = v }, + }, + ) catch unreachable; + + while (!input2.isExhausted()) { + result.append( + self.parser.allocator, + switch (input2.expectIdent()) { + .err => |e| return .{ .err = e }, + .result => |v| .{ .v = v }, + }, + ) catch unreachable; + } + + return .{ .result = result.items }; + } + }; + + const names = switch (input.parseNestedBlock([]Impl.SelectorImpl.Identifier, &Closure{ .parser = parser }, Closure.parsefn)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + return .{ .result = .{ .part_pseudo = names } }; + } + + if (parser.parseSlotted() and bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "slotted")) { + if (!state.allowsSlotted()) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + const Closure = struct { + parser: *SelectorParser, + state: *SelectorParsingState, + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(GenericSelector(Impl)) { + return parse_inner_compound_selector(Impl, this.parser, input2, this.state); + } + }; + var closure = Closure{ + .parser = parser, + .state = state, + }; + const selector = switch (input.parseNestedBlock(GenericSelector(Impl), &closure, Closure.parsefn)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = .{ .slotted_pseudo = selector } }; + } + + const Closure = struct { + parser: *SelectorParser, + state: *SelectorParsingState, + name: []const u8, + }; + break :pseudo_element switch (input.parseNestedBlock(Impl.SelectorImpl.PseudoElement, &Closure{ .parser = parser, .state = state, .name = name }, struct { + pub fn parseFn(closure: *const Closure, i: *css.Parser) Result(Impl.SelectorImpl.PseudoElement) { + return closure.parser.parseFunctionalPseudoElement(closure.name, i); + } + }.parseFn)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + } else pseudo_element: { + break :pseudo_element switch (parser.parsePseudoElement(location, name)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + }; + + if (state.intersects(.{ .after_slotted = true }) and pseudo_element.validAfterSlotted()) { + return .{ .result = .{ .pseudo_element = pseudo_element } }; + } + + return .{ .result = .{ .pseudo_element = pseudo_element } }; + } else { + const pseudo_class: GenericComponent(Impl) = if (is_functional) pseudo_class: { + const Closure = struct { + parser: *SelectorParser, + name: []const u8, + state: *SelectorParsingState, + pub fn parsefn(this: *@This(), input2: *css.Parser) Result(GenericComponent(Impl)) { + return parse_functional_pseudo_class(Impl, this.parser, input2, this.name, this.state); + } + }; + var closure = Closure{ + .parser = parser, + .name = name, + .state = state, + }; + + break :pseudo_class switch (input.parseNestedBlock(GenericComponent(Impl), &closure, Closure.parsefn)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + } else switch (parse_simple_pseudo_class(Impl, parser, location, name, state.*)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + return .{ .result = .{ .simple_selector = pseudo_class } }; + } + }, + .delim => |d| { + switch (d) { + '.' => { + if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + const location = input.currentSourceLocation(); + const class = switch ((switch (input.nextIncludingWhitespace()) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }).*) { + .ident => |class| class, + else => |t| { + const e = SelectorParseErrorKind{ .class_needs_ident = t }; + return .{ .err = location.newCustomError(e.intoDefaultParserError()) }; + }, + }; + return .{ .result = .{ .simple_selector = .{ .class = .{ .v = class } } } }; + }, + '&' => { + if (parser.isNestingAllowed()) { + state.insert(SelectorParsingState{ .after_nesting = true }); + return .{ .result = S{ + .simple_selector = .nesting, + } }; + } + }, + else => {}, + } + }, + else => {}, + } + + input.reset(&start); + return .{ .result = null }; +} + +pub fn parse_attribute_selector(comptime Impl: type, parser: *SelectorParser, input: *css.Parser) Result(GenericComponent(Impl)) { + const N = attrs.NamespaceConstraint(attrs.NamespaceUrl(Impl)); + + const namespace: ?N, const local_name: []const u8 = brk: { + input.skipWhitespace(); + + const _qname = switch (parse_qualified_name(Impl, parser, input, true)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + switch (_qname) { + .none => |t| return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .no_qualified_name_in_attribute_selector = t })) }, + .some => |qname| { + if (qname[1] == null) { + bun.unreachablePanic("", .{}); + } + const ns: QNamePrefix(Impl) = qname[0]; + const ln = qname[1].?; + break :brk .{ + switch (ns) { + .implicit_no_namespace, .explicit_no_namespace => null, + .explicit_namespace => |x| .{ .specific = .{ .prefix = x[0], .url = x[1] } }, + .explicit_any_namespace => .any, + .implicit_any_namespace, .implicit_default_namespace => { + bun.unreachablePanic("Not returned with in_attr_selector = true", .{}); + }, + }, + ln, + }; + }, + } + }; + + const location = input.currentSourceLocation(); + const operator: attrs.AttrSelectorOperator = operator: { + const tok = switch (input.next()) { + .result => |v| v, + .err => { + // [foo] + const local_name_lower = local_name_lower: { + const lower = parser.allocator.alloc(u8, local_name.len) catch unreachable; + _ = bun.strings.copyLowercase(local_name, lower); + break :local_name_lower lower; + }; + if (namespace) |ns| { + const x = attrs.AttrSelectorWithOptionalNamespace(Impl){ + .namespace = ns, + .local_name = .{ .v = local_name }, + .local_name_lower = .{ .v = local_name_lower }, + .never_matches = false, + .operation = .exists, + }; + return .{ + .result = .{ .attribute_other = bun.create(parser.allocator, attrs.AttrSelectorWithOptionalNamespace(Impl), x) }, + }; + } else { + return .{ .result = .{ + .attribute_in_no_namespace_exists = .{ + .local_name = .{ .v = local_name }, + .local_name_lower = .{ .v = local_name_lower }, + }, + } }; + } + }, + }; + + switch (tok.*) { + // [foo=bar] + .delim => |d| { + if (d == '=') break :operator .equal; + }, + // [foo~=bar] + .include_match => break :operator .includes, + // [foo|=bar] + .dash_match => break :operator .dash_match, + // [foo^=bar] + .prefix_match => break :operator .prefix, + // [foo*=bar] + .substring_match => break :operator .substring, + // [foo$=bar] + .suffix_match => break :operator .suffix, + else => {}, + } + return .{ .err = location.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .unexpected_token_in_attribute_selector = tok.* })) }; + }; + + const value_str: []const u8 = switch (input.expectIdentOrString()) { + .result => |v| v, + .err => |e| { + if (e.kind == .basic and e.kind.basic == .unexpected_token) { + return .{ .err = e.location.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .bad_value_in_attr = e.kind.basic.unexpected_token })) }; + } + return .{ + .err = .{ + .kind = e.kind, + .location = e.location, + }, + }; + }, + }; + const never_matches = switch (operator) { + .equal, .dash_match => false, + .includes => value_str.len == 0 or std.mem.indexOfAny(u8, value_str, SELECTOR_WHITESPACE) != null, + .prefix, .substring, .suffix => value_str.len == 0, + }; + + const attribute_flags = switch (parse_attribute_flags(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + const value: Impl.SelectorImpl.AttrValue = value_str; + const local_name_lower: Impl.SelectorImpl.LocalName, const local_name_is_ascii_lowercase: bool = brk: { + if (a: { + for (local_name, 0..) |b, i| { + if (b >= 'A' and b <= 'Z') break :a i; + } + break :a null; + }) |first_uppercase| { + const str = local_name[first_uppercase..]; + const lower = parser.allocator.alloc(u8, str.len) catch unreachable; + break :brk .{ .{ .v = bun.strings.copyLowercase(str, lower) }, false }; + } else { + break :brk .{ .{ .v = local_name }, true }; + } + }; + const case_sensitivity: attrs.ParsedCaseSensitivity = attribute_flags.toCaseSensitivity(local_name_lower.v, namespace != null); + if (namespace != null and !local_name_is_ascii_lowercase) { + return .{ .result = .{ + .attribute_other = brk: { + const x = attrs.AttrSelectorWithOptionalNamespace(Impl){ + .namespace = namespace, + .local_name = .{ .v = local_name }, + .local_name_lower = local_name_lower, + .never_matches = never_matches, + .operation = .{ + .with_value = .{ + .operator = operator, + .case_sensitivity = case_sensitivity, + .expected_value = value, + }, + }, + }; + break :brk bun.create(parser.allocator, @TypeOf(x), x); + }, + } }; + } else { + return .{ .result = .{ + .attribute_in_no_namespace = .{ + .local_name = .{ .v = local_name }, + .operator = operator, + .value = value, + .case_sensitivity = case_sensitivity, + .never_matches = never_matches, + }, + } }; + } +} + +/// Returns whether the name corresponds to a CSS2 pseudo-element that +/// can be specified with the single colon syntax (in addition to the +/// double-colon syntax, which can be used for all pseudo-elements). +pub fn is_css2_pseudo_element(name: []const u8) bool { + // ** Do not add to this list! ** + // TODO: todo_stuff.match_ignore_ascii_case + return bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "before") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "after") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-line") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-letter"); +} + +/// Parses one compound selector suitable for nested stuff like :-moz-any, etc. +pub fn parse_inner_compound_selector( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, +) Result(GenericSelector(Impl)) { + var child_state = brk: { + var child_state = state.*; + child_state.disallow_pseudos = true; + child_state.disallow_combinators = true; + break :brk child_state; + }; + const result = switch (parse_selector(Impl, parser, input, &child_state, NestingRequirement.none)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (child_state.after_nesting) { + state.after_nesting = true; + } + return .{ .result = result }; +} + +pub fn parse_functional_pseudo_class( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + name: []const u8, + state: *SelectorParsingState, +) Result(GenericComponent(Impl)) { + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-child")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .child); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-of-type")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .of_type); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-last-child")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .last_child); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-last-of-type")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .last_of_type); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-col")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .col); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "nth-last-col")) { + return parse_nth_pseudo_class(Impl, parser, input, state.*, .last_col); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "is") and parser.parseIsAndWhere()) { + return parse_is_or_where(Impl, parser, input, state, GenericComponent(Impl).convertHelper_is, .{}); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "where") and parser.parseIsAndWhere()) { + return parse_is_or_where(Impl, parser, input, state, GenericComponent(Impl).convertHelper_where, .{}); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "has")) { + return parse_has(Impl, parser, input, state); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "host")) { + if (!state.allowsTreeStructuralPseudoClasses()) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + return .{ .result = .{ + .host = switch (parse_inner_compound_selector(Impl, parser, input, state)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }, + } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "not")) { + return parse_negation(Impl, parser, input, state); + } else { + // + } + + if (parser.parseAnyPrefix(name)) |prefix| { + return parse_is_or_where(Impl, parser, input, state, GenericComponent(Impl).convertHelper_any, .{prefix}); + } + + if (!state.allowsCustomFunctionalPseudoClasses()) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + + const result = switch (parser.parseNonTsFunctionalPseudoClass(name, input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + return .{ .result = .{ .non_ts_pseudo_class = result } }; +} + +pub fn parse_simple_pseudo_class( + comptime Impl: type, + parser: *SelectorParser, + location: css.SourceLocation, + name: []const u8, + state: SelectorParsingState, +) Result(GenericComponent(Impl)) { + if (!state.allowsNonFunctionalPseudoClasses()) { + return .{ .err = location.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + + if (state.allowsTreeStructuralPseudoClasses()) { + // css.todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-child")) { + return .{ .result = .{ .nth = NthSelectorData.first(false) } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "last-child")) { + return .{ .result = .{ .nth = NthSelectorData.last(false) } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "only-child")) { + return .{ .result = .{ .nth = NthSelectorData.only(false) } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "root")) { + return .{ .result = .root }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "empty")) { + return .{ .result = .empty }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "scope")) { + return .{ .result = .scope }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "host")) { + return .{ .result = .{ .host = null } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-of-type")) { + return .{ .result = .{ .nth = NthSelectorData.first(true) } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "last-of-type")) { + return .{ .result = .{ .nth = NthSelectorData.last(true) } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "only-of-type")) { + return .{ .result = .{ .nth = NthSelectorData.only(true) } }; + } else {} + } + + // The view-transition pseudo elements accept the :only-child pseudo class. + // https://w3c.github.io/csswg-drafts/css-view-transitions-1/#pseudo-root + if (state.intersects(SelectorParsingState{ .after_view_transition = true })) { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "only-child")) { + return .{ .result = .{ .nth = NthSelectorData.only(false) } }; + } + } + + const pseudo_class = switch (parser.parseNonTsPseudoClass(location, name)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (state.intersects(SelectorParsingState{ .after_webkit_scrollbar = true })) { + if (!pseudo_class.isValidAfterWebkitScrollbar()) { + return .{ .err = location.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_pseudo_class_after_webkit_scrollbar)) }; + } + } else if (state.intersects(SelectorParsingState{ .after_pseudo_element = true })) { + if (!pseudo_class.isUserActionState()) { + return .{ .err = location.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_pseudo_class_after_pseudo_element)) }; + } + } else if (!pseudo_class.isValidBeforeWebkitScrollbar()) { + return .{ .err = location.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_pseudo_class_before_webkit_scrollbar)) }; + } + + return .{ .result = .{ .non_ts_pseudo_class = pseudo_class } }; +} + +pub fn parse_nth_pseudo_class( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: SelectorParsingState, + ty: NthType, +) Result(GenericComponent(Impl)) { + if (!state.allowsTreeStructuralPseudoClasses()) { + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + } + + const a, const b = switch (css.nth.parse_nth(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + const nth_data = NthSelectorData{ + .ty = ty, + .is_function = true, + .a = a, + .b = b, + }; + + if (!ty.allowsOfSelector()) { + return .{ .result = .{ .nth = nth_data } }; + } + + // Try to parse "of ". + if (input.tryParse(css.Parser.expectIdentMatching, .{"of"}).isErr()) { + return .{ .result = .{ .nth = nth_data } }; + } + + // Whitespace between "of" and the selector list is optional + // https://github.com/w3c/csswg-drafts/issues/8285 + var child_state = child_state: { + var s = state; + s.skip_default_namespace = true; + s.disallow_pseudos = true; + break :child_state s; + }; + + const selectors = switch (SelectorList.parseWithState( + parser, + input, + &child_state, + .ignore_invalid_selector, + .none, + )) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + return .{ .result = .{ + .nth_of = NthOfSelectorData(Impl){ + .data = nth_data, + .selectors = selectors.v.items, + }, + } }; +} + +/// `func` must be of the type: fn([]GenericSelector(Impl), ...@TypeOf(args_)) GenericComponent(Impl) +pub fn parse_is_or_where( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, + comptime func: anytype, + args_: anytype, +) Result(GenericComponent(Impl)) { + bun.debugAssert(parser.parseIsAndWhere()); + // https://drafts.csswg.org/selectors/#matches-pseudo: + // + // Pseudo-elements cannot be represented by the matches-any + // pseudo-class; they are not valid within :is(). + // + var child_state = brk: { + var child_state = state.*; + child_state.skip_default_namespace = true; + child_state.disallow_pseudos = true; + break :brk child_state; + }; + + const inner = switch (SelectorList.parseWithState(parser, input, &child_state, parser.isAndWhereErrorRecovery(), NestingRequirement.none)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + if (child_state.after_nesting) { + state.after_nesting = true; + } + + const selector_slice = inner.v.items; + + const result = result: { + const args = brk: { + var args: std.meta.ArgsTuple(@TypeOf(func)) = undefined; + args[0] = selector_slice; + + inline for (args_, 1..) |a, i| { + args[i] = a; + } + + break :brk args; + }; + + break :result @call(.auto, func, args); + }; + + return .{ .result = result }; +} + +pub fn parse_has( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, +) Result(GenericComponent(Impl)) { + var child_state = state.*; + const inner = switch (SelectorList.parseRelativeWithState( + parser, + input, + &child_state, + parser.isAndWhereErrorRecovery(), + .none, + )) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + if (child_state.after_nesting) { + state.after_nesting = true; + } + return .{ .result = .{ .has = inner.v.items } }; +} + +/// Level 3: Parse **one** simple_selector. (Though we might insert a second +/// implied "|*" type selector.) +pub fn parse_negation( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + state: *SelectorParsingState, +) Result(GenericComponent(Impl)) { + var child_state = state.*; + child_state.skip_default_namespace = true; + child_state.disallow_pseudos = true; + + const list = switch (SelectorList.parseWithState(parser, input, &child_state, .discard_list, .none)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + if (child_state.after_nesting) { + state.after_nesting = true; + } + + return .{ .result = .{ .negation = list.v.items } }; +} + +pub fn OptionalQName(comptime Impl: type) type { + return union(enum) { + some: struct { QNamePrefix(Impl), ?[]const u8 }, + none: css.Token, + }; +} + +pub fn QNamePrefix(comptime Impl: type) type { + return union(enum) { + implicit_no_namespace, // `foo` in attr selectors + implicit_any_namespace, // `foo` in type selectors, without a default ns + implicit_default_namespace: Impl.SelectorImpl.NamespaceUrl, // `foo` in type selectors, with a default ns + explicit_no_namespace, // `|foo` + explicit_any_namespace, // `*|foo` + explicit_namespace: struct { Impl.SelectorImpl.NamespacePrefix, Impl.SelectorImpl.NamespaceUrl }, // `prefix|foo` + }; +} + +/// * `Err(())`: Invalid selector, abort +/// * `Ok(None(token))`: Not a simple selector, could be something else. `input` was not consumed, +/// but the token is still returned. +/// * `Ok(Some(namespace, local_name))`: `None` for the local name means a `*` universal selector +pub fn parse_qualified_name( + comptime Impl: type, + parser: *SelectorParser, + input: *css.Parser, + in_attr_selector: bool, +) Result(OptionalQName(Impl)) { + const start = input.state(); + + const tok = switch (input.nextIncludingWhitespace()) { + .result => |v| v, + .err => |e| { + input.reset(&start); + return .{ .err = e }; + }, + }; + switch (tok.*) { + .ident => |value| { + const after_ident = input.state(); + const n = if (input.nextIncludingWhitespace().asValue()) |t| t.* == .delim and t.delim == '|' else false; + if (n) { + const prefix: Impl.SelectorImpl.NamespacePrefix = .{ .v = value }; + const result: ?Impl.SelectorImpl.NamespaceUrl = parser.namespaceForPrefix(prefix); + const url: Impl.SelectorImpl.NamespaceUrl = brk: { + if (result) |url| break :brk url; + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .unsupported_pseudo_class_or_element = value })) }; + }; + return parse_qualified_name_eplicit_namespace_helper( + Impl, + input, + .{ .explicit_namespace = .{ prefix, url } }, + in_attr_selector, + ); + } else { + input.reset(&after_ident); + if (in_attr_selector) return .{ .result = .{ .some = .{ .implicit_no_namespace, value } } }; + return .{ .result = parse_qualified_name_default_namespace_helper(Impl, parser, value) }; + } + }, + .delim => |c| { + switch (c) { + '*' => { + const after_star = input.state(); + const result = input.nextIncludingWhitespace(); + if (result.asValue()) |t| if (t.* == .delim and t.delim == '|') + return parse_qualified_name_eplicit_namespace_helper( + Impl, + input, + .explicit_any_namespace, + in_attr_selector, + ); + input.reset(&after_star); + if (in_attr_selector) { + switch (result) { + .result => |t| { + return .{ .err = after_star.sourceLocation().newCustomError(SelectorParseErrorKind{ + .expected_bar_in_attr = t.*, + }) }; + }, + .err => |e| { + return .{ .err = e }; + }, + } + } else { + return .{ .result = parse_qualified_name_default_namespace_helper(Impl, parser, null) }; + } + }, + '|' => return parse_qualified_name_eplicit_namespace_helper(Impl, input, .explicit_no_namespace, in_attr_selector), + else => {}, + } + }, + else => {}, + } + input.reset(&start); + return .{ .result = .{ .none = tok.* } }; +} + +fn parse_qualified_name_default_namespace_helper( + comptime Impl: type, + parser: *SelectorParser, + local_name: ?[]const u8, +) OptionalQName(Impl) { + const namespace: QNamePrefix(Impl) = if (parser.defaultNamespace()) |url| .{ .implicit_default_namespace = url } else .implicit_any_namespace; + return .{ + .some = .{ + namespace, + local_name, + }, + }; +} + +fn parse_qualified_name_eplicit_namespace_helper( + comptime Impl: type, + input: *css.Parser, + namespace: QNamePrefix(Impl), + in_attr_selector: bool, +) Result(OptionalQName(Impl)) { + const location = input.currentSourceLocation(); + const t = switch (input.nextIncludingWhitespace()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + switch (t.*) { + .ident => |local_name| return .{ .result = .{ .some = .{ namespace, local_name } } }, + .delim => |c| { + if (c == '*') { + return .{ .result = .{ .some = .{ namespace, null } } }; + } + }, + else => {}, + } + if (in_attr_selector) { + const e = SelectorParseErrorKind{ .invalid_qual_name_in_attr = t.* }; + return .{ .err = location.newCustomError(e) }; + } + return .{ .err = location.newCustomError(SelectorParseErrorKind{ .explicit_namespace_unexpected_token = t.* }) }; +} + +pub fn LocalName(comptime Impl: type) type { + return struct { + name: Impl.SelectorImpl.LocalName, + lower_name: Impl.SelectorImpl.LocalName, + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return css.IdentFns.toCss(&this.name, W, dest); + } + }; +} + +/// An attribute selector can have 's' or 'i' as flags, or no flags at all. +pub const AttributeFlags = enum { + // Matching should be case-sensitive ('s' flag). + case_sensitive, + // Matching should be case-insensitive ('i' flag). + ascii_case_insensitive, + // No flags. Matching behavior depends on the name of the attribute. + case_sensitivity_depends_on_name, + + pub fn toCaseSensitivity(this: AttributeFlags, local_name: []const u8, have_namespace: bool) attrs.ParsedCaseSensitivity { + return switch (this) { + .case_sensitive => .explicit_case_sensitive, + .ascii_case_insensitive => .ascii_case_insensitive, + .case_sensitivity_depends_on_name => { + // + const AsciiCaseInsensitiveHtmlAttributes = enum { + dir, + http_equiv, + rel, + enctype, + @"align", + accept, + nohref, + lang, + bgcolor, + direction, + valign, + checked, + frame, + link, + accept_charset, + hreflang, + text, + valuetype, + language, + nowrap, + vlink, + disabled, + noshade, + codetype, + @"defer", + noresize, + target, + scrolling, + rules, + scope, + rev, + media, + method, + charset, + alink, + selected, + multiple, + color, + shape, + type, + clear, + compact, + face, + declare, + axis, + readonly, + }; + const Map = comptime bun.ComptimeEnumMap(AsciiCaseInsensitiveHtmlAttributes); + if (!have_namespace and Map.has(local_name)) return .ascii_case_insensitive_if_in_html_element_in_html_document; + return .case_sensitive; + }, + }; + } +}; + +/// A [view transition part name](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector). +pub const ViewTransitionPartName = union(enum) { + /// * + all, + /// + name: css.css_values.ident.CustomIdent, + + pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + return switch (this.*) { + .all => try dest.writeStr("*"), + .name => |name| try css.CustomIdentFns.toCss(&name, W, dest), + }; + } +}; + +pub fn parse_attribute_flags(input: *css.Parser) Result(AttributeFlags) { + const location = input.currentSourceLocation(); + const token = switch (input.next()) { + .result => |v| v, + .err => { + // Selectors spec says language-defined; HTML says it depends on the + // exact attribute name. + return .{ .result = AttributeFlags.case_sensitivity_depends_on_name }; + }, + }; + + const ident = if (token.* == .ident) token.ident else return .{ .err = location.newBasicUnexpectedTokenError(token.*) }; + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "i")) { + return .{ .result = AttributeFlags.ascii_case_insensitive }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "s")) { + return .{ .result = AttributeFlags.case_sensitive }; + } else { + return .{ .err = location.newBasicUnexpectedTokenError(token.*) }; + } +} diff --git a/src/css/selectors/selector.zig b/src/css/selectors/selector.zig new file mode 100644 index 0000000000000..191b7704be307 --- /dev/null +++ b/src/css/selectors/selector.zig @@ -0,0 +1,1167 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +const CSSString = css.CSSString; +const CSSStringFns = css.CSSStringFns; + +pub const Printer = css.Printer; +pub const PrintErr = css.PrintErr; + +const Result = css.Result; +const PrintResult = css.PrintResult; + +const ArrayList = std.ArrayListUnmanaged; + +/// Our implementation of the `SelectorImpl` interface +/// +pub const impl = struct { + pub const Selectors = struct { + pub const SelectorImpl = struct { + pub const AttrValue = css.css_values.string.CSSString; + pub const Identifier = css.css_values.ident.Ident; + pub const LocalName = css.css_values.ident.Ident; + pub const NamespacePrefix = css.css_values.ident.Ident; + pub const NamespaceUrl = []const u8; + pub const BorrowedNamespaceUrl = []const u8; + pub const BorrowedLocalName = css.css_values.ident.Ident; + + pub const NonTSPseudoClass = parser.PseudoClass; + pub const PseudoElement = parser.PseudoElement; + pub const VendorPrefix = css.VendorPrefix; + pub const ExtraMatchingData = void; + }; + }; +}; + +pub const parser = @import("./parser.zig"); + +/// The serialization module ported from lightningcss. +/// +/// Note that we have two serialization modules, one from lightningcss and one from servo. +/// +/// This is because it actually uses both implementations. This is confusing. +pub const serialize = struct { + pub fn serializeSelectorList( + list: []const parser.Selector, + comptime W: type, + dest: *Printer(W), + context: ?*const css.StyleContext, + is_relative: bool, + ) PrintErr!void { + var first = true; + for (list) |*selector| { + if (!first) { + try dest.delim(',', false); + } + first = false; + try serializeSelector(selector, W, dest, context, is_relative); + } + } + + pub fn serializeSelector( + selector: *const parser.Selector, + comptime W: type, + dest: *css.Printer(W), + context: ?*const css.StyleContext, + __is_relative: bool, + ) PrintErr!void { + var is_relative = __is_relative; + + if (comptime bun.Environment.isDebug) { + for (selector.components.items) |*comp| { + std.debug.print("Selector components:\n {}", .{comp}); + } + + var compound_selectors = CompoundSelectorIter{ .sel = selector }; + while (compound_selectors.next()) |comp| { + for (comp) |c| { + std.debug.print(" {}, ", .{c}); + } + } + std.debug.print("\n", .{}); + } + + // Compound selectors invert the order of their contents, so we need to + // undo that during serialization. + // + // This two-iterator strategy involves walking over the selector twice. + // We could do something more clever, but selector serialization probably + // isn't hot enough to justify it, and the stringification likely + // dominates anyway. + // + // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(), + // which we need for |split|. So we split by combinators on a match-order + // sequence and then reverse. + var combinators = CombinatorIter{ .sel = selector }; + var compound_selectors = CompoundSelectorIter{ .sel = selector }; + const should_compile_nesting = dest.targets.shouldCompileSame(.nesting); + + var first = true; + var combinators_exhausted = false; + while (compound_selectors.next()) |_compound_| { + bun.debugAssert(!combinators_exhausted); + var compound = _compound_; + + // Skip implicit :scope in relative selectors (e.g. :has(:scope > foo) -> :has(> foo)) + if (is_relative and compound.len >= 1 and compound[0] == .scope) { + if (combinators.next()) |*combinator| { + try serializeCombinator(combinator, W, dest); + } + compound = compound[1..]; + is_relative = false; + } + + // https://drafts.csswg.org/cssom/#serializing-selectors + if (compound.len == 0) continue; + + const has_leading_nesting = first and compound[0] == .nesting; + const first_index: usize = if (has_leading_nesting) 1 else 0; + first = false; + + // 1. If there is only one simple selector in the compound selectors + // which is a universal selector, append the result of + // serializing the universal selector to s. + // + // Check if `!compound.empty()` first--this can happen if we have + // something like `... > ::before`, because we store `>` and `::` + // both as combinators internally. + // + // If we are in this case, after we have serialized the universal + // selector, we skip Step 2 and continue with the algorithm. + const can_elide_namespace, const first_non_namespace = if (first_index >= compound.len) + .{ true, first_index } + else switch (compound[0]) { + .explicit_any_namespace, .explicit_no_namespace, .namespace => .{ false, first_index + 1 }, + .default_namespace => .{ true, first_index + 1 }, + else => .{ true, first_index }, + }; + var perform_step_2 = true; + const next_combinator = combinators.next(); + if (first_non_namespace == compound.len - 1) { + // We have to be careful here, because if there is a + // pseudo element "combinator" there isn't really just + // the one simple selector. Technically this compound + // selector contains the pseudo element selector as well + // -- Combinator::PseudoElement, just like + // Combinator::SlotAssignment, don't exist in the + // spec. + if (next_combinator == .pseudo_element and compound[first_non_namespace].asCombinator() == .slot_assignment) { + // do nothing + } else if (compound[first_non_namespace] == .explicit_universal_type) { + // Iterate over everything so we serialize the namespace + // too. + const swap_nesting = has_leading_nesting and should_compile_nesting; + const slice = if (swap_nesting) brk: { + // Swap nesting and type selector (e.g. &div -> div&). + break :brk compound[@min(1, compound.len)..]; + } else compound; + + for (slice) |*simple| { + try serializeComponent(simple, W, dest, context); + } + + if (swap_nesting) { + try serializeNesting(W, dest, context, false); + } + + // Skip step 2, which is an "otherwise". + perform_step_2 = false; + } else { + // do nothing + } + } + + // 2. Otherwise, for each simple selector in the compound selectors + // that is not a universal selector of which the namespace prefix + // maps to a namespace that is not the default namespace + // serialize the simple selector and append the result to s. + // + // See https://github.com/w3c/csswg-drafts/issues/1606, which is + // proposing to change this to match up with the behavior asserted + // in cssom/serialize-namespaced-type-selectors.html, which the + // following code tries to match. + if (perform_step_2) { + const iter = compound; + var i: usize = 0; + if (has_leading_nesting and + should_compile_nesting and + isTypeSelector(if (first_non_namespace < compound.len) &compound[first_non_namespace] else null)) + { + // Swap nesting and type selector (e.g. &div -> div&). + // This ensures that the compiled selector is valid. e.g. (div.foo is valid, .foodiv is not). + const nesting = &iter[i]; + i += 1; + const local = &iter[i]; + i += 1; + try serializeComponent(local, W, dest, context); + + // Also check the next item in case of namespaces. + if (first_non_namespace > first_index) { + const local2 = &iter[i]; + i += 1; + try serializeComponent(local2, W, dest, context); + } + + try serializeComponent(nesting, W, dest, context); + } else if (has_leading_nesting and should_compile_nesting) { + // Nesting selector may serialize differently if it is leading, due to type selectors. + i += 1; + try serializeNesting(W, dest, context, true); + } + + if (i < compound.len) { + for (iter[i..]) |*simple| { + if (simple.* == .explicit_universal_type) { + // Can't have a namespace followed by a pseudo-element + // selector followed by a universal selector in the same + // compound selector, so we don't have to worry about the + // real namespace being in a different `compound`. + if (can_elide_namespace) { + continue; + } + } + try serializeComponent(simple, W, dest, context); + } + } + } + + // 3. If this is not the last part of the chain of the selector + // append a single SPACE (U+0020), followed by the combinator + // ">", "+", "~", ">>", "||", as appropriate, followed by another + // single SPACE (U+0020) if the combinator was not whitespace, to + // s. + if (next_combinator) |*c| { + try serializeCombinator(c, W, dest); + } else { + combinators_exhausted = true; + } + + // 4. If this is the last part of the chain of the selector and + // there is a pseudo-element, append "::" followed by the name of + // the pseudo-element, to s. + // + // (we handle this above) + } + } + + pub fn serializeComponent( + component: *const parser.Component, + comptime W: type, + dest: *css.Printer(W), + context: ?*const css.StyleContext, + ) PrintErr!void { + switch (component.*) { + .combinator => |c| return serializeCombinator(&c, W, dest), + .attribute_in_no_namespace => |*v| { + try dest.writeChar('['); + try css.css_values.ident.IdentFns.toCss(&v.local_name, W, dest); + try v.operator.toCss(W, dest); + + if (dest.minify) { + // PERF: should we put a scratch buffer in the printer + // Serialize as both an identifier and a string and choose the shorter one. + var id = std.ArrayList(u8).init(dest.allocator); + const writer = id.writer(); + css.serializer.serializeIdentifier(v.value, writer) catch return dest.addFmtError(); + + const s = try css.to_css.string(dest.allocator, CSSString, &v.value, css.PrinterOptions{}); + + if (id.items.len > 0 and id.items.len < s.len) { + try dest.writeStr(id.items); + } else { + try dest.writeStr(s); + } + } else { + try css.CSSStringFns.toCss(&v.value, W, dest); + } + + switch (v.case_sensitivity) { + .case_sensitive, .ascii_case_insensitive_if_in_html_element_in_html_document => {}, + .ascii_case_insensitive => try dest.writeStr(" i"), + .explicit_case_sensitive => try dest.writeStr(" s"), + } + return dest.writeChar(']'); + }, + .is, .where, .negation, .any => { + switch (component.*) { + .where => try dest.writeStr(":where("), + .is => |selectors| { + // If there's only one simple selector, serialize it directly. + if (shouldUnwrapIs(selectors)) { + return serializeSelector(&selectors[0], W, dest, context, false); + } + + const vp = dest.vendor_prefix; + if (vp.intersects(css.VendorPrefix{ .webkit = true, .moz = true })) { + try dest.writeChar(':'); + try vp.toCss(W, dest); + try dest.writeStr("any("); + } else { + try dest.writeStr(":is("); + } + }, + .negation => { + try dest.writeStr(":not("); + }, + .any => |v| { + const vp = dest.vendor_prefix.bitwiseOr(v.vendor_prefix); + if (vp.intersects(css.VendorPrefix{ .webkit = true, .moz = true })) { + try dest.writeChar(':'); + try vp.toCss(W, dest); + try dest.writeStr("any("); + } else { + try dest.writeStr(":is("); + } + }, + else => unreachable, + } + try serializeSelectorList(switch (component.*) { + .where, .is, .negation => |list| list, + .any => |v| v.selectors, + else => unreachable, + }, W, dest, context, false); + return dest.writeStr(")"); + }, + .has => |list| { + try dest.writeStr(":has("); + try serializeSelectorList(list, W, dest, context, true); + return dest.writeStr(")"); + }, + .non_ts_pseudo_class => |*pseudo| { + return serializePseudoClass(pseudo, W, dest, context); + }, + .pseudo_element => |*pseudo| { + return serializePseudoElement(pseudo, W, dest, context); + }, + .nesting => { + return serializeNesting(W, dest, context, false); + }, + .class => |class| { + try dest.writeChar('.'); + return dest.writeIdent(class.v, true); + }, + .id => |id| { + try dest.writeChar('#'); + return dest.writeIdent(id.v, true); + }, + .host => |selector| { + try dest.writeStr(":host"); + if (selector) |*sel| { + try dest.writeChar('('); + try serializeSelector(sel, W, dest, dest.context(), false); + try dest.writeChar(')'); + } + return; + }, + .slotted => |*selector| { + try dest.writeStr("::slotted("); + try serializeSelector(selector, W, dest, dest.context(), false); + try dest.writeChar(')'); + }, + // .nth => |nth_data| { + // try nth_data.writeStart(W, dest, nth_data.isFunction()); + // if (nth_data.isFunction()) { + // try nth_data.writeAffine(W, dest); + // try dest.writeChar(')'); + // } + // }, + + else => { + try tocss_servo.toCss_Component(component, W, dest); + }, + } + } + + pub fn serializeCombinator( + combinator: *const parser.Combinator, + comptime W: type, + dest: *Printer(W), + ) PrintErr!void { + switch (combinator.*) { + .child => try dest.delim('>', true), + .descendant => try dest.writeStr(" "), + .next_sibling => try dest.delim('+', true), + .later_sibling => try dest.delim('~', true), + .deep => try dest.writeStr(" /deep/ "), + .deep_descendant => { + try dest.whitespace(); + try dest.writeStr(">>>"); + try dest.whitespace(); + }, + .pseudo_element, .part, .slot_assignment => return, + } + } + + pub fn serializePseudoClass( + pseudo_class: *const parser.PseudoClass, + comptime W: type, + dest: *Printer(W), + context: ?*const css.StyleContext, + ) PrintErr!void { + switch (pseudo_class.*) { + .lang => { + try dest.writeStr(":lang("); + var first = true; + for (pseudo_class.lang.languages.items) |lang| { + if (first) { + first = false; + } else { + try dest.delim(',', false); + } + css.serializer.serializeIdentifier(lang, dest) catch return dest.addFmtError(); + } + return dest.writeStr(")"); + }, + .dir => { + const dir = pseudo_class.dir.direction; + try dest.writeStr(":dir("); + try dir.toCss(W, dest); + return try dest.writeStr(")"); + }, + else => {}, + } + + const Helpers = struct { + pub inline fn writePrefixed( + d: *Printer(W), + prefix: css.VendorPrefix, + comptime val: []const u8, + ) PrintErr!void { + try d.writeChar(':'); + // If the printer has a vendor prefix override, use that. + const vp = if (!d.vendor_prefix.isEmpty()) + d.vendor_prefix.bitwiseOr(prefix).orNone() + else + prefix; + + try vp.toCss(W, d); + try d.writeStr(val); + } + pub inline fn pseudo( + d: *Printer(W), + comptime key: []const u8, + comptime s: []const u8, + ) PrintErr!void { + const key_snake_case = comptime key_snake_case: { + var buf: [key.len]u8 = undefined; + for (key, 0..) |c, i| { + buf[i] = if (c >= 'A' and c <= 'Z') c + 32 else if (c == '-') '_' else c; + } + const buf2 = buf; + break :key_snake_case buf2; + }; + const _class = if (d.pseudo_classes) |*pseudo_classes| @field(pseudo_classes, &key_snake_case) else null; + + if (_class) |class| { + try d.writeChar('.'); + try d.writeIdent(class, true); + } else { + try d.writeStr(s); + } + } + }; + + switch (pseudo_class.*) { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + .hover => try Helpers.pseudo(dest, "hover", ":hover"), + .active => try Helpers.pseudo(dest, "active", ":active"), + .focus => try Helpers.pseudo(dest, "focus", ":focus"), + .focus_visible => try Helpers.pseudo(dest, "focus-visible", ":focus-visible"), + .focus_within => try Helpers.pseudo(dest, "focus-within", ":focus-within"), + + // https://drafts.csswg.org/selectors-4/#time-pseudos + .current => try dest.writeStr(":current"), + .past => try dest.writeStr(":past"), + .future => try dest.writeStr(":future"), + + // https://drafts.csswg.org/selectors-4/#resource-pseudos + .playing => try dest.writeStr(":playing"), + .paused => try dest.writeStr(":paused"), + .seeking => try dest.writeStr(":seeking"), + .buffering => try dest.writeStr(":buffering"), + .stalled => try dest.writeStr(":stalled"), + .muted => try dest.writeStr(":muted"), + .volume_locked => try dest.writeStr(":volume-locked"), + + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + .fullscreen => |prefix| { + try dest.writeChar(':'); + const vp = if (!dest.vendor_prefix.isEmpty()) + dest.vendor_prefix.bitwiseAnd(prefix).orNone() + else + prefix; + try vp.toCss(W, dest); + if (vp.webkit or vp.moz) { + try dest.writeStr("full-screen"); + } else { + try dest.writeStr("fullscreen"); + } + }, + + // https://drafts.csswg.org/selectors/#display-state-pseudos + .open => try dest.writeStr(":open"), + .closed => try dest.writeStr(":closed"), + .modal => try dest.writeStr(":modal"), + .picture_in_picture => try dest.writeStr(":picture-in-picture"), + + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open + .popover_open => try dest.writeStr(":popover-open"), + + // https://drafts.csswg.org/selectors-4/#the-defined-pseudo + .defined => try dest.writeStr(":defined"), + + // https://drafts.csswg.org/selectors-4/#location + .any_link => |prefix| try Helpers.writePrefixed(dest, prefix, "any-link"), + .link => try dest.writeStr(":link"), + .local_link => try dest.writeStr(":local-link"), + .target => try dest.writeStr(":target"), + .target_within => try dest.writeStr(":target-within"), + .visited => try dest.writeStr(":visited"), + + // https://drafts.csswg.org/selectors-4/#input-pseudos + .enabled => try dest.writeStr(":enabled"), + .disabled => try dest.writeStr(":disabled"), + .read_only => |prefix| try Helpers.writePrefixed(dest, prefix, "read-only"), + .read_write => |prefix| try Helpers.writePrefixed(dest, prefix, "read-write"), + .placeholder_shown => |prefix| try Helpers.writePrefixed(dest, prefix, "placeholder-shown"), + .default => try dest.writeStr(":default"), + .checked => try dest.writeStr(":checked"), + .indeterminate => try dest.writeStr(":indeterminate"), + .blank => try dest.writeStr(":blank"), + .valid => try dest.writeStr(":valid"), + .invalid => try dest.writeStr(":invalid"), + .in_range => try dest.writeStr(":in-range"), + .out_of_range => try dest.writeStr(":out-of-range"), + .required => try dest.writeStr(":required"), + .optional => try dest.writeStr(":optional"), + .user_valid => try dest.writeStr(":user-valid"), + .user_invalid => try dest.writeStr(":user-invalid"), + + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + .autofill => |prefix| try Helpers.writePrefixed(dest, prefix, "autofill"), + + .local => |selector| try serializeSelector(selector.selector, W, dest, context, false), + .global => |selector| { + const css_module = if (dest.css_module) |module| css_module: { + dest.css_module = null; + break :css_module module; + } else null; + try serializeSelector(selector.selector, W, dest, context, false); + dest.css_module = css_module; + }, + + // https://webkit.org/blog/363/styling-scrollbars/ + .webkit_scrollbar => |s| { + try dest.writeStr(switch (s) { + .horizontal => ":horizontal", + .vertical => ":vertical", + .decrement => ":decrement", + .increment => ":increment", + .start => ":start", + .end => ":end", + .double_button => ":double-button", + .single_button => ":single-button", + .no_button => ":no-button", + .corner_present => ":corner-present", + .window_inactive => ":window-inactive", + }); + }, + + .lang => unreachable, + .dir => unreachable, + .custom => |name| { + try dest.writeChar(':'); + return dest.writeStr(name.name); + }, + .custom_function => |v| { + try dest.writeChar(':'); + try dest.writeStr(v.name); + try dest.writeChar('('); + try v.arguments.toCssRaw(W, dest); + try dest.writeChar(')'); + }, + } + } + + pub fn serializePseudoElement( + pseudo_element: *const parser.PseudoElement, + comptime W: type, + dest: *Printer(W), + context: ?*const css.StyleContext, + ) PrintErr!void { + const Helpers = struct { + pub fn writePrefix(d: *Printer(W), prefix: css.VendorPrefix) PrintErr!css.VendorPrefix { + try d.writeStr("::"); + // If the printer has a vendor prefix override, use that. + const vp = if (!d.vendor_prefix.isEmpty()) d.vendor_prefix.bitwiseAnd(prefix).orNone() else prefix; + try vp.toCss(W, d); + return vp; + } + + pub fn writePrefixed(d: *Printer(W), prefix: css.VendorPrefix, comptime val: []const u8) PrintErr!void { + _ = try writePrefix(d, prefix); + try d.writeStr(val); + } + }; + // switch (pseudo_element.*) { + // // CSS2 pseudo elements support a single colon syntax in addition + // // to the more correct double colon for other pseudo elements. + // // We use that here because it's supported everywhere and is shorter. + // .after => try dest.writeStr(":after"), + // .before => try dest.writeStr(":before"), + // .marker => try dest.writeStr(":first-letter"), + // .selection => |prefix| Helpers.writePrefixed(dest, prefix, "selection"), + // .cue => dest.writeStr("::cue"), + // .cue_region => dest.writeStr("::cue-region"), + // .cue_function => |v| { + // dest.writeStr("::cue("); + // try serializeSelector(v.selector, W, dest, context, false); + // try dest.writeChar(')'); + // }, + // } + switch (pseudo_element.*) { + // CSS2 pseudo elements support a single colon syntax in addition + // to the more correct double colon for other pseudo elements. + // We use that here because it's supported everywhere and is shorter. + .after => try dest.writeStr(":after"), + .before => try dest.writeStr(":before"), + .first_line => try dest.writeStr(":first-line"), + .first_letter => try dest.writeStr(":first-letter"), + .marker => try dest.writeStr("::marker"), + .selection => |prefix| try Helpers.writePrefixed(dest, prefix, "selection"), + .cue => try dest.writeStr("::cue"), + .cue_region => try dest.writeStr("::cue-region"), + .cue_function => |v| { + try dest.writeStr("::cue("); + try serializeSelector(v.selector, W, dest, context, false); + try dest.writeChar(')'); + }, + .cue_region_function => |v| { + try dest.writeStr("::cue-region("); + try serializeSelector(v.selector, W, dest, context, false); + try dest.writeChar(')'); + }, + .placeholder => |prefix| { + const vp = try Helpers.writePrefix(dest, prefix); + if (vp.webkit or vp.ms) { + try dest.writeStr("input-placeholder"); + } else { + try dest.writeStr("placeholder"); + } + }, + .backdrop => |prefix| try Helpers.writePrefixed(dest, prefix, "backdrop"), + .file_selector_button => |prefix| { + const vp = try Helpers.writePrefix(dest, prefix); + if (vp.webkit) { + try dest.writeStr("file-upload-button"); + } else if (vp.ms) { + try dest.writeStr("browse"); + } else { + try dest.writeStr("file-selector-button"); + } + }, + .webkit_scrollbar => |s| { + try dest.writeStr(switch (s) { + .scrollbar => "::-webkit-scrollbar", + .button => "::-webkit-scrollbar-button", + .track => "::-webkit-scrollbar-track", + .track_piece => "::-webkit-scrollbar-track-piece", + .thumb => "::-webkit-scrollbar-thumb", + .corner => "::-webkit-scrollbar-corner", + .resizer => "::-webkit-resizer", + }); + }, + .view_transition => try dest.writeStr("::view-transition"), + .view_transition_group => |v| { + try dest.writeStr("::view-transition-group("); + try v.part_name.toCss(W, dest); + try dest.writeChar(')'); + }, + .view_transition_image_pair => |v| { + try dest.writeStr("::view-transition-image-pair("); + try v.part_name.toCss(W, dest); + try dest.writeChar(')'); + }, + .view_transition_old => |v| { + try dest.writeStr("::view-transition-old("); + try v.part_name.toCss(W, dest); + try dest.writeChar(')'); + }, + .view_transition_new => |v| { + try dest.writeStr("::view-transition-new("); + try v.part_name.toCss(W, dest); + try dest.writeChar(')'); + }, + .custom => |val| { + try dest.writeStr("::"); + return dest.writeStr(val.name); + }, + .custom_function => |v| { + const name = v.name; + try dest.writeStr("::"); + try dest.writeStr(name); + try dest.writeChar('('); + try v.arguments.toCssRaw(W, dest); + try dest.writeChar(')'); + }, + } + } + + pub fn serializeNesting( + comptime W: type, + dest: *Printer(W), + context: ?*const css.StyleContext, + first: bool, + ) PrintErr!void { + if (context) |ctx| { + // If there's only one simple selector, just serialize it directly. + // Otherwise, use an :is() pseudo class. + // Type selectors are only allowed at the start of a compound selector, + // so use :is() if that is not the case. + if (ctx.selectors.v.items.len == 1 and + (first or (!hasTypeSelector(&ctx.selectors.v.items[0]) and + isSimple(&ctx.selectors.v.items[0])))) + { + try serializeSelector(&ctx.selectors.v.items[0], W, dest, ctx.parent, false); + } else { + try dest.writeStr(":is("); + try serializeSelectorList(ctx.selectors.v.items, W, dest, ctx.parent, false); + try dest.writeChar(')'); + } + } else { + // If there is no context, we are at the root if nesting is supported. This is equivalent to :scope. + // Otherwise, if nesting is supported, serialize the nesting selector directly. + if (dest.targets.shouldCompileSame(.nesting)) { + try dest.writeStr(":scope"); + } else { + try dest.writeChar('&'); + } + } + } +}; + +const tocss_servo = struct { + pub fn toCss_SelectorList( + selectors: []const parser.Selector, + comptime W: type, + dest: *css.Printer(W), + ) PrintErr!void { + if (selectors.len == 0) { + return; + } + + try tocss_servo.toCss_Selector(&selectors[0], W, dest); + + if (selectors.len > 1) { + for (selectors[1..]) |*selector| { + try dest.writeStr(", "); + try tocss_servo.toCss_Selector(selector, W, dest); + } + } + } + + pub fn toCss_Selector( + selector: *const parser.Selector, + comptime W: type, + dest: *css.Printer(W), + ) PrintErr!void { + // Compound selectors invert the order of their contents, so we need to + // undo that during serialization. + // + // This two-iterator strategy involves walking over the selector twice. + // We could do something more clever, but selector serialization probably + // isn't hot enough to justify it, and the stringification likely + // dominates anyway. + // + // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(), + // which we need for |split|. So we split by combinators on a match-order + // sequence and then reverse. + var combinators = CombinatorIter{ .sel = selector }; + var compound_selectors = CompoundSelectorIter{ .sel = selector }; + + var combinators_exhausted = false; + while (compound_selectors.next()) |compound| { + bun.debugAssert(!combinators_exhausted); + + // https://drafts.csswg.org/cssom/#serializing-selectors + if (compound.len == 0) continue; + + // 1. If there is only one simple selector in the compound selectors + // which is a universal selector, append the result of + // serializing the universal selector to s. + // + // Check if `!compound.empty()` first--this can happen if we have + // something like `... > ::before`, because we store `>` and `::` + // both as combinators internally. + // + // If we are in this case, after we have serialized the universal + // selector, we skip Step 2 and continue with the algorithm. + const can_elide_namespace, const first_non_namespace: usize = if (0 >= compound.len) + .{ true, 0 } + else switch (compound[0]) { + .explicit_any_namespace, .explicit_no_namespace, .namespace => .{ false, 1 }, + .default_namespace => .{ true, 1 }, + else => .{ true, 0 }, + }; + var perform_step_2 = true; + const next_combinator = combinators.next(); + if (first_non_namespace == compound.len - 1) { + // We have to be careful here, because if there is a + // pseudo element "combinator" there isn't really just + // the one simple selector. Technically this compound + // selector contains the pseudo element selector as well + // -- Combinator::PseudoElement, just like + // Combinator::SlotAssignment, don't exist in the + // spec. + if (next_combinator == .pseudo_element and compound[first_non_namespace].asCombinator() == .slot_assignment) { + // do nothing + } else if (compound[first_non_namespace] == .explicit_universal_type) { + // Iterate over everything so we serialize the namespace + // too. + for (compound) |*simple| { + try tocss_servo.toCss_Component(simple, W, dest); + } + // Skip step 2, which is an "otherwise". + perform_step_2 = false; + } else { + // do nothing + } + } + + // 2. Otherwise, for each simple selector in the compound selectors + // that is not a universal selector of which the namespace prefix + // maps to a namespace that is not the default namespace + // serialize the simple selector and append the result to s. + // + // See https://github.com/w3c/csswg-drafts/issues/1606, which is + // proposing to change this to match up with the behavior asserted + // in cssom/serialize-namespaced-type-selectors.html, which the + // following code tries to match. + if (perform_step_2) { + for (compound) |*simple| { + if (simple.* == .explicit_universal_type) { + // Can't have a namespace followed by a pseudo-element + // selector followed by a universal selector in the same + // compound selector, so we don't have to worry about the + // real namespace being in a different `compound`. + if (can_elide_namespace) { + continue; + } + } + try tocss_servo.toCss_Component(simple, W, dest); + } + } + + // 3. If this is not the last part of the chain of the selector + // append a single SPACE (U+0020), followed by the combinator + // ">", "+", "~", ">>", "||", as appropriate, followed by another + // single SPACE (U+0020) if the combinator was not whitespace, to + // s. + if (next_combinator) |c| { + try toCss_Combinator(&c, W, dest); + } else { + combinators_exhausted = true; + } + + // 4. If this is the last part of the chain of the selector and + // there is a pseudo-element, append "::" followed by the name of + // the pseudo-element, to s. + // + // (we handle this above) + } + } + + pub fn toCss_Component( + component: *const parser.Component, + comptime W: type, + dest: *Printer(W), + ) PrintErr!void { + switch (component.*) { + .combinator => |*c| try toCss_Combinator(c, W, dest), + .slotted => |*selector| { + try dest.writeStr("::slotted("); + try tocss_servo.toCss_Selector(selector, W, dest); + try dest.writeChar(')'); + }, + .part => |part_names| { + try dest.writeStr("::part("); + for (part_names, 0..) |name, i| { + if (i != 0) { + try dest.writeChar(' '); + } + try css.IdentFns.toCss(&name, W, dest); + } + try dest.writeChar(')'); + }, + .pseudo_element => |*p| { + try p.toCss(W, dest); + }, + .id => |s| { + try dest.writeChar('#'); + try css.IdentFns.toCss(&s, W, dest); + }, + .class => |s| { + try dest.writeChar('.'); + try css.IdentFns.toCss(&s, W, dest); + }, + .local_name => |local_name| { + try local_name.toCss(W, dest); + }, + .explicit_universal_type => { + try dest.writeChar('*'); + }, + .default_namespace => return, + + .explicit_no_namespace => { + try dest.writeChar('|'); + }, + .explicit_any_namespace => { + try dest.writeStr("*|"); + }, + .namespace => |ns| { + try css.IdentFns.toCss(&ns.prefix, W, dest); + try dest.writeChar('|'); + }, + .attribute_in_no_namespace_exists => |v| { + try dest.writeChar('['); + try css.IdentFns.toCss(&v.local_name, W, dest); + try dest.writeChar(']'); + }, + .attribute_in_no_namespace => |v| { + try dest.writeChar('['); + try css.IdentFns.toCss(&v.local_name, W, dest); + try v.operator.toCss(W, dest); + try css.CSSStringFns.toCss(&v.value, W, dest); + switch (v.case_sensitivity) { + .case_sensitive, .ascii_case_insensitive_if_in_html_element_in_html_document => {}, + .ascii_case_insensitive => try dest.writeStr(" i"), + .explicit_case_sensitive => try dest.writeStr(" s"), + } + try dest.writeChar(']'); + }, + .attribute_other => |attr_selector| { + try attr_selector.toCss(W, dest); + }, + // Pseudo-classes + .root => { + try dest.writeStr(":root"); + }, + .empty => { + try dest.writeStr(":empty"); + }, + .scope => { + try dest.writeStr(":scope"); + }, + .host => |selector| { + try dest.writeStr(":host"); + if (selector) |*sel| { + try dest.writeChar('('); + try tocss_servo.toCss_Selector(sel, W, dest); + try dest.writeChar(')'); + } + }, + .nth => |nth_data| { + try nth_data.writeStart(W, dest, nth_data.isFunction()); + if (nth_data.isFunction()) { + try nth_data.writeAffine(W, dest); + try dest.writeChar(')'); + } + }, + .nth_of => |nth_of_data| { + const nth_data = nth_of_data.nthData(); + try nth_data.writeStart(W, dest, true); + // A selector must be a function to hold An+B notation + bun.debugAssert(nth_data.is_function); + try nth_data.writeAffine(W, dest); + // Only :nth-child or :nth-last-child can be of a selector list + bun.debugAssert(nth_data.ty == .child or nth_data.ty == .last_child); + // The selector list should not be empty + bun.debugAssert(nth_of_data.selectors.len != 0); + try dest.writeStr(" of "); + try tocss_servo.toCss_SelectorList(nth_of_data.selectors, W, dest); + try dest.writeChar(')'); + }, + .is, .where, .negation, .has, .any => { + switch (component.*) { + .where => try dest.writeStr(":where("), + .is => try dest.writeStr(":is("), + .negation => try dest.writeStr(":not("), + .has => try dest.writeStr(":has("), + .any => |v| { + try dest.writeChar(':'); + try v.vendor_prefix.toCss(W, dest); + try dest.writeStr("any("); + }, + else => unreachable, + } + try tocss_servo.toCss_SelectorList( + switch (component.*) { + .where, .is, .negation, .has => |list| list, + .any => |v| v.selectors, + else => unreachable, + }, + W, + dest, + ); + try dest.writeStr(")"); + }, + .non_ts_pseudo_class => |*pseudo| { + try pseudo.toCss(W, dest); + }, + .nesting => try dest.writeChar('&'), + } + } + + pub fn toCss_Combinator( + combinator: *const parser.Combinator, + comptime W: type, + dest: *Printer(W), + ) PrintErr!void { + switch (combinator.*) { + .child => try dest.writeStr(" > "), + .descendant => try dest.writeStr(" "), + .next_sibling => try dest.writeStr(" + "), + .later_sibling => try dest.writeStr(" ~ "), + .deep => try dest.writeStr(" /deep/ "), + .deep_descendant => { + try dest.writeStr(" >>> "); + }, + .pseudo_element, .part, .slot_assignment => return, + } + } + + pub fn toCss_PseudoElement( + pseudo_element: *const parser.PseudoElement, + comptime W: type, + dest: *Printer(W), + ) PrintErr!void { + switch (pseudo_element.*) { + .before => try dest.writeStr("::before"), + .after => try dest.writeStr("::after"), + } + } +}; + +pub fn shouldUnwrapIs(selectors: []const parser.Selector) bool { + if (selectors.len == 1) { + const first = selectors[0]; + if (!hasTypeSelector(&first) and isSimple(&first)) return true; + } + + return false; +} + +fn hasTypeSelector(selector: *const parser.Selector) bool { + var iter = selector.iterRawParseOrderFrom(0); + const first = iter.next(); + + if (isNamespace(if (first) |*f| f else null)) return isTypeSelector(if (iter.next()) |*n| n else null); + + return isTypeSelector(if (first) |*f| f else null); +} + +fn isNamespace(component: ?*const parser.Component) bool { + if (component) |c| return switch (c.*) { + .explicit_any_namespace, .explicit_no_namespace, .namespace, .default_namespace => true, + else => false, + }; + return false; +} + +fn isTypeSelector(component: ?*const parser.Component) bool { + if (component) |c| return switch (c.*) { + .local_name, .explicit_universal_type => true, + else => false, + }; + return false; +} + +fn isSimple(selector: *const parser.Selector) bool { + var iter = selector.iterRawParseOrderFrom(0); + while (iter.next()) |component| { + if (component.isCombinator()) return true; + } + return false; +} + +const CombinatorIter = struct { + sel: *const parser.Selector, + i: usize = 0, + + /// Original source has this iterator defined like so: + /// ```rs + /// selector + /// .iter_raw_match_order() // just returns an iterator + /// .rev() // reverses the iterator + /// .filter_map(|x| x.as_combinator()) // returns only entries which are combinators + /// ``` + pub fn next(this: *@This()) ?parser.Combinator { + while (this.i < this.sel.components.items.len) { + defer this.i += 1; + const combinator = this.sel.components.items[this.sel.components.items.len - 1 - this.i].asCombinator() orelse continue; + return combinator; + } + return null; + } +}; +const CompoundSelectorIter = struct { + sel: *const parser.Selector, + i: usize = 0, + + /// This iterator is basically like doing `selector.components.splitByCombinator()`. + /// + /// For example: + /// ```css + /// div > p.class + /// ``` + /// + /// The iterator would return: + /// ``` + /// First slice: + /// .{ + /// .{ .local_name = "div" } + /// } + /// + /// Second slice: + /// .{ + /// .{ .local_name = "p" }, + /// .{ .class = "class" } + /// } + /// ``` + /// + /// BUT, the selectors are stored in reverse order, so this code needs to split the components backwards. + /// + /// Original source has this iterator defined like so: + /// ```rs + /// selector + /// .iter_raw_match_order() + /// .as_slice() + /// .split(|x| x.is_combinator()) // splits the slice into subslices by elements that match over the predicate + /// .rev() // reverse + /// ``` + pub inline fn next(this: *@This()) ?[]const parser.Component { + while (this.i < this.sel.components.items.len) { + const next_index: ?usize = next_index: { + for (this.i..this.sel.components.items.len) |j| { + if (this.sel.components.items[this.sel.components.items.len - 1 - j].isCombinator()) break :next_index j; + } + break :next_index null; + }; + if (next_index) |combinator_index| { + const start = combinator_index - 1; + const end = this.i; + const slice = this.sel.components.items[this.sel.components.items.len - 1 - start .. this.sel.components.items.len - end]; + this.i = combinator_index + 1; + return slice; + } + const slice = this.sel.components.items[0 .. this.sel.components.items.len - 1 - this.i + 1]; + this.i = this.sel.components.items.len; + return slice; + } + return null; + } +}; diff --git a/src/css/sourcemap.zig b/src/css/sourcemap.zig new file mode 100644 index 0000000000000..57f5cab30ff9a --- /dev/null +++ b/src/css/sourcemap.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("./css_parser.zig"); +pub const css_values = @import("./values/values.zig"); +const DashedIdent = css_values.ident.DashedIdent; +const Ident = css_values.ident.Ident; +pub const Error = css.Error; +const Location = css.Location; +const ArrayList = std.ArrayListUnmanaged; + +pub const SourceMap = struct { + project_root: []const u8, + inner: SourceMapInner, +}; + +pub const SourceMapInner = struct { + sources: ArrayList([]const u8), + sources_content: ArrayList([]const u8), + names: ArrayList([]const u8), + mapping_lines: ArrayList(MappingLine), +}; + +pub const MappingLine = struct { mappings: ArrayList(LineMapping), last_column: u32, is_sorted: bool }; + +pub const LineMapping = struct { generated_column: u32, original: ?OriginalLocation }; + +pub const OriginalLocation = struct { + original_line: u32, + original_column: u32, + source: u32, + name: ?u32, +}; diff --git a/src/css/targets.zig b/src/css/targets.zig new file mode 100644 index 0000000000000..b0d7bd5c4de87 --- /dev/null +++ b/src/css/targets.zig @@ -0,0 +1,126 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; + +pub const css = @import("./css_parser.zig"); + +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const VendorPrefix = css.VendorPrefix; + +/// Target browsers and features to compile. +pub const Targets = struct { + /// Browser targets to compile the CSS for. + browsers: ?Browsers = null, + /// Features that should always be compiled, even when supported by targets. + include: Features = .{}, + /// Features that should never be compiled, even when unsupported by targets. + exclude: Features = .{}, + + pub fn prefixes(this: *const Targets, prefix: css.VendorPrefix, feature: css.prefixes.Feature) css.VendorPrefix { + if (prefix.contains(css.VendorPrefix{ .none = true }) and !this.exclude.contains(css.targets.Features{ .vendor_prefixes = true })) { + if (this.includes(css.targets.Features{ .vendor_prefixes = true })) { + return css.VendorPrefix.all(); + } else { + return if (this.browsers) |b| feature.prefixesFor(b) else prefix; + } + } else { + return prefix; + } + } + + pub fn shouldCompile(this: *const Targets, feature: css.compat.Feature, flag: Features) bool { + return this.include.contains(flag) or (!this.exclude.contains(flag) and !this.isCompatible(feature)); + } + + pub fn shouldCompileSame(this: *const Targets, comptime prop: @Type(.EnumLiteral)) bool { + const compat_feature: css.compat.Feature = prop; + const target_feature: css.targets.Features = target_feature: { + var feature: css.targets.Features = .{}; + @field(feature, @tagName(prop)) = true; + break :target_feature feature; + }; + + return shouldCompile(this, compat_feature, target_feature); + } + + pub fn isCompatible(this: *const Targets, feature: css.compat.Feature) bool { + if (this.browsers) |*targets| { + return feature.isCompatible(targets.*); + } + return true; + } +}; + +/// Autogenerated by build-prefixes.js +/// +/// Browser versions to compile CSS for. +/// +/// Versions are represented as a single 24-bit integer, with one byte +/// per `major.minor.patch` component. +/// +/// # Example +/// +/// This example represents a target of Safari 13.2.0. +/// +/// ``` +/// const Browsers = struct { +/// safari: ?u32 = (13 << 16) | (2 << 8), +/// ..Browsers{} +/// }; +/// ``` +pub const Browsers = struct { + android: ?u32 = null, + chrome: ?u32 = null, + edge: ?u32 = null, + firefox: ?u32 = null, + ie: ?u32 = null, + ios_saf: ?u32 = null, + opera: ?u32 = null, + safari: ?u32 = null, + samsung: ?u32 = null, + pub usingnamespace BrowsersImpl(@This()); +}; + +/// Autogenerated by build-prefixes.js +/// Features to explicitly enable or disable. +pub const Features = packed struct(u32) { + nesting: bool = false, + not_selector_list: bool = false, + dir_selector: bool = false, + lang_selector_list: bool = false, + is_selector: bool = false, + text_decoration_thickness_percent: bool = false, + media_interval_syntax: bool = false, + media_range_syntax: bool = false, + custom_media_queries: bool = false, + clamp_function: bool = false, + color_function: bool = false, + oklab_colors: bool = false, + lab_colors: bool = false, + p3_colors: bool = false, + hex_alpha_colors: bool = false, + space_separated_color_notation: bool = false, + font_family_system_ui: bool = false, + double_position_gradients: bool = false, + vendor_prefixes: bool = false, + logical_properties: bool = false, + __unused: u12 = 0, + + pub const selectors = Features.fromNames(&.{ "nesting", "not_selector_list", "dir_selector", "lang_selector_list", "is_selector" }); + pub const media_queries = Features.fromNames(&.{ "media_interval_syntax", "media_range_syntax", "custom_media_queries" }); + pub const colors = Features.fromNames(&.{ "color_function", "oklab_colors", "lab_colors", "p3_colors", "hex_alpha_colors", "space_separated_color_notation" }); + + pub usingnamespace css.Bitflags(@This()); + pub usingnamespace FeaturesImpl(@This()); +}; + +pub fn BrowsersImpl(comptime T: type) type { + _ = T; // autofix + return struct {}; +} + +pub fn FeaturesImpl(comptime T: type) type { + _ = T; // autofix + return struct {}; +} diff --git a/src/css/values/alpha.zig b/src/css/values/alpha.zig new file mode 100644 index 0000000000000..fae50717768e0 --- /dev/null +++ b/src/css/values/alpha.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const DimensionPercentage = css.css_values.percentage.DimensionPercentage; +const LengthPercentage = css.css_values.length.LengthPercentage; +const Length = css.css_values.length.Length; +const Percentage = css.css_values.percentage.Percentage; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const Url = css.css_values.url.Url; +const CSSInteger = css.css_values.number.CSSInteger; +const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +const Angle = css.css_values.angle.Angle; +const Time = css.css_values.time.Time; +const Resolution = css.css_values.resolution.Resolution; +const CustomIdent = css.css_values.ident.CustomIdent; +const CustomIdentFns = css.css_values.ident.CustomIdentFns; +const Ident = css.css_values.ident.Ident; +const NumberOrPercentage = css.css_values.percentage.NumberOrPercentage; + +/// A CSS [``](https://www.w3.org/TR/css-color-4/#typedef-alpha-value), +/// used to represent opacity. +/// +/// Parses either a `` or ``, but is always stored and serialized as a number. +pub const AlphaValue = struct { + v: f32, + + pub fn parse(input: *css.Parser) Result(AlphaValue) { + // For some reason NumberOrPercentage.parse makes zls crash, using this instead. + const val: NumberOrPercentage = @call(.auto, @field(NumberOrPercentage, "parse"), .{input}); + const final = switch (val) { + .percentage => |percent| AlphaValue{ .v = percent.v }, + .number => |num| AlphaValue{ .v = num }, + }; + return .{ .result = final }; + } + + pub fn toCss(this: *const AlphaValue, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + return CSSNumberFns.toCss(&this.v, W, dest); + } +}; diff --git a/src/css/values/angle.zig b/src/css/values/angle.zig new file mode 100644 index 0000000000000..7c9ea9e5f6927 --- /dev/null +++ b/src/css/values/angle.zig @@ -0,0 +1,290 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; + +const Tag = enum(u8) { + deg = 1, + rad = 2, + grad = 4, + turn = 8, +}; + +/// A CSS [``](https://www.w3.org/TR/css-values-4/#angles) value. +/// +/// Angles may be explicit or computed by `calc()`, but are always stored and serialized +/// as their computed value. +pub const Angle = union(Tag) { + /// An angle in degrees. There are 360 degrees in a full circle. + deg: CSSNumber, + /// An angle in radians. There are 2π radians in a full circle. + rad: CSSNumber, + /// An angle in gradians. There are 400 gradians in a full circle. + grad: CSSNumber, + /// An angle in turns. There is 1 turn in a full circle. + turn: CSSNumber, + + // ~toCssImpl + const This = @This(); + + pub fn parse(input: *css.Parser) Result(Angle) { + return Angle.parseInternal(input, false); + } + + fn parseInternal(input: *css.Parser, allow_unitless_zero: bool) Result(Angle) { + if (input.tryParse(Calc(Angle).parse, .{}).asValue()) |calc_value| { + if (calc_value == .value) return .{ .result = calc_value.value.* }; + // Angles are always compatible, so they will always compute to a value. + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + + const location = input.currentSourceLocation(); + const token = switch (input.next()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + switch (token.*) { + .dimension => |*dim| { + const value = dim.num.value; + const unit = dim.unit; + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("deg", unit)) { + return .{ .result = Angle{ .deg = value } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("grad", unit)) { + return .{ .result = Angle{ .grad = value } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("turn", unit)) { + return .{ .result = Angle{ .turn = value } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("rad", unit)) { + return .{ .result = Angle{ .rad = value } }; + } else { + return .{ .err = location.newUnexpectedTokenError(token.*) }; + } + }, + .number => |num| { + if (num.value == 0.0 and allow_unitless_zero) return .{ .result = Angle.zero() }; + }, + else => {}, + } + return .{ .err = location.newUnexpectedTokenError(token.*) }; + } + + pub fn parseWithUnitlessZero(input: *css.Parser) Result(Angle) { + return Angle.parseInternal(input, true); + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + const value, const unit = switch (this.*) { + .deg => |val| .{ val, "deg" }, + .grad => |val| .{ val, "grad" }, + .rad => |val| brk: { + const deg = this.toDegrees(); + + // We print 5 digits of precision by default. + // Switch to degrees if there are an even number of them. + if (css.fract(std.math.round(deg * 100000.0)) == 0) { + break :brk .{ val, "deg" }; + } else { + break :brk .{ val, "rad" }; + } + }, + .turn => |val| .{ val, "turn" }, + }; + css.serializer.serializeDimension(value, unit, W, dest) catch return dest.addFmtError(); + } + + pub fn toCssWithUnitlessZero(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (this.isZero()) { + const v: f32 = 0.0; + try CSSNumberFns.toCss(&v, W, dest); + } else { + return this.toCss(W, dest); + } + } + + pub fn tryFromAngle(angle: Angle) ?This { + return angle; + } + + pub fn tryFromToken(token: *const css.Token) css.Maybe(Angle, void) { + if (token.* == .dimension) { + const value = token.dimension.num.value; + const unit = token.dimension.unit; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "deg")) { + return .{ .result = .{ .deg = value } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "grad")) { + return .{ .result = .{ .grad = value } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "turn")) { + return .{ .result = .{ .turn = value } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "rad")) { + return .{ .result = .{ .rad = value } }; + } + } + return .{ .err = {} }; + } + + /// Returns the angle in radians. + pub fn toRadians(this: *const Angle) CSSNumber { + const RAD_PER_DEG: f32 = std.math.pi / 180.0; + return switch (this.*) { + .deg => |deg| return deg * RAD_PER_DEG, + .rad => |rad| return rad, + .grad => |grad| return grad * 180.0 / 200.0 * RAD_PER_DEG, + .turn => |turn| return turn * 360.0 * RAD_PER_DEG, + }; + } + + /// Returns the angle in degrees. + pub fn toDegrees(this: *const Angle) CSSNumber { + const DEG_PER_RAD: f32 = 180.0 / std.math.pi; + switch (this.*) { + .deg => |deg| return deg, + .rad => |rad| return rad * DEG_PER_RAD, + .grad => |grad| return grad * 180.0 / 200.0, + .turn => |turn| return turn * 360.0, + } + } + + pub fn zero() Angle { + return .{ .deg = 0.0 }; + } + + pub fn isZero(this: *const Angle) bool { + const v = switch (this.*) { + .deg => |deg| deg, + .rad => |rad| rad, + .grad => |grad| grad, + .turn => |turn| turn, + }; + return v == 0.0; + } + + pub fn intoCalc(this: *const Angle, allocator: std.mem.Allocator) Calc(Angle) { + return Calc(Angle){ + .value = bun.create(allocator, Angle, this.*), + }; + } + + pub fn map(this: *const Angle, comptime opfn: *const fn (f32) f32) Angle { + return switch (this.*) { + .deg => |deg| .{ .deg = opfn(deg) }, + .rad => |rad| .{ .rad = opfn(rad) }, + .grad => |grad| .{ .grad = opfn(grad) }, + .turn => |turn| .{ .turn = opfn(turn) }, + }; + } + + pub fn tryMap(this: *const Angle, comptime opfn: *const fn (f32) f32) ?Angle { + return map(this, opfn); + } + + pub fn add(this: Angle, rhs: Angle) Angle { + const addfn = struct { + pub fn add(_: void, a: f32, b: f32) f32 { + return a + b; + } + }; + return Angle.op(&this, &rhs, {}, addfn.add); + } + + pub fn eql(lhs: *const Angle, rhs: *const Angle) bool { + return lhs.toDegrees() == rhs.toDegrees(); + } + + pub fn mulF32(this: Angle, _: std.mem.Allocator, other: f32) Angle { + // return Angle.op(&this, &other, Angle.mulF32); + return switch (this) { + .deg => |v| .{ .deg = v * other }, + .rad => |v| .{ .rad = v * other }, + .grad => |v| .{ .grad = v * other }, + .turn => |v| .{ .turn = v * other }, + }; + } + + pub fn partialCmp(this: *const Angle, other: *const Angle) ?std.math.Order { + return css.generic.partialCmpF32(&this.toDegrees(), &other.toDegrees()); + } + + pub fn tryOp( + this: *const Angle, + other: *const Angle, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) f32, + ) ?Angle { + return Angle.op(this, other, ctx, op_fn); + } + + pub fn tryOpTo( + this: *const Angle, + other: *const Angle, + comptime R: type, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) R, + ) ?R { + return Angle.opTo(this, other, R, ctx, op_fn); + } + + pub fn op( + this: *const Angle, + other: *const Angle, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) f32, + ) Angle { + // PERF: not sure if this is faster + const self_tag: u8 = @intFromEnum(this.*); + const other_tag: u8 = @intFromEnum(this.*); + const DEG: u8 = @intFromEnum(Tag.deg); + const GRAD: u8 = @intFromEnum(Tag.grad); + const RAD: u8 = @intFromEnum(Tag.rad); + const TURN: u8 = @intFromEnum(Tag.turn); + + const switch_val: u8 = self_tag | other_tag; + return switch (switch_val) { + DEG | DEG => Angle{ .deg = op_fn(ctx, this.deg, other.deg) }, + RAD | RAD => Angle{ .rad = op_fn(ctx, this.rad, other.rad) }, + GRAD | GRAD => Angle{ .grad = op_fn(ctx, this.grad, other.grad) }, + TURN | TURN => Angle{ .turn = op_fn(ctx, this.turn, other.turn) }, + else => Angle{ .deg = op_fn(ctx, this.toDegrees(), other.toDegrees()) }, + }; + } + + pub fn opTo( + this: *const Angle, + other: *const Angle, + comptime T: type, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) T, + ) T { + // PERF: not sure if this is faster + const self_tag: u8 = @intFromEnum(this.*); + const other_tag: u8 = @intFromEnum(this.*); + const DEG: u8 = @intFromEnum(Tag.deg); + const GRAD: u8 = @intFromEnum(Tag.grad); + const RAD: u8 = @intFromEnum(Tag.rad); + const TURN: u8 = @intFromEnum(Tag.turn); + + const switch_val: u8 = self_tag | other_tag; + return switch (switch_val) { + DEG | DEG => op_fn(ctx, this.deg, other.deg), + RAD | RAD => op_fn(ctx, this.rad, other.rad), + GRAD | GRAD => op_fn(ctx, this.grad, other.grad), + TURN | TURN => op_fn(ctx, this.turn, other.turn), + else => op_fn(ctx, this.toDegrees(), other.toDegrees()), + }; + } + + pub fn sign(this: *const Angle) f32 { + return switch (this.*) { + .deg, .rad, .grad, .turn => |v| CSSNumberFns.sign(&v), + }; + } +}; + +/// A CSS [``](https://www.w3.org/TR/css-values-4/#typedef-angle-percentage) value. +/// May be specified as either an angle or a percentage that resolves to an angle. +pub const AnglePercentage = css.css_values.percentage.DimensionPercentage(Angle); diff --git a/src/css/values/calc.zig b/src/css/values/calc.zig new file mode 100644 index 0000000000000..176fb6c3b9ce6 --- /dev/null +++ b/src/css/values/calc.zig @@ -0,0 +1,1792 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Angle = css.css_values.angle.Angle; +const Length = css.css_values.length.Length; +const LengthValue = css.css_values.length.LengthValue; +const Percentage = css.css_values.percentage.Percentage; +const DimensionPercentage = css.css_values.percentage.DimensionPercentage; +const Time = css.css_values.time.Time; + +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; + +const eql = css.generic.eql; +const deepClone = css.deepClone; + +pub fn needsDeinit(comptime V: type) bool { + return switch (V) { + Length => true, + DimensionPercentage(Angle) => true, + DimensionPercentage(LengthValue) => true, + Percentage => false, + Angle => false, + Time => false, + f32 => false, + else => @compileError("Can't tell if " ++ @typeName(V) ++ " needs deinit, please add it to the switch statement."), + }; +} + +pub fn needsDeepclone(comptime V: type) bool { + return switch (V) { + Length => true, + DimensionPercentage(Angle) => true, + DimensionPercentage(LengthValue) => true, + Percentage => false, + Angle => false, + Time => false, + f32 => false, + else => @compileError("Can't tell if " ++ @typeName(V) ++ " needs deepclone, please add it to the switch statement."), + }; +} + +/// A mathematical expression used within the `calc()` function. +/// +/// This type supports generic value types. Values such as `Length`, `Percentage`, +/// `Time`, and `Angle` support `calc()` expressions. +pub fn Calc(comptime V: type) type { + const needs_deinit = needsDeinit(V); + const needs_deepclone = needsDeepclone(V); + + return union(Tag) { + /// A literal value. + /// PERF: this pointer feels unnecessary if V is small + value: *V, + /// A literal number. + number: CSSNumber, + /// A sum of two calc expressions. + sum: struct { + left: *Calc(V), + right: *Calc(V), + }, + /// A product of a number and another calc expression. + product: struct { + number: CSSNumber, + expression: *Calc(V), + }, + /// A math function, such as `calc()`, `min()`, or `max()`. + function: *MathFunction(V), + + const Tag = enum(u8) { + /// A literal value. + value = 1, + /// A literal number. + number = 2, + /// A sum of two calc expressions. + sum = 4, + /// A product of a number and another calc expression. + product = 8, + /// A math function, such as `calc()`, `min()`, or `max()`. + function = 16, + }; + + const This = @This(); + + pub fn deepClone(this: *const This, allocator: Allocator) This { + return switch (this.*) { + .value => |v| { + return .{ + .value = bun.create( + allocator, + V, + if (needs_deepclone) v.deepClone(allocator) else v.*, + ), + }; + }, + .number => this.*, + .sum => |sum| { + return .{ .sum = .{ + .left = bun.create(allocator, This, sum.left.deepClone(allocator)), + .right = bun.create(allocator, This, sum.right.deepClone(allocator)), + } }; + }, + .product => |product| { + return .{ + .product = .{ + .number = product.number, + .expression = bun.create(allocator, This, product.expression.deepClone(allocator)), + }, + }; + }, + .function => |function| { + return .{ + .function = bun.create( + allocator, + MathFunction(V), + function.deepClone(allocator), + ), + }; + }, + }; + } + + pub fn deinit(this: *This, allocator: Allocator) void { + return switch (this.*) { + .value => |v| { + if (comptime needs_deinit) { + v.deinit(allocator); + } + allocator.destroy(this.value); + }, + .number => {}, + .sum => |sum| { + sum.left.deinit(allocator); + sum.right.deinit(allocator); + allocator.destroy(sum.left); + allocator.destroy(sum.right); + }, + .product => |product| { + product.expression.deinit(allocator); + allocator.destroy(product.expression); + }, + .function => |function| { + function.deinit(allocator); + allocator.destroy(function); + }, + }; + } + + pub fn eql(this: *const @This(), other: *const @This()) bool { + return switch (this.*) { + .value => |a| return other.* == .value and css.generic.eql(V, a, other.value), + .number => |*a| return other.* == .number and css.generic.eql(f32, a, &other.number), + .sum => |s| return other.* == .sum and s.left.eql(other.sum.left) and s.right.eql(other.sum.right), + .product => |p| return other.* == .product and p.number == other.product.number and p.expression.eql(other.product.expression), + .function => |f| return other.* == .function and f.eql(other.function), + }; + } + + fn mulValueF32(lhs: V, allocator: Allocator, rhs: f32) V { + return switch (V) { + f32 => lhs * rhs, + else => lhs.mulF32(allocator, rhs), + }; + } + + // TODO: addValueOwned + fn addValue(allocator: Allocator, lhs: V, rhs: V) V { + return switch (V) { + f32 => return lhs + rhs, + Angle => return lhs.add(rhs), + // CSSNumber => return lhs.add(rhs), + Length => return lhs.add(allocator, rhs), + Percentage => return lhs.add(allocator, rhs), + Time => return lhs.add(allocator, rhs), + else => lhs.add(allocator, rhs), + }; + } + + // TODO: intoValueOwned + fn intoValue(this: @This(), allocator: std.mem.Allocator) V { + switch (V) { + Angle => return switch (this) { + .value => |v| v.*, + // TODO: give a better error message + else => bun.unreachablePanic("", .{}), + }, + CSSNumber => return switch (this) { + .value => |v| v.*, + .number => |n| n, + // TODO: give a better error message + else => bun.unreachablePanic("", .{}), + }, + Length => return Length{ + .calc = bun.create(allocator, Calc(Length), this), + }, + Percentage => return switch (this) { + .value => |v| v.*, + // TODO: give a better error message + else => bun.unreachablePanic("", .{}), + }, + Time => return switch (this) { + .value => |v| v.*, + // TODO: give a better error message + else => bun.unreachablePanic("", .{}), + }, + DimensionPercentage(LengthValue) => return DimensionPercentage(LengthValue){ .calc = bun.create( + allocator, + Calc(DimensionPercentage(LengthValue)), + this, + ) }, + DimensionPercentage(Angle) => return DimensionPercentage(Angle){ .calc = bun.create( + allocator, + Calc(DimensionPercentage(Angle)), + this, + ) }, + else => @compileError("Unimplemented, intoValue() for V = " ++ @typeName(V)), + } + } + + // TODO: change to addOwned() + pub fn add(this: @This(), allocator: std.mem.Allocator, rhs: @This()) @This() { + if (this == .value and rhs == .value) { + // PERF: we can reuse the allocation here + return .{ .value = bun.create(allocator, V, addValue(allocator, this.value.*, rhs.value.*)) }; + } else if (this == .number and rhs == .number) { + return .{ .number = this.number + rhs.number }; + } else if (this == .value) { + // PERF: we can reuse the allocation here + return .{ .value = bun.create(allocator, V, addValue(allocator, this.value.*, intoValue(rhs, allocator))) }; + } else if (rhs == .value) { + // PERF: we can reuse the allocation here + return .{ .value = bun.create(allocator, V, addValue(allocator, intoValue(this, allocator), rhs.value.*)) }; + } else if (this == .function) { + return This{ + .sum = .{ + .left = bun.create(allocator, This, this), + .right = bun.create(allocator, This, rhs), + }, + }; + } else if (rhs == .function) { + return This{ + .sum = .{ + .left = bun.create(allocator, This, this), + .right = bun.create(allocator, This, rhs), + }, + }; + } else { + return .{ .value = bun.create( + allocator, + V, + addValue(allocator, intoValue(this, allocator), intoValue(rhs, allocator)), + ) }; + } + } + + // TODO: users of this and `parseWith` don't need the pointer and often throwaway heap allocated values immediately + // use temp allocator or something? + pub fn parse(input: *css.Parser) Result(This) { + const Fn = struct { + pub fn parseWithFn(_: void, _: []const u8) ?This { + return null; + } + }; + return parseWith(input, {}, Fn.parseWithFn); + } + + const CalcUnit = enum { + abs, + acos, + asin, + atan, + atan2, + calc, + clamp, + cos, + exp, + hypot, + log, + max, + min, + mod, + pow, + rem, + round, + sign, + sin, + sqrt, + tan, + + pub const Map = bun.ComptimeEnumMap(CalcUnit); + }; + + pub fn parseWith( + input: *css.Parser, + ctx: anytype, + comptime parseIdent: *const fn (@TypeOf(ctx), []const u8) ?This, + ) Result(This) { + const location = input.currentSourceLocation(); + const f = switch (input.expectFunction()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + switch (CalcUnit.Map.getAnyCase(f) orelse return .{ .err = location.newUnexpectedTokenError(.{ .ident = f }) }) { + .calc => { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + return This.parseSum(i, self.ctx, parseIdent); + } + }; + var closure = Closure{ .ctx = ctx }; + const calc = switch (input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (calc == .value or calc == .number) return .{ .result = calc }; + return .{ .result = Calc(V){ + .function = bun.create( + input.allocator(), + MathFunction(V), + MathFunction(V){ .calc = calc }, + ), + } }; + }, + .min => { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(ArrayList(This)) { + return i.parseCommaSeparatedWithCtx(This, self, @This().parseOne); + } + pub fn parseOne(self: *@This(), i: *css.Parser) Result(This) { + return This.parseSum(i, self.ctx, parseIdent); + } + }; + var closure = Closure{ .ctx = ctx }; + var reduced = switch (input.parseNestedBlock(ArrayList(This), &closure, Closure.parseNestedBlockFn)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + // PERF(alloc): i don't like this additional allocation + // can we use stack fallback here if the common case is that there will be 1 argument? + This.reduceArgs(input.allocator(), &reduced, std.math.Order.lt); + // var reduced: ArrayList(This) = This.reduceArgs(&args, std.math.Order.lt); + if (reduced.items.len == 1) { + defer reduced.deinit(input.allocator()); + return .{ .result = reduced.swapRemove(0) }; + } + return .{ .result = This{ + .function = bun.create( + input.allocator(), + MathFunction(V), + MathFunction(V){ .min = reduced }, + ), + } }; + }, + .max => { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(ArrayList(This)) { + return i.parseCommaSeparatedWithCtx(This, self, @This().parseOne); + } + pub fn parseOne(self: *@This(), i: *css.Parser) Result(This) { + return This.parseSum(i, self.ctx, parseIdent); + } + }; + var closure = Closure{ .ctx = ctx }; + var reduced = switch (input.parseNestedBlock(ArrayList(This), &closure, Closure.parseNestedBlockFn)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + // PERF: i don't like this additional allocation + This.reduceArgs(input.allocator(), &reduced, std.math.Order.gt); + // var reduced: ArrayList(This) = This.reduceArgs(&args, std.math.Order.gt); + if (reduced.items.len == 1) { + return .{ .result = reduced.orderedRemove(0) }; + } + return .{ .result = This{ + .function = bun.create( + input.allocator(), + MathFunction(V), + MathFunction(V){ .max = reduced }, + ), + } }; + }, + .clamp => { + const ClosureResult = struct { ?This, This, ?This }; + const Closure = struct { + ctx: @TypeOf(ctx), + + pub fn parseNestedBlock(self: *@This(), i: *css.Parser) Result(ClosureResult) { + const min = switch (This.parseSum(i, self, parseIdentWrapper)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const center = switch (This.parseSum(i, self, parseIdentWrapper)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const max = switch (This.parseSum(i, self, parseIdentWrapper)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ min, center, max } }; + } + + pub fn parseIdentWrapper(self: *@This(), ident: []const u8) ?This { + return parseIdent(self.ctx, ident); + } + }; + var closure = Closure{ + .ctx = ctx, + }; + var min, var center, var max = switch (input.parseNestedBlock( + ClosureResult, + &closure, + Closure.parseNestedBlock, + )) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + // According to the spec, the minimum should "win" over the maximum if they are in the wrong order. + const cmp = if (max != null and max.? == .value and center == .value) + css.generic.partialCmp(V, center.value, max.?.value) + else + null; + + // If center is known to be greater than the maximum, replace it with maximum and remove the max argument. + // Otherwise, if center is known to be less than the maximum, remove the max argument. + if (cmp) |cmp_val| { + if (cmp_val == std.math.Order.gt) { + const val = max.?; + center = val; + max = null; + } else { + min = null; + } + } + + const switch_val: u8 = (@as(u8, @intFromBool(min != null)) << 1) | (@as(u8, @intFromBool(min != null))); + // switch (min, max) + return .{ .result = switch (switch_val) { + 0b00 => center, + 0b10 => This{ + .function = bun.create( + input.allocator(), + MathFunction(V), + MathFunction(V){ + .max = arr2( + input.allocator(), + min.?, + center, + ), + }, + ), + }, + 0b01 => This{ + .function = bun.create( + input.allocator(), + MathFunction(V), + MathFunction(V){ + .min = arr2( + input.allocator(), + max.?, + center, + ), + }, + ), + }, + 0b11 => This{ + .function = bun.create( + input.allocator(), + MathFunction(V), + MathFunction(V){ + .clamp = .{ + .min = min.?, + .center = center, + .max = max.?, + }, + }, + ), + }, + else => unreachable, + } }; + }, + .round => { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + const strategy = if (i.tryParse(RoundingStrategy.parse, .{}).asValue()) |s| brk: { + if (i.expectComma().asErr()) |e| return .{ .err = e }; + break :brk s; + } else RoundingStrategy.default(); + + const OpAndFallbackCtx = struct { + strategy: RoundingStrategy, + + pub fn op(this: *const @This(), a: f32, b: f32) f32 { + return round({}, a, b, this.strategy); + } + + pub fn fallback(this: *const @This(), a: This, b: This) MathFunction(V) { + return MathFunction(V){ + .round = .{ + .strategy = this.strategy, + .value = a, + .interval = b, + }, + }; + } + }; + var ctx_for_op_and_fallback = OpAndFallbackCtx{ + .strategy = strategy, + }; + return This.parseMathFn( + i, + &ctx_for_op_and_fallback, + OpAndFallbackCtx.op, + OpAndFallbackCtx.fallback, + self.ctx, + parseIdent, + ); + } + }; + var closure = Closure{ + .ctx = ctx, + }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn); + }, + .rem => { + const Closure = struct { + ctx: @TypeOf(ctx), + + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + return This.parseMathFn( + i, + {}, + @This().rem, + mathFunctionRem, + self.ctx, + parseIdent, + ); + } + + pub fn rem(_: void, a: f32, b: f32) f32 { + return @mod(a, b); + } + pub fn mathFunctionRem(_: void, a: This, b: This) MathFunction(V) { + return MathFunction(V){ + .rem = .{ + .dividend = a, + .divisor = b, + }, + }; + } + }; + var closure = Closure{ + .ctx = ctx, + }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn); + }, + .mod => { + const Closure = struct { + ctx: @TypeOf(ctx), + + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + return This.parseMathFn( + i, + {}, + @This().modulo, + mathFunctionMod, + self.ctx, + parseIdent, + ); + } + + pub fn modulo(_: void, a: f32, b: f32) f32 { + // return ((a % b) + b) % b; + return @mod((@mod(a, b) + b), b); + } + pub fn mathFunctionMod(_: void, a: This, b: This) MathFunction(V) { + return MathFunction(V){ + .mod_ = .{ + .dividend = a, + .divisor = b, + }, + }; + } + }; + var closure = Closure{ + .ctx = ctx, + }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn); + }, + .sin => { + return This.parseTrig(input, .sin, false, ctx, parseIdent); + }, + .cos => { + return This.parseTrig(input, .cos, false, ctx, parseIdent); + }, + .tan => { + return This.parseTrig(input, .tan, false, ctx, parseIdent); + }, + .asin => { + return This.parseTrig(input, .asin, true, ctx, parseIdent); + }, + .acos => { + return This.parseTrig(input, .acos, true, ctx, parseIdent); + }, + .atan => { + return This.parseTrig(input, .atan, true, ctx, parseIdent); + }, + .atan2 => { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + const res = switch (This.parseAtan2(i, self.ctx, parseIdent)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (css.generic.tryFromAngle(V, res)) |v| { + return .{ .result = This{ + .value = bun.create( + i.allocator(), + V, + v, + ), + } }; + } + + return .{ .err = i.newCustomError(css.ParserError{ .invalid_value = {} }) }; + } + }; + var closure = Closure{ .ctx = ctx }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn); + }, + .pow => { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + const a = switch (This.parseNumeric(i, self.ctx, parseIdent)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + if (i.expectComma().asErr()) |e| return .{ .err = e }; + + const b = switch (This.parseNumeric(i, self.ctx, parseIdent)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + return .{ .result = This{ + .number = std.math.pow(f32, a, b), + } }; + } + }; + var closure = Closure{ .ctx = ctx }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn); + }, + .log => { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + const value = switch (This.parseNumeric(i, self.ctx, parseIdent)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (i.tryParse(css.Parser.expectComma, .{}).isOk()) { + const base = switch (This.parseNumeric(i, self.ctx, parseIdent)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = This{ .number = std.math.log(f32, base, value) } }; + } + return .{ .result = This{ .number = std.math.log(f32, std.math.e, value) } }; + } + }; + var closure = Closure{ .ctx = ctx }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn); + }, + .sqrt => { + return This.parseNumericFn(input, .sqrt, ctx, parseIdent); + }, + .exp => { + return This.parseNumericFn(input, .exp, ctx, parseIdent); + }, + .hypot => { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + var args = switch (i.parseCommaSeparatedWithCtx(This, self, parseOne)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + const val = switch (This.parseHypot(i.allocator(), &args)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + if (val) |v| return .{ .result = v }; + + return .{ .result = This{ + .function = bun.create( + i.allocator(), + MathFunction(V), + MathFunction(V){ .hypot = args }, + ), + } }; + } + + pub fn parseOne(self: *@This(), i: *css.Parser) Result(This) { + return This.parseSum(i, self.ctx, parseIdent); + } + }; + var closure = Closure{ .ctx = ctx }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn); + }, + .abs => { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + const v = switch (This.parseSum(i, self.ctx, parseIdent)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ + .result = if (This.applyMap(&v, i.allocator(), absf)) |vv| vv else This{ + .function = bun.create( + i.allocator(), + MathFunction(V), + MathFunction(V){ .abs = v }, + ), + }, + }; + } + }; + var closure = Closure{ .ctx = ctx }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn); + }, + .sign => { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + const v = switch (This.parseSum(i, self.ctx, parseIdent)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + switch (v) { + .number => |*n| return .{ .result = This{ .number = std.math.sign(n.*) } }, + .value => |v2| { + const MapFn = struct { + pub fn sign(s: f32) f32 { + return std.math.sign(s); + } + }; + // First map so we ignore percentages, which must be resolved to their + // computed value in order to determine the sign. + if (css.generic.tryMap(V, v2, MapFn.sign)) |new_v| { + // sign() alwasy resolves to a number. + return .{ + .result = This{ + // .number = css.generic.trySign(V, &new_v) orelse bun.unreachablePanic("sign always resolved to a number.", .{}), + .number = css.generic.trySign(V, &new_v) orelse @panic("sign() always resolves to a number."), + }, + }; + } + }, + else => {}, + } + + return .{ .result = This{ + .function = bun.create( + i.allocator(), + MathFunction(V), + MathFunction(V){ .sign = v }, + ), + } }; + } + }; + var closure = Closure{ .ctx = ctx }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn); + }, + } + } + + pub fn parseNumericFn(input: *css.Parser, comptime op: enum { sqrt, exp }, ctx: anytype, comptime parse_ident: *const fn (@TypeOf(ctx), []const u8) ?This) Result(This) { + const Closure = struct { ctx: @TypeOf(ctx) }; + var closure = Closure{ .ctx = ctx }; + return input.parseNestedBlock(This, &closure, struct { + pub fn parseNestedBlockFn(self: *Closure, i: *css.Parser) Result(This) { + const v = switch (This.parseNumeric(i, self.ctx, parse_ident)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + return .{ + .result = Calc(V){ + .number = switch (op) { + .sqrt => std.math.sqrt(v), + .exp => std.math.exp(v), + }, + }, + }; + } + }.parseNestedBlockFn); + } + + pub fn parseMathFn( + input: *css.Parser, + ctx_for_op_and_fallback: anytype, + comptime op: *const fn (@TypeOf(ctx_for_op_and_fallback), f32, f32) f32, + comptime fallback: *const fn (@TypeOf(ctx_for_op_and_fallback), This, This) MathFunction(V), + ctx_for_parse_ident: anytype, + comptime parse_ident: *const fn (@TypeOf(ctx_for_parse_ident), []const u8) ?This, + ) Result(This) { + const a = switch (This.parseSum(input, ctx_for_parse_ident, parse_ident)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + const b = switch (This.parseSum(input, ctx_for_parse_ident, parse_ident)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + const val = This.applyOp(&a, &b, input.allocator(), ctx_for_op_and_fallback, op) orelse This{ + .function = bun.create( + input.allocator(), + MathFunction(V), + fallback(ctx_for_op_and_fallback, a, b), + ), + }; + + return .{ .result = val }; + } + + pub fn parseSum( + input: *css.Parser, + ctx: anytype, + comptime parse_ident: *const fn (@TypeOf(ctx), []const u8) ?This, + ) Result(This) { + var cur = switch (This.parseProduct(input, ctx, parse_ident)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + while (true) { + const start = input.state(); + const tok = switch (input.nextIncludingWhitespace()) { + .result => |vv| vv, + .err => { + input.reset(&start); + break; + }, + }; + + if (tok.* == .whitespace) { + if (input.isExhausted()) { + break; // allow trailing whitespace + } + const next_tok = switch (input.next()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (next_tok.* == .delim and next_tok.delim == '+') { + const next = switch (Calc(V).parseProduct(input, ctx, parse_ident)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + cur = cur.add(input.allocator(), next); + } else if (next_tok.* == .delim and next_tok.delim == '-') { + var rhs = switch (This.parseProduct(input, ctx, parse_ident)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + rhs = rhs.mulF32(input.allocator(), -1.0); + cur = cur.add(input.allocator(), rhs); + } else { + return .{ .err = input.newUnexpectedTokenError(next_tok.*) }; + } + continue; + } + input.reset(&start); + break; + } + + return .{ .result = cur }; + } + + pub fn parseProduct( + input: *css.Parser, + ctx: anytype, + comptime parse_ident: *const fn (@TypeOf(ctx), []const u8) ?This, + ) Result(This) { + var node = switch (This.parseValue(input, ctx, parse_ident)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + while (true) { + const start = input.state(); + const tok = switch (input.next()) { + .result => |vv| vv, + .err => { + input.reset(&start); + break; + }, + }; + + if (tok.* == .delim and tok.delim == '*') { + // At least one of the operands must be a number. + const rhs = switch (This.parseValue(input, ctx, parse_ident)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (rhs == .number) { + node = node.mulF32(input.allocator(), rhs.number); + } else if (node == .number) { + const val = node.number; + node = rhs; + node = node.mulF32(input.allocator(), val); + } else { + return .{ .err = input.newUnexpectedTokenError(.{ .delim = '*' }) }; + } + } else if (tok.* == .delim and tok.delim == '/') { + const rhs = switch (This.parseValue(input, ctx, parse_ident)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (rhs == .number) { + const val = rhs.number; + node = node.mulF32(input.allocator(), 1.0 / val); + continue; + } + return .{ .err = input.newCustomError(css.ParserError{ .invalid_value = {} }) }; + } else { + input.reset(&start); + break; + } + } + return .{ .result = node }; + } + + pub fn parseValue( + input: *css.Parser, + ctx: anytype, + comptime parse_ident: *const fn (@TypeOf(ctx), []const u8) ?This, + ) Result(This) { + // Parse nested calc() and other math functions. + if (input.tryParse(This.parse, .{}).asValue()) |_calc| { + const calc: This = _calc; + switch (calc) { + .function => |f| return switch (f.*) { + .calc => |c| .{ .result = c }, + else => .{ .result = .{ .function = f } }, + }, + else => return .{ .result = calc }, + } + } + + if (input.tryParse(css.Parser.expectParenthesisBlock, .{}).isOk()) { + const Closure = struct { + ctx: @TypeOf(ctx), + pub fn parseNestedBlockFn(self: *@This(), i: *css.Parser) Result(This) { + return This.parseSum(i, self.ctx, parse_ident); + } + }; + var closure = Closure{ + .ctx = ctx, + }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBlockFn); + } + + if (input.tryParse(css.Parser.expectNumber, .{}).asValue()) |num| { + return .{ .result = .{ .number = num } }; + } + + if (input.tryParse(Constant.parse, .{}).asValue()) |constant| { + return .{ .result = .{ .number = constant.intoF32() } }; + } + + const location = input.currentSourceLocation(); + if (input.tryParse(css.Parser.expectIdent, .{}).asValue()) |ident| { + if (parse_ident(ctx, ident)) |c| { + return .{ .result = c }; + } + + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + } + + const value = switch (input.tryParse(css.generic.parseFor(V), .{})) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ + .value = bun.create( + input.allocator(), + V, + value, + ), + } }; + } + + pub fn parseTrig( + input: *css.Parser, + comptime trig_fn_kind: enum { + sin, + cos, + tan, + asin, + acos, + atan, + }, + to_angle: bool, + ctx: anytype, + comptime parse_ident: *const fn (@TypeOf(ctx), []const u8) ?This, + ) Result(This) { + const trig_fn = struct { + pub fn run(x: f32) f32 { + const mathfn = comptime switch (trig_fn_kind) { + .sin => std.math.sin, + .cos => std.math.cos, + .tan => std.math.tan, + .asin => std.math.asin, + .acos => std.math.acos, + .atan => std.math.atan, + }; + return mathfn(x); + } + }; + const Closure = struct { + ctx: @TypeOf(ctx), + to_angle: bool, + + pub fn parseNestedBockFn(this: *@This(), i: *css.Parser) Result(This) { + const v = switch (Calc(Angle).parseSum( + i, + this, + @This().parseIdentFn, + )) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + const rad = rad: { + switch (v) { + .value => |angle| { + if (!this.to_angle) break :rad trig_fn.run(angle.toRadians()); + }, + .number => break :rad trig_fn.run(v.number), + else => {}, + } + return .{ .err = i.newCustomError(css.ParserError{ .invalid_value = {} }) }; + }; + + if (this.to_angle and !std.math.isNan(rad)) { + if (css.generic.tryFromAngle(V, .{ .rad = rad })) |val| { + return .{ .result = .{ + .value = bun.create( + i.allocator(), + V, + val, + ), + } }; + } + return .{ .err = i.newCustomError(css.ParserError{ .invalid_value = {} }) }; + } else { + return .{ .result = .{ .number = rad } }; + } + } + + pub fn parseIdentFn(this: *@This(), ident: []const u8) ?Calc(Angle) { + const v = parse_ident(this.ctx, ident) orelse return null; + if (v == .number) return .{ .number = v.number }; + return null; + } + }; + var closure = Closure{ + .ctx = ctx, + .to_angle = to_angle, + }; + return input.parseNestedBlock(This, &closure, Closure.parseNestedBockFn); + } + + pub fn ParseIdentNone(comptime Ctx: type, comptime Value: type) type { + return struct { + pub fn func(_: Ctx, _: []const u8) ?Calc(Value) { + return null; + } + }; + } + + pub fn parseAtan2( + input: *css.Parser, + ctx: anytype, + comptime parse_ident: *const fn (@TypeOf(ctx), []const u8) ?This, + ) Result(Angle) { + const Ctx = @TypeOf(ctx); + + // atan2 supports arguments of any , , or , even ones that wouldn't + // normally be supported by V. The only requirement is that the arguments be of the same type. + // Try parsing with each type, and return the first one that parses successfully. + if (tryParseAtan2Args(Ctx, Length, input, ctx).asValue()) |v| { + return .{ .result = v }; + } + + if (tryParseAtan2Args(Ctx, Percentage, input, ctx).asValue()) |v| { + return .{ .result = v }; + } + + if (tryParseAtan2Args(Ctx, Angle, input, ctx).asValue()) |v| { + return .{ .result = v }; + } + + if (tryParseAtan2Args(Ctx, Time, input, ctx).asValue()) |v| { + return .{ .result = v }; + } + + const Closure = struct { + ctx: @TypeOf(ctx), + + pub fn parseIdentFn(self: *@This(), ident: []const u8) ?Calc(CSSNumber) { + const v = parse_ident(self.ctx, ident) orelse return null; + if (v == .number) return .{ .number = v.number }; + return null; + } + }; + var closure = Closure{ + .ctx = ctx, + }; + return Calc(CSSNumber).parseAtan2Args(input, &closure, Closure.parseIdentFn); + } + + inline fn tryParseAtan2Args( + comptime Ctx: type, + comptime Value: type, + input: *css.Parser, + ctx: Ctx, + ) Result(Angle) { + const func = ParseIdentNone(Ctx, Value).func; + return input.tryParseImpl(Result(Angle), Calc(Value).parseAtan2Args, .{ input, ctx, func }); + } + + pub fn parseAtan2Args( + input: *css.Parser, + ctx: anytype, + comptime parse_ident: *const fn (@TypeOf(ctx), []const u8) ?This, + ) Result(Angle) { + const a = switch (This.parseSum(input, ctx, parse_ident)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + const b = switch (This.parseSum(input, ctx, parse_ident)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + if (a == .value and b == .value) { + const Fn = struct { + pub fn opToFn(_: void, x: f32, y: f32) Angle { + return .{ .rad = std.math.atan2(x, y) }; + } + }; + if (css.generic.tryOpTo(V, Angle, a.value, b.value, {}, Fn.opToFn)) |v| { + return .{ .result = v }; + } + } else if (a == .number and b == .number) { + return .{ .result = Angle{ .rad = std.math.atan2(a.number, b.number) } }; + } else { + // doo nothing + } + + // We don't have a way to represent arguments that aren't angles, so just error. + // This will fall back to an unparsed property, leaving the atan2() function intact. + return .{ .err = input.newCustomError(css.ParserError{ .invalid_value = {} }) }; + } + + pub fn parseNumeric( + input: *css.Parser, + ctx: anytype, + comptime parse_ident: *const fn (@TypeOf(ctx), []const u8) ?This, + ) Result(f32) { + const Closure = struct { + ctx: @TypeOf(ctx), + + pub fn parseIdentFn(self: *@This(), ident: []const u8) ?Calc(CSSNumber) { + const v = parse_ident(self.ctx, ident) orelse return null; + if (v == .number) return .{ .number = v.number }; + return null; + } + }; + var closure = Closure{ + .ctx = ctx, + }; + const v: Calc(CSSNumber) = switch (Calc(CSSNumber).parseSum(input, &closure, Closure.parseIdentFn)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + const val = switch (v) { + .number => v.number, + .value => v.value.*, + else => return .{ .err = input.newCustomError(css.ParserError.invalid_value) }, + }; + return .{ .result = val }; + } + + pub fn parseHypot(allocator: Allocator, args: *ArrayList(This)) Result(?This) { + if (args.items.len == 1) { + const v = args.items[0]; + args.items[0] = This{ .number = 0 }; + return .{ .result = v }; + } + + if (args.items.len == 2) { + return .{ .result = This.applyOp(&args.items[0], &args.items[1], allocator, {}, hypot) }; + } + + var i: usize = 0; + const first = if (This.applyMap( + &args.items[0], + allocator, + powi2, + )) |v| v else return .{ .result = null }; + i += 1; + var errored: bool = false; + var sum: This = first; + for (args.items[i..]) |*arg| { + const Fn = struct { + pub fn applyOpFn(_: void, a: f32, b: f32) f32 { + return a + std.math.pow(f32, b, 2); + } + }; + sum = This.applyOp(&sum, arg, allocator, {}, Fn.applyOpFn) orelse { + errored = true; + break; + }; + } + + if (errored) return .{ .result = null }; + + return .{ .result = This.applyMap(&sum, allocator, sqrtf32) }; + } + + pub fn applyOp( + a: *const This, + b: *const This, + allocator: std.mem.Allocator, + ctx: anytype, + comptime op: *const fn (@TypeOf(ctx), f32, f32) f32, + ) ?This { + if (a.* == .value and b.* == .value) { + if (css.generic.tryOp(V, a.value, b.value, ctx, op)) |v| { + return This{ + .value = bun.create( + allocator, + V, + v, + ), + }; + } + return null; + } + + if (a.* == .number and b.* == .number) { + return This{ + .number = op(ctx, a.number, b.number), + }; + } + + return null; + } + + pub fn applyMap(this: *const This, allocator: Allocator, comptime op: *const fn (f32) f32) ?This { + switch (this.*) { + .number => |n| return This{ .number = op(n) }, + .value => |v| { + if (css.generic.tryMap(V, v, op)) |new_v| { + return This{ + .value = bun.create( + allocator, + V, + new_v, + ), + }; + } + }, + else => {}, + } + + return null; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + const was_in_calc = dest.in_calc; + dest.in_calc = true; + + const res = toCssImpl(this, W, dest); + + dest.in_calc = was_in_calc; + return res; + } + + pub fn toCssImpl(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + return switch (this.*) { + .value => |v| v.toCss(W, dest), + .number => |n| CSSNumberFns.toCss(&n, W, dest), + .sum => |sum| { + const a = sum.left; + const b = sum.right; + try a.toCss(W, dest); + // White space is always required. + if (b.isSignNegative()) { + try dest.writeStr(" - "); + var b2 = b.deepClone(dest.allocator).mulF32(dest.allocator, -1.0); + defer b2.deinit(dest.allocator); + try b2.toCss(W, dest); + } else { + try dest.writeStr(" + "); + try b.toCss(W, dest); + } + return; + }, + .product => { + const num = this.product.number; + const calc = this.product.expression; + if (@abs(num) < 1.0) { + const div = 1.0 / num; + try calc.toCss(W, dest); + try dest.delim('/', true); + try CSSNumberFns.toCss(&div, W, dest); + } else { + try CSSNumberFns.toCss(&num, W, dest); + try dest.delim('*', true); + try calc.toCss(W, dest); + } + }, + .function => |f| return f.toCss(W, dest), + }; + } + + pub fn trySign(this: *const @This()) ?f32 { + return switch (this.*) { + .value => |v| return switch (V) { + f32 => css.signfns.signF32(v), + else => v.trySign(), + }, + .number => |n| css.signfns.signF32(n), + else => null, + }; + } + + pub fn isSignNegative(this: *const @This()) bool { + return css.signfns.isSignNegative(this.trySign() orelse return false); + } + + pub fn mulF32(this: @This(), allocator: Allocator, other: f32) This { + if (other == 1.0) { + return this; + } + + return switch (this) { + // PERF: why not reuse the allocation here? + .value => This{ .value = bun.create(allocator, V, mulValueF32(this.value.*, allocator, other)) }, + .number => This{ .number = this.number * other }, + // PERF: why not reuse the allocation here? + .sum => This{ .sum = .{ + .left = bun.create( + allocator, + This, + this.sum.left.mulF32(allocator, other), + ), + .right = bun.create( + allocator, + This, + this.sum.right.mulF32(allocator, other), + ), + } }, + .product => { + const num = this.product.number * other; + if (num == 1.0) { + return this.product.expression.*; + } + return This{ + .product = .{ + .number = num, + .expression = this.product.expression, + }, + }; + }, + .function => switch (this.function.*) { + // PERF: why not reuse the allocation here? + .calc => This{ + .function = bun.create( + allocator, + MathFunction(V), + MathFunction(V){ + .calc = this.function.calc.mulF32(allocator, other), + }, + ), + }, + else => This{ + .product = .{ + .number = other, + .expression = bun.create(allocator, This, this), + }, + }, + }, + }; + } + + /// PERF: + /// I don't like how this function requires allocating a second ArrayList + /// I am pretty sure we could do this reduction in place, or do it as the + /// arguments are being parsed. + fn reduceArgs(allocator: Allocator, args: *ArrayList(This), order: std.math.Order) void { + // Reduces the arguments of a min() or max() expression, combining compatible values. + // e.g. min(1px, 1em, 2px, 3in) => min(1px, 1em) + var reduced = ArrayList(This){}; + + for (args.items) |*arg| { + var found: ??*Calc(V) = null; + switch (arg.*) { + .value => |val| { + for (reduced.items) |*b| { + switch (b.*) { + .value => |v| { + const result = css.generic.partialCmp(V, val, v); + if (result != null) { + if (result == order) { + found = b; + break; + } else { + found = @as(?*Calc(V), null); + break; + } + } + }, + else => {}, + } + } + }, + else => {}, + } + + if (found) |__r| { + if (__r) |r| { + r.* = arg.*; + // set to dummy value since we moved it into `reduced` + arg.* = This{ .number = 420 }; + continue; + } + } else { + reduced.append(allocator, arg.*) catch bun.outOfMemory(); + // set to dummy value since we moved it into `reduced` + arg.* = This{ .number = 420 }; + continue; + } + arg.deinit(allocator); + arg.* = This{ .number = 420 }; + } + + css.deepDeinit(This, allocator, args); + args.* = reduced; + } + }; +} + +/// A CSS math function. +/// +/// Math functions may be used in most properties and values that accept numeric +/// values, including lengths, percentages, angles, times, etc. +pub fn MathFunction(comptime V: type) type { + return union(enum) { + /// The `calc()` function. + calc: Calc(V), + /// The `min()` function. + min: ArrayList(Calc(V)), + /// The `max()` function. + max: ArrayList(Calc(V)), + /// The `clamp()` function. + clamp: struct { + min: Calc(V), + center: Calc(V), + max: Calc(V), + }, + /// The `round()` function. + round: struct { + strategy: RoundingStrategy, + value: Calc(V), + interval: Calc(V), + }, + /// The `rem()` function. + rem: struct { + dividend: Calc(V), + divisor: Calc(V), + }, + /// The `mod()` function. + mod_: struct { + dividend: Calc(V), + divisor: Calc(V), + }, + /// The `abs()` function. + abs: Calc(V), + /// The `sign()` function. + sign: Calc(V), + /// The `hypot()` function. + hypot: ArrayList(Calc(V)), + + pub fn eql(this: *const @This(), other: *const @This()) bool { + return switch (this.*) { + .calc => |a| return other.* == .calc and a.eql(&other.calc), + .min => |*a| return other.* == .min and css.generic.eqlList(Calc(V), a, &other.min), + .max => |*a| return other.* == .max and css.generic.eqlList(Calc(V), a, &other.max), + .clamp => |*a| return other.* == .clamp and a.min.eql(&other.clamp.min) and a.center.eql(&other.clamp.center) and a.max.eql(&other.clamp.max), + .round => |*a| return other.* == .round and a.strategy == other.round.strategy and a.value.eql(&other.round.value) and a.interval.eql(&other.round.interval), + .rem => |*a| return other.* == .rem and a.dividend.eql(&other.rem.dividend) and a.divisor.eql(&other.rem.divisor), + .mod_ => |*a| return other.* == .mod_ and a.dividend.eql(&other.mod_.dividend) and a.divisor.eql(&other.mod_.divisor), + .abs => |*a| return other.* == .abs and a.eql(&other.abs), + .sign => |*a| return other.* == .sign and a.eql(&other.sign), + .hypot => |*a| return other.* == .hypot and css.generic.eqlList(Calc(V), a, &other.hypot), + }; + } + + pub fn deepClone(this: *const @This(), allocator: Allocator) @This() { + return switch (this.*) { + .calc => |*calc| .{ .calc = calc.deepClone(allocator) }, + .min => |*min| .{ .min = css.deepClone(Calc(V), allocator, min) }, + .max => |*max| .{ .max = css.deepClone(Calc(V), allocator, max) }, + .clamp => |*clamp| .{ + .clamp = .{ + .min = clamp.min.deepClone(allocator), + .center = clamp.center.deepClone(allocator), + .max = clamp.max.deepClone(allocator), + }, + }, + .round => |*rnd| .{ .round = .{ + .strategy = rnd.strategy, + .value = rnd.value.deepClone(allocator), + .interval = rnd.interval.deepClone(allocator), + } }, + .rem => |*rem| .{ .rem = .{ + .dividend = rem.dividend.deepClone(allocator), + .divisor = rem.divisor.deepClone(allocator), + } }, + .mod_ => |*mod_| .{ .mod_ = .{ + .dividend = mod_.dividend.deepClone(allocator), + .divisor = mod_.divisor.deepClone(allocator), + } }, + .abs => |*abs| .{ .abs = abs.deepClone(allocator) }, + .sign => |*sign| .{ .sign = sign.deepClone(allocator) }, + .hypot => |*hyp| .{ + .hypot = css.deepClone(Calc(V), allocator, hyp), + }, + }; + } + + pub fn deinit(this: *@This(), allocator: Allocator) void { + switch (this.*) { + .calc => |*calc| calc.deinit(allocator), + .min => |*min| css.deepDeinit(Calc(V), allocator, min), + .max => |*max| css.deepDeinit(Calc(V), allocator, max), + .clamp => |*clamp| { + clamp.min.deinit(allocator); + clamp.center.deinit(allocator); + clamp.max.deinit(allocator); + }, + .round => |*rnd| { + rnd.value.deinit(allocator); + rnd.interval.deinit(allocator); + }, + .rem => |*rem| { + rem.dividend.deinit(allocator); + rem.divisor.deinit(allocator); + }, + .mod_ => |*mod_| { + mod_.dividend.deinit(allocator); + mod_.divisor.deinit(allocator); + }, + .abs => |*abs| { + abs.deinit(allocator); + }, + .sign => |*sign| { + sign.deinit(allocator); + }, + .hypot => |*hyp| { + css.deepDeinit(Calc(V), allocator, hyp); + }, + } + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + return switch (this.*) { + .calc => |*calc| { + try dest.writeStr("calc("); + try calc.toCss(W, dest); + try dest.writeChar(')'); + }, + .min => |*args| { + try dest.writeStr("min("); + var first = true; + for (args.items) |*arg| { + if (first) { + first = false; + } else { + try dest.delim(',', false); + } + try arg.toCss(W, dest); + } + try dest.writeChar(')'); + }, + .max => |*args| { + try dest.writeStr("max("); + var first = true; + for (args.items) |*arg| { + if (first) { + first = false; + } else { + try dest.delim(',', false); + } + try arg.toCss(W, dest); + } + try dest.writeChar(')'); + }, + .clamp => |*clamp| { + try dest.writeStr("clamp("); + try clamp.min.toCss(W, dest); + try dest.delim(',', false); + try clamp.center.toCss(W, dest); + try dest.delim(',', false); + try clamp.max.toCss(W, dest); + try dest.writeChar(')'); + }, + .round => |*rnd| { + try dest.writeStr("round("); + if (rnd.strategy != RoundingStrategy.default()) { + try rnd.strategy.toCss(W, dest); + try dest.delim(',', false); + } + try rnd.value.toCss(W, dest); + try dest.delim(',', false); + try rnd.interval.toCss(W, dest); + try dest.writeChar(')'); + }, + .rem => |*rem| { + try dest.writeStr("rem("); + try rem.dividend.toCss(W, dest); + try dest.delim(',', false); + try rem.divisor.toCss(W, dest); + try dest.writeChar(')'); + }, + .mod_ => |*mod_| { + try dest.writeStr("mod("); + try mod_.dividend.toCss(W, dest); + try dest.delim(',', false); + try mod_.divisor.toCss(W, dest); + try dest.writeChar(')'); + }, + .abs => |*v| { + try dest.writeStr("abs("); + try v.toCss(W, dest); + try dest.writeChar(')'); + }, + .sign => |*v| { + try dest.writeStr("sign("); + try v.toCss(W, dest); + try dest.writeChar(')'); + }, + .hypot => |*args| { + try dest.writeStr("hypot("); + var first = true; + for (args.items) |*arg| { + if (first) { + first = false; + } else { + try dest.delim(',', false); + } + try arg.toCss(W, dest); + } + try dest.writeChar(')'); + }, + }; + } + }; +} + +/// A [rounding strategy](https://www.w3.org/TR/css-values-4/#typedef-rounding-strategy), +/// as used in the `round()` function. +pub const RoundingStrategy = enum { + /// Round to the nearest integer. + nearest, + /// Round up (ceil). + up, + /// Round down (floor). + down, + /// Round toward zero (truncate). + @"to-zero", + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + 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 default() RoundingStrategy { + return .nearest; + } +}; + +fn arr2(allocator: std.mem.Allocator, a: anytype, b: anytype) ArrayList(@TypeOf(a)) { + const T = @TypeOf(a); + if (T != @TypeOf(b)) { + @compileError("arr2: types must match"); + } + var arr = ArrayList(T){}; + arr.appendSlice(allocator, &.{ a, b }) catch bun.outOfMemory(); + return arr; +} + +fn round(_: void, value: f32, to: f32, strategy: RoundingStrategy) f32 { + const v = value / to; + return switch (strategy) { + .down => @floor(v) * to, + .up => @ceil(v) * to, + .nearest => @round(v) * to, + .@"to-zero" => @trunc(v) * to, + }; +} + +fn hypot(_: void, a: f32, b: f32) f32 { + return std.math.hypot(a, b); +} + +fn powi2(v: f32) f32 { + return std.math.pow(f32, v, 2); +} + +fn sqrtf32(v: f32) f32 { + return std.math.sqrt(v); +} +/// A mathematical constant. +pub const Constant = enum { + /// The base of the natural logarithm + e, + /// The ratio of a circle's circumference to its diameter + pi, + /// infinity + infinity, + /// -infinity + @"-infinity", + /// Not a number. + nan, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + 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 intoF32(this: *const @This()) f32 { + return switch (this.*) { + .e => std.math.e, + .pi => std.math.pi, + .infinity => std.math.inf(f32), + .@"-infinity" => -std.math.inf(f32), + .nan => std.math.nan(f32), + }; + } +}; + +fn absf(a: f32) f32 { + return @abs(a); +} diff --git a/src/css/values/color.zig b/src/css/values/color.zig new file mode 100644 index 0000000000000..f3a83e4da0799 --- /dev/null +++ b/src/css/values/color.zig @@ -0,0 +1,4294 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const Result = css.Result; + +const Percentage = css.css_values.percentage.Percentage; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const Angle = css.css_values.angle.Angle; + +const Printer = css.Printer; +const PrintErr = css.PrintErr; + +pub fn UnboundedColorGamut(comptime T: type) type { + return struct { + pub fn inGamut(_: *const T) bool { + return true; + } + + pub fn clip(this: *const T) T { + return this.*; + } + }; +} + +pub fn HslHwbColorGamut(comptime T: type, comptime a: []const u8, comptime b: []const u8) type { + return struct { + pub fn inGamut(this: *const T) bool { + return @field(this, a) >= 0.0 and + @field(this, a) <= 1.0 and + @field(this, b) >= 0.0 and + @field(this, b) <= 1.0; + } + + pub fn clip(this: *const T) T { + var result: T = this.*; + // result.h = this.h % 360.0; + result.h = @mod(this.h, 360.0); + @field(result, a) = bun.clamp(@field(this, a), 0.0, 1.0); + @field(result, b) = bun.clamp(@field(this, b), 0.0, 1.0); + result.alpha = bun.clamp(this.alpha, 0.0, 1.0); + return result; + } + }; +} + +/// A CSS `` value. +/// +/// CSS supports many different color spaces to represent colors. The most common values +/// are stored as RGBA using a single byte per component. Less common values are stored +/// using a `Box` to reduce the amount of memory used per color. +/// +/// Each color space is represented as a struct that implements the `From` and `Into` traits +/// for all other color spaces, so it is possible to convert between color spaces easily. +/// In addition, colors support interpolation as in the `color-mix()` function. +pub const CssColor = union(enum) { + /// The `currentColor` keyword. + current_color, + /// A value in the RGB color space, including values parsed as hex colors, or the `rgb()`, `hsl()`, and `hwb()` functions. + rgba: RGBA, + /// A value in a LAB color space, including the `lab()`, `lch()`, `oklab()`, and `oklch()` functions. + lab: *LABColor, + /// A value in a predefined color space, e.g. `display-p3`. + predefined: *PredefinedColor, + /// A floating point representation of an RGB, HSL, or HWB color when it contains `none` components. + float: *FloatColor, + /// The `light-dark()` function. + light_dark: struct { + // TODO: why box the two fields separately? why not one allocation? + light: *CssColor, + dark: *CssColor, + + pub fn takeLightFreeDark(this: *const @This(), allocator: Allocator) *CssColor { + const ret = this.light; + this.dark.deinit(allocator); + allocator.destroy(this.dark); + return ret; + } + + pub fn takeDarkFreeLight(this: *const @This(), allocator: Allocator) *CssColor { + const ret = this.dark; + this.light.deinit(allocator); + allocator.destroy(this.light); + return ret; + } + }, + /// A system color keyword. + system: SystemColor, + + const This = @This(); + + pub const jsFunctionColor = @import("./color_js.zig").jsFunctionColor; + + pub fn eql(this: *const This, other: *const This) bool { + if (@intFromEnum(this.*) != @intFromEnum(other.*)) return false; + + return switch (this.*) { + .current_color => true, + .rgba => std.meta.eql(this.rgba, other.rgba), + .lab => std.meta.eql(this.lab.*, other.lab.*), + .predefined => std.meta.eql(this.predefined.*, other.predefined.*), + .float => std.meta.eql(this.float.*, other.float.*), + .light_dark => this.light_dark.light.eql(other.light_dark.light) and this.light_dark.dark.eql(other.light_dark.dark), + .system => this.system == other.system, + }; + } + + pub fn toCss( + this: *const This, + comptime W: type, + dest: *Printer(W), + ) PrintErr!void { + switch (this.*) { + .current_color => try dest.writeStr("currentColor"), + .rgba => |*color| { + if (color.alpha == 255) { + const hex: u32 = (@as(u32, color.red) << 16) | (@as(u32, color.green) << 8) | @as(u32, color.blue); + if (shortColorName(hex)) |name| return dest.writeStr(name); + + const compact = compactHex(hex); + if (hex == expandHex(compact)) { + try dest.writeFmt("#{x:0>3}", .{compact}); + } else { + try dest.writeFmt("#{x:0>6}", .{hex}); + } + } else { + // If the #rrggbbaa syntax is not supported by the browser targets, output rgba() + if (dest.targets.shouldCompileSame(.hex_alpha_colors)) { + // If the browser doesn't support `#rrggbbaa` color syntax, it is converted to `transparent` when compressed(minify = true). + // https://www.w3.org/TR/css-color-4/#transparent-black + if (dest.minify and color.red == 0 and color.green == 0 and color.blue == 0 and color.alpha == 0) { + return dest.writeStr("transparent"); + } else { + try dest.writeFmt("rgba({d}", .{color.red}); + try dest.delim(',', false); + try dest.writeFmt("{d}", .{color.green}); + try dest.delim(',', false); + try dest.writeFmt("{d}", .{color.blue}); + try dest.delim(',', false); + + // Try first with two decimal places, then with three. + var rounded_alpha = @round(color.alphaF32() * 100.0) / 100.0; + const clamped: u8 = @intFromFloat(@min( + @max( + @round(rounded_alpha * 255.0), + 0.0, + ), + 255.0, + )); + if (clamped != color.alpha) { + rounded_alpha = @round(color.alphaF32() * 1000.0) / 1000.0; + } + + try CSSNumberFns.toCss(&rounded_alpha, W, dest); + try dest.writeChar(')'); + return; + } + } + + const hex: u32 = (@as(u32, color.red) << 24) | + (@as(u32, color.green) << 16) | + (@as(u32, color.blue) << 8) | + (@as(u32, color.alpha)); + const compact = compactHex(hex); + if (hex == expandHex(compact)) { + try dest.writeFmt("#{x:0>4}", .{compact}); + } else { + try dest.writeFmt("#{x:0>8}", .{hex}); + } + } + return; + }, + .lab => |_lab| { + return switch (_lab.*) { + .lab => |*lab| writeComponents( + "lab", + lab.l, + lab.a, + lab.b, + lab.alpha, + W, + dest, + ), + .lch => |*lch| writeComponents( + "lch", + lch.l, + lch.c, + lch.h, + lch.alpha, + W, + dest, + ), + .oklab => |*oklab| writeComponents( + "oklab", + oklab.l, + oklab.a, + oklab.b, + oklab.alpha, + W, + dest, + ), + .oklch => |*oklch| writeComponents( + "oklch", + oklch.l, + oklch.c, + oklch.h, + oklch.alpha, + W, + dest, + ), + }; + }, + .predefined => |predefined| return writePredefined(predefined, W, dest), + .float => |*float| { + // Serialize as hex. + const srgb = SRGB.fromFloatColor(float.*); + const as_css_color = srgb.intoCssColor(dest.allocator); + defer as_css_color.deinit(dest.allocator); + try as_css_color.toCss(W, dest); + }, + .light_dark => |*light_dark| { + if (!dest.targets.isCompatible(css.compat.Feature.light_dark)) { + // TODO(zack): lightningcss -> buncss + try dest.writeStr("var(--lightningcss-light"); + try dest.delim(',', false); + try light_dark.light.toCss(W, dest); + try dest.writeChar(')'); + try dest.whitespace(); + try dest.writeStr("var(--lightningcss-dark"); + try dest.delim(',', false); + try light_dark.dark.toCss(W, dest); + try light_dark.dark.toCss(W, dest); + return dest.writeChar(')'); + } + + try dest.writeStr("light-dark("); + try light_dark.light.toCss(W, dest); + try dest.delim(',', false); + try light_dark.dark.toCss(W, dest); + return dest.writeChar(')'); + }, + .system => |*system| return system.toCss(W, dest), + } + } + + pub const ParseResult = Result(CssColor); + pub fn parse(input: *css.Parser) ParseResult { + const location = input.currentSourceLocation(); + const token = switch (input.next()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + switch (token.*) { + .hash, .idhash => |v| { + const r, const g, const b, const a = css.color.parseHashColor(v) orelse return .{ .err = location.newUnexpectedTokenError(token.*) }; + return .{ .result = .{ + .rgba = RGBA.new(r, g, b, a), + } }; + }, + .ident => |value| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(value, "currentcolor")) { + return .{ .result = .current_color }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(value, "transparent")) { + return .{ .result = .{ + .rgba = RGBA.transparent(), + } }; + } else { + if (css.color.parseNamedColor(value)) |named| { + const r, const g, const b = named; + return .{ .result = .{ .rgba = RGBA.new(r, g, b, 255.0) } }; + // } else if (SystemColor.parseString(value)) |system_color| { + } else if (css.parse_utility.parseString(input.allocator(), SystemColor, value, SystemColor.parse).asValue()) |system_color| { + return .{ .result = .{ .system = system_color } }; + } else return .{ .err = location.newUnexpectedTokenError(token.*) }; + } + }, + .function => |name| return parseColorFunction(location, name, input), + else => return .{ + .err = location.newUnexpectedTokenError(token.*), + }, + } + } + + pub fn deinit(this: CssColor, allocator: Allocator) void { + switch (this) { + .current_color => {}, + .rgba => {}, + .lab => { + allocator.destroy(this.float); + }, + .predefined => { + allocator.destroy(this.float); + }, + .float => { + allocator.destroy(this.float); + }, + .light_dark => { + this.light_dark.light.deinit(allocator); + this.light_dark.dark.deinit(allocator); + allocator.destroy(this.light_dark.light); + allocator.destroy(this.light_dark.dark); + }, + .system => {}, + } + } + + pub fn deepClone(this: *const CssColor, allocator: Allocator) CssColor { + return switch (this.*) { + .current_color => .current_color, + .rgba => |rgba| CssColor{ .rgba = rgba }, + .lab => |lab| CssColor{ .lab = bun.create(allocator, LABColor, lab.*) }, + .predefined => |pre| CssColor{ .predefined = bun.create(allocator, PredefinedColor, pre.*) }, + .float => |float| CssColor{ .float = bun.create(allocator, FloatColor, float.*) }, + .light_dark => CssColor{ + .light_dark = .{ + .light = bun.create(allocator, CssColor, this.light_dark.light.deepClone(allocator)), + .dark = bun.create(allocator, CssColor, this.light_dark.dark.deepClone(allocator)), + }, + }, + .system => |sys| CssColor{ .system = sys }, + }; + } + + pub fn toLightDark(this: *const CssColor, allocator: Allocator) CssColor { + return switch (this.*) { + .light_dark => this.deepClone(allocator), + else => .{ + .light_dark = .{ + .light = bun.create(allocator, CssColor, this.deepClone(allocator)), + .dark = bun.create(allocator, CssColor, this.deepClone(allocator)), + }, + }, + }; + } + + /// Mixes this color with another color, including the specified amount of each. + /// Implemented according to the [`color-mix()`](https://www.w3.org/TR/css-color-5/#color-mix) function. + // PERF: these little allocations feel bad + pub fn interpolate( + this: *const CssColor, + allocator: Allocator, + comptime T: type, + p1_: f32, + other: *const CssColor, + p2_: f32, + method: HueInterpolationMethod, + ) ?CssColor { + var p1 = p1_; + var p2 = p2_; + + if (this.* == .current_color or other.* == .current_color) { + return null; + } + + if (this.* == .light_dark or other.* == .light_dark) { + const this_light_dark = this.toLightDark(allocator); + const other_light_dark = this.toLightDark(allocator); + + const al = this_light_dark.light_dark.light; + const ad = this_light_dark.light_dark.dark; + + const bl = other_light_dark.light_dark.light; + const bd = other_light_dark.light_dark.dark; + + return .{ + .light_dark = .{ + .light = bun.create( + allocator, + CssColor, + al.interpolate(allocator, T, p1, bl, p2, method) orelse return null, + ), + .dark = bun.create( + allocator, + CssColor, + ad.interpolate(allocator, T, p1, bd, p2, method) orelse return null, + ), + }, + }; + } + + const check_converted = struct { + fn run(color: *const CssColor) bool { + bun.debugAssert(color.* != .light_dark and color.* != .current_color and color.* != .system); + return switch (color.*) { + .rgba => T == RGBA, + .lab => |lab| switch (lab.*) { + .lab => T == LAB, + .lch => T == LCH, + .oklab => T == OKLAB, + .oklch => T == OKLCH, + }, + .predefined => |pre| switch (pre.*) { + .srgb => T == SRGB, + .srgb_linear => T == SRGBLinear, + .display_p3 => T == P3, + .a98 => T == A98, + .prophoto => T == ProPhoto, + .rec2020 => T == Rec2020, + .xyz_d50 => T == XYZd50, + .xyz_d65 => T == XYZd65, + }, + .float => |f| switch (f.*) { + .rgb => T == SRGB, + .hsl => T == HSL, + .hwb => T == HWB, + }, + .system => bun.Output.panic("Unreachable code: system colors cannot be converted to a color.\n\nThis is a bug in Bun's CSS color parser. Please file a bug report at https://github.com/oven-sh/bun/issues/new/choose", .{}), + // We checked these above + .light_dark, .current_color => unreachable, + }; + } + }; + + const converted_first = check_converted.run(this); + const converted_second = check_converted.run(other); + + // https://drafts.csswg.org/css-color-5/#color-mix-result + var first_color = T.tryFromCssColor(this) orelse return null; + var second_color = T.tryFromCssColor(other) orelse return null; + + if (converted_first and !first_color.inGamut()) { + first_color = mapGamut(T, first_color); + } + + if (converted_second and !second_color.inGamut()) { + second_color = mapGamut(T, second_color); + } + + // https://www.w3.org/TR/css-color-4/#powerless + if (converted_first) { + first_color.adjustPowerlessComponents(); + } + + if (converted_second) { + second_color.adjustPowerlessComponents(); + } + + // https://drafts.csswg.org/css-color-4/#interpolation-missing + first_color.fillMissingComponents(&second_color); + second_color.fillMissingComponents(&first_color); + + // https://www.w3.org/TR/css-color-4/#hue-interpolation + first_color.adjustHue(&second_color, method); + + // https://www.w3.org/TR/css-color-4/#interpolation-alpha + first_color.premultiply(); + second_color.premultiply(); + + // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm + var alpha_multiplier = p1 + p2; + if (alpha_multiplier != 1.0) { + p1 = p1 / alpha_multiplier; + p2 = p2 / alpha_multiplier; + if (alpha_multiplier > 1.0) { + alpha_multiplier = 1.0; + } + } + + var result_color = first_color.interpolate(p1, &second_color, p2); + + result_color.unpremultiply(alpha_multiplier); + + return result_color.intoCssColor(allocator); + } + + pub fn lightDarkOwned(allocator: Allocator, light: CssColor, dark: CssColor) CssColor { + return CssColor{ + .light_dark = .{ + .light = bun.create(allocator, CssColor, light), + .dark = bun.create(allocator, CssColor, dark), + }, + }; + } +}; + +pub fn parseColorFunction(location: css.SourceLocation, function: []const u8, input: *css.Parser) Result(CssColor) { + var parser = ComponentParser.new(true); + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "lab")) { + return parseLab(LAB, input, &parser, struct { + fn callback(l: f32, a: f32, b: f32, alpha: f32) LABColor { + return .{ .lab = .{ .l = l, .a = a, .b = b, .alpha = alpha } }; + } + }.callback); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "oklab")) { + return parseLab(OKLAB, input, &parser, struct { + fn callback(l: f32, a: f32, b: f32, alpha: f32) LABColor { + return .{ .oklab = .{ .l = l, .a = a, .b = b, .alpha = alpha } }; + } + }.callback); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "lch")) { + return parseLch(LCH, input, &parser, struct { + fn callback(l: f32, c: f32, h: f32, alpha: f32) LABColor { + return .{ .lch = .{ .l = l, .c = c, .h = h, .alpha = alpha } }; + } + }.callback); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "oklch")) { + return parseLch(OKLCH, input, &parser, struct { + fn callback(l: f32, c: f32, h: f32, alpha: f32) LABColor { + return .{ .oklch = .{ .l = l, .c = c, .h = h, .alpha = alpha } }; + } + }.callback); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "color")) { + return parsePredefined(input, &parser); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "hsl") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "hsla")) + { + return parseHslHwb(HSL, input, &parser, true, struct { + fn callback(allocator: Allocator, h: f32, s: f32, l: f32, a: f32) CssColor { + const hsl = HSL{ .h = h, .s = s, .l = l, .alpha = a }; + if (!std.math.isNan(h) and !std.math.isNan(s) and !std.math.isNan(l) and !std.math.isNan(a)) { + return CssColor{ .rgba = hsl.intoRGBA() }; + } else { + return CssColor{ .float = bun.create(allocator, FloatColor, .{ .hsl = hsl }) }; + } + } + }.callback); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "hwb")) { + return parseHslHwb(HWB, input, &parser, false, struct { + fn callback(allocator: Allocator, h: f32, w: f32, b: f32, a: f32) CssColor { + const hwb = HWB{ .h = h, .w = w, .b = b, .alpha = a }; + if (!std.math.isNan(h) and !std.math.isNan(w) and !std.math.isNan(b) and !std.math.isNan(a)) { + return CssColor{ .rgba = hwb.intoRGBA() }; + } else { + return CssColor{ .float = bun.create(allocator, FloatColor, .{ .hwb = hwb }) }; + } + } + }.callback); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "rgb") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "rgba")) + { + return parseRgb(input, &parser); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "color-mix")) { + return input.parseNestedBlock(CssColor, {}, struct { + pub fn parseFn(_: void, i: *css.Parser) Result(CssColor) { + return parseColorMix(i); + } + }.parseFn); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "light-dark")) { + return input.parseNestedBlock(CssColor, {}, struct { + fn callback(_: void, i: *css.Parser) Result(CssColor) { + const light = switch (switch (CssColor.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }) { + .light_dark => |ld| ld.takeLightFreeDark(i.allocator()), + else => |v| bun.create(i.allocator(), CssColor, v), + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const dark = switch (switch (CssColor.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }) { + .light_dark => |ld| ld.takeDarkFreeLight(i.allocator()), + else => |v| bun.create(i.allocator(), CssColor, v), + }; + return .{ .result = .{ + .light_dark = .{ + .light = light, + .dark = dark, + }, + } }; + } + }.callback); + } else { + return .{ .err = location.newUnexpectedTokenError(.{ .ident = function }) }; + } +} + +pub fn parseRGBComponents(input: *css.Parser, parser: *ComponentParser) Result(struct { f32, f32, f32, bool }) { + const red = switch (parser.parseNumberOrPercentage(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }; + + const is_legacy_syntax = parser.from == null and + !std.math.isNan(red.unitValue()) and + input.tryParse(css.Parser.expectComma, .{}).isOk(); + + const r, const g, const b = if (is_legacy_syntax) switch (red) { + .number => |v| brk: { + const r = bun.clamp(@round(v.value), 0.0, 255.0); + const g = switch (parser.parseNumber(input)) { + .err => |e| return .{ .err = e }, + .result => |vv| bun.clamp(@round(vv), 0.0, 255.0), + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + const b = switch (parser.parseNumber(input)) { + .err => |e| return .{ .err = e }, + .result => |vv| bun.clamp(@round(vv), 0.0, 255.0), + }; + break :brk .{ r, g, b }; + }, + .percentage => |v| brk: { + const r = bun.clamp(@round(v.unit_value * 255.0), 0.0, 255.0); + const g = switch (parser.parsePercentage(input)) { + .err => |e| return .{ .err = e }, + .result => |vv| bun.clamp(@round(vv * 255.0), 0.0, 255.0), + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + const b = switch (parser.parsePercentage(input)) { + .err => |e| return .{ .err = e }, + .result => |vv| bun.clamp(@round(vv * 255.0), 0.0, 255.0), + }; + break :brk .{ r, g, b }; + }, + } else blk: { + const getComponent = struct { + fn get(value: NumberOrPercentage) f32 { + return switch (value) { + .number => |v| if (std.math.isNan(v.value)) v.value else bun.clamp(@round(v.value), 0.0, 255.0) / 255.0, + .percentage => |v| bun.clamp(v.unit_value, 0.0, 1.0), + }; + } + }.get; + + const r = getComponent(red); + const g = getComponent(switch (parser.parseNumberOrPercentage(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }); + const b = getComponent(switch (parser.parseNumberOrPercentage(input)) { + .err => |e| return .{ .err = e }, + .result => |v| v, + }); + break :blk .{ r, g, b }; + }; + + if (is_legacy_syntax and (std.math.isNan(g) or std.math.isNan(b))) { + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + return .{ .result = .{ r, g, b, is_legacy_syntax } }; +} + +pub fn parseHSLHWBComponents(comptime T: type, input: *css.Parser, parser: *ComponentParser, allows_legacy: bool) Result(struct { f32, f32, f32, bool }) { + _ = T; // autofix + const h = switch (parseAngleOrNumber(input, parser)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + const is_legacy_syntax = allows_legacy and + parser.from == null and + !std.math.isNan(h) and + input.tryParse(css.Parser.expectComma, .{}).isOk(); + const a = switch (parser.parsePercentage(input)) { + .result => |v| bun.clamp(v, 0.0, 1.0), + .err => |e| return .{ .err = e }, + }; + if (is_legacy_syntax) { + if (input.expectColon().asErr()) |e| return .{ .err = e }; + } + const b = switch (parser.parsePercentage(input)) { + .result => |v| bun.clamp(v, 0.0, 1.0), + .err => |e| return .{ .err = e }, + }; + if (is_legacy_syntax and (std.math.isNan(a) or std.math.isNan(b))) { + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + return .{ .result = .{ h, a, b, is_legacy_syntax } }; +} + +pub fn mapGamut(comptime T: type, color: T) T { + const conversion_function_name = "into" ++ comptime bun.meta.typeName(T); + const JND: f32 = 0.02; + const EPSILON: f32 = 0.00001; + + // https://www.w3.org/TR/css-color-4/#binsearch + var current: OKLCH = color.intoOKLCH(); + + // If lightness is >= 100%, return pure white. + if (@abs(current.l - 1.0) < EPSILON or current.l > 1.0) { + const oklch = OKLCH{ + .l = 1.0, + .c = 0.0, + .h = 0.0, + .alpha = current.alpha, + }; + return @call(.auto, @field(OKLCH, conversion_function_name), .{&oklch}); + } + + // If lightness <= 0%, return pure black. + if (current.l < EPSILON) { + const oklch = OKLCH{ + .l = 0.0, + .c = 0.0, + .h = 0.0, + .alpha = current.alpha, + }; + return @call(.auto, @field(OKLCH, conversion_function_name), .{&oklch}); + } + + var min: f32 = 0.0; + var max = current.c; + + while ((max - min) > EPSILON) { + const chroma = (min + max) / 2.0; + current.c = chroma; + + const converted = @call(.auto, @field(OKLCH, conversion_function_name), .{¤t}); + if (converted.inGamut()) { + min = chroma; + continue; + } + + const clipped = converted.clip(); + const delta_e = deltaEok(T, clipped, current); + if (delta_e < JND) { + return clipped; + } + + max = chroma; + } + + return @call(.auto, @field(OKLCH, conversion_function_name), .{¤t}); +} + +pub fn deltaEok(comptime T: type, _a: T, _b: OKLCH) f32 { + // https://www.w3.org/TR/css-color-4/#color-difference-OK + const a = T.intoOKLAB(&_a); + const b: OKLAB = _b.intoOKLAB(); + + const delta_l = a.l - b.l; + const delta_a = a.a - b.a; + const delta_b = a.b - b.a; + + return @sqrt( + std.math.pow(f32, delta_l, 2) + + std.math.pow(f32, delta_a, 2) + + std.math.pow(f32, delta_b, 2), + ); +} + +pub fn parseLab( + comptime T: type, + input: *css.Parser, + parser: *ComponentParser, + comptime func: *const fn (f32, f32, f32, f32) LABColor, +) Result(CssColor) { + const Closure = struct { + parser: *ComponentParser, + + pub fn parsefn(this: *@This(), i: *css.Parser) Result(CssColor) { + return this.parser.parseRelative(i, T, CssColor, @This().innerfn, .{}); + } + + pub fn innerfn(i: *css.Parser, p: *ComponentParser) Result(CssColor) { + // f32::max() does not propagate NaN, so use clamp for now until f32::maximum() is stable. + const l = bun.clamp( + switch (p.parsePercentage(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }, + 0.0, + std.math.floatMax(f32), + ); + const a = switch (p.parseNumber(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const b = switch (p.parseNumber(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const alpha = switch (parseAlpha(i, p)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const lab = func(l, a, b, alpha); + const heap_lab = bun.create(i.allocator(), LABColor, lab); + heap_lab.* = lab; + return .{ .result = CssColor{ .lab = heap_lab } }; + } + }; + var closure = Closure{ + .parser = parser, + }; + // https://www.w3.org/TR/css-color-4/#funcdef-lab + return input.parseNestedBlock( + CssColor, + &closure, + Closure.parsefn, + ); +} + +pub fn parseLch( + comptime T: type, + input: *css.Parser, + parser: *ComponentParser, + comptime func: *const fn ( + f32, + f32, + f32, + f32, + ) LABColor, +) Result(CssColor) { + const Closure = struct { + parser: *ComponentParser, + + pub fn parseNestedBlockFn(this: *@This(), i: *css.Parser) Result(CssColor) { + return this.parser.parseRelative(i, T, CssColor, @This().parseRelativeFn, .{this}); + } + + pub fn parseRelativeFn(i: *css.Parser, p: *ComponentParser, this: *@This()) Result(CssColor) { + _ = this; // autofix + if (p.from) |*from| { + // Relative angles should be normalized. + // https://www.w3.org/TR/css-color-5/#relative-LCH + // from.components[2] %= 360.0; + from.components[2] = @mod(from.components[2], 360.0); + if (from.components[2] < 0.0) { + from.components[2] += 360.0; + } + } + + const l = bun.clamp( + switch (p.parsePercentage(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }, + 0.0, + std.math.floatMax(f32), + ); + const c = bun.clamp( + switch (p.parseNumber(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }, + 0.0, + std.math.floatMax(f32), + ); + const h = switch (parseAngleOrNumber(i, p)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const alpha = switch (parseAlpha(i, p)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const lab = func(l, c, h, alpha); + return .{ + .result = .{ + .lab = bun.create(i.allocator(), LABColor, lab), + }, + }; + } + }; + + var closure = Closure{ + .parser = parser, + }; + + return input.parseNestedBlock(CssColor, &closure, Closure.parseNestedBlockFn); +} + +/// Parses the hsl() and hwb() functions. +/// The results of this function are stored as floating point if there are any `none` components. +/// https://drafts.csswg.org/css-color-4/#the-hsl-notation +pub fn parseHslHwb( + comptime T: type, + input: *css.Parser, + parser: *ComponentParser, + allows_legacy: bool, + comptime func: *const fn ( + Allocator, + f32, + f32, + f32, + f32, + ) CssColor, +) Result(CssColor) { + const Closure = struct { + parser: *ComponentParser, + allows_legacy: bool, + + pub fn parseNestedBlockFn(this: *@This(), i: *css.Parser) Result(CssColor) { + return this.parser.parseRelative(i, T, CssColor, @This().parseRelativeFn, .{this}); + } + + pub fn parseRelativeFn(i: *css.Parser, p: *ComponentParser, this: *@This()) Result(CssColor) { + const h, const a, const b, const is_legacy = switch (parseHslHwbComponents(T, i, p, this.allows_legacy)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const alpha = switch (if (is_legacy) parseLegacyAlpha(i, p) else parseAlpha(i, p)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + return .{ .result = func(i.allocator(), h, a, b, alpha) }; + } + }; + + var closure = Closure{ + .parser = parser, + .allows_legacy = allows_legacy, + }; + + return input.parseNestedBlock(CssColor, &closure, Closure.parseNestedBlockFn); +} + +pub fn parseHslHwbComponents( + comptime T: type, + input: *css.Parser, + parser: *ComponentParser, + allows_legacy: bool, +) Result(struct { f32, f32, f32, bool }) { + _ = T; // autofix + const h = switch (parseAngleOrNumber(input, parser)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const is_legacy_syntax = allows_legacy and + parser.from == null and + !std.math.isNan(h) and + input.tryParse(css.Parser.expectComma, .{}).isOk(); + + const a = bun.clamp( + switch (parser.parsePercentage(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }, + 0.0, + 1.0, + ); + + if (is_legacy_syntax) { + if (input.expectComma().asErr()) |e| return .{ .err = e }; + } + + const b = bun.clamp( + switch (parser.parsePercentage(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }, + 0.0, + 1.0, + ); + + if (is_legacy_syntax and (std.math.isNan(a) or std.math.isNan(b))) { + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + + return .{ .result = .{ h, a, b, is_legacy_syntax } }; +} + +pub fn parseAngleOrNumber(input: *css.Parser, parser: *const ComponentParser) Result(f32) { + const result = switch (parser.parseAngleOrNumber(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ + .result = switch (result) { + .number => |v| v.value, + .angle => |v| v.degrees, + }, + }; +} + +fn parseRgb(input: *css.Parser, parser: *ComponentParser) Result(CssColor) { + // https://drafts.csswg.org/css-color-4/#rgb-functions + + const Closure = struct { + p: *ComponentParser, + + pub fn parseNestedBlockFn(this: *@This(), i: *css.Parser) Result(CssColor) { + return this.p.parseRelative(i, SRGB, CssColor, @This().parseRelativeFn, .{this}); + } + + pub fn parseRelativeFn(i: *css.Parser, p: *ComponentParser, this: *@This()) Result(CssColor) { + _ = this; // autofix + const r, const g, const b, const is_legacy = switch (parseRGBComponents(i, p)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const alpha = switch (if (is_legacy) parseLegacyAlpha(i, p) else parseAlpha(i, p)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + if (!std.math.isNan(r) and + !std.math.isNan(g) and + !std.math.isNan(b) and + !std.math.isNan(alpha)) + { + if (is_legacy) return .{ + .result = .{ + .rgba = RGBA.new( + @intFromFloat(r), + @intFromFloat(g), + @intFromFloat(b), + alpha, + ), + }, + }; + + return .{ + .result = .{ + .rgba = RGBA.fromFloats( + r, + g, + b, + alpha, + ), + }, + }; + } else { + return .{ + .result = .{ + .float = bun.create( + i.allocator(), + FloatColor, + .{ + .rgb = .{ + .r = r, + .g = g, + .b = b, + .alpha = alpha, + }, + }, + ), + }, + }; + } + } + }; + var closure = Closure{ + .p = parser, + }; + return input.parseNestedBlock(CssColor, &closure, Closure.parseNestedBlockFn); +} + +// pub fn parseRgbComponents(input: *css.Parser, parser: *ComponentParser) Result(struct { +// f32, +// f32, +// f32, +// bool, +// }) { +// const red = switch (parser.parseNumberOrPercentage(input)) { +// .result => |vv| vv, +// .err => |e| return .{ .err = e }, +// }; +// const is_legacy_syntax = parser.from == null and !std.math.isNan(red.unitValue()) and input.tryParse(css.Parser.expectComma, .{}).isOk(); + +// const r, const g, const b = if (is_legacy_syntax) switch (red) { +// .number => |num| brk: { +// const r = bun.clamp(@round(num.value), 0.0, 255.0); +// const g = bun.clamp( +// @round( +// switch (parser.parseNumber(input)) { +// .result => |vv| vv, +// .err => |e| return .{ .err = e }, +// }, +// ), +// 0.0, +// 255.0, +// ); +// if (input.expectComma().asErr()) |e| return .{ .err = e }; +// const b = bun.clamp( +// @round( +// switch (parser.parseNumber(input)) { +// .result => |vv| vv, +// .err => |e| return .{ .err = e }, +// }, +// ), +// 0.0, +// 255.0, +// ); +// break :brk .{ r, g, b }; +// }, +// .percentage => |per| brk: { +// const unit_value = per.unit_value; +// const r = bun.clamp(@round(unit_value * 255.0), 0.0, 255.0); +// const g = bun.clamp( +// @round( +// switch (parser.parsePercentage(input)) { +// .result => |vv| vv, +// .err => |e| return .{ .err = e }, +// } * 255.0, +// ), +// 0.0, +// 255.0, +// ); +// if (input.expectComma().asErr()) |e| return .{ .err = e }; +// const b = bun.clamp( +// @round( +// switch (parser.parsePercentage(input)) { +// .result => |vv| vv, +// .err => |e| return .{ .err = e }, +// } * 255.0, +// ), +// 0.0, +// 255.0, +// ); +// break :brk .{ r, g, b }; +// }, +// } else brk: { +// const get = struct { +// pub fn component(value: NumberOrPercentage) f32 { +// return switch (value) { +// .number => |num| { +// const v = num.value; +// if (std.math.isNan(v)) return v; +// return bun.clamp(@round(v), 0.0, 255.0) / 255.0; +// }, +// .percentage => |per| bun.clamp(per.unit_value, 0.0, 1.0), +// }; +// } +// }; +// const r = get.component(red); +// const g = get.component( +// switch (parser.parseNumberOrPercentage(input)) { +// .result => |vv| vv, +// .err => |e| return .{ .err = e }, +// }, +// ); +// const b = get.component( +// switch (parser.parseNumberOrPercentage(input)) { +// .result => |vv| vv, +// .err => |e| return .{ .err = e }, +// }, +// ); +// break :brk .{ r, g, b }; +// }; + +// if (is_legacy_syntax and (std.math.isNan(g) or std.math.isNan(b))) { +// return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; +// } + +// return .{ .result = .{ r, g, b, is_legacy_syntax } }; +// } + +fn parseLegacyAlpha(input: *css.Parser, parser: *const ComponentParser) Result(f32) { + if (!input.isExhausted()) { + if (input.expectComma().asErr()) |e| return .{ .err = e }; + return .{ .result = bun.clamp( + switch (parseNumberOrPercentage(input, parser)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }, + 0.0, + 1.0, + ) }; + } + return .{ .result = 1.0 }; +} + +fn parseAlpha(input: *css.Parser, parser: *const ComponentParser) Result(f32) { + const res = if (input.tryParse(css.Parser.expectDelim, .{'/'}).isOk()) + bun.clamp(switch (parseNumberOrPercentage(input, parser)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }, 0.0, 1.0) + else + 1.0; + + return .{ .result = res }; +} + +pub fn parseNumberOrPercentage(input: *css.Parser, parser: *const ComponentParser) Result(f32) { + const result = switch (parser.parseNumberOrPercentage(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return switch (result) { + .number => |value| .{ .result = value.value }, + .percentage => |value| .{ .result = value.unit_value }, + }; +} + +pub fn parseeColorFunction(location: css.SourceLocation, function: []const u8, input: *css.Parser) Result(CssColor) { + var parser = ComponentParser.new(true); + + // css.todo_stuff.match_ignore_ascii_case; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "lab")) { + return .{ .result = parseLab(LAB, input, &parser, LABColor.newLAB, .{}) }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "oklab")) { + return .{ .result = parseLab(OKLAB, input, &parser, LABColor.newOKLAB, .{}) }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "lch")) { + return .{ .result = parseLch(LCH, input, &parser, LABColor.newLCH, .{}) }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "oklch")) { + return .{ .result = parseLch(OKLCH, input, &parser, LABColor.newOKLCH, .{}) }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "color")) { + const predefined = switch (parsePredefined(input, &parser)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = predefined }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "hsl") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "hsla")) + { + const Fn = struct { + pub fn parsefn(allocator: Allocator, h: f32, s: f32, l: f32, a: f32) CssColor { + const hsl = HSL{ .h = h, .s = s, .l = l, .alpha = a }; + + if (!std.math.isNan(h) and !std.math.isNan(s) and !std.math.isNan(l) and !std.math.isNan(a)) { + return .{ .rgba = hsl.intoRgba() }; + } + + return .{ + .float = bun.create( + allocator, + FloatColor, + .{ .hsl = hsl }, + ), + }; + } + }; + return parseHslHwb(HSL, input, &parser, true, Fn.parsefn); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "hwb")) { + const Fn = struct { + pub fn parsefn(allocator: Allocator, h: f32, w: f32, b: f32, a: f32) CssColor { + const hwb = HWB{ .h = h, .w = w, .b = b, .alpha = a }; + if (!std.math.isNan(h) and !std.math.isNan(w) and !std.math.isNan(b) and !std.math.isNan(a)) { + return .{ .rgba = hwb.intoRGBA() }; + } else { + return .{ .float = bun.create(allocator, FloatColor, .{ .hwb = hwb }) }; + } + } + }; + return parseHslHwb(HWB, input, &parser, true, Fn.parsefn); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "rgb") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "rgba")) + { + return parseRgb(input, &parser); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "color-mix")) { + return input.parseNestedBlock(CssColor, void, css.voidWrap(CssColor, parseColorMix)); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(function, "light-dark")) { + const Fn = struct { + pub fn parsefn(_: void, i: *css.Parser) Result(CssColor) { + const first_color = switch (CssColor.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const light = switch (first_color) { + .light_dark => |c| c.light, + else => |light| bun.create( + i.allocator(), + CssColor, + light, + ), + }; + + if (i.expectComma().asErr()) |e| return .{ .err = e }; + + const second_color = switch (CssColor.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const dark = switch (second_color) { + .light_dark => |c| c.dark, + else => |dark| bun.create( + i.allocator(), + CssColor, + dark, + ), + }; + return .{ + .result = .{ + .light_dark = .{ + .light = light, + .dark = dark, + }, + }, + }; + } + }; + return input.parseNestedBlock(CssColor, {}, Fn.parsefn); + } else { + return .{ .err = location.newUnexpectedTokenError(.{ + .ident = function, + }) }; + } +} + +// Copied from an older version of cssparser. +/// A color with red, green, blue, and alpha components, in a byte each. +pub const RGBA = struct { + /// The red component. + red: u8, + /// The green component. + green: u8, + /// The blue component. + blue: u8, + /// The alpha component. + alpha: u8, + + pub usingnamespace color_conversions.convert_RGBA; + + pub fn new(red: u8, green: u8, blue: u8, alpha: f32) RGBA { + return RGBA{ + .red = red, + .green = green, + .blue = blue, + .alpha = clamp_unit_f32(alpha), + }; + } + + /// Constructs a new RGBA value from float components. It expects the red, + /// green, blue and alpha channels in that order, and all values will be + /// clamped to the 0.0 ... 1.0 range. + pub fn fromFloats(red: f32, green: f32, blue: f32, alpha: f32) RGBA { + return RGBA.new( + clamp_unit_f32(red), + clamp_unit_f32(green), + clamp_unit_f32(blue), + alpha, + ); + } + + pub fn transparent() RGBA { + return RGBA.new(0, 0, 0, 0.0); + } + + /// Returns the red channel in a floating point number form, from 0 to 1. + pub fn redF32(this: *const RGBA) f32 { + return @as(f32, @floatFromInt(this.red)) / 255.0; + } + + /// Returns the green channel in a floating point number form, from 0 to 1. + pub fn greenF32(this: *const RGBA) f32 { + return @as(f32, @floatFromInt(this.green)) / 255.0; + } + + /// Returns the blue channel in a floating point number form, from 0 to 1. + pub fn blueF32(this: *const RGBA) f32 { + return @as(f32, @floatFromInt(this.blue)) / 255.0; + } + + /// Returns the alpha channel in a floating point number form, from 0 to 1. + pub fn alphaF32(this: *const RGBA) f32 { + return @as(f32, @floatFromInt(this.alpha)) / 255.0; + } + + pub fn intoSRGB(rgb: *const RGBA) SRGB { + return SRGB{ + .r = rgb.redF32(), + .g = rgb.greenF32(), + .b = rgb.blueF32(), + .alpha = rgb.alphaF32(), + }; + } +}; + +fn clamp_unit_f32(val: f32) u8 { + // Whilst scaling by 256 and flooring would provide + // an equal distribution of integers to percentage inputs, + // this is not what Gecko does so we instead multiply by 255 + // and round (adding 0.5 and flooring is equivalent to rounding) + // + // Chrome does something similar for the alpha value, but not + // the rgb values. + // + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1340484 + // + // Clamping to 256 and rounding after would let 1.0 map to 256, and + // `256.0_f32 as u8` is undefined behavior: + // + // https://github.com/rust-lang/rust/issues/10184 + return clamp_floor_256_f32(val * 255.0); +} + +fn clamp_floor_256_f32(val: f32) u8 { + return @intFromFloat(@min(255.0, @max(0.0, @round(val)))); + // val.round().max(0.).min(255.) as u8 +} + +/// A color in a LAB color space, including the `lab()`, `lch()`, `oklab()`, and `oklch()` functions. +pub const LABColor = union(enum) { + /// A `lab()` color. + lab: LAB, + /// An `lch()` color. + lch: LCH, + /// An `oklab()` color. + oklab: OKLAB, + /// An `oklch()` color. + oklch: OKLCH, + + pub fn newLAB(l: f32, a: f32, b: f32, alpha: f32) LABColor { + return LABColor{ + .lab = LAB.new(l, a, b, alpha), + }; + } + + pub fn newOKLAB(l: f32, a: f32, b: f32, alpha: f32) LABColor { + return LABColor{ + .lab = OKLAB.new(l, a, b, alpha), + }; + } + + pub fn newLCH(l: f32, a: f32, b: f32, alpha: f32) LABColor { + return LABColor{ + .lab = LCH.new(l, a, b, alpha), + }; + } + + pub fn newOKLCH(l: f32, a: f32, b: f32, alpha: f32) LABColor { + return LABColor{ + .lab = LCH.new(l, a, b, alpha), + }; + } +}; + +/// A color in a predefined color space, e.g. `display-p3`. +pub const PredefinedColor = union(enum) { + /// A color in the `srgb` color space. + srgb: SRGB, + /// A color in the `srgb-linear` color space. + srgb_linear: SRGBLinear, + /// A color in the `display-p3` color space. + display_p3: P3, + /// A color in the `a98-rgb` color space. + a98: A98, + /// A color in the `prophoto-rgb` color space. + prophoto: ProPhoto, + /// A color in the `rec2020` color space. + rec2020: Rec2020, + /// A color in the `xyz-d50` color space. + xyz_d50: XYZd50, + /// A color in the `xyz-d65` color space. + xyz_d65: XYZd65, +}; + +/// A floating point representation of color types that +/// are usually stored as RGBA. These are used when there +/// are any `none` components, which are represented as NaN. +pub const FloatColor = union(enum) { + /// An RGB color. + rgb: SRGB, + /// An HSL color. + hsl: HSL, + /// An HWB color. + hwb: HWB, +}; + +/// A CSS [system color](https://drafts.csswg.org/css-color/#css-system-colors) keyword. +/// *NOTE* these are intentionally in flat case +pub const SystemColor = enum { + /// Background of accented user interface controls. + accentcolor, + /// Text of accented user interface controls. + accentcolortext, + /// Text in active links. For light backgrounds, traditionally red. + activetext, + /// The base border color for push buttons. + buttonborder, + /// The face background color for push buttons. + buttonface, + /// Text on push buttons. + buttontext, + /// Background of application content or documents. + canvas, + /// Text in application content or documents. + canvastext, + /// Background of input fields. + field, + /// Text in input fields. + fieldtext, + /// Disabled text. (Often, but not necessarily, gray.) + graytext, + /// Background of selected text, for example from ::selection. + highlight, + /// Text of selected text. + highlighttext, + /// Text in non-active, non-visited links. For light backgrounds, traditionally blue. + linktext, + /// Background of text that has been specially marked (such as by the HTML mark element). + mark, + /// Text that has been specially marked (such as by the HTML mark element). + marktext, + /// Background of selected items, for example a selected checkbox. + selecteditem, + /// Text of selected items. + selecteditemtext, + /// Text in visited links. For light backgrounds, traditionally purple. + visitedtext, + + // Deprecated colors: https://drafts.csswg.org/css-color/#deprecated-system-colors + + /// Active window border. Same as ButtonBorder. + activeborder, + /// Active window caption. Same as Canvas. + activecaption, + /// Background color of multiple document interface. Same as Canvas. + appworkspace, + /// Desktop background. Same as Canvas. + background, + /// The color of the border facing the light source for 3-D elements that appear 3-D due to one layer of surrounding border. Same as ButtonFace. + buttonhighlight, + /// The color of the border away from the light source for 3-D elements that appear 3-D due to one layer of surrounding border. Same as ButtonFace. + buttonshadow, + /// Text in caption, size box, and scrollbar arrow box. Same as CanvasText. + captiontext, + /// Inactive window border. Same as ButtonBorder. + inactiveborder, + /// Inactive window caption. Same as Canvas. + inactivecaption, + /// Color of text in an inactive caption. Same as GrayText. + inactivecaptiontext, + /// Background color for tooltip controls. Same as Canvas. + infobackground, + /// Text color for tooltip controls. Same as CanvasText. + infotext, + /// Menu background. Same as Canvas. + menu, + /// Text in menus. Same as CanvasText. + menutext, + /// Scroll bar gray area. Same as Canvas. + scrollbar, + /// The color of the darker (generally outer) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder. + threeddarkshadow, + /// The face background color for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonFace. + threedface, + /// The color of the lighter (generally outer) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder. + threedhighlight, + /// The color of the darker (generally inner) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder. + threedlightshadow, + /// The color of the lighter (generally inner) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder. + threedshadow, + /// Window background. Same as Canvas. + window, + /// Window frame. Same as ButtonBorder. + windowframe, + /// Text in windows. Same as CanvasText. + windowtext, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return css.enum_property_util.toCss(@This(), this, W, dest); + } +}; + +/// A color in the [CIE Lab](https://www.w3.org/TR/css-color-4/#cie-lab) color space. +pub const LAB = struct { + /// The lightness component. + l: f32, + /// The a component. + a: f32, + /// The b component. + b: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace UnboundedColorGamut(@This()); + + pub usingnamespace AdjustPowerlessLAB(@This()); + pub usingnamespace DeriveInterpolate(@This(), "l", "a", "b"); + pub usingnamespace RecangularPremultiply(@This(), "l", "a", "b"); + + pub usingnamespace color_conversions.convert_LAB; + + pub const ChannelTypeMap = .{ + .l = ChannelType{ .percentage = true }, + .a = ChannelType{ .number = true }, + .b = ChannelType{ .number = true }, + }; + + pub fn adjustHue(_: *@This(), _: *@This(), _: HueInterpolationMethod) void {} +}; + +/// A color in the [`sRGB`](https://www.w3.org/TR/css-color-4/#predefined-sRGB) color space. +pub const SRGB = struct { + /// The red component. + r: f32, + /// The green component. + g: f32, + /// The blue component. + b: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace BoundedColorGamut(@This()); + + pub usingnamespace DeriveInterpolate(@This(), "r", "g", "b"); + pub usingnamespace RecangularPremultiply(@This(), "r", "g", "b"); + + pub usingnamespace color_conversions.convert_SRGB; + + pub const ChannelTypeMap = .{ + .r = ChannelType{ .percentage = true }, + .g = ChannelType{ .percentage = true }, + .b = ChannelType{ .percentage = true }, + }; + + pub fn adjustPowerlessComponents(_: *@This()) void {} + pub fn adjustHue(_: *@This(), _: *@This(), _: HueInterpolationMethod) void {} + + pub fn intoRGBA(_rgb: *const SRGB) RGBA { + const rgb = _rgb.resolve(); + return RGBA.fromFloats( + rgb.r, + rgb.g, + rgb.b, + rgb.alpha, + ); + } +}; + +/// A color in the [`hsl`](https://www.w3.org/TR/css-color-4/#the-hsl-notation) color space. +pub const HSL = struct { + /// The hue component. + h: f32, + /// The saturation component. + s: f32, + /// The lightness component. + l: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace HslHwbColorGamut(@This(), "s", "l"); + + pub usingnamespace PolarPremultiply(@This(), "s", "l"); + pub usingnamespace DeriveInterpolate(@This(), "h", "s", "l"); + + pub usingnamespace color_conversions.convert_HSL; + + pub const ChannelTypeMap = .{ + .h = ChannelType{ .angle = true }, + .s = ChannelType{ .percentage = true }, + .l = ChannelType{ .percentage = true }, + }; + + pub fn adjustPowerlessComponents(this: *HSL) void { + // If the saturation of an HSL color is 0%, then the hue component is powerless. + // If the lightness of an HSL color is 0% or 100%, both the saturation and hue components are powerless. + if (@abs(this.s) < std.math.floatEps(f32)) { + this.h = std.math.nan(f32); + } + + if (@abs(this.l) < std.math.floatEps(f32) or @abs(this.l - 1.0) < std.math.floatEps(f32)) { + this.h = std.math.nan(f32); + this.s = std.math.nan(f32); + } + } + + pub fn adjustHue(this: *HSL, other: *HSL, method: HueInterpolationMethod) void { + _ = method.interpolate(&this.h, &other.h); + } +}; + +/// A color in the [`hwb`](https://www.w3.org/TR/css-color-4/#the-hwb-notation) color space. +pub const HWB = struct { + /// The hue component. + h: f32, + /// The whiteness component. + w: f32, + /// The blackness component. + b: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace HslHwbColorGamut(@This(), "w", "b"); + + pub usingnamespace PolarPremultiply(@This(), "w", "b"); + pub usingnamespace DeriveInterpolate(@This(), "h", "w", "b"); + + pub usingnamespace color_conversions.convert_HWB; + + pub const ChannelTypeMap = .{ + .h = ChannelType{ .angle = true }, + .w = ChannelType{ .percentage = true }, + .b = ChannelType{ .percentage = true }, + }; + + pub fn adjustPowerlessComponents(this: *HWB) void { + // If white+black is equal to 100% (after normalization), it defines an achromatic color, + // i.e. some shade of gray, without any hint of the chosen hue. In this case, the hue component is powerless. + if (@abs(this.w + this.b - 1.0) < std.math.floatEps(f32)) { + this.h = std.math.nan(f32); + } + } + + pub fn adjustHue(this: *HWB, other: *HWB, method: HueInterpolationMethod) void { + _ = method.interpolate(&this.h, &other.h); + } +}; + +/// A color in the [`sRGB-linear`](https://www.w3.org/TR/css-color-4/#predefined-sRGB-linear) color space. +pub const SRGBLinear = struct { + /// The red component. + r: f32, + /// The green component. + g: f32, + /// The blue component. + b: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace BoundedColorGamut(@This()); + + pub usingnamespace DeriveInterpolate(@This(), "r", "g", "b"); + pub usingnamespace RecangularPremultiply(@This(), "r", "g", "b"); + + pub usingnamespace color_conversions.convert_SRGBLinear; + + pub const ChannelTypeMap = .{ + .r = ChannelType{ .angle = true }, + .g = ChannelType{ .percentage = true }, + .b = ChannelType{ .percentage = true }, + }; + + pub fn adjustPowerlessComponents(_: *@This()) void {} + pub fn adjustHue(_: *@This(), _: *@This(), _: HueInterpolationMethod) void {} +}; + +/// A color in the [`display-p3`](https://www.w3.org/TR/css-color-4/#predefined-display-p3) color space. +pub const P3 = struct { + /// The red component. + r: f32, + /// The green component. + g: f32, + /// The blue component. + b: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace BoundedColorGamut(@This()); + + pub usingnamespace color_conversions.convert_P3; + + pub const ChannelTypeMap = .{ + .r = ChannelType{ .percentage = true }, + .g = ChannelType{ .percentage = true }, + .b = ChannelType{ .percentage = true }, + }; +}; + +/// A color in the [`a98-rgb`](https://www.w3.org/TR/css-color-4/#predefined-a98-rgb) color space. +pub const A98 = struct { + /// The red component. + r: f32, + /// The green component. + g: f32, + /// The blue component. + b: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace BoundedColorGamut(@This()); + + pub usingnamespace color_conversions.convert_A98; + + pub const ChannelTypeMap = .{ + .r = ChannelType{ .percentage = true }, + .g = ChannelType{ .percentage = true }, + .b = ChannelType{ .percentage = true }, + }; +}; + +/// A color in the [`prophoto-rgb`](https://www.w3.org/TR/css-color-4/#predefined-prophoto-rgb) color space. +pub const ProPhoto = struct { + /// The red component. + r: f32, + /// The green component. + g: f32, + /// The blue component. + b: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace BoundedColorGamut(@This()); + + pub usingnamespace color_conversions.convert_ProPhoto; + + pub const ChannelTypeMap = .{ + .r = ChannelType{ .percentage = true }, + .g = ChannelType{ .percentage = true }, + .b = ChannelType{ .percentage = true }, + }; +}; + +/// A color in the [`rec2020`](https://www.w3.org/TR/css-color-4/#predefined-rec2020) color space. +pub const Rec2020 = struct { + /// The red component. + r: f32, + /// The green component. + g: f32, + /// The blue component. + b: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace BoundedColorGamut(@This()); + + pub usingnamespace color_conversions.convert_Rec2020; + + pub const ChannelTypeMap = .{ + .r = ChannelType{ .percentage = true }, + .g = ChannelType{ .percentage = true }, + .b = ChannelType{ .percentage = true }, + }; +}; + +/// A color in the [`xyz-d50`](https://www.w3.org/TR/css-color-4/#predefined-xyz) color space. +pub const XYZd50 = struct { + /// The x component. + x: f32, + /// The y component. + y: f32, + /// The z component. + z: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace UnboundedColorGamut(@This()); + + pub usingnamespace DeriveInterpolate(@This(), "x", "y", "z"); + pub usingnamespace RecangularPremultiply(@This(), "x", "y", "z"); + + pub usingnamespace color_conversions.convert_XYZd50; + + pub const ChannelTypeMap = .{ + .x = ChannelType{ .percentage = true }, + .y = ChannelType{ .percentage = true }, + .z = ChannelType{ .percentage = true }, + }; +}; + +/// A color in the [`xyz-d65`](https://www.w3.org/TR/css-color-4/#predefined-xyz) color space. +pub const XYZd65 = struct { + /// The x component. + x: f32, + /// The y component. + y: f32, + /// The z component. + z: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace UnboundedColorGamut(@This()); + + pub usingnamespace DeriveInterpolate(@This(), "x", "y", "z"); + pub usingnamespace RecangularPremultiply(@This(), "x", "y", "z"); + + pub usingnamespace color_conversions.convert_XYZd65; + + pub const ChannelTypeMap = .{ + .x = ChannelType{ .percentage = true }, + .y = ChannelType{ .percentage = true }, + .z = ChannelType{ .percentage = true }, + }; + + pub fn adjustPowerlessComponents(_: *@This()) void {} + pub fn adjustHue(_: *@This(), _: *@This(), _: HueInterpolationMethod) void {} +}; + +/// A color in the [CIE LCH](https://www.w3.org/TR/css-color-4/#cie-lab) color space. +pub const LCH = struct { + /// The lightness component. + l: f32, + /// The chroma component. + c: f32, + /// The hue component. + h: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace UnboundedColorGamut(@This()); + + pub usingnamespace AdjustPowerlessLCH(@This()); + pub usingnamespace DeriveInterpolate(@This(), "l", "c", "h"); + pub usingnamespace RecangularPremultiply(@This(), "l", "c", "h"); + + pub usingnamespace color_conversions.convert_LCH; + + pub const ChannelTypeMap = .{ + .l = ChannelType{ .percentage = true }, + .c = ChannelType{ .number = true }, + .h = ChannelType{ .angle = true }, + }; +}; + +/// A color in the [OKLab](https://www.w3.org/TR/css-color-4/#ok-lab) color space. +pub const OKLAB = struct { + /// The lightness component. + l: f32, + /// The a component. + a: f32, + /// The b component. + b: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace UnboundedColorGamut(@This()); + + pub usingnamespace AdjustPowerlessLAB(@This()); + pub usingnamespace DeriveInterpolate(@This(), "l", "a", "b"); + pub usingnamespace RecangularPremultiply(@This(), "l", "a", "b"); + + pub usingnamespace color_conversions.convert_OKLAB; + + pub const ChannelTypeMap = .{ + .l = ChannelType{ .percentage = true }, + .a = ChannelType{ .number = true }, + .b = ChannelType{ .number = true }, + }; + + pub fn adjustHue(_: *@This(), _: *@This(), _: HueInterpolationMethod) void {} +}; + +/// A color in the [OKLCH](https://www.w3.org/TR/css-color-4/#ok-lab) color space. +pub const OKLCH = struct { + /// The lightness component. + l: f32, + /// The chroma component. + c: f32, + /// The hue component. + h: f32, + /// The alpha component. + alpha: f32, + + pub usingnamespace DefineColorspace(@This()); + pub usingnamespace UnboundedColorGamut(@This()); + + pub usingnamespace AdjustPowerlessLCH(@This()); + pub usingnamespace DeriveInterpolate(@This(), "l", "c", "h"); + pub usingnamespace RecangularPremultiply(@This(), "l", "c", "h"); + + pub usingnamespace color_conversions.convert_OKLCH; + + pub const ChannelTypeMap = .{ + .l = ChannelType{ .percentage = true }, + .c = ChannelType{ .number = true }, + .h = ChannelType{ .angle = true }, + }; +}; + +pub const ComponentParser = struct { + allow_none: bool, + from: ?RelativeComponentParser, + + pub fn new(allow_none: bool) ComponentParser { + return ComponentParser{ + .allow_none = allow_none, + .from = null, + }; + } + + /// `func` must be a function like: + /// fn (*css.Parser, *ComponentParser, ...args) + pub fn parseRelative( + this: *ComponentParser, + input: *css.Parser, + comptime T: type, + comptime C: type, + comptime func: anytype, + args_: anytype, + ) Result(C) { + if (input.tryParse(css.Parser.expectIdentMatching, .{"from"}).isOk()) { + const from = switch (CssColor.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return this.parseFrom(from, input, T, C, func, args_); + } + + const args = bun.meta.ConcatArgs2(func, input, this, args_); + return @call(.auto, func, args); + } + + pub fn parseFrom( + this: *ComponentParser, + from: CssColor, + input: *css.Parser, + comptime T: type, + comptime C: type, + comptime func: anytype, + args_: anytype, + ) Result(C) { + if (from == .light_dark) { + const state = input.state(); + const light = switch (this.parseFrom(from.light_dark.light.*, input, T, C, func, args_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + input.reset(&state); + const dark = switch (this.parseFrom(from.light_dark.dark.*, input, T, C, func, args_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = C.lightDarkOwned(input.allocator(), light, dark) }; + } + + const new_from = if (T.tryFromCssColor(&from)) |v| v.resolve() else return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + + this.from = RelativeComponentParser.new(&new_from); + + const args = bun.meta.ConcatArgs2(func, input, this, args_); + return @call(.auto, func, args); + } + + pub fn parseNumberOrPercentage(this: *const ComponentParser, input: *css.Parser) Result(NumberOrPercentage) { + if (this.from) |*from| { + if (input.tryParse(RelativeComponentParser.parseNumberOrPercentage, .{from}).asValue()) |res| { + return .{ .result = res }; + } + } + + if (input.tryParse(CSSNumberFns.parse, .{}).asValue()) |value| { + return .{ .result = NumberOrPercentage{ .number = .{ .value = value } } }; + } else if (input.tryParse(Percentage.parse, .{}).asValue()) |value| { + return .{ + .result = NumberOrPercentage{ + .percentage = .{ .unit_value = value.v }, + }, + }; + } else if (this.allow_none) { + if (input.expectIdentMatching("none").asErr()) |e| return .{ .err = e }; + return .{ .result = NumberOrPercentage{ + .number = .{ + .value = std.math.nan(f32), + }, + } }; + } else { + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + } + + pub fn parseAngleOrNumber(this: *const ComponentParser, input: *css.Parser) Result(css.color.AngleOrNumber) { + if (this.from) |*from| { + if (input.tryParse(RelativeComponentParser.parseAngleOrNumber, .{from}).asValue()) |res| { + return .{ .result = res }; + } + } + + if (input.tryParse(Angle.parse, .{}).asValue()) |angle| { + return .{ + .result = .{ + .angle = .{ + .degrees = angle.toDegrees(), + }, + }, + }; + } else if (input.tryParse(CSSNumberFns.parse, .{}).asValue()) |value| { + return .{ + .result = .{ + .number = .{ + .value = value, + }, + }, + }; + } else if (this.allow_none) { + if (input.expectIdentMatching("none").asErr()) |e| return .{ .err = e }; + return .{ + .result = .{ + .number = .{ + .value = std.math.nan(f32), + }, + }, + }; + } else { + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + } + + pub fn parsePercentage(this: *const ComponentParser, input: *css.Parser) Result(f32) { + if (this.from) |*from| { + if (input.tryParse(RelativeComponentParser.parsePercentage, .{from}).asValue()) |res| { + return .{ .result = res }; + } + } + + if (input.tryParse(Percentage.parse, .{}).asValue()) |val| { + return .{ .result = val.v }; + } else if (this.allow_none) { + if (input.expectIdentMatching("none").asErr()) |e| return .{ .err = e }; + return .{ .result = std.math.nan(f32) }; + } else { + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + } + + pub fn parseNumber(this: *const ComponentParser, input: *css.Parser) Result(f32) { + if (this.from) |*from| { + if (input.tryParse(RelativeComponentParser.parseNumber, .{from}).asValue()) |res| { + return .{ .result = res }; + } + } + + if (input.tryParse(CSSNumberFns.parse, .{}).asValue()) |val| { + return .{ .result = val }; + } else if (this.allow_none) { + if (input.expectIdentMatching("none").asErr()) |e| return .{ .err = e }; + return .{ .result = std.math.nan(f32) }; + } else { + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + } +}; + +/// Either a number or a percentage. +pub const NumberOrPercentage = union(enum) { + /// ``. + number: struct { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `` + percentage: struct { + /// The value as a float, divided by 100 so that the nominal range is + /// 0.0 to 1.0. + unit_value: f32, + }, + + /// Return the value as a percentage. + pub fn unitValue(this: *const NumberOrPercentage) f32 { + return switch (this.*) { + .number => |v| v.value, + .percentage => |v| v.unit_value, + }; + } + + /// Return the value as a number with a percentage adjusted to the + /// `percentage_basis`. + pub fn value(this: *const NumberOrPercentage, percentage_basis: f32) f32 { + return switch (this.*) { + .number => |v| v.value, + .percentage => |v| v.unit_value * percentage_basis, + }; + } +}; + +const RelativeComponentParser = struct { + names: struct { []const u8, []const u8, []const u8 }, + components: struct { f32, f32, f32, f32 }, + types: struct { ChannelType, ChannelType, ChannelType }, + + pub fn new(color: anytype) RelativeComponentParser { + return RelativeComponentParser{ + .names = color.channels(), + .components = color.components(), + .types = color.types(), + }; + } + + pub fn parseAngleOrNumber(input: *css.Parser, this: *const RelativeComponentParser) Result(css.color.AngleOrNumber) { + if (input.tryParse( + RelativeComponentParser.parseIdent, + .{ + this, + ChannelType{ .angle = true, .number = true }, + }, + ).asValue()) |value| { + return .{ .result = .{ + .number = .{ + .value = value, + }, + } }; + } + + if (input.tryParse( + RelativeComponentParser.parseCalc, + .{ + this, + ChannelType{ .angle = true, .number = true }, + }, + ).asValue()) |value| { + return .{ .result = .{ + .number = .{ + .value = value, + }, + } }; + } + + const Closure = struct { + angle: Angle, + parser: *const RelativeComponentParser, + pub fn tryParseFn(i: *css.Parser, t: *@This()) Result(Angle) { + if (Calc(Angle).parseWith(i, t, @This().calcParseIdentFn).asValue()) |val| { + if (val == .value) { + return .{ .result = val.value.* }; + } + } + return .{ .err = i.newCustomError(css.ParserError.invalid_value) }; + } + + pub fn calcParseIdentFn(t: *@This(), ident: []const u8) ?Calc(Angle) { + const value = t.parser.getIdent(ident, ChannelType{ .angle = true, .number = true }) orelse return null; + t.angle = .{ .deg = value }; + return Calc(Angle){ + .value = &t.angle, + }; + } + }; + var closure = Closure{ + .angle = undefined, + .parser = this, + }; + if (input.tryParse(Closure.tryParseFn, .{&closure}).asValue()) |value| { + return .{ .result = .{ + .angle = .{ + .degrees = value.toDegrees(), + }, + } }; + } + + return .{ .err = input.newErrorForNextToken() }; + } + + pub fn parseNumberOrPercentage(input: *css.Parser, this: *const RelativeComponentParser) Result(NumberOrPercentage) { + if (input.tryParse(RelativeComponentParser.parseIdent, .{ this, ChannelType{ .percentage = true, .number = true } }).asValue()) |value| { + return .{ .result = NumberOrPercentage{ .percentage = .{ .unit_value = value } } }; + } + + if (input.tryParse(RelativeComponentParser.parseCalc, .{ this, ChannelType{ .percentage = true, .number = true } }).asValue()) |value| { + return .{ .result = NumberOrPercentage{ + .percentage = .{ + .unit_value = value, + }, + } }; + } + + { + const Closure = struct { + parser: *const RelativeComponentParser, + percentage: Percentage = .{ .v = 0 }, + + pub fn parsefn(i: *css.Parser, self: *@This()) Result(Percentage) { + if (Calc(Percentage).parseWith(i, self, @This().calcparseident).asValue()) |calc_value| { + if (calc_value == .value) return .{ .result = calc_value.value.* }; + } + return .{ .err = i.newCustomError(css.ParserError.invalid_value) }; + } + + pub fn calcparseident(self: *@This(), ident: []const u8) ?Calc(Percentage) { + const v = self.parser.getIdent(ident, ChannelType{ .percentage = true, .number = true }) orelse return null; + self.percentage = .{ .v = v }; + // value variant is a *Percentage + // but we immediately dereference it and discard the pointer + // so using a field on this closure struct instead of making a gratuitous allocation + return .{ + .value = &self.percentage, + }; + } + }; + var closure = Closure{ + .parser = this, + }; + if (input.tryParse(Closure.parsefn, .{ + &closure, + }).asValue()) |value| { + return .{ .result = NumberOrPercentage{ + .percentage = .{ + .unit_value = value.v, + }, + } }; + } + } + + return .{ .err = input.newErrorForNextToken() }; + } + + pub fn parsePercentage( + input: *css.Parser, + this: *const RelativeComponentParser, + ) Result(f32) { + if (input.tryParse(RelativeComponentParser.parseIdent, .{ this, ChannelType{ .percentage = true } }).asValue()) |value| { + return .{ .result = value }; + } + + const Closure = struct { self: *const RelativeComponentParser, temp: Percentage = .{ .v = 0 } }; + var _closure = Closure{ .self = this }; + if (input.tryParse(struct { + pub fn parseFn(i: *css.Parser, closure: *Closure) Result(Percentage) { + const calc_value = switch (Calc(Percentage).parseWith(i, closure, parseIdentFn)) { + .result => |v| v, + .err => return .{ .err = i.newCustomError(css.ParserError.invalid_value) }, + }; + if (calc_value == .value) return .{ .result = calc_value.value.* }; + return .{ .err = i.newCustomError(css.ParserError.invalid_value) }; + } + + pub fn parseIdentFn(closure: *Closure, ident: []const u8) ?Calc(Percentage) { + const v = closure.self.getIdent(ident, ChannelType{ .percentage = true }) orelse return null; + closure.temp = .{ .v = v }; + return Calc(Percentage){ .value = &closure.temp }; + } + }.parseFn, .{&_closure}).asValue()) |value| { + return .{ .result = value.v }; + } + + return .{ .err = input.newErrorForNextToken() }; + } + + pub fn parseNumber( + input: *css.Parser, + this: *const RelativeComponentParser, + ) Result(f32) { + if (input.tryParse( + RelativeComponentParser.parseIdent, + .{ this, ChannelType{ .number = true } }, + ).asValue()) |value| { + return .{ .result = value }; + } + + if (input.tryParse( + RelativeComponentParser.parseCalc, + .{ this, ChannelType{ .number = true } }, + ).asValue()) |value| { + return .{ .result = value }; + } + + return .{ .err = input.newErrorForNextToken() }; + } + + pub fn parseIdent( + input: *css.Parser, + this: *const RelativeComponentParser, + allowed_types: ChannelType, + ) Result(f32) { + const v = this.getIdent( + switch (input.expectIdent()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }, + allowed_types, + ) orelse return .{ .err = input.newErrorForNextToken() }; + return .{ .result = v }; + } + + pub fn parseCalc( + input: *css.Parser, + this: *const RelativeComponentParser, + allowed_types: ChannelType, + ) Result(f32) { + const Closure = struct { + p: *const RelativeComponentParser, + allowed_types: ChannelType, + + pub fn parseIdentFn(self: *@This(), ident: []const u8) ?Calc(f32) { + const v = self.p.getIdent(ident, self.allowed_types) orelse return null; + return .{ .number = v }; + } + }; + var closure = Closure{ + .p = this, + .allowed_types = allowed_types, + }; + if (Calc(f32).parseWith(input, &closure, Closure.parseIdentFn).asValue()) |calc_val| { + // PERF: I don't like this redundant allocation + if (calc_val == .value) return .{ .result = calc_val.value.* }; + if (calc_val == .number) return .{ .result = calc_val.number }; + } + return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + } + + pub fn getIdent( + this: *const RelativeComponentParser, + ident: []const u8, + allowed_types: ChannelType, + ) ?f32 { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, this.names[0]) and allowed_types.intersects(this.types[0])) { + return this.components[0]; + } + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, this.names[1]) and allowed_types.intersects(this.types[1])) { + return this.components[1]; + } + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, this.names[2]) and allowed_types.intersects(this.types[2])) { + return this.components[2]; + } + + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "alpha") and allowed_types.intersects(ChannelType{ .percentage = true })) { + return this.components[3]; + } + + return null; + } +}; + +/// A channel type for a color space. +/// TODO(zack): why tf is this bitflags? +pub const ChannelType = packed struct(u8) { + /// Channel represents a percentage. + percentage: bool = false, + /// Channel represents an angle. + angle: bool = false, + /// Channel represents a number. + number: bool = false, + __unused: u5 = 0, + + pub usingnamespace css.Bitflags(@This()); +}; + +pub fn parsePredefined(input: *css.Parser, parser: *ComponentParser) Result(CssColor) { + // https://www.w3.org/TR/css-color-4/#color-function + const Closure = struct { + p: *ComponentParser, + pub fn parseNestedBlockFn(this: *@This(), i: *css.Parser) Result(CssColor) { + const from: ?CssColor = if (i.tryParse(css.Parser.expectIdentMatching, .{"from"}).isOk()) + switch (CssColor.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } + else + null; + + const colorspace = switch (i.expectIdent()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + if (from) |f| { + if (f == .light_dark) { + const state = i.state(); + const light = switch (parsePredefinedRelative(i, this.p, colorspace, f.light_dark.light)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + i.reset(&state); + const dark = switch (parsePredefinedRelative(i, this.p, colorspace, f.light_dark.dark)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = CssColor{ + .light_dark = .{ + .light = bun.create( + i.allocator(), + CssColor, + light, + ), + .dark = bun.create( + i.allocator(), + CssColor, + dark, + ), + }, + } }; + } + } + + return parsePredefinedRelative(i, this.p, colorspace, if (from) |*f| f else null); + } + }; + + var closure = Closure{ + .p = parser, + }; + + const res = switch (input.parseNestedBlock(CssColor, &closure, Closure.parseNestedBlockFn)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + return .{ .result = res }; +} + +pub fn parsePredefinedRelative( + input: *css.Parser, + parser: *ComponentParser, + colorspace: []const u8, + _from: ?*const CssColor, +) Result(CssColor) { + const location = input.currentSourceLocation(); + if (_from) |from| { + parser.from = set_from: { + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("srgb", colorspace)) { + break :set_from RelativeComponentParser.new( + if (SRGB.tryFromCssColor(from)) |v| v.resolveMissing() else return .{ .err = input.newCustomError(css.ParserError.invalid_value) }, + ); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("srgb-linear", colorspace)) { + break :set_from RelativeComponentParser.new( + if (SRGBLinear.tryFromCssColor(from)) |v| v.resolveMissing() else return .{ .err = input.newCustomError(css.ParserError.invalid_value) }, + ); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("display-p3", colorspace)) { + break :set_from RelativeComponentParser.new( + if (P3.tryFromCssColor(from)) |v| v.resolveMissing() else return .{ .err = input.newCustomError(css.ParserError.invalid_value) }, + ); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("a98-rgb", colorspace)) { + break :set_from RelativeComponentParser.new( + if (A98.tryFromCssColor(from)) |v| v.resolveMissing() else return .{ .err = input.newCustomError(css.ParserError.invalid_value) }, + ); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("prophoto-rgb", colorspace)) { + break :set_from RelativeComponentParser.new( + if (ProPhoto.tryFromCssColor(from)) |v| v.resolveMissing() else return .{ .err = input.newCustomError(css.ParserError.invalid_value) }, + ); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("rec2020", colorspace)) { + break :set_from RelativeComponentParser.new( + if (Rec2020.tryFromCssColor(from)) |v| v.resolveMissing() else return .{ .err = input.newCustomError(css.ParserError.invalid_value) }, + ); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("xyz-d50", colorspace)) { + break :set_from RelativeComponentParser.new( + if (XYZd50.tryFromCssColor(from)) |v| v.resolveMissing() else return .{ .err = input.newCustomError(css.ParserError.invalid_value) }, + ); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("xyz", colorspace) or + bun.strings.eqlCaseInsensitiveASCIIICheckLength("xyz-d65", colorspace)) + { + break :set_from RelativeComponentParser.new( + if (XYZd65.tryFromCssColor(from)) |v| v.resolveMissing() else return .{ .err = input.newCustomError(css.ParserError.invalid_value) }, + ); + } else { + return .{ .err = location.newUnexpectedTokenError(.{ .ident = colorspace }) }; + } + }; + } + + // Out of gamut values should not be clamped, i.e. values < 0 or > 1 should be preserved. + // The browser will gamut-map the color for the target device that it is rendered on. + const a = switch (input.tryParse(parseNumberOrPercentage, .{parser})) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const b = switch (input.tryParse(parseNumberOrPercentage, .{parser})) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const c = switch (input.tryParse(parseNumberOrPercentage, .{parser})) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const alpha = switch (parseAlpha(input, parser)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + const predefined: PredefinedColor = predefined: { + const Variants = enum { + srgb, + @"srgb-linear", + @"display-p3", + @"a99-rgb", + @"prophoto-rgb", + rec2020, + @"xyz-d50", + @"xyz-d65", + xyz, + }; + const Map = bun.ComptimeEnumMap(Variants); + if (Map.getAnyCase(colorspace)) |ret| { + switch (ret) { + .srgb => break :predefined .{ .srgb = SRGB{ + .r = a, + .g = b, + .b = c, + .alpha = alpha, + } }, + .@"srgb-linear" => break :predefined .{ .srgb_linear = SRGBLinear{ + .r = a, + .g = b, + .b = c, + .alpha = alpha, + } }, + .@"display-p3" => break :predefined .{ .display_p3 = P3{ + .r = a, + .g = b, + .b = c, + .alpha = alpha, + } }, + .@"a99-rgb" => break :predefined .{ .a98 = A98{ + .r = a, + .g = b, + .b = c, + .alpha = alpha, + } }, + .@"prophoto-rgb" => break :predefined .{ .prophoto = ProPhoto{ + .r = a, + .g = b, + .b = c, + .alpha = alpha, + } }, + .rec2020 => break :predefined .{ .rec2020 = Rec2020{ + .r = a, + .g = b, + .b = c, + .alpha = alpha, + } }, + .@"xyz-d50" => break :predefined .{ .xyz_d50 = XYZd50{ + .x = a, + .y = b, + .z = c, + .alpha = alpha, + } }, + .@"xyz-d65", .xyz => break :predefined .{ .xyz_d65 = XYZd65{ + .x = a, + .y = b, + .z = c, + .alpha = alpha, + } }, + } + } else return .{ .err = location.newUnexpectedTokenError(.{ .ident = colorspace }) }; + }; + + return .{ .result = .{ + .predefined = bun.create( + input.allocator(), + PredefinedColor, + predefined, + ), + } }; +} + +/// A [color space](https://www.w3.org/TR/css-color-4/#interpolation-space) keyword +/// used in interpolation functions such as `color-mix()`. +pub const ColorSpaceName = enum { + srgb, + @"srgb-linear", + lab, + oklab, + xyz, + @"xyz-d50", + @"xyz-d65", + hsl, + hwb, + lch, + oklch, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + 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 parseColorMix(input: *css.Parser) Result(CssColor) { + if (input.expectIdentMatching("in").asErr()) |e| return .{ .err = e }; + const method = switch (ColorSpaceName.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + const hue_method_: Result(HueInterpolationMethod) = if (switch (method) { + .hsl, .hwb, .lch, .oklch => true, + else => false, + }) brk: { + const hue_method = input.tryParse(HueInterpolationMethod.parse, .{}); + if (hue_method.isOk()) { + if (input.expectIdentMatching("hue").asErr()) |e| return .{ .err = e }; + } + break :brk hue_method; + } else .{ .result = HueInterpolationMethod.shorter }; + + const hue_method = hue_method_.unwrapOr(HueInterpolationMethod.shorter); + + const first_percent_ = input.tryParse(css.Parser.expectPercentage, .{}); + const first_color = switch (CssColor.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const first_percent = switch (first_percent_) { + .result => |v| v, + .err => switch (input.tryParse(css.Parser.expectPercentage, .{})) { + .result => |vv| vv, + .err => null, + }, + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + + const second_percent_ = input.tryParse(css.Parser.expectPercentage, .{}); + const second_color = switch (CssColor.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const second_percent = switch (second_percent_) { + .result => |vv| vv, + .err => switch (input.tryParse(css.Parser.expectPercentage, .{})) { + .result => |vv| vv, + .err => null, + }, + }; + + // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm + const p1, const p2 = if (first_percent == null and second_percent == null) .{ 0.5, 0.5 } else brk: { + const p2 = second_percent orelse (1.0 - first_percent.?); + const p1 = first_percent orelse (1.0 - second_percent.?); + break :brk .{ p1, p2 }; + }; + + if ((p1 + p2) == 0.0) return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + + const result = switch (method) { + .srgb => first_color.interpolate(input.allocator(), SRGB, p1, &second_color, p2, hue_method), + .@"srgb-linear" => first_color.interpolate(input.allocator(), SRGBLinear, p1, &second_color, p2, hue_method), + .hsl => first_color.interpolate(input.allocator(), HSL, p1, &second_color, p2, hue_method), + .hwb => first_color.interpolate(input.allocator(), HWB, p1, &second_color, p2, hue_method), + .lab => first_color.interpolate(input.allocator(), LAB, p1, &second_color, p2, hue_method), + .lch => first_color.interpolate(input.allocator(), LCH, p1, &second_color, p2, hue_method), + .oklab => first_color.interpolate(input.allocator(), OKLAB, p1, &second_color, p2, hue_method), + .oklch => first_color.interpolate(input.allocator(), OKLCH, p1, &second_color, p2, hue_method), + .xyz, .@"xyz-d65" => first_color.interpolate(input.allocator(), XYZd65, p1, &second_color, p2, hue_method), + .@"xyz-d50" => first_color.interpolate(input.allocator(), XYZd65, p1, &second_color, p2, hue_method), + } orelse return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + + return .{ .result = result }; +} + +/// A hue [interpolation method](https://www.w3.org/TR/css-color-4/#typedef-hue-interpolation-method) +/// used in interpolation functions such as `color-mix()`. +pub const HueInterpolationMethod = enum { + /// Angles are adjusted so that θ₂ - θ₁ ∈ [-180, 180]. + shorter, + /// Angles are adjusted so that θ₂ - θ₁ ∈ {0, [180, 360)}. + longer, + /// Angles are adjusted so that θ₂ - θ₁ ∈ [0, 360). + increasing, + /// Angles are adjusted so that θ₂ - θ₁ ∈ (-360, 0]. + decreasing, + /// No fixup is performed. Angles are interpolated in the same way as every other component. + specified, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + 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 interpolate( + this: *const HueInterpolationMethod, + a: *f32, + b: *f32, + ) void { + // https://drafts.csswg.org/css-color/#hue-interpolation + if (this.* == .specified) { + // a.* = ((a.* % 360.0) + 360.0) % 360.0; + // b.* = ((b.* % 360.0) + 360.0) % 360.0; + a.* = @mod((@mod(a.*, 360.0) + 360.0), 360.0); + b.* = @mod((@mod(b.*, 360.0) + 360.0), 360.0); + } + + switch (this.*) { + .shorter => { + // https://www.w3.org/TR/css-color-4/#hue-shorter + const delta = b.* - a.*; + if (delta > 180.0) { + a.* += 360.0; + } else if (delta < -180.0) { + b.* += 360.0; + } + }, + .longer => { + // https://www.w3.org/TR/css-color-4/#hue-longer + const delta = b.* - a.*; + if (0.0 < delta and delta < 180.0) { + a.* += 360.0; + } else if (-180.0 < delta and delta < 0.0) { + b.* += 360.0; + } + }, + .increasing => { + // https://www.w3.org/TR/css-color-4/#hue-decreasing + if (b.* < a.*) { + b.* += 360.0; + } + }, + .decreasing => { + // https://www.w3.org/TR/css-color-4/#hue-decreasing + if (a.* < b.*) { + a.* += 360.0; + } + }, + .specified => {}, + } + } +}; + +fn rectangularToPolar(l: f32, a: f32, b: f32) struct { f32, f32, f32 } { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L375 + + var h = std.math.atan2(a, b) * 180.0 / std.math.pi; + if (h < 0.0) { + h += 360.0; + } + + // const c = @sqrt(std.math.powi(f32, a, 2) + std.math.powi(f32, b, 2)); + // PERF: Zig does not have Rust's f32::powi + const c = @sqrt(std.math.pow(f32, a, 2) + std.math.pow(f32, b, 2)); + + // h = h % 360.0; + h = @mod(h, 360.0); + return .{ l, c, h }; +} + +pub fn DefineColorspace(comptime T: type) type { + if (!@hasDecl(T, "ChannelTypeMap")) { + @compileError("A Colorspace must define a ChannelTypeMap"); + } + const ChannelTypeMap = T.ChannelTypeMap; + + const fields: []const std.builtin.Type.StructField = std.meta.fields(T); + const a = fields[0].name; + const b = fields[1].name; + const c = fields[2].name; + const alpha = "alpha"; + if (!@hasField(T, "alpha")) { + @compileError("A Colorspace must define an alpha field"); + } + + if (!@hasField(@TypeOf(ChannelTypeMap), a)) { + @compileError("A Colorspace must define a field for each channel, missing: " ++ a); + } + if (!@hasField(@TypeOf(ChannelTypeMap), b)) { + @compileError("A Colorspace must define a field for each channel, missing: " ++ b); + } + if (!@hasField(@TypeOf(ChannelTypeMap), c)) { + @compileError("A Colorspace must define a field for each channel, missing: " ++ c); + } + + // e.g. T = LAB, so then: into_this_function_name = "intoLAB" + const into_this_function_name = "into" ++ comptime bun.meta.typeName(T); + + return struct { + pub fn components(this: *const T) struct { f32, f32, f32, f32 } { + return .{ + @field(this, a), + @field(this, b), + @field(this, c), + @field(this, alpha), + }; + } + + pub fn channels(_: *const T) struct { []const u8, []const u8, []const u8 } { + return .{ a, b, c }; + } + + pub fn types(_: *const T) struct { ChannelType, ChannelType, ChannelType } { + return .{ + @field(ChannelTypeMap, a), + @field(ChannelTypeMap, b), + @field(ChannelTypeMap, c), + }; + } + + pub fn resolveMissing(this: *const T) T { + var result: T = this.*; + @field(result, a) = if (std.math.isNan(@field(this, a))) 0.0 else @field(this, a); + @field(result, b) = if (std.math.isNan(@field(this, b))) 0.0 else @field(this, b); + @field(result, c) = if (std.math.isNan(@field(this, c))) 0.0 else @field(this, c); + @field(result, alpha) = if (std.math.isNan(@field(this, alpha))) 0.0 else @field(this, alpha); + return result; + } + + pub fn resolve(this: *const T) T { + var resolved = resolveMissing(this); + if (!resolved.inGamut()) { + resolved = mapGamut(T, resolved); + } + return resolved; + } + + pub fn fromLABColor(color: *const LABColor) T { + return switch (color.*) { + .lab => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .lch => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .oklab => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .oklch => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + }; + } + + pub fn fromPredefinedColor(color: *const PredefinedColor) T { + return switch (color.*) { + .srgb => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .srgb_linear => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .display_p3 => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .a98 => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .prophoto => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .rec2020 => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .xyz_d50 => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .xyz_d65 => |*v| { + if (comptime @TypeOf(v.*) == T) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + }; + } + + pub fn fromFloatColor(color: *const FloatColor) T { + return switch (color.*) { + .rgb => |*v| { + if (comptime T == SRGB) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .hsl => |*v| { + if (comptime T == HSL) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + .hwb => |*v| { + if (comptime T == HWB) return v.*; + return @call(.auto, @field(@TypeOf(v.*), into_this_function_name), .{v}); + }, + }; + } + + pub fn tryFromCssColor(color: *const CssColor) ?T { + return switch (color.*) { + .rgba => |*rgba| { + if (comptime T == RGBA) return rgba.*; + return @call(.auto, @field(@TypeOf(rgba.*), into_this_function_name), .{rgba}); + }, + .lab => |lab| fromLABColor(lab), + .predefined => |predefined| fromPredefinedColor(predefined), + .float => |float| fromFloatColor(float), + .current_color => null, + .light_dark => null, + .system => null, + }; + } + }; +} + +pub fn BoundedColorGamut(comptime T: type) type { + const fields: []const std.builtin.Type.StructField = std.meta.fields(T); + const a = fields[0].name; + const b = fields[1].name; + const c = fields[2].name; + return struct { + pub fn inGamut(this: *const T) bool { + return @field(this, a) >= 0.0 and + @field(this, a) <= 1.0 and + @field(this, b) >= 0.0 and + @field(this, b) <= 1.0 and + @field(this, c) >= 0.0 and + @field(this, c) <= 1.0; + } + + pub fn clip(this: *const T) T { + var result: T = this.*; + @field(result, a) = bun.clamp(@field(this, a), 0.0, 1.0); + @field(result, b) = bun.clamp(@field(this, b), 0.0, 1.0); + @field(result, c) = bun.clamp(@field(this, c), 0.0, 1.0); + result.alpha = bun.clamp(this.alpha, 0.0, 1.0); + return result; + } + }; +} + +pub fn DeriveInterpolate( + comptime T: type, + comptime a: []const u8, + comptime b: []const u8, + comptime c: []const u8, +) type { + if (!@hasField(T, a)) @compileError("Missing field: " ++ a); + if (!@hasField(T, b)) @compileError("Missing field: " ++ b); + if (!@hasField(T, c)) @compileError("Missing field: " ++ c); + + return struct { + pub fn fillMissingComponents(this: *T, other: *T) void { + if (std.math.isNan(@field(this, a))) { + @field(this, a) = @field(other, a); + } + + if (std.math.isNan(@field(this, b))) { + @field(this, b) = @field(other, b); + } + + if (std.math.isNan(@field(this, c))) { + @field(this, c) = @field(other, c); + } + + if (std.math.isNan(this.alpha)) { + this.alpha = other.alpha; + } + } + + pub fn interpolate(this: *const T, p1: f32, other: *const T, p2: f32) T { + var result: T = undefined; + @field(result, a) = @field(this, a) * p1 + @field(other, a) * p2; + @field(result, b) = @field(this, b) * p1 + @field(other, b) * p2; + @field(result, c) = @field(this, c) * p1 + @field(other, c) * p2; + result.alpha = this.alpha * p1 + other.alpha * p2; + return result; + } + }; +} + +// pub fn DerivePredefined(comptime T: type, comptime predefined_color_field: []const u8) type { +// return struct { +// pub fn +// }; +// } + +pub fn RecangularPremultiply( + comptime T: type, + comptime a: []const u8, + comptime b: []const u8, + comptime c: []const u8, +) type { + if (!@hasField(T, a)) @compileError("Missing field: " ++ a); + if (!@hasField(T, b)) @compileError("Missing field: " ++ b); + if (!@hasField(T, c)) @compileError("Missing field: " ++ c); + return struct { + pub fn premultiply(this: *T) void { + if (!std.math.isNan(this.alpha)) { + @field(this, a) *= this.alpha; + @field(this, b) *= this.alpha; + @field(this, c) *= this.alpha; + } + } + + pub fn unpremultiply(this: *T, alpha_multiplier: f32) void { + if (!std.math.isNan(this.alpha) and this.alpha != 0.0) { + // PERF: precalculate 1/alpha? + @field(this, a) /= this.alpha; + @field(this, b) /= this.alpha; + @field(this, c) /= this.alpha; + this.alpha *= alpha_multiplier; + } + } + }; +} + +pub fn PolarPremultiply( + comptime T: type, + comptime a: []const u8, + comptime b: []const u8, +) type { + if (!@hasField(T, a)) @compileError("Missing field: " ++ a); + if (!@hasField(T, b)) @compileError("Missing field: " ++ b); + return struct { + pub fn premultiply(this: *T) void { + if (!std.math.isNan(this.alpha)) { + @field(this, a) *= this.alpha; + @field(this, b) *= this.alpha; + } + } + + pub fn unpremultiply(this: *T, alpha_multiplier: f32) void { + // this.h %= 360.0; + this.h = @mod(this.h, 360.0); + if (!std.math.isNan(this.alpha)) { + // PERF: precalculate 1/alpha? + @field(this, a) /= this.alpha; + @field(this, b) /= this.alpha; + this.alpha *= alpha_multiplier; + } + } + }; +} + +pub fn AdjustPowerlessLAB(comptime T: type) type { + return struct { + pub fn adjustPowerlessComponents(this: *T) void { + // If the lightness of a LAB color is 0%, both the a and b components are powerless. + if (@abs(this.l) < std.math.floatEps(f32)) { + this.a = std.math.nan(f32); + this.b = std.math.nan(f32); + } + } + }; +} + +pub fn AdjustPowerlessLCH(comptime T: type) type { + return struct { + pub fn adjustPowerlessComponents(this: *T) void { + // If the chroma of an LCH color is 0%, the hue component is powerless. + // If the lightness of an LCH color is 0%, both the hue and chroma components are powerless. + if (@abs(this.c) < std.math.floatEps(f32)) { + this.h = std.math.nan(f32); + } + + if (@abs(this.l) < std.math.floatEps(f32)) { + this.c = std.math.nan(f32); + this.h = std.math.nan(f32); + } + } + + pub fn adjustHue(this: *T, other: *T, method: HueInterpolationMethod) void { + _ = method.interpolate(&this.h, &other.h); + } + }; +} + +pub fn shortColorName(v: u32) ?[]const u8 { + // These names are shorter than their hex codes + return switch (v) { + 0x000080 => "navy", + 0x008000 => "green", + 0x008080 => "teal", + 0x4b0082 => "indigo", + 0x800000 => "maroon", + 0x800080 => "purple", + 0x808000 => "olive", + 0x808080 => "gray", + 0xa0522d => "sienna", + 0xa52a2a => "brown", + 0xc0c0c0 => "silver", + 0xcd853f => "peru", + 0xd2b48c => "tan", + 0xda70d6 => "orchid", + 0xdda0dd => "plum", + 0xee82ee => "violet", + 0xf0e68c => "khaki", + 0xf0ffff => "azure", + 0xf5deb3 => "wheat", + 0xf5f5dc => "beige", + 0xfa8072 => "salmon", + 0xfaf0e6 => "linen", + 0xff0000 => "red", + 0xff6347 => "tomato", + 0xff7f50 => "coral", + 0xffa500 => "orange", + 0xffc0cb => "pink", + 0xffd700 => "gold", + 0xffe4c4 => "bisque", + 0xfffafa => "snow", + 0xfffff0 => "ivory", + else => return null, + }; +} + +// From esbuild: https://github.com/evanw/esbuild/blob/18e13bdfdca5cd3c7a2fae1a8bd739f8f891572c/internal/css_parser/css_decls_color.go#L218 +// 0xAABBCCDD => 0xABCD +pub fn compactHex(v: u32) u32 { + return ((v & 0x0FF00000) >> 12) | ((v & 0x00000FF0) >> 4); +} + +// 0xABCD => 0xAABBCCDD +pub fn expandHex(v: u32) u32 { + return ((v & 0xF000) << 16) | + ((v & 0xFF00) << 12) | + ((v & 0x0FF0) << 8) | + ((v & 0x00FF) << 4) | + (v & 0x000F); +} + +pub fn writeComponents( + name: []const u8, + a: f32, + b: f32, + c: f32, + alpha: f32, + comptime W: type, + dest: *Printer(W), +) PrintErr!void { + try dest.writeStr(name); + try dest.writeChar('('); + if (std.math.isNan(a)) { + try dest.writeStr("none"); + } else { + try (Percentage{ .v = a }).toCss(W, dest); + } + try dest.writeChar(' '); + try writeComponent(b, W, dest); + try dest.writeChar(' '); + try writeComponent(c, W, dest); + if (std.math.isNan(alpha) or @abs(alpha - 1.0) > std.math.floatEps(f32)) { + try dest.delim('/', true); + try writeComponent(alpha, W, dest); + } + return dest.writeChar(')'); +} + +pub fn writeComponent(c: f32, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (std.math.isNan(c)) { + return dest.writeStr("none"); + } else { + return CSSNumberFns.toCss(&c, W, dest); + } +} + +pub fn writePredefined( + predefined: *const PredefinedColor, + comptime W: type, + dest: *Printer(W), +) PrintErr!void { + const name, const a, const b, const c, const alpha = switch (predefined.*) { + .srgb => |*rgb| .{ "srgb", rgb.r, rgb.g, rgb.b, rgb.alpha }, + .srgb_linear => |*rgb| .{ "srgb-linear", rgb.r, rgb.g, rgb.b, rgb.alpha }, + .display_p3 => |*rgb| .{ "display-p3", rgb.r, rgb.g, rgb.b, rgb.alpha }, + .a98 => |*rgb| .{ "a98-rgb", rgb.r, rgb.g, rgb.b, rgb.alpha }, + .prophoto => |*rgb| .{ "prophoto-rgb", rgb.r, rgb.g, rgb.b, rgb.alpha }, + .rec2020 => |*rgb| .{ "rec2020", rgb.r, rgb.g, rgb.b, rgb.alpha }, + .xyz_d50 => |*xyz| .{ "xyz-d50", xyz.x, xyz.y, xyz.z, xyz.alpha }, + // "xyz" has better compatibility (Safari 15) than "xyz-d65", and it is shorter. + .xyz_d65 => |*xyz| .{ "xyz", xyz.x, xyz.y, xyz.z, xyz.alpha }, + }; + + try dest.writeStr("color("); + try dest.writeStr(name); + try dest.writeChar(' '); + try writeComponent(a, W, dest); + try dest.writeChar(' '); + try writeComponent(b, W, dest); + try dest.writeChar(' '); + try writeComponent(c, W, dest); + + if (std.math.isNan(alpha) or @abs(alpha - 1.0) > std.math.floatEps(f32)) { + try dest.delim('/', true); + try writeComponent(alpha, W, dest); + } + + return dest.writeChar(')'); +} + +pub fn gamSrgb(r: f32, g: f32, b: f32) struct { f32, f32, f32 } { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L31 + // convert an array of linear-light sRGB values in the range 0.0-1.0 + // to gamma corrected form + // https://en.wikipedia.org/wiki/SRGB + // Extended transfer function: + // For negative values, linear portion extends on reflection + // of axis, then uses reflected pow below that + + const Helpers = struct { + pub fn gamSrgbComponent(c: f32) f32 { + const abs = @abs(c); + if (abs > 0.0031308) { + const sign: f32 = if (c < 0.0) @as(f32, -1.0) else @as(f32, 1.0); + + return sign * (1.055 * std.math.pow(f32, abs, 1.0 / 2.4) - 0.055); + } + + return 12.92 * c; + } + }; + + return .{ + Helpers.gamSrgbComponent(r), + Helpers.gamSrgbComponent(g), + Helpers.gamSrgbComponent(b), + }; +} + +pub fn linSrgb(r: f32, g: f32, b: f32) struct { f32, f32, f32 } { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L11 + // convert sRGB values where in-gamut values are in the range [0 - 1] + // to linear light (un-companded) form. + // https://en.wikipedia.org/wiki/SRGB + // Extended transfer function: + // for negative values, linear portion is extended on reflection of axis, + // then reflected power function is used. + + const H = struct { + pub fn linSrgbComponent(c: f32) f32 { + const abs = @abs(c); + if (abs < 0.04045) { + return c / 12.92; + } + + const sign: f32 = if (c < 0.0) -1.0 else 1.0; + return sign * std.math.pow( + f32, + ((abs + 0.055) / 1.055), + 2.4, + ); + } + }; + + return .{ + H.linSrgbComponent(r), + H.linSrgbComponent(g), + H.linSrgbComponent(b), + }; +} + +/// PERF: SIMD? +pub fn multiplyMatrix(m: *const [9]f32, x: f32, y: f32, z: f32) struct { f32, f32, f32 } { + const a = m[0] * x + m[1] * y + m[2] * z; + const b = m[3] * x + m[4] * y + m[5] * z; + const c = m[6] * x + m[7] * y + m[8] * z; + return .{ a, b, c }; +} + +pub fn polarToRectangular(l: f32, c: f32, h: f32) struct { f32, f32, f32 } { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L385 + + const a = c * @cos(h * std.math.pi / 180.0); + const b = c * @sin(h * std.math.pi / 180.0); + return .{ l, a, b }; +} + +const D50: []const f32 = &.{ 0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585 }; + +const color_conversions = struct { + const generated = @import("./color_generated.zig").generated_color_conversions; + + pub const convert_RGBA = struct { + pub usingnamespace generated.convert_RGBA; + }; + + pub const convert_LAB = struct { + pub usingnamespace generated.convert_LAB; + + pub fn intoCssColor(c: *const LAB, allocator: Allocator) CssColor { + return CssColor{ .lab = bun.create( + allocator, + LABColor, + LABColor{ .lab = c.* }, + ) }; + } + + pub fn intoLCH(_lab: *const LAB) LCH { + const lab = _lab.resolveMissing(); + const l, const c, const h = rectangularToPolar(lab.l, lab.a, lab.b); + return LCH{ + .l = l, + .c = c, + .h = h, + .alpha = lab.alpha, + }; + } + + pub fn intoXYZd50(_lab: *const LAB) XYZd50 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L352 + const K: f32 = 24389.0 / 27.0; // 29^3/3^3 + const E: f32 = 216.0 / 24389.0; // 6^3/29^3 + + const lab = _lab.resolveMissing(); + const l = lab.l * 100.0; + const a = lab.a; + const b = lab.b; + + // compute f, starting with the luminance-related term + const f1 = (l + 16.0) / 116.0; + const f0 = a / 500.0 + f1; + const f2 = f1 - b / 200.0; + + // compute xyz + const x = if (std.math.pow(f32, f0, 3) > E) + std.math.pow(f32, f0, 3) + else + (116.0 * f0 - 16.0) / K; + + const y = if (l > K * E) std.math.pow(f32, (l + 16.0) / 116.0, 3) else l / K; + + const z = if (std.math.pow(f32, f2, 3) > E) + std.math.pow(f32, f0, 3) + else + (116.0 * f2 - 16.0) / K; + + // Compute XYZ by scaling xyz by reference white + return XYZd50{ + .x = x * D50[0], + .y = y * D50[1], + .z = z * D50[2], + .alpha = lab.alpha, + }; + } + }; + + pub const convert_SRGB = struct { + pub usingnamespace generated.convert_SRGB; + + pub fn intoCssColor(srgb: *const SRGB, _: Allocator) CssColor { + // TODO: should we serialize as color(srgb, ...)? + // would be more precise than 8-bit color. + return CssColor{ .rgba = srgb.intoRGBA() }; + } + + pub fn intoSRGBLinear(rgb: *const SRGB) SRGBLinear { + const srgb = rgb.resolveMissing(); + const r, const g, const b = linSrgb(srgb.r, srgb.g, srgb.b); + return SRGBLinear{ + .r = r, + .g = g, + .b = b, + .alpha = srgb.alpha, + }; + } + + pub fn intoHSL(_rgb: *const SRGB) HSL { + // https://drafts.csswg.org/css-color/#rgb-to-hsl + const rgb = _rgb.resolve(); + const r = rgb.r; + const g = rgb.g; + const b = rgb.b; + const max = @max( + @max(r, g), + b, + ); + const min = @min(@min(r, g), b); + var h = std.math.nan(f32); + var s: f32 = 0.0; + const l = (min + max) / 2.0; + const d = max - min; + + if (d != 0.0) { + s = if (l == 0.0 or l == 1.0) + 0.0 + else + (max - l) / @min(l, 1.0 - l); + + if (max == r) { + h = (g - b) / d + (if (g < b) @as(f32, 6.0) else @as(f32, 0.0)); + } else if (max == g) { + h = (b - r) / d + 2.0; + } else if (max == b) { + h = (r - g) / d + 4.0; + } + + h = h * 60.0; + } + + return HSL{ + .h = h, + .s = s, + .l = l, + .alpha = rgb.alpha, + }; + } + + pub fn intoHWB(_rgb: *const SRGB) HWB { + const rgb = _rgb.resolve(); + const hsl = rgb.intoHSL(); + const r = rgb.r; + const g = rgb.g; + const _b = rgb.b; + const w = @min(@min(r, g), _b); + const b = 1.0 - @max(@max(r, g), _b); + return HWB{ + .h = hsl.h, + .w = w, + .b = b, + .alpha = rgb.alpha, + }; + } + }; + + pub const convert_HSL = struct { + pub usingnamespace generated.convert_HSL; + + pub fn intoCssColor(c: *const HSL, _: Allocator) CssColor { + // TODO: should we serialize as color(srgb, ...)? + // would be more precise than 8-bit color. + return CssColor{ .rgba = c.intoRGBA() }; + } + + pub fn intoSRGB(hsl_: *const HSL) SRGB { + // https://drafts.csswg.org/css-color/#hsl-to-rgb + const hsl = hsl_.resolveMissing(); + const h = (hsl.h - 360.0 * @floor(hsl.h / 360.0)) / 360.0; + const r, const g, const b = css.color.hslToRgb(h, hsl.s, hsl.l); + return SRGB{ + .r = r, + .g = g, + .b = b, + .alpha = hsl.alpha, + }; + } + }; + + pub const convert_HWB = struct { + pub usingnamespace generated.convert_HWB; + + pub fn intoCssColor(c: *const HWB, _: Allocator) CssColor { + // TODO: should we serialize as color(srgb, ...)? + // would be more precise than 8-bit color. + return CssColor{ .rgba = c.intoRGBA() }; + } + + pub fn intoSRGB(_hwb: *const HWB) SRGB { + // https://drafts.csswg.org/css-color/#hwb-to-rgb + const hwb = _hwb.resolveMissing(); + const h = hwb.h; + const w = hwb.w; + const b = hwb.b; + + if (w + b >= 1.0) { + const gray = w / (w + b); + return SRGB{ + .r = gray, + .g = gray, + .b = gray, + .alpha = hwb.alpha, + }; + } + + var rgba = (HSL{ .h = h, .s = 1.0, .l = 0.5, .alpha = hwb.alpha }).intoSRGB(); + const x = 1.0 - w - b; + rgba.r = rgba.r * x + w; + rgba.g = rgba.g * x + w; + rgba.b = rgba.b * x + w; + return rgba; + } + }; + + pub const convert_SRGBLinear = struct { + pub usingnamespace generated.convert_SRGBLinear; + + pub fn intoPredefinedColor(rgb: *const SRGBLinear) PredefinedColor { + return PredefinedColor{ .srgb_linear = rgb.* }; + } + + pub fn intoCssColor(rgb: *const SRGBLinear, allocator: Allocator) CssColor { + return CssColor{ + .predefined = bun.create( + allocator, + PredefinedColor, + rgb.intoPredefinedColor(), + ), + }; + } + + pub fn intoSRGB(_rgb: *const SRGBLinear) SRGB { + const rgb = _rgb.resolveMissing(); + const r, const g, const b = gamSrgb(rgb.r, rgb.g, rgb.b); + return SRGB{ + .r = r, + .g = g, + .b = b, + .alpha = rgb.alpha, + }; + } + + pub fn intoXYZd65(_rgb: *const SRGBLinear) XYZd65 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L50 + // convert an array of linear-light sRGB values to CIE XYZ + // using sRGB's own white, D65 (no chromatic adaptation) + const MATRIX: [9]f32 = .{ + 0.41239079926595934, + 0.357584339383878, + 0.1804807884018343, + 0.21263900587151027, + 0.715168678767756, + 0.07219231536073371, + 0.01933081871559182, + 0.11919477979462598, + 0.9505321522496607, + }; + + const rgb = _rgb.resolveMissing(); + const x, const y, const z = multiplyMatrix(&MATRIX, rgb.r, rgb.g, rgb.b); + return XYZd65{ + .x = x, + .y = y, + .z = z, + .alpha = rgb.alpha, + }; + } + }; + + pub const convert_P3 = struct { + pub usingnamespace generated.convert_P3; + + pub fn intoPredefinedColor(rgb: *const P3) PredefinedColor { + return PredefinedColor{ .display_p3 = rgb.* }; + } + + pub fn intoCssColor(rgb: *const P3, allocator: Allocator) CssColor { + return CssColor{ + .predefined = bun.create( + allocator, + PredefinedColor, + rgb.intoPredefinedColor(), + ), + }; + } + + pub fn intoXYZd65(_p3: *const P3) XYZd65 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L91 + // convert linear-light display-p3 values to CIE XYZ + // using D65 (no chromatic adaptation) + // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + const MATRIX: [9]f32 = .{ + 0.4865709486482162, + 0.26566769316909306, + 0.1982172852343625, + 0.2289745640697488, + 0.6917385218365064, + 0.079286914093745, + 0.0000000000000000, + 0.04511338185890264, + 1.043944368900976, + }; + + const p3 = _p3.resolveMissing(); + const r, const g, const b = linSrgb(p3.r, p3.g, p3.b); + const x, const y, const z = multiplyMatrix(&MATRIX, r, g, b); + return XYZd65{ + .x = x, + .y = y, + .z = z, + .alpha = p3.alpha, + }; + } + }; + + pub const convert_A98 = struct { + pub usingnamespace generated.convert_A98; + + pub fn intoPredefinedColor(rgb: *const A98) PredefinedColor { + return PredefinedColor{ .a98 = rgb.* }; + } + + pub fn intoCssColor(rgb: *const A98, allocator: Allocator) CssColor { + return CssColor{ + .predefined = bun.create( + allocator, + PredefinedColor, + rgb.intoPredefinedColor(), + ), + }; + } + + pub fn intoXYZd65(_a98: *const A98) XYZd65 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L181 + const H = struct { + pub fn linA98rgbComponent(c: f32) f32 { + const sign: f32 = if (c < 0.0) @as(f32, -1.0) else @as(f32, 1.0); + return sign * std.math.pow(f32, @abs(c), 563.0 / 256.0); + } + }; + + // convert an array of a98-rgb values in the range 0.0 - 1.0 + // to linear light (un-companded) form. + // negative values are also now accepted + const a98 = _a98.resolveMissing(); + const r = H.linA98rgbComponent(a98.r); + const g = H.linA98rgbComponent(a98.g); + const b = H.linA98rgbComponent(a98.b); + + // convert an array of linear-light a98-rgb values to CIE XYZ + // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + // has greater numerical precision than section 4.3.5.3 of + // https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf + // but the values below were calculated from first principles + // from the chromaticity coordinates of R G B W + // see matrixmaker.html + const MATRIX: [9]f32 = .{ + 0.5766690429101305, + 0.1855582379065463, + 0.1882286462349947, + 0.29734497525053605, + 0.6273635662554661, + 0.07529145849399788, + 0.02703136138641234, + 0.07068885253582723, + 0.9913375368376388, + }; + + const x, const y, const z = multiplyMatrix(&MATRIX, r, g, b); + return XYZd65{ + .x = x, + .y = y, + .z = z, + .alpha = a98.alpha, + }; + } + }; + + pub const convert_ProPhoto = struct { + pub usingnamespace generated.convert_ProPhoto; + + pub fn intoPredefinedColor(rgb: *const ProPhoto) PredefinedColor { + return PredefinedColor{ .prophoto = rgb.* }; + } + + pub fn intoCssColor(rgb: *const ProPhoto, allocator: Allocator) CssColor { + return CssColor{ + .predefined = bun.create( + allocator, + PredefinedColor, + rgb.intoPredefinedColor(), + ), + }; + } + + pub fn intoXYZd50(_prophoto: *const ProPhoto) XYZd50 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L118 + // convert an array of prophoto-rgb values + // where in-gamut colors are in the range [0.0 - 1.0] + // to linear light (un-companded) form. + // Transfer curve is gamma 1.8 with a small linear portion + // Extended transfer function + + const H = struct { + pub fn linProPhotoComponent(c: f32) f32 { + const ET2: f32 = 16.0 / 512.0; + const abs = @abs(c); + if (abs <= ET2) { + return c / 16.0; + } + const sign: f32 = if (c < 0.0) -1.0 else 1.0; + return sign * std.math.pow(f32, abs, 1.8); + } + }; + + const prophoto = _prophoto.resolveMissing(); + const r = H.linProPhotoComponent(prophoto.r); + const g = H.linProPhotoComponent(prophoto.g); + const b = H.linProPhotoComponent(prophoto.b); + + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L155 + // convert an array of linear-light prophoto-rgb values to CIE XYZ + // using D50 (so no chromatic adaptation needed afterwards) + // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + const MATRIX: [9]f32 = .{ + 0.7977604896723027, + 0.13518583717574031, + 0.0313493495815248, + 0.2880711282292934, + 0.7118432178101014, + 0.00008565396060525902, + 0.0, + 0.0, + 0.8251046025104601, + }; + + const x, const y, const z = multiplyMatrix(&MATRIX, r, g, b); + return XYZd50{ + .x = x, + .y = y, + .z = z, + .alpha = prophoto.alpha, + }; + } + }; + + pub const convert_Rec2020 = struct { + pub usingnamespace generated.convert_Rec2020; + + pub fn intoPredefinedColor(rgb: *const Rec2020) PredefinedColor { + return PredefinedColor{ .rec2020 = rgb.* }; + } + + pub fn intoCssColor(rgb: *const Rec2020, allocator: Allocator) CssColor { + return CssColor{ + .predefined = bun.create( + allocator, + PredefinedColor, + rgb.intoPredefinedColor(), + ), + }; + } + + pub fn intoXYZd65(_rec2020: *const Rec2020) XYZd65 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L235 + // convert an array of rec2020 RGB values in the range 0.0 - 1.0 + // to linear light (un-companded) form. + // ITU-R BT.2020-2 p.4 + + const H = struct { + pub fn linRec2020Component(c: f32) f32 { + const A: f32 = 1.09929682680944; + const B: f32 = 0.018053968510807; + + const abs = @abs(c); + if (abs < B * 4.5) { + return c / 4.5; + } + + const sign: f32 = if (c < 0.0) -1.0 else 1.0; + return sign * std.math.pow( + f32, + (abs + A - 1.0) / A, + 1.0 / 0.45, + ); + } + }; + + const rec2020 = _rec2020.resolveMissing(); + const r = H.linRec2020Component(rec2020.r); + const g = H.linRec2020Component(rec2020.g); + const b = H.linRec2020Component(rec2020.b); + + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L276 + // convert an array of linear-light rec2020 values to CIE XYZ + // using D65 (no chromatic adaptation) + // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + const MATRIX: [9]f32 = .{ + 0.6369580483012914, + 0.14461690358620832, + 0.1688809751641721, + 0.2627002120112671, + 0.6779980715188708, + 0.05930171646986196, + 0.000000000000000, + 0.028072693049087428, + 1.060985057710791, + }; + + const x, const y, const z = multiplyMatrix(&MATRIX, r, g, b); + + return XYZd65{ + .x = x, + .y = y, + .z = z, + .alpha = rec2020.alpha, + }; + } + }; + + pub const convert_XYZd50 = struct { + pub usingnamespace generated.convert_XYZd50; + + pub fn intoPredefinedColor(rgb: *const XYZd50) PredefinedColor { + return PredefinedColor{ .xyz_d50 = rgb.* }; + } + + pub fn intoCssColor(rgb: *const XYZd50, allocator: Allocator) CssColor { + return CssColor{ + .predefined = bun.create( + allocator, + PredefinedColor, + rgb.intoPredefinedColor(), + ), + }; + } + + pub fn intoLAB(_xyz: *const XYZd50) LAB { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L332 + // Assuming XYZ is relative to D50, convert to CIE LAB + // from CIE standard, which now defines these as a rational fraction + const E: f32 = 216.0 / 24389.0; // 6^3/29^3 + const K: f32 = 24389.0 / 27.0; // 29^3/3^3 + + // compute xyz, which is XYZ scaled relative to reference white + const xyz = _xyz.resolveMissing(); + const x = xyz.x / D50[0]; + const y = xyz.y / D50[1]; + const z = xyz.y / D50[2]; + + // now compute f + + const f0 = if (x > E) std.math.cbrt(x) else (K * x + 16.0) / 116.0; + + const f1 = if (y > E) std.math.cbrt(y) else (K * y + 16.0) / 116.0; + + const f2 = if (z > E) std.math.cbrt(z) else (K * z + 16.0) / 116.0; + + const l = ((116.0 * f1) - 16.0) / 100.0; + const a = 500.0 * (f0 - f1); + const b = 500.0 * (f1 - f2); + + return LAB{ + .l = l, + .a = a, + .b = b, + .alpha = xyz.alpha, + }; + } + + pub fn intoXYZd65(_xyz: *const XYZd50) XYZd65 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L105 + const MATRIX: [9]f32 = .{ + 2.493496911941425, + -0.9313836179191239, + -0.40271078445071684, + -0.8294889695615747, + 1.7626640603183463, + 0.023624685841943577, + 0.03584583024378447, + -0.07617238926804182, + 0.9568845240076872, + }; + + const xyz = _xyz.resolveMissing(); + const x, const y, const z = multiplyMatrix(&MATRIX, xyz.x, xyz.y, xyz.z); + return XYZd65{ + .x = x, + .y = y, + .z = z, + .alpha = xyz.alpha, + }; + } + + pub fn intoProPhoto(_xyz: *const XYZd50) ProPhoto { + // convert XYZ to linear-light prophoto-rgb + const MATRIX: [9]f32 = .{ + 1.3457989731028281, + -0.25558010007997534, + -0.05110628506753401, + -0.5446224939028347, + 1.5082327413132781, + 0.02053603239147973, + 0.0, + 0.0, + 1.2119675456389454, + }; + const H = struct { + pub fn gamProPhotoComponent(c: f32) f32 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L137 + // convert linear-light prophoto-rgb in the range 0.0-1.0 + // to gamma corrected form + // Transfer curve is gamma 1.8 with a small linear portion + // TODO for negative values, extend linear portion on reflection of axis, then add pow below that + const ET: f32 = 1.0 / 512.0; + const abs = @abs(c); + if (abs >= ET) { + const sign: f32 = if (c < 0.0) -1.0 else 1.0; + return sign * std.math.pow(f32, abs, 1.0 / 1.8); + } + return 16.0 * c; + } + }; + const xyz = _xyz.resolveMissing(); + const r1, const g1, const b1 = multiplyMatrix(&MATRIX, xyz.x, xyz.y, xyz.z); + const r = H.gamProPhotoComponent(r1); + const g = H.gamProPhotoComponent(g1); + const b = H.gamProPhotoComponent(b1); + return ProPhoto{ + .r = r, + .g = g, + .b = b, + .alpha = xyz.alpha, + }; + } + }; + + pub const convert_XYZd65 = struct { + pub usingnamespace generated.convert_XYZd65; + + pub fn intoPredefinedColor(rgb: *const XYZd65) PredefinedColor { + return PredefinedColor{ .xyz_d65 = rgb.* }; + } + + pub fn intoCssColor(rgb: *const XYZd65, allocator: Allocator) CssColor { + return CssColor{ + .predefined = bun.create( + allocator, + PredefinedColor, + rgb.intoPredefinedColor(), + ), + }; + } + + pub fn intoXYZd50(_xyz: *const XYZd65) XYZd50 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L319 + + const MATRIX: [9]f32 = .{ + 1.0479298208405488, + 0.022946793341019088, + -0.05019222954313557, + 0.029627815688159344, + 0.990434484573249, + -0.01707382502938514, + -0.009243058152591178, + 0.015055144896577895, + 0.7518742899580008, + }; + + const xyz = _xyz.resolveMissing(); + const x, const y, const z = multiplyMatrix(&MATRIX, xyz.x, xyz.y, xyz.z); + return XYZd50{ + .x = x, + .y = y, + .z = z, + .alpha = xyz.alpha, + }; + } + + pub fn intoSRGBLinear(_xyz: *const XYZd65) SRGBLinear { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L62 + const MATRIX: [9]f32 = .{ + 3.2409699419045226, + -1.537383177570094, + -0.4986107602930034, + -0.9692436362808796, + 1.8759675015077202, + 0.04155505740717559, + 0.05563007969699366, + -0.20397695888897652, + 1.0569715142428786, + }; + + const xyz = _xyz.resolveMissing(); + const r, const g, const b = multiplyMatrix(&MATRIX, xyz.x, xyz.y, xyz.z); + return SRGBLinear{ + .r = r, + .g = g, + .b = b, + .alpha = xyz.alpha, + }; + } + + pub fn intoA98(_xyz: *const XYZd65) A98 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L222 + // convert XYZ to linear-light a98-rgb + + const MATRIX: [9]f32 = .{ + 2.0415879038107465, + -0.5650069742788596, + -0.34473135077832956, + -0.9692436362808795, + 1.8759675015077202, + 0.04155505740717557, + 0.013444280632031142, + -0.11836239223101838, + 1.0151749943912054, + }; + + const H = struct { + pub fn gamA98Component(c: f32) f32 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L193 + // convert linear-light a98-rgb in the range 0.0-1.0 + // to gamma corrected form + // negative values are also now accepted + const sign: f32 = if (c < 0.0) -1.0 else 1.0; + return sign * std.math.pow(f32, @abs(c), 256.0 / 563.0); + } + }; + + const xyz = _xyz.resolveMissing(); + const r1, const g1, const b1 = multiplyMatrix(&MATRIX, xyz.x, xyz.y, xyz.z); + const r = H.gamA98Component(r1); + const g = H.gamA98Component(g1); + const b = H.gamA98Component(b1); + return A98{ + .r = r, + .g = g, + .b = b, + .alpha = xyz.alpha, + }; + } + + pub fn intoRec2020(_xyz: *const XYZd65) Rec2020 { + // convert XYZ to linear-light rec2020 + const MATRIX: [9]f32 = .{ + 1.7166511879712674, + -0.35567078377639233, + -0.25336628137365974, + -0.6666843518324892, + 1.6164812366349395, + 0.01576854581391113, + 0.017639857445310783, + -0.042770613257808524, + 0.9421031212354738, + }; + + const H = struct { + pub fn gamRec2020Component(c: f32) f32 { + // convert linear-light rec2020 RGB in the range 0.0-1.0 + // to gamma corrected form + // ITU-R BT.2020-2 p.4 + + const A: f32 = 1.09929682680944; + const B: f32 = 0.018053968510807; + + const abs = @abs(c); + if (abs > B) { + const sign: f32 = if (c < 0.0) -1.0 else 1.0; + return sign * (A * std.math.pow(f32, abs, 0.45) - (A - 1.0)); + } + + return 4.5 * c; + } + }; + + const xyz = _xyz.resolveMissing(); + const r1, const g1, const b1 = multiplyMatrix(&MATRIX, xyz.x, xyz.y, xyz.z); + const r = H.gamRec2020Component(r1); + const g = H.gamRec2020Component(g1); + const b = H.gamRec2020Component(b1); + return Rec2020{ + .r = r, + .g = g, + .b = b, + .alpha = xyz.alpha, + }; + } + + pub fn intoOKLAB(_xyz: *const XYZd65) OKLAB { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L400 + const XYZ_TO_LMS: [9]f32 = .{ + 0.8190224432164319, + 0.3619062562801221, + -0.12887378261216414, + 0.0329836671980271, + 0.9292868468965546, + 0.03614466816999844, + 0.048177199566046255, + 0.26423952494422764, + 0.6335478258136937, + }; + + const LMS_TO_OKLAB: [9]f32 = .{ + 0.2104542553, + 0.7936177850, + -0.0040720468, + 1.9779984951, + -2.4285922050, + 0.4505937099, + 0.0259040371, + 0.7827717662, + -0.8086757660, + }; + + const xyz = _xyz.resolveMissing(); + const a1, const b1, const c1 = multiplyMatrix(&XYZ_TO_LMS, xyz.x, xyz.y, xyz.z); + const l, const a, const b = multiplyMatrix(&LMS_TO_OKLAB, a1, b1, c1); + + return OKLAB{ + .l = l, + .a = a, + .b = b, + .alpha = xyz.alpha, + }; + } + + pub fn intoP3(_xyz: *const XYZd65) P3 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L105 + const MATRIX: [9]f32 = .{ + 2.493496911941425, + -0.9313836179191239, + -0.40271078445071684, + -0.8294889695615747, + 1.7626640603183463, + 0.023624685841943577, + 0.03584583024378447, + -0.07617238926804182, + 0.9568845240076872, + }; + + const xyz = _xyz.resolveMissing(); + const r1, const g1, const b1 = multiplyMatrix(&MATRIX, xyz.x, xyz.y, xyz.z); + const r, const g, const b = gamSrgb(r1, g1, b1); // same as sRGB + return P3{ + .r = r, + .g = g, + .b = b, + .alpha = xyz.alpha, + }; + } + }; + + pub const convert_LCH = struct { + pub usingnamespace generated.convert_LCH; + + pub fn intoCssColor(c: *const LCH, allocator: Allocator) CssColor { + return CssColor{ .lab = bun.create( + allocator, + LABColor, + LABColor{ .lch = c.* }, + ) }; + } + + pub fn intoLAB(_lch: *const LCH) LAB { + const lch = _lch.resolveMissing(); + const l, const a, const b = polarToRectangular(lch.l, lch.c, lch.h); + return LAB{ + .l = l, + .a = a, + .b = b, + .alpha = lch.alpha, + }; + } + }; + + pub const convert_OKLAB = struct { + pub usingnamespace generated.convert_OKLAB; + + pub fn intoCssColor(c: *const OKLAB, allocator: Allocator) CssColor { + return CssColor{ .lab = bun.create( + allocator, + LABColor, + LABColor{ .oklab = c.* }, + ) }; + } + + pub fn intoOKLAB(labb: *const OKLAB) OKLAB { + return labb.*; + } + + pub fn intoOKLCH(labb: *const OKLAB) OKLCH { + const lab = labb.resolveMissing(); + const l, const c, const h = rectangularToPolar(lab.l, lab.a, lab.b); + return OKLCH{ + .l = l, + .c = c, + .h = h, + .alpha = lab.alpha, + }; + } + + pub fn intoXYZd65(_lab: *const OKLAB) XYZd65 { + // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L418 + const LMS_TO_XYZ: [9]f32 = .{ + 1.2268798733741557, + -0.5578149965554813, + 0.28139105017721583, + -0.04057576262431372, + 1.1122868293970594, + -0.07171106666151701, + -0.07637294974672142, + -0.4214933239627914, + 1.5869240244272418, + }; + + const OKLAB_TO_LMS: [9]f32 = .{ + 0.99999999845051981432, + 0.39633779217376785678, + 0.21580375806075880339, + 1.0000000088817607767, + -0.1055613423236563494, + -0.063854174771705903402, + 1.0000000546724109177, + -0.089484182094965759684, + -1.2914855378640917399, + }; + + const lab = _lab.resolveMissing(); + const a, const b, const c = multiplyMatrix(&OKLAB_TO_LMS, lab.l, lab.a, lab.b); + const x, const y, const z = multiplyMatrix( + &LMS_TO_XYZ, + std.math.pow(f32, a, 3), + std.math.pow(f32, b, 3), + std.math.pow(f32, c, 3), + ); + + return XYZd65{ + .x = x, + .y = y, + .z = z, + .alpha = lab.alpha, + }; + } + }; + + pub const convert_OKLCH = struct { + pub usingnamespace generated.convert_OKLCH; + + pub fn intoCssColor(c: *const OKLCH, allocator: Allocator) CssColor { + return CssColor{ .lab = bun.create( + allocator, + LABColor, + LABColor{ .oklch = c.* }, + ) }; + } + + pub fn intoOKLAB(_lch: *const OKLCH) OKLAB { + const lch = _lch.resolveMissing(); + const l, const a, const b = rectangularToPolar(lch.l, lch.c, lch.h); + return OKLAB{ + .l = l, + .a = a, + .b = b, + .alpha = lch.alpha, + }; + } + + pub fn intoOKLCH(x: *const OKLCH) OKLCH { + return x.*; + } + }; +}; diff --git a/src/css/values/color_generated.zig b/src/css/values/color_generated.zig new file mode 100644 index 0000000000000..a767990f1f9b5 --- /dev/null +++ b/src/css/values/color_generated.zig @@ -0,0 +1,945 @@ +//!This file is generated by `color_via.ts`. Do not edit it directly! +const color = @import("./color.zig"); +const RGBA = color.RGBA; +const LAB = color.LAB; +const LCH = color.LCH; +const SRGB = color.SRGB; +const HSL = color.HSL; +const HWB = color.HWB; +const SRGBLinear = color.SRGBLinear; +const P3 = color.P3; +const A98 = color.A98; +const ProPhoto = color.ProPhoto; +const XYZd50 = color.XYZd50; +const XYZd65 = color.XYZd65; +const OKLAB = color.OKLAB; +const OKLCH = color.OKLCH; +const Rec2020 = color.Rec2020; + +pub const generated_color_conversions = struct { + pub const convert_RGBA = struct { + pub fn intoLAB(this: *const RGBA) LAB { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const RGBA) LCH { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoLCH(); + } + + pub fn intoOKLAB(this: *const RGBA) OKLAB { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const RGBA) OKLCH { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoOKLCH(); + } + + pub fn intoP3(this: *const RGBA) P3 { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoP3(); + } + + pub fn intoSRGBLinear(this: *const RGBA) SRGBLinear { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoSRGBLinear(); + } + + pub fn intoA98(this: *const RGBA) A98 { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const RGBA) ProPhoto { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoProPhoto(); + } + + pub fn intoXYZd50(this: *const RGBA) XYZd50 { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoXYZd50(); + } + + pub fn intoXYZd65(this: *const RGBA) XYZd65 { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoXYZd65(); + } + + pub fn intoRec2020(this: *const RGBA) Rec2020 { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRec2020(); + } + + pub fn intoHSL(this: *const RGBA) HSL { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const RGBA) HWB { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoHWB(); + } + }; + pub const convert_LAB = struct { + pub fn intoXYZd65(this: *const LAB) XYZd65 { + const xyz: XYZd50 = this.intoXYZd50(); + return xyz.intoXYZd65(); + } + + pub fn intoOKLAB(this: *const LAB) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const LAB) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoSRGB(this: *const LAB) SRGB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGB(); + } + + pub fn intoSRGBLinear(this: *const LAB) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoP3(this: *const LAB) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoA98(this: *const LAB) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const LAB) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoRec2020(this: *const LAB) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoHSL(this: *const LAB) HSL { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const LAB) HWB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const LAB) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_SRGB = struct { + pub fn intoLAB(this: *const SRGB) LAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const SRGB) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoXYZd65(this: *const SRGB) XYZd65 { + const xyz: SRGBLinear = this.intoSRGBLinear(); + return xyz.intoXYZd65(); + } + + pub fn intoOKLAB(this: *const SRGB) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const SRGB) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoP3(this: *const SRGB) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoA98(this: *const SRGB) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const SRGB) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoRec2020(this: *const SRGB) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoXYZd50(this: *const SRGB) XYZd50 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoXYZd50(); + } + }; + pub const convert_HSL = struct { + pub fn intoLAB(this: *const HSL) LAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const HSL) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoP3(this: *const HSL) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoSRGBLinear(this: *const HSL) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoA98(this: *const HSL) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const HSL) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoXYZd50(this: *const HSL) XYZd50 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoXYZd50(); + } + + pub fn intoRec2020(this: *const HSL) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoOKLAB(this: *const HSL) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const HSL) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoXYZd65(this: *const HSL) XYZd65 { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoXYZd65(); + } + + pub fn intoHWB(this: *const HSL) HWB { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const HSL) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_HWB = struct { + pub fn intoLAB(this: *const HWB) LAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const HWB) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoP3(this: *const HWB) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoSRGBLinear(this: *const HWB) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoA98(this: *const HWB) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const HWB) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoXYZd50(this: *const HWB) XYZd50 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoXYZd50(); + } + + pub fn intoRec2020(this: *const HWB) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoHSL(this: *const HWB) HSL { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoHSL(); + } + + pub fn intoXYZd65(this: *const HWB) XYZd65 { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoXYZd65(); + } + + pub fn intoOKLAB(this: *const HWB) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const HWB) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoRGBA(this: *const HWB) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_SRGBLinear = struct { + pub fn intoLAB(this: *const SRGBLinear) LAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const SRGBLinear) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoP3(this: *const SRGBLinear) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoOKLAB(this: *const SRGBLinear) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const SRGBLinear) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoA98(this: *const SRGBLinear) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const SRGBLinear) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoRec2020(this: *const SRGBLinear) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoXYZd50(this: *const SRGBLinear) XYZd50 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoXYZd50(); + } + + pub fn intoHSL(this: *const SRGBLinear) HSL { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const SRGBLinear) HWB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const SRGBLinear) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_P3 = struct { + pub fn intoLAB(this: *const P3) LAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const P3) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoSRGB(this: *const P3) SRGB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGB(); + } + + pub fn intoSRGBLinear(this: *const P3) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoOKLAB(this: *const P3) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const P3) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoA98(this: *const P3) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const P3) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoRec2020(this: *const P3) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoXYZd50(this: *const P3) XYZd50 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoXYZd50(); + } + + pub fn intoHSL(this: *const P3) HSL { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const P3) HWB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const P3) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_A98 = struct { + pub fn intoLAB(this: *const A98) LAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const A98) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoSRGB(this: *const A98) SRGB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGB(); + } + + pub fn intoP3(this: *const A98) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoSRGBLinear(this: *const A98) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoOKLAB(this: *const A98) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const A98) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoProPhoto(this: *const A98) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoRec2020(this: *const A98) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoXYZd50(this: *const A98) XYZd50 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoXYZd50(); + } + + pub fn intoHSL(this: *const A98) HSL { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const A98) HWB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const A98) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_ProPhoto = struct { + pub fn intoXYZd65(this: *const ProPhoto) XYZd65 { + const xyz: XYZd50 = this.intoXYZd50(); + return xyz.intoXYZd65(); + } + + pub fn intoLAB(this: *const ProPhoto) LAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const ProPhoto) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoSRGB(this: *const ProPhoto) SRGB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGB(); + } + + pub fn intoP3(this: *const ProPhoto) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoSRGBLinear(this: *const ProPhoto) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoA98(this: *const ProPhoto) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoOKLAB(this: *const ProPhoto) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const ProPhoto) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoRec2020(this: *const ProPhoto) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoHSL(this: *const ProPhoto) HSL { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const ProPhoto) HWB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const ProPhoto) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_Rec2020 = struct { + pub fn intoLAB(this: *const Rec2020) LAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const Rec2020) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoSRGB(this: *const Rec2020) SRGB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGB(); + } + + pub fn intoP3(this: *const Rec2020) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoSRGBLinear(this: *const Rec2020) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoA98(this: *const Rec2020) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const Rec2020) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoXYZd50(this: *const Rec2020) XYZd50 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoXYZd50(); + } + + pub fn intoOKLAB(this: *const Rec2020) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const Rec2020) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoHSL(this: *const Rec2020) HSL { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const Rec2020) HWB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const Rec2020) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_XYZd50 = struct { + pub fn intoLCH(this: *const XYZd50) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoSRGB(this: *const XYZd50) SRGB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGB(); + } + + pub fn intoP3(this: *const XYZd50) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoSRGBLinear(this: *const XYZd50) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoA98(this: *const XYZd50) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoOKLAB(this: *const XYZd50) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const XYZd50) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoRec2020(this: *const XYZd50) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoHSL(this: *const XYZd50) HSL { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const XYZd50) HWB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const XYZd50) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_XYZd65 = struct { + pub fn intoLAB(this: *const XYZd65) LAB { + const xyz: XYZd50 = this.intoXYZd50(); + return xyz.intoLAB(); + } + + pub fn intoProPhoto(this: *const XYZd65) ProPhoto { + const xyz: XYZd50 = this.intoXYZd50(); + return xyz.intoProPhoto(); + } + + pub fn intoOKLCH(this: *const XYZd65) OKLCH { + const xyz: OKLAB = this.intoOKLAB(); + return xyz.intoOKLCH(); + } + + pub fn intoLCH(this: *const XYZd65) LCH { + const xyz: LAB = this.intoLAB(); + return xyz.intoLCH(); + } + + pub fn intoSRGB(this: *const XYZd65) SRGB { + const xyz: SRGBLinear = this.intoSRGBLinear(); + return xyz.intoSRGB(); + } + + pub fn intoHSL(this: *const XYZd65) HSL { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const XYZd65) HWB { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const XYZd65) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_LCH = struct { + pub fn intoXYZd65(this: *const LCH) XYZd65 { + const xyz: LAB = this.intoLAB(); + return xyz.intoXYZd65(); + } + + pub fn intoOKLAB(this: *const LCH) OKLAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLAB(); + } + + pub fn intoOKLCH(this: *const LCH) OKLCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoOKLCH(); + } + + pub fn intoSRGB(this: *const LCH) SRGB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGB(); + } + + pub fn intoSRGBLinear(this: *const LCH) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoP3(this: *const LCH) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoA98(this: *const LCH) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const LCH) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoRec2020(this: *const LCH) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoXYZd50(this: *const LCH) XYZd50 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoXYZd50(); + } + + pub fn intoHSL(this: *const LCH) HSL { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const LCH) HWB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const LCH) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_OKLAB = struct { + pub fn intoLAB(this: *const OKLAB) LAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const OKLAB) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoSRGB(this: *const OKLAB) SRGB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGB(); + } + + pub fn intoP3(this: *const OKLAB) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoSRGBLinear(this: *const OKLAB) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoA98(this: *const OKLAB) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const OKLAB) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoXYZd50(this: *const OKLAB) XYZd50 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoXYZd50(); + } + + pub fn intoRec2020(this: *const OKLAB) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoHSL(this: *const OKLAB) HSL { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const OKLAB) HWB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const OKLAB) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; + pub const convert_OKLCH = struct { + pub fn intoXYZd65(this: *const OKLCH) XYZd65 { + const xyz: OKLAB = this.intoOKLAB(); + return xyz.intoXYZd65(); + } + + pub fn intoLAB(this: *const OKLCH) LAB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLAB(); + } + + pub fn intoLCH(this: *const OKLCH) LCH { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoLCH(); + } + + pub fn intoSRGB(this: *const OKLCH) SRGB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGB(); + } + + pub fn intoP3(this: *const OKLCH) P3 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoP3(); + } + + pub fn intoSRGBLinear(this: *const OKLCH) SRGBLinear { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoSRGBLinear(); + } + + pub fn intoA98(this: *const OKLCH) A98 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoA98(); + } + + pub fn intoProPhoto(this: *const OKLCH) ProPhoto { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoProPhoto(); + } + + pub fn intoXYZd50(this: *const OKLCH) XYZd50 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoXYZd50(); + } + + pub fn intoRec2020(this: *const OKLCH) Rec2020 { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoRec2020(); + } + + pub fn intoHSL(this: *const OKLCH) HSL { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHSL(); + } + + pub fn intoHWB(this: *const OKLCH) HWB { + const xyz: XYZd65 = this.intoXYZd65(); + return xyz.intoHWB(); + } + + pub fn intoRGBA(this: *const OKLCH) RGBA { + const xyz: SRGB = this.intoSRGB(); + return xyz.intoRGBA(); + } + }; +}; diff --git a/src/css/values/color_js.zig b/src/css/values/color_js.zig new file mode 100644 index 0000000000000..389f58d01a73a --- /dev/null +++ b/src/css/values/color_js.zig @@ -0,0 +1,478 @@ +const bun = @import("root").bun; +const std = @import("std"); +const color = @import("./color.zig"); +const RGBA = color.RGBA; +const LAB = color.LAB; +const LCH = color.LCH; +const SRGB = color.SRGB; +const HSL = color.HSL; +const HWB = color.HWB; +const SRGBLinear = color.SRGBLinear; +const P3 = color.P3; +const JSC = bun.JSC; +const css = bun.css; + +const OutputColorFormat = enum { + ansi, + ansi_16, + ansi_256, + ansi_16m, + css, + hex, + HEX, + hsl, + lab, + number, + rgb, + rgba, + @"[rgb]", + @"[rgba]", + @"{rgb}", + @"{rgba}", + + pub const Map = bun.ComptimeStringMap(OutputColorFormat, .{ + .{ "[r,g,b,a]", .@"[rgba]" }, + .{ "[rgb]", .@"[rgb]" }, + .{ "[rgba]", .@"[rgba]" }, + .{ "{r,g,b}", .@"{rgb}" }, + .{ "{rgb}", .@"{rgb}" }, + .{ "{rgba}", .@"{rgba}" }, + .{ "ansi_256", .ansi_256 }, + .{ "ansi-256", .ansi_256 }, + .{ "ansi-16", .ansi_16 }, + .{ "ansi-16m", .ansi_16m }, + .{ "ansi-24bit", .ansi_16m }, + .{ "ansi-truecolor", .ansi_16m }, + .{ "ansi", .ansi }, + .{ "ansi256", .ansi_256 }, + .{ "css", .css }, + .{ "hex", .hex }, + .{ "HEX", .HEX }, + .{ "hsl", .hsl }, + .{ "lab", .lab }, + .{ "number", .number }, + .{ "rgb", .rgb }, + .{ "rgba", .rgba }, + }); +}; + +fn colorIntFromJS(globalThis: *JSC.JSGlobalObject, input: JSC.JSValue, comptime property: []const u8) ?i32 { + if (input == .zero or input == .undefined or !input.isNumber()) { + globalThis.throwInvalidArgumentType("color", property, "integer"); + + return null; + } + + // CSS spec says to clamp values to their valid range so we'll respect that here + return std.math.clamp(input.coerce(i32, globalThis), 0, 255); +} + +// https://github.com/tmux/tmux/blob/dae2868d1227b95fd076fb4a5efa6256c7245943/colour.c#L44-L55 +pub const Ansi256 = struct { + const q2c = [_]u32{ 0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff }; + + fn sqdist(R: u32, G: u32, B: u32, r: u32, g: u32, b: u32) u32 { + return ((R -% r) *% (R -% r) +% (G -% g) *% (G -% g) +% (B -% b) *% (B -% b)); + } + + fn to6Cube(v: u32) u32 { + if (v < 48) + return (0); + if (v < 114) + return (1); + return ((v - 35) / 40); + } + + fn get(r: u32, g: u32, b: u32) u32 { + const qr = to6Cube(r); + const cr = q2c[@intCast(qr)]; + const qg = to6Cube(g); + const cg = q2c[@intCast(qg)]; + const qb = to6Cube(b); + const cb = q2c[@intCast(qb)]; + + if (cr == r and cg == g and cb == b) { + return 16 +% (36 *% qr) +% (6 *% qg) +% qb; + } + + const grey_avg = (r +% g +% b) / 3; + const grey_idx = if (grey_avg > 238) 23 else (grey_avg -% 3) / 10; + const grey = 8 +% (10 *% grey_idx); + + const d = sqdist(cr, cg, cb, r, g, b); + const idx = if (sqdist(grey, grey, grey, r, g, b) < d) 232 +% grey_idx else 16 +% (36 *% qr) +% (6 *% qg) +% qb; + return idx; + } + + const table_256: [256]u8 = .{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 0, 4, 4, 4, 12, 12, 2, 6, 4, 4, 12, 12, 2, 2, 6, 4, + 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, + 10, 10, 10, 14, 1, 5, 4, 4, 12, 12, 3, 8, 4, 4, 12, 12, + 2, 2, 6, 4, 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, + 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 5, 4, 12, 12, 1, 1, + 5, 4, 12, 12, 3, 3, 8, 4, 12, 12, 2, 2, 2, 6, 12, 12, + 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 1, 5, + 12, 12, 1, 1, 1, 5, 12, 12, 1, 1, 1, 5, 12, 12, 3, 3, + 3, 7, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, + 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, + 13, 12, 9, 9, 9, 9, 13, 12, 11, 11, 11, 11, 7, 12, 10, 10, + 10, 10, 10, 14, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, + 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, + 9, 13, 11, 11, 11, 11, 11, 15, 0, 0, 0, 0, 0, 0, 8, 8, + 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 15, 15, 15, 15, 15, 15, + }; + + pub fn get16(r: u32, g: u32, b: u32) u8 { + const val = get(r, g, b); + return table_256[val & 0xff]; + } + + pub const Buffer = [24]u8; + + pub fn from(rgba: RGBA, buf: *Buffer) []u8 { + const val = get(rgba.red, rgba.green, rgba.blue); + // 0x1b is the escape character + buf[0] = 0x1b; + buf[1] = '['; + buf[2] = '3'; + buf[3] = '8'; + buf[4] = ';'; + buf[5] = '5'; + buf[6] = ';'; + const extra = std.fmt.bufPrint(buf[7..], "{d}m", .{val}) catch unreachable; + return buf[0 .. 7 + extra.len]; + } +}; + +pub fn jsFunctionColor(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + const args = callFrame.arguments(2).slice(); + if (args.len < 1 or args[0].isUndefined()) { + globalThis.throwNotEnoughArguments("Bun.color", 2, args.len); + return JSC.JSValue.jsUndefined(); + } + + var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + var stack_fallback = std.heap.stackFallback(4096, arena.allocator()); + const allocator = stack_fallback.get(); + + var log = bun.logger.Log.init(allocator); + defer log.deinit(); + + const unresolved_format: OutputColorFormat = brk: { + if (!args[1].isEmptyOrUndefinedOrNull()) { + if (!args[1].isString()) { + globalThis.throwInvalidArgumentType("color", "format", "string"); + return JSC.JSValue.jsUndefined(); + } + + break :brk args[1].toEnum(globalThis, "format", OutputColorFormat) catch return .zero; + } + + break :brk OutputColorFormat.css; + }; + var input = JSC.ZigString.Slice.empty; + defer input.deinit(); + + var parsed_color: css.CssColor.ParseResult = brk: { + if (args[0].isNumber()) { + const number: i64 = args[0].toInt64(); + const Packed = packed struct(u32) { + blue: u8, + green: u8, + red: u8, + alpha: u8, + }; + const int: u32 = @truncate(@abs(@mod(number, std.math.maxInt(u32)))); + const rgba: Packed = @bitCast(int); + + break :brk .{ .result = css.CssColor{ .rgba = .{ .alpha = rgba.alpha, .red = rgba.red, .green = rgba.green, .blue = rgba.blue } } }; + } else if (args[0].jsType().isArrayLike()) { + switch (args[0].getLength(globalThis)) { + 3 => { + const r = colorIntFromJS(globalThis, args[0].getIndex(globalThis, 0), "[0]") orelse return .zero; + if (globalThis.hasException()) { + return .zero; + } + const g = colorIntFromJS(globalThis, args[0].getIndex(globalThis, 1), "[1]") orelse return .zero; + if (globalThis.hasException()) { + return .zero; + } + const b = colorIntFromJS(globalThis, args[0].getIndex(globalThis, 2), "[2]") orelse return .zero; + if (globalThis.hasException()) { + return .zero; + } + break :brk .{ .result = css.CssColor{ .rgba = .{ .alpha = 255, .red = @intCast(r), .green = @intCast(g), .blue = @intCast(b) } } }; + }, + 4 => { + const r = colorIntFromJS(globalThis, args[0].getIndex(globalThis, 0), "[0]") orelse return .zero; + if (globalThis.hasException()) { + return .zero; + } + const g = colorIntFromJS(globalThis, args[0].getIndex(globalThis, 1), "[1]") orelse return .zero; + if (globalThis.hasException()) { + return .zero; + } + const b = colorIntFromJS(globalThis, args[0].getIndex(globalThis, 2), "[2]") orelse return .zero; + if (globalThis.hasException()) { + return .zero; + } + const a = colorIntFromJS(globalThis, args[0].getIndex(globalThis, 3), "[3]") orelse return .zero; + if (globalThis.hasException()) { + return .zero; + } + break :brk .{ .result = css.CssColor{ .rgba = .{ .alpha = @intCast(a), .red = @intCast(r), .green = @intCast(g), .blue = @intCast(b) } } }; + }, + else => { + globalThis.throw("Expected array length 3 or 4", .{}); + return JSC.JSValue.jsUndefined(); + }, + } + } else if (args[0].isObject()) { + const r = colorIntFromJS(globalThis, args[0].getOwn(globalThis, "r") orelse .zero, "r") orelse return .zero; + + if (globalThis.hasException()) { + return .zero; + } + const g = colorIntFromJS(globalThis, args[0].getOwn(globalThis, "g") orelse .zero, "g") orelse return .zero; + + if (globalThis.hasException()) { + return .zero; + } + const b = colorIntFromJS(globalThis, args[0].getOwn(globalThis, "b") orelse .zero, "b") orelse return .zero; + + if (globalThis.hasException()) { + return .zero; + } + + const a: ?u8 = if (args[0].getTruthy(globalThis, "a")) |a_value| brk2: { + if (a_value.isNumber()) { + break :brk2 @intCast(@mod(@as(i64, @intFromFloat(a_value.asNumber() * 255.0)), 256)); + } + break :brk2 null; + } else null; + if (globalThis.hasException()) { + return .zero; + } + + break :brk .{ + .result = css.CssColor{ + .rgba = .{ + .alpha = if (a != null) @intCast(a.?) else 255, + .red = @intCast(r), + .green = @intCast(g), + .blue = @intCast(b), + }, + }, + }; + } + + input = args[0].toSlice(globalThis, bun.default_allocator); + + var parser_input = css.ParserInput.new(allocator, input.slice()); + var parser = css.Parser.new(&parser_input); + break :brk css.CssColor.parse(&parser); + }; + + switch (parsed_color) { + .err => |err| { + if (log.msgs.items.len == 0) { + return .null; + } + + globalThis.throw("color() failed to parse {s}", .{@tagName(err.basic().kind)}); + return JSC.JSValue.jsUndefined(); + }, + .result => |*result| { + const format: OutputColorFormat = if (unresolved_format == .ansi) switch (bun.Output.Source.colorDepth()) { + // No color terminal, therefore return an empty string + .none => return JSC.JSValue.jsEmptyString(globalThis), + .@"16" => .ansi_16, + .@"16m" => .ansi_16m, + .@"256" => .ansi_256, + } else unresolved_format; + + formatted: { + var str = color: { + switch (format) { + // resolved above. + .ansi => unreachable, + + // Use the CSS printer. + .css => break :formatted, + + .number, + .rgb, + .rgba, + .hex, + .HEX, + .ansi_16, + .ansi_16m, + .ansi_256, + .@"{rgba}", + .@"{rgb}", + .@"[rgba]", + .@"[rgb]", + => |tag| { + const srgba = switch (result.*) { + .float => |float| switch (float.*) { + .rgb => |rgb| rgb, + inline else => |*val| val.intoSRGB(), + }, + .rgba => |*rgba| rgba.intoSRGB(), + .lab => |lab| switch (lab.*) { + inline else => |entry| entry.intoSRGB(), + }, + else => break :formatted, + }; + const rgba = srgba.intoRGBA(); + switch (tag) { + .@"{rgba}" => { + const object = JSC.JSValue.createEmptyObject(globalThis, 4); + object.put(globalThis, "r", JSC.JSValue.jsNumber(rgba.red)); + object.put(globalThis, "g", JSC.JSValue.jsNumber(rgba.green)); + object.put(globalThis, "b", JSC.JSValue.jsNumber(rgba.blue)); + object.put(globalThis, "a", JSC.JSValue.jsNumber(rgba.alphaF32())); + return object; + }, + .@"{rgb}" => { + const object = JSC.JSValue.createEmptyObject(globalThis, 4); + object.put(globalThis, "r", JSC.JSValue.jsNumber(rgba.red)); + object.put(globalThis, "g", JSC.JSValue.jsNumber(rgba.green)); + object.put(globalThis, "b", JSC.JSValue.jsNumber(rgba.blue)); + return object; + }, + .@"[rgb]" => { + const object = JSC.JSValue.createEmptyArray(globalThis, 3); + object.putIndex(globalThis, 0, JSC.JSValue.jsNumber(rgba.red)); + object.putIndex(globalThis, 1, JSC.JSValue.jsNumber(rgba.green)); + object.putIndex(globalThis, 2, JSC.JSValue.jsNumber(rgba.blue)); + return object; + }, + .@"[rgba]" => { + const object = JSC.JSValue.createEmptyArray(globalThis, 4); + object.putIndex(globalThis, 0, JSC.JSValue.jsNumber(rgba.red)); + object.putIndex(globalThis, 1, JSC.JSValue.jsNumber(rgba.green)); + object.putIndex(globalThis, 2, JSC.JSValue.jsNumber(rgba.blue)); + object.putIndex(globalThis, 3, JSC.JSValue.jsNumber(rgba.alpha)); + return object; + }, + .number => { + var int: u32 = 0; + int |= @as(u32, rgba.red) << 16; + int |= @as(u32, rgba.green) << 8; + int |= @as(u32, rgba.blue); + return JSC.JSValue.jsNumber(int); + }, + .hex => { + break :color bun.String.createFormat("#{}{}{}", .{ bun.fmt.hexIntLower(rgba.red), bun.fmt.hexIntLower(rgba.green), bun.fmt.hexIntLower(rgba.blue) }); + }, + .HEX => { + break :color bun.String.createFormat("#{}{}{}", .{ bun.fmt.hexIntUpper(rgba.red), bun.fmt.hexIntUpper(rgba.green), bun.fmt.hexIntUpper(rgba.blue) }); + }, + .rgb => { + break :color bun.String.createFormat("rgb({d}, {d}, {d})", .{ rgba.red, rgba.green, rgba.blue }); + }, + .rgba => { + break :color bun.String.createFormat("rgba({d}, {d}, {d}, {d})", .{ rgba.red, rgba.green, rgba.blue, rgba.alphaF32() }); + }, + .ansi_16 => { + const ansi_16_color = Ansi256.get16(rgba.red, rgba.green, rgba.blue); + // 16-color ansi, foreground text color + break :color bun.String.createLatin1(&[_]u8{ + // 0x1b is the escape character + // 38 is the foreground color code + // 5 is the 16-color mode + // {d} is the color index + 0x1b, '[', '3', '8', ';', '5', ';', ansi_16_color, 'm', + }); + }, + .ansi_16m => { + // true color ansi + var buf: [48]u8 = undefined; + // 0x1b is the escape character + buf[0] = 0x1b; + buf[1] = '['; + buf[2] = '3'; + buf[3] = '8'; + buf[4] = ';'; + buf[5] = '2'; + buf[6] = ';'; + const additional = std.fmt.bufPrint(buf[7..], "{d};{d};{d}m", .{ + rgba.red, + rgba.green, + rgba.blue, + }) catch unreachable; + + break :color bun.String.createLatin1(buf[0 .. 7 + additional.len]); + }, + .ansi_256 => { + // ANSI escape sequence + var buf: Ansi256.Buffer = undefined; + const val = Ansi256.from(rgba, &buf); + break :color bun.String.createLatin1(val); + }, + else => unreachable, + } + }, + + .hsl => { + const hsl = switch (result.*) { + .float => |float| brk: { + switch (float.*) { + .hsl => |hsl| break :brk hsl, + inline else => |*val| break :brk val.intoHSL(), + } + }, + .rgba => |*rgba| rgba.intoHSL(), + .lab => |lab| switch (lab.*) { + inline else => |entry| entry.intoHSL(), + }, + else => break :formatted, + }; + + break :color bun.String.createFormat("hsl({d}, {d}, {d})", .{ hsl.h, hsl.s, hsl.l }); + }, + .lab => { + const lab = switch (result.*) { + .float => |float| switch (float.*) { + inline else => |*val| val.intoLAB(), + }, + .lab => |lab| switch (lab.*) { + .lab => |lab_| lab_, + inline else => |entry| entry.intoLAB(), + }, + .rgba => |*rgba| rgba.intoLAB(), + else => break :formatted, + }; + + break :color bun.String.createFormat("lab({d}, {d}, {d})", .{ lab.l, lab.a, lab.b }); + }, + } + } catch bun.outOfMemory(); + + return str.transferToJS(globalThis); + } + + // Fallback to CSS string output + var dest = std.ArrayListUnmanaged(u8){}; + const writer = dest.writer(allocator); + + var printer = css.Printer(@TypeOf(writer)).new( + allocator, + std.ArrayList(u8).init(allocator), + writer, + .{}, + ); + + result.toCss(@TypeOf(writer), &printer) catch |err| { + globalThis.throw("color() internal error: {s}", .{@errorName(err)}); + return .zero; + }; + + var out = bun.String.createUTF8(dest.items); + return out.transferToJS(globalThis); + }, + } +} diff --git a/src/css/values/color_via.ts b/src/css/values/color_via.ts new file mode 100644 index 0000000000000..fd1127c6f0d29 --- /dev/null +++ b/src/css/values/color_via.ts @@ -0,0 +1,231 @@ +const RGBA = "RGBA"; +const LAB = "LAB"; +const SRGB = "SRGB"; +const HSL = "HSL"; +const HWB = "HWB"; +const SRGBLinear = "SRGBLinear"; +const P3 = "P3"; +const A98 = "A98"; +const ProPhoto = "ProPhoto"; +const Rec2020 = "Rec2020"; +const XYZd50 = "XYZd50"; +const XYZd65 = "XYZd65"; +const LCH = "LCH"; +const OKLAB = "OKLAB"; +const OKLCH = "OKLCH"; +const color_spaces = [ + RGBA, + LAB, + SRGB, + HSL, + HWB, + SRGBLinear, + P3, + A98, + ProPhoto, + Rec2020, + XYZd50, + XYZd65, + LCH, + OKLAB, + OKLCH, +]; + +type ColorSpaces = + | typeof RGBA + | typeof LAB + | typeof SRGB + | typeof HSL + | typeof HWB + | typeof SRGBLinear + | typeof P3 + | typeof A98 + | typeof ProPhoto + | typeof Rec2020 + | typeof XYZd50 + | typeof XYZd65 + | typeof LCH + | typeof OKLAB + | typeof OKLCH; + +type Foo = "a" | "b"; + +let code: Map = new Map(); + +initColorSpaces(); +addConversions(); +await generateCode(); + +function initColorSpaces() { + for (const space of color_spaces as ColorSpaces[]) { + code.set(space, []); + } +} + +async function generateCode() { + const output = `//!This file is generated by \`color_via.ts\`. Do not edit it directly! +const color = @import("./color.zig"); +const RGBA = color.RGBA; +const LAB = color.LAB; +const LCH = color.LCH; +const SRGB = color.SRGB; +const HSL = color.HSL; +const HWB = color.HWB; +const SRGBLinear = color.SRGBLinear; +const P3 = color.P3; +const A98 = color.A98; +const ProPhoto = color.ProPhoto; +const XYZd50 = color.XYZd50; +const XYZd65 = color.XYZd65; +const OKLAB = color.OKLAB; +const OKLCH = color.OKLCH; +const Rec2020 = color.Rec2020; + +pub const generated_color_conversions = struct { +${(() => { + let result = ""; + for (const [space, functions] of code) { + result += "\n"; + result += `pub const convert_${space} = struct {\n`; + result += functions.join("\n"); + result += "\n};"; + } + return result; +})()} +};`; + await Bun.$`echo ${output} > src/css/values/color_generated.zig; zig fmt src/css/values/color_generated.zig +`; +} + +function addConversions() { + // Once Rust specialization is stable, this could be simplified. + via("LAB", "XYZd50", "XYZd65"); + via("ProPhoto", "XYZd50", "XYZd65"); + via("OKLCH", "OKLAB", "XYZd65"); + + via("LAB", "XYZd65", "OKLAB"); + via("LAB", "XYZd65", "OKLCH"); + via("LAB", "XYZd65", "SRGB"); + via("LAB", "XYZd65", "SRGBLinear"); + via("LAB", "XYZd65", "P3"); + via("LAB", "XYZd65", "A98"); + via("LAB", "XYZd65", "ProPhoto"); + via("LAB", "XYZd65", "Rec2020"); + via("LAB", "XYZd65", "HSL"); + via("LAB", "XYZd65", "HWB"); + + via("LCH", "LAB", "XYZd65"); + via("LCH", "XYZd65", "OKLAB"); + via("LCH", "XYZd65", "OKLCH"); + via("LCH", "XYZd65", "SRGB"); + via("LCH", "XYZd65", "SRGBLinear"); + via("LCH", "XYZd65", "P3"); + via("LCH", "XYZd65", "A98"); + via("LCH", "XYZd65", "ProPhoto"); + via("LCH", "XYZd65", "Rec2020"); + via("LCH", "XYZd65", "XYZd50"); + via("LCH", "XYZd65", "HSL"); + via("LCH", "XYZd65", "HWB"); + + via("SRGB", "SRGBLinear", "XYZd65"); + via("SRGB", "XYZd65", "OKLAB"); + via("SRGB", "XYZd65", "OKLCH"); + via("SRGB", "XYZd65", "P3"); + via("SRGB", "XYZd65", "A98"); + via("SRGB", "XYZd65", "ProPhoto"); + via("SRGB", "XYZd65", "Rec2020"); + via("SRGB", "XYZd65", "XYZd50"); + + via("P3", "XYZd65", "SRGBLinear"); + via("P3", "XYZd65", "OKLAB"); + via("P3", "XYZd65", "OKLCH"); + via("P3", "XYZd65", "A98"); + via("P3", "XYZd65", "ProPhoto"); + via("P3", "XYZd65", "Rec2020"); + via("P3", "XYZd65", "XYZd50"); + via("P3", "XYZd65", "HSL"); + via("P3", "XYZd65", "HWB"); + + via("SRGBLinear", "XYZd65", "OKLAB"); + via("SRGBLinear", "XYZd65", "OKLCH"); + via("SRGBLinear", "XYZd65", "A98"); + via("SRGBLinear", "XYZd65", "ProPhoto"); + via("SRGBLinear", "XYZd65", "Rec2020"); + via("SRGBLinear", "XYZd65", "XYZd50"); + via("SRGBLinear", "XYZd65", "HSL"); + via("SRGBLinear", "XYZd65", "HWB"); + + via("A98", "XYZd65", "OKLAB"); + via("A98", "XYZd65", "OKLCH"); + via("A98", "XYZd65", "ProPhoto"); + via("A98", "XYZd65", "Rec2020"); + via("A98", "XYZd65", "XYZd50"); + via("A98", "XYZd65", "HSL"); + via("A98", "XYZd65", "HWB"); + + via("ProPhoto", "XYZd65", "OKLAB"); + via("ProPhoto", "XYZd65", "OKLCH"); + via("ProPhoto", "XYZd65", "Rec2020"); + via("ProPhoto", "XYZd65", "HSL"); + via("ProPhoto", "XYZd65", "HWB"); + + via("XYZd50", "XYZd65", "OKLAB"); + via("XYZd50", "XYZd65", "OKLCH"); + via("XYZd50", "XYZd65", "Rec2020"); + via("XYZd50", "XYZd65", "HSL"); + via("XYZd50", "XYZd65", "HWB"); + + via("Rec2020", "XYZd65", "OKLAB"); + via("Rec2020", "XYZd65", "OKLCH"); + via("Rec2020", "XYZd65", "HSL"); + via("Rec2020", "XYZd65", "HWB"); + + via("HSL", "XYZd65", "OKLAB"); + via("HSL", "XYZd65", "OKLCH"); + via("HSL", "SRGB", "XYZd65"); + via("HSL", "SRGB", "HWB"); + + via("HWB", "SRGB", "XYZd65"); + via("HWB", "XYZd65", "OKLAB"); + via("HWB", "XYZd65", "OKLCH"); + + // RGBA is an 8-bit version. Convert to SRGB, which is a + // more accurate floating point representation for all operations. + via("RGBA", "SRGB", "LAB"); + via("RGBA", "SRGB", "LCH"); + via("RGBA", "SRGB", "OKLAB"); + via("RGBA", "SRGB", "OKLCH"); + via("RGBA", "SRGB", "P3"); + via("RGBA", "SRGB", "SRGBLinear"); + via("RGBA", "SRGB", "A98"); + via("RGBA", "SRGB", "ProPhoto"); + via("RGBA", "SRGB", "XYZd50"); + via("RGBA", "SRGB", "XYZd65"); + via("RGBA", "SRGB", "Rec2020"); + via("RGBA", "SRGB", "HSL"); + via("RGBA", "SRGB", "HWB"); +} + +function via, V extends Exclude>( + from: T, + middle: U, + to: V, +) { + // Generate T, U, V function (where T, U, V are ColorSpaces) + let fromFunctions = code.get(from) || []; + fromFunctions.push(`pub fn into${to}(this: *const ${from}) ${to} { + const xyz: ${middle} = this.into${middle}(); + return xyz.into${to}(); +} +`); + code.set(from, fromFunctions); + + // Generate V, U, function + let toFunctions = code.get(to) || []; + toFunctions.push(`pub fn into${from}(this: *const ${to}) ${from} { + const xyz: ${middle} = this.into${middle}(); + return xyz.into${from}(); +} +`); + code.set(to, toFunctions); +} diff --git a/src/css/values/css_string.zig b/src/css/values/css_string.zig new file mode 100644 index 0000000000000..7cb042cd3dd17 --- /dev/null +++ b/src/css/values/css_string.zig @@ -0,0 +1,22 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const Result = css.Result; +pub const Printer = css.Printer; +pub const PrintErr = css.PrintErr; + +/// A quoted CSS string. +pub const CSSString = []const u8; +pub const CSSStringFns = struct { + pub fn parse(input: *css.Parser) Result(CSSString) { + return input.expectString(); + } + + pub fn toCss(this: *const []const u8, comptime W: type, dest: *Printer(W)) PrintErr!void { + return css.serializer.serializeString(this.*, dest) catch return dest.addFmtError(); + } +}; diff --git a/src/css/values/easing.zig b/src/css/values/easing.zig new file mode 100644 index 0000000000000..4d7c7cd00c9b1 --- /dev/null +++ b/src/css/values/easing.zig @@ -0,0 +1,257 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const DimensionPercentage = css.css_values.percentage.DimensionPercentage; +const LengthPercentage = css.css_values.length.LengthPercentage; +const Length = css.css_values.length.Length; +const Percentage = css.css_values.percentage.Percentage; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const Url = css.css_values.url.Url; +const CSSInteger = css.css_values.number.CSSInteger; +const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +const Angle = css.css_values.angle.Angle; +const Time = css.css_values.time.Time; +const Resolution = css.css_values.resolution.Resolution; +const CustomIdent = css.css_values.ident.CustomIdent; +const CustomIdentFns = css.css_values.ident.CustomIdentFns; +const Ident = css.css_values.ident.Ident; + +/// A CSS [easing function](https://www.w3.org/TR/css-easing-1/#easing-functions). +pub const EasingFunction = union(enum) { + /// A linear easing function. + linear, + /// Equivalent to `cubic-bezier(0.25, 0.1, 0.25, 1)`. + ease, + /// Equivalent to `cubic-bezier(0.42, 0, 1, 1)`. + ease_in, + /// Equivalent to `cubic-bezier(0, 0, 0.58, 1)`. + ease_out, + /// Equivalent to `cubic-bezier(0.42, 0, 0.58, 1)`. + ease_in_out, + /// A custom cubic Bézier easing function. + cubic_bezier: struct { + /// The x-position of the first point in the curve. + x1: CSSNumber, + /// The y-position of the first point in the curve. + y1: CSSNumber, + /// The x-position of the second point in the curve. + x2: CSSNumber, + /// The y-position of the second point in the curve. + y2: CSSNumber, + }, + /// A step easing function. + steps: struct { + /// The number of intervals in the function. + count: CSSInteger, + /// The step position. + position: StepPosition = StepPosition.default, + }, + + pub fn parse(input: *css.Parser) Result(EasingFunction) { + const location = input.currentSourceLocation(); + if (input.tryParse(struct { + fn parse(i: *css.Parser) Result([]const u8) { + return i.expectIdent(); + } + }.parse, .{}).asValue()) |ident| { + // todo_stuff.match_ignore_ascii_case + const keyword = if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "linear")) + EasingFunction.linear + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "ease")) + EasingFunction.ease + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "ease-in")) + EasingFunction.ease_in + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "ease-out")) + EasingFunction.ease_out + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "ease-in-out")) + EasingFunction.ease_in_out + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "step-start")) + EasingFunction{ .steps = .{ .count = 1, .position = .start } } + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "step-end")) + EasingFunction{ .steps = .{ .count = 1, .position = .end } } + else + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + return .{ .result = keyword }; + } + + const function = switch (input.expectFunction()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return input.parseNestedBlock( + EasingFunction, + .{ .loc = location, .function = function }, + struct { + fn parse( + closure: *const struct { loc: css.SourceLocation, function: []const u8 }, + i: *css.Parser, + ) Result(EasingFunction) { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "cubic-bezier")) { + const x1 = switch (CSSNumberFns.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const y1 = switch (CSSNumberFns.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const x2 = switch (CSSNumberFns.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const y2 = switch (CSSNumberFns.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = EasingFunction{ .cubic_bezier = .{ .x1 = x1, .y1 = y1, .x2 = x2, .y2 = y2 } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "steps")) { + const count = switch (CSSIntegerFns.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const position = i.tryParse(struct { + fn parse(p: *css.Parser) Result(StepPosition) { + if (p.expectComma().asErr()) |e| return .{ .err = e }; + return StepPosition.parse(p); + } + }.parse, .{}).unwrapOr(StepPosition.default); + return .{ .result = EasingFunction{ .steps = .{ .count = count, .position = position } } }; + } else { + return closure.loc.newUnexpectedTokenError(.{ .ident = closure.function }); + } + } + }.parse, + ); + } + + pub fn toCss(this: *const EasingFunction, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + return switch (this.*) { + .linear => try dest.writeStr("linear"), + .ease => try dest.writeStr("ease"), + .ease_in => try dest.writeStr("ease-in"), + .ease_out => try dest.writeStr("ease-out"), + .ease_in_out => try dest.writeStr("ease-in-out"), + else => { + if (this.isEase()) { + return dest.writeStr("ease"); + } else if (this == .cubic_bezier and std.meta.eql(this.cubic_bezier, .{ + .x1 = 0.42, + .y1 = 0.0, + .x2 = 1.0, + .y2 = 1.0, + })) { + return dest.writeStr("ease-in"); + } else if (this == .cubic_bezier and std.meta.eql(this.cubic_bezier, .{ + .x1 = 0.0, + .y1 = 0.0, + .x2 = 0.58, + .y2 = 1.0, + })) { + return dest.writeStr("ease-out"); + } else if (this == .cubic_bezier and std.meta.eql(this.cubic_bezier, .{ + .x1 = 0.42, + .y1 = 0.0, + .x2 = 0.58, + .y2 = 1.0, + })) { + return dest.writeStr("ease-in-out"); + } + + switch (this.*) { + .cubic_bezier => |cb| { + try dest.writeStr("cubic-bezier("); + try css.generic.toCss(cb.x1, W, dest); + try dest.writeChar(','); + try css.generic.toCss(cb.y1, W, dest); + try dest.writeChar(','); + try css.generic.toCss(cb.x2, W, dest); + try dest.writeChar(','); + try css.generic.toCss(cb.y2, W, dest); + try dest.writeChar(')'); + }, + .steps => { + if (this.steps.count == 1 and this.steps.position == .start) { + return try dest.writeStr("step-start"); + } + if (this.steps.count == 1 and this.steps.position == .end) { + return try dest.writeStr("step-end"); + } + try dest.writeStr("steps("); + try dest.writeFmt("steps({d}", .{this.steps.count}); + try dest.delim(',', false); + try this.steps.position.toCss(W, dest); + return try dest.writeChar(')'); + }, + .linear, .ease, .ease_in, .ease_out, .ease_in_out => unreachable, + } + }, + }; + } + + /// Returns whether the easing function is equivalent to the `ease` keyword. + pub fn isEase(this: *const EasingFunction) bool { + return this.* == .ease or + (this.* == .cubic_bezier and std.meta.eql(this.cubic_bezier == .{ + .x1 = 0.25, + .y1 = 0.1, + .x2 = 0.25, + .y2 = 1.0, + })); + } +}; + +/// A [step position](https://www.w3.org/TR/css-easing-1/#step-position), used within the `steps()` function. +pub const StepPosition = enum { + /// The first rise occurs at input progress value of 0. + start, + /// The last rise occurs at input progress value of 1. + end, + /// All rises occur within the range (0, 1). + jump_none, + /// The first rise occurs at input progress value of 0 and the last rise occurs at input progress value of 1. + jump_both, + + // TODO: implement this + // pub usingnamespace css.DeriveToCss(@This()); + + pub fn toCss(this: *const StepPosition, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn parse(input: *css.Parser) Result(StepPosition) { + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + // todo_stuff.match_ignore_ascii_case + const keyword = if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "start")) + StepPosition.start + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "end")) + StepPosition.end + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "jump-start")) + StepPosition.start + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "jump-end")) + StepPosition.end + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "jump-none")) + StepPosition.jump_none + else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "jump-both")) + StepPosition.jump_both + else + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + return .{ .result = keyword }; + } +}; diff --git a/src/css/values/gradient.zig b/src/css/values/gradient.zig new file mode 100644 index 0000000000000..1736efed25c13 --- /dev/null +++ b/src/css/values/gradient.zig @@ -0,0 +1,1077 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const ArrayList = std.ArrayListUnmanaged; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const VendorPrefix = css.VendorPrefix; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CssColor = css.css_values.color.CssColor; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Url = css.css_values.url.Url; +const Angle = css.css_values.angle.Angle; +const AnglePercentage = css.css_values.angle.AnglePercentage; +const HorizontalPositionKeyword = css.css_values.position.HorizontalPositionKeyword; +const VerticalPositionKeyword = css.css_values.position.VerticalPositionKeyword; +const Position = css.css_values.position.Position; +const Length = css.css_values.length.Length; +const LengthPercentage = css.css_values.length.LengthPercentage; +const NumberOrPercentage = css.css_values.percentage.NumberOrPercentage; + +/// A CSS [``](https://www.w3.org/TR/css-images-3/#gradients) value. +pub const Gradient = union(enum) { + /// A `linear-gradient()`, and its vendor prefix. + linear: LinearGradient, + /// A `repeating-linear-gradient()`, and its vendor prefix. + repeating_linear: LinearGradient, + /// A `radial-gradient()`, and its vendor prefix. + radial: RadialGradient, + /// A `repeating-radial-gradient`, and its vendor prefix. + repeating_radial: RadialGradient, + /// A `conic-gradient()`. + conic: ConicGradient, + /// A `repeating-conic-gradient()`. + repeating_conic: ConicGradient, + /// A legacy `-webkit-gradient()`. + @"webkit-gradient": WebKitGradient, + + pub fn parse(input: *css.Parser) Result(Gradient) { + const location = input.currentSourceLocation(); + const func = switch (input.expectFunction()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const Closure = struct { location: css.SourceLocation, func: []const u8 }; + return input.parseNestedBlock(Gradient, Closure{ .location = location, .func = func }, struct { + fn parse( + closure: struct { location: css.SourceLocation, func: []const u8 }, + input_: *css.Parser, + ) Result(Gradient) { + // css.todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "linear-gradient")) { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "repeating-linear-gradient")) { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "radial-gradient")) { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "repeating-radial-gradient")) { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "conic-gradient")) { + return .{ .result = .{ .conic = switch (ConicGradient.parse(input_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "repeating-conic-gradient")) { + return .{ .result = .{ .repeating_conic = switch (ConicGradient.parse(input_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-linear-gradient")) { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-repeating-linear-gradient")) { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-radial-gradient")) { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-repeating-radial-gradient")) { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-linear-gradient")) { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .mox = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-repeating-linear-gradient")) { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .mox = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-radial-gradient")) { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .mox = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-repeating-radial-gradient")) { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .mox = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-linear-gradient")) { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-repeating-linear-gradient")) { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-radial-gradient")) { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-repeating-radial-gradient")) { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-gradient")) { + return .{ .result = .{ .@"webkit-gradient" = switch (WebKitGradient.parse(input_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + } else { + return closure.location.newUnexpectedTokenError(.{ .ident = closure.func }); + } + } + }.parse); + } + + pub fn toCss(this: *const Gradient, comptime W: type, dest: *Printer(W)) PrintErr!void { + const f: []const u8, const prefix: ?css.VendorPrefix = switch (this.*) { + .linear => |g| .{ "linear-gradient(", g.vendor_prefix }, + .repeating_linear => |g| .{ "repeating-linear-gradient(", g.vendor_prefix }, + .radial => |g| .{ "radial-gradient(", g.vendor_prefix }, + .repeating_radial => |g| .{ "repeating-linear-gradient(", g.vendor_prefix }, + .conic => .{ "conic-gradient(", null }, + .repeating_conic => .{ "repeating-conic-gradient(", null }, + .@"webkit-gradient" => .{ "-webkit-gradient(", null }, + }; + + if (prefix) |p| { + try p.toCss(W, dest); + } + + try dest.writeStr(f); + + switch (this.*) { + .linear, .repeating_linear => |*linear| { + try linear.toCss(W, dest, linear.vendor_prefix.eq(css.VendorPrefix{ .none = true })); + }, + .radial, .repeating_radial => |*radial| { + try radial.toCss(W, dest); + }, + .conic, .repeating_conic => |*conic| { + try conic.toCss(W, dest); + }, + .@"webkit-gradient" => |*g| { + try g.toCss(W, dest); + }, + } + + return dest.writeChar(')'); + } +}; + +/// A CSS [`linear-gradient()`](https://www.w3.org/TR/css-images-3/#linear-gradients) or `repeating-linear-gradient()`. +pub const LinearGradient = struct { + /// The vendor prefixes for the gradient. + vendor_prefix: VendorPrefix, + /// The direction of the gradient. + direction: LineDirection, + /// The color stops and transition hints for the gradient. + items: ArrayList(GradientItem(LengthPercentage)), + + pub fn parse(input: *css.Parser, vendor_prefix: VendorPrefix) Result(LinearGradient) { + const direction = if (input.tryParse(LineDirection.parse, .{vendor_prefix != VendorPrefix{ .none = true }}).asValue()) |dir| direction: { + if (input.expectComma().asErr()) |e| return .{ .err = e }; + break :direction dir; + } else .{ .vertical = .bottom }; + const items = switch (parseItems(LengthPercentage, input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = LinearGradient{ .direction = direction, .items = items, .vendor_prefix = vendor_prefix } }; + } + + pub fn toCss(this: *const LinearGradient, comptime W: type, dest: *Printer(W), is_prefixed: bool) PrintErr!void { + const angle = switch (this.direction) { + .vertical => |v| switch (v) { + .bottom => 180.0, + .top => 0.0, + }, + .angle => |a| a.toDegrees(), + else => -1.0, + }; + + // We can omit `to bottom` or `180deg` because it is the default. + if (angle == 180.0) { + // todo_stuff.depth + try serializeItems(&this.items, W, dest); + } + // If we have `to top` or `0deg`, and all of the positions and hints are percentages, + // we can flip the gradient the other direction and omit the direction. + else if (angle == 0.0 and dest.minify and brk: { + for (this.items.items) |*item| { + if (item.* == .hint and item.hint != .percentage) break :brk false; + if (item.* == .color_stop and item.color_stop.position != null and item.color_stop.position != .percetage) break :brk false; + } + break :brk true; + }) { + var flipped_items = ArrayList(GradientItem(LengthPercentage)).initCapacity( + dest.allocator, + this.items.items.len, + ) catch bun.outOfMemory(); + defer flipped_items.deinit(); + + var i: usize = this.items.items.len; + while (i > 0) { + i -= 1; + const item = &this.items.items[i]; + switch (item.*) { + .hint => |*h| switch (h.*) { + .percentage => |p| try flipped_items.append(.{ .hint = .{ .percentage = .{ .value = 1.0 - p.v } } }), + else => unreachable, + }, + .color_stop => |*cs| try flipped_items.append(.{ + .color_stop = .{ + .color = cs.color, + .position = if (cs.position) |*p| switch (p) { + .percentage => |perc| .{ .percentage = .{ .value = 1.0 - perc.value } }, + else => unreachable, + } else null, + }, + }), + } + } + + try serializeItems(&flipped_items, W, dest); + } else { + if ((this.direction != .vertical or this.direction.vertical != .bottom) and + (this.direction != .angle or this.direction.angle.deg != 180.0)) + { + try this.direction.toCss(W, dest, is_prefixed); + try dest.delim(',', false); + } + + try serializeItems(&this.items, W, dest); + } + } +}; + +/// A CSS [`radial-gradient()`](https://www.w3.org/TR/css-images-3/#radial-gradients) or `repeating-radial-gradient()`. +pub const RadialGradient = struct { + /// The vendor prefixes for the gradient. + vendor_prefix: VendorPrefix, + /// The shape of the gradient. + shape: EndingShape, + /// The position of the gradient. + position: Position, + /// The color stops and transition hints for the gradient. + items: ArrayList(GradientItem(LengthPercentage)), + + pub fn parse(input: *css.Parser, vendor_prefix: VendorPrefix) Result(RadialGradient) { + // todo_stuff.depth + const shape = switch (input.tryParse(EndingShape.parse, .{})) { + .result => |vv| vv, + .err => null, + }; + const position = switch (input.tryParse(struct { + fn parse(input_: *css.Parser) Result(Position) { + if (input_.expectIdentMatching("at").asErr()) |e| return .{ .err = e }; + return Position.parse(input_); + } + }.parse, .{})) { + .result => |v| v, + .err => null, + }; + + if (shape != null or position != null) { + if (input.expectComma().asErr()) |e| return .{ .err = e }; + } + + const items = switch (parseItems(LengthPercentage, input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ + .result = RadialGradient{ + // todo_stuff.depth + .shape = shape orelse EndingShape.default(), + // todo_stuff.depth + .position = position orelse Position.center(), + .items = items, + .vendor_prefix = vendor_prefix, + }, + }; + } + + pub fn toCss(this: *const RadialGradient, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (std.meta.eql(this.shape, EndingShape.default())) { + try this.shape.toCss(W, dest); + if (this.position.isCenter()) { + try dest.delim(',', false); + } else { + try dest.writeChar(' '); + } + } + + if (!this.position.isCenter()) { + try dest.writeStr("at "); + try this.position.toCss(W, dest); + try dest.delim(',', false); + } + + try serializeItems(&this.items, W, dest); + } +}; + +/// A CSS [`conic-gradient()`](https://www.w3.org/TR/css-images-4/#conic-gradients) or `repeating-conic-gradient()`. +pub const ConicGradient = struct { + /// The angle of the gradient. + angle: Angle, + /// The position of the gradient. + position: Position, + /// The color stops and transition hints for the gradient. + items: ArrayList(GradientItem(AnglePercentage)), + + pub fn parse(input: *css.Parser) Result(ConicGradient) { + const angle = input.tryParse(struct { + inline fn parse(i: *css.Parser) Result(Angle) { + if (i.expectIdentMatching("from").asErr()) |e| return .{ .err = e }; + // Spec allows unitless zero angles for gradients. + // https://w3c.github.io/csswg-drafts/css-images-4/#valdef-conic-gradient-angle + return Angle.parseWithUnitlessZero(i); + } + }.parse, .{}).unwrapOr(Angle{ .deg = 0.0 }); + + const position = input.tryParse(struct { + inline fn parse(i: *css.Parser) Result(Position) { + if (i.expectIdentMatching("at").asErr()) |e| return .{ .err = e }; + return Position.parse(i); + } + }.parse, .{}).unwrapOr(Position.center()); + + if (angle != .{ .deg = 0.0 } or !std.meta.eql(position, Position.center())) { + if (input.expectComma().asErr()) |e| return .{ .err = e }; + } + + const items = switch (parseItems(AnglePercentage, input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = ConicGradient{ + .angle = angle, + .position = position, + .items = items, + } }; + } + + pub fn toCss(this: *const ConicGradient, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (!this.angle.isZero()) { + try dest.writeStr("from "); + try this.angle.toCss(W, dest); + + if (this.position.isCenter()) { + try dest.delim(',', false); + } else { + try dest.writeChar(' '); + } + } + + if (!this.position.isCenter()) { + try dest.writeStr("at "); + try this.position.toCss(W, dest); + try dest.delim(',', false); + } + + return try serializeItems(AnglePercentage, &this.items, W, dest); + } +}; + +/// A legacy `-webkit-gradient()`. +pub const WebKitGradient = union(enum) { + /// A linear `-webkit-gradient()`. + linear: struct { + /// The starting point of the gradient. + from: WebKitGradientPoint, + /// The ending point of the gradient. + to: WebKitGradientPoint, + /// The color stops in the gradient. + stops: ArrayList(WebKitColorStop), + }, + /// A radial `-webkit-gradient()`. + radial: struct { + /// The starting point of the gradient. + from: WebKitGradientPoint, + /// The starting radius of the gradient. + r0: CSSNumber, + /// The ending point of the gradient. + to: WebKitGradientPoint, + /// The ending radius of the gradient. + r1: CSSNumber, + /// The color stops in the gradient. + stops: ArrayList(WebKitColorStop), + }, + + pub fn parse(input: *css.Parser) Result(WebKitGradient) { + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "linear")) { + // todo_stuff.depth + const from = switch (WebKitGradientPoint.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + const to = switch (WebKitGradientPoint.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + const stops = switch (input.parseCommaSeparated(WebKitColorStop, WebKitColorStop.parse)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = WebKitGradient{ .linear = .{ + .from = from, + .to = to, + .stops = stops, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "radial")) { + const from = switch (WebKitGradientPoint.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + const r0 = switch (CSSNumberFns.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + const to = switch (WebKitGradientPoint.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + const r1 = switch (CSSNumberFns.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectComma().asErr()) |e| return .{ .err = e }; + // todo_stuff.depth + const stops = switch (input.parseCommaSeparated(WebKitColorStop, WebKitColorStop.parse)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = WebKitGradient{ + .radial = .{ + .from = from, + .r0 = r0, + .to = to, + .r1 = r1, + .stops = stops, + }, + } }; + } else { + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + } + } + + pub fn toCss(this: *const WebKitGradient, comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .linear => |*linear| { + try dest.writeStr("linear"); + try dest.delim(',', false); + try linear.from.toCss(W, dest); + try dest.delim(',', false); + try linear.to.toCss(W, dest); + for (linear.stops.items) |*stop| { + try dest.delim(',', false); + try stop.toCss(W, dest); + } + }, + .radial => |*radial| { + try dest.writeStr("radial"); + try dest.delim(',', false); + try radial.from.toCss(W, dest); + try dest.delim(',', false); + try radial.r0.toCss(W, dest); + try dest.delim(',', false); + try radial.to.toCss(W, dest); + try dest.delim(',', false); + try radial.r1.toCss(W, dest); + for (radial.stops.items) |*stop| { + try dest.delim(',', false); + try stop.toCss(W, dest); + } + }, + } + } +}; + +/// The direction of a CSS `linear-gradient()`. +/// +/// See [LinearGradient](LinearGradient). +pub const LineDirection = union(enum) { + /// An angle. + angle: Angle, + /// A horizontal position keyword, e.g. `left` or `right`. + horizontal: HorizontalPositionKeyword, + /// A vertical position keyword, e.g. `top` or `bottom`. + vertical: VerticalPositionKeyword, + /// A corner, e.g. `bottom left` or `top right`. + corner: struct { + /// A horizontal position keyword, e.g. `left` or `right`. + horizontal: HorizontalPositionKeyword, + /// A vertical position keyword, e.g. `top` or `bottom`. + vertical: VerticalPositionKeyword, + }, + + pub fn parse(input: *css.Parser, is_prefixed: bool) Result(Position) { + // Spec allows unitless zero angles for gradients. + // https://w3c.github.io/csswg-drafts/css-images-3/#linear-gradient-syntax + if (input.tryParse(Angle.parseWithUnitlessZero, .{}).asValue()) |angle| { + return .{ .result = LineDirection{ .angle = angle } }; + } + + if (!is_prefixed) { + if (input.expectIdentMatching("to").asErr()) |e| return .{ .err = e }; + } + + if (input.tryParse(HorizontalPositionKeyword.parse, .{}).asValue()) |x| { + if (input.tryParse(VerticalPositionKeyword.parse, .{}).asValue()) |y| { + return .{ .result = LineDirection{ .corner = .{ + .horizontal = x, + .vertical = y, + } } }; + } + return .{ .result = LineDirection{ .horizontal = x } }; + } + + const y = switch (VerticalPositionKeyword.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.tryParse(HorizontalPositionKeyword.parse, .{}).asValue()) |x| { + return .{ .result = LineDirection{ .corner = .{ + .horizontal = x, + .vertical = y, + } } }; + } + return .{ .result = LineDirection{ .vertical = y } }; + } + + pub fn toCss(this: *const LineDirection, comptime W: type, dest: *Printer(W), is_prefixed: bool) PrintErr!void { + switch (this.*) { + .angle => |*angle| try angle.toCss(W, dest), + .horizontal => |*k| { + if (dest.minify) { + try dest.writeStr(switch (k) { + .left => "270deg", + .right => "90deg", + }); + } else { + if (!is_prefixed) { + try dest.writeStr("to "); + } + try k.toCss(W, dest); + } + }, + .vertical => |*k| { + if (dest.minify) { + try dest.writeStr(switch (k) { + .top => "0deg", + .bottom => "180deg", + }); + } else { + if (!is_prefixed) { + try dest.writeStr("to "); + } + try k.toCss(W, dest); + } + }, + .corner => |*c| { + if (!is_prefixed) { + try dest.writeStr("to "); + } + try c.vertical.toCss(W, dest); + try dest.writeChar(' '); + try c.horizontal.toCss(W, dest); + }, + } + } +}; + +/// Either a color stop or interpolation hint within a gradient. +/// +/// This type is generic, and items may be either a [LengthPercentage](super::length::LengthPercentage) +/// or [Angle](super::angle::Angle) depending on what type of gradient it is within. +pub fn GradientItem(comptime D: type) type { + return union(enum) { + /// A color stop. + color_stop: ColorStop(D), + /// A color interpolation hint. + hint: D, + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.*) { + .color_stop => |*c| try c.toCss(W, dest), + .hint => |*h| try css.generic.toCss(D, h, W, dest), + }; + } + }; +} + +/// A `radial-gradient()` [ending shape](https://www.w3.org/TR/css-images-3/#valdef-radial-gradient-ending-shape). +/// +/// See [RadialGradient](RadialGradient). +pub const EndingShape = union(enum) { + /// An ellipse. + ellipse: Ellipse, + /// A circle. + circle: Circle, + + pub fn default() EndingShape { + return .{ .ellipse = .{ .extent = .@"farthest-corner" } }; + } +}; + +/// An x/y position within a legacy `-webkit-gradient()`. +pub const WebKitGradientPoint = struct { + /// The x-position. + x: WebKitGradientPointComponent(HorizontalPositionKeyword), + /// The y-position. + y: WebKitGradientPointComponent(VerticalPositionKeyword), + + pub fn parse(input: *css.Parser) Result(WebKitGradientPoint) { + const x = switch (WebKitGradientPointComponent(HorizontalPositionKeyword).parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + const y = switch (WebKitGradientPointComponent(VerticalPositionKeyword).parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .x = x, .y = y } }; + } + + pub fn toCss(this: *const WebKitGradientPoint, comptime W: type, dest: *Printer(W)) PrintErr!void { + try this.x.toCss(W, dest); + try dest.writeChar(' '); + return try this.y.toCss(W, dest); + } +}; + +/// A keyword or number within a [WebKitGradientPoint](WebKitGradientPoint). +pub fn WebKitGradientPointComponent(comptime S: type) type { + return union(enum) { + /// The `center` keyword. + center, + /// A number or percentage. + number: NumberOrPercentage, + /// A side keyword. + side: S, + + const This = @This(); + + pub fn parse(input: *css.Parser) Result(This) { + if (input.tryParse(css.Parser.expectIdentMatching, .{"center"}).isOk()) { + return .{ .result = .center }; + } + + if (input.tryParse(NumberOrPercentage.parse, .{}).asValue()) |number| { + return .{ .result = .{ .number = number } }; + } + + const keyword = switch (css.generic.parse(S, input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .side = keyword } }; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + switch (this.*) { + .center => { + if (dest.minify) { + try dest.writeStr("50%"); + } else { + try dest.writeStr("center"); + } + }, + .number => |*lp| { + if (lp == .percentage and lp.percentage.value == 0.0) { + try dest.writeChar('0'); + } else { + try lp.toCss(W, dest); + } + }, + .side => |*s| { + if (dest.minify) { + const lp: LengthPercentage = s.intoLengthPercentage(); + try lp.toCss(W, dest); + } else { + try s.toCss(W, dest); + } + }, + } + } + }; +} + +/// A color stop within a legacy `-webkit-gradient()`. +pub const WebKitColorStop = struct { + /// The color of the color stop. + color: CssColor, + /// The position of the color stop. + position: CSSNumber, + + pub fn parse(input: *css.Parser) Result(WebKitColorStop) { + const location = input.currentSourceLocation(); + const function = switch (input.expectFunction()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const Closure = struct { loc: css.SourceLocation, function: []const u8 }; + return input.parseNestedBlock( + WebKitColorStop, + Closure{ .loc = location, .function = function }, + struct { + fn parse( + closure: Closure, + i: *css.Parser, + ) Result(WebKitColorStop) { + // todo_stuff.match_ignore_ascii_case + const position: f32 = if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "color-stop")) position: { + const p: NumberOrPercentage = switch (@call(.auto, @field(NumberOrPercentage, "parse"), .{i})) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + break :position p.intoF32(); + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "from")) position: { + break :position 0.0; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "to")) position: { + break :position 1.0; + } else { + return closure.loc.newUnexpectedTokenError(.{ .ident = closure.function }); + }; + const color = switch (CssColor.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = WebKitColorStop{ .color = color, .position = position } }; + } + }.parse, + ); + } + + pub fn toCss(this: *const WebKitColorStop, comptime W: type, dest: *Printer(W)) PrintErr!void { + if (this.position == 0.0) { + try dest.writeStr("from("); + try this.color.toCss(W, dest); + } else if (this.position == 1.0) { + try dest.writeStr("to("); + try this.color.toCss(W, dest); + } else { + try dest.writeStr("color-stop("); + try css.generic.toCss(CSSNumber, &this.position, W, dest); + try dest.delim(',', false); + try this.color.toCss(W, dest); + } + try dest.writeChar(')'); + } +}; + +/// A [``](https://www.w3.org/TR/css-images-4/#color-stop-syntax) within a gradient. +/// +/// This type is generic, and may be either a [LengthPercentage](super::length::LengthPercentage) +/// or [Angle](super::angle::Angle) depending on what type of gradient it is within. +pub fn ColorStop(comptime D: type) type { + return struct { + /// The color of the color stop. + color: CssColor, + /// The position of the color stop. + position: ?D, + + const This = @This(); + + pub fn parse(input: *css.Parser) Result(ColorStop(D)) { + const color = switch (CssColor.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const position = switch (input.tryParse(css.generic.parseFor(D), .{})) { + .result => |v| v, + .err => null, + }; + return .{ .result = .{ .color = color, .position = position } }; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + try this.color.toCss(W, dest); + if (this.position) |*position| { + try dest.delim(',', false); + try css.generic.toCss(D, position, W, dest); + } + return; + } + }; +} + +/// An ellipse ending shape for a `radial-gradient()`. +/// +/// See [RadialGradient](RadialGradient). +pub const Ellipse = union(enum) { + /// An ellipse with a specified horizontal and vertical radius. + size: struct { + /// The x-radius of the ellipse. + x: LengthPercentage, + /// The y-radius of the ellipse. + y: LengthPercentage, + }, + /// A shape extent keyword. + extent: ShapeExtent, + + pub fn parse(input: *css.Parser) Result(Ellipse) { + if (input.tryParse(ShapeExtent.parse, .{}).asValue()) |extent| { + // The `ellipse` keyword is optional, but only if the `circle` keyword is not present. + // If it is, then we'll re-parse as a circle. + if (input.tryParse(css.Parser.expectIdentMatching, .{"circle"}).isOk()) { + return .{ .err = input.newErrorForNextToken() }; + } + _ = input.tryParse(css.Parser.expectIdentMatching, .{"ellipse"}); + return .{ .result = Ellipse{ .extent = extent } }; + } + + if (input.tryParse(LengthPercentage.parse, .{}).asValue()) |x| { + const y = switch (LengthPercentage.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + // The `ellipse` keyword is optional if there are two lengths. + _ = input.tryParse(css.Parser.expectIdentMatching, .{"ellipse"}); + return .{ .result = Ellipse{ .size = .{ .x = x, .y = y } } }; + } + + if (input.tryParse(css.Parser.expectIdentMatching, .{"ellipse"}).isOk()) { + if (input.tryParse(ShapeExtent.parse, .{}).asValue()) |extent| { + return .{ .result = Ellipse{ .extent = extent } }; + } + + if (input.tryParse(LengthPercentage.parse, .{}).asValue()) |x| { + const y = switch (LengthPercentage.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = Ellipse{ .size = .{ .x = x, .y = y } } }; + } + + // Assume `farthest-corner` if only the `ellipse` keyword is present. + return .{ .result = Ellipse{ .extent = .@"farthest-corner" } }; + } + + return .{ .err = input.newErrorForNextToken() }; + } + + pub fn toCss(this: *const Ellipse, comptime W: type, dest: *Printer(W)) PrintErr!void { + // The `ellipse` keyword is optional, so we don't emit it. + return switch (this.*) { + .size => |*s| { + try s.x.toCss(W, dest); + try dest.writeChar(' '); + return try s.y.toCss(W, dest); + }, + .extent => |*e| try e.toCss(W, dest), + }; + } +}; + +pub const ShapeExtent = enum { + /// The closest side of the box to the gradient's center. + @"closest-side", + /// The farthest side of the box from the gradient's center. + @"farthest-side", + /// The closest corner of the box to the gradient's center. + @"closest-corner", + /// The farthest corner of the box from the gradient's center. + @"farthest-corner", + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return css.enum_property_util.toCss(@This(), this, W, dest); + } +}; + +/// A circle ending shape for a `radial-gradient()`. +/// +/// See [RadialGradient](RadialGradient). +pub const Circle = union(enum) { + /// A circle with a specified radius. + radius: Length, + /// A shape extent keyword. + extent: ShapeExtent, + + pub fn parse(input: *css.Parser) Result(Circle) { + if (input.tryParse(ShapeExtent.parse, .{}).asValue()) |extent| { + // The `circle` keyword is required. If it's not there, then it's an ellipse. + if (input.expectIdentMatching("circle").asErr()) |e| return .{ .err = e }; + return .{ .result = Circle{ .extent = extent } }; + } + + if (input.tryParse(Length.parse, .{}).asValue()) |length| { + // The `circle` keyword is optional if there is only a single length. + // We are assuming here that Ellipse.parse ran first. + _ = input.tryParse(css.Parser.expectIdentMatching, .{"circle"}); + return .{ .result = Circle{ .radius = length } }; + } + + if (input.tryParse(css.Parser.expectIdentMatching, .{"circle"}).isOk()) { + if (input.tryParse(ShapeExtent.parse, .{}).asValue()) |extent| { + return .{ .result = Circle{ .extent = extent } }; + } + + if (input.tryParse(Length.parse, .{}).asValue()) |length| { + return .{ .result = Circle{ .radius = length } }; + } + + // If only the `circle` keyword was given, default to `farthest-corner`. + return .{ .result = Circle{ .extent = .@"farthest-corner" } }; + } + + return .{ .err = input.newErrorForNextToken() }; + } + + pub fn toCss(this: *const Circle, comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.*) { + .radius => |r| try r.toCss(W, dest), + .extent => |extent| { + try dest.writeStr("circle"); + if (extent != .@"farthest-corner") { + try dest.writeChar(' '); + try extent.toCss(W, dest); + } + }, + }; + } +}; + +pub fn parseItems(comptime D: type, input: *css.Parser) Result(ArrayList(GradientItem(D))) { + var items = ArrayList(GradientItem(D)){}; + var seen_stop = false; + + while (true) { + const Closure = struct { items: *ArrayList(GradientItem(D)), seen_stop: *bool }; + if (input.parseUntilBefore( + css.Delimiters{ .comma = true }, + Closure{ .items = &items, .seen_stop = &seen_stop }, + struct { + fn parse(closure: Closure, i: *css.Parser) Result(void) { + if (closure.seen_stop.*) { + if (i.tryParse(comptime css.generic.parseFor(D), .{}).asValue()) |hint| { + closure.seen_stop.* = false; + closure.items.append(.{ .hint = hint }) catch bun.outOfMemory(); + return Result(void).success; + } + } + + const stop = switch (ColorStop(D).parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + if (i.tryParse(comptime css.generic.parseFor(D), .{})) |position| { + const color = stop.color.deepClone(i.allocator()); + closure.items.append(.{ .color_stop = stop }) catch bun.outOfMemory(); + closure.items.append(.{ .color_stop = .{ + .color = color, + .position = position, + } }) catch bun.outOfMemory(); + } else { + closure.items.append(.{ .color_stop = stop }) catch bun.outOfMemory(); + } + + closure.seen_stop.* = true; + return Result(void).success; + } + }.parse, + ).asErr()) |e| return .{ .err = e }; + + if (input.next().asValue()) |tok| { + if (tok == .comma) continue; + bun.unreachablePanic("expected a comma after parsing a gradient", .{}); + } else { + break; + } + } + + return .{ .result = items }; +} + +pub fn serializeItems( + comptime D: type, + items: *const ArrayList(GradientItem(D)), + comptime W: type, + dest: *Printer(W), +) PrintErr!void { + var first = true; + var last: ?*const GradientItem(D) = null; + for (items.items) |*item| { + // Skip useless hints + if (item.* == .hint and item.hint == .percentage and item.hint.percentage.value == 0.5) { + continue; + } + + // Use double position stop if the last stop is the same color and all targets support it. + if (last) |prev| { + if (!dest.targets.shouldCompile(.double_position_gradients, .{ .double_position_gradients = true })) { + if (prev.* == .color_stop and prev.color_stop.position != null and + item.* == .color_stop and item.color_stop.position != null and + prev.color_stop.color.eql(&item.color_stop.color)) + { + try dest.writeChar(' '); + try item.color_stop.position.?.toCss(W, dest); + last = null; + continue; + } + } + } + + if (first) { + first = false; + } else { + try dest.delim(',', false); + } + try item.toCss(W, dest); + last = item; + } +} diff --git a/src/css/values/ident.zig b/src/css/values/ident.zig new file mode 100644 index 0000000000000..05943424ee046 --- /dev/null +++ b/src/css/values/ident.zig @@ -0,0 +1,149 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +const logger = bun.logger; +const Log = logger.Log; + +pub const css = @import("../css_parser.zig"); +pub const Result = css.Result; +pub const Printer = css.Printer; +pub const PrintErr = css.PrintErr; + +pub const Specifier = css.css_properties.css_modules.Specifier; + +/// A CSS [``](https://www.w3.org/TR/css-values-4/#dashed-idents) reference. +/// +/// Dashed idents are used in cases where an identifier can be either author defined _or_ CSS-defined. +/// Author defined idents must start with two dash characters ("--") or parsing will fail. +/// +/// In CSS modules, when the `dashed_idents` option is enabled, the identifier may be followed by the +/// `from` keyword and an argument indicating where the referenced identifier is declared (e.g. a filename). +pub const DashedIdentReference = struct { + /// The referenced identifier. + ident: DashedIdent, + /// CSS modules extension: the filename where the variable is defined. + /// Only enabled when the CSS modules `dashed_idents` option is turned on. + from: ?Specifier, + + pub fn parseWithOptions(input: *css.Parser, options: *const css.ParserOptions) Result(DashedIdentReference) { + const ident = switch (DashedIdentFns.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + const from = if (options.css_modules != null and options.css_modules.?.dashed_idents) from: { + if (input.tryParse(css.Parser.expectIdentMatching, .{"from"}).isOk()) break :from switch (Specifier.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + break :from null; + } else null; + + return .{ .result = DashedIdentReference{ .ident = ident, .from = from } }; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + if (dest.css_module) |*css_module| { + if (css_module.config.dashed_idents) { + if (css_module.referenceDashed(this.ident.v, &this.from, dest.loc.source_index)) |name| { + try dest.writeStr("--"); + css.serializer.serializeName(name, dest) catch return dest.addFmtError(); + return; + } + } + } + + return dest.writeDashedIdent(&this.ident, false); + } +}; + +pub const DashedIdentFns = DashedIdent; +/// A CSS [``](https://www.w3.org/TR/css-values-4/#dashed-idents) declaration. +/// +/// Dashed idents are used in cases where an identifier can be either author defined _or_ CSS-defined. +/// Author defined idents must start with two dash characters ("--") or parsing will fail. +pub const DashedIdent = struct { + v: []const u8, + + pub fn parse(input: *css.Parser) Result(DashedIdent) { + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (!bun.strings.startsWith(ident, "--")) return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + + return .{ .result = .{ .v = ident } }; + } + + const This = @This(); + + pub fn toCss(this: *const DashedIdent, comptime W: type, dest: *Printer(W)) PrintErr!void { + return dest.writeDashedIdent(this, true); + } +}; + +/// A CSS [``](https://www.w3.org/TR/css-values-4/#css-css-identifier). +pub const IdentFns = Ident; +pub const Ident = struct { + v: []const u8, + + pub fn parse(input: *css.Parser) Result(Ident) { + const ident = switch (input.expectIdent()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .v = ident } }; + } + + pub fn toCss(this: *const Ident, comptime W: type, dest: *Printer(W)) PrintErr!void { + return css.serializer.serializeIdentifier(this.v, dest) catch return dest.addFmtError(); + } +}; + +pub const CustomIdentFns = CustomIdent; +pub const CustomIdent = struct { + v: []const u8, + + pub fn parse(input: *css.Parser) Result(CustomIdent) { + const location = input.currentSourceLocation(); + const ident = switch (input.expectIdent()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + // css.todo_stuff.match_ignore_ascii_case + const valid = !(bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "initial") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "inherit") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "unset") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "default") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "revert") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(ident, "revert-layer")); + + if (!valid) return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + return .{ .result = .{ .v = ident } }; + } + + const This = @This(); + + pub fn toCss(this: *const CustomIdent, comptime W: type, dest: *Printer(W)) PrintErr!void { + return @This().toCssWithOptions(this, W, dest, true); + } + + /// Write the custom ident to CSS. + pub fn toCssWithOptions( + this: *const CustomIdent, + comptime W: type, + dest: *Printer(W), + enabled_css_modules: bool, + ) PrintErr!void { + const css_module_custom_idents_enabled = enabled_css_modules and + if (dest.css_module) |*css_module| + css_module.config.custom_idents + else + false; + return dest.writeIdent(this.v, css_module_custom_idents_enabled); + } +}; + +/// A list of CSS [``](https://www.w3.org/TR/css-values-4/#custom-idents) values. +pub const CustomIdentList = css.SmallList(CustomIdent, 1); diff --git a/src/css/values/image.zig b/src/css/values/image.zig new file mode 100644 index 0000000000000..aed0f1908416b --- /dev/null +++ b/src/css/values/image.zig @@ -0,0 +1,195 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Url = css.css_values.url.Url; +const Gradient = css.css_values.gradient.Gradient; +const Resolution = css.css_values.resolution.Resolution; +const VendorPrefix = css.VendorPrefix; +const UrlDependency = css.dependencies.UrlDependency; + +/// A CSS [``](https://www.w3.org/TR/css-images-3/#image-values) value. +pub const Image = union(enum) { + /// The `none` keyword. + none, + /// A `url()`. + url: Url, + /// A gradient. + gradient: *Gradient, + /// An `image-set()`. + image_set: *ImageSet, + + // pub usingnamespace css.DeriveParse(@This()); + // pub usingnamespace css.DeriveToCss(@This()); + + pub fn parse(input: *css.Parser) Result(Image) { + _ = input; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn toCss(this: *const Image, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @panic(css.todo_stuff.depth); + } +}; + +/// A CSS [`image-set()`](https://drafts.csswg.org/css-images-4/#image-set-notation) value. +/// +/// `image-set()` allows the user agent to choose between multiple versions of an image to +/// display the most appropriate resolution or file type that it supports. +pub const ImageSet = struct { + /// The image options to choose from. + options: ArrayList(ImageSetOption), + + /// The vendor prefix for the `image-set()` function. + vendor_prefix: VendorPrefix, + + pub fn parse(input: *css.Parser) Result(ImageSet) { + const location = input.currentSourceLocation(); + const f = input.expectFunction(); + const vendor_prefix = vendor_prefix: { + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("image-set", css.VendorPrefix{.none})) { + break :vendor_prefix .none; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("-webkit-image-set", css.VendorPrefix{.none})) { + break :vendor_prefix .webkit; + } else return .{ .err = location.newUnexpectedTokenError(.{ .ident = f }) }; + }; + + const Fn = struct { + pub fn parseNestedBlockFn(_: void, i: *css.Parser) Result(ArrayList(ImageSetOption)) { + return i.parseCommaSeparated(ImageSetOption, ImageSetOption.parse); + } + }; + + const options = switch (input.parseNestedBlock(ArrayList(ImageSetOption), {}, Fn.parseNestedBlockFn)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + return .{ .result = ImageSet{ + .options = options, + .vendor_prefix = vendor_prefix, + } }; + } + + pub fn toCss(this: *const ImageSet, comptime W: type, dest: *css.Printer(W)) PrintErr!void { + try this.vendor_prefix.toCss(W, dest); + try dest.writeStr("image-set("); + var first = true; + for (this.options.items) |*option| { + if (first) { + first = false; + } else { + try dest.delim(',', false); + } + try option.toCss(W, dest); + } + return dest.writeChar(')'); + } +}; + +/// An image option within the `image-set()` function. See [ImageSet](ImageSet). +pub const ImageSetOption = struct { + /// The image for this option. + image: Image, + /// The resolution of the image. + resolution: Resolution, + /// The mime type of the image. + file_type: ?[]const u8, + + pub fn parse(input: *css.Parser) Result(ImageSetOption) { + const loc = input.currentSourceLocation(); + const image = if (input.tryParse(css.Parser.expectUrlOrString, .{}).asValue()) |url| + Image{ .url = Url{ + .url = url, + .loc = loc, + } } + else switch (@call(.auto, @field(Image, "parse"), .{input})) { // For some reason, `Image.parse` makes zls crash, using this syntax until that's fixed + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + const resolution: Resolution, const file_type: ?[]const u8 = if (input.tryParse(Resolution.parse, .{}).asValue()) |res| brk: { + const file_type = input.tryParse(parseFileType, .{}).asValue(); + break :brk .{ res, file_type }; + } else brk: { + const file_type = input.tryParse(parseFileType, .{}).asValue(); + const resolution = input.tryParse(Resolution.parse, .{}).unwrapOr(Resolution{ .dppx = 1.0 }); + break :brk .{ resolution, file_type }; + }; + + return .{ .result = ImageSetOption{ + .image = image, + .resolution = resolution, + .file_type = if (file_type) |x| x else null, + } }; + } + + pub fn toCss( + this: *const ImageSetOption, + comptime W: type, + dest: *css.Printer(W), + is_prefixed: bool, + ) PrintErr!void { + if (this.image.* == .url and !is_prefixed) { + const _dep: ?UrlDependency = if (dest.dependencies != null) + UrlDependency.new(dest.allocator, &this.image.url.url, dest.filename()) + else + null; + + if (_dep) |dep| { + try css.serializer.serializeString(dep.placeholder, W, dest); + if (dest.dependencies) |*dependencies| { + dependencies.append( + dest.allocator, + .{ .url = dep }, + ) catch bun.outOfMemory(); + } + } else { + try css.serializer.serializeString(this.image.url.url, W, dest); + } + } else { + try this.image.toCss(W, dest); + } + + // TODO: Throwing an error when `self.resolution = Resolution::Dppx(0.0)` + // TODO: -webkit-image-set() does not support ` | | + // | | ` and `type()`. + try dest.writeChar(' '); + + // Safari only supports the x resolution unit in image-set(). + // In other places, x was added as an alias later. + // Temporarily ignore the targets while printing here. + const targets = targets: { + const targets = dest.targets; + dest.targets = .{}; + break :targets targets; + }; + try this.resolution.toCss(W, dest); + dest.targets = targets; + + if (this.file_type) |file_type| { + try dest.writeStr(" type("); + try css.serializer.serializeString(file_type, W, dest); + try dest.writeChar(')'); + } + } +}; + +fn parseFileType(input: *css.Parser) Result([]const u8) { + if (input.expectFunctionMatching("type").asErr()) |e| return .{ .err = e }; + const Fn = struct { + pub fn parseNestedBlockFn(_: void, i: *css.Parser) Result([]const u8) { + return i.expectString(); + } + }; + return input.parseNestedBlock([]const u8, {}, Fn.parseNestedBlockFn); +} diff --git a/src/css/values/length.zig b/src/css/values/length.zig new file mode 100644 index 0000000000000..c97fab17ff009 --- /dev/null +++ b/src/css/values/length.zig @@ -0,0 +1,500 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const DimensionPercentage = css.css_values.percentage.DimensionPercentage; + +/// Either a [``](https://www.w3.org/TR/css-values-4/#lengths) or a [``](https://www.w3.org/TR/css-values-4/#numbers). +pub const LengthOrNumber = union(enum) { + /// A number. + number: CSSNumber, + /// A length. + length: Length, +}; + +pub const LengthPercentage = DimensionPercentage(LengthValue); +/// Either a [``](https://www.w3.org/TR/css-values-4/#typedef-length-percentage), or the `auto` keyword. +pub const LengthPercentageOrAuto = union(enum) { + /// The `auto` keyword. + auto, + /// A [``](https://www.w3.org/TR/css-values-4/#typedef-length-percentage). + length: LengthPercentage, +}; + +const PX_PER_IN: f32 = 96.0; +const PX_PER_CM: f32 = PX_PER_IN / 2.54; +const PX_PER_MM: f32 = PX_PER_CM / 10.0; +const PX_PER_Q: f32 = PX_PER_CM / 40.0; +const PX_PER_PT: f32 = PX_PER_IN / 72.0; +const PX_PER_PC: f32 = PX_PER_IN / 6.0; + +pub const LengthValue = union(enum) { + // https://www.w3.org/TR/css-values-4/#absolute-lengths + /// A length in pixels. + px: CSSNumber, + /// A length in inches. 1in = 96px. + in: CSSNumber, + /// A length in centimeters. 1cm = 96px / 2.54. + cm: CSSNumber, + /// A length in millimeters. 1mm = 1/10th of 1cm. + mm: CSSNumber, + /// A length in quarter-millimeters. 1Q = 1/40th of 1cm. + q: CSSNumber, + /// A length in points. 1pt = 1/72nd of 1in. + pt: CSSNumber, + /// A length in picas. 1pc = 1/6th of 1in. + pc: CSSNumber, + + // https://www.w3.org/TR/css-values-4/#font-relative-lengths + /// A length in the `em` unit. An `em` is equal to the computed value of the + /// font-size property of the element on which it is used. + em: CSSNumber, + /// A length in the `rem` unit. A `rem` is equal to the computed value of the + /// `em` unit on the root element. + rem: CSSNumber, + /// A length in `ex` unit. An `ex` is equal to the x-height of the font. + ex: CSSNumber, + /// A length in the `rex` unit. A `rex` is equal to the value of the `ex` unit on the root element. + rex: CSSNumber, + /// A length in the `ch` unit. A `ch` is equal to the width of the zero ("0") character in the current font. + ch: CSSNumber, + /// A length in the `rch` unit. An `rch` is equal to the value of the `ch` unit on the root element. + rch: CSSNumber, + /// A length in the `cap` unit. A `cap` is equal to the cap-height of the font. + cap: CSSNumber, + /// A length in the `rcap` unit. An `rcap` is equal to the value of the `cap` unit on the root element. + rcap: CSSNumber, + /// A length in the `ic` unit. An `ic` is equal to the width of the “水” (CJK water ideograph) character in the current font. + ic: CSSNumber, + /// A length in the `ric` unit. An `ric` is equal to the value of the `ic` unit on the root element. + ric: CSSNumber, + /// A length in the `lh` unit. An `lh` is equal to the computed value of the `line-height` property. + lh: CSSNumber, + /// A length in the `rlh` unit. An `rlh` is equal to the value of the `lh` unit on the root element. + rlh: CSSNumber, + + // https://www.w3.org/TR/css-values-4/#viewport-relative-units + /// A length in the `vw` unit. A `vw` is equal to 1% of the [viewport width](https://www.w3.org/TR/css-values-4/#ua-default-viewport-size). + vw: CSSNumber, + /// A length in the `lvw` unit. An `lvw` is equal to 1% of the [large viewport width](https://www.w3.org/TR/css-values-4/#large-viewport-size). + lvw: CSSNumber, + /// A length in the `svw` unit. An `svw` is equal to 1% of the [small viewport width](https://www.w3.org/TR/css-values-4/#small-viewport-size). + svw: CSSNumber, + /// A length in the `dvw` unit. An `dvw` is equal to 1% of the [dynamic viewport width](https://www.w3.org/TR/css-values-4/#dynamic-viewport-size). + dvw: CSSNumber, + /// A length in the `cqw` unit. An `cqw` is equal to 1% of the [query container](https://drafts.csswg.org/css-contain-3/#query-container) width. + cqw: CSSNumber, + + /// A length in the `vh` unit. A `vh` is equal to 1% of the [viewport height](https://www.w3.org/TR/css-values-4/#ua-default-viewport-size). + vh: CSSNumber, + /// A length in the `lvh` unit. An `lvh` is equal to 1% of the [large viewport height](https://www.w3.org/TR/css-values-4/#large-viewport-size). + lvh: CSSNumber, + /// A length in the `svh` unit. An `svh` is equal to 1% of the [small viewport height](https://www.w3.org/TR/css-values-4/#small-viewport-size). + svh: CSSNumber, + /// A length in the `dvh` unit. An `dvh` is equal to 1% of the [dynamic viewport height](https://www.w3.org/TR/css-values-4/#dynamic-viewport-size). + dvh: CSSNumber, + /// A length in the `cqh` unit. An `cqh` is equal to 1% of the [query container](https://drafts.csswg.org/css-contain-3/#query-container) height. + cqh: CSSNumber, + + /// A length in the `vi` unit. A `vi` is equal to 1% of the [viewport size](https://www.w3.org/TR/css-values-4/#ua-default-viewport-size) + /// in the box's [inline axis](https://www.w3.org/TR/css-writing-modes-4/#inline-axis). + vi: CSSNumber, + /// A length in the `svi` unit. A `svi` is equal to 1% of the [small viewport size](https://www.w3.org/TR/css-values-4/#small-viewport-size) + /// in the box's [inline axis](https://www.w3.org/TR/css-writing-modes-4/#inline-axis). + svi: CSSNumber, + /// A length in the `lvi` unit. A `lvi` is equal to 1% of the [large viewport size](https://www.w3.org/TR/css-values-4/#large-viewport-size) + /// in the box's [inline axis](https://www.w3.org/TR/css-writing-modes-4/#inline-axis). + lvi: CSSNumber, + /// A length in the `dvi` unit. A `dvi` is equal to 1% of the [dynamic viewport size](https://www.w3.org/TR/css-values-4/#dynamic-viewport-size) + /// in the box's [inline axis](https://www.w3.org/TR/css-writing-modes-4/#inline-axis). + dvi: CSSNumber, + /// A length in the `cqi` unit. An `cqi` is equal to 1% of the [query container](https://drafts.csswg.org/css-contain-3/#query-container) inline size. + cqi: CSSNumber, + + /// A length in the `vb` unit. A `vb` is equal to 1% of the [viewport size](https://www.w3.org/TR/css-values-4/#ua-default-viewport-size) + /// in the box's [block axis](https://www.w3.org/TR/css-writing-modes-4/#block-axis). + vb: CSSNumber, + /// A length in the `svb` unit. A `svb` is equal to 1% of the [small viewport size](https://www.w3.org/TR/css-values-4/#small-viewport-size) + /// in the box's [block axis](https://www.w3.org/TR/css-writing-modes-4/#block-axis). + svb: CSSNumber, + /// A length in the `lvb` unit. A `lvb` is equal to 1% of the [large viewport size](https://www.w3.org/TR/css-values-4/#large-viewport-size) + /// in the box's [block axis](https://www.w3.org/TR/css-writing-modes-4/#block-axis). + lvb: CSSNumber, + /// A length in the `dvb` unit. A `dvb` is equal to 1% of the [dynamic viewport size](https://www.w3.org/TR/css-values-4/#dynamic-viewport-size) + /// in the box's [block axis](https://www.w3.org/TR/css-writing-modes-4/#block-axis). + dvb: CSSNumber, + /// A length in the `cqb` unit. An `cqb` is equal to 1% of the [query container](https://drafts.csswg.org/css-contain-3/#query-container) block size. + cqb: CSSNumber, + + /// A length in the `vmin` unit. A `vmin` is equal to the smaller of `vw` and `vh`. + vmin: CSSNumber, + /// A length in the `svmin` unit. An `svmin` is equal to the smaller of `svw` and `svh`. + svmin: CSSNumber, + /// A length in the `lvmin` unit. An `lvmin` is equal to the smaller of `lvw` and `lvh`. + lvmin: CSSNumber, + /// A length in the `dvmin` unit. An `dvmin` is equal to the smaller of `dvw` and `dvh`. + dvmin: CSSNumber, + /// A length in the `cqmin` unit. An `cqmin` is equal to the smaller of `cqi` and `cqb`. + cqmin: CSSNumber, + + /// A length in the `vmax` unit. A `vmax` is equal to the larger of `vw` and `vh`. + vmax: CSSNumber, + /// A length in the `svmax` unit. An `svmax` is equal to the larger of `svw` and `svh`. + svmax: CSSNumber, + /// A length in the `lvmax` unit. An `lvmax` is equal to the larger of `lvw` and `lvh`. + lvmax: CSSNumber, + /// A length in the `dvmax` unit. An `dvmax` is equal to the larger of `dvw` and `dvh`. + dvmax: CSSNumber, + /// A length in the `cqmax` unit. An `cqmin` is equal to the larger of `cqi` and `cqb`. + cqmax: CSSNumber, + + pub fn parse(input: *css.Parser) Result(@This()) { + const location = input.currentSourceLocation(); + const token = switch (input.next()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + switch (token.*) { + .dimension => |*dim| { + // todo_stuff.match_ignore_ascii_case + inline for (std.meta.fields(@This())) |field| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(field.name, dim.unit)) { + return .{ .result = @unionInit(LengthValue, field.name, dim.num.value) }; + } + } + }, + .number => |*num| return .{ .result = .{ .px = num.value } }, + else => {}, + } + return .{ .err = location.newUnexpectedTokenError(token.*) }; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + const value, const unit = this.toUnitValue(); + + // The unit can be omitted if the value is zero, except inside calc() + // expressions, where unitless numbers won't be parsed as dimensions. + if (!dest.in_calc and value == 0.0) { + return dest.writeChar('0'); + } + + return css.serializer.serializeDimension(value, unit, W, dest); + } + + /// Attempts to convert the value to pixels. + /// Returns `None` if the conversion is not possible. + pub fn toPx(this: *const @This()) ?CSSNumber { + return switch (this.*) { + .px => |v| v, + .in => |v| v * PX_PER_IN, + .cm => |v| v * PX_PER_CM, + .mm => |v| v * PX_PER_MM, + .q => |v| v * PX_PER_Q, + .pt => |v| v * PX_PER_PT, + .pc => |v| v * PX_PER_PC, + else => null, + }; + } + + pub inline fn eql(this: *const @This(), other: *const @This()) bool { + inline for (bun.meta.EnumFields(@This())) |field| { + if (field.value == @intFromEnum(this.*) and field.value == @intFromEnum(other.*)) { + return @field(this, field.name) == @field(other, field.name); + } + } + return false; + } + + pub fn trySign(this: *const @This()) ?f32 { + return sign(this); + } + + pub fn sign(this: *const @This()) f32 { + const enum_fields = @typeInfo(@typeInfo(@This()).Union.tag_type.?).Enum.fields; + inline for (std.meta.fields(@This()), 0..) |field, i| { + if (enum_fields[i].value == @intFromEnum(this.*)) { + return css.signfns.signF32(@field(this, field.name)); + } + } + unreachable; + } + + pub fn tryFromToken(token: *const css.Token) css.Maybe(@This(), void) { + switch (token.*) { + .dimension => |*dim| { + inline for (std.meta.fields(@This())) |field| { + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(field.name, dim.unit)) { + return .{ .result = @unionInit(LengthValue, field.name, dim.num.value) }; + } + } + }, + else => {}, + } + return .{ .err = {} }; + } + + pub fn toUnitValue(this: *const @This()) struct { CSSNumber, []const u8 } { + const enum_fields = @typeInfo(@typeInfo(@This()).Union.tag_type.?).Enum.fields; + inline for (std.meta.fields(@This()), 0..) |field, i| { + if (enum_fields[i].value == @intFromEnum(this.*)) { + return .{ @field(this, field.name), field.name }; + } + } + unreachable; + } + + pub fn map(this: *const @This(), comptime map_fn: *const fn (f32) f32) LengthValue { + inline for (comptime bun.meta.EnumFields(@This())) |field| { + if (field.value == @intFromEnum(this.*)) { + return @unionInit(LengthValue, field.name, map_fn(@field(this, field.name))); + } + } + unreachable; + } + + pub fn mulF32(this: @This(), _: Allocator, other: f32) LengthValue { + const fields = comptime bun.meta.EnumFields(@This()); + inline for (fields) |field| { + if (field.value == @intFromEnum(this)) { + return @unionInit(LengthValue, field.name, @field(this, field.name) * other); + } + } + unreachable; + } + + pub fn tryFromAngle(_: css.css_values.angle.Angle) ?@This() { + return null; + } + + pub fn partialCmp(this: *const LengthValue, other: *const LengthValue) ?std.math.Order { + if (@intFromEnum(this.*) == @intFromEnum(other.*)) { + inline for (bun.meta.EnumFields(LengthValue)) |field| { + if (field.value == @intFromEnum(this.*)) { + const a = @field(this, field.name); + const b = @field(other, field.name); + return css.generic.partialCmpF32(&a, &b); + } + } + unreachable; + } + + const a = this.toPx(); + const b = this.toPx(); + if (a != null and b != null) { + return css.generic.partialCmpF32(&a.?, &b.?); + } + return null; + } + + pub fn tryOp( + this: *const LengthValue, + other: *const LengthValue, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) f32, + ) ?LengthValue { + if (@intFromEnum(this.*) == @intFromEnum(other.*)) { + inline for (bun.meta.EnumFields(LengthValue)) |field| { + if (field.value == @intFromEnum(this.*)) { + const a = @field(this, field.name); + const b = @field(other, field.name); + return @unionInit(LengthValue, field.name, op_fn(ctx, a, b)); + } + } + unreachable; + } + + const a = this.toPx(); + const b = this.toPx(); + if (a != null and b != null) { + return .{ .px = op_fn(ctx, a.?, b.?) }; + } + return null; + } + + pub fn tryOpTo( + this: *const LengthValue, + other: *const LengthValue, + comptime R: type, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) R, + ) ?R { + if (@intFromEnum(this.*) == @intFromEnum(other.*)) { + inline for (bun.meta.EnumFields(LengthValue)) |field| { + if (field.value == @intFromEnum(this.*)) { + const a = @field(this, field.name); + const b = @field(other, field.name); + return op_fn(ctx, a, b); + } + } + unreachable; + } + + const a = this.toPx(); + const b = this.toPx(); + if (a != null and b != null) { + return op_fn(ctx, a.?, b.?); + } + return null; + } +}; + +/// A CSS [``](https://www.w3.org/TR/css-values-4/#lengths) value, with support for `calc()`. +pub const Length = union(enum) { + /// An explicitly specified length value. + value: LengthValue, + /// A computed length value using `calc()`. + calc: *Calc(Length), + + pub fn deepClone(this: *const Length, allocator: Allocator) Length { + return switch (this.*) { + .value => |v| .{ .value = v }, + .calc => |calc| .{ .calc = bun.create(allocator, Calc(Length), Calc(Length).deepClone(calc, allocator)) }, + }; + } + + pub fn deinit(this: *const Length, allocator: Allocator) void { + return switch (this.*) { + .calc => |calc| calc.deinit(allocator), + .value => {}, + }; + } + + pub fn parse(input: *css.Parser) Result(Length) { + if (input.tryParse(Calc(Length).parse, .{}).asValue()) |calc_value| { + // PERF: I don't like this redundant allocation + if (calc_value == .value) { + var mutable: *Calc(Length) = @constCast(&calc_value); + const ret = calc_value.value.*; + mutable.deinit(input.allocator()); + return .{ .result = ret }; + } + return .{ .result = .{ + .calc = bun.create( + input.allocator(), + Calc(Length), + calc_value, + ), + } }; + } + + const len = switch (LengthValue.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .value = len } }; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + return switch (this.*) { + .value => |a| a.toCss(W, dest), + .calc => |c| c.toCss(W, dest), + }; + } + + pub fn eql(this: *const @This(), other: *const @This()) bool { + return switch (this.*) { + .value => |a| other.* == .value and a.eql(&other.value), + .calc => |a| other.* == .calc and a.eql(other.calc), + }; + } + + pub fn px(p: CSSNumber) Length { + return .{ .value = .{ .px = p } }; + } + + pub fn mulF32(this: Length, allocator: Allocator, other: f32) Length { + return switch (this) { + .value => Length{ .value = this.value.mulF32(allocator, other) }, + .calc => Length{ + .calc = bun.create( + allocator, + Calc(Length), + this.calc.mulF32(allocator, other), + ), + }, + }; + } + + pub fn add(this: Length, allocator: Allocator, other: Length) Length { + // Unwrap calc(...) functions so we can add inside. + // Then wrap the result in a calc(...) again if necessary. + const a = unwrapCalc(allocator, this); + _ = a; // autofix + const b = unwrapCalc(allocator, other); + _ = b; // autofix + @panic(css.todo_stuff.depth); + } + + fn unwrapCalc(allocator: Allocator, length: Length) Length { + return switch (length) { + .calc => |c| switch (c.*) { + .function => |f| switch (f.*) { + .calc => |c2| .{ .calc = bun.create(allocator, Calc(Length), c2) }, + else => |c2| .{ .calc = bun.create( + allocator, + Calc(Length), + Calc(Length){ .function = bun.create(allocator, css.css_values.calc.MathFunction(Length), c2) }, + ) }, + }, + else => .{ .calc = c }, + }, + else => length, + }; + } + + pub fn trySign(this: *const Length) ?f32 { + return switch (this.*) { + .value => |v| v.sign(), + .calc => |v| v.trySign(), + }; + } + + pub fn partialCmp(this: *const Length, other: *const Length) ?std.math.Order { + if (this.* == .value and other.* == .value) return css.generic.partialCmp(LengthValue, &this.value, &other.value); + return null; + } + + pub fn tryFromAngle(_: css.css_values.angle.Angle) ?@This() { + return null; + } + + pub fn tryMap(this: *const Length, comptime map_fn: *const fn (f32) f32) ?Length { + return switch (this.*) { + .value => |v| .{ .value = v.map(map_fn) }, + else => null, + }; + } + + pub fn tryOp( + this: *const Length, + other: *const Length, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) f32, + ) ?Length { + if (this.* == .value and other.* == .value) { + if (this.value.tryOp(&other.value, ctx, op_fn)) |val| return .{ .value = val }; + return null; + } + return null; + } + + pub fn tryOpTo( + this: *const Length, + other: *const Length, + comptime R: type, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) R, + ) ?R { + if (this.* == .value and other.* == .value) { + return this.value.tryOpTo(&other.value, R, ctx, op_fn); + } + return null; + } +}; diff --git a/src/css/values/number.zig b/src/css/values/number.zig new file mode 100644 index 0000000000000..ddb701a7c0cc3 --- /dev/null +++ b/src/css/values/number.zig @@ -0,0 +1,69 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const Calc = css.css_values.calc.Calc; + +pub const CSSNumber = f32; +pub const CSSNumberFns = struct { + pub fn parse(input: *css.Parser) Result(CSSNumber) { + if (input.tryParse(Calc(f32).parse, .{}).asValue()) |calc_value| { + switch (calc_value) { + .value => |v| return .{ .result = v.* }, + .number => |n| return .{ .result = n }, + // Numbers are always compatible, so they will always compute to a value. + else => return .{ .err = input.newCustomError(css.ParserError.invalid_value) }, + } + } + + return input.expectNumber(); + } + + pub fn toCss(this: *const CSSNumber, comptime W: type, dest: *Printer(W)) PrintErr!void { + const number: f32 = this.*; + if (number != 0.0 and @abs(number) < 1.0) { + // PERF(alloc): Use stack fallback here? + // why the extra allocation anyway? isn't max amount of digits to stringify an f32 small? + var s = ArrayList(u8){}; + defer s.deinit(dest.allocator); + const writer = s.writer(dest.allocator); + css.to_css.float32(number, writer) catch { + return dest.addFmtError(); + }; + if (number < 0.0) { + try dest.writeChar('-'); + try dest.writeStr(bun.strings.trimLeadingPattern2(s.items, '-', '0')); + } else { + try dest.writeStr(bun.strings.trimLeadingChar(s.items, '0')); + } + } else { + return css.to_css.float32(number, dest) catch { + return dest.addFmtError(); + }; + } + } + + pub fn tryFromAngle(_: css.css_values.angle.Angle) ?CSSNumber { + return null; + } + + pub fn sign(this: *const CSSNumber) f32 { + if (this.* == 0.0) return if (css.signfns.isSignPositive(this.*)) 0.0 else 0.0; + return css.signfns.signum(this.*); + } +}; + +/// A CSS [``](https://www.w3.org/TR/css-values-4/#integers) value. +pub const CSSInteger = i32; +pub const CSSIntegerFns = struct { + pub fn parse(input: *css.Parser) Result(CSSInteger) { + // TODO: calc?? + return input.expectInteger(); + } + pub inline fn toCss(this: *const CSSInteger, comptime W: type, dest: *Printer(W)) PrintErr!void { + try css.to_css.integer(i32, this.*, W, dest); + } +}; diff --git a/src/css/values/percentage.zig b/src/css/values/percentage.zig new file mode 100644 index 0000000000000..7cf9948d3fdc4 --- /dev/null +++ b/src/css/values/percentage.zig @@ -0,0 +1,297 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; + +pub const Percentage = struct { + v: CSSNumber, + + pub fn parse(input: *css.Parser) Result(Percentage) { + if (input.tryParse(Calc(Percentage).parse, .{}).asValue()) |calc_value| { + if (calc_value == .value) return .{ .result = calc_value.value.* }; + // Percentages are always compatible, so they will always compute to a value. + bun.unreachablePanic("Percentages are always compatible, so they will always compute to a value.", .{}); + } + + const percent = switch (input.expectPercentage()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + return .{ .result = Percentage{ .v = percent } }; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + const x = this.v * 100.0; + const int_value: ?i32 = if ((x - @trunc(x)) == 0.0) + @intFromFloat(this.v) + else + null; + + const percent = css.Token{ .percentage = .{ + .has_sign = this.v < 0.0, + .unit_value = this.v, + .int_value = int_value, + } }; + + if (this.v != 0.0 and @abs(this.v) < 0.01) { + // TODO: is this the max length? + var buf: [32]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buf); + var string = std.ArrayList(u8).init(fba.allocator()); + const writer = string.writer(); + percent.toCssGeneric(writer) catch return dest.addFmtError(); + if (this.v < 0.0) { + try dest.writeChar('-'); + try dest.writeStr(bun.strings.trimLeadingPattern2(string.items, '-', '0')); + } else { + try dest.writeStr(bun.strings.trimLeadingChar(string.items, '0')); + } + } else { + try percent.toCss(W, dest); + } + } + + pub fn add(lhs: Percentage, _: std.mem.Allocator, rhs: Percentage) Percentage { + return Percentage{ .v = lhs.v + rhs.v }; + } + + pub fn mulF32(this: Percentage, _: std.mem.Allocator, other: f32) Percentage { + return Percentage{ .v = this.v * other }; + } + + pub fn isZero(this: *const Percentage) bool { + return this.v == 0.0; + } + + pub fn sign(this: *const Percentage) f32 { + return css.signfns.signF32(this.v); + } + + pub fn trySign(this: *const Percentage) ?f32 { + return this.sign(); + } + + pub fn partialCmp(this: *const Percentage, other: *const Percentage) ?std.math.Order { + return css.generic.partialCmp(f32, &this.v, &other.v); + } + + pub fn tryFromAngle(_: css.css_values.angle.Angle) ?Percentage { + return null; + } + + pub fn tryMap(_: *const Percentage, comptime _: *const fn (f32) f32) ?Percentage { + // Percentages cannot be mapped because we don't know what they will resolve to. + // For example, they might be positive or negative depending on what they are a + // percentage of, which we don't know. + return null; + } + + pub fn op( + this: *const Percentage, + other: *const Percentage, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) f32, + ) Percentage { + return Percentage{ .v = op_fn(ctx, this.v, other.v) }; + } + + pub fn opTo( + this: *const Percentage, + other: *const Percentage, + comptime R: type, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) R, + ) R { + return op_fn(ctx, this.v, other.v); + } + + pub fn tryOp( + this: *const Percentage, + other: *const Percentage, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) f32, + ) ?Percentage { + return Percentage{ .v = op_fn(ctx, this.v, other.v) }; + } +}; + +fn needsDeepclone(comptime D: type) bool { + return switch (D) { + css.css_values.angle.Angle => false, + css.css_values.length.LengthValue => false, + else => @compileError("Can't tell if " ++ @typeName(D) ++ " needs deepclone, please add it to this switch statement."), + }; +} + +pub fn DimensionPercentage(comptime D: type) type { + const needs_deepclone = needsDeepclone(D); + return union(enum) { + dimension: D, + percentage: Percentage, + calc: *Calc(DimensionPercentage(D)), + + const This = @This(); + + pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) This { + return switch (this.*) { + .dimension => |d| if (comptime needs_deepclone) .{ .dimension = d.deepClone(allocator) } else this.*, + .percentage => return this.*, + .calc => |calc| .{ .calc = bun.create(allocator, Calc(DimensionPercentage(D)), calc.deepClone(allocator)) }, + }; + } + + pub fn deinit(this: *const @This(), allocator: std.mem.Allocator) void { + return switch (this.*) { + .dimension => |d| if (comptime @hasDecl(D, "deinit")) d.deinit(allocator), + .percentage => {}, + .calc => |calc| calc.deinit(allocator), + }; + } + + pub fn parse(input: *css.Parser) Result(@This()) { + if (input.tryParse(Calc(This).parse, .{}).asValue()) |calc_value| { + if (calc_value == .value) return .{ .result = calc_value.value.* }; + return .{ .result = .{ + .calc = bun.create(input.allocator(), Calc(DimensionPercentage(D)), calc_value), + } }; + } + + if (input.tryParse(D.parse, .{}).asValue()) |length| { + return .{ .result = .{ .dimension = length } }; + } + + if (input.tryParse(Percentage.parse, .{}).asValue()) |percentage| { + return .{ .result = .{ .percentage = percentage } }; + } + + return .{ .err = input.newErrorForNextToken() }; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + return switch (this.*) { + .dimension => |*length| length.toCss(W, dest), + .percentage => |*per| per.toCss(W, dest), + .calc => |calc| calc.toCss(W, dest), + }; + } + + pub fn zero() This { + return .{ + .percentage = .{ + .value = switch (D) { + f32 => 0.0, + else => @compileError("TODO implement .zero() for " + @typeName(D)), + }, + }, + }; + } + + pub fn isZero(this: *const This) bool { + return switch (this.*) { + .dimension => |*d| switch (D) { + f32 => d == 0.0, + else => @compileError("TODO implement .isZero() for " + @typeName(D)), + }, + .percentage => |*p| p.isZero(), + else => false, + }; + } + + fn mulValueF32(lhs: D, allocator: std.mem.Allocator, rhs: f32) D { + return switch (D) { + f32 => lhs * rhs, + else => lhs.mulF32(allocator, rhs), + }; + } + + pub fn mulF32(this: This, allocator: std.mem.Allocator, other: f32) This { + return switch (this) { + .dimension => |d| .{ .dimension = mulValueF32(d, allocator, other) }, + .percentage => |p| .{ .percentage = p.mulF32(allocator, other) }, + .calc => |c| .{ .calc = bun.create(allocator, Calc(DimensionPercentage(D)), c.mulF32(allocator, other)) }, + }; + } + + pub fn add(this: This, allocator: std.mem.Allocator, other: This) This { + _ = this; // autofix + _ = allocator; // autofix + _ = other; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn partialCmp(this: *const This, other: *const This) ?std.math.Order { + _ = this; // autofix + _ = other; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn trySign(this: *const This) ?f32 { + return switch (this.*) { + .dimension => |d| d.trySign(), + .percentage => |p| p.trySign(), + .calc => |c| c.trySign(), + }; + } + + pub fn tryFromAngle(angle: css.css_values.angle.Angle) ?This { + return DimensionPercentage(D){ + .dimension = D.tryFromAngle(angle) orelse return null, + }; + } + + pub fn tryMap(this: *const This, comptime mapfn: *const fn (f32) f32) ?This { + return switch (this.*) { + .dimension => |vv| if (css.generic.tryMap(D, &vv, mapfn)) |v| .{ .dimension = v } else null, + else => null, + }; + } + + pub fn tryOp( + this: *const This, + other: *const This, + ctx: anytype, + comptime op_fn: *const fn (@TypeOf(ctx), a: f32, b: f32) f32, + ) ?This { + if (this.* == .dimension and other.* == .dimension) return .{ .dimension = css.generic.tryOp(D, &this.dimension, &other.dimension, ctx, op_fn) orelse return null }; + if (this.* == .percentage and other.* == .percentage) return .{ .percentage = Percentage{ .v = op_fn(ctx, this.percentage.v, other.percentage.v) } }; + return null; + } + }; +} + +/// Either a `` or ``. +pub const NumberOrPercentage = union(enum) { + /// A number. + number: CSSNumber, + /// A percentage. + percentage: Percentage, + + // TODO: implement this + // pub usingnamespace css.DeriveParse(@This()); + // pub usingnamespace css.DeriveToCss(@This()); + + pub fn parse(input: *css.Parser) Result(NumberOrPercentage) { + _ = input; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn toCss(this: *const NumberOrPercentage, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + _ = this; // autofix + _ = dest; // autofix + @panic(css.todo_stuff.depth); + } + + pub fn intoF32(this: *const @This()) f32 { + return switch (this.*) { + .number => this.number, + .percentage => this.percentage.v(), + }; + } +}; diff --git a/src/css/values/position.zig b/src/css/values/position.zig new file mode 100644 index 0000000000000..9a0e1058d2c6d --- /dev/null +++ b/src/css/values/position.zig @@ -0,0 +1,372 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const DimensionPercentage = css.css_values.percentage.DimensionPercentage; +const LengthPercentage = css.css_values.length.LengthPercentage; + +/// A CSS `` value, +/// as used in the `background-position` property, gradients, masks, etc. +pub const Position = struct { + /// The x-position. + x: HorizontalPosition, + /// The y-position. + y: VerticalPosition, + + /// Returns whether both the x and y positions are centered. + pub fn isCenter(this: *const @This()) bool { + this.x.isCenter() and this.y.isCenter(); + } + + pub fn center() Position { + return .{ .x = .center, .y = .center }; + } + + pub fn parse(input: *css.Parser) Result(Position) { + // Try parsing a horizontal position first + if (input.tryParse(HorizontalPosition.parse, .{}).asValue()) |horizontal_pos| { + switch (horizontal_pos) { + .center => { + // Try parsing a vertical position next + if (input.tryParse(VerticalPosition.parse, .{}).asValue()) |y| { + return .{ .result = Position{ + .x = .center, + .y = y, + } }; + } + + // If it didn't work, assume the first actually represents a y position, + // and the next is an x position. e.g. `center left` rather than `left center`. + const x = input.tryParse(HorizontalPosition.parse, .{}).unwrapOr(HorizontalPosition.center); + const y = VerticalPosition.center; + return .{ .result = Position{ .x = x, .y = y } }; + }, + .length => |*x| { + // If we got a length as the first component, then the second must + // be a keyword or length (not a side offset). + if (input.tryParse(VerticalPositionKeyword.parse, .{}).asValue()) |y_keyword| { + const y = VerticalPosition{ .side = .{ + .side = y_keyword, + .offset = null, + } }; + return .{ .result = Position{ .x = .{ .length = x.* }, .y = y } }; + } + if (input.tryParse(LengthPercentage.parse, .{}).asValue()) |y_lp| { + const y = VerticalPosition{ .length = y_lp }; + return .{ .result = Position{ .x = .{ .length = x.* }, .y = y } }; + } + const y = VerticalPosition.center; + _ = input.tryParse(css.Parser.expectIdentMatching, .{"center"}); + return .{ .result = Position{ .x = .{ .length = x.* }, .y = y } }; + }, + .side => |*side| { + const x_keyword = side.side; + const lp = side.offset; + + // If we got a horizontal side keyword (and optional offset), expect another for the vertical side. + // e.g. `left center` or `left 20px center` + if (input.tryParse(css.Parser.expectIdentMatching, .{"center"}).isOk()) { + const x = HorizontalPosition{ .side = .{ + .side = x_keyword, + .offset = lp, + } }; + const y = VerticalPosition.center; + return .{ .result = Position{ .x = x, .y = y } }; + } + + // e.g. `left top`, `left top 20px`, `left 20px top`, or `left 20px top 20px` + if (input.tryParse(VerticalPositionKeyword.parse, .{}).asValue()) |y_keyword| { + const y_lp = switch (input.tryParse(LengthPercentage.parse, .{})) { + .result => |vv| vv, + .err => null, + }; + const x = HorizontalPosition{ .side = .{ + .side = x_keyword, + .offset = lp, + } }; + const y = VerticalPosition{ .side = .{ + .side = y_keyword, + .offset = y_lp, + } }; + return .{ .result = Position{ .x = x, .y = y } }; + } + + // If we didn't get a vertical side keyword (e.g. `left 20px`), then apply the offset to the vertical side. + const x = HorizontalPosition{ .side = .{ + .side = x_keyword, + .offset = null, + } }; + const y = if (lp) |lp_val| + VerticalPosition{ .length = lp_val } + else + VerticalPosition.center; + return .{ .result = Position{ .x = x, .y = y } }; + }, + } + } + + // If the horizontal position didn't parse, then it must be out of order. Try vertical position keyword. + const y_keyword = switch (VerticalPositionKeyword.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const lp_and_x_pos = input.tryParse(struct { + fn parse(i: *css.Parser) Result(struct { ?LengthPercentage, HorizontalPosition }) { + const y_lp = i.tryParse(LengthPercentage.parse, .{}).asValue(); + if (i.tryParse(HorizontalPositionKeyword.parse, .{}).asValue()) |x_keyword| { + const x_lp = i.tryParse(LengthPercentage.parse, .{}).asValue(); + const x_pos = HorizontalPosition{ .side = .{ + .side = x_keyword, + .offset = x_lp, + } }; + return .{ .result = .{ y_lp, x_pos } }; + } + if (i.expectIdentMatching("center").asErr()) |e| return .{ .err = e }; + const x_pos = HorizontalPosition.center; + return .{ .result = .{ y_lp, x_pos } }; + } + }.parse, .{}); + + if (lp_and_x_pos.asValue()) |tuple| { + const y_lp = tuple[0]; + const x = tuple[1]; + const y = VerticalPosition{ .side = .{ + .side = y_keyword, + .offset = y_lp, + } }; + return .{ .result = Position{ .x = x, .y = y } }; + } + + const x = HorizontalPosition.center; + const y = VerticalPosition{ .side = .{ + .side = y_keyword, + .offset = null, + } }; + return .{ .result = Position{ .x = x, .y = y } }; + } + + pub fn toCss(this: *const Position, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + if (this.x == .side and this.y == .length and this.x.side != .left) { + try this.x.toCss(W, dest); + try dest.writeStr(" top "); + try this.y.length.toCss(W, dest); + } else if (this.x == .side and this.x.side != .left and this.y.isCenter()) { + // If there is a side keyword with an offset, "center" must be a keyword not a percentage. + try this.x.toCss(W, dest); + try dest.writeStr(" center"); + } else if (this.x == .length and this.y == .side and this.y.side != .top) { + try dest.writeStr("left "); + try this.x.length.toCss(W, dest); + try dest.writeStr(" "); + try this.y.toCss(W, dest); + } else if (this.x.isCenter() and this.y.isCenter()) { + // `center center` => 50% + try this.x.toCss(W, dest); + } else if (this.x == .length and this.y.isCenter()) { + // `center` is assumed if omitted. + try this.x.length.toCss(W, dest); + } else if (this.x == .side and this.x.side.offset == null and this.y.isCenter()) { + const p: LengthPercentage = this.x.side.side.intoLengthPercentage(); + try p.toCss(W, dest); + } else if (this.y == .side and this.y.side.offset == null and this.x.isCenter()) { + this.y.toCss(W, dest); + } else if (this.x == .side and this.x.side.offset == null and this.y == .side and this.y.side.offset == null) { + const x: LengthPercentage = this.x.side.side.intoLengthPercentage(); + const y: LengthPercentage = this.y.side.side.intoLengthPercentage(); + try x.toCss(W, dest); + try dest.writeStr(" "); + try y.toCss(W, dest); + } else { + const zero = LengthPercentage.zero(); + const fifty = LengthPercentage{ .percentage = .{ .v = 0.5 } }; + const x_len: ?*const LengthPercentage = x_len: { + switch (this.x) { + .side => |side| { + if (side.side == .left) { + if (side.offset) |*offset| { + if (offset.isZero()) { + break :x_len &zero; + } else { + break :x_len offset; + } + } else { + break :x_len &zero; + } + } + }, + .length => |len| { + if (len.isZero()) { + break :x_len &zero; + } + }, + .center => break :x_len &fifty, + else => {}, + } + break :x_len null; + }; + + const y_len: ?*const LengthPercentage = y_len: { + switch (this.y) { + .side => |side| { + if (side.side == .left) { + if (side.offset) |*offset| { + if (offset.isZero()) { + break :y_len &zero; + } else { + break :y_len offset; + } + } else { + break :y_len &zero; + } + } + }, + .length => |len| { + if (len.isZero()) { + break :y_len &zero; + } + }, + .center => break :y_len &fifty, + else => {}, + } + break :y_len null; + }; + + if (x_len != null and y_len != null) { + try x_len.?.toCss(W, dest); + try dest.writeStr(" "); + try y_len.?.toCss(W, dest); + } else { + try this.x.toCss(W, dest); + try dest.writeStr(" "); + try this.y.toCss(W, dest); + } + } + } +}; + +pub fn PositionComponent(comptime S: type) type { + return union(enum) { + /// The `center` keyword. + center, + /// A length or percentage from the top-left corner of the box. + length: LengthPercentage, + /// A side keyword with an optional offset. + side: struct { + /// A side keyword. + side: S, + /// Offset from the side. + offset: ?LengthPercentage, + }, + + const This = @This(); + + pub fn parse(input: *css.Parser) Result(This) { + if (input.tryParse( + struct { + fn parse(i: *css.Parser) Result(void) { + if (i.expectIdentMatching("center").asErr()) |e| return .{ .err = e }; + } + }.parse, + .{}, + ).isOk()) { + return .{ .result = .center }; + } + + if (input.tryParse(LengthPercentage.parse, .{}).asValue()) |lp| { + return .{ .result = .{ .length = lp } }; + } + + const side = switch (S.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const offset = input.tryParse(LengthPercentage.parse, .{}).asValue(); + return .{ .result = .{ .side = .{ .side = side, .offset = offset } } }; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + switch (this.*) { + .center => { + if (dest.minify) { + try dest.writeStr("50%"); + } else { + try dest.writeStr("center"); + } + }, + .length => |*lp| try lp.toCss(W, dest), + .side => |*s| { + try s.side.toCss(W, dest); + if (s.offset) |lp| { + try dest.writeStr(" "); + try lp.toCss(W, dest); + } + }, + } + } + + pub fn isCenter(this: *const This) bool { + switch (this.*) { + .center => return true, + .length => |*l| { + if (l == .percentage) return l.percentage.v == 0.5; + }, + else => {}, + } + return false; + } + }; +} + +pub const HorizontalPositionKeyword = enum { + /// The `left` keyword. + left, + /// The `right` keyword. + right, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + 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 intoLengthPercentage(this: *const @This()) LengthPercentage { + return switch (this.*) { + .left => LengthPercentage.zero(), + .right => .{ .percentage = .{ .v = 1.0 } }, + }; + } +}; + +pub const VerticalPositionKeyword = enum { + /// The `top` keyword. + top, + /// The `bottom` keyword. + bottom, + + pub fn asStr(this: *const @This()) []const u8 { + return css.enum_property_util.asStr(@This(), this); + } + + pub fn parse(input: *css.Parser) Result(@This()) { + return css.enum_property_util.parse(@This(), input); + } + + 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 const HorizontalPosition = PositionComponent(HorizontalPositionKeyword); +pub const VerticalPosition = PositionComponent(VerticalPositionKeyword); diff --git a/src/css/values/ratio.zig b/src/css/values/ratio.zig new file mode 100644 index 0000000000000..492eb641ea417 --- /dev/null +++ b/src/css/values/ratio.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const DimensionPercentage = css.css_values.percentage.DimensionPercentage; +const LengthPercentage = css.css_values.length.LengthPercentage; +const Length = css.css_values.length.Length; +const Percentage = css.css_values.percentage.Percentage; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const Url = css.css_values.url.Url; +const CSSInteger = css.css_values.number.CSSInteger; +const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +const Angle = css.css_values.angle.Angle; +const Time = css.css_values.time.Time; +const Resolution = css.css_values.resolution.Resolution; +const CustomIdent = css.css_values.ident.CustomIdent; +const CustomIdentFns = css.css_values.ident.CustomIdentFns; +const Ident = css.css_values.ident.Ident; + +/// A CSS [``](https://www.w3.org/TR/css-values-4/#ratios) value, +/// representing the ratio of two numeric values. +pub const Ratio = struct { + numerator: CSSNumber, + denominator: CSSNumber, + + pub fn parse(input: *css.Parser) Result(Ratio) { + const first = switch (CSSNumberFns.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const second = if (input.tryParse(css.Parser.expectDelim, .{'/'}).isOk()) switch (CSSNumberFns.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } else 1.0; + + return .{ .result = Ratio{ .numerator = first, .denominator = second } }; + } + + /// Parses a ratio where both operands are required. + pub fn parseRequired(input: *css.Parser) Result(Ratio) { + const first = switch (CSSNumberFns.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (input.expectDelim('/').asErr()) |e| return .{ .err = e }; + const second = switch (CSSNumberFns.parse(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + return .{ .result = Ratio{ .numerator = first, .denominator = second } }; + } + + pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + try CSSNumberFns.toCss(&this.numerator, W, dest); + if (this.denominator != 1.0) { + try dest.delim('/', true); + try CSSNumberFns.toCss(&this.denominator, W, dest); + } + } + + pub fn addF32(this: Ratio, _: std.mem.Allocator, other: f32) Ratio { + return .{ .numerator = this.numerator + other, .denominator = this.denominator }; + } +}; diff --git a/src/css/values/rect.zig b/src/css/values/rect.zig new file mode 100644 index 0000000000000..4c81618703592 --- /dev/null +++ b/src/css/values/rect.zig @@ -0,0 +1,98 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const DimensionPercentage = css.css_values.percentage.DimensionPercentage; +const LengthPercentage = css.css_values.length.LengthPercentage; +const Length = css.css_values.length.Length; +const Percentage = css.css_values.percentage.Percentage; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const Url = css.css_values.url.Url; +const CSSInteger = css.css_values.number.CSSInteger; +const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +const Angle = css.css_values.angle.Angle; +const Time = css.css_values.time.Time; +const Resolution = css.css_values.resolution.Resolution; +const CustomIdent = css.css_values.ident.CustomIdent; +const CustomIdentFns = css.css_values.ident.CustomIdentFns; +const Ident = css.css_values.ident.Ident; + +/// A generic value that represents a value for four sides of a box, +/// e.g. border-width, margin, padding, etc. +/// +/// When serialized, as few components as possible are written when +/// there are duplicate values. +pub fn Rect(comptime T: type) type { + return struct { + /// The top component. + top: T, + /// The right component. + right: T, + /// The bottom component. + bottom: T, + /// The left component. + left: T, + + const This = @This(); + + pub fn parse(input: *css.Parser) Result(This) { + return This.parseWith(input, valParse); + } + + pub fn parseWith(input: *css.Parser, comptime parse_fn: *const fn (*css.Parser) Result(T)) Result(This) { + const first = switch (parse_fn(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const second = switch (input.tryParse(parse_fn, .{})) { + .result => |v| v, + // + .err => return .{ .result = This{ .top = first, .right = first, .bottom = first, .left = first } }, + }; + const third = switch (input.tryParse(parse_fn, .{})) { + .result => |v| v, + // + .err => return This{ .top = first, .right = second, .bottom = first, .left = second }, + }; + const fourth = switch (input.tryParse(parse_fn, .{})) { + .result => |v| v, + // + .err => return This{ .top = first, .right = second, .bottom = third, .left = second }, + }; + // + return .{ .result = This{ .top = first, .right = second, .bottom = third, .left = fourth } }; + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + try css.generic.toCss(T, &this.top, W, dest); + const same_vertical = css.generic.eql(T, &this.top, &this.bottom); + const same_horizontal = css.generic.eql(T, &this.right, &this.left); + if (same_vertical and same_horizontal and css.generic.eql(T, &this.top, &this.right)) { + return; + } + try dest.writeStr(" "); + try css.generic.toCss(T, &this.right, W, dest); + if (same_vertical and same_horizontal) { + return; + } + try dest.writeStr(" "); + try css.generic.toCss(T, &this.bottom, W, dest); + if (same_horizontal) { + return; + } + try dest.writeStr(" "); + try css.generic.toCss(T, &this.left, W, dest); + } + + pub fn valParse(i: *css.Parser) Result(T) { + return css.generic.parse(T, i); + } + }; +} diff --git a/src/css/values/resolution.zig b/src/css/values/resolution.zig new file mode 100644 index 0000000000000..8201eb49b2619 --- /dev/null +++ b/src/css/values/resolution.zig @@ -0,0 +1,98 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const DimensionPercentage = css.css_values.percentage.DimensionPercentage; +const LengthPercentage = css.css_values.length.LengthPercentage; +const Length = css.css_values.length.Length; +const Percentage = css.css_values.percentage.Percentage; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const CSSInteger = css.css_values.number.CSSInteger; +const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +const Angle = css.css_values.angle.Angle; +const Time = css.css_values.time.Time; +const CustomIdent = css.css_values.ident.CustomIdent; +const CustomIdentFns = css.css_values.ident.CustomIdentFns; +const Ident = css.css_values.ident.Ident; + +/// A CSS `` value. +pub const Resolution = union(enum) { + /// A resolution in dots per inch. + dpi: CSSNumber, + /// A resolution in dots per centimeter. + dpcm: CSSNumber, + /// A resolution in dots per px. + dppx: CSSNumber, + + // ~toCssImpl + const This = @This(); + + pub fn parse(input: *css.Parser) Result(Resolution) { + // TODO: calc? + const location = input.currentSourceLocation(); + const tok = switch (input.next()) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + if (tok.* == .dimension) { + const value = tok.dimension.num.value; + const unit = tok.dimension.unit; + // css.todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "dpi")) return .{ .result = .{ .dpi = value } }; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "dpcm")) return .{ .result = .{ .dpcm = value } }; + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "dppx") or bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "x")) return .{ .result = .{ .dppx = value } }; + return .{ .err = location.newUnexpectedTokenError(.{ .ident = unit }) }; + } + return .{ .err = location.newUnexpectedTokenError(tok.*) }; + } + + pub fn tryFromToken(token: *const css.Token) css.Maybe(Resolution, void) { + switch (token.*) { + .dimension => |dim| { + const value = dim.num.value; + const unit = dim.unit; + // todo_stuff.match_ignore_ascii_case + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "dpi")) { + return .{ .result = .{ .dpi = value } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "dpcm")) { + return .{ .result = .{ .dpcm = value } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "dppx") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(unit, "x")) + { + return .{ .result = .{ .dppx = value } }; + } else { + return .{ .err = {} }; + } + }, + else => return .{ .err = {} }, + } + } + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + const value, const unit = switch (this.*) { + .dpi => |dpi| .{ dpi, "dpi" }, + .dpcm => |dpcm| .{ dpcm, "dpcm" }, + .dppx => |dppx| if (dest.targets.isCompatible(.x_resolution_unit)) + .{ dppx, "x" } + else + .{ dppx, "dppx" }, + }; + + return try css.serializer.serializeDimension(value, unit, W, dest); + } + + pub fn addF32(this: This, _: std.mem.Allocator, other: f32) Resolution { + return switch (this) { + .dpi => |dpi| .{ .dpi = dpi + other }, + .dpcm => |dpcm| .{ .dpcm = dpcm + other }, + .dppx => |dppx| .{ .dppx = dppx + other }, + }; + } +}; diff --git a/src/css/values/size.zig b/src/css/values/size.zig new file mode 100644 index 0000000000000..98f5e7f3a4c66 --- /dev/null +++ b/src/css/values/size.zig @@ -0,0 +1,84 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const DimensionPercentage = css.css_values.percentage.DimensionPercentage; +const LengthPercentage = css.css_values.length.LengthPercentage; +const Length = css.css_values.length.Length; +const Percentage = css.css_values.percentage.Percentage; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const Url = css.css_values.url.Url; +const CSSInteger = css.css_values.number.CSSInteger; +const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +const Angle = css.css_values.angle.Angle; +const Time = css.css_values.time.Time; +const Resolution = css.css_values.resolution.Resolution; +const CustomIdent = css.css_values.ident.CustomIdent; +const CustomIdentFns = css.css_values.ident.CustomIdentFns; +const Ident = css.css_values.ident.Ident; + +/// A generic value that represents a value with two components, e.g. a border radius. +/// +/// When serialized, only a single component will be written if both are equal. +pub fn Size2D(comptime T: type) type { + return struct { + a: T, + b: T, + + fn parseVal(input: *css.Parser) Result(T) { + return switch (T) { + f32 => return CSSNumberFns.parse(input), + LengthPercentage => return LengthPercentage.parse(input), + else => T.parse(input), + }; + } + + pub fn parse(input: *css.Parser) Result(Size2D(T)) { + const first = switch (parseVal(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + const second = input.tryParse(parseVal, .{}).unwrapOrNoOptmizations(first); + return .{ .result = Size2D(T){ + .a = first, + .b = second, + } }; + } + + pub fn toCss(this: *const Size2D(T), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + try valToCss(&this.a, W, dest); + if (!valEql(&this.b, &this.a)) { + try dest.writeStr(" "); + try valToCss(&this.b, W, dest); + } + } + + pub fn valToCss(val: *const T, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + return switch (T) { + f32 => CSSNumberFns.toCss(val, W, dest), + else => val.toCss(W, dest), + }; + } + + pub inline fn valEql(lhs: *const T, rhs: *const T) bool { + return switch (T) { + f32 => lhs.* == rhs.*, + else => lhs.eql(rhs), + }; + } + + pub inline fn eql(lhs: *const @This(), rhs: *const @This()) bool { + return switch (T) { + f32 => lhs.a == rhs.b, + else => lhs.a.eql(&rhs.b), + }; + } + }; +} diff --git a/src/css/values/syntax.zig b/src/css/values/syntax.zig new file mode 100644 index 0000000000000..f01c0fbe51909 --- /dev/null +++ b/src/css/values/syntax.zig @@ -0,0 +1,505 @@ +const std = @import("std"); +const bun = @import("root").bun; +pub const css = @import("../css_parser.zig"); +const Result = css.Result; +const ArrayList = std.ArrayListUnmanaged; +const Printer = css.Printer; +const PrintErr = css.PrintErr; +const CSSNumber = css.css_values.number.CSSNumber; +const CSSNumberFns = css.css_values.number.CSSNumberFns; +const Calc = css.css_values.calc.Calc; +const DimensionPercentage = css.css_values.percentage.DimensionPercentage; +const LengthPercentage = css.css_values.length.LengthPercentage; +const Length = css.css_values.length.Length; +const Percentage = css.css_values.percentage.Percentage; +const CssColor = css.css_values.color.CssColor; +const Image = css.css_values.image.Image; +const Url = css.css_values.url.Url; +const CSSInteger = css.css_values.number.CSSInteger; +const CSSIntegerFns = css.css_values.number.CSSIntegerFns; +const Angle = css.css_values.angle.Angle; +const Time = css.css_values.time.Time; +const Resolution = css.css_values.resolution.Resolution; +const CustomIdent = css.css_values.ident.CustomIdent; +const CustomIdentFns = css.css_values.ident.CustomIdentFns; +const Ident = css.css_values.ident.Ident; + +// https://drafts.csswg.org/css-syntax-3/#whitespace +const SPACE_CHARACTERS: []const u8 = &.{ 0x20, 0x09 }; + +/// A CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings) +/// used to define the grammar for a registered custom property. +pub const SyntaxString = union(enum) { + /// A list of syntax components. + components: ArrayList(SyntaxComponent), + /// The universal syntax definition. + universal, + + const This = @This(); + + pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { + try dest.writeChar('"'); + switch (this.*) { + .universal => try dest.writeChar('*'), + .components => |*components| { + var first = true; + for (components.items) |*component| { + if (first) { + first = false; + } else { + try dest.delim('|', true); + } + + try component.toCss(W, dest); + } + }, + } + + return dest.writeChar('"'); + } + + pub fn parse(input: *css.Parser) Result(SyntaxString) { + const string = switch (input.expectString()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + const result = SyntaxString.parseString(input.allocator(), string); + if (result.isErr()) return .{ .err = input.newCustomError(css.ParserError.invalid_value) }; + return .{ .result = result.result }; + } + + /// Parses a syntax string. + pub fn parseString(allocator: std.mem.Allocator, input: []const u8) css.Maybe(SyntaxString, void) { + // https://drafts.css-houdini.org/css-properties-values-api/#parsing-syntax + var trimmed_input = std.mem.trimLeft(u8, input, SPACE_CHARACTERS); + if (trimmed_input.len == 0) { + return .{ .err = {} }; + } + + if (bun.strings.eqlComptime(trimmed_input, "*")) { + return .{ .result = SyntaxString.universal }; + } + + var components = ArrayList(SyntaxComponent){}; + + // PERF(alloc): count first? + while (true) { + const component = switch (SyntaxComponent.parseString(trimmed_input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + components.append( + allocator, + component, + ) catch bun.outOfMemory(); + + trimmed_input = std.mem.trimLeft(u8, trimmed_input, SPACE_CHARACTERS); + if (trimmed_input.len == 0) { + break; + } + + if (bun.strings.startsWithChar(trimmed_input, '|')) { + trimmed_input = trimmed_input[1..]; + continue; + } + + return .{ .err = {} }; + } + + return .{ .result = SyntaxString{ .components = components } }; + } + + /// Parses a value according to the syntax grammar. + pub fn parseValue(this: *const SyntaxString, input: *css.Parser) Result(ParsedComponent) { + switch (this.*) { + .universal => return .{ .result = ParsedComponent{ + .token_list = switch (css.css_properties.custom.TokenList.parse( + input, + &css.ParserOptions.default(input.allocator(), null), + 0, + )) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }, + } }, + .components => |components| { + // Loop through each component, and return the first one that parses successfully. + for (components.items) |component| { + const state = input.state(); + // PERF: deinit this on error + var parsed = ArrayList(ParsedComponent){}; + + while (true) { + const value_result = input.tryParse(struct { + fn parse( + i: *css.Parser, + comp: SyntaxComponent, + ) Result(ParsedComponent) { + const value = switch (comp.kind) { + .length => ParsedComponent{ .length = switch (Length.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .number => ParsedComponent{ .number = switch (CSSNumberFns.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .percentage => ParsedComponent{ .percentage = switch (Percentage.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .length_percentage => ParsedComponent{ .length_percentage = switch (LengthPercentage.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .color => ParsedComponent{ .color = switch (CssColor.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .image => ParsedComponent{ .image = switch (Image.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .url => ParsedComponent{ .url = switch (Url.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .integer => ParsedComponent{ .integer = switch (CSSIntegerFns.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .angle => ParsedComponent{ .angle = switch (Angle.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .time => ParsedComponent{ .time = switch (Time.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .resolution => ParsedComponent{ .resolution = switch (Resolution.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .transform_function => ParsedComponent{ .transform_function = switch (css.css_properties.transform.Transform.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .transform_list => ParsedComponent{ .transform_list = switch (css.css_properties.transform.TransformList.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .custom_ident => ParsedComponent{ .custom_ident = switch (CustomIdentFns.parse(i)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } }, + .literal => |value| blk: { + const location = i.currentSourceLocation(); + const ident = switch (i.expectIdent()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (!bun.strings.eql(ident, value)) { + return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) }; + } + break :blk ParsedComponent{ .literal = .{ .v = ident } }; + }, + }; + return .{ .result = value }; + } + }.parse, .{component}); + + if (value_result.asValue()) |value| { + switch (component.multiplier) { + .none => return .{ .result = value }, + .space => { + parsed.append(input.allocator(), value) catch bun.outOfMemory(); + if (input.isExhausted()) { + return .{ .result = ParsedComponent{ .repeated = .{ + .components = parsed, + .multiplier = component.multiplier, + } } }; + } + }, + .comma => { + parsed.append(input.allocator(), value) catch bun.outOfMemory(); + if (input.next().asValue()) |token| { + if (token.* == .comma) continue; + break; + } else { + return .{ .result = ParsedComponent{ .repeated = .{ + .components = parsed, + .multiplier = component.multiplier, + } } }; + } + }, + } + } else { + break; + } + } + + input.reset(&state); + } + + return .{ .err = input.newErrorForNextToken() }; + }, + } + } +}; + +/// A [syntax component](https://drafts.css-houdini.org/css-properties-values-api/#syntax-component) +/// within a [SyntaxString](SyntaxString). +/// +/// A syntax component consists of a component kind an a multiplier, which indicates how the component +/// may repeat during parsing. +pub const SyntaxComponent = struct { + kind: SyntaxComponentKind, + multiplier: Multiplier, + + pub fn parseString(input_: []const u8) css.Maybe(SyntaxComponent, void) { + var input = input_; + const kind = switch (SyntaxComponentKind.parseString(input)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + }; + + // Pre-multiplied types cannot have multipliers. + if (kind == .transform_list) { + return .{ .result = SyntaxComponent{ + .kind = kind, + .multiplier = .none, + } }; + } + + var multiplier: Multiplier = .none; + if (bun.strings.startsWithChar(input, '+')) { + input = input[1..]; + multiplier = .space; + } else if (bun.strings.startsWithChar(input, '#')) { + input = input[1..]; + multiplier = .comma; + } + + return .{ .result = SyntaxComponent{ .kind = kind, .multiplier = multiplier } }; + } + + pub fn toCss(this: *const SyntaxComponent, comptime W: type, dest: *Printer(W)) PrintErr!void { + try this.kind.toCss(W, dest); + return switch (this.multiplier) { + .none => {}, + .comma => dest.writeChar('#'), + .space => dest.writeChar('+'), + }; + } +}; + +/// A [syntax component component name](https://drafts.css-houdini.org/css-properties-values-api/#supported-names). +pub const SyntaxComponentKind = union(enum) { + /// A `` component. + length, + /// A `` component. + number, + /// A `` component. + percentage, + /// A `` component. + length_percentage, + /// A `` component. + color, + /// An `` component. + image, + /// A `` component. + url, + /// An `` component. + integer, + /// An `` component. + angle, + /// A `