sommark 5.0.5 → 5.1.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
@@ -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
  *
@@ -9010,6 +9033,17 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9010
9033
 
9011
9034
  if (node.type === FOR_EACH) {
9012
9035
  const transpiledArgs = await transpileArgs(node.props);
9036
+
9037
+ if (!node.props || (node.props[0] === undefined && node.props["items"] === undefined)) {
9038
+ const line = node.range?.start?.line + 1 || 1;
9039
+ transpilerError([
9040
+ `<$red:Missing Prop Error in [for-each]:$>{line}`,
9041
+ `[for-each] requires an array as its first prop, e.g. [for-each = \${ array }\$]{line}`,
9042
+ `at line <$yellow:${line}$>{line}`
9043
+ ]);
9044
+ return "";
9045
+ }
9046
+
9013
9047
  const items = mapper_file ? mapper_file.safeArg({ props: transpiledArgs, index: 0, key: "items", fallBack: [] }) : [];
9014
9048
 
9015
9049
  if (!Array.isArray(items)) {
@@ -9023,11 +9057,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9023
9057
  }
9024
9058
 
9025
9059
  const asVar = transpiledArgs.as || "value";
9026
- if (asVar === "i") {
9060
+ if (asVar === "i" || asVar === "length") {
9027
9061
  const line = node.range?.start?.line + 1 || 1;
9028
9062
  transpilerError([
9029
9063
  `<$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}`,
9064
+ `'${asVar}' is a reserved variable name.{N}Use a different name for the 'as' prop, e.g. as: "item"{line}`,
9031
9065
  `at line <$yellow:${line}$>{line}`
9032
9066
  ]);
9033
9067
  return "";
@@ -9057,22 +9091,28 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9057
9091
  }
9058
9092
  }
9059
9093
 
9060
- let output = "";
9094
+ const rawJoin = transpiledArgs.join ?? null;
9095
+ const join = rawJoin !== null ? rawJoin.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\r/g, "\r") : null;
9096
+ const parts = [];
9061
9097
  let idx = 0;
9098
+ const length = items.length;
9062
9099
  for (const item of items) {
9063
9100
  Evaluator.pushScope();
9064
9101
  Evaluator.inject({
9065
9102
  [asVar]: item,
9066
- i: idx++
9103
+ i: idx++,
9104
+ length
9067
9105
  });
9068
9106
 
9107
+ let iterOutput = "";
9069
9108
  for (let j = 0; j < cleanedBody.length; j++) {
9070
- output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState, extraCtx);
9109
+ iterOutput += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState, extraCtx);
9071
9110
  }
9072
9111
 
9073
9112
  await Evaluator.popScope();
9113
+ parts.push(iterOutput);
9074
9114
  }
9075
- return output;
9115
+ return join !== null ? parts.join(join) : parts.join("");
9076
9116
  }
9077
9117
 
9078
9118
  let secretId = null;
@@ -9100,13 +9140,12 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9100
9140
  }
9101
9141
 
9102
9142
  // 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)) {
9143
+ if (node.type === BLOCK && (node.directives?.raw === "true" || node.directives?.raw === true)) {
9104
9144
  if (generateRuntimeOutput) return "";
9105
9145
  const rawContent = node.body?.map(n => String(n.text || "")).join("") || "";
9106
- const { "smark-raw": _, ...cleanArgs } = node.props;
9107
- const transpiledArgs = await transpileArgs(cleanArgs);
9146
+ const transpiledArgs = await transpileArgs(node.props);
9108
9147
  if (Evaluator.active?.hasDynamicTag?.(node.id)) {
9109
- return await Evaluator.active.executeDynamicTag(node.id, { props: transpiledArgs, content: rawContent, textContent: rawContent });
9148
+ return await Evaluator.active.executeDynamicTag(node.id, { props: transpiledArgs, directives: node.directives, content: rawContent, textContent: rawContent });
9110
9149
  }
9111
9150
  let rawTarget = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
9112
9151
  if (!rawTarget && mapper_file) rawTarget = mapper_file.getUnknownTag(node);
@@ -9114,6 +9153,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9114
9153
  const isManualMode = !!rawTarget.options?.handleAst;
9115
9154
  return await rawTarget.render.call(mapper_file, {
9116
9155
  props: transpiledArgs,
9156
+ directives: node.directives,
9117
9157
  content: rawContent,
9118
9158
  textContent: rawContent,
9119
9159
  ast: isManualMode ? node : undefined,
@@ -9225,6 +9265,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9225
9265
 
9226
9266
  return await target.render.call(mapper_file, {
9227
9267
  props: transpiledArgs,
9268
+ directives: node.directives,
9228
9269
  content: "",
9229
9270
  textContent: richText || textContent,
9230
9271
  ast: cleanAst,
@@ -9243,6 +9284,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9243
9284
  }
9244
9285
  result += await target.render.call(mapper_file, {
9245
9286
  props: transpiledArgs,
9287
+ directives: node.directives,
9246
9288
  content,
9247
9289
  textContent,
9248
9290
  ast: new Proxy({}, {
@@ -10666,11 +10708,20 @@ class Mapper {
10666
10708
 
10667
10709
  /**
10668
10710
  * Registers universal utility blocks shared across all SomMark mappers.
10669
- * These blocks are considered "Format Agnostic."
10670
10711
  *
10671
10712
  * @param {Mapper} mapper - The mapper instance to register tags on.
10672
10713
  */
10673
10714
  function registerSharedOutputs(mapper) {
10715
+ mapper.register(
10716
+ ["raw", "Raw"],
10717
+ ({ content }) => {
10718
+ return content;
10719
+ },
10720
+ {
10721
+ escape: false, rules: {
10722
+ required_directives: ["raw"]
10723
+ } }
10724
+ );
10674
10725
  }
10675
10726
 
10676
10727
  const SVG_ELEMENTS = new Set([
@@ -10823,6 +10874,7 @@ HTML.register(
10823
10874
  return "";
10824
10875
  },
10825
10876
  );
10877
+ registerSharedOutputs(HTML);
10826
10878
 
10827
10879
  /**
10828
10880
  * The Markdown Mapper used for generating Markdown text.
@@ -10849,42 +10901,37 @@ const MARKDOWN = Mapper.define({
10849
10901
  },
10850
10902
 
10851
10903
  /**
10852
- * Provides a fallback for unknown tags by using the HTML mapper instead.
10853
- */
10904
+ * Provides a fallback for unknown tags by rendering them as HTML elements.
10905
+ * Passes child nodes to the transpiler, which handles all node types (such as ForEach).
10906
+ **/
10854
10907
  getUnknownTag(node) {
10855
- const id = node.id.toLowerCase();
10856
-
10908
+ const id = node.id;
10857
10909
  return {
10858
- render: async ({ props, ast, isSelfClosing, renderChild }) => {
10910
+ options: { trimAndWrapBlocks: true },
10911
+ render: ({ props, content, isSelfClosing }) => {
10859
10912
  const element = this.tag(id).smartAttributes(props, this.customProps, this.options);
10860
10913
  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 }
10914
+ return element.body(content);
10915
+ }
10874
10916
  };
10875
10917
  }
10876
10918
  });
10877
10919
 
10878
10920
  MARKDOWN.inherit(HTML);
10879
10921
  const { md, safeArg } = MARKDOWN;
10922
+ registerSharedOutputs(MARKDOWN);
10880
10923
 
10881
10924
  /**
10882
10925
  * Quote - Renders blockquote content or GFM alerts.
10883
10926
  */
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 });
10927
+ MARKDOWN.register(
10928
+ "quote",
10929
+ ({ props, content }) => {
10930
+ const type = safeArg({ props, index: 0, key: "type", fallBack: "" });
10931
+ return md.quote(content, type);
10932
+ },
10933
+ { resolve: true }
10934
+ );
10888
10935
 
10889
10936
  /**
10890
10937
  * Headings - Renders H1-H6 block headings.
@@ -10964,12 +11011,12 @@ MARKDOWN.register(
10964
11011
  "link",
10965
11012
  ({ props, content, isSelfClosing }) => {
10966
11013
  if (isSelfClosing) {
10967
- const text = safeArg({ props, index: 0, key: "text", fallBack: "" });
10968
- const src = safeArg({ props, index: 1, key: "src", fallBack: "" });
11014
+ const text = safeArg({ props, index: 0, key: "text", fallBack: "" });
11015
+ const src = safeArg({ props, index: 1, key: "src", fallBack: "" });
10969
11016
  const title = safeArg({ props, index: 2, key: "title", fallBack: "" });
10970
11017
  return md.url("link", text, src, title);
10971
11018
  }
10972
- const src = safeArg({ props, index: 0, key: "src", fallBack: "" });
11019
+ const src = safeArg({ props, index: 0, key: "src", fallBack: "" });
10973
11020
  const title = safeArg({ props, index: 1, key: "title", fallBack: "" });
10974
11021
  return md.url("link", content, src, title);
10975
11022
  },
@@ -11007,10 +11054,14 @@ MARKDOWN.register(
11007
11054
  * Escape - Escapes special Markdown characters.
11008
11055
  * Self-closing: [escape = "text" !] or [escape = text: "text" !]
11009
11056
  */
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 });
11057
+ MARKDOWN.register(
11058
+ ["escape", "e"],
11059
+ function ({ props, content, isSelfClosing }) {
11060
+ const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
11061
+ return this.md.escape(text);
11062
+ },
11063
+ { resolve: true }
11064
+ );
11014
11065
 
11015
11066
  const ROW_SEP = "\x1E";
11016
11067
  const CELL_SEP = "\x1F";
@@ -11026,12 +11077,16 @@ MARKDOWN.register(
11026
11077
  const headers = [];
11027
11078
  const rows = [];
11028
11079
 
11029
- const extractRows = async (sectionNode) => {
11080
+ const extractRows = async sectionNode => {
11030
11081
  const sectionRows = [];
11031
- for (const child of (sectionNode.body || [])) {
11082
+ for (const child of sectionNode.body || []) {
11032
11083
  if (child.type === BLOCK && child.id?.toLowerCase() === "row") {
11033
11084
  const rendered = await renderChild(child, { inTable: true });
11034
- const cells = rendered.split(ROW_SEP)[0]?.split(CELL_SEP).filter(c => c !== "") ?? [];
11085
+ const cells =
11086
+ rendered
11087
+ .split(ROW_SEP)[0]
11088
+ ?.split(CELL_SEP)
11089
+ .filter(c => c !== "") ?? [];
11035
11090
  if (cells.length > 0) sectionRows.push(cells);
11036
11091
  } else if (child.type === FOR_EACH) {
11037
11092
  const rendered = await renderChild(child, { inTable: true });
@@ -11065,25 +11120,29 @@ MARKDOWN.register(
11065
11120
  */
11066
11121
  MARKDOWN.register(["header", "body"], ({ content }) => content);
11067
11122
 
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);
11123
+ MARKDOWN.register(
11124
+ "row",
11125
+ async function ({ ast, renderChild, inTable }) {
11126
+ if (!inTable) {
11127
+ let result = "";
11128
+ for (const child of ast.body) {
11129
+ if (child.type === TEXT$1) result += this.text(child.text);
11130
+ else if (child.type === BLOCK) result += await renderChild(child);
11131
+ }
11132
+ return result;
11074
11133
  }
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 });
11134
+ let cells = "";
11135
+ for (const child of ast.body) {
11136
+ if (child.type !== BLOCK) continue;
11137
+ const id = child.id?.toLowerCase();
11138
+ if (id === "cell" || id === "th" || id === "td") {
11139
+ cells += await renderChild(child, { inTable: true });
11140
+ }
11083
11141
  }
11084
- }
11085
- return cells + ROW_SEP;
11086
- }, { handleAst: true });
11142
+ return cells + ROW_SEP;
11143
+ },
11144
+ { handleAst: true }
11145
+ );
11087
11146
 
11088
11147
  MARKDOWN.register(["cell", "th", "td"], ({ content, inTable }) => {
11089
11148
  return inTable ? content.trim() + CELL_SEP : content;
@@ -11093,34 +11152,42 @@ MARKDOWN.register(["cell", "th", "td"], ({ content, inTable }) => {
11093
11152
  * Lists - Authoritative Native AST List resolution.
11094
11153
  * Supports Ordered (Number) and Unordered (Dotlex) lists with deep nesting.
11095
11154
  */
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 = [];
11155
+ MARKDOWN.register(
11156
+ ["list", "List"],
11157
+ async function ({ ast, props, renderChild }) {
11158
+ const indicator = safeArg({ props, index: 0, fallBack: "dot" });
11159
+ const isOrdered = indicator === "number" || indicator === "ol";
11160
+ const marker = isOrdered ? "" : indicator === "dot" ? "-" : indicator;
11161
+ const items = [];
11101
11162
 
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());
11163
+ for (const node of ast.body) {
11164
+ if (node.type !== BLOCK) continue;
11165
+ const id = node.id?.toLowerCase();
11166
+ if (id === "item") {
11167
+ items.push((await renderChild(node)).trim());
11168
+ }
11107
11169
  }
11108
- }
11109
11170
 
11110
- return isOrdered ? md.orderedList(items, 0) : md.unorderedList(items, 0, marker);
11111
- }, { handleAst: true, trimAndWrapBlocks: false });
11171
+ return isOrdered ? md.orderedList(items, 0) : md.unorderedList(items, 0, marker);
11172
+ },
11173
+ { handleAst: true, trimAndWrapBlocks: false }
11174
+ );
11112
11175
 
11113
11176
  /**
11114
11177
  * List Helpers - Internal tags for list structural organization.
11115
11178
  */
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 });
11179
+ MARKDOWN.register(
11180
+ ["item", "Item"],
11181
+ async function ({ ast, renderChild }) {
11182
+ let result = "";
11183
+ for (const child of ast.body) {
11184
+ if (child.type === TEXT$1) result += this.text(child.text);
11185
+ else if (child.type === BLOCK) result += await renderChild(child);
11186
+ }
11187
+ return result.trim();
11188
+ },
11189
+ { handleAst: true, trimAndWrapBlocks: false }
11190
+ );
11124
11191
 
11125
11192
  /**
11126
11193
  * Todo - Renders task list items with status markers.
@@ -11130,19 +11197,23 @@ MARKDOWN.register(["item", "Item"], async function ({ ast, renderChild }) {
11130
11197
  * [todo = "Add feature", "x" !] positional self-closing (task, status)
11131
11198
  * [todo = "x"]Add feature[end] status in prop, task in body
11132
11199
  */
11133
- MARKDOWN.register("todo", ({ props, content, isSelfClosing }) => {
11134
- let status, task;
11200
+ MARKDOWN.register(
11201
+ "todo",
11202
+ ({ props, content, isSelfClosing }) => {
11203
+ let status, task;
11135
11204
 
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
- }
11205
+ if (isSelfClosing) {
11206
+ task = safeArg({ props, index: 0, key: "task", fallBack: "" });
11207
+ status = safeArg({ props, index: 1, key: "status", fallBack: "" });
11208
+ } else {
11209
+ status = safeArg({ props, index: 0, fallBack: "" });
11210
+ task = content;
11211
+ }
11143
11212
 
11144
- return md.todo(status, task);
11145
- }, { trimAndWrapBlocks: false });
11213
+ return md.todo(status, task);
11214
+ },
11215
+ { trimAndWrapBlocks: false }
11216
+ );
11146
11217
 
11147
11218
  /**
11148
11219
  * The MDX Mapper used for generating Markdown with JSX.
@@ -11352,100 +11423,115 @@ Jsonc.register(["Array", "array"], async function ({ props, ast, depth = 0, inAr
11352
11423
  /**
11353
11424
  * Renders a standard XML tag based on the provided identifier and arguments.
11354
11425
  * Ensures strict attribute quoting and handles self-closing tags for empty bodies.
11355
- *
11426
+ *
11356
11427
  * @param {string} id - The XML tag identifier (case-sensitive).
11357
11428
  * @param {Object} props - Key-value pairs to be rendered as XML attributes.
11358
11429
  * @param {string} content - The rendered inner content of the tag.
11359
11430
  * @returns {string} The fully rendered XML tag string.
11360
11431
  */
11361
11432
  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
- });
11433
+ // XML is case-sensitive, so we use the exact id provided
11434
+ const element = this.tag(id);
11372
11435
 
11373
- // In XML, attributes must always have values (strict = true)
11374
- element.attributes(namedArgs, true);
11436
+ // Filter out positional indices (numeric keys) for XML attributes
11437
+ const namedArgs = {};
11438
+ Object.keys(props).forEach(key => {
11439
+ if (isNaN(parseInt(key))) {
11440
+ namedArgs[key] = props[key];
11441
+ }
11442
+ });
11375
11443
 
11376
- const hasBody = typeof content === "string" && content.trim().length > 0;
11444
+ // In XML, attributes must always have values (strict = true)
11445
+ element.attributes(namedArgs, true);
11377
11446
 
11378
- if (isSelfClosing || !hasBody) {
11379
- return element.selfClose();
11380
- }
11447
+ const hasBody = typeof content === "string" && content.trim().length > 0;
11381
11448
 
11382
- return element.body(content);
11449
+ if (isSelfClosing || !hasBody) {
11450
+ return element.selfClose();
11451
+ }
11452
+
11453
+ return element.body(content);
11383
11454
  };
11384
11455
 
11385
11456
  /**
11386
11457
  * The XML Mapper used for creating XML pages.
11387
11458
  */
11388
11459
  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
- }
11460
+ /**
11461
+ * Renders a comment in XML format.
11462
+ * @param {string} text - The comment content.
11463
+ * @returns {string}
11464
+ */
11465
+ comment(text) {
11466
+ return `<!-- ${text} -->`;
11467
+ },
11468
+
11469
+ /**
11470
+ * Resolves unknown tags by preserving their original case and applying XML rules.
11471
+ * @param {Object} node - The AST node representing the unknown tag.
11472
+ * @returns {Object} Renderer definition for the tag.
11473
+ */
11474
+ getUnknownTag(node) {
11475
+ const id = node.id;
11476
+ return {
11477
+ render: ({ props, content, isSelfClosing }) => renderXmlTag.call(this, id, props, content, isSelfClosing),
11478
+ options: {}
11479
+ };
11480
+ },
11481
+ options: {
11482
+ trimAndWrapBlocks: true
11483
+ }
11410
11484
  });
