UNPKG

@csvw-rdf-convertor/core

Version:

This library was generated with [Nx](https://nx.dev).

294 lines (293 loc) 12.2 kB
import jsonld from 'jsonld'; import { csvwNs } from './types/descriptor/namespace.js'; import { replaceUrl } from './utils/replace-url.js'; import { DataFactory } from 'n3'; import { validate as bcp47Validate } from 'bcp47-validate'; const { quad, fromQuad } = DataFactory; /** * Normalize the JSON-LD descriptor to a specific format. * @param descriptor descriptor either as parsed or stringified JSON * @param options options for the conversion * @returns wrapper containing the normalized descriptor */ export async function normalizeDescriptor(descriptor, options, issueTracker, url) { const docLoader = async (url)=>{ url = replaceUrl(url, options.pathOverrides); return { document: JSON.parse(await options.resolveJsonldFn(url, options.baseIri)), documentUrl: url }; }; let parsedDescriptor; if (typeof descriptor === 'string') { parsedDescriptor = JSON.parse(descriptor); } else { parsedDescriptor = descriptor; } if (parsedDescriptor['@id'] && typeof parsedDescriptor['@id'] !== 'string' && url) { issueTracker.addWarning(`Invalid @id: ${JSON.stringify(parsedDescriptor['@id'])}`); parsedDescriptor['@id'] = url; } validateIdsTypesLangmaps(parsedDescriptor, issueTracker); validateLanguage(parsedDescriptor, issueTracker); const originalCtx = parsedDescriptor['@context']; const expanded = await jsonld.expand(parsedDescriptor, { documentLoader: docLoader }); await loadReferencedSubdescriptors(expanded, docLoader, options, originalCtx, issueTracker); const compactedExpanded = await jsonld.compact(expanded, {}, { documentLoader: docLoader }); const [internal, idMap] = await splitExternalProps(compactedExpanded, issueTracker); return new DescriptorWrapper(await compactCsvwNs(internal, docLoader, parsedDescriptor['@context'] || csvwNs), idMap); } /** * Removes `@id` properties which are not strings from the descriptor, as well as invalid language maps. * This is needed because the JSON-LD parser throws errors but we want to ignore them. * Also validates `@id` and `@type` properties to ensure they are not blank nodes. * @param obj descriptor as parsed expanded JSON-LD */ function validateIdsTypesLangmaps(obj, issueTracker) { for(const key in obj){ if (key === '@id') { if (typeof obj[key] !== 'string') { issueTracker.addWarning(`Invalid @id: ${JSON.stringify(obj[key])}`); obj[key] = ''; } else if (obj[key].startsWith('_:')) { issueTracker.addError('@id cannot be a blank node'); } } else if (key === '@type') { if (!URL.canParse(obj[key]) && ![ 'Column', 'Dialect', 'Table', 'TableGroup', 'Schema', 'Template' ].includes(obj[key])) { issueTracker.addError(`Invalid @type: ${JSON.stringify(obj[key])}`); } else if (obj[key].startsWith('_:')) { issueTracker.addError('@type cannot be a blank node'); } } else if (key === 'titles' || key === csvwNs + '#title') { var _obj_key; const titles = (_obj_key = obj[key]) != null ? _obj_key : obj[csvwNs + '#title']; if (titles && typeof titles === 'object') { for(const key in titles){ if (typeof titles[key] !== 'string' && (!Array.isArray(titles[key]) || titles[key].some((t)=>typeof t !== 'string'))) { issueTracker.addWarning(`Invalid title: ${JSON.stringify(titles[key])}`); delete titles[key]; } } } } else if (key === '@language') { if (!('@value' in obj)) { issueTracker.addError(`A @language property must not be used on an object unless it also has a @value property.`); } } else if (key.startsWith('@') && ![ '@set', '@list', '@value', '@context' ].includes(key)) { issueTracker.addError(`Invalid keyword property: ${key}`); } else if (typeof obj[key] === 'object' && key !== '@context') { validateIdsTypesLangmaps(obj[key], issueTracker); } } } /** * Validates the language tags in the descriptor and removes invalid ones. * @param obj descriptor as parsed expanded JSON-LD */ function validateLanguage(obj, issueTracker) { const ctx = Array.isArray(obj['@context']) ? obj['@context'] : [ obj['@context'] ]; for (const c of ctx){ if (typeof c === 'object' && (c == null ? void 0 : c['@language'])) { if (!bcp47Validate(c['@language'])) { issueTracker.addWarning(`Invalid language tag: ${c['@language']}`); delete c['@language']; } } } } /** * The descriptor can include references to other descriptors. * This function loads these referenced descriptors and replaces the references in the original descriptor. * @param descriptor descriptor as parsed expanded JSON-LD */ async function loadReferencedSubdescriptors(descriptor, docLoader, options, originalCtx, issueTracker) { const root = descriptor[0]; const objects = [ root ]; if (csvwNs + '#table' in root) { objects.push(...root[csvwNs + '#table']); } const base = Array.isArray(originalCtx) && originalCtx[1]['@base'] || ''; for (const object of objects){ for (const key of [ '#tableSchema', '#dialect' ]){ if (csvwNs + key in object) { const refContainer = object[csvwNs + key][0]; if ('@id' in refContainer && Object.keys(refContainer).length === 1) { // is a reference const doc = await options.resolveJsonldFn(replaceUrl(base + refContainer['@id'], options.pathOverrides), options.baseIri); const parsed = JSON.parse(doc); if (parsed['@id'] && typeof parsed['@id'] !== 'string') { parsed['@id'] = refContainer['@id']; } validateIdsTypesLangmaps(parsed, issueTracker); validateLanguage(parsed, issueTracker); const subdescriptor = await jsonld.expand({ '@context': originalCtx, [csvwNs + key]: parsed }, { documentLoader: docLoader }); object[csvwNs + key] = subdescriptor[0][csvwNs + key]; } } } } } /** * Compacts the descriptor to remove the `csvw:` prefix from the properties. * @param descriptor descriptor as parsed JSON * @param docLoader loader function for the `jsonld` library */ async function compactCsvwNs(descriptor, docLoader, ctx) { const compacted = await jsonld.compact(descriptor, csvwNs, { documentLoader: docLoader }); shortenProps(compacted); compacted['@context'] = ctx; return compacted; } /** * Removes the `csvw:` prefix from properties of `descriptor` recursively in place. * @param descriptor any object to be shortened */ function shortenProps(descriptor) { if (typeof descriptor !== 'object' || descriptor === null) return; if (Array.isArray(descriptor)) { for (const item of descriptor){ shortenProps(item); } } for(const key in descriptor){ if (key.startsWith('csvw:')) { descriptor[key.slice(5)] = descriptor[key]; delete descriptor[key]; } else { shortenProps(descriptor[key]); } } } let externalSubjCounter = 0; /** * removes non-csvw properties and csvw:notes from `object` and stores them in `quadMap` as RDF quads with temporary subject. * The temporary subject id is stored in the csvw:notes property of the object. * @param object object to be split * @param quadMap map to store the RDF quads with temporary subject * @returns the object without non-csvw properties and the map of RDF quads */ async function splitExternalProps(object, issueTracker, quadMap = new Map()) { if (typeof object !== 'object' || object === null) { return [ object, quadMap ]; } if (Array.isArray(object)) { const result = []; for (const item of object){ result.push((await splitExternalProps(item, issueTracker, quadMap))[0]); } return [ result, quadMap ]; } const internal = {}; const external = {}; for(const key in object){ if (key.startsWith(csvwNs + '#')) { if (key === csvwNs + '#note') { for (const note of Array.isArray(object[key]) ? object[key] : [ object[key] ]){ external[csvwNs + '#note'] = note; } } else { internal[key] = (await splitExternalProps(object[key], issueTracker, quadMap))[0]; } } else if (key.startsWith('@')) { internal[key] = (await splitExternalProps(object[key], issueTracker, quadMap))[0]; if (key !== '@id') { external[key] = object[key]; } if (key === '@type' && object[key].startsWith('_:')) { issueTracker.addError('@type cannot be a blank node', false); } } else { external[key] = object[key]; } } if (!('@list' in internal || '@value' in internal || '@set' in internal) && Object.keys(external).length) { const externalId = `https://github.com/S0ft1/CSVW-RDF-convertor/externalsubj/${externalSubjCounter++}`; internal[csvwNs + '#note'] = externalId; external['@id'] = externalId; quadMap.set(externalId, await jsonldToRdf(external)); } return [ internal, quadMap ]; } async function jsonldToRdf(node) { // these are not full Quads, but they have very similar data structure const quads = await jsonld.toRDF(node); return quads.map((q)=>{ q.termType = 'Quad'; return fromQuad(q); }); } /** Class for manipulating the descriptor */ export class DescriptorWrapper { /** does the descriptor describe table group or table? */ get isTableGroup() { return this._isTableGroup(this.descriptor); } _isTableGroup(x) { return 'tables' in x; } /** iterator over referenced tables */ *getTables() { if (this._isTableGroup(this.descriptor)) { for (const element of this.descriptor.tables){ yield element; } } else { yield this.descriptor; } } /** * The CSVW descriptor can include extra non-csvw properties at various levels. The inner descriptor * does not include these properties, but they are stored separately as RDF quads with temporary subject. * The descriptor contains reference to this subject in a `notes` property. * This function retrieves these properties and inserts them into the RDF graph with a proper subject. * @param sourceId temporary subject id for the external properties * @param newSubj subject for the external properties * @param store store to insert the external properties into */ *getExternalProps(sourceId, newSubj) { const quads = this.externalProps.get(sourceId); if (!quads) return; for (const q of quads){ if (q.subject.value === sourceId) { yield quad(newSubj, q.predicate, q.object, q.graph); } else { yield q; } } } constructor(descriptor, /** extra non-csvw properties from the original descriptor */ externalProps){ this.descriptor = descriptor; this.externalProps = externalProps; } } //# sourceMappingURL=descriptor.js.map