follow PugJs
This commit is contained in:
901
src/walk.zig
Normal file
901
src/walk.zig
Normal file
@@ -0,0 +1,901 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user