@speclynx/apidom-parser-adapter-json 4.0.1 → 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,48 @@
1
+ import { Tree, Parser, Language } from 'web-tree-sitter';
2
+ import treeSitter from 'web-tree-sitter/web-tree-sitter.wasm';
3
+ // eslint-disable-next-line import/no-extraneous-dependencies
4
+ import treeSitterJson from 'tree-sitter-json/tree-sitter-json.wasm';
5
+ import { ApiDOMError } from '@speclynx/apidom-error';
6
+
7
+ let parser: Parser | null = null;
8
+ let parserInitLock: Promise<Parser> | null = null;
9
+
10
+ /**
11
+ * Lexical Analysis of source string using WebTreeSitter.
12
+ * This is WebAssembly version of TreeSitters Lexical Analysis.
13
+ *
14
+ * Given JavaScript doesn't support true parallelism, this
15
+ * code should be as lazy as possible and temporal safety should be fine.
16
+ * @public
17
+ */
18
+ const analyze = async (source: string): Promise<Tree> => {
19
+ if (parser === null && parserInitLock === null) {
20
+ // acquire lock
21
+ parserInitLock = Parser.init({
22
+ wasmBinary: treeSitter,
23
+ locateFile: (scriptName: string) => scriptName,
24
+ } as unknown as EmscriptenModule)
25
+ .then(() => Language.load(treeSitterJson))
26
+ .then((jsonLanguage) => {
27
+ const parserInstance = new Parser();
28
+ parserInstance.setLanguage(jsonLanguage);
29
+ return parserInstance;
30
+ })
31
+ .finally(() => {
32
+ // release lock
33
+ parserInitLock = null;
34
+ });
35
+ parser = await parserInitLock;
36
+ } else if (parser === null && parserInitLock !== null) {
37
+ // await for lock to be released if there is one
38
+ parser = await parserInitLock;
39
+ } else if (parser === null) {
40
+ throw new ApiDOMError(
41
+ 'Error while initializing web-tree-sitter and loading tree-sitter-json grammar.',
42
+ );
43
+ }
44
+
45
+ return parser.parse(source) as Tree;
46
+ };
47
+
48
+ export default analyze;
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = void 0;
5
+ var _apidomDatamodel = require("@speclynx/apidom-datamodel");
6
+ const getCursorInfo = cursor => ({
7
+ type: cursor.nodeType,
8
+ startPosition: cursor.startPosition,
9
+ endPosition: cursor.endPosition,
10
+ startIndex: cursor.startIndex,
11
+ endIndex: cursor.endIndex,
12
+ text: cursor.nodeText,
13
+ isNamed: cursor.nodeIsNamed,
14
+ isMissing: cursor.nodeIsMissing,
15
+ hasError: cursor.currentNode.hasError,
16
+ fieldName: cursor.currentFieldName
17
+ });
18
+ const maybeAddSourceMap = (info, element, ctx) => {
19
+ if (!ctx.sourceMap) {
20
+ return;
21
+ }
22
+ element.startLine = info.startPosition.row;
23
+ element.startCharacter = info.startPosition.column;
24
+ element.startOffset = info.startIndex;
25
+ element.endLine = info.endPosition.row;
26
+ element.endCharacter = info.endPosition.column;
27
+ element.endOffset = info.endIndex;
28
+ };
29
+
30
+ // build json style object for an element
31
+ const buildJsonStyle = (ctx, extras) => {
32
+ const jsonStyle = {
33
+ indent: ctx.indent
34
+ };
35
+ if (extras) Object.assign(jsonStyle, extras);
36
+ return {
37
+ json: jsonStyle
38
+ };
39
+ };
40
+
41
+ // detect indent from an object's first pair child position
42
+ // called during transformChildren when we encounter the first pair
43
+ const detectIndent = (objectColumn, firstPairColumn) => {
44
+ const diff = firstPairColumn - objectColumn;
45
+ return diff > 0 ? diff : 2;
46
+ };
47
+ const transform = (cursor, transformerMap, ctx) => {
48
+ const info = getCursorInfo(cursor);
49
+
50
+ // Handle missing anonymous literals
51
+ if (!info.isNamed && info.isMissing) {
52
+ const value = info.type || info.text;
53
+ const message = `(Missing ${value})`;
54
+ const element = new _apidomDatamodel.AnnotationElement(message);
55
+ element.classes.push('warning');
56
+ maybeAddSourceMap(info, element, ctx);
57
+ ctx.annotations.push(element);
58
+ return null;
59
+ }
60
+ const transformer = transformerMap[info.type];
61
+ if (!transformer) {
62
+ return null; // remove unrecognized nodes
63
+ }
64
+ return transformer(cursor, ctx);
65
+ };
66
+ const transformChildren = (cursor, transformerMap, ctx) => {
67
+ const results = [];
68
+ if (cursor.gotoFirstChild()) {
69
+ do {
70
+ const transformed = transform(cursor, transformerMap, ctx);
71
+ if (transformed !== null) {
72
+ results.push(transformed);
73
+ }
74
+ } while (cursor.gotoNextSibling());
75
+ cursor.gotoParent();
76
+ }
77
+ return results;
78
+ };
79
+ const createTransformers = transformerMap => ({
80
+ document(cursor, ctx) {
81
+ const info = getCursorInfo(cursor);
82
+ const element = new _apidomDatamodel.ParseResultElement();
83
+ maybeAddSourceMap(info, element, ctx);
84
+
85
+ // Transform children
86
+ const children = transformChildren(cursor, transformerMap, ctx);
87
+ for (const child of children) {
88
+ element.push(child);
89
+ }
90
+
91
+ // Mark first non-Annotation element as result
92
+ // @ts-ignore
93
+ const elements = element.findElements(_apidomDatamodel.isPrimitiveElement);
94
+ if (elements.length > 0) {
95
+ elements[0].classes.push('result');
96
+ }
97
+
98
+ // Add collected annotations
99
+ for (const annotation of ctx.annotations) {
100
+ element.push(annotation);
101
+ }
102
+ ctx.annotations = [];
103
+ return element;
104
+ },
105
+ object(cursor, ctx) {
106
+ const info = getCursorInfo(cursor);
107
+ const element = new _apidomDatamodel.ObjectElement();
108
+ maybeAddSourceMap(info, element, ctx);
109
+
110
+ // Detect indent from first pair if style is enabled and not yet detected
111
+ if (ctx.style && ctx.indent === 0) {
112
+ if (cursor.gotoFirstChild()) {
113
+ do {
114
+ if (cursor.nodeType === 'pair') {
115
+ ctx.indent = detectIndent(info.startPosition.column, cursor.startPosition.column);
116
+ break;
117
+ }
118
+ } while (cursor.gotoNextSibling());
119
+ cursor.gotoParent();
120
+ }
121
+ }
122
+
123
+ // Transform children (pairs)
124
+ const children = transformChildren(cursor, transformerMap, ctx);
125
+ for (const child of children) {
126
+ element.push(child);
127
+ }
128
+ if (ctx.style) {
129
+ element.style = buildJsonStyle(ctx);
130
+ }
131
+ return element;
132
+ },
133
+ array(cursor, ctx) {
134
+ const info = getCursorInfo(cursor);
135
+ const element = new _apidomDatamodel.ArrayElement();
136
+ maybeAddSourceMap(info, element, ctx);
137
+
138
+ // Transform children
139
+ const children = transformChildren(cursor, transformerMap, ctx);
140
+ for (const child of children) {
141
+ element.push(child);
142
+ }
143
+ if (ctx.style) {
144
+ element.style = buildJsonStyle(ctx);
145
+ }
146
+ return element;
147
+ },
148
+ pair(cursor, ctx) {
149
+ const info = getCursorInfo(cursor);
150
+ let key = null;
151
+ let value = null;
152
+
153
+ // Find key and value by field name
154
+ if (cursor.gotoFirstChild()) {
155
+ do {
156
+ const fieldName = cursor.currentFieldName;
157
+ if (fieldName === 'key') {
158
+ key = transform(cursor, transformerMap, ctx);
159
+ } else if (fieldName === 'value') {
160
+ value = transform(cursor, transformerMap, ctx);
161
+ } else if (cursor.nodeType === 'ERROR') {
162
+ // Process error nodes
163
+ transform(cursor, transformerMap, ctx);
164
+ }
165
+ } while (cursor.gotoNextSibling());
166
+ cursor.gotoParent();
167
+ }
168
+ const element = new _apidomDatamodel.MemberElement(key ?? new _apidomDatamodel.StringElement(''), value ?? new _apidomDatamodel.StringElement(''));
169
+ maybeAddSourceMap(info, element, ctx);
170
+ return element;
171
+ },
172
+ string(cursor, ctx) {
173
+ const info = getCursorInfo(cursor);
174
+ let element;
175
+ try {
176
+ element = new _apidomDatamodel.StringElement(JSON.parse(info.text));
177
+ } catch (error) {
178
+ element = new _apidomDatamodel.StringElement(info.text);
179
+ if (error instanceof Error) {
180
+ element.meta.set('jsonParse', {
181
+ isError: true,
182
+ errorType: error.name,
183
+ errorMessage: error.message
184
+ });
185
+ }
186
+ }
187
+ if (ctx.style) {
188
+ element.style = buildJsonStyle(ctx);
189
+ }
190
+ maybeAddSourceMap(info, element, ctx);
191
+ return element;
192
+ },
193
+ number(cursor, ctx) {
194
+ const info = getCursorInfo(cursor);
195
+ const element = new _apidomDatamodel.NumberElement(Number(info.text));
196
+ if (ctx.style) {
197
+ element.style = buildJsonStyle(ctx, {
198
+ rawContent: info.text
199
+ });
200
+ }
201
+ maybeAddSourceMap(info, element, ctx);
202
+ return element;
203
+ },
204
+ null(cursor, ctx) {
205
+ const info = getCursorInfo(cursor);
206
+ const element = new _apidomDatamodel.NullElement();
207
+ if (ctx.style) {
208
+ element.style = buildJsonStyle(ctx);
209
+ }
210
+ maybeAddSourceMap(info, element, ctx);
211
+ return element;
212
+ },
213
+ true(cursor, ctx) {
214
+ const info = getCursorInfo(cursor);
215
+ const element = new _apidomDatamodel.BooleanElement(true);
216
+ if (ctx.style) {
217
+ element.style = buildJsonStyle(ctx);
218
+ }
219
+ maybeAddSourceMap(info, element, ctx);
220
+ return element;
221
+ },
222
+ false(cursor, ctx) {
223
+ const info = getCursorInfo(cursor);
224
+ const element = new _apidomDatamodel.BooleanElement(false);
225
+ if (ctx.style) {
226
+ element.style = buildJsonStyle(ctx);
227
+ }
228
+ maybeAddSourceMap(info, element, ctx);
229
+ return element;
230
+ },
231
+ ERROR(cursor, ctx) {
232
+ const info = getCursorInfo(cursor);
233
+ const isUnexpected = !info.hasError;
234
+ const message = isUnexpected ? `(Unexpected ${info.text})` : `(Error ${info.text})`;
235
+ const element = new _apidomDatamodel.AnnotationElement(message);
236
+ element.classes.push('error');
237
+ maybeAddSourceMap(info, element, ctx);
238
+ ctx.annotations.push(element);
239
+ return null;
240
+ }
241
+ });
242
+
243
+ // Create the transformers map with self-reference for recursion
244
+ const transformers = {};
245
+ Object.assign(transformers, createTransformers(transformers));
246
+
247
+ /**
248
+ * Syntactic analysis translates TreeSitter CST directly into ApiDOM.
249
+ *
250
+ * Direct transformation from TreeSitter CST using TreeSitter cursor.
251
+ * TreeSitter cursor is a stateful object that allows us to walk syntax tree
252
+ * containing large number of nodes with maximum efficiency.
253
+ *
254
+ * Single pass transformation from CST to ApiDOM.
255
+ * @public
256
+ */
257
+ const analyze = (cst, {
258
+ sourceMap = false,
259
+ style = false
260
+ } = {}) => {
261
+ const cursor = cst.walk();
262
+ const ctx = {
263
+ sourceMap,
264
+ style,
265
+ indent: 0,
266
+ annotations: []
267
+ };
268
+ const result = transform(cursor, transformers, ctx);
269
+
270
+ // Handle case where there's no document, only errors
271
+ if (result === null) {
272
+ const parseResult = new _apidomDatamodel.ParseResultElement();
273
+ for (const annotation of ctx.annotations) {
274
+ parseResult.push(annotation);
275
+ }
276
+ return parseResult;
277
+ }
278
+ return result;
279
+ };
280
+ var _default = exports.default = analyze;
@@ -0,0 +1,276 @@
1
+ import { BooleanElement, NullElement, NumberElement, ParseResultElement, MemberElement, ObjectElement, ArrayElement, StringElement, AnnotationElement, isPrimitiveElement } from '@speclynx/apidom-datamodel';
2
+ const getCursorInfo = cursor => ({
3
+ type: cursor.nodeType,
4
+ startPosition: cursor.startPosition,
5
+ endPosition: cursor.endPosition,
6
+ startIndex: cursor.startIndex,
7
+ endIndex: cursor.endIndex,
8
+ text: cursor.nodeText,
9
+ isNamed: cursor.nodeIsNamed,
10
+ isMissing: cursor.nodeIsMissing,
11
+ hasError: cursor.currentNode.hasError,
12
+ fieldName: cursor.currentFieldName
13
+ });
14
+ const maybeAddSourceMap = (info, element, ctx) => {
15
+ if (!ctx.sourceMap) {
16
+ return;
17
+ }
18
+ element.startLine = info.startPosition.row;
19
+ element.startCharacter = info.startPosition.column;
20
+ element.startOffset = info.startIndex;
21
+ element.endLine = info.endPosition.row;
22
+ element.endCharacter = info.endPosition.column;
23
+ element.endOffset = info.endIndex;
24
+ };
25
+
26
+ // build json style object for an element
27
+ const buildJsonStyle = (ctx, extras) => {
28
+ const jsonStyle = {
29
+ indent: ctx.indent
30
+ };
31
+ if (extras) Object.assign(jsonStyle, extras);
32
+ return {
33
+ json: jsonStyle
34
+ };
35
+ };
36
+
37
+ // detect indent from an object's first pair child position
38
+ // called during transformChildren when we encounter the first pair
39
+ const detectIndent = (objectColumn, firstPairColumn) => {
40
+ const diff = firstPairColumn - objectColumn;
41
+ return diff > 0 ? diff : 2;
42
+ };
43
+ const transform = (cursor, transformerMap, ctx) => {
44
+ const info = getCursorInfo(cursor);
45
+
46
+ // Handle missing anonymous literals
47
+ if (!info.isNamed && info.isMissing) {
48
+ const value = info.type || info.text;
49
+ const message = `(Missing ${value})`;
50
+ const element = new AnnotationElement(message);
51
+ element.classes.push('warning');
52
+ maybeAddSourceMap(info, element, ctx);
53
+ ctx.annotations.push(element);
54
+ return null;
55
+ }
56
+ const transformer = transformerMap[info.type];
57
+ if (!transformer) {
58
+ return null; // remove unrecognized nodes
59
+ }
60
+ return transformer(cursor, ctx);
61
+ };
62
+ const transformChildren = (cursor, transformerMap, ctx) => {
63
+ const results = [];
64
+ if (cursor.gotoFirstChild()) {
65
+ do {
66
+ const transformed = transform(cursor, transformerMap, ctx);
67
+ if (transformed !== null) {
68
+ results.push(transformed);
69
+ }
70
+ } while (cursor.gotoNextSibling());
71
+ cursor.gotoParent();
72
+ }
73
+ return results;
74
+ };
75
+ const createTransformers = transformerMap => ({
76
+ document(cursor, ctx) {
77
+ const info = getCursorInfo(cursor);
78
+ const element = new ParseResultElement();
79
+ maybeAddSourceMap(info, element, ctx);
80
+
81
+ // Transform children
82
+ const children = transformChildren(cursor, transformerMap, ctx);
83
+ for (const child of children) {
84
+ element.push(child);
85
+ }
86
+
87
+ // Mark first non-Annotation element as result
88
+ // @ts-ignore
89
+ const elements = element.findElements(isPrimitiveElement);
90
+ if (elements.length > 0) {
91
+ elements[0].classes.push('result');
92
+ }
93
+
94
+ // Add collected annotations
95
+ for (const annotation of ctx.annotations) {
96
+ element.push(annotation);
97
+ }
98
+ ctx.annotations = [];
99
+ return element;
100
+ },
101
+ object(cursor, ctx) {
102
+ const info = getCursorInfo(cursor);
103
+ const element = new ObjectElement();
104
+ maybeAddSourceMap(info, element, ctx);
105
+
106
+ // Detect indent from first pair if style is enabled and not yet detected
107
+ if (ctx.style && ctx.indent === 0) {
108
+ if (cursor.gotoFirstChild()) {
109
+ do {
110
+ if (cursor.nodeType === 'pair') {
111
+ ctx.indent = detectIndent(info.startPosition.column, cursor.startPosition.column);
112
+ break;
113
+ }
114
+ } while (cursor.gotoNextSibling());
115
+ cursor.gotoParent();
116
+ }
117
+ }
118
+
119
+ // Transform children (pairs)
120
+ const children = transformChildren(cursor, transformerMap, ctx);
121
+ for (const child of children) {
122
+ element.push(child);
123
+ }
124
+ if (ctx.style) {
125
+ element.style = buildJsonStyle(ctx);
126
+ }
127
+ return element;
128
+ },
129
+ array(cursor, ctx) {
130
+ const info = getCursorInfo(cursor);
131
+ const element = new ArrayElement();
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
+ if (ctx.style) {
140
+ element.style = buildJsonStyle(ctx);
141
+ }
142
+ return element;
143
+ },
144
+ pair(cursor, ctx) {
145
+ const info = getCursorInfo(cursor);
146
+ let key = null;
147
+ let value = null;
148
+
149
+ // Find key and value by field name
150
+ if (cursor.gotoFirstChild()) {
151
+ do {
152
+ const fieldName = cursor.currentFieldName;
153
+ if (fieldName === 'key') {
154
+ key = transform(cursor, transformerMap, ctx);
155
+ } else if (fieldName === 'value') {
156
+ value = transform(cursor, transformerMap, ctx);
157
+ } else if (cursor.nodeType === 'ERROR') {
158
+ // Process error nodes
159
+ transform(cursor, transformerMap, ctx);
160
+ }
161
+ } while (cursor.gotoNextSibling());
162
+ cursor.gotoParent();
163
+ }
164
+ const element = new MemberElement(key ?? new StringElement(''), value ?? new StringElement(''));
165
+ maybeAddSourceMap(info, element, ctx);
166
+ return element;
167
+ },
168
+ string(cursor, ctx) {
169
+ const info = getCursorInfo(cursor);
170
+ let element;
171
+ try {
172
+ element = new StringElement(JSON.parse(info.text));
173
+ } catch (error) {
174
+ element = new StringElement(info.text);
175
+ if (error instanceof Error) {
176
+ element.meta.set('jsonParse', {
177
+ isError: true,
178
+ errorType: error.name,
179
+ errorMessage: error.message
180
+ });
181
+ }
182
+ }
183
+ if (ctx.style) {
184
+ element.style = buildJsonStyle(ctx);
185
+ }
186
+ maybeAddSourceMap(info, element, ctx);
187
+ return element;
188
+ },
189
+ number(cursor, ctx) {
190
+ const info = getCursorInfo(cursor);
191
+ const element = new NumberElement(Number(info.text));
192
+ if (ctx.style) {
193
+ element.style = buildJsonStyle(ctx, {
194
+ rawContent: info.text
195
+ });
196
+ }
197
+ maybeAddSourceMap(info, element, ctx);
198
+ return element;
199
+ },
200
+ null(cursor, ctx) {
201
+ const info = getCursorInfo(cursor);
202
+ const element = new NullElement();
203
+ if (ctx.style) {
204
+ element.style = buildJsonStyle(ctx);
205
+ }
206
+ maybeAddSourceMap(info, element, ctx);
207
+ return element;
208
+ },
209
+ true(cursor, ctx) {
210
+ const info = getCursorInfo(cursor);
211
+ const element = new BooleanElement(true);
212
+ if (ctx.style) {
213
+ element.style = buildJsonStyle(ctx);
214
+ }
215
+ maybeAddSourceMap(info, element, ctx);
216
+ return element;
217
+ },
218
+ false(cursor, ctx) {
219
+ const info = getCursorInfo(cursor);
220
+ const element = new BooleanElement(false);
221
+ if (ctx.style) {
222
+ element.style = buildJsonStyle(ctx);
223
+ }
224
+ maybeAddSourceMap(info, element, ctx);
225
+ return element;
226
+ },
227
+ ERROR(cursor, ctx) {
228
+ const info = getCursorInfo(cursor);
229
+ const isUnexpected = !info.hasError;
230
+ const message = isUnexpected ? `(Unexpected ${info.text})` : `(Error ${info.text})`;
231
+ const element = new AnnotationElement(message);
232
+ element.classes.push('error');
233
+ maybeAddSourceMap(info, element, ctx);
234
+ ctx.annotations.push(element);
235
+ return null;
236
+ }
237
+ });
238
+
239
+ // Create the transformers map with self-reference for recursion
240
+ const transformers = {};
241
+ Object.assign(transformers, createTransformers(transformers));
242
+
243
+ /**
244
+ * Syntactic analysis translates TreeSitter CST directly into ApiDOM.
245
+ *
246
+ * Direct transformation from TreeSitter CST using TreeSitter cursor.
247
+ * TreeSitter cursor is a stateful object that allows us to walk syntax tree
248
+ * containing large number of nodes with maximum efficiency.
249
+ *
250
+ * Single pass transformation from CST to ApiDOM.
251
+ * @public
252
+ */
253
+ const analyze = (cst, {
254
+ sourceMap = false,
255
+ style = false
256
+ } = {}) => {
257
+ const cursor = cst.walk();
258
+ const ctx = {
259
+ sourceMap,
260
+ style,
261
+ indent: 0,
262
+ annotations: []
263
+ };
264
+ const result = transform(cursor, transformers, ctx);
265
+
266
+ // Handle case where there's no document, only errors
267
+ if (result === null) {
268
+ const parseResult = new ParseResultElement();
269
+ for (const annotation of ctx.annotations) {
270
+ parseResult.push(annotation);
271
+ }
272
+ return parseResult;
273
+ }
274
+ return result;
275
+ };
276
+ export default analyze;