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 +131 -11
- package/core/helpers/lib.js +1 -1
- package/core/modules.js +24 -3
- package/core/pathe-bundle.js +1 -0
- package/core/transpiler.js +100 -5
- package/dist/sommark.browser.js +258 -21
- package/dist/sommark.browser.lite.js +127 -9
- package/index.js +70 -0
- package/index.shared.js +3 -1
- package/package.json +3 -2
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
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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 (
|
|
797
|
-
|
|
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
|
-
//
|
|
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) {
|
package/core/helpers/lib.js
CHANGED
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
|
-
|
|
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.
|
|
305
|
-
|
|
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";
|
package/core/transpiler.js
CHANGED
|
@@ -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}
|
|
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 {
|
package/dist/sommark.browser.js
CHANGED
|
@@ -8628,7 +8628,7 @@ function registerHostSettings(settings) {
|
|
|
8628
8628
|
hostSettings = settings || {};
|
|
8629
8629
|
}
|
|
8630
8630
|
|
|
8631
|
-
const version = "5.
|
|
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
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
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 (
|
|
9472
|
-
|
|
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
|
-
//
|
|
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}
|
|
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
|
-
|
|
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.
|
|
13721
|
-
|
|
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}
|
|
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
|
-
|
|
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.
|
|
12534
|
-
|
|
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.
|
|
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": {
|