]>
Commit | Line | Data |
---|---|---|
2c00a5a8 XL |
1 | #![allow(missing_docs)] // FIXME: Document this |
2 | ||
ea8adc8c | 3 | pub mod fs; |
2c00a5a8 XL |
4 | mod string; |
5 | use errors::Error; | |
ea8adc8c | 6 | |
2c00a5a8 XL |
7 | use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES, |
8 | OPTION_ENABLE_TABLES}; | |
ea8adc8c XL |
9 | use std::borrow::Cow; |
10 | ||
2c00a5a8 | 11 | pub use self::string::{RangeArgument, take_lines}; |
ea8adc8c | 12 | |
2c00a5a8 | 13 | /// Wrapper around the pulldown-cmark parser for rendering markdown to HTML. |
ea8adc8c XL |
14 | pub fn render_markdown(text: &str, curly_quotes: bool) -> String { |
15 | let mut s = String::with_capacity(text.len() * 3 / 2); | |
16 | ||
17 | let mut opts = Options::empty(); | |
18 | opts.insert(OPTION_ENABLE_TABLES); | |
19 | opts.insert(OPTION_ENABLE_FOOTNOTES); | |
20 | ||
21 | let p = Parser::new_ext(text, opts); | |
22 | let mut converter = EventQuoteConverter::new(curly_quotes); | |
2c00a5a8 XL |
23 | let events = p.map(clean_codeblock_headers) |
24 | .map(|event| converter.convert(event)); | |
ea8adc8c XL |
25 | |
26 | html::push_html(&mut s, events); | |
27 | s | |
28 | } | |
29 | ||
30 | struct EventQuoteConverter { | |
31 | enabled: bool, | |
32 | convert_text: bool, | |
33 | } | |
34 | ||
35 | impl EventQuoteConverter { | |
36 | fn new(enabled: bool) -> Self { | |
2c00a5a8 XL |
37 | EventQuoteConverter { |
38 | enabled: enabled, | |
39 | convert_text: true, | |
40 | } | |
ea8adc8c XL |
41 | } |
42 | ||
43 | fn convert<'a>(&mut self, event: Event<'a>) -> Event<'a> { | |
44 | if !self.enabled { | |
45 | return event; | |
46 | } | |
47 | ||
48 | match event { | |
2c00a5a8 | 49 | Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => { |
ea8adc8c XL |
50 | self.convert_text = false; |
51 | event | |
2c00a5a8 XL |
52 | } |
53 | Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => { | |
ea8adc8c XL |
54 | self.convert_text = true; |
55 | event | |
2c00a5a8 XL |
56 | } |
57 | Event::Text(ref text) if self.convert_text => { | |
58 | Event::Text(Cow::from(convert_quotes_to_curly(text))) | |
59 | } | |
ea8adc8c XL |
60 | _ => event, |
61 | } | |
62 | } | |
63 | } | |
64 | ||
65 | fn clean_codeblock_headers(event: Event) -> Event { | |
66 | match event { | |
67 | Event::Start(Tag::CodeBlock(ref info)) => { | |
2c00a5a8 | 68 | let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect(); |
ea8adc8c XL |
69 | |
70 | Event::Start(Tag::CodeBlock(Cow::from(info))) | |
2c00a5a8 | 71 | } |
ea8adc8c XL |
72 | _ => event, |
73 | } | |
74 | } | |
75 | ||
76 | ||
77 | fn convert_quotes_to_curly(original_text: &str) -> String { | |
78 | // We'll consider the start to be "whitespace". | |
79 | let mut preceded_by_whitespace = true; | |
80 | ||
2c00a5a8 XL |
81 | original_text.chars() |
82 | .map(|original_char| { | |
83 | let converted_char = match original_char { | |
84 | '\'' => { | |
85 | if preceded_by_whitespace { | |
86 | '‘' | |
87 | } else { | |
88 | '’' | |
89 | } | |
90 | } | |
91 | '"' => { | |
92 | if preceded_by_whitespace { | |
93 | '“' | |
94 | } else { | |
95 | '”' | |
96 | } | |
97 | } | |
98 | _ => original_char, | |
99 | }; | |
100 | ||
101 | preceded_by_whitespace = original_char.is_whitespace(); | |
102 | ||
103 | converted_char | |
104 | }) | |
105 | .collect() | |
106 | } | |
ea8adc8c | 107 | |
2c00a5a8 XL |
108 | /// Prints a "backtrace" of some `Error`. |
109 | pub fn log_backtrace(e: &Error) { | |
110 | error!("Error: {}", e); | |
ea8adc8c | 111 | |
2c00a5a8 XL |
112 | for cause in e.iter().skip(1) { |
113 | error!("\tCaused By: {}", cause); | |
114 | } | |
ea8adc8c XL |
115 | } |
116 | ||
117 | #[cfg(test)] | |
118 | mod tests { | |
119 | mod render_markdown { | |
120 | use super::super::render_markdown; | |
121 | ||
122 | #[test] | |
123 | fn it_can_keep_quotes_straight() { | |
124 | assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n"); | |
125 | } | |
126 | ||
127 | #[test] | |
128 | fn it_can_make_quotes_curly_except_when_they_are_in_code() { | |
129 | let input = r#" | |
130 | 'one' | |
131 | ``` | |
132 | 'two' | |
133 | ``` | |
134 | `'three'` 'four'"#; | |
135 | let expected = r#"<p>‘one’</p> | |
136 | <pre><code>'two' | |
137 | </code></pre> | |
138 | <p><code>'three'</code> ‘four’</p> | |
139 | "#; | |
140 | assert_eq!(render_markdown(input, true), expected); | |
141 | } | |
142 | ||
143 | #[test] | |
144 | fn whitespace_outside_of_codeblock_header_is_preserved() { | |
145 | let input = r#" | |
146 | some text with spaces | |
147 | ```rust | |
148 | fn main() { | |
149 | // code inside is unchanged | |
150 | } | |
151 | ``` | |
152 | more text with spaces | |
153 | "#; | |
154 | ||
155 | let expected = r#"<p>some text with spaces</p> | |
156 | <pre><code class="language-rust">fn main() { | |
157 | // code inside is unchanged | |
158 | } | |
159 | </code></pre> | |
160 | <p>more text with spaces</p> | |
161 | "#; | |
162 | assert_eq!(render_markdown(input, false), expected); | |
163 | assert_eq!(render_markdown(input, true), expected); | |
164 | } | |
165 | ||
166 | #[test] | |
167 | fn rust_code_block_properties_are_passed_as_space_delimited_class() { | |
168 | let input = r#" | |
169 | ```rust,no_run,should_panic,property_3 | |
170 | ``` | |
171 | "#; | |
172 | ||
2c00a5a8 XL |
173 | let expected = |
174 | r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre> | |
ea8adc8c XL |
175 | "#; |
176 | assert_eq!(render_markdown(input, false), expected); | |
177 | assert_eq!(render_markdown(input, true), expected); | |
178 | } | |
179 | ||
180 | #[test] | |
181 | fn rust_code_block_properties_with_whitespace_are_passed_as_space_delimited_class() { | |
182 | let input = r#" | |
183 | ```rust, no_run,,,should_panic , ,property_3 | |
184 | ``` | |
185 | "#; | |
186 | ||
2c00a5a8 XL |
187 | let expected = |
188 | r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre> | |
ea8adc8c XL |
189 | "#; |
190 | assert_eq!(render_markdown(input, false), expected); | |
191 | assert_eq!(render_markdown(input, true), expected); | |
192 | } | |
193 | ||
194 | #[test] | |
195 | fn rust_code_block_without_properties_has_proper_html_class() { | |
196 | let input = r#" | |
2c00a5a8 | 197 | ```rust |
ea8adc8c XL |
198 | ``` |
199 | "#; | |
200 | ||
201 | let expected = r#"<pre><code class="language-rust"></code></pre> | |
202 | "#; | |
203 | assert_eq!(render_markdown(input, false), expected); | |
204 | assert_eq!(render_markdown(input, true), expected); | |
205 | ||
206 | let input = r#" | |
207 | ```rust | |
208 | ``` | |
209 | "#; | |
210 | assert_eq!(render_markdown(input, false), expected); | |
211 | assert_eq!(render_markdown(input, true), expected); | |
ea8adc8c XL |
212 | } |
213 | } | |
214 | ||
215 | mod convert_quotes_to_curly { | |
216 | use super::super::convert_quotes_to_curly; | |
217 | ||
218 | #[test] | |
219 | fn it_converts_single_quotes() { | |
2c00a5a8 XL |
220 | assert_eq!(convert_quotes_to_curly("'one', 'two'"), |
221 | "‘one’, ‘two’"); | |
ea8adc8c XL |
222 | } |
223 | ||
224 | #[test] | |
225 | fn it_converts_double_quotes() { | |
2c00a5a8 XL |
226 | assert_eq!(convert_quotes_to_curly(r#""one", "two""#), |
227 | "“one”, “two”"); | |
ea8adc8c XL |
228 | } |
229 | ||
230 | #[test] | |
231 | fn it_treats_tab_as_whitespace() { | |
232 | assert_eq!(convert_quotes_to_curly("\t'one'"), "\t‘one’"); | |
233 | } | |
234 | } | |
235 | } |