yaml-dsl-spec 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -9,20 +9,4 @@ if (read.problems) for (const problem of read.problems) console.error(String(pro
9
9
  else for (const { definition } of read.dsls) console.log(definition.id);
10
10
  ```
11
11
 
12
- | Export | Is |
13
- |---|---|
14
- | `readDslDefinitions(file, text)` | `{ dsls }`, each DSL of a `yaml-dsl.yml`, `{ definition, block }`, validated against its version's schema and the checks the schema cannot state; or `{ problems }`, every problem at the field it is about |
15
- | `Diagnostic`, `Position` | a problem at its place: `message`, `position` and `notes` (`label`, `rule`, `help`) |
16
- | `yamlReader.parseYaml(file, text, tagKeys)` | `{ node }`, YAML read with each value's place, or `{ problem }` |
17
- | `Node`, `numberJson` | a YAML value as written, and a number's text as JSON |
18
- | `dslDefinition.dslDefinitionFromJson(block)` | a DSL block with its defaults, its fields named as the standard names them |
19
- | `jsonPath` | `PathPattern` and `PathSet`, the standard's JSONPath subset |
20
- | `resolved` | `Value`, a resolved document's value with its place |
21
- | `functionTable` | `FunctionTable`, a schema's `x-yaml-dsl-functions` |
22
- | `schemaValidation` | `SchemaCache` and `LoadedSchema`, a document validated against its schema, each problem at its place |
23
- | `parseCallKey`, `closestName` | a call written in a key, and the allowed name closest to a misspelt one |
24
- | `yamlDslSchema(version)`, `yamlDslSchemas` | the JSON Schema of `yaml-dsl.yml`, at a version, or by version |
25
- | `functionTableSchema(version)`, `functionTableSchemas` | the JSON Schema of a DSL's function table, `x-yaml-dsl-functions`, at a version, or by version |
26
- | `YAML_DSL_VERSIONS`, `FUNCTION_TABLE_VERSIONS` | the versions the package reads |
27
-
28
- A version is a minor, such as `1.0`, or a major, such as `1`, which is its newest minor in the package.
12
+ Its API is the core's, designed in the [package design](https://github.com/litebitus/yaml-dsl-spec/blob/main/docs/package-design.md).
package/index.js CHANGED
@@ -49,6 +49,7 @@ module.exports = {
49
49
  schemaValidation: {
50
50
  SchemaCache: schemaValidation.SchemaCache,
51
51
  LoadedSchema: schemaValidation.LoadedSchema,
52
+ ValidationRules: schemaValidation.ValidationRules,
52
53
  childValue: schemaValidation.childValue,
53
54
  },
54
55
  };
@@ -13,6 +13,7 @@ const { compilePattern, groupNamesOf } = require('./pattern-dialect');
13
13
  const { Position } = require('./source-position');
14
14
  const { Value } = require('./resolved-value');
15
15
  const { SchemaCache, closestName, nodeJsonText } = require('./schema-validation');
16
+ const { ValidationRules } = require('./validation-rules');
16
17
  const { parseYaml } = require('./yaml-reader');
17
18
  const { YAML_DSL_VERSIONS, yamlDslSchemaFile } = require('./schema-catalog');
18
19
 
@@ -37,7 +38,7 @@ function readDslDefinitions(file, text) {
37
38
  } catch (error) {
38
39
  return { problems: [Diagnostic.unplaced(error.message)] };
39
40
  }
40
- const schemaProblems = schema.validate(Value.fromNode(root), []);
41
+ const schemaProblems = schema.validate(Value.fromNode(root), new ValidationRules());
41
42
  if (schemaProblems.length > 0) return { problems: schemaProblems };
42
43
  const blocks = (root.get('dsls') && root.get('dsls').items()) || [];
43
44
  const problems = [];
@@ -125,7 +125,9 @@ const DSL_FIELDS = {
125
125
  })), emptyList),
126
126
  tag_keys: withDefault(listOf(struct({ tag: required(text), key: required(text) })), emptyList),
127
127
  validator: optional(struct({
128
+ merge_base_keys: withDefault(listOf(text), emptyList),
128
129
  child_values_from_parent_key: withDefault(listOf(struct({ pattern: required(text) })), emptyList),
130
+ allow_quoted_values: withDefault(listOf(oneOf(['boolean', 'integer', 'number'])), emptyList),
129
131
  })),
130
132
  };
131
133
 
@@ -64,7 +64,7 @@ function compareText(first, second) {
64
64
  }
65
65
 
66
66
  function functionEntry(name, entry) {
67
- const listed = isObject(entry) ? entry.arguments : undefined;
67
+ const listed = Array.isArray(entry) ? entry : isObject(entry) ? entry.arguments : undefined;
68
68
  if (!Array.isArray(listed)) throw new Error(`function \`${name}\` has no arguments list`);
69
69
  const functionArguments = listed.map((argument) => {
70
70
  const argumentName = isObject(argument) ? argument.name : undefined;
@@ -75,7 +75,9 @@ function functionEntry(name, entry) {
75
75
  default: hasOwn(argument, 'default') ? argument.default : null,
76
76
  };
77
77
  });
78
- const macroEntry = hasOwn(entry, 'macro') ? macroEntryOf(name, entry.macro) : null;
78
+ const macroEntry = isObject(entry) && hasOwn(entry, 'macro')
79
+ ? macroEntryOf(name, entry.macro)
80
+ : null;
79
81
  return { arguments: functionArguments, macroEntry };
80
82
  }
