@speclynx/apidom-parser-adapter-json 4.0.2 → 4.0.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.
@@ -0,0 +1,350 @@
1
+ import { TreeCursor, Tree, Point } from 'web-tree-sitter';
2
+ import {
3
+ BooleanElement,
4
+ NullElement,
5
+ NumberElement,
6
+ ParseResultElement,
7
+ Element,
8
+ MemberElement,
9
+ ObjectElement,
10
+ ArrayElement,
11
+ StringElement,
12
+ AnnotationElement,
13
+ isPrimitiveElement,
14
+ } from '@speclynx/apidom-datamodel';
15
+
16
+ interface TransformContext {
17
+ sourceMap: boolean;
18
+ style: boolean;
19
+ indent: number;
20
+ annotations: AnnotationElement[];
21
+ }
22
+
23
+ interface CursorInfo {
24
+ type: string;
25
+ startPosition: Point;
26
+ endPosition: Point;
27
+ startIndex: number;
28
+ endIndex: number;
29
+ text: string;
30
+ isNamed: boolean;
31
+ isMissing: boolean;
32
+ hasError: boolean;
33
+ fieldName: string | null;
34
+ }
35
+
36
+ const getCursorInfo = (cursor: TreeCursor): CursorInfo => ({
37
+ type: cursor.nodeType,
38
+ startPosition: cursor.startPosition,
39
+ endPosition: cursor.endPosition,
40
+ startIndex: cursor.startIndex,
41
+ endIndex: cursor.endIndex,
42
+ text: cursor.nodeText,
43
+ isNamed: cursor.nodeIsNamed,
44
+ isMissing: cursor.nodeIsMissing,
45
+ hasError: cursor.currentNode.hasError,
46
+ fieldName: cursor.currentFieldName,
47
+ });
48
+
49
+ const maybeAddSourceMap = (info: CursorInfo, element: Element, ctx: TransformContext): void => {
50
+ if (!ctx.sourceMap) {
51
+ return;
52
+ }
53
+
54
+ element.startLine = info.startPosition.row;
55
+ element.startCharacter = info.startPosition.column;
56
+ element.startOffset = info.startIndex;
57
+ element.endLine = info.endPosition.row;
58
+ element.endCharacter = info.endPosition.column;
59
+ element.endOffset = info.endIndex;
60
+ };
61
+
62
+ // build json style object for an element
63
+ const buildJsonStyle = (
64
+ ctx: TransformContext,
65
+ extras?: Record<string, unknown>,
66
+ ): Record<string, unknown> => {
67
+ const jsonStyle: Record<string, unknown> = { indent: ctx.indent };
68
+ if (extras) Object.assign(jsonStyle, extras);
69
+ return { json: jsonStyle };
70
+ };
71
+
72
+ // detect indent from an object's first pair child position
73
+ // called during transformChildren when we encounter the first pair
74
+ const detectIndent = (objectColumn: number, firstPairColumn: number): number => {
75
+ const diff = firstPairColumn - objectColumn;
76
+ return diff > 0 ? diff : 2;
77
+ };
78
+
79
+ type Transformer = (cursor: TreeCursor, ctx: TransformContext) => Element | null;
80
+ type TransformerMap = Record<string, Transformer>;
81
+
82
+ const transform = (
83
+ cursor: TreeCursor,
84
+ transformerMap: TransformerMap,
85
+ ctx: TransformContext,
86
+ ): Element | null => {
87
+ const info = getCursorInfo(cursor);
88
+
89
+ // Handle missing anonymous literals
90
+ if (!info.isNamed && info.isMissing) {
91
+ const value = info.type || info.text;
92
+ const message = `(Missing ${value})`;
93
+ const element = new AnnotationElement(message);
94
+ element.classes.push('warning');
95
+ maybeAddSourceMap(info, element, ctx);
96
+ ctx.annotations.push(element);
97
+ return null;
98
+ }
99
+
100
+ const transformer = transformerMap[info.type];
101
+ if (!transformer) {
102
+ return null; // remove unrecognized nodes
103
+ }
104
+
105
+ return transformer(cursor, ctx);
106
+ };
107
+
108
+ const transformChildren = (
109
+ cursor: TreeCursor,
110
+ transformerMap: TransformerMap,
111
+ ctx: TransformContext,
112
+ ): Element[] => {
113
+ const results: Element[] = [];
114
+
115
+ if (cursor.gotoFirstChild()) {
116
+ do {
117
+ const transformed = transform(cursor, transformerMap, ctx);
118
+ if (transformed !== null) {
119
+ results.push(transformed);
120
+ }
121
+ } while (cursor.gotoNextSibling());
122
+ cursor.gotoParent();
123
+ }
124
+
125
+ return results;
126
+ };
127
+
128
+ const createTransformers = (transformerMap: TransformerMap): TransformerMap => ({
129
+ document(cursor: TreeCursor, ctx: TransformContext): ParseResultElement {
130
+ const info = getCursorInfo(cursor);
131
+ const element = new ParseResultElement();
132
+ maybeAddSourceMap(info, element, ctx);
133
+
134
+ // Transform children
135
+ const children = transformChildren(cursor, transformerMap, ctx);
136
+ for (const child of children) {
137
+ element.push(child);
138
+ }
139
+
140
+ // Mark first non-Annotation element as result
141
+ // @ts-ignore
142
+ const elements = element.findElements(isPrimitiveElement);
143
+ if (elements.length > 0) {
144
+ elements[0].classes.push('result');
145
+ }
146
+
147
+ // Add collected annotations
148
+ for (const annotation of ctx.annotations) {
149
+ element.push(annotation);
150
+ }
151
+ ctx.annotations = [];
152
+
153
+ return element;
154
+ },
155
+
156
+ object(cursor: TreeCursor, ctx: TransformContext): ObjectElement {
157
+ const info = getCursorInfo(cursor);
158
+ const element = new ObjectElement();
159
+ maybeAddSourceMap(info, element, ctx);
160
+
161
+ // Detect indent from first pair if style is enabled and not yet detected
162
+ if (ctx.style && ctx.indent === 0) {
163
+ if (cursor.gotoFirstChild()) {
164
+ do {
165
+ if (cursor.nodeType === 'pair') {
166
+ ctx.indent = detectIndent(info.startPosition.column, cursor.startPosition.column);
167
+ break;
168
+ }
169
+ } while (cursor.gotoNextSibling());
170
+ cursor.gotoParent();
171
+ }
172
+ }
173
+
174
+ // Transform children (pairs)
175
+ const children = transformChildren(cursor, transformerMap, ctx);
176
+ for (const child of children) {
177
+ element.push(child);
178
+ }
179
+
180
+ if (ctx.style) {
181
+ element.style = buildJsonStyle(ctx);
182
+ }
183
+
184
+ return element;
185
+ },
186
+
187
+ array(cursor: TreeCursor, ctx: TransformContext): ArrayElement {
188
+ const info = getCursorInfo(cursor);
189
+ const element = new ArrayElement();
190
+ maybeAddSourceMap(info, element, ctx);
191
+
192
+ // Transform children
193
+ const children = transformChildren(cursor, transformerMap, ctx);
194
+ for (const child of children) {
195
+ element.push(child);
196
+ }
197
+
198
+ if (ctx.style) {
199
+ element.style = buildJsonStyle(ctx);
200
+ }
201
+
202
+ return element;
203
+ },
204
+
205
+ pair(cursor: TreeCursor, ctx: TransformContext): MemberElement {
206
+ const info = getCursorInfo(cursor);
207
+
208
+ let key: Element | null = null;
209
+ let value: Element | null = null;
210
+
211
+ // Find key and value by field name
212
+ if (cursor.gotoFirstChild()) {
213
+ do {
214
+ const fieldName = cursor.currentFieldName;
215
+ if (fieldName === 'key') {
216
+ key = transform(cursor, transformerMap, ctx);
217
+ } else if (fieldName === 'value') {
218
+ value = transform(cursor, transformerMap, ctx);
219
+ } else if (cursor.nodeType === 'ERROR') {
220
+ // Process error nodes
221
+ transform(cursor, transformerMap, ctx);
222
+ }
223
+ } while (cursor.gotoNextSibling());
224
+ cursor.gotoParent();
225
+ }
226
+
227
+ const element = new MemberElement(key ?? new StringElement(''), value ?? new StringElement(''));
228
+ maybeAddSourceMap(info, element, ctx);
229
+
230
+ return element;
231
+ },
232
+
233
+ string(cursor: TreeCursor, ctx: TransformContext): StringElement {
234
+ const info = getCursorInfo(cursor);
235
+ let element: StringElement;
236
+
237
+ try {
238
+ element = new StringElement(JSON.parse(info.text));
239
+ } catch (error: unknown) {
240
+ element = new StringElement(info.text);
241
+ if (error instanceof Error) {
242
+ element.meta.set('jsonParse', {
243
+ isError: true,
244
+ errorType: error.name,
245
+ errorMessage: error.message,
246
+ });
247
+ }
248
+ }
249
+
250
+ if (ctx.style) {
251
+ element.style = buildJsonStyle(ctx);
252
+ }
253
+ maybeAddSourceMap(info, element, ctx);
254
+ return element;
255
+ },
256
+
257
+ number(cursor: TreeCursor, ctx: TransformContext): NumberElement {
258
+ const info = getCursorInfo(cursor);
259
+ const element = new NumberElement(Number(info.text));
260
+ if (ctx.style) {
261
+ element.style = buildJsonStyle(ctx, { rawContent: info.text });
262
+ }
263
+ maybeAddSourceMap(info, element, ctx);
264
+ return element;
265
+ },
266
+
267
+ null(cursor: TreeCursor, ctx: TransformContext): NullElement {
268
+ const info = getCursorInfo(cursor);
269
+ const element = new NullElement();
270
+ if (ctx.style) {
271
+ element.style = buildJsonStyle(ctx);
272
+ }
273
+ maybeAddSourceMap(info, element, ctx);
274
+ return element;
275
+ },
276
+
277
+ true(cursor: TreeCursor, ctx: TransformContext): BooleanElement {
278
+ const info = getCursorInfo(cursor);
279
+ const element = new BooleanElement(true);
280
+ if (ctx.style) {
281
+ element.style = buildJsonStyle(ctx);
282
+ }
283
+ maybeAddSourceMap(info, element, ctx);
284
+ return element;
285
+ },
286
+
287
+ false(cursor: TreeCursor, ctx: TransformContext): BooleanElement {
288
+ const info = getCursorInfo(cursor);
289
+ const element = new BooleanElement(false);
290
+ if (ctx.style) {
291
+ element.style = buildJsonStyle(ctx);
292
+ }
293
+ maybeAddSourceMap(info, element, ctx);
294
+ return element;
295
+ },
296
+
297
+ ERROR(cursor: TreeCursor, ctx: TransformContext): ParseResultElement | null {
298
+ const info = getCursorInfo(cursor);
299
+ const isUnexpected = !info.hasError;
300
+ const message = isUnexpected ? `(Unexpected ${info.text})` : `(Error ${info.text})`;
301
+ const element = new AnnotationElement(message);
302
+
303
+ element.classes.push('error');
304
+ maybeAddSourceMap(info, element, ctx);
305
+
306
+ ctx.annotations.push(element);
307
+
308
+ return null;
309
+ },
310
+ });
311
+
312
+ // Create the transformers map with self-reference for recursion
313
+ const transformers: TransformerMap = {};
314
+ Object.assign(transformers, createTransformers(transformers));
315
+
316
+ /**
317
+ * Syntactic analysis translates TreeSitter CST directly into ApiDOM.
318
+ *
319
+ * Direct transformation from TreeSitter CST using TreeSitter cursor.
320
+ * TreeSitter cursor is a stateful object that allows us to walk syntax tree
321
+ * containing large number of nodes with maximum efficiency.
322
+ *
323
+ * Single pass transformation from CST to ApiDOM.
324
+ * @public
325
+ */
326
+ const analyze = (cst: Tree, { sourceMap = false, style = false } = {}): ParseResultElement => {
327
+ const cursor = cst.walk();
328
+
329
+ const ctx: TransformContext = {
330
+ sourceMap,
331
+ style,
332
+ indent: 0,
333
+ annotations: [],
334
+ };
335
+
336
+ const result = transform(cursor, transformers, ctx);
337
+
338
+ // Handle case where there's no document, only errors
339
+ if (result === null) {
340
+ const parseResult = new ParseResultElement();
341
+ for (const annotation of ctx.annotations) {
342
+ parseResult.push(annotation);
343
+ }
344
+ return parseResult;
345
+ }
346
+
347
+ return result as ParseResultElement;
348
+ };
349
+
350
+ export default analyze;
package/src/wasm.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * TypeScript module declarations for WebAssembly (.wasm) files.
3
+ * This allows importing .wasm files as modules without @ts-ignore.
4
+ */
5
+ declare module '*.wasm' {
6
+ const content: string;
7
+ export default content;
8
+ }
9
+
10
+ declare module 'web-tree-sitter/tree-sitter.wasm' {
11
+ const content: string;
12
+ export default content;
13
+ }
14
+
15
+ declare module 'tree-sitter-json/tree-sitter-json.wasm' {
16
+ const content: string;
17
+ export default content;
18
+ }