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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,163 @@
1
+ import { formatFiles } from '@nx/devkit';
2
+ import { createMigrationScope } from '../migrate-to-contentful-v5/migration-scope.js';
3
+ export const CONTENTFUL_ASSET_CLASSES_REPORT_PATH = 'contentful-asset-classes-migration-tasks.md';
4
+ const REMOVED_CLASS_INPUTS = {
5
+ 'et-contentful-video': { videoClass: 'et-contentful-video-video' },
6
+ 'et-contentful-audio': {
7
+ audioClass: 'et-contentful-audio-audio',
8
+ figureClass: 'et-contentful-audio-figure',
9
+ figcaptionClass: 'et-contentful-audio-figcaption',
10
+ },
11
+ 'et-contentful-file': { fileClass: 'et-contentful-file-anchor' },
12
+ 'et-contentful-link': { textClass: 'et-contentful-link-anchor', anchorClass: 'et-contentful-link-anchor' },
13
+ };
14
+ const TAG_REGEX = new RegExp(`<(${Object.keys(REMOVED_CLASS_INPUTS).join('|')})(?=[\\s/>])`, 'g');
15
+ const INLINE_TEMPLATE_REGEX = /template\s*:\s*`([\s\S]*?)`/g;
16
+ const LINK_INPUT_DECLARATION_REGEX = /\b(textClass|anchorClass)\s*=\s*input\b/g;
17
+ const lineOf = (source, index) => source.slice(0, index).split('\n').length;
18
+ const findTagEnd = (source, from) => {
19
+ let quote = null;
20
+ for (let index = from; index < source.length; index++) {
21
+ const character = source[index];
22
+ if (quote) {
23
+ if (character === quote)
24
+ quote = null;
25
+ }
26
+ else if (character === '"' || character === "'") {
27
+ quote = character;
28
+ }
29
+ else if (character === '>') {
30
+ return index;
31
+ }
32
+ }
33
+ return -1;
34
+ };
35
+ const isInsideQuotes = (tag, index) => {
36
+ let quote = null;
37
+ for (let position = 0; position < index; position++) {
38
+ const character = tag[position];
39
+ if (quote) {
40
+ if (character === quote)
41
+ quote = null;
42
+ }
43
+ else if (character === '"' || character === "'") {
44
+ quote = character;
45
+ }
46
+ }
47
+ return quote !== null;
48
+ };
49
+ const removeClassInputs = (template) => {
50
+ const removals = [];
51
+ let content = '';
52
+ let cursor = 0;
53
+ for (const tagMatch of template.matchAll(TAG_REGEX)) {
54
+ const start = tagMatch.index ?? 0;
55
+ if (start < cursor)
56
+ continue;
57
+ const end = findTagEnd(template, start);
58
+ if (end === -1)
59
+ break;
60
+ const tagName = tagMatch[1] ?? '';
61
+ const inputs = Object.keys(REMOVED_CLASS_INPUTS[tagName] ?? {});
62
+ const attributeRegex = new RegExp(`(\\s+)(\\[?(${inputs.join('|')})\\]?(?:\\s*=\\s*(?:"[^"]*"|'[^']*'))?)(?=[\\s/>])`, 'g');
63
+ const tag = template.slice(start, end + 1);
64
+ const attributeMatches = [...tag.matchAll(attributeRegex)].filter((attributeMatch) => !isInsideQuotes(tag, attributeMatch.index ?? 0));
65
+ let nextTag = tag;
66
+ for (const attributeMatch of [...attributeMatches].reverse()) {
67
+ const index = attributeMatch.index ?? 0;
68
+ nextTag = nextTag.slice(0, index) + nextTag.slice(index + attributeMatch[0].length);
69
+ }
70
+ for (const attributeMatch of attributeMatches) {
71
+ removals.push({
72
+ offset: start + (attributeMatch.index ?? 0) + (attributeMatch[1] ?? '').length,
73
+ tagName,
74
+ input: attributeMatch[3] ?? '',
75
+ attribute: attributeMatch[2] ?? '',
76
+ });
77
+ }
78
+ content += template.slice(cursor, start) + nextTag;
79
+ cursor = end + 1;
80
+ }
81
+ content += template.slice(cursor);
82
+ return { content, removals };
83
+ };
84
+ const createRemovalTask = (filePath, line, removal) => {
85
+ const replacement = REMOVED_CLASS_INPUTS[removal.tagName]?.[removal.input] ?? '';
86
+ return {
87
+ id: `asset-class-input:${filePath}:${line}:${removal.input}`,
88
+ file: filePath,
89
+ line,
90
+ message: `Removed \`${removal.attribute}\` from \`${removal.tagName}\`. Style the static \`.${replacement}\` class instead.`,
91
+ };
92
+ };
93
+ const migrateHtmlFile = (filePath, source) => {
94
+ const { content, removals } = removeClassInputs(source);
95
+ return {
96
+ content,
97
+ tasks: removals.map((removal) => createRemovalTask(filePath, lineOf(source, removal.offset), removal)),
98
+ };
99
+ };
100
+ const migrateTsFile = (filePath, source) => {
101
+ const tasks = [];
102
+ const content = source.replace(INLINE_TEMPLATE_REGEX, (match, template, offset) => {
103
+ const templateOffset = offset + match.indexOf('`') + 1;
104
+ const result = removeClassInputs(template);
105
+ for (const removal of result.removals) {
106
+ tasks.push(createRemovalTask(filePath, lineOf(source, templateOffset + removal.offset), removal));
107
+ }
108
+ return result.removals.length > 0 ? match.replace(template, () => result.content) : match;
109
+ });
110
+ for (const declaration of source.matchAll(LINK_INPUT_DECLARATION_REGEX)) {
111
+ const line = lineOf(source, declaration.index ?? 0);
112
+ tasks.push({
113
+ id: `link-class-input:${filePath}:${line}`,
114
+ file: filePath,
115
+ line,
116
+ message: `\`${declaration[1]}\` is declared as an input. If this is a custom \`components.link\`, the rich-text renderer no longer sets it - it passes \`marks\` (the shared mark types) and \`richText: true\` instead.`,
117
+ });
118
+ }
119
+ return { content, tasks };
120
+ };
121
+ const renderReport = (tasks) => [
122
+ '# Contentful asset class inputs migration tasks',
123
+ '',
124
+ 'The class inputs of `et-contentful-video`, `et-contentful-audio`, `et-contentful-file` and',
125
+ '`et-contentful-link` are gone. Every inner element carries a static `et-contentful-*` class instead.',
126
+ 'The migration removed the bindings below; move their styling onto the named class. Delete this file when done.',
127
+ '',
128
+ ...tasks.map((task) => [`## \`${task.id}\``, '', `- ${task.file}:${task.line}`, `- ${task.message}`, ''].join('\n')),
129
+ ].join('\n');
130
+ export default async function migrateContentfulAssetClasses(tree, schema) {
131
+ console.log('\n🔄 Removing the Contentful asset class inputs...');
132
+ const scope = createMigrationScope(tree, schema);
133
+ console.log(` Scope: ${scope.describe()}`);
134
+ const tasks = [];
135
+ let filesChanged = 0;
136
+ scope.visit(tree, (filePath) => {
137
+ const isHtml = filePath.endsWith('.html');
138
+ const isTs = filePath.endsWith('.ts') && !filePath.endsWith('.d.ts');
139
+ if (!isHtml && !isTs)
140
+ return;
141
+ const before = tree.read(filePath, 'utf-8');
142
+ if (!before)
143
+ return;
144
+ const result = isHtml ? migrateHtmlFile(filePath, before) : migrateTsFile(filePath, before);
145
+ tasks.push(...result.tasks);
146
+ if (result.content !== before) {
147
+ tree.write(filePath, result.content);
148
+ filesChanged++;
149
+ console.log(` ✓ ${filePath}`);
150
+ }
151
+ });
152
+ if (tasks.length > 0) {
153
+ tree.write(CONTENTFUL_ASSET_CLASSES_REPORT_PATH, renderReport(tasks));
154
+ }
155
+ if (!schema.skipFormat) {
156
+ await formatFiles(tree);
157
+ }
158
+ console.log(`\n✅ Updated ${filesChanged} file(s).`);
159
+ if (tasks.length > 0) {
160
+ console.log(`⚠️ ${tasks.length} site(s) need their styling moved — see ${CONTENTFUL_ASSET_CLASSES_REPORT_PATH}.`);
161
+ }
162
+ }
163
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "migrate-contentful-asset-classes",
4
+ "title": "Remove the Contentful asset and link class inputs",
5
+ "type": "object",
6
+ "properties": {
7
+ "projects": {
8
+ "type": "array",
9
+ "items": { "type": "string" },
10
+ "description": "Nx projects to migrate. Defaults to every project in the workspace. Every site is rewritten independently, so any scope is safe — use this to keep the diff reviewable.",
11
+ "alias": "project"
12
+ },
13
+ "include": {
14
+ "type": "array",
15
+ "items": { "type": "string" },
16
+ "description": "Path prefixes to migrate, relative to the workspace root (e.g. 'libs/ui'). Unioned with `projects`. Defaults to the whole workspace.",
17
+ "alias": "includePath"
18
+ },
19
+ "skipFormat": {
20
+ "type": "boolean",
21
+ "description": "Skip formatting files after migration",
22
+ "default": false
23
+ }
24
+ },
25
+ "required": []
26
+ }
@@ -19,9 +19,23 @@ const namesOwnComponents = (content, braceIndex) => {
19
19
  }
