@ulb-darmstadt/shacl-form 1.7.3 → 1.7.4

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.
@@ -28,6 +28,7 @@ export declare class ShaclPropertyTemplate {
28
28
  shaclAnd: string | undefined;
29
29
  shaclIn: string | undefined;
30
30
  shaclOr: Term[] | undefined;
31
+ shaclXone: Term[] | undefined;
31
32
  languageIn: Term[] | undefined;
32
33
  datatype: NamedNode | undefined;
33
34
  hasValue: Term | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulb-darmstadt/shacl-form",
3
- "version": "1.7.3",
3
+ "version": "1.7.4",
4
4
  "description": "SHACL form generator",
5
5
  "main": "dist/form-default.js",
6
6
  "module": "dist/form-default.js",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/jsonld": "^1.5.15",
36
- "@types/leaflet": "^1.9.18",
36
+ "@types/leaflet": "^1.9.19",
37
37
  "@types/leaflet-editable": "^1.2.6",
38
38
  "@types/leaflet.fullscreen": "^3.0.2",
39
39
  "@types/mapbox__mapbox-gl-draw": "^1.4.9",
@@ -49,12 +49,12 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@mapbox/mapbox-gl-draw": "^1.5.0",
52
- "bootstrap": "^5.3.6",
52
+ "bootstrap": "^5.3.7",
53
53
  "jsonld": "^8.3.3",
54
54
  "leaflet": "^1.9.4",
55
55
  "leaflet-editable": "^1.3.1",
56
56
  "leaflet.fullscreen": "^4.0.0",
57
- "mapbox-gl": "^3.12.0",
57
+ "mapbox-gl": "^3.13.0",
58
58
  "mdui": "^2.1.4",
59
59
  "n3": "^1.26.0",
60
60
  "shacl-engine": "^1.0.2",
package/src/constants.ts CHANGED
@@ -20,6 +20,7 @@ export const OWL_PREDICATE_IMPORTS = DataFactory.namedNode(PREFIX_OWL + 'imports
20
20
  export const OWL_OBJECT_NAMED_INDIVIDUAL = DataFactory.namedNode(PREFIX_OWL + 'NamedIndividual')
21
21
  export const SHACL_OBJECT_NODE_SHAPE = DataFactory.namedNode(PREFIX_SHACL + 'NodeShape')
22
22
  export const SHACL_OBJECT_IRI = DataFactory.namedNode(PREFIX_SHACL + 'IRI')
23
+ export const SHACL_PREDICATE_PROPERTY = DataFactory.namedNode(PREFIX_SHACL + 'property')
23
24
  export const SHACL_PREDICATE_CLASS = DataFactory.namedNode(PREFIX_SHACL + 'class')
24
25
  export const SHACL_PREDICATE_TARGET_CLASS = DataFactory.namedNode(PREFIX_SHACL + 'targetClass')
25
26
  export const SHACL_PREDICATE_NODE_KIND = DataFactory.namedNode(PREFIX_SHACL + 'nodeKind')
@@ -3,7 +3,7 @@ import { Term } from '@rdfjs/types'
3
3
  import { ShaclNode } from "./node"
4
4
  import { ShaclProperty, createPropertyInstance } from "./property"
5
5
  import { Config } from './config'
6
- import { PREFIX_SHACL, RDF_PREDICATE_TYPE, SHACL_PREDICATE_CLASS, SHACL_PREDICATE_TARGET_CLASS, SHACL_PREDICATE_NODE_KIND, SHACL_OBJECT_IRI } from './constants'
6
+ import { PREFIX_SHACL, RDF_PREDICATE_TYPE, SHACL_PREDICATE_CLASS, SHACL_PREDICATE_TARGET_CLASS, SHACL_PREDICATE_NODE_KIND, SHACL_OBJECT_IRI, SHACL_PREDICATE_PROPERTY } from './constants'
7
7
  import { findLabel, removePrefixes } from './util'
8
8
  import { ShaclPropertyTemplate } from './property-template'
9
9
  import { Editor, InputListEntry } from './theme'
@@ -17,18 +17,45 @@ export function createShaclOrConstraint(options: Term[], context: ShaclNode | Sh
17
17
  optionElements.push({ label: '--- please choose ---', value: '' })
18
18
 
19
19
  if (context instanceof ShaclNode) {
20
- const properties: ShaclProperty[] = []
21
- // expect options to be shacl properties
20
+ const properties: ShaclProperty[][] = []
21
+ // options can be shacl properties or blank nodes referring to (list of) properties
22
+ let optionsAreReferencedProperties = false
23
+ if (options.length) {
24
+ optionsAreReferencedProperties = config.shapesGraph.getObjects(options[0] , SHACL_PREDICATE_PROPERTY, null).length > 0
25
+ }
22
26
  for (let i = 0; i < options.length; i++) {
23
- const property = new ShaclProperty(options[i] as NamedNode | BlankNode, context, config)
24
- properties.push(property)
25
- optionElements.push({ label: property.template.label, value: i.toString() })
27
+ if (optionsAreReferencedProperties) {
28
+ const quads = config.shapesGraph.getObjects(options[i] , SHACL_PREDICATE_PROPERTY, null)
29
+ // option can be single property or list of properties
30
+ const list: ShaclProperty[] = []
31
+ let combinedText = ''
32
+ for (const subject of quads) {
33
+ const property = new ShaclProperty(subject as NamedNode | BlankNode, context, config)
34
+ list.push(property)
35
+ combinedText += (combinedText.length > 1 ? ' / ' : '') + property.template.label
36
+ }
37
+ properties.push(list)
38
+ optionElements.push({ label: combinedText, value: i.toString() })
39
+ } else {
40
+ const property = new ShaclProperty(options[i] as NamedNode | BlankNode, context, config)
41
+ properties.push([property])
42
+ optionElements.push({ label: property.template.label, value: i.toString() })
43
+ }
26
44
  }
27
45
  const editor = config.theme.createListEditor('Please choose', null, false, optionElements)
28
46
  const select = editor.querySelector('.editor') as Editor
29
47
  select.onchange = () => {
30
48
  if (select.value) {
31
- constraintElement.replaceWith(properties[parseInt(select.value)])
49
+ const selectedOptions = properties[parseInt(select.value)]
50
+ let lastAddedProperty: ShaclProperty
51
+ if (selectedOptions.length) {
52
+ lastAddedProperty = selectedOptions[0]
53
+ constraintElement.replaceWith(selectedOptions[0])
54
+ }
55
+ for (let i = 1; i < selectedOptions.length; i++) {
56
+ lastAddedProperty!.after(selectedOptions[1])
57
+ lastAddedProperty = selectedOptions[1]
58
+ }
32
59
  }
33
60
  }
34
61
  constraintElement.appendChild(editor)
@@ -54,53 +81,73 @@ export function createShaclOrConstraint(options: Term[], context: ShaclNode | Sh
54
81
  return constraintElement
55
82
  }
56
83
 
57
- export function resolveShaclOrConstraint(template: ShaclPropertyTemplate, value: Term): ShaclPropertyTemplate {
58
- if (!template.shaclOr) {
59
- console.warn('can\'t resolve sh:or because template has no options', template)
60
- return template
61
- }
84
+ export function resolveShaclOrConstraintOnProperty(subjects: Term[], value: Term, config: Config): Quad[] {
62
85
  if (value instanceof Literal) {
63
- // value is a literal, try to resolve sh:or by matching on given value datatype
86
+ // value is a literal, try to resolve sh:or/sh:xone by matching on given value datatype
64
87
  const valueType = value.datatype
65
- for (const subject of template.shaclOr) {
66
- const options = template.config.shapesGraph.getQuads(subject, null, null, null)
88
+ for (const subject of subjects) {
89
+ const options = config.shapesGraph.getQuads(subject, null, null, null)
67
90
  for (const quad of options) {
68
91
  if (quad.predicate.value === `${PREFIX_SHACL}datatype` && quad.object.equals(valueType)) {
69
- return template.clone().merge(options)
92
+ return options
70
93
  }
71
94
  }
72
95
  }
73
96
  } else {
74
- // value is a NamedNode or BlankNode, try to resolve sh:or by matching rdf:type of given value with sh:node or sh:class in data graph or shapes graph
75
- let types = template.config.dataGraph.getObjects(value, RDF_PREDICATE_TYPE, null)
76
- types.push(...template.config.shapesGraph.getObjects(value, RDF_PREDICATE_TYPE, null))
77
- for (const subject of template.shaclOr) {
78
- const options = template.config.shapesGraph.getQuads(subject, null, null, null)
97
+ // value is a NamedNode or BlankNode, try to resolve sh:or/sh:xone by matching rdf:type of given value with sh:node or sh:class in data graph or shapes graph
98
+ const types = config.dataGraph.getObjects(value, RDF_PREDICATE_TYPE, null)
99
+ types.push(...config.shapesGraph.getObjects(value, RDF_PREDICATE_TYPE, null))
100
+ for (const subject of subjects) {
101
+ const options = config.shapesGraph.getQuads(subject, null, null, null)
79
102
  for (const quad of options) {
80
103
  if (types.length > 0) {
81
- // try to find matching sh:node in sh:or values
104
+ // try to find matching sh:node in sh:or/sh:xone values
82
105
  if (quad.predicate.value === `${PREFIX_SHACL}node`) {
83
106
  for (const type of types) {
84
- if (template.config.shapesGraph.getQuads(quad.object, SHACL_PREDICATE_TARGET_CLASS, type, null).length > 0) {
85
- return template.clone().merge(options)
107
+ if (config.shapesGraph.getQuads(quad.object, SHACL_PREDICATE_TARGET_CLASS, type, null).length > 0) {
108
+ return options
86
109
  }
87
110
  }
88
111
  }
89
- // try to find matching sh:class in sh:or values
112
+ // try to find matching sh:class in sh:or/sh:xone values
90
113
  if (quad.predicate.equals(SHACL_PREDICATE_CLASS)) {
91
114
  for (const type of types) {
92
115
  if (quad.object.equals(type)) {
93
- return template.clone().merge(options)
116
+ return options
94
117
  }
95
118
  }
96
119
  }
97
120
  } else if (quad.predicate.equals(SHACL_PREDICATE_NODE_KIND) && quad.object.equals(SHACL_OBJECT_IRI)) {
98
121
  // if sh:nodeKind is sh:IRI, just use that
99
- return template.clone().merge(options)
122
+ return options
123
+ }
124
+ }
125
+ }
126
+ }
127
+ console.error('couldn\'t resolve sh:or/sh:xone on property for value', value)
128
+ return []
129
+ }
130
+
131
+ export function resolveShaclOrConstraintOnNode(subjects: Term[], value: Term, config: Config): Term[] {
132
+ for (const subject of subjects) {
133
+ let subjectMatches = true
134
+ const propertySubjects = config.shapesGraph.getObjects(subject, SHACL_PREDICATE_PROPERTY, null)
135
+ for (const propertySubject of propertySubjects) {
136
+ const paths = config.shapesGraph.getObjects(propertySubject, `${PREFIX_SHACL}path`, null)
137
+ for (const path of paths) {
138
+ const values = config.dataGraph.getObjects(value, path, null)
139
+ values.push(...config.shapesGraph.getObjects(value, path, null))
140
+ if (!values.length) {
141
+ subjectMatches = false
142
+ break
100
143
  }
101
144
  }
102
145
  }
146
+ if (subjectMatches) {
147
+ return propertySubjects
148
+ }
103
149
  }
104
- console.error('couldn\'t resolve sh:or for value', value)
105
- return template
150
+
151
+ console.error('couldn\'t resolve sh:or/sh:xone on node for value', value)
152
+ return []
106
153
  }
package/src/node.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { BlankNode, DataFactory, NamedNode, Store } from 'n3'
2
2
  import { Term } from '@rdfjs/types'
3
- import { PREFIX_SHACL, RDF_PREDICATE_TYPE, OWL_PREDICATE_IMPORTS } from './constants'
3
+ import { PREFIX_SHACL, RDF_PREDICATE_TYPE, OWL_PREDICATE_IMPORTS, SHACL_PREDICATE_PROPERTY } from './constants'
4
4
  import { ShaclProperty } from './property'
5
5
  import { createShaclGroup } from './group'
6
6
  import { v4 as uuidv4 } from 'uuid'
7
- import { createShaclOrConstraint } from './constraints'
7
+ import { createShaclOrConstraint, resolveShaclOrConstraintOnNode } from './constraints'
8
8
  import { Config } from './config'
9
9
 
10
10
  export class ShaclNode extends HTMLElement {
@@ -76,33 +76,8 @@ export class ShaclNode extends HTMLElement {
76
76
 
77
77
  for (const quad of quads) {
78
78
  switch (quad.predicate.id) {
79
- case `${PREFIX_SHACL}property`:
80
- let parentElement: HTMLElement = this
81
- // check if property belongs to a group
82
- const groupRef = config.shapesGraph.getQuads(quad.object as Term, `${PREFIX_SHACL}group`, null, null)
83
- if (groupRef.length > 0) {
84
- const groupSubject = groupRef[0].object.value
85
- if (config.groups.indexOf(groupSubject) > -1) {
86
- // check if group element already exists, otherwise create it
87
- let group = this.querySelector(`:scope > .shacl-group[data-subject='${groupSubject}']`) as HTMLElement
88
- if (!group) {
89
- group = createShaclGroup(groupSubject, config)
90
- this.appendChild(group)
91
- }
92
- parentElement = group
93
- } else {
94
- console.warn('ignoring unknown group reference', groupRef[0], 'existing groups:', config.groups)
95
- }
96
- }
97
- // delay creating/appending the property until we finished parsing the node.
98
- // This is needed to have possible owlImports parsed before creating the property.
99
- setTimeout(() => {
100
- const property = new ShaclProperty(quad.object as NamedNode | BlankNode, this, config, valueSubject)
101
- // do not add empty properties (i.e. properties with no instances). This can be the case e.g. in viewer mode when there is no data for the respective property.
102
- if (property.childElementCount > 0) {
103
- parentElement.appendChild(property)
104
- }
105
- })
79
+ case SHACL_PREDICATE_PROPERTY.id:
80
+ this.addPropertyInstance(quad.object, config, valueSubject)
106
81
  break;
107
82
  case `${PREFIX_SHACL}and`:
108
83
  // inheritance via sh:and
@@ -127,13 +102,10 @@ export class ShaclNode extends HTMLElement {
127
102
  this.owlImports.push(quad.object as NamedNode)
128
103
  break;
129
104
  case `${PREFIX_SHACL}or`:
130
- list = config.lists[quad.object.value]
131
- if (list?.length) {
132
- this.appendChild(createShaclOrConstraint(list, this, config))
133
- }
134
- else {
135
- console.error('list not found:', quad.object.value, 'existing lists:', config.lists)
136
- }
105
+ this.tryResolve(quad.object, valueSubject, config)
106
+ break;
107
+ case `${PREFIX_SHACL}xone`:
108
+ this.tryResolve(quad.object, valueSubject, config)
137
109
  break;
138
110
  }
139
111
  }
@@ -162,6 +134,57 @@ export class ShaclNode extends HTMLElement {
162
134
  }
163
135
  return subject
164
136
  }
137
+
138
+ addPropertyInstance(shaclSubject: Term, config: Config, valueSubject: NamedNode | BlankNode | undefined) {
139
+ let parentElement: HTMLElement = this
140
+ // check if property belongs to a group
141
+ const groupRef = config.shapesGraph.getQuads(shaclSubject as Term, `${PREFIX_SHACL}group`, null, null)
142
+ if (groupRef.length > 0) {
143
+ const groupSubject = groupRef[0].object.value
144
+ if (config.groups.indexOf(groupSubject) > -1) {
145
+ // check if group element already exists, otherwise create it
146
+ let group = this.querySelector(`:scope > .shacl-group[data-subject='${groupSubject}']`) as HTMLElement
147
+ if (!group) {
148
+ group = createShaclGroup(groupSubject, config)
149
+ this.appendChild(group)
150
+ }
151
+ parentElement = group
152
+ } else {
153
+ console.warn('ignoring unknown group reference', groupRef[0], 'existing groups:', config.groups)
154
+ }
155
+ }
156
+ // delay creating/appending the property until we finished parsing the node.
157
+ // This is needed to have possible owlImports parsed before creating the property.
158
+ setTimeout(() => {
159
+ const property = new ShaclProperty(shaclSubject as NamedNode | BlankNode, this, config, valueSubject)
160
+ // do not add empty properties (i.e. properties with no instances). This can be the case e.g. in viewer mode when there is no data for the respective property.
161
+ if (property.childElementCount > 0) {
162
+ parentElement.appendChild(property)
163
+ }
164
+ })
165
+ }
166
+
167
+ tryResolve(subject: Term, valueSubject: NamedNode | BlankNode | undefined, config: Config) {
168
+ const list = config.lists[subject.value]
169
+ if (list?.length) {
170
+ let resolved = false
171
+ if (valueSubject) {
172
+ const resolvedPropertySubjects = resolveShaclOrConstraintOnNode(list, valueSubject, config)
173
+ if (resolvedPropertySubjects.length) {
174
+ for (const propertySubject of resolvedPropertySubjects) {
175
+ this.addPropertyInstance(propertySubject, config, valueSubject)
176
+ }
177
+ resolved = true
178
+ }
179
+ }
180
+ if (!resolved) {
181
+ this.appendChild(createShaclOrConstraint(list, this, config))
182
+ }
183
+ }
184
+ else {
185
+ console.error('list for sh:or/sh:xone not found:', subject, 'existing lists:', config.lists)
186
+ }
187
+ }
165
188
  }
166
189
 
167
190
  window.customElements.define('shacl-node', ShaclNode)
@@ -34,8 +34,8 @@ const mappers: Record<string, (template: ShaclPropertyTemplate, term: Term) => v
34
34
  [`${PREFIX_SHACL}qualifiedValueShape`]: (template, term) => { template.qualifiedValueShape = term },
35
35
  [`${PREFIX_SHACL}qualifiedMinCount`]: (template, term) => { template.minCount = parseInt(term.value) },
36
36
  [`${PREFIX_SHACL}qualifiedMaxCount`]: (template, term) => { template.maxCount = parseInt(term.value) },
37
- [OWL_PREDICATE_IMPORTS.id]: (template, term) => { template.owlImports.push(term as NamedNode) },
38
- [SHACL_PREDICATE_CLASS.id]: (template, term) => {
37
+ [OWL_PREDICATE_IMPORTS.id]: (template, term) => { template.owlImports.push(term as NamedNode) },
38
+ [SHACL_PREDICATE_CLASS.id]: (template, term) => {
39
39
  template.class = term as NamedNode
40
40
  // try to find node shape that has requested target class
41
41
  const nodeShapes = template.config.shapesGraph.getSubjects(SHACL_PREDICATE_TARGET_CLASS, term, null)
@@ -48,7 +48,15 @@ const mappers: Record<string, (template: ShaclPropertyTemplate, term: Term) => v
48
48
  if (list?.length) {
49
49
  template.shaclOr = list
50
50
  } else {
51
- console.error('list not found:', term.value, 'existing lists:', template.config.lists)
51
+ console.error('list for sh:or not found:', term.value, 'existing lists:', template.config.lists)
52
+ }
53
+ },
54
+ [`${PREFIX_SHACL}xone`]: (template, term) => {
55
+ const list = template.config.lists[term.value]
56
+ if (list?.length) {
57
+ template.shaclXone = list
58
+ } else {
59
+ console.error('list for sh:xone not found:', term.value, 'existing lists:', template.config.lists)
52
60
  }
53
61
  }
54
62
  }
@@ -79,6 +87,7 @@ export class ShaclPropertyTemplate {
79
87
  shaclAnd: string | undefined
80
88
  shaclIn: string | undefined
81
89
  shaclOr: Term[] | undefined
90
+ shaclXone: Term[] | undefined
82
91
  languageIn: Term[] | undefined
83
92
  datatype: NamedNode | undefined
84
93
  hasValue: Term | undefined
@@ -134,6 +143,9 @@ export class ShaclPropertyTemplate {
134
143
  if (this.shaclOr) {
135
144
  copy.shaclOr = [ ...this.shaclOr ]
136
145
  }
146
+ if (this.shaclXone) {
147
+ copy.shaclXone = [ ...this.shaclXone ]
148
+ }
137
149
  copy.merge = this.merge.bind(copy)
138
150
  copy.clone = this.clone.bind(copy)
139
151
  return copy
package/src/property.ts CHANGED
@@ -2,7 +2,7 @@ import { BlankNode, DataFactory, NamedNode, Store } from 'n3'
2
2
  import { Term } from '@rdfjs/types'
3
3
  import { ShaclNode } from './node'
4
4
  import { focusFirstInputElement } from './util'
5
- import { createShaclOrConstraint, resolveShaclOrConstraint } from './constraints'
5
+ import { createShaclOrConstraint, resolveShaclOrConstraintOnProperty } from './constraints'
6
6
  import { Config } from './config'
7
7
  import { ShaclPropertyTemplate } from './property-template'
8
8
  import { Editor, fieldFactory } from './theme'
@@ -100,22 +100,29 @@ export class ShaclProperty extends HTMLElement {
100
100
 
101
101
  addPropertyInstance(value?: Term): HTMLElement {
102
102
  let instance: HTMLElement
103
- if (this.template.shaclOr?.length) {
103
+ if (this.template.shaclOr?.length || this.template.shaclXone?.length) {
104
+ const options = this.template.shaclOr?.length ? this.template.shaclOr : this.template.shaclXone as Term[]
105
+ let resolved = false
104
106
  if (value) {
105
- instance = createPropertyInstance(resolveShaclOrConstraint(this.template, value), value, true)
106
- } else {
107
- instance = createShaclOrConstraint(this.template.shaclOr, this, this.template.config)
107
+ const resolvedOptions = resolveShaclOrConstraintOnProperty(options, value, this.template.config)
108
+ if (resolvedOptions.length) {
109
+ instance = createPropertyInstance(this.template.clone().merge(resolvedOptions), value, true)
110
+ resolved = true
111
+ }
112
+ }
113
+ if (!resolved) {
114
+ instance = createShaclOrConstraint(options, this, this.template.config)
108
115
  appendRemoveButton(instance, '')
109
116
  }
110
117
  } else {
111
118
  instance = createPropertyInstance(this.template, value)
112
119
  }
113
120
  if (this.template.config.editMode) {
114
- this.insertBefore(instance, this.addButton!)
121
+ this.insertBefore(instance!, this.addButton!)
115
122
  } else {
116
- this.appendChild(instance)
123
+ this.appendChild(instance!)
117
124
  }
118
- return instance
125
+ return instance!
119
126
  }
120
127
 
121
128
  updateControls() {