forked from tools/josh
204 lines
6.3 KiB
TypeScript
204 lines
6.3 KiB
TypeScript
import "./Extensions";
|
|
|
|
|
|
/**
|
|
* The fancy ASCII header that is displayed at the start of a terminal session.
|
|
*/
|
|
const asciiHeader = ` ________ _______ _ _
|
|
| ____\\ \\ / / __ \\ | | | |
|
|
| |__ \\ \\ /\\ / /| | | | ___| | _| | _____ _ __
|
|
| __| \\ \\/ \\/ / | | | |/ _ \\ |/ / |/ / _ \\ '__|
|
|
| | \\ /\\ / | |__| | __/ <| < __/ |
|
|
|_| \\/ \\/ |_____/ \\___|_|\\_\\_|\\_\\___|_| `;
|
|
|
|
/**
|
|
* A responsive version of the header that displays a simple text instead of the ASCII header when the screen is not
|
|
* large enough.
|
|
*/
|
|
export const asciiHeaderHtml =
|
|
`<span class="wideScreenOnly">${asciiHeader}</span><span class="smallScreenOnly"><b><u>FWDekker</u></b></span>`;
|
|
|
|
/**
|
|
* A function that does nothing.
|
|
*/
|
|
export const emptyFunction = () => {};
|
|
|
|
|
|
/**
|
|
* Runs the given function as soon as the page is done loading by "appending" it to the current definition of
|
|
* `window.onload`.
|
|
*
|
|
* @param fun the function to run as soon as the page is done loading
|
|
*/
|
|
export function addOnLoad(fun: () => void): void {
|
|
const oldOnLoad = window.onload ?? emptyFunction;
|
|
|
|
window.onload = () => {
|
|
// @ts-ignore: Call works without parameters as well
|
|
oldOnLoad();
|
|
fun();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Replaces all special HTML characters with escaped variants.
|
|
*
|
|
* @param string the string to escape special HTML characters in
|
|
*/
|
|
export function escapeHtml(string: string): string {
|
|
return string
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* Given an input string, finds the word that ends at the indicated offset, and returns the string before the word, the
|
|
* word itself, and the string after the word.
|
|
*
|
|
* The word is preceded by a whitespace or forward slash, and ends at the indicated offset. Whitespace and forward
|
|
* slashes at the end of the word are ignored.
|
|
*
|
|
* @param input the input string to find the word in
|
|
* @param offset the right-most position of the word
|
|
* @param delimiters the delimiters to consider
|
|
* @return the string before the word, the word itself, and the string after the word
|
|
*/
|
|
export function extractWordBefore(input: string, offset: number, delimiters: string = " /"): [string, string, string] {
|
|
const right = input.slice(offset);
|
|
|
|
const leftPlusWord = input.slice(0, offset);
|
|
const trimmedLeftPlusWord =
|
|
delimiters.split("").reduce((acc, delimiter) => acc.trimRightChar(delimiter), leftPlusWord);
|
|
|
|
const wordStart = Math.max.apply(null, delimiters.split("").map((it) => trimmedLeftPlusWord.lastIndexOf(it)));
|
|
return [leftPlusWord.slice(0, wordStart + 1), leftPlusWord.slice(wordStart + 1, leftPlusWord.length), right];
|
|
}
|
|
|
|
/**
|
|
* Returns the extension of the given filename, or `""` if it doesn't have one.
|
|
*
|
|
* @param filename the filename to return the extension of
|
|
*/
|
|
export function getFileExtension(filename: string): string {
|
|
const extension = /^.+\.([^.]+)$/.exec(filename);
|
|
return extension == null ? "" : extension[1];
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if and only if the website is currently running in a standalone app on the user's phone.
|
|
*/
|
|
export function isStandalone(): boolean {
|
|
return window.matchMedia("(display-mode: standalone)").matches;
|
|
}
|
|
|
|
/**
|
|
* Moves the caret to the given position in the given node.
|
|
*
|
|
* @param node the node to move the caret in; if `null`, nothing happens
|
|
* @param position the position from the left to place the caret at
|
|
*/
|
|
export function moveCaretTo(node: Node | null, position: number): void {
|
|
if (node === null)
|
|
return;
|
|
|
|
const range = document.createRange();
|
|
range.setStart(node, position);
|
|
|
|
const selection = window.getSelection();
|
|
if (selection !== null) {
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves the caret to the end of the given node.
|
|
*
|
|
* @param node the node in which to move the caret to the end
|
|
*/
|
|
export function moveCaretToEndOf(node: Node): void {
|
|
moveCaretTo(node, (node.textContent ?? "").length);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of pixels in a CSS value that describes a number of pixels, or `0` if the given string is `null`
|
|
* or does not contain a number.
|
|
*
|
|
* For example, if the given string is `"3px"`, this function will return `3`.
|
|
*
|
|
* @param string the CSS value to extract the number of pixels from
|
|
* @throws if the given string does not end with the text `"px"`
|
|
*/
|
|
export function parseCssPixels(string: string | null): number {
|
|
if (string === null || string.trim() === "") {
|
|
return 0;
|
|
} else {
|
|
if (!string.endsWith("px"))
|
|
throw new IllegalArgumentError("CSS string is not expressed in pixels.");
|
|
|
|
const result = parseFloat(string);
|
|
return isNaN(result) ? 0 : result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Type-safe shorthand for `document.querySelector(query)`.
|
|
*
|
|
* @param query the query to run
|
|
* @throws if the element could not be found
|
|
*/
|
|
export function q(query: string): HTMLElement {
|
|
const element = document.querySelector(query);
|
|
if (!(element instanceof HTMLElement))
|
|
throw `Could not find element \`${query}\`.`;
|
|
|
|
return element;
|
|
}
|
|
|
|
|
|
/**
|
|
* Indicates that the application will exit under normal circumstances.
|
|
*
|
|
* That is, this is not actually an error. This "error" is thrown when the normal flow of execution should be
|
|
* interrupted right away so that the application can exit.
|
|
*/
|
|
export class ExpectedGoodbyeError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates that an argument is given to a function that should not have been given.
|
|
*
|
|
* The user should not be able to reach this state because user input should have been sanitized.
|
|
*/
|
|
export class IllegalArgumentError extends Error {
|
|
/**
|
|
* Constructs a new illegal argument error.
|
|
*
|
|
* @param message a message explaining why the error was thrown
|
|
*/
|
|
constructor(message: string) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates that the program has ended up in a state that it should never end up in.
|
|
*
|
|
* Indicates an error that could not have been caused by the user.
|
|
*/
|
|
export class IllegalStateError extends Error {
|
|
/**
|
|
* Constructs a new illegal state error.
|
|
*
|
|
* @param message a message explaining why the error was thrown
|
|
*/
|
|
constructor(message: string) {
|
|
super(message);
|
|
}
|
|
}
|