@ethlete/contentful 4.0.0-next.5 → 4.0.0-next.7

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.
@@ -1,15 +1,11 @@
1
1
  import { NgClass, DOCUMENT } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, InjectionToken, inject, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, reflectComponentType } from '@angular/core';
4
- import { gql } from '@ethlete/query';
5
- import { normalizePictureSizes, PictureComponent } from '@ethlete/cdk';
3
+ import { input, computed, ViewEncapsulation, Component, InjectionToken, inject, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, linkedSignal, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
4
+ import { normalizePictureSizes, PictureComponent } from '@ethlete/components';
6
5
  import { RouterLink } from '@angular/router';
7
- import { toSignal, toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
- import { INLINES, BLOCKS } from '@contentful/rich-text-types';
9
6
  import { RuntimeError, isObject, injectRenderer, getObjectProperty } from '@ethlete/core';
10
- import { startWith, pairwise, tap } from 'rxjs';
11
7
 
12
- const GQL_FRAGMENT_CONTENTFUL_ASSET = gql `
8
+ const GQL_FRAGMENT_CONTENTFUL_ASSET = `
13
9
  fragment AssetData on Asset {
14
10
  sys {
15
11
  id
@@ -56,17 +52,17 @@ class ContentfulAudioComponent {
56
52
  }, /* @ts-ignore */
57
53
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
58
54
  }
59
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
60
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ContentfulAudioComponent, isStandalone: true, selector: "et-contentful-audio", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, audioClass: { classPropertyName: "audioClass", publicName: "audioClass", isSignal: true, isRequired: false, transformFunction: null }, figureClass: { classPropertyName: "figureClass", publicName: "figureClass", isSignal: true, isRequired: false, transformFunction: null }, figcaptionClass: { classPropertyName: "figcaptionClass", publicName: "figcaptionClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
55
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
56
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulAudioComponent, isStandalone: true, selector: "et-contentful-audio", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, audioClass: { classPropertyName: "audioClass", publicName: "audioClass", isSignal: true, isRequired: false, transformFunction: null }, figureClass: { classPropertyName: "figureClass", publicName: "figureClass", isSignal: true, isRequired: false, transformFunction: null }, figcaptionClass: { classPropertyName: "figcaptionClass", publicName: "figcaptionClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
61
57
  @if (data(); as data) {
62
58
  <figure [ngClass]="figureClass()">
63
59
  <figcaption [ngClass]="figcaptionClass()">{{ data.title }}</figcaption>
64
60
  <audio [ngClass]="audioClass()" controls src="{{ data.url }}"></audio>
65
61
  </figure>
66
62
  }
67
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
63
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
68
64
  }
69
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulAudioComponent, decorators: [{
65
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulAudioComponent, decorators: [{
70
66
  type: Component,
71
67
  args: [{
72
68
  selector: 'et-contentful-audio',
@@ -78,7 +74,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78
74
  </figure>
79
75
  }
80
76
  `,
81
- changeDetection: ChangeDetectionStrategy.OnPush,
82
77
  encapsulation: ViewEncapsulation.None,
83
78
  imports: [NgClass],
84
79
  }]
@@ -112,16 +107,16 @@ class ContentfulFileComponent {
112
107
  }, /* @ts-ignore */
113
108
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
114
109
  }
115
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
116
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ContentfulFileComponent, isStandalone: true, selector: "et-contentful-file", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, fileClass: { classPropertyName: "fileClass", publicName: "fileClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
110
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
111
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulFileComponent, isStandalone: true, selector: "et-contentful-file", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, fileClass: { classPropertyName: "fileClass", publicName: "fileClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
117
112
  @if (data(); as data) {
118
113
  <a [href]="data.url" [ngClass]="fileClass()" class="underline" target="_blank">
119
114
  {{ data.title }} ({{ data.size }} Bytes)
120
115
  </a>
121
116
  }
122
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
117
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
123
118
  }
124
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulFileComponent, decorators: [{
119
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulFileComponent, decorators: [{
125
120
  type: Component,
126
121
  args: [{
127
122
  selector: 'et-contentful-file',
@@ -132,7 +127,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
132
127
  </a>
133
128
  }
134
129
  `,
135
- changeDetection: ChangeDetectionStrategy.OnPush,
136
130
  encapsulation: ViewEncapsulation.None,
137
131
  imports: [NgClass],
138
132
  }]
@@ -191,13 +185,13 @@ const parseContentfulImageSize = (size) => {
191
185
  height: height ? parseInt(height, 10) : null,
192
186
  };
193
187
  };
188
+ const SOURCE_TYPES = ['image/avif', 'image/webp', 'image/png', 'image/jpg'];
194
189
  const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior) => {
195
190
  const assetData = data;
196
191
  if (!assetData) {
197
192
  return [];
198
193
  }
199
194
  const sources = [];
200
- const SOURCE_TYPES = ['image/avif', 'image/webp', 'image/png', 'image/jpg'];
201
195
  for (const type of SOURCE_TYPES) {
202
196
  const baseUrl = isContentfulGqlAsset(assetData) ? assetData.url : assetData.fields.file.url;
203
197
  const sourceSets = [];
@@ -241,28 +235,20 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, qual
241
235
 
242
236
  class ContentfulImageComponent {
243
237
  constructor() {
244
- this._contentfulConfig = inject(CONTENTFUL_CONFIG);
238
+ this.contentfulConfig = inject(CONTENTFUL_CONFIG);
245
239
  this.asset = input.required(/* @ts-ignore */
246
240
  ...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
247
241
  this.backgroundColor = input(null, /* @ts-ignore */
248
242
  ...(ngDevMode ? [{ debugName: "backgroundColor" }] : /* istanbul ignore next */ []));
249
- this.srcsetSizes = input(this._contentfulConfig.imageOptions.srcsetSizes, /* @ts-ignore */
243
+ this.srcsetSizes = input(this.contentfulConfig.imageOptions.srcsetSizes, /* @ts-ignore */
250
244
  ...(ngDevMode ? [{ debugName: "srcsetSizes" }] : /* istanbul ignore next */ []));
251
245
  this.quality = input(null, { ...(ngDevMode ? { debugName: "quality" } : /* istanbul ignore next */ {}), transform: numberAttribute });
252
246
  this.focusArea = input(null, /* @ts-ignore */
253
247
  ...(ngDevMode ? [{ debugName: "focusArea" }] : /* istanbul ignore next */ []));
254
248
  this.resizeBehavior = input(null, /* @ts-ignore */
255
249
  ...(ngDevMode ? [{ debugName: "resizeBehavior" }] : /* istanbul ignore next */ []));
256
- this.hasPriority = input(false, { ...(ngDevMode ? { debugName: "hasPriority" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
257
- this.imgClass = input(null, /* @ts-ignore */
258
- ...(ngDevMode ? [{ debugName: "imgClass" }] : /* istanbul ignore next */ []));
259
- this.figureClass = input(null, /* @ts-ignore */
260
- ...(ngDevMode ? [{ debugName: "figureClass" }] : /* istanbul ignore next */ []));
261
- this.figcaptionClass = input(null, /* @ts-ignore */
262
- ...(ngDevMode ? [{ debugName: "figcaptionClass" }] : /* istanbul ignore next */ []));
263
- this.pictureClass = input(null, /* @ts-ignore */
264
- ...(ngDevMode ? [{ debugName: "pictureClass" }] : /* istanbul ignore next */ []));
265
- this.sizes = input(normalizePictureSizes(this._contentfulConfig.imageOptions.sizes), { ...(ngDevMode ? { debugName: "sizes" } : /* istanbul ignore next */ {}), transform: (v) => normalizePictureSizes(v) });
250
+ this.priority = input(false, { ...(ngDevMode ? { debugName: "priority" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
251
+ this.sizes = input(normalizePictureSizes(this.contentfulConfig.imageOptions.sizes), { ...(ngDevMode ? { debugName: "sizes" } : /* istanbul ignore next */ {}), transform: (v) => normalizePictureSizes(v) });
266
252
  this.normalizedAsset = computed(() => {
267
253
  const asset = this.asset();
268
254
  if (isContentfulGqlAsset(asset)) {
@@ -283,7 +269,7 @@ class ContentfulImageComponent {
283
269
  }
284
270
  }, /* @ts-ignore */
285
271
  ...(ngDevMode ? [{ debugName: "normalizedAsset" }] : /* istanbul ignore next */ []));
286
- this._sources = computed(() => {
272
+ this.sourcesValue = computed(() => {
287
273
  const asset = this.asset();
288
274
  const backgroundColor = this.backgroundColor();
289
275
  const srcsetSizes = this.srcsetSizes();
@@ -295,59 +281,50 @@ class ContentfulImageComponent {
295
281
  }
296
282
  return generateContentfulImageSources(asset, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior);
297
283
  }, /* @ts-ignore */
298
- ...(ngDevMode ? [{ debugName: "_sources" }] : /* istanbul ignore next */ []));
299
- this._defaultSrc = computed(() => {
284
+ ...(ngDevMode ? [{ debugName: "sourcesValue" }] : /* istanbul ignore next */ []));
285
+ this.defaultSrcValue = computed(() => {
300
286
  const asset = this.asset();
301
287
  return asset ? generateDefaultContentfulImageSource(asset) : null;
302
288
  }, /* @ts-ignore */
303
- ...(ngDevMode ? [{ debugName: "_defaultSrc" }] : /* istanbul ignore next */ []));
289
+ ...(ngDevMode ? [{ debugName: "defaultSrcValue" }] : /* istanbul ignore next */ []));
304
290
  }
305
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
306
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.5", type: ContentfulImageComponent, isStandalone: true, selector: "et-contentful-image", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, srcsetSizes: { classPropertyName: "srcsetSizes", publicName: "srcsetSizes", isSignal: true, isRequired: false, transformFunction: null }, quality: { classPropertyName: "quality", publicName: "quality", isSignal: true, isRequired: false, transformFunction: null }, focusArea: { classPropertyName: "focusArea", publicName: "focusArea", isSignal: true, isRequired: false, transformFunction: null }, resizeBehavior: { classPropertyName: "resizeBehavior", publicName: "resizeBehavior", isSignal: true, isRequired: false, transformFunction: null }, hasPriority: { classPropertyName: "hasPriority", publicName: "hasPriority", isSignal: true, isRequired: false, transformFunction: null }, imgClass: { classPropertyName: "imgClass", publicName: "imgClass", isSignal: true, isRequired: false, transformFunction: null }, figureClass: { classPropertyName: "figureClass", publicName: "figureClass", isSignal: true, isRequired: false, transformFunction: null }, figcaptionClass: { classPropertyName: "figcaptionClass", publicName: "figcaptionClass", isSignal: true, isRequired: false, transformFunction: null }, pictureClass: { classPropertyName: "pictureClass", publicName: "pictureClass", isSignal: true, isRequired: false, transformFunction: null }, sizes: { classPropertyName: "sizes", publicName: "sizes", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-image" }, ngImport: i0, template: `
291
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
292
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: ContentfulImageComponent, isStandalone: true, selector: "et-contentful-image", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, srcsetSizes: { classPropertyName: "srcsetSizes", publicName: "srcsetSizes", isSignal: true, isRequired: false, transformFunction: null }, quality: { classPropertyName: "quality", publicName: "quality", isSignal: true, isRequired: false, transformFunction: null }, focusArea: { classPropertyName: "focusArea", publicName: "focusArea", isSignal: true, isRequired: false, transformFunction: null }, resizeBehavior: { classPropertyName: "resizeBehavior", publicName: "resizeBehavior", isSignal: true, isRequired: false, transformFunction: null }, priority: { classPropertyName: "priority", publicName: "priority", isSignal: true, isRequired: false, transformFunction: null }, sizes: { classPropertyName: "sizes", publicName: "sizes", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-image" }, ngImport: i0, template: `
307
293
  <et-picture
308
- [imgClass]="imgClass()"
309
- [hasPriority]="hasPriority()"
310
- [figureClass]="figureClass()"
311
- [pictureClass]="pictureClass()"
312
- [figcaptionClass]="figcaptionClass()"
313
- [defaultSrc]="_defaultSrc()"
314
- [alt]="normalizedAsset().alt ?? null"
294
+ [priority]="priority()"
295
+ [defaultSrc]="defaultSrcValue()"
296
+ [alt]="normalizedAsset().alt ?? ''"
315
297
  [figcaption]="normalizedAsset().figcaption ?? null"
316
298
  [width]="normalizedAsset().width ?? null"
317
299
  [height]="normalizedAsset().height ?? null"
318
300
  [sizes]="sizes()"
319
- [sources]="_sources()"
301
+ [sources]="sourcesValue()"
320
302
  />
321
- `, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "hasPriority", "imgClass", "figureClass", "figcaptionClass", "pictureClass", "defaultSrc", "alt", "figcaption", "width", "height", "sizes"], outputs: ["imgLoaded", "imgError"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
303
+ `, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "defaultSrc", "alt", "figcaption", "priority", "width", "height", "aspectRatio", "sizes"], outputs: ["imgLoad", "imgError"] }], encapsulation: i0.ViewEncapsulation.None }); }
322
304
  }
323
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulImageComponent, decorators: [{
305
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulImageComponent, decorators: [{
324
306
  type: Component,
325
307
  args: [{
326
308
  selector: 'et-contentful-image',
327
309
  template: `
328
310
  <et-picture
329
- [imgClass]="imgClass()"
330
- [hasPriority]="hasPriority()"
331
- [figureClass]="figureClass()"
332
- [pictureClass]="pictureClass()"
333
- [figcaptionClass]="figcaptionClass()"
334
- [defaultSrc]="_defaultSrc()"
335
- [alt]="normalizedAsset().alt ?? null"
311
+ [priority]="priority()"
312
+ [defaultSrc]="defaultSrcValue()"
313
+ [alt]="normalizedAsset().alt ?? ''"
336
314
  [figcaption]="normalizedAsset().figcaption ?? null"
337
315
  [width]="normalizedAsset().width ?? null"
338
316
  [height]="normalizedAsset().height ?? null"
339
317
  [sizes]="sizes()"
340
- [sources]="_sources()"
318
+ [sources]="sourcesValue()"
341
319
  />
342
320
  `,
343
- changeDetection: ChangeDetectionStrategy.OnPush,
344
321
  encapsulation: ViewEncapsulation.None,
345
322
  imports: [PictureComponent],
346
323
  host: {
347
324
  class: 'et-contentful-image',
348
325
  },
349
326
  }]
350
- }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], srcsetSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "srcsetSizes", required: false }] }], quality: [{ type: i0.Input, args: [{ isSignal: true, alias: "quality", required: false }] }], focusArea: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusArea", required: false }] }], resizeBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizeBehavior", required: false }] }], hasPriority: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPriority", required: false }] }], imgClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "imgClass", required: false }] }], figureClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "figureClass", required: false }] }], figcaptionClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "figcaptionClass", required: false }] }], pictureClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "pictureClass", required: false }] }], sizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizes", required: false }] }] } });
327
+ }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], srcsetSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "srcsetSizes", required: false }] }], quality: [{ type: i0.Input, args: [{ isSignal: true, alias: "quality", required: false }] }], focusArea: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusArea", required: false }] }], resizeBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizeBehavior", required: false }] }], priority: [{ type: i0.Input, args: [{ isSignal: true, alias: "priority", required: false }] }], sizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizes", required: false }] }] } });
351
328
 
