diff --git a/bench/bun.lockb b/bench/bun.lockb index 679ce8aba1293..6704d64542576 100755 Binary files a/bench/bun.lockb and b/bench/bun.lockb differ diff --git a/bench/package.json b/bench/package.json index 6ea67f5c993df..11b0ab69bd1af 100644 --- a/bench/package.json +++ b/bench/package.json @@ -7,6 +7,7 @@ "@swc/core": "^1.2.133", "benchmark": "^2.1.4", "braces": "^3.0.2", + "color": "^4.2.3", "esbuild": "^0.14.12", "eventemitter3": "^5.0.0", "execa": "^8.0.1", @@ -14,6 +15,7 @@ "fdir": "^6.1.0", "mitata": "^0.1.6", "string-width": "7.1.0", + "tinycolor2": "^1.6.0", "zx": "^7.2.3" }, "scripts": { diff --git a/bench/snippets/color.mjs b/bench/snippets/color.mjs new file mode 100644 index 0000000000000..7d13177b2b412 --- /dev/null +++ b/bench/snippets/color.mjs @@ -0,0 +1,25 @@ +import Color from "color"; +import tinycolor from "tinycolor2"; +import { bench, run, group } from "./runner.mjs"; + +const inputs = ["#f00", "rgb(255, 0, 0)", "rgba(255, 0, 0, 1)", "hsl(0, 100%, 50%)"]; + +for (const input of inputs) { + group(`${input}`, () => { + 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 9e6b62e3f28c6..4ccfae2715a8e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/api/color.md b/docs/api/color.md new file mode 100644 index 0000000000000..0285d6da8f8e4 --- /dev/null +++ b/docs/api/color.md @@ -0,0 +1,263 @@ +`Bun.color(input, outputFormat?)` leverages Bun's CSS parser to parse, normalize, and convert colors from user input to a variety of output formats, including: + +| Format | Example | +| ------------ | -------------------------------- | +| `"css"` | `"red"` | +| `"ansi"` | `"\x1b[38;2;255;0;0m"` | +| `"ansi-16"` | `"\x1b[38;5;\tm"` | +| `"ansi-256"` | `"\x1b[38;5;196m"` | +| `"ansi-16m"` | `"\x1b[38;2;255;0;0m"` | +| `"number"` | `0x1a2b3c` | +| `"rgb"` | `"rgb(255, 99, 71)"` | +| `"rgba"` | `"rgba(255, 99, 71, 0.5)"` | +| `"hsl"` | `"hsl(120, 50%, 50%)"` | +| `"hex"` | `"#1a2b3c"` | +| `"HEX"` | `"#1A2B3C"` | +| `"{rgb}"` | `{ r: 255, g: 99, b: 71 }` | +| `"{rgba}"` | `{ r: 255, g: 99, b: 71, a: 1 }` | +| `"[rgb]"` | `[ 255, 99, 71 ]` | +| `"[rgba]"` | `[ 255, 99, 71, 255]` | + +There are many different ways to use this API: + +- Validate and normalize colors to persist in a database (`number` is the most database-friendly) +- Convert colors to different formats +- Colorful logging beyond the 16 colors many use today (use `ansi` if you don't want to figure out what the user's terminal supports, otherwise use `ansi-16`, `ansi-256`, or `ansi-16m` for how many colors the terminal supports) +- Format colors for use in CSS injected into HTML +- Get the `r`, `g`, `b`, and `a` color components as JavaScript objects or numbers from a CSS color string + +You can think of this as an alternative to the popular npm packages [`color`](https://github.com/Qix-/color) and [`tinycolor2`](https://github.com/bgrins/TinyColor) except with full support for parsing CSS color strings and zero dependencies built directly into Bun. + +### Flexible input + +You can pass in any of the following: + +- Standard CSS color names like `"red"` +- Numbers like `0xff0000` +- Hex strings like `"#f00"` +- RGB strings like `"rgb(255, 0, 0)"` +- RGBA strings like `"rgba(255, 0, 0, 1)"` +- HSL strings like `"hsl(0, 100%, 50%)"` +- HSLA strings like `"hsla(0, 100%, 50%, 1)"` +- RGB objects like `{ r: 255, g: 0, b: 0 }` +- RGBA objects like `{ r: 255, g: 0, b: 0, a: 1 }` +- RGB arrays like `[255, 0, 0]` +- RGBA arrays like `[255, 0, 0, 255]` +- LAB strings like `"lab(50% 50% 50%)"` +- LABA strings like `"laba(50% 50% 50% 1)"` +- ... anything else that CSS can parse as a single color value + +### Format colors as CSS + +The `"css"` format outputs valid CSS for use in stylesheets, inline styles, CSS variables, css-in-js, etc. It returns the most compact representation of the color as a string. + +```ts +Bun.color("red", "css"); // "red" +Bun.color(0xff0000, "css"); // "#f000" +Bun.color("#f00", "css"); // "red" +Bun.color("#ff0000", "css"); // "red" +Bun.color("rgb(255, 0, 0)", "css"); // "red" +Bun.color("rgba(255, 0, 0, 1)", "css"); // "red" +Bun.color("hsl(0, 100%, 50%)", "css"); // "red" +Bun.color("hsla(0, 100%, 50%, 1)", "css"); // "red" +Bun.color({ r: 255, g: 0, b: 0 }, "css"); // "red" +Bun.color({ r: 255, g: 0, b: 0, a: 1 }, "css"); // "red" +Bun.color([255, 0, 0], "css"); // "red" +Bun.color([255, 0, 0, 255], "css"); // "red" +``` + +If the input is unknown or fails to parse, `Bun.color` returns `null`. + +### Format colors as ANSI (for terminals) + +The `"ansi"` format outputs ANSI escape codes for use in terminals to make text colorful. + +```ts +Bun.color("red", "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color(0xff0000, "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color("#f00", "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color("#ff0000", "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color("rgb(255, 0, 0)", "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color("rgba(255, 0, 0, 1)", "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color("hsl(0, 100%, 50%)", "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color("hsla(0, 100%, 50%, 1)", "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color({ r: 255, g: 0, b: 0 }, "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color({ r: 255, g: 0, b: 0, a: 1 }, "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color([255, 0, 0], "ansi"); // "\u001b[38;2;255;0;0m" +Bun.color([255, 0, 0, 255], "ansi"); // "\u001b[38;2;255;0;0m" +``` + +This gets the color depth of stdout and automatically chooses one of `"ansi-16m"`, `"ansi-256"`, `"ansi-16"` based on the environment variables. If stdout doesn't support any form of ANSI color, it returns an empty string. As with the rest of Bun's color API, if the input is unknown or fails to parse, it returns `null`. + +#### 24-bit ANSI colors (`ansi-16m`) + +The `"ansi-16m"` format outputs 24-bit ANSI colors for use in terminals to make text colorful. 24-bit color means you can display 16 million colors on supported terminals, and requires a modern terminal that supports it. + +This converts the input color to RGBA, and then outputs that as an ANSI color. + +```ts +Bun.color("red", "ansi-16m"); // "\x1b[38;2;255;0;0m" +Bun.color(0xff0000, "ansi-16m"); // "\x1b[38;2;255;0;0m" +Bun.color("#f00", "ansi-16m"); // "\x1b[38;2;255;0;0m" +Bun.color("#ff0000", "ansi-16m"); // "\x1b[38;2;255;0;0m" +``` + +#### 256 ANSI colors (`ansi-256`) + +The `"ansi-256"` format approximates the input color to the nearest of the 256 ANSI colors supported by some terminals. + +```ts +Bun.color("red", "ansi-256"); // "\u001b[38;5;196m" +Bun.color(0xff0000, "ansi-256"); // "\u001b[38;5;196m" +Bun.color("#f00", "ansi-256"); // "\u001b[38;5;196m" +Bun.color("#ff0000", "ansi-256"); // "\u001b[38;5;196m" +``` + +To convert from RGBA to one of the 256 ANSI colors, we ported the algorithm that [`tmux` uses](https://github.com/tmux/tmux/blob/dae2868d1227b95fd076fb4a5efa6256c7245943/colour.c#L44-L55). + +#### 16 ANSI colors (`ansi-16`) + +The `"ansi-16"` format approximates the input color to the nearest of the 16 ANSI colors supported by most terminals. + +```ts +Bun.color("red", "ansi-16"); // "\u001b[38;5;\tm" +Bun.color(0xff0000, "ansi-16"); // "\u001b[38;5;\tm" +Bun.color("#f00", "ansi-16"); // "\u001b[38;5;\tm" +Bun.color("#ff0000", "ansi-16"); // "\u001b[38;5;\tm" +``` + +This works by first converting the input to a 24-bit RGB color space, then to `ansi-256`, and then we convert that to the nearest 16 ANSI color. + +### Format colors as numbers + +The `"number"` format outputs a 24-bit number for use in databases, configuration, or any other use case where a compact representation of the color is desired. + +```ts +Bun.color("red", "number"); // 16711680 +Bun.color(0xff0000, "number"); // 16711680 +Bun.color({ r: 255, g: 0, b: 0 }, "number"); // 16711680 +Bun.color([255, 0, 0], "number"); // 16711680 +Bun.color("rgb(255, 0, 0)", "number"); // 16711680 +Bun.color("rgba(255, 0, 0, 1)", "number"); // 16711680 +Bun.color("hsl(0, 100%, 50%)", "number"); // 16711680 +Bun.color("hsla(0, 100%, 50%, 1)", "number"); // 16711680 +``` + +### Get the red, green, blue, and alpha channels + +You can use the `"{rgba}"`, `"{rgb}"`, `"[rgba]"` and `"[rgb]"` formats to get the red, green, blue, and alpha channels as objects or arrays. + +#### `{rgba}` object + +The `"{rgba}"` format outputs an object with the red, green, blue, and alpha channels. + +```ts +type RGBAObject = { + // 0 - 255 + r: number; + // 0 - 255 + g: number; + // 0 - 255 + b: number; + // 0 - 1 + a: number; +}; +``` + +Example: + +```ts +Bun.color("hsl(0, 0%, 50%)", "{rgba}"); // { r: 128, g: 128, b: 128, a: 1 } +Bun.color("red", "{rgba}"); // { r: 255, g: 0, b: 0, a: 1 } +Bun.color(0xff0000, "{rgba}"); // { r: 255, g: 0, b: 0, a: 1 } +Bun.color({ r: 255, g: 0, b: 0 }, "{rgba}"); // { r: 255, g: 0, b: 0, a: 1 } +Bun.color([255, 0, 0], "{rgba}"); // { r: 255, g: 0, b: 0, a: 1 } +``` + +To behave similarly to CSS, the `a` channel is a decimal number between `0` and `1`. + +The `"{rgb}"` format is similar, but it doesn't include the alpha channel. + +```ts +Bun.color("hsl(0, 0%, 50%)", "{rgb}"); // { r: 128, g: 128, b: 128 } +Bun.color("red", "{rgb}"); // { r: 255, g: 0, b: 0 } +Bun.color(0xff0000, "{rgb}"); // { r: 255, g: 0, b: 0 } +Bun.color({ r: 255, g: 0, b: 0 }, "{rgb}"); // { r: 255, g: 0, b: 0 } +Bun.color([255, 0, 0], "{rgb}"); // { r: 255, g: 0, b: 0 } +``` + +#### `[rgba]` array + +The `"[rgba]"` format outputs an array with the red, green, blue, and alpha channels. + +```ts +// All values are 0 - 255 +type RGBAArray = [number, number, number, number]; +``` + +Example: + +```ts +Bun.color("hsl(0, 0%, 50%)", "[rgba]"); // [128, 128, 128, 255] +Bun.color("red", "[rgba]"); // [255, 0, 0, 255] +Bun.color(0xff0000, "[rgba]"); // [255, 0, 0, 255] +Bun.color({ r: 255, g: 0, b: 0 }, "[rgba]"); // [255, 0, 0, 255] +Bun.color([255, 0, 0], "[rgba]"); // [255, 0, 0, 255] +``` + +Unlike the `"{rgba}"` format, the alpha channel is an integer between `0` and `255`. This is useful for typed arrays where each channel must be the same underlying type. + +The `"[rgb]"` format is similar, but it doesn't include the alpha channel. + +```ts +Bun.color("hsl(0, 0%, 50%)", "[rgb]"); // [128, 128, 128] +Bun.color("red", "[rgb]"); // [255, 0, 0] +Bun.color(0xff0000, "[rgb]"); // [255, 0, 0] +Bun.color({ r: 255, g: 0, b: 0 }, "[rgb]"); // [255, 0, 0] +Bun.color([255, 0, 0], "[rgb]"); // [255, 0, 0] +``` + +### Format colors as hex strings + +The `"hex"` format outputs a lowercase hex string for use in CSS or other contexts. + +```ts +Bun.color("hsl(0, 0%, 50%)", "hex"); // "#808080" +Bun.color("red", "hex"); // "#ff0000" +Bun.color(0xff0000, "hex"); // "#ff0000" +Bun.color({ r: 255, g: 0, b: 0 }, "hex"); // "#ff0000" +Bun.color([255, 0, 0], "hex"); // "#ff0000" +``` + +The `"HEX"` format is similar, but it outputs a hex string with uppercase letters instead of lowercase letters. + +```ts +Bun.color("hsl(0, 0%, 50%)", "HEX"); // "#808080" +Bun.color("red", "HEX"); // "#FF0000" +Bun.color(0xff0000, "HEX"); // "#FF0000" +Bun.color({ r: 255, g: 0, b: 0 }, "HEX"); // "#FF0000" +Bun.color([255, 0, 0], "HEX"); // "#FF0000" +``` + +### Bundle-time client-side color formatting + +Like many of Bun's APIs, you can use macros to invoke `Bun.color` at bundle-time for use in client-side JavaScript builds: + +```ts#client-side.ts +import { color } from "bun" with { type: "macro" }; + +console.log(color("#f00", "css")); +``` + +Then, build the client-side code: + +```sh +bun build ./client-side.ts +``` + +This will output the following to `client-side.js`: + +```js +// client-side.ts +console.log("red"); +``` diff --git a/docs/nav.ts b/docs/nav.ts index 2b9202b556077..fffad700e10a2 100644 --- a/docs/nav.ts +++ b/docs/nav.ts @@ -374,6 +374,10 @@ export default { description: `Bun's native Semver implementation is 20x faster than the popular \`node-semver\` package.`, }), // "`Semver`"), + page("api/color", "Color", { + description: `Bun's color function leverages Bun's CSS parser for parsing, normalizing, and converting colors from user input to a variety of output formats.`, + }), // "`Color`"), + // divider("Dev Server"), // page("bun-dev", "Vanilla"), // page("dev/css", "CSS"), diff --git a/package.json b/package.json index 89d9f0f813da1..cc8766480beba 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,10 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "source-map-js": "^1.2.0", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "caniuse-lite": "^1.0.30001620", + "autoprefixer": "^10.4.19", + "@mdn/browser-compat-data": "~5.5.28" }, "resolutions": { "bun-types": "workspace:packages/bun-types" diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 5b5a316fa4e88..923e76471ce91 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3029,6 +3029,87 @@ declare module "bun" { type StringLike = string | { toString(): string }; + type ColorInput = + | { r: number; g: number; b: number; a?: number } + | [number, number, number] + | [number, number, number, number] + | Uint8Array + | Uint8ClampedArray + | Float32Array + | Float64Array + | string + | number + | { toString(): string }; + + function color( + input: ColorInput, + outputFormat?: /** + * True color ANSI color string, for use in terminals + * @example \x1b[38;2;100;200;200m + */ + | "ansi" + /** + * 256 color ANSI color string, for use in terminals which don't support true color + * + * Tries to match closest 24-bit color to 256 color palette + */ + | "ansi256" + /** + * Lowercase hex color string without alpha + * @example #aabb11 + */ + | "hex" + /** + * RGB color string without alpha + * rgb(100, 200, 200) + */ + | "rgb" + /** + * RGB color string with alpha + * rgba(100, 200, 200, 0.5) + */ + | "rgba" + | "hsl" + | "lab" + | "css" + | "lab" + | "HEX", + ): string | null; + + function color( + input: ColorInput, + /** + * An array of numbers representing the RGB color + * @example [100, 200, 200] + */ + outputFormat: "[rgb]", + ): [number, number, number] | null; + function color( + input: ColorInput, + /** + * An array of numbers representing the RGBA color + * @example [100, 200, 200, 255] + */ + outputFormat: "[rgba]", + ): [number, number, number, number] | null; + function color( + input: ColorInput, + /** + * An object representing the RGB color + * @example { r: 100, g: 200, b: 200 } + */ + outputFormat: "{rgb}", + ): { r: number; g: number; b: number } | null; + function color( + input: ColorInput, + /** + * An object representing the RGBA color + * @example { r: 100, g: 200, b: 200, a: 0.5 } + */ + outputFormat: "{rgba}", + ): { r: number; g: number; b: number; a: number } | null; + function color(input: ColorInput, outputFormat: "number"): number | null; + interface Semver { /** * Test if the version satisfies the range. Stringifies both arguments. Returns `true` or `false`. diff --git a/src/bitflags.zig b/src/bitflags.zig new file mode 100644 index 0000000000000..01bf9e08e1ab2 --- /dev/null +++ b/src/bitflags.zig @@ -0,0 +1,62 @@ +const std = @import("std"); + +pub fn Bitflags(comptime T: type) type { + const tyinfo = @typeInfo(T); + const IntType = tyinfo.Struct.backing_integer.?; + + return struct { + pub inline fn empty() T { + return @bitCast(@as(IntType, 0)); + } + + pub inline fn intersects(lhs: T, rhs: T) bool { + return asBits(lhs) & asBits(rhs) != 0; + } + + pub inline fn fromName(comptime name: []const u8) T { + var this: T = .{}; + @field(this, name) = true; + return this; + } + + pub inline fn fromNames(comptime names: []const []const u8) T { + var this: T = .{}; + inline for (names) |name| { + @field(this, name) = true; + } + return this; + } + + pub fn bitwiseOr(lhs: T, rhs: T) T { + return @bitCast(@as(IntType, @bitCast(lhs)) | @as(IntType, @bitCast(rhs))); + } + + pub fn bitwiseAnd(lhs: T, rhs: T) T { + return @bitCast(@as(IntType, asBits(lhs) & asBits(rhs))); + } + + pub inline fn insert(this: *T, other: T) void { + this.* = bitwiseOr(this.*, other); + } + + pub fn contains(lhs: T, rhs: T) bool { + return @as(IntType, @bitCast(lhs)) & @as(IntType, @bitCast(rhs)) != 0; + } + + pub inline fn asBits(this: T) IntType { + return @as(IntType, @bitCast(this)); + } + + pub fn isEmpty(this: T) bool { + return asBits(this) == 0; + } + + pub fn eq(lhs: T, rhs: T) bool { + return asBits(lhs) == asBits(rhs); + } + + pub fn neq(lhs: T, rhs: T) bool { + return asBits(lhs) != asBits(rhs); + } + }; +} diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 488b9dc9f0a5b..c22f173445302 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -42,6 +42,7 @@ pub const BunObject = struct { pub const shellEscape = toJSCallback(Bun.shellEscape); pub const createParsedShellScript = toJSCallback(bun.shell.ParsedShellScript.createParsedShellScript); pub const createShellInterpreter = toJSCallback(bun.shell.Interpreter.createShellInterpreter); + pub const color = bun.css.CssColor.jsFunctionColor; // --- Callbacks --- // --- Getters --- @@ -136,12 +137,13 @@ pub const BunObject = struct { // --- Getters -- // -- Callbacks -- - @export(BunObject.createParsedShellScript, .{ .name = callbackName("createParsedShellScript") }); - @export(BunObject.createShellInterpreter, .{ .name = callbackName("createShellInterpreter") }); @export(BunObject.allocUnsafe, .{ .name = callbackName("allocUnsafe") }); @export(BunObject.braces, .{ .name = callbackName("braces") }); @export(BunObject.build, .{ .name = callbackName("build") }); + @export(BunObject.color, .{ .name = callbackName("color") }); @export(BunObject.connect, .{ .name = callbackName("connect") }); + @export(BunObject.createParsedShellScript, .{ .name = callbackName("createParsedShellScript") }); + @export(BunObject.createShellInterpreter, .{ .name = callbackName("createShellInterpreter") }); @export(BunObject.deflateSync, .{ .name = callbackName("deflateSync") }); @export(BunObject.file, .{ .name = callbackName("file") }); @export(BunObject.gc, .{ .name = callbackName("gc") }); @@ -152,7 +154,6 @@ pub const BunObject = struct { @export(BunObject.inflateSync, .{ .name = callbackName("inflateSync") }); @export(BunObject.jest, .{ .name = callbackName("jest") }); @export(BunObject.listen, .{ .name = callbackName("listen") }); - @export(BunObject.udpSocket, .{ .name = callbackName("udpSocket") }); @export(BunObject.mmap, .{ .name = callbackName("mmap") }); @export(BunObject.nanoseconds, .{ .name = callbackName("nanoseconds") }); @export(BunObject.openInEditor, .{ .name = callbackName("openInEditor") }); @@ -161,14 +162,15 @@ pub const BunObject = struct { @export(BunObject.resolveSync, .{ .name = callbackName("resolveSync") }); @export(BunObject.serve, .{ .name = callbackName("serve") }); @export(BunObject.sha, .{ .name = callbackName("sha") }); + @export(BunObject.shellEscape, .{ .name = callbackName("shellEscape") }); @export(BunObject.shrink, .{ .name = callbackName("shrink") }); @export(BunObject.sleepSync, .{ .name = callbackName("sleepSync") }); @export(BunObject.spawn, .{ .name = callbackName("spawn") }); @export(BunObject.spawnSync, .{ .name = callbackName("spawnSync") }); + @export(BunObject.stringWidth, .{ .name = callbackName("stringWidth") }); + @export(BunObject.udpSocket, .{ .name = callbackName("udpSocket") }); @export(BunObject.which, .{ .name = callbackName("which") }); @export(BunObject.write, .{ .name = callbackName("write") }); - @export(BunObject.stringWidth, .{ .name = callbackName("stringWidth") }); - @export(BunObject.shellEscape, .{ .name = callbackName("shellEscape") }); // -- Callbacks -- } }; diff --git a/src/bun.js/bindings/BunObject+exports.h b/src/bun.js/bindings/BunObject+exports.h index 8d6bd26d59176..374af287744ae 100644 --- a/src/bun.js/bindings/BunObject+exports.h +++ b/src/bun.js/bindings/BunObject+exports.h @@ -38,6 +38,7 @@ macro(braces) \ macro(build) \ macro(connect) \ + macro(color) \ macro(deflateSync) \ macro(file) \ macro(fs) \ diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 9d0629aa23ecc..7be836dc1404a 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -580,6 +580,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj concatArrayBuffers functionConcatTypedArrays DontDelete|Function 3 connect BunObject_callback_connect DontDelete|Function 1 cwd BunObject_getter_wrap_cwd DontEnum|DontDelete|PropertyCallback + color BunObject_callback_color DontDelete|Function 2 deepEquals functionBunDeepEquals DontDelete|Function 2 deepMatch functionBunDeepMatch DontDelete|Function 2 deflateSync BunObject_callback_deflateSync DontDelete|Function 1 diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index c7413c265173f..37b7f83500ad0 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -5269,8 +5269,7 @@ pub const JSValue = enum(JSValueReprInt) { return error.JSError; } - const target_str = this.getZigString(globalThis); - return StringMap.getWithEql(target_str, ZigString.eqlComptime) orelse { + return StringMap.fromJS(globalThis, this) orelse { const one_of = struct { pub const list = brk: { var str: []const u8 = "'"; @@ -5288,7 +5287,8 @@ pub const JSValue = enum(JSValueReprInt) { pub const label = property_name ++ " must be one of " ++ list; }.label; - globalThis.throwInvalidArguments(one_of, .{}); + if (!globalThis.hasException()) + globalThis.throwInvalidArguments(one_of, .{}); return error.JSError; }; } diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index fe9b945d4dc8c..18f8f73e0b60d 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2674,6 +2674,8 @@ pub const VirtualMachine = struct { )) { .success => |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 `