Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide custom image adapter #269

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions examples/custom_images.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
extern crate comrak;

use std::io::{self, Write};

use comrak::{
adapters::{ImageAdapter, ImageMeta},
html::{escape, escape_href},
markdown_to_html_with_plugins,
nodes::Sourcepos,
ComrakOptions, ComrakPlugins,
};

struct CustomImages;

impl ImageAdapter for CustomImages {
fn render(
&self,
output: &mut dyn Write,
img_meta: ImageMeta,
sourcepos: Option<Sourcepos>,
) -> io::Result<()> {
output.write_all(b"<figure")?;
if let Some(sourcepos) = sourcepos {
write!(output, " data-sourcepos=\"{}\"", sourcepos)?;
}
output.write_all(b"><a href=\"")?;
escape_href(output, img_meta.url.as_bytes())?;
output.write_all(b"\" target=\"_blank\"><img src=\"")?;
escape_href(output, img_meta.url.as_bytes())?;
output.write_all(b"\"></a>")?;
if !img_meta.title.is_empty() {
output.write_all(b"<figcaption>")?;
escape(output, img_meta.title.as_bytes())?;
output.write_all(b"</figcaption>")?;
};
output.write_all(b"</figure>")?;
Ok(())
}
}

fn main() {
let adapter = CustomImages;

let mut options = ComrakOptions::default();
options.render.sourcepos = true;
let mut plugins = ComrakPlugins::default();
plugins.render.image_adapter = Some(&adapter);

let input = "![Here is a caption](/img/logo.png)";

let formatted = markdown_to_html_with_plugins(input, &options, &plugins);

println!("{}", formatted);
}
20 changes: 20 additions & 0 deletions src/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,23 @@ pub trait HeadingAdapter {
/// Render the closing tag.
fn exit(&self, output: &mut dyn Write, heading: &HeadingMeta) -> io::Result<()>;
}

/// Image data passed to the custom image adapter.
#[derive(Debug)]
pub struct ImageMeta<'a> {
/// The URL of the image.
pub url: &'a str,
/// The title of the image.
pub title: &'a str,
}

/// Implement this adapter to create a plugin for custom images.
pub trait ImageAdapter {
/// The rendering function for images.
fn render(
&self,
output: &mut dyn Write,
image_meta: ImageMeta,
sourcepos: Option<Sourcepos>,
) -> io::Result<()>;
}
51 changes: 35 additions & 16 deletions src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::io::{self, Write};
use std::str;

use crate::adapters::HeadingMeta;
use crate::adapters::ImageMeta;

/// Formats an AST as HTML, modified by the given options.
pub fn format_document<'a>(
Expand Down Expand Up @@ -817,25 +818,43 @@ impl<'o> HtmlFormatter<'o> {
self.output.write_all(b"</a>")?;
}
}
NodeValue::Image(ref nl) => {
if entering {
self.output.write_all(b"<img")?;
self.render_sourcepos(node)?;
self.output.write_all(b" src=\"")?;
let url = nl.url.as_bytes();
if self.options.render.unsafe_ || !dangerous_url(url) {
self.escape_href(url)?;
NodeValue::Image(ref nl) => match self.plugins.render.image_adapter {
None => {
if entering {
self.output.write_all(b"<img")?;
self.render_sourcepos(node)?;
self.output.write_all(b" src=\"")?;
let url = nl.url.as_bytes();
if self.options.render.unsafe_ || !dangerous_url(url) {
self.escape_href(url)?;
}
self.output.write_all(b"\" alt=\"")?;
return Ok(true);
} else {
if !nl.title.is_empty() {
self.output.write_all(b"\" title=\"")?;
self.escape(nl.title.as_bytes())?;
}
self.output.write_all(b"\" />")?;
}
self.output.write_all(b"\" alt=\"")?;
return Ok(true);
} else {
if !nl.title.is_empty() {
self.output.write_all(b"\" title=\"")?;
self.escape(nl.title.as_bytes())?;
}
Some(adapter) => {
if entering {
adapter.render(
self.output,
ImageMeta {
url: &nl.url,
title: &nl.title,
},
if self.options.render.sourcepos {
Some(node.data.borrow().sourcepos)
} else {
None
},
)?;
}
self.output.write_all(b"\" />")?;
}
}
},
#[cfg(feature = "shortcodes")]
NodeValue::ShortCode(ref nsc) => {
if entering {
Expand Down
5 changes: 4 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::mem;
use std::str;
use typed_arena::Arena;

use crate::adapters::HeadingAdapter;
use crate::adapters::{HeadingAdapter, ImageAdapter};

use self::inlines::RefMap;

Expand Down Expand Up @@ -578,6 +578,9 @@ pub struct ComrakRenderPlugins<'p> {

/// Optional heading adapter
pub heading_adapter: Option<&'p dyn HeadingAdapter>,

/// TODO
pub image_adapter: Option<&'p dyn ImageAdapter>,
}

impl Debug for ComrakRenderPlugins<'_> {
Expand Down
14 changes: 13 additions & 1 deletion src/tests/api.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
adapters::{HeadingAdapter, HeadingMeta, SyntaxHighlighterAdapter},
adapters::{HeadingAdapter, HeadingMeta, ImageAdapter, ImageMeta, SyntaxHighlighterAdapter},
nodes::Sourcepos,
};

Expand Down Expand Up @@ -109,12 +109,24 @@ fn exercise_full_api() {
}
}

impl ImageAdapter for MockAdapter {
fn render(
&self,
_output: &mut dyn Write,
_img_meta: ImageMeta,
_sourcepos: Option<Sourcepos>,
) -> io::Result<()> {
unreachable!()
}
}

let mock_adapter = MockAdapter {};

let _ = ComrakPlugins {
render: ComrakRenderPlugins {
codefence_syntax_highlighter: Some(&mock_adapter),
heading_adapter: Some(&mock_adapter),
image_adapter: Some(&mock_adapter),
},
};

Expand Down