@ulb-darmstadt/shacl-form 1.8.3 → 1.9.0
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/README.md +14 -1
- package/dist/form-bootstrap.js +100 -75
- package/dist/form-default.js +96 -71
- package/dist/form-material.js +131 -106
- package/dist/loader.d.ts +2 -3
- package/dist/plugins/leaflet.js +5 -5
- package/dist/plugins/mapbox.js +14 -14
- package/package.json +4 -3
- package/src/loader.ts +105 -65
- package/src/util.ts +24 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ulb-darmstadt/shacl-form",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "SHACL form generator",
|
|
5
5
|
"main": "dist/form-default.js",
|
|
6
6
|
"module": "dist/form-default.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@types/uuid": "^10.0.0",
|
|
56
56
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
57
57
|
"typescript": "^5.8.3",
|
|
58
|
-
"vite": "^7.0.
|
|
58
|
+
"vite": "^7.0.5",
|
|
59
59
|
"vite-plugin-dts": "^4.5.4"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
@@ -67,11 +67,12 @@
|
|
|
67
67
|
"leaflet.fullscreen": "^4.0.0",
|
|
68
68
|
"mapbox-gl": "^3.13.0",
|
|
69
69
|
"n3": "^1.26.0",
|
|
70
|
+
"rdfxml-streaming-parser": "^3.1.0",
|
|
70
71
|
"shacl-engine": "^1.0.2",
|
|
71
72
|
"uuid": "^11.1.0"
|
|
72
73
|
},
|
|
73
74
|
"peerDependencies": {
|
|
74
|
-
"@ro-kit/ui-widgets": "^0.0.
|
|
75
|
+
"@ro-kit/ui-widgets": "^0.0.33",
|
|
75
76
|
"mdui": "^2.1.4"
|
|
76
77
|
}
|
|
77
78
|
}
|
package/src/loader.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { Store,
|
|
2
|
-
import { toRDF } from 'jsonld'
|
|
1
|
+
import { Store, Quad, NamedNode, DataFactory, Parser, Prefixes } from 'n3'
|
|
3
2
|
import { DATA_GRAPH, DCTERMS_PREDICATE_CONFORMS_TO, OWL_PREDICATE_IMPORTS, RDF_PREDICATE_TYPE, SHACL_PREDICATE_CLASS, SHAPES_GRAPH } from './constants'
|
|
4
3
|
import { Config } from './config'
|
|
5
4
|
import { isURL } from './util'
|
|
5
|
+
import { RdfXmlParser } from 'rdfxml-streaming-parser'
|
|
6
|
+
import { toRDF } from 'jsonld'
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
// cache external data in module scope (and not in Loader instance) to avoid requesting
|
|
8
10
|
// them multiple times, e.g. when more than one shacl-form element is on the page
|
|
9
11
|
// that import the same resources
|
|
10
12
|
const loadedURLCache: Record<string, Promise<string>> = {}
|
|
11
13
|
const loadedClassesCache: Record<string, Promise<string>> = {}
|
|
14
|
+
const ttlParser = new Parser()
|
|
12
15
|
let sharedShapesGraph: Store | undefined
|
|
13
16
|
|
|
14
17
|
export class Loader {
|
|
@@ -31,9 +34,9 @@ export class Loader {
|
|
|
31
34
|
const promises: Promise<void>[] = []
|
|
32
35
|
if (!store) {
|
|
33
36
|
store = new Store()
|
|
34
|
-
promises.push(this.importRDF(this.config.attributes.shapes ? this.config.attributes.shapes : this.config.attributes.shapesUrl ?
|
|
37
|
+
promises.push(this.importRDF(this.config.attributes.shapes ? this.config.attributes.shapes : this.config.attributes.shapesUrl ? fetchRDF(this.config.attributes.shapesUrl) : '', store, SHAPES_GRAPH))
|
|
35
38
|
}
|
|
36
|
-
promises.push(this.importRDF(this.config.attributes.values ? this.config.attributes.values : this.config.attributes.valuesUrl ?
|
|
39
|
+
promises.push(this.importRDF(this.config.attributes.values ? this.config.attributes.values : this.config.attributes.valuesUrl ? fetchRDF(this.config.attributes.valuesUrl) : '', store, DATA_GRAPH))
|
|
37
40
|
await Promise.all(promises)
|
|
38
41
|
|
|
39
42
|
// if shapes graph is empty, but we have the following triples:
|
|
@@ -49,7 +52,7 @@ export class Loader {
|
|
|
49
52
|
const url = this.toURL(uri.value)
|
|
50
53
|
if (url && this.loadedExternalUrls.indexOf(url) < 0) {
|
|
51
54
|
this.loadedExternalUrls.push(url)
|
|
52
|
-
promises.push(this.importRDF(
|
|
55
|
+
promises.push(this.importRDF(fetchRDF(url), store, SHAPES_GRAPH))
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
try {
|
|
@@ -62,52 +65,77 @@ export class Loader {
|
|
|
62
65
|
this.config.store = store
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
async importRDF(input: string | Promise<string>, store: Store, graph?: NamedNode
|
|
66
|
-
const
|
|
67
|
-
const parse = async (text: string) => {
|
|
68
|
+
async importRDF(input: string | Promise<string>, store: Store, graph?: NamedNode) {
|
|
69
|
+
const parse = async (input: string) => {
|
|
68
70
|
const dependencies: Promise<void>[] = []
|
|
69
71
|
await new Promise((resolve, reject) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
}
|
|
72
|
+
const addQuad = (quad: Quad) => {
|
|
73
|
+
store.add(new Quad(quad.subject, quad.predicate, quad.object, graph))
|
|
74
|
+
// check if this is an owl:imports predicate and try to load the url
|
|
75
|
+
if (this.config.attributes.ignoreOwlImports === null && OWL_PREDICATE_IMPORTS.equals(quad.predicate)) {
|
|
76
|
+
const url = this.toURL(quad.object.value)
|
|
77
|
+
// import url only once
|
|
78
|
+
if (url && this.loadedExternalUrls.indexOf(url) < 0) {
|
|
79
|
+
this.loadedExternalUrls.push(url)
|
|
80
|
+
// import into separate graph
|
|
81
|
+
dependencies.push(this.importRDF(fetchRDF(url), store, DataFactory.namedNode(url)))
|
|
86
82
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.loadedClasses.push(className)
|
|
101
|
-
dependencies.push(this.importRDF(promise, store, graph, parser))
|
|
83
|
+
}
|
|
84
|
+
// check if this is an sh:class predicate and invoke class instance provider
|
|
85
|
+
if (this.config.classInstanceProvider && SHACL_PREDICATE_CLASS.equals(quad.predicate)) {
|
|
86
|
+
const className = quad.object.value
|
|
87
|
+
// import class definitions only once
|
|
88
|
+
if (this.loadedClasses.indexOf(className) < 0) {
|
|
89
|
+
let promise: Promise<string>
|
|
90
|
+
// check if class is in module scope cache
|
|
91
|
+
if (className in loadedClassesCache) {
|
|
92
|
+
promise = loadedClassesCache[className]
|
|
93
|
+
} else {
|
|
94
|
+
promise = this.config.classInstanceProvider(className)
|
|
95
|
+
loadedClassesCache[className] = promise
|
|
102
96
|
}
|
|
97
|
+
this.loadedClasses.push(className)
|
|
98
|
+
dependencies.push(this.importRDF(promise, store, graph))
|
|
103
99
|
}
|
|
104
|
-
return
|
|
105
100
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
}
|
|
102
|
+
const type = guessContentType(input)
|
|
103
|
+
if (type === 'xml') {
|
|
104
|
+
const parser = new RdfXmlParser()
|
|
105
|
+
parser.on('data', (quad: Quad) => {
|
|
106
|
+
addQuad(quad)
|
|
107
|
+
})
|
|
108
|
+
.on('error', (error) => {
|
|
109
|
+
console.warn('failed parsing graph', graph, error.message)
|
|
110
|
+
reject(error)
|
|
111
|
+
})
|
|
112
|
+
.on('prefix', (prefix, iri) => {
|
|
113
|
+
// ignore empty (default) namespace
|
|
114
|
+
if (prefix) {
|
|
115
|
+
this.config.prefixes[prefix] = iri
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
.on('end', () => {
|
|
119
|
+
resolve(null)
|
|
120
|
+
})
|
|
121
|
+
parser.write(input)
|
|
122
|
+
parser.end()
|
|
123
|
+
} else {
|
|
124
|
+
ttlParser.parse(input, (error: Error, quad: Quad, prefixes: Prefixes) => {
|
|
125
|
+
if (error) {
|
|
126
|
+
console.warn('failed parsing graph', graph, error.message)
|
|
127
|
+
return reject(error)
|
|
128
|
+
}
|
|
129
|
+
if (quad) {
|
|
130
|
+
addQuad(quad)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (prefixes) {
|
|
134
|
+
this.config.registerPrefixes(prefixes)
|
|
135
|
+
}
|
|
136
|
+
resolve(null)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
111
139
|
})
|
|
112
140
|
try {
|
|
113
141
|
await Promise.allSettled(dependencies)
|
|
@@ -120,31 +148,18 @@ export class Loader {
|
|
|
120
148
|
input = await input
|
|
121
149
|
}
|
|
122
150
|
if (input) {
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
151
|
+
if (guessContentType(input) === 'json') {
|
|
152
|
+
// convert json to n-quads
|
|
153
|
+
try {
|
|
154
|
+
input = await toRDF(JSON.parse(input), { format: 'application/n-quads' }) as string
|
|
155
|
+
} catch(e) {
|
|
156
|
+
console.error(e)
|
|
157
|
+
}
|
|
129
158
|
}
|
|
130
159
|
await parse(input)
|
|
131
160
|
}
|
|
132
161
|
}
|
|
133
162
|
|
|
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
163
|
toURL(id: string): string | null {
|
|
149
164
|
if (isURL(id)) {
|
|
150
165
|
return id
|
|
@@ -165,6 +180,31 @@ export class Loader {
|
|
|
165
180
|
}
|
|
166
181
|
return null
|
|
167
182
|
}
|
|
183
|
+
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function fetchRDF(url: string): Promise<string> {
|
|
187
|
+
// try to load from cache first
|
|
188
|
+
if (url in loadedURLCache) {
|
|
189
|
+
return loadedURLCache[url]
|
|
190
|
+
}
|
|
191
|
+
const promise = fetch(url, {
|
|
192
|
+
headers: {
|
|
193
|
+
'Accept': 'text/turtle, application/trig, application/n-triples, application/n-quads, text/n3, application/ld+json'
|
|
194
|
+
},
|
|
195
|
+
}).then(resp => resp.text())
|
|
196
|
+
loadedURLCache[url] = promise
|
|
197
|
+
return promise
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Can't rely on HTTP content-type header, since many resources are delivered with text/plain */
|
|
201
|
+
function guessContentType(input: string) {
|
|
202
|
+
if (/^\s*\{/.test(input)) {
|
|
203
|
+
return 'json'
|
|
204
|
+
} else if (/^\s*<\?xml/.test(input)) {
|
|
205
|
+
return 'xml'
|
|
206
|
+
}
|
|
207
|
+
return 'ttl'
|
|
168
208
|
}
|
|
169
209
|
|
|
170
210
|
export function setSharedShapesGraph(graph: Store) {
|
package/src/util.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Literal, NamedNode, Prefixes, Quad,
|
|
1
|
+
import { Literal, NamedNode, Prefixes, Quad, Store } from 'n3'
|
|
2
2
|
import { DATA_GRAPH, PREFIX_FOAF, PREFIX_RDFS, PREFIX_SHACL, PREFIX_SKOS, RDFS_PREDICATE_SUBCLASS_OF, RDF_PREDICATE_TYPE, SHAPES_GRAPH, SKOS_PREDICATE_BROADER, SKOS_PREDICATE_NARROWER } from './constants'
|
|
3
3
|
import { Term } from '@rdfjs/types'
|
|
4
4
|
import { InputListEntry } from './theme'
|
|
@@ -88,32 +88,47 @@ export function findInstancesOf(clazz: NamedNode, template: ShaclPropertyTemplat
|
|
|
88
88
|
const list = template.config.lists[template.shaclIn]
|
|
89
89
|
return createInputListEntries(list?.length ? list : [], template.config.store, template.config.languages)
|
|
90
90
|
} else {
|
|
91
|
+
// find instances in the shapes graph
|
|
91
92
|
const instances = template.config.store.getSubjects(RDF_PREDICATE_TYPE, clazz, SHAPES_GRAPH)
|
|
92
93
|
// find instances in the data graph
|
|
93
94
|
instances.push(...template.config.store.getSubjects(RDF_PREDICATE_TYPE, clazz, DATA_GRAPH))
|
|
94
95
|
// find instances in imported taxonomies
|
|
95
96
|
findClassInstancesFromOwlImports(clazz, template, template.config.store, instances)
|
|
96
97
|
|
|
97
|
-
// structures needed for
|
|
98
|
+
// initialize structures needed for building a class instance hierarchy
|
|
98
99
|
const nodes = new Map<string, InputListEntry>() // URI -> InputListEntry
|
|
99
100
|
const childToParent = new Map<string, string>() // URI -> parentURI
|
|
100
101
|
|
|
101
|
-
//
|
|
102
|
+
// initialize all instances as InputListEntry's with no children
|
|
102
103
|
for (const instance of instances) {
|
|
103
104
|
nodes.set(instance.id, { value: instance, label: findLabel(template.config.store.getQuads(instance, null, null, null), template.config.languages), children: [] })
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
//
|
|
107
|
+
// record broader/narrower/subClassOf hierarchical relationships
|
|
107
108
|
for (const instance of instances) {
|
|
108
|
-
|
|
109
|
+
for (const parent of template.config.store.getObjects(instance, SKOS_PREDICATE_BROADER, null)) {
|
|
110
|
+
if (nodes.has(parent.id)) {
|
|
111
|
+
childToParent.set(instance.id, parent.id)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const child of template.config.store.getObjects(instance, SKOS_PREDICATE_NARROWER, null)) {
|
|
115
|
+
if (nodes.has(child.id)) {
|
|
116
|
+
childToParent.set(child.id, instance.id)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const parent of template.config.store.getObjects(instance, RDFS_PREDICATE_SUBCLASS_OF, null)) {
|
|
120
|
+
if (nodes.has(parent.id)) {
|
|
121
|
+
childToParent.set(instance.id, parent.id)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
109
124
|
}
|
|
110
125
|
|
|
111
|
-
//
|
|
126
|
+
// build hierarchy by nesting children under parents
|
|
112
127
|
for (const [child, parent] of childToParent.entries()) {
|
|
113
|
-
nodes.get(parent)
|
|
128
|
+
nodes.get(parent)?.children?.push(nodes.get(child)!)
|
|
114
129
|
}
|
|
115
130
|
|
|
116
|
-
//
|
|
131
|
+
// find root nodes (no parent relationship)
|
|
117
132
|
const roots: InputListEntry[] = []
|
|
118
133
|
for (const [uri, node] of nodes.entries()) {
|
|
119
134
|
if (!childToParent.has(uri)) {
|
|
@@ -121,7 +136,7 @@ export function findInstancesOf(clazz: NamedNode, template: ShaclPropertyTemplat
|
|
|
121
136
|
}
|
|
122
137
|
}
|
|
123
138
|
|
|
124
|
-
//
|
|
139
|
+
// add sub class instances
|
|
125
140
|
for (const subClass of template.config.store.getSubjects(RDFS_PREDICATE_SUBCLASS_OF, clazz, null)) {
|
|
126
141
|
roots.push(...findInstancesOf(subClass as NamedNode, template))
|
|
127
142
|
}
|
|
@@ -129,23 +144,6 @@ export function findInstancesOf(clazz: NamedNode, template: ShaclPropertyTemplat
|
|
|
129
144
|
}
|
|
130
145
|
}
|
|
131
146
|
|
|
132
|
-
function appendSkosBroaderNarrower(subject: N3Term, nodes: Map<string, InputListEntry>, childToParent: Map<string, string>, template: ShaclPropertyTemplate) {
|
|
133
|
-
for (const parent of template.config.store.getObjects(subject, SKOS_PREDICATE_BROADER, null)) {
|
|
134
|
-
childToParent.set(subject.id, parent.id)
|
|
135
|
-
if (!nodes.has(parent.id)) {
|
|
136
|
-
nodes.set(parent.id, { value: parent, label: findLabel(template.config.store.getQuads(parent, null, null, null), template.config.languages), children: [] })
|
|
137
|
-
}
|
|
138
|
-
appendSkosBroaderNarrower(parent, nodes, childToParent, template)
|
|
139
|
-
}
|
|
140
|
-
for (const child of template.config.store.getObjects(subject, SKOS_PREDICATE_NARROWER, null)) {
|
|
141
|
-
childToParent.set(child.id, subject.id)
|
|
142
|
-
if (!nodes.has(child.id)) {
|
|
143
|
-
nodes.set(child.id, { value: child, label: findLabel(template.config.store.getQuads(child, null, null, null), template.config.languages), children: [] })
|
|
144
|
-
}
|
|
145
|
-
appendSkosBroaderNarrower(child, nodes, childToParent, template)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
147
|
export function isURL(input: string): boolean {
|
|
150
148
|
let url: URL
|
|
151
149
|
try {
|