Skip to content
English

单次模板替换与语义槽 ​

策略的具体实现如下。

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}`)
      );
    },
  });
}

接口参考、使用示例与开发指南。