Compare commits
24 Commits
API-2.0-de
...
master
Author | SHA1 | Date |
---|---|---|
|
68ad2d3e9c | |
|
70f7bb3f51 | |
|
540c8d582b | |
|
002dc545c2 | |
|
9f1d2fbc56 | |
|
9f5c9bb370 | |
|
af32cb79f9 | |
|
fbc9c11946 | |
|
2047bb27db | |
|
847fbd5bc0 | |
|
d488477ca3 | |
|
c901b953c5 | |
|
22772af364 | |
|
f295549122 | |
|
2c2c3926e7 | |
|
ecaaacb812 | |
|
6688cde7ab | |
|
8a7623d281 | |
|
0375ed95fc | |
|
4a825cec88 | |
|
aa4984f997 | |
|
78f4bce57c | |
|
ebfe1eceb9 | |
|
ec34490202 |
|
@ -8,3 +8,4 @@ pnpm-lock.yaml
|
|||
build
|
||||
/packages/inula-router/connectRouter
|
||||
/packages/inula-router/router
|
||||
.inula-max
|
||||
|
|
85
package.json
85
package.json
|
@ -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": {
|
||||
|
|
|
@ -32,8 +32,8 @@ const generatorType = fs
|
|||
});
|
||||
|
||||
const runGenerator = async (templatePath, { name = '', cwd = process.cwd(), args = {} }) => {
|
||||
let currentPath;
|
||||
return new Promise(resolve => {
|
||||
let currentPath;
|
||||
if (name) {
|
||||
mkdirp.sync(name);
|
||||
currentPath = path.join(cwd, name);
|
||||
|
|
|
@ -14,4 +14,3 @@
|
|||
*/
|
||||
|
||||
declare module 'crequire';
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
|
@ -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()];
|
||||
|
|
|
@ -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>
|
||||
<>
|
||||
<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} />*/}
|
||||
<Example5 />
|
||||
</div>
|
||||
<div className="button">
|
||||
<button onClick={handleChange}>切换语言</button>
|
||||
</div>
|
||||
</IntlProvider>
|
||||
<div className="container">
|
||||
<Example4 locale={locale} messages={message} />
|
||||
<Example5 />
|
||||
</div>
|
||||
<div className="container">
|
||||
<Example6 locale={{ locale }} messages={message} />
|
||||
</div>
|
||||
<div className="button">
|
||||
<button onClick={handleChange}>切换语言</button>
|
||||
</div>
|
||||
</IntlProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import Inula from 'openinula';
|
||||
import { FormattedMessage } from '../../index';
|
||||
|
||||
const Example3 = props => {
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import Inula from 'openinula';
|
||||
import { createIntl } from '../../index';
|
||||
|
||||
const Example4 = props => {
|
||||
|
|
|
@ -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;
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>injectIntl方式测试Demo</h2>
|
||||
<pre>{intl.formatMessage({ id: 'text4' })}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
import { useIntl } from '../../index';
|
||||
|
||||
const Example6Child = (props: any) => {
|
||||
const Example6Child = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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}!',
|
||||
};
|
||||
|
|
|
@ -18,4 +18,5 @@ export default {
|
|||
text2: '欢迎使用国际化组件!',
|
||||
text3: '欢迎使用国际化组件!',
|
||||
text4: '欢迎使用国际化组件!',
|
||||
text5: '渲染一个组件 {testComponent1} {testComponent2}!',
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ import I18nProvider from './src/core/components/I18nProvider';
|
|||
import injectIntl, { I18nContext, InjectProvider } from './src/core/components/InjectI18n';
|
||||
import 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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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": "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
|
@ -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,34 +30,55 @@ 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',
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
extensions,
|
||||
modulesOnly: true,
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
configFile: path.join(__dirname, '/babel.config.js'),
|
||||
extensions,
|
||||
}),
|
||||
typescript({
|
||||
tsconfig: 'tsconfig.json',
|
||||
include: ['./**/*.ts', './**/*.tsx'],
|
||||
}),
|
||||
terser(),
|
||||
],
|
||||
external: ['openinula', 'react', 'react-dom'],
|
||||
});
|
||||
}
|
||||
return {
|
||||
input: entry,
|
||||
output: outputList,
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
extensions,
|
||||
modulesOnly: true,
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
configFile: path.join(__dirname, '/.babelrc'),
|
||||
extensions,
|
||||
babelHelpers: 'runtime',
|
||||
}),
|
||||
typescript({
|
||||
tsconfig: 'tsconfig.json',
|
||||
include: ['./**/*.ts', './**/*.tsx'],
|
||||
}),
|
||||
terser(),
|
||||
],
|
||||
external: ['openinula', 'react', 'react-dom'],
|
||||
};
|
||||
};
|
||||
export default [BuildConfig('dev'), BuildConfig('prod')];
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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,28 +23,31 @@ import { I18nProviderProps } from '../../types/types';
|
|||
* @constructor
|
||||
*/
|
||||
const I18nProvider = (props: I18nProviderProps) => {
|
||||
const { locale, messages, children } = props;
|
||||
const { locale, messages, children, i18n } = props;
|
||||
|
||||
const i18n = useMemo(() => {
|
||||
return createI18nInstance({
|
||||
locale: locale,
|
||||
messages: messages,
|
||||
});
|
||||
}, [locale, messages]);
|
||||
const i18nInstance =
|
||||
i18n ||
|
||||
useMemo(() => {
|
||||
return createI18nInstance({
|
||||
locale: locale,
|
||||
messages: messages,
|
||||
});
|
||||
}, [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>;
|
||||
|
|
|
@ -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 => {
|
||||
isVariantI18n(context);
|
||||
if (ensureContext) {
|
||||
isVariantI18n(context);
|
||||
}
|
||||
|
||||
const i18nProps = {
|
||||
intl: context,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
return {
|
||||
i18n: i18n,
|
||||
formatMessage: i18n.formatMessage.bind(i18n),
|
||||
formatNumber: i18n.formatNumber.bind(i18n),
|
||||
formatDate: i18n.formatDate.bind(i18n),
|
||||
};
|
||||
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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,并返回新的字符串或者字符串数组
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 的元素是完整的匹配结果。
|
||||
* 索引为 1、2、3 等的元素是正则表达式中指定的捕获组的匹配结果。
|
||||
*/
|
||||
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;
|
||||
|
|
|
@ -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, // 匹配内容包含 plural、select 或 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, // 匹配message中是否包含偏移量offest信息
|
||||
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 },
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
throw new Error('If the /u flag is used, all!');
|
||||
} 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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
||||
// value:This is a rich text with a custom component: <1/>
|
||||
const arrays = value.replace(nlReg, '').split(tagReg);
|
||||
|
||||
// 若无InulaNode元素,则返回
|
||||
if (arrays.length === 1) return value;
|
||||
|
||||
const result: any = [];
|
||||
|
||||
const before = arrays.shift();
|
||||
if (before) {
|
||||
result.push(before);
|
||||
}
|
||||
|
||||
for (const [index, children, after] of getElements(arrays)) {
|
||||
let element = elements[index];
|
||||
|
||||
if (!element || (voidElementTags[element.type as string] && children)) {
|
||||
const errorMessage = !element
|
||||
? `Index not declared as ${index} in original translation`
|
||||
: `${element.type} , No child element exists. Please check.`;
|
||||
console.error(errorMessage);
|
||||
|
||||
// 对于异常元素,通过创建<></>来代替,并继续解析现有的子元素和之后的元素,并保证在构建数组时,不会因为缺少元素而导致索引错位。
|
||||
element = createElement(Fragment, {});
|
||||
}
|
||||
|
||||
// 如果存在子元素,则进行递归处理
|
||||
const formattedChildren = children ? formatElements(children, elements) : element.props.children;
|
||||
|
||||
// 更新element 的属性和子元素
|
||||
const clonedElement = cloneElement(element, { key: elementKeyID() }, formattedChildren);
|
||||
result.push(clonedElement);
|
||||
|
||||
if (after) {
|
||||
result.push(after);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从arrays数组中解析出标签元素和其子元素
|
||||
* @param arrays
|
||||
*/
|
||||
function getElements(arrays: string[]) {
|
||||
// 如果 arrays 数组为空,则返回空数组
|
||||
if (!arrays.length) return [];
|
||||
|
||||
/**
|
||||
* pairedIndex: 第一个元素表示配对标签的内容,即 <1>...</1> 形式的标签。
|
||||
* children: 第二个元素表示配对标签内的子元素内容。
|
||||
* unpairedIndex: 第三个元素表示自闭合标签的内容,即 <1/> 形式的标签。
|
||||
* textAfter: 第四个元素表示标签之后的文本内容,即标签后紧跟着的文本。
|
||||
* eg: [undefined,undefined,1,""]
|
||||
*/
|
||||
const [pairedIndex, children, unpairedIndex, textAfter] = arrays.splice(0, 4);
|
||||
|
||||
// 解析当前标签元素和它的子元素,返回一个包含标签索引、子元素和后续文本的数组
|
||||
const currentElement: [number, string, string] = [
|
||||
parseInt(pairedIndex || unpairedIndex), // 解析标签索引,如果是自闭合标签,则使用 unpaired
|
||||
children || '',
|
||||
textAfter || '',
|
||||
];
|
||||
|
||||
// 递归调用 getElements 函数,处理剩余的 arrays 数组
|
||||
const remainingElements = getElements(arrays);
|
||||
|
||||
// 将当前元素和递归处理后的元素数组合并并返回
|
||||
return [currentElement, ...remainingElements];
|
||||
}
|
||||
|
||||
// 对传入富文本元素的位置标志索引
|
||||
function getElementIndex(count = 0, prefix = '') {
|
||||
return function () {
|
||||
return `${prefix}_${count++}`;
|
||||
};
|
||||
}
|
|
@ -18,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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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>3月2日</span>;
|
||||
};
|
||||
const values = {
|
||||
today: <Today />,
|
||||
name: <Name />,
|
||||
age: <Age />,
|
||||
};
|
||||
const formattedMessage = i18n.formatMessage({ id: 'richText' }, values);
|
||||
|
||||
// 渲染格式化后的文本内容
|
||||
const { getByText } = render(<div>{formattedMessage}</div>);
|
||||
|
||||
// 检查文本内容中是否包含自定义组件的内容
|
||||
expect(getByText('my name is tom, and 16 years old!')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return the formatted date and time', () => {
|
||||
const i18n = new I18n({
|
||||
locale: 'fr',
|
||||
});
|
||||
const formattedDateTime = i18n.formatDate('2023-06-06T07:53:54.465Z', {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
expect(typeof formattedDateTime).toBe('string');
|
||||
expect(formattedDateTime).toEqual('mardi 6 juin 2023 à 15:53');
|
||||
});
|
||||
|
||||
it('should return the formatted number', () => {
|
||||
const i18n = new I18n({
|
||||
locale: 'en',
|
||||
});
|
||||
const formattedNumber = i18n.formatNumber(123456.789, { style: 'currency', currency: 'USD' });
|
||||
expect(typeof formattedNumber).toBe('string');
|
||||
expect(formattedNumber).toEqual('$123,456.79');
|
||||
});
|
||||
});
|
|
@ -43,7 +43,7 @@ describe('<FormattedMessage>', () => {
|
|||
);
|
||||
|
||||
setTimeout(() => {
|
||||
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' }, {}));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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":{}}'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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']],
|
||||
};
|
||||
|
|
|
@ -16,26 +16,24 @@
|
|||
</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) {
|
||||
message.innerHTML = '请求成功: ' + JSON.stringify(response.data, null, 2);
|
||||
}).catch(function(error) {
|
||||
if (inulaRequest.isCancel(error)) {
|
||||
message.innerHTML = '请求已被取消: ' + error.message;
|
||||
message.innerHTML = '请求已被取消:' + error.message;
|
||||
} else {
|
||||
message.innerHTML = '请求出错: ' + error.message;
|
||||
message.innerHTML = '请求出错:' + error.message;
|
||||
}
|
||||
});
|
||||
}, 1000)
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -21,16 +21,19 @@ import { babel } from '@rollup/plugin-babel';
|
|||
|
||||
export default {
|
||||
input: './index.ts',
|
||||
output: [{
|
||||
file: 'dist/inulaRequest.js',
|
||||
format: 'umd',
|
||||
exports: 'named',
|
||||
name: 'inulaRequest',
|
||||
sourcemap: false,
|
||||
}, {
|
||||
file: 'dist/inulaRequest.esm-browser.js',
|
||||
format: 'esm',
|
||||
}],
|
||||
output: [
|
||||
{
|
||||
file: 'dist/inulaRequest.js',
|
||||
format: 'umd',
|
||||
exports: 'named',
|
||||
name: 'inulaRequest',
|
||||
sourcemap: false,
|
||||
},
|
||||
{
|
||||
file: 'dist/inulaRequest.esm-browser.js',
|
||||
format: 'esm',
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
|
|
|
@ -15,7 +15,10 @@
|
|||
|
||||
// 检查是否为用户主动请求取消场景
|
||||
function checkCancel(input: any): boolean {
|
||||
return input.name === 'CanceledError' || input.cancelFlag || false;
|
||||
if (input) {
|
||||
return input.name === 'CanceledError' || input.cancelFlag || false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default checkCancel;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 直到结束
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
// 可以计算上传进度
|
||||
onUploadProgress!({ loaded: event.loaded, total: event.total });
|
||||
} else {
|
||||
onUploadProgress!({ loaded: event.loaded, total: totalBytesToUpload });
|
||||
}
|
||||
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!(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,
|
||||
|
|
|
@ -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>>;
|
||||
|
|
|
@ -386,10 +386,16 @@ 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
|
||||
// params 中 value 为数组时需特殊处理,如: { key: [1, 2, 3] } -> key[]=1&key[]=2&key[]=3
|
||||
if (Array.isArray(obj[key])) {
|
||||
let urlPart = '';
|
||||
obj[key].forEach((value: string) => {
|
||||
|
@ -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,7 +427,10 @@ function getNormalizedValue(value: string | any[] | boolean | null | number): st
|
|||
}
|
||||
|
||||
function isIE(): boolean {
|
||||
return /MSIE|Trident/.test(window.navigator.userAgent);
|
||||
if (typeof window !== 'undefined') {
|
||||
return /MSIE|Trident/.test(window.navigator.userAgent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getObjectByArray(arr: any[]): Record<string, any> {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
|
|
|
@ -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('/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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('/');
|
||||
}
|
||||
|
|
|
@ -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'>;
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ function Router<P extends RouterProps>(props: P) {
|
|||
pendingLocation.current = arg.location;
|
||||
});
|
||||
}
|
||||
|
||||
// 模拟componentDidMount和componentWillUnmount
|
||||
useLayoutEffect(() => {
|
||||
isMount.current = true;
|
||||
|
|
|
@ -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 "/".'));
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -22,12 +22,12 @@ describe('test for utils', () => {
|
|||
expect(generated).toBe('/www.a.com/b/c');
|
||||
});
|
||||
|
||||
it('parse score compare1', function () {
|
||||
it('parse score compare1', function() {
|
||||
const res = [[5], [10], [10, 5]].sort((a, b) => scoreCompare(a, b));
|
||||
expect(res).toStrictEqual([[10, 5], [10], [5]]);
|
||||
});
|
||||
|
||||
it('parse score compare2', function () {
|
||||
it('parse score compare2', function() {
|
||||
const res = [[10], [10], [10, 5]].sort((a, b) => scoreCompare(a, b));
|
||||
expect(res).toStrictEqual([[10, 5], [10], [10]]);
|
||||
});
|
||||
|
|
|
@ -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:
|
||||
{
|
||||
const hasOptional = lookToNextDelimiter(tokenIdx + 1);
|
||||
pattern += `/${hasOptional ? '?' : ''}`;
|
||||
}
|
||||
case TokenType.Delimiter: {
|
||||
// 该分隔符后有可选参数则该分割符在匹配时是可选的
|
||||
const hasOptional = lookToNextDelimiter(tokenIdx + 1);
|
||||
// 该分隔符为最后一个且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,32 +117,31 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
|
|||
}
|
||||
scores.push(MatchScore.static);
|
||||
break;
|
||||
case TokenType.Param:
|
||||
{
|
||||
// 动态参数支持形如/:param、/:param*、/:param?、/:param(\\d+)的形式
|
||||
let paramRegexp = '';
|
||||
if (nextToken) {
|
||||
switch (nextToken.type) {
|
||||
case TokenType.LBracket:
|
||||
// 跳过当前Token和左括号
|
||||
tokenIdx += 2;
|
||||
while (tokens[tokenIdx].type !== TokenType.RBracket) {
|
||||
paramRegexp += tokens[tokenIdx].value;
|
||||
tokenIdx++;
|
||||
}
|
||||
paramRegexp = `(${paramRegexp})`;
|
||||
break;
|
||||
case TokenType.Pattern:
|
||||
case TokenType.Param: {
|
||||
// 动态参数支持形如/:param、/:param*、/:param?、/:param(\\d+)的形式
|
||||
let paramRegexp = '';
|
||||
if (nextToken) {
|
||||
switch (nextToken.type) {
|
||||
case TokenType.LBracket:
|
||||
// 跳过当前Token和左括号
|
||||
tokenIdx += 2;
|
||||
while (tokens[tokenIdx].type !== TokenType.RBracket) {
|
||||
paramRegexp += tokens[tokenIdx].value;
|
||||
tokenIdx++;
|
||||
paramRegexp += `(${nextToken.value === '*' ? '.*' : BASE_PARAM_PATTERN})${nextToken.value}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
paramRegexp = `(${paramRegexp})`;
|
||||
break;
|
||||
case TokenType.Pattern:
|
||||
tokenIdx++;
|
||||
paramRegexp += `(${nextToken.value === '*' ? '.*' : BASE_PARAM_PATTERN})${nextToken.value}`;
|
||||
break;
|
||||
}
|
||||
pattern += paramRegexp ? `(?:${paramRegexp})` : `(${BASE_PARAM_PATTERN})`;
|
||||
keys.push(token.value);
|
||||
scores.push(MatchScore.param);
|
||||
}
|
||||
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:
|
||||
{
|
||||
const wildCard = params['*'];
|
||||
if (wildCard instanceof Array) {
|
||||
path += wildCard.join('/');
|
||||
} else {
|
||||
path += 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;
|
||||
|
|
|
@ -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 License,Version 2
|
||||
Mulan Permissive Software License,Version 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 IT’S 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 License,Version 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.
|
|
@ -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 },
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
},
|
||||
]),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
||||
// 获取不包括value,defaultValue的属性
|
||||
const props: Record<string, any> = getPropsWithoutValue(tagName, dom, rawProps);
|
||||
|
||||
// 初始化DOM属性(不包括value,defaultValue)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue