├── README.md ├── main.zig └── tests ├── bad-property.css ├── basic.css └── multiple-blocks.css /README.md: -------------------------------------------------------------------------------- 1 | # Metaprogramming in Zig and parsing a bit of CSS 2 | 3 | Minimal project to demonstrate metaprogramming in Zig to match parsed 4 | key-value pairs to struct field members and later print out the struct 5 | dynamically as well. 6 | 7 | This was live-streamed on [my Twitch](https://twitch.tv/eatonphil). 8 | 9 | The accompanying blog post is [available here](https://notes.eatonphil.com/2023-06-19-metaprogramming-in-zig-and-parsing-css.html). 10 | 11 | ```console 12 | $ zig build-exe main.zig 13 | $ ./main tests/basic.css 14 | selector: div 15 | background: white 16 | 17 | ``` 18 | -------------------------------------------------------------------------------- /main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const CSSProperty = union(enum) { 4 | unknown: void, 5 | color: []const u8, 6 | background: []const u8, 7 | }; 8 | 9 | fn match_property( 10 | name: []const u8, 11 | value: []const u8, 12 | ) !CSSProperty { 13 | const cssPropertyInfo = @typeInfo(CSSProperty); 14 | 15 | inline for (cssPropertyInfo.Union.fields) |u_field| { 16 | if (comptime !std.mem.eql(u8, u_field.name, "unknown")) { 17 | if (std.mem.eql(u8, u_field.name, name)) { 18 | return @unionInit(CSSProperty, u_field.name, value); 19 | } 20 | } 21 | } 22 | 23 | return error.UnknownProperty; 24 | } 25 | 26 | const CSSRule = struct { 27 | selector: []const u8, 28 | properties: []CSSProperty, 29 | }; 30 | 31 | const CSSSheet = struct { 32 | rules: []CSSRule, 33 | 34 | fn display(sheet: *CSSSheet) void { 35 | for (sheet.rules) |rule| { 36 | std.debug.print("selector: {s}\n", .{rule.selector}); 37 | for (rule.properties) |property| { 38 | inline for (@typeInfo(CSSProperty).Union.fields) |u_field| { 39 | if (comptime !std.mem.eql(u8, u_field.name, "unknown")) { 40 | if (std.mem.eql(u8, u_field.name, @tagName(property))) { 41 | std.debug.print(" {s}: {s}\n", .{ 42 | @tagName(property), 43 | @field(property, u_field.name), 44 | }); 45 | } 46 | } 47 | } 48 | } 49 | std.debug.print("\n", .{}); 50 | } 51 | } 52 | }; 53 | 54 | fn eat_whitespace( 55 | css: []const u8, 56 | initial_index: usize, 57 | ) usize { 58 | var index = initial_index; 59 | while (index < css.len and std.ascii.isWhitespace(css[index])) { 60 | index += 1; 61 | } 62 | 63 | return index; 64 | } 65 | 66 | fn debug_at( 67 | css: []const u8, 68 | index: usize, 69 | comptime msg: []const u8, 70 | args: anytype, 71 | ) void { 72 | var line_no: usize = 1; 73 | var col_no: usize = 0; 74 | 75 | var i: usize = 0; 76 | var line_beginning: usize = 0; 77 | var found_line = false; 78 | while (i < css.len) : (i += 1) { 79 | if (css[i] == '\n') { 80 | if (!found_line) { 81 | col_no = 0; 82 | line_beginning = i; 83 | line_no += 1; 84 | continue; 85 | } else { 86 | break; 87 | } 88 | } 89 | 90 | if (i == index) { 91 | found_line = true; 92 | } 93 | 94 | if (!found_line) { 95 | col_no += 1; 96 | } 97 | } 98 | 99 | std.debug.print("Error at line {}, column {}. ", .{ line_no, col_no }); 100 | std.debug.print(msg ++ "\n\n", args); 101 | std.debug.print("{s}\n", .{css[line_beginning..i]}); 102 | while (col_no > 0) : (col_no -= 1) { 103 | std.debug.print(" ", .{}); 104 | } 105 | std.debug.print("^ Near here.\n", .{}); 106 | } 107 | 108 | const ParseIdentifierResult = struct { 109 | identifier: []const u8, 110 | index: usize, 111 | }; 112 | fn parse_identifier( 113 | css: []const u8, 114 | initial_index: usize, 115 | ) !ParseIdentifierResult { 116 | var index = initial_index; 117 | while (index < css.len and std.ascii.isAlphabetic(css[index])) { 118 | index += 1; 119 | } 120 | 121 | if (index == initial_index) { 122 | debug_at(css, initial_index, "Expected valid identifier.", .{}); 123 | return error.InvalidIdentifier; 124 | } 125 | 126 | return ParseIdentifierResult{ 127 | .identifier = css[initial_index..index], 128 | .index = index, 129 | }; 130 | } 131 | 132 | fn parse_syntax( 133 | css: []const u8, 134 | initial_index: usize, 135 | syntax: u8, 136 | ) !usize { 137 | if (initial_index < css.len and css[initial_index] == syntax) { 138 | return initial_index + 1; 139 | } 140 | 141 | debug_at(css, initial_index, "Expected syntax: '{c}'.", .{syntax}); 142 | return error.NoSuchSyntax; 143 | } 144 | 145 | const ParsePropertyResult = struct { 146 | property: CSSProperty, 147 | index: usize, 148 | }; 149 | fn parse_property( 150 | css: []const u8, 151 | initial_index: usize, 152 | ) !ParsePropertyResult { 153 | var index = eat_whitespace(css, initial_index); 154 | 155 | // First parse property name. 156 | var name_res = parse_identifier(css, index) catch |e| { 157 | std.debug.print("Could not parse property name.\n", .{}); 158 | return e; 159 | }; 160 | index = name_res.index; 161 | 162 | index = eat_whitespace(css, index); 163 | 164 | // Then parse colon: :. 165 | index = try parse_syntax(css, index, ':'); 166 | 167 | index = eat_whitespace(css, index); 168 | 169 | // Then parse property value. 170 | var value_res = parse_identifier(css, index) catch |e| { 171 | std.debug.print("Could not parse property value.\n", .{}); 172 | return e; 173 | }; 174 | index = value_res.index; 175 | 176 | // Finally parse semi-colon: ;. 177 | index = try parse_syntax(css, index, ';'); 178 | 179 | var property = match_property(name_res.identifier, value_res.identifier) catch |e| { 180 | debug_at(css, initial_index, "Unknown property: '{s}'.", .{name_res.identifier}); 181 | return e; 182 | }; 183 | 184 | return ParsePropertyResult{ 185 | .property = property, 186 | .index = index, 187 | }; 188 | } 189 | 190 | const ParseRuleResult = struct { 191 | rule: CSSRule, 192 | index: usize, 193 | }; 194 | fn parse_rule( 195 | arena: *std.heap.ArenaAllocator, 196 | css: []const u8, 197 | initial_index: usize, 198 | ) !ParseRuleResult { 199 | var index = eat_whitespace(css, initial_index); 200 | 201 | // First parse selector(s). 202 | var selector_res = try parse_identifier(css, index); 203 | index = selector_res.index; 204 | 205 | index = eat_whitespace(css, index); 206 | 207 | // Then parse opening curly brace: {. 208 | index = try parse_syntax(css, index, '{'); 209 | 210 | index = eat_whitespace(css, index); 211 | 212 | var properties = std.ArrayList(CSSProperty).init(arena.allocator()); 213 | // Then parse any number of properties. 214 | while (index < css.len) { 215 | index = eat_whitespace(css, index); 216 | if (index < css.len and css[index] == '}') { 217 | break; 218 | } 219 | 220 | var attr_res = try parse_property(css, index); 221 | index = attr_res.index; 222 | 223 | try properties.append(attr_res.property); 224 | } 225 | 226 | index = eat_whitespace(css, index); 227 | 228 | // Then parse closing curly brace: }. 229 | index = try parse_syntax(css, index, '}'); 230 | 231 | return ParseRuleResult{ 232 | .rule = CSSRule{ 233 | .selector = selector_res.identifier, 234 | .properties = properties.items, 235 | }, 236 | .index = index, 237 | }; 238 | } 239 | 240 | fn parse( 241 | arena: *std.heap.ArenaAllocator, 242 | css: []const u8, 243 | ) !CSSSheet { 244 | var index: usize = 0; 245 | var rules = std.ArrayList(CSSRule).init(arena.allocator()); 246 | 247 | // Parse rules until EOF. 248 | while (index < css.len) { 249 | var res = try parse_rule(arena, css, index); 250 | index = res.index; 251 | try rules.append(res.rule); 252 | index = eat_whitespace(css, index); 253 | } 254 | 255 | return CSSSheet{ 256 | .rules = rules.items, 257 | }; 258 | } 259 | 260 | pub fn main() !void { 261 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 262 | defer arena.deinit(); 263 | 264 | const allocator = arena.allocator(); 265 | 266 | // Let's read in a CSS file. 267 | var args = std.process.args(); 268 | 269 | // Skips the program name. 270 | _ = args.next(); 271 | 272 | var file_name: []const u8 = ""; 273 | if (args.next()) |f| { 274 | file_name = f; 275 | } 276 | 277 | const file = try std.fs.cwd().openFile(file_name, .{}); 278 | defer file.close(); 279 | 280 | const file_size = try file.getEndPos(); 281 | var css_file = try allocator.alloc(u8, file_size); 282 | _ = try file.read(css_file); 283 | 284 | var sheet = parse(&arena, css_file) catch return; 285 | sheet.display(); 286 | } 287 | -------------------------------------------------------------------------------- /tests/bad-property.css: -------------------------------------------------------------------------------- 1 | a { 2 | big: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/basic.css: -------------------------------------------------------------------------------- 1 | div { 2 | background: white; 3 | } 4 | -------------------------------------------------------------------------------- /tests/multiple-blocks.css: -------------------------------------------------------------------------------- 1 | div { 2 | background: black; 3 | color: white; 4 | } 5 | 6 | a { 7 | color: blue; 8 | } 9 | --------------------------------------------------------------------------------