@lblod/ember-rdfa-editor-lblod-plugins 5.0.0 → 6.0.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +22 -2
  2. package/README.md +126 -70
  3. package/addon/components/address-plugin/insert.hbs +47 -0
  4. package/addon/components/address-plugin/insert.ts +100 -0
  5. package/addon/components/address-plugin/types.ts +26 -0
  6. package/addon/components/address-plugin/utils.ts +59 -0
  7. package/addon/components/besluit-type-plugin/toolbar-dropdown.ts +6 -5
  8. package/addon/components/{besluit-plugin/besluit-plugin-card.ts → decision-plugin/decision-plugin-card.ts} +22 -8
  9. package/addon/components/hover-tooltip.ts +15 -12
  10. package/addon/components/roadsign-regulation-plugin/measure-template.ts +5 -1
  11. package/addon/components/roadsign-regulation-plugin/roadsign-regulation-card.hbs +1 -1
  12. package/addon/components/roadsign-regulation-plugin/roadsigns-modal.hbs +1 -0
  13. package/addon/components/roadsign-regulation-plugin/roadsigns-modal.ts +24 -21
  14. package/addon/components/roadsign-regulation-plugin/roadsigns-table.ts +8 -11
  15. package/addon/components/table-of-contents-plugin/ember-nodes/table-of-contents.ts +16 -1
  16. package/addon/components/validation-plugin/validation-card.hbs +12 -0
  17. package/addon/components/validation-plugin/validation-card.ts +19 -0
  18. package/addon/components/validation-plugin/validation-item.hbs +2 -0
  19. package/addon/components/validation-plugin/validation-item.ts +55 -0
  20. package/addon/components/variable-plugin/template-variable-card.ts +8 -25
  21. package/addon/plugins/besluit-type-plugin/index.ts +3 -0
  22. package/addon/plugins/besluit-type-plugin/utils/fetchBesluitTypes.ts +1 -6
  23. package/addon/plugins/decision-plugin/commands/index.ts +4 -0
  24. package/addon/plugins/decision-plugin/commands/insert-article-container.ts +56 -0
  25. package/addon/plugins/decision-plugin/commands/insert-description.ts +53 -0
  26. package/addon/plugins/decision-plugin/commands/insert-motivation.ts +62 -0
  27. package/addon/plugins/decision-plugin/commands/insert-title.ts +54 -0
  28. package/addon/plugins/roadsign-regulation-plugin/index.ts +4 -0
  29. package/addon/plugins/standard-template-plugin/utils/nodes.ts +0 -1
  30. package/addon/plugins/table-of-contents-plugin/index.ts +2 -0
  31. package/addon/plugins/validation/README.md +84 -0
  32. package/addon/plugins/validation/index.ts +427 -0
  33. package/addon/plugins/validation/utils/transaction-complies-with-shapes.ts +28 -0
  34. package/addon/plugins/variable-plugin/index.ts +5 -0
  35. package/addon/services/roadsign-registry.ts +54 -50
  36. package/addon/utils/find-insertion-pos-in-ancestor-of-type.ts +35 -0
  37. package/app/components/{besluit-plugin/besluit-plugin-card.js → address-plugin/insert.js} +1 -1
  38. package/app/components/decision-plugin/decision-plugin-card.js +1 -0
  39. package/app/components/{besluit-plugin/besluit-context-card.js → validation-plugin/validation-card.js} +1 -1
  40. package/app/components/validation-plugin/validation-item.js +1 -0
  41. package/app/styles/address-plugin.scss +7 -0
  42. package/components/address-plugin/insert.d.ts +21 -0
  43. package/components/address-plugin/types.d.ts +25 -0
  44. package/components/address-plugin/utils.d.ts +9 -0
  45. package/components/besluit-type-plugin/toolbar-dropdown.d.ts +2 -0
  46. package/components/{besluit-plugin/besluit-plugin-card.d.ts → decision-plugin/decision-plugin-card.d.ts} +4 -1
  47. package/components/roadsign-regulation-plugin/measure-template.d.ts +2 -0
  48. package/components/roadsign-regulation-plugin/roadsigns-modal.d.ts +3 -0
  49. package/components/roadsign-regulation-plugin/roadsigns-table.d.ts +6 -1
  50. package/components/validation-plugin/validation-card.d.ts +13 -0
  51. package/components/validation-plugin/validation-item.d.ts +18 -0
  52. package/components/variable-plugin/template-variable-card.d.ts +2 -4
  53. package/package.json +1 -1
  54. package/plugins/besluit-type-plugin/index.d.ts +3 -0
  55. package/plugins/besluit-type-plugin/utils/fetchBesluitTypes.d.ts +1 -5
  56. package/plugins/decision-plugin/commands/index.d.ts +4 -0
  57. package/plugins/decision-plugin/commands/insert-article-container.d.ts +8 -0
  58. package/plugins/decision-plugin/commands/insert-description.d.ts +7 -0
  59. package/plugins/decision-plugin/commands/insert-motivation.d.ts +7 -0
  60. package/plugins/decision-plugin/commands/insert-title.d.ts +7 -0
  61. package/plugins/roadsign-regulation-plugin/index.d.ts +4 -0
  62. package/plugins/table-of-contents-plugin/index.d.ts +2 -0
  63. package/plugins/validation/index.d.ts +62 -0
  64. package/plugins/validation/utils/transaction-complies-with-shapes.d.ts +10 -0
  65. package/plugins/variable-plugin/index.d.ts +5 -0
  66. package/services/roadsign-registry.d.ts +7 -9
  67. package/translations/en-US.yaml +58 -47
  68. package/translations/nl-BE.yaml +18 -0
  69. package/utils/find-insertion-pos-in-ancestor-of-type.d.ts +11 -0
  70. package/addon/plugins/besluit-plugin/commands/index.ts +0 -1
  71. package/addon/plugins/besluit-plugin/commands/insert-title.ts +0 -43
  72. package/config/environment.js +0 -20
  73. package/plugins/besluit-plugin/commands/index.d.ts +0 -1
  74. package/plugins/besluit-plugin/commands/insert-title.d.ts +0 -3
  75. /package/addon/components/{besluit-plugin/besluit-plugin-card.hbs → decision-plugin/decision-plugin-card.hbs} +0 -0
  76. /package/addon/plugins/{besluit-plugin → decision-plugin}/utils/get-title-for-decision.ts +0 -0
  77. /package/plugins/{besluit-plugin → decision-plugin}/utils/get-title-for-decision.d.ts +0 -0
