@ulb-darmstadt/shacl-form 1.8.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulb-darmstadt/shacl-form",
3
- "version": "1.8.4",
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",
@@ -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.32",
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, Parser, Quad, Prefixes, NamedNode, DataFactory } from 'n3'
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 ? this.fetchRDF(this.config.attributes.shapesUrl) : '', store, SHAPES_GRAPH))
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 ? this.fetchRDF(this.config.attributes.valuesUrl) : '', store, DATA_GRAPH, new Parser({ blankNodePrefix: '' })))
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(this.fetchRDF(url), store, SHAPES_GRAPH))
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, parser?: Parser) {
66
- const p = parser || new Parser()
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
- p.parse(text, (error: Error, quad: Quad, prefixes: Prefixes) => {
71
- if (error) {
72
- console.warn('failed parsing graph', graph, error.message)
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
- }
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
- // 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))
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
- if (prefixes) {
107
- this.config.registerPrefixes(prefixes)
108
- }
109
- resolve(null)
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
- 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
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
@@ -104,18 +104,21 @@ export function findInstancesOf(clazz: NamedNode, template: ShaclPropertyTemplat
104
104
  nodes.set(instance.id, { value: instance, label: findLabel(template.config.store.getQuads(instance, null, null, null), template.config.languages), children: [] })
105
105
  }
106
106
 
107
- // record broader/narrower relationships (child -> parent)
107
+ // record broader/narrower/subClassOf hierarchical relationships
108
108
  for (const instance of instances) {
109
109
  for (const parent of template.config.store.getObjects(instance, SKOS_PREDICATE_BROADER, null)) {
110
- childToParent.set(instance.id, parent.id)
111
- if (!nodes.has(parent.id)) {
112
- nodes.set(parent.id, { value: parent, label: findLabel(template.config.store.getQuads(parent, null, null, null), template.config.languages), children: [] })
110
+ if (nodes.has(parent.id)) {
111
+ childToParent.set(instance.id, parent.id)
113
112
  }
114
113
  }
115
114
  for (const child of template.config.store.getObjects(instance, SKOS_PREDICATE_NARROWER, null)) {
116
- childToParent.set(child.id, instance.id)
117
- if (!nodes.has(child.id)) {
118
- nodes.set(child.id, { value: child, label: findLabel(template.config.store.getQuads(child, null, null, null), template.config.languages), children: [] })
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)
119
122
  }
120
123
  }
121
124
  }