mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-28 08:36:17 +08:00
Merge pull request #4200 from lightpanda-io/color-parsing
css: add color parsing
This commit is contained in:
3 files changed
+911
-9
No files matched your search
+846
-2
@@ -19,6 +19,8 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
const Tokenizer = @import("css/Tokenizer.zig");
|
||||
|
||||
fn isHexColor(value: []const u8) bool {
|
||||
if (value.len == 0) {
|
||||
return false;
|
||||
@@ -199,13 +201,13 @@ pub const RGBA = packed struct(u32) {
|
||||
pub const yellowgreen: RGBA = .init(154, 205, 50, 1);
|
||||
};
|
||||
|
||||
pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA {
|
||||
fn init(r: u8, g: u8, b: u8, a: f32) RGBA {
|
||||
const clamped = std.math.clamp(a, 0, 1);
|
||||
return .{ .r = r, .g = g, .b = b, .a = @trunc(clamped * 255) };
|
||||
}
|
||||
|
||||
/// Finds a color by its name.
|
||||
pub fn find(name: []const u8) ?RGBA {
|
||||
fn find(name: []const u8) ?RGBA {
|
||||
const match = std.meta.stringToEnum(std.meta.DeclEnum(Named), name) orelse return null;
|
||||
|
||||
return switch (match) {
|
||||
@@ -296,3 +298,845 @@ pub const RGBA = packed struct(u32) {
|
||||
return @as(f32, @floatFromInt(self.a)) / 255;
|
||||
}
|
||||
};
|
||||
|
||||
// CSS <color> values: parsing and the CSSOM serializations of the sRGB forms
|
||||
// (named, hex, rgb(), hsl(), hwb()).
|
||||
// https://drafts.csswg.org/css-color-4/#serializing-sRGB-values
|
||||
|
||||
/// What `parse` made of a CSS `<color>` value.
|
||||
const Parsed = union(enum) {
|
||||
invalid,
|
||||
// possibly valid, but we don't support it (e.g. lab(), color-mix(), var())
|
||||
// written as-is.
|
||||
unsupported,
|
||||
value: Value,
|
||||
};
|
||||
|
||||
const Value = union(enum) {
|
||||
/// A named color or `transparent`.
|
||||
named: struct { name: []const u8, rgb: Rgb },
|
||||
|
||||
/// `currentcolor`, a system color or a CSS-wide keyword, lowercased.
|
||||
keyword: []const u8,
|
||||
|
||||
rgb: Rgb,
|
||||
|
||||
/// An rgb()/hsl()/hwb() with a `none` component.
|
||||
missing: Missing,
|
||||
|
||||
pub fn writeSpecified(self: Value, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
switch (self) {
|
||||
.named => |n| try writer.writeAll(n.name),
|
||||
.keyword => |k| try writer.writeAll(k),
|
||||
.rgb => |rgb| try rgb.writeLegacy(writer),
|
||||
.missing => |m| switch (m.function) {
|
||||
// Legacy serialization, with none as 0.
|
||||
.rgb => try (Rgb{
|
||||
.r = m.channels[0] orelse 0,
|
||||
.g = m.channels[1] orelse 0,
|
||||
.b = m.channels[2] orelse 0,
|
||||
.alpha = m.alpha orelse 0,
|
||||
}).writeLegacy(writer),
|
||||
.hsl, .hwb => try m.writeModern(writer, ""),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeComputed(self: Value, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
switch (self) {
|
||||
.named => |n| try n.rgb.writeLegacy(writer),
|
||||
.keyword => |k| try writer.writeAll(k),
|
||||
.rgb => |rgb| try rgb.writeLegacy(writer),
|
||||
.missing => |m| switch (m.function) {
|
||||
// Missing components survive computation, which legacy
|
||||
// rgb() can't express.
|
||||
.rgb => {
|
||||
try writer.writeAll("color(srgb");
|
||||
for (m.channels) |channel| {
|
||||
try writer.writeByte(' ');
|
||||
try writeComponent(writer, if (channel) |c| c / 255 else null, 8, "");
|
||||
}
|
||||
try m.writeAlpha(writer);
|
||||
try writer.writeByte(')');
|
||||
},
|
||||
.hsl, .hwb => try m.writeModern(writer, "%"),
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Channels 0-255 and alpha 0-1, both clamped.
|
||||
const Rgb = struct {
|
||||
r: f64,
|
||||
g: f64,
|
||||
b: f64,
|
||||
alpha: f64 = 1,
|
||||
|
||||
fn writeLegacy(self: Rgb, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
const r = channelByte(self.r);
|
||||
const g = channelByte(self.g);
|
||||
const b = channelByte(self.b);
|
||||
if (self.alpha == 1) {
|
||||
return writer.print("rgb({d}, {d}, {d})", .{ r, g, b });
|
||||
}
|
||||
try writer.print("rgba({d}, {d}, {d}, ", .{ r, g, b });
|
||||
try writeNumber(writer, self.alpha, 6);
|
||||
try writer.writeByte(')');
|
||||
}
|
||||
|
||||
fn fromRGBA(rgba: RGBA) Rgb {
|
||||
return .{
|
||||
.r = @floatFromInt(rgba.r),
|
||||
.g = @floatFromInt(rgba.g),
|
||||
.b = @floatFromInt(rgba.b),
|
||||
.alpha = byteAlpha(rgba.a),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const Missing = struct {
|
||||
function: enum { rgb, hsl, hwb },
|
||||
/// rgb: 0-255. hsl/hwb: hue in degrees, then two 0-100 values.
|
||||
channels: [3]?f64,
|
||||
alpha: ?f64,
|
||||
|
||||
fn writeModern(self: Missing, writer: *Io.Writer, percent: []const u8) Io.Writer.Error!void {
|
||||
try writer.writeAll(@tagName(self.function));
|
||||
try writer.writeByte('(');
|
||||
try writeComponent(writer, self.channels[0], 6, "");
|
||||
try writer.writeByte(' ');
|
||||
try writeComponent(writer, self.channels[1], 6, percent);
|
||||
try writer.writeByte(' ');
|
||||
try writeComponent(writer, self.channels[2], 6, percent);
|
||||
try self.writeAlpha(writer);
|
||||
try writer.writeByte(')');
|
||||
}
|
||||
|
||||
fn writeAlpha(self: Missing, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
if (self.alpha == 1) return;
|
||||
try writer.writeAll(" / ");
|
||||
try writeComponent(writer, self.alpha, 6, "");
|
||||
}
|
||||
};
|
||||
|
||||
fn writeComponent(writer: *Io.Writer, value: ?f64, decimals: usize, unit: []const u8) Io.Writer.Error!void {
|
||||
const v = value orelse return writer.writeAll("none");
|
||||
try writeNumber(writer, v, decimals);
|
||||
try writer.writeAll(unit);
|
||||
}
|
||||
|
||||
fn writeNumber(writer: *Io.Writer, value: f64, decimals: usize) Io.Writer.Error!void {
|
||||
var buf: [400]u8 = undefined;
|
||||
var str: []const u8 = std.fmt.bufPrint(&buf, "{d:.[1]}", .{ value, decimals }) catch unreachable;
|
||||
if (std.mem.indexOfScalar(u8, str, '.') != null) {
|
||||
str = std.mem.trimEnd(u8, std.mem.trimEnd(u8, str, "0"), ".");
|
||||
}
|
||||
try writer.writeAll(if (std.mem.eql(u8, str, "-0")) "0" else str);
|
||||
}
|
||||
|
||||
fn channelByte(value: f64) u8 {
|
||||
// The epsilon keeps a computed exact half (hwb(320 30% 40%)'s blue is
|
||||
// 127.5) from rounding down after float error.
|
||||
return @intFromFloat(@floor(std.math.clamp(value, 0, 255) + 0.5 + 1e-6));
|
||||
}
|
||||
|
||||
// An 8-bit alpha serializes as the shortest decimal that maps back to the
|
||||
// same byte: #ffffff80 is rgba(255, 255, 255, 0.5).
|
||||
fn byteAlpha(a: u8) f64 {
|
||||
const byte: f64 = @floatFromInt(a);
|
||||
const two = @round(byte / 255 * 100) / 100;
|
||||
if (@round(two * 255) == byte) {
|
||||
return two;
|
||||
}
|
||||
return @round(byte / 255 * 1000) / 1000;
|
||||
}
|
||||
|
||||
pub fn parse(input: []const u8) Parsed {
|
||||
if (usesSubstitution(input)) {
|
||||
return .unsupported;
|
||||
}
|
||||
|
||||
var stream: Stream = .{ .tokenizer = .{ .input = input } };
|
||||
const token = stream.next() orelse return .invalid;
|
||||
const parsed: Parsed = switch (token) {
|
||||
.ident => |raw| parseKeyword(raw),
|
||||
.id_hash, .unrestricted_hash => |hex| parseHex(hex),
|
||||
.function => |raw| parseFunction(raw, &stream),
|
||||
else => .invalid,
|
||||
};
|
||||
if (parsed != .value) {
|
||||
return parsed;
|
||||
}
|
||||
if (stream.next() != null) {
|
||||
return .invalid;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const Stream = struct {
|
||||
tokenizer: Tokenizer,
|
||||
peeked: ?Tokenizer.Token = null,
|
||||
|
||||
fn next(self: *Stream) ?Tokenizer.Token {
|
||||
if (self.peeked) |token| {
|
||||
self.peeked = null;
|
||||
return token;
|
||||
}
|
||||
while (self.tokenizer.next()) |token| {
|
||||
switch (token) {
|
||||
.white_space, .comment => {},
|
||||
else => return token,
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn peek(self: *Stream) ?Tokenizer.Token {
|
||||
if (self.peeked == null) {
|
||||
self.peeked = self.next();
|
||||
}
|
||||
return self.peeked;
|
||||
}
|
||||
};
|
||||
|
||||
// var() and friends substitute at computed-value time; the value can't be
|
||||
// checked before then.
|
||||
fn usesSubstitution(input: []const u8) bool {
|
||||
var tokenizer: Tokenizer = .{ .input = input };
|
||||
while (tokenizer.next()) |token| {
|
||||
const raw = switch (token) {
|
||||
.function => |raw| raw,
|
||||
else => continue,
|
||||
};
|
||||
var buf: [8]u8 = undefined;
|
||||
const name = lowerName(raw, &buf) orelse continue;
|
||||
for ([_][]const u8{ "var", "env", "attr", "if", "inherit" }) |substitution| {
|
||||
if (std.mem.eql(u8, name, substitution)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The ASCII-lowercased ident with escapes decoded, or null when it doesn't
|
||||
/// fit `buf` or decodes to a non-ASCII code point (no keyword we match has
|
||||
/// one).
|
||||
fn lowerName(raw: []const u8, buf: []u8) ?[]const u8 {
|
||||
var len: usize = 0;
|
||||
var i: usize = 0;
|
||||
while (i < raw.len) {
|
||||
var c = raw[i];
|
||||
i += 1;
|
||||
if (c == '\\' and i < raw.len) {
|
||||
if (std.ascii.isHex(raw[i])) {
|
||||
var code: u32 = 0;
|
||||
var digits: usize = 0;
|
||||
while (i < raw.len and digits < 6 and std.ascii.isHex(raw[i])) : (digits += 1) {
|
||||
code = code * 16 + (std.fmt.charToDigit(raw[i], 16) catch unreachable);
|
||||
i += 1;
|
||||
}
|
||||
if (i < raw.len and std.ascii.isWhitespace(raw[i])) {
|
||||
i += 1;
|
||||
}
|
||||
if (code == 0 or code > 127) {
|
||||
return null;
|
||||
}
|
||||
c = @intCast(code);
|
||||
} else {
|
||||
c = raw[i];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
if (len == buf.len) {
|
||||
return null;
|
||||
}
|
||||
buf[len] = std.ascii.toLower(c);
|
||||
len += 1;
|
||||
}
|
||||
return buf[0..len];
|
||||
}
|
||||
|
||||
const keywords = [_][]const u8{
|
||||
"currentcolor",
|
||||
// CSS-wide keywords
|
||||
"inherit",
|
||||
"initial",
|
||||
"unset",
|
||||
"revert",
|
||||
"revert-layer",
|
||||
// System colors
|
||||
"accentcolor",
|
||||
"accentcolortext",
|
||||
"activetext",
|
||||
"buttonborder",
|
||||
"buttonface",
|
||||
"buttontext",
|
||||
"canvas",
|
||||
"canvastext",
|
||||
"field",
|
||||
"fieldtext",
|
||||
"graytext",
|
||||
"highlight",
|
||||
"highlighttext",
|
||||
"linktext",
|
||||
"mark",
|
||||
"marktext",
|
||||
"selecteditem",
|
||||
"selecteditemtext",
|
||||
"visitedtext",
|
||||
// Deprecated system colors
|
||||
"activeborder",
|
||||
"activecaption",
|
||||
"appworkspace",
|
||||
"background",
|
||||
"buttonhighlight",
|
||||
"buttonshadow",
|
||||
"captiontext",
|
||||
"inactiveborder",
|
||||
"inactivecaption",
|
||||
"inactivecaptiontext",
|
||||
"infobackground",
|
||||
"infotext",
|
||||
"menu",
|
||||
"menutext",
|
||||
"scrollbar",
|
||||
"threeddarkshadow",
|
||||
"threedface",
|
||||
"threedhighlight",
|
||||
"threedlightshadow",
|
||||
"threedshadow",
|
||||
"window",
|
||||
"windowframe",
|
||||
"windowtext",
|
||||
};
|
||||
|
||||
fn parseKeyword(raw: []const u8) Parsed {
|
||||
// Vendor keywords (-webkit-link, ...)
|
||||
if (std.mem.startsWith(u8, raw, "-")) return .unsupported;
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
const name = lowerName(raw, &buf) orelse return .invalid;
|
||||
if (std.meta.stringToEnum(std.meta.DeclEnum(RGBA.Named), name)) |match| {
|
||||
const rgba = switch (match) {
|
||||
inline else => |comptime_enum| @field(RGBA.Named, @tagName(comptime_enum)),
|
||||
};
|
||||
return .{ .value = .{ .named = .{ .name = @tagName(match), .rgb = .fromRGBA(rgba) } } };
|
||||
}
|
||||
for (keywords) |keyword| {
|
||||
if (std.mem.eql(u8, name, keyword)) {
|
||||
return .{ .value = .{ .keyword = keyword } };
|
||||
}
|
||||
}
|
||||
return .invalid;
|
||||
}
|
||||
|
||||
fn parseHex(hex: []const u8) Parsed {
|
||||
var buf: [9]u8 = undefined;
|
||||
if (hex.len >= buf.len) {
|
||||
return .invalid;
|
||||
}
|
||||
buf[0] = '#';
|
||||
@memcpy(buf[1..][0..hex.len], hex);
|
||||
const rgba = RGBA.parse(buf[0 .. hex.len + 1]) catch return .invalid;
|
||||
return .{ .value = .{ .rgb = .fromRGBA(rgba) } };
|
||||
}
|
||||
|
||||
// Color functions we don't parse but mustn't reject.
|
||||
const unsupported_functions = [_][]const u8{
|
||||
"lab", "lch", "oklab", "oklch", "color", "color-mix",
|
||||
"light-dark", "alpha", "device-cmyk", "color-layers", "contrast-color",
|
||||
};
|
||||
|
||||
fn parseFunction(raw: []const u8, stream: *Stream) Parsed {
|
||||
if (std.mem.startsWith(u8, raw, "-")) {
|
||||
return .unsupported;
|
||||
}
|
||||
|
||||
var buf: [16]u8 = undefined;
|
||||
const name = lowerName(raw, &buf) orelse return .invalid;
|
||||
if (std.mem.eql(u8, name, "rgb") or std.mem.eql(u8, name, "rgba")) {
|
||||
return parseRgb(stream) catch |err| fromError(err);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "hsl") or std.mem.eql(u8, name, "hsla")) {
|
||||
return parseHslLike(stream, .hsl) catch |err| fromError(err);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "hwb")) {
|
||||
return parseHslLike(stream, .hwb) catch |err| fromError(err);
|
||||
}
|
||||
for (unsupported_functions) |unsupported| {
|
||||
if (std.mem.eql(u8, name, unsupported)) {
|
||||
return .unsupported;
|
||||
}
|
||||
}
|
||||
return .invalid;
|
||||
}
|
||||
|
||||
const ParseError = error{ Invalid, Unsupported };
|
||||
|
||||
fn fromError(err: ParseError) Parsed {
|
||||
return switch (err) {
|
||||
error.Invalid => .invalid,
|
||||
error.Unsupported => .unsupported,
|
||||
};
|
||||
}
|
||||
|
||||
/// A function argument. Percentages are in percent (50% is 50), angles in
|
||||
/// degrees.
|
||||
const Component = union(enum) {
|
||||
number: f64,
|
||||
percent: f64,
|
||||
angle: f64,
|
||||
none,
|
||||
};
|
||||
|
||||
const Arg = union(enum) {
|
||||
comma,
|
||||
slash,
|
||||
value: Component,
|
||||
};
|
||||
|
||||
/// The arguments of a color function: legacy (comma-separated, no `none`)
|
||||
/// or modern (space-separated, `/ alpha`).
|
||||
const Args = struct {
|
||||
legacy: bool,
|
||||
channels: [3]Component,
|
||||
alpha: ?Component,
|
||||
};
|
||||
|
||||
fn readArgs(stream: *Stream) ParseError!Args {
|
||||
var buf: [8]Arg = undefined;
|
||||
var len: usize = 0;
|
||||
while (stream.next()) |token| {
|
||||
const arg: Arg = switch (token) {
|
||||
.close_parenthesis => break,
|
||||
.comma => .comma,
|
||||
.delim => |d| if (d == '/') .slash else return error.Invalid,
|
||||
.ident => |raw| blk: {
|
||||
var name_buf: [8]u8 = undefined;
|
||||
const name = lowerName(raw, &name_buf) orelse return error.Invalid;
|
||||
if (std.mem.eql(u8, name, "none")) {
|
||||
break :blk .{ .value = .none };
|
||||
}
|
||||
if (std.mem.eql(u8, name, "from")) {
|
||||
// Relative color syntax
|
||||
return error.Unsupported;
|
||||
}
|
||||
return error.Invalid;
|
||||
},
|
||||
.function => |raw| .{ .value = try calcFunction(raw, stream) },
|
||||
else => .{ .value = try numeric(token) },
|
||||
};
|
||||
if (len == buf.len) return error.Invalid;
|
||||
buf[len] = arg;
|
||||
len += 1;
|
||||
}
|
||||
// An unclosed function at the end of the input closes there.
|
||||
|
||||
const args = buf[0..len];
|
||||
var comma_count: usize = 0;
|
||||
for (args) |arg| {
|
||||
if (arg == .comma) {
|
||||
comma_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (comma_count > 0) {
|
||||
if (len != 5 and len != 7) {
|
||||
return error.Invalid;
|
||||
}
|
||||
var values: [4]Component = undefined;
|
||||
for (0..len / 2 + 1) |i| {
|
||||
values[i] = switch (args[i * 2]) {
|
||||
.value => |v| if (v == .none) return error.Invalid else v,
|
||||
else => return error.Invalid,
|
||||
};
|
||||
if (i * 2 + 1 < len and args[i * 2 + 1] != .comma) {
|
||||
return error.Invalid;
|
||||
}
|
||||
}
|
||||
return .{
|
||||
.legacy = true,
|
||||
.channels = values[0..3].*,
|
||||
.alpha = if (len == 7) values[3] else null,
|
||||
};
|
||||
}
|
||||
|
||||
if (len != 3 and len != 5) {
|
||||
return error.Invalid;
|
||||
}
|
||||
var values: [4]Component = undefined;
|
||||
for (args, 0..) |arg, i| {
|
||||
if (i == 3) {
|
||||
if (arg != .slash) {
|
||||
return error.Invalid;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
values[if (i == 4) 3 else i] = switch (arg) {
|
||||
.value => |v| v,
|
||||
else => return error.Invalid,
|
||||
};
|
||||
}
|
||||
return .{
|
||||
.legacy = false,
|
||||
.channels = values[0..3].*,
|
||||
.alpha = if (len == 5) values[3] else null,
|
||||
};
|
||||
}
|
||||
|
||||
fn numeric(token: Tokenizer.Token) ParseError!Component {
|
||||
switch (token) {
|
||||
.number => |n| return .{ .number = widen(n.value) },
|
||||
.percentage => |p| return .{
|
||||
.percent = if (p.int_value) |int| @floatFromInt(int) else widen(p.unit_value) * 100,
|
||||
},
|
||||
.dimension => |d| {
|
||||
const value = widen(d.value);
|
||||
var buf: [8]u8 = undefined;
|
||||
const unit = lowerName(d.unit, &buf) orelse return error.Invalid;
|
||||
if (std.mem.eql(u8, unit, "deg")) {
|
||||
return .{ .angle = value };
|
||||
}
|
||||
if (std.mem.eql(u8, unit, "grad")) {
|
||||
return .{ .angle = value * 0.9 };
|
||||
}
|
||||
if (std.mem.eql(u8, unit, "rad")) {
|
||||
return .{ .angle = std.math.radiansToDegrees(value) };
|
||||
}
|
||||
if (std.mem.eql(u8, unit, "turn")) {
|
||||
return .{ .angle = value * 360 };
|
||||
}
|
||||
return error.Invalid;
|
||||
},
|
||||
else => return error.Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
// The tokenizer's f32, as the shortest decimal that reads back the same: 0.8
|
||||
// stays 0.8, not 0.800000011920929.
|
||||
fn widen(value: f32) f64 {
|
||||
var buf: [64]u8 = undefined;
|
||||
const str = std.fmt.bufPrint(&buf, "{d}", .{value}) catch return value;
|
||||
return std.fmt.parseFloat(f64, str) catch value;
|
||||
}
|
||||
|
||||
// A calc() whose operands are all numbers, percentages or angles. Anything
|
||||
// needing layout (1em, sign(), min()) is left unevaluated.
|
||||
fn calcFunction(raw: []const u8, stream: *Stream) ParseError!Component {
|
||||
var buf: [8]u8 = undefined;
|
||||
const name = lowerName(raw, &buf) orelse return error.Unsupported;
|
||||
if (std.mem.eql(u8, name, "calc") == false) {
|
||||
return error.Unsupported;
|
||||
}
|
||||
|
||||
var result = try calcBlock(stream);
|
||||
// NaN is censored to zero at the top level of a calculation.
|
||||
switch (result) {
|
||||
.number, .percent, .angle => |*v| if (std.math.isNan(v.*)) {
|
||||
v.* = 0;
|
||||
},
|
||||
.none => unreachable,
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// A sum, closed by `)` (or the end of the input).
|
||||
fn calcBlock(stream: *Stream) ParseError!Component {
|
||||
const result = try calcSum(stream);
|
||||
if (stream.next()) |token| {
|
||||
if (token != .close_parenthesis) {
|
||||
return error.Invalid;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn calcSum(stream: *Stream) ParseError!Component {
|
||||
var acc = try calcProduct(stream);
|
||||
while (stream.peek()) |token| {
|
||||
const op = switch (token) {
|
||||
.delim => |d| if (d == '+' or d == '-') d else return acc,
|
||||
else => return acc,
|
||||
};
|
||||
_ = stream.next();
|
||||
const rhs = try calcProduct(stream);
|
||||
if (std.meta.activeTag(acc) != std.meta.activeTag(rhs)) {
|
||||
return error.Invalid;
|
||||
}
|
||||
const value = if (op == '+') magnitude(acc) + magnitude(rhs) else magnitude(acc) - magnitude(rhs);
|
||||
acc = withMagnitude(acc, value);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
fn calcProduct(stream: *Stream) ParseError!Component {
|
||||
var acc = try calcValue(stream);
|
||||
while (stream.peek()) |token| {
|
||||
const op = switch (token) {
|
||||
.delim => |d| if (d == '*' or d == '/') d else return acc,
|
||||
else => return acc,
|
||||
};
|
||||
_ = stream.next();
|
||||
const rhs = try calcValue(stream);
|
||||
if (op == '*') {
|
||||
const value = magnitude(acc) * magnitude(rhs);
|
||||
if (acc == .number) {
|
||||
acc = withMagnitude(rhs, value);
|
||||
} else if (rhs == .number) {
|
||||
acc = withMagnitude(acc, value);
|
||||
} else {
|
||||
return error.Invalid;
|
||||
}
|
||||
} else {
|
||||
if (rhs != .number) return error.Invalid;
|
||||
acc = withMagnitude(acc, magnitude(acc) / magnitude(rhs));
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
fn calcValue(stream: *Stream) ParseError!Component {
|
||||
const token = stream.next() orelse return error.Invalid;
|
||||
switch (token) {
|
||||
.parenthesis_block => return calcBlock(stream),
|
||||
.function => |raw| {
|
||||
var buf: [8]u8 = undefined;
|
||||
const name = lowerName(raw, &buf) orelse return error.Unsupported;
|
||||
if (std.mem.eql(u8, name, "calc") == false) {
|
||||
return error.Unsupported;
|
||||
}
|
||||
return calcBlock(stream);
|
||||
},
|
||||
.ident => |raw| {
|
||||
var buf: [16]u8 = undefined;
|
||||
const name = lowerName(raw, &buf) orelse return error.Invalid;
|
||||
const constants = [_]struct { []const u8, f64 }{
|
||||
.{ "e", std.math.e },
|
||||
.{ "pi", std.math.pi },
|
||||
.{ "infinity", std.math.inf(f64) },
|
||||
.{ "-infinity", -std.math.inf(f64) },
|
||||
.{ "nan", std.math.nan(f64) },
|
||||
};
|
||||
for (constants) |constant| {
|
||||
if (std.mem.eql(u8, name, constant[0])) {
|
||||
return .{ .number = constant[1] };
|
||||
}
|
||||
}
|
||||
return error.Invalid;
|
||||
},
|
||||
else => return numeric(token),
|
||||
}
|
||||
}
|
||||
|
||||
fn magnitude(component: Component) f64 {
|
||||
return switch (component) {
|
||||
.number, .percent, .angle => |v| v,
|
||||
.none => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn withMagnitude(component: Component, value: f64) Component {
|
||||
return switch (component) {
|
||||
.number => .{ .number = value },
|
||||
.percent => .{ .percent = value },
|
||||
.angle => .{ .angle = value },
|
||||
.none => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseRgb(stream: *Stream) ParseError!Parsed {
|
||||
const args = try readArgs(stream);
|
||||
if (args.legacy) {
|
||||
// Legacy channels are all numbers or all percentages.
|
||||
const tag = std.meta.activeTag(args.channels[0]);
|
||||
for (args.channels) |channel| {
|
||||
if (std.meta.activeTag(channel) != tag) {
|
||||
return error.Invalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var channels: [3]?f64 = undefined;
|
||||
var missing = false;
|
||||
for (args.channels, &channels) |component, *channel| {
|
||||
channel.* = switch (component) {
|
||||
.number => |n| std.math.clamp(n, 0, 255),
|
||||
.percent => |p| std.math.clamp(p / 100 * 255, 0, 255),
|
||||
.none => null,
|
||||
.angle => return error.Invalid,
|
||||
};
|
||||
if (channel.* == null) missing = true;
|
||||
}
|
||||
const alpha = try parseAlpha(args.alpha);
|
||||
if (missing or alpha == null) {
|
||||
return .{ .value = .{ .missing = .{ .function = .rgb, .channels = channels, .alpha = alpha } } };
|
||||
}
|
||||
return .{ .value = .{ .rgb = .{ .r = channels[0].?, .g = channels[1].?, .b = channels[2].?, .alpha = alpha.? } } };
|
||||
}
|
||||
|
||||
fn parseAlpha(component: ?Component) ParseError!?f64 {
|
||||
return switch (component orelse return 1) {
|
||||
.number => |n| std.math.clamp(n, 0, 1),
|
||||
.percent => |p| std.math.clamp(p / 100, 0, 1),
|
||||
.none => null,
|
||||
.angle => error.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseHslLike(stream: *Stream, function: @FieldType(Missing, "function")) ParseError!Parsed {
|
||||
const args = try readArgs(stream);
|
||||
if (args.legacy and function == .hwb) {
|
||||
return error.Invalid;
|
||||
}
|
||||
|
||||
var channels: [3]?f64 = undefined;
|
||||
channels[0] = switch (args.channels[0]) {
|
||||
// Hue normalization sends infinities to 0 too.
|
||||
.number, .angle => |h| if (std.math.isFinite(h)) h else 0,
|
||||
.none => null,
|
||||
.percent => return error.Invalid,
|
||||
};
|
||||
for (args.channels[1..], channels[1..]) |component, *channel| {
|
||||
channel.* = switch (component) {
|
||||
.percent => |p| p,
|
||||
// Legacy hsl() only takes percentages.
|
||||
.number => |n| if (args.legacy) return error.Invalid else n,
|
||||
.none => null,
|
||||
.angle => return error.Invalid,
|
||||
};
|
||||
}
|
||||
// Negative saturation clamps to 0 at parse time.
|
||||
if (function == .hsl) {
|
||||
if (channels[1]) |*s| s.* = @max(s.*, 0);
|
||||
}
|
||||
|
||||
const alpha = try parseAlpha(args.alpha);
|
||||
const hue = channels[0] orelse return missingValue(function, channels, alpha);
|
||||
const second = channels[1] orelse return missingValue(function, channels, alpha);
|
||||
const third = channels[2] orelse return missingValue(function, channels, alpha);
|
||||
const a = alpha orelse return missingValue(function, channels, alpha);
|
||||
|
||||
const rgb = switch (function) {
|
||||
.hsl => hslToRgb(hue, std.math.clamp(second / 100, 0, 1), std.math.clamp(third / 100, 0, 1)),
|
||||
.hwb => hwbToRgb(hue, @max(second / 100, 0), @max(third / 100, 0)),
|
||||
.rgb => unreachable,
|
||||
};
|
||||
return .{ .value = .{ .rgb = .{
|
||||
.r = std.math.clamp(rgb[0] * 255, 0, 255),
|
||||
.g = std.math.clamp(rgb[1] * 255, 0, 255),
|
||||
.b = std.math.clamp(rgb[2] * 255, 0, 255),
|
||||
.alpha = a,
|
||||
} } };
|
||||
}
|
||||
|
||||
fn missingValue(function: @FieldType(Missing, "function"), channels: [3]?f64, alpha: ?f64) Parsed {
|
||||
return .{ .value = .{ .missing = .{ .function = function, .channels = channels, .alpha = alpha } } };
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-color-4/#hsl-to-rgb
|
||||
fn hslToRgb(hue: f64, saturation: f64, lightness: f64) [3]f64 {
|
||||
var h = @mod(hue, 360);
|
||||
if (h < 0) h += 360;
|
||||
const a = saturation * @min(lightness, 1 - lightness);
|
||||
var rgb: [3]f64 = undefined;
|
||||
for ([_]f64{ 0, 8, 4 }, &rgb) |n, *channel| {
|
||||
const k = @mod(n + h / 30, 12);
|
||||
channel.* = lightness - a * @max(-1, @min(@min(k - 3, 9 - k), 1));
|
||||
}
|
||||
return rgb;
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-color-4/#hwb-to-rgb
|
||||
fn hwbToRgb(hue: f64, white: f64, black: f64) [3]f64 {
|
||||
if (white + black >= 1) {
|
||||
const gray = white / (white + black);
|
||||
return .{ gray, gray, gray };
|
||||
}
|
||||
var rgb = hslToRgb(hue, 1, 0.5);
|
||||
for (&rgb) |*channel| {
|
||||
channel.* = channel.* * (1 - white - black) + white;
|
||||
}
|
||||
return rgb;
|
||||
}
|
||||
|
||||
fn expectSerialized(input: []const u8, specified: ?[]const u8, computed: []const u8) !void {
|
||||
const parsed = parse(input);
|
||||
if (parsed != .value) {
|
||||
std.debug.print("{s}: {t}\n", .{ input, parsed });
|
||||
return error.TestUnexpectedResult;
|
||||
}
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: Io.Writer = .fixed(&buf);
|
||||
try parsed.value.writeSpecified(&writer);
|
||||
try std.testing.expectEqualStrings(specified orelse computed, writer.buffered());
|
||||
writer = .fixed(&buf);
|
||||
try parsed.value.writeComputed(&writer);
|
||||
try std.testing.expectEqualStrings(computed, writer.buffered());
|
||||
}
|
||||
|
||||
test "color: parse and serialize" {
|
||||
try expectSerialized("red", "red", "rgb(255, 0, 0)");
|
||||
try expectSerialized(" TransParent\n", "transparent", "rgba(0, 0, 0, 0)");
|
||||
try expectSerialized("/**/CyAn", "cyan", "rgb(0, 255, 255)");
|
||||
try expectSerialized("currentColor", "currentcolor", "currentcolor");
|
||||
try expectSerialized("ButtonFace", "buttonface", "buttonface");
|
||||
try expectSerialized("#234", null, "rgb(34, 51, 68)");
|
||||
try expectSerialized("#FEDCBA", null, "rgb(254, 220, 186)");
|
||||
try expectSerialized("#ffffff80", null, "rgba(255, 255, 255, 0.5)");
|
||||
try expectSerialized("rgb(-51, 306, 0)", null, "rgb(0, 255, 0)");
|
||||
try expectSerialized("rgb(42%, 3%, 50%)", null, "rgb(107, 8, 128)");
|
||||
try expectSerialized("rgb(/* R */0, /* G */51, /* B */255)", null, "rgb(0, 51, 255)");
|
||||
try expectSerialized("r\\67 b(00, 51, 102)", null, "rgb(0, 51, 102)");
|
||||
try expectSerialized("rgba(0, 51, 255, 0.42)", null, "rgba(0, 51, 255, 0.42)");
|
||||
try expectSerialized("rgba(0, 51, 255, 37)", null, "rgb(0, 51, 255)");
|
||||
try expectSerialized("rgb(250% 51 40%)", null, "rgb(255, 51, 102)");
|
||||
try expectSerialized("rgb(0 0 0 / 50%)", null, "rgba(0, 0, 0, 0.5)");
|
||||
try expectSerialized("rgb(calc(infinity), 0, 0)", null, "rgb(255, 0, 0)");
|
||||
try expectSerialized("rgba(0, 0, 0, calc(0 / 0))", null, "rgba(0, 0, 0, 0)");
|
||||
try expectSerialized("rgb(calc(10 + 20 * 2) 0 0)", null, "rgb(50, 0, 0)");
|
||||
try expectSerialized("rgb(128 none none)", "rgb(128, 0, 0)", "color(srgb 0.50196078 none none)");
|
||||
try expectSerialized("rgb(20% none none / none)", "rgba(51, 0, 0, 0)", "color(srgb 0.2 none none / none)");
|
||||
try expectSerialized("hsl(120 30% 50%)", null, "rgb(89, 166, 89)");
|
||||
try expectSerialized("hsla(-300, 100%, 37.5%, 0.2)", null, "rgba(191, 191, 0, 0.2)");
|
||||
try expectSerialized("hsl(30deg, 100%, 100%)", null, "rgb(255, 255, 255)");
|
||||
try expectSerialized("hsl(0.5turn 100% 50%)", null, "rgb(0, 255, 255)");
|
||||
try expectSerialized("hsl(0 -50% 40%)", null, "rgb(102, 102, 102)");
|
||||
try expectSerialized("hsl(calc(infinity) 100% 50%)", null, "rgb(255, 0, 0)");
|
||||
try expectSerialized("hsl(120 80% none)", "hsl(120 80 none)", "hsl(120 80% none)");
|
||||
try expectSerialized("hsl(120 100% 50% / none)", "hsl(120 100 50 / none)", "hsl(120 100% 50% / none)");
|
||||
try expectSerialized("hwb(320deg 30% 40%)", null, "rgb(153, 77, 128)");
|
||||
try expectSerialized("hwb(0 70% 60%)", null, "rgb(137, 137, 137)");
|
||||
try expectSerialized("hwb(none none none / none)", null, "hwb(none none none / none)");
|
||||
}
|
||||
|
||||
test "color: invalid and unsupported" {
|
||||
const invalid = [_][]const u8{
|
||||
"", " /* hey */\n", "4", "top",
|
||||
"current-Color", "blac\u{212A}", "red blue", "#12",
|
||||
"#ffg", "#123456789", "rgb", "rgb()",
|
||||
"rgb(1)", "rgb(1,2,3,4,5)", "rgb(10%, 20, 30%)", "rgb(none, none, none)",
|
||||
"rgb(0, 0 0)", "rgb(0, 0, 0,)", "rgb(0, 0,, 0)", "rgb(0, 0, 0deg)",
|
||||
"rgb(0, 0, light)", "rgb(257, 0, 5 / 0)", "hsl(10, 50%, 0)", "hsl(50%, 50%, 0%)",
|
||||
"hsl(0, 50, 30%)", "hwba(120 30% 50%)", "hwb(90, 50%, 50%)", "hsl(calc(0.56turn * -0.43turn), 47%, 4884.6%)",
|
||||
"rgba(-2, 300, 400%, -0.5)", "foo(1)",
|
||||
};
|
||||
for (invalid) |input| {
|
||||
std.testing.expectEqual(Parsed.invalid, parse(input)) catch |err| {
|
||||
std.debug.print("expected invalid: {s}\n", .{input});
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
const unsupported = [_][]const u8{
|
||||
"var(--x)", "rgb(var(--x), 0, 0)", "red var(--x)",
|
||||
"lab(50% 20 -30)", "color-mix(in srgb, red, blue)", "light-dark(black, white)",
|
||||
"rgb(from red r g b)", "rgb(calc(50% + (sign(1em - 10px) * 10%)), 0%, 0%)", "-webkit-link",
|
||||
};
|
||||
for (unsupported) |input| {
|
||||
std.testing.expectEqual(Parsed.unsupported, parse(input)) catch |err| {
|
||||
std.debug.print("expected unsupported: {s}\n", .{input});
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -391,7 +391,7 @@
|
||||
const impDiv = document.createElement('div');
|
||||
impDiv.setAttribute('style', 'color: red !important; color: blue');
|
||||
document.body.appendChild(impDiv);
|
||||
testing.expectEqual('red', window.getComputedStyle(impDiv).getPropertyValue('color'));
|
||||
testing.expectEqual('rgb(255, 0, 0)', window.getComputedStyle(impDiv).getPropertyValue('color'));
|
||||
testing.expectEqual('red', impDiv.style.getPropertyValue('color'));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const CssParser = @import("../../css/Parser.zig");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const color = @import("../../color.zig");
|
||||
const CssParser = @import("../../css/Parser.zig");
|
||||
|
||||
const Element = @import("../Element.zig");
|
||||
const DOMMatrixReadOnly = @import("../DOMMatrixReadOnly.zig");
|
||||
|
||||
@@ -78,14 +79,34 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
|
||||
const wrapped = String.wrap(normalized);
|
||||
const value = self.propertyValue(normalized, wrapped, frame);
|
||||
|
||||
// The resolved transform is always a matrix: animation libraries
|
||||
// (popmotion) regex it as matrix()/matrix3d() and crash on anything else.
|
||||
if (self._is_computed and wrapped.eql(comptime .wrap("transform"))) {
|
||||
if (self._is_computed == false) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (wrapped.eql(comptime .wrap("transform"))) {
|
||||
// The resolved transform is always a matrix: animation libraries
|
||||
// (popmotion) regex it as matrix()/matrix3d() and crash on anything
|
||||
// else.
|
||||
return resolvedTransform(value, frame);
|
||||
}
|
||||
|
||||
if (isColorProperty(normalized)) {
|
||||
return computedColor(value, frame);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
fn computedColor(value: []const u8, frame: *Frame) []const u8 {
|
||||
const parsed = switch (color.parse(value)) {
|
||||
.value => |v| v,
|
||||
.invalid, .unsupported => return value,
|
||||
};
|
||||
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
|
||||
parsed.writeComputed(&buf.writer) catch return value;
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
fn propertyValue(self: *const CSSStyleDeclaration, normalized: []const u8, wrapped: String, frame: *Frame) []const u8 {
|
||||
|
||||
// Computed styles must reflect stylesheet rules, not just the element's
|
||||
@@ -355,7 +376,21 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value:
|
||||
}
|
||||
|
||||
// Normalize the value for canonical serialization
|
||||
const normalized_value = try normalizePropertyValue(frame.local_arena, normalized, value);
|
||||
const normalized_value = blk: {
|
||||
if (isColorProperty(normalized)) {
|
||||
switch (color.parse(value)) {
|
||||
// An invalid value leaves the declaration as it was.
|
||||
.invalid => return false,
|
||||
.value => |parsed| {
|
||||
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
|
||||
try parsed.writeSpecified(&buf.writer);
|
||||
break :blk buf.written();
|
||||
},
|
||||
.unsupported => {},
|
||||
}
|
||||
}
|
||||
break :blk try normalizePropertyValue(frame.local_arena, normalized, value);
|
||||
};
|
||||
|
||||
// Find existing property
|
||||
if (self.findProperty(.wrap(normalized))) |existing| {
|
||||
@@ -548,6 +583,29 @@ fn normalizePropertyName(allocator: Allocator, name: []const u8) []const u8 {
|
||||
return std.ascii.allocLowerString(allocator, name) catch name;
|
||||
}
|
||||
|
||||
fn isColorProperty(name: []const u8) bool {
|
||||
const color_properties: std.StaticStringMap(void) = .initComptime(.{
|
||||
.{ "background-color", {} },
|
||||
.{ "border-block-end-color", {} },
|
||||
.{ "border-block-start-color", {} },
|
||||
.{ "border-bottom-color", {} },
|
||||
.{ "border-inline-end-color", {} },
|
||||
.{ "border-inline-start-color", {} },
|
||||
.{ "border-left-color", {} },
|
||||
.{ "border-right-color", {} },
|
||||
.{ "border-top-color", {} },
|
||||
.{ "color", {} },
|
||||
.{ "column-rule-color", {} },
|
||||
.{ "flood-color", {} },
|
||||
.{ "lighting-color", {} },
|
||||
.{ "outline-color", {} },
|
||||
.{ "stop-color", {} },
|
||||
.{ "text-decoration-color", {} },
|
||||
.{ "text-emphasis-color", {} },
|
||||
});
|
||||
return color_properties.has(name);
|
||||
}
|
||||
|
||||
fn isCustomProperty(name: []const u8) bool {
|
||||
return std.mem.startsWith(u8, name, "--");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user