@ethlete/contentful 4.0.0-next.12 → 4.0.0-next.14

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,8 +1,8 @@
1
- import { NgClass, DOCUMENT } from '@angular/common';
2
1
  import * as i0 from '@angular/core';
3
- import { input, computed, ViewEncapsulation, Component, InjectionToken, inject, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
2
+ import { input, computed, ViewEncapsulation, Component, InjectionToken, inject, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, Injector, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
4
3
  import { formatFileSize, normalizePictureSizes, PictureComponent } from '@ethlete/components';
5
- import { defineLabels, toProvideFn, toInjectFn, toToken, injectUrl, RuntimeError, isObject, injectRenderer, getObjectProperty } from '@ethlete/core';
4
+ import { defineLabels, toProvideFn, toInjectFn, toToken, injectUrl, RuntimeError, isObject, getObjectProperty, injectRenderer } from '@ethlete/core';
5
+ import { DOCUMENT, Location } from '@angular/common';
6
6
  import { Router, RouterLink } from '@angular/router';
7
7
 
8
8
  const GQL_FRAGMENT_CONTENTFUL_ASSET = `
@@ -29,12 +29,6 @@ class ContentfulAudioComponent {
29
29
  constructor() {
30
30
  this.asset = input.required(/* @ts-ignore */
31
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 */ []));
38
32
  this.data = computed(() => {
39
33
  const asset = this.asset();
40
34
  if (!asset) {
@@ -57,14 +51,16 @@ class ContentfulAudioComponent {
57
51
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
58
52
  }
59
53
  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: `
54
+ 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 } }, host: { classAttribute: "et-contentful-audio" }, ngImport: i0, template: `
61
55
  @if (data(); as data) {
62
- <figure [ngClass]="figureClass()">
63
- <figcaption [ngClass]="figcaptionClass()">{{ data.title }}</figcaption>
64
- <audio [ngClass]="audioClass()" [src]="data.url" controls></audio>
56
+ <figure class="et-contentful-audio-figure">
57
+ @if (data.title) {
58
+ <figcaption class="et-contentful-audio-figcaption">{{ data.title }}</figcaption>
59
+ }
60
+ <audio [src]="data.url" class="et-contentful-audio-audio" controls></audio>
65
61
  </figure>
66
62
  }
67
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
63
+ `, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
68
64
  }
69
65
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulAudioComponent, decorators: [{
70
66
  type: Component,
@@ -72,19 +68,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
72
68
  selector: 'et-contentful-audio',
73
69
  template: `
74
70
  @if (data(); as data) {
75
- <figure [ngClass]="figureClass()">
76
- <figcaption [ngClass]="figcaptionClass()">{{ data.title }}</figcaption>
77
- <audio [ngClass]="audioClass()" [src]="data.url" controls></audio>
71
+ <figure class="et-contentful-audio-figure">
72
+ @if (data.title) {
73
+ <figcaption class="et-contentful-audio-figcaption">{{ data.title }}</figcaption>
74
+ }
75
+ <audio [src]="data.url" class="et-contentful-audio-audio" controls></audio>
78
76
  </figure>
79
77
  }
80
78
  `,
81
79
  encapsulation: ViewEncapsulation.None,
82
- imports: [NgClass],
83
80
  host: {
84
81
  class: 'et-contentful-audio',
85
82
  },
86
83
  }]
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 }] }] } });
84
+ }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }] } });
88
85
 
89
86
  /** The built-in English labels. */
