1use regex::Regex;
10use std::iter::{IntoIterator, Iterator};
11
12lazy_static::lazy_static! {
13 static ref RE_CODE_RUST: Regex = Regex::new(r"^(?P<delimiter>`{3,4}|~{3,4})(?:rust|(?:(?:rust,)?(?:no_run|ignore|should_panic)))?$").unwrap();
15 static ref RE_CODE_TEXT: Regex = Regex::new(r"^(?P<delimiter>`{3,4}|~{3,4})text$").unwrap();
17 static ref RE_CODE_OTHER: Regex = Regex::new(r"^(?P<delimiter>`{3,4}|~{3,4})\w[\w,\+]*$").unwrap();
19}
20
21pub fn process_docs<S: Into<String>, L: Into<Vec<S>>>(
26 lines: L,
27 indent_headings: bool,
28) -> Vec<String> {
29 lines.into().into_iter().process_docs(indent_headings)
30}
31
32pub struct Processor {
33 section: Section,
34 indent_headings: bool,
35 delimiter: Option<String>,
36}
37
38impl Processor {
39 pub fn new(indent_headings: bool) -> Self {
40 Processor {
41 section: Section::None,
42 indent_headings,
43 delimiter: None,
44 }
45 }
46
47 pub fn process_line(&mut self, mut line: String) -> Option<String> {
48 if self.section == Section::CodeRust && line.starts_with("# ") {
50 return None;
51 }
52
53 if self.indent_headings && self.section == Section::None && line.starts_with('#') {
55 line.insert(0, '#');
56 } else if self.section == Section::None {
57 let l = line.clone();
58 if let Some(cap) = RE_CODE_RUST.captures(&l) {
59 self.section = Section::CodeRust;
60 self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned());
61 line = format!("{}rust", self.delimiter.as_ref().unwrap());
62 } else if let Some(cap) = RE_CODE_TEXT.captures(&l) {
63 self.section = Section::CodeOther;
64 self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned());
65 line = self.delimiter.clone().unwrap();
66 } else if let Some(cap) = RE_CODE_OTHER.captures(&l) {
67 self.section = Section::CodeOther;
68 self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned());
69 }
70 } else if self.section != Section::None && Some(&line) == self.delimiter.as_ref() {
71 self.section = Section::None;
72 line = self.delimiter.take().unwrap_or_else(|| "```".to_owned());
73 }
74
75 Some(line)
76 }
77}
78
79#[derive(PartialEq)]
80enum Section {
81 CodeRust,
82 CodeOther,
83 None,
84}
85
86pub trait DocProcess<S: Into<String>> {
87 fn process_docs(self, indent_headings: bool) -> Vec<String>
88 where
89 Self: Sized + Iterator<Item = S>,
90 {
91 let mut p = Processor::new(indent_headings);
92 self.into_iter()
93 .filter_map(|line| p.process_line(line.into()))
94 .collect()
95 }
96}
97
98impl<S: Into<String>, I: Iterator<Item = S>> DocProcess<S> for I {}
99
100#[cfg(test)]
101mod tests {
102 use super::process_docs;
103
104 const INPUT_HIDDEN_LINE: &[&str] = &[
105 "```",
106 "#[visible]",
107 "let visible = \"visible\";",
108 "# let hidden = \"hidden\";",
109 "```",
110 ];
111
112 const EXPECTED_HIDDEN_LINE: &[&str] =
113 &["```rust", "#[visible]", "let visible = \"visible\";", "```"];
114
115 #[test]
116 fn hide_line_in_rust_code_block() {
117 let result = process_docs(INPUT_HIDDEN_LINE, true);
118 assert_eq!(result, EXPECTED_HIDDEN_LINE);
119 }
120
121 const INPUT_NOT_HIDDEN_LINE: &[&str] = &[
122 "```",
123 "let visible = \"visible\";",
124 "# let hidden = \"hidden\";",
125 "```",
126 "",
127 "```python",
128 "# this line is visible",
129 "visible = True",
130 "```",
131 ];
132
133 const EXPECTED_NOT_HIDDEN_LINE: &[&str] = &[
134 "```rust",
135 "let visible = \"visible\";",
136 "```",
137 "",
138 "```python",
139 "# this line is visible",
140 "visible = True",
141 "```",
142 ];
143
144 #[test]
145 fn do_not_hide_line_in_code_block() {
146 let result = process_docs(INPUT_NOT_HIDDEN_LINE, true);
147 assert_eq!(result, EXPECTED_NOT_HIDDEN_LINE);
148 }
149
150 const INPUT_RUST_CODE_BLOCK: &[&str] = &[
151 "```",
152 "let block = \"simple code block\";",
153 "```",
154 "",
155 "```no_run",
156 "let run = false;",
157 "```",
158 "",
159 "```ignore",
160 "let ignore = true;",
161 "```",
162 "",
163 "```should_panic",
164 "panic!(\"at the disco\");",
165 "```",
166 "",
167 "```C",
168 "int i = 0; // no rust code",
169 "```",
170 ];
171
172 const EXPECTED_RUST_CODE_BLOCK: &[&str] = &[
173 "```rust",
174 "let block = \"simple code block\";",
175 "```",
176 "",
177 "```rust",
178 "let run = false;",
179 "```",
180 "",
181 "```rust",
182 "let ignore = true;",
183 "```",
184 "",
185 "```rust",
186 "panic!(\"at the disco\");",
187 "```",
188 "",
189 "```C",
190 "int i = 0; // no rust code",
191 "```",
192 ];
193
194 #[test]
195 fn transform_rust_code_block() {
196 let result = process_docs(INPUT_RUST_CODE_BLOCK, true);
197 assert_eq!(result, EXPECTED_RUST_CODE_BLOCK);
198 }
199
200 const INPUT_RUST_CODE_BLOCK_RUST_PREFIX: &[&str] = &[
201 "```rust",
202 "let block = \"simple code block\";",
203 "```",
204 "",
205 "```rust,no_run",
206 "let run = false;",
207 "```",
208 "",
209 "```rust,ignore",
210 "let ignore = true;",
211 "```",
212 "",
213 "```rust,should_panic",
214 "panic!(\"at the disco\");",
215 "```",
216 "",
217 "```C",
218 "int i = 0; // no rust code",
219 "```",
220 ];
221
222 #[test]
223 fn transform_rust_code_block_with_prefix() {
224 let result = process_docs(INPUT_RUST_CODE_BLOCK_RUST_PREFIX, true);
225 assert_eq!(result, EXPECTED_RUST_CODE_BLOCK);
226 }
227
228 const INPUT_TEXT_BLOCK: &[&str] = &["```text", "this is text", "```"];
229
230 const EXPECTED_TEXT_BLOCK: &[&str] = &["```", "this is text", "```"];
231
232 #[test]
233 fn transform_text_block() {
234 let result = process_docs(INPUT_TEXT_BLOCK, true);
235 assert_eq!(result, EXPECTED_TEXT_BLOCK);
236 }
237
238 const INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS: &[&str] = &[
239 "```html,django",
240 "{% if True %}True{% endif %}",
241 "```",
242 "",
243 "```html+django",
244 "{% if True %}True{% endif %}",
245 "```",
246 ];
247
248 #[test]
249 fn transform_other_code_block_with_symbols() {
250 let result = process_docs(INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS, true);
251 assert_eq!(result, INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS);
252 }
253
254 const INPUT_INDENT_HEADINGS: &[&str] = &[
255 "# heading 1",
256 "some text",
257 "## heading 2",
258 "some other text",
259 ];
260
261 const EXPECTED_INDENT_HEADINGS: &[&str] = &[
262 "## heading 1",
263 "some text",
264 "### heading 2",
265 "some other text",
266 ];
267
268 #[test]
269 fn indent_markdown_headings() {
270 let result = process_docs(INPUT_INDENT_HEADINGS, true);
271 assert_eq!(result, EXPECTED_INDENT_HEADINGS);
272 }
273
274 #[test]
275 fn do_not_indent_markdown_headings() {
276 let result = process_docs(INPUT_INDENT_HEADINGS, false);
277 assert_eq!(result, INPUT_INDENT_HEADINGS);
278 }
279
280 const INPUT_ALTERNATE_DELIMITER_4_BACKTICKS: &[&str] = &["````", "let i = 1;", "````"];
281
282 const EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS: &[&str] = &["````rust", "let i = 1;", "````"];
283
284 #[test]
285 fn alternate_delimiter_4_backticks() {
286 let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_BACKTICKS, false);
287 assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS);
288 }
289
290 const INPUT_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED: &[&str] = &[
291 "````",
292 "//! ```",
293 "//! let i = 1;",
294 "//! ```",
295 "```python",
296 "i = 1",
297 "```",
298 "````",
299 ];
300
301 const EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED: &[&str] = &[
302 "````rust",
303 "//! ```",
304 "//! let i = 1;",
305 "//! ```",
306 "```python",
307 "i = 1",
308 "```",
309 "````",
310 ];
311
312 #[test]
313 fn alternate_delimiter_4_backticks_nested() {
314 let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED, false);
315 assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED);
316 }
317
318 const INPUT_ALTERNATE_DELIMITER_3_TILDES: &[&str] = &["~~~", "let i = 1;", "~~~"];
319
320 const EXPECTED_ALTERNATE_DELIMITER_3_TILDES: &[&str] = &["~~~rust", "let i = 1;", "~~~"];
321
322 #[test]
323 fn alternate_delimiter_3_tildes() {
324 let result = process_docs(INPUT_ALTERNATE_DELIMITER_3_TILDES, false);
325 assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_3_TILDES);
326 }
327
328 const INPUT_ALTERNATE_DELIMITER_4_TILDES: &[&str] = &["~~~~", "let i = 1;", "~~~~"];
329
330 const EXPECTED_ALTERNATE_DELIMITER_4_TILDES: &[&str] = &["~~~~rust", "let i = 1;", "~~~~"];
331
332 #[test]
333 fn alternate_delimiter_4_tildes() {
334 let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_TILDES, false);
335 assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_TILDES);
336 }
337
338 const INPUT_ALTERNATE_DELIMITER_MIXED: &[&str] = &[
339 "```",
340 "let i = 1;",
341 "```",
342 "````",
343 "//! ```",
344 "//! let i = 1;",
345 "//! ```",
346 "```python",
347 "i = 1",
348 "```",
349 "````",
350 "~~~markdown",
351 "```python",
352 "i = 1",
353 "```",
354 "~~~",
355 ];
356
357 const EXPECTED_ALTERNATE_DELIMITER_MIXED: &[&str] = &[
358 "```rust",
359 "let i = 1;",
360 "```",
361 "````rust",
362 "//! ```",
363 "//! let i = 1;",
364 "//! ```",
365 "```python",
366 "i = 1",
367 "```",
368 "````",
369 "~~~markdown",
370 "```python",
371 "i = 1",
372 "```",
373 "~~~",
374 ];
375
376 #[test]
377 fn alternate_delimiter_mixed() {
378 let result = process_docs(INPUT_ALTERNATE_DELIMITER_MIXED, false);
379 assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_MIXED);
380 }
381}