parent
07e2ce09b4
commit
676fdf1ef6
|
@ -22,7 +22,9 @@
|
||||||
"test": "vitest --ui"
|
"test": "vitest --ui"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/generator": "^7.23.6",
|
||||||
"@types/babel__core": "^7.20.5",
|
"@types/babel__core": "^7.20.5",
|
||||||
|
"@types/babel__generator": "^7.6.8",
|
||||||
"@types/node": "^20.10.5",
|
"@types/node": "^20.10.5",
|
||||||
"@vitest/ui": "^1.2.1",
|
"@vitest/ui": "^1.2.1",
|
||||||
"tsup": "^6.7.0",
|
"tsup": "^6.7.0",
|
||||||
|
@ -31,6 +33,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
"@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",
|
"babel-plugin-syntax-typescript-new": "^1.0.0",
|
||||||
"minimatch": "^9.0.3"
|
"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 { InulaOption } from './types';
|
||||||
import type { ConfigAPI, TransformOptions } from '@babel/core';
|
import type { ConfigAPI, TransformOptions } from '@babel/core';
|
||||||
import inula from './plugin';
|
import inula from './plugin';
|
||||||
|
@ -11,8 +9,8 @@ export default function (
|
||||||
): TransformOptions {
|
): TransformOptions {
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
[syntaxJSX.default ?? syntaxJSX],
|
['@babel/plugin-syntax-jsx'],
|
||||||
[syntaxTypescript, {isJsx: true}],
|
['@babel/plugin-syntax-typescript', { isTSX: true }],
|
||||||
[inula, options],
|
[inula, options],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,13 +10,19 @@ export default function (api: typeof babel, options: InulaOption): PluginObj {
|
||||||
name: 'babel-plugin-inula-jsx',
|
name: 'babel-plugin-inula-jsx',
|
||||||
visitor: {
|
visitor: {
|
||||||
Program: {
|
Program: {
|
||||||
enter(path) {
|
enter(path, { filename }) {
|
||||||
console.log('babel-plugin-inula-jsx: Program enter');
|
pluginProvider.programEnterVisitor(path, filename);
|
||||||
},
|
},
|
||||||
exit(path) {
|
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 type babel from '@babel/core';
|
||||||
import { minimatch } from 'minimatch';
|
import { minimatch } from 'minimatch';
|
||||||
import { InulaOption } from './types';
|
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 {
|
export class PluginProvider {
|
||||||
|
@ -14,6 +16,12 @@ export class PluginProvider {
|
||||||
private readonly includes: string[]
|
private readonly includes: string[]
|
||||||
private readonly excludes: 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) {
|
constructor(babelApi: typeof babel, options: InulaOption) {
|
||||||
this.babelApi = babelApi;
|
this.babelApi = babelApi;
|
||||||
this.t = babelApi.types;
|
this.t = babelApi.types;
|
||||||
|
@ -22,6 +30,10 @@ export class PluginProvider {
|
||||||
const excludes = options.excludeFiles ?? ['**/{dist,node_modules,lib}'];
|
const excludes = options.excludeFiles ?? ['**/{dist,node_modules,lib}'];
|
||||||
this.includes = Array.isArray(includes) ? includes : [includes];
|
this.includes = Array.isArray(includes) ? includes : [includes];
|
||||||
this.excludes = Array.isArray(excludes) ? excludes : [excludes];
|
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:
|
// ---- Two levels of enter:
|
||||||
|
@ -30,13 +42,9 @@ export class PluginProvider {
|
||||||
private fileEnter = true
|
private fileEnter = true
|
||||||
|
|
||||||
// ---- File Level
|
// ---- File Level
|
||||||
private programNode?: t.Program
|
|
||||||
private allImports: t.ImportDeclaration[] = []
|
private allImports: t.ImportDeclaration[] = []
|
||||||
private didAlterImports = false
|
|
||||||
private transformedCount = 0
|
|
||||||
|
|
||||||
// ---- Component Level ----
|
|
||||||
|
|
||||||
|
private templateIdx = -1
|
||||||
|
|
||||||
programEnterVisitor(
|
programEnterVisitor(
|
||||||
path: NodePath<t.Program>,
|
path: NodePath<t.Program>,
|
||||||
|
@ -56,17 +64,34 @@ export class PluginProvider {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.programNode = path.node;
|
this.programNode = path.node;
|
||||||
this.transformedCount = 0;
|
this.templateIdx = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
programExitVisitor(path: NodePath<t.Program>): void {
|
programExitVisitor(path: NodePath<t.Program>): void {
|
||||||
if (!this.fileEnter) return;
|
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 false;
|
||||||
return true;
|
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 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) {
|
export function transformInula(code: string) {
|
||||||
return transform(code, {
|
return transform(code, {
|
||||||
presets: [inula]
|
presets: [[inula, {'files': '*'}]]
|
||||||
})?.code;
|
})?.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}
|
* @default ** /{dist,node_modules,lib}/*.{js,ts}
|
||||||
*/
|
*/
|
||||||
excludeFiles?: string | string[]
|
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",
|
"version": "0.0.0",
|
||||||
"description": "Inula jsx parser",
|
"description": "Inula jsx parser",
|
||||||
"author": {
|
"author": {
|
|
@ -184,8 +184,24 @@ export class ViewParser {
|
||||||
const childUnits = node.children.map(child => this.parseView(child)).flat();
|
const childUnits = node.children.map(child => this.parseView(child)).flat();
|
||||||
|
|
||||||
let unit: ViewUnit = { type, tag, props: propMap, children: childUnits };
|
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);
|
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;
|
if (!this.isHTMLTemplate(unit)) return unit;
|
||||||
unit = unit as HTMLUnit;
|
unit = unit as HTMLUnit;
|
||||||
return {
|
return {
|
||||||
|
@ -347,7 +364,7 @@ export class ViewParser {
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
|
|
||||||
let children: (HTMLUnit | TextUnit)[] | undefined;
|
let children: (HTMLUnit | TextUnit)[] = [];
|
||||||
if (unit.children) {
|
if (unit.children) {
|
||||||
children = unit.children
|
children = unit.children
|
||||||
.map(unit => {
|
.map(unit => {
|
||||||
|
@ -375,17 +392,26 @@ export class ViewParser {
|
||||||
*/
|
*/
|
||||||
private generateMutableUnits(htmlUnit: HTMLUnit): MutableUnit[] {
|
private generateMutableUnits(htmlUnit: HTMLUnit): MutableUnit[] {
|
||||||
const mutableUnits: MutableUnit[] = [];
|
const mutableUnits: MutableUnit[] = [];
|
||||||
|
|
||||||
const generateMutableUnit = (unit: HTMLUnit, path: number[] = []) => {
|
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
|
// ---- Generate mutable unit for current HTMLUnit
|
||||||
unit.children?.forEach((child, idx) => {
|
unit.children?.forEach((child) => {
|
||||||
if (
|
if (
|
||||||
!(child.type === 'html' && this.t.isStringLiteral(child.tag)) &&
|
!(child.type === 'html' && this.t.isStringLiteral(child.tag)) &&
|
||||||
!(child.type === 'text')
|
!(child.type === 'text')
|
||||||
) {
|
) {
|
||||||
|
const idx = htmlIdx + 1 >= maxHtmlIdx ? -1 : htmlIdx + 1;
|
||||||
mutableUnits.push({
|
mutableUnits.push({
|
||||||
path: [...path, idx],
|
path: [...path, idx],
|
||||||
...this.transformTemplate(child),
|
...this.transformTemplate(child),
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
htmlIdx++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// ---- Recursively generate mutable units for static HTMLUnit children
|
// ---- Recursively generate mutable units for static HTMLUnit children
|
||||||
|
@ -418,6 +444,7 @@ export class ViewParser {
|
||||||
.filter(([, prop]) => !this.isStaticProp(prop))
|
.filter(([, prop]) => !this.isStaticProp(prop))
|
||||||
.forEach(([key, prop]) => {
|
.forEach(([key, prop]) => {
|
||||||
templateProps.push({
|
templateProps.push({
|
||||||
|
tag: unit.tag,
|
||||||
name: (unit.tag as t.StringLiteral).value,
|
name: (unit.tag as t.StringLiteral).value,
|
||||||
key,
|
key,
|
||||||
path,
|
path,
|
||||||
|
@ -426,9 +453,9 @@ export class ViewParser {
|
||||||
});
|
});
|
||||||
// ---- Recursively generate props for static HTMLUnit children
|
// ---- Recursively generate props for static HTMLUnit children
|
||||||
unit.children
|
unit.children
|
||||||
?.forEach((child, idx) => {
|
?.filter(child => child.type === 'html' && this.t.isStringLiteral(child.tag))
|
||||||
if (child.type !== 'html') return;
|
.forEach((child, idx) => {
|
||||||
generateVariableProp(child, [...path, idx]);
|
generateVariableProp(child as HTMLUnit, [...path, idx]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
generateVariableProp(htmlUnit, []);
|
generateVariableProp(htmlUnit, []);
|
||||||
|
@ -489,7 +516,7 @@ export class ViewParser {
|
||||||
* @returns ViewUnit[]
|
* @returns ViewUnit[]
|
||||||
*/
|
*/
|
||||||
private parseView(node: AllowedJSXNode): 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"
|
|
@ -15,6 +15,7 @@ export interface TextUnit {
|
||||||
export type MutableUnit = ViewUnit & { path: number[]}
|
export type MutableUnit = ViewUnit & { path: number[]}
|
||||||
|
|
||||||
export interface TemplateProp {
|
export interface TemplateProp {
|
||||||
|
tag: t.Expression
|
||||||
name: string
|
name: string
|
||||||
key: string
|
key: string
|
||||||
path: number[]
|
path: number[]
|
||||||
|
@ -31,17 +32,15 @@ export interface TemplateUnit {
|
||||||
export interface HTMLUnit {
|
export interface HTMLUnit {
|
||||||
type: 'html'
|
type: 'html'
|
||||||
tag: t.Expression
|
tag: t.Expression
|
||||||
content?: UnitProp
|
props: Record<string, UnitProp>
|
||||||
props?: Record<string, UnitProp>
|
children: ViewUnit[]
|
||||||
children?: ViewUnit[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompUnit {
|
export interface CompUnit {
|
||||||
type: 'comp'
|
type: 'comp'
|
||||||
tag: t.Expression
|
tag: t.Expression
|
||||||
content?: UnitProp
|
props: Record<string, UnitProp>
|
||||||
props?: Record<string, UnitProp>
|
children: ViewUnit[]
|
||||||
children?: ViewUnit[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IfBranch {
|
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