90
87
  const DEFAULT_CONTENTFUL_FILE_LABELS = {
@@ -107,8 +104,6 @@ class ContentfulFileComponent {
107
104
  this.labels = injectContentfulFileLabels();
108
105
  this.asset = input.required(/* @ts-ignore */
109
106
  ...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
110
- this.fileClass = input(null, /* @ts-ignore */
111
- ...(ngDevMode ? [{ debugName: "fileClass" }] : /* istanbul ignore next */ []));
112
107
  this.data = computed(() => {
113
108
  const asset = this.asset();
114
109
  if (!asset) {
@@ -133,16 +128,16 @@ class ContentfulFileComponent {
133
128
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
134
129
  }
135
130
  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: `
131
+ 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 } }, host: { classAttribute: "et-contentful-file" }, ngImport: i0, template: `
137
132
  @if (data(); as data) {
138
- <a [href]="data.url" [ngClass]="fileClass()" target="_blank" rel="noopener noreferrer">
133
+ <a [href]="data.url" class="et-contentful-file-anchor" target="_blank" rel="noopener noreferrer">
139
134
  {{ data.title }}
140
135
  @if (data.size !== null) {
141
136
  {{ labels().fileSize(data.size) }}
142
137
  }
143
138
  </a>
144
139
  }
145
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
140
+ `, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
146
141
  }
147
142
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulFileComponent, decorators: [{
148
143
  type: Component,
@@ -150,7 +145,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
150
145
  selector: 'et-contentful-file',
151
146
  template: `
152
147
  @if (data(); as data) {
153
- <a [href]="data.url" [ngClass]="fileClass()" target="_blank" rel="noopener noreferrer">
148
+ <a [href]="data.url" class="et-contentful-file-anchor" target="_blank" rel="noopener noreferrer">
154
149
  {{ data.title }}
155
150
  @if (data.size !== null) {
156
151
  {{ labels().fileSize(data.size) }}
@@ -159,12 +154,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
159
154
  }
160
155
  `,
161
156
  encapsulation: ViewEncapsulation.None,
162
- imports: [NgClass],
163
157
  host: {
164
158
  class: 'et-contentful-file',
165
159
  },
166
160
  }]
167
- }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], fileClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileClass", required: false }] }] } });
161
+ }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }] } });
168
162
 
169
163
  const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
170
164
 
@@ -175,6 +169,7 @@ const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
175
169
  */
176
170
  const CONTENTFUL_FALLBACK_CONFIG = {
177
171
  internalHosts: [],
172
+ entryHref: null,
178
173
  components: {},
179
174
  customComponents: {},
180
175
  imageOptions: {
@@ -183,13 +178,16 @@ const CONTENTFUL_FALLBACK_CONFIG = {
183
178
  backgroundColor: null,
184
179
  },
185
180
  };
181
+ const withoutUndefined = (value) => Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined));
186
182
  const createContentfulConfig = (options) => {
187
183
  const config = options ?? {};
188
184
  return {
189
185
  ...CONTENTFUL_FALLBACK_CONFIG,
190
186
  ...config,
191
187
  components: config.components ?? CONTENTFUL_FALLBACK_CONFIG.components,
192
- imageOptions: { ...CONTENTFUL_FALLBACK_CONFIG.imageOptions, ...config.imageOptions },
188
+ customComponents: config.customComponents ?? CONTENTFUL_FALLBACK_CONFIG.customComponents,
189
+ internalHosts: config.internalHosts ?? CONTENTFUL_FALLBACK_CONFIG.internalHosts,
190
+ imageOptions: { ...CONTENTFUL_FALLBACK_CONFIG.imageOptions, ...withoutUndefined(config.imageOptions) },
193
191
  };
194
192
  };
195
193
  /**
@@ -199,29 +197,8 @@ const createContentfulConfig = (options) => {
199
197
  const injectContentfulConfig = () => inject(CONTENTFUL_CONFIG, { optional: true }) ?? CONTENTFUL_FALLBACK_CONFIG;
200
198
 
201
199
  const generateDefaultContentfulImageSource = (data) => {
202
- if (isContentfulGqlAsset(data)) {
203
- if (!data.contentType || !data.url) {
204
- return {
205
- type: '',
206
- srcset: '',
207
- };
208
- }
209
- return {
210
- type: data.contentType,
211
- srcset: data.url,
212
- };
213
- }
214
- const file = data.fields.file;
215
- if (!file?.contentType || !file.url) {
216
- return {
217
- type: '',
218
- srcset: '',
219
- };
220
- }
221
- return {
222
- type: file.contentType,
223
- srcset: file.url,
224
- };
200
+ const { url, contentType } = isContentfulGqlAsset(data) ? data : (data.fields.file ?? {});
201
+ return url ? { type: contentType ?? '', srcset: url } : { type: '', srcset: '' };
225
202
  };
226
203
  /**
227
204
  * Parses source set sizes into an object containing with and height. Eg.
@@ -267,7 +244,7 @@ const generateContentfulImageSources = (data, options = {}) => {
267
244
  const queryParams = [];
268
245
  queryParams.push(`fm=${type.split('/')[1]}`);
269
246
  if (backgroundColor) {
270
- queryParams.push(`bg=rgb:${backgroundColor}`);
247
+ queryParams.push(`bg=rgb:${backgroundColor.replace(/^#/, '')}`);
271
248
  }
272
249
  if (quality !== null && Number.isFinite(quality)) {
273
250
  queryParams.push(`q=${quality}`);
@@ -292,6 +269,11 @@ const generateContentfulImageSources = (data, options = {}) => {
292
269
  const derivedWidth = Math.round((height * imageDimensions.width) / imageDimensions.height);
293
270
  sourceSets.push(`${urlWithParams}&w=${derivedWidth}&h=${height} ${derivedWidth}w`);
294
271
  }
272
+ else if (ngDevMode && type === SOURCE_TYPES[0]) {
273
+ console.warn(height
274
+ ? `Contentful image: srcsetSizes entry "${size}" is dropped. A height-only size needs the asset's width and height to derive a width descriptor.`
275
+ : `Contentful image: srcsetSizes entry "${size}" is not a valid size and is dropped. Use "400", "400w", "400h" or "400x300"; viewport sizes like "50vw" belong in sizes.`);
276
+ }
295
277
  }
296
278
  }
297
279
  sources.push({
@@ -377,7 +359,7 @@ class ContentfulImageComponent {
377
359
  [sizes]="sizes()"
378
360
  [sources]="sourcesValue()"
379
361
  />
380
- `, 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 }); }
362
+ `, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "defaultSrc", "alt", "figcaption", "priority", "width", "height", "aspectRatio", "sizes", "fit"], outputs: ["imageLoad", "imageError"] }], encapsulation: i0.ViewEncapsulation.None }); }
381
363
  }
382
364
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulImageComponent, decorators: [{
383
365
  type: Component,
@@ -414,29 +396,46 @@ const parseWebUrl = (href) => {
414
396
  return null;
415
397
  }
416
398
  };
417
- const normalizeHostname = (host) => {
399
+ const parseConfiguredHost = (host) => {
418
400
  const value = host.trim().toLowerCase();
401
+ const wildcard = value.startsWith('*.');
402
+ const bare = wildcard ? value.slice(2) : value;
419
403
  try {
420
- return new URL(value.includes('://') ? value : 'https://' + value).hostname;
404
+ const url = new URL(bare.includes('://') ? bare : 'https://' + bare);
405
+ return { hostname: url.hostname, port: url.port, wildcard };
421
406
  }
422
407
  catch {
423
- return value.split(':')[0] ?? value;
408
+ return { hostname: bare.split(':')[0] ?? bare, port: '', wildcard };
424
409
  }
425
410
  };
426
- const matchesHostname = (hostname, configuredHost) => {
427
- const normalizedHost = normalizeHostname(configuredHost);
428
- return hostname === normalizedHost || hostname.endsWith('.' + normalizedHost);
411
+ const matchesConfiguredHost = (url, configuredHost) => {
412
+ const { hostname, port, wildcard } = parseConfiguredHost(configuredHost);
413
+ if (!hostname || (port && url.port !== port))
414
+ return false;
415
+ return wildcard ? url.hostname.endsWith('.' + hostname) : url.hostname === hostname;
429
416
  };
430
417
  const isInternalWebUrl = (url, { location, internalHosts }) => (url.hostname === location.hostname && url.port === location.port) ||
431
- internalHosts.some((host) => matchesHostname(url.hostname, host));
418
+ internalHosts.some((host) => matchesConfiguredHost(url, host));
432
419
  const isExternalWebHref = (href, context) => {
433
420
  const url = parseWebUrl(href);
434
421
  return url !== null && !isInternalWebUrl(url, context);
435
422
  };
423
+ const internalWebHrefPath = (href, context) => {
424
+ const url = parseWebUrl(href);
425
+ return url && isInternalWebUrl(url, context) ? url.pathname + url.search + url.hash : null;
426
+ };
427
+ const hasUrlScheme = (href) => /^[a-z][a-z\d+.-]*:/i.test(href);
428
+ const isRouteRelativeHref = (href) => !hasUrlScheme(href) && !href.startsWith('/');
429
+ const resolveHrefAgainstRoute = (href, routerUrl) => {
430
+ const resolved = new URL(href, 'https://contentful.invalid' + routerUrl);
431
+ return resolved.pathname + resolved.search + resolved.hash;
432
+ };
436
433
 
434
+ const RICH_TEXT_ANCHOR_CLASSES = ['et-contentful-rich-text-default-element', 'et-contentful-rich-text-default-a'];
437
435
  class ContentfulLinkComponent {
438
436
  constructor() {
439
437
  this.document = inject(DOCUMENT);
438
+ this.location = inject(Location);
440
439
  this.router = inject(Router);
441
440
  this.config = injectContentfulConfig();
442
441
  this.url = injectUrl();
@@ -444,10 +443,10 @@ class ContentfulLinkComponent {
444
443
  ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
445
444
  this.text = input.required(/* @ts-ignore */
446
445
  ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
447
- this.textClass = input('', /* @ts-ignore */
448
- ...(ngDevMode ? [{ debugName: "textClass" }] : /* istanbul ignore next */ []));
449
- this.anchorClass = input('', /* @ts-ignore */
450
- ...(ngDevMode ? [{ debugName: "anchorClass" }] : /* istanbul ignore next */ []));
446
+ this.marks = input([], /* @ts-ignore */
447
+ ...(ngDevMode ? [{ debugName: "marks" }] : /* istanbul ignore next */ []));
448
+ this.richText = input(false, /* @ts-ignore */
449
+ ...(ngDevMode ? [{ debugName: "richText" }] : /* istanbul ignore next */ []));
451
450
  this.usesRouterLink = computed(() => {
452
451
  const href = this.href();
453
452
  const absoluteUrl = parseWebUrl(href);
@@ -457,9 +456,14 @@ class ContentfulLinkComponent {
457
456
  internalHosts: this.config.internalHosts,
458
457
  });
459
458
  }
460
- return !href.startsWith('#') && !/^[a-z][a-z\d+.-]*:/i.test(href);
459
+ return !href.startsWith('#') && !hasUrlScheme(href);
461
460
  }, /* @ts-ignore */
462
461
  ...(ngDevMode ? [{ debugName: "usesRouterLink" }] : /* istanbul ignore next */ []));
462
+ this.anchorHref = computed(() => {
463
+ const href = this.href();
464
+ return href.startsWith('#') ? this.location.prepareExternalUrl(resolveHrefAgainstRoute(href, this.url())) : href;
465
+ }, /* @ts-ignore */
466
+ ...(ngDevMode ? [{ debugName: "anchorHref" }] : /* istanbul ignore next */ []));
463
467
  this.openInNewTab = computed(() => Boolean(parseWebUrl(this.href())) && !this.usesRouterLink(), /* @ts-ignore */
464
468
  ...(ngDevMode ? [{ debugName: "openInNewTab" }] : /* istanbul ignore next */ []));
465
469
  this.internalPath = computed(() => {
@@ -471,25 +475,28 @@ class ContentfulLinkComponent {
471
475
  if (href.startsWith('/')) {
472
476
  return href;
473
477
  }
474
- const resolved = new URL(href, 'https://contentful.invalid' + this.url());
475
- return resolved.pathname + resolved.search + resolved.hash;
478
+ return resolveHrefAgainstRoute(href, this.url());
476
479
  }, /* @ts-ignore */
477
480
  ...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
478
481
  this.internalUrlTree = computed(() => this.router.parseUrl(this.internalPath()), /* @ts-ignore */
479
482
  ...(ngDevMode ? [{ debugName: "internalUrlTree" }] : /* istanbul ignore next */ []));
480
- this.linkClass = computed(() => [this.anchorClass(), this.textClass()].filter(Boolean).join(' ') || null, /* @ts-ignore */
481
- ...(ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []));
483
+ this.modifierClasses = computed(() => [
484
+ ...(this.richText() ? RICH_TEXT_ANCHOR_CLASSES : []),
485
+ ...this.marks().map((mark) => `et-contentful-rich-text-mark-${mark}`),
486
+ ], /* @ts-ignore */
487
+ ...(ngDevMode ? [{ debugName: "modifierClasses" }] : /* istanbul ignore next */ []));
482
488
  }
483
489
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
484
- 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: `
490
+ 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 }, marks: { classPropertyName: "marks", publicName: "marks", isSignal: true, isRequired: false, transformFunction: null }, richText: { classPropertyName: "richText", publicName: "richText", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-link" }, ngImport: i0, template: `
485
491
  @if (usesRouterLink()) {
486
- <a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
492
+ <a [class]="modifierClasses()" [routerLink]="internalUrlTree()" class="et-contentful-link-anchor">{{ text() }}</a>
487
493
  } @else {
488
494
  <a
489
- [class]="linkClass()"
490
- [href]="href()"
491
- [target]="openInNewTab() ? '_blank' : null"
492
- [rel]="openInNewTab() ? 'noopener noreferrer' : null"
495
+ [class]="modifierClasses()"
496
+ [href]="anchorHref()"
497
+ [attr.target]="openInNewTab() ? '_blank' : null"
498
+ [attr.rel]="openInNewTab() ? 'noopener noreferrer' : null"
499
+ class="et-contentful-link-anchor"
493
500
  >{{ text() }}</a
494
501
  >
495
502
  }
@@ -499,20 +506,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
499
506
  type: Component,
500
507
  args: [{ selector: 'et-contentful-link', template: `
501
508
  @if (usesRouterLink()) {
502
- <a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
509
+ <a [class]="modifierClasses()" [routerLink]="internalUrlTree()" class="et-contentful-link-anchor">{{ text() }}</a>
503
510
  } @else {
504
511
  <a
505
- [class]="linkClass()"
506
- [href]="href()"
507
- [target]="openInNewTab() ? '_blank' : null"
508
- [rel]="openInNewTab() ? 'noopener noreferrer' : null"
512
+ [class]="modifierClasses()"
513
+ [href]="anchorHref()"
514
+ [attr.target]="openInNewTab() ? '_blank' : null"
515
+ [attr.rel]="openInNewTab() ? 'noopener noreferrer' : null"
516
+ class="et-contentful-link-anchor"
509
517
  >{{ text() }}</a
510
518
  >
511
519
  }
512
520
  `, encapsulation: ViewEncapsulation.None, imports: [RouterLink], host: {
513
521
  class: 'et-contentful-link',
514
522
  }, styles: ["@layer components{.et-contentful-link{display:contents}}\n"] }]
515
- }], 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 }] }] } });
523
+ }], propDecorators: { href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], marks: [{ type: i0.Input, args: [{ isSignal: true, alias: "marks", required: false }] }], richText: [{ type: i0.Input, args: [{ isSignal: true, alias: "richText", required: false }] }] } });
516
524
 