11411
11485
 
11412
11486
  /**
11413
11487
  * Registers the XML declaration as a self-closing block.
11414
11488
  * Usage: [xml = version: "1.0", encoding: "UTF-8"]
11415
11489
  */
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 } });
11490
+ XML.register(
11491
+ "xml",
11492
+ ({ props }) => {
11493
+ const version = props.version || "1.0";
11494
+ const encoding = props.encoding || "UTF-8";
11495
+ return `<?xml version="${version}" encoding="${encoding}"?>`;
11496
+ },
11497
+ { rules: { is_empty_body: true } }
11498
+ );
11421
11499
 
11422
11500
  /**
11423
11501
  * Registers the DOCTYPE declaration.
11424
11502
  * Usage: [doctype = root: "note", system: "note.dtd"]
11425
11503
  */
11426
- XML.register(["DOCTYPE", "doctype"], ({ props }) => {
11427
- const root = props.root || "root";
11428
- const system = props.system;
11429
- const pub = props.public || props.fpi;
11504
+ XML.register(
11505
+ ["DOCTYPE", "doctype"],
11506
+ ({ props }) => {
11507
+ const root = props.root || "root";
11508
+ const system = props.system;
11509
+ const pub = props.public || props.fpi;
11430
11510
 
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 } });
11511
+ if (pub && system) {
11512
+ return `<!DOCTYPE ${root} PUBLIC "${pub}" "${system}">`;
11513
+ } else if (system) {
11514
+ return `<!DOCTYPE ${root} SYSTEM "${system}">`;
11515
+ }
11516
+ return `<!DOCTYPE ${root}>`;
11517
+ },
11518
+ { rules: { is_empty_body: true } }
11519
+ );
11438
11520
 
