This commit is contained in:
Josh Thomas 2025-01-05 20:25:07 -06:00
parent 00eca98c6a
commit f1cc33f173
21 changed files with 1323 additions and 651 deletions

View file

@ -0,0 +1,482 @@
# Django Template AST Specification
## Overview
This document specifies the Abstract Syntax Tree (AST) design for parsing Django templates. The AST represents the structure and semantics of Django templates, enabling accurate parsing, analysis, and tooling support.
## Types
### `Ast`
The root of the AST, representing the entire parsed template.
```rust
pub struct Ast {
pub nodes: Vec<Node>, // Top-level nodes in the template
pub line_offsets: Vec<u32>, // Positions of line breaks for mapping offsets to line/column
}
```
### `Span`
Represents the position of a node within the source template.
```rust
pub struct Span {
pub start: u32, // Byte offset from the start of the template
pub length: u32, // Length in bytes
}
```
### `Node`
Enumeration of all possible node types in the AST.
```rust
pub enum Node {
Text {
content: String, // The raw text content
span: Span, // The position of the text in the template
},
Comment {
content: String, // The comment content
span: Span, // The position of the comment in the template
},
Variable {
bits: Vec<String>, // Components of the variable path
filters: Vec<DjangoFilter>, // Filters applied to the variable
span: Span, // The position of the variable in the template
},
Block(Block),
}
```
#### `Node::Text`
Represents raw text and HTML content outside of Django template tags.
```rust
Node::Text {
content: String, // The raw text content
span: Span, // The position of the text in the template
}
```
#### `Node::Comment`
Represents Django template comments (`{# ... #}`).
```rust
Node::Comment {
content: String, // The comment content
span: Span, // The position of the comment in the template
}
```
#### `Node::Variable`
Represents variable interpolation (`{{ variable|filter }}`).
```rust
Node::Variable {
bits: Vec<String>, // Components of the variable path
filters: Vec<DjangoFilter>, // Filters applied to the variable
span: Span, // The position of the variable in the template
}
```
##### `DjangoFilter`
Represents a filter applied to a variable.
```rust
pub struct DjangoFilter {
pub name: String, // Name of the filter
pub args: Vec<String>, // Arguments passed to the filter
}
```
#### `Node::Block`
Represents Django template tags that may have nested content, assignments, and control flow structures.
```rust
Node::Block(Block)
```
### `Block`
Represents Django template tags that may have nested content, assignments, and control flow structures.
```rust
pub enum Block {
Block {
tag: Tag,
nodes: Vec<Node>,
closing: Option<Box<Block>>, // Contains Block::Closing if present
assignments: Option<Vec<Assignment>>, // Assignments declared within the tag (e.g., `{% with var=value %}`)
},
Branch {
tag: Tag,
nodes: Vec<Node>,
},
Tag {
tag: Tag,
},
Inclusion {
tag: Tag,
template_name: String,
},
Variable {
tag: Tag,
},
Closing {
tag: Tag,
},
}
```
#### `Tag`
Shared structure for all tag-related nodes in `Block`.
```rust
pub struct Tag {
pub name: String, // Name of the tag (e.g., "if", "for", "include")
pub bits: Vec<String>, // Arguments or components of the tag
pub span: Span, // Span covering the entire tag
pub tag_span: Span, // Span covering just the tag declaration (`{% tag ... %}`)
pub assignment: Option<String>, // Optional assignment target variable name
}
```
#### `Assignment`
Represents an assignment within a tag (e.g., `{% with var=value %}` or `{% url 'some-view' as assigned_url %}`).
```rust
pub struct Assignment {
pub target: String, // Variable name to assign to
pub value: String, // Value assigned to the variable
}
```
#### Variants
##### `Block::Block`
Represents standard block tags that may contain child nodes and require a closing tag.
```rust
Block::Block {
tag: Tag, // The opening Tag of the block
nodes: Vec<Node>, // Nodes contained within the block
closing: Option<Box<Block>>, // Contains Block::Closing if present
assignments: Option<Vec<Assignment>>, // Assignments declared within the tag
}
```
Examples:
- `{% if %}...{% endif %}`
- `{% for %}...{% endfor %}`
- `{% with %}...{% endwith %}`
##### `Block::Branch`
Represents branch tags that are part of control flow structures and contain child nodes.
```rust
Block::Branch {
tag: Tag, // The Tag of the branch
nodes: Vec<Node>, // Nodes contained within the branch
}
```
Examples:
- `{% elif %}`
- `{% else %}`
- `{% empty %}`
##### `Block::Tag`
Represents standalone tags that do not contain child nodes or require a closing tag.
```rust
Block::Tag {
tag: Tag, // The Tag of the standalone tag
}
```
Examples:
- `{% csrf_token %}`
- `{% load %}`
- `{% now "Y-m-d" %}`
##### `Block::Inclusion`
Represents tags that include or extend templates.
```rust
Block::Inclusion {
tag: Tag, // The Tag of the inclusion tag
template_name: String, // Name of the template being included/extended
}
```
Examples:
- `{% include "template.html" %}`
- `{% extends "base.html" %}`
##### `Block::Variable`
Represents tags that output a value directly.
```rust
Block::Variable {
tag: Tag, // The Tag of the variable tag
}
```
Examples:
- `{% cycle %}`
- `{% firstof %}`
##### `Block::Closing`
Represents closing tags corresponding to opening block tags.
```rust
Block::Closing {
tag: Tag, // The Tag of the closing tag
}
```
Examples:
- `{% endif %}`
- `{% endfor %}`
- `{% endwith %}`
## TagSpecs
### Schema
Tag Specifications (TagSpecs) define how tags are parsed and understood. They allow the parser to handle custom tags without hard-coding them.
```toml
[tag_name]
type = "block" | "branch" | "tag" | "inclusion" | "variable"
closing = "closing_tag_name" # For block tags that require a closing tag
supports_assignment = true | false # Whether the tag supports 'as' assignment
branches = ["branch_tag_name", ...] # For block tags that support branches
[[tag_name.args]]
name = "argument_name"
required = true | false
```
### Configuration
- **Built-in TagSpecs**: The parser includes TagSpecs for Django's built-in tags and popular third-party tags.
- **User-defined TagSpecs**: Users can expand or override TagSpecs via `pyproject.toml` or `djls.toml` files in their project, allowing custom tags and configurations to be seamlessly integrated.
### Examples
#### If Tag
```toml
[if]
type = "block"
closing = "endif"
supports_assignment = false
branches = ["elif", "else"]
[[if.args]]
name = "condition"
required = true
position = 0
```
#### Include Tag
```toml
[include]
type = "inclusion"
supports_assignment = true
[[include.args]]
name = "template_name"
required = true
position = 0
```
#### Custom Tag Example
```toml
[my_custom_tag]
type = "tag"
supports_assignment = true
[[my_custom_tag.args]]
name = "arg1"
required = false
position = 0
```
### AST Examples
#### Standard Block with Branches
Template:
```django
{% if user.is_authenticated %}
Hello, {{ user.name }}!
{% elif user.is_guest %}
Welcome, guest!
{% else %}
Please log in.
{% endif %}
```
AST Representation:
```rust
Node::Block(Block::Block {
tag: Tag {
name: "if".to_string(),
bits: vec!["user.is_authenticated".to_string()],
span: Span { start: 0, length: 35 },
tag_span: Span { start: 0, length: 28 },
assignment: None,
},
nodes: vec![
Node::Text {
content: " Hello, ".to_string(),
span: Span { start: 35, length: 12 },
},
Node::Variable {
bits: vec!["user".to_string(), "name".to_string()],
filters: vec![],
span: Span { start: 47, length: 13 },
},
Node::Text {
content: "!\n".to_string(),
span: Span { start: 60, length: 2 },
},
Node::Block(Block::Branch {
tag: Tag {
name: "elif".to_string(),
bits: vec!["user.is_guest".to_string()],
span: Span { start: 62, length: 32 },
tag_span: Span { start: 62, length: 26 },
assignment: None,
},
nodes: vec![
Node::Text {
content: " Welcome, guest!\n".to_string(),
span: Span { start: 94, length: 22 },
},
],
}),
Node::Block(Block::Branch {
tag: Tag {
name: "else".to_string(),
bits: vec![],
span: Span { start: 116, length: 22 },
tag_span: Span { start: 116, length: 16 },
assignment: None,
},
nodes: vec![
Node::Text {
content: " Please log in.\n".to_string(),
span: Span { start: 138, length: 21 },
},
],
}),
],
closing: Some(Box::new(Block::Closing {
tag: Tag {
name: "endif".to_string(),
bits: vec![],
span: Span { start: 159, length: 9 },
tag_span: Span { start: 159, length: 9 },
assignment: None,
},
})),
assignments: None,
})
```
#### Inclusion Tag with Assignment
Template:
```django
{% include "header.html" as header_content %}
```
AST Representation:
```rust
Node::Block(Block::Inclusion {
tag: Tag {
name: "include".to_string(),
bits: vec!["\"header.html\"".to_string()],
span: Span { start: 0, length: 45 },
tag_span: Span { start: 0, length: 45 },
assignment: Some("header_content".to_string()),
},
template_name: "header.html".to_string(),
})
```
#### Variable Tag
Template:
```django
{% cycle 'odd' 'even' %}
```
AST Representation:
```rust
Node::Block(Block::Variable {
tag: Tag {
name: "cycle".to_string(),
bits: vec!["'odd'".to_string(), "'even'".to_string()],
span: Span { start: 0, length: 24 },
tag_span: Span { start: 0, length: 24 },
assignment: None,
},
})
```
## LSP Support
The AST design supports integration with Language Server Protocol (LSP) features:
- **Diagnostics**:
- Detect unclosed or mismatched tags.
- Identify invalid arguments or unknown tags/filters.
- Highlight syntax errors with precise location information.
- **Code Navigation**:
- Go to definitions of variables, tags, and included templates.
- Find references and usages of variables and blocks.
- Provide an outline of the template structure.
- **Code Completion**:
- Suggest tags, filters, and variables in context.
- Auto-complete tag names and attributes based on TagSpecs.
- **Hover Information**:
- Display documentation and usage information for tags and filters.
- Show variable types and values in context.
- **Refactoring Tools**:
- Support renaming of variables and blocks.
- Assist in extracting or inlining templates.
- Provide code actions for common refactoring tasks.

