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 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 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 let result = manager.process_line("!echo hello world", &state, &style);
403 assert_eq!(result, Some(vec!["hello world".to_string()]));
404
405 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 let result = manager.process_line("!start", &state, &style);
420 assert_eq!(result, Some(vec![]));
421 assert!(manager.has_active_plugin());
422
423 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 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 manager.process_line("!start", &state, &style);
449 manager.process_line("line 1", &state, &style);
450 manager.process_line("line 2", &state, &style);
451
452 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 manager.process_line("!start", &state, &style);
467 manager.process_line("line 1", &state, &style);
468 assert!(manager.has_active_plugin());
469
470 manager.reset();
472 assert!(!manager.has_active_plugin());
473
474 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}