sommark 4.3.0 → 4.5.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/cli/cli.mjs +9 -0
- package/cli/commands/bundle.js +144 -0
- package/cli/commands/help.js +4 -0
- package/cli/constants.js +1 -1
- package/core/evaluator.stub.js +44 -0
- package/core/helpers/lib.js +1 -1
- package/core/transpiler.js +94 -9
- package/dist/sommark.browser.js +98 -11
- package/dist/sommark.browser.lite.js +13526 -0
- package/dist/sommark.lexer.js +1039 -0
- package/dist/sommark.parser.js +2521 -0
- package/index.shared.js +3 -1
- package/package.json +7 -4
package/cli/cli.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { printOutput, printLex, printParse } from "./commands/print.js";
|
|
|
12
12
|
import { runInit } from "./commands/init.js";
|
|
13
13
|
import { runShow } from "./commands/show.js";
|
|
14
14
|
import { runColor } from "./commands/color.js";
|
|
15
|
+
import { runBundle } from "./commands/bundle.js";
|
|
15
16
|
import { extensions } from "./constants.js";
|
|
16
17
|
|
|
17
18
|
// ========================================================================== //
|
|
@@ -57,6 +58,14 @@ async function main() {
|
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// 4.5. Bundle
|
|
62
|
+
if (command === "bundle") {
|
|
63
|
+
const targetDir = args.find(a => !a.startsWith("--") && a !== "bundle");
|
|
64
|
+
const flags = args.filter(a => a.startsWith("--"));
|
|
65
|
+
await runBundle(targetDir, flags);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
|
|
61
70
|
|
|
62
71
|
// 5. Show
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { cliError, formatMessage } from "../../core/errors.js";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const DIST_SRC = path.resolve(__dirname, "../../dist");
|
|
9
|
+
|
|
10
|
+
const BUNDLES = {
|
|
11
|
+
"--lite": { file: "sommark.browser.lite.js", label: "Lite bundle", note: "no WASM — static/runtime blocks disabled" },
|
|
12
|
+
"--only-lexer": { file: "sommark.lexer.js", label: "Lexer bundle", note: "lexSync, lex, TOKEN_TYPES, labels only" },
|
|
13
|
+
"--only-parser": { file: "sommark.parser.js", label: "Parser bundle", note: "lexSync, lex, parseSync, parse, TOKEN_TYPES, labels only" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
async function copyDir(src, dest) {
|
|
17
|
+
await fs.mkdir(dest, { recursive: true });
|
|
18
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const srcPath = path.join(src, entry.name);
|
|
21
|
+
const destPath = path.join(dest, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
await copyDir(srcPath, destPath);
|
|
24
|
+
} else {
|
|
25
|
+
await fs.copyFile(srcPath, destPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function getDirSize(dir) {
|
|
31
|
+
let total = 0;
|
|
32
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const full = path.join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
total += await getDirSize(full);
|
|
37
|
+
} else {
|
|
38
|
+
const stat = await fs.stat(full);
|
|
39
|
+
total += stat.size;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return total;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function ensureDir(outDir) {
|
|
46
|
+
let exists = false;
|
|
47
|
+
try {
|
|
48
|
+
const stat = await fs.stat(outDir);
|
|
49
|
+
if (!stat.isDirectory()) {
|
|
50
|
+
cliError([
|
|
51
|
+
`{line}<$red:Target path$> <$yellow:'${outDir}'$> <$red:already exists but is a file, not a directory.$>{line}`
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
exists = true;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err.code !== "ENOENT") throw err;
|
|
57
|
+
}
|
|
58
|
+
if (!exists) {
|
|
59
|
+
try {
|
|
60
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
cliError([
|
|
63
|
+
`{line}<$red:Could not create directory$> <$yellow:'${outDir}'$>{N}`,
|
|
64
|
+
`<$blue:${err.message}$>{line}`
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runBundle(targetDir, flags = []) {
|
|
71
|
+
const outDir = targetDir ? path.resolve(process.cwd(), targetDir) : process.cwd();
|
|
72
|
+
|
|
73
|
+
// Partial bundle (--lite, --only-lexer, --only-parser)
|
|
74
|
+
const partialFlag = flags.find(f => BUNDLES[f]);
|
|
75
|
+
if (partialFlag) {
|
|
76
|
+
const { file, label, note } = BUNDLES[partialFlag];
|
|
77
|
+
const src = path.resolve(DIST_SRC, file);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await fs.access(src);
|
|
81
|
+
} catch {
|
|
82
|
+
cliError([
|
|
83
|
+
`{line}<$red:${label} not found at$> <$yellow:'${src}'$>{N}`,
|
|
84
|
+
`<$red:Your SomMark installation may be incomplete or corrupted.$>{line}`
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await ensureDir(outDir);
|
|
89
|
+
|
|
90
|
+
const dest = path.join(outDir, file);
|
|
91
|
+
try {
|
|
92
|
+
await fs.copyFile(src, dest);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
cliError([
|
|
95
|
+
`{line}<$red:Failed to copy ${label.toLowerCase()} to$> <$yellow:'${dest}'$>{N}`,
|
|
96
|
+
`<$blue:${err.message}$>{line}`
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const stats = await fs.stat(dest);
|
|
101
|
+
const date = new Date().toLocaleString();
|
|
102
|
+
console.log(formatMessage(
|
|
103
|
+
[
|
|
104
|
+
`{line}[<$yellow: STATUS$> : <$green: SUCCESS$>]{line}`,
|
|
105
|
+
`<$blue:${label}$> <$yellow:'${file}'$> <$blue:copied to$> <$yellow:'${outDir}'$>{N}`,
|
|
106
|
+
`<$blue:Size:$> <$yellow:${(stats.size / 1024).toFixed(1)} KB$> <$yellow:(${note})$>{N}`,
|
|
107
|
+
`<$blue:Date:$> <$yellow:${date}$>{line}`
|
|
108
|
+
].join("")
|
|
109
|
+
));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Full: copy entire dist folder
|
|
114
|
+
try {
|
|
115
|
+
await fs.access(DIST_SRC);
|
|
116
|
+
} catch {
|
|
117
|
+
cliError([
|
|
118
|
+
`{line}<$red:dist folder not found at$> <$yellow:'${DIST_SRC}'$>{N}`,
|
|
119
|
+
`<$red:Your SomMark installation may be incomplete or corrupted.$>{line}`
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await ensureDir(outDir);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await copyDir(DIST_SRC, outDir);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
cliError([
|
|
129
|
+
`{line}<$red:Failed to copy bundle to$> <$yellow:'${outDir}'$>{N}`,
|
|
130
|
+
`<$blue:${err.message}$>{line}`
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const totalSize = await getDirSize(outDir);
|
|
135
|
+
const date = new Date().toLocaleString();
|
|
136
|
+
console.log(formatMessage(
|
|
137
|
+
[
|
|
138
|
+
`{line}[<$yellow: STATUS$> : <$green: SUCCESS$>]{line}`,
|
|
139
|
+
`<$blue:Bundle copied to$> <$yellow:'${outDir}'$>{N}`,
|
|
140
|
+
`<$blue:Total size:$> <$yellow:${(totalSize / 1024).toFixed(1)} KB$>{N}`,
|
|
141
|
+
`<$blue:Date:$> <$yellow:${date}$>{line}`
|
|
142
|
+
].join("")
|
|
143
|
+
));
|
|
144
|
+
}
|
package/cli/commands/help.js
CHANGED
|
@@ -22,6 +22,10 @@ export function getHelp(unknown_option = true) {
|
|
|
22
22
|
"{N} <$green:show config [file]$> <$cyan: Display the configuration data (for a specific file or CWD)$>",
|
|
23
23
|
"{N} <$green:show --path-config [file]$> <$cyan: Display the absolute path to the active config file$>",
|
|
24
24
|
"{N} <$green:color on|off$> <$cyan: Help on enabling colors via Environment Variables$>",
|
|
25
|
+
"{N} <$green:bundle [dir]$> <$cyan: Copy the full browser bundle (JS + WASM) to a directory$>",
|
|
26
|
+
"{N} <$green:bundle [dir] --lite$> <$cyan: Lite bundle — no WASM, static/runtime blocks disabled$>",
|
|
27
|
+
"{N} <$green:bundle [dir] --only-lexer$> <$cyan: Lexer only — lexSync, lex, TOKEN_TYPES, labels$>",
|
|
28
|
+
"{N} <$green:bundle [dir] --only-parser$> <$cyan: Parser only — lexSync, parseSync and above$>",
|
|
25
29
|
|
|
26
30
|
"{N}{N}<$yellow:Transpilation Options:$>",
|
|
27
31
|
"{N}<$yellow:Usage:$> <$blue:sommark [option] [targetFile] [option] [outputFile] [outputDir]$>",
|
package/cli/constants.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/** @type {Array<string>} List of recognized CLI flags and commands. */
|
|
7
|
-
export const options = ["-v", "--version", "-h", "--help", "--html", "--markdown", "--mdx", "--json", "--jsonc", "--text", "--xml", "--print", "-p", "--lex", "--parse", "list"];
|
|
7
|
+
export const options = ["-v", "--version", "-h", "--help", "--html", "--markdown", "--mdx", "--json", "--jsonc", "--text", "--xml", "--print", "-p", "--lex", "--parse", "list", "bundle"];
|
|
8
8
|
|
|
9
9
|
/** @type {Object<string, string>} Map of output formats to their respective file extensions. */
|
|
10
10
|
export const extensions = {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Lite-mode stub — same interface as evaluator.js but without QuickJS/WASM.
|
|
2
|
+
// Static and runtime blocks throw a clear error instead of executing.
|
|
3
|
+
|
|
4
|
+
const LITE_ERROR =
|
|
5
|
+
"[SomMark lite] static ${}$ and runtime ${}$ blocks are not supported in lite mode. " +
|
|
6
|
+
"Use the full SomMark bundle to enable JS evaluation.";
|
|
7
|
+
|
|
8
|
+
export function setCompilerClass(_cls) {}
|
|
9
|
+
|
|
10
|
+
class EvaluatorStub {
|
|
11
|
+
setDefaultFs(_fs) {}
|
|
12
|
+
|
|
13
|
+
get active() {
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async init(_baseDir, _security, _settings, _mapperFile) {}
|
|
18
|
+
|
|
19
|
+
destroy() {}
|
|
20
|
+
|
|
21
|
+
pushScope() {}
|
|
22
|
+
|
|
23
|
+
async popScope() {}
|
|
24
|
+
|
|
25
|
+
inject(_vars) {}
|
|
26
|
+
|
|
27
|
+
async execute(_code) {
|
|
28
|
+
throw new Error(LITE_ERROR);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
hasDynamicTag(_id) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getDynamicTagOptions(_id) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async executeDynamicTag(_id, _payload) {
|
|
40
|
+
throw new Error(LITE_ERROR);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default new EvaluatorStub();
|
package/core/helpers/lib.js
CHANGED
package/core/transpiler.js
CHANGED
|
@@ -45,7 +45,7 @@ function getNodeText(node) {
|
|
|
45
45
|
* @param {Object} mapper_file - The rules for how to convert each node.
|
|
46
46
|
* @returns {Promise<string>} - The final text for this node.
|
|
47
47
|
*/
|
|
48
|
-
async function generateOutput(ast, i, format, mapper_file, security = {}, parentId = null, generateRuntimeOutput = false, hideRuntimeOutput = false, instance = null) {
|
|
48
|
+
async function generateOutput(ast, i, format, mapper_file, security = {}, parentId = null, generateRuntimeOutput = false, hideRuntimeOutput = false, instance = null, idState = null) {
|
|
49
49
|
const node = Array.isArray(ast) ? ast[i] : ast;
|
|
50
50
|
if (!node) return "";
|
|
51
51
|
|
|
@@ -60,7 +60,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
60
60
|
if (node.body) {
|
|
61
61
|
evaluator.pushScope();
|
|
62
62
|
for (let j = 0; j < node.body.length; j++) {
|
|
63
|
-
bodyOutput += await generateOutput(node.body, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance);
|
|
63
|
+
bodyOutput += await generateOutput(node.body, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
|
|
64
64
|
}
|
|
65
65
|
await evaluator.popScope();
|
|
66
66
|
}
|
|
@@ -168,7 +168,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
168
168
|
});
|
|
169
169
|
|
|
170
170
|
for (let j = 0; j < cleanedBody.length; j++) {
|
|
171
|
-
output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance);
|
|
171
|
+
output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
await evaluator.popScope();
|
|
@@ -191,7 +191,12 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
191
191
|
|
|
192
192
|
const hasRuntime = node.body?.some(child => child.type === RUNTIME_LOGIC);
|
|
193
193
|
if (hasRuntime) {
|
|
194
|
-
|
|
194
|
+
if (idState?.mode === 'replay') {
|
|
195
|
+
secretId = idState.ids[idState.idx++] ?? `sommark-${node.id.toLowerCase()}-${randomBytesHex(4)}`;
|
|
196
|
+
} else {
|
|
197
|
+
secretId = `sommark-${node.id.toLowerCase()}-${randomBytesHex(4)}`;
|
|
198
|
+
if (idState?.mode === 'record') idState.ids.push(secretId);
|
|
199
|
+
}
|
|
195
200
|
}
|
|
196
201
|
}
|
|
197
202
|
|
|
@@ -247,7 +252,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
247
252
|
let resolvedBody = "";
|
|
248
253
|
evaluator.pushScope();
|
|
249
254
|
for (let j = 0; j < node.body.length; j++) {
|
|
250
|
-
resolvedBody += await generateOutput(node.body, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance);
|
|
255
|
+
resolvedBody += await generateOutput(node.body, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
|
|
251
256
|
}
|
|
252
257
|
await evaluator.popScope();
|
|
253
258
|
content = dedentBy(resolvedBody, node.range?.start?.character || 0);
|
|
@@ -257,7 +262,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
257
262
|
let childrenOutput = "";
|
|
258
263
|
if (node.body) {
|
|
259
264
|
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);
|
|
265
|
+
childrenOutput += await generateOutput(node.body, j, format, mapper_file, security, secretId || parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
|
|
261
266
|
}
|
|
262
267
|
}
|
|
263
268
|
return childrenOutput;
|
|
@@ -369,7 +374,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
369
374
|
|
|
370
375
|
case FOR_EACH:
|
|
371
376
|
case BLOCK:
|
|
372
|
-
bodyOutput = await generateOutput(body_node, 0, format, mapper_file, security, secretId || parentId, generateRuntimeOutput, hideRuntimeOutput, instance);
|
|
377
|
+
bodyOutput = await generateOutput(body_node, 0, format, mapper_file, security, secretId || parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
|
|
373
378
|
break;
|
|
374
379
|
|
|
375
380
|
case RUNTIME_LOGIC:
|
|
@@ -488,6 +493,31 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
|
|
|
488
493
|
settings.fs = instance.fs;
|
|
489
494
|
}
|
|
490
495
|
|
|
496
|
+
const generateRuntimeOutput = optionsOrAst?.generateRuntimeOutput || false;
|
|
497
|
+
const hideRuntimeOutput = optionsOrAst?.hideRuntimeOutput || false;
|
|
498
|
+
const dualOutput = optionsOrAst?.dualOutput || false;
|
|
499
|
+
|
|
500
|
+
if (dualOutput && (generateRuntimeOutput || hideRuntimeOutput)) {
|
|
501
|
+
const flags = [
|
|
502
|
+
generateRuntimeOutput && "\x1b[36mgenerateRuntimeOutput\x1b[0m",
|
|
503
|
+
hideRuntimeOutput && "\x1b[36mhideRuntimeOutput\x1b[0m"
|
|
504
|
+
].filter(Boolean).join(" and ");
|
|
505
|
+
console.warn(
|
|
506
|
+
`\n[SomMark] \x1b[33m⚠ Ignored options when dualOutput is true\x1b[0m\n` +
|
|
507
|
+
` ${flags} ${generateRuntimeOutput && hideRuntimeOutput ? "are" : "is"} ignored when \x1b[32mdualOutput: true\x1b[0m is set.\n` +
|
|
508
|
+
` \x1b[2mdualOutput manages both HTML and JS passes internally — no need to set those flags.\x1b[0m\n`
|
|
509
|
+
);
|
|
510
|
+
} else if (generateRuntimeOutput && hideRuntimeOutput) {
|
|
511
|
+
console.warn(
|
|
512
|
+
"\n[SomMark] \x1b[33m⚠ Conflicting options — output will be empty\x1b[0m\n" +
|
|
513
|
+
" \x1b[36mgenerateRuntimeOutput: true\x1b[0m → outputs only JS, suppresses all HTML\n" +
|
|
514
|
+
" \x1b[36mhideRuntimeOutput: true\x1b[0m → suppresses all JS output\n" +
|
|
515
|
+
" Together they cancel each other out and produce nothing.\n" +
|
|
516
|
+
" \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"
|
|
517
|
+
);
|
|
518
|
+
return "";
|
|
519
|
+
}
|
|
520
|
+
|
|
491
521
|
// Initialize Logic Sandbox
|
|
492
522
|
await evaluator.init(null, security, settings, targetMapper);
|
|
493
523
|
// Inject global data
|
|
@@ -499,8 +529,63 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
|
|
|
499
529
|
let output = "";
|
|
500
530
|
let prev_body_node = null;
|
|
501
531
|
let prev_was_silent = false;
|
|
502
|
-
|
|
503
|
-
|
|
532
|
+
|
|
533
|
+
if (dualOutput) {
|
|
534
|
+
const idState = { mode: 'record', ids: [], idx: 0 };
|
|
535
|
+
|
|
536
|
+
// HTML pass — generate HTML, record element IDs for runtime blocks
|
|
537
|
+
let htmlOutput = "";
|
|
538
|
+
try {
|
|
539
|
+
for (let i = 0; i < body.length; i++) {
|
|
540
|
+
const node = body[i];
|
|
541
|
+
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, true, instance, idState);
|
|
542
|
+
let finalBlockOutput = blockOutput;
|
|
543
|
+
if (prev_was_silent && node.type === TEXT) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
|
|
544
|
+
if (finalBlockOutput) {
|
|
545
|
+
htmlOutput += finalBlockOutput;
|
|
546
|
+
prev_was_silent = false;
|
|
547
|
+
} else {
|
|
548
|
+
prev_was_silent = true;
|
|
549
|
+
if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
|
|
550
|
+
const nextNode = body[i + 1];
|
|
551
|
+
if (nextNode && nextNode.type === TEXT && (nextNode.text === "\n" || nextNode.text === "\r\n")) i++;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} finally {
|
|
556
|
+
evaluator.destroy();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// JS pass — replay the same IDs so querySelector targets match HTML
|
|
560
|
+
idState.mode = 'replay';
|
|
561
|
+
idState.idx = 0;
|
|
562
|
+
prev_was_silent = false;
|
|
563
|
+
|
|
564
|
+
await evaluator.init(null, security, settings, targetMapper);
|
|
565
|
+
evaluator.inject(placeholders);
|
|
566
|
+
evaluator.inject(variables);
|
|
567
|
+
|
|
568
|
+
let jsOutput = "";
|
|
569
|
+
try {
|
|
570
|
+
for (let i = 0; i < body.length; i++) {
|
|
571
|
+
const node = body[i];
|
|
572
|
+
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, true, false, instance, idState);
|
|
573
|
+
let finalBlockOutput = blockOutput;
|
|
574
|
+
if (prev_was_silent && node.type === TEXT) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
|
|
575
|
+
if (finalBlockOutput) {
|
|
576
|
+
jsOutput += finalBlockOutput;
|
|
577
|
+
prev_was_silent = false;
|
|
578
|
+
} else {
|
|
579
|
+
prev_was_silent = true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} finally {
|
|
583
|
+
evaluator.destroy();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return [htmlOutput.trim(), jsOutput.trim()];
|
|
587
|
+
}
|
|
588
|
+
|
|
504
589
|
try {
|
|
505
590
|
for (let i = 0; i < body.length; i++) {
|
|
506
591
|
const node = body[i];
|
package/dist/sommark.browser.js
CHANGED
|
@@ -9184,7 +9184,7 @@ function registerHostSettings(settings) {
|
|
|
9184
9184
|
hostSettings = settings || {};
|
|
9185
9185
|
}
|
|
9186
9186
|
|
|
9187
|
-
const version = "4.
|
|
9187
|
+
const version = "4.5.0";
|
|
9188
9188
|
|
|
9189
9189
|
const SomMark$1 = {
|
|
9190
9190
|
version,
|
|
@@ -10577,7 +10577,7 @@ function getNodeText$1(node) {
|
|
|
10577
10577
|
* @param {Object} mapper_file - The rules for how to convert each node.
|
|
10578
10578
|
* @returns {Promise<string>} - The final text for this node.
|
|
10579
10579
|
*/
|
|
10580
|
-
async function generateOutput(ast, i, format, mapper_file, security = {}, parentId = null, generateRuntimeOutput = false, hideRuntimeOutput = false, instance = null) {
|
|
10580
|
+
async function generateOutput(ast, i, format, mapper_file, security = {}, parentId = null, generateRuntimeOutput = false, hideRuntimeOutput = false, instance = null, idState = null) {
|
|
10581
10581
|
const node = Array.isArray(ast) ? ast[i] : ast;
|
|
10582
10582
|
if (!node) return "";
|
|
10583
10583
|
|
|
@@ -10592,7 +10592,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
10592
10592
|
if (node.body) {
|
|
10593
10593
|
Evaluator$1.pushScope();
|
|
10594
10594
|
for (let j = 0; j < node.body.length; j++) {
|
|
10595
|
-
bodyOutput += await generateOutput(node.body, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance);
|
|
10595
|
+
bodyOutput += await generateOutput(node.body, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
|
|
10596
10596
|
}
|
|
10597
10597
|
await Evaluator$1.popScope();
|
|
10598
10598
|
}
|
|
@@ -10700,7 +10700,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
10700
10700
|
});
|
|
10701
10701
|
|
|
10702
10702
|
for (let j = 0; j < cleanedBody.length; j++) {
|
|
10703
|
-
output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance);
|
|
10703
|
+
output += await generateOutput(cleanedBody, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
|
|
10704
10704
|
}
|
|
10705
10705
|
|
|
10706
10706
|
await Evaluator$1.popScope();
|
|
@@ -10723,7 +10723,12 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
10723
10723
|
|
|
10724
10724
|
const hasRuntime = node.body?.some(child => child.type === RUNTIME_LOGIC);
|
|
10725
10725
|
if (hasRuntime) {
|
|
10726
|
-
|
|
10726
|
+
if (idState?.mode === 'replay') {
|
|
10727
|
+
secretId = idState.ids[idState.idx++] ?? `sommark-${node.id.toLowerCase()}-${randomBytesHex(4)}`;
|
|
10728
|
+
} else {
|
|
10729
|
+
secretId = `sommark-${node.id.toLowerCase()}-${randomBytesHex(4)}`;
|
|
10730
|
+
if (idState?.mode === 'record') idState.ids.push(secretId);
|
|
10731
|
+
}
|
|
10727
10732
|
}
|
|
10728
10733
|
}
|
|
10729
10734
|
|
|
@@ -10779,7 +10784,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
10779
10784
|
let resolvedBody = "";
|
|
10780
10785
|
Evaluator$1.pushScope();
|
|
10781
10786
|
for (let j = 0; j < node.body.length; j++) {
|
|
10782
|
-
resolvedBody += await generateOutput(node.body, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance);
|
|
10787
|
+
resolvedBody += await generateOutput(node.body, j, format, mapper_file, security, parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
|
|
10783
10788
|
}
|
|
10784
10789
|
await Evaluator$1.popScope();
|
|
10785
10790
|
content = dedentBy(resolvedBody, node.range?.start?.character || 0);
|
|
@@ -10789,7 +10794,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
10789
10794
|
let childrenOutput = "";
|
|
10790
10795
|
if (node.body) {
|
|
10791
10796
|
for (let j = 0; j < node.body.length; j++) {
|
|
10792
|
-
childrenOutput += await generateOutput(node.body, j, format, mapper_file, security, secretId || parentId, generateRuntimeOutput, hideRuntimeOutput, instance);
|
|
10797
|
+
childrenOutput += await generateOutput(node.body, j, format, mapper_file, security, secretId || parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
|
|
10793
10798
|
}
|
|
10794
10799
|
}
|
|
10795
10800
|
return childrenOutput;
|
|
@@ -10900,7 +10905,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
|
|
|
10900
10905
|
|
|
10901
10906
|
case FOR_EACH:
|
|
10902
10907
|
case BLOCK:
|
|
10903
|
-
bodyOutput = await generateOutput(body_node, 0, format, mapper_file, security, secretId || parentId, generateRuntimeOutput, hideRuntimeOutput, instance);
|
|
10908
|
+
bodyOutput = await generateOutput(body_node, 0, format, mapper_file, security, secretId || parentId, generateRuntimeOutput, hideRuntimeOutput, instance, idState);
|
|
10904
10909
|
break;
|
|
10905
10910
|
|
|
10906
10911
|
case RUNTIME_LOGIC:
|
|
@@ -11019,6 +11024,31 @@ async function transpiler(optionsOrAst, format, mapperFile) {
|
|
|
11019
11024
|
settings.fs = instance.fs;
|
|
11020
11025
|
}
|
|
11021
11026
|
|
|
11027
|
+
const generateRuntimeOutput = optionsOrAst?.generateRuntimeOutput || false;
|
|
11028
|
+
const hideRuntimeOutput = optionsOrAst?.hideRuntimeOutput || false;
|
|
11029
|
+
const dualOutput = optionsOrAst?.dualOutput || false;
|
|
11030
|
+
|
|
11031
|
+
if (dualOutput && (generateRuntimeOutput || hideRuntimeOutput)) {
|
|
11032
|
+
const flags = [
|
|
11033
|
+
generateRuntimeOutput && "\x1b[36mgenerateRuntimeOutput\x1b[0m",
|
|
11034
|
+
hideRuntimeOutput && "\x1b[36mhideRuntimeOutput\x1b[0m"
|
|
11035
|
+
].filter(Boolean).join(" and ");
|
|
11036
|
+
console.warn(
|
|
11037
|
+
`\n[SomMark] \x1b[33m⚠ Ignored options when dualOutput is true\x1b[0m\n` +
|
|
11038
|
+
` ${flags} ${generateRuntimeOutput && hideRuntimeOutput ? "are" : "is"} ignored when \x1b[32mdualOutput: true\x1b[0m is set.\n` +
|
|
11039
|
+
` \x1b[2mdualOutput manages both HTML and JS passes internally — no need to set those flags.\x1b[0m\n`
|
|
11040
|
+
);
|
|
11041
|
+
} else if (generateRuntimeOutput && hideRuntimeOutput) {
|
|
11042
|
+
console.warn(
|
|
11043
|
+
"\n[SomMark] \x1b[33m⚠ Conflicting options — output will be empty\x1b[0m\n" +
|
|
11044
|
+
" \x1b[36mgenerateRuntimeOutput: true\x1b[0m → outputs only JS, suppresses all HTML\n" +
|
|
11045
|
+
" \x1b[36mhideRuntimeOutput: true\x1b[0m → suppresses all JS output\n" +
|
|
11046
|
+
" Together they cancel each other out and produce nothing.\n" +
|
|
11047
|
+
" \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"
|
|
11048
|
+
);
|
|
11049
|
+
return "";
|
|
11050
|
+
}
|
|
11051
|
+
|
|
11022
11052
|
// Initialize Logic Sandbox
|
|
11023
11053
|
await Evaluator$1.init(null, security, settings, targetMapper);
|
|
11024
11054
|
// Inject global data
|
|
@@ -11030,8 +11060,63 @@ async function transpiler(optionsOrAst, format, mapperFile) {
|
|
|
11030
11060
|
let output = "";
|
|
11031
11061
|
let prev_body_node = null;
|
|
11032
11062
|
let prev_was_silent = false;
|
|
11033
|
-
|
|
11034
|
-
|
|
11063
|
+
|
|
11064
|
+
if (dualOutput) {
|
|
11065
|
+
const idState = { mode: 'record', ids: [], idx: 0 };
|
|
11066
|
+
|
|
11067
|
+
// HTML pass — generate HTML, record element IDs for runtime blocks
|
|
11068
|
+
let htmlOutput = "";
|
|
11069
|
+
try {
|
|
11070
|
+
for (let i = 0; i < body.length; i++) {
|
|
11071
|
+
const node = body[i];
|
|
11072
|
+
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, true, instance, idState);
|
|
11073
|
+
let finalBlockOutput = blockOutput;
|
|
11074
|
+
if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
|
|
11075
|
+
if (finalBlockOutput) {
|
|
11076
|
+
htmlOutput += finalBlockOutput;
|
|
11077
|
+
prev_was_silent = false;
|
|
11078
|
+
} else {
|
|
11079
|
+
prev_was_silent = true;
|
|
11080
|
+
if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
|
|
11081
|
+
const nextNode = body[i + 1];
|
|
11082
|
+
if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) i++;
|
|
11083
|
+
}
|
|
11084
|
+
}
|
|
11085
|
+
}
|
|
11086
|
+
} finally {
|
|
11087
|
+
Evaluator$1.destroy();
|
|
11088
|
+
}
|
|
11089
|
+
|
|
11090
|
+
// JS pass — replay the same IDs so querySelector targets match HTML
|
|
11091
|
+
idState.mode = 'replay';
|
|
11092
|
+
idState.idx = 0;
|
|
11093
|
+
prev_was_silent = false;
|
|
11094
|
+
|
|
11095
|
+
await Evaluator$1.init(null, security, settings, targetMapper);
|
|
11096
|
+
Evaluator$1.inject(placeholders);
|
|
11097
|
+
Evaluator$1.inject(variables);
|
|
11098
|
+
|
|
11099
|
+
let jsOutput = "";
|
|
11100
|
+
try {
|
|
11101
|
+
for (let i = 0; i < body.length; i++) {
|
|
11102
|
+
const node = body[i];
|
|
11103
|
+
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, true, false, instance, idState);
|
|
11104
|
+
let finalBlockOutput = blockOutput;
|
|
11105
|
+
if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
|
|
11106
|
+
if (finalBlockOutput) {
|
|
11107
|
+
jsOutput += finalBlockOutput;
|
|
11108
|
+
prev_was_silent = false;
|
|
11109
|
+
} else {
|
|
11110
|
+
prev_was_silent = true;
|
|
11111
|
+
}
|
|
11112
|
+
}
|
|
11113
|
+
} finally {
|
|
11114
|
+
Evaluator$1.destroy();
|
|
11115
|
+
}
|
|
11116
|
+
|
|
11117
|
+
return [htmlOutput.trim(), jsOutput.trim()];
|
|
11118
|
+
}
|
|
11119
|
+
|
|
11035
11120
|
try {
|
|
11036
11121
|
for (let i = 0; i < body.length; i++) {
|
|
11037
11122
|
const node = body[i];
|
|
@@ -14095,7 +14180,7 @@ class SomMark {
|
|
|
14095
14180
|
* @param {string} [options.baseDir=null] - The base directory for resolving relative paths.
|
|
14096
14181
|
*/
|
|
14097
14182
|
constructor(options = {}) {
|
|
14098
|
-
const { src, ast = null, format, mapperFile = null, filename = "anonymous", removeComments = true, placeholders = {}, customProps = [], fallbackTarget = "style", outputValidator = null, importAliases = {}, importStack = [], baseDir = null, moduleCache = null, showSpinner = true, security = {}, generateRuntimeOutput = false, hideRuntimeOutput = false, moduleIdentityToken = null } = options;
|
|
14183
|
+
const { src, ast = null, format, mapperFile = null, filename = "anonymous", removeComments = true, placeholders = {}, customProps = [], fallbackTarget = "style", outputValidator = null, importAliases = {}, importStack = [], baseDir = null, moduleCache = null, showSpinner = true, security = {}, generateRuntimeOutput = false, hideRuntimeOutput = false, dualOutput = false, moduleIdentityToken = null } = options;
|
|
14099
14184
|
this.rawSettings = options;
|
|
14100
14185
|
this.src = src;
|
|
14101
14186
|
this.ast = ast;
|
|
@@ -14107,6 +14192,7 @@ class SomMark {
|
|
|
14107
14192
|
this.customProps = customProps;
|
|
14108
14193
|
this.generateRuntimeOutput = generateRuntimeOutput;
|
|
14109
14194
|
this.hideRuntimeOutput = hideRuntimeOutput;
|
|
14195
|
+
this.dualOutput = dualOutput;
|
|
14110
14196
|
this.cwd = options.baseDir || (options.files ? "/" : defaultCwd);
|
|
14111
14197
|
this.fs = options.fs
|
|
14112
14198
|
|| (options.files ? new VirtualFS(options.files) : null)
|
|
@@ -14322,6 +14408,7 @@ class SomMark {
|
|
|
14322
14408
|
settings: this.rawSettings,
|
|
14323
14409
|
generateRuntimeOutput: this.generateRuntimeOutput,
|
|
14324
14410
|
hideRuntimeOutput: this.hideRuntimeOutput,
|
|
14411
|
+
dualOutput: this.dualOutput,
|
|
14325
14412
|
instance: this
|
|
14326
14413
|
});
|
|
14327
14414
|
|