follow PugJs

This commit is contained in:
2026-01-24 23:53:19 +05:30
parent 621f8def47
commit 27c4898706
893 changed files with 44597 additions and 10484 deletions

View File

@@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"mcp__acp__Bash",
"mcp__acp__Write",
"mcp__acp__Edit"
]
}
}

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ zig-out/
zig-cache/ zig-cache/
.zig-cache/ .zig-cache/
.pugz-cache/ .pugz-cache/
.claude
node_modules node_modules
# compiled template file # compiled template file

460
CLAUDE.md
View File

@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Purpose ## Project Purpose
Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It implements Pug 3 syntax for indentation-based HTML templating with a focus on server-side rendering. Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It compiles Pug templates directly to HTML (unlike JS pug which compiles to JavaScript functions). It implements Pug 3 syntax for indentation-based HTML templating with a focus on server-side rendering.
## Rules ## Rules
- Do not auto commit, user will do it. - Do not auto commit, user will do it.
@@ -16,119 +16,142 @@ Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It implements Pug
- `zig build` - Build the project (output in `zig-out/`) - `zig build` - Build the project (output in `zig-out/`)
- `zig build test` - Run all tests - `zig build test` - Run all tests
- `zig build bench-compiled` - Run compiled templates benchmark (compare with Pug.js) - `zig build bench-v1` - Run v1 template benchmark
- `zig build bench-interpreted` - Inpterpret trmplates - `zig build bench-interpreted` - Run interpreted templates benchmark
## Architecture Overview ## Architecture Overview
The template engine supports two rendering modes: ### Compilation Pipeline
### 1. Runtime Rendering (Interpreted)
``` ```
Source → Lexer → Tokens → Parser → AST → Runtime → HTML Source → Lexer → Tokens → StripComments → Parser → AST → Linker → Codegen → HTML
``` ```
### 2. Build-Time Compilation (Compiled) ### Two Rendering Modes
```
Source → Lexer → Tokens → Parser → AST → build_templates.zig → generated.zig → Native Zig Code
```
The compiled mode is **~3x faster** than Pug.js. 1. **Static compilation** (`pug.compile`): Outputs HTML directly
2. **Data binding** (`template.renderWithData`): Supports `#{field}` interpolation with Zig structs
### Core Modules ### Core Modules
| Module | Purpose | | Module | File | Purpose |
|--------|---------| |--------|------|---------|
| **src/lexer.zig** | Tokenizes Pug source into tokens. Handles indentation tracking, raw text blocks, interpolation. | | **Lexer** | `src/lexer.zig` | Tokenizes Pug source into tokens |
| **src/parser.zig** | Converts token stream into AST. Handles nesting via indent/dedent tokens. | | **Parser** | `src/parser.zig` | Builds AST from tokens |
| **src/ast.zig** | AST node definitions (Element, Text, Conditional, Each, Mixin, etc.) | | **Runtime** | `src/runtime.zig` | Shared utilities (HTML escaping, etc.) |
| **src/runtime.zig** | Evaluates AST with data context, produces final HTML. Handles variable interpolation, conditionals, loops, mixins. | | **Error** | `src/error.zig` | Error formatting with source context |
| **src/build_templates.zig** | Build-time template compiler. Generates optimized Zig code from `.pug` templates. | | **Walk** | `src/walk.zig` | AST traversal with visitor pattern |
| **src/view_engine.zig** | High-level ViewEngine for web servers. Manages views directory, auto-loads mixins. | | **Strip Comments** | `src/strip_comments.zig` | Token filtering for comments |
| **src/root.zig** | Public library API - exports `ViewEngine`, `renderTemplate()`, `build_templates` and core types. | | **Load** | `src/load.zig` | File loading for includes/extends |
| **Linker** | `src/linker.zig` | Template inheritance (extends/blocks) |
| **Codegen** | `src/codegen.zig` | AST to HTML generation |
| **Template** | `src/template.zig` | Data binding renderer |
| **Pug** | `src/pug.zig` | Main entry point |
| **ViewEngine** | `src/view_engine.zig` | High-level API for web servers |
| **Root** | `src/root.zig` | Public library API exports |
### Test Files ### Test Files
- **src/tests/general_test.zig** - Comprehensive integration tests for all features - **src/tests/general_test.zig** - Comprehensive integration tests
- **src/tests/doctype_test.zig** - Doctype-specific tests - **src/tests/doctype_test.zig** - Doctype-specific tests
- **src/tests/inheritance_test.zig** - Template inheritance tests - **src/tests/check_list_test.zig** - Template output validation tests
- **src/lexer_test.zig** - Lexer unit tests
- **src/parser_test.zig** - Parser unit tests
## Build-Time Template Compilation ## API Usage
For maximum performance, templates can be compiled to native Zig code at build time. ### Static Compilation (no data)
### Setup in build.zig
```zig ```zig
const std = @import("std"); const std = @import("std");
const pug = @import("pugz").pug;
pub fn build(b: *std.Build) void { pub fn main() !void {
const pugz_dep = b.dependency("pugz", .{}); const allocator = std.heap.page_allocator;
// Compile templates at build time var result = try pug.compile(allocator, "p Hello World", .{});
const build_templates = @import("pugz").build_templates; defer result.deinit(allocator);
const compiled_templates = build_templates.compileTemplates(b, .{
.source_dir = "views", // Directory containing .pug files
});
const exe = b.addExecutable(.{ std.debug.print("{s}\n", .{result.html}); // <p>Hello World</p>
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.imports = &.{
.{ .name = "pugz", .module = pugz_dep.module("pugz") },
.{ .name = "tpls", .module = compiled_templates },
},
}),
});
} }
``` ```
### Usage in Code ### Dynamic Rendering with Data
```zig ```zig
const tpls = @import("tpls"); const std = @import("std");
const pugz = @import("pugz");
pub fn handleRequest(allocator: std.mem.Allocator) ![]u8 { pub fn main() !void {
// Zero-cost template rendering - just native Zig code var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
return try tpls.home(allocator, .{ defer arena.deinit();
const html = try pugz.renderTemplate(arena.allocator(),
\\h1 #{title}
\\p #{message}
, .{
.title = "Welcome", .title = "Welcome",
.user = .{ .name = "Alice", .email = "alice@example.com" }, .message = "Hello, World!",
.items = &[_][]const u8{ "One", "Two", "Three" }, });
std.debug.print("{s}\n", .{html});
// Output: <h1>Welcome</h1><p>Hello, World!</p>
}
```
### Data Binding Features
- **Interpolation**: `#{fieldName}` in text content
- **Attribute binding**: `a(href=url)` binds `url` field to href
- **Buffered code**: `p= message` outputs the `message` field
- **Auto-escaping**: HTML is escaped by default (XSS protection)
```zig
const html = try pugz.renderTemplate(allocator,
\\a(href=url, class=style) #{text}
, .{
.url = "https://example.com",
.style = "btn",
.text = "Click me!",
});
// Output: <a href="https://example.com" class="btn">Click me!</a>
```
### ViewEngine (for Web Servers)
```zig
const std = @import("std");
const pugz = @import("pugz");
const engine = pugz.ViewEngine.init(.{
.views_dir = "src/views",
.extension = ".pug",
});
// In request handler
pub fn handleRequest(allocator: std.mem.Allocator) ![]const u8 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
return try engine.render(arena.allocator(), "pages/home", .{
.title = "Home",
.user = .{ .name = "Alice" },
}); });
} }
``` ```
### Generated Code Features ### Compile Options
The compiler generates optimized Zig code with: ```zig
- **Static string merging** - Consecutive static content merged into single `appendSlice` calls pub const CompileOptions = struct {
- **Zero allocation for static templates** - Returns string literal directly filename: ?[]const u8 = null, // For error messages
- **Type-safe data access** - Uses `@field(d, "name")` for compile-time checked field access basedir: ?[]const u8 = null, // For absolute includes
- **Automatic type conversion** - `strVal()` helper converts integers to strings pretty: bool = false, // Pretty print output
- **Optional handling** - Nullable slices handled with `orelse &.{}` strip_unbuffered_comments: bool = true,
- **HTML escaping** - Lookup table for fast character escaping strip_buffered_comments: bool = false,
debug: bool = false,
### Benchmark Results (2000 iterations) doctype: ?[]const u8 = null,
};
| Template | Pug.js | Pugz | Speedup |
|----------|--------|------|---------|
| simple-0 | 0.8ms | 0.1ms | **8x** |
| simple-1 | 1.4ms | 0.6ms | **2.3x** |
| simple-2 | 1.8ms | 0.6ms | **3x** |
| if-expression | 0.6ms | 0.2ms | **3x** |
| projects-escaped | 4.4ms | 0.6ms | **7.3x** |
| search-results | 15.2ms | 5.6ms | **2.7x** |
| friends | 153.5ms | 54.0ms | **2.8x** |
| **TOTAL** | **177.6ms** | **61.6ms** | **~3x faster** |
Run benchmarks:
```bash
# Pugz (Zig)
zig build bench-compiled
# Pug.js (for comparison)
cd src/benchmarks/pugjs && npm install && npm run bench
``` ```
## Memory Management ## Memory Management
@@ -144,46 +167,30 @@ const html = try pugz.renderTemplate(arena.allocator(), template, data);
This pattern is recommended because template rendering creates many small allocations that are all freed together after the response is sent. This pattern is recommended because template rendering creates many small allocations that are all freed together after the response is sent.
## Key Implementation Details ## Key Implementation Notes
### Lexer State Machine ### Lexer (`lexer.zig`)
- `Lexer.init(allocator, source, options)` - Initialize
- `Lexer.getTokens()` - Returns token slice
- `Lexer.last_error` - Check for errors after failed `getTokens()`
The lexer tracks several states for handling complex syntax: ### Parser (`parser.zig`)
- `in_raw_block` / `raw_block_indent` / `raw_block_started` - For dot block text (e.g., `script.`) - `Parser.init(allocator, tokens, filename, source)` - Initialize
- `indent_stack` - Stack-based indent/dedent token generation - `Parser.parse()` - Returns AST root node
- `Parser.err` - Check for errors after failed `parse()`
**Important**: The lexer distinguishes between `#id` (ID selector), `#{expr}` (interpolation), and `#[tag]` (tag interpolation) by looking ahead at the next character. ### Codegen (`codegen.zig`)
- `Compiler.init(allocator, options)` - Initialize
- `Compiler.compile(ast)` - Returns HTML string
### Token Types ### Walk (`walk.zig`)
- Uses O(1) stack operations (append/pop) not O(n) insert/remove
- `getParent(index)` uses reverse indexing (0 = immediate parent)
- `initWithCapacity()` for pre-allocation optimization
Key tokens: `tag`, `class`, `id`, `lparen`, `rparen`, `attr_name`, `attr_value`, `text`, `interp_start`, `interp_end`, `indent`, `dedent`, `dot_block`, `pipe_text`, `literal_html`, `self_close`, `mixin_call`, etc. ### Runtime (`runtime.zig`)
- `escapeChar(c)` - Shared HTML escape function
### AST Node Types - `appendEscaped(list, allocator, str)` - Append with escaping
- `element` - HTML elements with tag, classes, id, attributes, children
- `text` - Text with segments (literal, escaped interpolation, unescaped interpolation, tag interpolation)
- `conditional` - if/else if/else/unless branches
- `each` - Iteration with value, optional index, else branch
- `mixin_def` / `mixin_call` - Mixin definitions and invocations
- `block` - Named blocks for template inheritance
- `include` / `extends` - File inclusion and inheritance
- `raw_text` - Literal HTML or text blocks
### Runtime Value System
```zig
pub const Value = union(enum) {
null,
bool: bool,
int: i64,
float: f64,
string: []const u8,
array: []const Value,
object: std.StringHashMapUnmanaged(Value),
};
```
The `toValue()` function converts Zig structs to runtime Values automatically.
## Supported Pug Features ## Supported Pug Features
@@ -222,12 +229,9 @@ p.
Multi-line Multi-line
text block text block
<p>Literal HTML</p> // passed through as-is <p>Literal HTML</p> // passed through as-is
// Interpolation-only text works too
h1.header #{title} // renders <h1 class="header">Title Value</h1>
``` ```
**Security Note**: By default, `#{}` and `=` escape HTML entities (`<`, `>`, `&`, `"`, `'`) to prevent XSS attacks. Only use `!{}` or `!=` for content you fully trust (e.g., pre-sanitized HTML from your own code). Never use unescaped output for user-provided data. **Security Note**: By default, `#{}` and `=` escape HTML entities (`<`, `>`, `&`, `"`, `'`) to prevent XSS attacks. Only use `!{}` or `!=` for content you fully trust.
### Tag Interpolation ### Tag Interpolation
```pug ```pug
@@ -240,11 +244,6 @@ p Click #[a(href="/") here] to continue
a: img(src="logo.png") // colon for inline nesting a: img(src="logo.png") // colon for inline nesting
``` ```
### Explicit Self-Closing
```pug
foo/ // renders as <foo />
```
### Conditionals ### Conditionals
```pug ```pug
if condition if condition
@@ -256,10 +255,6 @@ else
unless loggedIn unless loggedIn
p Please login p Please login
// String comparison in conditions
if status == "active"
p Active
``` ```
### Iteration ### Iteration
@@ -274,16 +269,6 @@ each item in items
li= item li= item
else else
li No items li No items
// Works with objects too (key as index)
each val, key in object
p #{key}: #{val}
// Nested iteration with field access
each friend in friends
li #{friend.name}
each tag in friend.tags
span= tag
``` ```
### Case/When ### Case/When
@@ -304,29 +289,6 @@ mixin button(text, type="primary")
+button("Click me") +button("Click me")
+button("Submit", "success") +button("Submit", "success")
// With block content
mixin card(title)
.card
h3= title
block
+card("My Card")
p Card content here
// Rest arguments
mixin list(id, ...items)
ul(id=id)
each item in items
li= item
+list("mylist", "a", "b", "c")
// Attributes pass-through
mixin link(href, text)
a(href=href)&attributes(attributes)= text
+link("/home", "Home")(class="nav-link" data-id="1")
``` ```
### Includes & Inheritance ### Includes & Inheritance
@@ -336,13 +298,6 @@ include header.pug
extends layout.pug extends layout.pug
block content block content
h1 Page Title h1 Page Title
// Block modes
block append scripts
script(src="extra.js")
block prepend styles
link(rel="stylesheet" href="extra.css")
``` ```
### Comments ### Comments
@@ -351,136 +306,57 @@ block prepend styles
//- This is a silent comment (not in output) //- This is a silent comment (not in output)
``` ```
## Server Usage ## Benchmark Results (2000 iterations)
### Compiled Templates (Recommended for Production) | Template | Time |
|----------|------|
| simple-0 | 0.8ms |
| simple-1 | 11.6ms |
| simple-2 | 8.2ms |
| if-expression | 7.4ms |
| projects-escaped | 7.1ms |
| search-results | 13.4ms |
| friends | 22.9ms |
| **TOTAL** | **71.3ms** |
Use build-time compilation for best performance. See "Build-Time Template Compilation" section above. ## Limitations vs JS Pug
### ViewEngine (Runtime Rendering) 1. **No JavaScript expressions**: `- var x = 1` not supported
2. **No nested field access**: `#{user.name}` not supported, only `#{name}`
The `ViewEngine` provides runtime template rendering with lazy-loading: 3. **No filters**: `:markdown`, `:coffee` etc. not implemented
4. **String fields only**: Data binding works best with `[]const u8` fields
```zig
const std = @import("std");
const pugz = @import("pugz");
// Initialize once at server startup
var engine = try pugz.ViewEngine.init(allocator, .{
.views_dir = "src/views", // Root views directory
.mixins_dir = "mixins", // Mixins dir for lazy-loading (optional, default: "mixins")
.extension = ".pug", // File extension (default: .pug)
.pretty = true, // Pretty-print output (default: true)
});
defer engine.deinit();
// In request handler - use arena allocator per request
pub fn handleRequest(engine: *pugz.ViewEngine, allocator: std.mem.Allocator) ![]u8 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
// Template path is relative to views_dir, extension added automatically
return try engine.render(arena.allocator(), "pages/home", .{
.title = "Home",
.user = .{ .name = "Alice" },
});
}
```
### Mixin Resolution (Lazy Loading)
Mixins are resolved in the following order:
1. **Same template** - Mixins defined in the current template file
2. **Mixins directory** - If not found, searches `views/mixins/*.pug` files (lazy-loaded on first use)
This lazy-loading approach means:
- Mixins are only parsed when first called
- No upfront loading of all mixin files at server startup
- Templates can override mixins from the mixins directory by defining them locally
### Directory Structure
```
src/views/
├── mixins/ # Lazy-loaded mixins (searched when mixin not found in template)
│ ├── buttons.pug # mixin btn(text), mixin btn-link(href, text)
│ └── cards.pug # mixin card(title), mixin card-simple(title, body)
├── layouts/
│ └── base.pug # Base layout with blocks
├── partials/
│ ├── header.pug
│ └── footer.pug
└── pages/
├── home.pug # extends layouts/base
└── about.pug # extends layouts/base
```
Templates can use:
- `extends layouts/base` - Paths relative to views_dir
- `include partials/header` - Paths relative to views_dir
- `+btn("Click")` - Mixins from mixins/ dir loaded on-demand
### Low-Level API
For inline templates or custom use cases:
```zig
const std = @import("std");
const pugz = @import("pugz");
pub fn handleRequest(allocator: std.mem.Allocator) ![]u8 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
return try pugz.renderTemplate(arena.allocator(),
\\html
\\ head
\\ title= title
\\ body
\\ h1 Hello, #{name}!
\\ if showList
\\ ul
\\ each item in items
\\ li= item
, .{
.title = "My Page",
.name = "World",
.showList = true,
.items = &[_][]const u8{ "One", "Two", "Three" },
});
}
```
## Testing
Run tests with `zig build test`. Tests cover:
- Basic element parsing and rendering
- Class and ID shorthand syntax
- Attribute parsing (quoted, unquoted, boolean, object literals)
- Text interpolation (escaped, unescaped, tag interpolation)
- Interpolation-only text (e.g., `h1.class #{var}`)
- Conditionals (if/else if/else/unless)
- Iteration (each with index, else branch, objects, nested loops)
- Case/when statements
- Mixin definitions and calls (with defaults, rest args, block content, attributes)
- Plain text (piped, dot blocks, literal HTML)
- Self-closing tags (void elements, explicit `/`)
- Block expansion with colon
- Comments (rendered and silent)
- String comparison in conditions
## Error Handling ## Error Handling
The lexer and parser return errors for invalid syntax: Uses error unions with detailed `PugError` context including line, column, and source snippet:
- `ParserError.UnexpectedToken` - `LexerError` - Tokenization errors
- `ParserError.MissingCondition` - `ParserError` - Syntax errors
- `ParserError.MissingMixinName` - `ViewEngineError` - Template not found, parse errors
- `RuntimeError.ParseError` (wrapped for convenience API)
## Future Improvements ## File Structure
Potential areas for enhancement: ```
- Filter support (`:markdown`, `:stylus`, etc.) src/
- More complete JavaScript expression evaluation ├── root.zig # Public library API
- Source maps for debugging ├── view_engine.zig # High-level ViewEngine
- Mixin support in compiled templates ├── pug.zig # Main entry point (static compilation)
├── template.zig # Data binding renderer
├── lexer.zig # Tokenizer
├── lexer_test.zig # Lexer tests
├── parser.zig # AST parser
├── parser_test.zig # Parser tests
├── runtime.zig # Shared utilities
├── error.zig # Error formatting
├── walk.zig # AST traversal
├── strip_comments.zig # Comment filtering
├── load.zig # File loading
├── linker.zig # Template inheritance
├── codegen.zig # HTML generation
├── tests/ # Integration tests
│ ├── general_test.zig
│ ├── doctype_test.zig
│ └── check_list_test.zig
└── benchmarks/ # Performance benchmarks
├── bench_v1.zig
└── bench_interpreted.zig
```

View File

@@ -1,6 +1,5 @@
*Yet not ready to use in production, i tried to get it done using Cluade but its not quite there where i want it* *! I am using ClaudeCode to build it*
*! Its Yet not ready for production use*
*So i will try it by my self keeping PugJS version as a reference*
# Pugz # Pugz

View File

@@ -1,8 +1,5 @@
const std = @import("std"); const std = @import("std");
// Re-export build_templates for use by dependent packages
pub const build_templates = @import("src/build_templates.zig");
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(.{});
@@ -46,19 +43,6 @@ pub fn build(b: *std.Build) void {
}); });
const run_doctype_tests = b.addRunArtifact(doctype_tests); const run_doctype_tests = b.addRunArtifact(doctype_tests);
// Integration tests - inheritance tests
const inheritance_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/tests/inheritance_test.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "pugz", .module = mod },
},
}),
});
const run_inheritance_tests = b.addRunArtifact(inheritance_tests);
// Integration tests - check_list tests (pug files vs expected html output) // Integration tests - check_list tests (pug files vs expected html output)
const check_list_tests = b.addTest(.{ const check_list_tests = b.addTest(.{
.root_module = b.createModule(.{ .root_module = b.createModule(.{
@@ -72,14 +56,11 @@ pub fn build(b: *std.Build) void {
}); });
const run_check_list_tests = b.addRunArtifact(check_list_tests); const run_check_list_tests = b.addRunArtifact(check_list_tests);
// A top level step for running all tests. dependOn can be called multiple // A top level step for running all tests.
// times and since the two run steps do not depend on one another, this will
// make the two of them run in parallel.
const test_step = b.step("test", "Run all tests"); const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_mod_tests.step); test_step.dependOn(&run_mod_tests.step);
test_step.dependOn(&run_general_tests.step); test_step.dependOn(&run_general_tests.step);
test_step.dependOn(&run_doctype_tests.step); test_step.dependOn(&run_doctype_tests.step);
test_step.dependOn(&run_inheritance_tests.step);
test_step.dependOn(&run_check_list_tests.step); test_step.dependOn(&run_check_list_tests.step);
// Individual test steps // Individual test steps
@@ -89,82 +70,28 @@ pub fn build(b: *std.Build) void {
const test_doctype_step = b.step("test-doctype", "Run doctype tests"); const test_doctype_step = b.step("test-doctype", "Run doctype tests");
test_doctype_step.dependOn(&run_doctype_tests.step); test_doctype_step.dependOn(&run_doctype_tests.step);
const test_inheritance_step = b.step("test-inheritance", "Run inheritance tests");
test_inheritance_step.dependOn(&run_inheritance_tests.step);
const test_unit_step = b.step("test-unit", "Run unit tests (lexer, parser, etc.)"); const test_unit_step = b.step("test-unit", "Run unit tests (lexer, parser, etc.)");
test_unit_step.dependOn(&run_mod_tests.step); test_unit_step.dependOn(&run_mod_tests.step);
const test_check_list_step = b.step("test-check-list", "Run check_list template tests"); const test_check_list_step = b.step("test-check-list", "Run check_list template tests");
test_check_list_step.dependOn(&run_check_list_tests.step); test_check_list_step.dependOn(&run_check_list_tests.step);
// ───────────────────────────────────────────────────────────────────────── // Benchmark executable
// Compiled Templates Benchmark (compare with Pug.js bench.js) const bench_exe = b.addExecutable(.{
// Uses auto-generated templates from src/benchmarks/templates/ .name = "bench",
// ─────────────────────────────────────────────────────────────────────────
const mod_fast = b.addModule("pugz-fast", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = .ReleaseFast,
});
const bench_templates = build_templates.compileTemplates(b, .{
.source_dir = "src/benchmarks/templates",
});
const bench_compiled = b.addExecutable(.{
.name = "bench-compiled",
.root_module = b.createModule(.{ .root_module = b.createModule(.{
.root_source_file = b.path("src/benchmarks/bench.zig"), .root_source_file = b.path("src/benchmarks/bench.zig"),
.target = target, .target = target,
.optimize = .ReleaseFast, .optimize = .ReleaseFast,
.imports = &.{ .imports = &.{
.{ .name = "pugz", .module = mod_fast }, .{ .name = "pugz", .module = mod },
.{ .name = "tpls", .module = bench_templates },
}, },
}), }),
}); });
b.installArtifact(bench_exe);
b.installArtifact(bench_compiled); const run_bench = b.addRunArtifact(bench_exe);
run_bench.setCwd(b.path("."));
const run_bench_compiled = b.addRunArtifact(bench_compiled); const bench_step = b.step("bench", "Run benchmark");
run_bench_compiled.step.dependOn(b.getInstallStep()); bench_step.dependOn(&run_bench.step);
const bench_compiled_step = b.step("bench-compiled", "Benchmark compiled templates (compare with Pug.js)");
bench_compiled_step.dependOn(&run_bench_compiled.step);
// ─────────────────────────────────────────────────────────────────────────
// Interpreted (Runtime) Benchmark
// ─────────────────────────────────────────────────────────────────────────
const bench_interpreted = b.addExecutable(.{
.name = "bench-interpreted",
.root_module = b.createModule(.{
.root_source_file = b.path("src/benchmarks/bench_interpreted.zig"),
.target = target,
.optimize = .ReleaseFast,
.imports = &.{
.{ .name = "pugz", .module = mod_fast },
},
}),
});
b.installArtifact(bench_interpreted);
const run_bench_interpreted = b.addRunArtifact(bench_interpreted);
run_bench_interpreted.step.dependOn(b.getInstallStep());
const bench_interpreted_step = b.step("bench-interpreted", "Benchmark interpreted (runtime) templates");
bench_interpreted_step.dependOn(&run_bench_interpreted.step);
// Just like flags, top level steps are also listed in the `--help` menu.
//
// The Zig build system is entirely implemented in userland, which means
// that it cannot hook into private compiler APIs. All compilation work
// orchestrated by the build system will result in other Zig compiler
// subcommands being invoked with the right flags defined. You can observe
// these invocations when one fails (or you pass a flag to increase
// verbosity) to validate assumptions and diagnose problems.
//
// Lastly, the Zig build system is relatively simple and self-contained,
// and reading its source code will allow you to master it.
} }

View File

@@ -14,13 +14,6 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .optimize = optimize,
}); });
// Compile templates at build time using pugz's build_templates
// Generates views/generated.zig with all templates
const build_templates = @import("pugz").build_templates;
const compiled_templates = build_templates.compileTemplates(b, .{
.source_dir = "views",
});
// Main executable // Main executable
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "demo", .name = "demo",
@@ -31,7 +24,6 @@ pub fn build(b: *std.Build) void {
.imports = &.{ .imports = &.{
.{ .name = "pugz", .module = pugz_dep.module("pugz") }, .{ .name = "pugz", .module = pugz_dep.module("pugz") },
.{ .name = "httpz", .module = httpz_dep.module("httpz") }, .{ .name = "httpz", .module = httpz_dep.module("httpz") },
.{ .name = "tpls", .module = compiled_templates },
}, },
}), }),
}); });

