parse/modules/wikidata/prop.js

/**
 * @module input/wikidata
 */

import wdk from 'wikidata-sdk'
import fetchFile from '../../../util/fetchFile'
import fetchFileAsync from '../../../util/fetchFileAsync'

import fetchWikidataType from './type'
import parseDate from '../../date'
import parseName from '../../name'

/**
 * Get the names of objects from Wikidata IDs
 *
 * @access private
 * @method fetchWikidataLabel
 *
 * @param {String|Array<String>} q - Wikidata IDs
 * @param {String} lang - Language
 *
 * @return {Array<String>} Array with labels of each prop
 */
const fetchWikidataLabel = function (q, lang) {
  const ids = Array.isArray(q) ? q : typeof q === 'string' ? q.split('|') : ''
  const url = wdk.getEntities(ids, [lang], 'labels')
  const entities = JSON.parse(fetchFile(url)).entities || {}

  return Object.keys(entities).map(entityKey => (entities[entityKey].labels[lang] || {}).value)
}

/**
 * Get series ordinal from qualifiers object
 *
 * @access private
 * @method parseWikidataProp
 *
 * @param {Object} qualifiers - qualifiers object
 *
 * @return {Number} series ordinal or -1
 */
const parseWikidataP1545 = qualifiers => qualifiers.P1545 ? parseInt(qualifiers.P1545[0]) : -1

/**
 * Map holding information on Wikidata fields.
 *
 *  * If false, field should be ignored
 *  * If string, use as field name
 *
 * @access private
 * @constant propMap
 * @default
 */
const propMap = {
  P31: 'type',
  P50: 'author',
  P57: 'director',
  P86: 'composer',
  P98: 'editor',
  P110: 'illustrator',
  P123: 'publisher',
  P136: 'genre',
  P212: 'ISBN',
  P236: 'ISSN',
  P291: 'publisher-place',
  P304: 'page',
  P348: 'version',
  P356: 'DOI',
  P393: 'edition',
  P433: 'issue',
  P478: 'volume',
  P577: 'issued',
  P655: 'translator',
  P698: 'PMID',
  P932: 'PMCID',
  P953: 'URL',
  P957: 'ISBN',
  P1104: 'number-of-pages',
  P1433: 'container-title',
  P1476: 'title',
  P2093: 'author',

  // ignore
  P2860: false, // Cites
  P921: false, // Main subject
  P3181: false, // OpenCitations bibliographic resource ID
  P364: false // Original language of work
}

/**
 * Transform property and value from Wikidata format to CSL.
 *
 * Returns additional _ordinal property on authors.
 *
 * @access protected
 * @method parseWikidataProp
 *
 * @param {String} name - Property name
 * @param {String|Number} [value] - Value
 * @param {String} [lang] - Language
 *
 * @return {Array<String>|String} Array with new prop and value or just the prop when function is called without value
 */
const parseWikidataProp = function (name, value, lang) {
  if (!propMap.hasOwnProperty(name)) {
    logger.info('[set]', `Unknown property: ${name}`)
    return undefined
  } else if (propMap[name] === false) {
    return undefined
  }

  const cslProp = propMap[name]

  if (!value) {
    return cslProp
  }

  const cslValue = ((prop, valueList) => {
    const value = valueList[0].value

    switch (prop) {
      case 'P31':
        const type = fetchWikidataType(value)

        if (!type) {
          logger.warn('[set]', `Wikidata entry type not recognized: ${value}. Defaulting to "book".`)
          return 'book'
        }

        return type

      case 'P50':
      case 'P57':
      case 'P86':
      case 'P98':
      case 'P110':
      case 'P655':
        return valueList.map(({value, qualifiers}) => {
          const name = parseName(fetchWikidataLabel(value, lang)[0])
          name._ordinal = parseWikidataP1545(qualifiers)
          return name
        })

      case 'P577':
        return parseDate(value)

      case 'P123':
      case 'P136':
      case 'P291':
      case 'P1433':
        return fetchWikidataLabel(value, lang)[0]

      case 'P2093':
        return valueList.map(({value, qualifiers}) => {
          const name = parseName(value)
          name._ordinal = parseWikidataP1545(qualifiers)
          return name
        })

      default:
        return value
    }
  })(name, value)

  return [cslProp, cslValue]
}

/**
 * Get the names of objects from Wikidata IDs (async)
 *
 * @access private
 * @method fetchWikidataLabelAsync
 *
 * @param {String|Array<String>} q - Wikidata IDs
 * @param {String} lang - Language
 *
 * @return {Promise<Array<String>>} Array with labels of each prop
 */
const fetchWikidataLabelAsync = async function (q, lang) {
  const ids = Array.isArray(q) ? q : typeof q === 'string' ? q.split('|') : ''
  const url = wdk.getEntities(ids, [lang], 'labels')
  const entities = JSON.parse(await fetchFileAsync(url)).entities || {}

  return Object.keys(entities).map(entityKey => (entities[entityKey].labels[lang] || {}).value)
}

/**
 * Transform property and value from Wikidata format to CSL (async).
 *
 * Returns additional _ordinal property on authors.
 *
 * @access protected
 * @method parseWikidataPropAsync
 *
 * @param {String} prop - Property
 * @param {String|Number} value - Value
 * @param {String} lang - Language
 *
 * @return {Promise<Array<String>>} Array with new prop and value
 */
const parseWikidataPropAsync = async function (prop, value, lang) {
  const cslValue = await (async (prop, valueList) => {
    const value = valueList[0].value

    switch (prop) {
      case 'P50':
      case 'P57':
      case 'P86':
      case 'P98':
      case 'P110':
      case 'P655':
        return Promise.all(valueList.map(async ({value, qualifiers}) => {
          const name = parseName((await fetchWikidataLabelAsync(value, lang))[0])
          name._ordinal = parseWikidataP1545(qualifiers)
          return name
        }))

      case 'P123':
      case 'P136':
      case 'P291':
      case 'P1433':
        return (await fetchWikidataLabelAsync(value, lang))[0]
    }
  })(prop, value)

  if (cslValue) {
    return [parseWikidataProp(prop), cslValue]
  } else {
    return parseWikidataProp(prop, value, lang)
  }
}

export {
  parseWikidataProp as parse,
  parseWikidataPropAsync as parseAsync,
  parseWikidataProp as default
}