ampscript-parser 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/README.md +117 -0
- package/package.json +33 -0
- package/src/index.js +1164 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# ampscript-parser
|
|
2
|
+
|
|
3
|
+
AMPscript lexer and parser that produces an AST for Salesforce Marketing Cloud (SFMC) tooling.
|
|
4
|
+
|
|
5
|
+
Handles all AMPscript embedding syntaxes:
|
|
6
|
+
|
|
7
|
+
- Block syntax: `%%[ ... ]%%`
|
|
8
|
+
- Script-tag syntax: `<script runat="server" language="ampscript"> ... </script>`
|
|
9
|
+
- Inline expressions: `%%=...=%%`
|
|
10
|
+
- Plain HTML/text content between AMPscript segments
|
|
11
|
+
|
|
12
|
+
This package is used internally by:
|
|
13
|
+
|
|
14
|
+
- [prettier-plugin-sfmc](https://www.npmjs.com/package/prettier-plugin-sfmc) — AMPscript formatting
|
|
15
|
+
- [eslint-plugin-sfmc](https://www.npmjs.com/package/eslint-plugin-sfmc) — AMPscript linting
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install ampscript-parser
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { parse, tokenizeBlock, parseStatements, TokenType } from 'ampscript-parser';
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `parse(text)`
|
|
30
|
+
|
|
31
|
+
Parses a full document string (HTML with embedded AMPscript) into a `Document` AST. This is the main entry point for most use cases.
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { parse } from 'ampscript-parser';
|
|
35
|
+
|
|
36
|
+
const doc = parse(`
|
|
37
|
+
<p>Hello %%=AttributeValue('firstname')=%%</p>
|
|
38
|
+
%%[
|
|
39
|
+
SET @greeting = "Welcome"
|
|
40
|
+
IF @greeting == "Welcome" THEN
|
|
41
|
+
Output(@greeting)
|
|
42
|
+
ENDIF
|
|
43
|
+
]%%
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
// doc.type === 'Document'
|
|
47
|
+
// doc.children — array of Content, Block, and InlineExpression nodes
|
|
48
|
+
for (const node of doc.children) {
|
|
49
|
+
console.log(node.type); // 'Content' | 'Block' | 'InlineExpression'
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### AST node types
|
|
54
|
+
|
|
55
|
+
| Node type | Description |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `Document` | Root node; has a `children` array |
|
|
58
|
+
| `Content` | Plain HTML/text segment between AMPscript regions |
|
|
59
|
+
| `Block` | A `%%[ ]%%` or script-tag block; has a `statements` array |
|
|
60
|
+
| `InlineExpression` | A `%%=...=%%` expression; has an `expression` property |
|
|
61
|
+
|
|
62
|
+
### `tokenizeBlock(code, offset?)`
|
|
63
|
+
|
|
64
|
+
Tokenizes a raw AMPscript code string (the content inside a block or inline expression, without the surrounding delimiters). Returns an array of token objects.
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import { tokenizeBlock } from 'ampscript-parser';
|
|
68
|
+
|
|
69
|
+
const tokens = tokenizeBlock("SET @name = 'World'");
|
|
70
|
+
|
|
71
|
+
for (const token of tokens) {
|
|
72
|
+
console.log(token.type); // e.g. 'SET', 'VARIABLE', 'EQUALS', 'STRING'
|
|
73
|
+
console.log(token.value); // raw text of the token
|
|
74
|
+
console.log(token.start); // character offset in the source
|
|
75
|
+
console.log(token.end); // end offset
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The optional `offset` parameter shifts all token positions by a base character offset, useful when the code snippet originates from a larger document.
|
|
80
|
+
|
|
81
|
+
### `parseStatements(tokens)`
|
|
82
|
+
|
|
83
|
+
Parses an array of tokens (as returned by `tokenizeBlock`) into an array of statement AST nodes. Useful when you already have tokens and want to build the AST incrementally.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
import { tokenizeBlock, parseStatements } from 'ampscript-parser';
|
|
87
|
+
|
|
88
|
+
const tokens = tokenizeBlock("VAR @x\nSET @x = Add(1, 2)");
|
|
89
|
+
const statements = parseStatements(tokens);
|
|
90
|
+
|
|
91
|
+
for (const stmt of statements) {
|
|
92
|
+
console.log(stmt.type); // e.g. 'VarStatement', 'SetStatement'
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `TokenType`
|
|
97
|
+
|
|
98
|
+
An object of token type constants used to identify tokens returned by `tokenizeBlock`.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { TokenType } from 'ampscript-parser';
|
|
102
|
+
|
|
103
|
+
console.log(TokenType.SET); // 'SET'
|
|
104
|
+
console.log(TokenType.IF); // 'IF'
|
|
105
|
+
console.log(TokenType.VARIABLE); // 'VARIABLE'
|
|
106
|
+
console.log(TokenType.STRING); // 'STRING'
|
|
107
|
+
console.log(TokenType.NUMBER); // 'NUMBER'
|
|
108
|
+
console.log(TokenType.BOOLEAN); // 'BOOLEAN'
|
|
109
|
+
console.log(TokenType.IDENTIFIER); // 'IDENTIFIER'
|
|
110
|
+
console.log(TokenType.COMMENT); // 'COMMENT'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Full list of token types: `BLOCK_OPEN`, `BLOCK_CLOSE`, `INLINE_OPEN`, `INLINE_CLOSE`, `VAR`, `SET`, `IF`, `THEN`, `ELSEIF`, `ELSE`, `ENDIF`, `FOR`, `TO`, `DOWNTO`, `DO`, `NEXT`, `AND`, `OR`, `NOT`, `COMMA`, `LPAREN`, `RPAREN`, `EQUALS`, `EQ`, `NEQ`, `GT`, `LT`, `GTE`, `LTE`, `STRING`, `NUMBER`, `BOOLEAN`, `IDENTIFIER`, `VARIABLE`, `COMMENT`, `NEWLINE`, `WHITESPACE`.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ampscript-parser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AMPscript lexer and parser producing an AST for SFMC tooling",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": ["src"],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/JoernBerkefeld/ampscript-parser.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/JoernBerkefeld/ampscript-parser/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/JoernBerkefeld/ampscript-parser#readme",
|
|
19
|
+
"author": "Joern Berkefeld",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ampscript",
|
|
22
|
+
"sfmc",
|
|
23
|
+
"salesforce",
|
|
24
|
+
"marketing-cloud",
|
|
25
|
+
"parser",
|
|
26
|
+
"ast",
|
|
27
|
+
"lexer"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AMPscript Parser
|
|
3
|
+
*
|
|
4
|
+
* Tokenizes and builds an AST from AMPscript code, which can be:
|
|
5
|
+
* - AMPscript blocks: %%[ ... ]%%
|
|
6
|
+
* - Script-tag blocks: <script runat="server" language="ampscript"> ... </script>
|
|
7
|
+
* - Inline expressions: %%=...=%%
|
|
8
|
+
* - HTML/text content between AMPscript segments
|
|
9
|
+
*
|
|
10
|
+
* The AST is a flat list of top-level nodes (Content, Block, InlineExpression).
|
|
11
|
+
* Inside blocks, statements are parsed into their own node types.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Prettier Ignore Marking ───────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Walks an array of statement nodes and marks nodes to be ignored based on
|
|
18
|
+
* prettier-ignore and prettier-ignore-start / prettier-ignore-end comments.
|
|
19
|
+
*/
|
|
20
|
+
function markPrettierIgnore(nodes) {
|
|
21
|
+
if (!Array.isArray(nodes)) return;
|
|
22
|
+
let index = 0;
|
|
23
|
+
while (index < nodes.length) {
|
|
24
|
+
const node = nodes[index];
|
|
25
|
+
if (
|
|
26
|
+
node &&
|
|
27
|
+
node.type === 'Comment' &&
|
|
28
|
+
/^\s*\/\*\s*prettier-ignore\s*\*\/\s*$/i.test(node.value)
|
|
29
|
+
) {
|
|
30
|
+
let index_ = index + 1;
|
|
31
|
+
while (index_ < nodes.length && nodes[index_].type === 'Comment') index_++;
|
|
32
|
+
if (index_ < nodes.length) nodes[index_].prettierIgnore = true;
|
|
33
|
+
index = index_;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (node && node.type === 'Comment' && /prettier-ignore-start/i.test(node.value)) {
|
|
37
|
+
let index_ = index + 1;
|
|
38
|
+
while (
|
|
39
|
+
index_ < nodes.length &&
|
|
40
|
+
!(
|
|
41
|
+
nodes[index_].type === 'Comment' &&
|
|
42
|
+
/prettier-ignore-end/i.test(nodes[index_].value)
|
|
43
|
+
)
|
|
44
|
+
) {
|
|
45
|
+
if (nodes[index_].type !== 'Comment') nodes[index_].prettierIgnore = true;
|
|
46
|
+
index_++;
|
|
47
|
+
}
|
|
48
|
+
index = index_ + 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (node && typeof node === 'object') {
|
|
52
|
+
if (Array.isArray(node.statements)) markPrettierIgnore(node.statements);
|
|
53
|
+
if (Array.isArray(node.consequent)) markPrettierIgnore(node.consequent);
|
|
54
|
+
if (Array.isArray(node.alternates)) {
|
|
55
|
+
for (const alt of node.alternates) {
|
|
56
|
+
if (Array.isArray(alt.body)) markPrettierIgnore(alt.body);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(node.body)) markPrettierIgnore(node.body);
|
|
60
|
+
}
|
|
61
|
+
index++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Token types ──────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
const TokenType = {
|
|
68
|
+
BLOCK_OPEN: 'BLOCK_OPEN',
|
|
69
|
+
BLOCK_CLOSE: 'BLOCK_CLOSE',
|
|
70
|
+
INLINE_OPEN: 'INLINE_OPEN',
|
|
71
|
+
INLINE_CLOSE: 'INLINE_CLOSE',
|
|
72
|
+
VAR: 'VAR',
|
|
73
|
+
SET: 'SET',
|
|
74
|
+
IF: 'IF',
|
|
75
|
+
THEN: 'THEN',
|
|
76
|
+
ELSEIF: 'ELSEIF',
|
|
77
|
+
ELSE: 'ELSE',
|
|
78
|
+
ENDIF: 'ENDIF',
|
|
79
|
+
FOR: 'FOR',
|
|
80
|
+
TO: 'TO',
|
|
81
|
+
DOWNTO: 'DOWNTO',
|
|
82
|
+
DO: 'DO',
|
|
83
|
+
NEXT: 'NEXT',
|
|
84
|
+
AND: 'AND',
|
|
85
|
+
OR: 'OR',
|
|
86
|
+
NOT: 'NOT',
|
|
87
|
+
COMMA: 'COMMA',
|
|
88
|
+
LPAREN: 'LPAREN',
|
|
89
|
+
RPAREN: 'RPAREN',
|
|
90
|
+
EQUALS: 'EQUALS',
|
|
91
|
+
EQ: 'EQ',
|
|
92
|
+
NEQ: 'NEQ',
|
|
93
|
+
GT: 'GT',
|
|
94
|
+
LT: 'LT',
|
|
95
|
+
GTE: 'GTE',
|
|
96
|
+
LTE: 'LTE',
|
|
97
|
+
STRING: 'STRING',
|
|
98
|
+
NUMBER: 'NUMBER',
|
|
99
|
+
BOOLEAN: 'BOOLEAN',
|
|
100
|
+
IDENTIFIER: 'IDENTIFIER',
|
|
101
|
+
VARIABLE: 'VARIABLE',
|
|
102
|
+
COMMENT: 'COMMENT',
|
|
103
|
+
NEWLINE: 'NEWLINE',
|
|
104
|
+
WHITESPACE: 'WHITESPACE',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const KEYWORDS = {
|
|
108
|
+
var: TokenType.VAR,
|
|
109
|
+
set: TokenType.SET,
|
|
110
|
+
if: TokenType.IF,
|
|
111
|
+
then: TokenType.THEN,
|
|
112
|
+
elseif: TokenType.ELSEIF,
|
|
113
|
+
else: TokenType.ELSE,
|
|
114
|
+
endif: TokenType.ENDIF,
|
|
115
|
+
for: TokenType.FOR,
|
|
116
|
+
to: TokenType.TO,
|
|
117
|
+
downto: TokenType.DOWNTO,
|
|
118
|
+
do: TokenType.DO,
|
|
119
|
+
next: TokenType.NEXT,
|
|
120
|
+
and: TokenType.AND,
|
|
121
|
+
or: TokenType.OR,
|
|
122
|
+
not: TokenType.NOT,
|
|
123
|
+
true: TokenType.BOOLEAN,
|
|
124
|
+
false: TokenType.BOOLEAN,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ── Tokenizer ────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
function tokenizeBlock(code, offset = 0) {
|
|
130
|
+
const tokens = [];
|
|
131
|
+
let index = 0;
|
|
132
|
+
|
|
133
|
+
while (index < code.length) {
|
|
134
|
+
if (code[index] === ' ' || code[index] === '\t') {
|
|
135
|
+
const start = index;
|
|
136
|
+
while (index < code.length && (code[index] === ' ' || code[index] === '\t')) index++;
|
|
137
|
+
tokens.push({
|
|
138
|
+
type: TokenType.WHITESPACE,
|
|
139
|
+
value: code.slice(start, index),
|
|
140
|
+
start: offset + start,
|
|
141
|
+
end: offset + index,
|
|
142
|
+
});
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (code[index] === '\n' || code[index] === '\r') {
|
|
147
|
+
const start = index;
|
|
148
|
+
if (code[index] === '\r' && code[index + 1] === '\n') index++;
|
|
149
|
+
index++;
|
|
150
|
+
tokens.push({
|
|
151
|
+
type: TokenType.NEWLINE,
|
|
152
|
+
value: code.slice(start, index),
|
|
153
|
+
start: offset + start,
|
|
154
|
+
end: offset + index,
|
|
155
|
+
});
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (code[index] === '/' && code[index + 1] === '*') {
|
|
160
|
+
const start = index;
|
|
161
|
+
index += 2;
|
|
162
|
+
while (index < code.length && !(code[index] === '*' && code[index + 1] === '/'))
|
|
163
|
+
index++;
|
|
164
|
+
if (index < code.length) index += 2;
|
|
165
|
+
tokens.push({
|
|
166
|
+
type: TokenType.COMMENT,
|
|
167
|
+
value: code.slice(start, index),
|
|
168
|
+
start: offset + start,
|
|
169
|
+
end: offset + index,
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (code[index] === '"' || code[index] === "'") {
|
|
175
|
+
const quote = code[index];
|
|
176
|
+
const start = index;
|
|
177
|
+
index++;
|
|
178
|
+
while (index < code.length && code[index] !== quote) {
|
|
179
|
+
index++;
|
|
180
|
+
}
|
|
181
|
+
if (index < code.length) index++;
|
|
182
|
+
tokens.push({
|
|
183
|
+
type: TokenType.STRING,
|
|
184
|
+
value: code.slice(start, index),
|
|
185
|
+
start: offset + start,
|
|
186
|
+
end: offset + index,
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (code[index] === '=' && code[index + 1] === '=') {
|
|
192
|
+
tokens.push({
|
|
193
|
+
type: TokenType.EQ,
|
|
194
|
+
value: '==',
|
|
195
|
+
start: offset + index,
|
|
196
|
+
end: offset + index + 2,
|
|
197
|
+
});
|
|
198
|
+
index += 2;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (code[index] === '!' && code[index + 1] === '=') {
|
|
202
|
+
tokens.push({
|
|
203
|
+
type: TokenType.NEQ,
|
|
204
|
+
value: '!=',
|
|
205
|
+
start: offset + index,
|
|
206
|
+
end: offset + index + 2,
|
|
207
|
+
});
|
|
208
|
+
index += 2;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (code[index] === '>' && code[index + 1] === '=') {
|
|
212
|
+
tokens.push({
|
|
213
|
+
type: TokenType.GTE,
|
|
214
|
+
value: '>=',
|
|
215
|
+
start: offset + index,
|
|
216
|
+
end: offset + index + 2,
|
|
217
|
+
});
|
|
218
|
+
index += 2;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (code[index] === '<' && code[index + 1] === '=') {
|
|
222
|
+
tokens.push({
|
|
223
|
+
type: TokenType.LTE,
|
|
224
|
+
value: '<=',
|
|
225
|
+
start: offset + index,
|
|
226
|
+
end: offset + index + 2,
|
|
227
|
+
});
|
|
228
|
+
index += 2;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (code[index] === '=') {
|
|
233
|
+
tokens.push({
|
|
234
|
+
type: TokenType.EQUALS,
|
|
235
|
+
value: '=',
|
|
236
|
+
start: offset + index,
|
|
237
|
+
end: offset + index + 1,
|
|
238
|
+
});
|
|
239
|
+
index++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (code[index] === '>') {
|
|
243
|
+
tokens.push({
|
|
244
|
+
type: TokenType.GT,
|
|
245
|
+
value: '>',
|
|
246
|
+
start: offset + index,
|
|
247
|
+
end: offset + index + 1,
|
|
248
|
+
});
|
|
249
|
+
index++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (code[index] === '<') {
|
|
253
|
+
tokens.push({
|
|
254
|
+
type: TokenType.LT,
|
|
255
|
+
value: '<',
|
|
256
|
+
start: offset + index,
|
|
257
|
+
end: offset + index + 1,
|
|
258
|
+
});
|
|
259
|
+
index++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (code[index] === '(') {
|
|
263
|
+
tokens.push({
|
|
264
|
+
type: TokenType.LPAREN,
|
|
265
|
+
value: '(',
|
|
266
|
+
start: offset + index,
|
|
267
|
+
end: offset + index + 1,
|
|
268
|
+
});
|
|
269
|
+
index++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (code[index] === ')') {
|
|
273
|
+
tokens.push({
|
|
274
|
+
type: TokenType.RPAREN,
|
|
275
|
+
value: ')',
|
|
276
|
+
start: offset + index,
|
|
277
|
+
end: offset + index + 1,
|
|
278
|
+
});
|
|
279
|
+
index++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (code[index] === ',') {
|
|
283
|
+
tokens.push({
|
|
284
|
+
type: TokenType.COMMA,
|
|
285
|
+
value: ',',
|
|
286
|
+
start: offset + index,
|
|
287
|
+
end: offset + index + 1,
|
|
288
|
+
});
|
|
289
|
+
index++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (code[index] === '@') {
|
|
294
|
+
const start = index;
|
|
295
|
+
index++;
|
|
296
|
+
if (index < code.length && code[index] === '@') index++;
|
|
297
|
+
while (index < code.length && /[a-zA-Z0-9_]/.test(code[index])) index++;
|
|
298
|
+
tokens.push({
|
|
299
|
+
type: TokenType.VARIABLE,
|
|
300
|
+
value: code.slice(start, index),
|
|
301
|
+
start: offset + start,
|
|
302
|
+
end: offset + index,
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
/[0-9]/.test(code[index]) ||
|
|
309
|
+
(code[index] === '-' && index + 1 < code.length && /[0-9]/.test(code[index + 1]))
|
|
310
|
+
) {
|
|
311
|
+
const start = index;
|
|
312
|
+
if (code[index] === '-') index++;
|
|
313
|
+
while (index < code.length && /[0-9]/.test(code[index])) index++;
|
|
314
|
+
if (index < code.length && code[index] === '.') {
|
|
315
|
+
index++;
|
|
316
|
+
while (index < code.length && /[0-9]/.test(code[index])) index++;
|
|
317
|
+
}
|
|
318
|
+
tokens.push({
|
|
319
|
+
type: TokenType.NUMBER,
|
|
320
|
+
value: code.slice(start, index),
|
|
321
|
+
start: offset + start,
|
|
322
|
+
end: offset + index,
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (/[a-zA-Z_]/.test(code[index])) {
|
|
328
|
+
const start = index;
|
|
329
|
+
while (index < code.length && /[a-zA-Z0-9_]/.test(code[index])) index++;
|
|
330
|
+
const word = code.slice(start, index);
|
|
331
|
+
const lower = word.toLowerCase();
|
|
332
|
+
const kwType = KEYWORDS[lower];
|
|
333
|
+
if (kwType) {
|
|
334
|
+
tokens.push({
|
|
335
|
+
type: kwType,
|
|
336
|
+
value: word,
|
|
337
|
+
start: offset + start,
|
|
338
|
+
end: offset + index,
|
|
339
|
+
});
|
|
340
|
+
} else {
|
|
341
|
+
tokens.push({
|
|
342
|
+
type: TokenType.IDENTIFIER,
|
|
343
|
+
value: word,
|
|
344
|
+
start: offset + start,
|
|
345
|
+
end: offset + index,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const start = index;
|
|
352
|
+
index++;
|
|
353
|
+
tokens.push({
|
|
354
|
+
type: 'RAW',
|
|
355
|
+
value: code[start],
|
|
356
|
+
start: offset + start,
|
|
357
|
+
end: offset + index,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return tokens;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── Statement Parser ─────────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
function parseStatements(tokens) {
|
|
367
|
+
const statements = [];
|
|
368
|
+
let pos = 0;
|
|
369
|
+
|
|
370
|
+
function _peek() {
|
|
371
|
+
while (
|
|
372
|
+
pos < tokens.length &&
|
|
373
|
+
(tokens[pos].type === TokenType.WHITESPACE || tokens[pos].type === TokenType.NEWLINE)
|
|
374
|
+
) {
|
|
375
|
+
pos++;
|
|
376
|
+
}
|
|
377
|
+
return pos < tokens.length ? tokens[pos] : null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function current() {
|
|
381
|
+
return pos < tokens.length ? tokens[pos] : null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function advance() {
|
|
385
|
+
return tokens[pos++];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function skipTrivia() {
|
|
389
|
+
let newlineCount = 0;
|
|
390
|
+
while (
|
|
391
|
+
pos < tokens.length &&
|
|
392
|
+
(tokens[pos].type === TokenType.WHITESPACE || tokens[pos].type === TokenType.NEWLINE)
|
|
393
|
+
) {
|
|
394
|
+
if (tokens[pos].type === TokenType.NEWLINE) newlineCount++;
|
|
395
|
+
pos++;
|
|
396
|
+
}
|
|
397
|
+
return newlineCount;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function parseExpression() {
|
|
401
|
+
return parseOrExpr();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function parseOrExpr() {
|
|
405
|
+
let left = parseAndExpr();
|
|
406
|
+
while (pos < tokens.length) {
|
|
407
|
+
skipTrivia();
|
|
408
|
+
const t = current();
|
|
409
|
+
if (t && t.type === TokenType.OR) {
|
|
410
|
+
const opToken = advance();
|
|
411
|
+
skipTrivia();
|
|
412
|
+
const right = parseAndExpr();
|
|
413
|
+
left = {
|
|
414
|
+
type: 'BinaryExpression',
|
|
415
|
+
operator: 'or',
|
|
416
|
+
originalOperator: opToken.value,
|
|
417
|
+
left,
|
|
418
|
+
right,
|
|
419
|
+
start: left.start,
|
|
420
|
+
end: right.end,
|
|
421
|
+
};
|
|
422
|
+
} else {
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return left;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function parseAndExpr() {
|
|
430
|
+
let left = parseNotExpr();
|
|
431
|
+
while (pos < tokens.length) {
|
|
432
|
+
skipTrivia();
|
|
433
|
+
const t = current();
|
|
434
|
+
if (t && t.type === TokenType.AND) {
|
|
435
|
+
const opToken = advance();
|
|
436
|
+
skipTrivia();
|
|
437
|
+
const right = parseNotExpr();
|
|
438
|
+
left = {
|
|
439
|
+
type: 'BinaryExpression',
|
|
440
|
+
operator: 'and',
|
|
441
|
+
originalOperator: opToken.value,
|
|
442
|
+
left,
|
|
443
|
+
right,
|
|
444
|
+
start: left.start,
|
|
445
|
+
end: right.end,
|
|
446
|
+
};
|
|
447
|
+
} else {
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return left;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function parseNotExpr() {
|
|
455
|
+
skipTrivia();
|
|
456
|
+
const t = current();
|
|
457
|
+
if (t && t.type === TokenType.NOT) {
|
|
458
|
+
const start = t.start;
|
|
459
|
+
const opToken = advance();
|
|
460
|
+
skipTrivia();
|
|
461
|
+
const expr = parseComparison();
|
|
462
|
+
return {
|
|
463
|
+
type: 'UnaryExpression',
|
|
464
|
+
operator: 'not',
|
|
465
|
+
originalOperator: opToken.value,
|
|
466
|
+
argument: expr,
|
|
467
|
+
start,
|
|
468
|
+
end: expr.end,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return parseComparison();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function parseComparison() {
|
|
475
|
+
let left = parsePrimary();
|
|
476
|
+
skipTrivia();
|
|
477
|
+
const t = current();
|
|
478
|
+
if (
|
|
479
|
+
t &&
|
|
480
|
+
(t.type === TokenType.EQ ||
|
|
481
|
+
t.type === TokenType.NEQ ||
|
|
482
|
+
t.type === TokenType.GT ||
|
|
483
|
+
t.type === TokenType.LT ||
|
|
484
|
+
t.type === TokenType.GTE ||
|
|
485
|
+
t.type === TokenType.LTE)
|
|
486
|
+
) {
|
|
487
|
+
const op = advance();
|
|
488
|
+
skipTrivia();
|
|
489
|
+
const right = parsePrimary();
|
|
490
|
+
return {
|
|
491
|
+
type: 'BinaryExpression',
|
|
492
|
+
operator: op.value,
|
|
493
|
+
left,
|
|
494
|
+
right,
|
|
495
|
+
start: left.start,
|
|
496
|
+
end: right.end,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return left;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function parsePrimary() {
|
|
503
|
+
skipTrivia();
|
|
504
|
+
const t = current();
|
|
505
|
+
if (!t) {
|
|
506
|
+
return { type: 'Empty', value: '', start: 0, end: 0 };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (t.type === TokenType.LPAREN) {
|
|
510
|
+
const start = t.start;
|
|
511
|
+
advance();
|
|
512
|
+
skipTrivia();
|
|
513
|
+
const expr = parseExpression();
|
|
514
|
+
skipTrivia();
|
|
515
|
+
const closing = current();
|
|
516
|
+
let end = expr.end;
|
|
517
|
+
if (closing && closing.type === TokenType.RPAREN) {
|
|
518
|
+
end = closing.end;
|
|
519
|
+
advance();
|
|
520
|
+
}
|
|
521
|
+
return { type: 'ParenExpression', expression: expr, start, end };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (t.type === TokenType.IDENTIFIER) {
|
|
525
|
+
const savedPos = pos;
|
|
526
|
+
const name = advance();
|
|
527
|
+
skipTrivia();
|
|
528
|
+
const next = current();
|
|
529
|
+
if (next && next.type === TokenType.LPAREN) {
|
|
530
|
+
advance();
|
|
531
|
+
const arguments_ = [];
|
|
532
|
+
skipTrivia();
|
|
533
|
+
if (current() && current().type !== TokenType.RPAREN) {
|
|
534
|
+
arguments_.push(parseExpression());
|
|
535
|
+
while (current() && current().type === TokenType.COMMA) {
|
|
536
|
+
advance();
|
|
537
|
+
skipTrivia();
|
|
538
|
+
arguments_.push(parseExpression());
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
skipTrivia();
|
|
542
|
+
let end = name.end;
|
|
543
|
+
if (current() && current().type === TokenType.RPAREN) {
|
|
544
|
+
end = current().end;
|
|
545
|
+
advance();
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
type: 'FunctionCall',
|
|
549
|
+
name: name.value,
|
|
550
|
+
arguments: arguments_,
|
|
551
|
+
start: name.start,
|
|
552
|
+
end,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
pos = savedPos;
|
|
556
|
+
advance();
|
|
557
|
+
return { type: 'Identifier', value: name.value, start: name.start, end: name.end };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (t.type === TokenType.VARIABLE) {
|
|
561
|
+
advance();
|
|
562
|
+
return { type: 'Variable', value: t.value, start: t.start, end: t.end };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (t.type === TokenType.STRING) {
|
|
566
|
+
advance();
|
|
567
|
+
const quote = t.value[0];
|
|
568
|
+
const content = t.value.slice(1, -1);
|
|
569
|
+
return { type: 'StringLiteral', value: content, quote, start: t.start, end: t.end };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (t.type === TokenType.NUMBER) {
|
|
573
|
+
advance();
|
|
574
|
+
return { type: 'NumberLiteral', value: t.value, start: t.start, end: t.end };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (t.type === TokenType.BOOLEAN) {
|
|
578
|
+
advance();
|
|
579
|
+
return {
|
|
580
|
+
type: 'BooleanLiteral',
|
|
581
|
+
value: t.value.toLowerCase(),
|
|
582
|
+
originalValue: t.value,
|
|
583
|
+
start: t.start,
|
|
584
|
+
end: t.end,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
advance();
|
|
589
|
+
return { type: 'Raw', value: t.value, start: t.start, end: t.end };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ── Main statement parsing loop ──
|
|
593
|
+
|
|
594
|
+
function pushStmt(stmt, blankLine) {
|
|
595
|
+
if (blankLine) stmt.blankLineBefore = true;
|
|
596
|
+
statements.push(stmt);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
while (pos < tokens.length) {
|
|
600
|
+
const newlines = skipTrivia();
|
|
601
|
+
if (pos >= tokens.length) break;
|
|
602
|
+
const hasBlankLine = newlines >= 2 && statements.length > 0;
|
|
603
|
+
|
|
604
|
+
const t = current();
|
|
605
|
+
|
|
606
|
+
if (t.type === TokenType.COMMENT) {
|
|
607
|
+
advance();
|
|
608
|
+
pushStmt({ type: 'Comment', value: t.value, start: t.start, end: t.end }, hasBlankLine);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (t.type === TokenType.VAR) {
|
|
613
|
+
const start = t.start;
|
|
614
|
+
const variableKeyword = t.value;
|
|
615
|
+
advance();
|
|
616
|
+
skipTrivia();
|
|
617
|
+
const variables = [];
|
|
618
|
+
while (current() && current().type === TokenType.VARIABLE) {
|
|
619
|
+
variables.push({
|
|
620
|
+
type: 'Variable',
|
|
621
|
+
value: current().value,
|
|
622
|
+
start: current().start,
|
|
623
|
+
end: current().end,
|
|
624
|
+
});
|
|
625
|
+
advance();
|
|
626
|
+
skipTrivia();
|
|
627
|
+
if (current() && current().type === TokenType.COMMA) {
|
|
628
|
+
advance();
|
|
629
|
+
skipTrivia();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
pushStmt(
|
|
633
|
+
{
|
|
634
|
+
type: 'VarDeclaration',
|
|
635
|
+
originalKeyword: variableKeyword,
|
|
636
|
+
variables,
|
|
637
|
+
start,
|
|
638
|
+
end: variables.length > 0 ? variables.at(-1).end : start + 3,
|
|
639
|
+
},
|
|
640
|
+
hasBlankLine,
|
|
641
|
+
);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (t.type === TokenType.SET) {
|
|
646
|
+
const start = t.start;
|
|
647
|
+
const setKeyword = t.value;
|
|
648
|
+
advance();
|
|
649
|
+
skipTrivia();
|
|
650
|
+
let target = null;
|
|
651
|
+
if (current() && current().type === TokenType.VARIABLE) {
|
|
652
|
+
target = {
|
|
653
|
+
type: 'Variable',
|
|
654
|
+
value: current().value,
|
|
655
|
+
start: current().start,
|
|
656
|
+
end: current().end,
|
|
657
|
+
};
|
|
658
|
+
advance();
|
|
659
|
+
}
|
|
660
|
+
skipTrivia();
|
|
661
|
+
if (current() && current().type === TokenType.EQUALS) {
|
|
662
|
+
advance();
|
|
663
|
+
}
|
|
664
|
+
skipTrivia();
|
|
665
|
+
const value = parseExpression();
|
|
666
|
+
pushStmt(
|
|
667
|
+
{
|
|
668
|
+
type: 'SetStatement',
|
|
669
|
+
originalKeyword: setKeyword,
|
|
670
|
+
target,
|
|
671
|
+
value,
|
|
672
|
+
start,
|
|
673
|
+
end: value.end,
|
|
674
|
+
},
|
|
675
|
+
hasBlankLine,
|
|
676
|
+
);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (t.type === TokenType.IF) {
|
|
681
|
+
const stmt = parseIfStatement();
|
|
682
|
+
pushStmt(stmt, hasBlankLine);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (t.type === TokenType.FOR) {
|
|
687
|
+
const stmt = parseForStatement();
|
|
688
|
+
pushStmt(stmt, hasBlankLine);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (t.type === TokenType.IDENTIFIER || t.type === TokenType.VARIABLE) {
|
|
693
|
+
const expr = parseExpression();
|
|
694
|
+
pushStmt(
|
|
695
|
+
{ type: 'ExpressionStatement', expression: expr, start: expr.start, end: expr.end },
|
|
696
|
+
hasBlankLine,
|
|
697
|
+
);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (
|
|
702
|
+
t.type === TokenType.ENDIF ||
|
|
703
|
+
t.type === TokenType.ELSE ||
|
|
704
|
+
t.type === TokenType.ELSEIF ||
|
|
705
|
+
t.type === TokenType.NEXT ||
|
|
706
|
+
t.type === TokenType.THEN ||
|
|
707
|
+
t.type === TokenType.DO
|
|
708
|
+
) {
|
|
709
|
+
const kw = advance();
|
|
710
|
+
if (t.type === TokenType.ELSEIF) {
|
|
711
|
+
skipTrivia();
|
|
712
|
+
if (current() && current().type !== TokenType.BLOCK_CLOSE) {
|
|
713
|
+
parseExpression();
|
|
714
|
+
skipTrivia();
|
|
715
|
+
if (current() && current().type === TokenType.THEN) advance();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (t.type === TokenType.NEXT) {
|
|
719
|
+
skipTrivia();
|
|
720
|
+
if (current() && current().type === TokenType.VARIABLE) advance();
|
|
721
|
+
}
|
|
722
|
+
pushStmt(
|
|
723
|
+
{
|
|
724
|
+
type: 'RawStatement',
|
|
725
|
+
value: kw.value,
|
|
726
|
+
keyword: kw.value,
|
|
727
|
+
start: kw.start,
|
|
728
|
+
end: kw.end,
|
|
729
|
+
},
|
|
730
|
+
hasBlankLine,
|
|
731
|
+
);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
advance();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function parseIfStatement() {
|
|
739
|
+
const ifToken = current();
|
|
740
|
+
const start = ifToken.start;
|
|
741
|
+
advance();
|
|
742
|
+
skipTrivia();
|
|
743
|
+
const condition = parseExpression();
|
|
744
|
+
skipTrivia();
|
|
745
|
+
let thenKeyword = 'then';
|
|
746
|
+
if (current() && current().type === TokenType.THEN) {
|
|
747
|
+
thenKeyword = current().value;
|
|
748
|
+
advance();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const originalKeywords = { if: ifToken.value, then: thenKeyword };
|
|
752
|
+
const consequent = [];
|
|
753
|
+
const alternates = [];
|
|
754
|
+
let currentBlock = consequent;
|
|
755
|
+
|
|
756
|
+
while (pos < tokens.length) {
|
|
757
|
+
skipTrivia();
|
|
758
|
+
if (pos >= tokens.length) break;
|
|
759
|
+
|
|
760
|
+
const t = current();
|
|
761
|
+
|
|
762
|
+
if (t.type === TokenType.ENDIF) {
|
|
763
|
+
originalKeywords.endif = t.value;
|
|
764
|
+
const endToken = advance();
|
|
765
|
+
return {
|
|
766
|
+
type: 'IfStatement',
|
|
767
|
+
originalKeywords,
|
|
768
|
+
condition,
|
|
769
|
+
consequent,
|
|
770
|
+
alternates,
|
|
771
|
+
start,
|
|
772
|
+
end: endToken.end,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (t.type === TokenType.ELSEIF) {
|
|
777
|
+
const elseifStart = t.start;
|
|
778
|
+
const elseifKeyword = t.value;
|
|
779
|
+
advance();
|
|
780
|
+
skipTrivia();
|
|
781
|
+
const elseifCondition = parseExpression();
|
|
782
|
+
skipTrivia();
|
|
783
|
+
let elseifThenKeyword = 'then';
|
|
784
|
+
if (current() && current().type === TokenType.THEN) {
|
|
785
|
+
elseifThenKeyword = current().value;
|
|
786
|
+
advance();
|
|
787
|
+
}
|
|
788
|
+
currentBlock = [];
|
|
789
|
+
alternates.push({
|
|
790
|
+
type: 'ElseIfClause',
|
|
791
|
+
originalKeywords: { elseif: elseifKeyword, then: elseifThenKeyword },
|
|
792
|
+
condition: elseifCondition,
|
|
793
|
+
body: currentBlock,
|
|
794
|
+
start: elseifStart,
|
|
795
|
+
end: elseifCondition.end,
|
|
796
|
+
});
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (t.type === TokenType.ELSE) {
|
|
801
|
+
const elseStart = t.start;
|
|
802
|
+
const elseKeyword = t.value;
|
|
803
|
+
advance();
|
|
804
|
+
currentBlock = [];
|
|
805
|
+
alternates.push({
|
|
806
|
+
type: 'ElseClause',
|
|
807
|
+
originalKeywords: { else: elseKeyword },
|
|
808
|
+
body: currentBlock,
|
|
809
|
+
start: elseStart,
|
|
810
|
+
end: elseStart + 4,
|
|
811
|
+
});
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const innerStatements = parseInnerStatement();
|
|
816
|
+
if (innerStatements) {
|
|
817
|
+
currentBlock.push(innerStatements);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
type: 'IfStatement',
|
|
823
|
+
originalKeywords,
|
|
824
|
+
condition,
|
|
825
|
+
consequent,
|
|
826
|
+
alternates,
|
|
827
|
+
start,
|
|
828
|
+
end: pos < tokens.length ? tokens[pos - 1].end : start,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function parseForStatement() {
|
|
833
|
+
const forToken = current();
|
|
834
|
+
const start = forToken.start;
|
|
835
|
+
const originalKeywords = { for: forToken.value };
|
|
836
|
+
advance();
|
|
837
|
+
skipTrivia();
|
|
838
|
+
|
|
839
|
+
let counter = null;
|
|
840
|
+
if (current() && current().type === TokenType.VARIABLE) {
|
|
841
|
+
counter = {
|
|
842
|
+
type: 'Variable',
|
|
843
|
+
value: current().value,
|
|
844
|
+
start: current().start,
|
|
845
|
+
end: current().end,
|
|
846
|
+
};
|
|
847
|
+
advance();
|
|
848
|
+
}
|
|
849
|
+
skipTrivia();
|
|
850
|
+
if (current() && current().type === TokenType.EQUALS) {
|
|
851
|
+
advance();
|
|
852
|
+
}
|
|
853
|
+
skipTrivia();
|
|
854
|
+
const startExpr = parseExpression();
|
|
855
|
+
skipTrivia();
|
|
856
|
+
|
|
857
|
+
let direction = 'to';
|
|
858
|
+
if (current() && current().type === TokenType.DOWNTO) {
|
|
859
|
+
direction = 'downto';
|
|
860
|
+
originalKeywords.direction = current().value;
|
|
861
|
+
advance();
|
|
862
|
+
} else if (current() && current().type === TokenType.TO) {
|
|
863
|
+
originalKeywords.direction = current().value;
|
|
864
|
+
advance();
|
|
865
|
+
}
|
|
866
|
+
skipTrivia();
|
|
867
|
+
const endExpr = parseExpression();
|
|
868
|
+
skipTrivia();
|
|
869
|
+
if (current() && current().type === TokenType.DO) {
|
|
870
|
+
originalKeywords.do = current().value;
|
|
871
|
+
advance();
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const body = [];
|
|
875
|
+
while (pos < tokens.length) {
|
|
876
|
+
skipTrivia();
|
|
877
|
+
if (pos >= tokens.length) break;
|
|
878
|
+
|
|
879
|
+
const t = current();
|
|
880
|
+
if (t.type === TokenType.NEXT) {
|
|
881
|
+
originalKeywords.next = t.value;
|
|
882
|
+
const nextToken = advance();
|
|
883
|
+
skipTrivia();
|
|
884
|
+
if (current() && current().type === TokenType.VARIABLE) {
|
|
885
|
+
advance();
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
type: 'ForStatement',
|
|
889
|
+
originalKeywords,
|
|
890
|
+
counter,
|
|
891
|
+
startExpr,
|
|
892
|
+
endExpr,
|
|
893
|
+
direction,
|
|
894
|
+
body,
|
|
895
|
+
start,
|
|
896
|
+
end: nextToken.end,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const stmt = parseInnerStatement();
|
|
901
|
+
if (stmt) body.push(stmt);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return {
|
|
905
|
+
type: 'ForStatement',
|
|
906
|
+
originalKeywords,
|
|
907
|
+
counter,
|
|
908
|
+
startExpr,
|
|
909
|
+
endExpr,
|
|
910
|
+
direction,
|
|
911
|
+
body,
|
|
912
|
+
start,
|
|
913
|
+
end: pos < tokens.length ? tokens[pos - 1].end : start,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function parseInnerStatement() {
|
|
918
|
+
skipTrivia();
|
|
919
|
+
if (pos >= tokens.length) return null;
|
|
920
|
+
|
|
921
|
+
const t = current();
|
|
922
|
+
|
|
923
|
+
if (t.type === TokenType.COMMENT) {
|
|
924
|
+
advance();
|
|
925
|
+
return { type: 'Comment', value: t.value, start: t.start, end: t.end };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (t.type === TokenType.VAR) {
|
|
929
|
+
const start = t.start;
|
|
930
|
+
const variableKeyword = t.value;
|
|
931
|
+
advance();
|
|
932
|
+
skipTrivia();
|
|
933
|
+
const variables = [];
|
|
934
|
+
while (current() && current().type === TokenType.VARIABLE) {
|
|
935
|
+
variables.push({
|
|
936
|
+
type: 'Variable',
|
|
937
|
+
value: current().value,
|
|
938
|
+
start: current().start,
|
|
939
|
+
end: current().end,
|
|
940
|
+
});
|
|
941
|
+
advance();
|
|
942
|
+
skipTrivia();
|
|
943
|
+
if (current() && current().type === TokenType.COMMA) {
|
|
944
|
+
advance();
|
|
945
|
+
skipTrivia();
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
type: 'VarDeclaration',
|
|
950
|
+
originalKeyword: variableKeyword,
|
|
951
|
+
variables,
|
|
952
|
+
start,
|
|
953
|
+
end: variables.length > 0 ? variables.at(-1).end : start + 3,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (t.type === TokenType.SET) {
|
|
958
|
+
const start = t.start;
|
|
959
|
+
const setKeyword = t.value;
|
|
960
|
+
advance();
|
|
961
|
+
skipTrivia();
|
|
962
|
+
let target = null;
|
|
963
|
+
if (current() && current().type === TokenType.VARIABLE) {
|
|
964
|
+
target = {
|
|
965
|
+
type: 'Variable',
|
|
966
|
+
value: current().value,
|
|
967
|
+
start: current().start,
|
|
968
|
+
end: current().end,
|
|
969
|
+
};
|
|
970
|
+
advance();
|
|
971
|
+
}
|
|
972
|
+
skipTrivia();
|
|
973
|
+
if (current() && current().type === TokenType.EQUALS) {
|
|
974
|
+
advance();
|
|
975
|
+
}
|
|
976
|
+
skipTrivia();
|
|
977
|
+
const value = parseExpression();
|
|
978
|
+
return {
|
|
979
|
+
type: 'SetStatement',
|
|
980
|
+
originalKeyword: setKeyword,
|
|
981
|
+
target,
|
|
982
|
+
value,
|
|
983
|
+
start,
|
|
984
|
+
end: value.end,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (t.type === TokenType.IF) {
|
|
989
|
+
return parseIfStatement();
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (t.type === TokenType.FOR) {
|
|
993
|
+
return parseForStatement();
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (
|
|
997
|
+
t.type === TokenType.IDENTIFIER ||
|
|
998
|
+
t.type === TokenType.VARIABLE ||
|
|
999
|
+
t.type === TokenType.LPAREN
|
|
1000
|
+
) {
|
|
1001
|
+
const expr = parseExpression();
|
|
1002
|
+
return {
|
|
1003
|
+
type: 'ExpressionStatement',
|
|
1004
|
+
expression: expr,
|
|
1005
|
+
start: expr.start,
|
|
1006
|
+
end: expr.end,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
advance();
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return statements;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// ── Top-level parser ─────────────────────────────────────────────────────────
|
|
1018
|
+
|
|
1019
|
+
function parse(text) {
|
|
1020
|
+
const children = [];
|
|
1021
|
+
let index = 0;
|
|
1022
|
+
let contentStart = 0;
|
|
1023
|
+
|
|
1024
|
+
function pushContent(end) {
|
|
1025
|
+
if (end > contentStart) {
|
|
1026
|
+
children.push({
|
|
1027
|
+
type: 'Content',
|
|
1028
|
+
value: text.slice(contentStart, end),
|
|
1029
|
+
start: contentStart,
|
|
1030
|
+
end,
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const scriptOpenRe =
|
|
1036
|
+
/^<script\b(?=[^>]*\brunat\s*=\s*['"]server['"])(?=[^>]*\blanguage\s*=\s*['"]ampscript['"])[^>]*>/i;
|
|
1037
|
+
const scriptCloseRe = /^<\/script\s*>/i;
|
|
1038
|
+
|
|
1039
|
+
while (index < text.length) {
|
|
1040
|
+
if (text[index] === '<') {
|
|
1041
|
+
const slice = text.slice(index);
|
|
1042
|
+
const openMatch = scriptOpenRe.exec(slice);
|
|
1043
|
+
if (openMatch) {
|
|
1044
|
+
pushContent(index);
|
|
1045
|
+
const blockStart = index;
|
|
1046
|
+
const openTag = openMatch[0];
|
|
1047
|
+
index += openTag.length;
|
|
1048
|
+
const codeStart = index;
|
|
1049
|
+
|
|
1050
|
+
while (index < text.length) {
|
|
1051
|
+
if (text[index] === '<') {
|
|
1052
|
+
const closeMatch = scriptCloseRe.exec(text.slice(index));
|
|
1053
|
+
if (closeMatch) break;
|
|
1054
|
+
}
|
|
1055
|
+
index++;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const codeEnd = index;
|
|
1059
|
+
const code = text.slice(codeStart, codeEnd);
|
|
1060
|
+
const closeMatch = scriptCloseRe.exec(text.slice(index));
|
|
1061
|
+
if (closeMatch) {
|
|
1062
|
+
index += closeMatch[0].length;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const tokens = tokenizeBlock(code, codeStart);
|
|
1066
|
+
const stmts = parseStatements(tokens);
|
|
1067
|
+
markPrettierIgnore(stmts);
|
|
1068
|
+
|
|
1069
|
+
children.push({
|
|
1070
|
+
type: 'Block',
|
|
1071
|
+
syntax: 'script-tag',
|
|
1072
|
+
statements: stmts,
|
|
1073
|
+
start: blockStart,
|
|
1074
|
+
end: index,
|
|
1075
|
+
});
|
|
1076
|
+
contentStart = index;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (text[index] === '%' && text[index + 1] === '%' && text[index + 2] === '[') {
|
|
1082
|
+
pushContent(index);
|
|
1083
|
+
const blockStart = index;
|
|
1084
|
+
index += 3;
|
|
1085
|
+
const codeStart = index;
|
|
1086
|
+
|
|
1087
|
+
let depth = 1;
|
|
1088
|
+
while (index < text.length) {
|
|
1089
|
+
if (text[index] === '%' && text[index + 1] === '%' && text[index + 2] === '[') {
|
|
1090
|
+
depth++;
|
|
1091
|
+
index += 3;
|
|
1092
|
+
} else if (
|
|
1093
|
+
text[index] === ']' &&
|
|
1094
|
+
text[index + 1] === '%' &&
|
|
1095
|
+
text[index + 2] === '%'
|
|
1096
|
+
) {
|
|
1097
|
+
depth--;
|
|
1098
|
+
if (depth === 0) break;
|
|
1099
|
+
index += 3;
|
|
1100
|
+
} else {
|
|
1101
|
+
index++;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const codeEnd = index;
|
|
1106
|
+
const code = text.slice(codeStart, codeEnd);
|
|
1107
|
+
index += 3;
|
|
1108
|
+
|
|
1109
|
+
const tokens = tokenizeBlock(code, codeStart);
|
|
1110
|
+
const stmts = parseStatements(tokens);
|
|
1111
|
+
markPrettierIgnore(stmts);
|
|
1112
|
+
|
|
1113
|
+
children.push({ type: 'Block', statements: stmts, start: blockStart, end: index });
|
|
1114
|
+
contentStart = index;
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (text[index] === '%' && text[index + 1] === '%' && text[index + 2] === '=') {
|
|
1119
|
+
pushContent(index);
|
|
1120
|
+
const inlineStart = index;
|
|
1121
|
+
index += 3;
|
|
1122
|
+
const codeStart = index;
|
|
1123
|
+
|
|
1124
|
+
while (index < text.length) {
|
|
1125
|
+
if (text[index] === '=' && text[index + 1] === '%' && text[index + 2] === '%') {
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
index++;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const codeEnd = index;
|
|
1132
|
+
const code = text.slice(codeStart, codeEnd);
|
|
1133
|
+
index += 3;
|
|
1134
|
+
|
|
1135
|
+
const tokens = tokenizeBlock(code, codeStart);
|
|
1136
|
+
const exprStatements = parseStatements(tokens);
|
|
1137
|
+
markPrettierIgnore(exprStatements);
|
|
1138
|
+
const expression = exprStatements.length > 0 ? exprStatements[0] : null;
|
|
1139
|
+
|
|
1140
|
+
const expr =
|
|
1141
|
+
expression && expression.type === 'ExpressionStatement'
|
|
1142
|
+
? expression.expression
|
|
1143
|
+
: expression;
|
|
1144
|
+
|
|
1145
|
+
children.push({
|
|
1146
|
+
type: 'InlineExpression',
|
|
1147
|
+
expression: expr,
|
|
1148
|
+
start: inlineStart,
|
|
1149
|
+
end: index,
|
|
1150
|
+
});
|
|
1151
|
+
contentStart = index;
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
index++;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
pushContent(text.length);
|
|
1159
|
+
markPrettierIgnore(children);
|
|
1160
|
+
|
|
1161
|
+
return { type: 'Document', children, start: 0, end: text.length };
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export { parse, tokenizeBlock, parseStatements, TokenType };
|