UNPKG

@analogjs/content

Version:

Content Rendering for Analog

777 lines (755 loc) 32.7 kB
import * as i0 from '@angular/core'; import { inject, HostListener, Directive, Injectable, TransferState, makeStateKey, InjectionToken, signal, ɵPendingTasksInternal as _PendingTasksInternal, Input, ViewEncapsulation, Component, NgZone, PLATFORM_ID, computed, input } from '@angular/core'; import { DOCUMENT, Location, AsyncPipe, isPlatformBrowser } from '@angular/common'; import { Router, ActivatedRoute } from '@angular/router'; import { isObservable, firstValueFrom, of, Observable, from } from 'rxjs'; import { switchMap, map, tap, catchError } from 'rxjs/operators'; import fm from 'front-matter'; import { gfmHeadingId, getHeadingList } from 'marked-gfm-heading-id'; import { marked } from 'marked'; import { mangle } from 'marked-mangle'; import { DomSanitizer } from '@angular/platform-browser'; import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop'; class AnchorNavigationDirective { constructor() { this.document = inject(DOCUMENT); this.location = inject(Location); this.router = inject(Router); } handleNavigation(element) { if (element instanceof HTMLAnchorElement && isInternalUrl(element, this.document) && hasTargetSelf(element) && !hasDownloadAttribute(element)) { const { pathname, search, hash } = element; const url = this.location.normalize(`${pathname}${search}${hash}`); this.router.navigateByUrl(url); return false; } return true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: AnchorNavigationDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.0", type: AnchorNavigationDirective, isStandalone: true, selector: "[analogAnchorNavigation]", host: { listeners: { "click": "handleNavigation($event.target)" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: AnchorNavigationDirective, decorators: [{ type: Directive, args: [{ selector: '[analogAnchorNavigation]', standalone: true, }] }], propDecorators: { handleNavigation: [{ type: HostListener, args: ['click', ['$event.target']] }] } }); function hasDownloadAttribute(anchorElement) { return anchorElement.getAttribute('download') !== null; } function hasTargetSelf(anchorElement) { return !anchorElement.target || anchorElement.target === '_self'; } function isInternalUrl(anchorElement, document) { return (anchorElement.host === document.location.host && anchorElement.protocol === document.location.protocol); } /// <reference types="vite/client" /> class ContentRenderer { async render(content) { return { content, toc: [] }; } // Backward-compatible API for consumers that read headings directly. getContentHeadings(_content) { return []; } // eslint-disable-next-line enhance() { } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ContentRenderer, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ContentRenderer }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ContentRenderer, decorators: [{ type: Injectable }] }); class NoopContentRenderer { constructor() { this.transferState = inject(TransferState); this.contentId = 0; } /** * Generates a hash from the content string * to be used with the transfer state */ generateHash(str) { let hash = 0; for (let i = 0, len = str.length; i < len; i++) { let chr = str.charCodeAt(i); hash = (hash << 5) - hash + chr; hash |= 0; // Convert to 32bit integer } return hash; } async render(content) { this.contentId = this.generateHash(content); const toc = this.getContentHeadings(content); const key = makeStateKey(`content-headings-${this.contentId}`); if (import.meta.env.SSR === true) { this.transferState.set(key, toc); return { content, toc }; } return { content, toc: this.transferState.get(key, toc), }; } enhance() { } getContentHeadings(content) { return this.extractHeadings(content); } extractHeadings(content) { const markdownHeadings = this.extractHeadingsFromMarkdown(content); if (markdownHeadings.length > 0) { return markdownHeadings; } const htmlHeadings = this.extractHeadingsFromHtml(content); return htmlHeadings; } extractHeadingsFromMarkdown(content) { const lines = content.split('\n'); const toc = []; const slugCounts = new Map(); for (const line of lines) { const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line); if (!match) { continue; } const level = match[1].length; const text = match[2].trim(); if (!text) { continue; } const baseSlug = text .toLowerCase() .replace(/[^\w\s-]/g, '') .trim() .replace(/\s+/g, '-'); const count = slugCounts.get(baseSlug) ?? 0; slugCounts.set(baseSlug, count + 1); const id = count === 0 ? baseSlug : `${baseSlug}-${count}`; toc.push({ id, level, text }); } return toc; } extractHeadingsFromHtml(content) { const toc = []; const slugCounts = new Map(); const headingRegex = /<h([1-6])([^>]*)>([\s\S]*?)<\/h\1>/gi; for (const match of content.matchAll(headingRegex)) { const level = Number(match[1]); const attrs = match[2] ?? ''; const rawInner = match[3] ?? ''; const text = rawInner.replace(/<[^>]+>/g, '').trim(); if (!text) { continue; } const idMatch = /\sid=(['"])(.*?)\1/i.exec(attrs) ?? /\sid=([^\s>]+)/i.exec(attrs); let id = idMatch?.[2] ?? idMatch?.[1] ?? ''; if (!id) { id = this.makeSlug(text, slugCounts); } toc.push({ id, level, text }); } return toc; } makeSlug(text, slugCounts) { const baseSlug = text .toLowerCase() .replace(/[^\w\s-]/g, '') .trim() .replace(/\s+/g, '-'); const count = slugCounts.get(baseSlug) ?? 0; slugCounts.set(baseSlug, count + 1); return count === 0 ? baseSlug : `${baseSlug}-${count}`; } } /** * Token for the active content locale. * Provided via `withLocale()` in `provideContent()`. * * When set, `injectContentFiles()` filters to content matching this locale, * and `injectContent()` resolves locale-prefixed content paths first. */ const CONTENT_LOCALE = new InjectionToken('@analogjs/content Locale'); /** * Injects the content locale, returning null if not configured. */ function injectContentLocale() { return inject(CONTENT_LOCALE, { optional: true }); } /** * Content feature that sets the active locale for content resolution. * * When provided, content APIs become locale-aware: * - `injectContentFiles()` filters to files in the locale subdirectory * or with a matching `locale` frontmatter attribute. * - `injectContent()` tries locale-prefixed paths first * (e.g., `content/fr/blog/post.md` before `content/blog/post.md`). * * Usage: * ```typescript * // With loader — runs in injection context * provideContent( * withMarkdownRenderer(), * withLocale({ loadLocale: injectLocale }), * ) * * // Static locale * provideContent( * withMarkdownRenderer(), * withLocale('fr'), * ) * ``` */ function withLocale(locale) { if (typeof locale === 'string') { return { provide: CONTENT_LOCALE, useValue: locale }; } return { provide: CONTENT_LOCALE, useFactory: locale.loadLocale }; } /** * Filters content files by locale using map-based key lookup. * * Matching rules: * 1. Frontmatter `locale` attribute matches the active locale. * 2. File is in the active locale subdirectory (e.g., `/content/fr/blog/post`). * 3. File has no locale marker and no localized variant exists — included as universal content. * * Files in a different locale's subdirectory are always excluded. */ function filterByLocale(files, locale) { const localePrefix = `/content/${locale}/`; // Collect all locale prefixes present in the file set const allLocalePrefixes = new Set(); for (const file of files) { const match = file.filename.match(/\/content\/([a-z]{2}(?:-[a-zA-Z]+)?)\//); if (match) { allLocalePrefixes.add(`/content/${match[1]}/`); } } // Build set of base paths that have a localized variant for the active locale const localizedBasePaths = new Set(); for (const file of files) { if (file.filename.includes(localePrefix)) { localizedBasePaths.add(file.filename.replace(localePrefix, '/content/')); } } return files.filter((file) => { // Frontmatter locale attribute takes priority if (file.attributes['locale']) { return file.attributes['locale'] === locale; } // File is in the active locale subdirectory — include if (file.filename.includes(localePrefix)) { return true; } // File is in a different locale's subdirectory — exclude for (const prefix of allLocalePrefixes) { if (prefix !== localePrefix && file.filename.includes(prefix)) { return false; } } // Universal content — include only if no localized variant exists return !localizedBasePaths.has(file.filename); }); } /** * Prepends locale-prefixed candidates before the standard candidates * for content file map key lookup. */ function withLocaleCandidates(candidates, locale) { if (!locale) { return candidates; } const localeCandidates = candidates.map((c) => c.replace('/src/content/', `/src/content/${locale}/`)); return [...localeCandidates, ...candidates]; } /** * Returns the list of content files by filename with ?analog-content-list=true. * We use the query param to transform the return into an array of * just front matter attributes. * * @returns */ const getContentFilesList = () => { let ANALOG_CONTENT_FILE_LIST = {}; return ANALOG_CONTENT_FILE_LIST; }; /** * Returns the lazy loaded content files for lookups. * * @returns */ const getContentFiles = () => { let ANALOG_CONTENT_ROUTE_FILES = {}; return ANALOG_CONTENT_ROUTE_FILES; }; function getSlug(filename) { // Extract the last path segment without its extension. // Handles names with dots like [[...slug]].md by stripping only the final extension. const lastSegment = (filename.split(/[/\\]/).pop() || '').trim(); const base = lastSegment.replace(/\.[^./\\]+$/, ''); // strip only the final extension // Treat index.md as index route => empty slug return base === 'index' ? '' : base; } const CONTENT_FILES_LIST_TOKEN = new InjectionToken('@analogjs/content Content Files List', { providedIn: 'root', factory() { const contentFiles = getContentFilesList(); return Object.keys(contentFiles).map((filename) => { const attributes = contentFiles[filename]; const slug = attributes['slug']; return { filename, attributes, slug: slug ? encodeURI(slug) : encodeURI(getSlug(filename)), }; }); }, }); const CONTENT_FILES_TOKEN = new InjectionToken('@analogjs/content Content Files', { providedIn: 'root', factory() { const contentFiles = getContentFiles(); const allFiles = { ...contentFiles }; const contentFilesList = inject(CONTENT_FILES_LIST_TOKEN); const lookup = {}; contentFilesList.forEach((item) => { const contentFilename = item.filename.replace(/(.*?)\/content/, '/src/content'); const fileParts = contentFilename.split('/'); const filePath = fileParts.slice(0, fileParts.length - 1).join('/'); const fileNameParts = fileParts[fileParts.length - 1].split('.'); const ext = fileNameParts[fileNameParts.length - 1]; let slug = (item.slug ?? ''); // Default empty slug to 'index' if (slug === '') { slug = 'index'; } // Slugs with path separators are scoped to the file's top-level // subdirectory (e.g. `/src/content/docs`) rather than absolute root, // so nested files keep resolving through `subdirectory` lookups. // Files directly under `/src/content` keep the original root-relative // behavior since they have no subdirectory of their own. const subdirRoot = fileParts.length > 4 ? fileParts.slice(0, 4).join('/') : '/src/content'; const newBase = slug.includes('/') ? `${subdirRoot}/${slug}` : `${filePath}/${slug}`; lookup[contentFilename] = `${newBase}.${ext}`.replace(/\/{2,}/g, '/'); }); const objectUsingSlugAttribute = {}; Object.entries(allFiles).forEach((entry) => { const filename = entry[0]; const value = entry[1]; const strippedFilename = filename.replace(/^\/(.*?)\/content/, '/src/content'); const newFilename = lookup[strippedFilename]; if (newFilename !== undefined) { const objectFilename = newFilename.replace(/^\/(.*?)\/content/, '/src/content'); objectUsingSlugAttribute[objectFilename] = value; } }); return objectUsingSlugAttribute; }, }); const CONTENT_FILES_MAP_TOKEN = new InjectionToken('@analogjs/content Content Files', { providedIn: 'root', factory() { return signal(inject(CONTENT_FILES_TOKEN)); }, }); function parseRawContentFile(rawContentFile) { const { body, attributes } = fm(rawContentFile); return { content: body, attributes }; } async function waitFor(prom) { if (isObservable(prom)) { prom = firstValueFrom(prom); } if (typeof Zone === 'undefined') { return prom; } const macroTask = Zone.current.scheduleMacroTask(`AnalogContentResolve-${Math.random()}`, () => { }, {}, () => { }); return prom.then((p) => { macroTask.invoke(); return p; }); } class RenderTaskService { #pendingTasks = inject(_PendingTasksInternal); addRenderTask() { return this.#pendingTasks.add(); } clearRenderTask(clear) { if (typeof clear === 'function') { clear(); } else if (typeof this.#pendingTasks.remove === 'function') { this.#pendingTasks.remove(clear); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: RenderTaskService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: RenderTaskService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: RenderTaskService, decorators: [{ type: Injectable }] }); /// <reference types="vite/client" /> function getContentFile(contentFiles, prefix, slug, fallback, renderTaskService, contentRenderer, locale) { // Normalize file keys so both "/src/content/..." and "/<project>/src/content/..." resolve. const normalizedFiles = {}; for (const [key, resolver] of Object.entries(contentFiles)) { const normalizedKey = key .replace(/^(?:.*)\/content/, '/src/content') .replace(/\/{2,}/g, '/'); normalizedFiles[normalizedKey] = resolver; } const base = `/src/content/${prefix}${slug}`.replace(/\/{2,}/g, '/'); const candidates = [ `${base}.md`, `${base}.agx`, `${base}/index.md`, `${base}/index.agx`, ]; const allCandidates = withLocaleCandidates(candidates, locale); const matchKey = allCandidates.find((k) => k in normalizedFiles); const contentFile = matchKey ? normalizedFiles[matchKey] : undefined; const resolvedBase = (matchKey || `${base}.md`).replace(/\.(md|agx)$/, ''); if (!contentFile) { return of({ filename: resolvedBase, attributes: {}, slug: '', content: fallback, toc: [], }); } const contentTask = renderTaskService.addRenderTask(); return new Observable((observer) => { const contentResolver = contentFile(); if (import.meta.env.SSR === true) { waitFor(contentResolver).then((content) => { observer.next(content); observer.complete(); setTimeout(() => renderTaskService.clearRenderTask(contentTask), 10); }); } else { contentResolver.then((content) => { observer.next(content); observer.complete(); }); } }).pipe(switchMap((contentFile) => { if (typeof contentFile === 'string') { const { content, attributes } = parseRawContentFile(contentFile); return from(contentRenderer.render(content)).pipe(map((rendered) => ({ filename: resolvedBase, slug, attributes, content, toc: rendered.toc ?? [], }))); } return of({ filename: resolvedBase, slug, attributes: contentFile.metadata, content: contentFile.default, toc: [], }); })); } /** * Retrieves the static content using the provided param and/or prefix. * * @param param route parameter (default: 'slug') * @param fallback fallback text if content file is not found (default: 'No Content Found') */ function injectContent(param = 'slug', fallback = 'No Content Found') { const contentFiles = inject(CONTENT_FILES_TOKEN); const contentRenderer = inject(ContentRenderer); const renderTaskService = inject(RenderTaskService); const locale = inject(CONTENT_LOCALE, { optional: true }); const task = renderTaskService.addRenderTask(); if (typeof param === 'string' || 'param' in param) { const prefix = typeof param === 'string' ? '' : `${param.subdirectory}/`; const route = inject(ActivatedRoute); const paramKey = typeof param === 'string' ? param : param.param; return route.paramMap.pipe(map((params) => params.get(paramKey)), switchMap((slug) => { if (slug) { return getContentFile(contentFiles, prefix, slug, fallback, renderTaskService, contentRenderer, locale); } return of({ filename: '', slug: '', attributes: {}, content: fallback, toc: [], }); }), tap(() => renderTaskService.clearRenderTask(task))); } else { return getContentFile(contentFiles, '', param.customFilename, fallback, renderTaskService, contentRenderer, locale).pipe(tap(() => renderTaskService.clearRenderTask(task))); } } function injectContentFiles(filterFn) { const renderTaskService = inject(RenderTaskService); const task = renderTaskService.addRenderTask(); const allContentFiles = inject(CONTENT_FILES_LIST_TOKEN); const locale = inject(CONTENT_LOCALE, { optional: true }); renderTaskService.clearRenderTask(task); let results = allContentFiles; if (locale) { results = filterByLocale(results, locale); } if (filterFn) { results = results.filter(filterFn); } return results; } function injectContentFilesMap() { return inject(CONTENT_FILES_TOKEN); } class MarkedContentHighlighter { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MarkedContentHighlighter, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MarkedContentHighlighter }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MarkedContentHighlighter, decorators: [{ type: Injectable }] }); function withHighlighter(provider) { return { provide: MarkedContentHighlighter, ...provider }; } /** * Credit goes to Scully for original implementation * https://github.com/scullyio/scully/blob/main/libs/scully/src/lib/fileHanderPlugins/markdown.ts */ class MarkedSetupService { constructor() { this.highlighter = inject(MarkedContentHighlighter, { optional: true, }); const renderer = new marked.Renderer(); renderer.code = ({ text, lang }) => { // Let's do a language based detection like on GitHub // So we can still have non-interpreted mermaid code if (lang === 'mermaid') { return '<pre class="mermaid">' + text + '</pre>'; } if (!lang) { return '<pre><code>' + text + '</code></pre>'; } if (this.highlighter?.augmentCodeBlock) { return this.highlighter?.augmentCodeBlock(text, lang); } return `<pre class="language-${lang}"><code class="language-${lang}">${text}</code></pre>`; }; const extensions = [gfmHeadingId(), mangle()]; if (this.highlighter) { extensions.push(this.highlighter.getHighlightExtension()); } marked.use(...extensions, { renderer, pedantic: false, gfm: true, breaks: false, }); this.marked = marked; } getMarkedInstance() { return this.marked; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MarkedSetupService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MarkedSetupService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MarkedSetupService, decorators: [{ type: Injectable }], ctorParameters: () => [] }); class MarkdownContentRendererService { #marked = inject(MarkedSetupService, { self: true }); async render(content) { const renderedContent = await this.#marked .getMarkedInstance() .parse(content); return { content: renderedContent, toc: getHeadingList(), }; } getContentHeadings(content) { return [...content.matchAll(/^(#{1,6})\s+(.+?)\s*$/gm)].map((match) => ({ id: match[2] .trim() .toLowerCase() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-'), level: match[1].length, text: match[2].trim(), })); } // eslint-disable-next-line enhance() { } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MarkdownContentRendererService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MarkdownContentRendererService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MarkdownContentRendererService, decorators: [{ type: Injectable }] }); const CONTENT_FILE_LOADER = new InjectionToken('@analogjs/content/resource File Loader'); function injectContentFileLoader() { return inject(CONTENT_FILE_LOADER); } function withContentFileLoader() { return { provide: CONTENT_FILE_LOADER, useFactory() { return async () => injectContentFilesMap(); }, }; } const CONTENT_LIST_LOADER = new InjectionToken('@analogjs/content/resource List Loader'); function injectContentListLoader() { return inject(CONTENT_LIST_LOADER); } function withContentListLoader() { return { provide: CONTENT_LIST_LOADER, useFactory() { return async () => injectContentFiles(); }, }; } const CONTENT_RENDERER_PROVIDERS = [ { provide: ContentRenderer, useClass: NoopContentRenderer, }, withContentFileLoader(), withContentListLoader(), ]; function withMarkdownRenderer(options) { return [ CONTENT_RENDERER_PROVIDERS, options?.loadMermaid ? [ { provide: MERMAID_IMPORT_TOKEN, useFactory: options.loadMermaid, }, ] : [], ]; } function provideContent(...features) { return [ { provide: RenderTaskService, useClass: RenderTaskService }, ...features, ]; } const MERMAID_IMPORT_TOKEN = new InjectionToken('mermaid_import'); class AnalogMarkdownRouteComponent { constructor() { this.sanitizer = inject(DomSanitizer); this.route = inject(ActivatedRoute); this.contentRenderer = inject(ContentRenderer); this.content = this.sanitizer.bypassSecurityTrustHtml(this.route.snapshot.data['renderedAnalogContent']); this.classes = 'analog-markdown-route'; } ngAfterViewChecked() { this.contentRenderer.enhance(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: AnalogMarkdownRouteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.0", 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 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: AnalogMarkdownRouteComponent, decorators: [{ type: Component, args: [{ selector: 'analog-markdown-route', standalone: true, imports: [AsyncPipe], hostDirectives: [AnchorNavigationDirective], preserveWhitespaces: true, encapsulation: ViewEncapsulation.None, template: `<div [innerHTML]="content" [class]="classes"></div>`, }] }], propDecorators: { classes: [{ type: Input }] } }); class AnalogMarkdownComponent { constructor() { this.sanitizer = inject(DomSanitizer); this.route = inject(ActivatedRoute); this.zone = inject(NgZone); this.platformId = inject(PLATFORM_ID); this.mermaidImport = inject(MERMAID_IMPORT_TOKEN, { optional: true, }); this.contentSource = toSignal(this.getContentSource()); this.htmlContent = computed(() => { const inputContent = this.content(); if (inputContent) { return this.sanitizer.bypassSecurityTrustHtml(inputContent); } return this.contentSource(); }, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "htmlContent" }] : /* istanbul ignore next */ [])); this.content = input(/* @ts-ignore */ ...(ngDevMode ? [undefined, { debugName: "content" }] : /* istanbul ignore next */ [])); this.classes = input('analog-markdown', /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ [])); this.contentRenderer = inject(ContentRenderer); if (isPlatformBrowser(this.platformId) && this.mermaidImport) { // Mermaid can only be loaded on client side this.loadMermaid(this.mermaidImport); } } getContentSource() { 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}`))); } async renderContent(content) { const rendered = await this.contentRenderer.render(content); return rendered.content; } ngAfterViewChecked() { this.contentRenderer.enhance(); this.zone.runOutsideAngular(() => this.mermaid?.default.run()); } loadMermaid(mermaidImport) { this.zone.runOutsideAngular(() => // Wrap into an observable to avoid redundant initialization once // the markdown component is destroyed before the promise is resolved. from(mermaidImport) .pipe(takeUntilDestroyed()) .subscribe((mermaid) => { this.mermaid = mermaid; this.mermaid.default.initialize({ startOnLoad: false }); // Explicitly running mermaid as ngAfterViewChecked // has probably already been called this.mermaid?.default.run(); })); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: AnalogMarkdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.0", 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 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: AnalogMarkdownComponent, decorators: [{ type: Component, args: [{ selector: 'analog-markdown', standalone: true, hostDirectives: [AnchorNavigationDirective], preserveWhitespaces: true, encapsulation: ViewEncapsulation.None, template: ` <div [innerHTML]="htmlContent()" [class]="classes()"></div> `, }] }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], classes: [{ type: i0.Input, args: [{ isSignal: true, alias: "classes", required: false }] }] } }); /** * Generated bundle index. Do not edit. */ 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 }; //# sourceMappingURL=analogjs-content.mjs.map