11439
11521
  /**
11440
11522
  * Registers the XML stylesheet processing instruction.
11441
11523
  * Usage: [xml-stylesheet = href: "style.xsl"]
11442
11524
  */
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 } });
11525
+ XML.register(
11526
+ "xml-stylesheet",
11527
+ ({ props }) => {
11528
+ const type = props.type || "text/xsl";
11529
+ const href = props.href;
11530
+ if (!href) return "";
11531
+ return `<?xml-stylesheet type="${type}" href="${href}"?>`;
11532
+ },
11533
+ { rules: { is_empty_body: true } }
11534
+ );
11449
11535
 
11450
11536
  /**
11451
11537
  * Registers CDATA sections.
@@ -11453,10 +11539,12 @@ XML.register("xml-stylesheet", ({ props }) => {
11453
11539
  * Self-closing: [cdata = "raw content" !] or [cdata = text: "raw content" !]
11454
11540
  */
11455
11541
  XML.register("cdata", ({ props, content, isSelfClosing }) => {
11456
- const text = isSelfClosing ? (props[0] ?? props.text ?? "") : content;
11457
- return `<![CDATA[${text}]]>`;
11542
+ const text = isSelfClosing ? (props[0] ?? props.text ?? "") : content;
11543
+ return `<![CDATA[${text}]]>`;
11458
11544
  });
