initial commit

This commit is contained in:
MindfulMinun committed 2022-06-26 11:34:26 -05:00
commit 9d7f83f838
7 files changed
+489

No files matched your search

+99
View File
@@ -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
View File
@@ -0,0 +1,109 @@
/**
* HTML escape sequences for unsafe characters
*/
export const HTML_ESCAPES = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"/": "&#x2F;"
}
/**
* 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
View File
@@ -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
View File
@@ -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"
+15
View File
@@ -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
}
+40
View File
@@ -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)
})
+1
View File
@@ -0,0 +1 @@
# pear 🍐