diff --git a/crates/djls-template-ast/src/ast.rs b/crates/djls-template-ast/src/ast.rs index c4e13b4..34009b5 100644 --- a/crates/djls-template-ast/src/ast.rs +++ b/crates/djls-template-ast/src/ast.rs @@ -47,9 +47,9 @@ impl LineOffsets { pub fn position_to_line_col(&self, position: usize) -> (usize, usize) { let position = position as u32; let line = match self.0.binary_search(&position) { - Ok(exact_line) => exact_line, // Position is at start of this line - Err(0) => 0, // Before first line start - Err(next_line) => next_line - 1, // We're on the previous line + Ok(exact_line) => exact_line, // Position is at start of this line + Err(0) => 0, // Before first line start + Err(next_line) => next_line - 1, // We're on the previous line }; // Calculate column as offset from line start @@ -284,7 +284,9 @@ mod tests { if let Node::Variable { span, .. } = var_node { // Variable starts after newline + "{{" - let (line, col) = ast.line_offsets().position_to_line_col(*span.start() as usize); + let (line, col) = ast + .line_offsets() + .position_to_line_col(*span.start() as usize); assert_eq!( (line, col), (2, 3), @@ -349,11 +351,11 @@ mod tests { eprintln!("Nodes: {:?}", nodes); assert_eq!(nodes.len(), 1); if let Node::Text { content, span } = &nodes[0] { - assert_eq!(content, " Welcome!"); + assert_eq!(content, "Welcome!"); eprintln!("Line offsets: {:?}", ast.line_offsets()); eprintln!("Span: {:?}", span); 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), (2, 2), "Content should be on line 2, col 2"); // Check closing tag if let Block::Closing { tag } = diff --git a/crates/djls-template-ast/src/parser.rs b/crates/djls-template-ast/src/parser.rs index 18ce7dd..37734e6 100644 --- a/crates/djls-template-ast/src/parser.rs +++ b/crates/djls-template-ast/src/parser.rs @@ -303,6 +303,20 @@ impl Parser { | TokenType::Comment(_, _, _) | TokenType::Newline | TokenType::Eof => break, + TokenType::Whitespace(_) => { + // Check if next token is a newline + if let Ok(next) = self.peek_at(1) { + if matches!(next.token_type(), TokenType::Newline) { + self.consume()?; + break; + } + } + // Not before newline, treat as normal text + let token_text = token.token_type().to_string(); + text.push_str(&token_text); + total_length += u32::try_from(token_text.len()).unwrap(); + self.consume()?; + } _ => { let token_text = token.token_type().to_string(); text.push_str(&token_text); @@ -428,6 +442,7 @@ mod tests { insta::assert_yaml_snapshot!(ast); assert!(errors.is_empty()); } + #[test] fn test_parse_html_tag() { let source = "
Hello
"; @@ -437,6 +452,7 @@ mod tests { insta::assert_yaml_snapshot!(ast); assert!(errors.is_empty()); } + #[test] fn test_parse_html_void() { let source = ""; @@ -447,6 +463,7 @@ mod tests { assert!(errors.is_empty()); } } + mod django { use super::*; #[test] @@ -458,6 +475,7 @@ mod tests { insta::assert_yaml_snapshot!(ast); assert!(errors.is_empty()); } + #[test] fn test_parse_filter_chains() { let source = "{{ value|default:'nothing'|title|upper }}"; @@ -467,6 +485,7 @@ mod tests { insta::assert_yaml_snapshot!(ast); assert!(errors.is_empty()); } + #[test] fn test_parse_django_if_block() { let source = "{% if user.is_authenticated %}Welcome{% endif %}"; @@ -476,6 +495,7 @@ mod tests { insta::assert_yaml_snapshot!(ast); assert!(errors.is_empty()); } + #[test] fn test_parse_django_for_block() { let source = "{% for item in items %}{{ item }}{% empty %}No items{% endfor %}"; @@ -485,6 +505,7 @@ mod tests { insta::assert_yaml_snapshot!(ast); assert!(errors.is_empty()); } + #[test] fn test_parse_complex_if_elif() { let source = "{% if x > 0 %}Positive{% elif x < 0 %}Negative{% else %}Zero{% endif %}"; @@ -494,6 +515,7 @@ mod tests { insta::assert_yaml_snapshot!(ast); assert!(errors.is_empty()); } + #[test] fn test_parse_nested_for_if() { let source = @@ -504,6 +526,7 @@ mod tests { insta::assert_yaml_snapshot!(ast); assert!(errors.is_empty()); } + #[test] fn test_parse_mixed_content() { let source = "Welcome, {% if user.is_authenticated %} @@ -526,8 +549,10 @@ mod tests { assert!(errors.is_empty()); } } + mod script { use super::*; + #[test] fn test_parse_script() { let source = r#"