@ethlete/contentful 4.0.0-next.0 → 4.0.0-next.10

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