diff --git a/crates/djls-server/src/completions.rs b/crates/djls-server/src/completions.rs index b9054f6..093faa3 100644 --- a/crates/djls-server/src/completions.rs +++ b/crates/djls-server/src/completions.rs @@ -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 { // 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 { - 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 = line.chars().collect(); - let prefix = chars[..cursor_offset].iter().collect::(); - let rest_of_line = chars[cursor_offset..].iter().collect::(); - 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 { 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()); } diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index 686db9e..a09ef0c 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -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() { diff --git a/crates/djls-templates/src/templatetags.rs b/crates/djls-templates/src/templatetags.rs index 71e57e7..107d409 100644 --- a/crates/djls-templates/src/templatetags.rs +++ b/crates/djls-templates/src/templatetags.rs @@ -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 { diff --git a/crates/djls-templates/src/templatetags/snippets.rs b/crates/djls-templates/src/templatetags/snippets.rs new file mode 100644 index 0000000..2d1cf90 --- /dev/null +++ b/crates/djls-templates/src/templatetags/snippets.rs @@ -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}"); + } +} diff --git a/crates/djls-templates/src/templatetags/specs.rs b/crates/djls-templates/src/templatetags/specs.rs index ce8344f..42e5865 100644 --- a/crates/djls-templates/src/templatetags/specs.rs +++ b/crates/djls-templates/src/templatetags/specs.rs @@ -207,9 +207,41 @@ pub struct TagSpec { #[serde(default, alias = "intermediates")] pub intermediate_tags: Option>, #[serde(default)] - pub args: Option, + pub args: Vec, } +#[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 }, +} + +#[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, + pub args: Vec, } #[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, - #[serde(default)] - pub max: Option, -} - 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!( diff --git a/crates/djls-templates/src/validation.rs b/crates/djls-templates/src/validation.rs index 1b3bdf3..a131448 100644 --- a/crates/djls-templates/src/validation.rs +++ b/crates/djls-templates/src/validation.rs @@ -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>, ) { - 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, + }); } } diff --git a/crates/djls-templates/tagspecs/README.md b/crates/djls-templates/tagspecs/README.md index 6b496bd..2b7153b 100644 --- a/crates/djls-templates/tagspecs/README.md +++ b/crates/djls-templates/tagspecs/README.md @@ -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} ``` diff --git a/crates/djls-templates/tagspecs/django.toml b/crates/djls-templates/tagspecs/django.toml index 0a8adeb..cdd9aa1 100644 --- a/crates/djls-templates/tagspecs/django.toml +++ b/crates/djls-templates/tagspecs/django.toml @@ -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 = []