use regex::Regex;
use std::iter::{IntoIterator, Iterator};
lazy_static::lazy_static! {
static ref RE_CODE_RUST: Regex = Regex::new(r"^(?P<delimiter>`{3,4}|~{3,4})(?:rust|(?:(?:rust,)?(?:no_run|ignore|should_panic)))?$").unwrap();
static ref RE_CODE_TEXT: Regex = Regex::new(r"^(?P<delimiter>`{3,4}|~{3,4})text$").unwrap();
static ref RE_CODE_OTHER: Regex = Regex::new(r"^(?P<delimiter>`{3,4}|~{3,4})\w[\w,\+]*$").unwrap();
}
pub fn process_docs<S: Into<String>, L: Into<Vec<S>>>(
lines: L,
indent_headings: bool,
) -> Vec<String> {
lines.into().into_iter().process_docs(indent_headings)
}
pub struct Processor {
section: Section,
indent_headings: bool,
delimiter: Option<String>,
}
impl Processor {
pub fn new(indent_headings: bool) -> Self {
Processor {
section: Section::None,
indent_headings,
delimiter: None,
}
}
pub fn process_line(&mut self, mut line: String) -> Option<String> {
if self.section == Section::CodeRust && line.starts_with("# ") {
return None;
}
if self.indent_headings && self.section == Section::None && line.starts_with('#') {
line.insert(0, '#');
} else if self.section == Section::None {
let l = line.clone();
if let Some(cap) = RE_CODE_RUST.captures(&l) {
self.section = Section::CodeRust;
self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned());
line = format!("{}rust", self.delimiter.as_ref().unwrap());
} else if let Some(cap) = RE_CODE_TEXT.captures(&l) {
self.section = Section::CodeOther;
self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned());
line = self.delimiter.clone().unwrap();
} else if let Some(cap) = RE_CODE_OTHER.captures(&l) {
self.section = Section::CodeOther;
self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned());
}
} else if self.section != Section::None && Some(&line) == self.delimiter.as_ref() {
self.section = Section::None;
line = self.delimiter.take().unwrap_or_else(|| "```".to_owned());
}
Some(line)
}
}
#[derive(PartialEq)]
enum Section {
CodeRust,
CodeOther,
None,
}
pub trait DocProcess<S: Into<String>> {
fn process_docs(self, indent_headings: bool) -> Vec<String>
where
Self: Sized + Iterator<Item = S>,
{
let mut p = Processor::new(indent_headings);
self.into_iter()
.filter_map(|line| p.process_line(line.into()))
.collect()
}
}
impl<S: Into<String>, I: Iterator<Item = S>> DocProcess<S> for I {}
#[cfg(test)]
mod tests {
use super::process_docs;
const INPUT_HIDDEN_LINE: &[&str] = &[
"```",
"#[visible]",
"let visible = \"visible\";",
"# let hidden = \"hidden\";",
"```",
];
const EXPECTED_HIDDEN_LINE: &[&str] =
&["```rust", "#[visible]", "let visible = \"visible\";", "```"];
#[test]
fn hide_line_in_rust_code_block() {
let result = process_docs(INPUT_HIDDEN_LINE, true);
assert_eq!(result, EXPECTED_HIDDEN_LINE);
}
const INPUT_NOT_HIDDEN_LINE: &[&str] = &[
"```",
"let visible = \"visible\";",
"# let hidden = \"hidden\";",
"```",
"",
"```python",
"# this line is visible",
"visible = True",
"```",
];
const EXPECTED_NOT_HIDDEN_LINE: &[&str] = &[
"```rust",
"let visible = \"visible\";",
"```",
"",
"```python",
"# this line is visible",
"visible = True",
"```",
];
#[test]
fn do_not_hide_line_in_code_block() {
let result = process_docs(INPUT_NOT_HIDDEN_LINE, true);
assert_eq!(result, EXPECTED_NOT_HIDDEN_LINE);
}
const INPUT_RUST_CODE_BLOCK: &[&str] = &[
"```",
"let block = \"simple code block\";",
"```",
"",
"```no_run",
"let run = false;",
"```",
"",
"```ignore",
"let ignore = true;",
"```",
"",
"```should_panic",
"panic!(\"at the disco\");",
"```",
"",
"```C",
"int i = 0; // no rust code",
"```",
];
const EXPECTED_RUST_CODE_BLOCK: &[&str] = &[
"```rust",
"let block = \"simple code block\";",
"```",
"",
"```rust",
"let run = false;",
"```",
"",
"```rust",
"let ignore = true;",
"```",
"",
"```rust",
"panic!(\"at the disco\");",
"```",
"",
"```C",
"int i = 0; // no rust code",
"```",
];
#[test]
fn transform_rust_code_block() {
let result = process_docs(INPUT_RUST_CODE_BLOCK, true);
assert_eq!(result, EXPECTED_RUST_CODE_BLOCK);
}
const INPUT_RUST_CODE_BLOCK_RUST_PREFIX: &[&str] = &[
"```rust",
"let block = \"simple code block\";",
"```",
"",
"```rust,no_run",
"let run = false;",
"```",
"",
"```rust,ignore",
"let ignore = true;",
"```",
"",
"```rust,should_panic",
"panic!(\"at the disco\");",
"```",
"",
"```C",
"int i = 0; // no rust code",
"```",
];
#[test]
fn transform_rust_code_block_with_prefix() {
let result = process_docs(INPUT_RUST_CODE_BLOCK_RUST_PREFIX, true);
assert_eq!(result, EXPECTED_RUST_CODE_BLOCK);
}
const INPUT_TEXT_BLOCK: &[&str] = &["```text", "this is text", "```"];
const EXPECTED_TEXT_BLOCK: &[&str] = &["```", "this is text", "```"];
#[test]
fn transform_text_block() {
let result = process_docs(INPUT_TEXT_BLOCK, true);
assert_eq!(result, EXPECTED_TEXT_BLOCK);
}
const INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS: &[&str] = &[
"```html,django",
"{% if True %}True{% endif %}",
"```",
"",
"```html+django",
"{% if True %}True{% endif %}",
"```",
];
#[test]
fn transform_other_code_block_with_symbols() {
let result = process_docs(INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS, true);
assert_eq!(result, INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS);
}
const INPUT_INDENT_HEADINGS: &[&str] = &[
"# heading 1",
"some text",
"## heading 2",
"some other text",
];
const EXPECTED_INDENT_HEADINGS: &[&str] = &[
"## heading 1",
"some text",
"### heading 2",
"some other text",
];
#[test]
fn indent_markdown_headings() {
let result = process_docs(INPUT_INDENT_HEADINGS, true);
assert_eq!(result, EXPECTED_INDENT_HEADINGS);
}
#[test]
fn do_not_indent_markdown_headings() {
let result = process_docs(INPUT_INDENT_HEADINGS, false);
assert_eq!(result, INPUT_INDENT_HEADINGS);
}
const INPUT_ALTERNATE_DELIMITER_4_BACKTICKS: &[&str] = &["````", "let i = 1;", "````"];
const EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS: &[&str] = &["````rust", "let i = 1;", "````"];
#[test]
fn alternate_delimiter_4_backticks() {
let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_BACKTICKS, false);
assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS);
}
const INPUT_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED: &[&str] = &[
"````",
"//! ```",
"//! let i = 1;",
"//! ```",
"```python",
"i = 1",
"```",
"````",
];
const EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED: &[&str] = &[
"````rust",
"//! ```",
"//! let i = 1;",
"//! ```",
"```python",
"i = 1",
"```",
"````",
];
#[test]
fn alternate_delimiter_4_backticks_nested() {
let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED, false);
assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED);
}
const INPUT_ALTERNATE_DELIMITER_3_TILDES: &[&str] = &["~~~", "let i = 1;", "~~~"];
const EXPECTED_ALTERNATE_DELIMITER_3_TILDES: &[&str] = &["~~~rust", "let i = 1;", "~~~"];
#[test]
fn alternate_delimiter_3_tildes() {
let result = process_docs(INPUT_ALTERNATE_DELIMITER_3_TILDES, false);
assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_3_TILDES);
}
const INPUT_ALTERNATE_DELIMITER_4_TILDES: &[&str] = &["~~~~", "let i = 1;", "~~~~"];
const EXPECTED_ALTERNATE_DELIMITER_4_TILDES: &[&str] = &["~~~~rust", "let i = 1;", "~~~~"];
#[test]
fn alternate_delimiter_4_tildes() {
let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_TILDES, false);
assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_TILDES);
}
const INPUT_ALTERNATE_DELIMITER_MIXED: &[&str] = &[
"```",
"let i = 1;",
"```",
"````",
"//! ```",
"//! let i = 1;",
"//! ```",
"```python",
"i = 1",
"```",
"````",
"~~~markdown",
"```python",
"i = 1",
"```",
"~~~",
];
const EXPECTED_ALTERNATE_DELIMITER_MIXED: &[&str] = &[
"```rust",
"let i = 1;",
"```",
"````rust",
"//! ```",
"//! let i = 1;",
"//! ```",
"```python",
"i = 1",
"```",
"````",
"~~~markdown",
"```python",
"i = 1",
"```",
"~~~",
];
#[test]
fn alternate_delimiter_mixed() {
let result = process_docs(INPUT_ALTERNATE_DELIMITER_MIXED, false);
assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_MIXED);
}
}