sommark 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/evaluator.js CHANGED
@@ -3,6 +3,9 @@ import path from "pathe";
3
3
  import * as acorn from "acorn";
4
4
  import SomMark, { registerHostCompile, registerHostSettings } from "./helpers/lib.js";
5
5
  import { formatMessage } from "./errors.js";
6
+ import { patheBundleCode } from "./pathe-bundle.js";
7
+ import lexer from "./lexer.js";
8
+ import parser from "./parser.js";
6
9
 
7
10
  // Set by index.js (Node.js) or index.browser.js (shim) — never imported directly.
8
11
  let evaluatorStorage = null;
@@ -145,7 +148,7 @@ const customFetchAdapter = async (input, init, security = {}) => {
145
148
  };
146
149
  };
147
150
 
148
- const customCompileAdapter = async (src, options, parentSecurity = {}) => {
151
+ const customCompileAdapter = async (src, options, parentSecurity = {}, parentFs = null, parentBaseDir = null) => {
149
152
  const maxDepth = parentSecurity?.maxDepth ?? 5;
150
153
  if (globalCompilationDepth >= maxDepth) {
151
154
  throw new Error(`Recursion Guard: Maximum Smark compilation depth exceeded (limit is ${maxDepth}).`);
@@ -161,7 +164,9 @@ const customCompileAdapter = async (src, options, parentSecurity = {}) => {
161
164
  ...cleanOptions,
162
165
  src,
163
166
  format: cleanOptions.format || "html",
164
- security: parentSecurity
167
+ security: parentSecurity,
168
+ fs: parentFs ?? undefined,
169
+ baseDir: cleanOptions.baseDir || parentBaseDir || undefined,
165
170
  };
166
171
  const sm = new compilerClass(compilerOptions);
167
172
  return await sm.transpile();
@@ -302,6 +307,7 @@ class EvaluatorState {
302
307
  } else {
303
308
  this.baseDir = "/";
304
309
  }
310
+ this.rootDir = settings?.instance?.cwd || this.baseDir;
305
311
  this.scopes = [{}];
306
312
  this.dynamicTagsStack = [new Map()];
307
313
  this.security = security;
@@ -342,13 +348,25 @@ class EvaluatorState {
342
348
  },
343
349
  __hostSomMarkVersion: SomMark.version,
344
350
  __hostSomMarkSettings: () => {
345
- const clean = { ...SomMark.settings };
346
- delete clean.instance;
347
- delete clean.fs;
348
- return JSON.stringify(clean);
349
- },
351
+ const s = SomMark.settings;
352
+ return JSON.stringify({
353
+ format: s.format ?? null,
354
+ dev: s.dev ?? false,
355
+ removeComments:s.removeComments ?? false,
356
+ allowRaw: s.allowRaw ?? true,
357
+ dualOutput: s.dualOutput ?? false,
358
+ webOutputs: s.webOutputs ?? false,
359
+ });
360
+ },
350
361
  __hostCompile: async (src, options) => {
351
- return await customCompileAdapter(src, options, this.security);
362
+ return await customCompileAdapter(src, options, this.security, this.nodeFs, this.baseDir);
363
+ },
364
+ __hostLexer: (src, filename) => {
365
+ return JSON.stringify(lexer(src, filename || "anonymous"));
366
+ },
367
+ __hostParser: (src, filename) => {
368
+ const tokens = lexer(src, filename || "anonymous");
369
+ return JSON.stringify(parser(tokens, filename || "anonymous"));
352
370
  },
353
371
  __hostFetch: async (input, initStr) => {
354
372
  const init = initStr ? JSON.parse(initStr) : undefined;
@@ -376,6 +394,59 @@ class EvaluatorState {
376
394
  const payload = JSON.parse(payloadStr);
377
395
  return await target.render.call(this.mapperFile, payload);
378
396
  },
397
+ __hostFileRead: async (filePath) => {
398
+ if (!this.nodeFs) {
399
+ throw new Error(
400
+ "[SomMark] fileHandler is not available in browser mode.\n" +
401
+ "File access is a server-side concept."
402
+ );
403
+ }
404
+ const abs = path.resolve(this.rootDir, filePath);
405
+ if (!abs.startsWith(this.rootDir)) {
406
+ throw new Error(
407
+ `[SomMark] fileHandler.read: path traversal outside project root is not allowed.\n` +
408
+ `Attempted path: ${abs}`
409
+ );
410
+ }
411
+ return this.nodeFs.readFile(abs, "utf-8");
412
+ },
413
+ __hostFileExists: async (filePath) => {
414
+ if (!this.nodeFs) return false;
415
+ const abs = path.resolve(this.rootDir, filePath);
416
+ if (!abs.startsWith(this.rootDir)) return false;
417
+ return this.nodeFs.exists(abs);
418
+ },
419
+ __hostFileGlob: async (pattern) => {
420
+ if (!this.nodeFs) throw new Error("[SomMark] fileHandler.glob is not available in browser mode.\nFile access is a server-side concept.");
421
+ if (!this.nodeFs.glob) throw new Error("[SomMark] fileHandler.glob requires Node.js 22 or later.");
422
+ const files = await this.nodeFs.glob(pattern, { cwd: this.rootDir });
423
+ return JSON.stringify(files);
424
+ },
425
+ __hostFileLastModified: async (filePath) => {
426
+ if (!this.nodeFs) throw new Error("[SomMark] fileHandler.lastModified is not available in browser mode.");
427
+ const abs = path.resolve(this.rootDir, filePath);
428
+ if (!abs.startsWith(this.rootDir)) throw new Error("[SomMark] fileHandler.lastModified: path traversal outside project root is not allowed.");
429
+ const stat = await this.nodeFs.stat(abs);
430
+ return stat.mtimeMs;
431
+ },
432
+ __hostFileStat: async (filePath) => {
433
+ if (!this.nodeFs) throw new Error("[SomMark] fileHandler.stat is not available in browser mode.\nFile access is a server-side concept.");
434
+ const abs = path.resolve(this.rootDir, filePath);
435
+ if (!abs.startsWith(this.rootDir)) throw new Error(`[SomMark] fileHandler.stat: path traversal outside project root is not allowed.\nAttempted path: ${abs}`);
436
+ try {
437
+ const s = await this.nodeFs.stat(abs);
438
+ return JSON.stringify({
439
+ size: s.size,
440
+ mtime: s.mtimeMs,
441
+ ctime: s.ctimeMs,
442
+ atime: s.atimeMs,
443
+ isFile: s.isFile(),
444
+ isDirectory: s.isDirectory(),
445
+ });
446
+ } catch {
447
+ return null;
448
+ }
449
+ },
379
450
  __allowRaw: this.security.allowRaw !== false
380
451
  });
381
452
 
@@ -528,6 +599,18 @@ class EvaluatorState {
528
599
  }
529
600
  return await __hostCompile(src, options);
530
601
  },
