import { LanguageSupport, LRLanguage } from "@codemirror/language";
import { styleTags, tags as t } from "@lezer/highlight";
import Mustache from "mustache";
import { parser } from "./mustacheLikeTemplating.syntax.grammar";
/**
* Defines the language for the Mustache-like templating system
*
* @see https://codemirror.net/examples/lang-package/
*
* @example
* ```
* {{question}}
*
* {
* "answer": {{answer}}
* }
* ```
* In this example, the variables are `question` and `answer`.
* Single braces are not considered as variables.
* Double braces will be interpolated with variable values on format.
*/
export const MustacheLikeTemplatingLanguage = LRLanguage.define({
parser: parser.configure({
props: [
// https://lezer.codemirror.net/docs/ref/#highlight.styleTags
styleTags({
// style the opening brace ({{) of a template, not floating braces
"Template/LBrace": t.quote,
// style the closing brace (}}) of a template, not floating braces
"Template/RBrace": t.quote,
// style variables (stuff inside {{}})
"Template/Variable": t.variableName,
// style invalid stuff, undefined tokens will be highlighted
"Template/⚠": t.invalid,
}),
],
}),
languageData: {},
});
/**
* Generates a string representation of the parse tree of the given text
*
* Useful for debugging the parser
*/
export const debugParser = (text: string) => {
const tree = MustacheLikeTemplatingLanguage.parser.parse(text);
return tree.toString();
};
/**
* Formats a Mustache-like template with the given variables.
*/
export const formatMustacheLike = ({
text,
variables,
}: {
text: string;
variables: Record<string, string | number | boolean | undefined>;
}) => {
if (!text) {
return "";
}
try {
return Mustache.render(text, variables, undefined, {
escape: (value: string) => value,
});
} catch {
return text;
}
};
/**
* Extract the root variable name from a dotted path.
*
* Mustache uses dot notation to traverse nested properties (e.g., output.available_tools
* means context["output"]["available_tools"]). For validation purposes, we only need
* to check that the root variable exists.
*
* @example
* getRootVariableName("output.available_tools") // => "output"
* getRootVariableName("user.name") // => "user"
* getRootVariableName("simple") // => "simple"
*/
const getRootVariableName = (variablePath: string): string => {
if (variablePath === ".") {
return variablePath;
}
return variablePath.split(".")[0];
};
/**
* Mustache token types that represent variables we want to extract.
* - "#" and "^": section/inverted section tags
* - "name": escaped variable {{name}}
* - "&" and "{": unescaped variables {{& name}} and {{{name}}}
*/
const VARIABLE_TOKEN_TYPES = new Set(["#", "^", "name", "&", "{"]);
/**
* Extracts the variables from a Mustache-like template.
*
* For dotted paths like {{output.available_tools}}, only the root variable
* name (output) is extracted, since Mustache traverses nested properties
* starting from the root.
*
* Mustache.parse() returns a flat array of top-level tokens. Nested tokens
* (inside sections) are encapsulated in the children array of section tokens,
* so we only need to iterate the top-level array to extract top-level variables.
*/
export const extractVariablesFromMustacheLike = (text: string): string[] => {
let tokens: ReturnType<typeof Mustache.parse>;
try {
tokens = Mustache.parse(text);
} catch {
return [];
}
const variables = new Set<string>();
for (const [type, value] of tokens) {
if (typeof value !== "string") continue;
if (VARIABLE_TOKEN_TYPES.has(type)) {
const root = getRootVariableName(value.trim());
if (root) variables.add(root);
}
}
return Array.from(variables);
};
export type MustacheSectionValidation = {
errors: string[];
warnings: string[];
};
/**
* Validates Mustache section tags for proper nesting and closure.
*
* Uses the native Mustache.js parser for spec-compliant validation.
* Parser exceptions are surfaced directly as error messages.
*/
export const validateMustacheSections = (
text: string
): MustacheSectionValidation => {
try {
Mustache.parse(text);
return { errors: [], warnings: [] };
} catch (parseError) {
const message =
parseError instanceof Error ? parseError.message : "Invalid template";
return {
errors: [message],
warnings: [],
};
}
};
/**
* Creates a CodeMirror extension for the FString templating system
*/
export function MustacheLikeTemplating() {
return new LanguageSupport(MustacheLikeTemplatingLanguage);
}