feat(csr): client event type generation

This commit is contained in:
Nurul Huda (Apon) 2026-04-18 23:39:41 +06:00
parent 689503e0ff
commit ff24de0290
No known key found for this signature in database
GPG Key ID: 5D3F1DE2855A2F79
12 changed files with 3680 additions and 28 deletions

View File

@ -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 {

View File

@ -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",

View File

@ -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 () => {

View File

@ -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;

View File

@ -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

View File

@ -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);
}
};

File diff suppressed because it is too large Load Diff

366
tools/codegen/events.zig Normal file
View 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,
});
}

View File

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

View File

@ -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) {

View File

@ -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();

View File

@ -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;
}