@ulb-darmstadt/shacl-form 1.6.2 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/group.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { PREFIX_RDFS } from './constants'
2
+ import { Config } from './config'
3
+ import { findObjectValueByPredicate } from './util'
4
+
5
+ export function createShaclGroup(groupSubject: string, config: Config): HTMLElement {
6
+ const group = document.createElement('div')
7
+ group.dataset['subject'] = groupSubject
8
+ group.classList.add('shacl-group')
9
+ let name = groupSubject
10
+ const quads = config.shapesGraph.getQuads(groupSubject, null, null, null)
11
+ const label = findObjectValueByPredicate(quads, "label", PREFIX_RDFS, config.languages)
12
+ if (label) {
13
+ name = label
14
+ }
15
+ const order = findObjectValueByPredicate(quads, "order")
16
+ if (order) {
17
+ group.style.order = order
18
+ }
19
+ const header = document.createElement('h1')
20
+ header.innerText = name
21
+ group.appendChild(header)
22
+
23
+ if (config.attributes.collapse !== null) {
24
+ group.classList.add('collapsible')
25
+ if (config.attributes.collapse === 'open') {
26
+ group.classList.add('open')
27
+ }
28
+ header.classList.add('activator')
29
+ header.addEventListener('click', () => {
30
+ group.classList.toggle('open')
31
+ })
32
+
33
+ }
34
+ return group
35
+ }
package/src/loader.ts ADDED
@@ -0,0 +1,172 @@
1
+ import { Store, Parser, Quad, Prefixes, NamedNode, DataFactory } from 'n3'
2
+ import { toRDF } from 'jsonld'
3
+ import { DCTERMS_PREDICATE_CONFORMS_TO, OWL_PREDICATE_IMPORTS, RDF_PREDICATE_TYPE, SHACL_PREDICATE_CLASS, SHAPES_GRAPH } from './constants'
4
+ import { Config } from './config'
5
+ import { isURL } from './util'
6
+
7
+ // cache external data in module scope (and not in Loader instance) to avoid requesting
8
+ // them multiple times, e.g. when more than one shacl-form element is on the page
9
+ // that import the same resources
10
+ const loadedURLCache: Record<string, Promise<string>> = {}
11
+ const loadedClassesCache: Record<string, Promise<string>> = {}
12
+ let sharedShapesGraph: Store | undefined
13
+
14
+ export class Loader {
15
+ private config: Config
16
+ private loadedExternalUrls: string[] = []
17
+ private loadedClasses: string[] = []
18
+
19
+ constructor(config: Config) {
20
+ this.config = config
21
+ }
22
+
23
+ async loadGraphs() {
24
+ // clear local caches
25
+ this.loadedExternalUrls = []
26
+ this.loadedClasses = []
27
+
28
+ let shapesStore = sharedShapesGraph
29
+ const valuesStore = new Store()
30
+ this.config.prefixes = {}
31
+
32
+ const promises = [ this.importRDF(this.config.attributes.values ? this.config.attributes.values : this.config.attributes.valuesUrl ? this.fetchRDF(this.config.attributes.valuesUrl) : '', valuesStore, undefined, new Parser({ blankNodePrefix: '' })) ]
33
+ if (!shapesStore) {
34
+ shapesStore = new Store()
35
+ promises.push(this.importRDF(this.config.attributes.shapes ? this.config.attributes.shapes : this.config.attributes.shapesUrl ? this.fetchRDF(this.config.attributes.shapesUrl) : '', shapesStore, SHAPES_GRAPH))
36
+ }
37
+ await Promise.all(promises)
38
+
39
+ // if shapes graph is empty, but we have the following triples:
40
+ // <valueSubject> a <uri> or <valueSubject> dcterms:conformsTo <uri>
41
+ // then try to load the referenced object into the shapes graph
42
+ if (!sharedShapesGraph && shapesStore?.size == 0 && this.config.attributes.valuesSubject) {
43
+ const shapeCandidates = [
44
+ ...valuesStore.getObjects(this.config.attributes.valuesSubject, RDF_PREDICATE_TYPE, null),
45
+ ...valuesStore.getObjects(this.config.attributes.valuesSubject, DCTERMS_PREDICATE_CONFORMS_TO, null)
46
+ ]
47
+ const promises: Promise<void>[] = []
48
+ for (const uri of shapeCandidates) {
49
+ const url = this.toURL(uri.value)
50
+ if (url && this.loadedExternalUrls.indexOf(url) < 0) {
51
+ this.loadedExternalUrls.push(url)
52
+ promises.push(this.importRDF(this.fetchRDF(url), shapesStore, SHAPES_GRAPH))
53
+ }
54
+ }
55
+ try {
56
+ await Promise.allSettled(promises)
57
+ } catch (e) {
58
+ console.warn(e)
59
+ }
60
+ }
61
+
62
+ this.config.shapesGraph = shapesStore
63
+ this.config.dataGraph = valuesStore
64
+ }
65
+
66
+ async importRDF(input: string | Promise<string>, store: Store, graph?: NamedNode, parser?: Parser) {
67
+ const p = parser || new Parser()
68
+ const parse = async (text: string) => {
69
+ const dependencies: Promise<void>[] = []
70
+ await new Promise((resolve, reject) => {
71
+ p.parse(text, (error: Error, quad: Quad, prefixes: Prefixes) => {
72
+ if (error) {
73
+ return reject(error)
74
+ }
75
+ if (quad) {
76
+ store.add(new Quad(quad.subject, quad.predicate, quad.object, graph))
77
+ // check if this is an owl:imports predicate and try to load the url
78
+ if (this.config.attributes.ignoreOwlImports === null && OWL_PREDICATE_IMPORTS.equals(quad.predicate)) {
79
+ const url = this.toURL(quad.object.value)
80
+ // import url only once
81
+ if (url && this.loadedExternalUrls.indexOf(url) < 0) {
82
+ this.loadedExternalUrls.push(url)
83
+ // import into separate graph
84
+ dependencies.push(this.importRDF(this.fetchRDF(url), store, DataFactory.namedNode(url), parser))
85
+ }
86
+ }
87
+ // check if this is an sh:class predicate and invoke class instance provider
88
+ if (this.config.classInstanceProvider && SHACL_PREDICATE_CLASS.equals(quad.predicate)) {
89
+ const className = quad.object.value
90
+ // import class definitions only once
91
+ if (this.loadedClasses.indexOf(className) < 0) {
92
+ let promise: Promise<string>
93
+ // check if class is in module scope cache
94
+ if (className in loadedClassesCache) {
95
+ promise = loadedClassesCache[className]
96
+ } else {
97
+ promise = this.config.classInstanceProvider(className)
98
+ loadedClassesCache[className] = promise
99
+ }
100
+ this.loadedClasses.push(className)
101
+ dependencies.push(this.importRDF(promise, store, graph, parser))
102
+ }
103
+ }
104
+ return
105
+ }
106
+ if (prefixes) {
107
+ this.config.registerPrefixes(prefixes)
108
+ }
109
+ resolve(null)
110
+ })
111
+ })
112
+ try {
113
+ await Promise.allSettled(dependencies)
114
+ } catch (e) {
115
+ console.warn(e)
116
+ }
117
+ }
118
+
119
+ if (input instanceof Promise) {
120
+ input = await input
121
+ }
122
+ if (input) {
123
+ try {
124
+ // check if input is JSON
125
+ // @ts-ignore, because result of toRDF is a string and not an object
126
+ input = await toRDF(JSON.parse(input), { format: 'application/n-quads' }) as string
127
+ } catch(_) {
128
+ // NOP, it wasn't JSON
129
+ }
130
+ await parse(input)
131
+ }
132
+ }
133
+
134
+ async fetchRDF(url: string): Promise<string> {
135
+ // try to load from cache first
136
+ if (url in loadedURLCache) {
137
+ return loadedURLCache[url]
138
+ }
139
+ const promise = fetch(url, {
140
+ headers: {
141
+ 'Accept': 'text/turtle, application/trig, application/n-triples, application/n-quads, text/n3, application/ld+json'
142
+ },
143
+ }).then(resp => resp.text())
144
+ loadedURLCache[url] = promise
145
+ return promise
146
+ }
147
+
148
+ toURL(id: string): string | null {
149
+ if (isURL(id)) {
150
+ return id
151
+ }
152
+ if (this.config.prefixes) {
153
+ const splitted = id.split(':')
154
+ if (splitted.length === 2) {
155
+ const prefix = this.config.prefixes[splitted[0]]
156
+ if (prefix) {
157
+ // need to ignore type check. 'prefix' is a string and not a NamedNode<string> (seems to be a bug in n3 typings)
158
+ // @ts-ignore
159
+ id = id.replace(`${splitted[0]}:`, prefix)
160
+ if (isURL(id)) {
161
+ return id
162
+ }
163
+ }
164
+ }
165
+ }
166
+ return null
167
+ }
168
+ }
169
+
170
+ export function setSharedShapesGraph(graph: Store) {
171
+ sharedShapesGraph = graph
172
+ }
package/src/node.ts ADDED
@@ -0,0 +1,167 @@
1
+ import { BlankNode, DataFactory, NamedNode, Store } from 'n3'
2
+ import { Term } from '@rdfjs/types'
3
+ import { PREFIX_SHACL, RDF_PREDICATE_TYPE, OWL_PREDICATE_IMPORTS } from './constants'
4
+ import { ShaclProperty } from './property'
5
+ import { createShaclGroup } from './group'
6
+ import { v4 as uuidv4 } from 'uuid'
7
+ import { createShaclOrConstraint } from './constraints'
8
+ import { Config } from './config'
9
+
10
+ export class ShaclNode extends HTMLElement {
11
+ parent: ShaclNode | undefined
12
+ shaclSubject: NamedNode
13
+ nodeId: NamedNode | BlankNode
14
+ targetClass: NamedNode | undefined
15
+ owlImports: NamedNode[] = []
16
+ config: Config
17
+
18
+ constructor(shaclSubject: NamedNode, config: Config, valueSubject: NamedNode | BlankNode | undefined, parent?: ShaclNode, nodeKind?: NamedNode, label?: string) {
19
+ super()
20
+
21
+ this.parent = parent
22
+ this.config = config
23
+ this.shaclSubject = shaclSubject
24
+ let nodeId: NamedNode | BlankNode | undefined = valueSubject
25
+ if (!nodeId) {
26
+ // if no value subject given, create new node id with a type depending on own nodeKind or given parent property nodeKind
27
+ if (!nodeKind) {
28
+ const spec = config.shapesGraph.getObjects(shaclSubject, `${PREFIX_SHACL}nodeKind`, null)
29
+ if (spec.length) {
30
+ nodeKind = spec[0] as NamedNode
31
+ }
32
+ }
33
+ // if nodeKind is not set, but a value namespace is configured or if nodeKind is sh:IRI, then create a NamedNode
34
+ if ((nodeKind === undefined && config.attributes.valuesNamespace) || nodeKind?.id === `${PREFIX_SHACL}IRI`) {
35
+ // no requirements on node type, so create a NamedNode and use configured value namespace
36
+ nodeId = DataFactory.namedNode(config.attributes.valuesNamespace + uuidv4())
37
+ } else {
38
+ // otherwise create a BlankNode
39
+ nodeId = DataFactory.blankNode(uuidv4())
40
+ }
41
+ }
42
+ this.nodeId = nodeId
43
+
44
+ // check if the form already contains the node/value pair to prevent recursion
45
+ const id = JSON.stringify([shaclSubject, valueSubject])
46
+ if (valueSubject && config.renderedNodes.has(id)) {
47
+ // node/value pair is already rendered in the form, so just display a reference
48
+ if (label && config.attributes.collapse === null) {
49
+ const labelElem = document.createElement('label')
50
+ labelElem.innerText = label
51
+ this.appendChild(labelElem)
52
+ }
53
+ const anchor = document.createElement('a')
54
+ anchor.innerText = valueSubject.id
55
+ anchor.classList.add('ref-link')
56
+ anchor.onclick = () => {
57
+ // if anchor is clicked, scroll referenced shacl node into view
58
+ this.config.form.querySelector(`shacl-node[data-node-id='${this.nodeId.id}']`)?.scrollIntoView()
59
+ }
60
+ this.appendChild(anchor)
61
+ this.style.flexDirection = 'row'
62
+ } else {
63
+ if (valueSubject) {
64
+ config.renderedNodes.add(id)
65
+ }
66
+ this.dataset.nodeId = this.nodeId.id
67
+ const quads = config.shapesGraph.getQuads(shaclSubject, null, null, null)
68
+ let list: Term[] | undefined
69
+
70
+ if (this.config.attributes.showNodeIds !== null) {
71
+ const div = document.createElement('div')
72
+ div.innerText = `id: ${this.nodeId.id}`
73
+ div.classList.add('node-id-display')
74
+ this.appendChild(div)
75
+ }
76
+
77
+ for (const quad of quads) {
78
+ switch (quad.predicate.id) {
79
+ case `${PREFIX_SHACL}property`:
80
+ let parentElement: HTMLElement = this
81
+ // check if property belongs to a group
82
+ const groupRef = config.shapesGraph.getQuads(quad.object as Term, `${PREFIX_SHACL}group`, null, null)
83
+ if (groupRef.length > 0) {
84
+ const groupSubject = groupRef[0].object.value
85
+ if (config.groups.indexOf(groupSubject) > -1) {
86
+ // check if group element already exists, otherwise create it
87
+ let group = this.querySelector(`:scope > .shacl-group[data-subject='${groupSubject}']`) as HTMLElement
88
+ if (!group) {
89
+ group = createShaclGroup(groupSubject, config)
90
+ this.appendChild(group)
91
+ }
92
+ parentElement = group
93
+ } else {
94
+ console.warn('ignoring unknown group reference', groupRef[0], 'existing groups:', config.groups)
95
+ }
96
+ }
97
+ // delay creating/appending the property until we finished parsing the node.
98
+ // This is needed to have possible owlImports parsed before creating the property.
99
+ setTimeout(() => {
100
+ const property = new ShaclProperty(quad.object as NamedNode | BlankNode, this, config, valueSubject)
101
+ // 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.
102
+ if (property.childElementCount > 0) {
103
+ parentElement.appendChild(property)
104
+ }
105
+ })
106
+ break;
107
+ case `${PREFIX_SHACL}and`:
108
+ // inheritance via sh:and
109
+ list = config.lists[quad.object.value]
110
+ if (list?.length) {
111
+ for (const shape of list) {
112
+ this.prepend(new ShaclNode(shape as NamedNode, config, valueSubject, this))
113
+ }
114
+ }
115
+ else {
116
+ console.error('list not found:', quad.object.value, 'existing lists:', config.lists)
117
+ }
118
+ break;
119
+ case `${PREFIX_SHACL}node`:
120
+ // inheritance via sh:node
121
+ this.prepend(new ShaclNode(quad.object as NamedNode, config, valueSubject, this))
122
+ break;
123
+ case `${PREFIX_SHACL}targetClass`:
124
+ this.targetClass = quad.object as NamedNode
125
+ break;
126
+ case OWL_PREDICATE_IMPORTS.id:
127
+ this.owlImports.push(quad.object as NamedNode)
128
+ break;
129
+ case `${PREFIX_SHACL}or`:
130
+ list = config.lists[quad.object.value]
131
+ if (list?.length) {
132
+ this.appendChild(createShaclOrConstraint(list, this, config))
133
+ }
134
+ else {
135
+ console.error('list not found:', quad.object.value, 'existing lists:', config.lists)
136
+ }
137
+ break;
138
+ }
139
+ }
140
+
141
+ if (label) {
142
+ const header = document.createElement('h1')
143
+ header.innerText = label
144
+ this.prepend(header)
145
+ }
146
+ }
147
+ }
148
+
149
+ toRDF(graph: Store, subject?: NamedNode | BlankNode): (NamedNode | BlankNode) {
150
+ if (!subject) {
151
+ subject = this.nodeId
152
+ }
153
+ for (const shape of this.querySelectorAll(':scope > shacl-node, :scope > .shacl-group > shacl-node, :scope > shacl-property, :scope > .shacl-group > shacl-property')) {
154
+ (shape as ShaclNode | ShaclProperty).toRDF(graph, subject)
155
+ }
156
+ if (this.targetClass) {
157
+ graph.addQuad(subject, RDF_PREDICATE_TYPE, this.targetClass)
158
+ }
159
+ // if this is the root shacl node, check if we should add one of the rdf:type or dcterms:conformsTo predicates
160
+ if (this.config.attributes.generateNodeShapeReference && !this.parent) {
161
+ graph.addQuad(subject, DataFactory.namedNode(this.config.attributes.generateNodeShapeReference), this.shaclSubject)
162
+ }
163
+ return subject
164
+ }
165
+ }
166
+
167
+ window.customElements.define('shacl-node', ShaclNode)
package/src/plugin.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { ShaclPropertyTemplate } from './property-template'
2
+ import { Term } from '@rdfjs/types'
3
+
4
+ // store plugins in module scope so that they apply to all shacl-form elements
5
+ const plugins: Record<string, Plugin> = {}
6
+
7
+ export function registerPlugin(plugin: Plugin) {
8
+ if (plugin.predicate === undefined && plugin.datatype === undefined) {
9
+ console.warn('not registering plugin because it does neither define "predicate" nor "datatype"', plugin)
10
+ } else {
11
+ plugins[`${plugin.predicate}^${plugin.datatype}`] = plugin
12
+ }
13
+ }
14
+
15
+ export function listPlugins(): Plugin[] {
16
+ return Object.entries(plugins).map((value: [_: string, plugin: Plugin]) => { return value[1] })
17
+ }
18
+
19
+ export function findPlugin(predicate: string | undefined, datatype: string | undefined): Plugin | undefined {
20
+ // first try to find plugin with matching predicate and datatype
21
+ let plugin = plugins[`${predicate}^${datatype}`]
22
+ if (plugin) {
23
+ return plugin
24
+ }
25
+ // now prefer predicate over datatype
26
+ plugin = plugins[`${predicate}^${undefined}`]
27
+ if (plugin) {
28
+ return plugin
29
+ }
30
+ // last, try to find plugin with matching datatype
31
+ return plugins[`${undefined}^${datatype}`]
32
+ }
33
+
34
+ export type PluginOptions = {
35
+ predicate?: string
36
+ datatype?: string
37
+ }
38
+
39
+ export abstract class Plugin {
40
+ predicate: string | undefined
41
+ datatype: string | undefined
42
+ stylesheet: CSSStyleSheet | undefined
43
+
44
+ constructor(options: PluginOptions, css?: string) {
45
+ this.predicate = options.predicate
46
+ this.datatype = options.datatype
47
+ if (css) {
48
+ this.stylesheet = new CSSStyleSheet()
49
+ this.stylesheet.replaceSync(css)
50
+ }
51
+ }
52
+
53
+ abstract createEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement
54
+
55
+ createViewer(template: ShaclPropertyTemplate, value: Term): HTMLElement {
56
+ return template.config.theme.createViewer(template.label, value, template)
57
+ }
58
+ }
59
+
60
+ export type ClassInstanceProvider = (clazz: string) => Promise<string>
@@ -0,0 +1,26 @@
1
+ import { Plugin, PluginOptions } from '../plugin'
2
+ import { ShaclPropertyTemplate } from '../property-template'
3
+
4
+ export class FileUploadPlugin extends Plugin {
5
+ onChange: (event: Event) => void
6
+ fileType: string | undefined
7
+
8
+ constructor(options: PluginOptions, onChange: (event: Event) => void, fileType?: string) {
9
+ super(options)
10
+ this.onChange = onChange
11
+ this.fileType = fileType
12
+ }
13
+
14
+ createEditor(template: ShaclPropertyTemplate): HTMLElement {
15
+ const required = template.minCount !== undefined && template.minCount > 0
16
+ const editor = template.config.theme.createFileEditor(template.label, null, required, template)
17
+ editor.addEventListener('change', event => {
18
+ event.stopPropagation()
19
+ this.onChange(event)
20
+ })
21
+ if (this.fileType) {
22
+ editor.querySelector('input[type="file"]')?.setAttribute('accept', this.fileType)
23
+ }
24
+ return editor
25
+ }
26
+ }
@@ -0,0 +1,19 @@
1
+ import { Plugin, PluginOptions } from '../plugin'
2
+ import { Term } from '@rdfjs/types'
3
+
4
+ import { ShaclPropertyTemplate } from '../property-template'
5
+ import { InputListEntry } from '../theme'
6
+
7
+ export class FixedListPlugin extends Plugin {
8
+ entries: InputListEntry[]
9
+
10
+ constructor(options: PluginOptions, entries: InputListEntry[]) {
11
+ super(options)
12
+ this.entries = entries
13
+ }
14
+
15
+ createEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
16
+ const required = template.minCount !== undefined && template.minCount > 0
17
+ return template.config.theme.createListEditor(template.label, value || null, required, this.entries, template)
18
+ }
19
+ }