lib/css/src/property/table.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const value = @import("../value/root.zig");
4
5 const Value = value.Value;
6 const color = value.color;
7
8 /// Every property this engine computes. Closed, so a declaration carries an
9 /// integer rather than a byte slice and a computed style is a dense array.
10 pub const Id = enum(u32) {
11 invalid = 0,
12 accent_color,
13 align_content,
14 align_items,
15 align_self,
16 background_attachment,
17 background_clip,
18 background_color,
19 background_image,
20 background_origin,
21 background_position_x,
22 background_position_y,
23 background_repeat,
24 background_size,
25 border_bottom_color,
26 border_bottom_left_radius,
27 border_bottom_right_radius,
28 border_bottom_style,
29 border_bottom_width,
30 border_left_color,
31 border_left_style,
32 border_left_width,
33 border_right_color,
34 border_right_style,
35 border_right_width,
36 border_top_color,
37 border_top_left_radius,
38 border_top_right_radius,
39 border_top_style,
40 border_top_width,
41 bottom,
42 box_sizing,
43 caret_color,
44 color,
45 column_gap,
46 content,
47 cursor,
48 direction,
49 display,
50 flex_basis,
51 flex_direction,
52 flex_grow,
53 flex_shrink,
54 flex_wrap,
55 font_family,
56 font_size,
57 font_style,
58 font_weight,
59 height,
60 justify_content,
61 justify_items,
62 justify_self,
63 left,
64 letter_spacing,
65 line_height,
66 list_style_position,
67 list_style_type,
68 margin_bottom,
69 margin_left,
70 margin_right,
71 margin_top,
72 max_height,
73 max_width,
74 min_height,
75 min_width,
76 object_fit,
77 opacity,
78 order,
79 outline_color,
80 outline_offset,
81 outline_style,
82 outline_width,
83 overflow_wrap,
84 overflow_x,
85 overflow_y,
86 padding_bottom,
87 padding_left,
88 padding_right,
89 padding_top,
90 pointer_events,
91 position,
92 right,
93 row_gap,
94 scroll_margin_bottom,
95 scroll_margin_left,
96 scroll_margin_right,
97 scroll_margin_top,
98 scroll_padding_bottom,
99 scroll_padding_left,
100 scroll_padding_right,
101 scroll_padding_top,
102 text_align,
103 text_decoration_color,
104 text_decoration_line,
105 text_decoration_style,
106 text_decoration_thickness,
107 text_indent,
108 text_overflow,
109 text_transform,
110 text_underline_offset,
111 top,
112 user_select,
113 vertical_align,
114 visibility,
115 white_space,
116 width,
117 word_break,
118 word_spacing,
119 z_index,
120 };
121
122 /// The initial value, inheritance rule, and value grammar of one property.
123 pub const Metadata = struct {
124 name: []const u8,
125 inherited: bool,
126 grammar: value.Grammar,
127 initial: Value,
128 };
129
130 /// The number of slots a dense per property array needs, `invalid` included.
131 pub const count: usize = entries.len + 1;
132
133 /// The property table, sorted by name so that `lookup` is a binary search.
134 pub const entries = [_]Metadata{
135 .{ .name = "accent-color", .inherited = false, .grammar = .color, .initial = Value.keyword(.currentcolor) },
136 .{ .name = "align-content", .inherited = false, .grammar = .justify, .initial = Value.keyword(.normal) },
137 .{ .name = "align-items", .inherited = false, .grammar = .@"align", .initial = Value.keyword(.normal) },
138 .{ .name = "align-self", .inherited = false, .grammar = .align_self, .initial = Value.keyword(.auto) },
139 .{ .name = "background-attachment", .inherited = false, .grammar = .background_attachment, .initial = Value.keyword(.scroll) },
140 .{ .name = "background-clip", .inherited = false, .grammar = .background_box, .initial = Value.keyword(.border_box) },
141 .{ .name = "background-color", .inherited = false, .grammar = .color, .initial = Value.color(color.transparent) },
142 .{ .name = "background-image", .inherited = false, .grammar = .url, .initial = Value.keyword(.none) },
143 .{ .name = "background-origin", .inherited = false, .grammar = .background_box, .initial = Value.keyword(.padding_box) },
144 .{ .name = "background-position-x", .inherited = false, .grammar = .length_percentage, .initial = Value.percent(0) },
145 .{ .name = "background-position-y", .inherited = false, .grammar = .length_percentage, .initial = Value.percent(0) },
146 .{ .name = "background-repeat", .inherited = false, .grammar = .background_repeat, .initial = Value.keyword(.repeat) },
147 .{ .name = "background-size", .inherited = false, .grammar = .background_size, .initial = Value.keyword(.auto) },
148 .{ .name = "border-bottom-color", .inherited = false, .grammar = .color, .initial = Value.keyword(.currentcolor) },
149 .{ .name = "border-bottom-left-radius", .inherited = false, .grammar = .length_percentage, .initial = Value.length(0, .px) },
150 .{ .name = "border-bottom-right-radius", .inherited = false, .grammar = .length_percentage, .initial = Value.length(0, .px) },
151 .{ .name = "border-bottom-style", .inherited = false, .grammar = .border_style, .initial = Value.keyword(.none) },
152 .{ .name = "border-bottom-width", .inherited = false, .grammar = .length, .initial = Value.length(3, .px) },
153 .{ .name = "border-left-color", .inherited = false, .grammar = .color, .initial = Value.keyword(.currentcolor) },
154 .{ .name = "border-left-style", .inherited = false, .grammar = .border_style, .initial = Value.keyword(.none) },
155 .{ .name = "border-left-width", .inherited = false, .grammar = .length, .initial = Value.length(3, .px) },
156 .{ .name = "border-right-color", .inherited = false, .grammar = .color, .initial = Value.keyword(.currentcolor) },
157 .{ .name = "border-right-style", .inherited = false, .grammar = .border_style, .initial = Value.keyword(.none) },
158 .{ .name = "border-right-width", .inherited = false, .grammar = .length, .initial = Value.length(3, .px) },
159 .{ .name = "border-top-color", .inherited = false, .grammar = .color, .initial = Value.keyword(.currentcolor) },
160 .{ .name = "border-top-left-radius", .inherited = false, .grammar = .length_percentage, .initial = Value.length(0, .px) },
161 .{ .name = "border-top-right-radius", .inherited = false, .grammar = .length_percentage, .initial = Value.length(0, .px) },
162 .{ .name = "border-top-style", .inherited = false, .grammar = .border_style, .initial = Value.keyword(.none) },
163 .{ .name = "border-top-width", .inherited = false, .grammar = .length, .initial = Value.length(3, .px) },
164 .{ .name = "bottom", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
165 .{ .name = "box-sizing", .inherited = false, .grammar = .box_sizing, .initial = Value.keyword(.content_box) },
166 .{ .name = "caret-color", .inherited = false, .grammar = .color, .initial = Value.keyword(.currentcolor) },
167 .{ .name = "color", .inherited = true, .grammar = .color, .initial = Value.color(color.pack(0, 0, 0, 255)) },
168 .{ .name = "column-gap", .inherited = false, .grammar = .gap, .initial = Value.keyword(.normal) },
169 .{ .name = "content", .inherited = false, .grammar = .content, .initial = Value.keyword(.normal) },
170 .{ .name = "cursor", .inherited = true, .grammar = .cursor, .initial = Value.keyword(.auto) },
171 .{ .name = "direction", .inherited = true, .grammar = .direction, .initial = Value.keyword(.ltr) },
172 .{ .name = "display", .inherited = false, .grammar = .display, .initial = Value.keyword(.@"inline") },
173 .{ .name = "flex-basis", .inherited = false, .grammar = .flex_basis, .initial = Value.keyword(.auto) },
174 .{ .name = "flex-direction", .inherited = false, .grammar = .flex_direction, .initial = Value.keyword(.row) },
175 .{ .name = "flex-grow", .inherited = false, .grammar = .number, .initial = Value.number(0) },
176 .{ .name = "flex-shrink", .inherited = false, .grammar = .number, .initial = Value.number(1) },
177 .{ .name = "flex-wrap", .inherited = false, .grammar = .flex_wrap, .initial = Value.keyword(.nowrap) },
178 .{ .name = "font-family", .inherited = true, .grammar = .font_family, .initial = Value.string(0, 0) },
179 .{ .name = "font-size", .inherited = true, .grammar = .length_percentage, .initial = Value.length(1, .rem) },
180 .{ .name = "font-style", .inherited = true, .grammar = .font_style, .initial = Value.keyword(.normal) },
181 .{ .name = "font-weight", .inherited = true, .grammar = .font_weight, .initial = Value.number(400) },
182 .{ .name = "height", .inherited = false, .grammar = .size, .initial = Value.keyword(.auto) },
183 .{ .name = "justify-content", .inherited = false, .grammar = .justify, .initial = Value.keyword(.flex_start) },
184 .{ .name = "justify-items", .inherited = false, .grammar = .justify, .initial = Value.keyword(.normal) },
185 .{ .name = "justify-self", .inherited = false, .grammar = .align_self, .initial = Value.keyword(.auto) },
186 .{ .name = "left", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
187 .{ .name = "letter-spacing", .inherited = true, .grammar = .length_normal, .initial = Value.keyword(.normal) },
188 .{ .name = "line-height", .inherited = true, .grammar = .line_height, .initial = Value.keyword(.normal) },
189 .{ .name = "list-style-position", .inherited = true, .grammar = .list_style_position, .initial = Value.keyword(.outside) },
190 .{ .name = "list-style-type", .inherited = true, .grammar = .list_style_type, .initial = Value.keyword(.disc) },
191 .{ .name = "margin-bottom", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.length(0, .px) },
192 .{ .name = "margin-left", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.length(0, .px) },
193 .{ .name = "margin-right", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.length(0, .px) },
194 .{ .name = "margin-top", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.length(0, .px) },
195 .{ .name = "max-height", .inherited = false, .grammar = .size, .initial = Value.keyword(.none) },
196 .{ .name = "max-width", .inherited = false, .grammar = .size, .initial = Value.keyword(.none) },
197 .{ .name = "min-height", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
198 .{ .name = "min-width", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
199 .{ .name = "object-fit", .inherited = false, .grammar = .object_fit, .initial = Value.keyword(.fill) },
200 .{ .name = "opacity", .inherited = false, .grammar = .opacity, .initial = Value.number(1) },
201 .{ .name = "order", .inherited = false, .grammar = .integer, .initial = Value.number(0) },
202 .{ .name = "outline-color", .inherited = false, .grammar = .color, .initial = Value.keyword(.currentcolor) },
203 .{ .name = "outline-offset", .inherited = false, .grammar = .length, .initial = Value.length(0, .px) },
204 .{ .name = "outline-style", .inherited = false, .grammar = .border_style, .initial = Value.keyword(.none) },
205 .{ .name = "outline-width", .inherited = false, .grammar = .length, .initial = Value.length(3, .px) },
206 .{ .name = "overflow-wrap", .inherited = true, .grammar = .overflow_wrap, .initial = Value.keyword(.normal) },
207 .{ .name = "overflow-x", .inherited = false, .grammar = .overflow, .initial = Value.keyword(.visible) },
208 .{ .name = "overflow-y", .inherited = false, .grammar = .overflow, .initial = Value.keyword(.visible) },
209 .{ .name = "padding-bottom", .inherited = false, .grammar = .length_percentage, .initial = Value.length(0, .px) },
210 .{ .name = "padding-left", .inherited = false, .grammar = .length_percentage, .initial = Value.length(0, .px) },
211 .{ .name = "padding-right", .inherited = false, .grammar = .length_percentage, .initial = Value.length(0, .px) },
212 .{ .name = "padding-top", .inherited = false, .grammar = .length_percentage, .initial = Value.length(0, .px) },
213 .{ .name = "pointer-events", .inherited = true, .grammar = .pointer_events, .initial = Value.keyword(.auto) },
214 .{ .name = "position", .inherited = false, .grammar = .position, .initial = Value.keyword(.static) },
215 .{ .name = "right", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
216 .{ .name = "row-gap", .inherited = false, .grammar = .gap, .initial = Value.keyword(.normal) },
217 .{ .name = "scroll-margin-bottom", .inherited = false, .grammar = .length, .initial = Value.length(0, .px) },
218 .{ .name = "scroll-margin-left", .inherited = false, .grammar = .length, .initial = Value.length(0, .px) },
219 .{ .name = "scroll-margin-right", .inherited = false, .grammar = .length, .initial = Value.length(0, .px) },
220 .{ .name = "scroll-margin-top", .inherited = false, .grammar = .length, .initial = Value.length(0, .px) },
221 .{ .name = "scroll-padding-bottom", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
222 .{ .name = "scroll-padding-left", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
223 .{ .name = "scroll-padding-right", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
224 .{ .name = "scroll-padding-top", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
225 .{ .name = "text-align", .inherited = true, .grammar = .text_align, .initial = Value.keyword(.start) },
226 .{ .name = "text-decoration-color", .inherited = false, .grammar = .color, .initial = Value.keyword(.currentcolor) },
227 .{ .name = "text-decoration-line", .inherited = false, .grammar = .text_decoration_line, .initial = Value.keyword(.none) },
228 .{ .name = "text-decoration-style", .inherited = false, .grammar = .border_style, .initial = Value.keyword(.solid) },
229 .{ .name = "text-decoration-thickness", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
230 .{ .name = "text-indent", .inherited = true, .grammar = .length_percentage, .initial = Value.length(0, .px) },
231 .{ .name = "text-overflow", .inherited = false, .grammar = .text_overflow, .initial = Value.keyword(.clip) },
232 .{ .name = "text-transform", .inherited = true, .grammar = .text_transform, .initial = Value.keyword(.none) },
233 .{ .name = "text-underline-offset", .inherited = true, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
234 .{ .name = "top", .inherited = false, .grammar = .length_percentage_auto, .initial = Value.keyword(.auto) },
235 .{ .name = "user-select", .inherited = true, .grammar = .user_select, .initial = Value.keyword(.auto) },
236 .{ .name = "vertical-align", .inherited = false, .grammar = .vertical_align, .initial = Value.keyword(.baseline) },
237 .{ .name = "visibility", .inherited = true, .grammar = .visibility, .initial = Value.keyword(.visible) },
238 .{ .name = "white-space", .inherited = true, .grammar = .white_space, .initial = Value.keyword(.normal) },
239 .{ .name = "width", .inherited = false, .grammar = .size, .initial = Value.keyword(.auto) },
240 .{ .name = "word-break", .inherited = true, .grammar = .word_break, .initial = Value.keyword(.normal) },
241 .{ .name = "word-spacing", .inherited = true, .grammar = .gap, .initial = Value.keyword(.normal) },
242 .{ .name = "z-index", .inherited = false, .grammar = .z_index, .initial = Value.keyword(.auto) },
243 };
244
245 comptime {
246 @setEvalBranchQuota(64_000);
247 const info = @typeInfo(Id).@"enum";
248 if (info.field_names.len != entries.len + 1) {
249 @compileError("the property enum and the property table must stay parallel");
250 }
251 for (entries[1..], 1..) |entry, index| {
252 if (std.mem.order(u8, entries[index - 1].name, entry.name) != .lt) {
253 @compileError("the property table must stay sorted by name");
254 }
255 }
256 }
257
258 /// The metadata of `id`. Asserts that the identifier names a real property.
259 pub fn metadata(id: Id) Metadata {
260 std.debug.assert(id != .invalid);
261 return entries[@backingInt(id) - 1];
262 }
263
264 /// Resolves a property name, ASCII case insensitively. Custom properties are
265 /// not in this table: they keep their author spelling and cascade as tokens.
266 pub fn lookup(name: []const u8) ?Id {
267 if (name.len == 0 or name.len > max_name_bytes) return null;
268 var buffer: [max_name_bytes]u8 = undefined;
269 for (name, 0..) |byte, index| buffer[index] = std.ascii.toLower(byte);
270 const folded = buffer[0..name.len];
271 var low: usize = 0;
272 var high: usize = entries.len;
273 while (low < high) {
274 const middle = low + (high - low) / 2;
275 switch (std.mem.order(u8, entries[middle].name, folded)) {
276 .lt => low = middle + 1,
277 .gt => high = middle,
278 .eq => return @fromBackingInt(@intCast(middle + 1)),
279 }
280 }
281 return null;
282 }
283
284 /// Whether `name` starts with the two dashes that mark a custom property.
285 pub fn isCustom(name: []const u8) bool {
286 return name.len > 2 and name[0] == '-' and name[1] == '-';
287 }
288
289 const max_name_bytes: usize = 32;
290
291 comptime {
292 for (entries) |entry| {
293 if (entry.name.len > max_name_bytes) @compileError("property name too long to fold");
294 }
295 }
296
297 test "every property resolves by name and reports its own metadata" {
298 for (entries, 1..) |entry, index| {
299 const id = lookup(entry.name) orelse return error.TestUnexpectedResult;
300 try std.testing.expectEqual(@as(u32, @intCast(index)), @backingInt(id));
301 try std.testing.expectEqualStrings(entry.name, metadata(id).name);
302 }
303 }
304
305 test "a property name folds and an unknown name resolves to nothing" {
306 try std.testing.expectEqual(Id.font_size, lookup("FONT-SIZE").?);
307 try std.testing.expectEqual(Id.display, lookup("display").?);
308 try std.testing.expect(lookup("not-a-property") == null);
309 try std.testing.expect(lookup("") == null);
310 try std.testing.expect(lookup("--theme") == null);
311 }
312
313 test "inheritance and initial values follow the cascade specification" {
314 try std.testing.expect(metadata(.color).inherited);
315 try std.testing.expect(metadata(.font_size).inherited);
316 try std.testing.expect(!metadata(.display).inherited);
317 try std.testing.expect(!metadata(.margin_top).inherited);
318 try std.testing.expectEqual(value.Keyword.@"inline", metadata(.display).initial.asKeyword());
319 try std.testing.expectEqual(value.Keyword.auto, metadata(.width).initial.asKeyword());
320 try std.testing.expectEqual(@as(f32, 1), metadata(.flex_shrink).initial.asNumber());
321 }
322
323 test "a custom property is recognised by its leading dashes" {
324 try std.testing.expect(isCustom("--brand"));
325 try std.testing.expect(!isCustom("--"));
326 try std.testing.expect(!isCustom("color"));
327 try std.testing.expect(!isCustom("-webkit-box"));
328 }