View File

@@ -1,22 +1,16 @@
//! Pugz Demo - Interpreted vs Compiled Templates //! Pugz Demo - ViewEngine Template Rendering
//! //!
//! This demo shows two approaches: //! This demo shows how to use ViewEngine for server-side rendering.
//! 1. **Interpreted** (ViewEngine) - supports extends/blocks, parsed at runtime
//! 2. **Compiled** (build-time) - 3x faster, templates compiled to Zig code
//! //!
//! Routes: //! Routes:
//! GET / - Compiled home page (fast) //! GET / - Home page
//! GET /users - Compiled users list (fast) //! GET /users - Users list
//! GET /interpreted - Interpreted with inheritance (flexible) //! GET /page-a - Page with data
//! GET /page-a - Interpreted page A
const std = @import("std"); const std = @import("std");
const httpz = @import("httpz"); const httpz = @import("httpz");
const pugz = @import("pugz"); const pugz = @import("pugz");
// Compiled templates - generated at build time from views/compiled/*.pug
const tpls = @import("tpls");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
/// Application state shared across all requests /// Application state shared across all requests
@@ -42,33 +36,28 @@ pub fn main() !void {
var app = App.init(allocator); var app = App.init(allocator);
const port = 8080; const port = 8081;
var server = try httpz.Server(*App).init(allocator, .{ .port = port }, &app); var server = try httpz.Server(*App).init(allocator, .{ .port = port }, &app);
defer server.deinit(); defer server.deinit();
var router = try server.router(.{}); var router = try server.router(.{});
// Compiled template routes (fast - 3x faster than Pug.js) router.get("/", index, .{});
router.get("/", indexCompiled, .{}); router.get("/users", users, .{});
router.get("/users", usersCompiled, .{});
// Interpreted template routes (flexible - supports extends/blocks)
router.get("/interpreted", indexInterpreted, .{});
router.get("/page-a", pageA, .{}); router.get("/page-a", pageA, .{});
router.get("/mixin-test", mixinTest, .{});
std.debug.print( std.debug.print(
\\ \\
\\Pugz Demo - Interpreted vs Compiled Templates \\Pugz Demo - ViewEngine Template Rendering
\\============================================= \\==========================================
\\Server running at http://localhost:{d} \\Server running at http://localhost:{d}
\\ \\
\\Compiled routes (3x faster than Pug.js): \\Routes:
\\ GET / - Home page (compiled) \\ GET / - Home page
\\ GET /users - Users list (compiled) \\ GET /users - Users list
\\ \\ GET /page-a - Page with data
\\Interpreted routes (supports extends/blocks): \\ GET /mixin-test - Mixin test page
\\ GET /interpreted - Home with ViewEngine
\\ GET /page-a - Page with inheritance
\\ \\
\\Press Ctrl+C to stop. \\Press Ctrl+C to stop.
\\ \\
@@ -77,57 +66,10 @@ pub fn main() !void {
try server.listen(); try server.listen();
} }
// ───────────────────────────────────────────────────────────────────────────── /// GET / - Home page
// Compiled template handlers (fast - no parsing at runtime) fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
// ─────────────────────────────────────────────────────────────────────────────
/// GET / - Compiled home page
fn indexCompiled(_: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = tpls.home(res.arena, .{
.title = "Welcome - Compiled",
.authenticated = true,
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// GET /users - Compiled users list
fn usersCompiled(_: *App, _: *httpz.Request, res: *httpz.Response) !void {
const User = struct {
name: []const u8,
email: []const u8,
};
const html = tpls.users(res.arena, .{
.title = "Users - Compiled",
.users = &[_]User{
.{ .name = "Alice", .email = "alice@example.com" },
.{ .name = "Bob", .email = "bob@example.com" },
.{ .name = "Charlie", .email = "charlie@example.com" },
},
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
// ─────────────────────────────────────────────────────────────────────────────
// Interpreted template handlers (flexible - supports inheritance)
// ─────────────────────────────────────────────────────────────────────────────
/// GET /interpreted - Uses ViewEngine (parsed at runtime)
fn indexInterpreted(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "index", .{ const html = app.view.render(res.arena, "index", .{
.title = "Home - Interpreted", .title = "Welcome",
.authenticated = true, .authenticated = true,
}) catch |err| { }) catch |err| {
res.status = 500; res.status = 500;
@@ -139,7 +81,21 @@ fn indexInterpreted(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
res.body = html; res.body = html;
} }
/// GET /page-a - Demonstrates extends and block override /// GET /users - Users list
fn users(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "users", .{
.title = "Users",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// GET /page-a - Page with data
fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void { fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "page-a", .{ const html = app.view.render(res.arena, "page-a", .{
.title = "Page A - Pets", .title = "Page A - Pets",
@@ -154,3 +110,15 @@ fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
res.content_type = .HTML; res.content_type = .HTML;
res.body = html; res.body = html;
} }
/// GET /mixin-test - Mixin test page
fn mixinTest(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "mixin-test", .{}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}

View File

@@ -214,6 +214,50 @@ pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 {
return o.items; return o.items;
} }
pub fn mixin_test(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<!DOCTYPE html><html><head><title>Mixin Test</title></head><body><h1>Mixin Test Page</h1><p>Testing button mixin:</p>");
{
const text = "Click Me";
const @"type" = "primary";
try o.appendSlice(a, "<button");
try o.appendSlice(a, " class=\"");
try o.appendSlice(a, "btn btn-");
try o.appendSlice(a, strVal(@"type"));
try o.appendSlice(a, "\"");
try o.appendSlice(a, ">");
try esc(&o, a, strVal(text));
try o.appendSlice(a, "</button>");
}
{
const text = "Cancel";
const @"type" = "btn btn-secondary";
try o.appendSlice(a, "<button");
try o.appendSlice(a, " class=\"");
try o.appendSlice(a, "btn btn-");
try o.appendSlice(a, strVal(@"type"));
try o.appendSlice(a, "\"");
try o.appendSlice(a, ">");
try esc(&o, a, strVal(text));
try o.appendSlice(a, "</button>");
}
try o.appendSlice(a, "<p>Testing link mixin:</p>");
{
const href = "/home";
const text = "Go Home";
try o.appendSlice(a, "<a class=\"btn btn-link\"");
try o.appendSlice(a, " href=\"");
try o.appendSlice(a, strVal(href));
try o.appendSlice(a, "\"");
try o.appendSlice(a, ">");
try esc(&o, a, strVal(text));
try o.appendSlice(a, "</a>");
}
try o.appendSlice(a, "</body></html>");
_ = d;
return o.items;
}
pub fn page_b(a: Allocator, d: anytype) Allocator.Error![]u8 { pub fn page_b(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty; var o: ArrayList = .empty;
try o.appendSlice(a, "<html><head><title>My Site - "); try o.appendSlice(a, "<html><head><title>My Site - ");
@@ -281,6 +325,7 @@ pub const template_names = [_][]const u8{
"mixins_input_text", "mixins_input_text",
"home", "home",
"page_a", "page_a",
"mixin_test",
"page_b", "page_b",
"layout_2", "layout_2",
"layout", "layout",

View File

@@ -0,0 +1,15 @@
include mixins/buttons.pug
doctype html
html
head
title Mixin Test
body
h1 Mixin Test Page
p Testing button mixin:
+btn("Click Me")
+btn("Cancel", "secondary")
p Testing link mixin:
+btn-link("/home", "Go Home")

View File

@@ -1,257 +0,0 @@
//! AST (Abstract Syntax Tree) definitions for Pug templates.
//!
//! The AST represents the hierarchical structure of a Pug document.
//! Each node type corresponds to a Pug language construct.
const std = @import("std");
/// An attribute on an element: name, value, and whether it's escaped.
pub const Attribute = struct {
name: []const u8,
value: ?[]const u8, // null for boolean attributes (e.g., `checked`)
escaped: bool, // true for `=`, false for `!=`
};
/// A segment of text content, which may be plain text or interpolation.
pub const TextSegment = union(enum) {
/// Plain text content.
literal: []const u8,
/// Escaped interpolation: #{expr} - HTML entities escaped.
interp_escaped: []const u8,
/// Unescaped interpolation: !{expr} - raw HTML output.
interp_unescaped: []const u8,
/// Tag interpolation: #[tag text] - inline HTML element.
interp_tag: InlineTag,
};
/// Inline tag from tag interpolation syntax: #[em text] or #[a(href='/') link]
pub const InlineTag = struct {
/// Tag name (e.g., "em", "a", "strong").
tag: []const u8,
/// CSS classes from `.class` syntax.
classes: []const []const u8,
/// Element ID from `#id` syntax.
id: ?[]const u8,
/// Attributes from `(attr=value)` syntax.
attributes: []Attribute,
/// Text content (may contain nested interpolations).
text_segments: []TextSegment,
};
/// All AST node types.
pub const Node = union(enum) {
/// Root document node containing all top-level nodes.
document: Document,
/// Doctype declaration: `doctype html`.
doctype: Doctype,
/// HTML element with optional tag, classes, id, attributes, and children.
element: Element,
/// Text content (may contain interpolations).
text: Text,
/// Buffered code output: `= expr` (escaped) or `!= expr` (unescaped).
code: Code,
/// Comment: `//` (rendered) or `//-` (silent).
comment: Comment,
/// Conditional: if/else if/else/unless chains.
conditional: Conditional,
/// Each loop: `each item in collection` or `each item, index in collection`.
each: Each,
/// While loop: `while condition`.
@"while": While,
/// Case/switch statement.
case: Case,
/// Mixin definition: `mixin name(args)`.
mixin_def: MixinDef,
/// Mixin call: `+name(args)`.
mixin_call: MixinCall,
/// Mixin block placeholder: `block` inside a mixin.
mixin_block: void,
/// Include directive: `include path`.
include: Include,
/// Extends directive: `extends path`.
extends: Extends,
/// Named block: `block name`.
block: Block,
/// Raw text block (after `.` on element).
raw_text: RawText,
};
/// Root document containing all top-level nodes.
pub const Document = struct {
nodes: []Node,
/// Optional extends directive (must be first if present).
extends_path: ?[]const u8 = null,
};
/// Doctype declaration node.
pub const Doctype = struct {
/// The doctype value (e.g., "html", "xml", "strict", or custom string).
/// Empty string means default to "html".
value: []const u8,
};
/// HTML element node.
pub const Element = struct {
/// Tag name (defaults to "div" if only class/id specified).
tag: []const u8,
/// CSS classes from `.class` syntax.
classes: []const []const u8,
/// Element ID from `#id` syntax.
id: ?[]const u8,
/// Attributes from `(attr=value)` syntax.
attributes: []Attribute,
/// Spread attributes from `&attributes({...})` syntax.
spread_attributes: ?[]const u8 = null,
/// Child nodes (nested elements, text, etc.).
children: []Node,
/// Whether this is a self-closing tag.
self_closing: bool,
/// Inline text content (e.g., `p Hello`).
inline_text: ?[]TextSegment,
/// Buffered code content (e.g., `p= expr` or `p!= expr`).
buffered_code: ?Code = null,
/// Whether children should be rendered inline (block expansion with `:`).
is_inline: bool = false,
};
/// Text content node.
pub const Text = struct {
/// Segments of text (literals and interpolations).
segments: []TextSegment,
};
/// Code output node: `= expr` or `!= expr`.
pub const Code = struct {
/// The expression to evaluate.
expression: []const u8,
/// Whether output is HTML-escaped.
escaped: bool,
};
/// Comment node.
pub const Comment = struct {
/// Comment text content.
content: []const u8,
/// Whether comment is rendered in output (`//`) or silent (`//-`).
rendered: bool,
/// Nested content (for block comments).
children: []Node,
};
/// Conditional node for if/else if/else/unless chains.
pub const Conditional = struct {
/// The condition branches in order.
branches: []Branch,
pub const Branch = struct {
/// Condition expression (null for `else`).
condition: ?[]const u8,
/// Whether this is `unless` (negated condition).
is_unless: bool,
/// Child nodes for this branch.
children: []Node,
};
};
/// Each loop node.
pub const Each = struct {
/// Iterator variable name.
value_name: []const u8,
/// Optional index variable name.
index_name: ?[]const u8,
/// Collection expression to iterate.
collection: []const u8,
/// Loop body nodes.
children: []Node,
/// Optional else branch (when collection is empty).
else_children: []Node,
};
/// While loop node.
pub const While = struct {
/// Loop condition expression.
condition: []const u8,
/// Loop body nodes.
children: []Node,
};
/// Case/switch node.
pub const Case = struct {
/// Expression to match against.
expression: []const u8,
/// When branches (in order, for fall-through support).
whens: []When,
/// Default branch children (if any).
default_children: []Node,
pub const When = struct {
/// Value to match.
value: []const u8,
/// Child nodes for this case. Empty means fall-through to next case.
children: []Node,
/// Explicit break (- break) means output nothing.
has_break: bool,
};
};
/// Mixin definition node.
pub const MixinDef = struct {
/// Mixin name.
name: []const u8,
/// Parameter names.
params: []const []const u8,
/// Default values for parameters (null if no default).
defaults: []?[]const u8,
/// Whether last param is rest parameter (...args).
has_rest: bool,
/// Mixin body nodes.
children: []Node,
};
/// Mixin call node.
pub const MixinCall = struct {
/// Mixin name to call.
name: []const u8,
/// Argument expressions.
args: []const []const u8,
/// Attributes passed to mixin.
attributes: []Attribute,
/// Block content passed to mixin.
block_children: []Node,
};
/// Include directive node.
pub const Include = struct {
/// Path to include.
path: []const u8,
/// Optional filter (e.g., `:markdown`).
filter: ?[]const u8,
};
/// Extends directive node.
pub const Extends = struct {
/// Path to parent template.
path: []const u8,
};
/// Named block node for template inheritance.
pub const Block = struct {
/// Block name.
name: []const u8,
/// Block mode: replace, append, or prepend.
mode: Mode,
/// Block content nodes.
children: []Node,
pub const Mode = enum {
replace,
append,
prepend,
};
};
/// Raw text block (from `.` syntax).
pub const RawText = struct {
/// Raw text content lines.
content: []const u8,
};

View File

@@ -1,22 +1,16 @@
//! Pugz Benchmark - Compiled Templates vs Pug.js //! Pugz Benchmark - Template Rendering
//! //!
//! Both Pugz and Pug.js benchmarks read from the same files: //! This benchmark uses template.zig renderWithData function.
//! src/benchmarks/templates/*.pug (templates)
//! src/benchmarks/templates/*.json (data)
//! //!
//! Run Pugz: zig build bench-all-compiled //! Run: zig build bench-v1
//! Run Pug.js: cd src/benchmarks/pugjs && npm install && npm run bench
const std = @import("std"); const std = @import("std");
const tpls = @import("tpls"); const pugz = @import("pugz");
const iterations: usize = 2000; const iterations: usize = 2000;
const templates_dir = "src/benchmarks/templates"; const templates_dir = "src/benchmarks/templates";
// ═══════════════════════════════════════════════════════════════════════════
// Data structures matching JSON files // Data structures matching JSON files
// ═══════════════════════════════════════════════════════════════════════════
const SubFriend = struct { const SubFriend = struct {
id: i64, id: i64,
name: []const u8, name: []const u8,
@@ -58,10 +52,6 @@ const SearchRecord = struct {
sizes: ?[]const []const u8, sizes: ?[]const []const u8,
}; };
// ═══════════════════════════════════════════════════════════════════════════
// Main
// ═══════════════════════════════════════════════════════════════════════════
pub fn main() !void { pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); defer _ = gpa.deinit();
@@ -69,20 +59,17 @@ pub fn main() !void {
std.debug.print("\n", .{}); std.debug.print("\n", .{});
std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{}); std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("Compiled Zig Templates Benchmark ({d} iterations) ║\n", .{iterations}); std.debug.print("V1 Template Benchmark ({d} iterations) \n", .{iterations});
std.debug.print("║ Templates: {s}/*.pug ║\n", .{templates_dir}); std.debug.print("║ Templates: {s}/*.pug ║\n", .{templates_dir});
std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{}); std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{});
// ─────────────────────────────────────────────────────────────────────────
// Load JSON data // Load JSON data
// ─────────────────────────────────────────────────────────────────────────
std.debug.print("\nLoading JSON data...\n", .{}); std.debug.print("\nLoading JSON data...\n", .{});
var data_arena = std.heap.ArenaAllocator.init(allocator); var data_arena = std.heap.ArenaAllocator.init(allocator);
defer data_arena.deinit(); defer data_arena.deinit();
const data_alloc = data_arena.allocator(); const data_alloc = data_arena.allocator();
// Load all JSON files
const simple0 = try loadJson(struct { name: []const u8 }, data_alloc, "simple-0.json"); const simple0 = try loadJson(struct { name: []const u8 }, data_alloc, "simple-0.json");
const simple1 = try loadJson(struct { const simple1 = try loadJson(struct {
name: []const u8, name: []const u8,
@@ -108,38 +95,27 @@ pub fn main() !void {
const search = try loadJson(struct { searchRecords: []const SearchRecord }, data_alloc, "search-results.json"); const search = try loadJson(struct { searchRecords: []const SearchRecord }, data_alloc, "search-results.json");
const friends_data = try loadJson(struct { friends: []const Friend }, data_alloc, "friends.json"); const friends_data = try loadJson(struct { friends: []const Friend }, data_alloc, "friends.json");
// Load template sources
const simple0_tpl = try loadTemplate(data_alloc, "simple-0.pug");
const simple1_tpl = try loadTemplate(data_alloc, "simple-1.pug");
const simple2_tpl = try loadTemplate(data_alloc, "simple-2.pug");
const if_expr_tpl = try loadTemplate(data_alloc, "if-expression.pug");
const projects_tpl = try loadTemplate(data_alloc, "projects-escaped.pug");
const search_tpl = try loadTemplate(data_alloc, "search-results.pug");
const friends_tpl = try loadTemplate(data_alloc, "friends.pug");
std.debug.print("Loaded. Starting benchmark...\n\n", .{}); std.debug.print("Loaded. Starting benchmark...\n\n", .{});
var total: f64 = 0; var total: f64 = 0;
// ───────────────────────────────────────────────────────────────────────── total += try bench("simple-0", allocator, simple0_tpl, simple0);
// Benchmark each template total += try bench("simple-1", allocator, simple1_tpl, simple1);
// ───────────────────────────────────────────────────────────────────────── total += try bench("simple-2", allocator, simple2_tpl, simple2);
total += try bench("if-expression", allocator, if_expr_tpl, if_expr);
total += try bench("projects-escaped", allocator, projects_tpl, projects);
total += try bench("search-results", allocator, search_tpl, search);
total += try bench("friends", allocator, friends_tpl, friends_data);
// simple-0
total += try bench("simple-0", allocator, tpls.simple_0, simple0);
// simple-1
total += try bench("simple-1", allocator, tpls.simple_1, simple1);
// simple-2
total += try bench("simple-2", allocator, tpls.simple_2, simple2);
// if-expression
total += try bench("if-expression", allocator, tpls.if_expression, if_expr);
// projects-escaped
total += try bench("projects-escaped", allocator, tpls.projects_escaped, projects);
// search-results
total += try bench("search-results", allocator, tpls.search_results, search);
// friends
total += try bench("friends", allocator, tpls.friends, friends_data);
// ─────────────────────────────────────────────────────────────────────────
// Summary
// ─────────────────────────────────────────────────────────────────────────
std.debug.print("\n", .{}); std.debug.print("\n", .{});
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL", total }); std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL", total });
std.debug.print("\n", .{}); std.debug.print("\n", .{});
@@ -152,10 +128,15 @@ fn loadJson(comptime T: type, alloc: std.mem.Allocator, comptime filename: []con
return parsed.value; return parsed.value;
} }
fn loadTemplate(alloc: std.mem.Allocator, comptime filename: []const u8) ![]const u8 {
const path = templates_dir ++ "/" ++ filename;
return try std.fs.cwd().readFileAlloc(alloc, path, 1 * 1024 * 1024);
}
fn bench( fn bench(
name: []const u8, name: []const u8,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
comptime render_fn: anytype, template: []const u8,
data: anytype, data: anytype,
) !f64 { ) !f64 {
var arena = std.heap.ArenaAllocator.init(allocator); var arena = std.heap.ArenaAllocator.init(allocator);
@@ -164,7 +145,10 @@ fn bench(
var timer = try std.time.Timer.start(); var timer = try std.time.Timer.start();
for (0..iterations) |_| { for (0..iterations) |_| {
_ = arena.reset(.retain_capacity); _ = arena.reset(.retain_capacity);
_ = try render_fn(arena.allocator(), data); _ = pugz.template.renderWithData(arena.allocator(), template, data) catch |err| {
std.debug.print(" {s:<20} => ERROR: {}\n", .{ name, err });
return 0;
};
} }
const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0; const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0;
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, ms }); std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, ms });

View File

@@ -1,154 +0,0 @@
//! Pugz Benchmark - Interpreted (Runtime) Mode
//!
//! This benchmark uses the ViewEngine to render templates at runtime,
//! reading from the same template/data files as the compiled benchmark.
//!
//! Run: zig build bench-interpreted
const std = @import("std");
const pugz = @import("pugz");
const iterations: usize = 2000;
const templates_dir = "src/benchmarks/templates";
// Data structures matching JSON files
const SubFriend = struct {
id: i64,
name: []const u8,
};
const Friend = struct {
name: []const u8,
balance: []const u8,
age: i64,
address: []const u8,
picture: []const u8,
company: []const u8,
email: []const u8,
emailHref: []const u8,
about: []const u8,
tags: []const []const u8,
friends: []const SubFriend,
};
const Account = struct {
balance: i64,
balanceFormatted: []const u8,
status: []const u8,
negative: bool,
};
const Project = struct {
name: []const u8,
url: []const u8,
description: []const u8,
};
const SearchRecord = struct {
imgUrl: []const u8,
viewItemUrl: []const u8,
title: []const u8,
description: []const u8,
featured: bool,
sizes: ?[]const []const u8,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
std.debug.print("\n", .{});
std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ Interpreted (Runtime) Benchmark ({d} iterations) ║\n", .{iterations});
std.debug.print("║ Templates: {s}/*.pug ║\n", .{templates_dir});
std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{});
// Load JSON data
std.debug.print("\nLoading JSON data...\n", .{});
var data_arena = std.heap.ArenaAllocator.init(allocator);
defer data_arena.deinit();
const data_alloc = data_arena.allocator();
const simple0 = try loadJson(struct { name: []const u8 }, data_alloc, "simple-0.json");
const simple1 = try loadJson(struct {
name: []const u8,
messageCount: i64,
colors: []const []const u8,
primary: bool,
}, data_alloc, "simple-1.json");
const simple2 = try loadJson(struct {
header: []const u8,
header2: []const u8,
header3: []const u8,
header4: []const u8,
header5: []const u8,
header6: []const u8,
list: []const []const u8,
}, data_alloc, "simple-2.json");
const if_expr = try loadJson(struct { accounts: []const Account }, data_alloc, "if-expression.json");
const projects = try loadJson(struct {
title: []const u8,
text: []const u8,
projects: []const Project,
}, data_alloc, "projects-escaped.json");
const search = try loadJson(struct { searchRecords: []const SearchRecord }, data_alloc, "search-results.json");
const friends_data = try loadJson(struct { friends: []const Friend }, data_alloc, "friends.json");
// Load template sources
const simple0_tpl = try loadTemplate(data_alloc, "simple-0.pug");
const simple1_tpl = try loadTemplate(data_alloc, "simple-1.pug");
const simple2_tpl = try loadTemplate(data_alloc, "simple-2.pug");
const if_expr_tpl = try loadTemplate(data_alloc, "if-expression.pug");
const projects_tpl = try loadTemplate(data_alloc, "projects-escaped.pug");
const search_tpl = try loadTemplate(data_alloc, "search-results.pug");
const friends_tpl = try loadTemplate(data_alloc, "friends.pug");
std.debug.print("Loaded. Starting benchmark...\n\n", .{});
var total: f64 = 0;
total += try bench("simple-0", allocator, simple0_tpl, simple0);
total += try bench("simple-1", allocator, simple1_tpl, simple1);
total += try bench("simple-2", allocator, simple2_tpl, simple2);
total += try bench("if-expression", allocator, if_expr_tpl, if_expr);
total += try bench("projects-escaped", allocator, projects_tpl, projects);
total += try bench("search-results", allocator, search_tpl, search);
total += try bench("friends", allocator, friends_tpl, friends_data);
std.debug.print("\n", .{});
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL", total });
std.debug.print("\n", .{});
}
fn loadJson(comptime T: type, alloc: std.mem.Allocator, comptime filename: []const u8) !T {
const path = templates_dir ++ "/" ++ filename;
const content = try std.fs.cwd().readFileAlloc(alloc, path, 10 * 1024 * 1024);
const parsed = try std.json.parseFromSlice(T, alloc, content, .{});
return parsed.value;
}
fn loadTemplate(alloc: std.mem.Allocator, comptime filename: []const u8) ![]const u8 {
const path = templates_dir ++ "/" ++ filename;
return try std.fs.cwd().readFileAlloc(alloc, path, 1 * 1024 * 1024);
}
fn bench(
name: []const u8,
allocator: std.mem.Allocator,
template: []const u8,
data: anytype,
) !f64 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
var timer = try std.time.Timer.start();
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
_ = try pugz.renderTemplate(arena.allocator(), template, data);
}
const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0;
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, ms });
return ms;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

403
src/error.zig Normal file
View File

@@ -0,0 +1,403 @@
const std = @import("std");
const mem = std.mem;
const Allocator = std.mem.Allocator;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
// ============================================================================
// Pug Error - Error formatting with source context
// Based on pug-error package
// ============================================================================
/// Pug error with source context and formatting
pub const PugError = struct {
/// Error code (e.g., "PUG:SYNTAX_ERROR")
code: []const u8,
/// Short error message
msg: []const u8,
/// Line number (1-indexed)
line: usize,
/// Column number (1-indexed, 0 if unknown)
column: usize,
/// Source filename (optional)
filename: ?[]const u8,
/// Source code (optional, for context display)
src: ?[]const u8,
/// Full formatted message with context
full_message: ?[]const u8,
allocator: Allocator,
/// Track if full_message was allocated
owns_full_message: bool,
pub fn deinit(self: *PugError) void {
if (self.owns_full_message) {
if (self.full_message) |msg| {
self.allocator.free(msg);
}
}
}
/// Get the formatted message (with context if available)
pub fn getMessage(self: *const PugError) []const u8 {
if (self.full_message) |msg| {
return msg;
}
return self.msg;
}
/// Format as JSON-like structure for serialization
pub fn toJson(self: *const PugError, allocator: Allocator) ![]const u8 {
var result: ArrayListUnmanaged(u8) = .{};
errdefer result.deinit(allocator);
try result.appendSlice(allocator, "{\"code\":\"");
try result.appendSlice(allocator, self.code);
try result.appendSlice(allocator, "\",\"msg\":\"");
try appendJsonEscaped(allocator, &result, self.msg);
try result.appendSlice(allocator, "\",\"line\":");
var buf: [32]u8 = undefined;
const line_str = std.fmt.bufPrint(&buf, "{d}", .{self.line}) catch return error.FormatError;
try result.appendSlice(allocator, line_str);
try result.appendSlice(allocator, ",\"column\":");
const col_str = std.fmt.bufPrint(&buf, "{d}", .{self.column}) catch return error.FormatError;
try result.appendSlice(allocator, col_str);
if (self.filename) |fname| {
try result.appendSlice(allocator, ",\"filename\":\"");
try appendJsonEscaped(allocator, &result, fname);
try result.append(allocator, '"');
}
try result.append(allocator, '}');
return try result.toOwnedSlice(allocator);
}
};
/// Append JSON-escaped string to result
fn appendJsonEscaped(allocator: Allocator, result: *ArrayListUnmanaged(u8), s: []const u8) !void {
for (s) |c| {
switch (c) {
'"' => try result.appendSlice(allocator, "\\\""),
'\\' => try result.appendSlice(allocator, "\\\\"),
'\n' => try result.appendSlice(allocator, "\\n"),
'\r' => try result.appendSlice(allocator, "\\r"),
'\t' => try result.appendSlice(allocator, "\\t"),
else => {
if (c < 0x20) {
// Control character - encode as \uXXXX
var hex_buf: [6]u8 = undefined;
_ = std.fmt.bufPrint(&hex_buf, "\\u{x:0>4}", .{c}) catch unreachable;
try result.appendSlice(allocator, &hex_buf);
} else {
try result.append(allocator, c);
}
},
}
}
}
/// Create a Pug error with formatted message and source context.
/// Equivalent to pug-error's makeError function.
pub fn makeError(
allocator: Allocator,
code: []const u8,
message: []const u8,
options: struct {
line: usize,
column: usize = 0,
filename: ?[]const u8 = null,
src: ?[]const u8 = null,
},
) !PugError {
var err = PugError{
.code = code,
.msg = message,
.line = options.line,
.column = options.column,
.filename = options.filename,
.src = options.src,
.full_message = null,
.allocator = allocator,
.owns_full_message = false,
};
// Format full message with context
err.full_message = try formatErrorMessage(
allocator,
code,
message,
options.line,
options.column,
options.filename,
options.src,
);
err.owns_full_message = true;
return err;
}
/// Format error message with source context (±3 lines)
fn formatErrorMessage(
allocator: Allocator,
code: []const u8,
message: []const u8,
line: usize,
column: usize,
filename: ?[]const u8,
src: ?[]const u8,
) ![]const u8 {
_ = code; // Code is embedded in PugError struct
var result: ArrayListUnmanaged(u8) = .{};
errdefer result.deinit(allocator);
// Header: filename:line:column or Pug:line:column
if (filename) |fname| {
try result.appendSlice(allocator, fname);
} else {
try result.appendSlice(allocator, "Pug");
}
try result.append(allocator, ':');
var buf: [32]u8 = undefined;
const line_str = std.fmt.bufPrint(&buf, "{d}", .{line}) catch return error.FormatError;
try result.appendSlice(allocator, line_str);
if (column > 0) {
try result.append(allocator, ':');
const col_str = std.fmt.bufPrint(&buf, "{d}", .{column}) catch return error.FormatError;
try result.appendSlice(allocator, col_str);
}
try result.append(allocator, '\n');
// Source context if available
if (src) |source| {
const lines = try splitLines(allocator, source);
defer allocator.free(lines);
if (line >= 1 and line <= lines.len) {
// Show ±3 lines around error
const start = if (line > 3) line - 3 else 1;
const end = @min(lines.len, line + 3);
var i = start;
while (i <= end) : (i += 1) {
const line_idx = i - 1;
if (line_idx >= lines.len) break;
const src_line = lines[line_idx];
// Preamble: " > 5| " or " 5| "
if (i == line) {
try result.appendSlice(allocator, " > ");
} else {
try result.appendSlice(allocator, " ");
}
// Line number (right-aligned)
const num_str = std.fmt.bufPrint(&buf, "{d}", .{i}) catch return error.FormatError;
try result.appendSlice(allocator, num_str);
try result.appendSlice(allocator, "| ");
// Source line
try result.appendSlice(allocator, src_line);
try result.append(allocator, '\n');
// Column marker for error line
if (i == line and column > 0) {
// Calculate preamble length
const preamble_len = 4 + num_str.len + 2; // " > " + num + "| "
var j: usize = 0;
while (j < preamble_len + column - 1) : (j += 1) {
try result.append(allocator, '-');
}
try result.append(allocator, '^');
try result.append(allocator, '\n');
}
}
try result.append(allocator, '\n');
}
} else {
try result.append(allocator, '\n');
}
// Error message
try result.appendSlice(allocator, message);
return try result.toOwnedSlice(allocator);
}
/// Split source into lines (handles \n, \r\n, \r)
fn splitLines(allocator: Allocator, src: []const u8) ![][]const u8 {
var lines: ArrayListUnmanaged([]const u8) = .{};
errdefer lines.deinit(allocator);
var start: usize = 0;
var i: usize = 0;
while (i < src.len) {
if (src[i] == '\n') {
try lines.append(allocator, src[start..i]);
start = i + 1;
i += 1;
} else if (src[i] == '\r') {
try lines.append(allocator, src[start..i]);
// Handle \r\n
if (i + 1 < src.len and src[i + 1] == '\n') {
i += 2;
} else {
i += 1;
}
start = i;
} else {
i += 1;
}
}
// Last line (may not end with newline)
if (start <= src.len) {
try lines.append(allocator, src[start..]);
}
return try lines.toOwnedSlice(allocator);
}
// ============================================================================
// Common error codes
// ============================================================================
pub const ErrorCode = struct {
pub const SYNTAX_ERROR = "PUG:SYNTAX_ERROR";
pub const INVALID_TOKEN = "PUG:INVALID_TOKEN";
pub const UNEXPECTED_TOKEN = "PUG:UNEXPECTED_TOKEN";
pub const INVALID_INDENTATION = "PUG:INVALID_INDENTATION";
pub const INCONSISTENT_INDENTATION = "PUG:INCONSISTENT_INDENTATION";
pub const EXTENDS_NOT_FIRST = "PUG:EXTENDS_NOT_FIRST";
pub const UNEXPECTED_BLOCK = "PUG:UNEXPECTED_BLOCK";
pub const UNEXPECTED_NODES_IN_EXTENDING_ROOT = "PUG:UNEXPECTED_NODES_IN_EXTENDING_ROOT";
pub const NO_EXTENDS_PATH = "PUG:NO_EXTENDS_PATH";
pub const NO_INCLUDE_PATH = "PUG:NO_INCLUDE_PATH";
pub const MALFORMED_EXTENDS = "PUG:MALFORMED_EXTENDS";
pub const MALFORMED_INCLUDE = "PUG:MALFORMED_INCLUDE";
pub const FILTER_NOT_FOUND = "PUG:FILTER_NOT_FOUND";
pub const INVALID_FILTER = "PUG:INVALID_FILTER";
};
// ============================================================================
// Tests
// ============================================================================
test "makeError - basic error without source" {
const allocator = std.testing.allocator;
var err = try makeError(allocator, "PUG:TEST", "test error", .{
.line = 5,
.column = 10,
.filename = "test.pug",
});
defer err.deinit();
try std.testing.expectEqualStrings("PUG:TEST", err.code);
try std.testing.expectEqualStrings("test error", err.msg);
try std.testing.expectEqual(@as(usize, 5), err.line);
try std.testing.expectEqual(@as(usize, 10), err.column);
try std.testing.expectEqualStrings("test.pug", err.filename.?);
const msg = err.getMessage();
try std.testing.expect(mem.indexOf(u8, msg, "test.pug:5:10") != null);
try std.testing.expect(mem.indexOf(u8, msg, "test error") != null);
}
test "makeError - error with source context" {
const allocator = std.testing.allocator;
const src = "line 1\nline 2\nline 3 with error\nline 4\nline 5";
var err = try makeError(allocator, "PUG:SYNTAX_ERROR", "unexpected token", .{
.line = 3,
.column = 8,
.filename = "template.pug",
.src = src,
});
defer err.deinit();
const msg = err.getMessage();
// Should contain filename:line:column
try std.testing.expect(mem.indexOf(u8, msg, "template.pug:3:8") != null);
// Should contain the error line with marker
try std.testing.expect(mem.indexOf(u8, msg, "line 3 with error") != null);
// Should contain the error message
try std.testing.expect(mem.indexOf(u8, msg, "unexpected token") != null);
// Should have column marker
try std.testing.expect(mem.indexOf(u8, msg, "^") != null);
}
test "makeError - error with source shows context lines" {
const allocator = std.testing.allocator;
const src = "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8";
var err = try makeError(allocator, "PUG:TEST", "test", .{
.line = 5,
.filename = null,
.src = src,
});
defer err.deinit();
const msg = err.getMessage();
// Should show lines 2-8 (5 ± 3)
try std.testing.expect(mem.indexOf(u8, msg, "line 2") != null);
try std.testing.expect(mem.indexOf(u8, msg, "line 5") != null);
try std.testing.expect(mem.indexOf(u8, msg, "line 8") != null);
// Line 1 should not be shown (too far before)
// Note: line 1 might appear in context depending on implementation
}
test "makeError - no filename uses Pug" {
const allocator = std.testing.allocator;
var err = try makeError(allocator, "PUG:TEST", "test error", .{
.line = 1,
});
defer err.deinit();
const msg = err.getMessage();
try std.testing.expect(mem.indexOf(u8, msg, "Pug:1") != null);
}
test "PugError.toJson" {
const allocator = std.testing.allocator;
var err = try makeError(allocator, "PUG:TEST", "test message", .{
.line = 10,
.column = 5,
.filename = "file.pug",
});
defer err.deinit();
const json = try err.toJson(allocator);
defer allocator.free(json);
try std.testing.expect(mem.indexOf(u8, json, "\"code\":\"PUG:TEST\"") != null);
try std.testing.expect(mem.indexOf(u8, json, "\"msg\":\"test message\"") != null);
try std.testing.expect(mem.indexOf(u8, json, "\"line\":10") != null);
try std.testing.expect(mem.indexOf(u8, json, "\"column\":5") != null);
try std.testing.expect(mem.indexOf(u8, json, "\"filename\":\"file.pug\"") != null);
}
test "splitLines - basic" {
const allocator = std.testing.allocator;
const lines = try splitLines(allocator, "a\nb\nc");
defer allocator.free(lines);
try std.testing.expectEqual(@as(usize, 3), lines.len);
try std.testing.expectEqualStrings("a", lines[0]);
try std.testing.expectEqualStrings("b", lines[1]);
try std.testing.expectEqualStrings("c", lines[2]);
}
test "splitLines - windows line endings" {
const allocator = std.testing.allocator;
const lines = try splitLines(allocator, "a\r\nb\r\nc");
defer allocator.free(lines);
try std.testing.expectEqual(@as(usize, 3), lines.len);
try std.testing.expectEqualStrings("a", lines[0]);
try std.testing.expectEqualStrings("b", lines[1]);
try std.testing.expectEqualStrings("c", lines[2]);
}

File diff suppressed because it is too large Load Diff

699
src/linker.zig Normal file
View File

@@ -0,0 +1,699 @@
// linker.zig - Zig port of pug-linker
//
// Handles template inheritance and linking:
// - Resolves extends (parent template inheritance)
// - Handles named blocks (replace/append/prepend modes)
// - Processes includes with yield blocks
// - Manages mixin hoisting from child to parent
const std = @import("std");
const Allocator = std.mem.Allocator;
const mem = std.mem;
// Import AST types from parser
const parser = @import("parser.zig");
pub const Node = parser.Node;
pub const NodeType = parser.NodeType;
// Import walk module
const walk_mod = @import("walk.zig");
pub const WalkOptions = walk_mod.WalkOptions;
pub const WalkContext = walk_mod.WalkContext;
pub const WalkError = walk_mod.WalkError;
pub const ReplaceResult = walk_mod.ReplaceResult;
// Import error types
const pug_error = @import("error.zig");
pub const PugError = pug_error.PugError;
// ============================================================================
// Linker Errors
// ============================================================================
pub const LinkerError = error{
OutOfMemory,
InvalidAST,
ExtendsNotFirst,
UnexpectedNodesInExtending,
UnexpectedBlock,
WalkError,
};
// ============================================================================
// Block Definitions Map
// ============================================================================
/// Map of block names to their definition nodes
pub const BlockDefinitions = std.StringHashMapUnmanaged(*Node);
// ============================================================================
// Linker Result
// ============================================================================
pub const LinkerResult = struct {
ast: *Node,
declared_blocks: BlockDefinitions,
has_extends: bool = false,
err: ?PugError = null,
pub fn deinit(self: *LinkerResult, allocator: Allocator) void {
self.declared_blocks.deinit(allocator);
if (self.err) |*e| {
e.deinit();
}
}
};
// ============================================================================
// Link Implementation
// ============================================================================
/// Link an AST, resolving extends and includes
pub fn link(allocator: Allocator, ast: *Node) LinkerError!LinkerResult {
// Top level must be a Block
if (ast.type != .Block) {
return error.InvalidAST;
}
var result = LinkerResult{
.ast = ast,
.declared_blocks = .{},
};
// Check for extends
var extends_node: ?*Node = null;
if (ast.nodes.items.len > 0) {
const first_node = ast.nodes.items[0];
if (first_node.type == .Extends) {
// Verify extends position
try checkExtendsPosition(allocator, ast);
// Remove extends node from the list
extends_node = ast.nodes.orderedRemove(0);
}
}
// Apply includes (convert RawInclude to Text, link Include ASTs)
result.ast = try applyIncludes(allocator, ast);
// Find declared blocks
result.declared_blocks = try findDeclaredBlocks(allocator, result.ast);
// Handle extends
if (extends_node) |ext_node| {
// Get mixins and expected blocks from current template
var mixins = std.ArrayListUnmanaged(*Node){};
defer mixins.deinit(allocator);
var expected_blocks = std.ArrayListUnmanaged(*Node){};
defer expected_blocks.deinit(allocator);
try collectMixinsAndBlocks(allocator, result.ast, &mixins, &expected_blocks);
// Link the parent template
if (ext_node.file) |file| {
_ = file;
// In a real implementation, we would:
// 1. Get file.ast (the loaded parent AST)
// 2. Recursively link it
// 3. Extend parent blocks with child blocks
// 4. Verify all expected blocks exist
// 5. Merge mixin definitions
// For now, mark that we have extends
result.has_extends = true;
}
}
return result;
}
/// Find all declared blocks (NamedBlock with mode="replace")
fn findDeclaredBlocks(allocator: Allocator, ast: *Node) LinkerError!BlockDefinitions {
var definitions = BlockDefinitions{};
const FindContext = struct {
defs: *BlockDefinitions,
alloc: Allocator,
fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
if (node.type == .NamedBlock) {
// Check mode - default is "replace"
const mode = node.mode orelse "replace";
if (mem.eql(u8, mode, "replace")) {
if (node.name) |name| {
self.defs.put(self.alloc, name, node) catch return error.OutOfMemory;
}
}
}
return null;
}
};
var find_ctx = FindContext{
.defs = &definitions,
.alloc = allocator,
};
var walk_options = WalkOptions{};
defer walk_options.deinit(allocator);
_ = walk_mod.walkASTWithUserData(
allocator,
ast,
FindContext.before,
null,
&walk_options,
&find_ctx,
) catch {
return error.WalkError;
};
return definitions;
}
/// Collect mixin definitions and named blocks from the AST
fn collectMixinsAndBlocks(
allocator: Allocator,
ast: *Node,
mixins: *std.ArrayListUnmanaged(*Node),
expected_blocks: *std.ArrayListUnmanaged(*Node),
) LinkerError!void {
for (ast.nodes.items) |node| {
switch (node.type) {
.NamedBlock => {
try expected_blocks.append(allocator, node);
},
.Block => {
// Recurse into nested blocks
try collectMixinsAndBlocks(allocator, node, mixins, expected_blocks);
},
.Mixin => {
// Only collect mixin definitions (not calls)
if (!node.call) {
try mixins.append(allocator, node);
}
},
else => {
// In extending template, only named blocks and mixins allowed at top level
// This would be an error in strict mode
},
}
}
}
/// Extend parent blocks with child block content
fn extendBlocks(
allocator: Allocator,
parent_blocks: *BlockDefinitions,
child_ast: *Node,
) LinkerError!void {
const ExtendContext = struct {
parent: *BlockDefinitions,
stack: std.StringHashMapUnmanaged(void),
alloc: Allocator,
fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
if (node.type == .NamedBlock) {
if (node.name) |name| {
// Check for circular reference
if (self.stack.contains(name)) {
return null; // Skip to avoid infinite loop
}
self.stack.put(self.alloc, name, {}) catch return error.OutOfMemory;
// Find parent block
if (self.parent.get(name)) |parent_block| {
const mode = node.mode orelse "replace";
if (mem.eql(u8, mode, "append")) {
// Append child nodes to parent
for (node.nodes.items) |child_node| {
parent_block.nodes.append(self.alloc, child_node) catch return error.OutOfMemory;
}
} else if (mem.eql(u8, mode, "prepend")) {
// Prepend child nodes to parent
for (node.nodes.items, 0..) |child_node, i| {
parent_block.nodes.insert(self.alloc, i, child_node) catch return error.OutOfMemory;
}
} else {
// Replace - clear parent and add child nodes
parent_block.nodes.clearRetainingCapacity();
for (node.nodes.items) |child_node| {
parent_block.nodes.append(self.alloc, child_node) catch return error.OutOfMemory;
}
}
}
}
}
return null;
}
fn after(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
if (node.type == .NamedBlock) {
if (node.name) |name| {
_ = self.stack.remove(name);
}
}
return null;
}
};
var extend_ctx = ExtendContext{
.parent = parent_blocks,
.stack = .{},
.alloc = allocator,
};
defer extend_ctx.stack.deinit(allocator);
var walk_options = WalkOptions{};
defer walk_options.deinit(allocator);
_ = walk_mod.walkASTWithUserData(
allocator,
child_ast,
ExtendContext.before,
ExtendContext.after,
&walk_options,
&extend_ctx,
) catch {
return error.WalkError;
};
}
/// Apply includes - convert RawInclude to Text, process Include nodes
fn applyIncludes(allocator: Allocator, ast: *Node) LinkerError!*Node {
const IncludeContext = struct {
alloc: Allocator,
fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
_ = self;
// Convert RawInclude to Text
if (node.type == .RawInclude) {
// In a real implementation:
// - Get file.str (the loaded file content)
// - Create a Text node with that content
// For now, just keep the node as-is
node.type = .Text;
// node.val = file.str with \r removed
}
return null;
}
fn after(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
_ = self;
// Process Include nodes
if (node.type == .Include) {
// In a real implementation:
// 1. Link the included file's AST
// 2. If it has extends, remove named blocks
// 3. Apply yield block
// For now, keep the node as-is
}
return null;
}
};
var include_ctx = IncludeContext{
.alloc = allocator,
};
var walk_options = WalkOptions{};
defer walk_options.deinit(allocator);
const result = walk_mod.walkASTWithUserData(
allocator,
ast,
IncludeContext.before,
IncludeContext.after,
&walk_options,
&include_ctx,
) catch {
return error.WalkError;
};
return result;
}
/// Check that extends is the first thing in the file
fn checkExtendsPosition(allocator: Allocator, ast: *Node) LinkerError!void {
var found_legit_extends = false;
const CheckContext = struct {
legit_extends: *bool,
has_extends: bool,
alloc: Allocator,
fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
if (node.type == .Extends) {
if (self.has_extends and !self.legit_extends.*) {
self.legit_extends.* = true;
} else {
// This would be an error - extends not first or multiple extends
// For now we just skip
}
}
return null;
}
};
var check_ctx = CheckContext{
.legit_extends = &found_legit_extends,
.has_extends = true,
.alloc = allocator,
};
var walk_options = WalkOptions{};
defer walk_options.deinit(allocator);
_ = walk_mod.walkASTWithUserData(
allocator,
ast,
CheckContext.before,
null,
&walk_options,
&check_ctx,
) catch {
return error.WalkError;
};
}
/// Remove named blocks (convert to regular blocks)
pub fn removeNamedBlocks(allocator: Allocator, ast: *Node) LinkerError!*Node {
const RemoveContext = struct {
alloc: Allocator,
fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
_ = self;
if (node.type == .NamedBlock) {
node.type = .Block;
node.name = null;
node.mode = null;
}
return null;
}
};
var remove_ctx = RemoveContext{
.alloc = allocator,
};
var walk_options = WalkOptions{};
defer walk_options.deinit(allocator);
return walk_mod.walkASTWithUserData(
allocator,
ast,
RemoveContext.before,
null,
&walk_options,
&remove_ctx,
) catch error.WalkError;
}
/// Apply yield block to included content
pub fn applyYield(allocator: Allocator, ast: *Node, block: ?*Node) LinkerError!*Node {
if (block == null or block.?.nodes.items.len == 0) {
return ast;
}
var replaced = false;
const YieldContext = struct {
yield_block: *Node,
was_replaced: *bool,
alloc: Allocator,
fn after(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
if (node.type == .YieldBlock) {
self.was_replaced.* = true;
node.type = .Block;
node.nodes.clearRetainingCapacity();
node.nodes.append(self.alloc, self.yield_block) catch return error.OutOfMemory;
}
return null;
}
};
var yield_ctx = YieldContext{
.yield_block = block.?,
.was_replaced = &replaced,
.alloc = allocator,
};
var walk_options = WalkOptions{};
defer walk_options.deinit(allocator);
const result = walk_mod.walkASTWithUserData(
allocator,
ast,
null,
YieldContext.after,
&walk_options,
&yield_ctx,
) catch {
return error.WalkError;
};
// If no yield block found, append to default location
if (!replaced) {
const default_loc = findDefaultYieldLocation(result);
default_loc.nodes.append(allocator, block.?) catch return error.OutOfMemory;
}
return result;
}
/// Find the default yield location (deepest block)
fn findDefaultYieldLocation(node: *Node) *Node {
var result = node;
for (node.nodes.items) |child| {
if (child.text_only) continue;
if (child.type == .Block) {
result = findDefaultYieldLocation(child);
} else if (child.nodes.items.len > 0) {
result = findDefaultYieldLocation(child);
}
}
return result;
}
// ============================================================================
// Tests
// ============================================================================
test "link - basic block" {
const allocator = std.testing.allocator;
// Create a simple AST
const text_node = try allocator.create(Node);
text_node.* = Node{
.type = .Text,
.val = "Hello",
.line = 1,
.column = 1,
};
var root = try allocator.create(Node);
root.* = Node{
.type = .Block,
.line = 1,
.column = 1,
};
try root.nodes.append(allocator, text_node);
defer {
root.deinit(allocator);
allocator.destroy(root);
}
var result = try link(allocator, root);
defer result.deinit(allocator);
try std.testing.expectEqual(root, result.ast);
try std.testing.expectEqual(false, result.has_extends);
}
test "link - with named block" {
const allocator = std.testing.allocator;
// Create named block
const text_node = try allocator.create(Node);
text_node.* = Node{
.type = .Text,
.val = "content",
.line = 2,
.column = 3,
};
const named_block = try allocator.create(Node);
named_block.* = Node{
.type = .NamedBlock,
.name = "content",
.mode = "replace",
.line = 2,
.column = 1,
};
try named_block.nodes.append(allocator, text_node);
var root = try allocator.create(Node);
root.* = Node{
.type = .Block,
.line = 1,
.column = 1,
};
try root.nodes.append(allocator, named_block);
defer {
root.deinit(allocator);
allocator.destroy(root);
}
var result = try link(allocator, root);
defer result.deinit(allocator);
// Should find the declared block
try std.testing.expect(result.declared_blocks.contains("content"));
}
test "findDeclaredBlocks - multiple blocks" {
const allocator = std.testing.allocator;
const block1 = try allocator.create(Node);
block1.* = Node{
.type = .NamedBlock,
.name = "header",
.mode = "replace",
.line = 1,
.column = 1,
};
const block2 = try allocator.create(Node);
block2.* = Node{
.type = .NamedBlock,
.name = "footer",
.mode = "replace",
.line = 5,
.column = 1,
};
const block3 = try allocator.create(Node);
block3.* = Node{
.type = .NamedBlock,
.name = "sidebar",
.mode = "append", // Should not be in declared blocks
.line = 10,
.column = 1,
};
var root = try allocator.create(Node);
root.* = Node{
.type = .Block,
.line = 1,
.column = 1,
};
try root.nodes.append(allocator, block1);
try root.nodes.append(allocator, block2);
try root.nodes.append(allocator, block3);
defer {
root.deinit(allocator);
allocator.destroy(root);
}
var blocks = try findDeclaredBlocks(allocator, root);
defer blocks.deinit(allocator);
try std.testing.expect(blocks.contains("header"));
try std.testing.expect(blocks.contains("footer"));
try std.testing.expect(!blocks.contains("sidebar")); // append mode
}
test "removeNamedBlocks" {
const allocator = std.testing.allocator;
const named_block = try allocator.create(Node);
named_block.* = Node{
.type = .NamedBlock,
.name = "content",
.mode = "replace",
.line = 1,
.column = 1,
};
var root = try allocator.create(Node);
root.* = Node{
.type = .Block,
.line = 1,
.column = 1,
};
try root.nodes.append(allocator, named_block);
defer {
root.deinit(allocator);
allocator.destroy(root);
}
const result = try removeNamedBlocks(allocator, root);
// Named block should now be a regular Block
try std.testing.expectEqual(NodeType.Block, result.nodes.items[0].type);
try std.testing.expectEqual(@as(?[]const u8, null), result.nodes.items[0].name);
}
test "findDefaultYieldLocation - nested blocks" {
const allocator = std.testing.allocator;
const inner_block = try allocator.create(Node);
inner_block.* = Node{
.type = .Block,
.line = 3,
.column = 1,
};
const outer_block = try allocator.create(Node);
outer_block.* = Node{
.type = .Block,
.line = 2,
.column = 1,
};
try outer_block.nodes.append(allocator, inner_block);
var root = try allocator.create(Node);
root.* = Node{
.type = .Block,
.line = 1,
.column = 1,
};
try root.nodes.append(allocator, outer_block);
defer {
root.deinit(allocator);
allocator.destroy(root);
}
const location = findDefaultYieldLocation(root);
// Should find the innermost block
try std.testing.expectEqual(inner_block, location);
}

412
src/load.zig Normal file
View File

@@ -0,0 +1,412 @@
// load.zig - Zig port of pug-load
//
// Handles loading of include/extends files during AST processing.
// Walks the AST and loads file dependencies.
const std = @import("std");
const fs = std.fs;
const Allocator = std.mem.Allocator;
const mem = std.mem;
// Import AST types from parser
const parser = @import("parser.zig");
pub const Node = parser.Node;
pub const NodeType = parser.NodeType;
pub const FileReference = parser.FileReference;
// Import walk module
const walk_mod = @import("walk.zig");
pub const walkAST = walk_mod.walkAST;
pub const WalkOptions = walk_mod.WalkOptions;
pub const WalkContext = walk_mod.WalkContext;
pub const WalkError = walk_mod.WalkError;
pub const ReplaceResult = walk_mod.ReplaceResult;
// Import lexer for lexing includes
const lexer = @import("lexer.zig");
pub const Token = lexer.Token;
pub const Lexer = lexer.Lexer;
// Import error types
const pug_error = @import("error.zig");
pub const PugError = pug_error.PugError;
// ============================================================================
// Load Options
// ============================================================================
/// Function type for resolving file paths
pub const ResolveFn = *const fn (
filename: []const u8,
source: ?[]const u8,
options: *const LoadOptions,
) LoadError![]const u8;
/// Function type for reading file contents
pub const ReadFn = *const fn (
allocator: Allocator,
filename: []const u8,
options: *const LoadOptions,
) LoadError![]const u8;
/// Function type for lexing source
pub const LexFn = *const fn (
allocator: Allocator,
src: []const u8,
options: *const LoadOptions,
) LoadError![]const Token;
/// Function type for parsing tokens
pub const ParseFn = *const fn (
allocator: Allocator,
tokens: []const Token,
options: *const LoadOptions,
) LoadError!*Node;
pub const LoadOptions = struct {
/// Base directory for absolute paths
basedir: ?[]const u8 = null,
/// Source filename
filename: ?[]const u8 = null,
/// Source content
src: ?[]const u8 = null,
/// Path resolution function
resolve: ?ResolveFn = null,
/// File reading function
read: ?ReadFn = null,
/// Lexer function
lex: ?LexFn = null,
/// Parser function
parse: ?ParseFn = null,
/// User data for callbacks
user_data: ?*anyopaque = null,
};
// ============================================================================
// Load Errors
// ============================================================================
pub const LoadError = error{
OutOfMemory,
FileNotFound,
AccessDenied,
InvalidPath,
MissingFilename,
MissingBasedir,
InvalidFileReference,
LexError,
ParseError,
WalkError,
InvalidUtf8,
};
// ============================================================================
// Load Result
// ============================================================================
pub const LoadResult = struct {
ast: *Node,
err: ?PugError = null,
pub fn deinit(self: *LoadResult, allocator: Allocator) void {
if (self.err) |*e| {
e.deinit();
}
self.ast.deinit(allocator);
allocator.destroy(self.ast);
}
};
// ============================================================================
// Default Implementations
// ============================================================================
/// Default path resolution - handles relative and absolute paths
pub fn defaultResolve(
filename: []const u8,
source: ?[]const u8,
options: *const LoadOptions,
) LoadError![]const u8 {
const trimmed = mem.trim(u8, filename, " \t\r\n");
if (trimmed.len == 0) {
return error.InvalidPath;
}
// Absolute path (starts with /)
if (trimmed[0] == '/') {
if (options.basedir == null) {
return error.MissingBasedir;
}
// Join basedir with filename (without leading /)
// Note: In a real implementation, we'd use path.join
// For now, return the path as-is for testing
return trimmed;
}
// Relative path
if (source == null) {
return error.MissingFilename;
}
// In a real implementation, join dirname(source) with filename
// For now, return the path as-is for testing
return trimmed;
}
/// Default file reading using std.fs
pub fn defaultRead(
allocator: Allocator,
filename: []const u8,
options: *const LoadOptions,
) LoadError![]const u8 {
_ = options;
const file = fs.cwd().openFile(filename, .{}) catch |err| {
return switch (err) {
error.FileNotFound => error.FileNotFound,
error.AccessDenied => error.AccessDenied,
else => error.FileNotFound,
};
};
defer file.close();
const content = file.readToEndAlloc(allocator, 1024 * 1024 * 10) catch {
return error.OutOfMemory;
};
return content;
}
// ============================================================================
// Load Implementation
// ============================================================================
/// Load file dependencies from an AST
/// Walks the AST and loads Include, RawInclude, and Extends nodes
pub fn load(
allocator: Allocator,
ast: *Node,
options: LoadOptions,
) LoadError!*Node {
// Create a context for the walk
const LoadContext = struct {
allocator: Allocator,
options: LoadOptions,
err: ?PugError = null,
fn beforeCallback(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
// Only process Include, RawInclude, and Extends nodes
if (node.type != .Include and node.type != .RawInclude and node.type != .Extends) {
return null;
}
// Check if already loaded (str is set)
if (node.file) |*file| {
// Load the file content
self.loadFileReference(file, node) catch {
// Store error but continue walking
return null;
};
}
return null;
}
fn loadFileReference(self: *@This(), file: *FileReference, node: *Node) LoadError!void {
_ = node;
if (file.path == null) {
return error.InvalidFileReference;
}
// Resolve the path
const resolve_fn = self.options.resolve orelse defaultResolve;
const resolved_path = try resolve_fn(file.path.?, self.options.filename, &self.options);
// Read the file
const read_fn = self.options.read orelse defaultRead;
const content = try read_fn(self.allocator, resolved_path, &self.options);
_ = content;
// For Include/Extends, parse the content into an AST
// This would require lexer and parser functions to be provided
// For now, we just load the raw content
}
};
var load_ctx = LoadContext{
.allocator = allocator,
.options = options,
};
var walk_options = WalkOptions{};
defer walk_options.deinit(allocator);
const result = walk_mod.walkASTWithUserData(
allocator,
ast,
LoadContext.beforeCallback,
null,
&walk_options,
&load_ctx,
) catch {
return error.WalkError;
};
if (load_ctx.err) |*e| {
e.deinit();
return error.FileNotFound;
}
return result;
}
/// Load from a string source
pub fn loadString(
allocator: Allocator,
src: []const u8,
options: LoadOptions,
) LoadError!*Node {
// Need lex and parse functions
const lex_fn = options.lex orelse return error.LexError;
const parse_fn = options.parse orelse return error.ParseError;
// Lex the source
const tokens = try lex_fn(allocator, src, &options);
// Parse the tokens
var parse_options = options;
parse_options.src = src;
const ast = try parse_fn(allocator, tokens, &parse_options);
// Load dependencies
return load(allocator, ast, parse_options);
}
/// Load from a file
pub fn loadFile(
allocator: Allocator,
filename: []const u8,
options: LoadOptions,
) LoadError!*Node {
// Read the file
const read_fn = options.read orelse defaultRead;
const content = try read_fn(allocator, filename, &options);
defer allocator.free(content);
// Load from string with filename set
var file_options = options;
file_options.filename = filename;
return loadString(allocator, content, file_options);
}
// ============================================================================
// Path Utilities
// ============================================================================
/// Get the directory name from a path
pub fn dirname(path: []const u8) []const u8 {
if (mem.lastIndexOf(u8, path, "/")) |idx| {
if (idx == 0) return "/";
return path[0..idx];
}
return ".";
}
/// Join two path components
pub fn pathJoin(allocator: Allocator, base: []const u8, relative: []const u8) ![]const u8 {
if (relative.len > 0 and relative[0] == '/') {
return allocator.dupe(u8, relative);
}
const base_dir = dirname(base);
// Handle .. and . components
var result = std.ArrayListUnmanaged(u8){};
errdefer result.deinit(allocator);
try result.appendSlice(allocator, base_dir);
if (base_dir.len > 0 and base_dir[base_dir.len - 1] != '/') {
try result.append(allocator, '/');
}
try result.appendSlice(allocator, relative);
return result.toOwnedSlice(allocator);
}
// ============================================================================
// Tests
// ============================================================================
test "dirname - basic paths" {
try std.testing.expectEqualStrings(".", dirname("file.pug"));
try std.testing.expectEqualStrings("/home/user", dirname("/home/user/file.pug"));
try std.testing.expectEqualStrings("views", dirname("views/file.pug"));
try std.testing.expectEqualStrings("/", dirname("/file.pug"));
try std.testing.expectEqualStrings(".", dirname(""));
}
test "pathJoin - relative paths" {
const allocator = std.testing.allocator;
const result1 = try pathJoin(allocator, "/home/user/views/index.pug", "partials/header.pug");
defer allocator.free(result1);
try std.testing.expectEqualStrings("/home/user/views/partials/header.pug", result1);
const result2 = try pathJoin(allocator, "views/index.pug", "footer.pug");
defer allocator.free(result2);
try std.testing.expectEqualStrings("views/footer.pug", result2);
}
test "pathJoin - absolute paths" {
const allocator = std.testing.allocator;
const result = try pathJoin(allocator, "/home/user/views/index.pug", "/absolute/path.pug");
defer allocator.free(result);
try std.testing.expectEqualStrings("/absolute/path.pug", result);
}
test "defaultResolve - missing basedir for absolute path" {
const options = LoadOptions{};
const result = defaultResolve("/absolute/path.pug", null, &options);
try std.testing.expectError(error.MissingBasedir, result);
}
test "defaultResolve - missing filename for relative path" {
const options = LoadOptions{ .basedir = "/base" };
const result = defaultResolve("relative/path.pug", null, &options);
try std.testing.expectError(error.MissingFilename, result);
}
test "load - basic AST without includes" {
const allocator = std.testing.allocator;
// Create a simple AST with no includes
const text_node = try allocator.create(Node);
text_node.* = Node{
.type = .Text,
.val = "Hello",
.line = 1,
.column = 1,
};
var root = try allocator.create(Node);
root.* = Node{
.type = .Block,
.line = 1,
.column = 1,
};
try root.nodes.append(allocator, text_node);
defer {
root.deinit(allocator);
allocator.destroy(root);
}
// Load should succeed with no changes
const result = try load(allocator, root, .{});
try std.testing.expectEqual(root, result);
}

581
src/mixin.zig Normal file
View File

@@ -0,0 +1,581 @@
// mixin.zig - Mixin registry and expansion
//
// Handles mixin definitions and calls:
// - Collects mixin definitions from AST into a registry
// - Expands mixin calls by substituting arguments and block content
//
// Usage pattern in Pug:
// mixin button(text, type)
// button(class="btn btn-" + type)= text
//
// +button("Click", "primary")
//
// Include pattern:
// include mixins/_buttons.pug
// +primary-button("Click")
const std = @import("std");
const Allocator = std.mem.Allocator;
const mem = std.mem;
const parser = @import("parser.zig");
pub const Node = parser.Node;
pub const NodeType = parser.NodeType;
// ============================================================================
// Mixin Registry
// ============================================================================
/// Registry for mixin definitions
pub const MixinRegistry = struct {
allocator: Allocator,
mixins: std.StringHashMapUnmanaged(*Node),
pub fn init(allocator: Allocator) MixinRegistry {
return .{
.allocator = allocator,
.mixins = .{},
};
}
pub fn deinit(self: *MixinRegistry) void {
self.mixins.deinit(self.allocator);
}
/// Register a mixin definition
pub fn register(self: *MixinRegistry, name: []const u8, node: *Node) !void {
try self.mixins.put(self.allocator, name, node);
}
/// Get a mixin definition by name
pub fn get(self: *const MixinRegistry, name: []const u8) ?*Node {
return self.mixins.get(name);
}
/// Check if a mixin exists
pub fn contains(self: *const MixinRegistry, name: []const u8) bool {
return self.mixins.contains(name);
}
};
// ============================================================================
// Mixin Collector - Collect definitions from AST
// ============================================================================
/// Collect all mixin definitions from an AST into the registry
pub fn collectMixins(allocator: Allocator, ast: *Node, registry: *MixinRegistry) !void {
try collectMixinsFromNode(allocator, ast, registry);
}
fn collectMixinsFromNode(allocator: Allocator, node: *Node, registry: *MixinRegistry) !void {
// If this is a mixin definition (not a call), register it
if (node.type == .Mixin and !node.call) {
if (node.name) |name| {
try registry.register(name, node);
}
}
// Recurse into children
for (node.nodes.items) |child| {
try collectMixinsFromNode(allocator, child, registry);
}
}
// ============================================================================
// Mixin Expander - Expand mixin calls in AST
// ============================================================================
/// Error types for mixin expansion
pub const MixinError = error{
OutOfMemory,
MixinNotFound,
InvalidMixinCall,
};
/// Expand all mixin calls in an AST using the registry
/// Returns a new AST with mixin calls replaced by their expanded content
pub fn expandMixins(allocator: Allocator, ast: *Node, registry: *const MixinRegistry) MixinError!*Node {
return expandNode(allocator, ast, registry, null);
}
fn expandNode(
allocator: Allocator,
node: *Node,
registry: *const MixinRegistry,
caller_block: ?*Node,
) MixinError!*Node {
// Handle mixin call
if (node.type == .Mixin and node.call) {
return expandMixinCall(allocator, node, registry, caller_block);
}
// Handle MixinBlock - replace with caller's block content
if (node.type == .MixinBlock) {
if (caller_block) |block| {
// Clone the caller's block
return cloneNode(allocator, block);
} else {
// No block provided, return empty block
const empty = allocator.create(Node) catch return error.OutOfMemory;
empty.* = Node{
.type = .Block,
.line = node.line,
.column = node.column,
};
return empty;
}
}
// For other nodes, clone and recurse into children
const new_node = allocator.create(Node) catch return error.OutOfMemory;
new_node.* = node.*;
new_node.nodes = .{};
// Clone and expand children
for (node.nodes.items) |child| {
const expanded_child = try expandNode(allocator, child, registry, caller_block);
new_node.nodes.append(allocator, expanded_child) catch return error.OutOfMemory;
}
return new_node;
}
fn expandMixinCall(
allocator: Allocator,
call_node: *Node,
registry: *const MixinRegistry,
_: ?*Node,
) MixinError!*Node {
const mixin_name = call_node.name orelse return error.InvalidMixinCall;
// Look up mixin definition
const mixin_def = registry.get(mixin_name) orelse {
// Mixin not found - return a comment node indicating the error
const error_node = allocator.create(Node) catch return error.OutOfMemory;
error_node.* = Node{
.type = .Comment,
.val = mixin_name,
.buffer = true,
.line = call_node.line,
.column = call_node.column,
};
return error_node;
};
// Get the block content from the call (if any)
var call_block: ?*Node = null;
if (call_node.nodes.items.len > 0) {
// Create a block node containing the call's children
const block = allocator.create(Node) catch return error.OutOfMemory;
block.* = Node{
.type = .Block,
.line = call_node.line,
.column = call_node.column,
};
for (call_node.nodes.items) |child| {
const cloned = try cloneNode(allocator, child);
block.nodes.append(allocator, cloned) catch return error.OutOfMemory;
}
call_block = block;
}
// Create argument bindings
var arg_bindings = std.StringHashMapUnmanaged([]const u8){};
defer arg_bindings.deinit(allocator);
// Bind call arguments to mixin parameters
if (mixin_def.args) |params| {
if (call_node.args) |args| {
try bindArguments(allocator, params, args, &arg_bindings);
}
}
// Clone and expand the mixin body
const result = allocator.create(Node) catch return error.OutOfMemory;
result.* = Node{
.type = .Block,
.line = call_node.line,
.column = call_node.column,
};
// Expand each node in the mixin definition's body
for (mixin_def.nodes.items) |child| {
const expanded = try expandNodeWithArgs(allocator, child, registry, call_block, &arg_bindings);
result.nodes.append(allocator, expanded) catch return error.OutOfMemory;
}
return result;
}
fn expandNodeWithArgs(
allocator: Allocator,
node: *Node,
registry: *const MixinRegistry,
caller_block: ?*Node,
arg_bindings: *const std.StringHashMapUnmanaged([]const u8),
) MixinError!*Node {
// Handle mixin call (nested)
if (node.type == .Mixin and node.call) {
return expandMixinCall(allocator, node, registry, caller_block);
}
// Handle MixinBlock - replace with caller's block content
if (node.type == .MixinBlock) {
if (caller_block) |block| {
return cloneNode(allocator, block);
} else {
const empty = allocator.create(Node) catch return error.OutOfMemory;
empty.* = Node{
.type = .Block,
.line = node.line,
.column = node.column,
};
return empty;
}
}
// Clone the node
const new_node = allocator.create(Node) catch return error.OutOfMemory;
new_node.* = node.*;
new_node.nodes = .{};
new_node.attrs = .{};
// Substitute argument references in text/val
if (node.val) |val| {
new_node.val = try substituteArgs(allocator, val, arg_bindings);
}
// Clone attributes with argument substitution
for (node.attrs.items) |attr| {
var new_attr = attr;
if (attr.val) |val| {
new_attr.val = try substituteArgs(allocator, val, arg_bindings);
}
new_node.attrs.append(allocator, new_attr) catch return error.OutOfMemory;
}
// Recurse into children
for (node.nodes.items) |child| {
const expanded = try expandNodeWithArgs(allocator, child, registry, caller_block, arg_bindings);
new_node.nodes.append(allocator, expanded) catch return error.OutOfMemory;
}
return new_node;
}
/// Substitute argument references in a string and evaluate simple expressions
fn substituteArgs(
allocator: Allocator,
text: []const u8,
bindings: *const std.StringHashMapUnmanaged([]const u8),
) MixinError![]const u8 {
// Quick check - if no bindings or text doesn't contain any param names, return as-is
if (bindings.count() == 0) {
return text;
}
// Check if any substitution is needed
var needs_substitution = false;
var iter = bindings.iterator();
while (iter.next()) |entry| {
if (mem.indexOf(u8, text, entry.key_ptr.*) != null) {
needs_substitution = true;
break;
}
}
if (!needs_substitution) {
return text;
}
// Perform substitution
var result = std.ArrayListUnmanaged(u8){};
errdefer result.deinit(allocator);
var i: usize = 0;
while (i < text.len) {
var found_match = false;
// Check for parameter match at current position
var iter2 = bindings.iterator();
while (iter2.next()) |entry| {
const param = entry.key_ptr.*;
const value = entry.value_ptr.*;
if (i + param.len <= text.len and mem.eql(u8, text[i .. i + param.len], param)) {
// Check it's a word boundary (not part of a larger identifier)
const before_ok = i == 0 or !isIdentChar(text[i - 1]);
const after_ok = i + param.len >= text.len or !isIdentChar(text[i + param.len]);
if (before_ok and after_ok) {
result.appendSlice(allocator, value) catch return error.OutOfMemory;
i += param.len;
found_match = true;
break;
}
}
}
if (!found_match) {
result.append(allocator, text[i]) catch return error.OutOfMemory;
i += 1;
}
}
const substituted = result.toOwnedSlice(allocator) catch return error.OutOfMemory;
// Evaluate string concatenation expressions like "btn btn-" + "primary"
return evaluateStringConcat(allocator, substituted) catch return error.OutOfMemory;
}
/// Evaluate simple string concatenation expressions
/// Handles: "btn btn-" + primary -> "btn btn-primary"
/// Also handles: "btn btn-" + "primary" -> "btn btn-primary"
fn evaluateStringConcat(allocator: Allocator, expr: []const u8) ![]const u8 {
// Check if there's a + operator (string concat)
_ = mem.indexOf(u8, expr, " + ") orelse return expr;
var result = std.ArrayListUnmanaged(u8){};
errdefer result.deinit(allocator);
var remaining = expr;
var is_first_part = true;
while (remaining.len > 0) {
const next_plus = mem.indexOf(u8, remaining, " + ");
const part = if (next_plus) |pos| remaining[0..pos] else remaining;
// Extract string value (strip quotes and whitespace)
const stripped = mem.trim(u8, part, " \t");
const unquoted = stripQuotes(stripped);
// For the first part, we might want to keep it quoted in the final output
// For subsequent parts, just append the value
if (is_first_part) {
// If the first part is a quoted string, we'll build an unquoted result
result.appendSlice(allocator, unquoted) catch return error.OutOfMemory;
is_first_part = false;
} else {
result.appendSlice(allocator, unquoted) catch return error.OutOfMemory;
}
if (next_plus) |pos| {
remaining = remaining[pos + 3 ..]; // Skip " + "
} else {
break;
}
}
// Free original and return concatenated result
allocator.free(expr);
return result.toOwnedSlice(allocator);
}
fn isIdentChar(c: u8) bool {
return (c >= 'a' and c <= 'z') or
(c >= 'A' and c <= 'Z') or
(c >= '0' and c <= '9') or
c == '_' or c == '-';
}
/// Bind call arguments to mixin parameters
fn bindArguments(
allocator: Allocator,
params: []const u8,
args: []const u8,
bindings: *std.StringHashMapUnmanaged([]const u8),
) MixinError!void {
// Parse parameter names from definition: "text, type" or "text, type='primary'"
var param_names = std.ArrayListUnmanaged([]const u8){};
defer param_names.deinit(allocator);
var param_iter = mem.splitSequence(u8, params, ",");
while (param_iter.next()) |param_part| {
const trimmed = mem.trim(u8, param_part, " \t");
if (trimmed.len == 0) continue;
// Handle default values: "type='primary'" -> just get "type"
var param_name = trimmed;
if (mem.indexOf(u8, trimmed, "=")) |eq_pos| {
param_name = mem.trim(u8, trimmed[0..eq_pos], " \t");
}
// Handle rest args: "...items" -> "items"
if (mem.startsWith(u8, param_name, "...")) {
param_name = param_name[3..];
}
param_names.append(allocator, param_name) catch return error.OutOfMemory;
}
// Parse argument values from call: "'Click', 'primary'" or "text='Click'"
var arg_values = std.ArrayListUnmanaged([]const u8){};
defer arg_values.deinit(allocator);
// Simple argument parsing - split by comma but respect quotes
var in_string = false;
var string_char: u8 = 0;
var paren_depth: usize = 0;
var start: usize = 0;
for (args, 0..) |c, idx| {
if (!in_string) {
if (c == '"' or c == '\'') {
in_string = true;
string_char = c;
} else if (c == '(') {
paren_depth += 1;
} else if (c == ')') {
if (paren_depth > 0) paren_depth -= 1;
} else if (c == ',' and paren_depth == 0) {
const arg_val = mem.trim(u8, args[start..idx], " \t");
arg_values.append(allocator, stripQuotes(arg_val)) catch return error.OutOfMemory;
start = idx + 1;
}
} else {
if (c == string_char) {
in_string = false;
}
}
}
// Add last argument
if (start < args.len) {
const arg_val = mem.trim(u8, args[start..], " \t");
if (arg_val.len > 0) {
arg_values.append(allocator, stripQuotes(arg_val)) catch return error.OutOfMemory;
}
}
// Bind positional arguments
const min_len = @min(param_names.items.len, arg_values.items.len);
for (0..min_len) |i| {
bindings.put(allocator, param_names.items[i], arg_values.items[i]) catch return error.OutOfMemory;
}
}
fn stripQuotes(val: []const u8) []const u8 {
if (val.len < 2) return val;
const first = val[0];
const last = val[val.len - 1];
if ((first == '"' and last == '"') or (first == '\'' and last == '\'')) {
return val[1 .. val.len - 1];
}
return val;
}
/// Clone a node and all its children
fn cloneNode(allocator: Allocator, node: *Node) MixinError!*Node {
const new_node = allocator.create(Node) catch return error.OutOfMemory;
new_node.* = node.*;
new_node.nodes = .{};
new_node.attrs = .{};
// Clone attributes
for (node.attrs.items) |attr| {
new_node.attrs.append(allocator, attr) catch return error.OutOfMemory;
}
// Clone children recursively
for (node.nodes.items) |child| {
const cloned_child = try cloneNode(allocator, child);
new_node.nodes.append(allocator, cloned_child) catch return error.OutOfMemory;
}
return new_node;
}
// ============================================================================
// Tests
// ============================================================================
test "MixinRegistry - basic operations" {
const allocator = std.testing.allocator;
var registry = MixinRegistry.init(allocator);
defer registry.deinit();
// Create a mock mixin node
var mixin_node = Node{
.type = .Mixin,
.name = "button",
.line = 1,
.column = 1,
};
try registry.register("button", &mixin_node);
try std.testing.expect(registry.contains("button"));
try std.testing.expect(!registry.contains("nonexistent"));
const retrieved = registry.get("button");
try std.testing.expect(retrieved != null);
try std.testing.expectEqualStrings("button", retrieved.?.name.?);
}
test "bindArguments - simple positional" {
const allocator = std.testing.allocator;
var bindings = std.StringHashMapUnmanaged([]const u8){};
defer bindings.deinit(allocator);
try bindArguments(allocator, "text, type", "'Click', 'primary'", &bindings);
try std.testing.expectEqualStrings("Click", bindings.get("text").?);
try std.testing.expectEqualStrings("primary", bindings.get("type").?);
}
test "substituteArgs - basic substitution" {
const allocator = std.testing.allocator;
var bindings = std.StringHashMapUnmanaged([]const u8){};
defer bindings.deinit(allocator);
bindings.put(allocator, "title", "Hello") catch unreachable;
bindings.put(allocator, "name", "World") catch unreachable;
const result = try substituteArgs(allocator, "title is title and name is name", &bindings);
defer allocator.free(result);
try std.testing.expectEqualStrings("Hello is Hello and World is World", result);
}
test "stripQuotes" {
try std.testing.expectEqualStrings("hello", stripQuotes("'hello'"));
try std.testing.expectEqualStrings("hello", stripQuotes("\"hello\""));
try std.testing.expectEqualStrings("hello", stripQuotes("hello"));
try std.testing.expectEqualStrings("", stripQuotes("''"));
}
test "substituteArgs - string concatenation expression" {
const allocator = std.testing.allocator;
var bindings = std.StringHashMapUnmanaged([]const u8){};
defer bindings.deinit(allocator);
try bindings.put(allocator, "type", "primary");
// Test the exact format that comes from the parser
const input = "\"btn btn-\" + type";
const result = try substituteArgs(allocator, input, &bindings);
defer allocator.free(result);
// After substitution and concatenation evaluation, should be: btn btn-primary
try std.testing.expectEqualStrings("btn btn-primary", result);
}
test "evaluateStringConcat - basic" {
const allocator = std.testing.allocator;
// Test with quoted + unquoted
const input1 = try allocator.dupe(u8, "\"btn btn-\" + primary");
const result1 = try evaluateStringConcat(allocator, input1);
defer allocator.free(result1);
try std.testing.expectEqualStrings("btn btn-primary", result1);
// Test with both quoted
const input2 = try allocator.dupe(u8, "\"btn btn-\" + \"primary\"");
const result2 = try evaluateStringConcat(allocator, input2);
defer allocator.free(result2);
try std.testing.expectEqualStrings("btn btn-primary", result2);
}

File diff suppressed because it is too large Load Diff

BIN
src/playground/benchmark Executable file

Binary file not shown.

View File

@@ -0,0 +1,66 @@
// benchmark.zig - Benchmark for pugz (Zig Pug implementation)
//
// This benchmark matches the JavaScript pug benchmark for comparison
// Uses exact same templates as packages/pug/support/benchmark.js
const std = @import("std");
const pug = @import("../pug.zig");
const MIN_ITERATIONS: usize = 200;
const MIN_TIME_NS: u64 = 200_000_000; // 200ms minimum
fn benchmark(comptime name: []const u8, template: []const u8, iterations: *usize, elapsed_ns: *u64) !void {
const allocator = std.heap.page_allocator;
// Warmup
for (0..10) |_| {
var result = try pug.compile(allocator, template, .{});
result.deinit(allocator);
}
var timer = try std.time.Timer.start();
var count: usize = 0;
while (count < MIN_ITERATIONS or timer.read() < MIN_TIME_NS) {
var result = try pug.compile(allocator, template, .{});
result.deinit(allocator);
count += 1;
}
const elapsed = timer.read();
iterations.* = count;
elapsed_ns.* = elapsed;
const ops_per_sec = @as(f64, @floatFromInt(count)) * 1_000_000_000.0 / @as(f64, @floatFromInt(elapsed));
std.debug.print("{s}: {d:.0}\n", .{ name, ops_per_sec });
}
pub fn main() !void {
var iterations: usize = 0;
var elapsed_ns: u64 = 0;
// Tiny template - exact match to JS: 'html\n body\n h1 Title'
const tiny = "html\n body\n h1 Title";
try benchmark("tiny", tiny, &iterations, &elapsed_ns);
// Small template - exact match to JS (note trailing \n on each line)
const small =
"html\n" ++
" body\n" ++
" h1 Title\n" ++
" ul#menu\n" ++
" li: a(href=\"#\") Home\n" ++
" li: a(href=\"#\") About Us\n" ++
" li: a(href=\"#\") Store\n" ++
" li: a(href=\"#\") FAQ\n" ++
" li: a(href=\"#\") Contact\n";
try benchmark("small", small, &iterations, &elapsed_ns);
// Medium template - Array(30).join(str) creates 29 copies in JS
const medium = small ** 29;
try benchmark("medium", medium, &iterations, &elapsed_ns);
// Large template - Array(100).join(str) creates 99 copies in JS
const large = small ** 99;
try benchmark("large", large, &iterations, &elapsed_ns);
}

View File

@@ -0,0 +1,274 @@
// benchmark_examples.zig - Benchmark pug example files
//
// Tests the same example files as the JS benchmark
const std = @import("std");
const pug = @import("../pug.zig");
const Example = struct {
name: []const u8,
source: []const u8,
};
// Example templates (matching JS pug examples that don't use includes/extends)
const examples = [_]Example{
.{
.name = "attributes.pug",
.source =
\\div#id.left.container(class='user user-' + name)
\\ h1.title= name
\\ form
\\ //- unbuffered comment :)
\\ // An example of attributes.
\\ input(type='text' name='user[name]' value=name)
\\ input(checked, type='checkbox', name='user[blocked]')
\\ input(type='submit', value='Update')
,
},
.{
.name = "code.pug",
.source =
\\- var title = "Things"
\\
\\-
\\ var subtitle = ["Really", "long",
\\ "list", "of",
\\ "words"]
\\h1= title
\\h2= subtitle.join(" ")
\\
\\ul#users
\\ each user, name in users
\\ // expands to if (user.isA == 'ferret')
\\ if user.isA == 'ferret'
\\ li(class='user-' + name) #{name} is just a ferret
\\ else
\\ li(class='user-' + name) #{name} #{user.email}
,
},
.{
.name = "dynamicscript.pug",
.source =
\\html
\\ head
\\ title Dynamic Inline JavaScript
\\ script.
\\ var users = !{JSON.stringify(users).replace(/<\//g, "<\\/")}
,
},
.{
.name = "each.pug",
.source =
\\ul#users
\\ each user, name in users
\\ li(class='user-' + name) #{name} #{user.email}
,
},
.{
.name = "extend-layout.pug",
.source =
\\html
\\ head
\\ h1 My Site - #{title}
\\ block scripts
\\ script(src='/jquery.js')
\\ body
\\ block content
\\ block foot
\\ #footer
\\ p some footer content
,
},
.{
.name = "form.pug",
.source =
\\form(method="post")
\\ fieldset
\\ legend General
\\ p
\\ label(for="user[name]") Username:
\\ input(type="text", name="user[name]", value=user.name)
\\ p
\\ label(for="user[email]") Email:
\\ input(type="text", name="user[email]", value=user.email)
\\ .tip.
\\ Enter a valid
\\ email address
\\ such as <em>tj@vision-media.ca</em>.
\\ fieldset
\\ legend Location
\\ p
\\ label(for="user[city]") City:
\\ input(type="text", name="user[city]", value=user.city)
\\ p
\\ select(name="user[province]")
\\ option(value="") -- Select Province --
\\ option(value="AB") Alberta
\\ option(value="BC") British Columbia
\\ option(value="SK") Saskatchewan
\\ option(value="MB") Manitoba
\\ option(value="ON") Ontario
\\ option(value="QC") Quebec
\\ p.buttons
\\ input(type="submit", value="Save")
,
},
.{
.name = "layout.pug",
.source =
\\doctype html
\\html(lang="en")
\\ head
\\ title Example
\\ script.
\\ if (foo) {
\\ bar();
\\ }
\\ body
\\ h1 Pug - node template engine
\\ #container
,
},
.{
.name = "pet.pug",
.source =
\\.pet
\\ h2= pet.name
\\ p #{pet.name} is <em>#{pet.age}</em> year(s) old.
,
},
.{
.name = "rss.pug",
.source =
\\doctype xml
\\rss(version='2.0')
\\channel
\\ title RSS Title
\\ description Some description here
\\ link http://google.com
\\ lastBuildDate Mon, 06 Sep 2010 00:01:00 +0000
\\ pubDate Mon, 06 Sep 2009 16:45:00 +0000
\\
\\ each item in items
\\ item
\\ title= item.title
\\ description= item.description
\\ link= item.link
,
},
.{
.name = "text.pug",
.source =
\\| An example of an
\\a(href='#') inline
\\| link.
\\
\\form
\\ label Username:
\\ input(type='text', name='user[name]')
\\ p
\\ | Just an example of some text usage.
\\ | You can have <em>inline</em> html,
\\ | as well as
\\ strong tags
\\ | .
\\
\\ | Interpolation is also supported. The
\\ | username is currently "#{name}".
\\
\\ label Email:
\\ input(type='text', name='user[email]')
\\ p
\\ | Email is currently
\\ em= email
\\ | .
,
},
.{
.name = "whitespace.pug",
.source =
\\- var js = '<script></script>'
\\doctype html
\\html
\\
\\ head
\\ title= "Some " + "JavaScript"
\\ != js
\\
\\
\\
\\ body
,
},
};
pub fn main() !void {
const allocator = std.heap.page_allocator;
std.debug.print("=== Zig Pugz Example Benchmark ===\n\n", .{});
var passed: usize = 0;
var failed: usize = 0;
var total_time_ns: u64 = 0;
var html_outputs: [examples.len]?[]const u8 = undefined;
for (&html_outputs) |*h| h.* = null;
for (examples, 0..) |example, idx| {
const iterations: usize = 100;
var success = false;
var time_ns: u64 = 0;
// Warmup
for (0..5) |_| {
var result = pug.compile(allocator, example.source, .{}) catch continue;
result.deinit(allocator);
}
// Benchmark
var timer = try std.time.Timer.start();
var i: usize = 0;
while (i < iterations) : (i += 1) {
var result = pug.compile(allocator, example.source, .{}) catch break;
if (i == iterations - 1) {
// Keep last HTML for output
html_outputs[idx] = result.html;
} else {
result.deinit(allocator);
}
success = true;
}
time_ns = timer.read();
if (success and i == iterations) {
const time_ms = @as(f64, @floatFromInt(time_ns)) / 1_000_000.0 / @as(f64, @floatFromInt(iterations));
std.debug.print("{s}: OK ({d:.3} ms)\n", .{ example.name, time_ms });
passed += 1;
total_time_ns += time_ns;
} else {
std.debug.print("{s}: FAILED\n", .{example.name});
failed += 1;
}
}
std.debug.print("\n=== Summary ===\n", .{});
std.debug.print("Passed: {d}/{d}\n", .{ passed, examples.len });
std.debug.print("Failed: {d}/{d}\n", .{ failed, examples.len });
if (passed > 0) {
const total_ms = @as(f64, @floatFromInt(total_time_ns)) / 1_000_000.0 / 100.0;
std.debug.print("Total time (successful): {d:.3} ms\n", .{total_ms});
std.debug.print("Average time: {d:.3} ms\n", .{total_ms / @as(f64, @floatFromInt(passed))});
}
// Output HTML for comparison
std.debug.print("\n=== HTML Output ===\n", .{});
for (examples, 0..) |example, idx| {
if (html_outputs[idx]) |html| {
std.debug.print("\n--- {s} ---\n", .{example.name});
const max_len = @min(html.len, 500);
std.debug.print("{s}", .{html[0..max_len]});
if (html.len > 500) std.debug.print("...", .{});
std.debug.print("\n", .{});
}
}
}

View File

@@ -0,0 +1,8 @@
div#id.left.container(class='user user-' + name)
h1.title= name
form
//- unbuffered comment :)
// An example of attributes.
input(type='text' name='user[name]' value=name)
input(checked, type='checkbox', name='user[blocked]')
input(type='submit', value='Update')

View File

@@ -0,0 +1,17 @@
- var title = "Things"
-
var subtitle = ["Really", "long",
"list", "of",
"words"]
h1= title
h2= subtitle.join(" ")
ul#users
each user, name in users
// expands to if (user.isA == 'ferret')
if user.isA == 'ferret'
li(class='user-' + name) #{name} is just a ferret
else
li(class='user-' + name) #{name} #{user.email}

View File

@@ -0,0 +1,5 @@
html
head
title Dynamic Inline JavaScript
script.
var users = !{JSON.stringify(users).replace(/<\//g, "<\\/")}

View File

@@ -0,0 +1,3 @@
ul#users
each user, name in users
li(class='user-' + name) #{name} #{user.email}

View File

@@ -0,0 +1,10 @@
html
head
h1 My Site - #{title}
block scripts
script(src='/jquery.js')
body
block content
block foot
#footer
p some footer content

View File

@@ -0,0 +1,11 @@
extends extend-layout.pug
block scripts
script(src='/jquery.js')
script(src='/pets.js')
block content
h1= title
each pet in pets
include pet.pug

View File

@@ -0,0 +1,29 @@
form(method="post")
fieldset
legend General
p
label(for="user[name]") Username:
input(type="text", name="user[name]", value=user.name)
p
label(for="user[email]") Email:
input(type="text", name="user[email]", value=user.email)
.tip.
Enter a valid
email address
such as <em>tj@vision-media.ca</em>.
fieldset
legend Location
p
label(for="user[city]") City:
input(type="text", name="user[city]", value=user.city)
p
select(name="user[province]")
option(value="") -- Select Province --
option(value="AB") Alberta
option(value="BC") British Columbia
option(value="SK") Saskatchewan
option(value="MB") Manitoba
option(value="ON") Ontario
option(value="QC") Quebec
p.buttons
input(type="submit", value="Save")

View File

@@ -0,0 +1,7 @@
html
include includes/head.pug
body
h1 My Site
p Welcome to my super lame site.
include includes/foot.pug

View File

@@ -0,0 +1,14 @@
doctype html
html(lang="en")
head
title Example
script.
if (foo) {
bar();
}
body
h1 Pug - node template engine
#container
:markdown-it
Pug is a _high performance_ template engine for [node](http://nodejs.org),
inspired by [haml](http://haml-lang.com/), and written by [TJ Holowaychuk](http://github.com/visionmedia).

View File

@@ -0,0 +1,14 @@
include mixins/dialog.pug
include mixins/profile.pug
.one
+dialog
.two
+dialog-title('Whoop')
.three
+dialog-title-desc('Whoop', 'Just a mixin')
#profile
+profile(user)

View File

@@ -0,0 +1,3 @@
.pet
h2= pet.name
p #{pet.name} is <em>#{pet.age}</em> year(s) old.

View File

@@ -0,0 +1,14 @@
doctype xml
rss(version='2.0')
channel
title RSS Title
description Some description here
link http://google.com
lastBuildDate Mon, 06 Sep 2010 00:01:00 +0000
pubDate Mon, 06 Sep 2009 16:45:00 +0000
each item in items
item
title= item.title
description= item.description
link= item.link

View File

@@ -0,0 +1,36 @@
| An example of an
a(href='#') inline
| link.
form
label Username:
input(type='text', name='user[name]')
p
| Just an example of some text usage.
| You can have <em>inline</em> html,
| as well as
strong tags
| .
| Interpolation is also supported. The
| username is currently "#{name}".
label Email:
input(type='text', name='user[email]')
p
| Email is currently
em= email
| .
// alternatively, if we plan on having only
// text or inline-html, we can use a trailing
// "." to let pug know we want to omit pipes
label Username:
input(type='text')
p.
Just an example, like before
however now we can omit those
annoying pipes!.
Wahoo.

View File

@@ -0,0 +1,11 @@
- var js = '<script></script>'
doctype html
html
head
title= "Some " + "JavaScript"
!= js
body

70
src/playground/run_js.js Normal file
View File

@@ -0,0 +1,70 @@
/**
* JS Pug - Process all .pug files in playground folder
*/
const fs = require('fs');
const path = require('path');
const pug = require('../../pug');
const dir = path.join(__dirname, 'examples');
// Get all .pug files
const pugFiles = fs.readdirSync(dir)
.filter(f => f.endsWith('.pug'))
.sort();
console.log('=== JS Pug Playground ===\n');
console.log(`Found ${pugFiles.length} .pug files\n`);
let passed = 0;
let failed = 0;
let totalTimeMs = 0;
for (const file of pugFiles) {
const filePath = path.join(dir, file);
const source = fs.readFileSync(filePath, 'utf8');
const iterations = 100;
let success = false;
let html = '';
let error = '';
let timeMs = 0;
try {
const start = process.hrtime.bigint();
for (let i = 0; i < iterations; i++) {
html = pug.render(source, {
filename: filePath,
basedir: dir
});
}
const end = process.hrtime.bigint();
timeMs = Number(end - start) / 1_000_000 / iterations;
success = true;
passed++;
totalTimeMs += timeMs;
} catch (e) {
error = e.message.split('\n')[0];
failed++;
}
if (success) {
console.log(`${file} (${timeMs.toFixed(3)} ms)`);
// Show first 200 chars of output
const preview = html.replace(/\s+/g, ' ').substring(0, 200);
console.log(`${preview}${html.length > 200 ? '...' : ''}\n`);
} else {
console.log(`${file}`);
console.log(`${error}\n`);
}
}
console.log('=== Summary ===');
console.log(`Passed: ${passed}/${pugFiles.length}`);
console.log(`Failed: ${failed}/${pugFiles.length}`);
if (passed > 0) {
console.log(`Total time: ${totalTimeMs.toFixed(3)} ms`);
console.log(`Average: ${(totalTimeMs / passed).toFixed(3)} ms per file`);
}

0
src/playground/run_zig Executable file
View File

120
src/playground/run_zig.zig Normal file
View File

@@ -0,0 +1,120 @@
// Zig Pugz - Process all .pug files in playground/examples folder
const std = @import("std");
const pug = @import("../pug.zig");
const fs = std.fs;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
std.debug.print("=== Zig Pugz Playground ===\n\n", .{});
// Open the examples directory relative to cwd
var dir = fs.cwd().openDir("playground/examples", .{ .iterate = true }) catch |err| {
// Try from playground directory
dir = fs.cwd().openDir("examples", .{ .iterate = true }) catch {
std.debug.print("Error opening examples directory: {}\n", .{err});
return;
};
};
defer dir.close();
// Collect .pug files
var files = std.ArrayList([]const u8).init(allocator);
defer {
for (files.items) |f| allocator.free(f);
files.deinit();
}
var iter = dir.iterate();
while (try iter.next()) |entry| {
if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".pug")) {
const name = try allocator.dupe(u8, entry.name);
try files.append(name);
}
}
// Sort files
std.mem.sort([]const u8, files.items, {}, struct {
fn lessThan(_: void, a: []const u8, b: []const u8) bool {
return std.mem.lessThan(u8, a, b);
}
}.lessThan);
std.debug.print("Found {d} .pug files\n\n", .{files.items.len});
var passed: usize = 0;
var failed: usize = 0;
var total_time_ns: u64 = 0;
for (files.items) |filename| {
// Read file
const file = dir.openFile(filename, .{}) catch {
std.debug.print("✗ {s}\n → Could not open file\n\n", .{filename});
failed += 1;
continue;
};
defer file.close();
const source = file.readToEndAlloc(allocator, 1024 * 1024) catch {
std.debug.print("✗ {s}\n → Could not read file\n\n", .{filename});
failed += 1;
continue;
};
defer allocator.free(source);
// Benchmark
const iterations: usize = 100;
var success = false;
var last_html: ?[]const u8 = null;
// Warmup
for (0..5) |_| {
var result = pug.compile(allocator, source, .{}) catch continue;
result.deinit(allocator);
}
var timer = try std.time.Timer.start();
var i: usize = 0;
while (i < iterations) : (i += 1) {
var result = pug.compile(allocator, source, .{}) catch break;
if (i == iterations - 1) {
last_html = result.html;
} else {
result.deinit(allocator);
}
success = true;
}
const elapsed_ns = timer.read();
if (success and i == iterations) {
const time_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0 / @as(f64, @floatFromInt(iterations));
std.debug.print("✓ {s} ({d:.3} ms)\n", .{ filename, time_ms });
// Show preview
if (last_html) |html| {
const max_len = @min(html.len, 200);
std.debug.print(" → {s}{s}\n\n", .{ html[0..max_len], if (html.len > 200) "..." else "" });
allocator.free(html);
}
passed += 1;
total_time_ns += elapsed_ns;
} else {
std.debug.print("✗ {s}\n → Compilation failed\n\n", .{filename});
failed += 1;
}
}
std.debug.print("=== Summary ===\n", .{});
std.debug.print("Passed: {d}/{d}\n", .{ passed, files.items.len });
std.debug.print("Failed: {d}/{d}\n", .{ failed, files.items.len });
if (passed > 0) {
const total_ms = @as(f64, @floatFromInt(total_time_ns)) / 1_000_000.0 / 100.0;
std.debug.print("Total time: {d:.3} ms\n", .{total_ms});
std.debug.print("Average: {d:.3} ms per file\n", .{total_ms / @as(f64, @floatFromInt(passed))});
}
}

457
src/pug.zig Normal file
View File

@@ -0,0 +1,457 @@
// pug.zig - Main entry point for Pug template engine in Zig
//
// This is the main module that ties together all the Pug compilation stages:
// 1. Lexer - tokenizes the source
// 2. Parser - builds the AST
// 3. Strip Comments - removes comment tokens
// 4. Load - loads includes and extends
// 5. Linker - resolves template inheritance
// 6. Codegen - generates HTML output
const std = @import("std");
const Allocator = std.mem.Allocator;
const mem = std.mem;
// ============================================================================
// Module Exports
// ============================================================================
pub const lexer = @import("lexer.zig");
pub const parser = @import("parser.zig");
pub const runtime = @import("runtime.zig");
pub const pug_error = @import("error.zig");
pub const walk = @import("walk.zig");
pub const strip_comments = @import("strip_comments.zig");
pub const load = @import("load.zig");
pub const linker = @import("linker.zig");
pub const codegen = @import("codegen.zig");
// Re-export commonly used types
pub const Token = lexer.Token;
pub const TokenType = lexer.TokenType;
pub const Lexer = lexer.Lexer;
pub const Parser = parser.Parser;
pub const Node = parser.Node;
pub const NodeType = parser.NodeType;
pub const PugError = pug_error.PugError;
pub const Compiler = codegen.Compiler;
// ============================================================================
// Compile Options
// ============================================================================
pub const CompileOptions = struct {
/// Source filename for error messages
filename: ?[]const u8 = null,
/// Base directory for absolute includes
basedir: ?[]const u8 = null,
/// Pretty print output with indentation
pretty: bool = false,
/// Strip unbuffered comments
strip_unbuffered_comments: bool = true,
/// Strip buffered comments
strip_buffered_comments: bool = false,
/// Include debug information
debug: bool = false,
/// Doctype to use
doctype: ?[]const u8 = null,
};
// ============================================================================
// Compile Result
// ============================================================================
pub const CompileResult = struct {
html: []const u8,
err: ?PugError = null,
pub fn deinit(self: *CompileResult, allocator: Allocator) void {
allocator.free(self.html);
if (self.err) |*e| {
e.deinit();
}
}
};
// ============================================================================
// Compilation Errors
// ============================================================================
pub const CompileError = error{
OutOfMemory,
LexerError,
ParserError,
LoadError,
LinkerError,
CodegenError,
FileNotFound,
AccessDenied,
InvalidUtf8,
};
// ============================================================================
// Main Compilation Functions
// ============================================================================
/// Compile a Pug template string to HTML
pub fn compile(
allocator: Allocator,
source: []const u8,
options: CompileOptions,
) CompileError!CompileResult {
var result = CompileResult{
.html = &[_]u8{},
};
// Stage 1: Lex the source
var lex_inst = Lexer.init(allocator, source, .{
.filename = options.filename,
}) catch {
return error.LexerError;
};
defer lex_inst.deinit();
const tokens = lex_inst.getTokens() catch {
if (lex_inst.last_error) |err| {
// Try to create detailed error, fall back to basic error if allocation fails
result.err = pug_error.makeError(
allocator,
"PUG:LEXER_ERROR",
err.message,
.{
.line = err.line,
.column = err.column,
.filename = options.filename,
.src = source,
},
) catch blk: {
// If error creation fails, create minimal error without source context
break :blk pug_error.makeError(allocator, "PUG:LEXER_ERROR", err.message, .{
.line = err.line,
.column = err.column,
.filename = options.filename,
.src = null, // Skip source to reduce allocation
}) catch null;
};
}
return error.LexerError;
};
// Stage 2: Strip comments
var stripped = strip_comments.stripComments(
allocator,
tokens,
.{
.strip_unbuffered = options.strip_unbuffered_comments,
.strip_buffered = options.strip_buffered_comments,
.filename = options.filename,
},
) catch {
return error.LexerError;
};
defer stripped.deinit(allocator);
// Stage 3: Parse tokens to AST
var parse = Parser.init(allocator, stripped.tokens.items, options.filename, source);
defer parse.deinit();
const ast = parse.parse() catch {
if (parse.err) |err| {
// Try to create detailed error, fall back to basic error if allocation fails
result.err = pug_error.makeError(
allocator,
"PUG:PARSER_ERROR",
err.message,
.{
.line = err.line,
.column = err.column,
.filename = options.filename,
.src = source,
},
) catch blk: {
// If error creation fails, create minimal error without source context
break :blk pug_error.makeError(allocator, "PUG:PARSER_ERROR", err.message, .{
.line = err.line,
.column = err.column,
.filename = options.filename,
.src = null,
}) catch null;
};
}
return error.ParserError;
};
defer {
ast.deinit(allocator);
allocator.destroy(ast);
}
// Stage 4: Link (resolve extends/blocks)
var link_result = linker.link(allocator, ast) catch {
return error.LinkerError;
};
defer link_result.deinit(allocator);
// Stage 5: Generate HTML
var compiler = Compiler.init(allocator, .{
.pretty = options.pretty,
.doctype = options.doctype,
.debug = options.debug,
});
defer compiler.deinit();
const html = compiler.compile(link_result.ast) catch {
return error.CodegenError;
};
result.html = html;
return result;
}
/// Compile a Pug file to HTML
pub fn compileFile(
allocator: Allocator,
filename: []const u8,
options: CompileOptions,
) CompileError!CompileResult {
// Read the file
const file = std.fs.cwd().openFile(filename, .{}) catch |err| {
return switch (err) {
error.FileNotFound => error.FileNotFound,
error.AccessDenied => error.AccessDenied,
else => error.FileNotFound,
};
};
defer file.close();
const source = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch {
return error.OutOfMemory;
};
defer allocator.free(source);
// Compile with filename set
var file_options = options;
file_options.filename = filename;
return compile(allocator, source, file_options);
}
/// Render a Pug template string to HTML (convenience function)
pub fn render(
allocator: Allocator,
source: []const u8,
) CompileError![]const u8 {
var result = try compile(allocator, source, .{});
if (result.err) |*e| {
e.deinit();
}
return result.html;
}
/// Render a Pug template string to pretty-printed HTML
pub fn renderPretty(
allocator: Allocator,
source: []const u8,
) CompileError![]const u8 {
var result = try compile(allocator, source, .{ .pretty = true });
if (result.err) |*e| {
e.deinit();
}
return result.html;
}
/// Render a Pug file to HTML
pub fn renderFile(
allocator: Allocator,
filename: []const u8,
) CompileError![]const u8 {
var result = try compileFile(allocator, filename, .{});
if (result.err) |*e| {
e.deinit();
}
return result.html;
}
// ============================================================================
// Tests
// ============================================================================
test "compile - simple text" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "| Hello, World!", .{});
defer result.deinit(allocator);
try std.testing.expectEqualStrings("Hello, World!", result.html);
}
test "compile - simple tag" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "div", .{});
defer result.deinit(allocator);
try std.testing.expectEqualStrings("<div></div>", result.html);
}
test "compile - tag with text" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "p Hello", .{});
defer result.deinit(allocator);
try std.testing.expectEqualStrings("<p>Hello</p>", result.html);
}
test "compile - tag with class shorthand" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "div.container", .{});
defer result.deinit(allocator);
// Parser stores class values with quotes, verify class attribute is present
try std.testing.expect(mem.indexOf(u8, result.html, "class=") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "container") != null);
}
test "compile - tag with id shorthand" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "div#main", .{});
defer result.deinit(allocator);
// Parser stores id values with quotes, verify id attribute is present
try std.testing.expect(mem.indexOf(u8, result.html, "id=") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "main") != null);
}
test "compile - tag with attributes" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "a(href=\"/home\") Home", .{});
defer result.deinit(allocator);
// Parser stores attribute values with quotes, verify attribute is present
try std.testing.expect(mem.indexOf(u8, result.html, "href=") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "/home") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "Home") != null);
}
test "compile - nested tags" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "div\n span Hello", .{});
defer result.deinit(allocator);
try std.testing.expectEqualStrings("<div><span>Hello</span></div>", result.html);
}
test "compile - self-closing tag" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "br", .{});
defer result.deinit(allocator);
try std.testing.expectEqualStrings("<br>", result.html);
}
test "compile - doctype" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "doctype html\nhtml", .{});
defer result.deinit(allocator);
try std.testing.expect(mem.startsWith(u8, result.html, "<!DOCTYPE html>"));
}
test "compile - unbuffered comment stripped" {
const allocator = std.testing.allocator;
// Unbuffered comments (//-) are stripped by default
var result = try compile(allocator, "//- This is stripped\ndiv", .{});
defer result.deinit(allocator);
// The comment text should not appear
try std.testing.expect(mem.indexOf(u8, result.html, "stripped") == null);
// But the div should
try std.testing.expect(mem.indexOf(u8, result.html, "<div>") != null);
}
test "compile - buffered comment visible" {
const allocator = std.testing.allocator;
// Buffered comments (//) are kept by default
var result = try compile(allocator, "// This is visible", .{});
defer result.deinit(allocator);
// Buffered comments should be in output
try std.testing.expect(mem.indexOf(u8, result.html, "<!--") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "visible") != null);
}
test "render - convenience function" {
const allocator = std.testing.allocator;
const html = try render(allocator, "p test");
defer allocator.free(html);
try std.testing.expectEqualStrings("<p>test</p>", html);
}
test "compile - multiple tags" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "p First\np Second", .{});
defer result.deinit(allocator);
try std.testing.expectEqualStrings("<p>First</p><p>Second</p>", result.html);
}
test "compile - interpolation text" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "p Hello, World!", .{});
defer result.deinit(allocator);
try std.testing.expectEqualStrings("<p>Hello, World!</p>", result.html);
}
test "compile - multiple classes" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "div.foo.bar", .{});
defer result.deinit(allocator);
try std.testing.expect(mem.indexOf(u8, result.html, "class=\"") != null);
}
test "compile - class and id" {
const allocator = std.testing.allocator;
var result = try compile(allocator, "div#main.container", .{});
defer result.deinit(allocator);
// Parser stores values with quotes, check that both id and class are present
try std.testing.expect(mem.indexOf(u8, result.html, "id=") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "main") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "class=") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "container") != null);
}
test "compile - deeply nested" {
const allocator = std.testing.allocator;
var result = try compile(allocator,
\\html
\\ head
\\ title Test
\\ body
\\ div Hello
, .{});
defer result.deinit(allocator);
try std.testing.expect(mem.indexOf(u8, result.html, "<html>") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "<head>") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "<title>Test</title>") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "<body>") != null);
try std.testing.expect(mem.indexOf(u8, result.html, "<div>Hello</div>") != null);
}

