Skip to content

Commit

Permalink
factor rendering into trait
Browse files Browse the repository at this point in the history
Fixes #21.
  • Loading branch information
euclio committed Jul 5, 2022
1 parent 1c43f7d commit cf70e5d
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 65 deletions.
57 changes: 28 additions & 29 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
//! [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
//!
//! ```no_run
//! use std::net::SocketAddr;
//! use aurelius::Server;
//! use aurelius::{Server, MarkdownRenderer};
//!
//! # tokio_test::block_on(async {
//! let addr = "127.0.0.1:1337".parse::<SocketAddr>()?;
//! let mut server = Server::bind(&addr).await?;
//! let mut server = Server::bind(&addr, MarkdownRenderer::new()).await?;
//!
//! server.open_browser()?;
//!
Expand Down Expand Up @@ -52,23 +53,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<R> {
addr: SocketAddr,
Expand All @@ -79,14 +80,17 @@ pub struct Server<R> {
_shutdown_tx: oneshot::Sender<()>,
}

impl<R> Server<R> where R: Renderer {
/// Binds the server to a specified address.
impl<R> Server<R>
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<Self> {
pub async fn bind(addr: &SocketAddr, renderer: R) -> io::Result<Server<R>> {
let (tx, rx) = watch::channel(String::new());
let (shutdown_tx, shutdown_rx) = oneshot::channel();

Expand Down Expand Up @@ -114,7 +118,7 @@ impl<R> Server<R> where R: Renderer {
Ok(Server {
addr,
config,
external_renderer: None,
renderer,
tx,
output: RefCell::new(String::new()),
_shutdown_tx: shutdown_tx,
Expand All @@ -126,22 +130,17 @@ impl<R> Server<R> 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));

Expand All @@ -152,7 +151,7 @@ impl<R> Server<R> 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<PathBuf>) {
Expand Down Expand Up @@ -265,11 +264,11 @@ mod tests {
use tokio::net::lookup_host;
use tokio::time::{timeout, Duration};

use super::Server;
use crate::{MarkdownRenderer, Server};

async fn new_server() -> anyhow::Result<Server> {
async fn new_server() -> anyhow::Result<Server<MarkdownRenderer>> {
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<S: AsyncRead + AsyncWrite + Unpin>(
Expand Down
24 changes: 15 additions & 9 deletions src/render.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
//! HTML rendering.
mod external;
mod markdown;

pub use external::ExternalCommand;
pub use markdown::Markdown;
pub use external::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()
}
Expand Down
28 changes: 14 additions & 14 deletions src/render/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::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<Command>,
}

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)?;

Expand Down
12 changes: 6 additions & 6 deletions src/render/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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> {
Expand Down
6 changes: 3 additions & 3 deletions tests/it/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use tokio::net::lookup_host;

use aurelius::Server;
use aurelius::{MarkdownRenderer, Server};

mod files;
mod options;

async fn new_server() -> anyhow::Result<Server> {
async fn new_server() -> anyhow::Result<Server<MarkdownRenderer>> {
let addr = lookup_host("localhost:0").await?.next().unwrap();
Ok(Server::bind(&addr).await?)
Ok(Server::bind(&addr, MarkdownRenderer::new()).await?)
}
11 changes: 7 additions & 4 deletions tests/it/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,14 @@ async fn highlight_theme() -> Result<(), Box<dyn Error>> {
#[cfg(not(windows))]
#[tokio::test]
async fn external_renderer() -> Result<(), Box<dyn Error>> {
use tokio::process::Command;
use aurelius::{CommandRenderer, 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?;
Expand Down

0 comments on commit cf70e5d

Please sign in to comment.