diff --git a/src/lib.rs b/src/lib.rs index 35b363f..eb7de56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,23 +60,26 @@ use tokio::sync::watch::{self, Sender}; use tower_http::trace::TraceLayer; use tracing::log::*; +mod render; mod service; +pub use render::*; + /// Markdown preview server. /// /// Listens for HTTP connections and serves a page containing a live markdown preview. The page /// contains JavaScript to open a websocket connection back to the server for rendering updates. #[derive(Debug)] -pub struct Server { +pub struct Server { addr: SocketAddr, config: Arc>, - external_renderer: Option>, + renderer: R, output: RefCell, tx: Sender, _shutdown_tx: oneshot::Sender<()>, } -impl Server { +impl Server where R: Renderer { /// Binds the server to a specified address. /// /// Binding to port 0 will request a port assignment from the OS. Use [`addr()`][Self::addr] @@ -131,30 +134,14 @@ impl Server { /// /// This method forwards errors from an external renderer, if set. Otherwise, the method is /// infallible. - pub async fn send(&self, markdown: &str) -> io::Result<()> { + pub async fn send(&self, markdown: &str) -> Result<(), R::Error> { let mut output = self.output.take(); output.clear(); // Heuristic taken from rustdoc output.reserve(markdown.len() * 3 / 2); - if let Some(renderer) = &self.external_renderer { - let child = renderer.borrow_mut().spawn()?; - - child.stdin.unwrap().write_all(markdown.as_bytes()).await?; - - child.stdout.unwrap().read_to_string(&mut output).await?; - } else { - let parser = Parser::new_ext( - markdown, - Options::ENABLE_FOOTNOTES - | Options::ENABLE_TABLES - | Options::ENABLE_STRIKETHROUGH - | Options::ENABLE_TASKLISTS, - ); - - pulldown_cmark::html::push_html(&mut output, parser); - }; + self.renderer.render(markdown, &mut output)?; self.output.replace(self.tx.send_replace(output)); @@ -210,48 +197,6 @@ impl Server { Ok(()) } - /// Set an external program to use for rendering the markdown. - /// - /// By default, aurelius uses [`pulldown_cmark`] to render markdown in-process. - /// `pulldown-cmark` is an extremely fast, [CommonMark]-compliant parser that is sufficient - /// for most use-cases. However, other markdown renderers may provide additional features. - /// - /// The `Command` supplied to this function should expect markdown on stdin and print HTML on - /// stdout. - /// - /// # Example - /// - /// To use [`pandoc`] to render markdown: - /// - /// - /// ```no_run - /// # async fn dox() -> Result<(), Box> { - /// use std::net::SocketAddr; - /// use tokio::process::Command; - /// use aurelius::Server; - /// - /// let addr = "127.0.0.1:1337".parse::()?; - /// let mut server = Server::bind(&addr).await?; - /// - /// let mut pandoc = Command::new("pandoc"); - /// pandoc.args(&["-f", "markdown", "-t", "html"]); - /// - /// server.set_external_renderer(pandoc); - /// # Ok(()) - /// # } - /// ``` - /// - /// [`pulldown_cmark`]: https://github.com/raphlinus/pulldown-cmark - /// [CommonMark]: https://commonmark.org/ - /// [`pandoc`]: https://pandoc.org/ - pub fn set_external_renderer(&mut self, mut command: Command) { - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()); - self.external_renderer = Some(RefCell::new(command)); - } - /// Opens the user's default browser with the server's URL in the background. /// /// This function uses platform-specific utilities to determine the browser. The following diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..e0356b0 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,27 @@ +mod external; +mod markdown; + +pub use external::ExternalCommand; +pub use markdown::Markdown; + +/// Markdown renderer implementation. +/// +/// Implementors of this trait convert markdown into HTML. +pub trait Renderer { + /// Potential errors returned by rendering. If rendering is infallible (markdown can always + /// produce HTML from its input), this type can be set to [`std::convert::Infallible`]. + type Error; + + /// Renders markdown as HTML. + /// + /// The HTML should be written directly into the `html` buffer. + fn render(&self, input: &str, html: &mut String) -> Result<(), Self::Error>; + + /// A hint for how many bytes the output will be. + /// + /// This hint should be cheap to compute and is not required to be accurate. However, accurate + /// hints may improve performance by saving intermediate allocations. + fn size_hint(&self, input: &str) -> usize { + input.len() + } +} diff --git a/src/render/external.rs b/src/render/external.rs new file mode 100644 index 0000000..26e46c9 --- /dev/null +++ b/src/render/external.rs @@ -0,0 +1,59 @@ +use std::cell::RefCell; +use std::io::{self, prelude::*}; +use std::process::{Command, Stdio}; + +use super::Renderer; + +/// Markdown renderer that uses an external command as a backend. +/// +/// The [`Markdown`] renderer uses an extremely fast, in-memory parser that is sufficient for most +/// use-cases. However, this renderer may be useful if your markdown requires features unsupported +/// by [`pulldown_cmark`]. +/// +/// # Example +/// +/// Creating an external renderer that uses [pandoc](https://pandoc.org/): +/// +/// ```no_run +/// use std::process::Command; +/// use aurelius::ExternalCommand; +/// +/// let mut pandoc = Command::new("pandoc"); +/// pandoc.args(&["-f", "markdown", "-t", "html"]); +/// +/// ExternalCommand::new(pandoc); +/// ``` +#[derive(Debug)] +pub struct ExternalCommand { + command: RefCell, +} + +impl ExternalCommand { + /// Create a new external command renderer that will spawn processes using the given `command`. + /// + /// The provided [`Command`] should expect markdown input on stdin and print HTML on stdout. + pub fn new(mut command: Command) -> ExternalCommand { + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()); + + ExternalCommand { + command: RefCell::new(command), + } + } +} + +impl Renderer for ExternalCommand { + type Error = io::Error; + + fn render(&self, markdown: &str, html: &mut String) -> Result<(), Self::Error> { + let child = self.command.borrow_mut().spawn()?; + + child.stdin.unwrap().write_all(markdown.as_bytes())?; + + child.stdout.unwrap().read_to_string(html)?; + + Ok(()) + } +} diff --git a/src/render/markdown.rs b/src/render/markdown.rs new file mode 100644 index 0000000..b015d00 --- /dev/null +++ b/src/render/markdown.rs @@ -0,0 +1,45 @@ +use std::convert::Infallible; + +use pulldown_cmark::{html, Options, Parser}; + +use super::Renderer; + +/// Markdown renderer that uses [`pulldown_cmark`] as the backend. +#[derive(Debug)] +pub struct Markdown { + options: Options, +} + +impl Markdown { + /// Create a new instance of the renderer. + pub fn new() -> Markdown { + Markdown { + options: Options::ENABLE_FOOTNOTES + | Options::ENABLE_TABLES + | Options::ENABLE_STRIKETHROUGH + | Options::ENABLE_TASKLISTS, + } + } +} + +impl Default for Markdown { + fn default() -> Self { + Self::new() + } +} + +impl Renderer for Markdown { + type Error = Infallible; + + fn render(&self, markdown: &str, html: &mut String) -> Result<(), Self::Error> { + let parser = Parser::new_ext(markdown, self.options); + + html::push_html(html, parser); + + Ok(()) + } + + fn size_hint(&self, input: &str) -> usize { + input.len() * 3 / 2 + } +}