summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/@types/transformer.ts (renamed from src/@types/optimizer.ts)4
-rw-r--r--src/build/bundle.ts4
-rw-r--r--src/compiler/codegen.ts67
-rw-r--r--src/compiler/index.ts8
-rw-r--r--src/compiler/transform/doctype.ts (renamed from src/compiler/optimize/doctype.ts)6
-rw-r--r--src/compiler/transform/index.ts (renamed from src/compiler/optimize/index.ts)36
-rw-r--r--src/compiler/transform/module-scripts.ts (renamed from src/compiler/optimize/module-scripts.ts)6
-rw-r--r--src/compiler/transform/postcss-scoped-styles/index.ts (renamed from src/compiler/optimize/postcss-scoped-styles/index.ts)0
-rw-r--r--src/compiler/transform/prism.ts (renamed from src/compiler/optimize/prism.ts)11
-rw-r--r--src/compiler/transform/styles.ts (renamed from src/compiler/optimize/styles.ts)10
-rw-r--r--src/parser/interfaces.ts12
-rw-r--r--src/parser/parse/acorn.ts42
-rw-r--r--src/parser/parse/read/context.ts2
-rw-r--r--src/parser/parse/read/expression.ts265
-rw-r--r--src/parser/parse/read/script.ts4
-rw-r--r--src/parser/parse/state/mustache.ts4
-rw-r--r--src/parser/parse/state/tag.ts4
-rw-r--r--test/astro-expr.test.js42
-rw-r--r--test/astro-scoped-styles.test.js2
-rw-r--r--test/fixtures/astro-expr/astro/components/Color.jsx2
-rw-r--r--test/fixtures/astro-expr/astro/pages/index.astro2
-rw-r--r--test/fixtures/astro-expr/astro/pages/line-comments.astro17
-rw-r--r--test/fixtures/astro-expr/astro/pages/multiline-comments.astro16
-rw-r--r--test/fixtures/astro-expr/astro/pages/strings.astro16
24 files changed, 425 insertions, 157 deletions
diff --git a/src/@types/optimizer.ts b/src/@types/transformer.ts
index b6459ab51..7f4167558 100644
--- a/src/@types/optimizer.ts
+++ b/src/@types/transformer.ts
@@ -8,7 +8,7 @@ export interface NodeVisitor {
leave?: VisitorFn;
}
-export interface Optimizer {
+export interface Transformer {
visitors?: {
html?: Record<string, NodeVisitor>;
css?: Record<string, NodeVisitor>;
@@ -16,7 +16,7 @@ export interface Optimizer {
finalize: () => Promise<void>;
}
-export interface OptimizeOptions {
+export interface TransformOptions {
compileOptions: CompileOptions;
filename: string;
fileID: string;
diff --git a/src/build/bundle.ts b/src/build/bundle.ts
index ba1b8f2c2..b55828c2e 100644
--- a/src/build/bundle.ts
+++ b/src/build/bundle.ts
@@ -7,7 +7,7 @@ import type { LogOptions } from '../logger';
import esbuild from 'esbuild';
import { promises as fsPromises } from 'fs';
import { parse } from '../parser/index.js';
-import { optimize } from '../compiler/optimize/index.js';
+import { transform } from '../compiler/transform/index.js';
import { getAttrValue } from '../ast.js';
import { walk } from 'estree-walker';
import babelParser from '@babel/parser';
@@ -86,7 +86,7 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin
return imports;
}
- await optimize(ast, {
+ await transform(ast, {
filename: filename.pathname,
fileID: '',
compileOptions: {
diff --git a/src/compiler/codegen.ts b/src/compiler/codegen.ts
index 59cc2c702..8bcf3f49d 100644
--- a/src/compiler/codegen.ts
+++ b/src/compiler/codegen.ts
@@ -78,9 +78,10 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
continue;
}
switch (val.type) {
- case 'MustacheTag':
- result[attr.name] = '(' + val.content + ')';
+ case 'MustacheTag': {
+ result[attr.name] = '(' + val.expression.codeStart + ')';
continue;
+ }
case 'Text':
result[attr.name] = JSON.stringify(getTextFromAttribute(val));
continue;
@@ -93,13 +94,21 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
/** Get value from a TemplateNode Attribute (text attributes only!) */
function getTextFromAttribute(attr: any): string {
- if (attr.raw !== undefined) {
- return attr.raw;
- }
- if (attr.data !== undefined) {
- return attr.data;
+ switch(attr.type) {
+ case 'Text': {
+ if (attr.raw !== undefined) {
+ return attr.raw;
+ }
+ if (attr.data !== undefined) {
+ return attr.data;
+ }
+ break;
+ }
+ case 'MustacheTag': {
+ return attr.expression.codeStart;
+ }
}
- throw new Error('UNKNOWN attr');
+ throw new Error(`Unknown attribute type ${attr.type}`);
}
/** Convert TemplateNode attributes to string */
@@ -238,7 +247,7 @@ function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo
}
}
-/** Evaluate mustache expression (safely) */
+/** Evaluate expression (safely) */
function compileExpressionSafe(raw: string): string {
let { code } = transformSync(raw, {
loader: 'tsx',
@@ -468,33 +477,19 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
walk(enterNode, {
enter(node: TemplateNode) {
switch (node.type) {
- case 'MustacheTag':
- let code = compileExpressionSafe(node.content);
-
- let matches: RegExpExecArray[] = [];
- let match: RegExpExecArray | null | undefined;
- const H_COMPONENT_SCANNER = /h\(['"]?([A-Z].*?)['"]?,/gs;
- const regex = new RegExp(H_COMPONENT_SCANNER);
- while ((match = regex.exec(code))) {
- matches.push(match);
- }
- for (const astroComponent of matches.reverse()) {
- const name = astroComponent[1];
- const [componentName, componentKind] = name.split(':');
- if (!components[componentName]) {
- throw new Error(`Unknown Component: ${componentName}`);
- }
- const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename });
- if (wrapperImport) {
- importExportStatements.add(wrapperImport);
- }
- if (wrapper !== name) {
- code = code.slice(0, astroComponent.index + 2) + wrapper + code.slice(astroComponent.index + astroComponent[0].length - 1);
- }
+ case 'Expression': {
+ let child = '';
+ if(node.children!.length) {
+ child = compileHtml(node.children![0], state, compileOptions);
}
- outSource += `,(${code.trim().replace(/\;$/, '')})`;
+ 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})`;
this.skip();
- return;
+ break;
+ }
+ case 'MustacheTag':
case 'Comment':
return;
case 'Fragment':
@@ -557,11 +552,11 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
leave(node, parent, prop, index) {
switch (node.type) {
case 'Text':
- case 'MustacheTag':
case 'Attribute':
case 'Comment':
- return;
case 'Fragment':
+ case 'Expression':
+ case 'MustacheTag':
return;
case 'Slot':
case 'Head':
diff --git a/src/compiler/index.ts b/src/compiler/index.ts
index d33527b9b..db50abec8 100644
--- a/src/compiler/index.ts
+++ b/src/compiler/index.ts
@@ -11,7 +11,7 @@ import { parse } from '../parser/index.js';
import { createMarkdownHeadersCollector } from './markdown/micromark-collect-headers.js';
import { encodeMarkdown } from './markdown/micromark-encode.js';
import { encodeAstroMdx } from './markdown/micromark-mdx-astro.js';
-import { optimize } from './optimize/index.js';
+import { transform } from './transform/index.js';
import { codegen } from './codegen.js';
/** Return Astro internal import URL */
@@ -29,7 +29,7 @@ interface ConvertAstroOptions {
* .astro -> .jsx
* Core function processing .astro files. Initiates all 3 phases of compilation:
* 1. Parse
- * 2. Optimize
+ * 2. Transform
* 3. Codegen
*/
async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> {
@@ -40,8 +40,8 @@ async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): P
filename,
});
- // 2. Optimize the AST
- await optimize(ast, opts);
+ // 2. Transform the AST
+ await transform(ast, opts);
// 3. Turn AST into JSX
return await codegen(ast, opts);
diff --git a/src/compiler/optimize/doctype.ts b/src/compiler/transform/doctype.ts
index 176880c08..d19b01f81 100644
--- a/src/compiler/optimize/doctype.ts
+++ b/src/compiler/transform/doctype.ts
@@ -1,7 +1,7 @@
-import { Optimizer } from '../../@types/optimizer';
+import { Transformer } from '../../@types/transformer';
-/** Optimize <!doctype> tg */
-export default function (_opts: { filename: string; fileID: string }): Optimizer {
+/** Transform <!doctype> tg */
+export default function (_opts: { filename: string; fileID: string }): Transformer {
let hasDoctype = false;
return {
diff --git a/src/compiler/optimize/index.ts b/src/compiler/transform/index.ts
index fcbd6e950..6a81b92b0 100644
--- a/src/compiler/optimize/index.ts
+++ b/src/compiler/transform/index.ts
@@ -1,13 +1,13 @@
import type { Ast, TemplateNode } from '../../parser/interfaces';
-import type { NodeVisitor, OptimizeOptions, Optimizer, VisitorFn } from '../../@types/optimizer';
+import type { NodeVisitor, TransformOptions, Transformer, VisitorFn } from '../../@types/transformer';
import { walk } from 'estree-walker';
-// Optimizers
-import optimizeStyles from './styles.js';
-import optimizeDoctype from './doctype.js';
-import optimizeModuleScripts from './module-scripts.js';
-import optimizeCodeBlocks from './prism.js';
+// Transformers
+import transformStyles from './styles.js';
+import transformDoctype from './doctype.js';
+import transformModuleScripts from './module-scripts.js';
+import transformCodeBlocks from './prism.js';
interface VisitorCollection {
enter: Map<string, VisitorFn[]>;
@@ -24,23 +24,23 @@ function addVisitor(visitor: NodeVisitor, collection: VisitorCollection, nodeNam
collection[event].set(nodeName, visitors);
}
-/** Compile visitor actions from optimizer */
-function collectVisitors(optimizer: Optimizer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) {
- if (optimizer.visitors) {
- if (optimizer.visitors.html) {
- for (const [nodeName, visitor] of Object.entries(optimizer.visitors.html)) {
+/** Compile visitor actions from transformer */
+function collectVisitors(transformer: Transformer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) {
+ if (transformer.visitors) {
+ if (transformer.visitors.html) {
+ for (const [nodeName, visitor] of Object.entries(transformer.visitors.html)) {
addVisitor(visitor, htmlVisitors, nodeName, 'enter');
addVisitor(visitor, htmlVisitors, nodeName, 'leave');
}
}
- if (optimizer.visitors.css) {
- for (const [nodeName, visitor] of Object.entries(optimizer.visitors.css)) {
+ if (transformer.visitors.css) {
+ for (const [nodeName, visitor] of Object.entries(transformer.visitors.css)) {
addVisitor(visitor, cssVisitors, nodeName, 'enter');
addVisitor(visitor, cssVisitors, nodeName, 'leave');
}
}
}
- finalizers.push(optimizer.finalize);
+ finalizers.push(transformer.finalize);
}
/** Utility for formatting visitors */
@@ -74,17 +74,17 @@ function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection)
}
/**
- * Optimize
+ * Transform
* Step 2/3 in Astro SSR.
- * Optimize is the point at which we mutate the AST before sending off to
+ * Transform is the point at which we mutate the AST before sending off to
* Codegen, and then to Snowpack. In some ways, it‘s a preprocessor.
*/
-export async function optimize(ast: Ast, opts: OptimizeOptions) {
+export async function transform(ast: Ast, opts: TransformOptions) {
const htmlVisitors = createVisitorCollection();
const cssVisitors = createVisitorCollection();
const finalizers: Array<() => Promise<void>> = [];
- const optimizers = [optimizeStyles(opts), optimizeDoctype(opts), optimizeModuleScripts(opts), optimizeCodeBlocks(ast.module)];
+ const optimizers = [transformStyles(opts), transformDoctype(opts), transformModuleScripts(opts), transformCodeBlocks(ast.module)];
for (const optimizer of optimizers) {
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
diff --git a/src/compiler/optimize/module-scripts.ts b/src/compiler/transform/module-scripts.ts
index 9d4949215..aff1ec4f6 100644
--- a/src/compiler/optimize/module-scripts.ts
+++ b/src/compiler/transform/module-scripts.ts
@@ -1,11 +1,11 @@
-import type { Optimizer } from '../../@types/optimizer';
+import type { Transformer } from '../../@types/transformer';
import type { CompileOptions } from '../../@types/compiler';
import path from 'path';
import { getAttrValue, setAttrValue } from '../../ast.js';
-/** Optimize <script type="module"> */
-export default function ({ compileOptions, filename }: { compileOptions: CompileOptions; filename: string; fileID: string }): Optimizer {
+/** Transform <script type="module"> */
+export default function ({ compileOptions, filename }: { compileOptions: CompileOptions; filename: string; fileID: string }): Transformer {
const { astroConfig } = compileOptions;
const { astroRoot } = astroConfig;
const fileUrl = new URL(`file://${filename}`);
diff --git a/src/compiler/optimize/postcss-scoped-styles/index.ts b/src/compiler/transform/postcss-scoped-styles/index.ts
index 23350869c..23350869c 100644
--- a/src/compiler/optimize/postcss-scoped-styles/index.ts
+++ b/src/compiler/transform/postcss-scoped-styles/index.ts
diff --git a/src/compiler/optimize/prism.ts b/src/compiler/transform/prism.ts
index 5c5364796..628dcce7e 100644
--- a/src/compiler/optimize/prism.ts
+++ b/src/compiler/transform/prism.ts
@@ -1,4 +1,4 @@
-import type { Optimizer } from '../../@types/optimizer';
+import type { Transformer } from '../../@types/transformer';
import type { Script } from '../../parser/interfaces';
import { getAttrValue } from '../../ast.js';
@@ -11,7 +11,7 @@ function escape(code: string) {
});
}
-export default function (module: Script): Optimizer {
+export default function (module: Script): Transformer {
let usesPrism = false;
return {
@@ -61,7 +61,12 @@ export default function (module: Script): Optimizer {
value: [
{
type: 'MustacheTag',
- content: '`' + escape(code) + '`',
+ expression: {
+ type: 'Expression',
+ codeStart: '`' + escape(code) + '`',
+ codeEnd: '',
+ children: []
+ }
},
],
},
diff --git a/src/compiler/optimize/styles.ts b/src/compiler/transform/styles.ts
index 807d869c9..d8e3196a1 100644
--- a/src/compiler/optimize/styles.ts
+++ b/src/compiler/transform/styles.ts
@@ -7,7 +7,7 @@ import postcssKeyframes from 'postcss-icss-keyframes';
import findUp from 'find-up';
import sass from 'sass';
import type { RuntimeMode } from '../../@types/astro';
-import type { OptimizeOptions, Optimizer } from '../../@types/optimizer';
+import type { TransformOptions, Transformer } from '../../@types/transformer';
import type { TemplateNode } from '../../parser/interfaces';
import { debug } from '../../logger.js';
import astroScopedStyles, { NEVER_SCOPED_TAGS } from './postcss-scoped-styles/index.js';
@@ -152,8 +152,8 @@ async function transformStyle(code: string, { type, filename, scopedClass, mode
return { css, type: styleType };
}
-/** Optimize <style> tags */
-export default function optimizeStyles({ compileOptions, filename, fileID }: OptimizeOptions): Optimizer {
+/** Transform <style> tags */
+export default function transformStyles({ compileOptions, filename, fileID }: TransformOptions): Transformer {
const styleNodes: TemplateNode[] = []; // <style> tags to be updated
const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize();
const scopedClass = `astro-${hashFromFilename(fileID)}`; // this *should* generate same hash from fileID every time
@@ -218,9 +218,9 @@ export default function optimizeStyles({ compileOptions, filename, fileID }: Opt
}
} 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].content.includes(`' ${scopedClass}'`)) {
+ if (!attr.value[k].expression.codeStart.includes(`' ${scopedClass}'`)) {
// MustacheTag
- attr.value[k].content = `(${attr.value[k].content}) + ' ${scopedClass}'`;
+ attr.value[k].expression.codeStart = `(${attr.value[k].expression.codeStart}) + ' ${scopedClass}'`;
}
}
}
diff --git a/src/parser/interfaces.ts b/src/parser/interfaces.ts
index 71b1812a3..4a4d43f71 100644
--- a/src/parser/interfaces.ts
+++ b/src/parser/interfaces.ts
@@ -1,7 +1,6 @@
-import type { Expression, Program } from '@babel/types';
import type { SourceMap } from 'magic-string';
-interface BaseNode {
+export interface BaseNode {
start: number;
end: number;
type: string;
@@ -50,6 +49,15 @@ export type Directive = BaseDirective | Transition;
export type TemplateNode = Text | MustacheTag | BaseNode | Directive | Transition;
+export interface Expression {
+ type: 'Expression';
+ start: number;
+ end: number;
+ codeStart: string;
+ codeEnd: string;
+ children: BaseNode[];
+}
+
export interface Parser {
readonly template: string;
readonly filename?: string;
diff --git a/src/parser/parse/acorn.ts b/src/parser/parse/acorn.ts
deleted file mode 100644
index c70756d79..000000000
--- a/src/parser/parse/acorn.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import type { Node } from 'acorn';
-import { parseExpression } from '@babel/parser';
-// import acorn from 'acorn';
-// // @ts-ignore
-// import jsx from 'acorn-jsx';
-// const acornJsx = acorn.Parser.extend(jsx());
-
-export const parse = (source: string): Node => {
- throw new Error('No longer used.');
- // acorn.parse(source, {
- // sourceType: 'module',
- // ecmaVersion: 2020,
- // locations: true,
- // });
-};
-
-export const parse_expression_at = (source: string, index: number): number => {
- // TODO: Clean up after acorn -> @babel/parser move
- try {
- // First, try to parse the expression. Unlike acorn, @babel/parser isn't relaxed
- // enough to just stop after the first expression, so we almost always expect a
- // parser error here instead. This is expected, so handle it.
- parseExpression(source.slice(index), {
- sourceType: 'module',
- plugins: ['jsx', 'typescript'],
- });
- throw new Error('Parse error.'); // Expected to fail.
- } catch (err) {
- if (err.message.startsWith('Unexpected token') && source[index + err.pos] === '}') {
- return index + err.pos;
- }
- if (err.pos) {
- err.pos = index + err.pos;
- }
- throw err;
- }
-};
-// acornJsx.parseExpressionAt(source, index, {
-// sourceType: 'module',
-// ecmaVersion: 2020,
-// locations: true,
-// });
diff --git a/src/parser/parse/read/context.ts b/src/parser/parse/read/context.ts
index 4d8f12060..565c66d18 100644
--- a/src/parser/parse/read/context.ts
+++ b/src/parser/parse/read/context.ts
@@ -4,7 +4,7 @@ import { Parser } from '../index.js';
import { isIdentifierStart } from 'acorn';
import full_char_code_at from '../../utils/full_char_code_at.js';
import { is_bracket_open, is_bracket_close, is_bracket_pair, get_bracket_close } from '../utils/bracket.js';
-import { parse_expression_at } from '../acorn.js';
+import { parse_expression_at } from './expression.js';
import { Pattern } from 'estree';
export default function read_context(parser: Parser): Pattern & { start: number; end: number } {
diff --git a/src/parser/parse/read/expression.ts b/src/parser/parse/read/expression.ts
index 6ea0ebc56..bdd6c4b81 100644
--- a/src/parser/parse/read/expression.ts
+++ b/src/parser/parse/read/expression.ts
@@ -1,39 +1,254 @@
-import { parse_expression_at } from '../acorn.js';
+
+import type { BaseNode, Expression } from '../../interfaces';
import { Parser } from '../index.js';
-import { whitespace } from '../../utils/patterns.js';
+import parseAstro from '../index.js';
-// @ts-ignore
-export default function read_expression(parser: Parser): string {
- try {
- const start = parser.index;
- let index = parse_expression_at(parser.template, parser.index);
- let num_parens = 0;
+interface ParseState {
+ source: string;
+ start: number;
+ index: number;
+ curlyCount: number;
+ bracketCount: number;
+ root: Expression;
+}
+
+function peek_char(state: ParseState) {
+ return state.source[state.index];
+}
+
+function peek_nonwhitespace(state: ParseState) {
+ let index = state.index;
+ do {
+ let char = state.source[index];
+ if(!/\s/.test(char)) {
+ return char;
+ }
+ index++;
+ } while(index < state.source.length);
+}
+
+function next_char(state: ParseState) {
+ return state.source[state.index++];
+}
+
+function in_bounds(state: ParseState) {
+ return state.index < state.source.length;
+}
+
+function consume_string(state: ParseState, stringChar: string) {
+ let inEscape;
+ do {
+ const char = next_char(state);
+
+ if(inEscape) {
+ inEscape = false;
+ } else if(char === '\\') {
+ inEscape = true;
+ } else if(char === stringChar) {
+ break;
+ }
+ } while(in_bounds(state));
+}
+
+function consume_multiline_comment(state: ParseState) {
+ do {
+ const char = next_char(state);
+
+ if(char === '*' && peek_char(state) === '/') {
+ break;
+ }
+ } while(in_bounds(state));
+}
- for (let i = parser.index; i < start; i += 1) {
- if (parser.template[i] === '(') num_parens += 1;
+function consume_line_comment(state: ParseState) {
+ do {
+ const char = next_char(state);
+ if(char === '\n') {
+ break;
}
+ } while(in_bounds(state));
+}
+
+const voidElements = new Set(['area', 'base', 'br', 'col', 'command', 'embed',
+ 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source',
+ 'track', 'wbr']);
- while (num_parens > 0) {
- const char = parser.template[index];
+function consume_tag(state: ParseState) {
+ const start = state.index - 1;
+ let tagName = '';
+ let inTag = false;
+ let inStart = true;
+ let selfClosed = false;
+ let inClose = false;
- if (char === ')') {
- num_parens -= 1;
- } else if (!whitespace.test(char)) {
- parser.error(
- {
- code: 'unexpected-token',
- message: 'Expected )',
- },
- index
- );
+ let bracketIndex = 1;
+ do {
+ const char = next_char(state);
+
+ switch(char) {
+ case '\'':
+ case '"': {
+ consume_string(state, char);
+ break;
+ }
+ case '<': {
+ inTag = false;
+ tagName = '';
+
+ if(peek_nonwhitespace(state) === '/') {
+ inClose = true;
+ bracketIndex--;
+ } else {
+ inStart = true;
+ bracketIndex++;
+ }
+ break;
}
+ case '>': {
+ // An arrow function, probably
+ if(!inStart && !inClose) {
+ break;
+ }
+
+ bracketIndex--;
- index += 1;
+ const addExpectedBrackets =
+ // Void elements don't need a closing
+ !voidElements.has(tagName.toLowerCase()) &&
+ // Self-closing don't need a closing
+ !selfClosed &&
+ // If we're in a start tag, we expect to find 2 more brackets
+ !inClose;
+
+ if(addExpectedBrackets) {
+ bracketIndex += 2;
+ }
+
+ inTag = false;
+ selfClosed = false;
+ inStart = false;
+ inClose = false;
+ break;
+ }
+ case ' ': {
+ inTag = true;
+ break;
+ }
+ case '/': {
+ if(inStart) {
+ selfClosed = true;
+ }
+ break;
+ }
+ default: {
+ if(!inTag) {
+ tagName += char;
+ }
+ break;
+ }
+ }
+
+ // Unclosed tags
+ if(state.curlyCount <= 0) {
+ break;
+ }
+
+ if(bracketIndex === 0) {
+ break;
}
+ } while(in_bounds(state));
+
+ const source = state.source.substring(start, state.index);
- parser.index = index;
- return parser.template.substring(start, index);
+ const ast = parseAstro(source);
+ const fragment = ast.html;
+
+ return fragment;
+}
+
+function consume_expression(source: string, start: number): Expression {
+ const expr: Expression = {
+ type: 'Expression',
+ start,
+ end: Number.NaN,
+ codeStart: '',
+ codeEnd: '',
+ children: []
+ };
+
+ let codeEndStart: number = 0;
+ const state: ParseState = {
+ source, start, index: start,
+ curlyCount: 1,
+ bracketCount: 0,
+ root: expr
+ };
+
+ do {
+ const char = next_char(state);
+
+ switch(char) {
+ case '{': {
+ state.curlyCount++;
+ break;
+ }
+ case '}': {
+ state.curlyCount--;
+ break;
+ }
+ case '<': {
+ expr.codeStart = source.substring(start, state.index - 1);
+ const tag = consume_tag(state);
+ expr.children.push(tag);
+ codeEndStart = state.index;
+ break;
+ }
+ case '\'':
+ case '"':
+ case '`': {
+ consume_string(state, char);
+ break;
+ }
+ case '/': {
+ switch(peek_char(state)) {
+ case '/': {
+ consume_line_comment(state);
+ break;
+ }
+ case '*': {
+ consume_multiline_comment(state);
+ break;
+ }
+ }
+ }
+ }
+ } while(in_bounds(state) && state.curlyCount > 0);
+
+ expr.end = state.index - 1;
+
+ if(codeEndStart) {
+ expr.codeEnd = source.substring(codeEndStart, expr.end);
+ } else {
+ expr.codeStart = source.substring(start, expr.end);
+ }
+
+ return expr;
+}
+
+export const parse_expression_at = (source: string, index: number): Expression => {
+ const expression = consume_expression(source, index);
+
+ return expression;
+};
+
+// @ts-ignore
+export default function read_expression(parser: Parser) {
+ try {
+ debugger;
+ const expression = parse_expression_at(parser.template, parser.index);
+ parser.index = expression.end;
+ return expression;
} catch (err) {
parser.acorn_error(err);
}
diff --git a/src/parser/parse/read/script.ts b/src/parser/parse/read/script.ts
index 7afbfb08f..4f1d31b44 100644
--- a/src/parser/parse/read/script.ts
+++ b/src/parser/parse/read/script.ts
@@ -1,9 +1,9 @@
// @ts-nocheck
-import * as acorn from '../acorn';
+import type { Node } from 'estree';
import { Parser } from '../index.js';
import { Script } from '../../interfaces.js';
-import { Node, Program } from 'estree';
+
const script_closing_tag = '</script>';
diff --git a/src/parser/parse/state/mustache.ts b/src/parser/parse/state/mustache.ts
index 8ffac4f85..79372d8d9 100644
--- a/src/parser/parse/state/mustache.ts
+++ b/src/parser/parse/state/mustache.ts
@@ -397,7 +397,7 @@ export default function mustache(parser: Parser) {
// });
throw new Error('@debug not yet supported');
} else {
- const content = read_expression(parser);
+ const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
@@ -407,7 +407,7 @@ export default function mustache(parser: Parser) {
start,
end: parser.index,
type: 'MustacheTag',
- content,
+ expression,
});
}
}
diff --git a/src/parser/parse/state/tag.ts b/src/parser/parse/state/tag.ts
index bacaffdef..a8b919a49 100644
--- a/src/parser/parse/state/tag.ts
+++ b/src/parser/parse/state/tag.ts
@@ -549,7 +549,7 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
flush();
parser.allow_whitespace();
- const content = read_expression(parser);
+ const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
@@ -557,7 +557,7 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
start: index,
end: parser.index,
type: 'MustacheTag',
- content,
+ expression,
});
current_chunk = {
diff --git a/test/astro-expr.test.js b/test/astro-expr.test.js
index 9c73d719f..689c32ced 100644
--- a/test/astro-expr.test.js
+++ b/test/astro-expr.test.js
@@ -10,9 +10,47 @@ setup(Expressions, './fixtures/astro-expr');
Expressions('Can load page', async ({ runtime }) => {
const result = await runtime.load('/');
- console.log(result);
assert.equal(result.statusCode, 200);
- console.log(result.contents);
+
+ const $ = doc(result.contents);
+
+ for(let col of ['red', 'yellow', 'blue']) {
+ assert.equal($('#' + col).length, 1);
+ }
+});
+
+Expressions('Ignores characters inside of strings', async ({ runtime }) => {
+ const result = await runtime.load('/strings');
+
+ assert.equal(result.statusCode, 200);
+
+ const $ = doc(result.contents);
+
+ for(let col of ['red', 'yellow', 'blue']) {
+ assert.equal($('#' + col).length, 1);
+ }
+});
+
+Expressions('Ignores characters inside of line comments', async ({ runtime }) => {
+ const result = await runtime.load('/line-comments');
+ assert.equal(result.statusCode, 200);
+
+ const $ = doc(result.contents);
+
+ for(let col of ['red', 'yellow', 'blue']) {
+ assert.equal($('#' + col).length, 1);
+ }
+});
+
+Expressions('Ignores characters inside of multiline comments', async ({ runtime }) => {
+ const result = await runtime.load('/multiline-comments');
+ assert.equal(result.statusCode, 200);
+
+ const $ = doc(result.contents);
+
+ for(let col of ['red', 'yellow', 'blue']) {
+ assert.equal($('#' + col).length, 1);
+ }
});
Expressions.run();
diff --git a/test/astro-scoped-styles.test.js b/test/astro-scoped-styles.test.js
index 5c01a31fb..295668b84 100644
--- a/test/astro-scoped-styles.test.js
+++ b/test/astro-scoped-styles.test.js
@@ -1,6 +1,6 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
-import { scopeRule } from '../lib/compiler/optimize/postcss-scoped-styles/index.js';
+import { scopeRule } from '../lib/compiler/transform/postcss-scoped-styles/index.js';
const ScopedStyles = suite('Astro PostCSS Scoped Styles Plugin');
diff --git a/test/fixtures/astro-expr/astro/components/Color.jsx b/test/fixtures/astro-expr/astro/components/Color.jsx
index 13a5049aa..c2681cc9b 100644
--- a/test/fixtures/astro-expr/astro/components/Color.jsx
+++ b/test/fixtures/astro-expr/astro/components/Color.jsx
@@ -1,5 +1,5 @@
import { h } from 'preact';
export default function({ name }) {
- return <div>{name}</div>
+ return <div id={name}>{name}</div>
} \ No newline at end of file
diff --git a/test/fixtures/astro-expr/astro/pages/index.astro b/test/fixtures/astro-expr/astro/pages/index.astro
index f0a4d2ab0..50af05d93 100644
--- a/test/fixtures/astro-expr/astro/pages/index.astro
+++ b/test/fixtures/astro-expr/astro/pages/index.astro
@@ -3,7 +3,7 @@ import Color from '../components/Color.jsx';
let title = 'My Site';
-const colors = ['red', 'yellow', 'blue']
+const colors = ['red', 'yellow', 'blue'];
---
<html lang="en">
diff --git a/test/fixtures/astro-expr/astro/pages/line-comments.astro b/test/fixtures/astro-expr/astro/pages/line-comments.astro
new file mode 100644
index 000000000..2fb7bf643
--- /dev/null
+++ b/test/fixtures/astro-expr/astro/pages/line-comments.astro
@@ -0,0 +1,17 @@
+---
+let title = 'My App';
+
+let colors = ['red', 'yellow', 'blue'];
+---
+
+<html>
+<head>
+ <title>{title}</title>
+</head>
+<body>
+ {colors.map(color => (
+ // foo < > < }
+ <div id={color}>color</div>
+ ))}
+</body>
+</html> \ No newline at end of file
diff --git a/test/fixtures/astro-expr/astro/pages/multiline-comments.astro b/test/fixtures/astro-expr/astro/pages/multiline-comments.astro
new file mode 100644
index 000000000..5c7016ee8
--- /dev/null
+++ b/test/fixtures/astro-expr/astro/pages/multiline-comments.astro
@@ -0,0 +1,16 @@
+---
+let title = 'My App';
+
+let colors = ['red', 'yellow', 'blue'];
+---
+
+<html>
+<head>
+ <title>{title}</title>
+</head>
+<body>
+ {colors.map(color => (
+ /* foo < > < } */ <div id={color}>color</div>
+ ))}
+</body>
+</html> \ No newline at end of file
diff --git a/test/fixtures/astro-expr/astro/pages/strings.astro b/test/fixtures/astro-expr/astro/pages/strings.astro
new file mode 100644
index 000000000..712df6120
--- /dev/null
+++ b/test/fixtures/astro-expr/astro/pages/strings.astro
@@ -0,0 +1,16 @@
+---
+let title = 'My App';
+
+let colors = ['red', 'yellow', 'blue'];
+---
+
+<html>
+<head>
+ <title>{title}</title>
+</head>
+<body>
+ {colors.map(color => (
+ 'foo < > < }' && <div id={color}>color</div>
+ ))}
+</body>
+</html> \ No newline at end of file