UNPKG

@ulb-darmstadt/shacl-form

Version:
275 lines (247 loc) 11.1 kB
import { Literal, NamedNode, Prefixes, Quad, Store } from 'n3' import { DATA_GRAPH, PREFIX_FOAF, PREFIX_RDF, PREFIX_RDFS, PREFIX_SHACL, PREFIX_SKOS, RDFS_PREDICATE_SUBCLASS_OF, RDF_PREDICATE_TYPE, SHAPES_GRAPH, SKOS_PREDICATE_BROADER, SKOS_PREDICATE_NARROWER } from './constants' import { Term } from '@rdfjs/types' import { InputListEntry } from './theme' import { ShaclPropertyTemplate } from './property-template' import { ShaclNode } from './node' export function findObjectValueByPredicate(quads: Quad[], predicate: string, prefix: string = PREFIX_SHACL, languages?: string[]): string { let result = '' const object = findObjectByPredicate(quads, predicate, prefix, languages) if (object) { result = object.value } return result } export function findObjectByPredicate(quads: Quad[], predicate: string, prefix: string = PREFIX_SHACL, languages?: string[]): Term | undefined { let candidate: Term | undefined const prefixedPredicate = prefix + predicate if (languages?.length) { for (const language of languages) { for (const quad of quads) { if (quad.predicate.value === prefixedPredicate) { if (quad.object.id.endsWith(`@${language}`)) { return quad.object } else if (quad.object.id.indexOf('@') < 0) { candidate = quad.object } else if (!candidate) { candidate = quad.object } } } } } else { for (const quad of quads) { if (quad.predicate.value === prefixedPredicate) { return quad.object } } } return candidate } export function focusFirstInputElement(context: HTMLElement) { (context.querySelector('.editor') as HTMLElement)?.focus() } export function findLabel(quads: Quad[], languages: string[]): string { return findObjectValueByPredicate(quads, 'prefLabel', PREFIX_SKOS, languages) || findObjectValueByPredicate(quads, 'label', PREFIX_RDFS, languages) || findObjectValueByPredicate(quads, 'name', PREFIX_FOAF, languages) } export function createInputListEntries(subjects: Term[], shapesGraph: Store, languages: string[]): InputListEntry[] { const entries: InputListEntry[] = [] for (const subject of subjects) { entries.push({ value: subject, label: findLabel(shapesGraph.getQuads(subject, null, null, null), languages), children: [] }) } return entries } export function removePrefixes(id: string, prefixes: Prefixes): string { for (const key in prefixes) { // need to ignore type check. 'prefix' is a string and not a NamedNode<string> (seems to be a bug in n3 typings) // @ts-ignore id = id.replace(prefixes[key], '') } return id } function findClassInstancesFromOwlImports(clazz: NamedNode, context: ShaclNode | ShaclPropertyTemplate, shapesGraph: Store, instances: Term[], alreadyCheckedImports = new Set<string>()) { for (const owlImport of context.owlImports) { if (!alreadyCheckedImports.has(owlImport.id)) { alreadyCheckedImports.add(owlImport.id) instances.push(...shapesGraph.getSubjects(RDF_PREDICATE_TYPE, clazz, owlImport)) } } if (context.parent) { findClassInstancesFromOwlImports(clazz, context.parent, shapesGraph, instances, alreadyCheckedImports) } } export function findInstancesOf(clazz: NamedNode, template: ShaclPropertyTemplate): InputListEntry[] { // if template has sh:in, then just use that as class instances if (template.shaclIn) { const list = template.config.lists[template.shaclIn] return createInputListEntries(list?.length ? list : [], template.config.store, template.config.languages) } else { // find instances in the shapes graph const instances = template.config.store.getSubjects(RDF_PREDICATE_TYPE, clazz, SHAPES_GRAPH) // find instances in the data graph instances.push(...template.config.store.getSubjects(RDF_PREDICATE_TYPE, clazz, DATA_GRAPH)) // find instances in imported taxonomies findClassInstancesFromOwlImports(clazz, template, template.config.store, instances) // initialize structures needed for building a class instance hierarchy const nodes = new Map<string, InputListEntry>() // URI -> InputListEntry const childToParent = new Map<string, string>() // URI -> parentURI // initialize all instances as InputListEntry's with no children for (const instance of instances) { nodes.set(instance.id, { value: instance, label: findLabel(template.config.store.getQuads(instance, null, null, null), template.config.languages), children: [] }) } // record broader/narrower/subClassOf hierarchical relationships for (const instance of instances) { for (const parent of template.config.store.getObjects(instance, SKOS_PREDICATE_BROADER, null)) { if (nodes.has(parent.id)) { childToParent.set(instance.id, parent.id) } } for (const child of template.config.store.getObjects(instance, SKOS_PREDICATE_NARROWER, null)) { if (nodes.has(child.id)) { childToParent.set(child.id, instance.id) } } for (const parent of template.config.store.getObjects(instance, RDFS_PREDICATE_SUBCLASS_OF, null)) { if (nodes.has(parent.id)) { childToParent.set(instance.id, parent.id) } } } // build hierarchy by nesting children under parents for (const [child, parent] of childToParent.entries()) { nodes.get(parent)?.children?.push(nodes.get(child)!) } // find root nodes (no parent relationship) const roots: InputListEntry[] = [] for (const [uri, node] of nodes.entries()) { if (!childToParent.has(uri)) { roots.push(node) } } // add sub class instances for (const subClass of template.config.store.getSubjects(RDFS_PREDICATE_SUBCLASS_OF, clazz, null)) { roots.push(...findInstancesOf(subClass as NamedNode, template)) } return roots } } export function isURL(input: string): boolean { let url: URL try { url = new URL(input) } catch (_) { return false } return url.protocol === 'http:' || url.protocol === 'https:' } export function prioritizeByLanguage(languages: string[], text1?: Literal, text2?: Literal): Literal | undefined { if (text1 === undefined) { return text2 } if (text2 === undefined) { return text1 } const index1 = languages.indexOf(text1.language) if (index1 < 0) { return text2 } const index2 = languages.indexOf(text2.language) if (index2 < 0) { return text1 } return index2 > index1 ? text1 : text2 } /* This code is taken from https://github.com/rdfjs/N3.js/blob/main/src/N3Store.js and adapted to allow rdf:type triples in lists. Can be removed as soon as https://github.com/rdfjs/N3.js/issues/546 is fixed. */ export function extractLists(store: Store, { remove = false, ignoreErrors = false } = {}) { const lists: Record<string, Term[]> = {} // has scalar keys so could be a simple Object const onError = ignoreErrors ? (() => true) : ((node: Term, message: string) => { throw new Error(`${node.value} ${message}`) }) // Traverse each list from its tail const tails = store.getQuads(null, PREFIX_RDF + 'rest', PREFIX_RDF + 'nil', null) const toRemove = remove ? [...tails] : [] tails.forEach(tailQuad => { const items = [] // the members found as objects of rdf:first quads let malformed = false // signals whether the current list is malformed let head // the head of the list (_:b1 in above example) let headPos: string // set to subject or object when head is set const graph = tailQuad.graph // make sure list is in exactly one graph // Traverse the list from tail to end let current: Term | null = tailQuad.subject while (current && !malformed) { const objectQuads = store.getQuads(null, null, current, null) const subjectQuads = store.getQuads(current, null, null, null).filter(quad => !quad.predicate.equals(RDF_PREDICATE_TYPE)) let quad, first = null, rest = null, parent = null // Find the first and rest of this list node for (let i = 0; i < subjectQuads.length && !malformed; i++) { quad = subjectQuads[i] if (!quad.graph.equals(graph)) malformed = onError(current, 'not confined to single graph') else if (head) malformed = onError(current, 'has non-list arcs out') // one rdf:first else if (quad.predicate.value === PREFIX_RDF + 'first') { if (first) malformed = onError(current, 'has multiple rdf:first arcs') else toRemove.push(first = quad) } // one rdf:rest else if (quad.predicate.value === PREFIX_RDF + 'rest') { if (rest) malformed = onError(current, 'has multiple rdf:rest arcs') else toRemove.push(rest = quad) } // alien triple else if (objectQuads.length) malformed = onError(current, 'can\'t be subject and object') else { head = quad // e.g. { (1 2 3) :p :o } headPos = 'subject' } } // { :s :p (1 2) } arrives here with no head // { (1 2) :p :o } arrives here with head set to the list. for (let i = 0; i < objectQuads.length && !malformed; ++i) { quad = objectQuads[i] if (head) malformed = onError(current, 'can\'t have coreferences') // one rdf:rest else if (quad.predicate.value === PREFIX_RDF + 'rest') { if (parent) malformed = onError(current, 'has incoming rdf:rest arcs') else parent = quad } else { head = quad // e.g. { :s :p (1 2) } headPos = 'object' } } // Store the list item and continue with parent if (!first) malformed = onError(current, 'has no list head') else items.unshift(first.object) current = parent && parent.subject } // Don't remove any quads if the list is malformed if (malformed) remove = false // Store the list under the value of its head else if (head) { // @ts-ignore lists[head[headPos].value] = items } }) // Remove list quads if requested if (remove) store.removeQuads(toRemove) return lists }