@@ -0,0 +1,62 @@
1
+ import {
2
+ Command,
3
+ EditorState,
4
+ NodeSelection,
5
+ Transaction,
6
+ } from '@lblod/ember-rdfa-editor';
7
+ import { v4 as uuid } from 'uuid';
8
+ import { isNone } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/option';
9
+ import { transactionCompliesWithShapes } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/validation/utils/transaction-complies-with-shapes';
10
+ import { findInsertionPosInAncestorOfType } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/find-insertion-pos-in-ancestor-of-type';
11
+
12
+ interface InsertMotivationArgs {
13
+ placeholderText?: string;
14
+ validateShapes?: Set<string>;
15
+ }
16
+
17
+ export default function insertMotivation({
18
+ validateShapes,
19
+ placeholderText = 'Insert motivation',
20
+ }: InsertMotivationArgs = {}): Command {
21
+ return function (state: EditorState, dispatch?: (tr: Transaction) => void) {
22
+ const { selection, schema } = state;
23
+ const nodeToInsert = schema.node('motivering', { __rdfaId: uuid() }, [
24
+ schema.node(
25
+ 'paragraph',
26
+ null,
27
+ schema.node('placeholder', {
28
+ placeholderText,
29
+ })
30
+ ),
31
+ ]);
32
+ // how the offset between the insertion point and the point where the cursor should end up
33
+ const cursorOffset = 2;
34
+
35
+ const insertionPos = findInsertionPosInAncestorOfType(
36
+ selection,
37
+ schema.nodes.besluit,
38
+ nodeToInsert
39
+ );
40
+ if (isNone(insertionPos)) {
41
+ return false;
42
+ }
43
+ const tr = state.tr;
44
+
45
+ tr.replaceRangeWith(insertionPos, insertionPos, nodeToInsert);
46
+ if (!transactionCompliesWithShapes(state, tr, validateShapes)) {
47
+ return false;
48
+ }
49
+ if (dispatch) {
50
+ console.log('inserts in ', insertionPos);
51
+ console.log('selection on ', insertionPos + cursorOffset);
52
+ const selectionPos = tr.doc.resolve(insertionPos + cursorOffset);
53
+ // const targetPos = tr.doc.resolve(insertionPos + cursorOffset + 1);
54
+ // TODO figure out why I cant just set a nodeSelection here
55
+ tr.setSelection(
56
+ new NodeSelection(tr.doc.resolve(selectionPos.posAtIndex(0)))
57
+ );
58
+ dispatch(tr);
59
+ }
60
+ return true;
61
+ };
62
+ }
@@ -0,0 +1,54 @@
1
+ import { v4 as uuid } from 'uuid';
2
+ import { isNone } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/option';
3
+ import {
4
+ EditorState,
5
+ NodeSelection,
6
+ Transaction,
7
+ } from '@lblod/ember-rdfa-editor';
8
+ import { transactionCompliesWithShapes } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/validation/utils/transaction-complies-with-shapes';
9
+ import { findInsertionPosInAncestorOfType } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/find-insertion-pos-in-ancestor-of-type';
10
+
11
+ interface InsertTitleArgs {
12
+ placeholderText: string;
13
+ validateShapes?: Set<string>;
14
+ }
15
+
16
+ export default function insertTitle({
17
+ placeholderText,
18
+ validateShapes,
19
+ }: InsertTitleArgs) {
20
+ return function (state: EditorState, dispatch?: (tr: Transaction) => void) {
21
+ const { selection, schema } = state;
22
+ const nodeToInsert = schema.node(
23
+ 'besluit_title',
24
+ { __rdfaId: uuid() },
25
+ schema.node(
26
+ 'paragraph',
27
+ null,
28
+ schema.node('placeholder', {
29
+ placeholderText,
30
+ })
31
+ )
32
+ );
33
+
34
+ const insertionPos = findInsertionPosInAncestorOfType(
35
+ selection,
36
+ schema.nodes.besluit,
37
+ nodeToInsert
38
+ );
39
+ if (isNone(insertionPos)) {
40
+ return false;
41
+ }
42
+ const tr = state.tr;
43
+ tr.replaceRangeWith(insertionPos, insertionPos, nodeToInsert);
44
+
45
+ if (!transactionCompliesWithShapes(state, tr, validateShapes)) {
46
+ return false;
47
+ }
48
+ if (dispatch) {
49
+ tr.setSelection(NodeSelection.create(tr.doc, insertionPos + 2));
50
+ dispatch(tr.scrollIntoView());
51
+ }
52
+ return true;
53
+ };
54
+ }
@@ -0,0 +1,4 @@
1
+ export type RoadsignRegulationPluginOptions = {
2
+ endpoint: string;
3
+ imageBaseUrl: string;
4
+ };
@@ -77,7 +77,6 @@ export const description: NodeSpec = {
77
77
  };
