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

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, inject, InjectionToken, 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, 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,178 +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 }] }] } });
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 }] }] } });
161
+ }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }] } });
334
162
 
335
163
  const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
336
164
 
@@ -349,28 +177,16 @@ const CONTENTFUL_FALLBACK_CONFIG = {
349
177
  backgroundColor: null,
350
178
  },
351
179
  };
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
- };
180
+ const withoutUndefined = (value) => Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined));
181
+ const createContentfulConfig = (options) => {
182
+ const config = options ?? {};
369
183
  return {
370
- ...defaults,
184
+ ...CONTENTFUL_FALLBACK_CONFIG,
371
185
  ...config,
372
- components: { ...defaults.components, ...config?.components },
373
- imageOptions: { ...defaults.imageOptions, ...config?.imageOptions },
186
+ components: config.components ?? CONTENTFUL_FALLBACK_CONFIG.components,
187
+ customComponents: config.customComponents ?? CONTENTFUL_FALLBACK_CONFIG.customComponents,
188
+ internalHosts: config.internalHosts ?? CONTENTFUL_FALLBACK_CONFIG.internalHosts,
189
+ imageOptions: { ...CONTENTFUL_FALLBACK_CONFIG.imageOptions, ...withoutUndefined(config.imageOptions) },
374
190
  };
375
191
  };
