Match-id-bfcb7929ddfbd97f026ce9f735b5dce734781e26
This commit is contained in:
parent
e143aeba6d
commit
3d2b561616
|
@ -1,74 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openGauss 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'prettier',
|
|
||||||
],
|
|
||||||
root: true,
|
|
||||||
|
|
||||||
plugins: ['jest', 'no-for-of-loops', 'no-function-declare-after-return', 'react', '@typescript-eslint'],
|
|
||||||
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 8,
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
modules: true,
|
|
||||||
experimentalObjectRestSpread: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
jest: true,
|
|
||||||
node: true,
|
|
||||||
es6: true,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
||||||
'@typescript-eslint/no-empty-function': 'off',
|
|
||||||
semi: ['warn', 'always'],
|
|
||||||
quotes: ['warn', 'single'],
|
|
||||||
'accessor-pairs': 'off',
|
|
||||||
'brace-style': ['error', '1tbs'],
|
|
||||||
'func-style': ['warn', 'declaration', { allowArrowFunctions: true }],
|
|
||||||
'max-lines-per-function': 'off',
|
|
||||||
'object-curly-newline': 'off',
|
|
||||||
// 尾随逗号
|
|
||||||
'comma-dangle': ['error', 'only-multiline'],
|
|
||||||
|
|
||||||
'no-constant-condition': 'off',
|
|
||||||
'no-for-of-loops/no-for-of-loops': 'error',
|
|
||||||
'no-function-declare-after-return/no-function-declare-after-return': 'error',
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
isDev: true,
|
|
||||||
isTest: true,
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['scripts/tests/**/*.js'],
|
|
||||||
globals: {
|
|
||||||
container: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,15 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
printWidth: 120, // 一行120字符数,如果超过会进行换行
|
|
||||||
tabWidth: 2, // tab等2个空格
|
|
||||||
useTabs: false, // 用空格缩进行
|
|
||||||
semi: true, // 行尾使用分号
|
|
||||||
singleQuote: true, // 字符串使用单引号
|
|
||||||
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
|
|
||||||
jsxSingleQuote: false, // 在JSX中使用双引号
|
|
||||||
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
|
|
||||||
bracketSpacing: true, // 对象的括号间增加空格
|
|
||||||
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
|
|
||||||
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
|
|
||||||
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
|
|
||||||
endOfLine: 'lf', // 仅限换行(\n)
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Inula-Intl
|
|
||||||
|
|
||||||
Inula-intl 是 inula 提供的生态组件,主要提供了国际化功能,涵盖了基本的国际化组件和钩子函数,便于用户构建具备国际化能力的前端界面。在 Inula-intl 中使用国际化时,无论是组件或者 Hooks,其目的就是获取当前应用程序的国际化实例,该实例提供了处理多语言文本、日期、时间等功能。
|
|
|
@ -1,26 +0,0 @@
|
||||||
const {preset} = require("./jest.config");
|
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
'@babel/preset-env',
|
|
||||||
{
|
|
||||||
targets: {
|
|
||||||
browsers: ['> 1%', 'last 2 versions', 'not ie <= 8'],
|
|
||||||
node: 'current',
|
|
||||||
},
|
|
||||||
useBuiltIns: 'usage',
|
|
||||||
corejs: 3,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'@babel/preset-typescript',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@babel/preset-react",
|
|
||||||
{
|
|
||||||
"runtime": "automatic",
|
|
||||||
"importSource": "inulajs"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Inula, { useState } from 'inulajs';
|
|
||||||
import { IntlProvider } from "../index";
|
|
||||||
import zh from "./locale/zh";
|
|
||||||
import en from "./locale/en";
|
|
||||||
import Example1 from "./components/Example1";
|
|
||||||
import Example2 from "./components/Example2";
|
|
||||||
import Example3 from "./components/Example3";
|
|
||||||
import Example4 from "./components/Example4";
|
|
||||||
import Example5 from "./components/Example5";
|
|
||||||
import Example6 from "./components/Example6";
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
const [locale, setLocale] = useState('zh');
|
|
||||||
const handleChange = () => {
|
|
||||||
locale === 'zh' ? setLocale('en') : setLocale('zh');
|
|
||||||
};
|
|
||||||
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/>
|
|
||||||
</div>
|
|
||||||
<div className='container'>
|
|
||||||
<Example4 locale={locale} messages={message}/>
|
|
||||||
<Example5/>
|
|
||||||
<Example6 locale={{ locale }} messages={message}/>
|
|
||||||
</div>
|
|
||||||
<div className='button'>
|
|
||||||
<button onClick={handleChange}>切换语言</button>
|
|
||||||
</div>
|
|
||||||
</IntlProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Inula from "inulajs";
|
|
||||||
import { useIntl } from "../../index";
|
|
||||||
|
|
||||||
const Example1 = () => {
|
|
||||||
const { i18n } = useIntl();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<h2>useIntl方式测试Demo</h2>
|
|
||||||
<pre>{i18n.formatMessage({ id: 'text1' })}</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Example1;
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import Inula from "inulajs";
|
|
||||||
import { FormattedMessage } from "../../index";
|
|
||||||
|
|
||||||
const Example2= () => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<h2>FormattedMessage方式测试Demo</h2>
|
|
||||||
<pre>
|
|
||||||
<FormattedMessage id='text2'/>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Example2;
|
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Inula from 'inulajs';
|
|
||||||
import { FormattedMessage } from "../../index";
|
|
||||||
|
|
||||||
const Example3 = (props) => {
|
|
||||||
const { locale, setLocale } = props;
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<h2>FormattedMessage方式测试Demo</h2>
|
|
||||||
<pre>
|
|
||||||
<button className="testButton" onClick={() => {
|
|
||||||
setLocale(locale === 'zh' ? 'en' : 'zh')
|
|
||||||
}}>
|
|
||||||
<FormattedMessage id={'button'}/>
|
|
||||||
</button>
|
|
||||||
<br/>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Example3;
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Inula from "inulajs";
|
|
||||||
import { createIntl } from "../../index";
|
|
||||||
|
|
||||||
const Example4 = (props) => {
|
|
||||||
// 受渲染时机影响,createIntl方式需控制时序,否则慢一拍
|
|
||||||
const intl = createIntl({ ...props });
|
|
||||||
const msg = intl.formatMessage({ id: 'text3' });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<h2>createIntl方式测试Demo</h2>
|
|
||||||
<pre>{msg}</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Example4;
|
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Inula, { Component } from 'inulajs';
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(Example5);
|
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Inula from "inulajs";
|
|
||||||
import { createIntl, createIntlCache, RawIntlProvider } from "../../index";
|
|
||||||
import Example6Child from "./Example6Child";
|
|
||||||
|
|
||||||
const Example6 = (props: any) => {
|
|
||||||
|
|
||||||
const { locale, messages } = props;
|
|
||||||
|
|
||||||
const cache = createIntlCache();
|
|
||||||
let i18n = createIntl(
|
|
||||||
{ locale: locale, messages: messages },
|
|
||||||
cache
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RawIntlProvider value={i18n}>
|
|
||||||
<Example6Child/>
|
|
||||||
</RawIntlProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Example6;
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useIntl } from "../../index";
|
|
||||||
|
|
||||||
const Example6Child = (props: any) => {
|
|
||||||
|
|
||||||
const {formatMessage} = useIntl();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<h2>RawIntlProvider方式测试Demo</h2>
|
|
||||||
<pre>{formatMessage({ id: 'text4' })}</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Example6Child;
|
|
|
@ -1,102 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Inula-Intl API Test</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 80px;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 50px;
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 0 5px #aaa;
|
|
||||||
width: 400px;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 20px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
box-shadow: 0 0 15px #aaa;
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 24px;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card pre {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
border-radius: 5px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testButton {
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #007bff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,15 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import * as Inula from 'inulajs';
|
|
||||||
import App from './App'
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
Inula.render(
|
|
||||||
<>
|
|
||||||
<App/>
|
|
||||||
</>,
|
|
||||||
document.querySelector('#root') as any
|
|
||||||
)
|
|
||||||
}
|
|
||||||
render();
|
|
|
@ -1,11 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
button: 'Welcome to the Horizon-Intl component!',
|
|
||||||
text1: 'Welcome to the Horizon-Intl component!',
|
|
||||||
text2: 'Welcome to the Horizon-Intl component!',
|
|
||||||
text3: 'Welcome to the Horizon-Intl component!',
|
|
||||||
text4: 'Welcome to the Horizon-Intl component!',
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
button: '欢迎使用国际化组件!',
|
|
||||||
text1: '欢迎使用国际化组件!',
|
|
||||||
text2: '欢迎使用国际化组件!',
|
|
||||||
text3: '欢迎使用国际化组件!',
|
|
||||||
text4: '欢迎使用国际化组件!',
|
|
||||||
};
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import DateTimeFormatter from './src/format/fomatters/DateTimeFormatter';
|
|
||||||
import NumberFormatter from './src/format/fomatters/NumberFormatter';
|
|
||||||
import I18n from './src/core/I18n';
|
|
||||||
import createI18nCache from './src/format/cache/cache';
|
|
||||||
import FormattedMessage from './src/core/components/FormattedMessage';
|
|
||||||
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';
|
|
||||||
// 函数API
|
|
||||||
export {
|
|
||||||
I18n,
|
|
||||||
createI18nCache as createIntlCache,
|
|
||||||
createI18n as createIntl,
|
|
||||||
DateTimeFormatter,
|
|
||||||
NumberFormatter,
|
|
||||||
useI18n as useIntl,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 组件
|
|
||||||
export {
|
|
||||||
FormattedMessage,
|
|
||||||
I18nContext,
|
|
||||||
I18nProvider as IntlProvider,
|
|
||||||
injectIntl as injectIntl,
|
|
||||||
InjectProvider as RawIntlProvider,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 用于定义文本
|
|
||||||
export function defineMessages<K extends keyof any, T = MessageDescriptor, U = Record<K, T>>(msgs: U): U {
|
|
||||||
return msgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defineMessage<T>(msg: T): T {
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InjectedIntlProps {
|
|
||||||
intl: InjectedIntl;
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
coverageDirectory: 'coverage',
|
|
||||||
resetModules: true,
|
|
||||||
preset: 'ts-jest/presets/js-with-ts',
|
|
||||||
rootDir: process.cwd(),
|
|
||||||
testMatch: [
|
|
||||||
'<rootDir>/tests/**/*.test.[jt]s?(x)'
|
|
||||||
],
|
|
||||||
moduleFileExtensions: ['ts', 'js', 'jsx', 'tsx'],
|
|
||||||
moduleDirectories: ['node_modules', 'src'],
|
|
||||||
moduleNameMapper: {
|
|
||||||
'^@/(.*)$': '<rootDir>/tests/$1',
|
|
||||||
},
|
|
||||||
transform: {
|
|
||||||
"^.+\\.(ts|js|jsx|tsx)$": "babel-jest"
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
"tsconfig": "tsconfig.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
testEnvironment: 'jsdom',
|
|
||||||
};
|
|
|
@ -1,63 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Inula-intl",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "",
|
|
||||||
"main": "build/intl.umd.js",
|
|
||||||
"type": "commonjs",
|
|
||||||
"types": "build/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"demo-serve": "webpack serve --mode=development",
|
|
||||||
"rollup-build": "rollup --config rollup.config.js",
|
|
||||||
"test": "jest --config jest.config.js",
|
|
||||||
"test-c": "jest --coverage"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://szv-open.codehub.huawei.com/innersource/fenghuang/horizon/horizon-core.git"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"build",
|
|
||||||
"example"
|
|
||||||
],
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"inulajs": "^0.0.11"
|
|
||||||
},
|
|
||||||
"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",
|
|
||||||
"@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",
|
|
||||||
"core-js": "3.31.0",
|
|
||||||
"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",
|
|
||||||
"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",
|
|
||||||
"ts-node": "10.9.1",
|
|
||||||
"typescript": "4.9.3",
|
|
||||||
"webpack": "^5.81.0",
|
|
||||||
"webpack-cli": "^5.1.4",
|
|
||||||
"webpack-dev-server": "^4.13.3",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
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';
|
|
||||||
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
const entry = path.join(__dirname, '/index.ts');
|
|
||||||
|
|
||||||
const output = path.join(__dirname, '/build');
|
|
||||||
|
|
||||||
const extensions = ['.js', '.ts', '.tsx'];
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: entry,
|
|
||||||
output: [
|
|
||||||
{
|
|
||||||
file: path.resolve(output, 'intl.umd.js'),
|
|
||||||
sourcemap: 'inline',
|
|
||||||
name: 'I18n',
|
|
||||||
format: 'umd',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
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:[
|
|
||||||
"inulajs",
|
|
||||||
"react",
|
|
||||||
"react-dom"
|
|
||||||
]
|
|
||||||
};
|
|
|
@ -1,68 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* \\(?:u\{[a-fA-F0-9]+}) 匹配形如 \u{0020} 的 Unicode 转义字符。
|
|
||||||
* \\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;
|
|
||||||
|
|
||||||
// Horizon需要被保留静态常量
|
|
||||||
export const HORIZON_STATICS = {
|
|
||||||
childContextTypes: true,
|
|
||||||
contextType: true,
|
|
||||||
contextTypes: true,
|
|
||||||
defaultProps: true,
|
|
||||||
displayName: true,
|
|
||||||
getDefaultProps: true,
|
|
||||||
getDerivedStateFromError: true,
|
|
||||||
getDerivedStateFromProps: true,
|
|
||||||
mixins: true,
|
|
||||||
propTypes: true,
|
|
||||||
type: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// JavaScript 需要被保留原生静态属性
|
|
||||||
export const NATIVE_STATICS = {
|
|
||||||
name: true,
|
|
||||||
length: true,
|
|
||||||
prototype: true,
|
|
||||||
caller: true,
|
|
||||||
callee: true,
|
|
||||||
arguments: true,
|
|
||||||
arity: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Horizon ForwardRef 组件的静态属性需要被保留
|
|
||||||
export const HORIZON_FORWARD_REF_STATICS = {
|
|
||||||
vtype: true,
|
|
||||||
render: true,
|
|
||||||
defaultProps: true,
|
|
||||||
key: true,
|
|
||||||
type: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// React ForwardRef 组件的静态属性需要被保留
|
|
||||||
export const REACT_FORWARD_REF_STATICS = {
|
|
||||||
$$typeof: true, // horizon 'vtype': true
|
|
||||||
render: true, // render
|
|
||||||
defaultProps: true, // props
|
|
||||||
displayName: true,
|
|
||||||
propTypes: true, // type: type,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FORWARD_REF_STATICS = {...HORIZON_FORWARD_REF_STATICS, ...REACT_FORWARD_REF_STATICS};
|
|
||||||
|
|
||||||
// Horizon Memo 组件的静态属性需要被保留
|
|
||||||
export const HORIZON_MEMO_STATICS = {
|
|
||||||
vtype: true, // horizon 'vtype': true
|
|
||||||
compare: true,
|
|
||||||
defaultProps: true,
|
|
||||||
type: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 默认复数规则
|
|
||||||
export const DEFAULT_PLURAL_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import EventDispatcher from '../utils/eventListener';
|
|
||||||
import DateTimeFormatter from "../format/fomatters/DateTimeFormatter";
|
|
||||||
import NumberFormatter from "../format/fomatters/NumberFormatter";
|
|
||||||
import { getFormatMessage } from '../format/getFormatMessage';
|
|
||||||
import { I18nProps, MessageDescriptor, MessageOptions } from '../types/interfaces';
|
|
||||||
import { Locale, Locales, Messages, AllLocaleConfig, AllMessages, LocaleConfig, Error, Events } from '../types/types';
|
|
||||||
|
|
||||||
export class I18n extends EventDispatcher<Events> {
|
|
||||||
public locale: Locale;
|
|
||||||
public locales: Locales;
|
|
||||||
private readonly _localeConfig: AllLocaleConfig;
|
|
||||||
private readonly allMessages: AllMessages;
|
|
||||||
public readonly error?: Error;
|
|
||||||
public readonly useMemorize?: boolean;
|
|
||||||
|
|
||||||
constructor(props: I18nProps) {
|
|
||||||
super();
|
|
||||||
this.locale = 'en';
|
|
||||||
this.locales = this.locale || '';
|
|
||||||
this.allMessages = {};
|
|
||||||
this._localeConfig = {};
|
|
||||||
this.error = props.error;
|
|
||||||
|
|
||||||
this.loadMessage(props.messages);
|
|
||||||
|
|
||||||
if (props.localeConfig) {
|
|
||||||
this.loadLocaleConfig(props.localeConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.locale || props.locales) {
|
|
||||||
this.changeLanguage(props.locale!, props.locales);
|
|
||||||
}
|
|
||||||
this.formatMessage = this.formatMessage.bind(this);
|
|
||||||
this.formatDate = this.formatDate.bind(this);
|
|
||||||
this.formatNumber = this.formatNumber.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
get messages(): string | Messages | AllMessages {
|
|
||||||
if (this.locale in this.allMessages) {
|
|
||||||
return this.allMessages[this.locale] ?? {};
|
|
||||||
} else {
|
|
||||||
return this.allMessages ?? {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get localeConfig(): LocaleConfig {
|
|
||||||
return this._localeConfig[this.locale] ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
setLocaleConfig(locale: Locale, localeData: LocaleConfig) {
|
|
||||||
if (this._localeConfig[locale]) {
|
|
||||||
Object.assign(this._localeConfig, localeData);
|
|
||||||
} else {
|
|
||||||
this._localeConfig[locale] = localeData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将热语言环境的本地化数据加载
|
|
||||||
loadLocaleConfig(localeOrAllData: Locale | AllLocaleConfig, localeConfig?: LocaleConfig) {
|
|
||||||
if (localeConfig) {
|
|
||||||
this.setLocaleConfig(localeOrAllData as Locale, localeConfig);
|
|
||||||
} else {
|
|
||||||
Object.keys(localeOrAllData).forEach(locale => {
|
|
||||||
this.setLocaleConfig(locale, localeOrAllData[locale]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.emit('change');
|
|
||||||
}
|
|
||||||
|
|
||||||
setMessage(locale: Locale, messages: Messages) {
|
|
||||||
if (this.allMessages[locale]) {
|
|
||||||
Object.assign(this.allMessages[locale], messages);
|
|
||||||
} else {
|
|
||||||
this.allMessages[locale] = messages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载messages
|
|
||||||
loadMessage(localeOrMessages: Locale | AllMessages | undefined, messages?: Messages) {
|
|
||||||
if (messages) {
|
|
||||||
//当 message 为空的时候,加载单一的message信息
|
|
||||||
this.setMessage(localeOrMessages as string, messages);
|
|
||||||
} else {
|
|
||||||
// 加载多对locale-message信息
|
|
||||||
localeOrMessages &&
|
|
||||||
Object.keys(localeOrMessages!).forEach(locale => this.setMessage(locale, localeOrMessages![locale]));
|
|
||||||
}
|
|
||||||
this.emit('change');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 改变当前的语言环境
|
|
||||||
changeLanguage(locale: Locale, locales?: Locales) {
|
|
||||||
this.locale = locale;
|
|
||||||
if (locales) {
|
|
||||||
this.locales = locales;
|
|
||||||
}
|
|
||||||
this.emit('change');
|
|
||||||
}
|
|
||||||
|
|
||||||
formatMessage(
|
|
||||||
id: MessageDescriptor | string,
|
|
||||||
values: Object | undefined = {},
|
|
||||||
{ message, context, formatOptions, useMemorize}: MessageOptions = {},
|
|
||||||
) {
|
|
||||||
return getFormatMessage(this, id, values, { message, context, formatOptions, useMemorize});
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDate(value: string | Date, formatOptions?: Intl.DateTimeFormatOptions): string {
|
|
||||||
const dateTimeFormatter = new DateTimeFormatter(this.locale || this.locales, formatOptions, this.useMemorize);
|
|
||||||
return dateTimeFormatter.dateTimeFormat(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
formatNumber(value: number, formatOptions?: Intl.NumberFormatOptions): string {
|
|
||||||
const numberFormatter = new NumberFormatter(this.locale || this.locales, formatOptions, this.useMemorize);
|
|
||||||
return numberFormatter.numberFormat(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default I18n;
|
|
||||||
|
|
||||||
export function createI18nInstance(i18nProps: I18nProps = {}): I18n {
|
|
||||||
return new I18n(i18nProps);
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import Horizon, { Children, Fragment } from 'inulajs';
|
|
||||||
import { FormattedMessageProps } from '../../types/interfaces';
|
|
||||||
import useI18n from '../hook/useI18n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FormattedMessage组件,接收一个消息键作为属性,并根据当前选择的语言环境,从对应的翻译资源中获取相应的消息文本,并可选地对文本进行格式化。
|
|
||||||
* @param props
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function FormattedMessage(props: FormattedMessageProps) {
|
|
||||||
const { i18n } = useI18n();
|
|
||||||
const { id, values, messages, formatOptions, context, tagName: TagName = Fragment, children, comment, useMemorize }: any = props;
|
|
||||||
|
|
||||||
const formatMessageOptions = {
|
|
||||||
comment,
|
|
||||||
messages,
|
|
||||||
context,
|
|
||||||
useMemorize,
|
|
||||||
formatOptions,
|
|
||||||
};
|
|
||||||
|
|
||||||
let formattedMessage = i18n.formatMessage(id, values, formatMessageOptions);
|
|
||||||
|
|
||||||
if (typeof children === 'function') {
|
|
||||||
const childNodes = Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage];
|
|
||||||
return children(childNodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TagName) {
|
|
||||||
return (
|
|
||||||
<TagName>{Children.toArray(formattedMessage)}</TagName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>{formattedMessage}</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FormattedMessage;
|
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import Horizon, {useRef, useState, useEffect, useMemo, Component} from 'inulajs';
|
|
||||||
import utils from '../../utils/utils';
|
|
||||||
import { InjectProvider } from './InjectI18n';
|
|
||||||
import { I18nProviderProps} from '../../types/interfaces';
|
|
||||||
import I18n, {createI18nInstance} from "../I18n";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用于为应用程序提供国际化的格式化功能,管理程序中的语言文本信息和本地化资源信息
|
|
||||||
* @param props
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
const I18nProvider = (props: I18nProviderProps)=> {
|
|
||||||
const { locale, messages, children } = props;
|
|
||||||
|
|
||||||
const 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);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleChange = () => {
|
|
||||||
if (localeRef.current !== i18n.locale) {
|
|
||||||
localeRef.current = i18n.locale;
|
|
||||||
setContext(i18n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let removeListener = i18n.on('change', handleChange);
|
|
||||||
|
|
||||||
// 手动触发一次 handleChange,以确保 context 的正确性
|
|
||||||
handleChange();
|
|
||||||
|
|
||||||
// 在组件卸载时取消事件监听
|
|
||||||
return () => {
|
|
||||||
removeListener();
|
|
||||||
};
|
|
||||||
}, [i18n]);
|
|
||||||
|
|
||||||
// 提供一个Provider组件
|
|
||||||
return <InjectProvider value={context}>{children}</InjectProvider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default I18nProvider;
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import Horizon , { createContext, forwardRef } from 'inulajs';
|
|
||||||
import { isVariantI18n } from '../../utils/utils';
|
|
||||||
import copyStaticProps from '../../utils/copyStaticProps';
|
|
||||||
import { InjectOptions } from '../../types/interfaces';
|
|
||||||
import I18n from "../I18n";
|
|
||||||
|
|
||||||
// 创建国际化组件对象上下文
|
|
||||||
export const I18nContext : any = createContext<I18n>(null as any);
|
|
||||||
const { Consumer, Provider } = I18nContext;
|
|
||||||
export const InjectProvider = Provider;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用于实现国际化的高阶组件,将国际化功能注入到组件中,使组件能够使用国际化的本文格式化功能
|
|
||||||
* @param Component
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
function injectI18n(Component, options?: InjectOptions): any {
|
|
||||||
const {
|
|
||||||
isUsingForwardRef = false, // 默认不使用
|
|
||||||
} = options || {};
|
|
||||||
|
|
||||||
// 定义一个名为 WrappedI18n 的函数组件,接收传入组件的 props 和 forwardedRef,返回传入组件并注入 i18n
|
|
||||||
const WrappedI18n = props => (
|
|
||||||
<Consumer>
|
|
||||||
{context => {
|
|
||||||
isVariantI18n(context);
|
|
||||||
|
|
||||||
const i18nProps = {
|
|
||||||
intl: context,
|
|
||||||
formatMessage: context.formatMessage,
|
|
||||||
formatDate: context.DateTimeFormat,
|
|
||||||
formatNumber: context.NumberFormat,
|
|
||||||
};
|
|
||||||
return <Component {...props} {...i18nProps} ref={isUsingForwardRef ? props.forwardedRef : null} />;
|
|
||||||
}}
|
|
||||||
</Consumer>
|
|
||||||
);
|
|
||||||
|
|
||||||
WrappedI18n.WrappedComponent = Component;
|
|
||||||
|
|
||||||
// 通过copyStatics方法,复制组件中的静态属性
|
|
||||||
return copyStaticProps(
|
|
||||||
isUsingForwardRef ?
|
|
||||||
forwardRef((props, ref) => <WrappedI18n {...props} forwardedRef={ref} />) :
|
|
||||||
WrappedI18n,
|
|
||||||
Component
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectI18n;
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import {I18nCache, I18nProviderProps} from '../types/interfaces';
|
|
||||||
import I18n, {createI18nInstance} from './I18n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* createI18n hook函数,用于创建国际化i8n实例,以进行相关的数据操作
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const createI18n = (config: I18nProviderProps, cache?: I18nCache): I18n => {
|
|
||||||
const { locale, defaultLocale, messages } = config;
|
|
||||||
return createI18nInstance({
|
|
||||||
locale: locale || defaultLocale || 'en',
|
|
||||||
messages: messages,
|
|
||||||
useMemorize: !!cache,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createI18n;
|
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import Inula, { useContext } from 'inulajs';
|
|
||||||
import utils from '../../utils/utils';
|
|
||||||
import { I18nContext } from '../components/InjectI18n';
|
|
||||||
import I18n from "../I18n";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* useI18n hook,与Horizon组件一起使用。
|
|
||||||
* 使用useI18n钩子函数可以更方便地在函数组件中进行国际化操作
|
|
||||||
*/
|
|
||||||
function useI18n() {
|
|
||||||
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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useI18n;
|
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { UNICODE_REG } from '../constants';
|
|
||||||
import { CompiledMessage, Locale, LocaleConfig, Locales } from '../types/types';
|
|
||||||
import generateFormatters from './generateFormatters';
|
|
||||||
import { FormatOptions } from '../types/interfaces';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取翻译结果
|
|
||||||
*/
|
|
||||||
class Translation {
|
|
||||||
private readonly compiledMessage: CompiledMessage;
|
|
||||||
private readonly locale: Locale;
|
|
||||||
private readonly locales: Locales;
|
|
||||||
private readonly localeConfig: Record<string, any>;
|
|
||||||
private readonly useMemorize?: boolean;
|
|
||||||
|
|
||||||
constructor(compiledMessage, locale, locales, localeConfig, memorize?) {
|
|
||||||
this.compiledMessage = compiledMessage;
|
|
||||||
this.locale = locale;
|
|
||||||
this.locales = locales;
|
|
||||||
this.localeConfig = localeConfig;
|
|
||||||
this.useMemorize = memorize ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param values 需要替换文本占位符的值
|
|
||||||
* @param formatOptions 需要格式化选项
|
|
||||||
*/
|
|
||||||
translate(values: object, formatOptions: FormatOptions = {}): string {
|
|
||||||
const createTextFormatter = (
|
|
||||||
locale: Locale,
|
|
||||||
locales: Locales,
|
|
||||||
values: object,
|
|
||||||
formatOptions: FormatOptions,
|
|
||||||
localeConfig: LocaleConfig,
|
|
||||||
useMemorize?: boolean
|
|
||||||
) => {
|
|
||||||
const textFormatter = (name: string, type: string, format: any) => {
|
|
||||||
const formatters = generateFormatters(locale, locales, localeConfig, formatOptions);
|
|
||||||
const value = values[name];
|
|
||||||
const formatter = formatters[type](value, format, useMemorize);
|
|
||||||
|
|
||||||
let message;
|
|
||||||
if (typeof formatter === 'function') {
|
|
||||||
message = formatter(textFormatter); // 递归调用
|
|
||||||
} else {
|
|
||||||
message = formatter; // 获得变量值 formatted: "Fred"
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.isArray(message) ? message.join('') : message;
|
|
||||||
};
|
|
||||||
|
|
||||||
return textFormatter;
|
|
||||||
};
|
|
||||||
|
|
||||||
let textFormatter = createTextFormatter(
|
|
||||||
this.locale,
|
|
||||||
this.locales,
|
|
||||||
values,
|
|
||||||
formatOptions,
|
|
||||||
this.localeConfig,
|
|
||||||
this.useMemorize
|
|
||||||
);
|
|
||||||
// 通过递归方法formatCore进行格式化处理
|
|
||||||
const result = this.formatMessage(this.compiledMessage, textFormatter);
|
|
||||||
return result; // 返回要格式化的结果
|
|
||||||
}
|
|
||||||
|
|
||||||
formatMessage(compiledMessage: CompiledMessage, textFormatter: Function) {
|
|
||||||
if (!Array.isArray(compiledMessage)) {
|
|
||||||
return compiledMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return compiledMessage.map(token => {
|
|
||||||
if (typeof token === 'string') {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [name, type, format] = token;
|
|
||||||
|
|
||||||
|
|
||||||
let replaceValueFormat = format;
|
|
||||||
|
|
||||||
// 如果 format 是对象,函数将递归地对它的每个值调用 formatMessage 后保存,否则直接保存
|
|
||||||
if (format && typeof format !== 'string') {
|
|
||||||
replaceValueFormat = Object.keys(replaceValueFormat).reduce((text, key) => {
|
|
||||||
text[key] = this.formatMessage(format[key], textFormatter);
|
|
||||||
return text;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
//调用 getContent 函数来获取给定 name、type 和 interpolateFormat 的值
|
|
||||||
const value = textFormatter(name, type, replaceValueFormat);
|
|
||||||
return value ?? '';
|
|
||||||
}).join('');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Translation;
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import { I18nCache } from '../../types/interfaces';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缓存机制
|
|
||||||
*/
|
|
||||||
function creatI18nCache(): I18nCache {
|
|
||||||
return {
|
|
||||||
dateTimeFormat: {},
|
|
||||||
numberFormat: {},
|
|
||||||
plurals: {},
|
|
||||||
messages: {},
|
|
||||||
select: {},
|
|
||||||
octothorpe: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default creatI18nCache;
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import creatI18nCache from '../cache/cache';
|
|
||||||
import utils from '../../utils/utils';
|
|
||||||
import { DatePool, Locales } from '../../types/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 时间格式化
|
|
||||||
*/
|
|
||||||
class DateTimeFormatter {
|
|
||||||
private readonly locales: Locales;
|
|
||||||
private readonly formatOptions: Intl.DateTimeFormatOptions;
|
|
||||||
|
|
||||||
// 是否进行存储
|
|
||||||
private readonly useMemorize: boolean;
|
|
||||||
|
|
||||||
// 创建一个缓存对象,用于存储DateTimeFormat的对象
|
|
||||||
private cache = creatI18nCache().dateTimeFormat;
|
|
||||||
|
|
||||||
constructor(locales: Locales, formatOptions?: Intl.DateTimeFormatOptions, useMemorize?: boolean) {
|
|
||||||
this.locales = locales;
|
|
||||||
this.formatOptions = formatOptions ?? {};
|
|
||||||
this.useMemorize = useMemorize ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
dateTimeFormat(value: DatePool, formatOptions?: Intl.DateTimeFormatOptions): string {
|
|
||||||
const options = formatOptions ?? this.formatOptions;
|
|
||||||
const formatter = new Intl.DateTimeFormat(this.locales, options);
|
|
||||||
// 将传输的字符串转变为日期对象
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
value = new Date(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果启用了记忆化且已经有对应的数字格式化器缓存,则直接返回缓存中的格式化结果。否则创建新的格式化数据,并进行缓存
|
|
||||||
if (this.useMemorize) {
|
|
||||||
// 造缓存的key,key包含区域设置和日期时间格式选项
|
|
||||||
const cacheKey = utils.generateKey<Intl.DateTimeFormatOptions>(this.locales, options);
|
|
||||||
|
|
||||||
if (this.cache[cacheKey]) {
|
|
||||||
return this.cache[cacheKey].format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询缓存中的key, 若无key则创建新key
|
|
||||||
this.cache[cacheKey] = formatter;
|
|
||||||
return formatter.format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回格式化后的时间
|
|
||||||
|
|
||||||
return formatter.format(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DateTimeFormatter;
|
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import creatI18nCache from '../cache/cache';
|
|
||||||
import { Locales } from '../../types/types';
|
|
||||||
import utils from '../../utils/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数字格式化
|
|
||||||
*/
|
|
||||||
class NumberFormatter {
|
|
||||||
private readonly locales: Locales;
|
|
||||||
private readonly formatOption?: Intl.NumberFormatOptions;
|
|
||||||
private readonly useMemorize?: boolean;
|
|
||||||
private cache = creatI18nCache().numberFormat; // 创建一个缓存对象,用于缓存已经创建的数字格式化器
|
|
||||||
|
|
||||||
constructor(locales: Locales, formatOption?: Intl.NumberFormatOptions, useMemorize?: boolean) {
|
|
||||||
this.locales = locales;
|
|
||||||
this.formatOption = formatOption ?? {};
|
|
||||||
this.useMemorize = useMemorize ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
numberFormat(value: number, formatOption?: Intl.NumberFormatOptions): string {
|
|
||||||
const options = formatOption ?? this.formatOption;
|
|
||||||
const formatter = new Intl.NumberFormat(this.locales, options);
|
|
||||||
|
|
||||||
// 如果启用了记忆化且已经有对应的数字格式化器缓存,则直接返回缓存中的格式化结果。否则创建新的格式化数据,并进行缓存
|
|
||||||
if (this.useMemorize) {
|
|
||||||
// 造缓存的key,key包含区域设置数字格式选项
|
|
||||||
const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locales, options);
|
|
||||||
|
|
||||||
if (this.cache[cacheKey]) {
|
|
||||||
return this.cache[cacheKey].format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cache[cacheKey] = formatter;
|
|
||||||
return formatter.format(value);
|
|
||||||
}
|
|
||||||
return formatter.format(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NumberFormatter;
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import utils from '../../utils/utils';
|
|
||||||
import NumberFormatter from './NumberFormatter';
|
|
||||||
import { Locale, Locales } from '../../types/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 复数格式化
|
|
||||||
*/
|
|
||||||
class PluralFormatter {
|
|
||||||
private readonly locale: Locale;
|
|
||||||
private readonly locales: Locales;
|
|
||||||
private readonly value: number;
|
|
||||||
private readonly message: any;
|
|
||||||
private readonly useMemorize: boolean;
|
|
||||||
private octothorpe: Record<string, any> = {};
|
|
||||||
|
|
||||||
constructor(locale, locales, value, message, useMemorize?) {
|
|
||||||
this.locale = locale;
|
|
||||||
this.locales = locales;
|
|
||||||
this.value = value;
|
|
||||||
this.message = message;
|
|
||||||
this.useMemorize = useMemorize ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将 message中的“#”替换为指定数字value,并返回新的字符串或者字符串数组
|
|
||||||
replaceSymbol(ctx: any) {
|
|
||||||
const msg = typeof this.message === 'function' ? this.message(ctx) : this.message;
|
|
||||||
const messages = Array.isArray(msg) ? msg : [msg];
|
|
||||||
|
|
||||||
const numberFormatter = new NumberFormatter(this.locales);
|
|
||||||
const valueStr = numberFormatter.numberFormat(this.value);
|
|
||||||
|
|
||||||
if (this.useMemorize) {
|
|
||||||
// 创建key,用于唯一标识
|
|
||||||
const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locale, this.message);
|
|
||||||
|
|
||||||
// 如果key存在,则使用缓存中的替代
|
|
||||||
if (this.octothorpe[cacheKey]) {
|
|
||||||
return messages.map(msg => (typeof msg === 'string' ? msg.replace('#', this.octothorpe[cacheKey]) : msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果不存在,则进行缓存
|
|
||||||
this.octothorpe[cacheKey] = valueStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages.map(msg => (typeof msg === 'string' ? msg.replace('#', valueStr) : msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PluralFormatter;
|
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import utils from '../../utils/utils';
|
|
||||||
import { Locale } from '../../types/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 规则选择器
|
|
||||||
* eg : 输入选择语句 female {She} other {They}} ,表示'female'和'other'是两种可能的值,它们分别对应着'She'和'They'两个输出结果。
|
|
||||||
* 如果调用select({ value: 'female' })则表示,输出 she
|
|
||||||
*/
|
|
||||||
class SelectFormatter {
|
|
||||||
private readonly locale: Locale;
|
|
||||||
private selectCache = {};
|
|
||||||
|
|
||||||
constructor(locale) {
|
|
||||||
this.locale = locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRule(value, rules, useMemorize?) {
|
|
||||||
if (useMemorize) {
|
|
||||||
// 创建key,用于唯一标识
|
|
||||||
const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locale, rules);
|
|
||||||
|
|
||||||
// 如果key存在,则使用缓存中的替代
|
|
||||||
if (this.selectCache[cacheKey]) {
|
|
||||||
return this.selectCache[cacheKey][value] || this.selectCache[cacheKey].other;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果不存在,则进行缓存
|
|
||||||
this.selectCache[cacheKey] = rules;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rules[value] || rules.other;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SelectFormatter;
|
|
|
@ -1,84 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import DateTimeFormatter from './fomatters/DateTimeFormatter';
|
|
||||||
import NumberFormatter from './fomatters/NumberFormatter';
|
|
||||||
import {DatePool, Locale, Locales, SelectPool} from '../types/types';
|
|
||||||
import PluralFormatter from './fomatters/PluralFormatter';
|
|
||||||
import SelectFormatter from './fomatters/SelectFormatter';
|
|
||||||
import {FormatOptions, IntlMessageFormat} from "../types/interfaces";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认格式化接口
|
|
||||||
*/
|
|
||||||
const generateFormatters = (
|
|
||||||
locale: Locale | Locales,
|
|
||||||
locales: Locales,
|
|
||||||
localeConfig: Record<string, any> = { plurals: undefined },
|
|
||||||
formatOptions: FormatOptions = {} // 自定义格式对象
|
|
||||||
): IntlMessageFormat => {
|
|
||||||
locale = locales || locale;
|
|
||||||
const { plurals } = localeConfig;
|
|
||||||
/**
|
|
||||||
* 样式函数 ,根据格式获取格式样式, 如货币百分比, 返回相应的格式的对象,如果没有设定格式,则返回一个空对象
|
|
||||||
* @param formatOption
|
|
||||||
*/
|
|
||||||
const getStyleOption = formatOption => {
|
|
||||||
if (typeof formatOption === 'string') {
|
|
||||||
return formatOptions[formatOption] || { option: formatOption };
|
|
||||||
} else {
|
|
||||||
return formatOption;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
// 复数规则
|
|
||||||
plural: (value: number, { offset = 0, ...rules }, useMemorize?) => {
|
|
||||||
const pluralFormatter = new PluralFormatter(
|
|
||||||
locale,
|
|
||||||
locales,
|
|
||||||
value - offset,
|
|
||||||
rules[value] || rules[(plurals as any)?.(value - offset)] || rules.other,
|
|
||||||
useMemorize
|
|
||||||
);
|
|
||||||
return pluralFormatter.replaceSymbol.bind(pluralFormatter);
|
|
||||||
},
|
|
||||||
|
|
||||||
selectordinal: (value: number, { offset = 0, ...rules }, useMemorize?) => {
|
|
||||||
const message = rules[value] || rules[(plurals as any)?.(value - offset, true)] || rules.other;
|
|
||||||
const pluralFormatter = new PluralFormatter(locale, locales, value - offset, message, useMemorize);
|
|
||||||
return pluralFormatter.replaceSymbol.bind(pluralFormatter);
|
|
||||||
},
|
|
||||||
|
|
||||||
// 选择规则,如果规则对象中包含与该值相对应的属性,则返回该属性的值;否则,返回 "other" 属性的值。
|
|
||||||
select: (value: SelectPool, formatRules, useMemorize?) => {
|
|
||||||
const selectFormatter = new SelectFormatter(locale);
|
|
||||||
return selectFormatter.getRule(value, formatRules, useMemorize);
|
|
||||||
},
|
|
||||||
|
|
||||||
// 用于将数字格式化为字符串,接受一个数字和一个格式化规则。它会根据规则返回格式化后的字符串。
|
|
||||||
numberFormat: (value : number, formatOption, useMemorize) => {
|
|
||||||
return new NumberFormatter(locales, getStyleOption(formatOption), useMemorize).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, useMemorize) => {
|
|
||||||
return new DateTimeFormatter(locales, getStyleOption(formatOption), useMemorize).dateTimeFormat(value, formatOption);
|
|
||||||
},
|
|
||||||
|
|
||||||
// 用于处理未定义的值,接受一个值并直接返回它。
|
|
||||||
undefined: value => value,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default generateFormatters;
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import utils from '../utils/utils';
|
|
||||||
import Translation from './Translation';
|
|
||||||
import I18n from '../core/I18n';
|
|
||||||
import { MessageDescriptor, MessageOptions } from '../types/interfaces';
|
|
||||||
import { CompiledMessage } from '../types/types';
|
|
||||||
|
|
||||||
export function getFormatMessage(
|
|
||||||
i18n: I18n,
|
|
||||||
id: MessageDescriptor | string,
|
|
||||||
values: Object | undefined = {},
|
|
||||||
options: MessageOptions = {}
|
|
||||||
) {
|
|
||||||
let { message, context, formatOptions, useMemorize } = options;
|
|
||||||
const memorize = useMemorize ?? i18n.useMemorize;
|
|
||||||
if (typeof id !== 'string') {
|
|
||||||
values = values || id.defaultValues;
|
|
||||||
message = id.message || id.defaultMessage;
|
|
||||||
context = id.context;
|
|
||||||
id = id.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对messages进行判空处理
|
|
||||||
const isMissingMessage = !context && !i18n.messages[id];
|
|
||||||
const isMissingContextMessage = context && !i18n.messages[context][id];
|
|
||||||
const messageUnavailable = isMissingContextMessage || isMissingMessage;
|
|
||||||
|
|
||||||
// 对错误消息进行处理
|
|
||||||
const messageError = i18n.error;
|
|
||||||
if (messageError && messageUnavailable) {
|
|
||||||
if (typeof messageError === 'function') {
|
|
||||||
return messageError(i18n.locale, id, context);
|
|
||||||
} else {
|
|
||||||
return messageError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let compliedMessage: CompiledMessage;
|
|
||||||
if (context) {
|
|
||||||
compliedMessage = i18n.messages[context][id] || message || id;
|
|
||||||
} else {
|
|
||||||
compliedMessage = i18n.messages[id] || message || id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对解析的messages进行parse解析,并输出解析后的Token
|
|
||||||
compliedMessage = typeof compliedMessage === 'string' ? utils.compile(compliedMessage) : compliedMessage;
|
|
||||||
|
|
||||||
const translation = new Translation(compliedMessage, i18n.locale, i18n.locales, i18n.localeConfig, memorize);
|
|
||||||
return translation.translate(values, formatOptions);
|
|
||||||
}
|
|
|
@ -1,189 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import ruleUtils from '../utils/parseRuleUtils';
|
|
||||||
import { LexerInterface } from "../types/interfaces";
|
|
||||||
|
|
||||||
const getMatch = ruleUtils.checkSticky()
|
|
||||||
? // 正则表达式具有 sticky 标志
|
|
||||||
(regexp, buffer) => regexp.exec(buffer)
|
|
||||||
: // 正则表达式具有 global 标志,匹配的字符串长度为 0,则表示匹配失败
|
|
||||||
(regexp, buffer) => (regexp.exec(buffer)[0].length === 0 ? null : regexp.exec(buffer));
|
|
||||||
|
|
||||||
class Lexer<T> implements LexerInterface<T> {
|
|
||||||
readonly startState: string;
|
|
||||||
readonly states: Record<string, any>;
|
|
||||||
private buffer: string = '';
|
|
||||||
private stack: string[] = [];
|
|
||||||
private index;
|
|
||||||
private line;
|
|
||||||
private col;
|
|
||||||
private queuedText;
|
|
||||||
private state;
|
|
||||||
private groups;
|
|
||||||
private error;
|
|
||||||
private regexp;
|
|
||||||
private fast;
|
|
||||||
private queuedGroup;
|
|
||||||
private value;
|
|
||||||
|
|
||||||
constructor(states, state) {
|
|
||||||
this.startState = state;
|
|
||||||
this.states = states;
|
|
||||||
this.buffer = '';
|
|
||||||
this.stack = [];
|
|
||||||
this.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
public reset(data?, info?) {
|
|
||||||
this.buffer = data || '';
|
|
||||||
this.index = 0;
|
|
||||||
this.line = info ? info.line : 1;
|
|
||||||
this.col = info ? info.col : 1;
|
|
||||||
this.queuedText = info ? info.queuedText : '';
|
|
||||||
this.setState(info ? info.state : this.startState);
|
|
||||||
this.stack = info && info.stack ? info.stack.slice() : [];
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setState(state) {
|
|
||||||
if (!state || this.state === state) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.state = state;
|
|
||||||
const info = this.states[state];
|
|
||||||
this.groups = info.groups;
|
|
||||||
this.error = info.error;
|
|
||||||
this.regexp = info.regexp;
|
|
||||||
this.fast = info.fast;
|
|
||||||
}
|
|
||||||
|
|
||||||
private popState() {
|
|
||||||
this.setState(this.stack.pop());
|
|
||||||
}
|
|
||||||
|
|
||||||
private pushState(state) {
|
|
||||||
this.stack.push(this.state);
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getGroup(match) {
|
|
||||||
const groupCount = this.groups.length;
|
|
||||||
for (let i = 0; i < groupCount; i++) {
|
|
||||||
if (match[i + 1] !== undefined) {
|
|
||||||
return this.groups[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error('No token type found matching text!');
|
|
||||||
}
|
|
||||||
|
|
||||||
private tokenToString() {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 迭代获取下一个 token
|
|
||||||
public next() {
|
|
||||||
const index = this.index;
|
|
||||||
|
|
||||||
if (this.queuedGroup) {
|
|
||||||
const token = this.getToken(this.queuedGroup, this.queuedText, index);
|
|
||||||
this.queuedGroup = null;
|
|
||||||
this.queuedText = '';
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = this.buffer;
|
|
||||||
if (index === buffer.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fastGroup = this.fast[buffer.charCodeAt(index)];
|
|
||||||
if (fastGroup) {
|
|
||||||
return this.getToken(fastGroup, buffer.charAt(index), index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有快速匹配,那么使用预先编译的正则表达式进行匹配操作
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = this.getGroup(match);
|
|
||||||
const text = match[0];
|
|
||||||
|
|
||||||
if (error.fallback && match.index !== index) {
|
|
||||||
this.queuedGroup = group;
|
|
||||||
this.queuedText = text;
|
|
||||||
return this.getToken(error, buffer.slice(index, match.index), index);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getToken(group, text, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getToken(group, text, offset) {
|
|
||||||
let lineNum = 0;
|
|
||||||
let last = 1; // 最后一个换行符的索引位置
|
|
||||||
if (group.lineBreaks) {
|
|
||||||
const matchNL = /\n/g;
|
|
||||||
if (text === '\n') {
|
|
||||||
lineNum = 1;
|
|
||||||
} else {
|
|
||||||
while (matchNL.exec(text)) {
|
|
||||||
lineNum++;
|
|
||||||
last = matchNL.lastIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = {
|
|
||||||
type: (typeof group.type === 'function' && group.type(text)) || group.defaultType,
|
|
||||||
value: typeof group.value === 'function' ? group.value(text) : text,
|
|
||||||
text: text,
|
|
||||||
toString: this.tokenToString,
|
|
||||||
offset: offset, // 标记在输入 buffer 中的偏移量
|
|
||||||
lineBreaks: lineNum,
|
|
||||||
line: this.line, // token 所在的行号
|
|
||||||
col: this.col, // token 所在的列号
|
|
||||||
};
|
|
||||||
|
|
||||||
const size = text.length;
|
|
||||||
this.index += size;
|
|
||||||
this.line += lineNum;
|
|
||||||
if (lineNum !== 0) {
|
|
||||||
this.col = size - last + 1;
|
|
||||||
} else {
|
|
||||||
this.col += size;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.shouldThrow) {
|
|
||||||
throw new Error('Invalid Syntax!');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.pop) {
|
|
||||||
this.popState();
|
|
||||||
} else if (group.push) {
|
|
||||||
this.pushState(group.push);
|
|
||||||
} else if (group.next) {
|
|
||||||
this.setState(group.next);
|
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 增加迭代器
|
|
||||||
[Symbol.iterator]() {
|
|
||||||
return {
|
|
||||||
next: (): IteratorResult<T> => {
|
|
||||||
const token = this.next();
|
|
||||||
return { value: token, done: !token } as IteratorResult<T>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Lexer;
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const body: Record<string, any> = {
|
|
||||||
doubleapos: { match: "''", value: () => "'" },
|
|
||||||
quoted: {
|
|
||||||
lineBreaks: true,
|
|
||||||
match: /'[{}#](?:[^]*?[^'])?'(?!')/u,
|
|
||||||
value: src => src.slice(1, -1).replace(/''/g, "'"),
|
|
||||||
},
|
|
||||||
argument: {
|
|
||||||
lineBreaks: true,
|
|
||||||
match: /\{\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u,
|
|
||||||
push: 'arg',
|
|
||||||
value: src => src.substring(1).trim(),
|
|
||||||
},
|
|
||||||
octothorpe: '#',
|
|
||||||
end: { match: '}', pop: 1 },
|
|
||||||
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(),
|
|
||||||
},
|
|
||||||
'func-args': {
|
|
||||||
lineBreaks: true,
|
|
||||||
match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*,/u,
|
|
||||||
next: 'body',
|
|
||||||
value: src => src.split(',')[1].trim(),
|
|
||||||
},
|
|
||||||
'func-simple': {
|
|
||||||
lineBreaks: true,
|
|
||||||
match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u,
|
|
||||||
value: src => src.substring(1).trim(),
|
|
||||||
},
|
|
||||||
end: { match: '}', pop: 1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const select: Record<string, any> = {
|
|
||||||
offset: {
|
|
||||||
lineBreaks: true,
|
|
||||||
match: /\s*offset\s*:\s*\d+\s*/u,
|
|
||||||
value: src => src.split(':')[1].trim(),
|
|
||||||
},
|
|
||||||
case: {
|
|
||||||
lineBreaks: true,
|
|
||||||
match: /\s*(?:=\d+|[^\p{Pat_Syn}\p{Pat_WS}]+)\s*\{/u,
|
|
||||||
push: 'body',
|
|
||||||
value: src => src.substring(0, src.indexOf('{')).trim(),
|
|
||||||
},
|
|
||||||
end: { match: /\s*\}/u, pop: 1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mappingRule: Record<string, any> = {
|
|
||||||
body,
|
|
||||||
arg,
|
|
||||||
select
|
|
||||||
};
|
|
|
@ -1,230 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Lexer from './Lexer';
|
|
||||||
import { mappingRule } from './mappingRule';
|
|
||||||
import ruleUtils from '../utils/parseRuleUtils';
|
|
||||||
import { RawToken } from "../types/types";
|
|
||||||
|
|
||||||
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;
|
|
||||||
const fast = {};
|
|
||||||
let enableFast = true;
|
|
||||||
let unicodeFlag = null;
|
|
||||||
const groups: Record<string, any>[] = [];
|
|
||||||
const parts: string[] = [];
|
|
||||||
|
|
||||||
// 检查是否存在 fallback 规则,若存在则禁用快速匹配
|
|
||||||
enableFast = isExistsFallback(rules, enableFast);
|
|
||||||
|
|
||||||
for (let i = 0; i < rules.length; i++) {
|
|
||||||
const options = rules[i];
|
|
||||||
if (options.include) {
|
|
||||||
throw new Error('Inheritance is not allowed in stateless lexers!');
|
|
||||||
}
|
|
||||||
|
|
||||||
errorRule = isOptionsErrorOrFallback(options, errorRule);
|
|
||||||
|
|
||||||
const match = options.match.slice();
|
|
||||||
if (enableFast) {
|
|
||||||
// 如果快速匹配允许,则将单字符的规则存入 fast 对象
|
|
||||||
processFast(match, fast, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查规则中是否存在不适当的状态切换选项
|
|
||||||
if (options.pop || options.push || options.next) {
|
|
||||||
checkStateOptions(hasStates, options);
|
|
||||||
}
|
|
||||||
// 只有具有 .match 的规则才会被包含在正则表达式中
|
|
||||||
if (match.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
enableFast = false;
|
|
||||||
|
|
||||||
groups.push(options);
|
|
||||||
|
|
||||||
// 检查是否所有规则都使用了 unicode 标志,或者都未使用
|
|
||||||
unicodeFlag = checkUnicode(match, unicodeFlag, options);
|
|
||||||
|
|
||||||
const pat = ruleUtils.getRegUnion(match.map(ruleUtils.getReg));
|
|
||||||
const regexp = new RegExp(pat);
|
|
||||||
if (regexp.test('')) {
|
|
||||||
throw new Error('The regex matched the empty string!');
|
|
||||||
}
|
|
||||||
const groupCount = ruleUtils.getRegGroups(pat);
|
|
||||||
if (groupCount > 0) {
|
|
||||||
throw new Error('The regular expression uses capture groups, use (?: … ) instead!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检测规则是否匹配换行符
|
|
||||||
if (!options.lineBreaks && regexp.test('\n')) {
|
|
||||||
throw new Error('The matching rule must contain lineBreaks.');
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.push(ruleUtils.getRegCapture(pat));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有 fallback 规则,则使用 sticky 标志,只在当前索引位置寻找匹配项,如果不支持 sticky 标志,则使用无法被否定的空模式来模拟
|
|
||||||
const fallbackRule = errorRule && errorRule.fallback;
|
|
||||||
let flags = ruleUtils.checkSticky() && !fallbackRule ? 'ym' : 'gm';
|
|
||||||
const suffix = ruleUtils.checkSticky() || fallbackRule ? '' : '|';
|
|
||||||
|
|
||||||
if (unicodeFlag === true) {
|
|
||||||
flags += 'u';
|
|
||||||
}
|
|
||||||
const combined = new RegExp(ruleUtils.getRegUnion(parts) + suffix, flags);
|
|
||||||
|
|
||||||
return {
|
|
||||||
regexp: combined,
|
|
||||||
groups: groups,
|
|
||||||
fast: fast,
|
|
||||||
error: errorRule || defaultErrorRule,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkStateGroup(group: Record<string, any>, name: string, map: Record<string, any>) {
|
|
||||||
const state = group && (group.push || group.next);
|
|
||||||
if (state && !map[state]) {
|
|
||||||
throw new Error('The state is missing.');
|
|
||||||
}
|
|
||||||
if (group && group.pop && +group.pop !== 1) {
|
|
||||||
throw new Error('The value of pop must be 1.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将国际化解析规则注入分词器中
|
|
||||||
function parseMappingRule(mappingRule: Record<string, any>, start?: string): Lexer<RawToken> {
|
|
||||||
const keys = Object.getOwnPropertyNames(mappingRule);
|
|
||||||
|
|
||||||
if (!start) {
|
|
||||||
start = keys[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将每个状态的规则解析为规则数组,并存储在 ruleMap 对象中
|
|
||||||
const ruleMap = keys.reduce((map, key) => {
|
|
||||||
map[key] = ruleUtils.getRules(mappingRule[key]);
|
|
||||||
return map;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// 处理规则中的 include 声明,将被包含的规则添加到相应的状态中
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
|
||||||
const key = keys[i];
|
|
||||||
const rules = ruleMap[key];
|
|
||||||
const included = {};
|
|
||||||
|
|
||||||
for (let j = 0; j < rules.length; j++) {
|
|
||||||
const rule = rules[j];
|
|
||||||
if (!rule.include) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const splice = [j, 1];
|
|
||||||
if (rule.include !== key && !included[rule.include]) {
|
|
||||||
included[rule.include] = true;
|
|
||||||
const newRules = ruleMap[rule.include];
|
|
||||||
|
|
||||||
if (!newRules) {
|
|
||||||
throw new Error('Cannot contain a state that does not exist!');
|
|
||||||
}
|
|
||||||
|
|
||||||
newRules.forEach(newRule => {
|
|
||||||
if (!rules.includes(newRule)) {
|
|
||||||
splice.push(newRule);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
rules.splice.apply(rules, splice);
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const map = {};
|
|
||||||
|
|
||||||
// 将规则映射为词法分析器数据结构,并存储在 map 对象中
|
|
||||||
keys.forEach(key => {
|
|
||||||
map[key] = parseRules(ruleMap[key], true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查状态组中的规则是否正确引用了其他状态
|
|
||||||
keys.forEach(name => {
|
|
||||||
const state = map[name];
|
|
||||||
const groups = state.groups;
|
|
||||||
groups.forEach(group => {
|
|
||||||
checkStateGroup(group, name, map);
|
|
||||||
});
|
|
||||||
const fastKeys = Object.getOwnPropertyNames(state.fast);
|
|
||||||
fastKeys.forEach(fastKey => {
|
|
||||||
checkStateGroup(state.fast[fastKey], name, map);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Lexer(map, start);
|
|
||||||
}
|
|
||||||
|
|
||||||
function processFast(match, fast: {}, options) {
|
|
||||||
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>) {
|
|
||||||
if (!options.fallback === !errorRule.fallback) {
|
|
||||||
throw new Error('errorRule can only set one!');
|
|
||||||
} else {
|
|
||||||
throw new Error('fallback and error cannot be set at the same time!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkUnicode(match, unicodeFlag, options) {
|
|
||||||
for (let j = 0; j < match.length; j++) {
|
|
||||||
const obj = match[j];
|
|
||||||
if (!ruleUtils.checkRegExp(obj)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unicodeFlag === null) {
|
|
||||||
unicodeFlag = obj.unicode;
|
|
||||||
} 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) {
|
|
||||||
if (!hasStates) {
|
|
||||||
throw new Error('State toggle options are not allowed in stateless tokenizers!');
|
|
||||||
}
|
|
||||||
if (options.fallback) {
|
|
||||||
throw new Error('State toggle options are not allowed on fallback tokens!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isExistsFallback(rules: Record<string, any>, enableFast: boolean) {
|
|
||||||
for (let i = 0; i < rules.length; i++) {
|
|
||||||
if (rules[i].fallback) {
|
|
||||||
enableFast = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return enableFast;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOptionsErrorOrFallback(options, errorRule: Record<string, any> | null) {
|
|
||||||
if (options.error || options.fallback) {
|
|
||||||
// 只能设置一个 errorRule
|
|
||||||
if (errorRule) {
|
|
||||||
handleErrorRule(options, errorRule);
|
|
||||||
}
|
|
||||||
errorRule = options;
|
|
||||||
}
|
|
||||||
return errorRule;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const lexer = parseMappingRule(mappingRule);
|
|
||||||
|
|
||||||
export default parseMappingRule;
|
|
|
@ -1,197 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { lexer } from './parseMappingRule';
|
|
||||||
import { RawToken, Token } 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';
|
|
||||||
}
|
|
||||||
|
|
||||||
class Parser {
|
|
||||||
lexer: Lexer<RawToken>;
|
|
||||||
cardinalKeys: string[] = DEFAULT_PLURAL_KEYS;
|
|
||||||
ordinalKeys: string[] = DEFAULT_PLURAL_KEYS;
|
|
||||||
|
|
||||||
constructor(message: string) {
|
|
||||||
this.lexer = lexer.reset(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
isSelectKeyValid(token: RawToken, type: Select['type'], value: string) {
|
|
||||||
if (value[0] === '=') {
|
|
||||||
if (type === 'select') {
|
|
||||||
throw new Error(`The key value of the select type is invalid.`);
|
|
||||||
}
|
|
||||||
} else if (type !== 'select') {
|
|
||||||
const values = type === 'plural' ? this.cardinalKeys : this.ordinalKeys;
|
|
||||||
if (values.length > 0 && !values.includes(value)) {
|
|
||||||
throw new Error(`${type} type key value is invalid.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processSelect({ value: arg }: any, isPlural: boolean, context: TokenContext, type: Select['type']): Select {
|
|
||||||
const select: Select = { type, arg, cases: [], ctx: context };
|
|
||||||
|
|
||||||
if (type === 'plural' || type === 'selectordinal') {
|
|
||||||
isPlural = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const token of this.lexer) {
|
|
||||||
switch (token.type) {
|
|
||||||
case 'offset': {
|
|
||||||
if (type === 'select') {
|
|
||||||
throw new Error('The complex offset of the select type is incorrect.');
|
|
||||||
}
|
|
||||||
if (select.cases.length > 0) {
|
|
||||||
throw new Error('The complex offset must be set before cases.');
|
|
||||||
}
|
|
||||||
|
|
||||||
select.offset = Number(token.value);
|
|
||||||
context.text += token.text;
|
|
||||||
context.lineNum += token.lineBreaks;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'case': {
|
|
||||||
this.isSelectKeyValid(token, type, token.value);
|
|
||||||
select.cases.push({
|
|
||||||
key: token.value.replace(/=/g, ''),
|
|
||||||
tokens: this.parse(isPlural),
|
|
||||||
ctx: getContext(token),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'end': {
|
|
||||||
return select;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error(`Unrecognized analyzer token: ${token.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error('The message end position is invalid.');
|
|
||||||
}
|
|
||||||
|
|
||||||
parseToken(token: RawToken, isPlural: boolean): PlainArg | FunctionArg | Select {
|
|
||||||
const context = getContext(token);
|
|
||||||
const nextToken = this.lexer.next();
|
|
||||||
|
|
||||||
if (!nextToken) {
|
|
||||||
throw new Error('The message end position is invalid.');
|
|
||||||
}
|
|
||||||
|
|
||||||
context.text += nextToken.text;
|
|
||||||
context.lineNum += nextToken.lineBreaks;
|
|
||||||
|
|
||||||
switch (nextToken.type) {
|
|
||||||
case 'end': {
|
|
||||||
return { type: 'argument', arg: token.value, ctx: context };
|
|
||||||
}
|
|
||||||
case 'func-simple': {
|
|
||||||
const end = this.lexer.next();
|
|
||||||
if (!end) {
|
|
||||||
throw new Error('The message end position is invalid.');
|
|
||||||
}
|
|
||||||
if (end.type !== 'end') {
|
|
||||||
throw new Error(`Unrecognized analyzer token: ${end.type}`);
|
|
||||||
}
|
|
||||||
context.text += end.text;
|
|
||||||
if (checkSelectType(nextToken.value.toLowerCase())) {
|
|
||||||
throw new Error(`Invalid parameter type: ${nextToken.value}`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'function',
|
|
||||||
arg: token.value,
|
|
||||||
key: nextToken.value,
|
|
||||||
ctx: context,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'func-args': {
|
|
||||||
if (checkSelectType(nextToken.value.toLowerCase())) {
|
|
||||||
throw new Error(`Invalid parameter type: ${nextToken.value}`);
|
|
||||||
}
|
|
||||||
let param = this.parse(isPlural);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'function',
|
|
||||||
arg: token.value,
|
|
||||||
key: nextToken.value,
|
|
||||||
param,
|
|
||||||
ctx: context,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'select':
|
|
||||||
if (checkSelectType(nextToken.value)) {
|
|
||||||
return this.processSelect(token, isPlural, context, nextToken.value);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Invalid select type: ${nextToken.value}`);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(`Unrecognized analyzer token: ${nextToken.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在根级别解析时,遇到结束符号即结束解析并返回结果;而在非根级别解析时,遇到结束符号会被视为不合法的结束位置,抛出错误
|
|
||||||
parse(isPlural: boolean, isRoot?: boolean): Array<Content | PlainArg | FunctionArg | Select> {
|
|
||||||
const tokens: any[] = [];
|
|
||||||
let content: string | Content | null = null;
|
|
||||||
|
|
||||||
for (const token of this.lexer) {
|
|
||||||
if (token.type === 'argument') {
|
|
||||||
if (content) {
|
|
||||||
content = null;
|
|
||||||
}
|
|
||||||
tokens.push(this.parseToken(token, isPlural));
|
|
||||||
} else if (token.type === 'octothorpe' && isPlural) {
|
|
||||||
if (content) {
|
|
||||||
content = null;
|
|
||||||
}
|
|
||||||
tokens.push({ type: 'octothorpe' });
|
|
||||||
} else if (token.type === 'end' && !isRoot) {
|
|
||||||
return tokens;
|
|
||||||
} else if (token.type === 'doubleapos') {
|
|
||||||
tokens.push(token.value);
|
|
||||||
} else if (token.type === 'quoted') {
|
|
||||||
tokens.push(token.value)
|
|
||||||
} else if (token.type === 'content') {
|
|
||||||
tokens.push(token.value);
|
|
||||||
} else {
|
|
||||||
let value = token.value;
|
|
||||||
if (!isPlural && token.type === 'quoted' && value[0] === '#') {
|
|
||||||
if (value.includes('{')) {
|
|
||||||
throw new Error(`Invalid template: ${value}`);
|
|
||||||
}
|
|
||||||
value = token.text;
|
|
||||||
}
|
|
||||||
if (content) {
|
|
||||||
content = value;
|
|
||||||
} else {
|
|
||||||
content = value;
|
|
||||||
tokens.push(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRoot) {
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
throw new Error('The message end position is invalid.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function parse(message: string): Array<Content | PlainArg | FunctionArg | Select> {
|
|
||||||
const parser = new Parser(message);
|
|
||||||
return parser.parse(false, true);
|
|
||||||
}
|
|
|
@ -1,192 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
AllLocaleConfig,
|
|
||||||
AllMessages,
|
|
||||||
Locale,
|
|
||||||
Locales,
|
|
||||||
Error,
|
|
||||||
PluralCategory,
|
|
||||||
DatePool,
|
|
||||||
SelectPool,
|
|
||||||
RawToken,
|
|
||||||
} from './types';
|
|
||||||
import I18n from '../core/I18n';
|
|
||||||
import Lexer from '../parser/Lexer';
|
|
||||||
import injectI18n from "../core/components/InjectI18n";
|
|
||||||
|
|
||||||
// FormattedMessage的参数定义
|
|
||||||
export interface FormattedMessageProps extends MessageDescriptor {
|
|
||||||
values?: object;
|
|
||||||
tagName?: string;
|
|
||||||
|
|
||||||
children?(nodes: any[]): any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 信息描述对象,有id、信息、内容
|
|
||||||
export interface MessageDescriptor extends MessageOptions {
|
|
||||||
id: string;
|
|
||||||
defaultMessage?: string;
|
|
||||||
defaultValues?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageOptions {
|
|
||||||
comment?: string;
|
|
||||||
message?: string;
|
|
||||||
context?: string;
|
|
||||||
formatOptions?: FormatOptions;
|
|
||||||
useMemorize?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// I18n类的缓存定义
|
|
||||||
export interface I18nCache {
|
|
||||||
dateTimeFormat: Record<string, Intl.DateTimeFormat>;
|
|
||||||
numberFormat: Record<string, Intl.NumberFormat>;
|
|
||||||
plurals: Record<string, Intl.PluralRules>;
|
|
||||||
messages: Record<string, IntlMessageFormat>;
|
|
||||||
select: Record<string, object>;
|
|
||||||
octothorpe: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// I18n类的传参
|
|
||||||
export interface I18nProps {
|
|
||||||
locale?: Locale;
|
|
||||||
locales?: Locales;
|
|
||||||
messages?: AllMessages;
|
|
||||||
localeConfig?: AllLocaleConfig;
|
|
||||||
useMemorize?: boolean;
|
|
||||||
error?: Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息格式化选项类型
|
|
||||||
export interface FormatOptions {
|
|
||||||
dateTimeFormat?: Intl.DateTimeFormatOptions;
|
|
||||||
numberFormat?: Intl.NumberFormatOptions;
|
|
||||||
plurals?: Intl.PluralRulesOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InjectOptions {
|
|
||||||
isUsingForwardRef?: boolean;
|
|
||||||
ensureContext?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface I18nProviderProps {
|
|
||||||
i18n?: I18n;
|
|
||||||
locale?: Locale;
|
|
||||||
messages?: AllMessages;
|
|
||||||
defaultLocale?: string;
|
|
||||||
RenderOnLocaleChange?: boolean;
|
|
||||||
children?: any;
|
|
||||||
uesMemorize?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IntlMessageFormat extends I18nProviderProps, MessageOptions {
|
|
||||||
plural: (
|
|
||||||
value: number,
|
|
||||||
{ offset, ...rules }: { [x: string]: any; offset?: number },
|
|
||||||
useMemorize?: boolean
|
|
||||||
) => (ctx: any) => any[];
|
|
||||||
selectordinal: (
|
|
||||||
value: number,
|
|
||||||
{ offset, ...rules }: { [x: string]: any; offset?: number },
|
|
||||||
useMemorize?: boolean
|
|
||||||
) => (ctx: any) => any[];
|
|
||||||
select: (value: SelectPool, formatRules: any, useMemorize?: boolean) => any;
|
|
||||||
numberFormat: (value: number, formatOption: any, useMemorize: boolean) => string;
|
|
||||||
dateTimeFormat: (value: DatePool, formatOption: any, useMemorize: boolean) => string;
|
|
||||||
undefined: (value: any) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
//错误信息的事件
|
|
||||||
export interface MissingMessageEvent {
|
|
||||||
locale: Locale;
|
|
||||||
id: string;
|
|
||||||
context?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LexerInterface<T> {
|
|
||||||
reset: (data?: string, info?: Record<string, any>) => Lexer<T>;
|
|
||||||
next: () => RawToken | undefined;
|
|
||||||
|
|
||||||
[Symbol.iterator](): Iterator<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TokenContext {
|
|
||||||
// token 索引值的偏移量
|
|
||||||
offset: number;
|
|
||||||
|
|
||||||
// token 开始计算的初始行号
|
|
||||||
line: number;
|
|
||||||
|
|
||||||
// token 开始计算的初始列号
|
|
||||||
col: number;
|
|
||||||
|
|
||||||
// 原始输入
|
|
||||||
text: string;
|
|
||||||
|
|
||||||
// 换行数
|
|
||||||
lineNum: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Content {
|
|
||||||
type: 'content';
|
|
||||||
value: string;
|
|
||||||
ctx: TokenContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要解析参数定义
|
|
||||||
export interface PlainArg {
|
|
||||||
type: 'argument';
|
|
||||||
arg: string;
|
|
||||||
ctx: TokenContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Octothorpe {
|
|
||||||
type: 'octothorpe';
|
|
||||||
ctx: TokenContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FunctionArg {
|
|
||||||
type: 'function';
|
|
||||||
arg: string;
|
|
||||||
key: string;
|
|
||||||
param?: Array<Content | PlainArg | FunctionArg | Select | Octothorpe>;
|
|
||||||
ctx: TokenContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelectCase {
|
|
||||||
key: string;
|
|
||||||
tokens: Array<Content | PlainArg | FunctionArg | Select | Octothorpe>;
|
|
||||||
ctx: TokenContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选择模式
|
|
||||||
export interface Select {
|
|
||||||
type: 'plural' | 'select' | 'selectordinal';
|
|
||||||
arg: string;
|
|
||||||
cases: SelectCase[];
|
|
||||||
offset?: number;
|
|
||||||
ctx: TokenContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InjectedIntl {
|
|
||||||
|
|
||||||
// 日期格式化
|
|
||||||
formatDate(value: DatePool, options?: Intl.DateTimeFormatOptions): string;
|
|
||||||
|
|
||||||
// 时间格式化
|
|
||||||
formatTime(value: DatePool, options?: Intl.DateTimeFormatOptions): string;
|
|
||||||
|
|
||||||
// 数字格式化
|
|
||||||
formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
|
|
||||||
|
|
||||||
// 信息格式化
|
|
||||||
formatMessage(
|
|
||||||
messageDescriptor: MessageDescriptor,
|
|
||||||
values?: object,
|
|
||||||
options?: MessageOptions,
|
|
||||||
useMemorize?: boolean
|
|
||||||
): string;
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Content, MissingMessageEvent, Octothorpe, PlainArg, Select, FunctionArg } from './interfaces';
|
|
||||||
|
|
||||||
export type Error = string | ((message, id, context) => string);
|
|
||||||
|
|
||||||
export type Locale = string;
|
|
||||||
|
|
||||||
export type Locales = Locale | Locale[];
|
|
||||||
|
|
||||||
export type LocaleConfig = { plurals?: Function };
|
|
||||||
|
|
||||||
export type AllLocaleConfig = Record<Locale, LocaleConfig>;
|
|
||||||
|
|
||||||
type CompiledMessagePart = string | Array<string | Array<string | (string | undefined)> | Record<string, unknown>>;
|
|
||||||
|
|
||||||
export type CompiledMessage = string | CompiledMessagePart[];
|
|
||||||
|
|
||||||
export type Messages = Record<string, string> | Record<string, CompiledMessage>;
|
|
||||||
|
|
||||||
export type AllMessages = Record<string, string> | Record<Locale, Messages>;
|
|
||||||
|
|
||||||
export type EventCallback = (...args: any[]) => any;
|
|
||||||
|
|
||||||
// 资源事件
|
|
||||||
export type Events = {
|
|
||||||
change: () => void;
|
|
||||||
missing: (event: MissingMessageEvent) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 默认复数规则
|
|
||||||
export type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
|
||||||
|
|
||||||
export type Token = Content | PlainArg | FunctionArg | Select | Octothorpe;
|
|
||||||
|
|
||||||
export type DatePool = Date | string;
|
|
||||||
|
|
||||||
export type SelectPool = string | object;
|
|
||||||
|
|
||||||
export type RawToken = {
|
|
||||||
type: string;
|
|
||||||
value: string;
|
|
||||||
text: string;
|
|
||||||
toString: () => string;
|
|
||||||
offset: number;
|
|
||||||
lineBreaks: number;
|
|
||||||
line: number;
|
|
||||||
col: number;
|
|
||||||
};
|
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import { isMemo, ForwardRef } from 'inulajs';
|
|
||||||
import {
|
|
||||||
HORIZON_FORWARD_REF_STATICS,
|
|
||||||
HORIZON_MEMO_STATICS,
|
|
||||||
HORIZON_STATICS,
|
|
||||||
NATIVE_STATICS,
|
|
||||||
} from '../constants';
|
|
||||||
|
|
||||||
const staticsMap = new Map();
|
|
||||||
staticsMap.set(ForwardRef, HORIZON_FORWARD_REF_STATICS);
|
|
||||||
|
|
||||||
// 确定给定的组件是否为Memo组件,并返回相应的静态属性
|
|
||||||
function getStatics(component) {
|
|
||||||
if (isMemo(component)) {
|
|
||||||
return HORIZON_MEMO_STATICS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (staticsMap.has(component['vtype'])) {
|
|
||||||
return staticsMap.get(component['vtype']) || HORIZON_STATICS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断给定的对象属性描述是否有效
|
|
||||||
* @param sourceComponent
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
function isDescriptorValid<U>(sourceComponent: U, key: string | symbol) {
|
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(sourceComponent, key);
|
|
||||||
return descriptor && (!descriptor.get || descriptor.get.prototype);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将一个对象的非react静态属性复制到另一个对象上,并返回马目标对象
|
|
||||||
function copyStaticProps<T, U>(targetComponent: T, sourceComponent: U): T {
|
|
||||||
if (typeof sourceComponent === 'string') {
|
|
||||||
return targetComponent;
|
|
||||||
}
|
|
||||||
// 递归拷贝静态属性
|
|
||||||
const inheritedComponent = Object.getPrototypeOf(sourceComponent);
|
|
||||||
if (inheritedComponent && inheritedComponent !== Object.prototype) {
|
|
||||||
copyStaticProps(targetComponent, inheritedComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取源组件的属性列表
|
|
||||||
const keys: (string | symbol)[] = [
|
|
||||||
//获取指定对象自身的所有属性的名称(包括不可枚举属性)
|
|
||||||
...Object.getOwnPropertyNames(sourceComponent),
|
|
||||||
|
|
||||||
//获取指定对象自身的所有 Symbol 类型的属性的名称(包括不可枚举属性)
|
|
||||||
...Object.getOwnPropertySymbols(sourceComponent),
|
|
||||||
];
|
|
||||||
|
|
||||||
// 获取目标组件和源组件的静态属性
|
|
||||||
const targetStatics = getStatics(targetComponent);
|
|
||||||
const sourceStatics = getStatics(sourceComponent);
|
|
||||||
|
|
||||||
keys.forEach(key => {
|
|
||||||
if (
|
|
||||||
!NATIVE_STATICS[key] &&
|
|
||||||
!(targetStatics && targetStatics[key]) &&
|
|
||||||
!(sourceStatics && sourceStatics[key]) &&
|
|
||||||
isDescriptorValid(sourceComponent, key)
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// 在一个已有的targetComponent对象上增加sourceComponent的属性
|
|
||||||
Object.defineProperty(targetComponent, key, Object.getOwnPropertyDescriptor(sourceComponent, key)!);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return targetComponent;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default copyStaticProps;
|
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { EventCallback } from "../types/types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个时间触发器类,使用泛型实现动态时间的监听
|
|
||||||
*/
|
|
||||||
class EventDispatcher<E extends Record<string, EventCallback>> {
|
|
||||||
// 声明_events,用于存储事件和对应的监听器
|
|
||||||
private _events: Map<keyof E, Set<EventCallback>>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this._events = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* on 方法,向指定的事件添加监听器,并返回一个用于移除该监听器的函数
|
|
||||||
* @param event
|
|
||||||
* @param listener
|
|
||||||
*/
|
|
||||||
on(event: keyof E, listener: E[keyof E]): () => void {
|
|
||||||
if (!this._events.has(event)) {
|
|
||||||
this._events.set(event, new Set());
|
|
||||||
}
|
|
||||||
const listeners = this._events.get(event)!;
|
|
||||||
listeners.add(listener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.removeListener(event, listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* removeListener 方法,移除指定事件的监听器
|
|
||||||
* @param event
|
|
||||||
* @param listener
|
|
||||||
*/
|
|
||||||
removeListener(event: keyof E, listener: E[keyof E]): void {
|
|
||||||
if (!this._events.has(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const listeners = this._events.get(event)!;
|
|
||||||
listeners.delete(listener);
|
|
||||||
if (listeners.size === 0) {
|
|
||||||
this._events.delete(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* emit 方法,触发指定事件,并按照监听器注册顺序执行监听器
|
|
||||||
* @param event
|
|
||||||
* @param args
|
|
||||||
*/
|
|
||||||
emit(event: keyof E, ...args: Parameters<E[keyof E]>): void {
|
|
||||||
if (!this._events.has(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取该事件对应的监听器集合,并按照注册顺序执行每个监听器
|
|
||||||
const listeners = this._events.get(event)!;
|
|
||||||
for (const listener of listeners) {
|
|
||||||
listener.apply(this, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EventDispatcher;
|
|
|
@ -1,42 +0,0 @@
|
||||||
/**
|
|
||||||
* 将parse后的Token数组针对不同的匀速类型进行处理
|
|
||||||
*/
|
|
||||||
enum TokenType {
|
|
||||||
octothorpe = 'OCTOTHORPE',
|
|
||||||
argument = 'ARGUMENT',
|
|
||||||
function = 'FUNCTION',
|
|
||||||
}
|
|
||||||
|
|
||||||
const processToken = token => {
|
|
||||||
if (typeof token === 'string') {
|
|
||||||
return token;
|
|
||||||
} else if (TokenType[token.type] === 'OCTOTHORPE') { // token为符号
|
|
||||||
return '#';
|
|
||||||
} else if (TokenType[token.type] === 'ARGUMENT') { // token为变量
|
|
||||||
return [token.arg];
|
|
||||||
} else if (TokenType[token.type] === 'FUNCTION') { // token为函数方法
|
|
||||||
const _param = token.param && token.param.tokens[0];
|
|
||||||
const param = typeof _param === 'string' ? _param.trim() : _param;
|
|
||||||
return [token.arg, token.key, param].filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = token.offset ? parseInt(token.offset) : undefined;
|
|
||||||
|
|
||||||
const tempFormatProps = {};
|
|
||||||
token.cases.forEach(item => {
|
|
||||||
tempFormatProps[item.key] = getTokenAST(item.tokens);
|
|
||||||
});
|
|
||||||
|
|
||||||
const mergedProps = Object.assign({}, { offset }, tempFormatProps);
|
|
||||||
|
|
||||||
return [token.arg, token.type, mergedProps];
|
|
||||||
};
|
|
||||||
|
|
||||||
function getTokenAST(tokens) {
|
|
||||||
if (!Array.isArray(tokens)) {
|
|
||||||
return tokens.join('');
|
|
||||||
}
|
|
||||||
return tokens.map(token => processToken(token));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getTokenAST;
|
|
|
@ -1,207 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getType(input: any): string {
|
|
||||||
const str: string = Object.prototype.toString.call(input);
|
|
||||||
return str.slice(8, -1).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
const createTypeChecker = (type: string) => {
|
|
||||||
return (input: any) => {
|
|
||||||
return getType(input) === type.toLowerCase();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkObject = (input: any) => input !== null && typeof input === 'object';
|
|
||||||
|
|
||||||
const checkRegExp = createTypeChecker('RegExp');
|
|
||||||
|
|
||||||
const checkSticky = () => typeof new RegExp('')?.sticky === 'boolean';
|
|
||||||
|
|
||||||
// 转义正则表达式中的特殊字符
|
|
||||||
function transferReg(s: string): string {
|
|
||||||
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算正则表达式中捕获组的数量
|
|
||||||
function getRegGroups(s: string): number {
|
|
||||||
const re = new RegExp('|' + s);
|
|
||||||
return re.exec('')?.length! - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建一个捕获组的正则表达式模式
|
|
||||||
function getRegCapture(s: string): string {
|
|
||||||
return '(' + s + ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将正则表达式合并为一个联合的正则表达式模式
|
|
||||||
function getRegUnion(regexps: string[]): string {
|
|
||||||
if (!regexps.length) {
|
|
||||||
return '(?!)';
|
|
||||||
}
|
|
||||||
const source = regexps.map(s => '(?:' + s + ')').join('|');
|
|
||||||
return '(?:' + source + ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getReg(input: string | Record<string, any>): string {
|
|
||||||
if (typeof input === 'string') {
|
|
||||||
return '(?:' + transferReg(input) + ')';
|
|
||||||
} else if (checkRegExp(input) || checkObject(input)) {
|
|
||||||
if (input.ignoreCase) {
|
|
||||||
throw new Error('/i 标志禁止使用');
|
|
||||||
}
|
|
||||||
if (input.global) {
|
|
||||||
throw new Error('/g 标志禁止使用');
|
|
||||||
}
|
|
||||||
if (input.sticky) {
|
|
||||||
throw new Error('/y 标志禁止使用');
|
|
||||||
}
|
|
||||||
if (input.multiline) {
|
|
||||||
throw new Error('/m 标志禁止使用');
|
|
||||||
}
|
|
||||||
return input.source;
|
|
||||||
} else {
|
|
||||||
throw new Error(`${input}不符合规范!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRulesByObject(object: Record<string, any>) {
|
|
||||||
const keys = Object.getOwnPropertyNames(object);
|
|
||||||
|
|
||||||
// 存储最终的规则数组
|
|
||||||
const result: any[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
|
||||||
const key = keys[i];
|
|
||||||
const thing = object[key];
|
|
||||||
|
|
||||||
// 将属性值转换为规则数组
|
|
||||||
const rules = [].concat(thing);
|
|
||||||
|
|
||||||
// 如果属性名为 'include',表示需要包含其他规则
|
|
||||||
if (key === 'include') {
|
|
||||||
for (let j = 0; j < rules.length; j++) {
|
|
||||||
result.push({ include: rules[j] });
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用于保存当前规则的匹配模式
|
|
||||||
let match = [];
|
|
||||||
rules.forEach(function (rule) {
|
|
||||||
if (checkObject(rule)) {
|
|
||||||
// 如果规则是一个对象,表示具有选项设置,添加该规则到结果数组中,并重置匹配模式数组
|
|
||||||
if (match.length) result.push(getRuleOptions(key, match));
|
|
||||||
result.push(getRuleOptions(key, rule));
|
|
||||||
match = [];
|
|
||||||
} else {
|
|
||||||
match.push(rule);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 如果匹配模式数组中还有剩余的匹配模式,创建规则对象并添加到结果数组中
|
|
||||||
if (match.length) result.push(getRuleOptions(key, match));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRulesByArray(array: any[]) {
|
|
||||||
const result: any[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
|
||||||
const obj = array[i];
|
|
||||||
|
|
||||||
// 如果元素具有 'include' 属性,表示需要包含其他规则
|
|
||||||
if (obj.include) {
|
|
||||||
const include = [].concat(obj.include);
|
|
||||||
for (let j = 0; j < include.length; j++) {
|
|
||||||
result.push({ include: include[j] });
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!obj.type) {
|
|
||||||
throw new Error('Rule 没有 type 属性');
|
|
||||||
}
|
|
||||||
result.push(getRuleOptions(obj.type, obj));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRuleOptions(type, obj) {
|
|
||||||
// 如果 obj 不是一个对象,则将其转换为包含 'match' 属性的对象
|
|
||||||
if (!checkObject(obj)) {
|
|
||||||
obj = { match: obj };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果 obj 包含 'include' 属性,则抛出错误,因为匹配规则不能包含状态
|
|
||||||
if (obj.include) {
|
|
||||||
throw new Error('匹配规则不能包含状态!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建默认的选项对象,初始化各个选项属性
|
|
||||||
const options: Record<string, any> = {
|
|
||||||
defaultType: type,
|
|
||||||
lineBreaks: !!obj.error || !!obj.fallback,
|
|
||||||
pop: false,
|
|
||||||
next: null,
|
|
||||||
push: null,
|
|
||||||
error: false,
|
|
||||||
fallback: false,
|
|
||||||
value: null,
|
|
||||||
type: null,
|
|
||||||
shouldThrow: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(options, obj);
|
|
||||||
|
|
||||||
if (typeof options.type === 'string' && type !== options.type) {
|
|
||||||
throw new Error('type 属性不能为字符串!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = options.match;
|
|
||||||
if (Array.isArray(match)) {
|
|
||||||
options.match = match;
|
|
||||||
} else if (match) {
|
|
||||||
options.match = [match];
|
|
||||||
} else {
|
|
||||||
options.match = [];
|
|
||||||
}
|
|
||||||
options.match.sort((a, b) => {
|
|
||||||
// 根据规则的类型进行排序,确保正则表达式排在最前面,长度较长的规则排在前面
|
|
||||||
if (checkRegExp(a) && checkRegExp(b)) {
|
|
||||||
return 0;
|
|
||||||
} else if (checkRegExp(b)) {
|
|
||||||
return -1;
|
|
||||||
} else if (checkRegExp(a)) {
|
|
||||||
return +1;
|
|
||||||
} else {
|
|
||||||
return b.length - a.length;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRules(spec) {
|
|
||||||
return Array.isArray(spec) ? getRulesByArray(spec) : getRulesByObject(spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ruleUtils = {
|
|
||||||
checkObject,
|
|
||||||
checkRegExp,
|
|
||||||
transferReg,
|
|
||||||
checkSticky,
|
|
||||||
getRegGroups,
|
|
||||||
getRegCapture,
|
|
||||||
getRegUnion,
|
|
||||||
getReg,
|
|
||||||
getRulesByObject,
|
|
||||||
getRulesByArray,
|
|
||||||
getRuleOptions,
|
|
||||||
getRules,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ruleUtils;
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import { CompiledMessage } from "../types/types";
|
|
||||||
import parse from "../parser/parser";
|
|
||||||
import getTokenAST from "./getTokenAST";
|
|
||||||
import I18n from "../core/I18n";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function isVariantI18n(i18n?: I18n) {
|
|
||||||
if (!i18n) {
|
|
||||||
throw new Error(`I18n object is not found!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateKey<T>(locales?: string | string[], options: T = {} as T) {
|
|
||||||
const localeKey = Array.isArray(locales) ? locales.sort().join('-') : locales;
|
|
||||||
return `${localeKey}:${JSON.stringify(options)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function compile(message: string): CompiledMessage {
|
|
||||||
try {
|
|
||||||
return getTokenAST(parse(message));
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Message cannot be parse due to syntax errors: ${message}`);
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const utils = {
|
|
||||||
isVariantI18n,
|
|
||||||
generateKey,
|
|
||||||
compile,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default utils;
|
|
|
@ -1,125 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import I18n from '../../src/core/I18n';
|
|
||||||
|
|
||||||
describe('I18n', () => {
|
|
||||||
it('load catalog and merge with existing', () => {
|
|
||||||
const i18n = new I18n({});
|
|
||||||
const messages = {
|
|
||||||
Hello: 'Hello',
|
|
||||||
};
|
|
||||||
|
|
||||||
i18n.loadMessage('en', messages);
|
|
||||||
i18n.changeLanguage('en');
|
|
||||||
expect(i18n.messages).toEqual(messages);
|
|
||||||
i18n.loadMessage('fr', { Hello: 'Salut' });
|
|
||||||
expect(i18n.messages).toEqual(messages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load multiple language ', function () {
|
|
||||||
const enMessages = {
|
|
||||||
Hello: 'Hello',
|
|
||||||
};
|
|
||||||
const frMessage = {
|
|
||||||
Hello: 'Salut',
|
|
||||||
};
|
|
||||||
const intl = new I18n({});
|
|
||||||
intl.loadMessage({
|
|
||||||
en: enMessages,
|
|
||||||
fr: frMessage,
|
|
||||||
});
|
|
||||||
intl.changeLanguage('en');
|
|
||||||
expect(intl.messages).toEqual(enMessages);
|
|
||||||
|
|
||||||
intl.changeLanguage('fr');
|
|
||||||
expect(intl.messages).toEqual(frMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch active locale', () => {
|
|
||||||
const messages = {
|
|
||||||
Hello: 'Salut',
|
|
||||||
};
|
|
||||||
|
|
||||||
const i18n = new I18n({
|
|
||||||
locale: 'en',
|
|
||||||
messages: {
|
|
||||||
fr: messages,
|
|
||||||
en: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(i18n.locale).toEqual('en');
|
|
||||||
expect(i18n.messages).toEqual({});
|
|
||||||
|
|
||||||
i18n.changeLanguage('fr');
|
|
||||||
expect(i18n.locale).toEqual('fr');
|
|
||||||
expect(i18n.messages).toEqual(messages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch active locale', () => {
|
|
||||||
const messages = {
|
|
||||||
Hello: 'Salut',
|
|
||||||
};
|
|
||||||
|
|
||||||
const i18n = new I18n({
|
|
||||||
locale: 'en',
|
|
||||||
messages: {
|
|
||||||
en: messages,
|
|
||||||
fr:{}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
i18n.changeLanguage('en');
|
|
||||||
expect(i18n.locale).toEqual('en');
|
|
||||||
expect(i18n.messages).toEqual(messages);
|
|
||||||
i18n.changeLanguage('fr');
|
|
||||||
expect(i18n.locale).toEqual('fr');
|
|
||||||
expect(i18n.messages).toEqual({});
|
|
||||||
});
|
|
||||||
it('._ allow escaping syntax characters', () => {
|
|
||||||
const messages = {
|
|
||||||
"My ''name'' is '{name}'": "Mi ''nombre'' es '{name}'",
|
|
||||||
};
|
|
||||||
const i18n = new I18n({
|
|
||||||
locale: 'es',
|
|
||||||
messages: { es: messages },
|
|
||||||
});
|
|
||||||
expect(i18n.formatMessage("My ''name'' is '{name}'")).toEqual("Mi 'nombre' es {name}");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('._ should format message from catalog', function () {
|
|
||||||
const messages = {
|
|
||||||
Hello: 'Salut',
|
|
||||||
id: "Je m'appelle {name}",
|
|
||||||
};
|
|
||||||
const i18n = new I18n({
|
|
||||||
locale: 'fr',
|
|
||||||
messages: { fr: messages },
|
|
||||||
});
|
|
||||||
expect(i18n.locale).toEqual('fr');
|
|
||||||
expect(i18n.formatMessage('Hello')).toEqual('Salut');
|
|
||||||
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual("Je m'appelle Fred");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the formatted date and time', () => {
|
|
||||||
const i18n = new I18n({
|
|
||||||
locale: 'fr',
|
|
||||||
});
|
|
||||||
const formattedDateTime = i18n.formatDate('2023-06-06T07:53:54.465Z', {
|
|
||||||
dateStyle: 'full',
|
|
||||||
timeStyle: 'short',
|
|
||||||
});
|
|
||||||
expect(typeof formattedDateTime).toBe('string');
|
|
||||||
expect(formattedDateTime).toEqual('mardi 6 juin 2023 à 15:53');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the formatted number', () => {
|
|
||||||
const i18n = new I18n({
|
|
||||||
locale: 'en',
|
|
||||||
});
|
|
||||||
const formattedNumber = i18n.formatNumber(123456.789, { style: 'currency', currency: 'USD' });
|
|
||||||
expect(typeof formattedNumber).toBe('string');
|
|
||||||
expect(formattedNumber).toEqual('$123,456.79');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import * as React from 'react';
|
|
||||||
import I18nProvider from '../../../src/core/components/I18nProvider';
|
|
||||||
import { FormattedMessage } from '../../../index';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
import {createI18nInstance} from "../../../src/core/I18n";
|
|
||||||
|
|
||||||
const dummyContext = React.createContext('');
|
|
||||||
const { Provider: DummyProvider, Consumer: DummyConsumer } = dummyContext;
|
|
||||||
|
|
||||||
describe('<FormattedMessage>', () => {
|
|
||||||
const enMessage = {
|
|
||||||
hello: '你好',
|
|
||||||
id: "Je m'appelle {name}",
|
|
||||||
};
|
|
||||||
const locale = 'en';
|
|
||||||
|
|
||||||
const i18n = createI18nInstance({
|
|
||||||
locale: locale,
|
|
||||||
messages: enMessage,
|
|
||||||
});
|
|
||||||
it('should format context', function () {
|
|
||||||
const { getByTestId } = render(
|
|
||||||
<I18nProvider key={locale} locale={locale} messages={enMessage}>
|
|
||||||
<span data-testid="id">
|
|
||||||
<FormattedMessage data-testid="id" id={enMessage.hello} />
|
|
||||||
</span>
|
|
||||||
</I18nProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('hello', '', {}));
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
it('should format context', function () {
|
|
||||||
const props = {
|
|
||||||
id: enMessage.id,
|
|
||||||
values: { name: 'fred' },
|
|
||||||
};
|
|
||||||
const { getByTestId } = render(
|
|
||||||
<I18nProvider key={locale} locale={'en'} messages={enMessage}>
|
|
||||||
<span data-testid="id">
|
|
||||||
<FormattedMessage data-testid="id" id={props.id} values={props.values} />
|
|
||||||
</span>
|
|
||||||
</I18nProvider>
|
|
||||||
);
|
|
||||||
expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('id', { name: 'fred' }, {}));
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import { act, render } from '@testing-library/react';
|
|
||||||
import '@testing-library/jest-dom/extend-expect';
|
|
||||||
import { IntlProvider } from '../../../index';
|
|
||||||
import {createI18nInstance} from "../../../src/core/I18n";
|
|
||||||
|
|
||||||
describe('I18nProvider', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
|
||||||
const locale = 'en';
|
|
||||||
const i18n = createI18nInstance({
|
|
||||||
locale: locale,
|
|
||||||
});
|
|
||||||
it('should re-render on locale changes', () => {
|
|
||||||
const CurrentLocale = () => {
|
|
||||||
return <span>{i18n.locale}</span>;
|
|
||||||
};
|
|
||||||
const { container } = render(
|
|
||||||
<IntlProvider key={locale} locale={locale} messages={{}}>
|
|
||||||
<CurrentLocale />
|
|
||||||
</IntlProvider>
|
|
||||||
);
|
|
||||||
// First render — no output, because locale isn't activated
|
|
||||||
expect(container.textContent).toEqual('en');
|
|
||||||
|
|
||||||
// act函数值当组件需要被重新渲染的时候进行调度
|
|
||||||
act(() => {
|
|
||||||
i18n.loadMessage('en', {});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(container.textContent).toEqual('en');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
i18n.loadMessage('cs', {});
|
|
||||||
i18n.changeLanguage('cs');
|
|
||||||
});
|
|
||||||
// After loading and activating locale, it's finally rendered.
|
|
||||||
setTimeout(() => {
|
|
||||||
expect(container.textContent).toEqual('cs');
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should subscribe for locale changes', () => {
|
|
||||||
const i18n = createI18nInstance();
|
|
||||||
i18n.on = jest.fn(() => jest.fn());
|
|
||||||
expect(i18n.on).not.toBeCalled();
|
|
||||||
render(
|
|
||||||
<IntlProvider
|
|
||||||
key={locale}
|
|
||||||
locale={locale}
|
|
||||||
messages={{}}
|
|
||||||
>
|
|
||||||
<p />
|
|
||||||
</IntlProvider>
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
|
||||||
expect(i18n.on).toBeCalledWith('change', expect.anything());
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
it('should subscribe for locale changes when param i18n', () => {
|
|
||||||
const i18n = createI18nInstance();
|
|
||||||
i18n.on = jest.fn(() => jest.fn());
|
|
||||||
expect(i18n.on).not.toBeCalled();
|
|
||||||
render(
|
|
||||||
<IntlProvider i18n={i18n}>
|
|
||||||
<p />
|
|
||||||
</IntlProvider>
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
|
||||||
expect(i18n.on).toBeCalledWith('change', expect.anything());
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render children', () => {
|
|
||||||
const child = <div data-testid="child" />;
|
|
||||||
|
|
||||||
const { getByTestId } = render(
|
|
||||||
<IntlProvider key={locale} locale={locale} messages={{}}>
|
|
||||||
{child}
|
|
||||||
</IntlProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByTestId('child')).toBeTruthy(); // toBeTruthy()匹配任何if语句为真
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import { injectIntl, IntlProvider } from '../../../index';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
|
|
||||||
const mountWithProvider = (el: JSX.Element) => render(<IntlProvider locale="en">{el}</IntlProvider>);
|
|
||||||
|
|
||||||
describe('InjectIntl', () => {
|
|
||||||
let Wrapped;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
Wrapped = ({ i18n }: { i18n }) => <div data-testid="test">{JSON.stringify(i18n)}</div>;
|
|
||||||
Wrapped.someStatic = {
|
|
||||||
type: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows introspection access to the wrapped component', () => {
|
|
||||||
expect((injectIntl(Wrapped) as any).WrappedComponent).toBe(Wrapped);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(' should copy statics', () => {
|
|
||||||
expect((injectIntl(Wrapped) as any).someStatic.type).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws when InjectI18n is missing from ancestry', () => {
|
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
||||||
const Injected = injectIntl(Wrapped);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
expect(() => render(<Injected />)).toThrow("Cannot read properties of null (reading 'i18n')");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain all props in WrappedComponent when use InjectI18n', () => {
|
|
||||||
const Injected = injectIntl(Wrapped) as any;
|
|
||||||
const props = {
|
|
||||||
foo: 'bar',
|
|
||||||
};
|
|
||||||
|
|
||||||
const { getByTestId } = mountWithProvider(<Injected {...props} />);
|
|
||||||
expect(getByTestId('test')).toHaveTextContent('{"_events":{},"locale":"en","locales":["en"],"allMessages":{},"_localeData":{}}');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import createI18n from '../../src/core/createI18n';
|
|
||||||
|
|
||||||
describe('createI18n', () => {
|
|
||||||
it('createIntl', function () {
|
|
||||||
const i18n = createI18n({
|
|
||||||
locale: 'en',
|
|
||||||
messages: {
|
|
||||||
foo: 'bar',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
i18n.formatMessage({
|
|
||||||
id: 'foo',
|
|
||||||
})
|
|
||||||
).toBe('bar');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not warn when defaultRichTextElements is not used', function () {
|
|
||||||
const onWarn = jest.fn();
|
|
||||||
createI18n({
|
|
||||||
locale: 'en',
|
|
||||||
messages: {
|
|
||||||
foo: 'bar',
|
|
||||||
},
|
|
||||||
onWarn,
|
|
||||||
});
|
|
||||||
expect(onWarn).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,116 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Translation from '../../src/format/Translation';
|
|
||||||
|
|
||||||
describe('Translation', () => {
|
|
||||||
let translation;
|
|
||||||
beforeEach(() => {
|
|
||||||
// 在每个测试之前创建一个新的 Translation 实例
|
|
||||||
const compiledMessage = ['Hello, ', ['name', 'text', null], '!'];
|
|
||||||
const locale = 'en';
|
|
||||||
const locales = {};
|
|
||||||
const localeConfig = {};
|
|
||||||
const useMemorize = true;
|
|
||||||
translation = new Translation(compiledMessage, locale, locales, localeConfig, useMemorize);
|
|
||||||
});
|
|
||||||
describe('formatMessage', () => {
|
|
||||||
it('should return the message if it is not an array', () => {
|
|
||||||
const result = translation.formatMessage('test message', jest.fn());
|
|
||||||
expect(result).toBe('test message');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should concatenate string tokens in the message array', () => {
|
|
||||||
const result = translation.formatMessage(['Hello, ', 'World!'], jest.fn());
|
|
||||||
expect(result).toBe('Hello, World!');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle token arrays and use ctx to get the value', () => {
|
|
||||||
const ctx = jest.fn().mockReturnValue('Hello');
|
|
||||||
const result = translation.formatMessage([['name', 'type', 'format']], ctx);
|
|
||||||
expect(result).toBe('Hello');
|
|
||||||
expect(ctx).toHaveBeenCalledWith('name', 'type', 'format');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip null values returned by ctx', () => {
|
|
||||||
const ctx = jest.fn().mockReturnValue(null);
|
|
||||||
const result = translation.formatMessage([['name', 'type', 'format'], 'World!'], ctx);
|
|
||||||
expect(result).toBe('World!');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle nested formats in the token array', () => {
|
|
||||||
const ctx = jest.fn((name, type, format) => format.value);
|
|
||||||
const formatObject = {
|
|
||||||
value: 'Hello',
|
|
||||||
};
|
|
||||||
const result = translation.formatMessage([['name', 'type', formatObject], ', World!'], ctx);
|
|
||||||
expect(result).toBe('Hello, World!');
|
|
||||||
});
|
|
||||||
it('should return a string when compiledMessage is a string', () => {
|
|
||||||
const compiledMessage = 'Hello, world!';
|
|
||||||
const textFormatter = jest.fn();
|
|
||||||
const result = translation.formatMessage(compiledMessage, textFormatter);
|
|
||||||
|
|
||||||
expect(result).toBe('Hello, world!');
|
|
||||||
expect(textFormatter).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format a message with placeholders', () => {
|
|
||||||
const compiledMessage = ['Hello, ', ['name', 'text', null], '!'];
|
|
||||||
// @ts-ignore
|
|
||||||
const textFormatter = jest.fn((name, type, format) => {
|
|
||||||
if (name === 'name') {
|
|
||||||
return 'John';
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
const result = translation.formatMessage(compiledMessage, textFormatter);
|
|
||||||
|
|
||||||
expect(result).toBe('Hello, John!');
|
|
||||||
expect(textFormatter).toHaveBeenCalledWith('name', 'text', null);
|
|
||||||
expect(textFormatter).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format a message with formatted placeholders', () => {
|
|
||||||
const compiledMessage = ['Hello, ', ['name', 'text', { text: 'Lowercase' }], '!'];
|
|
||||||
// @ts-ignore
|
|
||||||
const textFormatter = jest.fn((name, type, format) => {
|
|
||||||
if (name === 'name') {
|
|
||||||
return 'John';
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
const result = translation.formatMessage(compiledMessage, textFormatter);
|
|
||||||
|
|
||||||
expect(result).toBe('Hello, John!');
|
|
||||||
expect(textFormatter).toHaveBeenCalledWith('name', 'text', { text: 'Lowercase' });
|
|
||||||
expect(textFormatter).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should recursively format a message with nested placeholders', () => {
|
|
||||||
const compiledMessage = [
|
|
||||||
'Hello, ',
|
|
||||||
['name', 'text', null],
|
|
||||||
'! Your age is ',
|
|
||||||
['age', 'number', { style: 'decimal' }],
|
|
||||||
'.',
|
|
||||||
];
|
|
||||||
// @ts-ignore
|
|
||||||
const textFormatter = jest.fn((name, type, format) => {
|
|
||||||
if (name === 'name') {
|
|
||||||
return 'John';
|
|
||||||
} else if (name === 'age') {
|
|
||||||
return '30';
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
const result = translation.formatMessage(compiledMessage, textFormatter);
|
|
||||||
|
|
||||||
expect(result).toBe('Hello, John! Your age is 30.');
|
|
||||||
expect(textFormatter).toHaveBeenCalledWith('name', 'text', null);
|
|
||||||
expect(textFormatter).toHaveBeenCalledWith('age', 'number', { style: 'decimal' });
|
|
||||||
expect(textFormatter).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import creatI18nCache from '../../../src/format/cache/cache';
|
|
||||||
|
|
||||||
describe('creatI18nCache', () => {
|
|
||||||
it('should create an empty IntlCache object', () => {
|
|
||||||
const intlCache = creatI18nCache();
|
|
||||||
|
|
||||||
expect(intlCache).toEqual({
|
|
||||||
dateTimeFormat: {},
|
|
||||||
numberFormat: {},
|
|
||||||
messages: {},
|
|
||||||
plurals: {},
|
|
||||||
select: {},
|
|
||||||
octothorpe: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import Translation from '../../src/format/Translation';
|
|
||||||
import utils from '../../src/utils/utils';
|
|
||||||
|
|
||||||
describe('compile', function () {
|
|
||||||
const englishPlurals = {
|
|
||||||
plurals(value, ordinal) {
|
|
||||||
if (ordinal) {
|
|
||||||
return (
|
|
||||||
{
|
|
||||||
'1': 'one',
|
|
||||||
'2': 'two',
|
|
||||||
'3': 'few',
|
|
||||||
}[value] || 'other'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return value === 1 ? 'one' : 'other';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const prepare = (message, locale?, locales?) => {
|
|
||||||
const translation = new Translation(utils.compile(message), locale || 'en', locales, englishPlurals);
|
|
||||||
return translation.translate.bind(translation);
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should compile message with variable', function () {
|
|
||||||
const cache = utils.compile('Hey {name}!');
|
|
||||||
expect(new Translation(cache, 'en', [], {}).translate({ name: 'Joe' })).toEqual('Hey Joe!');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should compile select', function () {
|
|
||||||
const translate = prepare('{value, select, female {She} other {They}}');
|
|
||||||
expect(translate({ value: 'female' })).toEqual('She');
|
|
||||||
expect(translate({ value: 'n/a' })).toEqual('They');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should compile plurals', function () {
|
|
||||||
const translate = prepare('{value, plural, one {{value} Book} other {# Books}}');
|
|
||||||
expect(translate({ value: 1 })).toEqual('1 Book');
|
|
||||||
expect(translate({ value: 2 })).toEqual('2 Books');
|
|
||||||
|
|
||||||
|
|
||||||
const translatePlurals = prepare('{value, plural, offset:1 =0 {No Books} one {# Book} other {# Books}}');
|
|
||||||
|
|
||||||
expect(translatePlurals({ value: 0 })).toEqual('No Books');
|
|
||||||
expect(translatePlurals({ value: 2 })).toEqual('1 Book');
|
|
||||||
expect(translatePlurals({ value: 3 })).toEqual('2 Books');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should compile selectordinal', function () {
|
|
||||||
const translateSelectordinal = prepare('{value, selectordinal, one {#st Book} two {#nd Book}}');
|
|
||||||
expect(translateSelectordinal({ value: 1 })).toEqual('1st Book');
|
|
||||||
expect(translateSelectordinal({ value: 2 })).toEqual('2nd Book');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,89 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DateTimeFormatter } from '../../../index';
|
|
||||||
|
|
||||||
describe('DateTimeFormatter', () => {
|
|
||||||
const date = new Date('2023-04-03T12:34:56Z');
|
|
||||||
it('date formatter is memoized', async () => {
|
|
||||||
const firstRunt0 = performance.now();
|
|
||||||
const dateTimeFormatter1 = new DateTimeFormatter('es', {});
|
|
||||||
dateTimeFormatter1.dateTimeFormat(new Date());
|
|
||||||
const firstRunt1 = performance.now();
|
|
||||||
const firstRunResult = firstRunt1 - firstRunt0;
|
|
||||||
|
|
||||||
const seconddRunt0 = performance.now();
|
|
||||||
const dateTimeFormatter2 = new DateTimeFormatter('es', {}, false);
|
|
||||||
dateTimeFormatter2.dateTimeFormat(new Date());
|
|
||||||
const seconddRunt1 = performance.now();
|
|
||||||
const secondRunResult = seconddRunt1 - seconddRunt0;
|
|
||||||
|
|
||||||
expect(secondRunResult).toBeLessThan(firstRunResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format a date using default options', () => {
|
|
||||||
const formatted = new DateTimeFormatter('en').dateTimeFormat(date);
|
|
||||||
expect(formatted).toEqual('4/3/2023');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format a date using custom options', () => {
|
|
||||||
const formatted = new DateTimeFormatter('en-US', { weekday: 'long' }).dateTimeFormat(date);
|
|
||||||
expect(formatted).toEqual('Monday');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse a date string and format it', () => {
|
|
||||||
const formatted = new DateTimeFormatter('en-US').dateTimeFormat(date.toISOString());
|
|
||||||
expect(formatted).toEqual('4/3/2023');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should memoize formatter instances by options and locales', () => {
|
|
||||||
const spy = jest.spyOn(Intl, 'DateTimeFormat');
|
|
||||||
const formatter1 = new DateTimeFormatter('en-US', { month: 'short' });
|
|
||||||
const formatter2 = new DateTimeFormatter('en-US', { month: 'short' });
|
|
||||||
const formatter3 = new DateTimeFormatter('en-GB', { month: 'short' });
|
|
||||||
formatter1.dateTimeFormat(date);
|
|
||||||
formatter2.dateTimeFormat(date);
|
|
||||||
formatter3.dateTimeFormat(date);
|
|
||||||
expect(spy).toHaveBeenCalledWith('en-US', { month: 'short' });
|
|
||||||
expect(spy).toHaveBeenCalledWith('en-GB', { month: 'short' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not memoize formatter instances when memoize is false', () => {
|
|
||||||
const spy = jest.spyOn(Intl, 'DateTimeFormat');
|
|
||||||
const formatter1 = new DateTimeFormatter('en-US', { month: 'short' }, false);
|
|
||||||
const formatter2 = new DateTimeFormatter('en-US', { month: 'short' }, false);
|
|
||||||
formatter1.dateTimeFormat(date);
|
|
||||||
formatter2.dateTimeFormat(date);
|
|
||||||
expect(spy).toHaveBeenCalledTimes(5);
|
|
||||||
expect(spy).toHaveBeenCalledWith('en-US', { month: 'short' });
|
|
||||||
});
|
|
||||||
it('should format a Date object correctly', () => {
|
|
||||||
const formatter = new DateTimeFormatter('en-US');
|
|
||||||
const date = new Date(2023, 0, 1);
|
|
||||||
const formatted = formatter.dateTimeFormat(date);
|
|
||||||
expect(formatted).toEqual('1/1/2023');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format a string representation of date correctly', () => {
|
|
||||||
const formatter = new DateTimeFormatter('en-US');
|
|
||||||
const dateString = '2023-01-01';
|
|
||||||
const formatted = formatter.dateTimeFormat(dateString);
|
|
||||||
expect(formatted).toEqual('1/1/2023');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format using specified format options', () => {
|
|
||||||
const formatter = new DateTimeFormatter('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
||||||
const date = new Date(2023, 0, 1);
|
|
||||||
const formatted = formatter.dateTimeFormat(date);
|
|
||||||
expect(formatted).toEqual('January 1, 2023');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format using memorized formatter when useMemorize is true', () => {
|
|
||||||
const formatter = new DateTimeFormatter('en-US',{"year":'numeric'}, true);
|
|
||||||
const date = new Date(2023, 0, 1);
|
|
||||||
const formatted1 = formatter.dateTimeFormat(date);
|
|
||||||
const formatted2 = formatter.dateTimeFormat(date);
|
|
||||||
expect(formatted1).toEqual(formatted2);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,88 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NumberFormatter } from '../../../index';
|
|
||||||
|
|
||||||
describe('NumberFormatter', () => {
|
|
||||||
it('number formatter is memoized', async () => {
|
|
||||||
const firstRunt0 = performance.now();
|
|
||||||
const numberFormatter1 = new NumberFormatter('es', {});
|
|
||||||
numberFormatter1.numberFormat(10000);
|
|
||||||
const firstRunt1 = performance.now();
|
|
||||||
const firstRunResult = firstRunt1 - firstRunt0;
|
|
||||||
|
|
||||||
const seconddRunt0 = performance.now();
|
|
||||||
const numberFormatter = new NumberFormatter('es', {}, false);
|
|
||||||
numberFormatter.numberFormat(10000);
|
|
||||||
const secondRunt1 = performance.now();
|
|
||||||
const secondRunResult = secondRunt1 - seconddRunt0;
|
|
||||||
|
|
||||||
expect(secondRunResult).toBeLessThan(firstRunResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('formats a number with default options', () => {
|
|
||||||
const format = new NumberFormatter('en-US');
|
|
||||||
expect(format.numberFormat(1000)).toBe('1,000');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('formats a number with custom options', () => {
|
|
||||||
const format = new NumberFormatter('en-US', { style: 'currency', currency: 'USD' });
|
|
||||||
expect(format.numberFormat(1000)).toBe('$1,000.00');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not memoize the formatter with different options', () => {
|
|
||||||
const format1 = new NumberFormatter('en-US', { style: 'currency', currency: 'USD' });
|
|
||||||
const format2 = new NumberFormatter('en-US', { style: 'currency', currency: 'EUR' });
|
|
||||||
expect(format1).not.toBe(format2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('formats negative numbers', () => {
|
|
||||||
const format = new NumberFormatter('en-US', { style: 'currency', currency: 'USD' });
|
|
||||||
expect(format.numberFormat(-1000)).toBe('-$1,000.00');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('formats numbers with different locales', () => {
|
|
||||||
const format1 = new NumberFormatter('en-US', { style: 'currency', currency: 'USD' });
|
|
||||||
const format2 = new NumberFormatter('fr-FR', { style: 'currency', currency: 'EUR' });
|
|
||||||
expect(format1.numberFormat(1000)).toBe('$1,000.00');
|
|
||||||
|
|
||||||
});
|
|
||||||
it('should format a positive number correctly', () => {
|
|
||||||
const formatter = new NumberFormatter('en-US');
|
|
||||||
const number = 12345.6789;
|
|
||||||
const formatted = formatter.numberFormat(number);
|
|
||||||
expect(formatted).toEqual('12,345.679');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format a negative number correctly', () => {
|
|
||||||
const formatter = new NumberFormatter('en-US');
|
|
||||||
const number = -12345.6789;
|
|
||||||
const formatted = formatter.numberFormat(number);
|
|
||||||
expect(formatted).toEqual('-12,345.679');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format using specified format options', () => {
|
|
||||||
const formatOptions = { style: 'currency', currency: 'EUR' };
|
|
||||||
const formatter = new NumberFormatter('en-US', formatOptions);
|
|
||||||
const number = 12345.6789;
|
|
||||||
const formatted = formatter.numberFormat(number);
|
|
||||||
expect(formatted).toEqual('€12,345.68');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format using memorized formatter when useMemorize is true', () => {
|
|
||||||
const formatter = new NumberFormatter('en-US', undefined, true);
|
|
||||||
const number = 12345.6789;
|
|
||||||
const formatted1 = formatter.numberFormat(number);
|
|
||||||
const formatted2 = formatter.numberFormat(number);
|
|
||||||
expect(formatted1).toEqual(formatted2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a new formatter when useMemorize is false', () => {
|
|
||||||
const formatter = new NumberFormatter('en-US', undefined, false);
|
|
||||||
const number = 12345.6789;
|
|
||||||
const formatted1 = formatter.numberFormat(number);
|
|
||||||
const formatted2 = formatter.numberFormat(number);
|
|
||||||
expect(formatted1).toEqual(formatted2);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import {getFormatMessage} from "../../src/format/getFormatMessage";
|
|
||||||
import I18n from "../../src/core/I18n";
|
|
||||||
|
|
||||||
describe('getFormatMessage', () => {
|
|
||||||
// Mocked i18nInstance object
|
|
||||||
const i18nInstance = new I18n({
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
greeting: 'Hello, {name}!',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
locale: 'en',
|
|
||||||
error: "missingMessage"
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the correct translation for an existing message ID', () => {
|
|
||||||
const id = 'greeting';
|
|
||||||
const values = {name: 'John'};
|
|
||||||
const expectedResult = 'Hello, John!';
|
|
||||||
|
|
||||||
const result = getFormatMessage(i18nInstance, id, values);
|
|
||||||
|
|
||||||
expect(result).toEqual(expectedResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the default message when the translation is missing', () => {
|
|
||||||
const id = 'missingMessage';
|
|
||||||
const expectedResult = 'missingMessage';
|
|
||||||
|
|
||||||
const result = getFormatMessage(i18nInstance, id);
|
|
||||||
|
|
||||||
expect(result).toEqual(expectedResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import { defineMessage, defineMessages } from '../index';
|
|
||||||
|
|
||||||
describe('index', () => {
|
|
||||||
describe('defineMessages Test', () => {
|
|
||||||
it('return the input message object', () => {
|
|
||||||
const messages = {
|
|
||||||
greeting: {
|
|
||||||
id: 'greeting',
|
|
||||||
defaultMessage: 'Hello',
|
|
||||||
},
|
|
||||||
farewell: {
|
|
||||||
id: 'farewell',
|
|
||||||
defaultMessage: 'Goodbye',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = defineMessages(messages);
|
|
||||||
|
|
||||||
expect(result).toEqual(messages);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('defineMessage Test', () => {
|
|
||||||
it('return the input message object', () => {
|
|
||||||
const message = {
|
|
||||||
id: 'greeting',
|
|
||||||
defaultMessage: 'Hello',
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = defineMessage(message);
|
|
||||||
|
|
||||||
expect(result).toEqual(message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import { checkSelectType } from '../../src/parser/parser';
|
|
||||||
|
|
||||||
describe('checkSelectType function', () => {
|
|
||||||
it('should return true for valid select types', () => {
|
|
||||||
expect(checkSelectType('plural')).toBe(true);
|
|
||||||
expect(checkSelectType('select')).toBe(true);
|
|
||||||
expect(checkSelectType('selectordinal')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for invalid select types', () => {
|
|
||||||
expect(checkSelectType('invalid')).toBe(false);
|
|
||||||
expect(checkSelectType('other')).toBe(false);
|
|
||||||
expect(checkSelectType('')).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import { checkStateGroup } from '../../src/parser/parseMappingRule';
|
|
||||||
|
|
||||||
describe('checkStateGroup function', () => {
|
|
||||||
it('should throw an error if state is missing', () => {
|
|
||||||
const group = { push: 'missingState' };
|
|
||||||
const name = 'currentName';
|
|
||||||
const map = { existingState: {} };
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
checkStateGroup(group, name, map);
|
|
||||||
}).toThrowError('The state is missing.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if pop is not 1', () => {
|
|
||||||
const group = { pop: '2' };
|
|
||||||
const name = 'currentName';
|
|
||||||
const map = { existingState: {} };
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
checkStateGroup(group, name, map);
|
|
||||||
}).toThrowError('The value of pop must be 1.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not throw an error if state and pop are valid', () => {
|
|
||||||
const group = { push: 'existingState', pop: '1' };
|
|
||||||
const name = 'currentName';
|
|
||||||
const map = { existingState: {} };
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
checkStateGroup(group, name, map);
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import copyStaticProps from '../../src/utils/copyStaticProps';
|
|
||||||
|
|
||||||
describe('hoistNonReactStatics', () => {
|
|
||||||
test('should hoist static properties from sourceComponent to targetComponent', () => {
|
|
||||||
class SourceComponent {
|
|
||||||
static staticProp = 'sourceProp';
|
|
||||||
}
|
|
||||||
|
|
||||||
class TargetComponent {}
|
|
||||||
|
|
||||||
copyStaticProps(TargetComponent, SourceComponent);
|
|
||||||
|
|
||||||
expect((TargetComponent as any).staticProp).toBe('sourceProp');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should hoist static properties from inherited components', () => {
|
|
||||||
class SourceComponent {
|
|
||||||
static staticProp = 'sourceProp';
|
|
||||||
}
|
|
||||||
|
|
||||||
class InheritedComponent extends SourceComponent {}
|
|
||||||
|
|
||||||
class TargetComponent {}
|
|
||||||
|
|
||||||
copyStaticProps(TargetComponent, InheritedComponent);
|
|
||||||
|
|
||||||
expect((TargetComponent as any).staticProp).toBe('sourceProp');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not hoist properties if descriptor is not valid', () => {
|
|
||||||
class SourceComponent {
|
|
||||||
get staticProp() {
|
|
||||||
return 'sourceProp';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TargetComponent {}
|
|
||||||
|
|
||||||
copyStaticProps(TargetComponent, SourceComponent);
|
|
||||||
|
|
||||||
expect((TargetComponent as any).staticProp).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not hoist properties if descriptor is not valid', () => {
|
|
||||||
class SourceComponent {
|
|
||||||
static get staticProp() {
|
|
||||||
return 'sourceProp';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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', () => {
|
|
||||||
const targetComponent = { staticProp: 'target' };
|
|
||||||
const sourceComponent = { staticProp: 'source' };
|
|
||||||
copyStaticProps(targetComponent, sourceComponent);
|
|
||||||
expect(targetComponent.staticProp).toBe('source'); // The value should remain the same
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,49 +0,0 @@
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import EventEmitter from '../../src/utils/eventListener';
|
|
||||||
|
|
||||||
describe('eventEmitter', () => {
|
|
||||||
it('should call registered event listeners on emit', async () => {
|
|
||||||
const firstListener = jest.fn();
|
|
||||||
const secondListener = jest.fn(() => 'return value is ignored');
|
|
||||||
|
|
||||||
const emitter = new EventEmitter();
|
|
||||||
emitter.on('test', firstListener);
|
|
||||||
emitter.on('test', secondListener);
|
|
||||||
|
|
||||||
emitter.emit('test', 42);
|
|
||||||
|
|
||||||
expect(firstListener).toBeCalledWith(42);
|
|
||||||
expect(secondListener).toBeCalledWith(42);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow unsubscribing from events', () => {
|
|
||||||
const listener = jest.fn();
|
|
||||||
const emitter = new EventEmitter();
|
|
||||||
|
|
||||||
const unsubscribe = emitter.on('test', listener);
|
|
||||||
emitter.emit('test', 42);
|
|
||||||
expect(listener).toBeCalledWith(42);
|
|
||||||
|
|
||||||
listener.mockReset();
|
|
||||||
unsubscribe();
|
|
||||||
emitter.emit('test', 42);
|
|
||||||
expect(listener).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should do nothing when even doesn't exist", () => {
|
|
||||||
const unknown = jest.fn();
|
|
||||||
|
|
||||||
const emitter = new EventEmitter();
|
|
||||||
// this should not throw
|
|
||||||
emitter.emit('test', 42);
|
|
||||||
// this should not throw
|
|
||||||
emitter.removeListener('test', unknown);
|
|
||||||
|
|
||||||
emitter.on('test', jest.fn());
|
|
||||||
// this should not throw
|
|
||||||
emitter.removeListener('test', unknown);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import utils from '../../src/utils/utils';
|
|
||||||
|
|
||||||
describe('generateKey', () => {
|
|
||||||
it('should generate a key for a single locale without options', () => {
|
|
||||||
const result = utils.generateKey('en');
|
|
||||||
expect(result).toBe('en:{}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate a key for multiple locales without options', () => {
|
|
||||||
const result = utils.generateKey(['en', 'fr']);
|
|
||||||
expect(result).toBe('en-fr:{}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sort multiple locales before generating the key', () => {
|
|
||||||
const result = utils.generateKey(['fr', 'en']);
|
|
||||||
expect(result).toBe('en-fr:{}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate a key with options for a single locale', () => {
|
|
||||||
const result = utils.generateKey('en', { foo: 'bar' });
|
|
||||||
expect(result).toBe('en:{"foo":"bar"}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate a key with options for multiple locales', () => {
|
|
||||||
const result = utils.generateKey(['en', 'fr'], { foo: 'bar' });
|
|
||||||
expect(result).toBe('en-fr:{"foo":"bar"}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sort multiple locales and consider options before generating the key', () => {
|
|
||||||
const result = utils.generateKey(['fr', 'en'], { foo: 'bar' });
|
|
||||||
expect(result).toBe('en-fr:{"foo":"bar"}');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,162 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
import getTokenAST from '../../src/utils/getTokenAST';
|
|
||||||
import * as assert from 'assert';
|
|
||||||
|
|
||||||
describe('getTokenAST', () => {
|
|
||||||
it('should return an array containing a string', () => {
|
|
||||||
const result = getTokenAST(['Hello', 'world']);
|
|
||||||
expect(result).toEqual(['Hello', 'world']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle octothorpe tokens', () => {
|
|
||||||
const result = getTokenAST([{ type: 'octothorpe' }, 'Hello']);
|
|
||||||
expect(result).toContain('#');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle argument tokens', () => {
|
|
||||||
const result = getTokenAST([{ type: 'argument', arg: 'foo' }]);
|
|
||||||
expect(result).toEqual([['foo']]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle function tokens with param', () => {
|
|
||||||
const result = getTokenAST([{ type: 'function', arg: 'foo', key: 'bar', param: { tokens: ['baz'] } }]);
|
|
||||||
expect(result).toEqual([['foo', 'bar', 'baz']]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle function tokens without param', () => {
|
|
||||||
const result = getTokenAST([{ type: 'function', arg: 'foo', key: 'bar' }]);
|
|
||||||
expect(result).toEqual([['foo', 'bar']]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle other tokens with offset', () => {
|
|
||||||
const result = getTokenAST([{ type: 'other', arg: 'foo', offset: '1', cases: [{ key: 'one', tokens: ['bar'] }] }]);
|
|
||||||
expect(result).toEqual([['foo', 'other', { offset: 1, one: ['bar'] }]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle other tokens without offset', () => {
|
|
||||||
const result = getTokenAST([{ type: 'other', arg: 'foo', cases: [{ key: 'one', tokens: ['bar'] }] }]);
|
|
||||||
expect(result).toEqual([['foo', 'other', { one: ['bar'] }]]);
|
|
||||||
});
|
|
||||||
it('returns [arg, key, param] if token type is "function"', () => {
|
|
||||||
const tokens = [
|
|
||||||
{
|
|
||||||
type: 'function',
|
|
||||||
arg: 'arg1',
|
|
||||||
key: 'key1',
|
|
||||||
param: { tokens: ['param1'] },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const result = getTokenAST(tokens);
|
|
||||||
expect(result).toEqual([['arg1', 'key1', 'param1']]);
|
|
||||||
});
|
|
||||||
it('If the input parameter is not an array, an error should be thrown.', () => {
|
|
||||||
const input = 'invalid input';
|
|
||||||
assert.throws(() => getTokenAST(input), Error);
|
|
||||||
});
|
|
||||||
it('应该返回包含字符串的数组', () => {
|
|
||||||
const tokens = [
|
|
||||||
'Hello',
|
|
||||||
{ type: 'octothorpe' },
|
|
||||||
{ type: 'argument', arg: 'name' },
|
|
||||||
{
|
|
||||||
type: 'function',
|
|
||||||
arg: 'formatDate',
|
|
||||||
key: 'date',
|
|
||||||
param: {
|
|
||||||
tokens: ['YYYY-MM-DD'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const expected = ['Hello', '#', ['name'], ['formatDate', 'date', 'YYYY-MM-DD']];
|
|
||||||
const result = getTokenAST(tokens);
|
|
||||||
expect(result).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('对于复杂的 tokens 数组,应该返回嵌套的格式化数组', () => {
|
|
||||||
const expected = [
|
|
||||||
'Hello',
|
|
||||||
'#',
|
|
||||||
['name'],
|
|
||||||
['formatDate', 'date', 'YYYY-MM-DD'],
|
|
||||||
[
|
|
||||||
'formatNumber',
|
|
||||||
'number',
|
|
||||||
{
|
|
||||||
cases: [
|
|
||||||
{
|
|
||||||
key: 'uppercase',
|
|
||||||
tokens: [
|
|
||||||
'TRUE',
|
|
||||||
{
|
|
||||||
plural: {
|
|
||||||
cases: {
|
|
||||||
'=0': 'zero',
|
|
||||||
'=1': 'one',
|
|
||||||
other: 'other',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'lowercase',
|
|
||||||
tokens: [
|
|
||||||
'lowercase',
|
|
||||||
'none',
|
|
||||||
{
|
|
||||||
cases: {
|
|
||||||
'=0': 'zero',
|
|
||||||
'=1': 'one',
|
|
||||||
other: 'other',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
offset: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
const tokens = [
|
|
||||||
'Hello',
|
|
||||||
{ type: 'octothorpe' },
|
|
||||||
{ type: 'argument', arg: 'name' },
|
|
||||||
{
|
|
||||||
type: 'function',
|
|
||||||
arg: 'formatDate',
|
|
||||||
key: 'date',
|
|
||||||
param: {
|
|
||||||
tokens: ['YYYY-MM-DD'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'function',
|
|
||||||
arg: 'formatNumber',
|
|
||||||
key: 'number',
|
|
||||||
param: {
|
|
||||||
tokens: [
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
cases: [
|
|
||||||
{
|
|
||||||
key: 'uppercase',
|
|
||||||
tokens: ['TRUE', { plural: { cases: { '=0': 'zero', '=1': 'one', other: 'other' } } }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'lowercase',
|
|
||||||
tokens: ['lowercase', 'none', { cases: { '=0': 'zero', '=1': 'one', other: 'other' } }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = getTokenAST(tokens);
|
|
||||||
expect(result).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,269 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import ruleUtils from '../../src/utils/parseRuleUtils';
|
|
||||||
|
|
||||||
describe('ruleUtils test', () => {
|
|
||||||
describe('getRegGroups function', () => {
|
|
||||||
it('should return the correct number of capturing groups in the regular expression pattern', () => {
|
|
||||||
const pattern1 = 'abc(def)ghi(jkl)mno';
|
|
||||||
const pattern2 = 'abc(def)ghi';
|
|
||||||
const pattern3 = 'abc';
|
|
||||||
const pattern4 = '';
|
|
||||||
|
|
||||||
const result1 = ruleUtils.getRegGroups(pattern1);
|
|
||||||
const result2 = ruleUtils.getRegGroups(pattern2);
|
|
||||||
const result3 = ruleUtils.getRegGroups(pattern3);
|
|
||||||
const result4 = ruleUtils.getRegGroups(pattern4);
|
|
||||||
|
|
||||||
expect(result1).toBe(2);
|
|
||||||
expect(result2).toBe(1);
|
|
||||||
expect(result3).toBe(0);
|
|
||||||
expect(result4).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getReg', () => {
|
|
||||||
it('should return the correct regular expression when input is a string', () => {
|
|
||||||
const input1 = 'abc';
|
|
||||||
const input2 = '^\\d+$';
|
|
||||||
const input3 = '[a-zA-Z]';
|
|
||||||
|
|
||||||
const result1 = ruleUtils.getReg(input1);
|
|
||||||
const result2 = ruleUtils.getReg(input2);
|
|
||||||
const result3 = ruleUtils.getReg(input3);
|
|
||||||
|
|
||||||
expect(result1).toBe('(?:abc)');
|
|
||||||
expect(result2).toBe('(?:\\^\\\\d\\+\\$)');
|
|
||||||
expect(result3).toBe('(?:\\[a\\-zA\\-Z\\])');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error when input is an invalid regular expression object', () => {
|
|
||||||
const input = {
|
|
||||||
source: 'abc',
|
|
||||||
ignoreCase: true,
|
|
||||||
global: false,
|
|
||||||
sticky: false,
|
|
||||||
multiline: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
ruleUtils.getReg(input);
|
|
||||||
}).toThrowError('标志禁止使用');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error when input regular expression object has forbidden flags', () => {
|
|
||||||
const input1 = {
|
|
||||||
source: 'abc',
|
|
||||||
ignoreCase: true,
|
|
||||||
global: false,
|
|
||||||
sticky: false,
|
|
||||||
multiline: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const input2 = {
|
|
||||||
source: 'abc',
|
|
||||||
ignoreCase: false,
|
|
||||||
global: true,
|
|
||||||
sticky: false,
|
|
||||||
multiline: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const input3 = {
|
|
||||||
source: 'abc',
|
|
||||||
ignoreCase: false,
|
|
||||||
global: false,
|
|
||||||
sticky: true,
|
|
||||||
multiline: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const input4 = {
|
|
||||||
source: 'abc',
|
|
||||||
ignoreCase: false,
|
|
||||||
global: false,
|
|
||||||
sticky: false,
|
|
||||||
multiline: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
ruleUtils.getReg(input1);
|
|
||||||
}).toThrowError('/i 标志禁止使用');
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
ruleUtils.getReg(input2);
|
|
||||||
}).toThrowError('/g 标志禁止使用');
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
ruleUtils.getReg(input3);
|
|
||||||
}).toThrowError('/y 标志禁止使用');
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
ruleUtils.getReg(input4);
|
|
||||||
}).toThrowError('/m 标志禁止使用');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getRegUnion function', () => {
|
|
||||||
it('should return the correct regular expression union of input patterns', () => {
|
|
||||||
const patterns1 = ['abc', 'def', 'ghi'];
|
|
||||||
const patterns2 = ['\\d+', '[a-z]+', '[A-Z]+'];
|
|
||||||
const patterns3 = [];
|
|
||||||
|
|
||||||
const result1 = ruleUtils.getRegUnion(patterns1);
|
|
||||||
const result2 = ruleUtils.getRegUnion(patterns2);
|
|
||||||
const result3 = ruleUtils.getRegUnion(patterns3);
|
|
||||||
|
|
||||||
expect(result1).toBe('(?:(?:abc)|(?:def)|(?:ghi))');
|
|
||||||
expect(result2).toBe('(?:(?:\\d+)|(?:[a-z]+)|(?:[A-Z]+))');
|
|
||||||
expect(result3).toBe('(?!)');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getRuleOptions function', () => {
|
|
||||||
it('should return the correct options object with default type', () => {
|
|
||||||
const type = 'content';
|
|
||||||
const obj = { match: 'abc' };
|
|
||||||
|
|
||||||
const result = ruleUtils.getRuleOptions(type, obj);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
defaultType: type,
|
|
||||||
lineBreaks: false,
|
|
||||||
pop: false,
|
|
||||||
next: null,
|
|
||||||
push: null,
|
|
||||||
error: false,
|
|
||||||
fallback: false,
|
|
||||||
value: null,
|
|
||||||
type: null,
|
|
||||||
shouldThrow: false,
|
|
||||||
match: ['abc'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if type property is a string', () => {
|
|
||||||
const type = 'content';
|
|
||||||
const obj = { match: 'abc', type: 'invalid' };
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
ruleUtils.getRuleOptions(type, obj);
|
|
||||||
}).toThrowError('type 属性不能为字符串!');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if match property includes include property', () => {
|
|
||||||
const type = 'content';
|
|
||||||
const obj = { match: 'abc', include: 'state' };
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
ruleUtils.getRuleOptions(type, obj);
|
|
||||||
}).toThrowError('匹配规则不能包含状态!');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sort the match property correctly', () => {
|
|
||||||
const type = 'content';
|
|
||||||
const obj = { match: ['abc', /def/, 'ghi', /[0-9]+/, /^xyz$/] };
|
|
||||||
|
|
||||||
const result = ruleUtils.getRuleOptions(type, obj);
|
|
||||||
|
|
||||||
expect(result.match).toEqual(['abc', 'ghi', /def/, /[0-9]+/, /^xyz$/]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getRulesByArray', () => {
|
|
||||||
it('should return an empty array if the input array is empty', () => {
|
|
||||||
const array = [];
|
|
||||||
const result = ruleUtils.getRulesByArray(array);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if a rule object does not have a type property', () => {
|
|
||||||
const array = [{ match: 'abc' }, { type: 'content', match: 'def' }];
|
|
||||||
expect(() => {
|
|
||||||
ruleUtils.getRulesByArray(array);
|
|
||||||
}).toThrowError('Rule 没有 type 属性');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle multiple include properties', () => {
|
|
||||||
const array = [
|
|
||||||
{ include: [{ type: 'other1', match: 'abc' }] },
|
|
||||||
{
|
|
||||||
include: [
|
|
||||||
{ type: 'other2', match: 'def' },
|
|
||||||
{ type: 'other3', match: 'ghi' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const result = ruleUtils.getRulesByArray(array);
|
|
||||||
expect(result).toEqual([
|
|
||||||
{ include: { type: 'other1', match: 'abc' } },
|
|
||||||
{ include: { type: 'other2', match: 'def' } },
|
|
||||||
{ include: { type: 'other3', match: 'ghi' } },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getRulesByObject', () => {
|
|
||||||
it('should handle empty object correctly', () => {
|
|
||||||
const object = {};
|
|
||||||
|
|
||||||
const result = ruleUtils.getRulesByObject(object);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle include property correctly', () => {
|
|
||||||
const object = {
|
|
||||||
include: ['rule1', 'rule2'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = ruleUtils.getRulesByObject(object);
|
|
||||||
|
|
||||||
expect(result).toEqual([{ include: 'rule1' }, { include: 'rule2' }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle nested objects correctly', () => {
|
|
||||||
const object = {
|
|
||||||
rule1: {
|
|
||||||
match: 'abc',
|
|
||||||
next: 'rule2',
|
|
||||||
fallback: true,
|
|
||||||
},
|
|
||||||
rule2: [/def/, { match: 'ghi', push: 'rule3' }],
|
|
||||||
rule3: {
|
|
||||||
match: 'jkl',
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = ruleUtils.getRulesByObject(object);
|
|
||||||
|
|
||||||
const item1 = ruleUtils.getRuleOptions('rule1', { match: 'abc', next: 'rule2', fallback: true });
|
|
||||||
const item2 = ruleUtils.getRuleOptions('rule2', [/def/]);
|
|
||||||
delete item2[0];
|
|
||||||
const item3 = ruleUtils.getRuleOptions('rule2', { match: 'ghi', push: 'rule3' });
|
|
||||||
const item4 = ruleUtils.getRuleOptions('rule3', { match: 'jkl', error: true });
|
|
||||||
|
|
||||||
expect(result).toEqual([item1, item2, item3, item4]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('transferReg function', () => {
|
|
||||||
it('should escape special characters in the input string', () => {
|
|
||||||
const input = '[-/\\^$*+?.()|[\\]{}]';
|
|
||||||
const expected = '\\[\\-\\/\\\\\\^\\$\\*\\+\\?\\.\\(\\)\\|\\[\\\\\\]\\{\\}\\]';
|
|
||||||
|
|
||||||
const result = ruleUtils.transferReg(input);
|
|
||||||
|
|
||||||
expect(result).toBe(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the same string if no special characters are present', () => {
|
|
||||||
const input = 'abcdefg';
|
|
||||||
|
|
||||||
const result = ruleUtils.transferReg(input);
|
|
||||||
|
|
||||||
expect(result).toBe(input);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,71 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"outDir": "./build",
|
|
||||||
"incremental": false,
|
|
||||||
"sourceMap": true,
|
|
||||||
"allowJs": true,
|
|
||||||
// allowJs=true => tsc compile js as module, no type check
|
|
||||||
"checkJs": false,
|
|
||||||
// Disable ts error checking in js
|
|
||||||
"strict": true,
|
|
||||||
// js-ts mixed setting
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
// 等大部分js代码改成ts之后再启用.
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
// "strictNullChecks": true,
|
|
||||||
"module": "es2015",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"target": "es5",
|
|
||||||
"jsx": "preserve",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"allowUnreachableCode": true,
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"declaration": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"downlevelIteration": true,
|
|
||||||
// 赋值为空数组使@types/node不会起作用
|
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"esnext",
|
|
||||||
"ES2015",
|
|
||||||
"ES2016",
|
|
||||||
"ES2017",
|
|
||||||
"ES2018",
|
|
||||||
"ES2019",
|
|
||||||
"ES2020"
|
|
||||||
],
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"paths": {
|
|
||||||
"react": [
|
|
||||||
"node_modules/react"
|
|
||||||
],
|
|
||||||
"react-dom": [
|
|
||||||
"node_modules/react-dom"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"./src/**/*",
|
|
||||||
"./src/format/**/*.ts",
|
|
||||||
"./example/**/*"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"lib",
|
|
||||||
"**/*.spec.ts",
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
"types": [
|
|
||||||
"node",
|
|
||||||
"jest",
|
|
||||||
"@testing-library/jest-dom"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
const { resolve } = require('path');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
||||||
const entryPath = './example/index.tsx';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: resolve(__dirname, entryPath),
|
|
||||||
output: {
|
|
||||||
path: resolve(__dirname, './build'),
|
|
||||||
filename: 'main.js',
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.([t|j]s)x?$/i,
|
|
||||||
use: {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
presets: ['@babel/preset-env',
|
|
||||||
[
|
|
||||||
"@babel/preset-react",
|
|
||||||
{
|
|
||||||
"runtime": "automatic", // 新增
|
|
||||||
"importSource": "inulajs" // 新增
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'@babel/preset-typescript'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
mode: isDevelopment ? 'development' : 'production',
|
|
||||||
plugins: [
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: resolve(__dirname, './example/index.html'),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.tsx', '.jsx', '.ts', '.js', '.json'],
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
https: false,
|
|
||||||
host: 'localhost',
|
|
||||||
port: '8080',
|
|
||||||
open: true,
|
|
||||||
hot: true,
|
|
||||||
headers: {
|
|
||||||
connection: 'keep-alive',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,59 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'prettier',
|
|
||||||
],
|
|
||||||
root: true,
|
|
||||||
|
|
||||||
plugins: ['jest', 'no-for-of-loops', 'no-function-declare-after-return', 'react', '@typescript-eslint'],
|
|
||||||
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 8,
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
modules: true,
|
|
||||||
experimentalObjectRestSpread: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
jest: true,
|
|
||||||
node: true,
|
|
||||||
es6: true,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
||||||
'@typescript-eslint/no-empty-function': 'off',
|
|
||||||
semi: ['warn', 'always'],
|
|
||||||
quotes: ['warn', 'single'],
|
|
||||||
'accessor-pairs': 'off',
|
|
||||||
'brace-style': ['error', '1tbs'],
|
|
||||||
'func-style': ['warn', 'declaration', { allowArrowFunctions: true }],
|
|
||||||
'max-lines-per-function': 'off',
|
|
||||||
'object-curly-newline': 'off',
|
|
||||||
// 尾随逗号
|
|
||||||
'comma-dangle': ['error', 'only-multiline'],
|
|
||||||
'prefer-const': 'off',
|
|
||||||
'no-constant-condition': 'off',
|
|
||||||
'no-for-of-loops/no-for-of-loops': 'error',
|
|
||||||
'no-function-declare-after-return/no-function-declare-after-return': 'error',
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
isDev: true,
|
|
||||||
isTest: true,
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['scripts/__tests__/**/*.js'],
|
|
||||||
globals: {
|
|
||||||
container: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,2 +0,0 @@
|
||||||
/node_modules/
|
|
||||||
/dist/
|
|
|
@ -1 +0,0 @@
|
||||||
node_modules
|
|
|
@ -1,17 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
printWidth: 120, // 一行120字符数,如果超过会进行换行
|
|
||||||
tabWidth: 2, // tab等2个空格
|
|
||||||
useTabs: false, // 用空格缩进行
|
|
||||||
semi: true, // 行尾使用分号
|
|
||||||
singleQuote: true, // 字符串使用单引号
|
|
||||||
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
|
|
||||||
jsxSingleQuote: false, // 在JSX中使用双引号
|
|
||||||
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
|
|
||||||
bracketSpacing: true, // 对象的括号间增加空格
|
|
||||||
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
|
|
||||||
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
|
|
||||||
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
|
|
||||||
endOfLine: 'lf', // 仅限换行(\n)
|
|
||||||
};
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,18 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
"@babel/preset-env",
|
|
||||||
{
|
|
||||||
targets: {
|
|
||||||
"browsers" : ["> 1%", "last 2 versions", "not ie <= 8"],
|
|
||||||
"node": "current"
|
|
||||||
},
|
|
||||||
useBuiltIns: "usage",
|
|
||||||
corejs: 3,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'@babel/preset-typescript',
|
|
||||||
]
|
|
||||||
]
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<coverage generated="1691465395748" clover="3.2.0">
|
|
||||||
<project timestamp="1691465395748" name="All files">
|
|
||||||
<metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0" elements="0" coveredelements="0" complexity="0" loc="0" ncloc="0" packages="0" files="0" classes="0"/>
|
|
||||||
</project>
|
|
||||||
</coverage>
|
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
|
@ -1,224 +0,0 @@
|
||||||
body, html {
|
|
||||||
margin:0; padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
font-family: Helvetica Neue, Helvetica, Arial;
|
|
||||||
font-size: 14px;
|
|
||||||
color:#333;
|
|
||||||
}
|
|
||||||
.small { font-size: 12px; }
|
|
||||||
*, *:after, *:before {
|
|
||||||
-webkit-box-sizing:border-box;
|
|
||||||
-moz-box-sizing:border-box;
|
|
||||||
box-sizing:border-box;
|
|
||||||
}
|
|
||||||
h1 { font-size: 20px; margin: 0;}
|
|
||||||
h2 { font-size: 14px; }
|
|
||||||
pre {
|
|
||||||
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-moz-tab-size: 2;
|
|
||||||
-o-tab-size: 2;
|
|
||||||
tab-size: 2;
|
|
||||||
}
|
|
||||||
a { color:#0074D9; text-decoration:none; }
|
|
||||||
a:hover { text-decoration:underline; }
|
|
||||||
.strong { font-weight: bold; }
|
|
||||||
.space-top1 { padding: 10px 0 0 0; }
|
|
||||||
.pad2y { padding: 20px 0; }
|
|
||||||
.pad1y { padding: 10px 0; }
|
|
||||||
.pad2x { padding: 0 20px; }
|
|
||||||
.pad2 { padding: 20px; }
|
|
||||||
.pad1 { padding: 10px; }
|
|
||||||
.space-left2 { padding-left:55px; }
|
|
||||||
.space-right2 { padding-right:20px; }
|
|
||||||
.center { text-align:center; }
|
|
||||||
.clearfix { display:block; }
|
|
||||||
.clearfix:after {
|
|
||||||
content:'';
|
|
||||||
display:block;
|
|
||||||
height:0;
|
|
||||||
clear:both;
|
|
||||||
visibility:hidden;
|
|
||||||
}
|
|
||||||
.fl { float: left; }
|
|
||||||
@media only screen and (max-width:640px) {
|
|
||||||
.col3 { width:100%; max-width:100%; }
|
|
||||||
.hide-mobile { display:none!important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.quiet {
|
|
||||||
color: #7f7f7f;
|
|
||||||
color: rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
.quiet a { opacity: 0.7; }
|
|
||||||
|
|
||||||
.fraction {
|
|
||||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #555;
|
|
||||||
background: #E8E8E8;
|
|
||||||
padding: 4px 5px;
|
|
||||||
border-radius: 3px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.path a:link, div.path a:visited { color: #333; }
|
|
||||||
table.coverage {
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 10px 0 0 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.coverage td {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
table.coverage td.line-count {
|
|
||||||
text-align: right;
|
|
||||||
padding: 0 5px 0 20px;
|
|
||||||
}
|
|
||||||
table.coverage td.line-coverage {
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 10px;
|
|
||||||
min-width:20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.coverage td span.cline-any {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0 5px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.missing-if-branch {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 5px;
|
|
||||||
border-radius: 3px;
|
|
||||||
position: relative;
|
|
||||||
padding: 0 4px;
|
|
||||||
background: #333;
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-if-branch {
|
|
||||||
display: none;
|
|
||||||
margin-right: 10px;
|
|
||||||
position: relative;
|
|
||||||
padding: 0 4px;
|
|
||||||
background: #ccc;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.missing-if-branch .typ, .skip-if-branch .typ {
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
.coverage-summary {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
|
||||||
.keyline-all { border: 1px solid #ddd; }
|
|
||||||
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
|
||||||
.coverage-summary tbody { border: 1px solid #bbb; }
|
|
||||||
.coverage-summary td { border-right: 1px solid #bbb; }
|
|
||||||
.coverage-summary td:last-child { border-right: none; }
|
|
||||||
.coverage-summary th {
|
|
||||||
text-align: left;
|
|
||||||
font-weight: normal;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.coverage-summary th.file { border-right: none !important; }
|
|
||||||
.coverage-summary th.pct { }
|
|
||||||
.coverage-summary th.pic,
|
|
||||||
.coverage-summary th.abs,
|
|
||||||
.coverage-summary td.pct,
|
|
||||||
.coverage-summary td.abs { text-align: right; }
|
|
||||||
.coverage-summary td.file { white-space: nowrap; }
|
|
||||||
.coverage-summary td.pic { min-width: 120px !important; }
|
|
||||||
.coverage-summary tfoot td { }
|
|
||||||
|
|
||||||
.coverage-summary .sorter {
|
|
||||||
height: 10px;
|
|
||||||
width: 7px;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
|
||||||
}
|
|
||||||
.coverage-summary .sorted .sorter {
|
|
||||||
background-position: 0 -20px;
|
|
||||||
}
|
|
||||||
.coverage-summary .sorted-desc .sorter {
|
|
||||||
background-position: 0 -10px;
|
|
||||||
}
|
|
||||||
.status-line { height: 10px; }
|
|
||||||
/* yellow */
|
|
||||||
.cbranch-no { background: yellow !important; color: #111; }
|
|
||||||
/* dark red */
|
|
||||||
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
|
||||||
.low .chart { border:1px solid #C21F39 }
|
|
||||||
.highlighted,
|
|
||||||
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
|
||||||
background: #C21F39 !important;
|
|
||||||
}
|
|
||||||
/* medium red */
|
|
||||||
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
|
||||||
/* light red */
|
|
||||||
.low, .cline-no { background:#FCE1E5 }
|
|
||||||
/* light green */
|
|
||||||
.high, .cline-yes { background:rgb(230,245,208) }
|
|
||||||
/* medium green */
|
|
||||||
.cstat-yes { background:rgb(161,215,106) }
|
|
||||||
/* dark green */
|
|
||||||
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
|
||||||
.high .chart { border:1px solid rgb(77,146,33) }
|
|
||||||
/* dark yellow (gold) */
|
|
||||||
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
|
||||||
.medium .chart { border:1px solid #f9cd0b; }
|
|
||||||
/* light yellow */
|
|
||||||
.medium { background: #fff4c2; }
|
|
||||||
|
|
||||||
.cstat-skip { background: #ddd; color: #111; }
|
|
||||||
.fstat-skip { background: #ddd; color: #111 !important; }
|
|
||||||
.cbranch-skip { background: #ddd !important; color: #111; }
|
|
||||||
|
|
||||||
span.cline-neutral { background: #eaeaea; }
|
|
||||||
|
|
||||||
.coverage-summary td.empty {
|
|
||||||
opacity: .5;
|
|
||||||
padding-top: 4px;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
line-height: 1;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover-fill, .cover-empty {
|
|
||||||
display:inline-block;
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
.chart {
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
.cover-empty {
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
.cover-full {
|
|
||||||
border-right: none !important;
|
|
||||||
}
|
|
||||||
pre.prettyprint {
|
|
||||||
border: none !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
.com { color: #999 !important; }
|
|
||||||
.ignore-none { color: #999; font-weight: normal; }
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
min-height: 100%;
|
|
||||||
height: auto !important;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0 auto -48px;
|
|
||||||
}
|
|
||||||
.footer, .push {
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
var jumpToCode = (function init() {
|
|
||||||
// Classes of code we would like to highlight in the file view
|
|
||||||
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
|
|
||||||
|
|
||||||
// Elements to highlight in the file listing view
|
|
||||||
var fileListingElements = ['td.pct.low'];
|
|
||||||
|
|
||||||
// We don't want to select elements that are direct descendants of another match
|
|
||||||
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
|
|
||||||
|
|
||||||
// Selecter that finds elements on the page to which we can jump
|
|
||||||
var selector =
|
|
||||||
fileListingElements.join(', ') +
|
|
||||||
', ' +
|
|
||||||
notSelector +
|
|
||||||
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
|
|
||||||
|
|
||||||
// The NodeList of matching elements
|
|
||||||
var missingCoverageElements = document.querySelectorAll(selector);
|
|
||||||
|
|
||||||
var currentIndex;
|
|
||||||
|
|
||||||
function toggleClass(index) {
|
|
||||||
missingCoverageElements
|
|
||||||
.item(currentIndex)
|
|
||||||
.classList.remove('highlighted');
|
|
||||||
missingCoverageElements.item(index).classList.add('highlighted');
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCurrent(index) {
|
|
||||||
toggleClass(index);
|
|
||||||
currentIndex = index;
|
|
||||||
missingCoverageElements.item(index).scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'center',
|
|
||||||
inline: 'center'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToPrevious() {
|
|
||||||
var nextIndex = 0;
|
|
||||||
if (typeof currentIndex !== 'number' || currentIndex === 0) {
|
|
||||||
nextIndex = missingCoverageElements.length - 1;
|
|
||||||
} else if (missingCoverageElements.length > 1) {
|
|
||||||
nextIndex = currentIndex - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
makeCurrent(nextIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToNext() {
|
|
||||||
var nextIndex = 0;
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof currentIndex === 'number' &&
|
|
||||||
currentIndex < missingCoverageElements.length - 1
|
|
||||||
) {
|
|
||||||
nextIndex = currentIndex + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
makeCurrent(nextIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return function jump(event) {
|
|
||||||
if (
|
|
||||||
document.getElementById('fileSearch') === document.activeElement &&
|
|
||||||
document.activeElement != null
|
|
||||||
) {
|
|
||||||
// if we're currently focused on the search input, we don't want to navigate
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.which) {
|
|
||||||
case 78: // n
|
|
||||||
case 74: // j
|
|
||||||
goToNext();
|
|
||||||
break;
|
|
||||||
case 66: // b
|
|
||||||
case 75: // k
|
|
||||||
case 80: // p
|
|
||||||
goToPrevious();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
window.addEventListener('keydown', jumpToCode);
|
|
Binary file not shown.
Before Width: | Height: | Size: 445 B |
|
@ -1,101 +0,0 @@
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>Code coverage report for All files</title>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="stylesheet" href="prettify.css" />
|
|
||||||
<link rel="stylesheet" href="base.css" />
|
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<style type='text/css'>
|
|
||||||
.coverage-summary .sorter {
|
|
||||||
background-image: url(sort-arrow-sprite.png);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class='wrapper'>
|
|
||||||
<div class='pad1'>
|
|
||||||
<h1>All files</h1>
|
|
||||||
<div class='clearfix'>
|
|
||||||
|
|
||||||
<div class='fl pad1y space-right2'>
|
|
||||||
<span class="strong">Unknown% </span>
|
|
||||||
<span class="quiet">Statements</span>
|
|
||||||
<span class='fraction'>0/0</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class='fl pad1y space-right2'>
|
|
||||||
<span class="strong">Unknown% </span>
|
|
||||||
<span class="quiet">Branches</span>
|
|
||||||
<span class='fraction'>0/0</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class='fl pad1y space-right2'>
|
|
||||||
<span class="strong">Unknown% </span>
|
|
||||||
<span class="quiet">Functions</span>
|
|
||||||
<span class='fraction'>0/0</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class='fl pad1y space-right2'>
|
|
||||||
<span class="strong">Unknown% </span>
|
|
||||||
<span class="quiet">Lines</span>
|
|
||||||
<span class='fraction'>0/0</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<p class="quiet">
|
|
||||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
|
||||||
</p>
|
|
||||||
<template id="filterTemplate">
|
|
||||||
<div class="quiet">
|
|
||||||
Filter:
|
|
||||||
<input oninput="onInput()" type="search" id="fileSearch">
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class='status-line medium'></div>
|
|
||||||
<div class="pad1">
|
|
||||||
<table class="coverage-summary">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
|
||||||
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
|
||||||
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
|
||||||
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
|
||||||
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
|
||||||
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
|
||||||
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
|
||||||
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
|
||||||
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
|
||||||
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class='push'></div><!-- for sticky footer -->
|
|
||||||
</div><!-- /wrapper -->
|
|
||||||
<div class='footer quiet pad2 space-top1 center small'>
|
|
||||||
Code coverage generated by
|
|
||||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
||||||
at 2023-08-08T03:29:55.735Z
|
|
||||||
</div>
|
|
||||||
<script src="prettify.js"></script>
|
|
||||||
<script>
|
|
||||||
window.onload = function () {
|
|
||||||
prettyPrint();
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<script src="sorter.js"></script>
|
|
||||||
<script src="block-navigation.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 138 B |
|
@ -1,196 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
var addSorting = (function() {
|
|
||||||
'use strict';
|
|
||||||
var cols,
|
|
||||||
currentSort = {
|
|
||||||
index: 0,
|
|
||||||
desc: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// returns the summary table element
|
|
||||||
function getTable() {
|
|
||||||
return document.querySelector('.coverage-summary');
|
|
||||||
}
|
|
||||||
// returns the thead element of the summary table
|
|
||||||
function getTableHeader() {
|
|
||||||
return getTable().querySelector('thead tr');
|
|
||||||
}
|
|
||||||
// returns the tbody element of the summary table
|
|
||||||
function getTableBody() {
|
|
||||||
return getTable().querySelector('tbody');
|
|
||||||
}
|
|
||||||
// returns the th element for nth column
|
|
||||||
function getNthColumn(n) {
|
|
||||||
return getTableHeader().querySelectorAll('th')[n];
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFilterInput() {
|
|
||||||
const searchValue = document.getElementById('fileSearch').value;
|
|
||||||
const rows = document.getElementsByTagName('tbody')[0].children;
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
|
||||||
const row = rows[i];
|
|
||||||
if (
|
|
||||||
row.textContent
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchValue.toLowerCase())
|
|
||||||
) {
|
|
||||||
row.style.display = '';
|
|
||||||
} else {
|
|
||||||
row.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loads the search box
|
|
||||||
function addSearchBox() {
|
|
||||||
var template = document.getElementById('filterTemplate');
|
|
||||||
var templateClone = template.content.cloneNode(true);
|
|
||||||
templateClone.getElementById('fileSearch').oninput = onFilterInput;
|
|
||||||
template.parentElement.appendChild(templateClone);
|
|
||||||
}
|
|
||||||
|
|
||||||
// loads all columns
|
|
||||||
function loadColumns() {
|
|
||||||
var colNodes = getTableHeader().querySelectorAll('th'),
|
|
||||||
colNode,
|
|
||||||
cols = [],
|
|
||||||
col,
|
|
||||||
i;
|
|
||||||
|
|
||||||
for (i = 0; i < colNodes.length; i += 1) {
|
|
||||||
colNode = colNodes[i];
|
|
||||||
col = {
|
|
||||||
key: colNode.getAttribute('data-col'),
|
|
||||||
sortable: !colNode.getAttribute('data-nosort'),
|
|
||||||
type: colNode.getAttribute('data-type') || 'string'
|
|
||||||
};
|
|
||||||
cols.push(col);
|
|
||||||
if (col.sortable) {
|
|
||||||
col.defaultDescSort = col.type === 'number';
|
|
||||||
colNode.innerHTML =
|
|
||||||
colNode.innerHTML + '<span class="sorter"></span>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cols;
|
|
||||||
}
|
|
||||||
// attaches a data attribute to every tr element with an object
|
|
||||||
// of data values keyed by column name
|
|
||||||
function loadRowData(tableRow) {
|
|
||||||
var tableCols = tableRow.querySelectorAll('td'),
|
|
||||||
colNode,
|
|
||||||
col,
|
|
||||||
data = {},
|
|
||||||
i,
|
|
||||||
val;
|
|
||||||
for (i = 0; i < tableCols.length; i += 1) {
|
|
||||||
colNode = tableCols[i];
|
|
||||||
col = cols[i];
|
|
||||||
val = colNode.getAttribute('data-value');
|
|
||||||
if (col.type === 'number') {
|
|
||||||
val = Number(val);
|
|
||||||
}
|
|
||||||
data[col.key] = val;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
// loads all row data
|
|
||||||
function loadData() {
|
|
||||||
var rows = getTableBody().querySelectorAll('tr'),
|
|
||||||
i;
|
|
||||||
|
|
||||||
for (i = 0; i < rows.length; i += 1) {
|
|
||||||
rows[i].data = loadRowData(rows[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// sorts the table using the data for the ith column
|
|
||||||
function sortByIndex(index, desc) {
|
|
||||||
var key = cols[index].key,
|
|
||||||
sorter = function(a, b) {
|
|
||||||
a = a.data[key];
|
|
||||||
b = b.data[key];
|
|
||||||
return a < b ? -1 : a > b ? 1 : 0;
|
|
||||||
},
|
|
||||||
finalSorter = sorter,
|
|
||||||
tableBody = document.querySelector('.coverage-summary tbody'),
|
|
||||||
rowNodes = tableBody.querySelectorAll('tr'),
|
|
||||||
rows = [],
|
|
||||||
i;
|
|
||||||
|
|
||||||
if (desc) {
|
|
||||||
finalSorter = function(a, b) {
|
|
||||||
return -1 * sorter(a, b);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < rowNodes.length; i += 1) {
|
|
||||||
rows.push(rowNodes[i]);
|
|
||||||
tableBody.removeChild(rowNodes[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.sort(finalSorter);
|
|
||||||
|
|
||||||
for (i = 0; i < rows.length; i += 1) {
|
|
||||||
tableBody.appendChild(rows[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// removes sort indicators for current column being sorted
|
|
||||||
function removeSortIndicators() {
|
|
||||||
var col = getNthColumn(currentSort.index),
|
|
||||||
cls = col.className;
|
|
||||||
|
|
||||||
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
|
|
||||||
col.className = cls;
|
|
||||||
}
|
|
||||||
// adds sort indicators for current column being sorted
|
|
||||||
function addSortIndicators() {
|
|
||||||
getNthColumn(currentSort.index).className += currentSort.desc
|
|
||||||
? ' sorted-desc'
|
|
||||||
: ' sorted';
|
|
||||||
}
|
|
||||||
// adds event listeners for all sorter widgets
|
|
||||||
function enableUI() {
|
|
||||||
var i,
|
|
||||||
el,
|
|
||||||
ithSorter = function ithSorter(i) {
|
|
||||||
var col = cols[i];
|
|
||||||
|
|
||||||
return function() {
|
|
||||||
var desc = col.defaultDescSort;
|
|
||||||
|
|
||||||
if (currentSort.index === i) {
|
|
||||||
desc = !currentSort.desc;
|
|
||||||
}
|
|
||||||
sortByIndex(i, desc);
|
|
||||||
removeSortIndicators();
|
|
||||||
currentSort.index = i;
|
|
||||||
currentSort.desc = desc;
|
|
||||||
addSortIndicators();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
for (i = 0; i < cols.length; i += 1) {
|
|
||||||
if (cols[i].sortable) {
|
|
||||||
// add the click event handler on the th so users
|
|
||||||
// dont have to click on those tiny arrows
|
|
||||||
el = getNthColumn(i).querySelector('.sorter').parentElement;
|
|
||||||
if (el.addEventListener) {
|
|
||||||
el.addEventListener('click', ithSorter(i));
|
|
||||||
} else {
|
|
||||||
el.attachEvent('onclick', ithSorter(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// adds sorting functionality to the UI
|
|
||||||
return function() {
|
|
||||||
if (!getTable()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cols = loadColumns();
|
|
||||||
loadData();
|
|
||||||
addSearchBox();
|
|
||||||
addSortIndicators();
|
|
||||||
enableUI();
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
window.addEventListener('load', addSorting);
|
|
|
@ -1,58 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Horizon Request Cancel Request Test</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="cancelStyles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>Horizon Request Cancel Request Test</header>
|
|
||||||
<div class="container">
|
|
||||||
<div class="button-group">
|
|
||||||
<button id="sendRequestButton">Send Request</button>
|
|
||||||
<button id="cancelRequestButton">Cancel Request</button>
|
|
||||||
</div>
|
|
||||||
<div class="message" id="message">等待发送请求...</div>
|
|
||||||
</div>
|
|
||||||
<script src="../../dist/bundle.js"></script>
|
|
||||||
<script>
|
|
||||||
const sendRequestButton = document.getElementById('sendRequestButton');
|
|
||||||
const cancelRequestButton = document.getElementById('cancelRequestButton');
|
|
||||||
const message = document.getElementById('message');
|
|
||||||
|
|
||||||
let cancelTokenSource;
|
|
||||||
|
|
||||||
sendRequestButton.addEventListener('click', function() {
|
|
||||||
message.innerHTML = '';
|
|
||||||
cancelTokenSource = inulaRequest.CancelToken.source();
|
|
||||||
|
|
||||||
inulaRequest.get('http://localhost:3001/data', {
|
|
||||||
cancelToken: cancelTokenSource.token
|
|
||||||
}).then(function(response) {
|
|
||||||
message.innerHTML = '请求成功: ' + JSON.stringify(response.data, null, 2);
|
|
||||||
}).catch(function(error) {
|
|
||||||
message.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cancelRequestButton.addEventListener('click', function () {
|
|
||||||
const CancelToken = inulaRequest.CancelToken;
|
|
||||||
const source = CancelToken.source();
|
|
||||||
|
|
||||||
inulaRequest.get('http://localhost:3001/data', {
|
|
||||||
cancelToken: source.token
|
|
||||||
}).then(function(response) {
|
|
||||||
console.log(response.data);
|
|
||||||
}).catch(function(error) {
|
|
||||||
if (inulaRequest.isCancel(error)) {
|
|
||||||
message.innerHTML = '请求已被取消:' + error.message;
|
|
||||||
} else {
|
|
||||||
message.innerHTML = '请求出错:' + error.message;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
source.cancel('请求被用户取消。');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,58 +0,0 @@
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 80px;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #007bff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s ease-in-out;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #0062cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 18px;
|
|
||||||
margin-top: 30px;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #fff;
|
|
||||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 2rem;
|
|
||||||
background-color: #f0f2f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 80px;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: #333;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: #007bff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0.25rem 1rem;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
padding: 10px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
text-align: center;
|
|
||||||
font-size: large;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
width: 50vh;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.response-container {
|
|
||||||
margin-top: 1rem;
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre-container {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Inula Request Interceptor Test</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="interceptorStyles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>Inula Request interceptor Test</header>
|
|
||||||
<h2>使用拦截器:</h2>
|
|
||||||
<div class="response-container">
|
|
||||||
<h3>响应状态码:</h3>
|
|
||||||
<div class="pre-container">
|
|
||||||
<pre id="responseStatusWithInterceptor">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<h3>请求拦截反馈:</h3>
|
|
||||||
<div class="pre-container">
|
|
||||||
<pre id="requestInterceptorFeedback">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<h3>响应拦截反馈:</h3>
|
|
||||||
<div class="pre-container">
|
|
||||||
<pre id="responseInterceptorFeedback">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<h3>响应数据:</h3>
|
|
||||||
<div class="pre-container">
|
|
||||||
<pre id="responseDataWithInterceptor">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<div class="button">
|
|
||||||
<button id="sendRequestWithInterceptor">发送请求</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>不使用拦截器:</h2>
|
|
||||||
<div class="response-container">
|
|
||||||
<h3>响应状态码:</h3>
|
|
||||||
<div class="pre-container">
|
|
||||||
<pre id="responseStatusWithoutInterceptor">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<h3>拦截反馈:</h3>
|
|
||||||
<div class="pre-container">
|
|
||||||
<pre id="noInterceptorFeedback">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<h3>响应数据:</h3>
|
|
||||||
<div class="pre-container">
|
|
||||||
<pre id="responseDataWithoutInterceptor">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<div class="button">
|
|
||||||
<button id="sendRequestWithoutInterceptor">发送请求</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="../../dist/bundle.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// 创建使用拦截器的 IR 实例
|
|
||||||
const irInstance = inulaRequest.create();
|
|
||||||
|
|
||||||
// 添加请求拦截器
|
|
||||||
irInstance.interceptors.request.use(function(config) {
|
|
||||||
// 为请求添加自定义请求头
|
|
||||||
config.headers['IR-Custom-Header'] = 'CustomHeaderValue';
|
|
||||||
document.getElementById('requestInterceptorFeedback').textContent = '请求已拦截,并添加请求头IR-Custom-Header';
|
|
||||||
return config;
|
|
||||||
}, function(error) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加响应拦截器
|
|
||||||
irInstance.interceptors.response.use(function(response) {
|
|
||||||
// 更新响应状态码
|
|
||||||
response.status = 404;
|
|
||||||
document.getElementById('responseStatusWithInterceptor').textContent = String(response.status);
|
|
||||||
document.getElementById('responseInterceptorFeedback').textContent = '响应已被拦截,状态响应码被强制修改成404'
|
|
||||||
return response;
|
|
||||||
}, function(error) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用拦截器的请求
|
|
||||||
document.getElementById('sendRequestWithInterceptor').addEventListener('click', function () {
|
|
||||||
irInstance.get('http://localhost:3001/')
|
|
||||||
.then(function (response) {
|
|
||||||
document.getElementById('responseDataWithInterceptor').textContent = JSON.stringify(response.data, null, 2);
|
|
||||||
}).catch(function (error) {
|
|
||||||
document.getElementById('responseDataWithInterceptor').textContent = 'Request failed:' + error;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// 不使用拦截器的请求
|
|
||||||
document.getElementById('sendRequestWithoutInterceptor').addEventListener('click', function () {
|
|
||||||
inulaRequest.get('http://localhost:3001/')
|
|
||||||
.then(function (response) {
|
|
||||||
document.getElementById('responseStatusWithoutInterceptor').textContent = response.status;
|
|
||||||
document.getElementById('responseDataWithoutInterceptor').textContent = JSON.stringify(response.data, null, 2);
|
|
||||||
document.getElementById('noInterceptorFeedback').textContent = '请求未被拦截';
|
|
||||||
}).catch(function (error) {
|
|
||||||
document.getElementById('responseDataWithoutInterceptor').textContent = 'Request failed:' + error;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>downloadTest</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h3>Hello Horizon Request Master!</h3>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,86 +0,0 @@
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 80px;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 50px;
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 0 5px #aaa;
|
|
||||||
width: 320px;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 20px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
box-shadow: 0 0 15px #aaa;
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 24px;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card pre {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
border-radius: 5px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #007bff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
|
@ -1,214 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Inula Request API Test</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="requestStyles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>Inula Request API Test</header>
|
|
||||||
<div class="container">
|
|
||||||
<div class="card">
|
|
||||||
<h2>Request</h2>
|
|
||||||
<pre id="request-result">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="card">
|
|
||||||
<h2>GET Request</h2>
|
|
||||||
<pre id="get-result">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>POST Request</h2>
|
|
||||||
<pre id="post-result">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>PUT Request</h2>
|
|
||||||
<pre id="put-result">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>DELETE Request</h2>
|
|
||||||
<pre id="delete-result">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="card">
|
|
||||||
<h2>HEAD Request</h2>
|
|
||||||
<pre id="head-result">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>OPTIONS Request</h2>
|
|
||||||
<pre id="options-result">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>PATCH Request</h2>
|
|
||||||
<pre id="patch-result">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="card" style="height: 250px">
|
|
||||||
<h2>UPLOAD Request</h2>
|
|
||||||
<input type="file" id="fileInput">
|
|
||||||
<pre id="upload-progress">UploadProgress: 0%</pre>
|
|
||||||
<pre id="upload-result">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
<div class="card" style="height: 250px">
|
|
||||||
<h2>DOWNLOAD Request</h2>
|
|
||||||
<div style="height: 23px"></div>
|
|
||||||
<pre id="download-progress">DownloadProgress: 0%</pre>
|
|
||||||
<pre id="download-result">等待发送请求...</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="button">
|
|
||||||
<button id="queryButton">点击发送请求</button>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="button">
|
|
||||||
<button id="resetButton">点击重置</button>
|
|
||||||
</div>
|
|
||||||
<script src="../../dist/bundle.js"></script>
|
|
||||||
<script>
|
|
||||||
const requestResult = document.getElementById('request-result');
|
|
||||||
const getResult = document.getElementById('get-result');
|
|
||||||
const postResult = document.getElementById('post-result');
|
|
||||||
const putResult = document.getElementById('put-result');
|
|
||||||
const deleteResult = document.getElementById('delete-result');
|
|
||||||
const headResult = document.getElementById('head-result');
|
|
||||||
const optionsResult = document.getElementById('options-result');
|
|
||||||
const patchResult = document.getElementById('patch-result');
|
|
||||||
const fileInput = document.getElementById('fileInput');
|
|
||||||
const uploadProgress = document.getElementById('upload-progress');
|
|
||||||
const uploadResult = document.getElementById('upload-result');
|
|
||||||
const downloadProgress = document.getElementById('download-progress');
|
|
||||||
const downloadResult = document.getElementById('download-result');
|
|
||||||
const queryButton = document.getElementById('queryButton');
|
|
||||||
const resetButton = document.getElementById('resetButton');
|
|
||||||
|
|
||||||
queryButton.addEventListener('click', function () {
|
|
||||||
const irInstance = inulaRequest.create();
|
|
||||||
irInstance.request('http://localhost:3001/', {method: 'GET', data: {}})
|
|
||||||
.then(function (response) {
|
|
||||||
requestResult.innerHTML = JSON.stringify(response.data, null, 2);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
requestResult.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
})
|
|
||||||
|
|
||||||
inulaRequest.default.get('http://localhost:3001/')
|
|
||||||
.then(function (response) {
|
|
||||||
getResult.innerHTML = JSON.stringify(response.data, null, 2);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
getResult.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
inulaRequest('http://localhost:3001/', {method:'POST', name: 'Alice'})
|
|
||||||
.then(function (response) {
|
|
||||||
postResult.innerHTML = JSON.stringify(response.data, null, 2);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
postResult.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
inulaRequest.put('http://localhost:3001/users', {id: 1, name: 'Bob'})
|
|
||||||
.then(function (response) {
|
|
||||||
putResult.innerHTML = JSON.stringify(response.data, null, 2);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
putResult.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
inulaRequest.delete('http://localhost:3001/users', {params: {id: 1}})
|
|
||||||
.then(function (response) {
|
|
||||||
deleteResult.innerHTML = JSON.stringify(response.data, null, 2);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
deleteResult.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
inulaRequest.head('http://localhost:3001/')
|
|
||||||
.then(function (response) {
|
|
||||||
headResult.innerHTML = 'Header: ' + JSON.stringify(response.headers['x-powered-by'], null, 2); // IE 浏览器不支持 HEAD 方式访问响应头
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
headResult.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
inulaRequest.options('http://localhost:3001/', {
|
|
||||||
headers: {
|
|
||||||
'Access-Control-Request-Method': 'POST'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function (response) {
|
|
||||||
optionsResult.innerHTML = 'status: ' + JSON.stringify(response.status, null, 2);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
optionsResult.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
inulaRequest.patch('http://localhost:3001/', {name: 'IR'})
|
|
||||||
.then(function (response) {
|
|
||||||
patchResult.innerHTML = JSON.stringify(response.data, null, 2);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
patchResult.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
inulaRequest.get('http://localhost:3001/download', {
|
|
||||||
responseType: 'text',
|
|
||||||
onDownloadProgress: function (progressEvent) {
|
|
||||||
const loaded = progressEvent.loaded;
|
|
||||||
const total = progressEvent.total;
|
|
||||||
const progressPercentage = Math.round((loaded / total) * 100);
|
|
||||||
downloadProgress.innerHTML = 'Download progress: ' + progressPercentage + '%';
|
|
||||||
},
|
|
||||||
}).then(function (response) {
|
|
||||||
downloadResult.innerHTML = JSON.stringify(response.data, null, 2);
|
|
||||||
}).catch(function (error) {
|
|
||||||
downloadResult.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
resetButton.addEventListener('click', function () {
|
|
||||||
requestResult.innerHTML = '等待发送请求...';
|
|
||||||
getResult.innerHTML = '等待发送请求...';
|
|
||||||
postResult.innerHTML = '等待发送请求...';
|
|
||||||
putResult.innerHTML = '等待发送请求...';
|
|
||||||
deleteResult.innerHTML = '等待发送请求...';
|
|
||||||
headResult.innerHTML = '等待发送请求...';
|
|
||||||
optionsResult.innerHTML = '等待发送请求...';
|
|
||||||
patchResult.innerHTML = '等待发送请求...';
|
|
||||||
fileInput.value = '';
|
|
||||||
uploadProgress.innerHTML = 'UploadProgress: 0%';
|
|
||||||
uploadResult.innerHTML = '等待发送请求...';
|
|
||||||
downloadProgress.innerHTML = 'DownloadProgress: 0%';
|
|
||||||
downloadResult.innerHTML = '等待发送请求...';
|
|
||||||
});
|
|
||||||
|
|
||||||
function uploadFile(file) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
inulaRequest.post('http://localhost:3001/', formData, {
|
|
||||||
onUploadProgress: function (progressEvent) {
|
|
||||||
const loaded = progressEvent.loaded;
|
|
||||||
const total = progressEvent.total;
|
|
||||||
const progressPercentage = Math.round((loaded / total) * 100);
|
|
||||||
uploadProgress.innerHTML = 'Upload progress: ' + progressPercentage + '%';
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(function (response) {
|
|
||||||
uploadResult.innerHTML = JSON.stringify(response.data, null, 2);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
uploadResult.innerHTML = JSON.stringify(error, null, 2);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', function (event) {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
uploadFile(file);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,93 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import bodyParser from "body-parser";
|
|
||||||
import cors from "cors";
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const port = 3001;
|
|
||||||
|
|
||||||
app.use(bodyParser.json());
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
|
||||||
|
|
||||||
// 自定义 CORS 配置
|
|
||||||
const corsOptions = {
|
|
||||||
origin: '*',
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
|
|
||||||
allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'X-Requested-With,content-type', 'IR-Custom-Header'],
|
|
||||||
exposedHeaders: ['X-Powered-By'],
|
|
||||||
optionsSuccessStatus: 200, // 设置 OPTIONS 请求成功时的状态码为 200
|
|
||||||
credentials: true
|
|
||||||
};
|
|
||||||
|
|
||||||
app.use(cors(corsOptions));
|
|
||||||
|
|
||||||
// 处理 GET 请求
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.send('Hello Inula Request!');
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/data', (req, res) => {
|
|
||||||
const data = {
|
|
||||||
message: 'Hello Inula Request!',
|
|
||||||
};
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
res.send(JSON.stringify(data));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/download', (req, res) => {
|
|
||||||
const filePath = 'D:\\code\\MRcode\\inula-Request\\examples\\request\\downloadTest.html';
|
|
||||||
const fileName = 'downloadTest.html';
|
|
||||||
const fileSize = fs.statSync(filePath).size;
|
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
res.setHeader('Content-Disposition', `attachment; filename=${fileName}`);
|
|
||||||
res.setHeader('Content-Type', 'application/octet-stream');
|
|
||||||
res.setHeader('Content-Length', fileSize);
|
|
||||||
const fileStream = fs.createReadStream(filePath);
|
|
||||||
fileStream.pipe(res);
|
|
||||||
} else {
|
|
||||||
res.status(404).send('File not found');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理 POST 请求
|
|
||||||
app.post('/', (req, res) => {
|
|
||||||
res.send('Got a POST request!');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理 PUT 请求
|
|
||||||
app.put('/users', (req, res) => {
|
|
||||||
res.send('Got a PUT request at /users');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理 DELETE 请求
|
|
||||||
app.delete('/users', (req, res) => {
|
|
||||||
res.send('Got a DELETE request at /users');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理 HEAD 请求
|
|
||||||
app.head('/', (req, res) => {
|
|
||||||
res.setHeader('x-powered-by', 'Express');
|
|
||||||
res.json({ 'x-powered-by': 'Express' });
|
|
||||||
res.sendStatus(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理 OPTIONS 请求
|
|
||||||
app.options('/', (req, res) => {
|
|
||||||
res.sendStatus(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理 PATCH 请求
|
|
||||||
app.patch('/', (req, res) => {
|
|
||||||
const name = req.body.name;
|
|
||||||
const message = `Hello, ${name}! Your name has been updated.`;
|
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
|
||||||
res.send(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 启动服务器
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(`Server listening on port ${port}`);
|
|
||||||
});
|
|
|
@ -1,36 +0,0 @@
|
||||||
import Inula, { useState } from 'inulajs';
|
|
||||||
import { useIR } from '../../index';
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
|
|
||||||
const [message, setMessage] = useState('等待发送请求...');
|
|
||||||
const [isSent, setSend] = useState(false);
|
|
||||||
const options = {
|
|
||||||
pollingInterval: 3000,
|
|
||||||
enablePollingOptimization: true,
|
|
||||||
limitation: {minInterval: 500, maxInterval: 4000}
|
|
||||||
};
|
|
||||||
const {data} = useIR('http://localhost:3001/', null, options);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
setSend(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<header>useIR Test</header>
|
|
||||||
<div className="container">
|
|
||||||
<div className="card">
|
|
||||||
<h2 style={{whiteSpace: "pre-wrap"}}>{options ? `实时数据流已激活\n更新间隔:${options?.pollingInterval} ms`
|
|
||||||
: '实时数据流未激活'}</h2>
|
|
||||||
<pre>{isSent ? data : message}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="button">
|
|
||||||
<button onClick={handleClick}>点击发送请求</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,98 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>useIR Test</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 80px;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 50px;
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 0 5px #aaa;
|
|
||||||
width: 320px;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 20px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
box-shadow: 0 0 15px #aaa;
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 24px;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card pre {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
border-radius: 5px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #007bff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,9 +0,0 @@
|
||||||
import Inula from 'inulajs';
|
|
||||||
import App from "./App ";
|
|
||||||
|
|
||||||
Inula.render(
|
|
||||||
<Inula.Fragment>
|
|
||||||
<App/>
|
|
||||||
</Inula.Fragment>,
|
|
||||||
document.querySelector('#root')
|
|
||||||
)
|
|
|
@ -1,57 +0,0 @@
|
||||||
import inulaRequest from './src/inulaRequest';
|
|
||||||
import useIR from './src/core/useIR/useIR';
|
|
||||||
|
|
||||||
const {
|
|
||||||
create,
|
|
||||||
request,
|
|
||||||
get,
|
|
||||||
post,
|
|
||||||
put,
|
|
||||||
['delete']: propToDelete,
|
|
||||||
head,
|
|
||||||
options,
|
|
||||||
InulaRequest,
|
|
||||||
IrError,
|
|
||||||
CanceledError,
|
|
||||||
isCancel,
|
|
||||||
CancelToken,
|
|
||||||
all,
|
|
||||||
Cancel,
|
|
||||||
isIrError,
|
|
||||||
spread,
|
|
||||||
IrHeaders,
|
|
||||||
// 兼容axios
|
|
||||||
Axios,
|
|
||||||
AxiosError,
|
|
||||||
AxiosHeaders,
|
|
||||||
isAxiosError,
|
|
||||||
} = inulaRequest;
|
|
||||||
|
|
||||||
export {
|
|
||||||
create,
|
|
||||||
request,
|
|
||||||
get,
|
|
||||||
post,
|
|
||||||
put,
|
|
||||||
propToDelete as delete,
|
|
||||||
head,
|
|
||||||
options,
|
|
||||||
InulaRequest,
|
|
||||||
IrError,
|
|
||||||
CanceledError,
|
|
||||||
isCancel,
|
|
||||||
CancelToken,
|
|
||||||
all,
|
|
||||||
Cancel,
|
|
||||||
isIrError,
|
|
||||||
spread,
|
|
||||||
IrHeaders,
|
|
||||||
useIR,
|
|
||||||
// 兼容axios
|
|
||||||
Axios,
|
|
||||||
AxiosError,
|
|
||||||
AxiosHeaders,
|
|
||||||
isAxiosError,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default inulaRequest;
|
|
|
@ -1,25 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
clearMocks: true,
|
|
||||||
collectCoverage: true,
|
|
||||||
coverageDirectory: "coverage",
|
|
||||||
coverageProvider: "v8",
|
|
||||||
moduleFileExtensions: [
|
|
||||||
"js",
|
|
||||||
"mjs",
|
|
||||||
"cjs",
|
|
||||||
"jsx",
|
|
||||||
"ts",
|
|
||||||
"tsx",
|
|
||||||
"json",
|
|
||||||
"node"
|
|
||||||
],
|
|
||||||
testEnvironment: "jest-environment-jsdom",
|
|
||||||
testMatch: ["**/tests/**/*.test.[jt]s?(x)"],
|
|
||||||
testPathIgnorePatterns: [
|
|
||||||
"\\\\node_modules\\\\"
|
|
||||||
],
|
|
||||||
transform: {
|
|
||||||
"^.+\\.(js|jsx)$": "babel-jest",
|
|
||||||
"^.+\\.(ts|tsx)$": "babel-jest"
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"restartable": "rs",
|
|
||||||
"ignore": [
|
|
||||||
".git",
|
|
||||||
"node_modules/**/node_modules"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "development"
|
|
||||||
},
|
|
||||||
"ext": "js,json"
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
{
|
|
||||||
"name": "inula-request",
|
|
||||||
"version": "1.0.20",
|
|
||||||
"description": "Horizon-request brings you a convenient request experience!",
|
|
||||||
"main": "index.ts",
|
|
||||||
"scripts": {
|
|
||||||
"test": "jest --config jest.config.cjs",
|
|
||||||
"buildExample": "rollup -c rollup.config.example.js --bundleConfigAsCjs",
|
|
||||||
"build": "rollup -c rollup.config.js --bundleConfigAsCjs",
|
|
||||||
"useIRExample": "webpack serve --config webpack.useHR.config.js --mode development",
|
|
||||||
"server": "nodemon .\\examples\\server\\serverTest.mjs"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "ssh://git@szv-open.codehub.huawei.com:2222/innersource/fenghuang/horizon/horizon-ecosystem.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"inula-request"
|
|
||||||
],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"inulajs": "0.0.11"
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue