follow PugJs
This commit is contained in:
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"mcp__acp__Bash",
|
|
||||||
"mcp__acp__Write",
|
|
||||||
"mcp__acp__Edit"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
460
CLAUDE.md
@@ -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
|
||||||
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
93
build.zig
93
build.zig
@@ -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.
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
15
examples/demo/views/mixin-test.pug
Normal file
15
examples/demo/views/mixin-test.pug
Normal 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")
|
||||||
257
src/ast.zig
257
src/ast.zig
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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
1423
src/codegen.zig
1423
src/codegen.zig
File diff suppressed because it is too large
Load Diff
403
src/error.zig
Normal file
403
src/error.zig
Normal 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]);
|
||||||
|
}
|
||||||
4238
src/lexer.zig
4238
src/lexer.zig
File diff suppressed because it is too large
Load Diff
699
src/linker.zig
Normal file
699
src/linker.zig
Normal 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
412
src/load.zig
Normal 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
581
src/mixin.zig
Normal 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);
|
||||||
|
}
|
||||||
2813
src/parser.zig
2813
src/parser.zig
File diff suppressed because it is too large
Load Diff
BIN
src/playground/benchmark
Executable file
BIN
src/playground/benchmark
Executable file
Binary file not shown.
66
src/playground/benchmark.zig
Normal file
66
src/playground/benchmark.zig
Normal 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);
|
||||||
|
}
|
||||||
274
src/playground/benchmark_examples.zig
Normal file
274
src/playground/benchmark_examples.zig
Normal 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", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/playground/examples/attributes.pug
Normal file
8
src/playground/examples/attributes.pug
Normal 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')
|
||||||
17
src/playground/examples/code.pug
Normal file
17
src/playground/examples/code.pug
Normal 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}
|
||||||
5
src/playground/examples/dynamicscript.pug
Normal file
5
src/playground/examples/dynamicscript.pug
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
html
|
||||||
|
head
|
||||||
|
title Dynamic Inline JavaScript
|
||||||
|
script.
|
||||||
|
var users = !{JSON.stringify(users).replace(/<\//g, "<\\/")}
|
||||||
3
src/playground/examples/each.pug
Normal file
3
src/playground/examples/each.pug
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ul#users
|
||||||
|
each user, name in users
|
||||||
|
li(class='user-' + name) #{name} #{user.email}
|
||||||
10
src/playground/examples/extend-layout.pug
Normal file
10
src/playground/examples/extend-layout.pug
Normal 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
|
||||||
11
src/playground/examples/extend.pug
Normal file
11
src/playground/examples/extend.pug
Normal 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
|
||||||
29
src/playground/examples/form.pug
Normal file
29
src/playground/examples/form.pug
Normal 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")
|
||||||
7
src/playground/examples/includes.pug
Normal file
7
src/playground/examples/includes.pug
Normal 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
|
||||||
14
src/playground/examples/layout.pug
Normal file
14
src/playground/examples/layout.pug
Normal 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).
|
||||||
14
src/playground/examples/mixins.pug
Normal file
14
src/playground/examples/mixins.pug
Normal 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)
|
||||||
3
src/playground/examples/pet.pug
Normal file
3
src/playground/examples/pet.pug
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.pet
|
||||||
|
h2= pet.name
|
||||||
|
p #{pet.name} is <em>#{pet.age}</em> year(s) old.
|
||||||
14
src/playground/examples/rss.pug
Normal file
14
src/playground/examples/rss.pug
Normal 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
|
||||||
36
src/playground/examples/text.pug
Normal file
36
src/playground/examples/text.pug
Normal 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.
|
||||||
11
src/playground/examples/whitespace.pug
Normal file
11
src/playground/examples/whitespace.pug
Normal 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
70
src/playground/run_js.js
Normal 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
0
src/playground/run_zig
Executable file
120
src/playground/run_zig.zig
Normal file
120
src/playground/run_zig.zig
Normal 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
457
src/pug.zig
Normal 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);
|
||||||
|
}
|
||||||
82
src/root.zig
82
src/root.zig
@@ -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
118
src/run_playground.zig
Normal 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))});
|
||||||
|
}
|
||||||
|
}
|
||||||
3820
src/runtime.zig
3820
src/runtime.zig
File diff suppressed because it is too large
Load Diff
353
src/strip_comments.zig
Normal file
353
src/strip_comments.zig
Normal 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
683
src/template.zig
Normal 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, "<"),
|
||||||
|
'>' => try output.appendSlice(allocator, ">"),
|
||||||
|
'&' => {
|
||||||
|
if (isHtmlEntity(text[i..])) {
|
||||||
|
try output.append(allocator, c);
|
||||||
|
} else {
|
||||||
|
try output.appendSlice(allocator, "&");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 ’
|
||||||
|
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, "<"),
|
||||||
|
'>' => try output.appendSlice(allocator, ">"),
|
||||||
|
'&' => {
|
||||||
|
if (isHtmlEntity(str[i..])) {
|
||||||
|
try output.append(allocator, c);
|
||||||
|
} else {
|
||||||
|
try output.appendSlice(allocator, "&");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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><b>bold</b></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);
|
||||||
|
}
|
||||||
301
src/test-data/pug-attrs/index.test.js
Normal file
301
src/test-data/pug-attrs/index.test.js
Normal 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="<foo> <str>"',
|
||||||
|
{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="<foo>"'
|
||||||
|
);
|
||||||
|
test([{name: 'foo', val: 'foo', mustEscape: false}], ' foo="<foo>"', {
|
||||||
|
foo: '<foo>',
|
||||||
|
});
|
||||||
|
test([{name: 'foo', val: 'foo', mustEscape: true}], ' foo="<foo>"', {
|
||||||
|
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: '<foo> <str>'},
|
||||||
|
{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: '<foo>',
|
||||||
|
});
|
||||||
|
test(
|
||||||
|
[{name: 'foo', val: 'foo', mustEscape: false}],
|
||||||
|
{foo: '<foo>'},
|
||||||
|
{foo: '<foo>'}
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
[{name: 'foo', val: 'foo', mustEscape: true}],
|
||||||
|
{foo: '<foo>'},
|
||||||
|
{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}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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.",
|
||||||
|
}
|
||||||
|
`;
|
||||||
1074
src/test-data/pug-filters/test/__snapshots__/index.test.js.snap
Normal file
1074
src/test-data/pug-filters/test/__snapshots__/index.test.js.snap
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
101
src/test-data/pug-filters/test/cases/filters.custom.input.json
Normal file
101
src/test-data/pug-filters/test/cases/filters.custom.input.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
html
|
||||||
|
body
|
||||||
|
pre
|
||||||
|
include:custom(opt='val' num=2) filters.include.custom.pug
|
||||||
160
src/test-data/pug-filters/test/cases/filters.include.input.json
Normal file
160
src/test-data/pug-filters/test/cases/filters.include.input.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
113
src/test-data/pug-filters/test/cases/filters.less.input.json
Normal file
113
src/test-data/pug-filters/test/cases/filters.less.input.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
161
src/test-data/pug-filters/test/cases/filters.nested.input.json
Normal file
161
src/test-data/pug-filters/test/cases/filters.nested.input.json
Normal 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"
|
||||||
|
}
|
||||||
109
src/test-data/pug-filters/test/cases/filters.stylus.input.json
Normal file
109
src/test-data/pug-filters/test/cases/filters.stylus.input.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
math =
|
||||||
|
square: (value) -> value * value
|
||||||
3
src/test-data/pug-filters/test/cases/some.md
Normal file
3
src/test-data/pug-filters/test/cases/some.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Just _some_ markdown **tests**.
|
||||||
|
|
||||||
|
With new line.
|
||||||
9
src/test-data/pug-filters/test/custom-filters.js
Normal file
9
src/test-data/pug-filters/test/custom-filters.js
Normal 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';
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- var opt = 'a'
|
||||||
|
:cdata(option=opt)
|
||||||
|
hey
|
||||||
@@ -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
|
||||||
|
}
|
||||||
88
src/test-data/pug-filters/test/filter-aliases.test.js
Normal file
88
src/test-data/pug-filters/test/filter-aliases.test.js
Normal 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();
|
||||||
|
});
|
||||||
55
src/test-data/pug-filters/test/index.test.js
Normal file
55
src/test-data/pug-filters/test/index.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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']`
|
||||||
|
});
|
||||||
3
src/test-data/pug-lexer/cases/attr-es2015.pug
Normal file
3
src/test-data/pug-lexer/cases/attr-es2015.pug
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
- var avatar = '219b77f9d21de75e81851b6b886057c7'
|
||||||
|
|
||||||
|
div.avatar-div(style=`background-image: url(https://www.gravatar.com/avatar/${avatar})`)
|
||||||
7
src/test-data/pug-lexer/cases/attrs-data.pug
Normal file
7
src/test-data/pug-lexer/cases/attrs-data.pug
Normal 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: " this & that"})
|
||||||
|
foo(data-epoc=new Date(0))
|
||||||
22
src/test-data/pug-lexer/cases/attrs.js.pug
Normal file
22
src/test-data/pug-lexer/cases/attrs.js.pug
Normal 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")
|
||||||
43
src/test-data/pug-lexer/cases/attrs.pug
Normal file
43
src/test-data/pug-lexer/cases/attrs.pug
Normal 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')
|
||||||
3
src/test-data/pug-lexer/cases/attrs.unescaped.pug
Normal file
3
src/test-data/pug-lexer/cases/attrs.unescaped.pug
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
script(type='text/x-template')
|
||||||
|
div(id!='user-<%= user.id %>')
|
||||||
|
h1 <%= user.title %>
|
||||||
3
src/test-data/pug-lexer/cases/basic.pug
Normal file
3
src/test-data/pug-lexer/cases/basic.pug
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
html
|
||||||
|
body
|
||||||
|
h1 Title
|
||||||
8
src/test-data/pug-lexer/cases/blanks.pug
Normal file
8
src/test-data/pug-lexer/cases/blanks.pug
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
ul
|
||||||
|
li foo
|
||||||
|
|
||||||
|
li bar
|
||||||
|
|
||||||
|
li baz
|
||||||
12
src/test-data/pug-lexer/cases/block-code.pug
Normal file
12
src/test-data/pug-lexer/cases/block-code.pug
Normal 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
|
||||||
5
src/test-data/pug-lexer/cases/block-expansion.pug
Normal file
5
src/test-data/pug-lexer/cases/block-expansion.pug
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ul
|
||||||
|
li: a(href='#') foo
|
||||||
|
li: a(href='#') bar
|
||||||
|
|
||||||
|
p baz
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ul
|
||||||
|
li.list-item: .foo: #bar baz
|
||||||
4
src/test-data/pug-lexer/cases/blockquote.pug
Normal file
4
src/test-data/pug-lexer/cases/blockquote.pug
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
figure
|
||||||
|
blockquote
|
||||||
|
| Try to define yourself by what you do, and you’ll burnout every time. You are. That is enough. I rest in that.
|
||||||
|
figcaption from @thefray at 1:43pm on May 10
|
||||||
4
src/test-data/pug-lexer/cases/blocks-in-blocks.pug
Normal file
4
src/test-data/pug-lexer/cases/blocks-in-blocks.pug
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
extends ./auxiliary/blocks-in-blocks-layout.pug
|
||||||
|
|
||||||
|
block body
|
||||||
|
h1 Page 2
|
||||||
19
src/test-data/pug-lexer/cases/blocks-in-if.pug
Normal file
19
src/test-data/pug-lexer/cases/blocks-in-if.pug
Normal 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
|
||||||
10
src/test-data/pug-lexer/cases/case-blocks.pug
Normal file
10
src/test-data/pug-lexer/cases/case-blocks.pug
Normal 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
|
||||||
19
src/test-data/pug-lexer/cases/case.pug
Normal file
19
src/test-data/pug-lexer/cases/case.pug
Normal 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
|
||||||
3
src/test-data/pug-lexer/cases/classes-empty.pug
Normal file
3
src/test-data/pug-lexer/cases/classes-empty.pug
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
a(class='')
|
||||||
|
a(class=null)
|
||||||
|
a(class=undefined)
|
||||||
14
src/test-data/pug-lexer/cases/classes.pug
Normal file
14
src/test-data/pug-lexer/cases/classes.pug
Normal 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
|
||||||
43
src/test-data/pug-lexer/cases/code.conditionals.pug
Normal file
43
src/test-data/pug-lexer/cases/code.conditionals.pug
Normal 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
|
||||||
2
src/test-data/pug-lexer/cases/code.escape.pug
Normal file
2
src/test-data/pug-lexer/cases/code.escape.pug
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
p= '<script>'
|
||||||
|
p!= '<script>'
|
||||||
35
src/test-data/pug-lexer/cases/code.iteration.pug
Normal file
35
src/test-data/pug-lexer/cases/code.iteration.pug
Normal 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}
|
||||||
10
src/test-data/pug-lexer/cases/code.pug
Normal file
10
src/test-data/pug-lexer/cases/code.pug
Normal 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)
|
||||||
10
src/test-data/pug-lexer/cases/comments-in-case.pug
Normal file
10
src/test-data/pug-lexer/cases/comments-in-case.pug
Normal 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!
|
||||||
29
src/test-data/pug-lexer/cases/comments.pug
Normal file
29
src/test-data/pug-lexer/cases/comments.pug
Normal 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
|
||||||
9
src/test-data/pug-lexer/cases/comments.source.pug
Normal file
9
src/test-data/pug-lexer/cases/comments.source.pug
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//-
|
||||||
|
s/s.
|
||||||
|
|
||||||
|
//- test/cases/comments.source.pug
|
||||||
|
|
||||||
|
//-
|
||||||
|
test/cases/comments.source.pug
|
||||||
|
when
|
||||||
|
()
|
||||||
1
src/test-data/pug-lexer/cases/doctype.custom.pug
Normal file
1
src/test-data/pug-lexer/cases/doctype.custom.pug
Normal file
@@ -0,0 +1 @@
|
|||||||
|
doctype custom stuff
|
||||||
4
src/test-data/pug-lexer/cases/doctype.default.pug
Normal file
4
src/test-data/pug-lexer/cases/doctype.default.pug
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
doctype
|
||||||
|
html
|
||||||
|
body
|
||||||
|
h1 Title
|
||||||
1
src/test-data/pug-lexer/cases/doctype.keyword.pug
Normal file
1
src/test-data/pug-lexer/cases/doctype.keyword.pug
Normal file
@@ -0,0 +1 @@
|
|||||||
|
doctype html
|
||||||
52
src/test-data/pug-lexer/cases/each.else.pug
Normal file
52
src/test-data/pug-lexer/cases/each.else.pug
Normal 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
|
||||||
2
src/test-data/pug-lexer/cases/escape-chars.pug
Normal file
2
src/test-data/pug-lexer/cases/escape-chars.pug
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
script.
|
||||||
|
var re = /\d+/;
|
||||||
8
src/test-data/pug-lexer/cases/escape-test.pug
Normal file
8
src/test-data/pug-lexer/cases/escape-test.pug
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
doctype html
|
||||||
|
html
|
||||||
|
head
|
||||||
|
title escape-test
|
||||||
|
body
|
||||||
|
textarea
|
||||||
|
- var txt = '<param name="flashvars" value="a="value_a"&b="value_b"&c=3"/>'
|
||||||
|
| #{txt}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user