pub mod fs; mod string; use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES, OPTION_ENABLE_TABLES}; use std::borrow::Cow; pub use self::string::{RangeArgument, take_lines}; /// /// /// Wrapper around the pulldown-cmark parser and renderer to render markdown pub fn render_markdown(text: &str, curly_quotes: bool) -> String { let mut s = String::with_capacity(text.len() * 3 / 2); let mut opts = Options::empty(); opts.insert(OPTION_ENABLE_TABLES); opts.insert(OPTION_ENABLE_FOOTNOTES); let p = Parser::new_ext(text, opts); let mut converter = EventQuoteConverter::new(curly_quotes); let events = p.map(clean_codeblock_headers) .map(|event| converter.convert(event)); html::push_html(&mut s, events); s } struct EventQuoteConverter { enabled: bool, convert_text: bool, } impl EventQuoteConverter { fn new(enabled: bool) -> Self { EventQuoteConverter { enabled: enabled, convert_text: true, } } fn convert<'a>(&mut self, event: Event<'a>) -> Event<'a> { if !self.enabled { return event; } match event { Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => { self.convert_text = false; event } Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => { self.convert_text = true; event } Event::Text(ref text) if self.convert_text => { Event::Text(Cow::from(convert_quotes_to_curly(text))) } _ => event, } } } fn clean_codeblock_headers(event: Event) -> Event { match event { Event::Start(Tag::CodeBlock(ref info)) => { let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect(); Event::Start(Tag::CodeBlock(Cow::from(info))) } _ => event, } } fn convert_quotes_to_curly(original_text: &str) -> String { // We'll consider the start to be "whitespace". let mut preceded_by_whitespace = true; original_text.chars() .map(|original_char| { let converted_char = match original_char { '\'' => { if preceded_by_whitespace { '‘' } else { '’' } } '"' => { if preceded_by_whitespace { '“' } else { '”' } } _ => original_char, }; preceded_by_whitespace = original_char.is_whitespace(); converted_char }) .collect() } #[cfg(test)] mod tests { mod render_markdown { use super::super::render_markdown; #[test] fn it_can_keep_quotes_straight() { assert_eq!(render_markdown("'one'", false), "

'one'

\n"); } #[test] fn it_can_make_quotes_curly_except_when_they_are_in_code() { let input = r#" 'one' ``` 'two' ``` `'three'` 'four'"#; let expected = r#"

‘one’

'two'

'three' ‘four’

"#; assert_eq!(render_markdown(input, true), expected); } #[test] fn whitespace_outside_of_codeblock_header_is_preserved() { let input = r#" some text with spaces ```rust fn main() { // code inside is unchanged } ``` more text with spaces "#; let expected = r#"

some text with spaces

fn main() {
// code inside is unchanged
}

more text with spaces

"#; assert_eq!(render_markdown(input, false), expected); assert_eq!(render_markdown(input, true), expected); } #[test] fn rust_code_block_properties_are_passed_as_space_delimited_class() { let input = r#" ```rust,no_run,should_panic,property_3 ``` "#; let expected = r#"
"#; assert_eq!(render_markdown(input, false), expected); assert_eq!(render_markdown(input, true), expected); } #[test] fn rust_code_block_properties_with_whitespace_are_passed_as_space_delimited_class() { let input = r#" ```rust, no_run,,,should_panic , ,property_3 ``` "#; let expected = r#"
"#; assert_eq!(render_markdown(input, false), expected); assert_eq!(render_markdown(input, true), expected); } #[test] fn rust_code_block_without_properties_has_proper_html_class() { let input = r#" ```rust ``` "#; let expected = r#"
"#; assert_eq!(render_markdown(input, false), expected); assert_eq!(render_markdown(input, true), expected); let input = r#" ```rust ``` "#; assert_eq!(render_markdown(input, false), expected); assert_eq!(render_markdown(input, true), expected); } } mod convert_quotes_to_curly { use super::super::convert_quotes_to_curly; #[test] fn it_converts_single_quotes() { assert_eq!(convert_quotes_to_curly("'one', 'two'"), "‘one’, ‘two’"); } #[test] fn it_converts_double_quotes() { assert_eq!(convert_quotes_to_curly(r#""one", "two""#), "“one”, “two”"); } #[test] fn it_treats_tab_as_whitespace() { assert_eq!(convert_quotes_to_curly("\t'one'"), "\t‘one’"); } } }