sommark 4.5.2 → 5.0.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.
Files changed (42) hide show
  1. package/README.md +314 -178
  2. package/cli/cli.mjs +1 -1
  3. package/cli/commands/color.js +36 -14
  4. package/cli/commands/help.js +3 -0
  5. package/cli/commands/init.js +0 -2
  6. package/cli/constants.js +5 -2
  7. package/constants/html_props.js +66 -1
  8. package/constants/svg_elements.js +31 -0
  9. package/core/errors.js +5 -4
  10. package/core/evaluator.js +1 -2
  11. package/core/formats.js +7 -1
  12. package/core/helpers/config-loader.js +1 -3
  13. package/core/helpers/lib.js +1 -1
  14. package/core/labels.js +2 -15
  15. package/core/lexer.js +197 -313
  16. package/core/modules.js +13 -13
  17. package/core/parser.js +226 -535
  18. package/core/tokenTypes.js +6 -15
  19. package/core/transpiler.js +129 -110
  20. package/core/validator.js +6 -26
  21. package/dist/sommark.browser.js +1939 -2223
  22. package/dist/sommark.browser.lite.js +1937 -2220
  23. package/dist/sommark.lexer.js +392 -544
  24. package/dist/sommark.parser.js +604 -1200
  25. package/formatter/mark.js +34 -0
  26. package/formatter/tag.js +7 -33
  27. package/helpers/utils.js +15 -16
  28. package/index.js +9 -1
  29. package/index.shared.js +22 -12
  30. package/mappers/languages/csv.js +62 -0
  31. package/mappers/languages/html.js +21 -69
  32. package/mappers/languages/json.js +74 -156
  33. package/mappers/languages/jsonc.js +21 -63
  34. package/mappers/languages/markdown.js +159 -276
  35. package/mappers/languages/mdx.js +7 -62
  36. package/mappers/languages/text.js +2 -19
  37. package/mappers/languages/toml.js +231 -0
  38. package/mappers/languages/xml.js +25 -25
  39. package/mappers/languages/yaml.js +323 -0
  40. package/mappers/mapper.js +1 -22
  41. package/mappers/shared/index.js +3 -16
  42. package/package.json +5 -2
@@ -8,16 +8,10 @@
8
8
  * @property {string} END_KEYWORD - 'end' value.
9
9
  * @property {string} IDENTIFIER - Block or inline name (e.g. 'Person', 'import', '$use-module').
10
10
  * @property {string} EQUAL - '=' char.
11
- * @property {string} VALUE - Data values. Encapsulates Quoted Strings ("...") and Prefix Layers (js{}, p{}).
11
+ * @property {string} VALUE - Data values. Encapsulates Quoted Strings ("...") and Prefix Layers (p{}, v{}).
12
12
  * @property {string} TEXT - Plain unformatted text content.
13
- * @property {string} THIN_ARROW - '->' sequence.
14
- * @property {string} OPEN_PAREN - '(' char.
15
- * @property {string} CLOSE_PAREN - ')' char.
16
- * @property {string} OPEN_AT - '@_' sequence (At-Block start).
17
- * @property {string} CLOSE_AT - '_@' sequence (At-Header end).
18
13
  * @property {string} COLON - ':' char.
19
14
  * @property {string} COMMA - ',' char.
20
- * @property {string} SEMICOLON - ';' char (At-Block separator).
21
15
  * @property {string} COMMENT - '#' comments.
22
16
  * @property {string} COMMENT_BLOCK - '###' comments.
23
17
  * @property {string} ESCAPE - '\' char. Used for literalizing structural chars like '\"' or '\['.
@@ -25,7 +19,6 @@
25
19
  * @property {string} EXCLAMATION_MARK - '!' char.
26
20
  * @property {string} IMPORT - 'import' keyword.
27
21
  * @property {string} USE_MODULE - '$use-module' keyword.
28
- * @property {string} PREFIX_JS - 'js{}' prefix layer.
29
22
  * @property {string} PREFIX_P - 'p{}' placeholder layer.
30
23
  * @property {string} PREFIX_V - 'v{}' local variable layer.
31
24
  * @property {string} EOF - End of File indicator.
@@ -40,18 +33,11 @@ const TOKEN_TYPES = {
40
33
  EQUAL: "EQUAL",
41
34
  VALUE: "VALUE",
42
35
  QUOTE: "QUOTE",
43
- PREFIX_JS: "PREFIX_JS",
44
36
  PREFIX_P: "PREFIX_P",
45
37
  PREFIX_V: "PREFIX_V",
46
38
  TEXT: "TEXT",
47
- THIN_ARROW: "THIN_ARROW",
48
- OPEN_PAREN: "OPEN_PAREN",
49
- CLOSE_PAREN: "CLOSE_PAREN",
50
- OPEN_AT: "OPEN_AT",
51
- CLOSE_AT: "CLOSE_AT",
52
39
  COLON: "COLON",
53
40
  COMMA: "COMMA",
54
- SEMICOLON: "SEMICOLON",
55
41
  COMMENT: "COMMENT",
56
42
  COMMENT_BLOCK: "COMMENT_BLOCK",
57
43
  ESCAPE: "ESCAPE",
@@ -61,8 +47,13 @@ const TOKEN_TYPES = {
61
47
  WHITESPACE: "WHITESPACE",
62
48
  STATIC_KEYWORD: "STATIC_KEYWORD",
63
49
  RUNTIME_KEYWORD: "RUNTIME_KEYWORD",
50
+ LOGIC_OPEN: "LOGIC_OPEN",
64
51
  LOGIC: "LOGIC",
52
+ LOGIC_CLOSE: "LOGIC_CLOSE",
65
53
  FOR_EACH: "FOR_EACH",
54
+ PREFIX_OPEN: "PREFIX_OPEN",
55
+ PREFIX_CLOSE: "PREFIX_CLOSE",
56
+ PIPELINE: "PIPELINE",
66
57
  EOF: "EOF"
67
58
  };
68
59
 
@@ -1,4 +1,4 @@
1
- import { BLOCK, TEXT, INLINE, ATBLOCK, COMMENT, COMMENT_BLOCK, STATIC_LOGIC, RUNTIME_LOGIC, FOR_EACH } from "./labels.js";
1
+ import { BLOCK, TEXT, COMMENT, COMMENT_BLOCK, STATIC_LOGIC, RUNTIME_LOGIC, FOR_EACH } from "./labels.js";
2
2
  import { transpilerError } from "./errors.js";
3
3
  import evaluator from "./evaluator.js";
4
4
  import { matchedValue } from "../helpers/utils.js";
@@ -7,6 +7,22 @@ import { preprocessRuntimeLogic } from "./helpers/preprocessor.js";
7
7
  import { wrapRuntimeLogic } from "./helpers/runtimeOutput.js";
8
8
  import path from "pathe";
9
9
 
10
+ function warnDroppedVariables(variables) {
11
+ for (const [key, value] of Object.entries(variables)) {
12
+ if (value === undefined) {
13
+ console.warn(`[SomMark] variables.${key} is undefined and will be ignored.`);
14
+ } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
15
+ for (const [nestedKey, nestedVal] of Object.entries(value)) {
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}`);
18
+ } else if (nestedVal === undefined) {
19
+ console.warn(`[SomMark] variables.${key}.${nestedKey} is undefined and will be ignored.`);
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+
10
26
  const randomBytesHex = (size) => {
11
27
  const arr = new Uint8Array(size);
12
28
  globalThis.crypto.getRandomValues(arr);
@@ -22,14 +38,11 @@ const BODY_PLACEHOLDER = `SOMMARKBODYPLACEHOLDER${randomBytesHex(8)}SOMMARK`;
22
38
  * @returns {string} - The extracted text.
23
39
  */
24
40
  function getNodeText(node) {
25
- if (!node?.body && !node?.content) return "";
26
- if (node.type === ATBLOCK) return node.content || "";
41
+ if (!node?.body) return "";
27
42
  let text = "";
28
43
  if (node.body) {
29
44
  for (const child of node.body) {
30
45
  if (child.type === TEXT) text += child.text || "";
31
- else if (child.type === INLINE) text += child.value || "";
32
- else if (child.type === ATBLOCK) text += child.content || "";
33
46
  else if (child.type === BLOCK || child.type === FOR_EACH) text += getNodeText(child);
34
47
  }
35
48
  }
@@ -46,7 +59,7 @@ function getNodeText(node) {
46
59
  * @param {Object} mapper_file - The rules for how to convert each node.
47
60
  * @returns {Promise<string>} - The final text for this node.
48
61
  */
49
- async function generateOutput(ast, i, format, mapper_file, security = {}, parentId = null, generateRuntimeOutput = false, hideRuntimeOutput = false, instance = null, idState = null) {
62
+ async function generateOutput(ast, i, format, mapper_file, security = {}, parentId = null, generateRuntimeOutput = false, hideRuntimeOutput = false, instance = null, idState = null, extraCtx = {}) {
50
63
  const node = Array.isArray(ast) ? ast[i] : ast;
51
64
  if (!node) return "";
52
65
 
@@ -56,7 +69,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
56
69
 
57
70
  if (node.id === mapper_file?.options?.moduleIdentityToken) {
58
71
  const oldFilename = mapper_file.options.filename;
59
- mapper_file.options.filename = node.args?.filename || oldFilename;
72
+ mapper_file.options.filename = node.props?.filename || oldFilename;
60
73
  let bodyOutput = "";
61
74
  if (node.body) {
62
75
  evaluator.pushScope();
@@ -87,12 +100,8 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
87
100
 
88
101
  if (node.type === RUNTIME_LOGIC) {
89
102
  const preprocessed = await preprocessRuntimeLogic(node.code, mapper_file?.options?.filename, security, instance);
90
- if (hideRuntimeOutput) {
91
- return "";
92
- }
93
- if (generateRuntimeOutput) {
94
- return wrapRuntimeLogic(preprocessed, format, parentId, node.depth === 1);
95
- }
103
+ if (hideRuntimeOutput) return "";
104
+ if (generateRuntimeOutput) return wrapRuntimeLogic(preprocessed, format, parentId, node.depth === 1);
96
105
  return mapper_file ? mapper_file.runtimeLogic(preprocessed, node.depth === 1, parentId) : "";
97
106
  }
98
107
 
@@ -119,8 +128,8 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
119
128
  }
120
129
 
121
130
  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: [] }) : [];
131
+ const transpiledArgs = await transpileArgs(node.props);
132
+ const items = mapper_file ? mapper_file.safeArg({ props: transpiledArgs, index: 0, key: "items", fallBack: [] }) : [];
124
133
 
125
134
  if (!Array.isArray(items)) {
126
135
  const line = node.range?.start?.line + 1 || 1;
@@ -132,8 +141,16 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
132
141
  return "";
133
142
  }
134
143
 
135
- const asVar = transpiledArgs.as || "item";
136
- const indexVar = `${asVar}_index`;
144
+ const asVar = transpiledArgs.as || "value";
145
+ if (asVar === "i") {
146
+ const line = node.range?.start?.line + 1 || 1;
147
+ transpilerError([
148
+ `<$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}`,
150
+ `at line <$yellow:${line}$>{line}`
151
+ ]);
152
+ return "";
153
+ }
137
154
 
138
155
  // Trim structural whitespace/newlines at start and end of loop body for formatting clean output
139
156
  let cleanedBody = [];
@@ -165,11 +182,11 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
165
182
  evaluator.pushScope();
166
183
  evaluator.inject({
167
184
  [asVar]: item,
168
- [indexVar]: idx++
185
+ i: idx++
169
186
  });
170
187
 
171
188
  for (let j = 0; j < cleanedBody.length; j++) {
172
- output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
189
+ output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState, extraCtx);
173
190
  }
174
191
 
175
192
  await evaluator.popScope();
@@ -179,8 +196,8 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
179
196
 
180
197
  let secretId = null;
181
198
  if (node.type === BLOCK) {
182
- if (node.args) {
183
- for (const key of Object.keys(node.args)) {
199
+ if (node.props) {
200
+ for (const key of Object.keys(node.props)) {
184
201
  if (key.toLowerCase().startsWith("data-sommark")) {
185
202
  transpilerError([
186
203
  `<$red:Reserved Attribute Error:$> The attribute name '<$yellow:${key}$>' is reserved for SomMark's internal runtime compiler logic.{line}`,
@@ -201,6 +218,29 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
201
218
  }
202
219
  }
