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.
- package/README.md +274 -73
- package/cli/cli.mjs +2 -2
- package/cli/commands/build.js +3 -1
- package/cli/commands/help.js +4 -1
- package/cli/commands/init.js +25 -6
- package/cli/commands/show.js +20 -10
- package/cli/constants.js +2 -1
- package/cli/helpers/transpile.js +5 -2
- package/constants/html_props.js +1 -0
- package/core/evaluator.js +785 -0
- package/core/formats.js +15 -7
- package/core/helpers/config-loader.js +28 -15
- package/core/helpers/lib.js +75 -0
- package/core/helpers/preprocessor.js +185 -0
- package/core/helpers/runtimeOutput.js +28 -0
- package/core/labels.js +9 -2
- package/core/lexer.js +228 -61
- package/core/modules.js +331 -55
- package/core/parser.js +275 -55
- package/core/tokenTypes.js +11 -0
- package/core/transpiler.js +341 -59
- package/core/validator.js +85 -13
- package/formatter/tag.js +31 -7
- package/grammar.ebnf +21 -10
- package/helpers/safeDataParser.js +3 -3
- package/helpers/spinner.js +91 -0
- package/helpers/utils.js +46 -0
- package/index.js +125 -38
- package/mappers/languages/html.js +50 -9
- package/mappers/languages/json.js +81 -38
- package/mappers/languages/jsonc.js +82 -0
- package/mappers/languages/markdown.js +88 -48
- package/mappers/languages/mdx.js +50 -15
- package/mappers/languages/text.js +67 -0
- package/mappers/languages/xml.js +6 -6
- package/mappers/mapper.js +36 -4
- package/mappers/shared/index.js +12 -13
- package/package.json +6 -1
- package/core/formatter.js +0 -215
package/core/tokenTypes.js
CHANGED
|
@@ -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
|
|
package/core/transpiler.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import
|
|
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
|
|
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${
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
253
|
+
await evaluator.popScope();
|
|
254
|
+
content = dedentBy(resolvedBody, node.range?.start?.character || 0);
|
|
116
255
|
}
|
|
117
256
|
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
} else
|
|
212
|
-
result
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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;
|