#![deny(warnings, missing_docs, clippy::all)]
use std::io::{ErrorKind, Result, Write};
use std::path::Path;
use fehler::throws;
use pulldown_cmark::Event;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
pub use crate::resources::ResourceAccess;
pub use crate::terminal::*;
use url::Url;
mod magic;
mod references;
mod resources;
mod svg;
mod terminal;
mod render;
pub type Error = std::io::Error;
#[derive(Debug)]
pub struct Settings {
pub terminal_capabilities: TerminalCapabilities,
pub terminal_size: TerminalSize,
pub resource_access: ResourceAccess,
pub syntax_set: SyntaxSet,
}
#[derive(Debug)]
pub struct Environment {
pub base_url: Url,
pub hostname: String,
}
impl Environment {
pub fn for_localhost(base_url: Url) -> Result<Self> {
gethostname::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)
}
}
#[throws]
pub fn push_tty<'a, 'e, W, I>(
settings: &Settings,
environment: &Environment,
writer: &'a mut W,
mut events: I,
) -> ()
where
I: Iterator<Item = Event<'e>>,
W: Write,
{
let theme = &ThemeSet::load_defaults().themes["Solarized (dark)"];
use render::*;
let (final_state, final_data) = events.try_fold(
(State::default(), StateData::default()),
|(state, data), event| {
write_event(writer, settings, environment, &theme, state, data, event)
},
)?;
finish(writer, settings, environment, final_state, final_data)?;
}
#[throws]
pub fn dump_states<'a, 'e, W, I>(
settings: &Settings,
environment: &Environment,
writer: &'a mut W,
mut events: I,
) -> ()
where
I: Iterator<Item = Event<'e>>,
W: Write,
{
use ansi_term::*;
use render::*;
let theme = &ThemeSet::load_defaults().themes["Solarized (dark)"];
let mut sink = std::io::sink();
let (final_state, _) = events.try_fold(
(State::default(), StateData::default()),
|(state, data), event| {
let s = Style::new().fg(Colour::Blue).paint(format!("{:?}", state));
let sep = Style::new().fg(Colour::Yellow).paint("|>");
let e = Style::new()
.fg(Colour::Purple)
.paint(format!("{:?}", event));
writeln!(writer, "{} {} {}", s, sep, e)?;
write_event(&mut sink, settings, environment, &theme, state, data, event)
},
)?;
writeln!(writer, "{:?}", final_state)?;
}
#[cfg(test)]
mod tests {
use pulldown_cmark::Parser;
use super::*;
#[throws(anyhow::Error)]
fn render_string(input: &str, settings: &Settings) -> 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, &mut sink, source)?;
String::from_utf8_lossy(&sink).into()
}
mod layout {
use anyhow::Result;
use pretty_assertions::assert_eq;
use syntect::parsing::SyntaxSet;
use crate::*;
use super::render_string;
fn render(markup: &str) -> Result<String> {
render_string(
markup,
&Settings {
resource_access: ResourceAccess::LocalOnly,
syntax_set: SyntaxSet::default(),
terminal_capabilities: TerminalCapabilities::none(),
terminal_size: TerminalSize::default(),
},
)
}
#[test]
#[allow(non_snake_case)]
fn GH_49_format_no_colour_simple() {
assert_eq!(
render("_lorem_ **ipsum** dolor **sit** _amet_").unwrap(),
"lorem ipsum dolor sit amet\n",
)
}
#[test]
fn begins_with_rule() {
assert_eq!(render("----").unwrap(), "════════════════════════════════════════════════════════════════════════════════\n")
}
#[test]
fn begins_with_block_quote() {
assert_eq!(render("> Hello World").unwrap(), " Hello World\n")
}
#[test]
fn rule_in_block_quote() {
assert_eq!(
render(
"> Hello World
> ----"
)
.unwrap(),
" Hello World
════════════════════════════════════════════════════════════════════════════\n"
)
}
#[test]
fn heading_in_block_quote() {
assert_eq!(
render(
"> Hello World
> # Hello World"
)
.unwrap(),
" Hello World
┄Hello World\n"
)
}
#[test]
fn heading_levels() {
assert_eq!(
render(
"
# First
## Second
### Third"
)
.unwrap(),
"┄First
┄┄Second
┄┄┄Third\n"
)
}
#[test]
fn autolink_creates_no_reference() {
assert_eq!(
render("Hello <http://example.com>").unwrap(),
"Hello http://example.com\n"
)
}
#[test]
fn flush_ref_links_before_toplevel_heading() {
assert_eq!(
render(
"> Hello [World](http://example.com/world)
> # No refs before this headline
# But before this"
)
.unwrap(),
" Hello World[1]
┄No refs before this headline
[1]: http://example.com/world
┄But before this\n"
)
}
#[test]
fn flush_ref_links_at_end() {
assert_eq!(
render(
"Hello [World](http://example.com/world)
# Headline
Hello [Donald](http://example.com/Donald)"
)
.unwrap(),
"Hello World[1]
[1]: http://example.com/world
┄Headline
Hello Donald[2]
[2]: http://example.com/Donald\n"
)
}
}
mod disabled_features {
use anyhow::Result;
use pretty_assertions::assert_eq;
use syntect::parsing::SyntaxSet;
use crate::*;
use super::render_string;
fn render(markup: &str) -> Result<String> {
render_string(
markup,
&Settings {
resource_access: ResourceAccess::LocalOnly,
syntax_set: SyntaxSet::default(),
terminal_capabilities: TerminalCapabilities::none(),
terminal_size: TerminalSize::default(),
},
)
}
#[test]
#[allow(non_snake_case)]
fn GH_155_do_not_choke_on_footnoes() {
assert_eq!(
render(
"A footnote [^1]
[^1: We do not support footnotes."
)
.unwrap(),
"A footnote [^1]
[^1: We do not support footnotes.\n"
)
}
}
}