From 09727101c1f7e03d693d05278ca7141241121718 Mon Sep 17 00:00:00 2001 From: Raph Date: Sat, 24 May 2025 01:59:28 +0200 Subject: [PATCH] various fixes according to PR review --- src/browser/cssom/css_parser.zig | 214 ++++---- src/browser/cssom/css_style_declaration.zig | 23 +- src/browser/cssom/css_value_analyzer.zig | 576 ++++++++++++++++++-- 3 files changed, 651 insertions(+), 162 deletions(-) diff --git a/src/browser/cssom/css_parser.zig b/src/browser/cssom/css_parser.zig index 24879de7..6a4bfdf3 100644 --- a/src/browser/cssom/css_parser.zig +++ b/src/browser/cssom/css_parser.zig @@ -1,36 +1,44 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + const std = @import("std"); const CSSConstants = struct { - const IMPORTANT_KEYWORD = "!important"; - const IMPORTANT_LENGTH = IMPORTANT_KEYWORD.len; + const IMPORTANT = "!important"; const URL_PREFIX = "url("; - const URL_PREFIX_LENGTH = URL_PREFIX.len; }; pub const CSSParserState = enum { - seekName, - inName, - seekColon, - seekValue, - inValue, - inQuotedValue, - inSingleQuotedValue, - inUrl, - inImportant, + seek_name, + in_name, + seek_colon, + seek_value, + in_value, + in_quoted_value, + in_single_quoted_value, + in_url, + in_important, }; pub const CSSDeclaration = struct { name: []const u8, value: []const u8, is_important: bool, - - pub fn init(name: []const u8, value: []const u8, is_important: bool) CSSDeclaration { - return .{ - .name = name, - .value = value, - .is_important = is_important, - }; - } }; pub const CSSParser = struct { @@ -44,7 +52,7 @@ pub const CSSParser = struct { pub fn init() CSSParser { return .{ - .state = .seekName, + .state = .seek_name, .name_start = 0, .name_end = 0, .value_start = 0, @@ -54,59 +62,56 @@ pub const CSSParser = struct { }; } - pub fn parseDeclarations(text: []const u8, allocator: std.mem.Allocator) ![]CSSDeclaration { + pub fn parseDeclarations(allocator: std.mem.Allocator, text: []const u8) ![]CSSDeclaration { var parser = init(); - var declarations = std.ArrayList(CSSDeclaration).init(allocator); - errdefer declarations.deinit(); + var declarations = std.ArrayListUnmanaged(CSSDeclaration){}; while (parser.position < text.len) { const c = text[parser.position]; switch (parser.state) { - .seekName => { + .seek_name => { if (!std.ascii.isWhitespace(c)) { parser.name_start = parser.position; - parser.state = .inName; + parser.state = .in_name; continue; } }, - .inName => { + .in_name => { if (c == ':') { parser.name_end = parser.position; - parser.state = .seekValue; + parser.state = .seek_value; } else if (std.ascii.isWhitespace(c)) { parser.name_end = parser.position; - parser.state = .seekColon; + parser.state = .seek_colon; } }, - .seekColon => { + .seek_colon => { if (c == ':') { - parser.state = .seekValue; + parser.state = .seek_value; } else if (!std.ascii.isWhitespace(c)) { - parser.state = .seekName; + parser.state = .seek_name; continue; } }, - .seekValue => { + .seek_value => { if (!std.ascii.isWhitespace(c)) { parser.value_start = parser.position; if (c == '"') { - parser.state = .inQuotedValue; + parser.state = .in_quoted_value; } else if (c == '\'') { - parser.state = .inSingleQuotedValue; - } else if (c == 'u' and parser.position + 3 < text.len and - std.mem.eql(u8, text[parser.position .. parser.position + 4], CSSConstants.URL_PREFIX)) - { - parser.state = .inUrl; + parser.state = .in_single_quoted_value; + } else if (c == 'u' and parser.position + CSSConstants.URL_PREFIX.len <= text.len and std.mem.startsWith(u8, text[parser.position..], CSSConstants.URL_PREFIX)) { + parser.state = .in_url; parser.paren_depth = 1; parser.position += 3; } else { - parser.state = .inValue; + parser.state = .in_value; continue; } } }, - .inValue => { + .in_value => { if (parser.escape_next) { parser.escape_next = false; } else if (c == '\\') { @@ -116,29 +121,29 @@ pub const CSSParser = struct { } else if (c == ')' and parser.paren_depth > 0) { parser.paren_depth -= 1; } else if (c == ';' and parser.paren_depth == 0) { - try parser.finishDeclaration(text, &declarations); - parser.state = .seekName; + try parser.finishDeclaration(allocator, &declarations, text); + parser.state = .seek_name; } }, - .inQuotedValue => { + .in_quoted_value => { if (parser.escape_next) { parser.escape_next = false; } else if (c == '\\') { parser.escape_next = true; } else if (c == '"') { - parser.state = .inValue; + parser.state = .in_value; } }, - .inSingleQuotedValue => { + .in_single_quoted_value => { if (parser.escape_next) { parser.escape_next = false; } else if (c == '\\') { parser.escape_next = true; } else if (c == '\'') { - parser.state = .inValue; + parser.state = .in_value; } }, - .inUrl => { + .in_url => { if (parser.escape_next) { parser.escape_next = false; } else if (c == '\\') { @@ -148,22 +153,22 @@ pub const CSSParser = struct { } else if (c == ')') { parser.paren_depth -= 1; if (parser.paren_depth == 0) { - parser.state = .inValue; + parser.state = .in_value; } } }, - .inImportant => {}, + .in_important => {}, } parser.position += 1; } - try parser.finalize(text, &declarations); + try parser.finalize(allocator, &declarations, text); - return declarations.toOwnedSlice(); + return declarations.items; } - fn finishDeclaration(self: *CSSParser, text: []const u8, declarations: *std.ArrayList(CSSDeclaration)) !void { + fn finishDeclaration(self: *CSSParser, allocator: std.mem.Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void { const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace); if (name.len == 0) return; @@ -173,17 +178,20 @@ pub const CSSParser = struct { var final_value = value; var is_important = false; - if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT_KEYWORD)) { + if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) { is_important = true; - final_value = std.mem.trim(u8, value[0 .. value.len - CSSConstants.IMPORTANT_LENGTH], &std.ascii.whitespace); + final_value = std.mem.trim(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace); } - const declaration = CSSDeclaration.init(name, final_value, is_important); - try declarations.append(declaration); + try declarations.append(allocator, .{ + .name = name, + .value = final_value, + .is_important = is_important, + }); } - fn finalize(self: *CSSParser, text: []const u8, declarations: *std.ArrayList(CSSDeclaration)) !void { - if (self.state == .inValue) { + fn finalize(self: *CSSParser, allocator: std.mem.Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void { + if (self.state == .in_value) { const name = text[self.name_start..self.name_end]; const trimmed_name = std.mem.trim(u8, name, &std.ascii.whitespace); @@ -193,13 +201,16 @@ pub const CSSParser = struct { var final_value = value; var is_important = false; - if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT_KEYWORD)) { + if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) { is_important = true; - final_value = std.mem.trim(u8, value[0 .. value.len - CSSConstants.IMPORTANT_LENGTH], &std.ascii.whitespace); + final_value = std.mem.trim(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace); } - const declaration = CSSDeclaration.init(trimmed_name, final_value, is_important); - try declarations.append(declaration); + try declarations.append(allocator, .{ + .name = trimmed_name, + .value = final_value, + .is_important = is_important, + }); } } } @@ -217,92 +228,93 @@ pub const CSSParser = struct { } }; -const testing = std.testing; +const testing = @import("../../testing.zig"); test "CSSParser - Simple property" { - const text = "color: red;"; - const allocator = testing.allocator; + defer testing.reset(); - const declarations = try CSSParser.parseDeclarations(text, allocator); - defer allocator.free(declarations); + const text = "color: red;"; + const allocator = testing.arena_allocator; + + const declarations = try CSSParser.parseDeclarations(allocator, text); try testing.expect(declarations.len == 1); - try testing.expectEqualStrings("color", declarations[0].name); - try testing.expectEqualStrings("red", declarations[0].value); + try testing.expectEqual("color", declarations[0].name); + try testing.expectEqual("red", declarations[0].value); try testing.expect(!declarations[0].is_important); } test "CSSParser - Property with !important" { + defer testing.reset(); const text = "margin: 10px !important;"; - const allocator = testing.allocator; + const allocator = testing.arena_allocator; - const declarations = try CSSParser.parseDeclarations(text, allocator); - defer allocator.free(declarations); + const declarations = try CSSParser.parseDeclarations(allocator, text); try testing.expect(declarations.len == 1); - try testing.expectEqualStrings("margin", declarations[0].name); - try testing.expectEqualStrings("10px", declarations[0].value); + try testing.expectEqual("margin", declarations[0].name); + try testing.expectEqual("10px", declarations[0].value); try testing.expect(declarations[0].is_important); } test "CSSParser - Multiple properties" { + defer testing.reset(); const text = "color: red; font-size: 12px; margin: 5px !important;"; - const allocator = testing.allocator; + const allocator = testing.arena_allocator; - const declarations = try CSSParser.parseDeclarations(text, allocator); - defer allocator.free(declarations); + const declarations = try CSSParser.parseDeclarations(allocator, text); try testing.expect(declarations.len == 3); - try testing.expectEqualStrings("color", declarations[0].name); - try testing.expectEqualStrings("red", declarations[0].value); + try testing.expectEqual("color", declarations[0].name); + try testing.expectEqual("red", declarations[0].value); try testing.expect(!declarations[0].is_important); - try testing.expectEqualStrings("font-size", declarations[1].name); - try testing.expectEqualStrings("12px", declarations[1].value); + try testing.expectEqual("font-size", declarations[1].name); + try testing.expectEqual("12px", declarations[1].value); try testing.expect(!declarations[1].is_important); - try testing.expectEqualStrings("margin", declarations[2].name); - try testing.expectEqualStrings("5px", declarations[2].value); + try testing.expectEqual("margin", declarations[2].name); + try testing.expectEqual("5px", declarations[2].value); try testing.expect(declarations[2].is_important); } test "CSSParser - Quoted value with semicolon" { + defer testing.reset(); const text = "content: \"Hello; world!\";"; - const allocator = testing.allocator; + const allocator = testing.arena_allocator; - const declarations = try CSSParser.parseDeclarations(text, allocator); - defer allocator.free(declarations); + const declarations = try CSSParser.parseDeclarations(allocator, text); try testing.expect(declarations.len == 1); - try testing.expectEqualStrings("content", declarations[0].name); - try testing.expectEqualStrings("\"Hello; world!\"", declarations[0].value); + try testing.expectEqual("content", declarations[0].name); + try testing.expectEqual("\"Hello; world!\"", declarations[0].value); try testing.expect(!declarations[0].is_important); } test "CSSParser - URL value" { + defer testing.reset(); const text = "background-image: url(\"test.png\");"; - const allocator = testing.allocator; + const allocator = testing.arena_allocator; - const declarations = try CSSParser.parseDeclarations(text, allocator); - defer allocator.free(declarations); + const declarations = try CSSParser.parseDeclarations(allocator, text); try testing.expect(declarations.len == 1); - try testing.expectEqualStrings("background-image", declarations[0].name); - try testing.expectEqualStrings("url(\"test.png\")", declarations[0].value); + try testing.expectEqual("background-image", declarations[0].name); + try testing.expectEqual("url(\"test.png\")", declarations[0].value); try testing.expect(!declarations[0].is_important); } test "CSSParser - Whitespace handling" { + defer testing.reset(); const text = " color : purple ; margin : 10px ; "; - const allocator = testing.allocator; + const allocator = testing.arena_allocator; - const declarations = try CSSParser.parseDeclarations(text, allocator); - defer allocator.free(declarations); + const declarations = try CSSParser.parseDeclarations(allocator, text); try testing.expect(declarations.len == 2); - try testing.expectEqualStrings("color", declarations[0].name); - try testing.expectEqualStrings("purple", declarations[0].value); - try testing.expectEqualStrings("margin", declarations[1].name); - try testing.expectEqualStrings("10px", declarations[1].value); + try testing.expectEqual("color", declarations[0].name); + try testing.expectEqual("purple", declarations[0].value); + try testing.expectEqual("margin", declarations[1].name); + try testing.expectEqual("10px", declarations[1].value); } diff --git a/src/browser/cssom/css_style_declaration.zig b/src/browser/cssom/css_style_declaration.zig index 359d9dc9..153baa8d 100644 --- a/src/browser/cssom/css_style_declaration.zig +++ b/src/browser/cssom/css_style_declaration.zig @@ -48,31 +48,24 @@ pub const CSSStyleDeclaration = struct { } pub fn get_cssText(self: *const CSSStyleDeclaration, state: *SessionState) ![]const u8 { - var buffer = std.ArrayList(u8).init(state.arena); - const writer = buffer.writer(); + var buffer = std.ArrayListUnmanaged(u8){}; + const writer = buffer.writer(state.call_arena); for (self.order.items) |name| { const prop = self.store.get(name).?; - const escaped = try CSSValueAnalyzer.escapeCSSValue(state.arena, prop.value); + const escaped = try CSSValueAnalyzer.escapeCSSValue(state.call_arena, prop.value); try writer.print("{s}: {s}", .{ name, escaped }); if (prop.priority) try writer.writeAll(" !important"); try writer.writeAll("; "); } - return buffer.toOwnedSlice(); + return buffer.items; } // TODO Propagate also upward to parent node pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, state: *SessionState) !void { - var store_it = self.store.iterator(); - while (store_it.next()) |entry| { - state.arena.free(entry.key_ptr.*); - state.arena.free(entry.value_ptr.value); - } self.store.clearRetainingCapacity(); - for (self.order.items) |name| state.arena.free(name); self.order.clearRetainingCapacity(); - const declarations = try CSSParser.parseDeclarations(text, state.arena); - defer state.arena.free(declarations); + const declarations = try CSSParser.parseDeclarations(state.call_arena, text); for (declarations) |decl| { if (!CSSValueAnalyzer.isValidPropertyName(decl.name)) continue; @@ -104,13 +97,11 @@ pub const CSSStyleDeclaration = struct { pub fn _removeProperty(self: *CSSStyleDeclaration, name: []const u8, state: *SessionState) ![]const u8 { if (self.store.get(name)) |prop| { - const value_copy = try state.arena.dupe(u8, prop.value); + const value_copy = try state.call_arena.dupe(u8, prop.value); _ = self.store.remove(name); - state.arena.free(prop.value); var i: usize = 0; while (i < self.order.items.len) : (i += 1) { if (std.mem.eql(u8, self.order.items[i], name)) { - state.arena.free(self.order.items[i]); _ = self.order.orderedRemove(i); break; } @@ -130,8 +121,6 @@ pub const CSSStyleDeclaration = struct { try self.order.append(state.arena, name_copy); try self.store.put(state.arena, name_copy, Property{ .value = value_copy, .priority = is_important }); } else { - const prev = self.store.get(name).?; - state.arena.free(prev.value); try self.store.put(state.arena, name, Property{ .value = value_copy, .priority = is_important }); } } diff --git a/src/browser/cssom/css_value_analyzer.zig b/src/browser/cssom/css_value_analyzer.zig index 9b098aa8..36c4a0f2 100644 --- a/src/browser/cssom/css_value_analyzer.zig +++ b/src/browser/cssom/css_value_analyzer.zig @@ -39,8 +39,10 @@ pub const CSSValueAnalyzer = struct { } else if (c == '.' and !decimal_point) { decimal_point = true; } else if ((c == 'e' or c == 'E') and has_digit) { + if (i + 1 >= value.len) return false; + if (value[i + 1] != '+' and value[i + 1] != '-' and !std.ascii.isDigit(value[i + 1])) break; i += 1; - if (i < value.len and (value[i] == '+' or value[i] == '-')) { + if (value[i] == '+' or value[i] == '-') { i += 1; } var has_exp_digits = false; @@ -106,15 +108,39 @@ pub const CSSValueAnalyzer = struct { pub fn isValidPropertyName(name: []const u8) bool { if (name.len == 0) return false; + if (std.mem.startsWith(u8, name, "--")) { + if (name.len == 2) return false; + for (name[2..]) |c| { + if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_') { + return false; + } + } + return true; + } + const first_char = name[0]; if (!std.ascii.isAlphabetic(first_char) and first_char != '-') { return false; } - for (name[1..]) |c| { - if (!std.ascii.isAlphanumeric(c) and c != '-') { + if (first_char == '-') { + if (name.len < 2) return false; + + if (!std.ascii.isAlphabetic(name[1])) { return false; } + + for (name[2..]) |c| { + if (!std.ascii.isAlphanumeric(c) and c != '-') { + return false; + } + } + } else { + for (name[1..]) |c| { + if (!std.ascii.isAlphanumeric(c) and c != '-') { + return false; + } + } } return true; @@ -149,13 +175,12 @@ pub const CSSValueAnalyzer = struct { } pub fn escapeCSSValue(allocator: std.mem.Allocator, value: []const u8) ![]const u8 { - var out = std.ArrayList(u8).init(allocator); - errdefer out.deinit(); - const writer = out.writer(); + var out = std.ArrayListUnmanaged(u8){}; + const writer = out.writer(allocator); if (isAlreadyQuoted(value)) { try writer.writeAll(value); - return out.toOwnedSlice(); + return out.items; } const needs_quotes = needsQuotes(value); @@ -163,7 +188,7 @@ pub const CSSValueAnalyzer = struct { if (needs_quotes) { try writer.writeByte('"'); - for (value) |c| { + for (value, 0..) |c, i| { switch (c) { '"' => try writer.writeAll("\\\""), '\\' => try writer.writeAll("\\\\"), @@ -172,7 +197,7 @@ pub const CSSValueAnalyzer = struct { '\t' => try writer.writeAll("\\9 "), 0...8, 11, 12, 14...31, 127 => { try writer.print("\\{x}", .{c}); - if (c + 1 < value.len and std.ascii.isHex(value[c + 1])) { + if (i + 1 < value.len and std.ascii.isHex(value[i + 1])) { try writer.writeByte(' '); } }, @@ -185,7 +210,7 @@ pub const CSSValueAnalyzer = struct { try writer.writeAll(value); } - return out.toOwnedSlice(); + return out.items; } pub fn isKnownKeyword(value: []const u8) bool { @@ -198,84 +223,84 @@ pub const CSSValueAnalyzer = struct { }; const CSSKeywords = struct { - const BorderStyles = [_][]const u8{ + const border_styles = [_][]const u8{ "none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset", }; - const ColorNames = [_][]const u8{ + const color_names = [_][]const u8{ "black", "white", "red", "green", "blue", "yellow", "purple", "gray", "transparent", "currentColor", "inherit", }; - const PositionKeywords = [_][]const u8{ + const position_keywords = [_][]const u8{ "auto", "center", "left", "right", "top", "bottom", }; - const BackgroundRepeat = [_][]const u8{ + const background_repeat = [_][]const u8{ "repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round", }; - const FontStyles = [_][]const u8{ + const font_styles = [_][]const u8{ "normal", "italic", "oblique", "bold", "bolder", "lighter", }; - const FontSizes = [_][]const u8{ + const font_sizes = [_][]const u8{ "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "smaller", "larger", }; - const FontFamilies = [_][]const u8{ + const font_families = [_][]const u8{ "serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", }; - const CSSGlobal = [_][]const u8{ + const css_global = [_][]const u8{ "initial", "inherit", "unset", "revert", }; - const DisplayValues = [_][]const u8{ + const display_values = [_][]const u8{ "block", "inline", "inline-block", "flex", "grid", "none", }; - const LengthUnits = [_][]const u8{ + const length_units = [_][]const u8{ "px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm", "ex", "ch", "fr", }; - const AngleUnits = [_][]const u8{ + const angle_units = [_][]const u8{ "deg", "rad", "grad", "turn", }; - const TimeUnits = [_][]const u8{ + const time_units = [_][]const u8{ "s", "ms", }; - const FrequencyUnits = [_][]const u8{ + const frequency_units = [_][]const u8{ "Hz", "kHz", }; - const ResolutionUnits = [_][]const u8{ + const resolution_units = [_][]const u8{ "dpi", "dpcm", "dppx", }; - const SpecialChars = [_]u8{ - '"', '\'', ';', '{', '}', '\\', '<', '>', '/', + const special_chars = [_]u8{ + '"', '\'', ';', '{', '}', '\\', '<', '>', '/', '\n', '\t', '\r', '\x00', '\x7F', }; - const Functions = [_][]const u8{ - "rgb", "rgba", "hsl", "hsla", "url", "calc", "var", "attr", - "linear-gradient", "radial-gradient", "conic-gradient", "translate", "rotate", "scale", "skew", "matrix", + const functions = [_][]const u8{ + "rgb(", "rgba(", "hsl(", "hsla(", "url(", "calc(", "var(", "attr(", + "linear-gradient(", "radial-gradient(", "conic-gradient(", "translate(", "rotate(", "scale(", "skew(", "matrix(", }; pub fn isKnownKeyword(value: []const u8) bool { const all_categories = [_][]const []const u8{ - &BorderStyles, &ColorNames, &PositionKeywords, &BackgroundRepeat, - &FontStyles, &FontSizes, &FontFamilies, &CSSGlobal, - &DisplayValues, + &border_styles, &color_names, &position_keywords, &background_repeat, + &font_styles, &font_sizes, &font_families, &css_global, + &display_values, }; for (all_categories) |category| { for (category) |keyword| { - if (std.mem.eql(u8, value, keyword)) { + if (std.ascii.eqlIgnoreCase(value, keyword)) { return true; } } @@ -286,7 +311,7 @@ const CSSKeywords = struct { pub fn containsSpecialChar(value: []const u8) bool { for (value) |c| { - for (SpecialChars) |special| { + for (special_chars) |special| { if (c == special) { return true; } @@ -297,12 +322,12 @@ const CSSKeywords = struct { pub fn isValidUnit(unit: []const u8) bool { const all_units = [_][]const []const u8{ - &LengthUnits, &AngleUnits, &TimeUnits, &FrequencyUnits, &ResolutionUnits, + &length_units, &angle_units, &time_units, &frequency_units, &resolution_units, }; for (all_units) |category| { for (category) |valid_unit| { - if (std.mem.eql(u8, unit, valid_unit)) { + if (std.ascii.eqlIgnoreCase(unit, valid_unit)) { return true; } } @@ -312,16 +337,479 @@ const CSSKeywords = struct { } pub fn startsWithFunction(value: []const u8) bool { - for (Functions) |func| { - if (value.len >= func.len + 1 and - std.mem.startsWith(u8, value, func) and - value[func.len] == '(') - { - return true; + const open_paren = std.mem.indexOf(u8, value, "("); + const close_paren = std.mem.indexOf(u8, value, ")"); + + if (open_paren == null or close_paren == null) return false; + if (open_paren == 0) return false; + + const function_name = value[0..open_paren.?]; + return isValidFunctionName(function_name); + } + + fn isValidFunctionName(name: []const u8) bool { + if (name.len == 0) return false; + + const first = name[0]; + if (!std.ascii.isAlphabetic(first) and first != '_' and first != '-') { + return false; + } + + for (name[1..]) |c| { + if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '-') { + return false; } } - return std.mem.indexOf(u8, value, "(") != null and - std.mem.indexOf(u8, value, ")") != null; + return true; } }; + +const testing = @import("../../testing.zig"); + +test "isNumericWithUnit - valid numbers with units" { + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("10px")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("3.14em")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-5rem")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("+12.5%")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0vh")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit(".5vw")); +} + +test "isNumericWithUnit - scientific notation" { + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e5px")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("2.5E-3em")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e+2rem")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-3.14e10px")); +} + +test "isNumericWithUnit - edge cases and invalid inputs" { + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("")); + + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("px")); + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("--px")); + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(".px")); + + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e")); + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1epx")); + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e+")); + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e+px")); + + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1.2.3px")); + + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10xyz")); + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("5invalid")); + + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("10")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("3.14")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-5")); +} + +test "isHexColor - valid hex colors" { + try testing.expect(CSSValueAnalyzer.isHexColor("#000")); + try testing.expect(CSSValueAnalyzer.isHexColor("#fff")); + try testing.expect(CSSValueAnalyzer.isHexColor("#123456")); + try testing.expect(CSSValueAnalyzer.isHexColor("#abcdef")); + try testing.expect(CSSValueAnalyzer.isHexColor("#ABCDEF")); + try testing.expect(CSSValueAnalyzer.isHexColor("#12345678")); +} + +test "isHexColor - invalid hex colors" { + try testing.expect(!CSSValueAnalyzer.isHexColor("")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#")); + try testing.expect(!CSSValueAnalyzer.isHexColor("000")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#00")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#0000")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#00000")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#0000000")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#000000000")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#gggggg")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#123xyz")); +} + +test "isMultiValueProperty - valid multi-value properties" { + try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px")); + try testing.expect(CSSValueAnalyzer.isMultiValueProperty("solid red")); + try testing.expect(CSSValueAnalyzer.isMultiValueProperty("#fff black")); + try testing.expect(CSSValueAnalyzer.isMultiValueProperty("1em 2em 3em 4em")); + try testing.expect(CSSValueAnalyzer.isMultiValueProperty("rgb(255,0,0) solid")); +} + +test "isMultiValueProperty - invalid multi-value properties" { + try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("")); + try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px")); + try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("invalid unknown")); + try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px invalid")); + try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(" ")); +} + +test "isAlreadyQuoted - various quoting scenarios" { + try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"hello\"")); + try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'world'")); + try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"\"")); + try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("''")); + + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello'")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'hello\"")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello\"")); +} + +test "isValidPropertyName - valid property names" { + try testing.expect(CSSValueAnalyzer.isValidPropertyName("color")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("background-color")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("-webkit-transform")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("font-size")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("margin-top")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("z-index")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("line-height")); +} + +test "isValidPropertyName - invalid property names" { + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("")); + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("123color")); + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color!")); + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color space")); + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("@color")); + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color.test")); + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color_test")); +} + +test "extractImportant - with and without !important" { + var result = CSSValueAnalyzer.extractImportant("red !important"); + try testing.expect(result.is_important); + try testing.expectEqual("red", result.value); + + result = CSSValueAnalyzer.extractImportant("blue"); + try testing.expect(!result.is_important); + try testing.expectEqual("blue", result.value); + + result = CSSValueAnalyzer.extractImportant(" green !important "); + try testing.expect(result.is_important); + try testing.expectEqual("green", result.value); + + result = CSSValueAnalyzer.extractImportant("!important"); + try testing.expect(result.is_important); + try testing.expectEqual("", result.value); + + result = CSSValueAnalyzer.extractImportant("important"); + try testing.expect(!result.is_important); + try testing.expectEqual("important", result.value); +} + +test "needsQuotes - various scenarios" { + try testing.expect(CSSValueAnalyzer.needsQuotes("")); + try testing.expect(CSSValueAnalyzer.needsQuotes("hello world")); + try testing.expect(CSSValueAnalyzer.needsQuotes("test;")); + try testing.expect(CSSValueAnalyzer.needsQuotes("a{b}")); + try testing.expect(CSSValueAnalyzer.needsQuotes("test\"quote")); + + try testing.expect(!CSSValueAnalyzer.needsQuotes("\"already quoted\"")); + try testing.expect(!CSSValueAnalyzer.needsQuotes("'already quoted'")); + try testing.expect(!CSSValueAnalyzer.needsQuotes("url(image.png)")); + try testing.expect(!CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0)")); + try testing.expect(!CSSValueAnalyzer.needsQuotes("10px 20px")); + try testing.expect(!CSSValueAnalyzer.needsQuotes("simple")); +} + +test "escapeCSSValue - escaping various characters" { + const allocator = testing.arena_allocator; + + var result = try CSSValueAnalyzer.escapeCSSValue(allocator, "simple"); + try testing.expectEqual("simple", result); + + result = try CSSValueAnalyzer.escapeCSSValue(allocator, "\"already quoted\""); + try testing.expectEqual("\"already quoted\"", result); + + result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\"quote"); + try testing.expectEqual("\"test\\\"quote\"", result); + + result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\nline"); + try testing.expectEqual("\"test\\A line\"", result); + + result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\\back"); + try testing.expectEqual("\"test\\\\back\"", result); +} + +test "CSSKeywords.isKnownKeyword - case sensitivity" { + try testing.expect(CSSKeywords.isKnownKeyword("red")); + try testing.expect(CSSKeywords.isKnownKeyword("solid")); + try testing.expect(CSSKeywords.isKnownKeyword("center")); + try testing.expect(CSSKeywords.isKnownKeyword("inherit")); + + try testing.expect(CSSKeywords.isKnownKeyword("RED")); + try testing.expect(CSSKeywords.isKnownKeyword("Red")); + try testing.expect(CSSKeywords.isKnownKeyword("SOLID")); + try testing.expect(CSSKeywords.isKnownKeyword("Center")); + + try testing.expect(!CSSKeywords.isKnownKeyword("invalid")); + try testing.expect(!CSSKeywords.isKnownKeyword("unknown")); + try testing.expect(!CSSKeywords.isKnownKeyword("")); +} + +test "CSSKeywords.containsSpecialChar - various special characters" { + try testing.expect(CSSKeywords.containsSpecialChar("test\"quote")); + try testing.expect(CSSKeywords.containsSpecialChar("test'quote")); + try testing.expect(CSSKeywords.containsSpecialChar("test;end")); + try testing.expect(CSSKeywords.containsSpecialChar("test{brace")); + try testing.expect(CSSKeywords.containsSpecialChar("test}brace")); + try testing.expect(CSSKeywords.containsSpecialChar("test\\back")); + try testing.expect(CSSKeywords.containsSpecialChar("testangle")); + try testing.expect(CSSKeywords.containsSpecialChar("test/slash")); + + try testing.expect(!CSSKeywords.containsSpecialChar("normal-text")); + try testing.expect(!CSSKeywords.containsSpecialChar("text123")); + try testing.expect(!CSSKeywords.containsSpecialChar("")); +} + +test "CSSKeywords.isValidUnit - various units" { + try testing.expect(CSSKeywords.isValidUnit("px")); + try testing.expect(CSSKeywords.isValidUnit("em")); + try testing.expect(CSSKeywords.isValidUnit("rem")); + try testing.expect(CSSKeywords.isValidUnit("%")); + + try testing.expect(CSSKeywords.isValidUnit("deg")); + try testing.expect(CSSKeywords.isValidUnit("rad")); + + try testing.expect(CSSKeywords.isValidUnit("s")); + try testing.expect(CSSKeywords.isValidUnit("ms")); + + try testing.expect(CSSKeywords.isValidUnit("PX")); + + try testing.expect(!CSSKeywords.isValidUnit("invalid")); + try testing.expect(!CSSKeywords.isValidUnit("")); +} + +test "CSSKeywords.startsWithFunction - function detection" { + try testing.expect(CSSKeywords.startsWithFunction("rgb(255, 0, 0)")); + try testing.expect(CSSKeywords.startsWithFunction("rgba(255, 0, 0, 0.5)")); + try testing.expect(CSSKeywords.startsWithFunction("url(image.png)")); + try testing.expect(CSSKeywords.startsWithFunction("calc(100% - 20px)")); + try testing.expect(CSSKeywords.startsWithFunction("var(--custom-property)")); + try testing.expect(CSSKeywords.startsWithFunction("linear-gradient(to right, red, blue)")); + + try testing.expect(CSSKeywords.startsWithFunction("custom-function(args)")); + try testing.expect(CSSKeywords.startsWithFunction("unknown(test)")); + + try testing.expect(!CSSKeywords.startsWithFunction("not-a-function")); + try testing.expect(!CSSKeywords.startsWithFunction("missing-paren)")); + try testing.expect(!CSSKeywords.startsWithFunction("missing-close(")); + try testing.expect(!CSSKeywords.startsWithFunction("")); + try testing.expect(!CSSKeywords.startsWithFunction("rgb")); +} + +test "isNumericWithUnit - whitespace handling" { + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(" 10px")); + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10 px")); + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10px ")); + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(" 10 px ")); +} + +test "extractImportant - whitespace edge cases" { + var result = CSSValueAnalyzer.extractImportant(" "); + try testing.expect(!result.is_important); + try testing.expectEqual("", result.value); + + result = CSSValueAnalyzer.extractImportant("\t\n\r !important\t\n"); + try testing.expect(result.is_important); + try testing.expectEqual("", result.value); + + result = CSSValueAnalyzer.extractImportant("red\t!important"); + try testing.expect(result.is_important); + try testing.expectEqual("red", result.value); +} + +test "isHexColor - mixed case handling" { + try testing.expect(CSSValueAnalyzer.isHexColor("#AbC")); + try testing.expect(CSSValueAnalyzer.isHexColor("#123aBc")); + try testing.expect(CSSValueAnalyzer.isHexColor("#FFffFF")); + try testing.expect(CSSValueAnalyzer.isHexColor("#000FFF")); +} + +test "edge case - very long inputs" { + const long_valid = "a" ** 1000 ++ "px"; + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(long_valid)); // not numeric + + const long_property = "a-" ** 100 ++ "property"; + try testing.expect(CSSValueAnalyzer.isValidPropertyName(long_property)); + + const long_hex = "#" ++ "a" ** 20; + try testing.expect(!CSSValueAnalyzer.isHexColor(long_hex)); +} + +test "boundary conditions - numeric parsing" { + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0px")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.0px")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit(".0px")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.px")); + + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("999999999px")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1.7976931348623157e+308px")); + + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.000000001px")); + try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e-100px")); +} + +test "extractImportant - malformed important declarations" { + var result = CSSValueAnalyzer.extractImportant("red ! important"); + try testing.expect(!result.is_important); + try testing.expectEqual("red ! important", result.value); + + result = CSSValueAnalyzer.extractImportant("red !Important"); + try testing.expect(!result.is_important); + try testing.expectEqual("red !Important", result.value); + + result = CSSValueAnalyzer.extractImportant("red !IMPORTANT"); + try testing.expect(!result.is_important); + try testing.expectEqual("red !IMPORTANT", result.value); + + result = CSSValueAnalyzer.extractImportant("!importantred"); + try testing.expect(!result.is_important); + try testing.expectEqual("!importantred", result.value); + + result = CSSValueAnalyzer.extractImportant("red !important !important"); + try testing.expect(result.is_important); + try testing.expectEqual("red !important", result.value); +} + +test "isMultiValueProperty - complex spacing scenarios" { + try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px")); + try testing.expect(CSSValueAnalyzer.isMultiValueProperty("solid red")); + + try testing.expect(CSSValueAnalyzer.isMultiValueProperty(" 10px 20px ")); + + try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px\t20px")); + try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px\n20px")); + + try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px 30px")); +} + +test "isAlreadyQuoted - edge cases with quotes" { + try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"'hello'\"")); + try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'\"hello\"'")); + + try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"hello\\\"world\"")); + try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'hello\\'world'")); + + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello\"")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'hello")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello'")); + + try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"a\"")); + try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'b'")); +} + +test "needsQuotes - function and URL edge cases" { + try testing.expect(!CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0)")); + try testing.expect(!CSSValueAnalyzer.needsQuotes("calc(100% - 20px)")); + + try testing.expect(!CSSValueAnalyzer.needsQuotes("url(path with spaces.jpg)")); + + try testing.expect(!CSSValueAnalyzer.needsQuotes("linear-gradient(to right, red, blue)")); + + try testing.expect(CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0")); +} + +test "escapeCSSValue - control characters and Unicode" { + const allocator = testing.arena_allocator; + + var result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\ttab"); + try testing.expectEqual("\"test\\9 tab\"", result); + + result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\rreturn"); + try testing.expectEqual("\"test\\D return\"", result); + + result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\x00null"); + try testing.expectEqual("\"test\\0null\"", result); + + result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\x7Fdel"); + try testing.expectEqual("\"test\\7f del\"", result); + + result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\"quote\nline\\back"); + try testing.expectEqual("\"test\\\"quote\\A line\\\\back\"", result); + allocator.free(result); +} + +test "isValidPropertyName - CSS custom properties and vendor prefixes" { + try testing.expect(CSSValueAnalyzer.isValidPropertyName("--custom-color")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("--my-variable")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("--123")); + + try testing.expect(CSSValueAnalyzer.isValidPropertyName("-webkit-transform")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("-moz-border-radius")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("-ms-filter")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("-o-transition")); + + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("-123invalid")); + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("--")); + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("-")); +} + +test "startsWithFunction - case sensitivity and partial matches" { + try testing.expect(CSSKeywords.startsWithFunction("RGB(255, 0, 0)")); + try testing.expect(CSSKeywords.startsWithFunction("Rgb(255, 0, 0)")); + try testing.expect(CSSKeywords.startsWithFunction("URL(image.png)")); + + try testing.expect(CSSKeywords.startsWithFunction("rg(something)")); + try testing.expect(CSSKeywords.startsWithFunction("ur(something)")); + + try testing.expect(CSSKeywords.startsWithFunction("rgb(1,2,3)")); + try testing.expect(CSSKeywords.startsWithFunction("rgba(1,2,3,4)")); + + try testing.expect(CSSKeywords.startsWithFunction("my-custom-function(args)")); + try testing.expect(CSSKeywords.startsWithFunction("function-with-dashes(test)")); + + try testing.expect(!CSSKeywords.startsWithFunction("123function(test)")); +} + +test "isHexColor - Unicode and invalid characters" { + try testing.expect(!CSSValueAnalyzer.isHexColor("#ghijkl")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#12345g")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#xyz")); + + try testing.expect(!CSSValueAnalyzer.isHexColor("#АВС")); + + try testing.expect(!CSSValueAnalyzer.isHexColor("#1234567g")); + try testing.expect(!CSSValueAnalyzer.isHexColor("#g2345678")); +} + +test "complex integration scenarios" { + const allocator = testing.arena_allocator; + + try testing.expect(CSSValueAnalyzer.isMultiValueProperty("rgb(255,0,0) url(bg.jpg)")); + + try testing.expect(!CSSValueAnalyzer.needsQuotes("calc(100% - 20px)")); + + const result = try CSSValueAnalyzer.escapeCSSValue(allocator, "fake(function with spaces"); + try testing.expectEqual("\"fake(function with spaces\"", result); + + const important_result = CSSValueAnalyzer.extractImportant("rgb(255,0,0) !important"); + try testing.expect(important_result.is_important); + try testing.expectEqual("rgb(255,0,0)", important_result.value); +} + +test "performance edge cases - empty and minimal inputs" { + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("")); + try testing.expect(!CSSValueAnalyzer.isHexColor("")); + try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("")); + try testing.expect(!CSSValueAnalyzer.isValidPropertyName("")); + try testing.expect(CSSValueAnalyzer.needsQuotes("")); + try testing.expect(!CSSKeywords.isKnownKeyword("")); + try testing.expect(!CSSKeywords.containsSpecialChar("")); + try testing.expect(!CSSKeywords.isValidUnit("")); + try testing.expect(!CSSKeywords.startsWithFunction("")); + + try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("a")); + try testing.expect(!CSSValueAnalyzer.isHexColor("a")); + try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("a")); + try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("a")); + try testing.expect(CSSValueAnalyzer.isValidPropertyName("a")); + try testing.expect(!CSSValueAnalyzer.needsQuotes("a")); +}