Parse inner JSX as Astro (#67)
* Parse inner JSX as Astro This completes the compiler changes, updating the parser so that it parses inner "JSX" as Astro. It does this by finding the start and end of HTML tags and feeds that back into the parser. The result is a structure like this: ``` { type: 'MustacheTag', expression: [ { type: 'Expression', codeStart: 'colors.map(color => (', codeEnd: '}}' children: [ { type: 'Fragment', children: [ { type: 'Element', name: 'div' } ] } ] } ] } ``` There is a new Node type, `Expression`. Note that `MustacheTag` remains in the tree, all it contains is an Expression though. I could spend some time trying to remove it, there's just a few places that expect it to exist. * Update import to the transform * Transform prism components into expressions
This commit is contained in:
parent
084845f79d
commit
ad9c3b1d8d
24 changed files with 432 additions and 164 deletions
|
@ -8,7 +8,7 @@ export interface NodeVisitor {
|
||||||
leave?: VisitorFn;
|
leave?: VisitorFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Optimizer {
|
export interface Transformer {
|
||||||
visitors?: {
|
visitors?: {
|
||||||
html?: Record<string, NodeVisitor>;
|
html?: Record<string, NodeVisitor>;
|
||||||
css?: Record<string, NodeVisitor>;
|
css?: Record<string, NodeVisitor>;
|
||||||
|
@ -16,7 +16,7 @@ export interface Optimizer {
|
||||||
finalize: () => Promise<void>;
|
finalize: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OptimizeOptions {
|
export interface TransformOptions {
|
||||||
compileOptions: CompileOptions;
|
compileOptions: CompileOptions;
|
||||||
filename: string;
|
filename: string;
|
||||||
fileID: string;
|
fileID: string;
|
|
@ -7,7 +7,7 @@ import type { LogOptions } from '../logger';
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
import { promises as fsPromises } from 'fs';
|
import { promises as fsPromises } from 'fs';
|
||||||
import { parse } from '../parser/index.js';
|
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 { getAttrValue } from '../ast.js';
|
||||||
import { walk } from 'estree-walker';
|
import { walk } from 'estree-walker';
|
||||||
import babelParser from '@babel/parser';
|
import babelParser from '@babel/parser';
|
||||||
|
@ -86,7 +86,7 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin
|
||||||
return imports;
|
return imports;
|
||||||
}
|
}
|
||||||
|
|
||||||
await optimize(ast, {
|
await transform(ast, {
|
||||||
filename: filename.pathname,
|
filename: filename.pathname,
|
||||||
fileID: '',
|
fileID: '',
|
||||||
compileOptions: {
|
compileOptions: {
|
||||||
|
|
|
@ -78,9 +78,10 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
switch (val.type) {
|
switch (val.type) {
|
||||||
case 'MustacheTag':
|
case 'MustacheTag': {
|
||||||
result[attr.name] = '(' + val.content + ')';
|
result[attr.name] = '(' + val.expression.codeStart + ')';
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
case 'Text':
|
case 'Text':
|
||||||
result[attr.name] = JSON.stringify(getTextFromAttribute(val));
|
result[attr.name] = JSON.stringify(getTextFromAttribute(val));
|
||||||
continue;
|
continue;
|
||||||
|
@ -93,13 +94,21 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
|
||||||
|
|
||||||
/** Get value from a TemplateNode Attribute (text attributes only!) */
|
/** Get value from a TemplateNode Attribute (text attributes only!) */
|
||||||
function getTextFromAttribute(attr: any): string {
|
function getTextFromAttribute(attr: any): string {
|
||||||
|
switch(attr.type) {
|
||||||
|
case 'Text': {
|
||||||
if (attr.raw !== undefined) {
|
if (attr.raw !== undefined) {
|
||||||
return attr.raw;
|
return attr.raw;
|
||||||
}
|
}
|
||||||
if (attr.data !== undefined) {
|
if (attr.data !== undefined) {
|
||||||
return attr.data;
|
return attr.data;
|
||||||
}
|
}
|
||||||
throw new Error('UNKNOWN attr');
|
break;
|
||||||
|
}
|
||||||
|
case 'MustacheTag': {
|
||||||
|
return attr.expression.codeStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown attribute type ${attr.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert TemplateNode attributes to string */
|
/** 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 {
|
function compileExpressionSafe(raw: string): string {
|
||||||
let { code } = transformSync(raw, {
|
let { code } = transformSync(raw, {
|
||||||
loader: 'tsx',
|
loader: 'tsx',
|
||||||
|
@ -468,33 +477,19 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
|
||||||
walk(enterNode, {
|
walk(enterNode, {
|
||||||
enter(node: TemplateNode) {
|
enter(node: TemplateNode) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'MustacheTag':
|
case 'Expression': {
|
||||||
let code = compileExpressionSafe(node.content);
|
let child = '';
|
||||||
|
if(node.children!.length) {
|
||||||
let matches: RegExpExecArray[] = [];
|
child = compileHtml(node.children![0], state, compileOptions);
|
||||||
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()) {
|
let raw = node.codeStart + child + node.codeEnd;
|
||||||
const name = astroComponent[1];
|
// TODO Do we need to compile this now, or should we compile the entire module at the end?
|
||||||
const [componentName, componentKind] = name.split(':');
|
let code = compileExpressionSafe(raw).trim().replace(/\;$/, '');
|
||||||
if (!components[componentName]) {
|
outSource += `,(${code})`;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outSource += `,(${code.trim().replace(/\;$/, '')})`;
|
|
||||||
this.skip();
|
this.skip();
|
||||||
return;
|
break;
|
||||||
|
}
|
||||||
|
case 'MustacheTag':
|
||||||
case 'Comment':
|
case 'Comment':
|
||||||
return;
|
return;
|
||||||
case 'Fragment':
|
case 'Fragment':
|
||||||
|
@ -557,11 +552,11 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
|
||||||
leave(node, parent, prop, index) {
|
leave(node, parent, prop, index) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'Text':
|
case 'Text':
|
||||||
case 'MustacheTag':
|
|
||||||
case 'Attribute':
|
case 'Attribute':
|
||||||
case 'Comment':
|
case 'Comment':
|
||||||
return;
|
|
||||||
case 'Fragment':
|
case 'Fragment':
|
||||||
|
case 'Expression':
|
||||||
|
case 'MustacheTag':
|
||||||
return;
|
return;
|
||||||
case 'Slot':
|
case 'Slot':
|
||||||
case 'Head':
|
case 'Head':
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { parse } from '../parser/index.js';
|
||||||
import { createMarkdownHeadersCollector } from './markdown/micromark-collect-headers.js';
|
import { createMarkdownHeadersCollector } from './markdown/micromark-collect-headers.js';
|
||||||
import { encodeMarkdown } from './markdown/micromark-encode.js';
|
import { encodeMarkdown } from './markdown/micromark-encode.js';
|
||||||
import { encodeAstroMdx } from './markdown/micromark-mdx-astro.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';
|
import { codegen } from './codegen.js';
|
||||||
|
|
||||||
/** Return Astro internal import URL */
|
/** Return Astro internal import URL */
|
||||||
|
@ -29,7 +29,7 @@ interface ConvertAstroOptions {
|
||||||
* .astro -> .jsx
|
* .astro -> .jsx
|
||||||
* Core function processing .astro files. Initiates all 3 phases of compilation:
|
* Core function processing .astro files. Initiates all 3 phases of compilation:
|
||||||
* 1. Parse
|
* 1. Parse
|
||||||
* 2. Optimize
|
* 2. Transform
|
||||||
* 3. Codegen
|
* 3. Codegen
|
||||||
*/
|
*/
|
||||||
async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> {
|
async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> {
|
||||||
|
@ -40,8 +40,8 @@ async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): P
|
||||||
filename,
|
filename,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Optimize the AST
|
// 2. Transform the AST
|
||||||
await optimize(ast, opts);
|
await transform(ast, opts);
|
||||||
|
|
||||||
// 3. Turn AST into JSX
|
// 3. Turn AST into JSX
|
||||||
return await codegen(ast, opts);
|
return await codegen(ast, opts);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Optimizer } from '../../@types/optimizer';
|
import { Transformer } from '../../@types/transformer';
|
||||||
|
|
||||||
/** Optimize <!doctype> tg */
|
/** Transform <!doctype> tg */
|
||||||
export default function (_opts: { filename: string; fileID: string }): Optimizer {
|
export default function (_opts: { filename: string; fileID: string }): Transformer {
|
||||||
let hasDoctype = false;
|
let hasDoctype = false;
|
||||||
|
|
||||||
return {
|
return {
|
|
@ -1,13 +1,13 @@
|
||||||
import type { Ast, TemplateNode } from '../../parser/interfaces';
|
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';
|
import { walk } from 'estree-walker';
|
||||||
|
|
||||||
// Optimizers
|
// Transformers
|
||||||
import optimizeStyles from './styles.js';
|
import transformStyles from './styles.js';
|
||||||
import optimizeDoctype from './doctype.js';
|
import transformDoctype from './doctype.js';
|
||||||
import optimizeModuleScripts from './module-scripts.js';
|
import transformModuleScripts from './module-scripts.js';
|
||||||
import optimizeCodeBlocks from './prism.js';
|
import transformCodeBlocks from './prism.js';
|
||||||
|
|
||||||
interface VisitorCollection {
|
interface VisitorCollection {
|
||||||
enter: Map<string, VisitorFn[]>;
|
enter: Map<string, VisitorFn[]>;
|
||||||
|
@ -24,23 +24,23 @@ function addVisitor(visitor: NodeVisitor, collection: VisitorCollection, nodeNam
|
||||||
collection[event].set(nodeName, visitors);
|
collection[event].set(nodeName, visitors);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compile visitor actions from optimizer */
|
/** Compile visitor actions from transformer */
|
||||||
function collectVisitors(optimizer: Optimizer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) {
|
function collectVisitors(transformer: Transformer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) {
|
||||||
if (optimizer.visitors) {
|
if (transformer.visitors) {
|
||||||
if (optimizer.visitors.html) {
|
if (transformer.visitors.html) {
|
||||||
for (const [nodeName, visitor] of Object.entries(optimizer.visitors.html)) {
|
for (const [nodeName, visitor] of Object.entries(transformer.visitors.html)) {
|
||||||
addVisitor(visitor, htmlVisitors, nodeName, 'enter');
|
addVisitor(visitor, htmlVisitors, nodeName, 'enter');
|
||||||
addVisitor(visitor, htmlVisitors, nodeName, 'leave');
|
addVisitor(visitor, htmlVisitors, nodeName, 'leave');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (optimizer.visitors.css) {
|
if (transformer.visitors.css) {
|
||||||
for (const [nodeName, visitor] of Object.entries(optimizer.visitors.css)) {
|
for (const [nodeName, visitor] of Object.entries(transformer.visitors.css)) {
|
||||||
addVisitor(visitor, cssVisitors, nodeName, 'enter');
|
addVisitor(visitor, cssVisitors, nodeName, 'enter');
|
||||||
addVisitor(visitor, cssVisitors, nodeName, 'leave');
|
addVisitor(visitor, cssVisitors, nodeName, 'leave');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finalizers.push(optimizer.finalize);
|
finalizers.push(transformer.finalize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Utility for formatting visitors */
|
/** Utility for formatting visitors */
|
||||||
|
@ -74,17 +74,17 @@ function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optimize
|
* Transform
|
||||||
* Step 2/3 in Astro SSR.
|
* 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.
|
* 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 htmlVisitors = createVisitorCollection();
|
||||||
const cssVisitors = createVisitorCollection();
|
const cssVisitors = createVisitorCollection();
|
||||||
const finalizers: Array<() => Promise<void>> = [];
|
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) {
|
for (const optimizer of optimizers) {
|
||||||
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
|
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
|
|
@ -1,11 +1,11 @@
|
||||||
import type { Optimizer } from '../../@types/optimizer';
|
import type { Transformer } from '../../@types/transformer';
|
||||||
import type { CompileOptions } from '../../@types/compiler';
|
import type { CompileOptions } from '../../@types/compiler';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getAttrValue, setAttrValue } from '../../ast.js';
|
import { getAttrValue, setAttrValue } from '../../ast.js';
|
||||||
|
|
||||||
/** Optimize <script type="module"> */
|
/** Transform <script type="module"> */
|
||||||
export default function ({ compileOptions, filename }: { compileOptions: CompileOptions; filename: string; fileID: string }): Optimizer {
|
export default function ({ compileOptions, filename }: { compileOptions: CompileOptions; filename: string; fileID: string }): Transformer {
|
||||||
const { astroConfig } = compileOptions;
|
const { astroConfig } = compileOptions;
|
||||||
const { astroRoot } = astroConfig;
|
const { astroRoot } = astroConfig;
|
||||||
const fileUrl = new URL(`file://${filename}`);
|
const fileUrl = new URL(`file://${filename}`);
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Optimizer } from '../../@types/optimizer';
|
import type { Transformer } from '../../@types/transformer';
|
||||||
import type { Script } from '../../parser/interfaces';
|
import type { Script } from '../../parser/interfaces';
|
||||||
import { getAttrValue } from '../../ast.js';
|
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;
|
let usesPrism = false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -61,7 +61,12 @@ export default function (module: Script): Optimizer {
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
type: 'MustacheTag',
|
type: 'MustacheTag',
|
||||||
content: '`' + escape(code) + '`',
|
expression: {
|
||||||
|
type: 'Expression',
|
||||||
|
codeStart: '`' + escape(code) + '`',
|
||||||
|
codeEnd: '',
|
||||||
|
children: []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
|
@ -7,7 +7,7 @@ import postcssKeyframes from 'postcss-icss-keyframes';
|
||||||
import findUp from 'find-up';
|
import findUp from 'find-up';
|
||||||
import sass from 'sass';
|
import sass from 'sass';
|
||||||
import type { RuntimeMode } from '../../@types/astro';
|
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 type { TemplateNode } from '../../parser/interfaces';
|
||||||
import { debug } from '../../logger.js';
|
import { debug } from '../../logger.js';
|
||||||
import astroScopedStyles, { NEVER_SCOPED_TAGS } from './postcss-scoped-styles/index.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 };
|
return { css, type: styleType };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Optimize <style> tags */
|
/** Transform <style> tags */
|
||||||
export default function optimizeStyles({ compileOptions, filename, fileID }: OptimizeOptions): Optimizer {
|
export default function transformStyles({ compileOptions, filename, fileID }: TransformOptions): Transformer {
|
||||||
const styleNodes: TemplateNode[] = []; // <style> tags to be updated
|
const styleNodes: TemplateNode[] = []; // <style> tags to be updated
|
||||||
const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize();
|
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
|
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]) {
|
} 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)
|
// 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
|
// MustacheTag
|
||||||
attr.value[k].content = `(${attr.value[k].content}) + ' ${scopedClass}'`;
|
attr.value[k].expression.codeStart = `(${attr.value[k].expression.codeStart}) + ' ${scopedClass}'`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import type { Expression, Program } from '@babel/types';
|
|
||||||
import type { SourceMap } from 'magic-string';
|
import type { SourceMap } from 'magic-string';
|
||||||
|
|
||||||
interface BaseNode {
|
export interface BaseNode {
|
||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -50,6 +49,15 @@ export type Directive = BaseDirective | Transition;
|
||||||
|
|
||||||
export type TemplateNode = Text | MustacheTag | BaseNode | Directive | 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 {
|
export interface Parser {
|
||||||
readonly template: string;
|
readonly template: string;
|
||||||
readonly filename?: string;
|
readonly filename?: string;
|
||||||
|
|
|
@ -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,
|
|
||||||
// });
|
|
|
@ -4,7 +4,7 @@ import { Parser } from '../index.js';
|
||||||
import { isIdentifierStart } from 'acorn';
|
import { isIdentifierStart } from 'acorn';
|
||||||
import full_char_code_at from '../../utils/full_char_code_at.js';
|
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 { 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';
|
import { Pattern } from 'estree';
|
||||||
|
|
||||||
export default function read_context(parser: Parser): Pattern & { start: number; end: number } {
|
export default function read_context(parser: Parser): Pattern & { start: number; end: number } {
|
||||||
|
|
|
@ -1,39 +1,254 @@
|
||||||
import { parse_expression_at } from '../acorn.js';
|
|
||||||
|
import type { BaseNode, Expression } from '../../interfaces';
|
||||||
import { Parser } from '../index.js';
|
import { Parser } from '../index.js';
|
||||||
import { whitespace } from '../../utils/patterns.js';
|
import parseAstro from '../index.js';
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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']);
|
||||||
|
|
||||||
|
function consume_tag(state: ParseState) {
|
||||||
|
const start = state.index - 1;
|
||||||
|
let tagName = '';
|
||||||
|
let inTag = false;
|
||||||
|
let inStart = true;
|
||||||
|
let selfClosed = false;
|
||||||
|
let inClose = false;
|
||||||
|
|
||||||
|
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--;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
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
|
// @ts-ignore
|
||||||
export default function read_expression(parser: Parser): string {
|
export default function read_expression(parser: Parser) {
|
||||||
try {
|
try {
|
||||||
const start = parser.index;
|
debugger;
|
||||||
let index = parse_expression_at(parser.template, parser.index);
|
const expression = parse_expression_at(parser.template, parser.index);
|
||||||
let num_parens = 0;
|
parser.index = expression.end;
|
||||||
|
return expression;
|
||||||
for (let i = parser.index; i < start; i += 1) {
|
|
||||||
if (parser.template[i] === '(') num_parens += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (num_parens > 0) {
|
|
||||||
const char = parser.template[index];
|
|
||||||
|
|
||||||
if (char === ')') {
|
|
||||||
num_parens -= 1;
|
|
||||||
} else if (!whitespace.test(char)) {
|
|
||||||
parser.error(
|
|
||||||
{
|
|
||||||
code: 'unexpected-token',
|
|
||||||
message: 'Expected )',
|
|
||||||
},
|
|
||||||
index
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.index = index;
|
|
||||||
|
|
||||||
return parser.template.substring(start, index);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
parser.acorn_error(err);
|
parser.acorn_error(err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import * as acorn from '../acorn';
|
import type { Node } from 'estree';
|
||||||
import { Parser } from '../index.js';
|
import { Parser } from '../index.js';
|
||||||
import { Script } from '../../interfaces.js';
|
import { Script } from '../../interfaces.js';
|
||||||
import { Node, Program } from 'estree';
|
|
||||||
|
|
||||||
const script_closing_tag = '</script>';
|
const script_closing_tag = '</script>';
|
||||||
|
|
||||||
|
|
|
@ -397,7 +397,7 @@ export default function mustache(parser: Parser) {
|
||||||
// });
|
// });
|
||||||
throw new Error('@debug not yet supported');
|
throw new Error('@debug not yet supported');
|
||||||
} else {
|
} else {
|
||||||
const content = read_expression(parser);
|
const expression = read_expression(parser);
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
@ -407,7 +407,7 @@ export default function mustache(parser: Parser) {
|
||||||
start,
|
start,
|
||||||
end: parser.index,
|
end: parser.index,
|
||||||
type: 'MustacheTag',
|
type: 'MustacheTag',
|
||||||
content,
|
expression,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -549,7 +549,7 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
|
||||||
flush();
|
flush();
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
const content = read_expression(parser);
|
const expression = read_expression(parser);
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
|
||||||
|
@ -557,7 +557,7 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
|
||||||
start: index,
|
start: index,
|
||||||
end: parser.index,
|
end: parser.index,
|
||||||
type: 'MustacheTag',
|
type: 'MustacheTag',
|
||||||
content,
|
expression,
|
||||||
});
|
});
|
||||||
|
|
||||||
current_chunk = {
|
current_chunk = {
|
||||||
|
|
|
@ -10,9 +10,47 @@ setup(Expressions, './fixtures/astro-expr');
|
||||||
Expressions('Can load page', async ({ runtime }) => {
|
Expressions('Can load page', async ({ runtime }) => {
|
||||||
const result = await runtime.load('/');
|
const result = await runtime.load('/');
|
||||||
|
|
||||||
console.log(result);
|
|
||||||
assert.equal(result.statusCode, 200);
|
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();
|
Expressions.run();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { suite } from 'uvu';
|
import { suite } from 'uvu';
|
||||||
import * as assert from 'uvu/assert';
|
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');
|
const ScopedStyles = suite('Astro PostCSS Scoped Styles Plugin');
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
|
||||||
export default function({ name }) {
|
export default function({ name }) {
|
||||||
return <div>{name}</div>
|
return <div id={name}>{name}</div>
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@ import Color from '../components/Color.jsx';
|
||||||
|
|
||||||
let title = 'My Site';
|
let title = 'My Site';
|
||||||
|
|
||||||
const colors = ['red', 'yellow', 'blue']
|
const colors = ['red', 'yellow', 'blue'];
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
17
test/fixtures/astro-expr/astro/pages/line-comments.astro
vendored
Normal file
17
test/fixtures/astro-expr/astro/pages/line-comments.astro
vendored
Normal file
|
@ -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>
|
16
test/fixtures/astro-expr/astro/pages/multiline-comments.astro
vendored
Normal file
16
test/fixtures/astro-expr/astro/pages/multiline-comments.astro
vendored
Normal file
|
@ -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>
|
16
test/fixtures/astro-expr/astro/pages/strings.astro
vendored
Normal file
16
test/fixtures/astro-expr/astro/pages/strings.astro
vendored
Normal file
|
@ -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>
|
Loading…
Reference in a new issue