initial commit
This commit is contained in:
7 files changed
+489
No files matched your search
+99
@@ -0,0 +1,99 @@
|
||||
/// <reference lib="dom" />
|
||||
import { templateNoop, HTML_ESCAPES, xss } from "./helpers.ts"
|
||||
|
||||
export { HTML_ESCAPES }
|
||||
|
||||
/**
|
||||
* Parses HTML, kinda smart.
|
||||
*
|
||||
* As a function, it just parses the string as HTML. When used as a tagged template, expressions are XSS escaped using {@link xss}, except Nodes, which are added to the tree.
|
||||
*
|
||||
* ```js
|
||||
* html("<p>Hello world!</p>")
|
||||
* // DocumentFragment
|
||||
* // └─ p
|
||||
* // └─ "Hello world!"
|
||||
* html`<strong>
|
||||
* ${html`<em>Look ma, nested HTML!</em>`}
|
||||
* ${`<img src="/bogus/path" onerror="alert('xss')">`}
|
||||
* </strong>`
|
||||
* // DocumentFragment
|
||||
* // └─ strong
|
||||
* // ├─ em
|
||||
* // │ └─ "Look ma, nested HTML!"
|
||||
* // └─ "<img src="/bogus/path" onerror="alert('xss')">"
|
||||
* ```
|
||||
* @author MindfulMinun
|
||||
* @since 2020-06-23
|
||||
*/
|
||||
export function html(content: string): DocumentFragment
|
||||
export function html(strings: TemplateStringsArray, ...exprs: any[]): DocumentFragment
|
||||
export function html(strings: TemplateStringsArray | string, ...exprs: any[]): DocumentFragment {
|
||||
const temp = document.createElement("template")
|
||||
let out = ""
|
||||
|
||||
if (typeof strings === "string") {
|
||||
temp.innerHTML = strings
|
||||
return temp.content.cloneNode(true) as DocumentFragment
|
||||
}
|
||||
|
||||
// Join the strings together
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
// If it's another Node, put a template in its place so we can replace it later
|
||||
if (exprs[i] instanceof Node) {
|
||||
out += strings[i] + `<template data-replaceindex="${i}"></template>`
|
||||
continue
|
||||
}
|
||||
// Otherwise, just join the expressions together, escaping dynamic content
|
||||
out += strings[i] + (exprs[i] != null ? xss("" + exprs[i]) : "")
|
||||
}
|
||||
temp.innerHTML = out
|
||||
const clone = temp.content.cloneNode(true) as DocumentFragment
|
||||
|
||||
// Replace all templates with the real nodes
|
||||
clone.querySelectorAll("template[data-replaceindex]").forEach(node => {
|
||||
if (!(node instanceof HTMLElement)) return
|
||||
if (!node.parentNode) return
|
||||
const index = parseInt(node.getAttribute("data-replaceindex") || "", 10)
|
||||
node.parentNode.replaceChild(exprs[index], node);
|
||||
})
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a text node for use in the DOM. The text content is NOT escaped.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-06-04
|
||||
*/
|
||||
export function textNode(templ: string): Text
|
||||
export function textNode(templ: TemplateStringsArray, ...values: any[]): Text
|
||||
export function textNode(templ: string | TemplateStringsArray, ...values: any[]): Text {
|
||||
const string = templateNoop(templ, ...values)
|
||||
return document.createTextNode(string)
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the DOM recursively, calling the callback for each node.
|
||||
* @deprecated
|
||||
* @param {Node} root The root of the DOM walk
|
||||
* @param {number} [filters] A bitmask of what nodes to show. Defaults to `NodeFilter.SHOW_ELEMENT`.
|
||||
* @param {(node: Node, root: Node, walker: TreeWalker) => boolean} callback If this callback returns true, stop tree traversal.
|
||||
* @author MindfulMinun
|
||||
* @since 2020-06-29
|
||||
*/
|
||||
export function domWalker(
|
||||
root: Node,
|
||||
callback: (node: Node, root: Node, walker: TreeWalker) => boolean,
|
||||
filters: number = NodeFilter.SHOW_ELEMENT
|
||||
) {
|
||||
const walker = document.createTreeWalker(root, filters);
|
||||
let guard = false;
|
||||
let current: Node | null = walker.currentNode
|
||||
while (true) {
|
||||
if (!current) break
|
||||
if (guard) break
|
||||
guard = callback.call(null, current, root, walker);
|
||||
current = walker.nextNode()
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* HTML escape sequences for unsafe characters
|
||||
*/
|
||||
export const HTML_ESCAPES = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"/": "/"
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes indentation from multiline strings.
|
||||
* @since 2021-08-07
|
||||
*/
|
||||
export function dedent(
|
||||
templ: TemplateStringsArray | string,
|
||||
...values: unknown[]
|
||||
): string {
|
||||
let strings = Array.from(typeof templ === 'string' ? [templ] : templ)
|
||||
|
||||
// 1. Remove trailing whitespace.
|
||||
strings[strings.length - 1] = strings[strings.length - 1].trimRight()
|
||||
|
||||
// 2. Find all line breaks to determine the highest common indentation level.
|
||||
const indentLengths = strings.reduce((arr, str) => {
|
||||
const matches = str.match(/\n([\t ]+|(?!\s).)/g)
|
||||
if (matches) {
|
||||
return arr.concat(
|
||||
matches.map((match) => match.match(/[\t ]/g)?.length ?? 0)
|
||||
)
|
||||
}
|
||||
return arr
|
||||
}, <number[]>[])
|
||||
|
||||
// 3. Remove the common indentation from all strings.
|
||||
if (indentLengths.length) {
|
||||
const pattern = new RegExp(`\n[\t ]{${Math.min(...indentLengths)}}`, 'g')
|
||||
|
||||
strings = strings.map((str) => str.replace(pattern, '\n'))
|
||||
}
|
||||
|
||||
// 4. Remove leading whitespace.
|
||||
strings[0] = strings[0].trimStart()
|
||||
|
||||
// 5. Perform interpolation.
|
||||
let string = strings[0]
|
||||
|
||||
values.forEach((value, i) => {
|
||||
// 5.1 Read current indentation level
|
||||
const endentations = string.match(/(?:^|\n)( *)$/)
|
||||
const endentation = endentations ? endentations[1] : ''
|
||||
let indentedValue = value
|
||||
// 5.2 Add indentation to values with multiline strings
|
||||
if (typeof value === 'string' && value.includes('\n')) {
|
||||
indentedValue = String(value)
|
||||
.split('\n')
|
||||
.map((str, i) => i === 0 ? str : `${endentation}${str}`)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
string += indentedValue + strings[i + 1]
|
||||
})
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* A no-operation function for tagged template string literals
|
||||
* @author MindfulMinun
|
||||
* @since 2022-05-20
|
||||
*/
|
||||
export function templateNoop(
|
||||
templ: TemplateStringsArray | string,
|
||||
...values: unknown[]
|
||||
): string {
|
||||
let strings = Array.from(typeof templ === 'string' ? [templ] : templ)
|
||||
return strings.reduce((result, currentString, i) =>
|
||||
result + currentString + (values[i] != null ? values[i] : ''),
|
||||
'' // Start with the empty string
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise that resolves after timeout
|
||||
*
|
||||
* ```js
|
||||
* // Play hide and seek, count to 100 (seconds)
|
||||
* wait(100e3).then(() => "Ready or not, here I come!").then(seek)
|
||||
* ```
|
||||
* @since 2020-06-23
|
||||
*/
|
||||
export function wait(timeout: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, timeout))
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes unsafe strings for use in HTML
|
||||
*
|
||||
* ```js
|
||||
* xss(`<img src="/bogus/path" onerror="alert('xss')">`)
|
||||
* ```
|
||||
* @author MindfulMinun
|
||||
* @since 2020-06-23
|
||||
*/
|
||||
export function xss(unsafe: string): string {
|
||||
return unsafe.replace(/[&<>"'\/]/g, (key) => HTML_ESCAPES[key as keyof typeof HTML_ESCAPES])
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Represents a tuple, or a homogenous array of a fixed length.
|
||||
*
|
||||
* const deck: Tuple<Card, 52> = [Card, Card, Card, ...];
|
||||
*/
|
||||
export type Tuple<
|
||||
T,
|
||||
N extends number,
|
||||
R extends readonly T[] = [],
|
||||
> = R['length'] extends N ? R : Tuple<T, N, readonly [T, ...R]>;
|
||||
|
||||
/**
|
||||
* Chooses a random element from an array
|
||||
*
|
||||
* choose(document.all).click()
|
||||
* @since 2020-06-29
|
||||
*/
|
||||
export function choose<T>(arr: T[]): T {
|
||||
if (!arr.length) throw new Error('Cannot choose from an empty array')
|
||||
return arr[Math.floor(Math.random() * arr.length)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an iterable that always yields the given value.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-04-21
|
||||
*/
|
||||
export function* fill<T>(value = 0 as unknown as T, length = Infinity): Generator<T, void, unknown> {
|
||||
for (let i = 0; i < length; i++)
|
||||
yield value
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an iterable that yields numbers between a range.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-04-27
|
||||
*/
|
||||
export function range(end: number): Generator<number, void, unknown>
|
||||
export function range(start: number, end: number): Generator<number, void, unknown>
|
||||
export function range(start: number, end: number, step: number): Generator<number, void, unknown>
|
||||
export function* range(a: number, b?: number, c?: number) {
|
||||
const [start, end, step] = (
|
||||
typeof b === "undefined" ? [0, a, 1] :
|
||||
typeof c === "undefined" ? [a, b, 1] : [a, b, Math.abs(c)]
|
||||
)
|
||||
const delta = end < start ? -step : step
|
||||
for (let i = start; i < end; i += delta)
|
||||
yield i
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes elements from an array. Non-destructive, as all array operations should be :D
|
||||
*
|
||||
* // Removes 'b', at index 1
|
||||
* removeFromArraybyIndexes(['a', 'b', 'c'], 1) // ['a', 'c']
|
||||
* // Removes 'b' and 'd', at indexes 1 and 3
|
||||
* removeFromArraybyIndexes(['a', 'b', 'c', 'd'], [1, 3]) // ['a', 'c']
|
||||
* @since 2020-07-12
|
||||
*/
|
||||
export function removeFromArraybyIndexes<T>(
|
||||
arr: T[],
|
||||
indexes: number | number[]
|
||||
): T[] {
|
||||
if (Array.isArray(indexes)) {
|
||||
return arr.filter((_, i) => indexes.indexOf(i) === -1)
|
||||
} else {
|
||||
return arr.filter((_, i) => i !== indexes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffles an array. Note that this function swaps the elements
|
||||
* *in place*, meaning the original array is modified.
|
||||
* @since 2020-07-19
|
||||
*/
|
||||
export function shuffle<T>(arr: T[]): T[] {
|
||||
let j, i
|
||||
for (i = arr.length - 1; i > 0; i--) {
|
||||
j = Math.floor(Math.random() * (i + 1))
|
||||
swap(arr, i, j)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps two elements of an array. Note that this function swaps
|
||||
* the elements *in place*, meaning the original array is modified.
|
||||
* @since 2020-07-19
|
||||
*/
|
||||
export function swap<T>(arr: T[], i: number, j: number): void {
|
||||
let carry = arr[i]
|
||||
arr[i] = arr[j]
|
||||
arr[j] = carry
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to handle events and turn them into async iterables
|
||||
*
|
||||
* ```ts
|
||||
* const stream = new EventStream<MouseEvent>()
|
||||
*
|
||||
* document.body.addEventListener('click', ev => stream.emit(ev))
|
||||
* wait(10e3).then(() => stream.end())
|
||||
*
|
||||
* for await (const event of stream) {
|
||||
* alert("Clicked!")
|
||||
* }
|
||||
* ```
|
||||
* @since 2021-08-07
|
||||
*/
|
||||
export class EventStream<T> {
|
||||
#done: boolean
|
||||
#events: T[]
|
||||
#resolve: () => void
|
||||
#promise!: Promise<void>
|
||||
|
||||
constructor() {
|
||||
this.#done = false
|
||||
this.#events = []
|
||||
this.#resolve = () => { }
|
||||
this.#defer()
|
||||
}
|
||||
|
||||
#defer() {
|
||||
this.#promise = new Promise(r => this.#resolve = r)
|
||||
}
|
||||
|
||||
async*[Symbol.asyncIterator]() {
|
||||
while (!this.#done) {
|
||||
await this.#promise
|
||||
yield* this.#events
|
||||
this.#events = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event. For-await-of listeners of this class instance will recieve this event.
|
||||
* Note that once an event is emitted, it cannot be cancelled.
|
||||
* @since 2021-08-07
|
||||
*/
|
||||
emit(event: T) {
|
||||
this.#events.push(event)
|
||||
this.#resolve()
|
||||
this.#defer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a specific event to be emitted according to a predicate.
|
||||
*
|
||||
* @example
|
||||
* const stream = new EventStream<MouseEvent>()
|
||||
* // Some events happen...
|
||||
* const click = stream.once(ev => ev.type === 'click')
|
||||
* @since 2021-12-22
|
||||
*/
|
||||
once(predicate: (value: T) => boolean) {
|
||||
return once(this, predicate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the iterator. This terminates for-await-of loops listening for events,
|
||||
* and any newly dispatched events will not be sent to the iterator.
|
||||
* @since 2021-08-07
|
||||
*/
|
||||
end() {
|
||||
this.#done = true
|
||||
this.#resolve()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any async iterable to yield a specific value according to a predicate.
|
||||
*
|
||||
* @example
|
||||
* const stream = new EventStream<MouseEvent>()
|
||||
* // Some events happen...
|
||||
* const click = once(stream, ev => ev.type === 'click')
|
||||
* @since 2021-12-22
|
||||
*/
|
||||
export async function once<T>(source: AsyncIterable<T>, predicate: (value: T) => boolean) {
|
||||
for await (const value of source) {
|
||||
if (predicate(value)) return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A "destructured" Promise, useful for waiting for callbacks.
|
||||
* @example
|
||||
* // When awaited, this function returns a promise that resolves to
|
||||
* // an *OPEN* WebSocket
|
||||
* () => {
|
||||
* const sock = new WebSocket()
|
||||
* const [p, res, rej] = pinkyPromise()
|
||||
* sock.onerror = err => rej(err)
|
||||
* sock.onopen = () => res(sock)
|
||||
* return p
|
||||
* }
|
||||
*
|
||||
* @author MindfulMinun
|
||||
* @since 2022-06-05
|
||||
*/
|
||||
export function pinkyPromise<T>(): [Promise<T>, (value: T) => void, (error: any) => unknown] {
|
||||
let resolve: (value: T) => void
|
||||
let reject: (error: unknown) => void
|
||||
|
||||
// This works because callbacks to the Promise constructor are called synchronously.
|
||||
const p = new Promise<T>((yay, nay) => {
|
||||
resolve = yay
|
||||
reject = nay
|
||||
})
|
||||
|
||||
return [p, resolve!, reject!]
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Code I keep on rewriting for some reason
|
||||
* @author MindfulMinun <https://benjic.xyz>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
console.error("Hey there, past me! Please don't import <https://mirai.benjic.xyz/lib/mod.ts>, instead import specific files please!")
|
||||
|
||||
// export * from "./array.ts"
|
||||
// export * from "./helpers.ts"
|
||||
// export * from "./math.ts"
|
||||
// export * from "./dom.ts"
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Run a shell command that can be aborted using an `AbortSignal`
|
||||
* @since 2021-08-13
|
||||
*/
|
||||
export async function abortableExec(opts: { cmd: string[], signal?: AbortSignal }): Promise<void> {
|
||||
const { cmd, signal } = opts
|
||||
if (signal && signal.aborted) return
|
||||
const p = Deno.run({
|
||||
cmd,
|
||||
stdout: "null"
|
||||
})
|
||||
signal && signal.addEventListener('abort', () => p.kill("SIGINT"))
|
||||
await p.status()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
choose, fill, range, removeFromArraybyIndexes, shuffle, swap, once
|
||||
} from "../iterable.ts"
|
||||
|
||||
import { assertThrows, assertEquals, assert } from "https://deno.land/[email protected]/testing/asserts.ts"
|
||||
|
||||
Deno.test("choose: throw on empty array", () => {
|
||||
assertThrows(() => choose([]))
|
||||
})
|
||||
|
||||
Deno.test("fill, yield the same value every time", () => {
|
||||
const count = 50_000
|
||||
const item = "hi"
|
||||
let i = 0
|
||||
for (const el of fill(item, count)) {
|
||||
assertEquals(el, item)
|
||||
i++
|
||||
}
|
||||
assertEquals(i, count)
|
||||
})
|
||||
|
||||
|
||||
Deno.test("range: one arg", () => {
|
||||
const iter = range(50_000)
|
||||
for (let i = 0; i < 50_000; i++) {
|
||||
const next = iter.next()
|
||||
assertEquals(next.value, i)
|
||||
}
|
||||
assert(iter.next().done)
|
||||
})
|
||||
|
||||
|
||||
Deno.test("range: two args", () => {
|
||||
const iter = range(50_000)
|
||||
for (let i = 0; i < 50_000; i++) {
|
||||
const next = iter.next()
|
||||
assertEquals(next.value, i)
|
||||
}
|
||||
assert(iter.next().done)
|
||||
})
|
||||
Reference in new issue
Block a user