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