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 }