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};