From 29c625ad87b810985e351652231e91c0e32801aa Mon Sep 17 00:00:00 2001 From: Yash Goel <162511050+yashhzd@users.noreply.github.com> Date: Thu, 26 Feb 2026 07:19:46 +0530 Subject: [PATCH] fix: preserve parent data context for primitive optional blocks When an optional block guards a primitive value (string, number, boolean), the runtime incorrectly navigated into the property scope, causing named variables like {{age}} inside {{#optional age}} to resolve as $['age']['age'] instead of $['age']. This also affected the pre-processing step which tried to treat primitive values as objects. Changes: - getJsonPath: skip path navigation for primitive OptionalDefinition nodes so variables resolve from the parent data context - generateOptionalBlocks: skip pre-processing for primitive optional values and let the normal traverse handle them - Update test templates to use named variables instead of {{this}} for primitive optionals - Add conditional-variables test template Closes accordproject/template-playground#2 Signed-off-by: Yash Goel <162511050+yashhzd@users.noreply.github.com> --- src/TemplateMarkInterpreter.ts | 18 +- .../TemplateMarkInterpreter.test.ts.snap | 209 +++++++++++++++++- .../good/conditional-variables/data.json | 6 + .../good/conditional-variables/model.cto | 8 + .../good/conditional-variables/template.md | 5 + .../good/optional-nested/template.md | 2 +- .../good/optional_comprehensive/template.md | 10 +- 7 files changed, 241 insertions(+), 17 deletions(-) create mode 100644 test/templates/good/conditional-variables/data.json create mode 100644 test/templates/good/conditional-variables/model.cto create mode 100644 test/templates/good/conditional-variables/template.md diff --git a/src/TemplateMarkInterpreter.ts b/src/TemplateMarkInterpreter.ts index c9c2e09..74daab7 100644 --- a/src/TemplateMarkInterpreter.ts +++ b/src/TemplateMarkInterpreter.ts @@ -154,7 +154,13 @@ function getJsonPath(rootData: any, currentNode: any, paths: string[]): string { if (obj && obj.$class) { if (NAVIGATION_NODES.indexOf(obj.$class) >= 0) { if(obj.name !== 'top') { // HACK!! - withPath.push(`['${obj.name}']`); + // For primitive optional definitions, do not navigate into the + // property scope — variables inside should resolve from the parent context + const isOptional = OPTIONAL_DEFINITION_RE.test(obj.$class); + const isPrimitive = isOptional && obj.elementType && (ModelUtil as any).isPrimitiveType(obj.elementType); + if (!isPrimitive) { + withPath.push(`['${obj.name}']`); + } } } } @@ -244,6 +250,16 @@ async function generateOptionalBlocks(modelManager: ModelManager, clauseLibrary: if (variableValues.length > 0) { // Optional property exists, process whenSome with the property value as context const optionalPropertyValue = variableValues[0]; + + // For primitive optional values (string, number, boolean), skip + // pre-processing and let the normal traverse handle it with the + // full parent data context. This allows named variables like {{age}} + // to resolve correctly inside {{#optional age}}...{{/optional}}. + const isPrimitive = typeof optionalPropertyValue !== 'object' || optionalPropertyValue === null; + if (isPrimitive) { + continue; + } + if (context.whenSome && context.whenSome.length > 0) { // Create a paragraph wrapper for the whenSome content const whenSomeParagraph = { diff --git a/test/__snapshots__/TemplateMarkInterpreter.test.ts.snap b/test/__snapshots__/TemplateMarkInterpreter.test.ts.snap index ab6e431..7e50faf 100644 --- a/test/__snapshots__/TemplateMarkInterpreter.test.ts.snap +++ b/test/__snapshots__/TemplateMarkInterpreter.test.ts.snap @@ -189,6 +189,135 @@ exports[`templatemark interpreter should generate clause-optional 1`] = ` } `; +exports[`templatemark interpreter should generate conditional-variables 1`] = ` +{ + "$class": "org.accordproject.commonmark@0.5.0.Document", + "nodes": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Paragraph", + "nodes": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Paragraph", + "nodes": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "Hello ", + }, + { + "$class": "org.accordproject.ciceromark@0.6.0.Variable", + "elementType": "String", + "name": "name", + "value": "Alice", + }, + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": ".", + }, + ], + }, + { + "$class": "org.accordproject.commonmark@0.5.0.Paragraph", + "nodes": [ + { + "$class": "org.accordproject.ciceromark@0.6.0.Conditional", + "isTrue": true, + "name": "age", + "nodes": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "You are ", + }, + { + "$class": "org.accordproject.ciceromark@0.6.0.Variable", + "elementType": "Integer", + "name": "age", + "value": "30", + }, + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": " years old.", + }, + ], + "whenFalse": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "Age unknown.", + }, + ], + "whenTrue": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "You are ", + }, + { + "$class": "org.accordproject.ciceromark@0.6.0.Variable", + "elementType": "Integer", + "name": "age", + "value": "30", + }, + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": " years old.", + }, + ], + }, + ], + }, + { + "$class": "org.accordproject.commonmark@0.5.0.Paragraph", + "nodes": [ + { + "$class": "org.accordproject.ciceromark@0.6.0.Conditional", + "isTrue": true, + "name": "isActive", + "nodes": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "Active user: ", + }, + { + "$class": "org.accordproject.ciceromark@0.6.0.Variable", + "elementType": "String", + "name": "name", + "value": "Alice", + }, + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": ".", + }, + ], + "whenFalse": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "Inactive.", + }, + ], + "whenTrue": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "Active user: ", + }, + { + "$class": "org.accordproject.ciceromark@0.6.0.Variable", + "elementType": "String", + "name": "name", + "value": "Alice", + }, + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": ".", + }, + ], + }, + ], + }, + ], + }, + ], + "xmlns": "http://commonmark.org/xml/1.0", +} +`; + exports[`templatemark interpreter should generate formula-now 1`] = ` { "$class": "org.accordproject.commonmark@0.5.0.Document", @@ -2923,12 +3052,23 @@ exports[`templatemark interpreter should generate optional_comprehensive 1`] = ` { "$class": "org.accordproject.ciceromark@0.6.0.Variable", "elementType": "Integer", - "name": "this", + "name": "age", "value": "30", }, ], "whenNone": [], - "whenSome": [], + "whenSome": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "Age is ", + }, + { + "$class": "org.accordproject.ciceromark@0.6.0.Variable", + "elementType": "Integer", + "name": "age", + "value": "30", + }, + ], }, ], }, @@ -2990,12 +3130,23 @@ exports[`templatemark interpreter should generate optional_comprehensive 1`] = ` { "$class": "org.accordproject.ciceromark@0.6.0.Variable", "elementType": "Boolean", - "name": "this", + "name": "active", "value": "true", }, ], "whenNone": [], - "whenSome": [], + "whenSome": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "Status: ", + }, + { + "$class": "org.accordproject.ciceromark@0.6.0.Variable", + "elementType": "Boolean", + "name": "active", + "value": "true", + }, + ], }, ], }, @@ -3097,7 +3248,7 @@ exports[`templatemark interpreter should generate optional_comprehensive 1`] = ` { "$class": "org.accordproject.ciceromark@0.6.0.Variable", "elementType": "Integer", - "name": "this", + "name": "age", "value": "25", }, { @@ -3106,7 +3257,22 @@ exports[`templatemark interpreter should generate optional_comprehensive 1`] = ` }, ], "whenNone": [], - "whenSome": [], + "whenSome": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "(", + }, + { + "$class": "org.accordproject.ciceromark@0.6.0.Variable", + "elementType": "Integer", + "name": "age", + "value": "25", + }, + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": " years old)", + }, + ], }, ], "whenNone": [], @@ -3141,12 +3307,24 @@ exports[`templatemark interpreter should generate optional_comprehensive 1`] = ` "$class": "org.accordproject.ciceromark@0.6.0.FormattedVariable", "elementType": "DateTime", "format": "MMMM DD, YYYY", - "name": "this", + "name": "lastVisit", "value": "December 01, 2023", }, ], "whenNone": [], - "whenSome": [], + "whenSome": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "Last visit: ", + }, + { + "$class": "org.accordproject.ciceromark@0.6.0.FormattedVariable", + "elementType": "DateTime", + "format": "MMMM DD, YYYY", + "name": "lastVisit", + "value": "December 01, 2023", + }, + ], }, ], }, @@ -3783,12 +3961,23 @@ exports[`templatemark interpreter should generate optional-nested 1`] = ` { "$class": "org.accordproject.ciceromark@0.6.0.Variable", "elementType": "Integer", - "name": "this", + "name": "age", "value": "42", }, ], "whenNone": [], - "whenSome": [], + "whenSome": [ + { + "$class": "org.accordproject.commonmark@0.5.0.Text", + "text": "Age is provided as ", + }, + { + "$class": "org.accordproject.ciceromark@0.6.0.Variable", + "elementType": "Integer", + "name": "age", + "value": "42", + }, + ], }, ], }, diff --git a/test/templates/good/conditional-variables/data.json b/test/templates/good/conditional-variables/data.json new file mode 100644 index 0000000..73f138a --- /dev/null +++ b/test/templates/good/conditional-variables/data.json @@ -0,0 +1,6 @@ +{ + "$class": "test@1.0.0.TemplateData", + "age": 30, + "name": "Alice", + "isActive": true +} diff --git a/test/templates/good/conditional-variables/model.cto b/test/templates/good/conditional-variables/model.cto new file mode 100644 index 0000000..e092540 --- /dev/null +++ b/test/templates/good/conditional-variables/model.cto @@ -0,0 +1,8 @@ +namespace test@1.0.0 + +@template +concept TemplateData { + o Integer age optional + o String name + o Boolean isActive optional +} diff --git a/test/templates/good/conditional-variables/template.md b/test/templates/good/conditional-variables/template.md new file mode 100644 index 0000000..ed1d4d0 --- /dev/null +++ b/test/templates/good/conditional-variables/template.md @@ -0,0 +1,5 @@ +Hello {{name}}. + +{{#if age}}You are {{age}} years old.{{else}}Age unknown.{{/if}} + +{{#if isActive}}Active user: {{name}}.{{else}}Inactive.{{/if}} diff --git a/test/templates/good/optional-nested/template.md b/test/templates/good/optional-nested/template.md index b0e83d6..60dc143 100644 --- a/test/templates/good/optional-nested/template.md +++ b/test/templates/good/optional-nested/template.md @@ -53,7 +53,7 @@ Order total: {{% return '£' + order.orderLines.map(ol => ol.price * ol.quantity ### Verification Checking logic for top-level optionals: -{{#optional age}}Age is provided as {{this}}{{else}}Age is hidden{{/optional}} +{{#optional age}}Age is provided as {{age}}{{else}}Age is hidden{{/optional}} diff --git a/test/templates/good/optional_comprehensive/template.md b/test/templates/good/optional_comprehensive/template.md index 8523626..2e3bcca 100644 --- a/test/templates/good/optional_comprehensive/template.md +++ b/test/templates/good/optional_comprehensive/template.md @@ -2,9 +2,9 @@ ### Scalar Optional Cases -1. Integer: {{#optional age}}Age is {{this}}{{else}}No age{{/optional}} -2. String: {{#optional middleName}}Middle name: {{this}}{{else}}No middle name{{/optional}} -3. Boolean: {{#optional active}}Status: {{this}}{{else}}Status unknown{{/optional}} +1. Integer: {{#optional age}}Age is {{age}}{{else}}No age{{/optional}} +2. String: {{#optional middleName}}Middle name: {{middleName}}{{else}}No middle name{{/optional}} +3. Boolean: {{#optional active}}Status: {{active}}{{else}}Status unknown{{/optional}} ### Object Optional Cases @@ -12,10 +12,10 @@ ### Nested Optional Within Optional -{{#optional person}}Person: {{name}} {{#optional age}}({{this}} years old){{/optional}}{{else}}No person{{/optional}} +{{#optional person}}Person: {{name}} {{#optional age}}({{age}} years old){{/optional}}{{else}}No person{{/optional}} ### Formatted Optional -{{#optional lastVisit}}Last visit: {{this as "MMMM DD, YYYY"}}{{else}}Never visited{{/optional}} +{{#optional lastVisit}}Last visit: {{lastVisit as "MMMM DD, YYYY"}}{{else}}Never visited{{/optional}} Complete.