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 }