lib/css/src/cascade/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! The cascade over a parsed sheet: matching, lowering, and computed values.
2
3 const std = @import("std");
4
5 const bucket = @import("../bucket.zig");
6 const cascade = @import("root.zig");
7 const fixture = @import("../match/fixture/tree.zig");
8 const match = @import("../match/root.zig");
9 const property = @import("../property/root.zig");
10 const sheet = @import("../sheet.zig");
11 const value = @import("../value/root.zig");
12
13 const Styled = struct {
14 workspace: sheet.Workspace,
15 storage: bucket.Storage,
16 parsed: sheet.StyleSheet = .{},
17 resolver: cascade.Resolver = .{},
18 matched: [64]match.Match = undefined,
19
20 fn init(source: []const u8) !Styled {
21 var workspace = sheet.Workspace.init(std.testing.allocator);
22 errdefer workspace.deinit();
23 const parsed = try workspace.parse(source, .default());
24 var storage = try bucket.Storage.init(std.testing.allocator, bucket.Limits.inspect(&parsed));
25 storage.activate();
26 return .{ .workspace = workspace, .storage = storage, .parsed = parsed };
27 }
28
29 fn deinit(self: *Styled) void {
30 self.storage.deinit(std.testing.allocator);
31 self.workspace.deinit();
32 }
33
34 fn compute(
35 self: *Styled,
36 element: match.Element,
37 node: u32,
38 parent: ?*const cascade.Computed,
39 ) cascade.Computed {
40 const index = self.storage.index(&self.parsed);
41 const found = bucket.matchElement(index, element, node, &self.matched);
42 std.debug.assert(found.rejected == 0);
43 self.resolver.begin(self.parsed.source, &self.parsed.atoms, null);
44 for (self.matched[0..found.count]) |hit| {
45 const rule = self.parsed.rules[hit.rule];
46 for (rule.lowered) |item| self.resolver.add(item, .author, hit.specificity, hit.order);
47 for (rule.customs) |item| self.resolver.addCustom(item, .author, hit.specificity, hit.order);
48 }
49 var out: cascade.Computed = .{};
50 self.resolver.finish(parent, &out);
51 return out;
52 }
53 };
54
55 fn colored(red: u8, green: u8, blue: u8) value.Value {
56 return value.Value.color(value.color.pack(red, green, blue, 255));
57 }
58
59 test "a parsed sheet computes the record a hand written cascade expects" {
60 var styled = try Styled.init(
61 \\* { color: black }
62 \\article { width: 10px }
63 \\.card { width: 20px; color: red }
64 \\#main { color: blue !important }
65 \\.card { color: green }
66 );
67 defer styled.deinit();
68 var tree = fixture.Tree{};
69 _ = try tree.add(&styled.parsed.atoms, 0, "article#main.card");
70 tree.finish();
71
72 const computed = styled.compute(tree.element(), 0, null);
73 var expected = cascade.Computed.initial();
74 expected.set(.color, colored(0, 0, 255));
75 expected.set(.width, value.Value.length(20, .px));
76 try std.testing.expectEqualSlices(
77 u8,
78 std.mem.asBytes(&expected),
79 std.mem.asBytes(&computed),
80 );
81 }
82
83 test "a var reference substitutes, falls back, and refuses a cycle" {
84 var styled = try Styled.init(
85 \\html div.a { --gap: 8px; width: var(--gap) }
86 \\html div.b { width: var(--missing, 12px) }
87 \\html div.c { --x: var(--y); --y: var(--x); width: var(--x, 16px) }
88 );
89 defer styled.deinit();
90 var tree = fixture.Tree{};
91 _ = try tree.add(&styled.parsed.atoms, 0, "html");
92 _ = try tree.add(&styled.parsed.atoms, 1, "div.a");
93 _ = try tree.add(&styled.parsed.atoms, 1, "div.b");
94 _ = try tree.add(&styled.parsed.atoms, 1, "div.c");
95 tree.finish();
96
97 const substituted = styled.compute(tree.element(), 1, null);
98 try std.testing.expectEqual(value.Kind.length, substituted.get(.width).valueKind());
99 try std.testing.expectEqual(@as(f32, 8), substituted.get(.width).asNumber());
100
101 const fallen_back = styled.compute(tree.element(), 2, null);
102 try std.testing.expectEqual(@as(f32, 12), fallen_back.get(.width).asNumber());
103
104 const cyclic = styled.compute(tree.element(), 3, null);
105 try std.testing.expectEqual(@as(f32, 16), cyclic.get(.width).asNumber());
106 try std.testing.expectEqual(@as(u32, 0), styled.resolver.substitution_rejected);
107 }
108
109 test "a custom property declared above reaches a descendant" {
110 var styled = try Styled.init(
111 \\html div.a { --gap: 8px }
112 \\html div.b { width: var(--gap, 2px) }
113 );
114 defer styled.deinit();
115 var tree = fixture.Tree{};
116 _ = try tree.add(&styled.parsed.atoms, 0, "html");
117 _ = try tree.add(&styled.parsed.atoms, 1, "div.a");
118 _ = try tree.add(&styled.parsed.atoms, 1, "div.b");
119 tree.finish();
120
121 _ = styled.compute(tree.element(), 1, null);
122 var inherited = styled.resolver.customs;
123 const index = styled.storage.index(&styled.parsed);
124 const found = bucket.matchElement(index, tree.element(), 2, &styled.matched);
125 styled.resolver.begin(styled.parsed.source, &styled.parsed.atoms, &inherited);
126 for (styled.matched[0..found.count]) |hit| {
127 for (styled.parsed.rules[hit.rule].lowered) |item| {
128 styled.resolver.add(item, .author, hit.specificity, hit.order);
129 }
130 }
131 var out: cascade.Computed = .{};
132 styled.resolver.finish(null, &out);
133 try std.testing.expectEqual(@as(f32, 8), out.get(.width).asNumber());
134 }