@lblod/ember-rdfa-editor-lblod-plugins 32.3.0 → 32.4.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/CHANGELOG.md +17 -0
- package/addon/components/document-validation-plugin/card.gts +137 -0
- package/addon/components/location-plugin/insert-location-placeholder.gts +76 -0
- package/addon/components/location-plugin/insert.gts +2 -1
- package/addon/components/location-plugin/nodeview.gts +1 -1
- package/addon/components/roadsign-regulation-plugin/expanded-measure.gts +5 -1
- package/addon/components/roadsign-regulation-plugin/roadsigns-modal.gts +1 -1
- package/addon/plugins/document-validation-plugin/index.ts +196 -0
- package/addon/plugins/location-plugin/_private/node-contents/address.ts +252 -0
- package/addon/plugins/location-plugin/_private/node-contents/area.ts +64 -0
- package/addon/plugins/location-plugin/_private/node-contents/index.ts +46 -0
- package/addon/plugins/location-plugin/_private/node-contents/place.ts +64 -0
- package/addon/plugins/location-plugin/_private/node-contents/point.ts +89 -0
- package/addon/plugins/location-plugin/_private/utils/address-helpers.ts +407 -0
- package/addon/plugins/location-plugin/_private/utils/geo-helpers.ts +210 -0
- package/addon/plugins/location-plugin/_private/utils/node-utils.ts +82 -0
- package/addon/plugins/location-plugin/node-contents/address.ts +2 -252
- package/addon/plugins/location-plugin/node-contents/area.ts +2 -64
- package/addon/plugins/location-plugin/node-contents/index.ts +2 -46
- package/addon/plugins/location-plugin/node-contents/place.ts +2 -64
- package/addon/plugins/location-plugin/node-contents/point.ts +2 -89
- package/addon/plugins/location-plugin/node.ts +48 -16
- package/addon/plugins/location-plugin/utils/address-helpers.ts +2 -407
- package/addon/plugins/location-plugin/utils/geo-helpers.ts +2 -210
- package/addon/plugins/location-plugin/utils/node-utils.ts +10 -61
- package/addon/plugins/roadsign-regulation-plugin/queries/mobility-measure-concept.ts +3 -1
- package/addon/utils/remove-quotes.ts +7 -0
- package/app/components/document-validation-plugin/card.js +1 -0
- package/app/components/location-plugin/insert-location-placeholder.js +1 -0
- package/app/styles/document-validation.scss +26 -0
- package/declarations/addon/components/document-validation-plugin/card.d.ts +22 -0
- package/declarations/addon/components/location-plugin/insert-location-placeholder.d.ts +20 -0
- package/declarations/addon/plugins/document-validation-plugin/index.d.ts +28 -0
- package/declarations/addon/plugins/location-plugin/_private/node-contents/address.d.ts +11 -0
- package/declarations/addon/plugins/location-plugin/_private/node-contents/area.d.ts +9 -0
- package/declarations/addon/plugins/location-plugin/_private/node-contents/index.d.ts +32 -0
- package/declarations/addon/plugins/location-plugin/_private/node-contents/place.d.ts +9 -0
- package/declarations/addon/plugins/location-plugin/_private/node-contents/point.d.ts +6 -0
- package/declarations/addon/plugins/location-plugin/_private/utils/address-helpers.d.ts +62 -0
- package/declarations/addon/plugins/location-plugin/_private/utils/geo-helpers.d.ts +82 -0
- package/declarations/addon/plugins/location-plugin/_private/utils/node-utils.d.ts +17 -0
- package/declarations/addon/plugins/location-plugin/node-contents/address.d.ts +2 -11
- package/declarations/addon/plugins/location-plugin/node-contents/area.d.ts +2 -9
- package/declarations/addon/plugins/location-plugin/node-contents/index.d.ts +2 -32
- package/declarations/addon/plugins/location-plugin/node-contents/place.d.ts +2 -9
- package/declarations/addon/plugins/location-plugin/node-contents/point.d.ts +2 -6
- package/declarations/addon/plugins/location-plugin/utils/address-helpers.d.ts +2 -62
- package/declarations/addon/plugins/location-plugin/utils/geo-helpers.d.ts +2 -82
- package/declarations/addon/plugins/location-plugin/utils/node-utils.d.ts +1 -0
- package/declarations/addon/utils/remove-quotes.d.ts +1 -0
- package/package.json +7 -3
- package/pnpm-lock.yaml +1527 -1163
- package/translations/en-US.yaml +9 -0
- package/translations/nl-BE.yaml +9 -0
- package/types/global.d.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @lblod/ember-rdfa-editor-lblod-plugins
|
|
2
2
|
|
|
3
|
+
## 32.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#582](https://github.com/lblod/ember-rdfa-editor-lblod-plugins/pull/582) [`96baaf4`](https://github.com/lblod/ember-rdfa-editor-lblod-plugins/commit/96baaf43e1a976b167eea65f4d0255b6e74cd85f) Thanks [@lagartoverde](https://github.com/lagartoverde)! - Add new plugin for document validation with SHACL
|
|
8
|
+
|
|
9
|
+
- [#584](https://github.com/lblod/ember-rdfa-editor-lblod-plugins/pull/584) [`b8f445a`](https://github.com/lblod/ember-rdfa-editor-lblod-plugins/commit/b8f445ae720134f2dd579cb7c831812210c7c8f7) Thanks [@elpoelma](https://github.com/elpoelma)! - Add simple placeholder-mode to location variable
|
|
10
|
+
To correctly identify location placeholder nodes, the `data-say-variable` and `data-say-variable-type: oslo_location` attributes have been added to the serialization of oslo location nodes. This is similar to the approach used by other variable nodes.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- [#586](https://github.com/lblod/ember-rdfa-editor-lblod-plugins/pull/586) [`810f867`](https://github.com/lblod/ember-rdfa-editor-lblod-plugins/commit/810f86766ac5fbc11e20a68545ab950ec07f61a7) Thanks [@lagartoverde](https://github.com/lagartoverde)! - Fix bug in roadsign regulation plugin where you couldn't search a sign if you had selected a category
|
|
15
|
+
|
|
16
|
+
- [#587](https://github.com/lblod/ember-rdfa-editor-lblod-plugins/pull/587) [`d5914c0`](https://github.com/lblod/ember-rdfa-editor-lblod-plugins/commit/d5914c0ce63dba5aca15fbb00d634e811ece41b9) Thanks [@elpoelma](https://github.com/elpoelma)! - roadsign-regulation plugin: fix issue where measure could not be inserted as being variable/dynamic
|
|
17
|
+
|
|
18
|
+
- [#582](https://github.com/lblod/ember-rdfa-editor-lblod-plugins/pull/582) [`b21bd08`](https://github.com/lblod/ember-rdfa-editor-lblod-plugins/commit/b21bd08744fd58b7e7e7286d9f31da7de692defe) Thanks [@lagartoverde](https://github.com/lagartoverde)! - Fix missing translations in citation-plugin
|
|
19
|
+
|
|
3
20
|
## 32.3.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { SayController } from '@lblod/ember-rdfa-editor';
|
|
3
|
+
import AuCard from '@appuniversum/ember-appuniversum/components/au-card';
|
|
4
|
+
import { documentValidationPluginKey } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/document-validation-plugin';
|
|
5
|
+
import AuIcon from '@appuniversum/ember-appuniversum/components/au-icon';
|
|
6
|
+
import { CloseFilledIcon } from '@appuniversum/ember-appuniversum/components/icons/close-filled';
|
|
7
|
+
import { CheckFilledIcon } from '@appuniversum/ember-appuniversum/components/icons/check-filled';
|
|
8
|
+
import { selectNodeBySubject } from '@lblod/ember-rdfa-editor/commands/_private/rdfa-commands/select-node-by-subject';
|
|
9
|
+
import { on } from '@ember/modifier';
|
|
10
|
+
import { fn } from '@ember/helper';
|
|
11
|
+
import AuButton from '@appuniversum/ember-appuniversum/components/au-button';
|
|
12
|
+
import { ExternalLinkIcon } from '@appuniversum/ember-appuniversum/components/icons/external-link';
|
|
13
|
+
import t from 'ember-intl/helpers/t';
|
|
14
|
+
import { eq } from 'ember-truth-helpers';
|
|
15
|
+
|
|
16
|
+
interface Sig {
|
|
17
|
+
Args: {
|
|
18
|
+
controller: SayController;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default class DocumentValidationPluginCard extends Component<Sig> {
|
|
23
|
+
get controller() {
|
|
24
|
+
return this.args.controller;
|
|
25
|
+
}
|
|
26
|
+
get validationState() {
|
|
27
|
+
const state = documentValidationPluginKey.getState(
|
|
28
|
+
this.controller.mainEditorState,
|
|
29
|
+
);
|
|
30
|
+
if (!state) {
|
|
31
|
+
console.warn(
|
|
32
|
+
'DocumentValidationPluginCard needs the documentValidation plugin to function. Is it configured?',
|
|
33
|
+
);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return state;
|
|
37
|
+
}
|
|
38
|
+
get documentValidationErrors() {
|
|
39
|
+
if (!this.validationState) return [];
|
|
40
|
+
const { propertiesWithErrors } = this.validationState;
|
|
41
|
+
if (!propertiesWithErrors) return undefined;
|
|
42
|
+
|
|
43
|
+
return propertiesWithErrors;
|
|
44
|
+
}
|
|
45
|
+
get propertiesWithoutErrors() {
|
|
46
|
+
if (!this.validationState) return [];
|
|
47
|
+
const { propertiesWithoutErrors } = this.validationState;
|
|
48
|
+
return propertiesWithoutErrors;
|
|
49
|
+
}
|
|
50
|
+
goToSubject = (subject: string) => {
|
|
51
|
+
this.controller.doCommand(selectNodeBySubject({ subject }), {
|
|
52
|
+
view: this.controller.mainEditorView,
|
|
53
|
+
});
|
|
54
|
+
this.controller.focus();
|
|
55
|
+
};
|
|
56
|
+
get status() {
|
|
57
|
+
if (!this.validationState?.report) return 'not-run';
|
|
58
|
+
if (this.documentValidationErrors?.length === 0) {
|
|
59
|
+
if (this.propertiesWithoutErrors?.length === 0) return 'no-matches';
|
|
60
|
+
return 'valid';
|
|
61
|
+
}
|
|
62
|
+
return 'invalid';
|
|
63
|
+
}
|
|
64
|
+
get isSuccesslike() {
|
|
65
|
+
return ['valid', 'no-matches'].includes(this.status);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<AuCard
|
|
70
|
+
@flex={{true}}
|
|
71
|
+
@divided={{true}}
|
|
72
|
+
@isOpenInitially={{true}}
|
|
73
|
+
@expandable={{true}}
|
|
74
|
+
@shadow={{true}}
|
|
75
|
+
@size='small'
|
|
76
|
+
class={{if
|
|
77
|
+
this.isSuccesslike
|
|
78
|
+
'say-document-validation__card-valid'
|
|
79
|
+
'say-document-validation__card-invalid'
|
|
80
|
+
}}
|
|
81
|
+
as |c|
|
|
82
|
+
>
|
|
83
|
+
<c.header>
|
|
84
|
+
<p class='au-u-medium au-u-h6'>
|
|
85
|
+
{{#if (eq this.status 'valid')}}
|
|
86
|
+
{{t 'document-validation-plugin.valid-document-title'}}
|
|
87
|
+
{{else if (eq this.status 'invalid')}}
|
|
88
|
+
{{t 'document-validation-plugin.invalid-document-title'}}
|
|
89
|
+
{{else if (eq this.status 'not-run')}}
|
|
90
|
+
{{t 'document-validation-plugin.document-not-validated-title'}}
|
|
91
|
+
{{else if (eq this.status 'no-matches')}}
|
|
92
|
+
{{t 'document-validation-plugin.no-matching-rules'}}
|
|
93
|
+
{{/if}}
|
|
94
|
+
</p>
|
|
95
|
+
</c.header>
|
|
96
|
+
<c.content>
|
|
97
|
+
<p class='au-u-medium au-u-para-small'>{{t
|
|
98
|
+
'document-validation-plugin.description'
|
|
99
|
+
}}</p>
|
|
100
|
+
{{#each this.documentValidationErrors as |error|}}
|
|
101
|
+
<div class='say-document-validation__error-container'>
|
|
102
|
+
<AuIcon
|
|
103
|
+
@icon={{CloseFilledIcon}}
|
|
104
|
+
@size='large'
|
|
105
|
+
@ariaHidden={{true}}
|
|
106
|
+
class='say-document-validation__icon-error au-u-margin-right-small'
|
|
107
|
+
/>
|
|
108
|
+
<div>
|
|
109
|
+
{{error.message}}
|
|
110
|
+
<AuButton
|
|
111
|
+
class='au-u-padding-left-none au-u-padding-right-none'
|
|
112
|
+
@icon={{ExternalLinkIcon}}
|
|
113
|
+
@skin='link'
|
|
114
|
+
title={{error.subject}}
|
|
115
|
+
{{on 'click' (fn this.goToSubject error.subject)}}
|
|
116
|
+
>{{t 'document-validation-plugin.see-related-node'}}</AuButton>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
{{/each}}
|
|
120
|
+
{{#each this.propertiesWithoutErrors as |property|}}
|
|
121
|
+
<div class='say-document-validation__error-container'>
|
|
122
|
+
<div class='au-u-margin-right-small'>
|
|
123
|
+
<AuIcon
|
|
124
|
+
@icon={{CheckFilledIcon}}
|
|
125
|
+
@size='large'
|
|
126
|
+
@ariaHidden={{true}}
|
|
127
|
+
class='say-document-validation__icon-success'
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
{{property.message}}
|
|
131
|
+
</div>
|
|
132
|
+
{{/each}}
|
|
133
|
+
</c.content>
|
|
134
|
+
|
|
135
|
+
</AuCard>
|
|
136
|
+
</template>
|
|
137
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { service } from '@ember/service';
|
|
3
|
+
import { on } from '@ember/modifier';
|
|
4
|
+
import not from 'ember-truth-helpers/helpers/not';
|
|
5
|
+
import IntlService from 'ember-intl/services/intl';
|
|
6
|
+
import AuButton from '@appuniversum/ember-appuniversum/components/au-button';
|
|
7
|
+
import { AddIcon } from '@appuniversum/ember-appuniversum/components/icons/add';
|
|
8
|
+
import { NodeSelection, SayController } from '@lblod/ember-rdfa-editor';
|
|
9
|
+
import { type LocationPluginConfig } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/location-plugin/node';
|
|
10
|
+
import { action } from '@ember/object';
|
|
11
|
+
import t from 'ember-intl/helpers/t';
|
|
12
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
13
|
+
import { replaceSelectionWithLocation } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/location-plugin/_private/utils/node-utils';
|
|
14
|
+
|
|
15
|
+
interface Signature {
|
|
16
|
+
Args: {
|
|
17
|
+
controller: SayController;
|
|
18
|
+
config: LocationPluginConfig;
|
|
19
|
+
templateMode?: boolean;
|
|
20
|
+
};
|
|
21
|
+
Element: HTMLLIElement;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default class LocationPluginInsertComponent extends Component<Signature> {
|
|
25
|
+
@service declare intl: IntlService;
|
|
26
|
+
|
|
27
|
+
get selectedLocationNode() {
|
|
28
|
+
const { selection, schema } = this.controller.activeEditorState;
|
|
29
|
+
if (
|
|
30
|
+
selection instanceof NodeSelection &&
|
|
31
|
+
selection.node.type === schema.nodes.oslo_location
|
|
32
|
+
) {
|
|
33
|
+
return selection.node;
|
|
34
|
+
} else {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get controller() {
|
|
40
|
+
return this.args.controller;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get canInsert() {
|
|
44
|
+
return (
|
|
45
|
+
!!this.controller.activeEditorView.props.nodeViews?.oslo_location &&
|
|
46
|
+
!this.selectedLocationNode
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@action
|
|
51
|
+
insertLocationPlaceholder() {
|
|
52
|
+
const uri = `http://data.lblod.info/variables/${
|
|
53
|
+
this.args.templateMode ? '--ref-uuid4-' : ''
|
|
54
|
+
}${uuidv4()}`;
|
|
55
|
+
replaceSelectionWithLocation(
|
|
56
|
+
this.controller,
|
|
57
|
+
uri,
|
|
58
|
+
undefined,
|
|
59
|
+
this.args.config.subjectTypesToLinkTo,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<li class='au-c-list__item' ...attributes>
|
|
65
|
+
<AuButton
|
|
66
|
+
@icon={{AddIcon}}
|
|
67
|
+
@iconAlignment='left'
|
|
68
|
+
@skin='link'
|
|
69
|
+
{{on 'click' this.insertLocationPlaceholder}}
|
|
70
|
+
@disabled={{not this.canInsert}}
|
|
71
|
+
>
|
|
72
|
+
{{t 'location-plugin.insert-placeholder'}}
|
|
73
|
+
</AuButton>
|
|
74
|
+
</li>
|
|
75
|
+
</template>
|
|
76
|
+
}
|
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
Point,
|
|
25
25
|
Polygon,
|
|
26
26
|
} from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/location-plugin/utils/geo-helpers';
|
|
27
|
-
import { replaceSelectionWithLocation } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/location-plugin/utils/node-utils';
|
|
28
27
|
import { type LocationPluginConfig } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/location-plugin/node';
|
|
29
28
|
import { NodeContentsUtils } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/location-plugin/node-contents';
|
|
30
29
|
import Edit from './edit';
|
|
@@ -34,6 +33,7 @@ import LocationMap, {
|
|
|
34
33
|
} from './map';
|
|
35
34
|
import { transactionCombinator } from '@lblod/ember-rdfa-editor/utils/transaction-utils';
|
|
36
35
|
import { updateSubject } from '@lblod/ember-rdfa-editor/plugins/rdfa-info/utils';
|
|
36
|
+
import { replaceSelectionWithLocation } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/location-plugin/_private/utils/node-utils';
|
|
37
37
|
|
|
38
38
|
export type CurrentLocation = Address | GlobalCoordinates | undefined;
|
|
39
39
|
|
|
@@ -293,6 +293,7 @@ export default class LocationPluginInsertComponent extends Component<Signature>
|
|
|
293
293
|
// Insert
|
|
294
294
|
replaceSelectionWithLocation(
|
|
295
295
|
this.controller,
|
|
296
|
+
toInsert.uri,
|
|
296
297
|
toInsert,
|
|
297
298
|
this.args.config.subjectTypesToLinkTo,
|
|
298
299
|
);
|
|
@@ -42,7 +42,7 @@ export default class AddressNodeviewComponent extends Component<Signature> {
|
|
|
42
42
|
if (this.filled) {
|
|
43
43
|
return this.address?.formatted;
|
|
44
44
|
} else {
|
|
45
|
-
return this.label;
|
|
45
|
+
return this.label ?? this.intl.t('location-plugin.nodeview.placeholder');
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
get class() {
|
|
@@ -12,6 +12,7 @@ import { action } from '@ember/object';
|
|
|
12
12
|
import { on } from '@ember/modifier';
|
|
13
13
|
import { ZONALITY_OPTIONS } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/roadsign-regulation-plugin/constants';
|
|
14
14
|
import { Task } from 'ember-concurrency';
|
|
15
|
+
import { isSome } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/option';
|
|
15
16
|
|
|
16
17
|
export type InsertMobilityMeasureTask = Task<
|
|
17
18
|
void,
|
|
@@ -41,7 +42,10 @@ export default class ExpandedMeasure extends Component<Signature> {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
get insertButtonDisabled() {
|
|
44
|
-
return
|
|
45
|
+
return (
|
|
46
|
+
(this.isPotentiallyZonal && !this.zonalityValue) ||
|
|
47
|
+
(this.args.concept.variableSignage && !isSome(this.temporalValue))
|
|
48
|
+
);
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
@action
|
|
@@ -151,7 +151,7 @@ export default class RoadsignsModal extends Component<Signature> {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
searchCodes = restartableTask(async (term: string) => {
|
|
154
|
-
const category = this.selectedCategory?.
|
|
154
|
+
const category = this.selectedCategory?.uri;
|
|
155
155
|
const type = this.selectedType?.label;
|
|
156
156
|
const types = type ? [type] : undefined;
|
|
157
157
|
await timeout(DEBOUNCE_MS);
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import factory from '@rdfjs/dataset';
|
|
2
|
+
import SHACLValidator from 'rdf-validate-shacl';
|
|
3
|
+
import { Parser as ParserN3 } from 'n3';
|
|
4
|
+
import { RdfaParser } from 'rdfa-streaming-parser';
|
|
5
|
+
import { ProsePlugin, PluginKey, EditorView } from '@lblod/ember-rdfa-editor';
|
|
6
|
+
import removeQuotes from '@lblod/ember-rdfa-editor-lblod-plugins/utils/remove-quotes';
|
|
7
|
+
import {
|
|
8
|
+
BlankNode,
|
|
9
|
+
DataFactory,
|
|
10
|
+
DatasetCore,
|
|
11
|
+
DatasetCoreFactory,
|
|
12
|
+
NamedNode,
|
|
13
|
+
Quad,
|
|
14
|
+
} from '@rdfjs/types';
|
|
15
|
+
import ValidationReport from 'rdf-validate-shacl/src/validation-report';
|
|
16
|
+
import { SayDataFactory } from '@lblod/ember-rdfa-editor/core/say-data-factory';
|
|
17
|
+
|
|
18
|
+
export const documentValidationPluginKey =
|
|
19
|
+
new PluginKey<DocumentValidationPluginState>('DOCUMENT_VALIDATION');
|
|
20
|
+
|
|
21
|
+
interface DocumentValidationPluginArgs {
|
|
22
|
+
documentShape: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ShaclValidationReport = ValidationReport.ValidationReport<
|
|
26
|
+
DataFactory<Quad, Quad> &
|
|
27
|
+
DatasetCoreFactory<Quad, Quad, DatasetCore<Quad, Quad>>
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
interface DocumentValidationResult {
|
|
31
|
+
report?: ValidationReport;
|
|
32
|
+
propertiesWithoutErrors: { message: string }[];
|
|
33
|
+
propertiesWithErrors: (
|
|
34
|
+
| {
|
|
35
|
+
message: string;
|
|
36
|
+
subject: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
// TODO get rid of this?
|
|
39
|
+
| undefined
|
|
40
|
+
)[];
|
|
41
|
+
}
|
|
42
|
+
export interface DocumentValidationTransactionMeta
|
|
43
|
+
extends DocumentValidationResult {
|
|
44
|
+
type: string;
|
|
45
|
+
}
|
|
46
|
+
export interface DocumentValidationPluginState
|
|
47
|
+
extends DocumentValidationResult {
|
|
48
|
+
documentShape: string;
|
|
49
|
+
validationCallback: typeof validationCallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const documentValidationPlugin = (
|
|
53
|
+
options: DocumentValidationPluginArgs,
|
|
54
|
+
) =>
|
|
55
|
+
new ProsePlugin<DocumentValidationPluginState>({
|
|
56
|
+
key: documentValidationPluginKey,
|
|
57
|
+
state: {
|
|
58
|
+
init() {
|
|
59
|
+
return {
|
|
60
|
+
validationCallback: validationCallback,
|
|
61
|
+
documentShape: options.documentShape,
|
|
62
|
+
propertiesWithoutErrors: [],
|
|
63
|
+
propertiesWithErrors: [],
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
apply(tr, state) {
|
|
67
|
+
const pluginTransaction = tr.getMeta(documentValidationPluginKey) as
|
|
68
|
+
| DocumentValidationTransactionMeta
|
|
69
|
+
| undefined;
|
|
70
|
+
if (pluginTransaction && pluginTransaction.type === 'setNewReport') {
|
|
71
|
+
return {
|
|
72
|
+
...state,
|
|
73
|
+
report: pluginTransaction.report,
|
|
74
|
+
propertiesWithoutErrors: pluginTransaction.propertiesWithoutErrors,
|
|
75
|
+
propertiesWithErrors: pluginTransaction.propertiesWithErrors,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return state;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
async function validationCallback(view: EditorView, documentHtml: string) {
|
|
85
|
+
const documentValidationState = documentValidationPluginKey.getState(
|
|
86
|
+
view.state,
|
|
87
|
+
);
|
|
88
|
+
if (!documentValidationState) {
|
|
89
|
+
console.warn('No shape found in the document validation plugin');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const { documentShape } = documentValidationState;
|
|
93
|
+
const rdf = await htmlToRdf(documentHtml);
|
|
94
|
+
|
|
95
|
+
const shacl = await parse(documentShape);
|
|
96
|
+
|
|
97
|
+
const validator = new SHACLValidator(shacl, {
|
|
98
|
+
// @ts-expect-error ts doesn't recognize the configuration parameter not sure why
|
|
99
|
+
allowNamedNodeInList: true,
|
|
100
|
+
});
|
|
101
|
+
const report = validator.validate(rdf);
|
|
102
|
+
const sayFactory = new SayDataFactory();
|
|
103
|
+
const propertyPred = sayFactory.namedNode(
|
|
104
|
+
'http://www.w3.org/ns/shacl#property',
|
|
105
|
+
);
|
|
106
|
+
const propertyNodes = [
|
|
107
|
+
...shacl.match(undefined, propertyPred, undefined),
|
|
108
|
+
].map((quad: Quad) => quad.object);
|
|
109
|
+
|
|
110
|
+
const propertiesWithErrors: {
|
|
111
|
+
sourceShape: BlankNode | NamedNode<string>;
|
|
112
|
+
focusNode: BlankNode | NamedNode<string> | null;
|
|
113
|
+
}[] = [];
|
|
114
|
+
for (const r of report.results) {
|
|
115
|
+
const sourceShape = r.sourceShape;
|
|
116
|
+
if (sourceShape)
|
|
117
|
+
propertiesWithErrors.push({ sourceShape, focusNode: r.focusNode });
|
|
118
|
+
}
|
|
119
|
+
const errorMessagePred = sayFactory.namedNode(
|
|
120
|
+
'http://www.w3.org/ns/shacl#resultMessage',
|
|
121
|
+
);
|
|
122
|
+
const propertiesWithErrorsMessages = propertiesWithErrors
|
|
123
|
+
.map(({ sourceShape, focusNode }) => {
|
|
124
|
+
const match = shacl.match(sourceShape, errorMessagePred, undefined);
|
|
125
|
+
const message = [...match][0]?.object.value;
|
|
126
|
+
return message
|
|
127
|
+
? { message: removeQuotes(message), subject: focusNode?.value }
|
|
128
|
+
: undefined;
|
|
129
|
+
})
|
|
130
|
+
.filter((message) => message);
|
|
131
|
+
const propertiesWithoutErrorsArray = propertyNodes.filter((propertyNode) =>
|
|
132
|
+
propertiesWithErrors.some((propertyWithError) => {
|
|
133
|
+
return propertyWithError.sourceShape.value !== propertyNode.value;
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const successMessagePred = sayFactory.namedNode(
|
|
138
|
+
'http://mu.semte.ch/vocabularies/ext/successMessage',
|
|
139
|
+
);
|
|
140
|
+
const propertiesWithoutErrors = propertiesWithoutErrorsArray
|
|
141
|
+
.map((propertyNode) => {
|
|
142
|
+
const match = shacl.match(propertyNode, successMessagePred, undefined);
|
|
143
|
+
const message = [...match][0]?.object.value;
|
|
144
|
+
return message ? { message: removeQuotes(message) } : undefined;
|
|
145
|
+
})
|
|
146
|
+
.filter((message) => message);
|
|
147
|
+
const transaction = view.state.tr;
|
|
148
|
+
transaction.setMeta(documentValidationPluginKey, {
|
|
149
|
+
type: 'setNewReport',
|
|
150
|
+
report,
|
|
151
|
+
propertiesWithoutErrors,
|
|
152
|
+
propertiesWithErrors: propertiesWithErrorsMessages,
|
|
153
|
+
});
|
|
154
|
+
transaction.setMeta('addToHistory', false);
|
|
155
|
+
view.dispatch(transaction);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface N3Parser {
|
|
159
|
+
parse: (
|
|
160
|
+
triples: string,
|
|
161
|
+
callback: (error: string, quad: Quad) => void,
|
|
162
|
+
) => void;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function parse(triples: string): Promise<DatasetCore<Quad>> {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
// @ts-expect-error we have to use a custom type to make the quads compatible or else ts will complain
|
|
168
|
+
const parser: N3Parser = new ParserN3();
|
|
169
|
+
const dataset = factory.dataset();
|
|
170
|
+
parser.parse(triples, (error, quad) => {
|
|
171
|
+
if (error) {
|
|
172
|
+
console.warn(error);
|
|
173
|
+
reject(error);
|
|
174
|
+
} else if (quad) {
|
|
175
|
+
dataset.add(quad);
|
|
176
|
+
} else {
|
|
177
|
+
resolve(dataset);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function htmlToRdf(html: string): Promise<DatasetCore<Quad>> {
|
|
184
|
+
return new Promise((res, rej) => {
|
|
185
|
+
const myParser = new RdfaParser({ contentType: 'text/html' });
|
|
186
|
+
const dataset = factory.dataset();
|
|
187
|
+
myParser
|
|
188
|
+
.on('data', (data) => {
|
|
189
|
+
dataset.add(data);
|
|
190
|
+
})
|
|
191
|
+
.on('error', rej)
|
|
192
|
+
.on('end', () => res(dataset));
|
|
193
|
+
myParser.write(html);
|
|
194
|
+
myParser.end();
|
|
195
|
+
});
|
|
196
|
+
}
|