@analogjs/content 2.8.0-beta.10 → 2.8.0-beta.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,807 +1,794 @@
1
- import * as i0 from '@angular/core';
2
- import { inject, HostListener, Directive, Injectable, TransferState, makeStateKey, InjectionToken, signal, ɵPendingTasksInternal as _PendingTasksInternal, Input, ViewEncapsulation, Component, NgZone, PLATFORM_ID, computed, input } from '@angular/core';
3
- import { DOCUMENT, Location, AsyncPipe, isPlatformBrowser } from '@angular/common';
4
- import { Router, ActivatedRoute } from '@angular/router';
5
- import { isObservable, firstValueFrom, of, Observable, from } from 'rxjs';
6
- import { switchMap, map, tap, catchError } from 'rxjs/operators';
7
- import fm from 'front-matter';
8
- import { gfmHeadingId, getHeadingList } from 'marked-gfm-heading-id';
9
- import { marked } from 'marked';
10
- import { mangle } from 'marked-mangle';
11
- import { DomSanitizer } from '@angular/platform-browser';
12
- import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
13
-
14
- class AnchorNavigationDirective {
15
- constructor() {
16
- this.document = inject(DOCUMENT);
17
- this.location = inject(Location);
18
- this.router = inject(Router);
19
- }
20
- handleNavigation(element) {
21
- if (element instanceof HTMLAnchorElement &&
22
- isInternalUrl(element, this.document) &&
23
- hasTargetSelf(element) &&
24
- !hasDownloadAttribute(element)) {
25
- const { pathname, search, hash } = element;
26
- // A `<base>` element makes the browser resolve fragment-only hrefs
27
- // (e.g. `href="#some-id"`) against the base URL rather than the
28
- // current document, so `pathname`/`search` come back as the base's
29
- // rather than the current page's. Fall back to the document's own
30
- // location in that case, or an in-page anchor link would navigate
31
- // to the base route and lose the fragment there.
32
- const isFragmentOnly = element.getAttribute('href')?.startsWith('#');
33
- const resolvedPathname = isFragmentOnly
34
- ? this.document.location.pathname
35
- : pathname;
36
- const resolvedSearch = isFragmentOnly
37
- ? this.document.location.search
38
- : search;
39
- const url = this.location.normalize(`${resolvedPathname}${resolvedSearch}${hash}`);
40
- this.router.navigateByUrl(url);
41
- return false;
42
- }
43
- return true;
44
- }
45
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AnchorNavigationDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
46
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.5", type: AnchorNavigationDirective, isStandalone: true, selector: "[analogAnchorNavigation]", host: { listeners: { "click": "handleNavigation($event.target)" } }, ngImport: i0 }); }
47
- }
48
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AnchorNavigationDirective, decorators: [{
49
- type: Directive,
50
- args: [{
51
- selector: '[analogAnchorNavigation]',
52
- standalone: true,
53
- }]
54
- }], propDecorators: { handleNavigation: [{
55
- type: HostListener,
56
- args: ['click', ['$event.target']]
57
- }] } });
1
+ import * as i0 from "@angular/core";
2
+ import { Component, Directive, HostListener, Injectable, InjectionToken, Input, NgZone, PLATFORM_ID, TransferState, ViewEncapsulation, computed, inject, input, makeStateKey, signal, ɵPendingTasksInternal } from "@angular/core";
3
+ import { AsyncPipe, DOCUMENT, Location, isPlatformBrowser } from "@angular/common";
4
+ import { ActivatedRoute, Router } from "@angular/router";
5
+ import { Observable, firstValueFrom, from, isObservable, of } from "rxjs";
6
+ import { catchError, finalize, map, switchMap, tap } from "rxjs/operators";
7
+ import fm from "front-matter";
8
+ import { getHeadingList, gfmHeadingId } from "marked-gfm-heading-id";
9
+ import { marked } from "marked";
10
+ import { mangle } from "marked-mangle";
11
+ import { DomSanitizer } from "@angular/platform-browser";
12
+ import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
13
+ var AnchorNavigationDirective = class AnchorNavigationDirective {
14
+ constructor() {
15
+ this.document = inject(DOCUMENT);
16
+ this.location = inject(Location);
17
+ this.router = inject(Router);
18
+ }
19
+ handleNavigation(element) {
20
+ if (element instanceof HTMLAnchorElement && isInternalUrl(element, this.document) && hasTargetSelf(element) && !hasDownloadAttribute(element)) {
21
+ const { pathname, search, hash } = element;
22
+ const isFragmentOnly = element.getAttribute("href")?.startsWith("#");
23
+ const resolvedPathname = isFragmentOnly ? this.document.location.pathname : pathname;
24
+ const resolvedSearch = isFragmentOnly ? this.document.location.search : search;
25
+ const url = this.location.normalize(`${resolvedPathname}${resolvedSearch}${hash}`);
26
+ this.router.navigateByUrl(url);
27
+ return false;
28
+ }
29
+ return true;
30
+ }
31
+ static {
32
+ this.ɵfac = i0.ɵɵngDeclareFactory({
33
+ minVersion: "12.0.0",
34
+ version: "22.2.0",
35
+ ngImport: i0,
36
+ type: AnchorNavigationDirective,
37
+ deps: [],
38
+ target: i0.ɵɵFactoryTarget.Directive
39
+ });
40
+ }
41
+ static {
42
+ this.ɵdir = i0.ɵɵngDeclareDirective({
43
+ minVersion: "14.0.0",
44
+ version: "22.2.0",
45
+ type: AnchorNavigationDirective,
46
+ isStandalone: true,
47
+ selector: "[analogAnchorNavigation]",
48
+ host: { listeners: { "click": "handleNavigation($event.target)" } },
49
+ ngImport: i0
50
+ });
51
+ }
52
+ };
53
+ i0.ɵɵngDeclareClassMetadata({
54
+ minVersion: "12.0.0",
55
+ version: "22.2.0",
56
+ ngImport: i0,
57
+ type: AnchorNavigationDirective,
58
+ decorators: [{
59
+ type: Directive,
60
+ args: [{
61
+ selector: "[analogAnchorNavigation]",
62
+ standalone: true
63
+ }]
64
+ }],
65
+ propDecorators: { handleNavigation: [{
66
+ type: HostListener,
67
+ args: ["click", ["$event.target"]]
68
+ }] }
69
+ });
58
70
  function hasDownloadAttribute(anchorElement) {
59
- return anchorElement.getAttribute('download') !== null;
71
+ return anchorElement.getAttribute("download") !== null;
60
72
  }
61
73
  function hasTargetSelf(anchorElement) {
62
- return !anchorElement.target || anchorElement.target === '_self';
74
+ return !anchorElement.target || anchorElement.target === "_self";
63
75
  }