78
78
 
79
79
  export const motivering: NodeSpec = {
80
- group: 'block',
81
80
  content: 'block+',
82
81
  inline: false,
83
82
  canSplit: false,
@@ -1,3 +1,5 @@
1
1
  export type TableOfContentsConfig = {
2
2
  nodeHierarchy: string[];
3
+ scrollContainer?: HTMLElement;
4
+ scrollingPadding?: number;
3
5
  }[];
@@ -0,0 +1,84 @@
1
+ # Validation Plugin
2
+
3
+ A plugin to validate the document state, with an interface inspired by the SHACL specification.
4
+
5
+ ## usage
6
+
7
+ This plugin consists of a ProseMirror plugin as well as a `ValidationPlugin::ValidationCard` component
8
+ which provides an easy way to render the validation errors. It is a keyed plugin, meaning only one instance can be
9
+ active at any time.
10
+
11
+ ```ts
12
+
13
+ import { validation } from "@lblod/ember-rdfa-editor-lblod-plugins/plugins/validation";
14
+
15
+ validationPlugin = validation((schema: Schema) => ({
16
+ shapes: [
17
+ {
18
+ name: "exactly-one-title",
19
+ focusNodeType: schema.nodes.besluit,
20
+ path: ["title"],
21
+ message: "Document must contain exactly one title block.",
22
+ constraints: {
23
+ minCount: 1,
24
+ maxCount: 1
25
+ }
26
+ },
27
+ {
28
+ name: "exactly-one-description",
29
+ focusNodeType: schema.nodes.besluit,
30
+ path: ["description"],
31
+ message: "Document must contain exactly one description block.",
32
+ constraints: {
33
+ minCount: 1,
34
+ maxCount: 1
35
+ }
36
+ },
37
+ {
38
+ name: "max-one-motivation",
39
+ focusNodeType: schema.nodes.besluit,
40
+ path: ["motivering"],
41
+ message: "Document may not contain more than one motivation block.",
42
+ constraints: {
43
+ maxCount: 1
44
+ }
45
+ }
46
+ ]
47
+ }));
48
+ ```
49
+
50
+ ```handlebars
51
+
52
+ <ValidationPlugin::ValidationCard
53
+ @title={{t "dummy.validation-card.title"}}
54
+ @controller={{this.controller}}
55
+ @report={{this.report}}/>
56
+ ```
57
+ In which the report can be extracted from the plugin state of the validation plugin.
58
+
59
+ ## Validation algorithm
60
+
61
+ This high-level overview of the algorithm explains how the focusNode and path configurations define which
62
+ "shapes" are validated:
63
+
64
+ - traverse the entire document depth-first
65
+ - on every node, keep track of the validation shapes that have a focusNodeType set to that node's type
66
+ - for any shapes that have been triggered already, check if this node's type is correct for the corresponding
67
+ position in the path
68
+ - every time we find a complete and exact match for a focusNode->path chain of nodes, we count it
69
+ - once we've seen the entire subtree of a particular focusNode, we validate the constraints on all "completed" shapes
70
+
71
+ ## Currently supported constraints
72
+
73
+ ### maxCount
74
+
75
+ - type: `number`
76
+ - meaning: the specified shape may not occur more times than the specified amount in the focusNode
77
+
78
+
79
+
80
+ ### `minCount: number`
81
+
82
+ - type: `number`
83
+ - meaning: the specified shape must occur at least as many times as the specified amount in the focusNode
84
+
@@ -0,0 +1,427 @@
1
+ import {
2
+ EditorState,
3
+ NodeType,
4
+ PluginKey,
5
+ PNode,
6
+ ProsePlugin,
7
+ Schema,
8
+ Transaction,
9
+ } from '@lblod/ember-rdfa-editor';
10
+ import {
11
+ expect,
12
+ Option,
13
+ unwrap,
14
+ } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/option';
15
+
16
+ export function validateTransaction(
17
+ state: EditorState,
18
+ tr: Transaction
19
+ ): ValidationReport {
20
+ const validationState = VALIDATION_KEY.getState(state);
21
+ if (!validationState) {
22
+ return { conforms: true };
23
+ } else {
24
+ if (!tr.docChanged) {
25
+ return validationState.report;
26
+ }
27
+ const newState = state.apply(tr);
28
+ const newValidationState = VALIDATION_KEY.getState(newState);
29
+ if (!newValidationState) {
30
+ return { conforms: true };
31
+ }
32
+ const cachedValidation = tr.getMeta(
33
+ 'validated'
34
+ ) as Option<ValidationReport>;
35
+ if (cachedValidation) {
36
+ return cachedValidation;
37
+ }
38
+ tr.setMeta('validated', newValidationState);
39
+ tr.setMeta('firstPass', true);
40
+
41
+ return newValidationState.report;
42
+ }
43
+ }
44
+
45
+ export const VALIDATION_KEY: PluginKey<ValidationState> =
46
+ new PluginKey<ValidationState>('validation');
47
+
48
+ export interface ValidationSpec {
49
+ [key: string]: ValidationShape[];
50
+ }
51
+
52
+ export type ValidationState = {
53
+ report: ValidationReport;
54
+ spec: ValidationSpec;
55
+ };
56
+
57
+ export type PropertyPath = string[];
58
+
59
+ export type Severity = 'info' | 'warning' | 'violation';
60
+
61
+ export interface ValidationReport {
62
+ conforms: boolean;
63
+ results?: ValidationResult[];
64
+ }
65
+
66
+ export type ValidationPlugin = ProsePlugin<ValidationState>;
67
+
68
+ export interface MinCountConstraint {
69
+ kind: 'minCount';
70
+ value: number;
71
+ }
72
+
73
+ export interface MaxCountConstraint {
74
+ kind: 'maxCount';
75
+ value: number;
76
+ }
77
+
78
+ export type ValidationConstraint = MinCountConstraint | MaxCountConstraint;
79
+
80
+ export type ConstraintMap = {
81
+ [C in ValidationConstraint as C['kind']]?: C['value'];
82
+ };
83
+
84
+ export type ConstraintValidator = (
85
+ constraint: ValidationConstraint,
86
+ shapeContext: ShapeContext
87
+ ) => Option<ValidationResult>;
88
+ export type ConstraintValidatorMap = {
89
+ [C in ValidationConstraint as C['kind']]: ConstraintValidator;
90
+ };
91
+
92
+ export interface ValidationShape {
93
+ name: string;
94
+ focusNodeType: NodeType;
95
+ path: string[] | number[];
96
+
97
+ message?: string;
98
+ constraints?: ConstraintMap;
99
+
100
+ severity?: Severity;
101
+ }
102
+
103
+ export interface ValidationResult {
104
+ focusNode: PNode;
105
+
106
+ resultPath: PNode[];
107
+
108
+ value?: unknown;
109
+
110
+ sourceShape: ValidationShape;
111
+
112
+ sourceConstraint: ValidationConstraint;
113
+
114
+ message: string;
115
+ severity: Severity;
116
+ }
117
+
118
+ export interface ValidationPluginConfig {
119
+ shapes: ValidationShape[];
120
+ }
121
+
122
+ const CONSTRAINT_VALIDATOR_MAP: ConstraintValidatorMap = {
123
+ maxCount: validateMaxCount,
124
+ minCount: validateMinCount,
125
+ };
126
+
127
+ export function validation(
128
+ configurator: (schema: Schema) => ValidationPluginConfig
129
+ ): ValidationPlugin {
130
+ const validation = new ProsePlugin<ValidationState>({
131
+ key: VALIDATION_KEY,
132
+ state: {
133
+ init(_stateConfig, state) {
134
+ const spec = compileSpec(configurator, state.schema);
135
+ return {
136
+ spec,
137
+ report: { conforms: true },
138
+ };
139
+ },
140
+ apply(tr, oldPluginState, oldState, newState): ValidationState {
141
+ const schemaChanged = oldState.schema !== newState.schema;
142
+ if (schemaChanged) {
143
+ const spec = compileSpec(configurator, newState.schema);
144
+ const newValidation = doValidation(
145
+ tr,
146
+ oldPluginState,
147
+ oldState,
148
+ newState,
149
+ spec
150
+ );
151
+ tr.setMeta('validated', newValidation);
152
+ tr.setMeta('firstPass', false);
153
+ return { spec, report: newValidation };
154
+ } else {
155
+ const spec = oldPluginState.spec;
156
+
157
+ if (!tr.docChanged) {
158
+ return oldPluginState;
159
+ }
160
+ const cachedValidation = tr.getMeta(
161
+ 'validated'
162
+ ) as Option<ValidationState>;
163
+ const firstPass = tr.getMeta('firstPass') as Option<boolean>;
164
+
165
+ if (firstPass && cachedValidation) {
166
+ tr.setMeta('firstPass', false);
167
+ return cachedValidation;
168
+ }
169
+ const newValidation = doValidation(
170
+ tr,
171
+ oldPluginState,
172
+ oldState,
173
+ newState,
174
+ spec
175
+ );
176
+ tr.setMeta('validated', newValidation);
177
+ tr.setMeta('firstPass', false);
178
+ return { spec, report: newValidation };
179
+ }
180
+ },
181
+ },
182
+ });
183
+ return validation;
184
+ }
185
+
186
+ /**
187
+ * Calculate the validation shapes and organize them by the name of the type
188
+ * of their focusNode
189
+ * @param configurator
190
+ * @param schema
191
+ */
192
+ function compileSpec(
193
+ configurator: (schema: Schema) => ValidationPluginConfig,
194
+ schema: Schema
195
+ ): ValidationSpec {
196
+ const spec: ValidationSpec = {};
197
+ const shapes = configurator(schema).shapes;
198
+ for (const shape of shapes) {
199
+ const typeName = shape.focusNodeType.name;
200
+ if (spec[typeName]) {
201
+ spec[typeName].push(shape);
202
+ } else {
203
+ spec[typeName] = [shape];
204
+ }
205
+ }
206
+ return spec;
207
+ }
208
+
209
+ interface ShapeContext {
210
+ shape: ValidationShape;
211
+ focusNode: PNode;
212
+ count: number;
213
+
214
+ path: PNode[];
215
+ }
216
+
217
+ interface ValidationContext {
218
+ activeShapes: { depth: number; shape: ValidationShape }[][];
219
+ path: PNode[];
220
+ spec: ValidationSpec;
221
+ shapeContext: Map<ValidationShape, ShapeContext>;
222
+ }
223
+
224
+ /**
225
+ * Main validation driver. Sets up the recursive validation function.
226
+ * @param tr
227
+ * @param oldPluginState
228
+ * @param oldState
229
+ * @param newState
230
+ * @param spec
231
+ */
232
+ function doValidation(
233
+ tr: Transaction,
234
+ oldPluginState: ValidationState,
235
+ oldState: EditorState,
236
+ newState: EditorState,
237
+ spec: ValidationSpec
238
+ ): ValidationReport {
239
+ const results: ValidationResult[] = [];
240
+ const context: ValidationContext = {
241
+ activeShapes: [],
242
+ path: [],
243
+ spec,
244
+ shapeContext: new Map(),
245
+ };
246
+ recValidate(context, results, newState.doc, 0);
247
+ let conforms = true;
248
+ for (const result of results) {
249
+ if (result.severity === 'violation') {
250
+ conforms = false;
251
+ break;
252
+ }
253
+ }
254
+ return { conforms, results };
255
+ }
256
+
257
+ /**
258
+ * The actual validation function. Traverses the document with DFS.
259
+ * Collects the results in the array given in the results argument.
260
+ * Uses the context argument to keep track of various needed state.
261
+ * @param context
262
+ * @param results
263
+ * @param node
264
+ * @param currentDepth
265
+ */
266
+ function recValidate(
267
+ context: ValidationContext,
268
+ results: ValidationResult[],
269
+ node: PNode,
270
+ currentDepth: number
271
+ ) {
272
+ // keep track of the global path we're on
273
+ context.path.push(node);
274
+ // get all shapes that focus on the current node
275
+ // and start a new context for them
276
+ const nodeShapes =
277
+ context.spec[node.type.name]?.map((shape) => ({
278
+ depth: currentDepth,
279
+ shape,
280
+ })) ?? [];
281
+ for (const { shape } of nodeShapes) {
282
+ context.shapeContext.set(shape, {
283
+ path: context.path,
284
+ focusNode: node,
285
+ shape,
286
+ count: 0,
287
+ });
288
+ }
289
+ // get the shapes that are active in this depth
290
+ const activeShapes = context.activeShapes.length
291
+ ? context.activeShapes[context.activeShapes.length - 1]
292
+ : [];
293
+ const shapesToCheck = [...activeShapes, ...nodeShapes];
294
+ const nodeIsTargetOfShapes = [];
295
+ const pathIsValidForShapes = [];
296
+
297
+ for (const depthShape of shapesToCheck) {
298
+ const {
299
+ depth,
300
+ shape: { path },
301
+ } = depthShape;
302
+ // check if we're at the right depth for the shape's path to matter
303
+ // if we are deeper than the length of the path + the depth
304
+ // of the shapes focusNode, we can ignore the shape
305
+ if (path.length + depth === currentDepth) {
306
+ if (path.length === 0 || path[path.length - 1] === node.type.name) {
307
+ // we're at the end of the shapeSpec's path, and our node
308
+ // has the right type. This means we have fully completed the shape,
309
+ // and we need to validate it
310
+ nodeIsTargetOfShapes.push(depthShape);
311
+ }
312
+ } else if (path.length + depth > currentDepth) {
313
+ // we're somewhere along the spec's path.
314
+ // So either our node has the right type,
315
+ // meaning we're still "on the path", or it doesn't,
316
+ // and we can abandon this particular shape
317
+ // reminder that currentDepth is our "global" depth in the doc tree,
318
+ // and depth is the depth of the shape's focusNode
319
+ // so currentDepth - depth gives us the depth relative to the focusNode,
320
+ // which is how far along its path we are. Subtract 1 for usual array
321
+ // index shenanigans.
322
+ if (currentDepth - depth - 1 > 0) {
323
+ if (path[currentDepth - 1 - depth] === node.type.name) {
324
+ pathIsValidForShapes.push(depthShape);
325
+ }
326
+ } else {
327
+ // special case where we're at the same depth as the focusNode
328
+ // meaning we just activated the shape, so it's definitely still valid
329
+ // the fact that I need to write this comment probably means this
330
+ // could be refactored to be a bit cleaner, but here we are
331
+ pathIsValidForShapes.push(depthShape);
332
+ }
333
+ }
334
+ }
335
+ // count all shapes we just completed with our node
336
+ for (const { shape } of nodeIsTargetOfShapes) {
337
+ const shapeContext = unwrap(context.shapeContext.get(shape));
338
+ context.shapeContext.set(shape, {
339
+ ...shapeContext,
340
+ count: shapeContext.count + 1,
341
+ });
342
+ }
343
+
344
+ // store all shapes where we're still on the path
345
+ context.activeShapes.push(pathIsValidForShapes);
346
+
347
+ // it's recursion time
348
+ for (let i = 0; i < node.content.childCount; i++) {
349
+ const child = node.content.child(i);
350
+ recValidate(context, results, child, currentDepth + 1);
351
+ }
352
+
353
+ // after the recursion, we've seen the entire subtree under our node
354
+ // so we're ready to validate any shapes that focus this node
355
+ for (const { shape } of nodeShapes) {
356
+ // a "this should never be null" nullcheck
357
+ const shapeContext = expect(
358
+ 'Shapecontext not initialized',
359
+ context.shapeContext.get(shape)
360
+ );
361
+ // imagine if after all that it turns out the shape has no constraints...
362
+ if (shape.constraints) {
363
+ // with the shape's context nicely filled, we can now validate
364
+ for (const constraint of Object.entries(shape.constraints).map(
365
+ ([kind, value]) => ({ kind, value } as ValidationConstraint)
366
+ )) {
367
+ const result = validateConstraint(constraint, shapeContext);
368
+ if (result) {
369
+ results.push(result);
370
+ }
371
+ }
372
+ }
373
+ }
374
+ // clean up our context stack before we go back up
375
+ context.path.pop();
376
+ context.activeShapes.pop();
377
+ for (const depthShape of nodeShapes) {
378
+ context.shapeContext.delete(depthShape.shape);
379
+ }
380
+ }
381
+
382
+ function validateConstraint(
383
+ constraint: ValidationConstraint,
384
+ shapeContext: ShapeContext
385
+ ): Option<ValidationResult> {
386
+ return CONSTRAINT_VALIDATOR_MAP[constraint.kind](constraint, shapeContext);
387
+ }
388
+
389
+ function validateMinCount(
390
+ constraint: MinCountConstraint,
391
+ shapeContext: ShapeContext
392
+ ): Option<ValidationResult> {
393
+ const { shape, focusNode, count, path } = shapeContext;
394
+ if (count < constraint.value) {
395
+ return {
396
+ focusNode,
397
+ sourceConstraint: constraint,
398
+ sourceShape: shape,
399
+ message:
400
+ shape.message ??
401
+ `minCount violation for node with type ${focusNode.type.name}`,
402
+ resultPath: path,
403
+ severity: shape.severity ?? 'violation',
404
+ };
405
+ }
406
+
407
+ return null;
408
+ }
409
+
410
+ function validateMaxCount(
411
+ constraint: MaxCountConstraint,
412
+ shapeContext: ShapeContext
413
+ ): Option<ValidationResult> {
414
+ const { shape, focusNode, count, path } = shapeContext;
415
+ if (count > constraint.value) {
416
+ return {
417
+ focusNode,
418
+ sourceConstraint: constraint,
419
+ sourceShape: shape,
420
+ message: `maxCount violation for node with type ${focusNode.type.name}`,
421
+ resultPath: path,
422
+ severity: shape.severity ?? 'violation',
423
+ };
424
+ }
425
+
426
+ return null;
427
+ }
@@ -0,0 +1,28 @@
1
+ import { EditorState, Transaction } from '@lblod/ember-rdfa-editor';
2
+ import { validateTransaction } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/validation';
3
+
4
+ /**
5
+ * Validate a given transaction against a given set of shapes. Returns true when the
6
+ * state after applying the transaction is valid according to all given shapes,
7
+ * false otherwise.
8
+ * @param state the state the transaction was created from
9
+ * @param tr
10
+ * @param validateShapes
11
+ */
12
+ export function transactionCompliesWithShapes(
13
+ state: EditorState,
14
+ tr: Transaction,
15
+ validateShapes?: Set<string>
16
+ ) {
17
+ if (validateShapes?.size) {
18
+ const report = validateTransaction(state, tr);
19
+ if (
20
+ report.results?.some((result) =>
21
+ validateShapes.has(result.sourceShape.name)
22
+ )
23
+ ) {
24
+ return false;
25
+ }
26
+ }
27
+ return true;
28
+ }
@@ -0,0 +1,5 @@
1
+ export type TemplateVariablePluginOptions = {
2
+ endpoint: string;
3
+ zonalLocationCodelistUri: string;
4
+ nonZonalLocationCodelistUri: string;
5
+ };