View File

@@ -1,69 +1,23 @@
//! Pugz - A Pug-like HTML template engine written in Zig. // Pugz - A Pug-like HTML template engine written in Zig
//! //
//! Pugz provides a clean, indentation-based syntax for writing HTML templates, // Quick Start:
//! inspired by Pug (formerly Jade). It supports: // const pugz = @import("pugz");
//! - Indentation-based nesting // const engine = pugz.ViewEngine.init(.{ .views_dir = "views" });
//! - Tag, class, and ID shorthand syntax // const html = try engine.render(allocator, "index", .{ .title = "Home" });
//! - Attributes and text interpolation
//! - Control flow (if/else, each, while)
//! - Mixins and template inheritance
//!
//! ## Quick Start (Server Usage)
//!
//! ```zig
//! const pugz = @import("pugz");
//!
//! // Initialize view engine once at startup
//! var engine = try pugz.ViewEngine.init(allocator, .{
//! .views_dir = "src/views",
//! });
//! defer engine.deinit();
//!
//! // Render templates (use arena allocator per request)
//! var arena = std.heap.ArenaAllocator.init(allocator);
//! defer arena.deinit();
//!
//! const html = try engine.render(arena.allocator(), "pages/home", .{
//! .title = "Home",
//! });
//! ```
pub const lexer = @import("lexer.zig"); pub const pug = @import("pug.zig");
pub const ast = @import("ast.zig");
pub const parser = @import("parser.zig");
pub const codegen = @import("codegen.zig");
pub const runtime = @import("runtime.zig");
pub const view_engine = @import("view_engine.zig"); pub const view_engine = @import("view_engine.zig");
pub const diagnostic = @import("diagnostic.zig"); pub const template = @import("template.zig");
// Re-export main types for convenience // Re-export main types
pub const Lexer = lexer.Lexer;
pub const Token = lexer.Token;
pub const TokenType = lexer.TokenType;
pub const Parser = parser.Parser;
pub const Node = ast.Node;
pub const Document = ast.Document;
pub const CodeGen = codegen.CodeGen;
pub const generate = codegen.generate;
pub const Runtime = runtime.Runtime;
pub const Context = runtime.Context;
pub const Value = runtime.Value;
pub const render = runtime.render;
pub const renderWithOptions = runtime.renderWithOptions;
pub const RenderOptions = runtime.RenderOptions;
pub const renderTemplate = runtime.renderTemplate;
// High-level API
pub const ViewEngine = view_engine.ViewEngine; pub const ViewEngine = view_engine.ViewEngine;
pub const CompiledTemplate = view_engine.CompiledTemplate; pub const compile = pug.compile;
pub const compileFile = pug.compileFile;
pub const render = pug.render;
pub const renderFile = pug.renderFile;
pub const CompileOptions = pug.CompileOptions;
pub const CompileResult = pug.CompileResult;
pub const CompileError = pug.CompileError;
// Build-time template compilation // Convenience function for inline templates with data
pub const build_templates = @import("build_templates.zig"); pub const renderTemplate = template.renderWithData;
pub const compileTemplates = build_templates.compileTemplates;
test {
_ = @import("std").testing.refAllDecls(@This());
}

