refactor parseExpression function

This commit is contained in:
Bowen Tan 2021-11-15 18:28:43 +08:00
parent 83b559d913
commit d767784feb
4 changed files with 201 additions and 97 deletions

View File

@ -24,7 +24,7 @@
"traits": [] "traits": []
}, },
{ {
"id": "{{$moduleId}}1", "id": "{{$moduleId}}text",
"type": "core/v1/text", "type": "core/v1/text",
"properties": { "properties": {
"value": { "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", "id": "{{$moduleId}}input",
"type": "chakra_ui/v1/input", "type": "chakra_ui/v1/input",
@ -174,7 +195,7 @@
"componentId": "$utils", "componentId": "$utils",
"method": { "method": {
"name": "alert", "name": "alert",
"parameters": "listen module vent{{ littleItem1.value }}" "parameters": "listen module event{{ $listItem.name }}!"
}, },
"wait": {}, "wait": {},
"disabled": false "disabled": false

View File

@ -1,36 +1,109 @@
import { StateManager } from '../src/services/stateStore'; import { StateManager, parseExpression } from '../src/services/stateStore';
describe('parseExpression function', () => { describe('parseExpression function', () => {
const parseExpression = new StateManager().parseExpression;
it('can parse {{}} expression', () => { it('can parse {{}} expression', () => {
expect(parseExpression('{{ value }}')).toMatchObject([ expect(parseExpression('value')).toMatchObject(['value']);
{ expression: 'value', isDynamic: true }, // wrong: {{{id: 123}}}. Must have space between {{ and {
]); expect(parseExpression('{{ {id: 123} }}')).toMatchObject([[' {id: 123} ']]);
expect(parseExpression('Hello, {{ value }}!')).toMatchObject([ expect(parseExpression('Hello, {{ value }}!')).toMatchObject([
{ expression: 'Hello, ', isDynamic: false }, 'Hello, ',
{ expression: 'value', isDynamic: true }, [' value '],
{ expression: '!', isDynamic: false }, '!',
]); ]);
expect(
parseExpression('{{ $listItem.name }} is in {{ root.listTitle }} list') expect(parseExpression('{{input1.value}}')).toMatchObject([['input1.value']]);
).toMatchObject([ expect(parseExpression('{{fetch.data}}')).toMatchObject([['fetch.data']]);
{ expression: '{{$listItem.name}}', isDynamic: false },
{ expression: ' is in ', isDynamic: false }, expect(parseExpression('{{{{}}}}')).toMatchObject([[[]]]);
{ expression: 'root.listTitle', isDynamic: true },
{ expression: ' list', isDynamic: false }, expect(parseExpression('{{ value }}, {{ input1.value }}!')).toMatchObject([
]); [' value '],
expect(parseExpression('{{{{}}}}}}')).toMatchObject([ ', ',
{ expression: '{{', isDynamic: true }, [' input1.value '],
{ expression: '}}}}', isDynamic: false }, '!',
]); ]);
}); });
it('can parse $listItem expression', () => { it('can parse $listItem expression', () => {
expect(parseExpression('{{ $listItem.value }}')).toMatchObject([ expect(parseExpression('{{ $listItem.value }}')).toMatchObject([
{ expression: '{{$listItem.value}}', isDynamic: false }, '{{ $listItem.value }}',
]); ]);
expect(parseExpression('{{ $listItem.value }}', true)).toMatchObject([ 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!');
});
});

View File

@ -5,6 +5,7 @@ import { ImplWrapper } from './services/ImplWrapper';
import { resolveAppComponents } from './services/resolveAppComponents'; import { resolveAppComponents } from './services/resolveAppComponents';
import { AppProps, UIServices } from './types/RuntimeSchema'; import { AppProps, UIServices } from './types/RuntimeSchema';
import { DebugEvent, DebugStore } from './services/DebugComponents'; import { DebugEvent, DebugStore } from './services/DebugComponents';
import { css } from '@emotion/react';
// inject modules to App // inject modules to App
export function genApp(services: UIServices) { export function genApp(services: UIServices) {
@ -38,7 +39,7 @@ export const App: React.FC<AppProps> = props => {
); );
return ( return (
<div className="App"> <div className="App" css={css`height: 100vh; overflow: auto`}>
{topLevelComponents.map(c => { {topLevelComponents.map(c => {
return ( return (
<ImplWrapper <ImplWrapper

View File

@ -14,10 +14,7 @@ dayjs.extend(isLeapYear);
dayjs.extend(LocalizedFormat); dayjs.extend(LocalizedFormat);
dayjs.locale('zh-cn'); dayjs.locale('zh-cn');
type ExpChunk = { type ExpChunk = string | ExpChunk[];
expression: string;
isDynamic: boolean;
};
// TODO: use web worker // TODO: use web worker
const builtIn = { const builtIn = {
@ -34,60 +31,27 @@ function isNumeric(x: string | number) {
export function initStateManager() { export function initStateManager() {
return new StateManager(); return new StateManager();
} }
export class StateManager { export class StateManager {
store = reactive<Record<string, any>>({}); store = reactive<Record<string, any>>({});
parseExpression(str: string, parseListItem = false): ExpChunk[] { evalExp(expChunk: ExpChunk, scopeObject = {}): unknown {
let l = 0; if (typeof expChunk === 'string') {
let r = 0; return expChunk;
let isInBrackets = false;
const res = [];
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,
});
}
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) { const evalText = expChunk.map(ex => this.evalExp(ex, scopeObject)).join('');
res.push({ let evaled;
expression: str.substring(l, r + 1), try {
isDynamic: false, evaled = new Function(`with(this) { return ${evalText} }`).call({
...this.store,
...builtIn,
...scopeObject,
}); });
} catch (e: any) {
return `{{ ${evalText} }}`;
} }
return res; return evaled;
} }
maskedEval(raw: string, evalListItem = false, scopeObject = {}) { maskedEval(raw: string, evalListItem = false, scopeObject = {}) {
@ -100,26 +64,17 @@ export class StateManager {
if (raw === 'false') { if (raw === 'false') {
return false; return false;
} }
const expChunk = parseExpression(raw, evalListItem);
const expChunks = this.parseExpression(raw, evalListItem); if (typeof expChunk === 'string') {
const evaled = expChunks.map(({ expression: exp, isDynamic }) => { return expChunk;
if (!isDynamic) { }
return exp;
}
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( mapValuesDeep(
@ -150,14 +105,13 @@ export class StateManager {
const evaluated = this.mapValuesDeep(obj, ({ value: v, path }) => { const evaluated = this.mapValuesDeep(obj, ({ value: v, path }) => {
if (typeof v === 'string') { if (typeof v === 'string') {
const isDynamicExpression = this.parseExpression(v).some( const isDynamicExpression = parseExpression(v).some(exp => typeof exp !== 'string');
({ isDynamic }) => isDynamic
);
const result = this.maskedEval(v); const result = this.maskedEval(v);
if (isDynamicExpression && watcher) { if (isDynamicExpression && watcher) {
const stop = watch( const stop = watch(
() => { () => {
return this.maskedEval(v); const result = this.maskedEval(v);
return result;
}, },
newV => { newV => {
set(evaluated, path, 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;
};