@jxsuite/compiler 0.6.1 → 0.7.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/dist/compiler.js +256 -252
- package/package.json +3 -3
- package/src/shared.js +103 -89
- package/src/site/content-loader.js +2 -2
- package/src/site/image-cache.js +1 -8
- package/src/site/image-transform.js +104 -6
- package/src/site/layout-resolver.js +2 -1
- package/src/site/prototype-resolver.js +12 -2
- package/src/site/site-build.js +123 -84
- package/src/targets/compile-element.js +14 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@apidevtools/json-schema-ref-parser": "^15.3.5",
|
|
40
|
-
"@jxsuite/parser": "^0.
|
|
41
|
-
"@jxsuite/runtime": "^0.
|
|
40
|
+
"@jxsuite/parser": "^0.6.2",
|
|
41
|
+
"@jxsuite/runtime": "^0.6.2",
|
|
42
42
|
"remark-gfm": "^4.0.1",
|
|
43
43
|
"remark-stringify": "^11.0.0",
|
|
44
44
|
"sharp": "^0.34.5",
|
package/src/shared.js
CHANGED
|
@@ -393,25 +393,7 @@ export function buildAttrs(def, scope) {
|
|
|
393
393
|
if (lang) out += ` lang="${escapeHtml(lang)}"`;
|
|
394
394
|
if (dir) out += ` dir="${escapeHtml(dir)}"`;
|
|
395
395
|
|
|
396
|
-
if (def.style) {
|
|
397
|
-
// Collect properties that have @media overrides — these must NOT be inline
|
|
398
|
-
// because inline styles (specificity 1,0,0,0) always beat stylesheet @media rules.
|
|
399
|
-
const mediaOverriddenProps = new Set();
|
|
400
|
-
for (const [k, v] of Object.entries(def.style)) {
|
|
401
|
-
if (k.startsWith("@") && v && typeof v === "object") {
|
|
402
|
-
for (const prop of Object.keys(/** @type {Record<string, any>} */ (v))) {
|
|
403
|
-
if (
|
|
404
|
-
!prop.startsWith(":") &&
|
|
405
|
-
!prop.startsWith(".") &&
|
|
406
|
-
!prop.startsWith("&") &&
|
|
407
|
-
!prop.startsWith("[")
|
|
408
|
-
) {
|
|
409
|
-
mediaOverriddenProps.add(prop);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
396
|
+
if (def.style && scope) {
|
|
415
397
|
const inline = Object.entries(def.style)
|
|
416
398
|
.filter(
|
|
417
399
|
([k, v]) =>
|
|
@@ -420,9 +402,10 @@ export function buildAttrs(def, scope) {
|
|
|
420
402
|
!k.startsWith("&") &&
|
|
421
403
|
!k.startsWith("[") &&
|
|
422
404
|
!k.startsWith("@") &&
|
|
423
|
-
!mediaOverriddenProps.has(k) &&
|
|
424
405
|
v !== null &&
|
|
425
|
-
typeof v !== "object"
|
|
406
|
+
typeof v !== "object" &&
|
|
407
|
+
typeof v === "string" &&
|
|
408
|
+
isTemplateString(v),
|
|
426
409
|
)
|
|
427
410
|
.map(([k, v]) => {
|
|
428
411
|
const value = resolveStaticValue(v, scope);
|
|
@@ -432,7 +415,6 @@ export function buildAttrs(def, scope) {
|
|
|
432
415
|
.join("; ");
|
|
433
416
|
if (inline) out += ` style="${inline}"`;
|
|
434
417
|
}
|
|
435
|
-
|
|
436
418
|
if (def.attributes) {
|
|
437
419
|
for (const [k, v] of Object.entries(def.attributes)) {
|
|
438
420
|
const value = resolveStaticValue(v, scope);
|
|
@@ -451,8 +433,10 @@ export function buildAttrs(def, scope) {
|
|
|
451
433
|
if (!def.attributes?.decoding) out += ` decoding="async"`;
|
|
452
434
|
}
|
|
453
435
|
|
|
454
|
-
if (def.$
|
|
455
|
-
out += ` data-jx-
|
|
436
|
+
if (def.$static) {
|
|
437
|
+
out += ` data-jx-static`;
|
|
438
|
+
} else if (def.$prerendered) {
|
|
439
|
+
out += ` data-jx-prerendered`;
|
|
456
440
|
}
|
|
457
441
|
|
|
458
442
|
return out;
|
|
@@ -506,7 +490,16 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
506
490
|
if (projectStyle && typeof projectStyle === "object") {
|
|
507
491
|
for (const [key, val] of Object.entries(projectStyle)) {
|
|
508
492
|
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
|
|
509
|
-
// Standalone selector (e.g. `.dark`)
|
|
493
|
+
// Standalone selector (e.g. `.dark`, `:focus-visible`)
|
|
494
|
+
rules.push(`${key} { ${toCSSText(/** @type {any} */ (val))} }`);
|
|
495
|
+
} else if (
|
|
496
|
+
val !== null &&
|
|
497
|
+
typeof val === "object" &&
|
|
498
|
+
!Array.isArray(val) &&
|
|
499
|
+
!key.startsWith("@") &&
|
|
500
|
+
!key.startsWith("--")
|
|
501
|
+
) {
|
|
502
|
+
// Element selector with object value (e.g. `html`, `*`)
|
|
510
503
|
rules.push(`${key} { ${toCSSText(/** @type {any} */ (val))} }`);
|
|
511
504
|
} else if (key.startsWith("@")) {
|
|
512
505
|
// @media block
|
|
@@ -555,23 +548,19 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
555
548
|
* @param {string} [_parentSel]
|
|
556
549
|
* @param {{ n: number }} [counter]
|
|
557
550
|
*/
|
|
558
|
-
export function collectStyles(
|
|
551
|
+
export function collectStyles(
|
|
552
|
+
def,
|
|
553
|
+
rules,
|
|
554
|
+
mediaQueries,
|
|
555
|
+
_parentSel = "",
|
|
556
|
+
counter = { n: 0 },
|
|
557
|
+
prefix = "jx",
|
|
558
|
+
) {
|
|
559
559
|
if (!def || typeof def !== "object") return;
|
|
560
560
|
|
|
561
561
|
if (def.style) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
(k) =>
|
|
565
|
-
k.startsWith("@") ||
|
|
566
|
-
k.startsWith(":") ||
|
|
567
|
-
k.startsWith(".") ||
|
|
568
|
-
k.startsWith("&") ||
|
|
569
|
-
k.startsWith("["),
|
|
570
|
-
);
|
|
571
|
-
|
|
572
|
-
// Auto-scope elements that need CSS rules but lack a unique selector
|
|
573
|
-
if (needsCSS && !def.id && !def.className) {
|
|
574
|
-
def.className = `jx-${counter.n++}`;
|
|
562
|
+
if (!def.id && !def.className) {
|
|
563
|
+
def.className = `${prefix}-${counter.n++}`;
|
|
575
564
|
}
|
|
576
565
|
}
|
|
577
566
|
|
|
@@ -582,36 +571,22 @@ export function collectStyles(def, rules, mediaQueries, _parentSel = "", counter
|
|
|
582
571
|
: (def.tagName ?? "*");
|
|
583
572
|
|
|
584
573
|
if (def.style) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
574
|
+
const baseDecls = [];
|
|
575
|
+
for (const [prop, value] of Object.entries(def.style)) {
|
|
576
|
+
if (
|
|
577
|
+
prop.startsWith(":") ||
|
|
578
|
+
prop.startsWith(".") ||
|
|
579
|
+
prop.startsWith("&") ||
|
|
580
|
+
prop.startsWith("[") ||
|
|
581
|
+
prop.startsWith("@")
|
|
582
|
+
)
|
|
583
|
+
continue;
|
|
584
|
+
if (value === null || typeof value === "object") continue;
|
|
585
|
+
if (typeof value === "string" && isTemplateString(value)) continue;
|
|
586
|
+
baseDecls.push(` ${camelToKebab(prop)}: ${value};`);
|
|
601
587
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (mediaOverriddenProps.size > 0) {
|
|
605
|
-
const baseDecls = [];
|
|
606
|
-
for (const p of mediaOverriddenProps) {
|
|
607
|
-
const v = def.style[p];
|
|
608
|
-
if (v !== null && v !== undefined && typeof v !== "object") {
|
|
609
|
-
baseDecls.push(`${camelToKebab(p)}: ${v}`);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
if (baseDecls.length > 0) {
|
|
613
|
-
rules.push(`${selector} { ${baseDecls.join("; ")} }`);
|
|
614
|
-
}
|
|
588
|
+
if (baseDecls.length > 0) {
|
|
589
|
+
rules.push(`${selector} {\n${baseDecls.join("\n")}\n}`);
|
|
615
590
|
}
|
|
616
591
|
|
|
617
592
|
for (const [prop, val] of Object.entries(def.style)) {
|
|
@@ -649,7 +624,7 @@ export function collectStyles(def, rules, mediaQueries, _parentSel = "", counter
|
|
|
649
624
|
|
|
650
625
|
if (Array.isArray(def.children)) {
|
|
651
626
|
def.children.forEach((/** @type {any} */ c) => {
|
|
652
|
-
collectStyles(c, rules, mediaQueries, selector, counter);
|
|
627
|
+
collectStyles(c, rules, mediaQueries, selector, counter, prefix);
|
|
653
628
|
});
|
|
654
629
|
}
|
|
655
630
|
}
|
|
@@ -785,7 +760,13 @@ const SELF_CLOSING = new Set(["input", "br", "hr", "img", "meta", "link", "area"
|
|
|
785
760
|
* @returns {string}
|
|
786
761
|
*/
|
|
787
762
|
export function renderStaticNode(node, scope, slotContent = null) {
|
|
788
|
-
if (typeof node === "string")
|
|
763
|
+
if (typeof node === "string") {
|
|
764
|
+
if (isTemplateString(node) && scope) {
|
|
765
|
+
const val = evaluateStaticTemplate(node, scope);
|
|
766
|
+
return val != null ? escapeHtml(String(val)) : escapeHtml(node);
|
|
767
|
+
}
|
|
768
|
+
return escapeHtml(node);
|
|
769
|
+
}
|
|
789
770
|
if (typeof node === "number" || typeof node === "boolean") return escapeHtml(String(node));
|
|
790
771
|
if (Array.isArray(node))
|
|
791
772
|
return node.map((/** @type {any} */ c) => renderStaticNode(c, scope, slotContent)).join("\n");
|
|
@@ -911,30 +892,63 @@ function _isStaticNode(node) {
|
|
|
911
892
|
}
|
|
912
893
|
|
|
913
894
|
/**
|
|
914
|
-
* Generate
|
|
915
|
-
*
|
|
895
|
+
* Generate CSS rules for a component: host-level styles using tag selector, plus inner element
|
|
896
|
+
* styles using .jx-N selectors via collectStyles.
|
|
916
897
|
*
|
|
917
898
|
* @param {string} tagName - The custom element tag name (used as CSS selector)
|
|
918
899
|
* @param {any} styleDef - The component's style object
|
|
900
|
+
* @param {any} [doc] - The full component document (for walking children)
|
|
901
|
+
* @param {Record<string, any>} [mediaQueries] - Project media query definitions
|
|
919
902
|
* @returns {string} CSS text, or empty string if no styles
|
|
920
903
|
*/
|
|
921
|
-
export function buildComponentCSS(tagName, styleDef) {
|
|
922
|
-
if (!styleDef || typeof styleDef !== "object") return "";
|
|
904
|
+
export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries = {}) {
|
|
923
905
|
/** @type {string[]} */
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
906
|
+
const rules = [];
|
|
907
|
+
|
|
908
|
+
if (styleDef && typeof styleDef === "object") {
|
|
909
|
+
/** @type {string[]} */
|
|
910
|
+
const decls = [];
|
|
911
|
+
for (const [prop, value] of Object.entries(styleDef)) {
|
|
912
|
+
if (
|
|
913
|
+
prop.startsWith(":") ||
|
|
914
|
+
prop.startsWith(".") ||
|
|
915
|
+
prop.startsWith("&") ||
|
|
916
|
+
prop.startsWith("[") ||
|
|
917
|
+
prop.startsWith("@")
|
|
918
|
+
)
|
|
919
|
+
continue;
|
|
920
|
+
if (value === null || typeof value === "object") continue;
|
|
921
|
+
if (typeof value === "string" && isTemplateString(value)) continue;
|
|
922
|
+
decls.push(` ${camelToKebab(prop)}: ${value};`);
|
|
923
|
+
}
|
|
924
|
+
if (decls.length > 0) {
|
|
925
|
+
rules.push(`${tagName} {\n${decls.join("\n")}\n}`);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
for (const [prop, val] of Object.entries(styleDef)) {
|
|
929
|
+
if (prop.startsWith("@")) {
|
|
930
|
+
const query = prop.startsWith("@--")
|
|
931
|
+
? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
|
|
932
|
+
: prop.slice(1);
|
|
933
|
+
rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {any} */ (val))} } }`);
|
|
934
|
+
} else if (
|
|
935
|
+
prop.startsWith(":") ||
|
|
936
|
+
prop.startsWith(".") ||
|
|
937
|
+
prop.startsWith("&") ||
|
|
938
|
+
prop.startsWith("[")
|
|
939
|
+
) {
|
|
940
|
+
const resolved = prop.startsWith("&") ? prop.replace("&", tagName) : `${tagName}${prop}`;
|
|
941
|
+
rules.push(`${resolved} { ${toCSSText(/** @type {any} */ (val))} }`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
937
944
|
}
|
|
938
|
-
|
|
939
|
-
|
|
945
|
+
|
|
946
|
+
if (doc && Array.isArray(doc.children)) {
|
|
947
|
+
const counter = { n: 0 };
|
|
948
|
+
for (const child of doc.children) {
|
|
949
|
+
collectStyles(child, rules, mediaQueries, "", counter, tagName);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
return rules.length > 0 ? rules.join("\n") + "\n" : "";
|
|
940
954
|
}
|
|
@@ -126,12 +126,12 @@ async function getMarkdownModule() {
|
|
|
126
126
|
async function loadMarkdownEntry(filePath, directiveOptions) {
|
|
127
127
|
const { MarkdownFile } = await getMarkdownModule();
|
|
128
128
|
const file = new MarkdownFile({ src: filePath, directiveOptions });
|
|
129
|
-
const result =
|
|
129
|
+
const result = file.resolve();
|
|
130
130
|
return {
|
|
131
131
|
id: result.slug,
|
|
132
132
|
data: result.frontmatter,
|
|
133
133
|
body: readFileSync(filePath, "utf-8"),
|
|
134
|
-
|
|
134
|
+
$children: result.$children,
|
|
135
135
|
_meta: {
|
|
136
136
|
excerpt: result.$excerpt,
|
|
137
137
|
toc: result.$toc,
|
package/src/site/image-cache.js
CHANGED
|
@@ -74,18 +74,11 @@ export function saveCache(projectRoot, cache) {
|
|
|
74
74
|
*
|
|
75
75
|
* @param {CacheManifest} cache
|
|
76
76
|
* @param {string} key
|
|
77
|
-
* @param {string} outDir - Absolute path to build output dir
|
|
78
77
|
* @returns {ImageManifest | null}
|
|
79
78
|
*/
|
|
80
|
-
export function getCached(cache, key
|
|
79
|
+
export function getCached(cache, key) {
|
|
81
80
|
const entry = cache.entries[key];
|
|
82
81
|
if (!entry) return null;
|
|
83
|
-
|
|
84
|
-
const allExist = entry.manifest.variants.every((v) =>
|
|
85
|
-
existsSync(resolve(outDir, v.outputPath.slice(1))),
|
|
86
|
-
);
|
|
87
|
-
if (!allExist) return null;
|
|
88
|
-
|
|
89
82
|
return entry.manifest;
|
|
90
83
|
}
|
|
91
84
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
-
import { resolve, extname } from "node:path";
|
|
10
|
+
import { resolve, extname, basename } from "node:path";
|
|
11
11
|
import { processImage, buildSrcset, contentHash, configHash } from "./image-optimizer.js";
|
|
12
12
|
import { getCached, setCached } from "./image-cache.js";
|
|
13
13
|
|
|
@@ -22,6 +22,29 @@ import { getCached, setCached } from "./image-cache.js";
|
|
|
22
22
|
const SKIP_EXTENSIONS = new Set([".svg", ".gif"]);
|
|
23
23
|
const EXTERNAL_PREFIXES = ["http://", "https://", "data:", "//"];
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} absoluteSrc
|
|
27
|
+
* @param {string} src
|
|
28
|
+
* @param {ImageConfig} config
|
|
29
|
+
* @param {string} outDir
|
|
30
|
+
* @param {CacheManifest} cache
|
|
31
|
+
* @returns {Promise<ImageManifest>}
|
|
32
|
+
*/
|
|
33
|
+
async function resolveManifest(absoluteSrc, src, config, outDir, cache) {
|
|
34
|
+
const key = `${contentHash(absoluteSrc)}:${configHash(config)}`;
|
|
35
|
+
const cached = getCached(cache, key);
|
|
36
|
+
|
|
37
|
+
if (cached) {
|
|
38
|
+
const allExist = cached.variants.every((v) => existsSync(v.absolutePath));
|
|
39
|
+
if (allExist) return cached;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!cached) console.log(` Optimizing ${basename(absoluteSrc)}...`);
|
|
43
|
+
const manifest = await processImage(absoluteSrc, outDir, config);
|
|
44
|
+
setCached(cache, key, src, manifest);
|
|
45
|
+
return manifest;
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
/**
|
|
26
49
|
* Check if a src value should be skipped for optimization.
|
|
27
50
|
*
|
|
@@ -91,6 +114,17 @@ async function walkAndTransform(node, config, projectRoot, outDir, cache, imageR
|
|
|
91
114
|
await transformImgNode(node, config, projectRoot, outDir, cache, imageRefs);
|
|
92
115
|
}
|
|
93
116
|
|
|
117
|
+
if (typeof node.innerHTML === "string" && node.innerHTML.includes("<img")) {
|
|
118
|
+
node.innerHTML = await transformInnerHtmlImages(
|
|
119
|
+
node.innerHTML,
|
|
120
|
+
config,
|
|
121
|
+
projectRoot,
|
|
122
|
+
outDir,
|
|
123
|
+
cache,
|
|
124
|
+
imageRefs,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
if (Array.isArray(node.children)) {
|
|
95
129
|
for (const child of node.children) {
|
|
96
130
|
await walkAndTransform(child, config, projectRoot, outDir, cache, imageRefs);
|
|
@@ -119,11 +153,7 @@ async function transformImgNode(node, config, projectRoot, outDir, cache, imageR
|
|
|
119
153
|
let manifest = imageRefs.get(absoluteSrc);
|
|
120
154
|
|
|
121
155
|
if (!manifest) {
|
|
122
|
-
|
|
123
|
-
const cached = getCached(cache, key, outDir);
|
|
124
|
-
manifest = cached ?? (await processImage(absoluteSrc, outDir, config));
|
|
125
|
-
|
|
126
|
-
setCached(cache, key, src, manifest);
|
|
156
|
+
manifest = await resolveManifest(absoluteSrc, src, config, outDir, cache);
|
|
127
157
|
imageRefs.set(absoluteSrc, manifest);
|
|
128
158
|
}
|
|
129
159
|
|
|
@@ -147,3 +177,71 @@ async function transformImgNode(node, config, projectRoot, outDir, cache, imageR
|
|
|
147
177
|
node.attributes.decoding = "async";
|
|
148
178
|
}
|
|
149
179
|
}
|
|
180
|
+
|
|
181
|
+
const IMG_TAG_RE = /<img\b([^>]*)>/gi;
|
|
182
|
+
const SRC_ATTR_RE = /\bsrc="([^"]*)"/;
|
|
183
|
+
const SRCSET_ATTR_RE = /\bsrcset="/;
|
|
184
|
+
const DATA_NO_OPT_RE = /\bdata-no-optimize\b/;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Transform `<img>` tags embedded in pre-rendered innerHTML strings.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} html
|
|
190
|
+
* @param {ImageConfig} config
|
|
191
|
+
* @param {string} projectRoot
|
|
192
|
+
* @param {string} outDir
|
|
193
|
+
* @param {CacheManifest} cache
|
|
194
|
+
* @param {Map<string, ImageManifest>} imageRefs
|
|
195
|
+
* @returns {Promise<string>}
|
|
196
|
+
*/
|
|
197
|
+
async function transformInnerHtmlImages(html, config, projectRoot, outDir, cache, imageRefs) {
|
|
198
|
+
/** @type {{ match: string; replacement: string }[]} */
|
|
199
|
+
const replacements = [];
|
|
200
|
+
|
|
201
|
+
for (const m of html.matchAll(IMG_TAG_RE)) {
|
|
202
|
+
const tag = m[0];
|
|
203
|
+
const attrs = m[1];
|
|
204
|
+
|
|
205
|
+
if (SRCSET_ATTR_RE.test(attrs)) continue;
|
|
206
|
+
if (DATA_NO_OPT_RE.test(attrs)) continue;
|
|
207
|
+
|
|
208
|
+
const srcMatch = attrs.match(SRC_ATTR_RE);
|
|
209
|
+
if (!srcMatch) continue;
|
|
210
|
+
|
|
211
|
+
const src = srcMatch[1];
|
|
212
|
+
if (shouldSkip(src)) continue;
|
|
213
|
+
|
|
214
|
+
const absoluteSrc = resolveImagePath(src, projectRoot);
|
|
215
|
+
if (!existsSync(absoluteSrc)) continue;
|
|
216
|
+
|
|
217
|
+
let manifest = imageRefs.get(absoluteSrc);
|
|
218
|
+
if (!manifest) {
|
|
219
|
+
manifest = await resolveManifest(absoluteSrc, src, config, outDir, cache);
|
|
220
|
+
imageRefs.set(absoluteSrc, manifest);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const preferredFormat = config.formats.includes("avif") ? "avif" : config.formats[0];
|
|
224
|
+
const srcset = buildSrcset(manifest.variants, preferredFormat);
|
|
225
|
+
if (!srcset) continue;
|
|
226
|
+
|
|
227
|
+
let extra = ` srcset="${srcset}" sizes="${config.sizes}"`;
|
|
228
|
+
if (!/\bwidth=/.test(attrs) && manifest.original.width) {
|
|
229
|
+
extra += ` width="${manifest.original.width}"`;
|
|
230
|
+
}
|
|
231
|
+
if (!/\bheight=/.test(attrs) && manifest.original.height) {
|
|
232
|
+
extra += ` height="${manifest.original.height}"`;
|
|
233
|
+
}
|
|
234
|
+
if (config.lazyLoad && !/\bloading="eager"/.test(attrs)) {
|
|
235
|
+
if (!/\bloading=/.test(attrs)) extra += ` loading="lazy"`;
|
|
236
|
+
if (!/\bdecoding=/.test(attrs)) extra += ` decoding="async"`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
replacements.push({ match: tag, replacement: `<img${attrs}${extra}>` });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let result = html;
|
|
243
|
+
for (const { match, replacement } of replacements) {
|
|
244
|
+
result = result.replace(match, replacement);
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
@@ -54,7 +54,8 @@ export function resolveLayout(pageDoc, projectConfig, projectRoot) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// Distribute page children into layout slots
|
|
57
|
-
const
|
|
57
|
+
const rawChildren = pageDoc.children ?? [];
|
|
58
|
+
const pageChildren = typeof rawChildren === "string" ? [rawChildren] : rawChildren;
|
|
58
59
|
const merged = deepClone(layoutDoc);
|
|
59
60
|
|
|
60
61
|
distributeSlots(merged, pageChildren);
|
|
@@ -32,6 +32,16 @@ const SKIP_PROTOTYPES = new Set([
|
|
|
32
32
|
"ContentEntry",
|
|
33
33
|
]);
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Built-in $prototype → .class.json mappings. These resolve automatically without requiring an
|
|
37
|
+
* explicit `imports` entry in project.json. Project-level imports take precedence on collision.
|
|
38
|
+
*/
|
|
39
|
+
/** @type {Record<string, string>} */
|
|
40
|
+
const BUILTIN_CLASS_MAPPINGS = {
|
|
41
|
+
MarkdownFile: "@jxsuite/parser/MarkdownFile.class.json",
|
|
42
|
+
MarkdownCollection: "@jxsuite/parser/MarkdownCollection.class.json",
|
|
43
|
+
};
|
|
44
|
+
|
|
35
45
|
/**
|
|
36
46
|
* Keys reserved by the Jx prototype system — stripped before passing config to the external class
|
|
37
47
|
* constructor. Mirrors runtime's EXTERNAL_RESERVED.
|
|
@@ -73,8 +83,8 @@ export async function resolvePrototypes(doc, route, projectRoot) {
|
|
|
73
83
|
|
|
74
84
|
// Look up in imports if no $src already set
|
|
75
85
|
if (!def.$src) {
|
|
76
|
-
const mapped = imports[def.$prototype];
|
|
77
|
-
if (!mapped) continue;
|
|
86
|
+
const mapped = imports[def.$prototype] ?? BUILTIN_CLASS_MAPPINGS[def.$prototype];
|
|
87
|
+
if (!mapped) continue;
|
|
78
88
|
def.$src = mapped;
|
|
79
89
|
}
|
|
80
90
|
|