20
20
  return /(^|[,\s])components\s*:/.test(topLevel);
21
21
  };
22
+ const contentfulImportNaming = (content, name) => [...content.matchAll(CONTENTFUL_IMPORT)].find((match) => name.test(match[1] ?? ''));
23
+ export const importsProvideContentfulConfig = (content) => contentfulImportNaming(content, /\bprovideContentfulConfig\b/) !== undefined;
24
+ export const findUnprovidedRendererTask = (filePath, content) => {
25
+ const match = contentfulImportNaming(content, /\b(ContentfulRichTextRendererComponent|ContentfulImports)\b/);
26
+ if (!match)
27
+ return null;
28
+ const line = lineOf(content, match.index ?? 0);
29
+ return {
30
+ id: taskId(filePath, line),
31
+ file: filePath,
32
+ line,
33
+ message: `Renders Contentful rich text, but no file calls \`provideContentfulConfig\`. Without it embedded assets are skipped and links lose router navigation. Add \`provideContentfulConfig({ ${SPREAD} })\` to the application providers.`,
34
+ };
35
+ };
22
36
  export const scanContentfulDefaultComponentsInFile = (filePath, content) => {
23
37
  const tasks = [];
24
- const importStatement = [...content.matchAll(CONTENTFUL_IMPORT)].find((match) => /\bprovideContentfulConfig\b/.test(match[1] ?? ''));
38
+ const importStatement = contentfulImportNaming(content, /\bprovideContentfulConfig\b/);
25
39
  if (!importStatement || content.includes(CONSTANT))
26
40
  return { next: null, tasks };
27
41
  const report = (index, message) => {
@@ -1,13 +1,23 @@
1
- import { formatFiles } from '@nx/devkit';
1
+ import { formatFiles, visitNotIgnoredFiles } from '@nx/devkit';
2
2
  import { createMigrationScope } from '../migrate-to-contentful-v5/migration-scope.js';
3
- import { scanContentfulDefaultComponentsInFile, } from './contentful-default-components.js';
3
+ import { findUnprovidedRendererTask, importsProvideContentfulConfig, scanContentfulDefaultComponentsInFile, } from './contentful-default-components.js';
4
4
  export const CONTENTFUL_DEFAULT_COMPONENTS_REPORT_PATH = 'contentful-default-components-migration-tasks.md';
5
+ const isSourceFile = (filePath) => filePath.endsWith('.ts') && !filePath.endsWith('.d.ts');
6
+ const workspaceProvidesContentfulConfig = (tree) => {
7
+ let found = false;
8
+ visitNotIgnoredFiles(tree, '', (filePath) => {
9
+ if (found || !isSourceFile(filePath))
10
+ return;
11
+ found = importsProvideContentfulConfig(tree.read(filePath, 'utf-8') ?? '');
12
+ });
13
+ return found;
14
+ };
5
15
  const renderReport = (tasks) => [
6
16
  '# Contentful default components migration tasks',
7
17
  '',
8
18
  'The shipped asset and link components are opt-in. Without them embedded assets are skipped and',
9
19
  'hyperlinks render as plain anchors. The migration spread `CONTENTFUL_DEFAULT_COMPONENTS` into every',
10
- 'literal `provideContentfulConfig` call; the calls below need it by hand:',
20
+ 'literal `provideContentfulConfig` call; the places below need it by hand:',
11
21
  '',
12
22
  '```ts',
13
23
  "import { CONTENTFUL_DEFAULT_COMPONENTS, provideContentfulConfig } from '@ethlete/contentful';",
@@ -25,19 +35,26 @@ export default async function migrateContentfulDefaultComponents(tree, schema) {
25
35
  console.log(` Scope: ${scope.describe()}`);
26
36
  const changed = [];
27
37
  const tasks = [];
38
+ const rendererTasks = [];
28
39
  scope.visit(tree, (filePath) => {
29
- if (!filePath.endsWith('.ts') || filePath.endsWith('.d.ts'))
40
+ if (!isSourceFile(filePath))
30
41
  return;
31
42
  const content = tree.read(filePath, 'utf-8');
32
43
  if (!content)
33
44
  return;
34
45
  const result = scanContentfulDefaultComponentsInFile(filePath, content);
46
+ const rendererTask = findUnprovidedRendererTask(filePath, content);
35
47
  tasks.push(...result.tasks);
48
+ if (rendererTask)
49
+ rendererTasks.push(rendererTask);
36
50
  if (result.next !== null) {
37
51
  tree.write(filePath, result.next);
38
52
  changed.push(filePath);
39
53
  }
40
54
  });
55
+ if (rendererTasks.length > 0 && !workspaceProvidesContentfulConfig(tree)) {
56
+ tasks.push(...rendererTasks);
57
+ }
41
58
  if (tasks.length > 0) {
42
59
  tree.write(CONTENTFUL_DEFAULT_COMPONENTS_REPORT_PATH, renderReport(tasks));
43
60
  }
@@ -48,7 +65,7 @@ export default async function migrateContentfulDefaultComponents(tree, schema) {
48
65
  console.log(`\n✅ Spread CONTENTFUL_DEFAULT_COMPONENTS in ${changed.length} file(s).`);
49
66
  }
50
67
  if (tasks.length > 0) {
51
- console.log(`\n⚠️ ${tasks.length} call(s) need the default components by hand — see ${CONTENTFUL_DEFAULT_COMPONENTS_REPORT_PATH}.`);
68
+ console.log(`\n⚠️ ${tasks.length} place(s) need the default components by hand — see ${CONTENTFUL_DEFAULT_COMPONENTS_REPORT_PATH}.`);
52
69
  }
53
70
  else if (changed.length === 0) {
54
71
  console.log('\n✅ Nothing to do.');
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/schema",
3
3
  "$id": "migrate-contentful-default-components",
4
- "title": "Add withContentfulDefaultComponents() to provideContentfulConfig",
4
+ "title": "Spread CONTENTFUL_DEFAULT_COMPONENTS into provideContentfulConfig",
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "projects": {
@@ -3,7 +3,7 @@ import { createMigrationScope } from './migration-scope.js';
3
3
  export const CONTENTFUL_V5_REPORT_PATH = 'contentful-v5-migration-tasks.md';
4
4
  const COMPONENTS_PACKAGE = '@ethlete/components';
5
5
  const COMPONENTS_VERSION = '^1.0.0-next.32';
6
- /** Inputs that v5 dropped — the picture element now only carries static `et-picture-*` classes. */
6
+ /** Inputs that 4.0 dropped — the picture element now only carries static `et-picture-*` classes. */
7
7
  const REMOVED_CLASS_INPUTS = ['imgClass', 'figureClass', 'figcaptionClass', 'pictureClass'];
8
8
  /** Renderer internals that are no longer exported from `@ethlete/contentful`. */
9
9
  const REMOVED_EXPORTS = [
@@ -188,7 +188,7 @@ const migratePackageJson = (tree, filePath) => {
188
188
  return { changed, tasks };
189
189
  };
190
190
  const renderReport = (tasks) => [
191
- '# @ethlete/contentful v5 migration tasks',
191
+ '# @ethlete/contentful 4 migration tasks',
192
192
  '',
193
193
  'The codemod renamed the `et-contentful-image` `hasPriority` input to `priority`, removed the dropped',
194
194
  '`useTailwindClasses` config option and made sure `@ethlete/components` is declared. The sites below',
@@ -205,7 +205,7 @@ const renderReport = (tasks) => [
205
205
  ...tasks.map((task) => [`## \`${task.id}\``, '', `- ${task.file}${task.line ? `:${task.line}` : ''}`, `- ${task.message}`, ''].join('\n')),
206
206
  ].join('\n');
207
207
  export default async function migrateToContentfulV5(tree, schema) {
208
- console.log('\n🔄 Migrating to @ethlete/contentful v5...');
208
+ console.log('\n🔄 Migrating to @ethlete/contentful 4...');
209
209
  const scope = createMigrationScope(tree, schema);
210
210
  console.log(` Scope: ${scope.describe()}`);
211
211
  const tasks = [];
package/migrations.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "name": "to-contentful-v5",
5
5
  "version": "4.0.0-next.7",
6
6
  "kind": "auto",
7
+ "level": "required",
7
8
  "description": "Move to the components picture, the renamed image inputs and the removed renderer internals.",
8
9
  "generator": "@ethlete/contentful:migrate-to-contentful-v5",
9
10
  "docs": "/contentful/"
@@ -12,9 +13,19 @@
12
13
  "name": "contentful-default-components",
13
14
  "version": "4.0.0-next.12",
14
15
  "kind": "auto",
15
- "description": "Add withContentfulDefaultComponents() to provideContentfulConfig, and report the calls that need it by hand.",
16
+ "level": "required",
17
+ "description": "Spread CONTENTFUL_DEFAULT_COMPONENTS into provideContentfulConfig, and report the calls that need it by hand.",
16
18
  "generator": "@ethlete/contentful:migrate-contentful-default-components",
17
19
  "docs": "/contentful/"
20
+ },
21
+ {
22
+ "name": "contentful-asset-classes",
23
+ "version": "4.0.0-next.13",
24
+ "kind": "auto",
25
+ "level": "required",
26
+ "description": "Remove the dropped video, audio, file and link class inputs, and report each removal with the static class that replaces it.",
27
+ "generator": "@ethlete/contentful:migrate-contentful-asset-classes",
28
+ "docs": "/contentful/"
18
29
  }
19
30
  ]
20
31
  }
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@ethlete/contentful",
3
- "version": "4.0.0-next.12",
3
+ "version": "4.0.0-next.14",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@angular/common": "22.1.6",
8
- "@angular/core": "22.1.6",
9
- "@angular/router": "22.1.6",
10
- "@contentful/rich-text-types": "17.2.7",
7
+ "@angular/common": "^22.1.0",
8
+ "@angular/core": "^22.1.0",
9
+ "@angular/router": "^22.1.0",
10
+ "@contentful/rich-text-types": "^17.0.0",
11
11
  "@ethlete/components": "^1.0.0-next.32",
12
12
  "@ethlete/core": "^5.0.0-beta.11",
13
13
  "@ethlete/query": "^6.0.0-beta.8",
14
- "@nx/devkit": "23.1.0"
14
+ "@nx/devkit": "^23.0.0"
15
15
  },
16
16
  "peerDependenciesMeta": {
17
17
  "@nx/devkit": {
@@ -1,9 +1,8 @@
1
1
  import * as _angular_core from '@angular/core';
2
2
  import { Type, InputSignal, InjectionToken, Provider } from '@angular/core';
3
- import * as _ethlete_core from '@ethlete/core';
4
- import { NgClassType } from '@ethlete/core';
5
3
  import { GQL } from '@ethlete/query';
6
4
  import { Mark, BLOCKS, INLINES, NodeData, Block } from '@contentful/rich-text-types';
5
+ import * as _ethlete_core from '@ethlete/core';
7
6
  import * as _ethlete_contentful from '@ethlete/contentful';
8
7
  import * as _ethlete_components from '@ethlete/components';
9
8
  import { PictureSource } from '@ethlete/components';
@@ -88,6 +87,43 @@ type ContentfulGqlCollectionFilterVariables<TCustomWhere extends Record<string,
88
87
  locale?: string;
89
88
  };
90
89
 
90
+ type ContentfulGqlRichTextEntry = {
91
+ __typename?: string | null;
92
+ sys: {
93
+ id: string;
94
+ };
95
+ [field: string]: unknown;
96
+ };
97
+ type ContentfulGqlLinkList<T> = (T | null | undefined)[] | null;
98
+ /**
99
+ * A rich-text field as returned by the Contentful GraphQL API. Select `json` and the `links` the
100
+ * document references, and pass the field to `[gqlRichText]` as-is.
101
+ */
102
+ type ContentfulGqlRichText = {
103
+ json: RichTextResponse;
104
+ links?: {
105
+ assets?: {
106
+ block?: ContentfulGqlLinkList<ContentfulGqlAsset>;
107
+ hyperlink?: ContentfulGqlLinkList<ContentfulGqlAsset>;
108
+ } | null;
109
+ entries?: {
110
+ block?: ContentfulGqlLinkList<ContentfulGqlRichTextEntry>;
111
+ inline?: ContentfulGqlLinkList<ContentfulGqlRichTextEntry>;
112
+ hyperlink?: ContentfulGqlLinkList<ContentfulGqlRichTextEntry>;
113
+ } | null;
114
+ } | null;
115
+ };
116
+ /**
117
+ * Maps the `links` of a GraphQL rich-text field to the entries and assets the renderer resolves
118
+ * embeds against. An entry's content type id is the id in `contentTypeIds` whose GraphQL type name
119
+ * (PascalCase, separators dropped: `product-teaser` → `ProductTeaser`) equals its `__typename`, else the
120
+ * `__typename` with the first letter lower-cased. Entries without a `__typename` are dropped.
121
+ */
122
+ declare const mapContentfulGqlLinks: (links: ContentfulGqlRichText["links"], contentTypeIds?: readonly string[]) => {
123
+ assets: ContentfulRestAsset[];
124
+ entries: ContentfulEntry[];
125
+ };
126
+
91
127
  type MarkTagName = 'strong' | 'em' | 'u' | 'code' | 's' | 'sub' | 'sup';
92
128
  /**
93
129
  * Translates contentful text marks into the semantic html elements the marked text is
@@ -149,6 +185,9 @@ declare class ContentfulRichTextRendererComponent {
149
185
  private elementRef;
150
186
  private config;
151
187
  private document;
188
+ private location;
189
+ private url;
190
+ private injector;
152
191
  /**
153
192
  * The contentful response gotten via their REST api.
154
193
  * @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
@@ -158,9 +197,25 @@ declare class ContentfulRichTextRendererComponent {
158
197
  * The path to where the rich text field is inside the contentful response. Dot and array notation is allowed.
159
198
  * @example "items[0].fields.html"
160
199
  */
161
- richTextPath: _angular_core.InputSignal<string>;
200
+ richTextPath: _angular_core.InputSignal<string | undefined>;
201
+ /**
202
+ * A rich-text field from the Contentful GraphQL API, passed as-is. An alternative to `content` and
203
+ * `richTextPath`; setting both is an error in dev mode.
204
+ * @example { json, links: { assets: { block, hyperlink }, entries: { block, inline, hyperlink } } }
205
+ */
206
+ gqlRichText: _angular_core.InputSignal<ContentfulGqlRichText | null | undefined>;
207
+ /**
208
+ * A bare rich-text document, e.g. a rich-text field of an embedded entry. Pair it with `includes` so its
209
+ * embeds resolve. An alternative to `content` / `richTextPath` and `gqlRichText`.
210
+ * @example <et-contentful-rich-text-renderer [richText]="fields().body" [includes]="includes()" />
211
+ */
212
+ richText: _angular_core.InputSignal<RichTextResponse | null | undefined>;
213
+ /** The linked entries and assets `richText` resolves its embeds against, e.g. a custom component's `includes`. */
214
+ includes: _angular_core.InputSignal<ContentfulIncludeMap | null | undefined>;
215
+ private ancestorEntryIds;
162
216
  private readonly executedCommandsCache;
163
217
  private contentIncludesMap;
218
+ private linkLookup;
164
219
  private richTextData;
165
220
  private renderCommands;
166
221
  private renderInstructions;
@@ -175,7 +230,7 @@ declare class ContentfulRichTextRendererComponent {
175
230
  private findParent;
176
231
  private renderInsertAfter;
177
232
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ContentfulRichTextRendererComponent, never>;
178
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulRichTextRendererComponent, "et-contentful-rich-text-renderer", never, { "content": { "alias": "content"; "required": true; "isSignal": true; }; "richTextPath": { "alias": "richTextPath"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
233
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulRichTextRendererComponent, "et-contentful-rich-text-renderer", never, { "content": { "alias": "content"; "required": false; "isSignal": true; }; "richTextPath": { "alias": "richTextPath"; "required": false; "isSignal": true; }; "gqlRichText": { "alias": "gqlRichText"; "required": false; "isSignal": true; }; "richText": { "alias": "richText"; "required": false; "isSignal": true; }; "includes": { "alias": "includes"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
179
234
  }
180
235
 
181
236
  declare const isRichTextRootNode: (node: unknown) => node is RichTextResponse;
@@ -194,8 +249,8 @@ type ComponentLikeWithAsset = Type<{
194
249
  type ComponentLikeWithLink = Type<{
195
250
  href: InputSignal<string>;
196
251
  text: InputSignal<string>;
197
- textClass: InputSignal<string>;
198
- anchorClass?: InputSignal<string>;
252
+ marks?: InputSignal<readonly string[]>;
253
+ richText?: InputSignal<boolean>;
199
254
  }>;
200
255
  type ComponentLikeWithContentfulRendererInputs = Type<{
201
256
  fields?: InputSignal<any>;
@@ -221,14 +276,21 @@ type ContentfulConfig = {
221
276
  */
222
277
  customComponents: Record<string, ComponentLikeWithContentfulRendererInputs>;
223
278
  /**
224
- * Additional hostnames that should be treated as internal links (in addition to the current page's host).
279
+ * Additional hosts that should be treated as internal links (in addition to the current page's host).
225
280
  * Useful when the app runs on localhost during development but Contentful content references the production domain.
226
- * A configured hostname also covers its subdomains, so adding "example.com" treats
227
- * "foo.example.com" as internal without matching unrelated public-suffix siblings.
281
+ * A host matches exactly, so a subdomain needs its own entry; `'*.example.com'` matches every subdomain of
282
+ * `example.com` (but not `example.com` itself).
228
283
  *
229
- * @example ['example.com']
284
+ * @example ['example.com', '*.example.com']
230
285
  */
231
286
  internalHosts: string[];
287
+ /**
288
+ * Turns the entry an `entry-hyperlink` points at into an href. Return `null` to render the link text without a link.
289
+ * Without it, entry hyperlinks render as text.
290
+ *
291
+ * @example (entry) => (entry.sys.contentType.sys.id === 'page' ? `/${entry.fields.slug}` : null)
292
+ */
293
+ entryHref: ((entry: ContentfulEntry) => string | null) | null;
232
294
  /**
233
295
  * Default options for the contentful image api
234
296
  */
@@ -250,12 +312,15 @@ type ContentfulConfig = {
250
312
  */
251
313
  sizes: string[];
252
314
  /**
253
- * Background color for the image in hex. Eg. `"000000"`
315
+ * Background color for the image in hex. Eg. `"000000"` (a leading `#` is stripped)
254
316
  */
255
317
  backgroundColor: string | null;
256
318
  };
257
319
  };
258
- type ContentfulConfigOptions = Partial<ContentfulConfig>;
320
+ type ContentfulConfigOptions = Partial<Omit<ContentfulConfig, 'imageOptions'>> & {
321
+ /** Merged one level deep into the defaults, so a single option can be overridden. */
322
+ imageOptions?: Partial<ContentfulConfig['imageOptions']>;
323
+ };
259
324
  type ContentfulLinkType = 'Space' | 'ContentType' | 'Environment' | 'Entry' | 'Asset' | 'Tag';
260
325
  type ContentfulLink<T extends ContentfulLinkType> = {
261
326
  type: 'Link';
@@ -349,15 +414,12 @@ type RichTextResponse = {
349
414
 
350
415
  declare class ContentfulAudioComponent {
351
416
  asset: _angular_core.InputSignal<ContentfulGqlAsset | ContentfulRestAsset | null | undefined>;
352
- audioClass: _angular_core.InputSignal<NgClassType>;
353
- figureClass: _angular_core.InputSignal<NgClassType>;
354
- figcaptionClass: _angular_core.InputSignal<NgClassType>;
355
417
  protected data: _angular_core.Signal<{
356
418
  url: string;
357
419
  title: string | null;
358
420
  } | null>;
359
421
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ContentfulAudioComponent, never>;
360
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulAudioComponent, "et-contentful-audio", never, { "asset": { "alias": "asset"; "required": true; "isSignal": true; }; "audioClass": { "alias": "audioClass"; "required": false; "isSignal": true; }; "figureClass": { "alias": "figureClass"; "required": false; "isSignal": true; }; "figcaptionClass": { "alias": "figcaptionClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
422
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulAudioComponent, "et-contentful-audio", never, { "asset": { "alias": "asset"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
361
423
  }
362
424
 
363
425
  /** Every string `et-contentful-file` renders itself. Override them app-wide with {@link provideContentfulFileLabels}. */
@@ -381,14 +443,13 @@ declare const CONTENTFUL_FILE_LABELS: _angular_core.InjectionToken<_ethlete_core
381
443
  declare class ContentfulFileComponent {
382
444
  protected labels: _angular_core.Signal<_ethlete_contentful.ContentfulFileLabels>;
383
445
  asset: _angular_core.InputSignal<ContentfulGqlAsset | ContentfulRestAsset | null | undefined>;
384
- fileClass: _angular_core.InputSignal<NgClassType>;
385
446
  protected data: _angular_core.Signal<{
386
447
  url: string;
387
448
  size: number | null;
388
449
  title: string | null;
389
450
  } | null>;
390
451
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ContentfulFileComponent, never>;
391
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulFileComponent, "et-contentful-file", never, { "asset": { "alias": "asset"; "required": true; "isSignal": true; }; "fileClass": { "alias": "fileClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
452
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulFileComponent, "et-contentful-file", never, { "asset": { "alias": "asset"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
392
453
  }
393
454
 
394
455
  declare class ContentfulImageComponent {
@@ -443,31 +504,32 @@ declare const generateContentfulImageSources: (data: ContentfulRestAsset | Conte
443
504
 
444
505
  declare class ContentfulLinkComponent {
445
506
  private document;
507
+ private location;
446
508
  private router;
447
509
  private config;
448
510
  private url;
449
511
  href: _angular_core.InputSignal<string>;
450
512
  text: _angular_core.InputSignal<string>;
451
- textClass: _angular_core.InputSignal<string>;
452
- anchorClass: _angular_core.InputSignal<string>;
513
+ marks: _angular_core.InputSignal<readonly string[]>;
514
+ richText: _angular_core.InputSignal<boolean>;
453
515
  protected usesRouterLink: _angular_core.Signal<boolean>;
516
+ protected anchorHref: _angular_core.Signal<string>;
454
517
  protected openInNewTab: _angular_core.Signal<boolean>;
455
518
  protected internalPath: _angular_core.Signal<string>;
456
519
  protected internalUrlTree: _angular_core.Signal<_angular_router.UrlTree>;
457
- protected linkClass: _angular_core.Signal<string | null>;
520
+ protected modifierClasses: _angular_core.Signal<string[]>;
458
521
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ContentfulLinkComponent, never>;
459
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulLinkComponent, "et-contentful-link", never, { "href": { "alias": "href"; "required": true; "isSignal": true; }; "text": { "alias": "text"; "required": true; "isSignal": true; }; "textClass": { "alias": "textClass"; "required": false; "isSignal": true; }; "anchorClass": { "alias": "anchorClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
522
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulLinkComponent, "et-contentful-link", never, { "href": { "alias": "href"; "required": true; "isSignal": true; }; "text": { "alias": "text"; "required": true; "isSignal": true; }; "marks": { "alias": "marks"; "required": false; "isSignal": true; }; "richText": { "alias": "richText"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
460
523
  }
461
524
 
462
525
  declare class ContentfulVideoComponent {
463
526
  asset: _angular_core.InputSignal<ContentfulGqlAsset | ContentfulRestAsset | null | undefined>;
464
- videoClass: _angular_core.InputSignal<NgClassType>;
465
527
  protected data: _angular_core.Signal<{
466
528
  url: string;
467
529
  contentType: string | null;
468
530
  } | null>;
469
531
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ContentfulVideoComponent, never>;
470
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulVideoComponent, "et-contentful-video", never, { "asset": { "alias": "asset"; "required": true; "isSignal": true; }; "videoClass": { "alias": "videoClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
532
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulVideoComponent, "et-contentful-video", never, { "asset": { "alias": "asset"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
471
533
  }
472
534
 
473
535
  declare const CONTENTFUL_CONFIG: InjectionToken<ContentfulConfig>;
@@ -502,5 +564,5 @@ declare const CONTENTFUL_DEFAULT_COMPONENTS: {
502
564
  declare const provideContentfulConfig: (contentfulConfig?: ContentfulConfigOptions | null) => Provider;
503
565
  declare const isContentfulEntryType: <T extends ContentfulEntry>(entry: ContentfulEntry, type: string) => entry is T;
504
566
 
505
- export { CONTENTFUL_CONFIG, CONTENTFUL_DEFAULT_COMPONENTS, CONTENTFUL_FILE_LABELS, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, DEFAULT_CONTENTFUL_FILE_LABELS, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, injectContentfulConfig, injectContentfulFileLabels, isContentfulEntryType, isContentfulGqlAsset, isRichTextRootNode, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, provideContentfulFileLabels, translateContentfulNodeTypeToHtmlTag };
506
- export type { ComponentLikeWithAsset, ComponentLikeWithContentfulRendererInputs, ComponentLikeWithLink, ContentfulAssetComponents, ContentfulAssetFileData, ContentfulAssetImageData, ContentfulAssetLink, ContentfulAssetLinkItem, ContentfulCollection, ContentfulConfig, ContentfulConfigOptions, ContentfulContentTypeLink, ContentfulEntry, ContentfulEntryLink, ContentfulEntryLinkItem, ContentfulEntrySys, ContentfulEnvironmentLink, ContentfulFileLabels, ContentfulGqlAsset, ContentfulGqlCollectionFilterVariables, ContentfulGqlOrder, ContentfulGqlWhereFilter, ContentfulImageFocusArea, ContentfulImageResizeBehavior, ContentfulImageSourcesOptions, ContentfulIncludeMap, ContentfulLink, ContentfulLinkType, ContentfulMetadata, ContentfulRestAsset, ContentfulSpaceLink, ContentfulSys, ContentfulTagLink, ContentfulTagLinkItem, CreateContentfulIncludeMapConfig, RichTextResponse };
567
+ 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 };
568
+ export type { ComponentLikeWithAsset, ComponentLikeWithContentfulRendererInputs, ComponentLikeWithLink, ContentfulAssetComponents, ContentfulAssetFileData, ContentfulAssetImageData, ContentfulAssetLink, ContentfulAssetLinkItem, ContentfulCollection, ContentfulConfig, ContentfulConfigOptions, ContentfulContentTypeLink, ContentfulEntry, ContentfulEntryLink, ContentfulEntryLinkItem, ContentfulEntrySys, ContentfulEnvironmentLink, ContentfulFileLabels, ContentfulGqlAsset, ContentfulGqlCollectionFilterVariables, ContentfulGqlOrder, ContentfulGqlRichText, ContentfulGqlRichTextEntry, ContentfulGqlWhereFilter, ContentfulImageFocusArea, ContentfulImageResizeBehavior, ContentfulImageSourcesOptions, ContentfulIncludeMap, ContentfulLink, ContentfulLinkType, ContentfulMetadata, ContentfulRestAsset, ContentfulSpaceLink, ContentfulSys, ContentfulTagLink, ContentfulTagLinkItem, CreateContentfulIncludeMapConfig, RichTextResponse };