Skip to content

Commit

Permalink
Merge pull request #38 from rezo-labs/feature/literal_string
Browse files Browse the repository at this point in the history
Allow literal strings
  • Loading branch information
duydvu authored Apr 30, 2023
2 parents a6070ea + 71abd01 commit 0f714ce
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 21 deletions.
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ npm i directus-extension-computed-interface

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

# Syntax

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

## Examples
Sum 2 numbers:
Expand Down Expand Up @@ -60,6 +60,11 @@ Complex calculation:
{{ SUM(MULTIPLY(2, x), b) }}
```

Literal strings are enclosed by double quotes (`"`):
```
{{ CONCAT(file, ".txt") }}
```

## Available operators

### Type conversion
Expand Down Expand Up @@ -119,9 +124,9 @@ Operator | Description
`LOWER(a)` | to lower case
`UPPER(a)` | to upper case
`TRIM(a)` | removes whitespace at the beginning and end of string.
`CONCAT(a, b)` | concat 2 strings
`LEFT(a, b)` | extract `b` characters from the beginning of the string.
`RIGHT(a, b)` | extract `b` characters from the end of the string.
`CONCAT(a, b)` | concat 2 strings `a` and `b`.
`LEFT(a, b)` | extract `b` characters from the beginning of the string `a`.
`RIGHT(a, b)` | extract `b` characters from the end of the string `a`.

### Boolean

Expand Down Expand Up @@ -162,7 +167,3 @@ Operator | Description
There are 2 dynamic variables available that you can use in the expressions:
- `$NOW`: return the current Date object. Example: `{{ YEAR($NOW) }}` returns the current year.
- `$CURRENT_USER`: return the current user's id. Example: `{{ EQUAL($CURRENT_USER, user) }}` checks if the `user` field is the current user.


# Limitation
- Cannot parse literal strings (`{{ 's' }}`).
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "directus-extension-computed-interface",
"version": "1.5.0",
"version": "1.6.0",
"description": "Perform computed value based on other fields",
"author": {
"email": "duydvu98@gmail.com",
Expand Down
83 changes: 83 additions & 0 deletions src/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,64 @@ describe('Test parseExpression', () => {
expect(parseExpression('IF(EQUAL(a, 5), b, c)', { a: 5, b: 1, c: 2})).toBe(1);
expect(parseExpression('IF(AND(GT(a, 0), LT(a, 10)), b, c)', { a: 5, b: 1, c: 2})).toBe(1);
});

describe('Nested expressions', () => {
test('Simple nested numeric expression', () => {
expect(parseExpression('SUM(a, MULTIPLY(b, c))', { a: 1, b: 2, c: 3 })).toBe(7);
});

test('Complex nested numeric expression', () => {
expect(parseExpression('SUM(a, MULTIPLY(b, SUM(c, d)))', { a: 1, b: 2, c: 3, d: 4 })).toBe(15);
});

test('Simple nested boolean expression', () => {
expect(parseExpression('AND(a, OR(b, c))', { a: true, b: false, c: false })).toBe(false);
});

test('Complex nested boolean expression', () => {
expect(parseExpression('AND(a, OR(b, AND(c, d)))', { a: true, b: false, c: true, d: false })).toBe(false);
});

test('Simple nested string expression', () => {
expect(parseExpression('CONCAT(a, CONCAT(b, c))', { a: 'a', b: 'b', c: 'c' })).toBe('abc');
});

test('Complex nested string expression', () => {
expect(parseExpression('CONCAT(a, CONCAT(b, CONCAT(c, d)))', { a: 'a', b: 'b', c: 'c', d: 'd' })).toBe('abcd');
});
});

describe('Literal strings', () => {
test('Simple string', () => {
expect(parseExpression('"a"', {})).toBe('a');
});

test('String with escaped quotes', () => {
expect(parseExpression('"a\\"b"', {})).toBe('a"b');
});

test('String with escaped backslash', () => {
expect(parseExpression('"a\\b"', {})).toBe('a\\b');
});

test('String with parentheses and comma', () => {
expect(parseExpression('"a(b,c)d"', {})).toBe('a(b,c)d');
});

test('String with all special characters', () => {
expect(parseExpression('"a(b,c)d\\"e\\f"', {})).toBe('a(b,c)d"e\\f');
});

test('String operator 1', () => {
expect(parseExpression('RIGHT(CONCAT(UPPER(CONCAT(a, "c")), 1), 3)', { a: 'ab' })).toBe('BC1');
});

test('String operator 2', () => {
expect(parseExpression('EQUAL(CONCAT(LOWER("A,()\\""), a), "a,()\\"bc")', { a: 'bc' })).toBe(true);
expect(parseExpression('EQUAL(CONCAT("A,()\\"", a), "a,()\\"bc")', { a: 'bc' })).toBe(false);
expect(parseExpression('EQUAL(CONCAT("A,()\\"", a), "A,()\\"bc")', { a: 'bc' })).toBe(true);
});
});
});

describe('Test parseOp', () => {
Expand Down Expand Up @@ -345,6 +403,31 @@ describe('Test parseOp', () => {
args: ['OP_(var1)', 'var2', 'var3'],
});
});

test('Contains space at both ends', () => {
expect(parseOp(' OP_(var1) ')).toStrictEqual({
op: 'OP_',
args: ['var1'],
});
});

test('Handle literal string', () => {
expect(parseOp('OP_("(abc)\\", \\"(def)", ")(,\\"")')).toStrictEqual({
op: 'OP_',
args: ['"(abc)\\", \\"(def)"', '")(,\\""'],
});
});

test('Handle literal string in complex op', () => {
expect(parseOp('OP_(OP_(var1), OP_("(abc)\\", \\"(def)"))')).toStrictEqual({
op: 'OP_',
args: ['OP_(var1)', 'OP_("(abc)\\", \\"(def)")'],
});
expect(parseOp('OP_(OP_("(abc)\\", \\"(def)"), OP_(var1))')).toStrictEqual({
op: 'OP_',
args: ['OP_("(abc)\\", \\"(def)")', 'OP_(var1)'],
});
});
});

describe('Test toSlug', () => {
Expand Down
29 changes: 22 additions & 7 deletions src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ export function parseExpression(exp: string, values: Record<string, any>, defaul
if (values) {
exp = exp.trim();

// literal string
if (exp.startsWith('"') && exp.endsWith('"')) {
return exp.slice(1, -1).replace(/\\"/g, '"');
}

let { value, found } = findValueByPath(values, exp);

if(!found || value === null) {
Expand Down Expand Up @@ -222,24 +227,34 @@ export function parseOp(exp: string): {
op: string;
args: string[];
} | null {
const match = exp.match(/^([A-Z_]+)\((.+)\)$/);
const match = exp.trim().match(/^([A-Z_]+)\((.+)\)$/);
if (match) {
const args = [];
const op = match[1] as string;
const innerExp = match[2] as string;

let braceCount = 0, i = 0, j = 0;
let braceCount = 0,
i = 0,
j = 0,
inQuote = false,
escapeNext = false;
for (; i < innerExp.length; i += 1) {
const c = innerExp[i];
if (c === '(') braceCount += 1;
if (c === ')') braceCount -= 1;
if (c === ',' && braceCount === 0) {
args.push(innerExp.slice(j, i));
if (c === '(' && !inQuote) braceCount += 1;
else if (c === ')' && !inQuote) braceCount -= 1;
else if (c === ',' && !inQuote && braceCount === 0) {
args.push(innerExp.slice(j, i).trim());
j = i + 1;
}
else if (c === '"' && !escapeNext) inQuote = !inQuote;
else if (c === '\\' && inQuote) {
escapeNext = true;
continue;
}
escapeNext = false;
}
if (j < i) {
args.push(innerExp.slice(j, i));
args.push(innerExp.slice(j, i).trim());
}

return { op, args };
Expand Down

0 comments on commit 0f714ce

Please sign in to comment.