View file

@ -5,7 +5,6 @@ use thiserror::Error;
pub struct Ast { pub struct Ast {
nodes: Vec<Node>, nodes: Vec<Node>,
line_offsets: LineOffsets, line_offsets: LineOffsets,
errors: Vec<AstError>,
} }
impl Ast { impl Ast {
@ -17,10 +16,6 @@ impl Ast {
&self.line_offsets &self.line_offsets
} }
pub fn errors(&self) -> &Vec<AstError> {
&self.errors
}
pub fn add_node(&mut self, node: Node) { pub fn add_node(&mut self, node: Node) {
self.nodes.push(node); self.nodes.push(node);
} }
@ -29,12 +24,8 @@ impl Ast {
self.line_offsets = line_offsets self.line_offsets = line_offsets
} }
pub fn add_error(&mut self, error: AstError) {
self.errors.push(error);
}
pub fn finalize(&mut self) -> Result<Ast, AstError> { pub fn finalize(&mut self) -> Result<Ast, AstError> {
if self.nodes.is_empty() && self.errors.is_empty() { if self.nodes.is_empty() {
return Err(AstError::EmptyAst); return Err(AstError::EmptyAst);
} }
Ok(self.clone()) Ok(self.clone())
@ -55,8 +46,6 @@ impl LineOffsets {
} }
pub fn position_to_line_col(&self, offset: u32) -> (u32, u32) { pub fn position_to_line_col(&self, offset: u32) -> (u32, u32) {
eprintln!("LineOffsets: Converting position {} to line/col. Offsets: {:?}", offset, self.0);
// Find which line contains this offset by looking for the first line start // Find which line contains this offset by looking for the first line start
// that's greater than our position // that's greater than our position
let line = match self.0.binary_search(&offset) { let line = match self.0.binary_search(&offset) {
@ -79,9 +68,6 @@ impl LineOffsets {
// Calculate column as offset from line start // Calculate column as offset from line start
let col = offset - self.0[line]; let col = offset - self.0[line];
eprintln!("LineOffsets: Found line {} starting at offset {}", line, self.0[line]);
eprintln!("LineOffsets: Calculated col {} as {} - {}", col, offset, self.0[line]);
(line as u32, col) (line as u32, col)
} }
@ -93,11 +79,11 @@ impl LineOffsets {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub struct Span { pub struct Span {
start: u32, start: u32,
length: u16, length: u32,
} }
impl Span { impl Span {
pub fn new(start: u32, length: u16) -> Self { pub fn new(start: u32, length: u32) -> Self {
Self { start, length } Self { start, length }
} }
@ -105,28 +91,21 @@ impl Span {
&self.start &self.start
} }
pub fn length(&self) -> &u16 { pub fn length(&self) -> &u32 {
&self.length &self.length
} }
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub enum Node { pub enum Node {
Text { Block(Block),
content: String,
span: Span,
},
Comment { Comment {
content: String, content: String,
span: Span, span: Span,
}, },
Block { Text {
block_type: BlockType, content: String,
name: String,
bits: Vec<String>,
children: Option<Vec<Node>>,
span: Span, span: Span,
tag_span: Span,
}, },
Variable { Variable {
bits: Vec<String>, bits: Vec<String>,
@ -136,10 +115,45 @@ pub enum Node {
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub enum BlockType { pub enum Block {
Standard, Block {
Branch, tag: Tag,
Closing, nodes: Vec<Node>,
closing: Option<Box<Block>>,
assignments: Option<Vec<Assignment>>,
},
Branch {
tag: Tag,
nodes: Vec<Node>,
},
Tag {
tag: Tag,
},
Inclusion {
tag: Tag,
template_name: String,
},
Variable {
tag: Tag,
},
Closing {
tag: Tag,
},
}
#[derive(Clone, Debug, Serialize)]
pub struct Tag {
pub name: String,
pub bits: Vec<String>,
pub span: Span,
pub tag_span: Span,
pub assignment: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct Assignment {
pub target: String,
pub value: String,
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
@ -271,8 +285,8 @@ mod tests {
} }
} }
// Full block span should cover entire template // Full block span should cover only the opening tag
assert_eq!(*span.length() as u32, 42); assert_eq!(*span.length() as u32, 14);
} }
} }
@ -292,33 +306,27 @@ mod tests {
let ast = parser.parse().unwrap(); let ast = parser.parse().unwrap();
// Test nested block positions // Test nested block positions
let (outer_if, inner_if) = {
let nodes = ast.nodes(); let nodes = ast.nodes();
let outer = nodes let outer_if = nodes
.iter() .iter()
.find(|n| matches!(n, Node::Block { .. })) .find(|n| matches!(n, Node::Block { .. }))
.unwrap(); .unwrap();
let inner = if let Node::Block { children, .. } = outer {
children if let Node::Block {
.as_ref() span: outer_span,
.unwrap() children: Some(children),
..
} = outer_if
{
// Find the inner if block in the children
let inner_if = children
.iter() .iter()
.find(|n| matches!(n, Node::Block { .. })) .find(|n| matches!(n, Node::Block { .. }))
.unwrap() .unwrap();
} else {
panic!("Expected block with children");
};
(outer, inner)
};
if let ( if let Node::Block {
Node::Block {
span: outer_span, ..
},
Node::Block {
span: inner_span, .. span: inner_span, ..
}, } = inner_if
) = (outer_if, inner_if)
{ {
// Verify outer if starts at the right line/column // Verify outer if starts at the right line/column
let (outer_line, outer_col) = let (outer_line, outer_col) =
@ -338,3 +346,4 @@ mod tests {
} }
} }
} }
}

View file

@ -1,5 +1,5 @@
use crate::ast::{Ast, AstError, BlockType, DjangoFilter, LineOffsets, Node, Span}; use crate::ast::{Ast, AstError, BlockType, DjangoFilter, LineOffsets, Node, Span};
use crate::tagspecs::TagSpec; use crate::tagspecs::{TagSpec, TagType};
use crate::tokens::{Token, TokenStream, TokenType}; use crate::tokens::{Token, TokenStream, TokenType};
use thiserror::Error; use thiserror::Error;
@ -147,154 +147,101 @@ impl Parser {
let specs = TagSpec::load_builtin_specs().unwrap_or_default(); let specs = TagSpec::load_builtin_specs().unwrap_or_default();
// Check if this is a closing or branch tag // Get the tag spec if it exists
for (_, spec) in specs.iter() { let tag_spec = specs.get(&tag_name);
if Some(&tag_name) == spec.closing.as_ref()
|| spec // Check if this is a closing tag
.branches if let Some(spec) = tag_spec {
.as_ref() if let Some(closing) = &spec.closing {
.map(|ints| ints.iter().any(|i| i.name == tag_name)) if tag_name == *closing {
.unwrap_or(false) // This is a closing tag, return a signal instead of a node
{ return Err(ParserError::Signal(Signal::ClosingTagFound(tag_name)));
return Err(ParserError::ErrorSignal(Signal::SpecialTag(tag_name))); }
} }
} }
let tag_spec = specs.get(tag_name.as_str()).cloned(); // Check if this is a branch tag
if let Some(spec) = tag_spec {
if let Some(branches) = &spec.branches {
if branches.iter().any(|b| b.name == tag_name) {
let mut children = Vec::new(); let mut children = Vec::new();
let mut current_branch: Option<(String, Vec<String>, Vec<Node>)> = None;
let mut found_closing_tag = false;
while !self.is_at_end() { while !self.is_at_end() {
match self.next_node() { match self.peek()?.token_type() {
Ok(node) => { TokenType::DjangoBlock(next_block) => {
if let Some((_, _, branch_children)) = &mut current_branch { let next_bits: Vec<String> = next_block.split_whitespace().map(String::from).collect();
branch_children.push(node); if let Some(next_tag) = next_bits.first() {
} else { // If we hit another branch or closing tag, we're done
children.push(node); if branches.iter().any(|b| &b.name == next_tag) ||
spec.closing.as_ref().map(|s| s.as_str()) == Some(next_tag.as_str()) {
break;
} }
} }
Err(ParserError::ErrorSignal(Signal::SpecialTag(tag))) => { children.push(self.next_node()?);
if let Some(spec) = &tag_spec { }
// Check if closing tag _ => children.push(self.next_node()?),
if spec.closing.as_deref() == Some(&tag) { }
// If we have a current branch, add it to children }
if let Some((name, bits, branch_children)) = current_branch { return Ok(Node::Block {
let branch_span = Span::new(start_pos, 0); // Removed total_length initialization
children.push(Node::Block {
block_type: BlockType::Branch, block_type: BlockType::Branch,
name,
bits,
children: Some(branch_children),
span: branch_span,
tag_span,
});
}
let closing_token = self.peek_previous()?;
let closing_content = match closing_token.token_type() {
TokenType::DjangoBlock(content) => content.len() + 5, // Add 5 for {% and %}
_ => 0,
};
let closing_start = closing_token.start().unwrap_or(0);
let total_length = (closing_start - start_pos as usize) + closing_content;
let closing_span = Span::new(
closing_start as u32,
closing_content as u16,
);
children.push(Node::Block {
block_type: BlockType::Closing,
name: tag,
bits: vec![],
children: None,
span: closing_span,
tag_span,
});
found_closing_tag = true;
// Set the final span length
let span = Span::new(start_pos, total_length as u16);
let node = Node::Block {
block_type: BlockType::Standard,
name: tag_name, name: tag_name,
bits, bits,
children: Some(children), children: Some(children),
span, span: tag_span,
tag_span,
};
return Ok(node);
}
// Check if intermediate tag
if let Some(branches) = &spec.branches {
if let Some(branch) = branches.iter().find(|b| b.name == tag) {
// If we have a current branch, add it to children
if let Some((name, bits, branch_children)) = current_branch {
let branch_span = Span::new(start_pos, 0); // Removed total_length initialization
children.push(Node::Block {
block_type: BlockType::Branch,
name,
bits,
children: Some(branch_children),
span: branch_span,
tag_span, tag_span,
}); });
} }
// Create new branch node
let branch_bits = if branch.args {
match &self.tokens[self.current - 1].token_type() {
TokenType::DjangoBlock(content) => content
.split_whitespace()
.skip(1) // Skip the tag name
.map(|s| s.to_string())
.collect(),
_ => vec![tag.clone()],
} }
} else { }
vec![]
}; // This is a standard block
current_branch = Some((tag, branch_bits, Vec::new())); let mut children = Vec::new();
let mut found_closing = false;
let mut end_pos = start_pos + s.len() as u32;
while !self.is_at_end() {
match self.peek()?.token_type() {
TokenType::DjangoBlock(next_block) => {
let next_bits: Vec<String> = next_block.split_whitespace().map(String::from).collect();
if let Some(next_tag) = next_bits.first() {
// Check if this is a closing tag
if let Some(spec) = tag_spec {
if let Some(closing) = &spec.closing {
if next_tag == closing {
found_closing = true;
let closing_token = self.consume()?;
end_pos = closing_token.start().unwrap_or(0) as u32 + next_block.len() as u32;
break;
}
}
// Check if this is a branch tag
if let Some(branches) = &spec.branches {
if branches.iter().any(|b| &b.name == next_tag) {
children.push(self.next_node()?);
continue; continue;
} }
} }
} }
// If we get here, it's an unexpected tag
let node = Node::Block {
block_type: BlockType::Standard,
name: tag_name.clone(),
bits: bits.clone(),
children: Some(children.clone()),
span: Span::new(start_pos, 0), // Removed total_length initialization
tag_span,
};
return Err(ParserError::Ast(AstError::UnexpectedTag(tag), Some(node)));
} }
Err(ParserError::Ast(AstError::StreamError(kind), _)) if kind == "AtEnd" => { children.push(self.next_node()?);
break;
} }
Err(e) => return Err(e), _ => children.push(self.next_node()?),
} }
} }
let span = Span::new(start_pos, 0); // Removed total_length initialization if !found_closing && tag_spec.map_or(false, |s| s.closing.is_some()) {
return Err(ParserError::from(
let node = Node::Block { AstError::UnclosedTag(tag_name.clone()),
block_type: BlockType::Standard,
name: tag_name.clone(),
bits,
children: Some(children),
span,
tag_span,
};
if !found_closing_tag {
return Err(ParserError::Ast(
AstError::UnclosedTag(tag_name),
Some(node),
)); ));
} }
Ok(node) Ok(Node::Block {
block_type: BlockType::Standard,
name: tag_name,
bits,
children: Some(children),
span: Span::new(start_pos, (end_pos - start_pos) as u16),
tag_span,
})
} }
fn parse_django_variable(&mut self, s: &str) -> Result<Node, ParserError> { fn parse_django_variable(&mut self, s: &str) -> Result<Node, ParserError> {

View file

@ -17,58 +17,61 @@ nodes:
span: span:
start: 14 start: 14
length: 8 length: 8
span:
start: 0
length: 8
tag_span:
start: 0
length: 8
- Block: - Block:
block_type: Branch block_type: Standalone
name: elif name: elif
bits: bits:
- elif
- x - x
- "<" - "<"
- "0" - "0"
children: children: ~
span:
start: 22
length: 10
tag_span:
start: 22
length: 10
- Text: - Text:
content: Negative content: Negative
span: span:
start: 38 start: 38
length: 8 length: 8
span:
start: 0
length: 0
tag_span:
start: 0
length: 8
- Block: - Block:
block_type: Branch block_type: Standalone
name: else name: else
bits: [] bits:
children: - else
children: ~
span:
start: 46
length: 4
tag_span:
start: 46
length: 4
- Text: - Text:
content: Zero content: Zero
span: span:
start: 56 start: 56
length: 4 length: 4
span:
start: 0
length: 0
tag_span:
start: 0
length: 8
- Block: - Block:
block_type: Closing block_type: Standalone
name: endif name: endif
bits: [] bits:
- endif
children: ~ children: ~
span: span:
start: 60 start: 60
length: 10 length: 5
tag_span: tag_span:
start: 0 start: 60
length: 8 length: 5
span:
start: 0
length: 70
tag_span:
start: 0
length: 8
line_offsets: line_offsets:
- 0 - 0
errors: [] errors: []

View file

@ -0,0 +1,11 @@
---
source: crates/djls-template-ast/src/parser.rs
assertion_line: 527
expression: ast
snapshot_kind: text
---
nodes: []
line_offsets:
- 0
errors:
- UnclosedTag: if

View file

@ -19,39 +19,41 @@ nodes:
span: span:
start: 26 start: 26
length: 4 length: 4
span:
start: 0
length: 17
tag_span:
start: 0
length: 17
- Block: - Block:
block_type: Branch block_type: Standalone
name: empty name: empty
bits: [] bits:
children: - empty
children: ~
span:
start: 33
length: 5
tag_span:
start: 33
length: 5
- Text: - Text:
content: No items content: No items
span: span:
start: 44 start: 44
length: 8 length: 8
span:
start: 0
length: 0
tag_span:
start: 0
length: 17
- Block: - Block:
block_type: Closing block_type: Standalone
name: endfor name: endfor
bits: [] bits:
- endfor
children: ~ children: ~
span: span:
start: 52 start: 52
length: 11 length: 6
tag_span: tag_span:
start: 0 start: 52
length: 17 length: 6
span:
start: 0
length: 63
tag_span:
start: 0
length: 17
line_offsets: line_offsets:
- 0 - 0
errors: [] errors: []

View file

@ -0,0 +1,11 @@
---
source: crates/djls-template-ast/src/parser.rs
assertion_line: 518
expression: ast
snapshot_kind: text
---
nodes: []
line_offsets:
- 0
errors:
- UnclosedTag: for

View file

@ -15,23 +15,24 @@ nodes:
span: span:
start: 30 start: 30
length: 7 length: 7
span:
start: 0
length: 24
tag_span:
start: 0
length: 24
- Block: - Block:
block_type: Closing block_type: Standalone
name: endif name: endif
bits: [] bits:
- endif
children: ~ children: ~
span: span:
start: 37 start: 37
length: 10 length: 5
tag_span: tag_span:
start: 0 start: 37
length: 24 length: 5
span:
start: 0
length: 47
tag_span:
start: 0
length: 24
line_offsets: line_offsets:
- 0 - 0
errors: [] errors: []

View file

@ -0,0 +1,28 @@
---
source: crates/djls-template-ast/src/parser.rs
assertion_line: 510
expression: ast
snapshot_kind: text
---
nodes:
- Block:
block_type: Standard
name: if
bits:
- if
- user.is_authenticated
children:
- Text:
content: Welcome
span:
start: 30
length: 7
span:
start: 0
length: 24
tag_span:
start: 0
length: 24
line_offsets:
- 0
errors: []

View file

@ -44,6 +44,12 @@ nodes:
span: span:
start: 86 start: 86
length: 5 length: 5
span:
start: 9
length: 24
tag_span:
start: 9
length: 24
- Block: - Block:
block_type: Standard block_type: Standard
name: for name: for
@ -58,6 +64,12 @@ nodes:
span: span:
start: 125 start: 125
length: 9 length: 9
span:
start: 86
length: 24
tag_span:
start: 86
length: 24
- Block: - Block:
block_type: Standard block_type: Standard
name: if name: if
@ -70,23 +82,24 @@ nodes:
span: span:
start: 147 start: 147
length: 1 length: 1
span:
start: 125
length: 16
tag_span:
start: 125
length: 16
- Block: - Block:
block_type: Closing block_type: Standalone
name: endif name: endif
bits: [] bits:
- endif
children: ~ children: ~
span: span:
start: 148 start: 148
length: 10 length: 5
tag_span: tag_span:
start: 125 start: 148
length: 16 length: 5
span:
start: 125
length: 33
tag_span:
start: 125
length: 16
- Text: - Text:
content: "\n " content: "\n "
span: span:
@ -118,23 +131,24 @@ nodes:
span: span:
start: 218 start: 218
length: 2 length: 2
span:
start: 193
length: 19
tag_span:
start: 193
length: 19
- Block: - Block:
block_type: Closing block_type: Standalone
name: endif name: endif
bits: [] bits:
- endif
children: ~ children: ~
span: span:
start: 220 start: 220
length: 10 length: 5
tag_span: tag_span:
start: 193 start: 220
length: 19 length: 5
span:
start: 193
length: 37
tag_span:
start: 193
length: 19
- Text: - Text:
content: "\n " content: "\n "
span: span:
@ -152,99 +166,92 @@ nodes:
span: span:
start: 261 start: 261
length: 1 length: 1
span:
start: 240
length: 15
tag_span:
start: 240
length: 15
- Block: - Block:
block_type: Closing block_type: Standalone
name: endif name: endif
bits: [] bits:
- endif
children: ~ children: ~
span: span:
start: 262 start: 262
length: 10 length: 5
tag_span: tag_span:
start: 240 start: 262
length: 15 length: 5
span:
start: 240
length: 32
tag_span:
start: 240
length: 15
- Text: - Text:
content: "\n " content: "\n "
span: span:
start: 278 start: 278
length: 5 length: 5
- Block: - Block:
block_type: Branch block_type: Standalone
name: empty name: empty
bits: [] bits:
children: - empty
children: ~
span:
start: 278
length: 5
tag_span:
start: 278
length: 5
- Text: - Text:
content: "\n (no groups)\n " content: "\n (no groups)\n "
span: span:
start: 298 start: 298
length: 25 length: 25
span:
start: 86
length: 0
tag_span:
start: 86
length: 24
- Block: - Block:
block_type: Closing block_type: Standalone
name: endfor name: endfor
bits: [] bits:
- endfor
children: ~ children: ~
span: span:
start: 314 start: 314
length: 11 length: 6
tag_span: tag_span:
start: 86 start: 314
length: 24 length: 6
span:
start: 86
length: 239
tag_span:
start: 86
length: 24
- Text: - Text:
content: "\n" content: "\n"
span: span:
start: 327 start: 327
length: 1 length: 1
- Block: - Block:
block_type: Branch block_type: Standalone
name: else name: else
bits: [] bits:
children: - else
children: ~
span:
start: 327
length: 4
tag_span:
start: 327
length: 4
- Text: - Text:
content: "\n Guest\n" content: "\n Guest\n"
span: span:
start: 342 start: 342
length: 11 length: 11
span:
start: 9
length: 0
tag_span:
start: 9
length: 24
- Block: - Block:
block_type: Closing block_type: Standalone
name: endif name: endif
bits: [] bits:
- endif
children: ~ children: ~
span: span:
start: 348 start: 348
length: 10 length: 5
tag_span: tag_span:
start: 9 start: 348
length: 24 length: 5
span:
start: 9
length: 349
tag_span:
start: 9
length: 24
- Text: - Text:
content: "!" content: "!"
span: span:

View file

@ -0,0 +1,28 @@
---
source: crates/djls-template-ast/src/parser.rs
assertion_line: 556
expression: ast
snapshot_kind: text
---
nodes:
- Text:
content: "Welcome, "
span:
start: 0
length: 9
line_offsets:
- 0
- 40
- 82
- 117
- 160
- 185
- 232
- 274
- 290
- 310
- 327
- 338
- 348
errors:
- UnclosedTag: for

View file

@ -11,7 +11,13 @@ nodes:
- item - item
- in - in
- items - items
children: children: []
span:
start: 0
length: 17
tag_span:
start: 0
length: 17
- Block: - Block:
block_type: Standard block_type: Standard
name: if name: if
@ -27,40 +33,36 @@ nodes:
span: span:
start: 46 start: 46
length: 9 length: 9
span:
start: 23
length: 14
tag_span:
start: 23
length: 14
- Block: - Block:
block_type: Closing block_type: Standalone
name: endif name: endif
bits: [] bits:
- endif
children: ~ children: ~
span: span:
start: 58 start: 58
length: 10 length: 5
tag_span: tag_span:
start: 23 start: 58
length: 14 length: 5
span:
start: 23
length: 45
tag_span:
start: 23
length: 14
- Block: - Block:
block_type: Closing block_type: Standalone
name: endfor name: endfor
bits: [] bits:
- endfor
children: ~ children: ~
span: span:
start: 69 start: 69
length: 11 length: 6
tag_span: tag_span:
start: 0 start: 69
length: 17 length: 6
span:
start: 0
length: 80
tag_span:
start: 0
length: 17
line_offsets: line_offsets:
- 0 - 0
errors: [] errors: []

View file

@ -0,0 +1,46 @@
---
source: crates/djls-template-ast/src/parser.rs
assertion_line: 536
expression: ast
snapshot_kind: text
---
nodes:
- Block:
block_type: Standard
name: for
bits:
- for
- item
- in
- items
children:
- Block:
block_type: Standard
name: if
bits:
- if
- item.active
children:
- Variable:
bits:
- item
- name
filters: []
span:
start: 46
length: 9
span:
start: 23
length: 14
tag_span:
start: 23
length: 14
span:
start: 0
length: 17
tag_span:
start: 0
length: 17
line_offsets:
- 0
errors: []

View file

@ -53,6 +53,12 @@ nodes:
span: span:
start: 252 start: 252
length: 9 length: 9
span:
start: 48
length: 24
tag_span:
start: 48
length: 24
- Block: - Block:
block_type: Standard block_type: Standard
name: for name: for
@ -79,34 +85,29 @@ nodes:
span: span:
start: 304 start: 304
length: 16 length: 16
span:
start: 252
length: 17
tag_span:
start: 252
length: 17
- Block: - Block:
block_type: Closing block_type: Standalone
name: endfor name: endfor
bits: [] bits:
- endfor
children: ~ children: ~
span: span:
start: 320 start: 320
length: 11 length: 6
tag_span: tag_span:
start: 252 start: 320
length: 17 length: 6
span:
start: 252
length: 79
tag_span:
start: 252
length: 17
- Text: - Text:
content: "\n <footer>Page Footer</footer>\n</div>" content: "\n <footer>Page Footer</footer>\n</div>"
span: span:
start: 337 start: 337
length: 40 length: 40
span:
start: 48
length: 0
tag_span:
start: 48
length: 24
line_offsets: line_offsets:
- 0 - 0
- 24 - 24
@ -120,5 +121,4 @@ line_offsets:
- 312 - 312
- 333 - 333
- 366 - 366
errors: errors: []
- UnclosedTag: if

View file

@ -0,0 +1,27 @@
---
source: crates/djls-template-ast/src/parser.rs
assertion_line: 669
expression: ast
snapshot_kind: text
---
nodes:
- Text:
content: "<div class=\"container\">\n <h1>Header</h1>\n "
span:
start: 0
length: 48
line_offsets:
- 0
- 24
- 44
- 79
- 131
- 170
- 184
- 244
- 276
- 312
- 333
- 366
errors:
- UnclosedTag: if

View file

@ -22,11 +22,10 @@ nodes:
length: 9 length: 9
span: span:
start: 0 start: 0
length: 0 length: 17
tag_span: tag_span:
start: 0 start: 0
length: 17 length: 17
line_offsets: line_offsets:
- 0 - 0
errors: errors: []
- UnclosedTag: for

View file

@ -0,0 +1,11 @@
---
source: crates/djls-template-ast/src/parser.rs
assertion_line: 630
expression: ast
snapshot_kind: text
---
nodes: []
line_offsets:
- 0
errors:
- UnclosedTag: for

View file

@ -17,11 +17,10 @@ nodes:
length: 7 length: 7
span: span:
start: 0 start: 0
length: 0 length: 24
tag_span: tag_span:
start: 0 start: 0
length: 24 length: 24
line_offsets: line_offsets:
- 0 - 0
errors: errors: []
- UnclosedTag: if

View file

@ -0,0 +1,11 @@
---
source: crates/djls-template-ast/src/parser.rs
assertion_line: 620
expression: ast
snapshot_kind: text
---
nodes: []
line_offsets:
- 0
errors:
- UnclosedTag: if

View file

@ -54,6 +54,12 @@ nodes:
span: span:
start: 598 start: 598
length: 23 length: 23
span:
start: 463
length: 24
tag_span:
start: 463
length: 24
- Block: - Block:
block_type: Standard block_type: Standard
name: if name: if
@ -66,61 +72,58 @@ nodes:
span: span:
start: 664 start: 664
length: 56 length: 56
span:
start: 621
length: 16
tag_span:
start: 621
length: 16
- Block: - Block:
block_type: Branch block_type: Standalone
name: else name: else
bits: [] bits:
children: - else
children: ~
span:
start: 699
length: 4
tag_span:
start: 699
length: 4
- Text: - Text:
content: "\n <span>User</span>\n " content: "\n <span>User</span>\n "
span: span:
start: 730 start: 730
length: 55 length: 55
span:
start: 621
length: 0
tag_span:
start: 621
length: 16
- Block: - Block:
block_type: Closing block_type: Standalone
name: endif name: endif
bits: [] bits:
- endif
children: ~ children: ~
span: span:
start: 764 start: 764
length: 10 length: 5
tag_span: tag_span:
start: 621 start: 764
length: 16 length: 5
span:
start: 621
length: 153
tag_span:
start: 621
length: 16
- Text: - Text:
content: "\n " content: "\n "
span: span:
start: 788 start: 788
length: 13 length: 13
- Block: - Block:
block_type: Closing block_type: Standalone
name: endif name: endif
bits: [] bits:
- endif
children: ~ children: ~
span: span:
start: 788 start: 788
length: 10 length: 5
tag_span: tag_span:
start: 463 start: 788
length: 24 length: 5
span:
start: 463
length: 335
tag_span:
start: 463
length: 24
- Text: - Text:
content: "\n </div>\n </body>\n</html>" content: "\n </div>\n </body>\n</html>"
span: span:

View file

@ -0,0 +1,45 @@
---
source: crates/djls-template-ast/src/parser.rs
assertion_line: 712
expression: ast
snapshot_kind: text
---
nodes:
- Text:
content: "<!DOCTYPE html>\n<html>\n <head>\n <style type=\"text/css\">\n /* Style header */\n .header { color: blue; }\n </style>\n <script type=\"text/javascript\">\n // Init app\n const app = {\n /* Config */\n debug: true\n };\n </script>\n </head>\n <body>\n <!-- Header section -->\n <div class=\"header\" id=\"main\" data-value=\"123\" disabled>\n "
span:
start: 0
length: 463
line_offsets:
- 0
- 16
- 23
- 34
- 66
- 97
- 134
- 151
- 191
- 215
- 241
- 270
- 298
- 313
- 331
- 343
- 354
- 386
- 451
- 494
- 532
- 605
- 644
- 683
- 710
- 748
- 776
- 800
- 815
- 827
errors:
- UnclosedTag: if