902 lines
25 KiB
Zig
902 lines
25 KiB
Zig
|
|
// walk.zig - Zig port of pug-walk
|
||
|
|
//
|
||
|
|
// AST traversal utility with visitor pattern for Pug AST nodes.
|
||
|
|
// Provides before/after callbacks for each node with optional replacement.
|
||
|
|
|
||
|
|
const std = @import("std");
|
||
|
|
const Allocator = std.mem.Allocator;
|
||
|
|
|
||
|
|
// Import AST types from parser
|
||
|
|
const parser = @import("parser.zig");
|
||
|
|
pub const Node = parser.Node;
|
||
|
|
pub const NodeType = parser.NodeType;
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Walk Options
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
pub const WalkOptions = struct {
|
||
|
|
/// Include dependencies (traverse into FileReference.ast if present)
|
||
|
|
include_dependencies: bool = false,
|
||
|
|
/// Parent node stack (managed internally during walk)
|
||
|
|
parents: std.ArrayListUnmanaged(*Node) = .{},
|
||
|
|
|
||
|
|
pub fn deinit(self: *WalkOptions, allocator: Allocator) void {
|
||
|
|
self.parents.deinit(allocator);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Initialize with pre-allocated capacity for expected tree depth
|
||
|
|
/// Reduces allocations during deep AST traversal
|
||
|
|
pub fn initWithCapacity(allocator: Allocator, capacity: usize) !WalkOptions {
|
||
|
|
var opts = WalkOptions{};
|
||
|
|
try opts.parents.ensureTotalCapacity(allocator, capacity);
|
||
|
|
return opts;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Replace Result
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
/// Result of a replace operation
|
||
|
|
pub const ReplaceResult = union(enum) {
|
||
|
|
/// Keep the current node unchanged
|
||
|
|
keep,
|
||
|
|
/// Replace with a single node
|
||
|
|
single: *Node,
|
||
|
|
/// Replace with multiple nodes (only valid in Block/NamedBlock contexts)
|
||
|
|
multiple: []*Node,
|
||
|
|
/// Remove the node (replace with nothing)
|
||
|
|
remove,
|
||
|
|
};
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Callback Types
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
/// Before callback - called before visiting children
|
||
|
|
/// Return false to skip traversing children, null to continue
|
||
|
|
/// Can use ReplaceResult to replace the current node
|
||
|
|
pub const BeforeCallback = *const fn (
|
||
|
|
node: *Node,
|
||
|
|
replace_allowed: bool,
|
||
|
|
ctx: *WalkContext,
|
||
|
|
) WalkError!?ReplaceResult;
|
||
|
|
|
||
|
|
/// After callback - called after visiting children
|
||
|
|
pub const AfterCallback = *const fn (
|
||
|
|
node: *Node,
|
||
|
|
replace_allowed: bool,
|
||
|
|
ctx: *WalkContext,
|
||
|
|
) WalkError!?ReplaceResult;
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Walk Context
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
pub const WalkContext = struct {
|
||
|
|
allocator: Allocator,
|
||
|
|
options: *WalkOptions,
|
||
|
|
user_data: ?*anyopaque = null,
|
||
|
|
|
||
|
|
/// Get parent at index (0 = immediate parent, 1 = grandparent, etc.)
|
||
|
|
/// Uses reverse indexing since parents are stored with oldest first (stack-like append/pop)
|
||
|
|
pub fn getParent(self: *WalkContext, index: usize) ?*Node {
|
||
|
|
const len = self.options.parents.items.len;
|
||
|
|
if (index >= len) return null;
|
||
|
|
// Reverse index: 0 = last item (immediate parent), 1 = second-to-last, etc.
|
||
|
|
return self.options.parents.items[len - 1 - index];
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get the immediate parent node
|
||
|
|
pub fn parent(self: *WalkContext) ?*Node {
|
||
|
|
const items = self.options.parents.items;
|
||
|
|
if (items.len == 0) return null;
|
||
|
|
return items[items.len - 1];
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get number of parents in the stack
|
||
|
|
pub fn depth(self: *WalkContext) usize {
|
||
|
|
return self.options.parents.items.len;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Walk Errors
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
pub const WalkError = error{
|
||
|
|
OutOfMemory,
|
||
|
|
ArrayReplaceNotAllowed,
|
||
|
|
UnexpectedNodeType,
|
||
|
|
};
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Walk Implementation
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
/// Walk the AST tree, calling before/after callbacks for each node
|
||
|
|
pub fn walkAST(
|
||
|
|
allocator: Allocator,
|
||
|
|
ast: *Node,
|
||
|
|
before: ?BeforeCallback,
|
||
|
|
after: ?AfterCallback,
|
||
|
|
options: *WalkOptions,
|
||
|
|
) WalkError!*Node {
|
||
|
|
return walkASTWithUserData(allocator, ast, before, after, options, null);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Walk the AST tree with user-provided context data
|
||
|
|
pub fn walkASTWithUserData(
|
||
|
|
allocator: Allocator,
|
||
|
|
ast: *Node,
|
||
|
|
before: ?BeforeCallback,
|
||
|
|
after: ?AfterCallback,
|
||
|
|
options: *WalkOptions,
|
||
|
|
user_data: ?*anyopaque,
|
||
|
|
) WalkError!*Node {
|
||
|
|
var current = ast;
|
||
|
|
|
||
|
|
// Check if array replacement is allowed based on parent context
|
||
|
|
const replace_allowed = isArrayReplaceAllowed(options, current);
|
||
|
|
|
||
|
|
var ctx = WalkContext{
|
||
|
|
.allocator = allocator,
|
||
|
|
.options = options,
|
||
|
|
.user_data = user_data,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Call before callback
|
||
|
|
if (before) |before_fn| {
|
||
|
|
if (try before_fn(current, replace_allowed, &ctx)) |result| {
|
||
|
|
switch (result) {
|
||
|
|
.keep => {},
|
||
|
|
.single => |replacement| {
|
||
|
|
current = replacement;
|
||
|
|
},
|
||
|
|
.multiple => {
|
||
|
|
// Array replacement - return the original node as marker
|
||
|
|
// The caller (walkAndMergeNodes) will handle expansion
|
||
|
|
return current;
|
||
|
|
},
|
||
|
|
.remove => {
|
||
|
|
// Return null marker - handled by caller
|
||
|
|
return current;
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Push current node to parents stack (O(1) append instead of O(n) insert at 0)
|
||
|
|
try options.parents.append(allocator, current);
|
||
|
|
defer _ = options.parents.pop();
|
||
|
|
|
||
|
|
// Visit children based on node type
|
||
|
|
try visitChildren(allocator, current, before, after, options, user_data);
|
||
|
|
|
||
|
|
// Call after callback
|
||
|
|
if (after) |after_fn| {
|
||
|
|
if (try after_fn(current, replace_allowed, &ctx)) |result| {
|
||
|
|
switch (result) {
|
||
|
|
.keep => {},
|
||
|
|
.single => |replacement| {
|
||
|
|
current = replacement;
|
||
|
|
},
|
||
|
|
.multiple, .remove => {
|
||
|
|
// Handled by caller
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return current;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Check if array replacement is allowed for the current context
|
||
|
|
fn isArrayReplaceAllowed(options: *WalkOptions, node: *Node) bool {
|
||
|
|
const items = options.parents.items;
|
||
|
|
if (items.len == 0) return false;
|
||
|
|
|
||
|
|
// Get immediate parent (last item in stack)
|
||
|
|
const parent_node = items[items.len - 1];
|
||
|
|
|
||
|
|
// Array replacement allowed in Block/NamedBlock
|
||
|
|
if (parent_node.type == .Block or parent_node.type == .NamedBlock) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Also allowed for IncludeFilter in RawInclude
|
||
|
|
if (parent_node.type == .RawInclude and node.type == .IncludeFilter) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Visit children of a node based on its type
|
||
|
|
fn visitChildren(
|
||
|
|
allocator: Allocator,
|
||
|
|
node: *Node,
|
||
|
|
before: ?BeforeCallback,
|
||
|
|
after: ?AfterCallback,
|
||
|
|
options: *WalkOptions,
|
||
|
|
user_data: ?*anyopaque,
|
||
|
|
) WalkError!void {
|
||
|
|
switch (node.type) {
|
||
|
|
.NamedBlock, .Block => {
|
||
|
|
// Walk and merge nodes
|
||
|
|
try walkAndMergeNodes(allocator, &node.nodes, before, after, options, user_data);
|
||
|
|
},
|
||
|
|
|
||
|
|
.Case, .Filter, .Mixin, .Tag, .InterpolatedTag, .When, .Code, .While => {
|
||
|
|
// Walk block if present (represented by non-empty nodes)
|
||
|
|
if (node.nodes.items.len > 0) {
|
||
|
|
// Find the block node (first child that is a Block)
|
||
|
|
for (node.nodes.items, 0..) |child, i| {
|
||
|
|
if (child.type == .Block or child.type == .NamedBlock) {
|
||
|
|
node.nodes.items[i] = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
child,
|
||
|
|
before,
|
||
|
|
after,
|
||
|
|
options,
|
||
|
|
user_data,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
.Each => {
|
||
|
|
// Walk block
|
||
|
|
if (node.nodes.items.len > 0) {
|
||
|
|
for (node.nodes.items, 0..) |child, i| {
|
||
|
|
if (child.type == .Block or child.type == .NamedBlock) {
|
||
|
|
node.nodes.items[i] = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
child,
|
||
|
|
before,
|
||
|
|
after,
|
||
|
|
options,
|
||
|
|
user_data,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Walk alternate
|
||
|
|
if (node.alternate) |alt| {
|
||
|
|
node.alternate = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
alt,
|
||
|
|
before,
|
||
|
|
after,
|
||
|
|
options,
|
||
|
|
user_data,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
.EachOf => {
|
||
|
|
// Walk block only
|
||
|
|
if (node.nodes.items.len > 0) {
|
||
|
|
for (node.nodes.items, 0..) |child, i| {
|
||
|
|
if (child.type == .Block or child.type == .NamedBlock) {
|
||
|
|
node.nodes.items[i] = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
child,
|
||
|
|
before,
|
||
|
|
after,
|
||
|
|
options,
|
||
|
|
user_data,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
.Conditional => {
|
||
|
|
// Walk consequent
|
||
|
|
if (node.consequent) |cons| {
|
||
|
|
node.consequent = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
cons,
|
||
|
|
before,
|
||
|
|
after,
|
||
|
|
options,
|
||
|
|
user_data,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
// Walk alternate
|
||
|
|
if (node.alternate) |alt| {
|
||
|
|
node.alternate = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
alt,
|
||
|
|
before,
|
||
|
|
after,
|
||
|
|
options,
|
||
|
|
user_data,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
.Include => {
|
||
|
|
// Walk block (represented as child nodes)
|
||
|
|
try walkAndMergeNodes(allocator, &node.nodes, before, after, options, user_data);
|
||
|
|
// Note: file is a FileReference struct, not a Node, so we don't walk it
|
||
|
|
},
|
||
|
|
|
||
|
|
.Extends => {
|
||
|
|
// Note: file is a FileReference struct, not a Node
|
||
|
|
},
|
||
|
|
|
||
|
|
.RawInclude => {
|
||
|
|
// Walk filters
|
||
|
|
try walkAndMergeNodes(allocator, &node.filters, before, after, options, user_data);
|
||
|
|
// Note: file is a FileReference struct
|
||
|
|
},
|
||
|
|
|
||
|
|
.FileReference => {
|
||
|
|
// Walk into ast if includeDependencies is set
|
||
|
|
// Note: In our implementation, FileReference doesn't hold a nested AST directly
|
||
|
|
// This would need to be handled by the loader
|
||
|
|
_ = options.include_dependencies;
|
||
|
|
},
|
||
|
|
|
||
|
|
// Leaf nodes - no children to visit
|
||
|
|
.AttributeBlock,
|
||
|
|
.BlockComment,
|
||
|
|
.Comment,
|
||
|
|
.Doctype,
|
||
|
|
.IncludeFilter,
|
||
|
|
.MixinBlock,
|
||
|
|
.YieldBlock,
|
||
|
|
.Text,
|
||
|
|
=> {},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Walk a list of nodes and merge results (handling array replacements)
|
||
|
|
fn walkAndMergeNodes(
|
||
|
|
allocator: Allocator,
|
||
|
|
nodes: *std.ArrayListUnmanaged(*Node),
|
||
|
|
before: ?BeforeCallback,
|
||
|
|
after: ?AfterCallback,
|
||
|
|
options: *WalkOptions,
|
||
|
|
user_data: ?*anyopaque,
|
||
|
|
) WalkError!void {
|
||
|
|
var i: usize = 0;
|
||
|
|
while (i < nodes.items.len) {
|
||
|
|
const result = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
nodes.items[i],
|
||
|
|
before,
|
||
|
|
after,
|
||
|
|
options,
|
||
|
|
user_data,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Update the node in place
|
||
|
|
nodes.items[i] = result;
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Convenience Functions
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
/// Simple walk that just calls a callback for each node (no replacement)
|
||
|
|
pub fn walk(
|
||
|
|
allocator: Allocator,
|
||
|
|
ast: *Node,
|
||
|
|
callback: *const fn (node: *Node, ctx: *WalkContext) WalkError!void,
|
||
|
|
) WalkError!void {
|
||
|
|
const Wrapper = struct {
|
||
|
|
fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
|
||
|
|
const cb: *const fn (*Node, *WalkContext) WalkError!void = @ptrCast(@alignCast(ctx.user_data.?));
|
||
|
|
try cb(node, ctx);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
var options = WalkOptions{};
|
||
|
|
defer options.deinit(allocator);
|
||
|
|
|
||
|
|
_ = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
ast,
|
||
|
|
Wrapper.before,
|
||
|
|
null,
|
||
|
|
&options,
|
||
|
|
@ptrCast(@constCast(callback)),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Count nodes of a specific type
|
||
|
|
pub fn countNodes(allocator: Allocator, ast: *Node, node_type: NodeType) WalkError!usize {
|
||
|
|
const Counter = struct {
|
||
|
|
count: usize = 0,
|
||
|
|
target_type: NodeType,
|
||
|
|
|
||
|
|
fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
|
||
|
|
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
|
||
|
|
if (node.type == self.target_type) {
|
||
|
|
self.count += 1;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
var counter = Counter{ .target_type = node_type };
|
||
|
|
var options = WalkOptions{};
|
||
|
|
defer options.deinit(allocator);
|
||
|
|
|
||
|
|
_ = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
ast,
|
||
|
|
Counter.before,
|
||
|
|
null,
|
||
|
|
&options,
|
||
|
|
&counter,
|
||
|
|
);
|
||
|
|
|
||
|
|
return counter.count;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Find the first node matching a predicate
|
||
|
|
pub fn findNode(
|
||
|
|
allocator: Allocator,
|
||
|
|
ast: *Node,
|
||
|
|
predicate: *const fn (node: *Node) bool,
|
||
|
|
) WalkError!?*Node {
|
||
|
|
const Finder = struct {
|
||
|
|
found: ?*Node = null,
|
||
|
|
pred: *const fn (*Node) bool,
|
||
|
|
|
||
|
|
fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
|
||
|
|
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
|
||
|
|
if (self.found == null and self.pred(node)) {
|
||
|
|
self.found = node;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
var finder = Finder{ .pred = predicate };
|
||
|
|
var options = WalkOptions{};
|
||
|
|
defer options.deinit(allocator);
|
||
|
|
|
||
|
|
_ = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
ast,
|
||
|
|
Finder.before,
|
||
|
|
null,
|
||
|
|
&options,
|
||
|
|
&finder,
|
||
|
|
);
|
||
|
|
|
||
|
|
return finder.found;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Collect all nodes of a specific type
|
||
|
|
pub fn collectNodes(
|
||
|
|
allocator: Allocator,
|
||
|
|
ast: *Node,
|
||
|
|
node_type: NodeType,
|
||
|
|
) WalkError!std.ArrayListUnmanaged(*Node) {
|
||
|
|
const Collector = struct {
|
||
|
|
collected: std.ArrayListUnmanaged(*Node) = .{},
|
||
|
|
alloc: Allocator,
|
||
|
|
target_type: NodeType,
|
||
|
|
|
||
|
|
fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
|
||
|
|
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
|
||
|
|
if (node.type == self.target_type) {
|
||
|
|
self.collected.append(self.alloc, node) catch return error.OutOfMemory;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
var collector = Collector{
|
||
|
|
.alloc = allocator,
|
||
|
|
.target_type = node_type,
|
||
|
|
};
|
||
|
|
var options = WalkOptions{};
|
||
|
|
defer options.deinit(allocator);
|
||
|
|
|
||
|
|
_ = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
ast,
|
||
|
|
Collector.before,
|
||
|
|
null,
|
||
|
|
&options,
|
||
|
|
&collector,
|
||
|
|
);
|
||
|
|
|
||
|
|
return collector.collected;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Tests
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test "walkAST - basic traversal" {
|
||
|
|
const allocator = std.testing.allocator;
|
||
|
|
|
||
|
|
// Create a simple AST: Block -> Tag -> Text
|
||
|
|
const text_node = try allocator.create(Node);
|
||
|
|
text_node.* = Node{
|
||
|
|
.type = .Text,
|
||
|
|
.val = "Hello",
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
var tag_block = try allocator.create(Node);
|
||
|
|
tag_block.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try tag_block.nodes.append(allocator, text_node);
|
||
|
|
|
||
|
|
var tag_node = try allocator.create(Node);
|
||
|
|
tag_node.* = Node{
|
||
|
|
.type = .Tag,
|
||
|
|
.name = "div",
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try tag_node.nodes.append(allocator, tag_block);
|
||
|
|
|
||
|
|
var root = try allocator.create(Node);
|
||
|
|
root.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try root.nodes.append(allocator, tag_node);
|
||
|
|
|
||
|
|
defer {
|
||
|
|
root.deinit(allocator);
|
||
|
|
allocator.destroy(root);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Count nodes
|
||
|
|
const count = try countNodes(allocator, root, .Tag);
|
||
|
|
try std.testing.expectEqual(@as(usize, 1), count);
|
||
|
|
|
||
|
|
const block_count = try countNodes(allocator, root, .Block);
|
||
|
|
try std.testing.expectEqual(@as(usize, 2), block_count);
|
||
|
|
|
||
|
|
const text_count = try countNodes(allocator, root, .Text);
|
||
|
|
try std.testing.expectEqual(@as(usize, 1), text_count);
|
||
|
|
}
|
||
|
|
|
||
|
|
test "walkAST - conditional traversal" {
|
||
|
|
const allocator = std.testing.allocator;
|
||
|
|
|
||
|
|
// Create AST with conditional: if (test) then else
|
||
|
|
const then_block = try allocator.create(Node);
|
||
|
|
then_block.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
const else_block = try allocator.create(Node);
|
||
|
|
else_block.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 2,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
const cond_node = try allocator.create(Node);
|
||
|
|
cond_node.* = Node{
|
||
|
|
.type = .Conditional,
|
||
|
|
.test_expr = "true",
|
||
|
|
.consequent = then_block,
|
||
|
|
.alternate = else_block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
var root = try allocator.create(Node);
|
||
|
|
root.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try root.nodes.append(allocator, cond_node);
|
||
|
|
|
||
|
|
defer {
|
||
|
|
root.deinit(allocator);
|
||
|
|
allocator.destroy(root);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Count all blocks (root + then + else = 3)
|
||
|
|
const block_count = try countNodes(allocator, root, .Block);
|
||
|
|
try std.testing.expectEqual(@as(usize, 3), block_count);
|
||
|
|
|
||
|
|
// Count conditionals
|
||
|
|
const cond_count = try countNodes(allocator, root, .Conditional);
|
||
|
|
try std.testing.expectEqual(@as(usize, 1), cond_count);
|
||
|
|
}
|
||
|
|
|
||
|
|
test "walkAST - each with alternate" {
|
||
|
|
const allocator = std.testing.allocator;
|
||
|
|
|
||
|
|
// Create Each node with block and alternate
|
||
|
|
const loop_block = try allocator.create(Node);
|
||
|
|
loop_block.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
const alt_block = try allocator.create(Node);
|
||
|
|
alt_block.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 2,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
var each_node = try allocator.create(Node);
|
||
|
|
each_node.* = Node{
|
||
|
|
.type = .Each,
|
||
|
|
.val = "item",
|
||
|
|
.obj = "items",
|
||
|
|
.alternate = alt_block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try each_node.nodes.append(allocator, loop_block);
|
||
|
|
|
||
|
|
var root = try allocator.create(Node);
|
||
|
|
root.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try root.nodes.append(allocator, each_node);
|
||
|
|
|
||
|
|
defer {
|
||
|
|
root.deinit(allocator);
|
||
|
|
allocator.destroy(root);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Count blocks (root + loop_block + alt_block = 3)
|
||
|
|
const block_count = try countNodes(allocator, root, .Block);
|
||
|
|
try std.testing.expectEqual(@as(usize, 3), block_count);
|
||
|
|
|
||
|
|
// Count each nodes
|
||
|
|
const each_count = try countNodes(allocator, root, .Each);
|
||
|
|
try std.testing.expectEqual(@as(usize, 1), each_count);
|
||
|
|
}
|
||
|
|
|
||
|
|
test "walkAST - findNode" {
|
||
|
|
const allocator = std.testing.allocator;
|
||
|
|
|
||
|
|
// Create a simple AST with multiple tags
|
||
|
|
const tag1 = try allocator.create(Node);
|
||
|
|
tag1.* = Node{
|
||
|
|
.type = .Tag,
|
||
|
|
.name = "div",
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
const tag2 = try allocator.create(Node);
|
||
|
|
tag2.* = Node{
|
||
|
|
.type = .Tag,
|
||
|
|
.name = "span",
|
||
|
|
.line = 2,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
var root = try allocator.create(Node);
|
||
|
|
root.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try root.nodes.append(allocator, tag1);
|
||
|
|
try root.nodes.append(allocator, tag2);
|
||
|
|
|
||
|
|
defer {
|
||
|
|
root.deinit(allocator);
|
||
|
|
allocator.destroy(root);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find span tag
|
||
|
|
const isSpan = struct {
|
||
|
|
fn check(node: *Node) bool {
|
||
|
|
return node.type == .Tag and
|
||
|
|
node.name != null and
|
||
|
|
std.mem.eql(u8, node.name.?, "span");
|
||
|
|
}
|
||
|
|
}.check;
|
||
|
|
|
||
|
|
const found = try findNode(allocator, root, isSpan);
|
||
|
|
try std.testing.expect(found != null);
|
||
|
|
try std.testing.expectEqualStrings("span", found.?.name.?);
|
||
|
|
}
|
||
|
|
|
||
|
|
test "walkAST - collectNodes" {
|
||
|
|
const allocator = std.testing.allocator;
|
||
|
|
|
||
|
|
// Create AST with multiple text nodes
|
||
|
|
const text1 = try allocator.create(Node);
|
||
|
|
text1.* = Node{
|
||
|
|
.type = .Text,
|
||
|
|
.val = "Hello",
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
const text2 = try allocator.create(Node);
|
||
|
|
text2.* = Node{
|
||
|
|
.type = .Text,
|
||
|
|
.val = "World",
|
||
|
|
.line = 2,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
const tag = try allocator.create(Node);
|
||
|
|
tag.* = Node{
|
||
|
|
.type = .Tag,
|
||
|
|
.name = "div",
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
var root = try allocator.create(Node);
|
||
|
|
root.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try root.nodes.append(allocator, text1);
|
||
|
|
try root.nodes.append(allocator, tag);
|
||
|
|
try root.nodes.append(allocator, text2);
|
||
|
|
|
||
|
|
defer {
|
||
|
|
root.deinit(allocator);
|
||
|
|
allocator.destroy(root);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Collect all text nodes
|
||
|
|
var collected = try collectNodes(allocator, root, .Text);
|
||
|
|
defer collected.deinit(allocator);
|
||
|
|
|
||
|
|
try std.testing.expectEqual(@as(usize, 2), collected.items.len);
|
||
|
|
try std.testing.expectEqualStrings("Hello", collected.items[0].val.?);
|
||
|
|
try std.testing.expectEqualStrings("World", collected.items[1].val.?);
|
||
|
|
}
|
||
|
|
|
||
|
|
test "walkAST - parent tracking" {
|
||
|
|
const allocator = std.testing.allocator;
|
||
|
|
|
||
|
|
// Create nested structure
|
||
|
|
const inner_text = try allocator.create(Node);
|
||
|
|
inner_text.* = Node{
|
||
|
|
.type = .Text,
|
||
|
|
.val = "nested",
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
var inner_block = try allocator.create(Node);
|
||
|
|
inner_block.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try inner_block.nodes.append(allocator, inner_text);
|
||
|
|
|
||
|
|
var tag = try allocator.create(Node);
|
||
|
|
tag.* = Node{
|
||
|
|
.type = .Tag,
|
||
|
|
.name = "div",
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try tag.nodes.append(allocator, inner_block);
|
||
|
|
|
||
|
|
var root = try allocator.create(Node);
|
||
|
|
root.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try root.nodes.append(allocator, tag);
|
||
|
|
|
||
|
|
defer {
|
||
|
|
root.deinit(allocator);
|
||
|
|
allocator.destroy(root);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Track parent depths for text node
|
||
|
|
const ParentTracker = struct {
|
||
|
|
text_depth: usize = 0,
|
||
|
|
text_parent_type: ?NodeType = null,
|
||
|
|
|
||
|
|
fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult {
|
||
|
|
const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?));
|
||
|
|
if (node.type == .Text) {
|
||
|
|
self.text_depth = ctx.depth();
|
||
|
|
if (ctx.parent()) |p| {
|
||
|
|
self.text_parent_type = p.type;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
var tracker = ParentTracker{};
|
||
|
|
var options = WalkOptions{};
|
||
|
|
defer options.deinit(allocator);
|
||
|
|
|
||
|
|
_ = try walkASTWithUserData(
|
||
|
|
allocator,
|
||
|
|
root,
|
||
|
|
ParentTracker.before,
|
||
|
|
null,
|
||
|
|
&options,
|
||
|
|
&tracker,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Text node should have depth 3 (root Block -> Tag -> inner Block -> Text)
|
||
|
|
// Parent should be the inner Block
|
||
|
|
try std.testing.expectEqual(@as(usize, 3), tracker.text_depth);
|
||
|
|
try std.testing.expectEqual(NodeType.Block, tracker.text_parent_type.?);
|
||
|
|
}
|
||
|
|
|
||
|
|
test "walkAST - RawInclude with filters" {
|
||
|
|
const allocator = std.testing.allocator;
|
||
|
|
|
||
|
|
// Create RawInclude with filters
|
||
|
|
const filter1 = try allocator.create(Node);
|
||
|
|
filter1.* = Node{
|
||
|
|
.type = .IncludeFilter,
|
||
|
|
.name = "markdown",
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
const filter2 = try allocator.create(Node);
|
||
|
|
filter2.* = Node{
|
||
|
|
.type = .IncludeFilter,
|
||
|
|
.name = "escape",
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
var raw_include = try allocator.create(Node);
|
||
|
|
raw_include.* = Node{
|
||
|
|
.type = .RawInclude,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try raw_include.filters.append(allocator, filter1);
|
||
|
|
try raw_include.filters.append(allocator, filter2);
|
||
|
|
|
||
|
|
var root = try allocator.create(Node);
|
||
|
|
root.* = Node{
|
||
|
|
.type = .Block,
|
||
|
|
.line = 1,
|
||
|
|
.column = 1,
|
||
|
|
};
|
||
|
|
try root.nodes.append(allocator, raw_include);
|
||
|
|
|
||
|
|
defer {
|
||
|
|
root.deinit(allocator);
|
||
|
|
allocator.destroy(root);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Count IncludeFilter nodes
|
||
|
|
const filter_count = try countNodes(allocator, root, .IncludeFilter);
|
||
|
|
try std.testing.expectEqual(@as(usize, 2), filter_count);
|
||
|
|
}
|