81
83
 
@@ -101,6 +103,9 @@ function macroEntryOf(name, value) {
101
103
  for (const role of ROLE_NAMES) {
102
104
  if (typeof value[role] === 'string') roles[role] = value[role];
103
105
  }
106
+ if (!hasOwn(roles, 'loop_variables') && typeof value.loop_variable === 'string') {
107
+ roles.loop_variables = value.loop_variable;
108
+ }
104
109
  return new MacroEntry(kind, written || 'shallow', nameSuffix, roles);
105
110
  }
106
111
 
package/lib/json-path.js CHANGED
@@ -11,6 +11,23 @@ function indexStep(index) {
11
11
  return { index };
12
12
  }
13
13
 
14
+ /** A `['name']` step's name and the text after the step, or `null`. */
15
+ function quotedName(text) {
16
+ if (!text.startsWith("['")) return null;
17
+ const close = text.indexOf("'", 2);
18
+ if (close <= 2 || text[close + 1] !== ']') return null;
19
+ return { name: text.slice(2, close), remaining: text.slice(close + 2) };
20
+ }
21
+
22
+ /** A dotted step's name, which runs to the next `.` or `[`, and the text after it, or `null`. */
23
+ function dottedName(text) {
24
+ if (text.startsWith('*')) return null;
25
+ const stepEnd = text.search(/[.[]/);
26
+ const end = stepEnd === -1 ? text.length : stepEnd;
27
+ if (end === 0) return null;
28
+ return { name: text.slice(0, end), remaining: text.slice(end) };
29
+ }
30
+
14
31
  /** A parsed path and the keys its wildcard steps do not select or descend into. */
15
32
  class PathPattern {
16
33
  constructor(selectors, skipKeys) {
@@ -23,38 +40,37 @@ class PathPattern {
23
40
  const invalid = () =>
24
41
  Diagnostic.unplaced(`\`${text}\` is not a path of the supported JSONPath subset`);
25
42
  if (!text.startsWith('$')) throw invalid();
26
- const rest = text.slice(1);
43
+ let rest = text.slice(1);
27
44
  const selectors = [];
28
- let index = 0;
29
- while (index < rest.length) {
30
- const descendant = rest.startsWith('..', index);
31
- if (descendant) index += 2;
32
- else if (rest[index] === '.') index += 1;
33
- let name;
34
- let consumed;
35
- if (rest.startsWith("['", index)) {
36
- const close = rest.indexOf("']", index + 2);
37
- if (close === -1) throw invalid();
38
- name = rest.slice(index + 2, close);
39
- consumed = close - index + 2;
40
- } else if (rest.startsWith('[*]', index)) {
41
- name = null;
42
- consumed = 3;
43
- } else if (rest.startsWith('*', index)) {
44
- name = null;
45
- consumed = 1;
45
+ while (rest.length > 0) {
46
+ let step;
47
+ if (rest.startsWith('..')) {
48
+ const after = rest.slice(2);
49
+ if (after === '*') {
50
+ step = { selector: { kind: 'descendant_all' }, remaining: '' };
51
+ } else {
52
+ const named = quotedName(after) || dottedName(after);
53
+ if (!named) throw invalid();
54
+ step = {
55
+ selector: { kind: 'descendant_name', name: named.name },
56
+ remaining: named.remaining,
57
+ };
58
+ }
59
+ } else if (rest.startsWith('.*')) {
60
+ step = { selector: { kind: 'wildcard' }, remaining: rest.slice(2) };
61
+ } else if (rest.startsWith('.')) {
62
+ const named = dottedName(rest.slice(1));
63
+ if (!named) throw invalid();
64
+ step = { selector: { kind: 'name', name: named.name }, remaining: named.remaining };
65
+ } else if (rest.startsWith('[*]')) {
66
+ step = { selector: { kind: 'wildcard' }, remaining: rest.slice(3) };
46
67
  } else {
47
- const ends = [rest.indexOf('.', index), rest.indexOf('[', index)].filter((end) => end !== -1);
48
- const end = ends.length > 0 ? Math.min(...ends) : rest.length;
49
- if (end === index) throw invalid();
50
- name = rest.slice(index, end);
51
- consumed = end - index;
68
+ const named = quotedName(rest);
69
+ if (!named) throw invalid();
70
+ step = { selector: { kind: 'name', name: named.name }, remaining: named.remaining };
52
71
  }
53
- index += consumed;
54
- if (!descendant) selectors.push(name === null ? { kind: 'wildcard' } : { kind: 'name', name });
55
- else if (name !== null) selectors.push({ kind: 'descendant_name', name });
56
- else if (index === rest.length) selectors.push({ kind: 'descendant_all' });
57
- else throw invalid();
72
+ selectors.push(step.selector);
73
+ rest = step.remaining;
58
74
  }
59
75
  return new PathPattern(selectors, [...skipKeys]);
60
76
  }
@@ -0,0 +1,223 @@
1
+ 'use strict';
2
+
3
+ /* How a schema problem is worded: the field it names and the sentence about it. */
4
+
5
+ const { descriptionText } = require('./requirement-markers');
6
+
7
+ const DEPENDENCY_KEYWORDS = ['dependencies', 'dependentRequired', 'dependentSchemas'];
8
+ const SCHEMA_MAP_KEYWORDS = [
9
+ '$defs',
10
+ 'definitions',
11
+ 'dependencies',
12
+ 'dependentSchemas',
13
+ 'patternProperties',
14
+ 'properties',
15
+ ];
16
+ const DEFINITION_KEYWORDS = ['$defs', 'definitions'];
17
+ const UNSTATED_CONDITION = 'a condition the schema states';
18
+
19
+ const hasOwn = (object, field) => Object.prototype.hasOwnProperty.call(object, field);
20
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
21
+
22
+ function unescapePointer(step) {
23
+ return step.replaceAll('~1', '/').replaceAll('~0', '~');
24
+ }
25
+
26
+ function escapePointer(key) {
27
+ return key.replaceAll('~', '~0').replaceAll('/', '~1');
28
+ }
29
+
30
+ function schemaNodeAt(root, pointer) {
31
+ if (pointer === '') return root;
32
+ let node = root;
33
+ for (const step of pointer.slice(1).split('/')) {
34
+ const key = unescapePointer(step);
35
+ if (node === null || typeof node !== 'object' || !hasOwn(node, key)) return undefined;
36
+ node = node[key];
37
+ }
38
+ return node;
39
+ }
40
+
41
+ /**
42
+ * The field a problem names: the key of the value at `pointer` in `document`, then
43
+ * `[<index>]` for each list index below it; the root is `the document`.
44
+ */
45
+ function fieldName(document, pointer) {
46
+ if (pointer === '') return 'the document';
47
+ const steps = [];
48
+ let node = document;
49
+ for (const step of pointer.slice(1).split('/')) {
50
+ const key = unescapePointer(step);
51
+ const inList = Array.isArray(node);
52
+ if (inList) node = node[Number(key)];
53
+ else node = isObject(node) && hasOwn(node, key) ? node[key] : undefined;
54
+ steps.push({ key, inList });
55
+ }
56
+ let keyAt = steps.length - 1;
57
+ while (keyAt >= 0 && steps[keyAt].inList) keyAt -= 1;
58
+ const key = keyAt >= 0 ? steps[keyAt].key : '';
59
+ const indexes = steps.slice(keyAt + 1).map((step) => `[${step.key}]`).join('');
60
+ return `\`${key}${indexes}\``;
61
+ }
62
+
63
+ /** A JSON number as a report writes it. */
64
+ function numberText(number) {
65
+ return String(number);
66
+ }
67
+
68
+ /**
69
+ * The sentence of an alternative none of whose forms holds: the first sentence of
70
+ * the definition holding the alternatives, where they are a described
71
+ * definition's, `#/$defs/<name>` or `#/definitions/<name>`; else `null`.
72
+ */
73
+ function describedForm(schema, holderPointer) {
74
+ const steps = holderPointer.split('/').slice(1);
75
+ if (steps.length !== 2 || !DEFINITION_KEYWORDS.includes(steps[0])) return null;
76
+ const description = descriptionText(schemaNodeAt(schema, holderPointer));
77
+ if (!description) return null;
78
+ const sentence = description.split(/(?<=\.)\s/u)[0];
79
+ const firstSentence = sentence.endsWith('.') ? sentence.slice(0, -1) : sentence;
80
+ const leadingWord = /^[A-Za-z0-9]*/u.exec(firstSentence)[0];
81
+ if (leadingWord.length >= 2 && /^[A-Z]+$/u.test(leadingWord)) return firstSentence;
82
+ const [first, ...rest] = Array.from(firstSentence);
83
+ return first === undefined ? null : `${first.toLowerCase()}${rest.join('')}`;
84
+ }
85
+
86
+ /**
87
+ * The dependency keyword's pointer and the key it names, `{ keywordPointer, key }`,
88
+ * where `keywordPointer` lies inside a dependency: the key read from the pointer,
89
+ * or, for a dependency listing fields, from the field `holder` misses; else `null`.
90
+ */
91
+ function dependencyOf(schema, keywordPointer, holder, missingField) {
92
+ const steps = keywordPointer.split('/').slice(1).map(unescapePointer);
93
+ let at = steps.length - 1;
94
+ while (at >= 0 && !(DEPENDENCY_KEYWORDS.includes(steps[at])
95
+ && (at === 0 || !SCHEMA_MAP_KEYWORDS.includes(steps[at - 1])))) at -= 1;
96
+ if (at < 0) return null;
97
+ const dependencyPointer = steps.slice(0, at + 1)
98
+ .map((step) => `/${escapePointer(step)}`)
99
+ .join('');
100
+ let key = steps[at + 1];
101
+ if (key === undefined) {
102
+ const listed = schemaNodeAt(schema, dependencyPointer);
103
+ if (typeof missingField !== 'string' || !isObject(listed)) return null;
104
+ key = Object.keys(listed).find((candidate) => isObject(holder) && hasOwn(holder, candidate)
105
+ && Array.isArray(listed[candidate]) && listed[candidate].includes(missingField));
106
+ if (key === undefined) return null;
107
+ }
108
+ if (!isObject(holder) || !hasOwn(holder, key)) return null;
109
+ return { keywordPointer: dependencyPointer, key };
110
+ }
111
+
112
+ /**
113
+ * `` `key` needs <what> ``: what each dependency of the node holding
114
+ * `keywordPointer` states for `key`.
115
+ */
116
+ function dependencyMessage(schema, keywordPointer, key) {
117
+ const holder = schemaNodeAt(schema, keywordPointer.slice(0, keywordPointer.lastIndexOf('/')));
118
+ const statesKey = (keyword) => isObject(holder)
119
+ && isObject(holder[keyword])
120
+ && hasOwn(holder[keyword], key);
121
+ const neededTexts = DEPENDENCY_KEYWORDS
122
+ .filter(statesKey)
123
+ .map((keyword) => neededText(holder[keyword][key]));
124
+ return `\`${key}\` needs ${neededTexts.length > 0 ? neededTexts.join('; ') : UNSTATED_CONDITION}`;
125
+ }
126
+
127
+ function neededText(needed) {
128
+ if (Array.isArray(needed)) return keyList(needed);
129
+ return consequenceText(needed) || UNSTATED_CONDITION;
130
+ }
131
+
132
+ function consequenceText(node) {
133
+ if (!isObject(node)) return null;
134
+ const parts = [];
135
+ if (Array.isArray(node.required) && node.required.length > 0) parts.push(keyList(node.required));
136
+ if (isObject(node.not) && Array.isArray(node.not.required) && node.not.required.length > 0) {
137
+ parts.push(`${keyList(node.not.required)} must not be set`);
138
+ }
139
+ const properties = isObject(node.properties) ? node.properties : {};
140
+ for (const [field, fieldSchema] of Object.entries(properties)) {
141
+ if (isObject(fieldSchema) && typeof fieldSchema.$ref === 'string') {
142
+ parts.push(`\`${field}\` is a \`${referenceName(fieldSchema.$ref)}\``);
143
+ }
144
+ }
145
+ for (const part of Array.isArray(node.allOf) ? node.allOf : []) {
146
+ const partText = consequenceText(part);
147
+ if (partText) parts.push(partText);
148
+ }
149
+ const alternatives = ['anyOf', 'oneOf']
150
+ .flatMap((keyword) => (Array.isArray(node[keyword]) ? node[keyword] : []));
151
+ if (alternatives.length > 0) {
152
+ parts.push(alternatives
153
+ .map((alternative) => consequenceText(alternative) || UNSTATED_CONDITION)
154
+ .join(', or '));
155
+ }
156
+ if (parts.length === 0 && typeof node.$ref === 'string') {
157
+ parts.push(`a \`${referenceName(node.$ref)}\``);
158
+ }
159
+ return parts.length > 0 ? parts.join('; ') : null;
160
+ }
161
+
162
+ function referenceName(reference) {
163
+ return reference.slice(reference.lastIndexOf('/') + 1).replace(/^#/u, '');
164
+ }
165
+
166
+ function keyList(keys) {
167
+ const spans = keys.map((key) => `\`${typeof key === 'string' ? key : JSON.stringify(key)}\``);
168
+ return spans.length > 1 ? `${spans.slice(0, -1).join(', ')} and ${spans.at(-1)}` : spans.join('');
169
+ }
170
+
171
+ /**
172
+ * The indexes of the duplicate items a list reports, later first, `[later, earlier]`:
173
+ * from the last item back, the first one equal to an item after it, and the
174
+ * nearest such item; `null` for a list without duplicates.
175
+ */
176
+ function duplicateItems(items) {
177
+ for (let earlier = items.length - 1; earlier >= 0; earlier -= 1) {
178
+ for (let later = earlier + 1; later < items.length; later += 1) {
179
+ if (jsonEqual(items[earlier], items[later])) return [later, earlier];
180
+ }
181
+ }
182
+ return null;
183
+ }
184
+
185
+ function jsonEqual(first, second) {
186
+ if (Array.isArray(first) && Array.isArray(second)) {
187
+ return first.length === second.length
188
+ && first.every((item, index) => jsonEqual(item, second[index]));
189
+ }
190
+ if (isObject(first) && isObject(second)) {
191
+ const keys = Object.keys(first);
192
+ return keys.length === Object.keys(second).length
193
+ && keys.every((key) => hasOwn(second, key) && jsonEqual(first[key], second[key]));
194
+ }
195
+ return first === second;
196
+ }
197
+
198
+ /**
199
+ * `must contain at least <min> valid item(s)`, with `and no more than <max>`
200
+ * where `holder` bounds it.
201
+ */
202
+ function containsMessage(holder) {
203
+ const bound = (keyword) => (isObject(holder) && typeof holder[keyword] === 'number'
204
+ ? numberText(holder[keyword])
205
+ : null);
206
+ const minimum = bound('minContains') || '1';
207
+ const maximum = bound('maxContains');
208
+ return maximum === null
209
+ ? `must contain at least ${minimum} valid item(s)`
210
+ : `must contain at least ${minimum} and no more than ${maximum} valid item(s)`;
211
+ }
212
+
213
+ module.exports = {
214
+ fieldName,
215
+ numberText,
216
+ describedForm,
217
+ dependencyOf,
218
+ dependencyMessage,
219
+ duplicateItems,
220
+ containsMessage,
221
+ schemaNodeAt,
222
+ escapePointer,
223
+ };
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Requirement markers: a field's `description` opening with `[required]`,
5
+ * `[~required]` or `[optional]` decides whether the field is required, over its
6
+ * parent's `required` array.
7
+ */
8
+
9
+ const REQUIRED_MARKER = '[required]';
10
+ const REQUIREMENT_MARKERS = [REQUIRED_MARKER, '[~required]', '[optional]'];
11
+ const CONDITION_KEYWORDS = new Set(['if', 'not']);
12
+ const FORM_SELECTING_KEYWORDS = new Set(['anyOf', 'oneOf']);
13
+ const SAME_INSTANCE_SCHEMA_KEYWORDS = new Set(['else', 'if', 'not', 'then']);
14
+ const SAME_INSTANCE_LIST_KEYWORDS = new Set(['allOf', 'anyOf', 'oneOf']);
15
+ const SCHEMA_MAP_KEYWORDS = new Set([
16
+ '$defs',
17
+ 'definitions',
18
+ 'dependentSchemas',
19
+ 'patternProperties',
20
+ 'properties',
21
+ ]);
22
+ const DRAFT_KEYWORDS = {
23
+ 'draft-07': {
24
+ childSchema: new Set([
25
+ 'additionalItems',
26
+ 'additionalProperties',
27
+ 'contains',
28
+ 'items',
29
+ 'propertyNames',
30
+ ]),
31
+ schemaMap: new Set([
32
+ '$defs',
33
+ 'definitions',
34
+ 'dependencies',
35
+ 'patternProperties',
36
+ 'properties',
37
+ ]),
38
+ },
39
+ 'draft 2019-09': {
40
+ childSchema: new Set([
41
+ 'additionalItems',
42
+ 'additionalProperties',
43
+ 'contains',
44
+ 'contentSchema',
45
+ 'items',
46
+ 'propertyNames',
47
+ 'unevaluatedItems',
48
+ 'unevaluatedProperties',
49
+ ]),
50
+ schemaMap: SCHEMA_MAP_KEYWORDS,
51
+ },
52
+ 'draft 2020-12': {
53
+ childSchema: new Set([
54
+ 'additionalProperties',
55
+ 'contains',
56
+ 'contentSchema',
57
+ 'items',
58
+ 'prefixItems',
59
+ 'propertyNames',
60
+ 'unevaluatedItems',
61
+ 'unevaluatedProperties',
62
+ ]),
63
+ schemaMap: SCHEMA_MAP_KEYWORDS,
64
+ },
65
+ };
66
+
67
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
68
+
69
+ /** The marker a schema node's `description` opens with, or `null`. */
70
+ function requirementMarker(node) {
71
+ if (!isObject(node) || typeof node.description !== 'string') return null;
72
+ const description = node.description.trimStart();
73
+ return REQUIREMENT_MARKERS.find((marker) => description.startsWith(marker)) || null;
74
+ }
75
+
76
+ /** A node's description without the marker it opens with, or `null`. */
77
+ function descriptionText(node) {
78
+ if (!isObject(node) || typeof node.description !== 'string') return null;
79
+ const marker = requirementMarker(node);
80
+ const text = marker ? node.description.trimStart().slice(marker.length) : node.description;
81
+ return text.trim() || null;
82
+ }
83
+
84
+ /** `schema`, read in the draft named `draftName`, with each `required` array decided by markers. */
85
+ function markedSchema(schema, draftName) {
86
+ return markedNode(schema, null, DRAFT_KEYWORDS[draftName]);
87
+ }
88
+
89
+ function markersOfFields(properties) {
90
+ const markers = new Map();
91
+ for (const [key, field] of Object.entries(properties)) {
92
+ const marker = requirementMarker(field);
93
+ if (marker) markers.set(key, marker);
94
+ }
95
+ return markers;
96
+ }
97
+
98
+ function markedNode(node, inheritedMarkers, keywords) {
99
+ if (!isObject(node)) return node;
100
+ const ownMarkers = isObject(node.properties) ? markersOfFields(node.properties) : null;
101
+ const markers = ownMarkers || inheritedMarkers;
102
+ const marked = {};
103
+ const markedChild = (child) => markedNode(child, null, keywords);
104
+ for (const [keyword, value] of Object.entries(node)) {
105
+ if (keyword === 'required') continue;
106
+ if (CONDITION_KEYWORDS.has(keyword)) marked[keyword] = value;
107
+ else if (SAME_INSTANCE_SCHEMA_KEYWORDS.has(keyword)) {
108
+ marked[keyword] = markedNode(value, markers, keywords);
109
+ } else if (SAME_INSTANCE_LIST_KEYWORDS.has(keyword) && Array.isArray(value)) {
110
+ const branchMarkers = FORM_SELECTING_KEYWORDS.has(keyword) ? null : markers;
111
+ marked[keyword] = value.map((branch) => markedNode(branch, branchMarkers, keywords));
112
+ } else if (keywords.childSchema.has(keyword)) {
113
+ marked[keyword] = Array.isArray(value) ? value.map(markedChild) : markedChild(value);
114
+ } else if (keywords.schemaMap.has(keyword) && isObject(value)) {
115
+ marked[keyword] = Object.fromEntries(Object.entries(value)
116
+ .map(([name, schema]) => [name, markedChild(schema)]));
117
+ } else {
118
+ marked[keyword] = value;
119
+ }
120
+ }
121
+ const listed = Array.isArray(node.required) ? node.required : [];
122
+ const required = listed.filter((key) => !markers || !markers.has(key));
123
+ for (const [key, marker] of ownMarkers || []) if (marker === REQUIRED_MARKER) required.push(key);
124
+ if (required.length > 0) marked.required = required;
125
+ return marked;
126
+ }
127
+
128
+ module.exports = { requirementMarker, descriptionText, markedSchema };