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