use crate::utils::CodeBlock;
#[derive(Debug, Clone)]
pub struct BlockMapping {
pub concatenated_range: std::ops::Range<usize>,
pub original_range: std::ops::Range<usize>,
pub start_line: usize,
}
#[derive(Debug, Clone)]
pub struct ConcatenatedBlocks {
pub content: String,
pub mappings: Vec<BlockMapping>,
}
pub fn concatenate_with_blanks_and_mapping(blocks: &[CodeBlock]) -> ConcatenatedBlocks {
if blocks.is_empty() {
return ConcatenatedBlocks {
content: String::new(),
mappings: Vec::new(),
};
}
let mut content = String::new();
let mut mappings = Vec::new();
let mut current_line = 1;
for block in blocks {
while current_line < block.start_line {
content.push('\n');
current_line += 1;
}
let concat_start = content.len();
content.push_str(&block.content);
let concat_end = content.len();
mappings.push(BlockMapping {
concatenated_range: concat_start..concat_end,
original_range: block.original_range.clone(),
start_line: block.start_line,
});
let lines_added = block.content.lines().count().max(1);
current_line += lines_added;
if !block.content.ends_with('\n') {
content.push('\n');
current_line += 1;
}
}
ConcatenatedBlocks { content, mappings }
}
pub fn concatenate_with_blanks(blocks: &[CodeBlock]) -> String {
concatenate_with_blanks_and_mapping(blocks).content
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, Flavor};
use crate::parse;
use crate::utils::{CodeBlock, collect_code_blocks, offset_to_line};
#[test]
fn test_collect_single_r_block() {
let input = r#"# Test
```r
x <- 1
y <- 2
```
"#;
let tree = parse(input, None);
let blocks = collect_code_blocks(&tree, input);
assert_eq!(blocks.len(), 1);
assert!(blocks.contains_key("r"));
let r_blocks = &blocks["r"];
assert_eq!(r_blocks.len(), 1);
assert_eq!(r_blocks[0].language, "r");
assert_eq!(r_blocks[0].content, "x <- 1\ny <- 2\n");
assert_eq!(r_blocks[0].start_line, 4); }
#[test]
fn test_collect_multiple_blocks_same_language() {
let input = r#"```r
x <- 1
```
Text in between.
```r
y <- 2
```
"#;
let tree = parse(input, None);
let blocks = collect_code_blocks(&tree, input);
assert_eq!(blocks.len(), 1);
let r_blocks = &blocks["r"];
assert_eq!(r_blocks.len(), 2);
assert_eq!(r_blocks[0].start_line, 2); assert_eq!(r_blocks[1].start_line, 8); }
#[test]
fn test_collect_multiple_languages() {
let input = r#"```python
print("hello")
```
```r
print("hello")
```
"#;
let tree = parse(input, None);
let blocks = collect_code_blocks(&tree, input);
assert_eq!(blocks.len(), 2);
assert!(blocks.contains_key("python"));
assert!(blocks.contains_key("r"));
}
#[test]
fn test_concatenate_with_blanks_single_block() {
let blocks = vec![CodeBlock {
language: "r".to_string(),
content: "x <- 1\n".to_string(),
start_line: 5,
original_range: 100..107, }];
let result = concatenate_with_blanks(&blocks);
let expected = "\n\n\n\nx <- 1\n";
assert_eq!(result, expected);
}
#[test]
fn test_concatenate_with_blanks_multiple_blocks() {
let blocks = vec![
CodeBlock {
language: "r".to_string(),
content: "x <- 1\n".to_string(),
start_line: 2,
original_range: 50..57,
},
CodeBlock {
language: "r".to_string(),
content: "y <- 2\n".to_string(),
start_line: 6,
original_range: 150..157,
},
];
let result = concatenate_with_blanks(&blocks);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 6);
assert_eq!(lines[0], ""); assert_eq!(lines[1], "x <- 1"); assert_eq!(lines[2], ""); assert_eq!(lines[3], ""); assert_eq!(lines[4], ""); assert_eq!(lines[5], "y <- 2"); }
#[test]
fn test_concatenate_preserves_line_numbers() {
let blocks = vec![
CodeBlock {
language: "r".to_string(),
content: "a <- 1\n".to_string(),
start_line: 10,
original_range: 200..207,
},
CodeBlock {
language: "r".to_string(),
content: "b <- 2\n".to_string(),
start_line: 20,
original_range: 400..407,
},
];
let result = concatenate_with_blanks(&blocks);
let line_count = result.lines().count();
assert_eq!(line_count, 20);
let line_10 = result.lines().nth(9).unwrap(); assert_eq!(line_10, "a <- 1");
let line_20 = result.lines().nth(19).unwrap();
assert_eq!(line_20, "b <- 2");
}
#[test]
fn test_offset_to_line() {
let input = "line1\nline2\nline3\n";
assert_eq!(offset_to_line(input, 0), 1); assert_eq!(offset_to_line(input, 5), 1); assert_eq!(offset_to_line(input, 6), 2); assert_eq!(offset_to_line(input, 12), 3); }
#[test]
fn test_quarto_style_braces() {
let input = r#"```{r}
x <- 1
```
"#;
let config = Config {
flavor: Flavor::Quarto,
extensions: crate::config::Extensions::for_flavor(Flavor::Quarto),
..Default::default()
};
let tree = parse(input, Some(config));
let blocks = collect_code_blocks(&tree, input);
assert_eq!(blocks.len(), 1);
assert!(blocks.contains_key("r"), "Should extract 'r' from '{{r}}'");
let r_blocks = &blocks["r"];
assert_eq!(r_blocks.len(), 1);
assert_eq!(r_blocks[0].language, "r");
assert_eq!(r_blocks[0].content, "x <- 1\n");
}
#[test]
fn test_quarto_style_braces_with_options() {
let input = r#"```{r my-label, echo=FALSE}
x <- 1
```
"#;
let config = Config {
flavor: Flavor::Quarto,
extensions: crate::config::Extensions::for_flavor(Flavor::Quarto),
..Default::default()
};
let tree = parse(input, Some(config));
let blocks = collect_code_blocks(&tree, input);
assert_eq!(blocks.len(), 1);
assert!(
blocks.contains_key("r"),
"Should extract 'r' from '{{r my-label, echo=FALSE}}'"
);
let r_blocks = &blocks["r"];
assert_eq!(r_blocks.len(), 1);
assert_eq!(r_blocks[0].language, "r");
}
#[test]
fn test_quarto_display_class_language_normalized() {
let input = "```{.bash filename=\"Terminal\"}\necho hi\n```\n";
let config = Config {
flavor: Flavor::Quarto,
extensions: crate::config::Extensions::for_flavor(Flavor::Quarto),
..Default::default()
};
let tree = parse(input, Some(config));
let blocks = collect_code_blocks(&tree, input);
assert!(blocks.contains_key("bash"));
let bash_blocks = &blocks["bash"];
assert_eq!(bash_blocks.len(), 1);
assert_eq!(bash_blocks[0].language, "bash");
}
#[test]
fn test_quarto_various_syntaxes() {
let input = r#"```{r}
a <- 1
```
```{python}
b = 2
```
```{r chunk-label}
c <- 3
```
```{r chunk2, echo=FALSE}
d <- 4
```
"#;
let config = Config {
flavor: Flavor::Quarto,
extensions: crate::config::Extensions::for_flavor(Flavor::Quarto),
..Default::default()
};
let tree = parse(input, Some(config));
let blocks = collect_code_blocks(&tree, input);
assert_eq!(blocks.len(), 2);
assert!(blocks.contains_key("r"));
assert!(blocks.contains_key("python"));
let r_blocks = &blocks["r"];
assert_eq!(r_blocks.len(), 3, "Should find all three R blocks");
let py_blocks = &blocks["python"];
assert_eq!(py_blocks.len(), 1);
}
#[test]
fn test_concatenate_with_mapping() {
let blocks = vec![
CodeBlock {
language: "r".to_string(),
content: "x <- 1\n".to_string(),
start_line: 2,
original_range: 10..17, },
CodeBlock {
language: "r".to_string(),
content: "y <- 2\n".to_string(),
start_line: 6,
original_range: 50..57,
},
];
let result = concatenate_with_blanks_and_mapping(&blocks);
let lines: Vec<&str> = result.content.lines().collect();
assert_eq!(lines.len(), 6);
assert_eq!(lines[1], "x <- 1"); assert_eq!(lines[5], "y <- 2");
assert_eq!(result.mappings.len(), 2);
assert_eq!(result.mappings[0].start_line, 2);
assert_eq!(result.mappings[0].original_range, 10..17);
assert_eq!(result.mappings[0].concatenated_range.start, 1);
assert_eq!(result.mappings[0].concatenated_range.end, 8);
assert_eq!(result.mappings[1].start_line, 6);
assert_eq!(result.mappings[1].original_range, 50..57);
assert_eq!(result.mappings[1].concatenated_range.start, 11);
assert_eq!(result.mappings[1].concatenated_range.end, 18);
}
}