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.
@@ -1,5 +1,3 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
-
3
1
  let _lazyMatch = () => { var __lib__=(()=>{var m=Object.defineProperty,V=Object.getOwnPropertyDescriptor,G=Object.getOwnPropertyNames,T=Object.prototype.hasOwnProperty,q=(r,e)=>{for(var n in e)m(r,n,{get:e[n],enumerable:true});},H=(r,e,n,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of G(e))!T.call(r,t)&&t!==n&&m(r,t,{get:()=>e[t],enumerable:!(a=V(e,t))||a.enumerable});return r},J=r=>H(m({},"__esModule",{value:true}),r),w={};q(w,{default:()=>re});var A=r=>Array.isArray(r),d=r=>typeof r=="function",Q=r=>r.length===0,W=r=>typeof r=="number",K=r=>typeof r=="object"&&r!==null,X=r=>r instanceof RegExp,b=r=>typeof r=="string",h=r=>r===void 0,Y=r=>{const e=new Map;return n=>{const a=e.get(n);if(a)return a;const t=r(n);return e.set(n,t),t}},rr=(r,e,n={})=>{const a={cache:{},input:r,index:0,indexMax:0,options:n,output:[]};if(v(e)(a)&&a.index===r.length)return a.output;throw new Error(`Failed to parse at index ${a.indexMax}`)},i=(r,e)=>A(r)?er(r,e):b(r)?ar(r,e):nr(r,e),er=(r,e)=>{const n={};for(const a of r){if(a.length!==1)throw new Error(`Invalid character: "${a}"`);const t=a.charCodeAt(0);n[t]=true;}return a=>{const t=a.index,o=a.input;for(;a.index<o.length&&o.charCodeAt(a.index)in n;)a.index+=1;const u=a.index;if(u>t){if(!h(e)&&!a.options.silent){const s=a.input.slice(t,u),c=d(e)?e(s,o,String(t)):e;h(c)||a.output.push(c);}a.indexMax=Math.max(a.indexMax,a.index);}return true}},nr=(r,e)=>{const n=r.source,a=r.flags.replace(/y|$/,"y"),t=new RegExp(n,a);return g(o=>{t.lastIndex=o.index;const u=t.exec(o.input);if(u){if(!h(e)&&!o.options.silent){const s=d(e)?e(...u,o.input,String(o.index)):e;h(s)||o.output.push(s);}return o.index+=u[0].length,o.indexMax=Math.max(o.indexMax,o.index),true}else return false})},ar=(r,e)=>n=>{if(n.input.startsWith(r,n.index)){if(!h(e)&&!n.options.silent){const t=d(e)?e(r,n.input,String(n.index)):e;h(t)||n.output.push(t);}return n.index+=r.length,n.indexMax=Math.max(n.indexMax,n.index),true}else return false},C=(r,e,n,a)=>{const t=v(r);return g(_(M(o=>{let u=0;for(;u<n;){const s=o.index;if(!t(o)||(u+=1,o.index===s))break}return u>=e})))},tr=(r,e)=>C(r,0,1),f=(r,e)=>C(r,0,1/0),x=(r,e)=>{const n=r.map(v);return g(_(M(a=>{for(let t=0,o=n.length;t<o;t++)if(!n[t](a))return false;return true})))},l=(r,e)=>{const n=r.map(v);return g(_(a=>{for(let t=0,o=n.length;t<o;t++)if(n[t](a))return true;return false}))},M=(r,e=false)=>{const n=v(r);return a=>{const t=a.index,o=a.output.length,u=n(a);return (!u||e)&&(a.index=t,a.output.length!==o&&(a.output.length=o)),u}},_=(r,e)=>{const n=v(r);return n},g=(()=>{let r=0;return e=>{const n=v(e),a=r+=1;return t=>{var o;if(t.options.memoization===false)return n(t);const u=t.index,s=(o=t.cache)[a]||(o[a]=new Map),c=s.get(u);if(c===false)return false;if(W(c))return t.index=c,true;if(c)return t.index=c.index,c.output?.length&&t.output.push(...c.output),true;{const Z=t.output.length;if(n(t)){const D=t.index,U=t.output.length;if(U>Z){const ee=t.output.slice(Z,U);s.set(u,{index:D,output:ee});}else s.set(u,D);return true}else return s.set(u,false),false}}}})(),E=r=>{let e;return n=>(e||(e=v(r())),e(n))},v=Y(r=>{if(d(r))return Q(r)?E(r):r;if(b(r)||X(r))return i(r);if(A(r))return x(r);if(K(r))return l(Object.values(r));throw new Error("Invalid rule")}),P="abcdefghijklmnopqrstuvwxyz",ir=r=>{let e="";for(;r>0;){const n=(r-1)%26;e=P[n]+e,r=Math.floor((r-1)/26);}return e},O=r=>{let e=0;for(let n=0,a=r.length;n<a;n++)e=e*26+P.indexOf(r[n])+1;return e},S=(r,e)=>{if(e<r)return S(e,r);const n=[];for(;r<=e;)n.push(r++);return n},or=(r,e,n)=>S(r,e).map(a=>String(a).padStart(n,"0")),R=(r,e)=>S(O(r),O(e)).map(ir),p=r=>r,z=r=>ur(e=>rr(e,r,{memoization:false}).join("")),ur=r=>{const e={};return n=>e[n]??(e[n]=r(n))},sr=i(/^\*\*\/\*$/,".*"),cr=i(/^\*\*\/(\*)?([ a-zA-Z0-9._-]+)$/,(r,e,n)=>`.*${e?"":"(?:^|/)"}${n.replaceAll(".","\\.")}`),lr=i(/^\*\*\/(\*)?([ a-zA-Z0-9._-]*)\{([ a-zA-Z0-9._-]+(?:,[ a-zA-Z0-9._-]+)*)\}$/,(r,e,n,a)=>`.*${e?"":"(?:^|/)"}${n.replaceAll(".","\\.")}(?:${a.replaceAll(",","|").replaceAll(".","\\.")})`),y=i(/\\./,p),pr=i(/[$.*+?^(){}[\]\|]/,r=>`\\${r}`),vr=i(/./,p),hr=i(/^(?:!!)*!(.*)$/,(r,e)=>`(?!^${L(e)}$).*?`),dr=i(/^(!!)+/,""),fr=l([hr,dr]),xr=i(/\/(\*\*\/)+/,"(?:/.+/|/)"),gr=i(/^(\*\*\/)+/,"(?:^|.*/)"),mr=i(/\/(\*\*)$/,"(?:/.*|$)"),_r=i(/\*\*/,".*"),j=l([xr,gr,mr,_r]),Sr=i(/\*\/(?!\*\*\/)/,"[^/]*/"),yr=i(/\*/,"[^/]*"),N=l([Sr,yr]),k=i("?","[^/]"),$r=i("[",p),wr=i("]",p),Ar=i(/[!^]/,"^/"),br=i(/[a-z]-[a-z]|[0-9]-[0-9]/i,p),Cr=i(/[$.*+?^(){}[\|]/,r=>`\\${r}`),Mr=i(/[^\]]/,p),Er=l([y,Cr,br,Mr]),B=x([$r,tr(Ar),f(Er),wr]),Pr=i("{","(?:"),Or=i("}",")"),Rr=i(/(\d+)\.\.(\d+)/,(r,e,n)=>or(+e,+n,Math.min(e.length,n.length)).join("|")),zr=i(/([a-z]+)\.\.([a-z]+)/,(r,e,n)=>R(e,n).join("|")),jr=i(/([A-Z]+)\.\.([A-Z]+)/,(r,e,n)=>R(e.toLowerCase(),n.toLowerCase()).join("|").toUpperCase()),Nr=l([Rr,zr,jr]),I=x([Pr,Nr,Or]),kr=i("{","(?:"),Br=i("}",")"),Ir=i(",","|"),Fr=i(/[$.*+?^(){[\]\|]/,r=>`\\${r}`),Lr=i(/[^}]/,p),Zr=E(()=>F),Dr=l([j,N,k,B,I,Zr,y,Fr,Ir,Lr]),F=x([kr,f(Dr),Br]),Ur=f(l([sr,cr,lr,fr,j,N,k,B,I,F,y,pr,vr])),Vr=Ur,Gr=z(Vr),L=Gr,Tr=i(/\\./,p),qr=i(/./,p),Hr=i(/\*\*\*+/,"*"),Jr=i(/([^/{[(!])\*\*/,(r,e)=>`${e}*`),Qr=i(/(^|.)\*\*(?=[^*/)\]}])/,(r,e)=>`${e}*`),Wr=f(l([Tr,Hr,Jr,Qr,qr])),Kr=Wr,Xr=z(Kr),Yr=Xr,$=(r,e)=>{const n=Array.isArray(r)?r:[r];if(!n.length)return false;const a=n.map($.compile),t=n.every(s=>/(\/(?:\*\*)?|\[\/\])$/.test(s)),o=e.replace(/[\\\/]+/g,"/").replace(/\/$/,t?"/":"");return a.some(s=>s.test(o))};$.compile=r=>new RegExp(`^${L(Yr(r))}$`,"s");var re=$;return J(w)})();
