314 lines
9.6 KiB
Zig
314 lines
9.6 KiB
Zig
|
|
//! 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,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Text content node.
|
||
|
|
pub const Text = struct {
|
||
|
|
/// Segments of text (literals and interpolations).
|
||
|
|
segments: []TextSegment,
|
||
|
|
/// Whether this is from pipe syntax `|`.
|
||
|
|
is_piped: bool,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// 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,
|
||
|
|
};
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
// AST Builder Helpers
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
/// Creates an empty document node.
|
||
|
|
pub fn emptyDocument() Document {
|
||
|
|
return .{
|
||
|
|
.nodes = &.{},
|
||
|
|
.extends_path = null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Creates a simple element with just a tag name.
|
||
|
|
pub fn simpleElement(tag: []const u8) Element {
|
||
|
|
return .{
|
||
|
|
.tag = tag,
|
||
|
|
.classes = &.{},
|
||
|
|
.id = null,
|
||
|
|
.attributes = &.{},
|
||
|
|
.children = &.{},
|
||
|
|
.self_closing = false,
|
||
|
|
.inline_text = null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Creates a text node from a single literal string.
|
||
|
|
/// Note: The returned Text has a pointer to static memory for segments.
|
||
|
|
/// For dynamic text, allocate segments separately.
|
||
|
|
pub fn literalText(allocator: std.mem.Allocator, content: []const u8) !Text {
|
||
|
|
const segments = try allocator.alloc(TextSegment, 1);
|
||
|
|
segments[0] = .{ .literal = content };
|
||
|
|
return .{
|
||
|
|
.segments = segments,
|
||
|
|
.is_piped = false,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
// Tests
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
test "create simple element" {
|
||
|
|
const elem = simpleElement("div");
|
||
|
|
try std.testing.expectEqualStrings("div", elem.tag);
|
||
|
|
try std.testing.expectEqual(@as(usize, 0), elem.children.len);
|
||
|
|
}
|
||
|
|
|
||
|
|
test "create literal text" {
|
||
|
|
const allocator = std.testing.allocator;
|
||
|
|
const text = try literalText(allocator, "Hello, world!");
|
||
|
|
defer allocator.free(text.segments);
|
||
|
|
|
||
|
|
try std.testing.expectEqual(@as(usize, 1), text.segments.len);
|
||
|
|
try std.testing.expectEqualStrings("Hello, world!", text.segments[0].literal);
|
||
|
|
}
|