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