@ulb-darmstadt/shacl-form
Version:
SHACL form generator
168 lines (158 loc) • 8.56 kB
text/typescript
import { BlankNode, DataFactory, NamedNode, Store } from 'n3'
import { Term } from '@rdfjs/types'
import { PREFIX_SHACL, RDF_PREDICATE_TYPE, OWL_PREDICATE_IMPORTS } from './constants'
import { ShaclProperty } from './property'
import { createShaclGroup } from './group'
import { v4 as uuidv4 } from 'uuid'
import { createShaclOrConstraint } from './constraints'
import { Config } from './config'
export class ShaclNode extends HTMLElement {
parent: ShaclNode | undefined
shaclSubject: NamedNode
nodeId: NamedNode | BlankNode
targetClass: NamedNode | undefined
owlImports: NamedNode[] = []
config: Config
constructor(shaclSubject: NamedNode, config: Config, valueSubject: NamedNode | BlankNode | undefined, parent?: ShaclNode, nodeKind?: NamedNode, label?: string) {
super()
this.parent = parent
this.config = config
this.shaclSubject = shaclSubject
let nodeId: NamedNode | BlankNode | undefined = valueSubject
if (!nodeId) {
// if no value subject given, create new node id with a type depending on own nodeKind or given parent property nodeKind
if (!nodeKind) {
const spec = config.shapesGraph.getObjects(shaclSubject, `${PREFIX_SHACL}nodeKind`, null)
if (spec.length) {
nodeKind = spec[0] as NamedNode
}
}
// if nodeKind is not set, but a value namespace is configured or if nodeKind is sh:IRI, then create a NamedNode
if ((nodeKind === undefined && config.attributes.valuesNamespace) || nodeKind?.id === `${PREFIX_SHACL}IRI`) {
// no requirements on node type, so create a NamedNode and use configured value namespace
nodeId = DataFactory.namedNode(config.attributes.valuesNamespace + uuidv4())
} else {
// otherwise create a BlankNode
nodeId = DataFactory.blankNode(uuidv4())
}
}
this.nodeId = nodeId
// check if the form already contains the node/value pair to prevent recursion
const id = JSON.stringify([shaclSubject, valueSubject])
if (valueSubject && config.renderedNodes.has(id)) {
// node/value pair is already rendered in the form, so just display a reference
if (label && config.attributes.collapse === null) {
const labelElem = document.createElement('label')
labelElem.innerText = label
this.appendChild(labelElem)
}
const anchor = document.createElement('a')
anchor.innerText = valueSubject.id
anchor.classList.add('ref-link')
anchor.onclick = () => {
// if anchor is clicked, scroll referenced shacl node into view
this.config.form.querySelector(`shacl-node[data-node-id='${this.nodeId.id}']`)?.scrollIntoView()
}
this.appendChild(anchor)
this.style.flexDirection = 'row'
} else {
if (valueSubject) {
config.renderedNodes.add(id)
}
this.dataset.nodeId = this.nodeId.id
const quads = config.shapesGraph.getQuads(shaclSubject, null, null, null)
let list: Term[] | undefined
if (this.config.attributes.showNodeIds !== null) {
const div = document.createElement('div')
div.innerText = `id: ${this.nodeId.id}`
div.classList.add('node-id-display')
this.appendChild(div)
}
for (const quad of quads) {
switch (quad.predicate.id) {
case `${PREFIX_SHACL}property`:
let parentElement: HTMLElement = this
// check if property belongs to a group
const groupRef = config.shapesGraph.getQuads(quad.object as Term, `${PREFIX_SHACL}group`, null, null)
if (groupRef.length > 0) {
const groupSubject = groupRef[0].object.value
if (config.groups.indexOf(groupSubject) > -1) {
// check if group element already exists, otherwise create it
let group = this.querySelector(`:scope > .shacl-group[data-subject='${groupSubject}']`) as HTMLElement
if (!group) {
group = createShaclGroup(groupSubject, config)
this.appendChild(group)
}
parentElement = group
} else {
console.warn('ignoring unknown group reference', groupRef[0], 'existing groups:', config.groups)
}
}
// delay creating/appending the property until we finished parsing the node.
// This is needed to have possible owlImports parsed before creating the property.
setTimeout(() => {
const property = new ShaclProperty(quad.object as NamedNode | BlankNode, this, config, valueSubject)
// do not add empty properties (i.e. properties with no instances). This can be the case e.g. in viewer mode when there is no data for the respective property.
if (property.childElementCount > 0) {
parentElement.appendChild(property)
}
})
break;
case `${PREFIX_SHACL}and`:
// inheritance via sh:and
list = config.lists[quad.object.value]
if (list?.length) {
for (const shape of list) {
this.prepend(new ShaclNode(shape as NamedNode, config, valueSubject, this))
}
}
else {
console.error('list not found:', quad.object.value, 'existing lists:', config.lists)
}
break;
case `${PREFIX_SHACL}node`:
// inheritance via sh:node
this.prepend(new ShaclNode(quad.object as NamedNode, config, valueSubject, this))
break;
case `${PREFIX_SHACL}targetClass`:
this.targetClass = quad.object as NamedNode
break;
case OWL_PREDICATE_IMPORTS.id:
this.owlImports.push(quad.object as NamedNode)
break;
case `${PREFIX_SHACL}or`:
list = config.lists[quad.object.value]
if (list?.length) {
this.appendChild(createShaclOrConstraint(list, this, config))
}
else {
console.error('list not found:', quad.object.value, 'existing lists:', config.lists)
}
break;
}
}
if (label) {
const header = document.createElement('h1')
header.innerText = label
this.prepend(header)
}
}
}
toRDF(graph: Store, subject?: NamedNode | BlankNode): (NamedNode | BlankNode) {
if (!subject) {
subject = this.nodeId
}
for (const shape of this.querySelectorAll(':scope > shacl-node, :scope > .shacl-group > shacl-node, :scope > shacl-property, :scope > .shacl-group > shacl-property')) {
(shape as ShaclNode | ShaclProperty).toRDF(graph, subject)
}
if (this.targetClass) {
graph.addQuad(subject, RDF_PREDICATE_TYPE, this.targetClass, this.config.valuesGraph)
}
// if this is the root shacl node, check if we should add one of the rdf:type or dcterms:conformsTo predicates
if (this.config.attributes.generateNodeShapeReference && !this.parent) {
graph.addQuad(subject, DataFactory.namedNode(this.config.attributes.generateNodeShapeReference), this.shaclSubject, this.config.valuesGraph)
}
return subject
}
}
window.customElements.define('shacl-node', ShaclNode)