376
192
  /**
@@ -380,29 +196,8 @@ const createContentfulConfig = (config) => {
380
196
  const injectContentfulConfig = () => inject(CONTENTFUL_CONFIG, { optional: true }) ?? CONTENTFUL_FALLBACK_CONFIG;
381
197
 
382
198
  const generateDefaultContentfulImageSource = (data) => {
383
- if (isContentfulGqlAsset(data)) {
384
- if (!data.contentType || !data.url) {
385
- return {
386
- type: '',
387
- srcset: '',
388
- };
389
- }
390
- return {
391
- type: data.contentType,
392
- srcset: data.url,
393
- };
394
- }
395
- const file = data.fields.file;
396
- if (!file?.contentType || !file.url) {
397
- return {
398
- type: '',
399
- srcset: '',
400
- };
401
- }
402
- return {
403
- type: file.contentType,
404
- srcset: file.url,
405
- };
199
+ const { url, contentType } = isContentfulGqlAsset(data) ? data : (data.fields.file ?? {});
200
+ return url ? { type: contentType ?? '', srcset: url } : { type: '', srcset: '' };
406
201
  };
407
202
  /**
408
203
  * Parses source set sizes into an object containing with and height. Eg.
@@ -448,7 +243,7 @@ const generateContentfulImageSources = (data, options = {}) => {
448
243
  const queryParams = [];
449
244
  queryParams.push(`fm=${type.split('/')[1]}`);
450
245
  if (backgroundColor) {
451
- queryParams.push(`bg=rgb:${backgroundColor}`);
246
+ queryParams.push(`bg=rgb:${backgroundColor.replace(/^#/, '')}`);
452
247
  }
453
248
  if (quality !== null && Number.isFinite(quality)) {
454
249
  queryParams.push(`q=${quality}`);
@@ -473,6 +268,11 @@ const generateContentfulImageSources = (data, options = {}) => {
473
268
  const derivedWidth = Math.round((height * imageDimensions.width) / imageDimensions.height);
474
269
  sourceSets.push(`${urlWithParams}&w=${derivedWidth}&h=${height} ${derivedWidth}w`);
475
270
  }
271
+ else if (ngDevMode && type === SOURCE_TYPES[0]) {
272
+ console.warn(height
273
+ ? `Contentful image: srcsetSizes entry "${size}" is dropped. A height-only size needs the asset's width and height to derive a width descriptor.`
274
+ : `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.`);
275
+ }
476
276
  }
477
277
  }
478
278
  sources.push({
@@ -558,7 +358,7 @@ class ContentfulImageComponent {
558
358
  [sizes]="sizes()"
559
359
  [sources]="sourcesValue()"
560
360
  />
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 }); }
361
+ `, 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 }); }
562
362
  }
563
363
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulImageComponent, decorators: [{
564
364
  type: Component,
@@ -584,6 +384,134 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
584
384
  }]
585
385
  }], 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 }] }] } });
586
386
 
387
+ const parseWebUrl = (href) => {
388
+ if (!/^(?:https?:)?\/\//i.test(href)) {
389
+ return null;
390
+ }
391
+ try {
392
+ return new URL(href.startsWith('//') ? 'https:' + href : href);
393
+ }
394
+ catch {
395
+ return null;
396
+ }
397
+ };
398
+ const normalizeHostname = (host) => {
399
+ const value = host.trim().toLowerCase();
400
+ try {
401
+ return new URL(value.includes('://') ? value : 'https://' + value).hostname;
402
+ }
403
+ catch {
404
+ return value.split(':')[0] ?? value;
405
+ }
406
+ };
407
+ const matchesHostname = (hostname, configuredHost) => {
408
+ const normalizedHost = normalizeHostname(configuredHost);
409
+ return hostname === normalizedHost || hostname.endsWith('.' + normalizedHost);
410
+ };
411
+ const isInternalWebUrl = (url, { location, internalHosts }) => (url.hostname === location.hostname && url.port === location.port) ||
412
+ internalHosts.some((host) => matchesHostname(url.hostname, host));
413
+ const isExternalWebHref = (href, context) => {
414
+ const url = parseWebUrl(href);
415
+ return url !== null && !isInternalWebUrl(url, context);
416
+ };
417
+ const hasUrlScheme = (href) => /^[a-z][a-z\d+.-]*:/i.test(href);
418
+ const isRouteRelativeHref = (href) => !hasUrlScheme(href) && !href.startsWith('/');
419
+ const resolveHrefAgainstRoute = (href, routerUrl) => {
420
+ const resolved = new URL(href, 'https://contentful.invalid' + routerUrl);
421
+ return resolved.pathname + resolved.search + resolved.hash;
422
+ };
423
+
424
+ const RICH_TEXT_ANCHOR_CLASSES = ['et-contentful-rich-text-default-element', 'et-contentful-rich-text-default-a'];
425
+ class ContentfulLinkComponent {
426
+ constructor() {
427
+ this.document = inject(DOCUMENT);
428
+ this.location = inject(Location);
429
+ this.router = inject(Router);
430
+ this.config = injectContentfulConfig();
431
+ this.url = injectUrl();
432
+ this.href = input.required(/* @ts-ignore */
433
+ ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
434
+ this.text = input.required(/* @ts-ignore */
435
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
436
+ this.marks = input([], /* @ts-ignore */
437
+ ...(ngDevMode ? [{ debugName: "marks" }] : /* istanbul ignore next */ []));
438
+ this.richText = input(false, /* @ts-ignore */
439
+ ...(ngDevMode ? [{ debugName: "richText" }] : /* istanbul ignore next */ []));
440
+ this.usesRouterLink = computed(() => {
441
+ const href = this.href();
442
+ const absoluteUrl = parseWebUrl(href);
443
+ if (absoluteUrl) {
444
+ return isInternalWebUrl(absoluteUrl, {
445
+ location: this.document.location,
446
+ internalHosts: this.config.internalHosts,
447
+ });
448
+ }
449
+ return !href.startsWith('#') && !hasUrlScheme(href);
450
+ }, /* @ts-ignore */
451
+ ...(ngDevMode ? [{ debugName: "usesRouterLink" }] : /* istanbul ignore next */ []));
452
+ this.anchorHref = computed(() => {
453
+ const href = this.href();
454
+ return href.startsWith('#') ? this.location.prepareExternalUrl(resolveHrefAgainstRoute(href, this.url())) : href;
455
+ }, /* @ts-ignore */
456
+ ...(ngDevMode ? [{ debugName: "anchorHref" }] : /* istanbul ignore next */ []));
457
+ this.openInNewTab = computed(() => Boolean(parseWebUrl(this.href())) && !this.usesRouterLink(), /* @ts-ignore */
458
+ ...(ngDevMode ? [{ debugName: "openInNewTab" }] : /* istanbul ignore next */ []));
459
+ this.internalPath = computed(() => {
460
+ const href = this.href();
461
+ const url = parseWebUrl(href);
462
+ if (url) {
463
+ return url.pathname + url.search + url.hash;
464
+ }
465
+ if (href.startsWith('/')) {
466
+ return href;
467
+ }
468
+ return resolveHrefAgainstRoute(href, this.url());
469
+ }, /* @ts-ignore */
470
+ ...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
471
+ this.internalUrlTree = computed(() => this.router.parseUrl(this.internalPath()), /* @ts-ignore */
472
+ ...(ngDevMode ? [{ debugName: "internalUrlTree" }] : /* istanbul ignore next */ []));
473
+ this.modifierClasses = computed(() => [
474
+ ...(this.richText() ? RICH_TEXT_ANCHOR_CLASSES : []),
475
+ ...this.marks().map((mark) => `et-contentful-rich-text-mark-${mark}`),
476
+ ], /* @ts-ignore */
477
+ ...(ngDevMode ? [{ debugName: "modifierClasses" }] : /* istanbul ignore next */ []));
478
+ }
479
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
480
+ 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: `
481
+ @if (usesRouterLink()) {
482
+ <a [class]="modifierClasses()" [routerLink]="internalUrlTree()" class="et-contentful-link-anchor">{{ text() }}</a>
483
+ } @else {
484
+ <a
485
+ [class]="modifierClasses()"
486
+ [href]="anchorHref()"
487
+ [attr.target]="openInNewTab() ? '_blank' : null"
488
+ [attr.rel]="openInNewTab() ? 'noopener noreferrer' : null"
489
+ class="et-contentful-link-anchor"
490
+ >{{ text() }}</a
491
+ >
492
+ }
493
+ `, 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 }); }
494
+ }
495
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
496
+ type: Component,
497
+ args: [{ selector: 'et-contentful-link', template: `
498
+ @if (usesRouterLink()) {
499
+ <a [class]="modifierClasses()" [routerLink]="internalUrlTree()" class="et-contentful-link-anchor">{{ text() }}</a>
500
+ } @else {
501
+ <a
502
+ [class]="modifierClasses()"
503
+ [href]="anchorHref()"
504
+ [attr.target]="openInNewTab() ? '_blank' : null"
505
+ [attr.rel]="openInNewTab() ? 'noopener noreferrer' : null"
506
+ class="et-contentful-link-anchor"
507
+ >{{ text() }}</a
508
+ >
509
+ }
510
+ `, encapsulation: ViewEncapsulation.None, imports: [RouterLink], host: {
511
+ class: 'et-contentful-link',
512
+ }, styles: ["@layer components{.et-contentful-link{display:contents}}\n"] }]
513
+ }], 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 }] }] } });
514
+
587
515
  /**
588
516
  * The `BLOCKS` node type strings of `@contentful/rich-text-types`, as a plain literal map.
589
517
  * Keep the values in sync with the enum - they are part of the Contentful Delivery API.
@@ -628,8 +556,9 @@ const CF_INLINES = {
628
556
  };
629
557
 
630
558
  const RICH_TEXT_RENDERER_ERRORS = {
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.',
559
+ 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).`,
632
560
  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".',
561
+ rich_text_conflicting_inputs: 'gqlRichText cannot be combined with content or richTextPath. Pass either a GraphQL rich-text field through gqlRichText, or a REST response through content and richTextPath.',
633
562
  asset_id_not_found: 'The asset ID was not found. This node is not supported.',
634
563
  entry_id_not_found: 'The entry ID was not found. This node is not supported.',
635
564
  text_parent_not_found: 'The parent node is not found. This structure is not supported.',
@@ -637,8 +566,9 @@ const RICH_TEXT_RENDERER_ERRORS = {
637
566
  cached_command_not_component: 'The rendered node for a component command is not a component. The renderer state is out of sync.',
638
567
  };
639
568
  const RICH_TEXT_RENDERER_ERROR_CODES = {
640
- rich_text_undefined: 0,
569
+ rich_text_not_object: 0,
641
570
  rich_text_wrong_type: 1,
571
+ rich_text_conflicting_inputs: 11,
642
572
  asset_id_not_found: 2,
643
573
  entry_id_not_found: 3,
644
574
  text_parent_not_found: 7,
@@ -646,10 +576,75 @@ const RICH_TEXT_RENDERER_ERROR_CODES = {
646
576
  cached_command_not_component: 10,
647
577
  };
648
578
  const richTextRendererError = (code, data) => {
649
- const message = `<et-contentful-rich-text-renderer>: ${RICH_TEXT_RENDERER_ERRORS[code]}`;
579
+ const entry = RICH_TEXT_RENDERER_ERRORS[code];
580
+ const message = `<et-contentful-rich-text-renderer>: ${typeof entry === 'function' ? entry(data) : entry}`;
650
581
  throw new RuntimeError(RICH_TEXT_RENDERER_ERROR_CODES[code], message, data);
651
582
  };
652
583
 
584
+ const lowerCaseFirst = (value) => value.charAt(0).toLowerCase() + value.slice(1);
585
+ const toRestAsset = (asset) => ({
586
+ sys: { type: 'Asset', id: asset.sys.id, createdAt: '', updatedAt: '', locale: '' },
587
+ fields: {
588
+ title: asset.title ?? '',
589
+ description: asset.description ?? '',
590
+ file: {
591
+ url: asset.url,
592
+ details: {
593
+ size: asset.size,
594
+ ...(asset.width !== null && asset.height !== null
595
+ ? { image: { width: asset.width, height: asset.height } }
596
+ : {}),
597
+ },
598
+ fileName: asset.fileName ?? null,
599
+ contentType: asset.contentType,
600
+ },
601
+ },
602
+ metadata: { tags: [] },
603
+ });
604
+ const toRestEntry = (entry) => {
605
+ const { __typename, sys, ...fields } = entry;
606
+ if (!__typename) {
607
+ return null;
608
+ }
609
+ return {
610
+ sys: {
611
+ type: 'Entry',
612
+ id: sys.id,
613
+ createdAt: '',
614
+ updatedAt: '',
615
+ locale: '',
616
+ contentType: { sys: { type: 'Link', linkType: 'ContentType', id: lowerCaseFirst(__typename) } },
617
+ },
618
+ fields,
619
+ metadata: { tags: [] },
620
+ };
621
+ };
622
+ const present = (items) => (items ?? []).filter((item) => item !== null && item !== undefined);
623
+ /**
624
+ * Maps the `links` of a GraphQL rich-text field to the entries and assets the renderer resolves
625
+ * embeds against. An entry's content type id is its `__typename` with the first letter lower-cased,
626
+ * which is how Contentful derives a GraphQL type name from a content type id. Entries without a
627
+ * `__typename` are dropped.
628
+ */
629
+ const mapContentfulGqlLinks = (links) => {
630
+ const assets = new Map();
631
+ const entries = new Map();
632
+ for (const asset of [...present(links?.assets?.block), ...present(links?.assets?.hyperlink)]) {
633
+ assets.set(asset.sys.id, toRestAsset(asset));
634
+ }
635
+ for (const gqlEntry of [
636
+ ...present(links?.entries?.block),
637
+ ...present(links?.entries?.inline),
638
+ ...present(links?.entries?.hyperlink),
639
+ ]) {
640
+ const entry = toRestEntry(gqlEntry);
641
+ if (entry) {
642
+ entries.set(entry.sys.id, entry);
643
+ }
644
+ }
645
+ return { assets: [...assets.values()], entries: [...entries.values()] };
646
+ };
647
+
653
648
  const isRichTextRootNode = (node) => {
654
649
  return isObject(node) && 'nodeType' in node && node['nodeType'] === 'document';
655
650
  };
