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.
Files changed (74) hide show
  1. package/dist/build-static.js +7 -7
  2. package/dist/chunks/{chunk-UUA5LEWF.js → chunk-6IVUG7FY.js} +138 -7
  3. package/dist/chunks/chunk-6IVUG7FY.js.map +7 -0
  4. package/dist/chunks/{chunk-B2RTLDXY.js → chunk-AZQYF6KE.js} +132 -1
  5. package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
  6. package/dist/chunks/{chunk-NKUV77SR.js → chunk-CHD5UCFF.js} +21 -9
  7. package/dist/chunks/{chunk-NKUV77SR.js.map → chunk-CHD5UCFF.js.map} +2 -2
  8. package/dist/chunks/{chunk-TPQ7APVQ.js → chunk-EQYDSPBB.js} +418 -62
  9. package/dist/chunks/chunk-EQYDSPBB.js.map +7 -0
  10. package/dist/chunks/{chunk-RQSTH2BS.js → chunk-H4JSCDNW.js} +2 -2
  11. package/dist/chunks/{chunk-EK4KESLU.js → chunk-J23ZX5AP.js} +8 -2
  12. package/dist/chunks/{chunk-EK4KESLU.js.map → chunk-J23ZX5AP.js.map} +2 -2
  13. package/dist/chunks/{chunk-D5E3OKSL.js → chunk-JER5NQVM.js} +5 -5
  14. package/dist/chunks/{chunk-BJRKEPMP.js → chunk-KPU2XHOS.js} +5 -2
  15. package/dist/chunks/{chunk-BJRKEPMP.js.map → chunk-KPU2XHOS.js.map} +2 -2
  16. package/dist/chunks/{chunk-NP76N4HQ.js → chunk-LKAGAQ3M.js} +2 -2
  17. package/dist/chunks/{chunk-3FHJUHAS.js → chunk-S2CX6HFM.js} +260 -25
  18. package/dist/chunks/chunk-S2CX6HFM.js.map +7 -0
  19. package/dist/chunks/{configService-IGJEC3MC.js → configService-CCA6AIDI.js} +3 -3
  20. package/dist/entries/server-router.js +9 -9
  21. package/dist/entries/server-router.js.map +2 -2
  22. package/dist/lib/client/index.js +54 -20
  23. package/dist/lib/client/index.js.map +3 -3
  24. package/dist/lib/server/index.js +9 -9
  25. package/dist/lib/shared/index.js +46 -10
  26. package/dist/lib/shared/index.js.map +3 -3
  27. package/entries/server-router.tsx +6 -2
  28. package/lib/client/core/ComponentBuilder.ts +8 -1
  29. package/lib/client/core/builders/embedBuilder.ts +15 -2
  30. package/lib/client/core/builders/linkNodeBuilder.ts +15 -2
  31. package/lib/client/core/builders/localeListBuilder.ts +17 -6
  32. package/lib/client/styles/StyleInjector.ts +3 -2
  33. package/lib/client/theme.ts +4 -4
  34. package/lib/server/cssGenerator.test.ts +64 -1
  35. package/lib/server/cssGenerator.ts +48 -9
  36. package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
  37. package/lib/server/providers/fileSystemCMSProvider.ts +200 -11
  38. package/lib/server/routes/index.ts +1 -1
  39. package/lib/server/routes/pages.ts +23 -1
  40. package/lib/server/services/cmsService.test.ts +246 -0
  41. package/lib/server/services/cmsService.ts +122 -5
  42. package/lib/server/services/configService.ts +5 -0
  43. package/lib/server/ssr/attributeBuilder.ts +41 -0
  44. package/lib/server/ssr/htmlGenerator.test.ts +113 -0
  45. package/lib/server/ssr/htmlGenerator.ts +51 -4
  46. package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
  47. package/lib/server/ssr/ssrRenderer.test.ts +306 -0
  48. package/lib/server/ssr/ssrRenderer.ts +182 -44
  49. package/lib/shared/cssGeneration.test.ts +267 -1
  50. package/lib/shared/cssGeneration.ts +240 -18
  51. package/lib/shared/cssProperties.test.ts +247 -1
  52. package/lib/shared/cssProperties.ts +196 -6
  53. package/lib/shared/interfaces/contentProvider.ts +39 -6
  54. package/lib/shared/pathSecurity.ts +16 -0
  55. package/lib/shared/responsiveScaling.test.ts +143 -0
  56. package/lib/shared/responsiveScaling.ts +253 -2
  57. package/lib/shared/themeDefaults.test.ts +3 -3
  58. package/lib/shared/themeDefaults.ts +3 -3
  59. package/lib/shared/types/cms.ts +28 -3
  60. package/lib/shared/types/index.ts +1 -0
  61. package/lib/shared/utilityClassConfig.ts +3 -0
  62. package/lib/shared/utilityClassMapper.test.ts +123 -0
  63. package/lib/shared/utilityClassMapper.ts +179 -8
  64. package/lib/shared/validation/schemas.ts +15 -1
  65. package/lib/shared/validation/validators.ts +26 -1
  66. package/package.json +1 -1
  67. package/dist/chunks/chunk-3FHJUHAS.js.map +0 -7
  68. package/dist/chunks/chunk-B2RTLDXY.js.map +0 -7
  69. package/dist/chunks/chunk-TPQ7APVQ.js.map +0 -7
  70. package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
  71. /package/dist/chunks/{chunk-RQSTH2BS.js.map → chunk-H4JSCDNW.js.map} +0 -0
  72. /package/dist/chunks/{chunk-D5E3OKSL.js.map → chunk-JER5NQVM.js.map} +0 -0
  73. /package/dist/chunks/{chunk-NP76N4HQ.js.map → chunk-LKAGAQ3M.js.map} +0 -0
  74. /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
- return responsiveStylesToClasses(processedStyle as ResponsiveStyleObject);
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 template context.
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, templateContext: TemplateContext | undefined): unknown {
267
- if (!templateContext || typeof value !== 'string' || !value.startsWith('{{') || !value.endsWith('}}')) {
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(templateContext as Record<string, unknown>, path);
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}}" from parent template context.
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
- templateContext: TemplateContext | undefined
333
+ scope: Record<string, unknown> | undefined
282
334
  ): CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined {
283
- if (!filter || !templateContext) return 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, templateContext)
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, templateContext)
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, templateContext);
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 template context (for nested lists)
652
- const parentContext = ctx.templateContext || { _type: 'template' as const };
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 for nested list support)
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.templateContext),
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: ctx.elementPath || [],
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
- // Reset element path to component-relative for stable class names
1356
- // Pass resolved props for interactive style mapping resolution
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
- elementPath: [0],
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
- const childrenHTML = Array.isArray(children)
1439
- ? (await Promise.all((children as (ComponentNode | string)[]).map((child, index) => {
1440
- const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
1441
- return renderNode(child, { ...ctx, elementPath: childPath });
1442
- }))).join('')
1443
- : await renderNode(children as ComponentNode, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
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