@speclynx/apidom-traverse 1.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/AFL-3.0.txt +182 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/LICENSES/BSD-3-Clause.txt +26 -0
- package/LICENSES/MIT.txt +9 -0
- package/NOTICE +74 -0
- package/README.md +238 -0
- package/dist/apidom-traverse.browser.js +6900 -0
- package/dist/apidom-traverse.browser.min.js +1 -0
- package/package.json +59 -0
- package/src/Path.cjs +276 -0
- package/src/Path.mjs +271 -0
- package/src/index.cjs +43 -0
- package/src/index.mjs +19 -0
- package/src/operations/filter.cjs +21 -0
- package/src/operations/filter.mjs +17 -0
- package/src/operations/find-at-offset.cjs +45 -0
- package/src/operations/find-at-offset.mjs +40 -0
- package/src/operations/find.cjs +22 -0
- package/src/operations/find.mjs +18 -0
- package/src/operations/for-each.cjs +37 -0
- package/src/operations/for-each.mjs +31 -0
- package/src/operations/parents.cjs +21 -0
- package/src/operations/parents.mjs +17 -0
- package/src/operations/reject.cjs +14 -0
- package/src/operations/reject.mjs +9 -0
- package/src/operations/some.cjs +14 -0
- package/src/operations/some.mjs +9 -0
- package/src/traversal.cjs +313 -0
- package/src/traversal.mjs +305 -0
- package/src/visitors.cjs +420 -0
- package/src/visitors.mjs +407 -0
- package/types/apidom-traverse.d.ts +403 -0
package/src/visitors.mjs
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { ApiDOMStructuredError } from '@speclynx/apidom-error';
|
|
2
|
+
import { isElement, isMemberElement, isArrayElement, isObjectElement, cloneShallow } from '@speclynx/apidom-datamodel';
|
|
3
|
+
import { isPromise } from 'ramda-adjunct';
|
|
4
|
+
import { Path } from "./Path.mjs";
|
|
5
|
+
/**
|
|
6
|
+
* Enter/leave visitor structure for a specific node type.
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Default implementations for ApiDOM
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default node type getter - reads the `element` property and converts to Element class name.
|
|
15
|
+
* E.g., "string" -\> "StringElement", "openApi3_1" -\> "OpenApi3_1Element"
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export const getNodeType = node => {
|
|
19
|
+
const type = node?.element;
|
|
20
|
+
if (type === undefined || type === 'element') return 'Element';
|
|
21
|
+
return `${type.charAt(0).toUpperCase()}${type.slice(1)}Element`;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Alternative node type getter using primitive type.
|
|
26
|
+
* Returns the base element class name based on the node's primitive type.
|
|
27
|
+
* E.g., ContactElement (primitive='object') -\> "ObjectElement"
|
|
28
|
+
*
|
|
29
|
+
* Use this with `nodeTypeGetter` option when you want polymorphic behavior
|
|
30
|
+
* where specific elements fall back to their primitive type handlers.
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export const getNodePrimitiveType = node => {
|
|
34
|
+
const type = node.primitive();
|
|
35
|
+
if (type === undefined || type === 'element') return 'Element';
|
|
36
|
+
return `${type.charAt(0).toUpperCase()}${type.slice(1)}Element`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Default node predicate - checks if value is an ApiDOM Element.
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export const isNode = value => isElement(value);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default node clone function - creates a shallow clone of ApiDOM Elements.
|
|
47
|
+
* Uses cloneShallow from apidom-datamodel for proper handling of meta/attributes.
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export const cloneNode = node => cloneShallow(node);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Default mutation function that handles ApiDOM structures.
|
|
54
|
+
* - MemberElement: sets parent.value
|
|
55
|
+
* - Arrays: sets parent[key]
|
|
56
|
+
* - Objects: sets parent[key] or deletes if null
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
export const mutateNode = (parent, key, value) => {
|
|
60
|
+
if (isMemberElement(parent)) {
|
|
61
|
+
// MemberElement stores value in .value property
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
parent.value = value;
|
|
64
|
+
} else if (Array.isArray(parent)) {
|
|
65
|
+
if (value === null) {
|
|
66
|
+
// For arrays, set to undefined (caller handles cleanup)
|
|
67
|
+
parent[key] = undefined;
|
|
68
|
+
} else {
|
|
69
|
+
parent[key] = value;
|
|
70
|
+
}
|
|
71
|
+
} else if (value === null) {
|
|
72
|
+
delete parent[key];
|
|
73
|
+
} else {
|
|
74
|
+
parent[key] = value;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default function to get traversable keys for a node.
|
|
80
|
+
* Uses predicates to handle all ApiDOM element types automatically.
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
export const getNodeKeys = node => {
|
|
84
|
+
if (isMemberElement(node)) return ['key', 'value'];
|
|
85
|
+
if (isArrayElement(node) || isObjectElement(node)) return ['content'];
|
|
86
|
+
return [];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// Visitor function resolution
|
|
91
|
+
// =============================================================================
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Lookup by type with pipe-separated key support ("TypeA|TypeB").
|
|
95
|
+
* Optimized: no array allocations, uses indexOf + boundary checks.
|
|
96
|
+
*/
|
|
97
|
+
const lookup = (record, type) => {
|
|
98
|
+
// Fast path: exact match
|
|
99
|
+
if (record[type] !== undefined) return record[type];
|
|
100
|
+
|
|
101
|
+
// Slow path: check pipe-separated keys
|
|
102
|
+
const len = type.length;
|
|
103
|
+
for (const key in record) {
|
|
104
|
+
// Skip keys without pipe (most common case)
|
|
105
|
+
if (!key.includes('|')) continue;
|
|
106
|
+
const idx = key.indexOf(type);
|
|
107
|
+
if (idx === -1) continue;
|
|
108
|
+
|
|
109
|
+
// Verify it's a complete segment (bounded by | or string edges)
|
|
110
|
+
const before = idx === 0 || key[idx - 1] === '|';
|
|
111
|
+
const after = idx + len === key.length || key[idx + len] === '|';
|
|
112
|
+
if (before && after) return record[key];
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Gets the appropriate visitor function for a node type and phase.
|
|
119
|
+
* Supports pipe-separated type keys like "TypeA|TypeB".
|
|
120
|
+
* @public
|
|
121
|
+
*/
|
|
122
|
+
export const getVisitFn = (visitor, type, isLeaving) => {
|
|
123
|
+
if (type === undefined) return null;
|
|
124
|
+
const visitorRecord = visitor;
|
|
125
|
+
const phase = isLeaving ? 'leave' : 'enter';
|
|
126
|
+
|
|
127
|
+
// Pattern 1: { Type() {} } - shorthand for enter only
|
|
128
|
+
const typeVisitor = lookup(visitorRecord, type);
|
|
129
|
+
if (!isLeaving && typeof typeVisitor === 'function') {
|
|
130
|
+
return typeVisitor;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Pattern 2: { Type: { enter, leave } }
|
|
134
|
+
if (typeVisitor != null) {
|
|
135
|
+
const phaseVisitor = typeVisitor[phase];
|
|
136
|
+
if (typeof phaseVisitor === 'function') {
|
|
137
|
+
return phaseVisitor;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Pattern 3: { enter() {}, leave() {} }
|
|
142
|
+
const genericVisitor = visitorRecord[phase];
|
|
143
|
+
if (typeof genericVisitor === 'function') {
|
|
144
|
+
return genericVisitor;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Pattern 4: { enter: { Type() {} } }
|
|
148
|
+
if (genericVisitor != null) {
|
|
149
|
+
const typeInPhase = lookup(genericVisitor, type);
|
|
150
|
+
if (typeof typeInPhase === 'function') {
|
|
151
|
+
return typeInPhase;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// Visitor merging
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Options for mergeVisitors.
|
|
163
|
+
* @public
|
|
164
|
+
*/
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Sync version of merged visitor.
|
|
168
|
+
* @public
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Async version of merged visitor.
|
|
173
|
+
* @public
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Creates a new visitor instance which delegates to many visitors to run in
|
|
178
|
+
* parallel. Each visitor will be visited for each node before moving on.
|
|
179
|
+
*
|
|
180
|
+
* If a prior visitor edits a node, no following visitors will see that node.
|
|
181
|
+
* `exposeEdits=true` can be used to expose the edited node from the previous visitors.
|
|
182
|
+
* @public
|
|
183
|
+
*/
|
|
184
|
+
export const mergeVisitors = (visitors, options = {}) => {
|
|
185
|
+
const {
|
|
186
|
+
visitFnGetter = getVisitFn,
|
|
187
|
+
nodeTypeGetter = getNodeType,
|
|
188
|
+
exposeEdits = false
|
|
189
|
+
} = options;
|
|
190
|
+
|
|
191
|
+
// Internal symbols for tracking visitor state
|
|
192
|
+
const internalSkipSymbol = Symbol('internal-skip');
|
|
193
|
+
const breakSymbol = Symbol('break');
|
|
194
|
+
// Tracks which visitors should be skipped for current subtree or permanently stopped
|
|
195
|
+
const skipping = new Array(visitors.length).fill(internalSkipSymbol);
|
|
196
|
+
return {
|
|
197
|
+
enter(path) {
|
|
198
|
+
let currentNode = path.node;
|
|
199
|
+
let hasChanged = false;
|
|
200
|
+
for (let i = 0; i < visitors.length; i += 1) {
|
|
201
|
+
if (skipping[i] === internalSkipSymbol) {
|
|
202
|
+
const visitFn = visitFnGetter(visitors[i], nodeTypeGetter(currentNode), false);
|
|
203
|
+
if (typeof visitFn === 'function') {
|
|
204
|
+
// Create a proxy path that tracks changes per-visitor
|
|
205
|
+
const proxyPath = createPathProxy(path, currentNode);
|
|
206
|
+
const result = visitFn.call(visitors[i], proxyPath);
|
|
207
|
+
|
|
208
|
+
// Check if the visitor is async
|
|
209
|
+
if (isPromise(result)) {
|
|
210
|
+
throw new ApiDOMStructuredError('Async visitor not supported in sync mode', {
|
|
211
|
+
visitor: visitors[i],
|
|
212
|
+
visitFn
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Handle path-based control flow
|
|
217
|
+
if (proxyPath.shouldStop) {
|
|
218
|
+
skipping[i] = breakSymbol;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
if (proxyPath.shouldSkip) {
|
|
222
|
+
skipping[i] = currentNode;
|
|
223
|
+
}
|
|
224
|
+
if (proxyPath.removed) {
|
|
225
|
+
path.remove();
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
if (proxyPath._wasReplaced()) {
|
|
229
|
+
const replacement = proxyPath._getReplacementNode();
|
|
230
|
+
if (exposeEdits) {
|
|
231
|
+
currentNode = replacement;
|
|
232
|
+
hasChanged = true;
|
|
233
|
+
} else {
|
|
234
|
+
path.replaceWith(replacement);
|
|
235
|
+
return replacement;
|
|
236
|
+
}
|
|
237
|
+
} else if (result !== undefined) {
|
|
238
|
+
// Support return value replacement for backwards compatibility
|
|
239
|
+
if (exposeEdits) {
|
|
240
|
+
currentNode = result;
|
|
241
|
+
hasChanged = true;
|
|
242
|
+
} else {
|
|
243
|
+
path.replaceWith(result);
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (hasChanged) {
|
|
251
|
+
path.replaceWith(currentNode);
|
|
252
|
+
return currentNode;
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
},
|
|
256
|
+
leave(path) {
|
|
257
|
+
const currentNode = path.node;
|
|
258
|
+
for (let i = 0; i < visitors.length; i += 1) {
|
|
259
|
+
if (skipping[i] === internalSkipSymbol) {
|
|
260
|
+
const visitFn = visitFnGetter(visitors[i], nodeTypeGetter(currentNode), true);
|
|
261
|
+
if (typeof visitFn === 'function') {
|
|
262
|
+
// Create a proxy path for leave phase
|
|
263
|
+
const proxyPath = createPathProxy(path, currentNode);
|
|
264
|
+
const result = visitFn.call(visitors[i], proxyPath);
|
|
265
|
+
|
|
266
|
+
// Check if the visitor is async
|
|
267
|
+
if (isPromise(result)) {
|
|
268
|
+
throw new ApiDOMStructuredError('Async visitor not supported in sync mode', {
|
|
269
|
+
visitor: visitors[i],
|
|
270
|
+
visitFn
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Handle path-based control flow
|
|
275
|
+
if (proxyPath.shouldStop) {
|
|
276
|
+
skipping[i] = breakSymbol;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
if (proxyPath.removed) {
|
|
280
|
+
path.remove();
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
if (proxyPath._wasReplaced()) {
|
|
284
|
+
const replacement = proxyPath._getReplacementNode();
|
|
285
|
+
path.replaceWith(replacement);
|
|
286
|
+
return replacement;
|
|
287
|
+
} else if (result !== undefined) {
|
|
288
|
+
path.replaceWith(result);
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} else if (skipping[i] === currentNode) {
|
|
293
|
+
// Reset skip state when leaving the node that was skipped
|
|
294
|
+
skipping[i] = internalSkipSymbol;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Async version of mergeVisitors.
|
|
304
|
+
* @public
|
|
305
|
+
*/
|
|
306
|
+
export const mergeVisitorsAsync = (visitors, options = {}) => {
|
|
307
|
+
const {
|
|
308
|
+
visitFnGetter = getVisitFn,
|
|
309
|
+
nodeTypeGetter = getNodeType,
|
|
310
|
+
exposeEdits = false
|
|
311
|
+
} = options;
|
|
312
|
+
const internalSkipSymbol = Symbol('internal-skip');
|
|
313
|
+
const breakSymbol = Symbol('break');
|
|
314
|
+
const skipping = new Array(visitors.length).fill(internalSkipSymbol);
|
|
315
|
+
return {
|
|
316
|
+
async enter(path) {
|
|
317
|
+
let currentNode = path.node;
|
|
318
|
+
let hasChanged = false;
|
|
319
|
+
for (let i = 0; i < visitors.length; i += 1) {
|
|
320
|
+
if (skipping[i] === internalSkipSymbol) {
|
|
321
|
+
const visitFn = visitFnGetter(visitors[i], nodeTypeGetter(currentNode), false);
|
|
322
|
+
if (typeof visitFn === 'function') {
|
|
323
|
+
const proxyPath = createPathProxy(path, currentNode);
|
|
324
|
+
const result = await visitFn.call(visitors[i], proxyPath);
|
|
325
|
+
if (proxyPath.shouldStop) {
|
|
326
|
+
skipping[i] = breakSymbol;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
if (proxyPath.shouldSkip) {
|
|
330
|
+
skipping[i] = currentNode;
|
|
331
|
+
}
|
|
332
|
+
if (proxyPath.removed) {
|
|
333
|
+
path.remove();
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
if (proxyPath._wasReplaced()) {
|
|
337
|
+
const replacement = proxyPath._getReplacementNode();
|
|
338
|
+
if (exposeEdits) {
|
|
339
|
+
currentNode = replacement;
|
|
340
|
+
hasChanged = true;
|
|
341
|
+
} else {
|
|
342
|
+
path.replaceWith(replacement);
|
|
343
|
+
return replacement;
|
|
344
|
+
}
|
|
345
|
+
} else if (result !== undefined) {
|
|
346
|
+
if (exposeEdits) {
|
|
347
|
+
currentNode = result;
|
|
348
|
+
hasChanged = true;
|
|
349
|
+
} else {
|
|
350
|
+
path.replaceWith(result);
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (hasChanged) {
|
|
358
|
+
path.replaceWith(currentNode);
|
|
359
|
+
return currentNode;
|
|
360
|
+
}
|
|
361
|
+
return undefined;
|
|
362
|
+
},
|
|
363
|
+
async leave(path) {
|
|
364
|
+
const currentNode = path.node;
|
|
365
|
+
for (let i = 0; i < visitors.length; i += 1) {
|
|
366
|
+
if (skipping[i] === internalSkipSymbol) {
|
|
367
|
+
const visitFn = visitFnGetter(visitors[i], nodeTypeGetter(currentNode), true);
|
|
368
|
+
if (typeof visitFn === 'function') {
|
|
369
|
+
const proxyPath = createPathProxy(path, currentNode);
|
|
370
|
+
const result = await visitFn.call(visitors[i], proxyPath);
|
|
371
|
+
if (proxyPath.shouldStop) {
|
|
372
|
+
skipping[i] = breakSymbol;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
if (proxyPath.removed) {
|
|
376
|
+
path.remove();
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
if (proxyPath._wasReplaced()) {
|
|
380
|
+
const replacement = proxyPath._getReplacementNode();
|
|
381
|
+
path.replaceWith(replacement);
|
|
382
|
+
return replacement;
|
|
383
|
+
} else if (result !== undefined) {
|
|
384
|
+
path.replaceWith(result);
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} else if (skipping[i] === currentNode) {
|
|
389
|
+
skipping[i] = internalSkipSymbol;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// Attach async version for promisify compatibility
|
|
398
|
+
mergeVisitors[Symbol.for('nodejs.util.promisify.custom')] = mergeVisitorsAsync;
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Creates a proxy Path that allows individual visitors to track their own
|
|
402
|
+
* control flow state without affecting the original Path.
|
|
403
|
+
* @internal
|
|
404
|
+
*/
|
|
405
|
+
function createPathProxy(originalPath, currentNode) {
|
|
406
|
+
return new Path(currentNode, originalPath.parent, originalPath.parentPath, originalPath.key, originalPath.inList);
|
|
407
|
+
}
|