517
525
  /**
518
526
  * The `BLOCKS` node type strings of `@contentful/rich-text-types`, as a plain literal map.
@@ -558,8 +566,9 @@ const CF_INLINES = {
558
566
  };
559
567
 
560
568
  const RICH_TEXT_RENDERER_ERRORS = {
561
- rich_text_undefined: 'The property value given at richTextPath is undefined. Use the richTextPath input to specify the path to the rich text object.',
569
+ rich_text_not_object: (data) => `The value at richTextPath is a ${data.type}, not a rich-text document object. Point richTextPath at the rich-text field itself (e.g. items[0].fields.body).`,
562
570
  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".',
571
+ rich_text_conflicting_inputs: 'Set exactly one rich-text source: a GraphQL rich-text field through gqlRichText, a REST response through content and richTextPath, or a bare document through richText (with includes).',
563
572
  asset_id_not_found: 'The asset ID was not found. This node is not supported.',
564
573
  entry_id_not_found: 'The entry ID was not found. This node is not supported.',
565
574
  text_parent_not_found: 'The parent node is not found. This structure is not supported.',
@@ -567,8 +576,9 @@ const RICH_TEXT_RENDERER_ERRORS = {
567
576
  cached_command_not_component: 'The rendered node for a component command is not a component. The renderer state is out of sync.',
568
577
  };
569
578
  const RICH_TEXT_RENDERER_ERROR_CODES = {
570
- rich_text_undefined: 0,
579
+ rich_text_not_object: 0,
571
580
  rich_text_wrong_type: 1,
581
+ rich_text_conflicting_inputs: 11,
572
582
  asset_id_not_found: 2,
573
583
  entry_id_not_found: 3,
574
584
  text_parent_not_found: 7,
@@ -576,10 +586,80 @@ const RICH_TEXT_RENDERER_ERROR_CODES = {
576
586
  cached_command_not_component: 10,
577
587
  };
578
588
  const richTextRendererError = (code, data) => {
579
- const message = `<et-contentful-rich-text-renderer>: ${RICH_TEXT_RENDERER_ERRORS[code]}`;
589
+ const entry = RICH_TEXT_RENDERER_ERRORS[code];
590
+ const message = `<et-contentful-rich-text-renderer>: ${typeof entry === 'function' ? entry(data) : entry}`;
580
591
  throw new RuntimeError(RICH_TEXT_RENDERER_ERROR_CODES[code], message, data);
581
592
  };
582
593
 
594
+ const lowerCaseFirst = (value) => value.charAt(0).toLowerCase() + value.slice(1);
595
+ const toGqlTypeName = (contentTypeId) => {
596
+ const pascal = contentTypeId.replace(/[^a-z0-9]+(.)?/gi, (_, next) => next?.toUpperCase() ?? '');
597
+ return pascal.charAt(0).toUpperCase() + pascal.slice(1);
598
+ };
599
+ const toContentTypeId = (typename, contentTypeIds) => contentTypeIds.find((id) => toGqlTypeName(id) === typename) ?? lowerCaseFirst(typename);
600
+ const toRestAsset = (asset) => ({
601
+ sys: { type: 'Asset', id: asset.sys.id, createdAt: '', updatedAt: '', locale: '' },
602
+ fields: {
603
+ title: asset.title ?? '',
604
+ description: asset.description ?? '',
605
+ file: {
606
+ url: asset.url,
607
+ details: {
608
+ size: asset.size,
609
+ ...(asset.width !== null && asset.height !== null
610
+ ? { image: { width: asset.width, height: asset.height } }
611
+ : {}),
612
+ },
613
+ fileName: asset.fileName ?? null,
614
+ contentType: asset.contentType,
615
+ },
616
+ },
617
+ metadata: { tags: [] },
618
+ });
619
+ const toRestEntry = (entry, contentTypeIds) => {
620
+ const { __typename, sys, ...fields } = entry;
621
+ if (!__typename) {
622
+ return null;
623
+ }
624
+ return {
625
+ sys: {
626
+ type: 'Entry',
627
+ id: sys.id,
628
+ createdAt: '',
629
+ updatedAt: '',
630
+ locale: '',
631
+ contentType: { sys: { type: 'Link', linkType: 'ContentType', id: toContentTypeId(__typename, contentTypeIds) } },
632
+ },
633
+ fields,
634
+ metadata: { tags: [] },
635
+ };
636
+ };
637
+ const present = (items) => (items ?? []).filter((item) => item !== null && item !== undefined);
638
+ /**
639
+ * Maps the `links` of a GraphQL rich-text field to the entries and assets the renderer resolves
640
+ * embeds against. An entry's content type id is the id in `contentTypeIds` whose GraphQL type name
641
+ * (PascalCase, separators dropped: `product-teaser` → `ProductTeaser`) equals its `__typename`, else the
642
+ * `__typename` with the first letter lower-cased. Entries without a `__typename` are dropped.
643
+ */
644
+ const mapContentfulGqlLinks = (links, contentTypeIds = []) => {
645
+ const assets = new Map();
646
+ const entries = new Map();
647
+ for (const asset of [...present(links?.assets?.block), ...present(links?.assets?.hyperlink)]) {
648
+ assets.set(asset.sys.id, toRestAsset(asset));
649
+ }
650
+ for (const gqlEntry of [
651
+ ...present(links?.entries?.block),
652
+ ...present(links?.entries?.inline),
653
+ ...present(links?.entries?.hyperlink),
654
+ ]) {
655
+ const entry = toRestEntry(gqlEntry, contentTypeIds);
656
+ if (entry) {
657
+ entries.set(entry.sys.id, entry);
658
+ }
659
+ }
660
+ return { assets: [...assets.values()], entries: [...entries.values()] };
661
+ };
662
+
583
663
  const isRichTextRootNode = (node) => {
584
664
  return isObject(node) && 'nodeType' in node && node['nodeType'] === 'document';
585
665
  };