352
329
  class ContentfulVideoComponent {
353
330
  constructor() {
@@ -373,16 +350,16 @@ class ContentfulVideoComponent {
373
350
  }, /* @ts-ignore */
374
351
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
375
352
  }
376
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
377
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ContentfulVideoComponent, isStandalone: true, selector: "et-contentful-video", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, videoClass: { classPropertyName: "videoClass", publicName: "videoClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
353
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
354
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulVideoComponent, isStandalone: true, selector: "et-contentful-video", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, videoClass: { classPropertyName: "videoClass", publicName: "videoClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
378
355
  @if (data(); as data) {
379
356
  <video [ngClass]="videoClass()" controls>
380
357
  <source src="{{ data.url }}" type="{{ data.contentType }}" />
381
358
  </video>
382
359
  }
383
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
360
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
384
361
  }
385
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
386
363
  type: Component,
387
364
  args: [{
388
365
  selector: 'et-contentful-video',
@@ -393,16 +370,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
393
370
  </video>
394
371
  }
395
372
  `,
396
- changeDetection: ChangeDetectionStrategy.OnPush,
397
373
  encapsulation: ViewEncapsulation.None,
398
374
  imports: [NgClass],
399
375
  }]
400
376
  }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], videoClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoClass", required: false }] }] } });
401
377
 
378
+ /**
379
+ * The config used when no `provideContentfulConfig()` is in scope. It declares no
380
+ * `components`, so embedded assets are skipped and hyperlinks render as plain anchors
381
+ * until a config is provided.
382
+ */
383
+ const CONTENTFUL_FALLBACK_CONFIG = {
384
+ internalHosts: [],
385
+ components: {},
386
+ customComponents: {},
387
+ imageOptions: {
388
+ srcsetSizes: ['375w', '1280w', '1920w', '2560w'],
389
+ sizes: [],
390
+ backgroundColor: null,
391
+ },
392
+ };
402
393
  const createContentfulConfig = (config = {}) => ({
403
394
  // This default config must be inline and not saved inside a const for some weird webpack reason
404
395
  ...{
405
- useTailwindClasses: false,
406
396
  internalHosts: [],
407
397
  components: {
408
398
  audio: ContentfulAudioComponent,
@@ -420,6 +410,11 @@ const createContentfulConfig = (config = {}) => ({
420
410
  },
421
411
  ...(config || {}),
422
412
  });
413
+ /**
414
+ * Reads the contentful config from the current injector. Falls back to a config without
415
+ * any `components` when none was provided via `provideContentfulConfig()`.
416
+ */
417
+ const injectContentfulConfig = () => inject(CONTENTFUL_CONFIG, { optional: true }) ?? CONTENTFUL_FALLBACK_CONFIG;
423
418
 
424
419
  const getPrimaryDomain = (host) => {
425
420
  const parts = host.split('.');
@@ -427,8 +422,8 @@ const getPrimaryDomain = (host) => {
427
422
  };
428
423
  class ContentfulLinkComponent {
429
424
  constructor() {
430
- this._document = inject(DOCUMENT);
431
- this._config = inject(CONTENTFUL_CONFIG, { optional: true }) ?? createContentfulConfig();
425
+ this.document = inject(DOCUMENT);
426
+ this.config = injectContentfulConfig();
432
427
  this.href = input.required(/* @ts-ignore */
433
428
  ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
434
429
  this.text = input.required(/* @ts-ignore */
@@ -442,7 +437,7 @@ class ContentfulLinkComponent {
442
437
  return false;
443
438
  try {
444
439
  const parsed = new URL(href.startsWith('//') ? `https:${href}` : href);
445
- const internalHosts = [this._document.location.host, ...this._config.internalHosts];
440
+ const internalHosts = [this.document.location.host, ...this.config.internalHosts];
446
441
  return !internalHosts.includes(parsed.host);
447
442
  }
448
443
  catch {
@@ -457,8 +452,8 @@ class ContentfulLinkComponent {
457
452
  try {
458
453
  const parsed = new URL(href.startsWith('//') ? `https:${href}` : href);
459
454
  const internalPrimaryDomains = [
460
- getPrimaryDomain(this._document.location.host),
461
- ...this._config.internalHosts.map(getPrimaryDomain),
455
+ getPrimaryDomain(this.document.location.host),
456
+ ...this.config.internalHosts.map(getPrimaryDomain),
462
457
  ];
463
458
  return !internalPrimaryDomains.includes(getPrimaryDomain(parsed.host));
464
459
  }
@@ -488,8 +483,8 @@ class ContentfulLinkComponent {
488
483
  }, /* @ts-ignore */
489
484
  ...(ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []));
490
485
  }
491
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
492
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ContentfulLinkComponent, isStandalone: true, selector: "et-contentful-link", inputs: { href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, textClass: { classPropertyName: "textClass", publicName: "textClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
486
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
487
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulLinkComponent, isStandalone: true, selector: "et-contentful-link", inputs: { href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, textClass: { classPropertyName: "textClass", publicName: "textClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
493
488
  @if (isExternal()) {
494
489
  <a
495
490
  [class]="linkClass()"
@@ -501,9 +496,9 @@ class ContentfulLinkComponent {
501
496
  } @else {
502
497
  <a [class]="linkClass()" [routerLink]="internalPath()">{{ text() }}</a>
503
498
  }
504
- `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
499
+ `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }], encapsulation: i0.ViewEncapsulation.None }); }
505
500
  }
506
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
501
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
507
502
  type: Component,
508
503
  args: [{
509
504
  selector: 'et-contentful-link',
@@ -520,15 +515,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
520
515
  <a [class]="linkClass()" [routerLink]="internalPath()">{{ text() }}</a>
521
516
  }
522
517
  `,
518
+ encapsulation: ViewEncapsulation.None,
519
+ imports: [RouterLink],
523
520
  host: {
524
521
  style: 'display: contents',
525
522
  },
526
- imports: [RouterLink],
527
- changeDetection: ChangeDetectionStrategy.OnPush,
528
- encapsulation: ViewEncapsulation.None,
529
523
  }]
530
524
  }], propDecorators: { href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], textClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "textClass", required: false }] }] } });
