streamdown_plugin/
lib.rs

1//! Streamdown Plugin System
2//!
3//! This crate provides the plugin architecture for extending streamdown
4//! with custom content processors. Plugins can intercept lines of input
5//! and transform them before normal markdown processing.
6//!
7//! # Plugin Behavior
8//!
9//! - If a plugin returns `None`, it's not interested in the line
10//! - If a plugin returns `Some(ProcessResult::Lines(vec))`, those lines are emitted
11//! - If a plugin returns `Some(ProcessResult::Continue)`, it's buffering input
12//! - A plugin that returns non-None gets priority until it returns None
13//!
14//! # Example
15//!
16//! ```
17//! use streamdown_plugin::{Plugin, ProcessResult, PluginManager};
18//! use streamdown_core::state::ParseState;
19//! use streamdown_config::ComputedStyle;
20//!
21//! struct EchoPlugin;
22//!
23//! impl Plugin for EchoPlugin {
24//!     fn name(&self) -> &str { "echo" }
25//!
26//!     fn process_line(
27//!         &mut self,
28//!         line: &str,
29//!         _state: &ParseState,
30//!         _style: &ComputedStyle,
31//!     ) -> Option<ProcessResult> {
32//!         if line.starts_with("!echo ") {
33//!             Some(ProcessResult::Lines(vec![line[6..].to_string()]))
34//!         } else {
35//!             None
36//!         }
37//!     }
38//!
39//!     fn flush(&mut self) -> Option<Vec<String>> { None }
40//!     fn reset(&mut self) {}
41//! }
42//! ```
43
44pub mod builtin;
45pub mod latex;
46
47use streamdown_config::ComputedStyle;
48use streamdown_core::state::ParseState;
49
50/// Result of plugin processing.
51#[derive(Debug, Clone, PartialEq)]
52pub enum ProcessResult {
53    /// Emit these formatted lines instead of normal processing
54    Lines(Vec<String>),
55    /// Plugin is buffering, continue without further processing
56    Continue,
57}
58
59impl ProcessResult {
60    /// Create a result with a single line.
61    pub fn line(s: impl Into<String>) -> Self {
62        Self::Lines(vec![s.into()])
63    }
64
65    /// Create a result with multiple lines.
66    pub fn lines(lines: Vec<String>) -> Self {
67        Self::Lines(lines)
68    }
69
70    /// Create a continue result.
71    pub fn cont() -> Self {
72        Self::Continue
73    }
74}
75
76/// Plugin trait for custom content processors.
77///
78/// Plugins intercept input lines and can:
79/// - Transform them into different output
80/// - Buffer multiple lines before emitting
81/// - Pass through to normal processing
82pub trait Plugin: Send + Sync {
83    /// Plugin name for identification and logging.
84    fn name(&self) -> &str;
85
86    /// Process a line of input.
87    ///
88    /// # Returns
89    /// - `None`: Plugin not interested, continue normal processing
90    /// - `Some(ProcessResult::Lines(vec))`: Emit these lines instead
91    /// - `Some(ProcessResult::Continue)`: Plugin consumed input, keep buffering
92    fn process_line(
93        &mut self,
94        line: &str,
95        state: &ParseState,
96        style: &ComputedStyle,
97    ) -> Option<ProcessResult>;
98
99    /// Called when stream ends to flush any buffered content.
100    ///
101    /// # Returns
102    /// - `None`: Nothing to flush
103    /// - `Some(vec)`: Remaining buffered lines to emit
104    fn flush(&mut self) -> Option<Vec<String>>;
105
106    /// Reset plugin state.
107    ///
108    /// Called when starting a new document or clearing state.
109    fn reset(&mut self);
110
111    /// Plugin priority (lower = higher priority).
112    ///
113    /// Default is 0. Plugins with lower priority numbers are called first.
114    fn priority(&self) -> i32 {
115        0
116    }
117
118    /// Whether this plugin is currently active (buffering).
119    ///
120    /// Active plugins get priority for subsequent lines.
121    fn is_active(&self) -> bool {
122        false
123    }
124}
125
126/// Plugin manager for registering and coordinating plugins.
127///
128/// The manager handles:
129/// - Plugin registration with priority sorting
130/// - Active plugin priority (a plugin processing multi-line content)
131/// - Flushing all plugins at end of stream
132#[derive(Default)]
133pub struct PluginManager {
134    /// Registered plugins (sorted by priority)
135    plugins: Vec<Box<dyn Plugin>>,
136    /// Index of the currently active plugin (has priority)
137    active_plugin: Option<usize>,
138}
139
140impl PluginManager {
141    /// Create a new empty plugin manager.
142    pub fn new() -> Self {
143        Self::default()
144    }
145
146    /// Create a plugin manager with built-in plugins.
147    pub fn with_builtins() -> Self {
148        let mut manager = Self::new();
149        manager.register(Box::new(latex::LatexPlugin::new()));
150        manager
151    }
152
153    /// Register a plugin.
154    ///
155    /// Plugins are sorted by priority after registration.
156    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    /// Get the number of registered plugins.
162    pub fn plugin_count(&self) -> usize {
163        self.plugins.len()
164    }
165
166    /// Get plugin names.
167    pub fn plugin_names(&self) -> Vec<&str> {
168        self.plugins.iter().map(|p| p.name()).collect()
169    }
170
171    /// Process a line through registered plugins.
172    ///
173    /// # Returns
174    /// - `None`: No plugin handled the line, continue normal processing
175    /// - `Some(vec)`: Plugin produced these lines, skip normal processing
176    pub fn process_line(
177        &mut self,
178        line: &str,
179        state: &ParseState,
180        style: &ComputedStyle,
181    ) -> Option<Vec<String>> {
182        // If there's an active plugin, give it priority
183        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                    // Plugin finished, clear active
188                    self.active_plugin = None;
189                    return Some(lines);
190                }
191                Some(ProcessResult::Continue) => {
192                    // Plugin still active
193                    return Some(vec![]);
194                }
195                None => {
196                    // Plugin released priority
197                    self.active_plugin = None;
198                }
199            }
200        }
201
202        // Try each plugin in priority order
203        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                    // This plugin is now active
210                    self.active_plugin = Some(idx);
211                    return Some(vec![]);
212                }
213                None => continue,
214            }
215        }
216
217        None
218    }
219
220    /// Flush all plugins at end of stream.
221    ///
222    /// Returns all remaining buffered content from all plugins.
223    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    /// Reset all plugins.
237    pub fn reset(&mut self) {
238        for plugin in &mut self.plugins {
239            plugin.reset();
240        }
241        self.active_plugin = None;
242    }
243
244    /// Check if any plugin is currently active.
245    pub fn has_active_plugin(&self) -> bool {
246        self.active_plugin.is_some()
247    }
248
249    /// Get the name of the active plugin, if any.
250    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    /// Simple test plugin that echoes lines starting with "!echo "
260    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    /// Test plugin that buffers lines until "!end"
288    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        // Echo plugin should handle this
405        let result = manager.process_line("!echo hello world", &state, &style);
406        assert_eq!(result, Some(vec!["hello world".to_string()]));
407
408        // Echo plugin should not handle this
409        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        // Start buffering
422        let result = manager.process_line("!start", &state, &style);
423        assert_eq!(result, Some(vec![]));
424        assert!(manager.has_active_plugin());
425
426        // Buffer lines
427        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        // End buffering
434        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        // Start buffering without ending
451        manager.process_line("!start", &state, &style);
452        manager.process_line("line 1", &state, &style);
453        manager.process_line("line 2", &state, &style);
454
455        // Flush should return buffered content
456        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        // Start buffering
469        manager.process_line("!start", &state, &style);
470        manager.process_line("line 1", &state, &style);
471        assert!(manager.has_active_plugin());
472
473        // Reset
474        manager.reset();
475        assert!(!manager.has_active_plugin());
476
477        // Flush should return nothing after reset
478        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}