118
src/run_playground.zig Normal file
View File

@@ -0,0 +1,118 @@
// Zig Pugz - Process all .pug files in playground/examples folder
const std = @import("std");
const pug = @import("pug.zig");
const fs = std.fs;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
std.debug.print("=== Zig Pugz Playground ===\n\n", .{});
// Open the examples directory
var dir = fs.cwd().openDir("playground/examples", .{ .iterate = true }) catch {
std.debug.print("Error: Could not open playground/examples directory\n", .{});
std.debug.print("Run from packages/pugz/ directory\n", .{});
return;
};
defer dir.close();
// Collect .pug files
var files = std.ArrayListUnmanaged([]const u8){};
defer {
for (files.items) |f| allocator.free(f);
files.deinit(allocator);
}
var iter = dir.iterate();
while (try iter.next()) |entry| {
if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".pug")) {
const name = try allocator.dupe(u8, entry.name);
try files.append(allocator, name);
}
}
// Sort files
std.mem.sort([]const u8, files.items, {}, struct {
fn lessThan(_: void, a: []const u8, b: []const u8) bool {
return std.mem.lessThan(u8, a, b);
}
}.lessThan);
std.debug.print("Found {d} .pug files\n\n", .{files.items.len});
var passed: usize = 0;
var failed: usize = 0;
var total_time_ns: u64 = 0;
for (files.items) |filename| {
// Read file
const file = dir.openFile(filename, .{}) catch {
std.debug.print("x {s}\n -> Could not open file\n\n", .{filename});
failed += 1;
continue;
};
defer file.close();
const source = file.readToEndAlloc(allocator, 1024 * 1024) catch {
std.debug.print("x {s}\n -> Could not read file\n\n", .{filename});
failed += 1;
continue;
};
defer allocator.free(source);
// Benchmark
const iterations: usize = 100;
var success = false;
var last_html: ?[]const u8 = null;
// Warmup
for (0..5) |_| {
var result = pug.compile(allocator, source, .{}) catch continue;
result.deinit(allocator);
}
var timer = try std.time.Timer.start();
var i: usize = 0;
while (i < iterations) : (i += 1) {
var result = pug.compile(allocator, source, .{}) catch break;
if (i == iterations - 1) {
last_html = result.html;
} else {
result.deinit(allocator);
}
success = true;
}
const elapsed_ns = timer.read();
if (success and i == iterations) {
const time_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0 / @as(f64, @floatFromInt(iterations));
std.debug.print("OK {s} ({d:.3} ms)\n", .{ filename, time_ms });
// Show preview
if (last_html) |html| {
const max_len = @min(html.len, 200);
std.debug.print(" -> {s}{s}\n\n", .{ html[0..max_len], if (html.len > 200) "..." else "" });
allocator.free(html);
}
passed += 1;
total_time_ns += elapsed_ns;
} else {
std.debug.print("FAIL {s}\n -> Compilation failed\n\n", .{filename});
failed += 1;
}
}
std.debug.print("=== Summary ===\n", .{});
std.debug.print("Passed: {d}/{d}\n", .{ passed, files.items.len });
std.debug.print("Failed: {d}/{d}\n", .{ failed, files.items.len });
if (passed > 0) {
const total_ms = @as(f64, @floatFromInt(total_time_ns)) / 1_000_000.0 / 100.0;
std.debug.print("Total time: {d:.3} ms\n", .{total_ms});
std.debug.print("Average: {d:.3} ms per file\n", .{total_ms / @as(f64, @floatFromInt(passed))});
}
}

