diff --git a/src/browser/cssom/css_parser.zig b/src/browser/cssom/css_parser.zig new file mode 100644 index 00000000..24879de7 --- /dev/null +++ b/src/browser/cssom/css_parser.zig @@ -0,0 +1,308 @@ +const std = @import("std"); + +const CSSConstants = struct { + const IMPORTANT_KEYWORD = "!important"; + const IMPORTANT_LENGTH = IMPORTANT_KEYWORD.len; + const URL_PREFIX = "url("; + const URL_PREFIX_LENGTH = URL_PREFIX.len; +}; + +pub const CSSParserState = enum { + seekName, + inName, + seekColon, + seekValue, + inValue, + inQuotedValue, + inSingleQuotedValue, + inUrl, + inImportant, +}; + +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 { + state: CSSParserState, + name_start: usize, + name_end: usize, + value_start: usize, + position: usize, + paren_depth: usize, + escape_next: bool, + + pub fn init() CSSParser { + return .{ + .state = .seekName, + .name_start = 0, + .name_end = 0, + .value_start = 0, + .position = 0, + .paren_depth = 0, + .escape_next = false, + }; + } + + pub fn parseDeclarations(text: []const u8, allocator: std.mem.Allocator) ![]CSSDeclaration { + var parser = init(); + var declarations = std.ArrayList(CSSDeclaration).init(allocator); + errdefer declarations.deinit(); + + while (parser.position < text.len) { + const c = text[parser.position]; + + switch (parser.state) { + .seekName => { + if (!std.ascii.isWhitespace(c)) { + parser.name_start = parser.position; + parser.state = .inName; + continue; + } + }, + .inName => { + if (c == ':') { + parser.name_end = parser.position; + parser.state = .seekValue; + } else if (std.ascii.isWhitespace(c)) { + parser.name_end = parser.position; + parser.state = .seekColon; + } + }, + .seekColon => { + if (c == ':') { + parser.state = .seekValue; + } else if (!std.ascii.isWhitespace(c)) { + parser.state = .seekName; + continue; + } + }, + .seekValue => { + if (!std.ascii.isWhitespace(c)) { + parser.value_start = parser.position; + if (c == '"') { + parser.state = .inQuotedValue; + } 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.paren_depth = 1; + parser.position += 3; + } else { + parser.state = .inValue; + continue; + } + } + }, + .inValue => { + if (parser.escape_next) { + parser.escape_next = false; + } else if (c == '\\') { + parser.escape_next = true; + } else if (c == '(') { + parser.paren_depth += 1; + } 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; + } + }, + .inQuotedValue => { + if (parser.escape_next) { + parser.escape_next = false; + } else if (c == '\\') { + parser.escape_next = true; + } else if (c == '"') { + parser.state = .inValue; + } + }, + .inSingleQuotedValue => { + if (parser.escape_next) { + parser.escape_next = false; + } else if (c == '\\') { + parser.escape_next = true; + } else if (c == '\'') { + parser.state = .inValue; + } + }, + .inUrl => { + if (parser.escape_next) { + parser.escape_next = false; + } else if (c == '\\') { + parser.escape_next = true; + } else if (c == '(') { + parser.paren_depth += 1; + } else if (c == ')') { + parser.paren_depth -= 1; + if (parser.paren_depth == 0) { + parser.state = .inValue; + } + } + }, + .inImportant => {}, + } + + parser.position += 1; + } + + try parser.finalize(text, &declarations); + + return declarations.toOwnedSlice(); + } + + fn finishDeclaration(self: *CSSParser, text: []const u8, declarations: *std.ArrayList(CSSDeclaration)) !void { + const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace); + if (name.len == 0) return; + + const raw_value = text[self.value_start..self.position]; + const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace); + + var final_value = value; + var is_important = false; + + if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT_KEYWORD)) { + is_important = true; + final_value = std.mem.trim(u8, value[0 .. value.len - CSSConstants.IMPORTANT_LENGTH], &std.ascii.whitespace); + } + + const declaration = CSSDeclaration.init(name, final_value, is_important); + try declarations.append(declaration); + } + + fn finalize(self: *CSSParser, text: []const u8, declarations: *std.ArrayList(CSSDeclaration)) !void { + if (self.state == .inValue) { + const name = text[self.name_start..self.name_end]; + const trimmed_name = std.mem.trim(u8, name, &std.ascii.whitespace); + + if (trimmed_name.len > 0) { + const raw_value = text[self.value_start..self.position]; + const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace); + + var final_value = value; + var is_important = false; + if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT_KEYWORD)) { + is_important = true; + final_value = std.mem.trim(u8, value[0 .. value.len - CSSConstants.IMPORTANT_LENGTH], &std.ascii.whitespace); + } + + const declaration = CSSDeclaration.init(trimmed_name, final_value, is_important); + try declarations.append(declaration); + } + } + } + + pub fn getState(self: *const CSSParser) CSSParserState { + return self.state; + } + + pub fn getPosition(self: *const CSSParser) usize { + return self.position; + } + + pub fn reset(self: *CSSParser) void { + self.* = init(); + } +}; + +const testing = std.testing; + +test "CSSParser - Simple property" { + const text = "color: red;"; + const allocator = testing.allocator; + + const declarations = try CSSParser.parseDeclarations(text, allocator); + defer allocator.free(declarations); + + try testing.expect(declarations.len == 1); + try testing.expectEqualStrings("color", declarations[0].name); + try testing.expectEqualStrings("red", declarations[0].value); + try testing.expect(!declarations[0].is_important); +} + +test "CSSParser - Property with !important" { + const text = "margin: 10px !important;"; + const allocator = testing.allocator; + + const declarations = try CSSParser.parseDeclarations(text, allocator); + defer allocator.free(declarations); + + try testing.expect(declarations.len == 1); + try testing.expectEqualStrings("margin", declarations[0].name); + try testing.expectEqualStrings("10px", declarations[0].value); + try testing.expect(declarations[0].is_important); +} + +test "CSSParser - Multiple properties" { + const text = "color: red; font-size: 12px; margin: 5px !important;"; + const allocator = testing.allocator; + + const declarations = try CSSParser.parseDeclarations(text, allocator); + defer allocator.free(declarations); + + try testing.expect(declarations.len == 3); + + try testing.expectEqualStrings("color", declarations[0].name); + try testing.expectEqualStrings("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.expect(!declarations[1].is_important); + + try testing.expectEqualStrings("margin", declarations[2].name); + try testing.expectEqualStrings("5px", declarations[2].value); + try testing.expect(declarations[2].is_important); +} + +test "CSSParser - Quoted value with semicolon" { + const text = "content: \"Hello; world!\";"; + const allocator = testing.allocator; + + const declarations = try CSSParser.parseDeclarations(text, allocator); + defer allocator.free(declarations); + + try testing.expect(declarations.len == 1); + try testing.expectEqualStrings("content", declarations[0].name); + try testing.expectEqualStrings("\"Hello; world!\"", declarations[0].value); + try testing.expect(!declarations[0].is_important); +} + +test "CSSParser - URL value" { + const text = "background-image: url(\"test.png\");"; + const allocator = testing.allocator; + + const declarations = try CSSParser.parseDeclarations(text, allocator); + defer allocator.free(declarations); + + 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.expect(!declarations[0].is_important); +} + +test "CSSParser - Whitespace handling" { + const text = " color : purple ; margin : 10px ; "; + const allocator = testing.allocator; + + const declarations = try CSSParser.parseDeclarations(text, allocator); + defer allocator.free(declarations); + + 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); +} diff --git a/src/browser/cssom/css_style_declaration.zig b/src/browser/cssom/css_style_declaration.zig new file mode 100644 index 00000000..359d9dc9 --- /dev/null +++ b/src/browser/cssom/css_style_declaration.zig @@ -0,0 +1,236 @@ +// 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 CSSParser = @import("./css_parser.zig").CSSParser; +const CSSValueAnalyzer = @import("./css_value_analyzer.zig").CSSValueAnalyzer; +const SessionState = @import("../env.zig").SessionState; + +pub const Interfaces = .{ + CSSStyleDeclaration, + CSSRule, +}; + +const CSSRule = struct {}; + +pub const CSSStyleDeclaration = struct { + store: std.StringHashMapUnmanaged(Property), + order: std.ArrayListUnmanaged([]const u8), + + const Property = struct { + value: []const u8, + priority: bool, + }; + + pub fn get_cssFloat(self: *const CSSStyleDeclaration) []const u8 { + return self._getPropertyValue("float"); + } + + pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, state: *SessionState) !void { + const final_value = value orelse ""; + return self._setProperty("float", final_value, null, state); + } + + pub fn get_cssText(self: *const CSSStyleDeclaration, state: *SessionState) ![]const u8 { + var buffer = std.ArrayList(u8).init(state.arena); + const writer = buffer.writer(); + for (self.order.items) |name| { + const prop = self.store.get(name).?; + const escaped = try CSSValueAnalyzer.escapeCSSValue(state.arena, prop.value); + try writer.print("{s}: {s}", .{ name, escaped }); + if (prop.priority) try writer.writeAll(" !important"); + try writer.writeAll("; "); + } + return buffer.toOwnedSlice(); + } + + // 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); + + for (declarations) |decl| { + if (!CSSValueAnalyzer.isValidPropertyName(decl.name)) continue; + const priority: ?[]const u8 = if (decl.is_important) "important" else null; + try self._setProperty(decl.name, decl.value, priority, state); + } + } + + pub fn get_length(self: *const CSSStyleDeclaration) usize { + return self.order.items.len; + } + + pub fn get_parentRule() ?CSSRule { + return null; + } + + pub fn _getPropertyPriority(self: *const CSSStyleDeclaration, name: []const u8) []const u8 { + return if (self.store.get(name)) |prop| (if (prop.priority) "important" else "") else ""; + } + + // TODO should handle properly shorthand properties and canonical forms + pub fn _getPropertyValue(self: *const CSSStyleDeclaration, name: []const u8) []const u8 { + return if (self.store.get(name)) |prop| prop.value else ""; + } + + pub fn _item(self: *const CSSStyleDeclaration, index: usize) []const u8 { + return if (index < self.order.items.len) self.order.items[index] else ""; + } + + 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); + _ = 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; + } + } + return value_copy; + } + return ""; + } + + pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, state: *SessionState) !void { + const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important"); + + const value_copy = try state.arena.dupe(u8, value); + + if (!self.store.contains(name)) { + const name_copy = try state.arena.dupe(u8, name); + 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 }); + } + } +}; + +const testing = @import("../../testing.zig"); + +test "CSSOM.CSSStyleDeclaration" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let style = document.getElementById('content').style", "undefined" }, + .{ "style.cssText = 'color: red; font-size: 12px; margin: 5px !important;'", "color: red; font-size: 12px; margin: 5px !important;" }, + .{ "style.length", "3" }, + }, .{}); + + try runner.testCases(&.{ + .{ "style.getPropertyValue('color')", "red" }, + .{ "style.getPropertyValue('font-size')", "12px" }, + .{ "style.getPropertyValue('unknown-property')", "" }, + + .{ "style.getPropertyPriority('margin')", "important" }, + .{ "style.getPropertyPriority('color')", "" }, + .{ "style.getPropertyPriority('unknown-property')", "" }, + + .{ "style.item(0)", "color" }, + .{ "style.item(1)", "font-size" }, + .{ "style.item(2)", "margin" }, + .{ "style.item(3)", "" }, + }, .{}); + + try runner.testCases(&.{ + .{ "style.setProperty('background-color', 'blue')", "undefined" }, + .{ "style.getPropertyValue('background-color')", "blue" }, + .{ "style.length", "4" }, + + .{ "style.setProperty('color', 'green')", "undefined" }, + .{ "style.getPropertyValue('color')", "green" }, + .{ "style.length", "4" }, + + .{ "style.setProperty('padding', '10px', 'important')", "undefined" }, + .{ "style.getPropertyValue('padding')", "10px" }, + .{ "style.getPropertyPriority('padding')", "important" }, + + .{ "style.setProperty('border', '1px solid black', 'IMPORTANT')", "undefined" }, + .{ "style.getPropertyPriority('border')", "important" }, + }, .{}); + + try runner.testCases(&.{ + .{ "style.removeProperty('color')", "green" }, + .{ "style.getPropertyValue('color')", "" }, + .{ "style.length", "5" }, + + .{ "style.removeProperty('unknown-property')", "" }, + }, .{}); + + try runner.testCases(&.{ + .{ "style.cssText.includes('font-size: 12px;')", "true" }, + .{ "style.cssText.includes('margin: 5px !important;')", "true" }, + .{ "style.cssText.includes('padding: 10px !important;')", "true" }, + .{ "style.cssText.includes('border: 1px solid black !important;')", "true" }, + + .{ "style.cssText = 'color: purple; text-align: center;'", "color: purple; text-align: center;" }, + .{ "style.length", "2" }, + .{ "style.getPropertyValue('color')", "purple" }, + .{ "style.getPropertyValue('text-align')", "center" }, + .{ "style.getPropertyValue('font-size')", "" }, + + .{ "style.setProperty('cont', 'Hello; world!')", "undefined" }, + .{ "style.getPropertyValue('cont')", "Hello; world!" }, + + .{ "style.cssText = 'content: \"Hello; world!\"; background-image: url(\"test.png\");'", "content: \"Hello; world!\"; background-image: url(\"test.png\");" }, + .{ "style.getPropertyValue('content')", "\"Hello; world!\"" }, + .{ "style.getPropertyValue('background-image')", "url(\"test.png\")" }, + }, .{}); + + try runner.testCases(&.{ + .{ "style.cssFloat", "" }, + .{ "style.cssFloat = 'left'", "left" }, + .{ "style.cssFloat", "left" }, + .{ "style.getPropertyValue('float')", "left" }, + + .{ "style.cssFloat = 'right'", "right" }, + .{ "style.cssFloat", "right" }, + + .{ "style.cssFloat = null", "null" }, + .{ "style.cssFloat", "" }, + }, .{}); + + try runner.testCases(&.{ + .{ "style.setProperty('display', '')", "undefined" }, + .{ "style.getPropertyValue('display')", "" }, + + .{ "style.cssText = ' color : purple ; margin : 10px ; '", " color : purple ; margin : 10px ; " }, + .{ "style.getPropertyValue('color')", "purple" }, + .{ "style.getPropertyValue('margin')", "10px" }, + + .{ "style.setProperty('border-bottom-left-radius', '5px')", "undefined" }, + .{ "style.getPropertyValue('border-bottom-left-radius')", "5px" }, + }, .{}); +} diff --git a/src/browser/cssom/css_value_analyzer.zig b/src/browser/cssom/css_value_analyzer.zig new file mode 100644 index 00000000..9b098aa8 --- /dev/null +++ b/src/browser/cssom/css_value_analyzer.zig @@ -0,0 +1,327 @@ +// 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"); + +pub const CSSValueAnalyzer = struct { + pub fn isNumericWithUnit(value: []const u8) bool { + if (value.len == 0) return false; + + if (!std.ascii.isDigit(value[0]) and + value[0] != '+' and value[0] != '-' and value[0] != '.') + { + return false; + } + + var i: usize = 0; + var has_digit = false; + var decimal_point = false; + + while (i < value.len) : (i += 1) { + const c = value[i]; + if (std.ascii.isDigit(c)) { + has_digit = true; + } else if (c == '.' and !decimal_point) { + decimal_point = true; + } else if ((c == 'e' or c == 'E') and has_digit) { + i += 1; + if (i < value.len and (value[i] == '+' or value[i] == '-')) { + i += 1; + } + var has_exp_digits = false; + while (i < value.len and std.ascii.isDigit(value[i])) : (i += 1) { + has_exp_digits = true; + } + if (!has_exp_digits) return false; + break; + } else if (c != '-' and c != '+') { + break; + } + } + + if (!has_digit) return false; + + if (i == value.len) return true; + + const unit = value[i..]; + return CSSKeywords.isValidUnit(unit); + } + + pub fn isHexColor(value: []const u8) bool { + if (!std.mem.startsWith(u8, value, "#")) return false; + + const hex_part = value[1..]; + if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) return false; + + for (hex_part) |c| { + if (!std.ascii.isHex(c)) return false; + } + + return true; + } + + pub fn isMultiValueProperty(value: []const u8) bool { + var parts = std.mem.splitAny(u8, value, " "); + var multi_value_parts: usize = 0; + var all_parts_valid = true; + + while (parts.next()) |part| { + if (part.len == 0) continue; + multi_value_parts += 1; + + const is_numeric = isNumericWithUnit(part); + const is_hex_color = isHexColor(part); + const is_known_keyword = CSSKeywords.isKnownKeyword(part); + const is_function = CSSKeywords.startsWithFunction(part); + + if (!is_numeric and !is_hex_color and !is_known_keyword and !is_function) { + all_parts_valid = false; + break; + } + } + + return multi_value_parts >= 2 and all_parts_valid; + } + + pub fn isAlreadyQuoted(value: []const u8) bool { + return value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or + (value[0] == '\'' and value[value.len - 1] == '\'')); + } + + pub fn isValidPropertyName(name: []const u8) bool { + if (name.len == 0) return false; + + 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 != '-') { + return false; + } + } + + return true; + } + + pub fn extractImportant(value: []const u8) struct { value: []const u8, is_important: bool } { + const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace); + + if (std.mem.endsWith(u8, trimmed, "!important")) { + const clean_value = std.mem.trim(u8, trimmed[0 .. trimmed.len - 10], &std.ascii.whitespace); + return .{ .value = clean_value, .is_important = true }; + } + + return .{ .value = trimmed, .is_important = false }; + } + + pub fn needsQuotes(value: []const u8) bool { + if (value.len == 0) return true; + if (isAlreadyQuoted(value)) return false; + + const has_spaces = std.mem.indexOf(u8, value, " ") != null; + const has_special_chars = CSSKeywords.containsSpecialChar(value); + const is_url = std.mem.startsWith(u8, value, "url("); + const is_function = CSSKeywords.startsWithFunction(value); + + const space_requires_quotes = has_spaces and + !isMultiValueProperty(value) and + !is_url and + !is_function; + + return has_special_chars or space_requires_quotes; + } + + 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(); + + if (isAlreadyQuoted(value)) { + try writer.writeAll(value); + return out.toOwnedSlice(); + } + + const needs_quotes = needsQuotes(value); + + if (needs_quotes) { + try writer.writeByte('"'); + + for (value) |c| { + switch (c) { + '"' => try writer.writeAll("\\\""), + '\\' => try writer.writeAll("\\\\"), + '\n' => try writer.writeAll("\\A "), + '\r' => try writer.writeAll("\\D "), + '\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])) { + try writer.writeByte(' '); + } + }, + else => try writer.writeByte(c), + } + } + + try writer.writeByte('"'); + } else { + try writer.writeAll(value); + } + + return out.toOwnedSlice(); + } + + pub fn isKnownKeyword(value: []const u8) bool { + return CSSKeywords.isKnownKeyword(value); + } + + pub fn containsSpecialChar(value: []const u8) bool { + return CSSKeywords.containsSpecialChar(value); + } +}; + +const CSSKeywords = struct { + const BorderStyles = [_][]const u8{ + "none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset", + }; + + const ColorNames = [_][]const u8{ + "black", "white", "red", "green", "blue", "yellow", "purple", "gray", "transparent", + "currentColor", "inherit", + }; + + const PositionKeywords = [_][]const u8{ + "auto", "center", "left", "right", "top", "bottom", + }; + + const BackgroundRepeat = [_][]const u8{ + "repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round", + }; + + const FontStyles = [_][]const u8{ + "normal", "italic", "oblique", "bold", "bolder", "lighter", + }; + + const FontSizes = [_][]const u8{ + "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", + "smaller", "larger", + }; + + const FontFamilies = [_][]const u8{ + "serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", + }; + + const CSSGlobal = [_][]const u8{ + "initial", "inherit", "unset", "revert", + }; + + const DisplayValues = [_][]const u8{ + "block", "inline", "inline-block", "flex", "grid", "none", + }; + + const LengthUnits = [_][]const u8{ + "px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm", + "ex", "ch", "fr", + }; + + const AngleUnits = [_][]const u8{ + "deg", "rad", "grad", "turn", + }; + + const TimeUnits = [_][]const u8{ + "s", "ms", + }; + + const FrequencyUnits = [_][]const u8{ + "Hz", "kHz", + }; + + const ResolutionUnits = [_][]const u8{ + "dpi", "dpcm", "dppx", + }; + + const SpecialChars = [_]u8{ + '"', '\'', ';', '{', '}', '\\', '<', '>', '/', + }; + + 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, + }; + + for (all_categories) |category| { + for (category) |keyword| { + if (std.mem.eql(u8, value, keyword)) { + return true; + } + } + } + + return false; + } + + pub fn containsSpecialChar(value: []const u8) bool { + for (value) |c| { + for (SpecialChars) |special| { + if (c == special) { + return true; + } + } + } + return false; + } + + pub fn isValidUnit(unit: []const u8) bool { + const all_units = [_][]const []const u8{ + &LengthUnits, &AngleUnits, &TimeUnits, &FrequencyUnits, &ResolutionUnits, + }; + + for (all_units) |category| { + for (category) |valid_unit| { + if (std.mem.eql(u8, unit, valid_unit)) { + return true; + } + } + } + + return false; + } + + 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; + } + } + + return std.mem.indexOf(u8, value, "(") != null and + std.mem.indexOf(u8, value, ")") != null; + } +}; diff --git a/src/browser/env.zig b/src/browser/env.zig index e45ff75d..df747ac8 100644 --- a/src/browser/env.zig +++ b/src/browser/env.zig @@ -27,6 +27,7 @@ const WebApis = struct { pub const Interfaces = generate.Tuple(.{ @import("crypto/crypto.zig").Crypto, @import("console/console.zig").Console, + @import("cssom/css_style_declaration.zig").Interfaces, @import("dom/dom.zig").Interfaces, @import("encoding/text_encoder.zig").Interfaces, @import("events/event.zig").Interfaces, diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index b5f62f8a..f34f8330 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -25,6 +25,8 @@ const URL = @import("../url/url.zig").URL; const Node = @import("../dom/node.zig").Node; const Element = @import("../dom/element.zig").Element; +const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration; + // HTMLElement interfaces pub const Interfaces = .{ HTMLElement, @@ -92,7 +94,6 @@ pub const Interfaces = .{ HTMLTrackElement, HTMLUListElement, HTMLVideoElement, - CSSProperties, @import("form.zig").HTMLFormElement, @import("select.zig").HTMLSelectElement, @@ -103,15 +104,19 @@ pub const Union = generate.Union(Interfaces); // Abstract class // -------------- -const CSSProperties = struct {}; - pub const HTMLElement = struct { pub const Self = parser.ElementHTML; pub const prototype = *Element; pub const subtype = .node; - pub fn get_style(_: *parser.ElementHTML) CSSProperties { - return .{}; + style: CSSStyleDeclaration = .{ + .store = .{}, + .order = .{}, + }, + + pub fn get_style(e: *parser.ElementHTML, state: *SessionState) !*CSSStyleDeclaration { + const self = try state.getNodeWrapper(HTMLElement, @ptrCast(e)); + return &self.style; } pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {