@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulb-darmstadt/shacl-form",
3
- "version": "1.8.3",
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.4",
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.31",
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
@@ -1,4 +1,4 @@
1
- import { Literal, NamedNode, Prefixes, Quad, Term as N3Term, Store } from 'n3'
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 build a class instance hierarchy
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
- // Step 1: Initialize all instances as InputListEntry's with no children
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
- // Step 2: Record broader/narrower relationships (child -> parent)
107
+ // record broader/narrower/subClassOf hierarchical relationships
107
108
  for (const instance of instances) {
108
- appendSkosBroaderNarrower(instance, nodes, childToParent, template)
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
- // Step 3: Build hierarchy by nesting children under parents
126
+ // build hierarchy by nesting children under parents
112
127
  for (const [child, parent] of childToParent.entries()) {
113
- nodes.get(parent)!.children!.push(nodes.get(child)!)
128
+ nodes.get(parent)?.children?.push(nodes.get(child)!)
114
129
  }
115
130
 
116
- // Step 4: Find root nodes (no broader relationship)
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
- // Step 5: Add sub class instances
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 {