File diff suppressed because it is too large Load Diff

353
src/strip_comments.zig Normal file
View File

@@ -0,0 +1,353 @@
// strip_comments.zig - Zig port of pug-strip-comments
//
// Filters out comment tokens from a token stream.
// Handles both buffered and unbuffered comments with pipeless text support.
const std = @import("std");
const Allocator = std.mem.Allocator;
// Import token types from lexer
const lexer = @import("lexer.zig");
pub const Token = lexer.Token;
pub const TokenType = lexer.TokenType;
// Import error types
const pug_error = @import("error.zig");
pub const PugError = pug_error.PugError;
// ============================================================================
// Strip Comments Options
// ============================================================================
pub const StripCommentsOptions = struct {
/// Strip unbuffered comments (default: true)
strip_unbuffered: bool = true,
/// Strip buffered comments (default: false)
strip_buffered: bool = false,
/// Source filename for error messages
filename: ?[]const u8 = null,
};
// ============================================================================
// Errors
// ============================================================================
pub const StripCommentsError = error{
OutOfMemory,
UnexpectedToken,
};
// ============================================================================
// Strip Comments Result
// ============================================================================
pub const StripCommentsResult = struct {
tokens: std.ArrayListUnmanaged(Token),
err: ?PugError = null,
pub fn deinit(self: *StripCommentsResult, allocator: Allocator) void {
self.tokens.deinit(allocator);
}
};
// ============================================================================
// Strip Comments Implementation
// ============================================================================
/// Strip comments from a token stream
/// Returns filtered tokens with comments removed based on options
pub fn stripComments(
allocator: Allocator,
input: []const Token,
options: StripCommentsOptions,
) StripCommentsError!StripCommentsResult {
var result = StripCommentsResult{
.tokens = .{},
};
// State tracking
var in_comment = false;
var in_pipeless_text = false;
var comment_is_buffered = false;
for (input) |tok| {
const should_include = switch (tok.type) {
.comment => blk: {
if (in_comment) {
// Unexpected comment while already in comment
result.err = pug_error.makeError(
allocator,
"UNEXPECTED_TOKEN",
"`comment` encountered when already in a comment",
.{
.line = tok.loc.start.line,
.column = tok.loc.start.column,
.filename = options.filename,
.src = null,
},
) catch null;
return error.UnexpectedToken;
}
// Check if this is a buffered comment
comment_is_buffered = tok.isBuffered();
// Determine if we should strip this comment
if (comment_is_buffered) {
in_comment = options.strip_buffered;
} else {
in_comment = options.strip_unbuffered;
}
break :blk !in_comment;
},
.start_pipeless_text => blk: {
if (!in_comment) {
break :blk true;
}
if (in_pipeless_text) {
// Unexpected start_pipeless_text
result.err = pug_error.makeError(
allocator,
"UNEXPECTED_TOKEN",
"`start-pipeless-text` encountered when already in pipeless text mode",
.{
.line = tok.loc.start.line,
.column = tok.loc.start.column,
.filename = options.filename,
.src = null,
},
) catch null;
return error.UnexpectedToken;
}
in_pipeless_text = true;
break :blk false;
},
.end_pipeless_text => blk: {
if (!in_comment) {
break :blk true;
}
if (!in_pipeless_text) {
// Unexpected end_pipeless_text
result.err = pug_error.makeError(
allocator,
"UNEXPECTED_TOKEN",
"`end-pipeless-text` encountered when not in pipeless text mode",
.{
.line = tok.loc.start.line,
.column = tok.loc.start.column,
.filename = options.filename,
.src = null,
},
) catch null;
return error.UnexpectedToken;
}
in_pipeless_text = false;
in_comment = false;
break :blk false;
},
// Text tokens right after comment but before pipeless text
.text, .text_html => !in_comment,
// All other tokens
else => blk: {
if (in_pipeless_text) {
break :blk false;
}
in_comment = false;
break :blk true;
},
};
if (should_include) {
try result.tokens.append(allocator, tok);
}
}
return result;
}
/// Convenience function - strip with default options (unbuffered only)
pub fn stripUnbufferedComments(
allocator: Allocator,
input: []const Token,
) StripCommentsError!StripCommentsResult {
return stripComments(allocator, input, .{});
}
/// Convenience function - strip all comments
pub fn stripAllComments(
allocator: Allocator,
input: []const Token,
) StripCommentsError!StripCommentsResult {
return stripComments(allocator, input, .{
.strip_unbuffered = true,
.strip_buffered = true,
});
}
// ============================================================================
// Tests
// ============================================================================
test "stripComments - no comments" {
const allocator = std.testing.allocator;
const tokens = [_]Token{
.{ .type = .tag, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .val = .{ .string = "div" } },
.{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 4 } } },
.{ .type = .eos, .loc = .{ .start = .{ .line = 2, .column = 1 } } },
};
var result = try stripComments(allocator, &tokens, .{});
defer result.deinit(allocator);
try std.testing.expectEqual(@as(usize, 3), result.tokens.items.len);
}
test "stripComments - strip unbuffered comment" {
const allocator = std.testing.allocator;
const tokens = [_]Token{
.{ .type = .tag, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .val = .{ .string = "div" } },
.{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 4 } } },
.{ .type = .comment, .loc = .{ .start = .{ .line = 2, .column = 1 } }, .buffer = .{ .boolean = false } },
.{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 4 } }, .val = .{ .string = "comment text" } },
.{ .type = .newline, .loc = .{ .start = .{ .line = 2, .column = 16 } } },
.{ .type = .tag, .loc = .{ .start = .{ .line = 3, .column = 1 } }, .val = .{ .string = "span" } },
.{ .type = .eos, .loc = .{ .start = .{ .line = 4, .column = 1 } } },
};
var result = try stripComments(allocator, &tokens, .{});
defer result.deinit(allocator);
// Should strip comment and its text, keep tags and structure
try std.testing.expectEqual(@as(usize, 5), result.tokens.items.len);
try std.testing.expectEqual(TokenType.tag, result.tokens.items[0].type);
try std.testing.expectEqual(TokenType.newline, result.tokens.items[1].type);
try std.testing.expectEqual(TokenType.newline, result.tokens.items[2].type);
try std.testing.expectEqual(TokenType.tag, result.tokens.items[3].type);
try std.testing.expectEqual(TokenType.eos, result.tokens.items[4].type);
}
test "stripComments - keep buffered comment by default" {
const allocator = std.testing.allocator;
const tokens = [_]Token{
.{ .type = .tag, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .val = .{ .string = "div" } },
.{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 4 } } },
.{ .type = .comment, .loc = .{ .start = .{ .line = 2, .column = 1 } }, .buffer = .{ .boolean = true } },
.{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 4 } }, .val = .{ .string = "buffered comment" } },
.{ .type = .newline, .loc = .{ .start = .{ .line = 2, .column = 20 } } },
.{ .type = .eos, .loc = .{ .start = .{ .line = 3, .column = 1 } } },
};
var result = try stripComments(allocator, &tokens, .{});
defer result.deinit(allocator);
// Should keep buffered comment
try std.testing.expectEqual(@as(usize, 6), result.tokens.items.len);
}
test "stripComments - strip buffered when option set" {
const allocator = std.testing.allocator;
const tokens = [_]Token{
.{ .type = .tag, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .val = .{ .string = "div" } },
.{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 4 } } },
.{ .type = .comment, .loc = .{ .start = .{ .line = 2, .column = 1 } }, .buffer = .{ .boolean = true } },
.{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 4 } }, .val = .{ .string = "buffered comment" } },
.{ .type = .newline, .loc = .{ .start = .{ .line = 2, .column = 20 } } },
.{ .type = .eos, .loc = .{ .start = .{ .line = 3, .column = 1 } } },
};
var result = try stripComments(allocator, &tokens, .{ .strip_buffered = true });
defer result.deinit(allocator);
// Should strip buffered comment
try std.testing.expectEqual(@as(usize, 4), result.tokens.items.len);
}
test "stripComments - pipeless text in comment" {
const allocator = std.testing.allocator;
const tokens = [_]Token{
.{ .type = .comment, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .buffer = .{ .boolean = false } },
.{ .type = .start_pipeless_text, .loc = .{ .start = .{ .line = 1, .column = 1 } } },
.{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 3 } }, .val = .{ .string = "line 1" } },
.{ .type = .text, .loc = .{ .start = .{ .line = 3, .column = 3 } }, .val = .{ .string = "line 2" } },
.{ .type = .end_pipeless_text, .loc = .{ .start = .{ .line = 4, .column = 1 } } },
.{ .type = .tag, .loc = .{ .start = .{ .line = 5, .column = 1 } }, .val = .{ .string = "div" } },
.{ .type = .eos, .loc = .{ .start = .{ .line = 6, .column = 1 } } },
};
var result = try stripComments(allocator, &tokens, .{});
defer result.deinit(allocator);
// Should strip everything in the comment including pipeless text
try std.testing.expectEqual(@as(usize, 2), result.tokens.items.len);
try std.testing.expectEqual(TokenType.tag, result.tokens.items[0].type);
try std.testing.expectEqual(TokenType.eos, result.tokens.items[1].type);
}
test "stripComments - pipeless text outside comment" {
const allocator = std.testing.allocator;
const tokens = [_]Token{
.{ .type = .tag, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .val = .{ .string = "script" } },
.{ .type = .dot, .loc = .{ .start = .{ .line = 1, .column = 7 } } },
.{ .type = .start_pipeless_text, .loc = .{ .start = .{ .line = 1, .column = 8 } } },
.{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 3 } }, .val = .{ .string = "var x = 1;" } },
.{ .type = .end_pipeless_text, .loc = .{ .start = .{ .line = 3, .column = 1 } } },
.{ .type = .eos, .loc = .{ .start = .{ .line = 4, .column = 1 } } },
};
var result = try stripComments(allocator, &tokens, .{});
defer result.deinit(allocator);
// Should keep all tokens - no comments
try std.testing.expectEqual(@as(usize, 6), result.tokens.items.len);
}
test "stripComments - keep unbuffered when option disabled" {
const allocator = std.testing.allocator;
const tokens = [_]Token{
.{ .type = .comment, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .buffer = .{ .boolean = false } },
.{ .type = .text, .loc = .{ .start = .{ .line = 1, .column = 4 } }, .val = .{ .string = "keep me" } },
.{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 11 } } },
.{ .type = .eos, .loc = .{ .start = .{ .line = 2, .column = 1 } } },
};
var result = try stripComments(allocator, &tokens, .{ .strip_unbuffered = false });
defer result.deinit(allocator);
// Should keep unbuffered comment
try std.testing.expectEqual(@as(usize, 4), result.tokens.items.len);
}
test "stripAllComments - strips both types" {
const allocator = std.testing.allocator;
const tokens = [_]Token{
.{ .type = .comment, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .buffer = .{ .boolean = false } },
.{ .type = .text, .loc = .{ .start = .{ .line = 1, .column = 4 } }, .val = .{ .string = "unbuffered" } },
.{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 14 } } },
.{ .type = .comment, .loc = .{ .start = .{ .line = 2, .column = 1 } }, .buffer = .{ .boolean = true } },
.{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 4 } }, .val = .{ .string = "buffered" } },
.{ .type = .newline, .loc = .{ .start = .{ .line = 2, .column = 12 } } },
.{ .type = .tag, .loc = .{ .start = .{ .line = 3, .column = 1 } }, .val = .{ .string = "div" } },
.{ .type = .eos, .loc = .{ .start = .{ .line = 4, .column = 1 } } },
};
var result = try stripAllComments(allocator, &tokens);
defer result.deinit(allocator);
// Should strip both comments, keep tag and structure
try std.testing.expectEqual(@as(usize, 4), result.tokens.items.len);
try std.testing.expectEqual(TokenType.newline, result.tokens.items[0].type);
try std.testing.expectEqual(TokenType.newline, result.tokens.items[1].type);
try std.testing.expectEqual(TokenType.tag, result.tokens.items[2].type);
try std.testing.expectEqual(TokenType.eos, result.tokens.items[3].type);
}

