sommark 5.0.5 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -31,6 +31,7 @@ const randomBytesHex = (size) => {
31
31
 
32
32
  const BODY_PLACEHOLDER = `SOMMARKBODYPLACEHOLDER${randomBytesHex(8)}SOMMARK`;
33
33
 
34
+
34
35
  /**
35
36
  * Extracts all plain text from a node and its children.
36
37
  *
@@ -120,15 +121,28 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
120
121
  const out = (result !== undefined && typeof result !== "object") ? String(result) : "";
121
122
  return mapper_file ? mapper_file.text(out) : out;
122
123
  } catch (err) {
124
+ const line = node.range?.start?.line + 1 || 1;
123
125
  transpilerError([
124
126
  `<$red:Logic Error:$> ${err.message}{line}`,
125
- `<$yellow:Code:$> <$blue:${node.code}$>{line}`
127
+ `<$yellow:Code:$> <$blue:${node.code}$>{line}`,
128
+ `at line <$yellow:${line}$>{line}`
126
129
  ]);
127
130
  }
128
131
  }
129
132
 
130
133
  if (node.type === FOR_EACH) {
131
134
  const transpiledArgs = await transpileArgs(node.props);
135
+
136
+ if (!node.props || (node.props[0] === undefined && node.props["items"] === undefined)) {
137
+ const line = node.range?.start?.line + 1 || 1;
138
+ transpilerError([
139
+ `<$red:Missing Prop Error in [for-each]:$>{line}`,
140
+ `[for-each] requires an array as its first prop, e.g. [for-each = \${ array }\$]{line}`,
141
+ `at line <$yellow:${line}$>{line}`
142
+ ]);
143
+ return "";
144
+ }
145
+
132
146
  const items = mapper_file ? mapper_file.safeArg({ props: transpiledArgs, index: 0, key: "items", fallBack: [] }) : [];
133
147
 
134
148
  if (!Array.isArray(items)) {
@@ -142,11 +156,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
142
156
  }
143
157
 
144
158
  const asVar = transpiledArgs.as || "value";
145
- if (asVar === "i") {
159
+ if (asVar === "i" || asVar === "length") {
146
160
  const line = node.range?.start?.line + 1 || 1;
147
161
  transpilerError([
148
162
  `<$red:Reserved Variable Error in [for-each]:$>{line}`,
149
- `'i' is a reserved variable name for the loop index.{N}Use a different name for the 'as' prop, e.g. as: "item"{line}`,
163
+ `'${asVar}' is a reserved variable name.{N}Use a different name for the 'as' prop, e.g. as: "item"{line}`,
150
164
  `at line <$yellow:${line}$>{line}`
151
165
  ]);
152
166
  return "";
@@ -176,22 +190,28 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
176
190
  }
177
191
  }
178
192
 
179
- let output = "";
193
+ const rawJoin = transpiledArgs.join ?? null;
194
+ const join = rawJoin !== null ? rawJoin.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\r/g, "\r") : null;
195
+ const parts = [];
180
196
  let idx = 0;
197
+ const length = items.length;
181
198
  for (const item of items) {
182
199
  evaluator.pushScope();
183
200
  evaluator.inject({
184
201
  [asVar]: item,
185
- i: idx++
202
+ i: idx++,
203
+ length
186
204
  });
187
205
 
206
+ let iterOutput = "";
188
207
  for (let j = 0; j < cleanedBody.length; j++) {
189
- output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState, extraCtx);
208
+ iterOutput += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState, extraCtx);
190
209
  }
191
210
 
192
211
  await evaluator.popScope();
212
+ parts.push(iterOutput);
193
213
  }
194
- return output;
214
+ return join !== null ? parts.join(join) : parts.join("");
195
215
  }
196
216
 
197
217
  let secretId = null;
@@ -219,13 +239,12 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
219
239
  }
220
240
 
221
241
  // smark-raw block — body collected verbatim by lexer, bypasses normal body processing pipeline
222
- if (node.type === BLOCK && (node.props?.["smark-raw"] === "true" || node.props?.["smark-raw"] === true)) {
242
+ if (node.type === BLOCK && (node.directives?.raw === "true" || node.directives?.raw === true)) {
223
243
  if (generateRuntimeOutput) return "";
224
244
  const rawContent = node.body?.map(n => String(n.text || "")).join("") || "";
225
- const { "smark-raw": _, ...cleanArgs } = node.props;
226
- const transpiledArgs = await transpileArgs(cleanArgs);
245
+ const transpiledArgs = await transpileArgs(node.props);
227
246
  if (evaluator.active?.hasDynamicTag?.(node.id)) {
228
- return await evaluator.active.executeDynamicTag(node.id, { props: transpiledArgs, content: rawContent, textContent: rawContent });
247
+ return await evaluator.active.executeDynamicTag(node.id, { props: transpiledArgs, directives: node.directives, content: rawContent, textContent: rawContent });
229
248
  }
230
249
  let rawTarget = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
231
250
  if (!rawTarget && mapper_file) rawTarget = mapper_file.getUnknownTag(node);
@@ -233,6 +252,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
233
252
  const isManualMode = !!rawTarget.options?.handleAst;
234
253
  return await rawTarget.render.call(mapper_file, {
235
254
  props: transpiledArgs,
255
+ directives: node.directives,
236
256
  content: rawContent,
237
257
  textContent: rawContent,
238
258
  ast: isManualMode ? node : undefined,
@@ -316,9 +336,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
316
336
  const val = await evaluator.execute(child.code, child.baseDir || null);
317
337
  if (val !== undefined && typeof val !== "object") richText += String(val);
318
338
  } catch (err) {
339
+ const line = child.range?.start?.line + 1 || 1;
319
340
  transpilerError([
320
341
  `<$red:Logic Error:$> ${err.message}{line}`,
321
- `<$yellow:Code:$> <$blue:${child.code}$>{line}`
342
+ `<$yellow:Code:$> <$blue:${child.code}$>{line}`,
343
+ `at line <$yellow:${line}$>{line}`
322
344
  ]);
323
345
  }
324
346
  } else if (child.type === COMMENT) {
@@ -344,6 +366,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
344
366
 
345
367
  return await target.render.call(mapper_file, {
346
368
  props: transpiledArgs,
369
+ directives: node.directives,
347
370
  content: "",
348
371
  textContent: richText || textContent,
349
372
  ast: cleanAst,
@@ -362,6 +385,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
362
385
  }
363
386
  result += await target.render.call(mapper_file, {
364
387
  props: transpiledArgs,
388
+ directives: node.directives,
365
389
  content,
366
390
  textContent,
367
391
  ast: new Proxy({}, {
@@ -444,9 +468,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
444
468
  bodyOutput = mapper_file ? mapper_file.text(out, { ...target?.options, escape: parentEscape }) : out;
445
469
  }
446
470
  } catch (err) {
471
+ const line = body_node.range?.start?.line + 1 || 1;
447
472
  transpilerError([
448
473
  `<$red:Logic Error:$> ${err.message}{line}`,
449
- `<$yellow:Code:$> <$blue:${body_node.code}$>{line}`
474
+ `<$yellow:Code:$> <$blue:${body_node.code}$>{line}`,
475
+ `at line <$yellow:${line}$>{line}`
450
476
  ]);
451
477
  }
452
478
  break;
@@ -545,6 +571,10 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
545
571
  })();
546
572
 
547
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
+ }
548
578
  const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
549
579
  const variables = optionsOrAst?.variables || settings?.variables || {};
550
580
  warnDroppedVariables(variables);
@@ -559,6 +589,89 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
559
589
  let prev_body_node = null;
560
590
  let prev_was_silent = false;
561
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
+
562
675
  if (dualOutput) {
563
676
  const idState = { mode: 'record', ids: [], idx: 0 };
564
677
 
@@ -671,9 +784,11 @@ async function transpileArgs(props) {
671
784
  try {
672
785
  result[key] = await evaluator.execute(value.code, value.baseDir || null);
673
786
  } catch (err) {
787
+ const line = value.range?.start?.line + 1 || 1;
674
788
  transpilerError([
675
789
  `<$red:Logic Error (Argument):$> ${err.message}{line}`,
676
- `<$yellow:Code:$> <$blue:${value.code}$>{line}`
790
+ `<$yellow:Code:$> <$blue:${value.code}$>{line}`,
791
+ `at line <$yellow:${line}$>{line}`
677
792
  ]);
678
793
  }
679
794
  } else {
package/core/validator.js CHANGED
@@ -55,10 +55,7 @@ const runValidations = (node, target, instance) => {
55
55
  const isStructural = node.type === "Block";
56
56
  if (isStructural && rules.required_args && Array.isArray(rules.required_args)) {
57
57
  const missingArgs = rules.required_args.filter(arg => {
58
- // Check if the argument exists in named args or as a positional arg (if arg is a number)
59
- if (typeof arg === "number") {
60
- return node.props[arg] === undefined;
61
- }
58
+ if (typeof arg === "number") return node.props[arg] === undefined;
62
59
  return node.props[arg] === undefined;
63
60
  });
64
61
 
@@ -73,6 +70,22 @@ const runValidations = (node, target, instance) => {
73
70
  );
74
71
  }
75
72
  }
73
+
74
+ // -- Directives Validation (Required Directives) ----------------------- //
75
+ if (isStructural && rules.required_directives && Array.isArray(rules.required_directives)) {
76
+ const missingDirectives = rules.required_directives.filter(key => node.directives?.[key] === undefined);
77
+
78
+ if (missingDirectives.length > 0) {
79
+ transpilerError(
80
+ [
81
+ "{N}",
82
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is missing required directive props:$> <$red:${missingDirectives.map(k => `smark-${k}`).join(", ")}$>{N}`,
83
+ `<$blue:Please ensure these directive props are provided in the template usage.$>`
84
+ ],
85
+ context
86
+ );
87
+ }
88
+ }
76
89
  };
77
90
 
78
91
  /**