const std = @import("std"); const astgen = @import("astgen.zig"); // WebIDL primitive -> Zig type mapping. // Returns null to signal "skip this field" for unsupported types. fn idlToZig(idl_type: []const u8, nullable: bool) ?[]const u8 { const inner: []const u8 = blk: { if (eql(idl_type, "boolean")) break :blk "bool"; if (eql(idl_type, "long") or eql(idl_type, "signed long")) break :blk "i32"; if (eql(idl_type, "unsigned long")) break :blk "u32"; if (eql(idl_type, "short")) break :blk "i16"; if (eql(idl_type, "unsigned short")) break :blk "u16"; if (eql(idl_type, "long long")) break :blk "i64"; if (eql(idl_type, "unsigned long long")) break :blk "u64"; if (eql(idl_type, "octet") or eql(idl_type, "byte")) break :blk "u8"; if (eql(idl_type, "double") or eql(idl_type, "float") or eql(idl_type, "DOMHighResTimeStamp") or eql(idl_type, "CSSNumberish")) break :blk "f64"; if (eql(idl_type, "DOMString") or eql(idl_type, "USVString") or eql(idl_type, "CSSOMString")) break :blk "[]const u8"; // EventTarget gets a common-fields sub-struct (see EventTarget decl below) if (eql(idl_type, "EventTarget") or eql(idl_type, "Element") or eql(idl_type, "HTMLElement") or eql(idl_type, "Node")) break :blk "EventTarget"; // Anything else (exotic interface, union, array, promise, …) is skipped return null; }; if (nullable) { // Allocate a short buffer — caller uses arena so leaking is fine const buf = std.heap.page_allocator.alloc(u8, inner.len + 1) catch return null; buf[0] = '?'; @memcpy(buf[1..], inner); return buf; } return inner; } fn eql(a: []const u8, b: []const u8) bool { return std.mem.eql(u8, a, b); } fn isZigKeyword(name: []const u8) bool { const keywords = [_][]const u8{ "addrspace", "align", "allowzero", "and", "anyframe", "anytype", "asm", "async", "await", "break", "callconv", "catch", "comptime", "const", "continue", "defer", "else", "enum", "errdefer", "error", "export", "extern", "fn", "for", "if", "inline", "linksection", "noalias", "noinline", "nosuspend", "opaque", "or", "orelse", "packed", "pub", "resume", "return", "struct", "suspend", "switch", "test", "threadlocal", "try", "union", "unreachable", "usingnamespace", "var", "volatile", "while", }; for (keywords) |keyword| { if (eql(name, keyword)) return true; } return false; } fn zigIdent(allocator: std.mem.Allocator, name: []const u8) ![]const u8 { if (isValidIdent(name) and !isZigKeyword(name)) return name; return try std.fmt.allocPrint(allocator, "@\"{s}\"", .{name}); } // Converts a camelCase WebIDL identifier to snake_case for Zig field naming. // Runs of uppercase letters are treated as acronyms, so `blockedURI` → `blocked_uri` // and `newURL` → `new_url`. The original camelCase is preserved separately as the // JS property name to read from the underlying DOM event object. fn camelToSnake(allocator: std.mem.Allocator, name: []const u8) ![]const u8 { var out: std.ArrayListUnmanaged(u8) = .empty; for (name, 0..) |c, i| { const is_upper = c >= 'A' and c <= 'Z'; if (is_upper and i > 0) { const prev = name[i - 1]; const prev_is_lower = prev >= 'a' and prev <= 'z'; const next_is_lower = (i + 1 < name.len) and name[i + 1] >= 'a' and name[i + 1] <= 'z'; const prev_is_upper = prev >= 'A' and prev <= 'Z'; if (prev_is_lower or (prev_is_upper and next_is_lower)) { try out.append(allocator, '_'); } } if (is_upper) { try out.append(allocator, c - 'A' + 'a'); } else { try out.append(allocator, c); } } return out.toOwnedSlice(allocator); } // A parsed interface: flat list of (name, zig_type) after resolving inheritance. const Interface = struct { name: []const u8, fields: std.ArrayListUnmanaged(Field), href: []const u8 = "", const Field = struct { name: []const u8, js_name: []const u8, zig_type: []const u8, }; }; pub fn main(init: std.process.Init) !void { const allocator = init.gpa; const io = init.io; const source = try generate(io, allocator); defer allocator.free(source); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = "src/runtime/client/events/generated.zig", .data = source, }); std.debug.print("Generated src/runtime/client/events/generated.zig\n", .{}); } pub fn generate(io: std.Io, allocator: std.mem.Allocator) ![]const u8 { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const a = arena.allocator(); // ------------------------------------------------------------------------- // Step 1: build a global name->members map by scanning all idlparsed specs. // Key: interface name, Value: array of raw member JSON objects. // ------------------------------------------------------------------------- var raw_members = std.StringHashMap( std.ArrayListUnmanaged(std.json.Value), ).init(a); var raw_inheritance = std.StringHashMap([]const u8).init(a); var spec_dir = try std.Io.Dir.cwd().openDir(io, "vendor/webref/ed/idlparsed", .{ .iterate = true }); defer spec_dir.close(io); var it = spec_dir.iterate(); while (try it.next(io)) |entry| { if (!std.mem.endsWith(u8, entry.name, ".json")) continue; const content = try spec_dir.readFileAlloc(io, entry.name, a, .unlimited); const parsed = std.json.parseFromSlice(std.json.Value, a, content, .{}) catch continue; const idlp = parsed.value.object.get("idlparsed") orelse continue; const idl_names = (idlp.object.get("idlNames") orelse continue).object; for (idl_names.keys(), idl_names.values()) |name, def| { const def_type = (def.object.get("type") orelse continue).string; if (!eql(def_type, "interface")) continue; if (!std.mem.endsWith(u8, name, "Event")) continue; if (std.mem.indexOf(u8, name, "Init") != null) continue; // Record inheritance if (def.object.get("inheritance")) |inh| { if (inh != .null and inh != .string) {} else if (inh == .string) { try raw_inheritance.put(name, inh.string); } } // Collect attribute members — merge with existing if partial const members_json = (def.object.get("members") orelse continue).array; const entry2 = try raw_members.getOrPut(name); if (!entry2.found_existing) { entry2.value_ptr.* = .empty; } for (members_json.items) |m| { const mtype = (m.object.get("type") orelse continue).string; if (!eql(mtype, "attribute")) continue; try entry2.value_ptr.append(a, m); } } // Also handle idlExtendedNames (partials) const idl_ext = (idlp.object.get("idlExtendedNames") orelse continue).object; for (idl_ext.keys(), idl_ext.values()) |name, partials| { if (!std.mem.endsWith(u8, name, "Event")) continue; if (std.mem.indexOf(u8, name, "Init") != null) continue; for (partials.array.items) |partial| { const members_json = (partial.object.get("members") orelse continue).array; const entry2 = try raw_members.getOrPut(name); if (!entry2.found_existing) { entry2.value_ptr.* = .empty; } for (members_json.items) |m| { const mtype = (m.object.get("type") orelse continue).string; if (!eql(mtype, "attribute")) continue; try entry2.value_ptr.append(a, m); } } } } // ------------------------------------------------------------------------- // Step 2: flatten inheritance chains into resolved Interface structs. // ------------------------------------------------------------------------- var interfaces = std.StringHashMap(Interface).init(a); var key_it = raw_members.keyIterator(); while (key_it.next()) |key| { const name = key.*; if (interfaces.contains(name)) continue; try resolveInterface(name, a, &raw_members, &raw_inheritance, &interfaces); } // ------------------------------------------------------------------------- // Step 3: load events.json → event-name to interface map // ------------------------------------------------------------------------- const events_content = try std.Io.Dir.cwd().readFileAlloc(io, "vendor/webref/ed/events.json", a, .unlimited); const events_parsed = try std.json.parseFromSlice(std.json.Value, a, events_content, .{}); const events_array = events_parsed.value.array; // Deduplicate: one canonical event name -> interface name var event_map = std.StringHashMap([]const u8).init(a); for (events_array.items) |ev| { const ev_type = (ev.object.get("type") orelse continue).string; const iface = (ev.object.get("interface") orelse continue).string; if (!event_map.contains(ev_type)) { try event_map.put(ev_type, iface); } } const interface_overrides = [_]struct { ev: []const u8, iface: []const u8 }{ .{ .ev = "click", .iface = "PointerEvent" }, .{ .ev = "auxclick", .iface = "PointerEvent" }, .{ .ev = "contextmenu", .iface = "PointerEvent" }, .{ .ev = "dblclick", .iface = "MouseEvent" }, .{ .ev = "focus", .iface = "FocusEvent" }, .{ .ev = "blur", .iface = "FocusEvent" }, }; for (interface_overrides) |o| { if (interfaces.get(o.iface)) |iface| { if (iface.fields.items.len > 0) { try event_map.put(o.ev, o.iface); } } } // ------------------------------------------------------------------------- // Step 4: emit Zig source via astgen // ------------------------------------------------------------------------- var file = astgen.File.init(allocator); defer file.deinit(); try file.addRaw( \\//! Generated by tools/codegen/events.zig — do not edit manually. \\//! Run `zig build eventsgen` to regenerate. \\//! \\//! Provides typed Zig structs for all browser DOM events. \\//! Use with `event.as(events.MouseEvent, allocator)`. \\const std = @import("std"); ); // Emit EventTarget helper struct first. Field names are snake_case; the // original camelCase DOM property names are resolved via `jsName` below. try file.addRaw( \\/// Minimal representation of EventTarget/Element for use as a nested field. \\/// Contains the most commonly accessed DOM element properties. \\pub const EventTarget = struct { \\ tag_name: []const u8 = "", \\ id: []const u8 = "", \\ name: []const u8 = "", \\ value: []const u8 = "", \\ @"type": []const u8 = "", \\ checked: bool = false, \\ disabled: bool = false, \\}; ); // Track snake_case → camelCase for every field whose names differ, so that // `Event.as(T, …)` can translate Zig field names back to the JS property // name when reading from the native DOM event object. var js_name_pairs = std.StringHashMap([]const u8).init(a); try js_name_pairs.put("tag_name", "tagName"); // Emit each interface struct var iface_it = interfaces.iterator(); while (iface_it.next()) |iface_entry| { const iface = iface_entry.value_ptr.*; if (iface.fields.items.len == 0) continue; const container = try file.addStruct(iface.name); for (iface.fields.items) |field| { // For string types we need a default of "" so zero-init works const default: ?[]const u8 = blk: { if (eql(field.zig_type, "[]const u8")) break :blk "\"\""; if (eql(field.zig_type, "bool")) break :blk "false"; if (std.mem.startsWith(u8, field.zig_type, "?")) break :blk "null"; if (eql(field.zig_type, "EventTarget")) break :blk ".{}"; break :blk "0"; }; try container.addField(file.arena.allocator(), "", try zigIdent(file.arena.allocator(), field.name), field.zig_type, default); if (!eql(field.name, field.js_name)) { _ = try js_name_pairs.getOrPutValue(field.name, field.js_name); } } } // Collect (event_name, iface_name) pairs that have a backing struct so we can // emit the aliases, the `Kind` enum, and the `Data` switch in one pass. const Pair = struct { ev: []const u8, iface: []const u8 }; var pairs: std.ArrayListUnmanaged(Pair) = .empty; var ev_it = event_map.iterator(); while (ev_it.next()) |entry| { const ev_name = entry.key_ptr.*; const iface_name = entry.value_ptr.*; if (!interfaces.contains(iface_name)) continue; if (interfaces.get(iface_name).?.fields.items.len == 0) continue; try pairs.append(a, .{ .ev = ev_name, .iface = iface_name }); } // Emit event name -> struct alias namespace var aliases: std.Io.Writer.Allocating = .init(a); const w = &aliases.writer; try w.writeAll("/// Maps browser event names to their typed structs.\n"); try w.writeAll("/// Example: `event.as(events.click, allocator)` or `event.as(events.MouseEvent, allocator)`.\n"); try w.writeAll("pub const events = struct {\n"); for (pairs.items) |p| { const safe = isValidIdent(p.ev) and !isZigKeyword(p.ev); if (safe) { try w.print(" pub const {s} = {s};\n", .{ p.ev, p.iface }); } else { try w.print(" pub const @\"{s}\" = {s};\n", .{ p.ev, p.iface }); } } try w.writeAll("};\n\n"); // Emit an explicit `Kind` enum. This is what gives editors like ZLS proper // dot-completion on `event.data(.…)` — an enum derived via // `std.meta.DeclEnum` at comptime does not currently surface its tags to // the language server, so we declare the tags literally. try w.writeAll("/// Enum of all supported DOM event names. Use with `event.data(.kind, …)`.\n"); try w.writeAll("pub const Kind = enum {\n"); for (pairs.items) |p| { const safe = isValidIdent(p.ev) and !isZigKeyword(p.ev); if (safe) { try w.print(" {s},\n", .{p.ev}); } else { try w.print(" @\"{s}\",\n", .{p.ev}); } } try w.writeAll("};\n\n"); // Emit a `Data` function mapping each `Kind` tag to its typed struct. // This mirrors `@field(events, @tagName(kind))` but keeps the mapping in // one generated place so consumers can `const T = generated.Data(.click);`. try w.writeAll("/// Returns the typed struct corresponding to a given event `Kind`.\n"); try w.writeAll("pub fn Data(comptime kind: Kind) type {\n"); try w.writeAll(" return switch (kind) {\n"); for (pairs.items) |p| { const safe = isValidIdent(p.ev) and !isZigKeyword(p.ev); if (safe) { try w.print(" .{s} => {s},\n", .{ p.ev, p.iface }); } else { try w.print(" .@\"{s}\" => {s},\n", .{ p.ev, p.iface }); } } try w.writeAll(" };\n"); try w.writeAll("}\n\n"); try w.writeAll("/// Resolves an event `type` string to its `Kind`, or null if unknown.\n"); try w.writeAll("pub fn kindForType(type_str: []const u8) ?Kind {\n"); try w.writeAll(" return kind_for_type.get(type_str);\n"); try w.writeAll("}\n"); try w.writeAll("const kind_for_type = std.StaticStringMap(Kind).initComptime(.{\n"); for (pairs.items) |p| { const safe = isValidIdent(p.ev) and !isZigKeyword(p.ev); if (safe) { try w.print(" .{{ \"{s}\", .{s} }},\n", .{ p.ev, p.ev }); } else { try w.print(" .{{ \"{s}\", .@\"{s}\" }},\n", .{ p.ev, p.ev }); } } try w.writeAll("});\n\n"); // Emit the snake_case → camelCase map used by `Event.as` at runtime to // translate Zig field names back to the original DOM property names. try w.writeAll("/// Maps snake_case Zig field names to their camelCase DOM property names.\n"); try w.writeAll("/// Fields whose snake_case equals the DOM name are omitted; callers should\n"); try w.writeAll("/// fall back to the original identifier when no mapping is present.\n"); try w.writeAll("pub const js_field_names = std.StaticStringMap([]const u8).initComptime(.{\n"); var pair_it = js_name_pairs.iterator(); // Sort entries for deterministic output. var sorted_keys: std.ArrayListUnmanaged([]const u8) = .empty; while (pair_it.next()) |e| try sorted_keys.append(a, e.key_ptr.*); std.mem.sort([]const u8, sorted_keys.items, {}, struct { fn lt(_: void, l: []const u8, r: []const u8) bool { return std.mem.lessThan(u8, l, r); } }.lt); for (sorted_keys.items) |k| { const v = js_name_pairs.get(k).?; try w.print(" .{{ \"{s}\", \"{s}\" }},\n", .{ k, v }); } try w.writeAll("});\n\n"); try w.writeAll("/// Resolves the DOM property name for a given Zig field name.\n"); try w.writeAll("/// Returns the mapped camelCase identifier, or the input unchanged when none exists.\n"); try w.writeAll("pub fn jsName(name: []const u8) []const u8 {\n"); try w.writeAll(" return js_field_names.get(name) orelse name;\n"); try w.writeAll("}"); try file.addRaw(aliases.written()); return try file.finish(); } fn isValidIdent(name: []const u8) bool { if (name.len == 0) return false; for (name, 0..) |c, i| { const ok = (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or c == '_' or (i > 0 and c >= '0' and c <= '9'); if (!ok) return false; } return true; } fn resolveInterface( name: []const u8, a: std.mem.Allocator, raw_members: *std.StringHashMap(std.ArrayListUnmanaged(std.json.Value)), raw_inheritance: *std.StringHashMap([]const u8), out: *std.StringHashMap(Interface), ) !void { if (out.contains(name)) return; var fields: std.ArrayListUnmanaged(Interface.Field) = .empty; var seen_names = std.StringHashMap(void).init(a); // Walk inheritance chain depth-first: child fields first, then parent var current: ?[]const u8 = name; while (current) |iname| { if (raw_members.get(iname)) |members| { for (members.items) |m| { const field_name = (m.object.get("name") orelse continue).string; if (seen_names.contains(field_name)) continue; const idt = m.object.get("idlType") orelse continue; const generic = (idt.object.get("generic") orelse continue).string; // Skip arrays/promises/sequences if (generic.len > 0) continue; const is_union = (idt.object.get("union") orelse continue).bool; if (is_union) continue; const nullable = (idt.object.get("nullable") orelse continue).bool; const raw_type = idt.object.get("idlType") orelse continue; if (raw_type != .string) continue; const zig_type = idlToZig(raw_type.string, nullable) orelse continue; try seen_names.put(field_name, {}); const snake = try camelToSnake(a, field_name); try fields.append(a, .{ .name = snake, .js_name = field_name, .zig_type = zig_type }); } } current = raw_inheritance.get(iname); // Break cycles if (current != null and std.mem.eql(u8, current.?, name)) break; } try out.put(name, .{ .name = try a.dupe(u8, name), .fields = fields, }); }