From bfd5088fa24f7fc72a851206f16f3e204872876b Mon Sep 17 00:00:00 2001 From: Andy Russell Date: Tue, 5 Jul 2022 18:40:16 -0400 Subject: [PATCH] factor rendering into trait Fixes #21. --- benches/benches.rs | 7 +++- src/lib.rs | 57 +++++++++++++------------- src/render.rs | 26 +++++++----- src/render/{external.rs => command.rs} | 28 ++++++------- src/render/markdown.rs | 12 +++--- tests/it/main.rs | 5 ++- tests/it/options.rs | 12 ++++-- 7 files changed, 81 insertions(+), 66 deletions(-) rename src/render/{external.rs => command.rs} (57%) diff --git a/benches/benches.rs b/benches/benches.rs index dc52e16..8522948 100644 --- a/benches/benches.rs +++ b/benches/benches.rs @@ -2,6 +2,7 @@ use criterion::BenchmarkId; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use tokio::runtime; +use aurelius::render::MarkdownRenderer; use aurelius::Server; fn from_elem(c: &mut Criterion) { @@ -15,7 +16,8 @@ fn from_elem(c: &mut Criterion) { let addr = "127.0.0.1:0".parse().unwrap(); - let server = runtime.block_on(async { Server::bind(&addr).await.unwrap() }); + let server = + runtime.block_on(async { Server::bind(&addr, MarkdownRenderer::new()).await.unwrap() }); b.to_async(&runtime).iter(|| async { server.send(black_box("Hello, world!")).await.unwrap(); @@ -30,7 +32,8 @@ fn from_elem(c: &mut Criterion) { let addr = "127.0.0.1:0".parse().unwrap(); - let server = runtime.block_on(async { Server::bind(&addr).await.unwrap() }); + let server = + runtime.block_on(async { Server::bind(&addr, MarkdownRenderer::new()).await.unwrap() }); let markdown = "a ".repeat(100_000); diff --git a/src/lib.rs b/src/lib.rs index eb7de56..d256e53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,14 @@ //! [aurelius](https://github.com/euclio/aurelius) is a complete solution for live-previewing -//! markdown as HTML. +//! markdown (and more!) as HTML. //! -//! This crate provides a server that can render and update an HTML preview of markdown without a +//! This crate provides a [`Server`] that can render and update an HTML preview of input without a //! client-side refresh. Upon receiving an HTTP request, the server responds with an HTML page -//! containing a rendering of supplied markdown. Client-side JavaScript then initiates a WebSocket +//! containing a rendering of the input. Client-side JavaScript then initiates a WebSocket //! connection which allows the server to push changes to the client. //! //! This crate was designed to power [vim-markdown-composer], a markdown preview plugin for -//! [Neovim](http://neovim.io), but it may be used to implement similar plugins for any editor. +//! [Neovim](http://neovim.io), but it may be used to implement similar plugins for any editor. It +//! also supports arbitrary renderers through the [`Renderer`] trait. //! See [vim-markdown-composer] for a real-world usage example. //! //! # Example @@ -15,10 +16,11 @@ //! ```no_run //! use std::net::SocketAddr; //! use aurelius::Server; +//! use aurelius::render::MarkdownRenderer; //! //! # tokio_test::block_on(async { //! let addr = "127.0.0.1:1337".parse::()?; -//! let mut server = Server::bind(&addr).await?; +//! let mut server = Server::bind(&addr, MarkdownRenderer::new()).await?; //! //! server.open_browser()?; //! @@ -52,23 +54,23 @@ use std::process::Stdio; use std::sync::{Arc, RwLock}; use axum::{extract::Extension, http::Uri, routing::get, Router}; -use pulldown_cmark::{Options, Parser}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::process::Command; use tokio::sync::oneshot; use tokio::sync::watch::{self, Sender}; use tower_http::trace::TraceLayer; use tracing::log::*; -mod render; +pub mod render; mod service; -pub use render::*; +use crate::render::Renderer; -/// Markdown preview server. +/// Live preview server. /// -/// Listens for HTTP connections and serves a page containing a live markdown preview. The page +/// Listens for HTTP connections and serves a page containing a live rendered preview. The page /// contains JavaScript to open a websocket connection back to the server for rendering updates. +/// +/// The server is asynchronous, and assumes that a `tokio` runtime is in use. #[derive(Debug)] pub struct Server { addr: SocketAddr, @@ -79,14 +81,17 @@ pub struct Server { _shutdown_tx: oneshot::Sender<()>, } -impl Server where R: Renderer { - /// Binds the server to a specified address. +impl Server +where + R: Renderer, +{ + /// Binds the server to a specified address `addr` using the provided `renderer`. /// /// Binding to port 0 will request a port assignment from the OS. Use [`addr()`][Self::addr] /// to determine what port was assigned. /// /// The server must be bound using a Tokio runtime. - pub async fn bind(addr: &SocketAddr) -> io::Result { + pub async fn bind(addr: &SocketAddr, renderer: R) -> io::Result> { let (tx, rx) = watch::channel(String::new()); let (shutdown_tx, shutdown_rx) = oneshot::channel(); @@ -114,7 +119,7 @@ impl Server where R: Renderer { Ok(Server { addr, config, - external_renderer: None, + renderer, tx, output: RefCell::new(String::new()), _shutdown_tx: shutdown_tx, @@ -126,22 +131,17 @@ impl Server where R: Renderer { self.addr } - /// Publish new markdown to be rendered by the server. + /// Publish new input to be rendered by the server. /// /// The new HTML will be sent to all connected websocket clients. - /// - /// # Errors - /// - /// This method forwards errors from an external renderer, if set. Otherwise, the method is - /// infallible. - pub async fn send(&self, markdown: &str) -> Result<(), R::Error> { + pub async fn send(&self, input: &str) -> Result<(), R::Error> { let mut output = self.output.take(); output.clear(); // Heuristic taken from rustdoc - output.reserve(markdown.len() * 3 / 2); + output.reserve(input.len() * 3 / 2); - self.renderer.render(markdown, &mut output)?; + self.renderer.render(input, &mut output)?; self.output.replace(self.tx.send_replace(output)); @@ -152,7 +152,7 @@ impl Server where R: Renderer { /// /// This can be thought of as the "working directory" of the server. Any HTTP requests with /// non-root paths will be joined to this folder and used to serve files from the filesystem. - /// Typically this is used to serve image links relative to the markdown file. + /// Typically this is used to serve image links relative to the input file. /// /// By default, the server will not serve static files. pub fn set_static_root(&mut self, root: impl Into) { @@ -265,11 +265,12 @@ mod tests { use tokio::net::lookup_host; use tokio::time::{timeout, Duration}; - use super::Server; + use crate::render::MarkdownRenderer; + use crate::Server; - async fn new_server() -> anyhow::Result { + async fn new_server() -> anyhow::Result> { let addr = lookup_host("localhost:0").await?.next().unwrap(); - Ok(Server::bind(&addr).await?) + Ok(Server::bind(&addr, MarkdownRenderer::new()).await?) } async fn assert_websocket_closed( diff --git a/src/render.rs b/src/render.rs index e0356b0..af6bdaf 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,26 +1,32 @@ -mod external; +//! HTML rendering. + +mod command; mod markdown; -pub use external::ExternalCommand; -pub use markdown::Markdown; +pub use command::CommandRenderer; +pub use markdown::MarkdownRenderer; -/// Markdown renderer implementation. +/// HTML renderer implementation. /// -/// Implementors of this trait convert markdown into HTML. +/// Implementors of this trait convert input 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`]. + /// Potential errors returned by the rendering. If rendering is infallible (for example, + /// markdown can always produce HTML from its input), this type can be set to + /// [`std::convert::Infallible`]. type Error; - /// Renders markdown as HTML. + /// Renders input as HTML. /// - /// The HTML should be written directly into the `html` buffer. + /// The HTML should be written directly into the `html` buffer. The buffer will be reused + /// between multiple calls to this method, with its capacity already reserved, so this function + /// only needs to write the HTML. 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. + /// hints may improve performance by saving intermediate allocations when reserving capacity + /// for the output buffer. fn size_hint(&self, input: &str) -> usize { input.len() } diff --git a/src/render/external.rs b/src/render/command.rs similarity index 57% rename from src/render/external.rs rename to src/render/command.rs index 26e46c9..880e379 100644 --- a/src/render/external.rs +++ b/src/render/command.rs @@ -4,53 +4,53 @@ use std::process::{Command, Stdio}; use super::Renderer; -/// Markdown renderer that uses an external command as a backend. +/// Renderer that uses an external command to render input. /// -/// 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`]. +/// [`MarkdownRenderer`](crate::render::MarkdownRenderer) 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/): +/// Creating an external renderer that uses [pandoc](https://pandoc.org/) to render markdown: /// /// ```no_run /// use std::process::Command; -/// use aurelius::ExternalCommand; +/// use aurelius::render::CommandRenderer; /// /// let mut pandoc = Command::new("pandoc"); /// pandoc.args(&["-f", "markdown", "-t", "html"]); /// -/// ExternalCommand::new(pandoc); +/// CommandRenderer::new(pandoc); /// ``` #[derive(Debug)] -pub struct ExternalCommand { +pub struct CommandRenderer { command: RefCell, } -impl ExternalCommand { +impl CommandRenderer { /// 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 { + pub fn new(mut command: Command) -> CommandRenderer { command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()); - ExternalCommand { + CommandRenderer { command: RefCell::new(command), } } } -impl Renderer for ExternalCommand { +impl Renderer for CommandRenderer { type Error = io::Error; - fn render(&self, markdown: &str, html: &mut String) -> Result<(), Self::Error> { + fn render(&self, input: &str, html: &mut String) -> Result<(), Self::Error> { let child = self.command.borrow_mut().spawn()?; - child.stdin.unwrap().write_all(markdown.as_bytes())?; + child.stdin.unwrap().write_all(input.as_bytes())?; child.stdout.unwrap().read_to_string(html)?; diff --git a/src/render/markdown.rs b/src/render/markdown.rs index b015d00..e28e5ce 100644 --- a/src/render/markdown.rs +++ b/src/render/markdown.rs @@ -6,14 +6,14 @@ use super::Renderer; /// Markdown renderer that uses [`pulldown_cmark`] as the backend. #[derive(Debug)] -pub struct Markdown { +pub struct MarkdownRenderer { options: Options, } -impl Markdown { +impl MarkdownRenderer { /// Create a new instance of the renderer. - pub fn new() -> Markdown { - Markdown { + pub fn new() -> MarkdownRenderer { + MarkdownRenderer { options: Options::ENABLE_FOOTNOTES | Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH @@ -22,13 +22,13 @@ impl Markdown { } } -impl Default for Markdown { +impl Default for MarkdownRenderer { fn default() -> Self { Self::new() } } -impl Renderer for Markdown { +impl Renderer for MarkdownRenderer { type Error = Infallible; fn render(&self, markdown: &str, html: &mut String) -> Result<(), Self::Error> { diff --git a/tests/it/main.rs b/tests/it/main.rs index bcea6b7..d543528 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -1,11 +1,12 @@ use tokio::net::lookup_host; +use aurelius::render::MarkdownRenderer; use aurelius::Server; mod files; mod options; -async fn new_server() -> anyhow::Result { +async fn new_server() -> anyhow::Result> { let addr = lookup_host("localhost:0").await?.next().unwrap(); - Ok(Server::bind(&addr).await?) + Ok(Server::bind(&addr, MarkdownRenderer::new()).await?) } diff --git a/tests/it/options.rs b/tests/it/options.rs index 7cd990c..5021058 100644 --- a/tests/it/options.rs +++ b/tests/it/options.rs @@ -93,11 +93,15 @@ async fn highlight_theme() -> Result<(), Box> { #[cfg(not(windows))] #[tokio::test] async fn external_renderer() -> Result<(), Box> { - use tokio::process::Command; + use aurelius::render::CommandRenderer; + use aurelius::Server; + use std::process::Command; - let mut server = new_server().await?; - - server.set_external_renderer(Command::new("cat")); + let addr = tokio::net::lookup_host("localhost:0") + .await? + .next() + .unwrap(); + let server = Server::bind(&addr, CommandRenderer::new(Command::new("cat"))).await?; let (mut websocket, _) = async_tungstenite::tokio::connect_async(format!("ws://{}", server.addr())).await?;