This commit is contained in:
Josh Thomas 2025-01-06 09:15:18 -06:00
parent 4b6aab5c69
commit c559baea1f
11 changed files with 164 additions and 126 deletions

View file

@ -44,25 +44,18 @@ impl LineOffsets {
self.0.push(offset); self.0.push(offset);
} }
pub fn position_to_line_col(&self, offset: u32) -> (u32, u32) { pub fn position_to_line_col(&self, position: usize) -> (usize, usize) {
// Find which line contains this offset by looking for the first line start let position = position as u32;
// that's greater than our position let line = match self.0.binary_search(&position) {
let line = match self.0.binary_search(&offset) { Ok(_) => self.0.partition_point(|&x| x <= position),
Ok(exact_line) => exact_line, // We're exactly at a line start Err(i) => i,
Err(next_line) => {
if next_line == 0 {
0 // Before first line start, so we're on line 1
} else {
next_line - 1 // We're on the previous line
}
}
}; };
let col = if line == 0 {
// Calculate column as offset from line start position as usize
let col = offset - self.0[line]; } else {
(position - self.0[line - 1]) as usize
// Convert to 1-based line number };
(line as u32 + 1, col) (line + 1, col)
} }
pub fn line_col_to_position(&self, line: u32, col: u32) -> u32 { pub fn line_col_to_position(&self, line: u32, col: u32) -> u32 {
@ -286,7 +279,7 @@ mod tests {
if let Node::Variable { span, .. } = var_node { if let Node::Variable { span, .. } = var_node {
// Variable starts after newline + "{{" // Variable starts after newline + "{{"
let (line, col) = ast.line_offsets().position_to_line_col(*span.start()); let (line, col) = ast.line_offsets().position_to_line_col(*span.start() as usize);
assert_eq!( assert_eq!(
(line, col), (line, col),
(2, 3), (2, 3),
@ -354,8 +347,8 @@ mod tests {
assert_eq!(content, " Welcome!"); assert_eq!(content, " Welcome!");
eprintln!("Line offsets: {:?}", ast.line_offsets()); eprintln!("Line offsets: {:?}", ast.line_offsets());
eprintln!("Span: {:?}", span); eprintln!("Span: {:?}", span);
let (line, col) = ast.line_offsets().position_to_line_col(span.start); let (line, col) = ast.line_offsets().position_to_line_col(span.start as usize);
assert_eq!((line, col), (2, 0), "Content should be on line 2, col 0"); assert_eq!((line, col), (1, 0), "Content should be on line 1, col 0");
// Check closing tag // Check closing tag
if let Block::Closing { tag } = if let Block::Closing { tag } =

View file

@ -86,82 +86,113 @@ impl Parser {
let total_length = token.length().unwrap_or(0); let total_length = token.length().unwrap_or(0);
let span = Span::new(start_pos, total_length); let span = Span::new(start_pos, total_length);
// Parse the tag name and any assignments let bits: Vec<String> = content.split_whitespace().map(String::from).collect();
let mut bits = content.split_whitespace(); let tag_name = bits.first().ok_or(ParserError::EmptyTag)?.clone();
let tag_name = bits.next().unwrap_or_default().to_string();
let bits_vec: Vec<String> = bits.map(|s| s.to_string()).collect();
// Check for assignment syntax
let mut assignments = Vec::new();
let mut assignment = None;
if bits_vec.len() > 2 && bits_vec[1] == "as" {
assignment = Some(bits_vec[2].clone());
assignments.push(Assignment {
target: bits_vec[2].clone(),
value: bits_vec[3..].join(" "),
});
}
let tag = Tag { let tag = Tag {
name: tag_name.clone(), name: tag_name.clone(),
bits: content.split_whitespace().map(|s| s.to_string()).collect(), bits: bits.clone(),
span, span,
tag_span: span, tag_span: span,
assignment, assignment: None,
}; };
// Check if this is a closing tag
if tag_name.starts_with("end") {
return Ok(Node::Block(Block::Closing { tag }));
}
// Load tag specs
let specs = TagSpec::load_builtin_specs()?; let specs = TagSpec::load_builtin_specs()?;
let spec = match specs.get(&tag_name) { let spec = match specs.get(&tag_name) {
Some(spec) => spec, Some(spec) => spec,
None => return Ok(Node::Block(Block::Tag { tag })), None => return Ok(Node::Block(Block::Tag { tag })),
}; };
match spec.tag_type { let block = match spec.tag_type {
TagType::Block => { TagType::Block => {
let mut nodes = Vec::new(); let mut nodes = Vec::new();
let mut closing = None;
// Parse child nodes until we find the closing tag while !self.is_at_end() {
while let Ok(node) = self.next_node() { match self.next_node() {
if let Node::Block(Block::Closing { tag: closing_tag }) = &node { Ok(Node::Block(Block::Tag { tag })) => {
if let Some(expected_closing) = &spec.closing { if let Some(expected_closing) = &spec.closing {
if closing_tag.name == *expected_closing { if tag.name == *expected_closing {
return Ok(Node::Block(Block::Block { closing = Some(Box::new(Block::Closing { tag }));
tag, break;
nodes, }
closing: Some(Box::new(Block::Closing {
tag: closing_tag.clone(),
})),
assignments: Some(assignments),
}));
} }
// If we get here, either there was no expected closing tag or it didn't match
if let Some(branches) = &spec.branches {
if branches.iter().any(|b| b.name == tag.name) {
let mut branch_tag = tag.clone();
let mut branch_nodes = Vec::new();
let mut found_closing = false;
while let Ok(node) = self.next_node() {
match &node {
Node::Block(Block::Tag { tag: next_tag }) => {
if let Some(expected_closing) = &spec.closing {
if next_tag.name == *expected_closing {
// Found the closing tag
nodes.push(Node::Block(Block::Branch {
tag: branch_tag.clone(),
nodes: branch_nodes.clone(),
}));
closing = Some(Box::new(Block::Closing { tag: next_tag.clone() }));
found_closing = true;
break;
}
}
// Check if this is another branch tag
if branches.iter().any(|b| b.name == next_tag.name) {
// Push the current branch and start a new one
nodes.push(Node::Block(Block::Branch {
tag: branch_tag.clone(),
nodes: branch_nodes.clone(),
}));
branch_nodes = Vec::new();
branch_tag = next_tag.clone();
continue;
}
branch_nodes.push(node);
}
_ => branch_nodes.push(node),
}
}
if !found_closing {
// Push the last branch if we didn't find a closing tag
nodes.push(Node::Block(Block::Branch {
tag: branch_tag,
nodes: branch_nodes,
}));
}
if found_closing {
break;
}
continue;
}
}
nodes.push(Node::Block(Block::Tag { tag }));
}
Ok(node) => nodes.push(node),
Err(e) => {
self.errors.push(e);
break;
} }
} }
nodes.push(node);
} }
// Add error for unclosed tag Block::Block {
self.errors.push(ParserError::Ast(AstError::UnclosedTag(tag_name.clone())));
Ok(Node::Block(Block::Block {
tag, tag,
nodes, nodes,
closing: None, closing,
assignments: Some(assignments), assignments: None,
})) }
} }
TagType::Tag => Ok(Node::Block(Block::Tag { tag })), TagType::Tag => Block::Tag { tag },
TagType::Variable => Ok(Node::Block(Block::Variable { tag })), TagType::Variable => Block::Variable { tag },
TagType::Inclusion => { TagType::Inclusion => {
let template_name = bits_vec.get(1).cloned().unwrap_or_default(); let template_name = bits.get(1).cloned().unwrap_or_default();
Ok(Node::Block(Block::Inclusion { tag, template_name })) Block::Inclusion { tag, template_name }
} }
} };
Ok(Node::Block(block))
} }
fn parse_django_variable(&mut self, content: &str) -> Result<Node, ParserError> { fn parse_django_variable(&mut self, content: &str) -> Result<Node, ParserError> {
@ -356,6 +387,8 @@ pub enum ParserError {
ErrorSignal(Signal), ErrorSignal(Signal),
#[error("{0}")] #[error("{0}")]
Other(#[from] anyhow::Error), Other(#[from] anyhow::Error),
#[error("empty tag")]
EmptyTag,
} }
impl ParserError { impl ParserError {
@ -544,7 +577,9 @@ mod tests {
let (ast, errors) = parser.parse().unwrap(); let (ast, errors) = parser.parse().unwrap();
insta::assert_yaml_snapshot!(ast); insta::assert_yaml_snapshot!(ast);
assert_eq!(errors.len(), 1); assert_eq!(errors.len(), 1);
assert!(matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "if")); assert!(
matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "if")
);
} }
#[test] #[test]
fn test_parse_unclosed_django_for() { fn test_parse_unclosed_django_for() {
@ -554,7 +589,9 @@ mod tests {
let (ast, errors) = parser.parse().unwrap(); let (ast, errors) = parser.parse().unwrap();
insta::assert_yaml_snapshot!(ast); insta::assert_yaml_snapshot!(ast);
assert_eq!(errors.len(), 1); assert_eq!(errors.len(), 1);
assert!(matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "for")); assert!(
matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "for")
);
} }
#[test] #[test]
fn test_parse_unclosed_script() { fn test_parse_unclosed_script() {
@ -593,7 +630,9 @@ mod tests {
let (ast, errors) = parser.parse().unwrap(); let (ast, errors) = parser.parse().unwrap();
insta::assert_yaml_snapshot!(ast); insta::assert_yaml_snapshot!(ast);
assert_eq!(errors.len(), 1); assert_eq!(errors.len(), 1);
assert!(matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "if")); assert!(
matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "if")
);
} }
} }

View file

@ -26,7 +26,7 @@ nodes:
start: 14 start: 14
length: 8 length: 8
- Block: - Block:
Tag: Branch:
tag: tag:
name: elif name: elif
bits: bits:
@ -41,13 +41,14 @@ nodes:
start: 22 start: 22
length: 10 length: 10
assignment: ~ assignment: ~
- Text: nodes:
content: Negative - Text:
span: content: Negative
start: 38 span:
length: 8 start: 38
length: 8
- Block: - Block:
Tag: Branch:
tag: tag:
name: else name: else
bits: bits:
@ -59,11 +60,12 @@ nodes:
start: 46 start: 46
length: 4 length: 4
assignment: ~ assignment: ~
- Text: nodes:
content: Zero - Text:
span: content: Zero
start: 56 span:
length: 4 start: 56
length: 4
closing: closing:
Closing: Closing:
tag: tag:
@ -77,6 +79,6 @@ nodes:
start: 60 start: 60
length: 5 length: 5
assignment: ~ assignment: ~
assignments: [] assignments: ~
line_offsets: line_offsets:
- 0 - 0

View file

@ -28,7 +28,7 @@ nodes:
start: 26 start: 26
length: 4 length: 4
- Block: - Block:
Tag: Branch:
tag: tag:
name: empty name: empty
bits: bits:
@ -40,11 +40,12 @@ nodes:
start: 33 start: 33
length: 5 length: 5
assignment: ~ assignment: ~
- Text: nodes:
content: No items - Text:
span: content: No items
start: 44 span:
length: 8 start: 44
length: 8
closing: closing:
Closing: Closing:
tag: tag:
@ -58,6 +59,6 @@ nodes:
start: 52 start: 52
length: 6 length: 6
assignment: ~ assignment: ~
assignments: [] assignments: ~
line_offsets: line_offsets:
- 0 - 0

View file

@ -36,6 +36,6 @@ nodes:
start: 37 start: 37
length: 5 length: 5
assignment: ~ assignment: ~
assignments: [] assignments: ~
line_offsets: line_offsets:
- 0 - 0

View file

@ -92,7 +92,7 @@ nodes:
start: 148 start: 148
length: 5 length: 5
assignment: ~ assignment: ~
assignments: [] assignments: ~
- Variable: - Variable:
bits: bits:
- group - group
@ -135,7 +135,7 @@ nodes:
start: 220 start: 220
length: 5 length: 5
assignment: ~ assignment: ~
assignments: [] assignments: ~
- Block: - Block:
Block: Block:
tag: tag:
@ -169,9 +169,9 @@ nodes:
start: 262 start: 262
length: 5 length: 5
assignment: ~ assignment: ~
assignments: [] assignments: ~
- Block: - Block:
Tag: Branch:
tag: tag:
name: empty name: empty
bits: bits:
@ -183,11 +183,12 @@ nodes:
start: 278 start: 278
length: 5 length: 5
assignment: ~ assignment: ~
- Text: nodes:
content: " (no groups)" - Text:
span: content: " (no groups)"
start: 290 span:
length: 20 start: 290
length: 20
closing: closing:
Closing: Closing:
tag: tag:
@ -201,9 +202,9 @@ nodes:
start: 314 start: 314
length: 6 length: 6
assignment: ~ assignment: ~
assignments: [] assignments: ~
- Block: - Block:
Tag: Branch:
tag: tag:
name: else name: else
bits: bits:
@ -215,11 +216,12 @@ nodes:
start: 327 start: 327
length: 4 length: 4
assignment: ~ assignment: ~
- Text: nodes:
content: " Guest" - Text:
span: content: " Guest"
start: 338 span:
length: 10 start: 338
length: 10
closing: closing:
Closing: Closing:
tag: tag:
@ -233,7 +235,7 @@ nodes:
start: 348 start: 348
length: 5 length: 5
assignment: ~ assignment: ~
assignments: [] assignments: ~
- Text: - Text:
content: "!" content: "!"
span: span:

View file

@ -56,7 +56,7 @@ nodes:
start: 58 start: 58
length: 5 length: 5
assignment: ~ assignment: ~
assignments: [] assignments: ~
closing: closing:
Closing: Closing:
tag: tag:
@ -70,6 +70,6 @@ nodes:
start: 69 start: 69
length: 6 length: 6
assignment: ~ assignment: ~
assignments: [] assignments: ~
line_offsets: line_offsets:
- 0 - 0

View file

@ -108,7 +108,7 @@ nodes:
start: 320 start: 320
length: 6 length: 6
assignment: ~ assignment: ~
assignments: [] assignments: ~
- Text: - Text:
content: " <footer>Page Footer</footer>" content: " <footer>Page Footer</footer>"
span: span:
@ -120,7 +120,7 @@ nodes:
start: 366 start: 366
length: 6 length: 6
closing: ~ closing: ~
assignments: [] assignments: ~
line_offsets: line_offsets:
- 0 - 0
- 24 - 24

View file

@ -29,6 +29,6 @@ nodes:
start: 26 start: 26
length: 9 length: 9
closing: ~ closing: ~
assignments: [] assignments: ~
line_offsets: line_offsets:
- 0 - 0

View file

@ -24,6 +24,6 @@ nodes:
start: 30 start: 30
length: 7 length: 7
closing: ~ closing: ~
assignments: [] assignments: ~
line_offsets: line_offsets:
- 0 - 0

View file

@ -163,7 +163,7 @@ nodes:
start: 644 start: 644
length: 39 length: 39
- Block: - Block:
Tag: Branch:
tag: tag:
name: else name: else
bits: bits:
@ -175,11 +175,12 @@ nodes:
start: 699 start: 699
length: 4 length: 4
assignment: ~ assignment: ~
- Text: nodes:
content: " <span>User</span>" - Text:
span: content: " <span>User</span>"
start: 710 span:
length: 38 start: 710
length: 38
closing: closing:
Closing: Closing:
tag: tag:
@ -193,7 +194,7 @@ nodes:
start: 764 start: 764
length: 5 length: 5
assignment: ~ assignment: ~
assignments: [] assignments: ~
closing: closing:
Closing: Closing:
tag: tag:
@ -207,7 +208,7 @@ nodes:
start: 788 start: 788
length: 5 length: 5
assignment: ~ assignment: ~
assignments: [] assignments: ~
- Text: - Text:
content: " </div>" content: " </div>"
span: span: