#![deny(warnings, missing_docs, clippy::all)]
#![forbid(unsafe_code)]
use std::io::{Error, ErrorKind, Result, Write};
use std::path::Path;
use gethostname::gethostname;
use pulldown_cmark::Event;
use syntect::parsing::SyntaxSet;
use tracing::instrument;
use url::Url;
pub use crate::resources::ResourceUrlHandler;
pub use crate::terminal::capabilities::TerminalCapabilities;
pub use crate::terminal::{TerminalProgram, TerminalSize};
pub use crate::theme::Theme;
mod references;
pub mod resources;
pub mod terminal;
mod theme;
mod render;
#[derive(Debug)]
pub struct Settings<'a> {
pub terminal_capabilities: TerminalCapabilities,
pub terminal_size: TerminalSize,
pub syntax_set: &'a SyntaxSet,
pub theme: Theme,
}
#[derive(Debug)]
pub struct Environment {
pub base_url: Url,
pub hostname: String,
}
impl Environment {
pub fn for_localhost(base_url: Url) -> Result<Self> {
gethostname()
.into_string()
.map_err(|raw| {
Error::new(
ErrorKind::InvalidData,
format!("gethostname() returned invalid unicode data: {raw:?}"),
)
})
.map(|hostname| Environment { base_url, hostname })
}
pub fn for_local_directory<P: AsRef<Path>>(base_dir: &P) -> Result<Self> {
Url::from_directory_path(base_dir)
.map_err(|_| {
Error::new(
ErrorKind::InvalidInput,
format!(
"Base directory {} must be an absolute path",
base_dir.as_ref().display()
),
)
})
.and_then(Self::for_localhost)
}
}
#[instrument(level = "debug", skip_all, fields(environment.hostname = environment.hostname.as_str(), environment.base_url = &environment.base_url.as_str()))]
pub fn push_tty<'a, 'e, W, I>(
settings: &Settings,
environment: &Environment,
resource_handler: &dyn ResourceUrlHandler,
writer: &'a mut W,
mut events: I,
) -> Result<()>
where
I: Iterator<Item = Event<'e>>,
W: Write,
{
use render::*;
let StateAndData(final_state, final_data) = events.try_fold(
StateAndData(State::default(), StateData::default()),
|StateAndData(state, data), event| {
write_event(
writer,
settings,
environment,
&resource_handler,
state,
data,
event,
)
},
)?;
finish(writer, settings, environment, final_state, final_data)
}
#[cfg(test)]
mod tests {
use pulldown_cmark::Parser;
use crate::resources::NoopResourceHandler;
use super::*;
fn render_string(input: &str, settings: &Settings) -> Result<String> {
let source = Parser::new(input);
let mut sink = Vec::new();
let env =
Environment::for_local_directory(&std::env::current_dir().expect("Working directory"))?;
push_tty(settings, &env, &NoopResourceHandler, &mut sink, source)?;
Ok(String::from_utf8_lossy(&sink).into())
}
fn render_string_dumb(markup: &str) -> Result<String> {
render_string(
markup,
&Settings {
syntax_set: &SyntaxSet::default(),
terminal_capabilities: TerminalProgram::Dumb.capabilities(),
terminal_size: TerminalSize::default(),
theme: Theme::default(),
},
)
}
mod layout {
use super::render_string_dumb;
use insta::assert_snapshot;
#[test]
#[allow(non_snake_case)]
fn GH_49_format_no_colour_simple() {
assert_eq!(
render_string_dumb("_lorem_ **ipsum** dolor **sit** _amet_").unwrap(),
"lorem ipsum dolor sit amet\n",
)
}
#[test]
fn begins_with_rule() {
assert_snapshot!(render_string_dumb("----").unwrap())
}
#[test]
fn begins_with_block_quote() {
assert_snapshot!(render_string_dumb("> Hello World").unwrap());
}
#[test]
fn rule_in_block_quote() {
assert_snapshot!(render_string_dumb(
"> Hello World
> ----"
)
.unwrap());
}
#[test]
fn heading_in_block_quote() {
assert_snapshot!(render_string_dumb(
"> Hello World
> # Hello World"
)
.unwrap())
}
#[test]
fn heading_levels() {
assert_snapshot!(render_string_dumb(
"
# First
## Second
### Third"
)
.unwrap())
}
#[test]
fn autolink_creates_no_reference() {
assert_eq!(
render_string_dumb("Hello <http://example.com>").unwrap(),
"Hello http://example.com\n"
)
}
#[test]
fn flush_ref_links_before_toplevel_heading() {
assert_snapshot!(render_string_dumb(
"> Hello [World](http://example.com/world)
> # No refs before this headline
# But before this"
)
.unwrap())
}
#[test]
fn flush_ref_links_at_end() {
assert_snapshot!(render_string_dumb(
"Hello [World](http://example.com/world)
# Headline
Hello [Donald](http://example.com/Donald)"
)
.unwrap())
}
}
mod disabled_features {
use insta::assert_snapshot;
use super::render_string_dumb;
#[test]
#[allow(non_snake_case)]
fn GH_155_do_not_choke_on_footnotes() {
assert_snapshot!(render_string_dumb(
"A footnote [^1]
[^1: We do not support footnotes."
)
.unwrap())
}
}
}