packages/miew/src/utils.js

Summary

Maintainability
F
2 wks
Test Coverage
import logger from './utils/logger'
import {
  assign,
  isPlainObject,
  forIn,
  isEqual,
  isEmpty,
  isString
} from 'lodash'

const browserType = {
  DEFAULT: 0,
  SAFARI: 1
}
// ----------------------------------------------------------------------------
// Query string

/**
 * Escape only dangerous chars in a query string component, use a plus instead of a space.
 *
 * [RFC 3986](https://tools.ietf.org/html/rfc3986) allows the following chars in the query (see 3.4):
 *
 *       A-Z a-z 0-9 - _ . ~ ! $ & ' ( ) * + , ; = : @ / ?
 *
 * For query string elements we need to escape ampersand, equal sign, and plus,
 * but encodeURIComponent() function encodes anything except for the following:
 *
 *       A-Z a-z 0-9 - _ . ~ ! ' ( ) *
 *
 * @param {string} text - key or value to encode
 * @param {string} excludeExp - regexp for symbols to exclude from encoding
 * @returns {string} encoded string
 */
function encodeQueryComponent(text, excludeExp) {
  const encode = (code) => String.fromCharCode(parseInt(code.substr(1), 16))
  return encodeURIComponent(text)
    .replace(excludeExp, encode)
    .replace(/%20/g, '+')
}

/**
 * Unescape dangerous chars in a query string component.
 *
 * @param {string} text - encoded key or value
 * @returns {string} decoded string
 * @see {@link encodeQueryComponent}
 */
function decodeQueryComponent(text) {
  return decodeURIComponent(text.replace(/\+/g, ' '))
}

/**
 * Parse URL and extract an array of parameters.
 * @param {string?} url - URL or query string to parse
 * @returns {Array} array of (key, value) pairs.
 */
function getUrlParameters(url) {
  url = url || window.location.search

  const query = url.substring(url.indexOf('?') + 1)
  const search = /([^&=]+)=?([^&]*)/g
  const result = []
  let match

  while ((match = search.exec(query)) !== null) {
    // eslint-disable-line no-cond-assign
    result.push([
      decodeQueryComponent(match[1]),
      decodeQueryComponent(match[2])
    ])
  }

  return result
}

/**
 * Parse URL and extract an array of parameters as a hash.
 * @param {string?} url - URL or query string to parse
 * @returns {Object}
 */
function getUrlParametersAsDict(url) {
  const result = {}
  const a = getUrlParameters(url)
  for (let i = 0; i < a.length; ++i) {
    const [key, value] = a[i]
    result[key] = value
  }
  return result
}

function resolveURL(str) {
  if (typeof URL !== 'undefined') {
    try {
      if (typeof window !== 'undefined') {
        return new URL(str, window.location).href
      }
      return new URL(str).href
    } catch (error) {
      // IE 11 has a URL object with no constructor available so just try a different approach instead
    }
  }
  if (typeof document !== 'undefined') {
    const anchor = document.createElement('a')
    anchor.href = str
    return anchor.href
  }
  return str
}

/**
 * Generates regular expression object that includes all symbols
 * listed in the argument
 * @param symbolStr {string} - String containing characters list.
 * @returns {RegExp} - Regular expression.
 */
function generateRegExp(symbolStr) {
  const symbolList = []

  for (let i = 0, n = symbolStr.length; i < n; ++i) {
    symbolList[symbolList.length] = symbolStr[i].charCodeAt(0).toString(16)
  }

  const listStr = symbolList.join('|')

  return new RegExp(`%(?:${listStr})`, 'gi')
}

// ----------------------------------------------------------------------------
// Create HTML element

function createElement(tag, attrs, content) {
  const element = document.createElement(tag)
  let i
  let n
  if (attrs) {
    const keys = Object.keys(attrs)
    for (i = 0, n = keys.length; i < n; ++i) {
      const key = keys[i]
      element.setAttribute(key, attrs[key])
    }
  }
  if (content) {
    if (!(content instanceof Array)) {
      content = [content]
    }
    for (i = 0, n = content.length; i < n; ++i) {
      const child = content[i]
      if (typeof child === 'string') {
        element.appendChild(document.createTextNode(child))
      } else if (child instanceof HTMLElement) {
        element.appendChild(child)
      }
    }
  }
  return element
}

