use super::embed_renderer::{
AudioRenderer, EmbedRenderer, IframeRenderer, MarkdownEmbedRenderer,
ModelViewerRenderer, NotebookRenderer, PdfRenderer, TableRenderer, VideoRenderer,
};
pub struct RendererRegistry {
renderers: Vec<&'static dyn EmbedRenderer>,
}
impl RendererRegistry {
pub fn builtin() -> RendererRegistryBuilder {
RendererRegistryBuilder::new().with_builtins()
}
pub fn empty() -> RendererRegistryBuilder {
RendererRegistryBuilder::new()
}
pub fn lookup(&self, ext: &str) -> Option<&'static dyn EmbedRenderer> {
if ext.is_empty() {
return None;
}
self.renderers
.iter()
.copied()
.find(|r| r.extensions().iter().any(|e| e.eq_ignore_ascii_case(ext)))
}
pub fn all(&self) -> &[&'static dyn EmbedRenderer] {
&self.renderers
}
}
pub struct RendererRegistryBuilder {
renderers: Vec<&'static dyn EmbedRenderer>,
}
impl RendererRegistryBuilder {
fn new() -> Self {
Self {
renderers: Vec::new(),
}
}
fn with_builtins(mut self) -> Self {
self.renderers.push(&MarkdownEmbedRenderer);
self.renderers.push(&IframeRenderer);
self.renderers.push(&PdfRenderer);
self.renderers.push(&AudioRenderer);
self.renderers.push(&VideoRenderer);
self.renderers.push(&NotebookRenderer);
self.renderers.push(&ModelViewerRenderer);
self.renderers.push(&TableRenderer);
self
}
pub fn with_static(mut self, r: &'static dyn EmbedRenderer) -> Self {
self.renderers.push(r);
self
}
pub fn with_boxed(mut self, r: Box<dyn EmbedRenderer>) -> Self {
let leaked: &'static dyn EmbedRenderer = Box::leak(r);
self.renderers.push(leaked);
self
}
pub fn build(self) -> RendererRegistry {
RendererRegistry {
renderers: self.renderers,
}
}
}
impl Default for RendererRegistry {
fn default() -> Self {
RendererRegistry::builtin().build()
}
}
#[cfg(test)]
mod tests {
use super::super::embed_renderer::{ParsedEmbed, RenderedEmbed};
use super::*;
#[derive(Debug)]
struct CustomRenderer;
impl EmbedRenderer for CustomRenderer {
fn extensions(&self) -> &[&'static str] {
&["xyz"]
}
fn render(&self, _: &ParsedEmbed<'_>) -> RenderedEmbed {
RenderedEmbed::Inline("custom".to_string())
}
}
#[test]
fn test_builtin_registry_has_core_renderers() {
let reg = RendererRegistry::builtin().build();
for ext in ["md", "html", "pdf", "mp3", "mp4", "ipynb", "glb", "csv"] {
assert!(
reg.lookup(ext).is_some(),
"builtin missing renderer for .{}",
ext
);
}
assert!(reg.lookup("xyz").is_none());
}
#[test]
fn test_builder_adds_custom_renderer() {
let reg = RendererRegistry::builtin()
.with_boxed(Box::new(CustomRenderer))
.build();
assert!(reg.lookup("xyz").is_some());
assert!(reg.lookup("md").is_some(), "built-ins still present");
}
#[test]
fn test_builtin_wins_on_collision() {
#[derive(Debug)]
struct FakeHtmlRenderer;
impl EmbedRenderer for FakeHtmlRenderer {
fn extensions(&self) -> &[&'static str] {
&["html"]
}
fn render(&self, _: &ParsedEmbed<'_>) -> RenderedEmbed {
RenderedEmbed::Html("<fake></fake>".to_string())
}
}
let reg = RendererRegistry::builtin()
.with_boxed(Box::new(FakeHtmlRenderer))
.build();
let r = reg.lookup("html").expect("has renderer");
let out = r.render(&ParsedEmbed {
resolved_path: "x.html",
from_path: "post.md",
pinned_url: "x.html",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
});
match out {
RenderedEmbed::Inline(s) => assert_eq!(s, "[x](x.html)"),
_ => panic!("expected Inline (Stage 1 markdown) from built-in IframeRenderer"),
}
}
#[test]
fn test_empty_registry() {
let reg = RendererRegistry::empty().build();
assert!(reg.lookup("jpg").is_none());
}
#[test]
fn test_lookup_case_insensitive() {
let reg = RendererRegistry::builtin().build();
assert!(reg.lookup("MD").is_some());
assert!(reg.lookup("Md").is_some());
}
#[test]
fn test_all_returns_registered_renderers() {
let reg = RendererRegistry::builtin().build();
assert_eq!(reg.all().len(), 8);
}
}