mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-18 16:20:24 +00:00
add TagSpecs and alter Parser (#68)
Some checks failed
lint / pre-commit (push) Waiting to run
test / test (macos-latest) (push) Waiting to run
test / test (ubuntu-latest) (push) Waiting to run
test / test (windows-latest) (push) Waiting to run
release / linux (map[runner:ubuntu-22.04 target:aarch64]) (push) Failing after 7s
release / linux (map[runner:ubuntu-22.04 target:armv7]) (push) Failing after 3s
release / linux (map[runner:ubuntu-22.04 target:ppc64le]) (push) Failing after 2s
release / linux (map[runner:ubuntu-22.04 target:s390x]) (push) Failing after 2s
release / linux (map[runner:ubuntu-22.04 target:x86]) (push) Failing after 3s
release / linux (map[runner:ubuntu-22.04 target:x86_64]) (push) Failing after 3s
release / musllinux (map[runner:ubuntu-22.04 target:aarch64]) (push) Failing after 3s
release / musllinux (map[runner:ubuntu-22.04 target:armv7]) (push) Failing after 2s
release / musllinux (map[runner:ubuntu-22.04 target:x86]) (push) Failing after 2s
release / musllinux (map[runner:ubuntu-22.04 target:x86_64]) (push) Failing after 2s
release / sdist (push) Has been cancelled
release / windows (map[runner:windows-latest target:x64]) (push) Has been cancelled
release / windows (map[runner:windows-latest target:x86]) (push) Has been cancelled
release / macos (map[runner:macos-13 target:x86_64]) (push) Has been cancelled
release / macos (map[runner:macos-14 target:aarch64]) (push) Has been cancelled
release / release (push) Has been cancelled
Some checks failed
lint / pre-commit (push) Waiting to run
test / test (macos-latest) (push) Waiting to run
test / test (ubuntu-latest) (push) Waiting to run
test / test (windows-latest) (push) Waiting to run
release / linux (map[runner:ubuntu-22.04 target:aarch64]) (push) Failing after 7s
release / linux (map[runner:ubuntu-22.04 target:armv7]) (push) Failing after 3s
release / linux (map[runner:ubuntu-22.04 target:ppc64le]) (push) Failing after 2s
release / linux (map[runner:ubuntu-22.04 target:s390x]) (push) Failing after 2s
release / linux (map[runner:ubuntu-22.04 target:x86]) (push) Failing after 3s
release / linux (map[runner:ubuntu-22.04 target:x86_64]) (push) Failing after 3s
release / musllinux (map[runner:ubuntu-22.04 target:aarch64]) (push) Failing after 3s
release / musllinux (map[runner:ubuntu-22.04 target:armv7]) (push) Failing after 2s
release / musllinux (map[runner:ubuntu-22.04 target:x86]) (push) Failing after 2s
release / musllinux (map[runner:ubuntu-22.04 target:x86_64]) (push) Failing after 2s
release / sdist (push) Has been cancelled
release / windows (map[runner:windows-latest target:x64]) (push) Has been cancelled
release / windows (map[runner:windows-latest target:x86]) (push) Has been cancelled
release / macos (map[runner:macos-13 target:x86_64]) (push) Has been cancelled
release / macos (map[runner:macos-14 target:aarch64]) (push) Has been cancelled
release / release (push) Has been cancelled
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
e59f601596
commit
42d089dcc6
51 changed files with 2337 additions and 1127 deletions
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
|
@ -14,7 +14,6 @@ env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
FORCE_COLOR: "1"
|
FORCE_COLOR: "1"
|
||||||
PYTHONUNBUFFERED: "1"
|
PYTHONUNBUFFERED: "1"
|
||||||
UV_VERSION: "0.4.x"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
@ -33,7 +32,7 @@ jobs:
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@v5
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
version: ${{ env.UV_VERSION }}
|
pyproject-file: pyproject.toml
|
||||||
|
|
||||||
- name: Install dependencies and build
|
- name: Install dependencies and build
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
imports_granularity = "Item"
|
|
||||||
unstable_features = true
|
|
|
@ -18,6 +18,7 @@ serde_json = "1.0"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
tokio = { version = "1.42", features = ["full"] }
|
tokio = { version = "1.42", features = ["full"] }
|
||||||
tower-lsp = { version = "0.20", features = ["proposed"] }
|
tower-lsp = { version = "0.20", features = ["proposed"] }
|
||||||
|
lsp-types = "0.94"
|
||||||
|
|
||||||
[profile.dev.package]
|
[profile.dev.package]
|
||||||
insta.opt-level = 3
|
insta.opt-level = 3
|
||||||
|
|
|
@ -95,6 +95,7 @@ impl fmt::Display for DjangoProject {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct PythonEnvironment {
|
struct PythonEnvironment {
|
||||||
|
#[allow(dead_code)]
|
||||||
python_path: PathBuf,
|
python_path: PathBuf,
|
||||||
sys_path: Vec<PathBuf>,
|
sys_path: Vec<PathBuf>,
|
||||||
sys_prefix: PathBuf,
|
sys_prefix: PathBuf,
|
||||||
|
|
|
@ -4,8 +4,12 @@ version = "0.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
lsp-types = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
toml = "0.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = { version = "1.41", features = ["yaml"] }
|
insta = { version = "1.42", features = ["yaml"] }
|
||||||
|
tempfile = "3.19"
|
||||||
|
|
|
@ -1,225 +1,207 @@
|
||||||
|
use crate::tokens::Token;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
#[derive(Clone, Default, Debug, Serialize)]
|
||||||
pub struct Ast {
|
pub struct NodeList {
|
||||||
nodes: Vec<Node>,
|
nodes: Vec<Node>,
|
||||||
|
line_offsets: LineOffsets,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ast {
|
impl NodeList {
|
||||||
pub fn nodes(&self) -> &Vec<Node> {
|
pub fn nodes(&self) -> &Vec<Node> {
|
||||||
&self.nodes
|
&self.nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn line_offsets(&self) -> &LineOffsets {
|
||||||
|
&self.line_offsets
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_node(&mut self, node: Node) {
|
pub fn add_node(&mut self, node: Node) {
|
||||||
self.nodes.push(node);
|
self.nodes.push(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finalize(&mut self) -> Result<Ast, AstError> {
|
pub fn set_line_offsets(&mut self, line_offsets: LineOffsets) {
|
||||||
if self.nodes.is_empty() {
|
self.line_offsets = line_offsets
|
||||||
return Err(AstError::EmptyAst);
|
}
|
||||||
}
|
|
||||||
Ok(self.clone())
|
pub fn finalize(&mut self) -> NodeList {
|
||||||
|
self.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Debug, Serialize)]
|
||||||
|
pub struct LineOffsets(pub Vec<u32>);
|
||||||
|
|
||||||
|
impl LineOffsets {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(vec![0])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_line(&mut self, offset: u32) {
|
||||||
|
self.0.push(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate column as offset from line start
|
||||||
|
let col = if line == 0 {
|
||||||
|
position as usize
|
||||||
|
} else {
|
||||||
|
(position - self.0[line]) as usize
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to 1-based line number
|
||||||
|
(line + 1, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn line_col_to_position(&self, line: u32, col: u32) -> u32 {
|
||||||
|
// line is 1-based, so subtract 1 to get the index
|
||||||
|
self.0[(line - 1) as usize] + col
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum Node {
|
pub enum Node {
|
||||||
Django(DjangoNode),
|
|
||||||
Html(HtmlNode),
|
|
||||||
Script(ScriptNode),
|
|
||||||
Style(StyleNode),
|
|
||||||
Text(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub enum DjangoNode {
|
|
||||||
Comment(String),
|
|
||||||
Tag {
|
Tag {
|
||||||
kind: DjangoTagKind,
|
name: String,
|
||||||
bits: Vec<String>,
|
bits: Vec<String>,
|
||||||
children: Vec<Node>,
|
span: Span,
|
||||||
},
|
},
|
||||||
Variable {
|
|
||||||
bits: Vec<String>,
|
|
||||||
filters: Vec<DjangoFilter>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub enum DjangoTagKind {
|
|
||||||
Autoescape,
|
|
||||||
Block,
|
|
||||||
Comment,
|
|
||||||
CsrfToken,
|
|
||||||
Cycle,
|
|
||||||
Debug,
|
|
||||||
Elif,
|
|
||||||
Else,
|
|
||||||
Empty,
|
|
||||||
Extends,
|
|
||||||
Filter,
|
|
||||||
FirstOf,
|
|
||||||
For,
|
|
||||||
If,
|
|
||||||
IfChanged,
|
|
||||||
Include,
|
|
||||||
Load,
|
|
||||||
Lorem,
|
|
||||||
Now,
|
|
||||||
Other(String),
|
|
||||||
Querystring, // 5.1
|
|
||||||
Regroup,
|
|
||||||
ResetCycle,
|
|
||||||
Spaceless,
|
|
||||||
TemplateTag,
|
|
||||||
Url,
|
|
||||||
Verbatim,
|
|
||||||
WidthRatio,
|
|
||||||
With,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DjangoTagKind {
|
|
||||||
const AUTOESCAPE: &'static str = "autoescape";
|
|
||||||
const BLOCK: &'static str = "block";
|
|
||||||
const COMMENT: &'static str = "comment";
|
|
||||||
const CSRF_TOKEN: &'static str = "csrf_token";
|
|
||||||
const CYCLE: &'static str = "cycle";
|
|
||||||
const DEBUG: &'static str = "debug";
|
|
||||||
const ELIF: &'static str = "elif";
|
|
||||||
const ELSE: &'static str = "else";
|
|
||||||
const EMPTY: &'static str = "empty";
|
|
||||||
const EXTENDS: &'static str = "extends";
|
|
||||||
const FILTER: &'static str = "filter";
|
|
||||||
const FIRST_OF: &'static str = "firstof";
|
|
||||||
const FOR: &'static str = "for";
|
|
||||||
const IF: &'static str = "if";
|
|
||||||
const IF_CHANGED: &'static str = "ifchanged";
|
|
||||||
const INCLUDE: &'static str = "include";
|
|
||||||
const LOAD: &'static str = "load";
|
|
||||||
const LOREM: &'static str = "lorem";
|
|
||||||
const NOW: &'static str = "now";
|
|
||||||
const QUERYSTRING: &'static str = "querystring";
|
|
||||||
const REGROUP: &'static str = "regroup";
|
|
||||||
const RESET_CYCLE: &'static str = "resetcycle";
|
|
||||||
const SPACELESS: &'static str = "spaceless";
|
|
||||||
const TEMPLATE_TAG: &'static str = "templatetag";
|
|
||||||
const URL: &'static str = "url";
|
|
||||||
const VERBATIM: &'static str = "verbatim";
|
|
||||||
const WIDTH_RATIO: &'static str = "widthratio";
|
|
||||||
const WITH: &'static str = "with";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for DjangoTagKind {
|
|
||||||
type Err = AstError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
if s.is_empty() {
|
|
||||||
return Err(AstError::EmptyTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
match s {
|
|
||||||
Self::AUTOESCAPE => Ok(Self::Autoescape),
|
|
||||||
Self::BLOCK => Ok(Self::Block),
|
|
||||||
Self::COMMENT => Ok(Self::Comment),
|
|
||||||
Self::CSRF_TOKEN => Ok(Self::CsrfToken),
|
|
||||||
Self::CYCLE => Ok(Self::Cycle),
|
|
||||||
Self::DEBUG => Ok(Self::Debug),
|
|
||||||
Self::ELIF => Ok(Self::Elif),
|
|
||||||
Self::ELSE => Ok(Self::Else),
|
|
||||||
Self::EMPTY => Ok(Self::Empty),
|
|
||||||
Self::EXTENDS => Ok(Self::Extends),
|
|
||||||
Self::FILTER => Ok(Self::Filter),
|
|
||||||
Self::FIRST_OF => Ok(Self::FirstOf),
|
|
||||||
Self::FOR => Ok(Self::For),
|
|
||||||
Self::IF => Ok(Self::If),
|
|
||||||
Self::IF_CHANGED => Ok(Self::IfChanged),
|
|
||||||
Self::INCLUDE => Ok(Self::Include),
|
|
||||||
Self::LOAD => Ok(Self::Load),
|
|
||||||
Self::LOREM => Ok(Self::Lorem),
|
|
||||||
Self::NOW => Ok(Self::Now),
|
|
||||||
Self::QUERYSTRING => Ok(Self::Querystring),
|
|
||||||
Self::REGROUP => Ok(Self::Regroup),
|
|
||||||
Self::RESET_CYCLE => Ok(Self::ResetCycle),
|
|
||||||
Self::SPACELESS => Ok(Self::Spaceless),
|
|
||||||
Self::TEMPLATE_TAG => Ok(Self::TemplateTag),
|
|
||||||
Self::URL => Ok(Self::Url),
|
|
||||||
Self::VERBATIM => Ok(Self::Verbatim),
|
|
||||||
Self::WIDTH_RATIO => Ok(Self::WidthRatio),
|
|
||||||
Self::WITH => Ok(Self::With),
|
|
||||||
other => Ok(Self::Other(other.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub struct DjangoFilter {
|
|
||||||
name: String,
|
|
||||||
arguments: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DjangoFilter {
|
|
||||||
pub fn new(name: String, arguments: Vec<String>) -> Self {
|
|
||||||
Self { name, arguments }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub enum HtmlNode {
|
|
||||||
Comment(String),
|
|
||||||
Doctype(String),
|
|
||||||
Element {
|
|
||||||
tag_name: String,
|
|
||||||
attributes: Attributes,
|
|
||||||
children: Vec<Node>,
|
|
||||||
},
|
|
||||||
Void {
|
|
||||||
tag_name: String,
|
|
||||||
attributes: Attributes,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub enum ScriptNode {
|
|
||||||
Comment {
|
Comment {
|
||||||
content: String,
|
content: String,
|
||||||
kind: ScriptCommentKind,
|
span: Span,
|
||||||
},
|
},
|
||||||
Element {
|
Text {
|
||||||
attributes: Attributes,
|
content: String,
|
||||||
children: Vec<Node>,
|
span: Span,
|
||||||
|
},
|
||||||
|
Variable {
|
||||||
|
var: String,
|
||||||
|
filters: Vec<String>,
|
||||||
|
span: Span,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
||||||
pub enum ScriptCommentKind {
|
pub struct Span {
|
||||||
SingleLine, // //
|
start: u32,
|
||||||
MultiLine, // /* */
|
length: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
impl Span {
|
||||||
pub enum StyleNode {
|
pub fn new(start: u32, length: u32) -> Self {
|
||||||
Comment(String),
|
Self { start, length }
|
||||||
Element {
|
}
|
||||||
attributes: Attributes,
|
|
||||||
children: Vec<Node>,
|
pub fn start(&self) -> &u32 {
|
||||||
},
|
&self.start
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn length(&self) -> &u32 {
|
||||||
|
&self.length
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
impl From<Token> for Span {
|
||||||
pub enum AttributeValue {
|
fn from(token: Token) -> Self {
|
||||||
Value(String),
|
let start = token.start().unwrap_or(0);
|
||||||
Boolean,
|
let length = token.content().len() as u32;
|
||||||
|
Span::new(start, length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Attributes = BTreeMap<String, AttributeValue>;
|
#[derive(Clone, Debug, Error, Serialize)]
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum AstError {
|
pub enum AstError {
|
||||||
#[error("error parsing django tag, recieved empty tag name")]
|
#[error("Empty AST")]
|
||||||
EmptyTag,
|
|
||||||
#[error("empty ast")]
|
|
||||||
EmptyAst,
|
EmptyAst,
|
||||||
|
#[error("Invalid tag '{tag}' structure: {reason}")]
|
||||||
|
InvalidTagStructure {
|
||||||
|
tag: String,
|
||||||
|
reason: String,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
#[error("Unbalanced structure: '{opening_tag}' at {opening_span:?} missing closing '{expected_closing}'")]
|
||||||
|
UnbalancedStructure {
|
||||||
|
opening_tag: String,
|
||||||
|
expected_closing: String,
|
||||||
|
opening_span: Span,
|
||||||
|
closing_span: Option<Span>,
|
||||||
|
},
|
||||||
|
#[error("Invalid {node_type} node: {reason}")]
|
||||||
|
InvalidNode {
|
||||||
|
node_type: String,
|
||||||
|
reason: String,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
#[error("Unclosed tag: {0}")]
|
||||||
|
UnclosedTag(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::lexer::Lexer;
|
||||||
|
use crate::parser::Parser;
|
||||||
|
|
||||||
|
mod line_offsets {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_starts_at_zero() {
|
||||||
|
let offsets = LineOffsets::new();
|
||||||
|
assert_eq!(offsets.position_to_line_col(0), (1, 0)); // Line 1, column 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_start_of_lines() {
|
||||||
|
let mut offsets = LineOffsets::new();
|
||||||
|
offsets.add_line(10); // Line 2 starts at offset 10
|
||||||
|
offsets.add_line(25); // Line 3 starts at offset 25
|
||||||
|
|
||||||
|
assert_eq!(offsets.position_to_line_col(0), (1, 0)); // Line 1, start
|
||||||
|
assert_eq!(offsets.position_to_line_col(10), (2, 0)); // Line 2, start
|
||||||
|
assert_eq!(offsets.position_to_line_col(25), (3, 0)); // Line 3, start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod spans_and_positions {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_variable_spans() {
|
||||||
|
let template = "Hello\n{{ user.name }}\nWorld";
|
||||||
|
let tokens = Lexer::new(template).tokenize().unwrap();
|
||||||
|
let mut parser = Parser::new(tokens);
|
||||||
|
let (nodelist, errors) = parser.parse().unwrap();
|
||||||
|
assert!(errors.is_empty());
|
||||||
|
|
||||||
|
// Find the variable node
|
||||||
|
let nodes = nodelist.nodes();
|
||||||
|
let var_node = nodes
|
||||||
|
.iter()
|
||||||
|
.find(|n| matches!(n, Node::Variable { .. }))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let Node::Variable { span, .. } = var_node {
|
||||||
|
// Variable starts after newline + "{{"
|
||||||
|
let (line, col) = nodelist
|
||||||
|
.line_offsets()
|
||||||
|
.position_to_line_col(*span.start() as usize);
|
||||||
|
assert_eq!(
|
||||||
|
(line, col),
|
||||||
|
(2, 0),
|
||||||
|
"Variable should start at line 2, col 3"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(*span.length(), 9, "Variable span should cover 'user.name'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
99
crates/djls-template-ast/src/error.rs
Normal file
99
crates/djls-template-ast/src/error.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
use crate::ast::{AstError, Span};
|
||||||
|
use crate::lexer::LexerError;
|
||||||
|
use crate::parser::ParserError;
|
||||||
|
use lsp_types;
|
||||||
|
use serde::Serialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Serialize)]
|
||||||
|
pub enum TemplateError {
|
||||||
|
#[error("Lexer error: {0}")]
|
||||||
|
Lexer(String),
|
||||||
|
|
||||||
|
#[error("Parser error: {0}")]
|
||||||
|
Parser(String),
|
||||||
|
|
||||||
|
#[error("Validation error: {0}")]
|
||||||
|
Validation(#[from] AstError),
|
||||||
|
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(String),
|
||||||
|
|
||||||
|
#[error("Configuration error: {0}")]
|
||||||
|
Config(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LexerError> for TemplateError {
|
||||||
|
fn from(err: LexerError) -> Self {
|
||||||
|
Self::Lexer(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParserError> for TemplateError {
|
||||||
|
fn from(err: ParserError) -> Self {
|
||||||
|
Self::Parser(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for TemplateError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
Self::Io(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateError {
|
||||||
|
pub fn span(&self) -> Option<Span> {
|
||||||
|
match self {
|
||||||
|
TemplateError::Validation(AstError::InvalidTagStructure { span, .. }) => Some(*span),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn severity(&self) -> lsp_types::DiagnosticSeverity {
|
||||||
|
match self {
|
||||||
|
TemplateError::Lexer(_) | TemplateError::Parser(_) => {
|
||||||
|
lsp_types::DiagnosticSeverity::ERROR
|
||||||
|
}
|
||||||
|
TemplateError::Validation(_) => lsp_types::DiagnosticSeverity::WARNING,
|
||||||
|
_ => lsp_types::DiagnosticSeverity::INFORMATION,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TemplateError::Lexer(_) => "LEX",
|
||||||
|
TemplateError::Parser(_) => "PAR",
|
||||||
|
TemplateError::Validation(_) => "VAL",
|
||||||
|
TemplateError::Io(_) => "IO",
|
||||||
|
TemplateError::Config(_) => "CFG",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_lsp_diagnostic(error: &TemplateError, _source: &str) -> lsp_types::Diagnostic {
|
||||||
|
let range = error.span().map_or_else(
|
||||||
|
|| lsp_types::Range::default(),
|
||||||
|
|span| {
|
||||||
|
let start = lsp_types::Position::new(0, *span.start());
|
||||||
|
let end = lsp_types::Position::new(0, span.start() + span.length());
|
||||||
|
lsp_types::Range::new(start, end)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
lsp_types::Diagnostic {
|
||||||
|
range,
|
||||||
|
severity: Some(error.severity()),
|
||||||
|
code: Some(lsp_types::NumberOrString::String(error.code().to_string())),
|
||||||
|
code_description: None,
|
||||||
|
source: Some("djls-template".to_string()),
|
||||||
|
message: error.to_string(),
|
||||||
|
related_information: None,
|
||||||
|
tags: None,
|
||||||
|
data: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QuickFix {
|
||||||
|
pub title: String,
|
||||||
|
pub edit: String,
|
||||||
|
}
|
|
@ -22,140 +22,136 @@ impl Lexer {
|
||||||
|
|
||||||
pub fn tokenize(&mut self) -> Result<TokenStream, LexerError> {
|
pub fn tokenize(&mut self) -> Result<TokenStream, LexerError> {
|
||||||
let mut tokens = TokenStream::default();
|
let mut tokens = TokenStream::default();
|
||||||
|
|
||||||
while !self.is_at_end() {
|
while !self.is_at_end() {
|
||||||
let token = self.next_token()?;
|
self.start = self.current;
|
||||||
|
|
||||||
|
let token_type = match self.peek()? {
|
||||||
|
'{' => match self.peek_next()? {
|
||||||
|
'%' => {
|
||||||
|
self.consume_n(2)?; // {%
|
||||||
|
let content = self.consume_until("%}")?;
|
||||||
|
self.consume_n(2)?; // %}
|
||||||
|
TokenType::DjangoBlock(content)
|
||||||
|
}
|
||||||
|
'{' => {
|
||||||
|
self.consume_n(2)?; // {{
|
||||||
|
let content = self.consume_until("}}")?;
|
||||||
|
self.consume_n(2)?; // }}
|
||||||
|
TokenType::DjangoVariable(content)
|
||||||
|
}
|
||||||
|
'#' => {
|
||||||
|
self.consume_n(2)?; // {#
|
||||||
|
let content = self.consume_until("#}")?;
|
||||||
|
self.consume_n(2)?; // #}
|
||||||
|
TokenType::Comment(content, "{#".to_string(), Some("#}".to_string()))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.consume()?; // {
|
||||||
|
TokenType::Text(String::from("{"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'<' => match self.peek_next()? {
|
||||||
|
'/' => {
|
||||||
|
self.consume_n(2)?; // </
|
||||||
|
let tag = self.consume_until(">")?;
|
||||||
|
self.consume()?; // >
|
||||||
|
TokenType::HtmlTagClose(tag)
|
||||||
|
}
|
||||||
|
'!' if self.matches("<!--")? => {
|
||||||
|
self.consume_n(4)?; // <!--
|
||||||
|
let content = self.consume_until("-->")?;
|
||||||
|
self.consume_n(3)?; // -->
|
||||||
|
TokenType::Comment(content, "<!--".to_string(), Some("-->".to_string()))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.consume()?; // consume <
|
||||||
|
let tag = self.consume_until(">")?;
|
||||||
|
self.consume()?; // consume >
|
||||||
|
if tag.starts_with("script") {
|
||||||
|
TokenType::ScriptTagOpen(tag)
|
||||||
|
} else if tag.starts_with("style") {
|
||||||
|
TokenType::StyleTagOpen(tag)
|
||||||
|
} else if tag.ends_with("/") {
|
||||||
|
TokenType::HtmlTagVoid(tag.trim_end_matches("/").to_string())
|
||||||
|
} else {
|
||||||
|
TokenType::HtmlTagOpen(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'/' => match self.peek_next()? {
|
||||||
|
'/' => {
|
||||||
|
self.consume_n(2)?; // //
|
||||||
|
let content = self.consume_until("\n")?;
|
||||||
|
TokenType::Comment(content, "//".to_string(), None)
|
||||||
|
}
|
||||||
|
'*' => {
|
||||||
|
self.consume_n(2)?; // /*
|
||||||
|
let content = self.consume_until("*/")?;
|
||||||
|
self.consume_n(2)?; // */
|
||||||
|
TokenType::Comment(content, "/*".to_string(), Some("*/".to_string()))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.consume()?;
|
||||||
|
TokenType::Text("/".to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
c if c.is_whitespace() => {
|
||||||
|
if c == '\n' || c == '\r' {
|
||||||
|
self.consume()?; // \r or \n
|
||||||
|
if c == '\r' && self.peek()? == '\n' {
|
||||||
|
self.consume()?; // \n of \r\n
|
||||||
|
}
|
||||||
|
TokenType::Newline
|
||||||
|
} else {
|
||||||
|
self.consume()?; // Consume the first whitespace
|
||||||
|
while !self.is_at_end() && self.peek()?.is_whitespace() {
|
||||||
|
if self.peek()? == '\n' || self.peek()? == '\r' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.consume()?;
|
||||||
|
}
|
||||||
|
let whitespace_count = self.current - self.start;
|
||||||
|
TokenType::Whitespace(whitespace_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
let mut text = String::new();
|
||||||
|
while !self.is_at_end() {
|
||||||
|
let c = self.peek()?;
|
||||||
|
if c == '{' || c == '<' || c == '\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
text.push(c);
|
||||||
|
self.consume()?;
|
||||||
|
}
|
||||||
|
TokenType::Text(text)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let token = Token::new(token_type, self.line, Some(self.start));
|
||||||
|
|
||||||
|
match self.peek_previous()? {
|
||||||
|
'\n' => self.line += 1,
|
||||||
|
'\r' => {
|
||||||
|
self.line += 1;
|
||||||
|
if self.peek()? == '\n' {
|
||||||
|
self.current += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
tokens.add_token(token);
|
tokens.add_token(token);
|
||||||
}
|
}
|
||||||
tokens.finalize(self.line);
|
tokens.finalize(self.line);
|
||||||
Ok(tokens)
|
Ok(tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_token(&mut self) -> Result<Token, LexerError> {
|
|
||||||
self.start = self.current;
|
|
||||||
|
|
||||||
let token_type = match self.peek()? {
|
|
||||||
'{' => match self.peek_next()? {
|
|
||||||
'%' => {
|
|
||||||
self.consume_n(2)?; // {%
|
|
||||||
let content = self.consume_until("%}")?;
|
|
||||||
self.consume_n(2)?; // %}
|
|
||||||
TokenType::DjangoBlock(content)
|
|
||||||
}
|
|
||||||
'{' => {
|
|
||||||
self.consume_n(2)?; // {{
|
|
||||||
let content = self.consume_until("}}")?;
|
|
||||||
self.consume_n(2)?; // }}
|
|
||||||
TokenType::DjangoVariable(content)
|
|
||||||
}
|
|
||||||
'#' => {
|
|
||||||
self.consume_n(2)?; // {#
|
|
||||||
let content = self.consume_until("#}")?;
|
|
||||||
self.consume_n(2)?; // #}
|
|
||||||
TokenType::Comment(content, "{#".to_string(), Some("#}".to_string()))
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.consume()?; // {
|
|
||||||
TokenType::Text(String::from("{"))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'<' => match self.peek_next()? {
|
|
||||||
'/' => {
|
|
||||||
self.consume_n(2)?; // </
|
|
||||||
let tag = self.consume_until(">")?;
|
|
||||||
self.consume()?; // >
|
|
||||||
TokenType::HtmlTagClose(tag)
|
|
||||||
}
|
|
||||||
'!' if self.matches("<!--")? => {
|
|
||||||
self.consume_n(4)?; // <!--
|
|
||||||
let content = self.consume_until("-->")?;
|
|
||||||
self.consume_n(3)?; // -->
|
|
||||||
TokenType::Comment(content, "<!--".to_string(), Some("-->".to_string()))
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.consume()?; // consume <
|
|
||||||
let tag = self.consume_until(">")?;
|
|
||||||
self.consume()?; // consume >
|
|
||||||
if tag.starts_with("script") {
|
|
||||||
TokenType::ScriptTagOpen(tag)
|
|
||||||
} else if tag.starts_with("style") {
|
|
||||||
TokenType::StyleTagOpen(tag)
|
|
||||||
} else if tag.ends_with("/") {
|
|
||||||
TokenType::HtmlTagVoid(tag.trim_end_matches("/").to_string())
|
|
||||||
} else {
|
|
||||||
TokenType::HtmlTagOpen(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'/' => match self.peek_next()? {
|
|
||||||
'/' => {
|
|
||||||
self.consume_n(2)?; // //
|
|
||||||
let content = self.consume_until("\n")?;
|
|
||||||
TokenType::Comment(content, "//".to_string(), None)
|
|
||||||
}
|
|
||||||
'*' => {
|
|
||||||
self.consume_n(2)?; // /*
|
|
||||||
let content = self.consume_until("*/")?;
|
|
||||||
self.consume_n(2)?; // */
|
|
||||||
TokenType::Comment(content, "/*".to_string(), Some("*/".to_string()))
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.consume()?;
|
|
||||||
TokenType::Text("/".to_string())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
c if c.is_whitespace() => {
|
|
||||||
if c == '\n' || c == '\r' {
|
|
||||||
self.consume()?; // \r or \n
|
|
||||||
if c == '\r' && self.peek()? == '\n' {
|
|
||||||
self.consume()?; // \n of \r\n
|
|
||||||
}
|
|
||||||
TokenType::Newline
|
|
||||||
} else {
|
|
||||||
self.consume()?; // Consume the first whitespace
|
|
||||||
while !self.is_at_end() && self.peek()?.is_whitespace() {
|
|
||||||
if self.peek()? == '\n' || self.peek()? == '\r' {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
self.consume()?;
|
|
||||||
}
|
|
||||||
let whitespace_count = self.current - self.start;
|
|
||||||
TokenType::Whitespace(whitespace_count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
let mut text = String::new();
|
|
||||||
while !self.is_at_end() {
|
|
||||||
let c = self.peek()?;
|
|
||||||
if c == '{' || c == '<' || c == '\n' {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
text.push(c);
|
|
||||||
self.consume()?;
|
|
||||||
}
|
|
||||||
TokenType::Text(text)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let token = Token::new(token_type, self.line, Some(self.start));
|
|
||||||
|
|
||||||
match self.peek_previous()? {
|
|
||||||
'\n' => self.line += 1,
|
|
||||||
'\r' => {
|
|
||||||
self.line += 1;
|
|
||||||
if self.peek()? == '\n' {
|
|
||||||
self.current += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn peek(&self) -> Result<char, LexerError> {
|
fn peek(&self) -> Result<char, LexerError> {
|
||||||
self.peek_at(0)
|
self.peek_at(0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,44 @@
|
||||||
mod ast;
|
mod ast;
|
||||||
|
mod error;
|
||||||
mod lexer;
|
mod lexer;
|
||||||
mod parser;
|
mod parser;
|
||||||
|
mod tagspecs;
|
||||||
mod tokens;
|
mod tokens;
|
||||||
|
|
||||||
pub use ast::Ast;
|
use ast::NodeList;
|
||||||
pub use parser::Parser;
|
pub use error::{to_lsp_diagnostic, QuickFix, TemplateError};
|
||||||
|
|
||||||
|
use lexer::Lexer;
|
||||||
|
pub use parser::{Parser, ParserError};
|
||||||
|
|
||||||
|
/// Parses a Django template and returns the AST and any parsing errors.
|
||||||
|
///
|
||||||
|
/// - `source`: The template source code as a `&str`.
|
||||||
|
/// - `tag_specs`: Optional `TagSpecs` to use for parsing (e.g., custom tags).
|
||||||
|
///
|
||||||
|
/// Returns a `Result` containing a tuple of `(Ast, Vec<ParserError>)` on success,
|
||||||
|
/// or a `ParserError` on failure.
|
||||||
|
pub fn parse_template(source: &str) -> Result<(NodeList, Vec<TemplateError>), TemplateError> {
|
||||||
|
let tokens = Lexer::new(source)
|
||||||
|
.tokenize()
|
||||||
|
.map_err(|e| TemplateError::Lexer(e.to_string()))?;
|
||||||
|
|
||||||
|
// let tag_specs = match tag_specs {
|
||||||
|
// Some(specs) => specs.clone(),
|
||||||
|
// None => TagSpecs::load_builtin_specs()
|
||||||
|
// .map_err(|e| TemplateError::Config(format!("Failed to load builtin specs: {}", e)))?,
|
||||||
|
// };
|
||||||
|
|
||||||
|
let mut parser = Parser::new(tokens);
|
||||||
|
let (nodelist, parser_errors) = parser
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| TemplateError::Parser(e.to_string()))?;
|
||||||
|
|
||||||
|
// Convert parser errors to TemplateError
|
||||||
|
let all_errors = parser_errors
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| TemplateError::Parser(e.to_string()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok((nodelist, all_errors))
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<!-- HTML comment -->"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 21
|
||||||
|
- Comment:
|
||||||
|
content: Django comment
|
||||||
|
span:
|
||||||
|
start: 21
|
||||||
|
length: 14
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Tag:
|
||||||
|
name: if
|
||||||
|
bits:
|
||||||
|
- x
|
||||||
|
- ">"
|
||||||
|
- "0"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 8
|
||||||
|
- Text:
|
||||||
|
content: Positive
|
||||||
|
span:
|
||||||
|
start: 14
|
||||||
|
length: 8
|
||||||
|
- Tag:
|
||||||
|
name: elif
|
||||||
|
bits:
|
||||||
|
- x
|
||||||
|
- "<"
|
||||||
|
- "0"
|
||||||
|
span:
|
||||||
|
start: 22
|
||||||
|
length: 10
|
||||||
|
- Text:
|
||||||
|
content: Negative
|
||||||
|
span:
|
||||||
|
start: 38
|
||||||
|
length: 8
|
||||||
|
- Tag:
|
||||||
|
name: else
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 46
|
||||||
|
length: 4
|
||||||
|
- Text:
|
||||||
|
content: Zero
|
||||||
|
span:
|
||||||
|
start: 56
|
||||||
|
length: 4
|
||||||
|
- Tag:
|
||||||
|
name: endif
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 60
|
||||||
|
length: 5
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Tag:
|
||||||
|
name: for
|
||||||
|
bits:
|
||||||
|
- item
|
||||||
|
- in
|
||||||
|
- items
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 17
|
||||||
|
- Variable:
|
||||||
|
var: item
|
||||||
|
filters: []
|
||||||
|
span:
|
||||||
|
start: 23
|
||||||
|
length: 4
|
||||||
|
- Tag:
|
||||||
|
name: empty
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 33
|
||||||
|
length: 5
|
||||||
|
- Text:
|
||||||
|
content: No items
|
||||||
|
span:
|
||||||
|
start: 44
|
||||||
|
length: 8
|
||||||
|
- Tag:
|
||||||
|
name: endfor
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 52
|
||||||
|
length: 6
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Tag:
|
||||||
|
name: if
|
||||||
|
bits:
|
||||||
|
- user.is_authenticated
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 24
|
||||||
|
- Text:
|
||||||
|
content: Welcome
|
||||||
|
span:
|
||||||
|
start: 30
|
||||||
|
length: 7
|
||||||
|
- Tag:
|
||||||
|
name: endif
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 37
|
||||||
|
length: 5
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Tag:
|
||||||
|
name: url
|
||||||
|
bits:
|
||||||
|
- "'view-name'"
|
||||||
|
- as
|
||||||
|
- view
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 23
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Variable:
|
||||||
|
var: user.name
|
||||||
|
filters: []
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 9
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Variable:
|
||||||
|
var: user.name
|
||||||
|
filters:
|
||||||
|
- title
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 15
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Variable:
|
||||||
|
var: value
|
||||||
|
filters:
|
||||||
|
- "default:'nothing'"
|
||||||
|
- title
|
||||||
|
- upper
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 35
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,148 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "Welcome,"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 8
|
||||||
|
- Tag:
|
||||||
|
name: if
|
||||||
|
bits:
|
||||||
|
- user.is_authenticated
|
||||||
|
span:
|
||||||
|
start: 9
|
||||||
|
length: 24
|
||||||
|
- Variable:
|
||||||
|
var: user.name
|
||||||
|
filters:
|
||||||
|
- title
|
||||||
|
- "default:'Guest'"
|
||||||
|
span:
|
||||||
|
start: 44
|
||||||
|
length: 31
|
||||||
|
- Tag:
|
||||||
|
name: for
|
||||||
|
bits:
|
||||||
|
- group
|
||||||
|
- in
|
||||||
|
- user.groups
|
||||||
|
span:
|
||||||
|
start: 86
|
||||||
|
length: 24
|
||||||
|
- Tag:
|
||||||
|
name: if
|
||||||
|
bits:
|
||||||
|
- forloop.first
|
||||||
|
span:
|
||||||
|
start: 125
|
||||||
|
length: 16
|
||||||
|
- Text:
|
||||||
|
content: (
|
||||||
|
span:
|
||||||
|
start: 147
|
||||||
|
length: 1
|
||||||
|
- Tag:
|
||||||
|
name: endif
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 148
|
||||||
|
length: 5
|
||||||
|
- Variable:
|
||||||
|
var: group.name
|
||||||
|
filters: []
|
||||||
|
span:
|
||||||
|
start: 168
|
||||||
|
length: 10
|
||||||
|
- Tag:
|
||||||
|
name: if
|
||||||
|
bits:
|
||||||
|
- not
|
||||||
|
- forloop.last
|
||||||
|
span:
|
||||||
|
start: 193
|
||||||
|
length: 19
|
||||||
|
- Text:
|
||||||
|
content: ","
|
||||||
|
span:
|
||||||
|
start: 218
|
||||||
|
length: 1
|
||||||
|
- Tag:
|
||||||
|
name: endif
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 220
|
||||||
|
length: 5
|
||||||
|
- Tag:
|
||||||
|
name: if
|
||||||
|
bits:
|
||||||
|
- forloop.last
|
||||||
|
span:
|
||||||
|
start: 240
|
||||||
|
length: 15
|
||||||
|
- Text:
|
||||||
|
content: )
|
||||||
|
span:
|
||||||
|
start: 261
|
||||||
|
length: 1
|
||||||
|
- Tag:
|
||||||
|
name: endif
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 262
|
||||||
|
length: 5
|
||||||
|
- Tag:
|
||||||
|
name: empty
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 278
|
||||||
|
length: 5
|
||||||
|
- Text:
|
||||||
|
content: (no groups)
|
||||||
|
span:
|
||||||
|
start: 298
|
||||||
|
length: 11
|
||||||
|
- Tag:
|
||||||
|
name: endfor
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 314
|
||||||
|
length: 6
|
||||||
|
- Tag:
|
||||||
|
name: else
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 327
|
||||||
|
length: 4
|
||||||
|
- Text:
|
||||||
|
content: Guest
|
||||||
|
span:
|
||||||
|
start: 342
|
||||||
|
length: 5
|
||||||
|
- Tag:
|
||||||
|
name: endif
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 348
|
||||||
|
length: 5
|
||||||
|
- Text:
|
||||||
|
content: "!"
|
||||||
|
span:
|
||||||
|
start: 359
|
||||||
|
length: 1
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
||||||
|
- 40
|
||||||
|
- 82
|
||||||
|
- 117
|
||||||
|
- 160
|
||||||
|
- 185
|
||||||
|
- 232
|
||||||
|
- 274
|
||||||
|
- 290
|
||||||
|
- 310
|
||||||
|
- 327
|
||||||
|
- 338
|
||||||
|
- 348
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Tag:
|
||||||
|
name: for
|
||||||
|
bits:
|
||||||
|
- item
|
||||||
|
- in
|
||||||
|
- items
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 17
|
||||||
|
- Tag:
|
||||||
|
name: if
|
||||||
|
bits:
|
||||||
|
- item.active
|
||||||
|
span:
|
||||||
|
start: 23
|
||||||
|
length: 14
|
||||||
|
- Variable:
|
||||||
|
var: item.name
|
||||||
|
filters: []
|
||||||
|
span:
|
||||||
|
start: 43
|
||||||
|
length: 9
|
||||||
|
- Tag:
|
||||||
|
name: endif
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 58
|
||||||
|
length: 5
|
||||||
|
- Tag:
|
||||||
|
name: endfor
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 69
|
||||||
|
length: 6
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<div class=\"container\">"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 23
|
||||||
|
- Text:
|
||||||
|
content: "<h1>Header</h1>"
|
||||||
|
span:
|
||||||
|
start: 28
|
||||||
|
length: 15
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
||||||
|
- 24
|
||||||
|
- 44
|
||||||
|
- 54
|
||||||
|
- 106
|
||||||
|
- 145
|
||||||
|
- 159
|
||||||
|
- 219
|
||||||
|
- 251
|
||||||
|
- 287
|
||||||
|
- 308
|
||||||
|
- 341
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Tag:
|
||||||
|
name: for
|
||||||
|
bits:
|
||||||
|
- item
|
||||||
|
- in
|
||||||
|
- items
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 17
|
||||||
|
- Variable:
|
||||||
|
var: item.name
|
||||||
|
filters: []
|
||||||
|
span:
|
||||||
|
start: 23
|
||||||
|
length: 9
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Tag:
|
||||||
|
name: if
|
||||||
|
bits:
|
||||||
|
- user.is_authenticated
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 24
|
||||||
|
- Text:
|
||||||
|
content: Welcome
|
||||||
|
span:
|
||||||
|
start: 30
|
||||||
|
length: 7
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<div>"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 5
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<script>console.log('test');"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 28
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<style>body { color: blue;"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 26
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,206 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<!DOCTYPE html>"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 15
|
||||||
|
- Text:
|
||||||
|
content: "<html>"
|
||||||
|
span:
|
||||||
|
start: 16
|
||||||
|
length: 6
|
||||||
|
- Text:
|
||||||
|
content: "<head>"
|
||||||
|
span:
|
||||||
|
start: 27
|
||||||
|
length: 6
|
||||||
|
- Text:
|
||||||
|
content: "<style type=\"text/css\">"
|
||||||
|
span:
|
||||||
|
start: 42
|
||||||
|
length: 23
|
||||||
|
- Text:
|
||||||
|
content: /* Style header */
|
||||||
|
span:
|
||||||
|
start: 78
|
||||||
|
length: 18
|
||||||
|
- Text:
|
||||||
|
content: ".header { color: blue; }"
|
||||||
|
span:
|
||||||
|
start: 109
|
||||||
|
length: 24
|
||||||
|
- Text:
|
||||||
|
content: "</style>"
|
||||||
|
span:
|
||||||
|
start: 142
|
||||||
|
length: 8
|
||||||
|
- Text:
|
||||||
|
content: "<script type=\"text/javascript\">"
|
||||||
|
span:
|
||||||
|
start: 159
|
||||||
|
length: 31
|
||||||
|
- Text:
|
||||||
|
content: // Init app
|
||||||
|
span:
|
||||||
|
start: 203
|
||||||
|
length: 11
|
||||||
|
- Text:
|
||||||
|
content: "const app = {"
|
||||||
|
span:
|
||||||
|
start: 227
|
||||||
|
length: 13
|
||||||
|
- Text:
|
||||||
|
content: /* Config */
|
||||||
|
span:
|
||||||
|
start: 257
|
||||||
|
length: 12
|
||||||
|
- Text:
|
||||||
|
content: "debug: true"
|
||||||
|
span:
|
||||||
|
start: 286
|
||||||
|
length: 11
|
||||||
|
- Text:
|
||||||
|
content: "};"
|
||||||
|
span:
|
||||||
|
start: 310
|
||||||
|
length: 2
|
||||||
|
- Text:
|
||||||
|
content: "</script>"
|
||||||
|
span:
|
||||||
|
start: 321
|
||||||
|
length: 9
|
||||||
|
- Text:
|
||||||
|
content: "</head>"
|
||||||
|
span:
|
||||||
|
start: 335
|
||||||
|
length: 7
|
||||||
|
- Text:
|
||||||
|
content: "<body>"
|
||||||
|
span:
|
||||||
|
start: 347
|
||||||
|
length: 6
|
||||||
|
- Text:
|
||||||
|
content: "<!-- Header section -->"
|
||||||
|
span:
|
||||||
|
start: 362
|
||||||
|
length: 23
|
||||||
|
- Text:
|
||||||
|
content: "<div class=\"header\" id=\"main\" data-value=\"123\" disabled>"
|
||||||
|
span:
|
||||||
|
start: 394
|
||||||
|
length: 56
|
||||||
|
- Tag:
|
||||||
|
name: if
|
||||||
|
bits:
|
||||||
|
- user.is_authenticated
|
||||||
|
span:
|
||||||
|
start: 463
|
||||||
|
length: 24
|
||||||
|
- Comment:
|
||||||
|
content: Welcome message
|
||||||
|
span:
|
||||||
|
start: 510
|
||||||
|
length: 15
|
||||||
|
- Text:
|
||||||
|
content: "<h1>Welcome,"
|
||||||
|
span:
|
||||||
|
start: 548
|
||||||
|
length: 12
|
||||||
|
- Variable:
|
||||||
|
var: user.name
|
||||||
|
filters:
|
||||||
|
- title
|
||||||
|
- "default:'Guest'"
|
||||||
|
span:
|
||||||
|
start: 561
|
||||||
|
length: 31
|
||||||
|
- Text:
|
||||||
|
content: "!</h1>"
|
||||||
|
span:
|
||||||
|
start: 598
|
||||||
|
length: 6
|
||||||
|
- Tag:
|
||||||
|
name: if
|
||||||
|
bits:
|
||||||
|
- user.is_staff
|
||||||
|
span:
|
||||||
|
start: 621
|
||||||
|
length: 16
|
||||||
|
- Text:
|
||||||
|
content: "<span>Admin</span>"
|
||||||
|
span:
|
||||||
|
start: 664
|
||||||
|
length: 18
|
||||||
|
- Tag:
|
||||||
|
name: else
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 699
|
||||||
|
length: 4
|
||||||
|
- Text:
|
||||||
|
content: "<span>User</span>"
|
||||||
|
span:
|
||||||
|
start: 730
|
||||||
|
length: 17
|
||||||
|
- Tag:
|
||||||
|
name: endif
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 764
|
||||||
|
length: 5
|
||||||
|
- Tag:
|
||||||
|
name: endif
|
||||||
|
bits: []
|
||||||
|
span:
|
||||||
|
start: 788
|
||||||
|
length: 5
|
||||||
|
- Text:
|
||||||
|
content: "</div>"
|
||||||
|
span:
|
||||||
|
start: 808
|
||||||
|
length: 6
|
||||||
|
- Text:
|
||||||
|
content: "</body>"
|
||||||
|
span:
|
||||||
|
start: 819
|
||||||
|
length: 7
|
||||||
|
- Text:
|
||||||
|
content: "</html>"
|
||||||
|
span:
|
||||||
|
start: 827
|
||||||
|
length: 7
|
||||||
|
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
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<!DOCTYPE html>"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 15
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<div class=\"container\">Hello</div>"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 34
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<input type=\"text\" />"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 21
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -1,28 +0,0 @@
|
||||||
---
|
|
||||||
source: crates/djls-template-ast/src/parser.rs
|
|
||||||
expression: ast
|
|
||||||
---
|
|
||||||
nodes:
|
|
||||||
- Html:
|
|
||||||
Comment: HTML comment
|
|
||||||
- Django:
|
|
||||||
Comment: Django comment
|
|
||||||
- Script:
|
|
||||||
Element:
|
|
||||||
attributes:
|
|
||||||
script: Boolean
|
|
||||||
children:
|
|
||||||
- Script:
|
|
||||||
Comment:
|
|
||||||
content: JS single line
|
|
||||||
kind: SingleLine
|
|
||||||
- Script:
|
|
||||||
Comment:
|
|
||||||
content: "JS multi\n line"
|
|
||||||
kind: MultiLine
|
|
||||||
- Style:
|
|
||||||
Element:
|
|
||||||
attributes: {}
|
|
||||||
children:
|
|
||||||
- Style:
|
|
||||||
Comment: CSS comment
|
|
|
@ -1,13 +0,0 @@
|
||||||
---
|
|
||||||
source: crates/djls-template-ast/src/parser.rs
|
|
||||||
expression: ast
|
|
||||||
---
|
|
||||||
nodes:
|
|
||||||
- Django:
|
|
||||||
Tag:
|
|
||||||
kind: If
|
|
||||||
bits:
|
|
||||||
- if
|
|
||||||
- user.is_staff
|
|
||||||
children:
|
|
||||||
- Text: Admin
|
|
|
@ -1,16 +0,0 @@
|
||||||
---
|
|
||||||
source: crates/djls-template-ast/src/parser.rs
|
|
||||||
expression: ast
|
|
||||||
---
|
|
||||||
nodes:
|
|
||||||
- Django:
|
|
||||||
Variable:
|
|
||||||
bits:
|
|
||||||
- user
|
|
||||||
- name
|
|
||||||
filters:
|
|
||||||
- name: default
|
|
||||||
arguments:
|
|
||||||
- Anonymous
|
|
||||||
- name: title
|
|
||||||
arguments: []
|
|
|
@ -1,106 +0,0 @@
|
||||||
---
|
|
||||||
source: crates/djls-template-ast/src/parser.rs
|
|
||||||
expression: ast
|
|
||||||
---
|
|
||||||
nodes:
|
|
||||||
- Html:
|
|
||||||
Doctype: "!DOCTYPE"
|
|
||||||
- Html:
|
|
||||||
Element:
|
|
||||||
tag_name: html
|
|
||||||
attributes: {}
|
|
||||||
children:
|
|
||||||
- Html:
|
|
||||||
Element:
|
|
||||||
tag_name: head
|
|
||||||
attributes: {}
|
|
||||||
children:
|
|
||||||
- Style:
|
|
||||||
Element:
|
|
||||||
attributes:
|
|
||||||
type:
|
|
||||||
Value: text/css
|
|
||||||
children:
|
|
||||||
- Style:
|
|
||||||
Comment: Style header
|
|
||||||
- Text: ".header "
|
|
||||||
- Text: "{"
|
|
||||||
- Text: "color: blue; }"
|
|
||||||
- Script:
|
|
||||||
Element:
|
|
||||||
attributes:
|
|
||||||
script: Boolean
|
|
||||||
type:
|
|
||||||
Value: text/javascript
|
|
||||||
children:
|
|
||||||
- Script:
|
|
||||||
Comment:
|
|
||||||
content: Init app
|
|
||||||
kind: SingleLine
|
|
||||||
- Text: "const app = "
|
|
||||||
- Text: "{"
|
|
||||||
- Script:
|
|
||||||
Comment:
|
|
||||||
content: Config
|
|
||||||
kind: MultiLine
|
|
||||||
- Text: "debug: true"
|
|
||||||
- Text: "};"
|
|
||||||
- Html:
|
|
||||||
Element:
|
|
||||||
tag_name: body
|
|
||||||
attributes: {}
|
|
||||||
children:
|
|
||||||
- Html:
|
|
||||||
Comment: Header section
|
|
||||||
- Html:
|
|
||||||
Element:
|
|
||||||
tag_name: div
|
|
||||||
attributes:
|
|
||||||
class:
|
|
||||||
Value: header
|
|
||||||
data-value:
|
|
||||||
Value: "123"
|
|
||||||
disabled: Boolean
|
|
||||||
id:
|
|
||||||
Value: main
|
|
||||||
children:
|
|
||||||
- Django:
|
|
||||||
Tag:
|
|
||||||
kind: If
|
|
||||||
bits:
|
|
||||||
- if
|
|
||||||
- user.is_authenticated
|
|
||||||
children:
|
|
||||||
- Django:
|
|
||||||
Comment: Welcome message
|
|
||||||
- Html:
|
|
||||||
Element:
|
|
||||||
tag_name: h1
|
|
||||||
attributes: {}
|
|
||||||
children:
|
|
||||||
- Text: "Welcome, "
|
|
||||||
- Django:
|
|
||||||
Variable:
|
|
||||||
bits:
|
|
||||||
- user
|
|
||||||
- name
|
|
||||||
filters:
|
|
||||||
- name: default
|
|
||||||
arguments:
|
|
||||||
- Guest
|
|
||||||
- name: title
|
|
||||||
arguments: []
|
|
||||||
- Text: "!"
|
|
||||||
- Django:
|
|
||||||
Tag:
|
|
||||||
kind: If
|
|
||||||
bits:
|
|
||||||
- if
|
|
||||||
- user.is_staff
|
|
||||||
children:
|
|
||||||
- Html:
|
|
||||||
Element:
|
|
||||||
tag_name: span
|
|
||||||
attributes: {}
|
|
||||||
children:
|
|
||||||
- Text: Admin
|
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
source: crates/djls-template-ast/src/parser.rs
|
|
||||||
expression: ast
|
|
||||||
---
|
|
||||||
nodes:
|
|
||||||
- Html:
|
|
||||||
Doctype: "!DOCTYPE"
|
|
|
@ -1,15 +0,0 @@
|
||||||
---
|
|
||||||
source: crates/djls-template-ast/src/parser.rs
|
|
||||||
expression: ast
|
|
||||||
---
|
|
||||||
nodes:
|
|
||||||
- Html:
|
|
||||||
Element:
|
|
||||||
tag_name: div
|
|
||||||
attributes:
|
|
||||||
class:
|
|
||||||
Value: container
|
|
||||||
disabled: Boolean
|
|
||||||
id:
|
|
||||||
Value: main
|
|
||||||
children: []
|
|
|
@ -1,11 +0,0 @@
|
||||||
---
|
|
||||||
source: crates/djls-template-ast/src/parser.rs
|
|
||||||
expression: ast
|
|
||||||
---
|
|
||||||
nodes:
|
|
||||||
- Html:
|
|
||||||
Void:
|
|
||||||
tag_name: img
|
|
||||||
attributes:
|
|
||||||
src:
|
|
||||||
Value: example.png
|
|
|
@ -1,22 +0,0 @@
|
||||||
---
|
|
||||||
source: crates/djls-template-ast/src/parser.rs
|
|
||||||
expression: ast
|
|
||||||
---
|
|
||||||
nodes:
|
|
||||||
- Script:
|
|
||||||
Element:
|
|
||||||
attributes:
|
|
||||||
script: Boolean
|
|
||||||
type:
|
|
||||||
Value: text/javascript
|
|
||||||
children:
|
|
||||||
- Script:
|
|
||||||
Comment:
|
|
||||||
content: Single line comment
|
|
||||||
kind: SingleLine
|
|
||||||
- Text: const x = 1;
|
|
||||||
- Script:
|
|
||||||
Comment:
|
|
||||||
content: "Multi-line\n comment"
|
|
||||||
kind: MultiLine
|
|
||||||
- Text: console.log(x);
|
|
|
@ -1,17 +0,0 @@
|
||||||
---
|
|
||||||
source: crates/djls-template-ast/src/parser.rs
|
|
||||||
expression: ast
|
|
||||||
---
|
|
||||||
nodes:
|
|
||||||
- Style:
|
|
||||||
Element:
|
|
||||||
attributes:
|
|
||||||
type:
|
|
||||||
Value: text/css
|
|
||||||
children:
|
|
||||||
- Style:
|
|
||||||
Comment: Header styles
|
|
||||||
- Text: ".header "
|
|
||||||
- Text: "{"
|
|
||||||
- Text: "color: blue;"
|
|
||||||
- Text: "}"
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<script type=\"text/javascript\">"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 31
|
||||||
|
- Text:
|
||||||
|
content: // Single line comment
|
||||||
|
span:
|
||||||
|
start: 36
|
||||||
|
length: 22
|
||||||
|
- Text:
|
||||||
|
content: const x = 1;
|
||||||
|
span:
|
||||||
|
start: 63
|
||||||
|
length: 12
|
||||||
|
- Text:
|
||||||
|
content: "/* Multi-line\n comment */"
|
||||||
|
span:
|
||||||
|
start: 80
|
||||||
|
length: 32
|
||||||
|
- Text:
|
||||||
|
content: console.log(x);
|
||||||
|
span:
|
||||||
|
start: 117
|
||||||
|
length: 15
|
||||||
|
- Text:
|
||||||
|
content: "</script>"
|
||||||
|
span:
|
||||||
|
start: 133
|
||||||
|
length: 9
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
||||||
|
- 32
|
||||||
|
- 59
|
||||||
|
- 76
|
||||||
|
- 113
|
||||||
|
- 133
|
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: "<style type=\"text/css\">"
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 23
|
||||||
|
- Text:
|
||||||
|
content: /* Header styles */
|
||||||
|
span:
|
||||||
|
start: 28
|
||||||
|
length: 19
|
||||||
|
- Text:
|
||||||
|
content: ".header {"
|
||||||
|
span:
|
||||||
|
start: 52
|
||||||
|
length: 9
|
||||||
|
- Text:
|
||||||
|
content: "color: blue;"
|
||||||
|
span:
|
||||||
|
start: 70
|
||||||
|
length: 12
|
||||||
|
- Text:
|
||||||
|
content: "}"
|
||||||
|
span:
|
||||||
|
start: 87
|
||||||
|
length: 1
|
||||||
|
- Text:
|
||||||
|
content: "</style>"
|
||||||
|
span:
|
||||||
|
start: 89
|
||||||
|
length: 8
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
||||||
|
- 24
|
||||||
|
- 48
|
||||||
|
- 62
|
||||||
|
- 83
|
||||||
|
- 89
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: hello
|
||||||
|
span:
|
||||||
|
start: 5
|
||||||
|
length: 5
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: hello
|
||||||
|
span:
|
||||||
|
start: 6
|
||||||
|
length: 5
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
||||||
|
- 1
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: hello
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 5
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
source: crates/djls-template-ast/src/parser.rs
|
||||||
|
expression: nodelist
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
- Text:
|
||||||
|
content: hello
|
||||||
|
span:
|
||||||
|
start: 0
|
||||||
|
length: 5
|
||||||
|
line_offsets:
|
||||||
|
- 0
|
||||||
|
- 11
|
311
crates/djls-template-ast/src/tagspecs.rs
Normal file
311
crates/djls-template-ast/src/tagspecs.rs
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use thiserror::Error;
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum TagSpecError {
|
||||||
|
#[error("Failed to read file: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("Failed to parse TOML: {0}")]
|
||||||
|
Toml(#[from] toml::de::Error),
|
||||||
|
#[error("Failed to extract specs: {0}")]
|
||||||
|
Extract(String),
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct TagSpecs(HashMap<String, TagSpec>);
|
||||||
|
|
||||||
|
impl TagSpecs {
|
||||||
|
pub fn get(&self, key: &str) -> Option<&TagSpec> {
|
||||||
|
self.0.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load specs from a TOML file, looking under the specified table path
|
||||||
|
fn load_from_toml(path: &Path, table_path: &[&str]) -> Result<Self, anyhow::Error> {
|
||||||
|
let content = fs::read_to_string(path)?;
|
||||||
|
let value: Value = toml::from_str(&content)?;
|
||||||
|
|
||||||
|
// Navigate to the specified table
|
||||||
|
let table = table_path
|
||||||
|
.iter()
|
||||||
|
.try_fold(&value, |current, &key| {
|
||||||
|
current
|
||||||
|
.get(key)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing table: {}", key))
|
||||||
|
})
|
||||||
|
.unwrap_or(&value);
|
||||||
|
|
||||||
|
let mut specs = HashMap::new();
|
||||||
|
TagSpec::extract_specs(table, None, &mut specs)
|
||||||
|
.map_err(|e| TagSpecError::Extract(e.to_string()))?;
|
||||||
|
Ok(TagSpecs(specs))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load specs from a user's project directory
|
||||||
|
pub fn load_user_specs(project_root: &Path) -> Result<Self, anyhow::Error> {
|
||||||
|
// List of config files to try, in priority order
|
||||||
|
let config_files = ["djls.toml", ".djls.toml", "pyproject.toml"];
|
||||||
|
|
||||||
|
for &file in &config_files {
|
||||||
|
let path = project_root.join(file);
|
||||||
|
if path.exists() {
|
||||||
|
return match file {
|
||||||
|
"pyproject.toml" => Self::load_from_toml(&path, &["tool", "djls", "tagspecs"]),
|
||||||
|
_ => Self::load_from_toml(&path, &["tagspecs"]), // Root level for other files
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Self::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load builtin specs from the crate's tagspecs directory
|
||||||
|
pub fn load_builtin_specs() -> Result<Self, anyhow::Error> {
|
||||||
|
let specs_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tagspecs");
|
||||||
|
let mut specs = HashMap::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(&specs_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(|ext| ext.to_str()) == Some("toml") {
|
||||||
|
let file_specs = Self::load_from_toml(&path, &[])?;
|
||||||
|
specs.extend(file_specs.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TagSpecs(specs))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge another TagSpecs into this one, with the other taking precedence
|
||||||
|
pub fn merge(&mut self, other: TagSpecs) -> &mut Self {
|
||||||
|
self.0.extend(other.0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load both builtin and user specs, with user specs taking precedence
|
||||||
|
pub fn load_all(project_root: &Path) -> Result<Self, anyhow::Error> {
|
||||||
|
let mut specs = Self::load_builtin_specs()?;
|
||||||
|
let user_specs = Self::load_user_specs(project_root)?;
|
||||||
|
Ok(specs.merge(user_specs).clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct TagSpec {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub tag_type: TagType,
|
||||||
|
pub closing: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub branches: Option<Vec<String>>,
|
||||||
|
pub args: Option<Vec<ArgSpec>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagSpec {
|
||||||
|
fn extract_specs(
|
||||||
|
value: &Value,
|
||||||
|
prefix: Option<&str>,
|
||||||
|
specs: &mut HashMap<String, TagSpec>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Try to deserialize as a tag spec first
|
||||||
|
match TagSpec::deserialize(value.clone()) {
|
||||||
|
Ok(tag_spec) => {
|
||||||
|
let name = prefix.map_or_else(String::new, |p| {
|
||||||
|
p.split('.').last().unwrap_or(p).to_string()
|
||||||
|
});
|
||||||
|
specs.insert(name, tag_spec);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Not a tag spec, try recursing into any table values
|
||||||
|
for (key, value) in value.as_table().iter().flat_map(|t| t.iter()) {
|
||||||
|
let new_prefix = match prefix {
|
||||||
|
None => key.clone(),
|
||||||
|
Some(p) => format!("{}.{}", p, key),
|
||||||
|
};
|
||||||
|
Self::extract_specs(value, Some(&new_prefix), specs)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum TagType {
|
||||||
|
Container,
|
||||||
|
Inclusion,
|
||||||
|
Single,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct ArgSpec {
|
||||||
|
pub name: String,
|
||||||
|
pub required: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub allowed_values: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_kwarg: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArgSpec {
|
||||||
|
pub fn is_placeholder(arg: &str) -> bool {
|
||||||
|
arg.starts_with('{') && arg.ends_with('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_placeholder_name(arg: &str) -> Option<&str> {
|
||||||
|
if Self::is_placeholder(arg) {
|
||||||
|
Some(&arg[1..arg.len() - 1])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_load_builtins() -> Result<(), anyhow::Error> {
|
||||||
|
let specs = TagSpecs::load_builtin_specs()?;
|
||||||
|
|
||||||
|
assert!(!specs.0.is_empty(), "Should have loaded at least one spec");
|
||||||
|
|
||||||
|
for name in specs.0.keys() {
|
||||||
|
assert!(!name.is_empty(), "Tag name should not be empty");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_builtin_django_tags() -> Result<(), anyhow::Error> {
|
||||||
|
let specs = TagSpecs::load_builtin_specs()?;
|
||||||
|
|
||||||
|
let expected_tags = [
|
||||||
|
"autoescape",
|
||||||
|
"block",
|
||||||
|
"comment",
|
||||||
|
"cycle",
|
||||||
|
"debug",
|
||||||
|
"extends",
|
||||||
|
"filter",
|
||||||
|
"for",
|
||||||
|
"firstof",
|
||||||
|
"if",
|
||||||
|
"include",
|
||||||
|
"load",
|
||||||
|
"now",
|
||||||
|
"spaceless",
|
||||||
|
"templatetag",
|
||||||
|
"url",
|
||||||
|
"verbatim",
|
||||||
|
"with",
|
||||||
|
];
|
||||||
|
let missing_tags = [
|
||||||
|
"csrf_token",
|
||||||
|
"ifchanged",
|
||||||
|
"lorem",
|
||||||
|
"querystring", // 5.1
|
||||||
|
"regroup",
|
||||||
|
"resetcycle",
|
||||||
|
"widthratio",
|
||||||
|
];
|
||||||
|
|
||||||
|
for tag in expected_tags {
|
||||||
|
assert!(specs.get(tag).is_some(), "{} tag should be present", tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag in missing_tags {
|
||||||
|
assert!(
|
||||||
|
specs.get(tag).is_none(),
|
||||||
|
"{} tag should not be present yet",
|
||||||
|
tag
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_user_defined_tags() -> Result<(), anyhow::Error> {
|
||||||
|
let dir = tempfile::tempdir()?;
|
||||||
|
let root = dir.path();
|
||||||
|
|
||||||
|
let pyproject_content = r#"
|
||||||
|
[tool.djls.template.tags.mytag]
|
||||||
|
type = "container"
|
||||||
|
closing = "endmytag"
|
||||||
|
branches = ["mybranch"]
|
||||||
|
args = [{ name = "myarg", required = true }]
|
||||||
|
"#;
|
||||||
|
fs::write(root.join("pyproject.toml"), pyproject_content)?;
|
||||||
|
|
||||||
|
let specs = TagSpecs::load_all(root)?;
|
||||||
|
|
||||||
|
let if_tag = specs.get("if").expect("if tag should be present");
|
||||||
|
assert_eq!(if_tag.tag_type, TagType::Container);
|
||||||
|
|
||||||
|
let my_tag = specs.get("mytag").expect("mytag should be present");
|
||||||
|
assert_eq!(my_tag.tag_type, TagType::Container);
|
||||||
|
assert_eq!(my_tag.closing, Some("endmytag".to_string()));
|
||||||
|
|
||||||
|
let branches = my_tag
|
||||||
|
.branches
|
||||||
|
.as_ref()
|
||||||
|
.expect("mytag should have branches");
|
||||||
|
assert!(branches.iter().any(|b| b == "mybranch"));
|
||||||
|
|
||||||
|
let args = my_tag.args.as_ref().expect("mytag should have args");
|
||||||
|
let arg = &args[0];
|
||||||
|
assert_eq!(arg.name, "myarg");
|
||||||
|
assert!(arg.required);
|
||||||
|
|
||||||
|
dir.close()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_file_priority() -> Result<(), anyhow::Error> {
|
||||||
|
let dir = tempfile::tempdir()?;
|
||||||
|
let root = dir.path();
|
||||||
|
|
||||||
|
let djls_content = r#"
|
||||||
|
[mytag1]
|
||||||
|
type = "container"
|
||||||
|
closing = "endmytag1"
|
||||||
|
"#;
|
||||||
|
fs::write(root.join("djls.toml"), djls_content)?;
|
||||||
|
|
||||||
|
let pyproject_content = r#"
|
||||||
|
[tool.djls.template.tags]
|
||||||
|
mytag2.type = "container"
|
||||||
|
mytag2.closing = "endmytag2"
|
||||||
|
"#;
|
||||||
|
fs::write(root.join("pyproject.toml"), pyproject_content)?;
|
||||||
|
|
||||||
|
let specs = TagSpecs::load_user_specs(root)?;
|
||||||
|
|
||||||
|
assert!(specs.get("mytag1").is_some(), "mytag1 should be present");
|
||||||
|
assert!(
|
||||||
|
specs.get("mytag2").is_none(),
|
||||||
|
"mytag2 should not be present"
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_file(root.join("djls.toml"))?;
|
||||||
|
let specs = TagSpecs::load_user_specs(root)?;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
specs.get("mytag1").is_none(),
|
||||||
|
"mytag1 should not be present"
|
||||||
|
);
|
||||||
|
assert!(specs.get("mytag2").is_some(), "mytag2 should be present");
|
||||||
|
|
||||||
|
dir.close()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fmt;
|
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, PartialEq)]
|
||||||
|
@ -33,57 +32,10 @@ impl TokenType {
|
||||||
| TokenType::StyleTagOpen(s)
|
| TokenType::StyleTagOpen(s)
|
||||||
| TokenType::StyleTagClose(s)
|
| TokenType::StyleTagClose(s)
|
||||||
| TokenType::Text(s) => Some(s.len()),
|
| TokenType::Text(s) => Some(s.len()),
|
||||||
TokenType::Comment(content, start, end) => {
|
TokenType::Comment(content, _, _) => Some(content.len()),
|
||||||
Some(content.len() + start.len() + end.as_ref().map_or(0, |e| e.len()))
|
TokenType::Whitespace(n) => Some(*n),
|
||||||
}
|
|
||||||
TokenType::Whitespace(len) => Some(len.clone()),
|
|
||||||
TokenType::Newline => Some(1),
|
TokenType::Newline => Some(1),
|
||||||
TokenType::Eof => None,
|
TokenType::Eof => Some(0),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lexeme(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
TokenType::DjangoBlock(s)
|
|
||||||
| TokenType::DjangoVariable(s)
|
|
||||||
| TokenType::HtmlTagOpen(s)
|
|
||||||
| TokenType::HtmlTagClose(s)
|
|
||||||
| TokenType::HtmlTagVoid(s)
|
|
||||||
| TokenType::ScriptTagOpen(s)
|
|
||||||
| TokenType::ScriptTagClose(s)
|
|
||||||
| TokenType::StyleTagOpen(s)
|
|
||||||
| TokenType::StyleTagClose(s)
|
|
||||||
| TokenType::Text(s) => s,
|
|
||||||
TokenType::Comment(content, _, _) => content, // Just return the content
|
|
||||||
TokenType::Whitespace(_) => " ",
|
|
||||||
TokenType::Newline => "\n",
|
|
||||||
TokenType::Eof => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for TokenType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
use TokenType::*;
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Comment(content, start, end) => match end {
|
|
||||||
Some(end) => write!(f, "{}{}{}", start, content, end),
|
|
||||||
None => write!(f, "{}{}", start, content),
|
|
||||||
},
|
|
||||||
DjangoBlock(s) => write!(f, "{{% {} %}}", s),
|
|
||||||
DjangoVariable(s) => write!(f, "{{{{ {} }}}}", s),
|
|
||||||
Eof => Ok(()),
|
|
||||||
HtmlTagOpen(s) => write!(f, "<{}>", s),
|
|
||||||
HtmlTagClose(s) => write!(f, "</{}>", s),
|
|
||||||
HtmlTagVoid(s) => write!(f, "<{}/>", s),
|
|
||||||
Newline => f.write_str("\n"),
|
|
||||||
ScriptTagOpen(s) => write!(f, "<script{}>", s),
|
|
||||||
ScriptTagClose(_) => f.write_str("</script>"),
|
|
||||||
StyleTagOpen(s) => write!(f, "<style{}>", s),
|
|
||||||
StyleTagClose(_) => f.write_str("</style>"),
|
|
||||||
Text(s) => f.write_str(s),
|
|
||||||
Whitespace(len) => f.write_str(&" ".repeat(*len)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,21 +56,61 @@ impl Token {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lexeme_from_source<'a>(&self, source: &'a str) -> Option<&'a str> {
|
pub fn lexeme(&self) -> String {
|
||||||
match (self.start, self.token_type.len()) {
|
match &self.token_type {
|
||||||
(Some(start), Some(len)) => Some(&source[start..start + len]),
|
TokenType::Comment(_, start, end) => match end {
|
||||||
_ => None,
|
Some(end) => format!("{} {} {}", start, self.content(), end),
|
||||||
|
None => format!("{} {}", start, self.content()),
|
||||||
|
},
|
||||||
|
TokenType::DjangoBlock(_) => format!("{{% {} %}}", self.content()),
|
||||||
|
TokenType::DjangoVariable(_) => format!("{{{{ {} }}}}", self.content()),
|
||||||
|
TokenType::Eof => String::new(),
|
||||||
|
TokenType::HtmlTagOpen(_)
|
||||||
|
| TokenType::ScriptTagOpen(_)
|
||||||
|
| TokenType::StyleTagOpen(_) => format!("<{}>", self.content()),
|
||||||
|
TokenType::HtmlTagClose(_)
|
||||||
|
| TokenType::StyleTagClose(_)
|
||||||
|
| TokenType::ScriptTagClose(_) => format!("</{}>", self.content()),
|
||||||
|
TokenType::HtmlTagVoid(_) => format!("<{}/>", self.content()),
|
||||||
|
TokenType::Newline | TokenType::Text(_) | TokenType::Whitespace(_) => self.content(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lexeme(&self) -> &str {
|
pub fn content(&self) -> String {
|
||||||
self.token_type.lexeme()
|
match &self.token_type {
|
||||||
|
TokenType::Comment(s, _, _)
|
||||||
|
| TokenType::DjangoBlock(s)
|
||||||
|
| TokenType::DjangoVariable(s)
|
||||||
|
| TokenType::Text(s)
|
||||||
|
| TokenType::HtmlTagOpen(s)
|
||||||
|
| TokenType::HtmlTagClose(s)
|
||||||
|
| TokenType::HtmlTagVoid(s)
|
||||||
|
| TokenType::ScriptTagOpen(s)
|
||||||
|
| TokenType::ScriptTagClose(s)
|
||||||
|
| TokenType::StyleTagOpen(s)
|
||||||
|
| TokenType::StyleTagClose(s) => s.to_string(),
|
||||||
|
TokenType::Whitespace(len) => " ".repeat(*len),
|
||||||
|
TokenType::Newline => "\n".to_string(),
|
||||||
|
TokenType::Eof => "".to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn token_type(&self) -> &TokenType {
|
pub fn token_type(&self) -> &TokenType {
|
||||||
&self.token_type
|
&self.token_type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn line(&self) -> &usize {
|
||||||
|
&self.line
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&self) -> Option<u32> {
|
||||||
|
self.start.map(|s| s as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn length(&self) -> Option<u32> {
|
||||||
|
self.token_type.len().map(|l| l as u32)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_token_type(&self, token_type: &TokenType) -> bool {
|
pub fn is_token_type(&self, token_type: &TokenType) -> bool {
|
||||||
&self.token_type == token_type
|
&self.token_type == token_type
|
||||||
}
|
}
|
||||||
|
|
89
crates/djls-template-ast/tagspecs/README.md
Normal file
89
crates/djls-template-ast/tagspecs/README.md
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# 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
|
||||||
|
[package.module.path.tag_name] # Path where tag is registered, e.g., django.template.defaulttags
|
||||||
|
type = "container" | "inclusion" | "single"
|
||||||
|
closing = "closing_tag_name" # For block tags that require a closing tag
|
||||||
|
branches = ["branch_tag_name", ...] # For block tags that support branches
|
||||||
|
|
||||||
|
# Arguments can be positional (matched by order) or keyword (matched by name)
|
||||||
|
args = [
|
||||||
|
# Positional argument (position inferred from array index)
|
||||||
|
{ name = "setting", required = true, allowed_values = ["on", "off"] },
|
||||||
|
# Keyword argument
|
||||||
|
{ name = "key", required = false, is_kwarg = true }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The `name` field in args should match the internal name used in Django's node implementation. For example, the `autoescape` tag's argument is stored as `setting` in Django's `AutoEscapeControlNode`.
|
||||||
|
|
||||||
|
## Tag Types
|
||||||
|
|
||||||
|
- `container`: Tags that wrap content and require a closing tag
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% if condition %}content{% endif %}
|
||||||
|
{% for item in items %}content{% endfor %}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `inclusion`: Tags that include or extend templates.
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% include "partial.html" %}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `single`: Single tags that don't wrap content
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% csrf_token %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
[django.template.defaulttags.if]
|
||||||
|
type = "container"
|
||||||
|
closing = "endif"
|
||||||
|
branches = ["elif", "else"]
|
||||||
|
args = [{ name = "condition", required = true }]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Include Tag
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[django.template.defaulttags.includes]
|
||||||
|
type = "inclusion"
|
||||||
|
args = [{ name = "template_name", required = true }]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Autoescape Tag
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[django.template.defaulttags.autoescape]
|
||||||
|
type = "container"
|
||||||
|
closing = "endautoescape"
|
||||||
|
args = [{ name = "setting", required = true, allowed_values = ["on", "off"] }]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Tag with Kwargs
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[my_module.templatetags.my_tags.my_custom_tag]
|
||||||
|
type = "single"
|
||||||
|
args = [
|
||||||
|
{ name = "arg1", required = true },
|
||||||
|
{ name = "kwarg1", required = false, is_kwarg = true }
|
||||||
|
]
|
||||||
|
```
|
104
crates/djls-template-ast/tagspecs/django.toml
Normal file
104
crates/djls-template-ast/tagspecs/django.toml
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
[django.template.defaulttags.autoescape]
|
||||||
|
args = [{ name = "setting", required = true, allowed_values = ["on", "off"] }]
|
||||||
|
closing = "endautoescape"
|
||||||
|
type = "container"
|
||||||
|
|
||||||
|
[django.template.defaulttags.block]
|
||||||
|
closing = "endblock"
|
||||||
|
type = "container"
|
||||||
|
|
||||||
|
[django.template.defaulttags.comment]
|
||||||
|
closing = "endcomment"
|
||||||
|
type = "container"
|
||||||
|
|
||||||
|
[django.template.defaulttags.cycle]
|
||||||
|
args = [
|
||||||
|
{ name = "cyclevars", required = true },
|
||||||
|
{ name = "variable_name", required = false, is_kwarg = true },
|
||||||
|
]
|
||||||
|
type = "single"
|
||||||
|
|
||||||
|
[django.template.defaulttags.debug]
|
||||||
|
type = "single"
|
||||||
|
|
||||||
|
[django.template.defaulttags.extends]
|
||||||
|
args = [{ name = "parent_name", required = true }]
|
||||||
|
type = "inclusion"
|
||||||
|
|
||||||
|
[django.template.defaulttags.for]
|
||||||
|
args = [
|
||||||
|
{ name = "{item}", required = true },
|
||||||
|
{ name = "in", required = true },
|
||||||
|
{ name = "{iterable}", required = true },
|
||||||
|
]
|
||||||
|
branches = ["empty"]
|
||||||
|
closing = "endfor"
|
||||||
|
type = "container"
|
||||||
|
|
||||||
|
[django.template.defaulttags.filter]
|
||||||
|
args = [{ name = "filter_expr", required = true }]
|
||||||
|
closing = "endfilter"
|
||||||
|
type = "container"
|
||||||
|
|
||||||
|
[django.template.defaulttags.firstof]
|
||||||
|
args = [{ name = "variables", required = true }]
|
||||||
|
type = "single"
|
||||||
|
|
||||||
|
[django.template.defaulttags.if]
|
||||||
|
args = [{ name = "condition", required = true }]
|
||||||
|
branches = ["elif", "else"]
|
||||||
|
closing = "endif"
|
||||||
|
type = "container"
|
||||||
|
|
||||||
|
[django.template.defaulttags.include]
|
||||||
|
args = [
|
||||||
|
{ name = "template", required = true },
|
||||||
|
{ name = "with", required = false, is_kwarg = true },
|
||||||
|
{ name = "only", required = false, is_kwarg = true },
|
||||||
|
]
|
||||||
|
type = "inclusion"
|
||||||
|
|
||||||
|
[django.template.defaulttags.load]
|
||||||
|
args = [{ name = "library", required = true }]
|
||||||
|
type = "single"
|
||||||
|
|
||||||
|
[django.template.defaulttags.now]
|
||||||
|
args = [{ name = "format_string", required = true }]
|
||||||
|
type = "single"
|
||||||
|
|
||||||
|
[django.template.defaulttags.spaceless]
|
||||||
|
closing = "endspaceless"
|
||||||
|
type = "container"
|
||||||
|
|
||||||
|
[django.template.defaulttags.templatetag]
|
||||||
|
type = "single"
|
||||||
|
|
||||||
|
[[django.template.defaulttags.templatetag.args]]
|
||||||
|
allowed_values = [
|
||||||
|
"openblock",
|
||||||
|
"closeblock",
|
||||||
|
"openvariable",
|
||||||
|
"closevariable",
|
||||||
|
"openbrace",
|
||||||
|
"closebrace",
|
||||||
|
"opencomment",
|
||||||
|
"closecomment",
|
||||||
|
]
|
||||||
|
name = "tagtype"
|
||||||
|
required = true
|
||||||
|
|
||||||
|
[django.template.defaulttags.url]
|
||||||
|
args = [
|
||||||
|
{ name = "view_name", required = true },
|
||||||
|
{ name = "asvar", required = false, is_kwarg = true },
|
||||||
|
]
|
||||||
|
type = "single"
|
||||||
|
|
||||||
|
[django.template.defaulttags.verbatim]
|
||||||
|
closing = "endverbatim"
|
||||||
|
type = "container"
|
||||||
|
|
||||||
|
[django.template.defaulttags.with]
|
||||||
|
args = [{ name = "extra_context", required = true }]
|
||||||
|
closing = "endwith"
|
||||||
|
type = "container"
|
|
@ -68,6 +68,10 @@ impl Worker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempts to execute a task immediately without waiting.
|
||||||
|
/// Returns an error if the worker's channel is full.
|
||||||
|
///
|
||||||
|
/// Best for non-critical tasks where backpressure is desired.
|
||||||
pub fn execute<T>(&self, task: T) -> Result<()>
|
pub fn execute<T>(&self, task: T) -> Result<()>
|
||||||
where
|
where
|
||||||
T: Task + 'static,
|
T: Task + 'static,
|
||||||
|
@ -78,6 +82,10 @@ impl Worker {
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to execute task: {}", e))
|
.map_err(|e| anyhow::anyhow!("Failed to execute task: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Submits a task asynchronously, waiting if the channel is full.
|
||||||
|
///
|
||||||
|
/// Good for tasks that must be processed but where you don't need
|
||||||
|
/// the result immediately.
|
||||||
pub async fn submit<T>(&self, task: T) -> Result<()>
|
pub async fn submit<T>(&self, task: T) -> Result<()>
|
||||||
where
|
where
|
||||||
T: Task + 'static,
|
T: Task + 'static,
|
||||||
|
@ -89,6 +97,10 @@ impl Worker {
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to submit task: {}", e))
|
.map_err(|e| anyhow::anyhow!("Failed to submit task: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Submits a task and waits for its result.
|
||||||
|
///
|
||||||
|
/// Best when you need the output of the task. This method will
|
||||||
|
/// wait both for space to submit the task and for its completion.
|
||||||
pub async fn wait_for<T>(&self, task: T) -> Result<T::Output>
|
pub async fn wait_for<T>(&self, task: T) -> Result<T::Output>
|
||||||
where
|
where
|
||||||
T: Task + 'static,
|
T: Task + 'static,
|
||||||
|
|
|
@ -143,3 +143,6 @@ required-imports = ["from __future__ import annotations"]
|
||||||
[tool.ruff.lint.pyupgrade]
|
[tool.ruff.lint.pyupgrade]
|
||||||
# Preserve types, even if a file imports `from __future__ import annotations`.
|
# Preserve types, even if a file imports `from __future__ import annotations`.
|
||||||
keep-runtime-typing = true
|
keep-runtime-typing = true
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
required-version = "<0.7"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue