add typed argspecs for LSP snippets to tagspecs config (#206)

This commit is contained in:
Josh Thomas 2025-09-09 00:16:37 -05:00 committed by GitHub
parent 03f18d2211
commit 792bdd8e13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 602 additions and 122 deletions

View file

@ -4,6 +4,8 @@
//! and generating appropriate completion items for Django templates.
use djls_project::TemplateTags;
use djls_templates::templatetags::generate_snippet_for_tag;
use djls_templates::templatetags::TagSpecs;
use djls_workspace::FileKind;
use djls_workspace::PositionEncoding;
use djls_workspace::TextDocument;
@ -57,6 +59,8 @@ pub fn handle_completion(
encoding: PositionEncoding,
file_kind: FileKind,
template_tags: Option<&TemplateTags>,
tag_specs: Option<&TagSpecs>,
supports_snippets: bool,
) -> Vec<CompletionItem> {
// Only handle template files
if file_kind != FileKind::Template {
@ -74,7 +78,7 @@ pub fn handle_completion(
};
// Generate completions based on available template tags
generate_template_completions(&context, template_tags)
generate_template_completions(&context, template_tags, tag_specs, supports_snippets)
}
/// Extract line information from document at given position
@ -126,49 +130,49 @@ fn get_line_info(
/// Analyze a line of template text to determine completion context
fn analyze_template_context(line: &str, cursor_offset: usize) -> Option<TemplateTagContext> {
if cursor_offset > line.chars().count() {
return None;
}
// Find the last {% before cursor position
let prefix = &line[..cursor_offset.min(line.len())];
let tag_start = prefix.rfind("{%")?;
let chars: Vec<char> = line.chars().collect();
let prefix = chars[..cursor_offset].iter().collect::<String>();
let rest_of_line = chars[cursor_offset..].iter().collect::<String>();
let rest_trimmed = rest_of_line.trim_start();
// Get the content between {% and cursor
let content_start = tag_start + 2;
let content = &prefix[content_start..];
prefix.rfind("{%").map(|tag_start| {
let closing_brace = if rest_trimmed.starts_with("%}") {
ClosingBrace::FullClose
} else if rest_trimmed.starts_with('}') {
ClosingBrace::PartialClose
} else {
ClosingBrace::None
};
// Check if we need a leading space (no space after {%)
let needs_leading_space = content.is_empty() || !content.starts_with(' ');
let partial_tag_start = tag_start + 2; // Skip "{%"
let content_after_tag = if partial_tag_start < prefix.len() {
&prefix[partial_tag_start..]
} else {
""
};
// Extract the partial tag name
let partial_tag = content.trim_start().to_string();
// Check if we need a leading space - true if there's no space after {%
let needs_leading_space =
!content_after_tag.starts_with(' ') && !content_after_tag.is_empty();
// Check what's after the cursor for closing detection
let suffix = &line[cursor_offset.min(line.len())..];
let closing_brace = detect_closing_brace(suffix);
let partial_tag = content_after_tag.trim().to_string();
TemplateTagContext {
partial_tag,
closing_brace,
needs_leading_space,
}
Some(TemplateTagContext {
partial_tag,
closing_brace,
needs_leading_space,
})
}
/// Detect what closing brace is present after the cursor
fn detect_closing_brace(suffix: &str) -> ClosingBrace {
let trimmed = suffix.trim_start();
if trimmed.starts_with("%}") {
ClosingBrace::FullClose
} else if trimmed.starts_with('}') {
ClosingBrace::PartialClose
} else {
ClosingBrace::None
}
}
/// Generate Django template tag completion items based on context
fn generate_template_completions(
context: &TemplateTagContext,
template_tags: Option<&TemplateTags>,
tag_specs: Option<&TagSpecs>,
supports_snippets: bool,
) -> Vec<CompletionItem> {
let Some(tags) = template_tags else {
return Vec::new();
@ -177,25 +181,47 @@ fn generate_template_completions(
let mut completions = Vec::new();
for tag in tags.iter() {
// Filter tags based on partial match
if tag.name().starts_with(&context.partial_tag) {
// Determine insertion text based on context
let mut insert_text = String::new();
// Try to get snippet from TagSpecs if available and client supports snippets
let (insert_text, insert_format) = if supports_snippets {
if let Some(specs) = tag_specs {
if let Some(spec) = specs.get(tag.name()) {
if spec.args.is_empty() {
// No args, use plain text
build_plain_insert(tag.name(), context)
} else {
// Generate snippet from tag spec
let mut text = String::new();
// Add leading space if needed (cursor right after {%)
if context.needs_leading_space {
insert_text.push(' ');
}
// Add leading space if needed
if context.needs_leading_space {
text.push(' ');
}
// Add the tag name
insert_text.push_str(tag.name());
// Add tag name and snippet arguments
text.push_str(&generate_snippet_for_tag(tag.name(), spec));
// Add closing based on what's already present
match context.closing_brace {
ClosingBrace::None => insert_text.push_str(" %}"),
ClosingBrace::PartialClose => insert_text.push('%'),
ClosingBrace::FullClose => {} // No closing needed
}
// Add closing based on what's already present
match context.closing_brace {
ClosingBrace::None => text.push_str(" %}"),
ClosingBrace::PartialClose => text.push('%'),
ClosingBrace::FullClose => {} // No closing needed
}
(text, InsertTextFormat::SNIPPET)
}
} else {
// No spec found, use plain text
build_plain_insert(tag.name(), context)
}
} else {
// No specs available, use plain text
build_plain_insert(tag.name(), context)
}
} else {
// Client doesn't support snippets
build_plain_insert(tag.name(), context)
};
// Create completion item
let completion_item = CompletionItem {
@ -204,7 +230,7 @@ fn generate_template_completions(
detail: Some(format!("from {}", tag.library())),
documentation: tag.doc().map(|doc| Documentation::String(doc.clone())),
insert_text: Some(insert_text),
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
insert_text_format: Some(insert_format),
filter_text: Some(tag.name().clone()),
..Default::default()
};
@ -216,6 +242,28 @@ fn generate_template_completions(
completions
}
/// Build plain insert text without snippets
fn build_plain_insert(tag_name: &str, context: &TemplateTagContext) -> (String, InsertTextFormat) {
let mut insert_text = String::new();
// Add leading space if needed (cursor right after {%)
if context.needs_leading_space {
insert_text.push(' ');
}
// Add the tag name
insert_text.push_str(tag_name);
// Add closing based on what's already present
match context.closing_brace {
ClosingBrace::None => insert_text.push_str(" %}"),
ClosingBrace::PartialClose => insert_text.push('%'),
ClosingBrace::FullClose => {} // No closing needed
}
(insert_text, InsertTextFormat::PLAIN_TEXT)
}
#[cfg(test)]
mod tests {
use super::*;
@ -286,7 +334,7 @@ mod tests {
closing_brace: ClosingBrace::None,
};
let completions = generate_template_completions(&context, None);
let completions = generate_template_completions(&context, None, None, false);
assert!(completions.is_empty());
}

View file

@ -370,6 +370,8 @@ impl LanguageServer for DjangoLanguageServer {
let encoding = session.position_encoding();
let file_kind = FileKind::from_path(&path);
let template_tags = session.project().and_then(|p| p.template_tags());
let tag_specs = session.with_db(djls_templates::Db::tag_specs);
let supports_snippets = true; // TODO: Get from client capabilities
let completions = crate::completions::handle_completion(
&document,
@ -377,6 +379,8 @@ impl LanguageServer for DjangoLanguageServer {
encoding,
file_kind,
template_tags,
Some(&tag_specs),
supports_snippets,
);
if completions.is_empty() {

View file

@ -1,6 +1,11 @@
mod snippets;
mod specs;
pub use specs::ArgSpec;
pub use snippets::generate_snippet_for_tag;
pub use snippets::generate_snippet_from_args;
pub use specs::Arg;
pub use specs::ArgType;
pub use specs::SimpleArgType;
pub use specs::TagSpecs;
pub enum TagType {

View file

@ -0,0 +1,194 @@
use super::specs::Arg;
use super::specs::ArgType;
use super::specs::SimpleArgType;
use super::specs::TagSpec;
/// Generate an LSP snippet pattern from an array of arguments
#[must_use]
pub fn generate_snippet_from_args(args: &[Arg]) -> String {
let mut parts = Vec::new();
let mut placeholder_index = 1;
for arg in args {
// Skip optional args if we haven't seen any required args after them
// This prevents generating snippets like: "{% for %}" when everything is optional
if !arg.required && parts.is_empty() {
continue;
}
let snippet_part = match &arg.arg_type {
ArgType::Simple(simple_type) => match simple_type {
SimpleArgType::Literal => {
if arg.required {
// Required literals are just plain text (e.g., "in", "as", "by")
arg.name.clone()
} else {
// Optional literals become placeholders
let result = format!("${{{}:{}}}", placeholder_index, arg.name);
placeholder_index += 1;
result
}
}
SimpleArgType::Variable | SimpleArgType::Expression => {
// Variables and expressions become placeholders
let result = format!("${{{}:{}}}", placeholder_index, arg.name);
placeholder_index += 1;
result
}
SimpleArgType::String => {
// Strings get quotes around them
let result = format!("\"${{{}:{}}}\"", placeholder_index, arg.name);
placeholder_index += 1;
result
}
SimpleArgType::Assignment => {
// Assignments use the name as-is (e.g., "var=value")
let result = format!("${{{}:{}}}", placeholder_index, arg.name);
placeholder_index += 1;
result
}
SimpleArgType::VarArgs => {
// Variable arguments, just use the name
let result = format!("${{{}:{}}}", placeholder_index, arg.name);
placeholder_index += 1;
result
}
},
ArgType::Choice { choice } => {
// Choice placeholders with options
let result = format!("${{{}|{}|}}", placeholder_index, choice.join(","));
placeholder_index += 1;
result
}
};
parts.push(snippet_part);
}
parts.join(" ")
}
/// Generate a complete LSP snippet for a tag including the tag name
#[must_use]
pub fn generate_snippet_for_tag(tag_name: &str, spec: &TagSpec) -> String {
let args_snippet = generate_snippet_from_args(&spec.args);
if args_snippet.is_empty() {
// Tag with no arguments
tag_name.to_string()
} else {
// Tag with arguments
format!("{tag_name} {args_snippet}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::templatetags::specs::ArgType;
use crate::templatetags::specs::SimpleArgType;
#[test]
fn test_snippet_for_for_tag() {
let args = vec![
Arg {
name: "item".to_string(),
required: true,
arg_type: ArgType::Simple(SimpleArgType::Variable),
},
Arg {
name: "in".to_string(),
required: true,
arg_type: ArgType::Simple(SimpleArgType::Literal),
},
Arg {
name: "items".to_string(),
required: true,
arg_type: ArgType::Simple(SimpleArgType::Variable),
},
Arg {
name: "reversed".to_string(),
required: false,
arg_type: ArgType::Simple(SimpleArgType::Literal),
},
];
let snippet = generate_snippet_from_args(&args);
assert_eq!(snippet, "${1:item} in ${2:items} ${3:reversed}");
}
#[test]
fn test_snippet_for_if_tag() {
let args = vec![Arg {
name: "condition".to_string(),
required: true,
arg_type: ArgType::Simple(SimpleArgType::Expression),
}];
let snippet = generate_snippet_from_args(&args);
assert_eq!(snippet, "${1:condition}");
}
#[test]
fn test_snippet_for_autoescape_tag() {
let args = vec![Arg {
name: "mode".to_string(),
required: true,
arg_type: ArgType::Choice {
choice: vec!["on".to_string(), "off".to_string()],
},
}];
let snippet = generate_snippet_from_args(&args);
assert_eq!(snippet, "${1|on,off|}");
}
#[test]
fn test_snippet_for_extends_tag() {
let args = vec![Arg {
name: "template".to_string(),
required: true,
arg_type: ArgType::Simple(SimpleArgType::String),
}];
let snippet = generate_snippet_from_args(&args);
assert_eq!(snippet, "\"${1:template}\"");
}
#[test]
fn test_snippet_for_csrf_token_tag() {
let args = vec![];
let snippet = generate_snippet_from_args(&args);
assert_eq!(snippet, "");
}
#[test]
fn test_snippet_for_url_tag() {
let args = vec![
Arg {
name: "view_name".to_string(),
required: true,
arg_type: ArgType::Simple(SimpleArgType::String),
},
Arg {
name: "args".to_string(),
required: false,
arg_type: ArgType::Simple(SimpleArgType::VarArgs),
},
Arg {
name: "as".to_string(),
required: false,
arg_type: ArgType::Simple(SimpleArgType::Literal),
},
Arg {
name: "varname".to_string(),
required: false,
arg_type: ArgType::Simple(SimpleArgType::Variable),
},
];
let snippet = generate_snippet_from_args(&args);
assert_eq!(snippet, "\"${1:view_name}\" ${2:args} ${3:as} ${4:varname}");
}
}

View file

@ -207,9 +207,41 @@ pub struct TagSpec {
#[serde(default, alias = "intermediates")]
pub intermediate_tags: Option<Vec<IntermediateTag>>,
#[serde(default)]
pub args: Option<ArgSpec>,
pub args: Vec<Arg>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Arg {
pub name: String,
#[serde(default = "default_true")]
pub required: bool,
#[serde(rename = "type")]
pub arg_type: ArgType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ArgType {
Simple(SimpleArgType),
Choice { choice: Vec<String> },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SimpleArgType {
Literal,
Variable,
String,
Expression,
Assignment,
VarArgs,
}
fn default_true() -> bool {
true
}
// Keep ArgSpec for backward compatibility in EndTag
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EndTag {
#[serde(alias = "tag")]
@ -217,7 +249,7 @@ pub struct EndTag {
#[serde(default)]
pub optional: bool,
#[serde(default)]
pub args: Option<ArgSpec>,
pub args: Vec<Arg>,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
@ -244,14 +276,6 @@ impl<'de> Deserialize<'de> for IntermediateTag {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ArgSpec {
#[serde(default)]
pub min: Option<usize>,
#[serde(default)]
pub max: Option<usize>,
}
impl TagSpec {
/// Recursive extraction: Check if node is spec, otherwise recurse if table.
#[allow(dead_code)]
@ -405,15 +429,17 @@ mod tests {
assert!(specs.get(tag).is_some(), "{tag} tag should be present");
}
// Check that newly added tags are present
let additional_tags = ["debug", "firstof", "lorem", "regroup", "widthratio"];
for tag in additional_tags {
assert!(specs.get(tag).is_some(), "{tag} tag should be present");
}
// Check that some tags are still missing
let missing_tags = [
"debug",
"firstof",
"lorem",
"querystring", // 5.1
"regroup",
"querystring", // Django 5.1+
"resetcycle",
"widthratio",
];
for tag in missing_tags {
@ -452,7 +478,7 @@ end = { tag = "endanothertag", optional = true }
Some(EndTag {
name: "endmytag".to_string(),
optional: false,
args: None,
args: vec![],
})
);
assert_eq!(
@ -470,7 +496,25 @@ end = { tag = "endanothertag", optional = true }
Some(EndTag {
name: "endanothertag".to_string(),
optional: true,
args: None,
args: vec![],
})
);
assert_eq!(
my_tag.intermediate_tags,
Some(vec![IntermediateTag {
name: "mybranch".to_string()
}])
);
let another_tag = specs
.get("anothertag")
.expect("anothertag should be present");
assert_eq!(
another_tag.end_tag,
Some(EndTag {
name: "endanothertag".to_string(),
optional: true,
args: vec![],
})
);
assert!(

View file

@ -23,7 +23,9 @@ use crate::ast::Span;
use crate::ast::TagName;
use crate::ast::TagNode;
use crate::db::Db as TemplateDb;
use crate::templatetags::ArgSpec;
use crate::templatetags::Arg;
use crate::templatetags::ArgType;
use crate::templatetags::SimpleArgType;
use crate::templatetags::TagType;
use crate::Ast;
@ -57,14 +59,14 @@ impl<'db> TagValidator<'db> {
let tag_specs = self.db.tag_specs();
let tag_type = TagType::for_name(&name_str, &tag_specs);
let arg_spec = match tag_type {
let args = match tag_type {
TagType::Closer => tag_specs
.get_end_spec_for_closer(&name_str)
.and_then(|s| s.args.as_ref()),
_ => tag_specs.get(&name_str).and_then(|s| s.args.as_ref()),
.map(|s| &s.args),
_ => tag_specs.get(&name_str).map(|s| &s.args),
};
self.check_arguments(&name_str, &bits, span, arg_spec);
self.check_arguments(&name_str, &bits, span, args);
match tag_type {
TagType::Opener => {
@ -104,30 +106,34 @@ impl<'db> TagValidator<'db> {
name: &str,
bits: &[String],
span: Span,
arg_spec: Option<&ArgSpec>,
args: Option<&Vec<Arg>>,
) {
let Some(arg_spec) = arg_spec else {
let Some(args) = args else {
return;
};
if let Some(min) = arg_spec.min {
if bits.len() < min {
self.errors.push(AstError::MissingRequiredArguments {
tag: name.to_string(),
min,
span,
});
}
// Count required arguments
let required_count = args.iter().filter(|arg| arg.required).count();
if bits.len() < required_count {
self.errors.push(AstError::MissingRequiredArguments {
tag: name.to_string(),
min: required_count,
span,
});
}
if let Some(max) = arg_spec.max {
if bits.len() > max {
self.errors.push(AstError::TooManyArguments {
tag: name.to_string(),
max,
span,
});
}
// If there are more bits than defined args, that might be okay for varargs
let has_varargs = args
.iter()
.any(|arg| matches!(arg.arg_type, ArgType::Simple(SimpleArgType::VarArgs)));
if !has_varargs && bits.len() > args.len() {
self.errors.push(AstError::TooManyArguments {
tag: name.to_string(),
max: args.len(),
span,
});
}
}

View file

@ -4,29 +4,46 @@ Tag Specifications (TagSpecs) define how template tags are structured, helping t
## Schema
Tag Specifications (TagSpecs) define how tags are parsed and understood. They allow the parser to handle custom tags without hard-coding them.
Tag Specifications (TagSpecs) define how tags are parsed and understood. They allow the parser to handle custom tags without hard-coding them and provide rich autocompletion with LSP snippets.
```toml
[[path.to.module]] # Array of tables for the module, e.g., tagspecs.django.template.defaulttags
name = "tag_name" # The tag name (e.g., "if", "for", "my_custom_tag")
end_tag = { name = "end_tag_name", optional = false } # Optional: Defines the closing tag
intermediate_tags = [{ name = "tag_name" }, ...] # Optional: Defines intermediate tags
args = { min = 1, max = 3 } # Optional: Argument constraints
args = [ # Defines tag arguments for validation and snippets
{ name = "arg_name", type = "arg_type", required = true }
]
```
### Core Fields
The `name` field specifies the tag name (e.g., "if", "for", "my_custom_tag").
The `end_tag` table defines the closing tag for a block tag.
- `name`: The name of the closing tag (e.g., "endif").
- `optional`: Whether the closing tag is optional (defaults to `false`).
- `args`: Optional argument constraints for the end tag.
The `end_tag` table defines the closing tag for a block tag:
- `name`: The name of the closing tag (e.g., "endif")
- `optional`: Whether the closing tag is optional (defaults to `false`)
- `args`: Optional array of arguments for the end tag (e.g., endblock can take a name)
The `intermediate_tags` array lists tags that can appear between the opening and closing tags. Each intermediate tag is an object with:
- `name`: The name of the intermediate tag (e.g., "else", "elif").
- `name`: The name of the intermediate tag (e.g., "else", "elif")
The `args` table defines argument constraints:
- `min`: Minimum number of arguments required.
- `max`: Maximum number of arguments allowed.
### Argument Specification
The `args` array defines the expected arguments for a tag. Each argument has:
- `name`: The argument name (used as placeholder text in LSP snippets)
- `type`: The argument type (see below)
- `required`: Whether the argument is required (defaults to `true`)
#### Argument Types
- `"literal"`: A literal keyword that must appear exactly (e.g., "in", "as", "by")
- `"variable"`: A template variable name
- `"string"`: A string literal (will be wrapped in quotes in snippets)
- `"expression"`: A template expression or condition
- `"assignment"`: A variable assignment (e.g., "var=value")
- `"varargs"`: Variable number of arguments
- `{ choice = ["option1", "option2"] }`: A choice from specific options (generates choice snippets)
## Configuration
@ -42,7 +59,10 @@ The `args` table defines argument constraints:
name = "if"
end_tag = { name = "endif" }
intermediate_tags = [{ name = "elif" }, { name = "else" }]
args = { min = 1 } # condition
args = [
{ name = "condition", type = "expression" }
]
# Generates snippet: if ${1:condition}
```
### For Tag
@ -52,7 +72,13 @@ args = { min = 1 } # condition
name = "for"
end_tag = { name = "endfor" }
intermediate_tags = [{ name = "empty" }]
args = { min = 3 } # item in items (at minimum)
args = [
{ name = "item", type = "variable" },
{ name = "in", type = "literal" },
{ name = "items", type = "variable" },
{ name = "reversed", required = false, type = "literal" }
]
# Generates snippet: for ${1:item} in ${2:items} ${3:reversed}
```
### Autoescape Tag
@ -61,7 +87,24 @@ args = { min = 3 } # item in items (at minimum)
[[tagspecs.django.template.defaulttags]]
name = "autoescape"
end_tag = { name = "endautoescape" }
args = { min = 1, max = 1 } # on or off
args = [
{ name = "mode", type = { choice = ["on", "off"] } }
]
# Generates snippet: autoescape ${1|on,off|}
```
### URL Tag with Optional Arguments
```toml
[[tagspecs.django.template.defaulttags]]
name = "url"
args = [
{ name = "view_name", type = "string" },
{ name = "args", required = false, type = "varargs" },
{ name = "as", required = false, type = "literal" },
{ name = "varname", required = false, type = "variable" }
]
# Generates snippet: url "${1:view_name}" ${2:args} ${3:as} ${4:varname}
```
### Custom Tag
@ -71,16 +114,26 @@ args = { min = 1, max = 1 } # on or off
name = "my_custom_tag"
end_tag = { name = "endmycustomtag", optional = true }
intermediate_tags = [{ name = "myintermediate" }]
args = [
{ name = "param1", type = "variable" },
{ name = "with", type = "literal" },
{ name = "param2", type = "string" }
]
# Generates snippet: my_custom_tag ${1:param1} with "${2:param2}"
```
### Standalone Tags (no end tag)
### Standalone Tags
```toml
[[tagspecs.django.template.defaulttags]]
name = "csrf_token"
args = { min = 0, max = 0 } # no arguments
args = [] # No arguments
# Generates snippet: csrf_token
[[tagspecs.django.template.defaulttags]]
name = "load"
args = { min = 1 } # library name(s)
args = [
{ name = "libraries", type = "varargs" }
]
# Generates snippet: load ${1:libraries}
```

View file

@ -1,101 +1,227 @@
[[tagspecs.django.template.defaulttags]]
name = "autoescape"
end_tag = { name = "endautoescape" }
args = { min = 1, max = 1 } # on or off
args = [
{ name = "mode", type = { choice = ["on", "off"] } }
]
[[tagspecs.django.template.defaulttags]]
name = "block"
end_tag = { name = "endblock", args = { min = 0, max = 1 } }
args = { min = 1, max = 1 } # block name
end_tag = { name = "endblock", args = [{ name = "name", required = false, type = "variable" }] }
args = [
{ name = "name", type = "variable" }
]
[[tagspecs.django.template.defaulttags]]
name = "comment"
end_tag = { name = "endcomment" }
args = []
[[tagspecs.django.template.defaulttags]]
name = "csrf_token"
args = { min = 0, max = 0 } # no arguments
args = []
[[tagspecs.django.template.defaulttags]]
name = "cycle"
args = { min = 1 } # values to cycle through
args = [
{ name = "values", type = "varargs" },
{ name = "as", required = false, type = "literal" },
{ name = "varname", required = false, type = "variable" }
]
[[tagspecs.django.template.defaulttags]]
name = "extends"
args = { min = 1, max = 1 } # template name
args = [
{ name = "template", type = "string" }
]
[[tagspecs.django.template.defaulttags]]
name = "filter"
end_tag = { name = "endfilter" }
args = { min = 1 } # filter expression
args = [
{ name = "filter_expr", type = "expression" }
]
[[tagspecs.django.template.defaulttags]]
name = "for"
end_tag = { name = "endfor" }
intermediate_tags = [ { name = "empty" } ]
args = { min = 3 } # item in items (at minimum)
args = [
{ name = "item", type = "variable" },
{ name = "in", type = "literal" },
{ name = "items", type = "variable" },
{ name = "reversed", required = false, type = "literal" }
]
[[tagspecs.django.template.defaulttags]]
name = "include"
args = { min = 1 } # template name [with context]
args = [
{ name = "template", type = "string" },
{ name = "with", required = false, type = "literal" },
{ name = "context", required = false, type = "varargs" },
{ name = "only", required = false, type = "literal" }
]
[[tagspecs.django.template.defaulttags]]
name = "if"
end_tag = { name = "endif" }
intermediate_tags = [ { name = "elif" }, { name = "else" } ]
args = { min = 1 } # condition
args = [
{ name = "condition", type = "expression" }
]
[[tagspecs.django.template.defaulttags]]
name = "ifchanged"
end_tag = { name = "endifchanged" }
intermediate_tags = [ { name = "else" } ]
args = [
{ name = "variables", required = false, type = "varargs" }
]
[[tagspecs.django.template.defaulttags]]
name = "load"
args = { min = 1 } # library name(s)
args = [
{ name = "libraries", type = "varargs" }
]
[[tagspecs.django.template.defaulttags]]
name = "now"
args = { min = 1, max = 1 } # format string
args = [
{ name = "format_string", type = "string" },
{ name = "as", required = false, type = "literal" },
{ name = "varname", required = false, type = "variable" }
]
[[tagspecs.django.template.defaulttags]]
name = "regroup"
args = [
{ name = "list", type = "variable" },
{ name = "by", type = "literal" },
{ name = "attribute", type = "variable" },
{ name = "as", type = "literal" },
{ name = "grouped", type = "variable" }
]
[[tagspecs.django.template.defaulttags]]
name = "spaceless"
end_tag = { name = "endspaceless" }
args = []
[[tagspecs.django.template.defaulttags]]
name = "static"
args = [
{ name = "path", type = "string" }
]
[[tagspecs.django.template.defaulttags]]
name = "templatetag"
args = { min = 1, max = 1 } # special character name
args = [
{ name = "tagbit", type = { choice = ["openblock", "closeblock", "openvariable", "closevariable", "openbrace", "closebrace", "opencomment", "closecomment"] } }
]
[[tagspecs.django.template.defaulttags]]
name = "url"
args = { min = 1 } # view name [args...]
args = [
{ name = "view_name", type = "string" },
{ name = "args", required = false, type = "varargs" },
{ name = "as", required = false, type = "literal" },
{ name = "varname", required = false, type = "variable" }
]
[[tagspecs.django.template.defaulttags]]
name = "verbatim"
end_tag = { name = "endverbatim" }
args = [
{ name = "name", required = false, type = "string" }
]
[[tagspecs.django.template.defaulttags]]
name = "widthratio"
args = [
{ name = "this_value", type = "variable" },
{ name = "max_value", type = "variable" },
{ name = "max_width", type = "variable" },
{ name = "as", required = false, type = "literal" },
{ name = "varname", required = false, type = "variable" }
]
[[tagspecs.django.template.defaulttags]]
name = "with"
end_tag = { name = "endwith" }
args = { min = 1 } # variable assignment(s)
args = [
{ name = "assignments", type = "varargs" }
]
# Cache tags
[[tagspecs.django.templatetags.cache]]
name = "cache"
end_tag = { name = "endcache" }
args = [
{ name = "timeout", type = "variable" },
{ name = "cache_key", type = "variable" },
{ name = "variables", required = false, type = "varargs" }
]
# Internationalization tags
[[tagspecs.django.templatetags.i10n]]
name = "localize"
end_tag = { name = "endlocalize" }
args = [
{ name = "mode", required = false, type = { choice = ["on", "off"] } }
]
[[tagspecs.django.templatetags.i18n]]
name = "blocktranslate"
end_tag = { name = "endblocktranslate" }
intermediate_tags = [ { name = "plural" } ]
args = [
{ name = "context", required = false, type = "string" },
{ name = "with", required = false, type = "literal" },
{ name = "assignments", required = false, type = "varargs" },
{ name = "asvar", required = false, type = "literal" },
{ name = "varname", required = false, type = "variable" }
]
[[tagspecs.django.templatetags.i18n]]
name = "trans"
args = [
{ name = "message", type = "string" },
{ name = "context", required = false, type = "string" },
{ name = "as", required = false, type = "literal" },
{ name = "varname", required = false, type = "variable" },
{ name = "noop", required = false, type = "literal" }
]
# Timezone tags
[[tagspecs.django.templatetags.tz]]
name = "localtime"
end_tag = { name = "endlocaltime" }
args = [
{ name = "mode", required = false, type = { choice = ["on", "off"] } }
]
[[tagspecs.django.templatetags.tz]]
name = "timezone"
end_tag = { name = "endtimezone" }
args = [
{ name = "timezone", type = "variable" }
]
# Additional commonly used tags
[[tagspecs.django.template.defaulttags]]
name = "firstof"
args = [
{ name = "variables", type = "varargs" }
]
[[tagspecs.django.template.defaulttags]]
name = "lorem"
args = [
{ name = "count", required = false, type = "variable" },
{ name = "method", required = false, type = { choice = ["w", "p", "b"] } },
{ name = "random", required = false, type = "literal" }
]
[[tagspecs.django.template.defaulttags]]
name = "debug"
args = []