@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.
@@ -110,6 +110,9 @@ export const RichMessageEditor = ({
110
110
  if (selection) {
111
111
  onSelectionChange(selection);
112
112
  }
113
+ },
114
+ onDelete: () => {
115
+ setShowDrop(undefined);
113
116
  }
114
117
  }), showDrop && React.createElement("div", {
115
118
  style: floatingStyles,
@@ -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
  }
@@ -13,6 +13,7 @@ export declare const SlateEditor: FC<SlateEditor>;
13
13
  type SlateEditableProps = {
14
14
  minHeight?: string;
15
15
  onFocus?: () => void;
16
+ onDelete: () => void;
16
17
  };
17
18
  export declare const SlateEditable: FC<SlateEditableProps>;
18
19
  export {};
@@ -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: renderElement,
35
+ renderElement: renderElementPersistent,
31
36
  onFocus: onFocus
32
37
  });
33
38
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "1.0.65",
3
+ "version": "1.0.67",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -0,0 +1,2 @@
1
+ export type { LangMessages, LangProps, UseLangHook } from './types';
2
+ export { LangProvider, LangBase, useLangBase } from './react';
@@ -0,0 +1 @@
1
+ export { LangProvider, LangBase, useLangBase } from './react';
@@ -0,0 +1,3 @@
1
+ import type { Context, ScalarOrVariable, ScalarValue } from './types';
2
+ export type FilterFn = (value: ScalarValue, args: ScalarOrVariable[], context: Context) => ScalarValue;
3
+ export declare const filters: Record<string, FilterFn>;
@@ -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,3 @@
1
+ import { type ScalarValue, type ASTNodeList, type IfOperator } from './types';
2
+ export declare const ifOperatorFns: Record<IfOperator, (a: ScalarValue, b: ScalarValue) => boolean>;
3
+ export declare function stringToAst(template: string): ASTNodeList;
@@ -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,9 @@
1
+ export type LangMessages<T> = {
2
+ [K in keyof T]: string;
3
+ };
4
+ export type LangProps<M> = {
5
+ [K in keyof M]: {
6
+ message: K;
7
+ } & M[K];
8
+ }[keyof M];
9
+ export type UseLangHook<M> = () => (props: LangProps<M>) => string;
@@ -0,0 +1 @@
1
+ export {};