531
525
 
526
+ /**
527
+ * The `BLOCKS` node type strings of `@contentful/rich-text-types`, as a plain literal map.
528
+ * Keep the values in sync with the enum - they are part of the Contentful Delivery API.
529
+ */
530
+ const CF_BLOCKS = {
531
+ DOCUMENT: 'document',
532
+ PARAGRAPH: 'paragraph',
533
+ HEADING_1: 'heading-1',
534
+ HEADING_2: 'heading-2',
535
+ HEADING_3: 'heading-3',
536
+ HEADING_4: 'heading-4',
537
+ HEADING_5: 'heading-5',
538
+ HEADING_6: 'heading-6',
539
+ OL_LIST: 'ordered-list',
540
+ UL_LIST: 'unordered-list',
541
+ LIST_ITEM: 'list-item',
542
+ HR: 'hr',
543
+ QUOTE: 'blockquote',
544
+ EMBEDDED_ENTRY: 'embedded-entry-block',
545
+ EMBEDDED_ASSET: 'embedded-asset-block',
546
+ EMBEDDED_RESOURCE: 'embedded-resource-block',
547
+ TABLE: 'table',
548
+ TABLE_ROW: 'table-row',
549
+ TABLE_CELL: 'table-cell',
550
+ TABLE_HEADER_CELL: 'table-header-cell',
551
+ };
552
+ /**
553
+ * The `INLINES` node type strings of `@contentful/rich-text-types`, as a plain literal map.
554
+ * Keep the values in sync with the enum - they are part of the Contentful Delivery API.
555
+ */
556
+ const CF_INLINES = {
557
+ ASSET_HYPERLINK: 'asset-hyperlink',
558
+ EMBEDDED_ENTRY: 'embedded-entry-inline',
559
+ EMBEDDED_RESOURCE: 'embedded-resource-inline',
560
+ ENTRY_HYPERLINK: 'entry-hyperlink',
561
+ HYPERLINK: 'hyperlink',
562
+ RESOURCE_HYPERLINK: 'resource-hyperlink',
563
+ };
564
+
532
565
  const RICH_TEXT_RENDERER_ERRORS = {
533
566
  rich_text_undefined: 'The property value given at richTextPath is undefined. Use the richTextPath input to specify the path to the rich text object.',
534
567
  rich_text_wrong_type: 'The rich text object does not satisfy the RichTextResponse interface. It should contain a property named "nodeType" with the value "document".',
@@ -540,10 +573,17 @@ const RICH_TEXT_RENDERER_ERRORS = {
540
573
  text_parent_not_found: 'The parent node is not found. This structure is not supported.',
541
574
  text_parent_wrong_type: 'The parent node neither a html element nor a custom component. This structure is not supported.',
542
575
  };
543
- const RICH_TEXT_RENDERER_ERROR_CODES = Object.keys(RICH_TEXT_RENDERER_ERRORS).reduce((acc, key, index) => {
544
- acc[key] = index;
545
- return acc;
546
- }, {});
576
+ const RICH_TEXT_RENDERER_ERROR_CODES = {
577
+ rich_text_undefined: 0,
578
+ rich_text_wrong_type: 1,
579
+ asset_id_not_found: 2,
580
+ entry_id_not_found: 3,
581
+ asset_not_found: 4,
582
+ entry_not_found: 5,
583
+ custom_component_not_found: 6,
584
+ text_parent_not_found: 7,
585
+ text_parent_wrong_type: 8,
586
+ };
547
587
  const richTextRendererError = (code, data) => {
548
588
  const message = `<et-contentful-rich-text-renderer>: ${RICH_TEXT_RENDERER_ERRORS[code]}`;
549
589
  throw new RuntimeError(RICH_TEXT_RENDERER_ERROR_CODES[code], message, data);
@@ -554,160 +594,93 @@ const isRichTextRootNode = (node) => {
554
594
  };
555
595
  const translateContentfulNodeTypeToHtmlTag = (nodeType) => {
556
596
  switch (nodeType) {
557
- case BLOCKS.HEADING_1:
597
+ case CF_BLOCKS.HEADING_1:
558
598
  return 'h1';
559
- case BLOCKS.HEADING_2:
599
+ case CF_BLOCKS.HEADING_2:
560
600
  return 'h2';
561
- case BLOCKS.HEADING_3:
601
+ case CF_BLOCKS.HEADING_3:
562
602
  return 'h3';
563
- case BLOCKS.HEADING_4:
603
+ case CF_BLOCKS.HEADING_4:
564
604
  return 'h4';
565
- case BLOCKS.HEADING_5:
605
+ case CF_BLOCKS.HEADING_5:
566
606
  return 'h5';
567
- case BLOCKS.HEADING_6:
607
+ case CF_BLOCKS.HEADING_6:
568
608
  return 'h6';
569
- case BLOCKS.PARAGRAPH:
609
+ case CF_BLOCKS.PARAGRAPH:
570
610
  return 'p';
571
- case BLOCKS.UL_LIST:
611
+ case CF_BLOCKS.UL_LIST:
572
612
  return 'ul';
573
- case BLOCKS.OL_LIST:
613
+ case CF_BLOCKS.OL_LIST:
574
614
  return 'ol';
575
- case BLOCKS.LIST_ITEM:
615
+ case CF_BLOCKS.LIST_ITEM:
576
616
  return 'li';
577
- case BLOCKS.HR:
617
+ case CF_BLOCKS.HR:
578
618
  return 'hr';
579
- case BLOCKS.QUOTE:
619
+ case CF_BLOCKS.QUOTE:
580
620
  return 'blockquote';
581
- case BLOCKS.TABLE:
621
+ case CF_BLOCKS.TABLE:
582
622
  return 'table';
583
- case BLOCKS.TABLE_ROW:
623
+ case CF_BLOCKS.TABLE_ROW:
584
624
  return 'tr';
585
- case BLOCKS.TABLE_CELL:
625
+ case CF_BLOCKS.TABLE_CELL:
586
626
  return 'td';
587
- case BLOCKS.TABLE_HEADER_CELL:
627
+ case CF_BLOCKS.TABLE_HEADER_CELL:
588
628
  return 'th';
589
- case BLOCKS.EMBEDDED_ASSET:
629
+ case CF_BLOCKS.EMBEDDED_ASSET:
590
630
  return 'div';
591
- case INLINES.ENTRY_HYPERLINK:
631
+ case CF_INLINES.ENTRY_HYPERLINK:
592
632
  return 'a';
593
- case INLINES.ASSET_HYPERLINK:
633
+ case CF_INLINES.ASSET_HYPERLINK:
594
634
  return 'a';
595
635
  case 'text':
596
636
  return 'span';
597
637
  // Will be ignored by the renderer
598
638
  case 'document':
599
639
  return 'div';
600
- case BLOCKS.EMBEDDED_ENTRY:
640
+ case CF_BLOCKS.EMBEDDED_ENTRY:
601
641
  return 'div';
602
- case INLINES.EMBEDDED_ENTRY:
642
+ case CF_INLINES.EMBEDDED_ENTRY:
603
643
  return 'div';
604
644
  default:
605
645
  return 'div';
606
646
  }
607
647
  };
608
648
 
609
- const RENDER_COMMAND_TYPE = {
610
- HTML_OPEN: 0,
611
- HTML_CLOSE: 1,
612
- TEXT: 2,
613
- COMPONENT: 3,
614
- };
615
- const RENDER_COMMAND_POSITION = {
616
- TYPE: 0,
617
- NESTING_LEVEL: 1,
618
- DOM_POSITION: 2,
619
- INDEX: 3,
620
- };
621
- const HTML_OPEN_RENDER_COMMAND_POSITION = {
622
- ATTRIBUTES: 4,
623
- TAG_NAME: 5,
624
- ELEMENT_ID: 6,
649
+ const MARK_TAG_MAP = {
650
+ bold: 'strong',
651
+ italic: 'em',
652
+ underline: 'u',
653
+ code: 'code',
654
+ strikethrough: 's',
655
+ subscript: 'sub',
656
+ superscript: 'sup',
625
657
  };
626
- const HTML_CLOSE_RENDER_COMMAND_POSITION = {
627
- TAG_NAME: 4,
628
- ELEMENT_ID: 5,
629
- };
630
- const TEXT_RENDER_COMMAND_POSITION = {
631
- ATTRIBUTES: 4,
632
- TEXT: 5,
633
- TEXT_ID: 6,
634
- };
635
- const COMPONENT_RENDER_COMMAND_POSITION = {
636
- ATTRIBUTES: 4,
637
- COMPONENT: 5,
638
- INPUTS: 6,
639
- COMPONENT_ID: 7,
640
- };
641
- const RENDER_INSTRUCTION_POSITION = {
642
- TYPE: 0,
643
- COMMAND: 1,
644
- };
645
- const RENDER_INSTRUCTION_TYPE = {
646
- CREATE: 0,
647
- UPDATE: 1,
648
- DELETE: 2,
649
- MOVE: 3,
650
- };
651
- const MARK_TAILWIND_MAP = {
652
- bold: 'font-bold',
653
- italic: 'italic',
654
- underline: 'underline',
655
- code: 'font-mono',
656
- };
657
- const marksToClass = (marks) => {
658
- const classes = [];
658
+ /**
659
+ * Translates contentful text marks into the semantic html elements the marked text is
660
+ * wrapped in, outermost first. Unknown marks are skipped (with a dev-mode warning).
661
+ */
662
+ const marksToTags = (marks) => {
663
+ const tags = [];
659
664
  for (const mark of marks) {
660
- let klass = MARK_TAILWIND_MAP[mark.type];
661
- if (!klass) {
662
- console.warn(`No class found for mark type! Falling back to "${mark.type}".`, mark);
663
- klass = mark.type;
665
+ const tag = MARK_TAG_MAP[mark.type];
666
+ if (!tag) {
667
+ if (isDevMode()) {
668
+ console.warn(`No element found for mark type "${mark.type}"! The mark is ignored.`, mark);
669
+ }
670
+ continue;
664
671
  }
665
- classes.push(klass);
666
- }
667
- return classes.join(' ');
668
- };
669
- const isHtmlOpenRenderCommand = (command) => {
670
- return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_OPEN;
671
- };
672
- const isHtmlCloseRenderCommand = (command) => {
673
- return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_CLOSE;
674
- };
675
- const isTextRenderCommand = (command) => {
676
- return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.TEXT;
677
- };
678
- const isComponentRenderCommand = (command) => {
679
- return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.COMPONENT;
680
- };
681
- const getRenderCommandId = (command) => {
682
- if (isHtmlOpenRenderCommand(command)) {
683
- return command[HTML_OPEN_RENDER_COMMAND_POSITION.ELEMENT_ID];
684
- }
685
- else if (isHtmlCloseRenderCommand(command)) {
686
- return command[HTML_CLOSE_RENDER_COMMAND_POSITION.ELEMENT_ID];
687
- }
688
- else if (isTextRenderCommand(command)) {
689
- return command[TEXT_RENDER_COMMAND_POSITION.TEXT_ID];
690
- }
691
- else {
692
- return command[COMPONENT_RENDER_COMMAND_POSITION.COMPONENT_ID];
672
+ tags.push(tag);
693
673
  }
674
+ return tags;
694
675
  };
695
- const CLASS_ATTR = 'class';
696
- const DEFAULT_COMPONENT_TYPES = {
697
- IMAGE: '$$$_et-image',
698
- VIDEO: '$$$_et-video',
699
- AUDIO: '$$$_et-audio',
700
- FILE: '$$$_et-file',
701
- LINK: '$$$_et-link',
702
- };
676
+ /**
677
+ * The classes marked text inside a hyperlink is rendered with, since the link component
678
+ * receives its text as a plain string.
679
+ */
680
+ const marksToClass = (marks) => marks.map((mark) => `et-contentful-rich-text-mark-${mark.type}`).join(' ');
681
+ const LINK_COMPONENT_TYPE = '$$$_et-link';
703
682
  const isExecutedComponentCommandCacheItem = (cache) => {
704
- return isComponentRenderCommand(cache.command);
705
- };
706
- const isExecutedHtmlOrTextCommandCacheItem = (cache) => {
707
- return isHtmlCloseRenderCommand(cache.command) || isHtmlOpenRenderCommand(cache.command);
708
- };
709
- const isExecutedTextCommandCacheItem = (cache) => {
710
- return isTextRenderCommand(cache.command);
683
+ return cache.command.kind === 'component';
711
684
  };
712
685
  const ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID = '$$$_et-contentful-any-entry-content-type-sys-id';
713
686
  /**
@@ -763,17 +736,10 @@ const createContentfulIncludeMap = (config) => {
763
736
  };
764
737
  class ContentfulRichTextRendererComponent {
765
738
  constructor() {
766
- this._viewContainerRef = inject(ViewContainerRef);
767
- this._renderer = injectRenderer();
768
- this._elementRef = inject(ElementRef);
769
- this._document = inject(DOCUMENT);
770
- this._config = inject(CONTENTFUL_CONFIG, { optional: true }) ?? createContentfulConfig();
771
- /**
772
- * A cache for all executed commands that are not deleted.
773
- * This is used to keep track of all rendered elements and components.
774
- * The key is the render command element ID.
775
- */
776
- this._executedCommandsCache = new Map();
739
+ this.viewContainerRef = inject(ViewContainerRef);
740
+ this.renderer = injectRenderer();
741
+ this.elementRef = inject(ElementRef);
742
+ this.config = injectContentfulConfig();
777
743
  /**
778
744
  * The contentful response gotten via their REST api.
779
745
  * @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
@@ -786,6 +752,12 @@ class ContentfulRichTextRendererComponent {
786
752
  */
787
753
  this.richTextPath = input.required(/* @ts-ignore */
788
754
  ...(ngDevMode ? [{ debugName: "richTextPath" }] : /* istanbul ignore next */ []));
755
+ /**
756
+ * A cache for all executed commands that are not deleted.
757
+ * This is used to keep track of all rendered elements and components.
758
+ * The key is the render command ID.
759
+ */
760
+ this.executedCommandsCache = new Map();
789
761
  /**
790
762
  * A map of all includes in the contentful response.
791
763
  * This is useful for looking up assets and entries that are referenced in the rich text without having to use loops.
@@ -821,93 +793,116 @@ class ContentfulRichTextRendererComponent {
821
793
  if (!richTextData) {
822
794
  return [];
823
795
  }
824
- return this._createRenderCommands(richTextData);
796
+ return this.createRenderCommands(richTextData);
825
797
  }, /* @ts-ignore */
826
798
  ...(ngDevMode ? [{ debugName: "renderCommands" }] : /* istanbul ignore next */ []));
827
- this.renderCommandHistory = toSignal(toObservable(this.renderCommands).pipe(startWith([]), pairwise()), { initialValue: [[], []] });
799
+ this.renderCommandHistory = linkedSignal({ ...(ngDevMode ? { debugName: "renderCommandHistory" } : /* istanbul ignore next */ {}), source: this.renderCommands,
800
+ computation: (commands, previous) => [previous?.source ?? [], commands] });
828
801
  this.previousRenderCommandMap = computed(() => {
829
802
  const [prevCommands] = this.renderCommandHistory();
830
803
  const map = new Map();
831
804
  for (const command of prevCommands) {
832
- map.set(getRenderCommandId(command), command);
805
+ map.set(command.id, command);
833
806
  }
834
807
  return map;
835
808
  }, /* @ts-ignore */
836
809
  ...(ngDevMode ? [{ debugName: "previousRenderCommandMap" }] : /* istanbul ignore next */ []));
837
810
  this.renderInstructions = computed(() => {
838
811
  const commands = this.renderCommands();
839
- const previousRenderCommandMap = new Map(this.previousRenderCommandMap());
812
+ const previousRenderCommandMap = this.previousRenderCommandMap();
840
813
  const instructions = [];
841
- // remove deleted components as well as all other elements
842
- for (const [, command] of previousRenderCommandMap) {
843
- if (isComponentRenderCommand(command)) {
844
- // keep the component around if it's still in the new commands
845
- if (commands.some((c) => getRenderCommandId(c) === getRenderCommandId(command))) {
846
- continue;
814
+ // Decide, in document order, which commands survive the change with their DOM intact.
815
+ // A plain element or text node survives when the same id renders the same output at the same
816
+ // spot inside a surviving parent. Ancestors are decided first because parents precede their
817
+ // children in command order. Components survive on id alone — their DOM can be reattached.
818
+ const preserved = new Set();
819
+ const needsReattach = new Set();
820
+ const parentIdOf = (command) => {
821
+ if (command.nestingLevel === 0) {
822
+ return null;
823
+ }
824
+ for (let i = command.index - 1; i >= 0; i--) {
825
+ const cmd = commands[i];
826
+ if (cmd && cmd.nestingLevel === command.nestingLevel - 1 && cmd.kind === 'htmlOpen') {
827
+ return cmd.id;
847
828
  }
848
829
  }
849
- instructions.push([RENDER_INSTRUCTION_TYPE.DELETE, command]);
850
- // remove from map to mark as used
851
- previousRenderCommandMap.delete(getRenderCommandId(command));
852
- }
853
- // Find the indexes of the commands remaining in the previousRenderCommandMap in the new commands array.
854
- const lastComponentIndexes = [];
855
- for (const [id, command] of previousRenderCommandMap) {
856
- const index = commands.findIndex((c) => getRenderCommandId(c) === id);
857
- const newCommand = commands[index];
858
- if (index === -1) {
859
- throw new Error('Command not found!');
830
+ return null;
831
+ };
832
+ const markTagsEqual = (a, b) => a.length === b.length && a.every((tag, index) => tag === b[index]);
833
+ const attributesEqual = (a, b) => {
834
+ const aKeys = Object.keys(a);
835
+ return aKeys.length === Object.keys(b).length && aKeys.every((key) => a[key] === b[key]);
836
+ };
837
+ for (const command of commands) {
838
+ if (command.kind === 'htmlClose') {
839
+ continue;
860
840
  }
861
- lastComponentIndexes.push({
862
- newIndex: index,
863
- prevIndex: command[RENDER_COMMAND_POSITION.INDEX],
864
- command: newCommand,
865
- });
866
- }
867
- lastComponentIndexes.sort((a, b) => a.newIndex - b.newIndex);
868
- // check if the prevIndex are in order ascending
869
- // for indexes that are not in order, create a move instruction
870
- // for indexes that are in order, create an update instruction
871
- let moveCheckIndex = 0;
872
- while (moveCheckIndex < lastComponentIndexes.length - 1) {
873
- const moveCurr = lastComponentIndexes[moveCheckIndex];
874
- const moveNext = lastComponentIndexes[moveCheckIndex + 1];
875
- if (!moveCurr || !moveNext) {
876
- break;
841
+ const previous = previousRenderCommandMap.get(command.id);
842
+ if (!previous || previous.kind !== command.kind) {
843
+ continue;
877
844
  }
878
- if (moveCurr.prevIndex > moveNext.prevIndex) {
879
- lastComponentIndexes.splice(moveCheckIndex + 1, 1);
880
- instructions.push([RENDER_INSTRUCTION_TYPE.MOVE, moveNext.command]);
845
+ // The same key must render the same component class — a changed class means a fresh instance.
846
+ if (previous.kind === 'component' && command.kind === 'component' && previous.component !== command.component) {
847
+ continue;
881
848
  }
882
- else {
883
- instructions.push([RENDER_INSTRUCTION_TYPE.UPDATE, moveNext.command]);
884
- moveCheckIndex++;
849
+ const parentId = parentIdOf(command);
850
+ const parentPreserved = parentId === null || preserved.has(parentId);
851
+ if (command.kind === 'component') {
852
+ preserved.add(command.id);
853
+ const samePlace = parentPreserved &&
854
+ previous.nestingLevel === command.nestingLevel &&
855
+ previous.domPosition === command.domPosition;
856
+ if (!samePlace) {
857
+ needsReattach.add(command.id);
858
+ }
859
+ continue;
860
+ }
861
+ const sameOutput = previous.kind === command.kind &&
862
+ (previous.kind !== 'htmlOpen' || previous.tagName === command.tagName) &&
863
+ (previous.kind !== 'text' ||
864
+ (previous.text === command.text &&
865
+ markTagsEqual(previous.markTags, command.markTags)));
866
+ if (parentPreserved &&
867
+ sameOutput &&
868
+ previous.nestingLevel === command.nestingLevel &&
869
+ previous.domPosition === command.domPosition &&
870
+ attributesEqual(previous.attributes, command.attributes)) {
871
+ preserved.add(command.id);
872
+ }
873
+ }
874
+ // Everything that did not survive is removed first, so the ordered pass below can rebuild
875
+ // into a clean tree. Close commands never render DOM and are skipped throughout.
876
+ for (const [id, command] of previousRenderCommandMap) {
877
+ if (command.kind !== 'htmlClose' && !preserved.has(id)) {
878
+ instructions.push({ type: 'delete', command });
885
879
  }
886
880
  }
881
+ // One pass in document order: parents are created before a child component is reattached.
882
+ // Surviving plain elements still get an update so the cache tracks their fresh command data.
887
883
  for (const command of commands) {
888
- const id = getRenderCommandId(command);
889
- if (isComponentRenderCommand(command)) {
890
- const previousCommand = previousRenderCommandMap.get(id);
891
- if (!previousCommand) {
892
- // create
893
- instructions.push([RENDER_INSTRUCTION_TYPE.CREATE, command]);
894
- }
884
+ if (command.kind === 'htmlClose') {
885
+ continue;
886
+ }
887
+ if (!preserved.has(command.id)) {
888
+ instructions.push({ type: 'create', command });
889
+ }
890
+ else if (command.kind === 'component' && needsReattach.has(command.id)) {
891
+ instructions.push({ type: 'move', command });
895
892
  }
896
893
  else {
897
- // create
898
- instructions.push([RENDER_INSTRUCTION_TYPE.CREATE, command]);
894
+ instructions.push({ type: 'update', command });
899
895
  }
900
896
  }
901
897
  return instructions;
902
898
  }, /* @ts-ignore */
903
899
  ...(ngDevMode ? [{ debugName: "renderInstructions" }] : /* istanbul ignore next */ []));
904
- toObservable(this.renderInstructions)
905
- .pipe(takeUntilDestroyed(), tap((instructions) => {
906
- this._execInstructions(instructions);
907
- }))
908
- .subscribe();
900
+ effect(() => {
901
+ const instructions = this.renderInstructions();
902
+ untracked(() => this.execInstructions(instructions));
903
+ });
909
904
  }
910
- _createRenderCommands(richTextData) {
905
+ createRenderCommands(richTextData) {
911
906
  /** List of all render commands */
912
907
  const rootCommands = [];
913
908
  /** Counter for generating unique html element IDs. */
@@ -925,32 +920,26 @@ class ContentfulRichTextRendererComponent {
925
920
  const traverse = (node) => {
926
921
  switch (node.nodeType) {
927
922
  case 'text': {
928
- // Render media
929
- const type = RENDER_COMMAND_TYPE.TEXT;
930
923
  const text = node.value;
931
924
  if (!text)
932
925
  break;
933
926
  const attributes = {
934
927
  class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
935
928
  };
936
- if (node.marks.length) {
937
- const markClasses = marksToClass(node.marks);
938
- if (CLASS_ATTR in attributes) {
939
- attributes[CLASS_ATTR] += ` ${markClasses}`;
940
- }
941
- else {
942
- attributes[CLASS_ATTR] = markClasses;
943
- }
944
- }
945
- const id = 't' + textId++;
946
- const command = [type, nestingLevel, domPosition, commandIndex++, attributes, text, id];
947
- rootCommands.push(command);
929
+ rootCommands.push({
930
+ kind: 'text',
931
+ nestingLevel,
932
+ domPosition,
933
+ index: commandIndex++,
934
+ attributes,
935
+ markTags: node.marks.length ? marksToTags(node.marks) : [],
936
+ text,
937
+ id: 't' + textId++,
938
+ });
948
939
  domPosition++;
949
940
  break;
950
941
  }
951
- case BLOCKS.EMBEDDED_ASSET: {
952
- // Render media
953
- const type = RENDER_COMMAND_TYPE.COMPONENT;
942
+ case CF_BLOCKS.EMBEDDED_ASSET: {
954
943
  const assetId = node.data['target']?.sys?.id;
955
944
  if (!assetId) {
956
945
  throw richTextRendererError('asset_id_not_found', { node });
@@ -960,7 +949,7 @@ class ContentfulRichTextRendererComponent {
960
949
  throw richTextRendererError('asset_not_found', { assetId, node });
961
950
  }
962
951
  const contentType = asset.fields.file.contentType;
963
- const assetComponents = this._config.components;
952
+ const assetComponents = this.config.components;
964
953
  // Every property inside the asset will be null if no file was provided for a translation.
965
954
  // In this case, we can assume that the asset is missing due to user error.
966
955
  const isMissing = !contentType && !asset.fields.file.url;
@@ -980,35 +969,29 @@ class ContentfulRichTextRendererComponent {
980
969
  : isAudio
981
970
  ? assetComponents.audio
982
971
  : assetComponents.file;
983
- const componentType = isImage
984
- ? DEFAULT_COMPONENT_TYPES.IMAGE
985
- : isVideo
986
- ? DEFAULT_COMPONENT_TYPES.VIDEO
987
- : isAudio
988
- ? DEFAULT_COMPONENT_TYPES.AUDIO
989
- : DEFAULT_COMPONENT_TYPES.FILE;
990
- let componentId = componentIdMap.get(componentType) ?? -1;
991
- const id = componentType + ++componentId;
992
- componentIdMap.set(componentType, componentId);
993
- const attributes = {};
994
- const inputs = {
995
- asset,
996
- };
997
- const command = [
998
- type,
972
+ if (!component) {
973
+ if (isDevMode()) {
974
+ console.warn('No component registered for this embedded asset! The asset will be skipped. Provide one via provideContentfulConfig({ components: … }).', { asset });
975
+ }
976
+ break;
977
+ }
978
+ const occurrenceKey = 'asset:' + assetId;
979
+ const occurrence = (componentIdMap.get(occurrenceKey) ?? -1) + 1;
980
+ componentIdMap.set(occurrenceKey, occurrence);
981
+ const id = occurrence === 0 ? occurrenceKey : `${occurrenceKey}:${occurrence}`;
982
+ rootCommands.push({
983
+ kind: 'component',
999
984
  nestingLevel,
1000
985
  domPosition,
1001
- commandIndex++,
1002
- attributes,
986
+ index: commandIndex++,
1003
987
  component,
1004
- inputs,
988
+ inputs: { asset },
1005
989
  id,
1006
- ];
1007
- rootCommands.push(command);
990
+ });
1008
991
  domPosition++;
1009
992
  break;
1010
993
  }
1011
- case INLINES.HYPERLINK: {
994
+ case CF_INLINES.HYPERLINK: {
1012
995
  const uri = node.data['uri'] ?? '';
1013
996
  // Collect all text and marks from children (hyperlink children are text nodes)
1014
997
  let linkText = '';
@@ -1019,28 +1002,67 @@ class ContentfulRichTextRendererComponent {
1019
1002
  linkMarks.push(...child.marks);
1020
1003
  }
1021
1004
  }
1005
+ const linkComponent = this.config.components.link;
1006
+ if (!linkComponent) {
1007
+ // Without a link component the hyperlink still renders - as a plain anchor.
1008
+ rootCommands.push({
1009
+ kind: 'htmlOpen',
1010
+ nestingLevel,
1011
+ domPosition,
1012
+ index: commandIndex++,
1013
+ attributes: {
1014
+ class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a',
1015
+ href: uri,
1016
+ },
1017
+ tagName: 'a',
1018
+ id: 'e-o' + elementOpenId++,
1019
+ });
1020
+ const anchorDomPosition = domPosition;
1021
+ nestingLevel++;
1022
+ domPosition = 0;
1023
+ rootCommands.push({
1024
+ kind: 'text',
1025
+ nestingLevel,
1026
+ domPosition,
1027
+ index: commandIndex++,
1028
+ attributes: {
1029
+ class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
1030
+ },
1031
+ markTags: marksToTags(linkMarks),
1032
+ text: linkText,
1033
+ id: 't' + textId++,
1034
+ });
1035
+ nestingLevel--;
1036
+ domPosition = anchorDomPosition;
1037
+ rootCommands.push({
1038
+ kind: 'htmlClose',
1039
+ nestingLevel,
1040
+ domPosition,
1041
+ index: commandIndex++,
1042
+ tagName: 'a',
1043
+ id: 'e-c' + elementCloseId++,
1044
+ });
1045
+ domPosition++;
1046
+ break;
1047
+ }
1022
1048
  const textClass = linkMarks.length ? marksToClass(linkMarks) : '';
1023
- let linkComponentId = componentIdMap.get(DEFAULT_COMPONENT_TYPES.LINK) ?? -1;
1024
- const linkId = DEFAULT_COMPONENT_TYPES.LINK + ++linkComponentId;
1025
- componentIdMap.set(DEFAULT_COMPONENT_TYPES.LINK, linkComponentId);
1026
- const linkCommand = [
1027
- RENDER_COMMAND_TYPE.COMPONENT,
1049
+ let linkComponentId = componentIdMap.get(LINK_COMPONENT_TYPE) ?? -1;
1050
+ const linkId = LINK_COMPONENT_TYPE + ++linkComponentId;
1051
+ componentIdMap.set(LINK_COMPONENT_TYPE, linkComponentId);
1052
+ rootCommands.push({
1053
+ kind: 'component',
1028
1054
  nestingLevel,
1029
1055
  domPosition,
1030
- commandIndex++,
1031
- {},
1032
- this._config.components.link,
1033
- { href: uri, text: linkText, textClass },
1034
- linkId,
1035
- ];
1036
- rootCommands.push(linkCommand);
1056
+ index: commandIndex++,
1057
+ component: linkComponent,
1058
+ inputs: { href: uri, text: linkText, textClass },
1059
+ id: linkId,
1060
+ });
1037
1061
  domPosition++;
1038
1062
  break;
1039
1063
  }
1040
- case BLOCKS.EMBEDDED_ENTRY:
1041
- case INLINES.EMBEDDED_ENTRY: {
1042
- // Render component
1043
- const type = RENDER_COMMAND_TYPE.COMPONENT;
1064
+ case CF_BLOCKS.EMBEDDED_ENTRY:
1065
+ case CF_INLINES.EMBEDDED_ENTRY: {
1044
1066
  const entryId = node.data['target']?.sys?.id;
1045
1067
  if (!entryId) {
1046
1068
  throw richTextRendererError('entry_id_not_found', { node });
@@ -1050,47 +1072,49 @@ class ContentfulRichTextRendererComponent {
1050
1072
  throw richTextRendererError('entry_not_found', { entryId, node });
1051
1073
  }
1052
1074
  const componentType = entry.sys.contentType.sys.id;
1053
- const component = this._config.customComponents[componentType];
1075
+ const component = this.config.customComponents[componentType];
1054
1076
  if (!component) {
1055
1077
  throw richTextRendererError('custom_component_not_found', {
1056
1078
  componentType,
1057
- customComponents: this._config.customComponents,
1079
+ customComponents: this.config.customComponents,
1058
1080
  entry,
1059
1081
  });
1060
1082
  }
1061
- const attributes = {};
1062
- let componentId = componentIdMap.get(componentType) ?? -1;
1063
- const id = componentType + ++componentId;
1064
- componentIdMap.set(componentType, componentId);
1065
- const inputs = {
1066
- fields: entry.fields,
1067
- metadata: entry.metadata,
1068
- sys: entry.sys,
1069
- includes: this.contentIncludesMap(),
1070
- };
1071
- const command = [
1072
- type,
1083
+ const occurrenceKey = 'entry:' + entryId;
1084
+ const occurrence = (componentIdMap.get(occurrenceKey) ?? -1) + 1;
1085
+ componentIdMap.set(occurrenceKey, occurrence);
1086
+ const id = occurrence === 0 ? occurrenceKey : `${occurrenceKey}:${occurrence}`;
1087
+ rootCommands.push({
1088
+ kind: 'component',
1073
1089
  nestingLevel,
1074
1090
  domPosition,
1075
- commandIndex++,
1076
- attributes,
1091
+ index: commandIndex++,
1077
1092
  component,
1078
- inputs,
1093
+ inputs: {
1094
+ fields: entry.fields,
1095
+ metadata: entry.metadata,
1096
+ sys: entry.sys,
1097
+ includes: this.contentIncludesMap(),
1098
+ },
1079
1099
  id,
1080
- ];
1081
- rootCommands.push(command);
1100
+ });
1082
1101
  domPosition++;
1083
1102
  break;
1084
1103
  }
1085
1104
  default: {
1086
- const type = RENDER_COMMAND_TYPE.HTML_OPEN;
1087
1105
  const tag = translateContentfulNodeTypeToHtmlTag(node.nodeType);
1088
1106
  const attributes = {
1089
1107
  class: `et-contentful-rich-text-default-element et-contentful-rich-text-default-${tag}`,
1090
1108
  };
1091
- const id = 'e-o' + elementOpenId++;
1092
- const command = [type, nestingLevel, domPosition, commandIndex++, attributes, tag, id];
1093
- rootCommands.push(command);
1109
+ rootCommands.push({
1110
+ kind: 'htmlOpen',
1111
+ nestingLevel,
1112
+ domPosition,
1113
+ index: commandIndex++,
1114
+ attributes,
1115
+ tagName: tag,
1116
+ id: 'e-o' + elementOpenId++,
1117
+ });
1094
1118
  const domPositionAtThisLevel = domPosition;
1095
1119
  // Normal html elements can have children
1096
1120
  for (const child of node.content) {
@@ -1101,25 +1125,21 @@ class ContentfulRichTextRendererComponent {
1101
1125
  domPosition = domPositionAtThisLevel;
1102
1126
  }
1103
1127
  const lastCommand = rootCommands[rootCommands.length - 1];
1104
- if (lastCommand?.[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_OPEN &&
1105
- lastCommand[HTML_OPEN_RENDER_COMMAND_POSITION.TAG_NAME] !== 'td' &&
1106
- lastCommand[HTML_OPEN_RENDER_COMMAND_POSITION.TAG_NAME] !== 'hr') {
1128
+ if (lastCommand?.kind === 'htmlOpen' && lastCommand.tagName !== 'td' && lastCommand.tagName !== 'hr') {
1107
1129
  // If the last command is an open command, we can remove it since it's empty
1108
1130
  rootCommands.pop();
1109
1131
  elementOpenId--;
1110
1132
  commandIndex--;
1111
1133
  }
1112
1134
  else {
1113
- const closeId = 'e-c' + elementCloseId++;
1114
- const closeCommand = [
1115
- RENDER_COMMAND_TYPE.HTML_CLOSE,
1135
+ rootCommands.push({
1136
+ kind: 'htmlClose',
1116
1137
  nestingLevel,
1117
1138
  domPosition,
1118
- commandIndex++,
1119
- tag,
1120
- closeId,
1121
- ];
1122
- rootCommands.push(closeCommand);
1139
+ index: commandIndex++,
1140
+ tagName: tag,
1141
+ id: 'e-c' + elementCloseId++,
1142
+ });
1123
1143
  domPosition++;
1124
1144
  }
1125
1145
  break;
@@ -1131,194 +1151,171 @@ class ContentfulRichTextRendererComponent {
1131
1151
  }
1132
1152
  return rootCommands;
1133
1153
  }
1134
- _execInstructions(instructions) {
1154
+ execInstructions(instructions) {
1135
1155
  for (const instruction of instructions) {
1136
- const type = instruction[RENDER_INSTRUCTION_POSITION.TYPE];
1137
- const command = instruction[RENDER_INSTRUCTION_POSITION.COMMAND];
1138
- switch (type) {
1139
- case RENDER_INSTRUCTION_TYPE.CREATE:
1140
- this._runCreateInstruction(command);
1156
+ switch (instruction.type) {
1157
+ case 'create':
1158
+ this.runCreateInstruction(instruction.command);
1141
1159
  break;
1142
- case RENDER_INSTRUCTION_TYPE.UPDATE:
1143
- this._runUpdateInstruction(command);
1160
+ case 'update':
1161
+ this.runUpdateInstruction(instruction.command);
1144
1162
  break;
1145
- case RENDER_INSTRUCTION_TYPE.MOVE:
1146
- this._runMoveInstruction(command);
1163
+ case 'move':
1164
+ this.runMoveInstruction(instruction.command);
1147
1165
  break;
1148
- case RENDER_INSTRUCTION_TYPE.DELETE:
1149
- this._runDeleteInstruction(command);
1166
+ case 'delete':
1167
+ this.runDeleteInstruction(instruction.command);
1150
1168
  break;
1151
1169
  }
1152
1170
  }
1153
1171
  }
1154
- _runCreateInstruction(command) {
1155
- const id = getRenderCommandId(command);
1156
- const parentElement = this._findParent(command);
1157
- const nextElement = this._findFollowingElement(command);
1158
- if (isComponentRenderCommand(command)) {
1159
- const componentRef = this._viewContainerRef.createComponent(command[COMPONENT_RENDER_COMMAND_POSITION.COMPONENT]);
1160
- this._updateComponentInputs(command, componentRef);
1161
- const rootNode = this._getComponentRootNode(componentRef);
1162
- this._renderInsertOrAppend(rootNode, parentElement, nextElement);
1163
- this._executedCommandsCache.set(id, {
1172
+ runCreateInstruction(command) {
1173
+ const parentElement = this.findParent(command);
1174
+ const nextElement = this.findFollowingElement(command);
1175
+ if (command.kind === 'component') {
1176
+ const inputs = signal(command.inputs, /* @ts-ignore */
1177
+ ...(ngDevMode ? [{ debugName: "inputs" }] : /* istanbul ignore next */ []));
1178
+ // Components may declare any subset of the offered inputs — only bind the declared ones.
1179
+ const declaredInputs = reflectComponentType(command.component)?.inputs ?? [];
1180
+ const bindings = declaredInputs
1181
+ .filter((declared) => declared.propName in command.inputs)
1182
+ .map((declared) => inputBinding(declared.templateName, () => inputs()[declared.propName]));
1183
+ const componentRef = this.viewContainerRef.createComponent(command.component, { bindings });
1184
+ const rootNode = this.getComponentRootNode(componentRef);
1185
+ this.renderInsertOrAppend(rootNode, parentElement, nextElement);
1186
+ this.executedCommandsCache.set(command.id, {
1164
1187
  command,
1165
1188
  componentRef,
1189
+ inputs,
1166
1190
  element: rootNode,
1167
1191
  });
1168
1192
  }
1169
- else if (isTextRenderCommand(command)) {
1170
- const text = command[TEXT_RENDER_COMMAND_POSITION.TEXT];
1171
- const span = this._renderer.createElement('span');
1172
- const attributes = command[TEXT_RENDER_COMMAND_POSITION.ATTRIBUTES];
1173
- const textSplitInLineBreaks = text.split('\n').filter((t) => t.trim().length > 0);
1174
- const textNodes = [];
1175
- for (const [key, value] of Object.entries(attributes)) {
1176
- this._renderer.setAttribute(span, key, value);
1193
+ else if (command.kind === 'text') {
1194
+ const span = this.renderer.createElement('span');
1195
+ const textSplitInLineBreaks = command.text.split('\n').filter((t) => t.trim().length > 0);
1196
+ for (const [key, value] of Object.entries(command.attributes)) {
1197
+ this.renderer.setAttribute(span, key, value);
1177
1198
  }
1178
- if (text.startsWith('\n')) {
1179
- const brNode = this._renderer.createElement('br');
1180
- this._renderer.appendChild(parentElement, brNode);
1199
+ if (command.text.startsWith('\n')) {
1200
+ const brNode = this.renderer.createElement('br');
1201
+ this.renderer.appendChild(parentElement, brNode);
1202
+ }
1203
+ // Marks are rendered as nested semantic elements inside the span, outermost mark first.
1204
+ let textContainer = span;
1205
+ for (const tag of command.markTags) {
1206
+ const markElement = this.renderer.createElement(tag);
1207
+ this.renderer.appendChild(textContainer, markElement);
1208
+ textContainer = markElement;
1181
1209
  }
1182
1210
  for (const [textPartIndex, textPart] of textSplitInLineBreaks.entries()) {
1183
1211
  if (textPartIndex > 0) {
1184
- const brNode = this._renderer.createElement('br');
1185
- this._renderer.appendChild(span, brNode);
1212
+ const brNode = this.renderer.createElement('br');
1213
+ this.renderer.appendChild(textContainer, brNode);
1186
1214
  }
1187
- const textNode = this._renderer.createText(textPart);
1188
- this._renderer.appendChild(span, textNode);
1189
- textNodes.push(textNode);
1215
+ const textNode = this.renderer.createText(textPart);
1216
+ this.renderer.appendChild(textContainer, textNode);
1190
1217
  }
1191
- this._renderInsertOrAppend(span, parentElement, nextElement);
1192
- this._executedCommandsCache.set(id, {
1218
+ this.renderInsertOrAppend(span, parentElement, nextElement);
1219
+ this.executedCommandsCache.set(command.id, {
1193
1220
  command,
1194
1221
  element: span,
1195
- textNode: textNodes,
1196
1222
  });
1197
1223
  }
1198
- else if (isHtmlOpenRenderCommand(command)) {
1199
- const tag = command[HTML_OPEN_RENDER_COMMAND_POSITION.TAG_NAME];
1200
- const attributes = command[HTML_OPEN_RENDER_COMMAND_POSITION.ATTRIBUTES];
1201
- const element = this._renderer.createElement(tag);
1202
- for (const [key, value] of Object.entries(attributes)) {
1203
- this._renderer.setAttribute(element, key, value);
1224
+ else if (command.kind === 'htmlOpen') {
1225
+ const element = this.renderer.createElement(command.tagName);
1226
+ for (const [key, value] of Object.entries(command.attributes)) {
1227
+ this.renderer.setAttribute(element, key, value);
1204
1228
  }
1205
- this._renderInsertOrAppend(element, parentElement, nextElement);
1206
- this._executedCommandsCache.set(id, {
1229
+ this.renderInsertOrAppend(element, parentElement, nextElement);
1230
+ this.executedCommandsCache.set(command.id, {
1207
1231
  command,
1208
1232
  element,
1209
1233
  });
1210
1234
  }
1211
1235
  }
1212
- _runUpdateInstruction(command) {
1213
- const id = getRenderCommandId(command);
1214
- const cached = this._executedCommandsCache.get(id);
1236
+ runUpdateInstruction(command) {
1237
+ const cached = this.executedCommandsCache.get(command.id);
1215
1238
  if (!cached) {
1216
1239
  throw new Error('Cached command not found!');
1217
1240
  }
1218
- if (isComponentRenderCommand(command)) {
1241
+ if (command.kind === 'component') {
1219
1242
  if (!isExecutedComponentCommandCacheItem(cached)) {
1220
1243
  throw new Error('Cached command is not a component command!');
1221
1244
  }
1222
- const componentRef = cached.componentRef;
1223
- if (!componentRef) {
1224
- throw new Error('Component ref not found!');
1225
- }
1226
- this._updateComponentInputs(command, componentRef);
1245
+ cached.inputs.set(command.inputs);
1227
1246
  }
1228
- this._executedCommandsCache.set(id, {
1247
+ // `cached` and the fresh `command` share the same id and therefore the same union variant,
1248
+ // but TS can't correlate the two after the spread — assert the merged object as the union.
1249
+ this.executedCommandsCache.set(command.id, {
1229
1250
  ...cached,
1230
- command: command,
1251
+ command,
1231
1252
  });
1232
1253
  }
1233
- _runMoveInstruction(command) {
1234
- const id = getRenderCommandId(command);
1235
- const cached = this._executedCommandsCache.get(id);
1254
+ runMoveInstruction(command) {
1255
+ const cached = this.executedCommandsCache.get(command.id);
1236
1256
  if (!cached) {
1237
1257
  throw new Error('Cached command not found!');
1238
1258
  }
1239
- if (isComponentRenderCommand(command)) {
1259
+ if (command.kind === 'component') {
1240
1260
  if (!isExecutedComponentCommandCacheItem(cached)) {
1241
1261
  throw new Error('Cached command is not a component command!');
1242
1262
  }
1243
- const componentRef = cached.componentRef;
1244
- if (!componentRef) {
1245
- throw new Error('Component ref not found!');
1246
- }
1247
1263
  const rootNode = cached.element;
1248
1264
  const oldParentElement = cached.element.parentElement;
1249
1265
  if (oldParentElement) {
1250
- this._renderer.removeChild(oldParentElement, rootNode);
1266
+ this.renderer.removeChild(oldParentElement, rootNode);
1251
1267
  }
1252
- const newParentElement = this._findParent(command);
1253
- const nextElement = this._findFollowingElement(command);
1254
- this._renderInsertOrAppend(rootNode, newParentElement, nextElement);
1255
- this._updateComponentInputs(command, componentRef);
1256
- this._executedCommandsCache.set(id, {
1268
+ const newParentElement = this.findParent(command);
1269
+ const nextElement = this.findFollowingElement(command);
1270
+ this.renderInsertOrAppend(rootNode, newParentElement, nextElement);
1271
+ cached.inputs.set(command.inputs);
1272
+ this.executedCommandsCache.set(command.id, {
1257
1273
  ...cached,
1258
- command: command,
1274
+ command,
1259
1275
  });
1260
1276
  }
1261
1277
  }
1262
- _runDeleteInstruction(command) {
1263
- const id = getRenderCommandId(command);
1264
- const cached = this._executedCommandsCache.get(id);
1278
+ runDeleteInstruction(command) {
1279
+ const cached = this.executedCommandsCache.get(command.id);
1265
1280
  if (!cached) {
1266
- if (isHtmlCloseRenderCommand(command)) {
1281
+ if (command.kind === 'htmlClose') {
1267
1282
  return;
1268
1283
  }
1269
1284
  throw new Error('Cached command not found!');
1270
1285
  }
1271
- if (isComponentRenderCommand(command)) {
1286
+ if (command.kind === 'component') {
1272
1287
  if (!isExecutedComponentCommandCacheItem(cached)) {
1273
1288
  throw new Error('Cached command is not a component command!');
1274
1289
  }
1275
- cached.componentRef?.destroy();
1276
- }
1277
- else if (isTextRenderCommand(command)) {
1278
- if (cached.element?.parentElement) {
1279
- this._renderer.removeChild(cached.element.parentElement, cached.element);
1280
- }
1290
+ cached.componentRef.destroy();
1281
1291
  }
1282
- else if (isHtmlOpenRenderCommand(command)) {
1283
- if (cached.element?.parentElement) {
1284
- this._renderer.removeChild(cached.element.parentElement, cached.element);
1292
+ else if (command.kind === 'text' || command.kind === 'htmlOpen') {
1293
+ if (cached.element.parentElement) {
1294
+ this.renderer.removeChild(cached.element.parentElement, cached.element);
1285
1295
  }
1286
1296
  }
1287
- this._executedCommandsCache.delete(id);
1297
+ this.executedCommandsCache.delete(command.id);
1288
1298
  }
1289
- _getComponentRootNode(componentRef) {
1299
+ getComponentRootNode(componentRef) {
1290
1300
  return componentRef.hostView.rootNodes[0];
1291
1301
  }
1292
- _updateComponentInputs(command, componentRef) {
1293
- const inputMeta = reflectComponentType(command[COMPONENT_RENDER_COMMAND_POSITION.COMPONENT])?.inputs ?? [];
1294
- const inputsByProp = new Map(inputMeta.map((i) => [i.propName, i.templateName]));
1295
- for (const [key, value] of Object.entries(command[COMPONENT_RENDER_COMMAND_POSITION.INPUTS])) {
1296
- const publicName = inputsByProp.get(key);
1297
- if (publicName === undefined)
1298
- continue;
1299
- componentRef.setInput(publicName, value);
1300
- }
1301
- }
1302
- _findParent(command) {
1303
- const hostElement = this._elementRef.nativeElement;
1304
- const nestingLevel = command[RENDER_COMMAND_POSITION.NESTING_LEVEL];
1302
+ findParent(command) {
1303
+ const hostElement = this.elementRef.nativeElement;
1305
1304
  let parentElement;
1306
- if (nestingLevel === 0) {
1305
+ if (command.nestingLevel === 0) {
1307
1306
  parentElement = hostElement;
1308
1307
  }
1309
1308
  else {
1310
- // Reverse search all render commands beginning from the current one using a for loop.
1311
- // The parent is found when the command's nesting level ist the same as the current nesting level minus 1.
1312
- // If the parent is not found, throw an error.
1309
+ // Reverse search all render commands beginning from the current one.
1310
+ // The parent is the closest preceding html open command one nesting level up.
1313
1311
  const allCommands = this.renderCommands();
1314
- const currentCommandIndex = command[RENDER_COMMAND_POSITION.INDEX];
1315
1312
  let parentCommand = null;
1316
- for (let i = currentCommandIndex - 1; i >= 0; i--) {
1313
+ for (let i = command.index - 1; i >= 0; i--) {
1317
1314
  const cmd = allCommands[i];
1318
1315
  if (!cmd) {
1319
1316
  throw new Error('Command not found!');
1320
1317
  }
1321
- if (cmd[RENDER_COMMAND_POSITION.NESTING_LEVEL] === nestingLevel - 1 && isHtmlOpenRenderCommand(cmd)) {
1318
+ if (cmd.nestingLevel === command.nestingLevel - 1 && cmd.kind === 'htmlOpen') {
1322
1319
  parentCommand = cmd;
1323
1320
  break;
1324
1321
  }
@@ -1326,51 +1323,46 @@ class ContentfulRichTextRendererComponent {
1326
1323
  if (!parentCommand) {
1327
1324
  throw new Error('Parent command not found!');
1328
1325
  }
1329
- parentElement = this._executedCommandsCache.get(getRenderCommandId(parentCommand))?.element;
1326
+ parentElement = this.executedCommandsCache.get(parentCommand.id)?.element;
1330
1327
  }
1331
1328
  if (!parentElement) {
1332
1329
  throw new Error('Parent element not found!');
1333
1330
  }
1334
1331
  return parentElement;
1335
1332
  }
1336
- _findFollowingElement(command) {
1337
- const cacheArray = Array.from(this._executedCommandsCache.values());
1338
- let nextElement = undefined;
1339
- const domPosition = command[RENDER_COMMAND_POSITION.DOM_POSITION];
1340
- const nestingLevel = command[RENDER_COMMAND_POSITION.NESTING_LEVEL];
1341
- const type = command[RENDER_COMMAND_POSITION.TYPE];
1342
- // Try to find an already rendered element with a domPosition that is greater than the current domPosition. If there are multiple, use the one with the smallest domPosition.
1343
- // If it does not exist, use .appendChild to simply append the element to the end of the parent element.
1344
- // If it does exist, use .insertBefore to insert the element before the found element.
1345
- for (const cached of cacheArray) {
1346
- if (cached.command[RENDER_COMMAND_POSITION.DOM_POSITION] > domPosition &&
1347
- cached.command[RENDER_COMMAND_POSITION.NESTING_LEVEL] === nestingLevel &&
1348
- // Make sure the element does not find itself
1349
- cached.command[RENDER_COMMAND_POSITION.TYPE] !== type) {
1333
+ findFollowingElement(command) {
1334
+ // Find the closest already rendered element at the same nesting level with a greater
1335
+ // domPosition, to insert before. Without one the element is appended to the parent.
1336
+ let nextElement;
1337
+ let nextDomPosition = Infinity;
1338
+ for (const cached of this.executedCommandsCache.values()) {
1339
+ if (cached.command.domPosition > command.domPosition &&
1340
+ cached.command.domPosition < nextDomPosition &&
1341
+ cached.command.nestingLevel === command.nestingLevel &&
1342
+ cached.command.id !== command.id) {
1350
1343
  nextElement = cached.element;
1351
- break;
1344
+ nextDomPosition = cached.command.domPosition;
1352
1345
  }
1353
1346
  }
1354
1347
  return nextElement;
1355
1348
  }
1356
- _renderInsertOrAppend(nodeToRender, parentElement, nextElement) {
1349
+ renderInsertOrAppend(nodeToRender, parentElement, nextElement) {
1357
1350
  if (nextElement) {
1358
- this._renderer.insertBefore(parentElement, nodeToRender, nextElement);
1351
+ this.renderer.insertBefore(parentElement, nodeToRender, nextElement);
1359
1352
  }
1360
1353
  else {
1361
- this._renderer.appendChild(parentElement, nodeToRender);
1354
+ this.renderer.appendChild(parentElement, nodeToRender);
1362
1355
  }
1363
1356
  }
1364
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1365
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.5", type: ContentfulRichTextRendererComponent, isStandalone: true, selector: "et-contentful-rich-text-renderer", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: true, transformFunction: null }, richTextPath: { classPropertyName: "richTextPath", publicName: "richTextPath", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-contentful-rich-text-renderer" }, ngImport: i0, template: ``, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
1357
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1358
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: ContentfulRichTextRendererComponent, isStandalone: true, selector: "et-contentful-rich-text-renderer", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: true, transformFunction: null }, richTextPath: { classPropertyName: "richTextPath", publicName: "richTextPath", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-contentful-rich-text-renderer" }, ngImport: i0, template: ``, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
1366
1359
  }
1367
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
1360
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
1368
1361
  type: Component,
1369
1362
  args: [{
1370
1363
  selector: 'et-contentful-rich-text-renderer',
1371
1364
  template: ``,
1372
1365
  encapsulation: ViewEncapsulation.None,
1373
- changeDetection: ChangeDetectionStrategy.OnPush,
1374
1366
  host: {
1375
1367
  class: 'et-contentful-rich-text-renderer',
1376
1368
  },
@@ -1390,11 +1382,9 @@ const provideContentfulConfig = (contentfulConfig = {}) => {
1390
1382
  };
1391
1383
  const isContentfulEntryType = (entry, type) => entry.sys.contentType.sys.id === type;
1392
1384
 
1393
- // Burst cache
1394
-
1395
1385
  /**
1396
1386
  * Generated bundle index. Do not edit.
1397
1387
  */
1398
1388
 
1399
- export { COMPONENT_RENDER_COMMAND_POSITION, CONTENTFUL_CONFIG, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, HTML_CLOSE_RENDER_COMMAND_POSITION, HTML_OPEN_RENDER_COMMAND_POSITION, RENDER_COMMAND_POSITION, RENDER_COMMAND_TYPE, RENDER_INSTRUCTION_POSITION, RENDER_INSTRUCTION_TYPE, TEXT_RENDER_COMMAND_POSITION, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, getRenderCommandId, isComponentRenderCommand, isContentfulEntryType, isContentfulGqlAsset, isExecutedComponentCommandCacheItem, isExecutedHtmlOrTextCommandCacheItem, isExecutedTextCommandCacheItem, isHtmlCloseRenderCommand, isHtmlOpenRenderCommand, isRichTextRootNode, isTextRenderCommand, marksToClass, parseContentfulImageSize, provideContentfulConfig, translateContentfulNodeTypeToHtmlTag };
1389
+ export { CONTENTFUL_CONFIG, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, injectContentfulConfig, isContentfulEntryType, isContentfulGqlAsset, isRichTextRootNode, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, translateContentfulNodeTypeToHtmlTag };
1400
1390
  //# sourceMappingURL=ethlete-contentful.mjs.map