prototype-3x5/src/words.ts

114 lines
3.3 KiB
TypeScript
Raw Normal View History

2023-08-26 05:16:22 +00:00
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;
};
2023-08-26 05:16:22 +00:00
export type TextPiece = EnchantedWord | TextWord | HtmlWord;
2023-08-23 05:09:56 +00:00
export type VariablePiece = { variable: string };
2023-08-25 23:10:45 +00:00
export type ScriptPiece = { script: Script };
export type InterpolatedPiece = TextPiece | VariablePiece | ScriptPiece;
2023-08-23 05:09:56 +00:00
/**
* 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 = {
2023-08-23 05:09:56 +00:00
pieces: InterpolatedPiece[];
};
2023-08-26 05:16:22 +00:00
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 "";
}
}
2023-08-23 05:09:56 +00:00
// safely concatenate text pieces, converting as needed
2023-08-26 05:16:22 +00:00
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) };
}
2023-08-23 05:09:56 +00:00
}
2023-08-25 15:13:00 +00:00
function IsTextPiece(piece: InterpolatedPiece | undefined): piece is TextPiece {
2023-08-26 05:16:22 +00:00
return piece
? "text" in piece || "enchanted" in piece || "html" in piece
: false;
2023-08-25 15:13:00 +00:00
}
2023-08-23 05:09:56 +00:00
export function SimplifyWord(
pieces: InterpolatedPiece[]
2023-08-26 05:16:22 +00:00
): InterpolatedWord | EnchantedWord | TextWord | HtmlWord {
2023-08-23 05:09:56 +00:00
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[];