@@ -669,6 +749,12 @@ const isExecutedComponentCommandCacheItem = (cache) => {
669
749
  return cache.command.kind === 'component';
670
750
  };
671
751
  const ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID = '$$$_et-contentful-any-entry-content-type-sys-id';
752
+ const includeMapLookups = /* @__PURE__ */ new WeakMap();
753
+ const lookupOf = (includes) => includeMapLookups.get(includes) ?? {
754
+ entry: (id) => includes.getEntry(id, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID),
755
+ asset: (id) => includes.getAsset(id),
756
+ };
757
+ const ANCESTOR_ENTRY_IDS = /* @__PURE__ */ new InjectionToken('ContentfulRichTextAncestorEntryIds');
672
758
  /**
673
759
  * Create a contentful include map using the provided entries and assets.
674
760
  */
@@ -681,7 +767,7 @@ const createContentfulIncludeMap = (config) => {
681
767
  const entry = entryMap.get(id);
682
768
  if (!entry) {
683
769
  if (isDevMode()) {
684
- console.warn('Entry not found! Will return null. Is the include query param too low?', { id, entryMap });
770
+ console.warn('Entry not found! Will return null. Is it unpublished or deleted, or is the include query param too low?', { id });
685
771
  }
686
772
  return null;
687
773
  }
@@ -713,12 +799,29 @@ const createContentfulIncludeMap = (config) => {
713
799
  const getAssets = (ids) => {
714
800
  return ids.map((id) => getAsset(id)).filter((asset) => asset !== null);
715
801
  };
716
- return {
717
- getEntry,
718
- getEntries,
719
- getAsset,
720
- getAssets,
721
- };
802
+ const includeMap = { getEntry, getEntries, getAsset, getAssets };
803
+ includeMapLookups.set(includeMap, {
804
+ entry: (id) => entryMap.get(id) ?? null,
805
+ asset: (id) => assetMap.get(id) ?? null,
806
+ });
807
+ return includeMap;
808
+ };
809
+ const warnedRichTextPaths = /* @__PURE__ */ new Set();
810
+ const warnOnUnresolvedParentPath = (content, richTextPath) => {
811
+ const segments = richTextPath.split('.');
812
+ const root = content;
813
+ if (segments.length < 2 || isObject(getObjectProperty(root, segments.slice(0, -1).join('.'))))
814
+ return;
815
+ if (warnedRichTextPaths.has(richTextPath))
816
+ return;
817
+ warnedRichTextPaths.add(richTextPath);
818
+ let resolvedSegments = 0;
819
+ while (resolvedSegments < segments.length - 1 &&
820
+ isObject(getObjectProperty(root, segments.slice(0, resolvedSegments + 1).join('.')))) {
821
+ resolvedSegments++;
822
+ }
823
+ const resolvedPath = segments.slice(0, resolvedSegments).join('.') || '(the content itself)';
824
+ console.warn(`<et-contentful-rich-text-renderer>: richTextPath "${richTextPath}" does not resolve, so nothing is rendered. The deepest part that resolves is ${resolvedPath}; check the next segment for a typo or a missing item.`, { content, richTextPath });
722
825
  };
723
826
  class ContentfulRichTextRendererComponent {
724
827
  constructor() {
@@ -727,43 +830,102 @@ class ContentfulRichTextRendererComponent {
727
830
  this.elementRef = inject(ElementRef);
728
831
  this.config = injectContentfulConfig();
729
832
  this.document = inject(DOCUMENT);
833
+ this.location = inject(Location);
834
+ this.url = injectUrl();
835
+ this.injector = inject(Injector);
730
836
  /**
731
837
  * The contentful response gotten via their REST api.
732
838
  * @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
733
839
  */
734
- this.content = input.required(/* @ts-ignore */
735
- ...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
840
+ this.content = input(/* @ts-ignore */
841
+ ...(ngDevMode ? [undefined, { debugName: "content" }] : /* istanbul ignore next */ []));
736
842
  /**
737
843
  * The path to where the rich text field is inside the contentful response. Dot and array notation is allowed.
738
844
  * @example "items[0].fields.html"
739
845
  */
740
- this.richTextPath = input.required(/* @ts-ignore */
741
- ...(ngDevMode ? [{ debugName: "richTextPath" }] : /* istanbul ignore next */ []));
846
+ this.richTextPath = input(/* @ts-ignore */
847
+ ...(ngDevMode ? [undefined, { debugName: "richTextPath" }] : /* istanbul ignore next */ []));
848
+ /**
849
+ * A rich-text field from the Contentful GraphQL API, passed as-is. An alternative to `content` and
850
+ * `richTextPath`; setting both is an error in dev mode.
851
+ * @example { json, links: { assets: { block, hyperlink }, entries: { block, inline, hyperlink } } }
852
+ */
853
+ this.gqlRichText = input(/* @ts-ignore */
854
+ ...(ngDevMode ? [undefined, { debugName: "gqlRichText" }] : /* istanbul ignore next */ []));
855
+ /**
856
+ * A bare rich-text document, e.g. a rich-text field of an embedded entry. Pair it with `includes` so its
857
+ * embeds resolve. An alternative to `content` / `richTextPath` and `gqlRichText`.
858
+ * @example <et-contentful-rich-text-renderer [richText]="fields().body" [includes]="includes()" />
859
+ */
860
+ this.richText = input(/* @ts-ignore */
861
+ ...(ngDevMode ? [undefined, { debugName: "richText" }] : /* istanbul ignore next */ []));
862
+ /** The linked entries and assets `richText` resolves its embeds against, e.g. a custom component's `includes`. */
863
+ this.includes = input(/* @ts-ignore */
864
+ ...(ngDevMode ? [undefined, { debugName: "includes" }] : /* istanbul ignore next */ []));
865
+ this.ancestorEntryIds = inject(ANCESTOR_ENTRY_IDS, { optional: true }) ?? new Set();
742
866
  this.executedCommandsCache = new Map();
743
867
  this.contentIncludesMap = computed(() => {
868
+ const includes = this.includes();
869
+ if (includes) {
870
+ return includes;
871
+ }
872
+ const gqlRichText = this.gqlRichText();
873
+ if (gqlRichText) {
874
+ return createContentfulIncludeMap(mapContentfulGqlLinks(gqlRichText.links, Object.keys(this.config.customComponents)));
875
+ }
744
876
  const content = this.content();
745
- const assets = content?.includes?.Asset;
746
- const entries = content?.includes?.Entry;
747
- return createContentfulIncludeMap({ assets: assets ?? [], entries: entries ?? [] });
877
+ const entries = new Map();
878
+ for (const entry of [...(content?.items ?? []), ...(content?.includes?.Entry ?? [])]) {
879
+ entries.set(entry.sys.id, entry);
880
+ }
881
+ return createContentfulIncludeMap({ assets: content?.includes?.Asset ?? [], entries: [...entries.values()] });
748
882
  }, /* @ts-ignore */
749
883
  ...(ngDevMode ? [{ debugName: "contentIncludesMap" }] : /* istanbul ignore next */ []));
884
+ this.linkLookup = computed(() => lookupOf(this.contentIncludesMap()), /* @ts-ignore */
885
+ ...(ngDevMode ? [{ debugName: "linkLookup" }] : /* istanbul ignore next */ []));
750
886
  this.richTextData = computed(() => {
887
+ const gqlRichText = this.gqlRichText();
751
888
  const content = this.content();
752
889
  const richTextPath = this.richTextPath();
890
+ const richText = this.richText();
891
+ if (ngDevMode && [gqlRichText, content || richTextPath, richText].filter(Boolean).length > 1) {
892
+ throw richTextRendererError('rich_text_conflicting_inputs', { gqlRichText, content, richTextPath, richText });
893
+ }
894
+ if (richText) {
895
+ if (!isRichTextRootNode(richText)) {
896
+ throw richTextRendererError('rich_text_wrong_type', { richText });
897
+ }
898
+ return richText;
899
+ }
900
+ if (gqlRichText) {
901
+ if (!isRichTextRootNode(gqlRichText.json)) {
902
+ throw richTextRendererError('rich_text_wrong_type', { gqlRichText });
903
+ }
904
+ return gqlRichText.json;
905
+ }
753
906
  if (!content) {
754
907
  return null;
755
908
  }
756
- const richText = getObjectProperty(content, richTextPath);
757
- if (richText === null || richText === undefined) {
909
+ if (!richTextPath) {
910
+ if (ngDevMode) {
911
+ console.warn('<et-contentful-rich-text-renderer>: content is set without richTextPath, so nothing is rendered. Set richTextPath, or pass a GraphQL field through gqlRichText.');
912
+ }
913
+ return null;
914
+ }
915
+ const pathValue = getObjectProperty(content, richTextPath);
916
+ if (pathValue === null || pathValue === undefined) {
917
+ if (ngDevMode && pathValue === undefined) {
918
+ warnOnUnresolvedParentPath(content, richTextPath);
919
+ }
758
920
  return null;
759
921
  }
760
- if (!isObject(richText)) {
761
- throw richTextRendererError('rich_text_undefined', { content, richTextPath });
922
+ if (!isObject(pathValue)) {
923
+ throw richTextRendererError('rich_text_not_object', { content, richTextPath, type: typeof pathValue });
762
924
  }
763
- if (!isRichTextRootNode(richText)) {
925
+ if (!isRichTextRootNode(pathValue)) {
764
926
  throw richTextRendererError('rich_text_wrong_type', { content, richTextPath });
765
927
  }
766
- return richText;
928
+ return pathValue;
767
929
  }, /* @ts-ignore */
768
930
  ...(ngDevMode ? [{ debugName: "richTextData" }] : /* istanbul ignore next */ []));
769
931
  this.renderCommands = computed(() => {
@@ -883,7 +1045,7 @@ class ContentfulRichTextRendererComponent {
883
1045
  if (!assetId) {
884
1046
  throw richTextRendererError('asset_id_not_found', { node });
885
1047
  }
886
- const asset = this.contentIncludesMap().getAsset(assetId);
1048
+ const asset = this.linkLookup().asset(assetId);
887
1049
  if (!asset) {
888
1050
  if (isDevMode()) {
889
1051
  console.warn('Embedded asset is missing from the includes! The asset will be skipped. Is it unpublished or deleted?', { assetId, node });
@@ -892,8 +1054,7 @@ class ContentfulRichTextRendererComponent {
892
1054
  }
893
1055
  const contentType = asset.fields.file?.contentType;
894
1056
  const assetComponents = this.config.components;
895
- const isMissing = !contentType && !asset.fields.file?.url;
896
- if (isMissing) {
1057
+ if (!asset.fields.file?.url) {
897
1058
  if (isDevMode()) {
898
1059
  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 });
899
1060
  }
@@ -940,16 +1101,32 @@ class ContentfulRichTextRendererComponent {
940
1101
  }
941
1102
  else if (node.nodeType === CF_INLINES.ASSET_HYPERLINK) {
942
1103
  const assetId = node.data['target']?.sys?.id;
943
- const asset = assetId ? this.contentIncludesMap().getAsset(assetId) : null;
1104
+ const asset = assetId ? this.linkLookup().asset(assetId) : null;
944
1105
  const url = asset?.fields.file?.url;
945
1106
  href = url ? normalizeHref(url) : null;
946
1107
  }
1108
+ else if (this.config.entryHref) {
1109
+ const entryId = node.data['target']?.sys?.id;
1110
+ const entry = entryId ? this.linkLookup().entry(entryId) : null;
1111
+ const entryHref = entry ? this.config.entryHref(entry) : null;
1112
+ href = entryHref ? normalizeHref(entryHref) : null;
1113
+ }
947
1114
  const linkTexts = node.content.filter((child) => child.nodeType === 'text' && Boolean(child.value));
1115
+ if (!linkTexts.length) {
1116
+ break;
1117
+ }
948
1118
  const linkText = linkTexts.map((child) => child.value).join('');
949
1119
  const linkComponent = href ? this.config.components.link : null;
950
1120
  if (href && !linkComponent) {
951
- const attributes = { class: DEFAULT_ANCHOR_CLASS, href };
952
- if (isExternalWebHref(href, { location: this.document.location, internalHosts: this.config.internalHosts })) {
1121
+ const webUrlContext = { location: this.document.location, internalHosts: this.config.internalHosts };
1122
+ const internalPath = internalWebHrefPath(href, webUrlContext);
1123
+ const anchorHref = internalPath !== null
1124
+ ? this.location.prepareExternalUrl(internalPath)
1125
+ : isRouteRelativeHref(href)
1126
+ ? this.location.prepareExternalUrl(resolveHrefAgainstRoute(href, this.url()))
1127
+ : href;
1128
+ const attributes = { class: DEFAULT_ANCHOR_CLASS, href: anchorHref };
1129
+ if (isExternalWebHref(href, webUrlContext)) {
953
1130
  attributes['target'] = '_blank';
954
1131
  attributes['rel'] = 'noopener noreferrer';
955
1132
  }
@@ -993,7 +1170,6 @@ class ContentfulRichTextRendererComponent {
993
1170
  break;
994
1171
  }
995
1172
  const sharedMarks = linkTexts[0]?.marks.filter((mark) => linkTexts.every((child) => child.marks.some((other) => other.type === mark.type))) ?? [];
996
- const textClass = marksToClass(sharedMarks);
997
1173
  let linkComponentId = componentIdMap.get(LINK_COMPONENT_TYPE) ?? -1;
998
1174
  const linkId = LINK_COMPONENT_TYPE + ++linkComponentId;
999
1175
  componentIdMap.set(LINK_COMPONENT_TYPE, linkComponentId);
@@ -1003,7 +1179,7 @@ class ContentfulRichTextRendererComponent {
1003
1179
  domPosition,
1004
1180
  parentId: openElementIds.at(-1) ?? null,
1005
1181
  component: linkComponent,
1006
- inputs: { href, text: linkText, textClass, anchorClass: DEFAULT_ANCHOR_CLASS },
1182
+ inputs: { href, text: linkText, marks: sharedMarks.map((mark) => mark.type), richText: true },
1007
1183
  id: linkId,
1008
1184
  });
1009
1185
  domPosition++;
@@ -1015,10 +1191,16 @@ class ContentfulRichTextRendererComponent {
1015
1191
  if (!entryId) {
1016
1192
  throw richTextRendererError('entry_id_not_found', { node });
1017
1193
  }
1018
- const entry = this.contentIncludesMap().getEntry(entryId, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID);
1194
+ if (this.ancestorEntryIds.has(entryId)) {
1195
+ if (isDevMode()) {
1196
+ console.warn('Embedded entry embeds itself through its own rich text! The nested occurrence is skipped to stop the cycle.', { entryId, node });
1197
+ }
1198
+ break;
1199
+ }
1200
+ const entry = this.linkLookup().entry(entryId);
1019
1201
  if (!entry) {
1020
1202
  if (isDevMode()) {
1021
- console.warn('Embedded entry is missing from the includes! The entry will be skipped. Is it unpublished or deleted?', { entryId, node });
1203
+ console.warn('Embedded entry is missing from the includes! The entry will be skipped. Is it unpublished or deleted, or is the include query param too low?', { entryId, node });
1022
1204
  }
1023
1205
  break;
1024
1206
  }
@@ -1046,11 +1228,22 @@ class ContentfulRichTextRendererComponent {
1046
1228
  sys: entry.sys,
1047
1229
  includes: this.contentIncludesMap(),
1048
1230
  },
1231
+ entryId,
1049
1232
  id,
1050
1233
  });
1051
1234
  domPosition++;
1052
1235
  break;
1053
1236
  }
1237
+ case CF_BLOCKS.EMBEDDED_RESOURCE:
1238
+ case CF_INLINES.EMBEDDED_RESOURCE: {
1239
+ if (isDevMode()) {
1240
+ console.warn(`Rich text node type "${node.nodeType}" is not supported and is skipped.`, {
1241
+ urn: node.data['target']?.sys?.urn,
1242
+ node,
1243
+ });
1244
+ }
1245
+ break;
1246
+ }
1054
1247
  default: {
1055
1248
  const tag = translateContentfulNodeTypeToHtmlTag(node.nodeType);
1056
1249
  const attributes = {
@@ -1137,7 +1330,15 @@ class ContentfulRichTextRendererComponent {
1137
1330
  const bindings = declaredInputs
1138
1331
  .filter((declared) => declared.propName in command.inputs)
1139
1332
  .map((declared) => inputBinding(declared.templateName, () => inputs()[declared.propName]));
1140
- const componentRef = this.viewContainerRef.createComponent(command.component, { bindings });
1333
+ const injector = command.entryId
1334
+ ? Injector.create({
1335
+ providers: [
1336
+ { provide: ANCESTOR_ENTRY_IDS, useValue: new Set([...this.ancestorEntryIds, command.entryId]) },
1337
+ ],
1338
+ parent: this.injector,
1339
+ })
1340
+ : undefined;
1341
+ const componentRef = this.viewContainerRef.createComponent(command.component, { bindings, injector });
1141
1342
  const rootNode = this.getComponentRootNode(componentRef);
1142
1343
  this.renderInsertAfter(rootNode, { parentElement, previousSibling });
1143
1344
  this.executedCommandsCache.set(command.id, {
@@ -1263,7 +1464,7 @@ class ContentfulRichTextRendererComponent {
1263
1464
  this.renderer.insertBefore(parentElement, node, previousSibling ? previousSibling.nextSibling : parentElement.firstChild);
1264
1465
  }
1265
1466
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1266
- 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 }); }
1467
+ 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: false, transformFunction: null }, richTextPath: { classPropertyName: "richTextPath", publicName: "richTextPath", isSignal: true, isRequired: false, transformFunction: null }, gqlRichText: { classPropertyName: "gqlRichText", publicName: "gqlRichText", isSignal: true, isRequired: false, transformFunction: null }, richText: { classPropertyName: "richText", publicName: "richText", isSignal: true, isRequired: false, transformFunction: null }, includes: { classPropertyName: "includes", publicName: "includes", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "ngSkipHydration": "true" }, classAttribute: "et-contentful-rich-text-renderer" }, ngImport: i0, template: ``, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
1267
1468
  }
1268
1469
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
1269
1470
  type: Component,
@@ -1273,16 +1474,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
1273
1474
  encapsulation: ViewEncapsulation.None,
1274
1475
  host: {
1275
1476
  class: 'et-contentful-rich-text-renderer',
1477
+ ngSkipHydration: 'true',
1276
1478
  },
1277
1479
  }]
1278
- }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: true }] }], richTextPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "richTextPath", required: true }] }] } });
1480
+ }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], richTextPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "richTextPath", required: false }] }], gqlRichText: [{ type: i0.Input, args: [{ isSignal: true, alias: "gqlRichText", required: false }] }], richText: [{ type: i0.Input, args: [{ isSignal: true, alias: "richText", required: false }] }], includes: [{ type: i0.Input, args: [{ isSignal: true, alias: "includes", required: false }] }] } });
1279
1481
 
