mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
Added support for CSSStyleDeclaration API
This commit is contained in:
308
src/browser/cssom/css_parser.zig
Normal file
308
src/browser/cssom/css_parser.zig
Normal 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);
|
||||
}
|
||||
236
src/browser/cssom/css_style_declaration.zig
Normal file
236
src/browser/cssom/css_style_declaration.zig
Normal 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" },
|
||||
}, .{});
|
||||
}
|
||||
327
src/browser/cssom/css_value_analyzer.zig
Normal file
327
src/browser/cssom/css_value_analyzer.zig
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user