diff --git a/prettier-plugin-astro/index.js b/prettier-plugin-astro/index.js index a1194de9d0..ef7977351c 100644 --- a/prettier-plugin-astro/index.js +++ b/prettier-plugin-astro/index.js @@ -55,12 +55,13 @@ const findExpressionsInAST = (node, collect = []) => { return collect; } -const formatExpression = ({ expression: { codeStart, codeEnd, children }}, text, options) => { +const formatExpression = ({ expression: { codeChunks, children }}, text, options) => { if (children.length === 0) { - if ([`'`, `"`].includes(codeStart[0])) { - return `` + const codeStart = codeChunks[0]; // If no children, there should only exist a single chunk. + if (codeStart && [`'`, `"`].includes(codeStart[0])) { + return `` } - return `{${codeStart}${codeEnd}}`; + return `{${codeChunks.join('')}}`; } return ``; diff --git a/src/compiler/codegen/index.ts b/src/compiler/codegen/index.ts index 8d968e1ea5..a10268883c 100644 --- a/src/compiler/codegen/index.ts +++ b/src/compiler/codegen/index.ts @@ -75,7 +75,8 @@ function getAttributes(attrs: Attribute[]): Record { } switch (val.type) { case 'MustacheTag': { - result[attr.name] = '(' + val.expression.codeStart + ')'; + // FIXME: this won't work when JSX element can appear in attributes (rare but possible). + result[attr.name] = '(' + val.expression.codeChunks[0] + ')'; continue; } case 'Text': @@ -101,7 +102,8 @@ function getTextFromAttribute(attr: any): string { break; } case 'MustacheTag': { - return attr.expression.codeStart; + // FIXME: this won't work when JSX element can appear in attributes (rare but possible). + return attr.expression.codeChunks[0]; } } throw new Error(`Unknown attribute type ${attr.type}`); @@ -520,13 +522,20 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption enter(node: TemplateNode) { switch (node.type) { case 'Expression': { - let child = ''; + let children: string[] = []; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (node.children!.length) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - child = compileHtml(node.children![0], state, compileOptions); + for (const child of node.children!) { + children.push(compileHtml(child, state, compileOptions)); + } + let raw = ''; + let nextChildIndex = 0; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + for (const chunk of node.codeChunks!) { + raw += chunk; + if (nextChildIndex < children.length) { + raw += children[nextChildIndex++]; + } } - let raw = node.codeStart + child + node.codeEnd; // TODO Do we need to compile this now, or should we compile the entire module at the end? let code = compileExpressionSafe(raw).trim().replace(/\;$/, ''); outSource += `,(${code})`; diff --git a/src/compiler/transform/prism.ts b/src/compiler/transform/prism.ts index 7848e16721..1bb024a845 100644 --- a/src/compiler/transform/prism.ts +++ b/src/compiler/transform/prism.ts @@ -63,8 +63,7 @@ export default function (module: Script): Transformer { type: 'MustacheTag', expression: { type: 'Expression', - codeStart: '`' + escape(code) + '`', - codeEnd: '', + codeChunks: ['`' + escape(code) + '`'], children: [], }, }, diff --git a/src/compiler/transform/styles.ts b/src/compiler/transform/styles.ts index 77eedfa896..53585651f1 100644 --- a/src/compiler/transform/styles.ts +++ b/src/compiler/transform/styles.ts @@ -222,9 +222,10 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr } } else if (attr.value[k].type === 'MustacheTag' && attr.value[k]) { // don‘t add same scopedClass twice (this check is a little more basic, but should suffice) - if (!attr.value[k].expression.codeStart.includes(`' ${scopedClass}'`)) { + if (!attr.value[k].expression.codeChunks[0].includes(`' ${scopedClass}'`)) { // MustacheTag - attr.value[k].expression.codeStart = `(${attr.value[k].expression.codeStart}) + ' ${scopedClass}'`; + // FIXME: this won't work when JSX element can appear in attributes (rare but possible). + attr.value[k].expression.codeChunks[0] = `(${attr.value[k].expression.codeChunks[0]}) + ' ${scopedClass}'`; } } } diff --git a/src/parser/interfaces.ts b/src/parser/interfaces.ts index 4a4d43f718..3273b8be15 100644 --- a/src/parser/interfaces.ts +++ b/src/parser/interfaces.ts @@ -53,8 +53,7 @@ export interface Expression { type: 'Expression'; start: number; end: number; - codeStart: string; - codeEnd: string; + codeChunks: string[]; children: BaseNode[]; } diff --git a/src/parser/parse/read/expression.ts b/src/parser/parse/read/expression.ts index f0033354d0..9d0d091756 100644 --- a/src/parser/parse/read/expression.ts +++ b/src/parser/parse/read/expression.ts @@ -168,12 +168,12 @@ function consume_expression(source: string, start: number): Expression { type: 'Expression', start, end: Number.NaN, - codeStart: '', - codeEnd: '', + codeChunks: [], children: [], }; - let codeEndStart: number = 0; + let codeStart: number = start; + const state: ParseState = { source, start, @@ -196,10 +196,11 @@ function consume_expression(source: string, start: number): Expression { break; } case '<': { - expr.codeStart = source.substring(start, state.index - 1); + const chunk = source.substring(codeStart, state.index - 1); + expr.codeChunks.push(chunk); const tag = consume_tag(state); expr.children.push(tag); - codeEndStart = state.index; + codeStart = state.index; break; } case "'": @@ -225,10 +226,8 @@ function consume_expression(source: string, start: number): Expression { expr.end = state.index - 1; - if (codeEndStart) { - expr.codeEnd = source.substring(codeEndStart, expr.end); - } else { - expr.codeStart = source.substring(start, expr.end); + if (expr.children.length || !expr.codeChunks.length) { + expr.codeChunks.push(source.substring(codeStart, expr.end)); } return expr; diff --git a/test/astro-expr.test.js b/test/astro-expr.test.js index ea461af4bf..c3c985712f 100644 --- a/test/astro-expr.test.js +++ b/test/astro-expr.test.js @@ -53,4 +53,11 @@ Expressions('Ignores characters inside of multiline comments', async ({ runtime } }); +Expressions('Allows multiple JSX children in mustache', async ({ runtime }) => { + const result = await runtime.load('/multiple-children'); + assert.equal(result.statusCode, 200); + + assert.ok(result.contents.includes('#f') && !result.contents.includes('#t')); +}); + Expressions.run(); diff --git a/test/astro-prettier.test.js b/test/astro-prettier.test.js index 1dd1887f4f..f3d1626c5a 100644 --- a/test/astro-prettier.test.js +++ b/test/astro-prettier.test.js @@ -7,15 +7,15 @@ const Prettier = suite('Prettier formatting'); setup(Prettier, './fixtures/astro-prettier'); -/** - * Utility to get `[src, out]` files - * @param name {string} - * @param ctx {any} - */ +/** + * Utility to get `[src, out]` files + * @param name {string} + * @param ctx {any} + */ const getFiles = async (name, { readFile }) => { const [src, out] = await Promise.all([readFile(`/in/${name}.astro`), readFile(`/out/${name}.astro`)]); return [src, out]; -} +}; Prettier('can format a basic Astro file', async (ctx) => { const [src, out] = await getFiles('basic', ctx); @@ -28,7 +28,7 @@ Prettier('can format a basic Astro file', async (ctx) => { Prettier('can format an Astro file with frontmatter', async (ctx) => { const [src, out] = await getFiles('frontmatter', ctx); assert.not.equal(src, out); - + const formatted = format(src); assert.equal(formatted, out); }); @@ -36,7 +36,7 @@ Prettier('can format an Astro file with frontmatter', async (ctx) => { Prettier('can format an Astro file with embedded JSX expressions', async (ctx) => { const [src, out] = await getFiles('embedded-expr', ctx); assert.not.equal(src, out); - + const formatted = format(src); assert.equal(formatted, out); }); diff --git a/test/fixtures/astro-expr/astro/pages/multiple-children.astro b/test/fixtures/astro-expr/astro/pages/multiple-children.astro new file mode 100644 index 0000000000..fb0fafd4a8 --- /dev/null +++ b/test/fixtures/astro-expr/astro/pages/multiple-children.astro @@ -0,0 +1,14 @@ +--- +let title = 'My Site'; +--- + + + + My site + + +

{title}

+ + {false ?

#t

:

#f

} + + \ No newline at end of file diff --git a/test/helpers.js b/test/helpers.js index eb7cabb0bf..b140055637 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -28,8 +28,8 @@ export function setup(Suite, fixturePath) { context.runtime = runtime; context.readFile = async (path) => { const resolved = fileURLToPath(new URL(`${fixturePath}${path}`, import.meta.url)); - return readFile(resolved).then(r => r.toString('utf-8')); - } + return readFile(resolved).then((r) => r.toString('utf-8')); + }; }); Suite.after(async () => { diff --git a/test/test-utils.js b/test/test-utils.js index 5d51826363..6a71d834ac 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -6,13 +6,13 @@ export function doc(html) { return cheerio.load(html); } -/** - * format the contents of an astro file - * @param contents {string} - */ +/** + * format the contents of an astro file + * @param contents {string} + */ export function format(contents) { return prettier.format(contents, { - parser: 'astro', - plugins: [fileURLToPath(new URL('../prettier-plugin-astro', import.meta.url))] - }) + parser: 'astro', + plugins: [fileURLToPath(new URL('../prettier-plugin-astro', import.meta.url))], + }); }