602
+ lexer: (src, filename) => {
603
+ if (typeof src !== "string") {
604
+ throw new Error("SomMark.lexer Error: Source must be a string.");
605
+ }
606
+ return JSON.parse(__hostLexer(src, filename));
607
+ },
608
+ parser: (src, filename) => {
609
+ if (typeof src !== "string") {
610
+ throw new Error("SomMark.parser Error: Source must be a string.");
611
+ }
612
+ return JSON.parse(__hostParser(src, filename));
613
+ },
531
614
  raw: (html) => {
532
615
  if (typeof __allowRaw !== "undefined" && !__allowRaw) {
533
616
  throw new Error("Security Error: SomMark.raw is disabled in this environment.");
@@ -568,6 +651,20 @@ class EvaluatorState {
568
651
  configurable: false
569
652
  });
570
653
 
654
+ Object.defineProperty(globalThis, "Smark", {
655
+ value: SomMark,
656
+ writable: false,
657
+ configurable: false
658
+ });
659
+
660
+ globalThis.fileHandler = Object.freeze({
661
+ read: async (path) => await __hostFileRead(path),
662
+ exists: async (path) => await __hostFileExists(path),
663
+ glob: async (pattern) => JSON.parse(await __hostFileGlob(pattern)),
664
+ lastModified: async (path) => await __hostFileLastModified(path),
665
+ stat: async (path) => { const r = await __hostFileStat(path); return r ? JSON.parse(r) : null; },
666
+ });
667
+
571
668
  delete globalThis.fetch;
572
669
  delete globalThis.process;
573
670
  `);
@@ -579,6 +676,13 @@ class EvaluatorState {
579
676
  }
580
677
  setupRes.value.dispose();
581
678
 
679
+ const patheRes = this.context.evalCode(patheBundleCode);
680
+ if (patheRes.error) {
681
+ patheRes.error.dispose();
682
+ } else {
683
+ patheRes.value.dispose();
684
+ }
685
+
582
686
  // Configure module loader using virtual FS implementation.
583
687
  // The normalizer resolves every import to an absolute path so the module
584
688
  // cache key is always absolute — <smark> (the eval module name) can never
@@ -793,10 +897,17 @@ class EvaluatorState {
793
897
  if (!this.context) return;
794
898
  const safe = {};
795
899
  for (const [key, value] of Object.entries(vars)) {
796
- if (!isPlainData(value)) {
797
- console.warn(`[SomMark] Security: "${key}" contains functions and was blocked. Only plain data can be injected. Use SomMark built-ins for host capabilities.`);
900
+ if (typeof value === "function") {
901
+ const src = value.toString();
902
+ if (src.includes("SomMark.")) {
903
+ console.warn(`[SomMark] variables.${key}: references 'SomMark' which bundlers may rename. Use 'Smark' instead.`);
904
+ }
905
+ const res = this.context.evalCode(`globalThis[${JSON.stringify(key)}] = ${src}`);
906
+ if (res.error) res.error.dispose();
907
+ else res.value.dispose();
798
908
  continue;
799
909
  }
910
+ if (!isPlainData(value)) continue;
800
911
  safe[key] = value;
801
912
  }
802
913
  const currentScope = this.scopes[this.scopes.length - 1];
@@ -871,7 +982,16 @@ class EvaluatorState {
871
982
  }
872
983
  }
873
984
  } catch (err) {
874
- // Ignore parsing errors and fallback to raw code
985
+ // Parse failed as a statement try as a parenthesised expression.
986
+ // This handles object/array literals like {a: 1} or [1, 2] which are
987
+ // ambiguous in statement context but valid when wrapped in parens.
988
+ try {
989
+ const trimmed = code.trim();
990
+ acorn.parse(`(${trimmed})`, { ecmaVersion: 'latest', sourceType: 'module' });
991
+ finalCode = `export default (${trimmed});`;
992
+ } catch {
993
+ // Give up — let QuickJS surface the error.
994
+ }
875
995
  }
876
996
 
877
997
  if (autoExportedNames.length > 0 && !hasExplicitExports) {
@@ -18,7 +18,7 @@ export function registerHostSettings(settings) {
18
18
  hostSettings = settings || {};
19
19
  }
20
20
 
21
- const version = "5.1.0";
21
+ const version = "5.2.0";
22
22
 
23
23
  const SomMark = {
24
24
  version,
package/core/modules.js CHANGED
@@ -296,13 +296,34 @@ export async function resolveModules(ast, context) {
296
296
  let resolvedPath = filePath;
297
297
  for (const [prefix, replacement] of Object.entries(importAliases)) {
298
298
  if (filePath.startsWith(prefix)) {
299
- resolvedPath = path.resolve(context.instance.cwd || "/", filePath.replace(prefix, replacement));
299
+ const replaced = filePath.replace(prefix, replacement);
300
+ // Preserve scheme prefixes (pkg:, http:, etc.) — don't path.resolve them
301
+ resolvedPath = replaced.startsWith("pkg:") || replaced.startsWith("http://") || replaced.startsWith("https://")
302
+ ? replaced
303
+ : path.resolve(context.instance.cwd || "/", replaced);
300
304
  break;
301
305
  }
302
306
  }
303
307
 
304
- // 1b. Resolve relative to current base (FS)
305
- const absolutePath = resolveModulePath(resolvedPath, currentBaseDir);
308
+ // 1b. pkg: resolve from node_modules at project root
309
+ let absolutePath;
310
+ if (resolvedPath.startsWith("pkg:")) {
311
+ if (!context.instance.fs?.__isNodeFs) {
312
+ runtimeError([`<$red:Module Error:$> <$cyan:pkg:$> imports are not supported in browser or virtual filesystem mode at line <$yellow:${node.range.start.line + 1}$>`]);
313
+ }
314
+ const pkgPath = resolvedPath.slice(4);
315
+ if (!pkgPath || pkgPath.trim() === "") {
316
+ runtimeError([`<$red:Module Error:$> <$cyan:pkg:$> path cannot be empty at line <$yellow:${node.range.start.line + 1}$>`]);
317
+ }
318
+ const nodeModulesRoot = path.resolve(context.instance.cwd || "/", "node_modules");
319
+ absolutePath = path.resolve(nodeModulesRoot, pkgPath);
320
+ if (!absolutePath.startsWith(nodeModulesRoot + path.sep) && absolutePath !== nodeModulesRoot) {
321
+ 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}$>`]);
322
+ }
323
+ } else {
324
+ // 1c. Resolve relative to current base (FS)
325
+ absolutePath = resolveModulePath(resolvedPath, currentBaseDir);
326
+ }
306
327
 
