diff --git a/.changeset/chilled-starfishes-search.md b/.changeset/chilled-starfishes-search.md
new file mode 100644
index 000000000..56d23aee3
--- /dev/null
+++ b/.changeset/chilled-starfishes-search.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/compiler': patch
+---
+
+Fixes an issue where unterminated quoted attributes caused the compiler to crash
diff --git a/internal/printer/print-to-json.go b/internal/printer/print-to-json.go
index f9308e555..3367633d1 100644
--- a/internal/printer/print-to-json.go
+++ b/internal/printer/print-to-json.go
@@ -230,7 +230,7 @@ func renderNode(p *printer, parent *ASTNode, n *Node, opts t.ParseOptions) {
}
position := attrPositionAt(p, &attr, opts)
raw := ""
- if attr.Type == QuotedAttribute {
+ if attr.Type == QuotedAttribute || attr.Type == TemplateLiteralAttribute {
start := attr.ValLoc.Start - 1
end := attr.ValLoc.Start + len(attr.Val)
diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go
index e37d7af1e..ed8d40d88 100644
--- a/internal/printer/printer_test.go
+++ b/internal/printer/printer_test.go
@@ -3342,6 +3342,21 @@ const c = '\''
source: `
Hello world!
`,
want: []ASTNode{{Type: "element", Name: "html", Children: []ASTNode{{Type: "element", Name: "body", Children: []ASTNode{{Type: "element", Name: "h1", Children: []ASTNode{{Type: "text", Value: "Hello world!"}}}, {Type: "element", Name: "style"}}}}}},
},
+ {
+ name: "element with unterminated double quote attribute",
+ source: ``,
+ want: []ASTNode{{Type: "element", Name: "main", Attributes: []ASTNode{{Type: "attribute", Kind: "template-literal", Name: "id", Value: "gotcha", Raw: "`gotcha"}}}},
+ },
}
for _, tt := range tests {
diff --git a/internal/token.go b/internal/token.go
index afae341bc..ef2ea3fa2 100644
--- a/internal/token.go
+++ b/internal/token.go
@@ -1448,6 +1448,29 @@ func (z *Tokenizer) readTagAttrVal() {
for {
c := z.readByte()
if z.err != nil {
+ if z.err == io.EOF {
+ // rescan, closing any potentially unterminated quoted attribute values
+ for i := z.pendingAttr[1].Start; i < z.raw.End; i++ {
+ c := z.buf[i]
+ if unicode.IsSpace(rune(c)) || c == '/' || c == '>' {
+ z.pendingAttr[1].End = i
+ break
+ }
+ if i == z.raw.End-1 {
+ z.pendingAttr[1].End = i
+ break
+ }
+ }
+ z.handler.AppendError(&loc.ErrorWithRange{
+ Code: loc.ERROR_UNTERMINATED_STRING,
+ Text: `Unterminated quoted attribute`,
+ Range: loc.Range{
+ Loc: loc.Loc{Start: z.data.Start},
+ Len: z.raw.End,
+ },
+ })
+ return
+ }
z.pendingAttr[1].End = z.raw.End
return
}
@@ -1463,7 +1486,18 @@ func (z *Tokenizer) readTagAttrVal() {
c := z.readByte()
if z.err != nil {
if z.err == io.EOF {
- z.pendingAttr[1].End = z.pendingAttr[1].Start
+ // rescan, closing any potentially unterminated attribute values
+ for i := z.pendingAttr[1].Start; i < z.raw.End; i++ {
+ c := z.buf[i]
+ if unicode.IsSpace(rune(c)) || c == '/' || c == '>' {
+ z.pendingAttr[1].End = i
+ break
+ }
+ if i == z.raw.End-1 {
+ z.pendingAttr[1].End = i
+ break
+ }
+ }
z.handler.AppendError(&loc.ErrorWithRange{
Code: loc.ERROR_UNTERMINATED_STRING,
Text: `Unterminated template literal attribute`,
diff --git a/packages/compiler/test/tsx-sourcemaps/unfinished-literal.ts b/packages/compiler/test/tsx-sourcemaps/unfinished-literal.ts
index 803bcbdc5..22d015005 100644
--- a/packages/compiler/test/tsx-sourcemaps/unfinished-literal.ts
+++ b/packages/compiler/test/tsx-sourcemaps/unfinished-literal.ts
@@ -2,10 +2,9 @@ import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { convertToTSX } from '@astrojs/compiler';
-const input = `
-`;
-
test('does not panic on unfinished template literal attribute', async () => {
+ const input = `
+ `;
let error = 0;
try {
const output = await convertToTSX(input, { filename: 'index.astro', sourcemap: 'inline' });
@@ -16,3 +15,29 @@ test('does not panic on unfinished template literal attribute', async () => {
assert.equal(error, 0, `compiler should not have panicked`);
});
+
+test('does not panic on unfinished double quoted attribute', async () => {
+ const input = ` {
+ const input = `