Progress on preparsing

This commit is contained in:
Matthew Phillips 2021-03-16 10:23:29 -04:00
parent 01c34ac5d4
commit e5ebebbcd7
3 changed files with 350 additions and 1 deletions

View file

@ -35,7 +35,7 @@ export default async function(astroConfig: AstroConfig) {
[internalPath.pathname]: '/__hmx_internal__'
},
plugins: [
['hmx-v2/snowpack-plugin', hmxPlugOptions]
['astro/snowpack-plugin', hmxPlugOptions]
],
devOptions: {
open: 'none',

304
src/parser.ts Normal file
View file

@ -0,0 +1,304 @@
const [
CHARS,
TAG_START,
TAG_END,
END_TAG_START,
EQ,
EOF,
UNKNOWN
] = Array.from(new Array(20), (x, i) => i + 1);
const voidTags = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr',
'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
type Visitor = (tag: Tag) => Tag;
interface State {
code: string;
index: number;
visitor: Visitor;
tagName?: string;
}
interface Attribute {
name: string;
value?: string;
boolean: boolean;
start: number;
end: number;
}
interface Text {
type: 0;
data: string;
start: number;
end: number;
}
export interface Tag {
type: 1;
tagName: string;
attributes: Array<Attribute>;
children: Array<Tag | Text>;
void: boolean;
start: number;
end: number;
}
interface Document {
children: Array<Tag | Text>;
}
function stateChar(state: State) {
return state.code[state.index];
}
function stateNext(state: State) {
state.index++;
return stateChar(state);
}
function stateRewind(state: State) {
state.index--;
return stateChar(state);
}
function stateInBounds(state: State) {
return state.index < state.code.length;
}
function createState(code: string, visitor: Visitor): State {
return {
code,
index: 0,
visitor
};
}
function* _stringify(tag: Tag): Generator<string, void, unknown> {
yield '<';
yield tag.tagName;
for(let attr of tag.attributes) {
yield ' ';
yield `"${attr.name}"`;
if(!attr.boolean) {
yield '=';
yield `"${attr.value}"`;
}
}
if(!tag.void) {
for(let child of tag.children) {
if(child.type === 0) {
yield child.data;
} else {
yield * _stringify(child);
}
}
}
}
function stringify(tag: Tag) {
let out = '';
for(let chunk of _stringify(tag)) {
out += chunk;
}
return out;
}
function spliceSlice(str: string, index: number, count: number, add: string) {
// We cannot pass negative indexes directly to the 2nd slicing operation.
if (index < 0) {
index = str.length + index;
if (index < 0) {
index = 0;
}
}
return str.slice(0, index) + (add || "") + str.slice(index + count);
}
function replaceTag(state: State, tag: Tag) {
const origLen = tag.end - tag.start;
const html = stringify(tag);
const newLen = html.length;
const newCurIndex = tag.start + newLen;
state.code = spliceSlice(state.code, tag.start, origLen, html);
state.index = newCurIndex;
}
function consumeToken(state: State) {
do {
const c = stateNext(state);
if(/\s/.test(c)) {
continue;
}
if(c === '<') {
return TAG_START;
}
if(c === '>') {
return TAG_END;
}
if(c === '/') {
return END_TAG_START;
}
if(/[a-zA-Z]/.test(c)) {
return CHARS;
}
return UNKNOWN;
} while(stateInBounds(state));
return EOF;
}
function consumeText(state: State): Text {
let start = state.index;
let data = '';
let c = stateNext(state);
while(stateInBounds(state) && c !== '<') {
data += c;
c = stateNext(state);
}
return {
type: 0,
data,
start,
end: state.index - 1
};
}
function consumeTagName(state: State): string {
let name = '';
let token = consumeToken(state);
while(token === CHARS) {
name += stateChar(state);
token = consumeToken(state);
}
return name.toLowerCase();
}
function consumeAttribute(state: State): Attribute {
let start = state.index;
let name = '', token;
do {
name += stateChar(state).toLowerCase();
token = consumeToken(state);
} while(token === CHARS);
if(token !== EQ) {
stateRewind(state);
return {
name,
boolean: true,
start,
end: state.index - 1
};
}
let value = '';
do {
value += stateChar(state).toLowerCase();
token = consumeToken(state);
} while(token === CHARS);
return {
name,
value,
boolean: false,
start,
end: state.index - 1
};
}
function consumeChildren(state: State): Array<Tag | Text> {
const children: Array<Tag | Text> = [];
childLoop: while(stateInBounds(state)) {
const token = consumeToken(state);
switch(token) {
case TAG_START: {
const next = consumeToken(state);
if(next === END_TAG_START) {
consumeTagName(state);
consumeToken(state); // >
break childLoop;
} else {
stateRewind(state);
consumeTag(state);
}
break;
}
case CHARS: {
children.push(consumeText(state));
break;
}
default: {
break;
}
}
}
return children;
}
function consumeTag(state: State): Tag {
const start = state.index - 1;
const tagName = consumeTagName(state);
const attributes: Array<Attribute> = [];
let token = consumeToken(state);
// Collect attributes
attrLoop: while(token !== TAG_END) {
switch(token) {
case CHARS: {
attributes.push(consumeAttribute(state));
break;
}
default: {
break attrLoop;
}
}
token = consumeToken(state);
}
const children: Array<Tag | Text> = consumeChildren(state);
const node: Tag = {
type: 1,
tagName,
attributes,
children,
void: voidTags.has(tagName),
start,
end: state.index - 1
};
const replacement = state.visitor(node);
if(replacement !== node) {
replaceTag(state, node);
}
return node;
}
function consumeDocument(state: State): Document {
const children: Array<Tag | Text> = consumeChildren(state);
return {
children
};
}
export function preparse(code: string, visitor: Visitor) {
const state = createState(code, visitor);
consumeDocument(state);
}

View file

@ -11,6 +11,7 @@ import matter from "gray-matter";
import gfmHtml from "micromark-extension-gfm/html.js";
import { walk, parse } from "./compiler.js";
import markdownEncode from './markdown-encode.js';
import { preparse } from './parser.js';
const { transformSync } = esbuild;
@ -161,11 +162,55 @@ function getComponentWrapper(
throw new Error("Unknown Component Type: " + name);
}
function runPreparser(template: string): string {
const doc = preparse(template, tag => {
if(tag.tagName === 'script') {
let isSetup = false;
for(let attr of tag.attributes) {
if(attr.name === 'hmx' && attr.value === 'setup') {
isSetup = true;
break;
}
}
if(isSetup && typeof tag.children[0] === 'string') {
debugger;
const content = tag.children[0];
let { code } = transformSync(content, {
loader: "tsx",
jsxFactory: "h",
jsxFragment: "Fragment",
charset: "utf8",
});
return {
...tag,
children: [{
type: 0,
data: code,
start: 0,
end: 0
}]
};
}
}
return tag;
});
// TODO codegen
return template;
}
async function convertHmxToJsx(template: string, compileOptions: CompileOptions) {
await eslexer.init;
//template = runPreparser(template);
const ast = parse(template, {});
// Todo: Validate that `h` and `Fragment` aren't defined in the script
const script = ast.instance ? astring.generate(ast.instance.content) : "";
const [scriptImports] = eslexer.parse(script, "optional-sourcename");
const components = Object.fromEntries(
scriptImports.map((imp) => {