Compare commits

..

38 Commits

Author SHA1 Message Date
openinula cd9bddbe10 Merge pull request 'feat: add runtime and alter parser' (#7) from iandx/inula:API-2.0 into API-2.0 2024-10-16 09:46:23 +08:00
iandxssxx 4657df9532 feat: add runtime and alter parser 2024-10-15 11:00:42 -04:00
Hoikan b15d746697 fix(props): rename the prop without alias and deconstruction 2024-05-24 17:56:42 +08:00
Hoikan c7de85630e fix(reactivity): fix for 2024-05-24 17:20:30 +08:00
Hoikan 00418eba82 docs(parser): docs 2024-05-22 17:38:53 +08:00
Hoikan 4a877cdc34 refactor(parser): prune unused bit 2024-05-22 17:13:43 +08:00
Hoikan 5fc41b8e5e fix(reactivity): remove used bit 2024-05-17 17:57:07 +08:00
Hoikan 3665d927ae fix(reactivity): fix for view analyze 2024-05-15 16:13:14 +08:00
Hoikan dabc0eed20 feat: autoNaming, autoReturn, deconstructing, jsxSlice and props 2024-05-14 21:03:38 +08:00
Hoikan 4ca2d66fac fix(reactivity): fix for dependency 2024-05-10 16:54:38 +08:00
Hoikan 601381032d refactor(parse): use bitmap instead of dependency map 2024-05-08 15:56:42 +08:00
Hoikan 0dcad572f3 refactor(parse): use bitmap instead of dependency map 2024-04-29 17:58:19 +08:00
Hoikan f32da0e9c7 refactor(proposal): use bitmap instead of dependency map 2024-04-28 21:02:37 +08:00
Hoikan be4456f225 feat: viewNode as child 2024-04-25 15:58:26 +08:00
Hoikan f15b7d1a14 feat: sub component 2024-04-25 11:46:11 +08:00
Hoikan 5427f13880 feat: analyze watch lifeCycle properties 2024-04-24 16:21:40 +08:00
Hoikan fcc734e05f feat: init 2024-04-19 17:58:52 +08:00
Hoikan a536958ad4 feat(fn2cls): env 2024-04-15 20:59:31 +08:00
Hoikan 2d5d3c29e4 test(inula-next): add test 2024-04-15 20:57:16 +08:00
Hoikan 37d6ba1033 Merge remote-tracking branch 'gitee/api2/for' into dev-04-15
# Conflicts:
#	demos/benchmark/src/main.jsx
#	demos/v2/src/App.view.tsx
#	packages/transpiler/babel-preset-inula-next/CHANGELOG.md
#	packages/transpiler/babel-preset-inula-next/package.json
#	packages/transpiler/class-transformer/package.json
#	packages/transpiler/class-transformer/src/pluginProvider.ts
#	packages/transpiler/vite-plugin-inula-next/CHANGELOG.md
#	packages/transpiler/vite-plugin-inula-next/package.json
2024-04-15 11:51:44 +08:00
HoikanChan f447bb8989 feat(watch): switch to label watch 2024-04-15 11:46:05 +08:00
Hoikan 7b4c3a35d0 feat: cls2fn 2024-04-15 11:44:13 +08:00
iandxssxx 4608422c0a feat: publish (feat: add lifecycles and watch) 2024-04-10 23:00:49 -04:00
iandxssxx 325f4c406a feat: add lifecycles and watch 2024-04-10 22:59:55 -04:00
HoikanChan bf90ea1b7f feat(watch): switch to label watch 2024-04-11 10:38:03 +08:00
Hoikan 8f60ec6b26 feat: test 2024-04-10 17:58:16 +08:00
IanDxSSXX be4b0cb024 !172 Add for support and benchmark
* refactor: publish new version
* feat: add changeset and changed package name
* Merge branch 'API-2.0' into api2/for
* fix: import package from workspace
* feat: add for unit parsing
* feat: init benchmark demo
* refactor: gitignore add history
2024-04-10 02:06:11 +00:00
iandxssxx 1f4b164952 refactor: publish new version 2024-04-09 21:58:08 -04:00
iandxssxx 2f9d3737db feat: add changeset and changed package name 2024-04-09 03:28:10 -04:00
iandxssxx ef4126b767 Merge branch 'API-2.0' into api2/for 2024-04-07 23:17:00 -04:00
陈超涛 b7756e9732
!171 fix: this patcher
Merge pull request !171 from Hoikan/API-2.0-4-8
2024-04-08 03:16:05 +00:00
Hoikan bf1bd09721 refactor: add this with thisPatcher.ts 2024-04-08 11:11:46 +08:00
iandxssxx d6ba039445 fix: import package from workspace 2024-04-07 23:09:39 -04:00
iandxssxx 4467bdae73 feat: add for unit parsing 2024-04-07 23:09:21 -04:00
iandxssxx 627a8b7785 feat: init benchmark demo 2024-04-07 23:08:50 -04:00
iandxssxx 109746acef refactor: gitignore add history 2024-04-07 23:08:39 -04:00
Hoikan d599b36eaa !169 feat: inula 2.0 init
* feat: update doc
* feat: inula next
2024-04-03 08:55:27 +00:00
Hoikan 83c80341dc !168 inula api2.0
* feat: inula-next init
* feat: v2 init
* feat(class-transform): add watch decorator
* feat(class-transform): update docs
* feat(class-transform): init
2024-04-03 08:41:11 +00:00
501 changed files with 26456 additions and 14090 deletions

BIN
.DS_Store vendored

Binary file not shown.

8
.changeset/README.md Normal file
View File

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

11
.changeset/config.json Normal file
View File

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

6
.gitignore vendored
View File

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

55
api-2.1-fn.md Normal file
View File

@ -0,0 +1,55 @@
```js
function MyComp({prop1}) {
let count = 1
let doubleCount = count * 2
const updateCount = () => {
count++
}
return <div>{prop1}</div>;
}
```
```js
function MyComp() {
let prop1$$prop
let count = 1
let doubleCount = count * 2
let update$$doubleCount = () => {
if (cached()) return
doubleCount = count * 2
}
const updateCount = () => {
viewModel.update(count++)
}
// ----
const node = createElement('div')
setProps(node, "textContent", [prop1])
const viewModel = Inula.createElement({
node,
updateProp: (propName, value) => {
if (propName === 'prop1') {
prop1$$prop = value
}
},
updateState: (changed) => {
if (changed & 0x1) {
update$$doubleCount()
}
},
updateView: (changed) => {
if (changed & 0x1) {
setProps(node, "textContent", [prop1$$prop])
}
}
})
return viewModel
}
```

2029
api-2.1.md Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,25 @@
{
"name": "dev",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@babel/standalone": "^7.22.4",
"@openinula/next": "workspace:*",
"@iandx/easy-css": "^0.10.14",
"@openinula/babel-preset-inula-next": "workspace:*"
},
"devDependencies": {
"typescript": "^5.2.2",
"vite": "^4.4.9",
"@openinula/vite-plugin-inula-next": "workspace:*"
},
"keywords": [
"inula-next"
]
}

