-
Notifications
You must be signed in to change notification settings - Fork 286
CSSStyleDeclaration implementation #680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
All contributors have signed the CLA ✍️ ✅ |
| @@ -0,0 +1,308 @@ | |||
| const std = @import("std"); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The license header is missing.
| CSSRule, | ||
| }; | ||
|
|
||
| const CSSRule = struct {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't be pub const?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also wasn't sure, but I checked, and it doesn't have to be (I made XMLHttpRequest non public and everything worked fine)
src/browser/cssom/css_parser.zig
Outdated
| const declaration = CSSDeclaration.init(name, final_value, is_important); | ||
| try declarations.append(declaration); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I would remove CSSDeclaration.init function and use instead
try declarations.append(.{
.name = name,
.value = value,
.is_important = is_important,
});
src/browser/cssom/css_parser.zig
Outdated
| const declaration = CSSDeclaration.init(trimmed_name, final_value, is_important); | ||
| try declarations.append(declaration); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
| } | ||
|
|
||
| pub fn get_cssText(self: *const CSSStyleDeclaration, state: *SessionState) ![]const u8 { | ||
| var buffer = std.ArrayList(u8).init(state.arena); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should use the state.call_arena if the buffer must live only build the string returned by the function.
| if (prop.priority) try writer.writeAll(" !important"); | ||
| try writer.writeAll("; "); | ||
| } | ||
| return buffer.toOwnedSlice(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we are using an arena and we know it, you can direclyt retun buffer.items to avoid a useless double allocation.
| const writer = buffer.writer(); | ||
| for (self.order.items) |name| { | ||
| const prop = self.store.get(name).?; | ||
| const escaped = try CSSValueAnalyzer.escapeCSSValue(state.arena, prop.value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same about the call_arena
| var store_it = self.store.iterator(); | ||
| while (store_it.next()) |entry| { | ||
| state.arena.free(entry.key_ptr.*); | ||
| state.arena.free(entry.value_ptr.value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in arena, free are noop. So this is useless...
src/browser/cssom/css_parser.zig
Outdated
| }; | ||
|
|
||
| pub const CSSParserState = enum { | ||
| seekName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we would normally use snake_case for these, like a variable.
src/browser/cssom/css_parser.zig
Outdated
| }; | ||
| } | ||
|
|
||
| pub fn parseDeclarations(text: []const u8, allocator: std.mem.Allocator) ![]CSSDeclaration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a general convention in Zig code, the allocator tends to be the first parameter (or the 2nd one if it's a method, where the 1st has to be the receiver).
src/browser/cssom/css_parser.zig
Outdated
|
|
||
| pub fn parseDeclarations(text: []const u8, allocator: std.mem.Allocator) ![]CSSDeclaration { | ||
| var parser = init(); | ||
| var declarations = std.ArrayList(CSSDeclaration).init(allocator); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a chance the "managed" structures get removed from Zig. So we tend to favor std.ArrayListUmamanged(CSSDeclaration). But I can see here how that would be a bit annoying, since you pass it around, you'd also have to pass the allocator around.
I can think of 3 options.
1 - leave it as-is, we can deal with it if ever the stdlib removes this. I'm fine with this.
2 - Pass the allocator wherever you pass &declarations
3 - Add the allocator and declarations as fields to CSSParser, then you don't need to pass either around. You could create a helper. self.addDeclaration(...):
fn addDeclaration(self: *CSSParser, decl: CSSDeclaration) !void {
return self.declarations.append(self.allocator, decl);
src/browser/cssom/css_parser.zig
Outdated
| parser.state = .inQuotedValue; | ||
| } else if (c == '\'') { | ||
| parser.state = .inSingleQuotedValue; | ||
| } else if (c == 'u' and parser.position + 3 < text.len and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is equivalent:
else if (c == 'u' and std.mem.startsWith(u8, text[parser.position...], URL_PREFIX)) {
src/browser/cssom/css_parser.zig
Outdated
|
|
||
| try parser.finalize(text, &declarations); | ||
|
|
||
| return declarations.toOwnedSlice(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit of an eternal debate here. If this was a library, this would probably be right. But within our code, we know that allocator is actually an arena, so all this is doing is creating another copy of the slice that we don't actually need.
You can keep it as-is, which makes this code more universally usable.
You could also return declarations.items and "leak" the ArrayList..but it wouldn't really be leaking because we know we're using an arena. If you opt to go this route, you could rename allocator to arena, just so that you make it clear you expect callers to provide an arena-backed allocator. If you go this route, you might as well remove the deinit at the top of the function.
It would make your testing a bit more complicated, but instead of const testing = std.testing, you can do:
const testing = @import("../../testing.zig");And then do:
test "CSSParser - Property with !important" {
defer testing.reset(); // this resets the testing.arena_allocator
const declarations = try CSSParser.parseDeclarations(text, testing.arena_allocator);
// ...
}| "linear-gradient", "radial-gradient", "conic-gradient", "translate", "rotate", "scale", "skew", "matrix", | ||
| }; | ||
|
|
||
| pub fn isKnownKeyword(value: []const u8) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re case-sensitivity...
if you assume the longest special value is 15 characters, i.e. radial-gradient, you could:
// 15 is our longest special value
// TODO: would be nice not to have this automatically derived from our definition somehow?
if (value.len > 15) {
return false;
}
var buf: [15]u8 = undefined;
const v = std.ascii.lowerString(&buf, value);and then use v instead of value to get case-insensitive comparison. You'll have to make sure all the constants are lowercase, e.g. Hz -> hz
At this point, you could look at alternatives to looping, like std.meta.stringToEnum(e.g. https://github.com/lightpanda-io/browser/blob/main/src/browser/mime.zig#L174)..but I've benchmarked this before and the difference tends to be pretty small (but more obvious for short values, since the SIMD operation of std.mem.eql can't be leveraged, which is what we have here).
Well, I think the case-sensitivity needs to be fixed, we can optimize at a later time if needed.
| "none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset", | ||
| }; | ||
|
|
||
| const ColorNames = [_][]const u8{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PascalCase is generally used for Types.
For non-global consts, I think we do color_names.
| .order = .{}, | ||
| }, | ||
|
|
||
| pub fn get_style(e: *parser.ElementHTML, state: *SessionState) !*CSSStyleDeclaration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add some basic tests for this? Nothing nearly as exhaustive as what you've already done, but maybe just calling el.style.setProperty with a straightforward key-value and then asserting the value with el.style.getPropertyValue
|
|
||
| const std = @import("std"); | ||
|
|
||
| pub const CSSValueAnalyzer = struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know the tests in css_style_declarations are good, but this is very unit-testing-friendly code where you can easily test a number of edge / error cases. I could see test cases that cover things like weird numbers, case sensitivity, various whitespaces, invalid quoting, etc, being quite useful to prevent any regression if we update the code in the future.
| pub fn startsWithFunction(value: []const u8) bool { | ||
| for (Functions) |func| { | ||
| if (value.len >= func.len + 1 and | ||
| std.mem.startsWith(u8, value, func) and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could include the ( in the constants of Functions ?
|
I have read the CLA Document and I hereby sign the CLA |
Use expectEqual where possible deduplicate finalize and finishDeclaration
Don't dupe value if it doesn't need to be quoted.
Added also a css parser and a css value analyzer util