sommark 4.0.3 → 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 +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 +785 -0
- package/core/formats.js +15 -7
- package/core/helpers/config-loader.js +8 -0
- 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 +70 -7
- 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/modules.js
CHANGED
|
@@ -2,16 +2,33 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { runtimeError } from "./errors.js";
|
|
5
|
-
import { IMPORT, USE_MODULE, TEXT, COMMENT } from "./labels.js";
|
|
5
|
+
import { IMPORT, USE_MODULE, TEXT, BLOCK, COMMENT, SLOT } from "./labels.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves a module path relative to a base directory.
|
|
9
|
+
*/
|
|
10
|
+
const resolveModulePath = (filePath, currentBaseDir) => {
|
|
11
|
+
return path.resolve(currentBaseDir, filePath);
|
|
12
|
+
};
|
|
6
13
|
|
|
7
14
|
/**
|
|
8
15
|
* Changes a filename or file URL into a full, absolute file path.
|
|
9
16
|
*
|
|
10
17
|
* @param {string} filename - The name of the file or its URL.
|
|
18
|
+
* @param {Object} [aliases={}] - Custom path aliases for modules.
|
|
11
19
|
* @returns {string} - The corrected absolute path.
|
|
12
20
|
*/
|
|
13
|
-
const normalizePath = (filename) => {
|
|
21
|
+
const normalizePath = (filename, aliases = {}) => {
|
|
14
22
|
if (!filename || filename === "anonymous") return process.cwd();
|
|
23
|
+
|
|
24
|
+
// Handle Aliases (like @/components)
|
|
25
|
+
for (const [prefix, replacement] of Object.entries(aliases)) {
|
|
26
|
+
if (filename.startsWith(prefix)) {
|
|
27
|
+
const resolvedPath = path.resolve(process.cwd(), filename.replace(prefix, replacement));
|
|
28
|
+
return resolvedPath;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
if (filename.startsWith("file://")) {
|
|
16
33
|
try {
|
|
17
34
|
return fileURLToPath(filename);
|
|
@@ -19,9 +36,93 @@ const normalizePath = (filename) => {
|
|
|
19
36
|
return filename;
|
|
20
37
|
}
|
|
21
38
|
}
|
|
39
|
+
|
|
22
40
|
return path.resolve(process.cwd(), filename);
|
|
23
41
|
};
|
|
24
42
|
|
|
43
|
+
const VAR_PATTERN = /SOMMARK_UNRESOLVED_v_(.+?)_SOMMARK/g;
|
|
44
|
+
const VAR_PREFIX = "SOMMARK_UNRESOLVED_v_";
|
|
45
|
+
const VAR_SUFFIX = "_SOMMARK";
|
|
46
|
+
|
|
47
|
+
const resolveAstVariables = (nodes, variables) => {
|
|
48
|
+
if (!nodes) return;
|
|
49
|
+
|
|
50
|
+
for (const node of nodes) {
|
|
51
|
+
if (node.type === TEXT) {
|
|
52
|
+
if (node.text.includes(VAR_PREFIX)) {
|
|
53
|
+
node.text = node.text.replace(VAR_PATTERN, (match, key) => {
|
|
54
|
+
if (variables[key] !== undefined) {
|
|
55
|
+
if (!variables.__consumed__) {
|
|
56
|
+
Object.defineProperty(variables, "__consumed__", {
|
|
57
|
+
value: new Set(),
|
|
58
|
+
writable: true,
|
|
59
|
+
enumerable: false,
|
|
60
|
+
configurable: true
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
variables.__consumed__.add(key);
|
|
64
|
+
return String(variables[key]);
|
|
65
|
+
}
|
|
66
|
+
return match;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
} else if (node.type === BLOCK) {
|
|
70
|
+
// Resolve any unresolved variables in block arguments
|
|
71
|
+
for (const [argKey, argVal] of Object.entries(node.args)) {
|
|
72
|
+
if (typeof argVal === "string" && argVal.startsWith(VAR_PREFIX) && argVal.endsWith(VAR_SUFFIX)) {
|
|
73
|
+
const varKey = argVal.slice(VAR_PREFIX.length, -VAR_SUFFIX.length);
|
|
74
|
+
if (variables[varKey] !== undefined) {
|
|
75
|
+
node.args[argKey] = variables[varKey];
|
|
76
|
+
if (!variables.__consumed__) {
|
|
77
|
+
Object.defineProperty(variables, "__consumed__", {
|
|
78
|
+
value: new Set(),
|
|
79
|
+
writable: true,
|
|
80
|
+
enumerable: false,
|
|
81
|
+
configurable: true
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
variables.__consumed__.add(varKey);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (node.body) {
|
|
89
|
+
resolveAstVariables(node.body, variables);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Hand-optimized AST cloner.
|
|
97
|
+
* Native structuredClone is extremely slow for basic JSON-like tree data.
|
|
98
|
+
* This helper achieves up to 11x faster performance by cloning only required AST fields.
|
|
99
|
+
*/
|
|
100
|
+
const cloneAst = (nodes) => {
|
|
101
|
+
if (!nodes) return [];
|
|
102
|
+
const len = nodes.length;
|
|
103
|
+
const copy = new Array(len);
|
|
104
|
+
for (let i = 0; i < len; i++) {
|
|
105
|
+
const node = nodes[i];
|
|
106
|
+
const nodeCopy = {
|
|
107
|
+
type: node.type,
|
|
108
|
+
range: node.range
|
|
109
|
+
};
|
|
110
|
+
if (node.structure !== undefined) nodeCopy.structure = node.structure;
|
|
111
|
+
if (node.text !== undefined) nodeCopy.text = node.text;
|
|
112
|
+
if (node.id !== undefined) nodeCopy.id = node.id;
|
|
113
|
+
if (node.code !== undefined) nodeCopy.code = node.code;
|
|
114
|
+
if (node.isSelfClosing !== undefined) nodeCopy.isSelfClosing = node.isSelfClosing;
|
|
115
|
+
if (node.args !== undefined) {
|
|
116
|
+
nodeCopy.args = { ...node.args };
|
|
117
|
+
}
|
|
118
|
+
if (node.body !== undefined) {
|
|
119
|
+
nodeCopy.body = cloneAst(node.body);
|
|
120
|
+
}
|
|
121
|
+
copy[i] = nodeCopy;
|
|
122
|
+
}
|
|
123
|
+
return copy;
|
|
124
|
+
};
|
|
125
|
+
|
|
25
126
|
/**
|
|
26
127
|
* Handles all [import] and [$use-module] blocks in your code.
|
|
27
128
|
* It loads the requested files, checks for errors, and puts the content into the main document.
|
|
@@ -33,9 +134,83 @@ const normalizePath = (filename) => {
|
|
|
33
134
|
export async function resolveModules(ast, context) {
|
|
34
135
|
const modules = new Map();
|
|
35
136
|
const filename = context.filename || "anonymous";
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
137
|
+
const importAliases = context.instance.importAliases || {};
|
|
138
|
+
const absFilename = normalizePath(filename, importAliases);
|
|
139
|
+
|
|
140
|
+
// baseDir can be a local path
|
|
141
|
+
const baseDir = context.instance.baseDir || ((filename === "anonymous") ? absFilename : path.dirname(absFilename));
|
|
142
|
+
|
|
143
|
+
// 1. Helper: Trim AST to remove file-boundary whitespace and "ghost" newlines
|
|
144
|
+
const trimAst = (nodes) => {
|
|
145
|
+
if (!nodes) return [];
|
|
146
|
+
|
|
147
|
+
// 1. Filter out internal whitespace-only nodes that are adjacent to non-rendering nodes
|
|
148
|
+
// (Comments, Imports, etc. shouldn't leave "ghost" newlines)
|
|
149
|
+
const nonRenderingTypes = [COMMENT, IMPORT, USE_MODULE];
|
|
150
|
+
let res = nodes.filter((node, idx) => {
|
|
151
|
+
if (node.type !== TEXT || node.text.trim() !== "") return true;
|
|
152
|
+
|
|
153
|
+
const prev = nodes[idx - 1];
|
|
154
|
+
const next = nodes[idx + 1];
|
|
155
|
+
const isAdjacentToNonRendering =
|
|
156
|
+
(prev && nonRenderingTypes.includes(prev.type)) ||
|
|
157
|
+
(next && nonRenderingTypes.includes(next.type));
|
|
158
|
+
|
|
159
|
+
return !isAdjacentToNonRendering;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// 2. Final pass: trim leading/trailing newlines from the remaining boundary text nodes
|
|
163
|
+
if (res.length > 0 && res[0].type === TEXT) {
|
|
164
|
+
res[0].text = res[0].text.replace(/^[\r\n]+/, "");
|
|
165
|
+
}
|
|
166
|
+
if (res.length > 0 && res[res.length - 1].type === TEXT) {
|
|
167
|
+
res[res.length - 1].text = res[res.length - 1].text.replace(/[\r\n]+\s*$/, "");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. Remove any nodes that became purely empty after trimming
|
|
171
|
+
return res.filter(node => node.type !== TEXT || node.text !== "");
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// 2. Helper: Inject Slots with Indentation Propagation
|
|
175
|
+
const injectSlots = (nodes, callerBody) => {
|
|
176
|
+
const result = [];
|
|
177
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
178
|
+
const child = nodes[i];
|
|
179
|
+
if (child.type === SLOT) {
|
|
180
|
+
if (callerBody && callerBody.length > 0) {
|
|
181
|
+
// Detect leading indentation from the preceding text node
|
|
182
|
+
let indentation = "";
|
|
183
|
+
const prev = result[result.length - 1];
|
|
184
|
+
if (prev && prev.type === TEXT) {
|
|
185
|
+
const lines = prev.text.split("\n");
|
|
186
|
+
const lastLine = lines[lines.length - 1];
|
|
187
|
+
if (lastLine.trim() === "" && lastLine.length > 0) {
|
|
188
|
+
indentation = lastLine;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Clone and Indent caller body if needed
|
|
193
|
+
const indentedBody = callerBody.map(node => {
|
|
194
|
+
if (node.type === TEXT && indentation) {
|
|
195
|
+
return { ...node, text: node.text.split("\n").map((line, idx) => idx === 0 ? line : indentation + line).join("\n") };
|
|
196
|
+
}
|
|
197
|
+
return { ...node };
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
result.push(...indentedBody);
|
|
201
|
+
} else {
|
|
202
|
+
result.push(...child.body);
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
if (child.body && Array.isArray(child.body)) {
|
|
206
|
+
child.body = injectSlots(child.body, callerBody);
|
|
207
|
+
}
|
|
208
|
+
result.push(child);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
};
|
|
213
|
+
|
|
39
214
|
let hasContentStarted = false;
|
|
40
215
|
|
|
41
216
|
const processNodes = async (nodes, currentBaseDir, isTopLevel = false) => {
|
|
@@ -50,32 +225,45 @@ export async function resolveModules(ast, context) {
|
|
|
50
225
|
|
|
51
226
|
const alias = Object.keys(node.args).find(k => isNaN(k));
|
|
52
227
|
let filePath = alias ? node.args[alias] : node.args[0];
|
|
53
|
-
|
|
54
|
-
if (typeof filePath === "string") {
|
|
55
|
-
filePath = filePath.trim().replace(/^["']|["']$/g, "");
|
|
56
|
-
}
|
|
228
|
+
if (typeof filePath === "string") filePath = filePath.trim().replace(/^["']|["']$/g, "");
|
|
57
229
|
|
|
58
|
-
|
|
59
|
-
|
|
230
|
+
// 1a. Handle Aliases
|
|
231
|
+
let resolvedPath = filePath;
|
|
232
|
+
for (const [prefix, replacement] of Object.entries(importAliases)) {
|
|
233
|
+
if (filePath.startsWith(prefix)) {
|
|
234
|
+
resolvedPath = path.resolve(process.cwd(), filePath.replace(prefix, replacement));
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
60
237
|
}
|
|
61
238
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
239
|
+
// 1b. Resolve relative to current base (FS)
|
|
240
|
+
const absolutePath = resolveModulePath(resolvedPath, currentBaseDir);
|
|
241
|
+
|
|
242
|
+
// Local Path Resolution with Auto-Extension
|
|
243
|
+
let localPath = absolutePath;
|
|
244
|
+
if (!fs.existsSync(localPath) && !localPath.endsWith(".smark")) {
|
|
245
|
+
const withSmark = localPath + ".smark";
|
|
246
|
+
if (fs.existsSync(withSmark)) localPath = withSmark;
|
|
247
|
+
}
|
|
248
|
+
if (!fs.existsSync(localPath)) {
|
|
65
249
|
runtimeError([`<$red:Module Path Error:$> File not found: <$magenta:${filePath}$> at line <$yellow:${node.range.start.line + 1}$>`]);
|
|
66
250
|
}
|
|
251
|
+
let mod = { path: absolutePath, localPath: localPath, type: "smark" };
|
|
67
252
|
|
|
68
|
-
const ext = path.extname(
|
|
253
|
+
const ext = path.extname(mod.localPath).slice(1);
|
|
69
254
|
if (ext !== "smark") {
|
|
70
255
|
runtimeError([`<$red:Module Extension Error:$> Unsupported extension .${ext} for module <$magenta:${alias}$>. Only .smark files are supported.`]);
|
|
71
256
|
}
|
|
72
257
|
|
|
73
|
-
modules.set(alias, {
|
|
74
|
-
|
|
75
|
-
// Remove import node from AST
|
|
258
|
+
modules.set(alias, { ...mod, used: false, range: node.range });
|
|
76
259
|
nodes.splice(i, 1);
|
|
260
|
+
const next = nodes[i];
|
|
261
|
+
if (next && next.type === TEXT && next.text.startsWith("\n")) {
|
|
262
|
+
next.text = next.text.slice(1);
|
|
263
|
+
if (next.text === "") nodes.splice(i, 1);
|
|
264
|
+
}
|
|
77
265
|
i--;
|
|
78
|
-
}
|
|
266
|
+
}
|
|
79
267
|
// 2. Handle Usage Node: [$use-module = alias]
|
|
80
268
|
else if (node.type === USE_MODULE) {
|
|
81
269
|
hasContentStarted = true;
|
|
@@ -86,57 +274,145 @@ export async function resolveModules(ast, context) {
|
|
|
86
274
|
|
|
87
275
|
const mod = modules.get(alias);
|
|
88
276
|
mod.used = true;
|
|
89
|
-
|
|
90
|
-
if (mod.type === "smark") {
|
|
91
|
-
const stack = context.importStack || [];
|
|
92
|
-
if (stack.includes(mod.path)) {
|
|
93
|
-
const chain = [...stack, mod.path].map(p => path.basename(p)).join(" -> ");
|
|
94
|
-
runtimeError([
|
|
95
|
-
`{line}<$red:Circular Dependency Detected$>:`,
|
|
96
|
-
`<$yellow:The following import chain was found:$>`,
|
|
97
|
-
`<$magenta:${chain}$>{line}`
|
|
98
|
-
]);
|
|
99
|
-
}
|
|
100
277
|
|
|
101
|
-
|
|
102
|
-
|
|
278
|
+
const stack = context.importStack || [];
|
|
279
|
+
const maxDepth = context.instance.security?.maxDepth ?? 5;
|
|
280
|
+
if (stack.length >= maxDepth) {
|
|
281
|
+
runtimeError([`<$red:Security Error:$> Recursion Guard: Maximum Smark compilation depth exceeded (limit is ${maxDepth}).`]);
|
|
282
|
+
}
|
|
283
|
+
if (stack.includes(mod.path)) {
|
|
284
|
+
runtimeError([`<$red:Circular Dependency Detected$>: ${mod.path}`]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const cached = context.instance.moduleCache.get(mod.localPath);
|
|
288
|
+
let expandedNodes;
|
|
289
|
+
if (cached) {
|
|
290
|
+
expandedNodes = trimAst(cloneAst(cached));
|
|
291
|
+
} else {
|
|
292
|
+
const content = fs.readFileSync(mod.localPath, "utf-8");
|
|
103
293
|
const SomMark = context.instance.constructor;
|
|
104
|
-
|
|
105
294
|
const subSmark = new SomMark({
|
|
106
295
|
src: content,
|
|
107
296
|
format: context.format,
|
|
108
297
|
filename: mod.path,
|
|
298
|
+
baseDir: path.dirname(mod.localPath),
|
|
109
299
|
mapperFile: context.instance.mapperFile,
|
|
110
|
-
removeComments: context.instance.removeComments,
|
|
111
300
|
placeholders: context.instance.placeholders,
|
|
112
|
-
|
|
301
|
+
variables: {},
|
|
302
|
+
importAliases: context.instance.importAliases,
|
|
303
|
+
customProps: context.instance.customProps,
|
|
304
|
+
fallbackTarget: context.instance.fallbackTarget,
|
|
305
|
+
removeComments: context.instance.removeComments,
|
|
306
|
+
security: context.instance.security,
|
|
307
|
+
showSpinner: context.instance.showSpinner,
|
|
308
|
+
importStack: [...stack, absFilename],
|
|
309
|
+
moduleIdentityToken: context.instance.moduleIdentityToken,
|
|
310
|
+
moduleCache: context.instance.moduleCache
|
|
113
311
|
});
|
|
114
|
-
|
|
312
|
+
|
|
115
313
|
const subAst = await subSmark.parse();
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
314
|
+
context.instance.moduleCache.set(mod.localPath, subAst);
|
|
315
|
+
expandedNodes = trimAst(subAst);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const boundaryNode = {
|
|
319
|
+
type: BLOCK,
|
|
320
|
+
id: context.instance.moduleIdentityToken,
|
|
321
|
+
args: { filename: mod.path },
|
|
322
|
+
body: expandedNodes
|
|
323
|
+
};
|
|
324
|
+
nodes.splice(i, 1, boundaryNode);
|
|
325
|
+
const next = nodes[i + 1];
|
|
326
|
+
if (next && next.type === TEXT && next.text.startsWith("\n")) {
|
|
327
|
+
next.text = next.text.slice(1);
|
|
328
|
+
if (next.text === "") nodes.splice(i + 1, 1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// 3. Handle Component Usage: [Alias] ... [end]
|
|
332
|
+
else if (node.type === BLOCK && modules.has(node.id)) {
|
|
333
|
+
hasContentStarted = true;
|
|
334
|
+
const mod = modules.get(node.id);
|
|
335
|
+
mod.used = true;
|
|
336
|
+
const stack = context.importStack || [];
|
|
337
|
+
const maxDepth = context.instance.security?.maxDepth ?? 5;
|
|
338
|
+
if (stack.length >= maxDepth) {
|
|
339
|
+
runtimeError([`<$red:Security Error:$> Recursion Guard: Maximum Smark compilation depth exceeded (limit is ${maxDepth}).`]);
|
|
340
|
+
}
|
|
341
|
+
if (stack.includes(mod.path)) {
|
|
342
|
+
runtimeError([`<$red:Circular Dependency Detected$>: ${mod.path}`]);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const cached = context.instance.moduleCache.get(mod.localPath);
|
|
346
|
+
let subAst;
|
|
347
|
+
if (cached) {
|
|
348
|
+
subAst = cloneAst(cached);
|
|
130
349
|
} else {
|
|
131
|
-
|
|
350
|
+
const content = fs.readFileSync(mod.localPath, "utf-8");
|
|
351
|
+
const SomMark = context.instance.constructor;
|
|
352
|
+
const subSmark = new SomMark({
|
|
353
|
+
src: content,
|
|
354
|
+
format: context.format,
|
|
355
|
+
filename: mod.path,
|
|
356
|
+
baseDir: path.dirname(mod.localPath),
|
|
357
|
+
mapperFile: context.instance.mapperFile,
|
|
358
|
+
placeholders: context.instance.placeholders,
|
|
359
|
+
variables: {}, // Parse without variables to keep the cached AST pure
|
|
360
|
+
importAliases: context.instance.importAliases,
|
|
361
|
+
customProps: context.instance.customProps,
|
|
362
|
+
fallbackTarget: context.instance.fallbackTarget,
|
|
363
|
+
removeComments: context.instance.removeComments,
|
|
364
|
+
security: context.instance.security,
|
|
365
|
+
showSpinner: context.instance.showSpinner,
|
|
366
|
+
importStack: [...stack, absFilename],
|
|
367
|
+
moduleCache: context.instance.moduleCache
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
subAst = await subSmark.parse();
|
|
371
|
+
context.instance.moduleCache.set(mod.localPath, subAst);
|
|
372
|
+
subAst = cloneAst(subAst);
|
|
132
373
|
}
|
|
133
|
-
|
|
134
|
-
|
|
374
|
+
|
|
375
|
+
// Dynamically resolve variable placeholders inside the cloned AST
|
|
376
|
+
resolveAstVariables(subAst, node.args);
|
|
377
|
+
|
|
378
|
+
await processNodes(node.body, currentBaseDir, false);
|
|
379
|
+
const expandedNodes = injectSlots(trimAst(subAst), trimAst(node.body));
|
|
380
|
+
const rootTag = expandedNodes.find(n => n.type === BLOCK);
|
|
381
|
+
if (rootTag) {
|
|
382
|
+
const consumed = node.args.__consumed__ || new Set();
|
|
383
|
+
|
|
384
|
+
const publicArgs = Object.fromEntries(
|
|
385
|
+
Object.entries(node.args).filter(([key]) => {
|
|
386
|
+
if (key === "__consumed__") return false;
|
|
387
|
+
if (consumed.has(key)) return false; // THE FIX: Filter if hit by v{}
|
|
388
|
+
return true;
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
rootTag.args = { ...rootTag.args, ...publicArgs };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const boundaryNode = {
|
|
395
|
+
type: BLOCK,
|
|
396
|
+
id: context.instance.moduleIdentityToken,
|
|
397
|
+
args: { filename: mod.path },
|
|
398
|
+
body: expandedNodes
|
|
399
|
+
};
|
|
400
|
+
nodes.splice(i, 1, boundaryNode);
|
|
401
|
+
}
|
|
402
|
+
// 4. Handle Regular Blocks: Process body recursively for nested components and trim whitespace
|
|
403
|
+
else if (node.type === BLOCK) {
|
|
404
|
+
hasContentStarted = true;
|
|
405
|
+
if (node.body && Array.isArray(node.body)) {
|
|
406
|
+
node.body = trimAst(node.body);
|
|
407
|
+
await processNodes(node.body, currentBaseDir, false);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 4. Recurse into children (Standard Blocks)
|
|
135
412
|
else {
|
|
136
413
|
if (node.type === TEXT && node.text.trim() === "") {
|
|
137
|
-
//
|
|
414
|
+
// Structural whitespace
|
|
138
415
|
} else if (node.type !== COMMENT) {
|
|
139
|
-
// Any meaningful node that isn't an IMPORT or COMMENT is considered "Content"
|
|
140
416
|
hasContentStarted = true;
|
|
141
417
|
}
|
|
142
418
|
|