diff --git a/.changeset/eight-teachers-sniff.md b/.changeset/eight-teachers-sniff.md new file mode 100644 index 000000000..ac6b9ae77 --- /dev/null +++ b/.changeset/eight-teachers-sniff.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Properly handle nested expressions that return multiple elements diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index ac3cde831..c3c5cd02a 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -619,6 +619,27 @@ const groups = [[0, 1, 2], [3, 4, 5]]; code: `${$$maybeRenderHead($$result)}
${(previous || next) && $$render` + BACKTICK + `` + BACKTICK + `}
`, }, }, + { + name: "nested expressions II", + source: `
{(previous || next) && }
`, + want: want{ + code: `${$$maybeRenderHead($$result)}
${(previous || next) && $$render` + BACKTICK + `` + BACKTICK + `}
`, + }, + }, + { + name: "nested expressions III", + source: `
{x.map((x) => x ?
{true ? {x} : null}
:
{false ? null : {x}}
)}
`, + want: want{ + code: "${$$maybeRenderHead($$result)}
${x.map((x) => x ? $$render`
${true ? $$render`${x}` : null}
` : $$render`
${false ? null : $$render`${x}`}
`)}
", + }, + }, + { + name: "nested expressions IV", + source: `
{() => { if (value > 0.25) { return Default } else if (value > 0.5) { return Another } else if (value > 0.75) { return Other } return Yet Other }}
`, + want: want{ + code: "${$$maybeRenderHead($$result)}
${() => { if (value > 0.25) { return $$render`Default`} else if (value > 0.5) { return $$render`Another`} else if (value > 0.75) { return $$render`Other`} return $$render`Yet Other`}}
", + }, + }, { name: "expressions with JS comments", source: `--- diff --git a/internal/token.go b/internal/token.go index 8bfe7b4dc..15cf0d20e 100644 --- a/internal/token.go +++ b/internal/token.go @@ -250,6 +250,7 @@ type Tokenizer struct { // tt is the TokenType of the current token. tt TokenType prevTokenType TokenType + prevTokens []Token fm FrontmatterState m MarkdownState // err is the first error encountered during tokenization. It is possible @@ -1331,23 +1332,51 @@ func (z *Tokenizer) isAtExpressionBoundary() bool { return true } +func (z *Tokenizer) trackPreviousTokens() { + // Reset stack on expression boundaries + if z.tt == StartExpressionToken || z.tt == EndExpressionToken { + z.prevTokens = make([]Token, 0) + } + if z.tt == StartTagToken { + z.prevTokens = append(z.prevTokens, z.Token()) + } else if z.tt == EndTagToken { + if len(z.prevTokens) > 0 { + // This is a very simple stack that pops matching closing elements off the stack, + // which is good enough for our purposes. + // We only use this to track when `{` should be `StartExpressionToken` or `TextToken` + for i := 1; i < len(z.prevTokens)+1; i++ { + tok := z.prevTokens[len(z.prevTokens)-i] + if tok.Data == string(z.buf[z.data.Start:z.data.End]) { + if len(z.prevTokens) == 1 { + z.prevTokens = make([]Token, 0) + } else { + z.prevTokens = z.prevTokens[0:i] + z.prevTokens = append(z.prevTokens, z.prevTokens[i:]...) + } + } + } + } + } +} + // Next scans the next token and returns its type. func (z *Tokenizer) Next() TokenType { + z.trackPreviousTokens() z.raw.Start = z.raw.End z.data.Start = z.raw.End z.data.End = z.raw.End z.prevTokenType = z.tt - // This handles expressions nested inside of Frontmatter elements - // but preserves `{}` as text outside of elements - if z.fm == FrontmatterOpen { + // Properly handle multiple nested expressions + if len(z.expressionStack) > 0 && len(z.prevTokens) == 0 { tt := z.Token().Type switch tt { - case StartTagToken, EndTagToken: + case StartTagToken, EndExpressionToken, TextToken: default: z.openBraceIsExpressionStart = false } } + if z.rawTag != "" { if z.rawTag == "plaintext" { // Read everything up to EOF. diff --git a/internal/token_test.go b/internal/token_test.go index 1a5e035a5..1f1cd8f03 100644 --- a/internal/token_test.go +++ b/internal/token_test.go @@ -198,6 +198,20 @@ func TestBasic(t *testing.T) { }}`, []TokenType{StartTagToken, StartExpressionToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, TextToken, EndExpressionToken, EndTagToken}, }, + { + "expression with multiple elements", + `
{() => { + if (value > 0.25) { + return Default + } else if (value > 0.5) { + return Another + } else if (value > 0.75) { + return Other + } + return Yet Other + }}
`, + []TokenType{StartTagToken, StartExpressionToken, TextToken, TextToken, TextToken, TextToken, TextToken, StartTagToken, TextToken, EndTagToken, TextToken, TextToken, TextToken, TextToken, StartTagToken, TextToken, EndTagToken, TextToken, TextToken, TextToken, TextToken, StartTagToken, TextToken, EndTagToken, TextToken, TextToken, StartTagToken, TextToken, EndTagToken, TextToken, TextToken, EndExpressionToken, EndTagToken}, + }, { "attribute expression with quoted braces", `
`,