mirror of
https://github.com/ziex-dev/ziex.git
synced 2026-07-20 02:29:36 -06:00
feat(csr): client event type generation
This commit is contained in:
parent
689503e0ff
commit
ff24de0290
22
build.zig
22
build.zig
@ -218,6 +218,28 @@ pub fn build(b: *std.Build) !void {
|
||||
css_gen_step.dependOn(&css_gen_run.step);
|
||||
}
|
||||
|
||||
// --- Steps: Events Generator --- //
|
||||
{
|
||||
const events_gen_step = b.step("eventsgen", "Generate DOM event types from webref");
|
||||
|
||||
const events_gen_exe = b.addExecutable(.{
|
||||
.name = "eventsgen",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("tools/codegen/events.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
const events_gen_run = b.addRunArtifact(events_gen_exe);
|
||||
|
||||
if (std.fs.cwd().access("vendor/webref", .{})) |_| {} else |_| {
|
||||
const sync_cmd = b.addSystemCommand(&.{ "./tools/syncvendor", "webref" });
|
||||
events_gen_run.step.dependOn(&sync_cmd.step);
|
||||
}
|
||||
|
||||
events_gen_step.dependOn(&events_gen_run.step);
|
||||
}
|
||||
|
||||
// --- ZX Releases (Cross-compilation targets for all platforms) --- //
|
||||
{
|
||||
const release_targets = [_]struct {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "zx-injection",
|
||||
"scopeName": "source.zx.injection",
|
||||
"injectionSelector": "L:source.zig, L:source.zx",
|
||||
"injectionSelector": "L:(source.zig, source.zx) - string - comment",
|
||||
"patterns": [
|
||||
{
|
||||
"comment": "SQL injection for db/database run/query/exec/prepare",
|
||||
|
||||
@ -593,26 +593,28 @@ const TAG_NAMES = [
|
||||
] as const;
|
||||
|
||||
const DELEGATED_EVENTS = [
|
||||
'click', 'dblclick',
|
||||
'input', 'change', 'submit',
|
||||
'focus', 'blur',
|
||||
'keydown', 'keyup', 'keypress',
|
||||
'mouseenter', 'mouseleave',
|
||||
'mousedown', 'mouseup', 'mousemove',
|
||||
'touchstart', 'touchend', 'touchmove',
|
||||
'scroll',
|
||||
{ domType: 'click', eventTypeId: 0 },
|
||||
{ domType: 'dblclick', eventTypeId: 1 },
|
||||
{ domType: 'input', eventTypeId: 2 },
|
||||
{ domType: 'change', eventTypeId: 3 },
|
||||
{ domType: 'submit', eventTypeId: 4 },
|
||||
// focus/blur do not bubble, so delegated handlers must listen to focusin/focusout.
|
||||
{ domType: 'focusin', eventTypeId: 5 },
|
||||
{ domType: 'focusout', eventTypeId: 6 },
|
||||
{ domType: 'keydown', eventTypeId: 7 },
|
||||
{ domType: 'keyup', eventTypeId: 8 },
|
||||
{ domType: 'keypress', eventTypeId: 9 },
|
||||
{ domType: 'mouseenter', eventTypeId: 10 },
|
||||
{ domType: 'mouseleave', eventTypeId: 11 },
|
||||
{ domType: 'mousedown', eventTypeId: 12 },
|
||||
{ domType: 'mouseup', eventTypeId: 13 },
|
||||
{ domType: 'mousemove', eventTypeId: 14 },
|
||||
{ domType: 'touchstart', eventTypeId: 15 },
|
||||
{ domType: 'touchend', eventTypeId: 16 },
|
||||
{ domType: 'touchmove', eventTypeId: 17 },
|
||||
{ domType: 'scroll', eventTypeId: 18 },
|
||||
] as const;
|
||||
|
||||
type DelegatedEvent = typeof DELEGATED_EVENTS[number];
|
||||
|
||||
const EVENT_TYPE_MAP: Record<DelegatedEvent, number> = {
|
||||
'click': 0, 'dblclick': 1, 'input': 2, 'change': 3, 'submit': 4,
|
||||
'focus': 5, 'blur': 6, 'keydown': 7, 'keyup': 8, 'keypress': 9,
|
||||
'mouseenter': 10, 'mouseleave': 11, 'mousedown': 12, 'mouseup': 13,
|
||||
'mousemove': 14, 'touchstart': 15, 'touchend': 16, 'touchmove': 17,
|
||||
'scroll': 18,
|
||||
};
|
||||
|
||||
const eventHandlerModes = new Map<bigint, number>();
|
||||
|
||||
/** Initialize event delegation */
|
||||
@ -622,23 +624,23 @@ export function initEventDelegation(bridge: ZxBridge, rootSelector: string = 'bo
|
||||
|
||||
const removers: Array<() => void> = [];
|
||||
|
||||
for (const eventType of DELEGATED_EVENTS) {
|
||||
for (const delegatedEvent of DELEGATED_EVENTS) {
|
||||
const listener = (event: Event) => {
|
||||
let target = event.target as HTMLElement | null;
|
||||
while (target && target !== document.body) {
|
||||
const zxRef = (target as any).__zx_ref;
|
||||
if (zxRef !== undefined) {
|
||||
bridge.eventbridge(BigInt(zxRef), EVENT_TYPE_MAP[eventType] ?? 0, event);
|
||||
bridge.eventbridge(BigInt(zxRef), delegatedEvent.eventTypeId, event);
|
||||
if (event.cancelBubble) break;
|
||||
}
|
||||
target = target.parentElement;
|
||||
}
|
||||
};
|
||||
|
||||
const options = { passive: eventType.startsWith('touch') || eventType === 'scroll' };
|
||||
root.addEventListener(eventType, listener, options);
|
||||
const options = { passive: delegatedEvent.domType.startsWith('touch') || delegatedEvent.domType === 'scroll' };
|
||||
root.addEventListener(delegatedEvent.domType, listener, options);
|
||||
// @ts-ignore
|
||||
removers.push(() => root.removeEventListener(eventType, listener, options));
|
||||
removers.push(() => root.removeEventListener(delegatedEvent.domType, listener, options));
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
@ -19,19 +19,77 @@ pub fn ClientCounter(ctx: *zx.ComponentContext) !zx.Component {
|
||||
return (
|
||||
<div @allocator={ctx.allocator}>
|
||||
<button onclick={ctx.bind(onclick)}>
|
||||
Client count: {if (count.get() == 0)("...")else (<>{count}</>)}
|
||||
Client count: {if (count.get() == 0) ("...") else (<>{count}</>)}
|
||||
</button>
|
||||
<div style="margin-top: 1rem;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type here to test generated events"
|
||||
onfocus={ctx.bind(onfocus)}
|
||||
oninput={ctx.bind(oninput)}
|
||||
onkeydown={ctx.bind(onkeydown)}
|
||||
/>
|
||||
<form onsubmit={ctx.bind(onsubmit)} style="margin-top: 0.75rem;">
|
||||
<button type="submit">Submit Test Form</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fn onclick(ctx: *zx.client.Event.Stateful) void {
|
||||
zx.log.info("counter click\n", .{});
|
||||
|
||||
const data = ctx.data(.mousedown, zx.allocator);
|
||||
zx.log.info("event: type={s} at ({d},{d}) on <{s}>", .{
|
||||
data.type,
|
||||
data.clientX,
|
||||
data.clientY,
|
||||
if (data.target) |target| target.tagName else "unknown",
|
||||
});
|
||||
|
||||
tryonclick(ctx) catch |e| {
|
||||
zx.log.err("Error in onclick handler: {s}", .{@errorName(e)});
|
||||
};
|
||||
}
|
||||
|
||||
fn onfocus(ctx: *zx.client.Event.Stateful) void {
|
||||
const data = ctx.data(.focusin, zx.allocator);
|
||||
zx.log.info("focus: type={s} relatedTarget?={any}", .{
|
||||
data.type,
|
||||
data.relatedTarget != null,
|
||||
});
|
||||
}
|
||||
|
||||
fn oninput(ctx: *zx.client.Event.Stateful) void {
|
||||
const data = ctx.data(.input, zx.allocator);
|
||||
zx.log.info("input: type={s} value={s} bubbles={any}", .{
|
||||
data.type,
|
||||
ctx.value() orelse "",
|
||||
data.bubbles,
|
||||
});
|
||||
}
|
||||
|
||||
fn onkeydown(ctx: *zx.client.Event.Stateful) void {
|
||||
const data = ctx.data(.keydown, zx.allocator);
|
||||
zx.log.info("keydown: key={s} code={s} ctrl={any} shift={any}", .{
|
||||
data.key,
|
||||
data.code,
|
||||
data.ctrlKey,
|
||||
data.shiftKey,
|
||||
});
|
||||
}
|
||||
|
||||
fn onsubmit(ctx: *zx.client.Event.Stateful) void {
|
||||
ctx.preventDefault();
|
||||
const data = ctx.data(.submit, zx.allocator);
|
||||
zx.log.info("submit: type={s} bubbles={any} cancelable={any}", .{
|
||||
data.type,
|
||||
data.bubbles,
|
||||
data.cancelable,
|
||||
});
|
||||
}
|
||||
|
||||
fn init(count: *zx.State(u64)) !void {
|
||||
if (zx.platform.role != .client or initialized)
|
||||
return;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const window = @import("client/window.zig");
|
||||
|
||||
pub const Event = @import("client/Event.zig");
|
||||
pub const events = @import("client/events_generated.zig");
|
||||
pub const jsx = @import("client/jsx.zig");
|
||||
|
||||
// Legacy --- may get removed/renamed
|
||||
|
||||
@ -7,10 +7,12 @@ const std = @import("std");
|
||||
const zx = @import("../../root.zig");
|
||||
const pltfm = @import("../../platform.zig");
|
||||
const client = @import("window.zig");
|
||||
const generated_events = @import("events_generated.zig");
|
||||
const reactivity = client.reactivity;
|
||||
|
||||
const platform_role = pltfm.platform.role;
|
||||
const gpa = if (@import("builtin").os.tag == .freestanding) std.heap.wasm_allocator else std.heap.page_allocator;
|
||||
pub const Kind = generated_events.Kind;
|
||||
|
||||
const Event = @This();
|
||||
|
||||
@ -52,6 +54,68 @@ pub fn key(self: Event) ?[]const u8 {
|
||||
return event.ref.getAlloc(real_js.String, gpa, "key") catch null;
|
||||
}
|
||||
|
||||
/// Get the event data by providing zx.client.events.<Type>.
|
||||
pub fn as(self: Event, comptime T: type, allocator: std.mem.Allocator) T {
|
||||
if (platform_role != .client) return std.mem.zeroInit(T, .{});
|
||||
return readStruct(T, allocator, self.getEvent().ref);
|
||||
}
|
||||
|
||||
pub fn data(self: Event, comptime kind: Kind, allocator: std.mem.Allocator) Data(kind) {
|
||||
return self.as(Data(kind), allocator);
|
||||
}
|
||||
|
||||
pub const Data = generated_events.Data;
|
||||
|
||||
fn readStruct(comptime T: type, allocator: std.mem.Allocator, obj: @import("js").Object) T {
|
||||
const info = @typeInfo(T).@"struct";
|
||||
var result: T = std.mem.zeroInit(T, .{});
|
||||
inline for (info.fields) |field| {
|
||||
if (readField(field.type, allocator, obj, field.name)) |v| {
|
||||
@field(result, field.name) = v;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn readField(comptime F: type, allocator: std.mem.Allocator, obj: @import("js").Object, comptime name: []const u8) ?F {
|
||||
const real_js = @import("js");
|
||||
const finfo = @typeInfo(F);
|
||||
const Child = if (finfo == .optional) finfo.optional.child else F;
|
||||
const child_info = @typeInfo(Child);
|
||||
const raw = obj.value.get(name) catch return null;
|
||||
defer raw.deinit();
|
||||
|
||||
const raw_type = raw.typeOf();
|
||||
if (raw_type == .null or raw_type == .undefined) return null;
|
||||
|
||||
switch (child_info) {
|
||||
.@"struct" => {
|
||||
if (raw_type != .object and raw_type != .function) return null;
|
||||
const sub = real_js.Object{ .value = raw };
|
||||
return readStruct(Child, allocator, sub);
|
||||
},
|
||||
.pointer => |p| {
|
||||
if (p.size == .slice and p.child == u8) {
|
||||
if (raw_type != .string) return null;
|
||||
return raw.string(allocator) catch null;
|
||||
}
|
||||
@compileError("unsupported pointer field type in Event.as: " ++ @typeName(F));
|
||||
},
|
||||
.int, .float => {
|
||||
if (raw_type != .number) return null;
|
||||
if (child_info == .int) {
|
||||
return @as(Child, @intFromFloat(raw.float() catch return null));
|
||||
}
|
||||
return @as(Child, @floatCast(raw.float() catch return null));
|
||||
},
|
||||
.bool => {
|
||||
if (raw_type != .boolean) return null;
|
||||
return raw.boolean() catch null;
|
||||
},
|
||||
else => @compileError("unsupported field type in Event.as: " ++ @typeName(F)),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Stateful --- //
|
||||
|
||||
/// Stateful client event - provides `state()` access to bound component state.
|
||||
@ -88,4 +152,12 @@ pub const Stateful = struct {
|
||||
pub fn key(self: Stateful) ?[]const u8 {
|
||||
return self._inner.key();
|
||||
}
|
||||
|
||||
pub fn as(self: Stateful, comptime T: type, allocator: std.mem.Allocator) T {
|
||||
return self._inner.as(T, allocator);
|
||||
}
|
||||
|
||||
pub fn data(self: Stateful, comptime kind: Kind, allocator: std.mem.Allocator) Data(kind) {
|
||||
return self._inner.data(kind, allocator);
|
||||
}
|
||||
};
|
||||
|
||||
3112
src/runtime/client/events_generated.zig
Normal file
3112
src/runtime/client/events_generated.zig
Normal file
File diff suppressed because it is too large
Load Diff
366
tools/codegen/events.zig
Normal file
366
tools/codegen/events.zig
Normal file
@ -0,0 +1,366 @@
|
||||
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});
|
||||
}
|
||||
|
||||
// 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,
|
||||
zig_type: []const u8,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
const source = try generate(allocator);
|
||||
defer allocator.free(source);
|
||||
const file = try std.fs.cwd().createFile("src/runtime/client/events_generated.zig", .{});
|
||||
defer file.close();
|
||||
try file.writeAll(source);
|
||||
std.debug.print("Generated src/runtime/client/events_generated.zig\n", .{});
|
||||
}
|
||||
|
||||
pub fn generate(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.fs.cwd().openDir("vendor/webref/ed/idlparsed", .{ .iterate = true });
|
||||
defer spec_dir.close();
|
||||
var it = spec_dir.iterate();
|
||||
while (try it.next()) |entry| {
|
||||
if (!std.mem.endsWith(u8, entry.name, ".json")) continue;
|
||||
const f = try spec_dir.openFile(entry.name, .{});
|
||||
defer f.close();
|
||||
const content = try f.readToEndAlloc(a, 8 * 1024 * 1024);
|
||||
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_file = try std.fs.cwd().openFile("vendor/webref/ed/events.json", .{});
|
||||
defer events_file.close();
|
||||
const events_content = try events_file.readToEndAlloc(a, 4 * 1024 * 1024);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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)`.
|
||||
);
|
||||
|
||||
// Emit EventTarget helper struct first
|
||||
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 {
|
||||
\\ tagName: []const u8 = "",
|
||||
\\ id: []const u8 = "",
|
||||
\\ name: []const u8 = "",
|
||||
\\ value: []const u8 = "",
|
||||
\\ @"type": []const u8 = "",
|
||||
\\ checked: bool = false,
|
||||
\\ disabled: bool = false,
|
||||
\\};
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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.ArrayListUnmanaged(u8) = .empty;
|
||||
const w = aliases.writer(a);
|
||||
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("}");
|
||||
try file.addRaw(try aliases.toOwnedSlice(a));
|
||||
|
||||
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, {});
|
||||
try fields.append(a, .{ .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,
|
||||
});
|
||||
}
|
||||
@ -27,6 +27,6 @@
|
||||
"repo": "git@github.com:w3c/webref.git",
|
||||
"https": "https://github.com/w3c/webref",
|
||||
"branch": "curated",
|
||||
"commit": "b1bed72dca526e68a6f0aac228f5c2550f5e93ee"
|
||||
"commit": "fb5c62408f5cc49e5cdafa725af9135208bf433d"
|
||||
}
|
||||
}
|
||||
|
||||
6
vendor/jsz/js/src/zigjs.ts
vendored
6
vendor/jsz/js/src/zigjs.ts
vendored
@ -230,6 +230,12 @@ export class ZigJS {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof val === "boolean") {
|
||||
view.setUint32(out, val ? predefined.true : predefined.false, true);
|
||||
view.setUint32(out + 4, NAN_PREFIX, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine our ID
|
||||
let id = this.idPool.pop();
|
||||
if (id === undefined) {
|
||||
|
||||
13
vendor/jsz/js/tests/zigjs.test.ts
vendored
13
vendor/jsz/js/tests/zigjs.test.ts
vendored
@ -179,6 +179,19 @@ test('valueSet: ref', () => {
|
||||
expect(globalThis[key]).toEqual(true);
|
||||
});
|
||||
|
||||
test('storeValue: boolean uses predefined refs', () => {
|
||||
const st = new ZigJS();
|
||||
const memory = new WebAssembly.Memory({ initial: 1 });
|
||||
const view = new DataView(memory.buffer);
|
||||
st.memory = memory;
|
||||
|
||||
st.storeValue(0, true);
|
||||
expect(view.getUint32(0, true)).toEqual(predefined.true);
|
||||
|
||||
st.storeValue(8, false);
|
||||
expect(view.getUint32(8, true)).toEqual(predefined.false);
|
||||
});
|
||||
|
||||
test('valueStringCreate', () => {
|
||||
const st = new ZigJS();
|
||||
const obj = st.importObject();
|
||||
|
||||
4
vendor/jsz/src/value.zig
vendored
4
vendor/jsz/src/value.zig
vendored
@ -138,7 +138,7 @@ pub const Value = enum(u64) {
|
||||
/// Get the value of a property of an object.
|
||||
pub fn get(self: Value, n: []const u8) !Value {
|
||||
if (self.typeOf() != .object) return js.Error.InvalidType;
|
||||
var result: u64 = undefined;
|
||||
var result: u64 = @bitCast(js.Ref.undefined);
|
||||
ext.valueGet(&result, self.ref().id, n.ptr, n.len);
|
||||
return @enumFromInt(result);
|
||||
}
|
||||
@ -177,7 +177,7 @@ pub const Value = enum(u64) {
|
||||
}
|
||||
|
||||
/// Returns the bool value if this is a boolean.
|
||||
pub fn boolean(self: Value) !f64 {
|
||||
pub fn boolean(self: Value) !bool {
|
||||
if (self.typeOf() != .boolean) return js.Error.InvalidType;
|
||||
return self == .true;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user