Allow multiple JSX children appear in Mustache tag (#125)

* fix(www): link styles (#100)

Co-authored-by: Nate Moore <nate@skypack.dev>

* Add `assets/` (#102)

* chore: add assets

* docs: update readme

Co-authored-by: Nate Moore <nate@skypack.dev>

* docs: fix readme

* docs: fix readme

* chore: remove github banner

* Allow multiple JSX in mustache

* Manually discard package-lock update (due to local use of npm v7)

* Tidy up

* Revert mode ts-ignore

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
Co-authored-by: Nate Moore <nate@skypack.dev>
This commit is contained in:
Kevin (Kun) "Kassimo" Qian 2021-04-22 12:10:06 -07:00 committed by GitHub
parent f5384b139d
commit 5eb232501f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 72 additions and 43 deletions

View file

@ -55,12 +55,13 @@ const findExpressionsInAST = (node, collect = []) => {
return collect; return collect;
} }
const formatExpression = ({ expression: { codeStart, codeEnd, children }}, text, options) => { const formatExpression = ({ expression: { codeChunks, children }}, text, options) => {
if (children.length === 0) { if (children.length === 0) {
if ([`'`, `"`].includes(codeStart[0])) { const codeStart = codeChunks[0]; // If no children, there should only exist a single chunk.
return `<script $ lang="ts">${codeStart}${codeEnd}</script>` if (codeStart && [`'`, `"`].includes(codeStart[0])) {
return `<script $ lang="ts">${codeChunks.join('')}</script>`
} }
return `{${codeStart}${codeEnd}}`; return `{${codeChunks.join('')}}`;
} }
return `<script $ lang="ts">${text}</script>`; return `<script $ lang="ts">${text}</script>`;

View file

@ -75,7 +75,8 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
} }
switch (val.type) { switch (val.type) {
case 'MustacheTag': { 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; continue;
} }
case 'Text': case 'Text':
@ -101,7 +102,8 @@ function getTextFromAttribute(attr: any): string {
break; break;
} }
case 'MustacheTag': { 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}`); throw new Error(`Unknown attribute type ${attr.type}`);
@ -520,13 +522,20 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
enter(node: TemplateNode) { enter(node: TemplateNode) {
switch (node.type) { switch (node.type) {
case 'Expression': { case 'Expression': {
let child = ''; let children: string[] = [];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (node.children!.length) { for (const child of node.children!) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion children.push(compileHtml(child, state, compileOptions));
child = compileHtml(node.children![0], 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? // TODO Do we need to compile this now, or should we compile the entire module at the end?
let code = compileExpressionSafe(raw).trim().replace(/\;$/, ''); let code = compileExpressionSafe(raw).trim().replace(/\;$/, '');
outSource += `,(${code})`; outSource += `,(${code})`;

View file

@ -63,8 +63,7 @@ export default function (module: Script): Transformer {
type: 'MustacheTag', type: 'MustacheTag',
expression: { expression: {
type: 'Expression', type: 'Expression',
codeStart: '`' + escape(code) + '`', codeChunks: ['`' + escape(code) + '`'],
codeEnd: '',
children: [], children: [],
}, },
}, },

View file

@ -222,9 +222,10 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
} }
} else if (attr.value[k].type === 'MustacheTag' && attr.value[k]) { } else if (attr.value[k].type === 'MustacheTag' && attr.value[k]) {
// dont add same scopedClass twice (this check is a little more basic, but should suffice) // dont 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 // 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}'`;
} }
} }
} }

View file

@ -53,8 +53,7 @@ export interface Expression {
type: 'Expression'; type: 'Expression';
start: number; start: number;
end: number; end: number;
codeStart: string; codeChunks: string[];
codeEnd: string;
children: BaseNode[]; children: BaseNode[];
} }

View file

@ -168,12 +168,12 @@ function consume_expression(source: string, start: number): Expression {
type: 'Expression', type: 'Expression',
start, start,
end: Number.NaN, end: Number.NaN,
codeStart: '', codeChunks: [],
codeEnd: '',
children: [], children: [],
}; };
let codeEndStart: number = 0; let codeStart: number = start;
const state: ParseState = { const state: ParseState = {
source, source,
start, start,
@ -196,10 +196,11 @@ function consume_expression(source: string, start: number): Expression {
break; break;
} }
case '<': { 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); const tag = consume_tag(state);
expr.children.push(tag); expr.children.push(tag);
codeEndStart = state.index; codeStart = state.index;
break; break;
} }
case "'": case "'":
@ -225,10 +226,8 @@ function consume_expression(source: string, start: number): Expression {
expr.end = state.index - 1; expr.end = state.index - 1;
if (codeEndStart) { if (expr.children.length || !expr.codeChunks.length) {
expr.codeEnd = source.substring(codeEndStart, expr.end); expr.codeChunks.push(source.substring(codeStart, expr.end));
} else {
expr.codeStart = source.substring(start, expr.end);
} }
return expr; return expr;

View file

@ -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(); Expressions.run();

View file

@ -8,14 +8,14 @@ const Prettier = suite('Prettier formatting');
setup(Prettier, './fixtures/astro-prettier'); setup(Prettier, './fixtures/astro-prettier');
/** /**
* Utility to get `[src, out]` files * Utility to get `[src, out]` files
* @param name {string} * @param name {string}
* @param ctx {any} * @param ctx {any}
*/ */
const getFiles = async (name, { readFile }) => { const getFiles = async (name, { readFile }) => {
const [src, out] = await Promise.all([readFile(`/in/${name}.astro`), readFile(`/out/${name}.astro`)]); const [src, out] = await Promise.all([readFile(`/in/${name}.astro`), readFile(`/out/${name}.astro`)]);
return [src, out]; return [src, out];
} };
Prettier('can format a basic Astro file', async (ctx) => { Prettier('can format a basic Astro file', async (ctx) => {
const [src, out] = await getFiles('basic', ctx); const [src, out] = await getFiles('basic', ctx);

View file

@ -0,0 +1,14 @@
---
let title = 'My Site';
---
<html lang="en">
<head>
<title>My site</title>
</head>
<body>
<h1>{title}</h1>
{false ? <h1>#t</h1> : <h1>#f</h1>}
</body>
</html>

View file

@ -28,8 +28,8 @@ export function setup(Suite, fixturePath) {
context.runtime = runtime; context.runtime = runtime;
context.readFile = async (path) => { context.readFile = async (path) => {
const resolved = fileURLToPath(new URL(`${fixturePath}${path}`, import.meta.url)); 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 () => { Suite.after(async () => {

View file

@ -7,12 +7,12 @@ export function doc(html) {
} }
/** /**
* format the contents of an astro file * format the contents of an astro file
* @param contents {string} * @param contents {string}
*/ */
export function format(contents) { export function format(contents) {
return prettier.format(contents, { return prettier.format(contents, {
parser: 'astro', parser: 'astro',
plugins: [fileURLToPath(new URL('../prettier-plugin-astro', import.meta.url))] plugins: [fileURLToPath(new URL('../prettier-plugin-astro', import.meta.url))],
}) });
} }