@@ -751,7 +746,7 @@ const createContentfulIncludeMap = (config) => {
751
746
  const entry = entryMap.get(id);
752
747
  if (!entry) {
753
748
  if (isDevMode()) {
754
- console.warn('Entry not found! Will return null. Is the include query param too low?', { id, entryMap });
749
+ console.warn('Entry not found! Will return null. Is it unpublished or deleted, or is the include query param too low?', { id });
755
750
  }
756
751
  return null;
757
752
  }
@@ -790,6 +785,23 @@ const createContentfulIncludeMap = (config) => {
790
785
  getAssets,
791
786
  };
792
787
  };
788
+ const warnedRichTextPaths = /* @__PURE__ */ new Set();
789
+ const warnOnUnresolvedParentPath = (content, richTextPath) => {
790
+ const segments = richTextPath.split('.');
791
+ const root = content;
792
+ if (segments.length < 2 || isObject(getObjectProperty(root, segments.slice(0, -1).join('.'))))
793
+ return;
794
+ if (warnedRichTextPaths.has(richTextPath))
795
+ return;
796
+ warnedRichTextPaths.add(richTextPath);
797
+ let resolvedSegments = 0;
798
+ while (resolvedSegments < segments.length - 1 &&
799
+ isObject(getObjectProperty(root, segments.slice(0, resolvedSegments + 1).join('.')))) {
800
+ resolvedSegments++;
801
+ }
802
+ const resolvedPath = segments.slice(0, resolvedSegments).join('.') || '(the content itself)';
803
+ 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 });
804
+ };
793
805
  class ContentfulRichTextRendererComponent {
794
806
  constructor() {
795
807
  this.viewContainerRef = inject(ViewContainerRef);
@@ -797,38 +809,72 @@ class ContentfulRichTextRendererComponent {
797
809
  this.elementRef = inject(ElementRef);
798
810
  this.config = injectContentfulConfig();
799
811
  this.document = inject(DOCUMENT);
812
+ this.location = inject(Location);
813
+ this.url = injectUrl();
800
814
  /**
801
815
  * The contentful response gotten via their REST api.
802
816
  * @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
803
817
  */
804
- this.content = input.required(/* @ts-ignore */
805
- ...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
818
+ this.content = input(/* @ts-ignore */
819
+ ...(ngDevMode ? [undefined, { debugName: "content" }] : /* istanbul ignore next */ []));
806
820
  /**
807
821
  * The path to where the rich text field is inside the contentful response. Dot and array notation is allowed.
808
822
  * @example "items[0].fields.html"
809
823
  */
810
- this.richTextPath = input.required(/* @ts-ignore */
811
- ...(ngDevMode ? [{ debugName: "richTextPath" }] : /* istanbul ignore next */ []));
824
+ this.richTextPath = input(/* @ts-ignore */
825
+ ...(ngDevMode ? [undefined, { debugName: "richTextPath" }] : /* istanbul ignore next */ []));
826
+ /**
827
+ * A rich-text field from the Contentful GraphQL API, passed as-is. An alternative to `content` and
828
+ * `richTextPath`; setting both is an error in dev mode.
829
+ * @example { json, links: { assets: { block, hyperlink }, entries: { block, inline, hyperlink } } }
830
+ */
831
+ this.gqlRichText = input(/* @ts-ignore */
832
+ ...(ngDevMode ? [undefined, { debugName: "gqlRichText" }] : /* istanbul ignore next */ []));
812
833
  this.executedCommandsCache = new Map();
813
- this.contentIncludesMap = computed(() => {
814
- const content = this.content();
815
- const assets = content?.includes?.Asset;
816
- const entries = content?.includes?.Entry;
817
- return createContentfulIncludeMap({ assets: assets ?? [], entries: entries ?? [] });
834
+ this.includes = computed(() => {
835
+ const gqlRichText = this.gqlRichText();
836
+ if (gqlRichText) {
837
+ return mapContentfulGqlLinks(gqlRichText.links);
838
+ }
839
+ const includes = this.content()?.includes;
840
+ return { assets: includes?.Asset ?? [], entries: includes?.Entry ?? [] };
818
841
  }, /* @ts-ignore */
842
+ ...(ngDevMode ? [{ debugName: "includes" }] : /* istanbul ignore next */ []));
843
+ this.contentIncludesMap = computed(() => createContentfulIncludeMap(this.includes()), /* @ts-ignore */
819
844
  ...(ngDevMode ? [{ debugName: "contentIncludesMap" }] : /* istanbul ignore next */ []));
845
+ this.includedEntries = computed(() => new Map(this.includes().entries.map((entry) => [entry.sys.id, entry])), /* @ts-ignore */
846
+ ...(ngDevMode ? [{ debugName: "includedEntries" }] : /* istanbul ignore next */ []));
820
847
  this.richTextData = computed(() => {
848
+ const gqlRichText = this.gqlRichText();
821
849
  const content = this.content();
822
850
  const richTextPath = this.richTextPath();
851
+ if (ngDevMode && gqlRichText && (content || richTextPath)) {
852
+ throw richTextRendererError('rich_text_conflicting_inputs', { gqlRichText, content, richTextPath });
853
+ }
854
+ if (gqlRichText) {
855
+ if (!isRichTextRootNode(gqlRichText.json)) {
856
+ throw richTextRendererError('rich_text_wrong_type', { gqlRichText });
857
+ }
858
+ return gqlRichText.json;
859
+ }
823
860
  if (!content) {
824
861
  return null;
825
862
  }
863
+ if (!richTextPath) {
864
+ if (ngDevMode) {
865
+ 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.');
866
+ }
867
+ return null;
868
+ }
826
869
  const richText = getObjectProperty(content, richTextPath);
827
870
  if (richText === null || richText === undefined) {
871
+ if (ngDevMode && richText === undefined) {
872
+ warnOnUnresolvedParentPath(content, richTextPath);
873
+ }
828
874
  return null;
829
875
  }
830
876
  if (!isObject(richText)) {
831
- throw richTextRendererError('rich_text_undefined', { content, richTextPath });
877
+ throw richTextRendererError('rich_text_not_object', { content, richTextPath, type: typeof richText });
832
878
  }
833
879
  if (!isRichTextRootNode(richText)) {
834
880
  throw richTextRendererError('rich_text_wrong_type', { content, richTextPath });
@@ -962,8 +1008,7 @@ class ContentfulRichTextRendererComponent {
962
1008
  }
963
1009
  const contentType = asset.fields.file?.contentType;
964
1010
  const assetComponents = this.config.components;
965
- const isMissing = !contentType && !asset.fields.file?.url;
966
- if (isMissing) {
1011
+ if (!asset.fields.file?.url) {
967
1012
  if (isDevMode()) {
968
1013
  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 });
969
1014
  }
@@ -1015,10 +1060,16 @@ class ContentfulRichTextRendererComponent {
1015
1060
  href = url ? normalizeHref(url) : null;
1016
1061
  }
1017
1062
  const linkTexts = node.content.filter((child) => child.nodeType === 'text' && Boolean(child.value));
1063
+ if (!linkTexts.length) {
1064
+ break;
1065
+ }
1018
1066
  const linkText = linkTexts.map((child) => child.value).join('');
1019
1067
  const linkComponent = href ? this.config.components.link : null;
1020
1068
  if (href && !linkComponent) {
1021
- const attributes = { class: DEFAULT_ANCHOR_CLASS, href };
1069
+ const anchorHref = isRouteRelativeHref(href)
1070
+ ? this.location.prepareExternalUrl(resolveHrefAgainstRoute(href, this.url()))
1071
+ : href;
1072
+ const attributes = { class: DEFAULT_ANCHOR_CLASS, href: anchorHref };
1022
1073
  if (isExternalWebHref(href, { location: this.document.location, internalHosts: this.config.internalHosts })) {
1023
1074
  attributes['target'] = '_blank';
1024
1075
  attributes['rel'] = 'noopener noreferrer';
@@ -1063,7 +1114,6 @@ class ContentfulRichTextRendererComponent {
1063
1114
  break;
1064
1115
  }
1065
1116
  const sharedMarks = linkTexts[0]?.marks.filter((mark) => linkTexts.every((child) => child.marks.some((other) => other.type === mark.type))) ?? [];
1066
- const textClass = marksToClass(sharedMarks);
1067
1117
  let linkComponentId = componentIdMap.get(LINK_COMPONENT_TYPE) ?? -1;
1068
1118
  const linkId = LINK_COMPONENT_TYPE + ++linkComponentId;
1069
1119
  componentIdMap.set(LINK_COMPONENT_TYPE, linkComponentId);
@@ -1073,7 +1123,7 @@ class ContentfulRichTextRendererComponent {
1073
1123
  domPosition,
1074
1124
  parentId: openElementIds.at(-1) ?? null,
1075
1125
  component: linkComponent,
1076
- inputs: { href, text: linkText, textClass, anchorClass: DEFAULT_ANCHOR_CLASS },
1126
+ inputs: { href, text: linkText, marks: sharedMarks.map((mark) => mark.type), richText: true },
1077
1127
  id: linkId,
1078
1128
  });
1079
1129
  domPosition++;
@@ -1085,10 +1135,10 @@ class ContentfulRichTextRendererComponent {
1085
1135
  if (!entryId) {
1086
1136
  throw richTextRendererError('entry_id_not_found', { node });
1087
1137
  }
1088
- const entry = this.contentIncludesMap().getEntry(entryId, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID);
1138
+ const entry = this.includedEntries().get(entryId);
1089
1139
  if (!entry) {
1090
1140
  if (isDevMode()) {
1091
- console.warn('Embedded entry is missing from the includes! The entry will be skipped. Is it unpublished or deleted?', { entryId, node });
1141
+ 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 });
1092
1142
  }
1093
1143
  break;
1094
1144
  }
@@ -1333,7 +1383,7 @@ class ContentfulRichTextRendererComponent {
1333
1383
  this.renderer.insertBefore(parentElement, node, previousSibling ? previousSibling.nextSibling : parentElement.firstChild);
1334
1384
  }
1335
1385
  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 }); }
1386
+ 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 } }, host: { classAttribute: "et-contentful-rich-text-renderer" }, ngImport: i0, template: ``, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
1337
1387
  }
1338
1388
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
1339
1389
  type: Component,
@@ -1345,7 +1395,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
1345
1395
  class: 'et-contentful-rich-text-renderer',
1346
1396
  },
1347
1397
  }]