683
src/template.zig Normal file
View File

@@ -0,0 +1,683 @@
// template.zig - Runtime template rendering with data binding
//
// This module provides runtime data binding for Pug templates.
// It allows passing a Zig struct and rendering dynamic content.
// Reuses utilities from runtime.zig for escaping and attribute rendering.
const std = @import("std");
const Allocator = std.mem.Allocator;
const pug = @import("pug.zig");
const parser = @import("parser.zig");
const Node = parser.Node;
const runtime = @import("runtime.zig");
pub const TemplateError = error{
OutOfMemory,
LexerError,
ParserError,
};
/// Render context tracks state like doctype mode
pub const RenderContext = struct {
/// true = HTML5 terse mode (default), false = XHTML mode
terse: bool = true,
};
/// Render a template with data
pub fn renderWithData(allocator: Allocator, source: []const u8, data: anytype) ![]const u8 {
// Lex
var lex = pug.lexer.Lexer.init(allocator, source, .{}) catch return error.OutOfMemory;
defer lex.deinit();
const tokens = lex.getTokens() catch return error.LexerError;
// Strip comments
var stripped = pug.strip_comments.stripComments(allocator, tokens, .{}) catch return error.OutOfMemory;
defer stripped.deinit(allocator);
// Parse
var parse = pug.parser.Parser.init(allocator, stripped.tokens.items, null, source);
defer parse.deinit();
const ast = parse.parse() catch {
return error.ParserError;
};
defer {
ast.deinit(allocator);
allocator.destroy(ast);
}
// Render with data
var output = std.ArrayListUnmanaged(u8){};
errdefer output.deinit(allocator);
// Detect doctype to set terse mode
var ctx = RenderContext{};
detectDoctype(ast, &ctx);
try renderNode(allocator, &output, ast, data, &ctx);
return output.toOwnedSlice(allocator);
}
/// Scan AST for doctype and set terse mode accordingly
fn detectDoctype(node: *Node, ctx: *RenderContext) void {
if (node.type == .Doctype) {
if (node.val) |val| {
// XHTML doctypes use non-terse mode
if (std.mem.eql(u8, val, "xml") or
std.mem.eql(u8, val, "strict") or
std.mem.eql(u8, val, "transitional") or
std.mem.eql(u8, val, "frameset") or
std.mem.eql(u8, val, "1.1") or
std.mem.eql(u8, val, "basic") or
std.mem.eql(u8, val, "mobile"))
{
ctx.terse = false;
}
}
return;
}
// Check children
for (node.nodes.items) |child| {
detectDoctype(child, ctx);
if (!ctx.terse) return;
}
}
fn renderNode(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), node: *Node, data: anytype, ctx: *const RenderContext) Allocator.Error!void {
switch (node.type) {
.Block, .NamedBlock => {
for (node.nodes.items) |child| {
try renderNode(allocator, output, child, data, ctx);
}
},
.Tag, .InterpolatedTag => try renderTag(allocator, output, node, data, ctx),
.Text => try renderText(allocator, output, node, data),
.Code => try renderCode(allocator, output, node, data, ctx),
.Comment => try renderComment(allocator, output, node),
.BlockComment => try renderBlockComment(allocator, output, node, data, ctx),
.Doctype => try renderDoctype(allocator, output, node),
.Each => try renderEach(allocator, output, node, data, ctx),
.Mixin => {
// Mixin definitions are skipped (only mixin calls render)
if (!node.call) return;
for (node.nodes.items) |child| {
try renderNode(allocator, output, child, data, ctx);
}
},
else => {
for (node.nodes.items) |child| {
try renderNode(allocator, output, child, data, ctx);
}
},
}
}
fn renderTag(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), tag: *Node, data: anytype, ctx: *const RenderContext) Allocator.Error!void {
const name = tag.name orelse "div";
try output.appendSlice(allocator, "<");
try output.appendSlice(allocator, name);
// Render attributes using runtime.attr()
for (tag.attrs.items) |attr| {
const attr_val = try evaluateAttrValue(allocator, attr.val, data);
const attr_str = runtime.attr(allocator, attr.name, attr_val, true, ctx.terse) catch |err| switch (err) {
error.FormatError => return error.OutOfMemory,
error.OutOfMemory => return error.OutOfMemory,
};
defer allocator.free(attr_str);
try output.appendSlice(allocator, attr_str);
}
// Self-closing logic differs by mode:
// - HTML5 terse: void elements are self-closing without />
// - XHTML/XML: only explicit / makes tags self-closing
const is_void = isSelfClosing(name);
const is_self_closing = if (ctx.terse)
tag.self_closing or is_void
else
tag.self_closing;
if (is_self_closing and tag.nodes.items.len == 0 and tag.val == null) {
if (ctx.terse and !tag.self_closing) {
try output.appendSlice(allocator, ">");
} else {
try output.appendSlice(allocator, "/>");
}
return;
}
try output.appendSlice(allocator, ">");
// Render text content
if (tag.val) |val| {
try processInterpolation(allocator, output, val, false, data);
}
// Render children
for (tag.nodes.items) |child| {
try renderNode(allocator, output, child, data, ctx);
}
// Close tag
if (!is_self_closing) {
try output.appendSlice(allocator, "</");
try output.appendSlice(allocator, name);
try output.appendSlice(allocator, ">");
}
}
/// Evaluate attribute value from AST to runtime.AttrValue
fn evaluateAttrValue(allocator: Allocator, val: ?[]const u8, data: anytype) !runtime.AttrValue {
_ = allocator;
const v = val orelse return .{ .boolean = true }; // No value = boolean attribute
// Handle boolean literals
if (std.mem.eql(u8, v, "true")) return .{ .boolean = true };
if (std.mem.eql(u8, v, "false")) return .{ .boolean = false };
if (std.mem.eql(u8, v, "null") or std.mem.eql(u8, v, "undefined")) return .none;
// Quoted string - extract inner value
if (v.len >= 2 and (v[0] == '"' or v[0] == '\'')) {
return .{ .string = v[1 .. v.len - 1] };
}
// Expression - try to look up in data
if (getFieldValue(data, v)) |value| {
return .{ .string = value };
}
// Unknown expression - return as string literal
return .{ .string = v };
}
fn renderText(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), text: *Node, data: anytype) Allocator.Error!void {
if (text.val) |val| {
try processInterpolation(allocator, output, val, false, data);
}
}
fn renderCode(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), code: *Node, data: anytype, ctx: *const RenderContext) Allocator.Error!void {
if (code.buffer) {
if (code.val) |val| {
// Check if it's a string literal (quoted)
if (val.len >= 2 and (val[0] == '"' or val[0] == '\'')) {
const inner = val[1 .. val.len - 1];
if (code.must_escape) {
try runtime.appendEscaped(allocator, output, inner);
} else {
try output.appendSlice(allocator, inner);
}
} else if (getFieldValue(data, val)) |value| {
if (code.must_escape) {
try runtime.appendEscaped(allocator, output, value);
} else {
try output.appendSlice(allocator, value);
}
}
}
}
for (code.nodes.items) |child| {
try renderNode(allocator, output, child, data, ctx);
}
}
fn renderEach(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), each: *Node, data: anytype, ctx: *const RenderContext) Allocator.Error!void {
const collection_name = each.obj orelse return;
const item_name = each.val orelse "item";
_ = item_name;
const T = @TypeOf(data);
const info = @typeInfo(T);
if (info != .@"struct") return;
inline for (info.@"struct".fields) |field| {
if (std.mem.eql(u8, field.name, collection_name)) {
const collection = @field(data, field.name);
const CollType = @TypeOf(collection);
const coll_info = @typeInfo(CollType);
if (coll_info == .pointer and coll_info.pointer.size == .slice) {
for (collection) |item| {
const ItemType = @TypeOf(item);
if (ItemType == []const u8) {
for (each.nodes.items) |child| {
try renderNodeWithItem(allocator, output, child, data, item, ctx);
}
} else {
for (each.nodes.items) |child| {
try renderNode(allocator, output, child, data, ctx);
}
}
}
return;
}
}
}
if (each.alternate) |alt| {
try renderNode(allocator, output, alt, data, ctx);
}
}
fn renderNodeWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), node: *Node, data: anytype, item: []const u8, ctx: *const RenderContext) Allocator.Error!void {
switch (node.type) {
.Block, .NamedBlock => {
for (node.nodes.items) |child| {
try renderNodeWithItem(allocator, output, child, data, item, ctx);
}
},
.Tag, .InterpolatedTag => try renderTagWithItem(allocator, output, node, data, item, ctx),
.Text => try renderTextWithItem(allocator, output, node, item),
.Code => {
if (node.buffer) {
if (node.must_escape) {
try runtime.appendEscaped(allocator, output, item);
} else {
try output.appendSlice(allocator, item);
}
}
},
else => {
for (node.nodes.items) |child| {
try renderNodeWithItem(allocator, output, child, data, item, ctx);
}
},
}
}
fn renderTagWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), tag: *Node, data: anytype, item: []const u8, ctx: *const RenderContext) Allocator.Error!void {
const name = tag.name orelse "div";
try output.appendSlice(allocator, "<");
try output.appendSlice(allocator, name);
// Render attributes using runtime.attr()
for (tag.attrs.items) |attr| {
const attr_val = try evaluateAttrValue(allocator, attr.val, data);
const attr_str = runtime.attr(allocator, attr.name, attr_val, true, ctx.terse) catch |err| switch (err) {
error.FormatError => return error.OutOfMemory,
error.OutOfMemory => return error.OutOfMemory,
};
defer allocator.free(attr_str);
try output.appendSlice(allocator, attr_str);
}
const is_void = isSelfClosing(name);
const is_self_closing = if (ctx.terse)
tag.self_closing or is_void
else
tag.self_closing;
if (is_self_closing and tag.nodes.items.len == 0 and tag.val == null) {
if (ctx.terse and !tag.self_closing) {
try output.appendSlice(allocator, ">");
} else {
try output.appendSlice(allocator, "/>");
}
return;
}
try output.appendSlice(allocator, ">");
if (tag.val) |val| {
try processInterpolationWithItem(allocator, output, val, true, data, item);
}
for (tag.nodes.items) |child| {
try renderNodeWithItem(allocator, output, child, data, item, ctx);
}
if (!is_self_closing) {
try output.appendSlice(allocator, "</");
try output.appendSlice(allocator, name);
try output.appendSlice(allocator, ">");
}
}
fn renderTextWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), text: *Node, item: []const u8) Allocator.Error!void {
if (text.val) |val| {
try runtime.appendEscaped(allocator, output, val);
_ = item;
}
}
fn processInterpolationWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), text: []const u8, escape: bool, data: anytype, item: []const u8) Allocator.Error!void {
_ = data;
var i: usize = 0;
while (i < text.len) {
if (i + 1 < text.len and text[i] == '#' and text[i + 1] == '{') {
var j = i + 2;
var brace_count: usize = 1;
while (j < text.len and brace_count > 0) {
if (text[j] == '{') brace_count += 1;
if (text[j] == '}') brace_count -= 1;
j += 1;
}
if (brace_count == 0) {
if (escape) {
try runtime.appendEscaped(allocator, output, item);
} else {
try output.appendSlice(allocator, item);
}
i = j;
continue;
}
}
if (escape) {
if (runtime.escapeChar(text[i])) |esc| {
try output.appendSlice(allocator, esc);
} else {
try output.append(allocator, text[i]);
}
} else {
try output.append(allocator, text[i]);
}
i += 1;
}
}
fn renderComment(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), comment: *Node) Allocator.Error!void {
if (!comment.buffer) return;
try output.appendSlice(allocator, "<!--");
if (comment.val) |val| {
try output.appendSlice(allocator, val);
}
try output.appendSlice(allocator, "-->");
}
fn renderBlockComment(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), comment: *Node, data: anytype, ctx: *const RenderContext) Allocator.Error!void {
if (!comment.buffer) return;
try output.appendSlice(allocator, "<!--");
if (comment.val) |val| {
try output.appendSlice(allocator, val);
}
for (comment.nodes.items) |child| {
try renderNode(allocator, output, child, data, ctx);
}
try output.appendSlice(allocator, "-->");
}
// Doctype mappings
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\">" },
});
fn renderDoctype(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), doctype: *Node) Allocator.Error!void {
if (doctype.val) |val| {
if (doctypes.get(val)) |dt| {
try output.appendSlice(allocator, dt);
} else {
try output.appendSlice(allocator, "<!DOCTYPE ");
try output.appendSlice(allocator, val);
try output.appendSlice(allocator, ">");
}
} else {
try output.appendSlice(allocator, "<!DOCTYPE html>");
}
}
/// Process interpolation #{expr} in text
/// escape_quotes: true for attribute values (escape "), false for text content
fn processInterpolation(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), text: []const u8, escape_quotes: bool, data: anytype) Allocator.Error!void {
var i: usize = 0;
while (i < text.len) {
if (i + 1 < text.len and text[i] == '#' and text[i + 1] == '{') {
var j = i + 2;
var brace_count: usize = 1;
while (j < text.len and brace_count > 0) {
if (text[j] == '{') brace_count += 1;
if (text[j] == '}') brace_count -= 1;
j += 1;
}
if (brace_count == 0) {
const expr = std.mem.trim(u8, text[i + 2 .. j - 1], " \t");
if (getFieldValue(data, expr)) |value| {
if (escape_quotes) {
try runtime.appendEscaped(allocator, output, value);
} else {
// Text content: escape < > & but not quotes
try appendTextEscaped(allocator, output, value);
}
}
i = j;
continue;
}
}
// Regular character - use appropriate escaping
const c = text[i];
if (escape_quotes) {
if (runtime.escapeChar(c)) |esc| {
try output.appendSlice(allocator, esc);
} else {
try output.append(allocator, c);
}
} else {
// Text content: escape < > & but not quotes, preserve HTML entities
switch (c) {
'<' => try output.appendSlice(allocator, "&lt;"),
'>' => try output.appendSlice(allocator, "&gt;"),
'&' => {
if (isHtmlEntity(text[i..])) {
try output.append(allocator, c);
} else {
try output.appendSlice(allocator, "&amp;");
}
},
else => try output.append(allocator, c),
}
}
i += 1;
}
}
/// Get a field value from the data struct by name
fn getFieldValue(data: anytype, name: []const u8) ?[]const u8 {
const T = @TypeOf(data);
const info = @typeInfo(T);
if (info != .@"struct") return null;
inline for (info.@"struct".fields) |field| {
if (std.mem.eql(u8, field.name, name)) {
const value = @field(data, field.name);
const ValueType = @TypeOf(value);
if (ValueType == []const u8) {
return value;
}
const value_info = @typeInfo(ValueType);
if (value_info == .pointer) {
const ptr = value_info.pointer;
if (ptr.size == .one) {
const child_info = @typeInfo(ptr.child);
if (child_info == .array and child_info.array.child == u8) {
return value;
}
}
}
}
}
return null;
}
/// Escape for text content - escapes < > & (NOT quotes)
/// Preserves existing HTML entities like &#8217;
fn appendTextEscaped(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), str: []const u8) Allocator.Error!void {
var i: usize = 0;
while (i < str.len) {
const c = str[i];
switch (c) {
'<' => try output.appendSlice(allocator, "&lt;"),
'>' => try output.appendSlice(allocator, "&gt;"),
'&' => {
if (isHtmlEntity(str[i..])) {
try output.append(allocator, c);
} else {
try output.appendSlice(allocator, "&amp;");
}
},
else => try output.append(allocator, c),
}
i += 1;
}
}
/// Check if string starts with a valid HTML entity
fn isHtmlEntity(str: []const u8) bool {
if (str.len < 3 or str[0] != '&') return false;
var i: usize = 1;
// Numeric entity: &#digits; or &#xhex;
if (str[i] == '#') {
i += 1;
if (i >= str.len) return false;
if (str[i] == 'x' or str[i] == 'X') {
i += 1;
if (i >= str.len) return false;
var has_hex = false;
while (i < str.len and i < 10) : (i += 1) {
const ch = str[i];
if (ch == ';') return has_hex;
if ((ch >= '0' and ch <= '9') or
(ch >= 'a' and ch <= 'f') or
(ch >= 'A' and ch <= 'F'))
{
has_hex = true;
} else {
return false;
}
}
return false;
}
var has_digit = false;
while (i < str.len and i < 10) : (i += 1) {
const ch = str[i];
if (ch == ';') return has_digit;
if (ch >= '0' and ch <= '9') {
has_digit = true;
} else {
return false;
}
}
return false;
}
// Named entity: &name;
var has_alpha = false;
while (i < str.len and i < 32) : (i += 1) {
const ch = str[i];
if (ch == ';') return has_alpha;
if ((ch >= 'a' and ch <= 'z') or (ch >= 'A' and ch <= 'Z') or (ch >= '0' and ch <= '9')) {
has_alpha = true;
} else {
return false;
}
}
return false;
}
fn isSelfClosing(name: []const u8) bool {
const self_closing_tags = [_][]const u8{
"area", "base", "br", "col", "embed", "hr", "img", "input",
"link", "meta", "param", "source", "track", "wbr",
};
for (self_closing_tags) |tag| {
if (std.mem.eql(u8, name, tag)) return true;
}
return false;
}
// ============================================================================
// Tests
// ============================================================================
test "simple interpolation" {
const allocator = std.testing.allocator;
const html = try renderWithData(allocator, "p Hello, #{name}!", .{ .name = "World" });
defer allocator.free(html);
try std.testing.expectEqualStrings("<p>Hello, World!</p>", html);
}
test "multiple interpolations" {
const allocator = std.testing.allocator;
const html = try renderWithData(allocator, "p #{greeting}, #{name}!", .{
.greeting = "Hello",
.name = "World",
});
defer allocator.free(html);
try std.testing.expectEqualStrings("<p>Hello, World!</p>", html);
}
test "attribute with data" {
const allocator = std.testing.allocator;
const html = try renderWithData(allocator, "a(href=url) Click", .{ .url = "/home" });
defer allocator.free(html);
try std.testing.expectEqualStrings("<a href=\"/home\">Click</a>", html);
}
test "buffered code" {
const allocator = std.testing.allocator;
const html = try renderWithData(allocator, "p= message", .{ .message = "Hello" });
defer allocator.free(html);
try std.testing.expectEqualStrings("<p>Hello</p>", html);
}
test "escape html" {
const allocator = std.testing.allocator;
const html = try renderWithData(allocator, "p #{content}", .{ .content = "<b>bold</b>" });
defer allocator.free(html);
try std.testing.expectEqualStrings("<p>&lt;b&gt;bold&lt;/b&gt;</p>", html);
}
test "no data - static template" {
const allocator = std.testing.allocator;
const html = try renderWithData(allocator, "p Hello, World!", .{});
defer allocator.free(html);
try std.testing.expectEqualStrings("<p>Hello, World!</p>", html);
}
test "nested tags with data" {
const allocator = std.testing.allocator;
const html = try renderWithData(allocator,
\\div
\\ h1 #{title}
\\ p #{body}
, .{
.title = "Welcome",
.body = "Hello there!",
});
defer allocator.free(html);
try std.testing.expectEqualStrings("<div><h1>Welcome</h1><p>Hello there!</p></div>", html);
}

