@jxsuite/compiler 0.1.0 → 0.5.1
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/dist/compiler.js +257 -90
- package/package.json +21 -10
- package/src/compiler.js +15 -4
- package/src/shared.js +251 -6
- package/src/site/content-loader.js +3 -3
- package/src/site/pages-discovery.js +20 -8
- package/src/site/site-build.js +178 -14
- package/src/targets/compile-client.js +9 -7
- package/src/targets/compile-element.js +57 -2
- package/src/targets/compile-markdown.js +942 -0
- package/src/targets/compile-static.js +3 -2
package/src/site/site-build.js
CHANGED
|
@@ -29,11 +29,15 @@ import {
|
|
|
29
29
|
buildInitialScope,
|
|
30
30
|
isTemplateString,
|
|
31
31
|
evaluateStaticTemplate,
|
|
32
|
+
preRenderComponentHtml,
|
|
33
|
+
isComponentFullyStatic,
|
|
34
|
+
buildComponentCSS,
|
|
32
35
|
DEFAULT_REACTIVITY_SRC,
|
|
33
36
|
DEFAULT_LIT_HTML_SRC,
|
|
34
37
|
} from "../shared.js";
|
|
35
38
|
import { loadCollections, loadContentConfig, resolveCollectionRefs } from "./content-loader.js";
|
|
36
39
|
import { resolvePrototypes } from "./prototype-resolver.js";
|
|
40
|
+
import { compileMarkdown } from "../targets/compile-markdown.js";
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
43
|
* Build an entire Jx site from a project directory.
|
|
@@ -96,10 +100,16 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
96
100
|
const componentsDir = resolve(projectRoot, "components");
|
|
97
101
|
/** @type {string[]} */
|
|
98
102
|
const compiledComponentTags = [];
|
|
103
|
+
/** @type {Map<string, string>} */
|
|
104
|
+
const preRendered = new Map(); // tagName → innerHTML (default state, fallback)
|
|
105
|
+
/** @type {Map<string, string>} */
|
|
106
|
+
const componentCSS = new Map(); // tagName → CSS text
|
|
107
|
+
/** @type {Map<string, any>} */
|
|
108
|
+
const componentDefs = new Map(); // tagName → parsed component definition
|
|
99
109
|
if (existsSync(componentsDir)) {
|
|
100
110
|
log("Compiling components...");
|
|
101
|
-
const componentFiles = readdirSync(componentsDir).filter(
|
|
102
|
-
f.endsWith(".json"),
|
|
111
|
+
const componentFiles = readdirSync(componentsDir).filter(
|
|
112
|
+
(/** @type {string} */ f) => f.endsWith(".json") || f.endsWith(".md"),
|
|
103
113
|
);
|
|
104
114
|
const componentOutDir = resolve(outDir, "components");
|
|
105
115
|
mkdirSync(componentOutDir, { recursive: true });
|
|
@@ -116,6 +126,25 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
116
126
|
if (f.tagName) compiledComponentTags.push(f.tagName);
|
|
117
127
|
fileCount++;
|
|
118
128
|
}
|
|
129
|
+
|
|
130
|
+
// Pre-render component HTML scaffold and CSS sidecar
|
|
131
|
+
const doc = componentPath.endsWith(".md")
|
|
132
|
+
? (await import("@jxsuite/parser/transpile")).transpileJxMarkdown(
|
|
133
|
+
readFileSync(componentPath, "utf8"),
|
|
134
|
+
)
|
|
135
|
+
: JSON.parse(readFileSync(componentPath, "utf8"));
|
|
136
|
+
if (doc.tagName) {
|
|
137
|
+
componentDefs.set(doc.tagName, doc);
|
|
138
|
+
const innerHTML = preRenderComponentHtml(doc);
|
|
139
|
+
if (innerHTML) preRendered.set(doc.tagName, innerHTML);
|
|
140
|
+
|
|
141
|
+
const css = buildComponentCSS(doc.tagName, doc.style);
|
|
142
|
+
if (css) {
|
|
143
|
+
componentCSS.set(doc.tagName, css);
|
|
144
|
+
writeFileSync(resolve(componentOutDir, `${doc.tagName}.css`), css, "utf8");
|
|
145
|
+
fileCount++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
119
148
|
} catch (e) {
|
|
120
149
|
const err = /** @type {any} */ (e);
|
|
121
150
|
errors.push(`Error compiling component ${file}: ${err.message}`);
|
|
@@ -134,9 +163,24 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
134
163
|
log(` Compiling ${route.urlPattern} ...`);
|
|
135
164
|
const result = await compilePage(route, projectConfig, projectRoot, collections);
|
|
136
165
|
|
|
137
|
-
// Inject component
|
|
166
|
+
// Inject pre-rendered component HTML scaffolding (instance-aware)
|
|
167
|
+
// Must happen before script injection so we know which tags are fully static
|
|
168
|
+
/** @type {Set<string>} */
|
|
169
|
+
let staticTags = new Set();
|
|
170
|
+
if (componentDefs.size > 0) {
|
|
171
|
+
const preResult = injectPreRenderedComponents(result.html, preRendered, componentDefs);
|
|
172
|
+
result.html = preResult.html;
|
|
173
|
+
staticTags = preResult.staticTags;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Inject component CSS and JS scripts
|
|
138
177
|
if (compiledComponentTags.length > 0) {
|
|
139
|
-
result.html = injectComponentScripts(
|
|
178
|
+
result.html = injectComponentScripts(
|
|
179
|
+
result.html,
|
|
180
|
+
compiledComponentTags,
|
|
181
|
+
componentCSS,
|
|
182
|
+
staticTags,
|
|
183
|
+
);
|
|
140
184
|
}
|
|
141
185
|
|
|
142
186
|
// Determine output path
|
|
@@ -145,6 +189,19 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
145
189
|
writeFileSync(outPath, result.html, "utf8");
|
|
146
190
|
fileCount++;
|
|
147
191
|
|
|
192
|
+
// Write markdown export alongside HTML
|
|
193
|
+
try {
|
|
194
|
+
const md = compileMarkdown(result.doc, componentDefs);
|
|
195
|
+
if (md.content) {
|
|
196
|
+
const mdPath = outPath.replace(/\.html$/, ".md");
|
|
197
|
+
writeFileSync(mdPath, md.content, "utf8");
|
|
198
|
+
fileCount++;
|
|
199
|
+
}
|
|
200
|
+
} catch (e) {
|
|
201
|
+
const err = /** @type {any} */ (e);
|
|
202
|
+
errors.push(`Error exporting markdown for ${route.urlPattern}: ${err.message}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
148
205
|
// Write any additional files (island modules, etc.)
|
|
149
206
|
for (const file of result.files) {
|
|
150
207
|
const filePath = resolve(dirname(outPath), file.path);
|
|
@@ -209,11 +266,17 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
209
266
|
* @param {any} projectConfig
|
|
210
267
|
* @param {string} projectRoot
|
|
211
268
|
* @param {Map<string, any[]>} [collections]
|
|
212
|
-
* @returns {Promise<{ html: string; files: any[]; serverHandler: string | null }>}
|
|
269
|
+
* @returns {Promise<{ html: string; files: any[]; serverHandler: string | null; doc: any }>}
|
|
213
270
|
*/
|
|
214
271
|
async function compilePage(route, projectConfig, projectRoot, collections = new Map()) {
|
|
215
272
|
// Load the raw page document
|
|
216
|
-
let pageDoc
|
|
273
|
+
let pageDoc;
|
|
274
|
+
if (route.sourcePath.endsWith(".md")) {
|
|
275
|
+
const { transpileJxMarkdown } = await import("@jxsuite/parser/transpile");
|
|
276
|
+
pageDoc = transpileJxMarkdown(readFileSync(route.sourcePath, "utf8"));
|
|
277
|
+
} else {
|
|
278
|
+
pageDoc = JSON.parse(readFileSync(route.sourcePath, "utf8"));
|
|
279
|
+
}
|
|
217
280
|
|
|
218
281
|
// Resolve layout (wraps page in layout with slot distribution)
|
|
219
282
|
const layoutDoc = resolveLayout(pageDoc, projectConfig, projectRoot);
|
|
@@ -278,10 +341,16 @@ async function compilePage(route, projectConfig, projectRoot, collections = new
|
|
|
278
341
|
pageUrl: route.urlPattern,
|
|
279
342
|
});
|
|
280
343
|
|
|
344
|
+
// Merge project-level $media into the layout document so responsive queries are available
|
|
345
|
+
if (projectConfig.$media) {
|
|
346
|
+
layoutDoc.$media = { ...projectConfig.$media, ...layoutDoc.$media };
|
|
347
|
+
}
|
|
348
|
+
|
|
281
349
|
// Compile the document using the existing compiler
|
|
282
350
|
const result = await compile(layoutDoc, {
|
|
283
351
|
title,
|
|
284
352
|
lang: projectConfig.defaults?.lang ?? "en",
|
|
353
|
+
projectStyle: projectConfig.style ?? null,
|
|
285
354
|
});
|
|
286
355
|
|
|
287
356
|
// Post-process: inject merged <head> content into the compiled HTML
|
|
@@ -307,7 +376,7 @@ async function compilePage(route, projectConfig, projectRoot, collections = new
|
|
|
307
376
|
// No server entries — that's fine
|
|
308
377
|
}
|
|
309
378
|
|
|
310
|
-
return { html: result.html, files: result.files, serverHandler };
|
|
379
|
+
return { html: result.html, files: result.files, serverHandler, doc: layoutDoc };
|
|
311
380
|
}
|
|
312
381
|
|
|
313
382
|
/**
|
|
@@ -386,7 +455,13 @@ function resolveDocTemplates(node, scope) {
|
|
|
386
455
|
if (!node || typeof node !== "object") return;
|
|
387
456
|
|
|
388
457
|
if (typeof node.innerHTML === "string" && isTemplateString(node.innerHTML)) {
|
|
389
|
-
|
|
458
|
+
const resolved = evaluateStaticTemplate(node.innerHTML, scope);
|
|
459
|
+
if (resolved != null) {
|
|
460
|
+
// Encode any remaining `${` as HTML entities so the compile phase won't
|
|
461
|
+
// re-interpret them as template expressions. After resolution, any `${` in the
|
|
462
|
+
// result is literal content (e.g., code examples), not an intentional template.
|
|
463
|
+
node.innerHTML = resolved.replaceAll("${", "${");
|
|
464
|
+
}
|
|
390
465
|
}
|
|
391
466
|
if (typeof node.textContent === "string" && isTemplateString(node.textContent)) {
|
|
392
467
|
node.textContent = evaluateStaticTemplate(node.textContent, scope) ?? node.textContent;
|
|
@@ -413,20 +488,40 @@ function resolveDocTemplates(node, scope) {
|
|
|
413
488
|
}
|
|
414
489
|
|
|
415
490
|
/**
|
|
416
|
-
* Inject component script tags into compiled HTML for any referenced custom elements.
|
|
417
|
-
* import map and module scripts before </body>.
|
|
491
|
+
* Inject component script and CSS link tags into compiled HTML for any referenced custom elements.
|
|
492
|
+
* Adds an import map and module scripts before </body>, and CSS links in <head>.
|
|
418
493
|
*
|
|
419
494
|
* @param {string} html
|
|
420
495
|
* @param {string[]} allComponentTags - All compiled component tag names
|
|
496
|
+
* @param {Map<string, string>} [cssMap] - TagName → CSS text (for link injection)
|
|
497
|
+
* @param {Set<string>} [staticTags] - Tags where ALL instances are fully static (skip JS)
|
|
421
498
|
* @returns {string}
|
|
422
499
|
*/
|
|
423
|
-
function injectComponentScripts(
|
|
500
|
+
function injectComponentScripts(
|
|
501
|
+
html,
|
|
502
|
+
allComponentTags,
|
|
503
|
+
cssMap = new Map(),
|
|
504
|
+
staticTags = new Set(),
|
|
505
|
+
) {
|
|
424
506
|
// Find which components are actually referenced in this page
|
|
425
507
|
const usedTags = allComponentTags.filter(
|
|
426
508
|
(/** @type {string} */ tag) => html.includes(`<${tag}`), // matches <tag> and <tag ...>
|
|
427
509
|
);
|
|
428
510
|
if (usedTags.length === 0) return html;
|
|
429
511
|
|
|
512
|
+
// Inject CSS links in <head> for ALL components that have CSS sidecars
|
|
513
|
+
const cssLinks = usedTags
|
|
514
|
+
.filter((/** @type {string} */ tag) => cssMap.has(tag))
|
|
515
|
+
.map((/** @type {string} */ tag) => `<link rel="stylesheet" href="./components/${tag}.css">`)
|
|
516
|
+
.join("\n ");
|
|
517
|
+
if (cssLinks) {
|
|
518
|
+
html = html.replace("</head>", ` ${cssLinks}\n</head>`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Only inject JS for components that have non-static instances
|
|
522
|
+
const jsTags = usedTags.filter((/** @type {string} */ tag) => !staticTags.has(tag));
|
|
523
|
+
if (jsTags.length === 0) return html;
|
|
524
|
+
|
|
430
525
|
// Build import map (needed for @vue/reactivity and lit-html)
|
|
431
526
|
const importMap = `<script type="importmap">
|
|
432
527
|
{
|
|
@@ -437,9 +532,9 @@ function injectComponentScripts(html, allComponentTags) {
|
|
|
437
532
|
}
|
|
438
533
|
</script>`;
|
|
439
534
|
|
|
440
|
-
const moduleScripts =
|
|
535
|
+
const moduleScripts = jsTags
|
|
441
536
|
.map(
|
|
442
|
-
(/** @type {string} */ tag) => `<script type="module" src="
|
|
537
|
+
(/** @type {string} */ tag) => `<script type="module" src="./components/${tag}.js"></script>`,
|
|
443
538
|
)
|
|
444
539
|
.join("\n ");
|
|
445
540
|
|
|
@@ -450,6 +545,74 @@ function injectComponentScripts(html, allComponentTags) {
|
|
|
450
545
|
return html.replace("</body>", ` ${injection}\n</body>`);
|
|
451
546
|
}
|
|
452
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Inject pre-rendered HTML scaffolding into component tags, using instance-specific props.
|
|
550
|
+
*
|
|
551
|
+
* @param {string} html
|
|
552
|
+
* @param {Map<string, string>} preRendered - TagName → default innerHTML (fallback)
|
|
553
|
+
* @param {Map<string, any>} componentDefs - TagName → parsed component definition
|
|
554
|
+
* @returns {{ html: string; staticTags: Set<string> }}
|
|
555
|
+
*/
|
|
556
|
+
function injectPreRenderedComponents(html, preRendered, componentDefs) {
|
|
557
|
+
/** @type {Set<string>} */
|
|
558
|
+
const staticTags = new Set();
|
|
559
|
+
/** @type {Map<string, boolean>} */
|
|
560
|
+
const tagHasNonStaticInstance = new Map();
|
|
561
|
+
|
|
562
|
+
for (const [tag] of componentDefs) {
|
|
563
|
+
// Match both empty tags and tags with inner content (for slotted components)
|
|
564
|
+
const pattern = new RegExp(`<${tag}(\\s[^>]*?)?>([\\s\\S]*?)</${tag}>`, "g");
|
|
565
|
+
html = html.replace(pattern, (match, attrsStr, existingInner) => {
|
|
566
|
+
const attrs = attrsStr ?? "";
|
|
567
|
+
const doc = componentDefs.get(tag);
|
|
568
|
+
if (!doc) {
|
|
569
|
+
// No definition, use default pre-rendered content
|
|
570
|
+
const fallback = preRendered.get(tag) ?? "";
|
|
571
|
+
return `<${tag}${attrs}>${fallback}</${tag}>`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Extract data-jx-props from the attribute string
|
|
575
|
+
/** @type {Record<string, any> | null} */
|
|
576
|
+
let props = null;
|
|
577
|
+
const propsMatch = attrs.match(/\sdata-jx-props="([^"]*)"/);
|
|
578
|
+
if (propsMatch) {
|
|
579
|
+
try {
|
|
580
|
+
props = JSON.parse(
|
|
581
|
+
propsMatch[1]
|
|
582
|
+
.replace(/"/g, '"')
|
|
583
|
+
.replace(/&/g, "&")
|
|
584
|
+
.replace(/</g, "<")
|
|
585
|
+
.replace(/>/g, ">")
|
|
586
|
+
.replace(/'/g, "'"),
|
|
587
|
+
);
|
|
588
|
+
} catch {}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Pre-render with instance-specific props
|
|
592
|
+
const slotContent = existingInner.trim() || null;
|
|
593
|
+
const innerHTML = preRenderComponentHtml(doc, props, slotContent);
|
|
594
|
+
|
|
595
|
+
const isStatic = isComponentFullyStatic(doc);
|
|
596
|
+
if (!isStatic) tagHasNonStaticInstance.set(tag, true);
|
|
597
|
+
|
|
598
|
+
if (isStatic) {
|
|
599
|
+
// Strip data-jx-props from attrs for fully static instances
|
|
600
|
+
let cleanAttrs = attrs.replace(/\s*data-jx-props="[^"]*"/, "");
|
|
601
|
+
return `<${tag}${cleanAttrs} data-jx-static>${innerHTML}</${tag}>`;
|
|
602
|
+
} else {
|
|
603
|
+
return `<${tag}${attrs} data-jx-prerendered>${innerHTML}</${tag}>`;
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Track whether ALL instances of this tag are static
|
|
608
|
+
if (componentDefs.has(tag) && !tagHasNonStaticInstance.has(tag)) {
|
|
609
|
+
staticTags.add(tag);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return { html, staticTags };
|
|
614
|
+
}
|
|
615
|
+
|
|
453
616
|
/**
|
|
454
617
|
* Inject <script type="module"> tags for npm package $elements (cherry-picked component imports).
|
|
455
618
|
* Bare specifiers are resolved to /node_modules/ paths.
|
|
@@ -462,7 +625,8 @@ function injectComponentScripts(html, allComponentTags) {
|
|
|
462
625
|
function injectNpmElementScripts(html, npmElements) {
|
|
463
626
|
const scripts = npmElements
|
|
464
627
|
.map(
|
|
465
|
-
(/** @type {string} */ spec) =>
|
|
628
|
+
(/** @type {string} */ spec) =>
|
|
629
|
+
`<script type="module" src="./node_modules/${spec}"></script>`,
|
|
466
630
|
)
|
|
467
631
|
.join("\n ");
|
|
468
632
|
|
|
@@ -41,7 +41,7 @@ export function compileClient(raw, opts) {
|
|
|
41
41
|
} = opts;
|
|
42
42
|
|
|
43
43
|
const context = createCompileContext(raw, null, raw.state ?? {}, raw.$media ?? {});
|
|
44
|
-
const styleBlock = compileStyles(raw, raw.$media ?? {});
|
|
44
|
+
const styleBlock = compileStyles(raw, raw.$media ?? {}, opts.projectStyle ?? null);
|
|
45
45
|
|
|
46
46
|
// Collectors for bindings and handlers
|
|
47
47
|
const counter = { t: 0, s: 0, h: 0, m: 0, sw: 0, l: 0, needsLit: false };
|
|
@@ -164,11 +164,11 @@ export function compileClient(raw, opts) {
|
|
|
164
164
|
reactivitySrc,
|
|
165
165
|
);
|
|
166
166
|
|
|
167
|
-
// Build importmap entries
|
|
168
|
-
const importmapEntries = [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
// Build importmap entries — always include lit-html since compiled custom elements need it
|
|
168
|
+
const importmapEntries = [
|
|
169
|
+
` "@vue/reactivity": "${reactivitySrc}"`,
|
|
170
|
+
` "lit-html": "${litHtmlSrc}"`,
|
|
171
|
+
];
|
|
172
172
|
|
|
173
173
|
const html = `<!DOCTYPE html>
|
|
174
174
|
<html lang="en">
|
|
@@ -349,7 +349,9 @@ function buildClientNode(def, raw, context, bindings, handlers, counter) {
|
|
|
349
349
|
inner = "";
|
|
350
350
|
}
|
|
351
351
|
} else if (source.innerHTML) {
|
|
352
|
-
|
|
352
|
+
// resolveStaticValue may return null if innerHTML contains `${` from rendered content
|
|
353
|
+
// (e.g., code examples) that isn't an actual template expression. Fall back to raw value.
|
|
354
|
+
inner = resolveStaticValue(source.innerHTML, nextContext.scope) ?? source.innerHTML;
|
|
353
355
|
} else if (
|
|
354
356
|
source.children &&
|
|
355
357
|
typeof source.children === "object" &&
|
|
@@ -37,7 +37,12 @@ export async function compileElement(sourcePath, opts = {}) {
|
|
|
37
37
|
filePath = parentDir ? resolve(parentDir, srcPath) : resolve(srcPath);
|
|
38
38
|
if (visited.has(filePath)) return;
|
|
39
39
|
visited.add(filePath);
|
|
40
|
-
|
|
40
|
+
if (filePath.endsWith(".md")) {
|
|
41
|
+
const { transpileJxMarkdown } = await import("@jxsuite/parser/transpile");
|
|
42
|
+
doc = transpileJxMarkdown(readFileSync(filePath, "utf8"));
|
|
43
|
+
} else {
|
|
44
|
+
doc = JSON.parse(readFileSync(filePath, "utf8"));
|
|
45
|
+
}
|
|
41
46
|
} else {
|
|
42
47
|
doc = srcPath;
|
|
43
48
|
filePath = null;
|
|
@@ -288,8 +293,21 @@ export function emitElementModule(doc, className, elementImports) {
|
|
|
288
293
|
|
|
289
294
|
// connectedCallback
|
|
290
295
|
lines.push(" connectedCallback() {");
|
|
296
|
+
// Read $props from data-jx-props attribute (set by compiler for pre-rendered instances)
|
|
297
|
+
lines.push(" const _pa = this.getAttribute('data-jx-props');");
|
|
298
|
+
lines.push(" if (_pa) {");
|
|
299
|
+
lines.push(" try {");
|
|
300
|
+
lines.push(" const _p = JSON.parse(_pa);");
|
|
301
|
+
lines.push(" for (const [k, v] of Object.entries(_p)) {");
|
|
302
|
+
lines.push(" if (k in this.state) this.state[k] = v;");
|
|
303
|
+
lines.push(" }");
|
|
304
|
+
lines.push(" } catch {}");
|
|
305
|
+
lines.push(" this.removeAttribute('data-jx-props');");
|
|
306
|
+
lines.push(" }");
|
|
307
|
+
// Merge JS properties set before connection (by parent runtime).
|
|
308
|
+
// Only check own properties to avoid inherited DOM properties like `title`.
|
|
291
309
|
lines.push(" for (const key of Object.keys(this.state)) {");
|
|
292
|
-
lines.push(" if (key
|
|
310
|
+
lines.push(" if (this.hasOwnProperty(key) && this[key] !== undefined) {");
|
|
293
311
|
lines.push(" this.state[key] = this[key];");
|
|
294
312
|
lines.push(" }");
|
|
295
313
|
lines.push(" }");
|
|
@@ -333,7 +351,28 @@ export function emitElementModule(doc, className, elementImports) {
|
|
|
333
351
|
lines.push(" });");
|
|
334
352
|
}
|
|
335
353
|
}
|
|
354
|
+
const hasSlot = treeHasSlot(doc.children);
|
|
355
|
+
if (hasSlot) {
|
|
356
|
+
// Save light DOM children (slotted content) before clearing
|
|
357
|
+
lines.push(
|
|
358
|
+
" const _slotted = Array.from(this.childNodes).filter(n => n.nodeType === 1 || (n.nodeType === 3 && n.textContent.trim()));",
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
// Skip clearing innerHTML if content was pre-rendered with correct props
|
|
362
|
+
lines.push(" if (this.hasAttribute('data-jx-prerendered')) {");
|
|
363
|
+
lines.push(" this.removeAttribute('data-jx-prerendered');");
|
|
364
|
+
lines.push(" } else {");
|
|
365
|
+
lines.push(" this.innerHTML = '';");
|
|
366
|
+
lines.push(" }");
|
|
336
367
|
lines.push(" this.#dispose = effect(() => render(this.template(), this));");
|
|
368
|
+
if (hasSlot) {
|
|
369
|
+
// Replace <slot> placeholder with saved slotted content
|
|
370
|
+
lines.push(" const _slot = this.querySelector('slot');");
|
|
371
|
+
lines.push(" if (_slot && _slotted.length > 0) {");
|
|
372
|
+
lines.push(" for (const n of _slotted) _slot.before(n);");
|
|
373
|
+
lines.push(" _slot.remove();");
|
|
374
|
+
lines.push(" }");
|
|
375
|
+
}
|
|
337
376
|
lines.push(" }");
|
|
338
377
|
lines.push("");
|
|
339
378
|
|
|
@@ -617,3 +656,19 @@ function emitStyleString(styleDef) {
|
|
|
617
656
|
|
|
618
657
|
return parts.join("; ");
|
|
619
658
|
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Check if a children tree contains a `<slot>` element.
|
|
662
|
+
*
|
|
663
|
+
* @param {any} children
|
|
664
|
+
* @returns {boolean}
|
|
665
|
+
*/
|
|
666
|
+
function treeHasSlot(children) {
|
|
667
|
+
if (!Array.isArray(children)) return false;
|
|
668
|
+
for (const child of children) {
|
|
669
|
+
if (!child || typeof child !== "object") continue;
|
|
670
|
+
if (child.tagName === "slot") return true;
|
|
671
|
+
if (treeHasSlot(child.children)) return true;
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|