- removed cache
- few comptime related changes
This commit is contained in:
17
README.md
17
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Pugz
|
# 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
|
## 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
|
- Mixins with parameters, defaults, rest args, and block content
|
||||||
- Comments (rendered and unbuffered)
|
- Comments (rendered and unbuffered)
|
||||||
- Pretty printing with indentation
|
- Pretty printing with indentation
|
||||||
- LRU cache with configurable size and TTL
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ exe.root_module.addImport("pugz", pugz_dep.module("pugz"));
|
|||||||
|
|
||||||
### ViewEngine (Recommended)
|
### 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
|
```zig
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
@@ -52,7 +51,7 @@ pub fn main() !void {
|
|||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
// Initialize once at server startup
|
// Initialize once at server startup
|
||||||
var engine = try pugz.ViewEngine.init(allocator, .{
|
var engine = pugz.ViewEngine.init(.{
|
||||||
.views_dir = "views",
|
.views_dir = "views",
|
||||||
});
|
});
|
||||||
defer engine.deinit();
|
defer engine.deinit();
|
||||||
@@ -95,7 +94,7 @@ const httpz = @import("httpz");
|
|||||||
var engine: pugz.ViewEngine = undefined;
|
var engine: pugz.ViewEngine = undefined;
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
engine = try pugz.ViewEngine.init(allocator, .{
|
engine = pugz.ViewEngine.init(.{
|
||||||
.views_dir = "views",
|
.views_dir = "views",
|
||||||
});
|
});
|
||||||
defer engine.deinit();
|
defer engine.deinit();
|
||||||
@@ -118,13 +117,10 @@ fn handler(_: *Handler, _: *httpz.Request, res: *httpz.Response) !void {
|
|||||||
## ViewEngine Options
|
## ViewEngine Options
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
var engine = try pugz.ViewEngine.init(allocator, .{
|
var engine = pugz.ViewEngine.init(.{
|
||||||
.views_dir = "views", // Root directory for templates
|
.views_dir = "views", // Root directory for templates
|
||||||
.extension = ".pug", // File extension (default: .pug)
|
.extension = ".pug", // File extension (default: .pug)
|
||||||
.pretty = false, // Enable pretty-printed output
|
.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 |
|
| `views_dir` | `"views"` | Root directory containing templates |
|
||||||
| `extension` | `".pug"` | File extension for templates |
|
| `extension` | `".pug"` | File extension for templates |
|
||||||
| `pretty` | `false` | Enable pretty-printed HTML with indentation |
|
| `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) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3,19 +3,11 @@ const std = @import("std");
|
|||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) void {
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
// Get cache.zig dependency
|
|
||||||
const cache_dep = b.dependency("cache", .{
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mod = b.addModule("pugz", .{
|
const mod = b.addModule("pugz", .{
|
||||||
.root_source_file = b.path("src/root.zig"),
|
.root_source_file = b.path("src/root.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
.imports = &.{
|
|
||||||
.{ .name = "cache", .module = cache_dep.module("cache") },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Creates an executable that will run `test` blocks from the provided module.
|
// Creates an executable that will run `test` blocks from the provided module.
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
.version = "0.3.1",
|
.version = "0.3.1",
|
||||||
.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 = .{},
|
||||||
.cache = .{
|
|
||||||
.url = "https://github.com/karlseguin/cache.zig/archive/b8b04054bc56bac1026ad72487983a89e5b7f93c.tar.gz",
|
|
||||||
.hash = "cache-0.0.0-winRwGaTAABp4XWPw3uPq-zvkue-fQi0L5KxpyyJEePO",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
.paths = .{
|
.paths = .{
|
||||||
"build.zig",
|
"build.zig",
|
||||||
"build.zig.zon",
|
"build.zig.zon",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
//! - Conditionals and loops
|
//! - Conditionals and loops
|
||||||
//! - Data binding
|
//! - Data binding
|
||||||
//! - Pretty printing
|
//! - Pretty printing
|
||||||
//! - LRU cache with TTL
|
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const httpz = @import("httpz");
|
const httpz = @import("httpz");
|
||||||
@@ -208,11 +207,9 @@ const App = struct {
|
|||||||
pub fn init(allocator: Allocator) !App {
|
pub fn init(allocator: Allocator) !App {
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.view = try pugz.ViewEngine.init(allocator, .{
|
.view = pugz.ViewEngine.init(.{
|
||||||
.views_dir = "views",
|
.views_dir = "views",
|
||||||
.pretty = true,
|
.pretty = true,
|
||||||
.max_cached_templates = 50,
|
|
||||||
.cache_ttl_seconds = 10, // 10s TTL for development
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,30 +13,20 @@ pub const Node = parser.Node;
|
|||||||
pub const NodeType = parser.NodeType;
|
pub const NodeType = parser.NodeType;
|
||||||
pub const Attribute = parser.Attribute;
|
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");
|
const runtime = @import("runtime.zig");
|
||||||
pub const escapeChar = runtime.escapeChar;
|
pub const escapeChar = runtime.escapeChar;
|
||||||
|
pub const doctypes = runtime.doctypes;
|
||||||
|
pub const whitespace_sensitive_tags = runtime.whitespace_sensitive_tags;
|
||||||
|
|
||||||
// Import error types
|
// Import error types
|
||||||
const pug_error = @import("error.zig");
|
const pug_error = @import("error.zig");
|
||||||
pub const PugError = pug_error.PugError;
|
pub const PugError = pug_error.PugError;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Doctypes
|
// Void Elements
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
pub const doctypes = std.StaticStringMap([]const u8).initComptime(.{
|
|
||||||
.{ "html", "<!DOCTYPE html>" },
|
|
||||||
.{ "xml", "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" },
|
|
||||||
.{ "transitional", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">" },
|
|
||||||
.{ "strict", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" },
|
|
||||||
.{ "frameset", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">" },
|
|
||||||
.{ "1.1", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">" },
|
|
||||||
.{ "basic", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML Basic 1.1//EN\" \"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd\">" },
|
|
||||||
.{ "mobile", "<!DOCTYPE html PUBLIC \"-//WAPFORUM//DTD XHTML Mobile 1.2//EN\" \"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd\">" },
|
|
||||||
.{ "plist", "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Self-closing (void) elements in HTML5
|
// Self-closing (void) elements in HTML5
|
||||||
pub const void_elements = std.StaticStringMap(void).initComptime(.{
|
pub const void_elements = std.StaticStringMap(void).initComptime(.{
|
||||||
.{ "area", {} },
|
.{ "area", {} },
|
||||||
@@ -55,14 +45,6 @@ pub const void_elements = std.StaticStringMap(void).initComptime(.{
|
|||||||
.{ "wbr", {} },
|
.{ "wbr", {} },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Whitespace-sensitive tags
|
|
||||||
pub const whitespace_sensitive_tags = std.StaticStringMap(void).initComptime(.{
|
|
||||||
.{ "pre", {} },
|
|
||||||
.{ "textarea", {} },
|
|
||||||
.{ "script", {} },
|
|
||||||
.{ "style", {} },
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Compiler Options
|
// Compiler Options
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -785,11 +785,21 @@ pub const Lexer = struct {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isWordChar(c: u8) bool {
|
/// Comptime-generated lookup table for word characters (alphanumeric + underscore)
|
||||||
return (c >= 'a' and c <= 'z') or
|
const word_char_table: [256]bool = blk: {
|
||||||
(c >= 'A' and c <= 'Z') or
|
var table: [256]bool = .{false} ** 256;
|
||||||
(c >= '0' and c <= '9') or
|
var i: u16 = 'a';
|
||||||
c == '_';
|
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 {
|
fn filter(self: *Lexer, in_include: bool) bool {
|
||||||
|
|||||||
@@ -14,33 +14,31 @@ pub const Token = lexer.Token;
|
|||||||
// Inline Tags (tags that are typically inline in HTML)
|
// Inline Tags (tags that are typically inline in HTML)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const inline_tags = [_][]const u8{
|
/// Comptime hash map for O(1) inline tag lookup instead of O(19) linear search
|
||||||
"a",
|
const inline_tags_map = std.StaticStringMap(void).initComptime(.{
|
||||||
"abbr",
|
.{ "a", {} },
|
||||||
"acronym",
|
.{ "abbr", {} },
|
||||||
"b",
|
.{ "acronym", {} },
|
||||||
"br",
|
.{ "b", {} },
|
||||||
"code",
|
.{ "br", {} },
|
||||||
"em",
|
.{ "code", {} },
|
||||||
"font",
|
.{ "em", {} },
|
||||||
"i",
|
.{ "font", {} },
|
||||||
"img",
|
.{ "i", {} },
|
||||||
"ins",
|
.{ "img", {} },
|
||||||
"kbd",
|
.{ "ins", {} },
|
||||||
"map",
|
.{ "kbd", {} },
|
||||||
"samp",
|
.{ "map", {} },
|
||||||
"small",
|
.{ "samp", {} },
|
||||||
"span",
|
.{ "small", {} },
|
||||||
"strong",
|
.{ "span", {} },
|
||||||
"sub",
|
.{ "strong", {} },
|
||||||
"sup",
|
.{ "sub", {} },
|
||||||
};
|
.{ "sup", {} },
|
||||||
|
});
|
||||||
|
|
||||||
fn isInlineTag(name: []const u8) bool {
|
inline fn isInlineTag(name: []const u8) bool {
|
||||||
for (inline_tags) |tag| {
|
return inline_tags_map.has(name);
|
||||||
if (mem.eql(u8, name, tag)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -7,6 +7,28 @@ const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
|||||||
// Pug Runtime - HTML generation utilities
|
// Pug Runtime - HTML generation utilities
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/// DOCTYPE mappings - shared across codegen and template modules
|
||||||
|
pub const doctypes = std.StaticStringMap([]const u8).initComptime(.{
|
||||||
|
.{ "html", "<!DOCTYPE html>" },
|
||||||
|
.{ "xml", "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" },
|
||||||
|
.{ "transitional", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">" },
|
||||||
|
.{ "strict", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" },
|
||||||
|
.{ "frameset", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">" },
|
||||||
|
.{ "1.1", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">" },
|
||||||
|
.{ "basic", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML Basic 1.1//EN\" \"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd\">" },
|
||||||
|
.{ "mobile", "<!DOCTYPE html PUBLIC \"-//WAPFORUM//DTD XHTML Mobile 1.2//EN\" \"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd\">" },
|
||||||
|
.{ "plist", "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" },
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 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.
|
/// Escape HTML special characters in a string.
|
||||||
/// Characters escaped: " & < >
|
/// Characters escaped: " & < >
|
||||||
pub fn escape(allocator: Allocator, html: []const u8) ![]const u8 {
|
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
|
/// Escape a single character, returning the escape sequence or null if no escaping needed
|
||||||
pub fn escapeChar(c: u8) ?[]const u8 {
|
/// Uses comptime lookup table for O(1) access instead of switch statement
|
||||||
return switch (c) {
|
pub inline fn escapeChar(c: u8) ?[]const u8 {
|
||||||
'"' => """,
|
return escape_table[c];
|
||||||
'&' => "&",
|
|
||||||
'<' => "<",
|
|
||||||
'>' => ">",
|
|
||||||
else => null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attribute entry for attrs function
|
/// 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);
|
var result = MergedAttrs.init(allocator);
|
||||||
errdefer result.deinit();
|
errdefer result.deinit();
|
||||||
|
|
||||||
// Pre-allocate capacity to avoid reallocations (cache-friendly)
|
// Pre-allocate capacity to avoid reallocations
|
||||||
const total_entries = a.len + b.len;
|
const total_entries = a.len + b.len;
|
||||||
if (total_entries > 0) {
|
if (total_entries > 0) {
|
||||||
try result.entries.ensureTotalCapacity(allocator, total_entries);
|
try result.entries.ensureTotalCapacity(allocator, total_entries);
|
||||||
@@ -492,7 +519,7 @@ fn mergeEntry(result: *MergedAttrs, entry: MergedAttrEntry) !void {
|
|||||||
|
|
||||||
switch (key_type) {
|
switch (key_type) {
|
||||||
.class => {
|
.class => {
|
||||||
// O(1) lookup using cached index
|
// O(1) lookup using stored index
|
||||||
if (result.class_idx) |idx| {
|
if (result.class_idx) |idx| {
|
||||||
@branchHint(.likely);
|
@branchHint(.likely);
|
||||||
try mergeClassValue(result, idx, entry.value);
|
try mergeClassValue(result, idx, entry.value);
|
||||||
@@ -502,7 +529,7 @@ fn mergeEntry(result: *MergedAttrs, entry: MergedAttrEntry) !void {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
.style => {
|
.style => {
|
||||||
// O(1) lookup using cached index
|
// O(1) lookup using stored index
|
||||||
if (result.style_idx) |idx| {
|
if (result.style_idx) |idx| {
|
||||||
@branchHint(.likely);
|
@branchHint(.likely);
|
||||||
try mergeStyleValue(result, idx, entry.value);
|
try mergeStyleValue(result, idx, entry.value);
|
||||||
|
|||||||
@@ -205,14 +205,8 @@ fn detectDoctype(node: *Node, ctx: *RenderContext) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags where whitespace is significant - don't add indentation inside these
|
// Tags where whitespace is significant - import from runtime (shared with codegen)
|
||||||
const whitespace_sensitive_tags = std.StaticStringMap(void).initComptime(.{
|
const whitespace_sensitive_tags = runtime.whitespace_sensitive_tags;
|
||||||
.{ "pre", {} },
|
|
||||||
.{ "textarea", {} },
|
|
||||||
.{ "script", {} },
|
|
||||||
.{ "style", {} },
|
|
||||||
.{ "code", {} },
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Write indentation (two spaces per level)
|
/// Write indentation (two spaces per level)
|
||||||
fn writeIndent(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), level: u32) Allocator.Error!void {
|
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
|
// Doctype mappings
|
||||||
const doctypes = std.StaticStringMap([]const u8).initComptime(.{
|
// Import doctypes from runtime (shared with codegen)
|
||||||
.{ "html", "<!DOCTYPE html>" },
|
const doctypes = runtime.doctypes;
|
||||||
.{ "xml", "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" },
|
|
||||||
.{ "transitional", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">" },
|
|
||||||
.{ "strict", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" },
|
|
||||||
.{ "frameset", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">" },
|
|
||||||
.{ "1.1", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">" },
|
|
||||||
.{ "basic", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML Basic 1.1//EN\" \"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd\">" },
|
|
||||||
.{ "mobile", "<!DOCTYPE html PUBLIC \"-//WAPFORUM//DTD XHTML Mobile 1.2//EN\" \"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd\">" },
|
|
||||||
.{ "plist", "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" },
|
|
||||||
});
|
|
||||||
|
|
||||||
fn renderDoctype(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), doctype: *Node) Allocator.Error!void {
|
fn renderDoctype(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), doctype: *Node) Allocator.Error!void {
|
||||||
if (doctype.val) |val| {
|
if (doctype.val) |val| {
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
// ViewEngine - Template engine with include/mixin support for web servers
|
// ViewEngine - Template engine with include/mixin support for web servers
|
||||||
//
|
//
|
||||||
// Provides a high-level API for rendering Pug templates from a views directory.
|
// 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.
|
// Handles include statements and mixin resolution automatically.
|
||||||
//
|
//
|
||||||
// Usage:
|
// Usage:
|
||||||
// var engine = try ViewEngine.init(allocator, .{ .views_dir = "views" });
|
// var engine = ViewEngine.init(.{ .views_dir = "views" });
|
||||||
// defer engine.deinit();
|
|
||||||
//
|
//
|
||||||
// const html = try engine.render(request_allocator, "pages/home", .{ .title = "Home" });
|
// 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 parser = @import("parser.zig");
|
||||||
const mixin = @import("mixin.zig");
|
const mixin = @import("mixin.zig");
|
||||||
const load = @import("load.zig");
|
const load = @import("load.zig");
|
||||||
const cache = @import("cache");
|
|
||||||
const Node = parser.Node;
|
const Node = parser.Node;
|
||||||
const MixinRegistry = mixin.MixinRegistry;
|
const MixinRegistry = mixin.MixinRegistry;
|
||||||
|
|
||||||
@@ -40,7 +37,6 @@ pub const ViewEngineError = error{
|
|||||||
ViewsDirNotFound,
|
ViewsDirNotFound,
|
||||||
IncludeNotFound,
|
IncludeNotFound,
|
||||||
PathEscapesRoot,
|
PathEscapesRoot,
|
||||||
CacheInitError,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Options = struct {
|
pub const Options = struct {
|
||||||
@@ -50,82 +46,19 @@ pub const Options = struct {
|
|||||||
extension: []const u8 = ".pug",
|
extension: []const u8 = ".pug",
|
||||||
/// Enable pretty-printing with indentation and newlines
|
/// Enable pretty-printing with indentation and newlines
|
||||||
pretty: bool = false,
|
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 {
|
pub const ViewEngine = struct {
|
||||||
options: Options,
|
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 {
|
pub fn init(options: Options) 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 .{
|
return .{
|
||||||
.options = options,
|
.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 deinit(self: *ViewEngine) void {
|
pub fn deinit(self: *ViewEngine) void {
|
||||||
if (self.simple_cache) |*sc| {
|
_ = self;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a template file with the given data context.
|
/// Renders a template file with the given data context.
|
||||||
@@ -136,134 +69,77 @@ pub const ViewEngine = struct {
|
|||||||
var registry = MixinRegistry.init(allocator);
|
var registry = MixinRegistry.init(allocator);
|
||||||
defer registry.deinit();
|
defer registry.deinit();
|
||||||
|
|
||||||
// Get or parse the main AST and process includes
|
// Parse the main AST and process includes
|
||||||
const ast = try self.getOrParseWithIncludes(template_path, ®istry);
|
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, .{
|
return template.renderAstWithMixinsAndOptions(allocator, ast, data, ®istry, .{
|
||||||
.pretty = self.options.pretty,
|
.pretty = self.options.pretty,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get cached AST or parse it, processing includes recursively
|
/// Parse a template and process includes recursively
|
||||||
fn getOrParseWithIncludes(self: *ViewEngine, template_path: []const u8, registry: *MixinRegistry) !*Node {
|
fn parseWithIncludes(self: *ViewEngine, allocator: std.mem.Allocator, 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build full path (relative to views_dir)
|
// Build full path (relative to views_dir)
|
||||||
const full_path = try self.resolvePath(self.cache_allocator, template_path);
|
const full_path = try self.resolvePath(allocator, template_path);
|
||||||
defer self.cache_allocator.free(full_path);
|
defer allocator.free(full_path);
|
||||||
|
|
||||||
// Read template file
|
// 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) {
|
return switch (err) {
|
||||||
error.FileNotFound => ViewEngineError.TemplateNotFound,
|
error.FileNotFound => ViewEngineError.TemplateNotFound,
|
||||||
else => ViewEngineError.ReadError,
|
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
|
// Parse template
|
||||||
var parse_result = template.parseWithSource(self.cache_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("failed to parse template '{s}': {}", .{ full_path, err });
|
||||||
return ViewEngineError.ParseError;
|
return ViewEngineError.ParseError;
|
||||||
};
|
};
|
||||||
errdefer parse_result.deinit(self.cache_allocator);
|
errdefer parse_result.deinit(allocator);
|
||||||
|
|
||||||
// Process extends (template inheritance) - must be done before includes
|
// 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
|
// Process includes in the AST
|
||||||
try self.processIncludes(final_ast, registry);
|
try self.processIncludes(allocator, final_ast, registry);
|
||||||
|
|
||||||
// Collect mixins from this template
|
// 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
|
return final_ast;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process all include statements in the 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
|
// Process Include nodes - load the file and inline its content
|
||||||
if (node.type == .Include or node.type == .RawInclude) {
|
if (node.type == .Include or node.type == .RawInclude) {
|
||||||
if (node.file) |file| {
|
if (node.file) |file| {
|
||||||
if (file.path) |include_path| {
|
if (file.path) |include_path| {
|
||||||
// Load the included file (path relative to views_dir)
|
// 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
|
// For includes, convert TemplateNotFound to IncludeNotFound
|
||||||
if (err == ViewEngineError.TemplateNotFound) {
|
if (err == ViewEngineError.TemplateNotFound) {
|
||||||
return ViewEngineError.IncludeNotFound;
|
return ViewEngineError.IncludeNotFound;
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
defer {
|
||||||
|
included_ast.deinit(allocator);
|
||||||
|
allocator.destroy(included_ast);
|
||||||
|
}
|
||||||
|
|
||||||
// For pug includes, inline the content into the node
|
// For pug includes, inline the content into the node
|
||||||
if (node.type == .Include) {
|
if (node.type == .Include) {
|
||||||
// Copy children from included AST to this node
|
// Copy children from included AST to this node
|
||||||
for (included_ast.nodes.items) |child| {
|
for (included_ast.nodes.items) |child| {
|
||||||
node.nodes.append(self.cache_allocator, child) catch {
|
node.nodes.append(allocator, child) catch {
|
||||||
return ViewEngineError.OutOfMemory;
|
return ViewEngineError.OutOfMemory;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -274,12 +150,12 @@ pub const ViewEngine = struct {
|
|||||||
|
|
||||||
// Recurse into children
|
// Recurse into children
|
||||||
for (node.nodes.items) |child| {
|
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
|
/// 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;
|
if (ast.nodes.items.len == 0) return ast;
|
||||||
|
|
||||||
// Check if first node is Extends
|
// Check if first node is Extends
|
||||||
@@ -291,15 +167,15 @@ pub const ViewEngine = struct {
|
|||||||
if (parent_path == null) return ast;
|
if (parent_path == null) return ast;
|
||||||
|
|
||||||
// Collect named blocks from child template (excluding the extends node)
|
// 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();
|
defer child_blocks.deinit();
|
||||||
|
|
||||||
for (ast.nodes.items[1..]) |node| {
|
for (ast.nodes.items[1..]) |node| {
|
||||||
self.collectNamedBlocks(node, &child_blocks);
|
self.collectNamedBlocks(node, &child_blocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load parent template WITHOUT caching (each child gets its own copy)
|
// Load parent template
|
||||||
const parent_ast = self.parseTemplateNoCache(parent_path.?, registry) catch |err| {
|
const parent_ast = self.parseWithIncludes(allocator, parent_path.?, registry) catch |err| {
|
||||||
if (err == ViewEngineError.TemplateNotFound) {
|
if (err == ViewEngineError.TemplateNotFound) {
|
||||||
return ViewEngineError.IncludeNotFound;
|
return ViewEngineError.IncludeNotFound;
|
||||||
}
|
}
|
||||||
@@ -307,41 +183,11 @@ pub const ViewEngine = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Replace blocks in parent with child blocks
|
// Replace blocks in parent with child blocks
|
||||||
self.replaceBlocks(parent_ast, &child_blocks);
|
self.replaceBlocks(allocator, parent_ast, &child_blocks);
|
||||||
|
|
||||||
return parent_ast;
|
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
|
/// Collect all named blocks from a node tree
|
||||||
fn collectNamedBlocks(self: *ViewEngine, node: *Node, blocks: *std.StringHashMap(*Node)) void {
|
fn collectNamedBlocks(self: *ViewEngine, node: *Node, blocks: *std.StringHashMap(*Node)) void {
|
||||||
if (node.type == .NamedBlock) {
|
if (node.type == .NamedBlock) {
|
||||||
@@ -355,7 +201,7 @@ pub const ViewEngine = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Replace named blocks in parent with child block content
|
/// 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.type == .NamedBlock) {
|
||||||
if (node.name) |name| {
|
if (node.name) |name| {
|
||||||
if (child_blocks.get(name)) |child_block| {
|
if (child_blocks.get(name)) |child_block| {
|
||||||
@@ -365,20 +211,20 @@ pub const ViewEngine = struct {
|
|||||||
if (std.mem.eql(u8, mode, "append")) {
|
if (std.mem.eql(u8, mode, "append")) {
|
||||||
// Append child content to parent block
|
// Append child content to parent block
|
||||||
for (child_block.nodes.items) |child_node| {
|
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")) {
|
} else if (std.mem.eql(u8, mode, "prepend")) {
|
||||||
// Prepend child content to parent block
|
// Prepend child content to parent block
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
for (child_block.nodes.items) |child_node| {
|
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;
|
i += 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Replace (default): clear parent and use child content
|
// Replace (default): clear parent and use child content
|
||||||
node.nodes.clearRetainingCapacity();
|
node.nodes.clearRetainingCapacity();
|
||||||
for (child_block.nodes.items) |child_node| {
|
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
|
// Recurse into children
|
||||||
for (node.nodes.items) |child| {
|
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.
|
/// 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: *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" {
|
test "ViewEngine - basic init and deinit" {
|
||||||
const allocator = std.testing.allocator;
|
var engine = ViewEngine.init(.{});
|
||||||
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,
|
|
||||||
});
|
|
||||||
defer engine.deinit();
|
defer engine.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +283,7 @@ test "isPathSafe - unsafe paths" {
|
|||||||
test "ViewEngine - path escape protection" {
|
test "ViewEngine - path escape protection" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
var engine = try ViewEngine.init(allocator, .{
|
var engine = ViewEngine.init(.{
|
||||||
.views_dir = "tests/test_views",
|
.views_dir = "tests/test_views",
|
||||||
});
|
});
|
||||||
defer engine.deinit();
|
defer engine.deinit();
|
||||||
|
|||||||
@@ -7,12 +7,9 @@ pub fn main() !void {
|
|||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
// Test: Simple include from test_views
|
// Test: Simple include from test_views
|
||||||
var engine = pugz.ViewEngine.init(allocator, .{
|
var engine = pugz.ViewEngine.init(.{
|
||||||
.views_dir = "tests/sample/01",
|
.views_dir = "tests/sample/01",
|
||||||
}) catch |err| {
|
});
|
||||||
std.debug.print("Init Error: {}\n", .{err});
|
|
||||||
return err;
|
|
||||||
};
|
|
||||||
defer engine.deinit();
|
defer engine.deinit();
|
||||||
|
|
||||||
const html = engine.render(allocator, "home", .{}) catch |err| {
|
const html = engine.render(allocator, "home", .{}) catch |err| {
|
||||||
|
|||||||
Reference in New Issue
Block a user