meno-core 1.0.48 → 1.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-UUA5LEWF.js → chunk-6IVUG7FY.js} +138 -7
- package/dist/chunks/chunk-6IVUG7FY.js.map +7 -0
- package/dist/chunks/{chunk-B2RTLDXY.js → chunk-AZQYF6KE.js} +132 -1
- package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
- package/dist/chunks/{chunk-NKUV77SR.js → chunk-CHD5UCFF.js} +21 -9
- package/dist/chunks/{chunk-NKUV77SR.js.map → chunk-CHD5UCFF.js.map} +2 -2
- package/dist/chunks/{chunk-TPQ7APVQ.js → chunk-EQYDSPBB.js} +418 -62
- package/dist/chunks/chunk-EQYDSPBB.js.map +7 -0
- package/dist/chunks/{chunk-RQSTH2BS.js → chunk-H4JSCDNW.js} +2 -2
- package/dist/chunks/{chunk-EK4KESLU.js → chunk-J23ZX5AP.js} +8 -2
- package/dist/chunks/{chunk-EK4KESLU.js.map → chunk-J23ZX5AP.js.map} +2 -2
- package/dist/chunks/{chunk-D5E3OKSL.js → chunk-JER5NQVM.js} +5 -5
- package/dist/chunks/{chunk-BJRKEPMP.js → chunk-KPU2XHOS.js} +5 -2
- package/dist/chunks/{chunk-BJRKEPMP.js.map → chunk-KPU2XHOS.js.map} +2 -2
- package/dist/chunks/{chunk-NP76N4HQ.js → chunk-LKAGAQ3M.js} +2 -2
- package/dist/chunks/{chunk-3FHJUHAS.js → chunk-S2CX6HFM.js} +260 -25
- package/dist/chunks/chunk-S2CX6HFM.js.map +7 -0
- package/dist/chunks/{configService-IGJEC3MC.js → configService-CCA6AIDI.js} +3 -3
- package/dist/entries/server-router.js +9 -9
- package/dist/entries/server-router.js.map +2 -2
- package/dist/lib/client/index.js +54 -20
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +9 -9
- package/dist/lib/shared/index.js +46 -10
- package/dist/lib/shared/index.js.map +3 -3
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/ComponentBuilder.ts +8 -1
- package/lib/client/core/builders/embedBuilder.ts +15 -2
- package/lib/client/core/builders/linkNodeBuilder.ts +15 -2
- package/lib/client/core/builders/localeListBuilder.ts +17 -6
- package/lib/client/styles/StyleInjector.ts +3 -2
- package/lib/client/theme.ts +4 -4
- package/lib/server/cssGenerator.test.ts +64 -1
- package/lib/server/cssGenerator.ts +48 -9
- package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +200 -11
- package/lib/server/routes/index.ts +1 -1
- package/lib/server/routes/pages.ts +23 -1
- package/lib/server/services/cmsService.test.ts +246 -0
- package/lib/server/services/cmsService.ts +122 -5
- package/lib/server/services/configService.ts +5 -0
- package/lib/server/ssr/attributeBuilder.ts +41 -0
- package/lib/server/ssr/htmlGenerator.test.ts +113 -0
- package/lib/server/ssr/htmlGenerator.ts +51 -4
- package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
- package/lib/server/ssr/ssrRenderer.test.ts +306 -0
- package/lib/server/ssr/ssrRenderer.ts +182 -44
- package/lib/shared/cssGeneration.test.ts +267 -1
- package/lib/shared/cssGeneration.ts +240 -18
- package/lib/shared/cssProperties.test.ts +247 -1
- package/lib/shared/cssProperties.ts +196 -6
- package/lib/shared/interfaces/contentProvider.ts +39 -6
- package/lib/shared/pathSecurity.ts +16 -0
- package/lib/shared/responsiveScaling.test.ts +143 -0
- package/lib/shared/responsiveScaling.ts +253 -2
- package/lib/shared/themeDefaults.test.ts +3 -3
- package/lib/shared/themeDefaults.ts +3 -3
- package/lib/shared/types/cms.ts +28 -3
- package/lib/shared/types/index.ts +1 -0
- package/lib/shared/utilityClassConfig.ts +3 -0
- package/lib/shared/utilityClassMapper.test.ts +123 -0
- package/lib/shared/utilityClassMapper.ts +179 -8
- package/lib/shared/validation/schemas.ts +15 -1
- package/lib/shared/validation/validators.ts +26 -1
- package/package.json +1 -1
- package/dist/chunks/chunk-3FHJUHAS.js.map +0 -7
- package/dist/chunks/chunk-B2RTLDXY.js.map +0 -7
- package/dist/chunks/chunk-TPQ7APVQ.js.map +0 -7
- package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
- /package/dist/chunks/{chunk-RQSTH2BS.js.map → chunk-H4JSCDNW.js.map} +0 -0
- /package/dist/chunks/{chunk-D5E3OKSL.js.map → chunk-JER5NQVM.js.map} +0 -0
- /package/dist/chunks/{chunk-NP76N4HQ.js.map → chunk-LKAGAQ3M.js.map} +0 -0
- /package/dist/chunks/{configService-IGJEC3MC.js.map → configService-CCA6AIDI.js.map} +0 -0
|
@@ -9,6 +9,7 @@ import { processItemTemplate, processItemPropsTemplate, hasItemTemplates, buildT
|
|
|
9
9
|
import { singularize, isItemDraftForLocale } from '../../shared/types/cms';
|
|
10
10
|
import type { ResponsiveStyleObject, StyleObject } from '../../shared/types';
|
|
11
11
|
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
12
|
+
import type { ResponsiveScales } from '../../shared/responsiveScaling';
|
|
12
13
|
import { evaluateTemplate, processStructure, isResponsiveStyle, isHtmlMapping, resolveHtmlMapping } from '../../client/templateEngine';
|
|
13
14
|
import { resolvePropsFromDefinition, isRichTextMarker, richTextMarkerToHtml } from '../../shared/propResolver';
|
|
14
15
|
import { loadBreakpointConfig, loadI18nConfig } from '../jsonLoader';
|
|
@@ -50,7 +51,7 @@ import type { SlugMap } from '../../shared/slugTranslator';
|
|
|
50
51
|
import { buildSlugIndex, getLocaleLinks, translatePath } from '../../shared/slugTranslator';
|
|
51
52
|
|
|
52
53
|
// Import from modularized files
|
|
53
|
-
import { escapeHtml, buildAttributes, styleToString } from './attributeBuilder';
|
|
54
|
+
import { escapeHtml, buildAttributes, buildEditorAttrs, styleToString } from './attributeBuilder';
|
|
54
55
|
import { extractPageMeta, generateMetaTags } from './metaTagGenerator';
|
|
55
56
|
import { collectComponentCSS } from './cssCollector';
|
|
56
57
|
import { collectComponentJavaScript } from './jsCollector';
|
|
@@ -128,6 +129,30 @@ interface SSRContext {
|
|
|
128
129
|
processedRawHtmlCollector?: Map<string, string>;
|
|
129
130
|
/** Image format: 'webp' uses plain <img>, 'avif' uses <picture> with AVIF+WebP sources */
|
|
130
131
|
imageFormat?: 'webp' | 'avif';
|
|
132
|
+
/**
|
|
133
|
+
* When true, emit editor-only attributes (data-element-path, data-cms-item-index,
|
|
134
|
+
* data-cms-context, data-component-root, data-parent-component, data-component-context)
|
|
135
|
+
* so XRayOverlay and click-to-select can hook into SSR-rendered DOM.
|
|
136
|
+
* Set only in preview/dev mode — never in production builds.
|
|
137
|
+
*/
|
|
138
|
+
injectEditorAttrs?: boolean;
|
|
139
|
+
/** CMS list item index path (parallel to cmsListPaths) for nested list selection support */
|
|
140
|
+
cmsItemIndexPath?: number[];
|
|
141
|
+
/** CMS list element paths (parallel to cmsItemIndexPath) — outer lists first */
|
|
142
|
+
cmsListPaths?: number[][];
|
|
143
|
+
/** Name of the enclosing component instance (for data-parent-component on descendants) */
|
|
144
|
+
parentComponentName?: string;
|
|
145
|
+
/**
|
|
146
|
+
* Element path of the current component instance's root, in the component's local
|
|
147
|
+
* (reset) coordinate space. When ctx.elementPath deeply equals this, the element
|
|
148
|
+
* being rendered is the component root and gets data-component-root="true".
|
|
149
|
+
*/
|
|
150
|
+
componentRootPath?: number[];
|
|
151
|
+
/**
|
|
152
|
+
* Responsive scales config — needed for fluid-mode style transformations
|
|
153
|
+
* (e.g. `applyContainerPattern` when width === maxWidth).
|
|
154
|
+
*/
|
|
155
|
+
responsiveScales?: ResponsiveScales;
|
|
131
156
|
}
|
|
132
157
|
|
|
133
158
|
/**
|
|
@@ -138,6 +163,21 @@ function getTemplateContext(ctx: SSRContext): TemplateContext | null {
|
|
|
138
163
|
return ctx.templateContext || null;
|
|
139
164
|
}
|
|
140
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Build the resolution scope for list `items` and `filter` value templates inside a
|
|
168
|
+
* component structure. Merge order (lowest → highest precedence):
|
|
169
|
+
* componentResolvedProps → templateContext (parent list loop variables)
|
|
170
|
+
* The cmsContext stays out of this scope — it's namespaced via {{cms.X}} and resolved
|
|
171
|
+
* on its own branch in getCollectionItems. Returns undefined when no scope is available
|
|
172
|
+
* (e.g. page root with no host component and no parent list).
|
|
173
|
+
*/
|
|
174
|
+
function buildListResolutionScope(ctx: SSRContext): Record<string, unknown> | undefined {
|
|
175
|
+
const tplCtx = ctx.templateContext as Record<string, unknown> | undefined;
|
|
176
|
+
const props = ctx.componentResolvedProps;
|
|
177
|
+
if (!tplCtx && !props) return undefined;
|
|
178
|
+
return { ...(props ?? {}), ...(tplCtx ?? {}) };
|
|
179
|
+
}
|
|
180
|
+
|
|
141
181
|
/**
|
|
142
182
|
* Create a value resolver for i18n resolution (for use with item templates)
|
|
143
183
|
*/
|
|
@@ -198,7 +238,18 @@ function processStyleToClasses(
|
|
|
198
238
|
getI18nResolver(ctx)
|
|
199
239
|
) as StyleObject | ResponsiveStyleObject;
|
|
200
240
|
}
|
|
201
|
-
|
|
241
|
+
|
|
242
|
+
// Fluid container pattern: in fluid mode `responsiveStylesToClasses` rewrites
|
|
243
|
+
// any tier whose `width === maxWidth` to use `calc(100% - var(--site-margin)*2)`
|
|
244
|
+
// with auto margins.
|
|
245
|
+
const fluidActive =
|
|
246
|
+
ctx.responsiveScales?.enabled === true &&
|
|
247
|
+
ctx.responsiveScales?.mode === 'fluid';
|
|
248
|
+
|
|
249
|
+
return responsiveStylesToClasses(
|
|
250
|
+
processedStyle as ResponsiveStyleObject,
|
|
251
|
+
{ fluidActive, responsiveScales: ctx.responsiveScales }
|
|
252
|
+
);
|
|
202
253
|
}
|
|
203
254
|
|
|
204
255
|
/**
|
|
@@ -260,33 +311,34 @@ function evaluateIfCondition(node: ComponentNode, ctx: SSRContext): boolean {
|
|
|
260
311
|
}
|
|
261
312
|
|
|
262
313
|
/**
|
|
263
|
-
* Resolve a template value like "{{category.slug}}" from
|
|
314
|
+
* Resolve a template value like "{{category.slug}}" from a flat scope.
|
|
264
315
|
* Returns the original value if not a template or can't be resolved.
|
|
265
316
|
*/
|
|
266
|
-
function resolveFilterValue(value: unknown,
|
|
267
|
-
if (!
|
|
317
|
+
function resolveFilterValue(value: unknown, scope: Record<string, unknown> | undefined): unknown {
|
|
318
|
+
if (!scope || typeof value !== 'string' || !value.startsWith('{{') || !value.endsWith('}}')) {
|
|
268
319
|
return value;
|
|
269
320
|
}
|
|
270
321
|
const path = value.slice(2, -2).trim();
|
|
271
|
-
const resolved = getNestedValue(
|
|
322
|
+
const resolved = getNestedValue(scope, path);
|
|
272
323
|
return resolved !== undefined ? resolved : value;
|
|
273
324
|
}
|
|
274
325
|
|
|
275
326
|
/**
|
|
276
327
|
* Resolve template values in CMS filter for nested cms-list support.
|
|
277
|
-
* Handles filter values like "{{category.slug}}"
|
|
328
|
+
* Handles filter values like "{{category.slug}}" against a flat scope built from
|
|
329
|
+
* parent template context overlaid on host-component resolved props.
|
|
278
330
|
*/
|
|
279
331
|
function resolveFilterTemplates(
|
|
280
332
|
filter: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined,
|
|
281
|
-
|
|
333
|
+
scope: Record<string, unknown> | undefined
|
|
282
334
|
): CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined {
|
|
283
|
-
if (!filter || !
|
|
335
|
+
if (!filter || !scope) return filter;
|
|
284
336
|
|
|
285
337
|
// Handle array of conditions
|
|
286
338
|
if (Array.isArray(filter)) {
|
|
287
339
|
return filter.map(cond => ({
|
|
288
340
|
...cond,
|
|
289
|
-
value: resolveFilterValue(cond.value,
|
|
341
|
+
value: resolveFilterValue(cond.value, scope)
|
|
290
342
|
}));
|
|
291
343
|
}
|
|
292
344
|
|
|
@@ -294,14 +346,14 @@ function resolveFilterTemplates(
|
|
|
294
346
|
if ('field' in filter && 'value' in filter) {
|
|
295
347
|
return {
|
|
296
348
|
...(filter as CMSFilterCondition),
|
|
297
|
-
value: resolveFilterValue((filter as CMSFilterCondition).value,
|
|
349
|
+
value: resolveFilterValue((filter as CMSFilterCondition).value, scope)
|
|
298
350
|
};
|
|
299
351
|
}
|
|
300
352
|
|
|
301
353
|
// Handle simple object filter: { category: "{{category.slug}}" }
|
|
302
354
|
const resolved: Record<string, unknown> = {};
|
|
303
355
|
for (const [key, value] of Object.entries(filter)) {
|
|
304
|
-
resolved[key] = resolveFilterValue(value,
|
|
356
|
+
resolved[key] = resolveFilterValue(value, scope);
|
|
305
357
|
}
|
|
306
358
|
return resolved;
|
|
307
359
|
}
|
|
@@ -383,7 +435,8 @@ export async function buildComponentHTML(
|
|
|
383
435
|
pagePath?: string,
|
|
384
436
|
cmsContext?: CMSContext,
|
|
385
437
|
cmsService?: CMSService,
|
|
386
|
-
isProductionBuild?: boolean
|
|
438
|
+
isProductionBuild?: boolean,
|
|
439
|
+
injectEditorAttrs?: boolean
|
|
387
440
|
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string>; processedRawHtmlCollector: Map<string, string> }> {
|
|
388
441
|
// Create map to collect interactive styles during render
|
|
389
442
|
const interactiveStylesMap = new Map<string, InteractiveStyles>();
|
|
@@ -430,6 +483,8 @@ export async function buildComponentHTML(
|
|
|
430
483
|
ssrFallbackCollector, // Collect SSR fallback HTML for complex nodes
|
|
431
484
|
processedRawHtmlCollector, // Collect raw→processed HTML for Astro exporter
|
|
432
485
|
imageFormat: configService.getImageFormat(),
|
|
486
|
+
injectEditorAttrs,
|
|
487
|
+
responsiveScales: configService.getResponsiveScales(),
|
|
433
488
|
};
|
|
434
489
|
|
|
435
490
|
const html = await renderNode(node, ctx);
|
|
@@ -482,7 +537,7 @@ async function renderNestedListPlaceholder(
|
|
|
482
537
|
const configJson = escapeHtml(JSON.stringify(config));
|
|
483
538
|
|
|
484
539
|
// Emit placeholder with embedded config and template
|
|
485
|
-
return `<div data-cms-list-nested="true" data-collection="${escapeHtml(sourceStr)}" data-cms-config="${configJson}">` +
|
|
540
|
+
return `<div data-cms-list-nested="true" data-collection="${escapeHtml(sourceStr)}" data-cms-config="${configJson}"${editorAttrs(ctx, { isCMSListContainer: true })}>` +
|
|
486
541
|
`<template data-nested-template>${templateContent}</template>` +
|
|
487
542
|
`</div>`;
|
|
488
543
|
}
|
|
@@ -585,6 +640,8 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
|
|
|
585
640
|
const itemCtx: SSRContext = {
|
|
586
641
|
...ctx,
|
|
587
642
|
templateContext,
|
|
643
|
+
cmsItemIndexPath: [...(ctx.cmsItemIndexPath ?? []), i],
|
|
644
|
+
cmsListPaths: [...(ctx.cmsListPaths ?? []), ctx.elementPath ?? []],
|
|
588
645
|
};
|
|
589
646
|
|
|
590
647
|
const childrenHtml = await renderChildrenAsync(node.children || [], itemCtx);
|
|
@@ -648,8 +705,12 @@ async function getCollectionItems(node: ListNode, source: string, ctx: SSRContex
|
|
|
648
705
|
resolvedIds = Array.isArray(value) ? value.map(v => String(v)) : String(value);
|
|
649
706
|
}
|
|
650
707
|
} else {
|
|
651
|
-
// Otherwise resolve from
|
|
652
|
-
|
|
708
|
+
// Otherwise resolve from a merged scope: parent list loop variables (highest precedence)
|
|
709
|
+
// overlaid on the host component's resolved props (lowest precedence). This lets
|
|
710
|
+
// {{config.tagIds}} resolve against a component prop named `config` while {{post.X}}
|
|
711
|
+
// from a parent list still wins on key collisions.
|
|
712
|
+
const mergedScope = buildListResolutionScope(ctx) ?? {};
|
|
713
|
+
const parentContext = { _type: 'template' as const, ...mergedScope } as TemplateContext;
|
|
653
714
|
resolvedIds = resolveItemsTemplate(node.items, parentContext);
|
|
654
715
|
}
|
|
655
716
|
|
|
@@ -668,10 +729,11 @@ async function getCollectionItems(node: ListNode, source: string, ctx: SSRContex
|
|
|
668
729
|
items = items.filter(item => !isItemDraftForLocale(item, ctx.locale!));
|
|
669
730
|
}
|
|
670
731
|
} else {
|
|
671
|
-
// Build query from node props (resolve filter templates
|
|
732
|
+
// Build query from node props (resolve filter templates against parent loop variables
|
|
733
|
+
// overlaid on host-component resolved props, with parent loop variables winning).
|
|
672
734
|
const query = {
|
|
673
735
|
collection: source,
|
|
674
|
-
filter: resolveFilterTemplates(node.filter as CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined, ctx
|
|
736
|
+
filter: resolveFilterTemplates(node.filter as CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined, buildListResolutionScope(ctx)),
|
|
675
737
|
sort: node.sort,
|
|
676
738
|
limit: node.limit,
|
|
677
739
|
offset: node.offset,
|
|
@@ -757,11 +819,18 @@ function buildNodeElementClass(
|
|
|
757
819
|
? pagePath.replace(/^\//, '').replace(/\//g, '_') || 'index'
|
|
758
820
|
: 'page');
|
|
759
821
|
|
|
822
|
+
// Slice off the component-instance prefix so class hashes stay stable across
|
|
823
|
+
// instances of the same component (mirrors ComponentBuilder.ts:678-680).
|
|
824
|
+
const rawPath = ctx.elementPath || [];
|
|
825
|
+
const path = useComponentContext && ctx.componentRootPath
|
|
826
|
+
? rawPath.slice(ctx.componentRootPath.length)
|
|
827
|
+
: rawPath;
|
|
828
|
+
|
|
760
829
|
const elementClassCtx: ElementClassContext = {
|
|
761
830
|
fileType: effectiveFileType,
|
|
762
831
|
fileName: effectiveFileName || 'page',
|
|
763
832
|
label,
|
|
764
|
-
path
|
|
833
|
+
path,
|
|
765
834
|
};
|
|
766
835
|
return generateElementClassName(elementClassCtx);
|
|
767
836
|
}
|
|
@@ -799,6 +868,42 @@ function buildCssVariableStyleAttr(cssVariables: Record<string, string>): string
|
|
|
799
868
|
return ` style="${escapeHtml(styleString)}"`;
|
|
800
869
|
}
|
|
801
870
|
|
|
871
|
+
function arraysEqual(a: number[] | undefined, b: number[] | undefined): boolean {
|
|
872
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
873
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Compute editor-only attributes for the current context.
|
|
879
|
+
* Returns '' when injectEditorAttrs is off — preview-only feature.
|
|
880
|
+
*/
|
|
881
|
+
function editorAttrs(
|
|
882
|
+
ctx: SSRContext,
|
|
883
|
+
opts: { isSlotContent?: boolean; isCMSListContainer?: boolean } = {}
|
|
884
|
+
): string {
|
|
885
|
+
if (!ctx.injectEditorAttrs) return '';
|
|
886
|
+
// Component root: inside a component instance and at the path the instance was reset to.
|
|
887
|
+
const isComponentRoot = !!ctx.componentContext
|
|
888
|
+
&& !opts.isSlotContent
|
|
889
|
+
&& arraysEqual(ctx.elementPath, ctx.componentRootPath);
|
|
890
|
+
// Mirror client semantics: at component root, parent is the outer component;
|
|
891
|
+
// for descendants of a component, parent is the current component itself.
|
|
892
|
+
const effectiveParent = opts.isSlotContent
|
|
893
|
+
? ctx.parentComponentName
|
|
894
|
+
: (isComponentRoot ? ctx.parentComponentName : (ctx.componentContext ?? ctx.parentComponentName));
|
|
895
|
+
return buildEditorAttrs({
|
|
896
|
+
elementPath: ctx.elementPath,
|
|
897
|
+
cmsItemIndexPath: ctx.cmsItemIndexPath,
|
|
898
|
+
cmsListPaths: ctx.cmsListPaths,
|
|
899
|
+
componentContext: ctx.componentContext,
|
|
900
|
+
parentComponentName: effectiveParent,
|
|
901
|
+
isComponentRoot,
|
|
902
|
+
isSlotContent: opts.isSlotContent,
|
|
903
|
+
isCMSListContainer: opts.isCMSListContainer,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
802
907
|
/**
|
|
803
908
|
* Render a node (or array of nodes) to HTML string
|
|
804
909
|
*/
|
|
@@ -960,7 +1065,7 @@ async function renderNode(
|
|
|
960
1065
|
const classAttr = classNames.length > 0 ? ` class="${escapeHtml(classNames.filter(Boolean).join(' '))}"` : '';
|
|
961
1066
|
|
|
962
1067
|
// Always use span for embeds - valid inside <p> and other phrasing content
|
|
963
|
-
return `<span${classAttr}${embedStyleAttr}${attrs}>${optimizedHtml}</span>`;
|
|
1068
|
+
return `<span${classAttr}${embedStyleAttr}${attrs}${editorAttrs(ctx, { isSlotContent: isSlotContent(node) })}>${optimizedHtml}</span>`;
|
|
964
1069
|
}
|
|
965
1070
|
|
|
966
1071
|
// Add attribute className if present (fallback when no interactive styles)
|
|
@@ -975,7 +1080,7 @@ async function renderNode(
|
|
|
975
1080
|
const classAttr = classNames.length > 0 ? ` class="${escapeHtml(classNames.filter(Boolean).join(' '))}"` : '';
|
|
976
1081
|
|
|
977
1082
|
// Always use span for embeds - valid inside <p> and other phrasing content
|
|
978
|
-
return `<span${classAttr}${attrs}>${optimizedHtml}</span>`;
|
|
1083
|
+
return `<span${classAttr}${attrs}${editorAttrs(ctx, { isSlotContent: isSlotContent(node) })}>${optimizedHtml}</span>`;
|
|
979
1084
|
}
|
|
980
1085
|
|
|
981
1086
|
// Handle link nodes (render as <a> tag in SSR)
|
|
@@ -1070,7 +1175,7 @@ async function renderNode(
|
|
|
1070
1175
|
}))).join('')
|
|
1071
1176
|
: await renderNode(children as ComponentNode | string | number | null | undefined, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
|
|
1072
1177
|
|
|
1073
|
-
return `<a href="${escapeHtml(String(href))}"${classAttr}${olinkStyleAttr}${attrs}>${childrenHTML}</a>`;
|
|
1178
|
+
return `<a href="${escapeHtml(String(href))}"${classAttr}${olinkStyleAttr}${attrs}${editorAttrs(ctx, { isSlotContent: isSlotContent(node) })}>${childrenHTML}</a>`;
|
|
1074
1179
|
}
|
|
1075
1180
|
|
|
1076
1181
|
// Handle locale-list node type (render locale switcher links)
|
|
@@ -1351,13 +1456,17 @@ async function renderComponent(
|
|
|
1351
1456
|
}
|
|
1352
1457
|
}
|
|
1353
1458
|
|
|
1354
|
-
// Render the processed component structure with component context
|
|
1355
|
-
//
|
|
1356
|
-
//
|
|
1459
|
+
// Render the processed component structure with component context.
|
|
1460
|
+
// elementPath stays page-absolute (matching client ComponentBuilder.ts:874) so
|
|
1461
|
+
// data-element-path values match XRay's tree paths. componentRootPath captures
|
|
1462
|
+
// the page-level path of this instance so buildNodeElementClass can slice it
|
|
1463
|
+
// off to keep class hashes component-relative across instances.
|
|
1357
1464
|
return await renderNode(processedStructure, {
|
|
1358
1465
|
...ctx,
|
|
1466
|
+
// The previously-active component (if any) becomes the parent for editor attrs
|
|
1467
|
+
parentComponentName: ctx.componentContext,
|
|
1359
1468
|
componentContext: componentName,
|
|
1360
|
-
|
|
1469
|
+
componentRootPath: ctx.elementPath,
|
|
1361
1470
|
componentResolvedProps: resolvedProps,
|
|
1362
1471
|
});
|
|
1363
1472
|
|
|
@@ -1394,7 +1503,7 @@ async function renderLinkNode(
|
|
|
1394
1503
|
}))).join('')
|
|
1395
1504
|
: await renderNode(children as ComponentNode, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
|
|
1396
1505
|
|
|
1397
|
-
return `<a href="${escapeHtml(String(href))}"${linkClassAttr}${attrs}>${childrenHTML}</a>`;
|
|
1506
|
+
return `<a href="${escapeHtml(String(href))}"${linkClassAttr}${attrs}${editorAttrs(ctx)}>${childrenHTML}</a>`;
|
|
1398
1507
|
}
|
|
1399
1508
|
|
|
1400
1509
|
/**
|
|
@@ -1435,25 +1544,47 @@ async function renderHtmlElement(
|
|
|
1435
1544
|
? ['style', 'className', ...imageProps]
|
|
1436
1545
|
: ['style', 'className'];
|
|
1437
1546
|
const attrs = buildAttributes(propsWithStyleAndAttrs, excludeProps);
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1547
|
+
// `<style>` and `<script>` are HTML "raw text" elements: their content is
|
|
1548
|
+
// not parsed for entities or child elements. Running text children through
|
|
1549
|
+
// `renderNode` (which calls `escapeHtml`) would convert `"`, `<`, `>` to
|
|
1550
|
+
// entities — breaking CSS selectors / @font-face URLs and JS code. Emit the
|
|
1551
|
+
// string children verbatim instead. This path is used by the chrome-
|
|
1552
|
+
// extension import flow to inject `@font-face` rules captured from the
|
|
1553
|
+
// source page (`HtmlToMenoConverter.convertHtmlStringToMeno`).
|
|
1554
|
+
const isRawTextElement = tag.toLowerCase() === 'style' || tag.toLowerCase() === 'script';
|
|
1555
|
+
let childrenHTML: string;
|
|
1556
|
+
if (isRawTextElement) {
|
|
1557
|
+
const flatten = (node: ComponentNode | ComponentNode[] | string | number | null | undefined): string => {
|
|
1558
|
+
if (node == null) return '';
|
|
1559
|
+
if (typeof node === 'string') return node;
|
|
1560
|
+
if (typeof node === 'number') return String(node);
|
|
1561
|
+
if (Array.isArray(node)) return node.map(flatten).join('');
|
|
1562
|
+
return '';
|
|
1563
|
+
};
|
|
1564
|
+
childrenHTML = flatten(children as any);
|
|
1565
|
+
} else {
|
|
1566
|
+
childrenHTML = Array.isArray(children)
|
|
1567
|
+
? (await Promise.all((children as (ComponentNode | string)[]).map((child, index) => {
|
|
1568
|
+
const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
|
|
1569
|
+
return renderNode(child, { ...ctx, elementPath: childPath });
|
|
1570
|
+
}))).join('')
|
|
1571
|
+
: await renderNode(children as ComponentNode, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
const ea = editorAttrs(ctx);
|
|
1444
1575
|
|
|
1445
1576
|
// Self-closing tags
|
|
1446
1577
|
const voidElements = ['img', 'input', 'br', 'hr', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'];
|
|
1447
1578
|
if (voidElements.includes(tag.toLowerCase())) {
|
|
1448
1579
|
// Special handling for img tags - inject srcset and render as <picture> when AVIF available
|
|
1449
1580
|
if (tag.toLowerCase() === 'img') {
|
|
1450
|
-
return renderImageElement(propsWithStyleAndAttrs, classAttr, styleAttr, attrs, ctx);
|
|
1581
|
+
return renderImageElement(propsWithStyleAndAttrs, classAttr, styleAttr, attrs + ea, ctx);
|
|
1451
1582
|
}
|
|
1452
1583
|
|
|
1453
|
-
return `<${tag}${classAttr}${styleAttr}${attrs} />`;
|
|
1584
|
+
return `<${tag}${classAttr}${styleAttr}${attrs}${ea} />`;
|
|
1454
1585
|
}
|
|
1455
1586
|
|
|
1456
|
-
return `<${tag}${classAttr}${styleAttr}${attrs}>${childrenHTML}</${tag}>`;
|
|
1587
|
+
return `<${tag}${classAttr}${styleAttr}${attrs}${ea}>${childrenHTML}</${tag}>`;
|
|
1457
1588
|
}
|
|
1458
1589
|
|
|
1459
1590
|
/**
|
|
@@ -1584,10 +1715,16 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
|
|
|
1584
1715
|
}
|
|
1585
1716
|
}
|
|
1586
1717
|
|
|
1718
|
+
const localeStyleOpts = {
|
|
1719
|
+
fluidActive:
|
|
1720
|
+
ctx.responsiveScales?.enabled === true && ctx.responsiveScales?.mode === 'fluid',
|
|
1721
|
+
responsiveScales: ctx.responsiveScales,
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1587
1724
|
// Convert container styles to utility classes
|
|
1588
1725
|
let containerClasses: string[] = [];
|
|
1589
1726
|
if (nodeStyle) {
|
|
1590
|
-
containerClasses = responsiveStylesToClasses(nodeStyle as ResponsiveStyleObject);
|
|
1727
|
+
containerClasses = responsiveStylesToClasses(nodeStyle as ResponsiveStyleObject, localeStyleOpts);
|
|
1591
1728
|
}
|
|
1592
1729
|
|
|
1593
1730
|
// Handle interactive styles for locale-list wrapper
|
|
@@ -1613,7 +1750,7 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
|
|
|
1613
1750
|
// Convert item styles to utility classes
|
|
1614
1751
|
let itemClasses: string[] = [];
|
|
1615
1752
|
if (node.itemStyle) {
|
|
1616
|
-
itemClasses = responsiveStylesToClasses(node.itemStyle as ResponsiveStyleObject);
|
|
1753
|
+
itemClasses = responsiveStylesToClasses(node.itemStyle as ResponsiveStyleObject, localeStyleOpts);
|
|
1617
1754
|
}
|
|
1618
1755
|
const itemClassAttr = itemClasses.length > 0
|
|
1619
1756
|
? ` class="${escapeHtml(itemClasses.join(' '))}"`
|
|
@@ -1622,13 +1759,13 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
|
|
|
1622
1759
|
// Convert active item styles to utility classes
|
|
1623
1760
|
let activeItemClasses: string[] = [];
|
|
1624
1761
|
if (node.activeItemStyle) {
|
|
1625
|
-
activeItemClasses = responsiveStylesToClasses(node.activeItemStyle as ResponsiveStyleObject);
|
|
1762
|
+
activeItemClasses = responsiveStylesToClasses(node.activeItemStyle as ResponsiveStyleObject, localeStyleOpts);
|
|
1626
1763
|
}
|
|
1627
1764
|
|
|
1628
1765
|
// Convert separator styles to utility classes
|
|
1629
1766
|
let separatorClasses: string[] = [];
|
|
1630
1767
|
if (node.separatorStyle) {
|
|
1631
|
-
separatorClasses = responsiveStylesToClasses(node.separatorStyle as ResponsiveStyleObject);
|
|
1768
|
+
separatorClasses = responsiveStylesToClasses(node.separatorStyle as ResponsiveStyleObject, localeStyleOpts);
|
|
1632
1769
|
}
|
|
1633
1770
|
const separatorClassAttr = separatorClasses.length > 0
|
|
1634
1771
|
? ` class="${escapeHtml(separatorClasses.join(' '))}"`
|
|
@@ -1637,7 +1774,7 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
|
|
|
1637
1774
|
// Convert flag styles to utility classes
|
|
1638
1775
|
let flagClasses: string[] = [];
|
|
1639
1776
|
if (node.flagStyle) {
|
|
1640
|
-
flagClasses = responsiveStylesToClasses(node.flagStyle as ResponsiveStyleObject);
|
|
1777
|
+
flagClasses = responsiveStylesToClasses(node.flagStyle as ResponsiveStyleObject, localeStyleOpts);
|
|
1641
1778
|
}
|
|
1642
1779
|
const flagClassAttr = flagClasses.length > 0
|
|
1643
1780
|
? ` class="${escapeHtml(flagClasses.join(' '))}"`
|
|
@@ -1692,7 +1829,7 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
|
|
|
1692
1829
|
const nodeAttributes = extractAttributesFromNode(node);
|
|
1693
1830
|
const attrsStr = buildAttributes(nodeAttributes);
|
|
1694
1831
|
|
|
1695
|
-
const localeListResult = `<div data-locale-list="true"${containerClassAttr}${localeListStyleAttr}${attrsStr}>${linksHTML}</div>`;
|
|
1832
|
+
const localeListResult = `<div data-locale-list="true"${containerClassAttr}${localeListStyleAttr}${attrsStr}${editorAttrs(ctx)}>${linksHTML}</div>`;
|
|
1696
1833
|
|
|
1697
1834
|
// Store SSR fallback for Astro export (locale-list nodes can't be expressed as static Astro components)
|
|
1698
1835
|
if (ctx.ssrFallbackCollector && ctx.elementPath) {
|
|
@@ -1718,7 +1855,8 @@ export async function renderPageSSR(
|
|
|
1718
1855
|
slugMappings?: SlugMap[],
|
|
1719
1856
|
cmsContext?: CMSContext,
|
|
1720
1857
|
cmsService?: CMSService,
|
|
1721
|
-
isProductionBuild?: boolean
|
|
1858
|
+
isProductionBuild?: boolean,
|
|
1859
|
+
injectEditorAttrs?: boolean
|
|
1722
1860
|
): Promise<{ html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string>; processedRawHtmlCollector: Map<string, string> }> {
|
|
1723
1861
|
// Extract page content
|
|
1724
1862
|
const rootNode = pageData?.root || undefined;
|
|
@@ -1756,7 +1894,7 @@ export async function renderPageSSR(
|
|
|
1756
1894
|
// Render the component tree to HTML with i18n and CMS support
|
|
1757
1895
|
// Also collect interactive styles, preload images, and needed collections during render
|
|
1758
1896
|
const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector } = rootNode
|
|
1759
|
-
? await buildComponentHTML(rootNode, globalComponents, pageComponents, effectiveLocale, config, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild)
|
|
1897
|
+
? await buildComponentHTML(rootNode, globalComponents, pageComponents, effectiveLocale, config, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild, injectEditorAttrs)
|
|
1760
1898
|
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [], neededCollections: new Set<string>(), ssrFallbackCollector: new Map<string, string>(), processedRawHtmlCollector: new Map<string, string>() };
|
|
1761
1899
|
|
|
1762
1900
|
// Collect JavaScript and CSS from all components
|