11use anyhow:: Result ;
2- use serde:: Deserialize ;
2+ use serde:: { Deserialize , Serialize } ;
33use std:: collections:: HashMap ;
44use std:: fs;
55use std:: path:: Path ;
@@ -27,38 +27,41 @@ impl TagSpecs {
2727 }
2828
2929 /// Load specs from a TOML file, looking under the specified table path
30- fn load_from_toml ( path : & Path , table_path : & [ & str ] ) -> Result < Self , anyhow :: Error > {
30+ fn load_from_toml ( path : & Path , table_path : & [ & str ] ) -> Result < Self , TagSpecError > {
3131 let content = fs:: read_to_string ( path) ?;
3232 let value: Value = toml:: from_str ( & content) ?;
3333
34- // Navigate to the specified table
35- let table = table_path
34+ let start_node = table_path
3635 . iter ( )
37- . try_fold ( & value, |current, & key| {
38- current
39- . get ( key)
40- . ok_or_else ( || anyhow:: anyhow!( "Missing table: {}" , key) )
41- } )
42- . unwrap_or ( & value) ;
36+ . try_fold ( & value, |current, & key| current. get ( key) ) ;
4337
4438 let mut specs = HashMap :: new ( ) ;
45- TagSpec :: extract_specs ( table, None , & mut specs)
46- . map_err ( |e| TagSpecError :: Extract ( e. to_string ( ) ) ) ?;
39+
40+ if let Some ( node) = start_node {
41+ let initial_prefix = if table_path. is_empty ( ) {
42+ None
43+ } else {
44+ Some ( table_path. join ( "." ) )
45+ } ;
46+ TagSpec :: extract_specs ( node, initial_prefix. as_deref ( ) , & mut specs)
47+ . map_err ( TagSpecError :: Extract ) ?;
48+ }
49+
4750 Ok ( TagSpecs ( specs) )
4851 }
4952
5053 /// Load specs from a user's project directory
5154 pub fn load_user_specs ( project_root : & Path ) -> Result < Self , anyhow:: Error > {
52- // List of config files to try, in priority order
5355 let config_files = [ "djls.toml" , ".djls.toml" , "pyproject.toml" ] ;
5456
5557 for & file in & config_files {
5658 let path = project_root. join ( file) ;
5759 if path. exists ( ) {
58- return match file {
60+ let result = match file {
5961 "pyproject.toml" => Self :: load_from_toml ( & path, & [ "tool" , "djls" , "tagspecs" ] ) ,
60- _ => Self :: load_from_toml ( & path, & [ "tagspecs" ] ) , // Root level for other files
62+ _ => Self :: load_from_toml ( & path, & [ "tagspecs" ] ) ,
6163 } ;
64+ return result. map_err ( anyhow:: Error :: from) ;
6265 }
6366 }
6467 Ok ( Self :: default ( ) )
@@ -72,8 +75,8 @@ impl TagSpecs {
7275 for entry in fs:: read_dir ( & specs_dir) ? {
7376 let entry = entry?;
7477 let path = entry. path ( ) ;
75- if path. extension ( ) . and_then ( |ext| ext. to_str ( ) ) == Some ( "toml" ) {
76- let file_specs = Self :: load_from_toml ( & path, & [ ] ) ?;
78+ if path. is_file ( ) && path . extension ( ) . and_then ( |ext| ext. to_str ( ) ) == Some ( "toml" ) {
79+ let file_specs = Self :: load_from_toml ( & path, & [ "tagspecs" ] ) ?;
7780 specs. extend ( file_specs. 0 ) ;
7881 }
7982 }
@@ -95,87 +98,94 @@ impl TagSpecs {
9598 }
9699}
97100
98- #[ derive( Debug , Clone , Deserialize ) ]
101+ #[ derive( Debug , Clone , Serialize , Deserialize , PartialEq ) ]
99102pub struct TagSpec {
100- #[ serde( rename = "type" ) ]
101- pub tag_type : TagType ,
102- pub closing : Option < String > ,
103+ pub end : Option < EndTag > ,
103104 #[ serde( default ) ]
104- pub branches : Option < Vec < String > > ,
105- pub args : Option < Vec < ArgSpec > > ,
105+ pub intermediates : Option < Vec < String > > ,
106106}
107107
108108impl TagSpec {
109+ /// Recursive extraction: Check if node is spec, otherwise recurse if table.
109110 fn extract_specs (
110111 value : & Value ,
111- prefix : Option < & str > ,
112+ prefix : Option < & str > , // Path *to* this value node
112113 specs : & mut HashMap < String , TagSpec > ,
113114 ) -> Result < ( ) , String > {
114- // Try to deserialize as a tag spec first
115- match TagSpec :: deserialize ( value. clone ( ) ) {
116- Ok ( tag_spec) => {
117- let name = prefix. map_or_else ( String :: new, |p| {
118- p. split ( '.' ) . last ( ) . unwrap_or ( p) . to_string ( )
119- } ) ;
120- specs. insert ( name, tag_spec) ;
115+ // Check if the current node *itself* represents a TagSpec definition
116+ // We can be more specific: check if it's a table containing 'end' or 'intermediates'
117+ let mut is_spec_node = false ;
118+ if let Some ( table) = value. as_table ( ) {
119+ if table. contains_key ( "end" ) || table. contains_key ( "intermediates" ) {
120+ // Looks like a spec, try to deserialize
121+ match TagSpec :: deserialize ( value. clone ( ) ) {
122+ Ok ( tag_spec) => {
123+ // It is a TagSpec. Get name from prefix.
124+ if let Some ( p) = prefix {
125+ if let Some ( name) = p. split ( '.' ) . next_back ( ) . filter ( |s| !s. is_empty ( ) ) {
126+ specs. insert ( name. to_string ( ) , tag_spec) ;
127+ is_spec_node = true ;
128+ } else {
129+ return Err ( format ! (
130+ "Invalid prefix '{}' resulted in empty tag name component." ,
131+ p
132+ ) ) ;
133+ }
134+ } else {
135+ return Err ( "Cannot determine tag name for TagSpec: prefix is None."
136+ . to_string ( ) ) ;
137+ }
138+ }
139+ Err ( e) => {
140+ // Looked like a spec but failed to deserialize. This is an error.
141+ return Err ( format ! (
142+ "Failed to deserialize potential TagSpec at prefix '{}': {}" ,
143+ prefix. unwrap_or( "<root>" ) ,
144+ e
145+ ) ) ;
146+ }
147+ }
121148 }
122- Err ( _) => {
123- // Not a tag spec, try recursing into any table values
124- for ( key, value) in value. as_table ( ) . iter ( ) . flat_map ( |t| t. iter ( ) ) {
149+ }
150+
151+ // If the node was successfully processed as a spec, DO NOT recurse into its fields.
152+ // Otherwise, if it's a table, recurse into its children.
153+ if !is_spec_node {
154+ if let Some ( table) = value. as_table ( ) {
155+ for ( key, inner_value) in table. iter ( ) {
125156 let new_prefix = match prefix {
126157 None => key. clone ( ) ,
127158 Some ( p) => format ! ( "{}.{}" , p, key) ,
128159 } ;
129- Self :: extract_specs ( value , Some ( & new_prefix) , specs) ?;
160+ Self :: extract_specs ( inner_value , Some ( & new_prefix) , specs) ?;
130161 }
131162 }
132163 }
164+
133165 Ok ( ( ) )
134166 }
135167}
136168
137- #[ derive( Clone , Debug , Deserialize , PartialEq ) ]
138- #[ serde( rename_all = "lowercase" ) ]
139- pub enum TagType {
140- Container ,
141- Inclusion ,
142- Single ,
143- }
144-
145- #[ derive( Clone , Debug , Deserialize ) ]
146- pub struct ArgSpec {
147- pub name : String ,
148- pub required : bool ,
169+ #[ derive( Debug , Clone , Serialize , Deserialize , PartialEq ) ]
170+ pub struct EndTag {
171+ pub tag : String ,
149172 #[ serde( default ) ]
150- pub allowed_values : Option < Vec < String > > ,
151- #[ serde( default ) ]
152- pub is_kwarg : bool ,
153- }
154-
155- impl ArgSpec {
156- pub fn is_placeholder ( arg : & str ) -> bool {
157- arg. starts_with ( '{' ) && arg. ends_with ( '}' )
158- }
159-
160- pub fn get_placeholder_name ( arg : & str ) -> Option < & str > {
161- if Self :: is_placeholder ( arg) {
162- Some ( & arg[ 1 ..arg. len ( ) - 1 ] )
163- } else {
164- None
165- }
166- }
173+ pub optional : bool ,
167174}
168175
169176#[ cfg( test) ]
170177mod tests {
171178 use super :: * ;
179+ use std:: fs;
172180
173181 #[ test]
174182 fn test_can_load_builtins ( ) -> Result < ( ) , anyhow:: Error > {
175183 let specs = TagSpecs :: load_builtin_specs ( ) ?;
176184
177185 assert ! ( !specs. 0 . is_empty( ) , "Should have loaded at least one spec" ) ;
178186
187+ assert ! ( specs. get( "if" ) . is_some( ) , "'if' tag should be present" ) ;
188+
179189 for name in specs. 0 . keys ( ) {
180190 assert ! ( !name. is_empty( ) , "Tag name should not be empty" ) ;
181191 }
@@ -190,29 +200,34 @@ mod tests {
190200 "autoescape" ,
191201 "block" ,
192202 "comment" ,
193- "cycle" ,
194- "debug" ,
195- "extends" ,
196203 "filter" ,
197204 "for" ,
198- "firstof" ,
199205 "if" ,
200- "include" ,
201- "load" ,
202- "now" ,
206+ "ifchanged" ,
203207 "spaceless" ,
204- "templatetag" ,
205- "url" ,
206208 "verbatim" ,
207209 "with" ,
210+ "cache" ,
211+ "localize" ,
212+ "blocktranslate" ,
213+ "localtime" ,
214+ "timezone" ,
208215 ] ;
209216 let missing_tags = [
210217 "csrf_token" ,
211- "ifchanged" ,
218+ "cycle" ,
219+ "debug" ,
220+ "extends" ,
221+ "firstof" ,
222+ "include" ,
223+ "load" ,
212224 "lorem" ,
225+ "now" ,
213226 "querystring" , // 5.1
214227 "regroup" ,
215228 "resetcycle" ,
229+ "templatetag" ,
230+ "url" ,
216231 "widthratio" ,
217232 ] ;
218233
@@ -237,33 +252,44 @@ mod tests {
237252 let root = dir. path ( ) ;
238253
239254 let pyproject_content = r#"
240- [tool.djls.template.tags.mytag]
241- type = "container"
242- closing = "endmytag"
243- branches = ["mybranch"]
244- args = [{ name = "myarg", required = true }]
255+ [tool.djls.tagspecs.mytag]
256+ end = { tag = "endmytag" }
257+ intermediates = ["mybranch"]
258+
259+ [tool.djls.tagspecs.anothertag]
260+ end = { tag = "endanothertag", optional = true }
245261"# ;
246262 fs:: write ( root. join ( "pyproject.toml" ) , pyproject_content) ?;
247263
264+ // Load all (built-in + user)
248265 let specs = TagSpecs :: load_all ( root) ?;
249266
250- let if_tag = specs. get ( "if" ) . expect ( "if tag should be present" ) ;
251- assert_eq ! ( if_tag. tag_type, TagType :: Container ) ;
267+ assert ! ( specs. get( "if" ) . is_some( ) , "'if' tag should be present" ) ;
252268
253269 let my_tag = specs. get ( "mytag" ) . expect ( "mytag should be present" ) ;
254- assert_eq ! ( my_tag. tag_type, TagType :: Container ) ;
255- assert_eq ! ( my_tag. closing, Some ( "endmytag" . to_string( ) ) ) ;
256-
257- let branches = my_tag
258- . branches
259- . as_ref ( )
260- . expect ( "mytag should have branches" ) ;
261- assert ! ( branches. iter( ) . any( |b| b == "mybranch" ) ) ;
262-
263- let args = my_tag. args . as_ref ( ) . expect ( "mytag should have args" ) ;
264- let arg = & args[ 0 ] ;
265- assert_eq ! ( arg. name, "myarg" ) ;
266- assert ! ( arg. required) ;
270+ assert_eq ! (
271+ my_tag. end,
272+ Some ( EndTag {
273+ tag: "endmytag" . to_string( ) ,
274+ optional: false
275+ } )
276+ ) ;
277+ assert_eq ! ( my_tag. intermediates, Some ( vec![ "mybranch" . to_string( ) ] ) ) ;
278+
279+ let another_tag = specs
280+ . get ( "anothertag" )
281+ . expect ( "anothertag should be present" ) ;
282+ assert_eq ! (
283+ another_tag. end,
284+ Some ( EndTag {
285+ tag: "endanothertag" . to_string( ) ,
286+ optional: true
287+ } )
288+ ) ;
289+ assert ! (
290+ another_tag. intermediates. is_none( ) ,
291+ "anothertag should have no intermediates"
292+ ) ;
267293
268294 dir. close ( ) ?;
269295 Ok ( ( ) )
@@ -274,36 +300,45 @@ args = [{ name = "myarg", required = true }]
274300 let dir = tempfile:: tempdir ( ) ?;
275301 let root = dir. path ( ) ;
276302
303+ // djls.toml has higher priority
277304 let djls_content = r#"
278- [mytag1]
279- type = "container"
280- closing = "endmytag1"
305+ [tagspecs.mytag1]
306+ end = { tag = "endmytag1_from_djls" }
281307"# ;
282308 fs:: write ( root. join ( "djls.toml" ) , djls_content) ?;
283309
310+ // pyproject.toml has lower priority
284311 let pyproject_content = r#"
285- [tool.djls.template.tags]
286- mytag2.type = "container"
287- mytag2.closing = "endmytag2"
312+ [tool.djls.tagspecs.mytag1]
313+ end = { tag = "endmytag1_from_pyproject" }
314+
315+ [tool.djls.tagspecs.mytag2]
316+ end = { tag = "endmytag2_from_pyproject" }
288317"# ;
289318 fs:: write ( root. join ( "pyproject.toml" ) , pyproject_content) ?;
290319
291320 let specs = TagSpecs :: load_user_specs ( root) ?;
292321
293- assert ! ( specs. get( "mytag1" ) . is_some( ) , "mytag1 should be present" ) ;
322+ let tag1 = specs. get ( "mytag1" ) . expect ( "mytag1 should be present" ) ;
323+ assert_eq ! ( tag1. end. as_ref( ) . unwrap( ) . tag, "endmytag1_from_djls" ) ;
324+
325+ // Should not find mytag2 because djls.toml was found first
294326 assert ! (
295327 specs. get( "mytag2" ) . is_none( ) ,
296- "mytag2 should not be present"
328+ "mytag2 should not be present when djls.toml exists "
297329 ) ;
298330
331+ // Remove djls.toml, now pyproject.toml should be used
299332 fs:: remove_file ( root. join ( "djls.toml" ) ) ?;
300333 let specs = TagSpecs :: load_user_specs ( root) ?;
301334
335+ let tag1 = specs. get ( "mytag1" ) . expect ( "mytag1 should be present now" ) ;
336+ assert_eq ! ( tag1. end. as_ref( ) . unwrap( ) . tag, "endmytag1_from_pyproject" ) ;
337+
302338 assert ! (
303- specs. get( "mytag1 " ) . is_none ( ) ,
304- "mytag1 should not be present"
339+ specs. get( "mytag2 " ) . is_some ( ) ,
340+ "mytag2 should be present when only pyproject.toml exists "
305341 ) ;
306- assert ! ( specs. get( "mytag2" ) . is_some( ) , "mytag2 should be present" ) ;
307342
308343 dir. close ( ) ?;
309344 Ok ( ( ) )
0 commit comments