1pub mod builtin;
45pub mod latex;
46
47use streamdown_config::ComputedStyle;
48use streamdown_core::state::ParseState;
49
50#[derive(Debug, Clone, PartialEq)]
52pub enum ProcessResult {
53 Lines(Vec<String>),
55 Continue,
57}
58
59impl ProcessResult {
60 pub fn line(s: impl Into<String>) -> Self {
62 Self::Lines(vec![s.into()])
63 }
64
65 pub fn lines(lines: Vec<String>) -> Self {
67 Self::Lines(lines)
68 }
69
70 pub fn cont() -> Self {
72 Self::Continue
73 }
74}
75
76pub trait Plugin: Send + Sync {
83 fn name(&self) -> &str;
85
86 fn process_line(
93 &mut self,
94 line: &str,
95 state: &ParseState,
96 style: &ComputedStyle,
97 ) -> Option<ProcessResult>;
98
99 fn flush(&mut self) -> Option<Vec<String>>;
105
106 fn reset(&mut self);
110
111 fn priority(&self) -> i32 {
115 0
116 }
117
118 fn is_active(&self) -> bool {
122 false
123 }
124}
125
126#[derive(Default)]
133pub struct PluginManager {
134 plugins: Vec<Box<dyn Plugin>>,
136 active_plugin: Option<usize>,
138}
139
140impl PluginManager {
141 pub fn new() -> Self {
143 Self::default()
144 }
145
146 pub fn with_builtins() -> Self {
148 let mut manager = Self::new();
149 manager.register(Box::new(latex::LatexPlugin::new()));
150 manager
151 }
152
153 pub fn register(&mut self, plugin: Box<dyn Plugin>) {
157 self.plugins.push(plugin);
158 self.plugins.sort_by_key(|p| p.priority());
159 }
160
161 pub fn plugin_count(&self) -> usize {
163 self.plugins.len()
164 }
165
166 pub fn plugin_names(&self) -> Vec<&str> {
168 self.plugins.iter().map(|p| p.name()).collect()
169 }
170
171 pub fn process_line(
177 &mut self,
178 line: &str,
179 state: &ParseState,
180 style: &ComputedStyle,
181 ) -> Option<Vec<String>> {
182 if let Some(idx) = self.active_plugin {
184 let plugin = &mut self.plugins[idx];
185 match plugin.process_line(line, state, style) {
186 Some(ProcessResult::Lines(lines)) => {
187 self.active_plugin = None;
189 return Some(lines);
190 }
191 Some(ProcessResult::Continue) => {
192 return Some(vec![]);
194 }
195 None => {
196 self.active_plugin = None;
198 }
199 }
200 }
201
202 for (idx, plugin) in self.plugins.iter_mut().enumerate() {
204 match plugin.process_line(line, state, style) {
205 Some(ProcessResult::Lines(lines)) => {
206 return Some(lines);
207 }
208 Some(ProcessResult::Continue) => {
209 self.active_plugin = Some(idx);
211 return Some(vec![]);
212 }
213 None => continue,
214 }
215 }
216
217 None
218 }
219
220 pub fn flush(&mut self) -> Vec<String> {
224 let mut result = Vec::new();
225
226 for plugin in &mut self.plugins {
227 if let Some(lines) = plugin.flush() {
228 result.extend(lines);
229 }
230 }
231
232 self.active_plugin = None;
233 result
234 }
235
236 pub fn reset(&mut self) {
238 for plugin in &mut self.plugins {
239 plugin.reset();
240 }
241 self.active_plugin = None;
242 }
243
244 pub fn has_active_plugin(&self) -> bool {
246 self.active_plugin.is_some()
247 }
248
249 pub fn active_plugin_name(&self) -> Option<&str> {
251 self.active_plugin.map(|idx| self.plugins[idx].name())
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 struct EchoPlugin;
261
262 impl Plugin for EchoPlugin {
263 fn name(&self) -> &str {
264 "echo"
265 }
266
267 fn process_line(
268 &mut self,
269 line: &str,
270 _state: &ParseState,
271 _style: &ComputedStyle,
272 ) -> Option<ProcessResult> {
273 if line.starts_with("!echo ") {
274 Some(ProcessResult::Lines(vec![line[6..].to_string()]))
275 } else {
276 None
277 }
278 }
279
280 fn flush(&mut self) -> Option<Vec<String>> {
281 None
282 }
283
284 fn reset(&mut self) {}
285 }
286
287 struct BufferPlugin {
289 buffer: Vec<String>,
290 active: bool,
291 }
292
293 impl BufferPlugin {
294 fn new() -> Self {
295 Self {
296 buffer: Vec::new(),
297 active: false,
298 }
299 }
300 }
301
302 impl Plugin for BufferPlugin {
303 fn name(&self) -> &str {
304 "buffer"
305 }
306
307 fn process_line(
308 &mut self,
309 line: &str,
310 _state: &ParseState,
311 _style: &ComputedStyle,
312 ) -> Option<ProcessResult> {
313 if line == "!start" {
314 self.active = true;
315 self.buffer.clear();
316 return Some(ProcessResult::Continue);
317 }
318
319 if !self.active {
320 return None;
321 }
322
323 if line == "!end" {
324 self.active = false;
325 let result = std::mem::take(&mut self.buffer);
326 return Some(ProcessResult::Lines(result));
327 }
328
329 self.buffer.push(line.to_string());
330 Some(ProcessResult::Continue)
331 }
332
333 fn flush(&mut self) -> Option<Vec<String>> {
334 if self.buffer.is_empty() {
335 None
336 } else {
337 Some(std::mem::take(&mut self.buffer))
338 }
339 }
340
341 fn reset(&mut self) {
342 self.buffer.clear();
343 self.active = false;
344 }
345
346 fn is_active(&self) -> bool {
347 self.active
348 }
349 }
350
351 fn default_state() -> ParseState {
352 ParseState::new()
353 }
354
355 fn default_style() -> ComputedStyle {
356 ComputedStyle::default()
357 }
358
359 #[test]
360 fn test_process_result_constructors() {
361 let r1 = ProcessResult::line("hello");
362 assert_eq!(r1, ProcessResult::Lines(vec!["hello".to_string()]));
363
364 let r2 = ProcessResult::lines(vec!["a".to_string(), "b".to_string()]);
365 assert_eq!(
366 r2,
367 ProcessResult::Lines(vec!["a".to_string(), "b".to_string()])
368 );
369
370 let r3 = ProcessResult::cont();
371 assert_eq!(r3, ProcessResult::Continue);
372 }
373
374 #[test]
375 fn test_plugin_manager_new() {
376 let manager = PluginManager::new();
377 assert_eq!(manager.plugin_count(), 0);
378 assert!(!manager.has_active_plugin());
379 }
380
381 #[test]
382 fn test_plugin_manager_register() {
383 let mut manager = PluginManager::new();
384 manager.register(Box::new(EchoPlugin));
385 assert_eq!(manager.plugin_count(), 1);
386 assert_eq!(manager.plugin_names(), vec!["echo"]);
387 }
388
389 #[test]
390 fn test_plugin_manager_with_builtins() {
391 let manager = PluginManager::with_builtins();
392 assert!(manager.plugin_count() >= 1);
393 assert!(manager.plugin_names().contains(&"latex"));
394 }
395
396 #[test]
397 fn test_echo_plugin() {
398 let mut manager = PluginManager::new();
399 manager.register(Box::new(EchoPlugin));
400
401 let state = default_state();
402 let style = default_style();
403
404 let result = manager.process_line("!echo hello world", &state, &style);
406 assert_eq!(result, Some(vec!["hello world".to_string()]));
407
408 let result = manager.process_line("normal line", &state, &style);
410 assert_eq!(result, None);
411 }
412
413 #[test]
414 fn test_buffer_plugin() {
415 let mut manager = PluginManager::new();
416 manager.register(Box::new(BufferPlugin::new()));
417
418 let state = default_state();
419 let style = default_style();
420
421 let result = manager.process_line("!start", &state, &style);
423 assert_eq!(result, Some(vec![]));
424 assert!(manager.has_active_plugin());
425
426 let result = manager.process_line("line 1", &state, &style);
428 assert_eq!(result, Some(vec![]));
429
430 let result = manager.process_line("line 2", &state, &style);
431 assert_eq!(result, Some(vec![]));
432
433 let result = manager.process_line("!end", &state, &style);
435 assert_eq!(
436 result,
437 Some(vec!["line 1".to_string(), "line 2".to_string()])
438 );
439 assert!(!manager.has_active_plugin());
440 }
441
442 #[test]
443 fn test_buffer_plugin_flush() {
444 let mut manager = PluginManager::new();
445 manager.register(Box::new(BufferPlugin::new()));
446
447 let state = default_state();
448 let style = default_style();
449
450 manager.process_line("!start", &state, &style);
452 manager.process_line("line 1", &state, &style);
453 manager.process_line("line 2", &state, &style);
454
455 let result = manager.flush();
457 assert_eq!(result, vec!["line 1".to_string(), "line 2".to_string()]);
458 }
459
460 #[test]
461 fn test_plugin_reset() {
462 let mut manager = PluginManager::new();
463 manager.register(Box::new(BufferPlugin::new()));
464
465 let state = default_state();
466 let style = default_style();
467
468 manager.process_line("!start", &state, &style);
470 manager.process_line("line 1", &state, &style);
471 assert!(manager.has_active_plugin());
472
473 manager.reset();
475 assert!(!manager.has_active_plugin());
476
477 let result = manager.flush();
479 assert!(result.is_empty());
480 }
481
482 #[test]
483 fn test_active_plugin_name() {
484 let mut manager = PluginManager::new();
485 manager.register(Box::new(BufferPlugin::new()));
486
487 let state = default_state();
488 let style = default_style();
489
490 assert_eq!(manager.active_plugin_name(), None);
491
492 manager.process_line("!start", &state, &style);
493 assert_eq!(manager.active_plugin_name(), Some("buffer"));
494
495 manager.process_line("!end", &state, &style);
496 assert_eq!(manager.active_plugin_name(), None);
497 }
498}