@shapeshift-labs/frontier-lang-parser 0.3.41 → 0.3.42
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/dist/action-body.js +119 -6
- package/dist/action-syntax-children.js +195 -0
- package/dist/source-syntax-children.js +1 -115
- package/package.json +1 -1
package/dist/action-body.js
CHANGED
|
@@ -2,13 +2,9 @@ import { readFrontierNestedBlocks } from './source-syntax-report.js';
|
|
|
2
2
|
|
|
3
3
|
export function readActionBodyRecords(body) {
|
|
4
4
|
const records = [];
|
|
5
|
+
const state = { index: 0 };
|
|
5
6
|
for (const bodyBlock of readFrontierNestedBlocks('body', body)) {
|
|
6
|
-
|
|
7
|
-
const line = rawLine.trim();
|
|
8
|
-
if (!line || line.startsWith('#')) continue;
|
|
9
|
-
const record = parseActionBodyLine(line, records.length);
|
|
10
|
-
if (record) records.push(record);
|
|
11
|
-
}
|
|
7
|
+
records.push(...parseActionBodyRecords(bodyBlock.body, state));
|
|
12
8
|
}
|
|
13
9
|
return records;
|
|
14
10
|
}
|
|
@@ -19,6 +15,58 @@ export function stripNestedBlocks(kind, source) {
|
|
|
19
15
|
return text;
|
|
20
16
|
}
|
|
21
17
|
|
|
18
|
+
function parseActionBodyRecords(source, state) {
|
|
19
|
+
const records = [];
|
|
20
|
+
let offset = 0;
|
|
21
|
+
while (offset < source.length) {
|
|
22
|
+
offset = skipWhitespaceAndComments(source, offset);
|
|
23
|
+
if (offset >= source.length) break;
|
|
24
|
+
const ifHeader = /^if\b([^{\n]*)\{/.exec(source.slice(offset));
|
|
25
|
+
if (ifHeader) {
|
|
26
|
+
const open = offset + ifHeader[0].length - 1;
|
|
27
|
+
const close = findMatchingBrace(source, open);
|
|
28
|
+
if (close < 0) break;
|
|
29
|
+
const index = state.index++;
|
|
30
|
+
records.push(parseActionIfBlock(ifHeader[1].trim(), source.slice(open + 1, close), index, state));
|
|
31
|
+
offset = close + 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const lineEnd = source.indexOf('\n', offset);
|
|
35
|
+
const end = lineEnd < 0 ? source.length : lineEnd;
|
|
36
|
+
const line = source.slice(offset, end).trim();
|
|
37
|
+
if (line && line !== '}') {
|
|
38
|
+
const record = parseActionBodyLine(line, state.index++);
|
|
39
|
+
if (record) records.push(record);
|
|
40
|
+
}
|
|
41
|
+
offset = end + 1;
|
|
42
|
+
}
|
|
43
|
+
return records;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseActionIfBlock(header, body, index, state) {
|
|
47
|
+
const details = parseActionIfHeader(header, index);
|
|
48
|
+
return compactRecord({
|
|
49
|
+
kind: 'if',
|
|
50
|
+
id: details.id,
|
|
51
|
+
name: details.name,
|
|
52
|
+
condition: details.condition,
|
|
53
|
+
body: parseActionBodyRecords(body, state)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseActionIfHeader(header, index) {
|
|
58
|
+
const withoutId = header.replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
59
|
+
const explicitCondition = /\bcondition\s+(.+)$/.exec(withoutId);
|
|
60
|
+
const nameText = explicitCondition ? withoutId.slice(0, explicitCondition.index).trim() : '';
|
|
61
|
+
const name = firstIdentifier(nameText) ?? `if_${index}`;
|
|
62
|
+
const conditionText = explicitCondition?.[1]?.trim() || withoutId;
|
|
63
|
+
return {
|
|
64
|
+
id: idFrom(header, `action_body_if_${name}`),
|
|
65
|
+
name,
|
|
66
|
+
condition: conditionText ? readActionValue(conditionText) : undefined
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
22
70
|
function parseActionBodyLine(line, index) {
|
|
23
71
|
const statement = /^([A-Za-z_$][\w$-]*)(?:\s+([A-Za-z_$@./:*+-][\w$./@:*+-]*))?(.*)$/.exec(line);
|
|
24
72
|
if (!statement) return undefined;
|
|
@@ -41,6 +89,70 @@ function parseActionBodyLine(line, index) {
|
|
|
41
89
|
return undefined;
|
|
42
90
|
}
|
|
43
91
|
|
|
92
|
+
function skipWhitespaceAndComments(source, offset) {
|
|
93
|
+
let index = offset;
|
|
94
|
+
while (index < source.length) {
|
|
95
|
+
while (index < source.length && /\s/.test(source[index])) index++;
|
|
96
|
+
if (source[index] !== '#') return index;
|
|
97
|
+
const lineEnd = source.indexOf('\n', index);
|
|
98
|
+
index = lineEnd < 0 ? source.length : lineEnd + 1;
|
|
99
|
+
}
|
|
100
|
+
return index;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findMatchingBrace(source, open) {
|
|
104
|
+
let depth = 0;
|
|
105
|
+
let state = 'code';
|
|
106
|
+
let quote = '';
|
|
107
|
+
for (let index = open; index < source.length; index++) {
|
|
108
|
+
const char = source[index];
|
|
109
|
+
const next = source[index + 1];
|
|
110
|
+
if (state === 'line-comment') {
|
|
111
|
+
if (char === '\n') state = 'code';
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (state === 'block-comment') {
|
|
115
|
+
if (char === '*' && next === '/') {
|
|
116
|
+
index++;
|
|
117
|
+
state = 'code';
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (state === 'string') {
|
|
122
|
+
if (char === '\\') {
|
|
123
|
+
index++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (char === quote) {
|
|
127
|
+
state = 'code';
|
|
128
|
+
quote = '';
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (char === '/' && next === '/') {
|
|
133
|
+
index++;
|
|
134
|
+
state = 'line-comment';
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (char === '/' && next === '*') {
|
|
138
|
+
index++;
|
|
139
|
+
state = 'block-comment';
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
143
|
+
state = 'string';
|
|
144
|
+
quote = char;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (char === '{') depth++;
|
|
148
|
+
if (char === '}') {
|
|
149
|
+
depth--;
|
|
150
|
+
if (depth === 0) return index;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return -1;
|
|
154
|
+
}
|
|
155
|
+
|
|
44
156
|
function readInlineActionValue(label, text) {
|
|
45
157
|
const value = new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim();
|
|
46
158
|
return value ? readActionValue(value) : undefined;
|
|
@@ -59,4 +171,5 @@ function readActionValue(value) {
|
|
|
59
171
|
|
|
60
172
|
function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
|
|
61
173
|
function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
|
|
174
|
+
function firstIdentifier(text) { return /^([A-Za-z_$][\w$-]*)\b/.exec(text)?.[1]; }
|
|
62
175
|
function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0))); }
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return', 'if']);
|
|
2
|
+
|
|
3
|
+
export function readActionSyntaxChildren(source, block, options) {
|
|
4
|
+
const body = source.slice(block.bodyStartOffset, block.bodyEndOffset);
|
|
5
|
+
const bodyBlocks = readNestedBodyBlocks('body', body);
|
|
6
|
+
const state = { rowIndex: 0 };
|
|
7
|
+
const children = [];
|
|
8
|
+
for (const bodyBlock of bodyBlocks) {
|
|
9
|
+
const bodyStartOffset = block.bodyStartOffset + bodyBlock.bodyStart;
|
|
10
|
+
const bodyEndOffset = block.bodyStartOffset + bodyBlock.bodyEnd;
|
|
11
|
+
children.push(...readActionSyntaxRows(source, block, options, state, bodyStartOffset, bodyEndOffset));
|
|
12
|
+
}
|
|
13
|
+
return children;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readActionSyntaxRows(source, block, options, state, startOffset, endOffset, parentActionBodyId) {
|
|
17
|
+
const children = [];
|
|
18
|
+
let offset = startOffset;
|
|
19
|
+
while (offset < endOffset) {
|
|
20
|
+
offset = skipWhitespaceAndLineComments(source, offset, endOffset);
|
|
21
|
+
if (offset >= endOffset) break;
|
|
22
|
+
const ifHeader = /^if\b([^{\n]*)\{/.exec(source.slice(offset, endOffset));
|
|
23
|
+
if (ifHeader) {
|
|
24
|
+
const open = offset + ifHeader[0].length - 1;
|
|
25
|
+
const close = findMatchingBrace(source, open);
|
|
26
|
+
if (close < 0 || close > endOffset) break;
|
|
27
|
+
const child = actionSyntaxChild(source, block, options, {
|
|
28
|
+
text: source.slice(offset, open + 1).trim(),
|
|
29
|
+
startOffset: offset,
|
|
30
|
+
endOffset: open + 1
|
|
31
|
+
}, state, parentActionBodyId);
|
|
32
|
+
children.push(child);
|
|
33
|
+
children.push(...readActionSyntaxRows(source, block, options, state, open + 1, close, child.id));
|
|
34
|
+
offset = close + 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const lineEnd = source.indexOf('\n', offset);
|
|
38
|
+
const rawEnd = lineEnd < 0 || lineEnd > endOffset ? endOffset : lineEnd;
|
|
39
|
+
const rawLine = source.slice(offset, rawEnd);
|
|
40
|
+
const leading = /^\s*/.exec(rawLine)?.[0].length ?? 0;
|
|
41
|
+
const trailing = /\s*$/.exec(rawLine)?.[0].length ?? 0;
|
|
42
|
+
const line = {
|
|
43
|
+
text: rawLine.trim(),
|
|
44
|
+
startOffset: offset + leading,
|
|
45
|
+
endOffset: Math.max(offset + leading, rawEnd - trailing)
|
|
46
|
+
};
|
|
47
|
+
if (line.text && line.text !== '}') children.push(actionSyntaxChild(source, block, options, line, state, parentActionBodyId));
|
|
48
|
+
offset = rawEnd + 1;
|
|
49
|
+
}
|
|
50
|
+
return children;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function actionSyntaxChild(source, block, options, line, state, parentActionBodyId) {
|
|
54
|
+
const rowIndex = state.rowIndex++;
|
|
55
|
+
const row = /^([A-Za-z_$][\w$-]*)(?:\s+([A-Za-z_$@./:*+-][\w$./@:*+-]*))?(.*)$/.exec(line.text);
|
|
56
|
+
const rowKind = row?.[1] ?? 'unknown';
|
|
57
|
+
const name = actionRowName(rowKind, row?.[2], rowIndex);
|
|
58
|
+
const rest = row?.[2]?.startsWith('@') ? ` ${row[2]}${row[3] ?? ''}` : (row?.[3] ?? '');
|
|
59
|
+
const recognized = ACTION_BODY_ROWS.has(rowKind);
|
|
60
|
+
return cleanRecord({
|
|
61
|
+
kind: recognized ? 'actionBodyRow' : 'actionUnknownRow',
|
|
62
|
+
rowKind,
|
|
63
|
+
normalizedRowKind: recognized ? rowKind : 'unknown',
|
|
64
|
+
name,
|
|
65
|
+
id: idFrom(rest, `action_body_${safeId(rowKind)}_${safeId(name)}_${rowIndex}`),
|
|
66
|
+
header: line.text,
|
|
67
|
+
startOffset: line.startOffset,
|
|
68
|
+
endOffset: line.endOffset,
|
|
69
|
+
location: sourcePosition(source, line.startOffset),
|
|
70
|
+
parentKind: block.kind,
|
|
71
|
+
parentId: block.id,
|
|
72
|
+
parentName: block.name,
|
|
73
|
+
moduleId: block.moduleId,
|
|
74
|
+
moduleName: block.moduleName,
|
|
75
|
+
parentActionBodyId,
|
|
76
|
+
sourceSpan: sourceSpan(source, block, line.startOffset, line.endOffset, options),
|
|
77
|
+
recognized,
|
|
78
|
+
reason: recognized ? undefined : 'unsupported-action-body-row'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function actionRowName(rowKind, rawName, rowIndex) {
|
|
83
|
+
if (rowKind === 'if') {
|
|
84
|
+
return rawName && !rawName.startsWith('@') && /^[A-Za-z_$][\w$-]*$/.test(rawName) ? rawName : `${rowKind}_${rowIndex}`;
|
|
85
|
+
}
|
|
86
|
+
return rawName && !rawName.startsWith('@') ? rawName : `${rowKind}_${rowIndex}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readNestedBodyBlocks(kind, source) {
|
|
90
|
+
const blocks = [];
|
|
91
|
+
const header = new RegExp('\\b' + kind.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:\\s+([^{}\\n]+?))?\\s*\\{', 'g');
|
|
92
|
+
let match;
|
|
93
|
+
while ((match = header.exec(source))) {
|
|
94
|
+
const open = header.lastIndex - 1;
|
|
95
|
+
const close = findMatchingBrace(source, open);
|
|
96
|
+
if (close < 0) continue;
|
|
97
|
+
blocks.push({
|
|
98
|
+
header: (match[1] ?? '').trim(),
|
|
99
|
+
start: match.index,
|
|
100
|
+
bodyStart: open + 1,
|
|
101
|
+
bodyEnd: close,
|
|
102
|
+
end: close + 1
|
|
103
|
+
});
|
|
104
|
+
header.lastIndex = close + 1;
|
|
105
|
+
}
|
|
106
|
+
return blocks;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function skipWhitespaceAndLineComments(source, offset, endOffset) {
|
|
110
|
+
let index = offset;
|
|
111
|
+
while (index < endOffset) {
|
|
112
|
+
while (index < endOffset && /\s/.test(source[index])) index++;
|
|
113
|
+
if (source[index] !== '#') return index;
|
|
114
|
+
const lineEnd = source.indexOf('\n', index);
|
|
115
|
+
index = lineEnd < 0 || lineEnd > endOffset ? endOffset : lineEnd + 1;
|
|
116
|
+
}
|
|
117
|
+
return index;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function findMatchingBrace(source, open) {
|
|
121
|
+
let depth = 0;
|
|
122
|
+
let state = 'code';
|
|
123
|
+
let quote = '';
|
|
124
|
+
for (let index = open; index < source.length; index++) {
|
|
125
|
+
const char = source[index];
|
|
126
|
+
const next = source[index + 1];
|
|
127
|
+
if (state === 'line-comment') {
|
|
128
|
+
if (char === '\n') state = 'code';
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (state === 'block-comment') {
|
|
132
|
+
if (char === '*' && next === '/') {
|
|
133
|
+
index++;
|
|
134
|
+
state = 'code';
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (state === 'string') {
|
|
139
|
+
if (char === '\\') {
|
|
140
|
+
index++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (char === quote) {
|
|
144
|
+
state = 'code';
|
|
145
|
+
quote = '';
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (char === '/' && next === '/') {
|
|
150
|
+
index++;
|
|
151
|
+
state = 'line-comment';
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (char === '/' && next === '*') {
|
|
155
|
+
index++;
|
|
156
|
+
state = 'block-comment';
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
160
|
+
state = 'string';
|
|
161
|
+
quote = char;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (char === '{') depth++;
|
|
165
|
+
if (char === '}') {
|
|
166
|
+
depth--;
|
|
167
|
+
if (depth === 0) return index;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return -1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function sourcePosition(source, offset) {
|
|
174
|
+
const lines = source.slice(0, offset).split('\n');
|
|
175
|
+
return { line: lines.length, column: lines[lines.length - 1].length + 1, offset };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sourceSpan(source, block, startOffset, endOffset, options = {}) {
|
|
179
|
+
return cleanRecord({
|
|
180
|
+
sourceId: options.documentId,
|
|
181
|
+
path: options.sourcePath,
|
|
182
|
+
blockId: block.id,
|
|
183
|
+
blockKind: block.kind,
|
|
184
|
+
startOffset,
|
|
185
|
+
endOffset,
|
|
186
|
+
start: sourcePosition(source, startOffset),
|
|
187
|
+
end: sourcePosition(source, endOffset)
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
|
|
192
|
+
function safeId(value) { return String(value).replace(/[^A-Za-z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'row'; }
|
|
193
|
+
function cleanRecord(record) {
|
|
194
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
|
|
195
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readActionSyntaxChildren } from './action-syntax-children.js';
|
|
1
2
|
import { ROW_SYNTAX_CONFIG } from './source-syntax-row-config.js';
|
|
2
3
|
|
|
3
4
|
const ROW_NAME_PATTERN = '([A-Za-z_$@./:*+-][\\w$./@:*+-]*)';
|
|
@@ -15,48 +16,6 @@ export function readSourceSyntaxChildren(source, block, options = {}) {
|
|
|
15
16
|
return [];
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return']);
|
|
19
|
-
|
|
20
|
-
function readActionSyntaxChildren(source, block, options) {
|
|
21
|
-
const children = [];
|
|
22
|
-
const body = source.slice(block.bodyStartOffset, block.bodyEndOffset);
|
|
23
|
-
const bodyBlocks = readNestedBodyBlocks('body', body);
|
|
24
|
-
let rowIndex = 0;
|
|
25
|
-
for (const bodyBlock of bodyBlocks) {
|
|
26
|
-
const bodyStartOffset = block.bodyStartOffset + bodyBlock.bodyStart;
|
|
27
|
-
const bodyEndOffset = block.bodyStartOffset + bodyBlock.bodyEnd;
|
|
28
|
-
for (const line of readTextLines(source, bodyStartOffset, bodyEndOffset)) {
|
|
29
|
-
if (!line.text || line.text.startsWith('#')) continue;
|
|
30
|
-
const row = /^([A-Za-z_$][\w$-]*)(?:\s+([A-Za-z_$@./:*+-][\w$./@:*+-]*))?(.*)$/.exec(line.text);
|
|
31
|
-
const rowKind = row?.[1] ?? 'unknown';
|
|
32
|
-
const name = row?.[2] && !row[2].startsWith('@') ? row[2] : `${rowKind}_${rowIndex}`;
|
|
33
|
-
const rest = row?.[2]?.startsWith('@') ? ` ${row[2]}${row[3] ?? ''}` : (row?.[3] ?? '');
|
|
34
|
-
const recognized = ACTION_BODY_ROWS.has(rowKind);
|
|
35
|
-
children.push(cleanRecord({
|
|
36
|
-
kind: recognized ? 'actionBodyRow' : 'actionUnknownRow',
|
|
37
|
-
rowKind,
|
|
38
|
-
normalizedRowKind: recognized ? rowKind : 'unknown',
|
|
39
|
-
name,
|
|
40
|
-
id: idFrom(rest, `action_body_${safeId(rowKind)}_${safeId(name)}_${rowIndex}`),
|
|
41
|
-
header: line.text,
|
|
42
|
-
startOffset: line.startOffset,
|
|
43
|
-
endOffset: line.endOffset,
|
|
44
|
-
location: sourcePosition(source, line.startOffset),
|
|
45
|
-
parentKind: block.kind,
|
|
46
|
-
parentId: block.id,
|
|
47
|
-
parentName: block.name,
|
|
48
|
-
moduleId: block.moduleId,
|
|
49
|
-
moduleName: block.moduleName,
|
|
50
|
-
sourceSpan: sourceSpan(source, block, line.startOffset, line.endOffset, options),
|
|
51
|
-
recognized,
|
|
52
|
-
reason: recognized ? undefined : 'unsupported-action-body-row'
|
|
53
|
-
}));
|
|
54
|
-
rowIndex++;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return children;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
19
|
function readConversionSyntaxChildren(source, block, options) {
|
|
61
20
|
const children = [];
|
|
62
21
|
for (const line of readBodyLines(source, block)) {
|
|
@@ -196,79 +155,6 @@ function readTextLines(source, startOffset, endOffset) {
|
|
|
196
155
|
return records;
|
|
197
156
|
}
|
|
198
157
|
|
|
199
|
-
function readNestedBodyBlocks(kind, source) {
|
|
200
|
-
const blocks = [];
|
|
201
|
-
const header = new RegExp('\\b' + kind.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:\\s+([^{}\\n]+?))?\\s*\\{', 'g');
|
|
202
|
-
let match;
|
|
203
|
-
while ((match = header.exec(source))) {
|
|
204
|
-
const open = header.lastIndex - 1;
|
|
205
|
-
const close = findMatchingBrace(source, open);
|
|
206
|
-
if (close < 0) continue;
|
|
207
|
-
blocks.push({
|
|
208
|
-
header: (match[1] ?? '').trim(),
|
|
209
|
-
start: match.index,
|
|
210
|
-
bodyStart: open + 1,
|
|
211
|
-
bodyEnd: close,
|
|
212
|
-
end: close + 1
|
|
213
|
-
});
|
|
214
|
-
header.lastIndex = close + 1;
|
|
215
|
-
}
|
|
216
|
-
return blocks;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function findMatchingBrace(source, open) {
|
|
220
|
-
let depth = 0;
|
|
221
|
-
let state = 'code';
|
|
222
|
-
let quote = '';
|
|
223
|
-
for (let index = open; index < source.length; index++) {
|
|
224
|
-
const char = source[index];
|
|
225
|
-
const next = source[index + 1];
|
|
226
|
-
if (state === 'line-comment') {
|
|
227
|
-
if (char === '\n') state = 'code';
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (state === 'block-comment') {
|
|
231
|
-
if (char === '*' && next === '/') {
|
|
232
|
-
index++;
|
|
233
|
-
state = 'code';
|
|
234
|
-
}
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
if (state === 'string') {
|
|
238
|
-
if (char === '\\') {
|
|
239
|
-
index++;
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (char === quote) {
|
|
243
|
-
state = 'code';
|
|
244
|
-
quote = '';
|
|
245
|
-
}
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
if (char === '/' && next === '/') {
|
|
249
|
-
index++;
|
|
250
|
-
state = 'line-comment';
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
if (char === '/' && next === '*') {
|
|
254
|
-
index++;
|
|
255
|
-
state = 'block-comment';
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (char === '"' || char === "'") {
|
|
259
|
-
state = 'string';
|
|
260
|
-
quote = char;
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
if (char === '{') depth++;
|
|
264
|
-
if (char === '}') {
|
|
265
|
-
depth--;
|
|
266
|
-
if (depth === 0) return index;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return -1;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
158
|
function sourcePosition(source, offset) {
|
|
273
159
|
const lines = source.slice(0, offset).split('\n');
|
|
274
160
|
return { line: lines.length, column: lines[lines.length - 1].length + 1, offset };
|