import { AbortSubstitutionException, PlaceholderException } from "./exceptions.js"; import { PlaceholderPattern } from "./pattern.js"; import { EMPTY as emptyResolver } from "./resolver/index.js"; import { VariableResolver } from "./resolver/types.js"; import { StringSection } from "./string-section.js"; import { UnknownVariableHandler } from "./unknown-variable-handler/types.js"; import { isDefined, Nullable } from "./utils.js"; export type PlaceholderOptions = { pattern?: PlaceholderPattern; escapeChar?: string; recursive?: boolean; resolver?: VariableResolver; unknownVariableHandler?: Nullable; }; const DEFAULT_OPTIONS: Required = { pattern: new PlaceholderPattern("${", "}"), escapeChar: '\\', recursive: false, resolver: emptyResolver, unknownVariableHandler: null }; export class PlaceholderSubstitutor { readonly #pattern: PlaceholderPattern; readonly #escapeChar: string; readonly #recursive: boolean; readonly #resolver: VariableResolver; readonly #unknownVariableHandler: Nullable; public constructor(options: PlaceholderOptions = DEFAULT_OPTIONS) { this.#pattern = options.pattern ?? DEFAULT_OPTIONS.pattern; this.#escapeChar = options.escapeChar ?? DEFAULT_OPTIONS.escapeChar; this.#recursive = options.recursive ?? DEFAULT_OPTIONS.recursive; this.#resolver = options.resolver ?? DEFAULT_OPTIONS.resolver; this.#unknownVariableHandler = options.unknownVariableHandler ?? DEFAULT_OPTIONS.unknownVariableHandler; } public get pattern() { return this.#pattern; } public get escapeChar() { return this.#escapeChar; } public get recursive() { return this.#recursive; } public get resolver() { return this.#resolver; } public get unknownVariableHandler() { return this.#unknownVariableHandler; } public replace(s: string): string; public replace(s: Nullable): Nullable; public replace(s: Nullable) { return s ? new Substitutor(this, new StringSection(s), false).replace() : s; } } class Substitutor { readonly #substitutor: PlaceholderSubstitutor; readonly #input: StringSection; readonly #inner: boolean; #pos = 0; constructor(substitutor: PlaceholderSubstitutor, input: StringSection, inner: boolean) { this.#substitutor = substitutor; this.#input = input; this.#inner = inner; } replace(): string { const result: (string | StringSection)[] = []; let escaping = false; let hasSuffix = false; let sectionOffset = -1; let sectionLength = 0; while (this.#pos < this.#input.length) { const c = this.#input.charAt(this.#pos); if (escaping) { result.push(c); escaping = false; } else if (c === this.#substitutor.escapeChar) { escaping = true; if (sectionOffset >= 0) { result.push(this.#input.subSection(sectionOffset, sectionLength)); sectionOffset = -1; sectionLength = 0; } } else if (this.tryAdvance(this.#substitutor.pattern.prefix)) { if (sectionOffset >= 0) { result.push(this.#input.subSection(sectionOffset, sectionLength)); sectionOffset = -1; sectionLength = 0; } const innerSubstitutor = new Substitutor(this.#substitutor, this.#input.subSection(this.#pos), true); const variable = innerSubstitutor.replace(); if (variable.length === 0) { throw new PlaceholderException(this.#input.getSection(), this.#pos, "Empty variable"); } const value = this.resolveVariable(variable); if (!isDefined(value)) { result.push(this.#substitutor.pattern.prefix, variable, this.#substitutor.pattern.suffix); } else if (this.#substitutor.recursive) { result.push(this.#substitutor.replace(value)); } else { result.push(value); } this.#pos += innerSubstitutor.#pos; continue; } else if (this.#inner && this.tryAdvance(this.#substitutor.pattern.suffix)) { hasSuffix = true; break; } else { if (sectionOffset < 0) { sectionOffset = this.#pos; } sectionLength++; } this.#pos++; } if (sectionOffset >= 0) { result.push(this.#input.subSection(sectionOffset, sectionLength)); } if (this.#inner && !hasSuffix) { throw new PlaceholderException(this.#input.getSection(), this.#pos, "Missing variable end delimiter"); } return result.join(""); } private continuesWith(sequence: string) { return this.#input.startsWith(sequence, this.#pos); } private tryAdvance(sequence: string) { const b = this.continuesWith(sequence); if (b) { this.#pos += sequence.length; } return b; } private resolveVariable(name: string) { const value = this.#substitutor.resolver(name); if (isDefined(value)) { return typeof value === "string" ? value : value(); } if (this.#substitutor.unknownVariableHandler) { try { return this.#substitutor.unknownVariableHandler(this.#substitutor, name); } catch (e: any) { if (e instanceof AbortSubstitutionException) { throw new PlaceholderException(this.#input.getSection(), this.#pos, "Substitution aborted due to unknown variable", e); } throw new PlaceholderException(this.#input.getSection(), this.#pos, "An error occurred while handling unknown variable", e); } } return null; } }