mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
various fixes according to PR review
This commit is contained in:
@@ -1,36 +1,44 @@
|
||||
// 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 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);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("test<angle"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test>angle"));
|
||||
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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user