Skip to content

Instantly share code, notes, and snippets.

@inad9300
Last active July 12, 2020 20:17
Show Gist options
  • Save inad9300/22ad9be994109bad525900c6b4fb656a to your computer and use it in GitHub Desktop.
Save inad9300/22ad9be994109bad525900c6b4fb656a to your computer and use it in GitHub Desktop.
TypeScript fiddlings.
import { Html } from '../components/Html'
const context = Html('canvas').getContext('2d')!
export function getTextWidth(text: string, fontSize: number, fontFamily = 'system-ui, sans-serif') {
context.font = `${fontSize}px ${fontFamily}`
return context.measureText(text).width
}
const msInOneDay = 24 * 60 * 60 * 1_000
// Source: https://stackoverflow.com/a/6117889/2018219
export function getWeekNumber(d: Date) {
d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()))
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7))
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
const msFromYearStart = d.getTime() - yearStart.getTime()
const weekNo = Math.ceil((msFromYearStart / msInOneDay + 1) / 7)
return [weekNo, d.getUTCFullYear()]
}
/**
* Options available in those methods that modify the URL.
*/
interface SearchStorageWriteOptions {
/**
* Whether to reload the page after the URL change or not.
* @default false
*/
reload?: boolean
/**
* Whether to add a new history entry or replace the last one.
* @default 'push'
*/
history?: 'push' | 'replace'
}
type Items = {
[key: string]: string
}
/**
* Interact with the URL's query parameters using the same interface than `LocalStorage` and
* `SessionStorage`. Some inspiration has also been taken from `URLSearchParams`.
*
* NOTE Index signatures have been intentionally left unimplemented. The reason is two-fold:
* a) A direct implementation cannot be provided as if they were regular functions or accessors,
* meaning the data must be stored locally in this object (and no extra options can be passed).
* b) The URL is a shared resource that might be updated from the outside, in whose case access by
* index would yell out-of-date results.
* In addition, the index signatures of `localStorage` and `sessionStorage` might lead to very
* confusing situations, since keys conflicting with methods of those objects will not be
* accessible through the corresponding string indexes, e.g. `localStorage.key` will point to a
* function, even after running `localStorage.key = '42'`.
*/
class SearchStorage implements Storage {
/**
* Return the number of query parameters in the URL.
* @override
*/
get length(): number {
return Object.keys(this.getItems()).length
}
/**
* Check if a given query parameter is present in the URL, by name.
*/
hasItem(key: string): boolean {
return this.getItems().hasOwnProperty(key)
}
/**
* Retrieve a query parameter by its position in the URL. Not recommended.
* @override
*/
key(index: number): string | null {
const items = this.getItems()
const item = Object.keys(items).map(key => items[key])[index]
return item === undefined ? null : item
}
/**
* Retrieve a query parameter by name.
* @override
*/
getItem(key: string): string | null {
const item = this.getItems()[key]
return item === undefined ? null : item
}
/**
* Retrieve all query parameters present in the URL as a key-value map.
*/
getItems(): Items {
const items: Items = {}
location.search
.substr(1)
.split('&')
.map(pair => pair.split('='))
.forEach(([key, value]) => items[key] = decodeURIComponent(value))
return items
}
/**
* Set or update the value of a query parameter.
* @override
*/
setItem(key: string, value: string, options?: SearchStorageWriteOptions): void {
const items = this.getItems()
items[key] = value
this.setItems(items, options)
}
/**
* Replace all existing query parameters with a new set of them, updating the history and
* reloading the page according to the options provided.
*/
setItems(items: Items, options: SearchStorageWriteOptions = {}): void {
const search = Object
.keys(items)
.filter(key => items[key] !== null && items[key] !== undefined)
.map(key => key + '=' + encodeURIComponent(items[key]))
.join('&')
const url = location.pathname + (search ? '?' + search : '') + location.hash
if (options.history === 'replace')
history.replaceState(null, document.title, url)
else
history.pushState(null, document.title, url)
if (options.reload === true)
location.reload()
}
/**
* Set or update the value of multiple query parameters at once.
*/
assignItems(items: Items, options?: SearchStorageWriteOptions): void {
this.setItems(Object.assign(this.getItems(), items), options)
}
/**
* Delete a specific query parameter from the URL.
* @override
*/
removeItem(key: string, options?: SearchStorageWriteOptions): void {
const items = this.getItems()
delete items[key]
this.setItems(items, options)
}
/**
* Remove all query parameters from the URL.
* @override
*/
clear(options?: SearchStorageWriteOptions): void {
this.setItems({}, options)
}
}
/**
* The one instance of `SearchStorage` that gives access to the functions to manipulate the query
* string part of the URL, analogously to `localStorage` and `sessionStorage`.
*/
export const searchStorage = new SearchStorage
const stdin = process.stdin
const stderr = process.stderr
const ansi = {
ctrlC: '\u0003',
ctrlD: '\u0004',
eraseLine: '\u001B[2K',
eraseEndLine: '\u001B[K',
cursorLeft: '\u001B[G',
cursorBackward: '\u001B[1D'
}
export async function asyncSecretPrompt(prelude: string) {
return secretPrompt(prelude)
}
export function secretPrompt(prelude = 'Password: '): Promise<string> {
return new Promise((resolve, reject) => {
let input = ''
function onData(ch: string) {
switch (ch) {
case ansi.ctrlD:
case '\r':
case '\n':
if (input.length > 0) {
stop()
return resolve(input.replace(/\r$/, ''))
}
return reject()
case ansi.ctrlC:
stop()
return reject()
default:
if (ch.charCodeAt(0) === 127) { // Backspace.
if (input.length > 0) {
input = input.substr(0, input.length - 1)
stderr.write(ansi.cursorBackward)
stderr.write(ansi.eraseEndLine)
}
} else {
input += ch
stderr.write('*'.repeat(ch.length))
}
}
}
function stop() {
stderr.write('\n')
stdin.removeListener('data', onData)
stdin.setRawMode!(false)
stdin.pause()
}
stdin.setEncoding('utf8')
stderr.write(ansi.eraseLine)
stderr.write(ansi.cursorLeft)
stderr.write(prelude)
stdin.resume()
stdin.setRawMode!(true)
stdin.on('data', onData)
})
}
interface Pushable<T> {
push: (promise: Promise<T>) => void
}
/**
* Wait for a number of promises to be available, then resolve a new promise with their results.
*/
function waitFor<T1>(expectedPromises: number, timeoutMs?: number): Promise<T1[]> & Pushable<T1>
function waitFor<T1, T2>(expectedPromises: number, timeoutMs?: number): Promise<(T1 | T2)[]> & Pushable<T1 | T2>
function waitFor<T1, T2, T3>(expectedPromises: number, timeoutMs?: number): Promise<(T1 | T2 | T3)[]> & Pushable<T1 | T2 | T3>
function waitFor<T1, T2, T3, T4>(expectedPromises: number, timeoutMs?: number): Promise<(T1 | T2 | T3 | T4)[]> & Pushable<T1 | T2 | T3 | T4>
function waitFor<T1, T2, T3, T4, T5>(expectedPromises: number, timeoutMs?: number): Promise<(T1 | T2 | T3 | T4 | T5)[]> & Pushable<T1 | T2 | T3 | T4 | T5>
function waitFor(expectedPromises: number, timeoutMs?: number) {
let _resolve: Function
let _reject: Function
const promise = new Promise((resolve, reject) => {
_resolve = resolve
_reject = reject
})
if (timeoutMs) {
setTimeout(() => _reject(`${timeoutMs} milliseconds timeout reached waiting for ${expectedPromises} promises.`), timeoutMs)
}
const promises = []
return Object.assign(promise, {
push: (promise: Promise<any>) => {
promises.push(promise)
if (promises.length === expectedPromises) {
_resolve(Promise.all(promises))
}
}
})
}
// Example.
const waiter = waitFor<string, string>(2)
waiter.push(Promise.resolve('A'))
waiter.push(Promise.resolve('B'))
waiter.then(([a, b]) => console.log('Done.', a, b))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment