sommark 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,12 +19,15 @@
19
19
  * @property {string} COMMA - ',' char.
20
20
  * @property {string} SEMICOLON - ';' char (At-Block separator).
21
21
  * @property {string} COMMENT - '#' comments.
22
+ * @property {string} COMMENT_BLOCK - '###' comments.
22
23
  * @property {string} ESCAPE - '\' char. Used for literalizing structural chars like '\"' or '\['.
23
24
  * @property {string} QUOTE - '"' delimiter.
25
+ * @property {string} EXCLAMATION_MARK - '!' char.
24
26
  * @property {string} IMPORT - 'import' keyword.
25
27
  * @property {string} USE_MODULE - '$use-module' keyword.
26
28
  * @property {string} PREFIX_JS - 'js{}' prefix layer.
27
29
  * @property {string} PREFIX_P - 'p{}' placeholder layer.
30
+ * @property {string} PREFIX_V - 'v{}' local variable layer.
28
31
  * @property {string} EOF - End of File indicator.
29
32
  */
30
33
  const TOKEN_TYPES = {
@@ -39,6 +42,7 @@ const TOKEN_TYPES = {
39
42
  QUOTE: "QUOTE",
40
43
  PREFIX_JS: "PREFIX_JS",
41
44
  PREFIX_P: "PREFIX_P",
45
+ PREFIX_V: "PREFIX_V",
42
46
  TEXT: "TEXT",
43
47
  THIN_ARROW: "THIN_ARROW",
44
48
  OPEN_PAREN: "OPEN_PAREN",
@@ -49,9 +53,16 @@ const TOKEN_TYPES = {
49
53
  COMMA: "COMMA",
50
54
  SEMICOLON: "SEMICOLON",
51
55
  COMMENT: "COMMENT",
56
+ COMMENT_BLOCK: "COMMENT_BLOCK",
52
57
  ESCAPE: "ESCAPE",
58
+ EXCLAMATION_MARK: "EXCLAMATION_MARK",
59
+ SLOT_KEYWORD: "SLOT_KEYWORD",
53
60
  KEY: "KEY",
54
61
  WHITESPACE: "WHITESPACE",
62
+ STATIC_KEYWORD: "STATIC_KEYWORD",
63
+ RUNTIME_KEYWORD: "RUNTIME_KEYWORD",
64
+ LOGIC: "LOGIC",
65
+ FOR_EACH: "FOR_EACH",
55
66
  EOF: "EOF"
56
67
  };
57
68
 
@@ -1,8 +1,11 @@
1
- import { BLOCK, TEXT, INLINE, ATBLOCK, COMMENT } from "./labels.js";
1
+ import crypto from "node:crypto";
2
+ import { BLOCK, TEXT, INLINE, ATBLOCK, COMMENT, COMMENT_BLOCK, STATIC_LOGIC, RUNTIME_LOGIC, FOR_EACH } from "./labels.js";
2
3
  import { transpilerError } from "./errors.js";
3
- import { textFormat, htmlFormat, markdownFormat, mdxFormat, xmlFormat } from "./formats.js";
4
+ import evaluator from "./evaluator.js";
4
5
  import { matchedValue } from "../helpers/utils.js";
5
6
  import { dedentBy } from "../helpers/dedent.js";
7
+ import { preprocessRuntimeLogic } from "./helpers/preprocessor.js";
8
+ import { wrapRuntimeLogic } from "./helpers/runtimeOutput.js";
6
9
 
7
10
  /**
8
11
  * SomMark Transpiler
@@ -10,7 +13,7 @@ import { dedentBy } from "../helpers/dedent.js";
10
13
  * using rules provided by a mapper.
11
14
  */
12
15
 
13
- const BODY_PLACEHOLDER = `SOMMARKBODYPLACEHOLDER${Math.random().toString(36).slice(2)}SOMMARK`;
16
+ const BODY_PLACEHOLDER = `SOMMARKBODYPLACEHOLDER${crypto.randomBytes(8).toString("hex")}SOMMARK`;
14
17
 
15
18
  /**
16
19
  * Extracts all plain text from a node and its children.
@@ -27,7 +30,7 @@ function getNodeText(node) {
27
30
  if (child.type === TEXT) text += child.text || "";
28
31
  else if (child.type === INLINE) text += child.value || "";
29
32
  else if (child.type === ATBLOCK) text += child.content || "";
30
- else if (child.type === BLOCK) text += getNodeText(child);
33
+ else if (child.type === BLOCK || child.type === FOR_EACH) text += getNodeText(child);
31
34
  }
32
35
  }
33
36
  return text;
@@ -43,7 +46,7 @@ function getNodeText(node) {
43
46
  * @param {Object} mapper_file - The rules for how to convert each node.
44
47
  * @returns {Promise<string>} - The final text for this node.
45
48
  */
46
- async function generateOutput(ast, i, format, mapper_file) {
49
+ async function generateOutput(ast, i, format, mapper_file, security = {}, parentId = null, generateRuntimeOutput = false, hideRuntimeOutput = false) {
47
50
  const node = Array.isArray(ast) ? ast[i] : ast;
48
51
  if (!node) return "";
49
52
 
@@ -56,28 +59,157 @@ async function generateOutput(ast, i, format, mapper_file) {
56
59
  mapper_file.options.filename = node.args?.filename || oldFilename;
57
60
  let bodyOutput = "";
58
61
  if (node.body) {
62
+ evaluator.pushScope();
59
63
  for (let j = 0; j < node.body.length; j++) {
60
- bodyOutput += await generateOutput(node.body, j, format, mapper_file);
64
+ bodyOutput += await generateOutput(node.body, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput);
61
65
  }
66
+ await evaluator.popScope();
62
67
  }
63
68
  mapper_file.options.filename = oldFilename;
64
69
  return bodyOutput;
65
70
  }
66
71
 
67
72
  if (node.type === TEXT) {
73
+ if (generateRuntimeOutput) return "";
68
74
  const text = String(node.text || "");
69
75
  return mapper_file ? mapper_file.text(text) : text;
70
76
  }
71
77
 
72
78
  if (node.type === COMMENT) {
73
- if (mapper_file?.options?.removeComments) return "";
74
- const cleanComment = String(node.text || "").replace(/^#/, "").trim();
75
- return " ".repeat(node.depth) + `${mapper_file?.comment(cleanComment) || ""}`;
79
+ if (generateRuntimeOutput || mapper_file?.options?.removeComments) return "";
80
+ return " ".repeat(node.depth) + `${mapper_file?.comment(node.text) || ""}`;
76
81
  }
77
82
 
78
- let target = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
79
- if (!target && mapper_file) {
80
- target = mapper_file.getUnknownTag(node);
83
+ if (node.type === COMMENT_BLOCK) {
84
+ if (generateRuntimeOutput || mapper_file?.options?.removeComments) return "";
85
+ return " ".repeat(node.depth) + `${mapper_file?.commentBlock(node.text) || ""}`;
86
+ }
87
+
88
+ if (node.type === RUNTIME_LOGIC) {
89
+ const preprocessed = await preprocessRuntimeLogic(node.code, mapper_file?.options?.filename, security);
90
+ if (hideRuntimeOutput) {
91
+ return "";
92
+ }
93
+ if (generateRuntimeOutput) {
94
+ return wrapRuntimeLogic(preprocessed, format, parentId, node.depth === 1);
95
+ }
96
+ return mapper_file ? mapper_file.runtimeLogic(preprocessed, node.depth === 1, parentId) : "";
97
+ }
98
+
99
+ if (node.type === STATIC_LOGIC) {
100
+ try {
101
+ const result = await evaluator.execute(node.code);
102
+ if (generateRuntimeOutput) return "";
103
+ if (result && typeof result === "object" && result.__raw !== undefined) {
104
+ if (security?.allowRaw === false) {
105
+ return mapper_file ? mapper_file.text(String(result.__raw)) : String(result.__raw);
106
+ }
107
+ const rawVal = String(result.__raw);
108
+ return (security?.sanitize && typeof security.sanitize === "function") ? security.sanitize(rawVal) : rawVal;
109
+ }
110
+ // Hide objects (like module exports) from the final output
111
+ const out = (result !== undefined && typeof result !== "object") ? String(result) : "";
112
+ return mapper_file ? mapper_file.text(out) : out;
113
+ } catch (err) {
114
+ transpilerError([
115
+ `<$red:Logic Error:$> ${err.message}{line}`,
116
+ `<$yellow:Code:$> <$blue:${node.code}$>{line}`
117
+ ]);
118
+ }
119
+ }
120
+
121
+ if (node.type === FOR_EACH) {
122
+ const transpiledArgs = await transpileArgs(node.args);
123
+ const items = mapper_file ? mapper_file.safeArg({ args: transpiledArgs, index: 0, key: "items", fallBack: [] }) : [];
124
+
125
+ if (!Array.isArray(items)) {
126
+ const line = node.range?.start?.line + 1 || 1;
127
+ transpilerError([
128
+ `<$red:Type Error in [for-each]:$>{line}`,
129
+ `Expected an <$green:Array$> for 'items', but received <$yellow:${typeof items}$>:<$cyan: ${JSON.stringify(items)}$>{line}`,
130
+ `at line <$yellow:${line}$>{line}`
131
+ ]);
132
+ return "";
133
+ }
134
+
135
+ const asVar = transpiledArgs.as || "item";
136
+ const indexVar = `${asVar}_index`;
137
+
138
+ // Trim structural whitespace/newlines at start and end of loop body for formatting clean output
139
+ let cleanedBody = [];
140
+ if (node.body) {
141
+ cleanedBody = [...node.body];
142
+
143
+ // Trim ALL leading pure-whitespace Text nodes
144
+ while (cleanedBody.length > 0 && cleanedBody[0].type === TEXT && /^\s*$/.test(cleanedBody[0].text)) {
145
+ cleanedBody.shift();
146
+ }
147
+ // If the now-first node is a Text node, trim its leading whitespace/newlines
148
+ if (cleanedBody.length > 0 && cleanedBody[0].type === TEXT) {
149
+ cleanedBody[0] = { ...cleanedBody[0], text: cleanedBody[0].text.replace(/^\s+/, "") };
150
+ }
151
+
152
+ // Trim ALL trailing pure-whitespace Text nodes
153
+ while (cleanedBody.length > 0 && cleanedBody[cleanedBody.length - 1].type === TEXT && /^\s*$/.test(cleanedBody[cleanedBody.length - 1].text)) {
154
+ cleanedBody.pop();
155
+ }
156
+ // If the now-last node is a Text node, trim its trailing whitespace/newlines
157
+ if (cleanedBody.length > 0 && cleanedBody[cleanedBody.length - 1].type === TEXT) {
158
+ cleanedBody[cleanedBody.length - 1] = { ...cleanedBody[cleanedBody.length - 1], text: cleanedBody[cleanedBody.length - 1].text.replace(/\s+$/, "") };
159
+ }
160
+ }
161
+
162
+ let output = "";
163
+ let idx = 0;
164
+ for (const item of items) {
165
+ evaluator.pushScope();
166
+ evaluator.inject({
167
+ [asVar]: item,
168
+ [indexVar]: idx++
169
+ });
170
+
171
+ for (let j = 0; j < cleanedBody.length; j++) {
172
+ output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput);
173
+ }
174
+
175
+ await evaluator.popScope();
176
+ }
177
+ return output;
178
+ }
179
+
180
+ let secretId = null;
181
+ if (node.type === BLOCK) {
182
+ if (node.args) {
183
+ for (const key of Object.keys(node.args)) {
184
+ if (key.toLowerCase().startsWith("data-sommark")) {
185
+ transpilerError([
186
+ `<$red:Reserved Attribute Error:$> The attribute name '<$yellow:${key}$>' is reserved for SomMark's internal runtime compiler logic.{line}`,
187
+ `Please use a different attribute name.`
188
+ ]);
189
+ }
190
+ }
191
+ }
192
+
193
+ const hasRuntime = node.body?.some(child => child.type === RUNTIME_LOGIC);
194
+ if (hasRuntime) {
195
+ secretId = `sommark-${node.id.toLowerCase()}-${crypto.randomBytes(4).toString("hex")}`;
196
+ }
197
+ }
198
+
199
+ let target = null;
200
+ if (evaluator.active && evaluator.active.hasDynamicTag(node.id)) {
201
+ target = {
202
+ id: node.id,
203
+ options: evaluator.active.getDynamicTagOptions(node.id) || {},
204
+ render: async function (payload) {
205
+ return await evaluator.active.executeDynamicTag(node.id, payload);
206
+ }
207
+ };
208
+ } else {
209
+ target = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
210
+ if (!target && mapper_file) {
211
+ target = mapper_file.getUnknownTag(node);
212
+ }
81
213
  }
82
214
 
83
215
  if (target) {
@@ -94,6 +226,11 @@ async function generateOutput(ast, i, format, mapper_file) {
94
226
  content = mapper_file ? mapper_file.inlineText(content, target.options) : content;
95
227
  }
96
228
 
229
+ if (node.type === ATBLOCK) {
230
+ content = String(content || "");
231
+ content = mapper_file ? mapper_file.atBlockBody(content, target.options) : content;
232
+ }
233
+
97
234
  // 1. Determine if this is a parent block that needs newline wrapping (Trim-and-Wrap)
98
235
  // Priority: Target options > Mapper global options
99
236
  const effectiveTrimAndWrap = (target.options?.trimAndWrapBlocks !== undefined)
@@ -109,23 +246,59 @@ async function generateOutput(ast, i, format, mapper_file) {
109
246
 
110
247
  if (shouldResolveImmediate && node.body) {
111
248
  let resolvedBody = "";
249
+ evaluator.pushScope();
112
250
  for (let j = 0; j < node.body.length; j++) {
113
- resolvedBody += await generateOutput(node.body, j, format, mapper_file);
251
+ resolvedBody += await generateOutput(node.body, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput);
114
252
  }
115
- content = resolvedBody;
253
+ await evaluator.popScope();
254
+ content = dedentBy(resolvedBody, node.range?.start?.character || 0);
116
255
  }
117
256
 
118
- result += await target.render.call(mapper_file, { nodeType: node.type, args: node.args, content, textContent, ast: node });
119
- if (isParentBlock) result = "\n" + result + "\n";
257
+ if (generateRuntimeOutput) {
258
+ let childrenOutput = "";
259
+ if (node.body) {
260
+ for (let j = 0; j < node.body.length; j++) {
261
+ childrenOutput += await generateOutput(node.body, j, format, mapper_file, security, secretId || parentId, generateRuntimeOutput, hideRuntimeOutput);
262
+ }
263
+ }
264
+ return childrenOutput;
265
+ }
266
+
267
+ const isManualMode = target.options?.handleAst === true;
268
+
269
+ const transpiledArgs = await transpileArgs(node.args);
270
+ if (secretId) {
271
+ transpiledArgs["data-sommark-id"] = secretId;
272
+ }
273
+ result += await target.render.call(mapper_file, {
274
+ nodeType: node.type,
275
+ args: transpiledArgs,
276
+ content,
277
+ textContent,
278
+ ast: isManualMode ? node : new Proxy({}, {
279
+ get(target, prop) {
280
+ if (prop === "then" || prop === "toJSON" || typeof prop === "symbol" || prop === "constructor" || prop === "inspect" || prop === "valueOf" || prop === "toString") {
281
+ return undefined;
282
+ }
283
+ transpilerError([
284
+ `<$red:Access Error:$> Attempted to access '<$yellow:ast.${String(prop)}$>', but '<$yellow:ast$>' is undefined because '<$cyan:handleAst$>' is false or not specified in this tag's registration options.{N}{N}`,
285
+ `Please set '<$green:handleAst: true$>' in the options object of your tag registration to get the actual AST node.`
286
+ ]);
287
+ }
288
+ }),
289
+ isSelfClosing: node.type === BLOCK ? (node.isSelfClosing || false) : undefined
290
+ });
291
+ // if (isParentBlock) result = "\n" + result;
120
292
 
121
293
  if (shouldResolveImmediate) {
122
294
  return result;
123
295
  }
124
296
 
125
- const isManualMode = target.options?.handleAst === true;
126
-
127
297
  if (!isManualMode && node.body) {
128
298
  let prev_body_node = null;
299
+ let prev_was_silent = false;
300
+ const parentEscape = (security?.allowRaw === false) ? true : (target.options?.escape !== false);
301
+ evaluator.pushScope();
129
302
  for (let j = 0; j < node.body.length; j++) {
130
303
  const body_node = node.body[j];
131
304
  let bodyOutput = "";
@@ -135,7 +308,11 @@ async function generateOutput(ast, i, format, mapper_file) {
135
308
  const text = String(body_node.text || "");
136
309
  // Dedent text relative to the parent block's indentation
137
310
  const localDedentedText = dedentBy(text, node.range?.start?.character || 0);
138
- bodyOutput = mapper_file ? mapper_file.text(localDedentedText, target?.options) : localDedentedText;
311
+ let bodyTextVal = mapper_file ? mapper_file.text(localDedentedText, { ...target?.options, escape: parentEscape }) : localDedentedText;
312
+ if (parentEscape === false && security?.sanitize && typeof security.sanitize === "function") {
313
+ bodyTextVal = security.sanitize(bodyTextVal);
314
+ }
315
+ bodyOutput = bodyTextVal;
139
316
  break;
140
317
 
141
318
  case INLINE:
@@ -175,54 +352,95 @@ async function generateOutput(ast, i, format, mapper_file) {
175
352
  }
176
353
 
177
354
  // Removed multiline injection since atBlockBody handles formatting
355
+ const transpiledAtArgs = await transpileArgs(body_node.args);
178
356
  bodyOutput = atTarget
179
- ? await atTarget.render.call(mapper_file, { nodeType: body_node.type, args: body_node.args, content: atContent, ast: body_node })
357
+ ? await atTarget.render.call(mapper_file, { nodeType: body_node.type, args: transpiledAtArgs, content: atContent, ast: body_node })
180
358
  : atContent;
181
359
  break;
182
360
 
183
361
  case COMMENT:
184
362
  if (mapper_file?.options?.removeComments) break;
185
- const cleanComment = String(body_node.text || "").replace(/^#/, "").trim();
186
- bodyOutput = " ".repeat(body_node.depth) + `${mapper_file.comment(cleanComment)}`;
363
+ bodyOutput = " ".repeat(body_node.depth) + `${mapper_file.comment(body_node.text)}`;
364
+ break;
365
+
366
+ case COMMENT_BLOCK:
367
+ if (mapper_file?.options?.removeComments) break;
368
+ bodyOutput = " ".repeat(body_node.depth) + `${mapper_file.commentBlock(body_node.text)}`;
187
369
  break;
188
370
 
371
+ case FOR_EACH:
189
372
  case BLOCK:
190
- bodyOutput = await generateOutput(body_node, 0, format, mapper_file);
373
+ bodyOutput = await generateOutput(body_node, 0, format, mapper_file, security, secretId || parentId, generateRuntimeOutput, hideRuntimeOutput);
191
374
  break;
375
+
376
+ case RUNTIME_LOGIC:
377
+ const preprocessedBody = await preprocessRuntimeLogic(body_node.code, mapper_file?.options?.filename, security);
378
+ if (hideRuntimeOutput) {
379
+ bodyOutput = "";
380
+ } else {
381
+ bodyOutput = mapper_file ? mapper_file.runtimeLogic(preprocessedBody, body_node.depth === 1, secretId || parentId) : "";
382
+ }
383
+ break;
384
+
385
+ case STATIC_LOGIC:
386
+ try {
387
+ const result = await evaluator.execute(body_node.code);
388
+ if (result && typeof result === "object" && result.__raw !== undefined) {
389
+ if (security?.allowRaw === false) {
390
+ bodyOutput = mapper_file ? mapper_file.text(String(result.__raw)) : String(result.__raw);
391
+ } else {
392
+ const rawVal = String(result.__raw);
393
+ bodyOutput = (security?.sanitize && typeof security.sanitize === "function") ? security.sanitize(rawVal) : rawVal;
394
+ }
395
+ } else {
396
+ const out = (result !== undefined && typeof result !== "object") ? String(result) : "";
397
+ bodyOutput = mapper_file ? mapper_file.text(out, { ...target?.options, escape: parentEscape }) : out;
398
+ }
399
+ } catch (err) {
400
+ transpilerError([
401
+ `<$red:Logic Error:$> ${err.message}{line}`,
402
+ `<$yellow:Code:$> <$blue:${body_node.code}$>{line}`
403
+ ]);
404
+ }
405
+ break;
406
+ }
407
+
408
+ if (prev_was_silent && body_node.type === TEXT) {
409
+ bodyOutput = bodyOutput.replace(/^\n/, "");
192
410
  }
193
411
 
194
412
  if (bodyOutput) {
195
413
  context += bodyOutput;
414
+ prev_was_silent = false;
415
+ } else {
416
+ prev_was_silent = true;
196
417
  }
197
418
  }
419
+ await evaluator.popScope();
198
420
  }
199
421
 
200
422
  const finalContext = effectiveTrimAndWrap ? context.replace(/^\s*[\r\n]+|[\r\n]+\s*$/g, "") : context;
201
423
 
202
424
  if (result.includes(BODY_PLACEHOLDER)) {
203
- result = result.replaceAll(BODY_PLACEHOLDER, finalContext);
204
- } else if (finalContext.trim()) {
205
- result += finalContext;
206
- }
207
- }
208
- else if (format === textFormat) {
209
- if (node.type === ATBLOCK) {
210
- result = node.content || "";
211
- } else if (node.type === INLINE) {
212
- result = node.value || "";
213
- } else if (node.body) {
214
- for (const body_node of node.body) {
215
- switch (body_node.type) {
216
- case TEXT: context += body_node.text || ""; break;
217
- case INLINE: context += (body_node.value || "") + " "; break;
218
- case ATBLOCK: context += (body_node.content || "").trimEnd() + "\n"; break;
219
- case BLOCK:
220
- const textBlockOutput = await generateOutput(body_node, 0, format, mapper_file);
221
- context = context.trim() ? context.trimEnd() + "\n" + textBlockOutput : context + textBlockOutput;
222
- break;
223
- }
425
+ if (finalContext === "") {
426
+ result = result
427
+ .replaceAll(`\n${BODY_PLACEHOLDER}\n`, "")
428
+ .replaceAll(`\r\n${BODY_PLACEHOLDER}\r\n`, "")
429
+ .replaceAll(BODY_PLACEHOLDER, "");
430
+ } else {
431
+ result = result.replaceAll(BODY_PLACEHOLDER, finalContext);
432
+ }
433
+ } else {
434
+ if (result.toLowerCase().includes(BODY_PLACEHOLDER.toLowerCase())) {
435
+ transpilerError([
436
+ `{line}<$red:Placeholder Corruption Error:$> Attempted to modify the '<$yellow:content$>' placeholder under '<$cyan:resolve: false$>' mode in tag '<$blue:${node.id}$>'.{line}`,
437
+ `This corrupts SomMark's internal compilation tokens and is not allowed.{line}`,
438
+ `If you need to read or alter the literal inner text, please use '<$green:textContent$>' instead.{line}`
439
+ ]);
440
+ }
441
+ if (finalContext.trim()) {
442
+ result += finalContext;
224
443
  }
225
- result += context;
226
444
  }
227
445
  } else {
228
446
  transpilerError([
@@ -247,6 +465,7 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
247
465
  let body = null;
248
466
  let targetFormat = format;
249
467
  let targetMapper = mapperFile;
468
+ const security = (optionsOrAst && optionsOrAst.security) ? optionsOrAst.security : {};
250
469
 
251
470
  if (typeof optionsOrAst === "object" && !Array.isArray(optionsOrAst) && (optionsOrAst.ast || Array.isArray(optionsOrAst))) {
252
471
  if (optionsOrAst.ast) {
@@ -263,28 +482,91 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
263
482
 
264
483
  if (!body || !Array.isArray(body)) return "";
265
484
 
485
+ const settings = optionsOrAst?.settings || { format: targetFormat || "html" };
486
+
487
+ // Initialize Logic Sandbox
488
+ await evaluator.init(null, security, settings, targetMapper);
489
+ // Inject global data
490
+ const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
491
+ const variables = optionsOrAst?.variables || settings?.variables || {};
492
+ evaluator.inject(placeholders);
493
+ evaluator.inject(variables);
494
+
266
495
  let output = "";
267
496
  let prev_body_node = null;
268
- for (let i = 0; i < body.length; i++) {
269
- const node = body[i];
270
- const blockOutput = await generateOutput(body, i, targetFormat, targetMapper);
271
-
272
- if (blockOutput) {
273
- output += blockOutput;
274
- if (node.type !== TEXT || node.text.trim().length > 0) {
275
- prev_body_node = node;
497
+ let prev_was_silent = false;
498
+ const generateRuntimeOutput = optionsOrAst?.generateRuntimeOutput || false;
499
+ const hideRuntimeOutput = optionsOrAst?.hideRuntimeOutput || false;
500
+ try {
501
+ for (let i = 0; i < body.length; i++) {
502
+ const node = body[i];
503
+ const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, generateRuntimeOutput, hideRuntimeOutput);
504
+
505
+ let finalBlockOutput = blockOutput;
506
+ if (prev_was_silent && node.type === TEXT) {
507
+ finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
276
508
  }
277
- } else if (node.type === COMMENT && targetMapper?.options?.removeComments) {
278
- // If a comment is removed, check the next node.
279
- // If it's just a blank line, skip it so we don't have extra gaps in the output.
280
- const nextNode = body[i + 1];
281
- if (nextNode && nextNode.type === TEXT && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
282
- i++; // Skip the next newline node
509
+
510
+ if (finalBlockOutput) {
511
+ output += finalBlockOutput;
512
+ prev_was_silent = false;
513
+ if (node.type !== TEXT || node.text.trim().length > 0) {
514
+ prev_body_node = node;
515
+ }
516
+ } else {
517
+ prev_was_silent = true;
518
+ if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
519
+ // If a comment is removed, check the next node.
520
+ // If it's just a blank line, skip it so we don't have extra gaps in the output.
521
+ const nextNode = body[i + 1];
522
+ if (nextNode && nextNode.type === TEXT && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
523
+ i++; // Skip the next newline node
524
+ }
525
+ }
283
526
  }
284
527
  }
528
+ } finally {
529
+ evaluator.destroy();
285
530
  }
286
531
 
287
532
  return output.trim();
288
533
  }
289
534
 
535
+ /**
536
+ * Transpiles block arguments, resolving logic or variables.
537
+ */
538
+ async function transpileArgs(args) {
539
+ const result = {};
540
+ if (!args) return result;
541
+
542
+ for (const [key, value] of Object.entries(args)) {
543
+ if (key.toLowerCase().startsWith("data-sommark") && key.toLowerCase() !== "data-sommark-id") {
544
+ transpilerError([
545
+ `<$red:Reserved Attribute Error:$> The attribute name '<$yellow:${key}$>' is reserved for SomMark's internal runtime compiler logic.{line}`,
546
+ `Please use a different attribute name.`
547
+ ]);
548
+ }
549
+ if (value && typeof value === "object") {
550
+ if (value.type === RUNTIME_LOGIC) {
551
+ // Discard runtime logic for security
552
+ result[key] = "";
553
+ } else if (value.type === STATIC_LOGIC) {
554
+ try {
555
+ result[key] = await evaluator.execute(value.code);
556
+ } catch (err) {
557
+ transpilerError([
558
+ `<$red:Logic Error (Argument):$> ${err.message}{line}`,
559
+ `<$yellow:Code:$> <$blue:${value.code}$>{line}`
560
+ ]);
561
+ }
562
+ } else {
563
+ result[key] = value;
564
+ }
565
+ } else {
566
+ result[key] = value;
567
+ }
568
+ }
569
+ return result;
570
+ }
571
+
290
572
  export default transpiler;