11459
11545
 
11546
+ registerSharedOutputs(XML);
11547
+
11460
11548
  const csvEscape = (value) => {
11461
11549
  const str = String(value ?? "").trim();
11462
11550
  if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
@@ -11513,6 +11601,8 @@ CSV.register(["col", "cell", "td"], ({ textContent }) => {
11513
11601
  return csvEscape(textContent);
11514
11602
  }, { handleAst: true, trimAndWrapBlocks: false });
11515
11603
 
11604
+ registerSharedOutputs(CSV);
11605
+
11516
11606
  const isValidInt$1 = (v) => v !== "" && !isNaN(Number(v)) && !v.includes(".");
11517
11607
  const isValidFloat$1 = (v) => v !== "" && !isNaN(Number(v)) && v.includes(".");
11518
11608
  const isValidNumber$1 = (v) => v !== "" && !isNaN(Number(v));
@@ -11736,6 +11826,8 @@ TOML.register("array", async ({ props, ast, renderChild }) => {
11736
11826
  return `${tomlKey(key)} = [${vals.join(", ")}]\n`;
11737
11827
  }, { handleAst: true });
11738
11828
 
11829
+ registerSharedOutputs(TOML);
11830
+
11739
11831
  const isValidInt = (v) => v !== "" && !isNaN(Number(v)) && !v.includes(".");
11740
11832
  const isValidFloat = (v) => v !== "" && !isNaN(Number(v)) && v.includes(".");
11741
11833
  const isValidNumber = (v) => v !== "" && !isNaN(Number(v));
@@ -12051,6 +12143,8 @@ YAML.register("doc-start", () => "---\n", { rules: { is_empty_body: true } });
12051
12143
  */
12052
12144
  YAML.register("doc-end", () => "...\n", { rules: { is_empty_body: true } });
12053
12145
 
12146
+ registerSharedOutputs(YAML);
12147
+
12054
12148
  /**
12055
12149
  * The Text Mapper used for plain-text extraction.
12056
12150
  */
@@ -12327,34 +12421,48 @@ async function resolveModules(ast, context) {
12327
12421
  const baseDir = context.instance.baseDir || ((filename === "anonymous") ? absFilename : posix.dirname(absFilename));
12328
12422
 
12329
12423
  // 1. Helper: Trim AST to remove file-boundary whitespace and "ghost" newlines
12330
- const trimAst = (nodes) => {
12424
+ const trimAst = (nodes, trimBoundaries = true) => {
12331
12425
  if (!nodes) return [];
12332
12426
 
12333
- // 1. Filter out internal whitespace-only nodes that are adjacent to non-rendering nodes
12334
- // (Comments, Imports, etc. shouldn't leave "ghost" newlines)
12427
+ // 1. Filter out whitespace-only text nodes adjacent (directly or through other whitespace)
12428
+ // to non-rendering nodes (Comments, Imports, USE_MODULE).
12335
12429
  const nonRenderingTypes = [COMMENT, IMPORT, USE_MODULE];
12336
12430
  let res = nodes.filter((node, idx) => {
12337
12431
  if (node.type !== TEXT$1 || node.text.trim() !== "") return true;
12338
12432
 
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));
12433
+ // Walk backwards through consecutive whitespace nodes to find prev non-whitespace
12434
+ let prevIsNonRendering = false;
12435
+ for (let j = idx - 1; j >= 0; j--) {
12436
+ if (nodes[j].type === TEXT$1 && nodes[j].text.trim() === "") continue;
12437
+ prevIsNonRendering = nonRenderingTypes.includes(nodes[j].type);
12438
+ break;
12439
+ }
12440
+
12441
+ // Walk forwards through consecutive whitespace nodes to find next non-whitespace
12442
+ let nextIsNonRendering = false;
12443
+ for (let j = idx + 1; j < nodes.length; j++) {
12444
+ if (nodes[j].type === TEXT$1 && nodes[j].text.trim() === "") continue;
12445
+ nextIsNonRendering = nonRenderingTypes.includes(nodes[j].type);
12446
+ break;
12447
+ }
12344
12448
 
12345
- return !isAdjacentToNonRendering;
12449
+ return !(prevIsNonRendering || nextIsNonRendering);
12346
12450
  });
12347
12451
 
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*$/, "");
12452
+ if (trimBoundaries) {
12453
+ // 2. Final pass: trim leading/trailing newlines from the remaining boundary text nodes
12454
+ if (res.length > 0 && res[0].type === TEXT$1) {
12455
+ res[0].text = res[0].text.replace(/^[\r\n]+/, "");
12456
+ }
12457
+ if (res.length > 0 && res[res.length - 1].type === TEXT$1) {
12458
+ res[res.length - 1].text = res[res.length - 1].text.replace(/[\r\n]+\s*$/, "");
12459
+ }
12460
+
12461
+ // 3. Remove any nodes that became purely empty after trimming
12462
+ res = res.filter(node => node.type !== TEXT$1 || node.text !== "");
12354
12463
  }
12355
12464
 
12356
- // 3. Remove any nodes that became purely empty after trimming
12357
- return res.filter(node => node.type !== TEXT$1 || node.text !== "");
12465
+ return res;
12358
12466
  };
