From 165eff2e5afb52708247667d9c4dd6ccec2e550d Mon Sep 17 00:00:00 2001 From: Michael Drury Date: Wed, 14 Aug 2024 17:21:40 +0100 Subject: [PATCH] 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`) +}