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 }