307
328
  if (!context.instance.fs) {
308
329
  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.`]);
@@ -0,0 +1 @@
1
+ export const patheBundleCode = "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)})();\n return __lib__.default || __lib__; };\nlet _match;\nconst zeptomatch = (path, pattern) => {\n if (!_match) {\n _match = _lazyMatch();\n _lazyMatch = null;\n }\n return _match(path, pattern);\n};\n\nconst _DRIVE_LETTER_START_RE = /^[A-Za-z]:\\//;\nfunction normalizeWindowsPath(input = \"\") {\n if (!input) {\n return input;\n }\n return input.replace(/\\\\/g, \"/\").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase());\n}\n\nconst _UNC_REGEX = /^[/\\\\]{2}/;\nconst _IS_ABSOLUTE_RE = /^[/\\\\](?![/\\\\])|^[/\\\\]{2}(?!\\.)|^[A-Za-z]:[/\\\\]/;\nconst _DRIVE_LETTER_RE = /^[A-Za-z]:$/;\nconst _ROOT_FOLDER_RE = /^\\/([A-Za-z]:)?$/;\nconst _EXTNAME_RE = /.(\\.[^./]+|\\.)$/;\nconst _PATH_ROOT_RE = /^[/\\\\]|^[a-zA-Z]:[/\\\\]/;\nconst sep = \"/\";\nconst normalize = function(path) {\n if (path.length === 0) {\n return \".\";\n }\n path = normalizeWindowsPath(path);\n const isUNCPath = path.match(_UNC_REGEX);\n const isPathAbsolute = isAbsolute(path);\n const trailingSeparator = path[path.length - 1] === \"/\";\n path = normalizeString(path, !isPathAbsolute);\n if (path.length === 0) {\n if (isPathAbsolute) {\n return \"/\";\n }\n return trailingSeparator ? \"./\" : \".\";\n }\n if (trailingSeparator) {\n path += \"/\";\n }\n if (_DRIVE_LETTER_RE.test(path)) {\n path += \"/\";\n }\n if (isUNCPath) {\n if (!isPathAbsolute) {\n return `//./${path}`;\n }\n return `//${path}`;\n }\n return isPathAbsolute && !isAbsolute(path) ? `/${path}` : path;\n};\nconst join = function(...segments) {\n let path = \"\";\n for (const seg of segments) {\n if (!seg) {\n continue;\n }\n if (path.length > 0) {\n const pathTrailing = path[path.length - 1] === \"/\";\n const segLeading = seg[0] === \"/\";\n const both = pathTrailing && segLeading;\n if (both) {\n path += seg.slice(1);\n } else {\n path += pathTrailing || segLeading ? seg : `/${seg}`;\n }\n } else {\n path += seg;\n }\n }\n return normalize(path);\n};\nfunction cwd() {\n if (typeof process !== \"undefined\" && typeof process.cwd === \"function\") {\n return process.cwd().replace(/\\\\/g, \"/\");\n }\n return \"/\";\n}\nconst resolve = function(...arguments_) {\n arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument));\n let resolvedPath = \"\";\n let resolvedAbsolute = false;\n for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) {\n const path = index >= 0 ? arguments_[index] : cwd();\n if (!path || path.length === 0) {\n continue;\n }\n resolvedPath = `${path}/${resolvedPath}`;\n resolvedAbsolute = isAbsolute(path);\n }\n resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);\n if (resolvedAbsolute && !isAbsolute(resolvedPath)) {\n return `/${resolvedPath}`;\n }\n return resolvedPath.length > 0 ? resolvedPath : \".\";\n};\nfunction normalizeString(path, allowAboveRoot) {\n let res = \"\";\n let lastSegmentLength = 0;\n let lastSlash = -1;\n let dots = 0;\n let char = null;\n for (let index = 0; index <= path.length; ++index) {\n if (index < path.length) {\n char = path[index];\n } else if (char === \"/\") {\n break;\n } else {\n char = \"/\";\n }\n if (char === \"/\") {\n if (lastSlash === index - 1 || dots === 1) ; else if (dots === 2) {\n if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== \".\" || res[res.length - 2] !== \".\") {\n if (res.length > 2) {\n const lastSlashIndex = res.lastIndexOf(\"/\");\n if (lastSlashIndex === -1) {\n res = \"\";\n lastSegmentLength = 0;\n } else {\n res = res.slice(0, lastSlashIndex);\n lastSegmentLength = res.length - 1 - res.lastIndexOf(\"/\");\n }\n lastSlash = index;\n dots = 0;\n continue;\n } else if (res.length > 0) {\n res = \"\";\n lastSegmentLength = 0;\n lastSlash = index;\n dots = 0;\n continue;\n }\n }\n if (allowAboveRoot) {\n res += res.length > 0 ? \"/..\" : \"..\";\n lastSegmentLength = 2;\n }\n } else {\n if (res.length > 0) {\n res += `/${path.slice(lastSlash + 1, index)}`;\n } else {\n res = path.slice(lastSlash + 1, index);\n }\n lastSegmentLength = index - lastSlash - 1;\n }\n lastSlash = index;\n dots = 0;\n } else if (char === \".\" && dots !== -1) {\n ++dots;\n } else {\n dots = -1;\n }\n }\n return res;\n}\nconst isAbsolute = function(p) {\n return _IS_ABSOLUTE_RE.test(p);\n};\nconst toNamespacedPath = function(p) {\n return normalizeWindowsPath(p);\n};\nconst extname = function(p) {\n if (p === \"..\") return \"\";\n const match = _EXTNAME_RE.exec(normalizeWindowsPath(p));\n return match && match[1] || \"\";\n};\nconst relative = function(from, to) {\n const _from = resolve(from).replace(_ROOT_FOLDER_RE, \"$1\").split(\"/\");\n const _to = resolve(to).replace(_ROOT_FOLDER_RE, \"$1\").split(\"/\");\n if (_to[0][1] === \":\" && _from[0][1] === \":\" && _from[0] !== _to[0]) {\n return _to.join(\"/\");\n }\n const _fromCopy = [..._from];\n for (const segment of _fromCopy) {\n if (_to[0] !== segment) {\n break;\n }\n _from.shift();\n _to.shift();\n }\n return [..._from.map(() => \"..\"), ..._to].join(\"/\");\n};\nconst dirname = function(p) {\n const segments = normalizeWindowsPath(p).replace(/\\/$/, \"\").split(\"/\").slice(0, -1);\n if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) {\n segments[0] += \"/\";\n }\n return segments.join(\"/\") || (isAbsolute(p) ? \"/\" : \".\");\n};\nconst format = function(p) {\n const ext = p.ext ? p.ext.startsWith(\".\") ? p.ext : `.${p.ext}` : \"\";\n const segments = [p.root, p.dir, p.base ?? (p.name ?? \"\") + ext].filter(\n Boolean\n );\n return normalizeWindowsPath(\n p.root ? resolve(...segments) : segments.join(\"/\")\n );\n};\nconst basename = function(p, extension) {\n const segments = normalizeWindowsPath(p).split(\"/\");\n let lastSegment = \"\";\n for (let i = segments.length - 1; i >= 0; i--) {\n const val = segments[i];\n if (val) {\n lastSegment = val;\n break;\n }\n }\n return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;\n};\nconst parse = function(p) {\n const root = _PATH_ROOT_RE.exec(p)?.[0]?.replace(/\\\\/g, \"/\") || \"\";\n const base = basename(p);\n const extension = extname(base);\n return {\n root,\n dir: dirname(p),\n base,\n ext: extension,\n name: base.slice(0, base.length - extension.length)\n };\n};\nconst matchesGlob = (path, pattern) => {\n return zeptomatch(pattern, normalize(path));\n};\n\nconst _path = {\n __proto__: null,\n basename: basename,\n dirname: dirname,\n extname: extname,\n format: format,\n isAbsolute: isAbsolute,\n join: join,\n matchesGlob: matchesGlob,\n normalize: normalize,\n normalizeString: normalizeString,\n parse: parse,\n relative: relative,\n resolve: resolve,\n sep: sep,\n toNamespacedPath: toNamespacedPath\n};\n\nconst delimiter = /* @__PURE__ */ (() => globalThis.process?.platform === \"win32\" ? \";\" : \":\")();\nconst _platforms = { posix: void 0, win32: void 0 };\nconst mix = (del = delimiter) => {\n return new Proxy(_path, {\n get(_, prop) {\n if (prop === \"delimiter\") return del;\n if (prop === \"posix\") return posix;\n if (prop === \"win32\") return win32;\n return _platforms[prop] || _path[prop];\n }\n });\n};\nconst posix = /* @__PURE__ */ mix(\":\");\nconst win32 = /* @__PURE__ */ mix(\";\");\n\nvar pathe = /*#__PURE__*/Object.freeze({\n __proto__: null,\n basename: basename,\n default: posix,\n delimiter: delimiter,\n dirname: dirname,\n extname: extname,\n format: format,\n isAbsolute: isAbsolute,\n join: join,\n matchesGlob: matchesGlob,\n normalize: normalize,\n normalizeString: normalizeString,\n parse: parse,\n posix: posix,\n relative: relative,\n resolve: resolve,\n sep: sep,\n toNamespacedPath: toNamespacedPath,\n win32: win32\n});\n\nglobalThis.pathHandler = pathe;\n";
@@ -14,7 +14,7 @@ function warnDroppedVariables(variables) {
14
14
  } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
15
15
  for (const [nestedKey, nestedVal] of Object.entries(value)) {
16
16
  if (typeof nestedVal === "function") {
17
- 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}`);
17
+ console.warn(`[SomMark] variables.${key}.${nestedKey}: nested functions inside objects are not supported. Define it as a top-level function instead: variables.${nestedKey}`);
18
18
  } else if (nestedVal === undefined) {
19
19
  console.warn(`[SomMark] variables.${key}.${nestedKey} is undefined and will be ignored.`);
20
20
  }
@@ -121,9 +121,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
121
121
  const out = (result !== undefined && typeof result !== "object") ? String(result) : "";
122
122
  return mapper_file ? mapper_file.text(out) : out;
123
123
  } catch (err) {
124
+ const line = node.range?.start?.line + 1 || 1;
124
125
  transpilerError([
125
126
  `<$red:Logic Error:$> ${err.message}{line}`,
126
- `<$yellow:Code:$> <$blue:${node.code}$>{line}`
127
+ `<$yellow:Code:$> <$blue:${node.code}$>{line}`,
128
+ `at line <$yellow:${line}$>{line}`
127
129
  ]);
128
130
  }
129
131
  }
@@ -334,9 +336,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
334
336
  const val = await evaluator.execute(child.code, child.baseDir || null);
335
337
  if (val !== undefined && typeof val !== "object") richText += String(val);
336
338
  } catch (err) {
339
+ const line = child.range?.start?.line + 1 || 1;
337
340
  transpilerError([
338
341
  `<$red:Logic Error:$> ${err.message}{line}`,
339
- `<$yellow:Code:$> <$blue:${child.code}$>{line}`
342
+ `<$yellow:Code:$> <$blue:${child.code}$>{line}`,
343
+ `at line <$yellow:${line}$>{line}`
340
344
  ]);
341
345
  }
342
346
  } else if (child.type === COMMENT) {
@@ -464,9 +468,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
464
468
  bodyOutput = mapper_file ? mapper_file.text(out, { ...target?.options, escape: parentEscape }) : out;
465
469
  }
466
470
  } catch (err) {
471
+ const line = body_node.range?.start?.line + 1 || 1;
467
472
  transpilerError([
468
473
  `<$red:Logic Error:$> ${err.message}{line}`,
469
- `<$yellow:Code:$> <$blue:${body_node.code}$>{line}`
474
+ `<$yellow:Code:$> <$blue:${body_node.code}$>{line}`,
475
+ `at line <$yellow:${line}$>{line}`
470
476
  ]);
471
477
  }
472
478
  break;
@@ -565,6 +571,10 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
565
571
  })();
566
572
 
567
573
  const dualOutput = optionsOrAst?.dualOutput || false;
574
+ const webOutputs = optionsOrAst?.webOutputs || false;
575
+ if (webOutputs && dualOutput) {
576
+ 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]).");
577
+ }
568
578
  const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
569
579
  const variables = optionsOrAst?.variables || settings?.variables || {};
570
580
  warnDroppedVariables(variables);
@@ -579,6 +589,89 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
579
589
  let prev_body_node = null;
580
590
  let prev_was_silent = false;
581
591
 
592
+ if (webOutputs) {
593
+ // Use unique markers so [style] content is extracted precisely —
594
+ // no <style> regex on the final HTML, works with static logic inside [style].
595
+ const CSS_OPEN = `SOMMARKCSSOPEN${randomBytesHex(8)}SOMMARK`;
596
+ const CSS_CLOSE = `SOMMARKCSSCLOSE${randomBytesHex(8)}SOMMARK`;
597
+
598
+ const webMapper = targetMapper.clone();
599
+ webMapper.register("style", function ({ content }) {
600
+ return `${CSS_OPEN}${content}${CSS_CLOSE}`;
601
+ }, { escape: false });
602
+ // [head] injects CSS variables as a raw <style> string via this.cssVariables —
603
+ // override it so those variables go through markers too.
604
+ webMapper.register("head", function ({ content }) {
605
+ const varsMarker = this.cssVariables
606
+ ? `${CSS_OPEN}:root { ${this.cssVariables} }${CSS_CLOSE}\n`
607
+ : "";
608
+ return this.tag("head").body(`${varsMarker}${content}`);
609
+ }, { escape: false });
610
+
611
+ const idState = { mode: 'record', ids: [], idx: 0 };
612
+
613
+ // HTML pass — [style] blocks emit markers instead of <style> tags
614
+ let htmlOutput = "";
615
+ try {
616
+ for (let i = 0; i < body.length; i++) {
617
+ const node = body[i];
618
+ const blockOutput = await generateOutput(body, i, targetFormat, webMapper, security, null, false, true, instance, idState);
619
+ let finalBlockOutput = blockOutput;
620
+ if (prev_was_silent && node.type === TEXT) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
621
+ if (finalBlockOutput) {
622
+ htmlOutput += finalBlockOutput;
623
+ prev_was_silent = false;
624
+ } else {
625
+ prev_was_silent = true;
626
+ if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
627
+ const nextNode = body[i + 1];
628
+ if (nextNode && nextNode.type === TEXT && (nextNode.text === "\n" || nextNode.text === "\r\n")) i++;
629
+ }
630
+ }
631
+ }
632
+ } finally {
633
+ evaluator.destroy();
634
+ }
635
+
636
+ // Extract CSS from markers — exact, no HTML regex
637
+ const cssChunks = [];
638
+ const markerRe = new RegExp(`${CSS_OPEN}([\\s\\S]*?)${CSS_CLOSE}`, "g");
639
+ htmlOutput = htmlOutput.replace(markerRe, (_, chunk) => {
640
+ cssChunks.push(chunk.trim());
641
+ return "";
642
+ });
643
+ const css = cssChunks.join("\n").trim();
644
+
645
+ // JS pass — replay IDs so querySelector targets match HTML
646
+ idState.mode = 'replay';
647
+ idState.idx = 0;
648
+ prev_was_silent = false;
649
+
650
+ await evaluator.init(fileBaseDir, security, settings, targetMapper);
651
+ evaluator.inject(placeholders);
652
+ evaluator.inject(variables);
653
+
654
+ let jsOutput = "";
655
+ try {
656
+ for (let i = 0; i < body.length; i++) {
657
+ const node = body[i];
658
+ const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, true, false, instance, idState);
659
+ let finalBlockOutput = blockOutput;
660
+ if (prev_was_silent && node.type === TEXT) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
661
+ if (finalBlockOutput) {
662
+ jsOutput += finalBlockOutput;
663
+ prev_was_silent = false;
664
+ } else {
665
+ prev_was_silent = true;
666
+ }
667
+ }
668
+ } finally {
669
+ evaluator.destroy();
670
+ }
671
+
672
+ return [htmlOutput.trim(), css, jsOutput.trim()];
673
+ }
674
+
582
675
  if (dualOutput) {
583
676
  const idState = { mode: 'record', ids: [], idx: 0 };
584
677
 
@@ -691,9 +784,11 @@ async function transpileArgs(props) {
691
784
  try {
692
785
  result[key] = await evaluator.execute(value.code, value.baseDir || null);
693
786
  } catch (err) {
787
+ const line = value.range?.start?.line + 1 || 1;
694
788
  transpilerError([
695
789
  `<$red:Logic Error (Argument):$> ${err.message}{line}`,
696
- `<$yellow:Code:$> <$blue:${value.code}$>{line}`
790
+ `<$yellow:Code:$> <$blue:${value.code}$>{line}`,
791
+ `at line <$yellow:${line}$>{line}`
697
792
  ]);
698
793
  }
699
794
  } else {
@@ -8628,7 +8628,7 @@ function registerHostSettings(settings) {
8628
8628
  hostSettings = settings || {};
8629
8629
  }
8630
8630
 
8631
- const version = "5.1.0";
8631
+ const version = "5.2.0";
8632
8632
 
8633
8633
  const SomMark$1 = {
8634
8634
  version,
@@ -8679,6 +8679,8 @@ const SomMark$1 = {
8679
8679
  // Freeze the entire Standard Library to make it completely immutable and tamper-proof
8680
8680
  Object.freeze(SomMark$1);
8681
8681
 
8682
+ const patheBundleCode = "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)})();\n return __lib__.default || __lib__; };\nlet _match;\nconst zeptomatch = (path, pattern) => {\n if (!_match) {\n _match = _lazyMatch();\n _lazyMatch = null;\n }\n return _match(path, pattern);\n};\n\nconst _DRIVE_LETTER_START_RE = /^[A-Za-z]:\\//;\nfunction normalizeWindowsPath(input = \"\") {\n if (!input) {\n return input;\n }\n return input.replace(/\\\\/g, \"/\").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase());\n}\n\nconst _UNC_REGEX = /^[/\\\\]{2}/;\nconst _IS_ABSOLUTE_RE = /^[/\\\\](?![/\\\\])|^[/\\\\]{2}(?!\\.)|^[A-Za-z]:[/\\\\]/;\nconst _DRIVE_LETTER_RE = /^[A-Za-z]:$/;\nconst _ROOT_FOLDER_RE = /^\\/([A-Za-z]:)?$/;\nconst _EXTNAME_RE = /.(\\.[^./]+|\\.)$/;\nconst _PATH_ROOT_RE = /^[/\\\\]|^[a-zA-Z]:[/\\\\]/;\nconst sep = \"/\";\nconst normalize = function(path) {\n if (path.length === 0) {\n return \".\";\n }\n path = normalizeWindowsPath(path);\n const isUNCPath = path.match(_UNC_REGEX);\n const isPathAbsolute = isAbsolute(path);\n const trailingSeparator = path[path.length - 1] === \"/\";\n path = normalizeString(path, !isPathAbsolute);\n if (path.length === 0) {\n if (isPathAbsolute) {\n return \"/\";\n }\n return trailingSeparator ? \"./\" : \".\";\n }\n if (trailingSeparator) {\n path += \"/\";\n }\n if (_DRIVE_LETTER_RE.test(path)) {\n path += \"/\";\n }\n if (isUNCPath) {\n if (!isPathAbsolute) {\n return `//./${path}`;\n }\n return `//${path}`;\n }\n return isPathAbsolute && !isAbsolute(path) ? `/${path}` : path;\n};\nconst join = function(...segments) {\n let path = \"\";\n for (const seg of segments) {\n if (!seg) {\n continue;\n }\n if (path.length > 0) {\n const pathTrailing = path[path.length - 1] === \"/\";\n const segLeading = seg[0] === \"/\";\n const both = pathTrailing && segLeading;\n if (both) {\n path += seg.slice(1);\n } else {\n path += pathTrailing || segLeading ? seg : `/${seg}`;\n }\n } else {\n path += seg;\n }\n }\n return normalize(path);\n};\nfunction cwd() {\n if (typeof process !== \"undefined\" && typeof process.cwd === \"function\") {\n return process.cwd().replace(/\\\\/g, \"/\");\n }\n return \"/\";\n}\nconst resolve = function(...arguments_) {\n arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument));\n let resolvedPath = \"\";\n let resolvedAbsolute = false;\n for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) {\n const path = index >= 0 ? arguments_[index] : cwd();\n if (!path || path.length === 0) {\n continue;\n }\n resolvedPath = `${path}/${resolvedPath}`;\n resolvedAbsolute = isAbsolute(path);\n }\n resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);\n if (resolvedAbsolute && !isAbsolute(resolvedPath)) {\n return `/${resolvedPath}`;\n }\n return resolvedPath.length > 0 ? resolvedPath : \".\";\n};\nfunction normalizeString(path, allowAboveRoot) {\n let res = \"\";\n let lastSegmentLength = 0;\n let lastSlash = -1;\n let dots = 0;\n let char = null;\n for (let index = 0; index <= path.length; ++index) {\n if (index < path.length) {\n char = path[index];\n } else if (char === \"/\") {\n break;\n } else {\n char = \"/\";\n }\n if (char === \"/\") {\n if (lastSlash === index - 1 || dots === 1) ; else if (dots === 2) {\n if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== \".\" || res[res.length - 2] !== \".\") {\n if (res.length > 2) {\n const lastSlashIndex = res.lastIndexOf(\"/\");\n if (lastSlashIndex === -1) {\n res = \"\";\n lastSegmentLength = 0;\n } else {\n res = res.slice(0, lastSlashIndex);\n lastSegmentLength = res.length - 1 - res.lastIndexOf(\"/\");\n }\n lastSlash = index;\n dots = 0;\n continue;\n } else if (res.length > 0) {\n res = \"\";\n lastSegmentLength = 0;\n lastSlash = index;\n dots = 0;\n continue;\n }\n }\n if (allowAboveRoot) {\n res += res.length > 0 ? \"/..\" : \"..\";\n lastSegmentLength = 2;\n }\n } else {\n if (res.length > 0) {\n res += `/${path.slice(lastSlash + 1, index)}`;\n } else {\n res = path.slice(lastSlash + 1, index);\n }\n lastSegmentLength = index - lastSlash - 1;\n }\n lastSlash = index;\n dots = 0;\n } else if (char === \".\" && dots !== -1) {\n ++dots;\n } else {\n dots = -1;\n }\n }\n return res;\n}\nconst isAbsolute = function(p) {\n return _IS_ABSOLUTE_RE.test(p);\n};\nconst toNamespacedPath = function(p) {\n return normalizeWindowsPath(p);\n};\nconst extname = function(p) {\n if (p === \"..\") return \"\";\n const match = _EXTNAME_RE.exec(normalizeWindowsPath(p));\n return match && match[1] || \"\";\n};\nconst relative = function(from, to) {\n const _from = resolve(from).replace(_ROOT_FOLDER_RE, \"$1\").split(\"/\");\n const _to = resolve(to).replace(_ROOT_FOLDER_RE, \"$1\").split(\"/\");\n if (_to[0][1] === \":\" && _from[0][1] === \":\" && _from[0] !== _to[0]) {\n return _to.join(\"/\");\n }\n const _fromCopy = [..._from];\n for (const segment of _fromCopy) {\n if (_to[0] !== segment) {\n break;\n }\n _from.shift();\n _to.shift();\n }\n return [..._from.map(() => \"..\"), ..._to].join(\"/\");\n};\nconst dirname = function(p) {\n const segments = normalizeWindowsPath(p).replace(/\\/$/, \"\").split(\"/\").slice(0, -1);\n if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) {\n segments[0] += \"/\";\n }\n return segments.join(\"/\") || (isAbsolute(p) ? \"/\" : \".\");\n};\nconst format = function(p) {\n const ext = p.ext ? p.ext.startsWith(\".\") ? p.ext : `.${p.ext}` : \"\";\n const segments = [p.root, p.dir, p.base ?? (p.name ?? \"\") + ext].filter(\n Boolean\n );\n return normalizeWindowsPath(\n p.root ? resolve(...segments) : segments.join(\"/\")\n );\n};\nconst basename = function(p, extension) {\n const segments = normalizeWindowsPath(p).split(\"/\");\n let lastSegment = \"\";\n for (let i = segments.length - 1; i >= 0; i--) {\n const val = segments[i];\n if (val) {\n lastSegment = val;\n break;\n }\n }\n return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;\n};\nconst parse = function(p) {\n const root = _PATH_ROOT_RE.exec(p)?.[0]?.replace(/\\\\/g, \"/\") || \"\";\n const base = basename(p);\n const extension = extname(base);\n return {\n root,\n dir: dirname(p),\n base,\n ext: extension,\n name: base.slice(0, base.length - extension.length)\n };\n};\nconst matchesGlob = (path, pattern) => {\n return zeptomatch(pattern, normalize(path));\n};\n\nconst _path = {\n __proto__: null,\n basename: basename,\n dirname: dirname,\n extname: extname,\n format: format,\n isAbsolute: isAbsolute,\n join: join,\n matchesGlob: matchesGlob,\n normalize: normalize,\n normalizeString: normalizeString,\n parse: parse,\n relative: relative,\n resolve: resolve,\n sep: sep,\n toNamespacedPath: toNamespacedPath\n};\n\nconst delimiter = /* @__PURE__ */ (() => globalThis.process?.platform === \"win32\" ? \";\" : \":\")();\nconst _platforms = { posix: void 0, win32: void 0 };\nconst mix = (del = delimiter) => {\n return new Proxy(_path, {\n get(_, prop) {\n if (prop === \"delimiter\") return del;\n if (prop === \"posix\") return posix;\n if (prop === \"win32\") return win32;\n return _platforms[prop] || _path[prop];\n }\n });\n};\nconst posix = /* @__PURE__ */ mix(\":\");\nconst win32 = /* @__PURE__ */ mix(\";\");\n\nvar pathe = /*#__PURE__*/Object.freeze({\n __proto__: null,\n basename: basename,\n default: posix,\n delimiter: delimiter,\n dirname: dirname,\n extname: extname,\n format: format,\n isAbsolute: isAbsolute,\n join: join,\n matchesGlob: matchesGlob,\n normalize: normalize,\n normalizeString: normalizeString,\n parse: parse,\n posix: posix,\n relative: relative,\n resolve: resolve,\n sep: sep,\n toNamespacedPath: toNamespacedPath,\n win32: win32\n});\n\nglobalThis.pathHandler = pathe;\n";
8683
+
8682
8684
  // Set by index.js (Node.js) or index.browser.js (shim) — never imported directly.
8683
8685
  let evaluatorStorage = null;
8684
8686
 
@@ -8820,7 +8822,7 @@ const customFetchAdapter = async (input, init, security = {}) => {
8820
8822
  };
8821
8823
  };
8822
8824
 
8823
- const customCompileAdapter = async (src, options, parentSecurity = {}) => {
8825
+ const customCompileAdapter = async (src, options, parentSecurity = {}, parentFs = null, parentBaseDir = null) => {
8824
8826
  const maxDepth = parentSecurity?.maxDepth ?? 5;
8825
8827
  if (globalCompilationDepth >= maxDepth) {
8826
8828
  throw new Error(`Recursion Guard: Maximum Smark compilation depth exceeded (limit is ${maxDepth}).`);
@@ -8836,7 +8838,9 @@ const customCompileAdapter = async (src, options, parentSecurity = {}) => {
8836
8838
  ...cleanOptions,
8837
8839
  src,
8838
8840
  format: cleanOptions.format || "html",
8839
- security: parentSecurity
8841
+ security: parentSecurity,
8842
+ fs: parentFs ?? undefined,
8843
+ baseDir: cleanOptions.baseDir || parentBaseDir || undefined,
8840
8844
  };
8841
8845
  const sm = new compilerClass(compilerOptions);
8842
8846
  return await sm.transpile();
@@ -8977,6 +8981,7 @@ class EvaluatorState {
8977
8981
  } else {
8978
8982
  this.baseDir = "/";
8979
8983
  }
8984
+ this.rootDir = settings?.instance?.cwd || this.baseDir;
8980
8985
  this.scopes = [{}];
8981
8986
  this.dynamicTagsStack = [new Map()];
8982
8987
  this.security = security;
@@ -9017,13 +9022,25 @@ class EvaluatorState {
9017
9022
  },
9018
9023
  __hostSomMarkVersion: SomMark$1.version,
9019
9024
  __hostSomMarkSettings: () => {
9020
- const clean = { ...SomMark$1.settings };
9021
- delete clean.instance;
9022
- delete clean.fs;
9023
- return JSON.stringify(clean);
9024
- },
9025
+ const s = SomMark$1.settings;
9026
+ return JSON.stringify({
9027
+ format: s.format ?? null,
9028
+ dev: s.dev ?? false,
9029
+ removeComments:s.removeComments ?? false,
9030
+ allowRaw: s.allowRaw ?? true,
9031
+ dualOutput: s.dualOutput ?? false,
9032
+ webOutputs: s.webOutputs ?? false,
9033
+ });
9034
+ },
9025
9035
  __hostCompile: async (src, options) => {
9026
- return await customCompileAdapter(src, options, this.security);
9036
+ return await customCompileAdapter(src, options, this.security, this.nodeFs, this.baseDir);
9037
+ },
9038
+ __hostLexer: (src, filename) => {
9039
+ return JSON.stringify(lexer(src, filename || "anonymous"));
9040
+ },
9041
+ __hostParser: (src, filename) => {
9042
+ const tokens = lexer(src, filename || "anonymous");
9043
+ return JSON.stringify(parser(tokens, filename || "anonymous"));
9027
9044
  },
9028
9045
  __hostFetch: async (input, initStr) => {
9029
9046
  const init = initStr ? JSON.parse(initStr) : undefined;
@@ -9051,6 +9068,59 @@ class EvaluatorState {
9051
9068
  const payload = JSON.parse(payloadStr);
9052
9069
  return await target.render.call(this.mapperFile, payload);
9053
9070
  },
9071
+ __hostFileRead: async (filePath) => {
9072
+ if (!this.nodeFs) {
9073
+ throw new Error(
9074
+ "[SomMark] fileHandler is not available in browser mode.\n" +
9075
+ "File access is a server-side concept."
9076
+ );
9077
+ }
9078
+ const abs = posix.resolve(this.rootDir, filePath);
9079
+ if (!abs.startsWith(this.rootDir)) {
9080
+ throw new Error(
9081
+ `[SomMark] fileHandler.read: path traversal outside project root is not allowed.\n` +
9082
+ `Attempted path: ${abs}`
9083
+ );
9084
+ }
9085
+ return this.nodeFs.readFile(abs, "utf-8");
9086
+ },
9087
+ __hostFileExists: async (filePath) => {
9088
+ if (!this.nodeFs) return false;
9089
+ const abs = posix.resolve(this.rootDir, filePath);
9090
+ if (!abs.startsWith(this.rootDir)) return false;
9091
+ return this.nodeFs.exists(abs);
9092
+ },
9093
+ __hostFileGlob: async (pattern) => {
9094
+ if (!this.nodeFs) throw new Error("[SomMark] fileHandler.glob is not available in browser mode.\nFile access is a server-side concept.");
9095
+ if (!this.nodeFs.glob) throw new Error("[SomMark] fileHandler.glob requires Node.js 22 or later.");
9096
+ const files = await this.nodeFs.glob(pattern, { cwd: this.rootDir });
9097
+ return JSON.stringify(files);
9098
+ },
9099
+ __hostFileLastModified: async (filePath) => {
9100
+ if (!this.nodeFs) throw new Error("[SomMark] fileHandler.lastModified is not available in browser mode.");
9101
+ const abs = posix.resolve(this.rootDir, filePath);
9102
+ if (!abs.startsWith(this.rootDir)) throw new Error("[SomMark] fileHandler.lastModified: path traversal outside project root is not allowed.");
9103
+ const stat = await this.nodeFs.stat(abs);
9104
+ return stat.mtimeMs;
9105
+ },
9106
+ __hostFileStat: async (filePath) => {
9107
+ if (!this.nodeFs) throw new Error("[SomMark] fileHandler.stat is not available in browser mode.\nFile access is a server-side concept.");
9108
+ const abs = posix.resolve(this.rootDir, filePath);
9109
+ if (!abs.startsWith(this.rootDir)) throw new Error(`[SomMark] fileHandler.stat: path traversal outside project root is not allowed.\nAttempted path: ${abs}`);
9110
+ try {
9111
+ const s = await this.nodeFs.stat(abs);
9112
+ return JSON.stringify({
9113
+ size: s.size,
9114
+ mtime: s.mtimeMs,
9115
+ ctime: s.ctimeMs,
9116
+ atime: s.atimeMs,
9117
+ isFile: s.isFile(),
9118
+ isDirectory: s.isDirectory(),
9119
+ });
9120
+ } catch {
9121
+ return null;
9122
+ }
9123
+ },
9054
9124
  __allowRaw: this.security.allowRaw !== false
9055
9125
  });
9056
9126
 
@@ -9203,6 +9273,18 @@ class EvaluatorState {
9203
9273
  }
9204
9274
  return await __hostCompile(src, options);
9205
9275
  },
9276
+ lexer: (src, filename) => {
9277
+ if (typeof src !== "string") {
9278
+ throw new Error("SomMark.lexer Error: Source must be a string.");
9279
+ }
9280
+ return JSON.parse(__hostLexer(src, filename));
9281
+ },
9282
+ parser: (src, filename) => {
9283
+ if (typeof src !== "string") {
9284
+ throw new Error("SomMark.parser Error: Source must be a string.");
9285
+ }
9286
+ return JSON.parse(__hostParser(src, filename));
9287
+ },
9206
9288
  raw: (html) => {
9207
9289
  if (typeof __allowRaw !== "undefined" && !__allowRaw) {
9208
9290
  throw new Error("Security Error: SomMark.raw is disabled in this environment.");
@@ -9243,6 +9325,20 @@ class EvaluatorState {
9243
9325
  configurable: false
9244
9326
  });
9245
9327
 
9328
+ Object.defineProperty(globalThis, "Smark", {
9329
+ value: SomMark,
9330
+ writable: false,
9331
+ configurable: false
9332
+ });
9333
+
9334
+ globalThis.fileHandler = Object.freeze({
9335
+ read: async (path) => await __hostFileRead(path),
9336
+ exists: async (path) => await __hostFileExists(path),
9337
+ glob: async (pattern) => JSON.parse(await __hostFileGlob(pattern)),
9338
+ lastModified: async (path) => await __hostFileLastModified(path),
9339
+ stat: async (path) => { const r = await __hostFileStat(path); return r ? JSON.parse(r) : null; },
9340
+ });
9341
+
9246
9342
  delete globalThis.fetch;
9247
9343
  delete globalThis.process;
9248
9344
  `);
@@ -9254,6 +9350,13 @@ class EvaluatorState {
9254
9350
  }
9255
9351
  setupRes.value.dispose();
9256
9352
 
9353
+ const patheRes = this.context.evalCode(patheBundleCode);
9354
+ if (patheRes.error) {
9355
+ patheRes.error.dispose();
9356
+ } else {
9357
+ patheRes.value.dispose();
9358
+ }
9359
+
9257
9360
  // Configure module loader using virtual FS implementation.
9258
9361
  // The normalizer resolves every import to an absolute path so the module
9259
9362
  // cache key is always absolute — <smark> (the eval module name) can never
@@ -9468,10 +9571,17 @@ class EvaluatorState {
9468
9571
  if (!this.context) return;
9469
9572
  const safe = {};
9470
9573
  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.`);
9574
+ if (typeof value === "function") {
9575
+ const src = value.toString();
9576
+ if (src.includes("SomMark.")) {
9577
+ console.warn(`[SomMark] variables.${key}: references 'SomMark' which bundlers may rename. Use 'Smark' instead.`);
9578
+ }
9579
+ const res = this.context.evalCode(`globalThis[${JSON.stringify(key)}] = ${src}`);
9580
+ if (res.error) res.error.dispose();
9581
+ else res.value.dispose();
9473
9582
  continue;
9474
9583
  }
9584
+ if (!isPlainData(value)) continue;
9475
9585
  safe[key] = value;
9476
9586
  }
