use proc_macro2::TokenStream;
use std::{fs, io};
pub fn include_textile(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_block = false;
let mut is_double_period = false;
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_block {
let trimmed = line.trim();
if trimmed.starts_with("bc") && has_matching_id(trimmed, name) {
is_double_period = trimmed.contains("..");
start_line = line_num;
if let Some(space_pos) = trimmed.find(' ') {
let content = &trimmed[space_pos + 1..];
if !content.is_empty() {
lines.push(content.to_string());
in_block = true;
}
}
}
} else {
let trimmed = line.trim();
if is_double_period {
if is_block_tag(trimmed) {
while let Some(last) = lines.last() {
if last.trim().is_empty() {
lines.pop();
} else {
break;
}
}
break;
}
lines.push(line);
} else {
if trimmed.is_empty() {
break;
}
lines.push(line);
}
}
}
Ok((start_line, lines))
}
fn has_matching_id(line: &str, name: &str) -> bool {
let pattern1 = format!("bc(rust#{})", name);
if let Some(pos) = line.find(&pattern1) {
let after_pattern = &line[pos + pattern1.len()..];
if after_pattern.starts_with('.') || after_pattern.starts_with("..") {
return true;
}
}
let pattern2 = format!("bc[rust](#{})", name);
if let Some(pos) = line.find(&pattern2) {
let after_pattern = &line[pos + pattern2.len()..];
if after_pattern.starts_with('.') || after_pattern.starts_with("..") {
return true;
}
}
let pattern3 = format!("bc(#{})[rust]", name);
if let Some(pos) = line.find(&pattern3) {
let after_pattern = &line[pos + pattern3.len()..];
if after_pattern.starts_with('.') || after_pattern.starts_with("..") {
return true;
}
}
false
}
fn is_block_tag(line: &str) -> bool {
if line.is_empty() {
return false;
}
let block_types = [
"p",
"bq",
"bc",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"table",
"pre",
"notextile",
];
for block_type in &block_types {
if let Some(after_block) = line.strip_prefix(block_type) {
let mut chars = after_block.chars().peekable();
let mut found_period = false;
while let Some(ch) = chars.next() {
match ch {
'.' => {
found_period = true;
break;
}
'<' | '>' | '=' => {
}
'(' => {
let mut depth = 1;
while let Some(&next_ch) = chars.peek() {
if next_ch == '.' {
break;
} else if next_ch == ')' {
chars.next(); depth -= 1;
if depth == 0 {
break;
}
} else if next_ch == '(' {
chars.next(); depth += 1;
} else {
chars.next(); }
}
}
')' => {
}
'[' => {
for inner_ch in chars.by_ref() {
if inner_ch == ']' {
break;
}
}
}
_ => {
break;
}
}
}
if found_period {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::collect;
use crate::extract;
use std::io;
#[test]
fn extract_no_code_blocks() {
let content = r#"This is a Textile file
with no code blocks 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_id() {
let content = r#"Some text here.
bc(rust#other). fn main() {
println!("Hello");
}
p. 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_single_period_with_content() {
let content = r#"Some introduction text.
bc[rust](#example). println!("hello, world!");
p. Text after the block."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(result, r#"println!("hello, world!");"#);
}
#[test]
fn extract_double_period_multiline() {
let content = r#"Some introduction text.
bc(#example)[rust].. fn test() {
assert_eq!(2 + 2, 4);
}
p. Text after the block."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"fn test() {
assert_eq!(2 + 2, 4);
}"#
);
}
#[test]
fn extract_multiline_until_next_block() {
let content = r#"Some introduction text.
bc[rust](#example).. let x = 42;
let y = x + 1;
bq. This is a quote block that ends the code."#;
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_multiple_blocks_one_match() {
let content = r#"Here's the first block:
bc(python#other). print("Not this one")
p. And here's the one we want:
bc(#example)[rust]. println!("This is the one!");
p. And another one."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(result, r#"println!("This is the one!");"#);
}
#[test]
fn extract_content_on_next_line() {
let content = r#"Text before.
bc(rust#example).. fn with_content() {
println!("On next line");
}
p. Text after."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"fn with_content() {
println!("On next line");
}"#
);
}
#[test]
fn extract_with_class_before_id() {
let content = r#"Text before.
bc[rust](#example). let value = 123;
p. Text after."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(result, "let value = 123;");
}
#[test]
fn extract_until_eof() {
let content = r#"Text before.
bc(rust#example).. struct Point {
x: i32,
y: i32,
}"#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"struct Point {
x: i32,
y: i32,
}"#
);
}
#[test]
fn extract_empty_lines_within_block() {
let content = r#"Text before.
bc(rust#example).. fn first() {}
fn second() {}
p. Text after."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"fn first() {}
fn second() {}"#
);
}
#[test]
fn extract_different_language() {
let content = r#"Text before.
bc(python#example). print("hello")
p. 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_ends_at_right_aligned_header() {
let content = r#"Text before.
bc(rust#example).. let x = 1;
let y = 2;
h1>. Right Aligned Header"#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"let x = 1;
let y = 2;"#
);
}
#[test]
fn extract_ends_at_justified_paragraph() {
let content = r#"Text before.
bc(rust#example).. fn test() {
println!("test");
}
p<>. Justified paragraph text."#;
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_ends_at_centered_header() {
let content = r#"Text before.
bc(rust#example).. let value = 42;
h2=. Centered Header"#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(result, "let value = 42;");
}
#[test]
fn extract_ends_at_indented_paragraph() {
let content = r#"Text before.
bc(rust#example).. struct Point {
x: i32,
}
p(. Indented paragraph."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"struct Point {
x: i32,
}"#
);
}
#[test]
fn extract_ends_at_table_with_class() {
let content = r#"Text before.
bc(rust#example).. let data = vec![1, 2, 3];
table(myclass). Some table content"#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(result, "let data = vec![1, 2, 3];");
}
#[test]
fn extract_ends_at_notextile_block() {
let content = r#"Text before.
bc(rust#example).. fn example() {
println!("code");
}
notextile. Raw content here."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"fn example() {
println!("code");
}"#
);
}
#[test]
fn extract_bc_with_bracket_syntax() {
let content = r#"Text before.
bc[rust](#example). let value = 100;
p. Text after."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(result, "let value = 100;");
}
#[test]
fn extract_ends_at_right_padded_paragraph() {
let content = r#"Text before.
bc(rust#example).. let x = 10;
let y = 20;
p))). Right padded paragraph."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"let x = 10;
let y = 20;"#
);
}
#[test]
fn extract_ends_at_combined_padding_paragraph() {
let content = r#"Text before.
bc(rust#example).. fn test() {
return 42;
}
p()). Left indent and right padding."#;
let cursor = io::Cursor::new(content);
let (_, result) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(
result,
r#"fn test() {
return 42;
}"#
);
}
#[test]
fn extract_start_line() {
let content = "Text.\n\nbc(rust#example). let x = 1;\n";
let cursor = io::Cursor::new(content);
let (start_line, _) = extract(cursor, "example", collect).expect("expected content");
assert_eq!(start_line, 3);
}
}