203
220
 
221
+ // 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)) {
223
+ const rawContent = node.body?.map(n => String(n.text || "")).join("") || "";
224
+ const { "smark-raw": _, ...cleanArgs } = node.props;
225
+ const transpiledArgs = await transpileArgs(cleanArgs);
226
+ if (evaluator.active?.hasDynamicTag?.(node.id)) {
227
+ return await evaluator.active.executeDynamicTag(node.id, { props: transpiledArgs, content: rawContent, textContent: rawContent });
228
+ }
229
+ let rawTarget = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
230
+ if (!rawTarget && mapper_file) rawTarget = mapper_file.getUnknownTag(node);
231
+ if (rawTarget) {
232
+ const isManualMode = !!rawTarget.options?.handleAst;
233
+ return await rawTarget.render.call(mapper_file, {
234
+ props: transpiledArgs,
235
+ content: rawContent,
236
+ textContent: rawContent,
237
+ ast: isManualMode ? node : undefined,
238
+ isSelfClosing: node.isSelfClosing || false
239
+ });
240
+ }
241
+ return rawContent;
242
+ }
243
+
204
244
  let target = null;
205
245
  if (evaluator.active && evaluator.active.hasDynamicTag(node.id)) {
206
246
  target = {
@@ -221,20 +261,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
221
261
  const shouldResolveImmediate = target.options?.resolve === true;
222
262
  const textContent = getNodeText(node);
223
263
 
224
- let content = (node.body?.length === 0) ? "" :
225
- (node.type === ATBLOCK ? dedentBy(node.content || "", node.range?.start?.character || 0).trim() :
226
- (node.type === INLINE ? (node.value || "") : BODY_PLACEHOLDER));
227
-
228
- // Apply pipelines to format literal values
229
- if (node.type === INLINE) {
230
- content = String(content || "");
231
- content = mapper_file ? mapper_file.inlineText(content, target.options) : content;
232
- }
233
-
234
- if (node.type === ATBLOCK) {
235
- content = String(content || "");
236
- content = mapper_file ? mapper_file.atBlockBody(content, target.options) : content;
237
- }
264
+ let content = (node.body?.length === 0) ? "" : BODY_PLACEHOLDER;
238
265
 
239
266
  // 1. Determine if this is a parent block that needs newline wrapping (Trim-and-Wrap)
240
267
  // Priority: Target options > Mapper global options
@@ -271,16 +298,72 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
271
298
 
272
299
  const isManualMode = target.options?.handleAst === true;
273
300
 
274
- const transpiledArgs = await transpileArgs(node.args);
301
+ if (isManualMode) {
302
+ const cleanBody = [];
303
+ let richText = "";
304
+
305
+ evaluator.pushScope();
306
+ try {
307
+ for (const child of (node.body || [])) {
308
+ if (child.type === BLOCK || child.type === TEXT || child.type === FOR_EACH) {
309
+ cleanBody.push(child);
310
+ if (child.type === TEXT) {
311
+ richText += mapper_file ? mapper_file.text(String(child.text || ""), target.options) : String(child.text || "");
312
+ }
313
+ } else if (child.type === STATIC_LOGIC) {
314
+ try {
315
+ const val = await evaluator.execute(child.code);
316
+ if (val !== undefined && typeof val !== "object") richText += String(val);
317
+ } catch (err) {
318
+ transpilerError([
319
+ `<$red:Logic Error:$> ${err.message}{line}`,
320
+ `<$yellow:Code:$> <$blue:${child.code}$>{line}`
321
+ ]);
322
+ }
323
+ } else if (child.type === COMMENT) {
324
+ if (!mapper_file?.options?.removeComments) richText += mapper_file?.comment(child.text) || "";
325
+ } else if (child.type === COMMENT_BLOCK) {
326
+ if (!mapper_file?.options?.removeComments) richText += mapper_file?.commentBlock(child.text) || "";
327
+ } else if (child.type === RUNTIME_LOGIC) {
328
+ if (!hideRuntimeOutput) {
329
+ const preprocessed = await preprocessRuntimeLogic(child.code, mapper_file?.options?.filename, security, instance);
330
+ richText += mapper_file ? mapper_file.runtimeLogic(preprocessed, child.depth === 1, secretId || parentId) : "";
331
+ }
332
+ }
333
+ // FOR_EACH → silently ignored
334
+ }
335
+
336
+ const cleanAst = { ...node, body: cleanBody };
337
+ const transpiledArgs = await transpileArgs(node.props);
338
+ if (secretId) transpiledArgs["data-sommark-id"] = secretId;
339
+
340
+ const renderChild = async (childNode, extra = {}) => {
341
+ return await generateOutput(childNode, 0, format, mapper_file, security, secretId || parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState, extra);
342
+ };
343
+
344
+ return await target.render.call(mapper_file, {
345
+ props: transpiledArgs,
346
+ content: "",
347
+ textContent: richText || textContent,
348
+ ast: cleanAst,
349
+ isSelfClosing: node.isSelfClosing || false,
350
+ ...extraCtx,
351
+ renderChild
352
+ }) ?? "";
353
+ } finally {
354
+ await evaluator.popScope();
355
+ }
356
+ }
357
+
358
+ const transpiledArgs = await transpileArgs(node.props);
275
359
  if (secretId) {
276
360
  transpiledArgs["data-sommark-id"] = secretId;
277
361
  }
278
362
  result += await target.render.call(mapper_file, {
279
- nodeType: node.type,
280
- args: transpiledArgs,
363
+ props: transpiledArgs,
281
364
  content,
282
365
  textContent,
283
- ast: isManualMode ? node : new Proxy({}, {
366
+ ast: new Proxy({}, {
284
367
  get(target, prop) {
285
368
  if (prop === "then" || prop === "toJSON" || typeof prop === "symbol" || prop === "constructor" || prop === "inspect" || prop === "valueOf" || prop === "toString") {
286
369
  return undefined;
@@ -291,7 +374,8 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
291
374
  ]);
292
375
  }
293
376
  }),
294
- isSelfClosing: node.type === BLOCK ? (node.isSelfClosing || false) : undefined
377
+ isSelfClosing: node.isSelfClosing || false,
378
+ ...extraCtx
295
379
  });
296
380
  // if (isParentBlock) result = "\n" + result;
297
381
 
@@ -320,49 +404,6 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
320
404
  bodyOutput = bodyTextVal;
321
405
  break;
322
406
 
323
- case INLINE:
324
- let inlineTarget = matchedValue(mapper_file.outputs, body_node.id);
325
- if (!inlineTarget) {
326
- inlineTarget = mapper_file.getUnknownTag(body_node);
327
- }
328
-
329
- if (inlineTarget) {
330
- let inlineValue = String(body_node.value || "").trim();
331
- if (mapper_file) inlineValue = mapper_file.inlineText(inlineValue, inlineTarget.options);
332
-
333
- const hasArgs = body_node.args && typeof body_node.args === "object" && Object.keys(body_node.args).length > 0;
334
- bodyOutput = await inlineTarget.render.call(mapper_file, {
335
- nodeType: body_node.type,
336
- args: hasArgs ? body_node.args : {},
337
- content: inlineValue,
338
- ast: body_node
339
- });
340
- } else {
341
- let fallback = body_node.value || "";
342
- if (mapper_file) fallback = mapper_file.inlineText(fallback, {});
343
- bodyOutput = fallback;
344
- }
345
- break;
346
-
347
- case ATBLOCK:
348
- let atTarget = matchedValue(mapper_file.outputs, body_node.id);
349
- if (!atTarget) {
350
- atTarget = mapper_file.getUnknownTag(body_node);
351
- }
352
-
353
- // AtBlocks handle their own absolute dedenting
354
- let atContent = dedentBy(body_node.content || "", body_node.range?.start?.character || 0).trim();
355
- if (mapper_file) {
356
- atContent = mapper_file.atBlockBody(atContent, atTarget?.options || {});
357
- }
358
-
359
- // Removed multiline injection since atBlockBody handles formatting
360
- const transpiledAtArgs = await transpileArgs(body_node.args);
361
- bodyOutput = atTarget
362
- ? await atTarget.render.call(mapper_file, { nodeType: body_node.type, args: transpiledAtArgs, content: atContent, ast: body_node })
363
- : atContent;
364
- break;
365
-
366
407
  case COMMENT:
367
408
  if (mapper_file?.options?.removeComments) break;
368
409
  bodyOutput = " ".repeat(body_node.depth) + `${mapper_file.comment(body_node.text)}`;
@@ -502,36 +543,14 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
502
543
  return path.dirname(abs);
503
544
  })();
504
545
 
505
- const generateRuntimeOutput = optionsOrAst?.generateRuntimeOutput || false;
506
- const hideRuntimeOutput = optionsOrAst?.hideRuntimeOutput || false;
507
546
  const dualOutput = optionsOrAst?.dualOutput || false;
508
547
 
509
- if (dualOutput && (generateRuntimeOutput || hideRuntimeOutput)) {
510
- const flags = [
511
- generateRuntimeOutput && "\x1b[36mgenerateRuntimeOutput\x1b[0m",
512
- hideRuntimeOutput && "\x1b[36mhideRuntimeOutput\x1b[0m"
513
- ].filter(Boolean).join(" and ");
514
- console.warn(
515
- `\n[SomMark] \x1b[33m⚠ Ignored options when dualOutput is true\x1b[0m\n` +
516
- ` ${flags} ${generateRuntimeOutput && hideRuntimeOutput ? "are" : "is"} ignored when \x1b[32mdualOutput: true\x1b[0m is set.\n` +
517
- ` \x1b[2mdualOutput manages both HTML and JS passes internally — no need to set those flags.\x1b[0m\n`
518
- );
519
- } else if (generateRuntimeOutput && hideRuntimeOutput) {
520
- console.warn(
521
- "\n[SomMark] \x1b[33m⚠ Conflicting options — output will be empty\x1b[0m\n" +
522
- " \x1b[36mgenerateRuntimeOutput: true\x1b[0m → outputs only JS, suppresses all HTML\n" +
523
- " \x1b[36mhideRuntimeOutput: true\x1b[0m → suppresses all JS output\n" +
524
- " Together they cancel each other out and produce nothing.\n" +
525
- " \x1b[2mHint: use one at a time, or \x1b[0m\x1b[32mdualOutput: true\x1b[0m\x1b[2m to get [html, js] in one call.\x1b[0m\n"
526
- );
527
- return "";
528
- }
529
-
530
548
  // Initialize Logic Sandbox
531
549
  await evaluator.init(fileBaseDir, security, settings, targetMapper);
532
550
  // Inject global data
533
551
  const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
534
552
  const variables = optionsOrAst?.variables || settings?.variables || {};
553
+ warnDroppedVariables(variables);
535
554
  evaluator.inject(placeholders);
536
555
  evaluator.inject(variables);
537
556
 
@@ -598,7 +617,7 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
598
617
  try {
599
618
  for (let i = 0; i < body.length; i++) {
600
619
  const node = body[i];
601
- const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, generateRuntimeOutput, hideRuntimeOutput, instance);
620
+ const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, false, instance);
602
621
 
603
622
  let finalBlockOutput = blockOutput;
604
623
  if (prev_was_silent && node.type === TEXT) {
@@ -633,11 +652,11 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
633
652
  /**
634
653
  * Transpiles block arguments, resolving logic or variables.
635
654
  */
636
- async function transpileArgs(args) {
655
+ async function transpileArgs(props) {
637
656
  const result = {};
638
- if (!args) return result;
657
+ if (!props) return result;
639
658
 
640
- for (const [key, value] of Object.entries(args)) {
659
+ for (const [key, value] of Object.entries(props)) {
641
660
  if (key.toLowerCase().startsWith("data-sommark") && key.toLowerCase() !== "data-sommark-id") {
642
661
  transpilerError([
643
662
  `<$red:Reserved Attribute Error:$> The attribute name '<$yellow:${key}$>' is reserved for SomMark's internal runtime compiler logic.{line}`,
package/core/validator.js CHANGED
@@ -29,27 +29,7 @@ const runValidations = (node, target, instance) => {
29
29
  const context = instance ? { src: instance.src, range: errorRange, filename: instance.filename } : null;
30
30
 
31
31
  // -- Structural Integrity (Empty Body / Self-Closing) ----------------- //
32
- const isEmptyBodyTarget = rules.is_empty_body || rules.is_self_closing;
33
-
34
- // -- Node Type Validation --------------------------------------------- //
35
- if (target.options.type) {
36
- const allowedTypes = Array.isArray(target.options.type) ? target.options.type : [target.options.type];
37
- const hasAny = allowedTypes.includes("any");
38
- if (!hasAny && !allowedTypes.includes(node.structure)) {
39
- const isReserved = ["import", "$use-module", "slot", "for-each"].includes(id.toLowerCase());
40
- const msg = isReserved
41
- ? `<$yellow:Reserved keyword$> <$blue:'${id}'$> <$yellow:is strictly defined as a [${allowedTypes.join(", ")}] structure node, but was used as a [${node.structure}] structure node.$>`
42
- : `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is defined as type(s) [${allowedTypes.join(", ")}], but was used as a [${node.structure}] structure node.$>`;
43
-
44
- transpilerError(
45
- [
46
- "{N}",
47
- msg
48
- ],
49
- context
50
- );
51
- }
52
- }
32
+ const isEmptyBodyTarget = rules.is_empty_body;
53
33
 
54
34
  if (isEmptyBodyTarget && node.type === "Block" && !node.isSelfClosing && node.body) {
55
35
  const hasContent = node.body.some(child => {
@@ -72,14 +52,14 @@ const runValidations = (node, target, instance) => {
72
52
  }
73
53
 
74
54
  // -- Arguments Validation (Required Args) ----------------------------- //
75
- const isStructural = node.type === "Block" || node.type === "AtBlock";
55
+ const isStructural = node.type === "Block";
76
56
  if (isStructural && rules.required_args && Array.isArray(rules.required_args)) {
77
57
  const missingArgs = rules.required_args.filter(arg => {
78
58
  // Check if the argument exists in named args or as a positional arg (if arg is a number)
79
59
  if (typeof arg === "number") {
80
- return node.args[arg] === undefined;
60
+ return node.props[arg] === undefined;
81
61
  }
82
- return node.args[arg] === undefined;
62
+ return node.props[arg] === undefined;
83
63
  });
84
64
 
85
65
  if (missingArgs.length > 0) {
@@ -112,7 +92,7 @@ export function validateAST(ast, mapperFile, instance) {
112
92
  // Handle filename context updates for module identity tokens
113
93
  if (instance?.moduleIdentityToken && node.id === instance.moduleIdentityToken) {
114
94
  const oldFilename = instance.filename;
115
- instance.filename = node.args?.filename || oldFilename;
95
+ instance.filename = node.props?.filename || oldFilename;
116
96
  if (node.body) {
117
97
  node.body.forEach(child => validateNode(child));
118
98
  }
@@ -127,7 +107,7 @@ export function validateAST(ast, mapperFile, instance) {
127
107
  if (["import", "$use-module", "slot", "for-each"].includes(lowerId)) {
128
108
  target = {
129
109
  id: lowerId,
130
- options: { type: "Block" }
110
+ options: {}
131
111
  };
132
112
  } else {
133
113
  target = mapperFile.get(node.id) || (mapperFile.getUnknownTag ? mapperFile.getUnknownTag(node) : null);