9477
9587
  const currentScope = this.scopes[this.scopes.length - 1];
@@ -9546,7 +9656,16 @@ class EvaluatorState {
9546
9656
  }
9547
9657
  }
9548
9658
  } catch (err) {
9549
- // Ignore parsing errors and fallback to raw code
9659
+ // Parse failed as a statement try as a parenthesised expression.
9660
+ // This handles object/array literals like {a: 1} or [1, 2] which are
9661
+ // ambiguous in statement context but valid when wrapped in parens.
9662
+ try {
9663
+ const trimmed = code.trim();
9664
+ parse$1(`(${trimmed})`, { ecmaVersion: 'latest', sourceType: 'module' });
9665
+ finalCode = `export default (${trimmed});`;
9666
+ } catch {
9667
+ // Give up — let QuickJS surface the error.
9668
+ }
9550
9669
  }
9551
9670
 
9552
9671
  if (autoExportedNames.length > 0 && !hasExplicitExports) {
@@ -10104,7 +10223,7 @@ function warnDroppedVariables(variables) {
10104
10223
  } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
10105
10224
  for (const [nestedKey, nestedVal] of Object.entries(value)) {
10106
10225
  if (typeof nestedVal === "function") {
10107
- 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}`);
10226
+ console.warn(`[SomMark] variables.${key}.${nestedKey}: nested functions inside objects are not supported. Define it as a top-level function instead: variables.${nestedKey}`);
10108
10227
  } else if (nestedVal === undefined) {
10109
10228
  console.warn(`[SomMark] variables.${key}.${nestedKey} is undefined and will be ignored.`);
10110
10229
  }
@@ -10211,9 +10330,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
10211
10330
  const out = (result !== undefined && typeof result !== "object") ? String(result) : "";
10212
10331
  return mapper_file ? mapper_file.text(out) : out;
10213
10332
  } catch (err) {
10333
+ const line = node.range?.start?.line + 1 || 1;
10214
10334
  transpilerError([
10215
10335
  `<$red:Logic Error:$> ${err.message}{line}`,
10216
- `<$yellow:Code:$> <$blue:${node.code}$>{line}`
10336
+ `<$yellow:Code:$> <$blue:${node.code}$>{line}`,
10337
+ `at line <$yellow:${line}$>{line}`
10217
10338
  ]);
10218
10339
  }
10219
10340
  }
@@ -10424,9 +10545,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
10424
10545
  const val = await Evaluator$1.execute(child.code, child.baseDir || null);
10425
10546
  if (val !== undefined && typeof val !== "object") richText += String(val);
10426
10547
  } catch (err) {
10548
+ const line = child.range?.start?.line + 1 || 1;
10427
10549
  transpilerError([
10428
10550
  `<$red:Logic Error:$> ${err.message}{line}`,
10429
- `<$yellow:Code:$> <$blue:${child.code}$>{line}`
10551
+ `<$yellow:Code:$> <$blue:${child.code}$>{line}`,
10552
+ `at line <$yellow:${line}$>{line}`
10430
10553
  ]);
10431
10554
  }
10432
10555
  } else if (child.type === COMMENT) {
@@ -10553,9 +10676,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
10553
10676
  bodyOutput = mapper_file ? mapper_file.text(out, { ...target?.options, escape: parentEscape }) : out;
10554
10677
  }
10555
10678
  } catch (err) {
10679
+ const line = body_node.range?.start?.line + 1 || 1;
10556
10680
  transpilerError([
10557
10681
  `<$red:Logic Error:$> ${err.message}{line}`,
10558
- `<$yellow:Code:$> <$blue:${body_node.code}$>{line}`
10682
+ `<$yellow:Code:$> <$blue:${body_node.code}$>{line}`,
10683
+ `at line <$yellow:${line}$>{line}`
10559
10684
  ]);
10560
10685
  }
10561
10686
  break;
@@ -10654,6 +10779,10 @@ async function transpiler(optionsOrAst, format, mapperFile) {
10654
10779
  })();
10655
10780
 
10656
10781
  const dualOutput = optionsOrAst?.dualOutput || false;
10782
+ const webOutputs = optionsOrAst?.webOutputs || false;
10783
+ if (webOutputs && dualOutput) {
10784
+ 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]).");
10785
+ }
10657
10786
  const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
10658
10787
  const variables = optionsOrAst?.variables || settings?.variables || {};
10659
10788
  warnDroppedVariables(variables);
@@ -10668,6 +10797,89 @@ async function transpiler(optionsOrAst, format, mapperFile) {
10668
10797
  let prev_body_node = null;
10669
10798
  let prev_was_silent = false;
10670
10799
 
10800
+ if (webOutputs) {
10801
+ // Use unique markers so [style] content is extracted precisely —
10802
+ // no <style> regex on the final HTML, works with static logic inside [style].
10803
+ const CSS_OPEN = `SOMMARKCSSOPEN${randomBytesHex(8)}SOMMARK`;
10804
+ const CSS_CLOSE = `SOMMARKCSSCLOSE${randomBytesHex(8)}SOMMARK`;
10805
+
10806
+ const webMapper = targetMapper.clone();
10807
+ webMapper.register("style", function ({ content }) {
10808
+ return `${CSS_OPEN}${content}${CSS_CLOSE}`;
10809
+ }, { escape: false });
10810
+ // [head] injects CSS variables as a raw <style> string via this.cssVariables —
10811
+ // override it so those variables go through markers too.
10812
+ webMapper.register("head", function ({ content }) {
10813
+ const varsMarker = this.cssVariables
10814
+ ? `${CSS_OPEN}:root { ${this.cssVariables} }${CSS_CLOSE}\n`
10815
+ : "";
10816
+ return this.tag("head").body(`${varsMarker}${content}`);
10817
+ }, { escape: false });
10818
+
10819
+ const idState = { mode: 'record', ids: [], idx: 0 };
10820
+
10821
+ // HTML pass — [style] blocks emit markers instead of <style> tags
10822
+ let htmlOutput = "";
10823
+ try {
10824
+ for (let i = 0; i < body.length; i++) {
10825
+ const node = body[i];
10826
+ const blockOutput = await generateOutput(body, i, targetFormat, webMapper, security, null, false, true, instance, idState);
10827
+ let finalBlockOutput = blockOutput;
10828
+ if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
10829
+ if (finalBlockOutput) {
10830
+ htmlOutput += finalBlockOutput;
10831
+ prev_was_silent = false;
10832
+ } else {
10833
+ prev_was_silent = true;
10834
+ if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
10835
+ const nextNode = body[i + 1];
10836
+ if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) i++;
10837
+ }
10838
+ }
10839
+ }
10840
+ } finally {
10841
+ Evaluator$1.destroy();
10842
+ }
10843
+
10844
+ // Extract CSS from markers — exact, no HTML regex
10845
+ const cssChunks = [];
10846
+ const markerRe = new RegExp(`${CSS_OPEN}([\\s\\S]*?)${CSS_CLOSE}`, "g");
10847
+ htmlOutput = htmlOutput.replace(markerRe, (_, chunk) => {
10848
+ cssChunks.push(chunk.trim());
10849
+ return "";
10850
+ });
10851
+ const css = cssChunks.join("\n").trim();
10852
+
10853
+ // JS pass — replay IDs so querySelector targets match HTML
10854
+ idState.mode = 'replay';
10855
+ idState.idx = 0;
10856
+ prev_was_silent = false;
10857
+
10858
+ await Evaluator$1.init(fileBaseDir, security, settings, targetMapper);
10859
+ Evaluator$1.inject(placeholders);
10860
+ Evaluator$1.inject(variables);
10861
+
10862
+ let jsOutput = "";
10863
+ try {
10864
+ for (let i = 0; i < body.length; i++) {
10865
+ const node = body[i];
10866
+ const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, true, false, instance, idState);
10867
+ let finalBlockOutput = blockOutput;
10868
+ if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
10869
+ if (finalBlockOutput) {
10870
+ jsOutput += finalBlockOutput;
10871
+ prev_was_silent = false;
10872
+ } else {
10873
+ prev_was_silent = true;
10874
+ }
10875
+ }
10876
+ } finally {
10877
+ Evaluator$1.destroy();
10878
+ }
10879
+
10880
+ return [htmlOutput.trim(), css, jsOutput.trim()];
10881
+ }
10882
+
10671
10883
  if (dualOutput) {
10672
10884
  const idState = { mode: 'record', ids: [], idx: 0 };
10673
10885
 
@@ -10780,9 +10992,11 @@ async function transpileArgs(props) {
10780
10992
  try {
10781
10993
  result[key] = await Evaluator$1.execute(value.code, value.baseDir || null);
10782
10994
  } catch (err) {
10995
+ const line = value.range?.start?.line + 1 || 1;
10783
10996
  transpilerError([
10784
10997
  `<$red:Logic Error (Argument):$> ${err.message}{line}`,
10785
- `<$yellow:Code:$> <$blue:${value.code}$>{line}`
10998
+ `<$yellow:Code:$> <$blue:${value.code}$>{line}`,
10999
+ `at line <$yellow:${line}$>{line}`
10786
11000
  ]);
10787
11001
  }
10788
11002
  } else {
@@ -13712,13 +13926,34 @@ async function resolveModules(ast, context) {
13712
13926
  let resolvedPath = filePath;
13713
13927
  for (const [prefix, replacement] of Object.entries(importAliases)) {
13714
13928
  if (filePath.startsWith(prefix)) {
13715
- resolvedPath = posix.resolve(context.instance.cwd || "/", filePath.replace(prefix, replacement));
13929
+ const replaced = filePath.replace(prefix, replacement);
13930
+ // Preserve scheme prefixes (pkg:, http:, etc.) — don't path.resolve them
13931
+ resolvedPath = replaced.startsWith("pkg:") || replaced.startsWith("http://") || replaced.startsWith("https://")
13932
+ ? replaced
13933
+ : posix.resolve(context.instance.cwd || "/", replaced);
13716
13934
  break;
13717
13935
  }
13718
13936
  }
13719
13937
 
13720
- // 1b. Resolve relative to current base (FS)
13721
- const absolutePath = resolveModulePath(resolvedPath, currentBaseDir);
13938
+ // 1b. pkg: resolve from node_modules at project root
13939
+ let absolutePath;
13940
+ if (resolvedPath.startsWith("pkg:")) {
13941
+ if (!context.instance.fs?.__isNodeFs) {
13942
+ runtimeError([`<$red:Module Error:$> <$cyan:pkg:$> imports are not supported in browser or virtual filesystem mode at line <$yellow:${node.range.start.line + 1}$>`]);
13943
+ }
13944
+ const pkgPath = resolvedPath.slice(4);
13945
+ if (!pkgPath || pkgPath.trim() === "") {
13946
+ runtimeError([`<$red:Module Error:$> <$cyan:pkg:$> path cannot be empty at line <$yellow:${node.range.start.line + 1}$>`]);
13947
+ }
13948
+ const nodeModulesRoot = posix.resolve(context.instance.cwd || "/", "node_modules");
13949
+ absolutePath = posix.resolve(nodeModulesRoot, pkgPath);
13950
+ if (!absolutePath.startsWith(nodeModulesRoot + posix.sep) && absolutePath !== nodeModulesRoot) {
13951
+ 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}$>`]);
13952
+ }
13953
+ } else {
13954
+ // 1c. Resolve relative to current base (FS)
13955
+ absolutePath = resolveModulePath(resolvedPath, currentBaseDir);
13956
+ }
13722
13957
 
