use crate::Position;
use crate::blocks::Block;
use crate::language_parsers::{Comment, CommentsParser};
use crate::tag_parser::{BlockTag, BlockTagParser, WinnowBlockTagParser};
use std::collections::HashMap;
use std::ops::{Range, RangeInclusive};
use std::rc::Rc;
pub trait BlocksParser {
fn parse(&mut self, contents: &str) -> anyhow::Result<Vec<Block>>;
}
pub struct BlocksFromCommentsParser<C: CommentsParser> {
comments_parser: C,
}
impl<C: CommentsParser> BlocksFromCommentsParser<C> {
pub(crate) fn new(comments_parser: C) -> Self {
Self { comments_parser }
}
}
impl<C: CommentsParser> BlocksParser for BlocksFromCommentsParser<C> {
fn parse(&mut self, contents: &str) -> anyhow::Result<Vec<Block>> {
parse_blocks_from_comments(self.comments_parser.parse(contents))
}
}
pub(crate) fn parse_blocks_from_comments(
comments: impl Iterator<Item = Comment>,
) -> anyhow::Result<Vec<Block>> {
let mut blocks = Vec::new();
let mut block_starts = Vec::new();
for partial_block in PartialBlocksIterator::new(comments) {
match partial_block? {
PartialBlock::Start(block_start) => {
block_starts.push(block_start);
}
PartialBlock::End(block_end) => {
if let Some(block_start) = block_starts.pop() {
blocks.push(block_end.into_block(block_start));
} else {
return Err(anyhow::anyhow!(
"Unexpected closed block at line {}, position {}",
block_end.comment.position_range.start.line,
block_end.comment.source_range.start + block_end.start_position
));
}
}
}
}
if let Some(unclosed_block) = block_starts.pop() {
return Err(anyhow::anyhow!(format!(
"Block at line {} is not closed",
unclosed_block.comment.position_range.start.line
)));
}
blocks.sort_by(|a, b| {
a.start_tag_position_range
.start()
.cmp(b.start_tag_position_range.start())
});
Ok(blocks)
}
pub(crate) struct PartialBlocksIterator<I: Iterator<Item = Comment>> {
comments: I,
comment: Option<Rc<Comment>>,
tags_parser_cursor: usize,
}
impl<I: Iterator<Item = Comment>> PartialBlocksIterator<I> {
pub(crate) fn new(comments: I) -> Self {
Self {
comments,
comment: None,
tags_parser_cursor: 0,
}
}
}
impl<I: Iterator<Item = Comment>> Iterator for PartialBlocksIterator<I> {
type Item = anyhow::Result<PartialBlock>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.comment.is_none() {
match self.comments.next() {
Some(c) => {
self.comment = Some(Rc::new(c));
self.tags_parser_cursor = 0;
}
None => return None,
}
}
let comment_rc = self.comment.as_ref().unwrap();
let mut tags_parser =
WinnowBlockTagParser::new(&comment_rc.comment_text, self.tags_parser_cursor);
let block_tag_result = tags_parser.next();
self.tags_parser_cursor = tags_parser.cursor();
return match block_tag_result {
Ok(Some(tag)) => match tag {
BlockTag::Start {
tag_range,
attributes,
} => Some(Ok(PartialBlock::Start(BlockStart::new(
Rc::clone(self.comment.as_ref().unwrap()),
attributes,
tag_range,
)))),
BlockTag::End { start_position } => Some(Ok(PartialBlock::End(BlockEnd::new(
Rc::clone(self.comment.as_ref().unwrap()),
start_position,
)))),
},
Ok(None) => {
self.comment = None;
continue;
}
Err(e) => Some(Err(e)),
};
}
}
}
pub(crate) enum PartialBlock {
Start(BlockStart),
End(BlockEnd),
}
pub(crate) struct BlockStart {
pub(crate) comment: Rc<Comment>,
pub(crate) attributes: HashMap<String, String>,
pub(crate) start_tag_position_range: RangeInclusive<Position>,
}
impl BlockStart {
fn new(
comment: Rc<Comment>,
attributes: HashMap<String, String>,
position_in_comment_range: Range<usize>,
) -> Self {
let start_tag_position_range =
Self::source_position_at(position_in_comment_range.start, &comment)
..=Self::source_position_at(position_in_comment_range.end - 1, &comment);
Self {
comment,
attributes,
start_tag_position_range,
}
}
fn source_position_at(position_in_comment: usize, comment: &Comment) -> Position {
let line_number = comment.position_range.start.line
+ comment.comment_text[..position_in_comment + 1]
.lines()
.count()
- 1;
Position::new(
line_number,
if line_number == comment.position_range.start.line {
comment.position_range.start.character + position_in_comment
} else {
position_in_comment
- comment.comment_text[..position_in_comment]
.rfind('\n')
.unwrap_or(0)
},
)
}
}
pub(crate) struct BlockEnd {
pub(crate) comment: Rc<Comment>,
pub(crate) start_position: usize,
}
impl BlockEnd {
fn new(end_tag_comment: Rc<Comment>, start_position: usize) -> Self {
Self {
comment: end_tag_comment,
start_position,
}
}
pub(crate) fn into_block(self, block_start: BlockStart) -> Block {
let content_range = if !Rc::ptr_eq(&self.comment, &block_start.comment) {
block_start.comment.source_range.end..self.comment.source_range.start
} else {
0..0
};
let content_start_position = block_start.comment.position_range.end.clone();
let content_end_position = self.comment.position_range.start.clone();
Block::new(
block_start.attributes,
block_start.start_tag_position_range,
content_range,
content_start_position..content_end_position,
)
}
}
#[cfg(test)]
mod tests {
use crate::block_parser::BlocksParser;
use crate::blocks::Block;
use crate::{Position, language_parsers, test_utils};
use std::collections::HashMap;
fn create_parser() -> impl BlocksParser {
language_parsers::rust::parser().unwrap()
}
#[test]
fn no_defined_blocks_returns_empty_blocks() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#""
fn say_hello_world() {
println!("hello world!");
}
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks, vec![]);
Ok(())
}
#[test]
fn single_block_with_single_line_content() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"/* <block> */ let say = "hi"; /* </block> */"#;
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![Block::new(
HashMap::new(),
Position::new(1, 4)..=Position::new(1, 10),
test_utils::substr_range(contents, " let say = \"hi\"; "),
Position::new(1, 14)..Position::new(1, 31),
),]
);
Ok(())
}
#[test]
fn single_block_with_multiple_lines_content() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = "// <block>\nlet say = \"hi\";\n// </block>";
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![Block::new(
HashMap::new(),
Position::new(1, 4)..=Position::new(1, 10),
test_utils::substr_range(contents, "\nlet say = \"hi\";\n"),
Position::new(1, 11)..Position::new(3, 1)
),]
);
Ok(())
}
#[test]
fn single_block_with_multiline_starting_block_tag() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = "/* <block\n> */ let say = \"hi\"; // </block>";
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![Block::new(
HashMap::new(),
Position::new(1, 4)..=Position::new(2, 1),
test_utils::substr_range(contents, " let say = \"hi\"; "),
Position::new(2, 5)..Position::new(2, 22),
),]
);
Ok(())
}
#[test]
fn single_block_with_multiline_ending_block_tag() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = "/* <block> */ let say = \"hi\"; /* </block\n> */";
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![Block::new(
HashMap::new(),
Position::new(1, 4)..=Position::new(1, 10),
test_utils::substr_range(contents, " let say = \"hi\"; "),
Position::new(1, 14)..Position::new(1, 31),
),]
);
Ok(())
}
#[test]
fn multiple_blocks_on_separate_lines() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"// <block>
println!("hello1");
// </block>
// <block>
println!("hello2");
// </block>"#;
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![
Block::new(
HashMap::new(),
Position::new(1, 4)..=Position::new(1, 10),
test_utils::substr_range(contents, "\nprintln!(\"hello1\");\n"),
Position::new(1, 11)..Position::new(3, 1),
),
Block::new(
HashMap::new(),
Position::new(4, 4)..=Position::new(4, 10),
test_utils::substr_range(contents, "\nprintln!(\"hello2\");\n"),
Position::new(4, 11)..Position::new(6, 1),
)
]
);
Ok(())
}
#[test]
fn multiple_blocks_on_intersecting_lines() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = "// <block>\nprintln!(\"hello1\");\n/* </block><block> */\nprintln!(\"hello2\");\n// </block>";
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![
Block::new(
HashMap::new(),
Position::new(1, 4)..=Position::new(1, 10),
test_utils::substr_range(contents, "\nprintln!(\"hello1\");\n"),
Position::new(1, 11)..Position::new(3, 1),
),
Block::new(
HashMap::new(),
Position::new(3, 12)..=Position::new(3, 18),
test_utils::substr_range(contents, "\nprintln!(\"hello2\");\n"),
Position::new(3, 22)..Position::new(5, 1),
)
]
);
Ok(())
}
#[test]
fn multiple_blocks_on_single_line() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = "/* <block> */println!(\"hello1\");/* </block><block> */println!(\"hello2\");// </block>";
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![
Block::new(
HashMap::new(),
Position::new(1, 4)..=Position::new(1, 10),
test_utils::substr_range(contents, "println!(\"hello1\");"),
Position::new(1, 14)..Position::new(1, 33),
),
Block::new(
HashMap::new(),
Position::new(1, 44)..=Position::new(1, 50),
test_utils::substr_range(contents, "println!(\"hello2\");"),
Position::new(1, 54)..Position::new(1, 73),
)
]
);
Ok(())
}
#[test]
fn block_starts_on_non_first_comment_line() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = "/* Some comment\n<block> */println!(\"hello1\");// </block>";
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![Block::new(
HashMap::new(),
Position::new(2, 1)..=Position::new(2, 7),
test_utils::substr_range(contents, "println!(\"hello1\");"),
Position::new(2, 11)..Position::new(2, 30),
),]
);
Ok(())
}
#[test]
fn block_ends_on_non_first_comment_line() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = "/* <block> */println!(\"hello1\");/* Some comment\n</block> */";
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![Block::new(
HashMap::new(),
Position::new(1, 4)..=Position::new(1, 10),
test_utils::substr_range(contents, "println!(\"hello1\");"),
Position::new(1, 14)..Position::new(1, 33),
),]
);
Ok(())
}
#[test]
fn nested_blocks() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block name="foo">
fn say_hello_world() {
println!("hello world!");
}
// <block name="bar">
fn say_hello_world_bar() {
println!("hello world bar!");
}
// <block name="bar-bar">
fn say_hello_world_bar_bar() {
println!("hello world bar bar!");
}
// </block>
// </block>
// <block name="buzz">
fn say_hello_world_buzz() {
println!("hello world buzz!");
}
// </block>
// </block>
// <block name="fizz">
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![
Block::new(
HashMap::from([("name".to_string(), "foo".to_string())]),
Position::new(2, 12)..=Position::new(2, 29),
30..620,
Position::new(2, 30)..Position::new(25, 9),
),
Block::new(
HashMap::from([("name".to_string(), "bar".to_string())]),
Position::new(7, 16)..=Position::new(7, 33),
142..440,
Position::new(7, 34)..Position::new(17, 13),
),
Block::new(
HashMap::from([("name".to_string(), "bar-bar".to_string())]),
Position::new(11, 20)..=Position::new(11, 41),
281..415,
Position::new(11, 42)..Position::new(15, 17),
),
Block::new(
HashMap::from([("name".to_string(), "buzz".to_string())]),
Position::new(19, 16)..=Position::new(19, 34),
487..599,
Position::new(19, 35)..Position::new(23, 13),
),
Block::new(
HashMap::from([("name".to_string(), "fizz".to_string())]),
Position::new(26, 12)..=Position::new(26, 30),
662..671,
Position::new(26, 31)..Position::new(27, 9),
),
]
);
Ok(())
}
#[test]
fn multiple_nested_blocks_at_same_level() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block name="parent">
// <block name="child1">
fn child1() {}
// </block>
// <block name="child2">
fn child2() {}
// </block>
// <block name="child3">
fn child3() {}
// </block>
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 4);
assert_eq!(blocks[0].attributes["name"], "parent");
assert_eq!(blocks[1].attributes["name"], "child1");
assert_eq!(blocks[2].attributes["name"], "child2");
assert_eq!(blocks[3].attributes["name"], "child3");
Ok(())
}
#[test]
fn block_contents_in_comments_is_ignored() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"// <block name="foo">This text is ignored
let word = "hello";
// </block> Some comment."#;
let blocks = parser.parse(contents)?;
assert_eq!(
blocks,
vec![Block::new(
HashMap::from([("name".to_string(), "foo".to_string())]),
Position::new(1, 4)..=Position::new(1, 21),
test_utils::substr_range(contents, "\n let word = \"hello\";\n "),
Position::new(1, 42)..Position::new(3, 9),
),]
);
Ok(())
}
#[test]
fn unclosed_block_returns_error() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block name="foo">
fn say_hello_world() {
println!("hello world!");
}
"#;
let error_message = parser.parse(contents).unwrap_err().to_string();
assert_eq!(error_message, "Block at line 2 is not closed");
Ok(())
}
#[test]
fn unclosed_nested_block_returns_error() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block name="foo">
fn say_hello_world() {
println!("hello world!");
}
// <block name="bar">
fn say_hello_world_bar() {
}
// </block>
"#;
let error_message = parser.parse(contents).unwrap_err().to_string();
assert_eq!(error_message, "Block at line 2 is not closed");
Ok(())
}
#[test]
fn incorrect_endblock_returns_error() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
fn say_hello_world() {
println!("hello world!");
}
// </block>
"#;
let result = parser.parse(contents);
assert!(result.is_err());
Ok(())
}
#[test]
fn attributes_on_single_line() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block foo="bar" fizz="buzz">
fn foo() {
println!("hello world!");
}
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 1);
assert_eq!(
blocks[0].attributes,
HashMap::from([
("foo".to_string(), "bar".to_string()),
("fizz".to_string(), "buzz".to_string())
])
);
Ok(())
}
#[test]
fn attributes_on_multiple_lines() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
/* <block
foo="bar"
fizz="buzz"> */
fn foo() {
println!("hello world!");
}
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 1);
assert_eq!(
blocks[0].attributes,
HashMap::from([
("foo".to_string(), "bar".to_string()),
("fizz".to_string(), "buzz".to_string())
])
);
Ok(())
}
#[test]
fn attributes_with_single_quotes() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block text='He said "Hello"'>
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks[0].attributes["text"], "He said \"Hello\"");
Ok(())
}
#[test]
fn attributes_with_html_escaped_quotes_are_not_decoded() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block text="He said "Hello"">
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks[0].attributes["text"], "He said "Hello"");
Ok(())
}
#[test]
fn attributes_with_no_quotes() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block color=red flavor=sweet>
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks[0].attributes["color"], "red");
assert_eq!(blocks[0].attributes["flavor"], "sweet");
Ok(())
}
#[test]
fn attributes_with_no_value() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block attr1 attr2>
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks[0].attributes["attr1"], "");
assert_eq!(blocks[0].attributes["attr2"], "");
Ok(())
}
#[test]
fn attributes_with_empty_string_value() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block name="" foo="" bar=''>
fn foo() {}
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(
blocks[0].attributes,
HashMap::from([
("name".to_string(), "".to_string()),
("foo".to_string(), "".to_string()),
("bar".to_string(), "".to_string())
])
);
Ok(())
}
#[test]
fn attributes_with_html_symbols() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block keep-unique="(?P<value>\w+)">
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks[0].attributes["keep-unique"], r"(?P<value>\w+)");
Ok(())
}
#[test]
fn attributes_with_unicode_value() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block name="🦀" desc="Rust">
fn unicode() {}
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks[0].attributes["name"], "🦀");
assert_eq!(blocks[0].attributes["desc"], "Rust");
Ok(())
}
#[test]
fn attributes_with_spaces_around_values() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block name = "foo" desc = 'bar'>
fn unicode() {}
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks[0].attributes["name"], "foo");
assert_eq!(blocks[0].attributes["desc"], "bar");
Ok(())
}
#[test]
fn multiple_mixed_attributes() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block color="red" attr1 align="center" attr2>
fn escaped() {}
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks[0].attributes["color"], "red");
assert_eq!(blocks[0].attributes["attr1"], "");
assert_eq!(blocks[0].attributes["align"], "center");
assert_eq!(blocks[0].attributes["attr2"], "");
Ok(())
}
#[test]
fn duplicated_attributes_uses_last_value() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block color="red" color="blue">
fn escaped() {}
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].attributes.get("color"), Some(&"blue".to_string()));
Ok(())
}
#[test]
fn nested_blocks_with_attributes() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block name="outer" foo="bar">
fn outer() {
// <block name="inner" fizz="buzz">
fn inner() {}
// </block>
}
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 2);
assert_eq!(
blocks[0].attributes,
HashMap::from([
("name".to_string(), "outer".to_string()),
("foo".to_string(), "bar".to_string()),
])
);
assert_eq!(
blocks[1].attributes,
HashMap::from([
("name".to_string(), "inner".to_string()),
("fizz".to_string(), "buzz".to_string())
])
);
Ok(())
}
#[test]
fn malformed_block_tag_returns_error() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <block name="foo" affects="file:block" invalid-attr=">
fn foo() {}
// </block>
"#;
assert!(parser.parse(contents).is_err());
Ok(())
}
#[test]
fn blocks_with_different_line_endings() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = "// <block>\r\nWindows\r\n// </block>\n// <block>\nUnix\n// </block>";
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 2);
assert!(blocks[0].content(contents).contains("\r\n"));
assert!(blocks[1].content(contents).contains("\n"));
Ok(())
}
#[test]
fn comments_with_xml_like_symbols() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
/*
Some logical expressions: a && b, a & b, a ^ b, a || b, a | b, a ^ !b
Arithmetic expressions: a < b, a << b, d > f
<block>
*/
fn unicode() {}
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 1);
Ok(())
}
#[test]
fn comments_with_quotes_and_parenthesis_symbols() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
/// "cxx" -> "c")
// "a" block
// "b" block
// "c" block
// <block name="foo-bar">
// </block>"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 1);
Ok(())
}
#[test]
fn comments_with_unrelated_tags() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <p>Paragraph</p><block><b>bold</b>
fn unicode() {}
// </block><body>hello</body>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 1);
Ok(())
}
#[test]
fn comments_with_unclosed_unrelated_tags() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <p>this tag has no ending tag <block></b> this tag has no starting tag
fn unicode() {}
// </block>
"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 1);
Ok(())
}
#[test]
fn comments_with_invalid_tags() -> anyhow::Result<()> {
let mut parser = create_parser();
let contents = r#"
// <invalid tag
// <block>
fn unicode() {}
// </block>"#;
let blocks = parser.parse(contents)?;
assert_eq!(blocks.len(), 1);
Ok(())
}
}