// ----------------------------------------------------------------------------
// Easy inheritance

/**
 * Derive the class from the base.
 * @param cls {function} - Class (constructor) to derive.
 * @param base {function} - Class (constructor) to derive from.
 * @param members {object=} - Optional instance members to add.
 * @param statics {object=} - Optional static class members to add.
 * @returns {function} Original class.
 */
function deriveClass(cls, base, members, statics) {
  cls.prototype = assign(
    Object.create(base.prototype),
    { constructor: cls },
    members
  )
  if (statics) {
    assign(cls, statics)
  }
  return cls
}

// ----------------------------------------------------------------------------
// Deep prototyping

function deriveDeep(obj, needZeroOwnProperties) {
  let res = obj
  let i
  let n
  if (obj instanceof Array) {
    res = new Array(obj.length)
    for (i = 0, n = obj.length; i < n; ++i) {
      res[i] = deriveDeep(obj[i])
    }
  } else if (obj instanceof Object) {
    res = Object.create(obj)
    const keys = Object.keys(obj)
    for (i = 0, n = keys.length; i < n; ++i) {
      const key = keys[i]
      const value = obj[key]
      const copy = deriveDeep(value)
      if (copy !== value) {
        res[key] = copy
      }
    }
    if (needZeroOwnProperties && Object.keys(res).length > 0) {
      res = Object.create(res)
    }
  }
  return res
}

// ----------------------------------------------------------------------------
// Colors

function hexColor(color) {
  const hex = `0000000${color.toString(16)}`.substr(-6)
  return `#${hex}`
}

// ----------------------------------------------------------------------------
// Debug tracing

function DebugTracer(namespace) {
  let enabled = false

  this.enable = function (on) {
    enabled = on
  }

  let indent = 0
  const methods = Object.keys(namespace)

  function wrap(method_, name_) {
    return function (...args) {
      const spaces = DebugTracer.spaces.substr(0, indent * 2)
      if (enabled) {
        logger.debug(`${spaces + name_} {`)
      }
      indent++
      const result = method_.apply(this, args) // eslint-disable-line no-invalid-this
      indent--
      if (enabled) {
        logger.debug(`${spaces}} // ${name_}`)
      }
      return result
    }
  }

  for (let i = 0, n = methods.length; i < n; ++i) {
    const name = methods[i]
    const method = namespace[name]
    if (method instanceof Function && name !== 'constructor') {
      namespace[name] = wrap(method, name)
    }
  }
}

DebugTracer.spaces =
  '                                                                                          '

class OutOfMemoryError extends Error {
  constructor(message) {
    super()
    this.name = 'OutOfMemoryError'
    this.message = message
  }
}

function allocateTyped(TypedArrayName, size) {
  let result = null
  try {
    result = new TypedArrayName(size)
  } catch (e) {
    if (e instanceof RangeError) {
      throw new OutOfMemoryError(e.message)
    } else {
      throw e
    }
  }
  return result
}

// ----------------------------------------------------------------------------
// Float array conversion

function bytesToBase64(/** ArrayBuffer */ buffer) {
  const bytes = new Uint8Array(buffer)
  let binary = ''
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i])
  }
  return window.btoa(binary)
}

function bytesFromBase64(/** string */ str) {
  const binary = window.atob(str)
  const bytes = new Uint8Array(binary.length)
  for (let i = 0; i < bytes.length; ++i) {
    bytes[i] = binary[i].charCodeAt(0)
  }
  return bytes.buffer
}

function arrayToBase64(/** number[] */ array, /** function */ TypedArrayClass) {
  return bytesToBase64(new TypedArrayClass(array).buffer)
}

function arrayFromBase64(/** string */ str, /** function */ TypedArrayClass) {
  return Array.prototype.slice.call(new TypedArrayClass(bytesFromBase64(str)))
}

