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