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