@ethlete/contentful 4.0.0-next.1 → 4.0.0-next.11

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