View File

@@ -0,0 +1,301 @@
'use strict';
var assert = require('assert');
var utils = require('util');
var attrs = require('../');
var options;
function test(input, expected, locals) {
var opts = options;
locals = locals || {};
locals.pug = locals.pug || require('pug-runtime');
it(
utils.inspect(input).replace(/\n/g, '') + ' => ' + utils.inspect(expected),
function() {
var src = attrs(input, opts);
var localKeys = Object.keys(locals).sort();
var output = Function(
localKeys.join(', '),
'return (' + src + ');'
).apply(
null,
localKeys.map(function(key) {
return locals[key];
})
);
if (opts.format === 'html') {
expect(output).toBe(expected);
} else {
expect(output).toEqual(expected);
}
}
);
}
function withOptions(opts, fn) {
describe('options: ' + utils.inspect(opts), function() {
options = opts;
fn();
});
}
withOptions(
{
terse: true,
format: 'html',
runtime: function(name) {
return 'pug.' + name;
},
},
function() {
test([], '');
test([{name: 'foo', val: 'false', mustEscape: true}], '');
test([{name: 'foo', val: 'true', mustEscape: true}], ' foo');
test([{name: 'foo', val: false, mustEscape: true}], '');
test([{name: 'foo', val: true, mustEscape: true}], ' foo');
test([{name: 'foo', val: 'foo', mustEscape: true}], '', {foo: false});
test([{name: 'foo', val: 'foo', mustEscape: true}], ' foo', {foo: true});
test([{name: 'foo', val: '"foo"', mustEscape: true}], ' foo="foo"');
test(
[
{name: 'foo', val: '"foo"', mustEscape: true},
{name: 'bar', val: '"bar"', mustEscape: true},
],
' foo="foo" bar="bar"'
);
test([{name: 'foo', val: 'foo', mustEscape: true}], ' foo="fooo"', {
foo: 'fooo',
});
test(
[
{name: 'foo', val: 'foo', mustEscape: true},
{name: 'bar', val: 'bar', mustEscape: true},
],
' foo="fooo" bar="baro"',
{foo: 'fooo', bar: 'baro'}
);
test(
[{name: 'style', val: '{color: "red"}', mustEscape: true}],
' style="color:red;"'
);
test(
[{name: 'style', val: '{color: color}', mustEscape: true}],
' style="color:red;"',
{color: 'red'}
);
test(
[
{name: 'class', val: '"foo"', mustEscape: true},
{name: 'class', val: '["bar", "baz"]', mustEscape: true},
],
' class="foo bar baz"'
);
test(
[
{name: 'class', val: '{foo: foo}', mustEscape: true},
{name: 'class', val: '["bar", "baz"]', mustEscape: true},
],
' class="foo bar baz"',
{foo: true}
);
test(
[
{name: 'class', val: '{foo: foo}', mustEscape: true},
{name: 'class', val: '["bar", "baz"]', mustEscape: true},
],
' class="bar baz"',
{foo: false}
);
test(
[
{name: 'class', val: 'foo', mustEscape: true},
{name: 'class', val: '"<str>"', mustEscape: true},
],
' class="&lt;foo&gt; &lt;str&gt;"',
{foo: '<foo>'}
);
test(
[
{name: 'foo', val: '"foo"', mustEscape: true},
{name: 'class', val: '["bar", "baz"]', mustEscape: true},
],
' class="bar baz" foo="foo"'
);
test(
[
{name: 'class', val: '["bar", "baz"]', mustEscape: true},
{name: 'foo', val: '"foo"', mustEscape: true},
],
' class="bar baz" foo="foo"'
);
test([{name: 'foo', val: '"<foo>"', mustEscape: false}], ' foo="<foo>"');
test(
[{name: 'foo', val: '"<foo>"', mustEscape: true}],
' foo="&lt;foo&gt;"'
);
test([{name: 'foo', val: 'foo', mustEscape: false}], ' foo="<foo>"', {
foo: '<foo>',
});
test([{name: 'foo', val: 'foo', mustEscape: true}], ' foo="&lt;foo&gt;"', {
foo: '<foo>',
});
}
);
withOptions(
{
terse: false,
format: 'html',
runtime: function(name) {
return 'pug.' + name;
},
},
function() {
test([{name: 'foo', val: 'false', mustEscape: true}], '');
test([{name: 'foo', val: 'true', mustEscape: true}], ' foo="foo"');
test([{name: 'foo', val: false, mustEscape: true}], '');
test([{name: 'foo', val: true, mustEscape: true}], ' foo="foo"');
test([{name: 'foo', val: 'foo', mustEscape: true}], '', {foo: false});
test([{name: 'foo', val: 'foo', mustEscape: true}], ' foo="foo"', {
foo: true,
});
}
);
withOptions(
{
terse: true,
format: 'object',
runtime: function(name) {
return 'pug.' + name;
},
},
function() {
test([], {});
test([{name: 'foo', val: 'false', mustEscape: true}], {foo: false});
test([{name: 'foo', val: 'true', mustEscape: true}], {foo: true});
test([{name: 'foo', val: false, mustEscape: true}], {foo: false});
test([{name: 'foo', val: true, mustEscape: true}], {foo: true});
test(
[{name: 'foo', val: 'foo', mustEscape: true}],
{foo: false},
{foo: false}
);
test(
[{name: 'foo', val: 'foo', mustEscape: true}],
{foo: true},
{foo: true}
);
test([{name: 'foo', val: '"foo"', mustEscape: true}], {foo: 'foo'});
test(
[
{name: 'foo', val: '"foo"', mustEscape: true},
{name: 'bar', val: '"bar"', mustEscape: true},
],
{foo: 'foo', bar: 'bar'}
);
test(
[{name: 'foo', val: 'foo', mustEscape: true}],
{foo: 'fooo'},
{foo: 'fooo'}
);
test(
[
{name: 'foo', val: 'foo', mustEscape: true},
{name: 'bar', val: 'bar', mustEscape: true},
],
{foo: 'fooo', bar: 'baro'},
{foo: 'fooo', bar: 'baro'}
);
test([{name: 'style', val: '{color: "red"}', mustEscape: true}], {
style: 'color:red;',
});
test(
[{name: 'style', val: '{color: color}', mustEscape: true}],
{style: 'color:red;'},
{color: 'red'}
);
test(
[
{name: 'class', val: '"foo"', mustEscape: true},
{name: 'class', val: '["bar", "baz"]', mustEscape: true},
],
{class: 'foo bar baz'}
);
test(
[
{name: 'class', val: '{foo: foo}', mustEscape: true},
{name: 'class', val: '["bar", "baz"]', mustEscape: true},
],
{class: 'foo bar baz'},
{foo: true}
);
test(
[
{name: 'class', val: '{foo: foo}', mustEscape: true},
{name: 'class', val: '["bar", "baz"]', mustEscape: true},
],
{class: 'bar baz'},
{foo: false}
);
test(
[
{name: 'class', val: 'foo', mustEscape: true},
{name: 'class', val: '"<str>"', mustEscape: true},
],
{class: '&lt;foo&gt; &lt;str&gt;'},
{foo: '<foo>'}
);
test(
[
{name: 'foo', val: '"foo"', mustEscape: true},
{name: 'class', val: '["bar", "baz"]', mustEscape: true},
],
{class: 'bar baz', foo: 'foo'}
);
test(
[
{name: 'class', val: '["bar", "baz"]', mustEscape: true},
{name: 'foo', val: '"foo"', mustEscape: true},
],
{class: 'bar baz', foo: 'foo'}
);
test([{name: 'foo', val: '"<foo>"', mustEscape: false}], {foo: '<foo>'});
test([{name: 'foo', val: '"<foo>"', mustEscape: true}], {
foo: '&lt;foo&gt;',
});
test(
[{name: 'foo', val: 'foo', mustEscape: false}],
{foo: '<foo>'},
{foo: '<foo>'}
);
test(
[{name: 'foo', val: 'foo', mustEscape: true}],
{foo: '&lt;foo&gt;'},
{foo: '<foo>'}
);
}
);
withOptions(
{
terse: false,
format: 'object',
runtime: function(name) {
return 'pug.' + name;
},
},
function() {
test([{name: 'foo', val: 'false', mustEscape: true}], {foo: false});
test([{name: 'foo', val: 'true', mustEscape: true}], {foo: true});
test([{name: 'foo', val: false, mustEscape: true}], {foo: false});
test([{name: 'foo', val: true, mustEscape: true}], {foo: true});
test(
[{name: 'foo', val: 'foo', mustEscape: true}],
{foo: false},
{foo: false}
);
test(
[{name: 'foo', val: 'foo', mustEscape: true}],
{foo: true},
{foo: true}
);
}
);

View File

@@ -0,0 +1,284 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`filters can be aliased 1`] = `
Object {
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 0,
"nodes": Array [
Object {
"attributeBlocks": Array [],
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 2,
"nodes": Array [
Object {
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 3,
"nodes": Array [
Object {
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 3,
"nodes": Array [
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 4,
"type": "Text",
"val": "function myFunc(foo) {",
},
Object {
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 5,
"type": "Text",
"val": "
",
},
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 5,
"type": "Text",
"val": " return foo;",
},
Object {
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 6,
"type": "Text",
"val": "
",
},
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 6,
"type": "Text",
"val": "}",
},
],
"type": "Block",
},
"column": 9,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 3,
"name": "minify",
"type": "Text",
"val": "function myFunc(n) {
return n;
}
",
},
],
"type": "Block",
},
"column": 3,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 3,
"name": "cdata",
"type": "Text",
"val": "<![CDATA[function myFunc(n){return n}]]>",
},
],
"type": "Block",
},
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"isInline": false,
"line": 2,
"name": "script",
"selfClosing": false,
"type": "Tag",
},
],
"type": "Block",
}
`;
exports[`options are applied before aliases 1`] = `
Object {
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 0,
"nodes": Array [
Object {
"attributeBlocks": Array [],
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 2,
"nodes": Array [
Object {
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 3,
"nodes": Array [
Object {
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 3,
"nodes": Array [
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 4,
"type": "Text",
"val": "function myFunc(foo) {",
},
Object {
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 5,
"type": "Text",
"val": "
",
},
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 5,
"type": "Text",
"val": " return foo;",
},
Object {
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 6,
"type": "Text",
"val": "
",
},
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 6,
"type": "Text",
"val": "}",
},
],
"type": "Block",
},
"column": 9,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 3,
"name": "minify",
"type": "Text",
"val": "function myFunc(n) {
return n;
}
",
},
],
"type": "Block",
},
"column": 3,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 3,
"name": "cdata",
"type": "Text",
"val": "<![CDATA[function myFunc(n) {
return n;
}]]>",
},
Object {
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 7,
"nodes": Array [
Object {
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 7,
"nodes": Array [
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 8,
"type": "Text",
"val": "function myFunc(foo) {",
},
Object {
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 9,
"type": "Text",
"val": "
",
},
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 9,
"type": "Text",
"val": " return foo;",
},
Object {
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 10,
"type": "Text",
"val": "
",
},
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 10,
"type": "Text",
"val": "}",
},
],
"type": "Block",
},
"column": 9,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 7,
"name": "uglify-js",
"type": "Text",
"val": "function myFunc(n) {
return n;
}
",
},
],
"type": "Block",
},
"column": 3,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"line": 7,
"name": "cdata",
"type": "Text",
"val": "<![CDATA[function myFunc(n){return n}]]>",
},
],
"type": "Block",
},
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/filter-aliases.test.js",
"isInline": false,
"line": 2,
"name": "script",
"selfClosing": false,
"type": "Tag",
},
],
"type": "Block",
}
`;
exports[`we do not support chains of aliases 1`] = `
Object {
"code": "PUG:FILTER_ALISE_CHAIN",
"message": "<basedir>/packages/pug-filters/test/filter-aliases.test.js:3:9
The filter \\"minify-js\\" is an alias for \\"minify\\", which is an alias for \\"uglify-js\\". Pug does not support chains of filter aliases.",
}
`;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,103 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`per filter options are applied, even to nested filters 1`] = `
Object {
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 0,
"nodes": Array [
Object {
"attributeBlocks": Array [],
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 2,
"nodes": Array [
Object {
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 3,
"nodes": Array [
Object {
"attrs": Array [],
"block": Object {
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 3,
"nodes": Array [
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 4,
"type": "Text",
"val": "function myFunc(foo) {",
},
Object {
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 5,
"type": "Text",
"val": "
",
},
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 5,
"type": "Text",
"val": " return foo;",
},
Object {
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 6,
"type": "Text",
"val": "
",
},
Object {
"column": 5,
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 6,
"type": "Text",
"val": "}",
},
],
"type": "Block",
},
"column": 9,
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 3,
"name": "uglify-js",
"type": "Text",
"val": "function myFunc(n) {
return n;
}
",
},
],
"type": "Block",
},
"column": 3,
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"line": 3,
"name": "cdata",
"type": "Text",
"val": "<![CDATA[function myFunc(n) {
return n;
}]]>",
},
],
"type": "Block",
},
"column": 1,
"filename": "<basedir>/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js",
"isInline": false,
"line": 2,
"name": "script",
"selfClosing": false,
"type": "Tag",
},
],
"type": "Block",
}
`;

View File

