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/dist/sommark.parser.js
CHANGED
|
@@ -8,16 +8,10 @@
|
|
|
8
8
|
* @property {string} END_KEYWORD - 'end' value.
|
|
9
9
|
* @property {string} IDENTIFIER - Block or inline name (e.g. 'Person', 'import', '$use-module').
|
|
10
10
|
* @property {string} EQUAL - '=' char.
|
|
11
|
-
* @property {string} VALUE - Data values. Encapsulates Quoted Strings ("...") and Prefix Layers (
|
|
11
|
+
* @property {string} VALUE - Data values. Encapsulates Quoted Strings ("...") and Prefix Layers (p{}, v{}).
|
|
12
12
|
* @property {string} TEXT - Plain unformatted text content.
|
|
13
|
-
* @property {string} THIN_ARROW - '->' sequence.
|
|
14
|
-
* @property {string} OPEN_PAREN - '(' char.
|
|
15
|
-
* @property {string} CLOSE_PAREN - ')' char.
|
|
16
|
-
* @property {string} OPEN_AT - '@_' sequence (At-Block start).
|
|
17
|
-
* @property {string} CLOSE_AT - '_@' sequence (At-Header end).
|
|
18
13
|
* @property {string} COLON - ':' char.
|
|
19
14
|
* @property {string} COMMA - ',' char.
|
|
20
|
-
* @property {string} SEMICOLON - ';' char (At-Block separator).
|
|
21
15
|
* @property {string} COMMENT - '#' comments.
|
|
22
16
|
* @property {string} COMMENT_BLOCK - '###' comments.
|
|
23
17
|
* @property {string} ESCAPE - '\' char. Used for literalizing structural chars like '\"' or '\['.
|
|
@@ -25,7 +19,6 @@
|
|
|
25
19
|
* @property {string} EXCLAMATION_MARK - '!' char.
|
|
26
20
|
* @property {string} IMPORT - 'import' keyword.
|
|
27
21
|
* @property {string} USE_MODULE - '$use-module' keyword.
|
|
28
|
-
* @property {string} PREFIX_JS - 'js{}' prefix layer.
|
|
29
22
|
* @property {string} PREFIX_P - 'p{}' placeholder layer.
|
|
30
23
|
* @property {string} PREFIX_V - 'v{}' local variable layer.
|
|
31
24
|
* @property {string} EOF - End of File indicator.
|
|
@@ -40,18 +33,11 @@ const TOKEN_TYPES = {
|
|
|
40
33
|
EQUAL: "EQUAL",
|
|
41
34
|
VALUE: "VALUE",
|
|
42
35
|
QUOTE: "QUOTE",
|
|
43
|
-
PREFIX_JS: "PREFIX_JS",
|
|
44
36
|
PREFIX_P: "PREFIX_P",
|
|
45
37
|
PREFIX_V: "PREFIX_V",
|
|
46
38
|
TEXT: "TEXT",
|
|
47
|
-
THIN_ARROW: "THIN_ARROW",
|
|
48
|
-
OPEN_PAREN: "OPEN_PAREN",
|
|
49
|
-
CLOSE_PAREN: "CLOSE_PAREN",
|
|
50
|
-
OPEN_AT: "OPEN_AT",
|
|
51
|
-
CLOSE_AT: "CLOSE_AT",
|
|
52
39
|
COLON: "COLON",
|
|
53
40
|
COMMA: "COMMA",
|
|
54
|
-
SEMICOLON: "SEMICOLON",
|
|
55
41
|
COMMENT: "COMMENT",
|
|
56
42
|
COMMENT_BLOCK: "COMMENT_BLOCK",
|
|
57
43
|
ESCAPE: "ESCAPE",
|
|
@@ -61,8 +47,13 @@ const TOKEN_TYPES = {
|
|
|
61
47
|
WHITESPACE: "WHITESPACE",
|
|
62
48
|
STATIC_KEYWORD: "STATIC_KEYWORD",
|
|
63
49
|
RUNTIME_KEYWORD: "RUNTIME_KEYWORD",
|
|
50
|
+
LOGIC_OPEN: "LOGIC_OPEN",
|
|
64
51
|
LOGIC: "LOGIC",
|
|
52
|
+
LOGIC_CLOSE: "LOGIC_CLOSE",
|
|
65
53
|
FOR_EACH: "FOR_EACH",
|
|
54
|
+
PREFIX_OPEN: "PREFIX_OPEN",
|
|
55
|
+
PREFIX_CLOSE: "PREFIX_CLOSE",
|
|
56
|
+
PIPELINE: "PIPELINE",
|
|
66
57
|
EOF: "EOF"
|
|
67
58
|
};
|
|
68
59
|
|
|
@@ -93,8 +84,6 @@ function peek(input, index, offset) {
|
|
|
93
84
|
*/
|
|
94
85
|
const BLOCK = "Block",
|
|
95
86
|
TEXT = "Text",
|
|
96
|
-
INLINE = "Inline",
|
|
97
|
-
ATBLOCK = "AtBlock",
|
|
98
87
|
COMMENT = "Comment",
|
|
99
88
|
COMMENT_BLOCK = "CommentBlock",
|
|
100
89
|
IMPORT = "Import",
|
|
@@ -107,13 +96,8 @@ const BLOCK = "Block",
|
|
|
107
96
|
/**
|
|
108
97
|
* Names for symbols used to separate parts of the code (like commas and colons).
|
|
109
98
|
*/
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
ATBLOCKCOMMA = "Atblock-comma",
|
|
113
|
-
INLINECOMMA = "Inline-comma",
|
|
114
|
-
BLOCKCOLON = "Block-colon",
|
|
115
|
-
ATBLOCKCOLON = "Atblock-colon",
|
|
116
|
-
INLINECOLON = "Inline-colon";
|
|
99
|
+
const BLOCKCOMMA = "Block-comma",
|
|
100
|
+
BLOCKCOLON = "Block-colon";
|
|
117
101
|
|
|
118
102
|
/**
|
|
119
103
|
* These names are used in error messages to tell you exactly which part
|
|
@@ -123,12 +107,6 @@ const block_id = "Block Identifier",
|
|
|
123
107
|
block_value = "Block Value",
|
|
124
108
|
block_key = "Block Key",
|
|
125
109
|
block_end = "Block end",
|
|
126
|
-
inline_id = "Inline Identifier",
|
|
127
|
-
inline_text = "Inline Text",
|
|
128
|
-
at_id = "At Identifier",
|
|
129
|
-
at_value = "At Value",
|
|
130
|
-
atblock_key = "AtBlock Key",
|
|
131
|
-
at_end = "Atblock End",
|
|
132
110
|
/** Reserved keyword for closing blocks */
|
|
133
111
|
end_keyword = "end",
|
|
134
112
|
slot_keyword = "slot",
|
|
@@ -136,9 +114,6 @@ const block_id = "Block Identifier",
|
|
|
136
114
|
|
|
137
115
|
var labels = /*#__PURE__*/Object.freeze({
|
|
138
116
|
__proto__: null,
|
|
139
|
-
ATBLOCK: ATBLOCK,
|
|
140
|
-
ATBLOCKCOLON: ATBLOCKCOLON,
|
|
141
|
-
ATBLOCKCOMMA: ATBLOCKCOMMA,
|
|
142
117
|
BLOCK: BLOCK,
|
|
143
118
|
BLOCKCOLON: BLOCKCOLON,
|
|
144
119
|
BLOCKCOMMA: BLOCKCOMMA,
|
|
@@ -146,225 +121,20 @@ var labels = /*#__PURE__*/Object.freeze({
|
|
|
146
121
|
COMMENT_BLOCK: COMMENT_BLOCK,
|
|
147
122
|
FOR_EACH: FOR_EACH,
|
|
148
123
|
IMPORT: IMPORT,
|
|
149
|
-
INLINE: INLINE,
|
|
150
|
-
INLINECOLON: INLINECOLON,
|
|
151
|
-
INLINECOMMA: INLINECOMMA,
|
|
152
124
|
RUNTIME_LOGIC: RUNTIME_LOGIC,
|
|
153
|
-
SEMICOLON: SEMICOLON,
|
|
154
125
|
SLOT: SLOT,
|
|
155
126
|
STATIC_LOGIC: STATIC_LOGIC,
|
|
156
127
|
TEXT: TEXT,
|
|
157
128
|
USE_MODULE: USE_MODULE,
|
|
158
|
-
at_end: at_end,
|
|
159
|
-
at_id: at_id,
|
|
160
|
-
at_value: at_value,
|
|
161
|
-
atblock_key: atblock_key,
|
|
162
129
|
block_end: block_end,
|
|
163
130
|
block_id: block_id,
|
|
164
131
|
block_key: block_key,
|
|
165
132
|
block_value: block_value,
|
|
166
133
|
end_keyword: end_keyword,
|
|
167
134
|
for_each_keyword: for_each_keyword,
|
|
168
|
-
inline_id: inline_id,
|
|
169
|
-
inline_text: inline_text,
|
|
170
135
|
slot_keyword: slot_keyword
|
|
171
136
|
});
|
|
172
137
|
|
|
173
|
-
/**
|
|
174
|
-
* Wraps your text in a color if colors are turned on.
|
|
175
|
-
*
|
|
176
|
-
* @param {string} color - The color to use (red, green, yellow, blue, magenta, or cyan).
|
|
177
|
-
* @param {string} text - The text you want to color.
|
|
178
|
-
* @returns {string} - The colored text, or plain text if colors are off.
|
|
179
|
-
* @throws {Error} - Fails if you forget to provide the text.
|
|
180
|
-
*/
|
|
181
|
-
function colorize(color, text) {
|
|
182
|
-
if (!text) throw new Error("argument 'text' is not defined.");
|
|
183
|
-
return text;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* SomMark Errors
|
|
188
|
-
* Handles formatting and throwing errors with beautiful CLI coloring and pointers.
|
|
189
|
-
*/
|
|
190
|
-
|
|
191
|
-
// ========================================================================== //
|
|
192
|
-
// Message Formatting //
|
|
193
|
-
// ========================================================================== //
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Processes a message by applying colors and formatting.
|
|
197
|
-
* Supports:
|
|
198
|
-
* - {line} : Adds a horizontal line
|
|
199
|
-
* - {N} : Adds a new line
|
|
200
|
-
* - <$color: Text$> : Adds color (red, yellow, green, blue, magenta, cyan)
|
|
201
|
-
*
|
|
202
|
-
* @param {string|string[]} text - The message or list of message parts to format.
|
|
203
|
-
* @returns {string} - The final formatted and colored string.
|
|
204
|
-
*/
|
|
205
|
-
function formatMessage(text) {
|
|
206
|
-
const horizontal_rule = "\n----------------------------------------------------------------------------------------------\n";
|
|
207
|
-
const pattern = /<\$([^:]+):([\s\S]*?)\$>/g;
|
|
208
|
-
|
|
209
|
-
if (Array.isArray(text)) {
|
|
210
|
-
text = text.join("");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
text = text.replace(pattern, (match, color, content) => {
|
|
214
|
-
return colorize(color, content.trim());
|
|
215
|
-
});
|
|
216
|
-
text = text.replaceAll("{line}", horizontal_rule);
|
|
217
|
-
text = text.replaceAll("{N}", "\n");
|
|
218
|
-
|
|
219
|
-
text = text
|
|
220
|
-
.split("\n")
|
|
221
|
-
.filter(value => value !== "")
|
|
222
|
-
.join("\n")
|
|
223
|
-
.trim();
|
|
224
|
-
|
|
225
|
-
return text;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Creates a detailed error message showing where the error happened in the code.
|
|
230
|
-
* It adds a line number, a snippet of the code, and a pointer (^) to the exact spot.
|
|
231
|
-
*
|
|
232
|
-
* @param {string} src - The original code being parsed.
|
|
233
|
-
* @param {Object} range - The location of the error (line and character).
|
|
234
|
-
* @param {string|null} filename - The name of the file (optional).
|
|
235
|
-
* @param {string|string[]} message - The error message to show.
|
|
236
|
-
* @param {string} typeName - The type of error (e.g., "Lexer" or "Parser").
|
|
237
|
-
* @returns {string[]} - A list of message parts that make up the final error report.
|
|
238
|
-
*/
|
|
239
|
-
function formatErrorWithContext(src, range, filename, message, typeName) {
|
|
240
|
-
if (!src || !range || !range.start) return message;
|
|
241
|
-
|
|
242
|
-
const lines = src.split("\n");
|
|
243
|
-
const lineIndex = range.start.line;
|
|
244
|
-
const lineContent = lines[lineIndex] || "";
|
|
245
|
-
const pointerPadding = " ".repeat(range.start.character);
|
|
246
|
-
const sourceLabel = filename ? ` [${filename}]` : "";
|
|
247
|
-
|
|
248
|
-
const rangeInfo =
|
|
249
|
-
range.start.line === range.end.line
|
|
250
|
-
? `from column <$yellow:${range.start.character}$> to <$yellow:${range.end.character}$>`
|
|
251
|
-
: `from line <$yellow:${range.start.line + 1}$>, column <$yellow:${range.start.character}$> to line <$yellow:${range.end.line + 1}$>, column <$yellow:${range.end.character}$>`;
|
|
252
|
-
|
|
253
|
-
const formattedMessage = [
|
|
254
|
-
`<$blue:{line}$><$red:Here where error occurred${sourceLabel}:$>{N}${lineContent}{N}${pointerPadding}<$yellow:^$>{N}{N}`,
|
|
255
|
-
`<$red:${typeName} Error:$> `,
|
|
256
|
-
...(Array.isArray(message) ? message : [message]),
|
|
257
|
-
`{N}at line <$yellow:${range.start.line + 1}$>, ${rangeInfo}{N}`,
|
|
258
|
-
"<$blue:{line}$>"
|
|
259
|
-
];
|
|
260
|
-
|
|
261
|
-
return formattedMessage;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ========================================================================== //
|
|
265
|
-
// Error Classes //
|
|
266
|
-
// ========================================================================== //
|
|
267
|
-
|
|
268
|
-
/** Base class for all SomMark errors that automatically formats messages for the terminal. */
|
|
269
|
-
class CustomError extends Error {
|
|
270
|
-
/**
|
|
271
|
-
* Creates a new error.
|
|
272
|
-
*
|
|
273
|
-
* @param {string|string[]} message - The text describing what went wrong.
|
|
274
|
-
* @param {string} name - The name of the error type.
|
|
275
|
-
*/
|
|
276
|
-
constructor(message, name) {
|
|
277
|
-
super(message);
|
|
278
|
-
this.name = name;
|
|
279
|
-
this.message = formatMessage(`<$cyan:[${this.name}]$>:`) + "\n" + formatMessage(message);
|
|
280
|
-
if (Error.captureStackTrace) {
|
|
281
|
-
Error.captureStackTrace(this, this.constructor);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
class ParserError extends CustomError {
|
|
287
|
-
constructor(message) { super(message, "Parser Error"); }
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
class LexerError extends CustomError {
|
|
291
|
-
constructor(message) { super(message, "Lexer Error"); }
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
class TranspilerError extends CustomError {
|
|
295
|
-
constructor(message) { super(message, "Transpiler Error"); }
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
class CLIError extends CustomError {
|
|
299
|
-
constructor(message) { super(message, "CLI Error"); }
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
class RuntimeError extends CustomError {
|
|
303
|
-
constructor(message) { super(message, "Runtime Error"); }
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
class SommarkError extends CustomError {
|
|
307
|
-
constructor(message) { super(message, "SomMark Error"); }
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ========================================================================== //
|
|
311
|
-
// Error Dispatcher (Helper) //
|
|
312
|
-
// ========================================================================== //
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* A helper that creates an error "dispatcher" for a specific category.
|
|
316
|
-
*
|
|
317
|
-
* @param {string} type - The category of error (e.g., 'lexer', 'parser').
|
|
318
|
-
* @returns {Function} - A function that throws the formatted error.
|
|
319
|
-
*/
|
|
320
|
-
function getError(type) {
|
|
321
|
-
const validate_msg = msg => (Array.isArray(msg) && msg.length > 0) || typeof msg === "string";
|
|
322
|
-
const typeNames = {
|
|
323
|
-
parser: "Parser",
|
|
324
|
-
transpiler: "Transpiler",
|
|
325
|
-
lexer: "Lexer",
|
|
326
|
-
cli: "CLI",
|
|
327
|
-
runtime: "Runtime",
|
|
328
|
-
sommark: "SomMark"
|
|
329
|
-
};
|
|
330
|
-
const ErrorClasses = {
|
|
331
|
-
parser: ParserError,
|
|
332
|
-
transpiler: TranspilerError,
|
|
333
|
-
lexer: LexerError,
|
|
334
|
-
cli: CLIError,
|
|
335
|
-
runtime: RuntimeError,
|
|
336
|
-
sommark: SommarkError
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
return (errorMessage, context = null) => {
|
|
340
|
-
if (validate_msg(errorMessage)) {
|
|
341
|
-
let finalMessage = errorMessage;
|
|
342
|
-
if (context && context.src && context.range) {
|
|
343
|
-
finalMessage = formatErrorWithContext(
|
|
344
|
-
context.src,
|
|
345
|
-
context.range,
|
|
346
|
-
context.filename,
|
|
347
|
-
errorMessage,
|
|
348
|
-
typeNames[type]
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
throw new ErrorClasses[type](finalMessage).message;
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/** Helper to throw Lexer errors. */
|
|
357
|
-
const lexerError = getError("lexer");
|
|
358
|
-
|
|
359
|
-
/** Helper to throw Parser errors. */
|
|
360
|
-
const parserError = getError("parser");
|
|
361
|
-
|
|
362
|
-
/** Helper to throw Runtime or Module errors. */
|
|
363
|
-
const runtimeError = getError("runtime");
|
|
364
|
-
|
|
365
|
-
/** Helper to throw general internal SomMark errors. */
|
|
366
|
-
const sommarkError = getError("sommark");
|
|
367
|
-
|
|
368
138
|
/**
|
|
369
139
|
* SomMark Lexer
|
|
370
140
|
*
|
|
@@ -384,12 +154,12 @@ function lexer(src, filename = "anonymous") {
|
|
|
384
154
|
let line = 0, character = 0;
|
|
385
155
|
|
|
386
156
|
// State Variables
|
|
387
|
-
let isInAtBlockBody = false;
|
|
388
157
|
let isInQuote = false;
|
|
389
|
-
let isInHeader = false;
|
|
390
|
-
let
|
|
391
|
-
let
|
|
392
|
-
let
|
|
158
|
+
let isInHeader = false; // Tracks if we are in a structural header context
|
|
159
|
+
let isInPVPrefix = false; // Tracks if we are scanning inside a p{} or v{} prefix
|
|
160
|
+
let pendingSmarkRaw = false; // Set when KEY "smark-raw" is seen — waiting for value
|
|
161
|
+
let hasSmarkRaw = false; // Set when smark-raw: true is confirmed in header
|
|
162
|
+
let isRawContent = false; // Set when inside a smark-raw block — content collected as-is, not parsed
|
|
393
163
|
|
|
394
164
|
/**
|
|
395
165
|
* Adds a token to the stream and updates the scanner's position tracking.
|
|
@@ -453,35 +223,63 @@ function lexer(src, filename = "anonymous") {
|
|
|
453
223
|
}
|
|
454
224
|
|
|
455
225
|
while (i < src.length) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
226
|
+
const char = src[i];
|
|
227
|
+
const next = src[i + 1];
|
|
228
|
+
|
|
229
|
+
// --- RAW CONTENT MODE ---
|
|
230
|
+
// Collect everything as-is until [end] or [end:name]. \[ escapes a literal [.
|
|
231
|
+
if (isRawContent) {
|
|
232
|
+
let raw = "";
|
|
233
|
+
while (i < src.length) {
|
|
234
|
+
if (src[i] === "\\" && src[i + 1] === "[") {
|
|
235
|
+
raw += "[";
|
|
236
|
+
i += 2;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (src[i] === "[") {
|
|
240
|
+
if (src.startsWith(`[${end_keyword}]`, i) || src.startsWith(`[${end_keyword}:`, i)) break;
|
|
241
|
+
}
|
|
242
|
+
raw += src[i];
|
|
243
|
+
i++;
|
|
244
|
+
}
|
|
245
|
+
if (raw) addToken(TOKEN_TYPES.TEXT, raw);
|
|
246
|
+
isRawContent = false;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- PHASE 1.5: PV PREFIX CONTENT MODE ---
|
|
251
|
+
// Handles structured content inside p{} and v{} prefixes.
|
|
252
|
+
if (isInPVPrefix && !isInQuote) {
|
|
253
|
+
if (char === '"' || char === "'") {
|
|
254
|
+
addToken(TOKEN_TYPES.QUOTE, char);
|
|
255
|
+
i++;
|
|
256
|
+
isInQuote = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (char === '|') {
|
|
260
|
+
addToken(TOKEN_TYPES.PIPELINE, "|");
|
|
261
|
+
i++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (char === '}') {
|
|
265
|
+
addToken(TOKEN_TYPES.PREFIX_CLOSE, "}");
|
|
266
|
+
isInPVPrefix = false;
|
|
267
|
+
i++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (char !== ' ' && char !== '\t' && char !== '\n' && char !== '\r') {
|
|
271
|
+
let word = '';
|
|
463
272
|
while (i < src.length) {
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
i += 2;
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
// Stop at end marker
|
|
471
|
-
if (src[i] === "@" && src[i + 1] === "_") {
|
|
472
|
-
break;
|
|
473
|
-
}
|
|
474
|
-
body += src[i];
|
|
273
|
+
const c = src[i];
|
|
274
|
+
if (c === '}' || c === '|' || c === '"' || c === "'" || c === ' ' || c === '\t' || c === '\n' || c === '\r') break;
|
|
275
|
+
word += c;
|
|
475
276
|
i++;
|
|
476
277
|
}
|
|
477
|
-
if (
|
|
478
|
-
addToken(TOKEN_TYPES.TEXT, body);
|
|
479
|
-
}
|
|
278
|
+
if (word) addToken(TOKEN_TYPES.KEY, word);
|
|
480
279
|
continue;
|
|
481
280
|
}
|
|
281
|
+
// Whitespace: fall through to PHASE 3 whitespace handling
|
|
482
282
|
}
|
|
483
|
-
const char = src[i];
|
|
484
|
-
const next = src[i + 1];
|
|
485
283
|
|
|
486
284
|
// --- PHASE 2: QUOTE MODE ---
|
|
487
285
|
// Handles balanced strings and allows prefix layers (js{}, p{}) inside them.
|
|
@@ -499,50 +297,57 @@ function lexer(src, filename = "anonymous") {
|
|
|
499
297
|
}
|
|
500
298
|
|
|
501
299
|
// Support Prefix Layers inside quotes!
|
|
502
|
-
if ((src[i] === "
|
|
503
|
-
const isJS = (src[i] === "j");
|
|
300
|
+
if ((src[i] === "p" && src[i + 1] === "{") || (src[i] === "v" && src[i + 1] === "{")) {
|
|
504
301
|
const isV = (src[i] === "v");
|
|
505
302
|
if (quoteValue.length > 0) {
|
|
506
303
|
addToken(TOKEN_TYPES.VALUE, quoteValue);
|
|
507
304
|
quoteValue = "";
|
|
508
305
|
}
|
|
509
306
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
307
|
+
{
|
|
308
|
+
// p{} or v{}: keyword + PREFIX_OPEN + unquoted key + optional PIPELINE + fallback + PREFIX_CLOSE
|
|
309
|
+
addToken(isV ? TOKEN_TYPES.PREFIX_V : TOKEN_TYPES.PREFIX_P, isV ? "v" : "p");
|
|
310
|
+
addToken(TOKEN_TYPES.PREFIX_OPEN, "{");
|
|
311
|
+
i += 2;
|
|
312
|
+
// Scan unquoted key (cannot use same quote char as outer string)
|
|
313
|
+
let key = "";
|
|
314
|
+
while (i < src.length && src[i] !== "|" && src[i] !== "}" && src[i] !== quoteChar) {
|
|
315
|
+
key += src[i];
|
|
316
|
+
i++;
|
|
317
|
+
}
|
|
318
|
+
if (key.trim()) addToken(TOKEN_TYPES.KEY, key.trim());
|
|
319
|
+
// Optional PIPELINE + fallback
|
|
320
|
+
if (i < src.length && src[i] === "|") {
|
|
321
|
+
addToken(TOKEN_TYPES.PIPELINE, "|");
|
|
322
|
+
i++;
|
|
323
|
+
let fallback = "";
|
|
324
|
+
while (i < src.length && src[i] !== "}" && src[i] !== quoteChar) {
|
|
325
|
+
fallback += src[i];
|
|
326
|
+
i++;
|
|
523
327
|
}
|
|
524
|
-
if (
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
328
|
+
if (fallback.trim()) addToken(TOKEN_TYPES.VALUE, fallback.trim());
|
|
329
|
+
}
|
|
330
|
+
// PREFIX_CLOSE
|
|
331
|
+
if (i < src.length && src[i] === "}") {
|
|
332
|
+
addToken(TOKEN_TYPES.PREFIX_CLOSE, "}");
|
|
333
|
+
i++;
|
|
529
334
|
}
|
|
530
|
-
prefixValue += c;
|
|
531
|
-
i++;
|
|
532
335
|
}
|
|
533
|
-
let tokenType = isJS ? TOKEN_TYPES.PREFIX_JS : (isV ? TOKEN_TYPES.PREFIX_V : TOKEN_TYPES.PREFIX_P);
|
|
534
|
-
addToken(tokenType, prefixValue);
|
|
535
336
|
continue;
|
|
536
337
|
}
|
|
537
338
|
|
|
538
339
|
if (src[i] === quoteChar) {
|
|
539
340
|
// Guess role based on next structural character
|
|
540
341
|
let nextStructural = peekStructural(i + 1);
|
|
541
|
-
let tokenType =
|
|
342
|
+
let tokenType = isInHeader && (nextStructural === ":" || nextStructural === "=")
|
|
542
343
|
? TOKEN_TYPES.KEY
|
|
543
344
|
: TOKEN_TYPES.VALUE;
|
|
544
345
|
|
|
545
346
|
if (quoteValue.length > 0) addToken(tokenType, quoteValue);
|
|
347
|
+
if (pendingSmarkRaw && tokenType === TOKEN_TYPES.VALUE && quoteValue === "true") {
|
|
348
|
+
hasSmarkRaw = true;
|
|
349
|
+
pendingSmarkRaw = false;
|
|
350
|
+
}
|
|
546
351
|
addToken(TOKEN_TYPES.QUOTE, quoteChar);
|
|
547
352
|
isInQuote = false;
|
|
548
353
|
i++;
|
|
@@ -610,84 +415,37 @@ function lexer(src, filename = "anonymous") {
|
|
|
610
415
|
continue;
|
|
611
416
|
}
|
|
612
417
|
|
|
613
|
-
// PREFIX LAYERS (
|
|
614
|
-
if ((char === "
|
|
615
|
-
const isJS = (char === "j");
|
|
418
|
+
// PREFIX LAYERS (p{...} or v{...})
|
|
419
|
+
if ((char === "p" && next === "{") || (char === "v" && next === "{")) {
|
|
616
420
|
const isP = (char === "p");
|
|
617
421
|
const isV = (char === "v");
|
|
618
422
|
|
|
619
423
|
// Context Check
|
|
620
|
-
const isBlockHeader = isInHeader
|
|
621
|
-
const isNormalText = !isInHeader
|
|
424
|
+
const isBlockHeader = isInHeader;
|
|
425
|
+
const isNormalText = !isInHeader;
|
|
622
426
|
|
|
623
427
|
let allowed = false;
|
|
624
|
-
if (isJS && isBlockHeader) allowed = true;
|
|
625
428
|
if (isP && (isBlockHeader || isNormalText)) allowed = true;
|
|
626
429
|
if (isV && (isBlockHeader || isNormalText)) allowed = true;
|
|
627
430
|
|
|
628
431
|
if (allowed) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
while (i < src.length && braceDepth > 0) {
|
|
635
|
-
const c = src[i];
|
|
636
|
-
const n = src[i + 1];
|
|
637
|
-
|
|
638
|
-
if (inString) {
|
|
639
|
-
if (c === "\\" && (n === inString || n === "\\")) {
|
|
640
|
-
prefixValue += c + n;
|
|
641
|
-
i += 2;
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
if (c === inString) inString = null;
|
|
645
|
-
} else {
|
|
646
|
-
if (c === "\"" || c === "'") inString = c;
|
|
647
|
-
else if (c === "{") braceDepth++;
|
|
648
|
-
else if (c === "}") braceDepth--;
|
|
649
|
-
}
|
|
650
|
-
prefixValue += c;
|
|
651
|
-
i++;
|
|
652
|
-
}
|
|
653
|
-
let tokenType = isJS ? TOKEN_TYPES.PREFIX_JS : (isV ? TOKEN_TYPES.PREFIX_V : TOKEN_TYPES.PREFIX_P);
|
|
654
|
-
addToken(tokenType, prefixValue);
|
|
432
|
+
// p{} or v{}: emit keyword + PREFIX_OPEN, enter structured content mode
|
|
433
|
+
addToken(isV ? TOKEN_TYPES.PREFIX_V : TOKEN_TYPES.PREFIX_P, isV ? "v" : "p");
|
|
434
|
+
addToken(TOKEN_TYPES.PREFIX_OPEN, "{");
|
|
435
|
+
i += 2; // skip "p{" or "v{"
|
|
436
|
+
isInPVPrefix = true;
|
|
655
437
|
continue;
|
|
656
438
|
}
|
|
657
439
|
// If not allowed, it will fall through to normal word scanning
|
|
658
440
|
}
|
|
659
441
|
|
|
660
|
-
// MULTI-CHAR MARKERS
|
|
661
|
-
if (char === "@" && next === "_") {
|
|
662
|
-
addToken(TOKEN_TYPES.OPEN_AT, "@_");
|
|
663
|
-
i += 2;
|
|
664
|
-
isInHeader = true; // At-Blocks start with a header part
|
|
665
|
-
isInAtBlockHeader = true;
|
|
666
|
-
continue;
|
|
667
|
-
}
|
|
668
|
-
if (char === "-" && next === ">") {
|
|
669
|
-
if (isInAtBlockBody || (parenDepth > 0 && !isInInlineHead)) {
|
|
670
|
-
addToken(TOKEN_TYPES.TEXT, "-");
|
|
671
|
-
i++; // Swallowed one char
|
|
672
|
-
} else {
|
|
673
|
-
addToken(TOKEN_TYPES.THIN_ARROW, "->");
|
|
674
|
-
i += 2;
|
|
675
|
-
isInInlineHead = true; // The following ( ) will be structural
|
|
676
|
-
}
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
442
|
// STATIC KEYWORD
|
|
681
443
|
if (char === "s" && src.slice(i, i + 6) === "static") {
|
|
682
444
|
const afterStatic = src.slice(i + 6);
|
|
683
445
|
const hasSpace = afterStatic.startsWith(" ");
|
|
684
446
|
const hasLogic = hasSpace ? afterStatic.slice(1).startsWith("${") : afterStatic.startsWith("${");
|
|
685
447
|
|
|
686
|
-
const isMainIdentifier =
|
|
687
|
-
last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET ||
|
|
688
|
-
last_non_junk_type === TOKEN_TYPES.OPEN_AT ||
|
|
689
|
-
(last_non_junk_type === TOKEN_TYPES.OPEN_PAREN && isInInlineHead)
|
|
690
|
-
);
|
|
448
|
+
const isMainIdentifier = last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET;
|
|
691
449
|
|
|
692
450
|
if ((hasLogic || isInHeader) && !isMainIdentifier) {
|
|
693
451
|
addToken(TOKEN_TYPES.STATIC_KEYWORD, hasSpace ? "static " : "static");
|
|
@@ -702,11 +460,7 @@ function lexer(src, filename = "anonymous") {
|
|
|
702
460
|
const hasSpace = afterRuntime.startsWith(" ");
|
|
703
461
|
const hasLogic = hasSpace ? afterRuntime.slice(1).startsWith("${") : afterRuntime.startsWith("${");
|
|
704
462
|
|
|
705
|
-
const isMainIdentifier =
|
|
706
|
-
last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET ||
|
|
707
|
-
last_non_junk_type === TOKEN_TYPES.OPEN_AT ||
|
|
708
|
-
(last_non_junk_type === TOKEN_TYPES.OPEN_PAREN && isInInlineHead)
|
|
709
|
-
);
|
|
463
|
+
const isMainIdentifier = last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET;
|
|
710
464
|
|
|
711
465
|
if ((hasLogic || isInHeader) && !isMainIdentifier) {
|
|
712
466
|
addToken(TOKEN_TYPES.RUNTIME_KEYWORD, hasSpace ? "runtime " : "runtime");
|
|
@@ -715,213 +469,126 @@ function lexer(src, filename = "anonymous") {
|
|
|
715
469
|
}
|
|
716
470
|
}
|
|
717
471
|
|
|
718
|
-
// LOGIC BLOCKS (${ ... }$)
|
|
719
|
-
if (char === "$" && next === "{"
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
let internalString = null;
|
|
726
|
-
let foundClosing = false;
|
|
472
|
+
// LOGIC BLOCKS (${ ... }$) — explicit: static/runtime ${ }$ shorthand: ${ }$ = static ${ }$
|
|
473
|
+
if (char === "$" && next === "{") {
|
|
474
|
+
{
|
|
475
|
+
const hasExplicitKeyword = last_non_junk_type === TOKEN_TYPES.STATIC_KEYWORD || last_non_junk_type === TOKEN_TYPES.RUNTIME_KEYWORD;
|
|
476
|
+
if (!hasExplicitKeyword) addToken(TOKEN_TYPES.STATIC_KEYWORD, "static");
|
|
477
|
+
addToken(TOKEN_TYPES.LOGIC_OPEN, "${");
|
|
478
|
+
i += 2;
|
|
727
479
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
480
|
+
let logicCode = "";
|
|
481
|
+
let depth = 0;
|
|
482
|
+
let internalString = null;
|
|
731
483
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
i
|
|
735
|
-
braceDepth = 0;
|
|
736
|
-
foundClosing = true;
|
|
737
|
-
break;
|
|
738
|
-
}
|
|
484
|
+
while (i < src.length) {
|
|
485
|
+
const c = src[i];
|
|
486
|
+
const n = src[i + 1];
|
|
739
487
|
|
|
740
|
-
|
|
741
|
-
if (c === "
|
|
742
|
-
|
|
743
|
-
i += 2;
|
|
744
|
-
continue;
|
|
488
|
+
// Close condition: }$ at depth 0, not followed by { (}${ is a template expression boundary)
|
|
489
|
+
if (c === "}" && n === "$" && !internalString && depth === 0 && src[i + 2] !== "{") {
|
|
490
|
+
break;
|
|
745
491
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
logicCode += src[i];
|
|
753
|
-
i++;
|
|
492
|
+
|
|
493
|
+
if (internalString) {
|
|
494
|
+
if (c === "\\" && (n === internalString || n === "\\")) {
|
|
495
|
+
logicCode += c + n;
|
|
496
|
+
i += 2;
|
|
497
|
+
continue;
|
|
754
498
|
}
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
i += 2;
|
|
764
|
-
break;
|
|
499
|
+
if (c === internalString) internalString = null;
|
|
500
|
+
} else {
|
|
501
|
+
if (c === "/" && n === "/") {
|
|
502
|
+
logicCode += c + n;
|
|
503
|
+
i += 2;
|
|
504
|
+
while (i < src.length && src[i] !== "\n" && src[i] !== "\r") {
|
|
505
|
+
logicCode += src[i];
|
|
506
|
+
i++;
|
|
765
507
|
}
|
|
766
|
-
|
|
767
|
-
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (c === "/" && n === "*") {
|
|
511
|
+
logicCode += c + n;
|
|
512
|
+
i += 2;
|
|
513
|
+
while (i < src.length) {
|
|
514
|
+
if (src[i] === "*" && src[i + 1] === "/") {
|
|
515
|
+
logicCode += "*/";
|
|
516
|
+
i += 2;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
logicCode += src[i];
|
|
520
|
+
i++;
|
|
521
|
+
}
|
|
522
|
+
continue;
|
|
768
523
|
}
|
|
769
|
-
|
|
524
|
+
|
|
525
|
+
if (c === "\"" || c === "'" || c === "`") internalString = c;
|
|
526
|
+
else if (c === "{") depth++;
|
|
527
|
+
else if (c === "}") depth--;
|
|
770
528
|
}
|
|
771
529
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
else if (c === "}") braceDepth--;
|
|
530
|
+
logicCode += c;
|
|
531
|
+
i++;
|
|
775
532
|
}
|
|
776
533
|
|
|
777
|
-
logicCode
|
|
778
|
-
i++;
|
|
779
|
-
}
|
|
534
|
+
addToken(TOKEN_TYPES.LOGIC, logicCode);
|
|
780
535
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
range: {
|
|
786
|
-
start: { line: startLine, character: startCharacter },
|
|
787
|
-
end: { line: startLine, character: startCharacter + 2 }
|
|
788
|
-
}
|
|
789
|
-
});
|
|
790
|
-
}
|
|
536
|
+
if (i < src.length && src[i] === "}" && src[i + 1] === "$") {
|
|
537
|
+
addToken(TOKEN_TYPES.LOGIC_CLOSE, "}$");
|
|
538
|
+
i += 2;
|
|
539
|
+
}
|
|
791
540
|
|
|
792
|
-
|
|
793
|
-
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
794
543
|
}
|
|
795
544
|
|
|
796
545
|
// SINGLE-CHAR MARKERS
|
|
797
546
|
if (char === "[") {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
isInHeader = true;
|
|
803
|
-
}
|
|
547
|
+
addToken(TOKEN_TYPES.OPEN_BRACKET, "[");
|
|
548
|
+
isInHeader = true;
|
|
549
|
+
pendingSmarkRaw = false;
|
|
550
|
+
hasSmarkRaw = false;
|
|
804
551
|
i++;
|
|
805
552
|
continue;
|
|
806
553
|
}
|
|
807
|
-
if (char === "_" && next === "@") {
|
|
808
|
-
if (isInAtBlockBody || (parenDepth > 0 && !isInInlineHead)) {
|
|
809
|
-
addToken(TOKEN_TYPES.TEXT, "_@");
|
|
810
|
-
} else {
|
|
811
|
-
const lastRealType = last_non_junk_type;
|
|
812
|
-
addToken(TOKEN_TYPES.CLOSE_AT, "_@");
|
|
813
|
-
// Removed delimiter stack check
|
|
814
|
-
if (lastRealType === TOKEN_TYPES.END_KEYWORD) {
|
|
815
|
-
isInAtBlockBody = false;
|
|
816
|
-
isInHeader = false;
|
|
817
|
-
isInAtBlockHeader = false;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
i += 2;
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
554
|
if (char === "]") {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
i++;
|
|
831
|
-
continue;
|
|
832
|
-
}
|
|
833
|
-
if (char === "(") {
|
|
834
|
-
if (isInAtBlockBody || (parenDepth > 0 && !isInInlineHead)) {
|
|
835
|
-
addToken(TOKEN_TYPES.TEXT, "(");
|
|
836
|
-
parenDepth++;
|
|
837
|
-
} else {
|
|
838
|
-
addToken(TOKEN_TYPES.OPEN_PAREN, "(");
|
|
839
|
-
parenDepth++;
|
|
840
|
-
}
|
|
841
|
-
i++;
|
|
842
|
-
continue;
|
|
843
|
-
}
|
|
844
|
-
if (char === ")") {
|
|
845
|
-
if (isInAtBlockBody || (parenDepth > 1 && !isInInlineHead)) {
|
|
846
|
-
addToken(TOKEN_TYPES.TEXT, ")");
|
|
847
|
-
parenDepth--;
|
|
848
|
-
} else if (parenDepth > 0) {
|
|
849
|
-
// This ends the content part if depth drops to 0
|
|
850
|
-
parenDepth--;
|
|
851
|
-
if (parenDepth === 0) {
|
|
852
|
-
addToken(TOKEN_TYPES.CLOSE_PAREN, ")");
|
|
853
|
-
if (isInInlineHead) {
|
|
854
|
-
isInInlineHead = false;
|
|
855
|
-
isInHeader = false;
|
|
856
|
-
}
|
|
857
|
-
} else {
|
|
858
|
-
addToken(TOKEN_TYPES.TEXT, ")");
|
|
859
|
-
}
|
|
860
|
-
} else {
|
|
861
|
-
addToken(TOKEN_TYPES.TEXT, ")");
|
|
555
|
+
addToken(TOKEN_TYPES.CLOSE_BRACKET, "]");
|
|
556
|
+
isInHeader = false;
|
|
557
|
+
if (hasSmarkRaw) {
|
|
558
|
+
isRawContent = true;
|
|
559
|
+
hasSmarkRaw = false;
|
|
862
560
|
}
|
|
561
|
+
pendingSmarkRaw = false;
|
|
863
562
|
i++;
|
|
864
563
|
continue;
|
|
865
564
|
}
|
|
866
565
|
if (char === ":") {
|
|
867
|
-
|
|
868
|
-
|
|
566
|
+
const colonAllowed = [TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.KEY, TOKEN_TYPES.VALUE, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.QUOTE, TOKEN_TYPES.PREFIX_V, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.PREFIX_CLOSE, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT, TOKEN_TYPES.LOGIC, TOKEN_TYPES.LOGIC_CLOSE, TOKEN_TYPES.STATIC_KEYWORD, TOKEN_TYPES.RUNTIME_KEYWORD, TOKEN_TYPES.FOR_EACH];
|
|
567
|
+
if (colonAllowed.includes(last_non_junk_type)) {
|
|
568
|
+
addToken(TOKEN_TYPES.COLON, ":");
|
|
569
|
+
isInHeader = true;
|
|
869
570
|
} else {
|
|
870
|
-
|
|
871
|
-
if (allowed.includes(last_non_junk_type)) {
|
|
872
|
-
addToken(TOKEN_TYPES.COLON, ":");
|
|
873
|
-
isInHeader = true;
|
|
874
|
-
} else {
|
|
875
|
-
addToken(TOKEN_TYPES.TEXT, ":");
|
|
876
|
-
}
|
|
571
|
+
addToken(TOKEN_TYPES.TEXT, ":");
|
|
877
572
|
}
|
|
878
573
|
i++;
|
|
879
574
|
continue;
|
|
880
575
|
}
|
|
881
576
|
if (char === "=") {
|
|
882
|
-
|
|
883
|
-
|
|
577
|
+
const eqAllowed = [TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.KEY, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.QUOTE, TOKEN_TYPES.PREFIX_V, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.PREFIX_CLOSE, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT, TOKEN_TYPES.LOGIC, TOKEN_TYPES.LOGIC_CLOSE, TOKEN_TYPES.STATIC_KEYWORD, TOKEN_TYPES.RUNTIME_KEYWORD, TOKEN_TYPES.FOR_EACH];
|
|
578
|
+
if (eqAllowed.includes(last_non_junk_type)) {
|
|
579
|
+
addToken(TOKEN_TYPES.EQUAL, "=");
|
|
884
580
|
} else {
|
|
885
|
-
|
|
886
|
-
if (allowed.includes(last_non_junk_type)) {
|
|
887
|
-
addToken(TOKEN_TYPES.EQUAL, "=");
|
|
888
|
-
} else {
|
|
889
|
-
addToken(TOKEN_TYPES.TEXT, "=");
|
|
890
|
-
}
|
|
581
|
+
addToken(TOKEN_TYPES.TEXT, "=");
|
|
891
582
|
}
|
|
892
583
|
i++;
|
|
893
584
|
continue;
|
|
894
585
|
}
|
|
895
586
|
if (char === ",") {
|
|
896
|
-
|
|
897
|
-
|
|
587
|
+
const commaAllowed = [TOKEN_TYPES.VALUE, TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.QUOTE, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.PREFIX_V, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.PREFIX_CLOSE, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT, TOKEN_TYPES.LOGIC, TOKEN_TYPES.LOGIC_CLOSE, TOKEN_TYPES.STATIC_KEYWORD, TOKEN_TYPES.RUNTIME_KEYWORD, TOKEN_TYPES.FOR_EACH];
|
|
588
|
+
if (commaAllowed.includes(last_non_junk_type)) {
|
|
589
|
+
addToken(TOKEN_TYPES.COMMA, ",");
|
|
898
590
|
} else {
|
|
899
|
-
|
|
900
|
-
if (allowed.includes(last_non_junk_type)) {
|
|
901
|
-
addToken(TOKEN_TYPES.COMMA, ",");
|
|
902
|
-
} else {
|
|
903
|
-
addToken(TOKEN_TYPES.TEXT, ",");
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
i++;
|
|
907
|
-
continue;
|
|
908
|
-
}
|
|
909
|
-
if (char === ";") {
|
|
910
|
-
if (isInAtBlockBody || (parenDepth > 0 && !isInInlineHead)) {
|
|
911
|
-
addToken(TOKEN_TYPES.TEXT, ";");
|
|
912
|
-
} else {
|
|
913
|
-
const allowed = [TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.VALUE, TOKEN_TYPES.CLOSE_AT, TOKEN_TYPES.CLOSE_PAREN, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.QUOTE, TOKEN_TYPES.PREFIX_JS, TOKEN_TYPES.PREFIX_V, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT, TOKEN_TYPES.LOGIC, TOKEN_TYPES.STATIC_KEYWORD, TOKEN_TYPES.RUNTIME_KEYWORD, TOKEN_TYPES.FOR_EACH];
|
|
914
|
-
if (allowed.includes(last_non_junk_type)) {
|
|
915
|
-
addToken(TOKEN_TYPES.SEMICOLON, ";");
|
|
916
|
-
// ONLY trigger body mode if we were actually in an At-Block header
|
|
917
|
-
if (isInAtBlockHeader) {
|
|
918
|
-
isInHeader = false;
|
|
919
|
-
isInAtBlockHeader = false;
|
|
920
|
-
isInAtBlockBody = true;
|
|
921
|
-
}
|
|
922
|
-
} else {
|
|
923
|
-
addToken(TOKEN_TYPES.TEXT, ";");
|
|
924
|
-
}
|
|
591
|
+
addToken(TOKEN_TYPES.TEXT, ",");
|
|
925
592
|
}
|
|
926
593
|
i++;
|
|
927
594
|
continue;
|
|
@@ -934,7 +601,7 @@ function lexer(src, filename = "anonymous") {
|
|
|
934
601
|
}
|
|
935
602
|
}
|
|
936
603
|
if (char === "\"" || char === "'") {
|
|
937
|
-
const valTriggers = [TOKEN_TYPES.COLON, TOKEN_TYPES.EQUAL, TOKEN_TYPES.COMMA, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.OPEN_BRACKET
|
|
604
|
+
const valTriggers = [TOKEN_TYPES.COLON, TOKEN_TYPES.EQUAL, TOKEN_TYPES.COMMA, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.OPEN_BRACKET];
|
|
938
605
|
const wasValueTrigger = valTriggers.includes(last_non_junk_type);
|
|
939
606
|
addToken(TOKEN_TYPES.QUOTE, char);
|
|
940
607
|
i++;
|
|
@@ -950,28 +617,22 @@ function lexer(src, filename = "anonymous") {
|
|
|
950
617
|
// This is the "Fallback" mode where we scan for identifiers, keys, or values.
|
|
951
618
|
// It uses lookahead and context variables to guess the role of a word.
|
|
952
619
|
let word = "";
|
|
953
|
-
// Only Blocks ([ ]) allow ':' in their main identifier.
|
|
954
|
-
// At-Blocks (@_) and Inlines (->( )) do NOT allow ':' in the ID.
|
|
955
620
|
const isStartOfBlockId = (last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET);
|
|
621
|
+
const isInNormalText = !isInHeader;
|
|
956
622
|
|
|
957
|
-
let stopChars = "[]
|
|
958
|
-
if (isStartOfBlockId
|
|
623
|
+
let stopChars = "[]{}:=,\"'#\\ \t\n\r!";
|
|
624
|
+
if (isStartOfBlockId) {
|
|
959
625
|
stopChars = stopChars.replace(":", "");
|
|
960
626
|
}
|
|
961
|
-
const isInNormalText = !isInHeader && !isInInlineHead && !isInAtBlockBody;
|
|
962
627
|
if (isInNormalText) {
|
|
963
|
-
stopChars = "[]
|
|
628
|
+
stopChars = "[]\\#\n\r"; // In normal text, stop only at block markers, escapes, comments and newlines
|
|
964
629
|
}
|
|
965
630
|
|
|
966
631
|
while (i < src.length && !stopChars.includes(src[i])) {
|
|
967
632
|
// Stop ONLY if $ is followed by { (Logic block start)
|
|
968
633
|
if (src[i] === "$" && src[i + 1] === "{") break;
|
|
969
634
|
|
|
970
|
-
// Lookahead for
|
|
971
|
-
if (src[i] === "_" && src[i + 1] === "@") break;
|
|
972
|
-
if (src[i] === "@" && src[i + 1] === "_") break;
|
|
973
|
-
|
|
974
|
-
// Lookahead for 'static ${' or 'runtime ${' (only if we're not at the very start of the word scanning)
|
|
635
|
+
// Lookahead for 'static ${' or 'runtime ${' mid-word
|
|
975
636
|
if (word.length > 0) {
|
|
976
637
|
if (src[i] === "s" && src.slice(i, i + 7) === "static " && src[i + 7] === "$" && src[i + 8] === "{") break;
|
|
977
638
|
if (src[i] === "s" && src.slice(i, i + 6) === "static" && src[i + 6] === "$" && src[i + 7] === "{") break;
|
|
@@ -979,53 +640,47 @@ function lexer(src, filename = "anonymous") {
|
|
|
979
640
|
if (src[i] === "r" && src.slice(i, i + 7) === "runtime" && src[i + 7] === "$" && src[i + 8] === "{") break;
|
|
980
641
|
}
|
|
981
642
|
|
|
982
|
-
// Lookahead for -> marker in normal text
|
|
983
|
-
if (!isInHeader && src[i] === "-" && src[i + 1] === ">") break;
|
|
984
|
-
|
|
985
643
|
// Stop if we hit an ALLOWED prefix trigger
|
|
986
644
|
if ((src[i] === "p" && src[i + 1] === "{") || (src[i] === "v" && src[i + 1] === "{")) {
|
|
987
645
|
if (isInHeader || isInNormalText) break;
|
|
988
646
|
}
|
|
989
|
-
if (src[i] === "j" && src[i + 1] === "s" && src[i + 2] === "{") {
|
|
990
|
-
if (isInHeader) break;
|
|
991
|
-
}
|
|
992
647
|
word += src[i];
|
|
993
648
|
i++;
|
|
994
649
|
}
|
|
995
650
|
|
|
996
651
|
if (word.length > 0) {
|
|
997
652
|
// Guess role based on context
|
|
998
|
-
if (
|
|
999
|
-
// Inside Inline Content (raw text)
|
|
1000
|
-
addToken(TOKEN_TYPES.TEXT, word);
|
|
1001
|
-
} else if (isInHeader || isInInlineHead) {
|
|
653
|
+
if (isInHeader) {
|
|
1002
654
|
// Inside a structural header context
|
|
1003
|
-
const isMainIdentifier =
|
|
1004
|
-
last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET ||
|
|
1005
|
-
last_non_junk_type === TOKEN_TYPES.OPEN_AT ||
|
|
1006
|
-
(last_non_junk_type === TOKEN_TYPES.OPEN_PAREN && isInInlineHead)
|
|
1007
|
-
);
|
|
655
|
+
const isMainIdentifier = last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET;
|
|
1008
656
|
|
|
1009
657
|
if (isMainIdentifier) {
|
|
1010
|
-
if (word === end_keyword) {
|
|
658
|
+
if (word === end_keyword || word.startsWith(end_keyword + ":")) {
|
|
1011
659
|
addToken(TOKEN_TYPES.END_KEYWORD, word);
|
|
1012
660
|
}
|
|
1013
661
|
else if (word === "import") addToken(TOKEN_TYPES.IMPORT, word);
|
|
1014
662
|
else if (word === "$use-module") addToken(TOKEN_TYPES.USE_MODULE, word);
|
|
1015
663
|
else if (word === "slot") addToken(TOKEN_TYPES.SLOT_KEYWORD, word);
|
|
1016
664
|
else if (word === "for-each") addToken(TOKEN_TYPES.FOR_EACH, word);
|
|
1017
|
-
else
|
|
665
|
+
else {
|
|
666
|
+
addToken(TOKEN_TYPES.IDENTIFIER, word);
|
|
667
|
+
}
|
|
1018
668
|
} else {
|
|
1019
669
|
// Use lookahead to distinguish KEY from VALUE
|
|
1020
670
|
const p = peekStructural(i);
|
|
1021
671
|
if (p === ":") {
|
|
1022
672
|
addToken(TOKEN_TYPES.KEY, word);
|
|
673
|
+
if (word === "smark-raw") pendingSmarkRaw = true;
|
|
1023
674
|
} else if (word === "static") {
|
|
1024
675
|
addToken(TOKEN_TYPES.STATIC_KEYWORD, word);
|
|
1025
676
|
} else if (word === "runtime") {
|
|
1026
677
|
addToken(TOKEN_TYPES.RUNTIME_KEYWORD, word);
|
|
1027
678
|
} else {
|
|
1028
679
|
addToken(TOKEN_TYPES.VALUE, word);
|
|
680
|
+
if (pendingSmarkRaw) {
|
|
681
|
+
if (word === "true") hasSmarkRaw = true;
|
|
682
|
+
pendingSmarkRaw = false;
|
|
683
|
+
}
|
|
1029
684
|
}
|
|
1030
685
|
}
|
|
1031
686
|
} else {
|
|
@@ -1052,120 +707,198 @@ function lexer(src, filename = "anonymous") {
|
|
|
1052
707
|
}
|
|
1053
708
|
|
|
1054
709
|
/**
|
|
1055
|
-
*
|
|
1056
|
-
* It is built to handle data structures without running any dangerous code or
|
|
1057
|
-
* accessing other parts of your project.
|
|
710
|
+
* Wraps your text in a color if colors are turned on.
|
|
1058
711
|
*
|
|
1059
|
-
*
|
|
1060
|
-
* -
|
|
1061
|
-
*
|
|
1062
|
-
* -
|
|
712
|
+
* @param {string} color - The color to use (red, green, yellow, blue, magenta, or cyan).
|
|
713
|
+
* @param {string} text - The text you want to color.
|
|
714
|
+
* @returns {string} - The colored text, or plain text if colors are off.
|
|
715
|
+
* @throws {Error} - Fails if you forget to provide the text.
|
|
716
|
+
*/
|
|
717
|
+
function colorize(color, text) {
|
|
718
|
+
if (!text) throw new Error("argument 'text' is not defined.");
|
|
719
|
+
return text;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* SomMark Errors
|
|
724
|
+
* Handles formatting and throwing errors with beautiful CLI coloring and pointers.
|
|
1063
725
|
*/
|
|
1064
|
-
function safeDataParse(str) {
|
|
1065
|
-
if (typeof str !== "string") return str;
|
|
1066
|
-
const s = str.trim();
|
|
1067
|
-
if (!s) return null;
|
|
1068
726
|
|
|
1069
|
-
|
|
727
|
+
// ========================================================================== //
|
|
728
|
+
// Message Formatting //
|
|
729
|
+
// ========================================================================== //
|
|
1070
730
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
731
|
+
/**
|
|
732
|
+
* Processes a message by applying colors and formatting.
|
|
733
|
+
* Supports:
|
|
734
|
+
* - {line} : Adds a horizontal line
|
|
735
|
+
* - {N} : Adds a new line
|
|
736
|
+
* - <$color: Text$> : Adds color (red, yellow, green, blue, magenta, cyan)
|
|
737
|
+
*
|
|
738
|
+
* @param {string|string[]} text - The message or list of message parts to format.
|
|
739
|
+
* @returns {string} - The final formatted and colored string.
|
|
740
|
+
*/
|
|
741
|
+
function formatMessage(text) {
|
|
742
|
+
const horizontal_rule = "\n" + colorize("blue", "-".repeat(90)) + "\n";
|
|
743
|
+
const pattern = /<\$([^:]+):([\s\S]*?)\$>/g;
|
|
1076
744
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
745
|
+
if (Array.isArray(text)) {
|
|
746
|
+
text = text.join("");
|
|
747
|
+
}
|
|
1080
748
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
749
|
+
// Apply {line} before color tags so the rule is never nested inside a color wrapper.
|
|
750
|
+
text = text.replaceAll("{line}", horizontal_rule);
|
|
751
|
+
text = text.replace(pattern, (match, color, content) => {
|
|
752
|
+
return colorize(color, content.trim());
|
|
753
|
+
});
|
|
754
|
+
text = text.replaceAll("{N}", "\n");
|
|
1084
755
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
756
|
+
text = text
|
|
757
|
+
.split("\n")
|
|
758
|
+
.filter(value => value !== "")
|
|
759
|
+
.join("\n")
|
|
760
|
+
.trim();
|
|
1088
761
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
let result = "";
|
|
1092
|
-
while (index < s.length && s[index] !== quote) {
|
|
1093
|
-
if (s[index] === '\\') index++; // Skip escape
|
|
1094
|
-
result += s[index++];
|
|
1095
|
-
}
|
|
1096
|
-
index++; // Skip closing quote
|
|
1097
|
-
return result;
|
|
1098
|
-
}
|
|
762
|
+
return text;
|
|
763
|
+
}
|
|
1099
764
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (!keyMatch) break;
|
|
1114
|
-
key = keyMatch[0];
|
|
1115
|
-
index += key.length;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
skipWhitespace();
|
|
1119
|
-
if (s[index] !== ':') break;
|
|
1120
|
-
index++; // Skip :
|
|
1121
|
-
|
|
1122
|
-
obj[key] = parseValue();
|
|
1123
|
-
|
|
1124
|
-
skipWhitespace();
|
|
1125
|
-
if (s[index] === ',') index++; // Skip optional comma
|
|
1126
|
-
skipWhitespace();
|
|
1127
|
-
}
|
|
1128
|
-
index++; // Skip }
|
|
1129
|
-
return obj;
|
|
1130
|
-
}
|
|
765
|
+
/**
|
|
766
|
+
* Creates a detailed error message showing where the error happened in the code.
|
|
767
|
+
* It adds a line number, a snippet of the code, and a pointer (^) to the exact spot.
|
|
768
|
+
*
|
|
769
|
+
* @param {string} src - The original code being parsed.
|
|
770
|
+
* @param {Object} range - The location of the error (line and character).
|
|
771
|
+
* @param {string|null} filename - The name of the file (optional).
|
|
772
|
+
* @param {string|string[]} message - The error message to show.
|
|
773
|
+
* @param {string} typeName - The type of error (e.g., "Lexer" or "Parser").
|
|
774
|
+
* @returns {string[]} - A list of message parts that make up the final error report.
|
|
775
|
+
*/
|
|
776
|
+
function formatErrorWithContext(src, range, filename, message, typeName) {
|
|
777
|
+
if (!src || !range || !range.start) return message;
|
|
1131
778
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
779
|
+
const lines = src.split("\n");
|
|
780
|
+
const lineIndex = range.start.line;
|
|
781
|
+
const lineContent = lines[lineIndex] || "";
|
|
782
|
+
const pointerPadding = " ".repeat(range.start.character);
|
|
783
|
+
const sourceLabel = filename ? ` [${filename}]` : "";
|
|
1136
784
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
skipWhitespace();
|
|
1142
|
-
}
|
|
1143
|
-
index++; // Skip ]
|
|
1144
|
-
return arr;
|
|
1145
|
-
}
|
|
785
|
+
const rangeInfo =
|
|
786
|
+
range.start.line === range.end.line
|
|
787
|
+
? `from column <$yellow:${range.start.character}$> to <$yellow:${range.end.character}$>`
|
|
788
|
+
: `from line <$yellow:${range.start.line + 1}$>, column <$yellow:${range.start.character}$> to line <$yellow:${range.end.line + 1}$>, column <$yellow:${range.end.character}$>`;
|
|
1146
789
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
790
|
+
const formattedMessage = [
|
|
791
|
+
`{line}<$red:Here where error occurred${sourceLabel}:$>{N}${lineContent}{N}${pointerPadding}<$yellow:^$>{N}`,
|
|
792
|
+
`<$red:${typeName} Error:$> `,
|
|
793
|
+
...(Array.isArray(message) ? message : [message]),
|
|
794
|
+
`{N}at line <$yellow:${range.start.line + 1}$>, ${rangeInfo}{N}`,
|
|
795
|
+
`{line}`
|
|
796
|
+
];
|
|
1153
797
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
if (token === "null") return null;
|
|
1157
|
-
if (!isNaN(Number(token))) return Number(token);
|
|
798
|
+
return formattedMessage;
|
|
799
|
+
}
|
|
1158
800
|
|
|
1159
|
-
|
|
1160
|
-
|
|
801
|
+
// ========================================================================== //
|
|
802
|
+
// Error Classes //
|
|
803
|
+
// ========================================================================== //
|
|
1161
804
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
805
|
+
/** Base class for all SomMark errors that automatically formats messages for the terminal. */
|
|
806
|
+
class CustomError extends Error {
|
|
807
|
+
/**
|
|
808
|
+
* Creates a new error.
|
|
809
|
+
*
|
|
810
|
+
* @param {string|string[]} message - The text describing what went wrong.
|
|
811
|
+
* @param {string} name - The name of the error type.
|
|
812
|
+
*/
|
|
813
|
+
constructor(message, name) {
|
|
814
|
+
super(message);
|
|
815
|
+
this.name = name;
|
|
816
|
+
this.message = formatMessage(`<$cyan:[${this.name}]$>:`) + "\n" + formatMessage(message);
|
|
817
|
+
if (Error.captureStackTrace) {
|
|
818
|
+
Error.captureStackTrace(this, this.constructor);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
class ParserError extends CustomError {
|
|
824
|
+
constructor(message) { super(message, "Parser Error"); }
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
class LexerError extends CustomError {
|
|
828
|
+
constructor(message) { super(message, "Lexer Error"); }
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
class TranspilerError extends CustomError {
|
|
832
|
+
constructor(message) { super(message, "Transpiler Error"); }
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
class CLIError extends CustomError {
|
|
836
|
+
constructor(message) { super(message, "CLI Error"); }
|
|
1167
837
|
}
|
|
1168
838
|
|
|
839
|
+
class RuntimeError extends CustomError {
|
|
840
|
+
constructor(message) { super(message, "Runtime Error"); }
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
class SommarkError extends CustomError {
|
|
844
|
+
constructor(message) { super(message, "SomMark Error"); }
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// ========================================================================== //
|
|
848
|
+
// Error Dispatcher (Helper) //
|
|
849
|
+
// ========================================================================== //
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* A helper that creates an error "dispatcher" for a specific category.
|
|
853
|
+
*
|
|
854
|
+
* @param {string} type - The category of error (e.g., 'lexer', 'parser').
|
|
855
|
+
* @returns {Function} - A function that throws the formatted error.
|
|
856
|
+
*/
|
|
857
|
+
function getError(type) {
|
|
858
|
+
const validate_msg = msg => (Array.isArray(msg) && msg.length > 0) || typeof msg === "string";
|
|
859
|
+
const typeNames = {
|
|
860
|
+
parser: "Parser",
|
|
861
|
+
transpiler: "Transpiler",
|
|
862
|
+
lexer: "Lexer",
|
|
863
|
+
cli: "CLI",
|
|
864
|
+
runtime: "Runtime",
|
|
865
|
+
sommark: "SomMark"
|
|
866
|
+
};
|
|
867
|
+
const ErrorClasses = {
|
|
868
|
+
parser: ParserError,
|
|
869
|
+
transpiler: TranspilerError,
|
|
870
|
+
lexer: LexerError,
|
|
871
|
+
cli: CLIError,
|
|
872
|
+
runtime: RuntimeError,
|
|
873
|
+
sommark: SommarkError
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
return (errorMessage, context = null) => {
|
|
877
|
+
if (validate_msg(errorMessage)) {
|
|
878
|
+
let finalMessage = errorMessage;
|
|
879
|
+
if (context && context.src && context.range) {
|
|
880
|
+
finalMessage = formatErrorWithContext(
|
|
881
|
+
context.src,
|
|
882
|
+
context.range,
|
|
883
|
+
context.filename,
|
|
884
|
+
errorMessage,
|
|
885
|
+
typeNames[type]
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
throw new ErrorClasses[type](finalMessage).message;
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** Helper to throw Parser errors. */
|
|
894
|
+
const parserError = getError("parser");
|
|
895
|
+
|
|
896
|
+
/** Helper to throw Runtime or Module errors. */
|
|
897
|
+
const runtimeError = getError("runtime");
|
|
898
|
+
|
|
899
|
+
/** Helper to throw general internal SomMark errors. */
|
|
900
|
+
const sommarkError = getError("sommark");
|
|
901
|
+
|
|
1169
902
|
/**
|
|
1170
903
|
* Calculates the Levenshtein distance between two strings.
|
|
1171
904
|
* Used for "Did you mean?" suggestions and fuzzy matching in validation.
|
|
@@ -1287,7 +1020,7 @@ function validateName(
|
|
|
1287
1020
|
: "must contain only letters, numbers, hyphens, underscores, or dollar signs ($)";
|
|
1288
1021
|
|
|
1289
1022
|
if (!keyRegex.test(id)) {
|
|
1290
|
-
parserError([`{line}<$red:Invalid ${name}:$><$blue: '${id}'$>{N}<$yellow:${name} ${ruleMessage}$> <$cyan: ${rule}
|
|
1023
|
+
parserError([`{line}<$red:Invalid ${name}:$><$blue: '${id}'$>{N}<$yellow:${name} ${ruleMessage}$> <$cyan: ${rule}.$>`]);
|
|
1291
1024
|
}
|
|
1292
1025
|
}
|
|
1293
1026
|
|
|
@@ -1297,7 +1030,7 @@ function makeBlockNode() {
|
|
|
1297
1030
|
type: BLOCK,
|
|
1298
1031
|
structure: "Block",
|
|
1299
1032
|
id: "",
|
|
1300
|
-
|
|
1033
|
+
props: {},
|
|
1301
1034
|
body: [],
|
|
1302
1035
|
depth: 0,
|
|
1303
1036
|
range: {
|
|
@@ -1332,41 +1065,6 @@ function makeCommentNode() {
|
|
|
1332
1065
|
}
|
|
1333
1066
|
};
|
|
1334
1067
|
}
|
|
1335
|
-
/** Creates a new empty Inline node. */
|
|
1336
|
-
function makeInlineNode() {
|
|
1337
|
-
return {
|
|
1338
|
-
type: INLINE,
|
|
1339
|
-
structure: "Inline",
|
|
1340
|
-
value: "",
|
|
1341
|
-
id: "",
|
|
1342
|
-
args: {},
|
|
1343
|
-
depth: 0,
|
|
1344
|
-
range: {
|
|
1345
|
-
start: { line: 0, character: 0 },
|
|
1346
|
-
end: { line: 0, character: 0 }
|
|
1347
|
-
}
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// ========================================================================== //
|
|
1352
|
-
// Node Creators //
|
|
1353
|
-
// ========================================================================== //
|
|
1354
|
-
/** Creates a new empty AtBlock node. */
|
|
1355
|
-
function makeAtBlockNode() {
|
|
1356
|
-
return {
|
|
1357
|
-
type: ATBLOCK,
|
|
1358
|
-
structure: "AtBlock",
|
|
1359
|
-
id: "",
|
|
1360
|
-
args: {},
|
|
1361
|
-
content: "",
|
|
1362
|
-
depth: 0,
|
|
1363
|
-
range: {
|
|
1364
|
-
start: { line: 0, character: 0 },
|
|
1365
|
-
end: { line: 0, character: 0 }
|
|
1366
|
-
}
|
|
1367
|
-
};
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
1068
|
/** Creates a new empty Logic node. */
|
|
1371
1069
|
function makeLogicNode(type = RUNTIME_LOGIC) {
|
|
1372
1070
|
return {
|
|
@@ -1399,53 +1097,66 @@ const updateData = (tokens, i) => {
|
|
|
1399
1097
|
|
|
1400
1098
|
const errorMessage = (tokens, i, expectedValue, behindValue, frontText, filename = null) => {
|
|
1401
1099
|
const current = tokens[i] || fallback;
|
|
1402
|
-
const
|
|
1403
|
-
current.range.start.character;
|
|
1100
|
+
const errorLine = current.range.start.line;
|
|
1101
|
+
const errorColStart = current.range.start.character;
|
|
1102
|
+
const errorColEnd = current.range.end.character;
|
|
1404
1103
|
const source = current.source || filename;
|
|
1405
|
-
const sourceLabel = source ? ` [${source}]` : "";
|
|
1406
1104
|
|
|
1105
|
+
// Collect all tokens on the error line for the source snippet
|
|
1407
1106
|
let lineStartIndex = i;
|
|
1408
1107
|
while (
|
|
1409
1108
|
lineStartIndex > 0 &&
|
|
1410
1109
|
tokens[lineStartIndex - 1] &&
|
|
1411
|
-
tokens[lineStartIndex - 1].range.start.line ===
|
|
1110
|
+
tokens[lineStartIndex - 1].range.start.line === errorLine &&
|
|
1412
1111
|
(tokens[lineStartIndex - 1].source || filename) === source
|
|
1413
1112
|
) {
|
|
1414
1113
|
lineStartIndex--;
|
|
1415
1114
|
}
|
|
1416
|
-
|
|
1417
1115
|
let lineEndIndex = i;
|
|
1418
1116
|
while (
|
|
1419
1117
|
lineEndIndex < tokens.length - 1 &&
|
|
1420
1118
|
tokens[lineEndIndex + 1] &&
|
|
1421
|
-
tokens[lineEndIndex + 1].range.start.line ===
|
|
1119
|
+
tokens[lineEndIndex + 1].range.start.line === errorLine &&
|
|
1422
1120
|
(tokens[lineEndIndex + 1].source || filename) === source
|
|
1423
1121
|
) {
|
|
1424
1122
|
lineEndIndex++;
|
|
1425
1123
|
}
|
|
1426
1124
|
|
|
1427
|
-
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
//
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
`<$
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
`
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1125
|
+
const lineContent = tokens.slice(lineStartIndex, lineEndIndex + 1).map(t => t.value).join('');
|
|
1126
|
+
const contentBefore = tokens.slice(lineStartIndex, i).map(t => t.value).join('');
|
|
1127
|
+
const pointerPadding = " ".repeat(contentBefore.length);
|
|
1128
|
+
|
|
1129
|
+
// Location header — file, line, column
|
|
1130
|
+
const lineNum = errorLine + 1;
|
|
1131
|
+
const isMultiLine = current.range.start.line !== current.range.end.line;
|
|
1132
|
+
const colDisplay = isMultiLine
|
|
1133
|
+
? `${errorColStart} → line ${current.range.end.line + 1} col ${errorColEnd}`
|
|
1134
|
+
: errorColStart === errorColEnd ? `${errorColStart}` : `${errorColStart}–${errorColEnd}`;
|
|
1135
|
+
|
|
1136
|
+
// Error description — avoid nested <$color:...$> tags (breaks the non-greedy regex)
|
|
1137
|
+
let errorDesc;
|
|
1138
|
+
if (frontText) {
|
|
1139
|
+
errorDesc = `<$red:${frontText}$>`;
|
|
1140
|
+
} else {
|
|
1141
|
+
errorDesc = `<$red:Expected$> <$blue:'${expectedValue}'$>`;
|
|
1142
|
+
if (behindValue) errorDesc += ` <$red:after$> <$blue:'${behindValue}'$>`;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const tokenDisplay = current.value === "" ? "end of input"
|
|
1146
|
+
: current.value === "\n" ? "newline (\\n)"
|
|
1147
|
+
: `'${current.value}'`;
|
|
1148
|
+
|
|
1149
|
+
const parts = [`{line}`];
|
|
1150
|
+
if (source) parts.push(`<$cyan:File:$> ${source}{N}`);
|
|
1151
|
+
parts.push(`<$cyan:Line:$> <$yellow:${lineNum}$> <$cyan:Col:$> <$yellow:${colDisplay}$>{N}`);
|
|
1152
|
+
parts.push(`{line}`);
|
|
1153
|
+
parts.push(`<$red:Here where error occurred:$>{N}`);
|
|
1154
|
+
parts.push(` ${lineContent}{N}`);
|
|
1155
|
+
parts.push(` ${pointerPadding}<$yellow:^$>{N}`);
|
|
1156
|
+
parts.push(`${errorDesc}{N}`);
|
|
1157
|
+
parts.push(`<$yellow:Received:$> <$blue:${tokenDisplay}$>{N}`);
|
|
1158
|
+
parts.push(`{line}`);
|
|
1159
|
+
return parts;
|
|
1449
1160
|
};
|
|
1450
1161
|
// ========================================================================== //
|
|
1451
1162
|
// Parse Key //
|
|
@@ -1467,6 +1178,88 @@ function parseKey(tokens, i) {
|
|
|
1467
1178
|
return [key, i];
|
|
1468
1179
|
}
|
|
1469
1180
|
// ========================================================================== //
|
|
1181
|
+
// Read Prefix Key/Fallback from structured p{}/v{} tokens //
|
|
1182
|
+
// ========================================================================== //
|
|
1183
|
+
function readPrefixKeyFallback(tokens, i, prefixType = "p") {
|
|
1184
|
+
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.PREFIX_OPEN) i++;
|
|
1185
|
+
i = skipJunk(tokens, i);
|
|
1186
|
+
|
|
1187
|
+
let key = "";
|
|
1188
|
+
let fallback = undefined;
|
|
1189
|
+
|
|
1190
|
+
// Read key — must be quoted or unquoted identifier
|
|
1191
|
+
const keyToken = current_token(tokens, i);
|
|
1192
|
+
if (!keyToken || keyToken.type === TOKEN_TYPES.PREFIX_CLOSE) {
|
|
1193
|
+
parserError(errorMessage(tokens, i, "key", "{", 'Prefix requires a key — write p{key} or p{key | "fallback"}'));
|
|
1194
|
+
}
|
|
1195
|
+
if (keyToken.type === TOKEN_TYPES.QUOTE) {
|
|
1196
|
+
i++; // skip opening QUOTE
|
|
1197
|
+
while (current_token(tokens, i) &&
|
|
1198
|
+
current_token(tokens, i).type !== TOKEN_TYPES.QUOTE &&
|
|
1199
|
+
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_CLOSE &&
|
|
1200
|
+
current_token(tokens, i).type !== TOKEN_TYPES.PIPELINE) {
|
|
1201
|
+
key += current_token(tokens, i).value;
|
|
1202
|
+
i++;
|
|
1203
|
+
}
|
|
1204
|
+
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.QUOTE) i++;
|
|
1205
|
+
} else if (keyToken.type === TOKEN_TYPES.KEY) {
|
|
1206
|
+
key = keyToken.value.trim();
|
|
1207
|
+
const isValidIdent = /^[a-zA-Z_$][a-zA-Z0-9_$-]*$/.test(key);
|
|
1208
|
+
const isNumeric = /^\d+$/.test(key);
|
|
1209
|
+
// p{} keys must be identifiers; v{} keys may also be positional integers
|
|
1210
|
+
if (!isValidIdent && !(prefixType === "v" && isNumeric)) {
|
|
1211
|
+
parserError(errorMessage(tokens, i, "key", "{", `Invalid prefix key '${key}' — must start with a letter, _ or $`));
|
|
1212
|
+
}
|
|
1213
|
+
i++;
|
|
1214
|
+
} else {
|
|
1215
|
+
parserError(errorMessage(tokens, i, "key", "{", "Invalid prefix key — must be a quoted string or identifier"));
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
i = skipJunk(tokens, i);
|
|
1219
|
+
|
|
1220
|
+
// After key: only | or } is valid
|
|
1221
|
+
const afterKey = current_token(tokens, i);
|
|
1222
|
+
if (!afterKey || (afterKey.type !== TOKEN_TYPES.PIPELINE && afterKey.type !== TOKEN_TYPES.PREFIX_CLOSE)) {
|
|
1223
|
+
parserError(errorMessage(tokens, i, "| or }", key, "Expected '|' or '}' after prefix key"));
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.PIPELINE) {
|
|
1227
|
+
i++; // skip PIPELINE
|
|
1228
|
+
i = skipJunk(tokens, i);
|
|
1229
|
+
|
|
1230
|
+
// Fallback must be a quoted string — any content allowed inside quotes
|
|
1231
|
+
const fallbackToken = current_token(tokens, i);
|
|
1232
|
+
if (!fallbackToken || fallbackToken.type === TOKEN_TYPES.PREFIX_CLOSE) {
|
|
1233
|
+
parserError(errorMessage(tokens, i, '"fallback"', "|", 'Expected a quoted fallback after \'|\' — write p{key | "default"}'));
|
|
1234
|
+
}
|
|
1235
|
+
if (fallbackToken.type === TOKEN_TYPES.QUOTE) {
|
|
1236
|
+
fallback = "";
|
|
1237
|
+
i++; // skip opening QUOTE
|
|
1238
|
+
while (current_token(tokens, i) &&
|
|
1239
|
+
current_token(tokens, i).type !== TOKEN_TYPES.QUOTE &&
|
|
1240
|
+
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_CLOSE) {
|
|
1241
|
+
fallback += current_token(tokens, i).value;
|
|
1242
|
+
i++;
|
|
1243
|
+
}
|
|
1244
|
+
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.QUOTE) i++;
|
|
1245
|
+
} else {
|
|
1246
|
+
parserError(errorMessage(tokens, i, '"fallback"', "|", 'Fallback must be a quoted string — write p{key | "default"}'));
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
i = skipJunk(tokens, i);
|
|
1251
|
+
|
|
1252
|
+
// After key (or fallback): only } is valid
|
|
1253
|
+
const afterFallback = current_token(tokens, i);
|
|
1254
|
+
if (!afterFallback || afterFallback.type !== TOKEN_TYPES.PREFIX_CLOSE) {
|
|
1255
|
+
parserError(errorMessage(tokens, i, "}", key, "Unexpected content inside prefix — only one key and one optional fallback are allowed"));
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.PREFIX_CLOSE) i++;
|
|
1259
|
+
|
|
1260
|
+
return [key, fallback, i];
|
|
1261
|
+
}
|
|
1262
|
+
// ========================================================================== //
|
|
1470
1263
|
// Parse Value //
|
|
1471
1264
|
// ========================================================================== //
|
|
1472
1265
|
function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = true) {
|
|
@@ -1477,7 +1270,7 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
1477
1270
|
val = "";
|
|
1478
1271
|
while (i < tokens.length && current_token(tokens, i).type !== TOKEN_TYPES.QUOTE) {
|
|
1479
1272
|
const token = current_token(tokens, i);
|
|
1480
|
-
if (token.type === TOKEN_TYPES.PREFIX_P || token.type === TOKEN_TYPES.
|
|
1273
|
+
if (token.type === TOKEN_TYPES.PREFIX_P || token.type === TOKEN_TYPES.PREFIX_V) {
|
|
1481
1274
|
const [resolvedVal, nextI] = parseValue(tokens, i, placeholders, variables, allowLogic);
|
|
1482
1275
|
val += resolvedVal;
|
|
1483
1276
|
i = nextI;
|
|
@@ -1492,73 +1285,56 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
1492
1285
|
}
|
|
1493
1286
|
|
|
1494
1287
|
i++; // consume closing QUOTE
|
|
1495
|
-
return [val, i, true];
|
|
1496
|
-
} else if (current_token(tokens, i).type === TOKEN_TYPES.
|
|
1497
|
-
val = current_token(tokens, i).value;
|
|
1498
|
-
// V4 NATIVE DATA: Strip js{ } and parse safely
|
|
1499
|
-
if (val.startsWith("js{") && val.endsWith("}")) {
|
|
1500
|
-
const clean = val.slice(3, -1).trim();
|
|
1501
|
-
val = safeDataParse(clean);
|
|
1502
|
-
}
|
|
1503
|
-
i++;
|
|
1504
|
-
return [val, i, false];
|
|
1505
|
-
} 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) {
|
|
1288
|
+
return [val, i, true];
|
|
1289
|
+
} else if (current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD || current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD) {
|
|
1506
1290
|
if (!allowLogic) {
|
|
1507
1291
|
parserError(errorMessage(tokens, i, "literal value", "", "Logic blocks are not allowed in this context."));
|
|
1508
1292
|
}
|
|
1509
1293
|
let isStatic = current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD;
|
|
1510
|
-
let
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
if (!current_token(tokens, nextI) || current_token(tokens, nextI).type !== TOKEN_TYPES.LOGIC) {
|
|
1516
|
-
// Treat as literal text if keyword is not followed by a logic block
|
|
1517
|
-
return [current_token(tokens, i).value, i + 1, false];
|
|
1518
|
-
}
|
|
1519
|
-
i = nextI;
|
|
1294
|
+
let nextI = skipJunk(tokens, i + 1);
|
|
1295
|
+
|
|
1296
|
+
if (!current_token(tokens, nextI) || current_token(tokens, nextI).type !== TOKEN_TYPES.LOGIC_OPEN) {
|
|
1297
|
+
// Keyword not followed by ${ — treat as literal text
|
|
1298
|
+
return [current_token(tokens, i).value, i + 1, false];
|
|
1520
1299
|
}
|
|
1521
1300
|
|
|
1522
|
-
|
|
1301
|
+
// Skip LOGIC_OPEN, read LOGIC body
|
|
1302
|
+
nextI++;
|
|
1303
|
+
const logicToken = current_token(tokens, nextI);
|
|
1523
1304
|
const node = makeLogicNode(isStatic ? STATIC_LOGIC : RUNTIME_LOGIC);
|
|
1524
|
-
node.code = logicToken.value;
|
|
1525
|
-
node.range = logicToken.range;
|
|
1305
|
+
node.code = logicToken ? logicToken.value : "";
|
|
1306
|
+
node.range = logicToken ? logicToken.range : current_token(tokens, i).range;
|
|
1307
|
+
nextI++;
|
|
1308
|
+
|
|
1309
|
+
// Consume LOGIC_CLOSE if present
|
|
1310
|
+
if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
|
|
1311
|
+
nextI++;
|
|
1312
|
+
}
|
|
1526
1313
|
|
|
1527
|
-
return [node,
|
|
1314
|
+
return [node, nextI, false];
|
|
1528
1315
|
} else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_V) {
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
});
|
|
1541
|
-
}
|
|
1542
|
-
variables.__consumed__.add(key);
|
|
1543
|
-
} else {
|
|
1544
|
-
val = getPrefixValue('v', key);
|
|
1316
|
+
i++; // consume PREFIX_V keyword
|
|
1317
|
+
const [vKey, vFallback, vNextI] = readPrefixKeyFallback(tokens, i, "v");
|
|
1318
|
+
i = vNextI;
|
|
1319
|
+
if (variables[vKey] !== undefined) {
|
|
1320
|
+
val = variables[vKey];
|
|
1321
|
+
if (!variables.__consumed__) {
|
|
1322
|
+
Object.defineProperty(variables, "__consumed__", {
|
|
1323
|
+
value: new Set(),
|
|
1324
|
+
enumerable: false,
|
|
1325
|
+
configurable: true
|
|
1326
|
+
});
|
|
1545
1327
|
}
|
|
1328
|
+
variables.__consumed__.add(vKey);
|
|
1329
|
+
} else {
|
|
1330
|
+
val = vFallback !== undefined ? vFallback : getPrefixValue('v', vKey);
|
|
1546
1331
|
}
|
|
1547
|
-
i++;
|
|
1548
|
-
return [val, i, false];
|
|
1549
|
-
} else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_C) {
|
|
1550
|
-
val = current_token(tokens, i).value;
|
|
1551
|
-
// PREFIX_C is preserved for the resolveModules expansion phase
|
|
1552
|
-
i++;
|
|
1553
1332
|
return [val, i, false];
|
|
1554
1333
|
} else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
val = placeholders[key] !== undefined ? placeholders[key] : getPrefixValue('p', key);
|
|
1560
|
-
}
|
|
1561
|
-
i++;
|
|
1334
|
+
i++; // consume PREFIX_P keyword
|
|
1335
|
+
const [pKey, pFallback, pNextI] = readPrefixKeyFallback(tokens, i);
|
|
1336
|
+
i = pNextI;
|
|
1337
|
+
val = placeholders[pKey] !== undefined ? placeholders[pKey] : (pFallback !== undefined ? pFallback : getPrefixValue('p', pKey));
|
|
1562
1338
|
return [val, i, false];
|
|
1563
1339
|
} else {
|
|
1564
1340
|
val = "";
|
|
@@ -1571,9 +1347,7 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
1571
1347
|
token.type === TOKEN_TYPES.COMMA ||
|
|
1572
1348
|
token.type === TOKEN_TYPES.CLOSE_BRACKET ||
|
|
1573
1349
|
token.type === TOKEN_TYPES.COLON ||
|
|
1574
|
-
token.type === TOKEN_TYPES.
|
|
1575
|
-
token.type === TOKEN_TYPES.EXCLAMATION_MARK ||
|
|
1576
|
-
token.type === TOKEN_TYPES.CLOSE_PAREN) break;
|
|
1350
|
+
token.type === TOKEN_TYPES.EXCLAMATION_MARK) break;
|
|
1577
1351
|
|
|
1578
1352
|
if (token.type === TOKEN_TYPES.ESCAPE) {
|
|
1579
1353
|
// Remove backslash
|
|
@@ -1589,34 +1363,6 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
1589
1363
|
return [val, i, false];
|
|
1590
1364
|
}
|
|
1591
1365
|
// ========================================================================== //
|
|
1592
|
-
// Parse ',' //
|
|
1593
|
-
// ========================================================================== //
|
|
1594
|
-
function parseComma(tokens, i, beforeChar = "") {
|
|
1595
|
-
i = skipJunk(tokens, i);
|
|
1596
|
-
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COMMA) {
|
|
1597
|
-
i++;
|
|
1598
|
-
i = skipJunk(tokens, i);
|
|
1599
|
-
updateData(tokens, i);
|
|
1600
|
-
|
|
1601
|
-
if (
|
|
1602
|
-
!current_token(tokens, i) ||
|
|
1603
|
-
(current_token(tokens, i) &&
|
|
1604
|
-
current_token(tokens, i).type !== TOKEN_TYPES.VALUE &&
|
|
1605
|
-
current_token(tokens, i).type !== TOKEN_TYPES.ESCAPE &&
|
|
1606
|
-
current_token(tokens, i).type !== TOKEN_TYPES.IDENTIFIER &&
|
|
1607
|
-
current_token(tokens, i).type !== TOKEN_TYPES.KEY &&
|
|
1608
|
-
current_token(tokens, i).type !== TOKEN_TYPES.QUOTE &&
|
|
1609
|
-
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_JS &&
|
|
1610
|
-
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_P)
|
|
1611
|
-
) {
|
|
1612
|
-
parserError(errorMessage(tokens, i, "value", ","));
|
|
1613
|
-
}
|
|
1614
|
-
} else {
|
|
1615
|
-
parserError(errorMessage(tokens, i, ",", beforeChar));
|
|
1616
|
-
}
|
|
1617
|
-
return i;
|
|
1618
|
-
}
|
|
1619
|
-
// ========================================================================== //
|
|
1620
1366
|
// Parse ':' //
|
|
1621
1367
|
// ========================================================================== //
|
|
1622
1368
|
function parseColon(tokens, i, afterChar = "") {
|
|
@@ -1629,19 +1375,6 @@ function parseColon(tokens, i, afterChar = "") {
|
|
|
1629
1375
|
updateData(tokens, i);
|
|
1630
1376
|
return i;
|
|
1631
1377
|
}
|
|
1632
|
-
// ========================================================================== //
|
|
1633
|
-
// Parse ';' //
|
|
1634
|
-
// ========================================================================== //
|
|
1635
|
-
function parseSemiColon(tokens, i, afterChar = "") {
|
|
1636
|
-
i = skipJunk(tokens, i);
|
|
1637
|
-
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.SEMICOLON)) {
|
|
1638
|
-
parserError(errorMessage(tokens, i, ";", afterChar));
|
|
1639
|
-
}
|
|
1640
|
-
i++;
|
|
1641
|
-
i = skipJunk(tokens, i);
|
|
1642
|
-
updateData(tokens, i);
|
|
1643
|
-
return i;
|
|
1644
|
-
}
|
|
1645
1378
|
/**
|
|
1646
1379
|
* Parses a standard SomMark Block ([id] ... [end]).
|
|
1647
1380
|
* Blocks are structural elements that can contain nested content.
|
|
@@ -1664,7 +1397,7 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
1664
1397
|
updateData(tokens, i);
|
|
1665
1398
|
|
|
1666
1399
|
const idToken = current_token(tokens, i);
|
|
1667
|
-
if (!idToken || idToken.type === TOKEN_TYPES.EOF) {
|
|
1400
|
+
if (!idToken || idToken.type === TOKEN_TYPES.EOF || idToken.type === TOKEN_TYPES.CLOSE_BRACKET) {
|
|
1668
1401
|
parserError(errorMessage(tokens, i, "Block ID", "[", "Missing Block Identifier"));
|
|
1669
1402
|
}
|
|
1670
1403
|
const id = idToken.value;
|
|
@@ -1716,10 +1449,9 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
1716
1449
|
current_token(tokens, i).type !== TOKEN_TYPES.END_KEYWORD &&
|
|
1717
1450
|
current_token(tokens, i).type !== TOKEN_TYPES.KEY &&
|
|
1718
1451
|
current_token(tokens, i).type !== TOKEN_TYPES.QUOTE &&
|
|
1719
|
-
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_JS &&
|
|
1720
1452
|
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_V &&
|
|
1721
1453
|
current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_P &&
|
|
1722
|
-
current_token(tokens, i).type !== TOKEN_TYPES.
|
|
1454
|
+
current_token(tokens, i).type !== TOKEN_TYPES.LOGIC_OPEN &&
|
|
1723
1455
|
current_token(tokens, i).type !== TOKEN_TYPES.STATIC_KEYWORD &&
|
|
1724
1456
|
current_token(tokens, i).type !== TOKEN_TYPES.RUNTIME_KEYWORD)
|
|
1725
1457
|
) {
|
|
@@ -1766,9 +1498,9 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
1766
1498
|
i = valueIndex;
|
|
1767
1499
|
|
|
1768
1500
|
// Store Argument
|
|
1769
|
-
blockNode.
|
|
1501
|
+
blockNode.props[String(argIndex++)] = v;
|
|
1770
1502
|
if (k) {
|
|
1771
|
-
blockNode.
|
|
1503
|
+
blockNode.props[k] = v;
|
|
1772
1504
|
}
|
|
1773
1505
|
k = "";
|
|
1774
1506
|
v = "";
|
|
@@ -1869,6 +1601,23 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
1869
1601
|
i++;
|
|
1870
1602
|
i = skipJunk(tokens, i);
|
|
1871
1603
|
updateData(tokens, i);
|
|
1604
|
+
|
|
1605
|
+
// Named closing: [end:blockname] — the lexer emits END_KEYWORD "end:name" as one
|
|
1606
|
+
// token because ':' is stripped from stop chars at block-start (XML namespace support).
|
|
1607
|
+
const endValue = current.value.trim();
|
|
1608
|
+
if (endValue.includes(":")) {
|
|
1609
|
+
const closingName = endValue.slice(endValue.indexOf(":") + 1);
|
|
1610
|
+
if (!closingName) {
|
|
1611
|
+
parserError(errorMessage(tokens, i - 1, "block name", "", "Missing block name — write [end:blockname] to name the closing tag"));
|
|
1612
|
+
}
|
|
1613
|
+
const expected = end_stack[end_stack.length - 1];
|
|
1614
|
+
if (expected && closingName !== expected.id) {
|
|
1615
|
+
parserError(errorMessage(tokens, i - 1, closingName, "",
|
|
1616
|
+
`Mismatched closing tag: [end:${closingName}] cannot close '${closingName}' — '${expected.id}' is still open (opened at line ${expected.line}, col ${expected.col})`
|
|
1617
|
+
));
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1872
1621
|
if (
|
|
1873
1622
|
!current_token(tokens, i) ||
|
|
1874
1623
|
(current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_BRACKET)
|
|
@@ -1903,147 +1652,6 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
1903
1652
|
}
|
|
1904
1653
|
return [blockNode, i];
|
|
1905
1654
|
}
|
|
1906
|
-
/**
|
|
1907
|
-
* Parses an Inline Statement ((content) -> (id)).
|
|
1908
|
-
* Inlines are fast, non-nesting formatting elements.
|
|
1909
|
-
*
|
|
1910
|
-
* @param {Object[]} tokens - Token stream.
|
|
1911
|
-
* @param {number} i - Initial index.
|
|
1912
|
-
* @param {Object} placeholders - Dynamic public API data.
|
|
1913
|
-
* @returns {[Object, number]} The parsed Inline node and new index.
|
|
1914
|
-
*/
|
|
1915
|
-
function parseInline(tokens, i, placeholders = {}, depth = 0) {
|
|
1916
|
-
const inlineNode = makeInlineNode();
|
|
1917
|
-
inlineNode.depth = depth;
|
|
1918
|
-
const openParenToken = current_token(tokens, i);
|
|
1919
|
-
inlineNode.range.start = openParenToken.range.start;
|
|
1920
|
-
|
|
1921
|
-
// consume '('
|
|
1922
|
-
i++;
|
|
1923
|
-
updateData(tokens, i);
|
|
1924
|
-
|
|
1925
|
-
// Phase 1: Content capture (Lexer provides high-level TEXT/ESCAPE tokens here)
|
|
1926
|
-
while (i < tokens.length) {
|
|
1927
|
-
const token = current_token(tokens, i);
|
|
1928
|
-
if (!token || token.type === TOKEN_TYPES.CLOSE_PAREN) break;
|
|
1929
|
-
|
|
1930
|
-
if (token.type === TOKEN_TYPES.ESCAPE) {
|
|
1931
|
-
inlineNode.value += token.value.slice(1);
|
|
1932
|
-
} else if (token.type !== TOKEN_TYPES.COMMENT) {
|
|
1933
|
-
inlineNode.value += token.value;
|
|
1934
|
-
}
|
|
1935
|
-
i++;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_PAREN) {
|
|
1939
|
-
parserError(errorMessage(tokens, i, ")", "inline content"));
|
|
1940
|
-
}
|
|
1941
|
-
i++; // consume ')'
|
|
1942
|
-
|
|
1943
|
-
// Collapse newlines and whitespace for "inline" behavior
|
|
1944
|
-
inlineNode.value = inlineNode.value.replace(/\s+/g, " ").trim();
|
|
1945
|
-
|
|
1946
|
-
i = skipJunk(tokens, i);
|
|
1947
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.THIN_ARROW) {
|
|
1948
|
-
parserError(errorMessage(tokens, i, "->", ")"));
|
|
1949
|
-
}
|
|
1950
|
-
i++; // consume '->'
|
|
1951
|
-
|
|
1952
|
-
i = skipJunk(tokens, i);
|
|
1953
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.OPEN_PAREN) {
|
|
1954
|
-
parserError(errorMessage(tokens, i, "(", "->"));
|
|
1955
|
-
}
|
|
1956
|
-
i++; // consume '('
|
|
1957
|
-
i = skipJunk(tokens, i);
|
|
1958
|
-
const idToken = current_token(tokens, i);
|
|
1959
|
-
const allowedInlineIdTypes = new Set([
|
|
1960
|
-
TOKEN_TYPES.IDENTIFIER,
|
|
1961
|
-
TOKEN_TYPES.KEY,
|
|
1962
|
-
TOKEN_TYPES.IMPORT,
|
|
1963
|
-
TOKEN_TYPES.USE_MODULE,
|
|
1964
|
-
TOKEN_TYPES.SLOT_KEYWORD,
|
|
1965
|
-
TOKEN_TYPES.FOR_EACH
|
|
1966
|
-
]);
|
|
1967
|
-
if (!idToken || !allowedInlineIdTypes.has(idToken.type)) {
|
|
1968
|
-
parserError(errorMessage(tokens, i, inline_id, "("));
|
|
1969
|
-
}
|
|
1970
|
-
inlineNode.id = idToken.value.trim();
|
|
1971
|
-
validateName(inlineNode.id);
|
|
1972
|
-
|
|
1973
|
-
i++; // consume ID
|
|
1974
|
-
i = skipJunk(tokens, i);
|
|
1975
|
-
|
|
1976
|
-
const hasArgsTrigger = current_token(tokens, i) && (
|
|
1977
|
-
current_token(tokens, i).type === TOKEN_TYPES.COLON ||
|
|
1978
|
-
current_token(tokens, i).type === TOKEN_TYPES.EQUAL
|
|
1979
|
-
);
|
|
1980
|
-
|
|
1981
|
-
if (hasArgsTrigger) {
|
|
1982
|
-
const separator = current_token(tokens, i).value;
|
|
1983
|
-
i++; // consume ':' or '='
|
|
1984
|
-
i = skipJunk(tokens, i);
|
|
1985
|
-
|
|
1986
|
-
// Ensure there is a value after the separator
|
|
1987
|
-
const nextToken = current_token(tokens, i);
|
|
1988
|
-
if (!nextToken || nextToken.type === TOKEN_TYPES.CLOSE_PAREN || nextToken.type === TOKEN_TYPES.COMMA) {
|
|
1989
|
-
parserError(errorMessage(tokens, i, inline_value, separator, `Missing value after ${separator === "=" ? "equals" : "colon"}`));
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
let k = "";
|
|
1993
|
-
let v = "";
|
|
1994
|
-
let argIndex = 0;
|
|
1995
|
-
|
|
1996
|
-
while (i < tokens.length) {
|
|
1997
|
-
i = skipJunk(tokens, i);
|
|
1998
|
-
const token = current_token(tokens, i);
|
|
1999
|
-
if (!token || token.type === TOKEN_TYPES.CLOSE_PAREN) break;
|
|
2000
|
-
|
|
2001
|
-
if (token.type === TOKEN_TYPES.KEY) {
|
|
2002
|
-
let [key, keyIndex] = parseKey(tokens, i);
|
|
2003
|
-
k = key;
|
|
2004
|
-
i = keyIndex;
|
|
2005
|
-
i = skipJunk(tokens, i);
|
|
2006
|
-
i = parseColon(tokens, i, "inline argument");
|
|
2007
|
-
i = skipJunk(tokens, i);
|
|
2008
|
-
|
|
2009
|
-
// Ensure there is a value after the colon
|
|
2010
|
-
const nextToken = current_token(tokens, i);
|
|
2011
|
-
if (!nextToken || nextToken.type === TOKEN_TYPES.CLOSE_PAREN || nextToken.type === TOKEN_TYPES.COMMA) {
|
|
2012
|
-
parserError(errorMessage(tokens, i, inline_value, ":", "Missing value after colon"));
|
|
2013
|
-
}
|
|
2014
|
-
validateName(k);
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
let [value, valueIndex, isQuoted] = parseValue(tokens, i, placeholders, {}, false);
|
|
2018
|
-
v = value;
|
|
2019
|
-
i = valueIndex;
|
|
2020
|
-
|
|
2021
|
-
inlineNode.args[String(argIndex++)] = v;
|
|
2022
|
-
if (k) {
|
|
2023
|
-
inlineNode.args[k] = v;
|
|
2024
|
-
}
|
|
2025
|
-
k = "";
|
|
2026
|
-
v = "";
|
|
2027
|
-
|
|
2028
|
-
i = skipJunk(tokens, i);
|
|
2029
|
-
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COMMA) {
|
|
2030
|
-
i = parseComma(tokens, i, "inline argument");
|
|
2031
|
-
} else {
|
|
2032
|
-
break;
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
i = skipJunk(tokens, i);
|
|
2038
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_PAREN) {
|
|
2039
|
-
parserError(errorMessage(tokens, i, ")", inlineNode.id));
|
|
2040
|
-
}
|
|
2041
|
-
const finalParenToken = current_token(tokens, i);
|
|
2042
|
-
i++; // consume ')'
|
|
2043
|
-
inlineNode.range.end = finalParenToken.range.end;
|
|
2044
|
-
|
|
2045
|
-
return [inlineNode, i];
|
|
2046
|
-
}
|
|
2047
1655
|
/**
|
|
2048
1656
|
* Parses a stream of text tokens into a single Text node.
|
|
2049
1657
|
* Handles unescaping and placeholder resolution.
|
|
@@ -2071,7 +1679,7 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
|
|
|
2071
1679
|
i++;
|
|
2072
1680
|
} else if (token.type === TOKEN_TYPES.STATIC_KEYWORD || token.type === TOKEN_TYPES.RUNTIME_KEYWORD) {
|
|
2073
1681
|
const nextIdx = skipJunk(tokens, i + 1);
|
|
2074
|
-
if (tokens[nextIdx] && tokens[nextIdx].type === TOKEN_TYPES.
|
|
1682
|
+
if (tokens[nextIdx] && tokens[nextIdx].type === TOKEN_TYPES.LOGIC_OPEN) {
|
|
2075
1683
|
// Stop consuming text; this is the start of a logic block
|
|
2076
1684
|
break;
|
|
2077
1685
|
}
|
|
@@ -2090,44 +1698,31 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
|
|
|
2090
1698
|
}
|
|
2091
1699
|
i++;
|
|
2092
1700
|
} else if (token.type === TOKEN_TYPES.PREFIX_P) {
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
if (placeholders[key] !== undefined) {
|
|
2100
|
-
textNode.text += String(placeholders[key]);
|
|
2101
|
-
} else {
|
|
2102
|
-
// Use the unique 'Unresolved Envelope' format via helper
|
|
2103
|
-
textNode.text += getPrefixValue(layer, key);
|
|
2104
|
-
}
|
|
1701
|
+
i++; // consume PREFIX_P keyword
|
|
1702
|
+
const [tpKey, tpFallback, tpNextI] = readPrefixKeyFallback(tokens, i);
|
|
1703
|
+
i = tpNextI;
|
|
1704
|
+
if (placeholders[tpKey] !== undefined) {
|
|
1705
|
+
textNode.text += String(placeholders[tpKey]);
|
|
2105
1706
|
} else {
|
|
2106
|
-
textNode.text +=
|
|
1707
|
+
textNode.text += tpFallback !== undefined ? tpFallback : getPrefixValue('p', tpKey);
|
|
2107
1708
|
}
|
|
2108
|
-
i++;
|
|
2109
1709
|
} else if (token.type === TOKEN_TYPES.PREFIX_V) {
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
}
|
|
2122
|
-
variables.__consumed__.add(key);
|
|
2123
|
-
} else {
|
|
2124
|
-
// Use the unique 'Unresolved Envelope' format via helper
|
|
2125
|
-
textNode.text += getPrefixValue('v', key);
|
|
1710
|
+
i++; // consume PREFIX_V keyword
|
|
1711
|
+
const [tvKey, tvFallback, tvNextI] = readPrefixKeyFallback(tokens, i, "v");
|
|
1712
|
+
i = tvNextI;
|
|
1713
|
+
if (variables[tvKey] !== undefined) {
|
|
1714
|
+
textNode.text += String(variables[tvKey]);
|
|
1715
|
+
if (!variables.__consumed__) {
|
|
1716
|
+
Object.defineProperty(variables, "__consumed__", {
|
|
1717
|
+
value: new Set(),
|
|
1718
|
+
enumerable: false,
|
|
1719
|
+
configurable: true
|
|
1720
|
+
});
|
|
2126
1721
|
}
|
|
1722
|
+
variables.__consumed__.add(tvKey);
|
|
2127
1723
|
} else {
|
|
2128
|
-
textNode.text +=
|
|
1724
|
+
textNode.text += tvFallback !== undefined ? tvFallback : getPrefixValue('v', tvKey);
|
|
2129
1725
|
}
|
|
2130
|
-
i++;
|
|
2131
1726
|
} else {
|
|
2132
1727
|
break;
|
|
2133
1728
|
}
|
|
@@ -2137,155 +1732,6 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
|
|
|
2137
1732
|
}
|
|
2138
1733
|
return [textNode, i];
|
|
2139
1734
|
}
|
|
2140
|
-
/**
|
|
2141
|
-
* Parses an At-Block (@_id_@: args; content @_end_@).
|
|
2142
|
-
* At-Blocks maintain raw content preservation.
|
|
2143
|
-
*
|
|
2144
|
-
* @param {Object[]} tokens - Token stream.
|
|
2145
|
-
* @param {number} i - Initial index.
|
|
2146
|
-
* @param {string|null} filename - Source filename.
|
|
2147
|
-
* @param {Object} placeholders - Dynamic public API data.
|
|
2148
|
-
* @returns {[Object, number]} The At-Block node and new index.
|
|
2149
|
-
*/
|
|
2150
|
-
function parseAtBlock(tokens, i, filename = null, placeholders = {}, depth = 0) {
|
|
2151
|
-
const atBlockNode = makeAtBlockNode();
|
|
2152
|
-
atBlockNode.depth = depth;
|
|
2153
|
-
const openAtToken = current_token(tokens, i);
|
|
2154
|
-
atBlockNode.range.start = openAtToken.range.start;
|
|
2155
|
-
|
|
2156
|
-
// consume '@_'
|
|
2157
|
-
i++;
|
|
2158
|
-
i = skipJunk(tokens, i);
|
|
2159
|
-
updateData(tokens, i);
|
|
2160
|
-
|
|
2161
|
-
const idToken = current_token(tokens, i);
|
|
2162
|
-
if (!idToken || idToken.type === TOKEN_TYPES.EOF) {
|
|
2163
|
-
parserError(errorMessage(tokens, i, "AtBlock ID", "@_", "Missing AtBlock Identifier"));
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
const id = idToken.value;
|
|
2167
|
-
if (id.trim() === end_keyword) {
|
|
2168
|
-
parserError(errorMessage(tokens, i, id, "", `'${id.trim()}' is a reserved keyword and cannot be used as an identifier.`));
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
atBlockNode.id = id.trim();
|
|
2172
|
-
validateName(atBlockNode.id);
|
|
2173
|
-
|
|
2174
|
-
// consume ID
|
|
2175
|
-
i++;
|
|
2176
|
-
i = skipJunk(tokens, i);
|
|
2177
|
-
updateData(tokens, i);
|
|
2178
|
-
|
|
2179
|
-
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_AT)) {
|
|
2180
|
-
parserError(errorMessage(tokens, i, "_@", "at-block identifier"));
|
|
2181
|
-
}
|
|
2182
|
-
// consume '_@'
|
|
2183
|
-
i++;
|
|
2184
|
-
i = skipJunk(tokens, i);
|
|
2185
|
-
updateData(tokens, i);
|
|
2186
|
-
|
|
2187
|
-
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COLON) {
|
|
2188
|
-
// consume ':'
|
|
2189
|
-
i++;
|
|
2190
|
-
i = skipJunk(tokens, i);
|
|
2191
|
-
|
|
2192
|
-
// Ensure there is a value after the colon
|
|
2193
|
-
const nextToken = current_token(tokens, i);
|
|
2194
|
-
if (!nextToken || nextToken.type === TOKEN_TYPES.SEMICOLON || nextToken.type === TOKEN_TYPES.COMMA) {
|
|
2195
|
-
parserError(errorMessage(tokens, i, at_value, ":", "Missing value after colon"));
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
let k = "";
|
|
2199
|
-
let v = "";
|
|
2200
|
-
let argIndex = 0;
|
|
2201
|
-
|
|
2202
|
-
while (i < tokens.length) {
|
|
2203
|
-
i = skipJunk(tokens, i);
|
|
2204
|
-
const token = current_token(tokens, i);
|
|
2205
|
-
if (!token || token.type === TOKEN_TYPES.SEMICOLON) break;
|
|
2206
|
-
|
|
2207
|
-
const isQuotedKey = token.type === TOKEN_TYPES.QUOTE && peek(tokens, i, 1) && (peek(tokens, i, 1).type === TOKEN_TYPES.KEY);
|
|
2208
|
-
|
|
2209
|
-
if (token.type === TOKEN_TYPES.KEY || isQuotedKey) {
|
|
2210
|
-
let [key, keyIndex] = parseKey(tokens, i);
|
|
2211
|
-
k = key;
|
|
2212
|
-
i = keyIndex;
|
|
2213
|
-
i = skipJunk(tokens, i);
|
|
2214
|
-
i = parseColon(tokens, i, "at-block argument");
|
|
2215
|
-
i = skipJunk(tokens, i);
|
|
2216
|
-
|
|
2217
|
-
// Ensure there is a value after the colon
|
|
2218
|
-
const nextToken = current_token(tokens, i);
|
|
2219
|
-
if (!nextToken || nextToken.type === TOKEN_TYPES.SEMICOLON || nextToken.type === TOKEN_TYPES.COMMA) {
|
|
2220
|
-
parserError(errorMessage(tokens, i, at_value, ":", "Missing value after colon"));
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
if (token.type === TOKEN_TYPES.KEY) {
|
|
2224
|
-
validateName(k);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
let [value, valueIndex, isQuoted] = parseValue(tokens, i, placeholders, {}, false);
|
|
2229
|
-
v = value;
|
|
2230
|
-
i = valueIndex;
|
|
2231
|
-
|
|
2232
|
-
atBlockNode.args[String(argIndex++)] = v;
|
|
2233
|
-
if (k) {
|
|
2234
|
-
atBlockNode.args[k] = v;
|
|
2235
|
-
}
|
|
2236
|
-
k = "";
|
|
2237
|
-
v = "";
|
|
2238
|
-
|
|
2239
|
-
i = skipJunk(tokens, i);
|
|
2240
|
-
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COMMA) {
|
|
2241
|
-
i = parseComma(tokens, i, "at-block argument");
|
|
2242
|
-
} else {
|
|
2243
|
-
break;
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
// Semicolon is ALWAYS required after ID or ARGS
|
|
2249
|
-
i = parseSemiColon(tokens, i, "at-block header");
|
|
2250
|
-
|
|
2251
|
-
// Body Capture
|
|
2252
|
-
i = skipJunk(tokens, i);
|
|
2253
|
-
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.TEXT) {
|
|
2254
|
-
atBlockNode.content = current_token(tokens, i).value;
|
|
2255
|
-
i++;
|
|
2256
|
-
} else {
|
|
2257
|
-
parserError(errorMessage(tokens, i, "content", "at-block body"));
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
// End Marker (@_end_@)
|
|
2261
|
-
i = skipJunk(tokens, i);
|
|
2262
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.OPEN_AT) {
|
|
2263
|
-
parserError(errorMessage(tokens, i, "@_", "at-block content"));
|
|
2264
|
-
}
|
|
2265
|
-
i++; // consume '@_'
|
|
2266
|
-
i = skipJunk(tokens, i);
|
|
2267
|
-
const endToken = current_token(tokens, i);
|
|
2268
|
-
if (!endToken || (endToken.type !== TOKEN_TYPES.END_KEYWORD && endToken.value.trim() !== end_keyword)) {
|
|
2269
|
-
let extraInfo = "";
|
|
2270
|
-
if (endToken && endToken.value) {
|
|
2271
|
-
const dist = levenshtein(endToken.value.trim().toLowerCase(), "end");
|
|
2272
|
-
if (dist > 0 && dist <= 2) {
|
|
2273
|
-
extraInfo = ` (Did you mean '@_end_@'?)`;
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
parserError(errorMessage(tokens, i, "end", "AtBlock Body", extraInfo));
|
|
2277
|
-
}
|
|
2278
|
-
i++; // consume 'end'
|
|
2279
|
-
i = skipJunk(tokens, i);
|
|
2280
|
-
if (!current_token(tokens, i) || current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_AT) {
|
|
2281
|
-
parserError(errorMessage(tokens, i, "_@", "end marker"));
|
|
2282
|
-
}
|
|
2283
|
-
const closeAtToken = current_token(tokens, i);
|
|
2284
|
-
i++; // consume '_@'
|
|
2285
|
-
atBlockNode.range.end = closeAtToken.range.end;
|
|
2286
|
-
|
|
2287
|
-
return [atBlockNode, i];
|
|
2288
|
-
}
|
|
2289
1735
|
// ========================================================================== //
|
|
2290
1736
|
// Parse Comments //
|
|
2291
1737
|
// ========================================================================== //
|
|
@@ -2341,72 +1787,36 @@ function parseNode(tokens, i, filename = null, placeholders = {}, variables = {}
|
|
|
2341
1787
|
return parseBlock(tokens, i, filename, placeholders, variables, depth);
|
|
2342
1788
|
}
|
|
2343
1789
|
// ========================================================================== //
|
|
2344
|
-
// Inline Statement or Text //
|
|
2345
|
-
// ========================================================================== //
|
|
2346
|
-
else if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.OPEN_PAREN) {
|
|
2347
|
-
let j = i + 1;
|
|
2348
|
-
let parenCount = 1;
|
|
2349
|
-
let foundArrow = false;
|
|
2350
|
-
while (j < tokens.length) {
|
|
2351
|
-
const token = tokens[j];
|
|
2352
|
-
if (token.type === TOKEN_TYPES.OPEN_PAREN) {
|
|
2353
|
-
parenCount++;
|
|
2354
|
-
} else if (token.type === TOKEN_TYPES.CLOSE_PAREN) {
|
|
2355
|
-
parenCount--;
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
if (parenCount === 0) {
|
|
2359
|
-
const nextIdx = skipJunk(tokens, j + 1);
|
|
2360
|
-
if (tokens[nextIdx] && tokens[nextIdx].type === TOKEN_TYPES.THIN_ARROW) {
|
|
2361
|
-
foundArrow = true;
|
|
2362
|
-
}
|
|
2363
|
-
break;
|
|
2364
|
-
}
|
|
2365
|
-
// Safe-guard: If we hit a [ or @, it's highly unlikely to be an inline statement content
|
|
2366
|
-
// unless it's escaped, but lexer already handles [ and @ as structural tokens if not escaped.
|
|
2367
|
-
if (token.type === TOKEN_TYPES.OPEN_BRACKET || token.type === TOKEN_TYPES.OPEN_AT) break;
|
|
2368
|
-
j++;
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
if (foundArrow) {
|
|
2372
|
-
return parseInline(tokens, i, placeholders, depth);
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
// Treat as text if not an inline
|
|
2376
|
-
const textNode = makeTextNode();
|
|
2377
|
-
textNode.text = current_token(tokens, i).value;
|
|
2378
|
-
textNode.depth = depth;
|
|
2379
|
-
textNode.range = current_token(tokens, i).range;
|
|
2380
|
-
return [textNode, i + 1];
|
|
2381
|
-
}
|
|
2382
|
-
// ========================================================================== //
|
|
2383
1790
|
// Logic Block //
|
|
2384
1791
|
// ========================================================================== //
|
|
2385
|
-
else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD || current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD
|
|
1792
|
+
else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD || current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD)) {
|
|
2386
1793
|
let isStatic = current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD;
|
|
2387
|
-
let isRuntimeKeyword = current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD;
|
|
2388
1794
|
let startRange = current_token(tokens, i).range;
|
|
2389
|
-
let nextI = i;
|
|
1795
|
+
let nextI = skipJunk(tokens, i + 1);
|
|
2390
1796
|
|
|
2391
|
-
if (
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
// Treat as normal text if keyword is not followed by a logic block
|
|
2395
|
-
return parseText(tokens, i, placeholders, variables, depth);
|
|
2396
|
-
}
|
|
2397
|
-
i = nextI;
|
|
1797
|
+
if (!current_token(tokens, nextI) || current_token(tokens, nextI).type !== TOKEN_TYPES.LOGIC_OPEN) {
|
|
1798
|
+
// Keyword not followed by ${ — treat as normal text
|
|
1799
|
+
return parseText(tokens, i, placeholders, variables, depth);
|
|
2398
1800
|
}
|
|
2399
1801
|
|
|
2400
|
-
|
|
1802
|
+
// Skip LOGIC_OPEN, read LOGIC body
|
|
1803
|
+
nextI++;
|
|
1804
|
+
const logicToken = current_token(tokens, nextI);
|
|
2401
1805
|
const node = makeLogicNode(isStatic ? STATIC_LOGIC : RUNTIME_LOGIC);
|
|
2402
|
-
node.code = logicToken.value;
|
|
1806
|
+
node.code = logicToken ? logicToken.value : "";
|
|
2403
1807
|
node.depth = depth;
|
|
2404
1808
|
node.range = {
|
|
2405
|
-
start:
|
|
2406
|
-
end: logicToken.range.end
|
|
1809
|
+
start: startRange.start,
|
|
1810
|
+
end: logicToken ? logicToken.range.end : startRange.end
|
|
2407
1811
|
};
|
|
1812
|
+
nextI++;
|
|
1813
|
+
|
|
1814
|
+
// Consume LOGIC_CLOSE if present
|
|
1815
|
+
if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
|
|
1816
|
+
nextI++;
|
|
1817
|
+
}
|
|
2408
1818
|
|
|
2409
|
-
return [node,
|
|
1819
|
+
return [node, nextI];
|
|
2410
1820
|
}
|
|
2411
1821
|
// ========================================================================== //
|
|
2412
1822
|
// Text or Placeholder //
|
|
@@ -2421,12 +1831,6 @@ function parseNode(tokens, i, filename = null, placeholders = {}, variables = {}
|
|
|
2421
1831
|
current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P)
|
|
2422
1832
|
) {
|
|
2423
1833
|
return parseText(tokens, i, placeholders, variables, depth);
|
|
2424
|
-
}
|
|
2425
|
-
// ========================================================================== //
|
|
2426
|
-
// Atblock //
|
|
2427
|
-
// ========================================================================== //
|
|
2428
|
-
else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.OPEN_AT)) {
|
|
2429
|
-
return parseAtBlock(tokens, i, filename, placeholders, depth);
|
|
2430
1834
|
} else {
|
|
2431
1835
|
// FALLBACK: Treat any other token as TEXT to avoid infinite loops and allow literal content
|
|
2432
1836
|
const textNode = makeTextNode();
|
|
@@ -2474,7 +1878,7 @@ function parser(tokens, filename = null, placeholders = {}, variables = {}) {
|
|
|
2474
1878
|
const val = token.value.trim().toLowerCase();
|
|
2475
1879
|
if (val === "") return "";
|
|
2476
1880
|
const dist = levenshtein(val, "end");
|
|
2477
|
-
if (dist > 0 && dist <= 2) return `
|
|
1881
|
+
if (dist > 0 && dist <= 2) return ` Did you mean '[end]'?`;
|
|
2478
1882
|
}
|
|
2479
1883
|
return "";
|
|
2480
1884
|
};
|