pub mod fs; use pulldown_cmark::{Parser, Event, Tag, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES}; use std::borrow::Cow; /// /// /// 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’
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’");
}
}
}