View File

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

View File

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

417
demos/todo-mvc/app.css Normal file
View File

@ -0,0 +1,417 @@
@charset "utf-8";
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #111111;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 400;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 400;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 400;
color: rgba(0, 0, 0, 0.4);
}
.todoapp h1 {
position: absolute;
top: -140px;
width: 100%;
font-size: 80px;
font-weight: 200;
text-align: center;
color: #b83f45;
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
height: 65px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}
.toggle-all + label {
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 65px;
font-size: 0;
position: absolute;
top: -65px;
left: -0;
}
.toggle-all + label:before {
content: '';
display: inline-block;
font-size: 22px;
color: #949494;
padding: 10px 27px 10px 27px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all:checked + label:before {
color: #484848;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E');
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
font-weight: 400;
color: #484848;
}
.todo-list li.completed label {
color: #949494;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #949494;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover,
.todo-list li .destroy:focus {
color: #C18585;
}
.todo-list li .destroy:after {
content: '×';
display: block;
height: 100%;
line-height: 1.1;
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
padding: 10px 15px;
height: 20px;
text-align: center;
font-size: 15px;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: #DB7676;
}
.filters li a.selected {
border-color: #CE4646;
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 19px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #4d4d4d;
font-size: 11px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
:focus,
.toggle:focus + label,
.toggle-all:focus + label {
box-shadow: 0 0 2px 2px #CF7D7D;
outline: 0;
}
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
.toggle-all {
width: 40px !important;
height: 60px !important;
right: auto !important;
}
.toggle-all-label {
pointer-events: none;
}

90
demos/todo-mvc/app.jsx Normal file
View File

@ -0,0 +1,90 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { render } from '@openinula/next';
import { Header } from './components/header';
import { Main } from './components/main';
import {
ADD_ITEM,
UPDATE_ITEM,
REMOVE_ITEM,
TOGGLE_ITEM,
REMOVE_ALL_ITEMS,
TOGGLE_ALL,
REMOVE_COMPLETED_ITEMS,
} from './constants';
import './app.css';
import { Footer } from './components/footer.jsx';
// This alphabet uses `A-Za-z0-9_-` symbols.
// The order of characters is optimized for better gzip and brotli compression.
// References to the same file (works both for gzip and brotli):
// `'use`, `andom`, and `rict'`
// References to the brotli default dictionary:
// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf`
let urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
function nanoid(size = 21) {
let id = '';
// A compact alternative for `for (var i = 0; i < step; i++)`.
let i = size;
while (i--) {
// `| 0` is more compact and faster than `Math.floor()`.
id += urlAlphabet[(Math.random() * 64) | 0];
}
return id;
}
export function App() {
let todos = [];
const todoReducer = (state, action) => {
switch (action.type) {
case ADD_ITEM:
return state.concat({ id: nanoid(), title: action.payload.title, completed: false });
case UPDATE_ITEM:
return state.map(todo => (todo.id === action.payload.id ? { ...todo, title: action.payload.title } : todo));
case REMOVE_ITEM:
return state.filter(todo => todo.id !== action.payload.id);
case TOGGLE_ITEM:
return state.map(todo => (todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo));
case REMOVE_ALL_ITEMS:
return [];
case TOGGLE_ALL:
return state.map(todo =>
todo.completed !== action.payload.completed
? {
...todo,
completed: action.payload.completed,
}
: todo
);
case REMOVE_COMPLETED_ITEMS:
return state.filter(todo => !todo.completed);
}
throw Error(`Unknown action: ${action.type}`);
};
const dispatch = action => {
todos = todoReducer(todos, action);
};
return (
<>
<Header dispatch={dispatch} />
<Main todos={todos} dispatch={dispatch} />
<Footer todos={todos} dispatch={dispatch} />
</>
);
}
render(App, 'main');

View File

@ -0,0 +1,38 @@
import { REMOVE_COMPLETED_ITEMS } from '../constants';
export function Footer({ todos, dispatch }) {
let route = window.location.hash;
window.addEventListener('hashchange', () => {
console.log('hashchange', window.location.hash);
route = window.location.hash.slice(1);
});
const activeTodos = todos.filter(todo => !todo.completed);
const removeCompleted = () => dispatch({ type: REMOVE_COMPLETED_ITEMS });
return (
<footer className="footer" data-testid="footer">
<span className="todo-count">{`${activeTodos.length} ${activeTodos.length === 1 ? 'item' : 'items'} left!`}</span>
<ul className="filters" data-testid="footer-navigation">
<li>
<a className={route === '/' ? 'selected' : ''} href="#/">
All
</a>
</li>
<li>
<a className={route === '/active' ? 'selected' : ''} href="#/active">
Active
</a>
</li>
<li>
<a className={route === '/completed' ? 'selected' : ''} href="#/completed">
Completed
</a>
</li>
</ul>
<button className="clear-completed" disabled={activeTodos.length === todos.length} onClick={removeCompleted}>
Clear completed
</button>
</footer>
);
}

View File

@ -0,0 +1,13 @@
import { Input } from './input';
import { ADD_ITEM } from '../constants';
export function Header({ dispatch }) {
const addItem = title => dispatch({ type: ADD_ITEM, payload: { title } });
return (
<header className="header" data-testid="header">
<h1>todos</h1>
<Input onSubmit={addItem} label="New Todo Input" placeholder="What needs to be done?" />
</header>
);
}

View File

@ -0,0 +1,52 @@
const sanitize = string => {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;',
};
const reg = /[&<>"'/]/gi;
return string.replace(reg, match => map[match]);
};
const hasValidMin = (value, min) => {
return value.length >= min;
};
export function Input({ onSubmit, placeholder, label, defaultValue, onBlur }) {
const handleBlur = () => {
if (onBlur) onBlur();
};
const handleKeyDown = e => {
if (e.key === 'Enter') {
const value = e.target.value.trim();
if (!hasValidMin(value, 2)) return;
onSubmit(sanitize(value));
e.target.value = '';
}
};
return (
<div className="input-container">
<input
className="new-todo"
id="todo-input"
type="text"
data-testid="text-input"
autoFocus
placeholder={placeholder}
defaultValue={defaultValue}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
/>
<label className="visually-hidden" htmlFor="todo-input">
{label}
</label>
</div>
);
}

View File

@ -0,0 +1,50 @@
import { Input } from './input';
import { TOGGLE_ITEM, REMOVE_ITEM, UPDATE_ITEM } from '../constants';
export const Item = function Item({ todo, dispatch }) {
let isWritable = false;
const { title, completed, id } = todo;
const toggleItem = () => dispatch({ type: TOGGLE_ITEM, payload: { id } });
const removeItem = () => dispatch({ type: REMOVE_ITEM, payload: { id } });
const updateItem = (id, title) => dispatch({ type: UPDATE_ITEM, payload: { id, title } });
const handleDoubleClick = () => {
isWritable = true;
};
const handleBlur = () => {
isWritable = false;
};
const handleUpdate = title => {
if (title.length === 0) removeItem(id);
else updateItem(id, title);
isWritable = false;
};
return (
<li className={todo.completed ? 'completed' : ''} data-testid="todo-item">
<div className="view">
<if cond={isWritable}>
<Input onSubmit={handleUpdate} label="Edit Todo Input" defaultValue={title} onBlur={handleBlur} />
</if>
<else>
<input
className="toggle"
type="checkbox"
data-testid="todo-item-toggle"
checked={completed}
onChange={toggleItem}
/>
<label data-testid="todo-item-label" onDoubleClick={handleDoubleClick}>
{title}
</label>
<button className="destroy" data-testid="todo-item-button" onClick={removeItem} />
</else>
</div>
</li>
);
};

View File

@ -0,0 +1,43 @@
import { TOGGLE_ALL } from '../constants';
import { Item } from './item.jsx';
export function Main({ todos, dispatch }) {
// listen for route changes
let route = window.location.hash;
window.addEventListener('hashchange', () => {
console.log('hashchange', window.location.hash);
route = window.location.hash.slice(1);
});
const visibleTodos = todos.filter(todo => {
if (route === '/active') return !todo.completed;
if (route === '/completed') return todo.completed;
return todo;
});
const toggleAll = e => dispatch({ type: TOGGLE_ALL, payload: { completed: e.target.checked } });
return (
<main className="main" data-testid="main">
<if cond={visibleTodos.length > 0}>
<div className="toggle-all-container">
<input
className="toggle-all"
type="checkbox"
data-testid="toggle-all"
checked={visibleTodos.every(todo => todo.completed)}
onChange={toggleAll}
/>
<label className="toggle-all-label" htmlFor="toggle-all">
Toggle All Input
</label>
</div>
</if>
<ul className="todo-list" data-testid="todo-list">
<for each={visibleTodos}>{todo => <Item todo={todo} key={todo.id} dispatch={dispatch} />}</for>
</ul>
</main>
);
}

View File

@ -0,0 +1,7 @@
export const ADD_ITEM = 'ADD_ITEM';
export const UPDATE_ITEM = 'UPDATE_ITEM';
export const REMOVE_ITEM = 'REMOVE_ITEM';
export const TOGGLE_ITEM = 'TOGGLE_ITEM';
export const REMOVE_ALL_ITEMS = 'REMOVE_ALL_ITEMS';
export const TOGGLE_ALL = 'TOGGLE_ALL';
export const REMOVE_COMPLETED_ITEMS = 'REMOVE_COMPLETED_ITEMS';

12
demos/todo-mvc/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Inula-Next</title>
<link rel="stylesheet" href="./app.css"/>
</head>
<body>
<div id="main" class="todoapp"></div>
<script type="module" src="./app.jsx"></script>
</body>
</html>

View File

@ -0,0 +1,25 @@
{
"name": "inula-next-todo-mvc",
"private": true,
"version": "0.0.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@babel/standalone": "^7.22.4",
"@iandx/easy-css": "^0.10.14",
"@openinula/next": "workspace:*",
"@openinula/babel-preset-inula-next": "workspace:*"
},
"devDependencies": {
"typescript": "^5.2.2",
"vite": "^4.4.9",
"@openinula/vite-plugin-inula-next": "workspace:*"
},
"keywords": [
"inula-next"
]
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"jsx": "preserve",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"experimentalDecorators": true
},
"ts-node": {
"esm": true
}
}

View File

@ -0,0 +1,16 @@
import { defineConfig } from 'vite';
import inula from '@openinula/vite-plugin-inula-next';
export default defineConfig({
build: {
minify: false, // 设置为 false 可以关闭代码压缩
},
server: {
port: 4320,
},
base: '',
optimizeDeps: {
disabled: true,
},
plugins: [inula({ files: '**/*.{ts,js,tsx,jsx}' })],
});

8
demos/v2/CHANGELOG.md Normal file
View File

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

12
demos/v2/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Inula-Next</title>
<link rel="stylesheet" href="/src/App.css"/>
</head>
<body>
<div id="main" class="todoapp"></div>
<script type="module" src="/src/App.tsx"></script>
</body>
</html>

24
demos/v2/package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "dev",
"private": true,
"version": "0.0.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@babel/standalone": "^7.22.4",
"@openinula/next": "0.0.2",
"@openinula/vite-plugin-inula-next": "0.0.4",
"classnames": "^2.5.1"
},
"devDependencies": {
"typescript": "^5.2.2",
"vite": "^4.4.9"
},
"keywords": [
"inula-next"
]
}

81
demos/v2/src/App.css Normal file
View File

@ -0,0 +1,81 @@
/* tree-view.css */
.app {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.app-title {
font-size: 24px;
margin-bottom: 20px;
color: #333;
}
.tree-view {
background-color: #f5f5f5;
border-radius: 4px;
padding: 10px;
}
.tree-view > .tree-node:first-child {
margin-top: 5px; /* Add margin to the first top-level tree-node */
}
.tree-node {
margin-bottom: 5px;
}
.tree-node-children > .tree-node:first-child {
margin-top: 5px; /* Add margin to the first top-level tree-node */
}
.tree-node-content {
display: flex;
align-items: center;
cursor: pointer;
padding: 5px;
border-radius: 4px;
transition: background-color 0.2s;
}
.tree-node-content:hover {
background-color: #e0e0e0;
}
.tree-node-toggle {
min-width: 24px;
text-align: center;
font-size: 12px;
color: #666;
transition: transform 0.2s;
}
.tree-node-toggle.expanded {
transform: rotate(0deg);
}
.tree-node-toggle.collapsed {
transform: rotate(-90deg);
}
.tree-node-name {
margin-left: 5px;
}
.tree-node-children {
margin-left: 16px;
padding-left: 16px;
border-left: 1px solid #ccc;
}
/* Add some color to differentiate levels */
.tree-node .tree-node .tree-node-content {
background-color: #f0f0f0;
}
.tree-node .tree-node .tree-node .tree-node-content {
background-color: #e8e8e8;
}

95
demos/v2/src/App.tsx Normal file
View File

@ -0,0 +1,95 @@
import { render } from '@openinula/next';
const TreeNode = ({ node, onToggle }) => {
let isExpanded = true;
const handleToggle = () => {
isExpanded = !isExpanded;
onToggle(node.id, !isExpanded);
};
return (
<div className="tree-node">
<div onClick={handleToggle} className="tree-node-content">
<if cond={node.children}>
<span className={`tree-node-toggle ${isExpanded ? 'expanded' : 'collapsed'}`}></span>
</if>
<else>
<span className="tree-node-toggle"></span>
</else>
<span className="tree-node-name">{node.name}</span>
</div>
<if cond={isExpanded}>
<div className="tree-node-children">
<if cond={node.children}>
<for each={node.children}>
{child => {
return <TreeNode key={child.id} node={child} onToggle={onToggle} />;
}}
</for>
</if>
</div>
</if>
</div>
);
};
const TreeView = ({ data }) => {
let expandedNodes = [];
const handleToggle = (nodeId, isExpanded) => {
if (isExpanded) {
expandedNodes = [...expandedNodes, nodeId];
} else {
expandedNodes = expandedNodes.filter(id => id !== nodeId);
}
};
return (
<div className="tree-view">
<for each={data}>{node => <TreeNode key={node.id} node={node} onToggle={handleToggle} />}</for>
</div>
);
};
function App() {
const arr = [
{
name: '我的文档',
children: [
{
name: '工作',
children: [
{ name: '项目方案.docx' },
{ name: '会议记录.txt' },
{
name: '财务报表',
children: [{ name: '2023年第一季度.xlsx' }, { name: '2023年第二季度.xlsx' }, { name: '年度总结.pptx' }],
},
],
},
{
name: '个人',
children: [
{ name: '简历.pdf' },
{ name: '家庭照片.jpg' },
{
name: '旅行计划',
children: [{ name: '暑假海岛游.md' }, { name: '冬季滑雪之旅.md' }],
},
],
},
{ name: '待办事项清单.txt' },
],
},
];
return (
<div className="app">
<h1 className="app-title">Tree View</h1>
<TreeView data={arr} />
</div>
);
}
render(App, document.getElementById('main'));

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { render } from '@openinula/next';
function AttributeBinding() {
let color = 'green';
function changeColor() {
color = color === 'red' ? 'green' : 'red';
}
return (
<h1 style={{ color }} onClick={changeColor}>
Click me to change color.
</h1>
);
}
render(AttributeBinding, document.getElementById('app'));

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { render } from '@openinula/next';
function TrafficLight() {
let lightIndex = 0;
const TRAFFIC_LIGHTS = ['red', 'green'];
let light = TRAFFIC_LIGHTS[lightIndex];
function nextLight() {
lightIndex = (lightIndex + 1) % 2;
}
return (
<>
<button onClick={nextLight}>Next light</button>
<p>Light is: {light}</p>
<p>
You must
<if cond={light === 'red'}>
<span>STOP</span>
</if>
<else-if cond={light === 'green'}>
<span>GO</span>
</else-if>
</p>
</>
);
}
render(TrafficLight, document.getElementById('app'));

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { render } from '@openinula/next';
function Form() {
let text = 'a';
let checked = true;
let picked = 'One';
let selected = 'A';
let multiSelected = [];
function updateValue(e) {
text = e.target.value;
}
function handleCheckboxChange(e) {
checked = e.target.checked;
}
function handleRadioChange(e) {
picked = e.target.value;
}
function handleSelectChange(e) {
selected = e.target.value;
}
function handleMultiSelectChange(e) {
multiSelected = Array.from(e.target.selectedOptions).map(option => option.value);
}
return (
<>
<h2>Text Input</h2>
<input value={text} onInput={updateValue} />
<p>{text}</p>
<h2>Checkbox</h2>
<input id="checkbox" type="checkbox" checked={checked} onChange={handleCheckboxChange} />
<label for="checkbox"> Checked: {checked + ''}</label>
<h2>Radio</h2>
<input type="radio" id="one" value="One" name="num" checked={picked === 'One'} onChange={handleRadioChange} />
<label for="one">One</label>
<br />
<input type="radio" id="two" value="Two" name="num" checked={picked === 'Two'} onChange={handleRadioChange} />
<label for="two">Two</label>
<p>Picked: {picked}</p>
<h2>Select</h2>
<select id="select" value={selected} onChange={handleSelectChange} style={{ width: '100px' }}>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
<p>Selected: {selected}</p>
<h2>Multi Select</h2>
<select multiple style={{ width: '100px' }} value={multiSelected} onChange={handleMultiSelectChange}>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
<p>Selected: {multiSelected}</p>
</>
);
}
render(Form, document.getElementById('app'));

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { render } from '@openinula/next';
const API_URL = 'https://jsonplaceholder.typicode.com/users';
const users = await (await fetch(API_URL)).json();
function List({ arr }) {
return (
<ul>
<for each={arr}>{item => <li>{item.name}</li>}</for>
</ul>
);
}
function App() {
return <List arr={users} />;
}
render(App, document.getElementById('app'));

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { render } from '@openinula/next';
function HelloWorld() {
return <h1>Hello World!</h1>;
}
render(HelloWorld, document.getElementById('app'));

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { render } from '@openinula/next';
function Colors() {
const colors = ['red', 'green', 'blue'];
return (
<ul>
<for each={colors}>{color => <li>{color}</li>}</for>
</ul>
);
}
render(Colors, document.getElementById('app'));

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { render } from '@openinula/next';
let fruits = ['apple', 'banana', 'pear'];
function List({ arr }) {
return (
<ul>
<for each={arr}>{item => <li>{item}</li>}</for>
</ul>
);
}
function App() {
return <List arr={fruits} />;
}
render(App, document.getElementById('app'));

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { render } from '@openinula/next';
function UserInput() {
let count = 0;
function incrementCount() {
count++;
}
return (
<>
<h1>{count}</h1>
<button onClick={incrementCount}>Add 1</button>
</>
);
}
render(UserInput, document.getElementById('app'));

20
demos/v2/tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"jsx": "preserve",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"experimentalDecorators": true
},
"ts-node": {
"esm": true
}
}

16
demos/v2/vite.config.ts Normal file
View File

@ -0,0 +1,16 @@
import { defineConfig } from 'vite';
import inula from '@openinula/vite-plugin-inula-next';
export default defineConfig({
build: {
minify: false, // 设置为 false 可以关闭代码压缩
},
server: {
port: 4320,
},
base: '',
optimizeDeps: {
disabled: true,
},
plugins: [inula({ files: '**/*.{ts,js,tsx,jsx}' })],
});

View File

@ -0,0 +1,27 @@
# OpenInula 2.0 Parser
This document describes the OpenInula 2.0 parser, which is used to parse the 2.0 API into a HIR (High-level Intermediate
Representation) that can be used by the OpenInula 2.0 compiler.
## Workflow
```mermaid
graph TD
A[OpenInula 2.0 Code] --> B[Visitor]
B --> C[VariableAnalyze]
B --> D[ViewAnalyze]
B --> E[fnMacroAnalyze]
B --> F[HookAnalyze-TODO]
C --> R[ReactivityParser]
D --> G[JSXParser]
G --> R
E --> R
F --> R
R --> |unused bit pruning|HIR
```
## Data Structure
see `types.ts` in `packages/transpiler/babel-inula-next-core/src/analyze/types.ts`
## TODO LIST
- [x] for analyze the local variable, we need to consider the scope of the variable
- [ ] hook analyze

117
docs/inula-next/README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,16 +13,23 @@
* See the Mulan PSL v2 for more details.
*/
import Inula, { Component } from 'openinula';
import { injectIntl } from '../../index';
const Example5 = ({ intl }) => {
// 使用intl.formatMessage来获取国际化消息
console.log(intl + '------------intl-------------');
return (
<div className="card">
<h2>injectIntl方式测试Demo</h2>
<pre>{intl.formatMessage({ id: 'text4' })}</pre>
</div>
);
};
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,33 @@
{
"name": "@openinula/next-shared",
"version": "0.0.1",
"description": "Inula Next Shared",
"keywords": [
"Inula-Next"
],
"license": "MIT",
"files": [
"src"
],
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsup --sourcemap"
},
"devDependencies": {
"typescript": "^5.3.2",
"tsup": "^8.2.4"
},
"tsup": {
"entry": [
"src/index.ts"
],
"format": [
"cjs",
"esm"
],
"clean": true,
"dts": true,
"sourceMap": true
}
}

View File

@ -0,0 +1,13 @@
export enum InulaNodeType {
Comp = 0,
For = 1,
Cond = 2,
Exp = 3,
Hook = 4,
Context = 5,
Children = 6,
}
export function getTypeName(type: InulaNodeType): string {
return InulaNodeType[type];
}

View File

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

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