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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  import { NgClass, DOCUMENT } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
3
  import { input, computed, ViewEncapsulation, Component, inject, InjectionToken, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
4
- import { normalizePictureSizes, PictureComponent } from '@ethlete/components';
4
+ import { formatFileSize, normalizePictureSizes, PictureComponent } from '@ethlete/components';
5
+ import { defineLabels, toProvideFn, toInjectFn, toToken, injectUrl, RuntimeError, isObject, injectRenderer, getObjectProperty } from '@ethlete/core';
5
6
  import { Router, RouterLink } from '@angular/router';
6
- import { RuntimeError, isObject, injectRenderer, getObjectProperty } from '@ethlete/core';
7
7
 
8
8
  const GQL_FRAGMENT_CONTENTFUL_ASSET = `
9
9
  fragment ContentfulAssetData on Asset {
@@ -12,6 +12,7 @@ const GQL_FRAGMENT_CONTENTFUL_ASSET = `
12
12
  }
13
13
  url
14
14
  title
15
+ fileName
15
16
  width
16
17
  height
17
18
  description
@@ -42,13 +43,13 @@ class ContentfulAudioComponent {
42
43
  if (isContentfulGqlAsset(asset) && asset.url) {
43
44
  return {
44
45
  url: asset.url,
45
- title: asset.title,
46
+ title: asset.title || asset.fileName || null,
46
47
  };
47
48
  }
48
- if (!isContentfulGqlAsset(asset) && asset.fields.file.url) {
49
+ if (!isContentfulGqlAsset(asset) && asset.fields.file?.url) {
49
50
  return {
50
51
  url: asset.fields.file.url,
51
- title: asset.fields.title,
52
+ title: asset.fields.title || asset.fields.file.fileName || null,
52
53
  };
53
54
  }
54
55
  return null;
@@ -85,8 +86,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
85
86
  }]
86
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 }] }] } });
87
88
 
