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