Compare commits

..

24 Commits

Author SHA1 Message Date
openinula 68ad2d3e9c Merge pull request '新增inula max 内容' (#8) from xiaohuoni/inula:master into master 2024-10-21 14:06:44 +08:00
xiaohuoni 70f7bb3f51 Merge pull request '增加inula max 框架' (#1) from feat-inula-max into master 2024-10-21 12:48:32 +08:00
xiaohuoni 540c8d582b feat: add inula-max 2024-10-17 19:25:06 +08:00
openinula 002dc545c2 Merge pull request '解耦reconciler' (#5) from kbdsg/inula:master into master 2024-10-16 09:47:16 +08:00
openinula 9f1d2fbc56 Merge pull request 'openInula 测试工具' (#6) from Maxwell_YCM/inula:master into master 2024-10-16 09:45:52 +08:00
Maxwell_YCM 9f5c9bb370 add test lib 2024-10-15 22:09:28 +08:00
超级无敌数码暴龙战士 af32cb79f9 feat: 解耦reconciler 2024-10-15 21:56:37 +08:00
openinula fbc9c11946 Merge pull request 'inula-testing-library' (#4) from Aojunha/inula:plugins into master 2024-10-15 20:59:04 +08:00
小豪 2047bb27db feat: inula-testing-library 2024-10-15 13:42:50 +08:00
openinula 847fbd5bc0 Merge pull request 'OpenInula AI 代码生成工具' (#2) from LirongRen/inula:master into master
XI
2024-10-15 09:52:15 +08:00
openinula d488477ca3 Merge pull request 'OpenInula 类型系统开发' (#3) from Shanyujia/inula:master into master 2024-10-15 09:50:40 +08:00
renlirong c901b953c5 feat: inula code generator 2024-10-14 19:23:14 +08:00
Shanyujia 22772af364 Delete .DS_Store 2024-10-14 18:50:07 +08:00
eleliauk f295549122 feat:类型系统开发 2024-10-14 18:16:29 +08:00
涂旭辉 2c2c3926e7
!167 fix(inulax): 状态管理器问题修复
Merge pull request !167 from xuan/sync
2024-04-07 02:14:31 +00:00
huangxuan ecaaacb812
fix(inulax): 修复嵌套使用connect的错误 2024-04-07 09:34:36 +08:00
huangxuan 6688cde7ab
fix(inulax): 修复动态组件死循环 2024-04-02 14:57:50 +08:00
涂旭辉 8a7623d281
!166 inula 代码同步
Merge pull request !166 from xuan/sync
2024-04-02 03:21:55 +00:00
huangxuan 0375ed95fc
fix(core): 修复antd Tree组件报错 2024-04-02 11:08:16 +08:00
huangxuan 4a825cec88
fix(inulax): 修复状态管理器触发类组件重新渲染,shouldComponentUpdate不生效的问题 2024-04-02 11:02:48 +08:00
huangxuan aa4984f997
feat(router): 修复HashRouter push与当前页面相同的URL时页面不刷新的问题 2024-04-02 10:53:25 +08:00
huangxuan 78f4bce57c
fix(router): inula-router路由匹配规则兼容react-router;HashHistory hash格式不合法时重定向至合法URL 2024-04-02 10:52:22 +08:00
huangxuan ebfe1eceb9
feat(core): 限制dangerouslySetInnerHTML API生效的条件,减少XSS攻击面 2024-04-02 10:47:31 +08:00
huangxuan ec34490202
feat(core): 导出version,兼容mobx 2024-04-02 09:51:58 +08:00
500 changed files with 14081 additions and 21084 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,8 +0,0 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

View File

@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": ["create-inula", "openinula", "inula-cli", "inula-dev-tools", "inula-intl", "inula-request", "inula-router", "inula-vite-app", "inula-webpack-app"]
}

6
.gitignore vendored
View File

@ -1,11 +1,11 @@
node_modules /node_modules
.idea .idea
.vscode .vscode
package-lock.json package-lock.json
pnpm-lock.yaml pnpm-lock.yaml
/packages/**/node_modules
/packages/inula-cli/lib /packages/inula-cli/lib
build build
/packages/inula-router/connectRouter /packages/inula-router/connectRouter
/packages/inula-router/router /packages/inula-router/router
dist .inula-max
.history

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Inula-next</title>
</head>
<body>
<div id="main"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -1,25 +0,0 @@
{
"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",
"@openinula/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"
]
}

View File

@ -1,148 +0,0 @@
import { render, View } from '@openinula/next';
let idCounter = 1;
const adjectives = [
'pretty',
'large',
'big',
'small',
'tall',
'short',
'long',
'handsome',
'plain',
'quaint',
'clean',
'elegant',
'easy',
'angry',
'crazy',
'helpful',
'mushy',
'odd',
'unsightly',
'adorable',
'important',
'inexpensive',
'cheap',
'expensive',
'fancy',
];
const colours = ['red', 'yellow', 'blue', 'green', 'pink', 'brown', 'purple', 'brown', 'white', 'black', 'orange'];
const nouns = [
'table',
'chair',
'house',
'bbq',
'desk',
'car',
'pony',
'cookie',
'sandwich',
'burger',
'pizza',
'mouse',
'keyboard',
];
function _random(max) {
return Math.round(Math.random() * 1000) % max;
}
function buildData(count) {
const data = new Array(count);
for (let i = 0; i < count; i++) {
data[i] = {
id: idCounter++,
label: `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`,
};
}
return data;
}
function Button({ id, text, fn }) {
return (
<div class="col-sm-6 smallpad">
<button id={id} class="btn btn-primary btn-block" type="button" onClick={fn}>
{text}
</button>
</div>
);
}
function App() {
let data = [];
let selected = null;
function run() {
data = buildData(1000);
}
function runLots() {
data = buildData(10000);
}
function add() {
data.push(...buildData(1000));
}
function update() {
for (let i = 0; i < data.length; i += 10) {
data[i].label += ' !!!';
}
}
function swapRows() {
if (data.length > 998) {
[data[1], data[998]] = [data[998], data[1]];
}
}
function clear() {
data = [];
}
function remove(id) {
data = data.filter(d => d.id !== id);
}
function select(id) {
selected = id;
}
return (
<div class="container">
<div class="jumbotron">
<div class="row">
<div class="col-md-6">
<h1>Inula-next Keyed</h1>
</div>
<div class="col-md-6">
<div class="row">
<Button id="run" text="Create 1,000 rows" fn={run} />
<Button id="runlots" text="Create 10,000 rows" fn={runLots} />
<Button id="add" text="Append 1,000 rows" fn={add} />
<Button id="update" text="Update every 10th row" fn={update} />
<Button id="clear" text="Clear" fn={clear} />
<Button id="swaprows" text="Swap Rows" fn={swapRows} />
</div>
</div>
</div>
</div>
<table class="table table-hover table-striped test-data">
<tbody>
<for array={data} item={{ id, label }} key={id}>
<tr class={selected === id ? 'danger' : ''}>
<td class="col-md-1" textContent={id} />
<td class="col-md-4">
<a onClick={select.bind(this, id)} textContent={label} />
</td>
<td class="col-md-1">
<a onClick={remove.bind(this, id)}>
<span class="glyphicon glyphicon-remove" aria-hidden="true" />
</a>
</td>
<td class="col-md-6" />
</tr>
</for>
</tbody>
</table>
<span class="preloadicon glyphicon glyphicon-remove" aria-hidden="true" />
</div>
);
}
render(App, 'main');

View File

@ -1,13 +0,0 @@
import { defineConfig } from 'vite';
import inula from 'vite-plugin-inula-next';
export default defineConfig({
server: {
port: 4320,
},
base: '',
optimizeDeps: {
disabled: true,
},
plugins: [inula({ files: '**/*.{tsx,jsx}' })],
});

View File

@ -1,8 +0,0 @@
# dev
## 0.0.1
### Patch Changes
- Updated dependencies [2f9d373]
- babel-preset-inula-next@0.0.2

View File

@ -1,12 +0,0 @@
<!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.tsx"></script>
</body>
</html>

View File

@ -1,25 +0,0 @@
{
"name": "dev",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@babel/standalone": "^7.22.4",
"@openinula/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"
]
}

View File

@ -1,3 +0,0 @@
.ok {
color: var(--color-ok);
}

View File

@ -1,17 +0,0 @@
import { View, render } from '@openinula/next';
function MyComp() {
let count = 0;
const 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>
</>
);
}
render(MyComp, 'main');

View File

@ -1,20 +0,0 @@
{
"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
}
}

View File

@ -1,13 +0,0 @@
import { defineConfig } from 'vite';
import inula from 'vite-plugin-inula-next';
export default defineConfig({
server: {
port: 4320,
},
base: '',
optimizeDeps: {
disabled: true,
},
plugins: [inula({ files: '**/*.{ts,js,tsx,jsx}' })],
});

View File

@ -1,117 +0,0 @@
# Todo-list
- [ ] function 2 class.
- [x] assignment 2 property
- [x] statement 2 watch func
- [ ] handle `props` @HQ
- [x] object destructuring
- [x] default value
- [ ] partial object destructuring
- [ ] nested object destructuring
- [ ] nested array destructuring
- [x] alias
- [x] add `this` @HQ
- [x] for (jsx-parser) -> playground + benchmark @YH
- [x] lifecycle @HQ
- [x] ref @HQ (to validate)
- [x] env @HQ (to validate)
- [ ] Sub component
- [ ] Early Return
- [ ] custom hook -> Model @YH
- [ ] JSX
- [x] style
- [x] fragment
- [x] ref (to validate)
- [ ] snippet
- [x] for
# function component syntax
- [ ] props (destructuring | partial destructuring | default value | alias)
- [ ] variable declaration -> class component property
- [ ] function declaration ( arrow function | async function )-> class method
- [ ] Statement -> watch function
- [ ] assignment
- [ ] function call
- [ ] class method call
- [ ] for loop
- [ ] while loop (do while, while, for, for in, for of)
- [ ] if statement
- [ ] switch statement
- [ ] try catch statement
- [ ] throw statement ? not support
- [ ] delete expression
- [ ] lifecycle -> LabeledStatement
- [ ] return statement -> render method(Body)
- [ ] iife
- [ ] early return
# Issues
- [ ] partial props destructuring -> support this.$props @YH
```jsx
function Input({onClick, xxx, ...props}) {
function handleClick() {
onClick()
}
return <input onClick={handleClick} {...props} />
}
```
- [ ] model class declaration should before class component declaration -> use Class polyfill
```jsx
// Code like this will cause error: `FetchModel` is not defined
@Main
@View
class MyComp {
fetchModel = use(FetchModel, { url: "https://api.example.com/data" })
Body() {}
}
@Model
class FetchModel {}
```
- [ ] custom hook early return @YH
- [ ] snippet
```jsx
const H1 = <h1></h1>;
// {H1}
const H1 = (name) => <h1 className={name}></h1>;
// {H1()} <H1/>
function H1() {
return <h1></h1>;
}
// <H1/>
```
- [ ] Render text and variable, Got Error
```jsx
// Uncaught DOMException: Failed to execute 'appendChild' on 'Node': This node type does not support this method.
<button>Add, Now is {count}</button>
```
# Proposal
## Watch
自动将Statement包裹Watch的反例
```jsx
// 前置操作: 场景为Table组件需要响应column变化先置空column再计算新的columnByKey
let columnByKey;
watch: {
columnByKey = {};
columns.forEach(col => {
columnByKey[col.key] = col;
});
}
// 临时变量: 场景为操作前的计算部分临时变量
watch: {
let col = columnByKey[sortBy];
if (
col !== undefined &&
col.sortable === true &&
typeof col.value === "function"
) {
sortFunction = r => col.value(r);
}
}
```

View File

@ -9,11 +9,13 @@
"prettier": "prettier .prettierrc.js -w packages/**/*.{ts,tsx,js,jsx}", "prettier": "prettier .prettierrc.js -w packages/**/*.{ts,tsx,js,jsx}",
"build:inula": "pnpm -F openinula build", "build:inula": "pnpm -F openinula build",
"test:inula": "pnpm -F openinula test", "test:inula": "pnpm -F openinula test",
"test:inula-intl": "pnpm -F inula-intl test",
"test:inula-request": "pnpm -F inula-request test",
"test:inula-router": "pnpm -F inula-router test",
"build:inula-cli": "pnpm -F inula-cli build", "build:inula-cli": "pnpm -F inula-cli build",
"build:inula-intl": "pnpm -F inula-intl build", "build:inula-intl": "pnpm -F inula-intl build",
"build:inula-request": "pnpm -F inula-request build", "build:inula-request": "pnpm -F inula-request build",
"build:inula-router": "pnpm -F inula-router build", "build:inula-router": "pnpm -F inula-router build",
"build:transpiler": "pnpm --filter './packages/transpiler/*' run build",
"commitlint": "commitlint --config commitlint.config.js -e", "commitlint": "commitlint --config commitlint.config.js -e",
"postinstall": "husky install" "postinstall": "husky install"
}, },
@ -23,46 +25,48 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.16.7", "@babel/core": "7.23.7",
"@babel/plugin-proposal-class-properties": "7.16.7", "@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7", "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-object-rest-spread": "7.16.7", "@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-proposal-optional-chaining": "7.16.7", "@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-proposal-private-methods": "7.16.7", "@babel/plugin-proposal-private-methods": "7.18.6",
"@babel/plugin-proposal-private-property-in-object": "7.16.7", "@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@babel/plugin-syntax-jsx": "7.16.7", "@babel/plugin-syntax-jsx": "7.23.3",
"@babel/plugin-transform-arrow-functions": "7.16.7", "@babel/plugin-transform-arrow-functions": "7.23.3",
"@babel/plugin-transform-block-scoped-functions": "7.16.7", "@babel/plugin-transform-block-scoped-functions": "7.23.3",
"@babel/plugin-transform-block-scoping": "7.16.7", "@babel/plugin-transform-block-scoping": "7.23.4",
"@babel/plugin-transform-classes": "7.16.7", "@babel/plugin-transform-classes": "7.23.8",
"@babel/plugin-transform-computed-properties": "7.16.7", "@babel/plugin-transform-computed-properties": "7.23.3",
"@babel/plugin-transform-destructuring": "7.16.7", "@babel/plugin-transform-destructuring": "7.23.3",
"@babel/plugin-transform-for-of": "7.16.7", "@babel/plugin-transform-for-of": "7.23.6",
"@babel/plugin-transform-literals": "7.16.7", "@babel/plugin-transform-literals": "7.23.3",
"@babel/plugin-transform-object-assign": "7.16.7", "@babel/plugin-transform-object-assign": "7.23.3",
"@babel/plugin-transform-object-super": "7.16.7", "@babel/plugin-transform-object-super": "7.23.3",
"@babel/plugin-transform-parameters": "7.16.7", "@babel/plugin-transform-parameters": "7.23.3",
"@babel/plugin-transform-react-jsx": "7.16.7", "@babel/plugin-transform-react-jsx": "7.23.4",
"@babel/plugin-transform-react-jsx-source": "^7.16.7", "@babel/plugin-transform-react-jsx-source": "^7.23.3",
"@babel/plugin-transform-runtime": "7.16.7", "@babel/plugin-transform-runtime": "7.23.7",
"@babel/plugin-transform-shorthand-properties": "7.16.7", "@babel/plugin-transform-shorthand-properties": "7.23.3",
"@babel/plugin-transform-spread": "7.16.7", "@babel/plugin-transform-spread": "7.23.3",
"@babel/plugin-transform-template-literals": "7.16.7", "@babel/plugin-transform-template-literals": "7.23.3",
"@babel/preset-env": "7.16.7", "@babel/preset-env": "7.23.8",
"@babel/preset-typescript": "7.16.7", "@babel/preset-typescript": "7.23.3",
"@babel/runtime": "7.16.7", "@babel/runtime": "7.23.8",
"@commitlint/cli": "^18.4.4", "@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^18.4.4", "@commitlint/config-conventional": "^17.8.1",
"@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^13.3.0", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^4.0.0", "@rollup/plugin-replace": "^4.0.0",
"@types/jest": "^26.0.24", "@types/jest": "^29.5.11",
"@types/node": "^17.0.18", "@types/node": "^17.0.18",
"@typescript-eslint/eslint-plugin": "4.8.0", "@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "4.8.0", "@typescript-eslint/parser": "6.18.1",
"babel-jest": "^27.5.1", "@babel/parser": "^7.24.7",
"magic-string": "^0.30.10",
"babel-jest": "^29.7.0",
"ejs": "^3.1.8", "ejs": "^3.1.8",
"eslint": "7.13.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^6.9.0", "eslint-config-prettier": "^6.9.0",
"eslint-plugin-jest": "^22.15.0", "eslint-plugin-jest": "^22.15.0",
"eslint-plugin-no-function-declare-after-return": "^1.0.0", "eslint-plugin-no-function-declare-after-return": "^1.0.0",
@ -73,17 +77,17 @@
"lint-staged": "^15.2.0", "lint-staged": "^15.2.0",
"openinula": "workspace:*", "openinula": "workspace:*",
"prettier": "^3.1.1", "prettier": "^3.1.1",
"rollup": "^2.75.5", "rollup": "^2.79.1",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-execute": "^1.1.1", "rollup-plugin-execute": "^1.1.1",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"ts-jest": "^29.1.1",
"typescript": "^4.9.5" "typescript": "^4.9.5"
}, },
"engines": { "engines": {
"node": ">=10.x", "node": ">=10.x",
"npm": ">=7.x" "npm": ">=7.x"
},
"dependencies": {
"@changesets/cli": "^2.27.1",
"changeset": "^0.2.6"
} }
} }

