meno-core 1.0.45 → 1.0.47
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/build-astro.ts +211 -124
- package/dist/bin/cli.js +2 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-NZTSJS5C.js → chunk-2QK6U5UK.js} +3 -2
- package/dist/chunks/{chunk-NZTSJS5C.js.map → chunk-2QK6U5UK.js.map} +2 -2
- package/dist/chunks/{chunk-TVH3TC2T.js → chunk-47UNLQUU.js} +6 -6
- package/dist/chunks/{chunk-F7MA62WG.js → chunk-BCLGRZ3U.js} +5 -5
- package/dist/chunks/{chunk-F7MA62WG.js.map → chunk-BCLGRZ3U.js.map} +2 -2
- package/dist/chunks/{chunk-5ZASE4IG.js → chunk-FED5MME6.js} +234 -11
- package/dist/chunks/{chunk-5ZASE4IG.js.map → chunk-FED5MME6.js.map} +3 -3
- package/dist/chunks/{chunk-BZQKEJQY.js → chunk-FGUZOYJX.js} +49 -30
- package/dist/chunks/chunk-FGUZOYJX.js.map +7 -0
- package/dist/chunks/{chunk-5Z5VQRTJ.js → chunk-I7YIGZXT.js} +4 -4
- package/dist/chunks/{chunk-5Z5VQRTJ.js.map → chunk-I7YIGZXT.js.map} +2 -2
- package/dist/chunks/{chunk-OUNJ76QM.js → chunk-LJFB5EBT.js} +5 -5
- package/dist/chunks/{chunk-GYF3ABI3.js → chunk-UUA5LEWF.js} +3 -3
- package/dist/chunks/{chunk-GYF3ABI3.js.map → chunk-UUA5LEWF.js.map} +2 -2
- package/dist/chunks/{chunk-WQSG5WHC.js → chunk-ZTKHJQ2Z.js} +2 -2
- package/dist/chunks/{configService-6KTT6GRT.js → configService-DYCUEURL.js} +3 -3
- package/dist/chunks/{constants-L5IKLB6U.js → constants-GWBAD66U.js} +2 -2
- package/dist/entries/server-router.js +7 -7
- package/dist/lib/client/index.js +7 -5
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +631 -208
- package/dist/lib/server/index.js.map +3 -3
- package/dist/lib/shared/index.js +7 -3
- package/dist/lib/shared/index.js.map +2 -2
- package/dist/lib/test-utils/index.js +1 -1
- package/lib/client/core/ComponentBuilder.test.ts +21 -0
- package/lib/client/core/ComponentBuilder.ts +8 -1
- package/lib/client/templateEngine.test.ts +64 -0
- package/lib/server/astro/astroEmitHelpers.ts +23 -0
- package/lib/server/astro/cmsPageEmitter.ts +46 -3
- package/lib/server/astro/componentEmitter.test.ts +59 -0
- package/lib/server/astro/componentEmitter.ts +53 -12
- package/lib/server/astro/cssCollector.ts +58 -11
- package/lib/server/astro/nodeToAstro.test.ts +397 -5
- package/lib/server/astro/nodeToAstro.ts +494 -65
- package/lib/server/astro/pageEmitter.ts +46 -3
- package/lib/server/astro/tailwindMapper.test.ts +119 -0
- package/lib/server/astro/tailwindMapper.ts +67 -1
- package/lib/server/runtime/httpServer.ts +12 -4
- package/lib/server/ssr/htmlGenerator.test.ts +3 -2
- package/lib/server/ssr/htmlGenerator.ts +6 -1
- package/lib/server/ssr/imageMetadata.ts +15 -9
- package/lib/server/ssr/jsCollector.ts +2 -2
- package/lib/server/ssr/ssrRenderer.test.ts +79 -0
- package/lib/server/ssr/ssrRenderer.ts +35 -20
- package/lib/shared/constants.ts +1 -0
- package/lib/shared/cssGeneration.test.ts +109 -3
- package/lib/shared/cssGeneration.ts +98 -13
- package/lib/shared/cssNamedColors.ts +47 -0
- package/lib/shared/cssProperties.ts +2 -2
- package/lib/shared/index.ts +1 -0
- package/lib/shared/styleNodeUtils.test.ts +47 -1
- package/lib/shared/styleNodeUtils.ts +7 -7
- package/package.json +1 -1
- package/dist/chunks/chunk-BZQKEJQY.js.map +0 -7
- /package/dist/chunks/{chunk-TVH3TC2T.js.map → chunk-47UNLQUU.js.map} +0 -0
- /package/dist/chunks/{chunk-OUNJ76QM.js.map → chunk-LJFB5EBT.js.map} +0 -0
- /package/dist/chunks/{chunk-WQSG5WHC.js.map → chunk-ZTKHJQ2Z.js.map} +0 -0
- /package/dist/chunks/{configService-6KTT6GRT.js.map → configService-DYCUEURL.js.map} +0 -0
- /package/dist/chunks/{constants-L5IKLB6U.js.map → constants-GWBAD66U.js.map} +0 -0
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
} from '../../shared/types/styles';
|
|
30
30
|
import { responsiveStylesToTailwind, propertyToTailwind } from './tailwindMapper';
|
|
31
31
|
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
32
|
+
import type { ResponsiveScales } from '../../shared/responsiveScaling';
|
|
32
33
|
import { generateElementClassName, type ElementClassContext } from '../../shared/elementClassName';
|
|
33
34
|
import { isVoidElement, hasIf, hasChildren, isSlotContent as isSlotContentNode } from '../../shared/nodeUtils';
|
|
34
35
|
import { NODE_TYPE, RAW_HTML_PREFIX } from '../../shared/constants';
|
|
@@ -38,6 +39,7 @@ import { isI18nValue, resolveI18nValue, DEFAULT_I18N_CONFIG, buildLocalizedPath
|
|
|
38
39
|
import { transformCMSTemplate, isTemplateExpression, transformItemTemplate, replaceItemMetaVars, rewriteItemVar } from './templateTransformer';
|
|
39
40
|
import type { SlugMap } from '../../shared/slugTranslator';
|
|
40
41
|
import { buildSlugIndex, translatePath } from '../../shared/slugTranslator';
|
|
42
|
+
import { stripRawHtmlPrefixDeep, astroComponentName } from './astroEmitHelpers';
|
|
41
43
|
|
|
42
44
|
// ---------------------------------------------------------------------------
|
|
43
45
|
// Types
|
|
@@ -64,6 +66,8 @@ export interface AstroEmitContext {
|
|
|
64
66
|
fileName: string;
|
|
65
67
|
/** Breakpoint config for responsive Tailwind classes */
|
|
66
68
|
breakpoints: BreakpointConfig;
|
|
69
|
+
/** Responsive scales config for auto-scaling spacing/typography/sizing */
|
|
70
|
+
responsiveScales?: ResponsiveScales;
|
|
67
71
|
/** Dynamic tag definitions collected during traversal (for frontmatter) */
|
|
68
72
|
dynamicTags?: Map<string, string>;
|
|
69
73
|
/** Image metadata map for responsive image generation */
|
|
@@ -102,8 +106,29 @@ export interface AstroEmitContext {
|
|
|
102
106
|
slugMappings?: SlugMap[];
|
|
103
107
|
/** I18n default locale (for slug translation) */
|
|
104
108
|
i18nDefaultLocale?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Raw-HTML slice → processed HTML captured during the SSR pass for this page.
|
|
111
|
+
* Used by the RAW_HTML_PREFIX branch to emit rich-text content that has had
|
|
112
|
+
* image rewriting, component expansion, and link localization applied.
|
|
113
|
+
*/
|
|
114
|
+
processedRawHtml?: Map<string, string>;
|
|
105
115
|
/** Image format: 'webp' uses plain <img>, 'avif' uses <picture> with AVIF+WebP sources */
|
|
106
116
|
imageFormat?: 'webp' | 'avif';
|
|
117
|
+
/**
|
|
118
|
+
* Static image imports collected during emission: varName → import path
|
|
119
|
+
* relative to the emitting file. Drives `import foo from '...'` in
|
|
120
|
+
* frontmatter and unlocks astro:assets <Picture> for static local images.
|
|
121
|
+
*
|
|
122
|
+
* Note: this is a Map (ref-shared across child ctx spreads), so mutations
|
|
123
|
+
* inside list/dynamic inner contexts remain visible to the parent. A
|
|
124
|
+
* separate boolean flag would be dropped by `{ ...ctx }` shallow copies —
|
|
125
|
+
* the presence of any entry here implies `import { Picture } from 'astro:assets'`.
|
|
126
|
+
*/
|
|
127
|
+
imageImports?: Map<string, string>;
|
|
128
|
+
/** File depth (relative to src/pages or src/components) for import path computation */
|
|
129
|
+
fileDepth?: number;
|
|
130
|
+
/** Collected interactive styles during emission: elementClass → InteractiveStyles */
|
|
131
|
+
collectedInteractiveStyles?: Map<string, InteractiveStyles>;
|
|
107
132
|
}
|
|
108
133
|
|
|
109
134
|
// ---------------------------------------------------------------------------
|
|
@@ -206,7 +231,12 @@ function escapeJSX(s: string): string {
|
|
|
206
231
|
* Escape a string for use inside a JS template literal
|
|
207
232
|
*/
|
|
208
233
|
function escapeTemplateLiteral(s: string): string {
|
|
209
|
-
return s
|
|
234
|
+
return s
|
|
235
|
+
.replace(/\\/g, '\\\\')
|
|
236
|
+
.replace(/`/g, '\\`')
|
|
237
|
+
.replace(/\$\{/g, '\\${')
|
|
238
|
+
.replace(/\u2028/g, '\\u2028')
|
|
239
|
+
.replace(/\u2029/g, '\\u2029');
|
|
210
240
|
}
|
|
211
241
|
|
|
212
242
|
/**
|
|
@@ -311,7 +341,7 @@ function buildClassAndStyleExpression(
|
|
|
311
341
|
): { classExpr: string; styleAttr: string } {
|
|
312
342
|
// Static Tailwind classes from non-mapping styles
|
|
313
343
|
const result = style
|
|
314
|
-
? responsiveStylesToTailwind(style, ctx.breakpoints)
|
|
344
|
+
? responsiveStylesToTailwind(style, ctx.breakpoints, ctx.responsiveScales)
|
|
315
345
|
: { classes: [], dynamicStyles: {} };
|
|
316
346
|
const staticClasses = result.classes;
|
|
317
347
|
const dynamicStyles = result.dynamicStyles;
|
|
@@ -439,7 +469,7 @@ function resolveTemplate(text: string, ctx: AstroEmitContext): string {
|
|
|
439
469
|
if (ctx.listItemBinding) propName = rewriteItemVar(propName, ctx.listItemBinding);
|
|
440
470
|
if (ctx.listIndexVar) propName = replaceItemMetaVars(propName, ctx.listIndexVar, ctx.listSourceVar);
|
|
441
471
|
// Rich-text props contain HTML - render unescaped via set:html
|
|
442
|
-
if (ctx.componentProps[propName]?.type === 'rich-text') {
|
|
472
|
+
if (ctx.componentProps[propName]?.type === 'rich-text' || ctx.componentProps[propName]?.type === 'embed') {
|
|
443
473
|
return `<Fragment set:html={${propName}} />`;
|
|
444
474
|
}
|
|
445
475
|
return `{${propName}}`;
|
|
@@ -475,6 +505,7 @@ function buildElementClass(
|
|
|
475
505
|
});
|
|
476
506
|
}
|
|
477
507
|
|
|
508
|
+
|
|
478
509
|
/**
|
|
479
510
|
* Build HTML attributes string from node attributes
|
|
480
511
|
*/
|
|
@@ -555,6 +586,7 @@ function buildAttributesString(
|
|
|
555
586
|
* Format a prop value for Astro template usage
|
|
556
587
|
*/
|
|
557
588
|
function formatPropValue(value: unknown): string {
|
|
589
|
+
value = stripRawHtmlPrefixDeep(value);
|
|
558
590
|
if (typeof value === 'string') return `"${escapeJSX(value)}"`;
|
|
559
591
|
if (typeof value === 'number') return `{${value}}`;
|
|
560
592
|
if (typeof value === 'boolean') return `{${value}}`;
|
|
@@ -591,12 +623,13 @@ export function nodeToAstro(
|
|
|
591
623
|
if (typeof node === 'object' && !Array.isArray(node) && isI18nValue(node)) {
|
|
592
624
|
const resolved = resolveI18n(node, ctx);
|
|
593
625
|
if (typeof resolved === 'string') {
|
|
594
|
-
|
|
626
|
+
// Delegate to the text-node path so RAW_HTML_PREFIX gets emitted via <Fragment set:html>.
|
|
627
|
+
return nodeToAstro(resolved, ctx);
|
|
595
628
|
}
|
|
596
629
|
// In component def without locale: wrap with r() resolver
|
|
597
630
|
if (ctx.isComponentDef && isI18nValue(resolved)) {
|
|
598
631
|
ctx.needsI18nResolver = true;
|
|
599
|
-
return `${ind(ctx)}{r(${JSON.stringify(resolved)})}\n`;
|
|
632
|
+
return `${ind(ctx)}{r(${JSON.stringify(stripRawHtmlPrefixDeep(resolved))})}\n`;
|
|
600
633
|
}
|
|
601
634
|
// If resolution returned a non-string (e.g. array), stringify it
|
|
602
635
|
return `${ind(ctx)}${String(resolved ?? '')}\n`;
|
|
@@ -617,9 +650,12 @@ export function nodeToAstro(
|
|
|
617
650
|
if (hasTemplates(node) && ctx.isComponentDef) {
|
|
618
651
|
return `${ind(ctx)}${resolveTemplate(node, ctx)}\n`;
|
|
619
652
|
}
|
|
620
|
-
// Raw HTML marker (from rich-text fields) - render unescaped via set:html
|
|
653
|
+
// Raw HTML marker (from rich-text fields) - render unescaped via set:html.
|
|
654
|
+
// Prefer the SSR-processed HTML when available so image rewriting, component
|
|
655
|
+
// expansion, and link localization match what meno-core renders at runtime.
|
|
621
656
|
if (node.startsWith(RAW_HTML_PREFIX)) {
|
|
622
|
-
const
|
|
657
|
+
const rawSlice = node.slice(RAW_HTML_PREFIX.length);
|
|
658
|
+
const rawHtml = ctx.processedRawHtml?.get(rawSlice) ?? rawSlice;
|
|
623
659
|
return `${ind(ctx)}<Fragment set:html={\`${escapeTemplateLiteral(rawHtml)}\`} />\n`;
|
|
624
660
|
}
|
|
625
661
|
return `${ind(ctx)}${escapeJSX(node)}\n`;
|
|
@@ -681,6 +717,12 @@ const IMG_OPACITY_PATTERN = /^opacity-/;
|
|
|
681
717
|
*/
|
|
682
718
|
const DEFAULT_SIZES = '100vw';
|
|
683
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Tailwind classes applied to the inner <img> so it fills its wrapper (<picture>)
|
|
722
|
+
* when a responsive/blurred image wraps the element.
|
|
723
|
+
*/
|
|
724
|
+
const IMG_FILL_CLASSES = ['block', 'w-full', 'h-full'];
|
|
725
|
+
|
|
684
726
|
/**
|
|
685
727
|
* Split Tailwind classes into picture (layout) and img (visual) groups.
|
|
686
728
|
* Layout classes go on <picture>, image-specific classes go on <img>.
|
|
@@ -702,10 +744,283 @@ function splitImageClasses(allClasses: string[]): { pictureClasses: string[]; im
|
|
|
702
744
|
return { pictureClasses, imgClasses };
|
|
703
745
|
}
|
|
704
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Extract a pixel width from a style value. Returns null for %, vw, auto,
|
|
749
|
+
* or any other non-px value we can't translate into a sizes hint.
|
|
750
|
+
*/
|
|
751
|
+
function extractPxWidth(val: unknown): number | null {
|
|
752
|
+
if (typeof val === 'number' && Number.isFinite(val) && val > 0) return val;
|
|
753
|
+
if (typeof val !== 'string') return null;
|
|
754
|
+
const match = val.trim().match(/^(\d+(?:\.\d+)?)px$/);
|
|
755
|
+
if (!match) return null;
|
|
756
|
+
const n = parseFloat(match[1]);
|
|
757
|
+
return n > 0 ? n : null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Compute a `sizes` attribute from a node's responsive style + breakpoint config.
|
|
762
|
+
* Produces something like `(max-width: 540px) 400px, (max-width: 1024px) 500px, 700px`
|
|
763
|
+
* so the browser can pick the smallest srcset variant that covers the rendered width
|
|
764
|
+
* at each viewport. Falls back to `100vw` when no usable px width is available.
|
|
765
|
+
*/
|
|
766
|
+
function computeSizesAttribute(
|
|
767
|
+
style: StyleObject | ResponsiveStyleObject | undefined,
|
|
768
|
+
breakpoints: BreakpointConfig
|
|
769
|
+
): string {
|
|
770
|
+
if (!style) return DEFAULT_SIZES;
|
|
771
|
+
|
|
772
|
+
const responsive = isResponsiveStyle(style as any);
|
|
773
|
+
const baseStyle: StyleObject | undefined = responsive
|
|
774
|
+
? (style as ResponsiveStyleObject).base
|
|
775
|
+
: (style as StyleObject);
|
|
776
|
+
|
|
777
|
+
// Base (desktop) width — the biggest viewport
|
|
778
|
+
const baseWidth = baseStyle ? extractPxWidth((baseStyle as StyleObject).width) : null;
|
|
779
|
+
// If even the base width is unknown, nothing we can do — use 100vw.
|
|
780
|
+
if (baseWidth == null) return DEFAULT_SIZES;
|
|
781
|
+
|
|
782
|
+
// Walk breakpoints from smallest to largest so the emitted media queries
|
|
783
|
+
// are ordered from narrowest to widest, with the unconditional base last.
|
|
784
|
+
const bpEntries = Object.entries(breakpoints).sort(
|
|
785
|
+
(a, b) => a[1].breakpoint - b[1].breakpoint
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
const parts: string[] = [];
|
|
789
|
+
// Track the "effective" width as we walk up: if a breakpoint doesn't set its
|
|
790
|
+
// own width, it inherits from the next step up, and ultimately from base.
|
|
791
|
+
// We walk ascending, so we need to resolve by looking from smallest bp up to base.
|
|
792
|
+
for (const [name, entry] of bpEntries) {
|
|
793
|
+
let effective: number | null = null;
|
|
794
|
+
if (responsive) {
|
|
795
|
+
// Look at this bp, then larger bps, then base
|
|
796
|
+
const bpStyle = (style as ResponsiveStyleObject)[name];
|
|
797
|
+
effective = bpStyle ? extractPxWidth((bpStyle as StyleObject).width) : null;
|
|
798
|
+
if (effective == null) {
|
|
799
|
+
// Inherit from next-larger breakpoint(s)
|
|
800
|
+
for (const [largerName, largerEntry] of bpEntries) {
|
|
801
|
+
if (largerEntry.breakpoint <= entry.breakpoint) continue;
|
|
802
|
+
const largerStyle = (style as ResponsiveStyleObject)[largerName];
|
|
803
|
+
const larger = largerStyle ? extractPxWidth((largerStyle as StyleObject).width) : null;
|
|
804
|
+
if (larger != null) {
|
|
805
|
+
effective = larger;
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (effective == null) effective = baseWidth;
|
|
811
|
+
} else {
|
|
812
|
+
effective = baseWidth;
|
|
813
|
+
}
|
|
814
|
+
parts.push(`(max-width: ${entry.breakpoint}px) ${effective}px`);
|
|
815
|
+
}
|
|
816
|
+
parts.push(`${baseWidth}px`);
|
|
817
|
+
return parts.join(', ');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Merge additional CSS declarations into an existing ` style="..."` attribute
|
|
822
|
+
* string. If no style attribute exists yet, builds one. The inputs are raw CSS
|
|
823
|
+
* already escaped for HTML attribute context.
|
|
824
|
+
*/
|
|
825
|
+
function injectInlineStyle(styleAttr: string, extraCss: string): string {
|
|
826
|
+
if (!extraCss) return styleAttr;
|
|
827
|
+
if (!styleAttr) return ` style="${extraCss}"`;
|
|
828
|
+
return styleAttr.replace(/style="([^"]*)"/, (_, existing: string) => {
|
|
829
|
+
const trimmed = existing.trimEnd();
|
|
830
|
+
const sep = trimmed.length > 0 && !trimmed.endsWith(';') ? ';' : '';
|
|
831
|
+
return `style="${existing}${sep}${extraCss}"`;
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
705
835
|
/**
|
|
706
836
|
* Emit an <img> node as a <picture> element with AVIF/WebP sources when
|
|
707
837
|
* image metadata is available, or as a plain <img> with srcset otherwise.
|
|
708
838
|
*/
|
|
839
|
+
// Widths used for <Picture> srcset generation. Mirrors
|
|
840
|
+
// RESPONSIVE_WIDTHS in imageMetadata.ts.
|
|
841
|
+
const PICTURE_WIDTHS = [500, 800, 1080, 1600, 2400];
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Turn an image path like `/images/sub/hero-photo.jpg` into a stable JS
|
|
845
|
+
* identifier (`imgSubHeroPhoto`) for use as an ESM import variable.
|
|
846
|
+
*/
|
|
847
|
+
function imagePathToVarName(srcPath: string): string {
|
|
848
|
+
const stripped = srcPath.replace(/^\/+/, '').replace(/^images\//, '').replace(/\.[^.]+$/, '');
|
|
849
|
+
const parts = stripped.split(/[/_\-\s.]+/).filter(Boolean);
|
|
850
|
+
if (parts.length === 0) return 'imgAsset';
|
|
851
|
+
const camel = parts
|
|
852
|
+
.map((p, i) => {
|
|
853
|
+
const clean = p.replace(/[^a-zA-Z0-9]/g, '');
|
|
854
|
+
if (!clean) return '';
|
|
855
|
+
if (i === 0) return clean.toLowerCase();
|
|
856
|
+
return clean[0].toUpperCase() + clean.slice(1).toLowerCase();
|
|
857
|
+
})
|
|
858
|
+
.join('');
|
|
859
|
+
if (!camel) return 'imgAsset';
|
|
860
|
+
return 'img' + camel[0].toUpperCase() + camel.slice(1);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Compute an ESM import path relative to the emitting file for a Meno image
|
|
865
|
+
* URL. For file depth N (e.g. `src/pages/en/about.astro` is depth 1), the
|
|
866
|
+
* path is `'../'.repeat(N + 1) + 'assets/images/<rest>'`.
|
|
867
|
+
*/
|
|
868
|
+
function imageImportPath(srcPath: string, fileDepth: number): string {
|
|
869
|
+
const rest = srcPath.replace(/^\/+/, '').replace(/^images\//, '');
|
|
870
|
+
const ups = '../'.repeat(Math.max(0, fileDepth) + 1);
|
|
871
|
+
return `${ups}assets/images/${rest}`;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Decide whether an image src can be switched to <Picture>. Only plain
|
|
876
|
+
* literal local paths (`/images/...`) with known metadata qualify — CMS
|
|
877
|
+
* templates, list-item bindings, component props, and remote URLs take the
|
|
878
|
+
* legacy <img>/<picture> path.
|
|
879
|
+
*/
|
|
880
|
+
function isStaticImageSrc(src: string | undefined, ctx: AstroEmitContext): src is string {
|
|
881
|
+
if (!src) return false;
|
|
882
|
+
if (typeof src !== 'string') return false;
|
|
883
|
+
if (hasTemplates(src)) return false;
|
|
884
|
+
if (!src.startsWith('/images/')) return false;
|
|
885
|
+
if (!ctx.imageMetadataMap?.has(src)) return false;
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Register (or reuse) an ESM image import on the emit context. Returns the
|
|
891
|
+
* JS variable name to reference in the generated template.
|
|
892
|
+
*/
|
|
893
|
+
function registerStaticImageImport(src: string, ctx: AstroEmitContext): string {
|
|
894
|
+
if (!ctx.imageImports) ctx.imageImports = new Map();
|
|
895
|
+
const depth = ctx.fileDepth ?? 0;
|
|
896
|
+
const importPath = imageImportPath(src, depth);
|
|
897
|
+
|
|
898
|
+
for (const [existingName, existingPath] of ctx.imageImports) {
|
|
899
|
+
if (existingPath === importPath) return existingName;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const base = imagePathToVarName(src);
|
|
903
|
+
let name = base;
|
|
904
|
+
let counter = 2;
|
|
905
|
+
while (ctx.imageImports.has(name)) {
|
|
906
|
+
name = `${base}${counter++}`;
|
|
907
|
+
}
|
|
908
|
+
ctx.imageImports.set(name, importPath);
|
|
909
|
+
return name;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Split a static `classExpr` into the outer layout classes (for <picture> /
|
|
914
|
+
* wrapper) and the inner image classes (for the emitted <img> inside
|
|
915
|
+
* <Picture>). Mirrors `splitImageClasses()` used by the legacy emitter so
|
|
916
|
+
* object-cover / opacity-* keep landing on the <img>. Returns null when the
|
|
917
|
+
* class expression is dynamic (class:list) — callers fall back to div wrap.
|
|
918
|
+
*/
|
|
919
|
+
function splitStaticClassExpr(
|
|
920
|
+
classExpr: string
|
|
921
|
+
): { outerClasses: string[]; innerClasses: string[] } | null {
|
|
922
|
+
if (classExpr.includes('class:list=')) return null;
|
|
923
|
+
const classMatch = classExpr.match(/class="([^"]*)"/);
|
|
924
|
+
if (!classMatch) return { outerClasses: [], innerClasses: [] };
|
|
925
|
+
const all = classMatch[1].split(/\s+/).filter(Boolean);
|
|
926
|
+
const { pictureClasses, imgClasses } = splitImageClasses(all);
|
|
927
|
+
return { outerClasses: pictureClasses, innerClasses: imgClasses };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Format a static class+style pair as an Astro `pictureAttributes={{...}}`
|
|
932
|
+
* prop. `classValue` comes pre-split (outer layout classes only).
|
|
933
|
+
*/
|
|
934
|
+
function formatPictureAttributesProp(
|
|
935
|
+
classValue: string,
|
|
936
|
+
styleAttr: string
|
|
937
|
+
): string {
|
|
938
|
+
const styleMatch = styleAttr.match(/style="([^"]*)"/);
|
|
939
|
+
const parts: string[] = [];
|
|
940
|
+
if (classValue) parts.push(`class: ${JSON.stringify(classValue)}`);
|
|
941
|
+
if (styleMatch) parts.push(`style: ${JSON.stringify(styleMatch[1])}`);
|
|
942
|
+
if (parts.length === 0) return '';
|
|
943
|
+
return ` pictureAttributes={{${parts.join(', ')}}}`;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Emit a static local image as an astro:assets <Picture>. Called only after
|
|
948
|
+
* isStaticImageSrc() has passed. Non-static paths should never reach here.
|
|
949
|
+
*/
|
|
950
|
+
function emitStaticPictureImage(
|
|
951
|
+
src: string,
|
|
952
|
+
alt: string | undefined,
|
|
953
|
+
loading: string | undefined,
|
|
954
|
+
fetchpriority: string | undefined,
|
|
955
|
+
sizesValue: string,
|
|
956
|
+
classExpr: string,
|
|
957
|
+
styleAttr: string,
|
|
958
|
+
ifExpr: string,
|
|
959
|
+
ifClose: string,
|
|
960
|
+
blurHash: string | undefined,
|
|
961
|
+
ctx: AstroEmitContext
|
|
962
|
+
): string {
|
|
963
|
+
const varName = registerStaticImageImport(src, ctx);
|
|
964
|
+
|
|
965
|
+
const widthsLiteral = `[${PICTURE_WIDTHS.join(', ')}]`;
|
|
966
|
+
const altAttr = alt !== undefined ? ` alt="${escapeJSX(String(alt))}"` : ' alt=""';
|
|
967
|
+
const loadingAttr = loading ? ` loading="${escapeJSX(loading)}"` : '';
|
|
968
|
+
const fetchpriorityAttr = fetchpriority ? ` fetchpriority="${escapeJSX(fetchpriority)}"` : '';
|
|
969
|
+
|
|
970
|
+
// Split classes so object-cover / opacity land on the <img>, not the
|
|
971
|
+
// outer <picture>/wrapper. splitStaticClassExpr returns null for dynamic
|
|
972
|
+
// class:list — callers fall back to a <div> wrapper without splitting.
|
|
973
|
+
const split = splitStaticClassExpr(classExpr);
|
|
974
|
+
|
|
975
|
+
if (blurHash) {
|
|
976
|
+
// Blur placeholder: put the blur background-image + layout classes on
|
|
977
|
+
// the <picture> element via pictureAttributes. This mirrors the legacy
|
|
978
|
+
// path where <picture> was both the blur host and layout container.
|
|
979
|
+
// The inner <img> gets fill + img-specific classes and clears the blur
|
|
980
|
+
// on load via this.parentElement (direct parent = <picture>).
|
|
981
|
+
const blurCss = `background-image:url(${escapeJSX(blurHash)});background-size:cover`;
|
|
982
|
+
const blurStyleAttr = injectInlineStyle(styleAttr, blurCss);
|
|
983
|
+
const onloadAttr = ` onload="this.parentElement.style.backgroundImage=''"`;
|
|
984
|
+
|
|
985
|
+
if (split) {
|
|
986
|
+
const outerClassValue = split.outerClasses.join(' ');
|
|
987
|
+
const pictureAttrs = formatPictureAttributesProp(outerClassValue, blurStyleAttr);
|
|
988
|
+
const innerClasses = [...split.innerClasses, ...IMG_FILL_CLASSES];
|
|
989
|
+
const innerClassAttr = ` class="${innerClasses.join(' ')}"`;
|
|
990
|
+
return `${ifExpr}${ind(ctx)}<Picture${pictureAttrs} src={${varName}}${altAttr}${innerClassAttr} formats={['avif','webp']} widths={${widthsLiteral}} sizes="${escapeJSX(sizesValue)}"${loadingAttr}${fetchpriorityAttr}${onloadAttr} />\n${ifClose}`;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Dynamic class:list: fall back to a <div> wrapper since we can't
|
|
994
|
+
// convert class:list to a pictureAttributes JS object.
|
|
995
|
+
const wrapperClassExpr = classExpr;
|
|
996
|
+
const wrapperStyleAttr = injectInlineStyle(styleAttr, blurCss);
|
|
997
|
+
const fillClassAttr = ` class="${IMG_FILL_CLASSES.join(' ')}"`;
|
|
998
|
+
return (
|
|
999
|
+
`${ifExpr}${ind(ctx)}<div${wrapperClassExpr}${wrapperStyleAttr}>\n` +
|
|
1000
|
+
`${ind(ctx)} <Picture pictureAttributes={{class: "${IMG_FILL_CLASSES.join(' ')}"}} src={${varName}}${altAttr}${fillClassAttr} formats={['avif','webp']} widths={${widthsLiteral}} sizes="${escapeJSX(sizesValue)}"${loadingAttr}${fetchpriorityAttr} onload="this.parentElement.parentElement.style.backgroundImage=''" />\n` +
|
|
1001
|
+
`${ind(ctx)}</div>\n${ifClose}`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Non-blur case: forward outer classes via pictureAttributes so they land
|
|
1006
|
+
// on the <picture>, inner img-specific classes as `class` on <Picture>.
|
|
1007
|
+
if (split) {
|
|
1008
|
+
const outerClassValue = split.outerClasses.join(' ');
|
|
1009
|
+
const innerClassAttr = split.innerClasses.length > 0
|
|
1010
|
+
? ` class="${split.innerClasses.join(' ')}"`
|
|
1011
|
+
: '';
|
|
1012
|
+
const pictureAttrs = formatPictureAttributesProp(outerClassValue, styleAttr);
|
|
1013
|
+
return `${ifExpr}${ind(ctx)}<Picture${pictureAttrs} src={${varName}}${altAttr}${innerClassAttr} formats={['avif','webp']} widths={${widthsLiteral}} sizes="${escapeJSX(sizesValue)}"${loadingAttr}${fetchpriorityAttr} />\n${ifClose}`;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Dynamic class:list: wrap in a <div> so the dynamic expression survives.
|
|
1017
|
+
return (
|
|
1018
|
+
`${ifExpr}${ind(ctx)}<div${classExpr}${styleAttr}>\n` +
|
|
1019
|
+
`${ind(ctx)} <Picture src={${varName}}${altAttr} formats={['avif','webp']} widths={${widthsLiteral}} sizes="${escapeJSX(sizesValue)}"${loadingAttr}${fetchpriorityAttr} />\n` +
|
|
1020
|
+
`${ind(ctx)}</div>\n${ifClose}`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
709
1024
|
function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
710
1025
|
const style = node.style as StyleObject | ResponsiveStyleObject | undefined;
|
|
711
1026
|
|
|
@@ -716,6 +1031,9 @@ function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
|
716
1031
|
node.generateElementClass
|
|
717
1032
|
) {
|
|
718
1033
|
elementClass = buildElementClass(ctx, node.label);
|
|
1034
|
+
if (elementClass && node.interactiveStyles?.length && ctx.collectedInteractiveStyles) {
|
|
1035
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1036
|
+
}
|
|
719
1037
|
}
|
|
720
1038
|
|
|
721
1039
|
const { classExpr, styleAttr } = buildClassAndStyleExpression(
|
|
@@ -744,7 +1062,26 @@ function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
|
744
1062
|
if (height === undefined && metadata.height) height = metadata.height;
|
|
745
1063
|
}
|
|
746
1064
|
|
|
747
|
-
|
|
1065
|
+
// Compute real sizes from responsive style widths; user override wins.
|
|
1066
|
+
const sizesValue = sizes || computeSizesAttribute(style, ctx.breakpoints);
|
|
1067
|
+
|
|
1068
|
+
// Static local image → astro:assets <Picture> (ESM import, Astro-managed
|
|
1069
|
+
// optimization). Predicate rejects CMS/list/template/remote src.
|
|
1070
|
+
if (isStaticImageSrc(src, ctx)) {
|
|
1071
|
+
return emitStaticPictureImage(
|
|
1072
|
+
src,
|
|
1073
|
+
alt,
|
|
1074
|
+
loading,
|
|
1075
|
+
fetchpriority,
|
|
1076
|
+
sizesValue,
|
|
1077
|
+
classExpr,
|
|
1078
|
+
styleAttr,
|
|
1079
|
+
emitIfOpen(node, ctx),
|
|
1080
|
+
emitIfClose(node, ctx),
|
|
1081
|
+
metadata?.blurHash,
|
|
1082
|
+
ctx
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
748
1085
|
|
|
749
1086
|
// Build remaining attributes (exclude image-specific ones we handle manually)
|
|
750
1087
|
const imageSpecificKeys = new Set(['src', 'alt', 'loading', 'width', 'height', 'sizes', 'srcset', 'fetchpriority']);
|
|
@@ -765,56 +1102,82 @@ function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
|
765
1102
|
if (width !== undefined) imgAttrs += ` width="${escapeJSX(String(width))}"`;
|
|
766
1103
|
if (height !== undefined) imgAttrs += ` height="${escapeJSX(String(height))}"`;
|
|
767
1104
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1105
|
+
const hasAvif = !!(metadata?.avifSrcset && ctx.imageFormat !== 'webp');
|
|
1106
|
+
const hasBlur = !!metadata?.blurHash;
|
|
1107
|
+
// Any time we need a wrapper to host the blur placeholder or <source> tags.
|
|
1108
|
+
const useWrapper = hasAvif || hasBlur;
|
|
1109
|
+
|
|
1110
|
+
// Blur placeholder is painted on the wrapper element (NOT the <img>, which
|
|
1111
|
+
// is a replaced element and doesn't paint CSS backgrounds reliably).
|
|
1112
|
+
// Once the real image loads we clear the background on the wrapper so the
|
|
1113
|
+
// low-quality placeholder disappears.
|
|
1114
|
+
const blurWrapperCss = hasBlur
|
|
1115
|
+
? `background-image:url(${escapeJSX(metadata!.blurHash!)});background-size:cover`
|
|
1116
|
+
: '';
|
|
1117
|
+
const blurOnload = hasBlur
|
|
1118
|
+
? ` onload="this.parentElement.style.backgroundImage=''"`
|
|
1119
|
+
: '';
|
|
773
1120
|
|
|
774
1121
|
// Conditional rendering
|
|
775
1122
|
const ifExpr = emitIfOpen(node, ctx);
|
|
776
1123
|
const ifClose = emitIfClose(node, ctx);
|
|
777
1124
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
//
|
|
781
|
-
const
|
|
1125
|
+
if (useWrapper) {
|
|
1126
|
+
// The inner <img> must fill the picture wrapper; otherwise it renders at
|
|
1127
|
+
// its intrinsic size and the wrapper's layout (w/h/position) is wasted.
|
|
1128
|
+
const imgFillClasses = IMG_FILL_CLASSES.slice();
|
|
1129
|
+
|
|
782
1130
|
const classListMatch = classExpr.match(/class:list={\[(.+)\]}/);
|
|
1131
|
+
const classMatch = classExpr.match(/class="([^"]*)"/);
|
|
783
1132
|
|
|
1133
|
+
let pictureClassExpr = '';
|
|
1134
|
+
let imgClassAttr = '';
|
|
784
1135
|
if (classListMatch) {
|
|
785
|
-
// Dynamic class:list - put it all on the picture
|
|
786
|
-
//
|
|
1136
|
+
// Dynamic class:list - put it all on the picture. The inner img only
|
|
1137
|
+
// needs fill classes (splitting dynamic class lists isn't worth it).
|
|
1138
|
+
pictureClassExpr = classExpr;
|
|
1139
|
+
imgClassAttr = ` class="${imgFillClasses.join(' ')}"`;
|
|
1140
|
+
} else {
|
|
1141
|
+
const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
|
|
1142
|
+
const { pictureClasses, imgClasses } = splitImageClasses(allClasses);
|
|
1143
|
+
const fullImgClasses = [...imgClasses, ...imgFillClasses];
|
|
1144
|
+
pictureClassExpr = pictureClasses.length > 0 ? ` class="${pictureClasses.join(' ')}"` : '';
|
|
1145
|
+
imgClassAttr = fullImgClasses.length > 0 ? ` class="${fullImgClasses.join(' ')}"` : '';
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const wrapperStyleAttr = injectInlineStyle(styleAttr, blurWrapperCss);
|
|
1149
|
+
|
|
1150
|
+
// With AVIF: full <picture> + <source> elements
|
|
1151
|
+
if (hasAvif) {
|
|
787
1152
|
return (
|
|
788
|
-
`${ifExpr}${ind(ctx)}<picture${
|
|
789
|
-
`${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata
|
|
790
|
-
`${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata
|
|
791
|
-
`${ind(ctx)} <img${imgAttrs}${
|
|
1153
|
+
`${ifExpr}${ind(ctx)}<picture${pictureClassExpr}${wrapperStyleAttr}>\n` +
|
|
1154
|
+
`${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata!.avifSrcset!)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
1155
|
+
`${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata!.srcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
1156
|
+
`${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurOnload}${otherAttrsStr} />\n` +
|
|
792
1157
|
`${ind(ctx)}</picture>\n${ifClose}`
|
|
793
1158
|
);
|
|
794
1159
|
}
|
|
795
1160
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1161
|
+
// No AVIF, but we still need the wrapper for blur. Put srcset on the <img>
|
|
1162
|
+
// itself since we're not using <source>.
|
|
1163
|
+
if (metadata?.srcset) {
|
|
1164
|
+
imgAttrs += ` srcset="${escapeJSX(metadata.srcset)}"`;
|
|
1165
|
+
imgAttrs += ` sizes="${escapeJSX(sizesValue)}"`;
|
|
1166
|
+
}
|
|
802
1167
|
return (
|
|
803
|
-
`${ifExpr}${ind(ctx)}<picture${
|
|
804
|
-
`${ind(ctx)} <
|
|
805
|
-
`${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata.srcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
806
|
-
`${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurStyle}${otherAttrsStr} />\n` +
|
|
1168
|
+
`${ifExpr}${ind(ctx)}<picture${pictureClassExpr}${wrapperStyleAttr}>\n` +
|
|
1169
|
+
`${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurOnload}${otherAttrsStr} />\n` +
|
|
807
1170
|
`${ind(ctx)}</picture>\n${ifClose}`
|
|
808
1171
|
);
|
|
809
1172
|
}
|
|
810
1173
|
|
|
811
|
-
//
|
|
1174
|
+
// No wrapper needed: plain <img> with optional srcset/sizes
|
|
812
1175
|
if (metadata?.srcset) {
|
|
813
1176
|
imgAttrs += ` srcset="${escapeJSX(metadata.srcset)}"`;
|
|
814
1177
|
imgAttrs += ` sizes="${escapeJSX(sizesValue)}"`;
|
|
815
1178
|
}
|
|
816
1179
|
|
|
817
|
-
return `${ifExpr}${ind(ctx)}<img${classExpr}${styleAttr}${imgAttrs}${
|
|
1180
|
+
return `${ifExpr}${ind(ctx)}<img${classExpr}${styleAttr}${imgAttrs}${otherAttrsStr} />\n${ifClose}`;
|
|
818
1181
|
}
|
|
819
1182
|
|
|
820
1183
|
function emitHtmlNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
@@ -857,6 +1220,9 @@ function emitHtmlNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
|
857
1220
|
node.generateElementClass
|
|
858
1221
|
) {
|
|
859
1222
|
elementClass = buildElementClass(ctx, label);
|
|
1223
|
+
if (elementClass && node.interactiveStyles?.length && ctx.collectedInteractiveStyles) {
|
|
1224
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1225
|
+
}
|
|
860
1226
|
}
|
|
861
1227
|
|
|
862
1228
|
const { classExpr, styleAttr } = buildClassAndStyleExpression(style, node.interactiveStyles as InteractiveStyles | undefined, elementClass, ctx);
|
|
@@ -883,8 +1249,8 @@ function emitHtmlNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
|
883
1249
|
}
|
|
884
1250
|
|
|
885
1251
|
function emitComponentInstance(node: ComponentInstanceNode, ctx: AstroEmitContext): string {
|
|
886
|
-
const name = node.component;
|
|
887
|
-
ctx.imports.add(
|
|
1252
|
+
const name = astroComponentName(node.component);
|
|
1253
|
+
ctx.imports.add(node.component);
|
|
888
1254
|
|
|
889
1255
|
const ifExpr = emitIfOpen(node, ctx);
|
|
890
1256
|
|
|
@@ -929,16 +1295,24 @@ function emitComponentInstance(node: ComponentInstanceNode, ctx: AstroEmitContex
|
|
|
929
1295
|
} else if (ctx.isComponentDef && isI18nValue(value)) {
|
|
930
1296
|
// i18n object in component def — wrap with r() resolver (resolved at runtime via Astro.currentLocale)
|
|
931
1297
|
ctx.needsI18nResolver = true;
|
|
932
|
-
propParts.push(`${key}={r(${JSON.stringify(value)})}`);
|
|
1298
|
+
propParts.push(`${key}={r(${JSON.stringify(stripRawHtmlPrefixDeep(value))})}`);
|
|
933
1299
|
} else {
|
|
934
|
-
|
|
1300
|
+
// Rich-text / embed props contain HTML/SVG — pass as template literal
|
|
1301
|
+
// so the component can render via set:html without entity escaping
|
|
1302
|
+
const targetInterface = ctx.globalComponents[name]?.component?.interface;
|
|
1303
|
+
const propDef = targetInterface?.[key];
|
|
1304
|
+
if (typeof value === 'string' && (propDef?.type === 'rich-text' || propDef?.type === 'embed')) {
|
|
1305
|
+
propParts.push(`${key}={\`${escapeTemplateLiteral(stripRawHtmlPrefixDeep(value) as string)}\`}`);
|
|
1306
|
+
} else {
|
|
1307
|
+
propParts.push(`${key}=${formatPropValue(value)}`);
|
|
1308
|
+
}
|
|
935
1309
|
}
|
|
936
1310
|
}
|
|
937
1311
|
}
|
|
938
1312
|
|
|
939
1313
|
// Instance-level style overrides as className (Tailwind)
|
|
940
1314
|
if (node.style) {
|
|
941
|
-
const { classes: instanceClasses } = responsiveStylesToTailwind(node.style as StyleObject | ResponsiveStyleObject, ctx.breakpoints);
|
|
1315
|
+
const { classes: instanceClasses } = responsiveStylesToTailwind(node.style as StyleObject | ResponsiveStyleObject, ctx.breakpoints, ctx.responsiveScales);
|
|
942
1316
|
if (instanceClasses.length > 0) {
|
|
943
1317
|
propParts.push(`class="${instanceClasses.join(' ')}"`);
|
|
944
1318
|
}
|
|
@@ -946,7 +1320,11 @@ function emitComponentInstance(node: ComponentInstanceNode, ctx: AstroEmitContex
|
|
|
946
1320
|
|
|
947
1321
|
const propsStr = propParts.length > 0 ? ' ' + propParts.join(' ') : '';
|
|
948
1322
|
|
|
949
|
-
|
|
1323
|
+
// Reset elementPath to [0] when entering a component instance, matching SSR's
|
|
1324
|
+
// renderComponent behavior. This ensures hash-based element class names for
|
|
1325
|
+
// slot content are identical between SSR (CSS) and Astro (HTML).
|
|
1326
|
+
const childCtx = { ...ctx, elementPath: [0] };
|
|
1327
|
+
const children = emitChildren(node.children, childCtx);
|
|
950
1328
|
|
|
951
1329
|
if (!children.trim()) {
|
|
952
1330
|
return `${ifExpr}${ind(ctx)}<${name}${propsStr} />\n${emitIfClose(node, ctx)}`;
|
|
@@ -983,6 +1361,9 @@ function emitEmbedNode(node: EmbedNode, ctx: AstroEmitContext): string {
|
|
|
983
1361
|
node.generateElementClass
|
|
984
1362
|
) {
|
|
985
1363
|
elementClass = buildElementClass(ctx, node.label);
|
|
1364
|
+
if (elementClass && node.interactiveStyles?.length && ctx.collectedInteractiveStyles) {
|
|
1365
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1366
|
+
}
|
|
986
1367
|
}
|
|
987
1368
|
|
|
988
1369
|
const { classExpr, styleAttr } = buildClassAndStyleExpression(style, node.interactiveStyles as InteractiveStyles | undefined, elementClass, ctx);
|
|
@@ -1055,6 +1436,9 @@ function emitLinkNode(node: LinkNode, ctx: AstroEmitContext): string {
|
|
|
1055
1436
|
node.generateElementClass
|
|
1056
1437
|
) {
|
|
1057
1438
|
elementClass = buildElementClass(ctx, node.label);
|
|
1439
|
+
if (elementClass && node.interactiveStyles?.length && ctx.collectedInteractiveStyles) {
|
|
1440
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1441
|
+
}
|
|
1058
1442
|
}
|
|
1059
1443
|
|
|
1060
1444
|
// Build class expression with olink base class
|
|
@@ -1151,6 +1535,9 @@ function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
|
|
|
1151
1535
|
node.generateElementClass
|
|
1152
1536
|
) {
|
|
1153
1537
|
elementClass = buildElementClass(ctx, node.label);
|
|
1538
|
+
if (elementClass && node.interactiveStyles?.length && ctx.collectedInteractiveStyles) {
|
|
1539
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1540
|
+
}
|
|
1154
1541
|
}
|
|
1155
1542
|
|
|
1156
1543
|
const { classExpr, styleAttr } = buildClassAndStyleExpression(
|
|
@@ -1163,6 +1550,24 @@ function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
|
|
|
1163
1550
|
const src = node.src as string | undefined;
|
|
1164
1551
|
const alt = node.alt as string | undefined;
|
|
1165
1552
|
|
|
1553
|
+
// Static local image → astro:assets <Picture>
|
|
1554
|
+
if (isStaticImageSrc(src, ctx)) {
|
|
1555
|
+
const staticMeta = ctx.imageMetadataMap!.get(src)!;
|
|
1556
|
+
return emitStaticPictureImage(
|
|
1557
|
+
src,
|
|
1558
|
+
alt,
|
|
1559
|
+
undefined,
|
|
1560
|
+
undefined,
|
|
1561
|
+
computeSizesAttribute(style, ctx.breakpoints),
|
|
1562
|
+
classExpr,
|
|
1563
|
+
styleAttr,
|
|
1564
|
+
'',
|
|
1565
|
+
'',
|
|
1566
|
+
staticMeta.blurHash,
|
|
1567
|
+
ctx
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1166
1571
|
let imgAttrs = '';
|
|
1167
1572
|
if (src) imgAttrs += ` src="${escapeJSX(String(src))}"`;
|
|
1168
1573
|
if (alt !== undefined) imgAttrs += ` alt="${escapeJSX(String(alt))}"`;
|
|
@@ -1170,32 +1575,43 @@ function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
|
|
|
1170
1575
|
// Check for image metadata for responsive images
|
|
1171
1576
|
const metadata = src ? ctx.imageMetadataMap?.get(String(src)) : undefined;
|
|
1172
1577
|
|
|
1173
|
-
if (metadata) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
if (width !== undefined) imgAttrs += ` width="${width}"`;
|
|
1177
|
-
if (height !== undefined) imgAttrs += ` height="${height}"`;
|
|
1178
|
-
|
|
1179
|
-
let blurStyle = '';
|
|
1180
|
-
if (metadata.blurHash) {
|
|
1181
|
-
blurStyle = ` style="background-image: url(${escapeJSX(metadata.blurHash)}); background-size: cover;" onload="this.style.backgroundImage=''"`;
|
|
1182
|
-
}
|
|
1578
|
+
if (!metadata) {
|
|
1579
|
+
return `${ind(ctx)}<img${classExpr}${styleAttr}${imgAttrs} />\n`;
|
|
1580
|
+
}
|
|
1183
1581
|
|
|
1184
|
-
|
|
1582
|
+
if (metadata.width !== undefined) imgAttrs += ` width="${metadata.width}"`;
|
|
1583
|
+
if (metadata.height !== undefined) imgAttrs += ` height="${metadata.height}"`;
|
|
1185
1584
|
|
|
1186
|
-
|
|
1187
|
-
const classMatch = classExpr.match(/class="([^"]*)"/);
|
|
1188
|
-
const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
|
|
1189
|
-
const { pictureClasses, imgClasses } = splitImageClasses(allClasses);
|
|
1585
|
+
const sizesValue = computeSizesAttribute(style, ctx.breakpoints);
|
|
1190
1586
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1587
|
+
const hasAvif = !!(metadata.avifSrcset && ctx.imageFormat !== 'webp');
|
|
1588
|
+
const hasBlur = !!metadata.blurHash;
|
|
1589
|
+
const useWrapper = hasAvif || hasBlur;
|
|
1193
1590
|
|
|
1591
|
+
const blurWrapperCss = hasBlur
|
|
1592
|
+
? `background-image:url(${escapeJSX(metadata.blurHash!)});background-size:cover`
|
|
1593
|
+
: '';
|
|
1594
|
+
const blurOnload = hasBlur
|
|
1595
|
+
? ` onload="this.parentElement.style.backgroundImage=''"`
|
|
1596
|
+
: '';
|
|
1597
|
+
|
|
1598
|
+
if (useWrapper) {
|
|
1599
|
+
const imgFillClasses = IMG_FILL_CLASSES.slice();
|
|
1600
|
+
const classMatch = classExpr.match(/class="([^"]*)"/);
|
|
1601
|
+
const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
|
|
1602
|
+
const { pictureClasses, imgClasses } = splitImageClasses(allClasses);
|
|
1603
|
+
const fullImgClasses = [...imgClasses, ...imgFillClasses];
|
|
1604
|
+
|
|
1605
|
+
const pictureClassAttr = pictureClasses.length > 0 ? ` class="${pictureClasses.join(' ')}"` : '';
|
|
1606
|
+
const imgClassAttr = fullImgClasses.length > 0 ? ` class="${fullImgClasses.join(' ')}"` : '';
|
|
1607
|
+
const wrapperStyleAttr = injectInlineStyle(styleAttr, blurWrapperCss);
|
|
1608
|
+
|
|
1609
|
+
if (hasAvif) {
|
|
1194
1610
|
return (
|
|
1195
|
-
`${ind(ctx)}<picture${pictureClassAttr}${
|
|
1196
|
-
`${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata.avifSrcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
1611
|
+
`${ind(ctx)}<picture${pictureClassAttr}${wrapperStyleAttr}>\n` +
|
|
1612
|
+
`${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata.avifSrcset!)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
1197
1613
|
`${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata.srcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
1198
|
-
`${ind(ctx)} <img${imgClassAttr}${imgAttrs}${
|
|
1614
|
+
`${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurOnload} />\n` +
|
|
1199
1615
|
`${ind(ctx)}</picture>\n`
|
|
1200
1616
|
);
|
|
1201
1617
|
}
|
|
@@ -1204,6 +1620,16 @@ function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
|
|
|
1204
1620
|
imgAttrs += ` srcset="${escapeJSX(metadata.srcset)}"`;
|
|
1205
1621
|
imgAttrs += ` sizes="${escapeJSX(sizesValue)}"`;
|
|
1206
1622
|
}
|
|
1623
|
+
return (
|
|
1624
|
+
`${ind(ctx)}<picture${pictureClassAttr}${wrapperStyleAttr}>\n` +
|
|
1625
|
+
`${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurOnload} />\n` +
|
|
1626
|
+
`${ind(ctx)}</picture>\n`
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (metadata.srcset) {
|
|
1631
|
+
imgAttrs += ` srcset="${escapeJSX(metadata.srcset)}"`;
|
|
1632
|
+
imgAttrs += ` sizes="${escapeJSX(sizesValue)}"`;
|
|
1207
1633
|
}
|
|
1208
1634
|
|
|
1209
1635
|
return `${ind(ctx)}<img${classExpr}${styleAttr}${imgAttrs} />\n`;
|
|
@@ -1370,6 +1796,9 @@ function emitLocaleListNode(node: LocaleListNode, ctx: AstroEmitContext): string
|
|
|
1370
1796
|
let elementClass: string | null = null;
|
|
1371
1797
|
if ((node.interactiveStyles && (node.interactiveStyles as InteractiveStyles).length > 0) || node.generateElementClass) {
|
|
1372
1798
|
elementClass = buildElementClass(ctx, node.label);
|
|
1799
|
+
if (elementClass && (node.interactiveStyles as InteractiveStyles)?.length && ctx.collectedInteractiveStyles) {
|
|
1800
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1801
|
+
}
|
|
1373
1802
|
}
|
|
1374
1803
|
|
|
1375
1804
|
const { classExpr: containerClassExpr, styleAttr: containerStyleAttr } = buildClassAndStyleExpression(
|
|
@@ -1381,17 +1810,17 @@ function emitLocaleListNode(node: LocaleListNode, ctx: AstroEmitContext): string
|
|
|
1381
1810
|
|
|
1382
1811
|
// Build item classes
|
|
1383
1812
|
const itemStyle = node.itemStyle as StyleObject | ResponsiveStyleObject | undefined;
|
|
1384
|
-
const itemResult = itemStyle ? responsiveStylesToTailwind(itemStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
|
|
1813
|
+
const itemResult = itemStyle ? responsiveStylesToTailwind(itemStyle, ctx.breakpoints, ctx.responsiveScales) : { classes: [], dynamicStyles: {} };
|
|
1385
1814
|
const itemClasses = itemResult.classes;
|
|
1386
1815
|
|
|
1387
1816
|
// Build active item classes (item + active combined)
|
|
1388
1817
|
const activeItemStyle = node.activeItemStyle as StyleObject | ResponsiveStyleObject | undefined;
|
|
1389
|
-
const activeResult = activeItemStyle ? responsiveStylesToTailwind(activeItemStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
|
|
1818
|
+
const activeResult = activeItemStyle ? responsiveStylesToTailwind(activeItemStyle, ctx.breakpoints, ctx.responsiveScales) : { classes: [], dynamicStyles: {} };
|
|
1390
1819
|
const activeItemClasses = [...itemClasses, ...activeResult.classes];
|
|
1391
1820
|
|
|
1392
1821
|
// Build separator classes
|
|
1393
1822
|
const separatorStyle = node.separatorStyle as StyleObject | ResponsiveStyleObject | undefined;
|
|
1394
|
-
const sepResult = separatorStyle ? responsiveStylesToTailwind(separatorStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
|
|
1823
|
+
const sepResult = separatorStyle ? responsiveStylesToTailwind(separatorStyle, ctx.breakpoints, ctx.responsiveScales) : { classes: [], dynamicStyles: {} };
|
|
1395
1824
|
const separatorClasses = sepResult.classes;
|
|
1396
1825
|
|
|
1397
1826
|
// Build locale icon map
|
|
@@ -1404,7 +1833,7 @@ function emitLocaleListNode(node: LocaleListNode, ctx: AstroEmitContext): string
|
|
|
1404
1833
|
|
|
1405
1834
|
// Flag classes
|
|
1406
1835
|
const flagStyle = node.flagStyle as StyleObject | ResponsiveStyleObject | undefined;
|
|
1407
|
-
const flagResult = flagStyle ? responsiveStylesToTailwind(flagStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
|
|
1836
|
+
const flagResult = flagStyle ? responsiveStylesToTailwind(flagStyle, ctx.breakpoints, ctx.responsiveScales) : { classes: [], dynamicStyles: {} };
|
|
1408
1837
|
const flagClasses = flagResult.classes;
|
|
1409
1838
|
|
|
1410
1839
|
// Build links
|