Compare commits

...

24 Commits

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

BIN
.DS_Store vendored Normal file

Binary file not shown.

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ pnpm-lock.yaml
build
/packages/inula-router/connectRouter
/packages/inula-router/router
.inula-max

View File

@ -9,6 +9,9 @@
"prettier": "prettier .prettierrc.js -w packages/**/*.{ts,tsx,js,jsx}",
"build:inula": "pnpm -F openinula build",
"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-intl": "pnpm -F inula-intl build",
"build:inula-request": "pnpm -F inula-request build",
@ -22,46 +25,48 @@
]
},
"devDependencies": {
"@babel/core": "7.16.7",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-proposal-private-methods": "7.16.7",
"@babel/plugin-proposal-private-property-in-object": "7.16.7",
"@babel/plugin-syntax-jsx": "7.16.7",
"@babel/plugin-transform-arrow-functions": "7.16.7",
"@babel/plugin-transform-block-scoped-functions": "7.16.7",
"@babel/plugin-transform-block-scoping": "7.16.7",
"@babel/plugin-transform-classes": "7.16.7",
"@babel/plugin-transform-computed-properties": "7.16.7",
"@babel/plugin-transform-destructuring": "7.16.7",
"@babel/plugin-transform-for-of": "7.16.7",
"@babel/plugin-transform-literals": "7.16.7",
"@babel/plugin-transform-object-assign": "7.16.7",
"@babel/plugin-transform-object-super": "7.16.7",
"@babel/plugin-transform-parameters": "7.16.7",
"@babel/plugin-transform-react-jsx": "7.16.7",
"@babel/plugin-transform-react-jsx-source": "^7.16.7",
"@babel/plugin-transform-runtime": "7.16.7",
"@babel/plugin-transform-shorthand-properties": "7.16.7",
"@babel/plugin-transform-spread": "7.16.7",
"@babel/plugin-transform-template-literals": "7.16.7",
"@babel/preset-env": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"@babel/runtime": "7.16.7",
"@commitlint/cli": "^18.4.4",
"@commitlint/config-conventional": "^18.4.4",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@babel/core": "7.23.7",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-proposal-private-methods": "7.18.6",
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@babel/plugin-syntax-jsx": "7.23.3",
"@babel/plugin-transform-arrow-functions": "7.23.3",
"@babel/plugin-transform-block-scoped-functions": "7.23.3",
"@babel/plugin-transform-block-scoping": "7.23.4",
"@babel/plugin-transform-classes": "7.23.8",
"@babel/plugin-transform-computed-properties": "7.23.3",
"@babel/plugin-transform-destructuring": "7.23.3",
"@babel/plugin-transform-for-of": "7.23.6",
"@babel/plugin-transform-literals": "7.23.3",
"@babel/plugin-transform-object-assign": "7.23.3",
"@babel/plugin-transform-object-super": "7.23.3",
"@babel/plugin-transform-parameters": "7.23.3",
"@babel/plugin-transform-react-jsx": "7.23.4",
"@babel/plugin-transform-react-jsx-source": "^7.23.3",
"@babel/plugin-transform-runtime": "7.23.7",
"@babel/plugin-transform-shorthand-properties": "7.23.3",
"@babel/plugin-transform-spread": "7.23.3",
"@babel/plugin-transform-template-literals": "7.23.3",
"@babel/preset-env": "7.23.8",
"@babel/preset-typescript": "7.23.3",
"@babel/runtime": "7.23.8",
"@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^17.8.1",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^4.0.0",
"@types/jest": "^26.0.24",
"@types/jest": "^29.5.11",
"@types/node": "^17.0.18",
"@typescript-eslint/eslint-plugin": "4.8.0",
"@typescript-eslint/parser": "4.8.0",
"babel-jest": "^27.5.1",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "6.18.1",
"@babel/parser": "^7.24.7",
"magic-string": "^0.30.10",
"babel-jest": "^29.7.0",
"ejs": "^3.1.8",
"eslint": "7.13.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^6.9.0",
"eslint-plugin-jest": "^22.15.0",
"eslint-plugin-no-function-declare-after-return": "^1.0.0",
@ -72,9 +77,13 @@
"lint-staged": "^15.2.0",
"openinula": "workspace:*",
"prettier": "^3.1.1",
"rollup": "^2.75.5",
"rollup": "^2.79.1",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-execute": "^1.1.1",
"rollup-plugin-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"
},
"engines": {

View File

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

View File

@ -14,4 +14,3 @@
*/
declare module 'crequire';

View File

@ -1,51 +0,0 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import webpack from 'webpack';
import { build } from 'vite';
export default (api: any) => {
api.registerCommand({
name: 'build',
description: 'build application for production',
initialState: api.buildConfig,
fn: async function (args: any, state: any) {
switch (api.compileMode) {
case 'webpack':
if (state) {
api.applyHook({ name: 'beforeCompile', args: state });
state.forEach((s: any) => {
webpack(s.config, (err: any, stats: any) => {
if (err || stats.hasErrors()) {
api.logger.error(`Build failed.err: ${err}, stats:${stats}`);
}
});
});
} else {
api.logger.error(`Build failed. Can't find build config.`);
}
break;
case 'vite':
if (state) {
api.applyHook({ name: 'beforeCompile' });
build(state);
} else {
api.logger.error(`Build failed. Can't find build config.`);
}
break;
}
},
});
};

View File

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

View File

@ -1,32 +0,0 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
const { preset } = require('./jest.config');
module.exports = {
presets: [
[
'@babel/preset-env',
{ targets: { node: 'current' } },
],
['@babel/preset-typescript'],
[
'@babel/preset-react',
{
runtime: 'automatic',
importSource: 'openinula',
},
],
],
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import Inula, { useContext } from 'openinula';
import { useContext, useMemo } from 'openinula';
import utils from '../../utils/utils';
import { I18nContext } from '../components/InjectI18n';
import I18n from '../I18n';
@ -23,15 +23,22 @@ import { IntlType } from '../../types/types';
* 使 useI18n 便
*/
function useI18n(): IntlType {
const i18nContext = useContext<I18n>(I18nContext);
utils.isVariantI18n(i18nContext);
const i18n = i18nContext;
const i18n = useContext<I18n>(I18nContext);
utils.isVariantI18n(i18n);
return useMemo(() => {
return {
i18n: i18n,
locale: i18n.locale,
messages: i18n.messages,
defaultLocale: i18n.defaultLocale,
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { cloneElement, createElement, Fragment, InulaElement } from 'openinula';
import { voidElementTags } from '../constants';
// 用于匹配标签的正则表达式
const tagReg = /<(\d+)>(.*?)<\/\1>|<(\d+)\/>/;
// 用于匹配换行符的正则表达式
const nlReg = /(?:\r\n|\r|\n)/g;
export function formatElements(
value: string,
elements: { [key: string]: InulaElement<any> } = {}
): string | Array<any> {
const elementKeyID = getElementIndex(0, '$Inula');
// valueThis is a rich text with a custom component: <1/>
const arrays = value.replace(nlReg, '').split(tagReg);
// 若无InulaNode元素则返回
if (arrays.length === 1) return value;
const result: any = [];
const before = arrays.shift();
if (before) {
result.push(before);
}
for (const [index, children, after] of getElements(arrays)) {
let element = elements[index];
if (!element || (voidElementTags[element.type as string] && children)) {
const errorMessage = !element
? `Index not declared as ${index} in original translation`
: `${element.type} , No child element exists. Please check.`;
console.error(errorMessage);
// 对于异常元素,通过创建<></>来代替,并继续解析现有的子元素和之后的元素,并保证在构建数组时,不会因为缺少元素而导致索引错位。
element = createElement(Fragment, {});
}
// 如果存在子元素,则进行递归处理
const formattedChildren = children ? formatElements(children, elements) : element.props.children;
// 更新element 的属性和子元素
const clonedElement = cloneElement(element, { key: elementKeyID() }, formattedChildren);
result.push(clonedElement);
if (after) {
result.push(after);
}
}
return result;
}
/**
* arrays数组中解析出标签元素和其子元素
* @param arrays
*/
function getElements(arrays: string[]) {
// 如果 arrays 数组为空,则返回空数组
if (!arrays.length) return [];
/**
* pairedIndex: 第一个元素表示配对标签的内容 <1>...</1>
* children: 第二个元素表示配对标签内的子元素内容
* unpairedIndex: 第三个元素表示自闭合标签的内容 <1/>
* textAfter: 第四个元素表示标签之后的文本内容
* eg: [undefined,undefined,1,""]
*/
const [pairedIndex, children, unpairedIndex, textAfter] = arrays.splice(0, 4);
// 解析当前标签元素和它的子元素,返回一个包含标签索引、子元素和后续文本的数组
const currentElement: [number, string, string] = [
parseInt(pairedIndex || unpairedIndex), // 解析标签索引,如果是自闭合标签,则使用 unpaired
children || '',
textAfter || '',
];
// 递归调用 getElements 函数,处理剩余的 arrays 数组
const remainingElements = getElements(arrays);
// 将当前元素和递归处理后的元素数组合并并返回
return [currentElement, ...remainingElements];
}
// 对传入富文本元素的位置标志索引
function getElementIndex(count = 0, prefix = '') {
return function () {
return `${prefix}_${count++}`;
};
}

View File

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

View File

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

View File

@ -1,136 +0,0 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import I18n from '../../src/core/I18n';
describe('I18n', () => {
it('load catalog and merge with existing', () => {
const i18n = new I18n({});
const messages = {
Hello: 'Hello',
};
i18n.loadMessage('en', messages);
i18n.changeLanguage('en');
expect(i18n.messages).toEqual(messages);
i18n.loadMessage('fr', { Hello: 'Salut' });
expect(i18n.messages).toEqual(messages);
});
it('should load multiple language ', function () {
const enMessages = {
Hello: 'Hello',
};
const frMessage = {
Hello: 'Salut',
};
const intl = new I18n({});
intl.loadMessage({
en: enMessages,
fr: frMessage,
});
intl.changeLanguage('en');
expect(intl.messages).toEqual(enMessages);
intl.changeLanguage('fr');
expect(intl.messages).toEqual(frMessage);
});
it('should switch active locale', () => {
const messages = {
Hello: 'Salut',
};
const i18n = new I18n({
locale: 'en',
messages: {
fr: messages,
en: {},
},
});
expect(i18n.locale).toEqual('en');
expect(i18n.messages).toEqual({});
i18n.changeLanguage('fr');
expect(i18n.locale).toEqual('fr');
expect(i18n.messages).toEqual(messages);
});
it('should switch active locale', () => {
const messages = {
Hello: 'Salut',
};
const i18n = new I18n({
locale: 'en',
messages: {
en: messages,
fr: {},
},
});
i18n.changeLanguage('en');
expect(i18n.locale).toEqual('en');
expect(i18n.messages).toEqual(messages);
i18n.changeLanguage('fr');
expect(i18n.locale).toEqual('fr');
expect(i18n.messages).toEqual({});
});
it('._ allow escaping syntax characters', () => {
const messages = {
'My \'\'name\'\' is \'{name}\'': 'Mi \'\'nombre\'\' es \'{name}\'',
};
const i18n = new I18n({
locale: 'es',
messages: { es: messages },
});
expect(i18n.formatMessage('My \'\'name\'\' is \'{name}\'')).toEqual('Mi \'nombre\' es {name}');
});
it('._ should format message from catalog', function () {
const messages = {
Hello: 'Salut',
id: 'Je m\'appelle {name}',
};
const i18n = new I18n({
locale: 'fr',
messages: { fr: messages },
});
expect(i18n.locale).toEqual('fr');
expect(i18n.formatMessage('Hello')).toEqual('Salut');
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual('Je m\'appelle Fred');
});
it('should return the formatted date and time', () => {
const i18n = new I18n({
locale: 'fr',
});
const formattedDateTime = i18n.formatDate('2023-06-06T07:53:54.465Z', {
dateStyle: 'full',
timeStyle: 'short',
});
expect(typeof formattedDateTime).toBe('string');
expect(formattedDateTime).toEqual('mardi 6 juin 2023 à 15:53');
});
it('should return the formatted number', () => {
const i18n = new I18n({
locale: 'en',
});
const formattedNumber = i18n.formatNumber(123456.789, { style: 'currency', currency: 'USD' });
expect(typeof formattedNumber).toBe('string');
expect(formattedNumber).toEqual('$123,456.79');
});
});

View File

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

View File

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

View File

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

View File

@ -29,6 +29,20 @@ describe('createI18n', () => {
).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 () {
const onWarn = jest.fn();
createI18n({

View File

@ -1,75 +0,0 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import * as React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider, useIntl } from '../../../index';
const FunctionComponent = ({ spy }: { spy?: Function }) => {
const { i18n } = useIntl();
spy!(i18n.locale);
return null;
};
const FC = () => {
const i18n = useIntl();
return i18n.formatNumber(10000, { style: 'currency', currency: 'USD' }) as any;
};
describe('useIntl() hooks', () => {
it('throws when <IntlProvider> is missing from ancestry', () => {
// So it doesn't spam the console
jest.spyOn(console, 'error').mockImplementation(() => {});
expect(() => render(<FunctionComponent />)).toThrow('I18n object is not found!');
});
it('hooks onto the intl context', () => {
const spy = jest.fn();
render(
<IntlProvider locale="en">
<FunctionComponent spy={spy} />
</IntlProvider>
);
expect(spy).toHaveBeenCalledWith('en');
});
it('should work when switching locale on provider', () => {
const { rerender, getByTestId } = render(
<IntlProvider locale="en">
<span data-testid="comp">
<FC />
</span>
</IntlProvider>
);
expect(getByTestId('comp')).toMatchSnapshot();
rerender(
<IntlProvider locale="es">
<span data-testid="comp">
<FC />
</span>
</IntlProvider>
);
expect(getByTestId('comp')).toMatchSnapshot();
rerender(
<IntlProvider locale="en">
<span data-testid="comp">
<FC />
</span>
</IntlProvider>
);
expect(getByTestId('comp')).toMatchSnapshot();
});
});

View File

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

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@ describe('eventEmitter', () => {
expect(listener).not.toBeCalled();
});
it('should do nothing when even doesn\'t exist', () => {
it("should do nothing when even doesn't exist", () => {
const unknown = jest.fn();
const emitter = new EventEmitter();

View File

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

View File

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

View File

@ -14,11 +14,5 @@
*/
module.exports = {
presets: [
[
'@babel/preset-env',
{ targets: { node: 'current' }},
],
['@babel/preset-typescript'],
],
presets: [['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-typescript']],
};

View File

@ -16,17 +16,15 @@
</div>
<script src="../../dist/bundle.js"></script>
<script>
import inulaRequest from "../../index";
const sendRequestButton = document.getElementById('sendRequestButton');
const cancelRequestButton = document.getElementById('cancelRequestButton');
const message = document.getElementById('message');
let controller = new AbortController;
let controller = new AbortController();
const signal = controller.signal;
sendRequestButton.addEventListener('click', function() {
setInterval(function() {
setInterval(() => {
inulaRequest.get('http://localhost:3001/data', {
signal
}).then(function(response) {

View File

@ -118,7 +118,7 @@
putResult.innerHTML = JSON.stringify(error, null, 2);
});
inulaRequest.delete('http://localhost:3001/users', {params: {id: 1}})
inulaRequest.delete('http://localhost:3001/users', {params: {id: 1, test:['{}']}})
.then(function (response) {
deleteResult.innerHTML = JSON.stringify(response.data, null, 2);
})

View File

@ -13,11 +13,11 @@
* See the Mulan PSL v2 for more details.
*/
import express from "express";
import * as fs from "fs";
import bodyParser from "body-parser";
import cors from "cors";
import * as path from "path";
import express from 'express';
import * as fs from 'fs';
import * as path from 'path';
import bodyParser from 'body-parser';
import cors from 'cors';
const app = express();
const port = 3001;

View File

@ -14,7 +14,6 @@
*/
import inulaRequest from './src/inulaRequest';
import useIR from './src/core/useIR/useIR';
const {
create,
@ -60,7 +59,6 @@ export {
isIrError,
spread,
IrHeaders,
useIR,
// 兼容axios
Axios,
AxiosError,

View File

@ -11,7 +11,7 @@
"server": "nodemon .\\examples\\server\\serverTest.mjs"
},
"files": [
"/dist",
"dist",
"README.md",
"CHANGELOG.md"
],
@ -26,38 +26,25 @@
"author": "",
"license": "MulanPSL2",
"devDependencies": {
"@babel/core": "^7.21.8",
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.21.4",
"@rollup/plugin-commonjs": "^19.0.0",
"@types/jest": "^29.2.5",
"@types/react": "^17.0.34",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"babel-jest": "^20.0.3",
"babel-loader": "^9.1.0",
"body-parser": "^1.20.2",
"core-js": "3.32.1",
"cors": "^2.8.5",
"eslint": "^8.31.0",
"express": "^4.18.2",
"html-webpack-plugin": "^5.5.3",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.4.1",
"jsdom": "^22.0.0",
"nodemon": "^2.0.22",
"prettier": "^2.6.2",
"rollup": "^3.20.2",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"ts-jest": "^29.0.4",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tslib": "^2.5.0",
"typescript": "^4.9.4",
"webpack": "^5.81.0",
"webpack-cli": "^5.0.2",
"webpack-dev-server": "^4.13.3"

View File

@ -21,16 +21,19 @@ import { babel } from '@rollup/plugin-babel';
export default {
input: './index.ts',
output: [{
output: [
{
file: 'dist/inulaRequest.js',
format: 'umd',
exports: 'named',
name: 'inulaRequest',
sourcemap: false,
}, {
},
{
file: 'dist/inulaRequest.esm-browser.js',
format: 'esm',
}],
},
],
plugins: [
resolve(),
commonjs(),

View File

@ -15,7 +15,10 @@
// 检查是否为用户主动请求取消场景
function checkCancel(input: any): boolean {
if (input) {
return input.name === 'CanceledError' || input.cancelFlag || false;
}
return false;
}
export default checkCancel;

View File

@ -69,6 +69,6 @@ inulaRequest.AxiosHeaders = IrHeaders;
export default inulaRequest;
// 兼容 IE 浏览器 fetch
if (utils.isIE()) {
if (typeof window !== 'undefined' && utils.isIE()) {
(window as any).fetch = fetchLike;
}

View File

@ -35,8 +35,10 @@ export const fetchRequest = (config: IrRequestConfig): Promise<IrResponse> => {
withCredentials = false,
onUploadProgress = null,
onDownloadProgress = null,
paramsSerializer,
} = config;
let { data = null, url, signal } = config;
let { signal, url, data = null } = config;
const controller = new AbortController();
if (!signal) {
@ -59,7 +61,7 @@ export const fetchRequest = (config: IrRequestConfig): Promise<IrResponse> => {
// 处理请求参数
if (params) {
const queryString = utils.objectToQueryString(utils.filterUndefinedValues(params));
const queryString = utils.objectToQueryString(utils.filterUndefinedValues(params), paramsSerializer);
if (queryString) {
url = `${url}${url!.includes('?') ? '&' : '?'}${queryString}`; // 支持用户将部分请求参数写在 url 中
}
@ -138,17 +140,18 @@ export const fetchRequest = (config: IrRequestConfig): Promise<IrResponse> => {
case 'json':
parseMethod = new Response(responseBody).text().then((text: string) => {
try {
return JSON.parse(text);
return text ? JSON.parse(text) : ''; // 如果服务端请求成功但不返回响应值,默认返回空字符串
} catch (e) {
// 显式指定返回类型 JSON解析失败报错
reject('parse error');
const error = new IrError('parse error', '', responseData.config, responseData.request, responseData);
reject(error);
}
});
break;
default:
parseMethod = new Response(responseBody).text().then((text: string) => {
try {
return JSON.parse(text);
return text ? JSON.parse(text) : '';
} catch (e) {
// 默认为 JSON 类型若JSON校验失败则直接返回服务端数据
return text;

View File

@ -33,6 +33,7 @@ export const ieFetchRequest = (config: IrRequestConfig): Promise<IrResponse> =>
cancelToken = null,
withCredentials = false,
} = config;
let { url } = config;
let controller: any;
let signal;
@ -96,7 +97,6 @@ export const ieFetchRequest = (config: IrRequestConfig): Promise<IrResponse> =>
headers: response.headers,
config,
request: null,
responseURL: response.url,
};
// 根据 responseType 选择相应的解析方法
@ -123,7 +123,8 @@ export const ieFetchRequest = (config: IrRequestConfig): Promise<IrResponse> =>
return JSON.parse(text);
} catch (e) {
// 显式指定返回类型 JSON解析失败报错
reject('parse error');
const error = new IrError('parse error', '', responseData.config, responseData.request, responseData);
reject(error);
}
});
break;

View File

@ -35,7 +35,7 @@ function processDownloadProgress(
}
totalBytesRead += value.byteLength;
onProgress!({ loaded: totalBytesRead, total: response.headers.get('Content-Length') });
onProgress!({ loaded: totalBytesRead, total: Number(response.headers.get('Content-Length')) });
controller.enqueue(value); // 将读取到的数据块添加到新的 ReadableStream 中
read(); // 递归调用,继续读取 stream 直到结束
});

View File

@ -16,6 +16,7 @@
import { IrProgressEvent, IrRequestConfig, IrResponse } from '../types/interfaces';
import IrError from '../core/IrError';
import CancelError from '../cancel/CancelError';
import { calculateTransRate } from '../utils/dataUtils/calculateTransRate';
function processUploadProgress(
onUploadProgress: (progressEvent: IrProgressEvent) => void | null,
@ -42,15 +43,29 @@ function processUploadProgress(
const handleUploadProgress = () => {
const xhr = new XMLHttpRequest();
let loadedBytes = 0;
const calculate = calculateTransRate(50, 250);
// 添加 progress 事件监听器
xhr.upload.addEventListener('progress', event => {
if (event.lengthComputable) {
const loaded = event.loaded;
const total = event.lengthComputable ? event.total : undefined;
const progressBytes = loaded - loadedBytes;
const rate = calculate(progressBytes);
const inRange = total && loaded <= total;
loadedBytes = loaded;
const feedback = {
loaded,
total,
progress: total ? loaded / total : undefined,
bytes: progressBytes,
rate: rate ?? undefined,
estimated: rate && total && inRange ? (total - loaded) / rate : undefined,
event,
upload: true,
};
// 可以计算上传进度
onUploadProgress!({ loaded: event.loaded, total: event.total });
} else {
onUploadProgress!({ loaded: event.loaded, total: totalBytesToUpload });
}
onUploadProgress!(feedback);
});
// 添加 readystatechange 事件监听器,当 xhr.readyState 变更时执行回调函数
@ -67,7 +82,16 @@ function processUploadProgress(
parsedText = xhr.responseText;
}
} catch (e) {
reject('parse error');
const responseData = {
data: parsedText,
status: xhr.status,
statusText: xhr.statusText,
headers: xhr.getAllResponseHeaders(),
config: config,
request: xhr,
};
const error = new IrError('parse error', '', config, xhr, responseData);
reject(error);
}
const response: IrResponse = {
data: parsedText,

View File

@ -60,6 +60,13 @@ export interface IrRequestConfig {
transitional?: TransitionalOptions;
validateStatus?: (status: number) => boolean;
// 自定义解析请求参数
paramsSerializer?: ParamsSerializer;
}
interface ParamsSerializer {
serialize?: (params: Record<string, any>) => string;
}
export interface TransitionalOptions {
@ -116,6 +123,8 @@ export interface IrInterface {
options<T = unknown>(url: string, config?: IrRequestConfig): Promise<IrResponse<T>>;
patch<T = unknown>(url: string, data?: any, config?: IrRequestConfig): Promise<IrResponse<T>>;
postForm<T = unknown>(url: string, data: any, config: IrRequestConfig): Promise<IrResponse<T>>;
putForm<T = unknown>(url: string, data: any, config: IrRequestConfig): Promise<IrResponse<T>>;

View File

@ -386,7 +386,13 @@ const convertToCamelCase = (str: string) => {
.join('');
};
function objectToQueryString(obj: Record<string, any>) {
function objectToQueryString(obj: Record<string, any>, options?: Record<string, any>) {
const serialize = options && options.serialize;
if (serialize) {
return serialize(obj);
}
return Object.keys(obj)
.map(key => {
// params 中 value 为数组时需特殊处理,如: { key: [1, 2, 3] } -> key[]=1&key[]=2&key[]=3
@ -397,6 +403,8 @@ function objectToQueryString(obj: Record<string, any>) {
return urlPart;
});
return urlPart.slice(0, -1);
} else if (utils.checkObject(obj[key])) {
return encodeURIComponent(key) + '=' + encodeURIComponent(JSON.stringify(obj[key]));
}
return encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]);
})
@ -419,8 +427,11 @@ function getNormalizedValue(value: string | any[] | boolean | null | number): st
}
function isIE(): boolean {
if (typeof window !== 'undefined') {
return /MSIE|Trident/.test(window.navigator.userAgent);
}
return false;
}
function getObjectByArray(arr: any[]): Record<string, any> {
return arr.reduce((obj, item, index) => {

View File

@ -0,0 +1,66 @@
/*
* 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.
*/
// 计算一段时间内平均传输速度
export function calculateTransRate(samplesNum: number, interval: number) {
// 采样数默认值为 10最小时间间隔默认值为 1000
const num = samplesNum ?? 10;
const minInterval = interval ?? 1000;
// 创建存储每个样本的数据量和时间戳的数组
const bytesArr = new Array(num);
const timestampsArr = new Array(num);
// 记录当前样本索引和第一个样本索引
let currentIndex = 0;
let firstIndex = 0;
let firstTimestamp: number;
return function calculate(dataSize: number) {
const now = Date.now();
const start = timestampsArr[firstIndex];
// 检查是否是第一个样本。如果是第一个样本,则将当前时间设置为 firstTimestamp
firstTimestamp = firstTimestamp ?? now;
// 存储当前数据量和时间戳
bytesArr[currentIndex] = dataSize;
timestampsArr[currentIndex] = now;
let index = firstIndex;
let totalSize = 0;
// 计算从第一个样本到当前的所有样本的数据量总和
while (index !== currentIndex) {
totalSize += bytesArr[index++];
index = index % num;
}
currentIndex = (currentIndex + 1) % num;
// 如果当前索引和第一个索引相等,表示样本数组已满,更新第一个 index 的值。
if (currentIndex === firstIndex) {
firstIndex = (firstIndex + 1) % num;
}
if (now - firstTimestamp < minInterval) {
return;
}
const totalTime = start && now - start;
return totalTime ? Math.round(totalSize * 1000 / totalTime) : undefined;
};
}

View File

@ -52,8 +52,36 @@ describe('objectToQueryString function', () => {
key4: [1, 2, 3],
key5: { a: 'b' },
};
const expectedResult = 'key1=string&key2=42&key3=true&key4[]=1&key4[]=2&key4[]=3&key5=%5Bobject%20Object%5D';
const expectedResult = 'key1=string&key2=42&key3=true&key4[]=1&key4[]=2&key4[]=3&key5=%7B%22a%22%3A%22b%22%7D';
const result = utils.objectToQueryString(input);
expect(result).toBe(expectedResult);
});
it('should handle custom params serializer when paramsSerializer is set', () => {
const input = {
key1: 'string',
key4: [1, 2, 3],
};
const options = {
serialize: params => {
return Object.keys(params)
.filter(item => Object.prototype.hasOwnProperty.call(params, item))
.reduce((pre, currentValue) => {
if (params[currentValue]) {
if (pre) {
return `${pre}&${currentValue}=${encodeURIComponent(params[currentValue])}`;
}
return `${currentValue}=${encodeURIComponent(params[currentValue])}`;
} else {
return pre;
}
}, '');
},
};
const expectedResult = 'key1=string&key4=1%2C2%2C3';
const result = utils.objectToQueryString(input, options);
expect(result).toBe(expectedResult);
});
});

View File

@ -0,0 +1,37 @@
/*
* 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 { calculateTransRate } from '../../../../src/utils/dataUtils/calculateTransRate';
describe('calculateTransRate', () => {
it('should return undefined when interval is undefined', () => {
const calc = calculateTransRate(10, undefined);
expect(calc(1000)).toBeUndefined();
});
it('should return correct rate when interval is 2000', () => {
const calc = calculateTransRate(10, 2000);
calc(1000);
expect(calc(1000)).toBeUndefined();
});
it('should return 5000 rate when interval is 2000', () => {
const calc = calculateTransRate(10, 2000);
calc(10000000000);
setTimeout(() => {
expect(calc(10000010000)).toBe(5000);
}, 2000);
});
});

View File

@ -21,49 +21,14 @@
],
"license": "MulanPSL2",
"devDependencies": {
"@babel/core": "7.21.3",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-syntax-jsx": "7.16.7",
"@babel/plugin-transform-arrow-functions": "7.16.7",
"@babel/plugin-transform-block-scoped-functions": "7.16.7",
"@babel/plugin-transform-block-scoping": "7.16.7",
"@babel/plugin-transform-classes": "7.16.7",
"@babel/plugin-transform-computed-properties": "7.16.7",
"@babel/plugin-transform-destructuring": "7.16.7",
"@babel/plugin-transform-for-of": "7.16.7",
"@babel/plugin-transform-literals": "7.16.7",
"@babel/plugin-transform-object-assign": "7.16.7",
"@babel/plugin-transform-object-super": "7.16.7",
"@babel/plugin-transform-parameters": "7.16.7",
"@babel/plugin-transform-react-jsx": "7.16.7",
"@babel/plugin-transform-react-jsx-source": "^7.16.7",
"@babel/plugin-transform-runtime": "7.16.7",
"@babel/plugin-transform-shorthand-properties": "7.16.7",
"@babel/plugin-transform-spread": "7.16.7",
"@babel/plugin-transform-template-literals": "7.16.7",
"@babel/preset-env": "7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.1.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.2.3",
"@types/react-redux": "7.0.0",
"jest": "29.3.1",
"jest-environment-jsdom": "^29.5.0",
"jsdom": "^21.1.1",
"prettier": "2.8.8",
"react-redux": "^3.8.1 || ^4.0.0",
"rollup": "2.79.1",
"rollup-plugin-dts": "^6.0.1",
"rollup-plugin-terser": "^5.1.3",
"redux": "4.2.1",
"ts-jest": "29.0.3",
"typescript": "4.9.3"
"redux": "4.2.1"
},
"peerDependencies": {
"openinula": ">=0.1.1"

View File

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details.
*/
import { createPath } from '../utils';
import { createPath, parseRelativePath } from '../utils';
describe('createPath', () => {
describe('given only a pathname', () => {
@ -73,4 +73,36 @@ describe('createPath', () => {
expect(path).toBe('https://google.com?something=cool#section-1');
});
});
describe('parse relative path', () => {
it('from is absolute path', () => {
const url = parseRelativePath('a', '/b/');
expect(url).toBe('/b/a');
const url2 = parseRelativePath('a', '');
expect(url2).toBe('/a');
});
it('to is end with slash', () => {
const url = parseRelativePath('a/', '/b/c/');
expect(url).toBe('/b/c/a/');
const url2 = parseRelativePath('a/', '/b/c');
expect(url2).toBe('/b/a/');
});
it('to contain .. or .', () => {
const url = parseRelativePath('../a/', '/b/c/');
expect(url).toBe('/b/a/');
const url2 = parseRelativePath('../a', '/b/c');
expect(url2).toBe('/a');
const url3 = parseRelativePath('../../a', '/b/c/d/e');
expect(url3).toBe('/b/a');
});
it('to only contain ..', () => {
const url = parseRelativePath('../..', '/b/c/e/');
expect(url).toBe('/b/');
const url2 = parseRelativePath('../..', '/b/c/e');
expect(url2).toBe('/');
});
});
});

View File

@ -66,7 +66,9 @@ export function getBaseHistory<S>(
Object.assign(historyProps, nextState);
}
historyProps.length = browserHistory.length;
const args = { location: historyProps.location, action: historyProps.action };
// 避免location引用相同时setState不触发
const location = Object.assign({}, historyProps.location);
const args = { location: location, action: historyProps.action };
transitionManager.notifyListeners(args);
};
}

View File

@ -57,6 +57,13 @@ export function createHashHistory<S = DefaultStateType>(option: HashHistoryOptio
const pathDecoder = addHeadSlash;
const pathEncoder = hashType === 'slash' ? addHeadSlash : stripHeadSlash;
const startLocation = getHashContent(window.location.href);
const encodeLocation = pathEncoder(startLocation);
// 初始化hash格式不合法时会重定向
if (startLocation !== encodeLocation) {
window.location.replace(stripHash(window.location.href) + '#' + encodeLocation);
}
function getLocation() {
let hashPath = pathDecoder(getHashContent(window.location.hash));
if (basename) {

View File

@ -66,6 +66,8 @@ export function createLocation<S>(current: string | Location, to: To, state?: S,
};
if (!location.pathname) {
location.pathname = pathname ? pathname : '/';
} else if (!location.pathname.startsWith('/')) {
location.pathname = parseRelativePath(location.pathname, pathname);
}
if (location.search && location.search[0] !== '?') {
location.search = '?' + location.search;
@ -154,3 +156,34 @@ function genRandomKey(length: number): () => string {
return Math.random().toString(18).substring(2, end);
};
}
export function parseRelativePath(to: string, from: string) {
if (to.startsWith('/')) {
return to;
}
const formParts = from.split('/');
const toParts = to.split('/');
const lastToPart = toParts[toParts.length - 1];
if (lastToPart === '..' || lastToPart === '.') {
toParts.push('');
}
let index = formParts.length - 1;
let toIndex = 0;
let urlPart: string;
while (toIndex < toParts.length) {
urlPart = toParts[toIndex];
if (urlPart === '.') {
continue;
}
if (urlPart === '..') {
if (index > 1) {
index--;
}
toIndex++;
} else {
break;
}
}
return formParts.slice(0, index).join('/') + '/' + toParts.slice(toIndex).join('/');
}

View File

@ -30,6 +30,7 @@ type NavLinkProps = {
sensitive?: boolean;
className?: string | ((isActive: boolean) => string);
activeClassName?: string;
// compat react-router NavLink props type
[key: string]: any;
} & Omit<LinkProps, 'className'>;

View File

@ -38,7 +38,6 @@ function Router<P extends RouterProps>(props: P) {
pendingLocation.current = arg.location;
});
}
// 模拟componentDidMount和componentWillUnmount
useLayoutEffect(() => {
isMount.current = true;

View File

@ -38,7 +38,7 @@ describe('path lexer Test', () => {
expect(tokens).toStrictEqual([{ type: 'delimiter', value: '/' }]);
});
it('don\'t start with a slash', () => {
it("don't start with a slash", () => {
const func = () => lexer('abc.com');
expect(func).toThrow(Error('Url must start with "/".'));
});

View File

@ -50,12 +50,14 @@ describe('parser test', () => {
const params = parser.parse('/www.a.com/a/b1/c1/d1');
const params1 = parser.parse('/www.a.com/a/b1/c1/');
const params2 = parser.parse('/www.a.com/a/b1/');
const params3 = parser.parse('/www.a.com/a1/b1/');
expect(params!.params).toStrictEqual({ '*': ['b1', 'c1', 'd1'] });
expect(params!.score).toStrictEqual([10, 10, 3, 3, 3]);
expect(params1!.params).toStrictEqual({ '*': ['b1', 'c1'] });
expect(params1!.score).toStrictEqual([10, 10, 3, 3]);
expect(params2!.params).toStrictEqual({ '*': ['b1'] });
expect(params2!.score).toStrictEqual([10, 10, 3]);
expect(params3).toBeNull();
});
it('compile wildcard', function () {
@ -121,7 +123,13 @@ describe('parser test', () => {
it('url without end slash match wildcard', function () {
const parser = createPathParser('/about/', { strictMode: false });
const matched = parser.parse('/about');
expect(matched).toBeNull();
expect(matched).toStrictEqual({
path: '/about/',
url: '/about',
score: [10],
isExact: true,
params: {},
});
});
it('url without end slash match wildcard (strictMode)', function () {
@ -259,7 +267,7 @@ describe('parser test', () => {
});
it('dynamic param with complex regexp pattern', () => {
const parser = createPathParser('/detail/:action([\\da-z]+)', { exact: true });
const parser = createPathParser('/detail/:action([\\da-z]+)', { exact: true, caseSensitive: true });
const res = parser.parse('/detail/a123');
expect(res).toEqual({
isExact: true,
@ -279,11 +287,14 @@ describe('parser test', () => {
it('wildcard param in centre', function () {
const parser = createPathParser('/a/b/*/:c/c');
const res = parser.parse('/a/b/d/x/yy/zzz/abc/c');
const res2 = parser.parse('/a/b/abc/c');
expect(res!.params).toEqual({
'*': ['d', 'x', 'yy', 'zzz'],
c: 'abc',
});
expect(res2).toBe(null);
});
it('support wildcard "*" in end of static path 1', function () {
const parser = createPathParser('/home*');
const res = parser.parse('/homeAbc/a123');

View File

@ -86,7 +86,10 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
const lookToNextDelimiter = (currentIdx: number): boolean => {
let hasOptionalParam = false;
while (currentIdx < tokens.length && tokens[currentIdx].type !== TokenType.Delimiter) {
if (tokens[currentIdx].value === '?' || tokens[currentIdx].value === '*') {
if (
tokens[currentIdx].value === '?' ||
(tokens[currentIdx].value === '*' && tokens[currentIdx].type !== TokenType.WildCard)
) {
hasOptionalParam = true;
}
currentIdx++;
@ -97,12 +100,14 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
const token = tokens[tokenIdx];
const nextToken = tokens[tokenIdx + 1];
switch (token.type) {
case TokenType.Delimiter:
{
case TokenType.Delimiter: {
// 该分隔符后有可选参数则该分割符在匹配时是可选的
const hasOptional = lookToNextDelimiter(tokenIdx + 1);
pattern += `/${hasOptional ? '?' : ''}`;
}
// 该分隔符为最后一个且strictMode===false时该分割符在匹配时是可选的
const isSlashOptional = nextToken === undefined && !strictMode;
pattern += `/${hasOptional || isSlashOptional ? '?' : ''}`;
break;
}
case TokenType.Static:
pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
if (nextToken && nextToken.type === TokenType.Pattern) {
@ -112,8 +117,7 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
}
scores.push(MatchScore.static);
break;
case TokenType.Param:
{
case TokenType.Param: {
// 动态参数支持形如/:param、/:param*、/:param?、/:param(\\d+)的形式
let paramRegexp = '';
if (nextToken) {
@ -136,8 +140,8 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
pattern += paramRegexp ? `(?:${paramRegexp})` : `(${BASE_PARAM_PATTERN})`;
keys.push(token.value);
scores.push(MatchScore.param);
}
break;
}
case TokenType.WildCard:
keys.push(token.value);
pattern += `((?:${BASE_PARAM_PATTERN})${onlyHasWildCard ? '?' : ''}(?:/(?:${BASE_PARAM_PATTERN}))*)`;
@ -215,16 +219,15 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
}
path += params[token.value];
break;
case TokenType.WildCard:
{
case TokenType.WildCard: {
const wildCard = params['*'];
if (wildCard instanceof Array) {
path += wildCard.join('/');
} else {
path += wildCard;
}
}
break;
}
case TokenType.Delimiter:
path += token.value;
break;

125
packages/inula/License Normal file
View File

@ -0,0 +1,125 @@
木兰宽松许可证, 第2版
木兰宽松许可证, 第2版
2020年1月 http://license.coscl.org.cn/MulanPSL2
您对“软件”的复制、使用、修改及分发受木兰宽松许可证第2版“本许可证”的如下条款的约束
0. 定义
“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
“法人实体” 是指提交贡献的机构及其“关联实体”。
“关联实体” 是指对“本许可证”下的行为方而言控制、受控制或与其共同受控制的机构此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
1. 授予版权许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。
2. 授予专利许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。
3. 无商标许可
“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可但您为满足第4条规定的声明义务而必须使用除外。
4. 分发限制
您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
5. 免责声明与责任限制
“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。
6. 语言
“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。
条款结束
如何将木兰宽松许可证第2版应用到您的软件
如果您希望将木兰宽松许可证第2版应用到您的新软件为了方便接收者查阅建议您完成如下三步
1 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字;
2 请您在软件包的一级目录下创建以“LICENSE”为名的文件将整个许可证文本放入该文件中
3 请将如下声明文本放入每个源文件的头部注释中。
Copyright (c) [Year] [name of copyright holder]
[Software Name] 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.
Mulan Permissive Software LicenseVersion 2
Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2)
January 2020 http://license.coscl.org.cn/MulanPSL2
Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions:
0. Definition
Software means the program and related documents which are licensed under this License and comprise all Contribution(s).
Contribution means the copyrightable work licensed by a particular Contributor under this License.
Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License.
Legal Entity means the entity making a Contribution and all its Affiliates.
Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, control means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity.
1. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not.
2. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken.
3. No Trademark License
No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in section 4.
4. Distribution Restriction
You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software.
5. Disclaimer of Warranty and Limitation of Liability
THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW ITS CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
6. Language
THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL.
END OF THE TERMS AND CONDITIONS
How to Apply the Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2) to Your Software
To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps:
Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner;
Create a file named "LICENSE" which contains the whole context of this License in the first directory of your software package;
Attach the statement to the appropriate annotated syntax at the beginning of each source file.
Copyright (c) [Year] [name of copyright holder]
[Software Name] 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.

View File

@ -19,17 +19,17 @@ module.exports = {
rootDir: process.cwd(),
setupFiles: [require.resolve('./__tests__/jest/jestEnvironment.js')],
setupFiles: [require.resolve('./tests/jest/jestEnvironment.js')],
setupFilesAfterEnv: [require.resolve('./__tests__/jest/jestSetting.js')],
setupFilesAfterEnv: [require.resolve('./tests/jest/jestSetting.js')],
testEnvironment: 'jest-environment-jsdom',
testMatch: [
// '<rootDir>/scripts/__tests__/InulaXTest/edgeCases/deepVariableObserver.test.tsx',
// '<rootDir>/scripts/__tests__/InulaXTest/StateManager/StateMap.test.tsx',
'<rootDir>/__tests__/**/*.test.js',
'<rootDir>/__tests__/**/*.test.tsx',
// '<rootDir>/tests/InulaXTest/edgeCases/deepVariableObserver.test.tsx',
// '<rootDir>/tests/InulaXTest/StateManager/StateMap.test.tsx',
'<rootDir>/tests/**/*.test.js',
'<rootDir>/tests/**/*.test.tsx',
],
fakeTimers: { enableGlobally: true },

View File

@ -16,20 +16,20 @@
"scripts": {
"build": "rollup --config ./scripts/rollup/rollup.config.js",
"build-types": "tsc -p tsconfig.build.json || echo \"WARNING: TSC exited with status $?\" && rollup -c ./scripts/rollup/build-types.js",
"build:inula3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js build:horizon3rdLib-dev",
"build:watch": "rollup --watch --config ./scripts/rollup/rollup.config.js",
"debug-test": "yarn test --debug",
"lint": "eslint . --ext .ts --fix",
"test": "jest --config=jest.config.js",
"watch-test": "yarn test --watch --dev"
},
"files": ["build/**/*", "README.md"],
"types": "./build/@types/index.d.ts",
"exports": {
".": {
"default": "./build/index.js"
"default": "./index.js"
},
"./package.json":"./package.json",
"./jsx-runtime": "./build/jsx-runtime.js",
"./jsx-dev-runtime": "./build/jsx-dev-runtime.js"
"./jsx-runtime": "./jsx-runtime.js",
"./jsx-dev-runtime": "./jsx-dev-runtime.js"
}
}

View File

@ -0,0 +1,25 @@
{
"name": "openinula",
"description": "OpenInula is a JavaScript framework library.",
"keywords": [
"openinula"
],
"version": "0.0.4",
"homepage": "",
"bugs": "",
"license": "MulanPSL2",
"main": "./index.js",
"repository": {},
"engines": {
"node": ">=0.10.0"
},
"types": "./@types/index.d.ts",
"exports": {
".": {
"default": "./index.js"
},
"./package.json":"./package.json",
"./jsx-runtime": "./jsx-runtime.js",
"./jsx-dev-runtime": "./jsx-dev-runtime.js"
}
}

View File

@ -101,6 +101,14 @@ function genConfig(mode) {
from: path.join(libDir, '/npm/index.js'),
to: path.join(outDir, 'index.js'),
},
{
from: path.join(__dirname,'..', 'package.json'),
to: path.join(outDir, 'package.json'),
},
{
from: path.join(libDir, 'README.md'),
to: path.join(outDir, 'README.md'),
},
]),
],
};

View File

@ -1,90 +0,0 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
/**
* dom节点赋 VNode
*/
import type { VNode } from '../renderer/Types';
import type { Container, Props } from './DOMOperator';
import { DomComponent, DomText, TreeRoot } from '../renderer/vnode/VNodeTags';
const randomKey = Math.random().toString(16).slice(2);
const INTERNAL_VNODE = `_inula_VNode_${randomKey}`;
const INTERNAL_PROPS = `_inula_Props_${randomKey}`;
const INTERNAL_NONDELEGATEEVENTS = `_inula_nonDelegatedEvents_${randomKey}`;
export const HANDLER_KEY = `_inula_valueChangeHandler_${randomKey}`;
export const EVENT_KEY = `_inula_ev_${randomKey}`;
// 通过 VNode 实例获取 DOM 节点
export function getDom(vNode: VNode): Element | Text | null {
const { tag } = vNode;
if (tag === DomComponent || tag === DomText) {
return vNode.realNode;
}
return null;
}
// 将 VNode 属性相关信息挂到 DOM 对象的特定属性上
export function saveVNode(vNode: VNode, dom: Element | Text | Container): void {
dom[INTERNAL_VNODE] = vNode;
}
// 用 DOM 节点,来找其对应的 VNode 实例
export function getVNode(dom: Node | Container): VNode | null {
const vNode = dom[INTERNAL_VNODE] || (dom as Container)._treeRoot;
if (vNode) {
const { tag } = vNode;
if (tag === DomComponent || tag === DomText || tag === TreeRoot) {
return vNode;
}
}
return null;
}
// 用 DOM 对象,来寻找其对应或者说是最近父级的 vNode
export function getNearestVNode(dom: Node): null | VNode {
let domNode: Node | null = dom;
// 寻找当前节点及其所有祖先节点是否有标记VNODE
while (domNode) {
const vNode = domNode[INTERNAL_VNODE];
if (vNode) {
return vNode;
}
domNode = domNode.parentNode;
}
return null;
}
// 获取 vNode 上的属性相关信息
export function getVNodeProps(dom: Element | Text): Props | null {
return dom[INTERNAL_PROPS] || null;
}
// 将 DOM 属性相关信息挂到 DOM 对象的特定属性上
export function updateVNodeProps(dom: Element | Text, props: Props): void {
dom[INTERNAL_PROPS] = props;
}
export function getNonDelegatedListenerMap(dom: Element | Text): Map<string, EventListener> {
let eventsMap = dom[INTERNAL_NONDELEGATEEVENTS];
if (!eventsMap) {
eventsMap = new Map();
dom[INTERNAL_NONDELEGATEEVENTS] = eventsMap;
}
return eventsMap;
}

View File

@ -1,33 +1,25 @@
/*
* 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.
*/
// /*
// * 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 { saveVNode, updateVNodeProps } from './DOMInternalKeys';
import { createDom } from './utils/DomCreator';
import { getSelectionInfo, resetSelectionRange, SelectionData } from './SelectionRangeHandler';
import { isDocument, shouldAutoFocus } from './utils/Common';
import { controlInputValue } from './valueHandler/ValueChangeHandler';
import { updateTextareaValue } from './valueHandler/TextareaValueHandler';
import { NSS } from './utils/DomCreator';
import { adjustStyleValue } from './DOMPropertiesHandler/StyleHandler';
import type { VNode } from '../renderer/Types';
import { setInitValue, getPropsWithoutValue, updateValue } from './valueHandler';
import { compareProps, setDomProps } from './DOMPropertiesHandler/DOMPropertiesHandler';
import { isNativeElement, validateProps } from './validators/ValidateProps';
import { watchValueChange } from './valueHandler/ValueChangeHandler';
import { DomComponent, DomText } from '../renderer/vnode/VNodeTags';
import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp';
import { getCurrentRoot } from '../renderer/RootStack';
import { type Container, CommonTags } from '../renderer/Types';
export type Props = Record<string, any> & {
autoFocus?: boolean;
children?: any;
@ -37,7 +29,6 @@ export type Props = Record<string, any> & {
style?: { display?: string };
};
export type Container = (Element & { _treeRoot?: VNode | null }) | (Document & { _treeRoot?: VNode | null });
let selectionInfo: null | SelectionData = null;
@ -57,7 +48,7 @@ function getChildNS(parentNS: string | null, tagName: string): string {
// 获取容器
export function getNSCtx(parentNS: string, type: string, dom?: Container): string {
return dom ? getChildNS(dom.namespaceURI ?? null, dom.nodeName) : getChildNS(parentNS, type);
return dom ? getChildNS((dom as Element).namespaceURI ?? null, dom.nodeName) : getChildNS(parentNS, type);
}
export function prepareForSubmit(): void {
@ -69,61 +60,28 @@ export function resetAfterSubmit(): void {
selectionInfo = null;
}
// 创建 DOM 对象
export function newDom(tagName: string, props: Props, parentNamespace: string, vNode: VNode): Element {
export function createElement(tagName: string, props: Props, parentNamespace: string, rootDom: Element): {element: any, props: Props} {
// document取值于treeRoot对应的DOM的ownerDocument。
// 解决在iframe中使用top的inula时inula在创建DOM时用到的document并不是iframe的document而是top中的document的问题。
const rootDom = getCurrentRoot()?.realNode;
const doc = isDocument(rootDom) ? rootDom : rootDom.ownerDocument;
const doc = rootDom instanceof Document ? rootDom : rootDom.ownerDocument;
const dom: Element = createDom(tagName, parentNamespace, doc);
// 将 vNode 节点挂到 DOM 对象上
saveVNode(vNode, dom);
// 将属性挂到 DOM 对象上
updateVNodeProps(dom, props);
return dom;
return {element: dom, props};
}
// 设置节点默认事件、属性
export function initDomProps(dom: Element, tagName: string, rawProps: Props): boolean {
validateProps(tagName, rawProps);
// 获取不包括valuedefaultValue的属性
const props: Record<string, any> = getPropsWithoutValue(tagName, dom, rawProps);
// 初始化DOM属性不包括valuedefaultValue
const isNativeTag = isNativeElement(tagName, props);
setDomProps(dom, props, isNativeTag, true);
if (tagName === 'input' || tagName === 'textarea') {
// 增加监听value和checked的set、get方法
watchValueChange(dom);
export function handleControledElements(target: Element , type: string, props: Props) {
switch (type) {
case 'input':
controlInputValue(<HTMLInputElement>target, props);
break;
case 'textarea':
updateTextareaValue(<HTMLTextAreaElement>target, props);
break;
default:
break;
}
// 设置dom.value值触发受控组件的set方法
setInitValue(tagName, dom, rawProps);
return shouldAutoFocus(tagName, rawProps);
}
// 准备更新之前进行一系列校验 DOM寻找属性差异等准备工作
export function getPropChangeList(
dom: Element,
type: string,
lastRawProps: Props,
nextRawProps: Props
): Record<string, any> {
// 校验两个对象的不同
validateProps(type, nextRawProps);
// 重新定义的属性不需要参与对比被代理的组件需要把这些属性覆盖到props中
const oldProps: Record<string, any> = getPropsWithoutValue(type, dom, lastRawProps);
const newProps: Record<string, any> = getPropsWithoutValue(type, dom, nextRawProps);
return compareProps(oldProps, newProps);
}
export function isTextChild(type: string, props: Props): boolean {
if (type === 'textarea' || type === 'option' || type === 'noscript') {
return true;
@ -140,52 +98,11 @@ export function isTextChild(type: string, props: Props): boolean {
);
}
}
export function newTextDom(text: string, processing: VNode): Text {
const textNode: Text = document.createTextNode(text);
saveVNode(processing, textNode);
return textNode;
export function createText(text: string) {
return document.createTextNode(text);
}
// 提交vNode的类型为DomComponent或者DomText的更新
export function submitDomUpdate(tag: string, vNode: VNode) {
const newProps = vNode.props;
const element: Element | null = vNode.realNode;
if (tag === DomComponent) {
// DomComponent类型
if (element !== null && element !== undefined) {
const type = vNode.type;
const changeList = vNode.changeList;
vNode.changeList = null;
if (changeList !== null) {
saveVNode(vNode, element);
updateVNodeProps(element, newProps);
// 应用diff更新Properties.
// 当一个选中的radio改变名称,浏览器使另一个radio的复选框为false.
if (
type === 'input' &&
newProps.type === 'radio' &&
newProps.name !== null &&
newProps.name !== undefined &&
newProps.checked !== null &&
newProps.checked !== undefined
) {
updateCommonProp(element, 'checked', newProps.checked, true);
}
const isNativeTag = isNativeElement(type, newProps);
setDomProps(element, changeList, isNativeTag, false);
updateValue(type, element, newProps);
}
}
} else if (tag === DomText) {
if (element != null) {
// text类型
element.textContent = newProps;
}
}
}
export function clearText(dom: Element): void {
dom.innerHTML = '';
@ -197,28 +114,29 @@ export function appendChildElement(parent: Element | Container, child: Element |
}
// 插入dom元素
export function insertDomBefore(parent: Element | Container, child: Element | Text, beforeChild: Element | Text) {
export function insertElementBefore(parent: Element | Container, child: Element | Text, beforeChild: Element | Text) {
parent.insertBefore(child, beforeChild);
}
export function removeChildDom(parent: Element | Container, child: Element | Text) {
export function removeChildElement(parent: Element | Container, child: Element | Text) {
parent.removeChild(child);
}
// 隐藏元素
export function hideDom(tag: string, dom: Element | Text) {
if (tag === DomComponent) {
export function hideElement(tag, dom) {
if (tag === CommonTags.ComponentElement) {
(dom as HTMLElement).style.display = 'none';
} else if (tag === DomText) {
} else if (tag === CommonTags.TextElement) {
dom.textContent = '';
}
}
// 不隐藏元素
export function unHideDom(tag: string, dom: Element | Text, props?: Props) {
if (tag === DomComponent) {
(dom as HTMLElement).style.display = adjustStyleValue('display', props?.style?.display ?? '');
} else if (tag === DomText) {
export function unHideElement(tag, dom, props?: Props) {
if (tag === CommonTags.ComponentElement) {
(dom as HTMLElement).style.display = adjustStyleValue('display', props?.style?.display ?? '') as string;
} else if (tag === CommonTags.TextElement) {
dom.textContent = props as any;
}
}

View File

@ -84,14 +84,16 @@ export function setStyles(dom, styles) {
const style = dom.style;
for (let name of Object.keys(styles)) {
const isCustomProperty = name.indexOf('--') == 0;
const styleVal = adjustStyleValue(name, styles[name], isCustomProperty);
const isCssVariable = name.startsWith('--');
const styleVal = adjustStyleValue(name, styles[name], isCssVariable);
if (name === 'float') {
name = 'cssFloat';
}
if (isCustomProperty) {
// 以--开始的样式直接设置即可
if (isCssVariable) {
style.setProperty(name, styleVal);
} else {
// 使用这种赋值方式,浏览器可以将'WebkitLineClamp' 'backgroundColor'分别识别为'-webkit-line-clamp'和'backgroud-color'
style[name] = styleVal;
}
}

View File

@ -13,11 +13,13 @@
* See the Mulan PSL v2 for more details.
*/
import { getPropDetails, PROPERTY_TYPE } from '../validators/PropertiesData';
import { isInvalidValue } from '../validators/ValidateProps';
//kb-tag
import { getPropDetails, PROPERTY_TYPE } from '../../renderer/props/PropertiesData';
import { isInvalidValue } from '../../renderer/props/ValidateProps';
import { getNamespaceCtx } from '../../renderer/ContextSaver';
import { NSS } from '../utils/DomCreator';
import { getDomTag } from '../utils/Common';
import { getTag } from '../../renderer/utils/common';
// 不需要装换的svg属性集合
const svgHumpAttr = new Set();
@ -111,7 +113,7 @@ export function updateCommonProp(dom: Element, attrName: string, value: any, isN
if (!isNativeTag || propDetails === null) {
// 特殊处理svg的属性把驼峰式的属性名称转成'-'
if (getDomTag(dom) === 'svg' || getNamespaceCtx() === NSS.svg) {
if (getTag(dom) === 'svg' || getNamespaceCtx() === NSS.svg) {
if (!svgHumpAttr.has(attrName)) {
attrName = convertToLowerCase(attrName);
}

View File

@ -17,8 +17,8 @@
*
*/
import { getIFrameFocusedDom, isText } from './utils/Common';
import { isElement } from './utils/Common';
import { getIFrameFocusedDom } from './utils/Common';
import { isElement, isText } from '../renderer/utils/common';
type SelectionRange = {
start: number | null;

View File

@ -0,0 +1,141 @@
import { ElementType, HostConfigType } from '../renderer/Types';
import {
createElement as createDom,
clearText,
appendChildElement,
removeChildElement,
insertElementBefore,
hideElement,
unHideElement,
isTextChild,
handleControledElements,
createText,
prepareForSubmit,
resetAfterSubmit,
} from './DOMOperator';
import { setStyles } from './DOMPropertiesHandler/StyleHandler';
import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp';
import { updateValue } from './valueHandler';
import { getPropsWithoutValue } from './valueHandler';
import { watchValueChange } from './valueHandler/ValueChangeHandler';
import { setInitValue } from './valueHandler';
import { InulaDom } from './utils/Interface';
import { updateInputHandlerIfChanged } from './valueHandler/ValueChangeHandler';
export const defaultHostConfig: Partial<HostConfigType> = {
elementConfig: {
common: document.createElement('div'),
text: document.createTextNode(''),
input: document.createElement('input'),
button: document.createElement('button'),
select: document.createElement('select'),
textarea: document.createElement('textarea'),
document: document,
},
addEventListener(element, eventName, handler, isCapture) {
element.addEventListener(eventName, handler, isCapture);
},
removeEventListener(element, eventName, handler) {
element.removeEventListener(eventName, handler);
},
createElement(tagName, props, parentNamespace, rootElement) {
return createDom(tagName, props, parentNamespace, rootElement as Element);
},
createText(text) {
return createText(text) as ElementType;
},
getProps(type, element, props) {
return getPropsWithoutValue(type, element as Element, props);
},
handleControledInputElements(target, type, props) {
return handleControledElements(target as Element, type, props);
},
isTextChild(type, props) {
return isTextChild(type, props);
},
setProps(element, propName, propVal, isNativeTag, isInit) {
if (propName === 'style') {
setStyles(element, propVal);
} else if (propName === 'children') {
// 只处理纯文本子节点其他children在VNode树中处理
const type = typeof propVal;
if (type === 'string' || type === 'number') {
element.textContent = propVal;
}
} else if (propName === 'dangerouslySetInnerHTML') {
element.innerHTML = propVal.__html;
} else if (!isInit || (propVal !== null && propVal !== undefined)) {
updateCommonProp(element as Element, propName, propVal, isNativeTag);
}
},
updateInputValue(type, element, props) {
updateValue(type, element as Element, props);
},
onSubmit(tag, type, element, newProps) {
if (tag === 'Component') {
if (
type === 'input' &&
newProps.type === 'radio' &&
newProps.name !== null &&
newProps.name !== undefined &&
newProps.checked !== null &&
newProps.checked !== undefined
) {
updateCommonProp(element as Element, 'checked', newProps.checked, true);
}
} else {
if (element != null) {
// text类型
element.textContent = newProps;
}
}
},
shouldTriggerChangeEvent(targetElement, elementTag, evtName) {
const {type} = targetElement
if (elementTag === 'select' || (elementTag === 'input' && type === 'file')) {
return evtName === 'change';
} else if (elementTag === 'input' && (type === 'checkbox' || type === 'radio')) {
if (evtName === 'click') {
return updateInputHandlerIfChanged(targetElement);
}
} else if (targetElement.nodeType === this.elementConfig?.input.nodeType) {
if (evtName === 'input' || evtName === 'change') {
return updateInputHandlerIfChanged(targetElement);
}
}
return false;
},
hideElement(tag, element) {
hideElement(tag, element);
},
unHideElement(tag, element, props) {
unHideElement(tag, element, props);
},
clearText(element) {
clearText(element as Element);
},
appendChildElement(parent, child) {
appendChildElement(parent as Element, child as Element | Text);
},
insertElementBefore(parent, child, beforeChild) {
insertElementBefore(parent as Element, child as Element | Text, beforeChild as Element | Text);
},
removeChildElement(parent, child) {
removeChildElement(parent as Element, child as Element | Text);
},
prepareForSubmit() {
prepareForSubmit();
},
resetAfterSubmit() {
resetAfterSubmit();
},
onPropInit(element, tagName, rawProps) {
if (tagName === this.elementConfig?.input.nodeName || tagName === this.elementConfig?.textarea.nodeName) {
// 增加监听value和checked的set、get方法
watchValueChange(element);
}
// 设置dom.value值触发受控组件的set方法
setInitValue(tagName, element as InulaDom, rawProps);
},
};

View File

@ -14,7 +14,6 @@
*/
import { InulaDom } from './Interface';
import { Props } from '../DOMOperator';
/**
* input textarea
@ -49,42 +48,3 @@ export function getIFrameFocusedDom() {
}
return focusedDom;
}
export function isElement(dom) {
return dom.nodeType === 1;
}
export function isText(dom) {
return dom.nodeType === 3;
}
export function isComment(dom) {
return dom.nodeType === 8;
}
export function isDocument(dom) {
return dom.nodeType === 9;
}
export function isDocumentFragment(dom) {
return dom.nodeType === 11;
}
export function getDomTag(dom) {
return dom.nodeName.toLowerCase();
}
export function isInputElement(dom: Element): dom is HTMLInputElement {
return getDomTag(dom) === 'input';
}
const types = ['button', 'input', 'select', 'textarea'];
// button、input、select、textarea、如果有 autoFocus 属性需要focus
export function shouldAutoFocus(tagName: string, props: Props): boolean {
return types.includes(tagName) ? Boolean(props.autoFocus) : false;
}
export function isNotNull(object: any): boolean {
return object !== null && object !== undefined;
}

View File

@ -14,7 +14,8 @@
*/
import { Children } from '../../external/ChildrenUtil';
import { Props } from '../utils/Interface';
// kb-tag
import { Props } from '../../renderer/Types';
// 把 const a = 'a'; <option>gir{a}ffe</option> 转成 giraffe
function concatChildren(children) {

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