feat: sync generated types

This commit is contained in:
Nurul Huda (Apon) 2026-06-10 08:34:55 +06:00
parent ac90841866
commit 991fda56c1
No known key found for this signature in database
GPG Key ID: 5D3F1DE2855A2F79
10 changed files with 1569 additions and 886 deletions

View File

@ -0,0 +1,133 @@
// Demonstrates and exercises `zx.client.Event.as(T)` across every case the
// debug-build type assertion guards:
//
// 1. Correct match as(PointerEvent) in a click handler reads data
// 2. Correct match as(KeyboardEvent) in a keydown handler reads data
// 3. Correct match as(InputEvent) in an input handler reads data
// 4. Correct match as(SubmitEvent) in a submit handler reads data
// 5. Catch-all as(Event) in any handler never asserts
// 6. Deliberate mismatch as(KeyboardEvent) in a click handler debug panic
//
// Open the browser console and interact with each control to watch the typed
// fields print. In a debug build, clicking "Trigger type mismatch" panics with
// a descriptive message that's the guard working.
pub fn Page(ctx: zx.PageContext) zx.Component {
return (
<main @allocator={ctx.arena}>
<h1>Event.as() type-safety demo</h1>
<p>Open the console, then interact with each control below.</p>
<EventDemo @rendering={.client} />
</main>
);
}
pub fn EventDemo(ctx: *zx.ComponentContext) zx.Component {
return (
<div @allocator={ctx.allocator}>
<section>
<p>1. PointerEvent (click)</p>
<button onclick={ctx.bind(onClick)}>Click me</button>
</section>
<section>
<p>2. KeyboardEvent (keydown) + 3. InputEvent (input)</p>
<input
type="text"
placeholder="Type here"
onkeydown={ctx.bind(onKeyDown)}
oninput={ctx.bind(onInput)}
/>
</section>
<section>
<p>4. SubmitEvent (submit)</p>
<form onsubmit={ctx.bind(onSubmit)}>
<button type="submit">Submit form</button>
</form>
</section>
<section>
<p>5. Catch-all Event (any type is valid)</p>
<button onclick={ctx.bind(onAnyEvent)}>Read as generic Event</button>
</section>
<section>
<p>6. Deliberate mismatch (debug build panics)</p>
<button onclick={ctx.bind(onMismatch)}>Trigger type mismatch</button>
</section>
</div>
);
}
// 1. Correct: click events are backed by PointerEvent (a superset of MouseEvent).
fn onClick(ctx: *zx.client.Event.Stateful) void {
const e = ctx.as(zx.client.events.PointerEvent, zx.allocator);
zx.log.info("PointerEvent: type={s} at ({d},{d}) button={d} pointerType={s} on <{s}>", .{
e.type,
e.client_x,
e.client_y,
e.button,
e.pointer_type,
if (e.target) |t| t.tag_name else "unknown",
});
}
// 2. Correct: keydown events are backed by KeyboardEvent.
fn onKeyDown(ctx: *zx.client.Event.Stateful) void {
const e = ctx.as(zx.client.events.KeyboardEvent, zx.allocator);
zx.log.info("KeyboardEvent: key={s} code={s} ctrl={any} shift={any} repeat={any}", .{
e.key,
e.code,
e.ctrl_key,
e.shift_key,
e.repeat,
});
}
// 3. Correct: input events are backed by InputEvent.
fn onInput(ctx: *zx.client.Event.Stateful) void {
const e = ctx.as(zx.client.events.InputEvent, zx.allocator);
zx.log.info("InputEvent: type={s} inputType={s} data={s} value={s}", .{
e.type,
e.input_type,
e.data orelse "",
ctx.value() orelse "",
});
}
// 4. Correct: submit events are backed by SubmitEvent.
fn onSubmit(ctx: *zx.client.Event.Stateful) void {
ctx.preventDefault();
const e = ctx.as(zx.client.events.SubmitEvent, zx.allocator);
zx.log.info("SubmitEvent: type={s} bubbles={any} cancelable={any}", .{
e.type,
e.bubbles,
e.cancelable,
});
}
// 5. Catch-all: `Event` is valid for every event type, so the guard is skipped.
fn onAnyEvent(ctx: *zx.client.Event.Stateful) void {
const e = ctx.as(zx.client.events.Event, zx.allocator);
zx.log.info("Event (catch-all): type={s} bubbles={any} isTrusted={any}", .{
e.type,
e.bubbles,
e.is_trusted,
});
}
// 6. Mismatch: a click is NOT a KeyboardEvent. In a debug build the assertion
// in `as()` panics with a message naming the requested vs. actual type. This is
// the bug the guard exists to catch.
fn onMismatch(ctx: *zx.client.Event.Stateful) void {
zx.log.info("About to request KeyboardEvent for a click — expect a debug panic.", .{});
const e = ctx.as(zx.client.events.KeyboardEvent, zx.allocator);
zx.log.info("Unreachable in debug builds: {s}", .{e.key});
}
pub const options: zx.PageOptions = .{
.methods = &.{ .GET, .POST },
};
const zx = @import("zx");

View File

