@ulb-darmstadt/shacl-form 1.10.4 → 2.0.0-rc1
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/dist/bundle.js +412 -0
- package/dist/constants.d.ts +16 -16
- package/dist/constraints.d.ts +2 -2
- package/dist/form.d.ts +4 -3
- package/dist/index.js +62 -0
- package/dist/plugins/assets/plugin-VN3CfgGe.js +1 -0
- package/dist/plugins/file-upload.js +1 -0
- package/dist/plugins/leaflet.d.ts +3 -1
- package/dist/plugins/leaflet.js +5 -708
- package/dist/plugins/map-util.js +1 -0
- package/dist/{themes/default.d.ts → theme.default.d.ts} +2 -2
- package/package.json +20 -26
- package/dist/form-bootstrap.d.ts +0 -5
- package/dist/form-bootstrap.js +0 -413
- package/dist/form-default.d.ts +0 -5
- package/dist/form-default.js +0 -402
- package/dist/form-material.d.ts +0 -5
- package/dist/form-material.js +0 -722
- package/dist/plugins/fixed-list.d.ts +0 -9
- package/dist/plugins/mapbox.d.ts +0 -19
- package/dist/plugins/mapbox.js +0 -2904
- package/dist/themes/bootstrap.d.ts +0 -10
- package/dist/themes/material.d.ts +0 -15
- package/src/config.ts +0 -110
- package/src/constants.ts +0 -30
- package/src/constraints.ts +0 -149
- package/src/exports.ts +0 -7
- package/src/form-bootstrap.ts +0 -12
- package/src/form-default.ts +0 -12
- package/src/form-material.ts +0 -12
- package/src/form.ts +0 -319
- package/src/globals.d.ts +0 -2
- package/src/group.ts +0 -34
- package/src/loader.ts +0 -187
- package/src/node.ts +0 -192
- package/src/plugin.ts +0 -60
- package/src/plugins/file-upload.ts +0 -26
- package/src/plugins/fixed-list.ts +0 -19
- package/src/plugins/leaflet.ts +0 -196
- package/src/plugins/map-util.ts +0 -41
- package/src/plugins/mapbox.ts +0 -157
- package/src/property-template.ts +0 -151
- package/src/property.ts +0 -309
- package/src/serialize.ts +0 -96
- package/src/shacl-engine.d.ts +0 -2
- package/src/styles.css +0 -49
- package/src/theme.ts +0 -132
- package/src/themes/bootstrap.css +0 -6
- package/src/themes/bootstrap.ts +0 -44
- package/src/themes/default.css +0 -4
- package/src/themes/default.ts +0 -258
- package/src/themes/material.css +0 -14
- package/src/themes/material.ts +0 -253
- package/src/util.ts +0 -275
package/src/form.ts
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import { ShaclNode } from './node'
|
|
2
|
-
import { Config } from './config'
|
|
3
|
-
import { ClassInstanceProvider, Plugin, listPlugins, registerPlugin } from './plugin'
|
|
4
|
-
import { Store, NamedNode, DataFactory, Quad, BlankNode } from 'n3'
|
|
5
|
-
import { DATA_GRAPH, DCTERMS_PREDICATE_CONFORMS_TO, PREFIX_SHACL, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, SHACL_PREDICATE_TARGET_CLASS, SHAPES_GRAPH } from './constants'
|
|
6
|
-
import { Editor, Theme } from './theme'
|
|
7
|
-
import { serialize } from './serialize'
|
|
8
|
-
import { Validator } from 'shacl-engine'
|
|
9
|
-
import { RokitCollapsible } from '@ro-kit/ui-widgets'
|
|
10
|
-
|
|
11
|
-
export class ShaclForm extends HTMLElement {
|
|
12
|
-
static get observedAttributes() { return Config.dataAttributes() }
|
|
13
|
-
|
|
14
|
-
config: Config
|
|
15
|
-
shape: ShaclNode | null = null
|
|
16
|
-
form: HTMLFormElement
|
|
17
|
-
initDebounceTimeout: ReturnType<typeof setTimeout> | undefined
|
|
18
|
-
|
|
19
|
-
constructor(theme: Theme) {
|
|
20
|
-
super()
|
|
21
|
-
|
|
22
|
-
this.attachShadow({ mode: 'open' })
|
|
23
|
-
this.form = document.createElement('form')
|
|
24
|
-
this.config = new Config(theme, this.form)
|
|
25
|
-
this.form.addEventListener('change', ev => {
|
|
26
|
-
ev.stopPropagation()
|
|
27
|
-
if (this.config.editMode) {
|
|
28
|
-
this.validate(true).then(report => {
|
|
29
|
-
this.dispatchEvent(new CustomEvent('change', { bubbles: true, cancelable: false, composed: true, detail: { 'valid': report.conforms, 'report': report } }))
|
|
30
|
-
}).catch(e => { console.warn(e) })
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
connectedCallback() {
|
|
36
|
-
this.shadowRoot!.prepend(this.form)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
attributeChangedCallback() {
|
|
40
|
-
this.config.updateAttributes(this)
|
|
41
|
-
this.initialize()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private initialize() {
|
|
45
|
-
clearTimeout(this.initDebounceTimeout)
|
|
46
|
-
// set loading attribute on element so that hosting app can apply special css rules
|
|
47
|
-
this.setAttribute('loading', '')
|
|
48
|
-
// remove all child elements from form and show loading indicator
|
|
49
|
-
this.form.replaceChildren(document.createTextNode(this.config.attributes.loading))
|
|
50
|
-
this.initDebounceTimeout = setTimeout(async () => {
|
|
51
|
-
try {
|
|
52
|
-
await this.config.loader.loadGraphs()
|
|
53
|
-
// remove loading indicator
|
|
54
|
-
this.form.replaceChildren()
|
|
55
|
-
// reset rendered node references
|
|
56
|
-
this.config.renderedNodes.clear()
|
|
57
|
-
// find root shacl shape
|
|
58
|
-
const rootShapeShaclSubject = this.findRootShaclShapeSubject()
|
|
59
|
-
if (rootShapeShaclSubject) {
|
|
60
|
-
// remove all previous css classes to have a defined state
|
|
61
|
-
this.form.classList.forEach(value => { this.form.classList.remove(value) })
|
|
62
|
-
this.form.classList.toggle('mode-edit', this.config.editMode)
|
|
63
|
-
this.form.classList.toggle('mode-view', !this.config.editMode)
|
|
64
|
-
// let theme add classes to form element
|
|
65
|
-
this.config.theme.apply(this.form)
|
|
66
|
-
// adopt stylesheets from theme and plugins
|
|
67
|
-
const styles: CSSStyleSheet[] = [ this.config.theme.stylesheet ]
|
|
68
|
-
for (const plugin of listPlugins()) {
|
|
69
|
-
if (plugin.stylesheet) {
|
|
70
|
-
styles.push(plugin.stylesheet)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
this.shadowRoot!.adoptedStyleSheets = styles
|
|
74
|
-
|
|
75
|
-
this.shape = new ShaclNode(rootShapeShaclSubject, this.config, this.config.attributes.valuesSubject ? DataFactory.namedNode(this.config.attributes.valuesSubject) : undefined)
|
|
76
|
-
this.form.appendChild(this.shape)
|
|
77
|
-
|
|
78
|
-
if (this.config.editMode) {
|
|
79
|
-
// add submit button
|
|
80
|
-
if (this.config.attributes.submitButton !== null) {
|
|
81
|
-
const button = this.config.theme.createButton(this.config.attributes.submitButton || 'Submit', true)
|
|
82
|
-
button.addEventListener('click', (event) => {
|
|
83
|
-
event.preventDefault()
|
|
84
|
-
// let browser check form validity first
|
|
85
|
-
if (this.form.reportValidity()) {
|
|
86
|
-
// now validate data graph
|
|
87
|
-
this.validate().then(report => {
|
|
88
|
-
if (report?.conforms) {
|
|
89
|
-
// form and data graph are valid, so fire submit event
|
|
90
|
-
this.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
|
91
|
-
} else {
|
|
92
|
-
// focus first invalid element
|
|
93
|
-
let invalidEditor = this.form.querySelector(':scope .invalid > .editor')
|
|
94
|
-
if (invalidEditor) {
|
|
95
|
-
(invalidEditor as HTMLElement).focus()
|
|
96
|
-
} else {
|
|
97
|
-
this.form.querySelector(':scope .invalid')?.scrollIntoView()
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
this.form.appendChild(button)
|
|
104
|
-
}
|
|
105
|
-
// delete bound values from data graph, otherwise validation would be confused
|
|
106
|
-
if (this.config.attributes.valuesSubject) {
|
|
107
|
-
this.removeFromDataGraph(DataFactory.namedNode(this.config.attributes.valuesSubject))
|
|
108
|
-
}
|
|
109
|
-
await this.validate(true)
|
|
110
|
-
}
|
|
111
|
-
} else if (this.config.store.countQuads(null, null, null, SHAPES_GRAPH) > 0) {
|
|
112
|
-
// raise error only when shapes graph is not empty
|
|
113
|
-
throw new Error('shacl root node shape not found')
|
|
114
|
-
}
|
|
115
|
-
} catch (e) {
|
|
116
|
-
console.error(e)
|
|
117
|
-
const errorDisplay = document.createElement('div')
|
|
118
|
-
errorDisplay.innerText = String(e)
|
|
119
|
-
this.form.replaceChildren(errorDisplay)
|
|
120
|
-
}
|
|
121
|
-
this.removeAttribute('loading')
|
|
122
|
-
}, 200)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
public serialize(format = 'text/turtle', graph = this.toRDF()): string {
|
|
126
|
-
const quads = graph.getQuads(null, null, null, null)
|
|
127
|
-
return serialize(quads, format, this.config.prefixes)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
public toRDF(graph = new Store()): Store {
|
|
131
|
-
this.shape?.toRDF(graph)
|
|
132
|
-
return graph
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
public registerPlugin(plugin: Plugin) {
|
|
136
|
-
registerPlugin(plugin)
|
|
137
|
-
this.initialize()
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
public setTheme(theme: Theme) {
|
|
141
|
-
this.config.theme = theme
|
|
142
|
-
this.initialize()
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
public setClassInstanceProvider(provider: ClassInstanceProvider) {
|
|
146
|
-
this.config.classInstanceProvider = provider
|
|
147
|
-
this.initialize()
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/* Returns the validation report */
|
|
151
|
-
public async validate(ignoreEmptyValues = false): Promise<any> {
|
|
152
|
-
for (const elem of this.form.querySelectorAll(':scope .validation-error')) {
|
|
153
|
-
elem.remove()
|
|
154
|
-
}
|
|
155
|
-
for (const elem of this.form.querySelectorAll(':scope .property-instance')) {
|
|
156
|
-
elem.classList.remove('invalid')
|
|
157
|
-
if (((elem.querySelector(':scope > .editor')) as Editor)?.value) {
|
|
158
|
-
elem.classList.add('valid')
|
|
159
|
-
} else {
|
|
160
|
-
elem.classList.remove('valid')
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
this.config.store.deleteGraph(this.config.valuesGraphId || '')
|
|
165
|
-
if (this.shape) {
|
|
166
|
-
this.shape.toRDF(this.config.store)
|
|
167
|
-
// add node target for validation. this is required in case of missing sh:targetClass in root shape
|
|
168
|
-
this.config.store.add(new Quad(this.shape.shaclSubject, DataFactory.namedNode(PREFIX_SHACL + 'targetNode'), this.shape.nodeId, this.config.valuesGraphId))
|
|
169
|
-
}
|
|
170
|
-
try {
|
|
171
|
-
const dataset = this.config.store
|
|
172
|
-
const report = await new Validator(dataset, { details: true, factory: DataFactory }).validate({ dataset })
|
|
173
|
-
|
|
174
|
-
for (const result of report.results) {
|
|
175
|
-
if (result.focusNode?.ptrs?.length) {
|
|
176
|
-
for (const ptr of result.focusNode.ptrs) {
|
|
177
|
-
const focusNode = ptr._term
|
|
178
|
-
// result.path can be empty, e.g. if a focus node does not contain a required property node
|
|
179
|
-
if (result.path?.length) {
|
|
180
|
-
const path = result.path[0].predicates[0]
|
|
181
|
-
// try to find most specific editor elements first
|
|
182
|
-
let invalidElements = this.form.querySelectorAll(`
|
|
183
|
-
:scope shacl-node[data-node-id='${focusNode.id}'] > shacl-property > .property-instance[data-path='${path.id}'] > .editor,
|
|
184
|
-
:scope shacl-node[data-node-id='${focusNode.id}'] > shacl-property > .shacl-group > .property-instance[data-path='${path.id}'] > .editor,
|
|
185
|
-
:scope shacl-node[data-node-id='${focusNode.id}'] > .shacl-group > shacl-property > .property-instance[data-path='${path.id}'] > .editor,
|
|
186
|
-
:scope shacl-node[data-node-id='${focusNode.id}'] > .shacl-group > shacl-property > .shacl-group > .property-instance[data-path='${path.id}'] > .editor`)
|
|
187
|
-
if (invalidElements.length === 0) {
|
|
188
|
-
// if no editors found, select respective node. this will be the case for node shape violations.
|
|
189
|
-
invalidElements = this.form.querySelectorAll(`
|
|
190
|
-
:scope [data-node-id='${focusNode.id}'] > shacl-property > .property-instance[data-path='${path.id}'],
|
|
191
|
-
:scope [data-node-id='${focusNode.id}'] > shacl-property > .shacl-group > .property-instance[data-path='${path.id}']`)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
for (const invalidElement of invalidElements) {
|
|
195
|
-
if (invalidElement.classList.contains('editor')) {
|
|
196
|
-
// this is a property shape violation
|
|
197
|
-
if (!ignoreEmptyValues || (invalidElement as Editor).value) {
|
|
198
|
-
let parent: HTMLElement | null = invalidElement.parentElement!
|
|
199
|
-
parent.classList.add('invalid')
|
|
200
|
-
parent.classList.remove('valid')
|
|
201
|
-
parent.appendChild(this.createValidationErrorDisplay(result))
|
|
202
|
-
do {
|
|
203
|
-
if (parent instanceof RokitCollapsible) {
|
|
204
|
-
parent.open = true
|
|
205
|
-
}
|
|
206
|
-
parent = parent.parentElement
|
|
207
|
-
} while (parent)
|
|
208
|
-
}
|
|
209
|
-
} else if (!ignoreEmptyValues) {
|
|
210
|
-
// this is a node shape violation
|
|
211
|
-
invalidElement.classList.add('invalid')
|
|
212
|
-
invalidElement.classList.remove('valid')
|
|
213
|
-
invalidElement.appendChild(this.createValidationErrorDisplay(result, 'node'))
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
} else if (!ignoreEmptyValues) {
|
|
217
|
-
this.form.querySelector(`:scope [data-node-id='${focusNode.id}']`)?.prepend(this.createValidationErrorDisplay(result, 'node'))
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return report
|
|
223
|
-
} catch(e) {
|
|
224
|
-
console.error(e)
|
|
225
|
-
return false
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private createValidationErrorDisplay(validatonResult?: any, clazz?: string): HTMLElement {
|
|
230
|
-
const messageElement = document.createElement('span')
|
|
231
|
-
messageElement.classList.add('validation-error')
|
|
232
|
-
if (clazz) {
|
|
233
|
-
messageElement.classList.add(clazz)
|
|
234
|
-
}
|
|
235
|
-
if (validatonResult) {
|
|
236
|
-
if (validatonResult.message?.length > 0) {
|
|
237
|
-
for (const message of validatonResult.message) {
|
|
238
|
-
messageElement.title += message.value + '\n'
|
|
239
|
-
}
|
|
240
|
-
} else {
|
|
241
|
-
messageElement.title = validatonResult.sourceConstraintComponent?.value
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return messageElement
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private findRootShaclShapeSubject(): NamedNode | undefined {
|
|
248
|
-
let rootShapeShaclSubject: NamedNode | null = null
|
|
249
|
-
// if data-shape-subject is set, use that
|
|
250
|
-
if (this.config.attributes.shapeSubject) {
|
|
251
|
-
rootShapeShaclSubject = DataFactory.namedNode(this.config.attributes.shapeSubject)
|
|
252
|
-
if (this.config.store.getQuads(rootShapeShaclSubject, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, null).length === 0) {
|
|
253
|
-
console.warn(`shapes graph does not contain requested root shape ${this.config.attributes.shapeSubject}`)
|
|
254
|
-
return
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
// if we have a data graph and data-values-subject is set, use shape of that
|
|
259
|
-
if (this.config.attributes.valuesSubject && this.config.store.countQuads(null, null, null, DATA_GRAPH) > 0) {
|
|
260
|
-
const rootValueSubject = DataFactory.namedNode(this.config.attributes.valuesSubject)
|
|
261
|
-
const rootValueSubjectTypes = [
|
|
262
|
-
...this.config.store.getQuads(rootValueSubject, RDF_PREDICATE_TYPE, null, DATA_GRAPH),
|
|
263
|
-
...this.config.store.getQuads(rootValueSubject, DCTERMS_PREDICATE_CONFORMS_TO, null, DATA_GRAPH)
|
|
264
|
-
]
|
|
265
|
-
if (rootValueSubjectTypes.length === 0) {
|
|
266
|
-
console.warn(`value subject '${this.config.attributes.valuesSubject}' has neither ${RDF_PREDICATE_TYPE.id} nor ${DCTERMS_PREDICATE_CONFORMS_TO.id} statement`)
|
|
267
|
-
return
|
|
268
|
-
}
|
|
269
|
-
// if type/conformsTo refers to a node shape, prioritize that over targetClass resolution
|
|
270
|
-
for (const rootValueSubjectType of rootValueSubjectTypes) {
|
|
271
|
-
if (this.config.store.getQuads(rootValueSubjectType.object as NamedNode, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, null).length > 0) {
|
|
272
|
-
rootShapeShaclSubject = rootValueSubjectType.object as NamedNode
|
|
273
|
-
break
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (!rootShapeShaclSubject) {
|
|
277
|
-
const rootShapes = this.config.store.getQuads(null, SHACL_PREDICATE_TARGET_CLASS, rootValueSubjectTypes[0].object, null)
|
|
278
|
-
if (rootShapes.length === 0) {
|
|
279
|
-
console.error(`value subject '${this.config.attributes.valuesSubject}' has no shacl shape definition in the shapes graph`)
|
|
280
|
-
return
|
|
281
|
-
}
|
|
282
|
-
if (rootShapes.length > 1) {
|
|
283
|
-
console.warn(`value subject '${this.config.attributes.valuesSubject}' has multiple shacl shape definitions in the shapes graph, choosing the first found (${rootShapes[0].subject})`)
|
|
284
|
-
}
|
|
285
|
-
if (this.config.store.getQuads(rootShapes[0].subject, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, null).length === 0) {
|
|
286
|
-
console.error(`value subject '${this.config.attributes.valuesSubject}' references a shape which is not a NodeShape (${rootShapes[0].subject})`)
|
|
287
|
-
return
|
|
288
|
-
}
|
|
289
|
-
rootShapeShaclSubject = rootShapes[0].subject as NamedNode
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
// choose first of all defined root shapes
|
|
294
|
-
const rootShapes = this.config.store.getQuads(null, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, null)
|
|
295
|
-
if (rootShapes.length == 0) {
|
|
296
|
-
console.warn('shapes graph does not contain any root shapes')
|
|
297
|
-
return
|
|
298
|
-
}
|
|
299
|
-
if (rootShapes.length > 1) {
|
|
300
|
-
console.warn('shapes graph contains', rootShapes.length, 'root shapes. choosing first found which is', rootShapes[0].subject.value)
|
|
301
|
-
console.info('hint: set the shape to use with attribute "data-shape-subject"')
|
|
302
|
-
}
|
|
303
|
-
rootShapeShaclSubject = rootShapes[0].subject as NamedNode
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return rootShapeShaclSubject
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private removeFromDataGraph(subject: NamedNode | BlankNode) {
|
|
310
|
-
this.config.attributes.valuesSubject
|
|
311
|
-
for (const quad of this.config.store.getQuads(subject, null, null, DATA_GRAPH)) {
|
|
312
|
-
this.config.store.delete(quad)
|
|
313
|
-
if (quad.object.termType === 'NamedNode' || quad.object.termType === 'BlankNode') {
|
|
314
|
-
// recurse
|
|
315
|
-
this.removeFromDataGraph(quad.object)
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
package/src/globals.d.ts
DELETED
package/src/group.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { PREFIX_RDFS } from './constants'
|
|
2
|
-
import { Config } from './config'
|
|
3
|
-
import { findObjectValueByPredicate } from './util'
|
|
4
|
-
import { RokitCollapsible } from '@ro-kit/ui-widgets'
|
|
5
|
-
|
|
6
|
-
export function createShaclGroup(groupSubject: string, config: Config): HTMLElement {
|
|
7
|
-
let name = groupSubject
|
|
8
|
-
const quads = config.store.getQuads(groupSubject, null, null, null)
|
|
9
|
-
const label = findObjectValueByPredicate(quads, "label", PREFIX_RDFS, config.languages)
|
|
10
|
-
if (label) {
|
|
11
|
-
name = label
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let group: HTMLElement
|
|
15
|
-
if (config.attributes.collapse !== null) {
|
|
16
|
-
group = new RokitCollapsible()
|
|
17
|
-
group.classList.add('collapsible');
|
|
18
|
-
(group as RokitCollapsible).open = config.attributes.collapse === 'open';
|
|
19
|
-
(group as RokitCollapsible).label = name
|
|
20
|
-
} else {
|
|
21
|
-
group = document.createElement('div')
|
|
22
|
-
const header = document.createElement('h1')
|
|
23
|
-
header.innerText = name
|
|
24
|
-
group.appendChild(header)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
group.dataset['subject'] = groupSubject
|
|
28
|
-
group.classList.add('shacl-group')
|
|
29
|
-
const order = findObjectValueByPredicate(quads, "order")
|
|
30
|
-
if (order) {
|
|
31
|
-
group.style.order = order
|
|
32
|
-
}
|
|
33
|
-
return group
|
|
34
|
-
}
|
package/src/loader.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { Store, Quad, NamedNode, DataFactory, StreamParser } from 'n3'
|
|
2
|
-
import { DATA_GRAPH, DCTERMS_PREDICATE_CONFORMS_TO, OWL_PREDICATE_IMPORTS, SHACL_PREDICATE_CLASS, SHACL_PREDICATE_TARGET_CLASS, SHAPES_GRAPH } from './constants'
|
|
3
|
-
import { Config } from './config'
|
|
4
|
-
import { isURL } from './util'
|
|
5
|
-
import { RdfXmlParser } from 'rdfxml-streaming-parser'
|
|
6
|
-
import { toRDF } from 'jsonld'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// cache external data in module scope (and not in Loader instance) to avoid requesting
|
|
10
|
-
// them multiple times, e.g. when more than one shacl-form element is on the page
|
|
11
|
-
// that import the same resources
|
|
12
|
-
const loadedURLCache: Record<string, Promise<string>> = {}
|
|
13
|
-
const loadedClassesCache: Record<string, Promise<string>> = {}
|
|
14
|
-
|
|
15
|
-
export class Loader {
|
|
16
|
-
private config: Config
|
|
17
|
-
private loadedExternalUrls: string[] = []
|
|
18
|
-
private loadedClasses: string[] = []
|
|
19
|
-
|
|
20
|
-
constructor(config: Config) {
|
|
21
|
-
this.config = config
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async loadGraphs() {
|
|
25
|
-
// clear local caches
|
|
26
|
-
this.loadedExternalUrls = []
|
|
27
|
-
this.loadedClasses = []
|
|
28
|
-
this.config.prefixes = {}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const promises: Promise<void>[] = []
|
|
32
|
-
const store = new Store()
|
|
33
|
-
promises.push(this.importRDF(this.config.attributes.shapes ? this.config.attributes.shapes : this.config.attributes.shapesUrl ? this.fetchRDF(this.config.attributes.shapesUrl) : '', store, SHAPES_GRAPH))
|
|
34
|
-
promises.push(this.importRDF(this.config.attributes.values ? this.config.attributes.values : this.config.attributes.valuesUrl ? this.fetchRDF(this.config.attributes.valuesUrl) : '', store, DATA_GRAPH))
|
|
35
|
-
await Promise.all(promises)
|
|
36
|
-
|
|
37
|
-
// if shapes graph is empty, but we have the following triples:
|
|
38
|
-
// <valueSubject> a <uri> or <valueSubject> dcterms:conformsTo <uri>
|
|
39
|
-
// or if we have data-shape-subject set on the form,
|
|
40
|
-
// then try to load the referenced object(s) into the shapes graph
|
|
41
|
-
if (store.countQuads(null, null, null, SHAPES_GRAPH) === 0 && this.config.attributes.valuesSubject) {
|
|
42
|
-
const shapeCandidates = [
|
|
43
|
-
// ...store.getObjects(this.config.attributes.valuesSubject, RDF_PREDICATE_TYPE, DATA_GRAPH),
|
|
44
|
-
...store.getObjects(this.config.attributes.valuesSubject, DCTERMS_PREDICATE_CONFORMS_TO, DATA_GRAPH)
|
|
45
|
-
]
|
|
46
|
-
const promises: Promise<void>[] = []
|
|
47
|
-
for (const uri of shapeCandidates) {
|
|
48
|
-
const url = this.toURL(uri.value)
|
|
49
|
-
if (url && this.loadedExternalUrls.indexOf(url) < 0) {
|
|
50
|
-
this.loadedExternalUrls.push(url)
|
|
51
|
-
promises.push(this.importRDF(this.fetchRDF(url), store, SHAPES_GRAPH))
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
try {
|
|
55
|
-
await Promise.allSettled(promises)
|
|
56
|
-
} catch (e) {
|
|
57
|
-
console.warn(e)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
this.config.store = store
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async importRDF(input: string | Promise<string>, store: Store, graph?: NamedNode) {
|
|
65
|
-
const parse = async (input: string) => {
|
|
66
|
-
const dependencies: Promise<void>[] = []
|
|
67
|
-
await new Promise((resolve, reject) => {
|
|
68
|
-
const parser = guessContentType(input) === 'xml' ? new RdfXmlParser() : new StreamParser()
|
|
69
|
-
parser.on('data', (quad: Quad) => {
|
|
70
|
-
store.add(new Quad(quad.subject, quad.predicate, quad.object, graph))
|
|
71
|
-
// check if this is an owl:imports predicate and try to load the url
|
|
72
|
-
if (this.config.attributes.ignoreOwlImports === null && OWL_PREDICATE_IMPORTS.equals(quad.predicate)) {
|
|
73
|
-
const url = this.toURL(quad.object.value)
|
|
74
|
-
// import url only once
|
|
75
|
-
if (url && this.loadedExternalUrls.indexOf(url) < 0) {
|
|
76
|
-
this.loadedExternalUrls.push(url)
|
|
77
|
-
// import into separate graph
|
|
78
|
-
dependencies.push(this.importRDF(this.fetchRDF(url), store, DataFactory.namedNode(url)))
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// check if this is an sh:class predicate and invoke class instance provider
|
|
82
|
-
if (this.config.classInstanceProvider && (SHACL_PREDICATE_CLASS.equals(quad.predicate) || SHACL_PREDICATE_TARGET_CLASS.equals(quad.predicate))) {
|
|
83
|
-
const className = quad.object.value
|
|
84
|
-
// import class definitions only once
|
|
85
|
-
if (this.loadedClasses.indexOf(className) < 0) {
|
|
86
|
-
let promise: Promise<string>
|
|
87
|
-
// check if class is in module scope cache
|
|
88
|
-
if (className in loadedClassesCache) {
|
|
89
|
-
promise = loadedClassesCache[className]
|
|
90
|
-
} else {
|
|
91
|
-
promise = this.config.classInstanceProvider(className)
|
|
92
|
-
loadedClassesCache[className] = promise
|
|
93
|
-
}
|
|
94
|
-
this.loadedClasses.push(className)
|
|
95
|
-
dependencies.push(this.importRDF(promise, store, graph))
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
.on('error', (error) => {
|
|
100
|
-
console.warn('failed parsing graph', graph, error.message)
|
|
101
|
-
reject(error)
|
|
102
|
-
})
|
|
103
|
-
.on('prefix', (prefix, iri) => {
|
|
104
|
-
// ignore empty (default) namespace
|
|
105
|
-
if (prefix) {
|
|
106
|
-
this.config.prefixes[prefix] = iri
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
.on('end', () => {
|
|
110
|
-
resolve(null)
|
|
111
|
-
})
|
|
112
|
-
parser.write(input)
|
|
113
|
-
parser.end()
|
|
114
|
-
})
|
|
115
|
-
try {
|
|
116
|
-
await Promise.allSettled(dependencies)
|
|
117
|
-
} catch (e) {
|
|
118
|
-
console.warn(e)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (input instanceof Promise) {
|
|
123
|
-
input = await input
|
|
124
|
-
}
|
|
125
|
-
if (input) {
|
|
126
|
-
if (guessContentType(input) === 'json') {
|
|
127
|
-
// convert json to n-quads
|
|
128
|
-
try {
|
|
129
|
-
input = await toRDF(JSON.parse(input), { format: 'application/n-quads' }) as string
|
|
130
|
-
} catch(e) {
|
|
131
|
-
console.error(e)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
await parse(input)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
toURL(id: string): string | null {
|
|
139
|
-
if (isURL(id)) {
|
|
140
|
-
return id
|
|
141
|
-
}
|
|
142
|
-
if (this.config.prefixes) {
|
|
143
|
-
const splitted = id.split(':')
|
|
144
|
-
if (splitted.length === 2) {
|
|
145
|
-
const prefix = this.config.prefixes[splitted[0]]
|
|
146
|
-
if (prefix) {
|
|
147
|
-
// need to ignore type check. 'prefix' is a string and not a NamedNode<string> (seems to be a bug in n3 typings)
|
|
148
|
-
// @ts-ignore
|
|
149
|
-
id = id.replace(`${splitted[0]}:`, prefix)
|
|
150
|
-
if (isURL(id)) {
|
|
151
|
-
return id
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return null
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async fetchRDF(url: string): Promise<string> {
|
|
160
|
-
// try to load from cache first
|
|
161
|
-
if (url in loadedURLCache) {
|
|
162
|
-
return loadedURLCache[url]
|
|
163
|
-
}
|
|
164
|
-
let proxiedURL = url
|
|
165
|
-
// if we have a proxy configured, then load url via proxy
|
|
166
|
-
if (this.config.attributes.proxy) {
|
|
167
|
-
proxiedURL = this.config.attributes.proxy + encodeURIComponent(url)
|
|
168
|
-
}
|
|
169
|
-
const promise = fetch(proxiedURL, {
|
|
170
|
-
headers: {
|
|
171
|
-
'Accept': 'text/turtle, application/trig, application/n-triples, application/n-quads, text/n3, application/ld+json'
|
|
172
|
-
},
|
|
173
|
-
}).then(resp => resp.text())
|
|
174
|
-
loadedURLCache[url] = promise
|
|
175
|
-
return promise
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/* Can't rely on HTTP content-type header, since many resources are delivered with text/plain */
|
|
180
|
-
function guessContentType(input: string) {
|
|
181
|
-
if (/^\s*\{/.test(input)) {
|
|
182
|
-
return 'json'
|
|
183
|
-
} else if (/^\s*<\?xml/.test(input)) {
|
|
184
|
-
return 'xml'
|
|
185
|
-
}
|
|
186
|
-
return 'ttl'
|
|
187
|
-
}
|