mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-11 12:56:46 +00:00
add typed argspecs for LSP snippets to tagspecs config (#206)
This commit is contained in:
parent
03f18d2211
commit
792bdd8e13
8 changed files with 602 additions and 122 deletions
|
@ -4,6 +4,8 @@
|
||||||
//! and generating appropriate completion items for Django templates.
|
//! and generating appropriate completion items for Django templates.
|
||||||
|
|
||||||
use djls_project::TemplateTags;
|
use djls_project::TemplateTags;
|
||||||
|
use djls_templates::templatetags::generate_snippet_for_tag;
|
||||||
|
use djls_templates::templatetags::TagSpecs;
|
||||||
use djls_workspace::FileKind;
|
use djls_workspace::FileKind;
|
||||||
use djls_workspace::PositionEncoding;
|
use djls_workspace::PositionEncoding;
|
||||||
use djls_workspace::TextDocument;
|
use djls_workspace::TextDocument;
|
||||||
|
@ -57,6 +59,8 @@ pub fn handle_completion(
|
||||||
encoding: PositionEncoding,
|
encoding: PositionEncoding,
|
||||||
file_kind: FileKind,
|
file_kind: FileKind,
|
||||||
template_tags: Option<&TemplateTags>,
|
template_tags: Option<&TemplateTags>,
|
||||||
|
tag_specs: Option<&TagSpecs>,
|
||||||
|
supports_snippets: bool,
|
||||||
) -> Vec<CompletionItem> {
|
) -> Vec<CompletionItem> {
|
||||||
// Only handle template files
|
// Only handle template files
|
||||||
if file_kind != FileKind::Template {
|
if file_kind != FileKind::Template {
|
||||||
|
@ -74,7 +78,7 @@ pub fn handle_completion(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate completions based on available template tags
|
// 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
|
/// 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
|
/// Analyze a line of template text to determine completion context
|
||||||
fn analyze_template_context(line: &str, cursor_offset: usize) -> Option<TemplateTagContext> {
|
fn analyze_template_context(line: &str, cursor_offset: usize) -> Option<TemplateTagContext> {
|
||||||
if cursor_offset > line.chars().count() {
|
// Find the last {% before cursor position
|
||||||
return None;
|
let prefix = &line[..cursor_offset.min(line.len())];
|
||||||
}
|
let tag_start = prefix.rfind("{%")?;
|
||||||
|
|
||||||
let chars: Vec<char> = line.chars().collect();
|
// Get the content between {% and cursor
|
||||||
let prefix = chars[..cursor_offset].iter().collect::<String>();
|
let content_start = tag_start + 2;
|
||||||
let rest_of_line = chars[cursor_offset..].iter().collect::<String>();
|
let content = &prefix[content_start..];
|
||||||
let rest_trimmed = rest_of_line.trim_start();
|
|
||||||
|
|
||||||
prefix.rfind("{%").map(|tag_start| {
|
// Check if we need a leading space (no space after {%)
|
||||||
let closing_brace = if rest_trimmed.starts_with("%}") {
|
let needs_leading_space = content.is_empty() || !content.starts_with(' ');
|
||||||
ClosingBrace::FullClose
|
|
||||||
} else if rest_trimmed.starts_with('}') {
|
|
||||||
ClosingBrace::PartialClose
|
|
||||||
} else {
|
|
||||||
ClosingBrace::None
|
|
||||||
};
|
|
||||||
|
|
||||||
let partial_tag_start = tag_start + 2; // Skip "{%"
|
// Extract the partial tag name
|
||||||
let content_after_tag = if partial_tag_start < prefix.len() {
|
let partial_tag = content.trim_start().to_string();
|
||||||
&prefix[partial_tag_start..]
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if we need a leading space - true if there's no space after {%
|
// Check what's after the cursor for closing detection
|
||||||
let needs_leading_space =
|
let suffix = &line[cursor_offset.min(line.len())..];
|
||||||
!content_after_tag.starts_with(' ') && !content_after_tag.is_empty();
|
let closing_brace = detect_closing_brace(suffix);
|
||||||
|
|
||||||
let partial_tag = content_after_tag.trim().to_string();
|
Some(TemplateTagContext {
|
||||||
|
partial_tag,
|
||||||
TemplateTagContext {
|
closing_brace,
|
||||||
partial_tag,
|
needs_leading_space,
|
||||||
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
|
/// Generate Django template tag completion items based on context
|
||||||
fn generate_template_completions(
|
fn generate_template_completions(
|
||||||
context: &TemplateTagContext,
|
context: &TemplateTagContext,
|
||||||
template_tags: Option<&TemplateTags>,
|
template_tags: Option<&TemplateTags>,
|
||||||
|
tag_specs: Option<&TagSpecs>,
|
||||||
|
supports_snippets: bool,
|
||||||
) -> Vec<CompletionItem> {
|
) -> Vec<CompletionItem> {
|
||||||
let Some(tags) = template_tags else {
|
let Some(tags) = template_tags else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
@ -177,25 +181,47 @@ fn generate_template_completions(
|
||||||
let mut completions = Vec::new();
|
let mut completions = Vec::new();
|
||||||
|
|
||||||
for tag in tags.iter() {
|
for tag in tags.iter() {
|
||||||
// Filter tags based on partial match
|
|
||||||
if tag.name().starts_with(&context.partial_tag) {
|
if tag.name().starts_with(&context.partial_tag) {
|
||||||
// Determine insertion text based on context
|
// Try to get snippet from TagSpecs if available and client supports snippets
|
||||||
let mut insert_text = String::new();
|
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 {%)
|
// Add leading space if needed
|
||||||
if context.needs_leading_space {
|
if context.needs_leading_space {
|
||||||
insert_text.push(' ');
|
text.push(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the tag name
|
// Add tag name and snippet arguments
|
||||||
insert_text.push_str(tag.name());
|
text.push_str(&generate_snippet_for_tag(tag.name(), spec));
|
||||||
|
|
||||||
// Add closing based on what's already present
|
// Add closing based on what's already present
|
||||||
match context.closing_brace {
|
match context.closing_brace {
|
||||||
ClosingBrace::None => insert_text.push_str(" %}"),
|
ClosingBrace::None => text.push_str(" %}"),
|
||||||
ClosingBrace::PartialClose => insert_text.push('%'),
|
ClosingBrace::PartialClose => text.push('%'),
|
||||||
ClosingBrace::FullClose => {} // No closing needed
|
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
|
// Create completion item
|
||||||
let completion_item = CompletionItem {
|
let completion_item = CompletionItem {
|
||||||
|
@ -204,7 +230,7 @@ fn generate_template_completions(
|
||||||
detail: Some(format!("from {}", tag.library())),
|
detail: Some(format!("from {}", tag.library())),
|
||||||
documentation: tag.doc().map(|doc| Documentation::String(doc.clone())),
|
documentation: tag.doc().map(|doc| Documentation::String(doc.clone())),
|
||||||
insert_text: Some(insert_text),
|
insert_text: Some(insert_text),
|
||||||
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
|
insert_text_format: Some(insert_format),
|
||||||
filter_text: Some(tag.name().clone()),
|
filter_text: Some(tag.name().clone()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
@ -216,6 +242,28 @@ fn generate_template_completions(
|
||||||
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -286,7 +334,7 @@ mod tests {
|
||||||
closing_brace: ClosingBrace::None,
|
closing_brace: ClosingBrace::None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let completions = generate_template_completions(&context, None);
|
let completions = generate_template_completions(&context, None, None, false);
|
||||||
|
|
||||||
assert!(completions.is_empty());
|
assert!(completions.is_empty());
|
||||||
}
|
}
|
||||||
|
|
|
@ -370,6 +370,8 @@ impl LanguageServer for DjangoLanguageServer {
|
||||||
let encoding = session.position_encoding();
|
let encoding = session.position_encoding();
|
||||||
let file_kind = FileKind::from_path(&path);
|
let file_kind = FileKind::from_path(&path);
|
||||||
let template_tags = session.project().and_then(|p| p.template_tags());
|
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(
|
let completions = crate::completions::handle_completion(
|
||||||
&document,
|
&document,
|
||||||
|
@ -377,6 +379,8 @@ impl LanguageServer for DjangoLanguageServer {
|
||||||
encoding,
|
encoding,
|
||||||
file_kind,
|
file_kind,
|
||||||
template_tags,
|
template_tags,
|
||||||
|
Some(&tag_specs),
|
||||||
|
supports_snippets,
|
||||||
);
|
);
|
||||||
|
|
||||||
if completions.is_empty() {
|
if completions.is_empty() {
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
mod snippets;
|
||||||
mod specs;
|
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 use specs::TagSpecs;
|
||||||
|
|
||||||
pub enum TagType {
|
pub enum TagType {
|
||||||
|
|
194
crates/djls-templates/src/templatetags/snippets.rs
Normal file
194
crates/djls-templates/src/templatetags/snippets.rs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
|
@ -207,9 +207,41 @@ pub struct TagSpec {
|
||||||
#[serde(default, alias = "intermediates")]
|
#[serde(default, alias = "intermediates")]
|
||||||
pub intermediate_tags: Option<Vec<IntermediateTag>>,
|
pub intermediate_tags: Option<Vec<IntermediateTag>>,
|
||||||
#[serde(default)]
|
#[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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct EndTag {
|
pub struct EndTag {
|
||||||
#[serde(alias = "tag")]
|
#[serde(alias = "tag")]
|
||||||
|
@ -217,7 +249,7 @@ pub struct EndTag {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub optional: bool,
|
pub optional: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub args: Option<ArgSpec>,
|
pub args: Vec<Arg>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
#[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 {
|
impl TagSpec {
|
||||||
/// Recursive extraction: Check if node is spec, otherwise recurse if table.
|
/// Recursive extraction: Check if node is spec, otherwise recurse if table.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -405,15 +429,17 @@ mod tests {
|
||||||
assert!(specs.get(tag).is_some(), "{tag} tag should be present");
|
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
|
// Check that some tags are still missing
|
||||||
let missing_tags = [
|
let missing_tags = [
|
||||||
"debug",
|
"querystring", // Django 5.1+
|
||||||
"firstof",
|
|
||||||
"lorem",
|
|
||||||
"querystring", // 5.1
|
|
||||||
"regroup",
|
|
||||||
"resetcycle",
|
"resetcycle",
|
||||||
"widthratio",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for tag in missing_tags {
|
for tag in missing_tags {
|
||||||
|
@ -452,7 +478,7 @@ end = { tag = "endanothertag", optional = true }
|
||||||
Some(EndTag {
|
Some(EndTag {
|
||||||
name: "endmytag".to_string(),
|
name: "endmytag".to_string(),
|
||||||
optional: false,
|
optional: false,
|
||||||
args: None,
|
args: vec![],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -470,7 +496,25 @@ end = { tag = "endanothertag", optional = true }
|
||||||
Some(EndTag {
|
Some(EndTag {
|
||||||
name: "endanothertag".to_string(),
|
name: "endanothertag".to_string(),
|
||||||
optional: true,
|
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!(
|
assert!(
|
||||||
|
|
|
@ -23,7 +23,9 @@ use crate::ast::Span;
|
||||||
use crate::ast::TagName;
|
use crate::ast::TagName;
|
||||||
use crate::ast::TagNode;
|
use crate::ast::TagNode;
|
||||||
use crate::db::Db as TemplateDb;
|
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::templatetags::TagType;
|
||||||
use crate::Ast;
|
use crate::Ast;
|
||||||
|
|
||||||
|
@ -57,14 +59,14 @@ impl<'db> TagValidator<'db> {
|
||||||
let tag_specs = self.db.tag_specs();
|
let tag_specs = self.db.tag_specs();
|
||||||
let tag_type = TagType::for_name(&name_str, &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
|
TagType::Closer => tag_specs
|
||||||
.get_end_spec_for_closer(&name_str)
|
.get_end_spec_for_closer(&name_str)
|
||||||
.and_then(|s| s.args.as_ref()),
|
.map(|s| &s.args),
|
||||||
_ => tag_specs.get(&name_str).and_then(|s| s.args.as_ref()),
|
_ => 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 {
|
match tag_type {
|
||||||
TagType::Opener => {
|
TagType::Opener => {
|
||||||
|
@ -104,30 +106,34 @@ impl<'db> TagValidator<'db> {
|
||||||
name: &str,
|
name: &str,
|
||||||
bits: &[String],
|
bits: &[String],
|
||||||
span: Span,
|
span: Span,
|
||||||
arg_spec: Option<&ArgSpec>,
|
args: Option<&Vec<Arg>>,
|
||||||
) {
|
) {
|
||||||
let Some(arg_spec) = arg_spec else {
|
let Some(args) = args else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(min) = arg_spec.min {
|
// Count required arguments
|
||||||
if bits.len() < min {
|
let required_count = args.iter().filter(|arg| arg.required).count();
|
||||||
self.errors.push(AstError::MissingRequiredArguments {
|
|
||||||
tag: name.to_string(),
|
if bits.len() < required_count {
|
||||||
min,
|
self.errors.push(AstError::MissingRequiredArguments {
|
||||||
span,
|
tag: name.to_string(),
|
||||||
});
|
min: required_count,
|
||||||
}
|
span,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(max) = arg_spec.max {
|
// If there are more bits than defined args, that might be okay for varargs
|
||||||
if bits.len() > max {
|
let has_varargs = args
|
||||||
self.errors.push(AstError::TooManyArguments {
|
.iter()
|
||||||
tag: name.to_string(),
|
.any(|arg| matches!(arg.arg_type, ArgType::Simple(SimpleArgType::VarArgs)));
|
||||||
max,
|
|
||||||
span,
|
if !has_varargs && bits.len() > args.len() {
|
||||||
});
|
self.errors.push(AstError::TooManyArguments {
|
||||||
}
|
tag: name.to_string(),
|
||||||
|
max: args.len(),
|
||||||
|
span,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,29 +4,46 @@ Tag Specifications (TagSpecs) define how template tags are structured, helping t
|
||||||
|
|
||||||
## Schema
|
## 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
|
```toml
|
||||||
[[path.to.module]] # Array of tables for the module, e.g., tagspecs.django.template.defaulttags
|
[[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")
|
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
|
end_tag = { name = "end_tag_name", optional = false } # Optional: Defines the closing tag
|
||||||
intermediate_tags = [{ name = "tag_name" }, ...] # Optional: Defines intermediate tags
|
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 `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.
|
The `end_tag` table defines the closing tag for a block tag:
|
||||||
- `name`: The name of the closing tag (e.g., "endif").
|
- `name`: The name of the closing tag (e.g., "endif")
|
||||||
- `optional`: Whether the closing tag is optional (defaults to `false`).
|
- `optional`: Whether the closing tag is optional (defaults to `false`)
|
||||||
- `args`: Optional argument constraints for the end tag.
|
- `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:
|
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:
|
### Argument Specification
|
||||||
- `min`: Minimum number of arguments required.
|
|
||||||
- `max`: Maximum number of arguments allowed.
|
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
|
## Configuration
|
||||||
|
|
||||||
|
@ -42,7 +59,10 @@ The `args` table defines argument constraints:
|
||||||
name = "if"
|
name = "if"
|
||||||
end_tag = { name = "endif" }
|
end_tag = { name = "endif" }
|
||||||
intermediate_tags = [{ name = "elif" }, { name = "else" }]
|
intermediate_tags = [{ name = "elif" }, { name = "else" }]
|
||||||
args = { min = 1 } # condition
|
args = [
|
||||||
|
{ name = "condition", type = "expression" }
|
||||||
|
]
|
||||||
|
# Generates snippet: if ${1:condition}
|
||||||
```
|
```
|
||||||
|
|
||||||
### For Tag
|
### For Tag
|
||||||
|
@ -52,7 +72,13 @@ args = { min = 1 } # condition
|
||||||
name = "for"
|
name = "for"
|
||||||
end_tag = { name = "endfor" }
|
end_tag = { name = "endfor" }
|
||||||
intermediate_tags = [{ name = "empty" }]
|
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
|
### Autoescape Tag
|
||||||
|
@ -61,7 +87,24 @@ args = { min = 3 } # item in items (at minimum)
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "autoescape"
|
name = "autoescape"
|
||||||
end_tag = { name = "endautoescape" }
|
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
|
### Custom Tag
|
||||||
|
@ -71,16 +114,26 @@ args = { min = 1, max = 1 } # on or off
|
||||||
name = "my_custom_tag"
|
name = "my_custom_tag"
|
||||||
end_tag = { name = "endmycustomtag", optional = true }
|
end_tag = { name = "endmycustomtag", optional = true }
|
||||||
intermediate_tags = [{ name = "myintermediate" }]
|
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
|
```toml
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "csrf_token"
|
name = "csrf_token"
|
||||||
args = { min = 0, max = 0 } # no arguments
|
args = [] # No arguments
|
||||||
|
# Generates snippet: csrf_token
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "load"
|
name = "load"
|
||||||
args = { min = 1 } # library name(s)
|
args = [
|
||||||
|
{ name = "libraries", type = "varargs" }
|
||||||
|
]
|
||||||
|
# Generates snippet: load ${1:libraries}
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,101 +1,227 @@
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "autoescape"
|
name = "autoescape"
|
||||||
end_tag = { name = "endautoescape" }
|
end_tag = { name = "endautoescape" }
|
||||||
args = { min = 1, max = 1 } # on or off
|
args = [
|
||||||
|
{ name = "mode", type = { choice = ["on", "off"] } }
|
||||||
|
]
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "block"
|
name = "block"
|
||||||
end_tag = { name = "endblock", args = { min = 0, max = 1 } }
|
end_tag = { name = "endblock", args = [{ name = "name", required = false, type = "variable" }] }
|
||||||
args = { min = 1, max = 1 } # block name
|
args = [
|
||||||
|
{ name = "name", type = "variable" }
|
||||||
|
]
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "comment"
|
name = "comment"
|
||||||
end_tag = { name = "endcomment" }
|
end_tag = { name = "endcomment" }
|
||||||
|
args = []
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "csrf_token"
|
name = "csrf_token"
|
||||||
args = { min = 0, max = 0 } # no arguments
|
args = []
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "cycle"
|
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]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "extends"
|
name = "extends"
|
||||||
args = { min = 1, max = 1 } # template name
|
args = [
|
||||||
|
{ name = "template", type = "string" }
|
||||||
|
]
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "filter"
|
name = "filter"
|
||||||
end_tag = { name = "endfilter" }
|
end_tag = { name = "endfilter" }
|
||||||
args = { min = 1 } # filter expression
|
args = [
|
||||||
|
{ name = "filter_expr", type = "expression" }
|
||||||
|
]
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "for"
|
name = "for"
|
||||||
end_tag = { name = "endfor" }
|
end_tag = { name = "endfor" }
|
||||||
intermediate_tags = [ { name = "empty" } ]
|
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]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "include"
|
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]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "if"
|
name = "if"
|
||||||
end_tag = { name = "endif" }
|
end_tag = { name = "endif" }
|
||||||
intermediate_tags = [ { name = "elif" }, { name = "else" } ]
|
intermediate_tags = [ { name = "elif" }, { name = "else" } ]
|
||||||
args = { min = 1 } # condition
|
args = [
|
||||||
|
{ name = "condition", type = "expression" }
|
||||||
|
]
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "ifchanged"
|
name = "ifchanged"
|
||||||
end_tag = { name = "endifchanged" }
|
end_tag = { name = "endifchanged" }
|
||||||
intermediate_tags = [ { name = "else" } ]
|
intermediate_tags = [ { name = "else" } ]
|
||||||
|
args = [
|
||||||
|
{ name = "variables", required = false, type = "varargs" }
|
||||||
|
]
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "load"
|
name = "load"
|
||||||
args = { min = 1 } # library name(s)
|
args = [
|
||||||
|
{ name = "libraries", type = "varargs" }
|
||||||
|
]
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "now"
|
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]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "spaceless"
|
name = "spaceless"
|
||||||
end_tag = { name = "endspaceless" }
|
end_tag = { name = "endspaceless" }
|
||||||
|
args = []
|
||||||
|
|
||||||
|
[[tagspecs.django.template.defaulttags]]
|
||||||
|
name = "static"
|
||||||
|
args = [
|
||||||
|
{ name = "path", type = "string" }
|
||||||
|
]
|
||||||
|
|
||||||
[[tagspecs.django.template.defaulttags]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "templatetag"
|
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]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "url"
|
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]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "verbatim"
|
name = "verbatim"
|
||||||
end_tag = { name = "endverbatim" }
|
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]]
|
[[tagspecs.django.template.defaulttags]]
|
||||||
name = "with"
|
name = "with"
|
||||||
end_tag = { name = "endwith" }
|
end_tag = { name = "endwith" }
|
||||||
args = { min = 1 } # variable assignment(s)
|
args = [
|
||||||
|
{ name = "assignments", type = "varargs" }
|
||||||
|
]
|
||||||
|
|
||||||
|
# Cache tags
|
||||||
[[tagspecs.django.templatetags.cache]]
|
[[tagspecs.django.templatetags.cache]]
|
||||||
name = "cache"
|
name = "cache"
|
||||||
end_tag = { name = "endcache" }
|
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]]
|
[[tagspecs.django.templatetags.i10n]]
|
||||||
name = "localize"
|
name = "localize"
|
||||||
end_tag = { name = "endlocalize" }
|
end_tag = { name = "endlocalize" }
|
||||||
|
args = [
|
||||||
|
{ name = "mode", required = false, type = { choice = ["on", "off"] } }
|
||||||
|
]
|
||||||
|
|
||||||
[[tagspecs.django.templatetags.i18n]]
|
[[tagspecs.django.templatetags.i18n]]
|
||||||
name = "blocktranslate"
|
name = "blocktranslate"
|
||||||
end_tag = { name = "endblocktranslate" }
|
end_tag = { name = "endblocktranslate" }
|
||||||
intermediate_tags = [ { name = "plural" } ]
|
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]]
|
[[tagspecs.django.templatetags.tz]]
|
||||||
name = "localtime"
|
name = "localtime"
|
||||||
end_tag = { name = "endlocaltime" }
|
end_tag = { name = "endlocaltime" }
|
||||||
|
args = [
|
||||||
|
{ name = "mode", required = false, type = { choice = ["on", "off"] } }
|
||||||
|
]
|
||||||
|
|
||||||
[[tagspecs.django.templatetags.tz]]
|
[[tagspecs.django.templatetags.tz]]
|
||||||
name = "timezone"
|
name = "timezone"
|
||||||
end_tag = { name = "endtimezone" }
|
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 = []
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue