Merge branch 'master' into grid-layout-expansion

This commit is contained in:
Andrew Kingston 2024-08-15 13:13:11 +01:00 committed by GitHub
commit daa464c4b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 117 additions and 27 deletions

View File

@ -1,17 +1,18 @@
import { Context, createContext, runInNewContext } from "vm" import { Context, createContext, runInNewContext } from "vm"
import { create, TemplateDelegate } from "handlebars" import { create, TemplateDelegate } from "handlebars"
import { registerAll, registerMinimum } from "./helpers/index" import { registerAll, registerMinimum } from "./helpers/index"
import { preprocess, postprocess } from "./processors" import { postprocess, preprocess } from "./processors"
import { import {
atob, atob,
btoa, btoa,
isBackendService,
FIND_HBS_REGEX,
FIND_ANY_HBS_REGEX, FIND_ANY_HBS_REGEX,
FIND_HBS_REGEX,
findDoubleHbsInstances, findDoubleHbsInstances,
isBackendService,
prefixStrings,
} from "./utilities" } from "./utilities"
import { convertHBSBlock } from "./conversion" import { convertHBSBlock } from "./conversion"
import { setJSRunner, removeJSRunner } from "./helpers/javascript" import { removeJSRunner, setJSRunner } from "./helpers/javascript"
import manifest from "./manifest.json" import manifest from "./manifest.json"
import { ProcessOptions } from "./types" import { ProcessOptions } from "./types"
@ -23,6 +24,7 @@ export { iifeWrapper } from "./iife"
const hbsInstance = create() const hbsInstance = create()
registerAll(hbsInstance) registerAll(hbsInstance)
const helperNames = Object.keys(hbsInstance.helpers)
const hbsInstanceNoHelpers = create() const hbsInstanceNoHelpers = create()
registerMinimum(hbsInstanceNoHelpers) registerMinimum(hbsInstanceNoHelpers)
const defaultOpts: ProcessOptions = { const defaultOpts: ProcessOptions = {
@ -45,12 +47,25 @@ function testObject(object: any) {
} }
} }
function findOverlappingHelpers(context?: object) {
if (!context) {
return []
}
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. * Creates a HBS template function for a given string, and optionally caches it.
*/ */
const templateCache: Record<string, TemplateDelegate<any>> = {} const templateCache: Record<string, TemplateDelegate<any>> = {}
function createTemplate(string: string, opts?: ProcessOptions) { function createTemplate(
string: string,
opts?: ProcessOptions,
context?: object
) {
opts = { ...defaultOpts, ...opts } opts = { ...defaultOpts, ...opts }
const helpersEnabled = !opts?.noHelpers
// Finalising adds a helper, can't do this with no helpers // Finalising adds a helper, can't do this with no helpers
const key = `${string}-${JSON.stringify(opts)}` const key = `${string}-${JSON.stringify(opts)}`
@ -60,7 +75,25 @@ function createTemplate(string: string, opts?: ProcessOptions) {
return templateCache[key] return templateCache[key]
} }
string = preprocess(string, opts) const overlappingHelpers = helpersEnabled
? findOverlappingHelpers(context)
: []
string = preprocess(string, {
...opts,
disabledHelpers: overlappingHelpers,
})
if (context && helpersEnabled) {
if (overlappingHelpers.length > 0) {
for (const block of findHBSBlocks(string)) {
string = string.replace(
block,
prefixStrings(block, overlappingHelpers, "./")
)
}
}
}
// Optionally disable built in HBS escaping // Optionally disable built in HBS escaping
if (opts.noEscaping) { if (opts.noEscaping) {
@ -70,6 +103,7 @@ function createTemplate(string: string, opts?: ProcessOptions) {
// This does not throw an error when template can't be fulfilled, // This does not throw an error when template can't be fulfilled,
// have to try correct beforehand // have to try correct beforehand
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
const template = instance.compile(string, { const template = instance.compile(string, {
strict: false, strict: false,
}) })
@ -171,7 +205,8 @@ export function processStringSync(
throw "Cannot process non-string types." throw "Cannot process non-string types."
} }
function process(stringPart: string) { 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 now = Math.floor(Date.now() / 1000) * 1000
const processedString = template({ const processedString = template({
now: new Date(now).toISOString(), now: new Date(now).toISOString(),

View File

@ -29,9 +29,9 @@ export function preprocess(string: string, opts: ProcessOptions) {
processor => processor.name !== preprocessor.PreprocessorNames.FINALISE processor => processor.name !== preprocessor.PreprocessorNames.FINALISE
) )
} }
return process(string, processors, opts) return process(string, processors, opts)
} }
export function postprocess(string: string) { export function postprocess(string: string) {
let processors = postprocessor.processors return process(string, postprocessor.processors)
return process(string, processors)
} }

View File

@ -1,19 +1,21 @@
import { LITERAL_MARKER } from "../helpers/constants" import { LITERAL_MARKER } from "../helpers/constants"
export const PostProcessorNames = { export enum PostProcessorNames {
CONVERT_LITERALS: "convert-literals", CONVERT_LITERALS = "convert-literals",
} }
class Postprocessor { type PostprocessorFn = (statement: string) => string
name: string
private fn: any
constructor(name: string, fn: any) { class Postprocessor {
name: PostProcessorNames
private readonly fn: PostprocessorFn
constructor(name: PostProcessorNames, fn: PostprocessorFn) {
this.name = name this.name = name
this.fn = fn this.fn = fn
} }
process(statement: any) { process(statement: string) {
return this.fn(statement) return this.fn(statement)
} }
} }

View File

@ -1,25 +1,28 @@
import { HelperNames } from "../helpers" import { HelperNames } from "../helpers"
import { swapStrings, isAlphaNumeric } from "../utilities" import { swapStrings, isAlphaNumeric } from "../utilities"
import { ProcessOptions } from "../types"
const FUNCTION_CASES = ["#", "else", "/"] const FUNCTION_CASES = ["#", "else", "/"]
export const PreprocessorNames = { export enum PreprocessorNames {
SWAP_TO_DOT: "swap-to-dot-notation", SWAP_TO_DOT = "swap-to-dot-notation",
FIX_FUNCTIONS: "fix-functions", FIX_FUNCTIONS = "fix-functions",
FINALISE: "finalise", FINALISE = "finalise",
NORMALIZE_SPACES: "normalize-spaces", NORMALIZE_SPACES = "normalize-spaces",
} }
type PreprocessorFn = (statement: string, opts?: ProcessOptions) => string
class Preprocessor { class Preprocessor {
name: string name: string
private fn: any private readonly fn: PreprocessorFn
constructor(name: string, fn: any) { constructor(name: PreprocessorNames, fn: PreprocessorFn) {
this.name = name this.name = name
this.fn = fn this.fn = fn
} }
process(fullString: string, statement: string, opts: Object) { process(fullString: string, statement: string, opts: ProcessOptions) {
const output = this.fn(statement, opts) const output = this.fn(statement, opts)
const idx = fullString.indexOf(statement) const idx = fullString.indexOf(statement)
return swapStrings(fullString, idx, statement.length, output) return swapStrings(fullString, idx, statement.length, output)
@ -56,8 +59,9 @@ export const processors = [
}), }),
new Preprocessor( new Preprocessor(
PreprocessorNames.FINALISE, PreprocessorNames.FINALISE,
(statement: string, opts: { noHelpers: any }) => { (statement: string, opts?: ProcessOptions) => {
const noHelpers = opts && opts.noHelpers const noHelpers = opts?.noHelpers
const helpersEnabled = !noHelpers
let insideStatement = statement.slice(2, statement.length - 2) let insideStatement = statement.slice(2, statement.length - 2)
if (insideStatement.charAt(0) === " ") { if (insideStatement.charAt(0) === " ") {
insideStatement = insideStatement.slice(1) insideStatement = insideStatement.slice(1)
@ -74,7 +78,8 @@ export const processors = [
} }
const testHelper = possibleHelper.trim().toLowerCase() const testHelper = possibleHelper.trim().toLowerCase()
if ( if (
!noHelpers && helpersEnabled &&
!opts?.disabledHelpers?.includes(testHelper) &&
HelperNames().some(option => testHelper === option.toLowerCase()) HelperNames().some(option => testHelper === option.toLowerCase())
) { ) {
insideStatement = `(${insideStatement})` insideStatement = `(${insideStatement})`

View File

@ -5,4 +5,5 @@ export interface ProcessOptions {
noFinalise?: boolean noFinalise?: boolean
escapeNewlines?: boolean escapeNewlines?: boolean
onlyFound?: boolean onlyFound?: boolean
disabledHelpers?: string[]
} }

View File

@ -66,3 +66,16 @@ export const btoa = (plainText: string) => {
export const atob = (base64: string) => { export const atob = (base64: string) => {
return Buffer.from(base64, "base64").toString("utf-8") 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`)
}

View File

@ -483,3 +483,37 @@ describe("uuid", () => {
expect(output).toMatch(UUID_REGEX) 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"])
})
it("should work as expected when no helpers are set", async () => {
const output = await processString(
"{{ sum }}",
{
sum: "a",
},
{ noHelpers: true }
)
expect(output).toEqual("a")
})
})