@ -1,16 +1,18 @@
const Event = @This();
const std = @import("std");
const builtin = @import("builtin");
const zx = @import("../../root.zig");
const client = @import("window.zig");
const generated_events = @import("events/generated.zig");
const js = zx.client.js;
const reactivity = client.reactivity;
const Allocator = zx.Allocator;
pub const Kind = generated_events.Kind;
const platform_role = zx.platform.role;
const Event = @This();
_internal: Internal = .{},
pub const Internal = struct {
@ -58,7 +60,35 @@ pub fn key(self: Event) ?[]const u8 {
/// Get the event data by providing zx.client.events.<Type>.
pub fn as(self: Event, comptime T: type, allocator: Allocator) T {
if (platform_role != .client) return std.mem.zeroInit(T, .{});
return readStruct(T, allocator, self.getEvent().ref);
const event = self.getEvent();
if (comptime builtin.mode == .Debug) assertType(T, event);
return readStruct(T, allocator, event.ref);
}
/// Debug-only guard: validates that requested struct `T` matches the live event.
inline fn assertType(comptime T: type, event: client.Event) void {
// The bare `Event` interface is the catch-all valid for every event.
if (comptime T == generated_events.Event) return;
const type_str = event.getType(zx.allocator) orelse return;
defer zx.allocator.free(type_str);
const kind = generated_events.kindForType(type_str) orelse return; // custom event
switch (kind) {
inline else => |k| {
if (generated_events.Data(k) != T) {
std.log.err(
\\Event.as, incorrect type requested for '{s}' event
\\
\\ Requested: {s}
\\ Expected: {s}
\\
,
.{ type_str, @typeName(T), @typeName(generated_events.Data(k)) },
);
}
},
}
}
pub fn data(self: Event, comptime kind: Kind, allocator: Allocator) Data(kind) {

View File

@ -1736,6 +1736,7 @@ pub const WheelEvent = struct {
delta_y: f64 = 0,
delta_z: f64 = 0,
delta_mode: u32 = 0,
momentum: bool = false,
page_x: f64 = 0,
page_y: f64 = 0,
x: f64 = 0,
@ -2191,6 +2192,7 @@ pub const events = struct {
pub const connect = Event;
pub const readingerror = Event;
pub const selectionchange = Event;
pub const toolchange = Event;
pub const open = Event;
pub const sinkchange = Event;
pub const beforetoggle = ToggleEvent;
@ -2249,6 +2251,7 @@ pub const events = struct {
pub const freeze = Event;
pub const notificationclose = NotificationEvent;
pub const gamepadconnected = GamepadEvent;
pub const quotaoverflow = Event;
pub const pointermove = PointerEvent;
pub const mouseover = MouseEvent;
pub const cuechange = Event;
@ -2288,7 +2291,7 @@ pub const events = struct {
pub const remove = AnimationPlaybackEvent;
pub const fetch = FetchEvent;
pub const screenschange = Event;
pub const blur = Event;
pub const blur = FocusEvent;
pub const pointerenter = PointerEvent;
pub const characterboundsupdate = CharacterBoundsUpdateEvent;
pub const tonechange = RTCDTMFToneChangeEvent;
@ -2339,7 +2342,7 @@ pub const events = struct {
pub const show = Event;
pub const validationstatuschange = Event;
pub const prerenderingchange = Event;
pub const focus = Event;
pub const focus = FocusEvent;
pub const gatheringstatechange = Event;
pub const midimessage = MIDIMessageEvent;
pub const selectstart = Event;
@ -2457,7 +2460,6 @@ pub const events = struct {
pub const play = Event;
pub const startstreaming = Event;
pub const stop = Event;
pub const onerror = Event;
pub const navbeforefocus = NavigationEvent;
pub const devicemotion = DeviceMotionEvent;
pub const selectedcandidatepairchange = Event;
@ -2468,18 +2470,19 @@ pub const events = struct {
pub const reflectionchange = Event;
pub const messageerror = Event;
pub const compositionend = CompositionEvent;
pub const contextoverflow = Event;
pub const removesourcebuffer = Event;
pub const repeat = TimeEvent;
pub const animationcancel = AnimationEvent;
pub const resourcetimingbufferfull = Event;
pub const repeat = TimeEvent;
pub const connectionavailable = PresentationConnectionAvailableEvent;
pub const click = Event;
pub const click = PointerEvent;
pub const loadingdone = FontFaceSetLoadEvent;
pub const soundstart = Event;
pub const resourcetimingbufferfull = Event;
pub const navigatesuccess = Event;
pub const soundstart = Event;
pub const webglcontextcreationerror = WebGLContextEvent;
pub const zoomlevelchange = Event;
pub const dispose = Event;
pub const zoomlevelchange = Event;
pub const beforeunload = BeforeUnloadEvent;
pub const lostpointercapture = PointerEvent;
};
@ -2504,6 +2507,7 @@ pub const Kind = enum {
connect,
readingerror,
selectionchange,
toolchange,
open,
sinkchange,
beforetoggle,
@ -2562,6 +2566,7 @@ pub const Kind = enum {
freeze,
notificationclose,
gamepadconnected,
quotaoverflow,
pointermove,
mouseover,
cuechange,
@ -2770,7 +2775,6 @@ pub const Kind = enum {
play,
startstreaming,
stop,
onerror,
navbeforefocus,
devicemotion,
selectedcandidatepairchange,
@ -2781,18 +2785,19 @@ pub const Kind = enum {
reflectionchange,
messageerror,
compositionend,
contextoverflow,
removesourcebuffer,
repeat,
animationcancel,
resourcetimingbufferfull,
repeat,
connectionavailable,
click,
loadingdone,
soundstart,
resourcetimingbufferfull,
navigatesuccess,
soundstart,
webglcontextcreationerror,
zoomlevelchange,
dispose,
zoomlevelchange,
beforeunload,
lostpointercapture,
};
@ -2818,6 +2823,7 @@ pub fn Data(comptime kind: Kind) type {
.connect => Event,
.readingerror => Event,
.selectionchange => Event,
.toolchange => Event,
.open => Event,
.sinkchange => Event,
.beforetoggle => ToggleEvent,
@ -2876,6 +2882,7 @@ pub fn Data(comptime kind: Kind) type {
.freeze => Event,
.notificationclose => NotificationEvent,
.gamepadconnected => GamepadEvent,
.quotaoverflow => Event,
.pointermove => PointerEvent,
.mouseover => MouseEvent,
.cuechange => Event,
@ -2915,7 +2922,7 @@ pub fn Data(comptime kind: Kind) type {
.remove => AnimationPlaybackEvent,
.fetch => FetchEvent,
.screenschange => Event,
.blur => Event,
.blur => FocusEvent,
.pointerenter => PointerEvent,
.characterboundsupdate => CharacterBoundsUpdateEvent,
.tonechange => RTCDTMFToneChangeEvent,
@ -2966,7 +2973,7 @@ pub fn Data(comptime kind: Kind) type {
.show => Event,
.validationstatuschange => Event,
.prerenderingchange => Event,
.focus => Event,
.focus => FocusEvent,
.gatheringstatechange => Event,
.midimessage => MIDIMessageEvent,
.selectstart => Event,
@ -3084,7 +3091,6 @@ pub fn Data(comptime kind: Kind) type {
.play => Event,
.startstreaming => Event,
.stop => Event,
.onerror => Event,
.navbeforefocus => NavigationEvent,
.devicemotion => DeviceMotionEvent,
.selectedcandidatepairchange => Event,
@ -3095,23 +3101,342 @@ pub fn Data(comptime kind: Kind) type {
.reflectionchange => Event,
.messageerror => Event,
.compositionend => CompositionEvent,
.contextoverflow => Event,
.removesourcebuffer => Event,
.repeat => TimeEvent,
.animationcancel => AnimationEvent,
.resourcetimingbufferfull => Event,
.repeat => TimeEvent,
.connectionavailable => PresentationConnectionAvailableEvent,
.click => Event,
.click => PointerEvent,
.loadingdone => FontFaceSetLoadEvent,
.soundstart => Event,
.resourcetimingbufferfull => Event,
.navigatesuccess => Event,
.soundstart => Event,
.webglcontextcreationerror => WebGLContextEvent,
.zoomlevelchange => Event,
.dispose => Event,
.zoomlevelchange => Event,
.beforeunload => BeforeUnloadEvent,
.lostpointercapture => PointerEvent,
};
}
/// Resolves an event `type` string to its `Kind`, or null if unknown.
pub fn kindForType(type_str: []const u8) ?Kind {
return kind_for_type.get(type_str);
}
const kind_for_type = std.StaticStringMap(Kind).initComptime(.{
.{ "DOMActivate", .DOMActivate },
.{ "gotpointercapture", .gotpointercapture },
.{ "keyframerequest", .keyframerequest },
.{ "signalingstatechange", .signalingstatechange },
.{ "promptaction", .promptaction },
.{ "afterprint", .afterprint },
.{ "popstate", .popstate },
.{ "scroll", .scroll },
.{ "promptdismiss", .promptdismiss },
.{ "fullscreenerror", .fullscreenerror },
.{ "beforeinput", .beforeinput },
.{ "periodicsync", .periodicsync },
.{ "durationchange", .durationchange },
.{ "dragenter", .dragenter },
.{ "pagereveal", .pagereveal },
.{ "connect", .connect },
.{ "readingerror", .readingerror },
.{ "selectionchange", .selectionchange },
.{ "toolchange", .toolchange },
.{ "open", .open },
.{ "sinkchange", .sinkchange },
.{ "beforetoggle", .beforetoggle },
.{ "suspend", .@"suspend" },
.{ "versionchange", .versionchange },
.{ "pointerrawupdate", .pointerrawupdate },
.{ "result", .result },
.{ "change", .change },
.{ "drop", .drop },
.{ "playing", .playing },
.{ "contextlost", .contextlost },
.{ "message", .message },
.{ "voiceschanged", .voiceschanged },
.{ "update", .update },
.{ "selectend", .selectend },
.{ "location", .location },
.{ "keypress", .keypress },
.{ "connecting", .connecting },
.{ "downloadprogress", .downloadprogress },
.{ "boundary", .boundary },
.{ "leavepictureinpicture", .leavepictureinpicture },
.{ "mouseenter", .mouseenter },
.{ "error", .@"error" },
.{ "hashchange", .hashchange },
.{ "keystatuseschange", .keystatuseschange },
.{ "focusin", .focusin },
.{ "start", .start },
.{ "auxclick", .auxclick },
.{ "mouseout", .mouseout },
.{ "currententrychange", .currententrychange },
.{ "resize", .resize },
.{ "compositionstart", .compositionstart },
.{ "sync", .sync },
.{ "textInput", .textInput },
.{ "dragend", .dragend },
.{ "timeupdate", .timeupdate },
.{ "transitioncancel", .transitioncancel },
.{ "release", .release },
.{ "pointerover", .pointerover },
.{ "reading", .reading },
.{ "transitionstart", .transitionstart },
.{ "load", .load },
.{ "securitypolicyviolation", .securitypolicyviolation },
.{ "cut", .cut },
.{ "canplaythrough", .canplaythrough },
.{ "squeeze", .squeeze },
.{ "touchcancel", .touchcancel },
.{ "pointerdown", .pointerdown },
.{ "backgroundfetchfail", .backgroundfetchfail },
.{ "controllerchange", .controllerchange },
.{ "toggle", .toggle },
.{ "isolationchange", .isolationchange },
.{ "formdata", .formdata },
.{ "sourceended", .sourceended },
.{ "ended", .ended },
.{ "freeze", .freeze },
.{ "notificationclose", .notificationclose },
.{ "gamepadconnected", .gamepadconnected },
.{ "quotaoverflow", .quotaoverflow },
.{ "pointermove", .pointermove },
.{ "mouseover", .mouseover },
.{ "cuechange", .cuechange },
.{ "gattserverdisconnected", .gattserverdisconnected },
.{ "pointerup", .pointerup },
.{ "chargingchange", .chargingchange },
.{ "finish", .finish },
.{ "servicechanged", .servicechanged },
.{ "devicechange", .devicechange },
.{ "mousedown", .mousedown },
.{ "shippingaddresschange", .shippingaddresschange },
.{ "transitionend", .transitionend },
.{ "slotchange", .slotchange },
.{ "rejectionhandled", .rejectionhandled },
.{ "unhandledrejection", .unhandledrejection },
.{ "mouseleave", .mouseleave },
.{ "loadstart", .loadstart },
.{ "touchend", .touchend },
.{ "icecandidateerror", .icecandidateerror },
.{ "copy", .copy },
.{ "paymentrequest", .paymentrequest },
.{ "pointerout", .pointerout },
.{ "prioritychange", .prioritychange },
.{ "audiostart", .audiostart },
.{ "nomatch", .nomatch },
.{ "submit", .submit },
.{ "processorerror", .processorerror },
.{ "addsourcebuffer", .addsourcebuffer },
.{ "contextmenu", .contextmenu },
.{ "closing", .closing },
.{ "updatefound", .updatefound },
.{ "waitingforkey", .waitingforkey },
.{ "DOMFocusOut", .DOMFocusOut },
.{ "capturehandlechange", .capturehandlechange },
.{ "volumechange", .volumechange },
.{ "disconnect", .disconnect },
.{ "remove", .remove },
.{ "fetch", .fetch },
.{ "screenschange", .screenschange },
.{ "blur", .blur },
.{ "pointerenter", .pointerenter },
.{ "characterboundsupdate", .characterboundsupdate },
.{ "tonechange", .tonechange },
.{ "seeked", .seeked },
.{ "dequeue", .dequeue },
.{ "pointerlockchange", .pointerlockchange },
.{ "stalled", .stalled },
.{ "install", .install },
.{ "input", .input },
.{ "audioprocess", .audioprocess },
.{ "touchmove", .touchmove },
.{ "transitionrun", .transitionrun },
.{ "progress", .progress },
.{ "scrollend", .scrollend },
.{ "dragstart", .dragstart },
.{ "datachannel", .datachannel },
.{ "loadingerror", .loadingerror },
.{ "icegatheringstatechange", .icegatheringstatechange },
.{ "pointercancel", .pointercancel },
.{ "dischargingtimechange", .dischargingtimechange },
.{ "frameratechange", .frameratechange },
.{ "autofill", .autofill },
.{ "resume", .@"resume" },
.{ "audioend", .audioend },
.{ "levelchange", .levelchange },
.{ "chargingtimechange", .chargingtimechange },
.{ "waiting", .waiting },
.{ "availabilitychanged", .availabilitychanged },
.{ "dblclick", .dblclick },
.{ "canmakepayment", .canmakepayment },
.{ "navigate", .navigate },
.{ "unload", .unload },
.{ "push", .push },
.{ "paste", .paste },
.{ "redraw", .redraw },
.{ "animationstart", .animationstart },
.{ "beforeinstallprompt", .beforeinstallprompt },
.{ "beforexrselect", .beforexrselect },
.{ "currentscreenchange", .currentscreenchange },
.{ "dataavailable", .dataavailable },
.{ "ratechange", .ratechange },
.{ "orientationchange", .orientationchange },
.{ "trackedsourceschange", .trackedsourceschange },
.{ "updateend", .updateend },
.{ "upgradeneeded", .upgradeneeded },
.{ "webglcontextrestored", .webglcontextrestored },
.{ "statechange", .statechange },
.{ "show", .show },
.{ "validationstatuschange", .validationstatuschange },
.{ "prerenderingchange", .prerenderingchange },
.{ "focus", .focus },
.{ "gatheringstatechange", .gatheringstatechange },
.{ "midimessage", .midimessage },
.{ "selectstart", .selectstart },
.{ "layoutchange", .layoutchange },
.{ "loading", .loading },
.{ "advertisementreceived", .advertisementreceived },
.{ "contextrestored", .contextrestored },
.{ "fullscreenchange", .fullscreenchange },
.{ "inputreport", .inputreport },
.{ "unmute", .unmute },
.{ "notificationclick", .notificationclick },
.{ "addtrack", .addtrack },
.{ "bufferedamountlow", .bufferedamountlow },
.{ "begin", .begin },
.{ "deviceorientationabsolute", .deviceorientationabsolute },
.{ "keyup", .keyup },
.{ "select", .select },
.{ "animationiteration", .animationiteration },
.{ "soundend", .soundend },
.{ "textupdate", .textupdate },
.{ "dragleave", .dragleave },
.{ "DOMFocusIn", .DOMFocusIn },
.{ "mouseup", .mouseup },
.{ "canplay", .canplay },
.{ "online", .online },
.{ "backgroundfetchabort", .backgroundfetchabort },
.{ "drag", .drag },
.{ "navnotarget", .navnotarget },
.{ "speechstart", .speechstart },
.{ "pageshow", .pageshow },
.{ "blocked", .blocked },
.{ "captureaction", .captureaction },
.{ "pause", .pause },
.{ "payerdetailchange", .payerdetailchange },
.{ "activate", .activate },
.{ "dragover", .dragover },
.{ "sourceopen", .sourceopen },
.{ "updatestart", .updatestart },
.{ "paymentmethodchange", .paymentmethodchange },
.{ "serviceadded", .serviceadded },
.{ "emptied", .emptied },
.{ "visibilitychange", .visibilitychange },
.{ "wheel", .wheel },
.{ "timeout", .timeout },
.{ "cookiechange", .cookiechange },
.{ "keydown", .keydown },
.{ "animationend", .animationend },
.{ "uncapturederror", .uncapturederror },
.{ "command", .command },
.{ "pageswap", .pageswap },
.{ "beforeprint", .beforeprint },
.{ "mark", .mark },
.{ "rtctransform", .rtctransform },
.{ "endstreaming", .endstreaming },
.{ "loadend", .loadend },
.{ "backgroundfetchclick", .backgroundfetchclick },
.{ "pointerlockerror", .pointerlockerror },
.{ "geometrychange", .geometrychange },
.{ "fencedtreeclick", .fencedtreeclick },
.{ "characteristicvaluechanged", .characteristicvaluechanged },
.{ "connectionstatechange", .connectionstatechange },
.{ "exit", .exit },
.{ "deviceorientation", .deviceorientation },
.{ "textformatupdate", .textformatupdate },
.{ "contentdelete", .contentdelete },
.{ "enterpictureinpicture", .enterpictureinpicture },
.{ "track", .track },
.{ "mute", .mute },
.{ "visibilitymaskchange", .visibilitymaskchange },
.{ "compositionupdate", .compositionupdate },
.{ "bufferedchange", .bufferedchange },
.{ "touchstart", .touchstart },
.{ "pointerleave", .pointerleave },
.{ "gamepaddisconnected", .gamepaddisconnected },
.{ "capturedmousechange", .capturedmousechange },
.{ "portalactivate", .portalactivate },
.{ "pagehide", .pagehide },
.{ "serviceremoved", .serviceremoved },
.{ "DOMContentLoaded", .DOMContentLoaded },
.{ "negotiationneeded", .negotiationneeded },
.{ "webglcontextlost", .webglcontextlost },
.{ "removetrack", .removetrack },
.{ "clipboardchange", .clipboardchange },
.{ "languagechange", .languagechange },
.{ "squeezestart", .squeezestart },
.{ "storage", .storage },
.{ "speechend", .speechend },
.{ "icecandidate", .icecandidate },
.{ "squeezeend", .squeezeend },
.{ "beforematch", .beforematch },
.{ "loadeddata", .loadeddata },
.{ "shippingoptionchange", .shippingoptionchange },
.{ "terminate", .terminate },
.{ "abort", .abort },
.{ "mousemove", .mousemove },
.{ "iceconnectionstatechange", .iceconnectionstatechange },
.{ "managedconfigurationchange", .managedconfigurationchange },
.{ "offline", .offline },
.{ "pushsubscriptionchange", .pushsubscriptionchange },
.{ "readystatechange", .readystatechange },
.{ "reset", .reset },
.{ "seeking", .seeking },
.{ "contentvisibilityautostatechange", .contentvisibilityautostatechange },
.{ "end", .end },
.{ "focusout", .focusout },
.{ "loadedmetadata", .loadedmetadata },
.{ "enter", .enter },
.{ "success", .success },
.{ "invalid", .invalid },
.{ "inputsourceschange", .inputsourceschange },
.{ "appinstalled", .appinstalled },
.{ "cancel", .cancel },
.{ "close", .close },
.{ "encrypted", .encrypted },
.{ "play", .play },
.{ "startstreaming", .startstreaming },
.{ "stop", .stop },
.{ "navbeforefocus", .navbeforefocus },
.{ "devicemotion", .devicemotion },
.{ "selectedcandidatepairchange", .selectedcandidatepairchange },
.{ "sourceclose", .sourceclose },
.{ "navigateerror", .navigateerror },
.{ "backgroundfetchsuccess", .backgroundfetchsuccess },
.{ "complete", .complete },
.{ "reflectionchange", .reflectionchange },
.{ "messageerror", .messageerror },
.{ "compositionend", .compositionend },
.{ "contextoverflow", .contextoverflow },
.{ "removesourcebuffer", .removesourcebuffer },
.{ "animationcancel", .animationcancel },
.{ "repeat", .repeat },
.{ "connectionavailable", .connectionavailable },
.{ "click", .click },
.{ "loadingdone", .loadingdone },
.{ "resourcetimingbufferfull", .resourcetimingbufferfull },
.{ "navigatesuccess", .navigatesuccess },
.{ "soundstart", .soundstart },
.{ "webglcontextcreationerror", .webglcontextcreationerror },
.{ "dispose", .dispose },
.{ "zoomlevelchange", .zoomlevelchange },
.{ "beforeunload", .beforeunload },
.{ "lostpointercapture", .lostpointercapture },
});
/// Maps snake_case Zig field names to their camelCase DOM property names.
/// Fields whose snake_case equals the DOM name are omitted; callers should
/// fall back to the original identifier when no mapping is present.

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,7 @@ pub const Editor = struct {
pub fn linef(self: *Editor, comptime fmt: []const u8, args: anytype) !void {
try self.writeIndent();
try self.buffer.writer(self.allocator).print(fmt, args);
try self.buffer.print(self.allocator, fmt, args);
try self.newline();
}
@ -144,9 +144,9 @@ pub const File = struct {
}
pub fn finish(self: *File) ![]const u8 {
var out: std.ArrayList(u8) = .empty;
defer out.deinit(self.allocator());
var w = out.writer(self.allocator());
var out: std.Io.Writer.Allocating = .init(self.allocator());
defer out.deinit();
const w = &out.writer;
for (self.decls.items) |decl| {
try decl.render(w);
@ -154,7 +154,7 @@ pub const File = struct {
try w.writeByte('\n');
}
const raw = try out.toOwnedSlice(self.allocator());
const raw = out.written();
const final_allocator = self.arena.child_allocator;
const sentinel = try final_allocator.alloc(u8, raw.len + 1);
@ -168,9 +168,7 @@ pub const File = struct {
};
defer tree.deinit(final_allocator);
if (tree.errors.len != 0) {
const f = try std.Io.Dir.cwd().createFile("ast_error.zig", .{});
defer f.close();
try f.writeAll(raw);
std.debug.print("Invalid generated source:\n{s}\n", .{raw});
return error.InvalidGeneratedSource;
}

View File

@ -2,7 +2,7 @@ const std = @import("std");
const astgen = @import("astgen.zig");
const helper = @import("helper.zig");
const TypeMap = std.StringArrayHashMap([]const u8);
const TypeMap = std.array_hash_map.String([]const u8);
const UnitSupport = struct {
length: bool = false,
@ -14,22 +14,19 @@ const UnitSupport = struct {
};
const PropMeta = struct {
keywords: std.StringArrayHashMap([]const u8),
keywords: std.array_hash_map.String([]const u8),
units: UnitSupport,
href: []const u8,
prose: []const u8 = "",
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
try writeFile(allocator, "src/style/generated.zig");
pub fn main(init: std.process.Init) !void {
try writeFile(init.io, init.gpa, "src/style/generated.zig");
std.debug.print("Successfully generated src/style/generated.zig using AST-Driven engine.\n", .{});
}
pub fn writeFile(allocator: std.mem.Allocator, path: []const u8) !void {
const source = generate(allocator) catch |err| {
pub fn writeFile(io: std.Io, allocator: std.mem.Allocator, path: []const u8) !void {
const source = generate(io, allocator) catch |err| {
if (err == error.InvalidGeneratedSource) {
// We can't easily get the raw buffer from File here without changes,
// but we can try to catch it in generate() or just look at what's happening.
@ -38,19 +35,17 @@ pub fn writeFile(allocator: std.mem.Allocator, path: []const u8) !void {
};
defer allocator.free(source);
const file = try std.Io.Dir.cwd().createFile(path, .{});
defer file.close();
try file.writeAll(source);
const file = try std.Io.Dir.cwd().createFile(io, path, .{});
defer file.close(io);
try file.writeStreamingAll(io, source);
}
pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
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();
const main_file = try std.Io.Dir.cwd().openFile("vendor/webref/ed/css.json", .{});
defer main_file.close();
const main_content = try main_file.readToEndAlloc(a, 15 * 1024 * 1024);
const main_content = try std.Io.Dir.cwd().readFileAlloc(io, "vendor/webref/ed/css.json", a, .unlimited);
const main_parsed = try std.json.parseFromSlice(std.json.Value, a, main_content, .{});
defer main_parsed.deinit();
@ -59,26 +54,24 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
const types_json = root.get("types").?.array;
const selectors = root.get("selectors").?.array;
var type_map = TypeMap.init(a);
defer type_map.deinit();
var type_map: TypeMap = .empty;
defer type_map.deinit(a);
for (types_json.items) |t| {
const name = t.object.get("name").?.string;
const syntax = if (t.object.get("syntax")) |s| s.string else "";
try type_map.put(name, syntax);
try type_map.put(a, name, syntax);
}
var prose_map = std.StringHashMap([]const u8).init(a);
defer prose_map.deinit();
var kw_prose_map = std.StringHashMap(std.StringHashMap([]const u8)).init(a);
defer kw_prose_map.deinit();
var dir = try std.Io.Dir.cwd().openDir("vendor/webref/ed/css/", .{ .iterate = true });
defer dir.close();
var dir = try std.Io.Dir.cwd().openDir(io, "vendor/webref/ed/css/", .{ .iterate = true });
defer dir.close(io);
var dir_it = dir.iterate();
while (try dir_it.next()) |entry| {
while (try dir_it.next(io)) |entry| {
if (!std.mem.endsWith(u8, entry.name, ".json")) continue;
const f = try dir.openFile(entry.name, .{});
defer f.close();
const fc = try f.readToEndAlloc(a, 5 * 1024 * 1024);
const fc = try dir.readFileAlloc(io, entry.name, a, .unlimited);
const p = std.json.parseFromSlice(std.json.Value, a, fc, .{}) catch continue;
defer p.deinit();
if (p.value.object.get("properties")) |props| {
@ -101,21 +94,21 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
}
}
var prop_data = std.StringArrayHashMap(PropMeta).init(a);
defer prop_data.deinit();
var prop_data: std.array_hash_map.String(PropMeta) = .empty;
defer prop_data.deinit(a);
for (properties.items) |prop| {
const name = prop.object.get("name").?.string;
const syntax = if (prop.object.get("syntax")) |s| s.string else "";
const href = if (prop.object.get("href")) |h| h.string else "";
var keywords = std.StringArrayHashMap([]const u8).init(a);
try keywords.put("none", "");
var keywords: std.array_hash_map.String([]const u8) = .empty;
try keywords.put(a, "none", "");
const globals = [_][]const u8{ "inherit", "initial", "revert", "revert-layer", "unset" };
for (globals) |g| {
if (!keywords.contains(g)) try keywords.put(g, "");
if (!keywords.contains(g)) try keywords.put(a, g, "");
}
var units = UnitSupport{};
try resolveMetaEnriched(a, syntax, &type_map, &keywords, &units, 0, kw_prose_map.get(name));
try prop_data.put(name, .{ .keywords = keywords, .units = units, .href = href, .prose = prose_map.get(name) orelse "" });
try prop_data.put(a, name, .{ .keywords = keywords, .units = units, .href = href, .prose = prose_map.get(name) orelse "" });
}
var file = astgen.File.init(allocator);
@ -141,14 +134,14 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
const prop_union = try file.addUnion(final_type_name, "enum");
try prop_union.setDoc(fa, prop_doc);
var tags = std.StringArrayHashMap(void).init(a);
var tags: std.array_hash_map.String(void) = .empty;
var k_it = data.keywords.iterator();
while (k_it.next()) |k_entry| {
const clean_kw = try cleanName(a, k_entry.key_ptr.*, .snake);
if (clean_kw.len > 0 and !tags.contains(clean_kw)) {
const kw_doc = if (k_entry.value_ptr.*.len > 0) try docText(fa, null, k_entry.value_ptr.*, null) else "";
try prop_union.addField(fa, kw_doc, clean_kw, "", null);
try tags.put(clean_kw, {});
try tags.put(a, clean_kw, {});
}
}
@ -214,8 +207,8 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
try style_struct.addField(fa, "", clean_p, final_type_name, ".none");
}
var selector_tags = std.StringArrayHashMap(void).init(a);
defer selector_tags.deinit();
var selector_tags: std.array_hash_map.String(void) = .empty;
defer selector_tags.deinit(a);
for (selectors.items) |sel| {
const name = sel.object.get("name").?.string;
if (std.mem.indexOf(u8, name, "(") != null) continue;
@ -234,7 +227,7 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
}
if (is_duplicate) continue;
try selector_tags.put(clean_s, {});
try selector_tags.put(a, clean_s, {});
try style_struct.addField(fa, "", clean_s, "?*const Style", "null");
}
@ -298,7 +291,7 @@ fn cleanName(allocator: std.mem.Allocator, name: []const u8, case: enum { pascal
return result;
}
fn resolveMetaEnriched(allocator: std.mem.Allocator, syntax: []const u8, type_map: *TypeMap, keywords: *std.StringArrayHashMap([]const u8), units: *UnitSupport, depth: usize, kprose: ?std.StringHashMap([]const u8)) !void {
fn resolveMetaEnriched(allocator: std.mem.Allocator, syntax: []const u8, type_map: *TypeMap, keywords: *std.array_hash_map.String([]const u8), units: *UnitSupport, depth: usize, kprose: ?std.StringHashMap([]const u8)) !void {
if (depth > 10) return;
const is_shorthand = std.mem.indexOf(u8, syntax, "{1,4}") != null or std.mem.indexOf(u8, syntax, "{1,2}") != null;
@ -360,15 +353,15 @@ fn resolveMetaEnriched(allocator: std.mem.Allocator, syntax: []const u8, type_ma
if (std.mem.eql(u8, cleaned, "unset")) break :blk "The unset CSS keyword resets a property to its inherited value if the property naturally inherits from its parent, and to its initial value otherwise.";
break :blk "";
};
try keywords.put(try allocator.dupe(u8, cleaned), try allocator.dupe(u8, pr));
try keywords.put(allocator, try allocator.dupe(u8, cleaned), try allocator.dupe(u8, pr));
}
}
}
fn docText(allocator: std.mem.Allocator, title: ?[]const u8, prose: []const u8, href: ?[]const u8) ![]const u8 {
var list: std.ArrayList(u8) = .empty;
defer list.deinit(allocator);
const w = list.writer(allocator);
var list: std.Io.Writer.Allocating = .init(allocator);
defer list.deinit();
const w = &list.writer;
var first = true;
if (title) |t| {
try w.print("{s}", .{t});
@ -383,7 +376,7 @@ fn docText(allocator: std.mem.Allocator, title: ?[]const u8, prose: []const u8,
if (!first) try w.writeAll("\n\n");
try w.print("- **W3C**: {s}", .{h});
}
return try list.toOwnedSlice(allocator);
return try allocator.dupe(u8, list.written());
}
fn isZigKeyword(name: []const u8) bool {

View File

@ -105,19 +105,20 @@ const Interface = struct {
};
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const source = try generate(allocator);
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);
const file = try std.Io.Dir.cwd().createFile("src/runtime/client/events/generated.zig", .{});
defer file.close();
try file.writeAll(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(allocator: std.mem.Allocator) ![]const u8 {
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();
@ -131,14 +132,12 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
).init(a);
var raw_inheritance = std.StringHashMap([]const u8).init(a);
var spec_dir = try std.Io.Dir.cwd().openDir("vendor/webref/ed/idlparsed", .{ .iterate = true });
defer spec_dir.close();
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()) |entry| {
while (try it.next(io)) |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 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;
@ -204,9 +203,7 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
// -------------------------------------------------------------------------
// Step 3: load events.json event-name to interface map
// -------------------------------------------------------------------------
const events_file = try std.Io.Dir.cwd().openFile("vendor/webref/ed/events.json", .{});
defer events_file.close();
const events_content = try events_file.readToEndAlloc(a, 4 * 1024 * 1024);
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;
@ -220,6 +217,22 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
}
}
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
// -------------------------------------------------------------------------
@ -293,8 +306,8 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
}
// Emit event name -> struct alias namespace
var aliases: std.ArrayListUnmanaged(u8) = .empty;
const w = aliases.writer(a);
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");
@ -341,6 +354,21 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
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");
@ -367,7 +395,7 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
try w.writeAll(" return js_field_names.get(name) orelse name;\n");
try w.writeAll("}");
try file.addRaw(try aliases.toOwnedSlice(a));
try file.addRaw(aliases.written());
return try file.finish();
}

View File

@ -1,11 +1,7 @@
const std = @import("std");
pub fn calcExprSource(allocator: std.mem.Allocator) ![]const u8 {
var out: std.ArrayList(u8) = .empty;
defer out.deinit(allocator);
const w = out.writer(allocator);
try w.writeAll(
return try allocator.dupe(u8,
\\pub const CalcExpr = struct {
\\ const Self = @This();
\\ pub const Unit = enum {
@ -138,11 +134,9 @@ pub fn calcExprSource(allocator: std.mem.Allocator) ![]const u8 {
\\ return self.combine(Self.numberLeaf(factor), .div);
\\ }
\\
\\ pub fn format(self: Self, w: *std.io.Writer) std.io.Writer.Error!void {
\\ pub fn format(self: Self, w: *std.Io.Writer) std.Io.Writer.Error!void {
\\ try w.writeAll(self.text());
\\ }
\\};
);
return try out.toOwnedSlice(allocator);
}

View File

@ -27,6 +27,6 @@
"repo": "git@github.com:w3c/webref.git",
"https": "https://github.com/w3c/webref",
"branch": "curated",
"commit": "fb5c62408f5cc49e5cdafa725af9135208bf433d"
"commit": "8e1af5a34d1229a3019a34caa424ef7ecd1faeaa"
}
}

View File

@ -237,6 +237,11 @@ export class ZigJS {
return;
}
if (typeof val === "bigint") {
view.setFloat64(out, Number(val), true);
return;
}
// Determine our ID
let id = this.idPool.pop();
if (id === undefined) {