// NOTE: this is 1-level comparison
function compareOptionsWithDefaults(opts, defOpts) {
  const optsStr = []
  if (defOpts && opts) {
    const keys = Object.keys(opts)
    for (let p = 0; p < keys.length; ++p) {
      const key = keys[p]
      const value = opts[key]
      // TODO add processing for tree structure
      if (
        !(value instanceof Object) &&
        typeof defOpts[key] !== 'undefined' &&
        defOpts[key] !== value
      ) {
        optsStr.push(`${key}:${value}`)
      }
    }
    if (optsStr.length > 0) {
      return `!${optsStr.join()}`
    }
  }
  return ''
}

function isAlmostPlainObject(o) {
  if (isPlainObject(o)) {
    return true
  }
  const proto = o && Object.getPrototypeOf(o)
  return (
    !!proto &&
    !Object.hasOwn(proto, 'constructor') &&
    isAlmostPlainObject(proto)
  )
}

/**
 * Build an object that contains properties (and subproperties) of `src` different from those
 * in `dst`. Objects are parsed recursively, other values (including arrays) are compared for
 * equality using `isEqual()`.
 * @param {!object} src - a new object to compare, may contain changed or new properties
 * @param {!object} dst - an old reference object
 */
function objectsDiff(src, dst) {
  const diff = {}
  forIn(src, (srcValue, key) => {
    const dstValue = dst[key]
    if (isAlmostPlainObject(srcValue) && isAlmostPlainObject(dstValue)) {
      const deepDiff = objectsDiff(srcValue, dstValue)
      if (!isEmpty(deepDiff)) {
        diff[key] = deepDiff
      }
    } else if (!isEqual(srcValue, dstValue)) {
      diff[key] = srcValue
    }
  })
  return diff
}

function forInRecursive(object, callback) {
  function iterateThrough(obj, prefix) {
    forIn(obj, (value, key) => {
      const newPref = prefix + (prefix.length > 0 ? '.' : '')
      if (value instanceof Object) {
        iterateThrough(value, newPref + key)
      } else if (value !== undefined) {
        callback(value, newPref + key)
      }
    })
  }
  iterateThrough(object, '')
}

function enquoteString(value) {
  if (isString(value)) {
    return `"${value.replace(/"/g, '\\"')}"`
  }
  return value
}