1348
- }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: true }] }], richTextPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "richTextPath", required: true }] }] } });
1398
+ }], 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 }] }] } });
1399
+
1400
+ class ContentfulVideoComponent {
1401
+ constructor() {
1402
+ this.asset = input.required(/* @ts-ignore */
1403
+ ...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
1404
+ this.data = computed(() => {
1405
+ const asset = this.asset();
1406
+ if (!asset) {
1407
+ return null;
1408
+ }
1409
+ if (isContentfulGqlAsset(asset) && asset.url) {
1410
+ return {
1411
+ url: asset.url,
1412
+ contentType: asset.contentType,
1413
+ };
1414
+ }
1415
+ if (!isContentfulGqlAsset(asset) && asset.fields.file?.url) {
1416
+ return {
1417
+ url: asset.fields.file.url,
1418
+ contentType: asset.fields.file.contentType,
1419
+ };
1420
+ }
1421
+ return null;
1422
+ }, /* @ts-ignore */
1423
+ ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
1424
+ }
1425
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1426
+ 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: `
1427
+ @if (data(); as data) {
1428
+ <video class="et-contentful-video-video" controls>
1429
+ <source [src]="data.url" [attr.type]="data.contentType || null" class="et-contentful-video-source" />
1430
+ </video>
1431
+ }
1432
+ `, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
1433
+ }
1434
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
1435
+ type: Component,
1436
+ args: [{
1437
+ selector: 'et-contentful-video',
1438
+ template: `
1439
+ @if (data(); as data) {
1440
+ <video class="et-contentful-video-video" controls>
1441
+ <source [src]="data.url" [attr.type]="data.contentType || null" class="et-contentful-video-source" />
1442
+ </video>
1443
+ }
1444
+ `,
1445
+ encapsulation: ViewEncapsulation.None,
1446
+ host: {
1447
+ class: 'et-contentful-video',
1448
+ },
1449
+ }]
1450
+ }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }] } });
1349
1451
 
1350
1452
  const ContentfulImports = [
1351
1453
  ContentfulAudioComponent,
@@ -1355,6 +1457,24 @@ const ContentfulImports = [
1355
1457
  ContentfulRichTextRendererComponent,
1356
1458
  ];
1357
1459
 
1460
+ /**
1461
+ * The shipped audio, file, image, video and link components, to spread into `provideContentfulConfig`.
1462
+ * An app that brings its own components, or renders text only, leaves this out and bundles none of them.
1463
+ * To replace one, spread the map instead: `components: { ...CONTENTFUL_DEFAULT_COMPONENTS.components, image: MyImage }`.
1464
+ *
1465
+ * @example
1466
+ * provideContentfulConfig({ ...CONTENTFUL_DEFAULT_COMPONENTS });
1467
+ */
1468
+ const CONTENTFUL_DEFAULT_COMPONENTS = {
1469
+ components: {
1470
+ audio: ContentfulAudioComponent,
1471
+ file: ContentfulFileComponent,
1472
+ image: ContentfulImageComponent,
1473
+ video: ContentfulVideoComponent,
1474
+ link: ContentfulLinkComponent,
1475
+ },
1476
+ };
1477
+
1358
1478
  const provideContentfulConfig = (contentfulConfig) => {
1359
1479
  return { provide: CONTENTFUL_CONFIG, useValue: createContentfulConfig(contentfulConfig) };
1360
1480
  };
@@ -1364,5 +1484,5 @@ const isContentfulEntryType = (entry, type) => entry.sys.contentType.sys.id ===
1364
1484
  * Generated bundle index. Do not edit.
1365
1485
  */
1366
1486
 
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 };
1487
+ 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 };
1368
1488
  //# sourceMappingURL=ethlete-contentful.mjs.map