All files / utils html-color-to-rgba.js

100% Statements 83/83
100% Branches 0/0
100% Functions 0/0
100% Lines 83/83

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 841x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
/**
 * @template {(...args: any) => any} T
 * @param {T} func - function to memoize
 */
const memoize = function (func) {
  const cache = /** @type {{[key: string]: ReturnType<typeof func>}} */({})
  return function (/** @type {string} */ key) {
    if (!(key in cache)) {
      cache[key] = func(key)
    }
    return cache[key]
  }
}
/**
 * Converts hex to RGBA, this function exists for cli use, since nodejs does not support OffscreenCanvas,
 * and on cli it will always use the default ones (customizable only on web-component)
 *
 * @param {string} hex - hex color code
 */
const hexToRGBA = (hex) => {
    if(!/^#([A-Fa-f0-9]{3,4}){1,2}$/.test(hex)) { return } // invalid hex
    const chunkSize = Math.floor((hex.length - 1) / 3)
    const hexArr = hex.slice(1).match(new RegExp(`.{${chunkSize}}`, "g"))
    if(!hexArr) { return } // invalid color
    const [r, g, b, a = 255] = hexArr.map((hexStr) => parseInt(hexStr.repeat(2 / hexStr.length), 16))
    return Object.freeze([r, g, b, a])
}
 
 
const colorToRGBA = (function () {
  /** @type {OffscreenCanvasRenderingContext2D} */
  let ctx 
  const buildCanvasContext = () => {
    const canvas = new OffscreenCanvas(1, 1)
    canvas.width = canvas.height = 1
    const ctxContext = canvas.getContext('2d')
    if (!ctxContext) { throw Error('unreachable code') }
    return ctxContext
  }
 
 
  /**
   * @param {string} color - color name or code
   */
  const convertColor = function (color) {
    if(color.startsWith("#")){
      return hexToRGBA(color)
    }
    ctx ??= buildCanvasContext()
    ctx.clearRect(0, 0, 1, 1)
    // In order to detect invalid values,
    // we can't rely on col being in the same format as what fillStyle is computed as,
    // but we can ask it to implicitly compute a normalized value twice and compare.
    ctx.fillStyle = '#000'
    ctx.fillStyle = color
    const computed = ctx.fillStyle
    ctx.fillStyle = '#fff'
    ctx.fillStyle = color
    if (computed !== ctx.fillStyle) {
      return // invalid color
    }
    ctx.fillRect(0, 0, 1, 1)
    return Object.freeze([...ctx.getImageData(0, 0, 1, 1).data])
  }
 
  return memoize(convertColor)
})()
 
/**
 * @param {string} color - color name or code
 * @param {string} fallbackColor - fallback color name or code
 * @returns {readonly number[]} rgba color values
 */
export function colorOrFallbackColorToRGBA (color, fallbackColor) {
  // Don't short-circuit getting the fallback RGBA -
  // it's already memoized, and we want to show an error
  // if the fallback color is invalid even if the main color is valid
  const fallbackRGBA = colorToRGBA(fallbackColor)
  if (!fallbackRGBA) {
    throw new Error(`Invalid fallbackColor ${fallbackColor != null ? JSON.stringify(fallbackColor) : fallbackColor}`)
  }
  return colorToRGBA(color) || fallbackRGBA
}