feat: add @TypeOf type hints for compiled templates
- Add TypeHint node type in parser for //- @TypeOf(field): type syntax
- Support scalar types (f32, i32, bool, etc.) and array/struct types
- Use helpers.appendValue() for non-string typed fields
- Filter out loop variable references from Data struct fields
- Preserve @TypeOf comments during comment stripping
Example usage:
//- @TypeOf(subtotal): f32
span $#{subtotal}
//- @TypeOf(items): []{name: []const u8, price: f32}
each item in items
h3 #{item.name}
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
.{
|
.{
|
||||||
.name = .pugz,
|
.name = .pugz,
|
||||||
.version = "0.3.1",
|
.version = "0.3.2",
|
||||||
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
|
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
|
||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{},
|
.dependencies = .{},
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const CartItem = struct {
|
|||||||
|
|
||||||
const Cart = struct {
|
const Cart = struct {
|
||||||
items: []const CartItem,
|
items: []const CartItem,
|
||||||
subtotal: []const u8,
|
subtotal: f32,
|
||||||
shipping: []const u8,
|
shipping: []const u8,
|
||||||
discount: ?[]const u8 = null,
|
discount: ?[]const u8 = null,
|
||||||
discountCode: ?[]const u8 = null,
|
discountCode: ?[]const u8 = null,
|
||||||
@@ -189,7 +189,7 @@ const sample_cart_items = [_]CartItem{
|
|||||||
|
|
||||||
const sample_cart = Cart{
|
const sample_cart = Cart{
|
||||||
.items = &sample_cart_items,
|
.items = &sample_cart_items,
|
||||||
.subtotal = "209.98",
|
.subtotal = 209.98,
|
||||||
.shipping = "0",
|
.shipping = "0",
|
||||||
.tax = "18.90",
|
.tax = "18.90",
|
||||||
.total = "228.88",
|
.total = "228.88",
|
||||||
@@ -309,11 +309,17 @@ fn productDetail(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
|
|||||||
fn cart(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
fn cart(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
||||||
const html = if (USE_COMPILED_TEMPLATES) blk: {
|
const html = if (USE_COMPILED_TEMPLATES) blk: {
|
||||||
const templates = @import("templates");
|
const templates = @import("templates");
|
||||||
break :blk try templates.pages_cart.render(res.arena, .{
|
const Data = templates.pages_cart.Data;
|
||||||
.cartCount = "2",
|
break :blk try templates.pages_cart.render(res.arena, Data{
|
||||||
.subtotal = sample_cart.subtotal,
|
.cartCount = "3",
|
||||||
.tax = sample_cart.tax,
|
.cartItems = &.{
|
||||||
.total = sample_cart.total,
|
.{ .variant = "Black", .name = "Wireless Headphones", .price = 79.99, .quantity = 1, .total = 79.99 },
|
||||||
|
.{ .variant = "Silver", .name = "Laptop", .price = 500.00, .quantity = 1, .total = 500.00 },
|
||||||
|
.{ .variant = "RGB", .name = "Mechanical Keyboard", .price = 129.99, .quantity = 1, .total = 129.99 },
|
||||||
|
},
|
||||||
|
.subtotal = 709.98,
|
||||||
|
.tax = 63.90,
|
||||||
|
.total = 773.88,
|
||||||
});
|
});
|
||||||
} else app.view.render(res.arena, "pages/cart", .{
|
} else app.view.render(res.arena, "pages/cart", .{
|
||||||
.title = "Shopping Cart",
|
.title = "Shopping Cart",
|
||||||
|
|||||||
@@ -14,17 +14,18 @@ block content
|
|||||||
.cart-layout
|
.cart-layout
|
||||||
.cart-main
|
.cart-main
|
||||||
.cart-items
|
.cart-items
|
||||||
|
//- @TypeOf(cartItems): []{name: []const u8, variant: []const u8, price: f32, quantity: u16, total: f32}
|
||||||
each item in cartItems
|
each item in cartItems
|
||||||
.cart-item
|
.cart-item
|
||||||
.cart-item-info
|
.cart-item-info
|
||||||
h3 #{name}
|
h3 #{item.name}
|
||||||
p.text-muted #{variant}
|
p.text-muted #{item.variant}
|
||||||
span.cart-item-price $#{price}
|
span.cart-item-price $#{item.price}
|
||||||
.cart-item-qty
|
.cart-item-qty
|
||||||
button.qty-btn -
|
button.qty-btn -
|
||||||
input.qty-input(type="text" value=quantity)
|
input.qty-input(type="text" value=item.quantity)
|
||||||
button.qty-btn +
|
button.qty-btn +
|
||||||
.cart-item-total $#{total}
|
.cart-item-total $#{item.total}
|
||||||
button.cart-item-remove x
|
button.cart-item-remove x
|
||||||
|
|
||||||
.cart-actions
|
.cart-actions
|
||||||
@@ -34,14 +35,17 @@ block content
|
|||||||
h3 Order Summary
|
h3 Order Summary
|
||||||
.summary-row
|
.summary-row
|
||||||
span Subtotal
|
span Subtotal
|
||||||
|
//- @TypeOf(subtotal): f32
|
||||||
span $#{subtotal}
|
span $#{subtotal}
|
||||||
.summary-row
|
.summary-row
|
||||||
span Shipping
|
span Shipping
|
||||||
span.text-success Free
|
span.text-success Free
|
||||||
.summary-row
|
.summary-row
|
||||||
span Tax
|
span Tax
|
||||||
|
//- @TypeOf(tax): f32
|
||||||
span $#{tax}
|
span $#{tax}
|
||||||
.summary-row.summary-total
|
.summary-row.summary-total
|
||||||
span Total
|
span Total
|
||||||
|
//- @TypeOf(total): f32
|
||||||
span $#{total}
|
span $#{total}
|
||||||
a.btn.btn-primary.btn-block(href="/checkout") Proceed to Checkout
|
a.btn.btn-primary.btn-block(href="/checkout") Proceed to Checkout
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ pub const Compiler = struct {
|
|||||||
.While => try self.visitWhile(node),
|
.While => try self.visitWhile(node),
|
||||||
.Each => try self.visitEach(node),
|
.Each => try self.visitEach(node),
|
||||||
.EachOf => try self.visitEachOf(node),
|
.EachOf => try self.visitEachOf(node),
|
||||||
.YieldBlock => {}, // No-op
|
.YieldBlock, .TypeHint => {}, // No-op (TypeHint is only for compiled templates)
|
||||||
.Include, .Extends, .RawInclude, .Filter, .IncludeFilter, .FileReference, .AttributeBlock => {
|
.Include, .Extends, .RawInclude, .Filter, .IncludeFilter, .FileReference, .AttributeBlock => {
|
||||||
// These should be processed by linker/loader before codegen
|
// These should be processed by linker/loader before codegen
|
||||||
return error.UnsupportedNodeType;
|
return error.UnsupportedNodeType;
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ pub const NodeType = enum {
|
|||||||
FileReference,
|
FileReference,
|
||||||
YieldBlock,
|
YieldBlock,
|
||||||
AttributeBlock,
|
AttributeBlock,
|
||||||
|
TypeHint, // Type annotation for compiled templates: //- @TypeOf(field): type
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -157,6 +158,10 @@ pub const Node = struct {
|
|||||||
// When/Conditional debug field
|
// When/Conditional debug field
|
||||||
debug: bool = true,
|
debug: bool = true,
|
||||||
|
|
||||||
|
// TypeHint fields (for //- @TypeOf(field): type annotations)
|
||||||
|
type_hint_field: ?[]const u8 = null, // Field name (e.g., "cartItems")
|
||||||
|
type_hint_type: ?[]const u8 = null, // Type spec (e.g., "[]{name: []const u8}")
|
||||||
|
|
||||||
// Memory ownership flags
|
// Memory ownership flags
|
||||||
val_owned: bool = false, // true if val was allocated and needs to be freed
|
val_owned: bool = false, // true if val was allocated and needs to be freed
|
||||||
|
|
||||||
@@ -966,6 +971,16 @@ pub const Parser = struct {
|
|||||||
fn parseComment(self: *Parser) !*Node {
|
fn parseComment(self: *Parser) !*Node {
|
||||||
const tok = try self.expect(.comment);
|
const tok = try self.expect(.comment);
|
||||||
|
|
||||||
|
// Check for type hint in unbuffered comment: //- @TypeOf(field): type
|
||||||
|
if (!tok.isBuffered()) {
|
||||||
|
if (tok.val.getString()) |text| {
|
||||||
|
const trimmed = mem.trim(u8, text, " \t");
|
||||||
|
if (mem.startsWith(u8, trimmed, "@TypeOf(")) {
|
||||||
|
return self.parseTypeHint(tok, trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (self.parseTextBlock()) |block| {
|
if (self.parseTextBlock()) |block| {
|
||||||
const node = try self.allocator.create(Node);
|
const node = try self.allocator.create(Node);
|
||||||
node.* = .{
|
node.* = .{
|
||||||
@@ -997,6 +1012,66 @@ pub const Parser = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// TypeHint Parsing (for compiled templates)
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Parse a type hint annotation: @TypeOf(fieldName): typeSpec
|
||||||
|
fn parseTypeHint(self: *Parser, tok: Token, text: []const u8) !*Node {
|
||||||
|
// Find closing paren: @TypeOf(fieldName)
|
||||||
|
const paren_start = "@TypeOf(".len;
|
||||||
|
var paren_end: usize = paren_start;
|
||||||
|
while (paren_end < text.len and text[paren_end] != ')') : (paren_end += 1) {}
|
||||||
|
|
||||||
|
if (paren_end >= text.len) {
|
||||||
|
// Malformed type hint - treat as regular comment
|
||||||
|
const node = try self.allocator.create(Node);
|
||||||
|
node.* = .{
|
||||||
|
.type = .Comment,
|
||||||
|
.val = tok.val.getString(),
|
||||||
|
.buffer = false,
|
||||||
|
.line = tok.loc.start.line,
|
||||||
|
.column = tok.loc.start.column,
|
||||||
|
.filename = self.filename,
|
||||||
|
};
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field_name = text[paren_start..paren_end];
|
||||||
|
|
||||||
|
// Find colon separator
|
||||||
|
var colon_pos = paren_end + 1;
|
||||||
|
while (colon_pos < text.len and (text[colon_pos] == ' ' or text[colon_pos] == '\t')) : (colon_pos += 1) {}
|
||||||
|
|
||||||
|
if (colon_pos >= text.len or text[colon_pos] != ':') {
|
||||||
|
// No colon found - treat as regular comment
|
||||||
|
const node = try self.allocator.create(Node);
|
||||||
|
node.* = .{
|
||||||
|
.type = .Comment,
|
||||||
|
.val = tok.val.getString(),
|
||||||
|
.buffer = false,
|
||||||
|
.line = tok.loc.start.line,
|
||||||
|
.column = tok.loc.start.column,
|
||||||
|
.filename = self.filename,
|
||||||
|
};
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get type spec after colon
|
||||||
|
const type_spec = mem.trim(u8, text[colon_pos + 1 ..], " \t");
|
||||||
|
|
||||||
|
const node = try self.allocator.create(Node);
|
||||||
|
node.* = .{
|
||||||
|
.type = .TypeHint,
|
||||||
|
.type_hint_field = field_name,
|
||||||
|
.type_hint_type = type_spec,
|
||||||
|
.line = tok.loc.start.line,
|
||||||
|
.column = tok.loc.start.column,
|
||||||
|
.filename = self.filename,
|
||||||
|
};
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Doctype Parsing
|
// Doctype Parsing
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub const StripCommentsOptions = struct {
|
|||||||
strip_unbuffered: bool = true,
|
strip_unbuffered: bool = true,
|
||||||
/// Strip buffered comments (default: false)
|
/// Strip buffered comments (default: false)
|
||||||
strip_buffered: bool = false,
|
strip_buffered: bool = false,
|
||||||
|
/// Preserve unbuffered comments starting with @TypeOf (for compiled templates)
|
||||||
|
preserve_type_hints: bool = true,
|
||||||
/// Source filename for error messages
|
/// Source filename for error messages
|
||||||
filename: ?[]const u8 = null,
|
filename: ?[]const u8 = null,
|
||||||
};
|
};
|
||||||
@@ -91,8 +93,20 @@ pub fn stripComments(
|
|||||||
// Check if this is a buffered comment
|
// Check if this is a buffered comment
|
||||||
comment_is_buffered = tok.isBuffered();
|
comment_is_buffered = tok.isBuffered();
|
||||||
|
|
||||||
|
// Check if this is a TypeHint comment that should be preserved
|
||||||
|
const is_type_hint = if (options.preserve_type_hints and !comment_is_buffered) blk2: {
|
||||||
|
if (tok.val.getString()) |text| {
|
||||||
|
const trimmed = std.mem.trim(u8, text, " \t");
|
||||||
|
break :blk2 std.mem.startsWith(u8, trimmed, "@TypeOf(");
|
||||||
|
}
|
||||||
|
break :blk2 false;
|
||||||
|
} else false;
|
||||||
|
|
||||||
// Determine if we should strip this comment
|
// Determine if we should strip this comment
|
||||||
if (comment_is_buffered) {
|
if (is_type_hint) {
|
||||||
|
// Always preserve TypeHint comments
|
||||||
|
in_comment = false;
|
||||||
|
} else if (comment_is_buffered) {
|
||||||
in_comment = options.strip_buffered;
|
in_comment = options.strip_buffered;
|
||||||
} else {
|
} else {
|
||||||
in_comment = options.strip_unbuffered;
|
in_comment = options.strip_unbuffered;
|
||||||
|
|||||||
@@ -31,3 +31,42 @@ pub fn isTruthy(val: anytype) bool {
|
|||||||
else => true,
|
else => true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Append an integer value to buffer (formatted as decimal string)
|
||||||
|
pub fn appendInt(buf: *std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator, value: anytype) !void {
|
||||||
|
var tmp: [32]u8 = undefined;
|
||||||
|
const str = std.fmt.bufPrint(&tmp, "{d}", .{value}) catch return;
|
||||||
|
try buf.appendSlice(allocator, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append a float value to buffer (formatted with 2 decimal places)
|
||||||
|
pub fn appendFloat(buf: *std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator, value: anytype) !void {
|
||||||
|
var tmp: [64]u8 = undefined;
|
||||||
|
const str = std.fmt.bufPrint(&tmp, "{d:.2}", .{value}) catch return;
|
||||||
|
try buf.appendSlice(allocator, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append any value to buffer (auto-detects type)
|
||||||
|
pub fn appendValue(buf: *std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator, value: anytype) !void {
|
||||||
|
const T = @TypeOf(value);
|
||||||
|
switch (@typeInfo(T)) {
|
||||||
|
.int, .comptime_int => try appendInt(buf, allocator, value),
|
||||||
|
.float, .comptime_float => try appendFloat(buf, allocator, value),
|
||||||
|
.bool => try buf.appendSlice(allocator, if (value) "true" else "false"),
|
||||||
|
.pointer => |ptr| {
|
||||||
|
if (ptr.size == .slice and ptr.child == u8) {
|
||||||
|
// String slice
|
||||||
|
try appendEscaped(buf, allocator, value);
|
||||||
|
} else {
|
||||||
|
// Other slices - not directly supported
|
||||||
|
try buf.appendSlice(allocator, "[...]");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
// Fallback - try to format it
|
||||||
|
var tmp: [128]u8 = undefined;
|
||||||
|
const str = std.fmt.bufPrint(&tmp, "{any}", .{value}) catch "[?]";
|
||||||
|
try buf.appendSlice(allocator, str);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,20 @@ pub const ZigCodegenError = error{
|
|||||||
UnsupportedFeature,
|
UnsupportedFeature,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Type information parsed from @TypeOf annotations
|
||||||
|
pub const TypeInfo = struct {
|
||||||
|
is_array: bool = false,
|
||||||
|
struct_fields: ?std.StringHashMap([]const u8) = null,
|
||||||
|
primitive_type: ?[]const u8 = null,
|
||||||
|
|
||||||
|
pub fn deinit(self: *TypeInfo, allocator: Allocator) void {
|
||||||
|
if (self.struct_fields) |*fields| {
|
||||||
|
fields.deinit();
|
||||||
|
}
|
||||||
|
_ = allocator;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub const Codegen = struct {
|
pub const Codegen = struct {
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
output: std.ArrayListUnmanaged(u8),
|
output: std.ArrayListUnmanaged(u8),
|
||||||
@@ -36,6 +50,10 @@ pub const Codegen = struct {
|
|||||||
terse: bool, // HTML5 mode vs XHTML
|
terse: bool, // HTML5 mode vs XHTML
|
||||||
// Buffer for combining consecutive static strings
|
// Buffer for combining consecutive static strings
|
||||||
static_buffer: std.ArrayListUnmanaged(u8),
|
static_buffer: std.ArrayListUnmanaged(u8),
|
||||||
|
// Type hints from @TypeOf annotations
|
||||||
|
type_hints: std.StringHashMap(TypeInfo),
|
||||||
|
// Current loop variable for field resolution inside each blocks
|
||||||
|
current_loop_var: ?[]const u8 = null,
|
||||||
|
|
||||||
pub fn init(allocator: Allocator) Codegen {
|
pub fn init(allocator: Allocator) Codegen {
|
||||||
return .{
|
return .{
|
||||||
@@ -44,12 +62,22 @@ pub const Codegen = struct {
|
|||||||
.indent_level = 0,
|
.indent_level = 0,
|
||||||
.terse = true, // Default to HTML5
|
.terse = true, // Default to HTML5
|
||||||
.static_buffer = .{},
|
.static_buffer = .{},
|
||||||
|
.type_hints = std.StringHashMap(TypeInfo).init(allocator),
|
||||||
|
.current_loop_var = null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Codegen) void {
|
pub fn deinit(self: *Codegen) void {
|
||||||
self.output.deinit(self.allocator);
|
self.output.deinit(self.allocator);
|
||||||
self.static_buffer.deinit(self.allocator);
|
self.static_buffer.deinit(self.allocator);
|
||||||
|
// Clean up type hints
|
||||||
|
var iter = self.type_hints.valueIterator();
|
||||||
|
while (iter.next()) |info| {
|
||||||
|
if (info.struct_fields) |*fields| {
|
||||||
|
fields.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.type_hints.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate Zig code for a template
|
/// Generate Zig code for a template
|
||||||
@@ -59,6 +87,19 @@ pub const Codegen = struct {
|
|||||||
self.static_buffer.clearRetainingCapacity();
|
self.static_buffer.clearRetainingCapacity();
|
||||||
self.indent_level = 0;
|
self.indent_level = 0;
|
||||||
self.terse = true;
|
self.terse = true;
|
||||||
|
self.current_loop_var = null;
|
||||||
|
|
||||||
|
// Clean up any existing type hints
|
||||||
|
var hint_iter = self.type_hints.valueIterator();
|
||||||
|
while (hint_iter.next()) |info| {
|
||||||
|
if (info.struct_fields) |*sf| {
|
||||||
|
sf.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.type_hints.clearRetainingCapacity();
|
||||||
|
|
||||||
|
// Collect type hints from AST
|
||||||
|
try collectTypeHints(self.allocator, ast, &self.type_hints);
|
||||||
|
|
||||||
// Detect doctype to set terse mode
|
// Detect doctype to set terse mode
|
||||||
self.detectDoctype(ast);
|
self.detectDoctype(ast);
|
||||||
@@ -68,14 +109,23 @@ pub const Codegen = struct {
|
|||||||
try self.writeLine("const helpers = @import(\"helpers.zig\");");
|
try self.writeLine("const helpers = @import(\"helpers.zig\");");
|
||||||
try self.writeLine("");
|
try self.writeLine("");
|
||||||
|
|
||||||
// Generate Data struct
|
// Generate Data struct with typed fields
|
||||||
try self.writeIndent();
|
try self.writeIndent();
|
||||||
try self.writeLine("pub const Data = struct {");
|
try self.writeLine("pub const Data = struct {");
|
||||||
self.indent_level += 1;
|
self.indent_level += 1;
|
||||||
|
|
||||||
for (fields) |field| {
|
for (fields) |field| {
|
||||||
try self.writeIndent();
|
try self.writeIndent();
|
||||||
try self.write(field);
|
try self.write(field);
|
||||||
try self.writeLine(": []const u8 = \"\",");
|
|
||||||
|
// Check if we have a type hint for this field
|
||||||
|
if (self.type_hints.get(field)) |type_info| {
|
||||||
|
try self.write(": ");
|
||||||
|
try self.writeTypeInfo(type_info);
|
||||||
|
} else {
|
||||||
|
try self.write(": []const u8 = \"\"");
|
||||||
|
}
|
||||||
|
try self.writeLine(",");
|
||||||
}
|
}
|
||||||
self.indent_level -= 1;
|
self.indent_level -= 1;
|
||||||
try self.writeIndent();
|
try self.writeIndent();
|
||||||
@@ -135,6 +185,8 @@ pub const Codegen = struct {
|
|||||||
.BlockComment => try self.generateBlockComment(node),
|
.BlockComment => try self.generateBlockComment(node),
|
||||||
.Doctype => try self.generateDoctype(node),
|
.Doctype => try self.generateDoctype(node),
|
||||||
.Conditional => try self.generateConditional(node),
|
.Conditional => try self.generateConditional(node),
|
||||||
|
.Each, .EachOf => try self.generateEach(node),
|
||||||
|
.TypeHint => {}, // Skip - processed during field extraction
|
||||||
else => {
|
else => {
|
||||||
// Unsupported nodes: skip or process children
|
// Unsupported nodes: skip or process children
|
||||||
for (node.nodes.items) |child| {
|
for (node.nodes.items) |child| {
|
||||||
@@ -203,14 +255,27 @@ pub const Codegen = struct {
|
|||||||
try self.write(attr.name);
|
try self.write(attr.name);
|
||||||
try self.writeLine("=\\\"\");");
|
try self.writeLine("=\\\"\");");
|
||||||
|
|
||||||
|
// Check if this is a loop variable reference or typed field
|
||||||
|
const is_loop_var = self.isLoopVariableReference(val);
|
||||||
|
const needs_value_helper = is_loop_var or self.hasNonStringTypeHint(val);
|
||||||
|
|
||||||
try self.writeIndent();
|
try self.writeIndent();
|
||||||
if (attr.must_escape) {
|
if (needs_value_helper) {
|
||||||
|
// Use appendValue for typed fields (handles any type)
|
||||||
|
try self.write("try helpers.appendValue(&buf, allocator, ");
|
||||||
|
if (is_loop_var) {
|
||||||
|
try self.writeFieldReference(val);
|
||||||
|
} else {
|
||||||
|
try self.write("data.");
|
||||||
|
try self.writeSanitizedFieldName(val);
|
||||||
|
}
|
||||||
|
} else if (attr.must_escape) {
|
||||||
try self.write("try helpers.appendEscaped(&buf, allocator, data.");
|
try self.write("try helpers.appendEscaped(&buf, allocator, data.");
|
||||||
|
try self.writeSanitizedFieldName(val);
|
||||||
} else {
|
} else {
|
||||||
try self.write("try buf.appendSlice(allocator, data.");
|
try self.write("try buf.appendSlice(allocator, data.");
|
||||||
|
try self.writeSanitizedFieldName(val);
|
||||||
}
|
}
|
||||||
// Sanitize field name
|
|
||||||
try self.writeSanitizedFieldName(val);
|
|
||||||
try self.writeLine(");");
|
try self.writeLine(");");
|
||||||
|
|
||||||
try self.writeIndent();
|
try self.writeIndent();
|
||||||
@@ -285,15 +350,43 @@ pub const Codegen = struct {
|
|||||||
// Output interpolated field
|
// Output interpolated field
|
||||||
const field_name = val[start..end];
|
const field_name = val[start..end];
|
||||||
try self.writeIndent();
|
try self.writeIndent();
|
||||||
|
|
||||||
|
// Check if this is a loop variable reference (e.g., item.name)
|
||||||
|
const is_loop_var = self.isLoopVariableReference(field_name);
|
||||||
|
// Check if the field has a non-string type hint
|
||||||
|
const needs_value_helper = is_loop_var or self.hasNonStringTypeHint(field_name);
|
||||||
|
|
||||||
if (text_node.buffer) {
|
if (text_node.buffer) {
|
||||||
// Escaped (default)
|
// Escaped (default)
|
||||||
try self.write("try helpers.appendEscaped(&buf, allocator, data.");
|
if (needs_value_helper) {
|
||||||
|
// Use appendValue for typed fields (handles any type)
|
||||||
|
try self.write("try helpers.appendValue(&buf, allocator, ");
|
||||||
|
if (is_loop_var) {
|
||||||
|
try self.writeFieldReference(field_name);
|
||||||
|
} else {
|
||||||
|
try self.write("data.");
|
||||||
|
try self.writeSanitizedFieldName(field_name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try self.write("try helpers.appendEscaped(&buf, allocator, data.");
|
||||||
|
try self.writeSanitizedFieldName(field_name);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unescaped (unsafe)
|
// Unescaped (unsafe)
|
||||||
try self.write("try buf.appendSlice(allocator, data.");
|
if (needs_value_helper) {
|
||||||
|
// Use appendValue for typed fields (handles any type)
|
||||||
|
try self.write("try helpers.appendValue(&buf, allocator, ");
|
||||||
|
if (is_loop_var) {
|
||||||
|
try self.writeFieldReference(field_name);
|
||||||
|
} else {
|
||||||
|
try self.write("data.");
|
||||||
|
try self.writeSanitizedFieldName(field_name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try self.write("try buf.appendSlice(allocator, data.");
|
||||||
|
try self.writeSanitizedFieldName(field_name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Sanitize field name (replace dots with underscores)
|
|
||||||
try self.writeSanitizedFieldName(field_name);
|
|
||||||
try self.writeLine(");");
|
try self.writeLine(");");
|
||||||
|
|
||||||
i = end + 1;
|
i = end + 1;
|
||||||
@@ -318,14 +411,27 @@ pub const Codegen = struct {
|
|||||||
// Flush static buffer before dynamic content
|
// Flush static buffer before dynamic content
|
||||||
try self.flushStaticBuffer();
|
try self.flushStaticBuffer();
|
||||||
|
|
||||||
|
// Check if this is a loop variable reference
|
||||||
|
const is_loop_var = self.isLoopVariableReference(val);
|
||||||
|
const needs_value_helper = is_loop_var or self.hasNonStringTypeHint(val);
|
||||||
|
|
||||||
try self.writeIndent();
|
try self.writeIndent();
|
||||||
if (code_node.must_escape) {
|
if (needs_value_helper) {
|
||||||
|
// Use appendValue for typed fields (handles any type)
|
||||||
|
try self.write("try helpers.appendValue(&buf, allocator, ");
|
||||||
|
if (is_loop_var) {
|
||||||
|
try self.writeFieldReference(val);
|
||||||
|
} else {
|
||||||
|
try self.write("data.");
|
||||||
|
try self.writeSanitizedFieldName(val);
|
||||||
|
}
|
||||||
|
} else if (code_node.must_escape) {
|
||||||
try self.write("try helpers.appendEscaped(&buf, allocator, data.");
|
try self.write("try helpers.appendEscaped(&buf, allocator, data.");
|
||||||
|
try self.writeSanitizedFieldName(val);
|
||||||
} else {
|
} else {
|
||||||
try self.write("try buf.appendSlice(allocator, data.");
|
try self.write("try buf.appendSlice(allocator, data.");
|
||||||
|
try self.writeSanitizedFieldName(val);
|
||||||
}
|
}
|
||||||
// Sanitize field name
|
|
||||||
try self.writeSanitizedFieldName(val);
|
|
||||||
try self.writeLine(");");
|
try self.writeLine(");");
|
||||||
}
|
}
|
||||||
// Unbuffered code is not supported in static compilation
|
// Unbuffered code is not supported in static compilation
|
||||||
@@ -365,6 +471,40 @@ pub const Codegen = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generateEach(self: *Codegen, node: *Node) !void {
|
||||||
|
const collection = node.obj orelse return;
|
||||||
|
const item_var = node.val orelse "item";
|
||||||
|
|
||||||
|
// Flush static content before loop
|
||||||
|
try self.flushStaticBuffer();
|
||||||
|
|
||||||
|
// Generate: for (data.collection) |item| {
|
||||||
|
try self.writeIndent();
|
||||||
|
try self.write("for (data.");
|
||||||
|
try self.writeSanitizedFieldName(collection);
|
||||||
|
try self.write(") |");
|
||||||
|
try self.write(item_var);
|
||||||
|
try self.writeLine("| {");
|
||||||
|
self.indent_level += 1;
|
||||||
|
|
||||||
|
// Track loop variable for field resolution
|
||||||
|
const prev_loop_var = self.current_loop_var;
|
||||||
|
self.current_loop_var = item_var;
|
||||||
|
|
||||||
|
// Generate loop body
|
||||||
|
for (node.nodes.items) |child| {
|
||||||
|
try self.generateNode(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any remaining static content
|
||||||
|
try self.flushStaticBuffer();
|
||||||
|
|
||||||
|
self.current_loop_var = prev_loop_var;
|
||||||
|
self.indent_level -= 1;
|
||||||
|
try self.writeIndent();
|
||||||
|
try self.writeLine("}");
|
||||||
|
}
|
||||||
|
|
||||||
fn generateConditional(self: *Codegen, cond: *Node) !void {
|
fn generateConditional(self: *Codegen, cond: *Node) !void {
|
||||||
// For compiled templates, generate Zig if/else statements
|
// For compiled templates, generate Zig if/else statements
|
||||||
// Only support simple field references like "isLoggedIn" or "count > 0"
|
// Only support simple field references like "isLoggedIn" or "count > 0"
|
||||||
@@ -519,6 +659,113 @@ pub const Codegen = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if field_name is a loop variable reference (e.g., "item" or "item.name" when current_loop_var is "item")
|
||||||
|
fn isLoopVariableReference(self: *Codegen, field_name: []const u8) bool {
|
||||||
|
const loop_var = self.current_loop_var orelse return false;
|
||||||
|
// Exact match (e.g., "item" when loop var is "item")
|
||||||
|
if (std.mem.eql(u8, field_name, loop_var)) return true;
|
||||||
|
// Field access (e.g., "item.name" when loop var is "item")
|
||||||
|
if (field_name.len > loop_var.len and
|
||||||
|
std.mem.startsWith(u8, field_name, loop_var) and
|
||||||
|
field_name[loop_var.len] == '.')
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a field has a non-string type hint (requires helpers.appendValue)
|
||||||
|
fn hasNonStringTypeHint(self: *Codegen, field_name: []const u8) bool {
|
||||||
|
const type_info = self.type_hints.get(field_name) orelse return false;
|
||||||
|
|
||||||
|
// Arrays always need appendValue
|
||||||
|
if (type_info.is_array) return true;
|
||||||
|
|
||||||
|
// Structs need appendValue
|
||||||
|
if (type_info.struct_fields != null) return true;
|
||||||
|
|
||||||
|
// Check primitive type
|
||||||
|
if (type_info.primitive_type) |prim| {
|
||||||
|
// String types don't need appendValue
|
||||||
|
if (std.mem.eql(u8, prim, "[]const u8")) return false;
|
||||||
|
// All other primitives (f32, i32, bool, etc.) need appendValue
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a field reference, handling loop variables (item.name -> item.name) vs data fields (field -> data.field)
|
||||||
|
fn writeFieldReference(self: *Codegen, field_name: []const u8) !void {
|
||||||
|
// For loop variable references, write directly (e.g., "item.name")
|
||||||
|
try self.write(field_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write type information to output (generates Zig type syntax with default value)
|
||||||
|
fn writeTypeInfo(self: *Codegen, type_info: TypeInfo) !void {
|
||||||
|
if (type_info.is_array) {
|
||||||
|
// Array type: []const struct { ... } = &.{}
|
||||||
|
if (type_info.struct_fields) |struct_fields| {
|
||||||
|
try self.write("[]const struct { ");
|
||||||
|
var iter = struct_fields.iterator();
|
||||||
|
var first = true;
|
||||||
|
while (iter.next()) |entry| {
|
||||||
|
if (!first) {
|
||||||
|
try self.write(", ");
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
try self.write(entry.key_ptr.*);
|
||||||
|
try self.write(": ");
|
||||||
|
try self.write(entry.value_ptr.*);
|
||||||
|
}
|
||||||
|
try self.write(" } = &.{}");
|
||||||
|
} else if (type_info.primitive_type) |prim| {
|
||||||
|
// Array of primitives: []const u8, []i32, etc.
|
||||||
|
try self.write("[]const ");
|
||||||
|
try self.write(prim);
|
||||||
|
try self.write(" = &.{}");
|
||||||
|
} else {
|
||||||
|
// Fallback: array of strings
|
||||||
|
try self.write("[]const []const u8 = &.{}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-array type
|
||||||
|
if (type_info.struct_fields) |struct_fields| {
|
||||||
|
// Anonymous struct
|
||||||
|
try self.write("struct { ");
|
||||||
|
var iter = struct_fields.iterator();
|
||||||
|
var first = true;
|
||||||
|
while (iter.next()) |entry| {
|
||||||
|
if (!first) {
|
||||||
|
try self.write(", ");
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
try self.write(entry.key_ptr.*);
|
||||||
|
try self.write(": ");
|
||||||
|
try self.write(entry.value_ptr.*);
|
||||||
|
}
|
||||||
|
try self.write(" } = .{}");
|
||||||
|
} else if (type_info.primitive_type) |prim| {
|
||||||
|
// Primitive type with appropriate default
|
||||||
|
try self.write(prim);
|
||||||
|
if (std.mem.eql(u8, prim, "[]const u8")) {
|
||||||
|
try self.write(" = \"\"");
|
||||||
|
} else if (std.mem.eql(u8, prim, "bool")) {
|
||||||
|
try self.write(" = false");
|
||||||
|
} else if (std.mem.startsWith(u8, prim, "i") or std.mem.startsWith(u8, prim, "u")) {
|
||||||
|
try self.write(" = 0");
|
||||||
|
} else if (std.mem.startsWith(u8, prim, "f")) {
|
||||||
|
try self.write(" = 0.0");
|
||||||
|
} else {
|
||||||
|
// Unknown type, no default
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: string
|
||||||
|
try self.write("[]const u8 = \"\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if value is a data field reference (simple identifier, may contain dots)
|
/// Check if value is a data field reference (simple identifier, may contain dots)
|
||||||
fn isDataFieldReference(self: *Codegen, val: []const u8) bool {
|
fn isDataFieldReference(self: *Codegen, val: []const u8) bool {
|
||||||
_ = self;
|
_ = self;
|
||||||
@@ -550,7 +797,10 @@ pub fn extractFieldNames(allocator: Allocator, ast: *Node) ![][]const u8 {
|
|||||||
var fields = std.StringHashMap(void).init(allocator);
|
var fields = std.StringHashMap(void).init(allocator);
|
||||||
defer fields.deinit();
|
defer fields.deinit();
|
||||||
|
|
||||||
try extractFieldNamesRecursive(ast, &fields);
|
var loop_vars = std.StringHashMap(void).init(allocator);
|
||||||
|
defer loop_vars.deinit();
|
||||||
|
|
||||||
|
try extractFieldNamesRecursive(ast, &fields, &loop_vars);
|
||||||
|
|
||||||
// Convert to sorted slice and sanitize field names
|
// Convert to sorted slice and sanitize field names
|
||||||
var result: std.ArrayListUnmanaged([]const u8) = .{};
|
var result: std.ArrayListUnmanaged([]const u8) = .{};
|
||||||
@@ -583,7 +833,38 @@ pub fn extractFieldNames(allocator: Allocator, ast: *Node) ![][]const u8 {
|
|||||||
return slice;
|
return slice;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void)) !void {
|
fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void), loop_vars: *std.StringHashMap(void)) !void {
|
||||||
|
// Handle TypeHint nodes - just add the field name, type info is handled separately
|
||||||
|
if (node.type == .TypeHint) {
|
||||||
|
if (node.type_hint_field) |field| {
|
||||||
|
try fields.put(field, {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Each/EachOf nodes - extract collection field and track loop variable
|
||||||
|
if (node.type == .Each or node.type == .EachOf) {
|
||||||
|
if (node.obj) |collection| {
|
||||||
|
const trimmed = std.mem.trim(u8, collection, " \t");
|
||||||
|
if (trimmed.len > 0) {
|
||||||
|
try fields.put(trimmed, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the loop variable (e.g., "item" in "each item in items")
|
||||||
|
const loop_var = node.val orelse "item";
|
||||||
|
try loop_vars.put(loop_var, {});
|
||||||
|
|
||||||
|
// Recurse into loop body
|
||||||
|
for (node.nodes.items) |child| {
|
||||||
|
try extractFieldNamesRecursive(child, fields, loop_vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove loop variable after processing (for nested loops with same var name)
|
||||||
|
_ = loop_vars.remove(loop_var);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract from text interpolations: #{field}
|
// Extract from text interpolations: #{field}
|
||||||
if (node.type == .Text or node.type == .Code) {
|
if (node.type == .Text or node.type == .Code) {
|
||||||
if (node.val) |val| {
|
if (node.val) |val| {
|
||||||
@@ -597,7 +878,10 @@ fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void)) !vo
|
|||||||
|
|
||||||
if (end < val.len) {
|
if (end < val.len) {
|
||||||
const field_name = val[start..end];
|
const field_name = val[start..end];
|
||||||
try fields.put(field_name, {});
|
// Skip if it's a loop variable reference
|
||||||
|
if (!isLoopVarReference(field_name, loop_vars)) {
|
||||||
|
try fields.put(field_name, {});
|
||||||
|
}
|
||||||
i = end + 1;
|
i = end + 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -607,7 +891,10 @@ fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void)) !vo
|
|||||||
|
|
||||||
// For Code nodes with buffer=true, the val itself is a field reference
|
// For Code nodes with buffer=true, the val itself is a field reference
|
||||||
if (node.type == .Code and node.buffer) {
|
if (node.type == .Code and node.buffer) {
|
||||||
try fields.put(val, {});
|
// Skip if it's a loop variable reference
|
||||||
|
if (!isLoopVarReference(val, loop_vars)) {
|
||||||
|
try fields.put(val, {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,7 +919,8 @@ fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void)) !vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (is_identifier) {
|
// Skip if it's a loop variable reference
|
||||||
|
if (is_identifier and !isLoopVarReference(val, loop_vars)) {
|
||||||
try fields.put(val, {});
|
try fields.put(val, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -660,7 +948,8 @@ fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void)) !vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (is_identifier) {
|
// Skip if it's a loop variable reference
|
||||||
|
if (is_identifier and !isLoopVarReference(field_name, loop_vars)) {
|
||||||
try fields.put(field_name, {});
|
try fields.put(field_name, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -668,16 +957,114 @@ fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void)) !vo
|
|||||||
|
|
||||||
// Recurse into consequent and alternate
|
// Recurse into consequent and alternate
|
||||||
if (node.consequent) |cons| {
|
if (node.consequent) |cons| {
|
||||||
try extractFieldNamesRecursive(cons, fields);
|
try extractFieldNamesRecursive(cons, fields, loop_vars);
|
||||||
}
|
}
|
||||||
if (node.alternate) |alt| {
|
if (node.alternate) |alt| {
|
||||||
try extractFieldNamesRecursive(alt, fields);
|
try extractFieldNamesRecursive(alt, fields, loop_vars);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recurse into children
|
// Recurse into children
|
||||||
for (node.nodes.items) |child| {
|
for (node.nodes.items) |child| {
|
||||||
try extractFieldNamesRecursive(child, fields);
|
try extractFieldNamesRecursive(child, fields, loop_vars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a field name is a loop variable reference (e.g., "item" or "item.name")
|
||||||
|
fn isLoopVarReference(field_name: []const u8, loop_vars: *std.StringHashMap(void)) bool {
|
||||||
|
// Check exact match (e.g., "item")
|
||||||
|
if (loop_vars.contains(field_name)) return true;
|
||||||
|
|
||||||
|
// Check field access (e.g., "item.name" -> check "item")
|
||||||
|
if (std.mem.indexOf(u8, field_name, ".")) |dot_idx| {
|
||||||
|
const base = field_name[0..dot_idx];
|
||||||
|
if (loop_vars.contains(base)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Type Hint Parsing
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Parse a type spec string (e.g., "[]{name: []const u8, price: f32}")
|
||||||
|
pub fn parseTypeHintSpec(allocator: Allocator, spec: []const u8) !TypeInfo {
|
||||||
|
var info = TypeInfo{};
|
||||||
|
var remaining = std.mem.trim(u8, spec, " \t");
|
||||||
|
|
||||||
|
// Check for array prefix []
|
||||||
|
if (std.mem.startsWith(u8, remaining, "[]")) {
|
||||||
|
info.is_array = true;
|
||||||
|
remaining = remaining[2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for struct definition {...}
|
||||||
|
if (remaining.len > 0 and remaining[0] == '{') {
|
||||||
|
info.struct_fields = try parseStructFields(allocator, remaining);
|
||||||
|
} else {
|
||||||
|
info.primitive_type = remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse struct fields from "{field1: type1, field2: type2}"
|
||||||
|
fn parseStructFields(allocator: Allocator, spec: []const u8) !std.StringHashMap([]const u8) {
|
||||||
|
var fields = std.StringHashMap([]const u8).init(allocator);
|
||||||
|
errdefer fields.deinit();
|
||||||
|
|
||||||
|
// Remove braces
|
||||||
|
if (spec.len < 2) return fields;
|
||||||
|
const inner = spec[1 .. spec.len - 1];
|
||||||
|
|
||||||
|
// Split by comma and parse each field
|
||||||
|
var iter = std.mem.splitSequence(u8, inner, ",");
|
||||||
|
while (iter.next()) |field_spec| {
|
||||||
|
const trimmed = std.mem.trim(u8, field_spec, " \t");
|
||||||
|
if (trimmed.len == 0) continue;
|
||||||
|
|
||||||
|
// Find colon separator
|
||||||
|
if (std.mem.indexOf(u8, trimmed, ":")) |colon_idx| {
|
||||||
|
const field_name = std.mem.trim(u8, trimmed[0..colon_idx], " \t");
|
||||||
|
const field_type = std.mem.trim(u8, trimmed[colon_idx + 1 ..], " \t");
|
||||||
|
try fields.put(field_name, field_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect type hints from an AST into a hash map
|
||||||
|
pub fn collectTypeHints(allocator: Allocator, ast: *Node, type_hints: *std.StringHashMap(TypeInfo)) !void {
|
||||||
|
try collectTypeHintsRecursive(allocator, ast, type_hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectTypeHintsRecursive(allocator: Allocator, node: *Node, type_hints: *std.StringHashMap(TypeInfo)) !void {
|
||||||
|
// Handle TypeHint nodes
|
||||||
|
if (node.type == .TypeHint) {
|
||||||
|
if (node.type_hint_field) |field| {
|
||||||
|
if (node.type_hint_type) |type_spec| {
|
||||||
|
const info = try parseTypeHintSpec(allocator, type_spec);
|
||||||
|
try type_hints.put(field, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into conditional branches
|
||||||
|
if (node.type == .Conditional) {
|
||||||
|
if (node.consequent) |cons| {
|
||||||
|
try collectTypeHintsRecursive(allocator, cons, type_hints);
|
||||||
|
}
|
||||||
|
if (node.alternate) |alt| {
|
||||||
|
try collectTypeHintsRecursive(allocator, alt, type_hints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into children
|
||||||
|
for (node.nodes.items) |child| {
|
||||||
|
try collectTypeHintsRecursive(allocator, child, type_hints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ fn visitChildren(
|
|||||||
.MixinBlock,
|
.MixinBlock,
|
||||||
.YieldBlock,
|
.YieldBlock,
|
||||||
.Text,
|
.Text,
|
||||||
|
.TypeHint,
|
||||||
=> {},
|
=> {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user