64
76
  function isInternalUrl(anchorElement, document) {
65
- return (anchorElement.host === document.location.host &&
66
- anchorElement.protocol === document.location.protocol);
67
- }
68
-
69
- /// <reference types="vite/client" />
70
- class ContentRenderer {
71
- async render(content) {
72
- return { content, toc: [] };
73
- }
74
- // Backward-compatible API for consumers that read headings directly.
75
- getContentHeadings(_content) {
76
- return [];
77
- }
78
- // eslint-disable-next-line
79
- enhance() { }
80
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ContentRenderer, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
81
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ContentRenderer }); }
82
- }
83
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ContentRenderer, decorators: [{
84
- type: Injectable
85
- }] });
86
- class NoopContentRenderer {
87
- constructor() {
88
- this.transferState = inject(TransferState);
89
- this.contentId = 0;
90
- }
91
- /**
92
- * Generates a hash from the content string
93
- * to be used with the transfer state
94
- */
95
- generateHash(str) {
96
- let hash = 0;
97
- for (let i = 0, len = str.length; i < len; i++) {
98
- let chr = str.charCodeAt(i);
99
- hash = (hash << 5) - hash + chr;
100
- hash |= 0; // Convert to 32bit integer
101
- }
102
- return hash;
103
- }
104
- async render(content) {
105
- this.contentId = this.generateHash(content);
106
- const toc = this.getContentHeadings(content);
107
- const key = makeStateKey(`content-headings-${this.contentId}`);
108
- if (import.meta.env.SSR === true) {
109
- this.transferState.set(key, toc);
110
- return { content, toc };
111
- }
112
- return {
113
- content,
114
- toc: this.transferState.get(key, toc),
115
- };
116
- }
117
- enhance() { }
118
- getContentHeadings(content) {
119
- return this.extractHeadings(content);
120
- }
121
- extractHeadings(content) {
122
- const markdownHeadings = this.extractHeadingsFromMarkdown(content);
123
- if (markdownHeadings.length > 0) {
124
- return markdownHeadings;
125
- }
126
- const htmlHeadings = this.extractHeadingsFromHtml(content);
127
- return htmlHeadings;
128
- }
129
- extractHeadingsFromMarkdown(content) {
130
- const lines = content.split('\n');
131
- const toc = [];
132
- const slugCounts = new Map();
133
- for (const line of lines) {
134
- const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
135
- if (!match) {
136
- continue;
137
- }
138
- const level = match[1].length;
139
- const text = match[2].trim();
140
- if (!text) {
141
- continue;
142
- }
143
- const baseSlug = text
144
- .toLowerCase()
145
- .replace(/[^\w\s-]/g, '')
146
- .trim()
147
- .replace(/\s+/g, '-');
148
- const count = slugCounts.get(baseSlug) ?? 0;
149
- slugCounts.set(baseSlug, count + 1);
150
- const id = count === 0 ? baseSlug : `${baseSlug}-${count}`;
151
- toc.push({ id, level, text });
152
- }
153
- return toc;
154
- }
155
- extractHeadingsFromHtml(content) {
156
- const toc = [];
157
- const slugCounts = new Map();
158
- const headingRegex = /<h([1-6])([^>]*)>([\s\S]*?)<\/h\1>/gi;
159
- for (const match of content.matchAll(headingRegex)) {
160
- const level = Number(match[1]);
161
- const attrs = match[2] ?? '';
162
- const rawInner = match[3] ?? '';
163
- const text = rawInner.replace(/<[^>]+>/g, '').trim();
164
- if (!text) {
165
- continue;
166
- }
167
- const idMatch = /\sid=(['"])(.*?)\1/i.exec(attrs) ?? /\sid=([^\s>]+)/i.exec(attrs);
168
- let id = idMatch?.[2] ?? idMatch?.[1] ?? '';
169
- if (!id) {
170
- id = this.makeSlug(text, slugCounts);
171
- }
172
- toc.push({ id, level, text });
173
- }
174
- return toc;
175
- }
176
- makeSlug(text, slugCounts) {
177
- const baseSlug = text
178
- .toLowerCase()
179
- .replace(/[^\w\s-]/g, '')
180
- .trim()
181
- .replace(/\s+/g, '-');
182
- const count = slugCounts.get(baseSlug) ?? 0;
183
- slugCounts.set(baseSlug, count + 1);
184
- return count === 0 ? baseSlug : `${baseSlug}-${count}`;
185
- }
186
- }
187
-
188
- /**
189
- * Token for the active content locale.
190
- * Provided via `withLocale()` in `provideContent()`.
191
- *
192
- * When set, `injectContentFiles()` filters to content matching this locale,
193
- * and `injectContent()` resolves locale-prefixed content paths first.
194
- */
195
- const CONTENT_LOCALE = new InjectionToken('@analogjs/content Locale');
196
- /**
197
- * Injects the content locale, returning null if not configured.
198
- */
77
+ return anchorElement.host === document.location.host && anchorElement.protocol === document.location.protocol;
78
+ }
79
+ var ContentRenderer = class ContentRenderer {
80
+ async render(content) {
81
+ return {
82
+ content,
83
+ toc: []
84
+ };
85
+ }
86
+ getContentHeadings(_content) {
87
+ return [];
88
+ }
89
+ enhance() {}
90
+ static {
91
+ this.ɵfac = i0.ɵɵngDeclareFactory({
92
+ minVersion: "12.0.0",
93
+ version: "22.2.0",
94
+ ngImport: i0,
95
+ type: ContentRenderer,
96
+ deps: [],
97
+ target: i0.ɵɵFactoryTarget.Injectable
98
+ });
99
+ }
100
+ static {
101
+ this.ɵprov = i0.ɵɵngDeclareInjectable({
102
+ minVersion: "12.0.0",
103
+ version: "22.2.0",
104
+ ngImport: i0,
105
+ type: ContentRenderer
106
+ });
107
+ }
108
+ };
109
+ i0.ɵɵngDeclareClassMetadata({
110
+ minVersion: "12.0.0",
111
+ version: "22.2.0",
112
+ ngImport: i0,
113
+ type: ContentRenderer,
114
+ decorators: [{ type: Injectable }]
115
+ });
116
+ var NoopContentRenderer = class {
117
+ constructor() {
118
+ this.transferState = inject(TransferState);
119
+ this.contentId = 0;
120
+ }
121
+ generateHash(str) {
122
+ let hash = 0;
123
+ for (let i = 0, len = str.length; i < len; i++) {
124
+ let chr = str.charCodeAt(i);
125
+ hash = (hash << 5) - hash + chr;
126
+ hash |= 0;
127
+ }
128
+ return hash;
129
+ }
130
+ async render(content) {
131
+ this.contentId = this.generateHash(content);
132
+ const toc = this.getContentHeadings(content);
133
+ const key = makeStateKey(`content-headings-${this.contentId}`);
134
+ if (import.meta.env.SSR === true) {
135
+ this.transferState.set(key, toc);
136
+ return {
137
+ content,
138
+ toc
139
+ };
140
+ }
141
+ return {
142
+ content,
143
+ toc: this.transferState.get(key, toc)
144
+ };
145
+ }
146
+ enhance() {}
147
+ getContentHeadings(content) {
148
+ return this.extractHeadings(content);
149
+ }
150
+ extractHeadings(content) {
151
+ const markdownHeadings = this.extractHeadingsFromMarkdown(content);
152
+ if (markdownHeadings.length > 0) return markdownHeadings;
153
+ return this.extractHeadingsFromHtml(content);
154
+ }
155
+ extractHeadingsFromMarkdown(content) {
156
+ const lines = content.split("\n");
157
+ const toc = [];
158
+ const slugCounts = /* @__PURE__ */ new Map();
159
+ for (const line of lines) {
160
+ const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
161
+ if (!match) continue;
162
+ const level = match[1].length;
163
+ const text = match[2].trim();
164
+ if (!text) continue;
165
+ const baseSlug = text.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
166
+ const count = slugCounts.get(baseSlug) ?? 0;
167
+ slugCounts.set(baseSlug, count + 1);
168
+ const id = count === 0 ? baseSlug : `${baseSlug}-${count}`;
169
+ toc.push({
170
+ id,
171
+ level,
172
+ text
173
+ });
174
+ }
175
+ return toc;
176
+ }
177
+ extractHeadingsFromHtml(content) {
178
+ const toc = [];
179
+ const slugCounts = /* @__PURE__ */ new Map();
180
+ for (const match of content.matchAll(/<h([1-6])([^>]*)>([\s\S]*?)<\/h\1>/gi)) {
181
+ const level = Number(match[1]);
182
+ const attrs = match[2] ?? "";
183
+ const text = (match[3] ?? "").replace(/<[^>]+>/g, "").trim();
184
+ if (!text) continue;
185
+ const idMatch = /\sid=(['"])(.*?)\1/i.exec(attrs) ?? /\sid=([^\s>]+)/i.exec(attrs);
186
+ let id = idMatch?.[2] ?? idMatch?.[1] ?? "";
187
+ if (!id) id = this.makeSlug(text, slugCounts);
188
+ toc.push({
189
+ id,
190
+ level,
191
+ text
192
+ });
193
+ }
194
+ return toc;
195
+ }
196
+ makeSlug(text, slugCounts) {
197
+ const baseSlug = text.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
198
+ const count = slugCounts.get(baseSlug) ?? 0;
199
+ slugCounts.set(baseSlug, count + 1);
200
+ return count === 0 ? baseSlug : `${baseSlug}-${count}`;
201
+ }
202
+ };
203
+ const CONTENT_LOCALE = new InjectionToken("@analogjs/content Locale");
199
204
  function injectContentLocale() {
200
- return inject(CONTENT_LOCALE, { optional: true });
205
+ return inject(CONTENT_LOCALE, { optional: true });
201
206
  }
202
- /**
203
- * Content feature that sets the active locale for content resolution.
204
- *
205
- * When provided, content APIs become locale-aware:
206
- * - `injectContentFiles()` filters to files in the locale subdirectory
207
- * or with a matching `locale` frontmatter attribute.
208
- * - `injectContent()` tries locale-prefixed paths first
209
- * (e.g., `content/fr/blog/post.md` before `content/blog/post.md`).
210
- *
211
- * Usage:
212
- * ```typescript
213
- * // With loader — runs in injection context
214
- * provideContent(
215
- * withMarkdownRenderer(),
216
- * withLocale({ loadLocale: injectLocale }),
217
- * )
218
- *
219
- * // Static locale
220
- * provideContent(
221
- * withMarkdownRenderer(),
222
- * withLocale('fr'),
223
- * )
224
- * ```
225
- */
226
207
  function withLocale(locale) {
227
- if (typeof locale === 'string') {
228
- return { provide: CONTENT_LOCALE, useValue: locale };
229
- }
230
- return { provide: CONTENT_LOCALE, useFactory: locale.loadLocale };
208
+ if (typeof locale === "string") return {
209
+ provide: CONTENT_LOCALE,
210
+ useValue: locale
211
+ };
212
+ return {
213
+ provide: CONTENT_LOCALE,
214
+ useFactory: locale.loadLocale
215
+ };
231
216
  }
232
- /**
233
- * Filters content files by locale using map-based key lookup.
234
- *
235
- * Matching rules:
236
- * 1. Frontmatter `locale` attribute matches the active locale.
237
- * 2. File is in the active locale subdirectory (e.g., `/content/fr/blog/post`).
238
- * 3. File has no locale marker and no localized variant exists — included as universal content.
239
- *
240
- * Files in a different locale's subdirectory are always excluded.
241
- */
242
217
  function filterByLocale(files, locale) {
243
- const localePrefix = `/content/${locale}/`;
244
- // Collect all locale prefixes present in the file set
245
- const allLocalePrefixes = new Set();
246
- for (const file of files) {
247
- const match = file.filename.match(/\/content\/([a-z]{2}(?:-[a-zA-Z]+)?)\//);
248
- if (match) {
249
- allLocalePrefixes.add(`/content/${match[1]}/`);
250
- }
251
- }
252
- // Build set of base paths that have a localized variant for the active locale
253
- const localizedBasePaths = new Set();
254
- for (const file of files) {
255
- if (file.filename.includes(localePrefix)) {
256
- localizedBasePaths.add(file.filename.replace(localePrefix, '/content/'));
257
- }
258
- }
259
- return files.filter((file) => {
260
- // Frontmatter locale attribute takes priority
261
- if (file.attributes['locale']) {
262
- return file.attributes['locale'] === locale;
263
- }
264
- // File is in the active locale subdirectory — include
265
- if (file.filename.includes(localePrefix)) {
266
- return true;
267
- }
268
- // File is in a different locale's subdirectory — exclude
269
- for (const prefix of allLocalePrefixes) {
270
- if (prefix !== localePrefix && file.filename.includes(prefix)) {
271
- return false;
272
- }
273
- }
274
- // Universal content — include only if no localized variant exists
275
- return !localizedBasePaths.has(file.filename);
276
- });
218
+ const localePrefix = `/content/${locale}/`;
219
+ const allLocalePrefixes = /* @__PURE__ */ new Set();
220
+ for (const file of files) {
221
+ const match = file.filename.match(/\/content\/([a-z]{2}(?:-[a-zA-Z]+)?)\//);
222
+ if (match) allLocalePrefixes.add(`/content/${match[1]}/`);
223
+ }
224
+ const localizedBasePaths = /* @__PURE__ */ new Set();
225
+ for (const file of files) if (file.filename.includes(localePrefix)) localizedBasePaths.add(file.filename.replace(localePrefix, "/content/"));
226
+ return files.filter((file) => {
227
+ if (file.attributes["locale"]) return file.attributes["locale"] === locale;
228
+ if (file.filename.includes(localePrefix)) return true;
229
+ for (const prefix of allLocalePrefixes) if (prefix !== localePrefix && file.filename.includes(prefix)) return false;
230
+ return !localizedBasePaths.has(file.filename);
231
+ });
277
232
  }
278
- /**
279
- * Prepends locale-prefixed candidates before the standard candidates
280
- * for content file map key lookup.
281
- */
282
233
  function withLocaleCandidates(candidates, locale) {
283
- if (!locale) {
284
- return candidates;
285
- }
286
- const localeCandidates = candidates.map((c) => c.replace('/src/content/', `/src/content/${locale}/`));
287
- return [...localeCandidates, ...candidates];
234
+ if (!locale) return candidates;
235
+ return [...candidates.map((c) => c.replace("/src/content/", `/src/content/${locale}/`)), ...candidates];
288
236
  }
289
-
290
- /**
291
- * Returns the list of content files by filename with ?analog-content-list=true.
292
- * We use the query param to transform the return into an array of
293
- * just front matter attributes.
294
- *
295
- * @returns
296
- */
237
+ let ANALOG_CONTENT_FILE_LIST = {};
238
+ let ANALOG_CONTENT_ROUTE_FILES = {};
297
239
  const getContentFilesList = () => {
298
- let ANALOG_CONTENT_FILE_LIST = {};
299
- return ANALOG_CONTENT_FILE_LIST;
240
+ return ANALOG_CONTENT_FILE_LIST;
300
241
  };
301
- /**
302
- * Returns the lazy loaded content files for lookups.
303
- *
304
- * @returns
305
- */
306
242
  const getContentFiles = () => {
307
- let ANALOG_CONTENT_ROUTE_FILES = {};
308
- return ANALOG_CONTENT_ROUTE_FILES;
243
+ return ANALOG_CONTENT_ROUTE_FILES;
309
244
  };
310
-
311
245
  function getSlug(filename) {
312
- // Extract the last path segment without its extension.
313
- // Handles names with dots like [[...slug]].md by stripping only the final extension.
314
- const lastSegment = (filename.split(/[/\\]/).pop() || '').trim();
315
- const base = lastSegment.replace(/\.[^./\\]+$/, ''); // strip only the final extension
316
- // Treat index.md as index route => empty slug
317
- return base === 'index' ? '' : base;
318
- }
319
- const CONTENT_FILES_LIST_TOKEN = new InjectionToken('@analogjs/content Content Files List', {
320
- providedIn: 'root',
321
- factory() {
322
- const contentFiles = getContentFilesList();
323
- return Object.keys(contentFiles).map((filename) => {
324
- const attributes = contentFiles[filename];
325
- const slug = attributes['slug'];
326
- return {
327
- filename,
328
- attributes,
329
- slug: slug ? encodeURI(slug) : encodeURI(getSlug(filename)),
330
- };
331
- });
332
- },
246
+ const base = (filename.split(/[/\\]/).pop() || "").trim().replace(/\.[^./\\]+$/, "");
247
+ return base === "index" ? "" : base;
248
+ }
249
+ const CONTENT_FILES_LIST_TOKEN = new InjectionToken("@analogjs/content Content Files List", {
250
+ providedIn: "root",
251
+ factory() {
252
+ const contentFiles = getContentFilesList();
253
+ return Object.keys(contentFiles).map((filename) => {
254
+ const attributes = contentFiles[filename];
255
+ const slug = attributes["slug"];
256
+ return {
257
+ filename,
258
+ attributes,
259
+ slug: slug ? encodeURI(slug) : encodeURI(getSlug(filename))
260
+ };
261
+ });
262
+ }
333
263
  });
334
-
335
- const CONTENT_FILES_TOKEN = new InjectionToken('@analogjs/content Content Files', {
336
- providedIn: 'root',
337
- factory() {
338
- const contentFiles = getContentFiles();
339
- const allFiles = { ...contentFiles };
340
- const contentFilesList = inject(CONTENT_FILES_LIST_TOKEN);
341
- const lookup = {};
342
- contentFilesList.forEach((item) => {
343
- const contentFilename = item.filename.replace(/(.*?)\/content/, '/src/content');
344
- const fileParts = contentFilename.split('/');
345
- const filePath = fileParts.slice(0, fileParts.length - 1).join('/');
346
- const fileNameParts = fileParts[fileParts.length - 1].split('.');
347
- const ext = fileNameParts[fileNameParts.length - 1];
348
- let slug = (item.slug ?? '');
349
- // Default empty slug to 'index'
350
- if (slug === '') {
351
- slug = 'index';
352
- }
353
- // Slugs with path separators are scoped to the file's top-level
354
- // subdirectory (e.g. `/src/content/docs`) rather than absolute root,
355
- // so nested files keep resolving through `subdirectory` lookups.
356
- // Files directly under `/src/content` keep the original root-relative
357
- // behavior since they have no subdirectory of their own.
358
- const subdirRoot = fileParts.length > 4 ? fileParts.slice(0, 4).join('/') : '/src/content';
359
- const newBase = slug.includes('/')
360
- ? `${subdirRoot}/${slug}`
361
- : `${filePath}/${slug}`;
362
- lookup[contentFilename] = `${newBase}.${ext}`.replace(/\/{2,}/g, '/');
363
- });
364
- const objectUsingSlugAttribute = {};
365
- Object.entries(allFiles).forEach((entry) => {
366
- const filename = entry[0];
367
- const value = entry[1];
368
- const strippedFilename = filename.replace(/^\/(.*?)\/content/, '/src/content');
369
- const newFilename = lookup[strippedFilename];
370
- if (newFilename !== undefined) {
371
- const objectFilename = newFilename.replace(/^\/(.*?)\/content/, '/src/content');
372
- objectUsingSlugAttribute[objectFilename] =
373
- value;
374
- }
375
- });
376
- return objectUsingSlugAttribute;
377
- },
264
+ const CONTENT_FILES_TOKEN = new InjectionToken("@analogjs/content Content Files", {
265
+ providedIn: "root",
266
+ factory() {
267
+ const allFiles = { ...getContentFiles() };
268
+ const contentFilesList = inject(CONTENT_FILES_LIST_TOKEN);
269
+ const lookup = {};
270
+ contentFilesList.forEach((item) => {
271
+ const contentFilename = item.filename.replace(/(.*?)\/content/, "/src/content");
272
+ const fileParts = contentFilename.split("/");
273
+ const filePath = fileParts.slice(0, fileParts.length - 1).join("/");
274
+ const fileNameParts = fileParts[fileParts.length - 1].split(".");
275
+ const ext = fileNameParts[fileNameParts.length - 1];
276
+ let slug = item.slug ?? "";
277
+ if (slug === "") slug = "index";
278
+ const subdirRoot = fileParts.length > 4 ? fileParts.slice(0, 4).join("/") : "/src/content";
279
+ const newBase = slug.includes("/") ? `${subdirRoot}/${slug}` : `${filePath}/${slug}`;
280
+ lookup[contentFilename] = `${newBase}.${ext}`.replace(/\/{2,}/g, "/");
281
+ });
282
+ const objectUsingSlugAttribute = {};
283
+ Object.entries(allFiles).forEach((entry) => {
284
+ const filename = entry[0];
285
+ const value = entry[1];
286
+ const strippedFilename = filename.replace(/^\/(.*?)\/content/, "/src/content");
287
+ const newFilename = lookup[strippedFilename];
288
+ if (newFilename !== void 0) {
289
+ const objectFilename = newFilename.replace(/^\/(.*?)\/content/, "/src/content");
290
+ objectUsingSlugAttribute[objectFilename] = value;
291
+ }
292
+ });
293
+ return objectUsingSlugAttribute;
294
+ }
378
295
  });
379
- const CONTENT_FILES_MAP_TOKEN = new InjectionToken('@analogjs/content Content Files', {
380
- providedIn: 'root',
381
- factory() {
382
- return signal(inject(CONTENT_FILES_TOKEN));
383
- },
296
+ new InjectionToken("@analogjs/content Content Files", {
297
+ providedIn: "root",
298
+ factory() {
299
+ return signal(inject(CONTENT_FILES_TOKEN));
300
+ }
384
301
  });
385
-
386
302
  function parseRawContentFile(rawContentFile) {
387
- const { body, attributes } = fm(rawContentFile);
388
- return { content: body, attributes };
303
+ const { body, attributes } = fm(rawContentFile);
304
+ return {
305
+ content: body,
306
+ attributes
307
+ };
389
308
  }
390
-
391
309
  async function waitFor(prom) {
392
- if (isObservable(prom)) {
393
- prom = firstValueFrom(prom);
394
- }
395
- if (typeof Zone === 'undefined') {
396
- return prom;
397
- }
398
- const macroTask = Zone.current.scheduleMacroTask(`AnalogContentResolve-${Math.random()}`, () => { }, {}, () => { });
399
- return prom.then((p) => {
400
- macroTask.invoke();
401
- return p;
402
- });
403
- }
404
-
405
- class RenderTaskService {
406
- #pendingTasks = inject(_PendingTasksInternal);
407
- addRenderTask() {
408
- return this.#pendingTasks.add();
409
- }
410
- clearRenderTask(clear) {
411
- if (typeof clear === 'function') {
412
- clear();
413
- }
414
- else if (typeof this.#pendingTasks.remove === 'function') {
415
- this.#pendingTasks.remove(clear);
416
- }
417
- }
418
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: RenderTaskService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
419
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: RenderTaskService }); }
420
- }
421
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: RenderTaskService, decorators: [{
422
- type: Injectable
423
- }] });
424
-
425
- /// <reference types="vite/client" />
310
+ if (isObservable(prom)) prom = firstValueFrom(prom);
311
+ if (typeof Zone === "undefined") return prom;
312
+ const macroTask = Zone.current.scheduleMacroTask(`AnalogContentResolve-${Math.random()}`, () => {}, {}, () => {});
313
+ return prom.then((p) => {
314
+ macroTask.invoke();
315
+ return p;
316
+ });
317
+ }
318
+ var RenderTaskService = class RenderTaskService {
319
+ #pendingTasks = inject(ɵPendingTasksInternal);
320
+ addRenderTask() {
321
+ return this.#pendingTasks.add();
322
+ }
323
+ clearRenderTask(clear) {
324
+ if (typeof clear === "function") clear();
325
+ else if (typeof this.#pendingTasks.remove === "function") this.#pendingTasks.remove(clear);
326
+ }
327
+ static {
328
+ this.ɵfac = i0.ɵɵngDeclareFactory({
329
+ minVersion: "12.0.0",
330
+ version: "22.2.0",
331
+ ngImport: i0,
332
+ type: RenderTaskService,
333
+ deps: [],
334
+ target: i0.ɵɵFactoryTarget.Injectable
335
+ });
336
+ }
337
+ static {
338
+ this.ɵprov = i0.ɵɵngDeclareInjectable({
339
+ minVersion: "12.0.0",
340
+ version: "22.2.0",
341
+ ngImport: i0,
342
+ type: RenderTaskService
343
+ });
344
+ }
345
+ };
346
+ i0.ɵɵngDeclareClassMetadata({
347
+ minVersion: "12.0.0",
348
+ version: "22.2.0",
349
+ ngImport: i0,
350
+ type: RenderTaskService,
351
+ decorators: [{ type: Injectable }]
352
+ });
426
353
  function getContentFile(contentFiles, prefix, slug, fallback, renderTaskService, contentRenderer, locale) {
427
- // Normalize file keys so both "/src/content/..." and "/<project>/src/content/..." resolve.
428
- const normalizedFiles = {};
429
- for (const [key, resolver] of Object.entries(contentFiles)) {
430
- const normalizedKey = key
431
- .replace(/^(?:.*?)\/content(?=\/)/, '/src/content')
432
- .replace(/\/{2,}/g, '/');
433
- normalizedFiles[normalizedKey] = resolver;
434
- }
435
- const base = `/src/content/${prefix}${slug}`.replace(/\/{2,}/g, '/');
436
- const candidates = [
437
- `${base}.md`,
438
- `${base}.agx`,
439
- `${base}/index.md`,
440
- `${base}/index.agx`,
441
- ];
442
- const allCandidates = withLocaleCandidates(candidates, locale);
443
- const matchKey = allCandidates.find((k) => k in normalizedFiles);
444
- const contentFile = matchKey ? normalizedFiles[matchKey] : undefined;
445
- const resolvedBase = (matchKey || `${base}.md`).replace(/\.(md|agx)$/, '');
446
- if (!contentFile) {
447
- return of({
448
- filename: resolvedBase,
449
- attributes: {},
450
- slug: '',
451
- content: fallback,
452
- toc: [],
453
- });
454
- }
455
- const contentTask = renderTaskService.addRenderTask();
456
- return new Observable((observer) => {
457
- const contentResolver = contentFile();
458
- if (import.meta.env.SSR === true) {
459
- waitFor(contentResolver).then((content) => {
460
- observer.next(content);
461
- observer.complete();
462
- setTimeout(() => renderTaskService.clearRenderTask(contentTask), 10);
463
- });
464
- }
465
- else {
466
- contentResolver.then((content) => {
467
- observer.next(content);
468
- observer.complete();
469
- });
470
- }
471
- }).pipe(switchMap((contentFile) => {
472
- if (typeof contentFile === 'string') {
473
- const { content, attributes } = parseRawContentFile(contentFile);
474
- return from(contentRenderer.render(content)).pipe(map((rendered) => ({
475
- filename: resolvedBase,
476
- slug,
477
- attributes,
478
- content,
479
- toc: rendered.toc ?? [],
480
- })));
481
- }
482
- return of({
483
- filename: resolvedBase,
484
- slug,
485
- attributes: contentFile.metadata,
486
- content: contentFile.default,
487
- toc: [],
488
- });
489
- }));
354
+ const normalizedFiles = {};
355
+ for (const [key, resolver] of Object.entries(contentFiles)) {
356
+ const normalizedKey = key.replace(/^(?:.*?)\/content(?=\/)/, "/src/content").replace(/\/{2,}/g, "/");
357
+ normalizedFiles[normalizedKey] = resolver;
358
+ }
359
+ const base = `/src/content/${prefix}${slug}`.replace(/\/{2,}/g, "/");
360
+ const matchKey = withLocaleCandidates([
361
+ `${base}.md`,
362
+ `${base}.agx`,
363
+ `${base}/index.md`,
364
+ `${base}/index.agx`
365
+ ], locale).find((k) => k in normalizedFiles);
366
+ const contentFile = matchKey ? normalizedFiles[matchKey] : void 0;
367
+ const resolvedBase = (matchKey || `${base}.md`).replace(/\.(md|agx)$/, "");
368
+ if (!contentFile) return of({
369
+ filename: resolvedBase,
370
+ attributes: {},
371
+ slug: "",
372
+ content: fallback,
373
+ toc: []
374
+ });
375
+ const contentTask = renderTaskService.addRenderTask();
376
+ return new Observable((observer) => {
377
+ const contentResolver = contentFile();
378
+ if (import.meta.env.SSR === true) waitFor(contentResolver).then((content) => {
379
+ observer.next(content);
380
+ observer.complete();
381
+ setTimeout(() => renderTaskService.clearRenderTask(contentTask), 10);
382
+ });
383
+ else contentResolver.then((content) => {
384
+ observer.next(content);
385
+ observer.complete();
386
+ });
387
+ }).pipe(switchMap((contentFile) => {
388
+ if (typeof contentFile === "string") {
389
+ const { content, attributes } = parseRawContentFile(contentFile);
390
+ return from(contentRenderer.render(content)).pipe(map((rendered) => ({
391
+ filename: resolvedBase,
392
+ slug,
393
+ attributes,
394
+ content,
395
+ toc: rendered.toc ?? []
396
+ })));
397
+ }
398
+ return of({
399
+ filename: resolvedBase,
400
+ slug,
401
+ attributes: contentFile.metadata,
402
+ content: contentFile.default,
403
+ toc: []
404
+ });
405
+ }), finalize(() => {
406
+ if (import.meta.env.SSR !== true) renderTaskService.clearRenderTask(contentTask);
407
+ }));
408
+ }
409
+ function injectContent(param = "slug", fallback = "No Content Found") {
410
+ const contentFiles = inject(CONTENT_FILES_TOKEN);
411
+ const contentRenderer = inject(ContentRenderer);
412
+ const renderTaskService = inject(RenderTaskService);
413
+ const locale = inject(CONTENT_LOCALE, { optional: true });
414
+ const task = renderTaskService.addRenderTask();
415
+ if (typeof param === "string" || "param" in param) {
416
+ const prefix = typeof param === "string" ? "" : `${param.subdirectory}/`;
417
+ const route = inject(ActivatedRoute);
418
+ const paramKey = typeof param === "string" ? param : param.param;
419
+ return route.paramMap.pipe(map((params) => params.get(paramKey)), switchMap((slug) => {
420
+ if (slug) return getContentFile(contentFiles, prefix, slug, fallback, renderTaskService, contentRenderer, locale);
421
+ return of({
422
+ filename: "",
423
+ slug: "",
424
+ attributes: {},
425
+ content: fallback,
426
+ toc: []
427
+ });
428
+ }), tap(() => renderTaskService.clearRenderTask(task)));
429
+ } else return getContentFile(contentFiles, "", param.customFilename, fallback, renderTaskService, contentRenderer, locale).pipe(tap(() => renderTaskService.clearRenderTask(task)));
490
430
  }
491
- /**
492
- * Retrieves the static content using the provided param and/or prefix.
493
- *
494
- * @param param route parameter (default: 'slug')
495
- * @param fallback fallback text if content file is not found (default: 'No Content Found')
496
- */
497
- function injectContent(param = 'slug', fallback = 'No Content Found') {
498
- const contentFiles = inject(CONTENT_FILES_TOKEN);
499
- const contentRenderer = inject(ContentRenderer);
500
- const renderTaskService = inject(RenderTaskService);
501
- const locale = inject(CONTENT_LOCALE, { optional: true });
502
- const task = renderTaskService.addRenderTask();
503
- if (typeof param === 'string' || 'param' in param) {
504
- const prefix = typeof param === 'string' ? '' : `${param.subdirectory}/`;
505
- const route = inject(ActivatedRoute);
506
- const paramKey = typeof param === 'string' ? param : param.param;
507
- return route.paramMap.pipe(map((params) => params.get(paramKey)), switchMap((slug) => {
508
- if (slug) {
509
- return getContentFile(contentFiles, prefix, slug, fallback, renderTaskService, contentRenderer, locale);
510
- }
511
- return of({
512
- filename: '',
513
- slug: '',
514
- attributes: {},
515
- content: fallback,
516
- toc: [],
517
- });
518
- }), tap(() => renderTaskService.clearRenderTask(task)));
519
- }
520
- else {
521
- return getContentFile(contentFiles, '', param.customFilename, fallback, renderTaskService, contentRenderer, locale).pipe(tap(() => renderTaskService.clearRenderTask(task)));
522
- }
523
- }
524
-
525
431
  function injectContentFiles(filterFn) {
526
- const renderTaskService = inject(RenderTaskService);
527
- const task = renderTaskService.addRenderTask();
528
- const allContentFiles = inject(CONTENT_FILES_LIST_TOKEN);
529
- const locale = inject(CONTENT_LOCALE, { optional: true });
530
- renderTaskService.clearRenderTask(task);
531
- let results = allContentFiles;
532
- if (locale) {
533
- results = filterByLocale(results, locale);
534
- }
535
- if (filterFn) {
536
- results = results.filter(filterFn);
537
- }
538
- return results;
432
+ const renderTaskService = inject(RenderTaskService);
433
+ const task = renderTaskService.addRenderTask();
434
+ const allContentFiles = inject(CONTENT_FILES_LIST_TOKEN);
435
+ const locale = inject(CONTENT_LOCALE, { optional: true });
436
+ renderTaskService.clearRenderTask(task);
437
+ let results = allContentFiles;
438
+ if (locale) results = filterByLocale(results, locale);
439
+ if (filterFn) results = results.filter(filterFn);
440
+ return results;
539
441
  }
540
442
  function injectContentFilesMap() {
541
- return inject(CONTENT_FILES_TOKEN);
542
- }
543
-
544
- class MarkedContentHighlighter {
545
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MarkedContentHighlighter, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
546
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MarkedContentHighlighter }); }
547
- }
548
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MarkedContentHighlighter, decorators: [{
549
- type: Injectable
550
- }] });
443
+ return inject(CONTENT_FILES_TOKEN);
444
+ }
445
+ var MarkedContentHighlighter = class MarkedContentHighlighter {
446
+ static {
447
+ this.ɵfac = i0.ɵɵngDeclareFactory({
448
+ minVersion: "12.0.0",
449
+ version: "22.2.0",
450
+ ngImport: i0,
451
+ type: MarkedContentHighlighter,
452
+ deps: [],
453
+ target: i0.ɵɵFactoryTarget.Injectable
454
+ });
455
+ }
456
+ static {
457
+ this.ɵprov = i0.ɵɵngDeclareInjectable({
458
+ minVersion: "12.0.0",
459
+ version: "22.2.0",
460
+ ngImport: i0,
461
+ type: MarkedContentHighlighter
462
+ });
463
+ }
464
+ };
465
+ i0.ɵɵngDeclareClassMetadata({
466
+ minVersion: "12.0.0",
467
+ version: "22.2.0",
468
+ ngImport: i0,
469
+ type: MarkedContentHighlighter,
470
+ decorators: [{ type: Injectable }]
471
+ });
551
472
  function withHighlighter(provider) {
552
- return { provide: MarkedContentHighlighter, ...provider };
473
+ return {
474
+ provide: MarkedContentHighlighter,
475
+ ...provider
476
+ };
553
477
  }
