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:
parent
f5384b139d
commit
5eb232501f
11 changed files with 72 additions and 43 deletions
|
@ -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 `<script $ lang="ts">${codeStart}${codeEnd}</script>`
|
||||
const codeStart = codeChunks[0]; // If no children, there should only exist a single chunk.
|
||||
if (codeStart && [`'`, `"`].includes(codeStart[0])) {
|
||||
return `<script $ lang="ts">${codeChunks.join('')}</script>`
|
||||
}
|
||||
return `{${codeStart}${codeEnd}}`;
|
||||
return `{${codeChunks.join('')}}`;
|
||||
}
|
||||
|
||||
return `<script $ lang="ts">${text}</script>`;
|
||||
|
|
|
@ -75,7 +75,8 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
|
|||
}
|
||||
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})`;
|
||||
|
|
|
@ -63,8 +63,7 @@ export default function (module: Script): Transformer {
|
|||
type: 'MustacheTag',
|
||||
expression: {
|
||||
type: 'Expression',
|
||||
codeStart: '`' + escape(code) + '`',
|
||||
codeEnd: '',
|
||||
codeChunks: ['`' + escape(code) + '`'],
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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}'`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,8 +53,7 @@ export interface Expression {
|
|||
type: 'Expression';
|
||||
start: number;
|
||||
end: number;
|
||||
codeStart: string;
|
||||
codeEnd: string;
|
||||
codeChunks: string[];
|
||||
children: BaseNode[];
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
14
test/fixtures/astro-expr/astro/pages/multiple-children.astro
vendored
Normal file
14
test/fixtures/astro-expr/astro/pages/multiple-children.astro
vendored
Normal 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>
|
|
@ -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 () => {
|
||||
|
|
|
@ -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))],
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue