From 90c8f6f2fbd0e884c5811ec48d9e97bb1393d1f2 Mon Sep 17 00:00:00 2001 From: Ankit Patial Date: Tue, 27 Jan 2026 16:04:02 +0530 Subject: [PATCH] - removed cache - few comptime related changes --- README.md | 17 +-- build.zig | 8 - build.zig.zon | 7 +- examples/demo/src/main.zig | 5 +- src/codegen.zig | 26 +--- src/lexer.zig | 20 ++- src/parser.zig | 50 +++--- src/runtime.zig | 49 ++++-- src/template.zig | 23 +-- src/view_engine.zig | 306 ++++++------------------------------- tests/test_includes.zig | 7 +- 11 files changed, 140 insertions(+), 378 deletions(-) diff --git a/README.md b/README.md index ce628ba..2db7129 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pugz -A Pug template engine written in Zig. Templates are parsed once and cached, then rendered with data at runtime. +A Pug template engine written in Zig. Templates are parsed and rendered with data at runtime. ## Features @@ -13,7 +13,6 @@ A Pug template engine written in Zig. Templates are parsed once and cached, then - Mixins with parameters, defaults, rest args, and block content - Comments (rendered and unbuffered) - Pretty printing with indentation -- LRU cache with configurable size and TTL ## Installation @@ -40,7 +39,7 @@ exe.root_module.addImport("pugz", pugz_dep.module("pugz")); ### ViewEngine (Recommended) -The `ViewEngine` provides template caching and file-based template management for web servers. +The `ViewEngine` provides file-based template management for web servers. ```zig const std = @import("std"); @@ -52,7 +51,7 @@ pub fn main() !void { const allocator = gpa.allocator(); // Initialize once at server startup - var engine = try pugz.ViewEngine.init(allocator, .{ + var engine = pugz.ViewEngine.init(.{ .views_dir = "views", }); defer engine.deinit(); @@ -95,7 +94,7 @@ const httpz = @import("httpz"); var engine: pugz.ViewEngine = undefined; pub fn main() !void { - engine = try pugz.ViewEngine.init(allocator, .{ + engine = pugz.ViewEngine.init(.{ .views_dir = "views", }); defer engine.deinit(); @@ -118,13 +117,10 @@ fn handler(_: *Handler, _: *httpz.Request, res: *httpz.Response) !void { ## ViewEngine Options ```zig -var engine = try pugz.ViewEngine.init(allocator, .{ +var engine = pugz.ViewEngine.init(.{ .views_dir = "views", // Root directory for templates .extension = ".pug", // File extension (default: .pug) .pretty = false, // Enable pretty-printed output - .cache_enabled = true, // Enable AST caching - .max_cached_templates = 100, // LRU cache size (0 = unlimited) - .cache_ttl_seconds = 5, // Cache TTL for development (0 = never expires) }); ``` @@ -133,9 +129,6 @@ var engine = try pugz.ViewEngine.init(allocator, .{ | `views_dir` | `"views"` | Root directory containing templates | | `extension` | `".pug"` | File extension for templates | | `pretty` | `false` | Enable pretty-printed HTML with indentation | -| `cache_enabled` | `true` | Enable AST caching for performance | -| `max_cached_templates` | `0` | Max templates in LRU cache (0 = unlimited hashmap) | -| `cache_ttl_seconds` | `0` | Cache TTL in seconds (0 = never expires) | --- diff --git a/build.zig b/build.zig index fbd136a..8e4206a 100644 --- a/build.zig +++ b/build.zig @@ -3,19 +3,11 @@ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - // Get cache.zig dependency - const cache_dep = b.dependency("cache", .{ - .target = target, - .optimize = optimize, - }); const mod = b.addModule("pugz", .{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, - .imports = &.{ - .{ .name = "cache", .module = cache_dep.module("cache") }, - }, }); // Creates an executable that will run `test` blocks from the provided module. diff --git a/build.zig.zon b/build.zig.zon index 88dd103..6ddf4ef 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,12 +3,7 @@ .version = "0.3.1", .fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications. .minimum_zig_version = "0.15.2", - .dependencies = .{ - .cache = .{ - .url = "https://github.com/karlseguin/cache.zig/archive/b8b04054bc56bac1026ad72487983a89e5b7f93c.tar.gz", - .hash = "cache-0.0.0-winRwGaTAABp4XWPw3uPq-zvkue-fQi0L5KxpyyJEePO", - }, - }, + .dependencies = .{}, .paths = .{ "build.zig", "build.zig.zon", diff --git a/examples/demo/src/main.zig b/examples/demo/src/main.zig index e96b6d9..e04c5f1 100644 --- a/examples/demo/src/main.zig +++ b/examples/demo/src/main.zig @@ -7,7 +7,6 @@ //! - Conditionals and loops //! - Data binding //! - Pretty printing -//! - LRU cache with TTL const std = @import("std"); const httpz = @import("httpz"); @@ -208,11 +207,9 @@ const App = struct { pub fn init(allocator: Allocator) !App { return .{ .allocator = allocator, - .view = try pugz.ViewEngine.init(allocator, .{ + .view = pugz.ViewEngine.init(.{ .views_dir = "views", .pretty = true, - .max_cached_templates = 50, - .cache_ttl_seconds = 10, // 10s TTL for development }), }; } diff --git a/src/codegen.zig b/src/codegen.zig index 0f7c379..5c06623 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -13,30 +13,20 @@ pub const Node = parser.Node; pub const NodeType = parser.NodeType; pub const Attribute = parser.Attribute; -// Import runtime for attribute handling and HTML escaping +// Import runtime for attribute handling, HTML escaping, and shared constants const runtime = @import("runtime.zig"); pub const escapeChar = runtime.escapeChar; +pub const doctypes = runtime.doctypes; +pub const whitespace_sensitive_tags = runtime.whitespace_sensitive_tags; // Import error types const pug_error = @import("error.zig"); pub const PugError = pug_error.PugError; // ============================================================================ -// Doctypes +// Void Elements // ============================================================================ -pub const doctypes = std.StaticStringMap([]const u8).initComptime(.{ - .{ "html", "" }, - .{ "xml", "" }, - .{ "transitional", "" }, - .{ "strict", "" }, - .{ "frameset", "" }, - .{ "1.1", "" }, - .{ "basic", "" }, - .{ "mobile", "" }, - .{ "plist", "" }, -}); - // Self-closing (void) elements in HTML5 pub const void_elements = std.StaticStringMap(void).initComptime(.{ .{ "area", {} }, @@ -55,14 +45,6 @@ pub const void_elements = std.StaticStringMap(void).initComptime(.{ .{ "wbr", {} }, }); -// Whitespace-sensitive tags -pub const whitespace_sensitive_tags = std.StaticStringMap(void).initComptime(.{ - .{ "pre", {} }, - .{ "textarea", {} }, - .{ "script", {} }, - .{ "style", {} }, -}); - // ============================================================================ // Compiler Options // ============================================================================ diff --git a/src/lexer.zig b/src/lexer.zig index 76218eb..e0a4397 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -785,11 +785,21 @@ pub const Lexer = struct { return true; } - fn isWordChar(c: u8) bool { - return (c >= 'a' and c <= 'z') or - (c >= 'A' and c <= 'Z') or - (c >= '0' and c <= '9') or - c == '_'; + /// Comptime-generated lookup table for word characters (alphanumeric + underscore) + const word_char_table: [256]bool = blk: { + var table: [256]bool = .{false} ** 256; + var i: u16 = 'a'; + while (i <= 'z') : (i += 1) table[i] = true; + i = 'A'; + while (i <= 'Z') : (i += 1) table[i] = true; + i = '0'; + while (i <= '9') : (i += 1) table[i] = true; + table['_'] = true; + break :blk table; + }; + + inline fn isWordChar(c: u8) bool { + return word_char_table[c]; } fn filter(self: *Lexer, in_include: bool) bool { diff --git a/src/parser.zig b/src/parser.zig index 53f97c8..fb27d00 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -14,33 +14,31 @@ pub const Token = lexer.Token; // Inline Tags (tags that are typically inline in HTML) // ============================================================================ -const inline_tags = [_][]const u8{ - "a", - "abbr", - "acronym", - "b", - "br", - "code", - "em", - "font", - "i", - "img", - "ins", - "kbd", - "map", - "samp", - "small", - "span", - "strong", - "sub", - "sup", -}; +/// Comptime hash map for O(1) inline tag lookup instead of O(19) linear search +const inline_tags_map = std.StaticStringMap(void).initComptime(.{ + .{ "a", {} }, + .{ "abbr", {} }, + .{ "acronym", {} }, + .{ "b", {} }, + .{ "br", {} }, + .{ "code", {} }, + .{ "em", {} }, + .{ "font", {} }, + .{ "i", {} }, + .{ "img", {} }, + .{ "ins", {} }, + .{ "kbd", {} }, + .{ "map", {} }, + .{ "samp", {} }, + .{ "small", {} }, + .{ "span", {} }, + .{ "strong", {} }, + .{ "sub", {} }, + .{ "sup", {} }, +}); -fn isInlineTag(name: []const u8) bool { - for (inline_tags) |tag| { - if (mem.eql(u8, name, tag)) return true; - } - return false; +inline fn isInlineTag(name: []const u8) bool { + return inline_tags_map.has(name); } // ============================================================================ diff --git a/src/runtime.zig b/src/runtime.zig index ac67d68..77c9f7b 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -7,6 +7,28 @@ const ArrayListUnmanaged = std.ArrayListUnmanaged; // Pug Runtime - HTML generation utilities // ============================================================================ +/// DOCTYPE mappings - shared across codegen and template modules +pub const doctypes = std.StaticStringMap([]const u8).initComptime(.{ + .{ "html", "" }, + .{ "xml", "" }, + .{ "transitional", "" }, + .{ "strict", "" }, + .{ "frameset", "" }, + .{ "1.1", "" }, + .{ "basic", "" }, + .{ "mobile", "" }, + .{ "plist", "" }, +}); + +/// Whitespace-sensitive tags - shared across codegen and template modules +pub const whitespace_sensitive_tags = std.StaticStringMap(void).initComptime(.{ + .{ "pre", {} }, + .{ "textarea", {} }, + .{ "script", {} }, + .{ "style", {} }, + .{ "code", {} }, +}); + /// Escape HTML special characters in a string. /// Characters escaped: " & < > pub fn escape(allocator: Allocator, html: []const u8) ![]const u8 { @@ -244,15 +266,20 @@ pub fn appendEscaped(allocator: Allocator, result: *ArrayListUnmanaged(u8), html } } +/// Comptime-generated lookup table for HTML character escaping +const escape_table: [256]?[]const u8 = blk: { + var table: [256]?[]const u8 = .{null} ** 256; + table['"'] = """; + table['&'] = "&"; + table['<'] = "<"; + table['>'] = ">"; + break :blk table; +}; + /// Escape a single character, returning the escape sequence or null if no escaping needed -pub fn escapeChar(c: u8) ?[]const u8 { - return switch (c) { - '"' => """, - '&' => "&", - '<' => "<", - '>' => ">", - else => null, - }; +/// Uses comptime lookup table for O(1) access instead of switch statement +pub inline fn escapeChar(c: u8) ?[]const u8 { + return escape_table[c]; } /// Attribute entry for attrs function @@ -452,7 +479,7 @@ pub fn merge(allocator: Allocator, a: []const MergedAttrEntry, b: []const Merged var result = MergedAttrs.init(allocator); errdefer result.deinit(); - // Pre-allocate capacity to avoid reallocations (cache-friendly) + // Pre-allocate capacity to avoid reallocations const total_entries = a.len + b.len; if (total_entries > 0) { try result.entries.ensureTotalCapacity(allocator, total_entries); @@ -492,7 +519,7 @@ fn mergeEntry(result: *MergedAttrs, entry: MergedAttrEntry) !void { switch (key_type) { .class => { - // O(1) lookup using cached index + // O(1) lookup using stored index if (result.class_idx) |idx| { @branchHint(.likely); try mergeClassValue(result, idx, entry.value); @@ -502,7 +529,7 @@ fn mergeEntry(result: *MergedAttrs, entry: MergedAttrEntry) !void { } }, .style => { - // O(1) lookup using cached index + // O(1) lookup using stored index if (result.style_idx) |idx| { @branchHint(.likely); try mergeStyleValue(result, idx, entry.value); diff --git a/src/template.zig b/src/template.zig index bf4d77e..c278e63 100644 --- a/src/template.zig +++ b/src/template.zig @@ -205,14 +205,8 @@ fn detectDoctype(node: *Node, ctx: *RenderContext) void { } } -// Tags where whitespace is significant - don't add indentation inside these -const whitespace_sensitive_tags = std.StaticStringMap(void).initComptime(.{ - .{ "pre", {} }, - .{ "textarea", {} }, - .{ "script", {} }, - .{ "style", {} }, - .{ "code", {} }, -}); +// Tags where whitespace is significant - import from runtime (shared with codegen) +const whitespace_sensitive_tags = runtime.whitespace_sensitive_tags; /// Write indentation (two spaces per level) fn writeIndent(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), level: u32) Allocator.Error!void { @@ -800,17 +794,8 @@ fn renderBlockComment(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), } // Doctype mappings -const doctypes = std.StaticStringMap([]const u8).initComptime(.{ - .{ "html", "" }, - .{ "xml", "" }, - .{ "transitional", "" }, - .{ "strict", "" }, - .{ "frameset", "" }, - .{ "1.1", "" }, - .{ "basic", "" }, - .{ "mobile", "" }, - .{ "plist", "" }, -}); +// Import doctypes from runtime (shared with codegen) +const doctypes = runtime.doctypes; fn renderDoctype(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), doctype: *Node) Allocator.Error!void { if (doctype.val) |val| { diff --git a/src/view_engine.zig b/src/view_engine.zig index f178419..8c6840e 100644 --- a/src/view_engine.zig +++ b/src/view_engine.zig @@ -1,12 +1,10 @@ // ViewEngine - Template engine with include/mixin support for web servers // // Provides a high-level API for rendering Pug templates from a views directory. -// Templates are parsed once and cached in memory for fast subsequent renders. // Handles include statements and mixin resolution automatically. // // Usage: -// var engine = try ViewEngine.init(allocator, .{ .views_dir = "views" }); -// defer engine.deinit(); +// var engine = ViewEngine.init(.{ .views_dir = "views" }); // // const html = try engine.render(request_allocator, "pages/home", .{ .title = "Home" }); // @@ -26,7 +24,6 @@ const template = @import("template.zig"); const parser = @import("parser.zig"); const mixin = @import("mixin.zig"); const load = @import("load.zig"); -const cache = @import("cache"); const Node = parser.Node; const MixinRegistry = mixin.MixinRegistry; @@ -40,7 +37,6 @@ pub const ViewEngineError = error{ ViewsDirNotFound, IncludeNotFound, PathEscapesRoot, - CacheInitError, }; pub const Options = struct { @@ -50,82 +46,19 @@ pub const Options = struct { extension: []const u8 = ".pug", /// Enable pretty-printing with indentation and newlines pretty: bool = false, - /// Enable AST caching (disable for development hot-reload) - cache_enabled: bool = true, - /// Maximum number of templates to keep in cache (0 = unlimited). When set, uses LRU eviction. - max_cached_templates: u32 = 0, - /// Cache TTL in seconds (0 = never expires). For development, set to e.g. 5. - /// Only works when max_cached_templates > 0 (LRU cache mode). - cache_ttl_seconds: u32 = 0, }; -/// Cached template entry - stores AST and normalized source (AST contains slices into it) -const CachedTemplate = struct { - ast: *Node, - /// Normalized source from lexer - AST strings are slices into this - normalized_source: []const u8, - /// Key stored for cleanup when using LRU cache - key: []const u8, - - fn deinit(self: *CachedTemplate, allocator: std.mem.Allocator) void { - self.ast.deinit(allocator); - allocator.destroy(self.ast); - allocator.free(self.normalized_source); - if (self.key.len > 0) { - allocator.free(self.key); - } - } -}; - -/// LRU cache type for templates -const LruCache = cache.Cache(*CachedTemplate); - pub const ViewEngine = struct { options: Options, - /// Allocator for cached ASTs (long-lived, typically GPA) - cache_allocator: std.mem.Allocator, - /// Simple hashmap cache (unlimited size, when max_cached_templates = 0) - simple_cache: ?std.StringHashMap(CachedTemplate), - /// LRU cache (limited size, when max_cached_templates > 0) - lru_cache: ?LruCache, - pub fn init(allocator: std.mem.Allocator, options: Options) ViewEngineError!ViewEngine { - if (options.max_cached_templates > 0) { - // Use LRU cache with size limit - const lru = LruCache.init(allocator, .{ - .max_size = options.max_cached_templates, - }) catch return ViewEngineError.CacheInitError; - return .{ - .options = options, - .cache_allocator = allocator, - .simple_cache = null, - .lru_cache = lru, - }; - } else { - // Use simple unlimited hashmap - return .{ - .options = options, - .cache_allocator = allocator, - .simple_cache = std.StringHashMap(CachedTemplate).init(allocator), - .lru_cache = null, - }; - } + pub fn init(options: Options) ViewEngine { + return .{ + .options = options, + }; } pub fn deinit(self: *ViewEngine) void { - if (self.simple_cache) |*sc| { - var it = sc.iterator(); - while (it.next()) |entry| { - self.cache_allocator.free(entry.key_ptr.*); - entry.value_ptr.ast.deinit(self.cache_allocator); - self.cache_allocator.destroy(entry.value_ptr.ast); - self.cache_allocator.free(entry.value_ptr.normalized_source); - } - sc.deinit(); - } - if (self.lru_cache) |*lru| { - lru.deinit(); - } + _ = self; } /// Renders a template file with the given data context. @@ -136,134 +69,77 @@ pub const ViewEngine = struct { var registry = MixinRegistry.init(allocator); defer registry.deinit(); - // Get or parse the main AST and process includes - const ast = try self.getOrParseWithIncludes(template_path, ®istry); + // Parse the main AST and process includes + const ast = try self.parseWithIncludes(allocator, template_path, ®istry); + defer { + ast.deinit(allocator); + allocator.destroy(ast); + } - // Render the AST with mixin registry - mixins are expanded inline during rendering + // Render the AST with mixin registry return template.renderAstWithMixinsAndOptions(allocator, ast, data, ®istry, .{ .pretty = self.options.pretty, }); } - /// Get cached AST or parse it, processing includes recursively - fn getOrParseWithIncludes(self: *ViewEngine, template_path: []const u8, registry: *MixinRegistry) !*Node { - // Check cache first (only if caching is enabled for read) - if (self.options.cache_enabled) { - if (self.lru_cache) |*lru| { - if (lru.get(template_path)) |entry| { - defer entry.release(); - const cached = entry.value; - mixin.collectMixins(self.cache_allocator, cached.ast, registry) catch {}; - return cached.ast; - } - } else if (self.simple_cache) |*sc| { - if (sc.get(template_path)) |cached| { - mixin.collectMixins(self.cache_allocator, cached.ast, registry) catch {}; - return cached.ast; - } - } - } - + /// Parse a template and process includes recursively + fn parseWithIncludes(self: *ViewEngine, allocator: std.mem.Allocator, template_path: []const u8, registry: *MixinRegistry) !*Node { // Build full path (relative to views_dir) - const full_path = try self.resolvePath(self.cache_allocator, template_path); - defer self.cache_allocator.free(full_path); + const full_path = try self.resolvePath(allocator, template_path); + defer allocator.free(full_path); // Read template file - const source = std.fs.cwd().readFileAlloc(self.cache_allocator, full_path, 10 * 1024 * 1024) catch |err| { + const source = std.fs.cwd().readFileAlloc(allocator, full_path, 10 * 1024 * 1024) catch |err| { return switch (err) { error.FileNotFound => ViewEngineError.TemplateNotFound, else => ViewEngineError.ReadError, }; }; - defer self.cache_allocator.free(source); + defer allocator.free(source); - // Parse template - returns AST and normalized source that AST strings point to - var parse_result = template.parseWithSource(self.cache_allocator, source) catch |err| { + // Parse template + var parse_result = template.parseWithSource(allocator, source) catch |err| { log.err("failed to parse template '{s}': {}", .{ full_path, err }); return ViewEngineError.ParseError; }; - errdefer parse_result.deinit(self.cache_allocator); + errdefer parse_result.deinit(allocator); // Process extends (template inheritance) - must be done before includes - const final_ast = try self.processExtends(parse_result.ast, registry); + const final_ast = try self.processExtends(allocator, parse_result.ast, registry); // Process includes in the AST - try self.processIncludes(final_ast, registry); + try self.processIncludes(allocator, final_ast, registry); // Collect mixins from this template - mixin.collectMixins(self.cache_allocator, final_ast, registry) catch {}; + mixin.collectMixins(allocator, final_ast, registry) catch {}; - // Update parse_result.ast to point to final_ast for caching - parse_result.ast = final_ast; - - // Cache the AST - if (self.lru_cache) |*lru| { - // For LRU cache, we need to allocate the CachedTemplate struct - const cached_ptr = self.cache_allocator.create(CachedTemplate) catch { - parse_result.deinit(self.cache_allocator); - return ViewEngineError.OutOfMemory; - }; - const cache_key = self.cache_allocator.dupe(u8, template_path) catch { - self.cache_allocator.destroy(cached_ptr); - parse_result.deinit(self.cache_allocator); - return ViewEngineError.OutOfMemory; - }; - cached_ptr.* = .{ - .ast = parse_result.ast, - .normalized_source = parse_result.normalized_source, - .key = cache_key, - }; - // TTL: 0 means never expires, otherwise use configured seconds - const ttl = if (self.options.cache_ttl_seconds == 0) - std.math.maxInt(u32) - else - self.options.cache_ttl_seconds; - lru.put(cache_key, cached_ptr, .{ .ttl = ttl }) catch { - cached_ptr.deinit(self.cache_allocator); - self.cache_allocator.destroy(cached_ptr); - return ViewEngineError.OutOfMemory; - }; - return parse_result.ast; - } else if (self.simple_cache) |*sc| { - const cache_key = self.cache_allocator.dupe(u8, template_path) catch { - parse_result.deinit(self.cache_allocator); - return ViewEngineError.OutOfMemory; - }; - sc.put(cache_key, .{ - .ast = parse_result.ast, - .normalized_source = parse_result.normalized_source, - .key = &.{}, - }) catch { - self.cache_allocator.free(cache_key); - parse_result.deinit(self.cache_allocator); - return ViewEngineError.OutOfMemory; - }; - return parse_result.ast; - } - - return parse_result.ast; + return final_ast; } /// Process all include statements in the AST - fn processIncludes(self: *ViewEngine, node: *Node, registry: *MixinRegistry) ViewEngineError!void { + fn processIncludes(self: *ViewEngine, allocator: std.mem.Allocator, node: *Node, registry: *MixinRegistry) ViewEngineError!void { // Process Include nodes - load the file and inline its content if (node.type == .Include or node.type == .RawInclude) { if (node.file) |file| { if (file.path) |include_path| { // Load the included file (path relative to views_dir) - const included_ast = self.getOrParseWithIncludes(include_path, registry) catch |err| { + const included_ast = self.parseWithIncludes(allocator, include_path, registry) catch |err| { // For includes, convert TemplateNotFound to IncludeNotFound if (err == ViewEngineError.TemplateNotFound) { return ViewEngineError.IncludeNotFound; } return err; }; + defer { + included_ast.deinit(allocator); + allocator.destroy(included_ast); + } // For pug includes, inline the content into the node if (node.type == .Include) { // Copy children from included AST to this node for (included_ast.nodes.items) |child| { - node.nodes.append(self.cache_allocator, child) catch { + node.nodes.append(allocator, child) catch { return ViewEngineError.OutOfMemory; }; } @@ -274,12 +150,12 @@ pub const ViewEngine = struct { // Recurse into children for (node.nodes.items) |child| { - try self.processIncludes(child, registry); + try self.processIncludes(allocator, child, registry); } } /// Process extends statement - loads parent template and merges blocks - fn processExtends(self: *ViewEngine, ast: *Node, registry: *MixinRegistry) ViewEngineError!*Node { + fn processExtends(self: *ViewEngine, allocator: std.mem.Allocator, ast: *Node, registry: *MixinRegistry) ViewEngineError!*Node { if (ast.nodes.items.len == 0) return ast; // Check if first node is Extends @@ -291,15 +167,15 @@ pub const ViewEngine = struct { if (parent_path == null) return ast; // Collect named blocks from child template (excluding the extends node) - var child_blocks = std.StringHashMap(*Node).init(self.cache_allocator); + var child_blocks = std.StringHashMap(*Node).init(allocator); defer child_blocks.deinit(); for (ast.nodes.items[1..]) |node| { self.collectNamedBlocks(node, &child_blocks); } - // Load parent template WITHOUT caching (each child gets its own copy) - const parent_ast = self.parseTemplateNoCache(parent_path.?, registry) catch |err| { + // Load parent template + const parent_ast = self.parseWithIncludes(allocator, parent_path.?, registry) catch |err| { if (err == ViewEngineError.TemplateNotFound) { return ViewEngineError.IncludeNotFound; } @@ -307,41 +183,11 @@ pub const ViewEngine = struct { }; // Replace blocks in parent with child blocks - self.replaceBlocks(parent_ast, &child_blocks); + self.replaceBlocks(allocator, parent_ast, &child_blocks); return parent_ast; } - /// Parse a template without caching - used for parent layouts in extends - fn parseTemplateNoCache(self: *ViewEngine, template_path: []const u8, registry: *MixinRegistry) ViewEngineError!*Node { - const full_path = try self.resolvePath(self.cache_allocator, template_path); - defer self.cache_allocator.free(full_path); - - const source = std.fs.cwd().readFileAlloc(self.cache_allocator, full_path, 10 * 1024 * 1024) catch |err| { - return switch (err) { - error.FileNotFound => ViewEngineError.TemplateNotFound, - else => ViewEngineError.ReadError, - }; - }; - defer self.cache_allocator.free(source); - - const parse_result = template.parseWithSource(self.cache_allocator, source) catch |err| { - log.err("failed to parse template '{s}': {}", .{ full_path, err }); - return ViewEngineError.ParseError; - }; - - // Process nested extends if parent also extends another layout - const final_ast = try self.processExtends(parse_result.ast, registry); - - // Process includes - try self.processIncludes(final_ast, registry); - - // Collect mixins - mixin.collectMixins(self.cache_allocator, final_ast, registry) catch {}; - - return final_ast; - } - /// Collect all named blocks from a node tree fn collectNamedBlocks(self: *ViewEngine, node: *Node, blocks: *std.StringHashMap(*Node)) void { if (node.type == .NamedBlock) { @@ -355,7 +201,7 @@ pub const ViewEngine = struct { } /// Replace named blocks in parent with child block content - fn replaceBlocks(self: *ViewEngine, node: *Node, child_blocks: *std.StringHashMap(*Node)) void { + fn replaceBlocks(self: *ViewEngine, allocator: std.mem.Allocator, node: *Node, child_blocks: *std.StringHashMap(*Node)) void { if (node.type == .NamedBlock) { if (node.name) |name| { if (child_blocks.get(name)) |child_block| { @@ -365,20 +211,20 @@ pub const ViewEngine = struct { if (std.mem.eql(u8, mode, "append")) { // Append child content to parent block for (child_block.nodes.items) |child_node| { - node.nodes.append(self.cache_allocator, child_node) catch {}; + node.nodes.append(allocator, child_node) catch {}; } } else if (std.mem.eql(u8, mode, "prepend")) { // Prepend child content to parent block var i: usize = 0; for (child_block.nodes.items) |child_node| { - node.nodes.insert(self.cache_allocator, i, child_node) catch {}; + node.nodes.insert(allocator, i, child_node) catch {}; i += 1; } } else { // Replace (default): clear parent and use child content node.nodes.clearRetainingCapacity(); for (child_block.nodes.items) |child_node| { - node.nodes.append(self.cache_allocator, child_node) catch {}; + node.nodes.append(allocator, child_node) catch {}; } } } @@ -387,61 +233,10 @@ pub const ViewEngine = struct { // Recurse into children for (node.nodes.items) |child| { - self.replaceBlocks(child, child_blocks); + self.replaceBlocks(allocator, child, child_blocks); } } - /// Pre-load and cache all templates from views directory - pub fn preload(self: *ViewEngine) !usize { - var count: usize = 0; - var dir = std.fs.cwd().openDir(self.options.views_dir, .{ .iterate = true }) catch { - return ViewEngineError.ViewsDirNotFound; - }; - defer dir.close(); - - var walker = dir.walk(self.cache_allocator) catch return ViewEngineError.OutOfMemory; - defer walker.deinit(); - - while (walker.next() catch null) |entry| { - if (entry.kind != .file) continue; - if (!std.mem.endsWith(u8, entry.basename, self.options.extension)) continue; - - const name_len = entry.path.len - self.options.extension.len; - const template_name = entry.path[0..name_len]; - - var registry = MixinRegistry.init(self.cache_allocator); - defer registry.deinit(); - _ = self.getOrParseWithIncludes(template_name, ®istry) catch continue; - count += 1; - } - - return count; - } - - /// Clear all cached templates - pub fn clearCache(self: *ViewEngine) void { - if (self.simple_cache) |*sc| { - var it = sc.iterator(); - while (it.next()) |entry| { - self.cache_allocator.free(entry.key_ptr.*); - entry.value_ptr.ast.deinit(self.cache_allocator); - self.cache_allocator.destroy(entry.value_ptr.ast); - self.cache_allocator.free(entry.value_ptr.normalized_source); - } - sc.clearRetainingCapacity(); - } - // Note: LRU cache doesn't have a clear method, would need to recreate - } - - /// Returns the number of cached templates - pub fn cacheCount(self: *const ViewEngine) usize { - if (self.simple_cache) |sc| { - return sc.count(); - } - // LRU cache doesn't expose count easily - return 0; - } - /// Resolves a template path relative to views directory. /// 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 { @@ -465,16 +260,7 @@ pub const ViewEngine = struct { // ============================================================================ test "ViewEngine - basic init and deinit" { - const allocator = std.testing.allocator; - var engine = try ViewEngine.init(allocator, .{}); - defer engine.deinit(); -} - -test "ViewEngine - init with LRU cache" { - const allocator = std.testing.allocator; - var engine = try ViewEngine.init(allocator, .{ - .max_cached_templates = 100, - }); + var engine = ViewEngine.init(.{}); defer engine.deinit(); } @@ -497,7 +283,7 @@ test "isPathSafe - unsafe paths" { test "ViewEngine - path escape protection" { const allocator = std.testing.allocator; - var engine = try ViewEngine.init(allocator, .{ + var engine = ViewEngine.init(.{ .views_dir = "tests/test_views", }); defer engine.deinit(); diff --git a/tests/test_includes.zig b/tests/test_includes.zig index 2d6afd0..bac1783 100644 --- a/tests/test_includes.zig +++ b/tests/test_includes.zig @@ -7,12 +7,9 @@ pub fn main() !void { const allocator = gpa.allocator(); // Test: Simple include from test_views - var engine = pugz.ViewEngine.init(allocator, .{ + var engine = pugz.ViewEngine.init(.{ .views_dir = "tests/sample/01", - }) catch |err| { - std.debug.print("Init Error: {}\n", .{err}); - return err; - }; + }); defer engine.deinit(); const html = engine.render(allocator, "home", .{}) catch |err| {