sommark 4.5.3 → 5.0.1
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 +315 -179
- package/cli/cli.mjs +1 -1
- package/cli/commands/color.js +36 -14
- package/cli/commands/help.js +3 -0
- package/cli/commands/init.js +1 -3
- package/cli/constants.js +5 -2
- package/constants/html_props.js +0 -5
- package/core/errors.js +5 -4
- package/core/evaluator.js +1 -2
- package/core/formats.js +7 -1
- package/core/helpers/config-loader.js +2 -4
- package/core/helpers/lib.js +1 -1
- package/core/labels.js +2 -15
- package/core/lexer.js +197 -313
- package/core/modules.js +13 -13
- package/core/parser.js +226 -535
- package/core/tokenTypes.js +6 -15
- package/core/transpiler.js +129 -110
- package/core/validator.js +6 -26
- package/dist/sommark.browser.js +1781 -2172
- package/dist/sommark.browser.lite.js +1779 -2169
- package/dist/sommark.lexer.js +392 -544
- package/dist/sommark.parser.js +604 -1200
- package/formatter/mark.js +34 -0
- package/formatter/tag.js +7 -33
- package/helpers/utils.js +15 -16
- package/index.js +9 -1
- package/index.shared.js +26 -16
- package/mappers/languages/csv.js +62 -0
- package/mappers/languages/html.js +12 -66
- package/mappers/languages/json.js +74 -156
- package/mappers/languages/jsonc.js +21 -63
- package/mappers/languages/markdown.js +159 -276
- package/mappers/languages/mdx.js +7 -62
- package/mappers/languages/text.js +2 -19
- package/mappers/languages/toml.js +231 -0
- package/mappers/languages/xml.js +25 -25
- package/mappers/languages/yaml.js +323 -0
- package/mappers/mapper.js +1 -22
- package/mappers/shared/index.js +3 -16
- package/package.json +5 -2
package/core/parser.js
CHANGED
|
@@ -4,12 +4,9 @@
|
|
|
4
4
|
import TOKEN_TYPES from "./tokenTypes.js";
|
|
5
5
|
import peek from "../helpers/peek.js";
|
|
6
6
|
import { parserError } from "./errors.js";
|
|
7
|
-
import { safeDataParse } from "../helpers/safeDataParser.js";
|
|
8
7
|
import {
|
|
9
8
|
BLOCK,
|
|
10
9
|
TEXT,
|
|
11
|
-
INLINE,
|
|
12
|
-
ATBLOCK,
|
|
13
10
|
COMMENT,
|
|
14
11
|
COMMENT_BLOCK,
|
|
15
12
|
STATIC_LOGIC,
|
|
@@ -19,11 +16,6 @@ import {
|
|
|
19
16
|
block_id,
|
|
20
17
|
block_key,
|
|
21
18
|
block_value,
|
|
22
|
-
inline_id,
|
|
23
|
-
inline_text,
|
|
24
|
-
at_id,
|
|
25
|
-
atblock_key,
|
|
26
|
-
at_value,
|
|
27
19
|
end_keyword,
|
|
28
20
|
SLOT,
|
|
29
21
|
slot_keyword,
|
|
@@ -93,7 +85,7 @@ function validateName(
|
|
|
93
85
|
: "must contain only letters, numbers, hyphens, underscores, or dollar signs ($)";
|
|
94
86
|
|
|
95
87
|
if (!keyRegex.test(id)) {
|
|
96
|
-
parserError([`{line}<$red:Invalid ${name}:$><$blue: '${id}'$>{N}<$yellow:${name} ${ruleMessage}$> <$cyan: ${rule}
|
|
88
|
+
parserError([`{line}<$red:Invalid ${name}:$><$blue: '${id}'$>{N}<$yellow:${name} ${ruleMessage}$> <$cyan: ${rule}.$>`]);
|
|
97
89
|
}
|
|
98
90
|
}
|
|
99
91
|
|
|
@@ -103,7 +95,7 @@ function makeBlockNode() {
|
|
|
103
95
|
type: BLOCK,
|
|
104
96
|
structure: "Block",
|
|
105
97
|
id: "",
|
|
106
|
-
|
|
98
|
+
props: {},
|
|
107
99
|
body: [],
|
|
108
100
|
depth: 0,
|
|
109
101
|
range: {
|
|
@@ -138,41 +130,6 @@ function makeCommentNode() {
|
|
|
138
130
|
}
|
|
139
131
|
};
|
|
140
132
|
}
|
|
141
|
-
/** Creates a new empty Inline node. */
|
|
142
|
-
function makeInlineNode() {
|
|
143
|
-
return {
|
|
144
|
-
type: INLINE,
|
|
145
|
-
structure: "Inline",
|
|
146
|
-
value: "",
|
|
147
|
-
id: "",
|
|
148
|
-
args: {},
|
|
149
|
-
depth: 0,
|
|
150
|
-
range: {
|
|
151
|
-
start: { line: 0, character: 0 },
|
|
152
|
-
end: { line: 0, character: 0 }
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ========================================================================== //
|
|
158
|
-
// Node Creators //
|
|
159
|
-
// ========================================================================== //
|
|
160
|
-
/** Creates a new empty AtBlock node. */
|
|
161
|
-
function makeAtBlockNode() {
|
|
162
|
-
return {
|
|
163
|
-
type: ATBLOCK,
|
|
164
|
-
structure: "AtBlock",
|
|
165
|
-
id: "",
|
|
166
|
-
args: {},
|
|
167
|
-
content: "",
|
|
168
|
-
depth: 0,
|
|
169
|
-
range: {
|
|
170
|
-
start: { line: 0, character: 0 },
|
|
171
|
-
end: { line: 0, character: 0 }
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
133
|
/** Creates a new empty Logic node. */
|
|
177
134
|
function makeLogicNode(type = RUNTIME_LOGIC) {
|
|
178
135
|
return {
|
|
@@ -218,53 +175,66 @@ const updateData = (tokens, i) => {
|
|
|
218
175
|
|
|
219
176
|
const errorMessage = (tokens, i, expectedValue, behindValue, frontText, filename = null) => {
|
|
220
177
|
const current = tokens[i] || fallback;
|
|
221
|
-
const
|
|
222
|
-
const
|
|
178
|
+
const errorLine = current.range.start.line;
|
|
179
|
+
const errorColStart = current.range.start.character;
|
|
180
|
+
const errorColEnd = current.range.end.character;
|
|
223
181
|
const source = current.source || filename;
|
|
224
|
-
const sourceLabel = source ? ` [${source}]` : "";
|
|
225
182
|
|
|
183
|
+
// Collect all tokens on the error line for the source snippet
|
|
226
184
|
let lineStartIndex = i;
|
|
227
185
|
while (
|
|
228
186
|
lineStartIndex > 0 &&
|
|
229
187
|
tokens[lineStartIndex - 1] &&
|
|
230
|
-
tokens[lineStartIndex - 1].range.start.line ===
|
|
188
|
+
tokens[lineStartIndex - 1].range.start.line === errorLine &&
|
|
231
189
|
(tokens[lineStartIndex - 1].source || filename) === source
|
|
232
190
|
) {
|
|
233
191
|
lineStartIndex--;
|
|
234
192
|
}
|
|
235
|
-
|
|
236
193
|
let lineEndIndex = i;
|
|
237
194
|
while (
|
|
238
195
|
lineEndIndex < tokens.length - 1 &&
|
|
239
196
|
tokens[lineEndIndex + 1] &&
|
|
240
|
-
tokens[lineEndIndex + 1].range.start.line ===
|
|
197
|
+
tokens[lineEndIndex + 1].range.start.line === errorLine &&
|
|
241
198
|
(tokens[lineEndIndex + 1].source || filename) === source
|
|
242
199
|
) {
|
|
243
200
|
lineEndIndex++;
|
|
244
201
|
}
|
|
245
202
|
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
`<$
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
`
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
203
|
+
const lineContent = tokens.slice(lineStartIndex, lineEndIndex + 1).map(t => t.value).join('');
|
|
204
|
+
const contentBefore = tokens.slice(lineStartIndex, i).map(t => t.value).join('');
|
|
205
|
+
const pointerPadding = " ".repeat(contentBefore.length);
|
|
206
|
+
|
|
207
|
+
// Location header — file, line, column
|
|
208
|
+
const lineNum = errorLine + 1;
|
|
209
|
+
const isMultiLine = current.range.start.line !== current.range.end.line;
|
|
210
|
+
const colDisplay = isMultiLine
|
|
211
|
+
? `${errorColStart} → line ${current.range.end.line + 1} col ${errorColEnd}`
|
|
212
|
+
: errorColStart === errorColEnd ? `${errorColStart}` : `${errorColStart}–${errorColEnd}`;
|
|
213
|
+
|
|
214
|
+
// Error description — avoid nested <$color:...$> tags (breaks the non-greedy regex)
|
|
215
|
+
let errorDesc;
|
|
216
|
+
if (frontText) {
|
|
217
|
+
errorDesc = `<$red:${frontText}$>`;
|
|
218
|
+
} else {
|
|
219
|
+
errorDesc = `<$red:Expected$> <$blue:'${expectedValue}'$>`;
|
|
220
|
+
if (behindValue) errorDesc += ` <$red:after$> <$blue:'${behindValue}'$>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const tokenDisplay = current.value === "" ? "end of input"
|
|
224
|
+
: current.value === "\n" ? "newline (\\n)"
|
|
225
|
+
: `'${current.value}'`;
|
|
226
|
+
|
|
227
|
+
const parts = [`{line}`];
|
|
228
|
+
if (source) parts.push(`<$cyan:File:$> ${source}{N}`);
|
|
229
|
+
parts.push(`<$cyan:Line:$> <$yellow:${lineNum}$> <$cyan:Col:$> <$yellow:${colDisplay}$>{N}`);
|
|
230
|
+
parts.push(`{line}`);
|
|
231
|
+
parts.push(`<$red:Here where error occurred:$>{N}`);
|
|
232
|
+
parts.push(` ${lineContent}{N}`);
|
|
233
|
+
parts.push(` ${pointerPadding}<$yellow:^$>{N}`);
|
|
234
|
+
parts.push(`${errorDesc}{N}`);
|
|
235
|
+
parts.push(`<$yellow:Received:$> <$blue:${tokenDisplay}$>{N}`);
|
|
236
|
+
parts.push(`{line}`);
|
|
237
|
+
return parts;
|
|
268
238
|
};
|
|
269
239
|
// ========================================================================== //
|
|
270
240
|
// Parse Key //
|
|
@@ -286,6 +256,88 @@ function parseKey(tokens, i) {
|
|
|
286
256
|
return [key, i];
|
|
287
257
|
}
|
|
288
258
|
// ========================================================================== //
|
|
259
|
+
// Read Prefix Key/Fallback from structured p{}/v{} tokens //
|
|
260
|
+
// ========================================================================== //
|
|
261
|
+
function readPrefixKeyFallback(tokens, i, prefixType = "p") {
|
|
262
|
+
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.PREFIX_OPEN) i++;
|
|
263
|
+
i = skipJunk(tokens, i);
|
|
264
|
+
|
|
265
|
+
let key = "";
|
|
266
|
+
let fallback = undefined;
|
|
267
|
+
|
|
268
|
+
// Read key — must be quoted or unquoted identifier
|
|
269
|
+
const keyToken = current_token(tokens, i);
|
|
270
|
+
if (!keyToken || keyToken.type === TOKEN_TYPES.PREFIX_CLOSE) {
|
|
271
|
+
parserError(errorMessage(tokens, i, "key", "{", 'Prefix requires a key — write p{key} or p{key | "fallback"}'));
|
|
272
|
+
}
|
|
273
|
+
if (keyToken.type === TOKEN_TYPES.QUOTE) {
|
|
274
|
+
i++; // skip opening QUOTE
|
|
275
|
+
while (current_token(tokens, i) &&
|
|
276
|
+
current_token(tokens, i).type !== TOKEN_TYPES.QUOTE &&
|
|
277
|
+
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_CLOSE &&
|
|
278
|
+
current_token(tokens, i).type !== TOKEN_TYPES.PIPELINE) {
|
|
279
|
+
key += current_token(tokens, i).value;
|
|
280
|
+
i++;
|
|
281
|
+
}
|
|
282
|
+
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.QUOTE) i++;
|
|
283
|
+
} else if (keyToken.type === TOKEN_TYPES.KEY) {
|
|
284
|
+
key = keyToken.value.trim();
|
|
285
|
+
const isValidIdent = /^[a-zA-Z_$][a-zA-Z0-9_$-]*$/.test(key);
|
|
286
|
+
const isNumeric = /^\d+$/.test(key);
|
|
287
|
+
// p{} keys must be identifiers; v{} keys may also be positional integers
|
|
288
|
+
if (!isValidIdent && !(prefixType === "v" && isNumeric)) {
|
|
289
|
+
parserError(errorMessage(tokens, i, "key", "{", `Invalid prefix key '${key}' — must start with a letter, _ or $`));
|
|
290
|
+
}
|
|
291
|
+
i++;
|
|
292
|
+
} else {
|
|
293
|
+
parserError(errorMessage(tokens, i, "key", "{", "Invalid prefix key — must be a quoted string or identifier"));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
i = skipJunk(tokens, i);
|
|
297
|
+
|
|
298
|
+
// After key: only | or } is valid
|
|
299
|
+
const afterKey = current_token(tokens, i);
|
|
300
|
+
if (!afterKey || (afterKey.type !== TOKEN_TYPES.PIPELINE && afterKey.type !== TOKEN_TYPES.PREFIX_CLOSE)) {
|
|
301
|
+
parserError(errorMessage(tokens, i, "| or }", key, "Expected '|' or '}' after prefix key"));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.PIPELINE) {
|
|
305
|
+
i++; // skip PIPELINE
|
|
306
|
+
i = skipJunk(tokens, i);
|
|
307
|
+
|
|
308
|
+
// Fallback must be a quoted string — any content allowed inside quotes
|
|
309
|
+
const fallbackToken = current_token(tokens, i);
|
|
310
|
+
if (!fallbackToken || fallbackToken.type === TOKEN_TYPES.PREFIX_CLOSE) {
|
|
311
|
+
parserError(errorMessage(tokens, i, '"fallback"', "|", 'Expected a quoted fallback after \'|\' — write p{key | "default"}'));
|
|
312
|
+
}
|
|
313
|
+
if (fallbackToken.type === TOKEN_TYPES.QUOTE) {
|
|
314
|
+
fallback = "";
|
|
315
|
+
i++; // skip opening QUOTE
|
|
316
|
+
while (current_token(tokens, i) &&
|
|
317
|
+
current_token(tokens, i).type !== TOKEN_TYPES.QUOTE &&
|
|
318
|
+
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_CLOSE) {
|
|
319
|
+
fallback += current_token(tokens, i).value;
|
|
320
|
+
i++;
|
|
321
|
+
}
|
|
322
|
+
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.QUOTE) i++;
|
|
323
|
+
} else {
|
|
324
|
+
parserError(errorMessage(tokens, i, '"fallback"', "|", 'Fallback must be a quoted string — write p{key | "default"}'));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
i = skipJunk(tokens, i);
|
|
329
|
+
|
|
330
|
+
// After key (or fallback): only } is valid
|
|
331
|
+
const afterFallback = current_token(tokens, i);
|
|
332
|
+
if (!afterFallback || afterFallback.type !== TOKEN_TYPES.PREFIX_CLOSE) {
|
|
333
|
+
parserError(errorMessage(tokens, i, "}", key, "Unexpected content inside prefix — only one key and one optional fallback are allowed"));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.PREFIX_CLOSE) i++;
|
|
337
|
+
|
|
338
|
+
return [key, fallback, i];
|
|
339
|
+
}
|
|
340
|
+
// ========================================================================== //
|
|
289
341
|
// Parse Value //
|
|
290
342
|
// ========================================================================== //
|
|
291
343
|
function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = true) {
|
|
@@ -296,7 +348,7 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
296
348
|
val = "";
|
|
297
349
|
while (i < tokens.length && current_token(tokens, i).type !== TOKEN_TYPES.QUOTE) {
|
|
298
350
|
const token = current_token(tokens, i);
|
|
299
|
-
if (token.type === TOKEN_TYPES.PREFIX_P || token.type === TOKEN_TYPES.
|
|
351
|
+
if (token.type === TOKEN_TYPES.PREFIX_P || token.type === TOKEN_TYPES.PREFIX_V) {
|
|
300
352
|
const [resolvedVal, nextI] = parseValue(tokens, i, placeholders, variables, allowLogic);
|
|
301
353
|
val += resolvedVal;
|
|
302
354
|
i = nextI;
|
|
@@ -312,72 +364,55 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
312
364
|
|
|
313
365
|
i++; // consume closing QUOTE
|
|
314
366
|
return [val, i, true];
|
|
315
|
-
} else if (current_token(tokens, i).type === TOKEN_TYPES.
|
|
316
|
-
val = current_token(tokens, i).value;
|
|
317
|
-
// V4 NATIVE DATA: Strip js{ } and parse safely
|
|
318
|
-
if (val.startsWith("js{") && val.endsWith("}")) {
|
|
319
|
-
const clean = val.slice(3, -1).trim();
|
|
320
|
-
val = safeDataParse(clean);
|
|
321
|
-
}
|
|
322
|
-
i++;
|
|
323
|
-
return [val, i, false];
|
|
324
|
-
} else if (current_token(tokens, i).type === TOKEN_TYPES.LOGIC || current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD || current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD) {
|
|
367
|
+
} else if (current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD || current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD) {
|
|
325
368
|
if (!allowLogic) {
|
|
326
369
|
parserError(errorMessage(tokens, i, "literal value", "", "Logic blocks are not allowed in this context."));
|
|
327
370
|
}
|
|
328
371
|
let isStatic = current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD;
|
|
329
|
-
let
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (!current_token(tokens, nextI) || current_token(tokens, nextI).type !== TOKEN_TYPES.LOGIC) {
|
|
335
|
-
// Treat as literal text if keyword is not followed by a logic block
|
|
336
|
-
return [current_token(tokens, i).value, i + 1, false];
|
|
337
|
-
}
|
|
338
|
-
i = nextI;
|
|
372
|
+
let nextI = skipJunk(tokens, i + 1);
|
|
373
|
+
|
|
374
|
+
if (!current_token(tokens, nextI) || current_token(tokens, nextI).type !== TOKEN_TYPES.LOGIC_OPEN) {
|
|
375
|
+
// Keyword not followed by ${ — treat as literal text
|
|
376
|
+
return [current_token(tokens, i).value, i + 1, false];
|
|
339
377
|
}
|
|
340
378
|
|
|
341
|
-
|
|
379
|
+
// Skip LOGIC_OPEN, read LOGIC body
|
|
380
|
+
nextI++;
|
|
381
|
+
const logicToken = current_token(tokens, nextI);
|
|
342
382
|
const node = makeLogicNode(isStatic ? STATIC_LOGIC : RUNTIME_LOGIC);
|
|
343
|
-
node.code = logicToken.value;
|
|
344
|
-
node.range = logicToken.range;
|
|
383
|
+
node.code = logicToken ? logicToken.value : "";
|
|
384
|
+
node.range = logicToken ? logicToken.range : current_token(tokens, i).range;
|
|
385
|
+
nextI++;
|
|
386
|
+
|
|
387
|
+
// Consume LOGIC_CLOSE if present
|
|
388
|
+
if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
|
|
389
|
+
nextI++;
|
|
390
|
+
}
|
|
345
391
|
|
|
346
|
-
return [node,
|
|
392
|
+
return [node, nextI, false];
|
|
347
393
|
} else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_V) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
variables.__consumed__.add(key);
|
|
362
|
-
} else {
|
|
363
|
-
val = getPrefixValue('v', key);
|
|
394
|
+
i++; // consume PREFIX_V keyword
|
|
395
|
+
const [vKey, vFallback, vNextI] = readPrefixKeyFallback(tokens, i, "v");
|
|
396
|
+
i = vNextI;
|
|
397
|
+
if (variables[vKey] !== undefined) {
|
|
398
|
+
val = variables[vKey];
|
|
399
|
+
if (!variables.__consumed__) {
|
|
400
|
+
Object.defineProperty(variables, "__consumed__", {
|
|
401
|
+
value: new Set(),
|
|
402
|
+
enumerable: false,
|
|
403
|
+
configurable: true
|
|
404
|
+
});
|
|
364
405
|
}
|
|
406
|
+
variables.__consumed__.add(vKey);
|
|
407
|
+
} else {
|
|
408
|
+
val = vFallback !== undefined ? vFallback : getPrefixValue('v', vKey);
|
|
365
409
|
}
|
|
366
|
-
i++;
|
|
367
|
-
return [val, i, false];
|
|
368
|
-
} else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_C) {
|
|
369
|
-
val = current_token(tokens, i).value;
|
|
370
|
-
// PREFIX_C is preserved for the resolveModules expansion phase
|
|
371
|
-
i++;
|
|
372
410
|
return [val, i, false];
|
|
373
411
|
} else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
val = placeholders[key] !== undefined ? placeholders[key] : getPrefixValue('p', key);
|
|
379
|
-
}
|
|
380
|
-
i++;
|
|
412
|
+
i++; // consume PREFIX_P keyword
|
|
413
|
+
const [pKey, pFallback, pNextI] = readPrefixKeyFallback(tokens, i);
|
|
414
|
+
i = pNextI;
|
|
415
|
+
val = placeholders[pKey] !== undefined ? placeholders[pKey] : (pFallback !== undefined ? pFallback : getPrefixValue('p', pKey));
|
|
381
416
|
return [val, i, false];
|
|
382
417
|
} else {
|
|
383
418
|
val = "";
|
|
@@ -390,9 +425,7 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
390
425
|
token.type === TOKEN_TYPES.COMMA ||
|
|
391
426
|
token.type === TOKEN_TYPES.CLOSE_BRACKET ||
|
|
392
427
|
token.type === TOKEN_TYPES.COLON ||
|
|
393
|
-
token.type === TOKEN_TYPES.
|
|
394
|
-
token.type === TOKEN_TYPES.EXCLAMATION_MARK ||
|
|
395
|
-
token.type === TOKEN_TYPES.CLOSE_PAREN) break;
|
|
428
|
+
token.type === TOKEN_TYPES.EXCLAMATION_MARK) break;
|
|
396
429
|
|
|
397
430
|
if (token.type === TOKEN_TYPES.ESCAPE) {
|
|
398
431
|
// Remove backslash
|
|
@@ -425,7 +458,6 @@ function parseComma(tokens, i, beforeChar = "") {
|
|
|
425
458
|
current_token(tokens, i).type !== TOKEN_TYPES.IDENTIFIER &&
|
|
426
459
|
current_token(tokens, i).type !== TOKEN_TYPES.KEY &&
|
|
427
460
|
current_token(tokens, i).type !== TOKEN_TYPES.QUOTE &&
|
|
428
|
-
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_JS &&
|
|
429
461
|
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_P)
|
|
430
462
|
) {
|
|
431
463
|
parserError(errorMessage(tokens, i, "value", ","));
|
|
@@ -460,19 +492,6 @@ function parseColon(tokens, i, afterChar = "") {
|
|
|
460
492
|
updateData(tokens, i);
|
|
461
493
|
return i;
|
|
462
494
|
}
|
|
463
|
-
// ========================================================================== //
|
|
464
|
-
// Parse ';' //
|
|
465
|
-
// ========================================================================== //
|
|
466
|
-
function parseSemiColon(tokens, i, afterChar = "") {
|
|
467
|
-
i = skipJunk(tokens, i);
|
|
468
|
-
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.SEMICOLON)) {
|
|
469
|
-
parserError(errorMessage(tokens, i, ";", afterChar));
|
|
470
|
-
}
|
|
471
|
-
i++;
|
|
472
|
-
i = skipJunk(tokens, i);
|
|
473
|
-
updateData(tokens, i);
|
|
474
|
-
return i;
|
|
475
|
-
}
|
|
476
495
|
/**
|
|
477
496
|
* Parses a standard SomMark Block ([id] ... [end]).
|
|
478
497
|
* Blocks are structural elements that can contain nested content.
|
|
@@ -495,7 +514,7 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
495
514
|
updateData(tokens, i);
|
|
496
515
|
|
|
497
516
|
const idToken = current_token(tokens, i);
|
|
498
|
-
if (!idToken || idToken.type === TOKEN_TYPES.EOF) {
|
|
517
|
+
if (!idToken || idToken.type === TOKEN_TYPES.EOF || idToken.type === TOKEN_TYPES.CLOSE_BRACKET) {
|
|
499
518
|
parserError(errorMessage(tokens, i, "Block ID", "[", "Missing Block Identifier"));
|
|
500
519
|
}
|
|
501
520
|
const id = idToken.value;
|
|
@@ -547,10 +566,9 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
547
566
|
current_token(tokens, i).type !== TOKEN_TYPES.END_KEYWORD &&
|
|
548
567
|
current_token(tokens, i).type !== TOKEN_TYPES.KEY &&
|
|
549
568
|
current_token(tokens, i).type !== TOKEN_TYPES.QUOTE &&
|
|
550
|
-
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_JS &&
|
|
551
569
|
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_V &&
|
|
552
570
|
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_P &&
|
|
553
|
-
current_token(tokens, i).type !== TOKEN_TYPES.
|
|
571
|
+
current_token(tokens, i).type !== TOKEN_TYPES.LOGIC_OPEN &&
|
|
554
572
|
current_token(tokens, i).type !== TOKEN_TYPES.STATIC_KEYWORD &&
|
|
555
573
|
current_token(tokens, i).type !== TOKEN_TYPES.RUNTIME_KEYWORD)
|
|
556
574
|
) {
|
|
@@ -597,9 +615,9 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
597
615
|
i = valueIndex;
|
|
598
616
|
|
|
599
617
|
// Store Argument
|
|
600
|
-
blockNode.
|
|
618
|
+
blockNode.props[String(argIndex++)] = v;
|
|
601
619
|
if (k) {
|
|
602
|
-
blockNode.
|
|
620
|
+
blockNode.props[k] = v;
|
|
603
621
|
}
|
|
604
622
|
k = "";
|
|
605
623
|
v = "";
|
|
@@ -700,6 +718,23 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
700
718
|
i++;
|
|
701
719
|
i = skipJunk(tokens, i);
|
|
702
720
|
updateData(tokens, i);
|
|
721
|
+
|
|
722
|
+
// Named closing: [end:blockname] — the lexer emits END_KEYWORD "end:name" as one
|
|
723
|
+
// token because ':' is stripped from stop chars at block-start (XML namespace support).
|
|
724
|
+
const endValue = current.value.trim();
|
|
725
|
+
if (endValue.includes(":")) {
|
|
726
|
+
const closingName = endValue.slice(endValue.indexOf(":") + 1);
|
|
727
|
+
if (!closingName) {
|
|
728
|
+
parserError(errorMessage(tokens, i - 1, "block name", "", "Missing block name — write [end:blockname] to name the closing tag"));
|
|
729
|
+
}
|
|
730
|
+
const expected = end_stack[end_stack.length - 1];
|
|
731
|
+
if (expected && closingName !== expected.id) {
|
|
732
|
+
parserError(errorMessage(tokens, i - 1, closingName, "",
|
|
733
|
+
`Mismatched closing tag: [end:${closingName}] cannot close '${closingName}' — '${expected.id}' is still open (opened at line ${expected.line}, col ${expected.col})`
|
|
734
|
+
));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
703
738
|
if (
|
|
704
739
|
!current_token(tokens, i) ||
|
|
705
740
|
(current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_BRACKET)
|
|
@@ -734,147 +769,6 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
734
769
|
}
|
|
735
770
|
return [blockNode, i];
|
|
736
771
|
}
|
|
737
|
-
/**
|
|
738
|
-
* Parses an Inline Statement ((content) -> (id)).
|
|
739
|
-
* Inlines are fast, non-nesting formatting elements.
|
|
740
|
-
*
|
|
741
|
-
* @param {Object[]} tokens - Token stream.
|
|
742
|
-
* @param {number} i - Initial index.
|
|
743
|
-
* @param {Object} placeholders - Dynamic public API data.
|
|
744
|
-
* @returns {[Object, number]} The parsed Inline node and new index.
|
|
745
|
-
*/
|
|
746
|
-
function parseInline(tokens, i, placeholders = {}, depth = 0) {
|
|
747
|
-
const inlineNode = makeInlineNode();
|
|
748
|
-
inlineNode.depth = depth;
|
|
749
|
-
const openParenToken = current_token(tokens, i);
|
|
750
|
-
inlineNode.range.start = openParenToken.range.start;
|
|
751
|
-
|
|
752
|
-
// consume '('
|
|
753
|
-
i++;
|
|
754
|
-
updateData(tokens, i);
|
|
755
|
-
|
|
756
|
-
// Phase 1: Content capture (Lexer provides high-level TEXT/ESCAPE tokens here)
|
|
757
|
-
while (i < tokens.length) {
|
|
758
|
-
const token = current_token(tokens, i);
|
|
759
|
-
if (!token || token.type === TOKEN_TYPES.CLOSE_PAREN) break;
|
|
760
|
-
|
|
761
|
-
if (token.type === TOKEN_TYPES.ESCAPE) {
|
|
762
|
-
inlineNode.value += token.value.slice(1);
|
|
763
|
-
} else if (token.type !== TOKEN_TYPES.COMMENT) {
|
|
764
|
-
inlineNode.value += token.value;
|
|
765
|
-
}
|
|
766
|
-
i++;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_PAREN) {
|
|
770
|
-
parserError(errorMessage(tokens, i, ")", "inline content"));
|
|
771
|
-
}
|
|
772
|
-
i++; // consume ')'
|
|
773
|
-
|
|
774
|
-
// Collapse newlines and whitespace for "inline" behavior
|
|
775
|
-
inlineNode.value = inlineNode.value.replace(/\s+/g, " ").trim();
|
|
776
|
-
|
|
777
|
-
i = skipJunk(tokens, i);
|
|
778
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.THIN_ARROW) {
|
|
779
|
-
parserError(errorMessage(tokens, i, "->", ")"));
|
|
780
|
-
}
|
|
781
|
-
i++; // consume '->'
|
|
782
|
-
|
|
783
|
-
i = skipJunk(tokens, i);
|
|
784
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.OPEN_PAREN) {
|
|
785
|
-
parserError(errorMessage(tokens, i, "(", "->"));
|
|
786
|
-
}
|
|
787
|
-
i++; // consume '('
|
|
788
|
-
i = skipJunk(tokens, i);
|
|
789
|
-
const idToken = current_token(tokens, i);
|
|
790
|
-
const allowedInlineIdTypes = new Set([
|
|
791
|
-
TOKEN_TYPES.IDENTIFIER,
|
|
792
|
-
TOKEN_TYPES.KEY,
|
|
793
|
-
TOKEN_TYPES.IMPORT,
|
|
794
|
-
TOKEN_TYPES.USE_MODULE,
|
|
795
|
-
TOKEN_TYPES.SLOT_KEYWORD,
|
|
796
|
-
TOKEN_TYPES.FOR_EACH
|
|
797
|
-
]);
|
|
798
|
-
if (!idToken || !allowedInlineIdTypes.has(idToken.type)) {
|
|
799
|
-
parserError(errorMessage(tokens, i, inline_id, "("));
|
|
800
|
-
}
|
|
801
|
-
inlineNode.id = idToken.value.trim();
|
|
802
|
-
validateName(inlineNode.id);
|
|
803
|
-
|
|
804
|
-
i++; // consume ID
|
|
805
|
-
i = skipJunk(tokens, i);
|
|
806
|
-
|
|
807
|
-
const hasArgsTrigger = current_token(tokens, i) && (
|
|
808
|
-
current_token(tokens, i).type === TOKEN_TYPES.COLON ||
|
|
809
|
-
current_token(tokens, i).type === TOKEN_TYPES.EQUAL
|
|
810
|
-
);
|
|
811
|
-
|
|
812
|
-
if (hasArgsTrigger) {
|
|
813
|
-
const separator = current_token(tokens, i).value;
|
|
814
|
-
i++; // consume ':' or '='
|
|
815
|
-
i = skipJunk(tokens, i);
|
|
816
|
-
|
|
817
|
-
// Ensure there is a value after the separator
|
|
818
|
-
const nextToken = current_token(tokens, i);
|
|
819
|
-
if (!nextToken || nextToken.type === TOKEN_TYPES.CLOSE_PAREN || nextToken.type === TOKEN_TYPES.COMMA) {
|
|
820
|
-
parserError(errorMessage(tokens, i, inline_value, separator, `Missing value after ${separator === "=" ? "equals" : "colon"}`));
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
let k = "";
|
|
824
|
-
let v = "";
|
|
825
|
-
let argIndex = 0;
|
|
826
|
-
|
|
827
|
-
while (i < tokens.length) {
|
|
828
|
-
i = skipJunk(tokens, i);
|
|
829
|
-
const token = current_token(tokens, i);
|
|
830
|
-
if (!token || token.type === TOKEN_TYPES.CLOSE_PAREN) break;
|
|
831
|
-
|
|
832
|
-
if (token.type === TOKEN_TYPES.KEY) {
|
|
833
|
-
let [key, keyIndex] = parseKey(tokens, i);
|
|
834
|
-
k = key;
|
|
835
|
-
i = keyIndex;
|
|
836
|
-
i = skipJunk(tokens, i);
|
|
837
|
-
i = parseColon(tokens, i, "inline argument");
|
|
838
|
-
i = skipJunk(tokens, i);
|
|
839
|
-
|
|
840
|
-
// Ensure there is a value after the colon
|
|
841
|
-
const nextToken = current_token(tokens, i);
|
|
842
|
-
if (!nextToken || nextToken.type === TOKEN_TYPES.CLOSE_PAREN || nextToken.type === TOKEN_TYPES.COMMA) {
|
|
843
|
-
parserError(errorMessage(tokens, i, inline_value, ":", "Missing value after colon"));
|
|
844
|
-
}
|
|
845
|
-
validateName(k);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
let [value, valueIndex, isQuoted] = parseValue(tokens, i, placeholders, {}, false);
|
|
849
|
-
v = value;
|
|
850
|
-
i = valueIndex;
|
|
851
|
-
|
|
852
|
-
inlineNode.args[String(argIndex++)] = v;
|
|
853
|
-
if (k) {
|
|
854
|
-
inlineNode.args[k] = v;
|
|
855
|
-
}
|
|
856
|
-
k = "";
|
|
857
|
-
v = "";
|
|
858
|
-
|
|
859
|
-
i = skipJunk(tokens, i);
|
|
860
|
-
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COMMA) {
|
|
861
|
-
i = parseComma(tokens, i, "inline argument");
|
|
862
|
-
} else {
|
|
863
|
-
break;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
i = skipJunk(tokens, i);
|
|
869
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_PAREN) {
|
|
870
|
-
parserError(errorMessage(tokens, i, ")", inlineNode.id));
|
|
871
|
-
}
|
|
872
|
-
const finalParenToken = current_token(tokens, i);
|
|
873
|
-
i++; // consume ')'
|
|
874
|
-
inlineNode.range.end = finalParenToken.range.end;
|
|
875
|
-
|
|
876
|
-
return [inlineNode, i];
|
|
877
|
-
}
|
|
878
772
|
/**
|
|
879
773
|
* Parses a stream of text tokens into a single Text node.
|
|
880
774
|
* Handles unescaping and placeholder resolution.
|
|
@@ -902,7 +796,7 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
|
|
|
902
796
|
i++;
|
|
903
797
|
} else if (token.type === TOKEN_TYPES.STATIC_KEYWORD || token.type === TOKEN_TYPES.RUNTIME_KEYWORD) {
|
|
904
798
|
const nextIdx = skipJunk(tokens, i + 1);
|
|
905
|
-
if (tokens[nextIdx] && tokens[nextIdx].type === TOKEN_TYPES.
|
|
799
|
+
if (tokens[nextIdx] && tokens[nextIdx].type === TOKEN_TYPES.LOGIC_OPEN) {
|
|
906
800
|
// Stop consuming text; this is the start of a logic block
|
|
907
801
|
break;
|
|
908
802
|
}
|
|
@@ -921,44 +815,31 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
|
|
|
921
815
|
}
|
|
922
816
|
i++;
|
|
923
817
|
} else if (token.type === TOKEN_TYPES.PREFIX_P) {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (placeholders[key] !== undefined) {
|
|
931
|
-
textNode.text += String(placeholders[key]);
|
|
932
|
-
} else {
|
|
933
|
-
// Use the unique 'Unresolved Envelope' format via helper
|
|
934
|
-
textNode.text += getPrefixValue(layer, key);
|
|
935
|
-
}
|
|
818
|
+
i++; // consume PREFIX_P keyword
|
|
819
|
+
const [tpKey, tpFallback, tpNextI] = readPrefixKeyFallback(tokens, i);
|
|
820
|
+
i = tpNextI;
|
|
821
|
+
if (placeholders[tpKey] !== undefined) {
|
|
822
|
+
textNode.text += String(placeholders[tpKey]);
|
|
936
823
|
} else {
|
|
937
|
-
textNode.text +=
|
|
824
|
+
textNode.text += tpFallback !== undefined ? tpFallback : getPrefixValue('p', tpKey);
|
|
938
825
|
}
|
|
939
|
-
i++;
|
|
940
826
|
} else if (token.type === TOKEN_TYPES.PREFIX_V) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
variables.__consumed__.add(key);
|
|
954
|
-
} else {
|
|
955
|
-
// Use the unique 'Unresolved Envelope' format via helper
|
|
956
|
-
textNode.text += getPrefixValue('v', key);
|
|
827
|
+
i++; // consume PREFIX_V keyword
|
|
828
|
+
const [tvKey, tvFallback, tvNextI] = readPrefixKeyFallback(tokens, i, "v");
|
|
829
|
+
i = tvNextI;
|
|
830
|
+
if (variables[tvKey] !== undefined) {
|
|
831
|
+
textNode.text += String(variables[tvKey]);
|
|
832
|
+
if (!variables.__consumed__) {
|
|
833
|
+
Object.defineProperty(variables, "__consumed__", {
|
|
834
|
+
value: new Set(),
|
|
835
|
+
enumerable: false,
|
|
836
|
+
configurable: true
|
|
837
|
+
});
|
|
957
838
|
}
|
|
839
|
+
variables.__consumed__.add(tvKey);
|
|
958
840
|
} else {
|
|
959
|
-
textNode.text +=
|
|
841
|
+
textNode.text += tvFallback !== undefined ? tvFallback : getPrefixValue('v', tvKey);
|
|
960
842
|
}
|
|
961
|
-
i++;
|
|
962
843
|
} else {
|
|
963
844
|
break;
|
|
964
845
|
}
|
|
@@ -968,155 +849,6 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
|
|
|
968
849
|
}
|
|
969
850
|
return [textNode, i];
|
|
970
851
|
}
|
|
971
|
-
/**
|
|
972
|
-
* Parses an At-Block (@_id_@: args; content @_end_@).
|
|
973
|
-
* At-Blocks maintain raw content preservation.
|
|
974
|
-
*
|
|
975
|
-
* @param {Object[]} tokens - Token stream.
|
|
976
|
-
* @param {number} i - Initial index.
|
|
977
|
-
* @param {string|null} filename - Source filename.
|
|
978
|
-
* @param {Object} placeholders - Dynamic public API data.
|
|
979
|
-
* @returns {[Object, number]} The At-Block node and new index.
|
|
980
|
-
*/
|
|
981
|
-
function parseAtBlock(tokens, i, filename = null, placeholders = {}, depth = 0) {
|
|
982
|
-
const atBlockNode = makeAtBlockNode();
|
|
983
|
-
atBlockNode.depth = depth;
|
|
984
|
-
const openAtToken = current_token(tokens, i);
|
|
985
|
-
atBlockNode.range.start = openAtToken.range.start;
|
|
986
|
-
|
|
987
|
-
// consume '@_'
|
|
988
|
-
i++;
|
|
989
|
-
i = skipJunk(tokens, i);
|
|
990
|
-
updateData(tokens, i);
|
|
991
|
-
|
|
992
|
-
const idToken = current_token(tokens, i);
|
|
993
|
-
if (!idToken || idToken.type === TOKEN_TYPES.EOF) {
|
|
994
|
-
parserError(errorMessage(tokens, i, "AtBlock ID", "@_", "Missing AtBlock Identifier"));
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
const id = idToken.value;
|
|
998
|
-
if (id.trim() === end_keyword) {
|
|
999
|
-
parserError(errorMessage(tokens, i, id, "", `'${id.trim()}' is a reserved keyword and cannot be used as an identifier.`));
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
atBlockNode.id = id.trim();
|
|
1003
|
-
validateName(atBlockNode.id);
|
|
1004
|
-
|
|
1005
|
-
// consume ID
|
|
1006
|
-
i++;
|
|
1007
|
-
i = skipJunk(tokens, i);
|
|
1008
|
-
updateData(tokens, i);
|
|
1009
|
-
|
|
1010
|
-
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_AT)) {
|
|
1011
|
-
parserError(errorMessage(tokens, i, "_@", "at-block identifier"));
|
|
1012
|
-
}
|
|
1013
|
-
// consume '_@'
|
|
1014
|
-
i++;
|
|
1015
|
-
i = skipJunk(tokens, i);
|
|
1016
|
-
updateData(tokens, i);
|
|
1017
|
-
|
|
1018
|
-
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COLON) {
|
|
1019
|
-
// consume ':'
|
|
1020
|
-
i++;
|
|
1021
|
-
i = skipJunk(tokens, i);
|
|
1022
|
-
|
|
1023
|
-
// Ensure there is a value after the colon
|
|
1024
|
-
const nextToken = current_token(tokens, i);
|
|
1025
|
-
if (!nextToken || nextToken.type === TOKEN_TYPES.SEMICOLON || nextToken.type === TOKEN_TYPES.COMMA) {
|
|
1026
|
-
parserError(errorMessage(tokens, i, at_value, ":", "Missing value after colon"));
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
let k = "";
|
|
1030
|
-
let v = "";
|
|
1031
|
-
let argIndex = 0;
|
|
1032
|
-
|
|
1033
|
-
while (i < tokens.length) {
|
|
1034
|
-
i = skipJunk(tokens, i);
|
|
1035
|
-
const token = current_token(tokens, i);
|
|
1036
|
-
if (!token || token.type === TOKEN_TYPES.SEMICOLON) break;
|
|
1037
|
-
|
|
1038
|
-
const isQuotedKey = token.type === TOKEN_TYPES.QUOTE && peek(tokens, i, 1) && (peek(tokens, i, 1).type === TOKEN_TYPES.KEY);
|
|
1039
|
-
|
|
1040
|
-
if (token.type === TOKEN_TYPES.KEY || isQuotedKey) {
|
|
1041
|
-
let [key, keyIndex] = parseKey(tokens, i);
|
|
1042
|
-
k = key;
|
|
1043
|
-
i = keyIndex;
|
|
1044
|
-
i = skipJunk(tokens, i);
|
|
1045
|
-
i = parseColon(tokens, i, "at-block argument");
|
|
1046
|
-
i = skipJunk(tokens, i);
|
|
1047
|
-
|
|
1048
|
-
// Ensure there is a value after the colon
|
|
1049
|
-
const nextToken = current_token(tokens, i);
|
|
1050
|
-
if (!nextToken || nextToken.type === TOKEN_TYPES.SEMICOLON || nextToken.type === TOKEN_TYPES.COMMA) {
|
|
1051
|
-
parserError(errorMessage(tokens, i, at_value, ":", "Missing value after colon"));
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
if (token.type === TOKEN_TYPES.KEY) {
|
|
1055
|
-
validateName(k);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
let [value, valueIndex, isQuoted] = parseValue(tokens, i, placeholders, {}, false);
|
|
1060
|
-
v = value;
|
|
1061
|
-
i = valueIndex;
|
|
1062
|
-
|
|
1063
|
-
atBlockNode.args[String(argIndex++)] = v;
|
|
1064
|
-
if (k) {
|
|
1065
|
-
atBlockNode.args[k] = v;
|
|
1066
|
-
}
|
|
1067
|
-
k = "";
|
|
1068
|
-
v = "";
|
|
1069
|
-
|
|
1070
|
-
i = skipJunk(tokens, i);
|
|
1071
|
-
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COMMA) {
|
|
1072
|
-
i = parseComma(tokens, i, "at-block argument");
|
|
1073
|
-
} else {
|
|
1074
|
-
break;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// Semicolon is ALWAYS required after ID or ARGS
|
|
1080
|
-
i = parseSemiColon(tokens, i, "at-block header");
|
|
1081
|
-
|
|
1082
|
-
// Body Capture
|
|
1083
|
-
i = skipJunk(tokens, i);
|
|
1084
|
-
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.TEXT) {
|
|
1085
|
-
atBlockNode.content = current_token(tokens, i).value;
|
|
1086
|
-
i++;
|
|
1087
|
-
} else {
|
|
1088
|
-
parserError(errorMessage(tokens, i, "content", "at-block body"));
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
// End Marker (@_end_@)
|
|
1092
|
-
i = skipJunk(tokens, i);
|
|
1093
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.OPEN_AT) {
|
|
1094
|
-
parserError(errorMessage(tokens, i, "@_", "at-block content"));
|
|
1095
|
-
}
|
|
1096
|
-
i++; // consume '@_'
|
|
1097
|
-
i = skipJunk(tokens, i);
|
|
1098
|
-
const endToken = current_token(tokens, i);
|
|
1099
|
-
if (!endToken || (endToken.type !== TOKEN_TYPES.END_KEYWORD && endToken.value.trim() !== end_keyword)) {
|
|
1100
|
-
let extraInfo = "";
|
|
1101
|
-
if (endToken && endToken.value) {
|
|
1102
|
-
const dist = levenshtein(endToken.value.trim().toLowerCase(), "end");
|
|
1103
|
-
if (dist > 0 && dist <= 2) {
|
|
1104
|
-
extraInfo = ` (Did you mean '@_end_@'?)`;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
parserError(errorMessage(tokens, i, "end", "AtBlock Body", extraInfo));
|
|
1108
|
-
}
|
|
1109
|
-
i++; // consume 'end'
|
|
1110
|
-
i = skipJunk(tokens, i);
|
|
1111
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_AT) {
|
|
1112
|
-
parserError(errorMessage(tokens, i, "_@", "end marker"));
|
|
1113
|
-
}
|
|
1114
|
-
const closeAtToken = current_token(tokens, i);
|
|
1115
|
-
i++; // consume '_@'
|
|
1116
|
-
atBlockNode.range.end = closeAtToken.range.end;
|
|
1117
|
-
|
|
1118
|
-
return [atBlockNode, i];
|
|
1119
|
-
}
|
|
1120
852
|
// ========================================================================== //
|
|
1121
853
|
// Parse Comments //
|
|
1122
854
|
// ========================================================================== //
|
|
@@ -1172,73 +904,38 @@ function parseNode(tokens, i, filename = null, placeholders = {}, variables = {}
|
|
|
1172
904
|
return parseBlock(tokens, i, filename, placeholders, variables, depth);
|
|
1173
905
|
}
|
|
1174
906
|
// ========================================================================== //
|
|
1175
|
-
// Inline Statement or Text //
|
|
1176
|
-
// ========================================================================== //
|
|
1177
|
-
else if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.OPEN_PAREN) {
|
|
1178
|
-
let j = i + 1;
|
|
1179
|
-
let parenCount = 1;
|
|
1180
|
-
let foundArrow = false;
|
|
1181
|
-
while (j < tokens.length) {
|
|
1182
|
-
const token = tokens[j];
|
|
1183
|
-
if (token.type === TOKEN_TYPES.OPEN_PAREN) {
|
|
1184
|
-
parenCount++;
|
|
1185
|
-
} else if (token.type === TOKEN_TYPES.CLOSE_PAREN) {
|
|
1186
|
-
parenCount--;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
if (parenCount === 0) {
|
|
1190
|
-
const nextIdx = skipJunk(tokens, j + 1);
|
|
1191
|
-
if (tokens[nextIdx] && tokens[nextIdx].type === TOKEN_TYPES.THIN_ARROW) {
|
|
1192
|
-
foundArrow = true;
|
|
1193
|
-
}
|
|
1194
|
-
break;
|
|
1195
|
-
}
|
|
1196
|
-
// Safe-guard: If we hit a [ or @, it's highly unlikely to be an inline statement content
|
|
1197
|
-
// unless it's escaped, but lexer already handles [ and @ as structural tokens if not escaped.
|
|
1198
|
-
if (token.type === TOKEN_TYPES.OPEN_BRACKET || token.type === TOKEN_TYPES.OPEN_AT) break;
|
|
1199
|
-
j++;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
if (foundArrow) {
|
|
1203
|
-
return parseInline(tokens, i, placeholders, depth);
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
// Treat as text if not an inline
|
|
1207
|
-
const textNode = makeTextNode();
|
|
1208
|
-
textNode.text = current_token(tokens, i).value;
|
|
1209
|
-
textNode.depth = depth;
|
|
1210
|
-
textNode.range = current_token(tokens, i).range;
|
|
1211
|
-
return [textNode, i + 1];
|
|
1212
|
-
}
|
|
1213
|
-
// ========================================================================== //
|
|
1214
907
|
// Logic Block //
|
|
1215
908
|
// ========================================================================== //
|
|
1216
|
-
else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD || current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD
|
|
909
|
+
else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD || current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD)) {
|
|
1217
910
|
let isStatic = current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD;
|
|
1218
|
-
let isRuntimeKeyword = current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD;
|
|
1219
911
|
let startRange = current_token(tokens, i).range;
|
|
1220
|
-
let nextI = i;
|
|
1221
|
-
|
|
1222
|
-
if (
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
if (!current_token(tokens, nextI) || current_token(tokens, nextI).type !== TOKEN_TYPES.LOGIC) {
|
|
1226
|
-
// Treat as normal text if keyword is not followed by a logic block
|
|
1227
|
-
return parseText(tokens, i, placeholders, variables, depth);
|
|
1228
|
-
}
|
|
1229
|
-
i = nextI;
|
|
912
|
+
let nextI = skipJunk(tokens, i + 1);
|
|
913
|
+
|
|
914
|
+
if (!current_token(tokens, nextI) || current_token(tokens, nextI).type !== TOKEN_TYPES.LOGIC_OPEN) {
|
|
915
|
+
// Keyword not followed by ${ — treat as normal text
|
|
916
|
+
return parseText(tokens, i, placeholders, variables, depth);
|
|
1230
917
|
}
|
|
1231
918
|
|
|
1232
|
-
|
|
919
|
+
if (isStatic) global_static_logic_count++;
|
|
920
|
+
|
|
921
|
+
// Skip LOGIC_OPEN, read LOGIC body
|
|
922
|
+
nextI++;
|
|
923
|
+
const logicToken = current_token(tokens, nextI);
|
|
1233
924
|
const node = makeLogicNode(isStatic ? STATIC_LOGIC : RUNTIME_LOGIC);
|
|
1234
|
-
node.code = logicToken.value;
|
|
925
|
+
node.code = logicToken ? logicToken.value : "";
|
|
1235
926
|
node.depth = depth;
|
|
1236
927
|
node.range = {
|
|
1237
|
-
start:
|
|
1238
|
-
end: logicToken.range.end
|
|
928
|
+
start: startRange.start,
|
|
929
|
+
end: logicToken ? logicToken.range.end : startRange.end
|
|
1239
930
|
};
|
|
931
|
+
nextI++;
|
|
1240
932
|
|
|
1241
|
-
|
|
933
|
+
// Consume LOGIC_CLOSE if present
|
|
934
|
+
if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
|
|
935
|
+
nextI++;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return [node, nextI];
|
|
1242
939
|
}
|
|
1243
940
|
// ========================================================================== //
|
|
1244
941
|
// Text or Placeholder //
|
|
@@ -1253,12 +950,6 @@ function parseNode(tokens, i, filename = null, placeholders = {}, variables = {}
|
|
|
1253
950
|
current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P)
|
|
1254
951
|
) {
|
|
1255
952
|
return parseText(tokens, i, placeholders, variables, depth);
|
|
1256
|
-
}
|
|
1257
|
-
// ========================================================================== //
|
|
1258
|
-
// Atblock //
|
|
1259
|
-
// ========================================================================== //
|
|
1260
|
-
else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.OPEN_AT)) {
|
|
1261
|
-
return parseAtBlock(tokens, i, filename, placeholders, depth);
|
|
1262
953
|
} else {
|
|
1263
954
|
// FALLBACK: Treat any other token as TEXT to avoid infinite loops and allow literal content
|
|
1264
955
|
const textNode = makeTextNode();
|
|
@@ -1313,7 +1004,7 @@ function parser(tokens, filename = null, placeholders = {}, variables = {}) {
|
|
|
1313
1004
|
const val = token.value.trim().toLowerCase();
|
|
1314
1005
|
if (val === "") return "";
|
|
1315
1006
|
const dist = levenshtein(val, "end");
|
|
1316
|
-
if (dist > 0 && dist <= 2) return `
|
|
1007
|
+
if (dist > 0 && dist <= 2) return ` Did you mean '[end]'?`;
|
|
1317
1008
|
}
|
|
1318
1009
|
return "";
|
|
1319
1010
|
};
|