-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathx.zig
More file actions
643 lines (551 loc) · 25.2 KB
/
x.zig
File metadata and controls
643 lines (551 loc) · 25.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
const std = @import("std");
const zx = @import("root.zig");
const prp = @import("props.zig");
const ElementTag = zx.ElementTag;
const Element = zx.Element;
const Allocator = std.mem.Allocator;
const BuiltinAttribute = zx.BuiltinAttribute;
const Component = zx.Component;
const ElementAttribute = zx.Element.Attribute;
const reactivity = zx.client.reactivity;
const platform = zx.platform;
//TODO: Do not escape ahead of time, remove escaping from this place
const escapHtmlTextNode = zx.util.html.escapeText;
pub const ClientComponentOptions = struct {
name: []const u8,
path: []const u8,
id: []const u8,
};
pub const ComponentClientOptions = struct {
name: []const u8,
id: []const u8,
};
const Options = struct {
children: ?[]const Component = null,
attributes: ?[]const Element.Attribute = null,
allocator: ?std.mem.Allocator = null,
escaping: ?BuiltinAttribute.Escaping = .html,
rendering: ?BuiltinAttribute.Rendering = .server,
async: ?BuiltinAttribute.Async = .sync,
fallback: ?*const Component = null,
caching: ?BuiltinAttribute.Caching = null,
client: ?ComponentClientOptions = null,
/// Component name used for devtools / debugging.
/// Pass `null` (or omit) in release builds to reduce binary size.
name: ?[]const u8 = null,
};
/// Initialize a Context without an allocator
/// The allocator must be provided via @allocator attribute on the parent element
pub fn init() Context {
return .{ .allocator = std.heap.page_allocator };
}
/// Initialize a Context with an allocator (for backward compatibility with direct API usage)
pub fn allocInit(allocator: std.mem.Allocator) Context {
return .{ .allocator = allocator };
}
/// Create a lazy component from a function
/// The function will be invoked during rendering, allowing for dynamic slot handling
/// Supports functions with 0 params (), 1 param (allocator), or 2 params (allocator, props)
pub fn lazy(allocator: Allocator, comptime func: anytype, props: anytype) Component {
return .{ .component_fn = Component.ComponentFn.init(func, allocator, props) };
}
/// Context for creating components with allocator support
const Context = struct {
allocator: ?std.mem.Allocator = null,
pub fn getAlloc(self: *Context) std.mem.Allocator {
return self.allocator orelse @panic("Allocator not set. Please provide @allocator attribute to the parent element.");
}
fn escapeHtml(self: *Context, text: []const u8) []const u8 {
// On browser, DOM APIs (textContent) handle escaping automatically
// We only need to escape when generating HTML strings on the server
// TODO: we would want to move the escaping logic at the time of rendering the element, and simply not use escapeHtml for client side rendering
if (platform.role == .client) return text;
const allocator = self.getAlloc();
// Use a buffer writer to leverage the shared escaping logic
// For text content, we only escape & < > (not quotes)
var aw = std.io.Writer.Allocating.init(allocator);
defer aw.deinit();
escapHtmlTextNode(&aw.writer, text) catch @panic("OOM");
return allocator.dupe(u8, aw.written()) catch @panic("OOM");
}
pub fn ele(self: *Context, tag: ElementTag, options: Options) Component {
// Set allocator from @allocator option if provided
if (options.allocator) |allocator| {
self.allocator = allocator;
}
const allocator = self.getAlloc();
// Allocate and copy children if provided
const children_copy = if (options.children) |children| blk: {
const copy = allocator.alloc(Component, children.len) catch @panic("OOM");
@memcpy(copy, children);
break :blk copy;
} else null;
// Allocate and copy attributes if provided
const attributes_copy = if (options.attributes) |attributes| blk: {
const copy = allocator.alloc(Element.Attribute, attributes.len) catch @panic("OOM");
@memcpy(copy, attributes);
break :blk copy;
} else null;
return .{ .element = .{
.tag = tag,
.children = children_copy,
.attributes = attributes_copy,
.escaping = options.escaping,
.rendering = options.rendering,
.async = options.async,
.fallback = options.fallback,
} };
}
pub fn txt(self: *Context, text: []const u8) Component {
const escaped = self.escapeHtml(text);
return .{ .text = escaped };
}
pub fn expr(self: *Context, val: anytype) Component {
const T = @TypeOf(val);
if (T == Component) return val;
if (comptime isStatePointer(T)) return self.expr(val.get());
const Cmp = switch (@typeInfo(T)) {
.comptime_int, .comptime_float, .float => self.fmt("{d}", .{val}),
.int => if (T == u8 and std.ascii.isPrint(val))
self.fmt("{c}", .{val})
else
self.fmt("{d}", .{val}),
.bool => self.fmt("{s}", .{if (val) "true" else "false"}),
.null => self.ele(.fragment, .{}), // Render nothing for null
.optional => if (val) |inner| self.expr(inner) else self.ele(.fragment, .{}),
.@"enum", .enum_literal => self.txt(@tagName(val)),
.pointer => |ptr_info| switch (ptr_info.size) {
.one => switch (@typeInfo(ptr_info.child)) {
.array => {
// Coerce `*[N]T` to `[]const T`.
const Slice = []const std.meta.Elem(ptr_info.child);
return self.expr(@as(Slice, val));
},
else => {
return self.expr(val.*);
},
},
.many, .slice => {
if (ptr_info.size == .many and ptr_info.sentinel() == null)
@compileError("unable to stringify type '" ++ @typeName(T) ++ "' without sentinel");
const slice = if (ptr_info.size == .many) std.mem.span(val) else val;
if (ptr_info.child == u8) {
// This is a []const u8, or some similar Zig string.
if (std.unicode.utf8ValidateSlice(slice)) {
return txt(self, slice);
}
}
// Handle slices of Components
if (ptr_info.child == Component) {
return .{ .element = .{
.tag = .fragment,
.children = val,
} };
}
return self.txt(slice);
},
else => @compileError("Unable to render type '" ++ @typeName(T) ++ "', supported types are: int, float, bool, string, enum, optional"),
},
.@"struct" => |struct_info| {
var aw = std.io.Writer.Allocating.init(self.getAlloc());
defer aw.deinit();
// aw.writer.print("{s} ", .{@tagName(struct_info)}) catch @panic("OOM");
_ = struct_info;
std.zon.stringify.serializeMaxDepth(val, .{ .whitespace = true }, &aw.writer, 100) catch |err| {
return self.fmt("{s}", .{@errorName(err)});
};
return self.txt(aw.written());
},
.array => |arr_info| {
// Handle arrays of Components
if (arr_info.child == Component) {
return .{ .element = .{
.tag = .fragment,
.children = &val,
} };
}
@compileError("Unable to render array of type '" ++ @typeName(arr_info.child) ++ "', only Component arrays are supported");
},
else => @compileError("Unable to render type '" ++ @typeName(T) ++ "', supported types are: int, float, bool, string, enum, optional"),
};
return Cmp;
}
pub fn fmt(self: *Context, comptime format: []const u8, args: anytype) Component {
const allocator = self.getAlloc();
const text = std.fmt.allocPrint(allocator, format, args) catch @panic("OOM");
return .{ .text = text };
}
pub fn printf(self: *Context, comptime format: []const u8, args: anytype) []const u8 {
const allocator = self.getAlloc();
const text = std.fmt.allocPrint(allocator, format, args) catch @panic("OOM");
return text;
}
/// Create an attribute with type-aware value handling
/// Returns null for values that should omit the attribute (false booleans, null optionals)
pub fn attr(self: *Context, comptime name: []const u8, val: anytype) ?Element.Attribute {
const T = @TypeOf(val);
if (comptime isStatePointer(T)) {
return self.attr(name, val.get());
}
return switch (@typeInfo(T)) {
// Strings and function pointers
.pointer => |ptr_info| blk: {
if (ptr_info.size == .slice and ptr_info.child == u8) {
break :blk .{ .name = name, .value = val };
}
if (ptr_info.size == .one) {
if (@typeInfo(ptr_info.child) == .array) {
const Slice = []const std.meta.Elem(ptr_info.child);
return self.attr(name, @as(Slice, val));
}
// Function pointer - treat as event handler
if (@typeInfo(ptr_info.child) == .@"fn") {
const fn_params = @typeInfo(ptr_info.child).@"fn".params;
const takes_ptr = fn_params.len == 1 and
@typeInfo(fn_params[0].type.?) == .pointer;
const handler = if (takes_ptr)
zx.EventHandler.runtimePtr(val)
else
zx.EventHandler.runtime(val);
break :blk .{ .name = name, .handler = handler };
}
}
@compileError("Unsupported pointer type for attribute: " ++ @typeName(T));
},
// Integers - format to string
.int, .comptime_int => .{
.name = name,
.value = self.printf("{d}", .{val}),
},
// Floats - format with default precision
.float, .comptime_float => .{
.name = name,
.value = self.printf("{d}", .{val}),
},
// Booleans - presence-only attribute (true) or omit (false)
.bool => if (val) .{ .name = name, .value = null } else null,
// Optionals - recurse if non-null, omit if null
.optional => if (val) |inner| self.attr(name, inner) else null,
// Enums - convert tag name to string
.@"enum", .enum_literal => .{
.name = name,
.value = @tagName(val),
},
// Event handlers - store as function pointer
.@"fn" => .{
.name = name,
.handler = if (comptime std.mem.eql(u8, name, "action"))
zx.EventHandler.action(val)
else
zx.EventHandler.wrap(val),
},
// Pre-built event handlers
.@"struct" => if (T == zx.EventHandler) .{
.name = name,
.handler = val,
} else if (comptime @hasDecl(T, "format")) blk: {
const allocator = self.getAlloc();
const str = std.fmt.allocPrint(allocator, "{f}", .{val}) catch @panic("OOM");
break :blk .{
.name = name,
.value = str,
};
} else @compileError("Unsupported struct type for attribute: " ++ @typeName(T)),
.@"union" => if (comptime @hasDecl(T, "format")) blk: {
const allocator = self.getAlloc();
const str = std.fmt.allocPrint(allocator, "{f}", .{val}) catch @panic("OOM");
break :blk .{
.name = name,
.value = str,
};
} else @compileError("Unsupported union type for attribute: " ++ @typeName(T)),
else => @compileError("Unsupported type for attribute value: " ++ @typeName(T)),
};
}
pub fn attrf(self: *Context, comptime name: []const u8, comptime format: []const u8, args: anytype) ?Element.Attribute {
const allocator = self.getAlloc();
const text = std.fmt.allocPrint(allocator, format, args) catch @panic("OOM");
return self.attr(name, text);
}
pub fn attrv(self: *Context, val: anytype) []const u8 {
const attrkv = self.attr("f", val);
if (attrkv) |a| {
return a.value orelse "";
}
return "";
}
pub fn propf(self: *Context, comptime format: []const u8, args: anytype) []const u8 {
const allocator = self.getAlloc();
return std.fmt.allocPrint(allocator, format, args) catch @panic("OOM");
}
pub const propv = attrv;
/// Filter and collect non-null attributes into a slice
pub fn attrs(self: *Context, inputs: anytype) []const Element.Attribute {
const allocator = self.getAlloc();
const InputType = @TypeOf(inputs);
const input_info = @typeInfo(InputType);
// Handle tuple/struct (comptime known)
if (input_info == .@"struct" and input_info.@"struct".is_tuple) {
// Count non-null attributes at runtime
var count: usize = 0;
inline for (inputs) |input| {
if (@TypeOf(input) == ?Element.Attribute) {
if (input != null) count += 1;
} else {
count += 1;
}
}
if (count == 0) return &.{};
const result = allocator.alloc(Element.Attribute, count) catch @panic("OOM");
var idx: usize = 0;
inline for (inputs) |input| {
if (@TypeOf(input) == ?Element.Attribute) {
if (input) |a| {
result[idx] = a;
idx += 1;
}
} else {
result[idx] = input;
idx += 1;
}
}
return result;
}
@compileError("attrs() expects a tuple of attributes");
}
/// Spread a struct's fields as attributes
/// Takes a struct and returns a slice of attributes for each field
pub fn attrSpr(self: *Context, props: anytype) []const ?Element.Attribute {
const allocator = self.getAlloc();
const T = @TypeOf(props);
const type_info = @typeInfo(T);
if (type_info != .@"struct") {
@compileError("attrSpr() expects a struct, got " ++ @typeName(T));
}
const fields = type_info.@"struct".fields;
if (fields.len == 0) return &.{};
const result = allocator.alloc(?Element.Attribute, fields.len) catch @panic("OOM");
inline for (fields, 0..) |field, i| {
const val = @field(props, field.name);
result[i] = self.attr(field.name, val);
}
return result;
}
/// Merge two structs for component props spreading
/// Later fields override earlier ones
pub fn propsM(_: *Context, base: anytype, overrides: anytype) prp.MergedPropsType(@TypeOf(base), @TypeOf(overrides)) {
const BaseType = @TypeOf(base);
const OverrideType = @TypeOf(overrides);
const ResultType = prp.MergedPropsType(BaseType, OverrideType);
var result: ResultType = undefined;
// Copy all fields from base
const base_info = @typeInfo(BaseType);
if (base_info == .@"struct") {
inline for (base_info.@"struct".fields) |field| {
if (@hasField(ResultType, field.name)) {
@field(result, field.name) = @field(base, field.name);
}
}
}
// Apply overrides (these take precedence)
const override_info = @typeInfo(OverrideType);
if (override_info == .@"struct") {
inline for (override_info.@"struct".fields) |field| {
@field(result, field.name) = @field(overrides, field.name);
}
}
return result;
}
/// Merge multiple attribute sources (including spread results) into a single slice
/// Accepts a tuple where each element can be:
/// - ?Element.Attribute (single attribute from attr())
/// - []const ?Element.Attribute (slice from attrSpr())
/// Later attributes with the same name override earlier ones (like JSX)
pub fn attrsM(self: *Context, inputs: anytype) []const Element.Attribute {
const allocator = self.getAlloc();
const InputType = @TypeOf(inputs);
const input_info = @typeInfo(InputType);
if (input_info != .@"struct" or !input_info.@"struct".is_tuple) {
@compileError("attrsM() expects a tuple of attributes or attribute slices");
}
// First pass: collect all attributes in order
var count: usize = 0;
inline for (inputs) |input| {
const T = @TypeOf(input);
if (T == ?Element.Attribute) {
if (input != null) count += 1;
} else if (T == []const ?Element.Attribute) {
for (input) |maybe_attr| {
if (maybe_attr != null) count += 1;
}
} else {
@compileError("attrsM() element must be ?Element.Attribute or []const ?Element.Attribute, got " ++ @typeName(T));
}
}
if (count == 0) return &.{};
// Collect all attributes in order (later ones override earlier)
const temp = allocator.alloc(Element.Attribute, count) catch @panic("OOM");
var idx: usize = 0;
inline for (inputs) |input| {
const T = @TypeOf(input);
if (T == ?Element.Attribute) {
if (input) |a| {
temp[idx] = a;
idx += 1;
}
} else if (T == []const ?Element.Attribute) {
for (input) |maybe_attr| {
if (maybe_attr) |a| {
temp[idx] = a;
idx += 1;
}
}
}
}
// Deduplicate atrrs, keep last occurrence
var unique_count: usize = 0;
var i: usize = temp.len;
while (i > 0) {
i -= 1;
const current = temp[i];
var found_later = false;
for (temp[i + 1 ..]) |later| {
if (std.mem.eql(u8, current.name, later.name)) {
found_later = true;
break;
}
}
if (!found_later) {
unique_count += 1;
}
}
const result = allocator.alloc(Element.Attribute, unique_count) catch @panic("OOM");
var result_idx: usize = 0;
for (temp, 0..) |current_attr, j| {
var found_later = false;
for (temp[j + 1 ..]) |later| {
if (std.mem.eql(u8, current_attr.name, later.name)) {
found_later = true;
break;
}
}
if (!found_later) {
result[result_idx] = current_attr;
result_idx += 1;
}
}
allocator.free(temp);
return result;
}
pub fn cmp(self: *Context, comptime func: anytype, options: Options, props: anytype) Component {
const allocator = self.getAlloc();
const FuncInfo = @typeInfo(@TypeOf(func));
const param_count = FuncInfo.@"fn".params.len;
const FirstPropType = FuncInfo.@"fn".params[0].type.?;
const first_is_ctx_ptr = @typeInfo(FirstPropType) == .pointer and
@hasField(@typeInfo(FirstPropType).pointer.child, "allocator") and
@hasField(@typeInfo(FirstPropType).pointer.child, "children");
const name = options.name orelse "";
// Context-based component or function with props parameter
var comp_fn = if (first_is_ctx_ptr or param_count == 2) blk: {
const PropsType = if (first_is_ctx_ptr) @TypeOf(props) else FuncInfo.@"fn".params[1].type.?;
const coerced_props = prp.coerceProps(PropsType, props);
break :blk Component.ComponentFn.init(func, name, allocator, coerced_props);
} else blk: {
break :blk Component.ComponentFn.init(func, name, allocator, props);
};
// Apply builtin attributes from options
comp_fn.async_mode = options.async orelse .sync;
comp_fn.fallback = options.fallback;
comp_fn.caching = options.caching;
// If client option is set, return a client component (for @rendering={.client})
// Render the component on the server for SSR, then hydrate on client.
// On Browser (already on the client), skip the CSR wrapper - just render
// the component directly as a component_fn.
if (options.client != null and zx.platform.role == .client) {
return .{ .component_fn = comp_fn };
}
if (options.client) |client_opts| {
const name_copy = allocator.alloc(u8, client_opts.name.len) catch @panic("OOM");
@memcpy(name_copy, client_opts.name);
const id_copy = allocator.alloc(u8, client_opts.id.len) catch @panic("OOM");
@memcpy(id_copy, client_opts.id);
// Call the component function to get SSR content
const rendered = comp_fn.call() catch @panic("Component call failed");
const children_ptr = allocator.create(Component) catch @panic("OOM");
children_ptr.* = rendered;
// Get the full props type from the component function signature
// and coerce partial props to include defaults - this ensures all fields are serialized
const props_data = blk: {
if (first_is_ctx_ptr) {
const CtxType = @typeInfo(FirstPropType).pointer.child;
if (@hasField(CtxType, "props")) {
const FullPropsType = @FieldType(CtxType, "props");
if (@typeInfo(FullPropsType) == .@"struct") {
const full_props = prp.coerceProps(FullPropsType, props);
break :blk prp.propsSerializer(FullPropsType, allocator, full_props);
}
}
} else if (param_count == 2) {
const FullPropsType = FuncInfo.@"fn".params[1].type.?;
if (@typeInfo(FullPropsType) == .@"struct") {
const full_props = prp.coerceProps(FullPropsType, props);
break :blk prp.propsSerializer(FullPropsType, allocator, full_props);
}
}
// Fallback: serialize the props as-is
break :blk prp.propsSerializer(@TypeOf(props), allocator, props);
};
return .{
.component_csr = .{
.name = name_copy,
.id = id_copy,
.props_ptr = props_data.ptr,
.writeProps = props_data.writeFn,
.children = children_ptr,
},
};
}
return .{ .component_fn = comp_fn };
}
/// Allocates a Component and returns a pointer to it (used for @fallback)
pub fn ptr(self: *Context, component: Component) *const Component {
const allocator = self.getAlloc();
const allocated = allocator.create(Component) catch @panic("OOM");
allocated.* = component;
return allocated;
}
/// Creates a React client-side rendered component.
/// Uses JSON serialization for props to match React's expected format.
pub fn client(self: *Context, options: ClientComponentOptions, props: anytype) Component {
const allocator = self.getAlloc();
const Props = @TypeOf(props);
const name_copy = allocator.alloc(u8, options.name.len) catch @panic("OOM");
@memcpy(name_copy, options.name);
const id_copy = allocator.alloc(u8, options.id.len) catch @panic("OOM");
@memcpy(id_copy, options.id);
// Use JSON serializer for React components
const props_data = prp.propsSerializerJson(Props, allocator, props);
return .{
.component_csr = .{
.name = name_copy,
.id = id_copy,
.props_ptr = props_data.ptr,
.writeProps = props_data.writeFn,
.is_react = true,
},
};
}
};
fn isStatePointer(comptime PT: type) bool {
const type_info = @typeInfo(PT);
if (type_info != .pointer) return false;
if (type_info.pointer.size != .one) return false;
const CT = type_info.pointer.child;
if (@typeInfo(CT) != .@"struct") return false;
return @hasField(CT, "value") and
@hasField(CT, "component_id") and
@hasDecl(CT, "get") and
@hasDecl(CT, "set") and
@hasDecl(CT, "update");
}