Compare commits
13 Commits
Author | SHA1 | Date |
---|---|---|
|
4608422c0a | |
|
325f4c406a | |
|
1f4b164952 | |
|
2f9d3737db | |
|
ef4126b767 | |
|
b7756e9732 | |
|
bf1bd09721 | |
|
d6ba039445 | |
|
4467bdae73 | |
|
627a8b7785 | |
|
109746acef | |
|
d599b36eaa | |
|
83c80341dc |
|
@ -0,0 +1,8 @@
|
||||||
|
# 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)
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"$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"]
|
||||||
|
}
|
|
@ -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
|
||||||
.inula-max
|
dist
|
||||||
|
.history
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/standalone": "^7.22.4",
|
||||||
|
"@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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
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('main', App);
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import inula from 'vite-plugin-inula-next';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 4320,
|
||||||
|
},
|
||||||
|
base: '',
|
||||||
|
optimizeDeps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
plugins: [inula({ files: '**/*.{tsx,jsx}' })],
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
# dev
|
||||||
|
|
||||||
|
## 0.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [2f9d373]
|
||||||
|
- babel-preset-inula-next@0.0.2
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<title>Dlight.JS</title>
|
||||||
|
<link rel="stylesheet" href="/src/App.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main"></div>
|
||||||
|
<script type="module" src="/src/App.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
.ok {
|
||||||
|
color: var(--color-ok);
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
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('main', MyComp);
|
|
@ -0,0 +1,143 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
import {
|
||||||
|
Children,
|
||||||
|
Content,
|
||||||
|
Main,
|
||||||
|
Model,
|
||||||
|
Prop,
|
||||||
|
View,
|
||||||
|
Watch,
|
||||||
|
button,
|
||||||
|
div,
|
||||||
|
input,
|
||||||
|
insertChildren,
|
||||||
|
use,
|
||||||
|
render,
|
||||||
|
} from '@openinula/next';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
function Button({ children, onClick }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
color: 'white',
|
||||||
|
backgroundColor: 'green',
|
||||||
|
border: 'none',
|
||||||
|
padding: '5px 10px',
|
||||||
|
marginRight: '10px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ArrayModification() {
|
||||||
|
const arr = [];
|
||||||
|
willMount(() => {});
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h1>ArrayModification</h1>
|
||||||
|
{arr.join(',')}
|
||||||
|
<button onClick={() => arr.push(arr.length)}>Add item</button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Counter() {
|
||||||
|
let count = 0;
|
||||||
|
const doubleCount = count * 2; // 当count变化时,doubleCount自动更新
|
||||||
|
|
||||||
|
// 当count变化时,watch会自动执行
|
||||||
|
watch(() => {
|
||||||
|
uploadToServer(count);
|
||||||
|
console.log(`count has changed: ${count}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只有在init的时候执行一次
|
||||||
|
console.log(`Counter willMount with count ${count}`);
|
||||||
|
// 在elements被挂载到DOM之后执行
|
||||||
|
didMount(() => {
|
||||||
|
console.log(`Counter didMount with count ${count}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
count: {count}, double is: {doubleCount}
|
||||||
|
<button onClick={() => (count ++)}>Add</button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Counter() {
|
||||||
|
let count = 0;
|
||||||
|
const doubleCount = count * 2; // 当count变化时,doubleCount自动更新
|
||||||
|
|
||||||
|
uploadToServer(count); // 当count变化时,uploadToServer会自动执行
|
||||||
|
console.log(`count has changed: ${count}`); // 当count变化时,console.log会自动执行
|
||||||
|
|
||||||
|
// 只有在init的时候执行一次
|
||||||
|
willMount(() => {
|
||||||
|
console.log(`Counter willMount with count ${count}`);
|
||||||
|
});
|
||||||
|
// 在elements被挂载到DOM之后执行
|
||||||
|
didMount(() => {
|
||||||
|
console.log(`Counter didMount with count ${count}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
count: {count}, double is: {doubleCount}
|
||||||
|
<button onClick={() => (count ++)}>Add</button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function MyComp() {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
{
|
||||||
|
console.log(count);
|
||||||
|
const i = count * 2;
|
||||||
|
console.log(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(count);
|
||||||
|
const i = count * 2;
|
||||||
|
console.log(i);
|
||||||
|
|
||||||
|
const XX = () => {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="123">Hello dlight fn comp</h1>
|
||||||
|
<section>
|
||||||
|
count: {count}, double is: {db}
|
||||||
|
<button onClick={() => (count += 1)}>Add</button>
|
||||||
|
</section>
|
||||||
|
<Button onClick={() => alert(count)}>Alter count</Button>
|
||||||
|
<ConditionalRendering count={count} />
|
||||||
|
<ArrayModification />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConditionalRendering({ count }) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h1>Condition</h1>
|
||||||
|
<if cond={count > 1}>{count} is bigger than is 1</if>
|
||||||
|
<else-if cond={count === 1}>{count} is equal to 1</else-if>
|
||||||
|
<else>{count} is smaller than 1</else>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render('main', MyComp);
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve",
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"experimentalDecorators": true
|
||||||
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"esm": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import inula from 'vite-plugin-inula-next';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 4320,
|
||||||
|
},
|
||||||
|
base: '',
|
||||||
|
optimizeDeps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
plugins: [inula({ files: '**/*.{ts,js,tsx,jsx}' })],
|
||||||
|
});
|
90
package.json
90
package.json
|
@ -9,13 +9,11 @@
|
||||||
"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"
|
||||||
},
|
},
|
||||||
|
@ -25,48 +23,46 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.23.7",
|
"@babel/core": "7.16.7",
|
||||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
|
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
|
||||||
"@babel/plugin-proposal-optional-chaining": "7.21.0",
|
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||||
"@babel/plugin-proposal-private-methods": "7.18.6",
|
"@babel/plugin-proposal-private-methods": "7.16.7",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "7.16.7",
|
||||||
"@babel/plugin-syntax-jsx": "7.23.3",
|
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||||
"@babel/plugin-transform-arrow-functions": "7.23.3",
|
"@babel/plugin-transform-arrow-functions": "7.16.7",
|
||||||
"@babel/plugin-transform-block-scoped-functions": "7.23.3",
|
"@babel/plugin-transform-block-scoped-functions": "7.16.7",
|
||||||
"@babel/plugin-transform-block-scoping": "7.23.4",
|
"@babel/plugin-transform-block-scoping": "7.16.7",
|
||||||
"@babel/plugin-transform-classes": "7.23.8",
|
"@babel/plugin-transform-classes": "7.16.7",
|
||||||
"@babel/plugin-transform-computed-properties": "7.23.3",
|
"@babel/plugin-transform-computed-properties": "7.16.7",
|
||||||
"@babel/plugin-transform-destructuring": "7.23.3",
|
"@babel/plugin-transform-destructuring": "7.16.7",
|
||||||
"@babel/plugin-transform-for-of": "7.23.6",
|
"@babel/plugin-transform-for-of": "7.16.7",
|
||||||
"@babel/plugin-transform-literals": "7.23.3",
|
"@babel/plugin-transform-literals": "7.16.7",
|
||||||
"@babel/plugin-transform-object-assign": "7.23.3",
|
"@babel/plugin-transform-object-assign": "7.16.7",
|
||||||
"@babel/plugin-transform-object-super": "7.23.3",
|
"@babel/plugin-transform-object-super": "7.16.7",
|
||||||
"@babel/plugin-transform-parameters": "7.23.3",
|
"@babel/plugin-transform-parameters": "7.16.7",
|
||||||
"@babel/plugin-transform-react-jsx": "7.23.4",
|
"@babel/plugin-transform-react-jsx": "7.16.7",
|
||||||
"@babel/plugin-transform-react-jsx-source": "^7.23.3",
|
"@babel/plugin-transform-react-jsx-source": "^7.16.7",
|
||||||
"@babel/plugin-transform-runtime": "7.23.7",
|
"@babel/plugin-transform-runtime": "7.16.7",
|
||||||
"@babel/plugin-transform-shorthand-properties": "7.23.3",
|
"@babel/plugin-transform-shorthand-properties": "7.16.7",
|
||||||
"@babel/plugin-transform-spread": "7.23.3",
|
"@babel/plugin-transform-spread": "7.16.7",
|
||||||
"@babel/plugin-transform-template-literals": "7.23.3",
|
"@babel/plugin-transform-template-literals": "7.16.7",
|
||||||
"@babel/preset-env": "7.23.8",
|
"@babel/preset-env": "7.16.7",
|
||||||
"@babel/preset-typescript": "7.23.3",
|
"@babel/preset-typescript": "7.16.7",
|
||||||
"@babel/runtime": "7.23.8",
|
"@babel/runtime": "7.16.7",
|
||||||
"@commitlint/cli": "^17.8.1",
|
"@commitlint/cli": "^18.4.4",
|
||||||
"@commitlint/config-conventional": "^17.8.1",
|
"@commitlint/config-conventional": "^18.4.4",
|
||||||
"@rollup/plugin-babel": "^6.0.4",
|
"@rollup/plugin-babel": "^5.3.1",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||||
"@rollup/plugin-replace": "^4.0.0",
|
"@rollup/plugin-replace": "^4.0.0",
|
||||||
"@types/jest": "^29.5.11",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/node": "^17.0.18",
|
"@types/node": "^17.0.18",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
"@typescript-eslint/eslint-plugin": "4.8.0",
|
||||||
"@typescript-eslint/parser": "6.18.1",
|
"@typescript-eslint/parser": "4.8.0",
|
||||||
"@babel/parser": "^7.24.7",
|
"babel-jest": "^27.5.1",
|
||||||
"magic-string": "^0.30.10",
|
|
||||||
"babel-jest": "^29.7.0",
|
|
||||||
"ejs": "^3.1.8",
|
"ejs": "^3.1.8",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "7.13.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",
|
||||||
|
@ -77,17 +73,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.79.1",
|
"rollup": "^2.75.5",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {} }) => {
|
||||||
let currentPath;
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
let currentPath;
|
||||||
if (name) {
|
if (name) {
|
||||||
mkdirp.sync(name);
|
mkdirp.sync(name);
|
||||||
currentPath = path.join(cwd, name);
|
currentPath = path.join(cwd, name);
|
||||||
|
|
|
@ -54,7 +54,7 @@ inula-cli的推荐目录结构如下:
|
||||||
│ └── inula-cli
|
│ └── inula-cli
|
||||||
│ ├── lib
|
│ ├── lib
|
||||||
├── mock // mock目录
|
├── mock // mock目录
|
||||||
│ └── mock.ts
|
│ └── transform.ts
|
||||||
├── src // 项目源码目录
|
├── src // 项目源码目录
|
||||||
│ ├── pages
|
│ ├── pages
|
||||||
│ │ ├── index.less
|
│ │ ├── index.less
|
||||||
|
@ -178,10 +178,10 @@ inula-cli的所有功能都围绕插件展开,插件可以很方便地让用
|
||||||
|
|
||||||
inula-cli支持用户集成已发布在npm仓库的插件,用户可以按需安装并运行这些插件。
|
inula-cli支持用户集成已发布在npm仓库的插件,用户可以按需安装并运行这些插件。
|
||||||
|
|
||||||
安装可以通过npm安装,这里以插件@inula/add为例:
|
安装可以通过npm安装,这里以插件@openinula/add为例:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm i --save-dev @inula/add
|
npm i --save-dev @openinula/add
|
||||||
```
|
```
|
||||||
|
|
||||||
如果需要运行插件,需要在配置文件中配置对应的插件路径
|
如果需要运行插件,需要在配置文件中配置对应的插件路径
|
||||||
|
@ -191,7 +191,7 @@ npm i --save-dev @inula/add
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...
|
...
|
||||||
plugins:["@inula/add"]
|
plugins:["@openinula/add"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -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:
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
|
@ -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 nextFilePath = path.join(filePath, file);
|
const nectFilePath = path.join(filePath, file);
|
||||||
const states = fs.lstatSync(nextFilePath);
|
const states = fs.lstatSync(nectFilePath);
|
||||||
if (states.isDirectory()) {
|
if (states.isDirectory()) {
|
||||||
deleteFolder(nextFilePath);
|
deleteFolder(nectFilePath);
|
||||||
} else {
|
} else {
|
||||||
fs.unlinkSync(nextFilePath);
|
fs.unlinkSync(nectFilePath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
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(f => deleteFolder(f));
|
folders.forEach(folder => deleteFolder(folder));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTypeConfig() {
|
function builderTypeConfig() {
|
||||||
return {
|
return {
|
||||||
input: './build/@types/index.d.ts',
|
input: './build/@types/index.d.ts',
|
||||||
output: {
|
output: {
|
||||||
|
@ -47,4 +47,4 @@ function buildTypeConfig() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default [buildTypeConfig()];
|
export default [builderTypeConfig()];
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'openinula';
|
import Inula, { 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,29 +32,23 @@ 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">
|
|
||||||
{/*<Example4 locale={locale} messages={message} />*/}
|
|
||||||
<Example5 />
|
|
||||||
</div>
|
|
||||||
<div className="button">
|
|
||||||
<button onClick={handleChange}>切换语言</button>
|
|
||||||
</div>
|
|
||||||
</IntlProvider>
|
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<Example4 locale={locale} messages={message} />
|
<Example4 locale={locale} messages={message} />
|
||||||
</div>
|
<Example5 />
|
||||||
<div className="container">
|
|
||||||
<Example6 locale={{ locale }} messages={message} />
|
<Example6 locale={{ locale }} messages={message} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div className="button">
|
||||||
|
<button onClick={handleChange}>切换语言</button>
|
||||||
|
</div>
|
||||||
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +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 from 'openinula';
|
||||||
import { FormattedMessage } from '../../index';
|
import { FormattedMessage } from '../../index';
|
||||||
|
|
||||||
const Example2 = () => {
|
const Example2 = () => {
|
||||||
|
@ -21,9 +22,6 @@ 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
* 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 => {
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
* 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 => {
|
||||||
|
|
|
@ -13,16 +13,23 @@
|
||||||
* 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';
|
||||||
|
|
||||||
const Example5 = ({ intl }) => {
|
class Example5 extends Component<any, any, any> {
|
||||||
// 使用intl.formatMessage来获取国际化消息
|
public constructor(props: any, context) {
|
||||||
console.log(intl + '------------intl-------------');
|
super(props, context);
|
||||||
return (
|
}
|
||||||
<div className="card">
|
|
||||||
<h2>injectIntl方式测试Demo</h2>
|
render() {
|
||||||
<pre>{intl.formatMessage({ id: 'text4' })}</pre>
|
const { intl } = this.props as any;
|
||||||
</div>
|
return (
|
||||||
);
|
<div className="card">
|
||||||
};
|
<h2>injectIntl方式测试Demo</h2>
|
||||||
|
<pre>{intl.formatMessage({ id: 'text4' })}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default injectIntl(Example5);
|
export default injectIntl(Example5);
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
* 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';
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ const Example6 = (props: any) => {
|
||||||
const { locale, messages } = props;
|
const { locale, messages } = props;
|
||||||
|
|
||||||
const cache = createIntlCache();
|
const cache = createIntlCache();
|
||||||
const i18n = createIntl({ locale: locale, messages: messages }, cache);
|
let i18n = createIntl({ locale: locale, messages: messages }, cache);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RawIntlProvider value={i18n}>
|
<RawIntlProvider value={i18n}>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
import { useIntl } from '../../index';
|
import { useIntl } from '../../index';
|
||||||
|
|
||||||
const Example6Child = () => {
|
const Example6Child = (props: any) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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 from 'openinula';
|
import * as Inula from 'openinula';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
|
|
@ -19,5 +19,4 @@ 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}!',
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,5 +18,4 @@ export default {
|
||||||
text2: '欢迎使用国际化组件!',
|
text2: '欢迎使用国际化组件!',
|
||||||
text3: '欢迎使用国际化组件!',
|
text3: '欢迎使用国际化组件!',
|
||||||
text4: '欢迎使用国际化组件!',
|
text4: '欢迎使用国际化组件!',
|
||||||
text5: '渲染一个组件 {testComponent1} {testComponent2}!',
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 { MessageDescriptor } from './src/types/interfaces';
|
import { InjectedIntl, MessageDescriptor } from './src/types/interfaces';
|
||||||
// 函数API
|
// 函数API
|
||||||
export {
|
export {
|
||||||
I18n,
|
I18n,
|
||||||
|
@ -36,7 +36,7 @@ export {
|
||||||
// 组件
|
// 组件
|
||||||
export {
|
export {
|
||||||
FormattedMessage,
|
FormattedMessage,
|
||||||
I18nContext as IntlContext,
|
I18nContext,
|
||||||
I18nProvider as IntlProvider,
|
I18nProvider as IntlProvider,
|
||||||
injectIntl as injectIntl,
|
injectIntl as injectIntl,
|
||||||
InjectProvider as RawIntlProvider,
|
InjectProvider as RawIntlProvider,
|
||||||
|
@ -64,3 +64,7 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default {
|
module.exports = {
|
||||||
coverageDirectory: 'coverage',
|
coverageDirectory: 'coverage',
|
||||||
resetModules: true,
|
resetModules: true,
|
||||||
preset: 'ts-jest/presets/js-with-ts',
|
preset: 'ts-jest/presets/js-with-ts',
|
||||||
|
@ -30,10 +30,8 @@ export default {
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
tsconfig: 'tsconfig.json',
|
tsconfig: 'tsconfig.json',
|
||||||
diagnostics: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
testPathIgnorePatterns: ['\\\\node_modules\\\\'],
|
|
||||||
|
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
};
|
};
|
||||||
|
|
|
@ -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": "module",
|
"type": "commonjs",
|
||||||
"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 --no-cache --config jest.config.js",
|
"test": "jest --config jest.config.js",
|
||||||
"test-c": "jest --coverage"
|
"test-c": "jest --coverage"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -17,7 +17,8 @@
|
||||||
"url": ""
|
"url": ""
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"/build"
|
"build",
|
||||||
|
"README.md"
|
||||||
],
|
],
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -26,23 +27,35 @@
|
||||||
"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",
|
||||||
"react": "18.2.0",
|
"prettier": "^2.8.7",
|
||||||
"react-dom": "18.2.0",
|
"rollup": "^2.0.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-visualizer": "^5.10.0",
|
"rollup-plugin-terser": "^5.3.0",
|
||||||
"ts-node": "10.9.1",
|
|
||||||
"tslib": "^2.6.1",
|
"tslib": "^2.6.1",
|
||||||
"webpack": "^5.72.1",
|
"ts-jest": "29.0.3",
|
||||||
|
"ts-node": "10.9.1",
|
||||||
|
"typescript": "4.9.3",
|
||||||
|
"webpack": "^5.81.0",
|
||||||
"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
|
@ -19,7 +19,6 @@ 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);
|
||||||
|
@ -30,55 +29,34 @@ const output = path.join(__dirname, '/build');
|
||||||
|
|
||||||
const extensions = ['.js', '.ts', '.tsx'];
|
const extensions = ['.js', '.ts', '.tsx'];
|
||||||
|
|
||||||
const BuildConfig = mode => {
|
export default {
|
||||||
const prod = mode.startsWith('prod');
|
input: entry,
|
||||||
const outputList = [
|
output: [
|
||||||
{
|
{
|
||||||
file: path.join(output, `cjs/intl.${prod ? 'min.' : ''}js`),
|
file: path.resolve(output, 'intl.umd.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',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
if (!prod) {
|
file: path.resolve(output, 'intl.esm-browser.js'),
|
||||||
outputList.push({
|
|
||||||
file: path.join(output, 'esm/intl.js'),
|
|
||||||
sourcemap: 'true',
|
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
});
|
},
|
||||||
}
|
],
|
||||||
return {
|
plugins: [
|
||||||
input: entry,
|
nodeResolve({
|
||||||
output: outputList,
|
extensions,
|
||||||
plugins: [
|
modulesOnly: true,
|
||||||
nodeResolve({
|
}),
|
||||||
extensions,
|
babel({
|
||||||
modulesOnly: true,
|
exclude: 'node_modules/**',
|
||||||
}),
|
configFile: path.join(__dirname, '/babel.config.js'),
|
||||||
babel({
|
extensions,
|
||||||
exclude: 'node_modules/**',
|
}),
|
||||||
configFile: path.join(__dirname, '/.babelrc'),
|
typescript({
|
||||||
extensions,
|
tsconfig: 'tsconfig.json',
|
||||||
babelHelpers: 'runtime',
|
include: ['./**/*.ts', './**/*.tsx'],
|
||||||
}),
|
}),
|
||||||
typescript({
|
terser(),
|
||||||
tsconfig: 'tsconfig.json',
|
],
|
||||||
include: ['./**/*.ts', './**/*.tsx'],
|
external: ['openinula', 'react', 'react-dom'],
|
||||||
}),
|
|
||||||
terser(),
|
|
||||||
],
|
|
||||||
external: ['openinula', 'react', 'react-dom'],
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
export default [BuildConfig('dev'), BuildConfig('prod')];
|
|
||||||
|
|
|
@ -18,13 +18,8 @@
|
||||||
* \\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: RegExp = /\\(?:u\{[a-fA-F0-9]+}|x[a-fA-F0-9]{2}|[nrtf'"])/g;
|
export const UNICODE_REG = /\\(?: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,
|
||||||
|
@ -81,22 +76,3 @@ 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',
|
|
||||||
];
|
|
||||||
|
|
|
@ -18,48 +18,30 @@ 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 {
|
import { Locale, Locales, Messages, AllLocaleConfig, AllMessages, LocaleConfig, Error, Events } from '../types/types';
|
||||||
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;
|
||||||
public readonly onError?: Error;
|
private readonly allMessages: AllMessages;
|
||||||
|
public readonly error?: Error;
|
||||||
public readonly cache?: I18nCache;
|
public readonly cache?: I18nCache;
|
||||||
|
|
||||||
constructor(props: I18nProps) {
|
constructor(props: I18nProps) {
|
||||||
super();
|
super();
|
||||||
this.defaultLocale = 'en';
|
this.locale = 'en';
|
||||||
this.locale = this.defaultLocale;
|
|
||||||
this.locales = this.locale || '';
|
this.locales = this.locale || '';
|
||||||
this.allMessages = {};
|
this.allMessages = {};
|
||||||
this._localeConfig = {};
|
this._localeConfig = {};
|
||||||
this.onError = props.onError;
|
this.error = props.error;
|
||||||
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);
|
||||||
|
@ -111,11 +93,6 @@ 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) {
|
||||||
|
@ -141,21 +118,9 @@ export class I18n extends EventDispatcher<Events> {
|
||||||
formatMessage(
|
formatMessage(
|
||||||
id: MessageDescriptor | string,
|
id: MessageDescriptor | string,
|
||||||
values: Record<string, unknown> | undefined = {},
|
values: Record<string, unknown> | undefined = {},
|
||||||
{ messages, context, formatOptions }: MessageOptions = {}
|
{ message, context, formatOptions }: MessageOptions = {}
|
||||||
) {
|
) {
|
||||||
// 在多次渲染时,保证存储component不丢失
|
return getFormatMessage(this, id, values, { message, context, formatOptions });
|
||||||
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 {
|
||||||
|
|
|
@ -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 { Children, Fragment } from 'openinula';
|
import Inula, { 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,17 +22,28 @@ import useI18n from '../hook/useI18n';
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function FormattedMessage(props: FormattedMessageProps) {
|
function FormattedMessage(props: FormattedMessageProps) {
|
||||||
const { formatMessage } = useI18n();
|
const { i18n } = useI18n();
|
||||||
const { id, values, messages, formatOptions, context, tagName: TagName = Fragment, children, comment }: any = props;
|
const {
|
||||||
|
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
const formattedMessage = formatMessage(id, values, formatMessageOptions);
|
let formattedMessage = i18n.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];
|
||||||
|
|
|
@ -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 { useRef, useState, useEffect, useMemo } from 'openinula';
|
import Inula, { 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 { AllMessages, I18nProviderProps, Messages } from '../../types/types';
|
import { I18nProviderProps } from '../../types/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用于为应用程序提供国际化的格式化功能,管理程序中的语言文本信息和本地化资源信息
|
* 用于为应用程序提供国际化的格式化功能,管理程序中的语言文本信息和本地化资源信息
|
||||||
|
@ -23,31 +23,28 @@ import { AllMessages, I18nProviderProps, Messages } from '../../types/types';
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const I18nProvider = (props: I18nProviderProps) => {
|
const I18nProvider = (props: I18nProviderProps) => {
|
||||||
const { locale, messages, children, i18n } = props;
|
const { locale, messages, children } = props;
|
||||||
|
|
||||||
const i18nInstance =
|
const i18n = useMemo(() => {
|
||||||
i18n ||
|
return createI18nInstance({
|
||||||
useMemo(() => {
|
locale: locale,
|
||||||
return createI18nInstance({
|
messages: messages,
|
||||||
locale: locale,
|
});
|
||||||
messages: messages,
|
}, [locale, messages]);
|
||||||
});
|
|
||||||
}, [locale, messages]);
|
|
||||||
|
|
||||||
// 使用useRef保存上次的locale值
|
// 使用useRef保存上次的locale值
|
||||||
const localeRef = useRef<string | undefined>(i18nInstance.locale);
|
const localeRef = useRef<string | undefined>(i18n.locale);
|
||||||
const localeMessage = useRef<string | Messages | AllMessages>(i18nInstance.messages);
|
|
||||||
const [context, setContext] = useState<I18n>(i18nInstance);
|
const [context, setContext] = useState<I18n>(i18n);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleChange = () => {
|
const handleChange = () => {
|
||||||
if (localeRef.current !== i18nInstance.locale || localeMessage.current !== i18nInstance.messages) {
|
if (localeRef.current !== i18n.locale) {
|
||||||
localeRef.current = i18nInstance.locale;
|
localeRef.current = i18n.locale;
|
||||||
localeMessage.current = i18nInstance.messages;
|
setContext(i18n);
|
||||||
setContext(i18nInstance);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const removeListener = i18nInstance.on('change', handleChange);
|
let removeListener = i18n.on('change', handleChange);
|
||||||
|
|
||||||
// 手动触发一次 handleChange,以确保 context 的正确性
|
// 手动触发一次 handleChange,以确保 context 的正确性
|
||||||
handleChange();
|
handleChange();
|
||||||
|
@ -56,7 +53,7 @@ const I18nProvider = (props: I18nProviderProps) => {
|
||||||
return () => {
|
return () => {
|
||||||
removeListener();
|
removeListener();
|
||||||
};
|
};
|
||||||
}, [i18nInstance]);
|
}, [i18n]);
|
||||||
|
|
||||||
// 提供一个Provider组件
|
// 提供一个Provider组件
|
||||||
return <InjectProvider value={context}>{children}</InjectProvider>;
|
return <InjectProvider value={context}>{children}</InjectProvider>;
|
||||||
|
|
|
@ -31,16 +31,13 @@ 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,
|
||||||
|
|
|
@ -13,29 +13,20 @@
|
||||||
* 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 { createI18nInstance } from './I18n';
|
import I18n, { 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): IntlType => {
|
export const createI18n = (config: configProps, cache?: I18nCache): I18n => {
|
||||||
const { locale, defaultLocale, messages } = config;
|
const { locale, defaultLocale, messages } = config;
|
||||||
const i18n = createI18nInstance({
|
return createI18nInstance({
|
||||||
locale: locale || defaultLocale || 'en',
|
locale: locale || defaultLocale || 'zh',
|
||||||
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;
|
||||||
|
|
|
@ -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 { useContext, useMemo } from 'openinula';
|
import Inula, { useContext } 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,22 +23,15 @@ import { IntlType } from '../../types/types';
|
||||||
* 使用 useI18n 钩子函数可以更方便地在函数组件中进行国际化操作
|
* 使用 useI18n 钩子函数可以更方便地在函数组件中进行国际化操作
|
||||||
*/
|
*/
|
||||||
function useI18n(): IntlType {
|
function useI18n(): IntlType {
|
||||||
const i18n = useContext<I18n>(I18nContext);
|
const i18nContext = useContext<I18n>(I18nContext);
|
||||||
utils.isVariantI18n(i18n);
|
utils.isVariantI18n(i18nContext);
|
||||||
return useMemo(() => {
|
const i18n = i18nContext;
|
||||||
return {
|
return {
|
||||||
i18n: i18n,
|
i18n: i18n,
|
||||||
locale: i18n.locale,
|
formatMessage: i18n.formatMessage.bind(i18n),
|
||||||
messages: i18n.messages,
|
formatNumber: i18n.formatNumber.bind(i18n),
|
||||||
defaultLocale: i18n.defaultLocale,
|
formatDate: i18n.formatDate.bind(i18n),
|
||||||
timeZone: i18n.timeZone,
|
};
|
||||||
onError: i18n.onError,
|
|
||||||
formatMessage: i18n.formatMessage.bind(i18n),
|
|
||||||
formatNumber: i18n.formatNumber.bind(i18n),
|
|
||||||
formatDate: i18n.formatDate.bind(i18n),
|
|
||||||
$t: i18n.formatMessage.bind(i18n),
|
|
||||||
};
|
|
||||||
}, [i18n]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useI18n;
|
export default useI18n;
|
||||||
|
|
|
@ -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 creatI18nCache from './cache/cache';
|
import { createIntlCache } from '../../index';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取翻译结果
|
* 获取翻译结果
|
||||||
|
@ -28,18 +28,12 @@ class Translation {
|
||||||
private readonly localeConfig: Record<string, any>;
|
private readonly localeConfig: Record<string, any>;
|
||||||
private readonly cache: I18nCache;
|
private readonly cache: I18nCache;
|
||||||
|
|
||||||
constructor(
|
constructor(compiledMessage, locale, locales, localeConfig, cache?) {
|
||||||
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 ?? creatI18nCache();
|
this.cache = cache ?? createIntlCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,7 +53,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: any;
|
let message;
|
||||||
if (typeof formatter === 'function') {
|
if (typeof formatter === 'function') {
|
||||||
message = formatter(textFormatter); // 递归调用
|
message = formatter(textFormatter); // 递归调用
|
||||||
} else {
|
} else {
|
||||||
|
@ -74,7 +68,8 @@ 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进行格式化处理
|
||||||
return this.formatMessage(this.compiledMessage, textFormatter); // 返回要格式化的结果
|
const result = this.formatMessage(this.compiledMessage, textFormatter);
|
||||||
|
return result; // 返回要格式化的结果
|
||||||
}
|
}
|
||||||
|
|
||||||
formatMessage(compiledMessage: CompiledMessage, textFormatter: (...args: any[]) => any) {
|
formatMessage(compiledMessage: CompiledMessage, textFormatter: (...args: any[]) => any) {
|
||||||
|
|
|
@ -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 creatI18nCache from '../cache/cache';
|
import { createIntlCache } from '../../../index';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 复数格式化
|
* 复数格式化
|
||||||
|
@ -29,12 +29,12 @@ class PluralFormatter {
|
||||||
private readonly message: any;
|
private readonly message: any;
|
||||||
private readonly cache: I18nCache;
|
private readonly cache: I18nCache;
|
||||||
|
|
||||||
constructor(locale: Locale, locales: Locales, value: any, message: any, cache?:I18nCache) {
|
constructor(locale, locales, value, message, cache?) {
|
||||||
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 ?? creatI18nCache();
|
this.cache = cache ?? createIntlCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将 message中的“#”替换为指定数字value,并返回新的字符串或者字符串数组
|
// 将 message中的“#”替换为指定数字value,并返回新的字符串或者字符串数组
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import utils from '../../utils/utils';
|
import utils from '../../utils/utils';
|
||||||
import {Locale, SelectPool} from '../../types/types';
|
import { Locale } 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: Locale, cache: I18nCache) {
|
constructor(locale, cache) {
|
||||||
this.locale = locale;
|
this.locale = locale;
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRule(value: SelectPool, rules: any) {
|
getRule(value, rules) {
|
||||||
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);
|
||||||
|
|
|
@ -19,23 +19,25 @@ 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,
|
locale: Locale | Locales,
|
||||||
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: string | number) => {
|
const getStyleOption = formatOption => {
|
||||||
if (typeof formatOption === 'string') {
|
if (typeof formatOption === 'string') {
|
||||||
return formatOptions[formatOption] || { option: formatOption };
|
return formatOptions[formatOption] || { option: formatOption };
|
||||||
} else {
|
} else {
|
||||||
|
@ -56,14 +58,14 @@ const generateFormatters = (
|
||||||
return pluralFormatter.replaceSymbol.bind(pluralFormatter);
|
return pluralFormatter.replaceSymbol.bind(pluralFormatter);
|
||||||
},
|
},
|
||||||
|
|
||||||
selectordinal: (value: number, { offset = 0, ...rules }) => {
|
selectordinal: (value: number, { offset = 0, ...rules }, useMemorize?) => {
|
||||||
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, cache);
|
const pluralFormatter = new PluralFormatter(locale, locales, value - offset, message, useMemorize);
|
||||||
return pluralFormatter.replaceSymbol.bind(pluralFormatter);
|
return pluralFormatter.replaceSymbol.bind(pluralFormatter);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 选择规则,如果规则对象中包含与该值相对应的属性,则返回该属性的值;否则,返回 "other" 属性的值。
|
// 选择规则,如果规则对象中包含与该值相对应的属性,则返回该属性的值;否则,返回 "other" 属性的值。
|
||||||
select: (value: SelectPool, formatRules: any) => {
|
select: (value: SelectPool, formatRules) => {
|
||||||
const selectFormatter = new SelectFormatter(locale, cache);
|
const selectFormatter = new SelectFormatter(locale, cache);
|
||||||
return selectFormatter.getRule(value, formatRules);
|
return selectFormatter.getRule(value, formatRules);
|
||||||
},
|
},
|
||||||
|
@ -73,16 +75,17 @@ 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: any) => {
|
dateTimeFormat: (value: DatePool, formatOption) => {
|
||||||
return new DateTimeFormatter(locales, getStyleOption(formatOption), cache).dateTimeFormat(value, formatOption);
|
return new DateTimeFormatter(locales, getStyleOption(formatOption), cache).dateTimeFormat(value, formatOption);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -19,21 +19,19 @@ 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 { messages, context } = options;
|
let { message, 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;
|
||||||
messages = id.messages || id.defaultMessage;
|
message = id.message || id.defaultMessage;
|
||||||
context = id.context;
|
context = id.context;
|
||||||
id = id.id;
|
id = id.id;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +42,7 @@ export function getFormatMessage(
|
||||||
const messageUnavailable = isMissingContextMessage || isMissingMessage;
|
const messageUnavailable = isMissingContextMessage || isMissingMessage;
|
||||||
|
|
||||||
// 对错误消息进行处理
|
// 对错误消息进行处理
|
||||||
const messageError = i18n.onError;
|
const messageError = i18n.error;
|
||||||
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);
|
||||||
|
@ -55,17 +53,14 @@ export function getFormatMessage(
|
||||||
|
|
||||||
let compliedMessage: CompiledMessage;
|
let compliedMessage: CompiledMessage;
|
||||||
if (context) {
|
if (context) {
|
||||||
compliedMessage = i18n.messages[context][id] || messages || id;
|
compliedMessage = i18n.messages[context][id] || message || id;
|
||||||
} else {
|
} else {
|
||||||
compliedMessage = i18n.messages[id] || messages || id;
|
compliedMessage = i18n.messages[id] || message || id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对解析的message进行parse解析,并输出解析后的Token
|
// 对解析的messages进行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);
|
||||||
const formatResult = translation.translate(values, formatOptions);
|
return translation.translate(values, formatOptions);
|
||||||
|
|
||||||
// 如果存在inula元素,则返回包含格式化的Inula元素的数组
|
|
||||||
return formatElements(formatResult, components);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,9 @@
|
||||||
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 unionReg: Record<string, any>;
|
readonly states: Record<string, any>;
|
||||||
private buffer = '';
|
private buffer = '';
|
||||||
private stack: string[] = [];
|
private stack: string[] = [];
|
||||||
private index = 0;
|
private index = 0;
|
||||||
|
@ -31,23 +28,19 @@ 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: any;
|
private regexp;
|
||||||
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.unionReg = unionReg;
|
this.states = 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;
|
||||||
|
@ -64,7 +57,7 @@ class Lexer<T> implements LexerInterface<T> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.state = state;
|
this.state = state;
|
||||||
const info = this.unionReg[state];
|
const info = this.states[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;
|
||||||
|
@ -80,7 +73,7 @@ class Lexer<T> implements LexerInterface<T> {
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getGroup(match: Record<string, object>) {
|
private getGroup(match: Record<string, any>) {
|
||||||
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) {
|
||||||
|
@ -94,9 +87,7 @@ 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;
|
||||||
|
|
||||||
|
@ -121,6 +112,7 @@ 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);
|
||||||
|
@ -139,9 +131,9 @@ class Lexer<T> implements LexerInterface<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取Token
|
* 獲取Token
|
||||||
* @param group 解析模板后获得的属性值
|
* @param group 解析模板后獲得的屬性值
|
||||||
* @param text 文本属性的信息
|
* @param text 文本屬性的信息
|
||||||
* @param offset 偏移量
|
* @param offset 偏移量
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
@ -195,7 +187,7 @@ class Lexer<T> implements LexerInterface<T> {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增加迭代器,允许逐个访问集合中的元素方法
|
// 增加迭代器
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator]() {
|
||||||
return {
|
return {
|
||||||
next: (): IteratorResult<T> => {
|
next: (): IteratorResult<T> => {
|
||||||
|
@ -206,15 +198,9 @@ class Lexer<T> implements LexerInterface<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据正则表达式,获取匹配到message的值
|
|
||||||
* 索引为 0 的元素是完整的匹配结果。
|
|
||||||
* 索引为 1、2、3 等的元素是正则表达式中指定的捕获组的匹配结果。
|
|
||||||
*/
|
|
||||||
const getMatch = ruleUtils.checkSticky()
|
const getMatch = ruleUtils.checkSticky()
|
||||||
? // 正则表达式具有 sticky 标志
|
? // 正则表达式具有 sticky 标志
|
||||||
(regexp: any, buffer: string) => regexp.exec(buffer)
|
(regexp, buffer) => regexp.exec(buffer)
|
||||||
: // 正则表达式具有 global 标志,匹配的字符串长度为 0,则表示匹配失败
|
: // 正则表达式具有 global 标志,匹配的字符串长度为 0,则表示匹配失败
|
||||||
(regexp: any, buffer: string) => (regexp.exec(buffer)[0].length === 0 ? null : regexp.exec(buffer));
|
(regexp, buffer) => (regexp.exec(buffer)[0].length === 0 ? null : regexp.exec(buffer));
|
||||||
|
|
||||||
export default Lexer;
|
export default Lexer;
|
||||||
|
|
|
@ -17,44 +17,37 @@ const body: Record<string, any> = {
|
||||||
doubleapos: { match: "''", value: () => "'" },
|
doubleapos: { match: "''", value: () => "'" },
|
||||||
quoted: {
|
quoted: {
|
||||||
lineBreaks: true,
|
lineBreaks: true,
|
||||||
match: /'[{}#](?:[^]*?[^'])?'(?!')/u, // 用以匹配单引号、花括号{}以及井号# 如'Hello' 、{name}、{}#
|
match: /'[{}#](?:[^]*?[^'])?'(?!')/u,
|
||||||
value: (src: string) => src.slice(1, -1).replace(/''/g, "'"),
|
value: src => 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: string) => src.substring(1).trim(),
|
value: src => src.substring(1).trim(),
|
||||||
},
|
},
|
||||||
octothorpe: '#',
|
octothorpe: '#',
|
||||||
end: { match: '}', pop: 1 },
|
end: { match: '}', pop: 1 },
|
||||||
content: {
|
content: { lineBreaks: true, match: /[^][^{}#']*/u },
|
||||||
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, // 匹配内容包含 plural、select 或 selectordinal
|
match: /,\s*(?:plural|select|selectordinal)\s*,\s*/u,
|
||||||
next: 'select', // 继续解析下一个参数
|
next: 'select',
|
||||||
value: (src: string) => src.split(',')[1].trim(), // 提取第二个参数,并处理收尾空格
|
value: src => 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: string) => src.split(',')[1].trim(), // 参数字符串去除逗号并去除首尾空格
|
value: src => 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: string) => src.substring(1).trim(),
|
value: src => src.substring(1).trim(),
|
||||||
},
|
},
|
||||||
end: { match: '}', pop: 1 },
|
end: { match: '}', pop: 1 },
|
||||||
};
|
};
|
||||||
|
@ -62,17 +55,14 @@ 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, // 匹配message中是否包含偏移量offest信息
|
match: /\s*offset\s*:\s*\d+\s*/u,
|
||||||
value: (src: string) => src.split(':')[1].trim(),
|
value: src => 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: string) => src.substring(0, src.indexOf('{')).trim(),
|
value: src => src.substring(0, src.indexOf('{')).trim(),
|
||||||
},
|
},
|
||||||
end: { match: /\s*\}/u, pop: 1 },
|
end: { match: /\s*\}/u, pop: 1 },
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,13 +17,12 @@ 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, object> {
|
function parseRules(rules: Record<string, any>, hasStates: boolean): Record<string, any> {
|
||||||
let errorRule: Record<string, object> | null = null;
|
let errorRule: Record<string, any> | 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;
|
||||||
|
@ -59,7 +58,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));
|
||||||
|
@ -82,11 +81,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 ? STICKY_FLAG : GLOBAL_FLAG;
|
let flags = ruleUtils.checkSticky() && !fallbackRule ? 'ym' : 'gm';
|
||||||
const suffix = ruleUtils.checkSticky() || fallbackRule ? '' : VERTICAL_LINE;
|
const suffix = ruleUtils.checkSticky() || fallbackRule ? '' : '|';
|
||||||
|
|
||||||
if (unicodeFlag === true) {
|
if (unicodeFlag === true) {
|
||||||
flags += UNICODE_FLAG;
|
flags += 'u';
|
||||||
}
|
}
|
||||||
const combined = new RegExp(ruleUtils.getRegUnion(parts) + suffix, flags);
|
const combined = new RegExp(ruleUtils.getRegUnion(parts) + suffix, flags);
|
||||||
|
|
||||||
|
@ -98,18 +97,18 @@ function parseRules(rules: Record<string, any>, hasStates: boolean): Record<stri
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkStateGroup(group: Record<string, any>, name: string, mappingRules: Record<string, object>) {
|
export function checkStateGroup(group: Record<string, any>, name: string, map: Record<string, any>) {
|
||||||
const state = group && (group.push || group.next);
|
const state = group && (group.push || group.next);
|
||||||
if (state && !mappingRules[state]) {
|
if (state && !map[state]) {
|
||||||
throw new Error('The state is missing.');
|
throw new Error('The state is missing.');
|
||||||
}
|
}
|
||||||
if (group && group.pop && +group.pop !== STATE_GROUP_START_INDEX) {
|
if (group && group.pop && +group.pop !== 1) {
|
||||||
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, object>, startState?: string): Lexer<RawToken> {
|
function parseMappingRule(mappingRule: Record<string, any>, startState?: string): Lexer<RawToken> {
|
||||||
const keys = Object.getOwnPropertyNames(mappingRule);
|
const keys = Object.getOwnPropertyNames(mappingRule);
|
||||||
|
|
||||||
if (!startState) {
|
if (!startState) {
|
||||||
|
@ -134,7 +133,7 @@ function parseMappingRule(mappingRule: Record<string, object>, startState?: stri
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const splice = [j, STATE_GROUP_START_INDEX];
|
const splice = [j, 1];
|
||||||
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];
|
||||||
|
@ -175,30 +174,17 @@ function parseMappingRule(mappingRule: Record<string, object>, startState?: stri
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 将规则注入到词法解析器
|
|
||||||
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 {
|
||||||
|
@ -206,13 +192,7 @@ function handleErrorRule(options: Record<string, object>, errorRule: Record<stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
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)) {
|
||||||
|
@ -221,16 +201,14 @@ function checkUnicode(match: Record<string, any>, unicodeFlag: boolean | null, o
|
||||||
|
|
||||||
if (unicodeFlag === null) {
|
if (unicodeFlag === null) {
|
||||||
unicodeFlag = obj.unicode;
|
unicodeFlag = obj.unicode;
|
||||||
} else {
|
} else if (unicodeFlag !== obj.unicode && options.fallback === false) {
|
||||||
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: Record<string, any>) {
|
function checkStateOptions(hasStates: boolean, options) {
|
||||||
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!');
|
||||||
}
|
}
|
||||||
|
@ -239,11 +217,6 @@ function checkStateOptions(hasStates: boolean, options: Record<string, any>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否存在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) {
|
||||||
|
@ -253,7 +226,7 @@ function isExistsFallback(rules: Record<string, any>, enableFast: boolean) {
|
||||||
return enableFast;
|
return enableFast;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOptionsErrorOrFallback(options: Record<string, object>, errorRule: Record<string, object> | null) {
|
function isOptionsErrorOrFallback(options, errorRule: Record<string, any> | null) {
|
||||||
if (options.error || options.fallback) {
|
if (options.error || options.fallback) {
|
||||||
// 只能设置一个 errorRule
|
// 只能设置一个 errorRule
|
||||||
if (errorRule) {
|
if (errorRule) {
|
||||||
|
|
|
@ -14,13 +14,23 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { lexer } from './parseMappingRule';
|
import { lexer } from './parseMappingRule';
|
||||||
import { RawToken } from '../types/types';
|
import { RawToken, Token } 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;
|
||||||
|
@ -29,7 +39,7 @@ class Parser {
|
||||||
lexer.reset(message);
|
lexer.reset(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSelectKeyValid(type: Select['type'], value: string) {
|
isSelectKeyValid(token: RawToken, 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.');
|
||||||
|
@ -65,7 +75,7 @@ class Parser {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'case': {
|
case 'case': {
|
||||||
this.isSelectKeyValid(type, token.value);
|
this.isSelectKeyValid(token, 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),
|
||||||
|
@ -84,11 +94,6 @@ 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();
|
||||||
|
@ -148,12 +153,7 @@ 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,23 +201,6 @@ 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);
|
||||||
|
|
|
@ -13,25 +13,15 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { AllLocaleConfig, AllMessages, Locale, Locales, Error, DatePool, SelectPool, RawToken } from './types';
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +34,7 @@ export interface MessageDescriptor extends MessageOptions {
|
||||||
|
|
||||||
export interface MessageOptions {
|
export interface MessageOptions {
|
||||||
comment?: string;
|
comment?: string;
|
||||||
messages?: string;
|
message?: string;
|
||||||
context?: string;
|
context?: string;
|
||||||
formatOptions?: FormatOptions;
|
formatOptions?: FormatOptions;
|
||||||
}
|
}
|
||||||
|
@ -58,26 +48,15 @@ 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 type I18nProps = RichText & {
|
export interface I18nProps {
|
||||||
locale?: Locale;
|
locale?: Locale;
|
||||||
locales?: Locales;
|
locales?: Locales;
|
||||||
messages?: AllMessages;
|
messages?: AllMessages;
|
||||||
defaultLocale?: string;
|
|
||||||
timeZone?: string;
|
|
||||||
localeConfig?: AllLocaleConfig;
|
localeConfig?: AllLocaleConfig;
|
||||||
cache?: I18nCache;
|
cache?: I18nCache;
|
||||||
onError?: Error;
|
error?: Error;
|
||||||
};
|
}
|
||||||
|
|
||||||
// 消息格式化选项类型
|
// 消息格式化选项类型
|
||||||
export interface FormatOptions {
|
export interface FormatOptions {
|
||||||
|
@ -95,13 +74,16 @@ export interface I18nContextProps {
|
||||||
i18n?: I18n;
|
i18n?: I18n;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type configProps = I18nProps & {
|
export interface configProps {
|
||||||
|
locale?: Locale;
|
||||||
|
messages?: AllMessages;
|
||||||
|
defaultLocale?: string;
|
||||||
RenderOnLocaleChange?: boolean;
|
RenderOnLocaleChange?: boolean;
|
||||||
children?: any;
|
children?: any;
|
||||||
onWarn?: Error;
|
onWarn?: Error;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface IntlMessageFormat {
|
export interface IntlMessageFormat extends configProps, MessageOptions {
|
||||||
plural: (
|
plural: (
|
||||||
value: number,
|
value: number,
|
||||||
{
|
{
|
||||||
|
@ -222,6 +204,7 @@ export interface InjectedIntl {
|
||||||
formatMessage(
|
formatMessage(
|
||||||
messageDescriptor: MessageDescriptor,
|
messageDescriptor: MessageDescriptor,
|
||||||
values?: Record<string, unknown>,
|
values?: Record<string, unknown>,
|
||||||
options?: MessageOptions
|
options?: MessageOptions,
|
||||||
): string | any[];
|
useMemorize?: boolean
|
||||||
|
): string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,17 +23,16 @@ import {
|
||||||
I18nContextProps,
|
I18nContextProps,
|
||||||
configProps,
|
configProps,
|
||||||
InjectedIntl,
|
InjectedIntl,
|
||||||
InulaPortal,
|
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { InulaElement } from 'openinula';
|
import I18n from '../core/I18n';
|
||||||
|
|
||||||
export type Error = string | ((message: any, id: any, context: any) => string);
|
export type Error = string | ((message, id, context) => string);
|
||||||
|
|
||||||
export type Locale = string;
|
export type Locale = string;
|
||||||
|
|
||||||
export type Locales = Locale | Locale[];
|
export type Locales = Locale | Locale[];
|
||||||
|
|
||||||
export type LocaleConfig = { plurals?: (...args: any[]) => any };
|
export type LocaleConfig = { plurals?: (...arg: any) => any };
|
||||||
|
|
||||||
export type AllLocaleConfig = Record<Locale, LocaleConfig>;
|
export type AllLocaleConfig = Record<Locale, LocaleConfig>;
|
||||||
|
|
||||||
|
@ -60,7 +59,7 @@ export type Token = Content | PlainArg | FunctionArg | Select | Octothorpe;
|
||||||
|
|
||||||
export type DatePool = Date | string;
|
export type DatePool = Date | string;
|
||||||
|
|
||||||
export type SelectPool = string | number;
|
export type SelectPool = string | Record<string, unknown>;
|
||||||
|
|
||||||
export type RawToken = {
|
export type RawToken = {
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -75,23 +74,13 @@ export type RawToken = {
|
||||||
|
|
||||||
export type I18nProviderProps = I18nContextProps & configProps;
|
export type I18nProviderProps = I18nContextProps & configProps;
|
||||||
|
|
||||||
export type IntlType = I18nContextProps & {
|
export type IntlType = {
|
||||||
defaultLocale?: string | undefined;
|
i18n: I18n;
|
||||||
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 type InjectedIntlProps = {
|
export interface InjectedIntlProps {
|
||||||
intl: InjectedIntl;
|
intl: InjectedIntl;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type InulaNode = InulaElement | string | number | Iterable<InulaNode> | InulaPortal | boolean | null | undefined;
|
|
||||||
|
|
|
@ -1,107 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
// value:This 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++}`;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -18,7 +18,6 @@ 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();
|
||||||
|
@ -29,25 +28,24 @@ 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(str: string): string {
|
function transferReg(s: string): string {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算正则表达式中捕获组的数量,用以匹配()
|
// 计算正则表达式中捕获组的数量
|
||||||
function getRegGroups(str: string): number {
|
function getRegGroups(s: string): number {
|
||||||
const regExp = new RegExp('|' + str);
|
const re = new RegExp('|' + s);
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
return regExp.exec('')?.length! - 1;
|
return re.exec('')?.length! - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建一个捕获组的正则表达式模式
|
// 创建一个捕获组的正则表达式模式
|
||||||
function getRegCapture(str: string): string {
|
function getRegCapture(s: string): string {
|
||||||
return '(' + str + ')';
|
return '(' + s + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将正则表达式合并为一个联合的正则表达式模式
|
// 将正则表达式合并为一个联合的正则表达式模式
|
||||||
|
@ -55,7 +53,7 @@ function getRegUnion(regexps: string[]): string {
|
||||||
if (!regexps.length) {
|
if (!regexps.length) {
|
||||||
return '(?!)';
|
return '(?!)';
|
||||||
}
|
}
|
||||||
const source = regexps.map(str => '(?:' + str + ')').join('|');
|
const source = regexps.map(s => '(?:' + s + ')').join('|');
|
||||||
return '(?:' + source + ')';
|
return '(?:' + source + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +143,7 @@ function getRulesByArray(array: any[]) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRuleOptions(type: any, obj: any) {
|
function getRuleOptions(type, obj) {
|
||||||
// 如果 obj 不是一个对象,则将其转换为包含 'match' 属性的对象
|
// 如果 obj 不是一个对象,则将其转换为包含 'match' 属性的对象
|
||||||
if (!checkObject(obj)) {
|
if (!checkObject(obj)) {
|
||||||
obj = { match: obj };
|
obj = { match: obj };
|
||||||
|
@ -184,23 +182,23 @@ function getRuleOptions(type: any, obj: any) {
|
||||||
} else {
|
} else {
|
||||||
options.match = [];
|
options.match = [];
|
||||||
}
|
}
|
||||||
options.match.sort((str1: string, str2: string) => {
|
options.match.sort((a, b) => {
|
||||||
// 根据规则的类型进行排序,确保正则表达式排在最前面,长度较长的规则排在前面
|
// 根据规则的类型进行排序,确保正则表达式排在最前面,长度较长的规则排在前面
|
||||||
if (checkRegExp(str1) && checkRegExp(str2)) {
|
if (checkRegExp(a) && checkRegExp(b)) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (checkRegExp(str2)) {
|
} else if (checkRegExp(b)) {
|
||||||
return -1;
|
return -1;
|
||||||
} else if (checkRegExp(str1)) {
|
} else if (checkRegExp(a)) {
|
||||||
return +1;
|
return +1;
|
||||||
} else {
|
} else {
|
||||||
return str2.length - str1.length;
|
return b.length - a.length;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRules(spec: any) {
|
function getRules(spec) {
|
||||||
return Array.isArray(spec) ? getRulesByArray(spec) : getRulesByObject(spec);
|
return Array.isArray(spec) ? getRulesByArray(spec) : getRulesByObject(spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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},cause by ${e}`);
|
console.error(`Message cannot be parse due to syntax errors: ${message}`);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* 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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,270 +0,0 @@
|
||||||
/*
|
|
||||||
* 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>3月2日</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');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -43,7 +43,7 @@ describe('<FormattedMessage>', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(getByTestId('id').textContent).toEqual(i18n.formatMessage('hello', {}, {}));
|
expect(getByTestId('id')).toHaveTextContent(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').textContent).toEqual(i18n.formatMessage('id', { name: 'fred' }, {}));
|
expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('id', { name: 'fred' }, {}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -42,6 +42,7 @@ 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')");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ describe('InjectIntl', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const { getByTestId } = mountWithProvider(<Injected {...props} />);
|
const { getByTestId } = mountWithProvider(<Injected {...props} />);
|
||||||
expect(JSON.stringify(getByTestId('test'))).toEqual(
|
expect(getByTestId('test')).toHaveTextContent(
|
||||||
'{"_events":{},"locale":"en","locales":["en"],"allMessages":{},"_localeData":{}}'
|
'{"_events":{},"locale":"en","locales":["en"],"allMessages":{},"_localeData":{}}'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,20 +29,6 @@ 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({
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 I18nCache object', () => {
|
it('should create an empty IntlCache object', () => {
|
||||||
const intlCache = creatI18nCache();
|
const intlCache = creatI18nCache();
|
||||||
|
|
||||||
expect(intlCache).toEqual({
|
expect(intlCache).toEqual({
|
||||||
|
|
|
@ -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 cache is effective', () => {
|
it('should not memoize formatter instances when memoize is false', () => {
|
||||||
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 cache is effective', () => {
|
it('should format using memorized formatter when useMemorize is true', () => {
|
||||||
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);
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe('getFormatMessage', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
onError: 'missingMessage',
|
error: '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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
import copyStaticProps from '../../src/utils/copyStaticProps';
|
import copyStaticProps from '../../src/utils/copyStaticProps';
|
||||||
|
|
||||||
describe('copyStaticProps', () => {
|
describe('copyStaticProps', () => {
|
||||||
it('should hoist static properties from sourceComponent to targetComponent', () => {
|
test('should hoist static properties from sourceComponent to targetComponent', () => {
|
||||||
class SourceComponent {
|
class SourceComponent {
|
||||||
static staticProp = 'sourceProp';
|
static staticProp = 'sourceProp';
|
||||||
}
|
}
|
||||||
|
@ -23,10 +23,11 @@ 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');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hoist static properties from inherited components', () => {
|
test('should hoist static properties from inherited components', () => {
|
||||||
class SourceComponent {
|
class SourceComponent {
|
||||||
static staticProp = 'sourceProp';
|
static staticProp = 'sourceProp';
|
||||||
}
|
}
|
||||||
|
@ -36,10 +37,11 @@ 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');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not hoist properties if descriptor is not valid', () => {
|
test('should not hoist properties if descriptor is not valid', () => {
|
||||||
class SourceComponent {
|
class SourceComponent {
|
||||||
get staticProp() {
|
get staticProp() {
|
||||||
return 'sourceProp';
|
return 'sourceProp';
|
||||||
|
@ -49,10 +51,11 @@ describe('copyStaticProps', () => {
|
||||||
class TargetComponent {}
|
class TargetComponent {}
|
||||||
|
|
||||||
copyStaticProps(TargetComponent, SourceComponent);
|
copyStaticProps(TargetComponent, SourceComponent);
|
||||||
|
|
||||||
expect((TargetComponent as any).staticProp).toBeUndefined();
|
expect((TargetComponent as any).staticProp).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not hoist properties if descriptor is not valid', () => {
|
test('should not hoist properties if descriptor is not valid', () => {
|
||||||
class SourceComponent {
|
class SourceComponent {
|
||||||
static get staticProp() {
|
static get staticProp() {
|
||||||
return 'sourceProp';
|
return 'sourceProp';
|
||||||
|
@ -62,10 +65,11 @@ 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');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('copyStaticProps should not copy static properties that already exist in target or source component', () => {
|
test('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);
|
||||||
|
|
|
@ -31,7 +31,6 @@
|
||||||
"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": [
|
||||||
|
@ -55,8 +54,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./index.ts",
|
"./index.ts"
|
||||||
|
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|
|
@ -12,18 +12,16 @@
|
||||||
* 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';
|
|
||||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
const { resolve } = require('path');
|
||||||
import { fileURLToPath } from 'url';
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||||
const entryPath = './example/index.tsx';
|
const entryPath = './example/index.tsx';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
module.exports = {
|
||||||
const __dirname = path.dirname(__filename);
|
entry: resolve(__dirname, entryPath),
|
||||||
export default {
|
|
||||||
entry: path.join(__dirname, entryPath),
|
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, './build'),
|
path: resolve(__dirname, './build'),
|
||||||
filename: 'main.js',
|
filename: 'main.js',
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
|
@ -52,7 +50,7 @@ export default {
|
||||||
mode: isDevelopment ? 'development' : 'production',
|
mode: isDevelopment ? 'development' : 'production',
|
||||||
plugins: [
|
plugins: [
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: path.join(__dirname, './example/index.html'),
|
template: resolve(__dirname, './example/index.html'),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export const Store = {};
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"esm": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
# DLight Main Package
|
||||||
|
See the website's documentations for usage.
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "@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/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": "^3.1.3",
|
||||||
|
"@openinula/store": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsup": "^6.5.0"
|
||||||
|
},
|
||||||
|
"tsup": {
|
||||||
|
"entry": [
|
||||||
|
"src/index.js"
|
||||||
|
],
|
||||||
|
"format": [
|
||||||
|
"cjs",
|
||||||
|
"esm"
|
||||||
|
],
|
||||||
|
"clean": true,
|
||||||
|
"minify": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,367 @@
|
||||||
|
import { DLNode, DLNodeType } from './DLNode';
|
||||||
|
import { forwardHTMLProp } from './HTMLNode';
|
||||||
|
import { DLStore, cached } from './store';
|
||||||
|
|
||||||
|
export class CompNode extends DLNode {
|
||||||
|
/**
|
||||||
|
* @brief Constructor, Comp type
|
||||||
|
* @internal
|
||||||
|
* * key - private property key
|
||||||
|
* * $$key - dependency number, e.g. 0b1, 0b10, 0b100
|
||||||
|
* * $s$key - set of properties that depend on this property
|
||||||
|
* * $p$key - exist if this property is a prop
|
||||||
|
* * $e$key - exist if this property is an env
|
||||||
|
* * $en$key - exist if this property is an env, and it's the innermost env that contains this env
|
||||||
|
* * $w$key - exist if this property is a watcher
|
||||||
|
* * $f$key - a function that returns the value of this property, called when the property's dependencies change
|
||||||
|
* * _$children - children nodes of type PropView
|
||||||
|
* * _$contentKey - the key key of the content prop
|
||||||
|
* * _$forwardProps - exist if this node is forwarding props
|
||||||
|
* * _$forwardPropsId - the keys of the props that this node is forwarding, collected in _$setForwardProp
|
||||||
|
* * _$forwardPropsSet - contain all the nodes that are forwarding props to this node, collected with _$addForwardProps
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super(DLNodeType.Comp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Init function, called explicitly in the subclass's constructor
|
||||||
|
* @param props - Object containing properties
|
||||||
|
* @param content - Content to be used
|
||||||
|
* @param children - Child nodes
|
||||||
|
* @param forwardPropsScope - Scope for forwarding properties
|
||||||
|
*/
|
||||||
|
_$init(props, content, children, forwardPropsScope) {
|
||||||
|
this._$notInitd = true;
|
||||||
|
|
||||||
|
// ---- Forward props first to allow internal props to override forwarded props
|
||||||
|
if (forwardPropsScope) forwardPropsScope._$addForwardProps(this);
|
||||||
|
if (content) this._$setContent(() => content[0], content[1]);
|
||||||
|
if (props)
|
||||||
|
props.forEach(([key, value, deps]) => {
|
||||||
|
if (key === 'props') return this._$setProps(() => value, deps);
|
||||||
|
this._$setProp(key, () => value, deps);
|
||||||
|
});
|
||||||
|
if (children) this._$children = children;
|
||||||
|
|
||||||
|
// ---- Add envs
|
||||||
|
DLStore.global.DLEnvStore &&
|
||||||
|
Object.entries(DLStore.global.DLEnvStore.envs).forEach(([key, [value, envNode]]) => {
|
||||||
|
if (key === '_$catchable') {
|
||||||
|
this._$catchable = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(`$e$${key}` in this)) return;
|
||||||
|
envNode.addNode(this);
|
||||||
|
this._$initEnv(key, value, envNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
const willCall = () => {
|
||||||
|
this._$callUpdatesBeforeInit();
|
||||||
|
this.didMount && DLNode.addDidMount(this, this.didMount.bind(this));
|
||||||
|
this.willUnmount && DLNode.addWillUnmount(this, this.willUnmount.bind(this));
|
||||||
|
DLNode.addDidUnmount(this, this._$setUnmounted.bind(this));
|
||||||
|
this.didUnmount && DLNode.addDidUnmount(this, this.didUnmount.bind(this));
|
||||||
|
this.willMount?.();
|
||||||
|
this._$nodes = this.Body?.() ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._$catchable) {
|
||||||
|
this._$catchable(willCall)();
|
||||||
|
if (this._$update) this._$update = this._$catchable(this._$update.bind(this));
|
||||||
|
this._$updateDerived = this._$catchable(this._$updateDerived.bind(this));
|
||||||
|
delete this._$catchable;
|
||||||
|
} else {
|
||||||
|
willCall();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_$setUnmounted() {
|
||||||
|
this._$unmounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Call updates manually before the node is mounted
|
||||||
|
*/
|
||||||
|
_$callUpdatesBeforeInit() {
|
||||||
|
const protoProps = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
|
||||||
|
const ownProps = Object.getOwnPropertyNames(this);
|
||||||
|
const allProps = [...protoProps, ...ownProps];
|
||||||
|
allProps.forEach(key => {
|
||||||
|
// ---- Run watcher
|
||||||
|
if (key.startsWith('$w$')) return this[key.slice(3)]();
|
||||||
|
// ---- Run model update
|
||||||
|
if (key.startsWith('$md$')) {
|
||||||
|
const realKey = key.slice(4);
|
||||||
|
this[realKey] = this[realKey]();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ---- Run derived value
|
||||||
|
if (key.startsWith('$f$')) {
|
||||||
|
const realKey = key.slice(3);
|
||||||
|
this[realKey] = this[key];
|
||||||
|
this._$updateDerived(realKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
delete this._$notInitd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set all the props to forward
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @param deps
|
||||||
|
*/
|
||||||
|
_$setPropToForward(key, value, deps) {
|
||||||
|
this._$forwardPropsSet.forEach(node => {
|
||||||
|
const isContent = key === '_$content';
|
||||||
|
if (node._$dlNodeType === DLNodeType.Comp) {
|
||||||
|
if (isContent) node._$setContent(() => value, deps);
|
||||||
|
else node._$setProp(key, () => value, deps);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node instanceof HTMLElement) {
|
||||||
|
if (isContent) key = 'textContent';
|
||||||
|
forwardHTMLProp(node, key, () => value, deps);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Define forward props
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
_$setForwardProp(key, valueFunc, deps) {
|
||||||
|
const notInitd = '_$notInitd' in this;
|
||||||
|
if (!notInitd && this._$cache(key, deps)) return;
|
||||||
|
const value = valueFunc();
|
||||||
|
if (key === '_$content' && this._$contentKey) {
|
||||||
|
this[this._$contentKey] = value;
|
||||||
|
this._$updateDerived(this._$contentKey);
|
||||||
|
}
|
||||||
|
this[key] = value;
|
||||||
|
this._$updateDerived(key);
|
||||||
|
if (notInitd) this._$forwardPropsId.push(key);
|
||||||
|
else this._$setPropToForward(key, value, deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add a node to the set of nodes that are forwarding props to this node and init these props
|
||||||
|
* @param node
|
||||||
|
*/
|
||||||
|
_$addForwardProps(node) {
|
||||||
|
this._$forwardPropsSet.add(node);
|
||||||
|
this._$forwardPropsId.forEach(key => {
|
||||||
|
this._$setPropToForward(key, this[key], []);
|
||||||
|
});
|
||||||
|
DLNode.addWillUnmount(node, this._$forwardPropsSet.delete.bind(this._$forwardPropsSet, node));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cache the deps and return true if the deps are the same as the previous deps
|
||||||
|
* @param key
|
||||||
|
* @param deps
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
_$cache(key, deps) {
|
||||||
|
if (!deps || !deps.length) return false;
|
||||||
|
const cacheKey = `$cc$${key}`;
|
||||||
|
if (cached(deps, this[cacheKey])) return true;
|
||||||
|
this[cacheKey] = deps;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the content prop, the key is stored in _$contentKey
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
_$setContent(valueFunc, deps) {
|
||||||
|
if ('_$forwardProps' in this) return this._$setForwardProp('_$content', valueFunc, deps);
|
||||||
|
const contentKey = this._$contentKey;
|
||||||
|
if (!contentKey) return;
|
||||||
|
if (this._$cache(contentKey, deps)) return;
|
||||||
|
this[contentKey] = valueFunc();
|
||||||
|
this._$updateDerived(contentKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set a prop directly, if this is a forwarded prop, go and init forwarded props
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @param deps
|
||||||
|
*/
|
||||||
|
_$setProp(key, valueFunc, deps) {
|
||||||
|
if ('_$forwardProps' in this) return this._$setForwardProp(key, valueFunc, deps);
|
||||||
|
if (!(`$p$${key}` in this)) {
|
||||||
|
console.warn(`[${key}] is not a prop in ${this.constructor.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._$cache(key, deps)) return;
|
||||||
|
this[key] = valueFunc();
|
||||||
|
this._$updateDerived(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
_$setProps(valueFunc, deps) {
|
||||||
|
if (this._$cache('props', deps)) return;
|
||||||
|
const props = valueFunc();
|
||||||
|
if (!props) return;
|
||||||
|
Object.entries(props).forEach(([key, value]) => {
|
||||||
|
this._$setProp(key, () => value, []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Init an env, put the corresponding innermost envNode in $en$key
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @param envNode
|
||||||
|
*/
|
||||||
|
_$initEnv(key, value, envNode) {
|
||||||
|
this[key] = value;
|
||||||
|
this[`$en$${key}`] = envNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Update functions
|
||||||
|
/**
|
||||||
|
* @brief Update an env, called in EnvNode._$update
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @param envNode
|
||||||
|
*/
|
||||||
|
_$updateEnv(key, value, envNode) {
|
||||||
|
if (!(`$e$${key}` in this)) return;
|
||||||
|
if (envNode !== this[`$en$${key}`]) return;
|
||||||
|
this[key] = value;
|
||||||
|
this._$updateDerived(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update a prop
|
||||||
|
*/
|
||||||
|
_$ud(exp, key) {
|
||||||
|
this._$updateDerived(key);
|
||||||
|
return exp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update properties that depend on this property
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
_$updateDerived(key) {
|
||||||
|
if ('_$notInitd' in this) return;
|
||||||
|
|
||||||
|
this[`$s$${key}`]?.forEach(k => {
|
||||||
|
if (`$w$${k}` in this) {
|
||||||
|
// ---- Watcher
|
||||||
|
this[k](key);
|
||||||
|
} else if (`$md$${k}` in this) {
|
||||||
|
this[k]._$update();
|
||||||
|
} else {
|
||||||
|
// ---- Regular derived value
|
||||||
|
this[k] = this[`$f$${k}`];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- "trigger-view"
|
||||||
|
this._$updateView(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
_$updateView(key) {
|
||||||
|
if (this._$modelCallee) return this._$updateModelCallee();
|
||||||
|
if (!('_$update' in this)) return;
|
||||||
|
const depNum = this[`$$${key}`];
|
||||||
|
if (!depNum) return;
|
||||||
|
// ---- Collect all depNums that need to be updated
|
||||||
|
if ('_$depNumsToUpdate' in this) {
|
||||||
|
this._$depNumsToUpdate.push(depNum);
|
||||||
|
} else {
|
||||||
|
this._$depNumsToUpdate = [depNum];
|
||||||
|
// ---- Update in the next microtask
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
// ---- Abort if unmounted
|
||||||
|
if (this._$unmounted) return;
|
||||||
|
const depNums = this._$depNumsToUpdate;
|
||||||
|
if (depNums.length > 0) {
|
||||||
|
const depNum = depNums.reduce((acc, cur) => acc | cur, 0);
|
||||||
|
this._$update(depNum);
|
||||||
|
}
|
||||||
|
delete this._$depNumsToUpdate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_$updateModelCallee() {
|
||||||
|
if ('_$depNumsToUpdate' in this) return;
|
||||||
|
this._$depNumsToUpdate = true;
|
||||||
|
// ---- Update in the next microtask
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
// ---- Abort if unmounted
|
||||||
|
if (this._$unmounted) return;
|
||||||
|
this._$modelCallee._$updateDerived(this._$modelKey);
|
||||||
|
delete this._$depNumsToUpdate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @brief Update all props and content of the model
|
||||||
|
*/
|
||||||
|
static _$updateModel(model, propsFunc, contentFunc) {
|
||||||
|
// ---- Suppress update because top level update will be performed
|
||||||
|
// directly by the state variable in the model callee, which will
|
||||||
|
// trigger the update of the model
|
||||||
|
const props = propsFunc() ?? {};
|
||||||
|
const collectedProps = props.s ?? [];
|
||||||
|
props.m?.forEach(([props, deps]) => {
|
||||||
|
Object.entries(props).forEach(([key, value]) => {
|
||||||
|
collectedProps.push([key, value, deps]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
collectedProps.forEach(([key, value, deps]) => {
|
||||||
|
model._$setProp(key, () => value, deps);
|
||||||
|
});
|
||||||
|
const content = contentFunc();
|
||||||
|
if (content) model._$setContent(() => content[0], content[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _$releaseModel() {
|
||||||
|
delete this._$modelCallee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Inject Dlight model in to a property
|
||||||
|
* @param ModelCls
|
||||||
|
* @param props { m: [props, deps], s: [key, value, deps] }
|
||||||
|
* @param content
|
||||||
|
* @param key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
_$injectModel(ModelCls, propsFunc, contentFunc, key) {
|
||||||
|
const props = propsFunc() ?? {};
|
||||||
|
const collectedProps = props.s ?? [];
|
||||||
|
props.m?.forEach(([props, deps]) => {
|
||||||
|
Object.entries(props).forEach(([key, value]) => {
|
||||||
|
collectedProps.push([key, value, deps]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const model = new ModelCls();
|
||||||
|
model._$init(collectedProps, contentFunc(), null, null);
|
||||||
|
model._$modelCallee = this;
|
||||||
|
model._$modelKey = key;
|
||||||
|
model._$update = CompNode._$updateModel.bind(null, model, propsFunc, contentFunc);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- @View -> class Comp extends View
|
||||||
|
export const View = CompNode;
|
||||||
|
export const Model = CompNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Run all update functions given the key
|
||||||
|
* @param dlNode
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
export function update(dlNode, key) {
|
||||||
|
dlNode._$updateDerived(key);
|
||||||
|
}
|
|
@ -0,0 +1,209 @@
|
||||||
|
import { DLStore } from './store';
|
||||||
|
|
||||||
|
export const DLNodeType = {
|
||||||
|
Comp: 0,
|
||||||
|
For: 1,
|
||||||
|
Cond: 2,
|
||||||
|
Env: 3,
|
||||||
|
Exp: 4,
|
||||||
|
Snippet: 5,
|
||||||
|
Try: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DLNode {
|
||||||
|
/**
|
||||||
|
* @brief Node type: HTML, Text, Custom, For, If, Env, Expression
|
||||||
|
*/
|
||||||
|
_$dlNodeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Constructor
|
||||||
|
* @param nodeType
|
||||||
|
*/
|
||||||
|
constructor(nodeType) {
|
||||||
|
this._$dlNodeType = nodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Node element
|
||||||
|
* Either one real element for HTMLNode and TextNode
|
||||||
|
* Or an array of DLNode for CustomNode, ForNode, IfNode, EnvNode, ExpNode
|
||||||
|
*/
|
||||||
|
get _$el() {
|
||||||
|
return DLNode.toEls(this._$nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Loop all child DLNodes to get all the child elements
|
||||||
|
* @param nodes
|
||||||
|
* @returns HTMLElement[]
|
||||||
|
*/
|
||||||
|
static toEls(nodes) {
|
||||||
|
const els = [];
|
||||||
|
this.loopShallowEls(nodes, el => {
|
||||||
|
els.push(el);
|
||||||
|
});
|
||||||
|
return els;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Loop nodes ----
|
||||||
|
/**
|
||||||
|
* @brief Loop all elements shallowly,
|
||||||
|
* i.e., don't loop the child nodes of dom elements and only call runFunc on dom elements
|
||||||
|
* @param nodes
|
||||||
|
* @param runFunc
|
||||||
|
*/
|
||||||
|
static loopShallowEls(nodes, runFunc) {
|
||||||
|
const stack = [...nodes].reverse();
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const node = stack.pop();
|
||||||
|
if (!('_$dlNodeType' in node)) runFunc(node);
|
||||||
|
else node._$nodes && stack.push(...[...node._$nodes].reverse());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add parentEl to all nodes until the first element
|
||||||
|
* @param nodes
|
||||||
|
* @param parentEl
|
||||||
|
*/
|
||||||
|
static addParentEl(nodes, parentEl) {
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if ('_$dlNodeType' in node) {
|
||||||
|
node._$parentEl = parentEl;
|
||||||
|
node._$nodes && DLNode.addParentEl(node._$nodes, parentEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Flow index and add child elements ----
|
||||||
|
/**
|
||||||
|
* @brief Get the total count of dom elements before the stop node
|
||||||
|
* @param nodes
|
||||||
|
* @param stopNode
|
||||||
|
* @returns total count of dom elements
|
||||||
|
*/
|
||||||
|
static getFlowIndexFromNodes(nodes, stopNode) {
|
||||||
|
let index = 0;
|
||||||
|
const stack = [...nodes].reverse();
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const node = stack.pop();
|
||||||
|
if (node === stopNode) break;
|
||||||
|
if ('_$dlNodeType' in node) {
|
||||||
|
node._$nodes && stack.push(...[...node._$nodes].reverse());
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Given an array of nodes, append them to the parentEl
|
||||||
|
* 1. If nextSibling is provided, insert the nodes before the nextSibling
|
||||||
|
* 2. If nextSibling is not provided, append the nodes to the parentEl
|
||||||
|
* @param nodes
|
||||||
|
* @param parentEl
|
||||||
|
* @param nextSibling
|
||||||
|
* @returns Added element count
|
||||||
|
*/
|
||||||
|
static appendNodesWithSibling(nodes, parentEl, nextSibling) {
|
||||||
|
if (nextSibling) return this.insertNodesBefore(nodes, parentEl, nextSibling);
|
||||||
|
return this.appendNodes(nodes, parentEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Given an array of nodes, append them to the parentEl using the index
|
||||||
|
* 1. If the index is the same as the length of the parentEl.childNodes, append the nodes to the parentEl
|
||||||
|
* 2. If the index is not the same as the length of the parentEl.childNodes, insert the nodes before the node at the index
|
||||||
|
* @param nodes
|
||||||
|
* @param parentEl
|
||||||
|
* @param index
|
||||||
|
* @param length
|
||||||
|
* @returns Added element count
|
||||||
|
*/
|
||||||
|
static appendNodesWithIndex(nodes, parentEl, index, length) {
|
||||||
|
length = length ?? parentEl.childNodes.length;
|
||||||
|
if (length !== index) return this.insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]);
|
||||||
|
return this.appendNodes(nodes, parentEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Insert nodes before the nextSibling
|
||||||
|
* @param nodes
|
||||||
|
* @param parentEl
|
||||||
|
* @param nextSibling
|
||||||
|
* @returns Added element count
|
||||||
|
*/
|
||||||
|
static insertNodesBefore(nodes, parentEl, nextSibling) {
|
||||||
|
let count = 0;
|
||||||
|
this.loopShallowEls(nodes, el => {
|
||||||
|
parentEl.insertBefore(el, nextSibling);
|
||||||
|
count++;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Append nodes to the parentEl
|
||||||
|
* @param nodes
|
||||||
|
* @param parentEl
|
||||||
|
* @returns Added element count
|
||||||
|
*/
|
||||||
|
static appendNodes(nodes, parentEl) {
|
||||||
|
let count = 0;
|
||||||
|
this.loopShallowEls(nodes, el => {
|
||||||
|
parentEl.appendChild(el);
|
||||||
|
count++;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Lifecycle ----
|
||||||
|
/**
|
||||||
|
* @brief Add willUnmount function to node
|
||||||
|
* @param node
|
||||||
|
* @param func
|
||||||
|
*/
|
||||||
|
static addWillUnmount(node, func) {
|
||||||
|
const willUnmountStore = DLStore.global.WillUnmountStore;
|
||||||
|
const currentStore = willUnmountStore[willUnmountStore.length - 1];
|
||||||
|
// ---- If the current store is empty, it means this node is not mutable
|
||||||
|
if (!currentStore) return;
|
||||||
|
currentStore.push(func.bind(null, node));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add didUnmount function to node
|
||||||
|
* @param node
|
||||||
|
* @param func
|
||||||
|
*/
|
||||||
|
static addDidUnmount(node, func) {
|
||||||
|
const didUnmountStore = DLStore.global.DidUnmountStore;
|
||||||
|
const currentStore = didUnmountStore[didUnmountStore.length - 1];
|
||||||
|
// ---- If the current store is empty, it means this node is not mutable
|
||||||
|
if (!currentStore) return;
|
||||||
|
currentStore.push(func.bind(null, node));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add didUnmount function to global store
|
||||||
|
* @param func
|
||||||
|
*/
|
||||||
|
static addDidMount(node, func) {
|
||||||
|
if (!DLStore.global.DidMountStore) DLStore.global.DidMountStore = [];
|
||||||
|
DLStore.global.DidMountStore.push(func.bind(null, node));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Run all didMount functions and reset the global store
|
||||||
|
*/
|
||||||
|
static runDidMount() {
|
||||||
|
const didMountStore = DLStore.global.DidMountStore;
|
||||||
|
if (!didMountStore || didMountStore.length === 0) return;
|
||||||
|
for (let i = didMountStore.length - 1; i >= 0; i--) {
|
||||||
|
didMountStore[i]();
|
||||||
|
}
|
||||||
|
DLStore.global.DidMountStore = [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { DLNode, DLNodeType } from './DLNode';
|
||||||
|
import { DLStore, cached } from './store';
|
||||||
|
|
||||||
|
export class EnvStoreClass {
|
||||||
|
constructor() {
|
||||||
|
this.envs = {};
|
||||||
|
this.currentEnvNodes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add a node to the current env and merge envs
|
||||||
|
* @param node - The node to add
|
||||||
|
*/
|
||||||
|
addEnvNode(node) {
|
||||||
|
this.currentEnvNodes.push(node);
|
||||||
|
this.mergeEnvs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Replace the current env with the given nodes and merge envs
|
||||||
|
* @param nodes - The nodes to replace the current environment with
|
||||||
|
*/
|
||||||
|
replaceEnvNodes(nodes) {
|
||||||
|
this.currentEnvNodes = nodes;
|
||||||
|
this.mergeEnvs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove the last node from the current env and merge envs
|
||||||
|
*/
|
||||||
|
removeEnvNode() {
|
||||||
|
this.currentEnvNodes.pop();
|
||||||
|
this.mergeEnvs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Merge all the envs in currentEnvNodes, inner envs override outer envs
|
||||||
|
*/
|
||||||
|
mergeEnvs() {
|
||||||
|
this.envs = {};
|
||||||
|
this.currentEnvNodes.forEach(envNode => {
|
||||||
|
Object.entries(envNode.envs).forEach(([key, value]) => {
|
||||||
|
this.envs[key] = [value, envNode];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EnvNode extends DLNode {
|
||||||
|
constructor(envs, depsArr) {
|
||||||
|
super(DLNodeType.Env);
|
||||||
|
// Declare a global variable to store the environment variables
|
||||||
|
if (!('DLEnvStore' in DLStore.global)) DLStore.global.DLEnvStore = new EnvStoreClass();
|
||||||
|
|
||||||
|
this.envs = envs;
|
||||||
|
this.depsArr = depsArr;
|
||||||
|
this.updateNodes = new Set();
|
||||||
|
|
||||||
|
DLStore.global.DLEnvStore.addEnvNode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
cached(deps, name) {
|
||||||
|
if (!deps || !deps.length) return false;
|
||||||
|
if (cached(deps, this.depsArr[name])) return true;
|
||||||
|
this.depsArr[name] = deps;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update a specific env, and update all the comp nodes that depend on this env
|
||||||
|
* @param name - The name of the environment variable to update
|
||||||
|
* @param value - The new value of the environment variable
|
||||||
|
*/
|
||||||
|
updateEnv(name, valueFunc, deps) {
|
||||||
|
if (this.cached(deps, name)) return;
|
||||||
|
const value = valueFunc();
|
||||||
|
this.envs[name] = value;
|
||||||
|
if (DLStore.global.DLEnvStore.currentEnvNodes.includes(this)) {
|
||||||
|
DLStore.global.DLEnvStore.mergeEnvs();
|
||||||
|
}
|
||||||
|
this.updateNodes.forEach(node => {
|
||||||
|
node._$updateEnv(name, value, this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts
|
||||||
|
* @param node - The node to add
|
||||||
|
*/
|
||||||
|
addNode(node) {
|
||||||
|
this.updateNodes.add(node);
|
||||||
|
DLNode.addWillUnmount(node, this.updateNodes.delete.bind(this.updateNodes, node));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set this._$nodes, and exit the current env
|
||||||
|
* @param nodes - The nodes to set
|
||||||
|
*/
|
||||||
|
initNodes(nodes) {
|
||||||
|
this._$nodes = nodes;
|
||||||
|
DLStore.global.DLEnvStore.removeEnvNode();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
import { DLNode } from './DLNode';
|
||||||
|
import { DLStore, cached } from './store';
|
||||||
|
|
||||||
|
function cache(el, key, deps) {
|
||||||
|
if (deps.length === 0) return false;
|
||||||
|
const cacheKey = `$${key}`;
|
||||||
|
if (cached(deps, el[cacheKey])) return true;
|
||||||
|
el[cacheKey] = deps;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Plainly set style
|
||||||
|
* @param el
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function setStyle(el, value) {
|
||||||
|
Object.entries(value).forEach(([key, value]) => {
|
||||||
|
if (key.startsWith('--')) {
|
||||||
|
el.style.setProperty(key, value);
|
||||||
|
} else {
|
||||||
|
el.style[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Plainly set dataset
|
||||||
|
* @param el
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function setDataset(el, value) {
|
||||||
|
Object.assign(el.dataset, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set HTML property with checking value equality first
|
||||||
|
* @param el
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function setHTMLProp(el, key, valueFunc, deps) {
|
||||||
|
// ---- Comparing deps, same value won't trigger
|
||||||
|
// will lead to a bug if the value is set outside of the DLNode
|
||||||
|
// e.g. setHTMLProp(el, "textContent", "value", [])
|
||||||
|
// => el.textContent = "other"
|
||||||
|
// => setHTMLProp(el, "textContent", "value", [])
|
||||||
|
// The value will be set to "other" instead of "value"
|
||||||
|
if (cache(el, key, deps)) return;
|
||||||
|
el[key] = valueFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Plainly set HTML properties
|
||||||
|
* @param el
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function setHTMLProps(el, value) {
|
||||||
|
Object.entries(value).forEach(([key, v]) => {
|
||||||
|
if (key === 'style') return setStyle(el, v);
|
||||||
|
if (key === 'dataset') return setDataset(el, v);
|
||||||
|
setHTMLProp(el, key, () => v, []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set HTML attribute with checking value equality first
|
||||||
|
* @param el
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function setHTMLAttr(el, key, valueFunc, deps) {
|
||||||
|
if (cache(el, key, deps)) return;
|
||||||
|
el.setAttribute(key, valueFunc());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Plainly set HTML attributes
|
||||||
|
* @param el
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function setHTMLAttrs(el, value) {
|
||||||
|
Object.entries(value).forEach(([key, v]) => {
|
||||||
|
setHTMLAttr(el, key, () => v, []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first
|
||||||
|
* @param el
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function setEvent(el, key, value) {
|
||||||
|
const prevEvent = el[`$on${key}`];
|
||||||
|
if (prevEvent) el.removeEventListener(key, prevEvent);
|
||||||
|
el.addEventListener(key, value);
|
||||||
|
el[`$on${key}`] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventHandler(e) {
|
||||||
|
const key = `$$${e.type}`;
|
||||||
|
for (const node of e.composedPath()) {
|
||||||
|
if (node[key]) node[key](e);
|
||||||
|
if (e.cancelBubble) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delegateEvent(el, key, value) {
|
||||||
|
if (el[`$$${key}`] === value) return;
|
||||||
|
el[`$$${key}`] = value;
|
||||||
|
if (!DLStore.delegatedEvents.has(key)) {
|
||||||
|
DLStore.delegatedEvents.add(key);
|
||||||
|
DLStore.document.addEventListener(key, eventHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @brief Shortcut for document.createElement
|
||||||
|
* @param tag
|
||||||
|
* @returns HTMLElement
|
||||||
|
*/
|
||||||
|
export function createElement(tag) {
|
||||||
|
return DLStore.document.createElement(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children
|
||||||
|
* @param el
|
||||||
|
* @param node
|
||||||
|
* @param position
|
||||||
|
*/
|
||||||
|
export function insertNode(el, node, position) {
|
||||||
|
// ---- Set _$nodes
|
||||||
|
if (!el._$nodes) el._$nodes = Array.from(el.childNodes);
|
||||||
|
el._$nodes.splice(position, 0, node);
|
||||||
|
|
||||||
|
// ---- Insert nodes' elements
|
||||||
|
const flowIdx = DLNode.getFlowIndexFromNodes(el._$nodes, node);
|
||||||
|
DLNode.appendNodesWithIndex([node], el, flowIdx);
|
||||||
|
// ---- Set parentEl
|
||||||
|
DLNode.addParentEl([node], el);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief An inclusive assign prop function that accepts any type of prop
|
||||||
|
* @param el
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function forwardHTMLProp(el, key, valueFunc, deps) {
|
||||||
|
if (key === 'style') return setStyle(el, valueFunc());
|
||||||
|
if (key === 'dataset') return setDataset(el, valueFunc());
|
||||||
|
if (key === 'element') return;
|
||||||
|
if (key === 'prop') return setHTMLProps(el, valueFunc());
|
||||||
|
if (key === 'attr') return setHTMLAttrs(el, valueFunc());
|
||||||
|
if (key === 'innerHTML') return setHTMLProp(el, 'innerHTML', valueFunc, deps);
|
||||||
|
if (key === 'textContent') return setHTMLProp(el, 'textContent', valueFunc, deps);
|
||||||
|
if (key === 'forwardProp') return;
|
||||||
|
if (key.startsWith('on')) {
|
||||||
|
return setEvent(el, key.slice(2).toLowerCase(), valueFunc());
|
||||||
|
}
|
||||||
|
setHTMLAttr(el, key, valueFunc, deps);
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { DLNodeType } from '../DLNode';
|
||||||
|
import { FlatNode } from './FlatNode';
|
||||||
|
|
||||||
|
export class CondNode extends FlatNode {
|
||||||
|
/**
|
||||||
|
* @brief Constructor, If type, accept a function that returns a list of nodes
|
||||||
|
* @param caseFunc
|
||||||
|
*/
|
||||||
|
constructor(depNum, condFunc) {
|
||||||
|
super(DLNodeType.Cond);
|
||||||
|
this.depNum = depNum;
|
||||||
|
this.cond = -1;
|
||||||
|
this.condFunc = condFunc;
|
||||||
|
this.initUnmountStore();
|
||||||
|
this._$nodes = this.condFunc(this);
|
||||||
|
this.setUnmountFuncs();
|
||||||
|
|
||||||
|
// ---- Add to the global UnmountStore
|
||||||
|
CondNode.addWillUnmount(this, this.runWillUnmount.bind(this));
|
||||||
|
CondNode.addDidUnmount(this, this.runDidUnmount.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the nodes in the environment
|
||||||
|
*/
|
||||||
|
updateCond(key) {
|
||||||
|
// ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv
|
||||||
|
// The reason is that if it didn't change, we don't need to unmount or remove the nodes
|
||||||
|
const prevFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
|
||||||
|
const newNodes = this.geneNewNodesInEnv(() => this.condFunc(this));
|
||||||
|
|
||||||
|
// ---- If the new nodes are the same as the old nodes, we only need to update children
|
||||||
|
if (this.didntChange) {
|
||||||
|
[this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
|
||||||
|
this.didntChange = false;
|
||||||
|
this.updateFunc?.(this.depNum, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ---- Remove old nodes
|
||||||
|
const newFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
|
||||||
|
[this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
|
||||||
|
this._$nodes && this._$nodes.length > 0 && this.removeNodes(this._$nodes);
|
||||||
|
[this.willUnmountFuncs, this.didUnmountFuncs] = newFuncs;
|
||||||
|
|
||||||
|
if (newNodes.length === 0) {
|
||||||
|
// ---- No branch has been taken
|
||||||
|
this._$nodes = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ---- Add new nodes
|
||||||
|
const parentEl = this._$parentEl;
|
||||||
|
// ---- Faster append with nextSibling rather than flowIndex
|
||||||
|
const flowIndex = CondNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
||||||
|
|
||||||
|
const nextSibling = parentEl.childNodes[flowIndex];
|
||||||
|
CondNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
||||||
|
CondNode.runDidMount();
|
||||||
|
this._$nodes = newNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The update function of IfNode's childNodes is stored in the first child node
|
||||||
|
* @param changed
|
||||||
|
*/
|
||||||
|
update(changed) {
|
||||||
|
if (!(~this.depNum & changed)) return;
|
||||||
|
this.updateFunc?.(changed);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { DLNodeType } from '../DLNode';
|
||||||
|
import { FlatNode } from './FlatNode';
|
||||||
|
import { DLStore, cached } from '../store';
|
||||||
|
|
||||||
|
export class ExpNode extends FlatNode {
|
||||||
|
/**
|
||||||
|
* @brief Constructor, Exp type, accept a function that returns a list of nodes
|
||||||
|
* @param nodesFunc
|
||||||
|
*/
|
||||||
|
constructor(value, deps) {
|
||||||
|
super(DLNodeType.Exp);
|
||||||
|
this.initUnmountStore();
|
||||||
|
this._$nodes = ExpNode.formatNodes(value);
|
||||||
|
this.setUnmountFuncs();
|
||||||
|
this.deps = this.parseDeps(deps);
|
||||||
|
// ---- Add to the global UnmountStore
|
||||||
|
ExpNode.addWillUnmount(this, this.runWillUnmount.bind(this));
|
||||||
|
ExpNode.addDidUnmount(this, this.runDidUnmount.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
parseDeps(deps) {
|
||||||
|
return deps.map(dep => {
|
||||||
|
// ---- CompNode
|
||||||
|
if (dep?.prototype?._$init) return dep.toString();
|
||||||
|
// ---- SnippetNode
|
||||||
|
if (dep?.propViewFunc) return dep.propViewFunc.toString();
|
||||||
|
return dep;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cache(deps) {
|
||||||
|
if (!deps || !deps.length) return false;
|
||||||
|
deps = this.parseDeps(deps);
|
||||||
|
if (cached(deps, this.deps)) return true;
|
||||||
|
this.deps = deps;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @brief Generate new nodes and replace the old nodes
|
||||||
|
*/
|
||||||
|
update(valueFunc, deps) {
|
||||||
|
if (this.cache(deps)) return;
|
||||||
|
this.removeNodes(this._$nodes);
|
||||||
|
const newNodes = this.geneNewNodesInEnv(() => ExpNode.formatNodes(valueFunc()));
|
||||||
|
if (newNodes.length === 0) {
|
||||||
|
this._$nodes = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Add new nodes
|
||||||
|
const parentEl = this._$parentEl;
|
||||||
|
const flowIndex = ExpNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
||||||
|
const nextSibling = parentEl.childNodes[flowIndex];
|
||||||
|
ExpNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
||||||
|
ExpNode.runDidMount();
|
||||||
|
|
||||||
|
this._$nodes = newNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Format the nodes
|
||||||
|
* @param nodes
|
||||||
|
* @returns New nodes
|
||||||
|
*/
|
||||||
|
static formatNodes(nodes) {
|
||||||
|
if (!Array.isArray(nodes)) nodes = [nodes];
|
||||||
|
return (
|
||||||
|
nodes
|
||||||
|
// ---- Flatten the nodes
|
||||||
|
.flat(1)
|
||||||
|
// ---- Filter out empty nodes
|
||||||
|
.filter(node => node !== undefined && node !== null && typeof node !== 'boolean')
|
||||||
|
.map(node => {
|
||||||
|
// ---- If the node is a string, number or bigint, convert it to a text node
|
||||||
|
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
|
||||||
|
return DLStore.document.createTextNode(`${node}`);
|
||||||
|
}
|
||||||
|
// ---- If the node has PropView, call it to get the view
|
||||||
|
if ('propViewFunc' in node) return node.build();
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
// ---- Flatten the nodes again
|
||||||
|
.flat(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { DLStore } from '../store';
|
||||||
|
import { MutableNode } from './MutableNode';
|
||||||
|
|
||||||
|
export class FlatNode extends MutableNode {
|
||||||
|
willUnmountFuncs = [];
|
||||||
|
didUnmountFuncs = [];
|
||||||
|
|
||||||
|
setUnmountFuncs() {
|
||||||
|
this.willUnmountFuncs = DLStore.global.WillUnmountStore.pop();
|
||||||
|
this.didUnmountFuncs = DLStore.global.DidUnmountStore.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
runWillUnmount() {
|
||||||
|
for (let i = 0; i < this.willUnmountFuncs.length; i++) this.willUnmountFuncs[i]();
|
||||||
|
}
|
||||||
|
|
||||||
|
runDidUnmount() {
|
||||||
|
for (let i = this.didUnmountFuncs.length - 1; i >= 0; i--) this.didUnmountFuncs[i]();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeNodes(nodes) {
|
||||||
|
this.runWillUnmount();
|
||||||
|
super.removeNodes(nodes);
|
||||||
|
this.runDidUnmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
geneNewNodesInEnv(newNodesFunc) {
|
||||||
|
this.initUnmountStore();
|
||||||
|
const nodes = super.geneNewNodesInEnv(newNodesFunc);
|
||||||
|
this.setUnmountFuncs();
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,406 @@
|
||||||
|
import { DLNodeType } from '../DLNode';
|
||||||
|
import { DLStore } from '../store';
|
||||||
|
import { MutableNode } from './MutableNode';
|
||||||
|
|
||||||
|
export class ForNode extends MutableNode {
|
||||||
|
array;
|
||||||
|
nodeFunc;
|
||||||
|
depNum;
|
||||||
|
|
||||||
|
nodesMap = new Map();
|
||||||
|
updateArr = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Getter for nodes
|
||||||
|
*/
|
||||||
|
get _$nodes() {
|
||||||
|
const nodes = [];
|
||||||
|
for (let idx = 0; idx < this.array.length; idx++) {
|
||||||
|
nodes.push(...this.nodesMap.get(this.keys?.[idx] ?? idx));
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Constructor, For type
|
||||||
|
* @param array
|
||||||
|
* @param nodeFunc
|
||||||
|
* @param keys
|
||||||
|
*/
|
||||||
|
constructor(array, depNum, keys, nodeFunc) {
|
||||||
|
super(DLNodeType.For);
|
||||||
|
this.array = [...array];
|
||||||
|
this.keys = keys;
|
||||||
|
this.depNum = depNum;
|
||||||
|
this.addNodeFunc(nodeFunc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief To be called immediately after the constructor
|
||||||
|
* @param nodeFunc
|
||||||
|
*/
|
||||||
|
addNodeFunc(nodeFunc) {
|
||||||
|
this.nodeFunc = nodeFunc;
|
||||||
|
this.array.forEach((item, idx) => {
|
||||||
|
this.initUnmountStore();
|
||||||
|
const key = this.keys?.[idx] ?? idx;
|
||||||
|
const nodes = nodeFunc(item, this.updateArr, idx);
|
||||||
|
this.nodesMap.set(key, nodes);
|
||||||
|
this.setUnmountMap(key);
|
||||||
|
});
|
||||||
|
// ---- For nested ForNode, the whole strategy is just like EnvStore
|
||||||
|
// we use array of function array to create "environment", popping and pushing
|
||||||
|
ForNode.addWillUnmount(this, this.runAllWillUnmount.bind(this));
|
||||||
|
ForNode.addDidUnmount(this, this.runAllDidUnmount.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the view related to one item in the array
|
||||||
|
* @param nodes
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
updateItem(idx, array, changed) {
|
||||||
|
// ---- The update function of ForNode's childNodes is stored in the first child node
|
||||||
|
this.updateArr[idx]?.(changed ?? this.depNum, array[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateItems(changed) {
|
||||||
|
for (let idx = 0; idx < this.array.length; idx++) {
|
||||||
|
this.updateItem(idx, this.array, changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Non-array update function
|
||||||
|
* @param changed
|
||||||
|
*/
|
||||||
|
update(changed) {
|
||||||
|
// ---- e.g. this.depNum -> 1110 changed-> 1010
|
||||||
|
// ~this.depNum & changed -> ~1110 & 1010 -> 0000
|
||||||
|
// no update because depNum contains all the changed
|
||||||
|
// ---- e.g. this.depNum -> 1110 changed-> 1101
|
||||||
|
// ~this.depNum & changed -> ~1110 & 1101 -> 0001
|
||||||
|
// update because depNum doesn't contain all the changed
|
||||||
|
if (!(~this.depNum & changed)) return;
|
||||||
|
this.updateItems(changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Array-related update function
|
||||||
|
* @param newArray
|
||||||
|
* @param newKeys
|
||||||
|
*/
|
||||||
|
updateArray(newArray, newKeys) {
|
||||||
|
if (newKeys) {
|
||||||
|
this.updateWithKey(newArray, newKeys);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateWithOutKey(newArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Shortcut to generate new nodes with idx and key
|
||||||
|
*/
|
||||||
|
getNewNodes(idx, key, array, updateArr) {
|
||||||
|
this.initUnmountStore();
|
||||||
|
const nodes = this.geneNewNodesInEnv(() => this.nodeFunc(array[idx], updateArr ?? this.updateArr, idx));
|
||||||
|
this.setUnmountMap(key);
|
||||||
|
this.nodesMap.set(key, nodes);
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the unmount map by getting the last unmount map from the global store
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
setUnmountMap(key) {
|
||||||
|
const willUnmountMap = DLStore.global.WillUnmountStore.pop();
|
||||||
|
if (willUnmountMap && willUnmountMap.length > 0) {
|
||||||
|
if (!this.willUnmountMap) this.willUnmountMap = new Map();
|
||||||
|
this.willUnmountMap.set(key, willUnmountMap);
|
||||||
|
}
|
||||||
|
const didUnmountMap = DLStore.global.DidUnmountStore.pop();
|
||||||
|
if (didUnmountMap && didUnmountMap.length > 0) {
|
||||||
|
if (!this.didUnmountMap) this.didUnmountMap = new Map();
|
||||||
|
this.didUnmountMap.set(key, didUnmountMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Run all the unmount functions and clear the unmount map
|
||||||
|
*/
|
||||||
|
runAllWillUnmount() {
|
||||||
|
if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
|
||||||
|
this.willUnmountMap.forEach(funcs => {
|
||||||
|
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
|
||||||
|
});
|
||||||
|
this.willUnmountMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Run all the unmount functions and clear the unmount map
|
||||||
|
*/
|
||||||
|
runAllDidUnmount() {
|
||||||
|
if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
|
||||||
|
this.didUnmountMap.forEach(funcs => {
|
||||||
|
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
|
||||||
|
});
|
||||||
|
this.didUnmountMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Run the unmount functions of the given key
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
runWillUnmount(key) {
|
||||||
|
if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
|
||||||
|
const funcs = this.willUnmountMap.get(key);
|
||||||
|
if (!funcs) return;
|
||||||
|
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
|
||||||
|
this.willUnmountMap.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Run the unmount functions of the given key
|
||||||
|
*/
|
||||||
|
runDidUnmount(key) {
|
||||||
|
if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
|
||||||
|
const funcs = this.didUnmountMap.get(key);
|
||||||
|
if (!funcs) return;
|
||||||
|
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
|
||||||
|
this.didUnmountMap.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
|
||||||
|
* @param nodes
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
removeNodes(nodes, key) {
|
||||||
|
this.runWillUnmount(key);
|
||||||
|
super.removeNodes(nodes);
|
||||||
|
this.runDidUnmount(key);
|
||||||
|
this.nodesMap.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the nodes without keys
|
||||||
|
* @param newArray
|
||||||
|
*/
|
||||||
|
updateWithOutKey(newArray) {
|
||||||
|
const preLength = this.array.length;
|
||||||
|
const currLength = newArray.length;
|
||||||
|
|
||||||
|
if (preLength === currLength) {
|
||||||
|
// ---- If the length is the same, we only need to update the nodes
|
||||||
|
for (let idx = 0; idx < this.array.length; idx++) {
|
||||||
|
this.updateItem(idx, newArray);
|
||||||
|
}
|
||||||
|
this.array = [...newArray];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parentEl = this._$parentEl;
|
||||||
|
// ---- If the new array is longer, add new nodes directly
|
||||||
|
if (preLength < currLength) {
|
||||||
|
let flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
||||||
|
// ---- Calling parentEl.childNodes.length is time-consuming,
|
||||||
|
// so we use a length variable to store the length
|
||||||
|
const length = parentEl.childNodes.length;
|
||||||
|
for (let idx = 0; idx < currLength; idx++) {
|
||||||
|
if (idx < preLength) {
|
||||||
|
flowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(idx));
|
||||||
|
this.updateItem(idx, newArray);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const newNodes = this.getNewNodes(idx, idx, newArray);
|
||||||
|
ForNode.appendNodesWithIndex(newNodes, parentEl, flowIndex, length);
|
||||||
|
}
|
||||||
|
ForNode.runDidMount();
|
||||||
|
this.array = [...newArray];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Update the nodes first
|
||||||
|
for (let idx = 0; idx < currLength; idx++) {
|
||||||
|
this.updateItem(idx, newArray);
|
||||||
|
}
|
||||||
|
// ---- If the new array is shorter, remove the extra nodes
|
||||||
|
for (let idx = currLength; idx < preLength; idx++) {
|
||||||
|
const nodes = this.nodesMap.get(idx);
|
||||||
|
this.removeNodes(nodes, idx);
|
||||||
|
}
|
||||||
|
this.updateArr.splice(currLength, preLength - currLength);
|
||||||
|
this.array = [...newArray];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the nodes with keys
|
||||||
|
* @param newArray
|
||||||
|
* @param newKeys
|
||||||
|
*/
|
||||||
|
updateWithKey(newArray, newKeys) {
|
||||||
|
if (newKeys.length !== new Set(newKeys).size) {
|
||||||
|
throw new Error('DLight: Duplicate keys in for loop are not allowed');
|
||||||
|
}
|
||||||
|
const prevKeys = this.keys;
|
||||||
|
this.keys = newKeys;
|
||||||
|
|
||||||
|
if (ForNode.arrayEqual(prevKeys, this.keys)) {
|
||||||
|
// ---- If the keys are the same, we only need to update the nodes
|
||||||
|
for (let idx = 0; idx < newArray.length; idx++) {
|
||||||
|
this.updateItem(idx, newArray);
|
||||||
|
}
|
||||||
|
this.array = [...newArray];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentEl = this._$parentEl;
|
||||||
|
|
||||||
|
// ---- No nodes after, delete all nodes
|
||||||
|
if (this.keys.length === 0) {
|
||||||
|
const parentNodes = parentEl._$nodes ?? [];
|
||||||
|
if (parentNodes.length === 1 && parentNodes[0] === this) {
|
||||||
|
// ---- ForNode is the only node in the parent node
|
||||||
|
// Frequently used in real life scenarios because we tend to always wrap for with a div element,
|
||||||
|
// so we optimize it here
|
||||||
|
this.runAllWillUnmount();
|
||||||
|
parentEl.innerHTML = '';
|
||||||
|
this.runAllDidUnmount();
|
||||||
|
} else {
|
||||||
|
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
|
||||||
|
const prevKey = prevKeys[prevIdx];
|
||||||
|
this.removeNodes(this.nodesMap.get(prevKey), prevKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nodesMap.clear();
|
||||||
|
this.updateArr = [];
|
||||||
|
this.array = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Record how many nodes are before this ForNode with the same parentNode
|
||||||
|
const flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
||||||
|
|
||||||
|
// ---- No nodes before, append all nodes
|
||||||
|
if (prevKeys.length === 0) {
|
||||||
|
const nextSibling = parentEl.childNodes[flowIndex];
|
||||||
|
for (let idx = 0; idx < this.keys.length; idx++) {
|
||||||
|
const newNodes = this.getNewNodes(idx, this.keys[idx], newArray);
|
||||||
|
ForNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
||||||
|
}
|
||||||
|
ForNode.runDidMount();
|
||||||
|
this.array = [...newArray];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shuffleKeys = [];
|
||||||
|
const newUpdateArr = [];
|
||||||
|
|
||||||
|
// ---- 1. Delete the nodes that are no longer in the array
|
||||||
|
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
|
||||||
|
const prevKey = prevKeys[prevIdx];
|
||||||
|
if (this.keys.includes(prevKey)) {
|
||||||
|
shuffleKeys.push(prevKey);
|
||||||
|
newUpdateArr.push(this.updateArr[prevIdx]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.removeNodes(this.nodesMap.get(prevKey), prevKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 2. Add the nodes that are not in the array but in the new array
|
||||||
|
// ---- Calling parentEl.childNodes.length is time-consuming,
|
||||||
|
// so we use a length variable to store the length
|
||||||
|
let length = parentEl.childNodes.length;
|
||||||
|
let newFlowIndex = flowIndex;
|
||||||
|
for (let idx = 0; idx < this.keys.length; idx++) {
|
||||||
|
const key = this.keys[idx];
|
||||||
|
const prevIdx = shuffleKeys.indexOf(key);
|
||||||
|
if (prevIdx !== -1) {
|
||||||
|
// ---- These nodes are already in the parentEl,
|
||||||
|
// and we need to keep track of their flowIndex
|
||||||
|
newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
|
||||||
|
newUpdateArr[prevIdx]?.(this.depNum, newArray[idx]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// ---- Insert updateArr first because in getNewNode the updateFunc will replace this null
|
||||||
|
newUpdateArr.splice(idx, 0, null);
|
||||||
|
const newNodes = this.getNewNodes(idx, key, newArray, newUpdateArr);
|
||||||
|
// ---- Add the new nodes
|
||||||
|
shuffleKeys.splice(idx, 0, key);
|
||||||
|
|
||||||
|
const count = ForNode.appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length);
|
||||||
|
newFlowIndex += count;
|
||||||
|
length += count;
|
||||||
|
}
|
||||||
|
ForNode.runDidMount();
|
||||||
|
|
||||||
|
// ---- After adding and deleting, the only thing left is to reorder the nodes,
|
||||||
|
// but if the keys are the same, we don't need to reorder
|
||||||
|
if (ForNode.arrayEqual(this.keys, shuffleKeys)) {
|
||||||
|
this.array = [...newArray];
|
||||||
|
this.updateArr = newUpdateArr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newFlowIndex = flowIndex;
|
||||||
|
const bufferNodes = new Map();
|
||||||
|
// ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm
|
||||||
|
for (let idx = 0; idx < this.keys.length; idx++) {
|
||||||
|
const key = this.keys[idx];
|
||||||
|
const prevIdx = shuffleKeys.indexOf(key);
|
||||||
|
|
||||||
|
const bufferedNode = bufferNodes.get(key);
|
||||||
|
if (bufferedNode) {
|
||||||
|
// ---- We need to add the flowIndex of the bufferedNode,
|
||||||
|
// because the bufferedNode is in the parentEl and the new position is ahead of the previous position
|
||||||
|
const bufferedFlowIndex = ForNode.getFlowIndexFromNodes(bufferedNode);
|
||||||
|
const lastEl = ForNode.toEls(bufferedNode).pop();
|
||||||
|
const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex];
|
||||||
|
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
|
||||||
|
// ---- If the node is buffered, we need to add it to the parentEl
|
||||||
|
ForNode.insertNodesBefore(bufferedNode, parentEl, nextSibling);
|
||||||
|
}
|
||||||
|
// ---- So the added length is the length of the bufferedNode
|
||||||
|
newFlowIndex += bufferedFlowIndex;
|
||||||
|
delete bufferNodes[idx];
|
||||||
|
} else if (prevIdx === idx) {
|
||||||
|
// ---- If the node is in the same position, we don't need to do anything
|
||||||
|
newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// ---- If the node is not in the same position, we need to buffer it
|
||||||
|
// We buffer the node of the previous position, and then replace it with the node of the current position
|
||||||
|
const prevKey = shuffleKeys[idx];
|
||||||
|
bufferNodes.set(prevKey, this.nodesMap.get(prevKey));
|
||||||
|
// ---- Length would never change, and the last will always be in the same position,
|
||||||
|
// so it'll always be insertBefore instead of appendChild
|
||||||
|
const childNodes = this.nodesMap.get(key);
|
||||||
|
const lastEl = ForNode.toEls(childNodes).pop();
|
||||||
|
const nextSibling = parentEl.childNodes[newFlowIndex];
|
||||||
|
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
|
||||||
|
newFlowIndex += ForNode.insertNodesBefore(childNodes, parentEl, nextSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---- Swap the keys
|
||||||
|
const tempKey = shuffleKeys[idx];
|
||||||
|
shuffleKeys[idx] = shuffleKeys[prevIdx];
|
||||||
|
shuffleKeys[prevIdx] = tempKey;
|
||||||
|
const tempUpdateFunc = newUpdateArr[idx];
|
||||||
|
newUpdateArr[idx] = newUpdateArr[prevIdx];
|
||||||
|
newUpdateArr[prevIdx] = tempUpdateFunc;
|
||||||
|
}
|
||||||
|
this.array = [...newArray];
|
||||||
|
this.updateArr = newUpdateArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Compare two arrays
|
||||||
|
* @param arr1
|
||||||
|
* @param arr2
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static arrayEqual(arr1, arr2) {
|
||||||
|
if (arr1.length !== arr2.length) return false;
|
||||||
|
return arr1.every((item, idx) => item === arr2[idx]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { DLNode } from '../DLNode';
|
||||||
|
import { DLStore } from '../store';
|
||||||
|
|
||||||
|
export class MutableNode extends DLNode {
|
||||||
|
/**
|
||||||
|
* @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention:
|
||||||
|
* 1. The environment of the new nodes should be the same as the old nodes
|
||||||
|
* 2. The new nodes should be added to the parentEl
|
||||||
|
* 3. The old nodes should be removed from the parentEl
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
constructor(type) {
|
||||||
|
super(type);
|
||||||
|
// ---- Save the current environment nodes, must be a new reference
|
||||||
|
if (DLStore.global.DLEnvStore && DLStore.global.DLEnvStore.currentEnvNodes.length > 0) {
|
||||||
|
this.savedEnvNodes = [...DLStore.global.DLEnvStore.currentEnvNodes];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the new nodes, add parentEl to all nodes
|
||||||
|
* @param nodes
|
||||||
|
*/
|
||||||
|
initNewNodes(nodes) {
|
||||||
|
// ---- Add parentEl to all nodes
|
||||||
|
DLNode.addParentEl(nodes, this._$parentEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Generate new nodes in the saved environment
|
||||||
|
* @param newNodesFunc
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
geneNewNodesInEnv(newNodesFunc) {
|
||||||
|
if (!this.savedEnvNodes) {
|
||||||
|
// ---- No saved environment, just generate new nodes
|
||||||
|
const newNodes = newNodesFunc();
|
||||||
|
// ---- Only for IfNode's same condition return
|
||||||
|
// ---- Initialize the new nodes
|
||||||
|
this.initNewNodes(newNodes);
|
||||||
|
return newNodes;
|
||||||
|
}
|
||||||
|
// ---- Save the current environment nodes
|
||||||
|
const currentEnvNodes = DLStore.global.DLEnvStore.currentEnvNodes;
|
||||||
|
// ---- Replace the saved environment nodes
|
||||||
|
DLStore.global.DLEnvStore.replaceEnvNodes(this.savedEnvNodes);
|
||||||
|
const newNodes = newNodesFunc();
|
||||||
|
// ---- Retrieve the current environment nodes
|
||||||
|
DLStore.global.DLEnvStore.replaceEnvNodes(currentEnvNodes);
|
||||||
|
// ---- Only for IfNode's same condition return
|
||||||
|
// ---- Initialize the new nodes
|
||||||
|
this.initNewNodes(newNodes);
|
||||||
|
return newNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
initUnmountStore() {
|
||||||
|
DLStore.global.WillUnmountStore.push([]);
|
||||||
|
DLStore.global.DidUnmountStore.push([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
|
||||||
|
* @param nodes
|
||||||
|
* @param removeEl Only remove outermost element
|
||||||
|
*/
|
||||||
|
removeNodes(nodes) {
|
||||||
|
DLNode.loopShallowEls(nodes, node => {
|
||||||
|
this._$parentEl.removeChild(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { DLNodeType } from '../DLNode';
|
||||||
|
import { FlatNode } from './FlatNode';
|
||||||
|
import { EnvNode } from '../EnvNode';
|
||||||
|
|
||||||
|
export class TryNode extends FlatNode {
|
||||||
|
constructor(tryFunc, catchFunc) {
|
||||||
|
super(DLNodeType.Try);
|
||||||
|
this.tryFunc = tryFunc;
|
||||||
|
const catchable = this.getCatchable(catchFunc);
|
||||||
|
this.envNode = new EnvNode({ _$catchable: catchable });
|
||||||
|
const nodes = tryFunc(this.setUpdateFunc.bind(this), catchable) ?? [];
|
||||||
|
this.envNode.initNodes(nodes);
|
||||||
|
this._$nodes = nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(changed) {
|
||||||
|
this.updateFunc?.(changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpdateFunc(updateFunc) {
|
||||||
|
this.updateFunc = updateFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCatchable(catchFunc) {
|
||||||
|
return callback =>
|
||||||
|
(...args) => {
|
||||||
|
try {
|
||||||
|
return callback(...args);
|
||||||
|
} catch (e) {
|
||||||
|
// ---- Run it in next tick to make sure when error occurs before
|
||||||
|
// didMount, this._$parentEl is not null
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
const nodes = this.geneNewNodesInEnv(() => catchFunc(this.setUpdateFunc.bind(this), e));
|
||||||
|
this._$nodes && this.removeNodes(this._$nodes);
|
||||||
|
const parentEl = this._$parentEl;
|
||||||
|
const flowIndex = FlatNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
||||||
|
const nextSibling = parentEl.childNodes[flowIndex];
|
||||||
|
FlatNode.appendNodesWithSibling(nodes, parentEl, nextSibling);
|
||||||
|
FlatNode.runDidMount();
|
||||||
|
this._$nodes = nodes;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { DLNode } from './DLNode';
|
||||||
|
import { insertNode } from './HTMLNode';
|
||||||
|
export class PropView {
|
||||||
|
propViewFunc;
|
||||||
|
dlUpdateFunc = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PropView constructor, accept a function that returns a list of DLNode
|
||||||
|
* @param propViewFunc - A function that when called, collects and returns an array of DLNode instances
|
||||||
|
*/
|
||||||
|
constructor(propViewFunc) {
|
||||||
|
this.propViewFunc = propViewFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Build the prop view by calling the propViewFunc and add every single instance of the returned DLNode to dlUpdateNodes
|
||||||
|
* @returns An array of DLNode instances returned by propViewFunc
|
||||||
|
*/
|
||||||
|
build() {
|
||||||
|
let update;
|
||||||
|
const addUpdate = updateFunc => {
|
||||||
|
update = updateFunc;
|
||||||
|
this.dlUpdateFunc.add(updateFunc);
|
||||||
|
};
|
||||||
|
const newNodes = this.propViewFunc(addUpdate);
|
||||||
|
if (newNodes.length === 0) return [];
|
||||||
|
if (update) {
|
||||||
|
// Remove the updateNode from dlUpdateNodes when it unmounts
|
||||||
|
DLNode.addWillUnmount(newNodes[0], this.dlUpdateFunc.delete.bind(this.dlUpdateFunc, update));
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update every node in dlUpdateNodes
|
||||||
|
* @param changed - A parameter indicating what changed to trigger the update
|
||||||
|
*/
|
||||||
|
update(...args) {
|
||||||
|
this.dlUpdateFunc.forEach(update => {
|
||||||
|
update(...args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertChildren(el, propView) {
|
||||||
|
insertNode(el, { _$nodes: propView.build(), _$dlNodeType: 7 }, 0);
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { DLNode, DLNodeType } from './DLNode';
|
||||||
|
import { cached } from './store';
|
||||||
|
|
||||||
|
export class SnippetNode extends DLNode {
|
||||||
|
constructor(depsArr) {
|
||||||
|
super(DLNodeType.Snippet);
|
||||||
|
this.depsArr = depsArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
cached(deps, changed) {
|
||||||
|
if (!deps || !deps.length) return false;
|
||||||
|
const idx = Math.log2(changed);
|
||||||
|
const prevDeps = this.depsArr[idx];
|
||||||
|
if (cached(deps, prevDeps)) return true;
|
||||||
|
this.depsArr[idx] = deps;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { DLStore, cached } from './store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Shorten document.createTextNode
|
||||||
|
* @param value
|
||||||
|
* @returns Text
|
||||||
|
*/
|
||||||
|
export function createTextNode(value, deps) {
|
||||||
|
const node = DLStore.document.createTextNode(value);
|
||||||
|
node.$$deps = deps;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update text node and check if the value is changed
|
||||||
|
* @param node
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function updateText(node, valueFunc, deps) {
|
||||||
|
if (cached(deps, node.$$deps)) return;
|
||||||
|
const value = valueFunc();
|
||||||
|
node.textContent = value;
|
||||||
|
node.$$deps = deps;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './types/index';
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { DLNode } from './DLNode';
|
||||||
|
import { insertNode } from './HTMLNode';
|
||||||
|
|
||||||
|
export * from './HTMLNode';
|
||||||
|
export * from './CompNode';
|
||||||
|
export * from './EnvNode';
|
||||||
|
export * from './TextNode';
|
||||||
|
export * from './PropView';
|
||||||
|
export * from './SnippetNode';
|
||||||
|
export * from './MutableNode/ForNode';
|
||||||
|
export * from './MutableNode/ExpNode';
|
||||||
|
export * from './MutableNode/CondNode';
|
||||||
|
export * from './MutableNode/TryNode';
|
||||||
|
|
||||||
|
import { DLStore } from './store';
|
||||||
|
export { setGlobal, setDocument } from './store';
|
||||||
|
|
||||||
|
function initStore() {
|
||||||
|
// Declare a global variable to store willUnmount functions
|
||||||
|
DLStore.global.WillUnmountStore = [];
|
||||||
|
// Declare a global variable to store didUnmount functions
|
||||||
|
DLStore.global.DidUnmountStore = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render(idOrEl, DL) {
|
||||||
|
let el = idOrEl;
|
||||||
|
if (typeof idOrEl === 'string') {
|
||||||
|
const elFound = DLStore.document.getElementById(idOrEl);
|
||||||
|
if (elFound) el = elFound;
|
||||||
|
else {
|
||||||
|
throw new Error(`DLight: Element with id ${idOrEl} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initStore();
|
||||||
|
el.innerHTML = '';
|
||||||
|
const dlNode = new DL();
|
||||||
|
dlNode._$init();
|
||||||
|
insertNode(el, dlNode, 0);
|
||||||
|
DLNode.runDidMount();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function manual(callback, _deps) {
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
export function escape(arg) {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $ = escape;
|
||||||
|
export const required = null;
|
||||||
|
|
||||||
|
export function use() {
|
||||||
|
console.error(
|
||||||
|
'DLight: use() is not supported be called directly. You can only assign `use(model)` to a dlight class property. Any other expressions are not allowed.'
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Store } from '@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);
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { type DLightHTMLAttributes } from './htmlTag';
|
||||||
|
|
||||||
|
// a very magical solution
|
||||||
|
// when vscode parse ts, if it is type A<T> = B<xxx<T>>, it will show the detailed type,
|
||||||
|
// but if type A<T> = B<xxx<T>> & xxx, it will only show alias (here is A)
|
||||||
|
// because I don't want to expose the detailed type, so type A<T> = B<xxx<T>> & Useless
|
||||||
|
// but if type Useless = { useless: never } will cause this type to have an additional property userless
|
||||||
|
// so just don't add key!
|
||||||
|
type Useless = { [key in '']: never };
|
||||||
|
|
||||||
|
export type DLightObject<T> = {
|
||||||
|
[K in keyof T]-?: undefined extends T[K]
|
||||||
|
? (value?: T[K]) => DLightObject<Omit<T, K>>
|
||||||
|
: (value: T[K]) => DLightObject<Omit<T, K>>;
|
||||||
|
};
|
||||||
|
interface CustomNodeProps {
|
||||||
|
willMount: (node: any) => void;
|
||||||
|
didMount: (node: any) => void;
|
||||||
|
willUnmount: (node: any) => void;
|
||||||
|
didUnmount: (node: any) => void;
|
||||||
|
didUpdate: (node: any, key: string, prevValue: any, currValue: any) => void;
|
||||||
|
ref: (node: any) => void;
|
||||||
|
elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
|
||||||
|
forwardProps: true | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContentProp<T = object> = T & { _$idContent: true };
|
||||||
|
|
||||||
|
export type RemoveOptional<T> = {
|
||||||
|
[K in keyof T]-?: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
type IsAny<T> = { _$isAny: true } extends T ? true : false;
|
||||||
|
|
||||||
|
export type ContentKeyName<T> = {
|
||||||
|
[K in keyof T]: IsAny<T[K]> extends true
|
||||||
|
? never
|
||||||
|
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
T[K] extends ContentProp<infer _>
|
||||||
|
? K
|
||||||
|
: never;
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
export type CheckContent<T> = RemoveOptional<T>[ContentKeyName<RemoveOptional<T>>];
|
||||||
|
|
||||||
|
type CustomClassTag<T, O> =
|
||||||
|
ContentKeyName<RemoveOptional<O>> extends undefined
|
||||||
|
? () => DLightObject<T>
|
||||||
|
: undefined extends O[ContentKeyName<RemoveOptional<O>>]
|
||||||
|
? CheckContent<O> extends ContentProp<infer U>
|
||||||
|
? (content?: U extends unknown ? any : unknown) => DLightObject<Omit<T, ContentKeyName<RemoveOptional<O>>>>
|
||||||
|
: never
|
||||||
|
: CheckContent<O> extends ContentProp<infer U>
|
||||||
|
? (content: U extends unknown ? any : unknown) => DLightObject<Omit<T, ContentKeyName<RemoveOptional<O>>>>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type CustomSnippetTag<T> = T extends { content: infer U }
|
||||||
|
? (content: U) => DLightObject<Omit<T, 'content'>>
|
||||||
|
: T extends { content?: infer U }
|
||||||
|
? (content?: U) => DLightObject<Omit<T, 'content'>>
|
||||||
|
: () => DLightObject<T>;
|
||||||
|
|
||||||
|
type CustomTagType<T, G> = CustomClassTag<
|
||||||
|
T & CustomNodeProps & (keyof G extends never ? object : DLightHTMLAttributes<G, object, HTMLElement>),
|
||||||
|
T
|
||||||
|
> &
|
||||||
|
Useless;
|
||||||
|
export type Typed<T = object, G = object> = CustomTagType<T, G> & Useless;
|
||||||
|
export type SnippetTyped<T = object> = CustomSnippetTag<T> & Useless;
|
||||||
|
|
||||||
|
export type Pretty = any;
|
||||||
|
|
||||||
|
// ---- reverse
|
||||||
|
export type UnTyped<T> = T extends Typed<infer U> ? U : never;
|
|
@ -0,0 +1,6 @@
|
||||||
|
// ---- env
|
||||||
|
import { DLightObject } from './compTag';
|
||||||
|
|
||||||
|
type AnyEnv = { _$anyEnv: true };
|
||||||
|
|
||||||
|
export const env: <T = AnyEnv>() => T extends AnyEnv ? any : DLightObject<T>;
|
|
@ -0,0 +1,13 @@
|
||||||
|
interface ExpressionTag {
|
||||||
|
willMount: (node: any) => void;
|
||||||
|
didMount: (node: any) => void;
|
||||||
|
willUnmount: (node: any) => void;
|
||||||
|
didUnmount: (node: any) => void;
|
||||||
|
didUpdate: <T>(node: any, key: string, prevValue: T, currValue: T) => void;
|
||||||
|
elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
|
||||||
|
ref: (node: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpressionTagFunc = (nodes: any) => ExpressionTag;
|
||||||
|
|
||||||
|
export const _: ExpressionTagFunc;
|
|
@ -0,0 +1,516 @@
|
||||||
|
export interface DLightGlobalEventHandlers {
|
||||||
|
/**
|
||||||
|
* Fires when the user aborts the download.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/abort_event)
|
||||||
|
*/
|
||||||
|
onAbort: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationcancel_event) */
|
||||||
|
onAnimationCancel: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event) */
|
||||||
|
onAnimationEnd: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event) */
|
||||||
|
onAnimationIteration: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event) */
|
||||||
|
onAnimationStart: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/auxclick_event) */
|
||||||
|
onAuxClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/beforeinput_event) */
|
||||||
|
onBeforeInput: ((this: GlobalEventHandlers, ev: InputEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the object loses the input focus.
|
||||||
|
* @param ev The focus event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/blur_event)
|
||||||
|
*/
|
||||||
|
onBlur: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/cancel_event) */
|
||||||
|
onCancel: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when playback is possible, but would require further buffering.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplay_event)
|
||||||
|
*/
|
||||||
|
onCanPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplaythrough_event) */
|
||||||
|
onCanPlayThrough: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the contents of the object or selection have changed.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)
|
||||||
|
*/
|
||||||
|
onChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user clicks the left mouse button on the object
|
||||||
|
* @param ev The mouse event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/click_event)
|
||||||
|
*/
|
||||||
|
onClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/close_event) */
|
||||||
|
onClose: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user clicks the right mouse button in the client area, opening the context menu.
|
||||||
|
* @param ev The mouse event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/contextmenu_event)
|
||||||
|
*/
|
||||||
|
onContextMenu: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/copy_event) */
|
||||||
|
onCopy: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLTrackElement/cuechange_event) */
|
||||||
|
onCueChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/cut_event) */
|
||||||
|
onCut: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user double-clicks the object.
|
||||||
|
* @param ev The mouse event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/dblclick_event)
|
||||||
|
*/
|
||||||
|
onDblClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires on the source object continuously during a drag operation.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drag_event)
|
||||||
|
*/
|
||||||
|
onDrag: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires on the source object when the user releases the mouse at the close of a drag operation.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragend_event)
|
||||||
|
*/
|
||||||
|
onDragEnd: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires on the target element when the user drags the object to a valid drop target.
|
||||||
|
* @param ev The drag event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragenter_event)
|
||||||
|
*/
|
||||||
|
onDragEnter: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires on the target object when the user moves the mouse out of a valid drop target during a drag operation.
|
||||||
|
* @param ev The drag event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragleave_event)
|
||||||
|
*/
|
||||||
|
onDragLeave: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires on the target element continuously while the user drags the object over a valid drop target.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragover_event)
|
||||||
|
*/
|
||||||
|
onDragOver: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires on the source object when the user starts to drag a text selection or selected object.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragstart_event)
|
||||||
|
*/
|
||||||
|
onDragStart: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drop_event) */
|
||||||
|
onDrop: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the duration attribute is updated.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/durationchange_event)
|
||||||
|
*/
|
||||||
|
onDurationChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the media element is reset to its initial state.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/emptied_event)
|
||||||
|
*/
|
||||||
|
onEmptied: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the end of playback is reached.
|
||||||
|
* @param ev The event
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ended_event)
|
||||||
|
*/
|
||||||
|
onEnded: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when an error occurs during object loading.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/error_event)
|
||||||
|
*/
|
||||||
|
onError: OnErrorEventHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the object receives focus.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/focus_event)
|
||||||
|
*/
|
||||||
|
onFocus: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/formdata_event) */
|
||||||
|
onFormData: ((this: GlobalEventHandlers, ev: FormDataEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/gotpointercapture_event) */
|
||||||
|
onGotPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event) */
|
||||||
|
onInput: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/invalid_event) */
|
||||||
|
onInvalid: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user presses a key.
|
||||||
|
* @param ev The keyboard event
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keydown_event)
|
||||||
|
*/
|
||||||
|
onKeyDown: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user presses an alphanumeric key.
|
||||||
|
* @param ev The event.
|
||||||
|
* @deprecated
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keypress_event)
|
||||||
|
*/
|
||||||
|
onKeyPress: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user releases a key.
|
||||||
|
* @param ev The keyboard event
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keyup_event)
|
||||||
|
*/
|
||||||
|
onKeyUp: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires immediately after the browser loads the object.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SVGElement/load_event)
|
||||||
|
*/
|
||||||
|
onLoad: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when media data is loaded at the current playback position.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadeddata_event)
|
||||||
|
*/
|
||||||
|
onLoadedData: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the duration and dimensions of the media have been determined.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadedmetadata_event)
|
||||||
|
*/
|
||||||
|
onLoadedMetadata: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when Internet Explorer begins looking for media data.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadstart_event)
|
||||||
|
*/
|
||||||
|
onLoadStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/lostpointercapture_event) */
|
||||||
|
onLostPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user clicks the object with either mouse button.
|
||||||
|
* @param ev The mouse event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousedown_event)
|
||||||
|
*/
|
||||||
|
onMouseDown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseenter_event) */
|
||||||
|
onMouseEnter: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseleave_event) */
|
||||||
|
onMouseLeave: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user moves the mouse over the object.
|
||||||
|
* @param ev The mouse event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousemove_event)
|
||||||
|
*/
|
||||||
|
onMouseMove: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user moves the mouse pointer outside the boundaries of the object.
|
||||||
|
* @param ev The mouse event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseout_event)
|
||||||
|
*/
|
||||||
|
onMouseOut: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user moves the mouse pointer into the object.
|
||||||
|
* @param ev The mouse event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseover_event)
|
||||||
|
*/
|
||||||
|
onMouseOver: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user releases a mouse button while the mouse is over the object.
|
||||||
|
* @param ev The mouse event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseup_event)
|
||||||
|
*/
|
||||||
|
onMouseUp: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/paste_event) */
|
||||||
|
onPaste: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when playback is paused.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/pause_event)
|
||||||
|
*/
|
||||||
|
onPause: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the play method is requested.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/play_event)
|
||||||
|
*/
|
||||||
|
onPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the audio or video has started playing.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/playing_event)
|
||||||
|
*/
|
||||||
|
onPlaying: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointercancel_event) */
|
||||||
|
onPointerCancel: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerdown_event) */
|
||||||
|
onPointerDown: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerenter_event) */
|
||||||
|
onPointerEnter: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerleave_event) */
|
||||||
|
onPointerLeave: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointermove_event) */
|
||||||
|
onPointerMove: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerout_event) */
|
||||||
|
onPointerOut: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerover_event) */
|
||||||
|
onPointerOver: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerup_event) */
|
||||||
|
onPointerUp: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs to indicate progress while downloading media data.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/progress_event)
|
||||||
|
*/
|
||||||
|
onProgress: ((this: GlobalEventHandlers, ev: ProgressEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the playback rate is increased or decreased.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ratechange_event)
|
||||||
|
*/
|
||||||
|
onRateChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user resets a form.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/reset_event)
|
||||||
|
*/
|
||||||
|
onReset: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLVideoElement/resize_event) */
|
||||||
|
onResize: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the user repositions the scroll box in the scroll bar on the object.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scroll_event)
|
||||||
|
*/
|
||||||
|
onScroll: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scrollend_event) */
|
||||||
|
onScrollEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/securitypolicyviolation_event) */
|
||||||
|
onSecurityPolicyViolation: ((this: GlobalEventHandlers, ev: SecurityPolicyViolationEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the seek operation ends.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeked_event)
|
||||||
|
*/
|
||||||
|
onSeeked: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the current playback position is moved.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeking_event)
|
||||||
|
*/
|
||||||
|
onSeeking: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when the current selection changes.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/select_event)
|
||||||
|
*/
|
||||||
|
onSelect: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/selectionchange_event) */
|
||||||
|
onSelectionChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/selectstart_event) */
|
||||||
|
onSelectStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLSlotElement/slotchange_event) */
|
||||||
|
onSlotChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the download has stopped.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/stalled_event)
|
||||||
|
*/
|
||||||
|
onStalled: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/submit_event) */
|
||||||
|
onSubmit: ((this: GlobalEventHandlers, ev: SubmitEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs if the load operation has been intentionally halted.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/suspend_event)
|
||||||
|
*/
|
||||||
|
onSuspend: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs to indicate the current playback position.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/timeupdate_event)
|
||||||
|
*/
|
||||||
|
onTimeUpdate: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDetailsElement/toggle_event) */
|
||||||
|
onToggle: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitioncancel_event) */
|
||||||
|
onTransitionCancel: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event) */
|
||||||
|
onTransitionEnd: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionrun_event) */
|
||||||
|
onTransitionRun: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionstart_event) */
|
||||||
|
onTransitionStart: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when the volume is changed, or playback is muted or unmuted.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/volumechange_event)
|
||||||
|
*/
|
||||||
|
onVolumeChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when playback stops because the next frame of a video resource is not available.
|
||||||
|
* @param ev The event.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/waiting_event)
|
||||||
|
*/
|
||||||
|
onWaiting: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This is a legacy alias of `onAnimationEnd`.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event)
|
||||||
|
*/
|
||||||
|
onWebkitAnimationEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This is a legacy alias of `onAnimationIteration`.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event)
|
||||||
|
*/
|
||||||
|
onWebkitAnimationIteration: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This is a legacy alias of `onAnimationStart`.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event)
|
||||||
|
*/
|
||||||
|
onWebkitAnimationStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This is a legacy alias of `onTransitionEnd`.
|
||||||
|
*
|
||||||
|
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event)
|
||||||
|
*/
|
||||||
|
onWebkitTransitionEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/wheel_event) */
|
||||||
|
onWheel: ((this: GlobalEventHandlers, ev: WheelEvent) => any) | null;
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { type Properties } from 'csstype';
|
||||||
|
|
||||||
|
// ---- Used to determine whether X and Y are equal, return A if equal, otherwise B
|
||||||
|
type IfEquals<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
|
||||||
|
|
||||||
|
export type OmitIndexSignature<ObjectType> = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
[KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? never : KeyType]: ObjectType[KeyType];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- For each key, check whether there is readonly, if there is, return never, and then Pick out is not never
|
||||||
|
type WritableKeysOf<T> = {
|
||||||
|
[P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never>;
|
||||||
|
}[keyof T];
|
||||||
|
type RemoveReadOnly<T> = Pick<T, WritableKeysOf<T>>;
|
||||||
|
|
||||||
|
// ---- Delete all functions
|
||||||
|
type OmitFunction<T> = Omit<T, { [K in keyof T]: T[K] extends (...args: any) => any ? K : never }[keyof T]>;
|
||||||
|
|
||||||
|
type OmitFuncAndReadOnly<T> = RemoveReadOnly<OmitFunction<OmitIndexSignature<T>>>;
|
||||||
|
|
||||||
|
// ---- properties
|
||||||
|
type OmitFuncAndReadOnlyProperty<G> = Omit<OmitFuncAndReadOnly<G>, 'className' | 'htmlFor' | 'style' | 'innerText'>;
|
||||||
|
|
||||||
|
type CustomCSSProperties = {
|
||||||
|
[Key in `--${string}`]: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HTMLAttributes<T> = OmitFuncAndReadOnlyProperty<T> & {
|
||||||
|
style: Properties & CustomCSSProperties;
|
||||||
|
class: string;
|
||||||
|
for: string;
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue