lib/css/src/value/scalar.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 /// How the `a` and `b` words of a value are read.
4 ///
5 /// `asset` and `pair` are part of the published record layout and are produced
6 /// by the tree publisher rather than by a stylesheet.
7 pub const Kind = enum(u8) {
8 invalid = 0,
9 keyword = 1,
10 length = 2,
11 percent = 3,
12 number = 4,
13 color = 5,
14 string = 6,
15 asset = 7,
16 pair = 8,
17 };
18
19 /// The unit a `length` carries. A percentage is a `Kind`, not a unit.
20 pub const Unit = enum(u8) {
21 none = 0,
22 px = 1,
23 em = 2,
24 rem = 3,
25 ch = 4,
26 lh = 5,
27 ex = 6,
28 vw = 7,
29 vh = 8,
30 vmin = 9,
31 vmax = 10,
32 fr = 11,
33 deg = 12,
34 s = 13,
35 ms = 14,
36
37 /// Resolves a dimension unit by name, folded to ASCII lower case.
38 pub fn parse(spelling: []const u8) ?Unit {
39 const info = @typeInfo(Unit).@"enum";
40 inline for (info.field_names, info.field_values) |field_name, field_value| {
41 if (field_value != 0 and std.ascii.eqlIgnoreCase(spelling, field_name)) {
42 return @fromBackingInt(@intCast(field_value));
43 }
44 }
45 return null;
46 }
47 };
48
49 /// Every keyword this engine understands, in one closed set so that a computed
50 /// keyword is an integer rather than a byte slice.
51 pub const Keyword = enum(u32) {
52 invalid = 0,
53 inherit,
54 initial,
55 unset,
56 revert,
57 auto,
58 none,
59 normal,
60 block,
61 @"inline",
62 inline_block,
63 flex,
64 inline_flex,
65 contents,
66 static,
67 relative,
68 absolute,
69 fixed,
70 sticky,
71 visible,
72 hidden,
73 scroll,
74 clip,
75 collapse,
76 content_box,
77 border_box,
78 row,
79 row_reverse,
80 column,
81 column_reverse,
82 nowrap,
83 wrap,
84 wrap_reverse,
85 flex_start,
86 flex_end,
87 center,
88 space_between,
89 space_around,
90 space_evenly,
91 start,
92 end,
93 stretch,
94 baseline,
95 left,
96 right,
97 justify,
98 capitalize,
99 uppercase,
100 lowercase,
101 pre,
102 pre_wrap,
103 pre_line,
104 break_spaces,
105 italic,
106 oblique,
107 bold,
108 bolder,
109 lighter,
110 solid,
111 dashed,
112 dotted,
113 double,
114 groove,
115 ridge,
116 inset,
117 outset,
118 underline,
119 overline,
120 line_through,
121 default,
122 pointer,
123 text,
124 move,
125 not_allowed,
126 grab,
127 grabbing,
128 crosshair,
129 wait,
130 help,
131 progress,
132 disc,
133 circle,
134 square,
135 decimal,
136 lower_alpha,
137 upper_alpha,
138 lower_roman,
139 upper_roman,
140 top,
141 middle,
142 bottom,
143 sub,
144 super,
145 text_top,
146 text_bottom,
147 min_content,
148 max_content,
149 fit_content,
150 content,
151 currentcolor,
152 transparent,
153 ltr,
154 rtl,
155 ellipsis,
156 break_all,
157 keep_all,
158 break_word,
159 anywhere,
160 fill,
161 contain,
162 cover,
163 scale_down,
164 all,
165 inside,
166 outside,
167 repeat,
168 repeat_x,
169 repeat_y,
170 no_repeat,
171 space,
172 round,
173 padding_box,
174 local,
175
176 /// Resolves a keyword by its CSS spelling, where an underscore in the tag
177 /// stands for a hyphen.
178 pub fn parse(word: []const u8) ?Keyword {
179 if (word.len == 0 or word.len > max_keyword_bytes) return null;
180 var buffer: [max_keyword_bytes]u8 = undefined;
181 for (word, 0..) |byte, index| {
182 buffer[index] = if (byte == '-') '_' else std.ascii.toLower(byte);
183 }
184 return std.meta.stringToEnum(Keyword, buffer[0..word.len]);
185 }
186
187 /// The CSS spelling of this keyword, with hyphens restored.
188 pub fn spelling(self: Keyword) []const u8 {
189 @setEvalBranchQuota(64_000);
190 return switch (self) {
191 inline else => |tag| comptime spell(@tagName(tag)),
192 };
193 }
194 };
195
196 const max_keyword_bytes: usize = 16;
197
198 fn spell(comptime tag: []const u8) []const u8 {
199 comptime {
200 @setEvalBranchQuota(16_000);
201 var buffer: [tag.len]u8 = undefined;
202 for (tag, 0..) |byte, index| buffer[index] = if (byte == '_') '-' else byte;
203 const frozen = buffer;
204 return &frozen;
205 }
206 }
207
208 /// The value grammar a property accepts. `value.parse` switches on it, so a
209 /// property table entry names a grammar rather than carrying a parser.
210 pub const Grammar = enum(u8) {
211 length,
212 length_percentage,
213 length_percentage_auto,
214 size,
215 number,
216 integer,
217 opacity,
218 color,
219 string,
220 url,
221 display,
222 position,
223 overflow,
224 visibility,
225 box_sizing,
226 flex_direction,
227 flex_wrap,
228 justify,
229 @"align",
230 align_self,
231 text_align,
232 text_transform,
233 white_space,
234 font_style,
235 font_weight,
236 font_family,
237 line_height,
238 border_style,
239 text_decoration_line,
240 cursor,
241 list_style_type,
242 vertical_align,
243 content,
244 z_index,
245 gap,
246 flex_basis,
247 length_normal,
248 direction,
249 text_overflow,
250 word_break,
251 overflow_wrap,
252 object_fit,
253 pointer_events,
254 user_select,
255 list_style_position,
256 background_repeat,
257 background_size,
258 background_box,
259 background_attachment,
260 };
261
262 /// Bit zero of the `flags` word marks an important declaration.
263 pub const flag_important: u16 = 1;
264
265 /// One computed value. Twelve bytes, laid out as the published declaration
266 /// record minus its property word.
267 pub const Value = extern struct {
268 kind: u8 = @backingInt(Kind.invalid),
269 unit: u8 = @backingInt(Unit.none),
270 flags: u16 = 0,
271 a: u32 = 0,
272 b: u32 = 0,
273
274 pub fn keyword(word: Keyword) Value {
275 return .{ .kind = @backingInt(Kind.keyword), .a = @backingInt(word) };
276 }
277
278 pub fn length(amount: f32, unit: Unit) Value {
279 return .{
280 .kind = @backingInt(Kind.length),
281 .unit = @backingInt(unit),
282 .a = @bitCast(amount),
283 };
284 }
285
286 pub fn percent(amount: f32) Value {
287 return .{ .kind = @backingInt(Kind.percent), .a = @bitCast(amount) };
288 }
289
290 pub fn number(amount: f32) Value {
291 return .{ .kind = @backingInt(Kind.number), .a = @bitCast(amount) };
292 }
293
294 pub fn color(packed_rgba: u32) Value {
295 return .{ .kind = @backingInt(Kind.color), .a = packed_rgba };
296 }
297
298 pub fn string(offset: u32, length_bytes: u32) Value {
299 return .{ .kind = @backingInt(Kind.string), .a = offset, .b = length_bytes };
300 }
301
302 pub fn valueKind(self: Value) Kind {
303 return @fromBackingInt(@intCast(self.kind));
304 }
305
306 pub fn valueUnit(self: Value) Unit {
307 return @fromBackingInt(@intCast(self.unit));
308 }
309
310 /// The keyword behind a keyword value, or `invalid` for any other kind.
311 pub fn asKeyword(self: Value) Keyword {
312 if (self.valueKind() != .keyword) return .invalid;
313 return @fromBackingInt(@intCast(self.a));
314 }
315
316 /// The scalar behind a length, percentage, or number.
317 pub fn asNumber(self: Value) f32 {
318 std.debug.assert(numeric(self.valueKind()));
319 return @bitCast(self.a);
320 }
321
322 /// The source span behind a string value.
323 pub fn asString(self: Value, source: []const u8) []const u8 {
324 std.debug.assert(self.valueKind() == .string);
325 return source[self.a..][0..self.b];
326 }
327
328 pub fn present(self: Value) bool {
329 return self.valueKind() != .invalid;
330 }
331
332 /// Binds this value to a property, producing the published record.
333 pub fn declare(self: Value, property: u32, important: bool) Declaration {
334 return .{
335 .property = property,
336 .kind = self.kind,
337 .unit = self.unit,
338 .flags = self.flags | (if (important) flag_important else 0),
339 .a = self.a,
340 .b = self.b,
341 };
342 }
343 };
344
345 /// The published sixteen byte declaration record. One representation serves a
346 /// parsed stylesheet and a directly authored inline style, so the publish path
347 /// parses no strings.
348 fn numeric(kind: Kind) bool {
349 return switch (kind) {
350 .length, .percent, .number => true,
351 else => false,
352 };
353 }
354
355 pub const Declaration = extern struct {
356 property: u32,
357 kind: u8,
358 unit: u8,
359 flags: u16,
360 a: u32,
361 b: u32,
362
363 /// The value this declaration carries. Importance is a cascade input
364 /// rather than part of the value, so it is dropped here.
365 pub fn value(self: Declaration) Value {
366 return .{
367 .kind = self.kind,
368 .unit = self.unit,
369 .flags = self.flags & ~flag_important,
370 .a = self.a,
371 .b = self.b,
372 };
373 }
374
375 pub fn important(self: Declaration) bool {
376 return self.flags & flag_important != 0;
377 }
378 };
379
380 comptime {
381 std.debug.assert(@sizeOf(Declaration) == 16);
382 std.debug.assert(@alignOf(Declaration) == 4);
383 std.debug.assert(@offsetOf(Declaration, "property") == 0);
384 std.debug.assert(@offsetOf(Declaration, "kind") == 4);
385 std.debug.assert(@offsetOf(Declaration, "unit") == 5);
386 std.debug.assert(@offsetOf(Declaration, "flags") == 6);
387 std.debug.assert(@offsetOf(Declaration, "a") == 8);
388 std.debug.assert(@offsetOf(Declaration, "b") == 12);
389 std.debug.assert(@sizeOf(Value) == 12);
390 }
391
392 test "the published declaration record matches the fixed layout" {
393 const declaration = Value.length(10, .px).declare(7, true);
394 try std.testing.expectEqual(@as(u32, 7), declaration.property);
395 try std.testing.expectEqual(@as(u8, @backingInt(Kind.length)), declaration.kind);
396 try std.testing.expectEqual(@as(u8, @backingInt(Unit.px)), declaration.unit);
397 try std.testing.expect(declaration.important());
398 try std.testing.expectEqual(@as(f32, 10), declaration.value().asNumber());
399 }
400
401 test "a keyword round trips through its CSS spelling" {
402 try std.testing.expectEqual(Keyword.inline_block, Keyword.parse("inline-block").?);
403 try std.testing.expectEqual(Keyword.@"inline", Keyword.parse("INLINE").?);
404 try std.testing.expectEqualStrings("inline-block", Keyword.inline_block.spelling());
405 try std.testing.expectEqualStrings("flex-start", Keyword.flex_start.spelling());
406 try std.testing.expect(Keyword.parse("not-a-keyword") == null);
407 try std.testing.expect(Keyword.parse("") == null);
408 }
409
410 test "a unit resolves by name and rejects an unknown dimension" {
411 try std.testing.expectEqual(Unit.rem, Unit.parse("rem").?);
412 try std.testing.expectEqual(Unit.px, Unit.parse("PX").?);
413 try std.testing.expect(Unit.parse("parsec") == null);
414 try std.testing.expect(Unit.parse("none") == null);
415 }
416
417 test "an absent value reports itself absent and a keyword reads back" {
418 const absent = Value{};
419 try std.testing.expect(!absent.present());
420 try std.testing.expectEqual(Keyword.invalid, absent.asKeyword());
421 const keyword = Value.keyword(.flex);
422 try std.testing.expect(keyword.present());
423 try std.testing.expectEqual(Keyword.flex, keyword.asKeyword());
424 }
425
426 test "a string value borrows a span of the stylesheet source" {
427 const source = "content: \"hello\"";
428 const value = Value.string(10, 5);
429 try std.testing.expectEqualStrings("hello", value.asString(source));
430 }