From a1f8d7cd8fc52af3baf0bac62e4a564591782acf Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Tue, 14 Jul 2026 06:11:59 -0600 Subject: [PATCH] support triple braces in mot files --- CHANGELOG.md | 6 +++++ internal/format/template.go | 40 ++++++++++++++++++++++---------- internal/format/template_test.go | 22 ++++++++++++++++++ 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9df301..02d2d5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Unreleased +* Fix `.mot`/`.mopt` formatting collapsing the space in a single-element array + literal whose element is a Jinja expression (e.g. `{ {{ data[...] }} }`), + which produced an ambiguous `{{{ ... }}}` triple-brace sequence. A single + disambiguating space is now preserved whenever a restored Jinja expression's + own `{{`/`}}` delimiters would otherwise merge with an adjacent Modelica + array brace. * Tweak default formatting for readability (issue #26): * Keep a function call with a single argument on one line instead of breaking the argument onto its own line, e.g. `pre(x)`, `der(y)`, `sin(z)`, diff --git a/internal/format/template.go b/internal/format/template.go index 34fb45f..1b6e67f 100644 --- a/internal/format/template.go +++ b/internal/format/template.go @@ -324,21 +324,37 @@ func reverseSub(text string, sub *subMap) (string, error) { text = blockCommentedSubRegex.ReplaceAllString(text, "${1}") text = trimWhitespaceBeforeRawPunctuation(text, sub) - var firstErr error - restored := normalSubRegex.ReplaceAllStringFunc(text, func(match string) string { - orig, err := sub.getText(match) + matches := normalSubRegex.FindAllStringIndex(text, -1) + var b strings.Builder + last := 0 + for _, m := range matches { + start, end := m[0], m[1] + orig, err := sub.getText(text[start:end]) if err != nil { - if firstErr == nil { - firstErr = err - } - return match + return "", err } - return orig - }) - if firstErr != nil { - return "", firstErr + + b.WriteString(text[last:start]) + + // A restored Jinja expression that starts/ends with its own `{{`/`}}` + // delimiters can collide with an immediately adjacent Modelica array + // brace (e.g. `{{{ ... }}}`), which is ambiguous when the template is + // re-rendered. Insert a single disambiguating space in that case. Only + // `{{`/`}}` expression delimiters are checked (not `{%`/`%}` control + // tags such as `{% raw %}`) so control-statement placeholders are + // unaffected. + if start > 0 && text[start-1] == '{' && strings.HasPrefix(orig, "{{") { + b.WriteByte(' ') + } + b.WriteString(orig) + if end < len(text) && text[end] == '}' && strings.HasSuffix(orig, "}}") { + b.WriteByte(' ') + } + + last = end } - return restored, nil + b.WriteString(text[last:]) + return b.String(), nil } func trimWhitespaceBeforeRawPunctuation(text string, sub *subMap) string { diff --git a/internal/format/template_test.go b/internal/format/template_test.go index b8c27a2..5daedd0 100644 --- a/internal/format/template_test.go +++ b/internal/format/template_test.go @@ -292,6 +292,28 @@ end LoopedArray; a.Contains(formatted, "{% for building in data[\"building_load_files\"] %}") } +// TestSingleElementArrayExpressionKeepsDisambiguatingSpace guards against the +// formatter collapsing `{ {{ expr }} }` into `{{{ expr }}}`. The Modelica +// array formatter normally strips the space inside a single-element array +// literal, but doing so here would make the restored Jinja expression's own +// `{{`/`}}` delimiters collide with the surrounding array braces, producing +// an ambiguous triple-brace sequence when the template is later rendered. +func TestSingleElementArrayExpressionKeepsDisambiguatingSpace(t *testing.T) { + a := require.New(t) + original := `model NominalArray + parameter Real a[3] = fill({ {{ data["nominal_values"]["boiler_efficiency"] }} }); +end NominalArray; +` + var out bytes.Buffer + err := processTemplate(original, &out, Config{-1, false, false, false}, DialectJinja, "nominal-array.mot") + a.NoError(err) + + formatted := out.String() + a.NotContains(formatted, `{{{`, "restored Jinja expression must not merge with a literal Modelica array brace") + a.NotContains(formatted, `}}}`, "restored Jinja expression must not merge with a literal Modelica array brace") + a.Contains(formatted, `{ {{ data["nominal_values"]["boiler_efficiency"] }} }`) +} + func TestInlineExpressionBeforeRawPunctuationDoesNotGainRenderedSpace(t *testing.T) { a := require.New(t) original := `model PumpTemplate