Skip to content

Commit 0f714ce

Browse files
authored
Merge pull request #38 from rezo-labs/feature/literal_string
Allow literal strings
2 parents a6070ea + 71abd01 commit 0f714ce

File tree

5 files changed

+120
-21
lines changed

5 files changed

+120
-21
lines changed

README.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ npm i directus-extension-computed-interface
1818

1919
# Get Started
2020
1. Go to **Settings**, create a new field with type string or number.
21-
2. In the **Interface** panel, choose **Computed** interface. There are 2 options:
21+
2. In the **Interface** panel, choose **Computed** interface. There are 5 options:
2222
1. **Template**: Similar to M2M interface, determine how the field is calculated. Learn more about syntax in the next section.
2323
2. **Field Mode**: Choose how the value is displayed.
2424
- **null**: Default option. Show an input with the computed value but still allow manual editing.
@@ -30,9 +30,9 @@ npm i directus-extension-computed-interface
3030

3131
# Syntax
3232

33-
The template consists of 2 elements: plain strings & expressions.
34-
- Plain strings are string literal, often used for text interpolation.
35-
- Expressions can contains operators, other fields & numbers. They must be enclosed by `{{` and `}}`.
33+
The template consists of 2 elements: **plain strings** & **expressions**.
34+
- **Plain** strings are string literal, often used for text interpolation.
35+
- **Expressions** can contains operators, field names, numbers & strings. They must be enclosed by `{{` and `}}`.
3636

3737
## Examples
3838
Sum 2 numbers:
@@ -60,6 +60,11 @@ Complex calculation:
6060
{{ SUM(MULTIPLY(2, x), b) }}
6161
```
6262

63+
Literal strings are enclosed by double quotes (`"`):
64+
```
65+
{{ CONCAT(file, ".txt") }}
66+
```
67+
6368
## Available operators
6469

6570
### Type conversion
@@ -119,9 +124,9 @@ Operator | Description
119124
`LOWER(a)` | to lower case
120125
`UPPER(a)` | to upper case
121126
`TRIM(a)` | removes whitespace at the beginning and end of string.
122-
`CONCAT(a, b)` | concat 2 strings
123-
`LEFT(a, b)` | extract `b` characters from the beginning of the string.
124-
`RIGHT(a, b)` | extract `b` characters from the end of the string.
127+
`CONCAT(a, b)` | concat 2 strings `a` and `b`.
128+
`LEFT(a, b)` | extract `b` characters from the beginning of the string `a`.
129+
`RIGHT(a, b)` | extract `b` characters from the end of the string `a`.
125130

126131
### Boolean
127132

@@ -162,7 +167,3 @@ Operator | Description
162167
There are 2 dynamic variables available that you can use in the expressions:
163168
- `$NOW`: return the current Date object. Example: `{{ YEAR($NOW) }}` returns the current year.
164169
- `$CURRENT_USER`: return the current user's id. Example: `{{ EQUAL($CURRENT_USER, user) }}` checks if the `user` field is the current user.
165-
166-
167-
# Limitation
168-
- Cannot parse literal strings (`{{ 's' }}`).

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "directus-extension-computed-interface",
3-
"version": "1.5.0",
3+
"version": "1.6.0",
44
"description": "Perform computed value based on other fields",
55
"author": {
66
"email": "duydvu98@gmail.com",

src/operations.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,64 @@ describe('Test parseExpression', () => {
272272
expect(parseExpression('IF(EQUAL(a, 5), b, c)', { a: 5, b: 1, c: 2})).toBe(1);
273273
expect(parseExpression('IF(AND(GT(a, 0), LT(a, 10)), b, c)', { a: 5, b: 1, c: 2})).toBe(1);
274274
});
275+
276+
describe('Nested expressions', () => {
277+
test('Simple nested numeric expression', () => {
278+
expect(parseExpression('SUM(a, MULTIPLY(b, c))', { a: 1, b: 2, c: 3 })).toBe(7);
279+
});
280+
281+
test('Complex nested numeric expression', () => {
282+
expect(parseExpression('SUM(a, MULTIPLY(b, SUM(c, d)))', { a: 1, b: 2, c: 3, d: 4 })).toBe(15);
283+
});
284+
285+
test('Simple nested boolean expression', () => {
286+
expect(parseExpression('AND(a, OR(b, c))', { a: true, b: false, c: false })).toBe(false);
287+
});
288+
289+
test('Complex nested boolean expression', () => {
290+
expect(parseExpression('AND(a, OR(b, AND(c, d)))', { a: true, b: false, c: true, d: false })).toBe(false);
291+
});
292+
293+
test('Simple nested string expression', () => {
294+
expect(parseExpression('CONCAT(a, CONCAT(b, c))', { a: 'a', b: 'b', c: 'c' })).toBe('abc');
295+
});
296+
297+
test('Complex nested string expression', () => {
298+
expect(parseExpression('CONCAT(a, CONCAT(b, CONCAT(c, d)))', { a: 'a', b: 'b', c: 'c', d: 'd' })).toBe('abcd');
299+
});
300+
});
301+
302+
describe('Literal strings', () => {
303+
test('Simple string', () => {
304+
expect(parseExpression('"a"', {})).toBe('a');
305+
});
306+
307+
test('String with escaped quotes', () => {
308+
expect(parseExpression('"a\\"b"', {})).toBe('a"b');
309+
});
310+
311+
test('String with escaped backslash', () => {
312+
expect(parseExpression('"a\\b"', {})).toBe('a\\b');
313+
});
314+
315+
test('String with parentheses and comma', () => {
316+
expect(parseExpression('"a(b,c)d"', {})).toBe('a(b,c)d');
317+
});
318+
319+
test('String with all special characters', () => {
320+
expect(parseExpression('"a(b,c)d\\"e\\f"', {})).toBe('a(b,c)d"e\\f');
321+
});
322+
323+
test('String operator 1', () => {
324+
expect(parseExpression('RIGHT(CONCAT(UPPER(CONCAT(a, "c")), 1), 3)', { a: 'ab' })).toBe('BC1');
325+
});
326+
327+
test('String operator 2', () => {
328+
expect(parseExpression('EQUAL(CONCAT(LOWER("A,()\\""), a), "a,()\\"bc")', { a: 'bc' })).toBe(true);
329+
expect(parseExpression('EQUAL(CONCAT("A,()\\"", a), "a,()\\"bc")', { a: 'bc' })).toBe(false);
330+
expect(parseExpression('EQUAL(CONCAT("A,()\\"", a), "A,()\\"bc")', { a: 'bc' })).toBe(true);
331+
});
332+
});
275333
});
276334

