Skip to content

Commit c559bae

Browse files
wip
1 parent 4b6aab5 commit c559bae

11 files changed

+165
-127
lines changed

crates/djls-template-ast/src/ast.rs

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,25 +44,18 @@ impl LineOffsets {
4444
self.0.push(offset);
4545
}
4646

47-
pub fn position_to_line_col(&self, offset: u32) -> (u32, u32) {
48-
// Find which line contains this offset by looking for the first line start
49-
// that's greater than our position
50-
let line = match self.0.binary_search(&offset) {
51-
Ok(exact_line) => exact_line, // We're exactly at a line start
52-
Err(next_line) => {
53-
if next_line == 0 {
54-
0 // Before first line start, so we're on line 1
55-
} else {
56-
next_line - 1 // We're on the previous line
57-
}
58-
}
47+
pub fn position_to_line_col(&self, position: usize) -> (usize, usize) {
48+
let position = position as u32;
49+
let line = match self.0.binary_search(&position) {
50+
Ok(_) => self.0.partition_point(|&x| x <= position),
51+
Err(i) => i,
5952
};
60-
61-
// Calculate column as offset from line start
62-
let col = offset - self.0[line];
63-
64-
// Convert to 1-based line number
65-
(line as u32 + 1, col)
53+
let col = if line == 0 {
54+
position as usize
55+
} else {
56+
(position - self.0[line - 1]) as usize
57+
};
58+
(line + 1, col)
6659
}
6760

6861
pub fn line_col_to_position(&self, line: u32, col: u32) -> u32 {
@@ -286,7 +279,7 @@ mod tests {
286279

287280
if let Node::Variable { span, .. } = var_node {
288281
// Variable starts after newline + "{{"
289-
let (line, col) = ast.line_offsets().position_to_line_col(*span.start());
282+
let (line, col) = ast.line_offsets().position_to_line_col(*span.start() as usize);
290283
assert_eq!(
291284
(line, col),
292285
(2, 3),
@@ -354,8 +347,8 @@ mod tests {
354347
assert_eq!(content, " Welcome!");
355348
eprintln!("Line offsets: {:?}", ast.line_offsets());
356349
eprintln!("Span: {:?}", span);
357-
let (line, col) = ast.line_offsets().position_to_line_col(span.start);
358-
assert_eq!((line, col), (2, 0), "Content should be on line 2, col 0");
350+
let (line, col) = ast.line_offsets().position_to_line_col(span.start as usize);
351+
assert_eq!((line, col), (1, 0), "Content should be on line 1, col 0");
359352

360353
// Check closing tag
361354
if let Block::Closing { tag } =

crates/djls-template-ast/src/parser.rs

Lines changed: 93 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -86,82 +86,113 @@ impl Parser {
8686
let total_length = token.length().unwrap_or(0);
8787
let span = Span::new(start_pos, total_length);
8888

89-
// Parse the tag name and any assignments
90-
let mut bits = content.split_whitespace();
91-
let tag_name = bits.next().unwrap_or_default().to_string();
92-
let bits_vec: Vec<String> = bits.map(|s| s.to_string()).collect();
93-
94-
// Check for assignment syntax
95-
let mut assignments = Vec::new();
96-
let mut assignment = None;
97-
if bits_vec.len() > 2 && bits_vec[1] == "as" {
98-
assignment = Some(bits_vec[2].clone());
99-
assignments.push(Assignment {
100-
target: bits_vec[2].clone(),
101-
value: bits_vec[3..].join(" "),
102-
});
103-
}
89+
let bits: Vec<String> = content.split_whitespace().map(String::from).collect();
90+
let tag_name = bits.first().ok_or(ParserError::EmptyTag)?.clone();
10491

10592
let tag = Tag {
10693
name: tag_name.clone(),
107-
bits: content.split_whitespace().map(|s| s.to_string()).collect(),
94+
bits: bits.clone(),
10895
span,
10996
tag_span: span,
110-
assignment,
97+
assignment: None,
11198
};
11299

113-
// Check if this is a closing tag
114-
if tag_name.starts_with("end") {
115-
return Ok(Node::Block(Block::Closing { tag }));
116-
}
117-
118-
// Load tag specs
119100
let specs = TagSpec::load_builtin_specs()?;
120101
let spec = match specs.get(&tag_name) {
121102
Some(spec) => spec,
122103
None => return Ok(Node::Block(Block::Tag { tag })),
123104
};
124105

125-
match spec.tag_type {
106+
let block = match spec.tag_type {
126107
TagType::Block => {
127108
let mut nodes = Vec::new();
128-
129-
// Parse child nodes until we find the closing tag
130-
while let Ok(node) = self.next_node() {
131-
if let Node::Block(Block::Closing { tag: closing_tag }) = &node {
132-
if let Some(expected_closing) = &spec.closing {
133-
if closing_tag.name == *expected_closing {
134-
return Ok(Node::Block(Block::Block {
135-
tag,
136-
nodes,
137-
closing: Some(Box::new(Block::Closing {
138-
tag: closing_tag.clone(),
139-
})),
140-
assignments: Some(assignments),
141-
}));
109+
let mut closing = None;
110+
111+
while !self.is_at_end() {
112+
match self.next_node() {
113+
Ok(Node::Block(Block::Tag { tag })) => {
114+
if let Some(expected_closing) = &spec.closing {
115+
if tag.name == *expected_closing {
116+
closing = Some(Box::new(Block::Closing { tag }));
117+
break;
118+
}
119+
}
120+
// If we get here, either there was no expected closing tag or it didn't match
121+
if let Some(branches) = &spec.branches {
122+
if branches.iter().any(|b| b.name == tag.name) {
123+
let mut branch_tag = tag.clone();
124+
let mut branch_nodes = Vec::new();
125+
let mut found_closing = false;
126+
while let Ok(node) = self.next_node() {
127+
match &node {
128+
Node::Block(Block::Tag { tag: next_tag }) => {
129+
if let Some(expected_closing) = &spec.closing {
130+
if next_tag.name == *expected_closing {
131+
// Found the closing tag
132+
nodes.push(Node::Block(Block::Branch {
133+
tag: branch_tag.clone(),
134+
nodes: branch_nodes.clone(),
135+
}));
136+
closing = Some(Box::new(Block::Closing { tag: next_tag.clone() }));
137+
found_closing = true;
138+
break;
139+
}
140+
}
141+
// Check if this is another branch tag
142+
if branches.iter().any(|b| b.name == next_tag.name) {
143+
// Push the current branch and start a new one
144+
nodes.push(Node::Block(Block::Branch {
145+
tag: branch_tag.clone(),
146+
nodes: branch_nodes.clone(),
147+
}));
148+
branch_nodes = Vec::new();
149+
branch_tag = next_tag.clone();
150+
continue;
151+
}
152+
branch_nodes.push(node);
153+
}
154+
_ => branch_nodes.push(node),
155+
}
156+
}
157+
if !found_closing {
158+
// Push the last branch if we didn't find a closing tag
159+
nodes.push(Node::Block(Block::Branch {
160+
tag: branch_tag,
161+
nodes: branch_nodes,
162+
}));
163+
}
164+
if found_closing {
165+
break;
166+
}
167+
continue;
168+
}
142169
}
170+
nodes.push(Node::Block(Block::Tag { tag }));
171+
}
172+
Ok(node) => nodes.push(node),
173+
Err(e) => {
174+
self.errors.push(e);
175+
break;
143176
}
144177
}
145-
nodes.push(node);
146178
}
147179

148-
// Add error for unclosed tag
149-
self.errors.push(ParserError::Ast(AstError::UnclosedTag(tag_name.clone())));
150-
151-
Ok(Node::Block(Block::Block {
180+
Block::Block {
152181
tag,
153182
nodes,
154-
closing: None,
155-
assignments: Some(assignments),
156-
}))
183+
closing,
184+
assignments: None,
185+
}
157186
}
158-
TagType::Tag => Ok(Node::Block(Block::Tag { tag })),
159-
TagType::Variable => Ok(Node::Block(Block::Variable { tag })),
187+
TagType::Tag => Block::Tag { tag },
188+
TagType::Variable => Block::Variable { tag },
160189
TagType::Inclusion => {
161-
let template_name = bits_vec.get(1).cloned().unwrap_or_default();
162-
Ok(Node::Block(Block::Inclusion { tag, template_name }))
190+
let template_name = bits.get(1).cloned().unwrap_or_default();
191+
Block::Inclusion { tag, template_name }
163192
}
164-
}
193+
};
194+
195+
Ok(Node::Block(block))
165196
}
166197

167198
fn parse_django_variable(&mut self, content: &str) -> Result<Node, ParserError> {
@@ -356,6 +387,8 @@ pub enum ParserError {
356387
ErrorSignal(Signal),
357388
#[error("{0}")]
358389
Other(#[from] anyhow::Error),
390+
#[error("empty tag")]
391+
EmptyTag,
359392
}
360393

361394
impl ParserError {
@@ -544,7 +577,9 @@ mod tests {
544577
let (ast, errors) = parser.parse().unwrap();
545578
insta::assert_yaml_snapshot!(ast);
546579
assert_eq!(errors.len(), 1);
547-
assert!(matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "if"));
580+
assert!(
581+
matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "if")
582+
);
548583
}
549584
#[test]
550585
fn test_parse_unclosed_django_for() {
@@ -554,7 +589,9 @@ mod tests {
554589
let (ast, errors) = parser.parse().unwrap();
555590
insta::assert_yaml_snapshot!(ast);
556591
assert_eq!(errors.len(), 1);
557-
assert!(matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "for"));
592+
assert!(
593+
matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "for")
594+
);
558595
}
559596
#[test]
560597
fn test_parse_unclosed_script() {
@@ -593,7 +630,9 @@ mod tests {
593630
let (ast, errors) = parser.parse().unwrap();
594631
insta::assert_yaml_snapshot!(ast);
595632
assert_eq!(errors.len(), 1);
596-
assert!(matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "if"));
633+
assert!(
634+
matches!(&errors[0], ParserError::Ast(AstError::UnclosedTag(tag)) if tag == "if")
635+
);
597636
}
598637
}
599638

crates/djls-template-ast/src/snapshots/djls_template_ast__parser__tests__django__parse_complex_if_elif.snap

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ nodes:
2626
start: 14
2727
length: 8
2828
- Block:
29-
Tag:
29+
Branch:
3030
tag:
3131
name: elif
3232
bits:
@@ -41,13 +41,14 @@ nodes:
4141
start: 22
4242
length: 10
4343
assignment: ~
44-
- Text:
45-
content: Negative
46-
span:
47-
start: 38
48-
length: 8
44+
nodes:
45+
- Text:
46+
content: Negative
47+
span:
48+
start: 38
49+
length: 8
4950
- Block:
50-
Tag:
51+
Branch:
5152
tag:
5253
name: else
5354
bits:
@@ -59,11 +60,12 @@ nodes:
5960
start: 46
6061
length: 4
6162
assignment: ~
62-
- Text:
63-
content: Zero
64-
span:
65-
start: 56
66-
length: 4
63+
nodes:
64+
- Text:
65+
content: Zero
66+
span:
67+
start: 56
68+
length: 4
6769
closing:
6870
Closing:
6971
tag:
@@ -77,6 +79,6 @@ nodes:
7779
start: 60
7880
length: 5
7981
assignment: ~
80-
assignments: []
82+
assignments: ~
8183
line_offsets:
8284
- 0

crates/djls-template-ast/src/snapshots/djls_template_ast__parser__tests__django__parse_django_for_block.snap

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ nodes:
2828
start: 26
2929
length: 4
3030
- Block:
31-
Tag:
31+
Branch:
3232
tag:
3333
name: empty
3434
bits:
@@ -40,11 +40,12 @@ nodes:
4040
start: 33
4141
length: 5
4242
assignment: ~
43-
- Text:
44-
content: No items
45-
span:
46-
start: 44
47-
length: 8
43+
nodes:
44+
- Text:
45+
content: No items
46+
span:
47+
start: 44
48+
length: 8
4849
closing:
4950
Closing:
5051
tag:
@@ -58,6 +59,6 @@ nodes:
5859
start: 52
5960
length: 6
6061
assignment: ~
61-
assignments: []
62+
assignments: ~
6263
line_offsets:
6364
- 0

crates/djls-template-ast/src/snapshots/djls_template_ast__parser__tests__django__parse_django_if_block.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@ nodes:
3636
start: 37
3737
length: 5
3838
assignment: ~
39-
assignments: []
39+
assignments: ~
4040
line_offsets:
4141
- 0

0 commit comments

Comments
 (0)