@ethlete/contentful 4.0.0-next.6 → 4.0.0-next.8

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