function unquoteString(value) {
  if (!isString(value)) {
    return value
  }
  if (value[0] === '"' && value[value.length - 1] === '"') {
    value = value.slice(1, value.length - 1)
    return value.replace(/\\"/g, '"')
  }
  if (value[0] === "'" && value[value.length - 1] === "'") {
    value = value.slice(1, value.length - 1)
    return value.replace(/\\'/g, "'")
  }
  throw new SyntaxError("Incorrect string format, can't unqute it")
}

function getFileExtension(fileName) {
  return fileName.slice(Math.max(0, fileName.lastIndexOf('.')) || Infinity)
}

function splitFileName(fileName) {
  const ext = getFileExtension(fileName)
  const name = fileName.slice(0, fileName.length - ext.length)
  return [name, ext]
}

function dataUrlToBlob(url) {
  const parts = url.split(/[:;,]/)
  const partsCount = parts.length
  if (partsCount >= 3 && parts[partsCount - 2] === 'base64') {
    return new Blob([bytesFromBase64(parts[partsCount - 1])])
  }
  return null
}

function getBrowser() {
  if (
    navigator.vendor &&
    navigator.vendor.indexOf('Apple') > -1 &&
    navigator.userAgent &&
    navigator.userAgent.indexOf('CriOS') === -1 &&
    navigator.userAgent.indexOf('FxiOS') === -1
  ) {
    return browserType.SAFARI
  }
  return browserType.DEFAULT
}

function shotOpen(url) {
  if (typeof window !== 'undefined') {
    window
      .open()
      .document.write(`<body style="margin:0"><img src="${url}" /></body>`)
  }
}

function shotDownload(dataUrl, filename) {
  if (!dataUrl || dataUrl.substr(0, 5) !== 'data:') {
    return
  }
  if (!filename) {
    filename = ['screenshot-', +new Date(), '.png'].join('')
  }
  if (
    typeof window !== 'undefined' &&
    window.navigator &&
    window.navigator.msSaveBlob
  ) {
    window.navigator.msSaveBlob(dataUrlToBlob(dataUrl), filename)
  } else if (typeof document !== 'undefined') {
    const link = document.createElement('a')
    link.download = filename
    link.innerHTML = 'download'
    link.href = window.URL.createObjectURL(dataUrlToBlob(dataUrl))
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
  }
}

function download(data, filename, type) {
  const blobData = new Blob([data])

  if (!filename) {
    filename = ['data', +new Date()].join('')
  }

  if (!type) {
    filename += blobData.type || '.bin'
  } else {
    filename += `.${type}`
  }

  if (
    typeof window !== 'undefined' &&
    window.navigator &&
    window.navigator.msSaveBlob
  ) {
    window.navigator.msSaveBlob(blobData, filename)
  } else if (typeof document !== 'undefined') {
    const link = document.createElement('a')
    link.download = filename
    link.innerHTML = 'download'
    link.href = window.URL.createObjectURL(blobData)
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
  }
}

function copySubArrays(src, dst, indices, itemSize) {
  for (let i = 0, n = indices.length; i < n; ++i) {
    for (let j = 0; j < itemSize; ++j) {
      dst[i * itemSize + j] = src[indices[i] * itemSize + j]
    }
  }
}

function shallowCloneNode(node) {
  const newNode = node.cloneNode(true)
  newNode.worldPos = node.worldPos
  // .style property is readonly, so "newNode.style = node.style;" won't work (and we don't need it, right?)
  return newNode
}

const unquotedStringRE = /^[a-zA-Z0-9_]*$/
const enquoteHelper = ['"', '', '"']

// verify and correct if needed selctor identifier
function correctSelectorIdentifier(value) {
  if (unquotedStringRE.test(value)) {
    return value
  }
  // quote incorrect identifier
  enquoteHelper[1] = value
  return enquoteHelper.join('')
}

/**
 * Concatenates two TypedArray. Doesn't check null refs o type equality
 * Attention! It must be use very rarely because requires memory reallocation every time. Use MergeTypedArraysUnsafe to
 * unite array of subarrays.
 * @param{TypedArray} first  - destination array
 * @param{TypedArray} second - source array
 * @returns{TypedArray} resulting concatenated array
 */
function concatTypedArraysUnsafe(first, second) {
  const result = new first.constructor(first.length + second.length)
  result.set(first)
  result.set(second, first.length)
  return result
}

/**
 * Merges array of TypedArray into TypedArray. Doesn't check null refs o type equality
 * @param{array} array  - source array of subarrays
 * @returns{TypedArray} resulting merged array
 */
function mergeTypedArraysUnsafe(array) {
  if (array.length <= 0) {
    return null
  }
  // count the size
  const size = array.reduce((acc, cur) => acc + cur.length, 0)
  // create combined array
  const result = new array[0].constructor(size)
  for (let i = 0, start = 0; i < array.length; i++) {
    const count = array[i].length
    result.set(array[i], start)
    start += count
  }
  return result
}

// ----------------------------------------------------------------------------
// Exports

export default {
  browserType,
  encodeQueryComponent,
  decodeQueryComponent,
  getUrlParameters,
  getUrlParametersAsDict,
  resolveURL,
  generateRegExp,
  createElement,
  deriveClass,
  deriveDeep,
  hexColor,
  DebugTracer,
  OutOfMemoryError,
  allocateTyped,
  bytesFromBase64,
  bytesToBase64,
  arrayFromBase64,
  arrayToBase64,
  compareOptionsWithDefaults,
  objectsDiff,
  forInRecursive,
  enquoteString,
  unquoteString,
  getBrowser,
  shotOpen,
  shotDownload,
  copySubArrays,
  shallowCloneNode,
  correctSelectorIdentifier,
  getFileExtension,
  splitFileName,
  download,
  concatTypedArraysUnsafe,
  mergeTypedArraysUnsafe
}