import * as _ from "./utils.js"
import { objectTypes, guessObjectType } from "./object-types.js"
import ConceptScheme from "./concept-scheme.js"
import languagePreference from "./language-preference.js"
import mappingTypes from "./mapping-types.js"
/**
* Tests if a string only contains uppercase letters.
* @private
* @param {string} str
*/
const isUpperCase = str => {
return (/^[A-Z]*$/).test(str)
}
/**
* Safely get a nested property.
* @private
* @param {*} object the object to access
* @param {*} path path expression
*/
const getNested = (object, path) => {
return path.split(".").reduce(
(xs, x) => (xs && xs[x]) ? xs[x] : null, object)
}
/**
* Add @context URI to a JSKOS resource or to an array of JSKOS resources.
* @memberof module:jskos-tools
* @param {object} jskos object or array of objects
*/
export const addContext = jskos => {
let array = jskos instanceof Array ? jskos : [jskos]
array.forEach(resource => {
resource["@context"] = "https://gbv.github.io/jskos/context.json"
})
return jskos
}
/**
* Recursively cleans JSKOS object by removing properties starting with _ or containing only uppercase letters.
* Warning: Works directly on the object without creating a copy!
* @memberof module:jskos-tools
* @param {object} jskos
*/
export const clean = jskos => {
Object.keys(jskos).forEach(key => {
if (isUpperCase(key) || key.startsWith("_")) {
delete jskos[key]
} else {
if (jskos[key] != null && typeof jskos[key] === "object") {
jskos[key] = clean(jskos[key])
}
}
})
return jskos
}
// cleanJSKOS as alias for clean.
export const cleanJSKOS = clean
/**
* Creates a deep copy of a JSKOS object, replacing possibly circular structures with open world [null] statements.
* All properties starting with an underscore (_) will be ignored.
* @memberof module:jskos-tools
* @param {object} object
* @param {array} replaceCircular - additional property names that should be replace with open world [null] statements
* @param {bool} skipUnderscore - whether to skip properties starting with `_` (default `true`)
*/
export const copyDeep = (object, replaceCircular = [], skipUnderscore = true) => {
replaceCircular = replaceCircular.concat([
"ancestors", "narrower", "broader", "mappings", "memberList", "TOPCONCEPTS", "MAPPINGS", "PROVIDER",
])
let clone = Array.isArray(object) ? [] : {}
for(let i in object) {
// Ignore all properties starting with _
if (skipUnderscore && i[0] == "_") {
continue
}
if (replaceCircular.includes(i)) {
// Remove circular structures, replace with [null] if it has elements
if (object[i] && Array.isArray(object[i])) {
if (object[i].length > 0) {
clone[i] = [null]
} else {
clone[i] = []
}
continue
} else {
clone[i] = null
continue
}
}
if (i == "inScheme") {
// Remove circular structur for inScheme and replace with new object consisting only of URI, notation, and prefLabel
let inScheme = []
for (let scheme of object.inScheme) {
let newScheme = { uri: scheme.uri }
if (scheme.notation) {
newScheme.notation = scheme.notation
}
if (scheme.prefLabel) {
newScheme.prefLabel = scheme.prefLabel
}
inScheme.push(newScheme)
}
clone.inScheme = inScheme
continue
}
if (object[i] != null && typeof(object[i]) == "object") {
clone[i] = copyDeep(object[i])
} else {
clone[i] = object[i]
}
}
return clone
}
// deepCopy as alias for copyDeep.
export const deepCopy = copyDeep
/**
* Returns all possible URIs for a JSKOS object. Takes into consideration both the uri and identifier properties.
*
* @memberof module:jskos-tools
* @param {object} object
*/
export const getAllUris = object => {
if (!object) {
return []
}
return (object.uri ? [object.uri] : []).concat(object.identifier || []).filter(Boolean)
}
/**
* Compares two objects based on their URIs, using getAllUris.
*
* @memberof module:jskos-tools
* @param {object} object1
* @param {object} object2
*/
export const compare = (object1, object2) => {
// Return true if both objects are null.
if (object1 == null && object2 == null) {
return true
}
// Check if any of object2's URIs is in object1's URIs.
const replaceUri = uri => uri.replace("http://").replace("https://")
const object1uris = getAllUris(object1).map(replaceUri)
for (let uri of getAllUris(object2).map(replaceUri)) {
if (object1uris.indexOf(uri) !== -1) {
return true
}
}
return false
}
// compareObjects, compareSchemes and compareConcepts as aliases for compare, for compatibility.
export const compareObjects = compare
export const compareSchemes = compare
export const compareConcepts = compare
/**
* Checks whether JSKOS object is a concept based on type property.
* @memberof module:jskos-tools
*/
export const isConcept = object => {
return _.get(object, "type", [])?.includes("http://www.w3.org/2004/02/skos/core#Concept") || _.get(object, "inScheme") != null || _.get(object, "topConceptOf") != null
}
/**
* Checks whether JSKOS object is a concept scheme based on type property.
* @memberof module:jskos-tools
*/
export const isScheme = object => {
return _.get(object, "type", [])?.includes("http://www.w3.org/2004/02/skos/core#ConceptScheme")
}
/**
* Checks whether an object is contained in a list of objects using compare.
* @memberof module:jskos-tools
*/
export const isContainedIn = (object, objects) => {
if (!object || !objects) {
return false
}
for (let o of objects) {
if (compare(object, o)) {
return true
}
}
return false
}
// isSchemeInList as alias for isContainedIn.
export const isSchemeInList = isContainedIn
/**
* Sorts a list of concepts by their notation, then URI.
*
* @memberof module:jskos-tools
* @param {*} concepts
*/
export const sortConcepts = (concepts, numerical = false) => {
return concepts.sort(
(a, b) => {
let _a = _.get(a, "notation[0]"), _b = _.get(b, "notation[0]")
if (_a && _b) {
_a = _a.toLowerCase()
_b = _b.toLowerCase()
}
if (numerical && _a && _b) {
let __a, __b
__a = parseFloat(_a)
__b = parseFloat(_b)
if (!__a && !__b) {
// Split notations into parts
const _a_split = `${_a}`.split(/[^\w\d]/)
const _b_split = `${_b}`.split(/[^\w\d]/)
for (let i = 0; i < Math.min(_a_split.length, _b_split.length); i += 1) {
if (_a_split[i] !== _b_split[i]) {
__a = parseFloat(_a_split[i]) || _a_split[i]
__b = parseFloat(_b_split[i]) || _b_split[i]
break
}
}
if (!__a && !__b) {
__a = _a
__b = _b
}
}
_a = __a
_b = __b
}
// Fallback to URI
if (!_a || !_b || _a == _b) {
_a = a.uri
_b = b.uri
}
if (_a && _b) {
if (_a > _b) {
return 1
} else if (_a < _b) {
return -1
}
}
return 0
},
)
}
/**
* Sorts a list of schemes by their prefLabel (German or English), then notation, then URI.
*
* @memberof module:jskos-tools
* @param {*} schemes
*/
export const sortSchemes = schemes => {
let order = ["notation[0]", ["prefLabel.de", "prefLabel.en"], "uri"]
return schemes.sort(
(a, b) => {
for (let path of order) {
let _a, _b
if (Array.isArray(path)) {
for (let path2 of path) {
_a = _a || _.get(a, path2)
_b = _b || _.get(b, path2)
}
} else {
_a = _.get(a, path)
_b = _.get(b, path)
}
if (_a != null && _b != null) {
_a = _a.toLowerCase()
_b = _b.toLowerCase()
if (_a > _b) {
return 1
} else if (_a < _b) {
return -1
}
}
}
return 0
},
)
}
/**
* @memberof module:jskos-tools
*/
export const minifyMapping = mapping => {
let newMapping = _.pick(copyDeep(mapping), ["from", "to", "fromScheme", "toScheme", "creator", "contributor", "type", "created", "modified", "note", "identifier", "uri", "partOf", "mappingRelevance"])
for (let fromTo of [newMapping.from, newMapping.to]) {
_.forOwn(fromTo, (value, key) => {
let conceptBundle = []
for (let concept of value) {
conceptBundle.push(_.pick(concept, ["uri", "notation"]))
}
fromTo[key] = conceptBundle
})
}
if (newMapping.fromScheme) {
newMapping.fromScheme = _.pick(newMapping.fromScheme, ["uri", "notation"])
}
if (newMapping.toScheme) {
newMapping.toScheme = _.pick(newMapping.toScheme, ["uri", "notation"])
}
if (newMapping.partOf) {
newMapping.partOf = newMapping.partOf.map(c => _.pick(c, ["uri"]))
}
return newMapping
}
/**
* @memberof module:jskos-tools
*
* Run `bin/localize-mapping-types` to update labels from Wikidata.
*
*/
export { mappingTypes }
/**
* @memberof module:jskos-tools
*/
export const mappingTypeByUri = function(uri) {
for(let mappingType of mappingTypes) {
if (uri == mappingType.uri) {
return mappingType
}
}
return null
}
/**
* @memberof module:jskos-tools
*/
export const defaultMappingType = mappingTypeByUri("http://www.w3.org/2004/02/skos/core#mappingRelation")
/**
* @memberof module:jskos-tools
*/
export const mappingTypeByType = function(type, defaultType = defaultMappingType) {
let uri
if (Array.isArray(type) && type.length > 0) {
uri = type[0]
} else {
// This is a workaround for the type being a string instead of an array.
uri = type
}
return mappingTypeByUri(uri) || defaultType
}
/**
* @memberof module:jskos-tools
*/
export const flattenMapping = (mapping, options = {}) => {
const { language } = options
let fromNotation = getNested(mapping, "from.memberSet.0.notation.0")
let toNotation = getNested(mapping, "to.memberSet.0.notation.0")
fromNotation = fromNotation !== null ? fromNotation : ""
toNotation = toNotation !== null ? toNotation : ""
let type = mappingTypeByUri(getNested(mapping, "type.0"))
type = type ? type.SHORT : ""
let fromLabel = prefLabel(getNested(mapping, "from.memberSet.0"), { language, fallbackToUri: false }) || ""
let toLabel = prefLabel(getNested(mapping, "to.memberSet.0"), { language, fallbackToUri: false }) || ""
let creator = prefLabel(getNested(mapping, "creator.0"), { language, fallbackToUri: false }) || ""
return {fromNotation, toNotation, fromLabel, toLabel, type, creator}
}
/**
* Returns a function to serialize an array as CSV row as configured.
* See CSV Dialect (<https://frictionlessdata.io/specs/csv-dialect/>).
*
* @memberof module:jskos-tools
*/
const csvSerializer = (options = {}) => {
const delimiter = options.delimiter || ","
const quoteChar = options.quoteChar || "\""
const lineTerminator = options.lineTerminator || "\n"
const doubleQuote = quoteChar + quoteChar
const quote = s => quoteChar + (s == null ? "" : s.split(quoteChar).join(doubleQuote)) + quoteChar
return row => row.map(quote).join(delimiter) + lineTerminator
}
/**
* Returns a list of concepts for a mapping.
*
* @memberof module:jskos-tools
* @param {*} mapping
* @param {*} side - Either `from` or `to`. Default is both.
*/
export const conceptsOfMapping = (mapping, side) => {
let concepts = []
for (let s of ["from", "to"]) {
if (side == null || s === side) {
concepts = concepts.concat(
_.get(mapping, `${s}.memberSet`) ||
_.get(mapping, `${s}.memberChoice`) ||
_.get(mapping, `${s}.memberList`) ||
[],
)
}
}
return concepts.filter(c => c != null)
}
/**
* Returns an object of preconfigured conversion functions to convert mappings into CSV.
*
* @memberof module:jskos-tools
* @param {object} options
*
* Possible options:
* - delimiter: delimiter character (default `,`)
* - quoteChar: quote character (default `"`)
* - lineTerminator: line terminator (default `\n`)
* - type: whether to include mapping type in output (default true)
* - schemes: whether to include scheme notations in output (default false)
* - labels: whether to include concept labels in output (default false)
* - creator: whether to include mapping creator in output (default false)
*
*/
export const mappingCSV = (options = {}) => {
const toCSV = csvSerializer(options)
const language = options.language || "en"
if (options.type == null) {
options.type = true
}
const header = (mappings) => {
mappings = mappings || []
let fields = []
for (let side of ["from", "to"]) {
// Scheme
if (options.schemes) {
fields.push(`${side}Scheme`)
}
// Minimum count: 1 (for 1-to-1 mappings)
let conceptCount = Math.max(...mappings.map(mapping => conceptsOfMapping(mapping, side).length), 1)
for (let i = 0; i < conceptCount; i += 1) {
// Notation
fields.push(`${side}Notation${i ? i + 1 : ""}`)
// Label
if (options.labels) {
fields.push(`${side}Label${i ? i + 1 : ""}`)
}
}
}
// Type
if (options.type) {
fields.push("type")
}
// Creator
if (options.creator) {
fields.push("creator")
}
return toCSV(fields)
}
/**
* Converts a single mapping into a CSV line.
*
* @param {*} mapping a single mapping
* @param {*} options2 an options object with properties `fromCount` and `toCount`
*/
const fromMapping = (mapping, options2 = {}) => {
let fields = []
for (let side of ["from", "to"]) {
// Scheme
if (options.schemes) {
fields.push(_.get(mapping, `${side}Scheme.notation[0]`, ""))
}
const concepts = conceptsOfMapping(mapping, side)
let conceptCount = options2[`${side}Count`]
if (conceptCount == null) {
conceptCount = concepts.length
}
// Minimum count: 1 (for 1-to-1 mappings)
conceptCount = Math.max(conceptCount, 1)
for (let i = 0; i < conceptCount; i += 1) {
// Notation
fields.push(_.get(concepts[i], "notation[0]", ""))
// Label
if (options.labels) {
fields.push(prefLabel(concepts[i], { language, fallbackToUri: false }))
}
}
}
// Type
if (options.type) {
fields.push(_.get(mappingTypeByUri(_.get(mapping, "type[0]")), "SHORT", ""))
}
// Creator
if (options.creator) {
fields.push(prefLabel(_.get(mapping, "creator[0]"), { language, fallbackToUri: false }))
}
return toCSV(fields)
}
/**
* Converts an array of mappings into CSV.
*
* @param {*} mapping an array of mappings
* @param {*} options2 an options object with optional property `header` (default true)
*/
const fromMappings = (mappings, options2 = { header: true }) => {
let result = ""
if (options2.header) {
result += header(mappings)
}
const fromMappingOptions = {
fromCount: Math.max(...mappings.map(mapping => conceptsOfMapping(mapping, "from").length)),
toCount: Math.max(...mappings.map(mapping => conceptsOfMapping(mapping, "to").length)),
}
for (let mapping of mappings) {
result += fromMapping(mapping, fromMappingOptions)
}
return result
}
return {
header,
fromMapping,
fromMappings,
}
}
/**
* Compare two mappings based on their properties. Concept sets and schemes are compared by URI.
*
* @memberof module:jskos-tools
*/
export function compareMappingsDeep(mapping1, mapping2) {
return _.isEqualWith(mapping1, mapping2, (object1, object2, prop) => {
let mapping1 = { [prop]: object1 }
let mapping2 = { [prop]: object2 }
if (prop == "from" || prop == "to") {
if (!_.isEqualWith(Object.getOwnPropertyNames(_.get(object1, prop, {})), Object.getOwnPropertyNames(_.get(object2, prop, {})))) {
return false
}
return _.isEqualWith(conceptsOfMapping(mapping1, prop), conceptsOfMapping(mapping2, prop), (concept1, concept2, index) => {
if (index != undefined) {
return compare(concept1, concept2)
}
return undefined
})
}
if (prop == "fromScheme" || prop == "toScheme") {
return compare(object1, object2)
}
// Let lodash's isEqual do the comparison
return undefined
})
}
/**
* Checks if two objects have a matching object type. Returns false only if types for both objects could be guessed and they did not match.
*
* @memberof module:jskos-tools
* @param {object} a
* @param {object} b
*/
export const matchObjectTypes = (a, b) => {
// Guess object types of both objects
let aType = guessObjectType(a), bType = guessObjectType(b)
// If both object types could be guessed, throw an error if they don't match
if (aType && bType && aType != bType) {
return false
}
return true
}
/**
* Sorts an array so that `null` values are at the end.
*
* @private
* @param {*} array
*/
const _nullSort = (array) => {
if (Array.isArray(array)) {
array.sort((a, b) => {
if (a === null) {
return 1
}
if (b === null) {
return -1
}
return 0
})
}
}
/**
* Merge URIs of two objects `a` and `b` into `a` by adding/removing URIs from identifier property.
*
* @memberof module:jskos-tools
* @param {object} a
* @param {object} b
*/
export const mergeUris = (a, b) => {
if (!a || !b) {
return a
}
// Merge identifier array
if (Array.isArray(a.identifier) || Array.isArray(b.identifier)) {
a.identifier = _.union(a.identifier || [], b.identifier || [])
}
// Add URI to a if necessary
if (!a.uri && b.uri) {
a.uri = b.uri
}
// Add b's URI to a's identifier if necessary
if (a.uri && b.uri && a.uri != b.uri && !(a.identifier || []).includes(b.uri)) {
a.identifier = (a.identifier || []).concat([b.uri])
}
// Remove a's URI from identifier if necessary
if (Array.isArray(a.identifier) && a.uri) {
a.identifier = a.identifier.filter(uri => uri !== a.uri)
}
// Remove identifier property if empty
if ((a.identifier || []).length == 0) {
delete a.identifier
}
// Sort null values to end
_nullSort(a.identifier)
return a
}
/**
* Merges two JSKOS objects. Object properties will be merged deeply. Array properties will be combined (and URIs will be used for comparison if it's an array of objects).
*
* The `options` property allows for different options:
* - `mergeUris` (boolean, default `false`) - appends b's URI to the result's `identifier` property if necessary, removes a's URI from the result's `identifier` property if necessary (useful for merging ConceptSchemes)
* - `detectMismatch` (array of strings, default `[]`) - throws an error if the value at a certain path does not match between the two objects (allows for deep properties, e.g. `prefLabel.de`)
* - `skipPaths` (array of strings, default `[]`) - provide paths to completely skip when merging
*
* @memberof module:jskos-tools
* @param {object} a
* @param {object} b
* @param {object} options - optional, see above
*/
export const merge = (a, b, options) => {
if (!a || !b) {
return a ? a : b
}
options = options || {}
// Path is used for deep detectMismatch checks
let path = options._path || ""
// Throw an error if two simple properties do not match between the objects
let detectMismatch = options.detectMismatch || []
let skipPaths = options.skipPaths || []
let result = {}
// Merge properties that are in both objects
_.forOwn(a, (value, key) => {
// Skip path if necessary
if (skipPaths.includes(path + key)) {
return
}
if (Array.isArray(value) && Array.isArray(b[key])) {
// Merge array properties
result[key] = _.unionWith(value, b[key], (first, second) => {
if (_.isObject(first) && _.isObject(second)) {
return compare(first, second)
}
return first === second
})
// Sort null values to the end while keeping the rest of the order the same
_nullSort(result[key])
} else if (_.isObject(value) && _.isObject(b[key])) {
// Merge object properties
result[key] = merge(value, b[key], Object.assign({ _path: `${path}${key}.` }, options))
} else {
if (value && b[key] && detectMismatch.includes(path + key) && !_.isEqualWith(value, b[key])) {
throw new Error("Property mismatch in " + path + key)
}
result[key] = value
}
})
// Add properties that are only in b
_.forOwn(b, (value, key) => {
if (!a[key]) {
result[key] = value
}
})
// Remove paths if necessary
if (path == "") {
_.omitMod(result, skipPaths)
}
// Merge URIs if necessary
if (options.mergeUris) {
mergeUris(result, b)
}
return result
}
/**
* Applies recursive unicode normalization to data.
*
* - If data is an array, it will recursively normalize all elements of that array.
* - If data is an object, it will recursively normalize all property values of that object.
* - If data is a string, it will apply unicode normalization to that string.
* - If data is of any other type, it will be returned as is.
*
* @param {*} data
*/
export const normalize = data => {
if (Array.isArray(data)) {
return data.map(element => normalize(element))
} else if (_.isObject(data)) {
_.forOwn(data, (value, key) => {
data[key] = normalize(value)
})
return data
} else {
if (_.isString(data)) {
return data.normalize()
} else {
return data
}
}
}
/**
* Checks whether a string is a valid URI.
*
* @param {string} uri URI to be tested
*/
export const isValidUri = (uri) => {
// from: http://jmrware.com/articles/2009/uri_regexp/URI_regex.html
const re_js_rfc3986_URI = /^[A-Za-z][A-Za-z0-9+\-.]*:(?:\/\/(?:(?:[A-Za-z0-9\-._~!$&'()*+,;=:]|%[0-9A-Fa-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\-._~!$&'()*+,;=]|%[0-9A-Fa-f]{2})*)(?::[0-9]*)?(?:\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|\/(?:(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?|(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|)(?:\?(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?(?:#(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?$/
return uri.match(re_js_rfc3986_URI) !== null
}
/**
* An object of compare functions (can be used by array.sort for example).
*
* TODO: Add more functions.
*/
export const compareFunctions = {
/**
* Compare mappings by their first concept.
*
* @param {*} mapping1 - first mapping
* @param {*} mapping2 - second mapping
* @param {*} fromTo - side, either `from` or `to`
*/
mappingsByConcepts: (mapping1, mapping2, fromTo) => {
let bundleFields = ["memberSet", "memberList", "memberChoice"], notation1, notation2
for (let field of bundleFields) {
notation1 = notation1 || _.get(mapping1, fromTo + "." + field + "[0].notation[0]")
notation2 = notation2 || _.get(mapping2, fromTo + "." + field + "[0].notation[0]")
}
if (notation1 == null || notation1 < notation2) {
return -1
}
if (notation2 == null || notation1 > notation2) {
return 1
}
return 0
},
}
/**
* Returns `true` if the user owns the mapping (i.e. is first creator), `false` if not.
*
* @param {*} user a login-server compatible user object
* @param {*} mapping a JSKOS mapping
*/
export const userOwnsMapping = (user, mapping) => {
if (!user || !mapping) {
return false
}
return [user.uri].concat(Object.values(user.identities || {}).map(identity => identity.uri)).filter(uri => uri != null).includes(_.get(mapping, "creator[0].uri"))
}
/**
* Returns the primary notation for a JSKOS Item. If there is no notation, it will return an empty string.
* Scheme notations will be uppercased.
*
* @param {object} item a JSKOS Item
* @param {string} type type of item (optional)
*/
export function notation(item, type) {
let notation
if (item && item.notation && item.notation.length) {
notation = item.notation[0]
if (isScheme(item) || type == "scheme") {
notation = notation.toUpperCase()
}
} else if (item && item.inScheme && item.inScheme[0] && item.uri) {
// Try to imply notation from scheme and concept URI
try {
const scheme = new ConceptScheme(item && item.inScheme && item.inScheme[0])
notation = scheme.notationFromUri(item.uri)
} catch (error) {
// Ignore error
}
}
return notation || ""
}
/**
* Returns the content of a language map for a JSKOS Item.
*
* @param {*} item a JSKOS Item
* @param {string} prop property of interest in the item
* @param {object} options options object:
* - `language`: preferred language
*/
export function languageMapContent(item, prop, { language } = {}) {
let languageMap = (item && prop) ? item[prop] : item
if (languageMap) {
if (languageMap[language]) {
return languageMap[language]
}
language = languagePreference.selectLanguage(languageMap)
if (language) {
return languageMap[language]
}
}
return null
}
/**
* Returns the prefLabel of a JSKOS Item. If there is no label, it will return the URI. If there is no URI, it will return an empty string.
*
* @param {*} item
* @param {object} options options object:
* - `fallbackToUri`: return URI if no prefLabel can be found (default: true)
* - `language`: preferred language
*/
export function prefLabel(item, options = {}) {
options = options || {}
const fallbackToUri = options.fallbackToUri == null ? true : options.fallbackToUri
return _.get(item, `prefLabel.${options.language}`)
|| languageMapContent(item, "prefLabel", options)
|| ((fallbackToUri && item && item.uri) ? item.uri : "")
}
/**
* Returns the definition of a JSKOS Item as an array. If there is no definition, an empty array will be returned.
*
* @param {*} item
* @param {object} options options object:
* - `language`: preferred language
*/
export function definition(item, options = {}) {
options = options || {}
let content = _.get(item, `definition.${options.language}`)
|| languageMapContent(item, "definition", options)
|| []
// Make sure an array is returned
if (_.isString(content)) {
content = [content]
}
return content
}
/**
* Returns whether a mapping registry has stored mappings (`true` = database) or not (`false` = recommendations).
*
* @param {object} registry JSKOS registry
*/
export function mappingRegistryIsStored(registry) {
return _.get(registry, "stored", _.get(registry, "constructor.stored", _.get(registry, "provider.constructor.stored", false)))
}
/**
* Returns the creator URI for an annotation.
*
* @param {object} annotation a JSKOS annotation
*/
export function annotationCreatorUri(annotation) {
if (_.isString(annotation.creator)) {
return annotation.creator
}
return annotation.creator && annotation.creator.id
}
/**
* Returns the craetor name for an annotation.
*
* @param {object} annotation a JSKOS annotation
*/
export function annotationCreatorName(annotation) {
return _.get(annotation, "creator.name") || ""
}
/**
* Matches an annotation's creator URI against a list of URIs (e.g. from a user).
*
* @param {object} annotation a JSKOS annotation
* @param {array} uris array of user URIs
*/
export function annotationCreatorMatches(annotation, uris) {
return !!(annotation && _.isString(annotation.creator) ? uris && uris.includes(annotation.creator) : uris && annotation.creator && uris.includes(annotation.creator.id))
}
export function guessSchemeFromNotation(notation, schemes) {
return schemes.filter(({notationPattern}) => {
if ((notationPattern||".+") === ".+") {
return false
}
return RegExp("^(" + notationPattern + ")$").test(notation)
})
}
export { guessObjectType, objectTypes }