From 165eff2e5afb52708247667d9c4dd6ccec2e550d Mon Sep 17 00:00:00 2001 From: Michael Drury Date: Wed, 14 Aug 2024 17:21:40 +0100 Subject: [PATCH 1/3] First pass - no tests yet, had to make some changes to how pre-processing works, as well as updating the string based on context, if there is any overlap between the helpers and context it will prefix the overlap with ./ - this means to look in context. --- packages/string-templates/src/index.ts | 45 ++++++++++++++++--- .../string-templates/src/processors/index.ts | 4 +- .../src/processors/preprocessor.ts | 10 ++++- packages/string-templates/src/types.ts | 1 + packages/string-templates/src/utilities.ts | 13 ++++++ 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index 0992813e9d..291807d5f0 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -1,17 +1,18 @@ import { Context, createContext, runInNewContext } from "vm" import { create, TemplateDelegate } from "handlebars" import { registerAll, registerMinimum } from "./helpers/index" -import { preprocess, postprocess } from "./processors" +import { postprocess, preprocess } from "./processors" import { atob, btoa, - isBackendService, - FIND_HBS_REGEX, FIND_ANY_HBS_REGEX, + FIND_HBS_REGEX, findDoubleHbsInstances, + isBackendService, + prefixStrings, } from "./utilities" import { convertHBSBlock } from "./conversion" -import { setJSRunner, removeJSRunner } from "./helpers/javascript" +import { removeJSRunner, setJSRunner } from "./helpers/javascript" import manifest from "./manifest.json" import { ProcessOptions } from "./types" @@ -23,6 +24,7 @@ export { iifeWrapper } from "./iife" const hbsInstance = create() registerAll(hbsInstance) +const helperNames = Object.keys(hbsInstance.helpers) const hbsInstanceNoHelpers = create() registerMinimum(hbsInstanceNoHelpers) const defaultOpts: ProcessOptions = { @@ -45,11 +47,20 @@ function testObject(object: any) { } } +function findOverlappingHelpers(context: object) { + const contextKeys = Object.keys(context) + return contextKeys.filter(key => helperNames.includes(key)) +} + /** * Creates a HBS template function for a given string, and optionally caches it. */ const templateCache: Record> = {} -function createTemplate(string: string, opts?: ProcessOptions) { +function createTemplate( + string: string, + opts?: ProcessOptions, + context?: object +) { opts = { ...defaultOpts, ...opts } // Finalising adds a helper, can't do this with no helpers @@ -60,7 +71,25 @@ function createTemplate(string: string, opts?: ProcessOptions) { return templateCache[key] } - string = preprocess(string, opts) + const overlappingHelpers = !opts?.noHelpers + ? findOverlappingHelpers(context) + : [] + + string = preprocess(string, { + ...opts, + disabledHelpers: overlappingHelpers, + }) + + if (context && !opts?.noHelpers) { + if (overlappingHelpers.length > 0) { + for (let block of findHBSBlocks(string)) { + string = string.replace( + block, + prefixStrings(block, overlappingHelpers, "./") + ) + } + } + } // Optionally disable built in HBS escaping if (opts.noEscaping) { @@ -70,6 +99,7 @@ function createTemplate(string: string, opts?: ProcessOptions) { // This does not throw an error when template can't be fulfilled, // have to try correct beforehand const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance + const template = instance.compile(string, { strict: false, }) @@ -171,7 +201,8 @@ export function processStringSync( throw "Cannot process non-string types." } function process(stringPart: string) { - const template = createTemplate(stringPart, opts) + // context is needed to check for overlap between helpers and context + const template = createTemplate(stringPart, opts, context) const now = Math.floor(Date.now() / 1000) * 1000 const processedString = template({ now: new Date(now).toISOString(), diff --git a/packages/string-templates/src/processors/index.ts b/packages/string-templates/src/processors/index.ts index 308ac5adf4..79085b0dfe 100644 --- a/packages/string-templates/src/processors/index.ts +++ b/packages/string-templates/src/processors/index.ts @@ -29,9 +29,9 @@ export function preprocess(string: string, opts: ProcessOptions) { processor => processor.name !== preprocessor.PreprocessorNames.FINALISE ) } + return process(string, processors, opts) } export function postprocess(string: string) { - let processors = postprocessor.processors - return process(string, processors) + return process(string, postprocessor.processors) } diff --git a/packages/string-templates/src/processors/preprocessor.ts b/packages/string-templates/src/processors/preprocessor.ts index 5e96336e32..ee76b5dbcf 100644 --- a/packages/string-templates/src/processors/preprocessor.ts +++ b/packages/string-templates/src/processors/preprocessor.ts @@ -13,10 +13,12 @@ export const PreprocessorNames = { class Preprocessor { name: string private fn: any + private helperNames: string[] constructor(name: string, fn: any) { this.name = name this.fn = fn + this.helperNames = HelperNames() } process(fullString: string, statement: string, opts: Object) { @@ -56,7 +58,10 @@ export const processors = [ }), new Preprocessor( PreprocessorNames.FINALISE, - (statement: string, opts: { noHelpers: any }) => { + ( + statement: string, + opts: { noHelpers: any; disabledHelpers?: string[] } + ) => { const noHelpers = opts && opts.noHelpers let insideStatement = statement.slice(2, statement.length - 2) if (insideStatement.charAt(0) === " ") { @@ -75,7 +80,8 @@ export const processors = [ const testHelper = possibleHelper.trim().toLowerCase() if ( !noHelpers && - HelperNames().some(option => testHelper === option.toLowerCase()) + !opts.disabledHelpers?.includes(testHelper) && + this.helperNames.some(option => testHelper === option.toLowerCase()) ) { insideStatement = `(${insideStatement})` } diff --git a/packages/string-templates/src/types.ts b/packages/string-templates/src/types.ts index 1e1ef048a4..19785b9273 100644 --- a/packages/string-templates/src/types.ts +++ b/packages/string-templates/src/types.ts @@ -5,4 +5,5 @@ export interface ProcessOptions { noFinalise?: boolean escapeNewlines?: boolean onlyFound?: boolean + disabledHelpers?: string[] } diff --git a/packages/string-templates/src/utilities.ts b/packages/string-templates/src/utilities.ts index bcb9987c89..18e0926b96 100644 --- a/packages/string-templates/src/utilities.ts +++ b/packages/string-templates/src/utilities.ts @@ -66,3 +66,16 @@ export const btoa = (plainText: string) => { export const atob = (base64: string) => { return Buffer.from(base64, "base64").toString("utf-8") } + +export const prefixStrings = ( + baseString: string, + strings: string[], + prefix: string +) => { + // Escape any special characters in the strings to avoid regex errors + const escapedStrings = strings.map(str => + str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ) + const regexPattern = new RegExp(`\\b(${escapedStrings.join("|")})\\b`, "g") + return baseString.replace(regexPattern, `${prefix}$1`) +} From 87999db65928ab8cc3586c2b619cea20be87ef7a Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 14 Aug 2024 18:12:33 +0100 Subject: [PATCH 2/3] Adding test cases. --- packages/string-templates/src/index.ts | 5 +++- .../src/processors/preprocessor.ts | 4 +--- .../string-templates/test/helpers.spec.ts | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index 291807d5f0..4a24328265 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -47,7 +47,10 @@ function testObject(object: any) { } } -function findOverlappingHelpers(context: object) { +function findOverlappingHelpers(context?: object) { + if (!context) { + return [] + } const contextKeys = Object.keys(context) return contextKeys.filter(key => helperNames.includes(key)) } diff --git a/packages/string-templates/src/processors/preprocessor.ts b/packages/string-templates/src/processors/preprocessor.ts index ee76b5dbcf..95a4d5500d 100644 --- a/packages/string-templates/src/processors/preprocessor.ts +++ b/packages/string-templates/src/processors/preprocessor.ts @@ -13,12 +13,10 @@ export const PreprocessorNames = { class Preprocessor { name: string private fn: any - private helperNames: string[] constructor(name: string, fn: any) { this.name = name this.fn = fn - this.helperNames = HelperNames() } process(fullString: string, statement: string, opts: Object) { @@ -81,7 +79,7 @@ export const processors = [ if ( !noHelpers && !opts.disabledHelpers?.includes(testHelper) && - this.helperNames.some(option => testHelper === option.toLowerCase()) + HelperNames().some(option => testHelper === option.toLowerCase()) ) { insideStatement = `(${insideStatement})` } diff --git a/packages/string-templates/test/helpers.spec.ts b/packages/string-templates/test/helpers.spec.ts index 5f1855535d..566be2b02b 100644 --- a/packages/string-templates/test/helpers.spec.ts +++ b/packages/string-templates/test/helpers.spec.ts @@ -483,3 +483,26 @@ describe("uuid", () => { expect(output).toMatch(UUID_REGEX) }) }) + +describe("helper overlap", () => { + it("should use context over helpers (regex test helper)", async () => { + const output = await processString("{{ test }}", { test: "a" }) + expect(output).toEqual("a") + }) + + it("should use helper if no sum in context, return the context value otherwise", async () => { + const hbs = "{{ sum 1 2 }}" + const output = await processString(hbs, {}) + expect(output).toEqual("3") + const secondaryOutput = await processString(hbs, { sum: "a" }) + expect(secondaryOutput).toEqual("a") + }) + + it("should handle multiple cases", async () => { + const output = await processString("{{ literal (split test sum) }}", { + test: "a-b", + sum: "-", + }) + expect(output).toEqual(["a", "b"]) + }) +}) From bbc33d37e62c154322f881f58cc75942a6391cc6 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 15 Aug 2024 11:04:57 +0100 Subject: [PATCH 3/3] PR comments. --- packages/string-templates/src/index.ts | 7 +++-- .../src/processors/postprocessor.ts | 16 +++++----- .../src/processors/preprocessor.ts | 31 ++++++++++--------- .../string-templates/test/helpers.spec.ts | 11 +++++++ 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index 4a24328265..8d5fe4c16d 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -65,6 +65,7 @@ function createTemplate( context?: object ) { opts = { ...defaultOpts, ...opts } + const helpersEnabled = !opts?.noHelpers // Finalising adds a helper, can't do this with no helpers const key = `${string}-${JSON.stringify(opts)}` @@ -74,7 +75,7 @@ function createTemplate( return templateCache[key] } - const overlappingHelpers = !opts?.noHelpers + const overlappingHelpers = helpersEnabled ? findOverlappingHelpers(context) : [] @@ -83,9 +84,9 @@ function createTemplate( disabledHelpers: overlappingHelpers, }) - if (context && !opts?.noHelpers) { + if (context && helpersEnabled) { if (overlappingHelpers.length > 0) { - for (let block of findHBSBlocks(string)) { + for (const block of findHBSBlocks(string)) { string = string.replace( block, prefixStrings(block, overlappingHelpers, "./") diff --git a/packages/string-templates/src/processors/postprocessor.ts b/packages/string-templates/src/processors/postprocessor.ts index b8d99682b1..6ddc0e67cd 100644 --- a/packages/string-templates/src/processors/postprocessor.ts +++ b/packages/string-templates/src/processors/postprocessor.ts @@ -1,19 +1,21 @@ import { LITERAL_MARKER } from "../helpers/constants" -export const PostProcessorNames = { - CONVERT_LITERALS: "convert-literals", +export enum PostProcessorNames { + CONVERT_LITERALS = "convert-literals", } -class Postprocessor { - name: string - private fn: any +type PostprocessorFn = (statement: string) => string - constructor(name: string, fn: any) { +class Postprocessor { + name: PostProcessorNames + private readonly fn: PostprocessorFn + + constructor(name: PostProcessorNames, fn: PostprocessorFn) { this.name = name this.fn = fn } - process(statement: any) { + process(statement: string) { return this.fn(statement) } } diff --git a/packages/string-templates/src/processors/preprocessor.ts b/packages/string-templates/src/processors/preprocessor.ts index 95a4d5500d..97e5c56fcc 100644 --- a/packages/string-templates/src/processors/preprocessor.ts +++ b/packages/string-templates/src/processors/preprocessor.ts @@ -1,25 +1,28 @@ import { HelperNames } from "../helpers" import { swapStrings, isAlphaNumeric } from "../utilities" +import { ProcessOptions } from "../types" const FUNCTION_CASES = ["#", "else", "/"] -export const PreprocessorNames = { - SWAP_TO_DOT: "swap-to-dot-notation", - FIX_FUNCTIONS: "fix-functions", - FINALISE: "finalise", - NORMALIZE_SPACES: "normalize-spaces", +export enum PreprocessorNames { + SWAP_TO_DOT = "swap-to-dot-notation", + FIX_FUNCTIONS = "fix-functions", + FINALISE = "finalise", + NORMALIZE_SPACES = "normalize-spaces", } +type PreprocessorFn = (statement: string, opts?: ProcessOptions) => string + class Preprocessor { name: string - private fn: any + private readonly fn: PreprocessorFn - constructor(name: string, fn: any) { + constructor(name: PreprocessorNames, fn: PreprocessorFn) { this.name = name this.fn = fn } - process(fullString: string, statement: string, opts: Object) { + process(fullString: string, statement: string, opts: ProcessOptions) { const output = this.fn(statement, opts) const idx = fullString.indexOf(statement) return swapStrings(fullString, idx, statement.length, output) @@ -56,11 +59,9 @@ export const processors = [ }), new Preprocessor( PreprocessorNames.FINALISE, - ( - statement: string, - opts: { noHelpers: any; disabledHelpers?: string[] } - ) => { - const noHelpers = opts && opts.noHelpers + (statement: string, opts?: ProcessOptions) => { + const noHelpers = opts?.noHelpers + const helpersEnabled = !noHelpers let insideStatement = statement.slice(2, statement.length - 2) if (insideStatement.charAt(0) === " ") { insideStatement = insideStatement.slice(1) @@ -77,8 +78,8 @@ export const processors = [ } const testHelper = possibleHelper.trim().toLowerCase() if ( - !noHelpers && - !opts.disabledHelpers?.includes(testHelper) && + helpersEnabled && + !opts?.disabledHelpers?.includes(testHelper) && HelperNames().some(option => testHelper === option.toLowerCase()) ) { insideStatement = `(${insideStatement})` diff --git a/packages/string-templates/test/helpers.spec.ts b/packages/string-templates/test/helpers.spec.ts index 566be2b02b..12de4f1c29 100644 --- a/packages/string-templates/test/helpers.spec.ts +++ b/packages/string-templates/test/helpers.spec.ts @@ -505,4 +505,15 @@ describe("helper overlap", () => { }) expect(output).toEqual(["a", "b"]) }) + + it("should work as expected when no helpers are set", async () => { + const output = await processString( + "{{ sum }}", + { + sum: "a", + }, + { noHelpers: true } + ) + expect(output).toEqual("a") + }) })