13723
13958
  if (!context.instance.fs) {
13724
13959
  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.`]);
@@ -14231,7 +14466,7 @@ class SomMark {
14231
14466
  * @param {string} [options.baseDir=null] - The base directory for resolving relative paths.
14232
14467
  */
14233
14468
  constructor(options = {}) {
14234
- 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;
14469
+ 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;
14235
14470
  this.rawSettings = options;
14236
14471
  this.src = src;
14237
14472
  this.ast = ast;
@@ -14242,6 +14477,7 @@ class SomMark {
14242
14477
  this.placeholders = placeholders;
14243
14478
  this.customProps = customProps;
14244
14479
  this.dualOutput = dualOutput;
14480
+ this.webOutputs = webOutputs;
14245
14481
  this.cwd = options.baseDir || (options.files ? "/" : defaultCwd);
14246
14482
  this.fs = options.fs
14247
14483
  || (options.files ? new VirtualFS(options.files) : null)
@@ -14458,6 +14694,7 @@ class SomMark {
14458
14694
  security: this.security,
14459
14695
  settings: this.rawSettings,
14460
14696
  dualOutput: this.dualOutput,
14697
+ webOutputs: this.webOutputs,
14461
14698
  instance: this
14462
14699
  });
14463
14700
 
@@ -8917,7 +8917,7 @@ function warnDroppedVariables(variables) {
8917
8917
  } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
8918
8918
  for (const [nestedKey, nestedVal] of Object.entries(value)) {
8919
8919
  if (typeof nestedVal === "function") {
8920
- 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}`);
8921
8921
  } else if (nestedVal === undefined) {
8922
8922
  console.warn(`[SomMark] variables.${key}.${nestedKey} is undefined and will be ignored.`);
8923
8923
  }
