Plugin Migration Guide¶
This guide explains how to migrate existing SVGO plugins to the new vexy_svgo plugin architecture.
Overview¶
The new plugin architecture uses composition over inheritance with a visitor pattern for SVG document traversal. Each plugin implements the Plugin
trait and uses visitors to perform transformations.
Migration Process¶
1. Plugin Structure¶
Old SVGO Plugin (JavaScript):
exports.type = 'visitor';
exports.name = 'removeComments';
exports.description = 'removes comments';
exports.fn = (root, params) => {
// Plugin logic here
};
New Vexy SVGO Plugin (Rust):
use crate::Plugin;
use vexy_svgo_core::ast::{Document, Element};
use vexy_svgo_core::visitor::Visitor;
use anyhow::Result;
pub struct RemoveCommentsPlugin {
preserve_patterns: bool,
}
impl Plugin for RemoveCommentsPlugin {
fn name(&self) -> &'static str {
"removeComments"
}
fn description(&self) -> &'static str {
"Remove comments from SVG document"
}
fn apply(&self, document: &mut Document) -> Result<()> {
let mut visitor = CommentRemovalVisitor::new(self.preserve_patterns);
vexy_svgo_core::visitor::walk_document(&mut visitor, document)?;
Ok(())
}
}
2. Visitor Implementation¶
Create a visitor struct that implements the Visitor
trait:
struct CommentRemovalVisitor {
preserve_patterns: bool,
}
impl Visitor<'_> for CommentRemovalVisitor {
fn visit_element_enter(&mut self, element: &mut Element<'_>) -> Result<()> {
// Apply transformations to the element
element.children.retain(|child| {
match child {
Node::Comment(comment) => self.should_keep_comment(comment),
_ => true,
}
});
Ok(())
}
}
3. Parameter Validation¶
Implement parameter validation in the plugin:
fn validate_params(&self, params: &serde_json::Value) -> anyhow::Result<()> {
if let Some(preserve) = params.get("preservePatterns") {
if !preserve.is_boolean() {
return Err(anyhow::anyhow!(
"preservePatterns must be a boolean"
));
}
}
Ok(())
}
Examples¶
Example 1: RemoveComments Plugin¶
Original SVGO Logic: - Remove all comments from SVG documents - Optionally preserve "legal" comments (starting with !)
Migration Steps:
1. Create RemoveCommentsPlugin
struct with configuration
2. Implement Plugin
trait with name, description, and apply methods
3. Create CommentRemovalVisitor
that filters comments based on configuration
4. Use visitor pattern to traverse and modify the document
Files:
- crates/plugin-sdk/src/plugins/remove_comments.rs
- Tests in the same file with #[cfg(test)]
Example 2: RemoveEmptyAttrs Plugin¶
Original SVGO Logic: - Remove attributes with empty values - Optionally preserve specific attributes (class, id)
Migration Steps:
1. Create RemoveEmptyAttrsPlugin
struct with preservation settings
2. Implement visitor that filters empty attributes
3. Add logic to handle preservation rules
4. Comprehensive testing for edge cases
Files:
- crates/plugin-sdk/src/plugins/remove_empty_attrs.rs
Best Practices¶
1. Configuration Management¶
pub struct MyPlugin {
option1: bool,
option2: String,
}
impl MyPlugin {
pub fn new() -> Self {
Self {
option1: true,
option2: "default".to_string(),
}
}
pub fn with_options(option1: bool, option2: String) -> Self {
Self { option1, option2 }
}
}
2. Error Handling¶
Use anyhow::Result
for error handling and provide meaningful error messages:
fn validate_params(&self, params: &serde_json::Value) -> anyhow::Result<()> {
if let Some(value) = params.get("myParam") {
if !value.is_boolean() {
return Err(anyhow::anyhow!("myParam must be a boolean, got: {}", value));
}
}
Ok(())
}
3. Testing Strategy¶
Create comprehensive tests covering:
- Plugin creation and configuration
- Parameter validation (valid and invalid cases)
- Visitor logic isolation
- Integration with documents
- Edge cases and error conditions
#[cfg(test)]
mod tests {
use super::*;
use vexy_svgo_core::ast::Document;
use serde_json::json;
#[test]
fn test_plugin_creation() {
let plugin = MyPlugin::new();
assert_eq!(plugin.name(), "myPlugin");
}
#[test]
fn test_parameter_validation() {
let plugin = MyPlugin::new();
assert!(plugin.validate_params(&json!({})).is_ok());
assert!(plugin.validate_params(&json!({"invalid": "value"})).is_err());
}
#[test]
fn test_plugin_application() {
let plugin = MyPlugin::new();
let mut doc = Document::new();
// Set up test document
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
// Verify transformations
}
}
4. Integration Testing¶
Create integration tests that verify plugins work together:
// crates/plugin-sdk/tests/integration_test.rs
#[test]
fn test_multiple_plugins() {
let mut registry = PluginRegistry::new();
registry.register(RemoveCommentsPlugin::new());
registry.register(RemoveEmptyAttrsPlugin::new());
let configs = vec![
PluginConfig { name: "removeComments".to_string(), /* ... */ },
PluginConfig { name: "removeEmptyAttrs".to_string(), /* ... */ },
];
let mut doc = create_test_document();
registry.apply_plugins(&mut doc, &configs).unwrap();
// Verify combined effects
}
Plugin Registry Integration¶
1. Register Plugins¶
// In create_default_registry() function
let mut registry = PluginRegistry::new();
registry.register(RemoveCommentsPlugin::new());
registry.register(RemoveEmptyAttrsPlugin::new());
// Add more plugins...
2. Plugin Configuration¶
let config = PluginConfig {
name: "removeComments".to_string(),
params: json!({
"preservePatterns": true
}),
enabled: true,
};
Common Migration Patterns¶
1. Simple Element Transformation¶
impl Visitor<'_> for MyVisitor {
fn visit_element_enter(&mut self, element: &mut Element<'_>) -> Result<()> {
// Modify attributes
element.attributes.retain(|name, value| {
// Filtering logic
});
// Modify children
element.children.retain(|child| {
// Filtering logic
});
Ok(())
}
}
2. Conditional Processing¶
impl Visitor<'_> for MyVisitor {
fn visit_element_enter(&mut self, element: &mut Element<'_>) -> Result<()> {
if self.should_process_element(&element.name) {
// Apply transformations
}
Ok(())
}
}
3. Stateful Visitors¶
struct StatefulVisitor {
state: SomeState,
counters: HashMap<String, usize>,
}
impl Visitor<'_> for StatefulVisitor {
fn visit_element_enter(&mut self, element: &mut Element<'_>) -> Result<()> {
// Update state based on element
self.state.update(&element);
Ok(())
}
}
Performance Considerations¶
- Minimize Allocations: Reuse data structures where possible
- Efficient Filtering: Use
retain()
instead of collect/filter/rebuild - Early Returns: Skip processing when conditions aren't met
- Visitor Efficiency: Only implement necessary visitor methods
File Organization¶
crates/plugin-sdk/src/plugins/
├── mod.rs # Plugin exports
├── remove_comments.rs # Remove comments plugin
├── remove_empty_attrs.rs # Remove empty attributes plugin
└── ... # Additional plugins
crates/plugin-sdk/tests/
├── integration_test.rs # Plugin integration tests
├── registry_test.rs # Registry system tests
└── ... # Additional test suites
crates/plugin-sdk/examples/
├── plugin_composition.rs # Multi-plugin usage example
└── ... # Additional examples
Migration Checklist¶
For each plugin migration:
- [ ] Create plugin struct with configuration options
- [ ] Implement
Plugin
trait (name, description, validate_params, apply) - [ ] Create visitor struct implementing
Visitor
trait - [ ] Add comprehensive unit tests
- [ ] Add integration tests
- [ ] Update plugin registry in
create_default_registry()
- [ ] Add plugin to module exports in
mod.rs
- [ ] Document any SVGO compatibility differences
- [ ] Verify performance characteristics
SVGO Compatibility Notes¶
- Plugin names should match SVGO plugin names exactly
- Parameter names should match SVGO parameter names
- Default behaviors should match SVGO defaults
- Document any intentional differences in behavior
- Maintain backward compatibility where possible