blob: 458c61fff34090fd49d193df1939958a81ce9069 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Marked from '../../third_party/marked/marked.js';
export interface PlaceholderToken extends Marked.Marked.Tokens.Generic {
type: 'placeholder';
raw: string;
key: string;
}
export const VALID_PLACEHOLDER_MATCH_PATTERN: RegExp = /\{(PLACEHOLDER_[a-zA-Z][a-zA-Z0-9_]*)\}/g;
const VALID_PLACEHOLDER_NAME_PATTERN = /^PLACEHOLDER_[a-zA-Z][a-zA-Z0-9_]*$/;
const placeholderExtension: Marked.Marked.TokenizerExtension = {
name: 'placeholder',
level: 'inline',
start(src: string): number |
undefined {
return src.match(/\{PLACEHOLDER_[a-zA-Z][a-zA-Z0-9_]*\}/)?.index;
},
tokenizer(src: string): PlaceholderToken |
undefined {
const match = /^\{(PLACEHOLDER_[a-zA-Z][a-zA-Z0-9_]*)\}/.exec(src);
if (match) {
return {
type: 'placeholder',
raw: match[0],
key: match[1],
};
}
return undefined;
},
};
export function validateSubstitutions(rawMarkdown: string, substitutions?: Map<string, string>): void {
if (!substitutions) {
return;
}
const unusedPlaceholders = new Set(substitutions.keys());
for (const key of unusedPlaceholders) {
if (!VALID_PLACEHOLDER_NAME_PATTERN.test(key)) {
throw new Error(`Invalid placeholder '${key}' provided in the substitutions map.`);
}
}
for (const [, placeholder] of rawMarkdown.matchAll(VALID_PLACEHOLDER_MATCH_PATTERN)) {
if (!substitutions.has(placeholder)) {
throw new Error(`No replacement provided for placeholder '${placeholder}'.`);
}
unusedPlaceholders.delete(placeholder);
}
if (unusedPlaceholders.size > 0) {
throw new Error(`Unused replacements provided: ${[...unusedPlaceholders]}`);
}
}
/**
* Tokenizes markdown into a Marked AST while preserving `{PLACEHOLDER_*}` tokens
* as `PlaceholderToken` AST nodes instead of substituting strings before lexing.
*/
export function tokenizeWithPlaceholders(markdown: string|Marked.Marked.Token[],
substitutions?: Map<string, string>): Marked.Marked.Token[] {
const rawMarkdown = typeof markdown === 'string' ? markdown : markdown.map(token => token.raw).join('');
validateSubstitutions(rawMarkdown, substitutions);
if (typeof markdown !== 'string' && !rawMarkdown.includes('{PLACEHOLDER_')) {
return markdown;
}
const markedInstance = new Marked.Marked.Marked({
extensions: [placeholderExtension],
});
return markedInstance.lexer(rawMarkdown);
}