277335
describe('Test parseOp', () => {
@@ -345,6 +403,31 @@ describe('Test parseOp', () => {
345403
args: ['OP_(var1)', 'var2', 'var3'],
346404
});
347405
});
406+
407+
test('Contains space at both ends', () => {
408+
expect(parseOp(' OP_(var1) ')).toStrictEqual({
409+
op: 'OP_',
410+
args: ['var1'],
411+
});
412+
});
413+
414+
test('Handle literal string', () => {
415+
expect(parseOp('OP_("(abc)\\", \\"(def)", ")(,\\"")')).toStrictEqual({
416+
op: 'OP_',
417+
args: ['"(abc)\\", \\"(def)"', '")(,\\""'],
418+
});
419+
});
420+
421+
test('Handle literal string in complex op', () => {
422+
expect(parseOp('OP_(OP_(var1), OP_("(abc)\\", \\"(def)"))')).toStrictEqual({
423+
op: 'OP_',
424+
args: ['OP_(var1)', 'OP_("(abc)\\", \\"(def)")'],
425+
});
426+
expect(parseOp('OP_(OP_("(abc)\\", \\"(def)"), OP_(var1))')).toStrictEqual({
427+
op: 'OP_',
428+
args: ['OP_("(abc)\\", \\"(def)")', 'OP_(var1)'],
429+
});
430+
});
348431
});
349432

350433
describe('Test toSlug', () => {

src/operations.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ export function parseExpression(exp: string, values: Record<string, any>, defaul
44
if (values) {
55
exp = exp.trim();
66

7+
// literal string
8+
if (exp.startsWith('"') && exp.endsWith('"')) {
9+
return exp.slice(1, -1).replace(/\\"/g, '"');
10+
}
11+
712
let { value, found } = findValueByPath(values, exp);
813

914
if(!found || value === null) {
@@ -222,24 +227,34 @@ export function parseOp(exp: string): {
222227
op: string;
223228
args: string[];
224229
} | null {
225-
const match = exp.match(/^([A-Z_]+)\((.+)\)$/);
230+
const match = exp.trim().match(/^([A-Z_]+)\((.+)\)$/);
226231
if (match) {
227232
const args = [];
228233
const op = match[1] as string;
229234
const innerExp = match[2] as string;
230235

231-
let braceCount = 0, i = 0, j = 0;
236+
let braceCount = 0,
237+
i = 0,
238+
j = 0,
239+
inQuote = false,
240+
escapeNext = false;
232241
for (; i < innerExp.length; i += 1) {
233242
const c = innerExp[i];
234-
if (c === '(') braceCount += 1;
235-
if (c === ')') braceCount -= 1;
236-
if (c === ',' && braceCount === 0) {
237-
args.push(innerExp.slice(j, i));
243+
if (c === '(' && !inQuote) braceCount += 1;
244+
else if (c === ')' && !inQuote) braceCount -= 1;
245+
else if (c === ',' && !inQuote && braceCount === 0) {
246+
args.push(innerExp.slice(j, i).trim());
238247
j = i + 1;
239248
}
249+
else if (c === '"' && !escapeNext) inQuote = !inQuote;
250+
else if (c === '\\' && inQuote) {
251+
escapeNext = true;
252+
continue;
253+
}
254+
escapeNext = false;
240255
}
241256
if (j < i) {
242-
args.push(innerExp.slice(j, i));
257+
args.push(innerExp.slice(j, i).trim());
243258
}
244259

245260
return { op, args };

0 commit comments

Comments
 (0)