4
2
  return __lib__.default || __lib__; };
5
3
  let _match;
@@ -1476,6 +1474,7 @@ function makeBlockNode() {
1476
1474
  structure: "Block",
1477
1475
  id: "",
1478
1476
  props: {},
1477
+ directives: {},
1479
1478
  body: [],
1480
1479
  depth: 0,
1481
1480
  range: {
@@ -1961,9 +1960,11 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
1961
1960
  i = valueIndex;
1962
1961
 
1963
1962
  // Store Argument
1964
- blockNode.props[String(argIndex++)] = v;
1965
- if (k) {
1966
- 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;
1967
1968
  }
1968
1969
  k = "";
1969
1970
  v = "";
@@ -8627,7 +8628,7 @@ function registerHostSettings(settings) {
8627
8628
  hostSettings = settings || {};
8628
8629
  }
8629
8630
 
8630
- const version = "5.0.5";
8631
+ const version = "5.1.0";
8631
8632
 
8632
8633
  const SomMark$1 = {
8633
8634
  version,
@@ -8678,14 +8679,19 @@ const SomMark$1 = {
8678
8679
  // Freeze the entire Standard Library to make it completely immutable and tamper-proof
8679
8680
  Object.freeze(SomMark$1);
8680
8681
 
8681
- // Each transpile() call gets its own isolated EvaluatorState stack via async context.
8682
- const evaluatorStorage = new AsyncLocalStorage();
8682
+ // Set by index.js (Node.js) or index.browser.js (shim) never imported directly.
8683
+ let evaluatorStorage = null;
8684
+
8685
+ function setDefaultAsyncLocalStorage$1(cls) {
8686
+ evaluatorStorage = cls ? new cls() : null;
8687
+ }
8683
8688
 
8684
8689
  /**
8685
8690
  * Runs fn inside an isolated evaluator context.
8686
8691
  * Concurrent transpile() calls each get their own stack — no cross-contamination.
8687
8692
  */
8688
8693
  function withEvaluator(fn) {
8694
+ if (!evaluatorStorage) return fn();
8689
8695
  return evaluatorStorage.run([], fn);
8690
8696
  }
8691
8697
 
@@ -8843,6 +8849,7 @@ const customCompileAdapter = async (src, options, parentSecurity = {}) => {
8843
8849
  registerHostCompile(customCompileAdapter);
8844
8850
 
8845
8851
  let defaultFs$1 = null;
8852
+ let defaultEnv = null;
8846
8853
  let quickJSInstance = null;
8847
8854
  async function getQuickJSModule() {
8848
8855
  if (!quickJSInstance) {
@@ -8866,6 +8873,16 @@ function objectToHandle(context, obj) {
8866
8873
  return result.unwrap();
8867
8874
  }
8868
8875
 
8876
+ function isPlainData(value, seen = new Set()) {
8877
+ if (value === null || value === undefined) return true;
8878
+ if (typeof value === "function") return false;
8879
+ if (typeof value !== "object") return true;
8880
+ if (seen.has(value)) return false;
8881
+ seen.add(value);
8882
+ if (Array.isArray(value)) return value.every(v => isPlainData(v, seen));
8883
+ return Object.values(value).every(v => isPlainData(v, seen));
8884
+ }
8885
+
8869
8886
  function expose(context, vars, pendingDeferreds) {
8870
8887
  for (const [key, value] of Object.entries(vars)) {
8871
8888
  let handle;
@@ -8986,6 +9003,18 @@ class EvaluatorState {
8986
9003
  });
8987
9004
 
8988
9005
  this.expose({
9006
+ __hostEnv: (key) => {
9007
+ if (defaultEnv === null) {
9008
+ throw new Error(
9009
+ "[SomMark] SomMark.env() is not available in browser mode.\n" +
9010
+ "Environment variables are a server-side concept.\n" +
9011
+ "Read env values at build time and pass them as placeholders instead."
9012
+ );
9013
+ }
9014
+ const allowlist = this.security?.env;
9015
+ if (!Array.isArray(allowlist) || !allowlist.includes(key)) return undefined;
9016
+ return defaultEnv[key] ?? undefined;
9017
+ },
8989
9018
  __hostSomMarkVersion: SomMark$1.version,
8990
9019
  __hostSomMarkSettings: () => {
8991
9020
  const clean = { ...SomMark$1.settings };
@@ -9197,6 +9226,12 @@ class EvaluatorState {
9197
9226
  throw new Error("SomMark.static Error: Argument must be a string.");
9198
9227
  }
9199
9228
  return globalThis.eval(expr);
9229
+ },
9230
+ env: (key) => {
9231
+ if (typeof key !== "string" || !key) {
9232
+ throw new Error("SomMark.env Error: Key must be a non-empty string.");
9233
+ }
9234
+ return __hostEnv(key);
9200
9235
  }
9201
9236
  };
9202
9237
 
@@ -9219,15 +9254,20 @@ class EvaluatorState {
9219
9254
  }
9220
9255
  setupRes.value.dispose();
9221
9256
 
9222
- // Configure module loader using virtual FS implementation
9257
+ // Configure module loader using virtual FS implementation.
9258
+ // The normalizer resolves every import to an absolute path so the module
9259
+ // cache key is always absolute — <smark> (the eval module name) can never
9260
+ // be reached by any user import regardless of what the file is named.
9223
9261
  this.runtime.setModuleLoader((moduleName) => {
9224
9262
  try {
9225
9263
  const isRaw = moduleName.endsWith("?raw");
9226
9264
  const cleanModuleName = isRaw ? moduleName.slice(0, -4) : moduleName;
9227
- const resolvedPath = /^https?:\/\//.test(this.baseDir)
9228
- ? new URL(cleanModuleName, this.baseDir.endsWith("/") ? this.baseDir : this.baseDir + "/").href
9265
+ // moduleName is already an absolute path (supplied by the normalizer below),
9266
+ // so resolve() is a no-op for absolute paths and a safe fallback for URLs.
9267
+ const resolvedPath = /^https?:\/\//.test(cleanModuleName)
9268
+ ? cleanModuleName
9229
9269
  : posix.resolve(this.baseDir, cleanModuleName);
9230
-
9270
+
9231
9271
  const fsImpl = this.settings?.fs || this.settings?.instance?.fs || this.nodeFs;
9232
9272
  if (!fsImpl) {
9233
9273
  throw new Error("No filesystem implementation available.");
@@ -9260,6 +9300,22 @@ class EvaluatorState {
9260
9300
  } catch (err) {
9261
9301
  throw err;
9262
9302
  }
9303
+ }, (baseName, moduleName) => {
9304
+ // Resolve every import to an absolute path so no user import can ever
9305
+ // normalize to <smark> (or any other virtual eval module name).
9306
+ const isRaw = moduleName.endsWith("?raw");
9307
+ const clean = isRaw ? moduleName.slice(0, -4) : moduleName;
9308
+ if (/^https?:\/\//.test(clean)) return moduleName;
9309
+ const baseDir = (baseName === "<smark>" || !posix.isAbsolute(baseName))
9310
+ ? this.baseDir
9311
+ : (/^https?:\/\//.test(baseName) ? baseName : posix.dirname(baseName));
9312
+ let resolved;
9313
+ if (/^https?:\/\//.test(baseDir)) {
9314
+ resolved = new URL(clean, baseDir).href;
9315
+ } else {
9316
+ resolved = posix.resolve(baseDir, clean);
9317
+ }
9318
+ return isRaw ? resolved + "?raw" : resolved;
9263
9319
  });
9264
9320
  }
9265
9321
 
@@ -9410,9 +9466,17 @@ class EvaluatorState {
9410
9466
 
9411
9467
  inject(vars) {
9412
9468
  if (!this.context) return;
9469
+ const safe = {};
9470
+ for (const [key, value] of Object.entries(vars)) {
9471
+ if (!isPlainData(value)) {
9472
+ console.warn(`[SomMark] Security: "${key}" contains functions and was blocked. Only plain data can be injected. Use SomMark built-ins for host capabilities.`);
9473
+ continue;
9474
+ }
9475
+ safe[key] = value;
9476
+ }
9413
9477
  const currentScope = this.scopes[this.scopes.length - 1];
9414
- Object.assign(currentScope, vars);
9415
- this.expose(vars);
9478
+ Object.assign(currentScope, safe);
9479
+ this.expose(safe);
9416
9480
  }
9417
9481
 
9418
9482
  async execute(code, baseDir = null) {
@@ -9496,7 +9560,7 @@ class EvaluatorState {
9496
9560
 
9497
9561
  let result;
9498
9562
  if (isModule) {
9499
- const evalRes = this.context.evalCode(finalCode, "main.js", { type: 'module' });
9563
+ const evalRes = this.context.evalCode(finalCode, "<smark>", { type: 'module' });
9500
9564
  if (evalRes.error) {
9501
9565
  const err = this.context.dump(evalRes.error);
9502
9566
  evalRes.error.dispose();
@@ -9565,7 +9629,7 @@ class EvaluatorState {
9565
9629
  }
9566
9630
 
9567
9631
  const defaultValue = this.context.dump(resolvedDefaultHandle);
9568
-
9632
+
9569
9633
  if (isPromise) {
9570
9634
  resolvedDefaultHandle.dispose();
9571
9635
  }
@@ -9605,7 +9669,7 @@ class EvaluatorState {
9605
9669
  result = res;
9606
9670
  }
9607
9671
  } else {
9608
- const evalRes = this.context.evalCode(code, "main.js");
9672
+ const evalRes = this.context.evalCode(code, "<smark>");
9609
9673
  if (evalRes.error) {
9610
9674
  const err = this.context.dump(evalRes.error);
9611
9675
  evalRes.error.dispose();
@@ -9641,7 +9705,7 @@ class EvaluatorState {
9641
9705
  return result;
9642
9706
  } catch (error) {
9643
9707
  const stack = error.stack || "";
9644
- const match = stack.match(/main\.js:(\d+):(\d+)/) || stack.match(/:(\d+):(\d+)/);
9708
+ const match = stack.match(/__smark__\.js:(\d+):(\d+)/) || stack.match(/:(\d+):(\d+)/);
9645
9709
 
9646
9710
  const err = new Error(error.message || error);
9647
9711
  if (match) {
@@ -9704,6 +9768,14 @@ class Evaluator {
9704
9768
  defaultFs$1 = fs;
9705
9769
  }
9706
9770
 
9771
+ setDefaultEnv(env) {
9772
+ defaultEnv = env;
9773
+ }
9774
+
9775
+ setDefaultAsyncLocalStorage(cls) {
9776
+ setDefaultAsyncLocalStorage$1(cls);
9777
+ }
9778
+
9707
9779
  get active() {
9708
9780
  const stack = this._getStack();
9709
9781
  if (stack.length === 0) {
@@ -9916,8 +9988,27 @@ async function preprocessRuntimeLogic(code, filename = null, security = {}, inst
9916
9988
  if (filename && filename !== "anonymous") {
9917
9989
  baseDir = posix.dirname(posix.resolve(filename));
9918
9990
  }
9991
+
9992
+ // Block absolute paths — path.resolve would ignore baseDir entirely
9993
+ if (posix.isAbsolute(argValue)) {
9994
+ transpilerError([
9995
+ `<$red:Security Error:$> Absolute import paths are not allowed: <$magenta:${argValue}$>{line}`,
9996
+ `<$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}`,
9997
+ `<$yellow:Base directory:$> <$blue:${baseDir}$>{line}`
9998
+ ]);
9999
+ }
10000
+
9919
10001
  const resolvedPath = posix.resolve(baseDir, argValue);
9920
10002
 
10003
+ // Block path traversal — resolved path must stay inside baseDir
10004
+ const safeBases = baseDir.endsWith(posix.sep) ? baseDir : baseDir + posix.sep;
10005
+ if (!resolvedPath.startsWith(safeBases) && resolvedPath !== baseDir) {
10006
+ transpilerError([
10007
+ `<$red:Security Error:$> Import path escapes the project directory: <$magenta:${argValue}$>{line}`,
10008
+ `<$yellow:Resolved Path:$> <$blue:${resolvedPath}$>{line}`
10009
+ ]);
10010
+ }
10011
+
9921
10012
  const fsImpl = instance?.fs || await getNodeFs();
9922
10013
 
9923
10014
  // File presence validation
@@ -10030,6 +10121,7 @@ const randomBytesHex = (size) => {
10030
10121
 
10031
10122
  const BODY_PLACEHOLDER = `SOMMARKBODYPLACEHOLDER${randomBytesHex(8)}SOMMARK`;
10032
10123
 
10124
+
10033
10125
  /**
10034
10126
  * Extracts all plain text from a node and its children.
10035
10127
  *
@@ -10128,6 +10220,17 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
10128
10220
 
10129
10221
  if (node.type === FOR_EACH) {
10130
10222
  const transpiledArgs = await transpileArgs(node.props);
10223
+
10224
+ if (!node.props || (node.props[0] === undefined && node.props["items"] === undefined)) {
10225
+ const line = node.range?.start?.line + 1 || 1;
10226
+ transpilerError([
10227
+ `<$red:Missing Prop Error in [for-each]:$>{line}`,
10228
+ `[for-each] requires an array as its first prop, e.g. [for-each = \${ array }\$]{line}`,
10229
+ `at line <$yellow:${line}$>{line}`
10230
+ ]);
10231
+ return "";
10232
+ }
10233
+
10131
10234
  const items = mapper_file ? mapper_file.safeArg({ props: transpiledArgs, index: 0, key: "items", fallBack: [] }) : [];
10132
10235
 
10133
10236
  if (!Array.isArray(items)) {
@@ -10141,11 +10244,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
10141
10244
  }
10142
10245
 
10143
10246
  const asVar = transpiledArgs.as || "value";
10144
- if (asVar === "i") {
10247
+ if (asVar === "i" || asVar === "length") {
10145
10248
  const line = node.range?.start?.line + 1 || 1;
10146
10249
  transpilerError([
10147
10250
  `<$red:Reserved Variable Error in [for-each]:$>{line}`,
10148
- `'i' is a reserved variable name for the loop index.{N}Use a different name for the 'as' prop, e.g. as: "item"{line}`,
10251
+ `'${asVar}' is a reserved variable name.{N}Use a different name for the 'as' prop, e.g. as: "item"{line}`,
10149
10252
  `at line <$yellow:${line}$>{line}`
10150
10253
  ]);
10151
10254
  return "";
@@ -10175,22 +10278,28 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
10175
10278
  }
10176
10279
  }
10177
10280
 
10178
- let output = "";
10281
+ const rawJoin = transpiledArgs.join ?? null;
10282
+ const join = rawJoin !== null ? rawJoin.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\r/g, "\r") : null;
10283
+ const parts = [];
10179
10284
  let idx = 0;
10285
+ const length = items.length;
10180
10286
  for (const item of items) {
10181
10287
  Evaluator$1.pushScope();
10182
10288
  Evaluator$1.inject({
10183
10289
  [asVar]: item,
10184
- i: idx++
10290
+ i: idx++,
10291
+ length
10185
10292
  });
10186
10293
 
10294
+ let iterOutput = "";
10187
10295
  for (let j = 0; j < cleanedBody.length; j++) {
10188
- output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState, extraCtx);
10296
+ iterOutput += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState, extraCtx);
10189
10297
  }
10190
10298
 
10191
10299
  await Evaluator$1.popScope();
10300
+ parts.push(iterOutput);
10192
10301
  }
10193
- return output;
10302
+ return join !== null ? parts.join(join) : parts.join("");
10194
10303
  }
10195
10304
 
10196
10305
  let secretId = null;
@@ -10218,13 +10327,12 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
10218
10327
  }
10219
10328
 
10220
10329
  // smark-raw block — body collected verbatim by lexer, bypasses normal body processing pipeline
10221
- if (node.type === BLOCK && (node.props?.["smark-raw"] === "true" || node.props?.["smark-raw"] === true)) {
10330
+ if (node.type === BLOCK && (node.directives?.raw === "true" || node.directives?.raw === true)) {
10222
10331
  if (generateRuntimeOutput) return "";
10223
10332
  const rawContent = node.body?.map(n => String(n.text || "")).join("") || "";
10224
- const { "smark-raw": _, ...cleanArgs } = node.props;
10225
- const transpiledArgs = await transpileArgs(cleanArgs);
10333
+ const transpiledArgs = await transpileArgs(node.props);
10226
10334
  if (Evaluator$1.active?.hasDynamicTag?.(node.id)) {
10227
- return await Evaluator$1.active.executeDynamicTag(node.id, { props: transpiledArgs, content: rawContent, textContent: rawContent });
10335
+ return await Evaluator$1.active.executeDynamicTag(node.id, { props: transpiledArgs, directives: node.directives, content: rawContent, textContent: rawContent });
10228
10336
  }
10229
10337
  let rawTarget = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
10230
10338
  if (!rawTarget && mapper_file) rawTarget = mapper_file.getUnknownTag(node);
@@ -10232,6 +10340,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
10232
10340
  const isManualMode = !!rawTarget.options?.handleAst;
10233
10341
  return await rawTarget.render.call(mapper_file, {
10234
10342
  props: transpiledArgs,
10343
+ directives: node.directives,
10235
10344
  content: rawContent,
10236
10345
  textContent: rawContent,
10237
10346
  ast: isManualMode ? node : undefined,
@@ -10343,6 +10452,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
10343
10452
 
10344
10453
  return await target.render.call(mapper_file, {
10345
10454
  props: transpiledArgs,
10455
+ directives: node.directives,
10346
10456
  content: "",
10347
10457
  textContent: richText || textContent,
10348
10458
  ast: cleanAst,
@@ -10361,6 +10471,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
10361
10471
  }
10362
10472
  result += await target.render.call(mapper_file, {
10363
10473
  props: transpiledArgs,
10474
+ directives: node.directives,
10364
10475
  content,
10365
10476
  textContent,
10366
10477
  ast: new Proxy({}, {
@@ -11784,11 +11895,20 @@ class Mapper {
11784
11895
 
11785
11896
  /**
11786
11897
  * Registers universal utility blocks shared across all SomMark mappers.
11787
- * These blocks are considered "Format Agnostic."
11788
11898
  *
11789
11899
  * @param {Mapper} mapper - The mapper instance to register tags on.
11790
11900
  */
11791
11901
  function registerSharedOutputs(mapper) {
11902
+ mapper.register(
11903
+ ["raw", "Raw"],
11904
+ ({ content }) => {
11905
+ return content;
11906
+ },
11907
+ {
11908
+ escape: false, rules: {
11909
+ required_directives: ["raw"]
11910
+ } }
11911
+ );
11792
11912
  }
11793
11913
 
11794
11914
  const SVG_ELEMENTS = new Set([
@@ -11941,6 +12061,7 @@ HTML.register(
11941
12061
  return "";
11942
12062
  },
11943
12063
  );
12064
+ registerSharedOutputs(HTML);
11944
12065
 
11945
12066
  /**
11946
12067
  * The Markdown Mapper used for generating Markdown text.
@@ -11967,42 +12088,37 @@ const MARKDOWN = Mapper.define({
11967
12088
  },
11968
12089
 
11969
12090
  /**
11970
- * Provides a fallback for unknown tags by using the HTML mapper instead.
11971
- */
12091
+ * Provides a fallback for unknown tags by rendering them as HTML elements.
12092
+ * Passes child nodes to the transpiler, which handles all node types (such as ForEach).
12093
+ **/
11972
12094
  getUnknownTag(node) {
11973
- const id = node.id.toLowerCase();
11974
-
12095
+ const id = node.id;
11975
12096
  return {
11976
- render: async ({ props, ast, isSelfClosing, renderChild }) => {
12097
+ options: { trimAndWrapBlocks: true },
12098
+ render: ({ props, content, isSelfClosing }) => {
11977
12099
  const element = this.tag(id).smartAttributes(props, this.customProps, this.options);
11978
12100
  if (isSelfClosing || VOID_ELEMENTS.has(id)) return element.selfClose();
11979
-
11980
- let rawContent = "";
11981
- for (const child of (ast.body || [])) {
11982
- if (child.type === TEXT$1) rawContent += this.text(child.text);
11983
- else if (child.type === BLOCK) rawContent += await renderChild(child);
11984
- }
11985
- rawContent = rawContent.trim();
11986
-
11987
- const meaningful = (ast.body || []).filter(c => c.type !== TEXT$1 || c.text.trim());
11988
- const finalContent = meaningful.length <= 1 ? rawContent : `\n${rawContent}\n`;
11989
- return element.body(finalContent);
11990
- },
11991
- options: { handleAst: true }
12101
+ return element.body(content);
12102
+ }
11992
12103
  };
11993
12104
  }
11994
12105
  });
11995
12106
 
11996
12107
  MARKDOWN.inherit(HTML);
11997
12108
  const { md, safeArg } = MARKDOWN;
12109
+ registerSharedOutputs(MARKDOWN);
11998
12110
 
11999
12111
  /**
12000
12112
  * Quote - Renders blockquote content or GFM alerts.
12001
12113
  */
12002
- MARKDOWN.register("quote", ({ props, content }) => {
12003
- const type = safeArg({ props, index: 0, key: "type", fallBack: "" });
12004
- return md.quote(content, type);
12005
- }, { resolve: true });
12114
+ MARKDOWN.register(
12115
+ "quote",
12116
+ ({ props, content }) => {
12117
+ const type = safeArg({ props, index: 0, key: "type", fallBack: "" });
12118
+ return md.quote(content, type);
12119
+ },
12120
+ { resolve: true }
12121
+ );
12006
12122
 
12007
12123
  /**
12008
12124
  * Headings - Renders H1-H6 block headings.
@@ -12082,12 +12198,12 @@ MARKDOWN.register(
12082
12198
  "link",
12083
12199
  ({ props, content, isSelfClosing }) => {
12084
12200
  if (isSelfClosing) {
12085
- const text = safeArg({ props, index: 0, key: "text", fallBack: "" });
12086
- const src = safeArg({ props, index: 1, key: "src", fallBack: "" });
12201
+ const text = safeArg({ props, index: 0, key: "text", fallBack: "" });
12202
+ const src = safeArg({ props, index: 1, key: "src", fallBack: "" });
12087
12203
  const title = safeArg({ props, index: 2, key: "title", fallBack: "" });
12088
12204
  return md.url("link", text, src, title);
12089
12205
  }
12090
- const src = safeArg({ props, index: 0, key: "src", fallBack: "" });
12206
+ const src = safeArg({ props, index: 0, key: "src", fallBack: "" });
12091
12207
  const title = safeArg({ props, index: 1, key: "title", fallBack: "" });
12092
12208
  return md.url("link", content, src, title);
12093
12209
  },
@@ -12125,10 +12241,14 @@ MARKDOWN.register(
12125
12241
  * Escape - Escapes special Markdown characters.
12126
12242
  * Self-closing: [escape = "text" !] or [escape = text: "text" !]
12127
12243
  */
12128
- MARKDOWN.register(["escape", "e"], function ({ props, content, isSelfClosing }) {
12129
- const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
12130
- return this.md.escape(text);
12131
- }, { resolve: true });
12244
+ MARKDOWN.register(
12245
+ ["escape", "e"],
12246
+ function ({ props, content, isSelfClosing }) {
12247
+ const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
12248
+ return this.md.escape(text);
12249
+ },
12250
+ { resolve: true }
12251
+ );
12132
12252
 
12133
12253
  const ROW_SEP = "\x1E";
12134
12254
  const CELL_SEP = "\x1F";
@@ -12144,12 +12264,16 @@ MARKDOWN.register(
12144
12264
  const headers = [];
12145
12265
  const rows = [];
12146
12266
 
12147
- const extractRows = async (sectionNode) => {
12267
+ const extractRows = async sectionNode => {
12148
12268
  const sectionRows = [];
12149
- for (const child of (sectionNode.body || [])) {
12269
+ for (const child of sectionNode.body || []) {
12150
12270
  if (child.type === BLOCK && child.id?.toLowerCase() === "row") {
12151
12271
  const rendered = await renderChild(child, { inTable: true });
12152
- const cells = rendered.split(ROW_SEP)[0]?.split(CELL_SEP).filter(c => c !== "") ?? [];
12272
+ const cells =
12273
+ rendered
12274
+ .split(ROW_SEP)[0]
12275
+ ?.split(CELL_SEP)
12276
+ .filter(c => c !== "") ?? [];
12153
12277
  if (cells.length > 0) sectionRows.push(cells);
12154
12278
  } else if (child.type === FOR_EACH) {
12155
12279
  const rendered = await renderChild(child, { inTable: true });
@@ -12183,25 +12307,29 @@ MARKDOWN.register(
12183
12307
  */
12184
12308
  MARKDOWN.register(["header", "body"], ({ content }) => content);
12185
12309
 
12186
- MARKDOWN.register("row", async function ({ ast, renderChild, inTable }) {
12187
- if (!inTable) {
12188
- let result = "";
12189
- for (const child of ast.body) {
12190
- if (child.type === TEXT$1) result += this.text(child.text);
12191
- else if (child.type === BLOCK) result += await renderChild(child);
12310
+ MARKDOWN.register(
12311
+ "row",
12312
+ async function ({ ast, renderChild, inTable }) {
12313
+ if (!inTable) {
12314
+ let result = "";
12315
+ for (const child of ast.body) {
12316
+ if (child.type === TEXT$1) result += this.text(child.text);
12317
+ else if (child.type === BLOCK) result += await renderChild(child);
12318
+ }
12319
+ return result;
12192
12320
  }
12193
- return result;
12194
- }
12195
- let cells = "";
12196
- for (const child of ast.body) {
12197
- if (child.type !== BLOCK) continue;
12198
- const id = child.id?.toLowerCase();
12199
- if (id === "cell" || id === "th" || id === "td") {
12200
- cells += await renderChild(child, { inTable: true });
12321
+ let cells = "";
12322
+ for (const child of ast.body) {
12323
+ if (child.type !== BLOCK) continue;
12324
+ const id = child.id?.toLowerCase();
12325
+ if (id === "cell" || id === "th" || id === "td") {
12326
+ cells += await renderChild(child, { inTable: true });
12327
+ }
12201
12328
  }
12202
- }
12203
- return cells + ROW_SEP;
12204
- }, { handleAst: true });
12329
+ return cells + ROW_SEP;
12330
+ },
12331
+ { handleAst: true }
12332
+ );
12205
12333
 
12206
12334
  MARKDOWN.register(["cell", "th", "td"], ({ content, inTable }) => {
12207
12335
  return inTable ? content.trim() + CELL_SEP : content;
@@ -12211,34 +12339,42 @@ MARKDOWN.register(["cell", "th", "td"], ({ content, inTable }) => {
12211
12339
  * Lists - Authoritative Native AST List resolution.
12212
12340
  * Supports Ordered (Number) and Unordered (Dotlex) lists with deep nesting.
12213
12341
  */
12214
- MARKDOWN.register(["list", "List"], async function ({ ast, props, renderChild }) {
12215
- const indicator = safeArg({ props, index: 0, fallBack: "dot" });
12216
- const isOrdered = indicator === "number" || indicator === "ol";
12217
- const marker = isOrdered ? "" : (indicator === "dot" ? "-" : indicator);
12218
- const items = [];
12342
+ MARKDOWN.register(
12343
+ ["list", "List"],
12344
+ async function ({ ast, props, renderChild }) {
12345
+ const indicator = safeArg({ props, index: 0, fallBack: "dot" });
12346
+ const isOrdered = indicator === "number" || indicator === "ol";
12347
+ const marker = isOrdered ? "" : indicator === "dot" ? "-" : indicator;
12348
+ const items = [];
12219
12349
 
12220
- for (const node of ast.body) {
12221
- if (node.type !== BLOCK) continue;
12222
- const id = node.id?.toLowerCase();
12223
- if (id === "item") {
12224
- items.push((await renderChild(node)).trim());
12350
+ for (const node of ast.body) {
12351
+ if (node.type !== BLOCK) continue;
12352
+ const id = node.id?.toLowerCase();
12353
+ if (id === "item") {
12354
+ items.push((await renderChild(node)).trim());
12355
+ }
12225
12356
  }
12226
- }
12227
12357
 
12228
- return isOrdered ? md.orderedList(items, 0) : md.unorderedList(items, 0, marker);
12229
- }, { handleAst: true, trimAndWrapBlocks: false });
12358
+ return isOrdered ? md.orderedList(items, 0) : md.unorderedList(items, 0, marker);
12359
+ },
12360
+ { handleAst: true, trimAndWrapBlocks: false }
12361
+ );
12230
12362
 
12231
12363
  /**
12232
12364
  * List Helpers - Internal tags for list structural organization.
12233
12365
  */
12234
- MARKDOWN.register(["item", "Item"], async function ({ ast, renderChild }) {
12235
- let result = "";
12236
- for (const child of ast.body) {
12237
- if (child.type === TEXT$1) result += this.text(child.text);
12238
- else if (child.type === BLOCK) result += await renderChild(child);
12239
- }
12240
- return result.trim();
12241
- }, { handleAst: true, trimAndWrapBlocks: false });
12366
+ MARKDOWN.register(
12367
+ ["item", "Item"],
12368
+ async function ({ ast, renderChild }) {
12369
+ let result = "";
12370
+ for (const child of ast.body) {
12371
+ if (child.type === TEXT$1) result += this.text(child.text);
12372
+ else if (child.type === BLOCK) result += await renderChild(child);
12373
+ }
12374
+ return result.trim();
12375
+ },
12376
+ { handleAst: true, trimAndWrapBlocks: false }
12377
+ );
12242
12378
 
12243
12379
  /**
12244
12380
  * Todo - Renders task list items with status markers.
@@ -12248,19 +12384,23 @@ MARKDOWN.register(["item", "Item"], async function ({ ast, renderChild }) {
12248
12384
  * [todo = "Add feature", "x" !] positional self-closing (task, status)
12249
12385
  * [todo = "x"]Add feature[end] status in prop, task in body
12250
12386
  */
12251
- MARKDOWN.register("todo", ({ props, content, isSelfClosing }) => {
12252
- let status, task;
12387
+ MARKDOWN.register(
12388
+ "todo",
12389
+ ({ props, content, isSelfClosing }) => {
12390
+ let status, task;
12253
12391
 
12254
- if (isSelfClosing) {
12255
- task = safeArg({ props, index: 0, key: "task", fallBack: "" });
12256
- status = safeArg({ props, index: 1, key: "status", fallBack: "" });
12257
- } else {
12258
- status = safeArg({ props, index: 0, fallBack: "" });
12259
- task = content;
12260
- }
12392
+ if (isSelfClosing) {
12393
+ task = safeArg({ props, index: 0, key: "task", fallBack: "" });
12394
+ status = safeArg({ props, index: 1, key: "status", fallBack: "" });
12395
+ } else {
12396
+ status = safeArg({ props, index: 0, fallBack: "" });
12397
+ task = content;
12398
+ }
12261
12399
 
12262
- return md.todo(status, task);
12263
- }, { trimAndWrapBlocks: false });
12400
+ return md.todo(status, task);
12401
+ },
12402
+ { trimAndWrapBlocks: false }
12403
+ );
12264
12404
 
12265
12405
  /**
12266
12406
  * The MDX Mapper used for generating Markdown with JSX.
@@ -12470,100 +12610,115 @@ Jsonc.register(["Array", "array"], async function ({ props, ast, depth = 0, inAr
12470
12610
  /**
12471
12611
  * Renders a standard XML tag based on the provided identifier and arguments.
12472
12612
  * Ensures strict attribute quoting and handles self-closing tags for empty bodies.
12473
- *
12613
+ *
12474
12614
  * @param {string} id - The XML tag identifier (case-sensitive).
12475
12615
  * @param {Object} props - Key-value pairs to be rendered as XML attributes.
12476
12616
  * @param {string} content - The rendered inner content of the tag.
12477
12617
  * @returns {string} The fully rendered XML tag string.
12478
12618
  */
12479
12619
  const renderXmlTag = function (id, props, content, isSelfClosing) {
12480
- // XML is case-sensitive, so we use the exact id provided
12481
- const element = this.tag(id);
12482
-
12483
- // Filter out positional indices (numeric keys) for XML attributes
12484
- const namedArgs = {};
12485
- Object.keys(props).forEach(key => {
12486
- if (isNaN(parseInt(key))) {
12487
- namedArgs[key] = props[key];
12488
- }
12489
- });
12620
+ // XML is case-sensitive, so we use the exact id provided
12621
+ const element = this.tag(id);
12490
12622
 
12491
- // In XML, attributes must always have values (strict = true)
12492
- element.attributes(namedArgs, true);
12623
+ // Filter out positional indices (numeric keys) for XML attributes
12624
+ const namedArgs = {};
12625
+ Object.keys(props).forEach(key => {
12626
+ if (isNaN(parseInt(key))) {
12627
+ namedArgs[key] = props[key];
12628
+ }
12629
+ });
12493
12630
 
12494
- const hasBody = typeof content === "string" && content.trim().length > 0;
12631
+ // In XML, attributes must always have values (strict = true)
12632
+ element.attributes(namedArgs, true);
12495
12633
 
12496
- if (isSelfClosing || !hasBody) {
12497
- return element.selfClose();
12498
- }
12634
+ const hasBody = typeof content === "string" && content.trim().length > 0;
12635
+
12636
+ if (isSelfClosing || !hasBody) {
12637
+ return element.selfClose();
12638
+ }
12499
12639
 
12500
- return element.body(content);
12640
+ return element.body(content);
12501
12641
  };
12502
12642
 
12503
12643
  /**
12504
12644
  * The XML Mapper used for creating XML pages.
12505
12645
  */
12506
12646
  const XML = Mapper.define({
12507
- /**
12508
- * Renders a comment in XML format.
12509
- * @param {string} text - The comment content.
12510
- * @returns {string}
12511
- */
12512
- comment(text) {
12513
- return `<!-- ${text} -->`;
12514
- },
12647
+ /**
12648
+ * Renders a comment in XML format.
12649
+ * @param {string} text - The comment content.
12650
+ * @returns {string}
12651
+ */
12652
+ comment(text) {
12653
+ return `<!-- ${text} -->`;
12654
+ },
12515
12655
 
12516
- /**
12517
- * Resolves unknown tags by preserving their original case and applying XML rules.
12518
- * @param {Object} node - The AST node representing the unknown tag.
12519
- * @returns {Object} Renderer definition for the tag.
12520
- */
12521
- getUnknownTag(node) {
12522
- const id = node.id;
12523
- return {
12524
- render: ({ props, content, isSelfClosing }) => renderXmlTag.call(this, id, props, content, isSelfClosing),
12525
- options: {}
12526
- };
12527
- }
12656
+ /**
12657
+ * Resolves unknown tags by preserving their original case and applying XML rules.
12658
+ * @param {Object} node - The AST node representing the unknown tag.
12659
+ * @returns {Object} Renderer definition for the tag.
12660
+ */
12661
+ getUnknownTag(node) {
12662
+ const id = node.id;
12663
+ return {
12664
+ render: ({ props, content, isSelfClosing }) => renderXmlTag.call(this, id, props, content, isSelfClosing),
12665
+ options: {}
12666
+ };
12667
+ },
12668
+ options: {
12669
+ trimAndWrapBlocks: true
12670
+ }
12528
12671
  });
12529
12672
 
12530
12673
  /**
12531
12674
  * Registers the XML declaration as a self-closing block.
12532
12675
  * Usage: [xml = version: "1.0", encoding: "UTF-8"]
12533
12676
  */
12534
- XML.register("xml", ({ props }) => {
12535
- const version = props.version || "1.0";
12536
- const encoding = props.encoding || "UTF-8";
12537
- return `<?xml version="${version}" encoding="${encoding}"?>`;
12538
- }, { rules: { is_empty_body: true } });
12677
+ XML.register(
12678
+ "xml",
12679
+ ({ props }) => {
12680
+ const version = props.version || "1.0";
12681
+ const encoding = props.encoding || "UTF-8";
12682
+ return `<?xml version="${version}" encoding="${encoding}"?>`;
12683
+ },
12684
+ { rules: { is_empty_body: true } }
12685
+ );
12539
12686
 
12540
12687
  /**
12541
12688
  * Registers the DOCTYPE declaration.
12542
12689
  * Usage: [doctype = root: "note", system: "note.dtd"]
12543
12690
  */
12544
- XML.register(["DOCTYPE", "doctype"], ({ props }) => {
12545
- const root = props.root || "root";
12546
- const system = props.system;
12547
- const pub = props.public || props.fpi;
12691
+ XML.register(
12692
+ ["DOCTYPE", "doctype"],
12693
+ ({ props }) => {
12694
+ const root = props.root || "root";
12695
+ const system = props.system;
12696
+ const pub = props.public || props.fpi;
12548
12697
 
12549
- if (pub && system) {
12550
- return `<!DOCTYPE ${root} PUBLIC "${pub}" "${system}">`;
12551
- } else if (system) {
12552
- return `<!DOCTYPE ${root} SYSTEM "${system}">`;
12553
- }
12554
- return `<!DOCTYPE ${root}>`;
12555
- }, { rules: { is_empty_body: true } });
12698
+ if (pub && system) {
12699
+ return `<!DOCTYPE ${root} PUBLIC "${pub}" "${system}">`;
12700
+ } else if (system) {
12701
+ return `<!DOCTYPE ${root} SYSTEM "${system}">`;
12702
+ }
12703
+ return `<!DOCTYPE ${root}>`;
12704
+ },
12705
+ { rules: { is_empty_body: true } }
12706
+ );
12556
12707
 
12557
12708
  /**
12558
12709
  * Registers the XML stylesheet processing instruction.
12559
12710
  * Usage: [xml-stylesheet = href: "style.xsl"]
12560
12711
  */
12561
- XML.register("xml-stylesheet", ({ props }) => {
12562
- const type = props.type || "text/xsl";
12563
- const href = props.href;
12564
- if (!href) return "";
12565
- return `<?xml-stylesheet type="${type}" href="${href}"?>`;
12566
- }, { rules: { is_empty_body: true } });
12712
+ XML.register(
12713
+ "xml-stylesheet",
12714
+ ({ props }) => {
12715
+ const type = props.type || "text/xsl";
12716
+ const href = props.href;
12717
+ if (!href) return "";
12718
+ return `<?xml-stylesheet type="${type}" href="${href}"?>`;
12719
+ },
12720
+ { rules: { is_empty_body: true } }
12721
+ );
12567
12722
 
12568
12723
  /**
12569
12724
  * Registers CDATA sections.
@@ -12571,10 +12726,12 @@ XML.register("xml-stylesheet", ({ props }) => {
12571
12726
  * Self-closing: [cdata = "raw content" !] or [cdata = text: "raw content" !]
12572
12727
  */
12573
12728
  XML.register("cdata", ({ props, content, isSelfClosing }) => {
12574
- const text = isSelfClosing ? (props[0] ?? props.text ?? "") : content;
12575
- return `<![CDATA[${text}]]>`;
12729
+ const text = isSelfClosing ? (props[0] ?? props.text ?? "") : content;
12730
+ return `<![CDATA[${text}]]>`;
12576
12731
  });
12577
12732
 
12733
+ registerSharedOutputs(XML);
12734
+
12578
12735
  const csvEscape = (value) => {
12579
12736
  const str = String(value ?? "").trim();
12580
12737
  if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
@@ -12631,6 +12788,8 @@ CSV.register(["col", "cell", "td"], ({ textContent }) => {
12631
12788
  return csvEscape(textContent);
12632
12789
  }, { handleAst: true, trimAndWrapBlocks: false });
12633
12790
 
12791
+ registerSharedOutputs(CSV);
12792
+
12634
12793
  const isValidInt$1 = (v) => v !== "" && !isNaN(Number(v)) && !v.includes(".");
12635
12794
  const isValidFloat$1 = (v) => v !== "" && !isNaN(Number(v)) && v.includes(".");
12636
12795
  const isValidNumber$1 = (v) => v !== "" && !isNaN(Number(v));
@@ -12854,6 +13013,8 @@ TOML.register("array", async ({ props, ast, renderChild }) => {
12854
13013
  return `${tomlKey(key)} = [${vals.join(", ")}]\n`;
12855
13014
  }, { handleAst: true });
12856
13015
 
13016
+ registerSharedOutputs(TOML);
13017
+
12857
13018
  const isValidInt = (v) => v !== "" && !isNaN(Number(v)) && !v.includes(".");
12858
13019
  const isValidFloat = (v) => v !== "" && !isNaN(Number(v)) && v.includes(".");
12859
13020
  const isValidNumber = (v) => v !== "" && !isNaN(Number(v));
@@ -13169,6 +13330,8 @@ YAML.register("doc-start", () => "---\n", { rules: { is_empty_body: true } });
13169
13330
  */
13170
13331
  YAML.register("doc-end", () => "...\n", { rules: { is_empty_body: true } });
13171
13332
 
13333
+ registerSharedOutputs(YAML);
13334
+
13172
13335
  /**
13173
13336
  * The Text Mapper used for plain-text extraction.
13174
13337
  */
@@ -13445,34 +13608,48 @@ async function resolveModules(ast, context) {
13445
13608
  const baseDir = context.instance.baseDir || ((filename === "anonymous") ? absFilename : posix.dirname(absFilename));
13446
13609
 
13447
13610
  // 1. Helper: Trim AST to remove file-boundary whitespace and "ghost" newlines
13448
- const trimAst = (nodes) => {
13611
+ const trimAst = (nodes, trimBoundaries = true) => {
13449
13612
  if (!nodes) return [];
13450
13613
 
13451
- // 1. Filter out internal whitespace-only nodes that are adjacent to non-rendering nodes
13452
- // (Comments, Imports, etc. shouldn't leave "ghost" newlines)
13614
+ // 1. Filter out whitespace-only text nodes adjacent (directly or through other whitespace)
13615
+ // to non-rendering nodes (Comments, Imports, USE_MODULE).
13453
13616
  const nonRenderingTypes = [COMMENT, IMPORT, USE_MODULE];
13454
13617
  let res = nodes.filter((node, idx) => {
13455
13618
  if (node.type !== TEXT$1 || node.text.trim() !== "") return true;
13456
13619
 
13457
- const prev = nodes[idx - 1];
13458
- const next = nodes[idx + 1];
13459
- const isAdjacentToNonRendering =
13460
- (prev && nonRenderingTypes.includes(prev.type)) ||
13461
- (next && nonRenderingTypes.includes(next.type));
13620
+ // Walk backwards through consecutive whitespace nodes to find prev non-whitespace
13621
+ let prevIsNonRendering = false;
13622
+ for (let j = idx - 1; j >= 0; j--) {
13623
+ if (nodes[j].type === TEXT$1 && nodes[j].text.trim() === "") continue;
13624
+ prevIsNonRendering = nonRenderingTypes.includes(nodes[j].type);
13625
+ break;
13626
+ }
13462
13627
 
13463
- return !isAdjacentToNonRendering;
13628
+ // Walk forwards through consecutive whitespace nodes to find next non-whitespace
13629
+ let nextIsNonRendering = false;
13630
+ for (let j = idx + 1; j < nodes.length; j++) {
13631
+ if (nodes[j].type === TEXT$1 && nodes[j].text.trim() === "") continue;
13632
+ nextIsNonRendering = nonRenderingTypes.includes(nodes[j].type);
13633
+ break;
13634
+ }
13635
+
13636
+ return !(prevIsNonRendering || nextIsNonRendering);
13464
13637
  });
13465
13638
 
13466
- // 2. Final pass: trim leading/trailing newlines from the remaining boundary text nodes
13467
- if (res.length > 0 && res[0].type === TEXT$1) {
13468
- res[0].text = res[0].text.replace(/^[\r\n]+/, "");
13469
- }
13470
- if (res.length > 0 && res[res.length - 1].type === TEXT$1) {
13471
- res[res.length - 1].text = res[res.length - 1].text.replace(/[\r\n]+\s*$/, "");
13639
+ if (trimBoundaries) {
13640
+ // 2. Final pass: trim leading/trailing newlines from the remaining boundary text nodes
13641
+ if (res.length > 0 && res[0].type === TEXT$1) {
13642
+ res[0].text = res[0].text.replace(/^[\r\n]+/, "");
13643
+ }
13644
+ if (res.length > 0 && res[res.length - 1].type === TEXT$1) {
13645
+ res[res.length - 1].text = res[res.length - 1].text.replace(/[\r\n]+\s*$/, "");
13646
+ }
13647
+
13648
+ // 3. Remove any nodes that became purely empty after trimming
13649
+ res = res.filter(node => node.type !== TEXT$1 || node.text !== "");
13472
13650
  }
13473
13651
 
13474
- // 3. Remove any nodes that became purely empty after trimming
13475
- return res.filter(node => node.type !== TEXT$1 || node.text !== "");
13652
+ return res;
13476
13653
  };
13477
13654
 
13478
13655
  // 2. Helper: Inject Slots with Indentation Propagation
@@ -13543,6 +13720,10 @@ async function resolveModules(ast, context) {
13543
13720
  // 1b. Resolve relative to current base (FS)
13544
13721
  const absolutePath = resolveModulePath(resolvedPath, currentBaseDir);
13545
13722
 
13723
+ if (!context.instance.fs) {
13724
+ 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.`]);
13725
+ }
13726
+
13546
13727
  // Local Path Resolution with Auto-Extension
13547
13728
  let localPath = absolutePath;
13548
13729
  if (!await context.instance.fs.exists(localPath) && !localPath.endsWith(".smark")) {
@@ -13694,7 +13875,6 @@ async function resolveModules(ast, context) {
13694
13875
  Object.entries(node.props).filter(([key]) => {
13695
13876
  if (key === "__consumed__") return false;
13696
13877
  if (consumed.has(key)) return false; // THE FIX: Filter if hit by v{}
13697
- if (key === "smark-raw") return false; // directive — must not leak onto root element
13698
13878
  return true;
13699
13879
  })
13700
13880
  );
@@ -13809,10 +13989,7 @@ const runValidations = (node, target, instance) => {
13809
13989
  const isStructural = node.type === "Block";
13810
13990
  if (isStructural && rules.required_args && Array.isArray(rules.required_args)) {
13811
13991
  const missingArgs = rules.required_args.filter(arg => {
13812
- // Check if the argument exists in named args or as a positional arg (if arg is a number)
13813
- if (typeof arg === "number") {
13814
- return node.props[arg] === undefined;
13815
- }
13992
+ if (typeof arg === "number") return node.props[arg] === undefined;
13816
13993
  return node.props[arg] === undefined;
13817
13994
  });
13818
13995
 
@@ -13827,6 +14004,22 @@ const runValidations = (node, target, instance) => {
13827
14004
  );
13828
14005
  }
13829
14006
  }
14007
+
14008
+ // -- Directives Validation (Required Directives) ----------------------- //
14009
+ if (isStructural && rules.required_directives && Array.isArray(rules.required_directives)) {
14010
+ const missingDirectives = rules.required_directives.filter(key => node.directives?.[key] === undefined);
14011
+
14012
+ if (missingDirectives.length > 0) {
14013
+ transpilerError(
14014
+ [
14015
+ "{N}",
14016
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is missing required directive props:$> <$red:${missingDirectives.map(k => `smark-${k}`).join(", ")}$>{N}`,
14017
+ `<$blue:Please ensure these directive props are provided in the template usage.$>`
14018
+ ],
14019
+ context
14020
+ );
14021
+ }
14022
+ }
13830
14023
  };
13831
14024
 
13832
14025
  /**
@@ -13998,6 +14191,14 @@ function setDefaultFs(fs) {
13998
14191
  Evaluator$1.setDefaultFs(fs);
13999
14192
  }
14000
14193
 
14194
+ function setDefaultEnv(env) {
14195
+ Evaluator$1.setDefaultEnv(env);
14196
+ }
14197
+
14198
+ function setDefaultAsyncLocalStorage(cls) {
14199
+ Evaluator$1.setDefaultAsyncLocalStorage(cls);
14200
+ }
14201
+
14001
14202
  function setDefaultResolvePath(fn) {
14002
14203
  defaultResolvePath = fn;
14003
14204
  }
@@ -14069,7 +14270,8 @@ class SomMark {
14069
14270
  allowFetch: security?.allowFetch !== false,
14070
14271
  allowHttp: security?.allowHttp === true,
14071
14272
  allowedOrigins: Array.isArray(security?.allowedOrigins) ? security.allowedOrigins.map(o => o.toLowerCase()) : null,
14072
- allowedExtensions: Array.isArray(security?.allowedExtensions) ? security.allowedExtensions.map(e => e.toLowerCase()) : null
14273
+ allowedExtensions: Array.isArray(security?.allowedExtensions) ? security.allowedExtensions.map(e => e.toLowerCase()) : null,
14274
+ env: Array.isArray(security?.env) ? security.env : []
14073
14275
  };
14074
14276
  this.warnings = [];
14075
14277
  this._prepared = false;
@@ -14381,8 +14583,24 @@ async function findAndLoadConfig(targetPath) {
14381
14583
  }
14382
14584
  setCompilerClass(SomMark);
14383
14585
 
14586
+ class AsyncLocalStorage {
14587
+ #store = undefined;
14588
+ run(store, fn) {
14589
+ const prev = this.#store;
14590
+ this.#store = store;
14591
+ try { return fn(); }
14592
+ finally { this.#store = prev; }
14593
+ }
14594
+ getStore() { return this.#store; }
14595
+ exit(fn) { return fn(); }
14596
+ enterWith(store) { this.#store = store; }
14597
+ disable() {}
14598
+ }
14599
+
14384
14600
  setDefaultFs(null);
14385
14601
  setDefaultCwd("/");
14602
+ setDefaultEnv(null);
14603
+ setDefaultAsyncLocalStorage(AsyncLocalStorage);
14386
14604
 
14387
14605
  /**
14388
14606
  * Resolves a relative path into a full URL using the current document location.
@@ -14464,4 +14682,4 @@ function renderCompiledHTML(container, html) {
14464
14682
  }
14465
14683
  }
14466
14684
 
14467
- export { CSV, Evaluator$1 as 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 };
14685
+ export { CSV, Evaluator$1 as 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 };