From f5af53c689a593e46108a2d898e9eb7da2583c63 Mon Sep 17 00:00:00 2001 From: Evgeny Biriulin Date: Thu, 30 May 2024 07:34:03 +0400 Subject: [PATCH] feat: liquid returns result with type as type of variable If liquid receives a string containing only one variable substitution, it will return a result with the type as the type of this variable ``` typeof liquid('{{count}}', {count: 10}) === 'number' ``` --- src/transform/liquid/index.ts | 5 ++- src/transform/liquid/lexical.ts | 2 + src/transform/liquid/substitutions.ts | 52 ++++++++++++++++++---- test/liquid/filters.test.ts | 2 +- test/liquid/lexical.test.ts | 53 +++++++++++++++++++++++ test/liquid/substitutions.test.ts | 62 +++++++++++++++++++++++++++ 6 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 test/liquid/lexical.test.ts diff --git a/src/transform/liquid/index.ts b/src/transform/liquid/index.ts index 69e4b563..1e47a429 100644 --- a/src/transform/liquid/index.ts +++ b/src/transform/liquid/index.ts @@ -124,7 +124,10 @@ function liquid< output = applySubstitutions(output, vars, path); } - output = conditionsInCode ? output : repairCode(output, codes); + if (!conditionsInCode && typeof output === 'string') { + output = repairCode(output, codes); + } + codes.length = 0; if (withSourceMap) { diff --git a/src/transform/liquid/lexical.ts b/src/transform/liquid/lexical.ts index b2bac334..5055ad25 100644 --- a/src/transform/liquid/lexical.ts +++ b/src/transform/liquid/lexical.ts @@ -6,6 +6,7 @@ const quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`); export const quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`); export const vars = /((not_var)?({{2}([. \w-|(),]+)}{2}))/gm; +export const singleVariable = /^{{2}([. \w-|(),]+)}{2}$/; // basic types const number = /-?\d+\.?\d*|\.?\d+/; @@ -66,6 +67,7 @@ export const getParsedMethod = (exp: String) => { export const isLiteral = (str: string) => literalLine.test(str); export const isVariable = (str: string) => variableLine.test(str); +export const isSingleVariable = (str: string) => singleVariable.test(str); export function parseLiteral(str: string) { let res = str.match(numberLine); diff --git a/src/transform/liquid/substitutions.ts b/src/transform/liquid/substitutions.ts index dd217093..e9feb21a 100644 --- a/src/transform/liquid/substitutions.ts +++ b/src/transform/liquid/substitutions.ts @@ -4,11 +4,35 @@ import ArgvService from './services/argv'; import getObject from '../getObject'; import {evalExp} from './evaluation'; import {log} from '../log'; -import {isVariable, vars as varsRe} from './lexical'; +import { + isSingleVariable, + isVariable, + singleVariable as singleVariableRe, + vars as varsRe, +} from './lexical'; const substitutions = (str: string, builtVars: Record, path?: string) => { const {keepNotVar} = ArgvService.getConfig(); + if (isSingleVariable(str)) { + const match = str.match(singleVariableRe); + + if (!match) { + return str; + } + + const trimVarPath = match[1].trim(); + const value = substituteVariable(trimVarPath, builtVars); + + if (value === undefined) { + logNotFoundVariable(trimVarPath, path); + + return str; + } + + return value; + } + return str.replace(varsRe, (match, _groupNotVar, flag, groupVar, groupVarValue) => { if (flag) { return keepNotVar ? _groupNotVar : groupVar; @@ -20,21 +44,31 @@ const substitutions = (str: string, builtVars: Record, path?: s return groupVar; } - let value; - if (isVariable(trimVarPath)) { - value = getObject(trimVarPath, builtVars); - } else { - value = evalExp(trimVarPath, builtVars); - } + const value = substituteVariable(trimVarPath, builtVars); if (value === undefined) { - value = match; + logNotFoundVariable(trimVarPath, path); - log.warn(`Variable ${bold(trimVarPath)} not found${path ? ` in ${bold(path)}` : ''}`); + return match; } return value; }); }; +function logNotFoundVariable(varPath: string, path?: string) { + log.warn(`Variable ${bold(varPath)} not found${path ? ` in ${bold(path)}` : ''}`); +} + +function substituteVariable(varPath: string, builtVars: Record) { + let value; + if (isVariable(varPath)) { + value = getObject(varPath, builtVars); + } else { + value = evalExp(varPath, builtVars); + } + + return value; +} + export = substitutions; diff --git a/test/liquid/filters.test.ts b/test/liquid/filters.test.ts index b1a24c2b..17fa26f8 100644 --- a/test/liquid/filters.test.ts +++ b/test/liquid/filters.test.ts @@ -20,7 +20,7 @@ describe('Filters', () => { ).toEqual('Users count: 2'); }); test('Test2', () => { - expect(substitutions('{{ test | length }}', {test: 'hello world'})).toEqual('11'); + expect(substitutions('{{ test | length }}', {test: 'hello world'})).toEqual(11); }); }); diff --git a/test/liquid/lexical.test.ts b/test/liquid/lexical.test.ts new file mode 100644 index 00000000..eebe83fe --- /dev/null +++ b/test/liquid/lexical.test.ts @@ -0,0 +1,53 @@ +import {isSingleVariable} from '../../src/transform/liquid/lexical'; + +describe('Lexical functions', () => { + describe('isSingleVariable', () => { + test('Valid single variable without surrounding text', () => { + expect(isSingleVariable('{{variable}}')).toEqual(true); + }); + + test('Two variables should return false', () => { + expect(isSingleVariable('{{variable1}} {{variable2}}')).toEqual(false); + }); + + test('Text before variable should return false', () => { + expect(isSingleVariable('some text {{variable}}')).toEqual(false); + }); + + test('Text after variable should return false', () => { + expect(isSingleVariable('{{variable}} some text')).toEqual(false); + }); + + test('Valid single variable with filter', () => { + expect(isSingleVariable('{{ variable | filter }}')).toEqual(true); + }); + + test('Single variable with leading and trailing space should return false', () => { + expect(isSingleVariable(' {{variable}} ')).toEqual(false); + }); + + test('Single variable with multiple leading and trailing spaces should return false', () => { + expect(isSingleVariable(' {{variable}} ')).toEqual(false); + }); + + test('Single variable with tabs and newlines should return false', () => { + expect(isSingleVariable('\t{{variable}} \n')).toEqual(false); + }); + + test('Empty string should return false', () => { + expect(isSingleVariable('')).toEqual(false); + }); + + test('Text without variables should return false', () => { + expect(isSingleVariable('just some text')).toEqual(false); + }); + + test('Single curly braces should return false', () => { + expect(isSingleVariable('{variable}')).toEqual(false); + }); + + test('Unmatched curly braces should return false', () => { + expect(isSingleVariable('{{variable}')).toEqual(false); + }); + }); +}); diff --git a/test/liquid/substitutions.test.ts b/test/liquid/substitutions.test.ts index 7d9dfbba..57f3a97c 100644 --- a/test/liquid/substitutions.test.ts +++ b/test/liquid/substitutions.test.ts @@ -19,4 +19,66 @@ describe('Substitutions', () => { }), ).toEqual('Hello not_var{{ user.name }}!'); }); + + test('Should return unchanged string if no variables present', () => { + const input = 'This is just a string'; + expect(liquid(input, {})).toEqual(input); + }); + + test('Should return unchanged string if variable not found in context', () => { + const input = 'Variable {{ notFound }} not found'; + expect(liquid(input, {})).toEqual(input); + }); + + test('Should substitute multiple occurrences of the same variable', () => { + const input = 'Repeated {{ variable }} here and also here: {{ variable }}'; + const context = {variable: 'value'}; + expect(liquid(input, context)).toEqual('Repeated value here and also here: value'); + }); + + describe('Should save type of variable, if possible', () => { + const string = 'Example'; + const number = 10; + const boolean = true; + const nullVar = null; + const array = ['item1', 'item2', 'item3']; + const object = {key1: 'value1', key2: 'value2'}; + const undefinedVar = undefined; + + test('Should substitute to string', () => { + expect(liquid('{{ string }}', {string})).toEqual(string); + }); + + test('Should substitute to number', () => { + expect(liquid('{{ number }}', {number})).toEqual(number); + }); + + test('Should substitute to boolean', () => { + expect(liquid('{{ boolean }}', {boolean})).toEqual(boolean); + }); + + test('Should substitute to null', () => { + expect(liquid('{{ nullVar }}', {nullVar})).toEqual(nullVar); + }); + + test('Should substitute to array', () => { + expect(liquid('{{ array }}', {array})).toEqual(array); + }); + + test('Should substitute to object', () => { + expect(liquid('{{ object }}', {object})).toEqual(object); + }); + + test('Should not substitute undefined vars', () => { + expect(liquid('{{ undefinedVar }}', {undefinedVar})).toEqual('{{ undefinedVar }}'); + }); + + test('Should substitute to string if input contains more than one variable', () => { + expect(liquid('{{ number }} {{ boolean }}', {number, boolean})).toEqual( + `${number} ${boolean}`, + ); + + expect(liquid('{{ number }} postfix', {number})).toEqual(`${number} postfix`); + }); + }); });