Skip to main content

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            line.strip_prefix("!echo ")
274                .map(|stripped| ProcessResult::Lines(vec![stripped.to_string()]))
275        }
276
277        fn flush(&mut self) -> Option<Vec<String>> {
278            None
279        }
280
281        fn reset(&mut self) {}
282    }
283
284    /// Test plugin that buffers lines until "!end"
285    struct BufferPlugin {
286        buffer: Vec<String>,
287        active: bool,
288    }
289
290    impl BufferPlugin {
291        fn new() -> Self {
292            Self {
293                buffer: Vec::new(),
294                active: false,
295            }
296        }
297    }
298
299    impl Plugin for BufferPlugin {
300        fn name(&self) -> &str {
301            "buffer"
302        }
303
304        fn process_line(
305            &mut self,
306            line: &str,
307            _state: &ParseState,
308            _style: &ComputedStyle,
309        ) -> Option<ProcessResult> {
310            if line == "!start" {
311                self.active = true;
312                self.buffer.clear();
313                return Some(ProcessResult::Continue);
314            }
315
316            if !self.active {
317                return None;
318            }
319
320            if line == "!end" {
321                self.active = false;
322                let result = std::mem::take(&mut self.buffer);
323                return Some(ProcessResult::Lines(result));
324            }
325
326            self.buffer.push(line.to_string());
327            Some(ProcessResult::Continue)
328        }
329
330        fn flush(&mut self) -> Option<Vec<String>> {
331            if self.buffer.is_empty() {
332                None
333            } else {
334                Some(std::mem::take(&mut self.buffer))
335            }
336        }
337
338        fn reset(&mut self) {
339            self.buffer.clear();
340            self.active = false;
341        }
342
343        fn is_active(&self) -> bool {
344            self.active
345        }
346    }
347
348    fn default_state() -> ParseState {
349        ParseState::new()
350    }
351
352    fn default_style() -> ComputedStyle {
353        ComputedStyle::default()
354    }
355
356    #[test]
357    fn test_process_result_constructors() {
358        let r1 = ProcessResult::line("hello");
359        assert_eq!(r1, ProcessResult::Lines(vec!["hello".to_string()]));
360
361        let r2 = ProcessResult::lines(vec!["a".to_string(), "b".to_string()]);
362        assert_eq!(
363            r2,
364            ProcessResult::Lines(vec!["a".to_string(), "b".to_string()])
365        );
366
367        let r3 = ProcessResult::cont();
368        assert_eq!(r3, ProcessResult::Continue);
369    }
370
371    #[test]
372    fn test_plugin_manager_new() {
373        let manager = PluginManager::new();
374        assert_eq!(manager.plugin_count(), 0);
375        assert!(!manager.has_active_plugin());
376    }
377
378    #[test]
379    fn test_plugin_manager_register() {
380        let mut manager = PluginManager::new();
381        manager.register(Box::new(EchoPlugin));
382        assert_eq!(manager.plugin_count(), 1);
383        assert_eq!(manager.plugin_names(), vec!["echo"]);
384    }
385
386    #[test]
387    fn test_plugin_manager_with_builtins() {
388        let manager = PluginManager::with_builtins();
389        assert!(manager.plugin_count() >= 1);
390        assert!(manager.plugin_names().contains(&"latex"));
391    }
392
393    #[test]
394    fn test_echo_plugin() {
395        let mut manager = PluginManager::new();
396        manager.register(Box::new(EchoPlugin));
397
398        let state = default_state();
399        let style = default_style();
400
401        // Echo plugin should handle this
402        let result = manager.process_line("!echo hello world", &state, &style);
403        assert_eq!(result, Some(vec!["hello world".to_string()]));
404
405        // Echo plugin should not handle this
406        let result = manager.process_line("normal line", &state, &style);
407        assert_eq!(result, None);
408    }
409
410    #[test]
411    fn test_buffer_plugin() {
412        let mut manager = PluginManager::new();
413        manager.register(Box::new(BufferPlugin::new()));
414
415        let state = default_state();
416        let style = default_style();
417
418        // Start buffering
419        let result = manager.process_line("!start", &state, &style);
420        assert_eq!(result, Some(vec![]));
421        assert!(manager.has_active_plugin());
422
423        // Buffer lines
424        let result = manager.process_line("line 1", &state, &style);
425        assert_eq!(result, Some(vec![]));
426
427        let result = manager.process_line("line 2", &state, &style);
428        assert_eq!(result, Some(vec![]));
429
430        // End buffering
431        let result = manager.process_line("!end", &state, &style);
432        assert_eq!(
433            result,
434            Some(vec!["line 1".to_string(), "line 2".to_string()])
435        );
436        assert!(!manager.has_active_plugin());
437    }
438
439    #[test]
440    fn test_buffer_plugin_flush() {
441        let mut manager = PluginManager::new();
442        manager.register(Box::new(BufferPlugin::new()));
443
444        let state = default_state();
445        let style = default_style();
446
447        // Start buffering without ending
448        manager.process_line("!start", &state, &style);
449        manager.process_line("line 1", &state, &style);
450        manager.process_line("line 2", &state, &style);
451
452        // Flush should return buffered content
453        let result = manager.flush();
454        assert_eq!(result, vec!["line 1".to_string(), "line 2".to_string()]);
455    }
456
457    #[test]
458    fn test_plugin_reset() {
459        let mut manager = PluginManager::new();
460        manager.register(Box::new(BufferPlugin::new()));
461
462        let state = default_state();
463        let style = default_style();
464
465        // Start buffering
466        manager.process_line("!start", &state, &style);
467        manager.process_line("line 1", &state, &style);
468        assert!(manager.has_active_plugin());
469
470        // Reset
471        manager.reset();
472        assert!(!manager.has_active_plugin());
473
474        // Flush should return nothing after reset
475        let result = manager.flush();
476        assert!(result.is_empty());
477    }
478
479    #[test]
480    fn test_active_plugin_name() {
481        let mut manager = PluginManager::new();
482        manager.register(Box::new(BufferPlugin::new()));
483
484        let state = default_state();
485        let style = default_style();
486
487        assert_eq!(manager.active_plugin_name(), None);
488
489        manager.process_line("!start", &state, &style);
490        assert_eq!(manager.active_plugin_name(), Some("buffer"));
491
492        manager.process_line("!end", &state, &style);
493        assert_eq!(manager.active_plugin_name(), None);
494    }
495}