@csvw-rdf-convertor/core
Version:
This library was generated with [Nx](https://nx.dev).
294 lines (293 loc) • 12.2 kB
JavaScript
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