1use crate::commands::{BuiltNode, Command, NewNode};
2use crate::context::{Diagnostic, PluginContext, Severity};
3use crate::data::{DataMap, TypedDataMap};
4use crate::plugin::{NodeView, Plugin, VisitResult};
5use crate::typed_nodes::*;
6use satteri_arena::{Arena, ArenaBuilder, Mdast};
7use satteri_ast::mdast::MdastNodeType;
8use satteri_ast::rebuild::{rebuild, Patch};
9
10pub struct PluginRunResult {
12 pub arena: Arena<Mdast>,
14 pub commands: Vec<Command>,
15 pub diagnostics: Vec<Diagnostic>,
16 pub has_mutations: bool,
17}
18
19pub 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 pub fn init(&mut self) {
31 for plugin in &mut self.plugins {
32 plugin.init();
33 }
34 }
35
36 pub fn run(
38 &mut self,
39 arena: Arena<Mdast>,
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(¤t_arena, data_map, typed_data);
49
50 plugin.before(¤t_arena, &mut ctx);
52
53 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 ¤t_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 plugin.after(¤t_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 let patches = commands_to_patches(commands.iter().collect(), ¤t_arena);
88 if !patches.is_empty() {
89 match rebuild(¤t_arena, &patches) {
90 Ok(rebuilt) => current_arena = rebuilt,
91 Err(err) => {
92 all_diagnostics.push(Diagnostic {
95 message: format!("invalid patch combination: {err}"),
96 node_id: None,
97 severity: Severity::Error,
98 });
99 }
100 }
101 }
102 all_commands.extend(commands);
103 }
104 }
107
108 let has_mutations = !all_commands.is_empty();
109
110 PluginRunResult {
111 arena: current_arena,
112 commands: all_commands,
113 diagnostics: all_diagnostics,
114 has_mutations,
115 }
116 }
117}
118
119fn commands_to_patches(commands: Vec<&Command>, arena: &Arena<Mdast>) -> Vec<Patch<Mdast>> {
124 commands
125 .into_iter()
126 .filter_map(|cmd| match cmd {
127 Command::Replace { node_id, new_node } => built_node_to_arena(new_node, arena.source())
128 .map(|sub| Patch::Replace {
129 node_id: *node_id,
130 new_tree: sub,
131 keep_children: false,
132 }),
133 Command::Remove { node_id } => Some(Patch::Remove { node_id: *node_id }),
134 Command::InsertBefore { node_id, new_node } => {
135 built_node_to_arena(new_node, arena.source()).map(|sub| Patch::InsertBefore {
136 node_id: *node_id,
137 new_tree: sub,
138 })
139 }
140 Command::InsertAfter { node_id, new_node } => {
141 built_node_to_arena(new_node, arena.source()).map(|sub| Patch::InsertAfter {
142 node_id: *node_id,
143 new_tree: sub,
144 })
145 }
146 Command::Wrap {
147 node_id,
148 parent_node,
149 } => built_node_to_arena(parent_node, arena.source()).map(|sub| Patch::Wrap {
150 node_id: *node_id,
151 parent_tree: sub,
152 }),
153 Command::PrependChild {
154 node_id,
155 child_node,
156 } => built_node_to_arena(child_node, arena.source()).map(|sub| Patch::PrependChild {
157 node_id: *node_id,
158 child_tree: sub,
159 }),
160 Command::AppendChild {
161 node_id,
162 child_node,
163 } => built_node_to_arena(child_node, arena.source()).map(|sub| Patch::AppendChild {
164 node_id: *node_id,
165 child_tree: sub,
166 }),
167 Command::SetData { .. } => {
168 None
170 }
171 })
172 .collect()
173}
174
175fn built_node_to_arena(new_node: &NewNode, source: &str) -> Option<Arena<Mdast>> {
178 match new_node {
179 NewNode::Raw(_) => None, NewNode::Built(built) => {
181 let mut builder = ArenaBuilder::<Mdast>::new(source.to_string());
182 emit_built_node(built, &mut builder);
183 Some(builder.finish())
184 }
185 }
186}
187
188fn emit_built_node(built: &BuiltNode, builder: &mut ArenaBuilder<Mdast>) {
190 builder.open_node(built.node_type as u8);
191 if !built.data_bytes.is_empty() {
192 builder.set_data_current(&built.data_bytes);
193 }
194 for child in &built.children {
195 match child {
196 NewNode::Built(child_built) => emit_built_node(child_built, builder),
197 NewNode::Raw(_) => {} }
199 }
200 builder.close_node();
201}
202
203fn dispatch_visitor(
206 plugin: &mut dyn Plugin,
207 node_type_byte: u8,
208 node_id: u32,
209 arena: &Arena<Mdast>,
210 ctx: &mut PluginContext,
211) -> VisitResult {
212 match MdastNodeType::from_u8(node_type_byte) {
213 Some(MdastNodeType::Heading) => plugin.visit_heading(&Heading { node_id, arena }, ctx),
214 Some(MdastNodeType::Paragraph) => {
215 plugin.visit_paragraph(&Paragraph { node_id, arena }, ctx)
216 }
217 Some(MdastNodeType::Text) => plugin.visit_text(&Text { node_id, arena }, ctx),
218 Some(MdastNodeType::Link) => plugin.visit_link(&Link { node_id, arena }, ctx),
219 Some(MdastNodeType::Image) => plugin.visit_image(&Image { node_id, arena }, ctx),
220 Some(MdastNodeType::Code) => plugin.visit_code(&Code { node_id, arena }, ctx),
221 Some(MdastNodeType::List) => plugin.visit_list(&NodeView { node_id, arena }, ctx),
222 Some(MdastNodeType::ListItem) => plugin.visit_list_item(&NodeView { node_id, arena }, ctx),
223 Some(MdastNodeType::Blockquote) => {
224 plugin.visit_blockquote(&NodeView { node_id, arena }, ctx)
225 }
226 Some(MdastNodeType::Emphasis) => plugin.visit_emphasis(&NodeView { node_id, arena }, ctx),
227 Some(MdastNodeType::Strong) => plugin.visit_strong(&NodeView { node_id, arena }, ctx),
228 Some(MdastNodeType::InlineCode) => plugin.visit_inline_code(&Text { node_id, arena }, ctx),
229 Some(MdastNodeType::Html) => plugin.visit_html(&Text { node_id, arena }, ctx),
230 Some(MdastNodeType::Table) => plugin.visit_table(&NodeView { node_id, arena }, ctx),
231 _ => VisitResult::NoChange,
232 }
233}