@@ -9024,9 +9024,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9024
9024
  const out = (result !== undefined && typeof result !== "object") ? String(result) : "";
9025
9025
  return mapper_file ? mapper_file.text(out) : out;
9026
9026
  } catch (err) {
9027
+ const line = node.range?.start?.line + 1 || 1;
9027
9028
  transpilerError([
9028
9029
  `<$red:Logic Error:$> ${err.message}{line}`,
9029
- `<$yellow:Code:$> <$blue:${node.code}$>{line}`
9030
+ `<$yellow:Code:$> <$blue:${node.code}$>{line}`,
9031
+ `at line <$yellow:${line}$>{line}`
9030
9032
  ]);
9031
9033
  }
9032
9034
  }
@@ -9237,9 +9239,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9237
9239
  const val = await Evaluator.execute(child.code, child.baseDir || null);
9238
9240
  if (val !== undefined && typeof val !== "object") richText += String(val);
9239
9241
  } catch (err) {
9242
+ const line = child.range?.start?.line + 1 || 1;
9240
9243
  transpilerError([
9241
9244
  `<$red:Logic Error:$> ${err.message}{line}`,
9242
- `<$yellow:Code:$> <$blue:${child.code}$>{line}`
9245
+ `<$yellow:Code:$> <$blue:${child.code}$>{line}`,
9246
+ `at line <$yellow:${line}$>{line}`
9243
9247
  ]);
9244
9248
  }
