mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-11 04:46:38 +00:00
Refactor tagspecs to use array of tables and consistent fields (#202)
This commit is contained in:
parent
6a4f1668e3
commit
095fb0a714
3 changed files with 232 additions and 114 deletions
|
@ -4,6 +4,7 @@ use std::path::Path;
|
|||
|
||||
use anyhow::Result;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
use toml::Value;
|
||||
|
@ -41,8 +42,8 @@ impl TagSpecs {
|
|||
#[must_use]
|
||||
pub fn find_opener_for_closer(&self, closer: &str) -> Option<String> {
|
||||
for (tag_name, spec) in &self.0 {
|
||||
if let Some(end_spec) = &spec.end {
|
||||
if end_spec.tag == closer {
|
||||
if let Some(end_spec) = &spec.end_tag {
|
||||
if end_spec.name == closer {
|
||||
return Some(tag_name.clone());
|
||||
}
|
||||
}
|
||||
|
@ -54,8 +55,8 @@ impl TagSpecs {
|
|||
#[must_use]
|
||||
pub fn get_end_spec_for_closer(&self, closer: &str) -> Option<&EndTag> {
|
||||
for spec in self.0.values() {
|
||||
if let Some(end_spec) = &spec.end {
|
||||
if end_spec.tag == closer {
|
||||
if let Some(end_spec) = &spec.end_tag {
|
||||
if end_spec.name == closer {
|
||||
return Some(end_spec);
|
||||
}
|
||||
}
|
||||
|
@ -67,24 +68,28 @@ impl TagSpecs {
|
|||
pub fn is_opener(&self, name: &str) -> bool {
|
||||
self.0
|
||||
.get(name)
|
||||
.and_then(|spec| spec.end.as_ref())
|
||||
.and_then(|spec| spec.end_tag.as_ref())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_intermediate(&self, name: &str) -> bool {
|
||||
self.0.values().any(|spec| {
|
||||
spec.intermediates
|
||||
spec.intermediate_tags
|
||||
.as_ref()
|
||||
.is_some_and(|intermediates| intermediates.contains(&name.to_string()))
|
||||
.is_some_and(|intermediate_tags| {
|
||||
intermediate_tags.iter().any(|tag| tag.name == name)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_closer(&self, name: &str) -> bool {
|
||||
self.0
|
||||
.values()
|
||||
.any(|spec| spec.end.as_ref().is_some_and(|end_tag| end_tag.tag == name))
|
||||
self.0.values().any(|spec| {
|
||||
spec.end_tag
|
||||
.as_ref()
|
||||
.is_some_and(|end_tag| end_tag.name == name)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the parent tags that can contain this intermediate tag
|
||||
|
@ -92,8 +97,8 @@ impl TagSpecs {
|
|||
pub fn get_parent_tags_for_intermediate(&self, intermediate: &str) -> Vec<String> {
|
||||
let mut parents = Vec::new();
|
||||
for (opener_name, spec) in &self.0 {
|
||||
if let Some(intermediates) = &spec.intermediates {
|
||||
if intermediates.contains(&intermediate.to_string()) {
|
||||
if let Some(intermediate_tags) = &spec.intermediate_tags {
|
||||
if intermediate_tags.iter().any(|tag| tag.name == intermediate) {
|
||||
parents.push(opener_name.clone());
|
||||
}
|
||||
}
|
||||
|
@ -195,22 +200,50 @@ impl TagSpecs {
|
|||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TagSpec {
|
||||
pub end: Option<EndTag>,
|
||||
#[serde(default)]
|
||||
pub intermediates: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(alias = "end")]
|
||||
pub end_tag: Option<EndTag>,
|
||||
#[serde(default, alias = "intermediates")]
|
||||
pub intermediate_tags: Option<Vec<IntermediateTag>>,
|
||||
#[serde(default)]
|
||||
pub args: Option<ArgSpec>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct EndTag {
|
||||
pub tag: String,
|
||||
#[serde(alias = "tag")]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub optional: bool,
|
||||
#[serde(default)]
|
||||
pub args: Option<ArgSpec>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
pub struct IntermediateTag {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for IntermediateTag {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum IntermediateTagHelper {
|
||||
String(String),
|
||||
Object { name: String },
|
||||
}
|
||||
|
||||
match IntermediateTagHelper::deserialize(deserializer)? {
|
||||
IntermediateTagHelper::String(s) => Ok(IntermediateTag { name: s }),
|
||||
IntermediateTagHelper::Object { name } => Ok(IntermediateTag { name }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ArgSpec {
|
||||
#[serde(default)]
|
||||
|
@ -227,12 +260,40 @@ impl TagSpec {
|
|||
prefix: Option<&str>, // Path *to* this value node
|
||||
specs: &mut HashMap<String, TagSpec>,
|
||||
) -> Result<(), String> {
|
||||
// Check if the current node *itself* represents a TagSpec definition
|
||||
// We can be more specific: check if it's a table containing 'end', 'intermediates', or 'args'
|
||||
// Check if this is an array of TagSpec entries (new format)
|
||||
if let Some(array) = value.as_array() {
|
||||
for item in array {
|
||||
if let Some(table) = item.as_table() {
|
||||
// Check if it has a 'name' field (new format)
|
||||
if table.contains_key("name") {
|
||||
match TagSpec::deserialize(item.clone()) {
|
||||
Ok(mut tag_spec) => {
|
||||
if let Some(name) = tag_spec.name.take() {
|
||||
specs.insert(name, tag_spec);
|
||||
} else {
|
||||
return Err(
|
||||
"TagSpec has 'name' field but it's None".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("Failed to deserialize TagSpec in array: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if the current node *itself* represents a TagSpec definition (old format)
|
||||
// We can be more specific: check if it's a table containing 'end'/'end_tag', 'intermediates'/'intermediate_tags', or 'args'
|
||||
let mut is_spec_node = false;
|
||||
if let Some(table) = value.as_table() {
|
||||
if table.contains_key("end")
|
||||
|| table.contains_key("end_tag")
|
||||
|| table.contains_key("intermediates")
|
||||
|| table.contains_key("intermediate_tags")
|
||||
|| table.contains_key("args")
|
||||
{
|
||||
// Looks like a spec, try to deserialize
|
||||
|
@ -387,29 +448,34 @@ end = { tag = "endanothertag", optional = true }
|
|||
|
||||
let my_tag = specs.get("mytag").expect("mytag should be present");
|
||||
assert_eq!(
|
||||
my_tag.end,
|
||||
my_tag.end_tag,
|
||||
Some(EndTag {
|
||||
tag: "endmytag".to_string(),
|
||||
name: "endmytag".to_string(),
|
||||
optional: false,
|
||||
args: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(my_tag.intermediates, Some(vec!["mybranch".to_string()]));
|
||||
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,
|
||||
another_tag.end_tag,
|
||||
Some(EndTag {
|
||||
tag: "endanothertag".to_string(),
|
||||
name: "endanothertag".to_string(),
|
||||
optional: true,
|
||||
args: None,
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
another_tag.intermediates.is_none(),
|
||||
"anothertag should have no intermediates"
|
||||
another_tag.intermediate_tags.is_none(),
|
||||
"anothertag should have no intermediate_tags"
|
||||
);
|
||||
|
||||
dir.close()?;
|
||||
|
@ -441,7 +507,7 @@ end = { tag = "endmytag2_from_pyproject" }
|
|||
let specs = TagSpecs::load_user_specs(root)?;
|
||||
|
||||
let tag1 = specs.get("mytag1").expect("mytag1 should be present");
|
||||
assert_eq!(tag1.end.as_ref().unwrap().tag, "endmytag1_from_djls");
|
||||
assert_eq!(tag1.end_tag.as_ref().unwrap().name, "endmytag1_from_djls");
|
||||
|
||||
// Should not find mytag2 because djls.toml was found first
|
||||
assert!(
|
||||
|
@ -454,7 +520,10 @@ end = { tag = "endmytag2_from_pyproject" }
|
|||
let specs = TagSpecs::load_user_specs(root)?;
|
||||
|
||||
let tag1 = specs.get("mytag1").expect("mytag1 should be present now");
|
||||
assert_eq!(tag1.end.as_ref().unwrap().tag, "endmytag1_from_pyproject");
|
||||
assert_eq!(
|
||||
tag1.end_tag.as_ref().unwrap().name,
|
||||
"endmytag1_from_pyproject"
|
||||
);
|
||||
|
||||
assert!(
|
||||
specs.get("mytag2").is_some(),
|
||||
|
|
|
@ -7,18 +7,26 @@ Tag Specifications (TagSpecs) define how template tags are structured, helping t
|
|||
Tag Specifications (TagSpecs) define how tags are parsed and understood. They allow the parser to handle custom tags without hard-coding them.
|
||||
|
||||
```toml
|
||||
[path.to.tag_name] # Path where tag is registered, e.g., django.template.defaulttags
|
||||
end = { tag = "end_tag_name", optional = false } # Optional: Defines the closing tag
|
||||
intermediates = ["intermediate_tag_name", ...] # Optional: Defines intermediate tags (like else, elif)
|
||||
[[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
|
||||
```
|
||||
|
||||
The `end` table defines the closing tag for a block tag.
|
||||
- `tag`: The name of the closing tag (e.g., "endif").
|
||||
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 `intermediates` array lists tags that can appear between the opening and closing tags (e.g., "else", "elif" for an "if" tag).
|
||||
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").
|
||||
|
||||
The tag name itself (e.g., `if`, `for`, `my_custom_tag`) is derived from the last segment of the TOML table path defining the spec.
|
||||
The `args` table defines argument constraints:
|
||||
- `min`: Minimum number of arguments required.
|
||||
- `max`: Maximum number of arguments allowed.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
@ -30,30 +38,49 @@ The tag name itself (e.g., `if`, `for`, `my_custom_tag`) is derived from the las
|
|||
### If Tag
|
||||
|
||||
```toml
|
||||
[tagspecs.django.template.defaulttags.if]
|
||||
end = { tag = "endif" }
|
||||
intermediates = ["elif", "else"]
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "if"
|
||||
end_tag = { name = "endif" }
|
||||
intermediate_tags = [{ name = "elif" }, { name = "else" }]
|
||||
args = { min = 1 } # condition
|
||||
```
|
||||
|
||||
### For Tag
|
||||
|
||||
```toml
|
||||
[tagspecs.django.template.defaulttags.for]
|
||||
end = { tag = "endfor" }
|
||||
intermediates = ["empty"]
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "for"
|
||||
end_tag = { name = "endfor" }
|
||||
intermediate_tags = [{ name = "empty" }]
|
||||
args = { min = 3 } # item in items (at minimum)
|
||||
```
|
||||
|
||||
### Autoescape Tag
|
||||
|
||||
```toml
|
||||
[tagspecs.django.template.defaulttags.autoescape]
|
||||
end = { tag = "endautoescape" }
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "autoescape"
|
||||
end_tag = { name = "endautoescape" }
|
||||
args = { min = 1, max = 1 } # on or off
|
||||
```
|
||||
|
||||
### Custom Tag
|
||||
|
||||
```toml
|
||||
[tagspecs.my_module.templatetags.my_tags.my_custom_tag]
|
||||
end = { tag = "endmycustomtag", optional = true }
|
||||
intermediates = ["myintermediate"]
|
||||
[[tagspecs.my_module.templatetags.my_tags]]
|
||||
name = "my_custom_tag"
|
||||
end_tag = { name = "endmycustomtag", optional = true }
|
||||
intermediate_tags = [{ name = "myintermediate" }]
|
||||
```
|
||||
|
||||
### Standalone Tags (no end tag)
|
||||
|
||||
```toml
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "csrf_token"
|
||||
args = { min = 0, max = 0 } # no arguments
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "load"
|
||||
args = { min = 1 } # library name(s)
|
||||
```
|
||||
|
|
|
@ -1,79 +1,101 @@
|
|||
[tagspecs.django.template.defaulttags.autoescape]
|
||||
end = { tag = "endautoescape" }
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "autoescape"
|
||||
end_tag = { name = "endautoescape" }
|
||||
args = { min = 1, max = 1 } # on or off
|
||||
|
||||
[tagspecs.django.template.defaulttags.block]
|
||||
end = { tag = "endblock", args = { min = 0, max = 1 } }
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "block"
|
||||
end_tag = { name = "endblock", args = { min = 0, max = 1 } }
|
||||
args = { min = 1, max = 1 } # block name
|
||||
|
||||
[tagspecs.django.template.defaulttags.comment]
|
||||
end = { tag = "endcomment" }
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "comment"
|
||||
end_tag = { name = "endcomment" }
|
||||
|
||||
[tagspecs.django.template.defaulttags.filter]
|
||||
end = { tag = "endfilter" }
|
||||
args = { min = 1 } # filter expression
|
||||
|
||||
[tagspecs.django.template.defaulttags.for]
|
||||
end = { tag = "endfor" }
|
||||
intermediates = [ "empty" ]
|
||||
args = { min = 3 } # item in items (at minimum)
|
||||
|
||||
[tagspecs.django.template.defaulttags.if]
|
||||
end = { tag = "endif" }
|
||||
intermediates = [ "elif", "else" ]
|
||||
args = { min = 1 } # condition
|
||||
|
||||
[tagspecs.django.template.defaulttags.ifchanged]
|
||||
end = { tag = "endifchanged" }
|
||||
intermediates = [ "else" ]
|
||||
|
||||
[tagspecs.django.template.defaulttags.spaceless]
|
||||
end = { tag = "endspaceless" }
|
||||
|
||||
[tagspecs.django.template.defaulttags.verbatim]
|
||||
end = { tag = "endverbatim" }
|
||||
|
||||
[tagspecs.django.template.defaulttags.with]
|
||||
end = { tag = "endwith" }
|
||||
args = { min = 1 } # variable assignment(s)
|
||||
|
||||
[tagspecs.django.templatetags.cache.cache]
|
||||
end = { tag = "endcache" }
|
||||
|
||||
[tagspecs.django.templatetags.i10n.localize]
|
||||
end = { tag = "endlocalize" }
|
||||
|
||||
[tagspecs.django.templatetags.i18n.blocktranslate]
|
||||
end = { tag = "endblocktranslate" }
|
||||
intermediates = [ "plural" ]
|
||||
|
||||
[tagspecs.django.templatetags.tz.localtime]
|
||||
end = { tag = "endlocaltime" }
|
||||
|
||||
[tagspecs.django.templatetags.tz.timezone]
|
||||
end = { tag = "endtimezone" }
|
||||
|
||||
# Standalone tags (no end tag)
|
||||
[tagspecs.django.template.defaulttags.extends]
|
||||
args = { min = 1, max = 1 } # template name
|
||||
|
||||
[tagspecs.django.template.defaulttags.include]
|
||||
args = { min = 1 } # template name [with context]
|
||||
|
||||
[tagspecs.django.template.defaulttags.load]
|
||||
args = { min = 1 } # library name(s)
|
||||
|
||||
[tagspecs.django.template.defaulttags.url]
|
||||
args = { min = 1 } # view name [args...]
|
||||
|
||||
[tagspecs.django.template.defaulttags.cycle]
|
||||
args = { min = 1 } # values to cycle through
|
||||
|
||||
[tagspecs.django.template.defaulttags.csrf_token]
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "csrf_token"
|
||||
args = { min = 0, max = 0 } # no arguments
|
||||
|
||||
[tagspecs.django.template.defaulttags.now]
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "cycle"
|
||||
args = { min = 1 } # values to cycle through
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "extends"
|
||||
args = { min = 1, max = 1 } # template name
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "filter"
|
||||
end_tag = { name = "endfilter" }
|
||||
args = { min = 1 } # filter expression
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "for"
|
||||
end_tag = { name = "endfor" }
|
||||
intermediate_tags = [ { name = "empty" } ]
|
||||
args = { min = 3 } # item in items (at minimum)
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "include"
|
||||
args = { min = 1 } # template name [with context]
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "if"
|
||||
end_tag = { name = "endif" }
|
||||
intermediate_tags = [ { name = "elif" }, { name = "else" } ]
|
||||
args = { min = 1 } # condition
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "ifchanged"
|
||||
end_tag = { name = "endifchanged" }
|
||||
intermediate_tags = [ { name = "else" } ]
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "load"
|
||||
args = { min = 1 } # library name(s)
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "now"
|
||||
args = { min = 1, max = 1 } # format string
|
||||
|
||||
[tagspecs.django.template.defaulttags.templatetag]
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "spaceless"
|
||||
end_tag = { name = "endspaceless" }
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "templatetag"
|
||||
args = { min = 1, max = 1 } # special character name
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "url"
|
||||
args = { min = 1 } # view name [args...]
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "verbatim"
|
||||
end_tag = { name = "endverbatim" }
|
||||
|
||||
[[tagspecs.django.template.defaulttags]]
|
||||
name = "with"
|
||||
end_tag = { name = "endwith" }
|
||||
args = { min = 1 } # variable assignment(s)
|
||||
|
||||
[[tagspecs.django.templatetags.cache]]
|
||||
name = "cache"
|
||||
end_tag = { name = "endcache" }
|
||||
|
||||
[[tagspecs.django.templatetags.i10n]]
|
||||
name = "localize"
|
||||
end_tag = { name = "endlocalize" }
|
||||
|
||||
[[tagspecs.django.templatetags.i18n]]
|
||||
name = "blocktranslate"
|
||||
end_tag = { name = "endblocktranslate" }
|
||||
intermediate_tags = [ { name = "plural" } ]
|
||||
|
||||
[[tagspecs.django.templatetags.tz]]
|
||||
name = "localtime"
|
||||
end_tag = { name = "endlocaltime" }
|
||||
|
||||
[[tagspecs.django.templatetags.tz]]
|
||||
name = "timezone"
|
||||
end_tag = { name = "endtimezone" }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue