feat: support @import type expressions in @TypeOf annotations
- Parser accepts '=' separator in addition to ':' for @TypeOf syntax - New import_type and is_optional fields in TypeInfo - Dotted field access (user.name) preserved as real Zig dot-access for import types - Optional import types use .? unwrap (data.user.?.name) - Field extraction collapses user.name to user when user has @import type - Added zig_codegen tests to integration test suite - Fixed stale test signatures in zig_codegen.zig
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
.{
|
.{
|
||||||
.name = .pugz,
|
.name = .pugz,
|
||||||
.version = "0.3.13",
|
.version = "0.3.14",
|
||||||
.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 = .{},
|
||||||
|
|||||||
@@ -1039,12 +1039,12 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
const field_name = text[paren_start..paren_end];
|
const field_name = text[paren_start..paren_end];
|
||||||
|
|
||||||
// Find colon separator
|
// Find separator (: or =)
|
||||||
var colon_pos = paren_end + 1;
|
var sep_pos = paren_end + 1;
|
||||||
while (colon_pos < text.len and (text[colon_pos] == ' ' or text[colon_pos] == '\t')) : (colon_pos += 1) {}
|
while (sep_pos < text.len and (text[sep_pos] == ' ' or text[sep_pos] == '\t')) : (sep_pos += 1) {}
|
||||||
|
|
||||||
if (colon_pos >= text.len or text[colon_pos] != ':') {
|
if (sep_pos >= text.len or (text[sep_pos] != ':' and text[sep_pos] != '=')) {
|
||||||
// No colon found - treat as regular comment
|
// No separator found - treat as regular comment
|
||||||
const node = try self.allocator.create(Node);
|
const node = try self.allocator.create(Node);
|
||||||
node.* = .{
|
node.* = .{
|
||||||
.type = .Comment,
|
.type = .Comment,
|
||||||
@@ -1057,8 +1057,8 @@ pub const Parser = struct {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get type spec after colon
|
// Get type spec after separator
|
||||||
const type_spec = mem.trim(u8, text[colon_pos + 1 ..], " \t");
|
const type_spec = mem.trim(u8, text[sep_pos + 1 ..], " \t");
|
||||||
|
|
||||||
const node = try self.allocator.create(Node);
|
const node = try self.allocator.create(Node);
|
||||||
node.* = .{
|
node.* = .{
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const pugz = @import("pugz");
|
||||||
|
|
||||||
comptime {
|
comptime {
|
||||||
_ = @import("check_list_test.zig");
|
_ = @import("check_list_test.zig");
|
||||||
_ = @import("doctype_test.zig");
|
_ = @import("doctype_test.zig");
|
||||||
_ = @import("general_test.zig");
|
_ = @import("general_test.zig");
|
||||||
_ = @import("tag_interp_test.zig");
|
_ = @import("tag_interp_test.zig");
|
||||||
|
_ = pugz.zig_codegen;
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ pub const ZigCodegenError = error{
|
|||||||
/// Type information parsed from @TypeOf annotations
|
/// Type information parsed from @TypeOf annotations
|
||||||
pub const TypeInfo = struct {
|
pub const TypeInfo = struct {
|
||||||
is_array: bool = false,
|
is_array: bool = false,
|
||||||
|
is_optional: bool = false,
|
||||||
struct_fields: ?std.StringHashMap([]const u8) = null,
|
struct_fields: ?std.StringHashMap([]const u8) = null,
|
||||||
primitive_type: ?[]const u8 = null,
|
primitive_type: ?[]const u8 = null,
|
||||||
|
import_type: ?[]const u8 = null,
|
||||||
|
|
||||||
pub fn deinit(self: *TypeInfo, allocator: Allocator) void {
|
pub fn deinit(self: *TypeInfo, allocator: Allocator) void {
|
||||||
if (self.struct_fields) |*fields| {
|
if (self.struct_fields) |*fields| {
|
||||||
@@ -674,8 +676,25 @@ pub const Codegen = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a field name with sanitization (replace dots with underscores)
|
/// Write a field name with sanitization (replace dots with underscores).
|
||||||
|
/// For @import-typed fields, preserves dots as real Zig field access and
|
||||||
|
/// inserts `.?` after the base if the type is optional (e.g., user.name -> user.?.name).
|
||||||
fn writeSanitizedFieldName(self: *Codegen, field_name: []const u8) !void {
|
fn writeSanitizedFieldName(self: *Codegen, field_name: []const u8) !void {
|
||||||
|
if (std.mem.indexOf(u8, field_name, ".")) |dot_idx| {
|
||||||
|
const base = field_name[0..dot_idx];
|
||||||
|
if (self.type_hints.get(base)) |info| {
|
||||||
|
if (info.import_type != null) {
|
||||||
|
// Preserve dots as real Zig field access
|
||||||
|
try self.write(base);
|
||||||
|
if (info.is_optional) {
|
||||||
|
try self.write(".?");
|
||||||
|
}
|
||||||
|
try self.write(field_name[dot_idx..]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default: replace dots with underscores
|
||||||
for (field_name) |c| {
|
for (field_name) |c| {
|
||||||
try self.output.append(self.allocator, if (c == '.') '_' else c);
|
try self.output.append(self.allocator, if (c == '.') '_' else c);
|
||||||
}
|
}
|
||||||
@@ -698,7 +717,16 @@ pub const Codegen = struct {
|
|||||||
|
|
||||||
/// Check if a field has a non-string type hint (requires helpers.appendValue)
|
/// Check if a field has a non-string type hint (requires helpers.appendValue)
|
||||||
fn hasNonStringTypeHint(self: *Codegen, field_name: []const u8) bool {
|
fn hasNonStringTypeHint(self: *Codegen, field_name: []const u8) bool {
|
||||||
const type_info = self.type_hints.get(field_name) orelse return false;
|
// Direct lookup first, then try base name for dotted fields (e.g., "user.name" -> "user")
|
||||||
|
const type_info = self.type_hints.get(field_name) orelse blk: {
|
||||||
|
if (std.mem.indexOf(u8, field_name, ".")) |dot_idx| {
|
||||||
|
break :blk self.type_hints.get(field_name[0..dot_idx]);
|
||||||
|
}
|
||||||
|
break :blk null;
|
||||||
|
} orelse return false;
|
||||||
|
|
||||||
|
// Import types always need appendValue (field access returns non-string types)
|
||||||
|
if (type_info.import_type != null) return true;
|
||||||
|
|
||||||
// Arrays always need appendValue
|
// Arrays always need appendValue
|
||||||
if (type_info.is_array) return true;
|
if (type_info.is_array) return true;
|
||||||
@@ -725,6 +753,23 @@ pub const Codegen = struct {
|
|||||||
|
|
||||||
/// Write type information to output (generates Zig type syntax with default value)
|
/// Write type information to output (generates Zig type syntax with default value)
|
||||||
fn writeTypeInfo(self: *Codegen, type_info: TypeInfo) !void {
|
fn writeTypeInfo(self: *Codegen, type_info: TypeInfo) !void {
|
||||||
|
// Handle @import(...) type expressions (verbatim Zig types)
|
||||||
|
if (type_info.import_type) |import_expr| {
|
||||||
|
if (type_info.is_array) {
|
||||||
|
try self.write("[]const ");
|
||||||
|
}
|
||||||
|
if (type_info.is_optional) {
|
||||||
|
try self.write("?");
|
||||||
|
}
|
||||||
|
try self.write(import_expr);
|
||||||
|
if (type_info.is_optional) {
|
||||||
|
try self.write(" = null");
|
||||||
|
} else if (type_info.is_array) {
|
||||||
|
try self.write(" = &.{}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (type_info.is_array) {
|
if (type_info.is_array) {
|
||||||
// Array type: []const struct { ... } = &.{}
|
// Array type: []const struct { ... } = &.{}
|
||||||
if (type_info.struct_fields) |struct_fields| {
|
if (type_info.struct_fields) |struct_fields| {
|
||||||
@@ -814,7 +859,8 @@ pub const Codegen = struct {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Extract all data field names referenced in an AST
|
/// Extract all data field names referenced in an AST
|
||||||
/// Sanitizes field names to be valid Zig identifiers (replaces '.' with '_')
|
/// Sanitizes field names to be valid Zig identifiers (replaces '.' with '_'),
|
||||||
|
/// except for fields whose base has an @import type hint (those collapse to the base name).
|
||||||
pub fn extractFieldNames(allocator: Allocator, ast: *Node) ![][]const u8 {
|
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();
|
||||||
@@ -824,16 +870,40 @@ pub fn extractFieldNames(allocator: Allocator, ast: *Node) ![][]const u8 {
|
|||||||
|
|
||||||
try extractFieldNamesRecursive(ast, &fields, &loop_vars);
|
try extractFieldNamesRecursive(ast, &fields, &loop_vars);
|
||||||
|
|
||||||
// Convert to sorted slice and sanitize field names
|
// Collect type hints to identify @import-typed fields
|
||||||
|
var type_hints = std.StringHashMap(TypeInfo).init(allocator);
|
||||||
|
defer type_hints.deinit();
|
||||||
|
try collectTypeHints(allocator, ast, &type_hints);
|
||||||
|
|
||||||
|
// Convert to sorted slice, collapsing dotted fields whose base has an @import type
|
||||||
var result: std.ArrayList([]const u8) = .{};
|
var result: std.ArrayList([]const u8) = .{};
|
||||||
errdefer {
|
errdefer {
|
||||||
for (result.items) |item| allocator.free(item);
|
for (result.items) |item| allocator.free(item);
|
||||||
result.deinit(allocator);
|
result.deinit(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track which base names we've already added (to avoid duplicates after collapsing)
|
||||||
|
var seen = std.StringHashMap(void).init(allocator);
|
||||||
|
defer seen.deinit();
|
||||||
|
|
||||||
var iter = fields.keyIterator();
|
var iter = fields.keyIterator();
|
||||||
while (iter.next()) |key| {
|
while (iter.next()) |key| {
|
||||||
// Sanitize: replace dots with underscores for valid Zig identifiers
|
// For dotted fields (e.g., "user.name"), check if base has an @import type
|
||||||
|
if (std.mem.indexOf(u8, key.*, ".")) |dot_idx| {
|
||||||
|
const base = key.*[0..dot_idx];
|
||||||
|
if (type_hints.get(base)) |info| {
|
||||||
|
if (info.import_type != null) {
|
||||||
|
// Collapse to base name — skip if already added
|
||||||
|
if (seen.contains(base)) continue;
|
||||||
|
try seen.put(base, {});
|
||||||
|
const duped = try allocator.dupe(u8, base);
|
||||||
|
try result.append(allocator, duped);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: sanitize dots to underscores for valid Zig identifiers
|
||||||
const sanitized = try allocator.alloc(u8, key.*.len);
|
const sanitized = try allocator.alloc(u8, key.*.len);
|
||||||
errdefer allocator.free(sanitized);
|
errdefer allocator.free(sanitized);
|
||||||
|
|
||||||
@@ -841,6 +911,12 @@ pub fn extractFieldNames(allocator: Allocator, ast: *Node) ![][]const u8 {
|
|||||||
sanitized[i] = if (c == '.') '_' else c;
|
sanitized[i] = if (c == '.') '_' else c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deduplicate (base name from TypeHint node may already exist)
|
||||||
|
if (seen.contains(sanitized)) {
|
||||||
|
allocator.free(sanitized);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try seen.put(sanitized, {});
|
||||||
try result.append(allocator, sanitized);
|
try result.append(allocator, sanitized);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1021,12 +1097,24 @@ pub fn parseTypeHintSpec(allocator: Allocator, spec: []const u8) !TypeInfo {
|
|||||||
var info = TypeInfo{};
|
var info = TypeInfo{};
|
||||||
var remaining = std.mem.trim(u8, spec, " \t");
|
var remaining = std.mem.trim(u8, spec, " \t");
|
||||||
|
|
||||||
|
// Check for optional prefix ?
|
||||||
|
if (remaining.len > 0 and remaining[0] == '?') {
|
||||||
|
info.is_optional = true;
|
||||||
|
remaining = remaining[1..];
|
||||||
|
}
|
||||||
|
|
||||||
// Check for array prefix []
|
// Check for array prefix []
|
||||||
if (std.mem.startsWith(u8, remaining, "[]")) {
|
if (std.mem.startsWith(u8, remaining, "[]")) {
|
||||||
info.is_array = true;
|
info.is_array = true;
|
||||||
remaining = remaining[2..];
|
remaining = remaining[2..];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for @import(...) type expression (verbatim Zig type)
|
||||||
|
if (std.mem.startsWith(u8, remaining, "@import(")) {
|
||||||
|
info.import_type = remaining;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for struct definition {...}
|
// Check for struct definition {...}
|
||||||
if (remaining.len > 0 and remaining[0] == '{') {
|
if (remaining.len > 0 and remaining[0] == '{') {
|
||||||
info.struct_fields = try parseStructFields(allocator, remaining);
|
info.struct_fields = try parseStructFields(allocator, remaining);
|
||||||
@@ -1135,10 +1223,10 @@ test "zig_codegen - static attributes" {
|
|||||||
allocator.free(fields);
|
allocator.free(fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
var cg = Codegen.init(allocator, .{});
|
var cg = Codegen.init(allocator);
|
||||||
defer cg.deinit();
|
defer cg.deinit();
|
||||||
|
|
||||||
const zig_code = try cg.generate(parse_result.ast, "render", fields);
|
const zig_code = try cg.generate(parse_result.ast, "render", fields, null);
|
||||||
defer allocator.free(zig_code);
|
defer allocator.free(zig_code);
|
||||||
|
|
||||||
// Static attributes should be in the string literal
|
// Static attributes should be in the string literal
|
||||||
@@ -1165,10 +1253,10 @@ test "zig_codegen - dynamic attributes" {
|
|||||||
try std.testing.expectEqual(@as(usize, 1), fields.len);
|
try std.testing.expectEqual(@as(usize, 1), fields.len);
|
||||||
try std.testing.expectEqualStrings("url", fields[0]);
|
try std.testing.expectEqualStrings("url", fields[0]);
|
||||||
|
|
||||||
var cg = Codegen.init(allocator, .{});
|
var cg = Codegen.init(allocator);
|
||||||
defer cg.deinit();
|
defer cg.deinit();
|
||||||
|
|
||||||
const zig_code = try cg.generate(parse_result.ast, "render", fields);
|
const zig_code = try cg.generate(parse_result.ast, "render", fields, null);
|
||||||
defer allocator.free(zig_code);
|
defer allocator.free(zig_code);
|
||||||
|
|
||||||
// Dynamic href should use data.url
|
// Dynamic href should use data.url
|
||||||
@@ -1176,3 +1264,75 @@ test "zig_codegen - dynamic attributes" {
|
|||||||
// Static class should still be in string
|
// Static class should still be in string
|
||||||
try std.testing.expect(std.mem.indexOf(u8, zig_code, "class=\\\"btn\\\"") != null);
|
try std.testing.expect(std.mem.indexOf(u8, zig_code, "class=\\\"btn\\\"") != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "zig_codegen - @TypeOf with optional @import type" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
const source =
|
||||||
|
\\//- @TypeOf(user): ?@import("api").account.AuthUser
|
||||||
|
\\if user
|
||||||
|
\\ span= user.name
|
||||||
|
\\else
|
||||||
|
\\ a(href="/login") Login
|
||||||
|
;
|
||||||
|
|
||||||
|
var parse_result = try template.parseWithSource(allocator, source);
|
||||||
|
defer parse_result.deinit(allocator);
|
||||||
|
|
||||||
|
// Field extraction should collapse user.name to just user
|
||||||
|
const fields = try extractFieldNames(allocator, parse_result.ast);
|
||||||
|
defer {
|
||||||
|
for (fields) |field| allocator.free(field);
|
||||||
|
allocator.free(fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), fields.len);
|
||||||
|
try std.testing.expectEqualStrings("user", fields[0]);
|
||||||
|
|
||||||
|
// Generated code should have correct type and field access
|
||||||
|
var cg = Codegen.init(allocator);
|
||||||
|
defer cg.deinit();
|
||||||
|
|
||||||
|
const zig_code = try cg.generate(parse_result.ast, "render", fields, null);
|
||||||
|
defer allocator.free(zig_code);
|
||||||
|
|
||||||
|
// Data struct should have optional import type with null default
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, zig_code, "?@import(\"api\").account.AuthUser = null") != null);
|
||||||
|
// Should NOT contain user_name as a separate field
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, zig_code, "user_name") == null);
|
||||||
|
// Should use .? for optional unwrap in field access
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, zig_code, "data.user.?.name") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "zig_codegen - @TypeOf with non-optional @import type" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
const source =
|
||||||
|
\\//- @TypeOf(config) = @import("conf").Config
|
||||||
|
\\p= config.title
|
||||||
|
;
|
||||||
|
|
||||||
|
var parse_result = try template.parseWithSource(allocator, source);
|
||||||
|
defer parse_result.deinit(allocator);
|
||||||
|
|
||||||
|
const fields = try extractFieldNames(allocator, parse_result.ast);
|
||||||
|
defer {
|
||||||
|
for (fields) |field| allocator.free(field);
|
||||||
|
allocator.free(fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), fields.len);
|
||||||
|
try std.testing.expectEqualStrings("config", fields[0]);
|
||||||
|
|
||||||
|
var cg = Codegen.init(allocator);
|
||||||
|
defer cg.deinit();
|
||||||
|
|
||||||
|
const zig_code = try cg.generate(parse_result.ast, "render", fields, null);
|
||||||
|
defer allocator.free(zig_code);
|
||||||
|
|
||||||
|
// Non-optional import type: no ? prefix, no = null default
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, zig_code, "@import(\"conf\").Config,") != null);
|
||||||
|
// Field access without .? (not optional)
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, zig_code, "data.config.title") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, zig_code, "config_title") == null);
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ pub const Options = struct {
|
|||||||
|
|
||||||
pub const ViewEngine = struct {
|
pub const ViewEngine = struct {
|
||||||
options: Options,
|
options: Options,
|
||||||
|
views_dir_logged: bool = false,
|
||||||
|
|
||||||
pub fn init(options: Options) ViewEngine {
|
pub fn init(options: Options) ViewEngine {
|
||||||
return .{
|
return .{
|
||||||
@@ -97,7 +98,10 @@ pub const ViewEngine = struct {
|
|||||||
|
|
||||||
// Build full path (relative to views_dir)
|
// Build full path (relative to views_dir)
|
||||||
const full_path = self.resolvePath(allocator, resolved_template_path) catch |err| {
|
const full_path = self.resolvePath(allocator, resolved_template_path) catch |err| {
|
||||||
log.debug("failed to resolve path '{s}': {}", .{ template_path, err });
|
log.err("❌ resolvePath: '{s}' — {}", .{ template_path, err });
|
||||||
|
if (@errorReturnTrace()) |trace| {
|
||||||
|
std.debug.dumpStackTrace(trace.*);
|
||||||
|
}
|
||||||
return switch (err) {
|
return switch (err) {
|
||||||
error.PathEscapesRoot => ViewEngineError.PathEscapesRoot,
|
error.PathEscapesRoot => ViewEngineError.PathEscapesRoot,
|
||||||
else => ViewEngineError.ReadError,
|
else => ViewEngineError.ReadError,
|
||||||
@@ -107,6 +111,10 @@ pub const ViewEngine = struct {
|
|||||||
|
|
||||||
// Read template file
|
// Read template file
|
||||||
const source = std.fs.cwd().readFileAlloc(allocator, full_path, 10 * 1024 * 1024) catch |err| {
|
const source = std.fs.cwd().readFileAlloc(allocator, full_path, 10 * 1024 * 1024) catch |err| {
|
||||||
|
log.err("❌ readFile: '{s}' — {}", .{ full_path, err });
|
||||||
|
if (@errorReturnTrace()) |trace| {
|
||||||
|
std.debug.dumpStackTrace(trace.*);
|
||||||
|
}
|
||||||
return switch (err) {
|
return switch (err) {
|
||||||
error.FileNotFound => ViewEngineError.TemplateNotFound,
|
error.FileNotFound => ViewEngineError.TemplateNotFound,
|
||||||
else => ViewEngineError.ReadError,
|
else => ViewEngineError.ReadError,
|
||||||
@@ -121,7 +129,10 @@ pub const ViewEngine = struct {
|
|||||||
// 3. Both will be freed together when render() completes
|
// 3. Both will be freed together when render() completes
|
||||||
// This is acceptable since ViewEngine.render() is short-lived (single request)
|
// This is acceptable since ViewEngine.render() is short-lived (single request)
|
||||||
var parse_result = template.parseWithSource(allocator, source) catch |err| {
|
var parse_result = template.parseWithSource(allocator, source) catch |err| {
|
||||||
log.err("failed to parse template '{s}': {}", .{ full_path, err });
|
log.err("❌ parse: '{s}' — {}", .{ full_path, err });
|
||||||
|
if (@errorReturnTrace()) |trace| {
|
||||||
|
std.debug.dumpStackTrace(trace.*);
|
||||||
|
}
|
||||||
return ViewEngineError.ParseError;
|
return ViewEngineError.ParseError;
|
||||||
};
|
};
|
||||||
errdefer parse_result.deinit(allocator);
|
errdefer parse_result.deinit(allocator);
|
||||||
@@ -141,6 +152,7 @@ pub const ViewEngine = struct {
|
|||||||
// Don't free parse_result.normalized_source - it's needed while AST is alive
|
// Don't free parse_result.normalized_source - it's needed while AST is alive
|
||||||
// It will be freed when the caller uses ArenaAllocator (typical usage pattern)
|
// It will be freed when the caller uses ArenaAllocator (typical usage pattern)
|
||||||
|
|
||||||
|
log.debug("✅ '{s}'", .{resolved_template_path});
|
||||||
return final_ast;
|
return final_ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,8 +350,11 @@ pub const ViewEngine = struct {
|
|||||||
|
|
||||||
/// Resolves a template path relative to views directory.
|
/// Resolves a template path relative to views directory.
|
||||||
/// Rejects paths that escape the views root (e.g., "../etc/passwd").
|
/// Rejects paths that escape the views root (e.g., "../etc/passwd").
|
||||||
fn resolvePath(self: *const ViewEngine, allocator: std.mem.Allocator, template_path: []const u8) ![]const u8 {
|
fn resolvePath(self: *ViewEngine, allocator: std.mem.Allocator, template_path: []const u8) ![]const u8 {
|
||||||
log.debug("resolvePath: template_path='{s}', views_dir='{s}'", .{ template_path, self.options.views_dir });
|
if (!self.views_dir_logged) {
|
||||||
|
log.debug("views_dir='{s}'", .{self.options.views_dir});
|
||||||
|
self.views_dir_logged = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Add extension if not present
|
// Add extension if not present
|
||||||
const with_ext = if (std.mem.endsWith(u8, template_path, self.options.extension))
|
const with_ext = if (std.mem.endsWith(u8, template_path, self.options.extension))
|
||||||
|
|||||||
Reference in New Issue
Block a user