@pushwoosh/dumb-components 1.0.65 → 1.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RichMessageEditor/RichMessageEditor.js +3 -0
- package/RichMessageEditor/slate/DynamicContentElement.d.ts +1 -1
- package/RichMessageEditor/slate/DynamicContentElement.js +9 -4
- package/RichMessageEditor/slate/SlateEditor.d.ts +1 -0
- package/RichMessageEditor/slate/SlateEditor.js +7 -2
- package/package.json +1 -1
- package/polyglot/index.d.ts +2 -0
- package/polyglot/index.js +1 -0
- package/polyglot/liquid/filters.d.ts +3 -0
- package/polyglot/liquid/filters.js +44 -0
- package/polyglot/liquid/helpers.d.ts +9 -0
- package/polyglot/liquid/helpers.js +111 -0
- package/polyglot/liquid/parser.d.ts +3 -0
- package/polyglot/liquid/parser.js +344 -0
- package/polyglot/liquid/render.d.ts +5 -0
- package/polyglot/liquid/render.js +96 -0
- package/polyglot/liquid/types.d.ts +72 -0
- package/polyglot/liquid/types.js +1 -0
- package/polyglot/react.d.ts +10 -0
- package/polyglot/react.js +60 -0
- package/polyglot/types.d.ts +9 -0
- package/polyglot/types.js +1 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
export declare function renderElement(props: any): React.JSX.Element;
|
|
2
|
+
export declare function renderElement(props: any, onDelete: () => void): React.JSX.Element;
|
|
@@ -6,6 +6,7 @@ import { Chip } from '../../Chip';
|
|
|
6
6
|
const DynamicContentElementComponent = ({
|
|
7
7
|
attributes,
|
|
8
8
|
element,
|
|
9
|
+
onDelete,
|
|
9
10
|
children
|
|
10
11
|
}) => {
|
|
11
12
|
const editor = useSlateStatic();
|
|
@@ -38,6 +39,7 @@ const DynamicContentElementComponent = ({
|
|
|
38
39
|
focus: point
|
|
39
40
|
});
|
|
40
41
|
}
|
|
42
|
+
onDelete();
|
|
41
43
|
}
|
|
42
44
|
}, React.createElement("span", null, tagName || 'Not set'), children);
|
|
43
45
|
};
|
|
@@ -45,12 +47,14 @@ const Element = props => {
|
|
|
45
47
|
const {
|
|
46
48
|
attributes,
|
|
47
49
|
children,
|
|
48
|
-
element
|
|
50
|
+
element,
|
|
51
|
+
onDelete
|
|
49
52
|
} = props;
|
|
50
53
|
switch (element.type) {
|
|
51
54
|
case 'dc':
|
|
52
55
|
return React.createElement(DynamicContentElementComponent, {
|
|
53
|
-
...props
|
|
56
|
+
...props,
|
|
57
|
+
onDelete: onDelete
|
|
54
58
|
});
|
|
55
59
|
default:
|
|
56
60
|
return React.createElement("p", {
|
|
@@ -58,8 +62,9 @@ const Element = props => {
|
|
|
58
62
|
}, children);
|
|
59
63
|
}
|
|
60
64
|
};
|
|
61
|
-
export function renderElement(props) {
|
|
65
|
+
export function renderElement(props, onDelete) {
|
|
62
66
|
return React.createElement(Element, {
|
|
63
|
-
...props
|
|
67
|
+
...props,
|
|
68
|
+
onDelete: onDelete
|
|
64
69
|
});
|
|
65
70
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Slate } from 'slate-react';
|
|
3
|
+
import { usePersistentFunction } from '@pushwoosh/kit-helpers';
|
|
3
4
|
import { renderElement } from './DynamicContentElement';
|
|
4
5
|
import { content2string, string2content } from './helpers';
|
|
5
6
|
import { EditableStyled } from './SlateEditor.styled';
|
|
@@ -23,11 +24,15 @@ export const SlateEditor = ({
|
|
|
23
24
|
};
|
|
24
25
|
export const SlateEditable = ({
|
|
25
26
|
minHeight,
|
|
26
|
-
onFocus
|
|
27
|
+
onFocus,
|
|
28
|
+
onDelete
|
|
27
29
|
}) => {
|
|
30
|
+
const renderElementPersistent = usePersistentFunction(props => {
|
|
31
|
+
return renderElement(props, onDelete);
|
|
32
|
+
});
|
|
28
33
|
return React.createElement(EditableStyled, {
|
|
29
34
|
"$minHeight": minHeight,
|
|
30
|
-
renderElement:
|
|
35
|
+
renderElement: renderElementPersistent,
|
|
31
36
|
onFocus: onFocus
|
|
32
37
|
});
|
|
33
38
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LangProvider, LangBase, useLangBase } from './react';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ensureNumber, ensureString, unpackScalarOrValue } from './helpers';
|
|
2
|
+
const universalPluralize = (num, plurals) => num === 1 ? plurals[0] : plurals[1];
|
|
3
|
+
const pluralizers = {
|
|
4
|
+
en: universalPluralize,
|
|
5
|
+
ru: (num, plurals) => {
|
|
6
|
+
if (num % 10 === 1 && num % 100 !== 11) {
|
|
7
|
+
return plurals[0];
|
|
8
|
+
}
|
|
9
|
+
if ([2, 3, 4].includes(num % 10) && ![12, 13, 14].includes(num % 100)) {
|
|
10
|
+
return plurals[1];
|
|
11
|
+
}
|
|
12
|
+
return plurals[2];
|
|
13
|
+
},
|
|
14
|
+
fr: universalPluralize,
|
|
15
|
+
de: universalPluralize,
|
|
16
|
+
es: universalPluralize
|
|
17
|
+
};
|
|
18
|
+
export const filters = {
|
|
19
|
+
capitalize: value => typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : value,
|
|
20
|
+
upperCase: value => typeof value === 'string' ? value.toUpperCase() : value,
|
|
21
|
+
lowerCase: value => typeof value === 'string' ? value.toLowerCase() : value,
|
|
22
|
+
pluralize: (value, args, context) => {
|
|
23
|
+
if (typeof value !== 'number') {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
const variants = args.map(arg => unpackScalarOrValue(arg, context));
|
|
27
|
+
const pluralizer = pluralizers[context.language] || universalPluralize;
|
|
28
|
+
return pluralizer(value, variants);
|
|
29
|
+
},
|
|
30
|
+
formatNumber: (value, args, context) => {
|
|
31
|
+
const [fractionDigits] = args.map(arg => ensureNumber(unpackScalarOrValue(arg, context)));
|
|
32
|
+
const parts = ensureString(value).split('.');
|
|
33
|
+
let integerPart = parts[0];
|
|
34
|
+
let decimalPart = parts.length > 1 ? parts[1] : '';
|
|
35
|
+
integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
|
36
|
+
if (fractionDigits !== undefined) {
|
|
37
|
+
decimalPart = decimalPart.substring(0, fractionDigits);
|
|
38
|
+
while (decimalPart.length < fractionDigits) {
|
|
39
|
+
decimalPart += '0';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return decimalPart ? `${integerPart},${decimalPart}` : integerPart;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Context, ScalarOrVariable, ScalarValue } from './types';
|
|
2
|
+
export declare function smartSplit(str: string, delim: string): string[];
|
|
3
|
+
export declare function smartSplitAtFirstDelimiter(str: string, delim: string): [string, string?];
|
|
4
|
+
export declare function ensureNumber(value: any): number;
|
|
5
|
+
export declare function ensureString(value: any): string;
|
|
6
|
+
export declare function addSpaceAroundOperators(expression: string): string;
|
|
7
|
+
export declare function parseScalarOrVariable(value: string): ScalarOrVariable;
|
|
8
|
+
export declare function stringifyScalarOrVariable(scalarOrVariable: ScalarOrVariable): string;
|
|
9
|
+
export declare function unpackScalarOrValue(scalarOrVariable: ScalarOrVariable, context: Context): ScalarValue;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export function smartSplit(str, delim) {
|
|
2
|
+
const result = [];
|
|
3
|
+
let currentSegment = '';
|
|
4
|
+
let quoteCharacter;
|
|
5
|
+
let i = 0;
|
|
6
|
+
while (i < str.length) {
|
|
7
|
+
const currentChar = str[i];
|
|
8
|
+
const nextChar = str[i + 1];
|
|
9
|
+
if (currentChar === '\\' && quoteCharacter && nextChar === quoteCharacter) {
|
|
10
|
+
currentSegment += currentChar + nextChar;
|
|
11
|
+
i += 2;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (str[i] === '"' || str[i] === '\'') {
|
|
15
|
+
if (!quoteCharacter) {
|
|
16
|
+
quoteCharacter = str[i];
|
|
17
|
+
} else if (str[i] === quoteCharacter) {
|
|
18
|
+
quoteCharacter = undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!quoteCharacter && str.startsWith(delim, i)) {
|
|
22
|
+
result.push(currentSegment);
|
|
23
|
+
currentSegment = '';
|
|
24
|
+
i += delim.length;
|
|
25
|
+
} else {
|
|
26
|
+
currentSegment += str[i];
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (currentSegment) {
|
|
31
|
+
result.push(currentSegment);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
export function smartSplitAtFirstDelimiter(str, delim) {
|
|
36
|
+
let quoteCharacter;
|
|
37
|
+
let i = 0;
|
|
38
|
+
while (i < str.length) {
|
|
39
|
+
if (str[i] === '\\' && quoteCharacter && str[i + 1] === quoteCharacter) {
|
|
40
|
+
i += 2;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (str[i] === '"' || str[i] === '\'') {
|
|
44
|
+
if (!quoteCharacter) {
|
|
45
|
+
quoteCharacter = str[i];
|
|
46
|
+
} else if (str[i] === quoteCharacter) {
|
|
47
|
+
quoteCharacter = undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!quoteCharacter && str.startsWith(delim, i)) {
|
|
51
|
+
return [str.slice(0, i), str.slice(i + delim.length)];
|
|
52
|
+
}
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
return [str];
|
|
56
|
+
}
|
|
57
|
+
export function ensureNumber(value) {
|
|
58
|
+
const parsed = parseFloat(value);
|
|
59
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
60
|
+
}
|
|
61
|
+
export function ensureString(value) {
|
|
62
|
+
if (value === undefined || value === null) {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'string') {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
return `${value}`;
|
|
69
|
+
}
|
|
70
|
+
export function addSpaceAroundOperators(expression) {
|
|
71
|
+
return expression.replace(/([=<>!]+)/g, match => ` ${match} `);
|
|
72
|
+
}
|
|
73
|
+
function isNumeric(value) {
|
|
74
|
+
return /^-?\d+(\.\d+)?$/.test(value);
|
|
75
|
+
}
|
|
76
|
+
export function parseScalarOrVariable(value) {
|
|
77
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
78
|
+
return {
|
|
79
|
+
type: 'Scalar',
|
|
80
|
+
value: value.slice(1, -1).replaceAll('\\"', '"')
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (value === 'true' || value === 'false') {
|
|
84
|
+
return {
|
|
85
|
+
type: 'Scalar',
|
|
86
|
+
value: value === 'true'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (isNumeric(value)) {
|
|
90
|
+
return {
|
|
91
|
+
type: 'Scalar',
|
|
92
|
+
value: parseFloat(value)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
type: 'Variable',
|
|
97
|
+
name: value
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function stringifyScalarOrVariable(scalarOrVariable) {
|
|
101
|
+
if (scalarOrVariable.type === 'Scalar') {
|
|
102
|
+
return JSON.stringify(scalarOrVariable.value);
|
|
103
|
+
}
|
|
104
|
+
return scalarOrVariable.name;
|
|
105
|
+
}
|
|
106
|
+
export function unpackScalarOrValue(scalarOrVariable, context) {
|
|
107
|
+
if (scalarOrVariable.type === 'Scalar') {
|
|
108
|
+
return scalarOrVariable.value;
|
|
109
|
+
}
|
|
110
|
+
return context.values[scalarOrVariable.name];
|
|
111
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { smartSplit, smartSplitAtFirstDelimiter, ensureString, addSpaceAroundOperators, parseScalarOrVariable } from './helpers';
|
|
2
|
+
const pairedControlTokens = {
|
|
3
|
+
for: 'endfor',
|
|
4
|
+
if: 'endif'
|
|
5
|
+
};
|
|
6
|
+
const pairedControlTokensReversed = {
|
|
7
|
+
endfor: 'for',
|
|
8
|
+
endif: 'if'
|
|
9
|
+
};
|
|
10
|
+
function tokenizeLiquid(template) {
|
|
11
|
+
const tokens = [];
|
|
12
|
+
let cursor = 0;
|
|
13
|
+
let lineNo = 1;
|
|
14
|
+
const isEOF = () => cursor >= template.length;
|
|
15
|
+
const nextChar = () => {
|
|
16
|
+
if (template[cursor] === '\n') {
|
|
17
|
+
lineNo++;
|
|
18
|
+
}
|
|
19
|
+
return template[cursor++];
|
|
20
|
+
};
|
|
21
|
+
// const addToken = (part: Omit<Token, 'lineNo'>) => tokens.push({ ...part, lineNo });
|
|
22
|
+
const captureText = () => {
|
|
23
|
+
let value = '';
|
|
24
|
+
while (!isEOF() && ['{{', '{%', '<'].every(delimiter => !template.startsWith(delimiter, cursor))) {
|
|
25
|
+
value += nextChar();
|
|
26
|
+
}
|
|
27
|
+
if (value) {
|
|
28
|
+
tokens.push({
|
|
29
|
+
type: 'Text',
|
|
30
|
+
value,
|
|
31
|
+
lineNo
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const captureUntil = (delimiter, skipChars) => {
|
|
36
|
+
cursor += skipChars; // Skip the {{ or {% or <
|
|
37
|
+
let value = '';
|
|
38
|
+
let inQuotes = false;
|
|
39
|
+
while (!isEOF()) {
|
|
40
|
+
if (template[cursor] === '"' || template[cursor] === '\'') {
|
|
41
|
+
inQuotes = !inQuotes;
|
|
42
|
+
}
|
|
43
|
+
if (!inQuotes && template.startsWith(delimiter, cursor)) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
value += nextChar();
|
|
47
|
+
}
|
|
48
|
+
cursor += skipChars; // Skip the }} or %} or >
|
|
49
|
+
return value;
|
|
50
|
+
};
|
|
51
|
+
const captureFilterArgs = filterPart => {
|
|
52
|
+
const [filter, argsStr] = smartSplitAtFirstDelimiter(filterPart, ':');
|
|
53
|
+
const args = argsStr ? smartSplit(argsStr, ',').map(arg => arg.trim()) : [];
|
|
54
|
+
tokens.push({
|
|
55
|
+
type: 'Filter',
|
|
56
|
+
value: filter,
|
|
57
|
+
lineNo
|
|
58
|
+
});
|
|
59
|
+
args.forEach(arg => {
|
|
60
|
+
tokens.push({
|
|
61
|
+
type: 'FilterArg',
|
|
62
|
+
value: arg,
|
|
63
|
+
lineNo
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const captureVariable = () => {
|
|
68
|
+
const value = captureUntil('}}', 2);
|
|
69
|
+
const [variableName, ...filters] = smartSplit(value.trim(), '|').map(part => part.trim());
|
|
70
|
+
if (variableName) {
|
|
71
|
+
tokens.push({
|
|
72
|
+
type: 'Variable',
|
|
73
|
+
value: variableName,
|
|
74
|
+
lineNo
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
filters.forEach(captureFilterArgs);
|
|
78
|
+
};
|
|
79
|
+
const captureControl = () => {
|
|
80
|
+
const value = captureUntil('%}', 2);
|
|
81
|
+
const trimLeft = value.startsWith('-');
|
|
82
|
+
const trimRight = value.endsWith('-');
|
|
83
|
+
const trimmedValue = value.replace(/^-|-$/g, '').trim();
|
|
84
|
+
const [control, arg] = smartSplitAtFirstDelimiter(trimmedValue, ' ');
|
|
85
|
+
if (pairedControlTokensReversed[control]) {
|
|
86
|
+
tokens.push({
|
|
87
|
+
type: 'ControlEnd',
|
|
88
|
+
value: pairedControlTokensReversed[control],
|
|
89
|
+
lineNo,
|
|
90
|
+
trimLeft,
|
|
91
|
+
trimRight
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
tokens.push({
|
|
95
|
+
type: 'Control',
|
|
96
|
+
value: control,
|
|
97
|
+
lineNo,
|
|
98
|
+
trimLeft,
|
|
99
|
+
trimRight
|
|
100
|
+
});
|
|
101
|
+
tokens.push({
|
|
102
|
+
type: 'ControlArg',
|
|
103
|
+
value: arg || '',
|
|
104
|
+
lineNo
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const captureTag = () => {
|
|
109
|
+
const value = captureUntil('>', 1);
|
|
110
|
+
const [tagName, args] = smartSplitAtFirstDelimiter(value, ' ');
|
|
111
|
+
if (tagName.startsWith('/')) {
|
|
112
|
+
tokens.push({
|
|
113
|
+
type: 'TagEnd',
|
|
114
|
+
value: tagName.slice(1),
|
|
115
|
+
lineNo
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
tokens.push({
|
|
119
|
+
type: 'Tag',
|
|
120
|
+
value: tagName,
|
|
121
|
+
lineNo
|
|
122
|
+
});
|
|
123
|
+
tokens.push({
|
|
124
|
+
type: 'TagArg',
|
|
125
|
+
value: args || '',
|
|
126
|
+
lineNo
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
while (!isEOF()) {
|
|
131
|
+
if (template.startsWith('{{', cursor)) {
|
|
132
|
+
captureVariable();
|
|
133
|
+
} else if (template.startsWith('{%', cursor)) {
|
|
134
|
+
captureControl();
|
|
135
|
+
} else if (template.startsWith('<', cursor)) {
|
|
136
|
+
captureTag();
|
|
137
|
+
} else {
|
|
138
|
+
captureText();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Add an EOF token at the end
|
|
142
|
+
tokens.push({
|
|
143
|
+
type: 'EOF',
|
|
144
|
+
value: '',
|
|
145
|
+
lineNo
|
|
146
|
+
});
|
|
147
|
+
return tokens;
|
|
148
|
+
}
|
|
149
|
+
export const ifOperatorFns = {
|
|
150
|
+
'==': (a, b) => a === b,
|
|
151
|
+
'!=': (a, b) => a !== b,
|
|
152
|
+
'>': (a, b) => a > b,
|
|
153
|
+
'<': (a, b) => a < b,
|
|
154
|
+
'>=': (a, b) => a >= b,
|
|
155
|
+
'<=': (a, b) => a <= b,
|
|
156
|
+
contains: (a, b) => ensureString(a).includes(ensureString(b)),
|
|
157
|
+
startsWith: (a, b) => ensureString(a).startsWith(ensureString(b)),
|
|
158
|
+
endsWith: (a, b) => ensureString(a).endsWith(ensureString(b))
|
|
159
|
+
};
|
|
160
|
+
const controlNodeParsers = {
|
|
161
|
+
if: (args, lineNo, childrens) => {
|
|
162
|
+
const [left, operator, right] = smartSplit(addSpaceAroundOperators(args), ' ').filter(part => part !== '');
|
|
163
|
+
if (!ifOperatorFns[operator]) {
|
|
164
|
+
throw new Error(`Syntax error: unknown operator ${operator} at line ${lineNo}`);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
type: 'Control',
|
|
168
|
+
name: 'if',
|
|
169
|
+
condition: [parseScalarOrVariable(left), operator, parseScalarOrVariable(right)],
|
|
170
|
+
children: childrens[0],
|
|
171
|
+
elseChildren: childrens[1],
|
|
172
|
+
lineNo
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
for: (args, lineNo, childrens) => {
|
|
176
|
+
const [varName,, over] = args.split(' ').filter(part => part !== '');
|
|
177
|
+
return {
|
|
178
|
+
type: 'Control',
|
|
179
|
+
name: 'for',
|
|
180
|
+
over: parseScalarOrVariable(over),
|
|
181
|
+
varName,
|
|
182
|
+
children: childrens[0],
|
|
183
|
+
lineNo
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
else: (_args, lineNo) => ({
|
|
187
|
+
type: 'Control',
|
|
188
|
+
name: 'else',
|
|
189
|
+
lineNo
|
|
190
|
+
})
|
|
191
|
+
};
|
|
192
|
+
function parseTokens(tokensArg) {
|
|
193
|
+
const controlStack = [];
|
|
194
|
+
let currentTokenIndex = 0;
|
|
195
|
+
function parseUntil(tokens, endType) {
|
|
196
|
+
const nodes = [];
|
|
197
|
+
while (currentTokenIndex < tokens.length) {
|
|
198
|
+
const token = tokens[currentTokenIndex];
|
|
199
|
+
if (token.type === endType) {
|
|
200
|
+
currentTokenIndex++;
|
|
201
|
+
return nodes;
|
|
202
|
+
}
|
|
203
|
+
const tokenHandlers = {
|
|
204
|
+
Text: () => {
|
|
205
|
+
nodes.push({
|
|
206
|
+
type: 'Text',
|
|
207
|
+
content: token.value
|
|
208
|
+
});
|
|
209
|
+
currentTokenIndex++;
|
|
210
|
+
},
|
|
211
|
+
Variable: () => {
|
|
212
|
+
const variableNode = {
|
|
213
|
+
type: 'Variable',
|
|
214
|
+
value: parseScalarOrVariable(token.value),
|
|
215
|
+
filters: [],
|
|
216
|
+
lineNo: token.lineNo
|
|
217
|
+
};
|
|
218
|
+
currentTokenIndex++;
|
|
219
|
+
while (tokens[currentTokenIndex] && tokens[currentTokenIndex].type === 'Filter') {
|
|
220
|
+
const filterToken = tokens[currentTokenIndex];
|
|
221
|
+
const filterNode = {
|
|
222
|
+
type: 'Filter',
|
|
223
|
+
name: filterToken.value,
|
|
224
|
+
args: [],
|
|
225
|
+
lineNo: filterToken.lineNo
|
|
226
|
+
};
|
|
227
|
+
currentTokenIndex++;
|
|
228
|
+
const args = [];
|
|
229
|
+
while (tokens[currentTokenIndex] && tokens[currentTokenIndex].type === 'FilterArg') {
|
|
230
|
+
args.push(parseScalarOrVariable(tokens[currentTokenIndex].value));
|
|
231
|
+
currentTokenIndex++;
|
|
232
|
+
}
|
|
233
|
+
filterNode.args = args;
|
|
234
|
+
variableNode.filters.push(filterNode);
|
|
235
|
+
}
|
|
236
|
+
nodes.push(variableNode);
|
|
237
|
+
},
|
|
238
|
+
Control: () => {
|
|
239
|
+
const controlNodeName = token.value;
|
|
240
|
+
const chidrens = [[], []];
|
|
241
|
+
let args = '';
|
|
242
|
+
currentTokenIndex++;
|
|
243
|
+
if (tokens[currentTokenIndex] && tokens[currentTokenIndex].type === 'ControlArg') {
|
|
244
|
+
args = tokens[currentTokenIndex].value;
|
|
245
|
+
} else {
|
|
246
|
+
throw new Error(`Syntax error: expected argument for ${controlNodeName} at line ${token.lineNo}`);
|
|
247
|
+
}
|
|
248
|
+
currentTokenIndex++;
|
|
249
|
+
if (pairedControlTokens[controlNodeName]) {
|
|
250
|
+
const controlChildren = parseUntil(tokens, 'ControlEnd');
|
|
251
|
+
const elseIndex = controlChildren.findIndex(child => child.type === 'Control' && child.name === 'else');
|
|
252
|
+
if (elseIndex !== -1) {
|
|
253
|
+
chidrens[0] = controlChildren.slice(0, elseIndex);
|
|
254
|
+
chidrens[1] = controlChildren.slice(elseIndex + 1);
|
|
255
|
+
} else {
|
|
256
|
+
chidrens[0] = controlChildren;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (!controlNodeParsers[controlNodeName]) {
|
|
260
|
+
throw new Error(`Syntax error: unknown control ${controlNodeName} at line ${token.lineNo}`);
|
|
261
|
+
}
|
|
262
|
+
const controlNode = controlNodeParsers[controlNodeName](args.trim(), token.lineNo, chidrens);
|
|
263
|
+
controlStack.unshift(controlNode);
|
|
264
|
+
nodes.push(controlNode);
|
|
265
|
+
},
|
|
266
|
+
ControlEnd: () => {
|
|
267
|
+
if (controlStack.length === 0 || controlStack[0].type === 'Control' && controlStack[0].name === token.value) {
|
|
268
|
+
throw new Error(`Syntax error: unmatched ${token.value} at line ${token.lineNo}`);
|
|
269
|
+
}
|
|
270
|
+
controlStack.shift();
|
|
271
|
+
currentTokenIndex++;
|
|
272
|
+
return nodes;
|
|
273
|
+
},
|
|
274
|
+
Tag: () => {
|
|
275
|
+
const tagName = token.value;
|
|
276
|
+
let args = '';
|
|
277
|
+
currentTokenIndex++;
|
|
278
|
+
if (tokens[currentTokenIndex] && tokens[currentTokenIndex].type === 'TagArg') {
|
|
279
|
+
args = tokens[currentTokenIndex].value || '';
|
|
280
|
+
currentTokenIndex++;
|
|
281
|
+
}
|
|
282
|
+
const tagChildren = parseUntil(tokens, 'TagEnd');
|
|
283
|
+
const tagNode = {
|
|
284
|
+
type: 'Tag',
|
|
285
|
+
name: tagName,
|
|
286
|
+
// parse tag params color="red" => { color: "red" }
|
|
287
|
+
params: Object.fromEntries(smartSplit(args, ' ').filter(arg => arg).map(arg => arg.split('=')).map(([key, value]) => [key, parseScalarOrVariable(value)])),
|
|
288
|
+
children: tagChildren,
|
|
289
|
+
lineNo: token.lineNo
|
|
290
|
+
};
|
|
291
|
+
controlStack.unshift(tagNode);
|
|
292
|
+
nodes.push(tagNode);
|
|
293
|
+
},
|
|
294
|
+
TagEnd: () => {
|
|
295
|
+
if (controlStack.length === 0 || controlStack[0].type === 'Tag' && controlStack[0].name === token.value) {
|
|
296
|
+
throw new Error(`Syntax error: unmatched ${token.value} at line ${token.lineNo}`);
|
|
297
|
+
}
|
|
298
|
+
controlStack.shift();
|
|
299
|
+
currentTokenIndex++;
|
|
300
|
+
return nodes;
|
|
301
|
+
},
|
|
302
|
+
EOF: () => {
|
|
303
|
+
if (controlStack.length > 0) {
|
|
304
|
+
throw new Error(`Syntax error: unclosed ${controlStack[controlStack.length - 1].name}`);
|
|
305
|
+
}
|
|
306
|
+
return nodes;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
if (tokenHandlers[token.type]) {
|
|
310
|
+
const checkerRes = tokenHandlers[token.type]();
|
|
311
|
+
if (checkerRes) {
|
|
312
|
+
return checkerRes;
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
throw new Error(`Unknown token type: ${token.type}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (endType !== 'EOF') {
|
|
319
|
+
throw new Error(`Syntax error: expected ${endType} before end of file`);
|
|
320
|
+
}
|
|
321
|
+
return nodes;
|
|
322
|
+
}
|
|
323
|
+
return parseUntil(tokensArg, 'EOF');
|
|
324
|
+
}
|
|
325
|
+
export function stringToAst(template) {
|
|
326
|
+
const tokens = tokenizeLiquid(template);
|
|
327
|
+
const trimmedTokens = tokens.map((token, index) => {
|
|
328
|
+
if (token.type === 'Text') {
|
|
329
|
+
// eslint-disable-next-line no-nested-ternary
|
|
330
|
+
const prevControl = tokens[index - 1] && tokens[index - 1].type === 'ControlArg' ? tokens[index - 2] : tokens[index - 1] && tokens[index - 1].type === 'Control' ? tokens[index - 1] : undefined;
|
|
331
|
+
const leftTrim = prevControl === null || prevControl === void 0 ? void 0 : prevControl.trimRight;
|
|
332
|
+
const rightTrim = tokens[index + 1] && tokens[index + 1].trimLeft;
|
|
333
|
+
if (leftTrim || rightTrim) {
|
|
334
|
+
const newValue = leftTrim ? token.value.trimStart() : token.value;
|
|
335
|
+
return {
|
|
336
|
+
...token,
|
|
337
|
+
value: rightTrim ? newValue.trimEnd() : newValue
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return token;
|
|
342
|
+
});
|
|
343
|
+
return parseTokens(trimmedTokens);
|
|
344
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type ASTNodeList, type Context, type ScalarOrVariable } from './types';
|
|
3
|
+
export declare function applyFilter(filterName: string, value: any, args: ScalarOrVariable[], context: Context): any;
|
|
4
|
+
export declare function renderToString(ast: ASTNodeList, context: Context): string;
|
|
5
|
+
export declare function renderToReact(ast: ASTNodeList, context: Context): React.ReactNode[];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Color } from '@pushwoosh/kit-constants';
|
|
3
|
+
import { Text } from '@pushwoosh/kit-typography';
|
|
4
|
+
import { filters } from './filters';
|
|
5
|
+
import { stringifyScalarOrVariable, unpackScalarOrValue } from './helpers';
|
|
6
|
+
import { ifOperatorFns } from './parser';
|
|
7
|
+
// Applies a filter to a value.
|
|
8
|
+
// @param filterName The name of the filter to apply.
|
|
9
|
+
// @param value The value to which the filter should be applied.
|
|
10
|
+
// @param args The filter arguments to pass to the filter.
|
|
11
|
+
// @param context The context in which the filter is applied.
|
|
12
|
+
export function applyFilter(filterName, value, args, context) {
|
|
13
|
+
const filterFunction = filters[filterName];
|
|
14
|
+
if (filterFunction) {
|
|
15
|
+
return filterFunction(value, args, context);
|
|
16
|
+
}
|
|
17
|
+
console.warn(`Unknown filter: ${filterName}`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
const controlNodeRenderers = {
|
|
21
|
+
if: (node, context, renderTag) => {
|
|
22
|
+
const ifNode = node;
|
|
23
|
+
const [left, operator, right] = ifNode.condition;
|
|
24
|
+
if (ifOperatorFns[operator](unpackScalarOrValue(left, context), unpackScalarOrValue(right, context))) {
|
|
25
|
+
return renderNodesToArray(ifNode.children, context, renderTag);
|
|
26
|
+
}
|
|
27
|
+
if (ifNode.elseChildren) {
|
|
28
|
+
return renderNodesToArray(ifNode.elseChildren, context, renderTag);
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
},
|
|
32
|
+
for: (node, context, renderTag) => {
|
|
33
|
+
const forNode = node;
|
|
34
|
+
const value = unpackScalarOrValue(forNode.over, context);
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
const output = [];
|
|
37
|
+
for (let i = 0; i < value.length; i++) {
|
|
38
|
+
const item = value[i];
|
|
39
|
+
const newValues = {
|
|
40
|
+
...context.values,
|
|
41
|
+
[forNode.varName]: item
|
|
42
|
+
};
|
|
43
|
+
output.push(...renderNodesToArray(forNode.children, {
|
|
44
|
+
...context,
|
|
45
|
+
values: newValues
|
|
46
|
+
}, renderTag));
|
|
47
|
+
}
|
|
48
|
+
return output;
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
},
|
|
52
|
+
else: () => []
|
|
53
|
+
};
|
|
54
|
+
function renderNodesToArray(ast, context, renderTag) {
|
|
55
|
+
const output = [];
|
|
56
|
+
for (let i = 0; i < ast.length; i++) {
|
|
57
|
+
const node = ast[i];
|
|
58
|
+
if (node.type === 'Text') {
|
|
59
|
+
output.push(node.content);
|
|
60
|
+
} else if (node.type === 'Variable') {
|
|
61
|
+
const variableNode = node;
|
|
62
|
+
const value = variableNode.filters.reduce((acc, filter) => applyFilter(filter.name, acc, filter.args, context), unpackScalarOrValue(variableNode.value, context));
|
|
63
|
+
if (value !== undefined) {
|
|
64
|
+
output.push(value);
|
|
65
|
+
}
|
|
66
|
+
} else if (node.type === 'Control') {
|
|
67
|
+
const controlNode = node;
|
|
68
|
+
output.push(...controlNodeRenderers[controlNode.name](controlNode, context, renderTag));
|
|
69
|
+
} else if (node.type === 'Tag') {
|
|
70
|
+
const tagNode = node;
|
|
71
|
+
output.push(...renderTag(tagNode, renderNodesToArray(tagNode.children, context, renderTag), i));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return output;
|
|
75
|
+
}
|
|
76
|
+
export function renderToString(ast, context) {
|
|
77
|
+
const outArr = renderNodesToArray(ast, context, (tagNode, children) => [`<${tagNode.name} ${Object.entries(tagNode.params).map(([key, value]) => `${key}=${stringifyScalarOrVariable(value)}`).join(' ')}>`, ...children, `</${tagNode.name}>`]);
|
|
78
|
+
return outArr.join('');
|
|
79
|
+
}
|
|
80
|
+
export function renderToReact(ast, context) {
|
|
81
|
+
return renderNodesToArray(ast, context, (tagNode, children, key) => {
|
|
82
|
+
const TagName = tagNode.name;
|
|
83
|
+
const props = Object.fromEntries(Object.entries(tagNode.params).map(([key, value]) => [key, unpackScalarOrValue(value, context)]));
|
|
84
|
+
if (TagName === 'text') {
|
|
85
|
+
return [React.createElement(Text, {
|
|
86
|
+
"$variant": props.variant,
|
|
87
|
+
"$color": props.color && (Color[props.color] ?? props.color),
|
|
88
|
+
key: key
|
|
89
|
+
}, children)];
|
|
90
|
+
}
|
|
91
|
+
return [React.createElement(TagName, {
|
|
92
|
+
...props,
|
|
93
|
+
key: key
|
|
94
|
+
}, children)];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type TokenType = 'Text' | 'Variable' | 'Filter' | 'FilterArg' | 'Tag' | 'TagArg' | 'TagEnd' | 'Control' | 'ControlEnd' | 'ControlArg' | 'EOF';
|
|
2
|
+
export type TokenTypeControl = 'Control' | 'ControlEnd';
|
|
3
|
+
export type TokenBase = {
|
|
4
|
+
value: string;
|
|
5
|
+
lineNo: number;
|
|
6
|
+
};
|
|
7
|
+
export interface TokenUniversal extends TokenBase {
|
|
8
|
+
type: Exclude<TokenType, TokenTypeControl>;
|
|
9
|
+
}
|
|
10
|
+
export interface TokenControl extends TokenBase {
|
|
11
|
+
type: TokenTypeControl;
|
|
12
|
+
trimLeft: boolean;
|
|
13
|
+
trimRight: boolean;
|
|
14
|
+
}
|
|
15
|
+
export type Token = TokenUniversal | TokenControl;
|
|
16
|
+
export type ScalarValue = string | number | boolean;
|
|
17
|
+
export type ScalarOrVariable = {
|
|
18
|
+
type: 'Scalar';
|
|
19
|
+
value: ScalarValue;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'Variable';
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
export type ControlName = 'if' | 'for' | 'else';
|
|
25
|
+
export interface ASTNode {
|
|
26
|
+
type: string;
|
|
27
|
+
lineNo: number;
|
|
28
|
+
}
|
|
29
|
+
export interface TextNode extends ASTNode {
|
|
30
|
+
content: string;
|
|
31
|
+
}
|
|
32
|
+
export interface VariableNode extends ASTNode {
|
|
33
|
+
value: ScalarOrVariable;
|
|
34
|
+
filters: FilterNode[];
|
|
35
|
+
}
|
|
36
|
+
export interface FilterNode extends ASTNode {
|
|
37
|
+
name: string;
|
|
38
|
+
args: ScalarOrVariable[];
|
|
39
|
+
}
|
|
40
|
+
export type ASTNodeList = ASTNode[];
|
|
41
|
+
/**
|
|
42
|
+
* ControlNode is a node that represents a control statement like {% if %} or {% for %}.
|
|
43
|
+
* It has a name, arguments, and children nodes.
|
|
44
|
+
* If the control statement has an {% else %} part, the elseChildren property will be set.
|
|
45
|
+
* The children and elseChildren properties are arrays of ASTNode.
|
|
46
|
+
*/
|
|
47
|
+
export interface ControlNodeIf extends ASTNode {
|
|
48
|
+
name: 'if';
|
|
49
|
+
condition: [ScalarOrVariable, string, ScalarOrVariable];
|
|
50
|
+
children: ASTNodeList;
|
|
51
|
+
elseChildren?: ASTNodeList;
|
|
52
|
+
}
|
|
53
|
+
export interface ControlNodeFor extends ASTNode {
|
|
54
|
+
name: 'for';
|
|
55
|
+
over: ScalarOrVariable;
|
|
56
|
+
varName: string;
|
|
57
|
+
children: ASTNodeList;
|
|
58
|
+
}
|
|
59
|
+
export interface ControlNodeElse extends ASTNode {
|
|
60
|
+
name: 'else';
|
|
61
|
+
}
|
|
62
|
+
export type ControlNode = ControlNodeIf | ControlNodeFor | ControlNodeElse;
|
|
63
|
+
export interface TagNode extends ASTNode {
|
|
64
|
+
name: string;
|
|
65
|
+
params: Record<string, ScalarOrVariable>;
|
|
66
|
+
children: ASTNodeList;
|
|
67
|
+
}
|
|
68
|
+
export type IfOperator = '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith';
|
|
69
|
+
export type Context = {
|
|
70
|
+
values: any;
|
|
71
|
+
language: string;
|
|
72
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type FC, type PropsWithChildren } from 'react';
|
|
2
|
+
import { type LangMessages } from './types';
|
|
3
|
+
type LangProviderProps = {
|
|
4
|
+
language: string;
|
|
5
|
+
languages: Record<string, LangMessages<any>>;
|
|
6
|
+
};
|
|
7
|
+
export declare const LangProvider: FC<PropsWithChildren<LangProviderProps>>;
|
|
8
|
+
export declare const LangBase: FC;
|
|
9
|
+
export declare const useLangBase: () => (allProps: any) => string;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import { stringToAst } from './liquid/parser';
|
|
3
|
+
import { renderToReact, renderToString } from './liquid/render';
|
|
4
|
+
const LangContext = createContext(null);
|
|
5
|
+
export const LangProvider = ({
|
|
6
|
+
language,
|
|
7
|
+
languages,
|
|
8
|
+
children
|
|
9
|
+
}) => {
|
|
10
|
+
const langFnPack = useMemo(() => {
|
|
11
|
+
const langMessages = languages[language];
|
|
12
|
+
const asts = Object.fromEntries(Object.entries(langMessages).map(([k, v]) => {
|
|
13
|
+
try {
|
|
14
|
+
return [k, stringToAst(v)];
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.error(k, v, e);
|
|
17
|
+
return [k, []];
|
|
18
|
+
}
|
|
19
|
+
}));
|
|
20
|
+
const getAst = message => {
|
|
21
|
+
const ast = asts[message];
|
|
22
|
+
if (!ast) {
|
|
23
|
+
console.error('no message', message);
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
return ast;
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
lang: allProps => {
|
|
30
|
+
const {
|
|
31
|
+
message,
|
|
32
|
+
...values
|
|
33
|
+
} = allProps;
|
|
34
|
+
const ast = getAst(message);
|
|
35
|
+
return renderToString(ast, {
|
|
36
|
+
values,
|
|
37
|
+
language
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
langReact: allProps => {
|
|
41
|
+
const {
|
|
42
|
+
message,
|
|
43
|
+
...values
|
|
44
|
+
} = allProps;
|
|
45
|
+
const ast = getAst(message);
|
|
46
|
+
return renderToReact(ast, {
|
|
47
|
+
values,
|
|
48
|
+
language
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}, [languages, language]);
|
|
53
|
+
return React.createElement(LangContext.Provider, {
|
|
54
|
+
value: langFnPack
|
|
55
|
+
}, children);
|
|
56
|
+
};
|
|
57
|
+
export const LangBase = props => {
|
|
58
|
+
return useContext(LangContext).langReact(props);
|
|
59
|
+
};
|
|
60
|
+
export const useLangBase = () => useContext(LangContext).lang;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|