parse-hcl 0.1.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.
- package/LICENSE +201 -0
- package/README.md +749 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +91 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +74 -0
- package/dist/parsers/genericParser.d.ts +167 -0
- package/dist/parsers/genericParser.js +268 -0
- package/dist/parsers/localsParser.d.ts +30 -0
- package/dist/parsers/localsParser.js +43 -0
- package/dist/parsers/outputParser.d.ts +25 -0
- package/dist/parsers/outputParser.js +44 -0
- package/dist/parsers/variableParser.d.ts +62 -0
- package/dist/parsers/variableParser.js +249 -0
- package/dist/services/artifactParsers.d.ts +12 -0
- package/dist/services/artifactParsers.js +157 -0
- package/dist/services/terraformJsonParser.d.ts +16 -0
- package/dist/services/terraformJsonParser.js +212 -0
- package/dist/services/terraformParser.d.ts +91 -0
- package/dist/services/terraformParser.js +191 -0
- package/dist/types/artifacts.d.ts +210 -0
- package/dist/types/artifacts.js +5 -0
- package/dist/types/blocks.d.ts +419 -0
- package/dist/types/blocks.js +28 -0
- package/dist/utils/common/errors.d.ts +46 -0
- package/dist/utils/common/errors.js +54 -0
- package/dist/utils/common/fs.d.ts +5 -0
- package/dist/utils/common/fs.js +48 -0
- package/dist/utils/common/logger.d.ts +5 -0
- package/dist/utils/common/logger.js +17 -0
- package/dist/utils/common/valueHelpers.d.ts +4 -0
- package/dist/utils/common/valueHelpers.js +23 -0
- package/dist/utils/graph/graphBuilder.d.ts +33 -0
- package/dist/utils/graph/graphBuilder.js +373 -0
- package/dist/utils/lexer/blockScanner.d.ts +36 -0
- package/dist/utils/lexer/blockScanner.js +143 -0
- package/dist/utils/lexer/hclLexer.d.ts +119 -0
- package/dist/utils/lexer/hclLexer.js +525 -0
- package/dist/utils/parser/bodyParser.d.ts +26 -0
- package/dist/utils/parser/bodyParser.js +81 -0
- package/dist/utils/parser/valueClassifier.d.ts +21 -0
- package/dist/utils/parser/valueClassifier.js +434 -0
- package/dist/utils/serialization/serializer.d.ts +9 -0
- package/dist/utils/serialization/serializer.js +63 -0
- package/dist/utils/serialization/yaml.d.ts +1 -0
- package/dist/utils/serialization/yaml.js +81 -0
- package/package.json +66 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared lexer utilities for HCL parsing.
|
|
4
|
+
* Provides common functions for tokenization and string handling.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.isQuote = isQuote;
|
|
8
|
+
exports.isEscaped = isEscaped;
|
|
9
|
+
exports.skipWhitespaceAndComments = skipWhitespaceAndComments;
|
|
10
|
+
exports.skipString = skipString;
|
|
11
|
+
exports.skipHeredoc = skipHeredoc;
|
|
12
|
+
exports.findMatchingBrace = findMatchingBrace;
|
|
13
|
+
exports.findMatchingBracket = findMatchingBracket;
|
|
14
|
+
exports.readIdentifier = readIdentifier;
|
|
15
|
+
exports.readDottedIdentifier = readDottedIdentifier;
|
|
16
|
+
exports.readQuotedString = readQuotedString;
|
|
17
|
+
exports.readValue = readValue;
|
|
18
|
+
exports.splitArrayElements = splitArrayElements;
|
|
19
|
+
exports.splitObjectEntries = splitObjectEntries;
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a character is a quote character (single or double).
|
|
22
|
+
* @param char - The character to check
|
|
23
|
+
* @returns True if the character is a quote
|
|
24
|
+
*/
|
|
25
|
+
function isQuote(char) {
|
|
26
|
+
return char === '"' || char === "'";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Checks if a character at a given position is escaped by counting preceding backslashes.
|
|
30
|
+
* Handles consecutive backslashes correctly (e.g., \\\\ is two escaped backslashes).
|
|
31
|
+
* @param text - The source text
|
|
32
|
+
* @param index - The index of the character to check
|
|
33
|
+
* @returns True if the character is escaped
|
|
34
|
+
*/
|
|
35
|
+
function isEscaped(text, index) {
|
|
36
|
+
let backslashCount = 0;
|
|
37
|
+
let pos = index - 1;
|
|
38
|
+
while (pos >= 0 && text[pos] === '\\') {
|
|
39
|
+
backslashCount++;
|
|
40
|
+
pos--;
|
|
41
|
+
}
|
|
42
|
+
return backslashCount % 2 === 1;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Skips whitespace and comments (line and block comments).
|
|
46
|
+
* Handles "//", block comments, and "#" style comments.
|
|
47
|
+
* @param text - The source text
|
|
48
|
+
* @param start - The starting index
|
|
49
|
+
* @returns The index of the next non-whitespace, non-comment character
|
|
50
|
+
*/
|
|
51
|
+
function skipWhitespaceAndComments(text, start) {
|
|
52
|
+
let index = start;
|
|
53
|
+
const length = text.length;
|
|
54
|
+
while (index < length) {
|
|
55
|
+
const char = text[index];
|
|
56
|
+
const next = text[index + 1];
|
|
57
|
+
// Skip whitespace
|
|
58
|
+
if (/\s/.test(char)) {
|
|
59
|
+
index++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Skip block comments /* ... */
|
|
63
|
+
if (char === '/' && next === '*') {
|
|
64
|
+
const end = text.indexOf('*/', index + 2);
|
|
65
|
+
index = end === -1 ? length : end + 2;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Skip line comments // ...
|
|
69
|
+
if (char === '/' && next === '/') {
|
|
70
|
+
const end = text.indexOf('\n', index + 2);
|
|
71
|
+
index = end === -1 ? length : end + 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Skip hash comments # ...
|
|
75
|
+
if (char === '#') {
|
|
76
|
+
const end = text.indexOf('\n', index + 1);
|
|
77
|
+
index = end === -1 ? length : end + 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
return index;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Skips a quoted string, handling escape sequences correctly.
|
|
86
|
+
* @param text - The source text
|
|
87
|
+
* @param start - The index of the opening quote
|
|
88
|
+
* @returns The index after the closing quote
|
|
89
|
+
*/
|
|
90
|
+
function skipString(text, start) {
|
|
91
|
+
const quote = text[start];
|
|
92
|
+
let index = start + 1;
|
|
93
|
+
const length = text.length;
|
|
94
|
+
while (index < length) {
|
|
95
|
+
const char = text[index];
|
|
96
|
+
if (char === quote && !isEscaped(text, index)) {
|
|
97
|
+
return index + 1;
|
|
98
|
+
}
|
|
99
|
+
index++;
|
|
100
|
+
}
|
|
101
|
+
return length;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Skips a heredoc string (<<EOF or <<-EOF style).
|
|
105
|
+
* @param text - The source text
|
|
106
|
+
* @param start - The index of the first '<'
|
|
107
|
+
* @returns The index after the heredoc terminator
|
|
108
|
+
*/
|
|
109
|
+
function skipHeredoc(text, start) {
|
|
110
|
+
const markerMatch = text.slice(start).match(/^<<-?\s*"?([A-Za-z0-9_]+)"?/);
|
|
111
|
+
if (!markerMatch) {
|
|
112
|
+
return start + 2;
|
|
113
|
+
}
|
|
114
|
+
const marker = markerMatch[1];
|
|
115
|
+
const afterMarker = start + markerMatch[0].length;
|
|
116
|
+
const terminatorIndex = text.indexOf(`\n${marker}`, afterMarker);
|
|
117
|
+
if (terminatorIndex === -1) {
|
|
118
|
+
return text.length;
|
|
119
|
+
}
|
|
120
|
+
const endOfTerminator = text.indexOf('\n', terminatorIndex + marker.length + 1);
|
|
121
|
+
return endOfTerminator === -1 ? text.length : endOfTerminator + 1;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Finds the matching closing brace for an opening brace.
|
|
125
|
+
* Handles nested braces, strings, comments, and heredocs.
|
|
126
|
+
* @param content - The source text
|
|
127
|
+
* @param startIndex - The index of the opening brace
|
|
128
|
+
* @returns The index of the matching closing brace, or -1 if not found
|
|
129
|
+
*/
|
|
130
|
+
function findMatchingBrace(content, startIndex) {
|
|
131
|
+
let depth = 0;
|
|
132
|
+
let index = startIndex;
|
|
133
|
+
const length = content.length;
|
|
134
|
+
while (index < length) {
|
|
135
|
+
const char = content[index];
|
|
136
|
+
const next = content[index + 1];
|
|
137
|
+
// Skip quoted strings
|
|
138
|
+
if (isQuote(char)) {
|
|
139
|
+
index = skipString(content, index);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Skip block comments
|
|
143
|
+
if (char === '/' && next === '*') {
|
|
144
|
+
const end = content.indexOf('*/', index + 2);
|
|
145
|
+
index = end === -1 ? length : end + 2;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Skip line comments
|
|
149
|
+
if (char === '/' && next === '/') {
|
|
150
|
+
const end = content.indexOf('\n', index + 2);
|
|
151
|
+
index = end === -1 ? length : end + 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// Skip hash comments
|
|
155
|
+
if (char === '#') {
|
|
156
|
+
const end = content.indexOf('\n', index + 1);
|
|
157
|
+
index = end === -1 ? length : end + 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// Skip heredocs
|
|
161
|
+
if (char === '<' && next === '<') {
|
|
162
|
+
index = skipHeredoc(content, index);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
// Track brace depth
|
|
166
|
+
if (char === '{') {
|
|
167
|
+
depth++;
|
|
168
|
+
}
|
|
169
|
+
else if (char === '}') {
|
|
170
|
+
depth--;
|
|
171
|
+
if (depth === 0) {
|
|
172
|
+
return index;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
index++;
|
|
176
|
+
}
|
|
177
|
+
return -1;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Finds the matching closing bracket for an opening bracket.
|
|
181
|
+
* Handles nested brackets of all types [], {}, ().
|
|
182
|
+
* @param content - The source text
|
|
183
|
+
* @param startIndex - The index of the opening bracket
|
|
184
|
+
* @param openChar - The opening bracket character
|
|
185
|
+
* @param closeChar - The closing bracket character
|
|
186
|
+
* @returns The index of the matching closing bracket, or -1 if not found
|
|
187
|
+
*/
|
|
188
|
+
function findMatchingBracket(content, startIndex, openChar, closeChar) {
|
|
189
|
+
let depth = 0;
|
|
190
|
+
let index = startIndex;
|
|
191
|
+
const length = content.length;
|
|
192
|
+
while (index < length) {
|
|
193
|
+
const char = content[index];
|
|
194
|
+
const next = content[index + 1];
|
|
195
|
+
// Skip quoted strings
|
|
196
|
+
if (isQuote(char)) {
|
|
197
|
+
index = skipString(content, index);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
// Skip block comments
|
|
201
|
+
if (char === '/' && next === '*') {
|
|
202
|
+
const end = content.indexOf('*/', index + 2);
|
|
203
|
+
index = end === -1 ? length : end + 2;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
// Skip line comments
|
|
207
|
+
if (char === '/' && next === '/') {
|
|
208
|
+
const end = content.indexOf('\n', index + 2);
|
|
209
|
+
index = end === -1 ? length : end + 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// Skip hash comments
|
|
213
|
+
if (char === '#') {
|
|
214
|
+
const end = content.indexOf('\n', index + 1);
|
|
215
|
+
index = end === -1 ? length : end + 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// Track bracket depth
|
|
219
|
+
if (char === openChar) {
|
|
220
|
+
depth++;
|
|
221
|
+
}
|
|
222
|
+
else if (char === closeChar) {
|
|
223
|
+
depth--;
|
|
224
|
+
if (depth === 0) {
|
|
225
|
+
return index;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
index++;
|
|
229
|
+
}
|
|
230
|
+
return -1;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Reads an identifier from the source text.
|
|
234
|
+
* Identifiers start with a letter or underscore, followed by letters, digits, underscores, or hyphens.
|
|
235
|
+
* @param text - The source text
|
|
236
|
+
* @param start - The starting index
|
|
237
|
+
* @returns The identifier string, or empty string if no identifier found
|
|
238
|
+
*/
|
|
239
|
+
function readIdentifier(text, start) {
|
|
240
|
+
const match = text.slice(start).match(/^[A-Za-z_][\w-]*/);
|
|
241
|
+
return match ? match[0] : '';
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Reads an identifier that may contain dots (for attribute access).
|
|
245
|
+
* @param text - The source text
|
|
246
|
+
* @param start - The starting index
|
|
247
|
+
* @returns The identifier string, or empty string if no identifier found
|
|
248
|
+
*/
|
|
249
|
+
function readDottedIdentifier(text, start) {
|
|
250
|
+
const match = text.slice(start).match(/^[\w.-]+/);
|
|
251
|
+
return match ? match[0] : '';
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Reads a quoted string and returns its unescaped content.
|
|
255
|
+
* @param text - The source text
|
|
256
|
+
* @param start - The index of the opening quote
|
|
257
|
+
* @returns The unquoted string and the index after the closing quote
|
|
258
|
+
*/
|
|
259
|
+
function readQuotedString(text, start) {
|
|
260
|
+
const quote = text[start];
|
|
261
|
+
let index = start + 1;
|
|
262
|
+
let value = '';
|
|
263
|
+
const length = text.length;
|
|
264
|
+
while (index < length) {
|
|
265
|
+
const char = text[index];
|
|
266
|
+
if (char === quote && !isEscaped(text, index)) {
|
|
267
|
+
return { text: value, end: index + 1 };
|
|
268
|
+
}
|
|
269
|
+
// Handle escape sequences
|
|
270
|
+
if (char === '\\' && index + 1 < length) {
|
|
271
|
+
const nextChar = text[index + 1];
|
|
272
|
+
if (nextChar === quote || nextChar === '\\' || nextChar === 'n' || nextChar === 't' || nextChar === 'r') {
|
|
273
|
+
switch (nextChar) {
|
|
274
|
+
case 'n':
|
|
275
|
+
value += '\n';
|
|
276
|
+
break;
|
|
277
|
+
case 't':
|
|
278
|
+
value += '\t';
|
|
279
|
+
break;
|
|
280
|
+
case 'r':
|
|
281
|
+
value += '\r';
|
|
282
|
+
break;
|
|
283
|
+
default:
|
|
284
|
+
value += nextChar;
|
|
285
|
+
}
|
|
286
|
+
index += 2;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
value += char;
|
|
291
|
+
index++;
|
|
292
|
+
}
|
|
293
|
+
return { text: value, end: length };
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Reads a value from HCL source (handles multiline values in brackets).
|
|
297
|
+
* @param text - The source text
|
|
298
|
+
* @param start - The starting index (after the '=' sign)
|
|
299
|
+
* @returns The raw value text and the index after the value
|
|
300
|
+
*/
|
|
301
|
+
function readValue(text, start) {
|
|
302
|
+
let index = skipWhitespaceAndComments(text, start);
|
|
303
|
+
const valueStart = index;
|
|
304
|
+
const length = text.length;
|
|
305
|
+
// Handle heredocs
|
|
306
|
+
if (text.slice(index, index + 2) === '<<') {
|
|
307
|
+
const newlineIndex = text.indexOf('\n', index);
|
|
308
|
+
const firstLine = newlineIndex === -1 ? text.slice(index) : text.slice(index, newlineIndex);
|
|
309
|
+
const markerMatch = firstLine.match(/^<<-?\s*"?([A-Za-z0-9_]+)"?/);
|
|
310
|
+
if (markerMatch) {
|
|
311
|
+
const marker = markerMatch[1];
|
|
312
|
+
const terminatorIndex = text.indexOf(`\n${marker}`, newlineIndex);
|
|
313
|
+
if (terminatorIndex !== -1) {
|
|
314
|
+
const endOfTerminator = text.indexOf('\n', terminatorIndex + marker.length + 1);
|
|
315
|
+
const endIndex = endOfTerminator === -1 ? length : endOfTerminator;
|
|
316
|
+
return { raw: text.slice(valueStart, endIndex).trim(), end: endIndex };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Track bracket depth for multiline values
|
|
321
|
+
let depth = 0;
|
|
322
|
+
let inString = false;
|
|
323
|
+
let stringChar = null;
|
|
324
|
+
while (index < length) {
|
|
325
|
+
const char = text[index];
|
|
326
|
+
const next = text[index + 1];
|
|
327
|
+
if (!inString) {
|
|
328
|
+
// Enter string
|
|
329
|
+
if (isQuote(char)) {
|
|
330
|
+
inString = true;
|
|
331
|
+
stringChar = char;
|
|
332
|
+
index++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// Skip block comments
|
|
336
|
+
if (char === '/' && next === '*') {
|
|
337
|
+
const end = text.indexOf('*/', index + 2);
|
|
338
|
+
index = end === -1 ? length : end + 2;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
// Skip line comments
|
|
342
|
+
if (char === '/' && next === '/') {
|
|
343
|
+
const end = text.indexOf('\n', index + 2);
|
|
344
|
+
index = end === -1 ? length : end + 1;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
// Track brackets
|
|
348
|
+
if (char === '{' || char === '[' || char === '(') {
|
|
349
|
+
depth++;
|
|
350
|
+
}
|
|
351
|
+
else if (char === '}' || char === ']' || char === ')') {
|
|
352
|
+
depth = Math.max(depth - 1, 0);
|
|
353
|
+
}
|
|
354
|
+
// End of value (newline at depth 0)
|
|
355
|
+
if ((char === '\n' || char === '\r') && depth === 0) {
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// Exit string
|
|
361
|
+
if (char === stringChar && !isEscaped(text, index)) {
|
|
362
|
+
inString = false;
|
|
363
|
+
stringChar = null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
index++;
|
|
367
|
+
}
|
|
368
|
+
return { raw: text.slice(valueStart, index).trim(), end: index };
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Splits an array literal into its elements.
|
|
372
|
+
* Handles nested arrays, objects, and strings correctly.
|
|
373
|
+
* @param raw - The raw array string including brackets
|
|
374
|
+
* @returns Array of raw element strings
|
|
375
|
+
*/
|
|
376
|
+
function splitArrayElements(raw) {
|
|
377
|
+
const inner = raw.slice(1, -1).trim();
|
|
378
|
+
if (!inner) {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
const elements = [];
|
|
382
|
+
let current = '';
|
|
383
|
+
let depth = 0;
|
|
384
|
+
let inString = false;
|
|
385
|
+
let stringChar = null;
|
|
386
|
+
for (let i = 0; i < inner.length; i++) {
|
|
387
|
+
const char = inner[i];
|
|
388
|
+
if (!inString) {
|
|
389
|
+
if (isQuote(char)) {
|
|
390
|
+
inString = true;
|
|
391
|
+
stringChar = char;
|
|
392
|
+
current += char;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (char === '{' || char === '[' || char === '(') {
|
|
396
|
+
depth++;
|
|
397
|
+
current += char;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (char === '}' || char === ']' || char === ')') {
|
|
401
|
+
depth--;
|
|
402
|
+
current += char;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (char === ',' && depth === 0) {
|
|
406
|
+
const trimmed = current.trim();
|
|
407
|
+
if (trimmed) {
|
|
408
|
+
elements.push(trimmed);
|
|
409
|
+
}
|
|
410
|
+
current = '';
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
current += char;
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
current += char;
|
|
417
|
+
if (char === stringChar && !isEscaped(inner, i)) {
|
|
418
|
+
inString = false;
|
|
419
|
+
stringChar = null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const trimmed = current.trim();
|
|
424
|
+
if (trimmed) {
|
|
425
|
+
elements.push(trimmed);
|
|
426
|
+
}
|
|
427
|
+
return elements;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Splits an object literal into key-value pairs.
|
|
431
|
+
* Handles nested objects, arrays, and strings correctly.
|
|
432
|
+
* @param raw - The raw object string including braces
|
|
433
|
+
* @returns Array of [key, value] tuples
|
|
434
|
+
*/
|
|
435
|
+
function splitObjectEntries(raw) {
|
|
436
|
+
const inner = raw.slice(1, -1).trim();
|
|
437
|
+
if (!inner) {
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
const entries = [];
|
|
441
|
+
let index = 0;
|
|
442
|
+
const length = inner.length;
|
|
443
|
+
while (index < length) {
|
|
444
|
+
index = skipWhitespaceAndComments(inner, index);
|
|
445
|
+
if (index >= length)
|
|
446
|
+
break;
|
|
447
|
+
// Read key (can be identifier or quoted string)
|
|
448
|
+
let key;
|
|
449
|
+
if (isQuote(inner[index])) {
|
|
450
|
+
const result = readQuotedString(inner, index);
|
|
451
|
+
key = result.text;
|
|
452
|
+
index = result.end;
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
key = readDottedIdentifier(inner, index);
|
|
456
|
+
index += key.length;
|
|
457
|
+
}
|
|
458
|
+
if (!key) {
|
|
459
|
+
index++;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
index = skipWhitespaceAndComments(inner, index);
|
|
463
|
+
// Expect = or :
|
|
464
|
+
if (inner[index] === '=' || inner[index] === ':') {
|
|
465
|
+
index++;
|
|
466
|
+
}
|
|
467
|
+
// Read value
|
|
468
|
+
const valueResult = readObjectValue(inner, index);
|
|
469
|
+
entries.push([key, valueResult.raw]);
|
|
470
|
+
index = valueResult.end;
|
|
471
|
+
// Skip comma if present
|
|
472
|
+
index = skipWhitespaceAndComments(inner, index);
|
|
473
|
+
if (inner[index] === ',') {
|
|
474
|
+
index++;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return entries;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Reads a value within an object (stops at comma or closing brace).
|
|
481
|
+
*/
|
|
482
|
+
function readObjectValue(text, start) {
|
|
483
|
+
let index = skipWhitespaceAndComments(text, start);
|
|
484
|
+
const valueStart = index;
|
|
485
|
+
const length = text.length;
|
|
486
|
+
let depth = 0;
|
|
487
|
+
let inString = false;
|
|
488
|
+
let stringChar = null;
|
|
489
|
+
while (index < length) {
|
|
490
|
+
const char = text[index];
|
|
491
|
+
if (!inString) {
|
|
492
|
+
if (isQuote(char)) {
|
|
493
|
+
inString = true;
|
|
494
|
+
stringChar = char;
|
|
495
|
+
index++;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (char === '{' || char === '[' || char === '(') {
|
|
499
|
+
depth++;
|
|
500
|
+
index++;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (char === '}' || char === ']' || char === ')') {
|
|
504
|
+
if (depth === 0) {
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
depth--;
|
|
508
|
+
index++;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if ((char === ',' || char === '\n') && depth === 0) {
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
index++;
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
if (char === stringChar && !isEscaped(text, index)) {
|
|
518
|
+
inString = false;
|
|
519
|
+
stringChar = null;
|
|
520
|
+
}
|
|
521
|
+
index++;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return { raw: text.slice(valueStart, index).trim(), end: index };
|
|
525
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for HCL block bodies.
|
|
3
|
+
* Extracts attributes (key = value pairs) and nested blocks from block content.
|
|
4
|
+
*/
|
|
5
|
+
import { ParsedBody } from '../../types/blocks';
|
|
6
|
+
/**
|
|
7
|
+
* Parses an HCL block body into structured attributes and nested blocks.
|
|
8
|
+
*
|
|
9
|
+
* @param body - The raw block body content (without outer braces)
|
|
10
|
+
* @returns ParsedBody containing attributes and nested blocks
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const body = `
|
|
15
|
+
* name = "example"
|
|
16
|
+
* count = 5
|
|
17
|
+
* tags {
|
|
18
|
+
* env = "prod"
|
|
19
|
+
* }
|
|
20
|
+
* `;
|
|
21
|
+
* const parsed = parseBlockBody(body);
|
|
22
|
+
* // parsed.attributes: { name: Value, count: Value }
|
|
23
|
+
* // parsed.blocks: [{ type: 'tags', ... }]
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseBlockBody(body: string): ParsedBody;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Parser for HCL block bodies.
|
|
4
|
+
* Extracts attributes (key = value pairs) and nested blocks from block content.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.parseBlockBody = parseBlockBody;
|
|
8
|
+
const valueClassifier_1 = require("./valueClassifier");
|
|
9
|
+
const hclLexer_1 = require("../lexer/hclLexer");
|
|
10
|
+
/**
|
|
11
|
+
* Parses an HCL block body into structured attributes and nested blocks.
|
|
12
|
+
*
|
|
13
|
+
* @param body - The raw block body content (without outer braces)
|
|
14
|
+
* @returns ParsedBody containing attributes and nested blocks
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const body = `
|
|
19
|
+
* name = "example"
|
|
20
|
+
* count = 5
|
|
21
|
+
* tags {
|
|
22
|
+
* env = "prod"
|
|
23
|
+
* }
|
|
24
|
+
* `;
|
|
25
|
+
* const parsed = parseBlockBody(body);
|
|
26
|
+
* // parsed.attributes: { name: Value, count: Value }
|
|
27
|
+
* // parsed.blocks: [{ type: 'tags', ... }]
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function parseBlockBody(body) {
|
|
31
|
+
const attributes = {};
|
|
32
|
+
const blocks = [];
|
|
33
|
+
let index = 0;
|
|
34
|
+
const length = body.length;
|
|
35
|
+
while (index < length) {
|
|
36
|
+
index = (0, hclLexer_1.skipWhitespaceAndComments)(body, index);
|
|
37
|
+
if (index >= length)
|
|
38
|
+
break;
|
|
39
|
+
const identifierStart = index;
|
|
40
|
+
const identifier = (0, hclLexer_1.readDottedIdentifier)(body, index);
|
|
41
|
+
if (!identifier) {
|
|
42
|
+
index++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
index += identifier.length;
|
|
46
|
+
index = (0, hclLexer_1.skipWhitespaceAndComments)(body, index);
|
|
47
|
+
// Check for attribute assignment (key = value)
|
|
48
|
+
if (body[index] === '=') {
|
|
49
|
+
const { raw, end } = (0, hclLexer_1.readValue)(body, index + 1);
|
|
50
|
+
attributes[identifier] = (0, valueClassifier_1.classifyValue)(raw);
|
|
51
|
+
index = end;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Check for nested block with labels
|
|
55
|
+
const labels = [];
|
|
56
|
+
while ((0, hclLexer_1.isQuote)(body[index])) {
|
|
57
|
+
const { text, end } = (0, hclLexer_1.readQuotedString)(body, index);
|
|
58
|
+
labels.push(text);
|
|
59
|
+
index = (0, hclLexer_1.skipWhitespaceAndComments)(body, end);
|
|
60
|
+
}
|
|
61
|
+
// Check for nested block opening brace
|
|
62
|
+
if (body[index] === '{') {
|
|
63
|
+
const closeIndex = (0, hclLexer_1.findMatchingBrace)(body, index);
|
|
64
|
+
const innerBody = body.slice(index + 1, closeIndex);
|
|
65
|
+
const parsed = parseBlockBody(innerBody);
|
|
66
|
+
const raw = body.slice(identifierStart, closeIndex + 1);
|
|
67
|
+
blocks.push({
|
|
68
|
+
type: identifier,
|
|
69
|
+
labels,
|
|
70
|
+
attributes: parsed.attributes,
|
|
71
|
+
blocks: parsed.blocks,
|
|
72
|
+
raw
|
|
73
|
+
});
|
|
74
|
+
index = closeIndex + 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Not recognized, skip character and continue
|
|
78
|
+
index++;
|
|
79
|
+
}
|
|
80
|
+
return { attributes, blocks };
|
|
81
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value classifier for HCL expressions.
|
|
3
|
+
* Classifies raw value strings into typed Value structures and extracts references.
|
|
4
|
+
*/
|
|
5
|
+
import { Value } from '../../types/blocks';
|
|
6
|
+
/**
|
|
7
|
+
* Classifies a raw HCL value string into a typed Value structure.
|
|
8
|
+
* Supports literals, quoted strings, arrays, objects, and expressions.
|
|
9
|
+
*
|
|
10
|
+
* @param raw - The raw value string to classify
|
|
11
|
+
* @returns The classified Value with type information and extracted references
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* classifyValue('true') // LiteralValue { type: 'literal', value: true }
|
|
16
|
+
* classifyValue('"hello"') // LiteralValue { type: 'literal', value: 'hello' }
|
|
17
|
+
* classifyValue('var.region') // ExpressionValue with variable reference
|
|
18
|
+
* classifyValue('[1, 2, 3]') // ArrayValue with parsed elements
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function classifyValue(raw: string): Value;
|