12359
12467
 
12360
12468
  // 2. Helper: Inject Slots with Indentation Propagation
@@ -12425,6 +12533,10 @@ async function resolveModules(ast, context) {
12425
12533
  // 1b. Resolve relative to current base (FS)
12426
12534
  const absolutePath = resolveModulePath(resolvedPath, currentBaseDir);
12427
12535
 
12536
+ if (!context.instance.fs) {
12537
+ 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.`]);
12538
+ }
12539
+
12428
12540
  // Local Path Resolution with Auto-Extension
12429
12541
  let localPath = absolutePath;
12430
12542
  if (!await context.instance.fs.exists(localPath) && !localPath.endsWith(".smark")) {
@@ -12576,7 +12688,6 @@ async function resolveModules(ast, context) {
12576
12688
  Object.entries(node.props).filter(([key]) => {
12577
12689
  if (key === "__consumed__") return false;
12578
12690
  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
12691
  return true;
12581
12692
  })
12582
12693
  );
@@ -12691,10 +12802,7 @@ const runValidations = (node, target, instance) => {
12691
12802
  const isStructural = node.type === "Block";
12692
12803
  if (isStructural && rules.required_args && Array.isArray(rules.required_args)) {
12693
12804
  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
- }
12805
+ if (typeof arg === "number") return node.props[arg] === undefined;
12698
12806
  return node.props[arg] === undefined;
12699
12807
  });
12700
12808
 
@@ -12709,6 +12817,22 @@ const runValidations = (node, target, instance) => {
12709
12817
  );
12710
12818
  }
12711
12819
  }
12820
+
12821
+ // -- Directives Validation (Required Directives) ----------------------- //
12822
+ if (isStructural && rules.required_directives && Array.isArray(rules.required_directives)) {
12823
+ const missingDirectives = rules.required_directives.filter(key => node.directives?.[key] === undefined);
12824
+
12825
+ if (missingDirectives.length > 0) {
12826
+ transpilerError(
12827
+ [
12828
+ "{N}",
12829
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is missing required directive props:$> <$red:${missingDirectives.map(k => `smark-${k}`).join(", ")}$>{N}`,
12830
+ `<$blue:Please ensure these directive props are provided in the template usage.$>`
12831
+ ],
12832
+ context
12833
+ );
12834
+ }
12835
+ }
12712
12836
  };
