sommark 5.0.2 → 5.0.4
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/core/evaluator.js +37 -10
- package/core/evaluator.stub.js +1 -0
- package/core/helpers/lib.js +1 -1
- package/core/lexer.js +0 -7
- package/core/modules.js +64 -4
- package/core/parser.js +43 -2
- package/core/transpiler.js +81 -82
- package/dist/sommark.browser.js +225 -104
- package/dist/sommark.browser.lite.js +187 -93
- package/dist/sommark.lexer.js +0 -7
- package/dist/sommark.parser.js +42 -9
- package/index.shared.js +2 -1
- package/package.json +1 -1
|
@@ -801,13 +801,6 @@ function lexer(src, filename = "anonymous") {
|
|
|
801
801
|
// LOGIC BLOCKS (${ ... }$) — explicit: static/runtime ${ }$ shorthand: ${ }$ = static ${ }$
|
|
802
802
|
if (char === "$" && next === "{") {
|
|
803
803
|
{
|
|
804
|
-
const hasExplicitKeyword = last_non_junk_type === TOKEN_TYPES.STATIC_KEYWORD || last_non_junk_type === TOKEN_TYPES.RUNTIME_KEYWORD;
|
|
805
|
-
if (!hasExplicitKeyword) {
|
|
806
|
-
// Zero-width: synthetic token has no source presence, must not shift position
|
|
807
|
-
tokens.push({ type: TOKEN_TYPES.STATIC_KEYWORD, value: "static", source: filename, range: { start: { line, character }, end: { line, character } } });
|
|
808
|
-
TOKEN_TYPES.STATIC_KEYWORD;
|
|
809
|
-
last_non_junk_type = TOKEN_TYPES.STATIC_KEYWORD;
|
|
810
|
-
}
|
|
811
804
|
addToken(TOKEN_TYPES.LOGIC_OPEN, "${");
|
|
812
805
|
i += 2;
|
|
813
806
|
|
|
@@ -1761,6 +1754,22 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
1761
1754
|
nextI++;
|
|
1762
1755
|
}
|
|
1763
1756
|
|
|
1757
|
+
return [node, nextI, false];
|
|
1758
|
+
} else if (current_token(tokens, i).type === TOKEN_TYPES.LOGIC_OPEN) {
|
|
1759
|
+
if (!allowLogic) {
|
|
1760
|
+
parserError(errorMessage(tokens, i, "literal value", "", "Logic blocks are not allowed in this context."));
|
|
1761
|
+
}
|
|
1762
|
+
let nextI = i + 1;
|
|
1763
|
+
const logicToken = current_token(tokens, nextI);
|
|
1764
|
+
const node = makeLogicNode(STATIC_LOGIC);
|
|
1765
|
+
node.code = logicToken ? logicToken.value : "";
|
|
1766
|
+
node.range = logicToken ? logicToken.range : current_token(tokens, i).range;
|
|
1767
|
+
nextI++;
|
|
1768
|
+
|
|
1769
|
+
if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
|
|
1770
|
+
nextI++;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1764
1773
|
return [node, nextI, false];
|
|
1765
1774
|
} else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_V) {
|
|
1766
1775
|
i++; // consume PREFIX_V keyword
|
|
@@ -1777,7 +1786,9 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
1777
1786
|
}
|
|
1778
1787
|
variables.__consumed__.add(vKey);
|
|
1779
1788
|
} else {
|
|
1780
|
-
|
|
1789
|
+
// Encode fallback in the envelope key so resolveAstVariables can apply it
|
|
1790
|
+
// at instantiation time instead of baking it in now.
|
|
1791
|
+
val = getPrefixValue('v', vFallback !== undefined ? `${vKey}|${vFallback}` : vKey);
|
|
1781
1792
|
}
|
|
1782
1793
|
return [val, i, false];
|
|
1783
1794
|
} else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P) {
|
|
@@ -2171,7 +2182,8 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
|
|
|
2171
2182
|
}
|
|
2172
2183
|
variables.__consumed__.add(tvKey);
|
|
2173
2184
|
} else {
|
|
2174
|
-
|
|
2185
|
+
// Encode fallback in envelope so resolveAstVariables can apply it later.
|
|
2186
|
+
textNode.text += getPrefixValue('v', tvFallback !== undefined ? `${tvKey}|${tvFallback}` : tvKey);
|
|
2175
2187
|
}
|
|
2176
2188
|
} else {
|
|
2177
2189
|
break;
|
|
@@ -2269,6 +2281,27 @@ function parseNode(tokens, i, filename = null, placeholders = {}, variables = {}
|
|
|
2269
2281
|
return [node, nextI];
|
|
2270
2282
|
}
|
|
2271
2283
|
// ========================================================================== //
|
|
2284
|
+
// Bare Logic Block (${ }$ without explicit static/runtime — defaults to static)
|
|
2285
|
+
// ========================================================================== //
|
|
2286
|
+
else if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.LOGIC_OPEN) {
|
|
2287
|
+
let nextI = i + 1;
|
|
2288
|
+
const logicToken = current_token(tokens, nextI);
|
|
2289
|
+
const node = makeLogicNode(STATIC_LOGIC);
|
|
2290
|
+
node.code = logicToken ? logicToken.value : "";
|
|
2291
|
+
node.depth = depth;
|
|
2292
|
+
node.range = {
|
|
2293
|
+
start: current_token(tokens, i).range.start,
|
|
2294
|
+
end: logicToken ? logicToken.range.end : current_token(tokens, i).range.end
|
|
2295
|
+
};
|
|
2296
|
+
nextI++;
|
|
2297
|
+
|
|
2298
|
+
if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
|
|
2299
|
+
nextI++;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
return [node, nextI];
|
|
2303
|
+
}
|
|
2304
|
+
// ========================================================================== //
|
|
2272
2305
|
// Text or Placeholder //
|
|
2273
2306
|
// ========================================================================== //
|
|
2274
2307
|
else if (
|
|
@@ -2353,6 +2386,7 @@ function parser(tokens, filename = null, placeholders = {}, variables = {}) {
|
|
|
2353
2386
|
const LITE_ERROR =
|
|
2354
2387
|
"[SomMark lite] static ${}$ and runtime ${}$ blocks are not supported in lite mode. " +
|
|
2355
2388
|
"Use the full SomMark bundle to enable JS evaluation.";
|
|
2389
|
+
function withEvaluator(fn) { return fn(); }
|
|
2356
2390
|
|
|
2357
2391
|
class EvaluatorStub {
|
|
2358
2392
|
setDefaultFs(_fs) {}
|
|
@@ -8954,7 +8988,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
8954
8988
|
|
|
8955
8989
|
if (node.type === STATIC_LOGIC) {
|
|
8956
8990
|
try {
|
|
8957
|
-
const result = await Evaluator.execute(node.code);
|
|
8991
|
+
const result = await Evaluator.execute(node.code, node.baseDir || null);
|
|
8958
8992
|
if (generateRuntimeOutput) return "";
|
|
8959
8993
|
if (result && typeof result === "object" && result.__raw !== undefined) {
|
|
8960
8994
|
if (security?.allowRaw === false) {
|
|
@@ -9160,7 +9194,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9160
9194
|
}
|
|
9161
9195
|
} else if (child.type === STATIC_LOGIC) {
|
|
9162
9196
|
try {
|
|
9163
|
-
const val = await Evaluator.execute(child.code);
|
|
9197
|
+
const val = await Evaluator.execute(child.code, child.baseDir || null);
|
|
9164
9198
|
if (val !== undefined && typeof val !== "object") richText += String(val);
|
|
9165
9199
|
} catch (err) {
|
|
9166
9200
|
transpilerError([
|
|
@@ -9277,7 +9311,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9277
9311
|
|
|
9278
9312
|
case STATIC_LOGIC:
|
|
9279
9313
|
try {
|
|
9280
|
-
const result = await Evaluator.execute(body_node.code);
|
|
9314
|
+
const result = await Evaluator.execute(body_node.code, body_node.baseDir || null);
|
|
9281
9315
|
if (result && typeof result === "object" && result.__raw !== undefined) {
|
|
9282
9316
|
if (security?.allowRaw === false) {
|
|
9283
9317
|
bodyOutput = mapper_file ? mapper_file.text(String(result.__raw)) : String(result.__raw);
|
|
@@ -9391,109 +9425,108 @@ async function transpiler(optionsOrAst, format, mapperFile) {
|
|
|
9391
9425
|
})();
|
|
9392
9426
|
|
|
9393
9427
|
const dualOutput = optionsOrAst?.dualOutput || false;
|
|
9394
|
-
|
|
9395
|
-
// Initialize Logic Sandbox
|
|
9396
|
-
await Evaluator.init(fileBaseDir, security, settings, targetMapper);
|
|
9397
|
-
// Inject global data
|
|
9398
9428
|
const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
|
|
9399
9429
|
const variables = optionsOrAst?.variables || settings?.variables || {};
|
|
9400
9430
|
warnDroppedVariables(variables);
|
|
9401
|
-
Evaluator.inject(placeholders);
|
|
9402
|
-
Evaluator.inject(variables);
|
|
9403
9431
|
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9432
|
+
return withEvaluator(async () => {
|
|
9433
|
+
// Initialize Logic Sandbox inside isolated async context
|
|
9434
|
+
await Evaluator.init(fileBaseDir, security, settings, targetMapper);
|
|
9435
|
+
Evaluator.inject(placeholders);
|
|
9436
|
+
Evaluator.inject(variables);
|
|
9407
9437
|
|
|
9408
|
-
|
|
9409
|
-
|
|
9438
|
+
let output = "";
|
|
9439
|
+
let prev_body_node = null;
|
|
9440
|
+
let prev_was_silent = false;
|
|
9410
9441
|
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
let
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
prev_was_silent =
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9442
|
+
if (dualOutput) {
|
|
9443
|
+
const idState = { mode: 'record', ids: [], idx: 0 };
|
|
9444
|
+
|
|
9445
|
+
// HTML pass — generate HTML, record element IDs for runtime blocks
|
|
9446
|
+
let htmlOutput = "";
|
|
9447
|
+
try {
|
|
9448
|
+
for (let i = 0; i < body.length; i++) {
|
|
9449
|
+
const node = body[i];
|
|
9450
|
+
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, true, instance, idState);
|
|
9451
|
+
let finalBlockOutput = blockOutput;
|
|
9452
|
+
if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
|
|
9453
|
+
if (finalBlockOutput) {
|
|
9454
|
+
htmlOutput += finalBlockOutput;
|
|
9455
|
+
prev_was_silent = false;
|
|
9456
|
+
} else {
|
|
9457
|
+
prev_was_silent = true;
|
|
9458
|
+
if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
|
|
9459
|
+
const nextNode = body[i + 1];
|
|
9460
|
+
if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) i++;
|
|
9461
|
+
}
|
|
9427
9462
|
}
|
|
9428
9463
|
}
|
|
9464
|
+
} finally {
|
|
9465
|
+
Evaluator.destroy();
|
|
9429
9466
|
}
|
|
9430
|
-
} finally {
|
|
9431
|
-
Evaluator.destroy();
|
|
9432
|
-
}
|
|
9433
9467
|
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9468
|
+
// JS pass — replay the same IDs so querySelector targets match HTML
|
|
9469
|
+
idState.mode = 'replay';
|
|
9470
|
+
idState.idx = 0;
|
|
9471
|
+
prev_was_silent = false;
|
|
9438
9472
|
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9473
|
+
await Evaluator.init(fileBaseDir, security, settings, targetMapper);
|
|
9474
|
+
Evaluator.inject(placeholders);
|
|
9475
|
+
Evaluator.inject(variables);
|
|
9476
|
+
|
|
9477
|
+
let jsOutput = "";
|
|
9478
|
+
try {
|
|
9479
|
+
for (let i = 0; i < body.length; i++) {
|
|
9480
|
+
const node = body[i];
|
|
9481
|
+
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, true, false, instance, idState);
|
|
9482
|
+
let finalBlockOutput = blockOutput;
|
|
9483
|
+
if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
|
|
9484
|
+
if (finalBlockOutput) {
|
|
9485
|
+
jsOutput += finalBlockOutput;
|
|
9486
|
+
prev_was_silent = false;
|
|
9487
|
+
} else {
|
|
9488
|
+
prev_was_silent = true;
|
|
9489
|
+
}
|
|
9490
|
+
}
|
|
9491
|
+
} finally {
|
|
9492
|
+
Evaluator.destroy();
|
|
9493
|
+
}
|
|
9494
|
+
|
|
9495
|
+
return [htmlOutput.trim(), jsOutput.trim()];
|
|
9496
|
+
}
|
|
9442
9497
|
|
|
9443
|
-
let jsOutput = "";
|
|
9444
9498
|
try {
|
|
9445
9499
|
for (let i = 0; i < body.length; i++) {
|
|
9446
9500
|
const node = body[i];
|
|
9447
|
-
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null,
|
|
9501
|
+
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, false, instance);
|
|
9502
|
+
|
|
9448
9503
|
let finalBlockOutput = blockOutput;
|
|
9449
|
-
if (prev_was_silent && node.type === TEXT$1)
|
|
9504
|
+
if (prev_was_silent && node.type === TEXT$1) {
|
|
9505
|
+
finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
|
|
9506
|
+
}
|
|
9507
|
+
|
|
9450
9508
|
if (finalBlockOutput) {
|
|
9451
|
-
|
|
9509
|
+
output += finalBlockOutput;
|
|
9452
9510
|
prev_was_silent = false;
|
|
9511
|
+
if (node.type !== TEXT$1 || node.text.trim().length > 0) {
|
|
9512
|
+
prev_body_node = node;
|
|
9513
|
+
}
|
|
9453
9514
|
} else {
|
|
9454
9515
|
prev_was_silent = true;
|
|
9516
|
+
if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
|
|
9517
|
+
const nextNode = body[i + 1];
|
|
9518
|
+
if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
|
|
9519
|
+
i++;
|
|
9520
|
+
}
|
|
9521
|
+
}
|
|
9455
9522
|
}
|
|
9456
9523
|
}
|
|
9457
9524
|
} finally {
|
|
9458
9525
|
Evaluator.destroy();
|
|
9459
9526
|
}
|
|
9460
9527
|
|
|
9461
|
-
return
|
|
9462
|
-
}
|
|
9463
|
-
|
|
9464
|
-
try {
|
|
9465
|
-
for (let i = 0; i < body.length; i++) {
|
|
9466
|
-
const node = body[i];
|
|
9467
|
-
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, false, instance);
|
|
9468
|
-
|
|
9469
|
-
let finalBlockOutput = blockOutput;
|
|
9470
|
-
if (prev_was_silent && node.type === TEXT$1) {
|
|
9471
|
-
finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
|
|
9472
|
-
}
|
|
9473
|
-
|
|
9474
|
-
if (finalBlockOutput) {
|
|
9475
|
-
output += finalBlockOutput;
|
|
9476
|
-
prev_was_silent = false;
|
|
9477
|
-
if (node.type !== TEXT$1 || node.text.trim().length > 0) {
|
|
9478
|
-
prev_body_node = node;
|
|
9479
|
-
}
|
|
9480
|
-
} else {
|
|
9481
|
-
prev_was_silent = true;
|
|
9482
|
-
if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
|
|
9483
|
-
// If a comment is removed, check the next node.
|
|
9484
|
-
// If it's just a blank line, skip it so we don't have extra gaps in the output.
|
|
9485
|
-
const nextNode = body[i + 1];
|
|
9486
|
-
if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
|
|
9487
|
-
i++; // Skip the next newline node
|
|
9488
|
-
}
|
|
9489
|
-
}
|
|
9490
|
-
}
|
|
9491
|
-
}
|
|
9492
|
-
} finally {
|
|
9493
|
-
Evaluator.destroy();
|
|
9494
|
-
}
|
|
9495
|
-
|
|
9496
|
-
return output.trim();
|
|
9528
|
+
return output.trim();
|
|
9529
|
+
});
|
|
9497
9530
|
}
|
|
9498
9531
|
|
|
9499
9532
|
/**
|
|
@@ -9516,7 +9549,7 @@ async function transpileArgs(props) {
|
|
|
9516
9549
|
result[key] = "";
|
|
9517
9550
|
} else if (value.type === STATIC_LOGIC) {
|
|
9518
9551
|
try {
|
|
9519
|
-
result[key] = await Evaluator.execute(value.code);
|
|
9552
|
+
result[key] = await Evaluator.execute(value.code, value.baseDir || null);
|
|
9520
9553
|
} catch (err) {
|
|
9521
9554
|
transpilerError([
|
|
9522
9555
|
`<$red:Logic Error (Argument):$> ${err.message}{line}`,
|
|
@@ -12154,7 +12187,10 @@ const resolveAstVariables = (nodes, variables) => {
|
|
|
12154
12187
|
for (const node of nodes) {
|
|
12155
12188
|
if (node.type === TEXT$1) {
|
|
12156
12189
|
if (node.text.includes(VAR_PREFIX)) {
|
|
12157
|
-
node.text = node.text.replace(VAR_PATTERN, (match,
|
|
12190
|
+
node.text = node.text.replace(VAR_PATTERN, (match, keyAndFallback) => {
|
|
12191
|
+
const pipeIdx = keyAndFallback.indexOf('|');
|
|
12192
|
+
const key = pipeIdx >= 0 ? keyAndFallback.slice(0, pipeIdx) : keyAndFallback;
|
|
12193
|
+
const fallback = pipeIdx >= 0 ? keyAndFallback.slice(pipeIdx + 1) : undefined;
|
|
12158
12194
|
if (variables[key] !== undefined) {
|
|
12159
12195
|
if (!variables.__consumed__) {
|
|
12160
12196
|
Object.defineProperty(variables, "__consumed__", {
|
|
@@ -12167,14 +12203,21 @@ const resolveAstVariables = (nodes, variables) => {
|
|
|
12167
12203
|
variables.__consumed__.add(key);
|
|
12168
12204
|
return String(variables[key]);
|
|
12169
12205
|
}
|
|
12206
|
+
if (fallback !== undefined) return fallback;
|
|
12170
12207
|
return match;
|
|
12171
12208
|
});
|
|
12172
12209
|
}
|
|
12173
12210
|
} else if (node.type === BLOCK) {
|
|
12174
12211
|
// Resolve any unresolved variables in block arguments
|
|
12175
12212
|
for (const [argKey, argVal] of Object.entries(node.props)) {
|
|
12176
|
-
if (typeof argVal
|
|
12177
|
-
|
|
12213
|
+
if (typeof argVal !== "string" || !argVal.includes(VAR_PREFIX)) continue;
|
|
12214
|
+
|
|
12215
|
+
if (argVal.startsWith(VAR_PREFIX) && argVal.endsWith(VAR_SUFFIX)) {
|
|
12216
|
+
// Entire value is an envelope — resolve to scalar or fallback
|
|
12217
|
+
const keyAndFallback = argVal.slice(VAR_PREFIX.length, -VAR_SUFFIX.length);
|
|
12218
|
+
const pipeIdx = keyAndFallback.indexOf('|');
|
|
12219
|
+
const varKey = pipeIdx >= 0 ? keyAndFallback.slice(0, pipeIdx) : keyAndFallback;
|
|
12220
|
+
const fallback = pipeIdx >= 0 ? keyAndFallback.slice(pipeIdx + 1) : undefined;
|
|
12178
12221
|
if (variables[varKey] !== undefined) {
|
|
12179
12222
|
node.props[argKey] = variables[varKey];
|
|
12180
12223
|
if (!variables.__consumed__) {
|
|
@@ -12186,7 +12229,31 @@ const resolveAstVariables = (nodes, variables) => {
|
|
|
12186
12229
|
});
|
|
12187
12230
|
}
|
|
12188
12231
|
variables.__consumed__.add(varKey);
|
|
12232
|
+
} else if (fallback !== undefined) {
|
|
12233
|
+
node.props[argKey] = fallback;
|
|
12189
12234
|
}
|
|
12235
|
+
} else {
|
|
12236
|
+
// Envelope embedded inside a larger string — replace in-place.
|
|
12237
|
+
// Unresolved envelopes become "" so they don't pollute class names etc.
|
|
12238
|
+
node.props[argKey] = argVal.replace(VAR_PATTERN, (match, keyAndFallback) => {
|
|
12239
|
+
const pipeIdx = keyAndFallback.indexOf('|');
|
|
12240
|
+
const key = pipeIdx >= 0 ? keyAndFallback.slice(0, pipeIdx) : keyAndFallback;
|
|
12241
|
+
const fallback = pipeIdx >= 0 ? keyAndFallback.slice(pipeIdx + 1) : undefined;
|
|
12242
|
+
if (variables[key] !== undefined) {
|
|
12243
|
+
if (!variables.__consumed__) {
|
|
12244
|
+
Object.defineProperty(variables, "__consumed__", {
|
|
12245
|
+
value: new Set(),
|
|
12246
|
+
writable: true,
|
|
12247
|
+
enumerable: false,
|
|
12248
|
+
configurable: true
|
|
12249
|
+
});
|
|
12250
|
+
}
|
|
12251
|
+
variables.__consumed__.add(key);
|
|
12252
|
+
return String(variables[key]);
|
|
12253
|
+
}
|
|
12254
|
+
if (fallback !== undefined) return fallback;
|
|
12255
|
+
return "";
|
|
12256
|
+
});
|
|
12190
12257
|
}
|
|
12191
12258
|
}
|
|
12192
12259
|
if (node.body) {
|
|
@@ -12216,6 +12283,7 @@ const cloneAst = (nodes) => {
|
|
|
12216
12283
|
if (node.id !== undefined) nodeCopy.id = node.id;
|
|
12217
12284
|
if (node.code !== undefined) nodeCopy.code = node.code;
|
|
12218
12285
|
if (node.isSelfClosing !== undefined) nodeCopy.isSelfClosing = node.isSelfClosing;
|
|
12286
|
+
if (node.baseDir !== undefined) nodeCopy.baseDir = node.baseDir;
|
|
12219
12287
|
if (node.props !== undefined) {
|
|
12220
12288
|
nodeCopy.props = { ...node.props };
|
|
12221
12289
|
}
|
|
@@ -12227,6 +12295,20 @@ const cloneAst = (nodes) => {
|
|
|
12227
12295
|
return copy;
|
|
12228
12296
|
};
|
|
12229
12297
|
|
|
12298
|
+
/**
|
|
12299
|
+
* Tags all STATIC_LOGIC and RUNTIME_LOGIC nodes in a subtree with their
|
|
12300
|
+
* source module's baseDir so the evaluator can resolve imports correctly.
|
|
12301
|
+
*/
|
|
12302
|
+
const tagLogicNodes = (nodes, baseDir) => {
|
|
12303
|
+
if (!nodes) return;
|
|
12304
|
+
for (const node of nodes) {
|
|
12305
|
+
if ((node.type === STATIC_LOGIC || node.type === RUNTIME_LOGIC) && !node.baseDir) {
|
|
12306
|
+
node.baseDir = baseDir;
|
|
12307
|
+
}
|
|
12308
|
+
if (node.body) tagLogicNodes(node.body, baseDir);
|
|
12309
|
+
}
|
|
12310
|
+
};
|
|
12311
|
+
|
|
12230
12312
|
/**
|
|
12231
12313
|
* Handles all [import] and [$use-module] blocks in your code.
|
|
12232
12314
|
* It loads the requested files, checks for errors, and puts the content into the main document.
|
|
@@ -12400,6 +12482,7 @@ async function resolveModules(ast, context) {
|
|
|
12400
12482
|
format: context.format,
|
|
12401
12483
|
filename: mod.path,
|
|
12402
12484
|
baseDir: posix.dirname(mod.localPath),
|
|
12485
|
+
fs: context.instance.fs,
|
|
12403
12486
|
mapperFile: context.instance.mapperFile,
|
|
12404
12487
|
placeholders: context.instance.placeholders,
|
|
12405
12488
|
variables: {},
|
|
@@ -12415,6 +12498,7 @@ async function resolveModules(ast, context) {
|
|
|
12415
12498
|
});
|
|
12416
12499
|
|
|
12417
12500
|
const subAst = await subSmark.parse();
|
|
12501
|
+
tagLogicNodes(subAst, posix.dirname(mod.localPath));
|
|
12418
12502
|
context.instance.moduleCache.set(mod.localPath, subAst);
|
|
12419
12503
|
expandedNodes = trimAst(subAst);
|
|
12420
12504
|
}
|
|
@@ -12458,6 +12542,7 @@ async function resolveModules(ast, context) {
|
|
|
12458
12542
|
format: context.format,
|
|
12459
12543
|
filename: mod.path,
|
|
12460
12544
|
baseDir: posix.dirname(mod.localPath),
|
|
12545
|
+
fs: context.instance.fs,
|
|
12461
12546
|
mapperFile: context.instance.mapperFile,
|
|
12462
12547
|
placeholders: context.instance.placeholders,
|
|
12463
12548
|
variables: {}, // Parse without variables to keep the cached AST pure
|
|
@@ -12473,6 +12558,7 @@ async function resolveModules(ast, context) {
|
|
|
12473
12558
|
});
|
|
12474
12559
|
|
|
12475
12560
|
subAst = await subSmark.parse();
|
|
12561
|
+
tagLogicNodes(subAst, posix.dirname(mod.localPath));
|
|
12476
12562
|
context.instance.moduleCache.set(mod.localPath, subAst);
|
|
12477
12563
|
subAst = cloneAst(subAst);
|
|
12478
12564
|
}
|
|
@@ -12542,6 +12628,13 @@ async function resolveModules(ast, context) {
|
|
|
12542
12628
|
return ast;
|
|
12543
12629
|
}
|
|
12544
12630
|
|
|
12631
|
+
/**
|
|
12632
|
+
* After full transpilation of the top-level file, apply any v{} fallbacks that
|
|
12633
|
+
* remain unresolved. Envelopes with no fallback are kept as-is (debugging signal).
|
|
12634
|
+
* Must NOT be called on sub-module ASTs — only on the final top-level AST.
|
|
12635
|
+
*/
|
|
12636
|
+
const applyVariableFallbacks = (ast) => resolveAstVariables(ast, {});
|
|
12637
|
+
|
|
12545
12638
|
/**
|
|
12546
12639
|
* SomMark Rules Validator
|
|
12547
12640
|
*
|
|
@@ -13036,6 +13129,7 @@ class SomMark {
|
|
|
13036
13129
|
if (this.showSpinner) startSpinner();
|
|
13037
13130
|
try {
|
|
13038
13131
|
const ast = this.ast || await this.parse(src);
|
|
13132
|
+
applyVariableFallbacks(ast);
|
|
13039
13133
|
let result = await transpiler({
|
|
13040
13134
|
ast,
|
|
13041
13135
|
format: this.targetFormat,
|
package/dist/sommark.lexer.js
CHANGED
|
@@ -451,13 +451,6 @@ function lexer(src, filename = "anonymous") {
|
|
|
451
451
|
// LOGIC BLOCKS (${ ... }$) — explicit: static/runtime ${ }$ shorthand: ${ }$ = static ${ }$
|
|
452
452
|
if (char === "$" && next === "{") {
|
|
453
453
|
{
|
|
454
|
-
const hasExplicitKeyword = last_non_junk_type === TOKEN_TYPES.STATIC_KEYWORD || last_non_junk_type === TOKEN_TYPES.RUNTIME_KEYWORD;
|
|
455
|
-
if (!hasExplicitKeyword) {
|
|
456
|
-
// Zero-width: synthetic token has no source presence, must not shift position
|
|
457
|
-
tokens.push({ type: TOKEN_TYPES.STATIC_KEYWORD, value: "static", source: filename, range: { start: { line, character }, end: { line, character } } });
|
|
458
|
-
TOKEN_TYPES.STATIC_KEYWORD;
|
|
459
|
-
last_non_junk_type = TOKEN_TYPES.STATIC_KEYWORD;
|
|
460
|
-
}
|
|
461
454
|
addToken(TOKEN_TYPES.LOGIC_OPEN, "${");
|
|
462
455
|
i += 2;
|
|
463
456
|
|
package/dist/sommark.parser.js
CHANGED
|
@@ -472,13 +472,6 @@ function lexer(src, filename = "anonymous") {
|
|
|
472
472
|
// LOGIC BLOCKS (${ ... }$) — explicit: static/runtime ${ }$ shorthand: ${ }$ = static ${ }$
|
|
473
473
|
if (char === "$" && next === "{") {
|
|
474
474
|
{
|
|
475
|
-
const hasExplicitKeyword = last_non_junk_type === TOKEN_TYPES.STATIC_KEYWORD || last_non_junk_type === TOKEN_TYPES.RUNTIME_KEYWORD;
|
|
476
|
-
if (!hasExplicitKeyword) {
|
|
477
|
-
// Zero-width: synthetic token has no source presence, must not shift position
|
|
478
|
-
tokens.push({ type: TOKEN_TYPES.STATIC_KEYWORD, value: "static", source: filename, range: { start: { line, character }, end: { line, character } } });
|
|
479
|
-
TOKEN_TYPES.STATIC_KEYWORD;
|
|
480
|
-
last_non_junk_type = TOKEN_TYPES.STATIC_KEYWORD;
|
|
481
|
-
}
|
|
482
475
|
addToken(TOKEN_TYPES.LOGIC_OPEN, "${");
|
|
483
476
|
i += 2;
|
|
484
477
|
|
|
@@ -1316,6 +1309,22 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
1316
1309
|
nextI++;
|
|
1317
1310
|
}
|
|
1318
1311
|
|
|
1312
|
+
return [node, nextI, false];
|
|
1313
|
+
} else if (current_token(tokens, i).type === TOKEN_TYPES.LOGIC_OPEN) {
|
|
1314
|
+
if (!allowLogic) {
|
|
1315
|
+
parserError(errorMessage(tokens, i, "literal value", "", "Logic blocks are not allowed in this context."));
|
|
1316
|
+
}
|
|
1317
|
+
let nextI = i + 1;
|
|
1318
|
+
const logicToken = current_token(tokens, nextI);
|
|
1319
|
+
const node = makeLogicNode(STATIC_LOGIC);
|
|
1320
|
+
node.code = logicToken ? logicToken.value : "";
|
|
1321
|
+
node.range = logicToken ? logicToken.range : current_token(tokens, i).range;
|
|
1322
|
+
nextI++;
|
|
1323
|
+
|
|
1324
|
+
if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
|
|
1325
|
+
nextI++;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1319
1328
|
return [node, nextI, false];
|
|
1320
1329
|
} else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_V) {
|
|
1321
1330
|
i++; // consume PREFIX_V keyword
|
|
@@ -1332,7 +1341,9 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
|
|
|
1332
1341
|
}
|
|
1333
1342
|
variables.__consumed__.add(vKey);
|
|
1334
1343
|
} else {
|
|
1335
|
-
|
|
1344
|
+
// Encode fallback in the envelope key so resolveAstVariables can apply it
|
|
1345
|
+
// at instantiation time instead of baking it in now.
|
|
1346
|
+
val = getPrefixValue('v', vFallback !== undefined ? `${vKey}|${vFallback}` : vKey);
|
|
1336
1347
|
}
|
|
1337
1348
|
return [val, i, false];
|
|
1338
1349
|
} else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P) {
|
|
@@ -1726,7 +1737,8 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
|
|
|
1726
1737
|
}
|
|
1727
1738
|
variables.__consumed__.add(tvKey);
|
|
1728
1739
|
} else {
|
|
1729
|
-
|
|
1740
|
+
// Encode fallback in envelope so resolveAstVariables can apply it later.
|
|
1741
|
+
textNode.text += getPrefixValue('v', tvFallback !== undefined ? `${tvKey}|${tvFallback}` : tvKey);
|
|
1730
1742
|
}
|
|
1731
1743
|
} else {
|
|
1732
1744
|
break;
|
|
@@ -1824,6 +1836,27 @@ function parseNode(tokens, i, filename = null, placeholders = {}, variables = {}
|
|
|
1824
1836
|
return [node, nextI];
|
|
1825
1837
|
}
|
|
1826
1838
|
// ========================================================================== //
|
|
1839
|
+
// Bare Logic Block (${ }$ without explicit static/runtime — defaults to static)
|
|
1840
|
+
// ========================================================================== //
|
|
1841
|
+
else if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.LOGIC_OPEN) {
|
|
1842
|
+
let nextI = i + 1;
|
|
1843
|
+
const logicToken = current_token(tokens, nextI);
|
|
1844
|
+
const node = makeLogicNode(STATIC_LOGIC);
|
|
1845
|
+
node.code = logicToken ? logicToken.value : "";
|
|
1846
|
+
node.depth = depth;
|
|
1847
|
+
node.range = {
|
|
1848
|
+
start: current_token(tokens, i).range.start,
|
|
1849
|
+
end: logicToken ? logicToken.range.end : current_token(tokens, i).range.end
|
|
1850
|
+
};
|
|
1851
|
+
nextI++;
|
|
1852
|
+
|
|
1853
|
+
if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
|
|
1854
|
+
nextI++;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
return [node, nextI];
|
|
1858
|
+
}
|
|
1859
|
+
// ========================================================================== //
|
|
1827
1860
|
// Text or Placeholder //
|
|
1828
1861
|
// ========================================================================== //
|
|
1829
1862
|
else if (
|
package/index.shared.js
CHANGED
|
@@ -19,7 +19,7 @@ import { runtimeError } from "./core/errors.js";
|
|
|
19
19
|
import FORMATS, { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat, jsoncFormat, xmlFormat, csvFormat, tomlFormat, yamlFormat } from "./core/formats.js";
|
|
20
20
|
import TOKEN_TYPES from "./core/tokenTypes.js";
|
|
21
21
|
import * as labels from "./core/labels.js";
|
|
22
|
-
import { resolveModules } from "./core/modules.js";
|
|
22
|
+
import { resolveModules, applyVariableFallbacks } from "./core/modules.js";
|
|
23
23
|
import { validateAST } from "./core/validator.js";
|
|
24
24
|
import { enableColor } from "./helpers/colorize.js";
|
|
25
25
|
import { safeArg } from "./helpers/utils.js";
|
|
@@ -292,6 +292,7 @@ class SomMark {
|
|
|
292
292
|
if (this.showSpinner) startSpinner();
|
|
293
293
|
try {
|
|
294
294
|
const ast = this.ast || await this.parse(src);
|
|
295
|
+
applyVariableFallbacks(ast);
|
|
295
296
|
let result = await transpiler({
|
|
296
297
|
ast,
|
|
297
298
|
format: this.targetFormat,
|
package/package.json
CHANGED