zclsyntax: template interpolation trimming

The ~ character can be used at the start and end of interpolation
sequences to trim off whitespace in neighboring literals, with the goal
of allowing extra whitespace to be included for readability without
including it in the resulting string.
This commit is contained in:
Martin Atkins 2017-06-01 08:43:59 -07:00
parent fdfdfc4f3d
commit 302487ce0f
2 changed files with 85 additions and 4 deletions

View File

@ -90,6 +90,44 @@ hello world
cty.True, // any single expression is unwrapped without stringification
0,
},
{
`trim ${~ "trim"}`,
nil,
cty.StringVal("trimtrim"),
0,
},
{
`${"trim" ~} trim`,
nil,
cty.StringVal("trimtrim"),
0,
},
{
`trim
${~"trim"~}
trim`,
nil,
cty.StringVal("trimtrimtrim"),
0,
},
{
` ${~ true ~} `,
nil,
cty.StringVal("true"), // can't trim space to reduce to a single expression
0,
},
{
`${"hello "}${~"trim"~}${" hello"}`,
nil,
cty.StringVal("hello trim hello"), // trimming can't reach into a neighboring interpolation
0,
},
{
`${true}${~"trim"~}${true}`,
nil,
cty.StringVal("truetrimtrue"), // trimming is no-op of neighbors aren't literal strings
0,
},
}
for _, test := range tests {

View File

@ -4,6 +4,8 @@ import (
"bufio"
"bytes"
"fmt"
"strings"
"unicode"
"github.com/apparentlymart/go-textseg/textseg"
"github.com/zclconf/go-cty/cty"
@ -547,6 +549,8 @@ func (p *parser) ParseTemplate(end TokenType) (Expression, zcl.Diagnostics) {
var diags zcl.Diagnostics
startRange := p.NextRange()
ltrimNext := false
nextCanTrimPrev := false
Token:
for {
@ -556,26 +560,65 @@ Token:
break
}
ltrim := ltrimNext
ltrimNext = false
canTrimPrev := nextCanTrimPrev
nextCanTrimPrev = false
switch next.Type {
case TokenStringLit, TokenQuotedLit:
str, strDiags := p.decodeStringLit(next)
diags = append(diags, strDiags...)
if ltrim {
str = strings.TrimLeftFunc(str, unicode.IsSpace)
}
parts = append(parts, &LiteralValueExpr{
Val: cty.StringVal(str),
SrcRange: next.Range,
})
nextCanTrimPrev = true
case TokenTemplateInterp:
// TODO: if opener has ~ mark, eat trailing spaces in the previous
// literal.
// if the opener is ${~ then we want to eat any trailing whitespace
// in the preceding literal token, assuming it is indeed a literal
// token.
if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 {
prevExpr := parts[len(parts)-1]
if lexpr, ok := prevExpr.(*LiteralValueExpr); ok {
val := lexpr.Val
if val.Type() == cty.String && val.IsKnown() && !val.IsNull() {
str := val.AsString()
str = strings.TrimRightFunc(str, unicode.IsSpace)
lexpr.Val = cty.StringVal(str)
}
}
}
expr, exprDiags := p.ParseExpression()
diags = append(diags, exprDiags...)
close := p.Peek()
if close.Type != TokenTemplateSeqEnd {
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Extra characters after interpolation expression",
Detail: "Expected a closing brace to end the interpolation expression, but found extra characters.",
Subject: &close.Range,
Context: zcl.RangeBetween(startRange, close.Range).Ptr(),
})
}
p.recover(TokenTemplateSeqEnd)
} else {
p.Read() // eat closing brace
// TODO: if closer has ~ mark, remember to eat leading spaces
// in the following literal.
// If the closer is ~} then we want to eat any leading
// whitespace on the next token, if it turns out to be a
// literal token.
if len(close.Bytes) == 2 && close.Bytes[0] == '~' {
ltrimNext = true
}
}
parts = append(parts, expr)
case TokenTemplateControl: