mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-27 08:39:59 +08:00
refactor parseExpression function
This commit is contained in:
parent
83b559d913
commit
d767784feb
@ -24,7 +24,7 @@
|
||||
"traits": []
|
||||
},
|
||||
{
|
||||
"id": "{{$moduleId}}1",
|
||||
"id": "{{$moduleId}}text",
|
||||
"type": "core/v1/text",
|
||||
"properties": {
|
||||
"value": {
|
||||
@ -44,6 +44,27 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{{$moduleId}}inputValueText",
|
||||
"type": "core/v1/text",
|
||||
"properties": {
|
||||
"value": {
|
||||
"raw": "**{{ {{$moduleId}}input.value }}**",
|
||||
"format": "md"
|
||||
}
|
||||
},
|
||||
"traits": [
|
||||
{
|
||||
"type": "core/v1/slot",
|
||||
"properties": {
|
||||
"container": {
|
||||
"id": "{{$moduleId}}hstack",
|
||||
"slot": "content"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{{$moduleId}}input",
|
||||
"type": "chakra_ui/v1/input",
|
||||
@ -174,7 +195,7 @@
|
||||
"componentId": "$utils",
|
||||
"method": {
|
||||
"name": "alert",
|
||||
"parameters": "listen module vent:{{ littleItem1.value }}!"
|
||||
"parameters": "listen module event:{{ $listItem.name }}!"
|
||||
},
|
||||
"wait": {},
|
||||
"disabled": false
|
||||
|
@ -1,36 +1,109 @@
|
||||
import { StateManager } from '../src/services/stateStore';
|
||||
import { StateManager, parseExpression } from '../src/services/stateStore';
|
||||
|
||||
describe('parseExpression function', () => {
|
||||
const parseExpression = new StateManager().parseExpression;
|
||||
it('can parse {{}} expression', () => {
|
||||
expect(parseExpression('{{ value }}')).toMatchObject([
|
||||
{ expression: 'value', isDynamic: true },
|
||||
]);
|
||||
expect(parseExpression('value')).toMatchObject(['value']);
|
||||
// wrong: {{{id: 123}}}. Must have space between {{ and {
|
||||
expect(parseExpression('{{ {id: 123} }}')).toMatchObject([[' {id: 123} ']]);
|
||||
expect(parseExpression('Hello, {{ value }}!')).toMatchObject([
|
||||
{ expression: 'Hello, ', isDynamic: false },
|
||||
{ expression: 'value', isDynamic: true },
|
||||
{ expression: '!', isDynamic: false },
|
||||
'Hello, ',
|
||||
[' value '],
|
||||
'!',
|
||||
]);
|
||||
expect(
|
||||
parseExpression('{{ $listItem.name }} is in {{ root.listTitle }} list')
|
||||
).toMatchObject([
|
||||
{ expression: '{{$listItem.name}}', isDynamic: false },
|
||||
{ expression: ' is in ', isDynamic: false },
|
||||
{ expression: 'root.listTitle', isDynamic: true },
|
||||
{ expression: ' list', isDynamic: false },
|
||||
]);
|
||||
expect(parseExpression('{{{{}}}}}}')).toMatchObject([
|
||||
{ expression: '{{', isDynamic: true },
|
||||
{ expression: '}}}}', isDynamic: false },
|
||||
|
||||
expect(parseExpression('{{input1.value}}')).toMatchObject([['input1.value']]);
|
||||
expect(parseExpression('{{fetch.data}}')).toMatchObject([['fetch.data']]);
|
||||
|
||||
expect(parseExpression('{{{{}}}}')).toMatchObject([[[]]]);
|
||||
|
||||
expect(parseExpression('{{ value }}, {{ input1.value }}!')).toMatchObject([
|
||||
[' value '],
|
||||
', ',
|
||||
[' input1.value '],
|
||||
'!',
|
||||
]);
|
||||
});
|
||||
|
||||
it('can parse $listItem expression', () => {
|
||||
expect(parseExpression('{{ $listItem.value }}')).toMatchObject([
|
||||
{ expression: '{{$listItem.value}}', isDynamic: false },
|
||||
'{{ $listItem.value }}',
|
||||
]);
|
||||
expect(parseExpression('{{ $listItem.value }}', true)).toMatchObject([
|
||||
{ expression: '$listItem.value', isDynamic: true },
|
||||
[' $listItem.value '],
|
||||
]);
|
||||
expect(
|
||||
parseExpression(
|
||||
'{{ {{$listItem.value}}input.value + {{$moduleId}}fetch.value }}!',
|
||||
true
|
||||
)
|
||||
).toMatchObject([
|
||||
[' ', ['$listItem.value'], 'input.value + ', ['$moduleId'], 'fetch.value '],
|
||||
'!',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evalExpression function', () => {
|
||||
const scope = {
|
||||
value: 'Hello',
|
||||
input1: {
|
||||
value: 'world',
|
||||
},
|
||||
fetch: {
|
||||
data: [{ id: 1 }, { id: 2 }],
|
||||
},
|
||||
checkbox: {
|
||||
value: true,
|
||||
},
|
||||
$listItem: {
|
||||
value: 'foo',
|
||||
},
|
||||
$moduleId: 'moduleBar',
|
||||
fooInput: {
|
||||
value: 'Yes, ',
|
||||
},
|
||||
moduleBarFetch: {
|
||||
value: 'ok',
|
||||
},
|
||||
};
|
||||
const stateStore = new StateManager();
|
||||
it('can eval {{}} expression', () => {
|
||||
expect(stateStore.maskedEval('value', false, scope)).toEqual('value');
|
||||
expect(stateStore.maskedEval('{{true}}', false, scope)).toEqual(true);
|
||||
expect(stateStore.maskedEval('{{ false }}', false, scope)).toEqual(false);
|
||||
expect(stateStore.maskedEval('{{[]}}', false, scope)).toEqual([]);
|
||||
expect(stateStore.maskedEval('{{ [] }}', false, scope)).toEqual([]);
|
||||
expect(stateStore.maskedEval('{{ [1,2,3] }}', false, scope)).toEqual([1, 2, 3]);
|
||||
|
||||
expect(stateStore.maskedEval('{{ {} }}', false, scope)).toEqual({});
|
||||
expect(stateStore.maskedEval('{{ {id: 123} }}', false, scope)).toEqual({ id: 123 });
|
||||
expect(stateStore.maskedEval('{{nothing}}', false, scope)).toEqual('{{ nothing }}');
|
||||
|
||||
expect(stateStore.maskedEval('{{input1.value}}', false, scope)).toEqual('world');
|
||||
expect(stateStore.maskedEval('{{checkbox.value}}', false, scope)).toEqual(true);
|
||||
expect(stateStore.maskedEval('{{fetch.data}}', false, scope)).toMatchObject([
|
||||
{ id: 1 },
|
||||
{ id: 2 },
|
||||
]);
|
||||
|
||||
expect(stateStore.maskedEval('{{{{}}}}', false, scope)).toEqual(undefined);
|
||||
|
||||
expect(
|
||||
stateStore.maskedEval('{{ value }}, {{ input1.value }}!', false, scope)
|
||||
).toEqual('Hello, world!');
|
||||
});
|
||||
|
||||
it('can eval $listItem expression', () => {
|
||||
expect(stateStore.maskedEval('{{ $listItem.value }}', false, scope)).toEqual(
|
||||
'{{ $listItem.value }}'
|
||||
);
|
||||
expect(stateStore.maskedEval('{{ $listItem.value }}', true, scope)).toEqual('foo');
|
||||
expect(
|
||||
stateStore.maskedEval(
|
||||
'{{ {{$listItem.value}}Input.value + {{$moduleId}}Fetch.value }}!',
|
||||
true,
|
||||
scope
|
||||
)
|
||||
).toEqual('Yes, ok!');
|
||||
});
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ import { ImplWrapper } from './services/ImplWrapper';
|
||||
import { resolveAppComponents } from './services/resolveAppComponents';
|
||||
import { AppProps, UIServices } from './types/RuntimeSchema';
|
||||
import { DebugEvent, DebugStore } from './services/DebugComponents';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
// inject modules to App
|
||||
export function genApp(services: UIServices) {
|
||||
@ -38,7 +39,7 @@ export const App: React.FC<AppProps> = props => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div className="App" css={css`height: 100vh; overflow: auto`}>
|
||||
{topLevelComponents.map(c => {
|
||||
return (
|
||||
<ImplWrapper
|
||||
|
@ -14,10 +14,7 @@ dayjs.extend(isLeapYear);
|
||||
dayjs.extend(LocalizedFormat);
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
type ExpChunk = {
|
||||
expression: string;
|
||||
isDynamic: boolean;
|
||||
};
|
||||
type ExpChunk = string | ExpChunk[];
|
||||
|
||||
// TODO: use web worker
|
||||
const builtIn = {
|
||||
@ -34,60 +31,27 @@ function isNumeric(x: string | number) {
|
||||
export function initStateManager() {
|
||||
return new StateManager();
|
||||
}
|
||||
|
||||
export class StateManager {
|
||||
store = reactive<Record<string, any>>({});
|
||||
|
||||
parseExpression(str: string, parseListItem = false): ExpChunk[] {
|
||||
let l = 0;
|
||||
let r = 0;
|
||||
let isInBrackets = false;
|
||||
const res = [];
|
||||
evalExp(expChunk: ExpChunk, scopeObject = {}): unknown {
|
||||
if (typeof expChunk === 'string') {
|
||||
return expChunk;
|
||||
}
|
||||
|
||||
while (r < str.length - 1) {
|
||||
if (!isInBrackets && str.substr(r, 2) === '{{') {
|
||||
if (l !== r) {
|
||||
const substr = str.substring(l, r);
|
||||
res.push({
|
||||
expression: substr,
|
||||
isDynamic: false,
|
||||
const evalText = expChunk.map(ex => this.evalExp(ex, scopeObject)).join('');
|
||||
let evaled;
|
||||
try {
|
||||
evaled = new Function(`with(this) { return ${evalText} }`).call({
|
||||
...this.store,
|
||||
...builtIn,
|
||||
...scopeObject,
|
||||
});
|
||||
} catch (e: any) {
|
||||
return `{{ ${evalText} }}`;
|
||||
}
|
||||
isInBrackets = true;
|
||||
r += 2;
|
||||
l = r;
|
||||
} else if (isInBrackets && str.substr(r, 2) === '}}') {
|
||||
// remove \n from start and end of substr
|
||||
const substr = str.substring(l, r).replace(/^\s+|\s+$/g, '');
|
||||
const chunk = {
|
||||
expression: substr,
|
||||
isDynamic: true,
|
||||
};
|
||||
// $listItem cannot be evaled in stateStore, so don't mark it as dynamic
|
||||
// unless explicitly pass parseListItem as true
|
||||
if (
|
||||
(substr.includes(LIST_ITEM_EXP) || substr.includes(LIST_ITEM_INDEX_EXP)) &&
|
||||
!parseListItem
|
||||
) {
|
||||
chunk.expression = `{{${substr}}}`;
|
||||
chunk.isDynamic = false;
|
||||
}
|
||||
res.push(chunk);
|
||||
|
||||
isInBrackets = false;
|
||||
r += 2;
|
||||
l = r;
|
||||
} else {
|
||||
r++;
|
||||
}
|
||||
}
|
||||
|
||||
if (r >= l && l < str.length) {
|
||||
res.push({
|
||||
expression: str.substring(l, r + 1),
|
||||
isDynamic: false,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
return evaled;
|
||||
}
|
||||
|
||||
maskedEval(raw: string, evalListItem = false, scopeObject = {}) {
|
||||
@ -100,26 +64,17 @@ export class StateManager {
|
||||
if (raw === 'false') {
|
||||
return false;
|
||||
}
|
||||
const expChunk = parseExpression(raw, evalListItem);
|
||||
|
||||
const expChunks = this.parseExpression(raw, evalListItem);
|
||||
const evaled = expChunks.map(({ expression: exp, isDynamic }) => {
|
||||
if (!isDynamic) {
|
||||
return exp;
|
||||
if (typeof expChunk === 'string') {
|
||||
return expChunk;
|
||||
}
|
||||
try {
|
||||
const result = new Function(`with(this) { return ${exp} }`).call({
|
||||
...this.store,
|
||||
...builtIn,
|
||||
...scopeObject,
|
||||
});
|
||||
return result;
|
||||
} catch (e: any) {
|
||||
// console.error(Error(`Cannot eval value '${exp}' in '${raw}': ${e.message}`));
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return evaled.length === 1 ? evaled[0] : evaled.join('');
|
||||
const result = expChunk.map(e => this.evalExp(e, scopeObject));
|
||||
if (result.length === 1) {
|
||||
return result[0];
|
||||
}
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
mapValuesDeep(
|
||||
@ -150,14 +105,13 @@ export class StateManager {
|
||||
|
||||
const evaluated = this.mapValuesDeep(obj, ({ value: v, path }) => {
|
||||
if (typeof v === 'string') {
|
||||
const isDynamicExpression = this.parseExpression(v).some(
|
||||
({ isDynamic }) => isDynamic
|
||||
);
|
||||
const isDynamicExpression = parseExpression(v).some(exp => typeof exp !== 'string');
|
||||
const result = this.maskedEval(v);
|
||||
if (isDynamicExpression && watcher) {
|
||||
const stop = watch(
|
||||
() => {
|
||||
return this.maskedEval(v);
|
||||
const result = this.maskedEval(v);
|
||||
return result;
|
||||
},
|
||||
newV => {
|
||||
set(evaluated, path, newV);
|
||||
@ -178,3 +132,58 @@ export class StateManager {
|
||||
};
|
||||
}
|
||||
}
|
||||
// copy and modify from
|
||||
// https://stackoverflow.com/questions/68161410/javascript-parse-multiple-brackets-recursively-from-a-string
|
||||
export const parseExpression = (exp: string, parseListItem = false): ExpChunk[] => {
|
||||
// $listItem cannot be evaled in stateStore, so don't mark it as dynamic
|
||||
// unless explicitly pass parseListItem as true
|
||||
if (
|
||||
(exp.includes(LIST_ITEM_EXP) || exp.includes(LIST_ITEM_INDEX_EXP)) &&
|
||||
!parseListItem
|
||||
) {
|
||||
return [exp];
|
||||
}
|
||||
|
||||
function lexer(str: string): string[] {
|
||||
let token = '';
|
||||
let chars = '';
|
||||
let i = 0;
|
||||
const res = [];
|
||||
while ((chars = str.slice(i, i + 2))) {
|
||||
switch (chars) {
|
||||
case '{{':
|
||||
case '}}':
|
||||
i++;
|
||||
if (token) {
|
||||
res.push(token);
|
||||
token = '';
|
||||
}
|
||||
res.push(chars);
|
||||
break;
|
||||
default:
|
||||
token += str[i];
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (token) {
|
||||
res.push(token);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function build(tokens: string[]): ExpChunk[] {
|
||||
const result: ExpChunk[] = [];
|
||||
let item;
|
||||
|
||||
while ((item = tokens.shift())) {
|
||||
if (item == '}}') return result;
|
||||
result.push(item == '{{' ? build(tokens) : item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const tokens = lexer(exp);
|
||||
const result = build(tokens);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user