meno-core 1.0.39 → 1.0.40
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 +195 -68
- package/dist/bin/cli.js +1 -1
- package/dist/build-static.js +6 -6
- package/dist/chunks/{chunk-WK5XLASY.js → chunk-3NOZVNM4.js} +3 -3
- package/dist/chunks/{chunk-W6HDII4T.js → chunk-GKICS7CF.js} +27 -14
- package/dist/chunks/chunk-GKICS7CF.js.map +7 -0
- package/dist/chunks/{chunk-P3FX5HJM.js → chunk-LOJLO2EY.js} +1 -1
- package/dist/chunks/chunk-LOJLO2EY.js.map +7 -0
- package/dist/chunks/{chunk-HNAS6BSS.js → chunk-MOCRENNU.js} +55 -5
- package/dist/chunks/{chunk-HNAS6BSS.js.map → chunk-MOCRENNU.js.map} +3 -3
- package/dist/chunks/{chunk-NV25WXCA.js → chunk-OJ5SROQN.js} +5 -3
- package/dist/chunks/chunk-OJ5SROQN.js.map +7 -0
- package/dist/chunks/{chunk-AIXKUVNG.js → chunk-V4SVSX3X.js} +3 -3
- package/dist/chunks/{chunk-KULPBDC7.js → chunk-Z7SAOCDG.js} +5 -2
- package/dist/chunks/{chunk-KULPBDC7.js.map → chunk-Z7SAOCDG.js.map} +2 -2
- package/dist/chunks/{constants-5CRJRQNR.js → constants-L75FR445.js} +2 -2
- package/dist/entries/server-router.js +6 -6
- package/dist/lib/client/index.js +5 -5
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +2007 -197
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +3 -3
- package/dist/lib/test-utils/index.js +1 -1
- package/lib/client/core/builders/embedBuilder.ts +2 -2
- package/lib/server/astro/cmsPageEmitter.ts +417 -0
- package/lib/server/astro/componentEmitter.ts +90 -5
- package/lib/server/astro/nodeToAstro.ts +830 -37
- package/lib/server/astro/pageEmitter.ts +39 -3
- package/lib/server/astro/tailwindMapper.ts +69 -8
- package/lib/server/astro/templateTransformer.ts +107 -0
- package/lib/server/index.ts +9 -0
- package/lib/server/routes/api/components.ts +62 -0
- package/lib/server/routes/api/core-routes.ts +8 -0
- package/lib/server/ssr/ssrRenderer.ts +30 -10
- package/lib/server/webflow/buildWebflow.ts +415 -0
- package/lib/server/webflow/index.ts +22 -0
- package/lib/server/webflow/nodeToWebflow.ts +423 -0
- package/lib/server/webflow/styleMapper.ts +241 -0
- package/lib/server/webflow/types.ts +196 -0
- package/lib/shared/constants.ts +2 -0
- package/lib/shared/types/components.ts +1 -0
- package/lib/shared/validation/schemas.ts +1 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-NV25WXCA.js.map +0 -7
- package/dist/chunks/chunk-P3FX5HJM.js.map +0 -7
- package/dist/chunks/chunk-W6HDII4T.js.map +0 -7
- /package/dist/chunks/{chunk-WK5XLASY.js.map → chunk-3NOZVNM4.js.map} +0 -0
- /package/dist/chunks/{chunk-AIXKUVNG.js.map → chunk-V4SVSX3X.js.map} +0 -0
- /package/dist/chunks/{constants-5CRJRQNR.js.map → constants-L75FR445.js.map} +0 -0
|
@@ -14,8 +14,11 @@ import type {
|
|
|
14
14
|
EmbedNode,
|
|
15
15
|
LinkNode,
|
|
16
16
|
LocaleListNode,
|
|
17
|
+
I18nConfig,
|
|
18
|
+
CMSSchema,
|
|
19
|
+
ListNode,
|
|
17
20
|
} from '../../shared/types';
|
|
18
|
-
import
|
|
21
|
+
import { singularize } from '../../shared/types';
|
|
19
22
|
import type {
|
|
20
23
|
StyleObject,
|
|
21
24
|
ResponsiveStyleObject,
|
|
@@ -28,8 +31,13 @@ import { responsiveStylesToTailwind, propertyToTailwind } from './tailwindMapper
|
|
|
28
31
|
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
29
32
|
import { generateElementClassName, type ElementClassContext } from '../../shared/elementClassName';
|
|
30
33
|
import { isVoidElement } from '../../shared/nodeUtils';
|
|
31
|
-
import { NODE_TYPE } from '../../shared/constants';
|
|
34
|
+
import { NODE_TYPE, RAW_HTML_PREFIX } from '../../shared/constants';
|
|
32
35
|
import { extractInteractiveStyleMappings, hasInteractiveStyleMappings } from '../../shared/interactiveStyleMappings';
|
|
36
|
+
import type { ImageMetadataMap } from '../ssr/imageMetadata';
|
|
37
|
+
import { isI18nValue, resolveI18nValue, DEFAULT_I18N_CONFIG, buildLocalizedPath } from '../../shared/i18n';
|
|
38
|
+
import { transformCMSTemplate, isTemplateExpression, transformItemTemplate, replaceItemMetaVars, rewriteItemVar } from './templateTransformer';
|
|
39
|
+
import type { SlugMap } from '../../shared/slugTranslator';
|
|
40
|
+
import { buildSlugIndex, translatePath } from '../../shared/slugTranslator';
|
|
33
41
|
|
|
34
42
|
// ---------------------------------------------------------------------------
|
|
35
43
|
// Types
|
|
@@ -58,6 +66,42 @@ export interface AstroEmitContext {
|
|
|
58
66
|
breakpoints: BreakpointConfig;
|
|
59
67
|
/** Dynamic tag definitions collected during traversal (for frontmatter) */
|
|
60
68
|
dynamicTags?: Map<string, string>;
|
|
69
|
+
/** Image metadata map for responsive image generation */
|
|
70
|
+
imageMetadataMap?: ImageMetadataMap;
|
|
71
|
+
/** Page locale for resolving i18n values */
|
|
72
|
+
locale?: string;
|
|
73
|
+
/** CMS mode: transform {{cms.field}} to entry data expressions */
|
|
74
|
+
cmsMode?: boolean;
|
|
75
|
+
/** Variable name bound to the CMS entry (default: 'entry') */
|
|
76
|
+
cmsEntryBinding?: string;
|
|
77
|
+
/** Rich-text field names for CMS mode (use Fragment set:html) */
|
|
78
|
+
cmsRichTextFields?: Set<string>;
|
|
79
|
+
/** Current list item iteration variable name */
|
|
80
|
+
listItemBinding?: string;
|
|
81
|
+
/** Extra frontmatter code lines collected during emission */
|
|
82
|
+
frontmatterLines?: string[];
|
|
83
|
+
/** Astro API imports needed (e.g., 'getCollection') */
|
|
84
|
+
astroImports?: Set<string>;
|
|
85
|
+
/** Full i18n config for locale list emission */
|
|
86
|
+
i18nConfig?: I18nConfig;
|
|
87
|
+
/** locale→slug for current page (for locale list links) */
|
|
88
|
+
currentPageSlugMap?: Record<string, string>;
|
|
89
|
+
/** CMS schema for rich-text field detection */
|
|
90
|
+
cmsSchema?: CMSSchema;
|
|
91
|
+
/** Wrap function name for i18n resolution (e.g., 'r') */
|
|
92
|
+
cmsWrapFn?: string;
|
|
93
|
+
/** Default locale for i18n resolver (used in component defs) */
|
|
94
|
+
defaultLocale?: string;
|
|
95
|
+
/** Set to true during emission when an i18n object is encountered in a component def */
|
|
96
|
+
needsI18nResolver?: boolean;
|
|
97
|
+
/** Index variable name for current list .map() callback (e.g., "itemIndex") */
|
|
98
|
+
listIndexVar?: string;
|
|
99
|
+
/** Source array expression for current list (e.g., "items"), used for itemLast */
|
|
100
|
+
listSourceVar?: string;
|
|
101
|
+
/** Slug mappings for translating internal links */
|
|
102
|
+
slugMappings?: SlugMap[];
|
|
103
|
+
/** I18n default locale (for slug translation) */
|
|
104
|
+
i18nDefaultLocale?: string;
|
|
61
105
|
}
|
|
62
106
|
|
|
63
107
|
// ---------------------------------------------------------------------------
|
|
@@ -68,6 +112,19 @@ function ind(ctx: AstroEmitContext): string {
|
|
|
68
112
|
return ' '.repeat(ctx.indent);
|
|
69
113
|
}
|
|
70
114
|
|
|
115
|
+
function localizeHref(href: string, ctx: AstroEmitContext): string {
|
|
116
|
+
if (!href.startsWith('/') || href.startsWith('//')) return href;
|
|
117
|
+
const { locale, i18nDefaultLocale, slugMappings } = ctx;
|
|
118
|
+
if (!locale || !i18nDefaultLocale) return href;
|
|
119
|
+
if (slugMappings && slugMappings.length > 0) {
|
|
120
|
+
const slugIndex = buildSlugIndex(slugMappings);
|
|
121
|
+
return translatePath(href, locale, i18nDefaultLocale, i18nDefaultLocale, slugIndex);
|
|
122
|
+
} else if (locale !== i18nDefaultLocale) {
|
|
123
|
+
return buildLocalizedPath(href, locale);
|
|
124
|
+
}
|
|
125
|
+
return href;
|
|
126
|
+
}
|
|
127
|
+
|
|
71
128
|
function isStyleMapping(value: unknown): value is StyleMapping {
|
|
72
129
|
return (
|
|
73
130
|
typeof value === 'object' &&
|
|
@@ -86,6 +143,41 @@ function isLinkMapping(value: unknown): value is LinkMapping {
|
|
|
86
143
|
);
|
|
87
144
|
}
|
|
88
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Emit a single attribute key=value, applying CMS/list template transformation if needed.
|
|
148
|
+
*/
|
|
149
|
+
function emitAttrValue(key: string, value: string, ctx: AstroEmitContext): string {
|
|
150
|
+
if (ctx.cmsMode && /\{\{cms\./.test(value)) {
|
|
151
|
+
const b = ctx.cmsEntryBinding || 'entry';
|
|
152
|
+
const w = (expr: string) => ctx.cmsWrapFn ? `${ctx.cmsWrapFn}(${expr})` : expr;
|
|
153
|
+
const fullMatch = value.match(/^\{\{cms\.([^}]+)\}\}$/);
|
|
154
|
+
if (fullMatch) {
|
|
155
|
+
return `${key}={${w(`${b}.data.${fullMatch[1].trim()}`)}}`;
|
|
156
|
+
}
|
|
157
|
+
const replaced = value.replace(/\{\{cms\.([^}]+)\}\}/g, (_, fp) =>
|
|
158
|
+
`\${${w(`${b}.data.${fp.trim()}`)}}`
|
|
159
|
+
);
|
|
160
|
+
return `${key}={\`${replaced}\`}`;
|
|
161
|
+
}
|
|
162
|
+
if (ctx.listItemBinding && /\{\{/.test(value)) {
|
|
163
|
+
const fullMatch = value.match(/^\{\{(.+)\}\}$/);
|
|
164
|
+
if (fullMatch) {
|
|
165
|
+
let expr = fullMatch[1].trim();
|
|
166
|
+
expr = rewriteItemVar(expr, ctx.listItemBinding);
|
|
167
|
+
if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
|
|
168
|
+
return `${key}={${expr}}`;
|
|
169
|
+
}
|
|
170
|
+
const replaced = value.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
171
|
+
let trimmed = expr.trim();
|
|
172
|
+
trimmed = rewriteItemVar(trimmed, ctx.listItemBinding!);
|
|
173
|
+
if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
|
|
174
|
+
return `\${${trimmed}}`;
|
|
175
|
+
});
|
|
176
|
+
return `${key}={\`${replaced}\`}`;
|
|
177
|
+
}
|
|
178
|
+
return `${key}="${escapeJSX(value)}"`;
|
|
179
|
+
}
|
|
180
|
+
|
|
89
181
|
function isHtmlMapping(value: unknown): value is HtmlMapping {
|
|
90
182
|
return (
|
|
91
183
|
typeof value === 'object' &&
|
|
@@ -167,14 +259,15 @@ function mappingToClassListEntries(
|
|
|
167
259
|
const cls1 = getClassForValue(property, css1, breakpointPrefix);
|
|
168
260
|
const cls2 = getClassForValue(property, css2, breakpointPrefix);
|
|
169
261
|
if (cls1 && cls2) {
|
|
170
|
-
|
|
262
|
+
// Use String() coercion so number props match string mapping keys (e.g., size=2 matches "2")
|
|
263
|
+
entries.push(`String(${propRef}) === ${JSON.stringify(String(coerceValue(val1)))} ? '${cls1}' : '${cls2}'`);
|
|
171
264
|
}
|
|
172
265
|
} else {
|
|
173
266
|
// Multiple values: use a lookup object or multiple ternaries
|
|
174
267
|
for (const [val, cssValue] of values) {
|
|
175
268
|
const cls = getClassForValue(property, cssValue, breakpointPrefix);
|
|
176
269
|
if (cls) {
|
|
177
|
-
entries.push(
|
|
270
|
+
entries.push(`String(${propRef}) === ${JSON.stringify(String(coerceValue(val)))} && '${cls}'`);
|
|
178
271
|
}
|
|
179
272
|
}
|
|
180
273
|
}
|
|
@@ -247,13 +340,23 @@ function buildClassAndStyleExpression(
|
|
|
247
340
|
const styleParts: string[] = [];
|
|
248
341
|
for (const [cssProp, value] of Object.entries(dynamicStyles)) {
|
|
249
342
|
// Convert {{propName}} to Astro expression in style
|
|
250
|
-
const resolved = value.replace(/\{\{(.+?)\}\}/g, (_, expr) =>
|
|
343
|
+
const resolved = value.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
344
|
+
let trimmed = expr.trim();
|
|
345
|
+
if (ctx.listItemBinding) trimmed = rewriteItemVar(trimmed, ctx.listItemBinding);
|
|
346
|
+
if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
|
|
347
|
+
return `\${${trimmed}}`;
|
|
348
|
+
});
|
|
251
349
|
styleParts.push(`${cssProp}: \${${resolved.includes('${') ? resolved.replace(/\$\{(.+?)\}/g, '$1') : `'${resolved}'`}}`);
|
|
252
350
|
}
|
|
253
351
|
// Build as template literal style attribute
|
|
254
352
|
const entries: string[] = [];
|
|
255
353
|
for (const [cssProp, value] of Object.entries(dynamicStyles)) {
|
|
256
|
-
const resolved = value.replace(/\{\{(.+?)\}\}/g, (_, expr) =>
|
|
354
|
+
const resolved = value.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
355
|
+
let trimmed = expr.trim();
|
|
356
|
+
if (ctx.listItemBinding) trimmed = rewriteItemVar(trimmed, ctx.listItemBinding);
|
|
357
|
+
if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
|
|
358
|
+
return `\${${trimmed}}`;
|
|
359
|
+
});
|
|
257
360
|
entries.push(`${cssProp}: ${resolved}`);
|
|
258
361
|
}
|
|
259
362
|
styleAttr = ` style={\`${entries.join('; ')}\`}`;
|
|
@@ -330,10 +433,22 @@ function resolveTemplate(text: string, ctx: AstroEmitContext): string {
|
|
|
330
433
|
// Check if entire text is a single {{expression}}
|
|
331
434
|
const fullMatch = text.match(/^\{\{(.+)\}\}$/);
|
|
332
435
|
if (fullMatch) {
|
|
333
|
-
|
|
436
|
+
let propName = fullMatch[1].trim();
|
|
437
|
+
if (ctx.listItemBinding) propName = rewriteItemVar(propName, ctx.listItemBinding);
|
|
438
|
+
if (ctx.listIndexVar) propName = replaceItemMetaVars(propName, ctx.listIndexVar, ctx.listSourceVar);
|
|
439
|
+
// Rich-text props contain HTML - render unescaped via set:html
|
|
440
|
+
if (ctx.componentProps[propName]?.type === 'rich-text') {
|
|
441
|
+
return `<Fragment set:html={${propName}} />`;
|
|
442
|
+
}
|
|
443
|
+
return `{${propName}}`;
|
|
334
444
|
}
|
|
335
445
|
// Mixed content: replace each {{expr}} with {expr}
|
|
336
|
-
return text.replace(/\{\{(.+?)\}\}/g, (_, expr) =>
|
|
446
|
+
return text.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
447
|
+
let trimmed = expr.trim();
|
|
448
|
+
if (ctx.listItemBinding) trimmed = rewriteItemVar(trimmed, ctx.listItemBinding);
|
|
449
|
+
if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
|
|
450
|
+
return `{${trimmed}}`;
|
|
451
|
+
});
|
|
337
452
|
}
|
|
338
453
|
|
|
339
454
|
/**
|
|
@@ -376,12 +491,56 @@ function buildAttributesString(
|
|
|
376
491
|
// Check if entire value is a single {{expression}}
|
|
377
492
|
const fullMatch = strVal.match(/^\{\{(.+)\}\}$/);
|
|
378
493
|
if (fullMatch) {
|
|
379
|
-
|
|
494
|
+
let expr = fullMatch[1].trim();
|
|
495
|
+
if (ctx.listItemBinding) expr = rewriteItemVar(expr, ctx.listItemBinding);
|
|
496
|
+
if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
|
|
497
|
+
const propDef = ctx.componentProps[expr];
|
|
498
|
+
if (propDef && propDef.type === 'link') {
|
|
499
|
+
parts.push(`${key}={${expr}?.href ?? "#"}`);
|
|
500
|
+
} else {
|
|
501
|
+
parts.push(`${key}={${expr} || undefined}`);
|
|
502
|
+
}
|
|
380
503
|
} else {
|
|
381
504
|
// Mixed content: use template literal
|
|
382
|
-
const resolved = strVal.replace(/\{\{(.+?)\}\}/g, (_, expr) =>
|
|
505
|
+
const resolved = strVal.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
506
|
+
let trimmed = expr.trim();
|
|
507
|
+
if (ctx.listItemBinding) trimmed = rewriteItemVar(trimmed, ctx.listItemBinding);
|
|
508
|
+
if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
|
|
509
|
+
const pd = ctx.componentProps[trimmed];
|
|
510
|
+
return pd?.type === 'link' ? `\${${trimmed}?.href ?? "#"}` : `\${${trimmed}}`;
|
|
511
|
+
});
|
|
512
|
+
parts.push(`${key}={\`${resolved}\`}`);
|
|
513
|
+
}
|
|
514
|
+
} else if (ctx.listItemBinding && hasTemplates(strVal)) {
|
|
515
|
+
// List item binding: transform {{item.field}} in attributes
|
|
516
|
+
const fullMatch = strVal.match(/^\{\{(.+)\}\}$/);
|
|
517
|
+
if (fullMatch) {
|
|
518
|
+
let expr = fullMatch[1].trim();
|
|
519
|
+
expr = rewriteItemVar(expr, ctx.listItemBinding);
|
|
520
|
+
if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
|
|
521
|
+
parts.push(`${key}={${expr} || undefined}`);
|
|
522
|
+
} else {
|
|
523
|
+
const resolved = strVal.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
524
|
+
let trimmed = expr.trim();
|
|
525
|
+
trimmed = rewriteItemVar(trimmed, ctx.listItemBinding!);
|
|
526
|
+
if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
|
|
527
|
+
return `\${${trimmed}}`;
|
|
528
|
+
});
|
|
383
529
|
parts.push(`${key}={\`${resolved}\`}`);
|
|
384
530
|
}
|
|
531
|
+
} else if (ctx.cmsMode && /\{\{cms\./.test(strVal)) {
|
|
532
|
+
// CMS mode: transform {{cms.field}} in attributes
|
|
533
|
+
const b = ctx.cmsEntryBinding || 'entry';
|
|
534
|
+
const w = (expr: string) => ctx.cmsWrapFn ? `${ctx.cmsWrapFn}(${expr})` : expr;
|
|
535
|
+
const fullMatch = strVal.match(/^\{\{cms\.([^}]+)\}\}$/);
|
|
536
|
+
if (fullMatch) {
|
|
537
|
+
parts.push(`${key}={${w(`${b}.data.${fullMatch[1].trim()}`)}}`);
|
|
538
|
+
} else {
|
|
539
|
+
const replaced = strVal.replace(/\{\{cms\.([^}]+)\}\}/g, (_, fieldPath) => {
|
|
540
|
+
return `\${${w(`${b}.data.${fieldPath.trim()}`)}}`;
|
|
541
|
+
});
|
|
542
|
+
parts.push(`${key}={\`${replaced}\`}`);
|
|
543
|
+
}
|
|
385
544
|
} else {
|
|
386
545
|
parts.push(`${key}="${escapeJSX(strVal)}"`);
|
|
387
546
|
}
|
|
@@ -406,6 +565,17 @@ function formatPropValue(value: unknown): string {
|
|
|
406
565
|
// Main recursive converter
|
|
407
566
|
// ---------------------------------------------------------------------------
|
|
408
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Resolve an i18n value to a locale string using the context locale.
|
|
570
|
+
* Returns the original value unchanged if it's not an i18n object or no locale is set.
|
|
571
|
+
*/
|
|
572
|
+
function resolveI18n(value: unknown, ctx: AstroEmitContext): unknown {
|
|
573
|
+
if (ctx.locale && isI18nValue(value)) {
|
|
574
|
+
return resolveI18nValue(value, ctx.locale, DEFAULT_I18N_CONFIG);
|
|
575
|
+
}
|
|
576
|
+
return value;
|
|
577
|
+
}
|
|
578
|
+
|
|
409
579
|
/**
|
|
410
580
|
* Convert a ComponentNode tree to Astro template markup
|
|
411
581
|
*/
|
|
@@ -415,24 +585,54 @@ export function nodeToAstro(
|
|
|
415
585
|
): string {
|
|
416
586
|
if (node === null || node === undefined) return '';
|
|
417
587
|
|
|
588
|
+
// Resolve i18n objects to locale strings before further processing
|
|
589
|
+
if (typeof node === 'object' && !Array.isArray(node) && isI18nValue(node)) {
|
|
590
|
+
const resolved = resolveI18n(node, ctx);
|
|
591
|
+
if (typeof resolved === 'string') {
|
|
592
|
+
return `${ind(ctx)}${escapeJSX(resolved)}\n`;
|
|
593
|
+
}
|
|
594
|
+
// In component def without locale: wrap with r() resolver
|
|
595
|
+
if (ctx.isComponentDef && isI18nValue(resolved)) {
|
|
596
|
+
ctx.needsI18nResolver = true;
|
|
597
|
+
return `${ind(ctx)}{r(${JSON.stringify(resolved)})}\n`;
|
|
598
|
+
}
|
|
599
|
+
// If resolution returned a non-string (e.g. array), stringify it
|
|
600
|
+
return `${ind(ctx)}${String(resolved ?? '')}\n`;
|
|
601
|
+
}
|
|
602
|
+
|
|
418
603
|
// Text/number
|
|
419
604
|
if (typeof node === 'string') {
|
|
605
|
+
// CMS mode: transform {{cms.field}} expressions to entry data access
|
|
606
|
+
if (ctx.cmsMode && isTemplateExpression(node) && /\{\{cms\./.test(node)) {
|
|
607
|
+
const transformed = transformCMSTemplate(node, ctx.cmsEntryBinding || 'entry', ctx.cmsRichTextFields, ctx.cmsWrapFn);
|
|
608
|
+
return `${ind(ctx)}${transformed}\n`;
|
|
609
|
+
}
|
|
610
|
+
// List item binding: transform {{item.field}} expressions
|
|
611
|
+
if (ctx.listItemBinding && isTemplateExpression(node)) {
|
|
612
|
+
const transformed = transformItemTemplate(node, ctx.listItemBinding, ctx.listIndexVar, ctx.listSourceVar);
|
|
613
|
+
return `${ind(ctx)}${transformed}\n`;
|
|
614
|
+
}
|
|
420
615
|
if (hasTemplates(node) && ctx.isComponentDef) {
|
|
421
616
|
return `${ind(ctx)}${resolveTemplate(node, ctx)}\n`;
|
|
422
617
|
}
|
|
618
|
+
// Raw HTML marker (from rich-text fields) - render unescaped via set:html
|
|
619
|
+
if (node.startsWith(RAW_HTML_PREFIX)) {
|
|
620
|
+
const rawHtml = node.slice(RAW_HTML_PREFIX.length);
|
|
621
|
+
return `${ind(ctx)}<Fragment set:html={\`${escapeTemplateLiteral(rawHtml)}\`} />\n`;
|
|
622
|
+
}
|
|
423
623
|
return `${ind(ctx)}${escapeJSX(node)}\n`;
|
|
424
624
|
}
|
|
425
625
|
if (typeof node === 'number') {
|
|
426
626
|
return `${ind(ctx)}${node}\n`;
|
|
427
627
|
}
|
|
428
628
|
|
|
429
|
-
// Array of nodes
|
|
629
|
+
// Array of nodes – replace the last path segment (matches SSR renderer's top-level array convention)
|
|
430
630
|
if (Array.isArray(node)) {
|
|
431
631
|
let result = '';
|
|
432
632
|
for (let i = 0; i < node.length; i++) {
|
|
433
633
|
const child = node[i];
|
|
434
634
|
const savedPath = [...ctx.elementPath];
|
|
435
|
-
ctx.elementPath = [...ctx.elementPath, i];
|
|
635
|
+
ctx.elementPath = [...ctx.elementPath.slice(0, -1), i];
|
|
436
636
|
result += nodeToAstro(child, ctx);
|
|
437
637
|
ctx.elementPath = savedPath;
|
|
438
638
|
}
|
|
@@ -452,10 +652,12 @@ export function nodeToAstro(
|
|
|
452
652
|
case NODE_TYPE.LINK:
|
|
453
653
|
return emitLinkNode(node as LinkNode, ctx);
|
|
454
654
|
case NODE_TYPE.LOCALE_LIST:
|
|
455
|
-
return
|
|
655
|
+
return emitLocaleListNode(node as LocaleListNode, ctx);
|
|
456
656
|
case NODE_TYPE.LIST:
|
|
457
657
|
case 'cms-list' as any:
|
|
458
|
-
return
|
|
658
|
+
return emitListNode(node as ListNode, ctx);
|
|
659
|
+
case 'image' as any:
|
|
660
|
+
return emitImageTypeNode(node, ctx);
|
|
459
661
|
default:
|
|
460
662
|
return emitFallback(ctx);
|
|
461
663
|
}
|
|
@@ -465,8 +667,167 @@ export function nodeToAstro(
|
|
|
465
667
|
// Node type emitters
|
|
466
668
|
// ---------------------------------------------------------------------------
|
|
467
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Tailwind class prefixes that belong on the <img> element (not the <picture> wrapper).
|
|
672
|
+
* These correspond to image-specific visual properties.
|
|
673
|
+
*/
|
|
674
|
+
const IMG_TAILWIND_PREFIXES = ['object-', 'rounded', 'border', 'shadow', '[filter', '[transform', 'mix-blend'];
|
|
675
|
+
const IMG_OPACITY_PATTERN = /^opacity-/;
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Default sizes attribute for responsive images
|
|
679
|
+
*/
|
|
680
|
+
const DEFAULT_SIZES = '100vw';
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Split Tailwind classes into picture (layout) and img (visual) groups.
|
|
684
|
+
* Layout classes go on <picture>, image-specific classes go on <img>.
|
|
685
|
+
*/
|
|
686
|
+
function splitImageClasses(allClasses: string[]): { pictureClasses: string[]; imgClasses: string[] } {
|
|
687
|
+
const imgClasses: string[] = [];
|
|
688
|
+
const pictureClasses: string[] = [];
|
|
689
|
+
|
|
690
|
+
for (const cls of allClasses) {
|
|
691
|
+
// Strip responsive prefix (e.g., "md:object-cover" -> "object-cover") for matching
|
|
692
|
+
const baseCls = cls.includes(':') ? cls.split(':').pop()! : cls;
|
|
693
|
+
if (IMG_TAILWIND_PREFIXES.some(p => baseCls.startsWith(p)) || IMG_OPACITY_PATTERN.test(baseCls)) {
|
|
694
|
+
imgClasses.push(cls);
|
|
695
|
+
} else {
|
|
696
|
+
pictureClasses.push(cls);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return { pictureClasses, imgClasses };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Emit an <img> node as a <picture> element with AVIF/WebP sources when
|
|
705
|
+
* image metadata is available, or as a plain <img> with srcset otherwise.
|
|
706
|
+
*/
|
|
707
|
+
function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
708
|
+
const style = node.style as StyleObject | ResponsiveStyleObject | undefined;
|
|
709
|
+
|
|
710
|
+
// Element class for interactive styles
|
|
711
|
+
let elementClass: string | null = null;
|
|
712
|
+
if (
|
|
713
|
+
(node.interactiveStyles && node.interactiveStyles.length > 0) ||
|
|
714
|
+
node.generateElementClass
|
|
715
|
+
) {
|
|
716
|
+
elementClass = buildElementClass(ctx, node.label);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const { classExpr, styleAttr } = buildClassAndStyleExpression(
|
|
720
|
+
style,
|
|
721
|
+
node.interactiveStyles as InteractiveStyles | undefined,
|
|
722
|
+
elementClass,
|
|
723
|
+
ctx
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
// Extract image-specific attributes
|
|
727
|
+
const attrs = node.attributes || {};
|
|
728
|
+
const src = attrs.src as string | undefined;
|
|
729
|
+
const alt = attrs.alt as string | undefined;
|
|
730
|
+
const loading = attrs.loading as string | undefined;
|
|
731
|
+
const fetchpriority = attrs.fetchpriority as string | undefined;
|
|
732
|
+
let width = attrs.width as string | number | undefined;
|
|
733
|
+
let height = attrs.height as string | number | undefined;
|
|
734
|
+
const sizes = attrs.sizes as string | undefined;
|
|
735
|
+
|
|
736
|
+
// Look up image metadata
|
|
737
|
+
const metadata = src ? ctx.imageMetadataMap?.get(String(src)) : undefined;
|
|
738
|
+
|
|
739
|
+
// Use dimensions from metadata if not explicitly set
|
|
740
|
+
if (metadata) {
|
|
741
|
+
if (width === undefined && metadata.width) width = metadata.width;
|
|
742
|
+
if (height === undefined && metadata.height) height = metadata.height;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const sizesValue = sizes || DEFAULT_SIZES;
|
|
746
|
+
|
|
747
|
+
// Build remaining attributes (exclude image-specific ones we handle manually)
|
|
748
|
+
const imageSpecificKeys = new Set(['src', 'alt', 'loading', 'width', 'height', 'sizes', 'srcset', 'fetchpriority']);
|
|
749
|
+
const otherAttrs: Record<string, string | number | boolean> = {};
|
|
750
|
+
if (node.attributes) {
|
|
751
|
+
for (const [k, v] of Object.entries(node.attributes)) {
|
|
752
|
+
if (!imageSpecificKeys.has(k)) otherAttrs[k] = v;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const otherAttrsStr = buildAttributesString(otherAttrs, ctx);
|
|
756
|
+
|
|
757
|
+
// Build core img attributes (with CMS/list template support)
|
|
758
|
+
let imgAttrs = '';
|
|
759
|
+
if (src) imgAttrs += ` ${emitAttrValue('src', String(src), ctx)}`;
|
|
760
|
+
if (alt !== undefined) imgAttrs += ` ${emitAttrValue('alt', String(alt), ctx)}`;
|
|
761
|
+
if (fetchpriority) imgAttrs += ` fetchpriority="${escapeJSX(String(fetchpriority))}"`;
|
|
762
|
+
if (loading) imgAttrs += ` loading="${escapeJSX(String(loading))}"`;
|
|
763
|
+
if (width !== undefined) imgAttrs += ` width="${escapeJSX(String(width))}"`;
|
|
764
|
+
if (height !== undefined) imgAttrs += ` height="${escapeJSX(String(height))}"`;
|
|
765
|
+
|
|
766
|
+
// Blur placeholder style
|
|
767
|
+
let blurStyle = '';
|
|
768
|
+
if (metadata?.blurHash) {
|
|
769
|
+
blurStyle = ` style="background-image: url(${escapeJSX(metadata.blurHash)}); background-size: cover;" onload="this.style.backgroundImage=''"`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Conditional rendering
|
|
773
|
+
const ifExpr = emitIfOpen(node, ctx);
|
|
774
|
+
const ifClose = emitIfClose(node, ctx);
|
|
775
|
+
|
|
776
|
+
// If we have AVIF srcset, render as <picture> with split classes
|
|
777
|
+
if (metadata?.avifSrcset) {
|
|
778
|
+
// Extract static classes from classExpr for splitting
|
|
779
|
+
const classMatch = classExpr.match(/class="([^"]*)"/);
|
|
780
|
+
const classListMatch = classExpr.match(/class:list={\[(.+)\]}/);
|
|
781
|
+
|
|
782
|
+
if (classListMatch) {
|
|
783
|
+
// Dynamic class:list - put it all on the picture, img gets none
|
|
784
|
+
// (splitting dynamic classes is too complex; layout on picture is the safer default)
|
|
785
|
+
return (
|
|
786
|
+
`${ifExpr}${ind(ctx)}<picture${classExpr}${styleAttr}>\n` +
|
|
787
|
+
`${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata.avifSrcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
788
|
+
`${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata.srcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
789
|
+
`${ind(ctx)} <img${imgAttrs}${blurStyle}${otherAttrsStr} />\n` +
|
|
790
|
+
`${ind(ctx)}</picture>\n${ifClose}`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
|
|
795
|
+
const { pictureClasses, imgClasses } = splitImageClasses(allClasses);
|
|
796
|
+
|
|
797
|
+
const pictureClassAttr = pictureClasses.length > 0 ? ` class="${pictureClasses.join(' ')}"` : '';
|
|
798
|
+
const imgClassAttr = imgClasses.length > 0 ? ` class="${imgClasses.join(' ')}"` : '';
|
|
799
|
+
|
|
800
|
+
return (
|
|
801
|
+
`${ifExpr}${ind(ctx)}<picture${pictureClassAttr}${styleAttr}>\n` +
|
|
802
|
+
`${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata.avifSrcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
803
|
+
`${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata.srcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
804
|
+
`${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurStyle}${otherAttrsStr} />\n` +
|
|
805
|
+
`${ind(ctx)}</picture>\n${ifClose}`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Fallback: regular img with WebP srcset if available
|
|
810
|
+
if (metadata?.srcset) {
|
|
811
|
+
imgAttrs += ` srcset="${escapeJSX(metadata.srcset)}"`;
|
|
812
|
+
imgAttrs += ` sizes="${escapeJSX(sizesValue)}"`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return `${ifExpr}${ind(ctx)}<img${classExpr}${styleAttr}${imgAttrs}${blurStyle}${otherAttrsStr} />\n${ifClose}`;
|
|
816
|
+
}
|
|
817
|
+
|
|
468
818
|
function emitHtmlNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
469
819
|
let tag = node.tag;
|
|
820
|
+
|
|
821
|
+
// Astro treats capitalized tags as component imports — lowercase custom tags to avoid this
|
|
822
|
+
if (tag && /^[A-Z]/.test(tag)) {
|
|
823
|
+
tag = tag.toLowerCase();
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Delegate img tags to the image-specific emitter when metadata is available
|
|
827
|
+
if (tag === 'img' && ctx.imageMetadataMap) {
|
|
828
|
+
return emitImageNode(node, ctx);
|
|
829
|
+
}
|
|
830
|
+
|
|
470
831
|
const label = node.label;
|
|
471
832
|
const style = node.style as StyleObject | ResponsiveStyleObject | undefined;
|
|
472
833
|
|
|
@@ -528,17 +889,45 @@ function emitComponentInstance(node: ComponentInstanceNode, ctx: AstroEmitContex
|
|
|
528
889
|
// Build prop expressions
|
|
529
890
|
const propParts: string[] = [];
|
|
530
891
|
if (node.props) {
|
|
531
|
-
for (const [key,
|
|
892
|
+
for (const [key, rawValue] of Object.entries(node.props)) {
|
|
532
893
|
if (key === 'children') continue;
|
|
894
|
+
|
|
895
|
+
// Resolve i18n values to the page locale
|
|
896
|
+
const value = resolveI18n(rawValue, ctx);
|
|
897
|
+
|
|
533
898
|
// Resolve template expressions in string props when inside component def
|
|
534
899
|
if (typeof value === 'string' && hasTemplates(value) && ctx.isComponentDef) {
|
|
535
900
|
const fullMatch = value.match(/^\{\{(.+)\}\}$/);
|
|
536
901
|
if (fullMatch) {
|
|
537
|
-
|
|
902
|
+
let expr = fullMatch[1].trim();
|
|
903
|
+
if (ctx.listItemBinding) expr = rewriteItemVar(expr, ctx.listItemBinding);
|
|
904
|
+
if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
|
|
905
|
+
propParts.push(`${key}={${expr}}`);
|
|
538
906
|
} else {
|
|
539
|
-
const resolved = value.replace(/\{\{(.+?)\}\}/g, (_, expr) =>
|
|
907
|
+
const resolved = value.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
908
|
+
let trimmed = expr.trim();
|
|
909
|
+
if (ctx.listItemBinding) trimmed = rewriteItemVar(trimmed, ctx.listItemBinding);
|
|
910
|
+
if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
|
|
911
|
+
return `\${${trimmed}}`;
|
|
912
|
+
});
|
|
540
913
|
propParts.push(`${key}={\`${resolved}\`}`);
|
|
541
914
|
}
|
|
915
|
+
} else if (typeof value === 'string' && ctx.cmsMode && /\{\{cms\./.test(value)) {
|
|
916
|
+
const b = ctx.cmsEntryBinding || 'entry';
|
|
917
|
+
const w = (expr: string) => ctx.cmsWrapFn ? `${ctx.cmsWrapFn}(${expr})` : expr;
|
|
918
|
+
const fullMatch = value.match(/^\{\{cms\.([^}]+)\}\}$/);
|
|
919
|
+
if (fullMatch) {
|
|
920
|
+
propParts.push(`${key}={${w(`${b}.data.${fullMatch[1].trim()}`)}}`);
|
|
921
|
+
} else {
|
|
922
|
+
const replaced = value.replace(/\{\{cms\.([^}]+)\}\}/g, (_, fieldPath) => {
|
|
923
|
+
return `\${${w(`${b}.data.${fieldPath.trim()}`)}}`;
|
|
924
|
+
});
|
|
925
|
+
propParts.push(`${key}={\`${replaced}\`}`);
|
|
926
|
+
}
|
|
927
|
+
} else if (ctx.isComponentDef && isI18nValue(value)) {
|
|
928
|
+
// i18n object in component def — wrap with r() resolver (resolved at runtime via Astro.currentLocale)
|
|
929
|
+
ctx.needsI18nResolver = true;
|
|
930
|
+
propParts.push(`${key}={r(${JSON.stringify(value)})}`);
|
|
542
931
|
} else {
|
|
543
932
|
propParts.push(`${key}=${formatPropValue(value)}`);
|
|
544
933
|
}
|
|
@@ -652,28 +1041,59 @@ function emitLinkNode(node: LinkNode, ctx: AstroEmitContext): string {
|
|
|
652
1041
|
}
|
|
653
1042
|
}
|
|
654
1043
|
|
|
1044
|
+
// Resolve i18n on href before processing
|
|
1045
|
+
const resolvedHref = resolveI18n(node.href, ctx);
|
|
1046
|
+
const nodeHref = resolvedHref as typeof node.href;
|
|
1047
|
+
|
|
655
1048
|
// Handle href
|
|
656
1049
|
let hrefAttr: string;
|
|
657
|
-
if (isLinkMapping(
|
|
1050
|
+
if (isLinkMapping(nodeHref)) {
|
|
658
1051
|
if (ctx.isComponentDef) {
|
|
659
|
-
const propRef =
|
|
1052
|
+
const propRef = (nodeHref as LinkMapping).prop;
|
|
660
1053
|
// Link props are objects with {href, target?}
|
|
661
|
-
hrefAttr = ` href={${propRef}
|
|
1054
|
+
hrefAttr = ` href={${propRef}?.href ?? "#"}`;
|
|
662
1055
|
} else {
|
|
663
1056
|
hrefAttr = ' href="#"';
|
|
664
1057
|
}
|
|
665
1058
|
} else {
|
|
666
|
-
const href = typeof
|
|
1059
|
+
const href = typeof nodeHref === 'string' ? nodeHref : '#';
|
|
667
1060
|
if (hasTemplates(href) && ctx.isComponentDef) {
|
|
668
1061
|
const fullMatch = href.match(/^\{\{(.+)\}\}$/);
|
|
669
1062
|
if (fullMatch) {
|
|
670
|
-
|
|
1063
|
+
let expr = fullMatch[1].trim();
|
|
1064
|
+
if (ctx.listItemBinding) expr = rewriteItemVar(expr, ctx.listItemBinding);
|
|
1065
|
+
if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
|
|
1066
|
+
const propDef = ctx.componentProps[expr];
|
|
1067
|
+
if (propDef && propDef.type === 'link') {
|
|
1068
|
+
hrefAttr = ` href={${expr}?.href ?? "#"}`;
|
|
1069
|
+
} else {
|
|
1070
|
+
hrefAttr = ` href={${expr}}`;
|
|
1071
|
+
}
|
|
671
1072
|
} else {
|
|
672
|
-
const resolved = href.replace(/\{\{(.+?)\}\}/g, (_, expr) =>
|
|
1073
|
+
const resolved = href.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
1074
|
+
let trimmed = expr.trim();
|
|
1075
|
+
if (ctx.listItemBinding) trimmed = rewriteItemVar(trimmed, ctx.listItemBinding);
|
|
1076
|
+
if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
|
|
1077
|
+
const pd = ctx.componentProps[trimmed];
|
|
1078
|
+
return pd?.type === 'link' ? `\${${trimmed}?.href ?? "#"}` : `\${${trimmed}}`;
|
|
1079
|
+
});
|
|
673
1080
|
hrefAttr = ` href={\`${resolved}\`}`;
|
|
674
1081
|
}
|
|
1082
|
+
} else if (ctx.cmsMode && /\{\{cms\./.test(href)) {
|
|
1083
|
+
const b = ctx.cmsEntryBinding || 'entry';
|
|
1084
|
+
const w = (expr: string) => ctx.cmsWrapFn ? `${ctx.cmsWrapFn}(${expr})` : expr;
|
|
1085
|
+
const fullMatch = href.match(/^\{\{cms\.([^}]+)\}\}$/);
|
|
1086
|
+
if (fullMatch) {
|
|
1087
|
+
hrefAttr = ` href={${w(`${b}.data.${fullMatch[1].trim()}`)}}`;
|
|
1088
|
+
} else {
|
|
1089
|
+
const replaced = href.replace(/\{\{cms\.([^}]+)\}\}/g, (_, fieldPath) => {
|
|
1090
|
+
return `\${${w(`${b}.data.${fieldPath.trim()}`)}}`;
|
|
1091
|
+
});
|
|
1092
|
+
hrefAttr = ` href={\`${replaced}\`}`;
|
|
1093
|
+
}
|
|
675
1094
|
} else {
|
|
676
|
-
|
|
1095
|
+
const localizedHref = localizeHref(href, ctx);
|
|
1096
|
+
hrefAttr = ` href="${escapeJSX(localizedHref)}"`;
|
|
677
1097
|
}
|
|
678
1098
|
}
|
|
679
1099
|
|
|
@@ -691,6 +1111,329 @@ function emitLinkNode(node: LinkNode, ctx: AstroEmitContext): string {
|
|
|
691
1111
|
);
|
|
692
1112
|
}
|
|
693
1113
|
|
|
1114
|
+
/**
|
|
1115
|
+
* Emit an "image" type node (standalone image, not an HTML img tag)
|
|
1116
|
+
* These have src, alt, style directly on the node.
|
|
1117
|
+
*/
|
|
1118
|
+
function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
|
|
1119
|
+
const style = node.style as StyleObject | ResponsiveStyleObject | undefined;
|
|
1120
|
+
|
|
1121
|
+
let elementClass: string | null = null;
|
|
1122
|
+
if (
|
|
1123
|
+
(node.interactiveStyles && node.interactiveStyles.length > 0) ||
|
|
1124
|
+
node.generateElementClass
|
|
1125
|
+
) {
|
|
1126
|
+
elementClass = buildElementClass(ctx, node.label);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const { classExpr, styleAttr } = buildClassAndStyleExpression(
|
|
1130
|
+
style,
|
|
1131
|
+
node.interactiveStyles as InteractiveStyles | undefined,
|
|
1132
|
+
elementClass,
|
|
1133
|
+
ctx
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
const src = node.src as string | undefined;
|
|
1137
|
+
const alt = node.alt as string | undefined;
|
|
1138
|
+
|
|
1139
|
+
let imgAttrs = '';
|
|
1140
|
+
if (src) imgAttrs += ` src="${escapeJSX(String(src))}"`;
|
|
1141
|
+
if (alt !== undefined) imgAttrs += ` alt="${escapeJSX(String(alt))}"`;
|
|
1142
|
+
|
|
1143
|
+
// Check for image metadata for responsive images
|
|
1144
|
+
const metadata = src ? ctx.imageMetadataMap?.get(String(src)) : undefined;
|
|
1145
|
+
|
|
1146
|
+
if (metadata) {
|
|
1147
|
+
let width = metadata.width;
|
|
1148
|
+
let height = metadata.height;
|
|
1149
|
+
if (width !== undefined) imgAttrs += ` width="${width}"`;
|
|
1150
|
+
if (height !== undefined) imgAttrs += ` height="${height}"`;
|
|
1151
|
+
|
|
1152
|
+
let blurStyle = '';
|
|
1153
|
+
if (metadata.blurHash) {
|
|
1154
|
+
blurStyle = ` style="background-image: url(${escapeJSX(metadata.blurHash)}); background-size: cover;" onload="this.style.backgroundImage=''"`;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const sizesValue = DEFAULT_SIZES;
|
|
1158
|
+
|
|
1159
|
+
if (metadata.avifSrcset) {
|
|
1160
|
+
const classMatch = classExpr.match(/class="([^"]*)"/);
|
|
1161
|
+
const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
|
|
1162
|
+
const { pictureClasses, imgClasses } = splitImageClasses(allClasses);
|
|
1163
|
+
|
|
1164
|
+
const pictureClassAttr = pictureClasses.length > 0 ? ` class="${pictureClasses.join(' ')}"` : '';
|
|
1165
|
+
const imgClassAttr = imgClasses.length > 0 ? ` class="${imgClasses.join(' ')}"` : '';
|
|
1166
|
+
|
|
1167
|
+
return (
|
|
1168
|
+
`${ind(ctx)}<picture${pictureClassAttr}${styleAttr}>\n` +
|
|
1169
|
+
`${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata.avifSrcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
1170
|
+
`${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata.srcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
|
|
1171
|
+
`${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurStyle} />\n` +
|
|
1172
|
+
`${ind(ctx)}</picture>\n`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (metadata.srcset) {
|
|
1177
|
+
imgAttrs += ` srcset="${escapeJSX(metadata.srcset)}"`;
|
|
1178
|
+
imgAttrs += ` sizes="${escapeJSX(sizesValue)}"`;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return `${ind(ctx)}<img${classExpr}${styleAttr}${imgAttrs} />\n`;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Emit a list node as native Astro .map() call
|
|
1187
|
+
*/
|
|
1188
|
+
function emitListNode(node: ListNode, ctx: AstroEmitContext): string {
|
|
1189
|
+
const sourceType = node.sourceType || 'prop';
|
|
1190
|
+
const itemAs = node.itemAs || 'item';
|
|
1191
|
+
|
|
1192
|
+
if (sourceType === 'collection') {
|
|
1193
|
+
return emitCollectionListNode(node, ctx);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Prop lists only work inside component definitions where the prop is available
|
|
1197
|
+
// On pages, the data source doesn't exist at template level - use SSR fallback
|
|
1198
|
+
if (!ctx.isComponentDef && !ctx.listItemBinding) {
|
|
1199
|
+
return emitFallback(ctx);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Prop list: emit {items.map((item, index) => (...))}
|
|
1203
|
+
// Resolve source: "{{features}}" → "features"
|
|
1204
|
+
let source = node.source || 'items';
|
|
1205
|
+
const templateMatch = source.match(/^\{\{(.+)\}\}$/);
|
|
1206
|
+
if (templateMatch) {
|
|
1207
|
+
source = templateMatch[1].trim();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Build the .map() source expression (compute before children so ctx has it)
|
|
1211
|
+
let mapSource = source;
|
|
1212
|
+
if (node.offset && node.limit) {
|
|
1213
|
+
mapSource = `${source}.slice(${node.offset}, ${node.offset + node.limit})`;
|
|
1214
|
+
} else if (node.offset) {
|
|
1215
|
+
mapSource = `${source}.slice(${node.offset})`;
|
|
1216
|
+
} else if (node.limit) {
|
|
1217
|
+
mapSource = `${source}.slice(0, ${node.limit})`;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Build children with list item binding context
|
|
1221
|
+
const indexVar = `${itemAs}Index`;
|
|
1222
|
+
const innerCtx: AstroEmitContext = {
|
|
1223
|
+
...ctx,
|
|
1224
|
+
indent: ctx.indent + 1,
|
|
1225
|
+
listItemBinding: itemAs,
|
|
1226
|
+
listIndexVar: indexVar,
|
|
1227
|
+
listSourceVar: mapSource,
|
|
1228
|
+
elementPath: [...ctx.elementPath, 0],
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
const children = node.children
|
|
1232
|
+
? (node.children as any[]).map((child, i) => {
|
|
1233
|
+
const childCtx = { ...innerCtx, elementPath: [...ctx.elementPath, i] };
|
|
1234
|
+
const out = nodeToAstro(child, childCtx);
|
|
1235
|
+
if (childCtx.needsI18nResolver) ctx.needsI18nResolver = true;
|
|
1236
|
+
return out;
|
|
1237
|
+
}).join('')
|
|
1238
|
+
: '';
|
|
1239
|
+
|
|
1240
|
+
return (
|
|
1241
|
+
`${ind(ctx)}{${mapSource}.map((${itemAs}, ${indexVar}) => (\n` +
|
|
1242
|
+
children +
|
|
1243
|
+
`${ind(ctx)}))}\n`
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Emit a collection-sourced list node using getCollection() in frontmatter
|
|
1249
|
+
*/
|
|
1250
|
+
function emitCollectionListNode(node: ListNode, ctx: AstroEmitContext): string {
|
|
1251
|
+
// For collection lists, the data fetching happens in frontmatter
|
|
1252
|
+
const source = node.source || '';
|
|
1253
|
+
const itemAs = node.itemAs || singularize(source);
|
|
1254
|
+
|
|
1255
|
+
// Add frontmatter lines and imports
|
|
1256
|
+
if (!ctx.frontmatterLines) ctx.frontmatterLines = [];
|
|
1257
|
+
if (!ctx.astroImports) ctx.astroImports = new Set();
|
|
1258
|
+
|
|
1259
|
+
ctx.astroImports.add('getCollection');
|
|
1260
|
+
|
|
1261
|
+
// Variable name for the collection data
|
|
1262
|
+
const collectionVar = `${source}List`;
|
|
1263
|
+
|
|
1264
|
+
// Build the collection query with filter/sort/limit
|
|
1265
|
+
let queryChain = `await getCollection('${source}')`;
|
|
1266
|
+
|
|
1267
|
+
if (node.filter) {
|
|
1268
|
+
// Simple filter support
|
|
1269
|
+
if (typeof node.filter === 'object' && !Array.isArray(node.filter) && 'field' in node.filter) {
|
|
1270
|
+
const f = node.filter as { field: string; operator?: string; value: unknown };
|
|
1271
|
+
const op = f.operator || 'eq';
|
|
1272
|
+
if (op === 'eq') {
|
|
1273
|
+
queryChain += `.then(items => items.filter(e => e.data.${f.field} === ${JSON.stringify(f.value)}))`;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (node.sort) {
|
|
1279
|
+
const sortConfig = Array.isArray(node.sort) ? node.sort[0] : node.sort;
|
|
1280
|
+
if (sortConfig) {
|
|
1281
|
+
const order = sortConfig.order === 'desc' ? -1 : 1;
|
|
1282
|
+
queryChain += `.then(items => items.sort((a, b) => a.data.${sortConfig.field} > b.data.${sortConfig.field} ? ${order} : ${-order}))`;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (node.offset || node.limit) {
|
|
1287
|
+
const start = node.offset || 0;
|
|
1288
|
+
const end = node.limit ? start + node.limit : undefined;
|
|
1289
|
+
queryChain += `.then(items => items.slice(${start}${end !== undefined ? `, ${end}` : ''}))`;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
ctx.frontmatterLines.push(`const ${collectionVar} = ${queryChain};`);
|
|
1293
|
+
|
|
1294
|
+
// Build children with item binding
|
|
1295
|
+
const indexVar = `${itemAs}Index`;
|
|
1296
|
+
const innerCtx: AstroEmitContext = {
|
|
1297
|
+
...ctx,
|
|
1298
|
+
indent: ctx.indent + 1,
|
|
1299
|
+
listItemBinding: itemAs,
|
|
1300
|
+
listIndexVar: indexVar,
|
|
1301
|
+
listSourceVar: collectionVar,
|
|
1302
|
+
cmsMode: true,
|
|
1303
|
+
cmsEntryBinding: itemAs,
|
|
1304
|
+
elementPath: [...ctx.elementPath, 0],
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
const children = node.children
|
|
1308
|
+
? (node.children as any[]).map((child, i) => {
|
|
1309
|
+
const childCtx = { ...innerCtx, elementPath: [...ctx.elementPath, i] };
|
|
1310
|
+
const out = nodeToAstro(child, childCtx);
|
|
1311
|
+
if (childCtx.needsI18nResolver) ctx.needsI18nResolver = true;
|
|
1312
|
+
return out;
|
|
1313
|
+
}).join('')
|
|
1314
|
+
: '';
|
|
1315
|
+
|
|
1316
|
+
return (
|
|
1317
|
+
`${ind(ctx)}{${collectionVar}.map((${itemAs}, ${indexVar}) => (\n` +
|
|
1318
|
+
children +
|
|
1319
|
+
`${ind(ctx)}))}\n`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Emit a locale list node with static locale links
|
|
1325
|
+
* Since locales are known at build time, we emit static HTML
|
|
1326
|
+
*/
|
|
1327
|
+
function emitLocaleListNode(node: LocaleListNode, ctx: AstroEmitContext): string {
|
|
1328
|
+
// If we don't have i18n config or slug map, fall back to SSR
|
|
1329
|
+
if (!ctx.i18nConfig || !ctx.currentPageSlugMap) {
|
|
1330
|
+
return emitFallback(ctx);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const i18nConfig = ctx.i18nConfig;
|
|
1334
|
+
const slugMap = ctx.currentPageSlugMap;
|
|
1335
|
+
const showCurrent = node.showCurrent !== false;
|
|
1336
|
+
const showSeparator = node.showSeparator !== false;
|
|
1337
|
+
const showFlag = node.showFlag !== false;
|
|
1338
|
+
const displayType = node.displayType || 'nativeName';
|
|
1339
|
+
|
|
1340
|
+
// Build container classes from style
|
|
1341
|
+
const style = node.style as StyleObject | ResponsiveStyleObject | undefined;
|
|
1342
|
+
|
|
1343
|
+
let elementClass: string | null = null;
|
|
1344
|
+
if ((node.interactiveStyles && (node.interactiveStyles as any[]).length > 0) || node.generateElementClass) {
|
|
1345
|
+
elementClass = buildElementClass(ctx, node.label);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const { classExpr: containerClassExpr, styleAttr: containerStyleAttr } = buildClassAndStyleExpression(
|
|
1349
|
+
style,
|
|
1350
|
+
node.interactiveStyles as InteractiveStyles | undefined,
|
|
1351
|
+
elementClass,
|
|
1352
|
+
ctx
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
// Build item classes
|
|
1356
|
+
const itemStyle = node.itemStyle as StyleObject | ResponsiveStyleObject | undefined;
|
|
1357
|
+
const itemResult = itemStyle ? responsiveStylesToTailwind(itemStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
|
|
1358
|
+
const itemClasses = itemResult.classes;
|
|
1359
|
+
|
|
1360
|
+
// Build active item classes (item + active combined)
|
|
1361
|
+
const activeItemStyle = node.activeItemStyle as StyleObject | ResponsiveStyleObject | undefined;
|
|
1362
|
+
const activeResult = activeItemStyle ? responsiveStylesToTailwind(activeItemStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
|
|
1363
|
+
const activeItemClasses = [...itemClasses, ...activeResult.classes];
|
|
1364
|
+
|
|
1365
|
+
// Build separator classes
|
|
1366
|
+
const separatorStyle = node.separatorStyle as StyleObject | ResponsiveStyleObject | undefined;
|
|
1367
|
+
const sepResult = separatorStyle ? responsiveStylesToTailwind(separatorStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
|
|
1368
|
+
const separatorClasses = sepResult.classes;
|
|
1369
|
+
|
|
1370
|
+
// Build locale icon map
|
|
1371
|
+
const localeIconMap = new Map<string, string>();
|
|
1372
|
+
for (const localeConfig of i18nConfig.locales) {
|
|
1373
|
+
if (localeConfig.icon) {
|
|
1374
|
+
localeIconMap.set(localeConfig.code, localeConfig.icon);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Flag classes
|
|
1379
|
+
const flagStyle = (node as any).flagStyle as StyleObject | ResponsiveStyleObject | undefined;
|
|
1380
|
+
const flagResult = flagStyle ? responsiveStylesToTailwind(flagStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
|
|
1381
|
+
const flagClasses = flagResult.classes;
|
|
1382
|
+
|
|
1383
|
+
// Build links
|
|
1384
|
+
const links: string[] = [];
|
|
1385
|
+
const currentLocale = ctx.locale || i18nConfig.defaultLocale;
|
|
1386
|
+
|
|
1387
|
+
for (const localeConfig of i18nConfig.locales) {
|
|
1388
|
+
const code = localeConfig.code;
|
|
1389
|
+
const isCurrent = code === currentLocale;
|
|
1390
|
+
|
|
1391
|
+
if (!showCurrent && isCurrent) continue;
|
|
1392
|
+
|
|
1393
|
+
const path = slugMap[code] || '/';
|
|
1394
|
+
const classes = isCurrent ? activeItemClasses : itemClasses;
|
|
1395
|
+
const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : '';
|
|
1396
|
+
const currentAttr = isCurrent ? ' data-current="true"' : ' data-current="false"';
|
|
1397
|
+
const hreflangAttr = ` hreflang="${localeConfig.langTag}"`;
|
|
1398
|
+
|
|
1399
|
+
// Display text
|
|
1400
|
+
let displayText: string;
|
|
1401
|
+
switch (displayType) {
|
|
1402
|
+
case 'code': displayText = code.toUpperCase(); break;
|
|
1403
|
+
case 'name': displayText = localeConfig.name; break;
|
|
1404
|
+
case 'nativeName': default: displayText = localeConfig.nativeName; break;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Build link content
|
|
1408
|
+
let linkContent = '';
|
|
1409
|
+
const localeIcon = localeIconMap.get(code);
|
|
1410
|
+
if (showFlag && localeIcon) {
|
|
1411
|
+
const flagClassAttr = flagClasses.length > 0 ? ` class="${flagClasses.join(' ')}"` : '';
|
|
1412
|
+
linkContent += `<img src="${escapeJSX(localeIcon)}" alt="${escapeJSX(localeConfig.nativeName)} flag"${flagClassAttr}>`;
|
|
1413
|
+
}
|
|
1414
|
+
linkContent += `<div>${escapeJSX(displayText)}</div>`;
|
|
1415
|
+
|
|
1416
|
+
links.push(`${ind(ctx)} <a href="${escapeJSX(path)}"${hreflangAttr}${currentAttr} data-locale="${escapeJSX(code)}"${classAttr}>${linkContent}</a>`);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Join with separator
|
|
1420
|
+
let linksContent: string;
|
|
1421
|
+
if (showSeparator && links.length > 1) {
|
|
1422
|
+
const sepClassAttr = separatorClasses.length > 0 ? ` class="${separatorClasses.join(' ')}"` : '';
|
|
1423
|
+
linksContent = links.join(`\n${ind(ctx)} <span${sepClassAttr}></span>\n`);
|
|
1424
|
+
} else {
|
|
1425
|
+
linksContent = links.join('\n');
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const attrs = buildAttributesString((node as any).attributes, ctx);
|
|
1429
|
+
|
|
1430
|
+
return (
|
|
1431
|
+
`${ind(ctx)}<div data-locale-list="true"${containerClassExpr}${containerStyleAttr}${attrs}>\n` +
|
|
1432
|
+
linksContent + '\n' +
|
|
1433
|
+
`${ind(ctx)}</div>\n`
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
694
1437
|
/**
|
|
695
1438
|
* Emit SSR fallback for complex nodes (list, locale-list)
|
|
696
1439
|
*/
|
|
@@ -716,15 +1459,48 @@ function emitIfOpen(node: ComponentNode, ctx: AstroEmitContext): string {
|
|
|
716
1459
|
return ifValue ? '' : `${ind(ctx)}{/* hidden */}\n`;
|
|
717
1460
|
}
|
|
718
1461
|
|
|
1462
|
+
// BooleanMapping → generate conditional from values map
|
|
719
1463
|
if (typeof ifValue === 'object' && ifValue._mapping && ctx.isComponentDef) {
|
|
720
|
-
|
|
1464
|
+
const trueValues = Object.entries(ifValue.values)
|
|
1465
|
+
.filter(([, v]) => v === true)
|
|
1466
|
+
.map(([k]) => `'${k}'`);
|
|
1467
|
+
if (trueValues.length === 0) return `${ind(ctx)}{/* hidden */}\n`;
|
|
1468
|
+
if (trueValues.length === 1) {
|
|
1469
|
+
return `${ind(ctx)}{${ifValue.prop} === ${trueValues[0]} && (\n`;
|
|
1470
|
+
}
|
|
1471
|
+
return `${ind(ctx)}{[${trueValues.join(', ')}].includes(${ifValue.prop}) && (\n`;
|
|
721
1472
|
}
|
|
722
1473
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1474
|
+
// String template
|
|
1475
|
+
if (typeof ifValue === 'string') {
|
|
1476
|
+
// CMS mode: {{cms.field}} → entry.data.field
|
|
1477
|
+
if (ctx.cmsMode && ifValue.includes('{{cms.')) {
|
|
1478
|
+
const match = ifValue.match(/^\{\{cms\.([^}]+)\}\}$/);
|
|
1479
|
+
if (match) {
|
|
1480
|
+
const binding = ctx.cmsEntryBinding || 'entry';
|
|
1481
|
+
return `${ind(ctx)}{${binding}.data.${match[1].trim()} && (\n`;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// List item context: {{item.field}} or {{!itemLast}} etc.
|
|
1486
|
+
if (ctx.listItemBinding && /\{\{/.test(ifValue)) {
|
|
1487
|
+
const match = ifValue.match(/^\{\{([^}]+)\}\}$/);
|
|
1488
|
+
if (match) {
|
|
1489
|
+
let expr = match[1].trim();
|
|
1490
|
+
expr = rewriteItemVar(expr, ctx.listItemBinding);
|
|
1491
|
+
if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
|
|
1492
|
+
return `${ind(ctx)}{${expr} && (\n`;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Component prop context
|
|
1497
|
+
if (ctx.isComponentDef) {
|
|
1498
|
+
const fullMatch = ifValue.match(/^\{\{(.+)\}\}$/);
|
|
1499
|
+
let expr = fullMatch ? fullMatch[1].trim() : ifValue.replace(/\{\{(.+?)\}\}/g, '$1');
|
|
1500
|
+
if (ctx.listItemBinding) expr = rewriteItemVar(expr, ctx.listItemBinding);
|
|
1501
|
+
if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
|
|
1502
|
+
return `${ind(ctx)}{${expr} && (\n`;
|
|
1503
|
+
}
|
|
728
1504
|
}
|
|
729
1505
|
|
|
730
1506
|
return '';
|
|
@@ -736,13 +1512,18 @@ function emitIfClose(node: ComponentNode, ctx: AstroEmitContext): string {
|
|
|
736
1512
|
|
|
737
1513
|
if (typeof ifValue === 'boolean') return '';
|
|
738
1514
|
|
|
739
|
-
if (
|
|
740
|
-
(typeof ifValue === 'object' && ifValue._mapping && ctx.isComponentDef) ||
|
|
741
|
-
(typeof ifValue === 'string' && ctx.isComponentDef)
|
|
742
|
-
) {
|
|
1515
|
+
if (typeof ifValue === 'object' && ifValue._mapping && ctx.isComponentDef) {
|
|
743
1516
|
return `${ind(ctx)})}\n`;
|
|
744
1517
|
}
|
|
745
1518
|
|
|
1519
|
+
if (typeof ifValue === 'string') {
|
|
1520
|
+
const hasCmsCondition = ctx.cmsMode && ifValue.includes('{{cms.');
|
|
1521
|
+
const hasItemCondition = ctx.listItemBinding && /\{\{/.test(ifValue);
|
|
1522
|
+
if (hasCmsCondition || hasItemCondition || ctx.isComponentDef) {
|
|
1523
|
+
return `${ind(ctx)})}\n`;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
746
1527
|
return '';
|
|
747
1528
|
}
|
|
748
1529
|
|
|
@@ -759,13 +1540,25 @@ function emitChildren(
|
|
|
759
1540
|
const innerCtx = { ...ctx, indent: ctx.indent + 1, elementPath: [...ctx.elementPath] };
|
|
760
1541
|
|
|
761
1542
|
if (typeof children === 'string') {
|
|
762
|
-
|
|
1543
|
+
const out = nodeToAstro(children, innerCtx);
|
|
1544
|
+
if (innerCtx.needsI18nResolver) ctx.needsI18nResolver = true;
|
|
1545
|
+
return out;
|
|
763
1546
|
}
|
|
764
1547
|
|
|
1548
|
+
// Iterate children arrays inline with appended index (matches SSR renderer's
|
|
1549
|
+
// children iteration which uses [...parentPath, index] for each child)
|
|
765
1550
|
if (Array.isArray(children)) {
|
|
766
|
-
|
|
1551
|
+
let result = '';
|
|
1552
|
+
for (let i = 0; i < children.length; i++) {
|
|
1553
|
+
const childCtx = { ...innerCtx, elementPath: [...ctx.elementPath, i] };
|
|
1554
|
+
result += nodeToAstro(children[i], childCtx);
|
|
1555
|
+
if (childCtx.needsI18nResolver) ctx.needsI18nResolver = true;
|
|
1556
|
+
}
|
|
1557
|
+
return result;
|
|
767
1558
|
}
|
|
768
1559
|
|
|
769
1560
|
// Single node
|
|
770
|
-
|
|
1561
|
+
const out = nodeToAstro(children, innerCtx);
|
|
1562
|
+
if (innerCtx.needsI18nResolver) ctx.needsI18nResolver = true;
|
|
1563
|
+
return out;
|
|
771
1564
|
}
|