554
-
555
478
  function escapeHtml(value) {
556
- return value
557
- .replaceAll('&', '&amp;')
558
- .replaceAll('<', '&lt;')
559
- .replaceAll('>', '&gt;')
560
- .replaceAll('"', '&quot;')
561
- .replaceAll("'", '&#39;');
479
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
562
480
  }
563
- /**
564
- * Reduces a fenced code-block info string to a safe language identifier so it
565
- * cannot break out of the `class="language-..."` attribute it is interpolated
566
- * into.
567
- */
568
481
  function sanitizeLanguage(lang) {
569
- return (lang ?? '').match(/^[a-zA-Z0-9-+#.]*/)?.[0] ?? '';
570
- }
571
-
572
- /**
573
- * Credit goes to Scully for original implementation
574
- * https://github.com/scullyio/scully/blob/main/libs/scully/src/lib/fileHanderPlugins/markdown.ts
575
- */
576
- class MarkedSetupService {
577
- constructor() {
578
- this.highlighter = inject(MarkedContentHighlighter, {
579
- optional: true,
580
- });
581
- const renderer = new marked.Renderer();
582
- renderer.code = ({ text, lang }) => {
583
- // Let's do a language based detection like on GitHub
584
- // So we can still have non-interpreted mermaid code
585
- const language = sanitizeLanguage(lang);
586
- if (language === 'mermaid') {
587
- return '<pre class="mermaid">' + escapeHtml(text) + '</pre>';
588
- }
589
- if (!language) {
590
- return '<pre><code>' + escapeHtml(text) + '</code></pre>';
591
- }
592
- if (this.highlighter?.augmentCodeBlock) {
593
- return this.highlighter?.augmentCodeBlock(text, language);
594
- }
595
- return `<pre class="language-${language}"><code class="language-${language}">${escapeHtml(text)}</code></pre>`;
596
- };
597
- const extensions = [gfmHeadingId(), mangle()];
598
- if (this.highlighter) {
599
- extensions.push(this.highlighter.getHighlightExtension());
600
- }
601
- marked.use(...extensions, {
602
- renderer,
603
- pedantic: false,
604
- gfm: true,
605
- breaks: false,
606
- });
607
- this.marked = marked;
608
- }
609
- getMarkedInstance() {
610
- return this.marked;
611
- }
612
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MarkedSetupService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
613
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MarkedSetupService }); }
614
- }
615
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MarkedSetupService, decorators: [{
616
- type: Injectable
617
- }], ctorParameters: () => [] });
618
-
619
- class MarkdownContentRendererService {
620
- #marked = inject(MarkedSetupService, { self: true });
621
- async render(content) {
622
- const renderedContent = await this.#marked
623
- .getMarkedInstance()
624
- .parse(content);
625
- return {
626
- content: renderedContent,
627
- toc: getHeadingList(),
628
- };
629
- }
630
- getContentHeadings(content) {
631
- return [...content.matchAll(/^(#{1,6})\s+(.+?)\s*$/gm)].map((match) => ({
632
- id: match[2]
633
- .trim()
634
- .toLowerCase()
635
- .replace(/[^\w\s-]/g, '')
636
- .replace(/\s+/g, '-'),
637
- level: match[1].length,
638
- text: match[2].trim(),
639
- }));
640
- }
641
- // eslint-disable-next-line
642
- enhance() { }
643
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MarkdownContentRendererService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
644
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MarkdownContentRendererService }); }
645
- }
646
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MarkdownContentRendererService, decorators: [{
647
- type: Injectable
648
- }] });
649
-
650
- const CONTENT_FILE_LOADER = new InjectionToken('@analogjs/content/resource File Loader');
482
+ return (lang ?? "").match(/^[a-zA-Z0-9-+#.]*/)?.[0] ?? "";
483
+ }
484
+ var MarkedSetupService = class MarkedSetupService {
485
+ constructor() {
486
+ this.highlighter = inject(MarkedContentHighlighter, { optional: true });
487
+ const renderer = new marked.Renderer();
488
+ renderer.code = ({ text, lang }) => {
489
+ const language = sanitizeLanguage(lang);
490
+ if (language === "mermaid") return "<pre class=\"mermaid\">" + escapeHtml(text) + "</pre>";
491
+ if (!language) return "<pre><code>" + escapeHtml(text) + "</code></pre>";
492
+ if (this.highlighter?.augmentCodeBlock) return this.highlighter?.augmentCodeBlock(text, language);
493
+ return `<pre class="language-${language}"><code class="language-${language}">${escapeHtml(text)}</code></pre>`;
494
+ };
495
+ const extensions = [gfmHeadingId(), mangle()];
496
+ if (this.highlighter) extensions.push(this.highlighter.getHighlightExtension());
497
+ marked.use(...extensions, {
498
+ renderer,
499
+ pedantic: false,
500
+ gfm: true,
501
+ breaks: false
502
+ });
503
+ this.marked = marked;
504
+ }
505
+ getMarkedInstance() {
506
+ return this.marked;
507
+ }
508
+ static {
509
+ this.ɵfac = i0.ɵɵngDeclareFactory({
510
+ minVersion: "12.0.0",
511
+ version: "22.2.0",
512
+ ngImport: i0,
513
+ type: MarkedSetupService,
514
+ deps: [],
515
+ target: i0.ɵɵFactoryTarget.Injectable
516
+ });
517
+ }
518
+ static {
519
+ this.ɵprov = i0.ɵɵngDeclareInjectable({
520
+ minVersion: "12.0.0",
521
+ version: "22.2.0",
522
+ ngImport: i0,
523
+ type: MarkedSetupService
524
+ });
525
+ }
526
+ };
527
+ i0.ɵɵngDeclareClassMetadata({
528
+ minVersion: "12.0.0",
529
+ version: "22.2.0",
530
+ ngImport: i0,
531
+ type: MarkedSetupService,
532
+ decorators: [{ type: Injectable }],
533
+ ctorParameters: () => []
534
+ });
535
+ var MarkdownContentRendererService = class MarkdownContentRendererService {
536
+ #marked = inject(MarkedSetupService, { self: true });
537
+ async render(content) {
538
+ return {
539
+ content: await this.#marked.getMarkedInstance().parse(content),
540
+ toc: getHeadingList()
541
+ };
542
+ }
543
+ getContentHeadings(content) {
544
+ return [...content.matchAll(/^(#{1,6})\s+(.+?)\s*$/gm)].map((match) => ({
545
+ id: match[2].trim().toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-"),
546
+ level: match[1].length,
547
+ text: match[2].trim()
548
+ }));
549
+ }
550
+ enhance() {}
551
+ static {
552
+ this.ɵfac = i0.ɵɵngDeclareFactory({
553
+ minVersion: "12.0.0",
554
+ version: "22.2.0",
555
+ ngImport: i0,
556
+ type: MarkdownContentRendererService,
557
+ deps: [],
558
+ target: i0.ɵɵFactoryTarget.Injectable
559
+ });
560
+ }
561
+ static {
562
+ this.ɵprov = i0.ɵɵngDeclareInjectable({
563
+ minVersion: "12.0.0",
564
+ version: "22.2.0",
565
+ ngImport: i0,
566
+ type: MarkdownContentRendererService
567
+ });
568
+ }
569
+ };
570
+ i0.ɵɵngDeclareClassMetadata({
571
+ minVersion: "12.0.0",
572
+ version: "22.2.0",
573
+ ngImport: i0,
574
+ type: MarkdownContentRendererService,
575
+ decorators: [{ type: Injectable }]
576
+ });
577
+ const CONTENT_FILE_LOADER = new InjectionToken("@analogjs/content/resource File Loader");
651
578
  function injectContentFileLoader() {
652
- return inject(CONTENT_FILE_LOADER);
579
+ return inject(CONTENT_FILE_LOADER);
653
580
  }
654
581
  function withContentFileLoader() {
655
- return {
656
- provide: CONTENT_FILE_LOADER,
657
- useFactory() {
658
- return async () => injectContentFilesMap();
659
- },
660
- };
661
- }
662
-
663
- const CONTENT_LIST_LOADER = new InjectionToken('@analogjs/content/resource List Loader');
582
+ return {
583
+ provide: CONTENT_FILE_LOADER,
584
+ useFactory() {
585
+ return async () => injectContentFilesMap();
586
+ }
587
+ };
588
+ }
589
+ const CONTENT_LIST_LOADER = new InjectionToken("@analogjs/content/resource List Loader");
664
590
  function injectContentListLoader() {
665
- return inject(CONTENT_LIST_LOADER);
591
+ return inject(CONTENT_LIST_LOADER);
666
592
  }
