@ulb-darmstadt/shacl-form 1.10.0 → 1.10.2
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/form-bootstrap.js +50 -50
- package/dist/form-default.js +51 -51
- package/dist/form-material.js +60 -60
- package/dist/loader.d.ts +2 -3
- package/package.json +1 -1
- package/src/config.ts +1 -1
- package/src/form.ts +4 -1
- package/src/loader.ts +57 -86
- package/src/property-template.ts +9 -11
- package/src/property.ts +1 -3
- package/src/serialize.ts +3 -1
- package/src/themes/default.ts +3 -1
package/dist/loader.d.ts
CHANGED
|
@@ -3,11 +3,10 @@ import { Config } from './config';
|
|
|
3
3
|
export declare class Loader {
|
|
4
4
|
private config;
|
|
5
5
|
private loadedExternalUrls;
|
|
6
|
+
private loadedClasses;
|
|
6
7
|
constructor(config: Config);
|
|
7
8
|
loadGraphs(): Promise<void>;
|
|
8
9
|
importRDF(input: string | Promise<string>, store: Store, graph?: NamedNode): Promise<void>;
|
|
9
|
-
fetchRDF(url: string): Promise<string>;
|
|
10
|
-
fetchOwlImports(store: Store): Promise<PromiseSettledResult<void>[] | undefined>;
|
|
11
|
-
fetchClassInstances(store: Store): Promise<void[] | undefined>;
|
|
12
10
|
toURL(id: string): string | null;
|
|
11
|
+
fetchRDF(url: string): Promise<string>;
|
|
13
12
|
}
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -101,7 +101,7 @@ export class Config {
|
|
|
101
101
|
|
|
102
102
|
set store(store: Store) {
|
|
103
103
|
this._store = store
|
|
104
|
-
this.lists = extractLists(store)
|
|
104
|
+
this.lists = extractLists(store, { ignoreErrors: true })
|
|
105
105
|
this.groups = []
|
|
106
106
|
store.forSubjects(subject => {
|
|
107
107
|
this.groups.push(subject.id)
|
package/src/form.ts
CHANGED
|
@@ -44,6 +44,8 @@ export class ShaclForm extends HTMLElement {
|
|
|
44
44
|
private initialize() {
|
|
45
45
|
clearTimeout(this.initDebounceTimeout)
|
|
46
46
|
this.initDebounceTimeout = setTimeout(async () => {
|
|
47
|
+
// set loading attribute on element so that hosting app can apply special css rules
|
|
48
|
+
this.setAttribute('loading', '')
|
|
47
49
|
// remove all child elements from form and show loading indicator
|
|
48
50
|
this.form.replaceChildren(document.createTextNode(this.config.attributes.loading))
|
|
49
51
|
try {
|
|
@@ -116,6 +118,7 @@ export class ShaclForm extends HTMLElement {
|
|
|
116
118
|
errorDisplay.innerText = String(e)
|
|
117
119
|
this.form.replaceChildren(errorDisplay)
|
|
118
120
|
}
|
|
121
|
+
this.removeAttribute('loading')
|
|
119
122
|
}, 200)
|
|
120
123
|
}
|
|
121
124
|
|
|
@@ -159,8 +162,8 @@ export class ShaclForm extends HTMLElement {
|
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
this.config.store.deleteGraph(this.config.valuesGraphId || '')
|
|
162
|
-
this.shape?.toRDF(this.config.store)
|
|
163
165
|
if (this.shape) {
|
|
166
|
+
this.shape.toRDF(this.config.store)
|
|
164
167
|
// add node target for validation. this is required in case of missing sh:targetClass in root shape
|
|
165
168
|
this.config.store.add(new Quad(this.shape.shaclSubject, DataFactory.namedNode(PREFIX_SHACL + 'targetNode'), this.shape.nodeId, this.config.valuesGraphId))
|
|
166
169
|
}
|
package/src/loader.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Store, Quad, NamedNode, DataFactory, StreamParser
|
|
2
|
-
import { DATA_GRAPH, DCTERMS_PREDICATE_CONFORMS_TO, OWL_PREDICATE_IMPORTS, SHACL_PREDICATE_CLASS,
|
|
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
3
|
import { Config } from './config'
|
|
4
4
|
import { isURL } from './util'
|
|
5
5
|
import { RdfXmlParser } from 'rdfxml-streaming-parser'
|
|
@@ -15,6 +15,7 @@ const loadedClassesCache: Record<string, Promise<string>> = {}
|
|
|
15
15
|
export class Loader {
|
|
16
16
|
private config: Config
|
|
17
17
|
private loadedExternalUrls: string[] = []
|
|
18
|
+
private loadedClasses: string[] = []
|
|
18
19
|
|
|
19
20
|
constructor(config: Config) {
|
|
20
21
|
this.config = config
|
|
@@ -23,29 +24,25 @@ export class Loader {
|
|
|
23
24
|
async loadGraphs() {
|
|
24
25
|
// clear local caches
|
|
25
26
|
this.loadedExternalUrls = []
|
|
27
|
+
this.loadedClasses = []
|
|
26
28
|
this.config.prefixes = {}
|
|
27
29
|
|
|
30
|
+
|
|
28
31
|
const promises: Promise<void>[] = []
|
|
29
|
-
const store =
|
|
32
|
+
const store = new Store()
|
|
30
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))
|
|
31
|
-
// load data graph
|
|
32
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))
|
|
33
35
|
await Promise.all(promises)
|
|
34
|
-
await this.fetchOwlImports(store)
|
|
35
|
-
await this.fetchClassInstances(store)
|
|
36
36
|
|
|
37
37
|
// if shapes graph is empty, but we have the following triples:
|
|
38
38
|
// <valueSubject> a <uri> or <valueSubject> dcterms:conformsTo <uri>
|
|
39
39
|
// or if we have data-shape-subject set on the form,
|
|
40
40
|
// then try to load the referenced object(s) into the shapes graph
|
|
41
|
-
if (store.countQuads(null, null, null, SHAPES_GRAPH) === 0) {
|
|
42
|
-
const shapeCandidates =
|
|
43
|
-
|
|
44
|
-
store.
|
|
45
|
-
|
|
46
|
-
if (this.config.attributes.shapeSubject) {
|
|
47
|
-
shapeCandidates.add(DataFactory.namedNode(this.config.attributes.shapeSubject))
|
|
48
|
-
}
|
|
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
|
+
]
|
|
49
46
|
const promises: Promise<void>[] = []
|
|
50
47
|
for (const uri of shapeCandidates) {
|
|
51
48
|
const url = this.toURL(uri.value)
|
|
@@ -71,6 +68,33 @@ export class Loader {
|
|
|
71
68
|
const parser = guessContentType(input) === 'xml' ? new RdfXmlParser() : new StreamParser()
|
|
72
69
|
parser.on('data', (quad: Quad) => {
|
|
73
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
|
+
}
|
|
74
98
|
})
|
|
75
99
|
.on('error', (error) => {
|
|
76
100
|
console.warn('failed parsing graph', graph, error.message)
|
|
@@ -111,78 +135,6 @@ export class Loader {
|
|
|
111
135
|
}
|
|
112
136
|
}
|
|
113
137
|
|
|
114
|
-
async fetchRDF(url: string): Promise<string> {
|
|
115
|
-
// try to load from cache first
|
|
116
|
-
if (url in loadedURLCache) {
|
|
117
|
-
console.log('--- cache hit', url)
|
|
118
|
-
return loadedURLCache[url]
|
|
119
|
-
}
|
|
120
|
-
let proxiedURL = url
|
|
121
|
-
// if we have a proxy configured, then load url via proxy
|
|
122
|
-
if (this.config.attributes.proxy) {
|
|
123
|
-
proxiedURL = this.config.attributes.proxy + encodeURIComponent(url)
|
|
124
|
-
}
|
|
125
|
-
const promise = fetch(proxiedURL, {
|
|
126
|
-
headers: {
|
|
127
|
-
'Accept': 'text/turtle, application/trig, application/n-triples, application/n-quads, text/n3, application/ld+json'
|
|
128
|
-
},
|
|
129
|
-
}).then(resp => resp.text())
|
|
130
|
-
loadedURLCache[url] = promise
|
|
131
|
-
return promise
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async fetchOwlImports(store: Store) {
|
|
135
|
-
if (this.config.attributes.ignoreOwlImports === null) {
|
|
136
|
-
const urls = new Set<string>()
|
|
137
|
-
// find all triples in all graphs of the form <s> <sh:class> <:className>
|
|
138
|
-
store.forObjects((url) => {
|
|
139
|
-
urls.add(url.value)
|
|
140
|
-
}, null, OWL_PREDICATE_IMPORTS, null)
|
|
141
|
-
|
|
142
|
-
const dependencies: Promise<void>[] = []
|
|
143
|
-
for (const url of urls) {
|
|
144
|
-
const convertedURL = this.toURL(url)
|
|
145
|
-
// import url only once
|
|
146
|
-
if (convertedURL) {
|
|
147
|
-
// import into separate graph
|
|
148
|
-
dependencies.push(this.importRDF(this.fetchRDF(convertedURL), store, DataFactory.namedNode(convertedURL)))
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return Promise.allSettled(dependencies)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async fetchClassInstances(store: Store) {
|
|
156
|
-
if (this.config.classInstanceProvider) {
|
|
157
|
-
const classNames = new Set<string>()
|
|
158
|
-
// find all triples in all graphs of the form <s> <sh:class> <:className>
|
|
159
|
-
store.forObjects((clazz) => {
|
|
160
|
-
classNames.add(clazz.value)
|
|
161
|
-
}, null, SHACL_PREDICATE_CLASS, null)
|
|
162
|
-
// find all triples in all graphs of the form <s> <sh:node> <o> and <o> <sh:targetClass> <:className>
|
|
163
|
-
store.forObjects((node) => {
|
|
164
|
-
store.forObjects((clazz) => {
|
|
165
|
-
classNames.add(clazz.value)
|
|
166
|
-
}, node, SHACL_PREDICATE_TARGET_CLASS, null)
|
|
167
|
-
}, null, SHACL_PREDICATE_NODE, null)
|
|
168
|
-
|
|
169
|
-
const dependencies: Promise<void>[] = []
|
|
170
|
-
for (const className of classNames) {
|
|
171
|
-
let promise: Promise<string>
|
|
172
|
-
// check if class is in module scope cache
|
|
173
|
-
if (className in loadedClassesCache) {
|
|
174
|
-
console.log('--- class cache hit', className)
|
|
175
|
-
promise = loadedClassesCache[className]
|
|
176
|
-
} else {
|
|
177
|
-
promise = this.config.classInstanceProvider(className)
|
|
178
|
-
loadedClassesCache[className] = promise
|
|
179
|
-
}
|
|
180
|
-
dependencies.push(this.importRDF(promise, store, SHAPES_GRAPH))
|
|
181
|
-
}
|
|
182
|
-
return Promise.all(dependencies)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
138
|
toURL(id: string): string | null {
|
|
187
139
|
if (isURL(id)) {
|
|
188
140
|
return id
|
|
@@ -203,6 +155,25 @@ export class Loader {
|
|
|
203
155
|
}
|
|
204
156
|
return null
|
|
205
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
|
+
}
|
|
206
177
|
}
|
|
207
178
|
|
|
208
179
|
/* Can't rely on HTTP content-type header, since many resources are delivered with text/plain */
|
package/src/property-template.ts
CHANGED
|
@@ -115,17 +115,15 @@ export class ShaclPropertyTemplate {
|
|
|
115
115
|
if (!this.label && !this.shaclAnd) {
|
|
116
116
|
this.label = this.path ? removePrefixes(this.path, this.config.prefixes) : 'unknown'
|
|
117
117
|
}
|
|
118
|
-
//
|
|
119
|
-
if (this.node
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
this.extendedShapes.push(node as NamedNode)
|
|
128
|
-
}
|
|
118
|
+
// register extended shapes
|
|
119
|
+
if (this.node) {
|
|
120
|
+
this.extendedShapes.push(this.node)
|
|
121
|
+
}
|
|
122
|
+
if (this.shaclAnd) {
|
|
123
|
+
const list = this.config.lists[this.shaclAnd]
|
|
124
|
+
if (list?.length) {
|
|
125
|
+
for (const node of list) {
|
|
126
|
+
this.extendedShapes.push(node as NamedNode)
|
|
129
127
|
}
|
|
130
128
|
}
|
|
131
129
|
}
|
package/src/property.ts
CHANGED
|
@@ -19,14 +19,13 @@ export class ShaclProperty extends HTMLElement {
|
|
|
19
19
|
constructor(shaclSubject: BlankNode | NamedNode, parent: ShaclNode, config: Config, valueSubject?: NamedNode | BlankNode) {
|
|
20
20
|
super()
|
|
21
21
|
this.template = new ShaclPropertyTemplate(config.store.getQuads(shaclSubject, null, null, null), parent, config)
|
|
22
|
+
this.container = this
|
|
22
23
|
if (this.template.extendedShapes.length && this.template.config.attributes.collapse !== null && (!this.template.maxCount || this.template.maxCount > 1)) {
|
|
23
24
|
const collapsible = new RokitCollapsible()
|
|
24
25
|
collapsible.classList.add('collapsible', 'shacl-group');
|
|
25
26
|
collapsible.open = config.attributes.collapse === 'open';
|
|
26
27
|
collapsible.label = this.template.label;
|
|
27
28
|
this.container = collapsible
|
|
28
|
-
} else {
|
|
29
|
-
this.container = this
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
if (this.template.order !== undefined) {
|
|
@@ -35,7 +34,6 @@ export class ShaclProperty extends HTMLElement {
|
|
|
35
34
|
if (this.template.cssClass) {
|
|
36
35
|
this.classList.add(this.template.cssClass)
|
|
37
36
|
}
|
|
38
|
-
|
|
39
37
|
if (config.editMode && !parent.linked) {
|
|
40
38
|
this.addButton = this.createAddButton()
|
|
41
39
|
this.container.appendChild(this.addButton)
|
package/src/serialize.ts
CHANGED
|
@@ -49,7 +49,9 @@ export function toRDF(editor: Editor): Literal | NamedNode | undefined {
|
|
|
49
49
|
let languageOrDatatype: NamedNode<string> | string | undefined = editor.shaclDatatype
|
|
50
50
|
let value: number | string = editor.value
|
|
51
51
|
if (value) {
|
|
52
|
-
if (
|
|
52
|
+
if (value.startsWith('<') && value.endsWith('>') && value.indexOf(':') > -1) {
|
|
53
|
+
return DataFactory.namedNode(value.substring(1, value.length - 1))
|
|
54
|
+
} else if (editor.dataset.class || editor.dataset.nodeKind === PREFIX_SHACL + 'IRI') {
|
|
53
55
|
return DataFactory.namedNode(value)
|
|
54
56
|
} else if (editor.dataset.link) {
|
|
55
57
|
return JSON.parse(editor.dataset.link)
|
package/src/themes/default.ts
CHANGED
|
@@ -220,8 +220,10 @@ export class DefaultTheme extends Theme {
|
|
|
220
220
|
} else {
|
|
221
221
|
if (entry.value instanceof Literal && entry.value.datatype.equals(XSD_DATATYPE_STRING)) {
|
|
222
222
|
li.dataset.value = entry.value.value
|
|
223
|
+
} else if (entry.value instanceof NamedNode) {
|
|
224
|
+
li.dataset.value = '<' + entry.value.value + ">"
|
|
223
225
|
} else {
|
|
224
|
-
// this is needed for typed rdf literals
|
|
226
|
+
// this is needed for typed rdf literals e.g. "ex"^^xsd:anyUri
|
|
225
227
|
li.dataset.value = (entry.value as N3Term).id
|
|
226
228
|
}
|
|
227
229
|
li.innerText = entry.label ? entry.label : entry.value.value
|