89
+ /** The built-in English labels. */
90
+ const DEFAULT_CONTENTFUL_FILE_LABELS = {
91
+ fileSize: (bytes) => `(${formatFileSize(bytes)})`,
92
+ };
93
+ const CONTENTFUL_FILE_LABELS_DEF = /* @__PURE__ */ defineLabels('CONTENTFUL_FILE_LABELS', DEFAULT_CONTENTFUL_FILE_LABELS);
94
+ /**
95
+ * Localize `et-contentful-file` for everything below this injector, and read the set in effect here as a signal.
96
+ * Partial - whatever you leave out keeps its {@link DEFAULT_CONTENTFUL_FILE_LABELS} value.
97
+ *
98
+ * @example
99
+ * provideContentfulFileLabels({ fileSize: (bytes) => `(${new Intl.NumberFormat('de').format(bytes)} Byte)` });
100
+ */
101
+ const provideContentfulFileLabels = /* @__PURE__ */ toProvideFn(CONTENTFUL_FILE_LABELS_DEF);
102
+ const injectContentfulFileLabels = /* @__PURE__ */ toInjectFn(CONTENTFUL_FILE_LABELS_DEF);
103
+ const CONTENTFUL_FILE_LABELS = /* @__PURE__ */ toToken(CONTENTFUL_FILE_LABELS_DEF);
104
+
88
105
  class ContentfulFileComponent {
89
106
  constructor() {
107
+ this.labels = injectContentfulFileLabels();
90
108
  this.asset = input.required(/* @ts-ignore */
91
109
  ...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
92
110
  this.fileClass = input(null, /* @ts-ignore */
@@ -100,14 +118,14 @@ class ContentfulFileComponent {
100
118
  return {
101
119
  url: asset.url,
102
120
  size: asset.size,
103
- title: asset.title,
121
+ title: asset.title || asset.fileName || null,
104
122
  };
105
123
  }
106
- if (!isContentfulGqlAsset(asset) && asset.fields.file.url) {
124
+ if (!isContentfulGqlAsset(asset) && asset.fields.file?.url) {
107
125
  return {
108
126
  url: asset.fields.file.url,
109
127
  size: asset.fields.file.details.size,
110
- title: asset.fields.title,
128
+ title: asset.fields.title || asset.fields.file.fileName || null,
111
129
  };
112
130
  }
113
131
  return null;
@@ -120,7 +138,7 @@ class ContentfulFileComponent {
120
138
  <a [href]="data.url" [ngClass]="fileClass()" target="_blank" rel="noopener noreferrer">
121
139
  {{ data.title }}
122
140
  @if (data.size !== null) {
123
- ({{ data.size }} Bytes)
141
+ {{ labels().fileSize(data.size) }}
124
142
  }
125
143
  </a>
126
144
  }
@@ -135,7 +153,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
135
153
  <a [href]="data.url" [ngClass]="fileClass()" target="_blank" rel="noopener noreferrer">
136
154
  {{ data.title }}
137
155
  @if (data.size !== null) {
138
- ({{ data.size }} Bytes)
156
+ {{ labels().fileSize(data.size) }}
139
157
  }
140
158
  </a>
141
159
  }
@@ -172,23 +190,35 @@ const matchesHostname = (hostname, configuredHost) => {
172
190
  const normalizedHost = normalizeHostname(configuredHost);
173
191
  return hostname === normalizedHost || hostname.endsWith('.' + normalizedHost);
174
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
+
175
200
  class ContentfulLinkComponent {
176
201
  constructor() {
177
202
  this.document = inject(DOCUMENT);
178
203
  this.router = inject(Router);
179
204
  this.config = injectContentfulConfig();
205
+ this.url = injectUrl();
180
206
  this.href = input.required(/* @ts-ignore */
181
207
  ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
182
208
  this.text = input.required(/* @ts-ignore */
183
209
  ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
184
210
  this.textClass = input('', /* @ts-ignore */
185
211
  ...(ngDevMode ? [{ debugName: "textClass" }] : /* istanbul ignore next */ []));
212
+ this.anchorClass = input('', /* @ts-ignore */
213
+ ...(ngDevMode ? [{ debugName: "anchorClass" }] : /* istanbul ignore next */ []));
186
214
  this.usesRouterLink = computed(() => {
187
215
  const href = this.href();
188
216
  const absoluteUrl = parseWebUrl(href);
189
217
  if (absoluteUrl) {
190
- const internalHosts = [this.document.location.hostname, ...this.config.internalHosts];
191
- return internalHosts.some((host) => matchesHostname(absoluteUrl.hostname, host));
218
+ return isInternalWebUrl(absoluteUrl, {
219
+ location: this.document.location,
220
+ internalHosts: this.config.internalHosts,
221
+ });
192
222
  }
193
223
  return !href.startsWith('#') && !/^[a-z][a-z\d+.-]*:/i.test(href);
194
224
  }, /* @ts-ignore */
@@ -198,23 +228,23 @@ class ContentfulLinkComponent {
198
228
  this.internalPath = computed(() => {
199
229
  const href = this.href();
200
230
  const url = parseWebUrl(href);
201
- if (!url) {
231
+ if (url) {
232
+ return url.pathname + url.search + url.hash;
233
+ }
234
+ if (href.startsWith('/')) {
202
235
  return href;
203
236
  }
204
- return url.pathname + url.search + url.hash;
237
+ const resolved = new URL(href, 'https://contentful.invalid' + this.url());
238
+ return resolved.pathname + resolved.search + resolved.hash;
205
239
  }, /* @ts-ignore */
206
240
  ...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
207
241
  this.internalUrlTree = computed(() => this.router.parseUrl(this.internalPath()), /* @ts-ignore */
208
242
  ...(ngDevMode ? [{ debugName: "internalUrlTree" }] : /* istanbul ignore next */ []));
209
- this.linkClass = computed(() => {
210
- const base = 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a';
211
- const extra = this.textClass();
212
- return extra ? `${base} ${extra}` : base;
213
- }, /* @ts-ignore */
243
+ this.linkClass = computed(() => [this.anchorClass(), this.textClass()].filter(Boolean).join(' ') || null, /* @ts-ignore */
214
244
  ...(ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []));
215
245
  }
216
246
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
217
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulLinkComponent, isStandalone: true, selector: "et-contentful-link", inputs: { href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, textClass: { classPropertyName: "textClass", publicName: "textClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-link" }, ngImport: i0, template: `
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: `
218
248
  @if (usesRouterLink()) {
219
249
  <a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
220
250
  } @else {
@@ -245,7 +275,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
245
275
  `, encapsulation: ViewEncapsulation.None, imports: [RouterLink], host: {
246
276
  class: 'et-contentful-link',
247
277
  }, styles: ["@layer components{.et-contentful-link{display:contents}}\n"] }]
248
- }], propDecorators: { href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], textClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "textClass", required: false }] }] } });
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 }] }] } });
249
279
 
250
280
  class ContentfulVideoComponent {
251
281
  constructor() {
@@ -264,7 +294,7 @@ class ContentfulVideoComponent {
264
294
  contentType: asset.contentType,
265
295
  };
266
296
  }
267
- if (!isContentfulGqlAsset(asset) && asset.fields.file.url) {
297
+ if (!isContentfulGqlAsset(asset) && asset.fields.file?.url) {
268
298
  return {
269
299
  url: asset.fields.file.url,
270
300
  contentType: asset.fields.file.contentType,
@@ -319,8 +349,8 @@ const CONTENTFUL_FALLBACK_CONFIG = {
319
349
  backgroundColor: null,
320
350
  },
321
351
  };
322
- const createContentfulConfig = (config) => ({
323
- ...{
352
+ const createContentfulConfig = (config) => {
353
+ const defaults = {
324
354
  internalHosts: [],
325
355
  components: {
326
356
  audio: ContentfulAudioComponent,
@@ -335,9 +365,14 @@ const createContentfulConfig = (config) => ({
335
365
  sizes: ['100vw'],
336
366
  backgroundColor: null,
337
367
  },
338
- },
339
- ...(config ?? {}),
340
- });
368
+ };
369
+ return {
370
+ ...defaults,
371
+ ...config,
372
+ components: { ...defaults.components, ...config?.components },
373
+ imageOptions: { ...defaults.imageOptions, ...config?.imageOptions },
374
+ };
375
+ };
341
376
  /**
342
377
  * Reads the contentful config from the current injector. Falls back to a config without
343
378
  * any `components` when none was provided via `provideContentfulConfig()`.
@@ -357,15 +392,16 @@ const generateDefaultContentfulImageSource = (data) => {
357
392
  srcset: data.url,
358
393
  };
359
394
  }
360
- if (!data.fields.file.contentType || !data.fields.file.url) {
395
+ const file = data.fields.file;
396
+ if (!file?.contentType || !file.url) {
361
397
  return {
362
398
  type: '',
363
399
  srcset: '',
364
400
  };
365
401
  }
366
402
  return {
367
- type: data.fields.file.contentType,
368
- srcset: data.fields.file.url,
403
+ type: file.contentType,
404
+ srcset: file.url,
369
405
  };
370
406
  };
371
407
  /**
@@ -392,13 +428,20 @@ const parseContentfulImageSize = (size) => {
392
428
  return width ? { width: Number(width[1]), height: null } : { width: null, height: null };
393
429
  };
394
430
  const SOURCE_TYPES = ['image/avif', 'image/webp'];
395
- const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior) => {
431
+ /**
432
+ * Builds AVIF and WebP `<source>` entries for a Contentful asset through the Contentful Images API.
433
+ *
434
+ * @example
435
+ * generateContentfulImageSources(asset, { srcsetSizes: ['400w', '800w'], quality: 80 });
436
+ */
437
+ const generateContentfulImageSources = (data, options = {}) => {
438
+ const { srcsetSizes = [], backgroundColor = null, quality = null, focusArea = null, resizeBehavior = null } = options;
396
439
  const isGqlAsset = isContentfulGqlAsset(data);
397
- const baseUrl = isGqlAsset ? data.url : data.fields.file.url;
440
+ const baseUrl = isGqlAsset ? data.url : data.fields.file?.url;
398
441
  if (!baseUrl) {
399
442
  return [];
400
443
  }
401
- const imageDimensions = isGqlAsset ? data : data.fields.file.details.image;
444
+ const imageDimensions = isGqlAsset ? data : data.fields.file?.details.image;
402
445
  const sources = [];
403
446
  for (const type of SOURCE_TYPES) {
404
447
  const sourceSets = [];
@@ -416,7 +459,7 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, qual
416
459
  if (resizeBehavior) {
417
460
  queryParams.push(`fit=${resizeBehavior}`);
418
461
  }
419
- if (srcsetSizes?.length) {
462
+ if (srcsetSizes.length) {
420
463
  const urlWithParams = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${queryParams.join('&')}`;
421
464
  for (const size of srcsetSizes) {
422
465
  const { width, height } = parseContentfulImageSize(size);
@@ -484,15 +527,17 @@ class ContentfulImageComponent {
484
527
  ...(ngDevMode ? [{ debugName: "normalizedAsset" }] : /* istanbul ignore next */ []));
485
528
  this.sourcesValue = computed(() => {
486
529
  const asset = this.asset();
487
- const backgroundColor = this.backgroundColor();
488
- const srcsetSizes = this.srcsetSizes();
489
- const quality = this.quality();
490
- const focusArea = this.focusArea();
491
- const resizeBehavior = this.resizeBehavior();
530
+ const options = {
531
+ srcsetSizes: this.srcsetSizes(),
532
+ backgroundColor: this.backgroundColor(),
533
+ quality: this.quality(),
534
+ focusArea: this.focusArea(),
535
+ resizeBehavior: this.resizeBehavior(),
536
+ };
492
537
  if (!asset) {
493
538
  return [];
494
539
  }
495
- return generateContentfulImageSources(asset, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior);
540
+ return generateContentfulImageSources(asset, options);
496
541
  }, /* @ts-ignore */
497
542
  ...(ngDevMode ? [{ debugName: "sourcesValue" }] : /* istanbul ignore next */ []));
498
543
  this.defaultSrcValue = computed(() => {
@@ -587,22 +632,18 @@ const RICH_TEXT_RENDERER_ERRORS = {
587
632
  rich_text_wrong_type: 'The rich text object does not satisfy the RichTextResponse interface. It should contain a property named "nodeType" with the value "document".',
588
633
  asset_id_not_found: 'The asset ID was not found. This node is not supported.',
589
634
  entry_id_not_found: 'The entry ID was not found. This node is not supported.',
590
- asset_not_found: 'The asset was not found. This node is not supported.',
591
- entry_not_found: 'The entry was not found. This node is not supported.',
592
- custom_component_not_found: 'No custom component found for entry type. Please provide one for this type.',
593
635
  text_parent_not_found: 'The parent node is not found. This structure is not supported.',
594
- text_parent_wrong_type: 'The parent node neither a html element nor a custom component. This structure is not supported.',
636
+ cached_command_not_found: 'No rendered node was found for a render command. The renderer state is out of sync.',
637
+ cached_command_not_component: 'The rendered node for a component command is not a component. The renderer state is out of sync.',
595
638
  };
596
639
  const RICH_TEXT_RENDERER_ERROR_CODES = {
597
640
  rich_text_undefined: 0,
598
641
  rich_text_wrong_type: 1,
599
642
  asset_id_not_found: 2,
600
643
  entry_id_not_found: 3,
601
- asset_not_found: 4,
602
- entry_not_found: 5,
603
- custom_component_not_found: 6,
604
644
  text_parent_not_found: 7,
605
- text_parent_wrong_type: 8,
645
+ cached_command_not_found: 9,
646
+ cached_command_not_component: 10,
606
647
  };
607
648
  const richTextRendererError = (code, data) => {
608
649
  const message = `<et-contentful-rich-text-renderer>: ${RICH_TEXT_RENDERER_ERRORS[code]}`;
@@ -612,60 +653,43 @@ const richTextRendererError = (code, data) => {
612
653
  const isRichTextRootNode = (node) => {
613
654
  return isObject(node) && 'nodeType' in node && node['nodeType'] === 'document';
614
655
  };
656
+ const NODE_TYPE_TAGS = /* @__PURE__ */ (() => ({
657
+ [CF_BLOCKS.HEADING_1]: 'h1',
658
+ [CF_BLOCKS.HEADING_2]: 'h2',
659
+ [CF_BLOCKS.HEADING_3]: 'h3',
660
+ [CF_BLOCKS.HEADING_4]: 'h4',
661
+ [CF_BLOCKS.HEADING_5]: 'h5',
662
+ [CF_BLOCKS.HEADING_6]: 'h6',
663
+ [CF_BLOCKS.PARAGRAPH]: 'p',
664
+ [CF_BLOCKS.UL_LIST]: 'ul',
665
+ [CF_BLOCKS.OL_LIST]: 'ol',
666
+ [CF_BLOCKS.LIST_ITEM]: 'li',
667
+ [CF_BLOCKS.HR]: 'hr',
668
+ [CF_BLOCKS.QUOTE]: 'blockquote',
669
+ [CF_BLOCKS.TABLE]: 'table',
670
+ [CF_BLOCKS.TABLE_ROW]: 'tr',
671
+ [CF_BLOCKS.TABLE_CELL]: 'td',
672
+ [CF_BLOCKS.TABLE_HEADER_CELL]: 'th',
673
+ }))();
674
+ const INLINE_NODE_TYPES = /* @__PURE__ */ (() => new Set(Object.values(CF_INLINES)))();
675
+ /**
676
+ * The html element a structural rich text node renders as. A node type without an element
677
+ * (an unsupported inline or block) falls back to `span` for inlines and `div` otherwise,
678
+ * with a dev-mode warning.
679
+ */
615
680
  const translateContentfulNodeTypeToHtmlTag = (nodeType) => {
616
- switch (nodeType) {
617
- case CF_BLOCKS.HEADING_1:
618
- return 'h1';
619
- case CF_BLOCKS.HEADING_2:
620
- return 'h2';
621
- case CF_BLOCKS.HEADING_3:
622
- return 'h3';
623
- case CF_BLOCKS.HEADING_4:
624
- return 'h4';
625
- case CF_BLOCKS.HEADING_5:
626
- return 'h5';
627
- case CF_BLOCKS.HEADING_6:
628
- return 'h6';
629
- case CF_BLOCKS.PARAGRAPH:
630
- return 'p';
631
- case CF_BLOCKS.UL_LIST:
632
- return 'ul';
633
- case CF_BLOCKS.OL_LIST:
634
- return 'ol';
635
- case CF_BLOCKS.LIST_ITEM:
636
- return 'li';
637
- case CF_BLOCKS.HR:
638
- return 'hr';
639
- case CF_BLOCKS.QUOTE:
640
- return 'blockquote';
641
- case CF_BLOCKS.TABLE:
642
- return 'table';
643
- case CF_BLOCKS.TABLE_ROW:
644
- return 'tr';
645
- case CF_BLOCKS.TABLE_CELL:
646
- return 'td';
647
- case CF_BLOCKS.TABLE_HEADER_CELL:
648
- return 'th';
649
- case CF_BLOCKS.EMBEDDED_ASSET:
650
- return 'div';
651
- case CF_INLINES.ENTRY_HYPERLINK:
652
- return 'a';
653
- case CF_INLINES.ASSET_HYPERLINK:
654
- return 'a';
655
- case 'text':
656
- return 'span';
657
- // Will be ignored by the renderer
658
- case 'document':
659
- return 'div';
660
- case CF_BLOCKS.EMBEDDED_ENTRY:
661
- return 'div';
662
- case CF_INLINES.EMBEDDED_ENTRY:
663
- return 'div';
664
- default:
665
- return 'div';
681
+ const tag = NODE_TYPE_TAGS[nodeType];
682
+ if (tag) {
683
+ return tag;
684
+ }
685
+ const fallback = INLINE_NODE_TYPES.has(nodeType) ? 'span' : 'div';
686
+ if (isDevMode()) {
687
+ console.warn(`Unsupported rich text node type "${nodeType}"! Its content is rendered inside a <${fallback}>.`);
666
688
  }
689
+ return fallback;
667
690
  };
668
691
 
692
+ const DEFAULT_ANCHOR_CLASS = 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a';
669
693
  const MARK_TAG_MAP = {
670
694
  bold: 'strong',
671
695
  italic: 'em',
@@ -727,7 +751,7 @@ const createContentfulIncludeMap = (config) => {
727
751
  const entry = entryMap.get(id);
728
752
  if (!entry) {
729
753
  if (isDevMode()) {
730
- console.warn('Entry not found! Will return null. Is the include query param to low?', { id, entryMap });
754
+ console.warn('Entry not found! Will return null. Is the include query param too low?', { id, entryMap });
731
755
  }
732
756
  return null;
733
757
  }
@@ -772,6 +796,7 @@ class ContentfulRichTextRendererComponent {
772
796
  this.renderer = injectRenderer();
773
797
  this.elementRef = inject(ElementRef);
774
798
  this.config = injectContentfulConfig();
799
+ this.document = inject(DOCUMENT);
775
800
  /**
776
801
  * The contentful response gotten via their REST api.
777
802
  * @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
@@ -825,18 +850,6 @@ class ContentfulRichTextRendererComponent {
825
850
  const instructions = [];
826
851
  const preserved = new Set();
827
852
  const needsReattach = new Set();
828
- const parentIdOf = (command) => {
829
- if (command.nestingLevel === 0) {
830
- return null;
831
- }
832
- for (let i = command.index - 1; i >= 0; i--) {
833
- const cmd = commands[i];
834
- if (cmd && cmd.nestingLevel === command.nestingLevel - 1 && cmd.kind === 'htmlOpen') {
835
- return cmd.id;
836
- }
837
- }
838
- return null;
839
- };
840
853
  const markTagsEqual = (a, b) => a.length === b.length && a.every((tag, index) => tag === b[index]);
841
854
  const attributesEqual = (a, b) => {
842
855
  const aKeys = Object.keys(a);
@@ -853,8 +866,7 @@ class ContentfulRichTextRendererComponent {
853
866
  if (previous.kind === 'component' && command.kind === 'component' && previous.component !== command.component) {
854
867
  continue;
855
868
  }
856
- const parentId = parentIdOf(command);
857
- const parentPreserved = parentId === null || preserved.has(parentId);
869
+ const parentPreserved = previous.parentId === command.parentId && (command.parentId === null || preserved.has(command.parentId));
858
870
  if (command.kind === 'component') {
859
871
  preserved.add(command.id);
860
872
  const samePlace = parentPreserved &&
@@ -913,7 +925,7 @@ class ContentfulRichTextRendererComponent {
913
925
  let nestingLevel = 0;
914
926
  let domPosition = 0;
915
927
  let textId = 0;
916
- let commandIndex = 0;
928
+ const openElementIds = [];
917
929
  const traverse = (node) => {
918
930
  switch (node.nodeType) {
919
931
  case 'text': {
@@ -927,7 +939,7 @@ class ContentfulRichTextRendererComponent {
927
939
  kind: 'text',
928
940
  nestingLevel,
929
941
  domPosition,
930
- index: commandIndex++,
942
+ parentId: openElementIds.at(-1) ?? null,
931
943
  attributes,
932
944
  markTags: node.marks.length ? marksToTags(node.marks) : [],
933
945
  text,
@@ -943,11 +955,14 @@ class ContentfulRichTextRendererComponent {
943
955
  }
944
956
  const asset = this.contentIncludesMap().getAsset(assetId);
945
957
  if (!asset) {
946
- throw richTextRendererError('asset_not_found', { assetId, node });
958
+ if (isDevMode()) {
959
+ console.warn('Embedded asset is missing from the includes! The asset will be skipped. Is it unpublished or deleted?', { assetId, node });
960
+ }
961
+ break;
947
962
  }
948
- const contentType = asset.fields.file.contentType;
963
+ const contentType = asset.fields.file?.contentType;
949
964
  const assetComponents = this.config.components;
950
- const isMissing = !contentType && !asset.fields.file.url;
965
+ const isMissing = !contentType && !asset.fields.file?.url;
951
966
  if (isMissing) {
952
967
  if (isDevMode()) {
953
968
  console.warn('Asset is missing file data! Asset will be skipped. Did you forget to upload a file for the current translation in Contentful?', { asset });
@@ -978,7 +993,7 @@ class ContentfulRichTextRendererComponent {
978
993
  kind: 'component',
979
994
  nestingLevel,
980
995
  domPosition,
981
- index: commandIndex++,
996
+ parentId: openElementIds.at(-1) ?? null,
982
997
  component,
983
998
  inputs: { asset },
984
999
  id,
@@ -996,52 +1011,42 @@ class ContentfulRichTextRendererComponent {
996
1011
  else if (node.nodeType === CF_INLINES.ASSET_HYPERLINK) {
997
1012
  const assetId = node.data['target']?.sys?.id;
998
1013
  const asset = assetId ? this.contentIncludesMap().getAsset(assetId) : null;
999
- href = asset?.fields.file.url ? normalizeHref(asset.fields.file.url) : null;
1000
- }
1001
- let linkText = '';
1002
- const linkMarks = [];
1003
- for (const child of node.content) {
1004
- if (child.nodeType === 'text') {
1005
- linkText += child.value;
1006
- linkMarks.push(...child.marks);
1007
- }
1014
+ const url = asset?.fields.file?.url;
1015
+ href = url ? normalizeHref(url) : null;
1008
1016
  }
1017
+ const linkTexts = node.content.filter((child) => child.nodeType === 'text' && Boolean(child.value));
1018
+ const linkText = linkTexts.map((child) => child.value).join('');
1009
1019
  const linkComponent = href ? this.config.components.link : null;
1010
1020
  if (href && !linkComponent) {
1021
+ const attributes = { class: DEFAULT_ANCHOR_CLASS, href };
1022
+ if (isExternalWebHref(href, { location: this.document.location, internalHosts: this.config.internalHosts })) {
1023
+ attributes['target'] = '_blank';
1024
+ attributes['rel'] = 'noopener noreferrer';
1025
+ }
1011
1026
  rootCommands.push({
1012
1027
  kind: 'htmlOpen',
1013
1028
  nestingLevel,
1014
1029
  domPosition,
1015
- index: commandIndex++,
1016
- attributes: {
1017
- class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a',
1018
- href,
1019
- },
1030
+ parentId: openElementIds.at(-1) ?? null,
1031
+ attributes,
1020
1032
  tagName: 'a',
1021
1033
  id: 'e-o' + elementOpenId++,
1022
1034
  });
1023
1035
  const anchorDomPosition = domPosition;
1036
+ openElementIds.push('e-o' + (elementOpenId - 1));
1024
1037
  nestingLevel++;
1025
1038
  domPosition = 0;
1026
- rootCommands.push({
1027
- kind: 'text',
1028
- nestingLevel,
1029
- domPosition,
1030
- index: commandIndex++,
1031
- attributes: {
1032
- class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
1033
- },
1034
- markTags: marksToTags(linkMarks),
1035
- text: linkText,
1036
- id: 't' + textId++,
1037
- });
1039
+ for (const child of linkTexts) {
1040
+ traverse(child);
1041
+ }
1042
+ openElementIds.pop();
1038
1043
  nestingLevel--;
1039
1044
  domPosition = anchorDomPosition;
1040
1045
  rootCommands.push({
1041
1046
  kind: 'htmlClose',
1042
1047
  nestingLevel,
1043
1048
  domPosition,
1044
- index: commandIndex++,
1049
+ parentId: openElementIds.at(-1) ?? null,
1045
1050
  tagName: 'a',
1046
1051
  id: 'e-c' + elementCloseId++,
1047
1052
  });
@@ -1049,25 +1054,16 @@ class ContentfulRichTextRendererComponent {
1049
1054
  break;
1050
1055
  }
1051
1056
  if (!href) {
1052
- rootCommands.push({
1053
- kind: 'text',
1054
- nestingLevel,
1055
- domPosition,
1056
- index: commandIndex++,
1057
- attributes: {
1058
- class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
1059
- },
1060
- markTags: marksToTags(linkMarks),
1061
- text: linkText,
1062
- id: 't' + textId++,
1063
- });
1064
- domPosition++;
1057
+ for (const child of linkTexts) {
1058
+ traverse(child);
1059
+ }
1065
1060
  break;
1066
1061
  }
1067
1062
  if (!linkComponent) {
1068
1063
  break;
1069
1064
  }
1070
- const textClass = linkMarks.length ? marksToClass(linkMarks) : '';
1065
+ const sharedMarks = linkTexts[0]?.marks.filter((mark) => linkTexts.every((child) => child.marks.some((other) => other.type === mark.type))) ?? [];
1066
+ const textClass = marksToClass(sharedMarks);
1071
1067
  let linkComponentId = componentIdMap.get(LINK_COMPONENT_TYPE) ?? -1;
1072
1068
  const linkId = LINK_COMPONENT_TYPE + ++linkComponentId;
1073
1069
  componentIdMap.set(LINK_COMPONENT_TYPE, linkComponentId);
@@ -1075,9 +1071,9 @@ class ContentfulRichTextRendererComponent {
1075
1071
  kind: 'component',
1076
1072
  nestingLevel,
1077
1073
  domPosition,
1078
- index: commandIndex++,
1074
+ parentId: openElementIds.at(-1) ?? null,
1079
1075
  component: linkComponent,
1080
- inputs: { href, text: linkText, textClass },
1076
+ inputs: { href, text: linkText, textClass, anchorClass: DEFAULT_ANCHOR_CLASS },
1081
1077
  id: linkId,
1082
1078
  });
1083
1079
  domPosition++;
@@ -1091,16 +1087,18 @@ class ContentfulRichTextRendererComponent {
1091
1087
  }
1092
1088
  const entry = this.contentIncludesMap().getEntry(entryId, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID);
1093
1089
  if (!entry) {
1094
- throw richTextRendererError('entry_not_found', { entryId, node });
1090
+ if (isDevMode()) {
1091
+ console.warn('Embedded entry is missing from the includes! The entry will be skipped. Is it unpublished or deleted?', { entryId, node });
1092
+ }
1093
+ break;
1095
1094
  }
1096
1095
  const componentType = entry.sys.contentType.sys.id;
1097
1096
  const component = this.config.customComponents[componentType];
1098
1097
  if (!component) {
1099
- throw richTextRendererError('custom_component_not_found', {
1100
- componentType,
1101
- customComponents: this.config.customComponents,
1102
- entry,
1103
- });
1098
+ if (isDevMode()) {
1099
+ console.warn('No custom component registered for this embedded entry type! The entry will be skipped. Provide one via provideContentfulConfig({ customComponents: … }).', { componentType, customComponents: this.config.customComponents, entry });
1100
+ }
1101
+ break;
1104
1102
  }
1105
1103
  const occurrenceKey = 'entry:' + entryId;
1106
1104
  const occurrence = (componentIdMap.get(occurrenceKey) ?? -1) + 1;
@@ -1110,7 +1108,7 @@ class ContentfulRichTextRendererComponent {
1110
1108
  kind: 'component',
1111
1109
  nestingLevel,
1112
1110
  domPosition,
1113
- index: commandIndex++,
1111
+ parentId: openElementIds.at(-1) ?? null,
1114
1112
  component,
1115
1113
  inputs: {
1116
1114
  fields: entry.fields,
@@ -1132,17 +1130,19 @@ class ContentfulRichTextRendererComponent {
1132
1130
  kind: 'htmlOpen',
1133
1131
  nestingLevel,
1134
1132
  domPosition,
1135
- index: commandIndex++,
1133
+ parentId: openElementIds.at(-1) ?? null,
1136
1134
  attributes,
1137
1135
  tagName: tag,
1138
1136
  id: 'e-o' + elementOpenId++,
1139
1137
  });
1140
1138
  const domPositionAtThisLevel = domPosition;
1139
+ openElementIds.push('e-o' + (elementOpenId - 1));
1141
1140
  nestingLevel++;
1142
1141
  domPosition = 0;
1143
1142
  for (const child of node.content) {
1144
1143
  traverse(child);
1145
1144
  }
1145
+ openElementIds.pop();
1146
1146
  nestingLevel--;
1147
1147
  domPosition = domPositionAtThisLevel;
1148
1148
  const lastCommand = rootCommands[rootCommands.length - 1];
@@ -1152,14 +1152,13 @@ class ContentfulRichTextRendererComponent {
1152
1152
  lastCommand.tagName !== 'hr') {
1153
1153
  rootCommands.pop();
1154
1154
  elementOpenId--;
1155
- commandIndex--;
1156
1155
  }
1157
1156
  else {
1158
1157
  rootCommands.push({
1159
1158
  kind: 'htmlClose',
1160
1159
  nestingLevel,
1161
1160
  domPosition,
1162
- index: commandIndex++,
1161
+ parentId: openElementIds.at(-1) ?? null,
1163
1162
  tagName: tag,
1164
1163
  id: 'e-c' + elementCloseId++,
1165
1164
  });
@@ -1175,26 +1174,32 @@ class ContentfulRichTextRendererComponent {
1175
1174
  return rootCommands;
1176
1175
  }
1177
1176
  execInstructions(instructions) {
1178
- for (const instruction of instructions) {
1179
- switch (instruction.type) {
1177
+ const lastPlacedChild = new Map();
1178
+ for (const { type, command } of instructions) {
1179
+ if (type === 'delete') {
1180
+ this.runDeleteInstruction(command);
1181
+ continue;
1182
+ }
1183
+ const previousSibling = lastPlacedChild.get(command.parentId) ?? null;
1184
+ switch (type) {
1180
1185
  case 'create':
1181
- this.runCreateInstruction(instruction.command);
1186
+ this.runCreateInstruction(command, previousSibling);
1182
1187
  break;
1183
1188
  case 'update':
1184
- this.runUpdateInstruction(instruction.command);
1189
+ this.runUpdateInstruction(command);
1185
1190
  break;
1186
1191
  case 'move':
1187
- this.runMoveInstruction(instruction.command);
1188
- break;
1189
- case 'delete':
1190
- this.runDeleteInstruction(instruction.command);
1192
+ this.runMoveInstruction(command, previousSibling);
1191
1193
  break;
1192
1194
  }
1195
+ const element = this.executedCommandsCache.get(command.id)?.element;
1196
+ if (element) {
1197
+ lastPlacedChild.set(command.parentId, element);
1198
+ }
1193
1199
  }
1194
1200
  }
1195
- runCreateInstruction(command) {
1201
+ runCreateInstruction(command, previousSibling) {
1196
1202
  const parentElement = this.findParent(command);
1197
- const nextElement = this.findFollowingElement(command);
1198
1203
  if (command.kind === 'component') {
1199
1204
  const inputs = signal(command.inputs, /* @ts-ignore */
1200
1205
  ...(ngDevMode ? [{ debugName: "inputs" }] : /* istanbul ignore next */ []));
@@ -1204,7 +1209,7 @@ class ContentfulRichTextRendererComponent {
1204
1209
  .map((declared) => inputBinding(declared.templateName, () => inputs()[declared.propName]));
1205
1210
  const componentRef = this.viewContainerRef.createComponent(command.component, { bindings });
1206
1211
  const rootNode = this.getComponentRootNode(componentRef);
1207
- this.renderInsertOrAppend(rootNode, parentElement, nextElement);
1212
+ this.renderInsertAfter(rootNode, { parentElement, previousSibling });
1208
1213
  this.executedCommandsCache.set(command.id, {
1209
1214
  command,
1210
1215
  componentRef,
@@ -1234,7 +1239,7 @@ class ContentfulRichTextRendererComponent {
1234
1239
  this.renderer.appendChild(textContainer, textNode);
1235
1240
  }
1236
1241
  }
1237
- this.renderInsertOrAppend(span, parentElement, nextElement);
1242
+ this.renderInsertAfter(span, { parentElement, previousSibling });
1238
1243
  this.executedCommandsCache.set(command.id, {
1239
1244
  command,
1240
1245
  element: span,
@@ -1245,7 +1250,7 @@ class ContentfulRichTextRendererComponent {
1245
1250
  for (const [key, value] of Object.entries(command.attributes)) {
1246
1251
  this.renderer.setAttribute(element, key, value);
1247
1252
  }
1248
- this.renderInsertOrAppend(element, parentElement, nextElement);
1253
+ this.renderInsertAfter(element, { parentElement, previousSibling });
1249
1254
  this.executedCommandsCache.set(command.id, {
1250
1255
  command,
1251
1256
  element,
@@ -1255,11 +1260,11 @@ class ContentfulRichTextRendererComponent {
1255
1260
  runUpdateInstruction(command) {
1256
1261
  const cached = this.executedCommandsCache.get(command.id);
1257
1262
  if (!cached) {
1258
- throw new Error('Cached command not found!');
1263
+ throw richTextRendererError('cached_command_not_found', { command });
1259
1264
  }
1260
1265
  if (command.kind === 'component') {
1261
1266
  if (!isExecutedComponentCommandCacheItem(cached)) {
1262
- throw new Error('Cached command is not a component command!');
1267
+ throw richTextRendererError('cached_command_not_component', { command });
1263
1268
  }
1264
1269
  cached.inputs.set(command.inputs);
1265
1270
  }
@@ -1268,23 +1273,21 @@ class ContentfulRichTextRendererComponent {
1268
1273
  command,
1269
1274
  });
1270
1275
  }
1271
- runMoveInstruction(command) {
1276
+ runMoveInstruction(command, previousSibling) {
1272
1277
  const cached = this.executedCommandsCache.get(command.id);
1273
1278
  if (!cached) {
1274
- throw new Error('Cached command not found!');
1279
+ throw richTextRendererError('cached_command_not_found', { command });
1275
1280
  }
1276
1281
  if (command.kind === 'component') {
1277
1282
  if (!isExecutedComponentCommandCacheItem(cached)) {
1278
- throw new Error('Cached command is not a component command!');
1283
+ throw richTextRendererError('cached_command_not_component', { command });
1279
1284
  }
1280
1285
  const rootNode = cached.element;
1281
1286
  const oldParentElement = cached.element.parentElement;
1282
1287
  if (oldParentElement) {
1283
1288
  this.renderer.removeChild(oldParentElement, rootNode);
1284
1289
  }
1285
- const newParentElement = this.findParent(command);
1286
- const nextElement = this.findFollowingElement(command);
1287
- this.renderInsertOrAppend(rootNode, newParentElement, nextElement);
1290
+ this.renderInsertAfter(rootNode, { parentElement: this.findParent(command), previousSibling });
1288
1291
  cached.inputs.set(command.inputs);
1289
1292
  this.executedCommandsCache.set(command.id, {
1290
1293
  ...cached,
@@ -1298,11 +1301,11 @@ class ContentfulRichTextRendererComponent {
1298
1301
  if (command.kind === 'htmlClose') {
1299
1302
  return;
1300
1303
  }
1301
- throw new Error('Cached command not found!');
1304
+ throw richTextRendererError('cached_command_not_found', { command });
1302
1305
  }
1303
1306
  if (command.kind === 'component') {
1304
1307
  if (!isExecutedComponentCommandCacheItem(cached)) {
1305
- throw new Error('Cached command is not a component command!');
1308
+ throw richTextRendererError('cached_command_not_component', { command });
1306
1309
  }
1307
1310
  cached.componentRef.destroy();
1308
1311
  }
@@ -1317,57 +1320,17 @@ class ContentfulRichTextRendererComponent {
1317
1320
  return componentRef.hostView.rootNodes[0];
1318
1321
  }
1319
1322
  findParent(command) {
1320
- const hostElement = this.elementRef.nativeElement;
1321
- let parentElement;
1322
- if (command.nestingLevel === 0) {
1323
- parentElement = hostElement;
1324
- }
1325
- else {
1326
- const allCommands = this.renderCommands();
1327
- let parentCommand = null;
1328
- for (let i = command.index - 1; i >= 0; i--) {
1329
- const cmd = allCommands[i];
1330
- if (!cmd) {
1331
- throw new Error('Command not found!');
1332
- }
1333
- if (cmd.nestingLevel === command.nestingLevel - 1 && cmd.kind === 'htmlOpen') {
1334
- parentCommand = cmd;
1335
- break;
1336
- }
1337
- }
1338
- if (!parentCommand) {
1339
- throw richTextRendererError('text_parent_not_found', { command });
1340
- }
1341
- parentElement = this.executedCommandsCache.get(parentCommand.id)?.element;
1323
+ if (command.parentId === null) {
1324
+ return this.elementRef.nativeElement;
1342
1325
  }
1326
+ const parentElement = this.executedCommandsCache.get(command.parentId)?.element;
1343
1327
  if (!parentElement) {
1344
- throw richTextRendererError('text_parent_wrong_type', { command });
1328
+ throw richTextRendererError('text_parent_not_found', { command });
1345
1329
  }
1346
1330
  return parentElement;
1347
1331
  }
1348
- findFollowingElement(command) {
1349
- const parentElement = this.findParent(command);
1350
- let nextElement;
1351
- let nextDomPosition = Infinity;
1352
- for (const cached of this.executedCommandsCache.values()) {
1353
- if (cached.command.domPosition > command.domPosition &&
1354
- cached.command.domPosition < nextDomPosition &&
1355
- cached.command.nestingLevel === command.nestingLevel &&
1356
- cached.element.parentElement === parentElement &&
1357
- cached.command.id !== command.id) {
1358
- nextElement = cached.element;
1359
- nextDomPosition = cached.command.domPosition;
1360
- }
1361
- }
1362
- return nextElement;
1363
- }
1364
- renderInsertOrAppend(nodeToRender, parentElement, nextElement) {
1365
- if (nextElement) {
1366
- this.renderer.insertBefore(parentElement, nodeToRender, nextElement);
1367
- }
1368
- else {
1369
- this.renderer.appendChild(parentElement, nodeToRender);
1370
- }
1332
+ renderInsertAfter(node, { parentElement, previousSibling }) {
1333
+ this.renderer.insertBefore(parentElement, node, previousSibling ? previousSibling.nextSibling : parentElement.firstChild);
1371
1334
  }
1372
1335
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1373
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 }); }
@@ -1401,5 +1364,5 @@ const isContentfulEntryType = (entry, type) => entry.sys.contentType.sys.id ===
1401
1364
  * Generated bundle index. Do not edit.
1402
1365
  */
1403
1366
 
1404
- export { CONTENTFUL_CONFIG, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, injectContentfulConfig, isContentfulEntryType, isContentfulGqlAsset, isRichTextRootNode, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, translateContentfulNodeTypeToHtmlTag };
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 };
1405
1368
  //# sourceMappingURL=ethlete-contentful.mjs.map