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.
@@ -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
- blockNode.props[String(argIndex++)] = v;
1963
- if (k) {
1964
- blockNode.props[k] = v;
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} is a function nested inside an object and will be ignored. Move it to the top level: variables.${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
- `'i' is a reserved variable name for the loop index.{N}Use a different name for the 'as' prop, e.g. as: "item"{line}`,
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
- let output = "";
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
- output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState, extraCtx);
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 output;
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.props?.["smark-raw"] === "true" || node.props?.["smark-raw"] === true)) {
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 { "smark-raw": _, ...cleanArgs } = node.props;
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 using the HTML mapper instead.
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.toLowerCase();
10856
-
11003
+ const id = node.id;
10857
11004
  return {
10858
- render: async ({ props, ast, isSelfClosing, renderChild }) => {
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
- let rawContent = "";
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("quote", ({ props, content }) => {
10885
- const type = safeArg({ props, index: 0, key: "type", fallBack: "" });
10886
- return md.quote(content, type);
10887
- }, { resolve: true });
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 = safeArg({ props, index: 0, key: "text", fallBack: "" });
10968
- const src = safeArg({ props, index: 1, key: "src", fallBack: "" });
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 = safeArg({ props, index: 0, key: "src", fallBack: "" });
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(["escape", "e"], function ({ props, content, isSelfClosing }) {
11011
- const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
11012
- return this.md.escape(text);
11013
- }, { resolve: true });
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 (sectionNode) => {
11175
+ const extractRows = async sectionNode => {
11030
11176
  const sectionRows = [];
11031
- for (const child of (sectionNode.body || [])) {
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 = rendered.split(ROW_SEP)[0]?.split(CELL_SEP).filter(c => c !== "") ?? [];
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("row", async function ({ ast, renderChild, inTable }) {
11069
- if (!inTable) {
11070
- let result = "";
11071
- for (const child of ast.body) {
11072
- if (child.type === TEXT$1) result += this.text(child.text);
11073
- else if (child.type === BLOCK) result += await renderChild(child);
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
- return result;
11076
- }
11077
- let cells = "";
11078
- for (const child of ast.body) {
11079
- if (child.type !== BLOCK) continue;
11080
- const id = child.id?.toLowerCase();
11081
- if (id === "cell" || id === "th" || id === "td") {
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
- return cells + ROW_SEP;
11086
- }, { handleAst: true });
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(["list", "List"], async function ({ ast, props, renderChild }) {
11097
- const indicator = safeArg({ props, index: 0, fallBack: "dot" });
11098
- const isOrdered = indicator === "number" || indicator === "ol";
11099
- const marker = isOrdered ? "" : (indicator === "dot" ? "-" : indicator);
11100
- const items = [];
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
- for (const node of ast.body) {
11103
- if (node.type !== BLOCK) continue;
11104
- const id = node.id?.toLowerCase();
11105
- if (id === "item") {
11106
- items.push((await renderChild(node)).trim());
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
- return isOrdered ? md.orderedList(items, 0) : md.unorderedList(items, 0, marker);
11111
- }, { handleAst: true, trimAndWrapBlocks: false });
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(["item", "Item"], async function ({ ast, renderChild }) {
11117
- let result = "";
11118
- for (const child of ast.body) {
11119
- if (child.type === TEXT$1) result += this.text(child.text);
11120
- else if (child.type === BLOCK) result += await renderChild(child);
11121
- }
11122
- return result.trim();
11123
- }, { handleAst: true, trimAndWrapBlocks: false });
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("todo", ({ props, content, isSelfClosing }) => {
11134
- let status, task;
11295
+ MARKDOWN.register(
11296
+ "todo",
11297
+ ({ props, content, isSelfClosing }) => {
11298
+ let status, task;
11135
11299
 
11136
- if (isSelfClosing) {
11137
- task = safeArg({ props, index: 0, key: "task", fallBack: "" });
11138
- status = safeArg({ props, index: 1, key: "status", fallBack: "" });
11139
- } else {
11140
- status = safeArg({ props, index: 0, fallBack: "" });
11141
- task = content;
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
- return md.todo(status, task);
11145
- }, { trimAndWrapBlocks: false });
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
- // XML is case-sensitive, so we use the exact id provided
11363
- const element = this.tag(id);
11364
-
11365
- // Filter out positional indices (numeric keys) for XML attributes
11366
- const namedArgs = {};
11367
- Object.keys(props).forEach(key => {
11368
- if (isNaN(parseInt(key))) {
11369
- namedArgs[key] = props[key];
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
- // In XML, attributes must always have values (strict = true)
11374
- element.attributes(namedArgs, true);
11539
+ // In XML, attributes must always have values (strict = true)
11540
+ element.attributes(namedArgs, true);
11375
11541
 
11376
- const hasBody = typeof content === "string" && content.trim().length > 0;
11542
+ const hasBody = typeof content === "string" && content.trim().length > 0;
11377
11543
 
11378
- if (isSelfClosing || !hasBody) {
11379
- return element.selfClose();
11380
- }
11544
+ if (isSelfClosing || !hasBody) {
11545
+ return element.selfClose();
11546
+ }
11381
11547
 
11382
- return element.body(content);
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
- * Renders a comment in XML format.
11391
- * @param {string} text - The comment content.
11392
- * @returns {string}
11393
- */
11394
- comment(text) {
11395
- return `<!-- ${text} -->`;
11396
- },
11397
-
11398
- /**
11399
- * Resolves unknown tags by preserving their original case and applying XML rules.
11400
- * @param {Object} node - The AST node representing the unknown tag.
11401
- * @returns {Object} Renderer definition for the tag.
11402
- */
11403
- getUnknownTag(node) {
11404
- const id = node.id;
11405
- return {
11406
- render: ({ props, content, isSelfClosing }) => renderXmlTag.call(this, id, props, content, isSelfClosing),
11407
- options: {}
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("xml", ({ props }) => {
11417
- const version = props.version || "1.0";
11418
- const encoding = props.encoding || "UTF-8";
11419
- return `<?xml version="${version}" encoding="${encoding}"?>`;
11420
- }, { rules: { is_empty_body: true } });
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(["DOCTYPE", "doctype"], ({ props }) => {
11427
- const root = props.root || "root";
11428
- const system = props.system;
11429
- const pub = props.public || props.fpi;
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
- if (pub && system) {
11432
- return `<!DOCTYPE ${root} PUBLIC "${pub}" "${system}">`;
11433
- } else if (system) {
11434
- return `<!DOCTYPE ${root} SYSTEM "${system}">`;
11435
- }
11436
- return `<!DOCTYPE ${root}>`;
11437
- }, { rules: { is_empty_body: true } });
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("xml-stylesheet", ({ props }) => {
11444
- const type = props.type || "text/xsl";
11445
- const href = props.href;
11446
- if (!href) return "";
11447
- return `<?xml-stylesheet type="${type}" href="${href}"?>`;
11448
- }, { rules: { is_empty_body: true } });
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
- const text = isSelfClosing ? (props[0] ?? props.text ?? "") : content;
11457
- return `<![CDATA[${text}]]>`;
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 internal whitespace-only nodes that are adjacent to non-rendering nodes
12334
- // (Comments, Imports, etc. shouldn't leave "ghost" newlines)
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
- const prev = nodes[idx - 1];
12340
- const next = nodes[idx + 1];
12341
- const isAdjacentToNonRendering =
12342
- (prev && nonRenderingTypes.includes(prev.type)) ||
12343
- (next && nonRenderingTypes.includes(next.type));
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 !isAdjacentToNonRendering;
12544
+ return !(prevIsNonRendering || nextIsNonRendering);
12346
12545
  });
12347
12546
 
12348
- // 2. Final pass: trim leading/trailing newlines from the remaining boundary text nodes
12349
- if (res.length > 0 && res[0].type === TEXT$1) {
12350
- res[0].text = res[0].text.replace(/^[\r\n]+/, "");
12351
- }
12352
- if (res.length > 0 && res[res.length - 1].type === TEXT$1) {
12353
- res[res.length - 1].text = res[res.length - 1].text.replace(/[\r\n]+\s*$/, "");
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
- // 3. Remove any nodes that became purely empty after trimming
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
- resolvedPath = posix.resolve(context.instance.cwd || "/", filePath.replace(prefix, replacement));
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. Resolve relative to current base (FS)
12426
- const absolutePath = resolveModulePath(resolvedPath, currentBaseDir);
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
- // Check if the argument exists in named args or as a positional arg (if arg is a number)
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 };