667
593
  function withContentListLoader() {
668
- return {
669
- provide: CONTENT_LIST_LOADER,
670
- useFactory() {
671
- return async () => injectContentFiles();
672
- },
673
- };
594
+ return {
595
+ provide: CONTENT_LIST_LOADER,
596
+ useFactory() {
597
+ return async () => injectContentFiles();
598
+ }
599
+ };
674
600
  }
675
-
676
601
  const CONTENT_RENDERER_PROVIDERS = [
677
- {
678
- provide: ContentRenderer,
679
- useClass: NoopContentRenderer,
680
- },
681
- withContentFileLoader(),
682
- withContentListLoader(),
602
+ {
603
+ provide: ContentRenderer,
604
+ useClass: NoopContentRenderer
605
+ },
606
+ withContentFileLoader(),
607
+ withContentListLoader()
683
608
  ];
684
609
  function withMarkdownRenderer(options) {
685
- return [
686
- CONTENT_RENDERER_PROVIDERS,
687
- options?.loadMermaid
688
- ? [
689
- {
690
- provide: MERMAID_IMPORT_TOKEN,
691
- useFactory: options.loadMermaid,
692
- },
693
- ]
694
- : [],
695
- ];
610
+ return [CONTENT_RENDERER_PROVIDERS, options?.loadMermaid ? [{
611
+ provide: MERMAID_IMPORT_TOKEN,
612
+ useFactory: options.loadMermaid
613
+ }] : []];
696
614
  }
