mod dmos;
mod meta;
mod opts;
mod title;
mod toc;
use std::fmt;
use std::io;
use jotdown::Parser;
use jotdown::Render;
pub use jotdown;
pub use dmos::DocumentRenderer;
pub use meta::MetaRenderer;
pub use opts::MetaFormat;
pub use opts::MetaOpts;
pub use opts::RenderOpts;
pub use opts::TocOpts;
pub use title::TitleRenderer;
pub use toc::TocRenderer;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("no title found")]
TitleNotFound,
#[error("error rendering output")]
Fmt(#[from] fmt::Error),
#[error("error rendering table of contents")]
Toc(#[source] fmt::Error),
#[error("error rendering document: {0}")]
Doc(#[source] std::io::Error),
}
pub fn meta<W>(input: &str, out: &mut W, opts: MetaOpts) -> Result<(), Error>
where
W: io::Write,
{
let events = Parser::new(input);
MetaRenderer::new(opts)
.write_events(events, out)
.map_err(Error::Doc)?;
Ok(())
}
pub fn title(input: &str) -> Result<String, Error> {
let mut title = String::new();
let events = Parser::new(input);
TitleRenderer::default()
.push_events(events, &mut title)
.map_err(|_| Error::TitleNotFound)?;
Ok(title.trim_end().to_string())
}
pub fn toc(input: &str, opts: TocOpts) -> Result<String, Error> {
let mut toc = String::new();
let events = Parser::new(input);
TocRenderer::new(opts)
.push_events(events, &mut toc)
.map_err(Error::Toc)?;
Ok(toc)
}
pub fn render<W>(input: &str, out: &mut W, opts: RenderOpts) -> Result<(), Error>
where
W: io::Write,
{
let events = Parser::new(input);
let toc_skip_title = opts.toc.skip_title;
let toc = if opts.toc.render {
let mut toc = String::new();
TocRenderer::new(opts.toc)
.push_events(events.clone(), &mut toc)
.map_err(Error::Toc)?;
Some(toc)
} else {
None
};
DocumentRenderer::new(
opts.anchor,
opts.no_emojis,
opts.symbols,
opts.skip_title,
toc,
toc_skip_title,
)
.write_events(events, out)
.map_err(Error::Doc)?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use indoc::indoc;
const TEST_DOC_SIMPLE: &str = indoc! {r#"
# Title heading
First content.
## Section heading
Lorem ipsum.
"#};
const TEST_DOC_ATTRIBUTES: &str = indoc! {r#"
{dmos:title="Document title"}
{dmos:author="Conrad Hoffmann"}
# Title heading
First content.
## Section heading
Lorem ipsum.
"#};
#[test]
fn render_simple_default() {
let want = indoc! {r#"
<section id="Title-heading">
<h1>Title heading</h1>
<p>First content.</p>
<section id="Section-heading">
<h2>Section heading</h2>
<p>Lorem ipsum.</p>
</section>
</section>
"#};
let mut out: Vec<u8> = Vec::new();
render(&TEST_DOC_SIMPLE, &mut out, RenderOpts::default()).unwrap();
let have = String::from_utf8(out).unwrap();
assert_eq!(have.as_str(), want);
}
#[test]
fn render_simple_toc_default() {
let want = indoc! {r##"
<section class="toc"><h3>Table of Contents</h3>
<ol>
<li><a href="#Title-heading">Title heading</a>
<ul>
<li><a href="#Section-heading">Section heading</a>
</ul>
</li>
</ol>
</section>
<section id="Title-heading">
<h1>Title heading</h1>
<p>First content.</p>
<section id="Section-heading">
<h2>Section heading</h2>
<p>Lorem ipsum.</p>
</section>
</section>
"##};
let opts = RenderOpts {
toc: TocOpts {
render: true,
..Default::default()
},
..Default::default()
};
let mut out: Vec<u8> = Vec::new();
render(&TEST_DOC_SIMPLE, &mut out, opts).unwrap();
let have = String::from_utf8(out).unwrap();
assert_eq!(have.as_str(), want);
}
#[test]
fn render_simple_toc_skip_title() {
let want = indoc! {r##"
<section id="Title-heading">
<h1>Title heading</h1>
<section class="toc"><h3>Table of Contents</h3>
<ol>
<li><a href="#Section-heading">Section heading</a></li>
</ol>
</section>
<p>First content.</p>
<section id="Section-heading">
<h2>Section heading</h2>
<p>Lorem ipsum.</p>
</section>
</section>
"##};
let opts = RenderOpts {
toc: TocOpts {
render: true,
skip_title: true,
..Default::default()
},
..Default::default()
};
let mut out: Vec<u8> = Vec::new();
render(&TEST_DOC_SIMPLE, &mut out, opts).unwrap();
let have = String::from_utf8(out).unwrap();
assert_eq!(have.as_str(), want);
}
#[test]
fn meta_simple() {
let want = indoc! {"
{
}
"};
let mut out: Vec<u8> = Vec::new();
meta(
&TEST_DOC_SIMPLE,
&mut out,
MetaOpts {
format: MetaFormat::Json,
},
)
.unwrap();
let have = String::from_utf8(out).unwrap();
assert_eq!(have.as_str(), want);
}
#[test]
fn meta_attributes() {
let want = indoc! {r#"
{
"dmos_title": "Document title",
"dmos_author": "Conrad Hoffmann"
}
"#};
let mut out: Vec<u8> = Vec::new();
meta(
&TEST_DOC_ATTRIBUTES,
&mut out,
MetaOpts {
format: MetaFormat::Json,
},
)
.unwrap();
let have = String::from_utf8(out).unwrap();
assert_eq!(have.as_str(), want);
}
#[test]
fn title_simple() {
let title = title(&TEST_DOC_SIMPLE).unwrap();
assert_eq!(title.as_str(), "Title heading");
}
}