!168 inula api2.0
* feat: inula-next init * feat: v2 init * feat(class-transform): add watch decorator * feat(class-transform): update docs * feat(class-transform): init
This commit is contained in:
parent
eb5cb8237b
commit
83c80341dc
|
@ -1,10 +1,10 @@
|
|||
/node_modules
|
||||
node_modules
|
||||
.idea
|
||||
.vscode
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
/packages/**/node_modules
|
||||
/packages/inula-cli/lib
|
||||
build
|
||||
/packages/inula-router/connectRouter
|
||||
/packages/inula-router/router
|
||||
dist
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Dlight.JS</title>
|
||||
<link rel="stylesheet" href="/src/App.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script type="module" src="/src/App.view.tsx"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "dev",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/standalone": "^7.22.4",
|
||||
"@inula/next": "workspace:*",
|
||||
"@iandx/easy-css": "^0.10.14",
|
||||
"babel-preset-inula-next": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-inula-next": "workspace:*"
|
||||
},
|
||||
"keywords": [
|
||||
"dlight.js"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.ok {
|
||||
color: var(--color-ok);
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// @ts-nocheck
|
||||
import {
|
||||
Children,
|
||||
Content,
|
||||
Main,
|
||||
Model,
|
||||
Prop,
|
||||
View,
|
||||
Watch,
|
||||
button,
|
||||
div,
|
||||
input,
|
||||
insertChildren,
|
||||
use,
|
||||
render,
|
||||
} from '@inula/next';
|
||||
|
||||
// @ts-ignore
|
||||
function Button({ children, onClick }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
style={{
|
||||
color: 'white',
|
||||
backgroundColor: 'green',
|
||||
border: 'none',
|
||||
padding: '5px 10px',
|
||||
marginRight: '10px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ArrayModification() {
|
||||
let arr = [];
|
||||
willMount(() => {});
|
||||
return (
|
||||
<section>
|
||||
<h1>ArrayModification</h1>
|
||||
{arr.join(',')}
|
||||
<button onClick={() => arr.push(arr.length)}>Add item</button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MyComp() {
|
||||
let count = 0;
|
||||
let db = count * 2;
|
||||
return (
|
||||
<>
|
||||
<h1 className="123">Hello dlight fn comp</h1>
|
||||
<section>
|
||||
count: {count}, double is: {db}
|
||||
<button onClick={() => (count += 1)}>Add</button>
|
||||
</section>
|
||||
<Button onClick={() => alert(count)}>Alter count</Button>
|
||||
<ConditionalRendering count={count} />
|
||||
<ArrayModification />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConditionalRendering({ count }) {
|
||||
return (
|
||||
<section>
|
||||
<h1>Condition</h1>
|
||||
<if cond={count > 1}>{count} is bigger than is 1</if>
|
||||
<else-if cond={count === 1}>{count} is equal to 1</else-if>
|
||||
<else>{count} is smaller than 1</else>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
render('main', MyComp);
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"noUnusedParameters": true,
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import inula from 'vite-plugin-inula-next';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 4320,
|
||||
},
|
||||
base: '',
|
||||
optimizeDeps: {
|
||||
disabled: true,
|
||||
},
|
||||
plugins: [inula({ files: '**/*.{view,model}.{ts,js,tsx,jsx}', enableDevTools: true })],
|
||||
});
|
|
@ -13,6 +13,7 @@
|
|||
"build:inula-intl": "pnpm -F inula-intl build",
|
||||
"build:inula-request": "pnpm -F inula-request build",
|
||||
"build:inula-router": "pnpm -F inula-router build",
|
||||
"build:transpiler": "pnpm --filter './packages/transpiler/*' run build",
|
||||
"commitlint": "commitlint --config commitlint.config.js -e",
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
|
|
|
@ -54,7 +54,7 @@ inula-cli的推荐目录结构如下:
|
|||
│ └── inula-cli
|
||||
│ ├── lib
|
||||
├── mock // mock目录
|
||||
│ └── mock.ts
|
||||
│ └── transform.ts
|
||||
├── src // 项目源码目录
|
||||
│ ├── pages
|
||||
│ │ ├── index.less
|
||||
|
|
|
@ -14,4 +14,3 @@
|
|||
*/
|
||||
|
||||
declare module 'crequire';
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ export default (api: API) => {
|
|||
api.applyHook({ name: 'afterStartDevServer' });
|
||||
});
|
||||
} else {
|
||||
api.logger.error('Can\'t find config');
|
||||
api.logger.error("Can't find config");
|
||||
}
|
||||
break;
|
||||
case 'vite':
|
||||
|
@ -70,7 +70,7 @@ export default (api: API) => {
|
|||
server.printUrls();
|
||||
});
|
||||
} else {
|
||||
api.logger.error('Can\'t find config');
|
||||
api.logger.error("Can't find config");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -33,7 +33,7 @@ export default (api: API) => {
|
|||
args._.shift();
|
||||
}
|
||||
if (args._.length === 0) {
|
||||
api.logger.warn('Can\'t find any generate options.');
|
||||
api.logger.warn("Can't find any generate options.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,7 @@
|
|||
const { preset } = require('./jest.config');
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{ targets: { node: 'current' } },
|
||||
],
|
||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||
['@babel/preset-typescript'],
|
||||
[
|
||||
'@babel/preset-react',
|
||||
|
|
|
@ -40,7 +40,7 @@ export default {
|
|||
{
|
||||
file: path.resolve(output, 'intl.esm-browser.js'),
|
||||
format: 'esm',
|
||||
}
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
|
|
|
@ -14,11 +14,11 @@
|
|||
*/
|
||||
|
||||
const body: Record<string, any> = {
|
||||
doubleapos: { match: '\'\'', value: () => '\'' },
|
||||
doubleapos: { match: "''", value: () => "'" },
|
||||
quoted: {
|
||||
lineBreaks: true,
|
||||
match: /'[{}#](?:[^]*?[^'])?'(?!')/u,
|
||||
value: src => src.slice(1, -1).replace(/''/g, '\''),
|
||||
value: src => src.slice(1, -1).replace(/''/g, "'"),
|
||||
},
|
||||
argument: {
|
||||
lineBreaks: true,
|
||||
|
|
|
@ -90,19 +90,19 @@ describe('I18n', () => {
|
|||
});
|
||||
it('._ allow escaping syntax characters', () => {
|
||||
const messages = {
|
||||
'My \'\'name\'\' is \'{name}\'': 'Mi \'\'nombre\'\' es \'{name}\'',
|
||||
"My ''name'' is '{name}'": "Mi ''nombre'' es '{name}'",
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'es',
|
||||
messages: { es: messages },
|
||||
});
|
||||
expect(i18n.formatMessage('My \'\'name\'\' is \'{name}\'')).toEqual('Mi \'nombre\' es {name}');
|
||||
expect(i18n.formatMessage("My ''name'' is '{name}'")).toEqual("Mi 'nombre' es {name}");
|
||||
});
|
||||
|
||||
it('._ should format message from catalog', function () {
|
||||
const messages = {
|
||||
Hello: 'Salut',
|
||||
id: 'Je m\'appelle {name}',
|
||||
id: "Je m'appelle {name}",
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'fr',
|
||||
|
@ -110,7 +110,7 @@ describe('I18n', () => {
|
|||
});
|
||||
expect(i18n.locale).toEqual('fr');
|
||||
expect(i18n.formatMessage('Hello')).toEqual('Salut');
|
||||
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual('Je m\'appelle Fred');
|
||||
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual("Je m'appelle Fred");
|
||||
});
|
||||
|
||||
it('should return the formatted date and time', () => {
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('eventEmitter', () => {
|
|||
expect(listener).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing when even doesn\'t exist', () => {
|
||||
it("should do nothing when even doesn't exist", () => {
|
||||
const unknown = jest.fn();
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
|
|
@ -14,11 +14,5 @@
|
|||
*/
|
||||
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{ targets: { node: 'current' }},
|
||||
],
|
||||
['@babel/preset-typescript'],
|
||||
],
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-typescript']],
|
||||
};
|
||||
|
|
|
@ -21,16 +21,19 @@ import { babel } from '@rollup/plugin-babel';
|
|||
|
||||
export default {
|
||||
input: './index.ts',
|
||||
output: [{
|
||||
output: [
|
||||
{
|
||||
file: 'dist/inulaRequest.js',
|
||||
format: 'umd',
|
||||
exports: 'named',
|
||||
name: 'inulaRequest',
|
||||
sourcemap: false,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
file: 'dist/inulaRequest.esm-browser.js',
|
||||
format: 'esm',
|
||||
}],
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
|
|
|
@ -38,7 +38,7 @@ describe('path lexer Test', () => {
|
|||
expect(tokens).toStrictEqual([{ type: 'delimiter', value: '/' }]);
|
||||
});
|
||||
|
||||
it('don\'t start with a slash', () => {
|
||||
it("don't start with a slash", () => {
|
||||
const func = () => lexer('abc.com');
|
||||
expect(func).toThrow(Error('Url must start with "/".'));
|
||||
});
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "@inula/store",
|
||||
"version": "0.0.0",
|
||||
"description": "DLight shared store",
|
||||
"author": {
|
||||
"name": "IanDx",
|
||||
"email": "iandxssxx@gmail.com"
|
||||
},
|
||||
"keywords": [
|
||||
"dlight.js"
|
||||
],
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup --sourcemap"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.5.0",
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"format": [
|
||||
"esm"
|
||||
],
|
||||
"clean": true,
|
||||
"dts": true,
|
||||
"minify": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export const Store = {};
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# DLight Main Package
|
||||
See the website's documentations for usage.
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "@inula/next",
|
||||
"version": "1.0.0-next.9",
|
||||
"author": {
|
||||
"name": "IanDx",
|
||||
"email": "iandxssxx@gmail.com"
|
||||
},
|
||||
"keywords": [
|
||||
"dlight.js"
|
||||
],
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup --sourcemap && cp src/index.d.ts dist/ && cp -r src/types dist/"
|
||||
},
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.3",
|
||||
"@inula/store": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.5.0"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/index.js"
|
||||
],
|
||||
"format": [
|
||||
"cjs",
|
||||
"esm"
|
||||
],
|
||||
"clean": true,
|
||||
"minify": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,367 @@
|
|||
import { DLNode, DLNodeType } from './DLNode';
|
||||
import { forwardHTMLProp } from './HTMLNode';
|
||||
import { DLStore, cached } from './store';
|
||||
|
||||
export class CompNode extends DLNode {
|
||||
/**
|
||||
* @brief Constructor, Comp type
|
||||
* @internal
|
||||
* * key - private property key
|
||||
* * $$key - dependency number, e.g. 0b1, 0b10, 0b100
|
||||
* * $s$key - set of properties that depend on this property
|
||||
* * $p$key - exist if this property is a prop
|
||||
* * $e$key - exist if this property is an env
|
||||
* * $en$key - exist if this property is an env, and it's the innermost env that contains this env
|
||||
* * $w$key - exist if this property is a watcher
|
||||
* * $f$key - a function that returns the value of this property, called when the property's dependencies change
|
||||
* * _$children - children nodes of type PropView
|
||||
* * _$contentKey - the key key of the content prop
|
||||
* * _$forwardProps - exist if this node is forwarding props
|
||||
* * _$forwardPropsId - the keys of the props that this node is forwarding, collected in _$setForwardProp
|
||||
* * _$forwardPropsSet - contain all the nodes that are forwarding props to this node, collected with _$addForwardProps
|
||||
*/
|
||||
constructor() {
|
||||
super(DLNodeType.Comp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Init function, called explicitly in the subclass's constructor
|
||||
* @param props - Object containing properties
|
||||
* @param content - Content to be used
|
||||
* @param children - Child nodes
|
||||
* @param forwardPropsScope - Scope for forwarding properties
|
||||
*/
|
||||
_$init(props, content, children, forwardPropsScope) {
|
||||
this._$notInitd = true;
|
||||
|
||||
// ---- Forward props first to allow internal props to override forwarded props
|
||||
if (forwardPropsScope) forwardPropsScope._$addForwardProps(this);
|
||||
if (content) this._$setContent(() => content[0], content[1]);
|
||||
if (props)
|
||||
props.forEach(([key, value, deps]) => {
|
||||
if (key === 'props') return this._$setProps(() => value, deps);
|
||||
this._$setProp(key, () => value, deps);
|
||||
});
|
||||
if (children) this._$children = children;
|
||||
|
||||
// ---- Add envs
|
||||
DLStore.global.DLEnvStore &&
|
||||
Object.entries(DLStore.global.DLEnvStore.envs).forEach(([key, [value, envNode]]) => {
|
||||
if (key === '_$catchable') {
|
||||
this._$catchable = value;
|
||||
return;
|
||||
}
|
||||
if (!(`$e$${key}` in this)) return;
|
||||
envNode.addNode(this);
|
||||
this._$initEnv(key, value, envNode);
|
||||
});
|
||||
|
||||
const willCall = () => {
|
||||
this._$callUpdatesBeforeInit();
|
||||
this.didMount && DLNode.addDidMount(this, this.didMount.bind(this));
|
||||
this.willUnmount && DLNode.addWillUnmount(this, this.willUnmount.bind(this));
|
||||
DLNode.addDidUnmount(this, this._$setUnmounted.bind(this));
|
||||
this.didUnmount && DLNode.addDidUnmount(this, this.didUnmount.bind(this));
|
||||
this.willMount?.();
|
||||
this._$nodes = this.Body?.() ?? [];
|
||||
};
|
||||
|
||||
if (this._$catchable) {
|
||||
this._$catchable(willCall)();
|
||||
if (this._$update) this._$update = this._$catchable(this._$update.bind(this));
|
||||
this._$updateDerived = this._$catchable(this._$updateDerived.bind(this));
|
||||
delete this._$catchable;
|
||||
} else {
|
||||
willCall();
|
||||
}
|
||||
}
|
||||
|
||||
_$setUnmounted() {
|
||||
this._$unmounted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Call updates manually before the node is mounted
|
||||
*/
|
||||
_$callUpdatesBeforeInit() {
|
||||
const protoProps = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
|
||||
const ownProps = Object.getOwnPropertyNames(this);
|
||||
const allProps = [...protoProps, ...ownProps];
|
||||
allProps.forEach(key => {
|
||||
// ---- Run watcher
|
||||
if (key.startsWith('$w$')) return this[key.slice(3)]();
|
||||
// ---- Run model update
|
||||
if (key.startsWith('$md$')) {
|
||||
const realKey = key.slice(4);
|
||||
this[realKey] = this[realKey]();
|
||||
return;
|
||||
}
|
||||
// ---- Run derived value
|
||||
if (key.startsWith('$f$')) {
|
||||
const realKey = key.slice(3);
|
||||
this[realKey] = this[key];
|
||||
this._$updateDerived(realKey);
|
||||
}
|
||||
});
|
||||
delete this._$notInitd;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set all the props to forward
|
||||
* @param key
|
||||
* @param value
|
||||
* @param deps
|
||||
*/
|
||||
_$setPropToForward(key, value, deps) {
|
||||
this._$forwardPropsSet.forEach(node => {
|
||||
const isContent = key === '_$content';
|
||||
if (node._$dlNodeType === DLNodeType.Comp) {
|
||||
if (isContent) node._$setContent(() => value, deps);
|
||||
else node._$setProp(key, () => value, deps);
|
||||
return;
|
||||
}
|
||||
if (node instanceof HTMLElement) {
|
||||
if (isContent) key = 'textContent';
|
||||
forwardHTMLProp(node, key, () => value, deps);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Define forward props
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
_$setForwardProp(key, valueFunc, deps) {
|
||||
const notInitd = '_$notInitd' in this;
|
||||
if (!notInitd && this._$cache(key, deps)) return;
|
||||
const value = valueFunc();
|
||||
if (key === '_$content' && this._$contentKey) {
|
||||
this[this._$contentKey] = value;
|
||||
this._$updateDerived(this._$contentKey);
|
||||
}
|
||||
this[key] = value;
|
||||
this._$updateDerived(key);
|
||||
if (notInitd) this._$forwardPropsId.push(key);
|
||||
else this._$setPropToForward(key, value, deps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add a node to the set of nodes that are forwarding props to this node and init these props
|
||||
* @param node
|
||||
*/
|
||||
_$addForwardProps(node) {
|
||||
this._$forwardPropsSet.add(node);
|
||||
this._$forwardPropsId.forEach(key => {
|
||||
this._$setPropToForward(key, this[key], []);
|
||||
});
|
||||
DLNode.addWillUnmount(node, this._$forwardPropsSet.delete.bind(this._$forwardPropsSet, node));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cache the deps and return true if the deps are the same as the previous deps
|
||||
* @param key
|
||||
* @param deps
|
||||
* @returns
|
||||
*/
|
||||
_$cache(key, deps) {
|
||||
if (!deps || !deps.length) return false;
|
||||
const cacheKey = `$cc$${key}`;
|
||||
if (cached(deps, this[cacheKey])) return true;
|
||||
this[cacheKey] = deps;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the content prop, the key is stored in _$contentKey
|
||||
* @param value
|
||||
*/
|
||||
_$setContent(valueFunc, deps) {
|
||||
if ('_$forwardProps' in this) return this._$setForwardProp('_$content', valueFunc, deps);
|
||||
const contentKey = this._$contentKey;
|
||||
if (!contentKey) return;
|
||||
if (this._$cache(contentKey, deps)) return;
|
||||
this[contentKey] = valueFunc();
|
||||
this._$updateDerived(contentKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set a prop directly, if this is a forwarded prop, go and init forwarded props
|
||||
* @param key
|
||||
* @param value
|
||||
* @param deps
|
||||
*/
|
||||
_$setProp(key, valueFunc, deps) {
|
||||
if ('_$forwardProps' in this) return this._$setForwardProp(key, valueFunc, deps);
|
||||
if (!(`$p$${key}` in this)) {
|
||||
console.warn(`[${key}] is not a prop in ${this.constructor.name}`);
|
||||
return;
|
||||
}
|
||||
if (this._$cache(key, deps)) return;
|
||||
this[key] = valueFunc();
|
||||
this._$updateDerived(key);
|
||||
}
|
||||
|
||||
_$setProps(valueFunc, deps) {
|
||||
if (this._$cache('props', deps)) return;
|
||||
const props = valueFunc();
|
||||
if (!props) return;
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
this._$setProp(key, () => value, []);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Init an env, put the corresponding innermost envNode in $en$key
|
||||
* @param key
|
||||
* @param value
|
||||
* @param envNode
|
||||
*/
|
||||
_$initEnv(key, value, envNode) {
|
||||
this[key] = value;
|
||||
this[`$en$${key}`] = envNode;
|
||||
}
|
||||
|
||||
// ---- Update functions
|
||||
/**
|
||||
* @brief Update an env, called in EnvNode._$update
|
||||
* @param key
|
||||
* @param value
|
||||
* @param envNode
|
||||
*/
|
||||
_$updateEnv(key, value, envNode) {
|
||||
if (!(`$e$${key}` in this)) return;
|
||||
if (envNode !== this[`$en$${key}`]) return;
|
||||
this[key] = value;
|
||||
this._$updateDerived(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update a prop
|
||||
*/
|
||||
_$ud(exp, key) {
|
||||
this._$updateDerived(key);
|
||||
return exp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update properties that depend on this property
|
||||
* @param key
|
||||
*/
|
||||
_$updateDerived(key) {
|
||||
if ('_$notInitd' in this) return;
|
||||
|
||||
this[`$s$${key}`]?.forEach(k => {
|
||||
if (`$w$${k}` in this) {
|
||||
// ---- Watcher
|
||||
this[k](key);
|
||||
} else if (`$md$${k}` in this) {
|
||||
this[k]._$update();
|
||||
} else {
|
||||
// ---- Regular derived value
|
||||
this[k] = this[`$f$${k}`];
|
||||
}
|
||||
});
|
||||
|
||||
// ---- "trigger-view"
|
||||
this._$updateView(key);
|
||||
}
|
||||
|
||||
_$updateView(key) {
|
||||
if (this._$modelCallee) return this._$updateModelCallee();
|
||||
if (!('_$update' in this)) return;
|
||||
const depNum = this[`$$${key}`];
|
||||
if (!depNum) return;
|
||||
// ---- Collect all depNums that need to be updated
|
||||
if ('_$depNumsToUpdate' in this) {
|
||||
this._$depNumsToUpdate.push(depNum);
|
||||
} else {
|
||||
this._$depNumsToUpdate = [depNum];
|
||||
// ---- Update in the next microtask
|
||||
Promise.resolve().then(() => {
|
||||
// ---- Abort if unmounted
|
||||
if (this._$unmounted) return;
|
||||
const depNums = this._$depNumsToUpdate;
|
||||
if (depNums.length > 0) {
|
||||
const depNum = depNums.reduce((acc, cur) => acc | cur, 0);
|
||||
this._$update(depNum);
|
||||
}
|
||||
delete this._$depNumsToUpdate;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_$updateModelCallee() {
|
||||
if ('_$depNumsToUpdate' in this) return;
|
||||
this._$depNumsToUpdate = true;
|
||||
// ---- Update in the next microtask
|
||||
Promise.resolve().then(() => {
|
||||
// ---- Abort if unmounted
|
||||
if (this._$unmounted) return;
|
||||
this._$modelCallee._$updateDerived(this._$modelKey);
|
||||
delete this._$depNumsToUpdate;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @brief Update all props and content of the model
|
||||
*/
|
||||
static _$updateModel(model, propsFunc, contentFunc) {
|
||||
// ---- Suppress update because top level update will be performed
|
||||
// directly by the state variable in the model callee, which will
|
||||
// trigger the update of the model
|
||||
const props = propsFunc() ?? {};
|
||||
const collectedProps = props.s ?? [];
|
||||
props.m?.forEach(([props, deps]) => {
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
collectedProps.push([key, value, deps]);
|
||||
});
|
||||
});
|
||||
collectedProps.forEach(([key, value, deps]) => {
|
||||
model._$setProp(key, () => value, deps);
|
||||
});
|
||||
const content = contentFunc();
|
||||
if (content) model._$setContent(() => content[0], content[1]);
|
||||
}
|
||||
|
||||
static _$releaseModel() {
|
||||
delete this._$modelCallee;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Inject Dlight model in to a property
|
||||
* @param ModelCls
|
||||
* @param props { m: [props, deps], s: [key, value, deps] }
|
||||
* @param content
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
_$injectModel(ModelCls, propsFunc, contentFunc, key) {
|
||||
const props = propsFunc() ?? {};
|
||||
const collectedProps = props.s ?? [];
|
||||
props.m?.forEach(([props, deps]) => {
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
collectedProps.push([key, value, deps]);
|
||||
});
|
||||
});
|
||||
const model = new ModelCls();
|
||||
model._$init(collectedProps, contentFunc(), null, null);
|
||||
model._$modelCallee = this;
|
||||
model._$modelKey = key;
|
||||
model._$update = CompNode._$updateModel.bind(null, model, propsFunc, contentFunc);
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- @View -> class Comp extends View
|
||||
export const View = CompNode;
|
||||
export const Model = CompNode;
|
||||
|
||||
/**
|
||||
* @brief Run all update functions given the key
|
||||
* @param dlNode
|
||||
* @param key
|
||||
*/
|
||||
export function update(dlNode, key) {
|
||||
dlNode._$updateDerived(key);
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
import { DLStore } from './store';
|
||||
|
||||
export const DLNodeType = {
|
||||
Comp: 0,
|
||||
For: 1,
|
||||
Cond: 2,
|
||||
Env: 3,
|
||||
Exp: 4,
|
||||
Snippet: 5,
|
||||
Try: 6,
|
||||
};
|
||||
|
||||
export class DLNode {
|
||||
/**
|
||||
* @brief Node type: HTML, Text, Custom, For, If, Env, Expression
|
||||
*/
|
||||
_$dlNodeType;
|
||||
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param nodeType
|
||||
*/
|
||||
constructor(nodeType) {
|
||||
this._$dlNodeType = nodeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Node element
|
||||
* Either one real element for HTMLNode and TextNode
|
||||
* Or an array of DLNode for CustomNode, ForNode, IfNode, EnvNode, ExpNode
|
||||
*/
|
||||
get _$el() {
|
||||
return DLNode.toEls(this._$nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Loop all child DLNodes to get all the child elements
|
||||
* @param nodes
|
||||
* @returns HTMLElement[]
|
||||
*/
|
||||
static toEls(nodes) {
|
||||
const els = [];
|
||||
this.loopShallowEls(nodes, el => {
|
||||
els.push(el);
|
||||
});
|
||||
return els;
|
||||
}
|
||||
|
||||
// ---- Loop nodes ----
|
||||
/**
|
||||
* @brief Loop all elements shallowly,
|
||||
* i.e., don't loop the child nodes of dom elements and only call runFunc on dom elements
|
||||
* @param nodes
|
||||
* @param runFunc
|
||||
*/
|
||||
static loopShallowEls(nodes, runFunc) {
|
||||
const stack = [...nodes].reverse();
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
if (!('_$dlNodeType' in node)) runFunc(node);
|
||||
else node._$nodes && stack.push(...[...node._$nodes].reverse());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add parentEl to all nodes until the first element
|
||||
* @param nodes
|
||||
* @param parentEl
|
||||
*/
|
||||
static addParentEl(nodes, parentEl) {
|
||||
nodes.forEach(node => {
|
||||
if ('_$dlNodeType' in node) {
|
||||
node._$parentEl = parentEl;
|
||||
node._$nodes && DLNode.addParentEl(node._$nodes, parentEl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Flow index and add child elements ----
|
||||
/**
|
||||
* @brief Get the total count of dom elements before the stop node
|
||||
* @param nodes
|
||||
* @param stopNode
|
||||
* @returns total count of dom elements
|
||||
*/
|
||||
static getFlowIndexFromNodes(nodes, stopNode) {
|
||||
let index = 0;
|
||||
const stack = [...nodes].reverse();
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
if (node === stopNode) break;
|
||||
if ('_$dlNodeType' in node) {
|
||||
node._$nodes && stack.push(...[...node._$nodes].reverse());
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Given an array of nodes, append them to the parentEl
|
||||
* 1. If nextSibling is provided, insert the nodes before the nextSibling
|
||||
* 2. If nextSibling is not provided, append the nodes to the parentEl
|
||||
* @param nodes
|
||||
* @param parentEl
|
||||
* @param nextSibling
|
||||
* @returns Added element count
|
||||
*/
|
||||
static appendNodesWithSibling(nodes, parentEl, nextSibling) {
|
||||
if (nextSibling) return this.insertNodesBefore(nodes, parentEl, nextSibling);
|
||||
return this.appendNodes(nodes, parentEl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Given an array of nodes, append them to the parentEl using the index
|
||||
* 1. If the index is the same as the length of the parentEl.childNodes, append the nodes to the parentEl
|
||||
* 2. If the index is not the same as the length of the parentEl.childNodes, insert the nodes before the node at the index
|
||||
* @param nodes
|
||||
* @param parentEl
|
||||
* @param index
|
||||
* @param length
|
||||
* @returns Added element count
|
||||
*/
|
||||
static appendNodesWithIndex(nodes, parentEl, index, length) {
|
||||
length = length ?? parentEl.childNodes.length;
|
||||
if (length !== index) return this.insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]);
|
||||
return this.appendNodes(nodes, parentEl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Insert nodes before the nextSibling
|
||||
* @param nodes
|
||||
* @param parentEl
|
||||
* @param nextSibling
|
||||
* @returns Added element count
|
||||
*/
|
||||
static insertNodesBefore(nodes, parentEl, nextSibling) {
|
||||
let count = 0;
|
||||
this.loopShallowEls(nodes, el => {
|
||||
parentEl.insertBefore(el, nextSibling);
|
||||
count++;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Append nodes to the parentEl
|
||||
* @param nodes
|
||||
* @param parentEl
|
||||
* @returns Added element count
|
||||
*/
|
||||
static appendNodes(nodes, parentEl) {
|
||||
let count = 0;
|
||||
this.loopShallowEls(nodes, el => {
|
||||
parentEl.appendChild(el);
|
||||
count++;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// ---- Lifecycle ----
|
||||
/**
|
||||
* @brief Add willUnmount function to node
|
||||
* @param node
|
||||
* @param func
|
||||
*/
|
||||
static addWillUnmount(node, func) {
|
||||
const willUnmountStore = DLStore.global.WillUnmountStore;
|
||||
const currentStore = willUnmountStore[willUnmountStore.length - 1];
|
||||
// ---- If the current store is empty, it means this node is not mutable
|
||||
if (!currentStore) return;
|
||||
currentStore.push(func.bind(null, node));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add didUnmount function to node
|
||||
* @param node
|
||||
* @param func
|
||||
*/
|
||||
static addDidUnmount(node, func) {
|
||||
const didUnmountStore = DLStore.global.DidUnmountStore;
|
||||
const currentStore = didUnmountStore[didUnmountStore.length - 1];
|
||||
// ---- If the current store is empty, it means this node is not mutable
|
||||
if (!currentStore) return;
|
||||
currentStore.push(func.bind(null, node));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add didUnmount function to global store
|
||||
* @param func
|
||||
*/
|
||||
static addDidMount(node, func) {
|
||||
if (!DLStore.global.DidMountStore) DLStore.global.DidMountStore = [];
|
||||
DLStore.global.DidMountStore.push(func.bind(null, node));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run all didMount functions and reset the global store
|
||||
*/
|
||||
static runDidMount() {
|
||||
const didMountStore = DLStore.global.DidMountStore;
|
||||
if (!didMountStore || didMountStore.length === 0) return;
|
||||
for (let i = didMountStore.length - 1; i >= 0; i--) {
|
||||
didMountStore[i]();
|
||||
}
|
||||
DLStore.global.DidMountStore = [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import { DLNode, DLNodeType } from './DLNode';
|
||||
import { DLStore, cached } from './store';
|
||||
|
||||
export class EnvStoreClass {
|
||||
constructor() {
|
||||
this.envs = {};
|
||||
this.currentEnvNodes = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add a node to the current env and merge envs
|
||||
* @param node - The node to add
|
||||
*/
|
||||
addEnvNode(node) {
|
||||
this.currentEnvNodes.push(node);
|
||||
this.mergeEnvs();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Replace the current env with the given nodes and merge envs
|
||||
* @param nodes - The nodes to replace the current environment with
|
||||
*/
|
||||
replaceEnvNodes(nodes) {
|
||||
this.currentEnvNodes = nodes;
|
||||
this.mergeEnvs();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remove the last node from the current env and merge envs
|
||||
*/
|
||||
removeEnvNode() {
|
||||
this.currentEnvNodes.pop();
|
||||
this.mergeEnvs();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Merge all the envs in currentEnvNodes, inner envs override outer envs
|
||||
*/
|
||||
mergeEnvs() {
|
||||
this.envs = {};
|
||||
this.currentEnvNodes.forEach(envNode => {
|
||||
Object.entries(envNode.envs).forEach(([key, value]) => {
|
||||
this.envs[key] = [value, envNode];
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class EnvNode extends DLNode {
|
||||
constructor(envs, depsArr) {
|
||||
super(DLNodeType.Env);
|
||||
// Declare a global variable to store the environment variables
|
||||
if (!('DLEnvStore' in DLStore.global)) DLStore.global.DLEnvStore = new EnvStoreClass();
|
||||
|
||||
this.envs = envs;
|
||||
this.depsArr = depsArr;
|
||||
this.updateNodes = new Set();
|
||||
|
||||
DLStore.global.DLEnvStore.addEnvNode(this);
|
||||
}
|
||||
|
||||
cached(deps, name) {
|
||||
if (!deps || !deps.length) return false;
|
||||
if (cached(deps, this.depsArr[name])) return true;
|
||||
this.depsArr[name] = deps;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update a specific env, and update all the comp nodes that depend on this env
|
||||
* @param name - The name of the environment variable to update
|
||||
* @param value - The new value of the environment variable
|
||||
*/
|
||||
updateEnv(name, valueFunc, deps) {
|
||||
if (this.cached(deps, name)) return;
|
||||
const value = valueFunc();
|
||||
this.envs[name] = value;
|
||||
if (DLStore.global.DLEnvStore.currentEnvNodes.includes(this)) {
|
||||
DLStore.global.DLEnvStore.mergeEnvs();
|
||||
}
|
||||
this.updateNodes.forEach(node => {
|
||||
node._$updateEnv(name, value, this);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts
|
||||
* @param node - The node to add
|
||||
*/
|
||||
addNode(node) {
|
||||
this.updateNodes.add(node);
|
||||
DLNode.addWillUnmount(node, this.updateNodes.delete.bind(this.updateNodes, node));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set this._$nodes, and exit the current env
|
||||
* @param nodes - The nodes to set
|
||||
*/
|
||||
initNodes(nodes) {
|
||||
this._$nodes = nodes;
|
||||
DLStore.global.DLEnvStore.removeEnvNode();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
import { DLNode } from './DLNode';
|
||||
import { DLStore, cached } from './store';
|
||||
|
||||
function cache(el, key, deps) {
|
||||
if (deps.length === 0) return false;
|
||||
const cacheKey = `$${key}`;
|
||||
if (cached(deps, el[cacheKey])) return true;
|
||||
el[cacheKey] = deps;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Plainly set style
|
||||
* @param el
|
||||
* @param value
|
||||
*/
|
||||
export function setStyle(el, value) {
|
||||
Object.entries(value).forEach(([key, value]) => {
|
||||
if (key.startsWith('--')) {
|
||||
el.style.setProperty(key, value);
|
||||
} else {
|
||||
el.style[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Plainly set dataset
|
||||
* @param el
|
||||
* @param value
|
||||
*/
|
||||
export function setDataset(el, value) {
|
||||
Object.assign(el.dataset, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set HTML property with checking value equality first
|
||||
* @param el
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
export function setHTMLProp(el, key, valueFunc, deps) {
|
||||
// ---- Comparing deps, same value won't trigger
|
||||
// will lead to a bug if the value is set outside of the DLNode
|
||||
// e.g. setHTMLProp(el, "textContent", "value", [])
|
||||
// => el.textContent = "other"
|
||||
// => setHTMLProp(el, "textContent", "value", [])
|
||||
// The value will be set to "other" instead of "value"
|
||||
if (cache(el, key, deps)) return;
|
||||
el[key] = valueFunc();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Plainly set HTML properties
|
||||
* @param el
|
||||
* @param value
|
||||
*/
|
||||
export function setHTMLProps(el, value) {
|
||||
Object.entries(value).forEach(([key, v]) => {
|
||||
if (key === 'style') return setStyle(el, v);
|
||||
if (key === 'dataset') return setDataset(el, v);
|
||||
setHTMLProp(el, key, () => v, []);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set HTML attribute with checking value equality first
|
||||
* @param el
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
export function setHTMLAttr(el, key, valueFunc, deps) {
|
||||
if (cache(el, key, deps)) return;
|
||||
el.setAttribute(key, valueFunc());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Plainly set HTML attributes
|
||||
* @param el
|
||||
* @param value
|
||||
*/
|
||||
export function setHTMLAttrs(el, value) {
|
||||
Object.entries(value).forEach(([key, v]) => {
|
||||
setHTMLAttr(el, key, () => v, []);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first
|
||||
* @param el
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
export function setEvent(el, key, value) {
|
||||
const prevEvent = el[`$on${key}`];
|
||||
if (prevEvent) el.removeEventListener(key, prevEvent);
|
||||
el.addEventListener(key, value);
|
||||
el[`$on${key}`] = value;
|
||||
}
|
||||
|
||||
function eventHandler(e) {
|
||||
const key = `$$${e.type}`;
|
||||
for (const node of e.composedPath()) {
|
||||
if (node[key]) node[key](e);
|
||||
if (e.cancelBubble) return;
|
||||
}
|
||||
}
|
||||
|
||||
export function delegateEvent(el, key, value) {
|
||||
if (el[`$$${key}`] === value) return;
|
||||
el[`$$${key}`] = value;
|
||||
if (!DLStore.delegatedEvents.has(key)) {
|
||||
DLStore.delegatedEvents.add(key);
|
||||
DLStore.document.addEventListener(key, eventHandler);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @brief Shortcut for document.createElement
|
||||
* @param tag
|
||||
* @returns HTMLElement
|
||||
*/
|
||||
export function createElement(tag) {
|
||||
return DLStore.document.createElement(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children
|
||||
* @param el
|
||||
* @param node
|
||||
* @param position
|
||||
*/
|
||||
export function insertNode(el, node, position) {
|
||||
// ---- Set _$nodes
|
||||
if (!el._$nodes) el._$nodes = Array.from(el.childNodes);
|
||||
el._$nodes.splice(position, 0, node);
|
||||
|
||||
// ---- Insert nodes' elements
|
||||
const flowIdx = DLNode.getFlowIndexFromNodes(el._$nodes, node);
|
||||
DLNode.appendNodesWithIndex([node], el, flowIdx);
|
||||
// ---- Set parentEl
|
||||
DLNode.addParentEl([node], el);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief An inclusive assign prop function that accepts any type of prop
|
||||
* @param el
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
export function forwardHTMLProp(el, key, valueFunc, deps) {
|
||||
if (key === 'style') return setStyle(el, valueFunc());
|
||||
if (key === 'dataset') return setDataset(el, valueFunc());
|
||||
if (key === 'element') return;
|
||||
if (key === 'prop') return setHTMLProps(el, valueFunc());
|
||||
if (key === 'attr') return setHTMLAttrs(el, valueFunc());
|
||||
if (key === 'innerHTML') return setHTMLProp(el, 'innerHTML', valueFunc, deps);
|
||||
if (key === 'textContent') return setHTMLProp(el, 'textContent', valueFunc, deps);
|
||||
if (key === 'forwardProp') return;
|
||||
if (key.startsWith('on')) {
|
||||
return setEvent(el, key.slice(2).toLowerCase(), valueFunc());
|
||||
}
|
||||
setHTMLAttr(el, key, valueFunc, deps);
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { DLNodeType } from '../DLNode';
|
||||
import { FlatNode } from './FlatNode';
|
||||
|
||||
export class CondNode extends FlatNode {
|
||||
/**
|
||||
* @brief Constructor, If type, accept a function that returns a list of nodes
|
||||
* @param caseFunc
|
||||
*/
|
||||
constructor(depNum, condFunc) {
|
||||
super(DLNodeType.Cond);
|
||||
this.depNum = depNum;
|
||||
this.cond = -1;
|
||||
this.condFunc = condFunc;
|
||||
this.initUnmountStore();
|
||||
this._$nodes = this.condFunc(this);
|
||||
this.setUnmountFuncs();
|
||||
|
||||
// ---- Add to the global UnmountStore
|
||||
CondNode.addWillUnmount(this, this.runWillUnmount.bind(this));
|
||||
CondNode.addDidUnmount(this, this.runDidUnmount.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update the nodes in the environment
|
||||
*/
|
||||
updateCond(key) {
|
||||
// ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv
|
||||
// The reason is that if it didn't change, we don't need to unmount or remove the nodes
|
||||
const prevFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
|
||||
const newNodes = this.geneNewNodesInEnv(() => this.condFunc(this));
|
||||
|
||||
// ---- If the new nodes are the same as the old nodes, we only need to update children
|
||||
if (this.didntChange) {
|
||||
[this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
|
||||
this.didntChange = false;
|
||||
this.updateFunc?.(this.depNum, key);
|
||||
return;
|
||||
}
|
||||
// ---- Remove old nodes
|
||||
const newFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
|
||||
[this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
|
||||
this._$nodes && this._$nodes.length > 0 && this.removeNodes(this._$nodes);
|
||||
[this.willUnmountFuncs, this.didUnmountFuncs] = newFuncs;
|
||||
|
||||
if (newNodes.length === 0) {
|
||||
// ---- No branch has been taken
|
||||
this._$nodes = [];
|
||||
return;
|
||||
}
|
||||
// ---- Add new nodes
|
||||
const parentEl = this._$parentEl;
|
||||
// ---- Faster append with nextSibling rather than flowIndex
|
||||
const flowIndex = CondNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
||||
|
||||
const nextSibling = parentEl.childNodes[flowIndex];
|
||||
CondNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
||||
CondNode.runDidMount();
|
||||
this._$nodes = newNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The update function of IfNode's childNodes is stored in the first child node
|
||||
* @param changed
|
||||
*/
|
||||
update(changed) {
|
||||
if (!(~this.depNum & changed)) return;
|
||||
this.updateFunc?.(changed);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import { DLNodeType } from '../DLNode';
|
||||
import { FlatNode } from './FlatNode';
|
||||
import { DLStore, cached } from '../store';
|
||||
|
||||
export class ExpNode extends FlatNode {
|
||||
/**
|
||||
* @brief Constructor, Exp type, accept a function that returns a list of nodes
|
||||
* @param nodesFunc
|
||||
*/
|
||||
constructor(value, deps) {
|
||||
super(DLNodeType.Exp);
|
||||
this.initUnmountStore();
|
||||
this._$nodes = ExpNode.formatNodes(value);
|
||||
this.setUnmountFuncs();
|
||||
this.deps = this.parseDeps(deps);
|
||||
// ---- Add to the global UnmountStore
|
||||
ExpNode.addWillUnmount(this, this.runWillUnmount.bind(this));
|
||||
ExpNode.addDidUnmount(this, this.runDidUnmount.bind(this));
|
||||
}
|
||||
|
||||
parseDeps(deps) {
|
||||
return deps.map(dep => {
|
||||
// ---- CompNode
|
||||
if (dep?.prototype?._$init) return dep.toString();
|
||||
// ---- SnippetNode
|
||||
if (dep?.propViewFunc) return dep.propViewFunc.toString();
|
||||
return dep;
|
||||
});
|
||||
}
|
||||
|
||||
cache(deps) {
|
||||
if (!deps || !deps.length) return false;
|
||||
deps = this.parseDeps(deps);
|
||||
if (cached(deps, this.deps)) return true;
|
||||
this.deps = deps;
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @brief Generate new nodes and replace the old nodes
|
||||
*/
|
||||
update(valueFunc, deps) {
|
||||
if (this.cache(deps)) return;
|
||||
this.removeNodes(this._$nodes);
|
||||
const newNodes = this.geneNewNodesInEnv(() => ExpNode.formatNodes(valueFunc()));
|
||||
if (newNodes.length === 0) {
|
||||
this._$nodes = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Add new nodes
|
||||
const parentEl = this._$parentEl;
|
||||
const flowIndex = ExpNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
||||
const nextSibling = parentEl.childNodes[flowIndex];
|
||||
ExpNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
||||
ExpNode.runDidMount();
|
||||
|
||||
this._$nodes = newNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Format the nodes
|
||||
* @param nodes
|
||||
* @returns New nodes
|
||||
*/
|
||||
static formatNodes(nodes) {
|
||||
if (!Array.isArray(nodes)) nodes = [nodes];
|
||||
return (
|
||||
nodes
|
||||
// ---- Flatten the nodes
|
||||
.flat(1)
|
||||
// ---- Filter out empty nodes
|
||||
.filter(node => node !== undefined && node !== null && typeof node !== 'boolean')
|
||||
.map(node => {
|
||||
// ---- If the node is a string, number or bigint, convert it to a text node
|
||||
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
|
||||
return DLStore.document.createTextNode(`${node}`);
|
||||
}
|
||||
// ---- If the node has PropView, call it to get the view
|
||||
if ('propViewFunc' in node) return node.build();
|
||||
return node;
|
||||
})
|
||||
// ---- Flatten the nodes again
|
||||
.flat(1)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { DLStore } from '../store';
|
||||
import { MutableNode } from './MutableNode';
|
||||
|
||||
export class FlatNode extends MutableNode {
|
||||
willUnmountFuncs = [];
|
||||
didUnmountFuncs = [];
|
||||
|
||||
setUnmountFuncs() {
|
||||
this.willUnmountFuncs = DLStore.global.WillUnmountStore.pop();
|
||||
this.didUnmountFuncs = DLStore.global.DidUnmountStore.pop();
|
||||
}
|
||||
|
||||
runWillUnmount() {
|
||||
for (let i = 0; i < this.willUnmountFuncs.length; i++) this.willUnmountFuncs[i]();
|
||||
}
|
||||
|
||||
runDidUnmount() {
|
||||
for (let i = this.didUnmountFuncs.length - 1; i >= 0; i--) this.didUnmountFuncs[i]();
|
||||
}
|
||||
|
||||
removeNodes(nodes) {
|
||||
this.runWillUnmount();
|
||||
super.removeNodes(nodes);
|
||||
this.runDidUnmount();
|
||||
}
|
||||
|
||||
geneNewNodesInEnv(newNodesFunc) {
|
||||
this.initUnmountStore();
|
||||
const nodes = super.geneNewNodesInEnv(newNodesFunc);
|
||||
this.setUnmountFuncs();
|
||||
return nodes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,406 @@
|
|||
import { DLNodeType } from '../DLNode';
|
||||
import { DLStore } from '../store';
|
||||
import { MutableNode } from './MutableNode';
|
||||
|
||||
export class ForNode extends MutableNode {
|
||||
array;
|
||||
nodeFunc;
|
||||
depNum;
|
||||
|
||||
nodesMap = new Map();
|
||||
updateArr = [];
|
||||
|
||||
/**
|
||||
* @brief Getter for nodes
|
||||
*/
|
||||
get _$nodes() {
|
||||
const nodes = [];
|
||||
for (let idx = 0; idx < this.array.length; idx++) {
|
||||
nodes.push(...this.nodesMap.get(this.keys?.[idx] ?? idx));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Constructor, For type
|
||||
* @param array
|
||||
* @param nodeFunc
|
||||
* @param keys
|
||||
*/
|
||||
constructor(array, depNum, keys, nodeFunc) {
|
||||
super(DLNodeType.For);
|
||||
this.array = [...array];
|
||||
this.keys = keys;
|
||||
this.depNum = depNum;
|
||||
this.addNodeFunc(nodeFunc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief To be called immediately after the constructor
|
||||
* @param nodeFunc
|
||||
*/
|
||||
addNodeFunc(nodeFunc) {
|
||||
this.nodeFunc = nodeFunc;
|
||||
this.array.forEach((item, idx) => {
|
||||
this.initUnmountStore();
|
||||
const key = this.keys?.[idx] ?? idx;
|
||||
const nodes = nodeFunc(item, this.updateArr, idx);
|
||||
this.nodesMap.set(key, nodes);
|
||||
this.setUnmountMap(key);
|
||||
});
|
||||
// ---- For nested ForNode, the whole strategy is just like EnvStore
|
||||
// we use array of function array to create "environment", popping and pushing
|
||||
ForNode.addWillUnmount(this, this.runAllWillUnmount.bind(this));
|
||||
ForNode.addDidUnmount(this, this.runAllDidUnmount.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update the view related to one item in the array
|
||||
* @param nodes
|
||||
* @param item
|
||||
*/
|
||||
updateItem(idx, array, changed) {
|
||||
// ---- The update function of ForNode's childNodes is stored in the first child node
|
||||
this.updateArr[idx]?.(changed ?? this.depNum, array[idx]);
|
||||
}
|
||||
|
||||
updateItems(changed) {
|
||||
for (let idx = 0; idx < this.array.length; idx++) {
|
||||
this.updateItem(idx, this.array, changed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Non-array update function
|
||||
* @param changed
|
||||
*/
|
||||
update(changed) {
|
||||
// ---- e.g. this.depNum -> 1110 changed-> 1010
|
||||
// ~this.depNum & changed -> ~1110 & 1010 -> 0000
|
||||
// no update because depNum contains all the changed
|
||||
// ---- e.g. this.depNum -> 1110 changed-> 1101
|
||||
// ~this.depNum & changed -> ~1110 & 1101 -> 0001
|
||||
// update because depNum doesn't contain all the changed
|
||||
if (!(~this.depNum & changed)) return;
|
||||
this.updateItems(changed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Array-related update function
|
||||
* @param newArray
|
||||
* @param newKeys
|
||||
*/
|
||||
updateArray(newArray, newKeys) {
|
||||
if (newKeys) {
|
||||
this.updateWithKey(newArray, newKeys);
|
||||
return;
|
||||
}
|
||||
this.updateWithOutKey(newArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shortcut to generate new nodes with idx and key
|
||||
*/
|
||||
getNewNodes(idx, key, array, updateArr) {
|
||||
this.initUnmountStore();
|
||||
const nodes = this.geneNewNodesInEnv(() => this.nodeFunc(array[idx], updateArr ?? this.updateArr, idx));
|
||||
this.setUnmountMap(key);
|
||||
this.nodesMap.set(key, nodes);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the unmount map by getting the last unmount map from the global store
|
||||
* @param key
|
||||
*/
|
||||
setUnmountMap(key) {
|
||||
const willUnmountMap = DLStore.global.WillUnmountStore.pop();
|
||||
if (willUnmountMap && willUnmountMap.length > 0) {
|
||||
if (!this.willUnmountMap) this.willUnmountMap = new Map();
|
||||
this.willUnmountMap.set(key, willUnmountMap);
|
||||
}
|
||||
const didUnmountMap = DLStore.global.DidUnmountStore.pop();
|
||||
if (didUnmountMap && didUnmountMap.length > 0) {
|
||||
if (!this.didUnmountMap) this.didUnmountMap = new Map();
|
||||
this.didUnmountMap.set(key, didUnmountMap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run all the unmount functions and clear the unmount map
|
||||
*/
|
||||
runAllWillUnmount() {
|
||||
if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
|
||||
this.willUnmountMap.forEach(funcs => {
|
||||
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
|
||||
});
|
||||
this.willUnmountMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run all the unmount functions and clear the unmount map
|
||||
*/
|
||||
runAllDidUnmount() {
|
||||
if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
|
||||
this.didUnmountMap.forEach(funcs => {
|
||||
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
|
||||
});
|
||||
this.didUnmountMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run the unmount functions of the given key
|
||||
* @param key
|
||||
*/
|
||||
runWillUnmount(key) {
|
||||
if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
|
||||
const funcs = this.willUnmountMap.get(key);
|
||||
if (!funcs) return;
|
||||
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
|
||||
this.willUnmountMap.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run the unmount functions of the given key
|
||||
*/
|
||||
runDidUnmount(key) {
|
||||
if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
|
||||
const funcs = this.didUnmountMap.get(key);
|
||||
if (!funcs) return;
|
||||
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
|
||||
this.didUnmountMap.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
|
||||
* @param nodes
|
||||
* @param key
|
||||
*/
|
||||
removeNodes(nodes, key) {
|
||||
this.runWillUnmount(key);
|
||||
super.removeNodes(nodes);
|
||||
this.runDidUnmount(key);
|
||||
this.nodesMap.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update the nodes without keys
|
||||
* @param newArray
|
||||
*/
|
||||
updateWithOutKey(newArray) {
|
||||
const preLength = this.array.length;
|
||||
const currLength = newArray.length;
|
||||
|
||||
if (preLength === currLength) {
|
||||
// ---- If the length is the same, we only need to update the nodes
|
||||
for (let idx = 0; idx < this.array.length; idx++) {
|
||||
this.updateItem(idx, newArray);
|
||||
}
|
||||
this.array = [...newArray];
|
||||
return;
|
||||
}
|
||||
const parentEl = this._$parentEl;
|
||||
// ---- If the new array is longer, add new nodes directly
|
||||
if (preLength < currLength) {
|
||||
let flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
||||
// ---- Calling parentEl.childNodes.length is time-consuming,
|
||||
// so we use a length variable to store the length
|
||||
const length = parentEl.childNodes.length;
|
||||
for (let idx = 0; idx < currLength; idx++) {
|
||||
if (idx < preLength) {
|
||||
flowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(idx));
|
||||
this.updateItem(idx, newArray);
|
||||
continue;
|
||||
}
|
||||
const newNodes = this.getNewNodes(idx, idx, newArray);
|
||||
ForNode.appendNodesWithIndex(newNodes, parentEl, flowIndex, length);
|
||||
}
|
||||
ForNode.runDidMount();
|
||||
this.array = [...newArray];
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Update the nodes first
|
||||
for (let idx = 0; idx < currLength; idx++) {
|
||||
this.updateItem(idx, newArray);
|
||||
}
|
||||
// ---- If the new array is shorter, remove the extra nodes
|
||||
for (let idx = currLength; idx < preLength; idx++) {
|
||||
const nodes = this.nodesMap.get(idx);
|
||||
this.removeNodes(nodes, idx);
|
||||
}
|
||||
this.updateArr.splice(currLength, preLength - currLength);
|
||||
this.array = [...newArray];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update the nodes with keys
|
||||
* @param newArray
|
||||
* @param newKeys
|
||||
*/
|
||||
updateWithKey(newArray, newKeys) {
|
||||
if (newKeys.length !== new Set(newKeys).size) {
|
||||
throw new Error('DLight: Duplicate keys in for loop are not allowed');
|
||||
}
|
||||
const prevKeys = this.keys;
|
||||
this.keys = newKeys;
|
||||
|
||||
if (ForNode.arrayEqual(prevKeys, this.keys)) {
|
||||
// ---- If the keys are the same, we only need to update the nodes
|
||||
for (let idx = 0; idx < newArray.length; idx++) {
|
||||
this.updateItem(idx, newArray);
|
||||
}
|
||||
this.array = [...newArray];
|
||||
return;
|
||||
}
|
||||
|
||||
const parentEl = this._$parentEl;
|
||||
|
||||
// ---- No nodes after, delete all nodes
|
||||
if (this.keys.length === 0) {
|
||||
const parentNodes = parentEl._$nodes ?? [];
|
||||
if (parentNodes.length === 1 && parentNodes[0] === this) {
|
||||
// ---- ForNode is the only node in the parent node
|
||||
// Frequently used in real life scenarios because we tend to always wrap for with a div element,
|
||||
// so we optimize it here
|
||||
this.runAllWillUnmount();
|
||||
parentEl.innerHTML = '';
|
||||
this.runAllDidUnmount();
|
||||
} else {
|
||||
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
|
||||
const prevKey = prevKeys[prevIdx];
|
||||
this.removeNodes(this.nodesMap.get(prevKey), prevKey);
|
||||
}
|
||||
}
|
||||
this.nodesMap.clear();
|
||||
this.updateArr = [];
|
||||
this.array = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Record how many nodes are before this ForNode with the same parentNode
|
||||
const flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
||||
|
||||
// ---- No nodes before, append all nodes
|
||||
if (prevKeys.length === 0) {
|
||||
const nextSibling = parentEl.childNodes[flowIndex];
|
||||
for (let idx = 0; idx < this.keys.length; idx++) {
|
||||
const newNodes = this.getNewNodes(idx, this.keys[idx], newArray);
|
||||
ForNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
||||
}
|
||||
ForNode.runDidMount();
|
||||
this.array = [...newArray];
|
||||
return;
|
||||
}
|
||||
|
||||
const shuffleKeys = [];
|
||||
const newUpdateArr = [];
|
||||
|
||||
// ---- 1. Delete the nodes that are no longer in the array
|
||||
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
|
||||
const prevKey = prevKeys[prevIdx];
|
||||
if (this.keys.includes(prevKey)) {
|
||||
shuffleKeys.push(prevKey);
|
||||
newUpdateArr.push(this.updateArr[prevIdx]);
|
||||
continue;
|
||||
}
|
||||
this.removeNodes(this.nodesMap.get(prevKey), prevKey);
|
||||
}
|
||||
|
||||
// ---- 2. Add the nodes that are not in the array but in the new array
|
||||
// ---- Calling parentEl.childNodes.length is time-consuming,
|
||||
// so we use a length variable to store the length
|
||||
let length = parentEl.childNodes.length;
|
||||
let newFlowIndex = flowIndex;
|
||||
for (let idx = 0; idx < this.keys.length; idx++) {
|
||||
const key = this.keys[idx];
|
||||
const prevIdx = shuffleKeys.indexOf(key);
|
||||
if (prevIdx !== -1) {
|
||||
// ---- These nodes are already in the parentEl,
|
||||
// and we need to keep track of their flowIndex
|
||||
newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
|
||||
newUpdateArr[prevIdx]?.(this.depNum, newArray[idx]);
|
||||
continue;
|
||||
}
|
||||
// ---- Insert updateArr first because in getNewNode the updateFunc will replace this null
|
||||
newUpdateArr.splice(idx, 0, null);
|
||||
const newNodes = this.getNewNodes(idx, key, newArray, newUpdateArr);
|
||||
// ---- Add the new nodes
|
||||
shuffleKeys.splice(idx, 0, key);
|
||||
|
||||
const count = ForNode.appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length);
|
||||
newFlowIndex += count;
|
||||
length += count;
|
||||
}
|
||||
ForNode.runDidMount();
|
||||
|
||||
// ---- After adding and deleting, the only thing left is to reorder the nodes,
|
||||
// but if the keys are the same, we don't need to reorder
|
||||
if (ForNode.arrayEqual(this.keys, shuffleKeys)) {
|
||||
this.array = [...newArray];
|
||||
this.updateArr = newUpdateArr;
|
||||
return;
|
||||
}
|
||||
|
||||
newFlowIndex = flowIndex;
|
||||
const bufferNodes = new Map();
|
||||
// ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm
|
||||
for (let idx = 0; idx < this.keys.length; idx++) {
|
||||
const key = this.keys[idx];
|
||||
const prevIdx = shuffleKeys.indexOf(key);
|
||||
|
||||
const bufferedNode = bufferNodes.get(key);
|
||||
if (bufferedNode) {
|
||||
// ---- We need to add the flowIndex of the bufferedNode,
|
||||
// because the bufferedNode is in the parentEl and the new position is ahead of the previous position
|
||||
const bufferedFlowIndex = ForNode.getFlowIndexFromNodes(bufferedNode);
|
||||
const lastEl = ForNode.toEls(bufferedNode).pop();
|
||||
const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex];
|
||||
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
|
||||
// ---- If the node is buffered, we need to add it to the parentEl
|
||||
ForNode.insertNodesBefore(bufferedNode, parentEl, nextSibling);
|
||||
}
|
||||
// ---- So the added length is the length of the bufferedNode
|
||||
newFlowIndex += bufferedFlowIndex;
|
||||
delete bufferNodes[idx];
|
||||
} else if (prevIdx === idx) {
|
||||
// ---- If the node is in the same position, we don't need to do anything
|
||||
newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
|
||||
continue;
|
||||
} else {
|
||||
// ---- If the node is not in the same position, we need to buffer it
|
||||
// We buffer the node of the previous position, and then replace it with the node of the current position
|
||||
const prevKey = shuffleKeys[idx];
|
||||
bufferNodes.set(prevKey, this.nodesMap.get(prevKey));
|
||||
// ---- Length would never change, and the last will always be in the same position,
|
||||
// so it'll always be insertBefore instead of appendChild
|
||||
const childNodes = this.nodesMap.get(key);
|
||||
const lastEl = ForNode.toEls(childNodes).pop();
|
||||
const nextSibling = parentEl.childNodes[newFlowIndex];
|
||||
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
|
||||
newFlowIndex += ForNode.insertNodesBefore(childNodes, parentEl, nextSibling);
|
||||
}
|
||||
}
|
||||
// ---- Swap the keys
|
||||
const tempKey = shuffleKeys[idx];
|
||||
shuffleKeys[idx] = shuffleKeys[prevIdx];
|
||||
shuffleKeys[prevIdx] = tempKey;
|
||||
const tempUpdateFunc = newUpdateArr[idx];
|
||||
newUpdateArr[idx] = newUpdateArr[prevIdx];
|
||||
newUpdateArr[prevIdx] = tempUpdateFunc;
|
||||
}
|
||||
this.array = [...newArray];
|
||||
this.updateArr = newUpdateArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Compare two arrays
|
||||
* @param arr1
|
||||
* @param arr2
|
||||
* @returns
|
||||
*/
|
||||
static arrayEqual(arr1, arr2) {
|
||||
if (arr1.length !== arr2.length) return false;
|
||||
return arr1.every((item, idx) => item === arr2[idx]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import { DLNode } from '../DLNode';
|
||||
import { DLStore } from '../store';
|
||||
|
||||
export class MutableNode extends DLNode {
|
||||
/**
|
||||
* @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention:
|
||||
* 1. The environment of the new nodes should be the same as the old nodes
|
||||
* 2. The new nodes should be added to the parentEl
|
||||
* 3. The old nodes should be removed from the parentEl
|
||||
* @param type
|
||||
*/
|
||||
constructor(type) {
|
||||
super(type);
|
||||
// ---- Save the current environment nodes, must be a new reference
|
||||
if (DLStore.global.DLEnvStore && DLStore.global.DLEnvStore.currentEnvNodes.length > 0) {
|
||||
this.savedEnvNodes = [...DLStore.global.DLEnvStore.currentEnvNodes];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize the new nodes, add parentEl to all nodes
|
||||
* @param nodes
|
||||
*/
|
||||
initNewNodes(nodes) {
|
||||
// ---- Add parentEl to all nodes
|
||||
DLNode.addParentEl(nodes, this._$parentEl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generate new nodes in the saved environment
|
||||
* @param newNodesFunc
|
||||
* @returns
|
||||
*/
|
||||
geneNewNodesInEnv(newNodesFunc) {
|
||||
if (!this.savedEnvNodes) {
|
||||
// ---- No saved environment, just generate new nodes
|
||||
const newNodes = newNodesFunc();
|
||||
// ---- Only for IfNode's same condition return
|
||||
// ---- Initialize the new nodes
|
||||
this.initNewNodes(newNodes);
|
||||
return newNodes;
|
||||
}
|
||||
// ---- Save the current environment nodes
|
||||
const currentEnvNodes = DLStore.global.DLEnvStore.currentEnvNodes;
|
||||
// ---- Replace the saved environment nodes
|
||||
DLStore.global.DLEnvStore.replaceEnvNodes(this.savedEnvNodes);
|
||||
const newNodes = newNodesFunc();
|
||||
// ---- Retrieve the current environment nodes
|
||||
DLStore.global.DLEnvStore.replaceEnvNodes(currentEnvNodes);
|
||||
// ---- Only for IfNode's same condition return
|
||||
// ---- Initialize the new nodes
|
||||
this.initNewNodes(newNodes);
|
||||
return newNodes;
|
||||
}
|
||||
|
||||
initUnmountStore() {
|
||||
DLStore.global.WillUnmountStore.push([]);
|
||||
DLStore.global.DidUnmountStore.push([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
|
||||
* @param nodes
|
||||
* @param removeEl Only remove outermost element
|
||||
*/
|
||||
removeNodes(nodes) {
|
||||
DLNode.loopShallowEls(nodes, node => {
|
||||
this._$parentEl.removeChild(node);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { DLNodeType } from '../DLNode';
|
||||
import { FlatNode } from './FlatNode';
|
||||
import { EnvNode } from '../EnvNode';
|
||||
|
||||
export class TryNode extends FlatNode {
|
||||
constructor(tryFunc, catchFunc) {
|
||||
super(DLNodeType.Try);
|
||||
this.tryFunc = tryFunc;
|
||||
const catchable = this.getCatchable(catchFunc);
|
||||
this.envNode = new EnvNode({ _$catchable: catchable });
|
||||
const nodes = tryFunc(this.setUpdateFunc.bind(this), catchable) ?? [];
|
||||
this.envNode.initNodes(nodes);
|
||||
this._$nodes = nodes;
|
||||
}
|
||||
|
||||
update(changed) {
|
||||
this.updateFunc?.(changed);
|
||||
}
|
||||
|
||||
setUpdateFunc(updateFunc) {
|
||||
this.updateFunc = updateFunc;
|
||||
}
|
||||
|
||||
getCatchable(catchFunc) {
|
||||
return callback =>
|
||||
(...args) => {
|
||||
try {
|
||||
return callback(...args);
|
||||
} catch (e) {
|
||||
// ---- Run it in next tick to make sure when error occurs before
|
||||
// didMount, this._$parentEl is not null
|
||||
Promise.resolve().then(() => {
|
||||
const nodes = this.geneNewNodesInEnv(() => catchFunc(this.setUpdateFunc.bind(this), e));
|
||||
this._$nodes && this.removeNodes(this._$nodes);
|
||||
const parentEl = this._$parentEl;
|
||||
const flowIndex = FlatNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
||||
const nextSibling = parentEl.childNodes[flowIndex];
|
||||
FlatNode.appendNodesWithSibling(nodes, parentEl, nextSibling);
|
||||
FlatNode.runDidMount();
|
||||
this._$nodes = nodes;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { DLNode } from './DLNode';
|
||||
import { insertNode } from './HTMLNode';
|
||||
export class PropView {
|
||||
propViewFunc;
|
||||
dlUpdateFunc = new Set();
|
||||
|
||||
/**
|
||||
* @brief PropView constructor, accept a function that returns a list of DLNode
|
||||
* @param propViewFunc - A function that when called, collects and returns an array of DLNode instances
|
||||
*/
|
||||
constructor(propViewFunc) {
|
||||
this.propViewFunc = propViewFunc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build the prop view by calling the propViewFunc and add every single instance of the returned DLNode to dlUpdateNodes
|
||||
* @returns An array of DLNode instances returned by propViewFunc
|
||||
*/
|
||||
build() {
|
||||
let update;
|
||||
const addUpdate = updateFunc => {
|
||||
update = updateFunc;
|
||||
this.dlUpdateFunc.add(updateFunc);
|
||||
};
|
||||
const newNodes = this.propViewFunc(addUpdate);
|
||||
if (newNodes.length === 0) return [];
|
||||
if (update) {
|
||||
// Remove the updateNode from dlUpdateNodes when it unmounts
|
||||
DLNode.addWillUnmount(newNodes[0], this.dlUpdateFunc.delete.bind(this.dlUpdateFunc, update));
|
||||
}
|
||||
|
||||
return newNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update every node in dlUpdateNodes
|
||||
* @param changed - A parameter indicating what changed to trigger the update
|
||||
*/
|
||||
update(...args) {
|
||||
this.dlUpdateFunc.forEach(update => {
|
||||
update(...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function insertChildren(el, propView) {
|
||||
insertNode(el, { _$nodes: propView.build(), _$dlNodeType: 7 }, 0);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { DLNode, DLNodeType } from './DLNode';
|
||||
import { cached } from './store';
|
||||
|
||||
export class SnippetNode extends DLNode {
|
||||
constructor(depsArr) {
|
||||
super(DLNodeType.Snippet);
|
||||
this.depsArr = depsArr;
|
||||
}
|
||||
|
||||
cached(deps, changed) {
|
||||
if (!deps || !deps.length) return false;
|
||||
const idx = Math.log2(changed);
|
||||
const prevDeps = this.depsArr[idx];
|
||||
if (cached(deps, prevDeps)) return true;
|
||||
this.depsArr[idx] = deps;
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { DLStore, cached } from './store';
|
||||
|
||||
/**
|
||||
* @brief Shorten document.createTextNode
|
||||
* @param value
|
||||
* @returns Text
|
||||
*/
|
||||
export function createTextNode(value, deps) {
|
||||
const node = DLStore.document.createTextNode(value);
|
||||
node.$$deps = deps;
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update text node and check if the value is changed
|
||||
* @param node
|
||||
* @param value
|
||||
*/
|
||||
export function updateText(node, valueFunc, deps) {
|
||||
if (cached(deps, node.$$deps)) return;
|
||||
const value = valueFunc();
|
||||
node.textContent = value;
|
||||
node.$$deps = deps;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './types/index';
|
|
@ -0,0 +1,56 @@
|
|||
import { DLNode } from './DLNode';
|
||||
import { insertNode } from './HTMLNode';
|
||||
|
||||
export * from './HTMLNode';
|
||||
export * from './CompNode';
|
||||
export * from './EnvNode';
|
||||
export * from './TextNode';
|
||||
export * from './PropView';
|
||||
export * from './SnippetNode';
|
||||
export * from './MutableNode/ForNode';
|
||||
export * from './MutableNode/ExpNode';
|
||||
export * from './MutableNode/CondNode';
|
||||
export * from './MutableNode/TryNode';
|
||||
|
||||
import { DLStore } from './store';
|
||||
export { setGlobal, setDocument } from './store';
|
||||
|
||||
function initStore() {
|
||||
// Declare a global variable to store willUnmount functions
|
||||
DLStore.global.WillUnmountStore = [];
|
||||
// Declare a global variable to store didUnmount functions
|
||||
DLStore.global.DidUnmountStore = [];
|
||||
}
|
||||
|
||||
export function render(idOrEl, DL) {
|
||||
let el = idOrEl;
|
||||
if (typeof idOrEl === 'string') {
|
||||
const elFound = DLStore.document.getElementById(idOrEl);
|
||||
if (elFound) el = elFound;
|
||||
else {
|
||||
throw new Error(`DLight: Element with id ${idOrEl} not found`);
|
||||
}
|
||||
}
|
||||
initStore();
|
||||
el.innerHTML = '';
|
||||
const dlNode = new DL();
|
||||
dlNode._$init();
|
||||
insertNode(el, dlNode, 0);
|
||||
DLNode.runDidMount();
|
||||
}
|
||||
|
||||
export function manual(callback, _deps) {
|
||||
return callback();
|
||||
}
|
||||
export function escape(arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
export const $ = escape;
|
||||
export const required = null;
|
||||
|
||||
export function use() {
|
||||
console.error(
|
||||
'DLight: use() is not supported be called directly. You can only assign `use(model)` to a dlight class property. Any other expressions are not allowed.'
|
||||
);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { Store } from '@inula/store';
|
||||
|
||||
// ---- Using external Store to store global and document
|
||||
// Because Store is a singleton, it is safe to use it as a global variable
|
||||
// If created in DLight package, different package versions will introduce
|
||||
// multiple Store instances.
|
||||
|
||||
if (!('global' in Store)) {
|
||||
if (typeof window !== 'undefined') {
|
||||
Store.global = window;
|
||||
} else if (typeof global !== 'undefined') {
|
||||
Store.global = global;
|
||||
} else {
|
||||
Store.global = {};
|
||||
}
|
||||
}
|
||||
if (!('document' in Store)) {
|
||||
if (typeof document !== 'undefined') {
|
||||
Store.document = document;
|
||||
}
|
||||
}
|
||||
|
||||
export const DLStore = { ...Store, delegatedEvents: new Set() };
|
||||
|
||||
export function setGlobal(globalObj) {
|
||||
DLStore.global = globalObj;
|
||||
}
|
||||
|
||||
export function setDocument(customDocument) {
|
||||
DLStore.document = customDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Compare the deps with the previous deps
|
||||
* @param deps
|
||||
* @param prevDeps
|
||||
* @returns
|
||||
*/
|
||||
export function cached(deps, prevDeps) {
|
||||
if (!prevDeps || deps.length !== prevDeps.length) return false;
|
||||
return deps.every((dep, i) => !(dep instanceof Object) && prevDeps[i] === dep);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { type DLightHTMLAttributes } from './htmlTag';
|
||||
|
||||
// a very magical solution
|
||||
// when vscode parse ts, if it is type A<T> = B<xxx<T>>, it will show the detailed type,
|
||||
// but if type A<T> = B<xxx<T>> & xxx, it will only show alias (here is A)
|
||||
// because I don't want to expose the detailed type, so type A<T> = B<xxx<T>> & Useless
|
||||
// but if type Useless = { useless: never } will cause this type to have an additional property userless
|
||||
// so just don't add key!
|
||||
type Useless = { [key in '']: never };
|
||||
|
||||
export type DLightObject<T> = {
|
||||
[K in keyof T]-?: undefined extends T[K]
|
||||
? (value?: T[K]) => DLightObject<Omit<T, K>>
|
||||
: (value: T[K]) => DLightObject<Omit<T, K>>;
|
||||
};
|
||||
interface CustomNodeProps {
|
||||
willMount: (node: any) => void;
|
||||
didMount: (node: any) => void;
|
||||
willUnmount: (node: any) => void;
|
||||
didUnmount: (node: any) => void;
|
||||
didUpdate: (node: any, key: string, prevValue: any, currValue: any) => void;
|
||||
ref: (node: any) => void;
|
||||
elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
|
||||
forwardProps: true | undefined;
|
||||
}
|
||||
|
||||
export type ContentProp<T = object> = T & { _$idContent: true };
|
||||
|
||||
export type RemoveOptional<T> = {
|
||||
[K in keyof T]-?: T[K];
|
||||
};
|
||||
|
||||
type IsAny<T> = { _$isAny: true } extends T ? true : false;
|
||||
|
||||
export type ContentKeyName<T> = {
|
||||
[K in keyof T]: IsAny<T[K]> extends true
|
||||
? never
|
||||
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
T[K] extends ContentProp<infer _>
|
||||
? K
|
||||
: never;
|
||||
}[keyof T];
|
||||
|
||||
export type CheckContent<T> = RemoveOptional<T>[ContentKeyName<RemoveOptional<T>>];
|
||||
|
||||
type CustomClassTag<T, O> =
|
||||
ContentKeyName<RemoveOptional<O>> extends undefined
|
||||
? () => DLightObject<T>
|
||||
: undefined extends O[ContentKeyName<RemoveOptional<O>>]
|
||||
? CheckContent<O> extends ContentProp<infer U>
|
||||
? (content?: U extends unknown ? any : unknown) => DLightObject<Omit<T, ContentKeyName<RemoveOptional<O>>>>
|
||||
: never
|
||||
: CheckContent<O> extends ContentProp<infer U>
|
||||
? (content: U extends unknown ? any : unknown) => DLightObject<Omit<T, ContentKeyName<RemoveOptional<O>>>>
|
||||
: never;
|
||||
|
||||
type CustomSnippetTag<T> = T extends { content: infer U }
|
||||
? (content: U) => DLightObject<Omit<T, 'content'>>
|
||||
: T extends { content?: infer U }
|
||||
? (content?: U) => DLightObject<Omit<T, 'content'>>
|
||||
: () => DLightObject<T>;
|
||||
|
||||
type CustomTagType<T, G> = CustomClassTag<
|
||||
T & CustomNodeProps & (keyof G extends never ? object : DLightHTMLAttributes<G, object, HTMLElement>),
|
||||
T
|
||||
> &
|
||||
Useless;
|
||||
export type Typed<T = object, G = object> = CustomTagType<T, G> & Useless;
|
||||
export type SnippetTyped<T = object> = CustomSnippetTag<T> & Useless;
|
||||
|
||||
export type Pretty = any;
|
||||
|
||||
// ---- reverse
|
||||
export type UnTyped<T> = T extends Typed<infer U> ? U : never;
|
|
@ -0,0 +1,6 @@
|
|||
// ---- env
|
||||
import { DLightObject } from './compTag';
|
||||
|
||||
type AnyEnv = { _$anyEnv: true };
|
||||
|
||||
export const env: <T = AnyEnv>() => T extends AnyEnv ? any : DLightObject<T>;
|
|
@ -0,0 +1,13 @@
|
|||
interface ExpressionTag {
|
||||
willMount: (node: any) => void;
|
||||
didMount: (node: any) => void;
|
||||
willUnmount: (node: any) => void;
|
||||
didUnmount: (node: any) => void;
|
||||
didUpdate: <T>(node: any, key: string, prevValue: T, currValue: T) => void;
|
||||
elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
|
||||
ref: (node: any) => void;
|
||||
}
|
||||
|
||||
type ExpressionTagFunc = (nodes: any) => ExpressionTag;
|
||||
|
||||
export const _: ExpressionTagFunc;
|
|
@ -0,0 +1,516 @@
|
|||
export interface DLightGlobalEventHandlers {
|
||||
/**
|
||||
* Fires when the user aborts the download.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/abort_event)
|
||||
*/
|
||||
onAbort: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationcancel_event) */
|
||||
onAnimationCancel: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event) */
|
||||
onAnimationEnd: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event) */
|
||||
onAnimationIteration: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event) */
|
||||
onAnimationStart: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/auxclick_event) */
|
||||
onAuxClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/beforeinput_event) */
|
||||
onBeforeInput: ((this: GlobalEventHandlers, ev: InputEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the object loses the input focus.
|
||||
* @param ev The focus event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/blur_event)
|
||||
*/
|
||||
onBlur: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/cancel_event) */
|
||||
onCancel: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when playback is possible, but would require further buffering.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplay_event)
|
||||
*/
|
||||
onCanPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplaythrough_event) */
|
||||
onCanPlayThrough: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the contents of the object or selection have changed.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)
|
||||
*/
|
||||
onChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user clicks the left mouse button on the object
|
||||
* @param ev The mouse event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/click_event)
|
||||
*/
|
||||
onClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/close_event) */
|
||||
onClose: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user clicks the right mouse button in the client area, opening the context menu.
|
||||
* @param ev The mouse event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/contextmenu_event)
|
||||
*/
|
||||
onContextMenu: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/copy_event) */
|
||||
onCopy: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLTrackElement/cuechange_event) */
|
||||
onCueChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/cut_event) */
|
||||
onCut: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user double-clicks the object.
|
||||
* @param ev The mouse event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/dblclick_event)
|
||||
*/
|
||||
onDblClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires on the source object continuously during a drag operation.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drag_event)
|
||||
*/
|
||||
onDrag: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires on the source object when the user releases the mouse at the close of a drag operation.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragend_event)
|
||||
*/
|
||||
onDragEnd: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires on the target element when the user drags the object to a valid drop target.
|
||||
* @param ev The drag event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragenter_event)
|
||||
*/
|
||||
onDragEnter: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires on the target object when the user moves the mouse out of a valid drop target during a drag operation.
|
||||
* @param ev The drag event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragleave_event)
|
||||
*/
|
||||
onDragLeave: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires on the target element continuously while the user drags the object over a valid drop target.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragover_event)
|
||||
*/
|
||||
onDragOver: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires on the source object when the user starts to drag a text selection or selected object.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragstart_event)
|
||||
*/
|
||||
onDragStart: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drop_event) */
|
||||
onDrop: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the duration attribute is updated.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/durationchange_event)
|
||||
*/
|
||||
onDurationChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the media element is reset to its initial state.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/emptied_event)
|
||||
*/
|
||||
onEmptied: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the end of playback is reached.
|
||||
* @param ev The event
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ended_event)
|
||||
*/
|
||||
onEnded: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when an error occurs during object loading.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/error_event)
|
||||
*/
|
||||
onError: OnErrorEventHandler;
|
||||
|
||||
/**
|
||||
* Fires when the object receives focus.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/focus_event)
|
||||
*/
|
||||
onFocus: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/formdata_event) */
|
||||
onFormData: ((this: GlobalEventHandlers, ev: FormDataEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/gotpointercapture_event) */
|
||||
onGotPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event) */
|
||||
onInput: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/invalid_event) */
|
||||
onInvalid: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user presses a key.
|
||||
* @param ev The keyboard event
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keydown_event)
|
||||
*/
|
||||
onKeyDown: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user presses an alphanumeric key.
|
||||
* @param ev The event.
|
||||
* @deprecated
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keypress_event)
|
||||
*/
|
||||
onKeyPress: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user releases a key.
|
||||
* @param ev The keyboard event
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keyup_event)
|
||||
*/
|
||||
onKeyUp: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires immediately after the browser loads the object.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SVGElement/load_event)
|
||||
*/
|
||||
onLoad: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when media data is loaded at the current playback position.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadeddata_event)
|
||||
*/
|
||||
onLoadedData: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the duration and dimensions of the media have been determined.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadedmetadata_event)
|
||||
*/
|
||||
onLoadedMetadata: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when Internet Explorer begins looking for media data.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadstart_event)
|
||||
*/
|
||||
onLoadStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/lostpointercapture_event) */
|
||||
onLostPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user clicks the object with either mouse button.
|
||||
* @param ev The mouse event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousedown_event)
|
||||
*/
|
||||
onMouseDown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseenter_event) */
|
||||
onMouseEnter: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseleave_event) */
|
||||
onMouseLeave: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user moves the mouse over the object.
|
||||
* @param ev The mouse event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousemove_event)
|
||||
*/
|
||||
onMouseMove: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user moves the mouse pointer outside the boundaries of the object.
|
||||
* @param ev The mouse event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseout_event)
|
||||
*/
|
||||
onMouseOut: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user moves the mouse pointer into the object.
|
||||
* @param ev The mouse event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseover_event)
|
||||
*/
|
||||
onMouseOver: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user releases a mouse button while the mouse is over the object.
|
||||
* @param ev The mouse event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseup_event)
|
||||
*/
|
||||
onMouseUp: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/paste_event) */
|
||||
onPaste: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when playback is paused.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/pause_event)
|
||||
*/
|
||||
onPause: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the play method is requested.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/play_event)
|
||||
*/
|
||||
onPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the audio or video has started playing.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/playing_event)
|
||||
*/
|
||||
onPlaying: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointercancel_event) */
|
||||
onPointerCancel: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerdown_event) */
|
||||
onPointerDown: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerenter_event) */
|
||||
onPointerEnter: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerleave_event) */
|
||||
onPointerLeave: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointermove_event) */
|
||||
onPointerMove: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerout_event) */
|
||||
onPointerOut: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerover_event) */
|
||||
onPointerOver: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerup_event) */
|
||||
onPointerUp: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs to indicate progress while downloading media data.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/progress_event)
|
||||
*/
|
||||
onProgress: ((this: GlobalEventHandlers, ev: ProgressEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the playback rate is increased or decreased.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ratechange_event)
|
||||
*/
|
||||
onRateChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user resets a form.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/reset_event)
|
||||
*/
|
||||
onReset: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLVideoElement/resize_event) */
|
||||
onResize: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the user repositions the scroll box in the scroll bar on the object.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scroll_event)
|
||||
*/
|
||||
onScroll: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scrollend_event) */
|
||||
onScrollEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/securitypolicyviolation_event) */
|
||||
onSecurityPolicyViolation: ((this: GlobalEventHandlers, ev: SecurityPolicyViolationEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the seek operation ends.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeked_event)
|
||||
*/
|
||||
onSeeked: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the current playback position is moved.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeking_event)
|
||||
*/
|
||||
onSeeking: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Fires when the current selection changes.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/select_event)
|
||||
*/
|
||||
onSelect: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/selectionchange_event) */
|
||||
onSelectionChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/selectstart_event) */
|
||||
onSelectStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLSlotElement/slotchange_event) */
|
||||
onSlotChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the download has stopped.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/stalled_event)
|
||||
*/
|
||||
onStalled: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/submit_event) */
|
||||
onSubmit: ((this: GlobalEventHandlers, ev: SubmitEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs if the load operation has been intentionally halted.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/suspend_event)
|
||||
*/
|
||||
onSuspend: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs to indicate the current playback position.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/timeupdate_event)
|
||||
*/
|
||||
onTimeUpdate: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDetailsElement/toggle_event) */
|
||||
onToggle: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitioncancel_event) */
|
||||
onTransitionCancel: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event) */
|
||||
onTransitionEnd: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionrun_event) */
|
||||
onTransitionRun: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionstart_event) */
|
||||
onTransitionStart: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when the volume is changed, or playback is muted or unmuted.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/volumechange_event)
|
||||
*/
|
||||
onVolumeChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* Occurs when playback stops because the next frame of a video resource is not available.
|
||||
* @param ev The event.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/waiting_event)
|
||||
*/
|
||||
onWaiting: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* @deprecated This is a legacy alias of `onAnimationEnd`.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event)
|
||||
*/
|
||||
onWebkitAnimationEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* @deprecated This is a legacy alias of `onAnimationIteration`.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event)
|
||||
*/
|
||||
onWebkitAnimationIteration: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* @deprecated This is a legacy alias of `onAnimationStart`.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event)
|
||||
*/
|
||||
onWebkitAnimationStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/**
|
||||
* @deprecated This is a legacy alias of `onTransitionEnd`.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event)
|
||||
*/
|
||||
onWebkitTransitionEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/wheel_event) */
|
||||
onWheel: ((this: GlobalEventHandlers, ev: WheelEvent) => any) | null;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { type Properties } from 'csstype';
|
||||
|
||||
// ---- Used to determine whether X and Y are equal, return A if equal, otherwise B
|
||||
type IfEquals<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
|
||||
|
||||
export type OmitIndexSignature<ObjectType> = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
[KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? never : KeyType]: ObjectType[KeyType];
|
||||
};
|
||||
|
||||
// ---- For each key, check whether there is readonly, if there is, return never, and then Pick out is not never
|
||||
type WritableKeysOf<T> = {
|
||||
[P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never>;
|
||||
}[keyof T];
|
||||
type RemoveReadOnly<T> = Pick<T, WritableKeysOf<T>>;
|
||||
|
||||
// ---- Delete all functions
|
||||
type OmitFunction<T> = Omit<T, { [K in keyof T]: T[K] extends (...args: any) => any ? K : never }[keyof T]>;
|
||||
|
||||
type OmitFuncAndReadOnly<T> = RemoveReadOnly<OmitFunction<OmitIndexSignature<T>>>;
|
||||
|
||||
// ---- properties
|
||||
type OmitFuncAndReadOnlyProperty<G> = Omit<OmitFuncAndReadOnly<G>, 'className' | 'htmlFor' | 'style' | 'innerText'>;
|
||||
|
||||
type CustomCSSProperties = {
|
||||
[Key in `--${string}`]: string | number;
|
||||
};
|
||||
|
||||
export type HTMLAttributes<T> = OmitFuncAndReadOnlyProperty<T> & {
|
||||
style: Properties & CustomCSSProperties;
|
||||
class: string;
|
||||
for: string;
|
||||
};
|
|
@ -0,0 +1,236 @@
|
|||
import type { DLightGlobalEventHandlers } from './event';
|
||||
import type { OmitIndexSignature, HTMLAttributes } from './htmlElement';
|
||||
|
||||
// ---- If there is an event(start with on), remove it
|
||||
export type PropertyWithEvent<G> = Omit<
|
||||
G,
|
||||
{
|
||||
[K in keyof G]: K extends `on${string}` ? K : never;
|
||||
}[keyof G]
|
||||
> &
|
||||
DLightGlobalEventHandlers;
|
||||
|
||||
interface DLightHtmlProps<El> {
|
||||
ref: El | ((holder: El) => void) | undefined;
|
||||
prop: Record<string, string | number | boolean>;
|
||||
attr: Record<string, string>;
|
||||
dataset: Record<string, string>;
|
||||
forwardProps: true | undefined;
|
||||
willMount: (el: El) => void;
|
||||
didMount: (el: El) => void;
|
||||
willUnmount: (el: El) => void;
|
||||
didUnmount: (el: El) => void;
|
||||
didUpdate: <T>(el: El, key: string, prevValue: T, currValue: T) => void;
|
||||
}
|
||||
|
||||
export type DLightHTMLAttributes<T, G, El> = DLightHtmlProps<El> & HTMLAttributes<T> & G;
|
||||
|
||||
export type DLightHTMLAttributesFunc<T, G, El> = {
|
||||
[K in keyof DLightHTMLAttributes<T, G, El>]: (
|
||||
value?: DLightHTMLAttributes<T, G, El>[K]
|
||||
) => Omit<DLightHTMLAttributesFunc<T, G, El>, K>;
|
||||
};
|
||||
|
||||
export type DLightHtmlTagFunc<T = HTMLElement, G = object> = (
|
||||
innerText?: string | number | ((View: never) => void)
|
||||
) => DLightHTMLAttributesFunc<PropertyWithEvent<OmitIndexSignature<T>>, G, T>;
|
||||
|
||||
export const a: DLightHtmlTagFunc<HTMLAnchorElement>;
|
||||
export const abbr: DLightHtmlTagFunc;
|
||||
export const address: DLightHtmlTagFunc;
|
||||
export const area: DLightHtmlTagFunc<HTMLAreaElement>;
|
||||
export const article: DLightHtmlTagFunc;
|
||||
export const aside: DLightHtmlTagFunc;
|
||||
export const audio: DLightHtmlTagFunc<HTMLAudioElement>;
|
||||
export const b: DLightHtmlTagFunc;
|
||||
export const base: DLightHtmlTagFunc<HTMLBaseElement>;
|
||||
export const bdi: DLightHtmlTagFunc;
|
||||
export const bdo: DLightHtmlTagFunc;
|
||||
export const blockquote: DLightHtmlTagFunc<HTMLQuoteElement>;
|
||||
export const body: DLightHtmlTagFunc<HTMLBodyElement>;
|
||||
export const br: DLightHtmlTagFunc<HTMLBRElement>;
|
||||
export const button: DLightHtmlTagFunc<HTMLButtonElement>;
|
||||
export const canvas: DLightHtmlTagFunc<HTMLCanvasElement>;
|
||||
export const caption: DLightHtmlTagFunc<HTMLTableCaptionElement>;
|
||||
export const cite: DLightHtmlTagFunc;
|
||||
export const code: DLightHtmlTagFunc;
|
||||
export const col: DLightHtmlTagFunc<HTMLTableColElement>;
|
||||
export const colgroup: DLightHtmlTagFunc<HTMLTableColElement>;
|
||||
export const data: DLightHtmlTagFunc<HTMLDataElement>;
|
||||
export const datalist: DLightHtmlTagFunc<HTMLDataListElement>;
|
||||
export const dd: DLightHtmlTagFunc;
|
||||
export const del: DLightHtmlTagFunc<HTMLModElement>;
|
||||
export const details: DLightHtmlTagFunc<HTMLDetailsElement>;
|
||||
export const dfn: DLightHtmlTagFunc;
|
||||
export const dialog: DLightHtmlTagFunc<HTMLDialogElement>;
|
||||
export const div: DLightHtmlTagFunc<HTMLDivElement>;
|
||||
export const dl: DLightHtmlTagFunc<HTMLDListElement>;
|
||||
export const dt: DLightHtmlTagFunc;
|
||||
export const em: DLightHtmlTagFunc;
|
||||
export const embed: DLightHtmlTagFunc<HTMLEmbedElement>;
|
||||
export const fieldset: DLightHtmlTagFunc<HTMLFieldSetElement>;
|
||||
export const figcaption: DLightHtmlTagFunc;
|
||||
export const figure: DLightHtmlTagFunc;
|
||||
export const footer: DLightHtmlTagFunc;
|
||||
export const form: DLightHtmlTagFunc<HTMLFormElement>;
|
||||
export const h1: DLightHtmlTagFunc<HTMLHeadingElement>;
|
||||
export const h2: DLightHtmlTagFunc<HTMLHeadingElement>;
|
||||
export const h3: DLightHtmlTagFunc<HTMLHeadingElement>;
|
||||
export const h4: DLightHtmlTagFunc<HTMLHeadingElement>;
|
||||
export const h5: DLightHtmlTagFunc<HTMLHeadingElement>;
|
||||
export const h6: DLightHtmlTagFunc<HTMLHeadingElement>;
|
||||
export const head: DLightHtmlTagFunc<HTMLHeadElement>;
|
||||
export const header: DLightHtmlTagFunc;
|
||||
export const hgroup: DLightHtmlTagFunc;
|
||||
export const hr: DLightHtmlTagFunc<HTMLHRElement>;
|
||||
export const html: DLightHtmlTagFunc<HTMLHtmlElement>;
|
||||
export const i: DLightHtmlTagFunc;
|
||||
export const iframe: DLightHtmlTagFunc<HTMLIFrameElement>;
|
||||
export const img: DLightHtmlTagFunc<HTMLImageElement>;
|
||||
export const input: DLightHtmlTagFunc<HTMLInputElement>;
|
||||
export const ins: DLightHtmlTagFunc<HTMLModElement>;
|
||||
export const kbd: DLightHtmlTagFunc;
|
||||
export const label: DLightHtmlTagFunc<HTMLLabelElement>;
|
||||
export const legend: DLightHtmlTagFunc<HTMLLegendElement>;
|
||||
export const li: DLightHtmlTagFunc<HTMLLIElement>;
|
||||
export const link: DLightHtmlTagFunc<HTMLLinkElement>;
|
||||
export const main: DLightHtmlTagFunc;
|
||||
export const map: DLightHtmlTagFunc<HTMLMapElement>;
|
||||
export const mark: DLightHtmlTagFunc;
|
||||
export const menu: DLightHtmlTagFunc<HTMLMenuElement>;
|
||||
export const meta: DLightHtmlTagFunc<HTMLMetaElement>;
|
||||
export const meter: DLightHtmlTagFunc<HTMLMeterElement>;
|
||||
export const nav: DLightHtmlTagFunc;
|
||||
export const noscript: DLightHtmlTagFunc;
|
||||
export const object: DLightHtmlTagFunc<HTMLObjectElement>;
|
||||
export const ol: DLightHtmlTagFunc<HTMLOListElement>;
|
||||
export const optgroup: DLightHtmlTagFunc<HTMLOptGroupElement>;
|
||||
export const option: DLightHtmlTagFunc<HTMLOptionElement>;
|
||||
export const output: DLightHtmlTagFunc<HTMLOutputElement>;
|
||||
export const p: DLightHtmlTagFunc<HTMLParagraphElement>;
|
||||
export const picture: DLightHtmlTagFunc<HTMLPictureElement>;
|
||||
export const pre: DLightHtmlTagFunc<HTMLPreElement>;
|
||||
export const progress: DLightHtmlTagFunc<HTMLProgressElement>;
|
||||
export const q: DLightHtmlTagFunc<HTMLQuoteElement>;
|
||||
export const rp: DLightHtmlTagFunc;
|
||||
export const rt: DLightHtmlTagFunc;
|
||||
export const ruby: DLightHtmlTagFunc;
|
||||
export const s: DLightHtmlTagFunc;
|
||||
export const samp: DLightHtmlTagFunc;
|
||||
export const script: DLightHtmlTagFunc<HTMLScriptElement>;
|
||||
export const section: DLightHtmlTagFunc;
|
||||
export const select: DLightHtmlTagFunc<HTMLSelectElement>;
|
||||
export const slot: DLightHtmlTagFunc<HTMLSlotElement>;
|
||||
export const small: DLightHtmlTagFunc;
|
||||
export const source: DLightHtmlTagFunc<HTMLSourceElement>;
|
||||
export const span: DLightHtmlTagFunc<HTMLSpanElement>;
|
||||
export const strong: DLightHtmlTagFunc;
|
||||
export const style: DLightHtmlTagFunc<HTMLStyleElement>;
|
||||
export const sub: DLightHtmlTagFunc;
|
||||
export const summary: DLightHtmlTagFunc;
|
||||
export const sup: DLightHtmlTagFunc;
|
||||
export const table: DLightHtmlTagFunc<HTMLTableElement>;
|
||||
export const tbody: DLightHtmlTagFunc<HTMLTableSectionElement>;
|
||||
export const td: DLightHtmlTagFunc<HTMLTableCellElement>;
|
||||
export const template: DLightHtmlTagFunc<HTMLTemplateElement>;
|
||||
export const textarea: DLightHtmlTagFunc<HTMLTextAreaElement>;
|
||||
export const tfoot: DLightHtmlTagFunc<HTMLTableSectionElement>;
|
||||
export const th: DLightHtmlTagFunc<HTMLTableCellElement>;
|
||||
export const thead: DLightHtmlTagFunc<HTMLTableSectionElement>;
|
||||
export const time: DLightHtmlTagFunc<HTMLTimeElement>;
|
||||
export const title: DLightHtmlTagFunc<HTMLTitleElement>;
|
||||
export const tr: DLightHtmlTagFunc<HTMLTableRowElement>;
|
||||
export const track: DLightHtmlTagFunc<HTMLTrackElement>;
|
||||
export const u: DLightHtmlTagFunc;
|
||||
export const ul: DLightHtmlTagFunc<HTMLUListElement>;
|
||||
export const var_: DLightHtmlTagFunc;
|
||||
export const video: DLightHtmlTagFunc<HTMLVideoElement>;
|
||||
export const wbr: DLightHtmlTagFunc;
|
||||
export const acronym: DLightHtmlTagFunc;
|
||||
export const applet: DLightHtmlTagFunc<HTMLUnknownElement>;
|
||||
export const basefont: DLightHtmlTagFunc;
|
||||
export const bgsound: DLightHtmlTagFunc<HTMLUnknownElement>;
|
||||
export const big: DLightHtmlTagFunc;
|
||||
export const blink: DLightHtmlTagFunc<HTMLUnknownElement>;
|
||||
export const center: DLightHtmlTagFunc;
|
||||
export const dir: DLightHtmlTagFunc<HTMLDirectoryElement>;
|
||||
export const font: DLightHtmlTagFunc<HTMLFontElement>;
|
||||
export const frame: DLightHtmlTagFunc<HTMLFrameElement>;
|
||||
export const frameset: DLightHtmlTagFunc<HTMLFrameSetElement>;
|
||||
export const isindex: DLightHtmlTagFunc<HTMLUnknownElement>;
|
||||
export const keygen: DLightHtmlTagFunc<HTMLUnknownElement>;
|
||||
export const listing: DLightHtmlTagFunc<HTMLPreElement>;
|
||||
export const marquee: DLightHtmlTagFunc<HTMLMarqueeElement>;
|
||||
export const menuitem: DLightHtmlTagFunc;
|
||||
export const multicol: DLightHtmlTagFunc<HTMLUnknownElement>;
|
||||
export const nextid: DLightHtmlTagFunc<HTMLUnknownElement>;
|
||||
export const nobr: DLightHtmlTagFunc;
|
||||
export const noembed: DLightHtmlTagFunc;
|
||||
export const noframes: DLightHtmlTagFunc;
|
||||
export const param: DLightHtmlTagFunc<HTMLParamElement>;
|
||||
export const plaintext: DLightHtmlTagFunc;
|
||||
export const rb: DLightHtmlTagFunc;
|
||||
export const rtc: DLightHtmlTagFunc;
|
||||
export const spacer: DLightHtmlTagFunc<HTMLUnknownElement>;
|
||||
export const strike: DLightHtmlTagFunc;
|
||||
export const tt: DLightHtmlTagFunc;
|
||||
export const xmp: DLightHtmlTagFunc<HTMLPreElement>;
|
||||
export const animate: DLightHtmlTagFunc<SVGAnimateElement>;
|
||||
export const animateMotion: DLightHtmlTagFunc<SVGAnimateMotionElement>;
|
||||
export const animateTransform: DLightHtmlTagFunc<SVGAnimateTransformElement>;
|
||||
export const circle: DLightHtmlTagFunc<SVGCircleElement>;
|
||||
export const clipPath: DLightHtmlTagFunc<SVGClipPathElement>;
|
||||
export const defs: DLightHtmlTagFunc<SVGDefsElement>;
|
||||
export const desc: DLightHtmlTagFunc<SVGDescElement>;
|
||||
export const ellipse: DLightHtmlTagFunc<SVGEllipseElement>;
|
||||
export const feBlend: DLightHtmlTagFunc<SVGFEBlendElement>;
|
||||
export const feColorMatrix: DLightHtmlTagFunc<SVGFEColorMatrixElement>;
|
||||
export const feComponentTransfer: DLightHtmlTagFunc<SVGFEComponentTransferElement>;
|
||||
export const feComposite: DLightHtmlTagFunc<SVGFECompositeElement>;
|
||||
export const feConvolveMatrix: DLightHtmlTagFunc<SVGFEConvolveMatrixElement>;
|
||||
export const feDiffuseLighting: DLightHtmlTagFunc<SVGFEDiffuseLightingElement>;
|
||||
export const feDisplacementMap: DLightHtmlTagFunc<SVGFEDisplacementMapElement>;
|
||||
export const feDistantLight: DLightHtmlTagFunc<SVGFEDistantLightElement>;
|
||||
export const feDropShadow: DLightHtmlTagFunc<SVGFEDropShadowElement>;
|
||||
export const feFlood: DLightHtmlTagFunc<SVGFEFloodElement>;
|
||||
export const feFuncA: DLightHtmlTagFunc<SVGFEFuncAElement>;
|
||||
export const feFuncB: DLightHtmlTagFunc<SVGFEFuncBElement>;
|
||||
export const feFuncG: DLightHtmlTagFunc<SVGFEFuncGElement>;
|
||||
export const feFuncR: DLightHtmlTagFunc<SVGFEFuncRElement>;
|
||||
export const feGaussianBlur: DLightHtmlTagFunc<SVGFEGaussianBlurElement>;
|
||||
export const feImage: DLightHtmlTagFunc<SVGFEImageElement>;
|
||||
export const feMerge: DLightHtmlTagFunc<SVGFEMergeElement>;
|
||||
export const feMergeNode: DLightHtmlTagFunc<SVGFEMergeNodeElement>;
|
||||
export const feMorphology: DLightHtmlTagFunc<SVGFEMorphologyElement>;
|
||||
export const feOffset: DLightHtmlTagFunc<SVGFEOffsetElement>;
|
||||
export const fePointLight: DLightHtmlTagFunc<SVGFEPointLightElement>;
|
||||
export const feSpecularLighting: DLightHtmlTagFunc<SVGFESpecularLightingElement>;
|
||||
export const feSpotLight: DLightHtmlTagFunc<SVGFESpotLightElement>;
|
||||
export const feTile: DLightHtmlTagFunc<SVGFETileElement>;
|
||||
export const feTurbulence: DLightHtmlTagFunc<SVGFETurbulenceElement>;
|
||||
export const filter: DLightHtmlTagFunc<SVGFilterElement>;
|
||||
export const foreignObject: DLightHtmlTagFunc<SVGForeignObjectElement>;
|
||||
export const g: DLightHtmlTagFunc<SVGGElement>;
|
||||
export const image: DLightHtmlTagFunc<SVGImageElement>;
|
||||
export const line: DLightHtmlTagFunc<SVGLineElement>;
|
||||
export const linearGradient: DLightHtmlTagFunc<SVGLinearGradientElement>;
|
||||
export const marker: DLightHtmlTagFunc<SVGMarkerElement>;
|
||||
export const mask: DLightHtmlTagFunc<SVGMaskElement>;
|
||||
export const metadata: DLightHtmlTagFunc<SVGMetadataElement>;
|
||||
export const mpath: DLightHtmlTagFunc<SVGMPathElement>;
|
||||
export const path: DLightHtmlTagFunc<SVGPathElement>;
|
||||
export const pattern: DLightHtmlTagFunc<SVGPatternElement>;
|
||||
export const polygon: DLightHtmlTagFunc<SVGPolygonElement>;
|
||||
export const polyline: DLightHtmlTagFunc<SVGPolylineElement>;
|
||||
export const radialGradient: DLightHtmlTagFunc<SVGRadialGradientElement>;
|
||||
export const rect: DLightHtmlTagFunc<SVGRectElement>;
|
||||
export const set: DLightHtmlTagFunc<SVGSetElement>;
|
||||
export const stop: DLightHtmlTagFunc<SVGStopElement>;
|
||||
export const svg: DLightHtmlTagFunc<SVGSVGElement>;
|
||||
export const switch_: DLightHtmlTagFunc<SVGSwitchElement>;
|
||||
export const symbol: DLightHtmlTagFunc<SVGSymbolElement>;
|
||||
export const text: DLightHtmlTagFunc<SVGTextElement>;
|
||||
export const textPath: DLightHtmlTagFunc<SVGTextPathElement>;
|
||||
export const tspan: DLightHtmlTagFunc<SVGTSpanElement>;
|
||||
// export const use: DLightHtmlTagFunc<SVGUseElement>
|
||||
export const view: DLightHtmlTagFunc<SVGViewElement>;
|
|
@ -0,0 +1,41 @@
|
|||
import { type Typed } from './compTag';
|
||||
import { type DLightHtmlTagFunc } from './htmlTag';
|
||||
export { type Properties as CSSProperties } from 'csstype';
|
||||
|
||||
export const comp: <T>(tag: T) => object extends T ? any : Typed<T>;
|
||||
export const tag: (tag: any) => DLightHtmlTagFunc;
|
||||
|
||||
export { _ } from './expressionTag';
|
||||
export * from './htmlTag';
|
||||
export * from './compTag';
|
||||
export * from './envTag';
|
||||
export * from './model';
|
||||
export const Static: any;
|
||||
export const Children: any;
|
||||
export const Content: any;
|
||||
export const Prop: any;
|
||||
export const Env: any;
|
||||
export const Watch: any;
|
||||
export const ForwardProps: any;
|
||||
export const Main: any;
|
||||
export const App: any;
|
||||
export const Mount: (idOrEl: string | HTMLElement) => any;
|
||||
|
||||
// ---- With actual value
|
||||
export function render(idOrEl: string | HTMLElement, DL: any): void;
|
||||
export function manual<T>(callback: () => T, _deps?: any[]): T;
|
||||
export function escape<T>(arg: T): T;
|
||||
export function setGlobal(globalObj: any): void;
|
||||
export function setDocument(customDocument: any): void;
|
||||
export const $: typeof escape;
|
||||
export const View: any;
|
||||
export const Snippet: any;
|
||||
export const Model: any;
|
||||
export const update: any;
|
||||
export const required: any;
|
||||
export function insertChildren<T>(parent: T, children: DLightViewProp): void;
|
||||
|
||||
// ---- View types
|
||||
export type DLightViewComp<T = any> = Typed<T>;
|
||||
export type DLightViewProp = (View: any) => void;
|
||||
export type DLightViewLazy<T = any> = () => Promise<{ default: T }>;
|
|
@ -0,0 +1,22 @@
|
|||
import { ContentKeyName, ContentProp } from './compTag';
|
||||
|
||||
type RemoveDLightInternal<T, Props> = Omit<T, 'willMount' | 'didMount' | 'didUpdate' | 'willUnmount' | keyof Props>;
|
||||
|
||||
export type Modeling<Model, Props = object> = (props: Props) => Model;
|
||||
|
||||
type GetProps<T> = keyof T extends never ? never : ContentKeyName<T> extends undefined ? T : Omit<T, ContentKeyName<T>>;
|
||||
|
||||
type GetContent<T> =
|
||||
ContentKeyName<T> extends undefined ? never : T[ContentKeyName<T>] extends ContentProp<infer U> ? U : never;
|
||||
|
||||
export const use: <M>(
|
||||
model: M,
|
||||
// @ts-expect-error Model should be a function
|
||||
props?: GetProps<Parameters<M>[0]>,
|
||||
// @ts-expect-error Model should be a function
|
||||
content?: GetContent<Parameters<M>[0]>
|
||||
// @ts-expect-error Model should be a function
|
||||
) => RemoveDLightInternal<ReturnType<M>, Parameters<M>[0]>;
|
||||
|
||||
// @ts-expect-error Model should be a function
|
||||
export type ModelType<T> = RemoveDLightInternal<ReturnType<T>, Parameters<T>[0]>;
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
|
@ -109,13 +109,16 @@ function genConfig(mode) {
|
|||
function genJSXRuntimeConfig(mode) {
|
||||
return {
|
||||
input: path.resolve(libDir, 'src', 'jsx-runtime.ts'),
|
||||
output: [{
|
||||
output: [
|
||||
{
|
||||
file: outputResolve('jsx-runtime.js'),
|
||||
format: 'cjs',
|
||||
}, {
|
||||
},
|
||||
{
|
||||
file: outputResolve('jsx-runtime.esm-browser.js'),
|
||||
format: 'esm',
|
||||
}],
|
||||
},
|
||||
],
|
||||
plugins: [...getBasicPlugins(mode)],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,7 +31,11 @@ export interface IObserver {
|
|||
clearByVNode: (vNode: any) => void;
|
||||
}
|
||||
|
||||
export type StoreConfig<S extends Record<string, unknown>, A extends UserActions<S>, C extends UserComputedValues<S>> = {
|
||||
export type StoreConfig<
|
||||
S extends Record<string, unknown>,
|
||||
A extends UserActions<S>,
|
||||
C extends UserComputedValues<S>,
|
||||
> = {
|
||||
id?: string;
|
||||
state?: S;
|
||||
actions?: A;
|
||||
|
@ -45,7 +49,11 @@ export type UserActions<S extends Record<string, unknown>> = {
|
|||
[K: string]: ActionFunction<S>;
|
||||
};
|
||||
|
||||
export type ActionFunction<S extends Record<string, unknown>> = (this: StoreObj<S, any, any>, state: S, ...args: any[]) => any;
|
||||
export type ActionFunction<S extends Record<string, unknown>> = (
|
||||
this: StoreObj<S, any, any>,
|
||||
state: S,
|
||||
...args: any[]
|
||||
) => any;
|
||||
|
||||
export type StoreActions<S extends Record<string, unknown>, A extends UserActions<S>> = {
|
||||
[K in keyof A]: Action<A[K], S>;
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "@inula/class-transformer",
|
||||
"version": "0.0.0",
|
||||
"description": "Inula view generator",
|
||||
"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/traverse": "^7.24.1",
|
||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/babel__generator": "^7.6.8",
|
||||
"@types/babel__traverse": "^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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { Option } from './types';
|
||||
import type { ConfigAPI, TransformOptions } from '@babel/core';
|
||||
import transformer from './plugin';
|
||||
|
||||
export default function (_: ConfigAPI, options: Option): TransformOptions {
|
||||
return {
|
||||
plugins: [
|
||||
['@babel/plugin-syntax-jsx'],
|
||||
['@babel/plugin-syntax-typescript', { isTSX: true }],
|
||||
[transformer, options],
|
||||
],
|
||||
};
|
||||
}
|
||||
export const plugin = transformer;
|
||||
|
||||
export type { Option };
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { PluginObj } from '@babel/core';
|
||||
import { Option } from './types';
|
||||
import * as babel from '@babel/core';
|
||||
import { PluginProvider } from './pluginProvider';
|
||||
|
||||
export default function (api: typeof babel, options: Option): PluginObj {
|
||||
const pluginProvider = new PluginProvider(api, options);
|
||||
|
||||
return {
|
||||
name: 'zouyu-2',
|
||||
visitor: {
|
||||
FunctionDeclaration(path) {
|
||||
pluginProvider.functionDeclarationVisitor(path);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,318 @@
|
|||
import { type types as t, NodePath } from '@babel/core';
|
||||
import * as babel from '@babel/core';
|
||||
import { Option } from './types';
|
||||
import type { Scope } from '@babel/traverse';
|
||||
|
||||
function replaceFnWithClass(path: NodePath<t.FunctionDeclaration>, classTransformer: ClassComponentTransformer) {
|
||||
const originalName = path.node.id.name;
|
||||
const tempName = path.node.id.name + 'Temp';
|
||||
const classComp = classTransformer.genClassComponent(tempName);
|
||||
path.replaceWith(classComp);
|
||||
path.scope.rename(tempName, originalName);
|
||||
}
|
||||
|
||||
export class PluginProvider {
|
||||
// ---- Plugin Level ----
|
||||
private readonly babelApi: typeof babel;
|
||||
private readonly t: typeof t;
|
||||
|
||||
private programNode: t.Program | undefined;
|
||||
|
||||
constructor(babelApi: typeof babel, options: Option) {
|
||||
this.babelApi = babelApi;
|
||||
this.t = babelApi.types;
|
||||
}
|
||||
|
||||
functionDeclarationVisitor(path: NodePath<t.FunctionDeclaration>): void {
|
||||
// find Component function by:
|
||||
// 1. has JSXElement as return value
|
||||
// 2. name is capitalized
|
||||
if (path.node.id?.name[0] !== path.node.id?.name[0].toUpperCase()) return;
|
||||
const returnStatement = path.node.body.body.find(n => this.t.isReturnStatement(n)) as t.ReturnStatement;
|
||||
if (!returnStatement) return;
|
||||
if (!(this.t.isJSXElement(returnStatement.argument) || this.t.isJSXFragment(returnStatement.argument))) return;
|
||||
const classTransformer = new ClassComponentTransformer(this.babelApi, path);
|
||||
// transform the parameters to props
|
||||
const params = path.node.params;
|
||||
const props = params[0];
|
||||
classTransformer.transformProps(props);
|
||||
// iterate the function body orderly
|
||||
const body = path.node.body.body;
|
||||
body.forEach((node, idx) => {
|
||||
if (this.t.isVariableDeclaration(node)) {
|
||||
classTransformer.transformStateDeclaration(node);
|
||||
return;
|
||||
}
|
||||
// handle method
|
||||
if (this.t.isFunctionDeclaration(node)) {
|
||||
classTransformer.transformMethods(node);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle watch
|
||||
if (classTransformer.shouldTransformWatch(node)) {
|
||||
// transform the watch statement to watch method
|
||||
classTransformer.transformWatch(node);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle return statement
|
||||
if (this.t.isReturnStatement(node)) {
|
||||
// handle early return
|
||||
if (idx !== body.length - 1) {
|
||||
// transform the return statement to render method
|
||||
// TODO: handle early return
|
||||
throw new Error('Early return is not supported yet.');
|
||||
}
|
||||
// transform the return statement to render method
|
||||
classTransformer.transformRenderMethod(node);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// replace the function declaration with class declaration
|
||||
replaceFnWithClass(path, classTransformer);
|
||||
}
|
||||
}
|
||||
|
||||
type ToWatchNode =
|
||||
| t.ExpressionStatement
|
||||
| t.ForStatement
|
||||
| t.WhileStatement
|
||||
| t.IfStatement
|
||||
| t.SwitchStatement
|
||||
| t.TryStatement;
|
||||
|
||||
class ClassComponentTransformer {
|
||||
properties: (t.ClassProperty | t.ClassMethod)[] = [];
|
||||
private readonly babelApi: typeof babel;
|
||||
private readonly t: typeof t;
|
||||
private readonly functionScope: Scope;
|
||||
|
||||
valueWrapper(node) {
|
||||
return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)]));
|
||||
}
|
||||
|
||||
addProperty(prop: t.ClassProperty | t.ClassMethod, name?: string) {
|
||||
this.properties.push(prop);
|
||||
if (name) {
|
||||
// replace the variable in scope to process the variable in class scope
|
||||
// e.g. replace () => count++ to () => this.count++
|
||||
// TODO: search for better solution
|
||||
this.functionScope.rename(name, `this.${name}`);
|
||||
this.functionScope.path.traverse({
|
||||
Identifier: path => {
|
||||
if (path.node.name === `this.${name}`) {
|
||||
path.replaceWith(this.t.memberExpression(this.t.thisExpression(), this.t.identifier(name)));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
constructor(babelApi: typeof babel, fnNode: NodePath<t.FunctionDeclaration>) {
|
||||
this.babelApi = babelApi;
|
||||
this.t = babelApi.types;
|
||||
// get the function body scope
|
||||
this.functionScope = fnNode.scope;
|
||||
}
|
||||
|
||||
// transform function component to class component extends View
|
||||
genClassComponent(name: string) {
|
||||
return this.t.classDeclaration(
|
||||
this.t.identifier(name),
|
||||
this.t.identifier('View'),
|
||||
this.t.classBody(this.properties),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform state declaration to class property
|
||||
* if the variable is declared with `let` or `const`, it should be transformed to class property
|
||||
* @param node
|
||||
*/
|
||||
transformStateDeclaration(node: t.VariableDeclaration) {
|
||||
// iterate the declarations
|
||||
node.declarations.forEach(declaration => {
|
||||
const id = declaration.id;
|
||||
// handle destructuring
|
||||
if (this.t.isObjectPattern(id)) {
|
||||
return this.transformPropsDestructuring(id);
|
||||
} else if (this.t.isArrayPattern(id)) {
|
||||
// TODO: handle array destructuring
|
||||
} else if (this.t.isIdentifier(id)) {
|
||||
// clone the id
|
||||
const cloneId = this.t.cloneNode(id);
|
||||
this.addProperty(this.t.classProperty(cloneId, declaration.init), id.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform render method to Body method
|
||||
* The Body method should return the original return statement
|
||||
* @param node
|
||||
*/
|
||||
transformRenderMethod(node: t.ReturnStatement) {
|
||||
const body = this.t.classMethod(
|
||||
'method',
|
||||
this.t.identifier('Body'),
|
||||
[],
|
||||
this.t.blockStatement([node]),
|
||||
false,
|
||||
false
|
||||
);
|
||||
this.addProperty(body, 'Body');
|
||||
}
|
||||
|
||||
transformLifeCycle() {}
|
||||
|
||||
transformComputed() {}
|
||||
|
||||
transformMethods(node: t.FunctionDeclaration) {
|
||||
// transform the function declaration to class method
|
||||
const methodName = node.id?.name;
|
||||
if (!methodName) return;
|
||||
const method = this.t.classMethod(
|
||||
'method',
|
||||
this.t.identifier(methodName),
|
||||
node.params,
|
||||
node.body,
|
||||
node.generator,
|
||||
node.async
|
||||
);
|
||||
this.addProperty(method, methodName);
|
||||
}
|
||||
|
||||
transformProps(param: t.Identifier | t.RestElement | t.Pattern) {
|
||||
if (!param) return;
|
||||
// handle destructuring
|
||||
if (this.isObjDestructuring(param)) {
|
||||
this.transformPropsDestructuring(param);
|
||||
return;
|
||||
}
|
||||
if (this.t.isIdentifier(param)) {
|
||||
// TODO: handle props identifier
|
||||
return;
|
||||
}
|
||||
throw new Error('Unsupported props type, please use object destructuring or identifier.');
|
||||
}
|
||||
|
||||
// transform node to method with watch decorator
|
||||
transformWatch(node: ToWatchNode) {
|
||||
const id = this.functionScope.generateUidIdentifier('watch');
|
||||
const method = this.t.classMethod('method', id, [], this.t.blockStatement([node]), false, false);
|
||||
method.decorators = [this.t.decorator(this.t.identifier('Watch'))];
|
||||
this.addProperty(method);
|
||||
}
|
||||
|
||||
private isObjDestructuring(param: t.Identifier | t.RestElement | t.Pattern): param is t.ObjectPattern {
|
||||
return this.t.isObjectPattern(param);
|
||||
}
|
||||
|
||||
private transformPropsDestructuring(param: t.ObjectPattern) {
|
||||
const propNames: t.Identifier[] = [];
|
||||
param.properties.forEach(prop => {
|
||||
if (this.t.isObjectProperty(prop)) {
|
||||
const key = prop.key;
|
||||
if (this.t.isIdentifier(key)) {
|
||||
if (this.t.isAssignmentPattern(prop.value)) {
|
||||
// handle default value
|
||||
const defaultValue = prop.value.right;
|
||||
this.addProp(key, defaultValue);
|
||||
propNames.push(key);
|
||||
return;
|
||||
} else if (this.t.isIdentifier(prop.value)) {
|
||||
// handle simple destructuring
|
||||
this.addProp(key, undefined, prop.value.name === 'children');
|
||||
propNames.push(key);
|
||||
return;
|
||||
} else if (this.t.isObjectPattern(prop.value)) {
|
||||
// TODO: handle nested destructuring
|
||||
this.transformPropsDestructuring(prop.value);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// handle default value
|
||||
if (this.t.isAssignmentPattern(prop.value)) {
|
||||
const defaultValue = prop.value.right;
|
||||
const propName = prop.value.left;
|
||||
if (this.t.isIdentifier(propName)) {
|
||||
this.addProp(propName, defaultValue);
|
||||
propNames.push(propName);
|
||||
}
|
||||
// TODO: handle nested destructuring
|
||||
return;
|
||||
}
|
||||
throw new Error('Unsupported props destructuring, please use simple object destructuring.');
|
||||
} else {
|
||||
// TODO: handle rest element
|
||||
}
|
||||
});
|
||||
|
||||
return propNames;
|
||||
}
|
||||
|
||||
// add prop to class, like @prop name = '';
|
||||
private addProp(key: t.Identifier, defaultValue?: t.Expression, isChildren = false) {
|
||||
// clone the key to avoid reference issue
|
||||
const id = this.t.cloneNode(key);
|
||||
this.addProperty(
|
||||
this.t.classProperty(
|
||||
id,
|
||||
defaultValue ?? undefined,
|
||||
undefined,
|
||||
// use prop decorator
|
||||
[this.t.decorator(this.t.identifier(isChildren ? 'Children' : 'Prop'))],
|
||||
undefined,
|
||||
false
|
||||
),
|
||||
key.name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node should be transformed to watch method, including:
|
||||
* 1. call expression.
|
||||
* 2. for loop
|
||||
* 3. while loop
|
||||
* 4. if statement
|
||||
* 5. switch statement
|
||||
* 6. assignment expression
|
||||
* 7. try statement
|
||||
* 8. ++/-- expression
|
||||
* @param node
|
||||
*/
|
||||
shouldTransformWatch(node: t.Node): node is ToWatchNode {
|
||||
if (this.t.isExpressionStatement(node)) {
|
||||
if (this.t.isCallExpression(node.expression)) {
|
||||
return true;
|
||||
}
|
||||
if (this.t.isAssignmentExpression(node.expression)) {
|
||||
return true;
|
||||
}
|
||||
if (this.t.isUpdateExpression(node.expression)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.t.isForStatement(node)) {
|
||||
return true;
|
||||
}
|
||||
if (this.t.isWhileStatement(node)) {
|
||||
return true;
|
||||
}
|
||||
if (this.t.isIfStatement(node)) {
|
||||
return true;
|
||||
}
|
||||
if (this.t.isSwitchStatement(node)) {
|
||||
return true;
|
||||
}
|
||||
if (this.t.isTryStatement(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { transform } from './transform';
|
||||
|
||||
describe('component-composition', () => {
|
||||
describe('props destructuring', () => {
|
||||
it('should support default values', () => {
|
||||
//language=JSX
|
||||
expect(
|
||||
transform(`
|
||||
function UserProfile({
|
||||
name = '',
|
||||
age = null,
|
||||
favouriteColors = [],
|
||||
isAvailable = false,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<p>My name is {name}!</p>
|
||||
<p>My age is {age}!</p>
|
||||
<p>My favourite colors are {favouriteColors.join(', ')}!</p>
|
||||
<p>I am {isAvailable ? 'available' : 'not available'}</p>
|
||||
</>
|
||||
);
|
||||
}`),
|
||||
`
|
||||
class UserProfile {
|
||||
@Prop name = ''
|
||||
@Prop age = null
|
||||
@Prop favouriteColors = []
|
||||
@Prop isAvailable = false
|
||||
|
||||
Body() {
|
||||
p(\`My name is \${this.name}!\`)
|
||||
p(\`My age is \${this.age}!\`)
|
||||
p(\`My favourite colors are \${this.favouriteColors.join(', ')}!\`)
|
||||
p(\`I am \${this.isAvailable ? 'available' : 'not available'}\`)
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should support nested destruing', () => {
|
||||
//language=JSX
|
||||
expect(
|
||||
transform(`
|
||||
function UserProfile({
|
||||
name = '',
|
||||
age = null,
|
||||
favouriteColors : [{r,g,b}, color2],
|
||||
isAvailable = false,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<p>My name is {name}!</p >
|
||||
<p>My age is {age}!</p >
|
||||
<p>My favourite colors are {favouriteColors.join(', ')}!</p >
|
||||
<p>I am {isAvailable ? 'available' : 'not available'}</p >
|
||||
</>
|
||||
);
|
||||
}`),
|
||||
`
|
||||
class UserProfile {
|
||||
@Prop name = '';
|
||||
@Prop age = null;
|
||||
@Prop favouriteColors = [];
|
||||
@Prop isAvailable = false;
|
||||
color1;
|
||||
color2;
|
||||
r;
|
||||
g;
|
||||
b;
|
||||
xx = (() => {
|
||||
const [{r, g, b},color2] = this.favouriteColors;
|
||||
this.r = r
|
||||
this.g = g
|
||||
this.b = b
|
||||
this.color2 = color2
|
||||
});
|
||||
|
||||
Body() {
|
||||
p(\`My name is \${this.name}!\`);
|
||||
p(\`My age is \${this.age}!\`);
|
||||
p(\`My favourite colors are \${this.favouriteColors.join(', ')}!\`);
|
||||
p(\`I am \${this.isAvailable ? 'available' : 'not available'}\`);
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should support children prop', () => {
|
||||
//language=JSX
|
||||
expect(
|
||||
transform(`
|
||||
function Card({ children }) {
|
||||
return (
|
||||
<div className="card">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}`),
|
||||
`
|
||||
class Card {
|
||||
@Children children
|
||||
|
||||
Body() {
|
||||
div(\`card\`, this.children)
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should support children prop with alias', () => {
|
||||
//language=JSX
|
||||
expect(
|
||||
transform(`
|
||||
function Card({ children: content }) {
|
||||
return (
|
||||
<div className="card">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}`),
|
||||
`
|
||||
class Card {
|
||||
@Children content
|
||||
|
||||
Body() {
|
||||
div(\`card\`, this.children)
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { test, it, expect } from 'vitest';
|
||||
import { transform } from './transform';
|
||||
|
||||
test('conditional', () => {});
|
|
@ -0,0 +1,299 @@
|
|||
import { it, describe, expect } from 'vitest';
|
||||
import { transform } from './transform';
|
||||
|
||||
describe('fn2Class', () => {
|
||||
it('should transform state assignment', () => {
|
||||
expect(
|
||||
//language=JSX
|
||||
transform(`
|
||||
export default function Name() {
|
||||
let name = 'John';
|
||||
|
||||
return <h1>{name}</h1>;
|
||||
}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"class Name extends View {
|
||||
name = 'John';
|
||||
Body() {
|
||||
return <h1>{this.name}</h1>;
|
||||
}
|
||||
}
|
||||
export { Name as default };"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform state modification ', () => {
|
||||
expect(
|
||||
transform(`
|
||||
function MyApp() {
|
||||
let count = 0;
|
||||
return <div onClick={() => count++}>{count}</div>
|
||||
}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"class MyApp extends View {
|
||||
count = 0;
|
||||
Body() {
|
||||
return <div onClick={() => this.count++}>{this.count}</div>;
|
||||
}
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not transform variable out of scope', () => {
|
||||
expect(
|
||||
//language=JSX
|
||||
transform(`
|
||||
const name = "John";
|
||||
export default function Name() {
|
||||
return <h1>{name}</h1>;
|
||||
}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"const name = \\"John\\";
|
||||
class Name extends View {
|
||||
Body() {
|
||||
return <h1>{name}</h1>;
|
||||
}
|
||||
}
|
||||
export { Name as default };"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform function declaration', () => {
|
||||
expect(
|
||||
//language=JSX
|
||||
transform(`
|
||||
const name = "John";
|
||||
|
||||
function Name() {
|
||||
function getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
console.log(getName());
|
||||
}
|
||||
return <h1 onClick={onClick}>{name}</h1>;
|
||||
}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"const name = \\"John\\";
|
||||
class Name extends View {
|
||||
getName() {
|
||||
return name;
|
||||
}
|
||||
onClick = () => {
|
||||
console.log(this.getName());
|
||||
};
|
||||
Body() {
|
||||
return <h1 onClick={this.onClick}>{name}</h1>;
|
||||
}
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not transform function parameter to this', () => {
|
||||
expect(
|
||||
//language=JSX
|
||||
transform(`
|
||||
function Name() {
|
||||
let name = 'Doe'
|
||||
|
||||
function getName(name) {
|
||||
return name + '!'
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
console.log(getName('John'));
|
||||
}
|
||||
return <h1 onClick={onClick}>{name}</h1>;
|
||||
}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"class Name extends View {
|
||||
name = 'Doe';
|
||||
getName(name) {
|
||||
return name + '!';
|
||||
}
|
||||
onClick = () => {
|
||||
console.log(this.getName('John'));
|
||||
};
|
||||
Body() {
|
||||
return <h1 onClick={this.onClick}>{this.name}</h1>;
|
||||
}
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not transform constant data', () => {
|
||||
expect(
|
||||
//language=JSX
|
||||
transform(`
|
||||
const name = "John";
|
||||
export default function Name() {
|
||||
return <h1>{name}</h1>;
|
||||
}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"const name = \\"John\\";
|
||||
class Name extends View {
|
||||
Body() {
|
||||
return <h1>{name}</h1>;
|
||||
}
|
||||
}
|
||||
export { Name as default };"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform derived assignment', () => {
|
||||
expect(
|
||||
//language=JSX
|
||||
transform(`
|
||||
export default function NameComp() {
|
||||
let firstName = "John";
|
||||
let lastName = "Doe";
|
||||
let fullName = \`\${firstName} \${lastName}\`
|
||||
|
||||
return <h1>{fullName}</h1>;
|
||||
}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"class NameComp extends View {
|
||||
firstName = \\"John\\";
|
||||
lastName = \\"Doe\\";
|
||||
fullName = \`\${this.firstName} \${this.lastName}\`;
|
||||
Body() {
|
||||
return <h1>{this.fullName}</h1>;
|
||||
}
|
||||
}
|
||||
export { NameComp as default };"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform watch from call expression', () => {
|
||||
expect(
|
||||
//language=JSX
|
||||
transform(`
|
||||
export default function CountComp() {
|
||||
let count = 0;
|
||||
console.log(count);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"class CountComp extends View {
|
||||
count = 0;
|
||||
@Watch
|
||||
_watch() {
|
||||
console.log(this.count);
|
||||
}
|
||||
Body() {
|
||||
return <div>{this.count}</div>;
|
||||
}
|
||||
}
|
||||
export { CountComp as default };"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform watch from block statement', () => {
|
||||
expect(
|
||||
//language=JSX
|
||||
transform(`
|
||||
export default function CountComp() {
|
||||
let count = 0;
|
||||
let countDown;
|
||||
let color
|
||||
for (let i = 0; i < count; i++) {
|
||||
console.log(\`The count change to: \${i}\`);
|
||||
}
|
||||
for (let i = 0; i < dbCount; i++) {
|
||||
console.log('color changed:', getColor());
|
||||
}
|
||||
function ()
|
||||
return <>
|
||||
<button onClick={() => count++}>Add</button>
|
||||
<div>{count}</div>
|
||||
</>;
|
||||
};
|
||||
`)
|
||||
).toMatchInlineSnapshot(
|
||||
`
|
||||
"class CountComp extends View {
|
||||
count = 0;
|
||||
@Watch
|
||||
_watch() {
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
console.log(\`The count change to: \${i}\`);
|
||||
}
|
||||
}
|
||||
Body() {
|
||||
return <>
|
||||
<button onClick={() => this.count++}>Add</button>
|
||||
<div>{this.count}</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
export { CountComp as default };
|
||||
;"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform watch from if statement', () => {
|
||||
expect(
|
||||
//language=JSX
|
||||
transform(`
|
||||
export default function CountComp() {
|
||||
let count = 0;
|
||||
if (count > 0) {
|
||||
console.log(\`The count is greater than 0\`);
|
||||
}
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"class CountComp extends View {
|
||||
count = 0;
|
||||
@Watch
|
||||
_watch() {
|
||||
if (this.count > 0) {
|
||||
console.log(\`The count is greater than 0\`);
|
||||
}
|
||||
}
|
||||
Body() {
|
||||
return <div>{this.count}</div>;
|
||||
}
|
||||
}
|
||||
export { CountComp as default };"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform function component reactively', () => {
|
||||
expect(
|
||||
transform(`
|
||||
function MyComp() {
|
||||
let count = 0
|
||||
return <>
|
||||
<h1 count='123'>Hello dlight fn, {count}</h1>
|
||||
<button onClick={() => count +=1}>Add</button>
|
||||
<Button />
|
||||
</>
|
||||
}`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"class MyComp extends View {
|
||||
count = 0;
|
||||
Body() {
|
||||
return <>
|
||||
<h1 count='123'>Hello dlight fn, {this.count}</h1>
|
||||
<button onClick={() => this.count += 1}>Add</button>
|
||||
<Button />
|
||||
</>;
|
||||
}
|
||||
}"
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { transform as transformWithBabel } from '@babel/core';
|
||||
import plugin from '../';
|
||||
|
||||
export function transform(code: string) {
|
||||
return transformWithBabel(code, {
|
||||
presets: [plugin],
|
||||
})?.code;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export interface Option {
|
||||
files?: string | string[];
|
||||
excludeFiles?: string | string[];
|
||||
htmlTags?: string[];
|
||||
parseTemplate?: boolean;
|
||||
attributeMap?: Record<string, string>;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "@inula/error-handler",
|
||||
"version": "1.0.0-alpha.0",
|
||||
"author": {
|
||||
"name": "IanDx",
|
||||
"email": "iandxssxx@gmail.com"
|
||||
},
|
||||
"keywords": [
|
||||
"dlight.js"
|
||||
],
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup --sourcemap"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.7.0",
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": ["src/index.ts"],
|
||||
"format": ["cjs", "esm"],
|
||||
"clean": true,
|
||||
"dts": true,
|
||||
"minify": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
type DLightErrMap = Record<number, string>;
|
||||
type ErrorMethod<T extends DLightErrMap, G extends string> = {
|
||||
[K in keyof T as `${G}${K & number}`]: (...args: string[]) => any;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Create error handler by given error space and error maps
|
||||
* e.g.
|
||||
* const errHandler = createErrorHandler("DLight", {
|
||||
* 1: "Cannot find node type: $0, throw"
|
||||
* }, {
|
||||
* 1: "This is an error: $0"
|
||||
* }, {
|
||||
* 1: "It's a warning"
|
||||
* })
|
||||
* errHandler.throw1("div") // -> throw new Error(":D - DLight[throw1]: Cannot find node type: div, throw")
|
||||
* errHandler.error1("div") // -> console.error(":D - DLight[error1]: This is an error: div")
|
||||
* errHandler.warn1() // -> console.warn(":D - DLight[warn1]: It's a warning")
|
||||
* @param errorSpace
|
||||
* @param throwMap
|
||||
* @param errorMap
|
||||
* @param warningMap
|
||||
* @returns Error handler
|
||||
*/
|
||||
export function createErrorHandler<A extends DLightErrMap, B extends DLightErrMap, C extends DLightErrMap>(
|
||||
errorSpace: string,
|
||||
throwMap: A = {} as any,
|
||||
errorMap: B = {} as any,
|
||||
warningMap: C = {} as any
|
||||
) {
|
||||
function handleError(map: DLightErrMap, type: string, func: (msg: string) => any) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(map).map(([code, msg]) => [
|
||||
`${type}${code}`,
|
||||
(...args: string[]) => {
|
||||
args.forEach((arg, i) => {
|
||||
msg = msg.replace(`$${i}`, arg);
|
||||
});
|
||||
return func(`:D - ${errorSpace}[${type}${code}]: ${msg}`);
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
const methods: ErrorMethod<A, 'throw'> & ErrorMethod<B, 'error'> & ErrorMethod<C, 'warn'> = {
|
||||
...handleError(throwMap, 'throw', msg => {
|
||||
throw new Error(msg);
|
||||
}),
|
||||
...handleError(errorMap, 'error', console.error),
|
||||
...handleError(warningMap, 'warn', console.warn),
|
||||
} as any;
|
||||
|
||||
function notDescribed(type: string) {
|
||||
return () => `:D ${errorSpace}: ${type} not described`;
|
||||
}
|
||||
|
||||
return {
|
||||
...methods,
|
||||
throwUnknown: notDescribed('throw'),
|
||||
errorUnknown: notDescribed('error'),
|
||||
warnUnknown: notDescribed('warn'),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "jsx-view-parser",
|
||||
"version": "0.0.0",
|
||||
"description": "Inula jsx parser",
|
||||
"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",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@vitest/ui": "^1.2.1",
|
||||
"tsup": "^6.7.0",
|
||||
"typescript": "^5.3.2",
|
||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||
"vitest": "^1.2.1"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"format": [
|
||||
"cjs",
|
||||
"esm"
|
||||
],
|
||||
"clean": true,
|
||||
"dts": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { ViewParser } from './parser';
|
||||
import type { ViewUnit, ViewParserConfig, AllowedJSXNode } from './types';
|
||||
|
||||
/**
|
||||
* @brief Generate view units from a babel ast
|
||||
* @param statement
|
||||
* @param config
|
||||
* @returns ViewUnit[]
|
||||
*/
|
||||
export function parseView(node: AllowedJSXNode, config: ViewParserConfig): ViewUnit[] {
|
||||
return new ViewParser(config).parse(node);
|
||||
}
|
||||
|
||||
export * from './types';
|
|
@ -0,0 +1,541 @@
|
|||
import type { NodePath, types as t, traverse as tr } from '@babel/core';
|
||||
import type {
|
||||
UnitProp,
|
||||
ViewUnit,
|
||||
ViewParserConfig,
|
||||
AllowedJSXNode,
|
||||
HTMLUnit,
|
||||
TextUnit,
|
||||
MutableUnit,
|
||||
TemplateProp,
|
||||
Context,
|
||||
} from './types';
|
||||
|
||||
export class ViewParser {
|
||||
// ---- Namespace and tag name
|
||||
private readonly htmlNamespace: string = 'html';
|
||||
private readonly htmlTagNamespace: string = 'tag';
|
||||
private readonly compTagNamespace: string = 'comp';
|
||||
private readonly envTagName: string = 'env';
|
||||
private readonly forTagName: string = 'for';
|
||||
private readonly ifTagName: string = 'if';
|
||||
private readonly elseIfTagName: string = 'else-if';
|
||||
private readonly elseTagName: string = 'else';
|
||||
private readonly customHTMLProps: string[] = ['ref'];
|
||||
|
||||
private readonly config: ViewParserConfig;
|
||||
private readonly htmlTags: string[];
|
||||
private readonly willParseTemplate: boolean;
|
||||
|
||||
private readonly t: typeof t;
|
||||
private readonly traverse: typeof tr;
|
||||
|
||||
private readonly viewUnits: ViewUnit[] = [];
|
||||
private context: Context;
|
||||
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param config
|
||||
* @param context
|
||||
*/
|
||||
constructor(config: ViewParserConfig, context: Context = { ifElseStack: [] }) {
|
||||
this.config = config;
|
||||
this.t = config.babelApi.types;
|
||||
this.traverse = config.babelApi.traverse;
|
||||
this.htmlTags = config.htmlTags;
|
||||
this.willParseTemplate = config.parseTemplate ?? true;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse the node into view units
|
||||
* @param node
|
||||
* @returns ViewUnit[]
|
||||
*/
|
||||
parse(node: AllowedJSXNode): ViewUnit[] {
|
||||
if (this.t.isJSXText(node)) this.parseText(node);
|
||||
else if (this.t.isJSXExpressionContainer(node)) this.parseExpression(node.expression);
|
||||
else if (this.t.isJSXElement(node)) this.parseElement(node);
|
||||
else if (this.t.isJSXFragment(node)) {
|
||||
node.children.forEach(child => {
|
||||
this.parse(child);
|
||||
});
|
||||
}
|
||||
|
||||
return this.viewUnits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse JSXText
|
||||
* @param node
|
||||
*/
|
||||
private parseText(node: t.JSXText): void {
|
||||
const text = node.value.trim();
|
||||
if (!text) return;
|
||||
this.viewUnits.push({
|
||||
type: 'text',
|
||||
content: this.t.stringLiteral(node.value),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse JSXExpressionContainer
|
||||
* @param node
|
||||
*/
|
||||
private parseExpression(node: t.Expression | t.JSXEmptyExpression): void {
|
||||
if (this.t.isJSXEmptyExpression(node)) return;
|
||||
if (this.t.isLiteral(node) && !this.t.isTemplateLiteral(node)) {
|
||||
// ---- Treat literal as text except template literal
|
||||
// Cuz template literal may have viewProp inside like:
|
||||
// <>{i18n`hello ${<MyView/>}`}</>
|
||||
this.viewUnits.push({
|
||||
type: 'text',
|
||||
content: node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.viewUnits.push({
|
||||
type: 'exp',
|
||||
content: this.parseProp(node),
|
||||
props: {},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse JSXElement
|
||||
* @param node
|
||||
*/
|
||||
private parseElement(node: t.JSXElement): void {
|
||||
let type: 'html' | 'comp';
|
||||
let tag: t.Expression;
|
||||
|
||||
// ---- Parse tag and type
|
||||
const openingName = node.openingElement.name;
|
||||
if (this.t.isJSXIdentifier(openingName)) {
|
||||
// ---- Opening name is a JSXIdentifier, e.g., <div>
|
||||
const name = openingName.name;
|
||||
// ---- Specially parse if and env
|
||||
if ([this.ifTagName, this.elseIfTagName, this.elseTagName].includes(name)) return this.parseIf(node);
|
||||
if (name === this.envTagName) return this.parseEnv(node);
|
||||
if (name === this.forTagName) return this.pareFor(node);
|
||||
else if (this.htmlTags.includes(name)) {
|
||||
type = 'html';
|
||||
tag = this.t.stringLiteral(name);
|
||||
} else {
|
||||
// ---- If the name is not in htmlTags, treat it as a comp
|
||||
type = 'comp';
|
||||
tag = this.t.identifier(name);
|
||||
}
|
||||
} else if (this.t.isJSXMemberExpression(openingName)) {
|
||||
// ---- Opening name is a JSXMemberExpression, e.g., <Comp.Div>
|
||||
// Treat it as a comp and set the tag as the opening name
|
||||
type = 'comp';
|
||||
// ---- Turn JSXMemberExpression into MemberExpression recursively
|
||||
const toMemberExpression = (node: t.JSXMemberExpression): t.MemberExpression => {
|
||||
if (this.t.isJSXMemberExpression(node.object)) {
|
||||
return this.t.memberExpression(toMemberExpression(node.object), this.t.identifier(node.property.name));
|
||||
}
|
||||
return this.t.memberExpression(this.t.identifier(node.object.name), this.t.identifier(node.property.name));
|
||||
};
|
||||
tag = toMemberExpression(openingName);
|
||||
} else {
|
||||
// ---- isJSXNamespacedName
|
||||
const namespace = openingName.namespace.name;
|
||||
switch (namespace) {
|
||||
case this.compTagNamespace:
|
||||
// ---- If the namespace is the same as the compTagNamespace, treat it as a comp
|
||||
// and set the tag as an identifier
|
||||
// e.g., <comp:div/> => ["comp", div]
|
||||
// this means you've declared a component named "div" and force it to be a comp instead an html
|
||||
type = 'comp';
|
||||
tag = this.t.identifier(openingName.name.name);
|
||||
break;
|
||||
case this.htmlNamespace:
|
||||
// ---- If the namespace is the same as the htmlTagNamespace, treat it as an html
|
||||
// and set the tag as a string literal
|
||||
// e.g., <html:MyWebComponent/> => ["html", "MyWebComponent"]
|
||||
// the tag will be treated as a string, i.e., <MyWebComponent/>
|
||||
type = 'html';
|
||||
tag = this.t.stringLiteral(openingName.name.name);
|
||||
break;
|
||||
case this.htmlTagNamespace:
|
||||
// ---- If the namespace is the same as the htmlTagNamespace, treat it as an html
|
||||
// and set the tag as an identifier
|
||||
// e.g., <tag:variable/> => ["html", variable]
|
||||
// this unit will be htmlUnit and the html string tag is stored in "variable"
|
||||
type = 'html';
|
||||
tag = this.t.identifier(openingName.name.name);
|
||||
break;
|
||||
default:
|
||||
// ---- Otherwise, treat it as an html tag and make the tag as the namespace:name
|
||||
type = 'html';
|
||||
tag = this.t.stringLiteral(`${namespace}:${openingName.name.name}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Parse the props
|
||||
const props = node.openingElement.attributes;
|
||||
const propMap: Record<string, UnitProp> = Object.fromEntries(props.map(prop => this.parseJSXProp(prop)));
|
||||
|
||||
// ---- Parse the children
|
||||
const childUnits = node.children.map(child => this.parseView(child)).flat();
|
||||
|
||||
let unit: ViewUnit = { type, tag, props: propMap, children: childUnits };
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse EnvUnit
|
||||
* @param node
|
||||
*/
|
||||
private parseEnv(node: t.JSXElement): void {
|
||||
const props = node.openingElement.attributes;
|
||||
const propMap: Record<string, UnitProp> = Object.fromEntries(props.map(prop => this.parseJSXProp(prop)));
|
||||
const children = node.children.map(child => this.parseView(child)).flat();
|
||||
this.viewUnits.push({
|
||||
type: 'env',
|
||||
props: propMap,
|
||||
children,
|
||||
});
|
||||
}
|
||||
|
||||
private parseIf(node: t.JSXElement): void {
|
||||
const name = (node.openingElement.name as t.JSXIdentifier).name;
|
||||
// ---- else
|
||||
if (name === this.elseTagName) {
|
||||
const lastUnit = this.context.ifElseStack[this.context.ifElseStack.length - 1];
|
||||
if (!lastUnit || lastUnit.type !== 'if') throw new Error(`Missing if for ${name}`);
|
||||
lastUnit.branches.push({
|
||||
condition: this.t.booleanLiteral(true),
|
||||
children: node.children.map(child => this.parseView(child)).flat(),
|
||||
});
|
||||
this.context.ifElseStack.pop();
|
||||
return;
|
||||
}
|
||||
|
||||
const condition = node.openingElement.attributes.filter(
|
||||
attr => this.t.isJSXAttribute(attr) && attr.name.name === 'cond'
|
||||
)[0];
|
||||
if (!condition) throw new Error(`Missing condition for ${name}`);
|
||||
if (!this.t.isJSXAttribute(condition)) throw new Error(`JSXSpreadAttribute is not supported for ${name} condition`);
|
||||
if (!this.t.isJSXExpressionContainer(condition.value) || !this.t.isExpression(condition.value.expression))
|
||||
throw new Error(`Invalid condition for ${name}`);
|
||||
|
||||
// ---- if
|
||||
if (name === this.ifTagName) {
|
||||
const unit = {
|
||||
type: 'if' as const,
|
||||
branches: [
|
||||
{
|
||||
condition: condition.value.expression,
|
||||
children: node.children.map(child => this.parseView(child)).flat(),
|
||||
},
|
||||
],
|
||||
};
|
||||
this.viewUnits.push(unit);
|
||||
this.context.ifElseStack.push(unit);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- else-if
|
||||
const lastUnit = this.context.ifElseStack[this.context.ifElseStack.length - 1];
|
||||
if (!lastUnit || lastUnit.type !== 'if') throw new Error(`Missing if for ${name}`);
|
||||
|
||||
lastUnit.branches.push({
|
||||
condition: condition.value.expression,
|
||||
children: node.children.map(child => this.parseView(child)).flat(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse JSXAttribute or JSXSpreadAttribute into UnitProp,
|
||||
* considering both namespace and expression
|
||||
* @param prop
|
||||
* @returns [propName, propValue]
|
||||
*/
|
||||
private parseJSXProp(prop: t.JSXAttribute | t.JSXSpreadAttribute): [string, UnitProp] {
|
||||
if (this.t.isJSXAttribute(prop)) {
|
||||
let propName: string, specifier: string | undefined;
|
||||
if (this.t.isJSXNamespacedName(prop.name)) {
|
||||
// ---- If the prop name is a JSXNamespacedName, e.g., bind:value
|
||||
// give it a special tag
|
||||
propName = prop.name.name.name;
|
||||
specifier = prop.name.namespace.name;
|
||||
} else {
|
||||
propName = prop.name.name;
|
||||
}
|
||||
let value = this.t.isJSXExpressionContainer(prop.value) ? prop.value.expression : prop.value;
|
||||
if (this.t.isJSXEmptyExpression(value)) value = undefined;
|
||||
return [propName, this.parseProp(value, specifier)];
|
||||
}
|
||||
// ---- Use *spread* as the propName to avoid conflict with other props
|
||||
return ['*spread*', this.parseProp(prop.argument)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse the prop node into UnitProp
|
||||
* @param propNode
|
||||
* @param specifier
|
||||
* @returns UnitProp
|
||||
*/
|
||||
private parseProp(propNode: t.Expression | undefined | null, specifier?: string): UnitProp {
|
||||
// ---- If there is no propNode, set the default prop as true
|
||||
if (!propNode) {
|
||||
return {
|
||||
value: this.t.booleanLiteral(true),
|
||||
viewPropMap: {},
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Collect sub jsx nodes as Prop
|
||||
const viewPropMap: Record<string, ViewUnit[]> = {};
|
||||
const parseViewProp = (innerPath: NodePath<t.JSXElement | t.JSXFragment>): void => {
|
||||
const id = this.uid();
|
||||
const node = innerPath.node;
|
||||
viewPropMap[id] = this.parseView(node);
|
||||
const newNode = this.t.stringLiteral(id);
|
||||
if (node === propNode) {
|
||||
// ---- If the node is the propNode, replace it with the new node
|
||||
propNode = newNode;
|
||||
}
|
||||
// ---- Replace the node and skip the inner path
|
||||
innerPath.replaceWith(newNode);
|
||||
innerPath.skip();
|
||||
};
|
||||
|
||||
// ---- Apply the parseViewProp to JSXElement and JSXFragment
|
||||
this.traverse(this.wrapWithFile(propNode), {
|
||||
JSXElement: parseViewProp,
|
||||
JSXFragment: parseViewProp,
|
||||
});
|
||||
|
||||
return {
|
||||
value: propNode,
|
||||
viewPropMap,
|
||||
specifier,
|
||||
};
|
||||
}
|
||||
|
||||
transformTemplate(unit: ViewUnit): ViewUnit {
|
||||
if (!this.willParseTemplate) return unit;
|
||||
if (!this.isHTMLTemplate(unit)) return unit;
|
||||
unit = unit as HTMLUnit;
|
||||
return {
|
||||
type: 'template',
|
||||
template: this.generateTemplate(unit),
|
||||
mutableUnits: this.generateMutableUnits(unit),
|
||||
props: this.parseTemplateProps(unit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generate the entire HTMLUnit
|
||||
* @param unit
|
||||
* @returns HTMLUnit
|
||||
*/
|
||||
private generateTemplate(unit: HTMLUnit): HTMLUnit {
|
||||
const staticProps = Object.fromEntries(
|
||||
this.filterTemplateProps(
|
||||
// ---- Get all the static props
|
||||
Object.entries(unit.props ?? []).filter(
|
||||
([, prop]) =>
|
||||
this.isStaticProp(prop) &&
|
||||
// ---- Filter out props with false values
|
||||
!(this.t.isBooleanLiteral(prop.value) && !prop.value.value)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
let children: (HTMLUnit | TextUnit)[] = [];
|
||||
if (unit.children) {
|
||||
children = unit.children
|
||||
.map(unit => {
|
||||
if (unit.type === 'text') return unit;
|
||||
if (unit.type === 'html' && this.t.isStringLiteral(unit.tag)) {
|
||||
return this.generateTemplate(unit);
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as (HTMLUnit | TextUnit)[];
|
||||
}
|
||||
return {
|
||||
type: 'html',
|
||||
tag: unit.tag,
|
||||
props: staticProps,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Collect all the mutable nodes in a static HTMLUnit
|
||||
* We use this function to collect mutable nodes' path and props,
|
||||
* so that in the generator, we know which position to insert the mutable nodes
|
||||
* @param htmlUnit
|
||||
* @returns mutable particles
|
||||
*/
|
||||
private generateMutableUnits(htmlUnit: HTMLUnit): MutableUnit[] {
|
||||
const mutableUnits: MutableUnit[] = [];
|
||||
|
||||
const generateMutableUnit = (unit: HTMLUnit, path: number[] = []) => {
|
||||
const maxHtmlIdx = unit.children?.filter(
|
||||
child => (child.type === 'html' && this.t.isStringLiteral(child.tag)) || child.type === 'text'
|
||||
).length;
|
||||
let htmlIdx = -1;
|
||||
// ---- Generate mutable unit for current HTMLUnit
|
||||
unit.children?.forEach(child => {
|
||||
if (!(child.type === 'html' && this.t.isStringLiteral(child.tag)) && !(child.type === 'text')) {
|
||||
const idx = htmlIdx + 1 >= maxHtmlIdx ? -1 : htmlIdx + 1;
|
||||
mutableUnits.push({
|
||||
path: [...path, idx],
|
||||
...this.transformTemplate(child),
|
||||
});
|
||||
} else {
|
||||
htmlIdx++;
|
||||
}
|
||||
});
|
||||
// ---- Recursively generate mutable units for static HTMLUnit children
|
||||
unit.children
|
||||
?.filter(child => child.type === 'html' && this.t.isStringLiteral(child.tag))
|
||||
.forEach((child, idx) => {
|
||||
generateMutableUnit(child as HTMLUnit, [...path, idx]);
|
||||
});
|
||||
};
|
||||
generateMutableUnit(htmlUnit);
|
||||
|
||||
return mutableUnits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Collect all the props in a static HTMLUnit or its nested HTMLUnit children
|
||||
* Just like the mutable nodes, props are also equipped with path,
|
||||
* so that we know which HTML ChildNode to insert the props
|
||||
* @param htmlUnit
|
||||
* @returns props
|
||||
*/
|
||||
private parseTemplateProps(htmlUnit: HTMLUnit): TemplateProp[] {
|
||||
const templateProps: TemplateProp[] = [];
|
||||
const generateVariableProp = (unit: HTMLUnit, path: number[]) => {
|
||||
// ---- Generate all non-static(string/number/boolean) props for current HTMLUnit
|
||||
// to be inserted further in the generator
|
||||
unit.props &&
|
||||
Object.entries(unit.props)
|
||||
.filter(([, prop]) => !this.isStaticProp(prop))
|
||||
.forEach(([key, prop]) => {
|
||||
templateProps.push({
|
||||
tag: unit.tag,
|
||||
name: (unit.tag as t.StringLiteral).value,
|
||||
key,
|
||||
path,
|
||||
value: prop.value,
|
||||
});
|
||||
});
|
||||
// ---- Recursively generate props for static HTMLUnit children
|
||||
unit.children
|
||||
?.filter(child => child.type === 'html' && this.t.isStringLiteral(child.tag))
|
||||
.forEach((child, idx) => {
|
||||
generateVariableProp(child as HTMLUnit, [...path, idx]);
|
||||
});
|
||||
};
|
||||
generateVariableProp(htmlUnit, []);
|
||||
|
||||
return templateProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if a ViewUnit is a static HTMLUnit that can be parsed into a template
|
||||
* Must satisfy:
|
||||
* 1. type is html
|
||||
* 2. tag is a string literal, i.e., non-dynamic tag
|
||||
* 3. has at least one child that is a static HTMLUnit,
|
||||
* or else just call a createElement function, no need for template clone
|
||||
* @param viewUnit
|
||||
* @returns is a static HTMLUnit
|
||||
*/
|
||||
private isHTMLTemplate(viewUnit: ViewUnit): boolean {
|
||||
return (
|
||||
viewUnit.type === 'html' &&
|
||||
this.t.isStringLiteral(viewUnit.tag) &&
|
||||
!!viewUnit.children?.some(child => child.type === 'html' && this.t.isStringLiteral(child.tag))
|
||||
);
|
||||
}
|
||||
|
||||
private isStaticProp(prop: UnitProp): boolean {
|
||||
return (
|
||||
this.t.isStringLiteral(prop.value) ||
|
||||
this.t.isNumericLiteral(prop.value) ||
|
||||
this.t.isBooleanLiteral(prop.value) ||
|
||||
this.t.isNullLiteral(prop.value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Filter out some props that are not needed in the template,
|
||||
* these are all special props to be parsed differently in the generator
|
||||
* @param props
|
||||
* @returns filtered props
|
||||
*/
|
||||
private filterTemplateProps<T>(props: Array<[string, T]>): Array<[string, T]> {
|
||||
return (
|
||||
props
|
||||
// ---- Filter out event listeners
|
||||
.filter(([key]) => !key.startsWith('on'))
|
||||
// ---- Filter out specific props
|
||||
.filter(([key]) => !this.customHTMLProps.includes(key))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse the view by duplicating current parser's classRootPath, statements and htmlTags
|
||||
* @param statements
|
||||
* @returns ViewUnit[]
|
||||
*/
|
||||
private parseView(node: AllowedJSXNode): ViewUnit[] {
|
||||
return new ViewParser({ ...this.config, parseTemplate: false }, this.context).parse(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wrap the value in a file
|
||||
* @param node
|
||||
* @returns wrapped value
|
||||
*/
|
||||
private wrapWithFile(node: t.Expression): t.File {
|
||||
return this.t.file(this.t.program([this.t.expressionStatement(node)]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generate a unique id
|
||||
* @returns a unique id
|
||||
*/
|
||||
private uid(): string {
|
||||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
private findProp(node: t.JSXElement, name: string) {
|
||||
const props = node.openingElement.attributes;
|
||||
|
||||
return props.find((prop): prop is t.JSXAttribute => this.t.isJSXAttribute(prop) && prop.name.name === name);
|
||||
}
|
||||
|
||||
private pareFor(node: t.JSXElement) {
|
||||
// TODO
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
import { describe, expect, it, afterAll, beforeAll } from 'vitest';
|
||||
import { config, parse, parseCode, parseView } from './mock';
|
||||
import { types as t } from '@babel/core';
|
||||
import type { CompUnit, HTMLUnit } from '../index';
|
||||
|
||||
describe('ElementUnit', () => {
|
||||
beforeAll(() => {
|
||||
// ---- Ignore template for this test
|
||||
config.parseTemplate = false;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.parseTemplate = true;
|
||||
});
|
||||
|
||||
// ---- Type
|
||||
it('should identify a JSX element with tag in htmlTags as an HTMLUnit', () => {
|
||||
const viewUnits = parse('<div></div>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('html');
|
||||
});
|
||||
|
||||
it('should identify a JSX element with tag not in htmlTags as an CompUnit', () => {
|
||||
const viewUnits = parse('<Comp></Comp>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('comp');
|
||||
});
|
||||
|
||||
it('should identify a JSX element with namespaced "html" outside htmlTags as an HTMLUnit', () => {
|
||||
const viewUnits = parse('<html:MyWebComponent></html:MyWebComponent>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('html');
|
||||
});
|
||||
|
||||
it('should identify a JSX element with namespaced "tag" outside htmlTags as an HTMLUnit', () => {
|
||||
const viewUnits = parse('<tag:variable></tag:variable>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('html');
|
||||
});
|
||||
|
||||
it('should identify a JSX element with namespaced "comp" inside htmlTags as an HTMLUnit', () => {
|
||||
const viewUnits = parse('<comp:div></comp:div>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('comp');
|
||||
});
|
||||
|
||||
it('should identify a JSX element with name equal to "env" as an EnvUnit', () => {
|
||||
const viewUnits = parse('<env></env>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('env');
|
||||
});
|
||||
|
||||
// ---- Tag
|
||||
it('should correctly parse the tag of an HTMLUnit', () => {
|
||||
const viewUnits = parse('<div></div>');
|
||||
const tag = (viewUnits[0] as HTMLUnit).tag;
|
||||
|
||||
expect(t.isStringLiteral(tag, { value: 'div' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse the tag of an HTMLUnit with namespaced "html"', () => {
|
||||
const viewUnits = parse('<html:MyWebComponent></html:MyWebComponent>');
|
||||
const tag = (viewUnits[0] as HTMLUnit).tag;
|
||||
|
||||
expect(t.isStringLiteral(tag, { value: 'MyWebComponent' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse the tag of an HTMLUnit with namespaced "tag"', () => {
|
||||
const viewUnits = parse('<tag:variable></tag:variable>');
|
||||
const tag = (viewUnits[0] as HTMLUnit).tag;
|
||||
|
||||
expect(t.isIdentifier(tag, { name: 'variable' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse the tag of an CompUnit', () => {
|
||||
const viewUnits = parse('<Comp></Comp>');
|
||||
const tag = (viewUnits[0] as HTMLUnit).tag;
|
||||
|
||||
expect(t.isIdentifier(tag, { name: 'Comp' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse the tag of an CompUnit with namespaced "comp"', () => {
|
||||
const viewUnits = parse('<comp:div></comp:div>');
|
||||
const tag = (viewUnits[0] as HTMLUnit).tag;
|
||||
|
||||
expect(t.isIdentifier(tag, { name: 'div' })).toBeTruthy();
|
||||
});
|
||||
|
||||
// ---- Props(for both HTMLUnit and CompUnit)
|
||||
it('should correctly parse the props', () => {
|
||||
const viewUnits = parse('<div id="myId"></div>');
|
||||
|
||||
const htmlUnit = viewUnits[0] as HTMLUnit;
|
||||
const props = htmlUnit.props!;
|
||||
expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse the props with a complex expression', () => {
|
||||
const ast = parseCode('<div onClick={() => {console.log("ok")}}></div>');
|
||||
const viewUnits = parseView(ast);
|
||||
|
||||
const originalExpression = (
|
||||
((ast as t.JSXElement).openingElement.attributes[0] as t.JSXAttribute).value as t.JSXExpressionContainer
|
||||
).expression;
|
||||
|
||||
const htmlUnit = viewUnits[0] as HTMLUnit;
|
||||
expect(htmlUnit.props!.onClick.value).toBe(originalExpression);
|
||||
});
|
||||
|
||||
it('should correctly parse multiple props', () => {
|
||||
const viewUnits = parse('<div id="myId" class="myClass"></div>');
|
||||
|
||||
const htmlUnit = viewUnits[0] as HTMLUnit;
|
||||
const props = htmlUnit.props!;
|
||||
expect(Object.keys(props).length).toBe(2);
|
||||
expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy();
|
||||
expect(t.isStringLiteral(props.class.value, { value: 'myClass' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse props with namespace as its specifier', () => {
|
||||
const viewUnits = parse('<div bind:id="myId"></div>');
|
||||
const htmlUnit = viewUnits[0] as HTMLUnit;
|
||||
const props = htmlUnit.props!;
|
||||
expect(props.id.specifier).toBe('bind');
|
||||
expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse spread props', () => {
|
||||
const viewUnits = parse('<Comp {...props}></Comp>');
|
||||
const htmlUnit = viewUnits[0] as CompUnit;
|
||||
const props = htmlUnit.props!;
|
||||
expect(t.isIdentifier(props['*spread*'].value, { name: 'props' })).toBeTruthy();
|
||||
});
|
||||
|
||||
// ---- View prop (other test cases can be found in ExpUnit.test.ts)
|
||||
it('should correctly parse sub jsx attribute as view prop', () => {
|
||||
const ast = parseCode('<Comp sub=<div>Ok</div>></Comp>');
|
||||
const viewUnits = parseView(ast);
|
||||
|
||||
const props = (viewUnits[0] as CompUnit).props!;
|
||||
const viewPropMap = props.sub.viewPropMap!;
|
||||
expect(Object.keys(viewPropMap).length).toBe(1);
|
||||
|
||||
const key = Object.keys(viewPropMap)[0];
|
||||
const viewProp = viewPropMap[key];
|
||||
expect(viewProp.length).toBe(1);
|
||||
expect(viewProp[0].type).toBe('html');
|
||||
|
||||
// ---- Prop View will be replaced with a random string and stored in props.viewPropMap
|
||||
const value = props.sub.value;
|
||||
expect(t.isStringLiteral(value, { value: key })).toBeTruthy();
|
||||
});
|
||||
|
||||
// ---- Children(for both HTMLUnit and CompUnit)
|
||||
it('should correctly parse the count of children', () => {
|
||||
const viewUnits = parse(`<div>
|
||||
<div>ok</div>
|
||||
<div>ok</div>
|
||||
<Comp></Comp>
|
||||
<Comp></Comp>
|
||||
</div>`);
|
||||
const htmlUnit = viewUnits[0] as HTMLUnit;
|
||||
expect(htmlUnit.children!.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should correctly parse the count of children with JSXExpressionContainer', () => {
|
||||
const viewUnits = parse(`<div>
|
||||
<div>ok</div>
|
||||
<div>ok</div>
|
||||
{count}
|
||||
{count}
|
||||
</div>`);
|
||||
const htmlUnit = viewUnits[0] as HTMLUnit;
|
||||
expect(htmlUnit.children!.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should correctly parse the count of children with JSXFragment', () => {
|
||||
const viewUnits = parse(`<div>
|
||||
<div>ok</div>
|
||||
<div>ok</div>
|
||||
<>
|
||||
<Comp></Comp>
|
||||
<Comp></Comp>
|
||||
</>
|
||||
</div>`);
|
||||
const htmlUnit = viewUnits[0] as HTMLUnit;
|
||||
expect(htmlUnit.children!.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should correctly parse the children', () => {
|
||||
const viewUnits = parse('<div><div>ok</div></div>');
|
||||
const htmlUnit = viewUnits[0] as HTMLUnit;
|
||||
const firstChild = htmlUnit.children![0];
|
||||
expect(firstChild.type).toBe('html');
|
||||
expect(t.isStringLiteral((firstChild as HTMLUnit).tag, { value: 'div' })).toBeTruthy();
|
||||
expect((firstChild as HTMLUnit).children![0].type).toBe('text');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { parse, parseCode, parseView, wrapWithFile } from './mock';
|
||||
import { types as t } from '@babel/core';
|
||||
import type { ExpUnit } from '../index';
|
||||
import { traverse } from '@babel/core';
|
||||
|
||||
describe('ExpUnit', () => {
|
||||
// ---- Type
|
||||
it('should identify expression unit', () => {
|
||||
const viewUnits = parse('<>{count}</>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('exp');
|
||||
});
|
||||
|
||||
it('should not identify literals as expression unit', () => {
|
||||
const viewUnits = parse('<>{1}</>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).not.toBe('exp');
|
||||
});
|
||||
|
||||
// ---- Content
|
||||
it('should correctly parse content for expression unit', () => {
|
||||
const viewUnits = parse('<>{count}</>');
|
||||
const content = (viewUnits[0] as ExpUnit).content;
|
||||
|
||||
expect(t.isIdentifier(content.value, { name: 'count' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse complex content for expression unit', () => {
|
||||
const ast = parseCode('<>{!console.log("hello world") && myComplexFunc(count + 100)}</>');
|
||||
const viewUnits = parseView(ast);
|
||||
|
||||
const originalExpression = ((ast as t.JSXFragment).children[0] as t.JSXExpressionContainer).expression;
|
||||
|
||||
const content = (viewUnits[0] as ExpUnit).content;
|
||||
expect(content.value).toBe(originalExpression);
|
||||
});
|
||||
|
||||
it('should correctly parse content with view prop for expression unit', () => {
|
||||
// ---- <div>Ok</div> will be replaced with a random string and stored in props.viewPropMap
|
||||
const viewUnits = parse('<>{<div>Ok</div>}</>');
|
||||
const content = (viewUnits[0] as ExpUnit).content;
|
||||
const viewPropMap = content.viewPropMap;
|
||||
|
||||
expect(Object.keys(viewPropMap).length).toBe(1);
|
||||
const key = Object.keys(viewPropMap)[0];
|
||||
const viewProp = viewPropMap[key];
|
||||
// ---- Only one view unit for <div>Ok</div>
|
||||
expect(viewProp.length).toBe(1);
|
||||
expect(viewProp[0].type).toBe('html');
|
||||
|
||||
// ---- The value of the replaced prop should be the key of the viewPropMap
|
||||
const value = content.value;
|
||||
expect(t.isStringLiteral(value, { value: key })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse content with view prop for expression unit with complex expression', () => {
|
||||
// ---- <div>Ok</div> will be replaced with a random string and stored in props.viewPropMap
|
||||
const ast = parseCode(`<>{
|
||||
someFunc(() => {
|
||||
console.log("hello world")
|
||||
doWhatever()
|
||||
return <div>Ok</div>
|
||||
})
|
||||
}</>`);
|
||||
const viewUnits = parseView(ast);
|
||||
|
||||
const content = (viewUnits[0] as ExpUnit).content;
|
||||
const viewPropMap = content.viewPropMap;
|
||||
|
||||
expect(Object.keys(viewPropMap).length).toBe(1);
|
||||
const key = Object.keys(viewPropMap)[0];
|
||||
const viewProp = viewPropMap[key];
|
||||
// ---- Only one view unit for <div>Ok</div>
|
||||
expect(viewProp.length).toBe(1);
|
||||
|
||||
// ---- Check the value of the replaced prop
|
||||
let idExistCount = 0;
|
||||
traverse(wrapWithFile(content.value), {
|
||||
StringLiteral(path) {
|
||||
if (path.node.value === key) idExistCount++;
|
||||
},
|
||||
});
|
||||
// ---- Expect the count of the id matching to be exactly 1
|
||||
expect(idExistCount).toBe(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,201 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { parse } from './mock';
|
||||
import { types as t } from '@babel/core';
|
||||
import { HTMLUnit, IfUnit } from '../types';
|
||||
|
||||
describe('IfUnit', () => {
|
||||
// ---- Type
|
||||
it('should identify if unit', () => {
|
||||
const viewUnits = parse('<if cond={true}>true</if>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('if');
|
||||
});
|
||||
|
||||
it('should identify if unit with else', () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={true}>true</if>
|
||||
<else>false</else>
|
||||
</>`);
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('if');
|
||||
});
|
||||
|
||||
it('should identify if unit with else-if', () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={true}>true</if>
|
||||
<else-if cond={false}>false</else-if>
|
||||
</>`);
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('if');
|
||||
});
|
||||
|
||||
it('should find matched if in html tag', () => {
|
||||
const viewUnits = parse(`<section>
|
||||
<if cond={true}>true</if>
|
||||
<else>false</else>
|
||||
</section>`) as unknown as HTMLUnit[];
|
||||
expect(viewUnits[0].children[0].type).toBe('if');
|
||||
});
|
||||
|
||||
it('should identify if unit with else-if and else', () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={true}>true</if>
|
||||
<else-if cond={false}>false</else-if>
|
||||
<else>else</else>
|
||||
</>`);
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('if');
|
||||
});
|
||||
|
||||
it('should identify if unit with multiple else-if', () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={true}>true</if>
|
||||
<else-if cond={flag1}>flag1</else-if>
|
||||
<else-if cond={flag2}>flag2</else-if>
|
||||
<else>else</else>
|
||||
</>`);
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('if');
|
||||
});
|
||||
|
||||
// ---- Branches
|
||||
it('should correctly parse branches count for if unit', () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={true}>true</if>
|
||||
<else-if cond={flag1}>flag1</else-if>
|
||||
<else-if cond={flag2}>flag2</else-if>
|
||||
<else>else</else>
|
||||
</>`);
|
||||
|
||||
const branches = (viewUnits[0] as IfUnit).branches;
|
||||
expect(branches.length).toBe(4);
|
||||
});
|
||||
|
||||
it("should correctly parse branches' condition for if unit", () => {
|
||||
const viewUnits = parse('<if cond={true}>true</if>');
|
||||
const branches = (viewUnits[0] as IfUnit).branches;
|
||||
|
||||
expect(t.isBooleanLiteral(branches[0].condition, { value: true })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should correctly parse branches' children for if unit", () => {
|
||||
const viewUnits = parse('<if cond={true}>true</if>');
|
||||
const branches = (viewUnits[0] as IfUnit).branches;
|
||||
|
||||
expect(branches[0].children.length).toBe(1);
|
||||
expect(branches[0].children[0].type).toBe('text');
|
||||
});
|
||||
|
||||
it("should correctly parse branches' condition for if unit with else", () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={flag1}>1</if>
|
||||
<else>2</else>
|
||||
</>`);
|
||||
const branches = (viewUnits[0] as IfUnit).branches;
|
||||
|
||||
expect(t.isIdentifier(branches[0].condition, { name: 'flag1' })).toBeTruthy();
|
||||
expect(t.isBooleanLiteral(branches[1].condition, { value: true })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should correctly parse branches' children for if unit with else", () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={true}>true</if>
|
||||
<else>false</else>
|
||||
</>`);
|
||||
const branches = (viewUnits[0] as IfUnit).branches;
|
||||
|
||||
expect(branches[0].children.length).toBe(1);
|
||||
expect(branches[0].children[0].type).toBe('text');
|
||||
expect(branches[1].children.length).toBe(1);
|
||||
expect(branches[1].children[0].type).toBe('text');
|
||||
});
|
||||
|
||||
it("should correctly parse branches' condition for if unit with else-if", () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={flag1}>1</if>
|
||||
<else-if cond={flag2}>2</else-if>
|
||||
</>`);
|
||||
const branches = (viewUnits[0] as IfUnit).branches;
|
||||
/**
|
||||
* () => {
|
||||
* if (flag1) {
|
||||
* this._prevCond
|
||||
* return 1
|
||||
* } else () {
|
||||
* if (flag2) {
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
expect(t.isIdentifier(branches[0].condition, { name: 'flag1' })).toBeTruthy();
|
||||
expect(t.isIdentifier(branches[1].condition, { name: 'flag2' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should correctly parse branches' children for if unit with else-if", () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={true}>true</if>
|
||||
<else-if cond={false}>false</else-if>
|
||||
</>`);
|
||||
const branches = (viewUnits[0] as IfUnit).branches;
|
||||
|
||||
expect(branches[0].children.length).toBe(1);
|
||||
expect(branches[0].children[0].type).toBe('text');
|
||||
expect(branches[1].children.length).toBe(1);
|
||||
expect(branches[1].children[0].type).toBe('text');
|
||||
});
|
||||
|
||||
// --- nested
|
||||
it('should correctly parse nested if unit', () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={true}>
|
||||
<if cond={true}>true</if>
|
||||
</if>
|
||||
</>`);
|
||||
const branches = (viewUnits[0] as IfUnit).branches;
|
||||
|
||||
expect(branches.length).toBe(1);
|
||||
expect(branches[0].children[0].type).toBe('if');
|
||||
});
|
||||
|
||||
it('should correctly parse nested if unit with else', () => {
|
||||
const viewUnits = parse(`<>
|
||||
<if cond={true}>
|
||||
<if cond={true}>true</if>
|
||||
<else>false</else>
|
||||
</if>
|
||||
</>`);
|
||||
const branches = (viewUnits[0] as IfUnit).branches;
|
||||
|
||||
expect(branches.length).toBe(1);
|
||||
expect(branches[0].children[0].type).toBe('if');
|
||||
});
|
||||
|
||||
it('should throw error for nested if unit with else-if', () => {
|
||||
expect(() => {
|
||||
parse(`<>
|
||||
<if cond={true}>
|
||||
<else>false</else-if>
|
||||
</if>
|
||||
</>`);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it('test', () => {
|
||||
expect(
|
||||
parse(`
|
||||
<>
|
||||
<h1 className="123">Hello dlight fn comp</h1>
|
||||
<section>
|
||||
count: {count}, double is: {db}
|
||||
<button onClick={() => (count += 1)}>Add</button>
|
||||
</section>
|
||||
<Button onClick={() => alert(count)}>Alter count</Button>
|
||||
<h1>Condition</h1>
|
||||
<if cond={count > 1}>{count} is bigger than is 1</if>
|
||||
<else>{count} is smaller than 1</else>
|
||||
<ArrayModification />
|
||||
</>
|
||||
`)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { parse } from './mock';
|
||||
import { types as t } from '@babel/core';
|
||||
import type { HTMLUnit, TemplateUnit } from '../index';
|
||||
|
||||
describe('TemplateUnit', () => {
|
||||
// ---- Type
|
||||
it('should not parse a single HTMLUnit to a TemplateUnit', () => {
|
||||
const viewUnits = parse('<div></div>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('html');
|
||||
});
|
||||
|
||||
it('should parse a nested HTMLUnit to a TemplateUnit', () => {
|
||||
const viewUnits = parse('<div><div></div></div>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('template');
|
||||
});
|
||||
|
||||
it("should correctly parse a nested HTMLUnit's structure into a template", () => {
|
||||
const viewUnits = parse('<div><div></div></div>');
|
||||
const template = (viewUnits[0] as TemplateUnit).template;
|
||||
|
||||
expect(t.isStringLiteral(template.tag, { value: 'div' })).toBeTruthy();
|
||||
expect(template.children).toHaveLength(1);
|
||||
const firstChild = template.children![0] as HTMLUnit;
|
||||
expect(t.isStringLiteral(firstChild.tag, { value: 'div' })).toBeTruthy();
|
||||
});
|
||||
|
||||
// ---- Props
|
||||
it("should correctly parse the path of TemplateUnit's dynamic props in root element", () => {
|
||||
const viewUnits = parse('<div class={this.name}><div></div></div>');
|
||||
const dynamicProps = (viewUnits[0] as TemplateUnit).props;
|
||||
|
||||
expect(dynamicProps).toHaveLength(1);
|
||||
const prop = dynamicProps[0];
|
||||
expect(prop.path).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should correctly parse the path of TemplateUnit's dynamic props in nested element", () => {
|
||||
const viewUnits = parse('<div><div class={this.name}></div></div>');
|
||||
const dynamicProps = (viewUnits[0] as TemplateUnit).props!;
|
||||
|
||||
expect(dynamicProps).toHaveLength(1);
|
||||
const prop = dynamicProps[0]!;
|
||||
expect(prop.path).toHaveLength(1);
|
||||
expect(prop.path[0]).toBe(0);
|
||||
});
|
||||
|
||||
it("should correctly parse the path of TemplateUnit's dynamic props with mutable particles ahead", () => {
|
||||
const viewUnits = parse('<div><Comp/><div class={this.name}></div></div>');
|
||||
const dynamicProps = (viewUnits[0] as TemplateUnit).props!;
|
||||
|
||||
expect(dynamicProps).toHaveLength(1);
|
||||
const prop = dynamicProps[0]!;
|
||||
expect(prop.path).toHaveLength(1);
|
||||
expect(prop.path[0]).toBe(1);
|
||||
});
|
||||
|
||||
it("should correctly parse the path of TemplateUnit's mutableUnits", () => {
|
||||
const viewUnits = parse('<div><Comp/><div class={this.name}></div></div>');
|
||||
const mutableParticles = (viewUnits[0] as TemplateUnit).mutableUnits!;
|
||||
|
||||
expect(mutableParticles).toHaveLength(1);
|
||||
const particle = mutableParticles[0]!;
|
||||
expect(particle.path).toHaveLength(1);
|
||||
expect(particle.path[0]).toBe(0);
|
||||
});
|
||||
|
||||
it("should correctly parse the path of multiple TemplateUnit's mutableUnits", () => {
|
||||
const viewUnits = parse('<div><Comp/><div class={this.name}></div><Comp/></div>');
|
||||
const mutableParticles = (viewUnits[0] as TemplateUnit).mutableUnits!;
|
||||
|
||||
expect(mutableParticles).toHaveLength(2);
|
||||
const firstParticle = mutableParticles[0]!;
|
||||
expect(firstParticle.path).toHaveLength(1);
|
||||
expect(firstParticle.path[0]).toBe(0);
|
||||
const secondParticle = mutableParticles[1]!;
|
||||
expect(secondParticle.path).toHaveLength(1);
|
||||
expect(secondParticle.path[0]).toBe(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { parse } from './mock';
|
||||
import { types as t } from '@babel/core';
|
||||
import type { TextUnit } from '../index';
|
||||
|
||||
describe('TextUnit', () => {
|
||||
// ---- Type
|
||||
it('should identify text unit', () => {
|
||||
const viewUnits = parse('<>hello world</>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('text');
|
||||
});
|
||||
|
||||
it('should identify text unit with boolean expression', () => {
|
||||
const viewUnits = parse('<>{true}</>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('text');
|
||||
});
|
||||
|
||||
it('should identify text unit with number expression', () => {
|
||||
const viewUnits = parse('<>{1}</>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('text');
|
||||
});
|
||||
|
||||
it('should identify text unit with null expression', () => {
|
||||
const viewUnits = parse('<>{null}</>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('text');
|
||||
});
|
||||
|
||||
it('should identify text unit with string literal expression', () => {
|
||||
const viewUnits = parse('<>{"hello world"}</>');
|
||||
expect(viewUnits.length).toBe(1);
|
||||
expect(viewUnits[0].type).toBe('text');
|
||||
});
|
||||
|
||||
// ---- Content
|
||||
it('should correctly parse content for text unit', () => {
|
||||
const viewUnits = parse('<>hello world</>');
|
||||
const content = (viewUnits[0] as TextUnit).content;
|
||||
|
||||
expect(t.isStringLiteral(content, { value: 'hello world' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse content for boolean text unit', () => {
|
||||
const viewUnits = parse('<>{true}</>');
|
||||
const content = (viewUnits[0] as TextUnit).content;
|
||||
|
||||
expect(t.isBooleanLiteral(content, { value: true })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse content for number text unit', () => {
|
||||
const viewUnits = parse('<>{1}</>');
|
||||
const content = (viewUnits[0] as TextUnit).content;
|
||||
|
||||
expect(t.isNumericLiteral(content, { value: 1 })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse content for null text unit', () => {
|
||||
const viewUnits = parse('<>{null}</>');
|
||||
const content = (viewUnits[0] as TextUnit).content;
|
||||
|
||||
expect(t.isNullLiteral(content)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should correctly parse content for string literal text unit', () => {
|
||||
const viewUnits = parse('<>{"hello world"}</>');
|
||||
const content = (viewUnits[0] as TextUnit).content;
|
||||
|
||||
expect(t.isStringLiteral(content)).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
declare module '@babel/plugin-syntax-jsx';
|
|
@ -0,0 +1,228 @@
|
|||
import babel, { parseSync, types as t } from '@babel/core';
|
||||
import { AllowedJSXNode, ViewParserConfig } from '../types';
|
||||
import { parseView as pV } from '..';
|
||||
import babelJSX from '@babel/plugin-syntax-jsx';
|
||||
|
||||
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',
|
||||
];
|
||||
|
||||
export const config: ViewParserConfig = {
|
||||
babelApi: babel,
|
||||
htmlTags,
|
||||
};
|
||||
|
||||
export function parseCode(code: string) {
|
||||
return (parseSync(code, { plugins: [babelJSX] })!.program.body[0] as t.ExpressionStatement)
|
||||
.expression as AllowedJSXNode;
|
||||
}
|
||||
|
||||
export function parseView(node: AllowedJSXNode) {
|
||||
return pV(node, config);
|
||||
}
|
||||
|
||||
export function parse(code: string) {
|
||||
return parseView(parseCode(code));
|
||||
}
|
||||
|
||||
export function wrapWithFile(node: t.Expression): t.File {
|
||||
return t.file(t.program([t.expressionStatement(node)]));
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import type Babel from '@babel/core';
|
||||
import type { types as t } from '@babel/core';
|
||||
|
||||
export interface Context {
|
||||
ifElseStack: IfUnit[];
|
||||
}
|
||||
export interface UnitProp {
|
||||
value: t.Expression;
|
||||
viewPropMap: Record<string, ViewUnit[]>;
|
||||
specifier?: string;
|
||||
}
|
||||
|
||||
export interface TextUnit {
|
||||
type: 'text';
|
||||
content: t.Literal;
|
||||
}
|
||||
|
||||
export type MutableUnit = ViewUnit & { path: number[] };
|
||||
|
||||
export interface TemplateProp {
|
||||
tag: t.Expression;
|
||||
name: string;
|
||||
key: string;
|
||||
path: number[];
|
||||
value: t.Expression;
|
||||
}
|
||||
|
||||
export interface TemplateUnit {
|
||||
type: 'template';
|
||||
template: HTMLUnit;
|
||||
mutableUnits: MutableUnit[];
|
||||
props: TemplateProp[];
|
||||
}
|
||||
|
||||
export interface HTMLUnit {
|
||||
type: 'html';
|
||||
tag: t.Expression;
|
||||
props: Record<string, UnitProp>;
|
||||
children: ViewUnit[];
|
||||
}
|
||||
|
||||
export interface CompUnit {
|
||||
type: 'comp';
|
||||
tag: t.Expression;
|
||||
props: Record<string, UnitProp>;
|
||||
children: ViewUnit[];
|
||||
}
|
||||
|
||||
export interface IfBranch {
|
||||
condition: t.Expression;
|
||||
children: ViewUnit[];
|
||||
}
|
||||
|
||||
export interface IfUnit {
|
||||
type: 'if';
|
||||
branches: IfBranch[];
|
||||
}
|
||||
|
||||
export interface ExpUnit {
|
||||
type: 'exp';
|
||||
content: UnitProp;
|
||||
}
|
||||
|
||||
export interface EnvUnit {
|
||||
type: 'env';
|
||||
props: Record<string, UnitProp>;
|
||||
children: ViewUnit[];
|
||||
}
|
||||
|
||||
export interface ForUnit {
|
||||
type: 'for';
|
||||
item: t.LVal;
|
||||
array: t.Expression;
|
||||
key: t.Expression;
|
||||
children: ViewUnit[];
|
||||
}
|
||||
export type ViewUnit = TextUnit | HTMLUnit | CompUnit | IfUnit | ExpUnit | EnvUnit | TemplateUnit | ForUnit;
|
||||
|
||||
export interface ViewParserConfig {
|
||||
babelApi: typeof Babel;
|
||||
htmlTags: string[];
|
||||
parseTemplate?: boolean;
|
||||
}
|
||||
|
||||
export type AllowedJSXNode = t.JSXElement | t.JSXFragment | t.JSXText | t.JSXExpressionContainer | t.JSXSpreadChild;
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "@inula/reactivity-parser",
|
||||
"version": "0.0.1",
|
||||
"author": {
|
||||
"name": "IanDx",
|
||||
"email": "iandxssxx@gmail.com"
|
||||
},
|
||||
"keywords": [
|
||||
"dlight.js"
|
||||
],
|
||||
"license": "MIT",
|
||||
"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",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@vitest/ui": "^0.34.5",
|
||||
"tsup": "^6.7.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vitest": "^0.34.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inula/error-handler": "workspace:*",
|
||||
"@inula/view-parser": "workspace:*"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"format": [
|
||||
"cjs",
|
||||
"esm"
|
||||
],
|
||||
"clean": true,
|
||||
"dts": true,
|
||||
"minify": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { createErrorHandler } from '@inula/error-handler';
|
||||
|
||||
export const DLError = createErrorHandler('ReactivityParser', {
|
||||
1: 'Invalid ViewUnit type',
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { type ViewUnit } from '@inula/view-parser';
|
||||
import { ReactivityParser } from './parser';
|
||||
import { type ViewParticle, type ReactivityParserConfig } from './types';
|
||||
|
||||
/**
|
||||
* @brief Parse view units to get used properties and view particles with reactivity
|
||||
* @param viewUnits
|
||||
* @param config
|
||||
* @param options
|
||||
* @returns [viewParticles, usedProperties]
|
||||
*/
|
||||
export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserConfig): [ViewParticle[], Set<string>] {
|
||||
// ---- ReactivityParser only accepts one view unit at a time,
|
||||
// so we loop through the view units and get all the used properties
|
||||
const usedProperties = new Set<string>();
|
||||
const dlParticles = viewUnits.map(viewUnit => {
|
||||
const parser = new ReactivityParser(config);
|
||||
const dlParticle = parser.parse(viewUnit);
|
||||
parser.usedProperties.forEach(usedProperties.add.bind(usedProperties));
|
||||
return dlParticle;
|
||||
});
|
||||
return [dlParticles, usedProperties];
|
||||
}
|
||||
|
||||
export type * from './types';
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,77 @@
|
|||
import { expect, describe, it } from 'vitest';
|
||||
import { availableProperties, parse, reactivityConfig } from './mock';
|
||||
import { type CompParticle } from '../types';
|
||||
|
||||
describe('Dependency', () => {
|
||||
it('should parse the correct dependency', () => {
|
||||
const viewParticles = parse('Comp(this.flag)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toContain(0);
|
||||
});
|
||||
|
||||
it('should parse the correct dependency when interfacing the dependency chain', () => {
|
||||
const viewParticles = parse('Comp(this.doubleCount)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
const dependency = content?.dependencyIndexArr;
|
||||
// ---- doubleCount depends on count, count depends on flag
|
||||
// so doubleCount depends on flag, count and doubleCount
|
||||
expect(dependency).toContain(availableProperties.indexOf('flag'));
|
||||
expect(dependency).toContain(availableProperties.indexOf('count'));
|
||||
expect(dependency).toContain(availableProperties.indexOf('doubleCount'));
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the property is not in the availableProperties', () => {
|
||||
const viewParticles = parse('Comp(this.notExist)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the identifier is not an property of a ThisExpression', () => {
|
||||
const viewParticles = parse('Comp(count)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the member expression is in an escaped function', () => {
|
||||
let viewParticles = parse('Comp(escape(this.flag))');
|
||||
let content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
|
||||
viewParticles = parse('Comp($(this.flag))');
|
||||
content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the member expression is in a manual function', () => {
|
||||
const viewParticles = parse('Comp(manual(() => this.count, []))');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should parse the dependencies in manual function's second parameter", () => {
|
||||
const viewParticles = parse('Comp(manual(() => {let a = this.count}, [this.flag]))');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the member expression is the left side of an assignment expression', () => {
|
||||
const viewParticles = parse('Comp(this.flag = 1)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the member expression is right side of an assignment expression', () => {
|
||||
const viewParticles = parse('Comp(this.flag = this.flag + 1)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should parse the dependency as identifiers', () => {
|
||||
reactivityConfig.dependencyParseType = 'identifier';
|
||||
const viewParticles = parse('Comp(flag + count)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toContain(availableProperties.indexOf('flag'));
|
||||
expect(content?.dependencyIndexArr).toContain(availableProperties.indexOf('count'));
|
||||
reactivityConfig.dependencyParseType = 'property';
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import { expect, describe, it } from 'vitest';
|
||||
import { parse } from './mock';
|
||||
import { type HTMLParticle, type ExpParticle, type CompParticle } from '../types';
|
||||
import { type types as t } from '@babel/core';
|
||||
|
||||
describe('MutableTagParticle', () => {
|
||||
// ---- HTML
|
||||
it('should parse an HTMLUnit with dynamic tag as an HTMLParticle', () => {
|
||||
const viewParticles = parse('tag(this.div)()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('html');
|
||||
});
|
||||
|
||||
it('should parse an HTMLUnit with no children as an HTMLParticle', () => {
|
||||
const viewParticles = parse('div()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('html');
|
||||
});
|
||||
|
||||
it('should parse an HTMLUnit with non-static-html children as an HTMLParticle', () => {
|
||||
const viewParticles = parse('div(); { Comp(); tag(this.div)(); }');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('html');
|
||||
});
|
||||
|
||||
it('should not parse an HTMLUnit with potential TemplateUnit as an HTMLParticle', () => {
|
||||
const viewParticles = parse('div(); { div() }');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).not.toBe('html');
|
||||
});
|
||||
|
||||
it('should parse an HTMLUnit with dynamic tag with dependencies as an ExpParticle', () => {
|
||||
const viewParticles = parse('tag(this.flag)()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('exp');
|
||||
const content = (viewParticles[0] as ExpParticle).content;
|
||||
|
||||
expect((content.value as t.StringLiteral).value).toBe(Object.keys(content.viewPropMap!)[0]);
|
||||
const htmlParticle = content.viewPropMap![Object.keys(content.viewPropMap!)[0]][0] as HTMLParticle;
|
||||
expect(htmlParticle.type).toBe('html');
|
||||
});
|
||||
|
||||
// ---- Comp
|
||||
it('should parse a CompUnit with dynamic tag as an HTMLParticle', () => {
|
||||
const viewParticles = parse('Comp()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('comp');
|
||||
});
|
||||
|
||||
it('should parse a CompUnit with dynamic tag with dependencies as an ExpParticle', () => {
|
||||
const viewParticles = parse('comp(CompList[this.flag])()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('exp');
|
||||
const content = (viewParticles[0] as ExpParticle).content;
|
||||
|
||||
expect((content.value as t.StringLiteral).value).toBe(Object.keys(content.viewPropMap!)[0]);
|
||||
const compParticle = content.viewPropMap![Object.keys(content.viewPropMap!)[0]][0] as CompParticle;
|
||||
|
||||
expect(compParticle.type).toBe('comp');
|
||||
});
|
||||
|
||||
// ---- Snippet
|
||||
it('should parse a SnippetUnit as an HTMLParticle', () => {
|
||||
const viewParticles = parse('this.MySnippet()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('snippet');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
import { expect, describe, it } from 'vitest';
|
||||
import { parse } from './mock';
|
||||
import { type ForParticle } from '../../dist';
|
||||
import { type HTMLParticle } from '../types';
|
||||
|
||||
describe('OtherParticle', () => {
|
||||
it('should parse a TextUnit as a TextParticle', () => {
|
||||
const viewParticles = parse('"Hello World"');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('text');
|
||||
});
|
||||
|
||||
it('should parse an IfUnit as an IfParticle', () => {
|
||||
const viewParticles = parse('if(this.flag) { div() }');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('if');
|
||||
});
|
||||
|
||||
it('should parse an IfUnit as an SwitchParticle', () => {
|
||||
const viewParticles = parse('switch(this.flag) { }');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('switch');
|
||||
});
|
||||
|
||||
it('should parse a ForUnit as a ForParticle', () => {
|
||||
const viewParticles = parse('for(const item of this.items) { div() }');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('for');
|
||||
});
|
||||
|
||||
it("should correctly parse ForUnit's item dependencies from array", () => {
|
||||
console.log('this');
|
||||
const viewParticles = parse('for(const item of this.array[this.count]) { div(item) }');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('for');
|
||||
|
||||
const divParticle = (viewParticles[0] as ForParticle).children[0] as HTMLParticle;
|
||||
const divDependency = divParticle.props?.textContent?.dependencyIndexArr;
|
||||
expect(divDependency).toContain(0);
|
||||
expect(divDependency).toContain(1);
|
||||
expect(divDependency).toContain(3);
|
||||
});
|
||||
|
||||
it("should correctly parse ForUnit's deconstruct item dependencies from array", () => {
|
||||
const viewParticles = parse('for(const { idx, item } of this.array[this.count]) { div(item) }');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('for');
|
||||
|
||||
const divParticle = (viewParticles[0] as ForParticle).children[0] as HTMLParticle;
|
||||
const divDependency = divParticle.props?.textContent?.dependencyIndexArr;
|
||||
expect(divDependency).toContain(0);
|
||||
expect(divDependency).toContain(1);
|
||||
expect(divDependency).toContain(3);
|
||||
});
|
||||
|
||||
it('should parse a EnvUnit as a EnvParticle', () => {
|
||||
const viewParticles = parse('env().count(2); { div() }');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('env');
|
||||
});
|
||||
|
||||
it('should parse a ExpUnit as a ExpParticle', () => {
|
||||
const viewParticles = parse('this.flag');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('exp');
|
||||
});
|
||||
|
||||
it('should parse a TryUnit as a TryParticle', () => {
|
||||
const viewParticles = parse('try { div() } catch(e) { div() }');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('try');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import { expect, describe, it } from 'vitest';
|
||||
import { parse } from './mock';
|
||||
import { type TemplateParticle } from '../types';
|
||||
import { types as t } from '@babel/core';
|
||||
|
||||
describe('TemplateUnit', () => {
|
||||
it('should not parse a single HTMLUnit to a TemplateUnit', () => {
|
||||
const viewParticles = parse('div()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).not.toBe('template');
|
||||
});
|
||||
|
||||
it('should parse a nested HTMLUnit to a TemplateUnit', () => {
|
||||
const viewParticles = parse('div(); {div()}');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('template');
|
||||
});
|
||||
|
||||
it("should correctly parse a nested HTMLUnit's structure into a template", () => {
|
||||
const viewParticles = parse('div(); {div()}');
|
||||
const template = (viewParticles[0] as any).template;
|
||||
expect(t.isStringLiteral(template.tag, { value: 'div' })).toBe(true);
|
||||
expect(template.children).toHaveLength(1);
|
||||
expect(t.isStringLiteral(template.children[0].tag, { value: 'div' })).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly parse the path of TemplateParticle's dynamic props in root element", () => {
|
||||
const viewParticles = parse('div().class(this.flag); {div("ok")}');
|
||||
|
||||
const dynamicProps = (viewParticles[0] as TemplateParticle).props;
|
||||
expect(dynamicProps).toHaveLength(1);
|
||||
const prop = dynamicProps[0];
|
||||
// ---- Path will be [] because it's the root element
|
||||
expect(prop.path).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should correctly parse the path of TemplateParticle's dynamic props in nested element", () => {
|
||||
const viewParticles = parse('div(); {div().class(this.flag)}');
|
||||
|
||||
const dynamicProps = (viewParticles[0] as TemplateParticle).props;
|
||||
expect(dynamicProps).toHaveLength(1);
|
||||
const prop = dynamicProps[0];
|
||||
// ---- Path will be [0] because it's the first child of the root element
|
||||
expect(prop.path).toHaveLength(1);
|
||||
expect(prop.path[0]).toBe(0);
|
||||
});
|
||||
|
||||
it("should correctly parse the path of TemplateParticle's dynamic props with mutable particles ahead", () => {
|
||||
const viewParticles = parse('div(); { Comp(); div().class(this.flag) }');
|
||||
|
||||
const dynamicProps = (viewParticles[0] as TemplateParticle).props;
|
||||
expect(dynamicProps).toHaveLength(1);
|
||||
const prop = dynamicProps[0];
|
||||
// ---- Path will be [0] because it's the first child of the root element
|
||||
expect(prop.path).toHaveLength(1);
|
||||
expect(prop.path[0]).toBe(0);
|
||||
});
|
||||
|
||||
it("should correctly parse the path of TemplateParticle's mutableParticles", () => {
|
||||
const viewParticles = parse('div(); {div(); Comp(); div();}');
|
||||
const mutableParticles = (viewParticles[0] as TemplateParticle).mutableParticles;
|
||||
expect(mutableParticles).toHaveLength(1);
|
||||
|
||||
const mutableParticle = mutableParticles[0];
|
||||
expect(mutableParticle.path).toHaveLength(1);
|
||||
expect(mutableParticle.path[0]).toBe(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,250 @@
|
|||
import babelApi, { parseSync, type types as t } from '@babel/core';
|
||||
import { type ReactivityParserConfig } from '../types';
|
||||
import { parseView as pV, type ViewParserConfig } from '@inula/view-parser';
|
||||
import { parseReactivity as pR } from '../index';
|
||||
|
||||
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',
|
||||
];
|
||||
const snippetNames = ['MySnippet', 'InnerButton'];
|
||||
|
||||
export const availableProperties = ['flag', 'count', 'doubleCount', 'array', 'state1', 'state2', 'state3', 'state4'];
|
||||
const dependencyMap = {
|
||||
count: ['flag'],
|
||||
doubleCount: ['count', 'flag'],
|
||||
array: ['count', 'flag'],
|
||||
state1: ['count', 'flag'],
|
||||
state2: ['count', 'flag', 'state1'],
|
||||
state3: ['count', 'flag', 'state1', 'state2'],
|
||||
state4: ['count', 'flag', 'state1', 'state2', 'state3'],
|
||||
};
|
||||
|
||||
const viewConfig: ViewParserConfig = {
|
||||
babelApi,
|
||||
htmlTags,
|
||||
snippetNames,
|
||||
};
|
||||
|
||||
export const reactivityConfig: ReactivityParserConfig = {
|
||||
babelApi,
|
||||
availableProperties,
|
||||
dependencyMap,
|
||||
};
|
||||
|
||||
export function parseCode(code: string) {
|
||||
return (parseSync(`function code() {${code}}`)!.program.body[0] as t.FunctionDeclaration).body;
|
||||
}
|
||||
|
||||
function parseViewFromStatement(statement: t.BlockStatement) {
|
||||
return pV(statement, viewConfig);
|
||||
}
|
||||
|
||||
export function parseView(code: string) {
|
||||
return parseViewFromStatement(parseCode(code));
|
||||
}
|
||||
|
||||
export function parseReactivity(statement: t.BlockStatement) {
|
||||
return pR(parseViewFromStatement(statement), reactivityConfig)[0];
|
||||
}
|
||||
|
||||
export function parse(code: string) {
|
||||
return pR(parseView(code), reactivityConfig)[0];
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import type Babel from '@babel/core';
|
||||
|
||||
export interface DependencyValue<T> {
|
||||
value: T;
|
||||
dynamic: boolean;
|
||||
dependencyIndexArr: number[];
|
||||
dependenciesNode: t.ArrayExpression;
|
||||
}
|
||||
|
||||
export interface DependencyProp {
|
||||
value: t.Expression;
|
||||
viewPropMap: Record<string, ViewParticle[]>;
|
||||
dynamic: boolean;
|
||||
dependencyIndexArr: number[];
|
||||
dependenciesNode: t.ArrayExpression;
|
||||
}
|
||||
|
||||
export interface TemplateProp {
|
||||
tag: string;
|
||||
key: string;
|
||||
path: number[];
|
||||
value: t.Expression;
|
||||
dynamic: boolean;
|
||||
dependencyIndexArr: number[];
|
||||
dependenciesNode: t.ArrayExpression;
|
||||
}
|
||||
|
||||
export type MutableParticle = ViewParticle & { path: number[] };
|
||||
|
||||
export interface TemplateParticle {
|
||||
type: 'template';
|
||||
template: HTMLParticle;
|
||||
mutableParticles: MutableParticle[];
|
||||
props: TemplateProp[];
|
||||
}
|
||||
|
||||
export interface TextParticle {
|
||||
type: 'text';
|
||||
content: DependencyValue<t.Expression>;
|
||||
}
|
||||
|
||||
export interface HTMLParticle {
|
||||
type: 'html';
|
||||
tag: t.Expression;
|
||||
props: Record<string, DependencyValue<t.Expression>>;
|
||||
children: ViewParticle[];
|
||||
}
|
||||
|
||||
export interface CompParticle {
|
||||
type: 'comp';
|
||||
tag: t.Expression;
|
||||
props: Record<string, DependencyProp>;
|
||||
children: ViewParticle[];
|
||||
}
|
||||
|
||||
export interface ForParticle {
|
||||
type: 'for';
|
||||
item: t.LVal;
|
||||
array: DependencyValue<t.Expression>;
|
||||
key: t.Expression;
|
||||
children: ViewParticle[];
|
||||
}
|
||||
|
||||
export interface IfBranch {
|
||||
condition: DependencyValue<t.Expression>;
|
||||
children: ViewParticle[];
|
||||
}
|
||||
|
||||
export interface IfParticle {
|
||||
type: 'if';
|
||||
branches: IfBranch[];
|
||||
}
|
||||
|
||||
export interface SwitchBranch {
|
||||
case: DependencyValue<t.Expression>;
|
||||
children: ViewParticle[];
|
||||
break: boolean;
|
||||
}
|
||||
|
||||
export interface SwitchParticle {
|
||||
type: 'switch';
|
||||
discriminant: DependencyValue<t.Expression>;
|
||||
branches: SwitchBranch[];
|
||||
}
|
||||
|
||||
export interface TryParticle {
|
||||
type: 'try';
|
||||
children: ViewParticle[];
|
||||
exception: t.Identifier | t.ArrayPattern | t.ObjectPattern | null;
|
||||
catchChildren: ViewParticle[];
|
||||
}
|
||||
|
||||
export interface EnvParticle {
|
||||
type: 'env';
|
||||
props: Record<string, DependencyProp>;
|
||||
children: ViewParticle[];
|
||||
}
|
||||
|
||||
export interface ExpParticle {
|
||||
type: 'exp';
|
||||
content: DependencyProp;
|
||||
props: Record<string, DependencyProp>;
|
||||
}
|
||||
|
||||
export interface SnippetParticle {
|
||||
type: 'snippet';
|
||||
tag: string;
|
||||
props: Record<string, DependencyProp>;
|
||||
children: ViewParticle[];
|
||||
}
|
||||
|
||||
export type ViewParticle =
|
||||
| TemplateParticle
|
||||
| TextParticle
|
||||
| HTMLParticle
|
||||
| CompParticle
|
||||
| ForParticle
|
||||
| IfParticle
|
||||
| EnvParticle
|
||||
| ExpParticle
|
||||
| SwitchParticle
|
||||
| SnippetParticle
|
||||
| TryParticle;
|
||||
|
||||
export interface ReactivityParserConfig {
|
||||
babelApi: typeof Babel;
|
||||
availableProperties: string[];
|
||||
availableIdentifiers?: string[];
|
||||
dependencyMap: Record<string, string[]>;
|
||||
identifierDepMap?: Record<string, string[]>;
|
||||
dependencyParseType?: 'property' | 'identifier';
|
||||
parseTemplate?: boolean;
|
||||
reactivityFuncNames?: string[];
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "@inula/view-generator",
|
||||
"version": "0.0.1",
|
||||
"author": {
|
||||
"name": "IanDx",
|
||||
"email": "iandxssxx@gmail.com"
|
||||
},
|
||||
"keywords": [
|
||||
"dlight.js"
|
||||
],
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup --sourcemap"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/node": "^20.10.5",
|
||||
"tsup": "^6.7.0",
|
||||
"typescript": "^5.3.2",
|
||||
"@inula/reactivity-parser": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inula/error-handler": "workspace:*"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"format": [
|
||||
"cjs",
|
||||
"esm"
|
||||
],
|
||||
"clean": true,
|
||||
"dts": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
import { type types as t, type traverse } from '@babel/core';
|
||||
import { type ViewParticle } from '@inula/reactivity-parser';
|
||||
import { type SnippetPropMap, type ViewGeneratorConfig } from '../types';
|
||||
import ViewGenerator from '../ViewGenerator';
|
||||
|
||||
export const prefixMap = { template: '$t', node: '$node' };
|
||||
|
||||
export default class BaseGenerator {
|
||||
readonly viewParticle: ViewParticle;
|
||||
readonly config: ViewGeneratorConfig;
|
||||
|
||||
readonly t: typeof t;
|
||||
readonly traverse: typeof traverse;
|
||||
readonly className: string;
|
||||
readonly importMap: Record<string, string>;
|
||||
readonly snippetPropMap: SnippetPropMap;
|
||||
readonly elementAttributeMap;
|
||||
readonly alterAttributeMap;
|
||||
|
||||
readonly viewGenerator;
|
||||
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param viewUnit
|
||||
* @param config
|
||||
*/
|
||||
constructor(viewParticle: ViewParticle, config: ViewGeneratorConfig) {
|
||||
this.viewParticle = viewParticle;
|
||||
this.config = config;
|
||||
this.t = config.babelApi.types;
|
||||
this.traverse = config.babelApi.traverse;
|
||||
this.className = config.className;
|
||||
this.importMap = config.importMap;
|
||||
this.snippetPropMap = config.snippetPropMap;
|
||||
this.viewGenerator = new ViewGenerator(config);
|
||||
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;
|
||||
}, {})
|
||||
: {};
|
||||
this.alterAttributeMap = config.alterAttributeMap;
|
||||
}
|
||||
|
||||
// ---- Init Statements
|
||||
private readonly initStatements: t.Statement[] = [];
|
||||
addInitStatement(...statements: (t.Statement | null)[]) {
|
||||
this.initStatements.push(...(statements.filter(Boolean) as t.Statement[]));
|
||||
}
|
||||
|
||||
// ---- Added Class Properties, typically used in for Template
|
||||
private readonly classProperties: t.ClassProperty[] = [];
|
||||
addStaticClassProperty(key: string, value: t.Expression) {
|
||||
this.classProperties.push(
|
||||
this.t.classProperty(this.t.identifier(key), value, undefined, undefined, undefined, true)
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Update Statements
|
||||
private readonly updateStatements: Record<number, t.Statement[]> = {};
|
||||
addUpdateStatements(dependencies: number[] | undefined, statement: t.Statement | undefined | null) {
|
||||
if (!dependencies || dependencies.length === 0) return;
|
||||
const depNum = BaseGenerator.calcDependencyNum(dependencies);
|
||||
if (!this.updateStatements[depNum]) this.updateStatements[depNum] = [];
|
||||
if (statement) this.updateStatements[depNum].push(statement);
|
||||
}
|
||||
|
||||
addUpdateStatementsWithoutDep(statement: t.Statement) {
|
||||
if (!this.updateStatements[0]) this.updateStatements[0] = [];
|
||||
this.updateStatements[0].push(statement);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns [initStatements, updateStatements, classProperties, nodeName]
|
||||
*/
|
||||
generate(): [t.Statement[], Record<number, t.Statement[]>, t.ClassProperty[], string] {
|
||||
const nodeName = this.run();
|
||||
return [this.initStatements, this.updateStatements, this.classProperties, nodeName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generate the view given the view particles, mainly used for child particles parsing
|
||||
* @param viewParticles
|
||||
* @param mergeStatements
|
||||
* @returns [initStatements, topLevelNodes, updateStatements]
|
||||
*/
|
||||
generateChildren(
|
||||
viewParticles: ViewParticle[],
|
||||
mergeStatements = true,
|
||||
newIdx = false
|
||||
): [t.Statement[], string[], Record<number, t.Statement[]>, number] {
|
||||
this.viewGenerator.nodeIdx = newIdx ? -1 : this.nodeIdx;
|
||||
this.viewGenerator.templateIdx = this.templateIdx;
|
||||
const [initStatements, updateStatements, classProperties, topLevelNodes] =
|
||||
this.viewGenerator.generateChildren(viewParticles);
|
||||
if (!newIdx) this.nodeIdx = this.viewGenerator.nodeIdx;
|
||||
this.templateIdx = this.viewGenerator.templateIdx;
|
||||
this.classProperties.push(...classProperties);
|
||||
if (mergeStatements) this.mergeStatements(updateStatements);
|
||||
|
||||
return [initStatements, topLevelNodes, updateStatements, this.viewGenerator.nodeIdx];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Merge the update statements
|
||||
* @param statements
|
||||
*/
|
||||
private mergeStatements(statements: Record<number, t.Statement[]>): void {
|
||||
Object.entries(statements).forEach(([depNum, statements]) => {
|
||||
if (!this.updateStatements[Number(depNum)]) {
|
||||
this.updateStatements[Number(depNum)] = [];
|
||||
}
|
||||
this.updateStatements[Number(depNum)].push(...statements);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generate the view given the view particle
|
||||
* @param viewParticle
|
||||
* @param mergeStatements
|
||||
* @returns [initStatements, nodeName, updateStatements]
|
||||
*/
|
||||
generateChild(
|
||||
viewParticle: ViewParticle,
|
||||
mergeStatements = true,
|
||||
newIdx = false
|
||||
): [t.Statement[], string, Record<number, t.Statement[]>, number] {
|
||||
this.viewGenerator.nodeIdx = newIdx ? -1 : this.nodeIdx;
|
||||
this.viewGenerator.templateIdx = this.templateIdx;
|
||||
const [initStatements, updateStatements, classProperties, nodeName] =
|
||||
this.viewGenerator.generateChild(viewParticle);
|
||||
if (!newIdx) this.nodeIdx = this.viewGenerator.nodeIdx;
|
||||
this.templateIdx = this.viewGenerator.templateIdx;
|
||||
this.classProperties.push(...classProperties);
|
||||
if (mergeStatements) this.mergeStatements(updateStatements);
|
||||
|
||||
return [initStatements, nodeName, updateStatements, this.viewGenerator.nodeIdx];
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* const $update = (changed) => { ${updateStatements} }
|
||||
*/
|
||||
geneUpdateFunc(updateStatements: Record<number, t.Statement[]>): t.Statement {
|
||||
return this.t.variableDeclaration('const', [
|
||||
this.t.variableDeclarator(
|
||||
this.t.identifier('$update'),
|
||||
this.t.arrowFunctionExpression([this.t.identifier('$changed')], this.geneUpdateBody(updateStatements))
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
get updateParams() {
|
||||
return [this.t.identifier('$changed')];
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* (changed) => {
|
||||
* if (changed & 1) {
|
||||
* ...
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
geneUpdateBody(updateStatements: Record<number, t.Statement[]>): t.BlockStatement {
|
||||
return this.t.blockStatement([
|
||||
...Object.entries(updateStatements)
|
||||
.filter(([depNum]) => depNum !== '0')
|
||||
.map(([depNum, statements]) => {
|
||||
return this.t.ifStatement(
|
||||
this.t.binaryExpression('&', this.t.identifier('$changed'), this.t.numericLiteral(Number(depNum))),
|
||||
this.t.blockStatement(statements)
|
||||
);
|
||||
}),
|
||||
...(updateStatements[0] ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* let node1, node2, ...
|
||||
*/
|
||||
declareNodes(nodeIdx: number): t.VariableDeclaration[] {
|
||||
if (nodeIdx === -1) return [];
|
||||
return [
|
||||
this.t.variableDeclaration(
|
||||
'let',
|
||||
Array.from({ length: nodeIdx + 1 }, (_, i) =>
|
||||
this.t.variableDeclarator(this.t.identifier(`${prefixMap.node}${i}`))
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* return [${topLevelNodes}]
|
||||
*/
|
||||
generateReturnStatement(topLevelNodes: string[]): t.ReturnStatement {
|
||||
return this.t.returnStatement(this.t.arrayExpression(topLevelNodes.map(name => this.t.identifier(name))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief To be implemented by the subclass as the main node generation function
|
||||
* @returns dlNodeName
|
||||
*/
|
||||
run(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
// ---- Name ----
|
||||
// ---- Used as dlNodeName for any node declaration
|
||||
nodeIdx = -1;
|
||||
generateNodeName(idx?: number): string {
|
||||
return `${prefixMap.node}${idx ?? ++this.nodeIdx}`;
|
||||
}
|
||||
|
||||
// ---- Used as template generation as class property
|
||||
templateIdx = -1;
|
||||
generateTemplateName(): string {
|
||||
return `${prefixMap.template}${++this.templateIdx}`;
|
||||
}
|
||||
|
||||
// ---- @Utils -----
|
||||
/**
|
||||
*
|
||||
* @param updateStatements
|
||||
* @returns
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Calculate the dependency number from an array of dependency index
|
||||
* e.g.
|
||||
* [0, 1, 2] => 0b111 => 7
|
||||
* [1, 3] => 0b1010 => 10
|
||||
* @param dependencies
|
||||
* @returns dependency number
|
||||
*/
|
||||
static calcDependencyNum(dependencies: number[] | undefined): number {
|
||||
if (!dependencies || dependencies.length === 0) return 0;
|
||||
dependencies = [...new Set(dependencies)];
|
||||
return dependencies.reduce((acc, dep) => acc + (1 << dep), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wrap the value in a file
|
||||
* @param node
|
||||
* @returns wrapped value
|
||||
*/
|
||||
valueWrapper(node: t.Expression | t.Statement): t.File {
|
||||
return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName} && ${expression}
|
||||
*/
|
||||
optionalExpression(dlNodeName: string, expression: t.Expression): t.Statement {
|
||||
return this.t.expressionStatement(this.t.logicalExpression('&&', this.t.identifier(dlNodeName), expression));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shorthand function for collecting statements in batch
|
||||
* @returns [statements, collect]
|
||||
*/
|
||||
static statementsCollector(): [t.Statement[], (...statements: t.Statement[] | t.Statement[][]) => void] {
|
||||
const statements: t.Statement[] = [];
|
||||
const collect = (...newStatements: t.Statement[] | t.Statement[][]) => {
|
||||
newStatements.forEach(s => {
|
||||
if (Array.isArray(s)) {
|
||||
statements.push(...s);
|
||||
} else {
|
||||
statements.push(s);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return [statements, collect];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import BaseGenerator from './BaseGenerator';
|
||||
|
||||
export default class CondGenerator extends BaseGenerator {
|
||||
/**
|
||||
* @View
|
||||
* $thisCond.cond = ${idx}
|
||||
*/
|
||||
geneCondIdx(idx: number): t.ExpressionStatement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')),
|
||||
this.t.numericLiteral(idx)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* if ($thisCond.cond === ${idx}) {
|
||||
* $thisCond.didntChange = true
|
||||
* return []
|
||||
* }
|
||||
*/
|
||||
geneCondCheck(idx: number): t.IfStatement {
|
||||
return this.t.ifStatement(
|
||||
this.t.binaryExpression(
|
||||
'===',
|
||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')),
|
||||
this.t.numericLiteral(idx)
|
||||
),
|
||||
this.t.blockStatement([
|
||||
this.t.expressionStatement(
|
||||
this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('didntChange')),
|
||||
this.t.booleanLiteral(true)
|
||||
)
|
||||
),
|
||||
this.t.returnStatement(this.t.arrayExpression([])),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}?.updateCond(key)
|
||||
*/
|
||||
updateCondNodeCond(dlNodeName: string): t.Statement {
|
||||
return this.optionalExpression(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('updateCond')), [
|
||||
...this.updateParams.slice(1),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}?.update(changed)
|
||||
*/
|
||||
updateCondNode(dlNodeName: string): t.Statement {
|
||||
return this.optionalExpression(
|
||||
dlNodeName,
|
||||
this.t.callExpression(
|
||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')),
|
||||
this.updateParams
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName} = new CondNode(${depNum}, ($thisCond) => {})
|
||||
*/
|
||||
declareCondNode(dlNodeName: string, condFunc: t.BlockStatement, deps: number[]): t.Statement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.newExpression(this.t.identifier(this.importMap.CondNode), [
|
||||
this.t.numericLiteral(CondGenerator.calcDependencyNum(deps)),
|
||||
this.t.arrowFunctionExpression([this.t.identifier('$thisCond')], condFunc),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* return $thisCond.cond === ${branchIdx} ? [${nodeNames}] : $thisCond.updateCond()
|
||||
*/
|
||||
geneCondReturnStatement(nodeNames: string[], branchIdx: number): t.Statement {
|
||||
// ---- If the returned cond is not the last one,
|
||||
// it means it's been altered in the childrenNodes,
|
||||
// so we update the cond again to get the right one
|
||||
return this.t.returnStatement(
|
||||
this.t.conditionalExpression(
|
||||
this.t.binaryExpression(
|
||||
'===',
|
||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')),
|
||||
this.t.numericLiteral(branchIdx)
|
||||
),
|
||||
this.t.arrayExpression(nodeNames.map(name => this.t.identifier(name))),
|
||||
this.t.callExpression(
|
||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('updateCond')),
|
||||
this.updateParams.slice(1)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import PropViewGenerator from './PropViewGenerator';
|
||||
|
||||
export default class ElementGenerator extends PropViewGenerator {
|
||||
/**
|
||||
* @View
|
||||
* el:
|
||||
* View.addDidMount(${dlNodeName}, () => (
|
||||
* typeof ${value} === "function" ? ${value}($nodeEl) : ${value} = $nodeEl
|
||||
* ))
|
||||
* not el:
|
||||
* typeof ${value} === "function" ? ${value}($nodeEl) : ${value} = $nodeEl
|
||||
* @param el true: dlNodeName._$el, false: dlNodeName
|
||||
*/
|
||||
initElement(dlNodeName: string, value: t.Expression, el = false): t.Statement {
|
||||
const elNode = el
|
||||
? this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$el'))
|
||||
: this.t.identifier(dlNodeName);
|
||||
let elementNode;
|
||||
if (this.isOnlyMemberExpression(value)) {
|
||||
elementNode = this.t.conditionalExpression(
|
||||
this.t.binaryExpression('===', this.t.unaryExpression('typeof', value, true), this.t.stringLiteral('function')),
|
||||
this.t.callExpression(value, [elNode]),
|
||||
this.t.assignmentExpression('=', value as t.LVal, elNode)
|
||||
);
|
||||
} else {
|
||||
elementNode = this.t.callExpression(value, [elNode]);
|
||||
}
|
||||
|
||||
return el
|
||||
? this.t.expressionStatement(
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier('View'), this.t.identifier('addDidMount')), [
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.arrowFunctionExpression([], elementNode),
|
||||
])
|
||||
)
|
||||
: this.t.expressionStatement(elementNode);
|
||||
}
|
||||
|
||||
// --- Utils
|
||||
private isOnlyMemberExpression(value: t.Expression): boolean {
|
||||
if (!this.t.isMemberExpression(value)) return false;
|
||||
while (value.property) {
|
||||
if (this.t.isMemberExpression(value.property)) {
|
||||
value = value.property;
|
||||
continue;
|
||||
} else if (this.t.isIdentifier(value.property)) break;
|
||||
else return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import ElementGenerator from './ElementGenerator';
|
||||
|
||||
export default class ForwardPropsGenerator extends ElementGenerator {
|
||||
/**
|
||||
* @View
|
||||
* this._$forwardProp(${dlNodeName})
|
||||
*/
|
||||
forwardProps(dlNodeName: string): t.ExpressionStatement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.callExpression(this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$addForwardProps')), [
|
||||
this.t.identifier(dlNodeName),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,363 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import { DLError } from '../error';
|
||||
import ForwardPropGenerator from './ForwardPropGenerator';
|
||||
|
||||
export default class HTMLPropGenerator extends ForwardPropGenerator {
|
||||
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',
|
||||
]);
|
||||
|
||||
/**
|
||||
* @brief Add any HTML props according to the key
|
||||
* @param name
|
||||
* @param tag
|
||||
* @param key
|
||||
* @param value
|
||||
* @param dependencyIndexArr
|
||||
* @returns t.Statement
|
||||
*/
|
||||
addHTMLProp(
|
||||
name: string,
|
||||
tag: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
dynamic: boolean,
|
||||
dependencyIndexArr: number[],
|
||||
dependenciesNode: t.ArrayExpression
|
||||
): t.Statement | null {
|
||||
// ---- Dynamic HTML prop with init and update
|
||||
if (dynamic) {
|
||||
this.addUpdateStatements(
|
||||
dependencyIndexArr,
|
||||
this.setDynamicHTMLProp(name, tag, key, value, dependenciesNode, true)
|
||||
);
|
||||
return this.setDynamicHTMLProp(name, tag, key, value, dependenciesNode, false);
|
||||
}
|
||||
// ---- Static HTML prop with init only
|
||||
return this.setStaticHTMLProp(name, tag, key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* insertNode(${dlNodeName}, ${childNodeName}, ${position})
|
||||
*/
|
||||
insertNode(dlNodeName: string, childNodeName: string, position: number): t.ExpressionStatement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.callExpression(this.t.identifier(this.importMap.insertNode), [
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.identifier(childNodeName),
|
||||
this.t.numericLiteral(position),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName} && ${expression}
|
||||
*/
|
||||
private setPropWithCheck(dlNodeName: string, expression: t.Expression, check: boolean): t.Statement {
|
||||
if (check) {
|
||||
return this.optionalExpression(dlNodeName, expression);
|
||||
}
|
||||
return this.t.expressionStatement(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setStyle(${dlNodeName}, ${value})
|
||||
*/
|
||||
private setHTMLStyle(dlNodeName: string, value: t.Expression, check: boolean): t.Statement {
|
||||
return this.setPropWithCheck(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.identifier(this.importMap.setStyle), [this.t.identifier(dlNodeName), value]),
|
||||
check
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setStyle(${dlNodeName}, ${value})
|
||||
*/
|
||||
private setHTMLDataset(dlNodeName: string, value: t.Expression, check: boolean): t.Statement {
|
||||
return this.setPropWithCheck(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.identifier(this.importMap.setDataset), [this.t.identifier(dlNodeName), value]),
|
||||
check
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}.${key} = ${value}
|
||||
*/
|
||||
private setHTMLProp(dlNodeName: string, key: string, value: t.Expression): t.Statement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier(key)),
|
||||
value
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}.setAttribute(${key}, ${value})
|
||||
*/
|
||||
private setHTMLAttr(dlNodeName: string, key: string, value: t.Expression): t.Statement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('setAttribute')), [
|
||||
this.t.stringLiteral(key),
|
||||
value,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}.addEventListener(${key}, ${value})
|
||||
*/
|
||||
private setHTMLEvent(dlNodeName: string, key: string, value: t.Expression): t.Statement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.callExpression(
|
||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('addEventListener')),
|
||||
[this.t.stringLiteral(key), value]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setEvent(${dlNodeName}, ${key}, ${value})
|
||||
*/
|
||||
private setEvent(dlNodeName: string, key: string, value: t.Expression, check: boolean): t.Statement {
|
||||
return this.setPropWithCheck(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.identifier(this.importMap.setEvent), [
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.stringLiteral(key),
|
||||
value,
|
||||
]),
|
||||
check
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* delegateEvent(${dlNodeName}, ${key}, ${value})
|
||||
*/
|
||||
private delegateEvent(dlNodeName: string, key: string, value: t.Expression, check: boolean): t.Statement {
|
||||
return this.setPropWithCheck(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.identifier(this.importMap.delegateEvent), [
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.stringLiteral(key),
|
||||
value,
|
||||
]),
|
||||
check
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setHTMLProp(${dlNodeName}, ${key}, ${valueFunc}, ${dependenciesNode})
|
||||
*/
|
||||
private setCachedProp(
|
||||
dlNodeName: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
dependenciesNode: t.ArrayExpression,
|
||||
check: boolean
|
||||
): t.Statement {
|
||||
return this.setPropWithCheck(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.identifier(this.importMap.setHTMLProp), [
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.stringLiteral(key),
|
||||
this.t.arrowFunctionExpression([], value),
|
||||
dependenciesNode,
|
||||
]),
|
||||
check
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setHTMLAttr(${dlNodeName}, ${key}, ${valueFunc}, ${dependenciesNode}, ${check})
|
||||
*/
|
||||
private setCachedAttr(
|
||||
dlNodeName: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
dependenciesNode: t.ArrayExpression,
|
||||
check: boolean
|
||||
): t.Statement {
|
||||
return this.setPropWithCheck(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.identifier(this.importMap.setHTMLAttr), [
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.stringLiteral(key),
|
||||
this.t.arrowFunctionExpression([], value),
|
||||
dependenciesNode,
|
||||
]),
|
||||
check
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setHTMLProps(${dlNodeName}, ${value})
|
||||
*/
|
||||
private setHTMLPropObject(dlNodeName: string, value: t.Expression, check: boolean): t.Statement {
|
||||
return this.setPropWithCheck(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.identifier(this.importMap.setHTMLProps), [this.t.identifier(dlNodeName), value]),
|
||||
check
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* setHTMLAttrs(${dlNodeName}, ${value})
|
||||
*/
|
||||
private setHTMLAttrObject(dlNodeName: string, value: t.Expression, check: boolean): t.Statement {
|
||||
return this.setPropWithCheck(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.identifier(this.importMap.setHTMLAttrs), [this.t.identifier(dlNodeName), value]),
|
||||
check
|
||||
);
|
||||
}
|
||||
|
||||
private static commonHTMLPropKeys = [
|
||||
'style',
|
||||
'dataset',
|
||||
'props',
|
||||
'ref',
|
||||
'attrs',
|
||||
'forwardProps',
|
||||
...HTMLPropGenerator.lifecycle,
|
||||
];
|
||||
|
||||
/**
|
||||
* For style/dataset/ref/attr/prop
|
||||
*/
|
||||
private addCommonHTMLProp(
|
||||
dlNodeName: string,
|
||||
attrName: string,
|
||||
value: t.Expression,
|
||||
check: boolean
|
||||
): t.Statement | null {
|
||||
if (HTMLPropGenerator.lifecycle.includes(attrName as (typeof HTMLPropGenerator.lifecycle)[number])) {
|
||||
if (!check) return this.addLifecycle(dlNodeName, attrName as (typeof HTMLPropGenerator.lifecycle)[number], value);
|
||||
return null;
|
||||
}
|
||||
if (attrName === 'ref') {
|
||||
if (!check) return this.initElement(dlNodeName, value);
|
||||
return null;
|
||||
}
|
||||
if (attrName === 'style') return this.setHTMLStyle(dlNodeName, value, check);
|
||||
if (attrName === 'dataset') return this.setHTMLDataset(dlNodeName, value, check);
|
||||
if (attrName === 'props') return this.setHTMLPropObject(dlNodeName, value, check);
|
||||
if (attrName === 'attrs') return this.setHTMLAttrObject(dlNodeName, value, check);
|
||||
if (attrName === 'forwardProps') return this.forwardProps(dlNodeName);
|
||||
return DLError.throw2();
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* 1. Event listener
|
||||
* - ${dlNodeName}.addEventListener(${key}, ${value})
|
||||
* 2. HTML internal attribute -> DOM property
|
||||
* - ${dlNodeName}.${key} = ${value}
|
||||
* 3. HTML custom attribute
|
||||
* - ${dlNodeName}.setAttribute(${key}, ${value})
|
||||
*/
|
||||
private setStaticHTMLProp(
|
||||
dlNodeName: string,
|
||||
tag: string,
|
||||
attrName: string,
|
||||
value: t.Expression
|
||||
): t.Statement | null {
|
||||
if (HTMLPropGenerator.commonHTMLPropKeys.includes(attrName))
|
||||
return this.addCommonHTMLProp(dlNodeName, attrName, value, false);
|
||||
if (attrName.startsWith('on')) {
|
||||
const eventName = attrName.slice(2).toLowerCase();
|
||||
if (HTMLPropGenerator.DelegatedEvents.has(eventName)) {
|
||||
return this.delegateEvent(dlNodeName, eventName, value, false);
|
||||
}
|
||||
return this.setHTMLEvent(dlNodeName, eventName, value);
|
||||
}
|
||||
if (this.isInternalAttribute(tag, attrName)) {
|
||||
if (attrName === 'class') attrName = 'className';
|
||||
else if (attrName === 'for') attrName = 'htmlFor';
|
||||
return this.setHTMLProp(dlNodeName, attrName, value);
|
||||
}
|
||||
return this.setHTMLAttr(dlNodeName, attrName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* 1. Event listener
|
||||
* - ${setEvent}(${dlNodeName}, ${key}, ${value})
|
||||
* 2. HTML internal attribute -> DOM property
|
||||
* - ${setHTMLProp}(${dlNodeName}, ${key}, ${value})
|
||||
* 3. HTML custom attribute
|
||||
* - ${setHTMLAttr}(${dlNodeName}, ${key}, ${value})
|
||||
*/
|
||||
private setDynamicHTMLProp(
|
||||
dlNodeName: string,
|
||||
tag: string,
|
||||
attrName: string,
|
||||
value: t.Expression,
|
||||
dependenciesNode: t.ArrayExpression,
|
||||
check: boolean
|
||||
): t.Statement | null {
|
||||
if (HTMLPropGenerator.commonHTMLPropKeys.includes(attrName))
|
||||
return this.addCommonHTMLProp(dlNodeName, attrName, value, check);
|
||||
if (attrName.startsWith('on')) {
|
||||
const eventName = attrName.slice(2).toLowerCase();
|
||||
if (HTMLPropGenerator.DelegatedEvents.has(eventName)) {
|
||||
return this.delegateEvent(dlNodeName, eventName, value, check);
|
||||
}
|
||||
return this.setEvent(dlNodeName, eventName, value, check);
|
||||
}
|
||||
if (this.alterAttributeMap[attrName]) {
|
||||
attrName = this.alterAttributeMap[attrName];
|
||||
}
|
||||
if (this.isInternalAttribute(tag, attrName)) {
|
||||
return this.setCachedProp(dlNodeName, attrName, value, dependenciesNode, check);
|
||||
}
|
||||
return this.setCachedAttr(dlNodeName, attrName, value, dependenciesNode, check);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,66 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import BaseGenerator from './BaseGenerator';
|
||||
|
||||
export default class LifecycleGenerator extends BaseGenerator {
|
||||
static lifecycle = ['willMount', 'didMount', 'willUnmount', 'didUnmount'] as const;
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName} && ${value}(${dlNodeName}, changed)
|
||||
*/
|
||||
addOnUpdate(dlNodeName: string, value: t.Expression): t.Statement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.logicalExpression(
|
||||
'&&',
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.callExpression(value, [this.t.identifier(dlNodeName), ...this.updateParams.slice(1)])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* willMount:
|
||||
* - ${value}(${dlNodeName})
|
||||
* didMount/willUnmount/didUnmount:
|
||||
* - View.addDidMount(${dlNodeName}, ${value})
|
||||
*/
|
||||
addLifecycle(
|
||||
dlNodeName: string,
|
||||
key: (typeof LifecycleGenerator.lifecycle)[number],
|
||||
value: t.Expression
|
||||
): t.Statement {
|
||||
if (key === 'willMount') {
|
||||
return this.addWillMount(dlNodeName, value);
|
||||
}
|
||||
return this.addOtherLifecycle(dlNodeName, value, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${value}(${dlNodeName})
|
||||
*/
|
||||
addWillMount(dlNodeName: string, value: t.Expression): t.ExpressionStatement {
|
||||
return this.t.expressionStatement(this.t.callExpression(value, [this.t.identifier(dlNodeName)]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* View.addDidMount(${dlNodeName}, ${value})
|
||||
*/
|
||||
addOtherLifecycle(
|
||||
dlNodeName: string,
|
||||
value: t.Expression,
|
||||
type: 'didMount' | 'willUnmount' | 'didUnmount'
|
||||
): t.ExpressionStatement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.callExpression(
|
||||
this.t.memberExpression(
|
||||
this.t.identifier('View'),
|
||||
this.t.identifier(`add${type[0].toUpperCase()}${type.slice(1)}`)
|
||||
),
|
||||
[this.t.identifier(dlNodeName), value]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import { type DependencyProp, type ViewParticle } from '@inula/reactivity-parser';
|
||||
import LifecycleGenerator from './LifecycleGenerator';
|
||||
|
||||
export default class PropViewGenerator extends LifecycleGenerator {
|
||||
/**
|
||||
* @brief Alter prop view in batch
|
||||
* @param props
|
||||
* @returns altered props
|
||||
*/
|
||||
alterPropViews<T extends Record<string, DependencyProp> | undefined>(props: T): T {
|
||||
if (!props) return props;
|
||||
return Object.fromEntries(
|
||||
Object.entries(props).map(([key, prop]) => {
|
||||
return [key, this.alterPropView(prop)!];
|
||||
})
|
||||
) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName} = new PropView(($addUpdate) => {
|
||||
* addUpdate((changed) => { ${updateStatements} })
|
||||
* ${initStatements}
|
||||
* return ${topLevelNodes}
|
||||
* })
|
||||
*/
|
||||
declarePropView(viewParticles: ViewParticle[]) {
|
||||
// ---- Generate PropView
|
||||
const [initStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(
|
||||
viewParticles,
|
||||
false,
|
||||
true
|
||||
);
|
||||
// ---- Add update function to the first node
|
||||
/**
|
||||
* $addUpdate((changed) => { ${updateStatements} })
|
||||
*/
|
||||
if (Object.keys(updateStatements).length > 0) {
|
||||
initStatements.unshift(
|
||||
this.t.expressionStatement(
|
||||
this.t.callExpression(this.t.identifier('$addUpdate'), [
|
||||
this.t.arrowFunctionExpression(this.updateParams, this.geneUpdateBody(updateStatements)),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
initStatements.unshift(...this.declareNodes(nodeIdx));
|
||||
initStatements.push(this.generateReturnStatement(topLevelNodes));
|
||||
|
||||
// ---- Assign as a dlNode
|
||||
const dlNodeName = this.generateNodeName();
|
||||
const propViewNode = this.t.expressionStatement(
|
||||
this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.newExpression(this.t.identifier(this.importMap.PropView), [
|
||||
this.t.arrowFunctionExpression([this.t.identifier('$addUpdate')], this.t.blockStatement(initStatements)),
|
||||
])
|
||||
)
|
||||
);
|
||||
this.addInitStatement(propViewNode);
|
||||
const propViewIdentifier = this.t.identifier(dlNodeName);
|
||||
|
||||
// ---- Add to update statements
|
||||
/**
|
||||
* ${dlNodeName}?.update(changed)
|
||||
*/
|
||||
this.addUpdateStatementsWithoutDep(
|
||||
this.optionalExpression(
|
||||
dlNodeName,
|
||||
this.t.callExpression(
|
||||
this.t.memberExpression(propViewIdentifier, this.t.identifier('update')),
|
||||
this.updateParams
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return dlNodeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Alter prop view by replacing prop view with a recursively generated prop view
|
||||
* @param prop
|
||||
* @returns altered prop
|
||||
*/
|
||||
alterPropView<T extends DependencyProp | undefined>(prop: T): T {
|
||||
if (!prop) return prop;
|
||||
const { value, viewPropMap } = prop;
|
||||
if (!viewPropMap) return { ...prop, value };
|
||||
let newValue = value;
|
||||
this.traverse(this.valueWrapper(value), {
|
||||
StringLiteral: innerPath => {
|
||||
const id = innerPath.node.value;
|
||||
const viewParticles = viewPropMap[id];
|
||||
if (!viewParticles) return;
|
||||
const propViewIdentifier = this.t.identifier(this.declarePropView(viewParticles));
|
||||
|
||||
if (value === innerPath.node) newValue = propViewIdentifier;
|
||||
innerPath.replaceWith(propViewIdentifier);
|
||||
innerPath.skip();
|
||||
},
|
||||
});
|
||||
return { ...prop, value: newValue };
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the dependency index array from the update statements' keys
|
||||
* i.e. [1, 2, 7] => [0b1, 0b10, 0b111] => [[1], [2], [0, 1, 2]] => [0, 1, 2]
|
||||
* @param updateStatements
|
||||
* @returns dependency index array
|
||||
*/
|
||||
private static reverseDependencyIndexArr(updateStatements: Record<number, t.Statement[]>): number[] {
|
||||
const allDepsNum = Object.keys(updateStatements)
|
||||
.map(Number)
|
||||
.reduce((acc, depNum) => acc | depNum, 0);
|
||||
const allDeps = [];
|
||||
for (let i = 0; i < String(allDepsNum).length; i++) {
|
||||
if (allDepsNum & (1 << i)) allDeps.push(i);
|
||||
}
|
||||
return allDeps;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import { type ViewParticle } from '@inula/reactivity-parser';
|
||||
import ViewGenerator from './ViewGenerator';
|
||||
|
||||
export default class MainViewGenerator extends ViewGenerator {
|
||||
/**
|
||||
* @brief Generate the main view, i.e., View() { ... }
|
||||
* @param viewParticles
|
||||
* @returns [viewBody, classProperties, templateIdx]
|
||||
*/
|
||||
generate(viewParticles: ViewParticle[]): [t.BlockStatement, t.ClassProperty[], number] {
|
||||
const allClassProperties: t.ClassProperty[] = [];
|
||||
const allInitStatements: t.Statement[] = [];
|
||||
const allUpdateStatements: Record<number, t.Statement[]> = {};
|
||||
const topLevelNodes: string[] = [];
|
||||
|
||||
viewParticles.forEach(viewParticle => {
|
||||
const [initStatements, updateStatements, classProperties, nodeName] = this.generateChild(viewParticle);
|
||||
allInitStatements.push(...initStatements);
|
||||
Object.entries(updateStatements).forEach(([depNum, statements]) => {
|
||||
if (!allUpdateStatements[Number(depNum)]) {
|
||||
allUpdateStatements[Number(depNum)] = [];
|
||||
}
|
||||
allUpdateStatements[Number(depNum)].push(...statements);
|
||||
});
|
||||
allClassProperties.push(...classProperties);
|
||||
topLevelNodes.push(nodeName);
|
||||
});
|
||||
|
||||
const viewBody = this.t.blockStatement([
|
||||
...this.declareNodes(),
|
||||
...this.geneUpdate(allUpdateStatements),
|
||||
...allInitStatements,
|
||||
this.geneReturn(topLevelNodes),
|
||||
]);
|
||||
|
||||
return [viewBody, allClassProperties, this.templateIdx];
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* this._$update = ($changed) => {
|
||||
* if ($changed & 1) {
|
||||
* ...
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
private geneUpdate(updateStatements: Record<number, t.Statement[]>): t.Statement[] {
|
||||
if (Object.keys(updateStatements).length === 0) return [];
|
||||
return [
|
||||
this.t.expressionStatement(
|
||||
this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$update'), false),
|
||||
this.t.arrowFunctionExpression(
|
||||
this.updateParams,
|
||||
this.t.blockStatement([
|
||||
...Object.entries(updateStatements)
|
||||
.filter(([depNum]) => depNum !== '0')
|
||||
.map(([depNum, statements]) => {
|
||||
return this.t.ifStatement(
|
||||
this.t.binaryExpression('&', this.t.identifier('$changed'), this.t.numericLiteral(Number(depNum))),
|
||||
this.t.blockStatement(statements)
|
||||
);
|
||||
}),
|
||||
...(updateStatements[0] ?? []),
|
||||
])
|
||||
)
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* return [${nodeNames}]
|
||||
*/
|
||||
private geneReturn(topLevelNodes: string[]) {
|
||||
return this.t.returnStatement(this.t.arrayExpression(topLevelNodes.map(nodeName => this.t.identifier(nodeName))));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import { type DependencyProp, type CompParticle, type ViewParticle } from '@inula/reactivity-parser';
|
||||
import ForwardPropGenerator from '../HelperGenerators/ForwardPropGenerator';
|
||||
|
||||
export default class CompGenerator extends ForwardPropGenerator {
|
||||
run() {
|
||||
let { props } = this.viewParticle as CompParticle;
|
||||
props = this.alterPropViews(props);
|
||||
const { tag, children } = this.viewParticle as CompParticle;
|
||||
|
||||
const dlNodeName = this.generateNodeName();
|
||||
|
||||
this.addInitStatement(...this.declareCompNode(dlNodeName, tag, props, children));
|
||||
const allDependencyIndexArr: number[] = [];
|
||||
|
||||
// ---- Resolve props
|
||||
Object.entries(props).forEach(([key, { value, dependencyIndexArr, dependenciesNode }]) => {
|
||||
if (key === 'forwardProps') return;
|
||||
if (key === 'didUpdate') return;
|
||||
allDependencyIndexArr.push(...dependencyIndexArr);
|
||||
if (CompGenerator.lifecycle.includes(key as (typeof CompGenerator.lifecycle)[number])) {
|
||||
this.addInitStatement(this.addLifecycle(dlNodeName, key as (typeof CompGenerator.lifecycle)[number], value));
|
||||
return;
|
||||
}
|
||||
if (key === 'ref') {
|
||||
this.addInitStatement(this.initElement(dlNodeName, value));
|
||||
return;
|
||||
}
|
||||
if (key === 'elements') {
|
||||
this.addInitStatement(this.initElement(dlNodeName, value, true));
|
||||
return;
|
||||
}
|
||||
if (key === '_$content') {
|
||||
this.addUpdateStatements(dependencyIndexArr, this.setCompContent(dlNodeName, value, dependenciesNode));
|
||||
return;
|
||||
}
|
||||
if (key === 'props') {
|
||||
this.addUpdateStatements(dependencyIndexArr, this.setCompProps(dlNodeName, value, dependenciesNode));
|
||||
return;
|
||||
}
|
||||
|
||||
this.addUpdateStatements(dependencyIndexArr, this.setCompProp(dlNodeName, key, value, dependenciesNode));
|
||||
});
|
||||
|
||||
// ---- Add addUpdate last
|
||||
if (props.didUpdate) {
|
||||
this.addUpdateStatements(allDependencyIndexArr, this.addOnUpdate(dlNodeName, props.didUpdate.value));
|
||||
}
|
||||
|
||||
return dlNodeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* null
|
||||
* or
|
||||
* [[prop1, value1, deps1], [prop2, value2, deps2], ...
|
||||
*/
|
||||
private generateCompProps(props: Record<string, DependencyProp>): t.Expression {
|
||||
if (Object.keys(props).length === 0) return this.t.nullLiteral();
|
||||
return this.t.arrayExpression(
|
||||
Object.entries(props).map(([key, { value, dependenciesNode }]) => {
|
||||
return this.t.arrayExpression([this.t.stringLiteral(key), value, dependenciesNode]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName} = new ${tag}()
|
||||
* ${dlNodeName}._$init(${props}, ${content}, ${children}, ${this})
|
||||
*/
|
||||
private declareCompNode(
|
||||
dlNodeName: string,
|
||||
tag: t.Expression,
|
||||
props: Record<string, DependencyProp>,
|
||||
children: ViewParticle[]
|
||||
): t.Statement[] {
|
||||
const willForwardProps = 'forwardProps' in props;
|
||||
const newProps = Object.fromEntries(
|
||||
Object.entries(props).filter(
|
||||
([key]) =>
|
||||
!['ref', 'elements', 'forwardProps', '_$content', 'didUpdate', 'props', ...CompGenerator.lifecycle].includes(
|
||||
key
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const content = props._$content;
|
||||
|
||||
return [
|
||||
this.t.expressionStatement(
|
||||
this.t.assignmentExpression('=', this.t.identifier(dlNodeName), this.t.newExpression(tag, []))
|
||||
),
|
||||
this.t.expressionStatement(
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$init')), [
|
||||
this.generateCompProps(newProps),
|
||||
content ? this.t.arrayExpression([content.value, content.dependenciesNode]) : this.t.nullLiteral(),
|
||||
children.length > 0 ? this.t.identifier(this.declarePropView(children)) : this.t.nullLiteral(),
|
||||
willForwardProps ? this.t.identifier('this') : this.t.nullLiteral(),
|
||||
])
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}._$setContent(() => ${value}, ${dependenciesNode})
|
||||
*/
|
||||
private setCompContent(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement {
|
||||
return this.optionalExpression(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$setContent')), [
|
||||
this.t.arrowFunctionExpression([], value),
|
||||
dependenciesNode,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}._$setProp(${key}, () => ${value}, ${dependenciesNode})
|
||||
*/
|
||||
private setCompProp(
|
||||
dlNodeName: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
dependenciesNode: t.ArrayExpression
|
||||
): t.Statement {
|
||||
return this.optionalExpression(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$setProp')), [
|
||||
this.t.stringLiteral(key),
|
||||
this.t.arrowFunctionExpression([], value),
|
||||
dependenciesNode,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}._$setProps(() => ${value}, ${dependenciesNode})
|
||||
*/
|
||||
private setCompProps(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement {
|
||||
return this.optionalExpression(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$setProps')), [
|
||||
this.t.arrowFunctionExpression([], value),
|
||||
dependenciesNode,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import { type ViewParticle, type DependencyProp, type EnvParticle } from '@inula/reactivity-parser';
|
||||
import PropViewGenerator from '../HelperGenerators/PropViewGenerator';
|
||||
|
||||
export default class EnvGenerator extends PropViewGenerator {
|
||||
run() {
|
||||
let { props } = this.viewParticle as EnvParticle;
|
||||
props = this.alterPropViews(props)!;
|
||||
const { children } = this.viewParticle as EnvParticle;
|
||||
|
||||
const dlNodeName = this.generateNodeName();
|
||||
|
||||
this.addInitStatement(this.declareEnvNode(dlNodeName, props));
|
||||
|
||||
// ---- Children
|
||||
this.addInitStatement(this.geneEnvChildren(dlNodeName, children));
|
||||
|
||||
// ---- Update props
|
||||
Object.entries(props).forEach(([key, { dependencyIndexArr, value, dependenciesNode }]) => {
|
||||
if (!dependencyIndexArr) return;
|
||||
this.addUpdateStatements(dependencyIndexArr, this.updateEnvNode(dlNodeName, key, value, dependenciesNode));
|
||||
});
|
||||
|
||||
return dlNodeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* { ${key}: ${value}, ... }
|
||||
* { ${key}: ${deps}, ... }
|
||||
*/
|
||||
private generateEnvs(props: Record<string, DependencyProp>): t.Expression[] {
|
||||
return [
|
||||
this.t.objectExpression(
|
||||
Object.entries(props).map(([key, { value }]) => this.t.objectProperty(this.t.identifier(key), value))
|
||||
),
|
||||
this.t.objectExpression(
|
||||
Object.entries(props)
|
||||
.map(
|
||||
([key, { dependenciesNode }]) =>
|
||||
dependenciesNode && this.t.objectProperty(this.t.identifier(key), dependenciesNode)
|
||||
)
|
||||
.filter(Boolean) as t.ObjectProperty[]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName} = new EnvNode(envs)
|
||||
*/
|
||||
private declareEnvNode(dlNodeName: string, props: Record<string, DependencyProp>): t.Statement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.newExpression(this.t.identifier(this.importMap.EnvNode), this.generateEnvs(props))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}.initNodes([${childrenNames}])
|
||||
*/
|
||||
private geneEnvChildren(dlNodeName: string, children: ViewParticle[]): t.Statement {
|
||||
const [statements, childrenNames] = this.generateChildren(children);
|
||||
this.addInitStatement(...statements);
|
||||
return this.t.expressionStatement(
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('initNodes')), [
|
||||
this.t.arrayExpression(childrenNames.map(name => this.t.identifier(name))),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}.updateEnv(${key}, () => ${value}, ${dependenciesNode})
|
||||
*/
|
||||
private updateEnvNode(
|
||||
dlNodeName: string,
|
||||
key: string,
|
||||
value: t.Expression,
|
||||
dependenciesNode: t.ArrayExpression
|
||||
): t.Statement {
|
||||
return this.optionalExpression(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('updateEnv')), [
|
||||
this.t.stringLiteral(key),
|
||||
this.t.arrowFunctionExpression([], value),
|
||||
dependenciesNode,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import { type ExpParticle } from '@inula/reactivity-parser';
|
||||
import ElementGenerator from '../HelperGenerators/ElementGenerator';
|
||||
import { DLError } from '../error';
|
||||
|
||||
export default class ExpGenerator extends ElementGenerator {
|
||||
run() {
|
||||
let { content, props } = this.viewParticle as ExpParticle;
|
||||
content = this.alterPropView(content)!;
|
||||
props = this.alterPropViews(props);
|
||||
|
||||
const dlNodeName = this.generateNodeName();
|
||||
|
||||
this.addInitStatement(this.declareExpNode(dlNodeName, content.value, content.dependenciesNode));
|
||||
|
||||
if (content.dynamic) {
|
||||
this.addUpdateStatements(
|
||||
content.dependencyIndexArr,
|
||||
this.updateExpNode(dlNodeName, content.value, content.dependenciesNode)
|
||||
);
|
||||
}
|
||||
|
||||
if (props) {
|
||||
Object.entries(props).forEach(([key, { value }]) => {
|
||||
if (ExpGenerator.lifecycle.includes(key as (typeof ExpGenerator.lifecycle)[number])) {
|
||||
return this.addInitStatement(
|
||||
this.addLifecycle(dlNodeName, key as (typeof ExpGenerator.lifecycle)[number], value)
|
||||
);
|
||||
}
|
||||
if (key === 'ref') {
|
||||
return this.addInitStatement(this.initElement(dlNodeName, value));
|
||||
}
|
||||
if (key === 'elements') {
|
||||
return this.addInitStatement(this.initElement(dlNodeName, value, true));
|
||||
}
|
||||
if (key === 'didUpdate') {
|
||||
return this.addUpdateStatements(content.dependencyIndexArr, this.addOnUpdate(dlNodeName, value));
|
||||
}
|
||||
DLError.warn1(key);
|
||||
});
|
||||
}
|
||||
|
||||
return dlNodeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName} = new ExpNode(${value}, dependenciesNode)
|
||||
*/
|
||||
private declareExpNode(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement {
|
||||
return this.t.expressionStatement(
|
||||
this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.newExpression(this.t.identifier(this.importMap.ExpNode), [
|
||||
value,
|
||||
dependenciesNode ?? this.t.nullLiteral(),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}.update(() => value, dependenciesNode)
|
||||
*/
|
||||
private updateExpNode(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement {
|
||||
return this.optionalExpression(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')), [
|
||||
this.t.arrowFunctionExpression([], value),
|
||||
dependenciesNode ?? this.t.nullLiteral(),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import BaseGenerator from '../HelperGenerators/BaseGenerator';
|
||||
import { type ForParticle, type ViewParticle } from '@inula/reactivity-parser';
|
||||
|
||||
export default class ForGenerator extends BaseGenerator {
|
||||
run() {
|
||||
const { item, array, key, children } = this.viewParticle as ForParticle;
|
||||
|
||||
const dlNodeName = this.generateNodeName();
|
||||
|
||||
// ---- Declare for node
|
||||
this.addInitStatement(
|
||||
this.declareForNode(
|
||||
dlNodeName,
|
||||
array.value,
|
||||
item,
|
||||
children,
|
||||
BaseGenerator.calcDependencyNum(array.dependencyIndexArr),
|
||||
key
|
||||
)
|
||||
);
|
||||
|
||||
// ---- Update statements
|
||||
this.addUpdateStatements(array.dependencyIndexArr, this.updateForNode(dlNodeName, array.value, item, key));
|
||||
this.addUpdateStatementsWithoutDep(this.updateForNodeItem(dlNodeName));
|
||||
|
||||
return dlNodeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName} = new ForNode(${array}, ${depNum}, ${array}.map(${item} => ${key}),
|
||||
* ((${item}, $updateArr, $idx) => {
|
||||
* $updateArr[$idx] = (changed, $item) => {
|
||||
* ${item} = $item
|
||||
* {$updateStatements}
|
||||
* })
|
||||
* ${children}
|
||||
* return [...${topLevelNodes}]
|
||||
* })
|
||||
*/
|
||||
private declareForNode(
|
||||
dlNodeName: string,
|
||||
array: t.Expression,
|
||||
item: t.LVal,
|
||||
children: ViewParticle[],
|
||||
depNum: number,
|
||||
key: t.Expression
|
||||
): t.Statement {
|
||||
// ---- NodeFunc
|
||||
const [childStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(children, false, true);
|
||||
|
||||
// ---- Update func
|
||||
childStatements.unshift(
|
||||
...this.declareNodes(nodeIdx),
|
||||
this.t.expressionStatement(
|
||||
this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.memberExpression(this.t.identifier('$updateArr'), this.t.identifier('$idx'), true),
|
||||
this.t.arrowFunctionExpression(
|
||||
[...this.updateParams, this.t.identifier('$item')],
|
||||
this.t.blockStatement([
|
||||
this.t.expressionStatement(this.t.assignmentExpression('=', item, this.t.identifier('$item'))),
|
||||
...this.geneUpdateBody(updateStatements).body,
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// ---- Return statement
|
||||
childStatements.push(this.generateReturnStatement(topLevelNodes));
|
||||
|
||||
return this.t.expressionStatement(
|
||||
this.t.assignmentExpression(
|
||||
'=',
|
||||
this.t.identifier(dlNodeName),
|
||||
this.t.newExpression(this.t.identifier(this.importMap.ForNode), [
|
||||
array,
|
||||
this.t.numericLiteral(depNum),
|
||||
this.getForKeyStatement(array, item, key),
|
||||
this.t.arrowFunctionExpression(
|
||||
[item as any, this.t.identifier('$updateArr'), this.t.identifier('$idx')],
|
||||
this.t.blockStatement(childStatements)
|
||||
),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${array}.map(${item} => ${key})
|
||||
*/
|
||||
private getForKeyStatement(array: t.Expression, item: t.LVal, key: t.Expression): t.Expression {
|
||||
return this.t.isNullLiteral(key)
|
||||
? key
|
||||
: this.t.callExpression(this.t.memberExpression(array, this.t.identifier('map')), [
|
||||
this.t.arrowFunctionExpression([item as any], key),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}.updateArray(${array}, ${array}.map(${item} => ${key}))
|
||||
*/
|
||||
private updateForNode(dlNodeName: string, array: t.Expression, item: t.LVal, key: t.Expression): t.Statement {
|
||||
return this.optionalExpression(
|
||||
dlNodeName,
|
||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('updateArray')), [
|
||||
array,
|
||||
...this.updateParams.slice(1),
|
||||
this.getForKeyStatement(array, item, key),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @View
|
||||
* ${dlNodeName}?.update(changed)
|
||||
*/
|
||||
private updateForNodeItem(dlNodeName: string): t.Statement {
|
||||
return this.optionalExpression(
|
||||
dlNodeName,
|
||||
this.t.callExpression(
|
||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')),
|
||||
this.updateParams
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue