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 {
nodes: Vec<Node>,
line_offsets: LineOffsets,
errors: Vec<AstError>,
}
impl Ast {
@ -17,10 +16,6 @@ impl Ast {
&self.line_offsets
}
pub fn errors(&self) -> &Vec<AstError> {
&self.errors
}
pub fn add_node(&mut self, node: Node) {
self.nodes.push(node);
}
@ -29,12 +24,8 @@ impl Ast {
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> {
if self.nodes.is_empty() && self.errors.is_empty() {
if self.nodes.is_empty() {
return Err(AstError::EmptyAst);
}
Ok(self.clone())
@ -55,15 +46,13 @@ impl LineOffsets {
}
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
// that's greater than our position
let line = match self.0.binary_search(&offset) {
Ok(exact_line) => exact_line, // We're exactly at a line start, so we're on that line
Ok(exact_line) => exact_line, // We're exactly at a line start, so we're on that line
Err(next_line) => {
if next_line == 0 {
0 // Before first line start, so we're on line 0
0 // Before first line start, so we're on line 0
} else {
let prev_line = next_line - 1;
// If we're at the start of the next line, we're on that line
@ -79,9 +68,6 @@ impl LineOffsets {
// Calculate column as offset from line start
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)
}
@ -93,11 +79,11 @@ impl LineOffsets {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub struct Span {
start: u32,
length: u16,
length: u32,
}
impl Span {
pub fn new(start: u32, length: u16) -> Self {
pub fn new(start: u32, length: u32) -> Self {
Self { start, length }
}
@ -105,28 +91,21 @@ impl Span {
&self.start
}
pub fn length(&self) -> &u16 {
pub fn length(&self) -> &u32 {
&self.length
}
}
#[derive(Clone, Debug, Serialize)]
pub enum Node {
Text {
content: String,
span: Span,
},
Block(Block),
Comment {
content: String,
span: Span,
},
Block {
block_type: BlockType,
name: String,
bits: Vec<String>,
children: Option<Vec<Node>>,
Text {
content: String,
span: Span,
tag_span: Span,
},
Variable {
bits: Vec<String>,
@ -136,10 +115,45 @@ pub enum Node {
}
#[derive(Clone, Debug, Serialize)]
pub enum BlockType {
Standard,
Branch,
Closing,
pub enum Block {
Block {
tag: Tag,
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)]
@ -271,8 +285,8 @@ mod tests {
}
}
// Full block span should cover entire template
assert_eq!(*span.length() as u32, 42);
// Full block span should cover only the opening tag
assert_eq!(*span.length() as u32, 14);
}
}
@ -292,48 +306,43 @@ mod tests {
let ast = parser.parse().unwrap();
// Test nested block positions
let (outer_if, inner_if) = {
let nodes = ast.nodes();
let outer = nodes
let nodes = ast.nodes();
let outer_if = nodes
.iter()
.find(|n| matches!(n, Node::Block { .. }))
.unwrap();
if let Node::Block {
span: outer_span,
children: Some(children),
..
} = outer_if
{
// Find the inner if block in the children
let inner_if = children
.iter()
.find(|n| matches!(n, Node::Block { .. }))
.unwrap();
let inner = if let Node::Block { children, .. } = outer {
children
.as_ref()
.unwrap()
.iter()
.find(|n| matches!(n, Node::Block { .. }))
.unwrap()
} else {
panic!("Expected block with children");
};
(outer, inner)
};
if let (
Node::Block {
span: outer_span, ..
},
Node::Block {
if let Node::Block {
span: inner_span, ..
},
) = (outer_if, inner_if)
{
// Verify outer if starts at the right line/column
let (outer_line, outer_col) =
ast.line_offsets.position_to_line_col(*outer_span.start());
assert_eq!(
(outer_line, outer_col),
(1, 4),
"Outer if should be indented"
);
} = inner_if
{
// Verify outer if starts at the right line/column
let (outer_line, outer_col) =
ast.line_offsets.position_to_line_col(*outer_span.start());
assert_eq!(
(outer_line, outer_col),
(1, 4),
"Outer if should be indented"
);
// Verify inner if is more indented than outer if
let (inner_line, inner_col) =
ast.line_offsets.position_to_line_col(*inner_span.start());
assert!(inner_col > outer_col, "Inner if should be more indented");
assert!(inner_line > outer_line, "Inner if should be on later line");
// Verify inner if is more indented than outer if
let (inner_line, inner_col) =
ast.line_offsets.position_to_line_col(*inner_span.start());
assert!(inner_col > outer_col, "Inner if should be more indented");
assert!(inner_line > outer_line, "Inner if should be on later line");
}
}
}
}

View file

@ -1,5 +1,5 @@
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 thiserror::Error;
@ -146,155 +146,102 @@ impl Parser {
let tag_name = bits.first().ok_or(AstError::EmptyTag)?.clone();
let specs = TagSpec::load_builtin_specs().unwrap_or_default();
// Get the tag spec if it exists
let tag_spec = specs.get(&tag_name);
// Check if this is a closing or branch tag
for (_, spec) in specs.iter() {
if Some(&tag_name) == spec.closing.as_ref()
|| spec
.branches
.as_ref()
.map(|ints| ints.iter().any(|i| i.name == tag_name))
.unwrap_or(false)
{
return Err(ParserError::ErrorSignal(Signal::SpecialTag(tag_name)));
// Check if this is a closing tag
if let Some(spec) = tag_spec {
if let Some(closing) = &spec.closing {
if tag_name == *closing {
// This is a closing tag, return a signal instead of a node
return Err(ParserError::Signal(Signal::ClosingTagFound(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();
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() {
// If we hit another branch or closing tag, we're done
if branches.iter().any(|b| &b.name == next_tag) ||
spec.closing.as_ref().map(|s| s.as_str()) == Some(next_tag.as_str()) {
break;
}
}
children.push(self.next_node()?);
}
_ => children.push(self.next_node()?),
}
}
return Ok(Node::Block {
block_type: BlockType::Branch,
name: tag_name,
bits,
children: Some(children),
span: tag_span,
tag_span,
});
}
}
}
// This is a standard block
let mut children = Vec::new();
let mut current_branch: Option<(String, Vec<String>, Vec<Node>)> = None;
let mut found_closing_tag = false;
let mut found_closing = false;
let mut end_pos = start_pos + s.len() as u32;
while !self.is_at_end() {
match self.next_node() {
Ok(node) => {
if let Some((_, _, branch_children)) = &mut current_branch {
branch_children.push(node);
} else {
children.push(node);
}
}
Err(ParserError::ErrorSignal(Signal::SpecialTag(tag))) => {
if let Some(spec) = &tag_spec {
// Check if closing tag
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 {
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,
});
}
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,
bits,
children: Some(children),
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,
});
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;
}
// 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![]
};
current_branch = Some((tag, branch_bits, Vec::new()));
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)));
children.push(self.next_node()?);
}
Err(ParserError::Ast(AstError::StreamError(kind), _)) if kind == "AtEnd" => {
break;
}
Err(e) => return Err(e),
_ => children.push(self.next_node()?),
}
}
let span = Span::new(start_pos, 0); // Removed total_length initialization
let node = Node::Block {
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),
if !found_closing && tag_spec.map_or(false, |s| s.closing.is_some()) {
return Err(ParserError::from(
AstError::UnclosedTag(tag_name.clone()),
));
}
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> {

View file

@ -17,58 +17,61 @@ nodes:
span:
start: 14
length: 8
- Block:
block_type: Branch
name: elif
bits:
- x
- "<"
- "0"
children:
- Text:
content: Negative
span:
start: 38
length: 8
span:
start: 0
length: 0
tag_span:
start: 0
length: 8
- Block:
block_type: Branch
name: else
bits: []
children:
- Text:
content: Zero
span:
start: 56
length: 4
span:
start: 0
length: 0
tag_span:
start: 0
length: 8
- Block:
block_type: Closing
name: endif
bits: []
children: ~
span:
start: 60
length: 10
tag_span:
start: 0
length: 8
span:
start: 0
length: 70
length: 8
tag_span:
start: 0
length: 8
- Block:
block_type: Standalone
name: elif
bits:
- elif
- x
- "<"
- "0"
children: ~
span:
start: 22
length: 10
tag_span:
start: 22
length: 10
- Text:
content: Negative
span:
start: 38
length: 8
- Block:
block_type: Standalone
name: else
bits:
- else
children: ~
span:
start: 46
length: 4
tag_span:
start: 46
length: 4
- Text:
content: Zero
span:
start: 56
length: 4
- Block:
block_type: Standalone
name: endif
bits:
- endif
children: ~
span:
start: 60
length: 5
tag_span:
start: 60
length: 5
line_offsets:
- 0
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:
start: 26
length: 4
- Block:
block_type: Branch
name: empty
bits: []
children:
- Text:
content: No items
span:
start: 44
length: 8
span:
start: 0
length: 0
tag_span:
start: 0
length: 17
- Block:
block_type: Closing
name: endfor
bits: []
children: ~
span:
start: 52
length: 11
tag_span:
start: 0
length: 17
span:
start: 0
length: 63
length: 17
tag_span:
start: 0
length: 17
- Block:
block_type: Standalone
name: empty
bits:
- empty
children: ~
span:
start: 33
length: 5
tag_span:
start: 33
length: 5
- Text:
content: No items
span:
start: 44
length: 8
- Block:
block_type: Standalone
name: endfor
bits:
- endfor
children: ~
span:
start: 52
length: 6
tag_span:
start: 52
length: 6
line_offsets:
- 0
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:
start: 30
length: 7
- Block:
block_type: Closing
name: endif
bits: []
children: ~
span:
start: 37
length: 10
tag_span:
start: 0
length: 24
span:
start: 0
length: 47
length: 24
tag_span:
start: 0
length: 24
- Block:
block_type: Standalone
name: endif
bits:
- endif
children: ~
span:
start: 37
length: 5
tag_span:
start: 37
length: 5
line_offsets:
- 0
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,207 +44,214 @@ nodes:
span:
start: 86
length: 5
- Block:
block_type: Standard
name: for
bits:
- for
- group
- in
- user.groups
children:
- Text:
content: "\n "
span:
start: 125
length: 9
- Block:
block_type: Standard
name: if
bits:
- if
- forloop.first
children:
- Text:
content: (
span:
start: 147
length: 1
- Block:
block_type: Closing
name: endif
bits: []
children: ~
span:
start: 148
length: 10
tag_span:
start: 125
length: 16
span:
start: 125
length: 33
tag_span:
start: 125
length: 16
- Text:
content: "\n "
span:
start: 168
length: 9
- Variable:
bits:
- group
- name
filters: []
span:
start: 171
length: 10
- Text:
content: "\n "
span:
start: 193
length: 9
- Block:
block_type: Standard
name: if
bits:
- if
- not
- forloop.last
children:
- Text:
content: ", "
span:
start: 218
length: 2
- Block:
block_type: Closing
name: endif
bits: []
children: ~
span:
start: 220
length: 10
tag_span:
start: 193
length: 19
span:
start: 193
length: 37
tag_span:
start: 193
length: 19
- Text:
content: "\n "
span:
start: 240
length: 9
- Block:
block_type: Standard
name: if
bits:
- if
- forloop.last
children:
- Text:
content: )
span:
start: 261
length: 1
- Block:
block_type: Closing
name: endif
bits: []
children: ~
span:
start: 262
length: 10
tag_span:
start: 240
length: 15
span:
start: 240
length: 32
tag_span:
start: 240
length: 15
- Text:
content: "\n "
span:
start: 278
length: 5
- Block:
block_type: Branch
name: empty
bits: []
children:
- Text:
content: "\n (no groups)\n "
span:
start: 298
length: 25
span:
start: 86
length: 0
tag_span:
start: 86
length: 24
- Block:
block_type: Closing
name: endfor
bits: []
children: ~
span:
start: 314
length: 11
tag_span:
start: 86
length: 24
span:
start: 86
length: 239
tag_span:
start: 86
length: 24
- Text:
content: "\n"
span:
start: 327
length: 1
- Block:
block_type: Branch
name: else
bits: []
children:
- Text:
content: "\n Guest\n"
span:
start: 342
length: 11
span:
start: 9
length: 0
tag_span:
start: 9
length: 24
- Block:
block_type: Closing
name: endif
bits: []
children: ~
span:
start: 348
length: 10
tag_span:
start: 9
length: 24
span:
start: 9
length: 349
length: 24
tag_span:
start: 9
length: 24
- Block:
block_type: Standard
name: for
bits:
- for
- group
- in
- user.groups
children:
- Text:
content: "\n "
span:
start: 125
length: 9
span:
start: 86
length: 24
tag_span:
start: 86
length: 24
- Block:
block_type: Standard
name: if
bits:
- if
- forloop.first
children:
- Text:
content: (
span:
start: 147
length: 1
span:
start: 125
length: 16
tag_span:
start: 125
length: 16
- Block:
block_type: Standalone
name: endif
bits:
- endif
children: ~
span:
start: 148
length: 5
tag_span:
start: 148
length: 5
- Text:
content: "\n "
span:
start: 168
length: 9
- Variable:
bits:
- group
- name
filters: []
span:
start: 171
length: 10
- Text:
content: "\n "
span:
start: 193
length: 9
- Block:
block_type: Standard
name: if
bits:
- if
- not
- forloop.last
children:
- Text:
content: ", "
span:
start: 218
length: 2
span:
start: 193
length: 19
tag_span:
start: 193
length: 19
- Block:
block_type: Standalone
name: endif
bits:
- endif
children: ~
span:
start: 220
length: 5
tag_span:
start: 220
length: 5
- Text:
content: "\n "
span:
start: 240
length: 9
- Block:
block_type: Standard
name: if
bits:
- if
- forloop.last
children:
- Text:
content: )
span:
start: 261
length: 1
span:
start: 240
length: 15
tag_span:
start: 240
length: 15
- Block:
block_type: Standalone
name: endif
bits:
- endif
children: ~
span:
start: 262
length: 5
tag_span:
start: 262
length: 5
- Text:
content: "\n "
span:
start: 278
length: 5
- Block:
block_type: Standalone
name: empty
bits:
- empty
children: ~
span:
start: 278
length: 5
tag_span:
start: 278
length: 5
- Text:
content: "\n (no groups)\n "
span:
start: 298
length: 25
- Block:
block_type: Standalone
name: endfor
bits:
- endfor
children: ~
span:
start: 314
length: 6
tag_span:
start: 314
length: 6
- Text:
content: "\n"
span:
start: 327
length: 1
- Block:
block_type: Standalone
name: else
bits:
- else
children: ~
span:
start: 327
length: 4
tag_span:
start: 327
length: 4
- Text:
content: "\n Guest\n"
span:
start: 342
length: 11
- Block:
block_type: Standalone
name: endif
bits:
- endif
children: ~
span:
start: 348
length: 5
tag_span:
start: 348
length: 5
- Text:
content: "!"
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,56 +11,58 @@ nodes:
- 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
- Block:
block_type: Closing
name: endif
bits: []
children: ~
span:
start: 58
length: 10
tag_span:
start: 23
length: 14
span:
start: 23
length: 45
tag_span:
start: 23
length: 14
- Block:
block_type: Closing
name: endfor
bits: []
children: ~
span:
start: 69
length: 11
tag_span:
start: 0
length: 17
children: []
span:
start: 0
length: 80
length: 17
tag_span:
start: 0
length: 17
- 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
- Block:
block_type: Standalone
name: endif
bits:
- endif
children: ~
span:
start: 58
length: 5
tag_span:
start: 58
length: 5
- Block:
block_type: Standalone
name: endfor
bits:
- endfor
children: ~
span:
start: 69
length: 6
tag_span:
start: 69
length: 6
line_offsets:
- 0
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,60 +53,61 @@ nodes:
span:
start: 252
length: 9
- Block:
block_type: Standard
name: for
bits:
- for
- item
- in
- items
children:
- Text:
content: "\n <span>"
span:
start: 288
length: 19
- Variable:
bits:
- item
filters: []
span:
start: 297
length: 4
- Text:
content: "</span>\n "
span:
start: 304
length: 16
- Block:
block_type: Closing
name: endfor
bits: []
children: ~
span:
start: 320
length: 11
tag_span:
start: 252
length: 17
span:
start: 252
length: 79
tag_span:
start: 252
length: 17
- Text:
content: "\n <footer>Page Footer</footer>\n</div>"
span:
start: 337
length: 40
span:
start: 48
length: 0
length: 24
tag_span:
start: 48
length: 24
- Block:
block_type: Standard
name: for
bits:
- for
- item
- in
- items
children:
- Text:
content: "\n <span>"
span:
start: 288
length: 19
- Variable:
bits:
- item
filters: []
span:
start: 297
length: 4
- Text:
content: "</span>\n "
span:
start: 304
length: 16
span:
start: 252
length: 17
tag_span:
start: 252
length: 17
- Block:
block_type: Standalone
name: endfor
bits:
- endfor
children: ~
span:
start: 320
length: 6
tag_span:
start: 320
length: 6
- Text:
content: "\n <footer>Page Footer</footer>\n</div>"
span:
start: 337
length: 40
line_offsets:
- 0
- 24
@ -120,5 +121,4 @@ line_offsets:
- 312
- 333
- 366
errors:
- UnclosedTag: if
errors: []

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
span:
start: 0
length: 0
length: 17
tag_span:
start: 0
length: 17
line_offsets:
- 0
errors:
- UnclosedTag: for
errors: []

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
span:
start: 0
length: 0
length: 24
tag_span:
start: 0
length: 24
line_offsets:
- 0
errors:
- UnclosedTag: if
errors: []

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,73 +54,76 @@ nodes:
span:
start: 598
length: 23
- Block:
block_type: Standard
name: if
bits:
- if
- user.is_staff
children:
- Text:
content: "\n <span>Admin</span>\n "
span:
start: 664
length: 56
- Block:
block_type: Branch
name: else
bits: []
children:
- Text:
content: "\n <span>User</span>\n "
span:
start: 730
length: 55
span:
start: 621
length: 0
tag_span:
start: 621
length: 16
- Block:
block_type: Closing
name: endif
bits: []
children: ~
span:
start: 764
length: 10
tag_span:
start: 621
length: 16
span:
start: 621
length: 153
tag_span:
start: 621
length: 16
- Text:
content: "\n "
span:
start: 788
length: 13
- Block:
block_type: Closing
name: endif
bits: []
children: ~
span:
start: 788
length: 10
tag_span:
start: 463
length: 24
span:
start: 463
length: 335
length: 24
tag_span:
start: 463
length: 24
- Block:
block_type: Standard
name: if
bits:
- if
- user.is_staff
children:
- Text:
content: "\n <span>Admin</span>\n "
span:
start: 664
length: 56
span:
start: 621
length: 16
tag_span:
start: 621
length: 16
- Block:
block_type: Standalone
name: else
bits:
- else
children: ~
span:
start: 699
length: 4
tag_span:
start: 699
length: 4
- Text:
content: "\n <span>User</span>\n "
span:
start: 730
length: 55
- Block:
block_type: Standalone
name: endif
bits:
- endif
children: ~
span:
start: 764
length: 5
tag_span:
start: 764
length: 5
- Text:
content: "\n "
span:
start: 788
length: 13
- Block:
block_type: Standalone
name: endif
bits:
- endif
children: ~
span:
start: 788
length: 5
tag_span:
start: 788
length: 5
- Text:
content: "\n </div>\n </body>\n</html>"
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