Skip to main content

satteri_plugin_api/
runner.rs

1use crate::commands::{BuiltNode, Command, NewNode};
2use crate::context::{Diagnostic, PluginContext};
3use crate::data::{DataMap, TypedDataMap};
4use crate::plugin::{NodeView, Plugin, VisitResult};
5use crate::typed_nodes::*;
6use satteri_arena::{Arena, ArenaBuilder};
7use satteri_ast::mdast::MdastNodeType;
8use satteri_ast::rebuild::{rebuild, Patch};
9
10/// Result of running plugins against an arena.
11pub struct PluginRunResult {
12    /// The (possibly modified) arena, same instance if no mutations, rebuilt if mutations occurred.
13    pub arena: Arena,
14    pub commands: Vec<Command>,
15    pub diagnostics: Vec<Diagnostic>,
16    pub has_mutations: bool,
17}
18
19/// Runs a list of Rust plugins sequentially against an arena.
20pub struct PluginRunner {
21    plugins: Vec<Box<dyn Plugin>>,
22}
23
24impl PluginRunner {
25    pub fn new(plugins: Vec<Box<dyn Plugin>>) -> Self {
26        Self { plugins }
27    }
28
29    /// Initialize all plugins (call init on each).
30    pub fn init(&mut self) {
31        for plugin in &mut self.plugins {
32            plugin.init();
33        }
34    }
35
36    /// Run all plugins against an arena. Returns the result.
37    pub fn run(
38        &mut self,
39        arena: Arena,
40        data_map: &mut DataMap,
41        typed_data: &mut TypedDataMap,
42    ) -> PluginRunResult {
43        let mut all_commands: Vec<Command> = Vec::new();
44        let mut all_diagnostics: Vec<Diagnostic> = Vec::new();
45        let mut current_arena = arena;
46
47        for plugin in &mut self.plugins {
48            let mut ctx = PluginContext::new(&current_arena, data_map, typed_data);
49
50            // Call before
51            plugin.before(&current_arena, &mut ctx);
52
53            // Walk the arena depth-first, dispatch to typed visitor methods
54            let node_count = current_arena.len() as u32;
55            for node_id in 0..node_count {
56                let node = current_arena.get_node(node_id);
57                let node_type_byte = node.node_type;
58
59                let result = dispatch_visitor(
60                    plugin.as_mut(),
61                    node_type_byte,
62                    node_id,
63                    &current_arena,
64                    &mut ctx,
65                );
66
67                match result {
68                    VisitResult::Replace(new_node) => {
69                        ctx.replace_node(node_id, new_node);
70                    }
71                    VisitResult::Remove => {
72                        ctx.remove_node(node_id);
73                    }
74                    VisitResult::NoChange => {}
75                }
76            }
77
78            // Call after
79            plugin.after(&current_arena, &mut ctx);
80
81            let (commands, diagnostics) = ctx.take_commands();
82            let has_cmds = !commands.is_empty();
83            all_diagnostics.extend(diagnostics);
84
85            if has_cmds {
86                // Convert commands to patches and rebuild the arena
87                let patches = commands_to_patches(commands.iter().collect(), &current_arena);
88                if !patches.is_empty() {
89                    current_arena = rebuild(&current_arena, &patches);
90                }
91                all_commands.extend(commands);
92            }
93            // else: skip optimization, current_arena passes through unchanged
94            // (Data mutations are already applied via data_map directly)
95        }
96
97        let has_mutations = !all_commands.is_empty();
98
99        PluginRunResult {
100            arena: current_arena,
101            commands: all_commands,
102            diagnostics: all_diagnostics,
103            has_mutations,
104        }
105    }
106}
107
108/// Convert a list of Commands into Patches.
109/// SetData commands are skipped (they are applied directly through the DataMap,
110/// not via arena structural mutation).
111/// NewNode::Raw commands are skipped (need parser, Phase 8).
112fn commands_to_patches(commands: Vec<&Command>, arena: &Arena) -> Vec<Patch> {
113    commands
114        .into_iter()
115        .filter_map(|cmd| match cmd {
116            Command::Replace { node_id, new_node } => built_node_to_arena(new_node, arena.source())
117                .map(|sub| Patch::Replace {
118                    node_id: *node_id,
119                    new_tree: sub,
120                    keep_children: false,
121                }),
122            Command::Remove { node_id } => Some(Patch::Remove { node_id: *node_id }),
123            Command::InsertBefore { node_id, new_node } => {
124                built_node_to_arena(new_node, arena.source()).map(|sub| Patch::InsertBefore {
125                    node_id: *node_id,
126                    new_tree: sub,
127                })
128            }
129            Command::InsertAfter { node_id, new_node } => {
130                built_node_to_arena(new_node, arena.source()).map(|sub| Patch::InsertAfter {
131                    node_id: *node_id,
132                    new_tree: sub,
133                })
134            }
135            Command::Wrap {
136                node_id,
137                parent_node,
138            } => built_node_to_arena(parent_node, arena.source()).map(|sub| Patch::Wrap {
139                node_id: *node_id,
140                parent_tree: sub,
141            }),
142            Command::PrependChild {
143                node_id,
144                child_node,
145            } => built_node_to_arena(child_node, arena.source()).map(|sub| Patch::PrependChild {
146                node_id: *node_id,
147                child_tree: sub,
148            }),
149            Command::AppendChild {
150                node_id,
151                child_node,
152            } => built_node_to_arena(child_node, arena.source()).map(|sub| Patch::AppendChild {
153                node_id: *node_id,
154                child_tree: sub,
155            }),
156            Command::SetData { .. } => {
157                // Already applied via DataMap in PluginContext, no arena rebuild needed
158                None
159            }
160        })
161        .collect()
162}
163
164/// Convert a NewNode into a mini Arena for use as a patch sub-tree.
165/// Returns None for Raw nodes (parser integration is Phase 8).
166fn built_node_to_arena(new_node: &NewNode, source: &str) -> Option<Arena> {
167    match new_node {
168        NewNode::Raw(_) => None, // Phase 8
169        NewNode::Built(built) => {
170            let mut builder = ArenaBuilder::new(source.to_string());
171            emit_built_node(built, &mut builder);
172            Some(builder.finish())
173        }
174    }
175}
176
177/// Recursively emit a BuiltNode into the builder.
178fn emit_built_node(built: &BuiltNode, builder: &mut ArenaBuilder) {
179    builder.open_node(built.node_type as u8);
180    if !built.data_bytes.is_empty() {
181        builder.set_data_current(&built.data_bytes);
182    }
183    for child in &built.children {
184        match child {
185            NewNode::Built(child_built) => emit_built_node(child_built, builder),
186            NewNode::Raw(_) => {} // skip
187        }
188    }
189    builder.close_node();
190}
191
192/// Dispatch a node to the appropriate typed visitor method.
193/// Returns VisitResult from the plugin.
194fn dispatch_visitor(
195    plugin: &mut dyn Plugin,
196    node_type_byte: u8,
197    node_id: u32,
198    arena: &Arena,
199    ctx: &mut PluginContext,
200) -> VisitResult {
201    match MdastNodeType::from_u8(node_type_byte) {
202        Some(MdastNodeType::Heading) => plugin.visit_heading(&Heading { node_id, arena }, ctx),
203        Some(MdastNodeType::Paragraph) => {
204            plugin.visit_paragraph(&Paragraph { node_id, arena }, ctx)
205        }
206        Some(MdastNodeType::Text) => plugin.visit_text(&Text { node_id, arena }, ctx),
207        Some(MdastNodeType::Link) => plugin.visit_link(&Link { node_id, arena }, ctx),
208        Some(MdastNodeType::Image) => plugin.visit_image(&Image { node_id, arena }, ctx),
209        Some(MdastNodeType::Code) => plugin.visit_code(&Code { node_id, arena }, ctx),
210        Some(MdastNodeType::List) => plugin.visit_list(&NodeView { node_id, arena }, ctx),
211        Some(MdastNodeType::ListItem) => plugin.visit_list_item(&NodeView { node_id, arena }, ctx),
212        Some(MdastNodeType::Blockquote) => {
213            plugin.visit_blockquote(&NodeView { node_id, arena }, ctx)
214        }
215        Some(MdastNodeType::Emphasis) => plugin.visit_emphasis(&NodeView { node_id, arena }, ctx),
216        Some(MdastNodeType::Strong) => plugin.visit_strong(&NodeView { node_id, arena }, ctx),
217        Some(MdastNodeType::InlineCode) => plugin.visit_inline_code(&Text { node_id, arena }, ctx),
218        Some(MdastNodeType::Html) => plugin.visit_html(&Text { node_id, arena }, ctx),
219        Some(MdastNodeType::Table) => plugin.visit_table(&NodeView { node_id, arena }, ctx),
220        _ => VisitResult::NoChange,
221    }
222}