diff --git a/CHANGELOG.md b/CHANGELOG.md index a6a5a2958..621a94e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2.2.0 (unreleased) - Do not reserialize existing Value when serializing to a Value +- Allow trimming start/end of component definition with metadata `{"trim": true}` ## 2.1.1 (2026-08-11) diff --git a/tera/src/parsing/ast.rs b/tera/src/parsing/ast.rs index 9d6500e05..041bbf4f1 100644 --- a/tera/src/parsing/ast.rs +++ b/tera/src/parsing/ast.rs @@ -755,6 +755,43 @@ impl ComponentDefinition { self.kwargs.keys().map(|k| k.as_str()).collect() } + /// Trim body unless the `trim` metadata field is set to `false` or unset + pub fn maybe_trim_body(&mut self) { + let needs_trimming = self + .metadata + .get("trim") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + if !needs_trimming { + return; + } + + // We might have multiple Node::Content at start/end if there were comment nodes that got dropped + while let Some(Node::Content(c)) = self.body.first_mut() { + let t = c.trim_start(); + if t.is_empty() { + self.body.remove(0); + } else { + if t.len() != c.len() { + *c = t.to_string(); + } + break; + } + } + + while let Some(Node::Content(c)) = self.body.last_mut() { + let t = c.trim_end(); + if t.is_empty() { + self.body.pop(); + } else { + if t.len() != c.len() { + *c = t.to_string(); + } + break; + } + } + } + /// Builds a validated context from provided kwargs, checking types and applying defaults. /// If rest_param_name is defined, unknown kwargs are collected into it. /// Otherwise, unknown kwargs will error. diff --git a/tera/src/parsing/parser.rs b/tera/src/parsing/parser.rs index 6970e178d..4533e892d 100644 --- a/tera/src/parsing/parser.rs +++ b/tera/src/parsing/parser.rs @@ -1450,6 +1450,8 @@ impl<'a> Parser<'a> { self.body_contexts.pop(); component_def.body = body; + component_def.maybe_trim_body(); + Ok(component_def) } diff --git a/tera/src/snapshot_tests/parser.rs b/tera/src/snapshot_tests/parser.rs index 38c9d90a8..ca5358dab 100644 --- a/tera/src/snapshot_tests/parser.rs +++ b/tera/src/snapshot_tests/parser.rs @@ -56,6 +56,7 @@ fn parser_errors() { fn parser_components_definition_success() { insta::glob!("parser_inputs/success/components/def/*.txt", |path| { let contents = std::fs::read_to_string(path).unwrap(); + let contents = normalize_line_endings(&contents); let components = &Parser::new("", &contents, Delimiters::default()) .parse() .unwrap() @@ -68,6 +69,7 @@ fn parser_components_definition_success() { fn parser_components_render_success() { insta::glob!("parser_inputs/success/components/*.txt", |path| { let contents = std::fs::read_to_string(path).unwrap(); + let contents = normalize_line_endings(&contents); println!("{path:?}"); let nodes = &Parser::new("", &contents, Delimiters::default()) .parse() diff --git a/tera/src/snapshot_tests/parser_inputs/success/components/def/trim_default.txt b/tera/src/snapshot_tests/parser_inputs/success/components/def/trim_default.txt new file mode 100644 index 000000000..4ee1f2a92 --- /dev/null +++ b/tera/src/snapshot_tests/parser_inputs/success/components/def/trim_default.txt @@ -0,0 +1,3 @@ +{% component greet(label) %} +{{label}} +{% endcomponent %} diff --git a/tera/src/snapshot_tests/parser_inputs/success/components/def/trim_default_with_comment.txt b/tera/src/snapshot_tests/parser_inputs/success/components/def/trim_default_with_comment.txt new file mode 100644 index 000000000..933b7302b --- /dev/null +++ b/tera/src/snapshot_tests/parser_inputs/success/components/def/trim_default_with_comment.txt @@ -0,0 +1,4 @@ +{% component greet(label) %} +{# this has ws after #} +{{label}} +{% endcomponent %} diff --git a/tera/src/snapshot_tests/parser_inputs/success/components/def/trim_optin.txt b/tera/src/snapshot_tests/parser_inputs/success/components/def/trim_optin.txt new file mode 100644 index 000000000..b2ea89288 --- /dev/null +++ b/tera/src/snapshot_tests/parser_inputs/success/components/def/trim_optin.txt @@ -0,0 +1,3 @@ +{% component greet(label) {"trim": true} %} +{{label}} +{% endcomponent %} diff --git a/tera/src/snapshot_tests/rendering_inputs/success/components/trim_body_def.txt b/tera/src/snapshot_tests/rendering_inputs/success/components/trim_body_def.txt new file mode 100644 index 000000000..78911bc19 --- /dev/null +++ b/tera/src/snapshot_tests/rendering_inputs/success/components/trim_body_def.txt @@ -0,0 +1,9 @@ +$$ components +{% component coverlink(dest) {"trim": true} %} +{{ dest }} +{% endcomponent coverlink %} +$$ tpl +
+{{}} +{{}} +
\ No newline at end of file diff --git a/tera/src/snapshot_tests/rendering_inputs/success/components/trim_def_doesnt_touch_body.txt b/tera/src/snapshot_tests/rendering_inputs/success/components/trim_def_doesnt_touch_body.txt new file mode 100644 index 000000000..32a8ad0a8 --- /dev/null +++ b/tera/src/snapshot_tests/rendering_inputs/success/components/trim_def_doesnt_touch_body.txt @@ -0,0 +1,8 @@ +$$ components +{% component note() {"trim": true} %} + +{% endcomponent note %} +$$ tpl +{% %} + hello +{% %} \ No newline at end of file diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_default.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_default.txt.snap new file mode 100644 index 000000000..dfc2466ce --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_default.txt.snap @@ -0,0 +1,23 @@ +--- +source: tera/src/snapshot_tests/parser.rs +expression: "components[0]" +input_file: tera/src/snapshot_tests/parser_inputs/success/components/def/trim_default.txt +--- +ComponentDefinition { + name: "greet", + kwargs: { + "label": ComponentArgument { + default: None, + typ: None, + }, + }, + rest_param_name: None, + metadata: {}, + body: [ + "\n", + Var { + name: "label", + } @ 2:2-2:7 (31..36), + "\n", + ], +} diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_default_with_comment.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_default_with_comment.txt.snap new file mode 100644 index 000000000..99f8bbc77 --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_default_with_comment.txt.snap @@ -0,0 +1,24 @@ +--- +source: tera/src/snapshot_tests/parser.rs +expression: "components[0]" +input_file: tera/src/snapshot_tests/parser_inputs/success/components/def/trim_default_with_comment.txt +--- +ComponentDefinition { + name: "greet", + kwargs: { + "label": ComponentArgument { + default: None, + typ: None, + }, + }, + rest_param_name: None, + metadata: {}, + body: [ + "\n", + "\n", + Var { + name: "label", + } @ 3:2-3:7 (55..60), + "\n", + ], +} diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_optin.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_optin.txt.snap new file mode 100644 index 000000000..7249c5e78 --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_optin.txt.snap @@ -0,0 +1,23 @@ +--- +source: tera/src/snapshot_tests/parser.rs +expression: "components[0]" +input_file: tera/src/snapshot_tests/parser_inputs/success/components/def/trim_optin.txt +--- +ComponentDefinition { + name: "greet", + kwargs: { + "label": ComponentArgument { + default: None, + typ: None, + }, + }, + rest_param_name: None, + metadata: { + "trim": Bool(true), + }, + body: [ + Var { + name: "label", + } @ 2:2-2:7 (46..51), + ], +} diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_optout.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_optout.txt.snap new file mode 100644 index 000000000..f9b83a4b3 --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_components_definition_success@trim_optout.txt.snap @@ -0,0 +1,25 @@ +--- +source: tera/src/snapshot_tests/parser.rs +expression: "components[0]" +input_file: tera/src/snapshot_tests/parser_inputs/success/components/def/trim_optout.txt +--- +ComponentDefinition { + name: "greet", + kwargs: { + "label": ComponentArgument { + default: None, + typ: None, + }, + }, + rest_param_name: None, + metadata: { + "trim": Bool(false), + }, + body: [ + "\n", + Var { + name: "label", + } @ 2:2-2:7 (47..52), + "\n", + ], +} diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_components_ok@trim_body_def.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_components_ok@trim_body_def.txt.snap new file mode 100644 index 000000000..ddb8b56b7 --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_components_ok@trim_body_def.txt.snap @@ -0,0 +1,9 @@ +--- +source: tera/src/snapshot_tests/rendering.rs +expression: "&out" +input_file: tera/src/snapshot_tests/rendering_inputs/success/components/trim_body_def.txt +--- +
+a +b +
diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_components_ok@trim_def_doesnt_touch_body.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_components_ok@trim_def_doesnt_touch_body.txt.snap new file mode 100644 index 000000000..2f6dcc755 --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_components_ok@trim_def_doesnt_touch_body.txt.snap @@ -0,0 +1,8 @@ +--- +source: tera/src/snapshot_tests/rendering.rs +expression: "&out" +input_file: tera/src/snapshot_tests/rendering_inputs/success/components/trim_def_doesnt_touch_body.txt +--- + diff --git a/tera/tests/snapshots/basic__rendering_realistic_benchmark.snap b/tera/tests/snapshots/basic__rendering_realistic_benchmark.snap index 3581fd4cc..66c009603 100644 --- a/tera/tests/snapshots/basic__rendering_realistic_benchmark.snap +++ b/tera/tests/snapshots/basic__rendering_realistic_benchmark.snap @@ -21,9 +21,7 @@ expression: out API Docs - - -