Added support for CSSStyleDeclaration API

This commit is contained in:
Raph
2025-05-22 01:51:03 +02:00
parent 6506fa792d
commit 913568aba2
5 changed files with 882 additions and 5 deletions

View File

@@ -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);
}

View File

@@ -0,0 +1,236 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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" },
}, .{});
}

View File

@@ -0,0 +1,327 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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;
}
};

View File

@@ -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,

View File

@@ -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 {