1280
1482
  class ContentfulVideoComponent {
1281
1483
  constructor() {
1282
1484
  this.asset = input.required(/* @ts-ignore */
1283
1485
  ...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
1284
- this.videoClass = input(null, /* @ts-ignore */
1285
- ...(ngDevMode ? [{ debugName: "videoClass" }] : /* istanbul ignore next */ []));
1286
1486
  this.data = computed(() => {
1287
1487
  const asset = this.asset();
1288
1488
  if (!asset) {
@@ -1305,13 +1505,13 @@ class ContentfulVideoComponent {
1305
1505
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
1306
1506
  }
1307
1507
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1308
- 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: `
1508
+ 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 } }, host: { classAttribute: "et-contentful-video" }, ngImport: i0, template: `
1309
1509
  @if (data(); as data) {
1310
- <video [ngClass]="videoClass()" controls>
1311
- <source [src]="data.url" [type]="data.contentType" />
1510
+ <video class="et-contentful-video-video" controls>
1511
+ <source [src]="data.url" [attr.type]="data.contentType || null" class="et-contentful-video-source" />
1312
1512
  </video>
1313
1513
  }
1314
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
1514
+ `, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
1315
1515
  }
1316
1516
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
1317
1517
  type: Component,
@@ -1319,18 +1519,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
1319
1519
  selector: 'et-contentful-video',
1320
1520
  template: `
1321
1521
  @if (data(); as data) {
1322
- <video [ngClass]="videoClass()" controls>
1323
- <source [src]="data.url" [type]="data.contentType" />
1522
+ <video class="et-contentful-video-video" controls>
1523
+ <source [src]="data.url" [attr.type]="data.contentType || null" class="et-contentful-video-source" />
1324
1524
  </video>
1325
1525
  }
1326
1526
  `,
1327
1527
  encapsulation: ViewEncapsulation.None,
1328
- imports: [NgClass],
1329
1528
  host: {
1330
1529
  class: 'et-contentful-video',
1331
1530
  },
1332
1531
  }]
1333
- }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], videoClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoClass", required: false }] }] } });
1532
+ }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }] } });
1334
1533
 
1335
1534
  const ContentfulImports = [
1336
1535
  ContentfulAudioComponent,
@@ -1367,5 +1566,5 @@ const isContentfulEntryType = (entry, type) => entry.sys.contentType.sys.id ===
1367
1566
  * Generated bundle index. Do not edit.
1368
1567
  */
1369
1568
 
1370
- export { CONTENTFUL_CONFIG, CONTENTFUL_DEFAULT_COMPONENTS, 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 };
1569
+ export { CONTENTFUL_CONFIG, CONTENTFUL_DEFAULT_COMPONENTS, 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, mapContentfulGqlLinks, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, provideContentfulFileLabels, translateContentfulNodeTypeToHtmlTag };
1371
1570
  //# sourceMappingURL=ethlete-contentful.mjs.map