697
615
  function provideContent(...features) {
698
- return [
699
- { provide: RenderTaskService, useClass: RenderTaskService },
700
- ...features,
701
- ];
702
- }
703
- const MERMAID_IMPORT_TOKEN = new InjectionToken('mermaid_import');
704
-
705
- class AnalogMarkdownRouteComponent {
706
- constructor() {
707
- this.sanitizer = inject(DomSanitizer);
708
- this.route = inject(ActivatedRoute);
709
- this.contentRenderer = inject(ContentRenderer);
710
- this.content = this.sanitizer.bypassSecurityTrustHtml(this.route.snapshot.data['renderedAnalogContent']);
711
- this.classes = 'analog-markdown-route';
712
- }
713
- ngAfterViewChecked() {
714
- this.contentRenderer.enhance();
715
- }
716
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AnalogMarkdownRouteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
717
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.5", type: AnalogMarkdownRouteComponent, isStandalone: true, selector: "analog-markdown-route", inputs: { classes: "classes" }, hostDirectives: [{ directive: AnchorNavigationDirective }], ngImport: i0, template: `<div [innerHTML]="content" [class]="classes"></div>`, isInline: true, encapsulation: i0.ViewEncapsulation.None, preserveWhitespaces: true }); }
718
- }
719
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AnalogMarkdownRouteComponent, decorators: [{
720
- type: Component,
721
- args: [{
722
- selector: 'analog-markdown-route',
723
- standalone: true,
724
- imports: [AsyncPipe],
725
- hostDirectives: [AnchorNavigationDirective],
726
- preserveWhitespaces: true,
727
- encapsulation: ViewEncapsulation.None,
728
- template: `<div [innerHTML]="content" [class]="classes"></div>`,
729
- }]
730
- }], propDecorators: { classes: [{
731
- type: Input
732
- }] } });
733
-
734
- class AnalogMarkdownComponent {
735
- constructor() {
736
- this.sanitizer = inject(DomSanitizer);
737
- this.route = inject(ActivatedRoute);
738
- this.zone = inject(NgZone);
739
- this.platformId = inject(PLATFORM_ID);
740
- this.mermaidImport = inject(MERMAID_IMPORT_TOKEN, {
741
- optional: true,
742
- });
743
- this.contentSource = toSignal(this.getContentSource());
744
- this.htmlContent = computed(() => {
745
- const inputContent = this.content();
746
- if (inputContent) {
747
- return this.sanitizer.bypassSecurityTrustHtml(inputContent);
748
- }
749
- return this.contentSource();
750
- }, /* @ts-ignore */
751
- ...(ngDevMode ? [{ debugName: "htmlContent" }] : /* istanbul ignore next */ []));
752
- this.content = input(/* @ts-ignore */
753
- ...(ngDevMode ? [undefined, { debugName: "content" }] : /* istanbul ignore next */ []));
754
- this.classes = input('analog-markdown', /* @ts-ignore */
755
- ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
756
- this.contentRenderer = inject(ContentRenderer);
757
- if (isPlatformBrowser(this.platformId) && this.mermaidImport) {
758
- // Mermaid can only be loaded on client side
759
- this.loadMermaid(this.mermaidImport);
760
- }
761
- }
762
- getContentSource() {
763
- return this.route.data.pipe(map((data) => data['_analogContent'] ?? ''), switchMap((contentString) => this.renderContent(contentString)), map((content) => this.sanitizer.bypassSecurityTrustHtml(content)), catchError((e) => of(`There was an error ${e}`)));
764
- }
765
- async renderContent(content) {
766
- const rendered = await this.contentRenderer.render(content);
767
- return rendered.content;
768
- }
769
- ngAfterViewChecked() {
770
- this.contentRenderer.enhance();
771
- this.zone.runOutsideAngular(() => this.mermaid?.default.run());
772
- }
773
- loadMermaid(mermaidImport) {
774
- this.zone.runOutsideAngular(() =>
775
- // Wrap into an observable to avoid redundant initialization once
776
- // the markdown component is destroyed before the promise is resolved.
777
- from(mermaidImport)
778
- .pipe(takeUntilDestroyed())
779
- .subscribe((mermaid) => {
780
- this.mermaid = mermaid;
781
- this.mermaid.default.initialize({ startOnLoad: false });
782
- // Explicitly running mermaid as ngAfterViewChecked
783
- // has probably already been called
784
- this.mermaid?.default.run();
785
- }));
786
- }
787
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AnalogMarkdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
788
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: AnalogMarkdownComponent, isStandalone: true, selector: "analog-markdown", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, classes: { classPropertyName: "classes", publicName: "classes", isSignal: true, isRequired: false, transformFunction: null } }, hostDirectives: [{ directive: AnchorNavigationDirective }], ngImport: i0, template: ` <div [innerHTML]="htmlContent()" [class]="classes()"></div> `, isInline: true, encapsulation: i0.ViewEncapsulation.None, preserveWhitespaces: true }); }
789
- }
790
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AnalogMarkdownComponent, decorators: [{
791
- type: Component,
792
- args: [{
793
- selector: 'analog-markdown',
794
- standalone: true,
795
- hostDirectives: [AnchorNavigationDirective],
796
- preserveWhitespaces: true,
797
- encapsulation: ViewEncapsulation.None,
798
- template: ` <div [innerHTML]="htmlContent()" [class]="classes()"></div> `,
799
- }]
800
- }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], classes: [{ type: i0.Input, args: [{ isSignal: true, alias: "classes", required: false }] }] } });
801
-
802
- /**
803
- * Generated bundle index. Do not edit.
804
- */
805
-
616
+ return [{
617
+ provide: RenderTaskService,
618
+ useClass: RenderTaskService
619
+ }, ...features];
620
+ }
621
+ const MERMAID_IMPORT_TOKEN = new InjectionToken("mermaid_import");
622
+ var AnalogMarkdownRouteComponent = class AnalogMarkdownRouteComponent {
623
+ constructor() {
624
+ this.sanitizer = inject(DomSanitizer);
625
+ this.route = inject(ActivatedRoute);
626
+ this.contentRenderer = inject(ContentRenderer);
627
+ this.content = this.sanitizer.bypassSecurityTrustHtml(this.route.snapshot.data["renderedAnalogContent"]);
628
+ this.classes = "analog-markdown-route";
629
+ }
630
+ ngAfterViewChecked() {
631
+ this.contentRenderer.enhance();
632
+ }
633
+ static {
634
+ this.ɵfac = i0.ɵɵngDeclareFactory({
635
+ minVersion: "12.0.0",
636
+ version: "22.2.0",
637
+ ngImport: i0,
638
+ type: AnalogMarkdownRouteComponent,
639
+ deps: [],
640
+ target: i0.ɵɵFactoryTarget.Component
641
+ });
642
+ }
643
+ static {
644
+ this.ɵcmp = i0.ɵɵngDeclareComponent({
645
+ minVersion: "14.0.0",
646
+ version: "22.2.0",
647
+ type: AnalogMarkdownRouteComponent,
648
+ isStandalone: true,
649
+ selector: "analog-markdown-route",
650
+ inputs: { classes: "classes" },
651
+ hostDirectives: [{ directive: AnchorNavigationDirective }],
652
+ ngImport: i0,
653
+ template: `<div [innerHTML]="content" [class]="classes"></div>`,
654
+ isInline: true,
655
+ encapsulation: i0.ViewEncapsulation.None,
656
+ preserveWhitespaces: true
657
+ });
658
+ }
659
+ };
660
+ i0.ɵɵngDeclareClassMetadata({
661
+ minVersion: "12.0.0",
662
+ version: "22.2.0",
663
+ ngImport: i0,
664
+ type: AnalogMarkdownRouteComponent,
665
+ decorators: [{
666
+ type: Component,
667
+ args: [{
668
+ selector: "analog-markdown-route",
669
+ standalone: true,
670
+ imports: [AsyncPipe],
671
+ hostDirectives: [AnchorNavigationDirective],
672
+ preserveWhitespaces: true,
673
+ encapsulation: ViewEncapsulation.None,
674
+ template: `<div [innerHTML]="content" [class]="classes"></div>`
675
+ }]
676
+ }],
677
+ propDecorators: { classes: [{ type: Input }] }
678
+ });
679
+ var AnalogMarkdownComponent = class AnalogMarkdownComponent {
680
+ constructor() {
681
+ this.sanitizer = inject(DomSanitizer);
682
+ this.route = inject(ActivatedRoute);
683
+ this.zone = inject(NgZone);
684
+ this.platformId = inject(PLATFORM_ID);
685
+ this.mermaidImport = inject(MERMAID_IMPORT_TOKEN, { optional: true });
686
+ this.contentSource = toSignal(this.getContentSource());
687
+ this.htmlContent = computed(() => {
688
+ const inputContent = this.content();
689
+ if (inputContent) return this.sanitizer.bypassSecurityTrustHtml(inputContent);
690
+ return this.contentSource();
691
+ }, ...ngDevMode ? [{ debugName: "htmlContent" }] : /* istanbul ignore next */ []);
692
+ this.content = input(...ngDevMode ? [void 0, { debugName: "content" }] : /* istanbul ignore next */ []);
693
+ this.classes = input("analog-markdown", ...ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []);
694
+ this.contentRenderer = inject(ContentRenderer);
695
+ if (isPlatformBrowser(this.platformId) && this.mermaidImport) this.loadMermaid(this.mermaidImport);
696
+ }
697
+ getContentSource() {
698
+ return this.route.data.pipe(map((data) => data["_analogContent"] ?? ""), switchMap((contentString) => this.renderContent(contentString)), map((content) => this.sanitizer.bypassSecurityTrustHtml(content)), catchError((e) => of(`There was an error ${e}`)));
699
+ }
700
+ async renderContent(content) {
701
+ return (await this.contentRenderer.render(content)).content;
702
+ }
703
+ ngAfterViewChecked() {
704
+ this.contentRenderer.enhance();
705
+ this.zone.runOutsideAngular(() => this.mermaid?.default.run());
706
+ }
707
+ loadMermaid(mermaidImport) {
708
+ this.zone.runOutsideAngular(() => from(mermaidImport).pipe(takeUntilDestroyed()).subscribe((mermaid) => {
709
+ this.mermaid = mermaid;
710
+ this.mermaid.default.initialize({ startOnLoad: false });
711
+ this.mermaid?.default.run();
712
+ }));
713
+ }
714
+ static {
715
+ this.ɵfac = i0.ɵɵngDeclareFactory({
716
+ minVersion: "12.0.0",
717
+ version: "22.2.0",
718
+ ngImport: i0,
719
+ type: AnalogMarkdownComponent,
720
+ deps: [],
721
+ target: i0.ɵɵFactoryTarget.Component
722
+ });
723
+ }
724
+ static {
725
+ this.ɵcmp = i0.ɵɵngDeclareComponent({
726
+ minVersion: "17.1.0",
727
+ version: "22.2.0",
728
+ type: AnalogMarkdownComponent,
729
+ isStandalone: true,
730
+ selector: "analog-markdown",
731
+ inputs: {
732
+ content: {
733
+ classPropertyName: "content",
734
+ publicName: "content",
735
+ isSignal: true,
736
+ isRequired: false,
737
+ transformFunction: null
738
+ },
739
+ classes: {
740
+ classPropertyName: "classes",
741
+ publicName: "classes",
742
+ isSignal: true,
743
+ isRequired: false,
744
+ transformFunction: null
745
+ }
746
+ },
747
+ hostDirectives: [{ directive: AnchorNavigationDirective }],
748
+ ngImport: i0,
749
+ template: ` <div [innerHTML]="htmlContent()" [class]="classes()"></div> `,
750
+ isInline: true,
751
+ encapsulation: i0.ViewEncapsulation.None,
752
+ preserveWhitespaces: true
753
+ });
754
+ }
755
+ };
756
+ i0.ɵɵngDeclareClassMetadata({
757
+ minVersion: "12.0.0",
758
+ version: "22.2.0",
759
+ ngImport: i0,
760
+ type: AnalogMarkdownComponent,
761
+ decorators: [{
762
+ type: Component,
763
+ args: [{
764
+ selector: "analog-markdown",
765
+ standalone: true,
766
+ hostDirectives: [AnchorNavigationDirective],
767
+ preserveWhitespaces: true,
768
+ encapsulation: ViewEncapsulation.None,
769
+ template: ` <div [innerHTML]="htmlContent()" [class]="classes()"></div> `
770
+ }]
771
+ }],
772
+ ctorParameters: () => [],
773
+ propDecorators: {
774
+ content: [{
775
+ type: i0.Input,
776
+ args: [{
777
+ isSignal: true,
778
+ alias: "content",
779
+ required: false
780
+ }]
781
+ }],
782
+ classes: [{
783
+ type: i0.Input,
784
+ args: [{
785
+ isSignal: true,
786
+ alias: "classes",
787
+ required: false
788
+ }]
789
+ }]
790
+ }
791
+ });
806
792
  export { AnchorNavigationDirective, CONTENT_FILE_LOADER, CONTENT_LIST_LOADER, CONTENT_LOCALE, ContentRenderer, MERMAID_IMPORT_TOKEN, AnalogMarkdownComponent as MarkdownComponent, MarkdownContentRendererService, AnalogMarkdownRouteComponent as MarkdownRouteComponent, MarkedContentHighlighter, MarkedSetupService, NoopContentRenderer, filterByLocale, injectContent, injectContentFileLoader, injectContentFiles, injectContentFilesMap, injectContentListLoader, injectContentLocale, parseRawContentFile, provideContent, withContentFileLoader, withContentListLoader, withHighlighter, withLocale, withLocaleCandidates, withMarkdownRenderer };
807
- //# sourceMappingURL=analogjs-content.mjs.map
793
+
794
+ //# sourceMappingURL=analogjs-content.mjs.map