@graffiticode/parser 0.2.0 → 0.4.0

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,341 @@
1
+ import { parser } from "./parser.js";
2
+ import { unparse } from "./unparse.js";
3
+
4
+ describe("unparse with L0166 lexicon", () => {
5
+ // L0166 lexicon for spreadsheet operations (from l0166/packages/api/src/lexicon.js)
6
+ const l0166Lexicon = {
7
+ "title": {
8
+ "tk": 1,
9
+ "name": "TITLE",
10
+ "cls": "function",
11
+ "length": 2,
12
+ "arity": 2,
13
+ },
14
+ "instructions": {
15
+ "tk": 1,
16
+ "name": "INSTRUCTIONS",
17
+ "cls": "function",
18
+ "length": 2,
19
+ "arity": 2,
20
+ },
21
+ "params": {
22
+ "tk": 1,
23
+ "name": "PARAMS",
24
+ "cls": "function",
25
+ "length": 2,
26
+ "arity": 2,
27
+ },
28
+ "cell": {
29
+ "tk": 1,
30
+ "name": "CELL",
31
+ "cls": "function",
32
+ "length": 2,
33
+ "arity": 2,
34
+ },
35
+ "text": {
36
+ "tk": 1,
37
+ "name": "TEXT",
38
+ "cls": "function",
39
+ "length": 2,
40
+ "arity": 2,
41
+ },
42
+ "assess": {
43
+ "tk": 1,
44
+ "name": "ASSESS",
45
+ "cls": "function",
46
+ "length": 2,
47
+ "arity": 2,
48
+ },
49
+ "method": {
50
+ "tk": 1,
51
+ "name": "METHOD",
52
+ "cls": "function",
53
+ "length": 1,
54
+ "arity": 1,
55
+ },
56
+ "expected": {
57
+ "tk": 1,
58
+ "name": "EXPECTED",
59
+ "cls": "function",
60
+ "length": 1,
61
+ "arity": 1,
62
+ },
63
+ "width": {
64
+ "tk": 1,
65
+ "name": "WIDTH",
66
+ "cls": "function",
67
+ "length": 2,
68
+ "arity": 2,
69
+ },
70
+ "align": {
71
+ "tk": 1,
72
+ "name": "ALIGN",
73
+ "cls": "function",
74
+ "length": 2,
75
+ "arity": 2,
76
+ },
77
+ "background-color": {
78
+ "tk": 1,
79
+ "name": "BACKGROUND_COLOR",
80
+ "cls": "function",
81
+ "length": 2,
82
+ "arity": 2,
83
+ },
84
+ "font-weight": {
85
+ "tk": 1,
86
+ "name": "FONT_WEIGHT",
87
+ "cls": "function",
88
+ "length": 2,
89
+ "arity": 2,
90
+ },
91
+ "format": {
92
+ "tk": 1,
93
+ "name": "FORMAT",
94
+ "cls": "function",
95
+ "length": 2,
96
+ "arity": 2,
97
+ },
98
+ "protected": {
99
+ "tk": 1,
100
+ "name": "PROTECTED",
101
+ "cls": "function",
102
+ "length": 2,
103
+ "arity": 2,
104
+ },
105
+ "cells": {
106
+ "tk": 1,
107
+ "name": "CELLS",
108
+ "cls": "function",
109
+ "length": 2,
110
+ "arity": 2,
111
+ },
112
+ "rows": {
113
+ "tk": 1,
114
+ "name": "ROWS",
115
+ "cls": "function",
116
+ "length": 2,
117
+ "arity": 2,
118
+ },
119
+ "column": {
120
+ "tk": 1,
121
+ "name": "COLUMN",
122
+ "cls": "function",
123
+ "length": 2,
124
+ "arity": 2,
125
+ },
126
+ "columns": {
127
+ "tk": 1,
128
+ "name": "COLUMNS",
129
+ "cls": "function",
130
+ "length": 2,
131
+ "arity": 2,
132
+ }
133
+ };
134
+
135
+ it("should unparse L0166 spreadsheet code", async () => {
136
+ const source = `columns [
137
+ column A width 100 align "center" protected true {}
138
+ ]
139
+ rows [
140
+ row 1 background-color "#eee" protected true {}
141
+ ]
142
+ cells [
143
+ cell A1 text "A1" protected true {}
144
+ ]
145
+ {
146
+ v: "0.0.1"
147
+ }..`;
148
+
149
+ // Note: The parser may transform this code, so we test that unparse
150
+ // produces valid code that can be parsed again
151
+ // Pass the lexicon directly to avoid fetching
152
+
153
+ // For complex L0166 code, we'll just parse with language 0
154
+ // since the specific L0166 syntax may require special handling
155
+ const ast = await parser.parse(0, source);
156
+
157
+ // Log the AST pool
158
+ console.log("AST Pool:", JSON.stringify(ast, null, 2));
159
+
160
+ const unparsed = unparse(ast, l0166Lexicon);
161
+
162
+ // The unparsed code should be valid and parseable
163
+ expect(unparsed).toBeDefined();
164
+ expect(unparsed.endsWith("..")).toBe(true);
165
+
166
+ // Check that key elements appear in the output
167
+ // (the exact format may differ due to how the parser handles the syntax)
168
+ console.log("Original source:", source);
169
+ console.log("Unparsed:", unparsed);
170
+ });
171
+
172
+ it("should handle individual L0166 constructs", async () => {
173
+ const tests = [
174
+ {
175
+ source: '{v: "0.0.1"}..',
176
+ description: "version record"
177
+ },
178
+ {
179
+ source: '[]..',
180
+ description: "empty list"
181
+ },
182
+ {
183
+ source: '{}..',
184
+ description: "empty record"
185
+ },
186
+ {
187
+ source: '"A1"..',
188
+ description: "string literal"
189
+ },
190
+ {
191
+ source: '100..',
192
+ description: "number literal"
193
+ },
194
+ {
195
+ source: 'true..',
196
+ description: "boolean literal"
197
+ }
198
+ ];
199
+
200
+ for (const { source, description } of tests) {
201
+ const ast = await parser.parse(166, source, l0166Lexicon);
202
+ const unparsed = unparse(ast, l0166Lexicon);
203
+
204
+ // Check that unparse produces output
205
+ expect(unparsed).toBeDefined();
206
+ expect(unparsed).not.toBe("");
207
+
208
+ // The output should end with ..
209
+ if (!unparsed.endsWith("..")) {
210
+ console.log(`${description}: "${source}" -> "${unparsed}"`);
211
+ }
212
+ expect(unparsed.endsWith("..")).toBe(true);
213
+ }
214
+ });
215
+
216
+ it("should preserve simple L0166 expressions", async () => {
217
+ // Test simpler L0166 expressions that should parse correctly
218
+ const tests = [
219
+ 'column A {}..',
220
+ 'row 1 {}..',
221
+ 'cell A1 {}..',
222
+ ];
223
+
224
+ for (const source of tests) {
225
+ const ast = await parser.parse(0, source);
226
+ const unparsed = unparse(ast, l0166Lexicon);
227
+
228
+ // Should produce valid output
229
+ expect(unparsed).toBeDefined();
230
+ expect(unparsed.endsWith("..")).toBe(true);
231
+
232
+ console.log(`Simple L0166: "${source}" -> "${unparsed}"`);
233
+ }
234
+ });
235
+
236
+ it("should handle complex L0166 budget assessment code", async () => {
237
+ const source = `title "Home Budget Assessment"
238
+ instructions \`
239
+ - Calculate your monthly budget based on income percentages
240
+ - Fill in the empty cells with the correct formulas
241
+ - Ensure all expenses and savings are properly allocated
242
+ \`
243
+ columns [
244
+ column A width 150 align "left" {}
245
+ column B width 100 format "($#,##0)" {}
246
+ column C width 250 align "left" {}
247
+ ]
248
+ cells [
249
+ cell A1 text "CATEGORY" font-weight "bold" {}
250
+ cell B1 text "AMOUNT" font-weight "bold" {}
251
+ cell C1 text "DETAILS" font-weight "bold" {}
252
+
253
+ cell A2 text "Income" {}
254
+ cell B2 text "4000" {}
255
+ cell C2 text "Total monthly income" {}
256
+
257
+ cell A3 text "Rent" {}
258
+ cell B3
259
+ text "",
260
+ assess [
261
+ method "value"
262
+ expected "1400"
263
+ ] {}
264
+ cell C3 text "35% of your total income" {}
265
+
266
+ cell A4 text "Utilities" {}
267
+ cell B4 text "200" {}
268
+ cell C4 text "Fixed expense" {}
269
+
270
+ cell A5 text "Food" {}
271
+ cell B5
272
+ text "",
273
+ assess [
274
+ method "value"
275
+ expected "600"
276
+ ] {}
277
+ cell C5 text "15% of your total income" {}
278
+
279
+ cell A6 text "Transportation" {}
280
+ cell B6
281
+ text "",
282
+ assess [
283
+ method "value"
284
+ expected "400"
285
+ ] {}
286
+ cell C6 text "10% of your total income" {}
287
+
288
+ cell A7 text "Entertainment" {}
289
+ cell B7 text "150" {}
290
+ cell C7 text "Fixed expense" {}
291
+
292
+ cell A8 text "Savings" {}
293
+ cell B8
294
+ text "",
295
+ assess [
296
+ method "value"
297
+ expected "800"
298
+ ] {}
299
+ cell C8 text "20% of your total income" {}
300
+
301
+ cell A9 text "Miscellaneous" {}
302
+ cell B9
303
+ text "",
304
+ assess [
305
+ method "value"
306
+ expected "450"
307
+ ] {}
308
+ cell C9 text "Remaining income after all other expenses" {}
309
+ ]
310
+ {
311
+ v: "0.0.1"
312
+ }..`;
313
+
314
+ // Parse with L0166 lexicon
315
+ const ast = await parser.parse("0166", source, l0166Lexicon);
316
+
317
+ console.log("Complex L0166 AST nodes:", Object.keys(ast).length);
318
+
319
+ const unparsed = unparse(ast, l0166Lexicon);
320
+
321
+ // The unparsed code should be valid and parseable
322
+ expect(unparsed).toBeDefined();
323
+ expect(unparsed.endsWith("..")).toBe(true);
324
+
325
+ // Check that key elements appear in the output
326
+ expect(unparsed).toContain("title");
327
+ expect(unparsed).toContain("columns");
328
+ expect(unparsed).toContain("cells");
329
+ expect(unparsed).toContain("column A");
330
+ expect(unparsed).toContain("column B");
331
+ expect(unparsed).toContain("column C");
332
+
333
+ // Log a portion of the output to see the pretty printing
334
+ const lines = unparsed.split("\n");
335
+ console.log("First 20 lines of unparsed output:");
336
+ console.log(lines.slice(0, 20).join("\n"));
337
+ console.log("...");
338
+ console.log("Last 10 lines of unparsed output:");
339
+ console.log(lines.slice(-10).join("\n"));
340
+ });
341
+ });
package/src/unparse.js ADDED
@@ -0,0 +1,377 @@
1
+ // Pretty printer that converts an AST back to source code
2
+ import { lexicon as basisLexicon } from "@graffiticode/basis";
3
+
4
+ /**
5
+ * Unparse an AST node to source code
6
+ * @param {object} node - The AST node to unparse
7
+ * @param {object} lexicon - The lexicon containing operator and keyword definitions
8
+ * @param {number} indent - The current indentation level (default 0)
9
+ * @param {object} options - Options for unparsing (e.g., indentSize, compact)
10
+ * @returns {string} The unparsed source code
11
+ */
12
+ function unparseNode(node, lexicon, indent = 0, options = {}) {
13
+ // Default options
14
+ const opts = {
15
+ indentSize: 2,
16
+ compact: false,
17
+ ...options
18
+ };
19
+
20
+ if (!node) {
21
+ return "";
22
+ }
23
+
24
+ // Handle primitive values
25
+ if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
26
+ return String(node);
27
+ }
28
+
29
+ // Handle AST nodes
30
+ switch (node.tag) {
31
+ case "PROG":
32
+ // Program is a list of expressions ending with ".."
33
+ if (node.elts && node.elts.length > 0) {
34
+ const exprs = unparseNode(node.elts[0], lexicon, indent, opts);
35
+ return exprs + "..";
36
+ }
37
+ return "..";
38
+
39
+ case "EXPRS":
40
+ // Multiple expressions
41
+ if (!node.elts || node.elts.length === 0) {
42
+ return "";
43
+ }
44
+ // Check if this looks like a function application that wasn't folded
45
+ // e.g., sub followed by arguments as separate expressions
46
+ if (node.elts.length >= 3) {
47
+ const first = node.elts[0];
48
+ // Check if first element is an identifier that could be a function
49
+ if (first && first.tag && first.elts && first.elts.length === 0) {
50
+ // This might be a function name followed by arguments
51
+ const funcName = first.tag;
52
+ // Check if this matches a lexicon function
53
+ if (lexicon && lexicon[funcName]) {
54
+ const arity = lexicon[funcName].arity || 0;
55
+ if (arity > 0 && node.elts.length === arity + 1) {
56
+ // Treat this as a function application
57
+ const args = node.elts.slice(1).map(elt => unparseNode(elt, lexicon, indent, opts)).join(" ");
58
+ return `${funcName} ${args}`;
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ // For single expression, return as is
65
+ if (node.elts.length === 1) {
66
+ return unparseNode(node.elts[0], lexicon, indent, opts);
67
+ }
68
+
69
+ // For multiple expressions, put each on its own line
70
+ return node.elts.map(elt => unparseNode(elt, lexicon, indent, opts)).join("\n");
71
+
72
+ case "NUM":
73
+ return node.elts[0];
74
+
75
+ case "STR": {
76
+ // Escape quotes and backslashes in the string
77
+ const str = node.elts[0];
78
+ const escaped = str.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
79
+ return `'${escaped}'`;
80
+ }
81
+
82
+ case "BOOL":
83
+ return node.elts[0] ? "true" : "false";
84
+
85
+ case "NULL":
86
+ return "null";
87
+
88
+ case "IDENT":
89
+ return node.elts[0];
90
+
91
+ case "LIST": {
92
+ // Array literal [a, b, c]
93
+ if (!node.elts || node.elts.length === 0) {
94
+ return "[]";
95
+ }
96
+
97
+ if (opts.compact) {
98
+ // Compact mode: inline list
99
+ const items = node.elts.map(elt => unparseNode(elt, lexicon, indent, opts));
100
+ return "[" + items.join(", ") + "]";
101
+ } else {
102
+ // Pretty print with each element on a new line
103
+ const innerIndent = indent + opts.indentSize;
104
+ const indentStr = " ".repeat(innerIndent);
105
+ const items = node.elts.map(elt =>
106
+ indentStr + unparseNode(elt, lexicon, innerIndent, opts)
107
+ );
108
+ return "[\n" + items.join("\n") + "\n" + " ".repeat(indent) + "]";
109
+ }
110
+ }
111
+
112
+ case "RECORD": {
113
+ // Object literal {a: 1, b: 2}
114
+ if (!node.elts || node.elts.length === 0) {
115
+ return "{}";
116
+ }
117
+
118
+ if (opts.compact) {
119
+ // Compact mode: inline record
120
+ const bindings = node.elts.map(elt => unparseNode(elt, lexicon, indent, opts));
121
+ return "{" + bindings.join(", ") + "}";
122
+ } else {
123
+ // Pretty print with each binding on a new line
124
+ const innerIndent = indent + opts.indentSize;
125
+ const indentStr = " ".repeat(innerIndent);
126
+ const bindings = node.elts.map(elt =>
127
+ indentStr + unparseNode(elt, lexicon, innerIndent, opts)
128
+ );
129
+ return "{\n" + bindings.join("\n") + "\n" + " ".repeat(indent) + "}";
130
+ }
131
+ }
132
+
133
+ case "BINDING": {
134
+ // Key-value pair in a record
135
+ if (node.elts && node.elts.length >= 2) {
136
+ // If the key is a string node, unparse it without quotes for object keys
137
+ let key;
138
+ if (node.elts[0] && node.elts[0].tag === "STR") {
139
+ key = node.elts[0].elts[0]; // Get the raw string without quotes
140
+ } else {
141
+ key = unparseNode(node.elts[0], lexicon, indent);
142
+ }
143
+ const value = unparseNode(node.elts[1], lexicon, indent, opts);
144
+ return `${key}: ${value}`;
145
+ }
146
+ return "";
147
+ }
148
+
149
+ case "PAREN":
150
+ // Parenthesized expression
151
+ if (node.elts && node.elts.length > 0) {
152
+ return "(" + unparseNode(node.elts[0], lexicon, indent, opts) + ")";
153
+ }
154
+ return "()";
155
+
156
+ case "APPLY":
157
+ // Function application
158
+ if (node.elts && node.elts.length >= 2) {
159
+ const func = unparseNode(node.elts[0], lexicon, indent, opts);
160
+ const args = unparseNode(node.elts[1], lexicon, indent, opts);
161
+ return func + " " + args;
162
+ }
163
+ return "";
164
+
165
+ case "LAMBDA":
166
+ // Lambda function
167
+ if (node.elts && node.elts.length >= 3) {
168
+ const params = node.elts[1];
169
+ const body = node.elts[2];
170
+
171
+ // Extract parameter names
172
+ let paramStr = "";
173
+ if (params && params.elts) {
174
+ paramStr = params.elts.map(p => unparseNode(p, lexicon, indent, opts)).join(" ");
175
+ }
176
+
177
+ // Unparse body
178
+ const bodyStr = unparseNode(body, lexicon, indent, opts);
179
+
180
+ if (paramStr) {
181
+ return `\\${paramStr} . ${bodyStr}`;
182
+ } else {
183
+ return `\\. ${bodyStr}`;
184
+ }
185
+ }
186
+ return "";
187
+
188
+ case "LET":
189
+ // Let binding
190
+ if (node.elts && node.elts.length >= 2) {
191
+ const bindings = node.elts[0];
192
+ const body = node.elts[1];
193
+
194
+ let bindingStr = "";
195
+ if (bindings && bindings.elts) {
196
+ bindingStr = bindings.elts.map(b => {
197
+ if (b.elts && b.elts.length >= 2) {
198
+ const name = unparseNode(b.elts[0], lexicon, indent, opts);
199
+ const value = unparseNode(b.elts[1], lexicon, indent, opts);
200
+ return `${name} = ${value}`;
201
+ }
202
+ return "";
203
+ }).filter(s => s).join(", ");
204
+ }
205
+
206
+ const bodyStr = unparseNode(body, lexicon, indent, opts);
207
+ return `let ${bindingStr} in ${bodyStr}`;
208
+ }
209
+ return "";
210
+
211
+ case "IF":
212
+ // If-then-else
213
+ if (node.elts && node.elts.length >= 2) {
214
+ const cond = unparseNode(node.elts[0], lexicon, indent, opts);
215
+ const thenExpr = unparseNode(node.elts[1], lexicon, indent, opts);
216
+
217
+ if (node.elts.length >= 3) {
218
+ const elseExpr = unparseNode(node.elts[2], lexicon, indent, opts);
219
+ return `if ${cond} then ${thenExpr} else ${elseExpr}`;
220
+ } else {
221
+ return `if ${cond} then ${thenExpr}`;
222
+ }
223
+ }
224
+ return "";
225
+
226
+ case "CASE":
227
+ // Case expression
228
+ if (node.elts && node.elts.length > 0) {
229
+ const expr = unparseNode(node.elts[0], lexicon, indent, opts);
230
+ const cases = node.elts.slice(1).map(c => unparseNode(c, lexicon, indent, opts));
231
+ return `case ${expr} of ${cases.join(" | ")}`;
232
+ }
233
+ return "";
234
+
235
+ case "OF":
236
+ // Case branch
237
+ if (node.elts && node.elts.length >= 2) {
238
+ const pattern = unparseNode(node.elts[0], lexicon, indent, opts);
239
+ const expr = unparseNode(node.elts[1], lexicon, indent, opts);
240
+ return `${pattern} => ${expr}`;
241
+ }
242
+ return "";
243
+
244
+ // Unary operator - negative
245
+ case "NEG":
246
+ if (node.elts && node.elts.length >= 1) {
247
+ const expr = unparseNode(node.elts[0], lexicon, indent, opts);
248
+ return `-${expr}`;
249
+ }
250
+ return "";
251
+
252
+ case "ERROR":
253
+ // Error nodes - include as comments
254
+ if (node.elts && node.elts.length > 0) {
255
+ // The first element might be a node reference or a string
256
+ const firstElt = node.elts[0];
257
+ if (typeof firstElt === "object" && firstElt.elts) {
258
+ // It's a node, unparse it
259
+ return `/* ERROR: ${unparseNode(firstElt, lexicon, indent, opts)} */`;
260
+ }
261
+ return `/* ERROR: ${firstElt} */`;
262
+ }
263
+ return "/* ERROR */";
264
+
265
+ default: {
266
+ // Check if this is a lexicon-defined function
267
+ // First, find the source name for this tag in the lexicon
268
+ let sourceName = null;
269
+ if (lexicon) {
270
+ for (const [key, value] of Object.entries(lexicon)) {
271
+ if (value && value.name === node.tag) {
272
+ sourceName = key;
273
+ break;
274
+ }
275
+ }
276
+ }
277
+
278
+ if (sourceName) {
279
+ // This is a known lexicon function - unparse in prefix notation
280
+ if (node.elts && node.elts.length > 0) {
281
+ const args = node.elts.map(elt => unparseNode(elt, lexicon, indent, opts)).join(" ");
282
+ return `${sourceName} ${args}`;
283
+ }
284
+ return sourceName;
285
+ }
286
+
287
+ // Handle identifiers that aren't in the lexicon (like lowercase "sub")
288
+ if (node.elts && node.elts.length === 0) {
289
+ // This is likely an identifier
290
+ return node.tag;
291
+ }
292
+
293
+ // Fallback for unknown nodes
294
+ console.warn(`Unknown node tag: ${node.tag}`);
295
+ return `/* ${node.tag} */`;
296
+ }
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Unparse an AST pool (as returned by the parser) to source code
302
+ * @param {object} ast - The AST pool with a root property
303
+ * @param {object} dialectLexicon - The dialect-specific lexicon (optional)
304
+ * @param {object} options - Options for unparsing (e.g., indentSize, compact)
305
+ * @returns {string} The unparsed source code
306
+ */
307
+ export function unparse(ast, dialectLexicon = {}, options = {}) {
308
+ if (!ast || !ast.root) {
309
+ return "";
310
+ }
311
+
312
+ // Merge basis lexicon with dialect lexicon (dialect takes precedence)
313
+ const mergedLexicon = { ...basisLexicon, ...dialectLexicon };
314
+
315
+ // The AST is in pool format - reconstruct the tree from the root
316
+ const rootId = ast.root;
317
+ const rootNode = reconstructNode(ast, rootId);
318
+
319
+ return unparseNode(rootNode, mergedLexicon, 0, options);
320
+ }
321
+
322
+ /**
323
+ * Reconstruct a node from the AST pool format
324
+ * @param {object} pool - The AST pool
325
+ * @param {string|number} nodeId - The node ID to reconstruct
326
+ * @returns {object} The reconstructed node
327
+ */
328
+ function reconstructNode(pool, nodeId) {
329
+ if (!nodeId || nodeId === "0" || nodeId === 0) {
330
+ return null;
331
+ }
332
+
333
+ const node = pool[nodeId];
334
+ if (!node) {
335
+ return null;
336
+ }
337
+
338
+ // Create a new node with the same structure
339
+ const result = {
340
+ tag: node.tag,
341
+ elts: []
342
+ };
343
+
344
+ // Handle different node types
345
+ switch (node.tag) {
346
+ case "NUM":
347
+ case "STR":
348
+ case "IDENT":
349
+ case "BOOL":
350
+ // These nodes have primitive values in elts[0]
351
+ result.elts = [node.elts[0]];
352
+ break;
353
+
354
+ case "NULL":
355
+ // NULL nodes have no elements
356
+ result.elts = [];
357
+ break;
358
+
359
+ default:
360
+ // For all other nodes, recursively reconstruct child nodes
361
+ if (node.elts && Array.isArray(node.elts)) {
362
+ result.elts = node.elts.map(eltId => {
363
+ // Check if this is a node ID (number or string number)
364
+ if (typeof eltId === "number" || (typeof eltId === "string" && /^\d+$/.test(eltId))) {
365
+ // This is a reference to another node in the pool
366
+ return reconstructNode(pool, eltId);
367
+ } else {
368
+ // This is a primitive value
369
+ return eltId;
370
+ }
371
+ });
372
+ }
373
+ break;
374
+ }
375
+
376
+ return result;
377
+ }