12713
12837
 
12714
12838
  /**
@@ -12880,6 +13004,14 @@ function setDefaultFs(fs) {
12880
13004
  Evaluator.setDefaultFs(fs);
12881
13005
  }
12882
13006
 
13007
+ function setDefaultEnv(env) {
13008
+ Evaluator.setDefaultEnv(env);
13009
+ }
13010
+
13011
+ function setDefaultAsyncLocalStorage(cls) {
13012
+ Evaluator.setDefaultAsyncLocalStorage(cls);
13013
+ }
13014
+
12883
13015
  function setDefaultResolvePath(fn) {
12884
13016
  defaultResolvePath = fn;
12885
13017
  }
@@ -12951,7 +13083,8 @@ class SomMark {
12951
13083
  allowFetch: security?.allowFetch !== false,
12952
13084
  allowHttp: security?.allowHttp === true,
12953
13085
  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
13086
+ allowedExtensions: Array.isArray(security?.allowedExtensions) ? security.allowedExtensions.map(e => e.toLowerCase()) : null,
13087
+ env: Array.isArray(security?.env) ? security.env : []
12955
13088
  };
12956
13089
  this.warnings = [];
12957
13090
  this._prepared = false;
@@ -13262,8 +13395,24 @@ async function findAndLoadConfig(targetPath) {
13262
13395
  return await defaultFindAndLoadConfig(targetPath);
13263
13396
  }
13264
13397
 
13398
+ class AsyncLocalStorage {
13399
+ #store = undefined;
13400
+ run(store, fn) {
13401
+ const prev = this.#store;
13402
+ this.#store = store;
13403
+ try { return fn(); }
13404
+ finally { this.#store = prev; }
13405
+ }
13406
+ getStore() { return this.#store; }
13407
+ exit(fn) { return fn(); }
13408
+ enterWith(store) { this.#store = store; }
13409
+ disable() {}
13410
+ }
13411
+
13265
13412
  setDefaultFs(null);
13266
13413
  setDefaultCwd("/");
13414
+ setDefaultEnv(null);
13415
+ setDefaultAsyncLocalStorage(AsyncLocalStorage);
13267
13416
 
13268
13417
  /**
13269
13418
  * Resolves a relative path into a full URL using the current document location.
@@ -13345,4 +13494,4 @@ function renderCompiledHTML(container, html) {
13345
13494
  }
13346
13495
  }
13347
13496
 
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 };
13497
+ 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 };