parent
07e2ce09b4
commit
676fdf1ef6
|
@ -22,7 +22,9 @@
|
|||
"test": "vitest --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/generator": "^7.23.6",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/babel__generator": "^7.6.8",
|
||||
"@types/node": "^20.10.5",
|
||||
"@vitest/ui": "^1.2.1",
|
||||
"tsup": "^6.7.0",
|
||||
|
@ -31,6 +33,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||
"@babel/plugin-syntax-typescript": "^7.23.3",
|
||||
"@inula/jsx-view-generator": "workspace:*",
|
||||
"@inula/jsx-view-parser": "workspace:*",
|
||||
"babel-plugin-syntax-typescript-new": "^1.0.0",
|
||||
"minimatch": "^9.0.3"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,513 @@
|
|||
|
||||
export const importMap = [
|
||||
'createElement',
|
||||
'setStyle',
|
||||
'setAttribute',
|
||||
'setDataset',
|
||||
'setProperty',
|
||||
'setEvent',
|
||||
'delegateEvent',
|
||||
'addEventListener',
|
||||
'watch',
|
||||
'insert',
|
||||
'createComponent',
|
||||
'createText'
|
||||
].reduce<Record<string, string>>((acc, cur) => {
|
||||
acc[cur] = cur;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export const htmlTags = [
|
||||
'a',
|
||||
'abbr',
|
||||
'address',
|
||||
'area',
|
||||
'article',
|
||||
'aside',
|
||||
'audio',
|
||||
'b',
|
||||
'base',
|
||||
'bdi',
|
||||
'bdo',
|
||||
'blockquote',
|
||||
'body',
|
||||
'br',
|
||||
'button',
|
||||
'canvas',
|
||||
'caption',
|
||||
'cite',
|
||||
'code',
|
||||
'col',
|
||||
'colgroup',
|
||||
'data',
|
||||
'datalist',
|
||||
'dd',
|
||||
'del',
|
||||
'details',
|
||||
'dfn',
|
||||
'dialog',
|
||||
'div',
|
||||
'dl',
|
||||
'dt',
|
||||
'em',
|
||||
'embed',
|
||||
'fieldset',
|
||||
'figcaption',
|
||||
'figure',
|
||||
'footer',
|
||||
'form',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'head',
|
||||
'header',
|
||||
'hgroup',
|
||||
'hr',
|
||||
'html',
|
||||
'i',
|
||||
'iframe',
|
||||
'img',
|
||||
'input',
|
||||
'ins',
|
||||
'kbd',
|
||||
'label',
|
||||
'legend',
|
||||
'li',
|
||||
'link',
|
||||
'main',
|
||||
'map',
|
||||
'mark',
|
||||
'menu',
|
||||
'meta',
|
||||
'meter',
|
||||
'nav',
|
||||
'noscript',
|
||||
'object',
|
||||
'ol',
|
||||
'optgroup',
|
||||
'option',
|
||||
'output',
|
||||
'p',
|
||||
'picture',
|
||||
'pre',
|
||||
'progress',
|
||||
'q',
|
||||
'rp',
|
||||
'rt',
|
||||
'ruby',
|
||||
's',
|
||||
'samp',
|
||||
'script',
|
||||
'section',
|
||||
'select',
|
||||
'slot',
|
||||
'small',
|
||||
'source',
|
||||
'span',
|
||||
'strong',
|
||||
'style',
|
||||
'sub',
|
||||
'summary',
|
||||
'sup',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'template',
|
||||
'textarea',
|
||||
'tfoot',
|
||||
'th',
|
||||
'thead',
|
||||
'time',
|
||||
'title',
|
||||
'tr',
|
||||
'track',
|
||||
'u',
|
||||
'ul',
|
||||
'var',
|
||||
'video',
|
||||
'wbr',
|
||||
'acronym',
|
||||
'applet',
|
||||
'basefont',
|
||||
'bgsound',
|
||||
'big',
|
||||
'blink',
|
||||
'center',
|
||||
'dir',
|
||||
'font',
|
||||
'frame',
|
||||
'frameset',
|
||||
'isindex',
|
||||
'keygen',
|
||||
'listing',
|
||||
'marquee',
|
||||
'menuitem',
|
||||
'multicol',
|
||||
'nextid',
|
||||
'nobr',
|
||||
'noembed',
|
||||
'noframes',
|
||||
'param',
|
||||
'plaintext',
|
||||
'rb',
|
||||
'rtc',
|
||||
'spacer',
|
||||
'strike',
|
||||
'tt',
|
||||
'xmp',
|
||||
'animate',
|
||||
'animateMotion',
|
||||
'animateTransform',
|
||||
'circle',
|
||||
'clipPath',
|
||||
'defs',
|
||||
'desc',
|
||||
'ellipse',
|
||||
'feBlend',
|
||||
'feColorMatrix',
|
||||
'feComponentTransfer',
|
||||
'feComposite',
|
||||
'feConvolveMatrix',
|
||||
'feDiffuseLighting',
|
||||
'feDisplacementMap',
|
||||
'feDistantLight',
|
||||
'feDropShadow',
|
||||
'feFlood',
|
||||
'feFuncA',
|
||||
'feFuncB',
|
||||
'feFuncG',
|
||||
'feFuncR',
|
||||
'feGaussianBlur',
|
||||
'feImage',
|
||||
'feMerge',
|
||||
'feMergeNode',
|
||||
'feMorphology',
|
||||
'feOffset',
|
||||
'fePointLight',
|
||||
'feSpecularLighting',
|
||||
'feSpotLight',
|
||||
'feTile',
|
||||
'feTurbulence',
|
||||
'filter',
|
||||
'foreignObject',
|
||||
'g',
|
||||
'image',
|
||||
'line',
|
||||
'linearGradient',
|
||||
'marker',
|
||||
'mask',
|
||||
'metadata',
|
||||
'mpath',
|
||||
'path',
|
||||
'pattern',
|
||||
'polygon',
|
||||
'polyline',
|
||||
'radialGradient',
|
||||
'rect',
|
||||
'set',
|
||||
'stop',
|
||||
'svg',
|
||||
'switch',
|
||||
'symbol',
|
||||
'text',
|
||||
'textPath',
|
||||
'tspan',
|
||||
'use',
|
||||
'view',
|
||||
];
|
||||
|
||||
/**
|
||||
* @brief HTML internal attribute map, can be accessed as js property
|
||||
*/
|
||||
export const attributeMap = {
|
||||
// ---- Other property as attribute
|
||||
textContent: ['*'],
|
||||
innerHTML: ['*'],
|
||||
// ---- Source: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes
|
||||
accept: ['form', 'input'],
|
||||
// ---- Original: accept-charset
|
||||
acceptCharset: ['form'],
|
||||
accesskey: ['*'],
|
||||
action: ['form'],
|
||||
align: [
|
||||
'caption',
|
||||
'col',
|
||||
'colgroup',
|
||||
'hr',
|
||||
'iframe',
|
||||
'img',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'tfoot',
|
||||
'th',
|
||||
'thead',
|
||||
'tr',
|
||||
],
|
||||
allow: ['iframe'],
|
||||
alt: ['area', 'img', 'input'],
|
||||
async: ['script'],
|
||||
autocapitalize: ['*'],
|
||||
autocomplete: ['form', 'input', 'select', 'textarea'],
|
||||
autofocus: ['button', 'input', 'select', 'textarea'],
|
||||
autoplay: ['audio', 'video'],
|
||||
background: ['body', 'table', 'td', 'th'],
|
||||
// ---- Original: base
|
||||
bgColor: [
|
||||
'body',
|
||||
'col',
|
||||
'colgroup',
|
||||
'marquee',
|
||||
'table',
|
||||
'tbody',
|
||||
'tfoot',
|
||||
'td',
|
||||
'th',
|
||||
'tr',
|
||||
],
|
||||
border: ['img', 'object', 'table'],
|
||||
buffered: ['audio', 'video'],
|
||||
capture: ['input'],
|
||||
charset: ['meta'],
|
||||
checked: ['input'],
|
||||
cite: ['blockquote', 'del', 'ins', 'q'],
|
||||
class: ['*'],
|
||||
color: ['font', 'hr'],
|
||||
cols: ['textarea'],
|
||||
// ---- Original: colspan
|
||||
colSpan: ['td', 'th'],
|
||||
content: ['meta'],
|
||||
// ---- Original: contenteditable
|
||||
contentEditable: ['*'],
|
||||
contextmenu: ['*'],
|
||||
controls: ['audio', 'video'],
|
||||
coords: ['area'],
|
||||
crossOrigin: ['audio', 'img', 'link', 'script', 'video'],
|
||||
csp: ['iframe'],
|
||||
data: ['object'],
|
||||
// ---- Original: datetime
|
||||
dateTime: ['del', 'ins', 'time'],
|
||||
decoding: ['img'],
|
||||
default: ['track'],
|
||||
defer: ['script'],
|
||||
dir: ['*'],
|
||||
dirname: ['input', 'textarea'],
|
||||
disabled: [
|
||||
'button',
|
||||
'fieldset',
|
||||
'input',
|
||||
'optgroup',
|
||||
'option',
|
||||
'select',
|
||||
'textarea',
|
||||
],
|
||||
download: ['a', 'area'],
|
||||
draggable: ['*'],
|
||||
enctype: ['form'],
|
||||
// ---- Original: enterkeyhint
|
||||
enterKeyHint: ['textarea', 'contenteditable'],
|
||||
for: ['label', 'output'],
|
||||
form: [
|
||||
'button',
|
||||
'fieldset',
|
||||
'input',
|
||||
'label',
|
||||
'meter',
|
||||
'object',
|
||||
'output',
|
||||
'progress',
|
||||
'select',
|
||||
'textarea',
|
||||
],
|
||||
// ---- Original: formaction
|
||||
formAction: ['input', 'button'],
|
||||
// ---- Original: formenctype
|
||||
formEnctype: ['button', 'input'],
|
||||
// ---- Original: formmethod
|
||||
formMethod: ['button', 'input'],
|
||||
// ---- Original: formnovalidate
|
||||
formNoValidate: ['button', 'input'],
|
||||
// ---- Original: formtarget
|
||||
formTarget: ['button', 'input'],
|
||||
headers: ['td', 'th'],
|
||||
height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
||||
hidden: ['*'],
|
||||
high: ['meter'],
|
||||
href: ['a', 'area', 'base', 'link'],
|
||||
hreflang: ['a', 'link'],
|
||||
// ---- Original: http-equiv
|
||||
httpEquiv: ['meta'],
|
||||
id: ['*'],
|
||||
integrity: ['link', 'script'],
|
||||
// ---- Original: intrinsicsize
|
||||
intrinsicSize: ['img'],
|
||||
// ---- Original: inputmode
|
||||
inputMode: ['textarea', 'contenteditable'],
|
||||
ismap: ['img'],
|
||||
// ---- Original: itemprop
|
||||
itemProp: ['*'],
|
||||
kind: ['track'],
|
||||
label: ['optgroup', 'option', 'track'],
|
||||
lang: ['*'],
|
||||
language: ['script'],
|
||||
loading: ['img', 'iframe'],
|
||||
list: ['input'],
|
||||
loop: ['audio', 'marquee', 'video'],
|
||||
low: ['meter'],
|
||||
manifest: ['html'],
|
||||
max: ['input', 'meter', 'progress'],
|
||||
// ---- Original: maxlength
|
||||
maxLength: ['input', 'textarea'],
|
||||
// ---- Original: minlength
|
||||
minLength: ['input', 'textarea'],
|
||||
media: ['a', 'area', 'link', 'source', 'style'],
|
||||
method: ['form'],
|
||||
min: ['input', 'meter'],
|
||||
multiple: ['input', 'select'],
|
||||
muted: ['audio', 'video'],
|
||||
name: [
|
||||
'button',
|
||||
'form',
|
||||
'fieldset',
|
||||
'iframe',
|
||||
'input',
|
||||
'object',
|
||||
'output',
|
||||
'select',
|
||||
'textarea',
|
||||
'map',
|
||||
'meta',
|
||||
'param',
|
||||
],
|
||||
// ---- Original: novalidate
|
||||
noValidate: ['form'],
|
||||
open: ['details', 'dialog'],
|
||||
optimum: ['meter'],
|
||||
pattern: ['input'],
|
||||
ping: ['a', 'area'],
|
||||
placeholder: ['input', 'textarea'],
|
||||
// ---- Original: playsinline
|
||||
playsInline: ['video'],
|
||||
poster: ['video'],
|
||||
preload: ['audio', 'video'],
|
||||
readonly: ['input', 'textarea'],
|
||||
// ---- Original: referrerpolicy
|
||||
referrerPolicy: ['a', 'area', 'iframe', 'img', 'link', 'script'],
|
||||
rel: ['a', 'area', 'link'],
|
||||
required: ['input', 'select', 'textarea'],
|
||||
reversed: ['ol'],
|
||||
role: ['*'],
|
||||
rows: ['textarea'],
|
||||
// ---- Original: rowspan
|
||||
rowSpan: ['td', 'th'],
|
||||
sandbox: ['iframe'],
|
||||
scope: ['th'],
|
||||
scoped: ['style'],
|
||||
selected: ['option'],
|
||||
shape: ['a', 'area'],
|
||||
size: ['input', 'select'],
|
||||
sizes: ['link', 'img', 'source'],
|
||||
slot: ['*'],
|
||||
span: ['col', 'colgroup'],
|
||||
spellcheck: ['*'],
|
||||
src: [
|
||||
'audio',
|
||||
'embed',
|
||||
'iframe',
|
||||
'img',
|
||||
'input',
|
||||
'script',
|
||||
'source',
|
||||
'track',
|
||||
'video',
|
||||
],
|
||||
srcdoc: ['iframe'],
|
||||
srclang: ['track'],
|
||||
srcset: ['img', 'source'],
|
||||
start: ['ol'],
|
||||
step: ['input'],
|
||||
style: ['*'],
|
||||
summary: ['table'],
|
||||
// ---- Original: tabindex
|
||||
tabIndex: ['*'],
|
||||
target: ['a', 'area', 'base', 'form'],
|
||||
title: ['*'],
|
||||
translate: ['*'],
|
||||
type: [
|
||||
'button',
|
||||
'input',
|
||||
'embed',
|
||||
'object',
|
||||
'ol',
|
||||
'script',
|
||||
'source',
|
||||
'style',
|
||||
'menu',
|
||||
'link',
|
||||
],
|
||||
usemap: ['img', 'input', 'object'],
|
||||
value: [
|
||||
'button',
|
||||
'data',
|
||||
'input',
|
||||
'li',
|
||||
'meter',
|
||||
'option',
|
||||
'progress',
|
||||
'param',
|
||||
'text' /** extra for TextNode */,
|
||||
],
|
||||
width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
||||
wrap: ['textarea'],
|
||||
// --- ARIA attributes
|
||||
// Source: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
|
||||
ariaAutocomplete: ['*'],
|
||||
ariaChecked: ['*'],
|
||||
ariaDisabled: ['*'],
|
||||
ariaErrorMessage: ['*'],
|
||||
ariaExpanded: ['*'],
|
||||
ariaHasPopup: ['*'],
|
||||
ariaHidden: ['*'],
|
||||
ariaInvalid: ['*'],
|
||||
ariaLabel: ['*'],
|
||||
ariaLevel: ['*'],
|
||||
ariaModal: ['*'],
|
||||
ariaMultiline: ['*'],
|
||||
ariaMultiSelectable: ['*'],
|
||||
ariaOrientation: ['*'],
|
||||
ariaPlaceholder: ['*'],
|
||||
ariaPressed: ['*'],
|
||||
ariaReadonly: ['*'],
|
||||
ariaRequired: ['*'],
|
||||
ariaSelected: ['*'],
|
||||
ariaSort: ['*'],
|
||||
ariaValuemax: ['*'],
|
||||
ariaValuemin: ['*'],
|
||||
ariaValueNow: ['*'],
|
||||
ariaValueText: ['*'],
|
||||
ariaBusy: ['*'],
|
||||
ariaLive: ['*'],
|
||||
ariaRelevant: ['*'],
|
||||
ariaAtomic: ['*'],
|
||||
ariaDropEffect: ['*'],
|
||||
ariaGrabbed: ['*'],
|
||||
ariaActiveDescendant: ['*'],
|
||||
ariaColCount: ['*'],
|
||||
ariaColIndex: ['*'],
|
||||
ariaColSpan: ['*'],
|
||||
ariaControls: ['*'],
|
||||
ariaDescribedBy: ['*'],
|
||||
ariaDescription: ['*'],
|
||||
ariaDetails: ['*'],
|
||||
ariaFlowTo: ['*'],
|
||||
ariaLabelledBy: ['*'],
|
||||
ariaOwns: ['*'],
|
||||
ariaPosInset: ['*'],
|
||||
ariaRowCount: ['*'],
|
||||
ariaRowIndex: ['*'],
|
||||
ariaRowSpan: ['*'],
|
||||
ariaSetSize: ['*'],
|
||||
};
|
|
@ -1,5 +1,3 @@
|
|||
import syntaxTypescript from 'babel-plugin-syntax-typescript-new';
|
||||
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
||||
import { InulaOption } from './types';
|
||||
import type { ConfigAPI, TransformOptions } from '@babel/core';
|
||||
import inula from './plugin';
|
||||
|
@ -11,8 +9,8 @@ export default function (
|
|||
): TransformOptions {
|
||||
return {
|
||||
plugins: [
|
||||
[syntaxJSX.default ?? syntaxJSX],
|
||||
[syntaxTypescript, {isJsx: true}],
|
||||
['@babel/plugin-syntax-jsx'],
|
||||
['@babel/plugin-syntax-typescript', { isTSX: true }],
|
||||
[inula, options],
|
||||
],
|
||||
};
|
||||
|
|
|
@ -10,13 +10,19 @@ export default function (api: typeof babel, options: InulaOption): PluginObj {
|
|||
name: 'babel-plugin-inula-jsx',
|
||||
visitor: {
|
||||
Program: {
|
||||
enter(path) {
|
||||
console.log('babel-plugin-inula-jsx: Program enter');
|
||||
enter(path, { filename }) {
|
||||
pluginProvider.programEnterVisitor(path, filename);
|
||||
},
|
||||
exit(path) {
|
||||
console.log('babel-plugin-inula-jsx: Program exit');
|
||||
pluginProvider.programExitVisitor(path);
|
||||
},
|
||||
},
|
||||
JSXElement(path) {
|
||||
pluginProvider.jsxElementVisitor(path);
|
||||
},
|
||||
JSXFragment(path) {
|
||||
pluginProvider.jsxElementVisitor(path);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -2,7 +2,9 @@ import type { types as t, NodePath } from '@babel/core';
|
|||
import type babel from '@babel/core';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { InulaOption } from './types';
|
||||
|
||||
import { generateView } from '@inula/jsx-view-generator';
|
||||
import { parseView } from '@inula/jsx-view-parser';
|
||||
import { attributeMap, htmlTags, importMap } from './const';
|
||||
|
||||
|
||||
export class PluginProvider {
|
||||
|
@ -14,6 +16,12 @@ export class PluginProvider {
|
|||
private readonly includes: string[]
|
||||
private readonly excludes: string[]
|
||||
|
||||
private readonly htmlTags
|
||||
private readonly parseTemplate
|
||||
private readonly attributeMap
|
||||
|
||||
private programNode: t.Program | undefined
|
||||
|
||||
constructor(babelApi: typeof babel, options: InulaOption) {
|
||||
this.babelApi = babelApi;
|
||||
this.t = babelApi.types;
|
||||
|
@ -22,6 +30,10 @@ export class PluginProvider {
|
|||
const excludes = options.excludeFiles ?? ['**/{dist,node_modules,lib}'];
|
||||
this.includes = Array.isArray(includes) ? includes : [includes];
|
||||
this.excludes = Array.isArray(excludes) ? excludes : [excludes];
|
||||
|
||||
this.htmlTags = options.htmlTags ?? htmlTags;
|
||||
this.parseTemplate = options.parseTemplate ?? true;
|
||||
this.attributeMap = options.attributeMap ?? attributeMap;
|
||||
}
|
||||
|
||||
// ---- Two levels of enter:
|
||||
|
@ -30,13 +42,9 @@ export class PluginProvider {
|
|||
private fileEnter = true
|
||||
|
||||
// ---- File Level
|
||||
private programNode?: t.Program
|
||||
private allImports: t.ImportDeclaration[] = []
|
||||
private didAlterImports = false
|
||||
private transformedCount = 0
|
||||
|
||||
// ---- Component Level ----
|
||||
|
||||
private templateIdx = -1
|
||||
|
||||
programEnterVisitor(
|
||||
path: NodePath<t.Program>,
|
||||
|
@ -56,17 +64,34 @@ export class PluginProvider {
|
|||
return;
|
||||
}
|
||||
this.programNode = path.node;
|
||||
this.transformedCount = 0;
|
||||
this.templateIdx = -1;
|
||||
}
|
||||
|
||||
programExitVisitor(path: NodePath<t.Program>): void {
|
||||
if (!this.fileEnter) return;
|
||||
|
||||
this.fileEnter = false;
|
||||
}
|
||||
|
||||
|
||||
jsxElementVisitor(path: NodePath<t.JSXElement>): void {
|
||||
jsxElementVisitor(path: NodePath<t.JSXElement | t.JSXFragment>): void {
|
||||
if (!this.fileEnter) return;
|
||||
const viewUnits = parseView(path.node, {
|
||||
babelApi: this.babelApi,
|
||||
htmlTags: this.htmlTags,
|
||||
parseTemplate: this.parseTemplate,
|
||||
});
|
||||
const [templates, viewAst] = generateView(viewUnits, {
|
||||
babelApi: this.babelApi,
|
||||
importMap,
|
||||
attributeMap: this.attributeMap,
|
||||
}, this.templateIdx);
|
||||
this.templateIdx += templates.length;
|
||||
// ---- Add templates to the program
|
||||
this.programNode!.body.unshift(...templates);
|
||||
|
||||
// ---- Replace the JSXElement with the viewAst
|
||||
path.replaceWith(viewAst);
|
||||
path.skip();
|
||||
}
|
||||
|
||||
|
||||
|
@ -87,19 +112,6 @@ export class PluginProvider {
|
|||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wrap the value in a file
|
||||
* @param node
|
||||
* @returns wrapped value
|
||||
*/
|
||||
private wrapWithFile(node: t.Expression | t.Statement): t.File {
|
||||
return this.t.file(
|
||||
this.t.program([
|
||||
this.t.isStatement(node) ? node : this.t.expressionStatement(node),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { transformInula } from './mock';
|
||||
|
||||
describe('Entering', () => {
|
||||
it('should use inula jsx preset in babel', () => {
|
||||
const code = 'console.log(\'hello world\');';
|
||||
expect(transformInula(code)).toBe(code);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,8 +1,22 @@
|
|||
import babel, { transform, types as t } from '@babel/core';
|
||||
import babel, { transform, types as t, parseSync } from '@babel/core';
|
||||
import inula from '../';
|
||||
import babelJSX from '@babel/plugin-syntax-jsx';
|
||||
import generate from '@babel/generator';
|
||||
import { expect as ep } from 'vitest';
|
||||
|
||||
function formatCode(code: string) {
|
||||
return generate(
|
||||
parseSync(code, {plugins: [babelJSX]})!
|
||||
)!.code;
|
||||
}
|
||||
|
||||
export function transformInula(code: string) {
|
||||
return transform(code, {
|
||||
presets: [inula]
|
||||
presets: [[inula, {'files': '*'}]]
|
||||
})?.code;
|
||||
}
|
||||
|
||||
export function expect(ori: string, expected: string) {
|
||||
const transformed = transformInula(ori)!;
|
||||
ep(formatCode(transformed)).toBe(formatCode(expected));
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import { describe, it } from 'vitest';
|
||||
import { expect } from './mock';
|
||||
|
||||
describe('Entering', () => {
|
||||
it('should use inula jsx preset in babel', () => {
|
||||
const code = 'console.log(\'hello world\');';
|
||||
expect(code, code);
|
||||
});
|
||||
|
||||
it('should transform jsx to inula view', () => {
|
||||
expect(/*jsx*/`
|
||||
import A from "inula"
|
||||
function App() {
|
||||
return (
|
||||
<div></div>
|
||||
)
|
||||
}
|
||||
`, /*js*/`
|
||||
import A from "inula";
|
||||
function App() {
|
||||
return (() => {
|
||||
const $node0 = createElement("div");
|
||||
return [$node0];
|
||||
})();
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform jsx to inula view with props', () => {
|
||||
expect(/*jsx*/`
|
||||
import A from "inula"
|
||||
function App() {
|
||||
return (
|
||||
<div id="myDiv"></div>
|
||||
)
|
||||
}
|
||||
`, /*js*/`
|
||||
import A from "inula";
|
||||
function App() {
|
||||
return (() => {
|
||||
const $node0 = createElement("div");
|
||||
$node0.id = "myDiv";
|
||||
return [$node0];
|
||||
})();
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform jsx to inula view with template', () => {
|
||||
expect(/*jsx*/`
|
||||
import A from "inula"
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<p>ok</p>
|
||||
<h1>fine</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
`, /*js*/`
|
||||
const $template0 = (() => {
|
||||
const $node0 = createElement("div");
|
||||
const $node1 = createElement("p");
|
||||
$node1.textContent = "ok";
|
||||
insert($node0, $node1);
|
||||
const $node2 = createElement("h1");
|
||||
$node2.textContent = "fine";
|
||||
insert($node0, $node2);
|
||||
return $node0;
|
||||
});
|
||||
import A from "inula";
|
||||
function App() {
|
||||
return (() => {
|
||||
const $node0 = $template0.cloneNode(true);
|
||||
return [$node0];
|
||||
})();
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -10,4 +10,21 @@ export interface InulaOption {
|
|||
* @default ** /{dist,node_modules,lib}/*.{js,ts}
|
||||
*/
|
||||
excludeFiles?: string | string[]
|
||||
/**
|
||||
* @brief Using AttributeMap to identify propertyfied attributes
|
||||
* Reason for adding this:
|
||||
* `el.prop = xxx` is faster than `el.setAttribute('prop', xxx)`
|
||||
* @example { href: ["a", "area", "base", "link"], id: ["*"] }
|
||||
*/
|
||||
attributeMap?: Record<string, string[]>
|
||||
/**
|
||||
* @brief Using htmlTags to identify the html tags
|
||||
* @example ["a", "area", "base", "link"]
|
||||
*/
|
||||
htmlTags?: string[]
|
||||
/**
|
||||
* @brief Using importMap to identify the import names
|
||||
* @example { createElement: 'createElement' }
|
||||
*/
|
||||
parseTemplate?: boolean
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,217 @@
|
|||
import { ViewUnit, UnitProp } from '@inula/jsx-view-parser';
|
||||
import Babel, { types, traverse } from '@babel/core';
|
||||
|
||||
interface ViewGeneratorConfig {
|
||||
babelApi: typeof Babel;
|
||||
importMap: Record<string, string>;
|
||||
/**
|
||||
* @brief Using AttributeMap to identify propertyfied attributes
|
||||
* Reason for adding this:
|
||||
* `el.prop = xxx` is faster than `el.setAttribute('prop', xxx)`
|
||||
* @example { href: ["a", "area", "base", "link"], id: ["*"] }
|
||||
*/
|
||||
attributeMap?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
declare class BaseGenerator {
|
||||
readonly viewUnit: ViewUnit;
|
||||
readonly config: ViewGeneratorConfig;
|
||||
readonly t: typeof types;
|
||||
readonly traverse: typeof traverse;
|
||||
readonly elementAttributeMap: Record<string, string[]>;
|
||||
readonly importMap: Record<string, string>;
|
||||
constructor(viewUnit: ViewUnit, config: ViewGeneratorConfig);
|
||||
private readonly initStatements;
|
||||
addStatement(...statements: types.Statement[]): void;
|
||||
private readonly templates;
|
||||
addTemplate(...template: types.Statement[]): void;
|
||||
/**
|
||||
* @brief To be implemented by the subclass
|
||||
*/
|
||||
run(): string;
|
||||
generate(): [string, types.Statement[], types.Statement[]];
|
||||
/**
|
||||
* @brief Check if the expression is reactive, which satisfies the following conditions:
|
||||
* 1. Contains .get() property
|
||||
* 2. The whole expression is not a function
|
||||
* @param expression
|
||||
* @returns
|
||||
*/
|
||||
checkReactive(expression: types.Expression): boolean;
|
||||
private readonly prefixMap;
|
||||
nodeIdx: number;
|
||||
geneNodeName(idx?: number): string;
|
||||
templateIdx: number;
|
||||
generateTemplateName(): string;
|
||||
/**
|
||||
* @brief Wrap the value in a file
|
||||
* @param node
|
||||
* @returns wrapped value
|
||||
*/
|
||||
wrapWithFile(node: types.Expression): types.File;
|
||||
addWatch(value: types.Expression): types.CallExpression;
|
||||
createCollector(): [types.Statement[], (statement: types.Statement | types.Statement[] | null) => void];
|
||||
parseViewProp(prop: UnitProp, generateView: (units: ViewUnit[], config: ViewGeneratorConfig, templateIdx: number) => [types.Statement[], types.ExpressionStatement]): types.Expression;
|
||||
parseProps(props: Record<string, UnitProp>, generateView: (units: ViewUnit[], config: ViewGeneratorConfig) => [types.Statement[], types.ExpressionStatement]): {
|
||||
[k: string]: types.Expression;
|
||||
};
|
||||
}
|
||||
|
||||
declare class CompGenerator extends BaseGenerator {
|
||||
run(): string;
|
||||
/**
|
||||
* @View
|
||||
* const $el = createComponent(tag, {
|
||||
* ...props
|
||||
* }, spreadProps)
|
||||
*/
|
||||
declareCompNode(tag: types.Expression, props: Record<string, types.Expression>, children: ViewUnit[]): [string, types.Statement];
|
||||
}
|
||||
|
||||
declare class HTMLPropGenerator extends BaseGenerator {
|
||||
static DelegatedEvents: Set<string>;
|
||||
addHTMLProp(nodeName: string, tag: string, key: string, value: types.Expression): types.Statement;
|
||||
/**
|
||||
* @View
|
||||
* setStyle($node, value)
|
||||
*/
|
||||
private setStyle;
|
||||
/**
|
||||
* @View
|
||||
* setDataset($node, value)
|
||||
*/
|
||||
private setDataset;
|
||||
/**
|
||||
* @View
|
||||
* $node.key = value
|
||||
*/
|
||||
private setStaticProperty;
|
||||
/**
|
||||
* @View
|
||||
* setProperty($node, key, value)
|
||||
*/
|
||||
private setDynamicProperty;
|
||||
/**
|
||||
* @View
|
||||
* $node.setAttribute(key, value)
|
||||
*/
|
||||
private setStaticAttribute;
|
||||
/**
|
||||
* @View
|
||||
* setAttribute($node, key, value)
|
||||
*/
|
||||
private setDynamicAttribute;
|
||||
/**
|
||||
* @View
|
||||
* delegateEvent($node, eventName, value)
|
||||
*/
|
||||
private setDelegatedEvent;
|
||||
/**
|
||||
* @View
|
||||
* $node.addEventListener(eventName, value)
|
||||
*/
|
||||
private setStaticEvent;
|
||||
/**
|
||||
* @View
|
||||
* addEventListener($node, eventName, value)
|
||||
*/
|
||||
private setDynamicEvent;
|
||||
private setDynamicHTMLProp;
|
||||
/**
|
||||
* @brief Check if the attribute is internal, i.e., can be accessed as js property
|
||||
* @param tag
|
||||
* @param attribute
|
||||
* @returns true if the attribute is internal
|
||||
*/
|
||||
isInternalAttribute(tag: string, attribute: string): boolean;
|
||||
}
|
||||
|
||||
declare class HTMLGenerator extends HTMLPropGenerator {
|
||||
run(): string;
|
||||
/**
|
||||
* @View
|
||||
* const $el = createElement(tag)
|
||||
*/
|
||||
declareHTMLNode(tag: types.Expression): [string, types.Statement];
|
||||
/**
|
||||
* @View
|
||||
* $insert($el, childNode)
|
||||
*/
|
||||
private insertChildNode;
|
||||
}
|
||||
|
||||
declare class TemplateGenerator extends HTMLPropGenerator {
|
||||
run(): string;
|
||||
private generateTemplate;
|
||||
/**
|
||||
* @View
|
||||
* const $el = template.cloneNode(true)
|
||||
*/
|
||||
private declareTemplateNode;
|
||||
/**
|
||||
* @View
|
||||
* $insert($el, childNode)
|
||||
*/
|
||||
private insertChildNode;
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}.firstChild
|
||||
* or
|
||||
* ${dlNodeName}.firstChild.nextSibling
|
||||
* or
|
||||
* ...
|
||||
* ${dlNodeName}.childNodes[${num}]
|
||||
*/
|
||||
private insertElement;
|
||||
/**
|
||||
* @brief Insert elements to the template node from the paths
|
||||
* @param paths
|
||||
* @param dlNodeName
|
||||
* @returns
|
||||
*/
|
||||
private insertElements;
|
||||
/**
|
||||
* @brief Extract common prefix from paths
|
||||
* e.g.
|
||||
* [0, 1, 2, 3] + [0, 1, 2, 4] => [0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 4]
|
||||
* [0, 1, 2] is the common prefix
|
||||
* @param paths
|
||||
* @returns paths with common prefix
|
||||
*/
|
||||
private static pathWithCommonPrefix;
|
||||
/**
|
||||
* @brief Find the best node name and path for the given path by looking into the nameMap.
|
||||
* If there's a full match, return the name and an empty path
|
||||
* If there's a partly match, return the name and the remaining path
|
||||
* If there's a nextSibling match, return the name and the remaining path with sibling offset
|
||||
* @param nameMap
|
||||
* @param path
|
||||
* @param defaultName
|
||||
* @returns [name, path, siblingOffset]
|
||||
*/
|
||||
private static findBestNodeAndPath;
|
||||
}
|
||||
|
||||
declare class TextGenerator extends BaseGenerator {
|
||||
run(): string;
|
||||
declareTextNode(content: types.Literal): [string, types.Statement];
|
||||
}
|
||||
|
||||
declare class ExpressionGenerator extends BaseGenerator {
|
||||
run(): string;
|
||||
declareExpressionNode(expression: types.Expression): [string, types.Statement];
|
||||
}
|
||||
|
||||
declare const viewGeneratorMap: {
|
||||
readonly html: typeof HTMLGenerator;
|
||||
readonly comp: typeof CompGenerator;
|
||||
readonly template: typeof TemplateGenerator;
|
||||
readonly text: typeof TextGenerator;
|
||||
readonly exp: typeof ExpressionGenerator;
|
||||
readonly if: typeof CompGenerator;
|
||||
readonly env: typeof CompGenerator;
|
||||
};
|
||||
declare function generateNew(oldGenerator: any, viewUnit: ViewUnit, resetIdx?: boolean): [string, types.Statement[], types.Statement[]];
|
||||
declare function generateView(viewUnits: ViewUnit[], config: ViewGeneratorConfig, templateIdx?: number): [types.Statement[], types.ExpressionStatement];
|
||||
|
||||
export { ViewGeneratorConfig, generateNew, generateView, viewGeneratorMap };
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "@inula/jsx-view-generator",
|
||||
"version": "0.0.0",
|
||||
"description": "Inula view generator",
|
||||
"author": {
|
||||
"name": "IanDx",
|
||||
"email": "iandxssxx@gmail.com"
|
||||
},
|
||||
"keywords": [
|
||||
"inula"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup --sourcemap",
|
||||
"test": "vitest --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/generator": "^7.23.6",
|
||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||
"@inula/jsx-view-parser": "workspace:*",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/babel__generator": "^7.6.8",
|
||||
"@vitest/ui": "^0.34.5",
|
||||
"tsup": "^6.7.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vitest": "^0.34.5"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"format": [
|
||||
"cjs",
|
||||
"esm"
|
||||
],
|
||||
"clean": true,
|
||||
"dts": true,
|
||||
"minify": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
import { UnitProp, ViewUnit } from '@inula/jsx-view-parser';
|
||||
import { ViewGeneratorConfig } from '../types';
|
||||
import type { types as t, traverse } from '@babel/core';
|
||||
|
||||
|
||||
export default class BaseGenerator {
|
||||
readonly viewUnit: ViewUnit
|
||||
readonly config: ViewGeneratorConfig
|
||||
readonly t
|
||||
readonly traverse: typeof traverse
|
||||
readonly elementAttributeMap
|
||||
readonly importMap
|
||||
|
||||
constructor(viewUnit: ViewUnit, config: ViewGeneratorConfig) {
|
||||
this.config = config;
|
||||
this.t = config.babelApi.types;
|
||||
this.traverse = config.babelApi.traverse;
|
||||
this.importMap = config.importMap;
|
||||
this.viewUnit = viewUnit;
|
||||
this.elementAttributeMap = config.attributeMap
|
||||
? Object.entries(config.attributeMap).reduce<Record<string, string[]>>(
|
||||
(acc, [key, elements]) => {
|
||||
elements.forEach(element => {
|
||||
if (!acc[element]) acc[element] = [];
|
||||
acc[element].push(key);
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
)
|
||||
: {};
|
||||
}
|
||||
|
||||
// ---- Init Statements
|
||||
private readonly initStatements: t.Statement[] = []
|
||||
addStatement(...statements: t.Statement[]) {
|
||||
this.initStatements.push(...statements);
|
||||
}
|
||||
|
||||
private readonly templates: t.Statement[] = []
|
||||
addTemplate(...template: t.Statement[]) {
|
||||
this.templates.push(...template);
|
||||
}
|
||||
|
||||
// ---- Generate ----
|
||||
/**
|
||||
* @brief To be implemented by the subclass
|
||||
*/
|
||||
run(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
generate(): [string, t.Statement[], t.Statement[]]{
|
||||
const nodeName = this.run();
|
||||
return [nodeName, this.initStatements, this.templates];
|
||||
}
|
||||
|
||||
// ---- Reactivity ----
|
||||
/**
|
||||
* @brief Check if the expression is reactive, which satisfies the following conditions:
|
||||
* 1. Contains .get() property
|
||||
* 2. The whole expression is not a function
|
||||
* @param expression
|
||||
* @returns
|
||||
*/
|
||||
checkReactive(expression: t.Expression) {
|
||||
if (this.t.isFunction(expression)) return false;
|
||||
let reactive = false;
|
||||
this.traverse(this.wrapWithFile(expression), {
|
||||
MemberExpression: path => {
|
||||
if (this.t.isIdentifier(path.node.property, { name: 'get' })) {
|
||||
reactive = true;
|
||||
path.stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
return reactive;
|
||||
}
|
||||
|
||||
// ---- Names ----
|
||||
private readonly prefixMap = {
|
||||
node: '$node',
|
||||
template: '$template',
|
||||
}
|
||||
|
||||
nodeIdx = -1;
|
||||
geneNodeName(idx?: number): string {
|
||||
return `${this.prefixMap.node}${idx ?? ++this.nodeIdx}`;
|
||||
}
|
||||
templateIdx = -1;
|
||||
generateTemplateName() {
|
||||
return `${this.prefixMap.template}${++this.templateIdx}`;
|
||||
}
|
||||
|
||||
|
||||
// ---- Utils ----
|
||||
/**
|
||||
* @brief Wrap the value in a file
|
||||
* @param node
|
||||
* @returns wrapped value
|
||||
*/
|
||||
wrapWithFile(node: t.Expression): t.File {
|
||||
return this.t.file(this.t.program([this.t.expressionStatement(node)]));
|
||||
}
|
||||
|
||||
addWatch(value: t.Expression) {
|
||||
return this.t.callExpression(
|
||||
this.t.identifier(this.importMap.watch),
|
||||
[this.t.arrowFunctionExpression([], value)]
|
||||
);
|
||||
}
|
||||
|
||||
createCollector(): [t.Statement[], (statement: t.Statement | t.Statement[] | null) => void]{
|
||||
const statements: t.Statement[] = [];
|
||||
function collect(statement: t.Statement | t.Statement[] | null) {
|
||||
if (Array.isArray(statement)) {
|
||||
statements.push(...statement);
|
||||
} else if (statement) {
|
||||
statements.push(statement);
|
||||
}
|
||||
}
|
||||
return [statements, collect];
|
||||
}
|
||||
|
||||
parseViewProp(prop: UnitProp, generateView: (units: ViewUnit[], config: ViewGeneratorConfig, templateIdx: number) => [t.Statement[], t.ExpressionStatement]): t.Expression {
|
||||
let value = prop.value;
|
||||
const viewPropMap = prop.viewPropMap;
|
||||
const propNodeMap = Object.fromEntries(
|
||||
Object.entries(viewPropMap).map(([key, units]) => {
|
||||
const [templates, statement] = generateView(units, this.config, this.templateIdx);
|
||||
this.addTemplate(...templates);
|
||||
return [key, statement];
|
||||
})
|
||||
);
|
||||
|
||||
this.traverse(this.wrapWithFile(value), {
|
||||
StringLiteral: path => {
|
||||
const key = path.node.value;
|
||||
if (propNodeMap[key]) {
|
||||
if (this.t.isNodesEquivalent(value, path.node)) {
|
||||
value = (propNodeMap[key] as t.ExpressionStatement).expression;
|
||||
}
|
||||
path.replaceWith(propNodeMap[key]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
parseProps(props: Record<string, UnitProp>, generateView: (units: ViewUnit[], config: ViewGeneratorConfig) => [t.Statement[], t.ExpressionStatement]) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(props).map(([key, prop]) => {
|
||||
return [key, this.parseViewProp(prop, generateView)];
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
import BaseGenerator from './BaseGenerator';
|
||||
import type { types as t } from '@babel/core';
|
||||
|
||||
export class HTMLPropGenerator extends BaseGenerator {
|
||||
static DelegatedEvents = new Set([
|
||||
'beforeinput',
|
||||
'click',
|
||||
'dblclick',
|
||||
'contextmenu',
|
||||
'focusin',
|
||||
'focusout',
|
||||
'input',
|
||||
'keydown',
|
||||
'keyup',
|
||||
'mousedown',
|
||||
'mousemove',
|
||||
'mouseout',
|
||||
'mouseover',
|
||||
'mouseup',
|
||||
'pointerdown',
|
||||
'pointermove',
|
||||
'pointerout',
|
||||
'pointerover',
|
||||
'pointerup',
|
||||
'touchend',
|
||||
'touchmove',
|
||||
'touchstart',
|
||||
])
|
||||
|
||||
addHTMLProp(
|
||||
nodeName: string,
|
||||
tag: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
): t.Statement {
|
||||
const shouldWatch = this.checkReactive(value);
|
||||
let expression = this.setDynamicHTMLProp(nodeName, tag, key, value, shouldWatch);
|
||||
if (shouldWatch) expression = this.addWatch(expression);
|
||||
|
||||
return this.t.expressionStatement(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setStyle($node, value)
|
||||
*/
|
||||
private setStyle(
|
||||
nodeName: string,
|
||||
value: t.Expression,
|
||||
) {
|
||||
return this.t.callExpression(
|
||||
this.t.identifier(this.importMap.setStyle),
|
||||
[this.t.identifier(nodeName), value]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setDataset($node, value)
|
||||
*/
|
||||
private setDataset(
|
||||
nodeName: string,
|
||||
value: t.Expression,
|
||||
) {
|
||||
return this.t.callExpression(
|
||||
this.t.identifier(this.importMap.setDataset),
|
||||
[this.t.identifier(nodeName), value]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* $node.key = value
|
||||
*/
|
||||
private setStaticProperty(
|
||||
nodeName: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
) {
|
||||
return this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.memberExpression(
|
||||
this.t.identifier(nodeName),
|
||||
this.t.identifier(key),
|
||||
),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setProperty($node, key, value)
|
||||
*/
|
||||
private setDynamicProperty(
|
||||
nodeName: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
) {
|
||||
return this.t.callExpression(
|
||||
this.t.identifier(this.importMap.setProperty),
|
||||
[this.t.identifier(nodeName), this.t.stringLiteral(key), value]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* $node.setAttribute(key, value)
|
||||
*/
|
||||
private setStaticAttribute(
|
||||
nodeName: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
) {
|
||||
return this.t.callExpression(
|
||||
this.t.memberExpression(
|
||||
this.t.identifier(nodeName),
|
||||
this.t.identifier('setAttribute'),
|
||||
),
|
||||
[this.t.stringLiteral(key), value]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setAttribute($node, key, value)
|
||||
*/
|
||||
private setDynamicAttribute(
|
||||
nodeName: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
) {
|
||||
return this.t.callExpression(
|
||||
this.t.identifier(this.importMap.setAttribute),
|
||||
[this.t.identifier(nodeName), this.t.stringLiteral(key), value]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* delegateEvent($node, eventName, value)
|
||||
*/
|
||||
private setDelegatedEvent(
|
||||
nodeName: string,
|
||||
eventName: string,
|
||||
value: t.Expression,
|
||||
) {
|
||||
return this.t.callExpression(
|
||||
this.t.identifier(this.importMap.delegateEvent),
|
||||
[this.t.identifier(nodeName), this.t.stringLiteral(eventName), value]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* $node.addEventListener(eventName, value)
|
||||
*/
|
||||
private setStaticEvent(
|
||||
nodeName: string,
|
||||
eventName: string,
|
||||
value: t.Expression,
|
||||
) {
|
||||
return this.t.callExpression(
|
||||
this.t.memberExpression(
|
||||
this.t.identifier(nodeName),
|
||||
this.t.identifier('addEventListener'),
|
||||
),
|
||||
[this.t.stringLiteral(eventName), value]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* addEventListener($node, eventName, value)
|
||||
*/
|
||||
private setDynamicEvent(
|
||||
nodeName: string,
|
||||
eventName: string,
|
||||
value: t.Expression,
|
||||
) {
|
||||
return this.t.callExpression(
|
||||
this.t.identifier(this.importMap.addEventListener),
|
||||
[this.t.identifier(nodeName), this.t.stringLiteral(eventName), value]
|
||||
);
|
||||
}
|
||||
|
||||
private setDynamicHTMLProp(
|
||||
nodeName: string,
|
||||
tag: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
dynamic: boolean
|
||||
): t.Expression {
|
||||
if (key === 'style') return this.setStyle(nodeName, value);
|
||||
if (key === 'dataset') return this.setDataset(nodeName, value);
|
||||
if (key.startsWith('on')) {
|
||||
const event = key.slice(2).toLowerCase();
|
||||
if (HTMLPropGenerator.DelegatedEvents.has(event)) {
|
||||
return this.setDelegatedEvent(nodeName, event, value);
|
||||
}
|
||||
return this[dynamic ? 'setDynamicEvent' : 'setStaticEvent'](nodeName, event, value);
|
||||
}
|
||||
if (this.isInternalAttribute(tag, key)) {
|
||||
if (key === 'class') key = 'className';
|
||||
else if (key === 'for') key = 'htmlFor';
|
||||
return this[dynamic ? 'setDynamicProperty' : 'setStaticProperty'](nodeName, key, value);
|
||||
}
|
||||
return this[dynamic ? 'setDynamicAttribute' : 'setStaticAttribute'](nodeName, key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the attribute is internal, i.e., can be accessed as js property
|
||||
* @param tag
|
||||
* @param attribute
|
||||
* @returns true if the attribute is internal
|
||||
*/
|
||||
isInternalAttribute(tag: string, attribute: string): boolean {
|
||||
return (
|
||||
this.elementAttributeMap['*']?.includes(attribute) ||
|
||||
this.elementAttributeMap[tag]?.includes(attribute)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { CompUnit, ViewUnit } from '@inula/jsx-view-parser';
|
||||
import BaseGenerator from '../HelperGenerators/BaseGenerator';
|
||||
import type { types as t } from '@babel/core';
|
||||
import { generateBlock, generateView } from '../generate';
|
||||
|
||||
export class CompGenerator extends BaseGenerator {
|
||||
run(): string {
|
||||
const { tag, props: propsWithView, children } = this.viewUnit as CompUnit;
|
||||
const props = this.parseProps(propsWithView, generateView);
|
||||
|
||||
const [nodeName, statement] = this.declareCompNode(tag, props, children);
|
||||
this.addStatement(statement);
|
||||
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* const $el = createComponent(tag, {
|
||||
* ...props
|
||||
* }, spreadProps)
|
||||
*/
|
||||
declareCompNode(tag: t.Expression, props: Record<string, t.Expression>, children: ViewUnit[]): [string, t.Statement] {
|
||||
const name = this.geneNodeName();
|
||||
const nodes = [];
|
||||
if (Object.keys(props).length > 0 || children.length > 0) {
|
||||
const propNode = this.t.objectExpression(Object.entries(props).map(([key, value]) => {
|
||||
const isReactive = this.checkReactive(value);
|
||||
if (isReactive) {
|
||||
/**
|
||||
* @View
|
||||
* get reactiveValue() {
|
||||
* return value
|
||||
* }
|
||||
*/
|
||||
return (
|
||||
this.t.objectMethod('get', this.t.identifier(key), [],
|
||||
this.t.blockStatement([
|
||||
this.t.returnStatement(value)
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
return this.t.objectProperty(
|
||||
this.t.identifier(key),
|
||||
value
|
||||
);
|
||||
}));
|
||||
if (children.length > 0) {
|
||||
const statement = generateBlock(children, this.config);
|
||||
propNode.properties.push(
|
||||
this.t.objectMethod('get', this.t.identifier('children'), [], statement)
|
||||
);
|
||||
}
|
||||
nodes.push(propNode);
|
||||
}
|
||||
|
||||
|
||||
return [name, this.t.variableDeclaration('const', [
|
||||
this.t.variableDeclarator(
|
||||
this.t.identifier(name),
|
||||
this.t.callExpression(
|
||||
this.t.identifier(this.importMap.createComponent),
|
||||
[tag, ...nodes]
|
||||
)
|
||||
)
|
||||
])];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { ExpUnit } from '@inula/jsx-view-parser';
|
||||
import BaseGenerator from '../HelperGenerators/BaseGenerator';
|
||||
import type { types as t } from '@babel/core';
|
||||
import { generateView } from '../generate';
|
||||
|
||||
export class ExpressionGenerator extends BaseGenerator {
|
||||
run() {
|
||||
const { content: contentWithProp } = this.viewUnit as ExpUnit;
|
||||
const content = this.parseViewProp(contentWithProp, generateView);
|
||||
const [nodeName, statement] = this.declareExpressionNode(content);
|
||||
this.addStatement(statement);
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
|
||||
declareExpressionNode(expression: t.Expression): [string, t.Statement] {
|
||||
const name = this.geneNodeName();
|
||||
return [name, this.t.variableDeclaration('const', [
|
||||
this.t.variableDeclarator(
|
||||
this.t.identifier(name),
|
||||
expression
|
||||
)
|
||||
])];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import { HTMLUnit } from '@inula/jsx-view-parser';
|
||||
import type { types as t } from '@babel/core';
|
||||
import { HTMLPropGenerator } from '../HelperGenerators/HTMLPropGenerator';
|
||||
import { generateNew, generateView } from '../generate';
|
||||
|
||||
export class HTMLGenerator extends HTMLPropGenerator {
|
||||
run(){
|
||||
const { tag, props: propsWithView, children } = this.viewUnit as HTMLUnit;
|
||||
const props = this.parseProps(propsWithView, generateView);
|
||||
const [nodeName, statement] = this.declareHTMLNode(tag);
|
||||
this.addStatement(statement);
|
||||
|
||||
// ---- Use the tag name to check if the prop is internal for the tag,
|
||||
// for dynamic tag, we can't check it, so we just assume it's not internal
|
||||
// represent by the "ANY" tag name
|
||||
const tagName = this.t.isStringLiteral(tag) ? tag.value : 'ANY';
|
||||
|
||||
Object.entries(props).forEach(([key, prop]) => {
|
||||
this.addStatement(this.addHTMLProp(nodeName, tagName, key, prop));
|
||||
});
|
||||
|
||||
children.forEach(child => {
|
||||
const [childNodeName, childStatements] = generateNew(this, child);
|
||||
this.addStatement(...childStatements);
|
||||
this.addStatement(this.insertChildNode(nodeName, childNodeName));
|
||||
});
|
||||
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* const $el = createElement(tag)
|
||||
*/
|
||||
declareHTMLNode(tag: t.Expression): [string, t.Statement] {
|
||||
const name = this.geneNodeName();
|
||||
return [name, this.t.variableDeclaration('const', [
|
||||
this.t.variableDeclarator(
|
||||
this.t.identifier(name),
|
||||
this.t.callExpression(
|
||||
this.t.identifier('createElement'),
|
||||
[tag]
|
||||
)
|
||||
)
|
||||
])];
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* $insert($el, childNode)
|
||||
*/
|
||||
private insertChildNode(
|
||||
parent: string,
|
||||
child: string
|
||||
) {
|
||||
return this.t.expressionStatement(
|
||||
this.t.callExpression(
|
||||
this.t.identifier(this.importMap.insert),
|
||||
[this.t.identifier(parent), this.t.identifier(child)]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
import { HTMLUnit, TemplateUnit } from '@inula/jsx-view-parser';
|
||||
import { HTMLPropGenerator } from '../HelperGenerators/HTMLPropGenerator';
|
||||
import { generateNew } from '../generate';
|
||||
import type { types as t } from '@babel/core';
|
||||
|
||||
|
||||
export class TemplateGenerator extends HTMLPropGenerator{
|
||||
run(): string {
|
||||
const { template, mutableUnits, props } = this.viewUnit as TemplateUnit;
|
||||
|
||||
const templateName = this.generateTemplate(template);
|
||||
const [nodeName, statement] = this.declareTemplateNode(templateName);
|
||||
this.addStatement(statement);
|
||||
|
||||
// ---- Insert elements first
|
||||
const paths: number[][] = [];
|
||||
props.forEach(({ path }) => {
|
||||
paths.push(path);
|
||||
});
|
||||
mutableUnits.forEach(({ path }) => {
|
||||
// ---- ParentPath and NextPath
|
||||
paths.push(path.slice(0, -1));
|
||||
if (path[path.length - 1] !== -1) paths.push(path);
|
||||
});
|
||||
const [insertElementStatements, pathNameMap] = this.insertElements(
|
||||
paths,
|
||||
nodeName
|
||||
);
|
||||
|
||||
this.addStatement(...insertElementStatements);
|
||||
|
||||
// ---- Resolve props
|
||||
props.forEach(
|
||||
({
|
||||
tag,
|
||||
path,
|
||||
key,
|
||||
value,
|
||||
}) => {
|
||||
const name = pathNameMap[path.join('.')];
|
||||
|
||||
const tagName = this.t.isStringLiteral(tag) ? tag.value : 'ANY';
|
||||
this.addStatement(
|
||||
this.addHTMLProp(
|
||||
name,
|
||||
tagName,
|
||||
key,
|
||||
value,
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// ---- Resolve mutable units
|
||||
mutableUnits.forEach(unit => {
|
||||
const path = unit.path;
|
||||
// ---- Find parent htmlElement
|
||||
const parentName = pathNameMap[path.slice(0, -1).join('.')];
|
||||
const nextName = pathNameMap[path.join('.')];
|
||||
console.log(nextName, path);
|
||||
const [childName, childStatements] = generateNew(this, unit);
|
||||
this.addStatement(...childStatements);
|
||||
this.addStatement(this.insertChildNode(parentName, childName, nextName));
|
||||
});
|
||||
|
||||
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
private generateTemplate(template: HTMLUnit): string {
|
||||
const templateName = this.generateTemplateName();
|
||||
const [name, statements] = generateNew(this, template, false);
|
||||
const returnStatement = this.t.returnStatement(this.t.identifier(name));
|
||||
|
||||
this.addTemplate(
|
||||
this.t.variableDeclaration('const', [
|
||||
this.t.variableDeclarator(
|
||||
this.t.identifier(templateName),
|
||||
this.t.arrowFunctionExpression([], this.t.blockStatement([...statements, returnStatement]))
|
||||
)
|
||||
])
|
||||
);
|
||||
|
||||
return templateName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* const $el = template.cloneNode(true)
|
||||
*/
|
||||
private declareTemplateNode(templateName: string): [string, t.Statement]{
|
||||
const name = this.geneNodeName();
|
||||
return [name, this.t.variableDeclaration('const', [
|
||||
this.t.variableDeclarator(
|
||||
this.t.identifier(name),
|
||||
this.t.callExpression(
|
||||
this.t.memberExpression(
|
||||
this.t.identifier(templateName),
|
||||
this.t.identifier('cloneNode')
|
||||
), [this.t.identifier('true')]
|
||||
)
|
||||
)
|
||||
])];
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* $insert($el, childNode)
|
||||
*/
|
||||
private insertChildNode(
|
||||
parent: string,
|
||||
child: string,
|
||||
nextName: string
|
||||
) {
|
||||
const nextNode = nextName ? [this.t.identifier(nextName)] : [];
|
||||
return this.t.expressionStatement(
|
||||
this.t.callExpression(
|
||||
this.t.identifier(this.importMap.insert),
|
||||
[this.t.identifier(parent), this.t.identifier(child), ...nextNode]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}.firstChild
|
||||
* or
|
||||
* ${dlNodeName}.firstChild.nextSibling
|
||||
* or
|
||||
* ...
|
||||
* ${dlNodeName}.childNodes[${num}]
|
||||
*/
|
||||
private insertElement(
|
||||
dlNodeName: string,
|
||||
path: number[],
|
||||
offset: number
|
||||
): t.Statement {
|
||||
const newNodeName = this.geneNodeName();
|
||||
if (path.length === 0) {
|
||||
return this.t.variableDeclaration('const', [
|
||||
this.t.variableDeclarator(
|
||||
this.t.identifier(newNodeName),
|
||||
Array.from({ length: offset }).reduce(
|
||||
(acc: t.Expression) =>
|
||||
this.t.memberExpression(acc, this.t.identifier('nextSibling')),
|
||||
this.t.identifier(dlNodeName)
|
||||
)
|
||||
)
|
||||
]);
|
||||
}
|
||||
const addFirstChild = (object: t.Expression) =>
|
||||
// ---- ${object}.firstChild
|
||||
this.t.memberExpression(object, this.t.identifier('firstChild'));
|
||||
const addSecondChild = (object: t.Expression) =>
|
||||
// ---- ${object}.firstChild.nextSibling
|
||||
this.t.memberExpression(
|
||||
addFirstChild(object),
|
||||
this.t.identifier('nextSibling')
|
||||
);
|
||||
const addThirdChild = (object: t.Expression) =>
|
||||
// ---- ${object}.firstChild.nextSibling.nextSibling
|
||||
this.t.memberExpression(
|
||||
addSecondChild(object),
|
||||
this.t.identifier('nextSibling')
|
||||
);
|
||||
const addOtherChild = (object: t.Expression, num: number) =>
|
||||
// ---- ${object}.childNodes[${num}]
|
||||
this.t.memberExpression(
|
||||
this.t.memberExpression(object, this.t.identifier('childNodes')),
|
||||
this.t.numericLiteral(num),
|
||||
true
|
||||
);
|
||||
const addNextSibling = (object: t.Expression) =>
|
||||
// ---- ${object}.nextSibling
|
||||
this.t.memberExpression(object, this.t.identifier('nextSibling'));
|
||||
|
||||
return this.t.variableDeclaration('const', [
|
||||
this.t.variableDeclarator(
|
||||
this.t.identifier(newNodeName),
|
||||
path.reduce((acc: t.Expression, cur: number, idx) => {
|
||||
if (idx === 0 && offset > 0) {
|
||||
for (let i = 0; i < offset; i++) acc = addNextSibling(acc);
|
||||
}
|
||||
if (cur === 0) return addFirstChild(acc);
|
||||
if (cur === 1) return addSecondChild(acc);
|
||||
if (cur === 2) return addThirdChild(acc);
|
||||
return addOtherChild(acc, cur);
|
||||
}, this.t.identifier(dlNodeName))
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Insert elements to the template node from the paths
|
||||
* @param paths
|
||||
* @param dlNodeName
|
||||
* @returns
|
||||
*/
|
||||
private insertElements(
|
||||
paths: number[][],
|
||||
dlNodeName: string
|
||||
): [t.Statement[], Record<string, string>] {
|
||||
const [statements, collect] = this.createCollector();
|
||||
const nameMap: Record<string, number[]> = { [dlNodeName]: [] };
|
||||
|
||||
const commonPrefixPaths = TemplateGenerator.pathWithCommonPrefix(paths);
|
||||
|
||||
commonPrefixPaths.forEach(path => {
|
||||
const res = TemplateGenerator.findBestNodeAndPath(
|
||||
nameMap,
|
||||
path,
|
||||
dlNodeName
|
||||
);
|
||||
const [, pat, offset] = res;
|
||||
let name = res[0];
|
||||
|
||||
if (pat.length !== 0 || offset !== 0) {
|
||||
collect(this.insertElement(name, pat, offset));
|
||||
name = this.geneNodeName(this.nodeIdx);
|
||||
nameMap[name] = path;
|
||||
}
|
||||
});
|
||||
const pathNameMap = Object.fromEntries(
|
||||
Object.entries(nameMap).map(([name, path]) => [path.join('.'), name])
|
||||
);
|
||||
|
||||
return [statements, pathNameMap];
|
||||
}
|
||||
|
||||
// ---- Path related
|
||||
/**
|
||||
* @brief Extract common prefix from paths
|
||||
* e.g.
|
||||
* [0, 1, 2, 3] + [0, 1, 2, 4] => [0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 4]
|
||||
* [0, 1, 2] is the common prefix
|
||||
* @param paths
|
||||
* @returns paths with common prefix
|
||||
*/
|
||||
private static pathWithCommonPrefix(paths: number[][]): number[][] {
|
||||
const allPaths = [...paths].sort();
|
||||
const ps = [...allPaths];
|
||||
ps.forEach(path0 => {
|
||||
ps.forEach(path1 => {
|
||||
if (path0 === path1) return;
|
||||
for (let i = 0; i < path0.length; i++) {
|
||||
if (path0[i] !== path1[i]) {
|
||||
if (i !== 0) {
|
||||
allPaths.push(path0.slice(0, i));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Sort by length and then by first element, small to large
|
||||
const sortedPaths = allPaths.sort((a, b) => {
|
||||
if (a.length !== b.length) return a.length - b.length;
|
||||
return a[0] - b[0];
|
||||
});
|
||||
|
||||
// ---- Deduplicate
|
||||
const deduplicatedPaths = [
|
||||
...new Set(sortedPaths.map(path => path.join('.'))),
|
||||
].map(path => path.split('.').filter(Boolean).map(Number));
|
||||
|
||||
return deduplicatedPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Find the best node name and path for the given path by looking into the nameMap.
|
||||
* If there's a full match, return the name and an empty path
|
||||
* If there's a partly match, return the name and the remaining path
|
||||
* If there's a nextSibling match, return the name and the remaining path with sibling offset
|
||||
* @param nameMap
|
||||
* @param path
|
||||
* @param defaultName
|
||||
* @returns [name, path, siblingOffset]
|
||||
*/
|
||||
private static findBestNodeAndPath(
|
||||
nameMap: Record<string, number[]>,
|
||||
path: number[],
|
||||
defaultName: string
|
||||
): [string, number[], number] {
|
||||
let bestMatchCount = 0;
|
||||
let bestMatchName: string | undefined;
|
||||
let bestHalfMatch: [string, number, number] | undefined;
|
||||
Object.entries(nameMap).forEach(([name, pat]) => {
|
||||
let matchCount = 0;
|
||||
const pathLength = pat.length;
|
||||
for (let i = 0; i < pathLength; i++) {
|
||||
if (pat[i] === path[i]) matchCount++;
|
||||
}
|
||||
// console.log(name, matchCount, pathLength - 1, pat);
|
||||
if (matchCount === pathLength - 1) {
|
||||
const offset = path[pathLength - 1] - pat[pathLength - 1];
|
||||
if (offset > 0 && offset <= 3) {
|
||||
bestHalfMatch = [name, matchCount, offset];
|
||||
}
|
||||
}
|
||||
if (matchCount !== pat.length) return;
|
||||
if (matchCount > bestMatchCount) {
|
||||
bestMatchName = name;
|
||||
bestMatchCount = matchCount;
|
||||
}
|
||||
});
|
||||
if (bestHalfMatch) {
|
||||
return [
|
||||
bestHalfMatch[0],
|
||||
path.slice(bestHalfMatch[1] + 1),
|
||||
bestHalfMatch[2],
|
||||
];
|
||||
}
|
||||
if (!bestMatchName) {
|
||||
return [defaultName, path, 0];
|
||||
}
|
||||
return [bestMatchName, path.slice(bestMatchCount), 0];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { TextUnit } from '@inula/jsx-view-parser';
|
||||
import BaseGenerator from '../HelperGenerators/BaseGenerator';
|
||||
import type { types as t } from '@babel/core';
|
||||
|
||||
export class TextGenerator extends BaseGenerator {
|
||||
run() {
|
||||
const { content } = this.viewUnit as TextUnit;
|
||||
const [nodeName, statement] = this.declareTextNode(content);
|
||||
this.addStatement(statement);
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
declareTextNode(content: t.Literal): [string, t.Statement] {
|
||||
const name = this.geneNodeName();
|
||||
return [name, this.t.variableDeclaration('const', [
|
||||
this.t.variableDeclarator(
|
||||
this.t.identifier(name),
|
||||
this.t.callExpression(
|
||||
this.t.identifier(this.importMap.createText),
|
||||
[content]
|
||||
)
|
||||
)
|
||||
])];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { ViewUnit } from '@inula/jsx-view-parser';
|
||||
import { CompGenerator } from './NodeGenerators/CompGenerator';
|
||||
import { HTMLGenerator } from './NodeGenerators/HTMLGenerator';
|
||||
import { ViewGeneratorConfig } from './types';
|
||||
import type { types as t } from '@babel/core';
|
||||
import { TemplateGenerator } from './NodeGenerators/TemplateGenerator';
|
||||
import { TextGenerator } from './NodeGenerators/TextGenerator';
|
||||
import { ExpressionGenerator } from './NodeGenerators/ExpressionGenerator';
|
||||
|
||||
export const viewGeneratorMap = {
|
||||
html: HTMLGenerator,
|
||||
comp: CompGenerator,
|
||||
template: TemplateGenerator,
|
||||
text: TextGenerator,
|
||||
exp: ExpressionGenerator,
|
||||
if: CompGenerator,
|
||||
env: CompGenerator,
|
||||
} as const;
|
||||
|
||||
|
||||
export function generateNew(oldGenerator: any, viewUnit: ViewUnit, resetIdx = true): [string, t.Statement[], t.Statement[]]{
|
||||
const generator = new viewGeneratorMap[viewUnit.type](viewUnit, oldGenerator.config);
|
||||
if (resetIdx) generator.nodeIdx = oldGenerator.nodeIdx;
|
||||
const [name, statements, templates] = generator.generate();
|
||||
if (resetIdx) oldGenerator.nodeIdx = generator.nodeIdx;
|
||||
return [name, statements, templates];
|
||||
}
|
||||
|
||||
export function generateBlock(viewUnits: ViewUnit[], config: ViewGeneratorConfig) {
|
||||
const t = config.babelApi.types;
|
||||
const names: string[] = [];
|
||||
const statements = viewUnits.flatMap(viewUnit => {
|
||||
const generator = new viewGeneratorMap[viewUnit.type](viewUnit, config);
|
||||
const [name, statements] = generator.generate();
|
||||
names.push(name);
|
||||
return statements;
|
||||
});
|
||||
|
||||
const returnStatement = t.returnStatement(t.arrayExpression(names.map(name => t.identifier(name))));
|
||||
return (
|
||||
t.blockStatement(statements.concat(returnStatement))
|
||||
);
|
||||
}
|
||||
|
||||
export function generateView(viewUnits: ViewUnit[], config: ViewGeneratorConfig, templateIdx=-1): [t.Statement[], t.ExpressionStatement] {
|
||||
const t = config.babelApi.types;
|
||||
const names: string[] = [];
|
||||
const allTemplates: t.Statement[] = [];
|
||||
let nodeIdx = -1;
|
||||
const statements = viewUnits.flatMap(viewUnit => {
|
||||
const generator = new viewGeneratorMap[viewUnit.type](viewUnit, config);
|
||||
generator.templateIdx = templateIdx;
|
||||
generator.nodeIdx = nodeIdx;
|
||||
const [name, statements, templates] = generator.generate();
|
||||
templateIdx = generator.templateIdx;
|
||||
nodeIdx = generator.nodeIdx;
|
||||
names.push(name);
|
||||
allTemplates.push(...templates);
|
||||
return statements;
|
||||
});
|
||||
|
||||
const returnStatement = t.returnStatement(t.arrayExpression(names.map(name => t.identifier(name))));
|
||||
|
||||
return [allTemplates, (
|
||||
t.expressionStatement(
|
||||
t.callExpression(
|
||||
t.arrowFunctionExpression([], t.blockStatement(statements.concat(returnStatement))),
|
||||
[]
|
||||
)
|
||||
)
|
||||
)];
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export {generateView, generateNew, viewGeneratorMap} from './generate';
|
||||
export type {ViewGeneratorConfig} from './types';
|
|
@ -0,0 +1,82 @@
|
|||
import { describe, it } from 'vitest';
|
||||
import { expectView } from './mock';
|
||||
|
||||
|
||||
describe('Comp', () => {
|
||||
it('should generate a Component', () => {
|
||||
expectView(/*jsx*/`
|
||||
<Comp/>
|
||||
`, /*js*/ `
|
||||
const $node0 = createComponent(Comp)
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a Component with props', () => {
|
||||
expectView(/*jsx*/`
|
||||
<Comp prop1="value1" prop2={value2}/>
|
||||
`, /*js*/`
|
||||
const $node0 = createComponent(Comp, {
|
||||
prop1: "value1",
|
||||
prop2: value2
|
||||
})
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a Component with children', () => {
|
||||
expectView(/*jsx*/`
|
||||
<Comp>
|
||||
<div></div>
|
||||
</Comp>
|
||||
`, /*js*/`
|
||||
const $node0 = createComponent(Comp, {
|
||||
get children() {
|
||||
const $node0 = createElement("div")
|
||||
return [$node0]
|
||||
}
|
||||
})
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a Component with props and children', () => {
|
||||
expectView(/*jsx*/`
|
||||
<Comp prop1="value1" prop2={value2}>
|
||||
<div></div>
|
||||
</Comp>
|
||||
`, /*js*/`
|
||||
const $node0 = createComponent(Comp, {
|
||||
prop1: "value1",
|
||||
prop2: value2,
|
||||
get children() {
|
||||
const $node0 = createElement("div")
|
||||
return [$node0]
|
||||
}
|
||||
})
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a Component with reactive props', () => {
|
||||
expectView(/*jsx*/`
|
||||
<Comp prop1={value.get()}/>
|
||||
`, /*js*/`
|
||||
const $node0 = createComponent(Comp, {
|
||||
get prop1() {
|
||||
return value.get()
|
||||
}
|
||||
})
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a Component with render/view props', () => {
|
||||
expectView(/*jsx*/`
|
||||
<Comp render={<div>ok</div>}/>
|
||||
`, /*js*/`
|
||||
const $node0 = createComponent(Comp, {
|
||||
render: (() => {
|
||||
const $node0 = createElement("div")
|
||||
$node0.textContent = "ok"
|
||||
return [$node0]
|
||||
})()
|
||||
})
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,513 @@
|
|||
|
||||
export const importMap = [
|
||||
'createElement',
|
||||
'setStyle',
|
||||
'setAttribute',
|
||||
'setDataset',
|
||||
'setProperty',
|
||||
'setEvent',
|
||||
'delegateEvent',
|
||||
'addEventListener',
|
||||
'watch',
|
||||
'insert',
|
||||
'createComponent',
|
||||
'createText'
|
||||
].reduce<Record<string, string>>((acc, cur) => {
|
||||
acc[cur] = cur;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export const htmlTags = [
|
||||
'a',
|
||||
'abbr',
|
||||
'address',
|
||||
'area',
|
||||
'article',
|
||||
'aside',
|
||||
'audio',
|
||||
'b',
|
||||
'base',
|
||||
'bdi',
|
||||
'bdo',
|
||||
'blockquote',
|
||||
'body',
|
||||
'br',
|
||||
'button',
|
||||
'canvas',
|
||||
'caption',
|
||||
'cite',
|
||||
'code',
|
||||
'col',
|
||||
'colgroup',
|
||||
'data',
|
||||
'datalist',
|
||||
'dd',
|
||||
'del',
|
||||
'details',
|
||||
'dfn',
|
||||
'dialog',
|
||||
'div',
|
||||
'dl',
|
||||
'dt',
|
||||
'em',
|
||||
'embed',
|
||||
'fieldset',
|
||||
'figcaption',
|
||||
'figure',
|
||||
'footer',
|
||||
'form',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'head',
|
||||
'header',
|
||||
'hgroup',
|
||||
'hr',
|
||||
'html',
|
||||
'i',
|
||||
'iframe',
|
||||
'img',
|
||||
'input',
|
||||
'ins',
|
||||
'kbd',
|
||||
'label',
|
||||
'legend',
|
||||
'li',
|
||||
'link',
|
||||
'main',
|
||||
'map',
|
||||
'mark',
|
||||
'menu',
|
||||
'meta',
|
||||
'meter',
|
||||
'nav',
|
||||
'noscript',
|
||||
'object',
|
||||
'ol',
|
||||
'optgroup',
|
||||
'option',
|
||||
'output',
|
||||
'p',
|
||||
'picture',
|
||||
'pre',
|
||||
'progress',
|
||||
'q',
|
||||
'rp',
|
||||
'rt',
|
||||
'ruby',
|
||||
's',
|
||||
'samp',
|
||||
'script',
|
||||
'section',
|
||||
'select',
|
||||
'slot',
|
||||
'small',
|
||||
'source',
|
||||
'span',
|
||||
'strong',
|
||||
'style',
|
||||
'sub',
|
||||
'summary',
|
||||
'sup',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'template',
|
||||
'textarea',
|
||||
'tfoot',
|
||||
'th',
|
||||
'thead',
|
||||
'time',
|
||||
'title',
|
||||
'tr',
|
||||
'track',
|
||||
'u',
|
||||
'ul',
|
||||
'var',
|
||||
'video',
|
||||
'wbr',
|
||||
'acronym',
|
||||
'applet',
|
||||
'basefont',
|
||||
'bgsound',
|
||||
'big',
|
||||
'blink',
|
||||
'center',
|
||||
'dir',
|
||||
'font',
|
||||
'frame',
|
||||
'frameset',
|
||||
'isindex',
|
||||
'keygen',
|
||||
'listing',
|
||||
'marquee',
|
||||
'menuitem',
|
||||
'multicol',
|
||||
'nextid',
|
||||
'nobr',
|
||||
'noembed',
|
||||
'noframes',
|
||||
'param',
|
||||
'plaintext',
|
||||
'rb',
|
||||
'rtc',
|
||||
'spacer',
|
||||
'strike',
|
||||
'tt',
|
||||
'xmp',
|
||||
'animate',
|
||||
'animateMotion',
|
||||
'animateTransform',
|
||||
'circle',
|
||||
'clipPath',
|
||||
'defs',
|
||||
'desc',
|
||||
'ellipse',
|
||||
'feBlend',
|
||||
'feColorMatrix',
|
||||
'feComponentTransfer',
|
||||
'feComposite',
|
||||
'feConvolveMatrix',
|
||||
'feDiffuseLighting',
|
||||
'feDisplacementMap',
|
||||
'feDistantLight',
|
||||
'feDropShadow',
|
||||
'feFlood',
|
||||
'feFuncA',
|
||||
'feFuncB',
|
||||
'feFuncG',
|
||||
'feFuncR',
|
||||
'feGaussianBlur',
|
||||
'feImage',
|
||||
'feMerge',
|
||||
'feMergeNode',
|
||||
'feMorphology',
|
||||
'feOffset',
|
||||
'fePointLight',
|
||||
'feSpecularLighting',
|
||||
'feSpotLight',
|
||||
'feTile',
|
||||
'feTurbulence',
|
||||
'filter',
|
||||
'foreignObject',
|
||||
'g',
|
||||
'image',
|
||||
'line',
|
||||
'linearGradient',
|
||||
'marker',
|
||||
'mask',
|
||||
'metadata',
|
||||
'mpath',
|
||||
'path',
|
||||
'pattern',
|
||||
'polygon',
|
||||
'polyline',
|
||||
'radialGradient',
|
||||
'rect',
|
||||
'set',
|
||||
'stop',
|
||||
'svg',
|
||||
'switch',
|
||||
'symbol',
|
||||
'text',
|
||||
'textPath',
|
||||
'tspan',
|
||||
'use',
|
||||
'view',
|
||||
];
|
||||
|
||||
/**
|
||||
* @brief HTML internal attribute map, can be accessed as js property
|
||||
*/
|
||||
export const attributeMap = {
|
||||
// ---- Other property as attribute
|
||||
textContent: ['*'],
|
||||
innerHTML: ['*'],
|
||||
// ---- Source: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes
|
||||
accept: ['form', 'input'],
|
||||
// ---- Original: accept-charset
|
||||
acceptCharset: ['form'],
|
||||
accesskey: ['*'],
|
||||
action: ['form'],
|
||||
align: [
|
||||
'caption',
|
||||
'col',
|
||||
'colgroup',
|
||||
'hr',
|
||||
'iframe',
|
||||
'img',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'tfoot',
|
||||
'th',
|
||||
'thead',
|
||||
'tr',
|
||||
],
|
||||
allow: ['iframe'],
|
||||
alt: ['area', 'img', 'input'],
|
||||
async: ['script'],
|
||||
autocapitalize: ['*'],
|
||||
autocomplete: ['form', 'input', 'select', 'textarea'],
|
||||
autofocus: ['button', 'input', 'select', 'textarea'],
|
||||
autoplay: ['audio', 'video'],
|
||||
background: ['body', 'table', 'td', 'th'],
|
||||
// ---- Original: base
|
||||
bgColor: [
|
||||
'body',
|
||||
'col',
|
||||
'colgroup',
|
||||
'marquee',
|
||||
'table',
|
||||
'tbody',
|
||||
'tfoot',
|
||||
'td',
|
||||
'th',
|
||||
'tr',
|
||||
],
|
||||
border: ['img', 'object', 'table'],
|
||||
buffered: ['audio', 'video'],
|
||||
capture: ['input'],
|
||||
charset: ['meta'],
|
||||
checked: ['input'],
|
||||
cite: ['blockquote', 'del', 'ins', 'q'],
|
||||
class: ['*'],
|
||||
color: ['font', 'hr'],
|
||||
cols: ['textarea'],
|
||||
// ---- Original: colspan
|
||||
colSpan: ['td', 'th'],
|
||||
content: ['meta'],
|
||||
// ---- Original: contenteditable
|
||||
contentEditable: ['*'],
|
||||
contextmenu: ['*'],
|
||||
controls: ['audio', 'video'],
|
||||
coords: ['area'],
|
||||
crossOrigin: ['audio', 'img', 'link', 'script', 'video'],
|
||||
csp: ['iframe'],
|
||||
data: ['object'],
|
||||
// ---- Original: datetime
|
||||
dateTime: ['del', 'ins', 'time'],
|
||||
decoding: ['img'],
|
||||
default: ['track'],
|
||||
defer: ['script'],
|
||||
dir: ['*'],
|
||||
dirname: ['input', 'textarea'],
|
||||
disabled: [
|
||||
'button',
|
||||
'fieldset',
|
||||
'input',
|
||||
'optgroup',
|
||||
'option',
|
||||
'select',
|
||||
'textarea',
|
||||
],
|
||||
download: ['a', 'area'],
|
||||
draggable: ['*'],
|
||||
enctype: ['form'],
|
||||
// ---- Original: enterkeyhint
|
||||
enterKeyHint: ['textarea', 'contenteditable'],
|
||||
for: ['label', 'output'],
|
||||
form: [
|
||||
'button',
|
||||
'fieldset',
|
||||
'input',
|
||||
'label',
|
||||
'meter',
|
||||
'object',
|
||||
'output',
|
||||
'progress',
|
||||
'select',
|
||||
'textarea',
|
||||
],
|
||||
// ---- Original: formaction
|
||||
formAction: ['input', 'button'],
|
||||
// ---- Original: formenctype
|
||||
formEnctype: ['button', 'input'],
|
||||
// ---- Original: formmethod
|
||||
formMethod: ['button', 'input'],
|
||||
// ---- Original: formnovalidate
|
||||
formNoValidate: ['button', 'input'],
|
||||
// ---- Original: formtarget
|
||||
formTarget: ['button', 'input'],
|
||||
headers: ['td', 'th'],
|
||||
height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
||||
hidden: ['*'],
|
||||
high: ['meter'],
|
||||
href: ['a', 'area', 'base', 'link'],
|
||||
hreflang: ['a', 'link'],
|
||||
// ---- Original: http-equiv
|
||||
httpEquiv: ['meta'],
|
||||
id: ['*'],
|
||||
integrity: ['link', 'script'],
|
||||
// ---- Original: intrinsicsize
|
||||
intrinsicSize: ['img'],
|
||||
// ---- Original: inputmode
|
||||
inputMode: ['textarea', 'contenteditable'],
|
||||
ismap: ['img'],
|
||||
// ---- Original: itemprop
|
||||
itemProp: ['*'],
|
||||
kind: ['track'],
|
||||
label: ['optgroup', 'option', 'track'],
|
||||
lang: ['*'],
|
||||
language: ['script'],
|
||||
loading: ['img', 'iframe'],
|
||||
list: ['input'],
|
||||
loop: ['audio', 'marquee', 'video'],
|
||||
low: ['meter'],
|
||||
manifest: ['html'],
|
||||
max: ['input', 'meter', 'progress'],
|
||||
// ---- Original: maxlength
|
||||
maxLength: ['input', 'textarea'],
|
||||
// ---- Original: minlength
|
||||
minLength: ['input', 'textarea'],
|
||||
media: ['a', 'area', 'link', 'source', 'style'],
|
||||
method: ['form'],
|
||||
min: ['input', 'meter'],
|
||||
multiple: ['input', 'select'],
|
||||
muted: ['audio', 'video'],
|
||||
name: [
|
||||
'button',
|
||||
'form',
|
||||
'fieldset',
|
||||
'iframe',
|
||||
'input',
|
||||
'object',
|
||||
'output',
|
||||
'select',
|
||||
'textarea',
|
||||
'map',
|
||||
'meta',
|
||||
'param',
|
||||
],
|
||||
// ---- Original: novalidate
|
||||
noValidate: ['form'],
|
||||
open: ['details', 'dialog'],
|
||||
optimum: ['meter'],
|
||||
pattern: ['input'],
|
||||
ping: ['a', 'area'],
|
||||
placeholder: ['input', 'textarea'],
|
||||
// ---- Original: playsinline
|
||||
playsInline: ['video'],
|
||||
poster: ['video'],
|
||||
preload: ['audio', 'video'],
|
||||
readonly: ['input', 'textarea'],
|
||||
// ---- Original: referrerpolicy
|
||||
referrerPolicy: ['a', 'area', 'iframe', 'img', 'link', 'script'],
|
||||
rel: ['a', 'area', 'link'],
|
||||
required: ['input', 'select', 'textarea'],
|
||||
reversed: ['ol'],
|
||||
role: ['*'],
|
||||
rows: ['textarea'],
|
||||
// ---- Original: rowspan
|
||||
rowSpan: ['td', 'th'],
|
||||
sandbox: ['iframe'],
|
||||
scope: ['th'],
|
||||
scoped: ['style'],
|
||||
selected: ['option'],
|
||||
shape: ['a', 'area'],
|
||||
size: ['input', 'select'],
|
||||
sizes: ['link', 'img', 'source'],
|
||||
slot: ['*'],
|
||||
span: ['col', 'colgroup'],
|
||||
spellcheck: ['*'],
|
||||
src: [
|
||||
'audio',
|
||||
'embed',
|
||||
'iframe',
|
||||
'img',
|
||||
'input',
|
||||
'script',
|
||||
'source',
|
||||
'track',
|
||||
'video',
|
||||
],
|
||||
srcdoc: ['iframe'],
|
||||
srclang: ['track'],
|
||||
srcset: ['img', 'source'],
|
||||
start: ['ol'],
|
||||
step: ['input'],
|
||||
style: ['*'],
|
||||
summary: ['table'],
|
||||
// ---- Original: tabindex
|
||||
tabIndex: ['*'],
|
||||
target: ['a', 'area', 'base', 'form'],
|
||||
title: ['*'],
|
||||
translate: ['*'],
|
||||
type: [
|
||||
'button',
|
||||
'input',
|
||||
'embed',
|
||||
'object',
|
||||
'ol',
|
||||
'script',
|
||||
'source',
|
||||
'style',
|
||||
'menu',
|
||||
'link',
|
||||
],
|
||||
usemap: ['img', 'input', 'object'],
|
||||
value: [
|
||||
'button',
|
||||
'data',
|
||||
'input',
|
||||
'li',
|
||||
'meter',
|
||||
'option',
|
||||
'progress',
|
||||
'param',
|
||||
'text' /** extra for TextNode */,
|
||||
],
|
||||
width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
||||
wrap: ['textarea'],
|
||||
// --- ARIA attributes
|
||||
// Source: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
|
||||
ariaAutocomplete: ['*'],
|
||||
ariaChecked: ['*'],
|
||||
ariaDisabled: ['*'],
|
||||
ariaErrorMessage: ['*'],
|
||||
ariaExpanded: ['*'],
|
||||
ariaHasPopup: ['*'],
|
||||
ariaHidden: ['*'],
|
||||
ariaInvalid: ['*'],
|
||||
ariaLabel: ['*'],
|
||||
ariaLevel: ['*'],
|
||||
ariaModal: ['*'],
|
||||
ariaMultiline: ['*'],
|
||||
ariaMultiSelectable: ['*'],
|
||||
ariaOrientation: ['*'],
|
||||
ariaPlaceholder: ['*'],
|
||||
ariaPressed: ['*'],
|
||||
ariaReadonly: ['*'],
|
||||
ariaRequired: ['*'],
|
||||
ariaSelected: ['*'],
|
||||
ariaSort: ['*'],
|
||||
ariaValuemax: ['*'],
|
||||
ariaValuemin: ['*'],
|
||||
ariaValueNow: ['*'],
|
||||
ariaValueText: ['*'],
|
||||
ariaBusy: ['*'],
|
||||
ariaLive: ['*'],
|
||||
ariaRelevant: ['*'],
|
||||
ariaAtomic: ['*'],
|
||||
ariaDropEffect: ['*'],
|
||||
ariaGrabbed: ['*'],
|
||||
ariaActiveDescendant: ['*'],
|
||||
ariaColCount: ['*'],
|
||||
ariaColIndex: ['*'],
|
||||
ariaColSpan: ['*'],
|
||||
ariaControls: ['*'],
|
||||
ariaDescribedBy: ['*'],
|
||||
ariaDescription: ['*'],
|
||||
ariaDetails: ['*'],
|
||||
ariaFlowTo: ['*'],
|
||||
ariaLabelledBy: ['*'],
|
||||
ariaOwns: ['*'],
|
||||
ariaPosInset: ['*'],
|
||||
ariaRowCount: ['*'],
|
||||
ariaRowIndex: ['*'],
|
||||
ariaRowSpan: ['*'],
|
||||
ariaSetSize: ['*'],
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
import { describe, it } from 'vitest';
|
||||
import { expectView } from './mock';
|
||||
|
||||
|
||||
describe('Expression', () => {
|
||||
it('should generate a expression Node', () => {
|
||||
expectView(/*jsx*/`
|
||||
<>{expr}</>
|
||||
`, /*js*/`
|
||||
const $node0 = expr
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
import { describe, it } from 'vitest';
|
||||
import { expectView } from './mock';
|
||||
|
||||
describe('HTML', () => {
|
||||
it('should generate a div element', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div></div>
|
||||
`, /*js*/ `
|
||||
const $node0 = createElement("div")
|
||||
`);
|
||||
});
|
||||
|
||||
// ---- Static Props
|
||||
it('should generate a div element with a static id', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div id="myDiv"></div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
$node0.id = "myDiv"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a div element with a static class', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div class="myClass"></div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
$node0.className = "myClass"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a div element with a static for', () => {
|
||||
expectView(/*jsx*/`
|
||||
<label for="myFor"></label>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("label")
|
||||
$node0.htmlFor = "myFor"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a div element with a static style', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div style="color: red"></div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
setStyle($node0, "color: red")
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a div element with a static dataset', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div dataset={myDataset}></div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
setDataset($node0, myDataset)
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a div element with a static event', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div onClick={myFunction}></div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
delegateEvent($node0, "click", myFunction)
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a div element with a static custom event', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div onMyEvent={myFunction}></div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
$node0.addEventListener("myevent", myFunction)
|
||||
`);
|
||||
});
|
||||
|
||||
// ---- Dynamic Props
|
||||
it('should generate a div element with a dynamic id', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div id={count.get()}></div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
watch(() => setProperty($node0, "id", count.get()))
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a div element with a dynamic style', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div style={styleList[count.get()]}></div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
watch(() => setStyle($node0, styleList[count.get()]))
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a div element with a dynamic dataset', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div dataset={datasetList[count.get()]}></div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
watch(() => setDataset($node0, datasetList[count.get()]))
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a div element with a dynamic event', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div onClick={() => { console.log(count.get()) }}></div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
delegateEvent(
|
||||
$node0, "click",
|
||||
() => { console.log(count.get()) }
|
||||
)
|
||||
`);
|
||||
});
|
||||
|
||||
it('should insert a element into a div', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div>
|
||||
<Comp />
|
||||
</div>
|
||||
`, /*js*/`
|
||||
const $node0 = createElement("div")
|
||||
const $node1 = createComponent(Comp)
|
||||
insert($node0, $node1)
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
import { ViewGeneratorConfig } from '../types';
|
||||
import babel, { parseSync, types as t } from '@babel/core';
|
||||
import { attributeMap, htmlTags, importMap } from './const';
|
||||
import { AllowedJSXNode, ViewParserConfig, parseView } from '@inula/jsx-view-parser';
|
||||
import babelJSX from '@babel/plugin-syntax-jsx';
|
||||
import { generateView as gV } from '..';
|
||||
import { expect } from 'vitest';
|
||||
import generate from '@babel/generator';
|
||||
|
||||
export const parserConfig: ViewParserConfig = {
|
||||
babelApi: babel,
|
||||
htmlTags,
|
||||
};
|
||||
|
||||
export function parseCode(code: string) {
|
||||
return (
|
||||
parseSync(code, {plugins: [babelJSX]})!.program
|
||||
.body[0] as t.ExpressionStatement
|
||||
).expression as AllowedJSXNode;
|
||||
}
|
||||
|
||||
export const generatorConfig: ViewGeneratorConfig = {
|
||||
babelApi: babel,
|
||||
importMap,
|
||||
attributeMap,
|
||||
};
|
||||
|
||||
export function generateView(code: string) {
|
||||
const viewUnits = parseView(parseCode(code), parserConfig);
|
||||
return gV(viewUnits, generatorConfig);
|
||||
}
|
||||
|
||||
function formatCode(code: string) {
|
||||
return generate(
|
||||
parseSync(code, {plugins: [babelJSX]})!
|
||||
)!.code;
|
||||
}
|
||||
|
||||
export function expectView(code: string, expected: string, expectedTemplates?: string[]) {
|
||||
const [templates, viewAst] = generateView(code);
|
||||
const statements = (((viewAst.expression as t.CallExpression)
|
||||
.callee as t.ArrowFunctionExpression)
|
||||
.body as t.BlockStatement)
|
||||
.body.slice(0, -1);
|
||||
const viewCode = generate(
|
||||
t.file(t.program(statements)),
|
||||
)!.code!;
|
||||
|
||||
const expectedCode = formatCode(expected);
|
||||
|
||||
expect(viewCode).toBe(expectedCode);
|
||||
if (expectedTemplates) {
|
||||
const templateCode = templates.map(template => generate(template).code);
|
||||
expectedTemplates = expectedTemplates.map(formatCode);
|
||||
expect(templateCode).toEqual(expectedTemplates.map(formatCode));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function js(strings: TemplateStringsArray, ...values: any[]) {
|
||||
return strings.reduce((acc, cur, i) => {
|
||||
return acc + cur + (values[i] || '');
|
||||
}, '');
|
||||
}
|
|
@ -0,0 +1,302 @@
|
|||
import { describe, it } from 'vitest';
|
||||
import { expectView } from './mock';
|
||||
|
||||
|
||||
describe('Template', () => {
|
||||
it('should generate a Template', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div>
|
||||
<p>ok</p>
|
||||
<h1>fine</h1>
|
||||
</div>
|
||||
`, /*js*/`
|
||||
const $node0 = $template0.cloneNode(true)
|
||||
`, [
|
||||
/*js*/`
|
||||
const $template0 = (() => {
|
||||
const $node0 = createElement("div")
|
||||
const $node1 = createElement("p")
|
||||
$node1.textContent = "ok"
|
||||
insert($node0, $node1)
|
||||
const $node2 = createElement("h1")
|
||||
$node2.textContent = "fine"
|
||||
insert($node0, $node2)
|
||||
return $node0
|
||||
})
|
||||
`
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate a Template with props', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div id="myDiv">
|
||||
<p class="ok">ok</p>
|
||||
<h1>fine</h1>
|
||||
</div>
|
||||
`, /*js*/`
|
||||
const $node0 = $template0.cloneNode(true)
|
||||
`, [
|
||||
/*js*/`
|
||||
const $template0 = (() => {
|
||||
const $node0 = createElement("div")
|
||||
$node0.id = "myDiv"
|
||||
const $node1 = createElement("p")
|
||||
$node1.className = "ok"
|
||||
$node1.textContent = "ok"
|
||||
insert($node0, $node1)
|
||||
const $node2 = createElement("h1")
|
||||
$node2.textContent = "fine"
|
||||
insert($node0, $node2)
|
||||
return $node0
|
||||
})
|
||||
`
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate a Template with dynamic props', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div id={id}>
|
||||
<p class={cls}></p>
|
||||
<h1></h1>
|
||||
</div>
|
||||
`, /*js*/`
|
||||
const $node0 = $template0.cloneNode(true)
|
||||
const $node1 = $node0.firstChild
|
||||
$node0.id = id
|
||||
$node1.className = cls
|
||||
`, [
|
||||
/*js*/`
|
||||
const $template0 = (() => {
|
||||
const $node0 = createElement("div")
|
||||
const $node1 = createElement("p")
|
||||
insert($node0, $node1)
|
||||
const $node2 = createElement("h1")
|
||||
insert($node0, $node2)
|
||||
return $node0
|
||||
})
|
||||
`
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate a Template with reactive props', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div id={id.get()}>
|
||||
<p class={cls.get()}></p>
|
||||
<h1></h1>
|
||||
</div>
|
||||
`, /*js*/`
|
||||
const $node0 = $template0.cloneNode(true)
|
||||
const $node1 = $node0.firstChild
|
||||
watch(() => setProperty($node0, "id", id.get()))
|
||||
watch(() => setProperty($node1, "className", cls.get()))
|
||||
`, [
|
||||
/*js*/`
|
||||
const $template0 = (() => {
|
||||
const $node0 = createElement("div")
|
||||
const $node1 = createElement("p")
|
||||
insert($node0, $node1)
|
||||
const $node2 = createElement("h1")
|
||||
insert($node0, $node2)
|
||||
return $node0
|
||||
})
|
||||
`
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate multiple Template', () => {
|
||||
expectView(/*jsx*/`
|
||||
<>
|
||||
<div>
|
||||
<p>ok</p>
|
||||
<h1>fine</h1>
|
||||
</div>
|
||||
<section>
|
||||
<div>second temp</div>
|
||||
<p>ok</p>
|
||||
<h1>fine</h1>
|
||||
</section>
|
||||
</>
|
||||
`, /*js*/`
|
||||
const $node0 = $template0.cloneNode(true)
|
||||
const $node1 = $template1.cloneNode(true)
|
||||
`, [
|
||||
/*js*/`
|
||||
const $template0 = (() => {
|
||||
const $node0 = createElement("div")
|
||||
const $node1 = createElement("p")
|
||||
$node1.textContent = "ok"
|
||||
insert($node0, $node1)
|
||||
const $node2 = createElement("h1")
|
||||
$node2.textContent = "fine"
|
||||
insert($node0, $node2)
|
||||
return $node0
|
||||
})
|
||||
`,
|
||||
/*js*/`
|
||||
const $template1 = (() => {
|
||||
const $node0 = createElement("section")
|
||||
const $node1 = createElement("div")
|
||||
$node1.textContent = "second temp"
|
||||
insert($node0, $node1)
|
||||
const $node2 = createElement("p")
|
||||
$node2.textContent = "ok"
|
||||
insert($node0, $node2)
|
||||
const $node3 = createElement("h1")
|
||||
$node3.textContent = "fine"
|
||||
insert($node0, $node3)
|
||||
return $node0
|
||||
})
|
||||
`
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate a Template with Components at end', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div>
|
||||
<p/>
|
||||
<Comp />
|
||||
</div>
|
||||
`, /*js*/`
|
||||
const $node0 = $template0.cloneNode(true)
|
||||
const $node1 = createComponent(Comp)
|
||||
insert($node0, $node1)
|
||||
`, [
|
||||
/*js*/`
|
||||
const $template0 = (() => {
|
||||
const $node0 = createElement("div")
|
||||
const $node1 = createElement("p")
|
||||
insert($node0, $node1)
|
||||
return $node0
|
||||
})
|
||||
`
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate a Template with Components in the middle', () => {
|
||||
expectView(/*jsx*/`
|
||||
<div>
|
||||
<p/>
|
||||
<Comp />
|
||||
<section/>
|
||||
</div>
|
||||
`, /*js*/`
|
||||
const $node0 = $template0.cloneNode(true)
|
||||
const $node1 = $node0.firstChild.nextSibling;
|
||||
const $node2 = createComponent(Comp);
|
||||
insert($node0, $node2, $node1);
|
||||
`, [
|
||||
/*js*/`
|
||||
const $template0 = (() => {
|
||||
const $node0 = createElement("div")
|
||||
const $node1 = createElement("p")
|
||||
insert($node0, $node1)
|
||||
const $node2 = createElement("section")
|
||||
insert($node0, $node2)
|
||||
return $node0
|
||||
})
|
||||
`
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate a Template with a very very complex structure', () => {
|
||||
expectView(/*jsx*/`
|
||||
<>
|
||||
<div>
|
||||
<p/>
|
||||
<Comp myProp={prop} reactiveProp={prop.get()}/>
|
||||
<section>
|
||||
<div onClick={() => {console.log(prop.get())}}>second temp</div>
|
||||
<p class={cls}>ok</p>
|
||||
<h1 id={id.get()}>fine</h1>
|
||||
</section>
|
||||
</div>
|
||||
<div>
|
||||
<p class="pp"/>
|
||||
<Comp />
|
||||
<section>
|
||||
<div>second temp</div>
|
||||
<div id={id.get()} class="very deep">
|
||||
<div class="nono">
|
||||
<div>
|
||||
<div>
|
||||
<p>stop here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
`, /*js*/`
|
||||
const $node0 = $template0.cloneNode(true)
|
||||
const $node1 = $node0.firstChild.nextSibling;
|
||||
const $node2 = $node1.firstChild;
|
||||
const $node3 = $node2.nextSibling;
|
||||
const $node4 = $node3.nextSibling;
|
||||
delegateEvent($node2, "click", () => {
|
||||
console.log(prop.get());
|
||||
});
|
||||
$node3.className = cls;
|
||||
watch(() => setProperty($node4, "id", id.get()));
|
||||
const $node5 = createComponent(Comp, {
|
||||
myProp: prop,
|
||||
get reactiveProp() {
|
||||
return prop.get();
|
||||
}
|
||||
});
|
||||
insert($node0, $node5, $node1);
|
||||
const $node6 = $template1.cloneNode(true);
|
||||
const $node7 = $node6.firstChild.nextSibling;
|
||||
const $node8 = $node7.firstChild.nextSibling;
|
||||
watch(() => setProperty($node8, "id", id.get()));
|
||||
const $node9 = createComponent(Comp);
|
||||
insert($node6, $node9, $node7);
|
||||
`, [
|
||||
/*js*/`
|
||||
const $template0 = (() => {
|
||||
const $node0 = createElement("div")
|
||||
const $node1 = createElement("p")
|
||||
insert($node0, $node1)
|
||||
const $node2 = createElement("section")
|
||||
const $node3 = createElement("div")
|
||||
$node3.textContent = "second temp"
|
||||
insert($node2, $node3)
|
||||
const $node4 = createElement("p")
|
||||
$node4.textContent = "ok"
|
||||
insert($node2, $node4)
|
||||
const $node5 = createElement("h1")
|
||||
$node5.textContent = "fine"
|
||||
insert($node2, $node5)
|
||||
insert($node0, $node2)
|
||||
return $node0
|
||||
})
|
||||
`, /*js*/`
|
||||
const $template1 = (() => {
|
||||
const $node0 = createElement("div")
|
||||
const $node1 = createElement("p")
|
||||
$node1.className = "pp"
|
||||
insert($node0, $node1)
|
||||
const $node2 = createElement("section");
|
||||
const $node3 = createElement("div");
|
||||
$node3.textContent = "second temp";
|
||||
insert($node2, $node3);
|
||||
const $node4 = createElement("div");
|
||||
$node4.className = "very deep";
|
||||
const $node5 = createElement("div");
|
||||
$node5.className = "nono";
|
||||
const $node6 = createElement("div");
|
||||
const $node7 = createElement("div");
|
||||
const $node8 = createElement("p");
|
||||
$node8.textContent = "stop here";
|
||||
insert($node7, $node8);
|
||||
insert($node6, $node7);
|
||||
insert($node5, $node6);
|
||||
insert($node4, $node5);
|
||||
insert($node2, $node4);
|
||||
insert($node0, $node2);
|
||||
return $node0
|
||||
})
|
||||
`
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import { describe, it } from 'vitest';
|
||||
import { expectView } from './mock';
|
||||
|
||||
|
||||
describe('Text', () => {
|
||||
it('should generate a text node', () => {
|
||||
expectView(/*jsx*/`
|
||||
<>hello world</>
|
||||
`, /*js*/`
|
||||
const $node0 = createText("hello world")
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a text node with a boolean expression', () => {
|
||||
expectView(/*jsx*/`
|
||||
<>{true}</>
|
||||
`, /*js*/`
|
||||
const $node0 = createText(true)
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a text node with a number expression', () => {
|
||||
expectView(/*jsx*/`
|
||||
<>{1}</>
|
||||
`, /*js*/`
|
||||
const $node0 = createText(1)
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a text node with a null expression', () => {
|
||||
expectView(/*jsx*/`
|
||||
<>{null}</>
|
||||
`, /*js*/`
|
||||
const $node0 = createText(null)
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a text node with a string literal expression', () => {
|
||||
expectView(/*jsx*/`
|
||||
<>{"hello world"}</>
|
||||
`, /*js*/`
|
||||
const $node0 = createText("hello world")
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import type Babel from '@babel/core';
|
||||
|
||||
export interface ViewGeneratorConfig {
|
||||
babelApi: typeof Babel;
|
||||
importMap: Record<string, string>;
|
||||
|
||||
/**
|
||||
* @brief Using AttributeMap to identify propertyfied attributes
|
||||
* Reason for adding this:
|
||||
* `el.prop = xxx` is faster than `el.setAttribute('prop', xxx)`
|
||||
* @example { href: ["a", "area", "base", "link"], id: ["*"] }
|
||||
*/
|
||||
attributeMap?: Record<string, string[]>
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,73 @@
|
|||
import Babel, { types } from '@babel/core';
|
||||
|
||||
interface UnitProp {
|
||||
value: types.Expression;
|
||||
viewPropMap: Record<string, ViewUnit[]>;
|
||||
specifier?: string;
|
||||
}
|
||||
interface TextUnit {
|
||||
type: 'text';
|
||||
content: types.Literal;
|
||||
}
|
||||
type MutableUnit = ViewUnit & {
|
||||
path: number[];
|
||||
};
|
||||
interface TemplateProp {
|
||||
tag: types.Expression;
|
||||
name: string;
|
||||
key: string;
|
||||
path: number[];
|
||||
value: types.Expression;
|
||||
}
|
||||
interface TemplateUnit {
|
||||
type: 'template';
|
||||
template: HTMLUnit;
|
||||
mutableUnits: MutableUnit[];
|
||||
props: TemplateProp[];
|
||||
}
|
||||
interface HTMLUnit {
|
||||
type: 'html';
|
||||
tag: types.Expression;
|
||||
props: Record<string, UnitProp>;
|
||||
children: ViewUnit[];
|
||||
}
|
||||
interface CompUnit {
|
||||
type: 'comp';
|
||||
tag: types.Expression;
|
||||
props: Record<string, UnitProp>;
|
||||
children: ViewUnit[];
|
||||
}
|
||||
interface IfBranch {
|
||||
condition: types.Expression;
|
||||
children: ViewUnit[];
|
||||
}
|
||||
interface IfUnit {
|
||||
type: 'if';
|
||||
branches: IfBranch[];
|
||||
}
|
||||
interface ExpUnit {
|
||||
type: 'exp';
|
||||
content: UnitProp;
|
||||
}
|
||||
interface EnvUnit {
|
||||
type: 'env';
|
||||
props: Record<string, UnitProp>;
|
||||
children: ViewUnit[];
|
||||
}
|
||||
type ViewUnit = TextUnit | HTMLUnit | CompUnit | IfUnit | ExpUnit | EnvUnit | TemplateUnit;
|
||||
interface ViewParserConfig {
|
||||
babelApi: typeof Babel;
|
||||
htmlTags: string[];
|
||||
parseTemplate?: boolean;
|
||||
}
|
||||
type AllowedJSXNode = types.JSXElement | types.JSXFragment | types.JSXText | types.JSXExpressionContainer | types.JSXSpreadChild;
|
||||
|
||||
/**
|
||||
* @brief Generate view units from a babel ast
|
||||
* @param statement
|
||||
* @param config
|
||||
* @returns ViewUnit[]
|
||||
*/
|
||||
declare function parseView(node: AllowedJSXNode, config: ViewParserConfig): ViewUnit[];
|
||||
|
||||
export { AllowedJSXNode, CompUnit, EnvUnit, ExpUnit, HTMLUnit, IfBranch, IfUnit, MutableUnit, TemplateProp, TemplateUnit, TextUnit, UnitProp, ViewParserConfig, ViewUnit, parseView };
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@inula/view-parser",
|
||||
"name": "@inula/jsx-view-parser",
|
||||
"version": "0.0.0",
|
||||
"description": "Inula jsx parser",
|
||||
"author": {
|
|
@ -11,7 +11,7 @@ export function parseView(
|
|||
node: AllowedJSXNode,
|
||||
config: ViewParserConfig
|
||||
): ViewUnit[] {
|
||||
return new ViewParser(config).parse(node);
|
||||
return new ViewParser(config).parse(node);
|
||||
}
|
||||
|
||||
export type * from './types';
|
|
@ -184,8 +184,24 @@ export class ViewParser {
|
|||
const childUnits = node.children.map(child => this.parseView(child)).flat();
|
||||
|
||||
let unit: ViewUnit = { type, tag, props: propMap, children: childUnits };
|
||||
if (unit.type === 'html' && this.willParseTemplate)
|
||||
unit = this.transformTemplate(unit);
|
||||
|
||||
if (unit.type === 'html' && childUnits.length === 1 && childUnits[0].type === 'text') {
|
||||
// ---- If the html unit only has one text child, merge the text into the html unit
|
||||
const text = childUnits[0] as TextUnit;
|
||||
unit = {
|
||||
...unit,
|
||||
children: [],
|
||||
props: {
|
||||
...unit.props,
|
||||
textContent: {
|
||||
value: text.content,
|
||||
viewPropMap: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (unit.type === 'html') unit = this.transformTemplate(unit);
|
||||
|
||||
this.viewUnits.push(unit);
|
||||
}
|
||||
|
@ -320,7 +336,8 @@ export class ViewParser {
|
|||
};
|
||||
}
|
||||
|
||||
private transformTemplate(unit: ViewUnit): ViewUnit {
|
||||
transformTemplate(unit: ViewUnit): ViewUnit {
|
||||
if (!this.willParseTemplate) return unit;
|
||||
if (!this.isHTMLTemplate(unit)) return unit;
|
||||
unit = unit as HTMLUnit;
|
||||
return {
|
||||
|
@ -347,7 +364,7 @@ export class ViewParser {
|
|||
)
|
||||
));
|
||||
|
||||
let children: (HTMLUnit | TextUnit)[] | undefined;
|
||||
let children: (HTMLUnit | TextUnit)[] = [];
|
||||
if (unit.children) {
|
||||
children = unit.children
|
||||
.map(unit => {
|
||||
|
@ -375,17 +392,26 @@ export class ViewParser {
|
|||
*/
|
||||
private generateMutableUnits(htmlUnit: HTMLUnit): MutableUnit[] {
|
||||
const mutableUnits: MutableUnit[] = [];
|
||||
|
||||
const generateMutableUnit = (unit: HTMLUnit, path: number[] = []) => {
|
||||
const maxHtmlIdx = unit.children?.filter(
|
||||
child => (child.type === 'html' && this.t.isStringLiteral(child.tag)) ||
|
||||
child.type === 'text'
|
||||
).length;
|
||||
let htmlIdx = -1;
|
||||
// ---- Generate mutable unit for current HTMLUnit
|
||||
unit.children?.forEach((child, idx) => {
|
||||
unit.children?.forEach((child) => {
|
||||
if (
|
||||
!(child.type === 'html' && this.t.isStringLiteral(child.tag)) &&
|
||||
!(child.type === 'text')
|
||||
) {
|
||||
const idx = htmlIdx + 1 >= maxHtmlIdx ? -1 : htmlIdx + 1;
|
||||
mutableUnits.push({
|
||||
path: [...path, idx],
|
||||
...this.transformTemplate(child),
|
||||
});
|
||||
} else {
|
||||
htmlIdx++;
|
||||
}
|
||||
});
|
||||
// ---- Recursively generate mutable units for static HTMLUnit children
|
||||
|
@ -418,6 +444,7 @@ export class ViewParser {
|
|||
.filter(([, prop]) => !this.isStaticProp(prop))
|
||||
.forEach(([key, prop]) => {
|
||||
templateProps.push({
|
||||
tag: unit.tag,
|
||||
name: (unit.tag as t.StringLiteral).value,
|
||||
key,
|
||||
path,
|
||||
|
@ -426,9 +453,9 @@ export class ViewParser {
|
|||
});
|
||||
// ---- Recursively generate props for static HTMLUnit children
|
||||
unit.children
|
||||
?.forEach((child, idx) => {
|
||||
if (child.type !== 'html') return;
|
||||
generateVariableProp(child, [...path, idx]);
|
||||
?.filter(child => child.type === 'html' && this.t.isStringLiteral(child.tag))
|
||||
.forEach((child, idx) => {
|
||||
generateVariableProp(child as HTMLUnit, [...path, idx]);
|
||||
});
|
||||
};
|
||||
generateVariableProp(htmlUnit, []);
|
||||
|
@ -489,7 +516,7 @@ export class ViewParser {
|
|||
* @returns ViewUnit[]
|
||||
*/
|
||||
private parseView(node: AllowedJSXNode): ViewUnit[] {
|
||||
return new ViewParser(this.config).parse(node);
|
||||
return new ViewParser({...this.config, parseTemplate:false}).parse(node);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
declare module "@babel/plugin-syntax-jsx"
|
|
@ -12,9 +12,10 @@ export interface TextUnit {
|
|||
content: t.Literal
|
||||
}
|
||||
|
||||
export type MutableUnit = ViewUnit & { path: number[] }
|
||||
export type MutableUnit = ViewUnit & { path: number[]}
|
||||
|
||||
export interface TemplateProp {
|
||||
tag: t.Expression
|
||||
name: string
|
||||
key: string
|
||||
path: number[]
|
||||
|
@ -31,17 +32,15 @@ export interface TemplateUnit {
|
|||
export interface HTMLUnit {
|
||||
type: 'html'
|
||||
tag: t.Expression
|
||||
content?: UnitProp
|
||||
props?: Record<string, UnitProp>
|
||||
children?: ViewUnit[]
|
||||
props: Record<string, UnitProp>
|
||||
children: ViewUnit[]
|
||||
}
|
||||
|
||||
export interface CompUnit {
|
||||
type: 'comp'
|
||||
tag: t.Expression
|
||||
content?: UnitProp
|
||||
props?: Record<string, UnitProp>
|
||||
children?: ViewUnit[]
|
||||
props: Record<string, UnitProp>
|
||||
children: ViewUnit[]
|
||||
}
|
||||
|
||||
export interface IfBranch {
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue