use proc_macro2::TokenStream;
use std::{fs, io};
pub fn include_markdown(item: TokenStream) -> syn::Result<TokenStream> {
super::include_file(item, collect::<fs::File>)
}
fn collect<R: io::Read>(
name: &str,
iter: io::Lines<io::BufReader<R>>,
) -> io::Result<(u32, Vec<String>)> {
let mut lines = Vec::new();
let mut in_fence = false;
let mut fence_char = '\0';
let mut fence_count = 0;
let mut fence_indent = 0;
let mut start_line: u32 = 0;
for (line_idx, line) in iter.enumerate() {
let line = line?;
let line_num = (line_idx + 1) as u32;
if !in_fence {
let trimmed_start = line.trim_start();
let indent = line.len() - trimmed_start.len();
let first_char = trimmed_start.chars().next();
if first_char == Some('`') || first_char == Some('~') {
let fence_ch = first_char.unwrap();
let count = trimmed_start.chars().take_while(|&c| c == fence_ch).count();
if count >= 3 {
let after_fence = &trimmed_start[count..];
let after_fence = after_fence.trim_start();
if after_fence.starts_with("rust") && after_fence.contains(name) {
in_fence = true;
fence_char = fence_ch;
fence_count = count;
fence_indent = indent;
start_line = line_num + 1;
}
}
}
} else {
let trimmed_start = line.trim_start();
let indent = line.len() - trimmed_start.len();
if indent == fence_indent {
let first_char = trimmed_start.chars().next();
if first_char == Some(fence_char) {
let count = trimmed_start
.chars()
.take_while(|&c| c == fence_char)
.count();
if count >= fence_count {
break;
}
}
}
if line.len() >= fence_indent {
let content = &line[fence_indent..];
lines.push(content.to_string());
} else {
lines.push(line);
}
}
}
Ok((start_line, lines))
}
#[cfg(test)]
mod tests {
use super::collect;
use crate::extract;
use std::io;
#[test]
fn extract_no_code_fences() {
let content = r#"This is a markdown file
with no code fences at all.
Just plain text."#;
let cursor = io::Cursor::new(content);
let result = extract(cursor, "example", collect);
assert!(matches!(result, Err(err) if err.kind() == io::ErrorKind::NotFound));
}
#[test]
fn extract_no_matching_name() {
let content = r#"Some text here.
```rust
fn main() {
println!("Hello");
}
```
More text."#;
let cursor = io::Cursor::new(content);
let result = extract(cursor, "example", collect);
assert!(matches!(result, Err(err) if err.kind() == io::ErrorKind::NotFound));
}
#[test]
fn extract_multiple_fences_one_match() {
let content = r#"Here's the first fence:
```javascript
console.log("Not this one");
```
And here's the one we want:
~~~rust example
fn main() {
println!("Hello, world!");
}
~~~
And another one:
```python
print("Also not this one")
```"#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"fn main() {
println!("Hello, world!");
}"#
);
}
#[test]
fn extract_nested_code_fence() {
let content = r#"Outer content:
````markdown
# Example
Here's a nested code fence:
```rust example
fn nested() {
println!("Inner code");
}
```
More content.
````
After the fence."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"fn nested() {
println!("Inner code");
}"#
);
}
#[test]
fn extract_with_indentation() {
let content = r#"Normal text.
~~~rust example
fn indented() {
println!("Indented code");
}
~~~
More text."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"fn indented() {
println!("Indented code");
}"#
);
}
#[test]
fn extract_backticks_with_name() {
let content = r#"Text before.
```rust example
let x = 42;
let y = x + 1;
```
Text after."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"let x = 42;
let y = x + 1;"#
);
}
#[test]
fn extract_tildes_with_name() {
let content = r#"Text before.
~~~rust example
fn hello() {
println!("Hello");
}
~~~
Text after."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"fn hello() {
println!("Hello");
}"#
);
}
#[test]
fn extract_more_closing_chars() {
let content = r#"Text before.
```rust example
fn test() {
println!("test");
}
`````
Text after."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"fn test() {
println!("test");
}"#
);
}
#[test]
fn extract_rust_with_whitespace() {
let content = r#"Text before.
``` rust example
let a = 1;
let b = 2;
```
Text after."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"let a = 1;
let b = 2;"#
);
}
#[test]
fn extract_no_rust_language() {
let content = r#"Text before.
```python example
def test():
pass
```
Text after."#;
let cursor = io::Cursor::new(content);
let result = extract(cursor, "example", collect);
assert!(matches!(result, Err(err) if err.kind() == io::ErrorKind::NotFound));
}
#[test]
fn extract_start_line() {
let content = "Text before.\n\n```rust example\nlet x = 42;\n```\n";
let cursor = io::Cursor::new(content);
let (start_line, _) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(start_line, 4);
}
#[test]
fn extract_start_line_with_preceding_fences() {
let content =
"Text.\n\n```rust other\nfn a() {}\n```\n\nMore.\n\n```rust example\nlet x = 1;\n```\n";
let cursor = io::Cursor::new(content);
let (start_line, _) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(start_line, 10);
}
}