diff --git a/src/browser/tests/css/stylesheet.html b/src/browser/tests/css/stylesheet.html index 5e4f3b27..e717e806 100644 --- a/src/browser/tests/css/stylesheet.html +++ b/src/browser/tests/css/stylesheet.html @@ -341,3 +341,43 @@ testing.expectEqual('10px 20px', div.style.gap); } + + diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 502e479f..adc8982a 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -258,9 +258,194 @@ fn normalizePropertyValue(arena: Allocator, property_name: []const u8, value: [] } } + // Canonicalize anchor-size() function: anchor name (dashed ident) comes before size keyword + if (std.mem.indexOf(u8, value, "anchor-size(") != null) { + return try canonicalizeAnchorSize(arena, value); + } + return value; } +// Canonicalize anchor-size() so that the dashed ident (anchor name) comes before the size keyword. +// e.g. "anchor-size(width --foo)" -> "anchor-size(--foo width)" +fn canonicalizeAnchorSize(arena: Allocator, value: []const u8) ![]const u8 { + var buf = std.Io.Writer.Allocating.init(arena); + var i: usize = 0; + + while (i < value.len) { + // Look for "anchor-size(" + if (std.mem.startsWith(u8, value[i..], "anchor-size(")) { + try buf.writer.writeAll("anchor-size("); + i += "anchor-size(".len; + + // Parse and canonicalize the arguments + i = try canonicalizeAnchorSizeArgs(value, i, &buf.writer); + } else { + try buf.writer.writeByte(value[i]); + i += 1; + } + } + + return buf.written(); +} + +// Parse anchor-size arguments and write them in canonical order +fn canonicalizeAnchorSizeArgs(value: []const u8, start: usize, writer: *std.Io.Writer) !usize { + var i = start; + var depth: usize = 1; + + // Skip leading whitespace + while (i < value.len and value[i] == ' ') : (i += 1) {} + + // Collect tokens before the comma or close paren + var first_token_start: ?usize = null; + var first_token_end: usize = 0; + var second_token_start: ?usize = null; + var second_token_end: usize = 0; + var comma_pos: ?usize = null; + var token_count: usize = 0; + + const args_start = i; + var in_token = false; + + // First pass: find the structure of arguments before comma/closing paren at depth 1 + while (i < value.len and depth > 0) { + const c = value[i]; + + if (c == '(') { + depth += 1; + in_token = true; + i += 1; + } else if (c == ')') { + depth -= 1; + if (depth == 0) { + if (in_token) { + if (token_count == 0) { + first_token_end = i; + } else if (token_count == 1) { + second_token_end = i; + } + } + break; + } + i += 1; + } else if (c == ',' and depth == 1) { + if (in_token) { + if (token_count == 0) { + first_token_end = i; + } else if (token_count == 1) { + second_token_end = i; + } + } + comma_pos = i; + break; + } else if (c == ' ') { + if (in_token and depth == 1) { + if (token_count == 0) { + first_token_end = i; + token_count = 1; + } else if (token_count == 1 and second_token_start != null) { + second_token_end = i; + token_count = 2; + } + in_token = false; + } + i += 1; + } else { + if (!in_token and depth == 1) { + if (token_count == 0) { + first_token_start = i; + } else if (token_count == 1) { + second_token_start = i; + } + in_token = true; + } + i += 1; + } + } + + // Handle end of tokens + if (in_token and token_count == 1 and second_token_start != null) { + second_token_end = i; + token_count = 2; + } else if (in_token and token_count == 0) { + first_token_end = i; + token_count = 1; + } + + // Check if we have exactly two tokens that need reordering + if (token_count == 2) { + const first_start = first_token_start orelse args_start; + const second_start = second_token_start orelse first_token_end; + + const first_token = value[first_start..first_token_end]; + const second_token = value[second_start..second_token_end]; + + // If second token is a dashed ident and first is a size keyword, swap them + if (std.mem.startsWith(u8, second_token, "--") and isAnchorSizeKeyword(first_token)) { + try writer.writeAll(second_token); + try writer.writeByte(' '); + try writer.writeAll(first_token); + } else { + // Keep original order + try writer.writeAll(first_token); + try writer.writeByte(' '); + try writer.writeAll(second_token); + } + } else if (first_token_start) |fts| { + // Single token, just copy it + try writer.writeAll(value[fts..first_token_end]); + } + + // Handle comma and fallback value (may contain nested anchor-size) + if (comma_pos) |cp| { + try writer.writeAll(", "); + i = cp + 1; + // Skip whitespace after comma + while (i < value.len and value[i] == ' ') : (i += 1) {} + + // Copy the fallback, recursively handling nested anchor-size + while (i < value.len and depth > 0) { + if (std.mem.startsWith(u8, value[i..], "anchor-size(")) { + try writer.writeAll("anchor-size("); + i += "anchor-size(".len; + depth += 1; + i = try canonicalizeAnchorSizeArgs(value, i, writer); + depth -= 1; + } else if (value[i] == '(') { + depth += 1; + try writer.writeByte(value[i]); + i += 1; + } else if (value[i] == ')') { + depth -= 1; + if (depth == 0) break; + try writer.writeByte(value[i]); + i += 1; + } else { + try writer.writeByte(value[i]); + i += 1; + } + } + } + + // Write closing paren + try writer.writeByte(')'); + + return i + 1; // Skip past the closing paren +} + +fn isAnchorSizeKeyword(token: []const u8) bool { + const keywords = std.StaticStringMap(void).initComptime(.{ + .{ "width", {} }, + .{ "height", {} }, + .{ "block", {} }, + .{ "inline", {} }, + .{ "self-block", {} }, + .{ "self-inline", {} }, + }); + return keywords.has(token); +} + // Check if a value is "X X" (duplicate) and return just "X" fn collapseDuplicateValue(value: []const u8) ?[]const u8 { const space_idx = std.mem.indexOfScalar(u8, value, ' ') orelse return null;