sommark 5.0.5 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/async-hooks.js +19 -0
- package/core/evaluator.js +212 -22
- package/core/helpers/config-loader.js +29 -9
- package/core/helpers/lib.js +1 -1
- package/core/helpers/preprocessor.js +19 -0
- package/core/modules.js +59 -21
- package/core/parser.js +6 -3
- package/core/pathe-bundle.js +1 -0
- package/core/transpiler.js +130 -15
- package/core/validator.js +17 -4
- package/dist/sommark.browser.js +669 -214
- package/dist/sommark.browser.lite.js +458 -191
- package/dist/sommark.parser.js +6 -3
- package/esbuild.js +64 -0
- package/index.browser.js +4 -1
- package/index.js +102 -1
- package/index.shared.js +13 -2
- package/mappers/languages/markdown.js +99 -81
- package/mappers/languages/xml.js +76 -61
- package/mappers/shared/index.js +10 -1
- package/package.json +11 -3
- package/rollup.js +79 -0
- package/vite.js +20 -0
|
@@ -1474,6 +1474,7 @@ function makeBlockNode() {
|
|
|
1474
1474
|
structure: "Block",
|
|
1475
1475
|
id: "",
|
|
1476
1476
|
props: {},
|
|
1477
|
+
directives: {},
|
|
1477
1478
|
body: [],
|
|
1478
1479
|
depth: 0,
|
|
1479
1480
|
range: {
|
|
@@ -1959,9 +1960,11 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
|
|
|
1959
1960
|
i = valueIndex;
|
|
1960
1961
|
|
|
1961
1962
|
// Store Argument
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1963
|
+
if (k && k.startsWith("smark-")) {
|
|
1964
|
+
blockNode.directives[k.slice(6)] = v; // strip "smark-" prefix
|
|
1965
|
+
} else {
|
|
1966
|
+
blockNode.props[String(argIndex++)] = v;
|
|
1967
|
+
if (k) blockNode.props[k] = v;
|
|
1965
1968
|
}
|
|
1966
1969
|
k = "";
|
|
1967
1970
|
v = "";
|
|
@@ -8798,8 +8801,27 @@ async function preprocessRuntimeLogic(code, filename = null, security = {}, inst
|
|
|
8798
8801
|
if (filename && filename !== "anonymous") {
|
|
8799
8802
|
baseDir = posix.dirname(posix.resolve(filename));
|
|
8800
8803
|
}
|
|
8804
|
+
|
|
8805
|
+
// Block absolute paths — path.resolve would ignore baseDir entirely
|
|
8806
|
+
if (posix.isAbsolute(argValue)) {
|
|
8807
|
+
transpilerError([
|
|
8808
|
+
`<$red:Security Error:$> Absolute import paths are not allowed: <$magenta:${argValue}$>{line}`,
|
|
8809
|
+
`<$yellow:Use a path relative to the template file, e.g.$> <$green:SomMark.import("./data.json")$> <$yellow:or$> <$green:SomMark.import("../shared/data.json")$><$yellow:.$>{line}`,
|
|
8810
|
+
`<$yellow:Base directory:$> <$blue:${baseDir}$>{line}`
|
|
8811
|
+
]);
|
|
8812
|
+
}
|
|
8813
|
+
|
|
8801
8814
|
const resolvedPath = posix.resolve(baseDir, argValue);
|
|
8802
8815
|
|
|
8816
|
+
// Block path traversal — resolved path must stay inside baseDir
|
|
8817
|
+
const safeBases = baseDir.endsWith(posix.sep) ? baseDir : baseDir + posix.sep;
|
|
8818
|
+
if (!resolvedPath.startsWith(safeBases) && resolvedPath !== baseDir) {
|
|
8819
|
+
transpilerError([
|
|
8820
|
+
`<$red:Security Error:$> Import path escapes the project directory: <$magenta:${argValue}$>{line}`,
|
|
8821
|
+
`<$yellow:Resolved Path:$> <$blue:${resolvedPath}$>{line}`
|
|
8822
|
+
]);
|
|
8823
|
+
}
|
|
8824
|
+
|
|
8803
8825
|
const fsImpl = instance?.fs || await getNodeFs();
|
|
8804
8826
|
|
|
8805
8827
|
// File presence validation
|
|
@@ -8895,7 +8917,7 @@ function warnDroppedVariables(variables) {
|
|
|
8895
8917
|
} else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
8896
8918
|
for (const [nestedKey, nestedVal] of Object.entries(value)) {
|
|
8897
8919
|
if (typeof nestedVal === "function") {
|
|
8898
|
-
console.warn(`[SomMark] variables.${key}.${nestedKey}
|
|
8920
|
+
console.warn(`[SomMark] variables.${key}.${nestedKey}: nested functions inside objects are not supported. Define it as a top-level function instead: variables.${nestedKey}`);
|
|
8899
8921
|
} else if (nestedVal === undefined) {
|
|
8900
8922
|
console.warn(`[SomMark] variables.${key}.${nestedKey} is undefined and will be ignored.`);
|
|
8901
8923
|
}
|
|
@@ -8912,6 +8934,7 @@ const randomBytesHex = (size) => {
|
|
|
8912
8934
|
|
|
8913
8935
|
const BODY_PLACEHOLDER = `SOMMARKBODYPLACEHOLDER${randomBytesHex(8)}SOMMARK`;
|
|
8914
8936
|
|
|
8937
|
+
|
|
8915
8938
|
/**
|
|
8916
8939
|
* Extracts all plain text from a node and its children.
|
|
8917
8940
|
*
|
|
@@ -9001,15 +9024,28 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9001
9024
|
const out = (result !== undefined && typeof result !== "object") ? String(result) : "";
|
|
9002
9025
|
return mapper_file ? mapper_file.text(out) : out;
|
|
9003
9026
|
} catch (err) {
|
|
9027
|
+
const line = node.range?.start?.line + 1 || 1;
|
|
9004
9028
|
transpilerError([
|
|
9005
9029
|
`<$red:Logic Error:$> ${err.message}{line}`,
|
|
9006
|
-
`<$yellow:Code:$> <$blue:${node.code}$>{line}
|
|
9030
|
+
`<$yellow:Code:$> <$blue:${node.code}$>{line}`,
|
|
9031
|
+
`at line <$yellow:${line}$>{line}`
|
|
9007
9032
|
]);
|
|
9008
9033
|
}
|
|
9009
9034
|
}
|
|
9010
9035
|
|
|
9011
9036
|
if (node.type === FOR_EACH) {
|
|
9012
9037
|
const transpiledArgs = await transpileArgs(node.props);
|
|
9038
|
+
|
|
9039
|
+
if (!node.props || (node.props[0] === undefined && node.props["items"] === undefined)) {
|
|
9040
|
+
const line = node.range?.start?.line + 1 || 1;
|
|
9041
|
+
transpilerError([
|
|
9042
|
+
`<$red:Missing Prop Error in [for-each]:$>{line}`,
|
|
9043
|
+
`[for-each] requires an array as its first prop, e.g. [for-each = \${ array }\$]{line}`,
|
|
9044
|
+
`at line <$yellow:${line}$>{line}`
|
|
9045
|
+
]);
|
|
9046
|
+
return "";
|
|
9047
|
+
}
|
|
9048
|
+
|
|
9013
9049
|
const items = mapper_file ? mapper_file.safeArg({ props: transpiledArgs, index: 0, key: "items", fallBack: [] }) : [];
|
|
9014
9050
|
|
|
9015
9051
|
if (!Array.isArray(items)) {
|
|
@@ -9023,11 +9059,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9023
9059
|
}
|
|
9024
9060
|
|
|
9025
9061
|
const asVar = transpiledArgs.as || "value";
|
|
9026
|
-
if (asVar === "i") {
|
|
9062
|
+
if (asVar === "i" || asVar === "length") {
|
|
9027
9063
|
const line = node.range?.start?.line + 1 || 1;
|
|
9028
9064
|
transpilerError([
|
|
9029
9065
|
`<$red:Reserved Variable Error in [for-each]:$>{line}`,
|
|
9030
|
-
`'
|
|
9066
|
+
`'${asVar}' is a reserved variable name.{N}Use a different name for the 'as' prop, e.g. as: "item"{line}`,
|
|
9031
9067
|
`at line <$yellow:${line}$>{line}`
|
|
9032
9068
|
]);
|
|
9033
9069
|
return "";
|
|
@@ -9057,22 +9093,28 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9057
9093
|
}
|
|
9058
9094
|
}
|
|
9059
9095
|
|
|
9060
|
-
|
|
9096
|
+
const rawJoin = transpiledArgs.join ?? null;
|
|
9097
|
+
const join = rawJoin !== null ? rawJoin.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\r/g, "\r") : null;
|
|
9098
|
+
const parts = [];
|
|
9061
9099
|
let idx = 0;
|
|
9100
|
+
const length = items.length;
|
|
9062
9101
|
for (const item of items) {
|
|
9063
9102
|
Evaluator.pushScope();
|
|
9064
9103
|
Evaluator.inject({
|
|
9065
9104
|
[asVar]: item,
|
|
9066
|
-
i: idx
|
|
9105
|
+
i: idx++,
|
|
9106
|
+
length
|
|
9067
9107
|
});
|
|
9068
9108
|
|
|
9109
|
+
let iterOutput = "";
|
|
9069
9110
|
for (let j = 0; j < cleanedBody.length; j++) {
|
|
9070
|
-
|
|
9111
|
+
iterOutput += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState, extraCtx);
|
|
9071
9112
|
}
|
|
9072
9113
|
|
|
9073
9114
|
await Evaluator.popScope();
|
|
9115
|
+
parts.push(iterOutput);
|
|
9074
9116
|
}
|
|
9075
|
-
return
|
|
9117
|
+
return join !== null ? parts.join(join) : parts.join("");
|
|
9076
9118
|
}
|
|
9077
9119
|
|
|
9078
9120
|
let secretId = null;
|
|
@@ -9100,13 +9142,12 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9100
9142
|
}
|
|
9101
9143
|
|
|
9102
9144
|
// smark-raw block — body collected verbatim by lexer, bypasses normal body processing pipeline
|
|
9103
|
-
if (node.type === BLOCK && (node.
|
|
9145
|
+
if (node.type === BLOCK && (node.directives?.raw === "true" || node.directives?.raw === true)) {
|
|
9104
9146
|
if (generateRuntimeOutput) return "";
|
|
9105
9147
|
const rawContent = node.body?.map(n => String(n.text || "")).join("") || "";
|
|
9106
|
-
const
|
|
9107
|
-
const transpiledArgs = await transpileArgs(cleanArgs);
|
|
9148
|
+
const transpiledArgs = await transpileArgs(node.props);
|
|
9108
9149
|
if (Evaluator.active?.hasDynamicTag?.(node.id)) {
|
|
9109
|
-
return await Evaluator.active.executeDynamicTag(node.id, { props: transpiledArgs, content: rawContent, textContent: rawContent });
|
|
9150
|
+
return await Evaluator.active.executeDynamicTag(node.id, { props: transpiledArgs, directives: node.directives, content: rawContent, textContent: rawContent });
|
|
9110
9151
|
}
|
|
9111
9152
|
let rawTarget = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
|
|
9112
9153
|
if (!rawTarget && mapper_file) rawTarget = mapper_file.getUnknownTag(node);
|
|
@@ -9114,6 +9155,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9114
9155
|
const isManualMode = !!rawTarget.options?.handleAst;
|
|
9115
9156
|
return await rawTarget.render.call(mapper_file, {
|
|
9116
9157
|
props: transpiledArgs,
|
|
9158
|
+
directives: node.directives,
|
|
9117
9159
|
content: rawContent,
|
|
9118
9160
|
textContent: rawContent,
|
|
9119
9161
|
ast: isManualMode ? node : undefined,
|
|
@@ -9197,9 +9239,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9197
9239
|
const val = await Evaluator.execute(child.code, child.baseDir || null);
|
|
9198
9240
|
if (val !== undefined && typeof val !== "object") richText += String(val);
|
|
9199
9241
|
} catch (err) {
|
|
9242
|
+
const line = child.range?.start?.line + 1 || 1;
|
|
9200
9243
|
transpilerError([
|
|
9201
9244
|
`<$red:Logic Error:$> ${err.message}{line}`,
|
|
9202
|
-
`<$yellow:Code:$> <$blue:${child.code}$>{line}
|
|
9245
|
+
`<$yellow:Code:$> <$blue:${child.code}$>{line}`,
|
|
9246
|
+
`at line <$yellow:${line}$>{line}`
|
|
9203
9247
|
]);
|
|
9204
9248
|
}
|
|
9205
9249
|
} else if (child.type === COMMENT) {
|
|
@@ -9225,6 +9269,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9225
9269
|
|
|
9226
9270
|
return await target.render.call(mapper_file, {
|
|
9227
9271
|
props: transpiledArgs,
|
|
9272
|
+
directives: node.directives,
|
|
9228
9273
|
content: "",
|
|
9229
9274
|
textContent: richText || textContent,
|
|
9230
9275
|
ast: cleanAst,
|
|
@@ -9243,6 +9288,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9243
9288
|
}
|
|
9244
9289
|
result += await target.render.call(mapper_file, {
|
|
9245
9290
|
props: transpiledArgs,
|
|
9291
|
+
directives: node.directives,
|
|
9246
9292
|
content,
|
|
9247
9293
|
textContent,
|
|
9248
9294
|
ast: new Proxy({}, {
|
|
@@ -9324,9 +9370,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
9324
9370
|
bodyOutput = mapper_file ? mapper_file.text(out, { ...target?.options, escape: parentEscape }) : out;
|
|
9325
9371
|
}
|
|
9326
9372
|
} catch (err) {
|
|
9373
|
+
const line = body_node.range?.start?.line + 1 || 1;
|
|
9327
9374
|
transpilerError([
|
|
9328
9375
|
`<$red:Logic Error:$> ${err.message}{line}`,
|
|
9329
|
-
`<$yellow:Code:$> <$blue:${body_node.code}$>{line}
|
|
9376
|
+
`<$yellow:Code:$> <$blue:${body_node.code}$>{line}`,
|
|
9377
|
+
`at line <$yellow:${line}$>{line}`
|
|
9330
9378
|
]);
|
|
9331
9379
|
}
|
|
9332
9380
|
break;
|
|
@@ -9425,6 +9473,10 @@ async function transpiler(optionsOrAst, format, mapperFile) {
|
|
|
9425
9473
|
})();
|
|
9426
9474
|
|
|
9427
9475
|
const dualOutput = optionsOrAst?.dualOutput || false;
|
|
9476
|
+
const webOutputs = optionsOrAst?.webOutputs || false;
|
|
9477
|
+
if (webOutputs && dualOutput) {
|
|
9478
|
+
throw new Error("[SomMark] Cannot use both 'webOutputs' and 'dualOutput' at the same time. Use 'webOutputs' (returns [html, css, js]) or 'dualOutput' (returns [html, js]).");
|
|
9479
|
+
}
|
|
9428
9480
|
const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
|
|
9429
9481
|
const variables = optionsOrAst?.variables || settings?.variables || {};
|
|
9430
9482
|
warnDroppedVariables(variables);
|
|
@@ -9439,6 +9491,89 @@ async function transpiler(optionsOrAst, format, mapperFile) {
|
|
|
9439
9491
|
let prev_body_node = null;
|
|
9440
9492
|
let prev_was_silent = false;
|
|
9441
9493
|
|
|
9494
|
+
if (webOutputs) {
|
|
9495
|
+
// Use unique markers so [style] content is extracted precisely —
|
|
9496
|
+
// no <style> regex on the final HTML, works with static logic inside [style].
|
|
9497
|
+
const CSS_OPEN = `SOMMARKCSSOPEN${randomBytesHex(8)}SOMMARK`;
|
|
9498
|
+
const CSS_CLOSE = `SOMMARKCSSCLOSE${randomBytesHex(8)}SOMMARK`;
|
|
9499
|
+
|
|
9500
|
+
const webMapper = targetMapper.clone();
|
|
9501
|
+
webMapper.register("style", function ({ content }) {
|
|
9502
|
+
return `${CSS_OPEN}${content}${CSS_CLOSE}`;
|
|
9503
|
+
}, { escape: false });
|
|
9504
|
+
// [head] injects CSS variables as a raw <style> string via this.cssVariables —
|
|
9505
|
+
// override it so those variables go through markers too.
|
|
9506
|
+
webMapper.register("head", function ({ content }) {
|
|
9507
|
+
const varsMarker = this.cssVariables
|
|
9508
|
+
? `${CSS_OPEN}:root { ${this.cssVariables} }${CSS_CLOSE}\n`
|
|
9509
|
+
: "";
|
|
9510
|
+
return this.tag("head").body(`${varsMarker}${content}`);
|
|
9511
|
+
}, { escape: false });
|
|
9512
|
+
|
|
9513
|
+
const idState = { mode: 'record', ids: [], idx: 0 };
|
|
9514
|
+
|
|
9515
|
+
// HTML pass — [style] blocks emit markers instead of <style> tags
|
|
9516
|
+
let htmlOutput = "";
|
|
9517
|
+
try {
|
|
9518
|
+
for (let i = 0; i < body.length; i++) {
|
|
9519
|
+
const node = body[i];
|
|
9520
|
+
const blockOutput = await generateOutput(body, i, targetFormat, webMapper, security, null, false, true, instance, idState);
|
|
9521
|
+
let finalBlockOutput = blockOutput;
|
|
9522
|
+
if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
|
|
9523
|
+
if (finalBlockOutput) {
|
|
9524
|
+
htmlOutput += finalBlockOutput;
|
|
9525
|
+
prev_was_silent = false;
|
|
9526
|
+
} else {
|
|
9527
|
+
prev_was_silent = true;
|
|
9528
|
+
if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
|
|
9529
|
+
const nextNode = body[i + 1];
|
|
9530
|
+
if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) i++;
|
|
9531
|
+
}
|
|
9532
|
+
}
|
|
9533
|
+
}
|
|
9534
|
+
} finally {
|
|
9535
|
+
Evaluator.destroy();
|
|
9536
|
+
}
|
|
9537
|
+
|
|
9538
|
+
// Extract CSS from markers — exact, no HTML regex
|
|
9539
|
+
const cssChunks = [];
|
|
9540
|
+
const markerRe = new RegExp(`${CSS_OPEN}([\\s\\S]*?)${CSS_CLOSE}`, "g");
|
|
9541
|
+
htmlOutput = htmlOutput.replace(markerRe, (_, chunk) => {
|
|
9542
|
+
cssChunks.push(chunk.trim());
|
|
9543
|
+
return "";
|
|
9544
|
+
});
|
|
9545
|
+
const css = cssChunks.join("\n").trim();
|
|
9546
|
+
|
|
9547
|
+
// JS pass — replay IDs so querySelector targets match HTML
|
|
9548
|
+
idState.mode = 'replay';
|
|
9549
|
+
idState.idx = 0;
|
|
9550
|
+
prev_was_silent = false;
|
|
9551
|
+
|
|
9552
|
+
await Evaluator.init(fileBaseDir, security, settings, targetMapper);
|
|
9553
|
+
Evaluator.inject(placeholders);
|
|
9554
|
+
Evaluator.inject(variables);
|
|
9555
|
+
|
|
9556
|
+
let jsOutput = "";
|
|
9557
|
+
try {
|
|
9558
|
+
for (let i = 0; i < body.length; i++) {
|
|
9559
|
+
const node = body[i];
|
|
9560
|
+
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, true, false, instance, idState);
|
|
9561
|
+
let finalBlockOutput = blockOutput;
|
|
9562
|
+
if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
|
|
9563
|
+
if (finalBlockOutput) {
|
|
9564
|
+
jsOutput += finalBlockOutput;
|
|
9565
|
+
prev_was_silent = false;
|
|
9566
|
+
} else {
|
|
9567
|
+
prev_was_silent = true;
|
|
9568
|
+
}
|
|
9569
|
+
}
|
|
9570
|
+
} finally {
|
|
9571
|
+
Evaluator.destroy();
|
|
9572
|
+
}
|
|
9573
|
+
|
|
9574
|
+
return [htmlOutput.trim(), css, jsOutput.trim()];
|
|
9575
|
+
}
|
|
9576
|
+
|
|
9442
9577
|
if (dualOutput) {
|
|
9443
9578
|
const idState = { mode: 'record', ids: [], idx: 0 };
|
|
9444
9579
|
|
|
@@ -9551,9 +9686,11 @@ async function transpileArgs(props) {
|
|
|
9551
9686
|
try {
|
|
9552
9687
|
result[key] = await Evaluator.execute(value.code, value.baseDir || null);
|
|
9553
9688
|
} catch (err) {
|
|
9689
|
+
const line = value.range?.start?.line + 1 || 1;
|
|
9554
9690
|
transpilerError([
|
|
9555
9691
|
`<$red:Logic Error (Argument):$> ${err.message}{line}`,
|
|
9556
|
-
`<$yellow:Code:$> <$blue:${value.code}$>{line}
|
|
9692
|
+
`<$yellow:Code:$> <$blue:${value.code}$>{line}`,
|
|
9693
|
+
`at line <$yellow:${line}$>{line}`
|
|
9557
9694
|
]);
|
|
9558
9695
|
}
|
|
9559
9696
|
} else {
|
|
@@ -10666,11 +10803,20 @@ class Mapper {
|
|
|
10666
10803
|
|
|
10667
10804
|
/**
|
|
10668
10805
|
* Registers universal utility blocks shared across all SomMark mappers.
|
|
10669
|
-
* These blocks are considered "Format Agnostic."
|
|
10670
10806
|
*
|
|
10671
10807
|
* @param {Mapper} mapper - The mapper instance to register tags on.
|
|
10672
10808
|
*/
|
|
10673
10809
|
function registerSharedOutputs(mapper) {
|
|
10810
|
+
mapper.register(
|
|
10811
|
+
["raw", "Raw"],
|
|
10812
|
+
({ content }) => {
|
|
10813
|
+
return content;
|
|
10814
|
+
},
|
|
10815
|
+
{
|
|
10816
|
+
escape: false, rules: {
|
|
10817
|
+
required_directives: ["raw"]
|
|
10818
|
+
} }
|
|
10819
|
+
);
|
|
10674
10820
|
}
|
|
10675
10821
|
|
|
10676
10822
|
const SVG_ELEMENTS = new Set([
|
|
@@ -10823,6 +10969,7 @@ HTML.register(
|
|
|
10823
10969
|
return "";
|
|
10824
10970
|
},
|
|
10825
10971
|
);
|
|
10972
|
+
registerSharedOutputs(HTML);
|
|
10826
10973
|
|
|
10827
10974
|
/**
|
|
10828
10975
|
* The Markdown Mapper used for generating Markdown text.
|
|
@@ -10849,42 +10996,37 @@ const MARKDOWN = Mapper.define({
|
|
|
10849
10996
|
},
|
|
10850
10997
|
|
|
10851
10998
|
/**
|
|
10852
|
-
* Provides a fallback for unknown tags by
|
|
10853
|
-
|
|
10999
|
+
* Provides a fallback for unknown tags by rendering them as HTML elements.
|
|
11000
|
+
* Passes child nodes to the transpiler, which handles all node types (such as ForEach).
|
|
11001
|
+
**/
|
|
10854
11002
|
getUnknownTag(node) {
|
|
10855
|
-
const id = node.id
|
|
10856
|
-
|
|
11003
|
+
const id = node.id;
|
|
10857
11004
|
return {
|
|
10858
|
-
|
|
11005
|
+
options: { trimAndWrapBlocks: true },
|
|
11006
|
+
render: ({ props, content, isSelfClosing }) => {
|
|
10859
11007
|
const element = this.tag(id).smartAttributes(props, this.customProps, this.options);
|
|
10860
11008
|
if (isSelfClosing || VOID_ELEMENTS.has(id)) return element.selfClose();
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
for (const child of (ast.body || [])) {
|
|
10864
|
-
if (child.type === TEXT$1) rawContent += this.text(child.text);
|
|
10865
|
-
else if (child.type === BLOCK) rawContent += await renderChild(child);
|
|
10866
|
-
}
|
|
10867
|
-
rawContent = rawContent.trim();
|
|
10868
|
-
|
|
10869
|
-
const meaningful = (ast.body || []).filter(c => c.type !== TEXT$1 || c.text.trim());
|
|
10870
|
-
const finalContent = meaningful.length <= 1 ? rawContent : `\n${rawContent}\n`;
|
|
10871
|
-
return element.body(finalContent);
|
|
10872
|
-
},
|
|
10873
|
-
options: { handleAst: true }
|
|
11009
|
+
return element.body(content);
|
|
11010
|
+
}
|
|
10874
11011
|
};
|
|
10875
11012
|
}
|
|
10876
11013
|
});
|
|
10877
11014
|
|
|
10878
11015
|
MARKDOWN.inherit(HTML);
|
|
10879
11016
|
const { md, safeArg } = MARKDOWN;
|
|
11017
|
+
registerSharedOutputs(MARKDOWN);
|
|
10880
11018
|
|
|
10881
11019
|
/**
|
|
10882
11020
|
* Quote - Renders blockquote content or GFM alerts.
|
|
10883
11021
|
*/
|
|
10884
|
-
MARKDOWN.register(
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
11022
|
+
MARKDOWN.register(
|
|
11023
|
+
"quote",
|
|
11024
|
+
({ props, content }) => {
|
|
11025
|
+
const type = safeArg({ props, index: 0, key: "type", fallBack: "" });
|
|
11026
|
+
return md.quote(content, type);
|
|
11027
|
+
},
|
|
11028
|
+
{ resolve: true }
|
|
11029
|
+
);
|
|
10888
11030
|
|
|
10889
11031
|
/**
|
|
10890
11032
|
* Headings - Renders H1-H6 block headings.
|
|
@@ -10964,12 +11106,12 @@ MARKDOWN.register(
|
|
|
10964
11106
|
"link",
|
|
10965
11107
|
({ props, content, isSelfClosing }) => {
|
|
10966
11108
|
if (isSelfClosing) {
|
|
10967
|
-
const text
|
|
10968
|
-
const src
|
|
11109
|
+
const text = safeArg({ props, index: 0, key: "text", fallBack: "" });
|
|
11110
|
+
const src = safeArg({ props, index: 1, key: "src", fallBack: "" });
|
|
10969
11111
|
const title = safeArg({ props, index: 2, key: "title", fallBack: "" });
|
|
10970
11112
|
return md.url("link", text, src, title);
|
|
10971
11113
|
}
|
|
10972
|
-
const src
|
|
11114
|
+
const src = safeArg({ props, index: 0, key: "src", fallBack: "" });
|
|
10973
11115
|
const title = safeArg({ props, index: 1, key: "title", fallBack: "" });
|
|
10974
11116
|
return md.url("link", content, src, title);
|
|
10975
11117
|
},
|
|
@@ -11007,10 +11149,14 @@ MARKDOWN.register(
|
|
|
11007
11149
|
* Escape - Escapes special Markdown characters.
|
|
11008
11150
|
* Self-closing: [escape = "text" !] or [escape = text: "text" !]
|
|
11009
11151
|
*/
|
|
11010
|
-
MARKDOWN.register(
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11152
|
+
MARKDOWN.register(
|
|
11153
|
+
["escape", "e"],
|
|
11154
|
+
function ({ props, content, isSelfClosing }) {
|
|
11155
|
+
const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
|
|
11156
|
+
return this.md.escape(text);
|
|
11157
|
+
},
|
|
11158
|
+
{ resolve: true }
|
|
11159
|
+
);
|
|
11014
11160
|
|
|
11015
11161
|
const ROW_SEP = "\x1E";
|
|
11016
11162
|
const CELL_SEP = "\x1F";
|
|
@@ -11026,12 +11172,16 @@ MARKDOWN.register(
|
|
|
11026
11172
|
const headers = [];
|
|
11027
11173
|
const rows = [];
|
|
11028
11174
|
|
|
11029
|
-
const extractRows = async
|
|
11175
|
+
const extractRows = async sectionNode => {
|
|
11030
11176
|
const sectionRows = [];
|
|
11031
|
-
for (const child of
|
|
11177
|
+
for (const child of sectionNode.body || []) {
|
|
11032
11178
|
if (child.type === BLOCK && child.id?.toLowerCase() === "row") {
|
|
11033
11179
|
const rendered = await renderChild(child, { inTable: true });
|
|
11034
|
-
const cells =
|
|
11180
|
+
const cells =
|
|
11181
|
+
rendered
|
|
11182
|
+
.split(ROW_SEP)[0]
|
|
11183
|
+
?.split(CELL_SEP)
|
|
11184
|
+
.filter(c => c !== "") ?? [];
|
|
11035
11185
|
if (cells.length > 0) sectionRows.push(cells);
|
|
11036
11186
|
} else if (child.type === FOR_EACH) {
|
|
11037
11187
|
const rendered = await renderChild(child, { inTable: true });
|
|
@@ -11065,25 +11215,29 @@ MARKDOWN.register(
|
|
|
11065
11215
|
*/
|
|
11066
11216
|
MARKDOWN.register(["header", "body"], ({ content }) => content);
|
|
11067
11217
|
|
|
11068
|
-
MARKDOWN.register(
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11218
|
+
MARKDOWN.register(
|
|
11219
|
+
"row",
|
|
11220
|
+
async function ({ ast, renderChild, inTable }) {
|
|
11221
|
+
if (!inTable) {
|
|
11222
|
+
let result = "";
|
|
11223
|
+
for (const child of ast.body) {
|
|
11224
|
+
if (child.type === TEXT$1) result += this.text(child.text);
|
|
11225
|
+
else if (child.type === BLOCK) result += await renderChild(child);
|
|
11226
|
+
}
|
|
11227
|
+
return result;
|
|
11074
11228
|
}
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
cells += await renderChild(child, { inTable: true });
|
|
11229
|
+
let cells = "";
|
|
11230
|
+
for (const child of ast.body) {
|
|
11231
|
+
if (child.type !== BLOCK) continue;
|
|
11232
|
+
const id = child.id?.toLowerCase();
|
|
11233
|
+
if (id === "cell" || id === "th" || id === "td") {
|
|
11234
|
+
cells += await renderChild(child, { inTable: true });
|
|
11235
|
+
}
|
|
11083
11236
|
}
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11237
|
+
return cells + ROW_SEP;
|
|
11238
|
+
},
|
|
11239
|
+
{ handleAst: true }
|
|
11240
|
+
);
|
|
11087
11241
|
|
|
11088
11242
|
MARKDOWN.register(["cell", "th", "td"], ({ content, inTable }) => {
|
|
11089
11243
|
return inTable ? content.trim() + CELL_SEP : content;
|
|
@@ -11093,34 +11247,42 @@ MARKDOWN.register(["cell", "th", "td"], ({ content, inTable }) => {
|
|
|
11093
11247
|
* Lists - Authoritative Native AST List resolution.
|
|
11094
11248
|
* Supports Ordered (Number) and Unordered (Dotlex) lists with deep nesting.
|
|
11095
11249
|
*/
|
|
11096
|
-
MARKDOWN.register(
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11250
|
+
MARKDOWN.register(
|
|
11251
|
+
["list", "List"],
|
|
11252
|
+
async function ({ ast, props, renderChild }) {
|
|
11253
|
+
const indicator = safeArg({ props, index: 0, fallBack: "dot" });
|
|
11254
|
+
const isOrdered = indicator === "number" || indicator === "ol";
|
|
11255
|
+
const marker = isOrdered ? "" : indicator === "dot" ? "-" : indicator;
|
|
11256
|
+
const items = [];
|
|
11101
11257
|
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11258
|
+
for (const node of ast.body) {
|
|
11259
|
+
if (node.type !== BLOCK) continue;
|
|
11260
|
+
const id = node.id?.toLowerCase();
|
|
11261
|
+
if (id === "item") {
|
|
11262
|
+
items.push((await renderChild(node)).trim());
|
|
11263
|
+
}
|
|
11107
11264
|
}
|
|
11108
|
-
}
|
|
11109
11265
|
|
|
11110
|
-
|
|
11111
|
-
},
|
|
11266
|
+
return isOrdered ? md.orderedList(items, 0) : md.unorderedList(items, 0, marker);
|
|
11267
|
+
},
|
|
11268
|
+
{ handleAst: true, trimAndWrapBlocks: false }
|
|
11269
|
+
);
|
|
11112
11270
|
|
|
11113
11271
|
/**
|
|
11114
11272
|
* List Helpers - Internal tags for list structural organization.
|
|
11115
11273
|
*/
|
|
11116
|
-
MARKDOWN.register(
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
}
|
|
11274
|
+
MARKDOWN.register(
|
|
11275
|
+
["item", "Item"],
|
|
11276
|
+
async function ({ ast, renderChild }) {
|
|
11277
|
+
let result = "";
|
|
11278
|
+
for (const child of ast.body) {
|
|
11279
|
+
if (child.type === TEXT$1) result += this.text(child.text);
|
|
11280
|
+
else if (child.type === BLOCK) result += await renderChild(child);
|
|
11281
|
+
}
|
|
11282
|
+
return result.trim();
|
|
11283
|
+
},
|
|
11284
|
+
{ handleAst: true, trimAndWrapBlocks: false }
|
|
11285
|
+
);
|
|
11124
11286
|
|
|
11125
11287
|
/**
|
|
11126
11288
|
* Todo - Renders task list items with status markers.
|
|
@@ -11130,19 +11292,23 @@ MARKDOWN.register(["item", "Item"], async function ({ ast, renderChild }) {
|
|
|
11130
11292
|
* [todo = "Add feature", "x" !] positional self-closing (task, status)
|
|
11131
11293
|
* [todo = "x"]Add feature[end] status in prop, task in body
|
|
11132
11294
|
*/
|
|
11133
|
-
MARKDOWN.register(
|
|
11134
|
-
|
|
11295
|
+
MARKDOWN.register(
|
|
11296
|
+
"todo",
|
|
11297
|
+
({ props, content, isSelfClosing }) => {
|
|
11298
|
+
let status, task;
|
|
11135
11299
|
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11300
|
+
if (isSelfClosing) {
|
|
11301
|
+
task = safeArg({ props, index: 0, key: "task", fallBack: "" });
|
|
11302
|
+
status = safeArg({ props, index: 1, key: "status", fallBack: "" });
|
|
11303
|
+
} else {
|
|
11304
|
+
status = safeArg({ props, index: 0, fallBack: "" });
|
|
11305
|
+
task = content;
|
|
11306
|
+
}
|
|
11143
11307
|
|
|
11144
|
-
|
|
11145
|
-
},
|
|
11308
|
+
return md.todo(status, task);
|
|
11309
|
+
},
|
|
11310
|
+
{ trimAndWrapBlocks: false }
|
|
11311
|
+
);
|
|
11146
11312
|
|
|
11147
11313
|
/**
|
|
11148
11314
|
* The MDX Mapper used for generating Markdown with JSX.
|
|
@@ -11352,100 +11518,115 @@ Jsonc.register(["Array", "array"], async function ({ props, ast, depth = 0, inAr
|
|
|
11352
11518
|
/**
|
|
11353
11519
|
* Renders a standard XML tag based on the provided identifier and arguments.
|
|
11354
11520
|
* Ensures strict attribute quoting and handles self-closing tags for empty bodies.
|
|
11355
|
-
*
|
|
11521
|
+
*
|
|
11356
11522
|
* @param {string} id - The XML tag identifier (case-sensitive).
|
|
11357
11523
|
* @param {Object} props - Key-value pairs to be rendered as XML attributes.
|
|
11358
11524
|
* @param {string} content - The rendered inner content of the tag.
|
|
11359
11525
|
* @returns {string} The fully rendered XML tag string.
|
|
11360
11526
|
*/
|
|
11361
11527
|
const renderXmlTag = function (id, props, content, isSelfClosing) {
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11528
|
+
// XML is case-sensitive, so we use the exact id provided
|
|
11529
|
+
const element = this.tag(id);
|
|
11530
|
+
|
|
11531
|
+
// Filter out positional indices (numeric keys) for XML attributes
|
|
11532
|
+
const namedArgs = {};
|
|
11533
|
+
Object.keys(props).forEach(key => {
|
|
11534
|
+
if (isNaN(parseInt(key))) {
|
|
11535
|
+
namedArgs[key] = props[key];
|
|
11536
|
+
}
|
|
11537
|
+
});
|
|
11372
11538
|
|
|
11373
|
-
|
|
11374
|
-
|
|
11539
|
+
// In XML, attributes must always have values (strict = true)
|
|
11540
|
+
element.attributes(namedArgs, true);
|
|
11375
11541
|
|
|
11376
|
-
|
|
11542
|
+
const hasBody = typeof content === "string" && content.trim().length > 0;
|
|
11377
11543
|
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11544
|
+
if (isSelfClosing || !hasBody) {
|
|
11545
|
+
return element.selfClose();
|
|
11546
|
+
}
|
|
11381
11547
|
|
|
11382
|
-
|
|
11548
|
+
return element.body(content);
|
|
11383
11549
|
};
|
|
11384
11550
|
|
|
11385
11551
|
/**
|
|
11386
11552
|
* The XML Mapper used for creating XML pages.
|
|
11387
11553
|
*/
|
|
11388
11554
|
const XML = Mapper.define({
|
|
11389
|
-
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11555
|
+
/**
|
|
11556
|
+
* Renders a comment in XML format.
|
|
11557
|
+
* @param {string} text - The comment content.
|
|
11558
|
+
* @returns {string}
|
|
11559
|
+
*/
|
|
11560
|
+
comment(text) {
|
|
11561
|
+
return `<!-- ${text} -->`;
|
|
11562
|
+
},
|
|
11563
|
+
|
|
11564
|
+
/**
|
|
11565
|
+
* Resolves unknown tags by preserving their original case and applying XML rules.
|
|
11566
|
+
* @param {Object} node - The AST node representing the unknown tag.
|
|
11567
|
+
* @returns {Object} Renderer definition for the tag.
|
|
11568
|
+
*/
|
|
11569
|
+
getUnknownTag(node) {
|
|
11570
|
+
const id = node.id;
|
|
11571
|
+
return {
|
|
11572
|
+
render: ({ props, content, isSelfClosing }) => renderXmlTag.call(this, id, props, content, isSelfClosing),
|
|
11573
|
+
options: {}
|
|
11574
|
+
};
|
|
11575
|
+
},
|
|
11576
|
+
options: {
|
|
11577
|
+
trimAndWrapBlocks: true
|
|
11578
|
+
}
|
|
11410
11579
|
});
|
|
11411
11580
|
|
|
11412
11581
|
/**
|
|
11413
11582
|
* Registers the XML declaration as a self-closing block.
|
|
11414
11583
|
* Usage: [xml = version: "1.0", encoding: "UTF-8"]
|
|
11415
11584
|
*/
|
|
11416
|
-
XML.register(
|
|
11417
|
-
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
|
|
11585
|
+
XML.register(
|
|
11586
|
+
"xml",
|
|
11587
|
+
({ props }) => {
|
|
11588
|
+
const version = props.version || "1.0";
|
|
11589
|
+
const encoding = props.encoding || "UTF-8";
|
|
11590
|
+
return `<?xml version="${version}" encoding="${encoding}"?>`;
|
|
11591
|
+
},
|
|
11592
|
+
{ rules: { is_empty_body: true } }
|
|
11593
|
+
);
|
|
11421
11594
|
|
|
11422
11595
|
/**
|
|
11423
11596
|
* Registers the DOCTYPE declaration.
|
|
11424
11597
|
* Usage: [doctype = root: "note", system: "note.dtd"]
|
|
11425
11598
|
*/
|
|
11426
|
-
XML.register(
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
|
|
11599
|
+
XML.register(
|
|
11600
|
+
["DOCTYPE", "doctype"],
|
|
11601
|
+
({ props }) => {
|
|
11602
|
+
const root = props.root || "root";
|
|
11603
|
+
const system = props.system;
|
|
11604
|
+
const pub = props.public || props.fpi;
|
|
11430
11605
|
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
},
|
|
11606
|
+
if (pub && system) {
|
|
11607
|
+
return `<!DOCTYPE ${root} PUBLIC "${pub}" "${system}">`;
|
|
11608
|
+
} else if (system) {
|
|
11609
|
+
return `<!DOCTYPE ${root} SYSTEM "${system}">`;
|
|
11610
|
+
}
|
|
11611
|
+
return `<!DOCTYPE ${root}>`;
|
|
11612
|
+
},
|
|
11613
|
+
{ rules: { is_empty_body: true } }
|
|
11614
|
+
);
|
|
11438
11615
|
|
|
11439
11616
|
/**
|
|
11440
11617
|
* Registers the XML stylesheet processing instruction.
|
|
11441
11618
|
* Usage: [xml-stylesheet = href: "style.xsl"]
|
|
11442
11619
|
*/
|
|
11443
|
-
XML.register(
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11620
|
+
XML.register(
|
|
11621
|
+
"xml-stylesheet",
|
|
11622
|
+
({ props }) => {
|
|
11623
|
+
const type = props.type || "text/xsl";
|
|
11624
|
+
const href = props.href;
|
|
11625
|
+
if (!href) return "";
|
|
11626
|
+
return `<?xml-stylesheet type="${type}" href="${href}"?>`;
|
|
11627
|
+
},
|
|
11628
|
+
{ rules: { is_empty_body: true } }
|
|
11629
|
+
);
|
|
11449
11630
|
|
|
11450
11631
|
/**
|
|
11451
11632
|
* Registers CDATA sections.
|
|
@@ -11453,10 +11634,12 @@ XML.register("xml-stylesheet", ({ props }) => {
|
|
|
11453
11634
|
* Self-closing: [cdata = "raw content" !] or [cdata = text: "raw content" !]
|
|
11454
11635
|
*/
|
|
11455
11636
|
XML.register("cdata", ({ props, content, isSelfClosing }) => {
|
|
11456
|
-
|
|
11457
|
-
|
|
11637
|
+
const text = isSelfClosing ? (props[0] ?? props.text ?? "") : content;
|
|
11638
|
+
return `<![CDATA[${text}]]>`;
|
|
11458
11639
|
});
|
|
11459
11640
|
|
|
11641
|
+
registerSharedOutputs(XML);
|
|
11642
|
+
|
|
11460
11643
|
const csvEscape = (value) => {
|
|
11461
11644
|
const str = String(value ?? "").trim();
|
|
11462
11645
|
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
@@ -11513,6 +11696,8 @@ CSV.register(["col", "cell", "td"], ({ textContent }) => {
|
|
|
11513
11696
|
return csvEscape(textContent);
|
|
11514
11697
|
}, { handleAst: true, trimAndWrapBlocks: false });
|
|
11515
11698
|
|
|
11699
|
+
registerSharedOutputs(CSV);
|
|
11700
|
+
|
|
11516
11701
|
const isValidInt$1 = (v) => v !== "" && !isNaN(Number(v)) && !v.includes(".");
|
|
11517
11702
|
const isValidFloat$1 = (v) => v !== "" && !isNaN(Number(v)) && v.includes(".");
|
|
11518
11703
|
const isValidNumber$1 = (v) => v !== "" && !isNaN(Number(v));
|
|
@@ -11736,6 +11921,8 @@ TOML.register("array", async ({ props, ast, renderChild }) => {
|
|
|
11736
11921
|
return `${tomlKey(key)} = [${vals.join(", ")}]\n`;
|
|
11737
11922
|
}, { handleAst: true });
|
|
11738
11923
|
|
|
11924
|
+
registerSharedOutputs(TOML);
|
|
11925
|
+
|
|
11739
11926
|
const isValidInt = (v) => v !== "" && !isNaN(Number(v)) && !v.includes(".");
|
|
11740
11927
|
const isValidFloat = (v) => v !== "" && !isNaN(Number(v)) && v.includes(".");
|
|
11741
11928
|
const isValidNumber = (v) => v !== "" && !isNaN(Number(v));
|
|
@@ -12051,6 +12238,8 @@ YAML.register("doc-start", () => "---\n", { rules: { is_empty_body: true } });
|
|
|
12051
12238
|
*/
|
|
12052
12239
|
YAML.register("doc-end", () => "...\n", { rules: { is_empty_body: true } });
|
|
12053
12240
|
|
|
12241
|
+
registerSharedOutputs(YAML);
|
|
12242
|
+
|
|
12054
12243
|
/**
|
|
12055
12244
|
* The Text Mapper used for plain-text extraction.
|
|
12056
12245
|
*/
|
|
@@ -12327,34 +12516,48 @@ async function resolveModules(ast, context) {
|
|
|
12327
12516
|
const baseDir = context.instance.baseDir || ((filename === "anonymous") ? absFilename : posix.dirname(absFilename));
|
|
12328
12517
|
|
|
12329
12518
|
// 1. Helper: Trim AST to remove file-boundary whitespace and "ghost" newlines
|
|
12330
|
-
const trimAst = (nodes) => {
|
|
12519
|
+
const trimAst = (nodes, trimBoundaries = true) => {
|
|
12331
12520
|
if (!nodes) return [];
|
|
12332
12521
|
|
|
12333
|
-
// 1. Filter out
|
|
12334
|
-
// (Comments, Imports,
|
|
12522
|
+
// 1. Filter out whitespace-only text nodes adjacent (directly or through other whitespace)
|
|
12523
|
+
// to non-rendering nodes (Comments, Imports, USE_MODULE).
|
|
12335
12524
|
const nonRenderingTypes = [COMMENT, IMPORT, USE_MODULE];
|
|
12336
12525
|
let res = nodes.filter((node, idx) => {
|
|
12337
12526
|
if (node.type !== TEXT$1 || node.text.trim() !== "") return true;
|
|
12338
12527
|
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
(
|
|
12343
|
-
|
|
12528
|
+
// Walk backwards through consecutive whitespace nodes to find prev non-whitespace
|
|
12529
|
+
let prevIsNonRendering = false;
|
|
12530
|
+
for (let j = idx - 1; j >= 0; j--) {
|
|
12531
|
+
if (nodes[j].type === TEXT$1 && nodes[j].text.trim() === "") continue;
|
|
12532
|
+
prevIsNonRendering = nonRenderingTypes.includes(nodes[j].type);
|
|
12533
|
+
break;
|
|
12534
|
+
}
|
|
12535
|
+
|
|
12536
|
+
// Walk forwards through consecutive whitespace nodes to find next non-whitespace
|
|
12537
|
+
let nextIsNonRendering = false;
|
|
12538
|
+
for (let j = idx + 1; j < nodes.length; j++) {
|
|
12539
|
+
if (nodes[j].type === TEXT$1 && nodes[j].text.trim() === "") continue;
|
|
12540
|
+
nextIsNonRendering = nonRenderingTypes.includes(nodes[j].type);
|
|
12541
|
+
break;
|
|
12542
|
+
}
|
|
12344
12543
|
|
|
12345
|
-
return !
|
|
12544
|
+
return !(prevIsNonRendering || nextIsNonRendering);
|
|
12346
12545
|
});
|
|
12347
12546
|
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
res
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
res
|
|
12547
|
+
if (trimBoundaries) {
|
|
12548
|
+
// 2. Final pass: trim leading/trailing newlines from the remaining boundary text nodes
|
|
12549
|
+
if (res.length > 0 && res[0].type === TEXT$1) {
|
|
12550
|
+
res[0].text = res[0].text.replace(/^[\r\n]+/, "");
|
|
12551
|
+
}
|
|
12552
|
+
if (res.length > 0 && res[res.length - 1].type === TEXT$1) {
|
|
12553
|
+
res[res.length - 1].text = res[res.length - 1].text.replace(/[\r\n]+\s*$/, "");
|
|
12554
|
+
}
|
|
12555
|
+
|
|
12556
|
+
// 3. Remove any nodes that became purely empty after trimming
|
|
12557
|
+
res = res.filter(node => node.type !== TEXT$1 || node.text !== "");
|
|
12354
12558
|
}
|
|
12355
12559
|
|
|
12356
|
-
|
|
12357
|
-
return res.filter(node => node.type !== TEXT$1 || node.text !== "");
|
|
12560
|
+
return res;
|
|
12358
12561
|
};
|
|
12359
12562
|
|
|
12360
12563
|
// 2. Helper: Inject Slots with Indentation Propagation
|
|
@@ -12417,13 +12620,38 @@ async function resolveModules(ast, context) {
|
|
|
12417
12620
|
let resolvedPath = filePath;
|
|
12418
12621
|
for (const [prefix, replacement] of Object.entries(importAliases)) {
|
|
12419
12622
|
if (filePath.startsWith(prefix)) {
|
|
12420
|
-
|
|
12623
|
+
const replaced = filePath.replace(prefix, replacement);
|
|
12624
|
+
// Preserve scheme prefixes (pkg:, http:, etc.) — don't path.resolve them
|
|
12625
|
+
resolvedPath = replaced.startsWith("pkg:") || replaced.startsWith("http://") || replaced.startsWith("https://")
|
|
12626
|
+
? replaced
|
|
12627
|
+
: posix.resolve(context.instance.cwd || "/", replaced);
|
|
12421
12628
|
break;
|
|
12422
12629
|
}
|
|
12423
12630
|
}
|
|
12424
12631
|
|
|
12425
|
-
// 1b.
|
|
12426
|
-
|
|
12632
|
+
// 1b. pkg: — resolve from node_modules at project root
|
|
12633
|
+
let absolutePath;
|
|
12634
|
+
if (resolvedPath.startsWith("pkg:")) {
|
|
12635
|
+
if (!context.instance.fs?.__isNodeFs) {
|
|
12636
|
+
runtimeError([`<$red:Module Error:$> <$cyan:pkg:$> imports are not supported in browser or virtual filesystem mode at line <$yellow:${node.range.start.line + 1}$>`]);
|
|
12637
|
+
}
|
|
12638
|
+
const pkgPath = resolvedPath.slice(4);
|
|
12639
|
+
if (!pkgPath || pkgPath.trim() === "") {
|
|
12640
|
+
runtimeError([`<$red:Module Error:$> <$cyan:pkg:$> path cannot be empty at line <$yellow:${node.range.start.line + 1}$>`]);
|
|
12641
|
+
}
|
|
12642
|
+
const nodeModulesRoot = posix.resolve(context.instance.cwd || "/", "node_modules");
|
|
12643
|
+
absolutePath = posix.resolve(nodeModulesRoot, pkgPath);
|
|
12644
|
+
if (!absolutePath.startsWith(nodeModulesRoot + posix.sep) && absolutePath !== nodeModulesRoot) {
|
|
12645
|
+
runtimeError([`<$red:Module Security Error:$> <$cyan:pkg:${pkgPath}$> resolves outside node_modules — path traversal is not allowed at line <$yellow:${node.range.start.line + 1}$>`]);
|
|
12646
|
+
}
|
|
12647
|
+
} else {
|
|
12648
|
+
// 1c. Resolve relative to current base (FS)
|
|
12649
|
+
absolutePath = resolveModulePath(resolvedPath, currentBaseDir);
|
|
12650
|
+
}
|
|
12651
|
+
|
|
12652
|
+
if (!context.instance.fs) {
|
|
12653
|
+
runtimeError([`<$red:Module Error:$> Cannot import <$magenta:${filePath}$> — no filesystem is available.{N}In browser mode, pass a URL-based <$cyan:baseDir$> or a <$cyan:files$> map to enable module loading.`]);
|
|
12654
|
+
}
|
|
12427
12655
|
|
|
12428
12656
|
// Local Path Resolution with Auto-Extension
|
|
12429
12657
|
let localPath = absolutePath;
|
|
@@ -12576,7 +12804,6 @@ async function resolveModules(ast, context) {
|
|
|
12576
12804
|
Object.entries(node.props).filter(([key]) => {
|
|
12577
12805
|
if (key === "__consumed__") return false;
|
|
12578
12806
|
if (consumed.has(key)) return false; // THE FIX: Filter if hit by v{}
|
|
12579
|
-
if (key === "smark-raw") return false; // directive — must not leak onto root element
|
|
12580
12807
|
return true;
|
|
12581
12808
|
})
|
|
12582
12809
|
);
|
|
@@ -12691,10 +12918,7 @@ const runValidations = (node, target, instance) => {
|
|
|
12691
12918
|
const isStructural = node.type === "Block";
|
|
12692
12919
|
if (isStructural && rules.required_args && Array.isArray(rules.required_args)) {
|
|
12693
12920
|
const missingArgs = rules.required_args.filter(arg => {
|
|
12694
|
-
|
|
12695
|
-
if (typeof arg === "number") {
|
|
12696
|
-
return node.props[arg] === undefined;
|
|
12697
|
-
}
|
|
12921
|
+
if (typeof arg === "number") return node.props[arg] === undefined;
|
|
12698
12922
|
return node.props[arg] === undefined;
|
|
12699
12923
|
});
|
|
12700
12924
|
|
|
@@ -12709,6 +12933,22 @@ const runValidations = (node, target, instance) => {
|
|
|
12709
12933
|
);
|
|
12710
12934
|
}
|
|
12711
12935
|
}
|
|
12936
|
+
|
|
12937
|
+
// -- Directives Validation (Required Directives) ----------------------- //
|
|
12938
|
+
if (isStructural && rules.required_directives && Array.isArray(rules.required_directives)) {
|
|
12939
|
+
const missingDirectives = rules.required_directives.filter(key => node.directives?.[key] === undefined);
|
|
12940
|
+
|
|
12941
|
+
if (missingDirectives.length > 0) {
|
|
12942
|
+
transpilerError(
|
|
12943
|
+
[
|
|
12944
|
+
"{N}",
|
|
12945
|
+
`<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is missing required directive props:$> <$red:${missingDirectives.map(k => `smark-${k}`).join(", ")}$>{N}`,
|
|
12946
|
+
`<$blue:Please ensure these directive props are provided in the template usage.$>`
|
|
12947
|
+
],
|
|
12948
|
+
context
|
|
12949
|
+
);
|
|
12950
|
+
}
|
|
12951
|
+
}
|
|
12712
12952
|
};
|
|
12713
12953
|
|
|
12714
12954
|
/**
|
|
@@ -12880,6 +13120,14 @@ function setDefaultFs(fs) {
|
|
|
12880
13120
|
Evaluator.setDefaultFs(fs);
|
|
12881
13121
|
}
|
|
12882
13122
|
|
|
13123
|
+
function setDefaultEnv(env) {
|
|
13124
|
+
Evaluator.setDefaultEnv(env);
|
|
13125
|
+
}
|
|
13126
|
+
|
|
13127
|
+
function setDefaultAsyncLocalStorage(cls) {
|
|
13128
|
+
Evaluator.setDefaultAsyncLocalStorage(cls);
|
|
13129
|
+
}
|
|
13130
|
+
|
|
12883
13131
|
function setDefaultResolvePath(fn) {
|
|
12884
13132
|
defaultResolvePath = fn;
|
|
12885
13133
|
}
|
|
@@ -12912,7 +13160,7 @@ class SomMark {
|
|
|
12912
13160
|
* @param {string} [options.baseDir=null] - The base directory for resolving relative paths.
|
|
12913
13161
|
*/
|
|
12914
13162
|
constructor(options = {}) {
|
|
12915
|
-
const { src, ast = null, format, mapperFile = null, filename = "anonymous", removeComments = true, placeholders = {}, customProps = [], fallbackTarget = true, outputValidator = null, importAliases = {}, importStack = [], baseDir = null, moduleCache = null, showSpinner = true, security = {}, dualOutput = false, moduleIdentityToken = null } = options;
|
|
13163
|
+
const { src, ast = null, format, mapperFile = null, filename = "anonymous", removeComments = true, placeholders = {}, customProps = [], fallbackTarget = true, outputValidator = null, importAliases = {}, importStack = [], baseDir = null, moduleCache = null, showSpinner = true, security = {}, dualOutput = false, webOutputs = false, moduleIdentityToken = null } = options;
|
|
12916
13164
|
this.rawSettings = options;
|
|
12917
13165
|
this.src = src;
|
|
12918
13166
|
this.ast = ast;
|
|
@@ -12923,6 +13171,7 @@ class SomMark {
|
|
|
12923
13171
|
this.placeholders = placeholders;
|
|
12924
13172
|
this.customProps = customProps;
|
|
12925
13173
|
this.dualOutput = dualOutput;
|
|
13174
|
+
this.webOutputs = webOutputs;
|
|
12926
13175
|
this.cwd = options.baseDir || (options.files ? "/" : defaultCwd);
|
|
12927
13176
|
this.fs = options.fs
|
|
12928
13177
|
|| (options.files ? new VirtualFS(options.files) : null)
|
|
@@ -12951,7 +13200,8 @@ class SomMark {
|
|
|
12951
13200
|
allowFetch: security?.allowFetch !== false,
|
|
12952
13201
|
allowHttp: security?.allowHttp === true,
|
|
12953
13202
|
allowedOrigins: Array.isArray(security?.allowedOrigins) ? security.allowedOrigins.map(o => o.toLowerCase()) : null,
|
|
12954
|
-
allowedExtensions: Array.isArray(security?.allowedExtensions) ? security.allowedExtensions.map(e => e.toLowerCase()) : null
|
|
13203
|
+
allowedExtensions: Array.isArray(security?.allowedExtensions) ? security.allowedExtensions.map(e => e.toLowerCase()) : null,
|
|
13204
|
+
env: Array.isArray(security?.env) ? security.env : []
|
|
12955
13205
|
};
|
|
12956
13206
|
this.warnings = [];
|
|
12957
13207
|
this._prepared = false;
|
|
@@ -13138,6 +13388,7 @@ class SomMark {
|
|
|
13138
13388
|
security: this.security,
|
|
13139
13389
|
settings: this.rawSettings,
|
|
13140
13390
|
dualOutput: this.dualOutput,
|
|
13391
|
+
webOutputs: this.webOutputs,
|
|
13141
13392
|
instance: this
|
|
13142
13393
|
});
|
|
13143
13394
|
|
|
@@ -13262,8 +13513,24 @@ async function findAndLoadConfig(targetPath) {
|
|
|
13262
13513
|
return await defaultFindAndLoadConfig(targetPath);
|
|
13263
13514
|
}
|
|
13264
13515
|
|
|
13516
|
+
class AsyncLocalStorage {
|
|
13517
|
+
#store = undefined;
|
|
13518
|
+
run(store, fn) {
|
|
13519
|
+
const prev = this.#store;
|
|
13520
|
+
this.#store = store;
|
|
13521
|
+
try { return fn(); }
|
|
13522
|
+
finally { this.#store = prev; }
|
|
13523
|
+
}
|
|
13524
|
+
getStore() { return this.#store; }
|
|
13525
|
+
exit(fn) { return fn(); }
|
|
13526
|
+
enterWith(store) { this.#store = store; }
|
|
13527
|
+
disable() {}
|
|
13528
|
+
}
|
|
13529
|
+
|
|
13265
13530
|
setDefaultFs(null);
|
|
13266
13531
|
setDefaultCwd("/");
|
|
13532
|
+
setDefaultEnv(null);
|
|
13533
|
+
setDefaultAsyncLocalStorage(AsyncLocalStorage);
|
|
13267
13534
|
|
|
13268
13535
|
/**
|
|
13269
13536
|
* Resolves a relative path into a full URL using the current document location.
|
|
@@ -13345,4 +13612,4 @@ function renderCompiledHTML(container, html) {
|
|
|
13345
13612
|
}
|
|
13346
13613
|
}
|
|
13347
13614
|
|
|
13348
|
-
export { CSV, Evaluator, formats as FORMATS, HTML, Json, Jsonc, MARKDOWN, MDX, Mapper, TOKEN_TYPES, TOML, XML, YAML, SomMark as default, enableColor, findAndLoadConfig, labels, lex, lexSync, parse, parseSync, preprocessRuntimeLogic, registerSharedOutputs, renderCompiledHTML, resolveBaseDir, safeArg$1 as safeArg, setDefaultCwd, setDefaultFindAndLoadConfig, setDefaultFs, setDefaultResolvePath, transpile };
|
|
13615
|
+
export { CSV, Evaluator, formats as FORMATS, HTML, Json, Jsonc, MARKDOWN, MDX, Mapper, TOKEN_TYPES, TOML, XML, YAML, SomMark as default, enableColor, findAndLoadConfig, labels, lex, lexSync, parse, parseSync, preprocessRuntimeLogic, registerSharedOutputs, renderCompiledHTML, resolveBaseDir, safeArg$1 as safeArg, setDefaultAsyncLocalStorage, setDefaultCwd, setDefaultEnv, setDefaultFindAndLoadConfig, setDefaultFs, setDefaultResolvePath, transpile };
|