import { escapeHtml } from "./helpers"; /** * A word whose value is text with provenance- this literal value appeared in the source * code, and was not the result of any backslash, variable, or command substitutions. * * This provides a level of intentionality that commands can use to distinguish switches: * * ```tcl * puts -stderr text ;# -stderr can be interpreted as a flag and is not part of the message to print * puts "-stderr" text ;# -stderr is not a flag, but is part of the message to print * puts [return -stderr] text ;# -stderr is not a flag, but is part of the message to print * puts $var text ;# The value of $var is part of the message to print, even if the value happens to be "-stderr" * ``` */ export type EnchantedWord = { enchanted: string; }; /** * A word whose value is plain text, with no special provenance. */ export type TextWord = { text: string; }; /** * A word whose value is "safe" HTML using allowlisted elements/attributes/styles, * suitable for inclusion in HTML output with no escaping. */ export type HtmlWord = { html: string; }; export type TextPiece = EnchantedWord | TextWord | HtmlWord; export type VariablePiece = { variable: string }; export type ScriptPiece = { script: Script }; export type InterpolatedPiece = TextPiece | VariablePiece | ScriptPiece; /** * A word whose value needs to be determined by evaluating some combination of variable and command * substitutions, and concatenating the results with any literal spans. */ export type InterpolatedWord = { pieces: InterpolatedPiece[]; }; export function AsText(word: TextPiece): string { if ("enchanted" in word) { return word.enchanted; } else if ("text" in word) { return word.text; } else if ("html" in word) { return word.html; } else { return ""; } } export function AsHtml(word: TextPiece): string { if ("enchanted" in word) { return escapeHtml(word.enchanted); } else if ("text" in word) { return escapeHtml(word.text); } else if ("html" in word) { return word.html; } else { return ""; } } // safely concatenate text pieces, converting as needed export function Concat(left: TextPiece | null, right: TextPiece) { if (left === null) { return "enchanted" in right ? { text: right.enchanted } : right; } if ("html" in left || "html" in right) { return { html: AsHtml(left) + AsHtml(right) }; } else { return { text: AsText(left) + AsText(right) }; } } function IsTextPiece(piece: InterpolatedPiece | undefined): piece is TextPiece { return piece ? "text" in piece || "enchanted" in piece || "html" in piece : false; } export function SimplifyWord( pieces: InterpolatedPiece[] ): InterpolatedWord | EnchantedWord | TextWord | HtmlWord { const consolidated: InterpolatedPiece[] = []; for (const piece of pieces) { const top = consolidated[consolidated.length - 1]; if (IsTextPiece(top) && IsTextPiece(piece)) { consolidated[consolidated.length - 1] = Concat(top, piece); } else { consolidated.push(piece); } } if (consolidated.length == 0) { return { text: "" }; } else if (consolidated.length == 1 && IsTextPiece(consolidated[0])) { return consolidated[0]; } else { return { pieces: consolidated }; } } export type Word = EnchantedWord | TextWord | HtmlWord | InterpolatedWord; export type Command = Word[]; export type Script = Command[];