Source: core/src/plugins/input/chain.js

import { deepCopy, upgradeCsl } from '../../util/index.js'
import logger from '../../logger.js'

import { get as getTypeInfo } from './register.js'
import { type as parseType } from './type.js'
import { data as parseData, dataAsync as parseDataAsync } from './data.js'
import { applyGraph, removeGraph } from './graph.js'

/**
 * @access private
 * @param {Array<Object>} graph
 * @return {String}
 */
function prepareParseGraph (graph) {
  return graph
    // collapse continuous iterations of the same type
    .reduce((array, next) => {
      const last = array[array.length - 1]
      if (last && last.type === next.type) {
        last.count = last.count + 1 || 2
      } else {
        array.push(next)
      }
      return array
    }, [])
    // presentation
    .map(element => (element.count > 1 ? element.count + 'x ' : '') + element.type)
    .join(' -> ')
}

/**
 * @access private
 * @memberof module:@citation-js/core.plugins.input
 * @constructor ChainParser
 *
 * @param {module:@citation-js/core~InputData} input
 * @param {module:@citation-js/core~InputOptions} options
 */
class ChainParser {
  constructor (input, options = {}) {
    this.options = Object.assign({
      generateGraph: true,
      forceType: parseType(input),
      maxChainLength: 10,
      strict: true,
      target: '@csl/list+object'
    }, options)

    this.type = this.options.forceType
    this.data = typeof input === 'object' ? deepCopy(input) : input
    this.graph = [
      { type: this.type, data: input }
    ]
    this.iteration = 0
  }

  /**
   * After a round of data parsing, update type information and check targets.
   *
   * @access public
   * @return {Boolean} Whether this is the last iteration or not
   */
  iterate () {
    if (this.iteration !== 0) {
      const typeInfo = getTypeInfo(this.type)

      if (typeInfo && typeInfo.outputs) {
        this.type = typeInfo.outputs
      } else {
        this.type = parseType(this.data)
      }

      this.graph.push({ type: this.type })
    }

    if (this.error || this.type === this.options.target) {
      return false
    } else if (this.iteration >= this.options.maxChainLength) {
      this.error = new RangeError(`Max. number of parsing iterations reached (${
        prepareParseGraph(this.graph)
      })`)
      return false
    } else {
      this.iteration++
      return true
    }
  }

  /**
   * Finish the iteration and return parsed data.
   *
   * @access public
   * @return Array<module:@citation-js/core~CSL>
   */
  end () {
    if (this.error) {
      logger.error('[core]', this.error.message)
      if (this.options.strict !== false) {
        throw this.error
      } else {
        return []
      }
    } else if (this.options.target === '@csl/list+object') {
      return upgradeCsl(this.data).map(this.options.generateGraph
        ? entry => applyGraph(entry, this.graph)
        : removeGraph
      )
    } else {
      return this.data
    }
  }
}

/**
 * Parse input until success.
 *
 * @access protected
 * @method chain
 * @memberof module:@citation-js/core.plugins.input
 *
 * @param {module:@citation-js/core~InputData} input - input data
 * @param {module:@citation-js/core~InputOptions} [options] - options
 *
 * @return {Array<module:@citation-js/core~CSL>} The parsed input
 */
export const chain = (...args) => {
  const chain = new ChainParser(...args)

  while (chain.iterate()) {
    try {
      chain.data = parseData(chain.data, chain.type)
    } catch (e) {
      chain.error = e
    }
  }

  return chain.end()
}

/**
 * Parse input once.
 *
 * @access protected
 * @method chainLink
 * @memberof module:@citation-js/core.plugins.input
 *
 * @param {module:@citation-js/core~InputData} input - input data
 *
 * @return {module:@citation-js/core~InputData} The parsed input
 */
export const chainLink = (input) => {
  const type = parseType(input)
  const output = type.match(/array|object/) ? deepCopy(input) : input

  return parseData(output, type)
}

/**
 * Parse input until success. (async)
 *
 * @access protected
 * @method chainAsync
 * @memberof module:@citation-js/core.plugins.input
 *
 * @param {module:@citation-js/core~InputData} input - input data
 * @param {module:@citation-js/core~InputOptions} [options] - options
 *
 * @return {Promise<Array<module:@citation-js/core~CSL>>} The parsed input
 */
export const chainAsync = async (...args) => {
  const chain = new ChainParser(...args)

  while (chain.iterate()) {
    chain.data = await parseDataAsync(chain.data, chain.type).catch(e => { chain.error = e })
  }

  return chain.end()
}

/**
 * Parse input once. (async)
 *
 * @access protected
 * @method chainLinkAsync
 * @memberof module:@citation-js/core.plugins.input
 *
 * @param {module:@citation-js/core~InputData} input - The input data
 *
 * @return {Promise<module:@citation-js/core~InputData>} The parsed input
 */
export const chainLinkAsync = async (input) => {
  const type = parseType(input)
  const output = type.match(/array|object/) ? deepCopy(input) : input

  return parseDataAsync(output, type)
}