9245
9249
  } else if (child.type === COMMENT) {
@@ -9366,9 +9370,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9366
9370
  bodyOutput = mapper_file ? mapper_file.text(out, { ...target?.options, escape: parentEscape }) : out;
9367
9371
  }
9368
9372
  } catch (err) {
9373
+ const line = body_node.range?.start?.line + 1 || 1;
9369
9374
  transpilerError([
9370
9375
  `<$red:Logic Error:$> ${err.message}{line}`,
9371
- `<$yellow:Code:$> <$blue:${body_node.code}$>{line}`
9376
+ `<$yellow:Code:$> <$blue:${body_node.code}$>{line}`,
9377
+ `at line <$yellow:${line}$>{line}`
9372
9378
  ]);
9373
9379
  }
9374
9380
  break;
@@ -9467,6 +9473,10 @@ async function transpiler(optionsOrAst, format, mapperFile) {
9467
9473
  })();
9468
9474
 
9469
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
+ }
9470
9480
  const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
9471
9481
  const variables = optionsOrAst?.variables || settings?.variables || {};
9472
9482
  warnDroppedVariables(variables);
@@ -9481,6 +9491,89 @@ async function transpiler(optionsOrAst, format, mapperFile) {
9481
9491
  let prev_body_node = null;
9482
9492
  let prev_was_silent = false;
9483
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
+
9484
9577
  if (dualOutput) {
9485
9578
  const idState = { mode: 'record', ids: [], idx: 0 };
9486
9579
 
@@ -9593,9 +9686,11 @@ async function transpileArgs(props) {
9593
9686
  try {
9594
9687
  result[key] = await Evaluator.execute(value.code, value.baseDir || null);
9595
9688
  } catch (err) {
9689
+ const line = value.range?.start?.line + 1 || 1;
9596
9690
  transpilerError([
9597
9691
  `<$red:Logic Error (Argument):$> ${err.message}{line}`,
9598
- `<$yellow:Code:$> <$blue:${value.code}$>{line}`
9692
+ `<$yellow:Code:$> <$blue:${value.code}$>{line}`,
9693
+ `at line <$yellow:${line}$>{line}`
9599
9694
  ]);
9600
9695
  }
9601
9696
  } else {
@@ -12525,13 +12620,34 @@ async function resolveModules(ast, context) {
12525
12620
  let resolvedPath = filePath;
12526
12621
  for (const [prefix, replacement] of Object.entries(importAliases)) {
12527
12622
  if (filePath.startsWith(prefix)) {
12528
- 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);
12529
12628
  break;
12530
12629
  }
12531
12630
  }
12532
12631
 
12533
- // 1b. Resolve relative to current base (FS)
12534
- 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
+ }
12535
12651
 
12536
12652
  if (!context.instance.fs) {
12537
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.`]);
@@ -13044,7 +13160,7 @@ class SomMark {
13044
13160
  * @param {string} [options.baseDir=null] - The base directory for resolving relative paths.
13045
13161
  */
13046
13162
  constructor(options = {}) {
13047
- 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;
13048
13164
  this.rawSettings = options;
13049
13165
  this.src = src;
13050
13166
  this.ast = ast;
@@ -13055,6 +13171,7 @@ class SomMark {
13055
13171
  this.placeholders = placeholders;
13056
13172
  this.customProps = customProps;
13057
13173
  this.dualOutput = dualOutput;
13174
+ this.webOutputs = webOutputs;
13058
13175
  this.cwd = options.baseDir || (options.files ? "/" : defaultCwd);
13059
13176
  this.fs = options.fs
13060
13177
  || (options.files ? new VirtualFS(options.files) : null)
@@ -13271,6 +13388,7 @@ class SomMark {
13271
13388
  security: this.security,
13272
13389
  settings: this.rawSettings,
13273
13390
  dualOutput: this.dualOutput,
13391
+ webOutputs: this.webOutputs,
13274
13392
  instance: this
13275
13393
  });
13276
13394
 
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import SomMark, { setDefaultFs, setDefaultCwd, setDefaultFindAndLoadConfig, setDefaultResolvePath, setDefaultEnv, setDefaultAsyncLocalStorage } from "./index.shared.js";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { resolve } from "pathe";
3
4
  export * from "./index.shared.js";
4
5
 
5
6
  setDefaultAsyncLocalStorage(AsyncLocalStorage);
@@ -12,6 +13,13 @@ if (typeof process !== "undefined" && process.versions?.node) {
12
13
  // Add async interface so modules.js can use await fs.exists / await fs.readFile
13
14
  nodeFs.exists = (p) => nodeFs.promises.access(p).then(() => true).catch(() => false);
14
15
  nodeFs.readFile = (p, enc) => nodeFs.promises.readFile(p, enc);
16
+ nodeFs.stat = (p) => nodeFs.promises.stat(p);
17
+ nodeFs.__isNodeFs = true;
18
+ nodeFs.glob = async (pattern, opts) => {
19
+ const results = [];
20
+ for await (const f of nodeFs.promises.glob(pattern, opts)) results.push(f);
21
+ return results;
22
+ };
15
23
  } catch (e) {}
16
24
  }
17
25
  setDefaultFs(nodeFs);
@@ -63,4 +71,66 @@ setDefaultFindAndLoadConfig(findAndLoadConfigFn);
63
71
  setDefaultEnv(mergedEnv);
64
72
  }
65
73
 
74
+ export const fileHandler = {
75
+ async read(filePath) {
76
+ if (!nodeFs) {
77
+ throw new Error(
78
+ "[SomMark] fileHandler is not available in browser mode.\n" +
79
+ "File access is a server-side concept."
80
+ );
81
+ }
82
+ const cwd = process.cwd();
83
+ const abs = resolve(cwd, filePath);
84
+ if (!abs.startsWith(cwd)) {
85
+ throw new Error(
86
+ `[SomMark] fileHandler.read: path traversal outside project root is not allowed.\n` +
87
+ `Attempted path: ${abs}`
88
+ );
89
+ }
90
+ return nodeFs.readFile(abs, "utf-8");
91
+ },
92
+ async exists(filePath) {
93
+ if (!nodeFs) return false;
94
+ const cwd = process.cwd();
95
+ const abs = resolve(cwd, filePath);
96
+ if (!abs.startsWith(cwd)) return false;
97
+ return nodeFs.exists(abs);
98
+ },
99
+ async glob(pattern) {
100
+ if (!nodeFs) throw new Error("[SomMark] fileHandler.glob is not available in browser mode.\nFile access is a server-side concept.");
101
+ if (!nodeFs.glob) throw new Error("[SomMark] fileHandler.glob requires Node.js 22 or later.");
102
+ const cwd = process.cwd();
103
+ return nodeFs.glob(pattern, { cwd });
104
+ },
105
+ async stat(filePath) {
106
+ if (!nodeFs) {
107
+ throw new Error(
108
+ "[SomMark] fileHandler.stat is not available in browser mode.\n" +
109
+ "File access is a server-side concept."
110
+ );
111
+ }
112
+ const cwd = process.cwd();
113
+ const abs = resolve(cwd, filePath);
114
+ if (!abs.startsWith(cwd)) {
115
+ throw new Error(
116
+ `[SomMark] fileHandler.stat: path traversal outside project root is not allowed.\n` +
117
+ `Attempted path: ${abs}`
118
+ );
119
+ }
120
+ try {
121
+ const s = await nodeFs.stat(abs);
122
+ return {
123
+ size: s.size,
124
+ mtime: s.mtimeMs,
125
+ ctime: s.ctimeMs,
126
+ atime: s.atimeMs,
127
+ isFile: s.isFile(),
128
+ isDirectory: s.isDirectory(),
129
+ };
130
+ } catch {
131
+ return null;
132
+ }
133
+ },
134
+ };
135
+
66
136
  export default SomMark;
package/index.shared.js CHANGED
@@ -82,7 +82,7 @@ class SomMark {
82
82
  * @param {string} [options.baseDir=null] - The base directory for resolving relative paths.
83
83
  */
84
84
  constructor(options = {}) {
85
- 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;
85
+ 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;
86
86
  this.rawSettings = options;
87
87
  this.src = src;
88
88
  this.ast = ast;
@@ -93,6 +93,7 @@ class SomMark {
93
93
  this.placeholders = placeholders;
94
94
  this.customProps = customProps;
95
95
  this.dualOutput = dualOutput;
96
+ this.webOutputs = webOutputs;
96
97
  this.cwd = options.baseDir || (options.files ? "/" : defaultCwd);
97
98
  this.fs = options.fs
98
99
  || (options.files ? new VirtualFS(options.files) : null)
@@ -309,6 +310,7 @@ class SomMark {
309
310
  security: this.security,
310
311
  settings: this.rawSettings,
311
312
  dualOutput: this.dualOutput,
313
+ webOutputs: this.webOutputs,
312
314
  instance: this
313
315
  });
314
316
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sommark",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "SomMark is a template language that compiles to multiple output formats — HTML, JSON, YAML, TOML, CSV, Markdown, XML, and more.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -39,11 +39,12 @@
39
39
  "test:run": "vitest run --pool=forks --maxWorkers=1",
40
40
  "test:watch": "vitest watch",
41
41
  "test:html": "vitest run tests/html",
42
+ "build:pathe": "node scripts/build-pathe.js",
42
43
  "build:browser": "rollup -c rollup.browser.config.js",
43
44
  "build:lite": "rollup -c rollup.browser.lite.config.js",
44
45
  "build:lexer": "rollup -c rollup.browser.lexer.config.js",
45
46
  "build:parser": "rollup -c rollup.browser.parser.config.js",
46
- "build:all": "npm run build:browser && npm run build:lite && npm run build:lexer && npm run build:parser",
47
+ "build:all": "npm run build:pathe && npm run build:browser && npm run build:lite && npm run build:lexer && npm run build:parser",
47
48
  "prepublishOnly": "npm run build:all"
48
49
  },
49
50
  "bin": {