单次模板替换与语义槽
策略的具体实现如下。
ts
export function defineTextTemplate(master: string, options: TextTemplateOptions = {}) {
const slots = Object.freeze([
...new Set([...master.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1])),
]);
const errors = options.errors,
missingField = errors?.missingField?.bind(errors),
observe = options.observe?.bind(options);
return Object.freeze({
slots,
render(values: Readonly<Record<string, string>>): string {
const result = master.replace(/\{\{(\w+)\}\}/g, (_token, key: string) => {
breakWhile(
!Object.hasOwn(values, key),
() => missingField?.(key) ?? new Error(`Missing template slot: ${key}`),
);
return values[key];
});
observeSafely(observe, result);
return result;
},
});
}
export function defineSemanticSlots(definition: SemanticSlotDefinition) {
const { namespace, maxOccurrences } = definition;
breakWhile([
{ occur: () => !/^[a-z][a-z0-9-]*$/.test(namespace), error: 'Invalid slot namespace' },
{
occur: () => !Number.isSafeInteger(maxOccurrences) || maxOccurrences < 0,
error: 'Invalid slot limit',
},
]);
const keys = new Set(definition.keys);
const pattern = new RegExp(`\\{\\{${namespace}:([^{}]*)\\}\\}`, 'g');
return Object.freeze({
namespace,
isValid(text: string): boolean {
const matches = [...text.matchAll(pattern)];
return (
matches.length <= maxOccurrences &&
matches.every((match) => keys.has(match[1])) &&
!text.replace(pattern, '').includes(`{{${namespace}`)
);
},
});
}