sommark 4.5.3 → 5.0.1
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/README.md +315 -179
- package/cli/cli.mjs +1 -1
- package/cli/commands/color.js +36 -14
- package/cli/commands/help.js +3 -0
- package/cli/commands/init.js +1 -3
- package/cli/constants.js +5 -2
- package/constants/html_props.js +0 -5
- package/core/errors.js +5 -4
- package/core/evaluator.js +1 -2
- package/core/formats.js +7 -1
- package/core/helpers/config-loader.js +2 -4
- package/core/helpers/lib.js +1 -1
- package/core/labels.js +2 -15
- package/core/lexer.js +197 -313
- package/core/modules.js +13 -13
- package/core/parser.js +226 -535
- package/core/tokenTypes.js +6 -15
- package/core/transpiler.js +129 -110
- package/core/validator.js +6 -26
- package/dist/sommark.browser.js +1781 -2172
- package/dist/sommark.browser.lite.js +1779 -2169
- package/dist/sommark.lexer.js +392 -544
- package/dist/sommark.parser.js +604 -1200
- package/formatter/mark.js +34 -0
- package/formatter/tag.js +7 -33
- package/helpers/utils.js +15 -16
- package/index.js +9 -1
- package/index.shared.js +26 -16
- package/mappers/languages/csv.js +62 -0
- package/mappers/languages/html.js +12 -66
- package/mappers/languages/json.js +74 -156
- package/mappers/languages/jsonc.js +21 -63
- package/mappers/languages/markdown.js +159 -276
- package/mappers/languages/mdx.js +7 -62
- package/mappers/languages/text.js +2 -19
- package/mappers/languages/toml.js +231 -0
- package/mappers/languages/xml.js +25 -25
- package/mappers/languages/yaml.js +323 -0
- package/mappers/mapper.js +1 -22
- package/mappers/shared/index.js +3 -16
- package/package.json +5 -2
package/core/tokenTypes.js
CHANGED
|
@@ -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 (
|
|
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
|
|
package/core/transpiler.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BLOCK, TEXT,
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
123
|
-
const items = mapper_file ? mapper_file.safeArg({
|
|
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 || "
|
|
136
|
-
|
|
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
|
-
|
|
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.
|
|
183
|
-
for (const key of Object.keys(node.
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
args: transpiledArgs,
|
|
363
|
+
props: transpiledArgs,
|
|
281
364
|
content,
|
|
282
365
|
textContent,
|
|
283
|
-
ast:
|
|
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.
|
|
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,
|
|
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(
|
|
655
|
+
async function transpileArgs(props) {
|
|
637
656
|
const result = {};
|
|
638
|
-
if (!
|
|
657
|
+
if (!props) return result;
|
|
639
658
|
|
|
640
|
-
for (const [key, value] of Object.entries(
|
|
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
|
|
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"
|
|
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.
|
|
60
|
+
return node.props[arg] === undefined;
|
|
81
61
|
}
|
|
82
|
-
return node.
|
|
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.
|
|
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: {
|
|
110
|
+
options: {}
|
|
131
111
|
};
|
|
132
112
|
} else {
|
|
133
113
|
target = mapperFile.get(node.id) || (mapperFile.getUnknownTag ? mapperFile.getUnknownTag(node) : null);
|