View File

@ -32,8 +32,8 @@ const generatorType = fs
}); });
const runGenerator = async (templatePath, { name = '', cwd = process.cwd(), args = {} }) => { const runGenerator = async (templatePath, { name = '', cwd = process.cwd(), args = {} }) => {
return new Promise(resolve => {
let currentPath; let currentPath;
return new Promise(resolve => {
if (name) { if (name) {
mkdirp.sync(name); mkdirp.sync(name);
currentPath = path.join(cwd, name); currentPath = path.join(cwd, name);

View File

@ -54,7 +54,7 @@ inula-cli的推荐目录结构如下
│ └── inula-cli │ └── inula-cli
│ ├── lib │ ├── lib
├── mock // mock目录 ├── mock // mock目录
│ └── transform.ts │ └── mock.ts
├── src // 项目源码目录 ├── src // 项目源码目录
│ ├── pages │ ├── pages
│ │ ├── index.less │ │ ├── index.less
@ -178,10 +178,10 @@ inula-cli的所有功能都围绕插件展开插件可以很方便地让用
inula-cli支持用户集成已发布在npm仓库的插件用户可以按需安装并运行这些插件。 inula-cli支持用户集成已发布在npm仓库的插件用户可以按需安装并运行这些插件。
安装可以通过npm安装这里以插件@openinula/add为例 安装可以通过npm安装这里以插件@inula/add为例
```shell ```shell
npm i --save-dev @openinula/add npm i --save-dev @inula/add
``` ```
如果需要运行插件,需要在配置文件中配置对应的插件路径 如果需要运行插件,需要在配置文件中配置对应的插件路径
@ -191,7 +191,7 @@ npm i --save-dev @openinula/add
export default { export default {
... ...
plugins:["@openinula/add"] plugins:["@inula/add"]
} }
``` ```

View File

@ -1,51 +0,0 @@
/*
* Copyright (c) 2023 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 webpack from 'webpack';
import { build } from 'vite';
export default (api: any) => {
api.registerCommand({
name: 'build',
description: 'build application for production',
initialState: api.buildConfig,
fn: async function (args: any, state: any) {
switch (api.compileMode) {
case 'webpack':
if (state) {
api.applyHook({ name: 'beforeCompile', args: state });
state.forEach((s: any) => {
webpack(s.config, (err: any, stats: any) => {
if (err || stats.hasErrors()) {
api.logger.error(`Build failed.err: ${err}, stats:${stats}`);
}
});
});
} else {
api.logger.error(`Build failed. Can't find build config.`);
}
break;
case 'vite':
if (state) {
api.applyHook({ name: 'beforeCompile' });
build(state);
} else {
api.logger.error(`Build failed. Can't find build config.`);
}
break;
}
},
});
};

View File

@ -57,7 +57,7 @@ export default (api: API) => {
api.applyHook({ name: 'afterStartDevServer' }); api.applyHook({ name: 'afterStartDevServer' });
}); });
} else { } else {
api.logger.error("Can't find config"); api.logger.error('Can\'t find config');
} }
break; break;
case 'vite': case 'vite':
@ -70,7 +70,7 @@ export default (api: API) => {
server.printUrls(); server.printUrls();
}); });
} else { } else {
api.logger.error("Can't find config"); api.logger.error('Can\'t find config');
} }
break; break;
default: default:

View File

@ -33,7 +33,7 @@ export default (api: API) => {
args._.shift(); args._.shift();
} }
if (args._.length === 0) { if (args._.length === 0) {
api.logger.warn("Can't find any generate options."); api.logger.warn('Can\'t find any generate options.');
return; return;
} }

View File

@ -0,0 +1,42 @@
{
"presets": [
["@babel/preset-env", {
"targets": {
"node": "current"
}
}],
"@babel/preset-typescript",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-syntax-jsx",
[
"@babel/plugin-transform-react-jsx",
{
"runtime": "automatic",
"importSource": "openinula"
}
],
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-proposal-private-methods", { "loose": true }],
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
"@babel/plugin-transform-object-assign",
"@babel/plugin-transform-object-super",
["@babel/plugin-proposal-object-rest-spread", { "loose": true, "useBuiltIns": true }],
["@babel/plugin-transform-template-literals", { "loose": true }],
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-literals",
"@babel/plugin-transform-for-of",
"@babel/plugin-transform-block-scoped-functions",
"@babel/plugin-transform-classes",
"@babel/plugin-transform-shorthand-properties",
"@babel/plugin-transform-computed-properties",
"@babel/plugin-transform-parameters",
["@babel/plugin-transform-spread", { "loose": true, "useBuiltIns": true }],
["@babel/plugin-transform-block-scoping", { "throwIfClosureRequired": false }],
["@babel/plugin-transform-destructuring", { "loose": true, "useBuiltIns": true }],
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-optional-chaining"
]
}

View File

@ -1,29 +0,0 @@
/*
* Copyright (c) 2023 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.
*/
const { preset } = require('./jest.config');
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-typescript'],
[
'@babel/preset-react',
{
runtime: 'automatic',
importSource: 'openinula',
},
],
],
};

View File

@ -7,12 +7,12 @@ function deleteFolder(filePath) {
if (fs.lstatSync(filePath).isDirectory()) { if (fs.lstatSync(filePath).isDirectory()) {
const files = fs.readdirSync(filePath); const files = fs.readdirSync(filePath);
files.forEach(file => { files.forEach(file => {
const nectFilePath = path.join(filePath, file); const nextFilePath = path.join(filePath, file);
const states = fs.lstatSync(nectFilePath); const states = fs.lstatSync(nextFilePath);
if (states.isDirectory()) { if (states.isDirectory()) {
deleteFolder(nectFilePath); deleteFolder(nextFilePath);
} else { } else {
fs.unlinkSync(nectFilePath); fs.unlinkSync(nextFilePath);
} }
}); });
fs.rmdirSync(filePath); fs.rmdirSync(filePath);
@ -31,12 +31,12 @@ export function cleanUp(folders) {
return { return {
name: 'clean-up', name: 'clean-up',
buildEnd() { buildEnd() {
folders.forEach(folder => deleteFolder(folder)); folders.forEach(f => deleteFolder(f));
}, },
}; };
} }
function builderTypeConfig() { function buildTypeConfig() {
return { return {
input: './build/@types/index.d.ts', input: './build/@types/index.d.ts',
output: { output: {
@ -47,4 +47,4 @@ function builderTypeConfig() {
}; };
} }
export default [builderTypeConfig()]; export default [buildTypeConfig()];

View File

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula, { useState } from 'openinula'; import { useState } from 'openinula';
import { IntlProvider } from '../index'; import { IntlProvider } from '../index';
import zh from './locale/zh'; import zh from './locale/zh';
import en from './locale/en'; import en from './locale/en';
@ -32,23 +32,29 @@ const App = () => {
const message = locale === 'zh' ? zh : en; const message = locale === 'zh' ? zh : en;
return ( return (
<>
<IntlProvider locale={locale} messages={locale === 'zh' ? zh : en}> <IntlProvider locale={locale} messages={locale === 'zh' ? zh : en}>
<header>Inula-Intl API Test Demo</header> <header>Inula-Intl API Test Demo</header>
<div className="container"> <div className="container">
<Example1 /> <Example1 />
<Example2 /> <Example2 />
<Example3 locale={locale} setLocale={setLocale} /> <Example3 locale={locale} setLocale={setLocale} />
</div> </div>
<div className="container"> <div className="container">
<Example4 locale={locale} messages={message} /> {/*<Example4 locale={locale} messages={message} />*/}
<Example5 /> <Example5 />
<Example6 locale={{ locale }} messages={message} />
</div> </div>
<div className="button"> <div className="button">
<button onClick={handleChange}></button> <button onClick={handleChange}></button>
</div> </div>
</IntlProvider> </IntlProvider>
<div className="container">
<Example4 locale={locale} messages={message} />
</div>
<div className="container">
<Example6 locale={{ locale }} messages={message} />
</div>
</>
); );
}; };

View File

@ -13,16 +13,16 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula';
import { useIntl } from '../../index'; import { useIntl } from '../../index';
const Example1 = () => { const Example1 = () => {
const { i18n } = useIntl(); const i18n = useIntl();
return ( return (
<div className="card"> <div className="card">
<h2>useIntl方式测试Demo</h2> <h2>useIntl方式测试Demo</h2>
<pre>{i18n.formatMessage({ id: 'text1' })}</pre> <pre>{i18n.formatMessage({ id: 'text1' })}</pre>
<pre>{i18n.$t({ id: 'text1' })}</pre>
</div> </div>
); );
}; };

View File

@ -12,7 +12,6 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula';
import { FormattedMessage } from '../../index'; import { FormattedMessage } from '../../index';
const Example2 = () => { const Example2 = () => {
@ -22,6 +21,9 @@ const Example2 = () => {
<pre> <pre>
<FormattedMessage id="text2" /> <FormattedMessage id="text2" />
</pre> </pre>
<pre>
<FormattedMessage id="text5" values={{ testComponent1: <b>123</b>, testComponent2: <b>456</b> }} />
</pre>
</div> </div>
); );
}; };

View File

@ -13,7 +13,6 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula';
import { FormattedMessage } from '../../index'; import { FormattedMessage } from '../../index';
const Example3 = props => { const Example3 = props => {

View File

@ -13,7 +13,6 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula';
import { createIntl } from '../../index'; import { createIntl } from '../../index';
const Example4 = props => { const Example4 = props => {

View File

@ -13,23 +13,16 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula, { Component } from 'openinula';
import { injectIntl } from '../../index'; import { injectIntl } from '../../index';
class Example5 extends Component<any, any, any> { const Example5 = ({ intl }) => {
public constructor(props: any, context) { // 使用intl.formatMessage来获取国际化消息
super(props, context); console.log(intl + '------------intl-------------');
}
render() {
const { intl } = this.props as any;
return ( return (
<div className="card"> <div className="card">
<h2>injectIntl方式测试Demo</h2> <h2>injectIntl方式测试Demo</h2>
<pre>{intl.formatMessage({ id: 'text4' })}</pre> <pre>{intl.formatMessage({ id: 'text4' })}</pre>
</div> </div>
); );
} };
}
export default injectIntl(Example5); export default injectIntl(Example5);

View File

@ -13,7 +13,6 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula';
import { createIntl, createIntlCache, RawIntlProvider } from '../../index'; import { createIntl, createIntlCache, RawIntlProvider } from '../../index';
import Example6Child from './Example6Child'; import Example6Child from './Example6Child';
@ -21,7 +20,7 @@ const Example6 = (props: any) => {
const { locale, messages } = props; const { locale, messages } = props;
const cache = createIntlCache(); const cache = createIntlCache();
let i18n = createIntl({ locale: locale, messages: messages }, cache); const i18n = createIntl({ locale: locale, messages: messages }, cache);
return ( return (
<RawIntlProvider value={i18n}> <RawIntlProvider value={i18n}>

View File

@ -15,7 +15,7 @@
import { useIntl } from '../../index'; import { useIntl } from '../../index';
const Example6Child = (props: any) => { const Example6Child = () => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (

View File

@ -12,7 +12,7 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import * as Inula from 'openinula'; import Inula from 'openinula';
import App from './App'; import App from './App';
function render() { function render() {

View File

@ -19,4 +19,5 @@ export default {
text2: 'Welcome to the Inula-Intl component!', text2: 'Welcome to the Inula-Intl component!',
text3: 'Welcome to the Inula-Intl component!', text3: 'Welcome to the Inula-Intl component!',
text4: 'Welcome to the Inula-Intl component!', text4: 'Welcome to the Inula-Intl component!',
text5: 'Render a component {testComponent1} {testComponent2}!',
}; };

View File

@ -18,4 +18,5 @@ export default {
text2: '欢迎使用国际化组件!', text2: '欢迎使用国际化组件!',
text3: '欢迎使用国际化组件!', text3: '欢迎使用国际化组件!',
text4: '欢迎使用国际化组件!', text4: '欢迎使用国际化组件!',
text5: '渲染一个组件 {testComponent1} {testComponent2}!',
}; };

View File

@ -22,7 +22,7 @@ import I18nProvider from './src/core/components/I18nProvider';
import injectIntl, { I18nContext, InjectProvider } from './src/core/components/InjectI18n'; import injectIntl, { I18nContext, InjectProvider } from './src/core/components/InjectI18n';
import useI18n from './src/core/hook/useI18n'; import useI18n from './src/core/hook/useI18n';
import createI18n from './src/core/createI18n'; import createI18n from './src/core/createI18n';
import { InjectedIntl, MessageDescriptor } from './src/types/interfaces'; import { MessageDescriptor } from './src/types/interfaces';
// 函数API // 函数API
export { export {
I18n, I18n,
@ -36,7 +36,7 @@ export {
// 组件 // 组件
export { export {
FormattedMessage, FormattedMessage,
I18nContext, I18nContext as IntlContext,
I18nProvider as IntlProvider, I18nProvider as IntlProvider,
injectIntl as injectIntl, injectIntl as injectIntl,
InjectProvider as RawIntlProvider, InjectProvider as RawIntlProvider,
@ -64,7 +64,3 @@ export function defineMessages<K extends keyof any, T = MessageDescriptor, U = R
export function defineMessage<T>(msg: T): T { export function defineMessage<T>(msg: T): T {
return msg; return msg;
} }
export interface InjectedIntlProps {
intl: InjectedIntl;
}

View File

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
module.exports = { export default {
coverageDirectory: 'coverage', coverageDirectory: 'coverage',
resetModules: true, resetModules: true,
preset: 'ts-jest/presets/js-with-ts', preset: 'ts-jest/presets/js-with-ts',
@ -30,8 +30,10 @@ module.exports = {
globals: { globals: {
'ts-jest': { 'ts-jest': {
tsconfig: 'tsconfig.json', tsconfig: 'tsconfig.json',
diagnostics: false,
}, },
}, },
testPathIgnorePatterns: ['\\\\node_modules\\\\'],
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
}; };

View File

@ -3,13 +3,13 @@
"version": "0.0.5", "version": "0.0.5",
"description": "", "description": "",
"main": "build/intl.umd.js", "main": "build/intl.umd.js",
"type": "commonjs", "type": "module",
"types": "build/@types/index.d.ts", "types": "build/@types/index.d.ts",
"scripts": { "scripts": {
"demo-serve": "webpack serve --mode=development", "demo-serve": "webpack serve --mode=development",
"build": "rollup --config rollup.config.js && npm run build-types ", "build": "rollup --config rollup.config.js && npm run build-types ",
"build-types": "tsc -p tsconfig.json && rollup -c build-type.js", "build-types": "tsc -p tsconfig.json && rollup -c build-type.js",
"test": "jest --config jest.config.js", "test": "jest --no-cache --config jest.config.js",
"test-c": "jest --coverage" "test-c": "jest --coverage"
}, },
"repository": { "repository": {
@ -17,8 +17,7 @@
"url": "" "url": ""
}, },
"files": [ "files": [
"build", "/build"
"README.md"
], ],
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -27,35 +26,23 @@
"openinula": ">=0.1.1" "openinula": ">=0.1.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.21.3",
"@babel/preset-env": "^7.16.7",
"@babel/preset-react": "^7.9.4", "@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "7.16.7",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-typescript": "^11.0.0", "@rollup/plugin-typescript": "^11.0.0",
"rollup-plugin-dts": "^6.1.0",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@types/node": "^16.18.27",
"@types/react": "18.0.25", "@types/react": "18.0.25",
"babel": "^6.23.0",
"babel-jest": "^29.5.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"html-webpack-plugin": "^5.5.1", "html-webpack-plugin": "^5.5.1",
"jest": "29.3.1",
"jest-environment-jsdom": "^29.5.0", "jest-environment-jsdom": "^29.5.0",
"jsdom": "^21.1.1", "jsdom": "^21.1.1",
"prettier": "^2.8.7", "react": "18.2.0",
"rollup": "^2.0.0", "react-dom": "18.2.0",
"rollup-plugin-livereload": "^2.0.5", "rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-serve": "^1.1.0", "rollup-plugin-serve": "^1.1.0",
"rollup-plugin-terser": "^5.3.0", "rollup-plugin-visualizer": "^5.10.0",
"tslib": "^2.6.1",
"ts-jest": "29.0.3",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"typescript": "4.9.3", "tslib": "^2.6.1",
"webpack": "^5.81.0", "webpack": "^5.72.1",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.13.3" "webpack-dev-server": "^4.13.3"
} }

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,7 @@ import babel from '@rollup/plugin-babel';
import nodeResolve from '@rollup/plugin-node-resolve'; import nodeResolve from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript'; import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser'; import { terser } from 'rollup-plugin-terser';
import { visualizer } from 'rollup-plugin-visualizer';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -29,19 +30,37 @@ const output = path.join(__dirname, '/build');
const extensions = ['.js', '.ts', '.tsx']; const extensions = ['.js', '.ts', '.tsx'];
export default { const BuildConfig = mode => {
input: entry, const prod = mode.startsWith('prod');
output: [ const outputList = [
{ {
file: path.resolve(output, 'intl.umd.js'), file: path.join(output, `cjs/intl.${prod ? 'min.' : ''}js`),
sourcemap: 'true',
format: 'cjs',
globals: {
openinula: 'Inula',
},
},
{
file: path.join(output, `umd/intl.${prod ? 'min.' : ''}js`),
name: 'InulaI18n', name: 'InulaI18n',
sourcemap: 'true',
format: 'umd', format: 'umd',
globals: {
openinula: 'Inula',
}, },
{ },
file: path.resolve(output, 'intl.esm-browser.js'), ];
if (!prod) {
outputList.push({
file: path.join(output, 'esm/intl.js'),
sourcemap: 'true',
format: 'esm', format: 'esm',
}, });
], }
return {
input: entry,
output: outputList,
plugins: [ plugins: [
nodeResolve({ nodeResolve({
extensions, extensions,
@ -49,8 +68,9 @@ export default {
}), }),
babel({ babel({
exclude: 'node_modules/**', exclude: 'node_modules/**',
configFile: path.join(__dirname, '/babel.config.js'), configFile: path.join(__dirname, '/.babelrc'),
extensions, extensions,
babelHelpers: 'runtime',
}), }),
typescript({ typescript({
tsconfig: 'tsconfig.json', tsconfig: 'tsconfig.json',
@ -60,3 +80,5 @@ export default {
], ],
external: ['openinula', 'react', 'react-dom'], external: ['openinula', 'react', 'react-dom'],
}; };
};
export default [BuildConfig('dev'), BuildConfig('prod')];

View File

@ -18,8 +18,13 @@
* \\x[a-fA-F0-9]{2} \x0A * \\x[a-fA-F0-9]{2} \x0A
* [nrtf'"] 匹配常见的转义字符:\n换行符、\r回车符、\t制表符、\f换页符、\' \" * [nrtf'"] 匹配常见的转义字符:\n换行符、\r回车符、\t制表符、\f换页符、\' \"
*/ */
export const UNICODE_REG = /\\(?:u\{[a-fA-F0-9]+}|x[a-fA-F0-9]{2}|[nrtf'"])/g; export const UNICODE_REG: RegExp = /\\(?:u\{[a-fA-F0-9]+}|x[a-fA-F0-9]{2}|[nrtf'"])/g;
export const STICKY_FLAG: string = 'ym';
export const GLOBAL_FLAG: string = 'gm';
export const VERTICAL_LINE: string = '|';
export const UNICODE_FLAG: string = 'u';
export const STATE_GROUP_START_INDEX: number = 1;
// Inula 需要被保留静态常量 // Inula 需要被保留静态常量
export const INULA_STATICS = { export const INULA_STATICS = {
childContextTypes: true, childContextTypes: true,
@ -76,3 +81,22 @@ export const INULA_MEMO_STATICS = {
// 默认复数规则 // 默认复数规则
export const DEFAULT_PLURAL_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other']; export const DEFAULT_PLURAL_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other'];
export const voidElementTags = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
'menuitem',
];

View File

@ -18,30 +18,48 @@ import DateTimeFormatter from '../format/fomatters/DateTimeFormatter';
import NumberFormatter from '../format/fomatters/NumberFormatter'; import NumberFormatter from '../format/fomatters/NumberFormatter';
import { getFormatMessage } from '../format/getFormatMessage'; import { getFormatMessage } from '../format/getFormatMessage';
import { I18nCache, I18nProps, MessageDescriptor, MessageOptions } from '../types/interfaces'; import { I18nCache, I18nProps, MessageDescriptor, MessageOptions } from '../types/interfaces';
import { Locale, Locales, Messages, AllLocaleConfig, AllMessages, LocaleConfig, Error, Events } from '../types/types'; import {
Locale,
Locales,
Messages,
AllLocaleConfig,
AllMessages,
LocaleConfig,
Error,
Events,
InulaNode,
} from '../types/types';
import creatI18nCache from '../format/cache/cache'; import creatI18nCache from '../format/cache/cache';
import { isValidElement } from 'openinula';
export class I18n extends EventDispatcher<Events> { export class I18n extends EventDispatcher<Events> {
public locale: Locale; public locale: Locale;
public locales: Locales; public locales: Locales;
public defaultLocale?: Locale;
public timeZone?: string;
private allMessages: AllMessages;
private readonly _localeConfig: AllLocaleConfig; private readonly _localeConfig: AllLocaleConfig;
private readonly allMessages: AllMessages; public readonly onError?: Error;
public readonly error?: Error;
public readonly cache?: I18nCache; public readonly cache?: I18nCache;
constructor(props: I18nProps) { constructor(props: I18nProps) {
super(); super();
this.locale = 'en'; this.defaultLocale = 'en';
this.locale = this.defaultLocale;
this.locales = this.locale || ''; this.locales = this.locale || '';
this.allMessages = {}; this.allMessages = {};
this._localeConfig = {}; this._localeConfig = {};
this.error = props.error; this.onError = props.onError;
this.timeZone = '';
this.loadMessage(props.messages); this.loadMessage(props.messages);
if (props.localeConfig) { if (props.localeConfig) {
this.loadLocaleConfig(props.localeConfig); this.loadLocaleConfig(props.localeConfig);
} }
if (props.messages) {
this.changeMessage(props.messages);
}
if (props.locale || props.locales) { if (props.locale || props.locales) {
this.changeLanguage(props.locale!, props.locales); this.changeLanguage(props.locale!, props.locales);
@ -93,6 +111,11 @@ export class I18n extends EventDispatcher<Events> {
} }
} }
changeMessage(messages: AllMessages) {
this.allMessages = messages;
this.emit('change');
}
// 加载messages // 加载messages
loadMessage(localeOrMessages: Locale | AllMessages | undefined, messages?: Messages) { loadMessage(localeOrMessages: Locale | AllMessages | undefined, messages?: Messages) {
if (messages) { if (messages) {
@ -118,9 +141,21 @@ export class I18n extends EventDispatcher<Events> {
formatMessage( formatMessage(
id: MessageDescriptor | string, id: MessageDescriptor | string,
values: Record<string, unknown> | undefined = {}, values: Record<string, unknown> | undefined = {},
{ message, context, formatOptions }: MessageOptions = {} { messages, context, formatOptions }: MessageOptions = {}
) { ) {
return getFormatMessage(this, id, values, { message, context, formatOptions }); // 在多次渲染时保证存储component不丢失
const components: { [key: string]: InulaNode } = {};
const tempValues: Record<string, unknown> = { ...values };
if (tempValues) {
Object.keys(tempValues).forEach((key, index) => {
const value = tempValues[key];
if (!isValidElement(value)) return;
// 将inula元素暂存
components[index] = value;
tempValues[key] = `<${index}/>`;
});
}
return getFormatMessage(this, id, tempValues, { messages, context, formatOptions }, components!);
} }
formatDate(value: string | Date, formatOptions?: Intl.DateTimeFormatOptions): string { formatDate(value: string | Date, formatOptions?: Intl.DateTimeFormatOptions): string {

View File

@ -12,7 +12,7 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula, { Children, Fragment } from 'openinula'; import { Children, Fragment } from 'openinula';
import { FormattedMessageProps } from '../../types/interfaces'; import { FormattedMessageProps } from '../../types/interfaces';
import useI18n from '../hook/useI18n'; import useI18n from '../hook/useI18n';
@ -22,28 +22,17 @@ import useI18n from '../hook/useI18n';
* @constructor * @constructor
*/ */
function FormattedMessage(props: FormattedMessageProps) { function FormattedMessage(props: FormattedMessageProps) {
const { i18n } = useI18n(); const { formatMessage } = useI18n();
const { const { id, values, messages, formatOptions, context, tagName: TagName = Fragment, children, comment }: any = props;
id,
values,
messages,
formatOptions,
context,
tagName: TagName = Fragment,
children,
comment,
useMemorize,
}: any = props;
const formatMessageOptions = { const formatMessageOptions = {
comment, comment,
messages, messages,
context, context,
useMemorize,
formatOptions, formatOptions,
}; };
let formattedMessage = i18n.formatMessage(id, values, formatMessageOptions); const formattedMessage = formatMessage(id, values, formatMessageOptions);
if (typeof children === 'function') { if (typeof children === 'function') {
const childNodes = Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage]; const childNodes = Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage];

View File

@ -12,10 +12,10 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula, { useRef, useState, useEffect, useMemo } from 'openinula'; import { useRef, useState, useEffect, useMemo } from 'openinula';
import { InjectProvider } from './InjectI18n'; import { InjectProvider } from './InjectI18n';
import I18n, { createI18nInstance } from '../I18n'; import I18n, { createI18nInstance } from '../I18n';
import { I18nProviderProps } from '../../types/types'; import { AllMessages, I18nProviderProps, Messages } from '../../types/types';
/** /**
* *
@ -23,9 +23,11 @@ import { I18nProviderProps } from '../../types/types';
* @constructor * @constructor
*/ */
const I18nProvider = (props: I18nProviderProps) => { const I18nProvider = (props: I18nProviderProps) => {
const { locale, messages, children } = props; const { locale, messages, children, i18n } = props;
const i18n = useMemo(() => { const i18nInstance =
i18n ||
useMemo(() => {
return createI18nInstance({ return createI18nInstance({
locale: locale, locale: locale,
messages: messages, messages: messages,
@ -33,18 +35,19 @@ const I18nProvider = (props: I18nProviderProps) => {
}, [locale, messages]); }, [locale, messages]);
// 使用useRef保存上次的locale值 // 使用useRef保存上次的locale值
const localeRef = useRef<string | undefined>(i18n.locale); const localeRef = useRef<string | undefined>(i18nInstance.locale);
const localeMessage = useRef<string | Messages | AllMessages>(i18nInstance.messages);
const [context, setContext] = useState<I18n>(i18n); const [context, setContext] = useState<I18n>(i18nInstance);
useEffect(() => { useEffect(() => {
const handleChange = () => { const handleChange = () => {
if (localeRef.current !== i18n.locale) { if (localeRef.current !== i18nInstance.locale || localeMessage.current !== i18nInstance.messages) {
localeRef.current = i18n.locale; localeRef.current = i18nInstance.locale;
setContext(i18n); localeMessage.current = i18nInstance.messages;
setContext(i18nInstance);
} }
}; };
let removeListener = i18n.on('change', handleChange); const removeListener = i18nInstance.on('change', handleChange);
// 手动触发一次 handleChange以确保 context 的正确性 // 手动触发一次 handleChange以确保 context 的正确性
handleChange(); handleChange();
@ -53,7 +56,7 @@ const I18nProvider = (props: I18nProviderProps) => {
return () => { return () => {
removeListener(); removeListener();
}; };
}, [i18n]); }, [i18nInstance]);
// 提供一个Provider组件 // 提供一个Provider组件
return <InjectProvider value={context}>{children}</InjectProvider>; return <InjectProvider value={context}>{children}</InjectProvider>;

View File

@ -31,13 +31,16 @@ export const InjectProvider = Provider;
function injectI18n(Component, options?: InjectOptions): any { function injectI18n(Component, options?: InjectOptions): any {
const { const {
isUsingForwardRef = false, // 默认不使用 isUsingForwardRef = false, // 默认不使用
ensureContext = false,
} = options || {}; } = options || {};
// 定义一个名为 WrappedI18n 的函数组件,接收传入组件的 props 和 forwardedRef返回传入组件并注入 i18n // 定义一个名为 WrappedI18n 的函数组件,接收传入组件的 props 和 forwardedRef返回传入组件并注入 i18n
const WrappedI18n = props => ( const WrappedI18n = props => (
<Consumer> <Consumer>
{context => { {context => {
if (ensureContext) {
isVariantI18n(context); isVariantI18n(context);
}
const i18nProps = { const i18nProps = {
intl: context, intl: context,

View File

@ -13,20 +13,29 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import { configProps, I18nCache } from '../types/interfaces'; import { configProps, I18nCache } from '../types/interfaces';
import I18n, { createI18nInstance } from './I18n'; import { createI18nInstance } from './I18n';
import creatI18nCache from '../format/cache/cache'; import creatI18nCache from '../format/cache/cache';
import { IntlType } from '../types/types';
/** /**
* createI18n hook函数i8n实例 * createI18n hook函数i8n实例
*/ */
export const createI18n = (config: configProps, cache?: I18nCache): I18n => { export const createI18n = (config: configProps, cache?: I18nCache): IntlType => {
const { locale, defaultLocale, messages } = config; const { locale, defaultLocale, messages } = config;
return createI18nInstance({ const i18n = createI18nInstance({
locale: locale || defaultLocale || 'zh', locale: locale || defaultLocale || 'en',
messages: messages, messages: messages,
cache: cache ?? creatI18nCache(), cache: cache ?? creatI18nCache(),
}); });
return {
i18n,
...config,
formatMessage: i18n.formatMessage.bind(i18n),
formatNumber: i18n.formatNumber.bind(i18n),
formatDate: i18n.formatDate.bind(i18n),
$t: i18n.formatMessage.bind(i18n),
};
}; };
export default createI18n; export default createI18n;

View File

@ -12,7 +12,7 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula, { useContext } from 'openinula'; import { useContext, useMemo } from 'openinula';
import utils from '../../utils/utils'; import utils from '../../utils/utils';
import { I18nContext } from '../components/InjectI18n'; import { I18nContext } from '../components/InjectI18n';
import I18n from '../I18n'; import I18n from '../I18n';
@ -23,15 +23,22 @@ import { IntlType } from '../../types/types';
* 使 useI18n 便 * 使 useI18n 便
*/ */
function useI18n(): IntlType { function useI18n(): IntlType {
const i18nContext = useContext<I18n>(I18nContext); const i18n = useContext<I18n>(I18nContext);
utils.isVariantI18n(i18nContext); utils.isVariantI18n(i18n);
const i18n = i18nContext; return useMemo(() => {
return { return {
i18n: i18n, i18n: i18n,
locale: i18n.locale,
messages: i18n.messages,
defaultLocale: i18n.defaultLocale,
timeZone: i18n.timeZone,
onError: i18n.onError,
formatMessage: i18n.formatMessage.bind(i18n), formatMessage: i18n.formatMessage.bind(i18n),
formatNumber: i18n.formatNumber.bind(i18n), formatNumber: i18n.formatNumber.bind(i18n),
formatDate: i18n.formatDate.bind(i18n), formatDate: i18n.formatDate.bind(i18n),
$t: i18n.formatMessage.bind(i18n),
}; };
}, [i18n]);
} }
export default useI18n; export default useI18n;

View File

@ -16,7 +16,7 @@
import { CompiledMessage, Locale, LocaleConfig, Locales } from '../types/types'; import { CompiledMessage, Locale, LocaleConfig, Locales } from '../types/types';
import generateFormatters from './generateFormatters'; import generateFormatters from './generateFormatters';
import { FormatOptions, I18nCache } from '../types/interfaces'; import { FormatOptions, I18nCache } from '../types/interfaces';
import { createIntlCache } from '../../index'; import creatI18nCache from './cache/cache';
/** /**
* *
@ -28,12 +28,18 @@ class Translation {
private readonly localeConfig: Record<string, any>; private readonly localeConfig: Record<string, any>;
private readonly cache: I18nCache; private readonly cache: I18nCache;
constructor(compiledMessage, locale, locales, localeConfig, cache?) { constructor(
compiledMessage: CompiledMessage,
locale: Locale,
locales: Locales,
localeConfig: LocaleConfig,
cache?: I18nCache
) {
this.compiledMessage = compiledMessage; this.compiledMessage = compiledMessage;
this.locale = locale; this.locale = locale;
this.locales = locales; this.locales = locales;
this.localeConfig = localeConfig; this.localeConfig = localeConfig;
this.cache = cache ?? createIntlCache; this.cache = cache ?? creatI18nCache();
} }
/** /**
@ -53,7 +59,7 @@ class Translation {
const value = values[name]; const value = values[name];
const formatter = formatters[type](value, format); const formatter = formatters[type](value, format);
let message; let message: any;
if (typeof formatter === 'function') { if (typeof formatter === 'function') {
message = formatter(textFormatter); // 递归调用 message = formatter(textFormatter); // 递归调用
} else { } else {
@ -68,8 +74,7 @@ class Translation {
const textFormatter = createTextFormatter(this.locale, this.locales, values, formatOptions, this.localeConfig); const textFormatter = createTextFormatter(this.locale, this.locales, values, formatOptions, this.localeConfig);
// 通过递归方法formatCore进行格式化处理 // 通过递归方法formatCore进行格式化处理
const result = this.formatMessage(this.compiledMessage, textFormatter); return this.formatMessage(this.compiledMessage, textFormatter); // 返回要格式化的结果
return result; // 返回要格式化的结果
} }
formatMessage(compiledMessage: CompiledMessage, textFormatter: (...args: any[]) => any) { formatMessage(compiledMessage: CompiledMessage, textFormatter: (...args: any[]) => any) {

View File

@ -17,7 +17,7 @@ import utils from '../../utils/utils';
import NumberFormatter from './NumberFormatter'; import NumberFormatter from './NumberFormatter';
import { Locale, Locales } from '../../types/types'; import { Locale, Locales } from '../../types/types';
import { I18nCache } from '../../types/interfaces'; import { I18nCache } from '../../types/interfaces';
import { createIntlCache } from '../../../index'; import creatI18nCache from '../cache/cache';
/** /**
* *
@ -29,12 +29,12 @@ class PluralFormatter {
private readonly message: any; private readonly message: any;
private readonly cache: I18nCache; private readonly cache: I18nCache;
constructor(locale, locales, value, message, cache?) { constructor(locale: Locale, locales: Locales, value: any, message: any, cache?:I18nCache) {
this.locale = locale; this.locale = locale;
this.locales = locales; this.locales = locales;
this.value = value; this.value = value;
this.message = message; this.message = message;
this.cache = cache ?? createIntlCache(); this.cache = cache ?? creatI18nCache();
} }
// 将 message中的“#”替换为指定数字value并返回新的字符串或者字符串数组 // 将 message中的“#”替换为指定数字value并返回新的字符串或者字符串数组

View File

@ -14,7 +14,7 @@
*/ */
import utils from '../../utils/utils'; import utils from '../../utils/utils';
import { Locale } from '../../types/types'; import {Locale, SelectPool} from '../../types/types';
import { I18nCache } from '../../types/interfaces'; import { I18nCache } from '../../types/interfaces';
/** /**
@ -26,12 +26,12 @@ class SelectFormatter {
private readonly locale: Locale; private readonly locale: Locale;
private readonly cache: I18nCache; private readonly cache: I18nCache;
constructor(locale, cache) { constructor(locale: Locale, cache: I18nCache) {
this.locale = locale; this.locale = locale;
this.cache = cache; this.cache = cache;
} }
getRule(value, rules) { getRule(value: SelectPool, rules: any) {
if (this.cache.select) { if (this.cache.select) {
// 创建key用于唯一标识 // 创建key用于唯一标识
const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locale, rules); const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locale, rules);

View File

@ -19,25 +19,23 @@ import { DatePool, Locale, Locales, SelectPool } from '../types/types';
import PluralFormatter from './fomatters/PluralFormatter'; import PluralFormatter from './fomatters/PluralFormatter';
import SelectFormatter from './fomatters/SelectFormatter'; import SelectFormatter from './fomatters/SelectFormatter';
import { FormatOptions, I18nCache, IntlMessageFormat } from '../types/interfaces'; import { FormatOptions, I18nCache, IntlMessageFormat } from '../types/interfaces';
import cache from './cache/cache';
/** /**
* *
*/ */
const generateFormatters = ( const generateFormatters = (
locale: Locale | Locales, locale: Locale,
locales: Locales, locales: Locales,
localeConfig: Record<string, any> = { plurals: undefined }, localeConfig: Record<string, any> = { plurals: undefined },
formatOptions: FormatOptions = {}, // 自定义格式对象 formatOptions: FormatOptions = {}, // 自定义格式对象
cache: I18nCache cache: I18nCache
): IntlMessageFormat => { ): IntlMessageFormat => {
locale = locales || locale;
const { plurals } = localeConfig; const { plurals } = localeConfig;
/** /**
* *
* @param formatOption * @param formatOption
*/ */
const getStyleOption = formatOption => { const getStyleOption = (formatOption: string | number) => {
if (typeof formatOption === 'string') { if (typeof formatOption === 'string') {
return formatOptions[formatOption] || { option: formatOption }; return formatOptions[formatOption] || { option: formatOption };
} else { } else {
@ -58,14 +56,14 @@ const generateFormatters = (
return pluralFormatter.replaceSymbol.bind(pluralFormatter); return pluralFormatter.replaceSymbol.bind(pluralFormatter);
}, },
selectordinal: (value: number, { offset = 0, ...rules }, useMemorize?) => { selectordinal: (value: number, { offset = 0, ...rules }) => {
const message = rules[value] || rules[(plurals as any)?.(value - offset, true)] || rules.other; const message = rules[value] || rules[(plurals as any)?.(value - offset, true)] || rules.other;
const pluralFormatter = new PluralFormatter(locale, locales, value - offset, message, useMemorize); const pluralFormatter = new PluralFormatter(locale, locales, value - offset, message, cache);
return pluralFormatter.replaceSymbol.bind(pluralFormatter); return pluralFormatter.replaceSymbol.bind(pluralFormatter);
}, },
// 选择规则,如果规则对象中包含与该值相对应的属性,则返回该属性的值;否则,返回 "other" 属性的值。 // 选择规则,如果规则对象中包含与该值相对应的属性,则返回该属性的值;否则,返回 "other" 属性的值。
select: (value: SelectPool, formatRules) => { select: (value: SelectPool, formatRules: any) => {
const selectFormatter = new SelectFormatter(locale, cache); const selectFormatter = new SelectFormatter(locale, cache);
return selectFormatter.getRule(value, formatRules); return selectFormatter.getRule(value, formatRules);
}, },
@ -75,17 +73,16 @@ const generateFormatters = (
return new NumberFormatter(locales, getStyleOption(formatOption), cache).numberFormat(value); return new NumberFormatter(locales, getStyleOption(formatOption), cache).numberFormat(value);
}, },
// 用于将日期格式化为字符串,接受一个日期对象和一个格式化规则。它会根据规则返回格式化后的字符串。
/** /**
*
* eg: { year: 'numeric', month: 'long', day: 'numeric' } DateTimeFormatter如何将日期对象转换为字符串的参数 * eg: { year: 'numeric', month: 'long', day: 'numeric' } DateTimeFormatter如何将日期对象转换为字符串的参数
* \year: 'numeric' 2023 * \year: 'numeric' 2023
* month: 'long' January * month: 'long' January
* day: 'numeric' 1 * day: 'numeric' 1
* @param value * @param value
* @param formatOption { year: 'numeric', month: 'long', day: 'numeric' } * @param formatOption { year: 'numeric', month: 'long', day: 'numeric' }
* @param useMemorize
*/ */
dateTimeFormat: (value: DatePool, formatOption) => { dateTimeFormat: (value: DatePool, formatOption: any) => {
return new DateTimeFormatter(locales, getStyleOption(formatOption), cache).dateTimeFormat(value, formatOption); return new DateTimeFormatter(locales, getStyleOption(formatOption), cache).dateTimeFormat(value, formatOption);
}, },

View File

@ -19,19 +19,21 @@ import I18n from '../core/I18n';
import { MessageDescriptor, MessageOptions } from '../types/interfaces'; import { MessageDescriptor, MessageOptions } from '../types/interfaces';
import { CompiledMessage } from '../types/types'; import { CompiledMessage } from '../types/types';
import creatI18nCache from './cache/cache'; import creatI18nCache from './cache/cache';
import { formatElements } from '../utils/formatElements';
export function getFormatMessage( export function getFormatMessage(
i18n: I18n, i18n: I18n,
id: MessageDescriptor | string, id: MessageDescriptor | string,
values: Record<string, unknown> | undefined = {}, values: Record<string, unknown> | undefined = {},
options: MessageOptions = {} options: MessageOptions = {},
components: any
) { ) {
let { message, context } = options; let { messages, context } = options;
const { formatOptions } = options; const { formatOptions } = options;
const cache = i18n.cache ?? creatI18nCache(); const cache = i18n.cache ?? creatI18nCache();
if (typeof id !== 'string') { if (typeof id !== 'string') {
values = values || id.defaultValues; values = values || id.defaultValues;
message = id.message || id.defaultMessage; messages = id.messages || id.defaultMessage;
context = id.context; context = id.context;
id = id.id; id = id.id;
} }
@ -42,7 +44,7 @@ export function getFormatMessage(
const messageUnavailable = isMissingContextMessage || isMissingMessage; const messageUnavailable = isMissingContextMessage || isMissingMessage;
// 对错误消息进行处理 // 对错误消息进行处理
const messageError = i18n.error; const messageError = i18n.onError;
if (messageError && messageUnavailable) { if (messageError && messageUnavailable) {
if (typeof messageError === 'function') { if (typeof messageError === 'function') {
return messageError(i18n.locale, id, context); return messageError(i18n.locale, id, context);
@ -53,14 +55,17 @@ export function getFormatMessage(
let compliedMessage: CompiledMessage; let compliedMessage: CompiledMessage;
if (context) { if (context) {
compliedMessage = i18n.messages[context][id] || message || id; compliedMessage = i18n.messages[context][id] || messages || id;
} else { } else {
compliedMessage = i18n.messages[id] || message || id; compliedMessage = i18n.messages[id] || messages || id;
} }
// 对解析的messages进行parse解析并输出解析后的Token // 对解析的message进行parse解析并输出解析后的Token
compliedMessage = typeof compliedMessage === 'string' ? utils.compile(compliedMessage) : compliedMessage; compliedMessage = typeof compliedMessage === 'string' ? utils.compile(compliedMessage) : compliedMessage;
const translation = new Translation(compliedMessage, i18n.locale, i18n.locales, i18n.localeConfig, cache); const translation = new Translation(compliedMessage, i18n.locale, i18n.locales, i18n.localeConfig, cache);
return translation.translate(values, formatOptions); const formatResult = translation.translate(values, formatOptions);
// 如果存在inula元素则返回包含格式化的Inula元素的数组
return formatElements(formatResult, components);
} }

View File

@ -16,9 +16,12 @@
import ruleUtils from '../utils/parseRuleUtils'; import ruleUtils from '../utils/parseRuleUtils';
import { LexerInterface } from '../types/interfaces'; import { LexerInterface } from '../types/interfaces';
/**
* message进行处理成Token
*/
class Lexer<T> implements LexerInterface<T> { class Lexer<T> implements LexerInterface<T> {
readonly startState: string; readonly startState: string;
readonly states: Record<string, any>; readonly unionReg: Record<string, any>;
private buffer = ''; private buffer = '';
private stack: string[] = []; private stack: string[] = [];
private index = 0; private index = 0;
@ -28,19 +31,23 @@ class Lexer<T> implements LexerInterface<T> {
private state = ''; private state = '';
private groups: string[] = []; private groups: string[] = [];
private error: Record<string, any> | undefined; private error: Record<string, any> | undefined;
private regexp; private regexp: any;
private fast: Record<string, unknown> = {}; private fast: Record<string, unknown> = {};
private queuedGroup: string | null = ''; private queuedGroup: string | null = '';
private value = ''; private value = '';
constructor(unionReg: Record<string, any>, startState: string) { constructor(unionReg: Record<string, any>, startState: string) {
this.startState = startState; this.startState = startState;
this.states = unionReg; this.unionReg = unionReg;
this.buffer = ''; this.buffer = '';
this.stack = []; this.stack = [];
this.reset(); this.reset();
} }
/**
*
* @param data
*/
public reset(data?: string) { public reset(data?: string) {
this.buffer = data || ''; this.buffer = data || '';
this.index = 0; this.index = 0;
@ -57,7 +64,7 @@ class Lexer<T> implements LexerInterface<T> {
return; return;
} }
this.state = state; this.state = state;
const info = this.states[state]; const info = this.unionReg[state];
this.groups = info.groups; this.groups = info.groups;
this.error = info.error; this.error = info.error;
this.regexp = info.regexp; this.regexp = info.regexp;
@ -73,7 +80,7 @@ class Lexer<T> implements LexerInterface<T> {
this.setState(state); this.setState(state);
} }
private getGroup(match: Record<string, any>) { private getGroup(match: Record<string, object>) {
const groupCount = this.groups.length; const groupCount = this.groups.length;
for (let i = 0; i < groupCount; i++) { for (let i = 0; i < groupCount; i++) {
if (match[i + 1] !== undefined) { if (match[i + 1] !== undefined) {
@ -87,7 +94,9 @@ class Lexer<T> implements LexerInterface<T> {
return this.value; return this.value;
} }
// 迭代获取下一个 token /**
* token
*/
public next() { public next() {
const index = this.index; const index = this.index;
@ -112,7 +121,6 @@ class Lexer<T> implements LexerInterface<T> {
const regexp = this.regexp; const regexp = this.regexp;
regexp.lastIndex = index; regexp.lastIndex = index;
const match = getMatch(regexp, buffer); const match = getMatch(regexp, buffer);
const error = this.error; const error = this.error;
if (match == null) { if (match == null) {
return this.getToken(error, buffer.slice(index, buffer.length), index); return this.getToken(error, buffer.slice(index, buffer.length), index);
@ -131,9 +139,9 @@ class Lexer<T> implements LexerInterface<T> {
} }
/** /**
* Token * Token
* @param group * @param group
* @param text * @param text
* @param offset * @param offset
* @private * @private
*/ */
@ -187,7 +195,7 @@ class Lexer<T> implements LexerInterface<T> {
return token; return token;
} }
// 增加迭代器 // 增加迭代器,允许逐个访问集合中的元素方法
[Symbol.iterator]() { [Symbol.iterator]() {
return { return {
next: (): IteratorResult<T> => { next: (): IteratorResult<T> => {
@ -198,9 +206,15 @@ class Lexer<T> implements LexerInterface<T> {
} }
} }
/**
* message的值
* 0
* 123
*/
const getMatch = ruleUtils.checkSticky() const getMatch = ruleUtils.checkSticky()
? // 正则表达式具有 sticky 标志 ? // 正则表达式具有 sticky 标志
(regexp, buffer) => regexp.exec(buffer) (regexp: any, buffer: string) => regexp.exec(buffer)
: // 正则表达式具有 global 标志,匹配的字符串长度为 0则表示匹配失败 : // 正则表达式具有 global 标志,匹配的字符串长度为 0则表示匹配失败
(regexp, buffer) => (regexp.exec(buffer)[0].length === 0 ? null : regexp.exec(buffer)); (regexp: any, buffer: string) => (regexp.exec(buffer)[0].length === 0 ? null : regexp.exec(buffer));
export default Lexer; export default Lexer;

View File

@ -17,37 +17,44 @@ const body: Record<string, any> = {
doubleapos: { match: "''", value: () => "'" }, doubleapos: { match: "''", value: () => "'" },
quoted: { quoted: {
lineBreaks: true, lineBreaks: true,
match: /'[{}#](?:[^]*?[^'])?'(?!')/u, match: /'[{}#](?:[^]*?[^'])?'(?!')/u, // {}# 'Hello' {name}{}#
value: src => src.slice(1, -1).replace(/''/g, "'"), value: (src: string) => src.slice(1, -1).replace(/''/g, "'"),
}, },
argument: { argument: {
lineBreaks: true, lineBreaks: true,
// 用于匹配{name、{Hello{World匹配{ }花括号中有任何Unicode字符如空格、制表符等
match: /\{\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u, match: /\{\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u,
push: 'arg', push: 'arg',
value: src => src.substring(1).trim(), value: (src: string) => src.substring(1).trim(),
}, },
octothorpe: '#', octothorpe: '#',
end: { match: '}', pop: 1 }, end: { match: '}', pop: 1 },
content: { lineBreaks: true, match: /[^][^{}#']*/u }, content: {
lineBreaks: true,
match: /[^][^{}#]*/u, // []{}#
},
}; };
const arg: Record<string, any> = { const arg: Record<string, any> = {
select: { select: {
lineBreaks: true, lineBreaks: true,
match: /,\s*(?:plural|select|selectordinal)\s*,\s*/u, match: /,\s*(?:plural|select|selectordinal)\s*,\s*/u, // pluralselect selectordinal
next: 'select', next: 'select', // 继续解析下一个参数
value: src => src.split(',')[1].trim(), value: (src: string) => src.split(',')[1].trim(), // 提取第二个参数,并处理收尾空格
}, },
'func-args': { 'func-args': {
// 匹配是否包含其他非特殊字符的参数,匹配结果包含特殊字符如param1, param2, param3
lineBreaks: true, lineBreaks: true,
match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*,/u, match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*,/u,
next: 'body', next: 'body',
value: src => src.split(',')[1].trim(), value: (src: string) => src.split(',')[1].trim(), // 参数字符串去除逗号并去除首尾空格
}, },
'func-simple': { 'func-simple': {
// 匹配是否包含其他简单参数匹配结果不包含标点符号param1 param2 param3
lineBreaks: true, lineBreaks: true,
match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u, match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u,
value: src => src.substring(1).trim(), value: (src: string) => src.substring(1).trim(),
}, },
end: { match: '}', pop: 1 }, end: { match: '}', pop: 1 },
}; };
@ -55,14 +62,17 @@ const arg: Record<string, any> = {
const select: Record<string, any> = { const select: Record<string, any> = {
offset: { offset: {
lineBreaks: true, lineBreaks: true,
match: /\s*offset\s*:\s*\d+\s*/u, match: /\s*offset\s*:\s*\d+\s*/u, // messageoffest
value: src => src.split(':')[1].trim(), value: (src: string) => src.split(':')[1].trim(),
}, },
case: { case: {
// 检查匹配该行是否包含分支信息。
lineBreaks: true, lineBreaks: true,
// 设置规则匹配以左大括号 { 结尾的字符串,以等号 = 后跟数字开头的字符串,或者以非特殊符号和非空白字符开头的字符串,如 '=1 {'
match: /\s*(?:=\d+|[^\p{Pat_Syn}\p{Pat_WS}]+)\s*\{/u, match: /\s*(?:=\d+|[^\p{Pat_Syn}\p{Pat_WS}]+)\s*\{/u,
push: 'body', push: 'body', // 匹配成功则会push到body栈中
value: src => src.substring(0, src.indexOf('{')).trim(), value: (src: string) => src.substring(0, src.indexOf('{')).trim(),
}, },
end: { match: /\s*\}/u, pop: 1 }, end: { match: /\s*\}/u, pop: 1 },
}; };

View File

@ -17,12 +17,13 @@ import Lexer from './Lexer';
import { mappingRule } from './mappingRule'; import { mappingRule } from './mappingRule';
import ruleUtils from '../utils/parseRuleUtils'; import ruleUtils from '../utils/parseRuleUtils';
import { RawToken } from '../types/types'; import { RawToken } from '../types/types';
import { STATE_GROUP_START_INDEX, GLOBAL_FLAG, STICKY_FLAG, UNICODE_FLAG, VERTICAL_LINE } from '../constants';
const defaultErrorRule = ruleUtils.getRuleOptions('error', { lineBreaks: true, shouldThrow: true }); const defaultErrorRule = ruleUtils.getRuleOptions('error', { lineBreaks: true, shouldThrow: true });
// 解析规则并生成词法分析器所需的数据结构,以便进行词法分析操作 // 解析规则并生成词法分析器所需的数据结构,以便进行词法分析操作
function parseRules(rules: Record<string, any>, hasStates: boolean): Record<string, any> { function parseRules(rules: Record<string, any>, hasStates: boolean): Record<string, object> {
let errorRule: Record<string, any> | null = null; let errorRule: Record<string, object> | null = null;
const fast: Record<string, unknown> = {}; const fast: Record<string, unknown> = {};
let enableFast = true; let enableFast = true;
let unicodeFlag: boolean | null = null; let unicodeFlag: boolean | null = null;
@ -58,7 +59,7 @@ function parseRules(rules: Record<string, any>, hasStates: boolean): Record<stri
groups.push(options); groups.push(options);
// 检查是否所有规则都使用了 unicode 标志,或者都未使用 // 检查是否所有规则都使用了unicode或者都未使用
unicodeFlag = checkUnicode(match, unicodeFlag, options); unicodeFlag = checkUnicode(match, unicodeFlag, options);
const pat = ruleUtils.getRegUnion(match.map(ruleUtils.getReg)); const pat = ruleUtils.getRegUnion(match.map(ruleUtils.getReg));
@ -81,11 +82,11 @@ function parseRules(rules: Record<string, any>, hasStates: boolean): Record<stri
// 如果没有 fallback 规则,则使用 sticky 标志,只在当前索引位置寻找匹配项,如果不支持 sticky 标志,则使用无法被否定的空模式来模拟 // 如果没有 fallback 规则,则使用 sticky 标志,只在当前索引位置寻找匹配项,如果不支持 sticky 标志,则使用无法被否定的空模式来模拟
const fallbackRule = errorRule && errorRule.fallback; const fallbackRule = errorRule && errorRule.fallback;
let flags = ruleUtils.checkSticky() && !fallbackRule ? 'ym' : 'gm'; let flags = ruleUtils.checkSticky() && !fallbackRule ? STICKY_FLAG : GLOBAL_FLAG;
const suffix = ruleUtils.checkSticky() || fallbackRule ? '' : '|'; const suffix = ruleUtils.checkSticky() || fallbackRule ? '' : VERTICAL_LINE;
if (unicodeFlag === true) { if (unicodeFlag === true) {
flags += 'u'; flags += UNICODE_FLAG;
} }
const combined = new RegExp(ruleUtils.getRegUnion(parts) + suffix, flags); const combined = new RegExp(ruleUtils.getRegUnion(parts) + suffix, flags);
@ -97,18 +98,18 @@ function parseRules(rules: Record<string, any>, hasStates: boolean): Record<stri
}; };
} }
export function checkStateGroup(group: Record<string, any>, name: string, map: Record<string, any>) { export function checkStateGroup(group: Record<string, any>, name: string, mappingRules: Record<string, object>) {
const state = group && (group.push || group.next); const state = group && (group.push || group.next);
if (state && !map[state]) { if (state && !mappingRules[state]) {
throw new Error('The state is missing.'); throw new Error('The state is missing.');
} }
if (group && group.pop && +group.pop !== 1) { if (group && group.pop && +group.pop !== STATE_GROUP_START_INDEX) {
throw new Error('The value of pop must be 1.'); throw new Error('The value of pop must be 1.');
} }
} }
// 将国际化解析规则注入分词器中 // 将国际化解析规则注入分词器中
function parseMappingRule(mappingRule: Record<string, any>, startState?: string): Lexer<RawToken> { function parseMappingRule(mappingRule: Record<string, object>, startState?: string): Lexer<RawToken> {
const keys = Object.getOwnPropertyNames(mappingRule); const keys = Object.getOwnPropertyNames(mappingRule);
if (!startState) { if (!startState) {
@ -133,7 +134,7 @@ function parseMappingRule(mappingRule: Record<string, any>, startState?: string)
continue; continue;
} }
const splice = [j, 1]; const splice = [j, STATE_GROUP_START_INDEX];
if (rule.include !== key && !included[rule.include]) { if (rule.include !== key && !included[rule.include]) {
included[rule.include] = true; included[rule.include] = true;
const newRules = ruleMap[rule.include]; const newRules = ruleMap[rule.include];
@ -174,17 +175,30 @@ function parseMappingRule(mappingRule: Record<string, any>, startState?: string)
}); });
}); });
// 将规则注入到词法解析器
return new Lexer(mappingAllRules, startState); return new Lexer(mappingAllRules, startState);
} }
function processFast(match, fast: Record<string, unknown>, options) { /**
*
* @param match
* @param fast
* @param options
*/
function processFast(match: Record<string, any>, fast: Record<string, unknown> = {}, options: Record<string, object>) {
while (match.length && typeof match[0] === 'string' && match[0].length === 1) { while (match.length && typeof match[0] === 'string' && match[0].length === 1) {
// 获取到数组的第一个元素
const word = match.shift(); const word = match.shift();
fast[word.charCodeAt(0)] = options; fast[word.charCodeAt(0)] = options;
} }
} }
function handleErrorRule(options, errorRule: Record<string, any>) { /**
*
* @param options
* @param errorRule
*/
function handleErrorRule(options: Record<string, object>, errorRule: Record<string, object>) {
if (!options.fallback === !errorRule.fallback) { if (!options.fallback === !errorRule.fallback) {
throw new Error('errorRule can only set one!'); throw new Error('errorRule can only set one!');
} else { } else {
@ -192,7 +206,13 @@ function handleErrorRule(options, errorRule: Record<string, any>) {
} }
} }
function checkUnicode(match, unicodeFlag, options) { /**
* message中是否包含Unicode
* @param match message
* @param unicodeFlag Unicode标志
* @param options
*/
function checkUnicode(match: Record<string, any>, unicodeFlag: boolean | null, options: Record<string, any>) {
for (let j = 0; j < match.length; j++) { for (let j = 0; j < match.length; j++) {
const obj = match[j]; const obj = match[j];
if (!ruleUtils.checkRegExp(obj)) { if (!ruleUtils.checkRegExp(obj)) {
@ -201,14 +221,16 @@ function checkUnicode(match, unicodeFlag, options) {
if (unicodeFlag === null) { if (unicodeFlag === null) {
unicodeFlag = obj.unicode; unicodeFlag = obj.unicode;
} else if (unicodeFlag !== obj.unicode && options.fallback === false) { } else {
if (unicodeFlag !== obj.unicode && options.fallback === false) {
throw new Error('If the /u flag is used, all!'); throw new Error('If the /u flag is used, all!');
} }
} }
}
return unicodeFlag; return unicodeFlag;
} }
function checkStateOptions(hasStates: boolean, options) { function checkStateOptions(hasStates: boolean, options: Record<string, any>) {
if (!hasStates) { if (!hasStates) {
throw new Error('State toggle options are not allowed in stateless tokenizers!'); throw new Error('State toggle options are not allowed in stateless tokenizers!');
} }
@ -217,6 +239,11 @@ function checkStateOptions(hasStates: boolean, options) {
} }
} }
/**
* fallback属性
* @param rules
* @param enableFast
*/
function isExistsFallback(rules: Record<string, any>, enableFast: boolean) { function isExistsFallback(rules: Record<string, any>, enableFast: boolean) {
for (let i = 0; i < rules.length; i++) { for (let i = 0; i < rules.length; i++) {
if (rules[i].fallback) { if (rules[i].fallback) {
@ -226,7 +253,7 @@ function isExistsFallback(rules: Record<string, any>, enableFast: boolean) {
return enableFast; return enableFast;
} }
function isOptionsErrorOrFallback(options, errorRule: Record<string, any> | null) { function isOptionsErrorOrFallback(options: Record<string, object>, errorRule: Record<string, object> | null) {
if (options.error || options.fallback) { if (options.error || options.fallback) {
// 只能设置一个 errorRule // 只能设置一个 errorRule
if (errorRule) { if (errorRule) {

View File

@ -14,23 +14,13 @@
*/ */
import { lexer } from './parseMappingRule'; import { lexer } from './parseMappingRule';
import { RawToken, Token } from '../types/types'; import { RawToken } from '../types/types';
import { DEFAULT_PLURAL_KEYS } from '../constants'; import { DEFAULT_PLURAL_KEYS } from '../constants';
import { Content, FunctionArg, PlainArg, Select, TokenContext } from '../types/interfaces'; import { Content, FunctionArg, PlainArg, Select, TokenContext } from '../types/interfaces';
import Lexer from './Lexer';
const getContext = (lt: Record<string, any>): TokenContext => ({
offset: lt.offset,
line: lt.line,
col: lt.col,
text: lt.text,
lineNum: lt.lineBreaks,
});
export const checkSelectType = (value: string): boolean => {
return value === 'plural' || value === 'select' || value === 'selectordinal';
};
/**
* Token,AST
*/
class Parser { class Parser {
cardinalKeys: string[] = DEFAULT_PLURAL_KEYS; cardinalKeys: string[] = DEFAULT_PLURAL_KEYS;
ordinalKeys: string[] = DEFAULT_PLURAL_KEYS; ordinalKeys: string[] = DEFAULT_PLURAL_KEYS;
@ -39,7 +29,7 @@ class Parser {
lexer.reset(message); lexer.reset(message);
} }
isSelectKeyValid(token: RawToken, type: Select['type'], value: string) { isSelectKeyValid(type: Select['type'], value: string) {
if (value[0] === '=') { if (value[0] === '=') {
if (type === 'select') { if (type === 'select') {
throw new Error('The key value of the select type is invalid.'); throw new Error('The key value of the select type is invalid.');
@ -75,7 +65,7 @@ class Parser {
break; break;
} }
case 'case': { case 'case': {
this.isSelectKeyValid(token, type, token.value); this.isSelectKeyValid(type, token.value);
select.cases.push({ select.cases.push({
key: token.value.replace(/=/g, ''), key: token.value.replace(/=/g, ''),
tokens: this.parse(isPlural), tokens: this.parse(isPlural),
@ -94,6 +84,11 @@ class Parser {
throw new Error('The message end position is invalid.'); throw new Error('The message end position is invalid.');
} }
/**
* Token
* @param token
* @param isPlural
*/
parseToken(token: RawToken, isPlural: boolean): PlainArg | FunctionArg | Select { parseToken(token: RawToken, isPlural: boolean): PlainArg | FunctionArg | Select {
const context = getContext(token); const context = getContext(token);
const nextToken = lexer.next(); const nextToken = lexer.next();
@ -153,7 +148,12 @@ class Parser {
} }
} }
// 在根级别解析时,遇到结束符号即结束解析并返回结果;而在非根级别解析时,遇到结束符号会被视为不合法的结束位置,抛出错误 /**
*
*
* @param isPlural
* @param isRoot
*/
parse(isPlural: boolean, isRoot?: boolean): Array<Content | PlainArg | FunctionArg | Select> { parse(isPlural: boolean, isRoot?: boolean): Array<Content | PlainArg | FunctionArg | Select> {
const tokens: any[] = []; const tokens: any[] = [];
let content: string | Content | null = null; let content: string | Content | null = null;
@ -201,6 +201,23 @@ class Parser {
} }
} }
/**
* Token
* @param Token Token
*/
const getContext = (Token: RawToken): TokenContext => ({
offset: Token.offset,
line: Token.line,
col: Token.col,
text: Token.text,
lineNum: Token.lineBreaks,
});
// 用以检查select规则中的类型
export const checkSelectType = (value: string): boolean => {
return value === 'plural' || value === 'select' || value === 'selectordinal';
};
export default function parse(message: string): Array<Content | PlainArg | FunctionArg | Select> { export default function parse(message: string): Array<Content | PlainArg | FunctionArg | Select> {
const parser = new Parser(message); const parser = new Parser(message);
return parser.parse(false, true); return parser.parse(false, true);

View File

@ -13,15 +13,25 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import { AllLocaleConfig, AllMessages, Locale, Locales, Error, DatePool, SelectPool, RawToken } from './types'; import {
AllLocaleConfig,
AllMessages,
Locale,
Locales,
Error,
DatePool,
SelectPool,
RawToken,
InulaNode,
} from './types';
import I18n from '../core/I18n'; import I18n from '../core/I18n';
import Lexer from '../parser/Lexer'; import Lexer from '../parser/Lexer';
import { InulaElement, Key } from 'openinula';
// FormattedMessage的参数定义 // FormattedMessage的参数定义
export interface FormattedMessageProps extends MessageDescriptor { export interface FormattedMessageProps extends MessageDescriptor {
values?: Record<string, unknown>; values?: Record<string, unknown>;
tagName?: string; tagName?: string;
children?(nodes: any[]): any; children?(nodes: any[]): any;
} }
@ -34,7 +44,7 @@ export interface MessageDescriptor extends MessageOptions {
export interface MessageOptions { export interface MessageOptions {
comment?: string; comment?: string;
message?: string; messages?: string;
context?: string; context?: string;
formatOptions?: FormatOptions; formatOptions?: FormatOptions;
} }
@ -48,15 +58,26 @@ export interface I18nCache {
octothorpe: Record<string, any>; octothorpe: Record<string, any>;
} }
export interface RichText {
components?: { [key: string]: InulaNode };
}
export interface InulaPortal extends InulaElement {
key: Key | null;
children: InulaNode;
}
// I18n类的传参 // I18n类的传参
export interface I18nProps { export type I18nProps = RichText & {
locale?: Locale; locale?: Locale;
locales?: Locales; locales?: Locales;
messages?: AllMessages; messages?: AllMessages;
defaultLocale?: string;
timeZone?: string;
localeConfig?: AllLocaleConfig; localeConfig?: AllLocaleConfig;
cache?: I18nCache; cache?: I18nCache;
error?: Error; onError?: Error;
} };
// 消息格式化选项类型 // 消息格式化选项类型
export interface FormatOptions { export interface FormatOptions {
@ -74,16 +95,13 @@ export interface I18nContextProps {
i18n?: I18n; i18n?: I18n;
} }
export interface configProps { export type configProps = I18nProps & {
locale?: Locale;
messages?: AllMessages;
defaultLocale?: string;
RenderOnLocaleChange?: boolean; RenderOnLocaleChange?: boolean;
children?: any; children?: any;
onWarn?: Error; onWarn?: Error;
} };
export interface IntlMessageFormat extends configProps, MessageOptions { export interface IntlMessageFormat {
plural: ( plural: (
value: number, value: number,
{ {
@ -204,7 +222,6 @@ export interface InjectedIntl {
formatMessage( formatMessage(
messageDescriptor: MessageDescriptor, messageDescriptor: MessageDescriptor,
values?: Record<string, unknown>, values?: Record<string, unknown>,
options?: MessageOptions, options?: MessageOptions
useMemorize?: boolean ): string | any[];
): string;
} }

View File

@ -23,16 +23,17 @@ import {
I18nContextProps, I18nContextProps,
configProps, configProps,
InjectedIntl, InjectedIntl,
InulaPortal,
} from './interfaces'; } from './interfaces';
import I18n from '../core/I18n'; import { InulaElement } from 'openinula';
export type Error = string | ((message, id, context) => string); export type Error = string | ((message: any, id: any, context: any) => string);
export type Locale = string; export type Locale = string;
export type Locales = Locale | Locale[]; export type Locales = Locale | Locale[];
export type LocaleConfig = { plurals?: (...arg: any) => any }; export type LocaleConfig = { plurals?: (...args: any[]) => any };
export type AllLocaleConfig = Record<Locale, LocaleConfig>; export type AllLocaleConfig = Record<Locale, LocaleConfig>;
@ -59,7 +60,7 @@ export type Token = Content | PlainArg | FunctionArg | Select | Octothorpe;
export type DatePool = Date | string; export type DatePool = Date | string;
export type SelectPool = string | Record<string, unknown>; export type SelectPool = string | number;
export type RawToken = { export type RawToken = {
type: string; type: string;
@ -74,13 +75,23 @@ export type RawToken = {
export type I18nProviderProps = I18nContextProps & configProps; export type I18nProviderProps = I18nContextProps & configProps;
export type IntlType = { export type IntlType = I18nContextProps & {
i18n: I18n; defaultLocale?: string | undefined;
onError?: Error | undefined;
messages?:
| string
| Record<string, string>
| Record<string, string | CompiledMessagePart[]>
| Record<string, Record<string, string> | Record<string, string | CompiledMessagePart[]>>;
locale?: string;
formatMessage: (...args: any[]) => any; formatMessage: (...args: any[]) => any;
formatNumber: (...args: any[]) => any; formatNumber: (...args: any[]) => any;
formatDate: (...args: any[]) => any; formatDate: (...args: any[]) => any;
$t?: (...args: any[]) => any;
}; };
export interface InjectedIntlProps { export type InjectedIntlProps = {
intl: InjectedIntl; intl: InjectedIntl;
} };
export type InulaNode = InulaElement | string | number | Iterable<InulaNode> | InulaPortal | boolean | null | undefined;

View File

@ -0,0 +1,107 @@
/*
* 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 { cloneElement, createElement, Fragment, InulaElement } from 'openinula';
import { voidElementTags } from '../constants';
// 用于匹配标签的正则表达式
const tagReg = /<(\d+)>(.*?)<\/\1>|<(\d+)\/>/;
// 用于匹配换行符的正则表达式
const nlReg = /(?:\r\n|\r|\n)/g;
export function formatElements(
value: string,
elements: { [key: string]: InulaElement<any> } = {}
): string | Array<any> {
const elementKeyID = getElementIndex(0, '$Inula');
// valueThis is a rich text with a custom component: <1/>
const arrays = value.replace(nlReg, '').split(tagReg);
// 若无InulaNode元素则返回
if (arrays.length === 1) return value;
const result: any = [];
const before = arrays.shift();
if (before) {
result.push(before);
}
for (const [index, children, after] of getElements(arrays)) {
let element = elements[index];
if (!element || (voidElementTags[element.type as string] && children)) {
const errorMessage = !element
? `Index not declared as ${index} in original translation`
: `${element.type} , No child element exists. Please check.`;
console.error(errorMessage);
// 对于异常元素,通过创建<></>来代替,并继续解析现有的子元素和之后的元素,并保证在构建数组时,不会因为缺少元素而导致索引错位。
element = createElement(Fragment, {});
}
// 如果存在子元素,则进行递归处理
const formattedChildren = children ? formatElements(children, elements) : element.props.children;
// 更新element 的属性和子元素
const clonedElement = cloneElement(element, { key: elementKeyID() }, formattedChildren);
result.push(clonedElement);
if (after) {
result.push(after);
}
}
return result;
}
/**
* arrays数组中解析出标签元素和其子元素
* @param arrays
*/
function getElements(arrays: string[]) {
// 如果 arrays 数组为空,则返回空数组
if (!arrays.length) return [];
/**
* pairedIndex: 第一个元素表示配对标签的内容 <1>...</1>
* children: 第二个元素表示配对标签内的子元素内容
* unpairedIndex: 第三个元素表示自闭合标签的内容 <1/>
* textAfter: 第四个元素表示标签之后的文本内容
* eg: [undefined,undefined,1,""]
*/
const [pairedIndex, children, unpairedIndex, textAfter] = arrays.splice(0, 4);
// 解析当前标签元素和它的子元素,返回一个包含标签索引、子元素和后续文本的数组
const currentElement: [number, string, string] = [
parseInt(pairedIndex || unpairedIndex), // 解析标签索引,如果是自闭合标签,则使用 unpaired
children || '',
textAfter || '',
];
// 递归调用 getElements 函数,处理剩余的 arrays 数组
const remainingElements = getElements(arrays);
// 将当前元素和递归处理后的元素数组合并并返回
return [currentElement, ...remainingElements];
}
// 对传入富文本元素的位置标志索引
function getElementIndex(count = 0, prefix = '') {
return function () {
return `${prefix}_${count++}`;
};
}

View File

@ -18,6 +18,7 @@ function getType(input: any): string {
return str.slice(8, -1).toLowerCase(); return str.slice(8, -1).toLowerCase();
} }
// 类型检查器
const createTypeChecker = (type: string) => { const createTypeChecker = (type: string) => {
return (input: any) => { return (input: any) => {
return getType(input) === type.toLowerCase(); return getType(input) === type.toLowerCase();
@ -28,24 +29,25 @@ const checkObject = (input: any) => input !== null && typeof input === 'object';
const checkRegExp = createTypeChecker('RegExp'); const checkRegExp = createTypeChecker('RegExp');
// 使用正则表达式如果对象存在则访问该属性用来判断当前环境是否支持正则表达式sticky属性。
const checkSticky = () => typeof new RegExp('')?.sticky === 'boolean'; const checkSticky = () => typeof new RegExp('')?.sticky === 'boolean';
// 转义正则表达式中的特殊字符 // 转义正则表达式中的特殊字符
function transferReg(s: string): string { function transferReg(str: string): string {
// eslint-disable-next-line // eslint-disable-next-line
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
} }
// 计算正则表达式中捕获组的数量 // 计算正则表达式中捕获组的数量,用以匹配()
function getRegGroups(s: string): number { function getRegGroups(str: string): number {
const re = new RegExp('|' + s); const regExp = new RegExp('|' + str);
// eslint-disable-next-line // eslint-disable-next-line
return re.exec('')?.length! - 1; return regExp.exec('')?.length! - 1;
} }
// 创建一个捕获组的正则表达式模式 // 创建一个捕获组的正则表达式模式
function getRegCapture(s: string): string { function getRegCapture(str: string): string {
return '(' + s + ')'; return '(' + str + ')';
} }
// 将正则表达式合并为一个联合的正则表达式模式 // 将正则表达式合并为一个联合的正则表达式模式
@ -53,7 +55,7 @@ function getRegUnion(regexps: string[]): string {
if (!regexps.length) { if (!regexps.length) {
return '(?!)'; return '(?!)';
} }
const source = regexps.map(s => '(?:' + s + ')').join('|'); const source = regexps.map(str => '(?:' + str + ')').join('|');
return '(?:' + source + ')'; return '(?:' + source + ')';
} }
@ -143,7 +145,7 @@ function getRulesByArray(array: any[]) {
return result; return result;
} }
function getRuleOptions(type, obj) { function getRuleOptions(type: any, obj: any) {
// 如果 obj 不是一个对象,则将其转换为包含 'match' 属性的对象 // 如果 obj 不是一个对象,则将其转换为包含 'match' 属性的对象
if (!checkObject(obj)) { if (!checkObject(obj)) {
obj = { match: obj }; obj = { match: obj };
@ -182,23 +184,23 @@ function getRuleOptions(type, obj) {
} else { } else {
options.match = []; options.match = [];
} }
options.match.sort((a, b) => { options.match.sort((str1: string, str2: string) => {
// 根据规则的类型进行排序,确保正则表达式排在最前面,长度较长的规则排在前面 // 根据规则的类型进行排序,确保正则表达式排在最前面,长度较长的规则排在前面
if (checkRegExp(a) && checkRegExp(b)) { if (checkRegExp(str1) && checkRegExp(str2)) {
return 0; return 0;
} else if (checkRegExp(b)) { } else if (checkRegExp(str2)) {
return -1; return -1;
} else if (checkRegExp(a)) { } else if (checkRegExp(str1)) {
return +1; return +1;
} else { } else {
return b.length - a.length; return str2.length - str1.length;
} }
}); });
return options; return options;
} }
function getRules(spec) { function getRules(spec: any) {
return Array.isArray(spec) ? getRulesByArray(spec) : getRulesByObject(spec); return Array.isArray(spec) ? getRulesByArray(spec) : getRulesByObject(spec);
} }

View File

@ -32,7 +32,7 @@ function compile(message: string): CompiledMessage {
try { try {
return getTokenAST(parse(message)); return getTokenAST(parse(message));
} catch (e) { } catch (e) {
console.error(`Message cannot be parse due to syntax errors: ${message}`); console.error(`Message cannot be parse due to syntax errors: ${message},cause by ${e}`);
return message; return message;
} }
} }

View File

@ -1,136 +0,0 @@
/*
* Copyright (c) 2023 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 I18n from '../../src/core/I18n';
describe('I18n', () => {
it('load catalog and merge with existing', () => {
const i18n = new I18n({});
const messages = {
Hello: 'Hello',
};
i18n.loadMessage('en', messages);
i18n.changeLanguage('en');
expect(i18n.messages).toEqual(messages);
i18n.loadMessage('fr', { Hello: 'Salut' });
expect(i18n.messages).toEqual(messages);
});
it('should load multiple language ', function () {
const enMessages = {
Hello: 'Hello',
};
const frMessage = {
Hello: 'Salut',
};
const intl = new I18n({});
intl.loadMessage({
en: enMessages,
fr: frMessage,
});
intl.changeLanguage('en');
expect(intl.messages).toEqual(enMessages);
intl.changeLanguage('fr');
expect(intl.messages).toEqual(frMessage);
});
it('should switch active locale', () => {
const messages = {
Hello: 'Salut',
};
const i18n = new I18n({
locale: 'en',
messages: {
fr: messages,
en: {},
},
});
expect(i18n.locale).toEqual('en');
expect(i18n.messages).toEqual({});
i18n.changeLanguage('fr');
expect(i18n.locale).toEqual('fr');
expect(i18n.messages).toEqual(messages);
});
it('should switch active locale', () => {
const messages = {
Hello: 'Salut',
};
const i18n = new I18n({
locale: 'en',
messages: {
en: messages,
fr: {},
},
});
i18n.changeLanguage('en');
expect(i18n.locale).toEqual('en');
expect(i18n.messages).toEqual(messages);
i18n.changeLanguage('fr');
expect(i18n.locale).toEqual('fr');
expect(i18n.messages).toEqual({});
});
it('._ allow escaping syntax characters', () => {
const messages = {
"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}");
});
it('._ should format message from catalog', function () {
const messages = {
Hello: 'Salut',
id: "Je m'appelle {name}",
};
const i18n = new I18n({
locale: 'fr',
messages: { fr: messages },
});
expect(i18n.locale).toEqual('fr');
expect(i18n.formatMessage('Hello')).toEqual('Salut');
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual("Je m'appelle Fred");
});
it('should return the formatted date and time', () => {
const i18n = new I18n({
locale: 'fr',
});
const formattedDateTime = i18n.formatDate('2023-06-06T07:53:54.465Z', {
dateStyle: 'full',
timeStyle: 'short',
});
expect(typeof formattedDateTime).toBe('string');
expect(formattedDateTime).toEqual('mardi 6 juin 2023 à 15:53');
});
it('should return the formatted number', () => {
const i18n = new I18n({
locale: 'en',
});
const formattedNumber = i18n.formatNumber(123456.789, { style: 'currency', currency: 'USD' });
expect(typeof formattedNumber).toBe('string');
expect(formattedNumber).toEqual('$123,456.79');
});
});

View File

@ -0,0 +1,270 @@
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
*/
import I18n from '../../src/core/I18n';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom/';
// 测试组件
const IndividualCustomComponent = () => {
return <span>Custom Component</span>;
};
const CustomComponent = (props: any) => {
return <div>{props.children}</div>;
};
const CustomComponentChildren = (props: any) => {
return <div>{props.children}</div>;
};
describe('I18n', () => {
it('load catalog and merge with existing', () => {
const i18n = new I18n({});
const messages = {
Hello: 'Hello',
};
i18n.loadMessage('en', messages);
i18n.changeLanguage('en');
expect(i18n.messages).toEqual(messages);
i18n.loadMessage('fr', { Hello: 'Salut' });
expect(i18n.messages).toEqual(messages);
i18n.changeMessage({ Hello: 'Salut' });
expect(i18n.messages).toEqual({ Hello: 'Salut' });
});
it('should load multiple language ', function () {
const enMessages = {
Hello: 'Hello',
};
const frMessage = {
Hello: 'Salut',
};
const intl = new I18n({});
intl.loadMessage({
en: enMessages,
fr: frMessage,
});
intl.changeLanguage('en');
expect(intl.messages).toEqual(enMessages);
intl.changeLanguage('fr');
expect(intl.messages).toEqual(frMessage);
});
it('should switch active locale', () => {
const messages = {
Hello: 'Salut',
};
const i18n = new I18n({
locale: 'en',
messages: {
fr: messages,
en: {},
},
});
expect(i18n.locale).toEqual('en');
expect(i18n.messages).toEqual({});
i18n.changeLanguage('fr');
expect(i18n.locale).toEqual('fr');
expect(i18n.messages).toEqual(messages);
});
it('should switch active locale', () => {
const messages = {
Hello: 'Salut',
};
const i18n = new I18n({
locale: 'en',
messages: {
en: messages,
fr: {},
},
});
i18n.changeLanguage('en');
expect(i18n.locale).toEqual('en');
expect(i18n.messages).toEqual(messages);
i18n.changeLanguage('fr');
expect(i18n.locale).toEqual('fr');
expect(i18n.messages).toEqual({});
});
it('._ allow escaping syntax characters', () => {
const messages = {
"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}'");
});
it('._ should format message from catalog', function () {
const messages = {
Hello: 'Salut',
id: "Je m'appelle {name}",
};
const i18n = new I18n({
locale: 'fr',
messages: { fr: messages },
});
expect(i18n.locale).toEqual('fr');
expect(i18n.formatMessage('Hello')).toEqual('Salut');
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual("Je m'appelle Fred");
});
it('should return information with html element', () => {
const messages = {
id: 'hello, {name}',
};
const i18n = new I18n({
locale: 'es',
messages: { es: messages },
});
const value = '<strong>Jane</strong>';
expect(i18n.formatMessage({ id: 'id' }, { name: value })).toEqual('hello, <strong>Jane</strong>');
});
it('test demo from product', () => {
const messages = {
id: "服务商名称长度不能超过64个字符允许输入中文、字母、数字、字符_-!@#$^.+'}{'且不能为关键字null(不区分大小写)。",
};
const i18n = new I18n({
locale: 'zh',
messages: { zh: messages },
});
expect(i18n.formatMessage('id')).toEqual(
"服务商名称长度不能超过64个字符允许输入中文、字母、数字、字符_-!@#$^.+'}{'且不能为关键字null(不区分大小写)。"
);
});
it('Should return information with dom element', () => {
const messages = {
richText: 'This is a rich text with a custom component: {customComponent}',
};
const i18n = new I18n({
locale: 'es',
messages: { es: messages },
});
const values = {
customComponent: <IndividualCustomComponent />,
};
const formattedMessage = i18n.formatMessage({ id: 'richText' }, values);
// 渲染格式化后的文本内容
const { getByText } = render(<div>{formattedMessage}</div>);
// 检查文本内容中是否包含自定义组件的内容
expect(getByText('This is a rich text with a custom component')).toContain(
'This is a rich text with a custom component'
);
});
it('Should return information for nested scenes with dom elements', () => {
const messages = {
richText: 'This is a rich text with a custom component: {customComponent}',
msg: 'test',
};
const i18n = new I18n({
locale: 'es',
messages: { es: messages },
});
const values = {
customComponent: (
<CustomComponent style={{ margin: '0 4px' }} text={'123'}>
<CustomComponentChildren>{i18n.formatMessage({ id: 'msg' })}</CustomComponentChildren>
</CustomComponent>
),
};
const formattedMessage = i18n.formatMessage({ id: 'richText' }, values);
// 渲染格式化后的文本内容
const { getByText } = render(<div>{formattedMessage}</div>);
// 检查文本内容中是否包含自定义组件的内容
expect(getByText('test')).toBeTruthy();
});
it('Should return information for nested scenes with dom elements', () => {
const messages = {
richText: 'This is a rich text with a custom component: {customComponent}',
msg: 'test',
};
const i18n = new I18n({
locale: 'es',
messages: { es: messages },
});
const values = {
customComponent: (
<CustomComponent style={{ margin: '0 4px' }} text={'123'}>
{i18n.formatMessage({ id: 'msg' })}
</CustomComponent>
),
};
const formattedMessage = i18n.formatMessage({ id: 'richText' }, values);
// 渲染格式化后的文本内容
const { getByText } = render(<div>{formattedMessage}</div>);
// 检查文本内容中是否包含自定义组件的内容
expect(getByText('test')).toBeTruthy();
});
it('should be returned as value when Multiple dom elements\n', () => {
const messages = {
richText: '{today}, my name is {name}, and {age} years old!',
};
const i18n = new I18n({
locale: 'es',
messages: { es: messages },
});
const Name = () => {
return <span>tom</span>;
};
const Age = () => {
return <span>16</span>;
};
const Today = () => {
return <span>32</span>;
};
const values = {
today: <Today />,
name: <Name />,
age: <Age />,
};
const formattedMessage = i18n.formatMessage({ id: 'richText' }, values);
// 渲染格式化后的文本内容
const { getByText } = render(<div>{formattedMessage}</div>);
// 检查文本内容中是否包含自定义组件的内容
expect(getByText('my name is tom, and 16 years old!')).toBeTruthy();
});
it('should return the formatted date and time', () => {
const i18n = new I18n({
locale: 'fr',
});
const formattedDateTime = i18n.formatDate('2023-06-06T07:53:54.465Z', {
dateStyle: 'full',
timeStyle: 'short',
});
expect(typeof formattedDateTime).toBe('string');
expect(formattedDateTime).toEqual('mardi 6 juin 2023 à 15:53');
});
it('should return the formatted number', () => {
const i18n = new I18n({
locale: 'en',
});
const formattedNumber = i18n.formatNumber(123456.789, { style: 'currency', currency: 'USD' });
expect(typeof formattedNumber).toBe('string');
expect(formattedNumber).toEqual('$123,456.79');
});
});

View File

@ -43,7 +43,7 @@ describe('<FormattedMessage>', () => {
); );
setTimeout(() => { setTimeout(() => {
expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('hello', '', {})); expect(getByTestId('id').textContent).toEqual(i18n.formatMessage('hello', {}, {}));
}, 1000); }, 1000);
}); });
it('should format context', function () { it('should format context', function () {
@ -58,6 +58,6 @@ describe('<FormattedMessage>', () => {
</span> </span>
</I18nProvider> </I18nProvider>
); );
expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('id', { name: 'fred' }, {})); expect(getByTestId('id').textContent).toEqual(i18n.formatMessage('id', { name: 'fred' }, {}));
}); });
}); });

View File

@ -42,7 +42,6 @@ describe('InjectIntl', () => {
jest.spyOn(console, 'error').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {});
const Injected = injectIntl(Wrapped); const Injected = injectIntl(Wrapped);
// @ts-ignore
expect(() => render(<Injected />)).toThrow("Cannot read properties of null (reading 'i18n')"); expect(() => render(<Injected />)).toThrow("Cannot read properties of null (reading 'i18n')");
}); });
@ -53,7 +52,7 @@ describe('InjectIntl', () => {
}; };
const { getByTestId } = mountWithProvider(<Injected {...props} />); const { getByTestId } = mountWithProvider(<Injected {...props} />);
expect(getByTestId('test')).toHaveTextContent( expect(JSON.stringify(getByTestId('test'))).toEqual(
'{"_events":{},"locale":"en","locales":["en"],"allMessages":{},"_localeData":{}}' '{"_events":{},"locale":"en","locales":["en"],"allMessages":{},"_localeData":{}}'
); );
}); });

View File

@ -29,6 +29,20 @@ describe('createI18n', () => {
).toBe('bar'); ).toBe('bar');
}); });
it('createIntl', function () {
const i18n = createI18n({
locale: 'en',
messages: {
test: 'test',
},
});
expect(
i18n.$t({
id: 'test',
})
).toBe('test');
});
it('should not warn when defaultRichTextElements is not used', function () { it('should not warn when defaultRichTextElements is not used', function () {
const onWarn = jest.fn(); const onWarn = jest.fn();
createI18n({ createI18n({

View File

@ -1,75 +0,0 @@
/*
* Copyright (c) 2023 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 * as React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider, useIntl } from '../../../index';
const FunctionComponent = ({ spy }: { spy?: Function }) => {
const { i18n } = useIntl();
spy!(i18n.locale);
return null;
};
const FC = () => {
const i18n = useIntl();
return i18n.formatNumber(10000, { style: 'currency', currency: 'USD' }) as any;
};
describe('useIntl() hooks', () => {
it('throws when <IntlProvider> is missing from ancestry', () => {
// So it doesn't spam the console
jest.spyOn(console, 'error').mockImplementation(() => {});
expect(() => render(<FunctionComponent />)).toThrow('I18n object is not found!');
});
it('hooks onto the intl context', () => {
const spy = jest.fn();
render(
<IntlProvider locale="en">
<FunctionComponent spy={spy} />
</IntlProvider>
);
expect(spy).toHaveBeenCalledWith('en');
});
it('should work when switching locale on provider', () => {
const { rerender, getByTestId } = render(
<IntlProvider locale="en">
<span data-testid="comp">
<FC />
</span>
</IntlProvider>
);
expect(getByTestId('comp')).toMatchSnapshot();
rerender(
<IntlProvider locale="es">
<span data-testid="comp">
<FC />
</span>
</IntlProvider>
);
expect(getByTestId('comp')).toMatchSnapshot();
rerender(
<IntlProvider locale="en">
<span data-testid="comp">
<FC />
</span>
</IntlProvider>
);
expect(getByTestId('comp')).toMatchSnapshot();
});
});

View File

@ -15,7 +15,7 @@
import creatI18nCache from '../../../src/format/cache/cache'; import creatI18nCache from '../../../src/format/cache/cache';
describe('creatI18nCache', () => { describe('creatI18nCache', () => {
it('should create an empty IntlCache object', () => { it('should create an empty I18nCache object', () => {
const intlCache = creatI18nCache(); const intlCache = creatI18nCache();
expect(intlCache).toEqual({ expect(intlCache).toEqual({

View File

@ -61,7 +61,7 @@ describe('DateTimeFormatter', () => {
expect(spy).toHaveBeenCalledWith('en-GB', { month: 'short' }); expect(spy).toHaveBeenCalledWith('en-GB', { month: 'short' });
}); });
it('should not memoize formatter instances when memoize is false', () => { it('should not memoize formatter instances when cache is effective', () => {
const spy = jest.spyOn(Intl, 'DateTimeFormat'); const spy = jest.spyOn(Intl, 'DateTimeFormat');
const formatter1 = new DateTimeFormatter('en-US', { month: 'short' }); const formatter1 = new DateTimeFormatter('en-US', { month: 'short' });
const formatter2 = new DateTimeFormatter('en-US', { month: 'short' }); const formatter2 = new DateTimeFormatter('en-US', { month: 'short' });
@ -91,7 +91,7 @@ describe('DateTimeFormatter', () => {
expect(formatted).toEqual('January 1, 2023'); expect(formatted).toEqual('January 1, 2023');
}); });
it('should format using memorized formatter when useMemorize is true', () => { it('should format using memorized formatter when cache is effective', () => {
const formatter = new DateTimeFormatter('en-US', { year: 'numeric' }, creatI18nCache()); const formatter = new DateTimeFormatter('en-US', { year: 'numeric' }, creatI18nCache());
const date = new Date(2023, 0, 1); const date = new Date(2023, 0, 1);
const formatted1 = formatter.dateTimeFormat(date); const formatted1 = formatter.dateTimeFormat(date);

View File

@ -24,7 +24,7 @@ describe('getFormatMessage', () => {
}, },
}, },
locale: 'en', locale: 'en',
error: 'missingMessage', onError: 'missingMessage',
}); });
it('should return the correct translation for an existing message ID', () => { it('should return the correct translation for an existing message ID', () => {
@ -32,7 +32,7 @@ describe('getFormatMessage', () => {
const values = { name: 'John' }; const values = { name: 'John' };
const expectedResult = 'Hello, John!'; const expectedResult = 'Hello, John!';
const result = getFormatMessage(i18nInstance, id, values); const result = getFormatMessage(i18nInstance, id, values, {}, {});
expect(result).toEqual(expectedResult); expect(result).toEqual(expectedResult);
}); });
@ -41,7 +41,7 @@ describe('getFormatMessage', () => {
const id = 'missingMessage'; const id = 'missingMessage';
const expectedResult = 'missingMessage'; const expectedResult = 'missingMessage';
const result = getFormatMessage(i18nInstance, id); const result = getFormatMessage(i18nInstance, id, {}, {}, {});
expect(result).toEqual(expectedResult); expect(result).toEqual(expectedResult);
}); });

View File

@ -15,7 +15,7 @@
import copyStaticProps from '../../src/utils/copyStaticProps'; import copyStaticProps from '../../src/utils/copyStaticProps';
describe('copyStaticProps', () => { describe('copyStaticProps', () => {
test('should hoist static properties from sourceComponent to targetComponent', () => { it('should hoist static properties from sourceComponent to targetComponent', () => {
class SourceComponent { class SourceComponent {
static staticProp = 'sourceProp'; static staticProp = 'sourceProp';
} }
@ -23,11 +23,10 @@ describe('copyStaticProps', () => {
class TargetComponent {} class TargetComponent {}
copyStaticProps(TargetComponent, SourceComponent); copyStaticProps(TargetComponent, SourceComponent);
expect((TargetComponent as any).staticProp).toBe('sourceProp'); expect((TargetComponent as any).staticProp).toBe('sourceProp');
}); });
test('should hoist static properties from inherited components', () => { it('should hoist static properties from inherited components', () => {
class SourceComponent { class SourceComponent {
static staticProp = 'sourceProp'; static staticProp = 'sourceProp';
} }
@ -37,11 +36,10 @@ describe('copyStaticProps', () => {
class TargetComponent {} class TargetComponent {}
copyStaticProps(TargetComponent, InheritedComponent); copyStaticProps(TargetComponent, InheritedComponent);
expect((TargetComponent as any).staticProp).toBe('sourceProp'); expect((TargetComponent as any).staticProp).toBe('sourceProp');
}); });
test('should not hoist properties if descriptor is not valid', () => { it('should not hoist properties if descriptor is not valid', () => {
class SourceComponent { class SourceComponent {
get staticProp() { get staticProp() {
return 'sourceProp'; return 'sourceProp';
@ -51,11 +49,10 @@ describe('copyStaticProps', () => {
class TargetComponent {} class TargetComponent {}
copyStaticProps(TargetComponent, SourceComponent); copyStaticProps(TargetComponent, SourceComponent);
expect((TargetComponent as any).staticProp).toBeUndefined(); expect((TargetComponent as any).staticProp).toBeUndefined();
}); });
test('should not hoist properties if descriptor is not valid', () => { it('should not hoist properties if descriptor is not valid', () => {
class SourceComponent { class SourceComponent {
static get staticProp() { static get staticProp() {
return 'sourceProp'; return 'sourceProp';
@ -65,11 +62,10 @@ describe('copyStaticProps', () => {
class TargetComponent {} class TargetComponent {}
copyStaticProps(TargetComponent, SourceComponent); copyStaticProps(TargetComponent, SourceComponent);
expect((TargetComponent as any).staticProp).toBe('sourceProp'); expect((TargetComponent as any).staticProp).toBe('sourceProp');
}); });
test('copyStaticProps should not copy static properties that already exist in target or source component', () => { it('copyStaticProps should not copy static properties that already exist in target or source component', () => {
const targetComponent = { staticProp: 'target' }; const targetComponent = { staticProp: 'target' };
const sourceComponent = { staticProp: 'source' }; const sourceComponent = { staticProp: 'source' };
copyStaticProps(targetComponent, sourceComponent); copyStaticProps(targetComponent, sourceComponent);

View File

@ -31,6 +31,7 @@
"declaration": true, "declaration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"downlevelIteration": true, "downlevelIteration": true,
"emitDeclarationOnly": true,
"declarationDir": "./build/@types", "declarationDir": "./build/@types",
// 使@types/node // 使@types/node
"lib": [ "lib": [
@ -54,7 +55,8 @@
} }
}, },
"include": [ "include": [
"./index.ts" "./index.ts",
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",

View File

@ -12,16 +12,18 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import path from 'path';
const { resolve } = require('path'); import HtmlWebpackPlugin from 'html-webpack-plugin';
const HtmlWebpackPlugin = require('html-webpack-plugin'); import { fileURLToPath } from 'url';
const isDevelopment = process.env.NODE_ENV === 'development'; const isDevelopment = process.env.NODE_ENV === 'development';
const entryPath = './example/index.tsx'; const entryPath = './example/index.tsx';
module.exports = { const __filename = fileURLToPath(import.meta.url);
entry: resolve(__dirname, entryPath), const __dirname = path.dirname(__filename);
export default {
entry: path.join(__dirname, entryPath),
output: { output: {
path: resolve(__dirname, './build'), path: path.join(__dirname, './build'),
filename: 'main.js', filename: 'main.js',
}, },
module: { module: {
@ -50,7 +52,7 @@ module.exports = {
mode: isDevelopment ? 'development' : 'production', mode: isDevelopment ? 'development' : 'production',
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: resolve(__dirname, './example/index.html'), template: path.join(__dirname, './example/index.html'),
}), }),
], ],
resolve: { resolve: {

View File

@ -1,38 +0,0 @@
{
"name": "@openinula/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
}
}

View File

@ -1 +0,0 @@
export const Store = {};

View File

@ -1,14 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}

View File

@ -1,2 +0,0 @@
# DLight Main Package
See the website's documentations for usage.

View File

@ -1,44 +0,0 @@
{
"name": "@openinula/next",
"version": "0.0.1",
"author": {
"name": "IanDx",
"email": "iandxssxx@gmail.com"
},
"keywords": [
"inula"
],
"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/",
"test": "vitest --ui"
},
"dependencies": {
"csstype": "^3.1.3",
"@openinula/store": "workspace:*"
},
"devDependencies": {
"tsup": "^6.5.0",
"vite-plugin-inula-next": "workspace:*",
"vitest": "^1.2.2"
},
"tsup": {
"entry": [
"src/index.js"
],
"format": [
"cjs",
"esm"
],
"clean": true,
"minify": true
}
}

View File

@ -1,373 +0,0 @@
import { DLNode, DLNodeType } from './DLNode';
import { forwardHTMLProp } from './HTMLNode';
import { DLStore, cached } from './store';
import { schedule } from './scheduler';
/**
* @class
* @extends import('./DLNode').DLNode
*/
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
schedule(() => {
// ---- 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);
}

View File

@ -1,210 +0,0 @@
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
* @return {void}
*/
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 = [];
}
}

View File

@ -1,103 +0,0 @@
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();
}
}

View File

@ -1,163 +0,0 @@
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);
}

View File

@ -1,69 +0,0 @@
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);
}
}

View File

@ -1,86 +0,0 @@
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)
);
}
}

View File

@ -1,33 +0,0 @@
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;
}
}

View File

@ -1,406 +0,0 @@
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]);
}
}

View File

@ -1,71 +0,0 @@
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);
});
}
}

View File

@ -1,45 +0,0 @@
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;
});
}
};
}
}

View File

@ -1,48 +0,0 @@
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);
}

View File

@ -1,18 +0,0 @@
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;
}
}

View File

@ -1,24 +0,0 @@
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;
}

View File

@ -1 +0,0 @@
export * from './types/index';

View File

@ -1,62 +0,0 @@
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 = [];
}
/**
* @brief Render the DL class to the element
* @param {typeof import('./CompNode').CompNode} Comp
* @param {HTMLElement | string} idOrEl
*/
export function render(Comp, idOrEl) {
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 Comp();
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.'
);
}

View File

@ -1,25 +0,0 @@
/*
* 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.
*/
const p = Promise.resolve();
/**
* Schedule a task to run in the next microtask.
*
* @param {() => void} task
*/
export function schedule(task) {
p.then(task);
}

View File

@ -1,42 +0,0 @@
import { Store } from '@openinula/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);
}

View File

@ -1,74 +0,0 @@
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;

View File

@ -1,6 +0,0 @@
// ---- env
import { DLightObject } from './compTag';
type AnyEnv = { _$anyEnv: true };
export const env: <T = AnyEnv>() => T extends AnyEnv ? any : DLightObject<T>;

View File

@ -1,13 +0,0 @@
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;

View File

@ -1,516 +0,0 @@
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;
}

Some files were not shown because too many files have changed in this diff Show More