xberg 1.0.5

High-performance document intelligence library for Rust. Extract text, metadata, and structured data from PDFs, Office documents, images, and 98 formats and 306 programming languages via tree-sitter code intelligence with async/sync APIs.
Documentation
//! Renderer plugin trait.
//!
//! This module defines the trait for implementing custom document renderers
//! that convert extraction results to output format strings.

use std::sync::Arc;

use crate::Result;
use crate::plugins::Plugin;
use crate::types::ExtractedDocument;
use crate::types::internal::InternalDocument;

/// Trait for document renderers that convert extraction results to output strings.
///
/// Renderers are typically stateless converters that transform extracted
/// content into a specific output format (Markdown, HTML, Djot, plain text,
/// etc.). They participate in the standard [`Plugin`]
/// lifecycle so custom renderers can be registered from any supported binding
/// language.
///
/// The format name is exposed via [`Plugin::name`]. For stateless renderers
/// the [`Plugin`] lifecycle methods (`version`, `initialize`, `shutdown`) all
/// take no-op defaults and need not be overridden.
///
/// # Thread Safety
///
/// Renderers must be `Send + Sync` (inherited from [`Plugin`]).
///
/// # Example
///
/// ```rust
/// use xberg::plugins::{Plugin, Renderer};
/// use xberg::{ExtractedDocument, Result};
///
/// struct CustomRenderer;
///
/// impl Plugin for CustomRenderer {
///     fn name(&self) -> &str { "custom" }
/// }
///
/// impl Renderer for CustomRenderer {
///     fn render_result(&self, result: &ExtractedDocument) -> Result<String> {
///         Ok(result.content.to_uppercase())
///     }
/// }
/// ```
pub trait Renderer: Plugin {
    /// Binding-safe rendering entry point for foreign-language plugin bridges.
    ///
    /// Accepts one public extraction result and returns the rendered output.
    fn render_result(&self, result: &ExtractedDocument) -> Result<String> {
        Ok(result.content.clone())
    }
}

/// Crate-private rendering capability used by native Rust renderers.
pub(crate) trait InternalRenderer: Plugin {
    /// Render the pipeline representation to the output format.
    fn render(&self, doc: &InternalDocument) -> Result<String>;
}

impl<T> Renderer for T
where
    T: InternalRenderer + ?Sized,
{
    fn render_result(&self, result: &ExtractedDocument) -> Result<String> {
        let doc = InternalDocument::from(result.clone());
        InternalRenderer::render(self, &doc)
    }
}

/// Register a renderer plugin with the global registry.
///
/// The renderer's format name is taken from [`Plugin::name`]. Registering a
/// renderer with a name that already exists replaces the previous renderer
/// for that format.
///
/// # Note on `Result` return type
///
/// Returns `Result<()>` for cross-language API symmetry required by the alef
/// trait-bridge codegen. The underlying `parking_lot::RwLock` cannot be
/// poisoned (parking_lot provides no poisoning semantics), so this function
/// never returns `Err` in practice.
pub fn register_renderer(renderer: Arc<dyn Renderer>) -> Result<()> {
    use crate::plugins::registry::get_renderer_registry;

    let registry = get_renderer_registry();
    let mut registry = registry.write();
    registry.register(renderer)
}

/// Unregister a renderer by format name.
///
/// # Errors
///
/// Returns an error if the registry lock is poisoned.
pub fn unregister_renderer(name: &str) -> Result<()> {
    use crate::plugins::registry::get_renderer_registry;

    let registry = get_renderer_registry();
    let mut registry = registry.write();
    registry.remove(name)
}

/// List names of all registered renderers.
///
/// # Errors
///
/// Returns an error if the registry lock is poisoned.
pub fn list_renderers() -> Result<Vec<String>> {
    use crate::plugins::registry::get_renderer_registry;

    let registry = get_renderer_registry();
    let registry = registry.read();
    Ok(registry.list())
}

/// Clear all renderers from the global registry.
///
/// Removes every renderer, including the built-in defaults (markdown, html,
/// djot, plain). After calling this no renderers are registered; re-register
/// as needed.
///
/// # Errors
///
/// Returns an error if the registry lock is poisoned.
pub fn clear_renderers() -> Result<()> {
    use crate::plugins::registry::get_renderer_registry;

    let registry = get_renderer_registry();
    let mut registry = registry.write();
    registry.clear_all()
}

#[cfg(test)]
mod tests {
    use super::*;

    struct MockRenderer {
        format: &'static str,
    }

    impl Plugin for MockRenderer {
        fn name(&self) -> &str {
            self.format
        }
    }

    impl InternalRenderer for MockRenderer {
        fn render(&self, doc: &crate::types::internal::InternalDocument) -> crate::Result<String> {
            Ok(format!("mock-{}-{}", self.format, doc.elements.len()))
        }
    }

    #[test]
    fn register_list_unregister_roundtrip() {
        register_renderer(Arc::new(MockRenderer { format: "test-fmt-a" })).unwrap();
        assert!(list_renderers().unwrap().contains(&"test-fmt-a".to_string()));

        unregister_renderer("test-fmt-a").unwrap();
        assert!(!list_renderers().unwrap().contains(&"test-fmt-a".to_string()));
    }

    #[test]
    fn register_list_clear_list_roundtrip() {
        register_renderer(Arc::new(MockRenderer { format: "test-fmt-b" })).unwrap();
        assert!(list_renderers().unwrap().contains(&"test-fmt-b".to_string()));

        clear_renderers().unwrap();
        assert!(list_renderers().unwrap().is_empty());

        use crate::plugins::registry::get_renderer_registry;
        let registry = get_renderer_registry();
        let mut registry = registry.write();
        registry.reset_to_defaults().unwrap();
    }
}