ass_core/plugin/mod.rs
1//! Plugin system for extending ASS parsing and rendering capabilities.
2//!
3//! This module provides a trait-based extension system allowing custom tag handlers,
4//! section processors, and rendering backends to be registered at runtime. Designed
5//! for zero-allocation hot paths and efficient lookup via optimized hash maps.
6//!
7//! ## Architecture
8//!
9//! - **`TagHandler`**: Process custom override tags (e.g., `{\custom}`)
10//! - **`SectionProcessor`**: Handle non-standard sections (e.g., `[Aegisub Project]`)
11//! - **`ExtensionRegistry`**: Central registry for all extensions
12//!
13//! ## Example
14//!
15//! ```rust
16//! use ass_core::plugin::{ExtensionRegistry, TagHandler, TagResult};
17//!
18//! struct CustomColorTag;
19//!
20//! impl TagHandler for CustomColorTag {
21//!     fn name(&self) -> &'static str { "customcolor" }
22//!
23//!     fn process(&self, args: &str) -> TagResult {
24//!         // Custom color processing logic
25//!         TagResult::Processed
26//!     }
27//! }
28//!
29//! let mut registry = ExtensionRegistry::new();
30//! registry.register_tag_handler(Box::new(CustomColorTag));
31//! ```
32
33use alloc::{
34    boxed::Box,
35    string::{String, ToString},
36    vec::Vec,
37};
38use core::fmt;
39
40#[cfg(feature = "std")]
41use std::collections::HashMap;
42
43#[cfg(not(feature = "std"))]
44use hashbrown::HashMap;
45pub mod sections;
46pub mod tags;
47
48#[cfg(test)]
49mod tests;
50
51/// Result of tag processing operations
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum TagResult {
54    /// Tag was successfully processed
55    Processed,
56    /// Tag was ignored (not handled by this processor)
57    Ignored,
58    /// Tag processing failed with error message
59    Failed(String),
60}
61
62/// Result of section processing operations
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum SectionResult {
65    /// Section was successfully processed
66    Processed,
67    /// Section was ignored (not handled by this processor)
68    Ignored,
69    /// Section processing failed with error message
70    Failed(String),
71}
72
73/// Trait for handling custom ASS override tags
74///
75/// Implementors can process custom tags that extend standard ASS functionality.
76/// Tag handlers are called during parsing when unknown tags are encountered.
77pub trait TagHandler: Send + Sync {
78    /// Unique name identifier for this tag handler
79    fn name(&self) -> &'static str;
80
81    /// Process a tag with its arguments
82    ///
83    /// # Arguments
84    /// * `args` - Raw tag arguments as string slice
85    ///
86    /// # Returns
87    /// * `TagResult::Processed` - Tag was handled successfully
88    /// * `TagResult::Ignored` - Tag not recognized by this handler
89    /// * `TagResult::Failed` - Error occurred during processing
90    fn process(&self, args: &str) -> TagResult;
91
92    /// Optional validation of tag arguments during parsing
93    fn validate(&self, args: &str) -> bool {
94        !args.is_empty()
95    }
96}
97
98/// Trait for handling custom ASS sections
99///
100/// Implementors can process non-standard sections that extend ASS functionality.
101/// Section processors are called when unknown section headers are encountered.
102pub trait SectionProcessor: Send + Sync {
103    /// Unique name identifier for this section processor
104    fn name(&self) -> &'static str;
105
106    /// Process section header and content lines
107    ///
108    /// # Arguments
109    /// * `header` - Section header (e.g., "Aegisub Project")
110    /// * `lines` - All lines belonging to this section
111    ///
112    /// # Returns
113    /// * `SectionResult::Processed` - Section was handled successfully
114    /// * `SectionResult::Ignored` - Section not recognized by this processor
115    /// * `SectionResult::Failed` - Error occurred during processing
116    fn process(&self, header: &str, lines: &[&str]) -> SectionResult;
117
118    /// Optional validation of section format
119    fn validate(&self, header: &str, lines: &[&str]) -> bool {
120        !header.is_empty() && !lines.is_empty()
121    }
122}
123
124/// Errors that can occur during plugin operations
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum PluginError {
127    /// Handler with same name already registered
128    DuplicateHandler(String),
129    /// Handler not found for given name
130    HandlerNotFound(String),
131    /// Plugin processing failed
132    ProcessingFailed(String),
133    /// Invalid plugin configuration
134    InvalidConfig(String),
135}
136
137impl fmt::Display for PluginError {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        match self {
140            Self::DuplicateHandler(name) => {
141                write!(f, "Handler '{name}' already registered")
142            }
143            Self::HandlerNotFound(name) => {
144                write!(f, "Handler '{name}' not found")
145            }
146            Self::ProcessingFailed(msg) => {
147                write!(f, "Plugin processing failed: {msg}")
148            }
149            Self::InvalidConfig(msg) => {
150                write!(f, "Invalid plugin configuration: {msg}")
151            }
152        }
153    }
154}
155
156#[cfg(feature = "std")]
157impl std::error::Error for PluginError {}
158
159/// Central registry for all ASS format extensions
160///
161/// Manages registration and lookup of tag handlers and section processors.
162/// Optimized for fast lookup during parsing with minimal memory overhead.
163pub struct ExtensionRegistry {
164    /// Registered tag handlers indexed by tag name
165    tag_handlers: HashMap<String, Box<dyn TagHandler>>,
166    /// Registered section processors indexed by section name
167    section_processors: HashMap<String, Box<dyn SectionProcessor>>,
168}
169
170impl ExtensionRegistry {
171    /// Create a new empty extension registry
172    #[must_use]
173    pub fn new() -> Self {
174        Self {
175            tag_handlers: HashMap::new(),
176            section_processors: HashMap::new(),
177        }
178    }
179
180    /// Register a new tag handler
181    ///
182    /// # Arguments
183    /// * `handler` - Boxed tag handler implementation
184    ///
185    /// # Errors
186    /// Returns `PluginError::DuplicateHandler` if handler name already exists
187    pub fn register_tag_handler(&mut self, handler: Box<dyn TagHandler>) -> Result<()> {
188        let name = handler.name().to_string();
189
190        if self.tag_handlers.contains_key(&name) {
191            return Err(PluginError::DuplicateHandler(name));
192        }
193
194        self.tag_handlers.insert(name, handler);
195        Ok(())
196    }
197
198    /// Register a new section processor
199    ///
200    /// # Arguments
201    /// * `processor` - Boxed section processor implementation
202    ///
203    /// # Errors
204    /// Returns `PluginError::DuplicateHandler` if processor name already exists
205    pub fn register_section_processor(
206        &mut self,
207        processor: Box<dyn SectionProcessor>,
208    ) -> Result<()> {
209        let name = processor.name().to_string();
210
211        if self.section_processors.contains_key(&name) {
212            return Err(PluginError::DuplicateHandler(name));
213        }
214
215        self.section_processors.insert(name, processor);
216        Ok(())
217    }
218
219    /// Process a tag using registered handlers
220    ///
221    /// # Arguments
222    /// * `tag_name` - Name of the tag to process
223    /// * `args` - Tag arguments as string slice
224    ///
225    /// # Returns
226    /// * `Some(TagResult)` - If a handler was found and executed
227    /// * `None` - If no handler was registered for this tag
228    #[must_use]
229    pub fn process_tag(&self, tag_name: &str, args: &str) -> Option<TagResult> {
230        self.tag_handlers
231            .get(tag_name)
232            .map(|handler| handler.process(args))
233    }
234
235    /// Process a section using registered processors
236    ///
237    /// # Arguments
238    /// * `section_name` - Name of the section to process
239    /// * `header` - Section header line
240    /// * `lines` - All lines in the section
241    ///
242    /// # Returns
243    /// * `Some(SectionResult)` - If a processor was found and executed
244    /// * `None` - If no processor was registered for this section
245    #[must_use]
246    pub fn process_section(
247        &self,
248        section_name: &str,
249        header: &str,
250        lines: &[&str],
251    ) -> Option<SectionResult> {
252        self.section_processors
253            .get(section_name)
254            .map(|processor| processor.process(header, lines))
255    }
256
257    /// Get list of registered tag handler names
258    #[must_use]
259    pub fn tag_handler_names(&self) -> Vec<&str> {
260        self.tag_handlers.keys().map(String::as_str).collect()
261    }
262
263    /// Get list of registered section processor names
264    #[must_use]
265    pub fn section_processor_names(&self) -> Vec<&str> {
266        self.section_processors.keys().map(String::as_str).collect()
267    }
268
269    /// Check if a tag handler is registered
270    #[must_use]
271    pub fn has_tag_handler(&self, name: &str) -> bool {
272        self.tag_handlers.contains_key(name)
273    }
274
275    /// Check if a section processor is registered
276    #[must_use]
277    pub fn has_section_processor(&self, name: &str) -> bool {
278        self.section_processors.contains_key(name)
279    }
280
281    /// Remove a tag handler by name
282    ///
283    /// # Returns
284    /// * `Some(handler)` - If handler was found and removed
285    /// * `None` - If no handler with that name was registered
286    pub fn remove_tag_handler(&mut self, name: &str) -> Option<Box<dyn TagHandler>> {
287        self.tag_handlers.remove(name)
288    }
289
290    /// Remove a section processor by name
291    ///
292    /// # Returns
293    /// * `Some(processor)` - If processor was found and removed
294    /// * `None` - If no processor with that name was registered
295    pub fn remove_section_processor(&mut self, name: &str) -> Option<Box<dyn SectionProcessor>> {
296        self.section_processors.remove(name)
297    }
298
299    /// Clear all registered handlers and processors
300    pub fn clear(&mut self) {
301        self.tag_handlers.clear();
302        self.section_processors.clear();
303    }
304
305    /// Get total number of registered extensions
306    #[must_use]
307    pub fn extension_count(&self) -> usize {
308        self.tag_handlers.len() + self.section_processors.len()
309    }
310}
311
312impl Default for ExtensionRegistry {
313    fn default() -> Self {
314        Self::new()
315    }
316}
317
318impl fmt::Debug for ExtensionRegistry {
319    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        f.debug_struct("ExtensionRegistry")
321            .field("tag_handlers", &self.tag_handler_names())
322            .field("section_processors", &self.section_processor_names())
323            .finish()
324    }
325}
326
327/// Result type for plugin operations
328pub type Result<T> = core::result::Result<T, PluginError>;
329
330pub use sections::aegisub::{
331    create_aegisub_processors, AegisubExtradataProcessor, AegisubProjectProcessor,
332};
333pub use tags::{
334    advanced::{create_advanced_handlers, BlurEdgesTagHandler, BorderTagHandler, ShadowTagHandler},
335    alignment::{
336        create_alignment_handlers, AlignmentTagHandler, NumpadAlignmentTagHandler,
337        WrappingStyleTagHandler,
338    },
339    animation::{
340        create_animation_handlers, FadeTagHandler, SimpleFadeTagHandler, TransformTagHandler,
341    },
342    clipping::{create_clipping_handlers, ClipTagHandler},
343    color::{
344        create_color_handlers, Alpha1TagHandler, Alpha2TagHandler, Alpha3TagHandler,
345        Alpha4TagHandler, AlphaTagHandler, Color1TagHandler, Color2TagHandler, Color3TagHandler,
346        Color4TagHandler, PrimaryColorTagHandler,
347    },
348    font::{create_font_handlers, FontEncodingTagHandler, FontNameTagHandler, FontSizeTagHandler},
349    formatting::{
350        create_formatting_handlers, BoldTagHandler, ItalicTagHandler, StrikeoutTagHandler,
351        UnderlineTagHandler,
352    },
353    karaoke::{
354        create_karaoke_handlers, BasicKaraokeTagHandler, FillKaraokeTagHandler,
355        KaraokeTimingTagHandler, OutlineKaraokeTagHandler,
356    },
357    misc::{create_misc_handlers, OriginTagHandler, ResetTagHandler, ShortRotationTagHandler},
358    position::{create_position_handlers, MoveTagHandler, PositionTagHandler},
359    special::{
360        create_special_handlers, HardLineBreakTagHandler, HardSpaceTagHandler,
361        SoftLineBreakTagHandler,
362    },
363    transform::{
364        create_transform_handlers, RotationXTagHandler, RotationYTagHandler, RotationZTagHandler,
365        ScaleXTagHandler, ScaleYTagHandler, ShearXTagHandler, ShearYTagHandler, SpacingTagHandler,
366    },
367};