Home Reference Source

src/types.js

// TODO: this should go into its own repo: `stencila/types`
import { isNil } from 'substance'

// Parent of each type
const parentTypes = {
  'any': null,
  'null': 'any',
  'boolean': 'any',
  'number': 'any',
  'integer': 'number',
  'string': 'any',
  'object': 'any',
  'array': 'any',
  'array[boolean]': 'array',
  'array[number]': 'array',
  'array[integer]': 'array[number]',
  'array[string]': 'array',
  'array[object]': 'array',
  'table': 'any'
}

// Children of each type
const childrenTypes = {}
for (let type of Object.keys(parentTypes)) {
  if (!childrenTypes[type]) childrenTypes[type] = []
  let base = parentTypes[type]
  if (!base) continue
  if (childrenTypes[base]) childrenTypes[base].push(type)
  else childrenTypes[base] = [type]
}

// Descendants (children, grandchildren etc) of each type
const descendantTypes = {}
for (let type of Object.keys(parentTypes)) {
  if (!descendantTypes[type]) descendantTypes[type] = []
  let parent = parentTypes[type]
  while (parent) {
    if (descendantTypes[parent]) descendantTypes[parent].push(type)
    else descendantTypes[parent] = [type]
    parent = parentTypes[parent]
  }
}

export { parentTypes, childrenTypes, descendantTypes }

export function coercedArrayType (arr) {
  let valType = arr.reduce(_mostSpecificType, undefined)
  if (valType === 'any') {
    return 'array'
  } else {
    return `array[${valType}]`
  }
}

/**
 * Get the type code for a value
 *
 * @memberof value
 * @param {*} value - A JavaScript value
 * @return {string} - Type code for value
 */
export function type (value) {
  let type = typeof value

  if (isNil(value) === null) {
    return 'null'
  } else if (type === 'boolean') {
    return 'boolean'
  } else if (type === 'number') {
    let isInteger = false
    if (value.isInteger) isInteger = value.isInteger()
    else isInteger = (value % 1) === 0
    return isInteger ? 'integer' : 'number'
  } else if (type === 'string') {
    return 'string'
  } else if (type === 'object') {
    if (value.constructor === Array) {
      return 'array'
    }
    if (value.type) return value.type
    else return 'object'
  } else {
    return 'unknown'
  }
}

/*
  A helper to get a coerced array value from a given array of values.
*/
export function coerceArray (arr) {
  return {
    type: coercedArrayType(arr),
    data: arr.map(v => {
      if (v) {
        return v.data
      } else {
        return undefined
      }
    })
  }
}

function _mostSpecificType (type, next) {
  if (!next) return 'any'
  let nextType = next.type
  if (!type) return nextType
  if (type === nextType) {
    return type
  }
  switch (type) {
    case 'number': {
      if (nextType === 'integer') {
        return 'number'
      }
      break
    }
    case 'integer': {
      if (nextType === 'number') {
        return 'number'
      }
      break
    }
    default:
      //
  }
  return 'any'
}

export function pack (value, opts = {}) {
  if (isNil(value)) {
    return { type: 'null', data: null }
  }
  if (typeof value === 'function') {
    let data = { name: value.name }
    if (opts.context) data.context = opts.context.id
    if (opts.cell) data.id = opts.cell.id
    return {
      type: 'function',
      data
    }
  }
  if (value.type === 'image') {
    return { type: 'image', src: value.src }
  } else {
    let _type = value.type || type(value)
    return { type: _type, data: value }
  }
}

export function unpack (pkg) {
  if (isNil(pkg)) return undefined
  return pkg.data
}