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,
|
||||
.version = "0.3.1",
|
||||
.version = "0.3.2",
|
||||
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{},
|
||||
|
||||
@@ -55,7 +55,7 @@ const CartItem = struct {
|
||||
|
||||
const Cart = struct {
|
||||
items: []const CartItem,
|
||||
subtotal: []const u8,
|
||||
subtotal: f32,
|
||||
shipping: []const u8,
|
||||
discount: ?[]const u8 = null,
|
||||
discountCode: ?[]const u8 = null,
|
||||
@@ -189,7 +189,7 @@ const sample_cart_items = [_]CartItem{
|
||||
|
||||
const sample_cart = Cart{
|
||||
.items = &sample_cart_items,
|
||||
.subtotal = "209.98",
|
||||
.subtotal = 209.98,
|
||||
.shipping = "0",
|
||||
.tax = "18.90",
|
||||
.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 {
|
||||
const html = if (USE_COMPILED_TEMPLATES) blk: {
|
||||
const templates = @import("templates");
|
||||
break :blk try templates.pages_cart.render(res.arena, .{
|
||||
.cartCount = "2",
|
||||
.subtotal = sample_cart.subtotal,
|
||||
.tax = sample_cart.tax,
|
||||
.total = sample_cart.total,
|
||||
const Data = templates.pages_cart.Data;
|
||||
break :blk try templates.pages_cart.render(res.arena, Data{
|
||||
.cartCount = "3",
|
||||
.cartItems = &.{
|
||||
.{ .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", .{
|
||||
.title = "Shopping Cart",
|
||||
|
||||
@@ -14,17 +14,18 @@ block content
|
||||
.cart-layout
|
||||
.cart-main
|
||||
.cart-items
|
||||
//- @TypeOf(cartItems): []{name: []const u8, variant: []const u8, price: f32, quantity: u16, total: f32}
|
||||
each item in cartItems
|
||||
.cart-item
|
||||
.cart-item-info
|
||||
h3 #{name}
|
||||
p.text-muted #{variant}
|
||||
span.cart-item-price $#{price}
|
||||
h3 #{item.name}
|
||||
p.text-muted #{item.variant}
|
||||
span.cart-item-price $#{item.price}
|
||||
.cart-item-qty
|
||||
button.qty-btn -
|
||||
input.qty-input(type="text" value=quantity)
|
||||
input.qty-input(type="text" value=item.quantity)
|
||||
button.qty-btn +
|
||||
.cart-item-total $#{total}
|
||||
.cart-item-total $#{item.total}
|
||||
button.cart-item-remove x
|
||||
|
||||
.cart-actions
|
||||
@@ -34,14 +35,17 @@ block content
|
||||
h3 Order Summary
|
||||
.summary-row
|
||||
span Subtotal
|
||||
//- @TypeOf(subtotal): f32
|
||||
span $#{subtotal}
|
||||
.summary-row
|
||||
span Shipping
|
||||
span.text-success Free
|
||||
.summary-row
|
||||
span Tax
|
||||
//- @TypeOf(tax): f32
|
||||
span $#{tax}
|
||||
.summary-row.summary-total
|
||||
span Total
|
||||
//- @TypeOf(total): f32
|
||||
span $#{total}
|
||||
a.btn.btn-primary.btn-block(href="/checkout") Proceed to Checkout
|
||||
|
||||
@@ -269,7 +269,7 @@ pub const Compiler = struct {
|
||||
.While => try self.visitWhile(node),
|
||||
.Each => try self.visitEach(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 => {
|
||||
// These should be processed by linker/loader before codegen
|
||||
return error.UnsupportedNodeType;
|
||||
|
||||
@@ -71,6 +71,7 @@ pub const NodeType = enum {
|
||||
FileReference,
|
||||
YieldBlock,
|
||||
AttributeBlock,
|
||||
TypeHint, // Type annotation for compiled templates: //- @TypeOf(field): type
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -157,6 +158,10 @@ pub const Node = struct {
|
||||
// When/Conditional debug field
|
||||
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
|
||||
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 {
|
||||
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| {
|
||||
const node = try self.allocator.create(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
|
||||
// ========================================================================
|
||||
|
||||
@@ -24,6 +24,8 @@ pub const StripCommentsOptions = struct {
|
||||
strip_unbuffered: bool = true,
|
||||
/// Strip buffered comments (default: false)
|
||||
strip_buffered: bool = false,
|
||||
/// Preserve unbuffered comments starting with @TypeOf (for compiled templates)
|
||||
preserve_type_hints: bool = true,
|
||||
/// Source filename for error messages
|
||||
filename: ?[]const u8 = null,
|
||||
};
|
||||
@@ -91,8 +93,20 @@ pub fn stripComments(
|
||||
// Check if this is a buffered comment
|
||||
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
|
||||
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;
|
||||
} else {
|
||||
in_comment = options.strip_unbuffered;
|
||||
|
||||
@@ -31,3 +31,42 @@ pub fn isTruthy(val: anytype) bool {
|
||||
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,
|
||||
};
|
||||
|
||||
/// 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 {
|
||||
allocator: Allocator,
|
||||
output: std.ArrayListUnmanaged(u8),
|
||||
@@ -36,6 +50,10 @@ pub const Codegen = struct {
|
||||
terse: bool, // HTML5 mode vs XHTML
|
||||
// Buffer for combining consecutive static strings
|
||||
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 {
|
||||
return .{
|
||||
@@ -44,12 +62,22 @@ pub const Codegen = struct {
|
||||
.indent_level = 0,
|
||||
.terse = true, // Default to HTML5
|
||||
.static_buffer = .{},
|
||||
.type_hints = std.StringHashMap(TypeInfo).init(allocator),
|
||||
.current_loop_var = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Codegen) void {
|
||||
self.output.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
|
||||
@@ -59,6 +87,19 @@ pub const Codegen = struct {
|
||||
self.static_buffer.clearRetainingCapacity();
|
||||
self.indent_level = 0;
|
||||
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
|
||||
self.detectDoctype(ast);
|
||||
@@ -68,14 +109,23 @@ pub const Codegen = struct {
|
||||
try self.writeLine("const helpers = @import(\"helpers.zig\");");
|
||||
try self.writeLine("");
|
||||
|
||||
// Generate Data struct
|
||||
// Generate Data struct with typed fields
|
||||
try self.writeIndent();
|
||||
try self.writeLine("pub const Data = struct {");
|
||||
self.indent_level += 1;
|
||||
|
||||
for (fields) |field| {
|
||||
try self.writeIndent();
|
||||
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;
|
||||
try self.writeIndent();
|
||||
@@ -135,6 +185,8 @@ pub const Codegen = struct {
|
||||
.BlockComment => try self.generateBlockComment(node),
|
||||
.Doctype => try self.generateDoctype(node),
|
||||
.Conditional => try self.generateConditional(node),
|
||||
.Each, .EachOf => try self.generateEach(node),
|
||||
.TypeHint => {}, // Skip - processed during field extraction
|
||||
else => {
|
||||
// Unsupported nodes: skip or process children
|
||||
for (node.nodes.items) |child| {
|
||||
@@ -203,14 +255,27 @@ pub const Codegen = struct {
|
||||
try self.write(attr.name);
|
||||
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();
|
||||
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.writeSanitizedFieldName(val);
|
||||
} else {
|
||||
try self.write("try buf.appendSlice(allocator, data.");
|
||||
}
|
||||
// Sanitize field name
|
||||
try self.writeSanitizedFieldName(val);
|
||||
}
|
||||
try self.writeLine(");");
|
||||
|
||||
try self.writeIndent();
|
||||
@@ -285,15 +350,43 @@ pub const Codegen = struct {
|
||||
// Output interpolated field
|
||||
const field_name = val[start..end];
|
||||
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) {
|
||||
// Escaped (default)
|
||||
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 {
|
||||
// Unescaped (unsafe)
|
||||
try self.write("try buf.appendSlice(allocator, data.");
|
||||
}
|
||||
// Sanitize field name (replace dots with underscores)
|
||||
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);
|
||||
}
|
||||
}
|
||||
try self.writeLine(");");
|
||||
|
||||
i = end + 1;
|
||||
@@ -318,14 +411,27 @@ pub const Codegen = struct {
|
||||
// Flush static buffer before dynamic content
|
||||
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();
|
||||
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.writeSanitizedFieldName(val);
|
||||
} else {
|
||||
try self.write("try buf.appendSlice(allocator, data.");
|
||||
}
|
||||
// Sanitize field name
|
||||
try self.writeSanitizedFieldName(val);
|
||||
}
|
||||
try self.writeLine(");");
|
||||
}
|
||||
// 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 {
|
||||
// For compiled templates, generate Zig if/else statements
|
||||
// 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)
|
||||
fn isDataFieldReference(self: *Codegen, val: []const u8) bool {
|
||||
_ = self;
|
||||
@@ -550,7 +797,10 @@ pub fn extractFieldNames(allocator: Allocator, ast: *Node) ![][]const u8 {
|
||||
var fields = std.StringHashMap(void).init(allocator);
|
||||
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
|
||||
var result: std.ArrayListUnmanaged([]const u8) = .{};
|
||||
@@ -583,7 +833,38 @@ pub fn extractFieldNames(allocator: Allocator, ast: *Node) ![][]const u8 {
|
||||
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}
|
||||
if (node.type == .Text or node.type == .Code) {
|
||||
if (node.val) |val| {
|
||||
@@ -597,7 +878,10 @@ fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void)) !vo
|
||||
|
||||
if (end < val.len) {
|
||||
const field_name = val[start..end];
|
||||
// Skip if it's a loop variable reference
|
||||
if (!isLoopVarReference(field_name, loop_vars)) {
|
||||
try fields.put(field_name, {});
|
||||
}
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
@@ -607,10 +891,13 @@ fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void)) !vo
|
||||
|
||||
// For Code nodes with buffer=true, the val itself is a field reference
|
||||
if (node.type == .Code and node.buffer) {
|
||||
// Skip if it's a loop variable reference
|
||||
if (!isLoopVarReference(val, loop_vars)) {
|
||||
try fields.put(val, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract from attribute bindings
|
||||
if (node.type == .Tag or node.type == .InterpolatedTag) {
|
||||
@@ -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, {});
|
||||
}
|
||||
}
|
||||
@@ -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, {});
|
||||
}
|
||||
}
|
||||
@@ -668,16 +957,114 @@ fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void)) !vo
|
||||
|
||||
// Recurse into consequent and alternate
|
||||
if (node.consequent) |cons| {
|
||||
try extractFieldNamesRecursive(cons, fields);
|
||||
try extractFieldNamesRecursive(cons, fields, loop_vars);
|
||||
}
|
||||
if (node.alternate) |alt| {
|
||||
try extractFieldNamesRecursive(alt, fields);
|
||||
try extractFieldNamesRecursive(alt, fields, loop_vars);
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into children
|
||||
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,
|
||||
.YieldBlock,
|
||||
.Text,
|
||||
.TypeHint,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user