@@ -0,0 +1,84 @@
{
"type": "Block",
"nodes": [
{
"type": "Code",
"val": "var users = [{ name: 'tobi', age: 2 }]",
"buffer": false,
"mustEscape": false,
"isInline": false,
"line": 1,
"filename": "filters-empty.tokens.json"
},
{
"type": "Tag",
"name": "fb:users",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Each",
"obj": "users",
"val": "user",
"key": null,
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "fb:user",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "cdata",
"block": {
"type": "Block",
"nodes": [],
"line": 6,
"filename": "filters-empty.tokens.json"
},
"attrs": [],
"line": 6,
"filename": "filters-empty.tokens.json"
}
],
"line": 5,
"filename": "filters-empty.tokens.json"
},
"attrs": [
{
"name": "age",
"val": "user.age",
"mustEscape": true
}
],
"attributeBlocks": [],
"isInline": false,
"line": 5,
"filename": "filters-empty.tokens.json"
}
],
"line": 5,
"filename": "filters-empty.tokens.json"
},
"line": 4,
"filename": "filters-empty.tokens.json"
}
],
"line": 3,
"filename": "filters-empty.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 3,
"filename": "filters-empty.tokens.json"
}
],
"line": 0,
"filename": "filters-empty.tokens.json"
}

View File

@@ -0,0 +1,83 @@
{
"type": "Block",
"nodes": [
{
"type": "Code",
"val": "users = [{ name: 'tobi', age: 2 }]",
"buffer": false,
"mustEscape": false,
"isInline": false,
"line": 2,
"filename": "filters.cdata.tokens.json"
},
{
"type": "Tag",
"name": "fb:users",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Each",
"obj": "users",
"val": "user",
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "fb:user",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "cdata",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "#{user.name}",
"line": 8
}
]
},
"attrs": [],
"line": 7,
"filename": "filters.cdata.tokens.json"
}
]
},
"attrs": [
{
"name": "age",
"val": "user.age",
"mustEscape": true
}
],
"attributeBlocks": [],
"isInline": false,
"line": 6,
"filename": "filters.cdata.tokens.json"
}
],
"line": 6,
"filename": "filters.cdata.tokens.json"
},
"line": 5,
"filename": "filters.cdata.tokens.json"
}
]
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 4,
"filename": "filters.cdata.tokens.json"
}
],
"line": 0,
"filename": "filters.cdata.tokens.json"
}

View File

@@ -0,0 +1,84 @@
{
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "script",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "coffee-script",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "regexp = /\\n/",
"line": 3
}
],
"line": 2,
"filename": "filters.coffeescript.tokens.json"
},
"attrs": [],
"line": 2,
"filename": "filters.coffeescript.tokens.json"
},
{
"type": "Filter",
"name": "coffee-script",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "math =",
"line": 5
},
{
"type": "Text",
"val": "\n",
"line": 6
},
{
"type": "Text",
"val": " square: (value) -> value * value",
"line": 6
}
],
"line": 4,
"filename": "filters.coffeescript.tokens.json"
},
"attrs": [
{
"name": "minify",
"val": "true",
"mustEscape": true
}
],
"line": 4,
"filename": "filters.coffeescript.tokens.json"
}
],
"line": 1,
"filename": "filters.coffeescript.tokens.json"
},
"attrs": [
{
"name": "type",
"val": "'text/javascript'",
"mustEscape": true
}
],
"attributeBlocks": [],
"isInline": false,
"line": 1,
"filename": "filters.coffeescript.tokens.json"
}
],
"line": 0,
"filename": "filters.coffeescript.tokens.json"
}

View File

@@ -0,0 +1,101 @@
{
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "html",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "body",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "custom",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "Line 1",
"line": 4
},
{
"type": "Text",
"val": "\n",
"line": 5
},
{
"type": "Text",
"val": "Line 2",
"line": 5
},
{
"type": "Text",
"val": "\n",
"line": 6
},
{
"type": "Text",
"val": "",
"line": 6
},
{
"type": "Text",
"val": "\n",
"line": 7
},
{
"type": "Text",
"val": "Line 4",
"line": 7
}
],
"line": 3,
"filename": "filters.custom.tokens.json"
},
"attrs": [
{
"name": "opt",
"val": "'val'",
"mustEscape": true
},
{
"name": "num",
"val": "2",
"mustEscape": true
}
],
"line": 3,
"filename": "filters.custom.tokens.json"
}
],
"line": 2,
"filename": "filters.custom.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 2,
"filename": "filters.custom.tokens.json"
}
],
"line": 1,
"filename": "filters.custom.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 1,
"filename": "filters.custom.tokens.json"
}
],
"line": 0,
"filename": "filters.custom.tokens.json"
}

View File

@@ -0,0 +1,91 @@
{
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "html",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "body",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "pre",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "RawInclude",
"file": {
"type": "FileReference",
"line": 4,
"filename": "filters.include.custom.tokens.json",
"path": "filters.include.custom.pug",
"fullPath": "test/cases/filters.include.custom.pug",
"str": "html\n body\n pre\n include:custom(opt='val' num=2) filters.include.custom.pug\n"
},
"line": 4,
"filename": "filters.include.custom.tokens.json",
"filters": [
{
"type": "IncludeFilter",
"name": "custom",
"attrs": [
{
"name": "opt",
"val": "'val'",
"mustEscape": true
},
{
"name": "num",
"val": "2",
"mustEscape": true
}
],
"line": 4,
"filename": "filters.include.custom.tokens.json"
}
]
}
],
"line": 3,
"filename": "filters.include.custom.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 3,
"filename": "filters.include.custom.tokens.json"
}
],
"line": 2,
"filename": "filters.include.custom.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 2,
"filename": "filters.include.custom.tokens.json"
}
],
"line": 1,
"filename": "filters.include.custom.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 1,
"filename": "filters.include.custom.tokens.json"
}
],
"line": 0,
"filename": "filters.include.custom.tokens.json"
}

View File

@@ -0,0 +1,4 @@
html
body
pre
include:custom(opt='val' num=2) filters.include.custom.pug

View File

@@ -0,0 +1,160 @@
{
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "html",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "body",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "RawInclude",
"file": {
"type": "FileReference",
"line": 3,
"filename": "filters.include.tokens.json",
"path": "some.md",
"fullPath": "test/cases/some.md",
"str": "Just _some_ markdown **tests**.\n\nWith new line.\n"
},
"line": 3,
"filename": "filters.include.tokens.json",
"filters": [
{
"type": "IncludeFilter",
"name": "markdown-it",
"attrs": [],
"line": 3,
"filename": "filters.include.tokens.json"
}
]
},
{
"type": "Tag",
"name": "script",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "RawInclude",
"file": {
"type": "FileReference",
"line": 5,
"filename": "filters.include.tokens.json",
"path": "include-filter-coffee.coffee",
"fullPath": "test/cases/include-filter-coffee.coffee",
"str": "math =\n square: (value) -> value * value\n"
},
"line": 5,
"filename": "filters.include.tokens.json",
"filters": [
{
"type": "IncludeFilter",
"name": "coffee-script",
"attrs": [
{
"name": "minify",
"val": "true",
"mustEscape": true
}
],
"line": 5,
"filename": "filters.include.tokens.json"
}
]
}
],
"line": 4,
"filename": "filters.include.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 4,
"filename": "filters.include.tokens.json"
},
{
"type": "Tag",
"name": "script",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "RawInclude",
"file": {
"type": "FileReference",
"line": 7,
"filename": "filters.include.tokens.json",
"path": "include-filter-coffee.coffee",
"fullPath": "test/cases/include-filter-coffee.coffee",
"str": "math =\n square: (value) -> value * value\n"
},
"line": 7,
"filename": "filters.include.tokens.json",
"filters": [
{
"type": "IncludeFilter",
"name": "cdata",
"attrs": [],
"line": 7,
"filename": "filters.include.tokens.json"
},
{
"type": "IncludeFilter",
"name": "coffee-script",
"attrs": [
{
"name": "minify",
"val": "false",
"mustEscape": true
}
],
"line": 7,
"filename": "filters.include.tokens.json"
}
]
}
],
"line": 6,
"filename": "filters.include.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 6,
"filename": "filters.include.tokens.json"
}
],
"line": 2,
"filename": "filters.include.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 2,
"filename": "filters.include.tokens.json"
}
],
"line": 1,
"filename": "filters.include.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 1,
"filename": "filters.include.tokens.json"
}
],
"line": 0,
"filename": "filters.include.tokens.json"
}

View File

@@ -0,0 +1,56 @@
{
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "p",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "before ",
"line": 1,
"filename": "filters.inline.tokens.json"
},
{
"type": "Filter",
"name": "cdata",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "inside",
"line": 1,
"filename": "filters.inline.tokens.json"
}
],
"line": 1,
"filename": "filters.inline.tokens.json"
},
"attrs": [],
"line": 1,
"filename": "filters.inline.tokens.json"
},
{
"type": "Text",
"val": " after",
"line": 1,
"filename": "filters.inline.tokens.json"
}
],
"line": 1,
"filename": "filters.inline.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 1,
"filename": "filters.inline.tokens.json"
}
],
"line": 0,
"filename": "filters.inline.tokens.json"
}

View File

@@ -0,0 +1,113 @@
{
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "html",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "head",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "style",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "less",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "@pad: 15px;",
"line": 5
},
{
"type": "Text",
"val": "\n",
"line": 6
},
{
"type": "Text",
"val": "body {",
"line": 6
},
{
"type": "Text",
"val": "\n",
"line": 7
},
{
"type": "Text",
"val": " padding: @pad;",
"line": 7
},
{
"type": "Text",
"val": "\n",
"line": 8
},
{
"type": "Text",
"val": "}",
"line": 8
}
],
"line": 4,
"filename": "filters.less.tokens.json"
},
"attrs": [],
"line": 4,
"filename": "filters.less.tokens.json"
}
],
"line": 3,
"filename": "filters.less.tokens.json"
},
"attrs": [
{
"name": "type",
"val": "\"text/css\"",
"mustEscape": true
}
],
"attributeBlocks": [],
"isInline": false,
"line": 3,
"filename": "filters.less.tokens.json"
}
],
"line": 2,
"filename": "filters.less.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 2,
"filename": "filters.less.tokens.json"
}
],
"line": 1,
"filename": "filters.less.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 1,
"filename": "filters.less.tokens.json"
}
],
"line": 0,
"filename": "filters.less.tokens.json"
}

View File

@@ -0,0 +1,70 @@
{
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "html",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "body",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "markdown-it",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "This is _some_ awesome **markdown**",
"line": 4
},
{
"type": "Text",
"val": "\n",
"line": 5
},
{
"type": "Text",
"val": "whoop.",
"line": 5
}
],
"line": 3,
"filename": "filters.markdown.tokens.json"
},
"attrs": [],
"line": 3,
"filename": "filters.markdown.tokens.json"
}
],
"line": 2,
"filename": "filters.markdown.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 2,
"filename": "filters.markdown.tokens.json"
}
],
"line": 1,
"filename": "filters.markdown.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 1,
"filename": "filters.markdown.tokens.json"
}
],
"line": 0,
"filename": "filters.markdown.tokens.json"
}

View File

@@ -0,0 +1,161 @@
{
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "script",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "cdata",
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "uglify-js",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "(function() {",
"line": 3
},
{
"type": "Text",
"val": "\n",
"line": 4
},
{
"type": "Text",
"val": " console.log('test')",
"line": 4
},
{
"type": "Text",
"val": "\n",
"line": 5
},
{
"type": "Text",
"val": "})()",
"line": 5
}
],
"line": 2,
"filename": "filters.nested.tokens.json"
},
"attrs": [],
"line": 2,
"filename": "filters.nested.tokens.json"
}
],
"line": 2,
"filename": "filters.nested.tokens.json"
},
"attrs": [],
"line": 2,
"filename": "filters.nested.tokens.json"
}
],
"line": 1,
"filename": "filters.nested.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 1,
"filename": "filters.nested.tokens.json"
},
{
"type": "Tag",
"name": "script",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "cdata",
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "uglify-js",
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "coffee-script",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "(->",
"line": 8
},
{
"type": "Text",
"val": "\n",
"line": 9
},
{
"type": "Text",
"val": " console.log 'test'",
"line": 9
},
{
"type": "Text",
"val": "\n",
"line": 10
},
{
"type": "Text",
"val": ")()",
"line": 10
}
],
"line": 7,
"filename": "filters.nested.tokens.json"
},
"attrs": [],
"line": 7,
"filename": "filters.nested.tokens.json"
}
],
"line": 7,
"filename": "filters.nested.tokens.json"
},
"attrs": [],
"line": 7,
"filename": "filters.nested.tokens.json"
}
],
"line": 7,
"filename": "filters.nested.tokens.json"
},
"attrs": [],
"line": 7,
"filename": "filters.nested.tokens.json"
}
],
"line": 6,
"filename": "filters.nested.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 6,
"filename": "filters.nested.tokens.json"
}
],
"line": 0,
"filename": "filters.nested.tokens.json"
}

View File

@@ -0,0 +1,109 @@
{
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "html",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "head",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Tag",
"name": "style",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [
{
"type": "Filter",
"name": "stylus",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "body",
"line": 5
},
{
"type": "Text",
"val": "\n",
"line": 6
},
{
"type": "Text",
"val": " padding: 50px",
"line": 6
}
],
"line": 4,
"filename": "filters.stylus.tokens.json"
},
"attrs": [],
"line": 4,
"filename": "filters.stylus.tokens.json"
}
],
"line": 3,
"filename": "filters.stylus.tokens.json"
},
"attrs": [
{
"name": "type",
"val": "\"text/css\"",
"mustEscape": true
}
],
"attributeBlocks": [],
"isInline": false,
"line": 3,
"filename": "filters.stylus.tokens.json"
}
],
"line": 2,
"filename": "filters.stylus.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 2,
"filename": "filters.stylus.tokens.json"
},
{
"type": "Tag",
"name": "body",
"selfClosing": false,
"block": {
"type": "Block",
"nodes": [],
"line": 7,
"filename": "filters.stylus.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 7,
"filename": "filters.stylus.tokens.json"
}
],
"line": 1,
"filename": "filters.stylus.tokens.json"
},
"attrs": [],
"attributeBlocks": [],
"isInline": false,
"line": 1,
"filename": "filters.stylus.tokens.json"
}
],
"line": 0,
"filename": "filters.stylus.tokens.json"
}

View File

@@ -0,0 +1,2 @@
math =
square: (value) -> value * value

View File

@@ -0,0 +1,3 @@
Just _some_ markdown **tests**.
With new line.

View File

@@ -0,0 +1,9 @@
var assert = require('assert');
module.exports = {
custom: function(str, options) {
expect(options.opt).toBe('val');
expect(options.num).toBe(2);
return 'BEGIN' + str + 'END';
},
};

View File

@@ -0,0 +1,3 @@
- var opt = 'a'
:cdata(option=opt)
hey

View File

@@ -0,0 +1,37 @@
{
"type": "Block",
"nodes": [
{
"type": "Code",
"val": "var opt = 'a'",
"buffer": false,
"escape": false,
"isInline": false,
"line": 1
},
{
"type": "Filter",
"name": "cdata",
"block": {
"type": "Block",
"nodes": [
{
"type": "Text",
"val": "hey",
"line": 3
}
],
"line": 2
},
"attrs": [
{
"name": "option",
"val": "opt",
"escaped": true
}
],
"line": 2
}
],
"line": 0
}

View File

@@ -0,0 +1,88 @@
const lex = require('pug-lexer');
const parse = require('pug-parser');
const handleFilters = require('../').handleFilters;
const customFilters = {};
test('filters can be aliased', () => {
const source = `
script
:cdata:minify
function myFunc(foo) {
return foo;
}
`;
const ast = parse(lex(source, {filename: __filename}), {
filename: __filename,
src: source,
});
const options = {};
const aliases = {
minify: 'uglify-js',
};
const output = handleFilters(ast, customFilters, options, aliases);
expect(output).toMatchSnapshot();
});
test('we do not support chains of aliases', () => {
const source = `
script
:cdata:minify-js
function myFunc(foo) {
return foo;
}
`;
const ast = parse(lex(source, {filename: __filename}), {
filename: __filename,
src: source,
});
const options = {};
const aliases = {
'minify-js': 'minify',
minify: 'uglify-js',
};
try {
const output = handleFilters(ast, customFilters, options, aliases);
} catch (ex) {
expect({
code: ex.code,
message: ex.message,
}).toMatchSnapshot();
return;
}
throw new Error('Expected an exception');
});
test('options are applied before aliases', () => {
const source = `
script
:cdata:minify
function myFunc(foo) {
return foo;
}
:cdata:uglify-js
function myFunc(foo) {
return foo;
}
`;
const ast = parse(lex(source, {filename: __filename}), {
filename: __filename,
src: source,
});
const options = {
minify: {output: {beautify: true}},
};
const aliases = {
minify: 'uglify-js',
};
const output = handleFilters(ast, customFilters, options, aliases);
expect(output).toMatchSnapshot();
});

View File

@@ -0,0 +1,55 @@
'use strict';
var fs = require('fs');
var assert = require('assert');
var handleFilters = require('../').handleFilters;
var customFilters = require('./custom-filters.js');
process.chdir(__dirname + '/../');
var testCases;
testCases = fs.readdirSync(__dirname + '/cases').filter(function(name) {
return /\.input\.json$/.test(name);
});
//
testCases.forEach(function(filename) {
function read(path) {
return fs.readFileSync(__dirname + '/cases/' + path, 'utf8');
}
test('cases/' + filename, function() {
var actualAst = JSON.stringify(
handleFilters(JSON.parse(read(filename)), customFilters),
null,
' '
);
expect(actualAst).toMatchSnapshot();
});
});
testCases = fs.readdirSync(__dirname + '/errors').filter(function(name) {
return /\.input\.json$/.test(name);
});
testCases.forEach(function(filename) {
function read(path) {
return fs.readFileSync(__dirname + '/errors/' + path, 'utf8');
}
test('errors/' + filename, function() {
var actual;
try {
handleFilters(JSON.parse(read(filename)), customFilters);
throw new Error('Expected ' + filename + ' to throw an exception.');
} catch (ex) {
if (!ex || !ex.code || ex.code.indexOf('PUG:') !== 0) throw ex;
actual = {
msg: ex.msg,
code: ex.code,
line: ex.line,
};
}
expect(actual).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,28 @@
const lex = require('pug-lexer');
const parse = require('pug-parser');
const handleFilters = require('../').handleFilters;
const customFilters = {};
test('per filter options are applied, even to nested filters', () => {
const source = `
script
:cdata:uglify-js
function myFunc(foo) {
return foo;
}
`;
const ast = parse(lex(source, {filename: __filename}), {
filename: __filename,
src: source,
});
const options = {
'uglify-js': {output: {beautify: true}},
};
const output = handleFilters(ast, customFilters, options);
expect(output).toMatchSnapshot();
// TODO: render with `options.filterOptions['uglify-js']`
});

View File

@@ -0,0 +1,3 @@
- var avatar = '219b77f9d21de75e81851b6b886057c7'
div.avatar-div(style=`background-image: url(https://www.gravatar.com/avatar/${avatar})`)

View File

@@ -0,0 +1,7 @@
- var user = { name: 'tobi' }
foo(data-user=user)
foo(data-items=[1,2,3])
foo(data-username='tobi')
foo(data-escaped={message: "Let's rock!"})
foo(data-ampersand={message: "a quote: &quot; this & that"})
foo(data-epoc=new Date(0))

View File

@@ -0,0 +1,22 @@
- var id = 5
- function answer() { return 42; }
a(href='/user/' + id, class='button')
a(href = '/user/' + id, class = 'button')
meta(key='answer', value=answer())
a(class = ['class1', 'class2'])
a.tag-class(class = ['class1', 'class2'])
a(href='/user/' + id class='button')
a(href = '/user/' + id class = 'button')
meta(key='answer' value=answer())
a(class = ['class1', 'class2'])
a.tag-class(class = ['class1', 'class2'])
div(id=id)&attributes({foo: 'bar'})
- var bar = null
div(foo=null bar=bar)&attributes({baz: 'baz'})
div(...object)
div(...object after="after")
div(before="before" ...object)
div(before="before" ...object after="after")

View File

@@ -0,0 +1,43 @@
a(href='/contact') contact
a(href='/save').button save
a(foo, bar, baz)
a(foo='foo, bar, baz', bar=1)
a(foo='((foo))', bar= (1) ? 1 : 0 )
select
option(value='foo', selected) Foo
option(selected, value='bar') Bar
a(foo="class:")
input(pattern='\\S+')
a(href='/contact') contact
a(href='/save').button save
a(foo bar baz)
a(foo='foo, bar, baz' bar=1)
a(foo='((foo))' bar= (1) ? 1 : 0 )
select
option(value='foo' selected) Foo
option(selected value='bar') Bar
a(foo="class:")
input(pattern='\\S+')
foo(terse="true")
foo(date=new Date(0))
foo(abc
,def)
foo(abc,
def)
foo(abc,
def)
foo(abc
,def)
foo(abc
def)
foo(abc
def)
- var attrs = {foo: 'bar', bar: '<baz>'}
div&attributes(attrs)
a(foo='foo' "bar"="bar")
a(foo='foo' 'bar'='bar')

View File

@@ -0,0 +1,3 @@
script(type='text/x-template')
div(id!='user-<%= user.id %>')
h1 <%= user.title %>

View File

@@ -0,0 +1,3 @@
html
body
h1 Title

View File

@@ -0,0 +1,8 @@
ul
li foo
li bar
li baz

View File

@@ -0,0 +1,12 @@
-
list = ["uno", "dos", "tres",
"cuatro", "cinco", "seis"];
//- Without a block, the element is accepted and no code is generated
-
each item in list
-
string = item.charAt(0)
.toUpperCase() +
item.slice(1);
li= string

View File

@@ -0,0 +1,5 @@
ul
li: a(href='#') foo
li: a(href='#') bar
p baz

View File

@@ -0,0 +1,2 @@
ul
li.list-item: .foo: #bar baz

View File

@@ -0,0 +1,4 @@
figure
blockquote
| Try to define yourself by what you do, and you&#8217;ll burnout every time. You are. That is enough. I rest in that.
figcaption from @thefray at 1:43pm on May 10

View File

@@ -0,0 +1,4 @@
extends ./auxiliary/blocks-in-blocks-layout.pug
block body
h1 Page 2

View File

@@ -0,0 +1,19 @@
//- see https://github.com/pugjs/pug/issues/1589
-var ajax = true
-if( ajax )
//- return only contents if ajax requests
block contents
p ajax contents
-else
//- return all html
doctype html
html
head
meta( charset='utf8' )
title sample
body
block contents
p all contetns

View File

@@ -0,0 +1,10 @@
html
body
- var friends = 1
case friends
when 0
p you have no friends
when 1
p you have a friend
default
p you have #{friends} friends

View File

@@ -0,0 +1,19 @@
html
body
- var friends = 1
case friends
when 0: p you have no friends
when 1: p you have a friend
default: p you have #{friends} friends
- var friends = 0
case friends
when 0
when 1
p you have very few friends
default
p you have #{friends} friends
- var friend = 'Tim:G'
case friend
when 'Tim:G': p Friend is a string
when {tim: 'g'}: p Friend is an object

View File

@@ -0,0 +1,3 @@
a(class='')
a(class=null)
a(class=undefined)

View File

@@ -0,0 +1,14 @@
a(class=['foo', 'bar', 'baz'])
a.foo(class='bar').baz
a.foo-bar_baz
a(class={foo: true, bar: false, baz: true})
a.-foo
a.3foo

View File

@@ -0,0 +1,43 @@
- if (true)
p foo
- else
p bar
- if (true) {
p foo
- } else {
p bar
- }
if true
p foo
p bar
p baz
else
p bar
unless true
p foo
else
p bar
if 'nested'
if 'works'
p yay
//- allow empty blocks
if false
else
.bar
if true
.bar
else
.bing
if false
.bing
else if false
.bar
else
.foo

View File

@@ -0,0 +1,2 @@
p= '<script>'
p!= '<script>'

View File

@@ -0,0 +1,35 @@
- var items = [1,2,3]
ul
- items.forEach(function(item){
li= item
- })
- var items = [1,2,3]
ul
for item, i in items
li(class='item-' + i)= item
ul
each item, i in items
li= item
ul
each $item in items
li= $item
- var nums = [1, 2, 3]
- var letters = ['a', 'b', 'c']
ul
for l in letters
for n in nums
li #{n}: #{l}
- var count = 1
- var counter = function() { return [count++, count++, count++] }
ul
for n in counter()
li #{n}

View File

@@ -0,0 +1,10 @@
p= null
p= undefined
p= ''
p= 0
p= false
p(foo=null)
p(foo=undefined)
p(foo='')
p(foo=0)
p(foo=false)

View File

@@ -0,0 +1,10 @@
doctype html
html
body
- var s = 'this'
case s
//- Comment
when 'this'
p It's this!
when 'that'
p It's that!

View File

@@ -0,0 +1,29 @@
// foo
ul
// bar
li one
// baz
li two
//
ul
li foo
// block
// inline follow
li three
// block
// inline followed by tags
ul
li four
//if IE lt 9
// inline
script(src='/lame.js')
// end-inline
p five
.foo // not a comment

View File

@@ -0,0 +1,9 @@
//-
s/s.
//- test/cases/comments.source.pug
//-
test/cases/comments.source.pug
when
()

View File

@@ -0,0 +1 @@
doctype custom stuff

View File

@@ -0,0 +1,4 @@
doctype
html
body
h1 Title

View File

@@ -0,0 +1 @@
doctype html

View File

@@ -0,0 +1,52 @@
- var users = []
ul
for user in users
li= user.name
else
li no users!
- var users = [{ name: 'tobi', friends: ['loki'] }, { name: 'loki' }]
if users
ul
for user in users
li= user.name
else
li no users!
- var user = { name: 'tobi', age: 10 }
ul
each val, key in user
li #{key}: #{val}
else
li user has no details!
- var user = {}
ul
each prop, key in user
li #{key}: #{val}
else
li user has no details!
- var user = Object.create(null)
- user.name = 'tobi'
ul
each val, key in user
li #{key}: #{val}
else
li user has no details!
- var ofKeyword = [{ name: 'tobi', friends: ['loki'] }, { name: 'loki' }]
ul
each val of ofKeyword
li= user.name
ul
each val of ["variable with of keyword"]
li= val

View File

@@ -0,0 +1,2 @@
script.
var re = /\d+/;

View File

@@ -0,0 +1,8 @@
doctype html
html
head
title escape-test
body
textarea
- var txt = '<param name="flashvars" value="a=&quot;value_a&quot;&b=&quot;value_b&quot;&c=3"/>'
| #{txt}

Some files were not shown because too many files have changed in this diff Show More