@fsegurai/ngx-markdown
Version:
Angular library that uses marked to parse markdown to html combined with Prism.js for syntax highlights
1,231 lines • 64.3 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, DestroyRef, model, signal, computed, ChangeDetectionStrategy, Component, InjectionToken, PLATFORM_ID, Injectable, SecurityContext, ElementRef, ViewContainerRef, input, booleanAttribute, output, effect, HostListener, Pipe, NgZone, NgModule } from '@angular/core';
import { Renderer, marked } from 'marked';
export { Renderer as MarkedRenderer } from 'marked';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { DomSanitizer } from '@angular/platform-browser';
import { Subject } from 'rxjs';
import { map, first } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router';
class ClipboardButtonComponent {
constructor() {
// * == SERVICE INJECTIONS ==
this._destroyRef = inject(DestroyRef);
// * == INPUTS ==
this.buttonTextCopy = model('Copy');
this.buttonTextCopied = model('Copied!');
this.copied = signal(false);
this.copiedText = computed(() => this.copied() ? this.buttonTextCopied() : this.buttonTextCopy());
this.registerDestroyCleanup();
}
/**
* Handles the click event to copy content to the clipboard.
* Sets a "copied" state to true, resets it to false after a timeout, and clears any existing timeouts if applicable.
*
* @protected - This method is intended for internal use within the component.
* @return {void} This method does not return a value.
*/
onCopyToClipboardClick() {
this.copied.set(true);
if (this.timeoutId)
clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(() => {
this.copied.set(false);
this.timeoutId = undefined;
}, 3000);
}
/**
* Clears an existing timeout if it has been set. This method is typically used to clean up resources when the component is destroyed.
* The timeout ID is reset to `undefined` after clearing to prevent unintended reuse.
*
* @private - This method is private and should not be accessed outside of this class
* @return {void} This method does not return a value.
*/
registerDestroyCleanup() {
this._destroyRef.onDestroy(() => {
// This code will run when the component is destroyed
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = undefined; // Optional, but good practice
}
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ClipboardButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.3", type: ClipboardButtonComponent, isStandalone: true, selector: "markdown-clipboard", inputs: { buttonTextCopy: { classPropertyName: "buttonTextCopy", publicName: "buttonTextCopy", isSignal: true, isRequired: false, transformFunction: null }, buttonTextCopied: { classPropertyName: "buttonTextCopied", publicName: "buttonTextCopied", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { buttonTextCopy: "buttonTextCopyChange", buttonTextCopied: "buttonTextCopiedChange" }, ngImport: i0, template: `
<button
class="markdown-clipboard-button"
[class.copied]="copied()"
(click)="onCopyToClipboardClick()">
{{ copiedText() }}
</button>
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ClipboardButtonComponent, decorators: [{
type: Component,
args: [{
selector: 'markdown-clipboard',
template: `
<button
class="markdown-clipboard-button"
[class.copied]="copied()"
(click)="onCopyToClipboardClick()">
{{ copiedText() }}
</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
}]
}], ctorParameters: () => [] });
const CLIPBOARD_OPTIONS = new InjectionToken('CLIPBOARD_OPTIONS');
/* eslint-disable */
class KatexSpecificOptions {
}
const MARKED_EXTENSIONS = new InjectionToken('MARKED_EXTENSIONS');
const MARKED_OPTIONS = new InjectionToken('MARKED_OPTIONS');
const MERMAID_OPTIONS = new InjectionToken('MERMAID_OPTIONS');
var PrismPlugin;
(function (PrismPlugin) {
PrismPlugin["CommandLine"] = "command-line";
PrismPlugin["LineHighlight"] = "line-highlight";
PrismPlugin["LineNumbers"] = "line-numbers";
})(PrismPlugin || (PrismPlugin = {}));
const ERROR_JOYPIXELS_NOT_LOADED = '[ngx-markdown] Emoji-Toolkit files required. See README for more information';
const ERROR_KATEX_NOT_LOADED = '[ngx-markdown] KaTeX files required. See README for more information';
const ERROR_MERMAID_NOT_LOADED = '[ngx-markdown] Mermaid files required. See README for more information';
const ERROR_CLIPBOARD_NOT_LOADED = '[ngx-markdown] Clipboard files required. See README for more information';
const ERROR_CLIPBOARD_VIEW_CONTAINER_REQUIRED = '[ngx-markdown] viewContainerRef parameter required for clipboard';
const ERROR_SRC_WITHOUT_HTTP_CLIENT = '[ngx-markdown] HttpClient required for src attribute. See README for more information';
const SECURITY_CONTEXT = new InjectionToken('SECURITY_CONTEXT');
class ExtendedRenderer extends Renderer {
constructor() {
super(...arguments);
this.ɵNgxMarkdownRendererExtendedForExtensions = false;
this.ɵNgxMarkdownRendererExtendedForMermaid = false;
}
}
class MarkdownService {
get options() {
return this._options;
}
set options(value) {
this._options = { ...this.DEFAULT_MARKED_OPTIONS, ...value };
}
get renderer() {
// Ensure the renderer always exists, falling back to a new instance if needed
if (!this.options.renderer)
this.options.renderer = new Renderer();
return this.options.renderer;
}
set renderer(value) {
this.options.renderer = value;
}
constructor() {
// * == SERVICE INJECTIONS ==
this._clipboardOptions = inject(CLIPBOARD_OPTIONS, { optional: true });
this._extensions = inject(MARKED_EXTENSIONS, { optional: true });
this._mermaidOptions = inject(MERMAID_OPTIONS, { optional: true });
this._platform = inject(PLATFORM_ID);
this._securityContext = inject(SECURITY_CONTEXT);
this._http = inject(HttpClient, { optional: true });
this._sanitizer = inject(DomSanitizer);
this._userMarkedOptions = inject(MARKED_OPTIONS, { optional: true });
// * == DEFAULT OPTIONS ==
this.DEFAULT_MARKED_OPTIONS = { renderer: new Renderer() };
this.DEFAULT_KATEX_OPTIONS = {
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$', right: '$', display: false },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: true },
{ left: '\\begin{align}', right: '\\end{align}', display: true },
{ left: '\\begin{align*}', right: '\\end{align*}', display: true },
{ left: '\\begin{aligned}', right: '\\end{aligned}', display: true },
{ left: '\\begin{alignat}', right: '\\end{alignat}', display: true },
{ left: '\\begin{alignat*}', right: '\\end{alignat*}', display: true },
{ left: '\\begin{alignedat}', right: '\\end{alignedat}', display: true },
{ left: '\\begin{array}', right: '\\end{array}', display: true },
{ left: '\\begin{bmatrix}', right: '\\end{bmatrix}', display: true },
{ left: '\\begin{cases}', right: '\\end{cases}', display: true },
{ left: '\\begin{CD}', right: '\\end{CD}', display: true },
{ left: '\\begin{equation}', right: '\\end{equation}', display: true },
{ left: '\\begin{gather}', right: '\\end{gather}', display: true },
{ left: '\\begin{matrix}', right: '\\end{matrix}', display: true },
{ left: '\\begin{pmatrix}', right: '\\end{pmatrix}', display: true },
{ left: '\\begin{rcases}', right: '\\end{rcases}', display: true },
{ left: '\\begin{smallmatrix}', right: '\\end{smallmatrix}', display: true },
{ left: '\\begin{vmatrix}', right: '\\end{vmatrix}', display: true },
{ left: '\\begin{Vmatrix}', right: '\\end{Vmatrix}', display: true },
],
};
this.DEFAULT_MERMAID_OPTIONS = { startOnLoad: false };
this.DEFAULT_CLIPBOARD_OPTIONS = { buttonComponent: undefined };
this.DEFAULT_PARSE_OPTIONS = {
decodeHtml: false,
inline: false,
emoji: false,
mermaid: false,
markedOptions: undefined,
disableSanitizer: false,
};
this.DEFAULT_RENDER_OPTIONS = {
clipboard: false,
clipboardOptions: undefined,
katex: false,
katexOptions: undefined,
mermaid: false,
mermaidOptions: undefined,
};
this._reload$ = new Subject();
this.reload$ = this._reload$.asObservable();
this._options = { ...this.DEFAULT_MARKED_OPTIONS, ...this._userMarkedOptions };
}
/**
* Parses a Markdown string into HTML.
* @param markdown The Markdown string to parse.
* @param parseOptions Optional configuration for the parsing process.
* @returns The parsed HTML string or a Promise of a string if extensions are asynchronous.
*/
parse(markdown, parseOptions = this.DEFAULT_PARSE_OPTIONS) {
const { decodeHtml, inline, emoji, mermaid, disableSanitizer, markedOptions: userMarkedOptions, } = parseOptions;
const markedOptions = { ...this.options, ...userMarkedOptions };
const renderer = markedOptions.renderer || this.renderer;
if (this._extensions)
this.renderer = this.extendRenderer(renderer, 'extensions');
if (mermaid)
this.renderer = this.extendRenderer(renderer, 'mermaid');
const trimmed = this.trimIndentation(markdown);
const decoded = decodeHtml ? this.decodeHtml(trimmed) : trimmed;
const emojified = emoji ? this.parseEmoji(decoded) : decoded;
const markedOutput = this.parseMarked(emojified, markedOptions, inline);
if (markedOutput instanceof Promise) {
return markedOutput.then(output => this.sanitizeOutput(output, disableSanitizer));
}
return this.sanitizeOutput(markedOutput, disableSanitizer);
}
/**
* Parses an inline Markdown string into HTML.
* @param markdown The inline Markdown string to parse.
* @param options Optional Marked options.
* @returns The parsed inline HTML string or a Promise of a string.
*/
parseInline(markdown, options) {
return marked.parseInline(markdown, options);
}
/**
* Renders additional features (clipboard, KaTeX, Mermaid) within an HTML element.
* @param element The HTML element where features should be rendered.
* @param options Optional rendering options.
* @param viewContainerRef Optional `ViewContainerRef` for dynamic component creation (required for clipboard button).
*/
render(element, options = this.DEFAULT_RENDER_OPTIONS, viewContainerRef) {
const { clipboard, clipboardOptions, katex, katexOptions, mermaid, mermaidOptions, } = options;
if (katex)
this.renderKatex(element, { ...this.DEFAULT_KATEX_OPTIONS, ...katexOptions });
if (mermaid)
this.renderMermaid(element, { ...this.DEFAULT_MERMAID_OPTIONS, ...this._mermaidOptions, ...mermaidOptions });
if (clipboard)
this.renderClipboard(element, viewContainerRef, { ...this.DEFAULT_CLIPBOARD_OPTIONS, ...this._clipboardOptions, ...clipboardOptions });
this.highlight(element);
}
/**
* Triggers a reload of Markdown content in components using this service.
*/
reload() {
this._reload$.next();
}
/**
* Fetches Markdown content from a given URL or file path.
* Automatically adds a language fence if the extension is not `.md`.
* @param src The URL or file path to the Markdown source.
* @returns An `Observable` of the Markdown content as a string.
* @throws Error if `HttpClient` is not available.
*/
getSource(src) {
if (!this._http)
throw new Error(ERROR_SRC_WITHOUT_HTTP_CLIENT);
return this._http.get(src, { responseType: 'text' }).pipe(map(markdown => this.handleExtension(src, markdown)));
}
/**
* Highlights code blocks within a specified HTML element using Prism.js.
* @param element The HTML element containing the code blocks to highlight. Defaults to `document`.
*/
highlight(element) {
if (!isPlatformBrowser(this._platform))
return;
if (typeof Prism === 'undefined' || typeof Prism.highlightAllUnder === 'undefined') {
console.warn('Prism.js not loaded. Code highlighting will not be applied.');
return;
}
const targetElement = element || document;
const noLanguageElements = targetElement.querySelectorAll('pre code:not([class*="language-"])');
noLanguageElements.forEach(x => x.classList.add('language-none'));
Prism.highlightAllUnder(targetElement);
}
/**
* Decodes HTML entities in a given HTML string.
* @param html The HTML string to decode.
*
* @private - This method is private and should not be accessed outside of this class
* @returns The HTML string with decoded entities.
*/
decodeHtml(html) {
if (!isPlatformBrowser(this._platform))
return html;
const textarea = document.createElement('textarea');
textarea.innerHTML = html;
return textarea.value;
}
/**
* Extends the Marked.js renderer with custom functionalities like extensions or Mermaid handling.
* Prevents re-extension by checking internal flags on the renderer instance.
* @param renderer The Marked.js renderer instance to extend.
* @param type The type of extension ('extensions' or 'mermaid').
*
* @private - This method is private and should not be accessed outside of this class
* @returns The extended renderer instance.
*/
extendRenderer(renderer, type) {
const extendedRenderer = renderer;
const flag = type === 'extensions' ? 'ɵNgxMarkdownRendererExtendedForExtensions' : 'ɵNgxMarkdownRendererExtendedForMermaid';
if (extendedRenderer[flag])
return renderer;
if (type === 'extensions' && this._extensions?.length > 0)
marked.use(...this._extensions);
if (type === 'mermaid') {
// eslint-disable-next-line @typescript-eslint/unbound-method
const defaultCode = renderer.code;
renderer.code = (codeToken) => {
if (codeToken.lang === 'mermaid') {
return `<div class="mermaid">${codeToken.text}</div>`;
}
else if (defaultCode) {
return defaultCode.call(renderer, codeToken);
}
return '';
};
}
extendedRenderer[flag] = true;
return renderer;
}
/**
* Adds a language fence to Markdown content if the source URL's extension is not `.md`.
* Useful for displaying code snippets from files with other extensions.
* @param src The source URL or file path.
* @param markdown The raw Markdown content.
*
* @private - This method is private and should not be accessed outside of this class
* @returns The Markdown content, possibly with a language fence.
*/
handleExtension(src, markdown) {
const extensionMatch = src.match(/\.([a-zA-Z0-9]+)(?:[?#].*)?$/);
const extension = extensionMatch ? extensionMatch[1] : '';
return extension && extension !== 'md'
? `\`\`\`${extension}\n${markdown}\n\`\`\``
: markdown;
}
/**
* Parses emoji shortcodes (e.g., `:smile:`) into Unicode emoji characters.
* Requires `joypixels` (Emoji-Toolkit) to be loaded.
* @param markdown The Markdown string to parse for emojis.
*
* @private - This method is private and should not be accessed outside of this class
* @returns The Markdown string with emojis replaced.
* @throws Error if `joypixels` is not loaded.
*/
parseEmoji(markdown) {
if (!isPlatformBrowser(this._platform))
return markdown;
if (typeof joypixels === 'undefined' || typeof joypixels.shortnameToUnicode === 'undefined') {
throw new Error(ERROR_JOYPIXELS_NOT_LOADED);
}
return joypixels.shortnameToUnicode(markdown);
}
/**
* Parses a Markdown string using Marked.js with the specified options.
* Handles both inline and block parsing.
* @param markdown The Markdown string to parse.
* @param options The Marked.js options to use for parsing.
* @param inline Whether to parse as inline Markdown.
*
* @private - This method is private and should not be accessed outside of this class
* @returns The parsed HTML string or a Promise of a string.
*/
parseMarked(markdown, options, inline = false) {
if (options.renderer) {
// Clone renderer and remove extended flags to prevent Marked.js errors
const renderer = { ...options.renderer };
delete renderer.ɵNgxMarkdownRendererExtendedForExtensions;
delete renderer.ɵNgxMarkdownRendererExtendedForMermaid;
marked.use({ renderer });
}
return inline ? this.parseInline(markdown, options) : marked(markdown, options);
}
/**
* Sanitizes the given HTML output using Angular's `DomSanitizer`.
* @param html The HTML string to sanitize.
* @param disableSanitizer If `true`, sanitation is skipped.
*
* @private - This method is private and should not be accessed outside of this class
* @returns The sanitized HTML string.
*/
sanitizeOutput(html, disableSanitizer) {
return disableSanitizer ? html : this._sanitizer.sanitize(this._securityContext, html) || '';
}
/**
* Renders clipboard copy buttons for code blocks within the given HTML element.
* Requires `ClipboardJS` to be loaded and a `ViewContainerRef` for component creation.
* @param element The HTML element containing code blocks.
* @param viewContainerRef The `ViewContainerRef` to attach the clipboard button component/template.
* @param options Clipboard rendering options.
* @private - This method is private and should not be accessed outside of this class
* @throws Error if `ClipboardJS` is not loaded or `viewContainerRef` is missing.
*/
renderClipboard(element, viewContainerRef, options) {
if (!isPlatformBrowser(this._platform))
return;
if (typeof ClipboardJS === 'undefined')
throw new Error(ERROR_CLIPBOARD_NOT_LOADED);
if (!viewContainerRef)
throw new Error(ERROR_CLIPBOARD_VIEW_CONTAINER_REQUIRED);
const { buttonComponent, buttonTemplate, buttonTextCopy, buttonTextCopied, languageButton, } = options;
const preElements = element.querySelectorAll('pre');
preElements.forEach(preElement => {
const preWrapperElement = this.createPreWrapper(preElement);
const toolbarWrapperElement = this.createToolbar(preWrapperElement);
// Register mouse enter/leave listeners
this.addToolbarHoverListeners(preWrapperElement, toolbarWrapperElement);
// Create a button component or template
const embeddedViewRef = this.createClipboardButton(viewContainerRef, buttonComponent, buttonTemplate, preElement, languageButton, buttonTextCopy, buttonTextCopied);
// Attach clipboard.js to the root node
this.attachClipboardJS(embeddedViewRef, toolbarWrapperElement, preElement);
});
}
/**
* Creates a wrapper `div` around a `<pre>` element for styling and positioning.
* @param preElement The `<pre>` element to wrap.
*
* @private - This method is private and should not be accessed outside of this class
* @returns The newly created wrapper `div`.
*/
createPreWrapper(preElement) {
const preWrapperElement = document.createElement('div');
preWrapperElement.style.position = 'relative';
preElement.parentNode.insertBefore(preWrapperElement, preElement);
preWrapperElement.appendChild(preElement);
return preWrapperElement;
}
/**
* Creates a toolbar `div` within the pre-wrapper for housing the clipboard button.
* @param preWrapperElement The wrapper `div` for the `<pre>` element.
*
* @private - This method is private and should not be accessed outside of this class
* @returns The newly created toolbar `div`.
*/
createToolbar(preWrapperElement) {
const toolbarWrapperElement = document.createElement('div');
toolbarWrapperElement.classList.add('markdown-clipboard-toolbar');
toolbarWrapperElement.style.position = 'absolute';
toolbarWrapperElement.style.top = '.5em';
toolbarWrapperElement.style.right = '.5em';
toolbarWrapperElement.style.zIndex = '1';
preWrapperElement.appendChild(toolbarWrapperElement);
return toolbarWrapperElement;
}
/**
* Adds mouse enter/leave listeners to the pre-wrapper to control toolbar visibility.
* @param preWrapperElement The wrapper `div` for the `<pre>` element.
* @param toolbarWrapperElement The toolbar `div`.
* @private - This method is private and should not be accessed outside of this class
*/
addToolbarHoverListeners(preWrapperElement, toolbarWrapperElement) {
preWrapperElement.addEventListener('mouseenter', () => toolbarWrapperElement.classList.add('hover'));
preWrapperElement.addEventListener('mouseleave', () => toolbarWrapperElement.classList.remove('hover'));
}
/**
* Creates and returns an `EmbeddedViewRef` for the clipboard button, using either a
* provided component, template, or the default `ClipboardButtonComponent`.
* @param viewContainerRef The `ViewContainerRef` to create the component/template in.
* @param buttonComponent Optional custom button component type.
* @param buttonTemplate Optional custom button template.
* @param preElement The `<pre>` element associated with the button.
* @param languageButton Whether to display the detected language on the button.
* @param buttonTextCopy Custom text for the "copy" state.
* @param buttonTextCopied Custom text for the "copied" state.
*
* @private - This method is private and should not be accessed outside of this class
* @returns An `EmbeddedViewRef` representing the created button.
*/
createClipboardButton(viewContainerRef, buttonComponent, buttonTemplate, preElement, languageButton, buttonTextCopy, buttonTextCopied) {
// declare embeddedViewRef holding variable
let embeddedViewRef;
if (buttonComponent) { // ? use the provided component via input property or provided via ClipboardOptions provider
const componentRef = viewContainerRef.createComponent(buttonComponent);
embeddedViewRef = componentRef.hostView;
componentRef.changeDetectorRef.markForCheck();
}
else if (buttonTemplate) { // ? use the provided template via input property
embeddedViewRef = viewContainerRef.createEmbeddedView(buttonTemplate);
}
else { // ? use default component
const componentRef = viewContainerRef.createComponent(ClipboardButtonComponent);
this.setClipboardButtonText(componentRef.instance, preElement, languageButton, buttonTextCopy, buttonTextCopied);
embeddedViewRef = componentRef.hostView;
componentRef.changeDetectorRef.markForCheck();
}
return embeddedViewRef;
}
/**
* Sets the `buttonTextCopy` and `buttonTextCopied` signals on a `ClipboardButtonComponent` instance.
* @param instance The `ClipboardButtonComponent` instance.
* @param preElement The associated `<pre>` element.
* @param languageButton Whether to derive the "copy" text from the code language.
* @param buttonTextCopy Custom text for the "copy" state.
* @param buttonTextCopied Custom text for the "copied" state.
* @private - This method is private and should not be accessed outside of this class
*/
setClipboardButtonText(instance, preElement, languageButton, buttonTextCopy, buttonTextCopied) {
if (!instance) {
console.error('ClipboardButtonComponent instance is undefined. Cannot set button text.');
return;
}
const detectedLanguage = languageButton ? preElement.querySelector('code')?.className.replace('language-', '') || 'Copy' : 'Copy';
instance.buttonTextCopy.set(buttonTextCopy || detectedLanguage);
instance.buttonTextCopied.set(buttonTextCopied || 'Copied!');
}
/**
* Attaches Clipboard.js functionality to the clipboard button's root node.
* Destroys the Clipboard.js instance when the `embeddedViewRef` is destroyed.
* @param embeddedViewRef The `EmbeddedViewRef` of the clipboard button.
* @param toolbarWrapperElement The toolbar `div` where the button is appended.
* @param preElement The `<pre>` element whose content will be copied.
* @private - This method is private and should not be accessed outside of this class
*/
attachClipboardJS(embeddedViewRef, toolbarWrapperElement, preElement) {
let clipboardInstance;
embeddedViewRef.rootNodes.forEach((node) => {
toolbarWrapperElement.appendChild(node);
clipboardInstance = new ClipboardJS(node, { text: () => preElement.innerText });
});
embeddedViewRef.onDestroy(() => {
if (clipboardInstance)
clipboardInstance.destroy();
});
}
/**
* Renders mathematical expressions using KaTeX within the given HTML element.
* Requires `katex` and `renderMathInElement` to be loaded.
* @param element The HTML element where KaTeX expressions should be rendered.
* @param options Optional KaTeX options.
* @private - This method is private and should not be accessed outside of this class
* @throws Error if KaTeX files are not loaded.
*/
renderKatex(element, options) {
if (!isPlatformBrowser(this._platform))
return;
if (typeof katex === 'undefined' || typeof renderMathInElement === 'undefined') {
throw new Error(ERROR_KATEX_NOT_LOADED);
}
renderMathInElement(element, options);
}
/**
* Renders Mermaid diagrams within the given HTML element.
* Requires `mermaid` to be loaded.
* @param element The HTML element containing Mermaid diagrams.
* @param options Optional Mermaid configuration.
* @private - This method is private and should not be accessed outside of this class
* @throws Error if Mermaid files are not loaded.
*/
renderMermaid(element, options = this.DEFAULT_MERMAID_OPTIONS) {
if (!isPlatformBrowser(this._platform))
return;
if (typeof mermaid === 'undefined' || typeof mermaid.initialize === 'undefined') {
throw new Error(ERROR_MERMAID_NOT_LOADED);
}
const mermaidElements = element.querySelectorAll('.mermaid');
if (mermaidElements.length > 0) {
mermaid.initialize(options);
mermaid.run({ nodes: mermaidElements });
}
}
/**
* Trims common leading indentation from each line of a Markdown string.
* This prevents unintended code block rendering in some Markdown processors.
* @param markdown The Markdown string to trim.
*
* @private - This method is private and should not be accessed outside of this class
* @returns The Markdown string with common indentation removed.
*/
trimIndentation(markdown) {
if (!markdown)
return '';
const lines = markdown.split('\n');
if (lines.length === 0)
return '';
let minIndent = Number.POSITIVE_INFINITY;
// Find the minimum indentation of non-empty lines
for (const line of lines) {
if (line.trim().length > 0) {
const indentMatch = line.match(/^\s*/);
if (indentMatch)
minIndent = Math.min(minIndent, indentMatch[0].length);
}
}
if (minIndent === Number.POSITIVE_INFINITY || minIndent === 0) {
return markdown; // No common indentation or only empty lines
}
// Remove the common indentation from each line
return lines.map(line => line.substring(minIndent)).join('\n');
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root', // Make the service a singleton and tree-shakable
}]
}], ctorParameters: () => [] });
function provideMarkdown(markdownModuleConfig) {
return [
MarkdownService,
markdownModuleConfig?.loader ?? [],
markdownModuleConfig?.clipboardOptions ?? [],
markdownModuleConfig?.markedOptions ?? [],
markdownModuleConfig?.mermaidOptions ?? [],
markdownModuleConfig?.markedExtensions ?? [],
{
provide: SECURITY_CONTEXT,
useValue: markdownModuleConfig?.sanitize ?? SecurityContext.HTML,
},
];
}
class MarkdownLinkService {
constructor() {
// * == SERVICE INJECTIONS ==
this._router = inject(Router);
/**
* Defines a set of known external URL patterns that should always be opened outside the Angular application.
* This includes common web protocols, mailto, tel, SMS, geo, file, and data URIs.
*/
this.EXTERNAL_URL_PATTERNS = [
/^https?:\/\//, // http:// or https://
/^www\./, // common web prefix (e.g., www.example.com)
/^ftp:\/\//,
/^ftps:\/\//,
/^mailto:/,
/^tel:/,
/^sms:/,
/^geo:/,
/^file:\/\//, // Explicitly file:/// to avoid `/localFile:` confusion
/^data:/,
];
/**
* Defines a set of known internal URL patterns that should be handled by the Angular router
* or specific internal application logic (like scrolling or local file access).
* This includes fragment identifiers, custom routerLink flags, relative paths,
* absolute paths within the app, and the custom '/localFile:' directive.
*/
this.INTERNAL_URL_PATTERNS = [
/^#/, // Fragment identifiers (e.g., #section)
/^\/routerLink:/, // Custom Angular router link flag (e.g., /routerLink:/path/to/route)
/^\.\.\//, // Relative parent directory (e.g., ../some-page)
/^\.\//, // Relative current directory (e.g., ./some-page)
/^\//, // Absolute path within the application (e.g., /dashboard, /users/profile)
/^\/localFile:/, // Custom flag for local file access (e.g., /localFile:assets/doc.pdf)
];
}
/**
* Checks if a given URL is an external link.
* External URLs typically start with a protocol (http, https, ftp, mailto, tel, sms, geo, file, data)
* or a known external domain prefix (www.).
* @param href The URL string to check.
*
* @private - This method is private and should not be accessed outside this class
* @returns True if the URL is external, false otherwise.
*/
isExternalUrl(href) {
if (!href)
return false;
return this.EXTERNAL_URL_PATTERNS.some(pattern => pattern.test(href));
}
/**
* Handles external URLs by opening them in a new tab.
* Removes any custom internal flags like '/localFile': before opening.
* @param target The HTMLAnchorElement that triggered the action.
* @private - This method is private and should not be accessed outside of this class
*/
externalUrlHandler(target) {
const hyperlink = target.getAttribute('href');
if (!hyperlink) {
console.warn('Attempted to handle external URL without href attribute.');
return;
}
target.setAttribute('target', '_blank');
window.open(hyperlink, '_blank');
}
/**
* Checks if a given URL is an internal link.
* Internal URLs are considered those starting with '#' (fragments),
* '/routerLink:' (custom Angular routing flag), or '.. /' (relative paths).
* It also includes paths that don't match external URL patterns.
* @param href The URL string to check.
*
* @private - This method is private and should not be accessed outside this class
* @returns True if the URL is internal, false otherwise.
*/
isInternalUrl(href) {
if (!href)
return false;
// If it's explicitly an external URL, it's not internal.
if (this.isExternalUrl(href))
return false;
// Otherwise, check if it matches any of the internal patterns.
return this.INTERNAL_URL_PATTERNS.some(pattern => pattern.test(href));
}
/**
* Navigates using the Angular Router with optional fragment and NavigationExtras.
* This helper function centralizes the routing logic.
* @param commands The path segments for Angular Router.
* @param fragment The URL fragment to scroll to (optional).
* @param routerLinkOptions Options containing global or path-specific NavigationExtras.
* @private - This method is private and should not be accessed outside of this class
*/
handleRouterNavigation(commands, fragment, routerLinkOptions) {
let extras = {};
if (routerLinkOptions?.paths?.[commands]) {
extras = { ...routerLinkOptions.paths[commands] }; // Clone to avoid modifying the original
}
else if (routerLinkOptions?.global) {
extras = { ...routerLinkOptions.global }; // Clone to avoid modifying the original
}
if (fragment) {
extras.fragment = fragment;
}
void this._router.navigate([commands], extras);
}
/**
* Handles navigation for internal URLs using the Angular Router.
* Supports hash fragments, custom routerLink paths, and general internal paths.
* Applies global or path-specific `NavigationExtras` if provided.
* @param target The HTMLAnchorElement that triggered the action.
* @param routerLinkOptions Optional options for router link behavior.
* @private - This method is private and should not be accessed outside of this class
*/
internalUrlHandler(target, routerLinkOptions) {
const path = target.getAttribute('href');
if (!path) {
console.warn('Attempted to handle internal URL without href attribute.');
return;
}
if (routerLinkOptions?.internalBrowserHandler) {
// --- Special handling for /localFile: URLs ---
if (path.startsWith('/localFile:')) {
const localFilePath = path.replace('/localFile:', '');
target.setAttribute('target', '_blank'); // Ensure it opens in a new tab
window.open(localFilePath, '_blank'); // Open local file paths externally
return;
}
// --- End special handling ---
if (path.startsWith('#')) {
void this._router.navigate([], { fragment: path.slice(1) });
return;
}
if (path.startsWith('/routerLink:')) {
const routerLinkPath = path.replace('/routerLink:', '');
const [commands, fragment] = routerLinkPath.split('#');
this.handleRouterNavigation(commands, fragment);
return;
}
// Default handling for other internal paths (e.g., relative paths, absolute paths)
const [commands, fragment] = path.split('#');
this.handleRouterNavigation(commands, fragment);
return;
}
else {
// Assuming internalDesktopHandler implies scrolling to ID without Angular Router
try {
const elementId = path.startsWith('#') ? path.slice(1) : path;
const targetElement = document.getElementById(elementId);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth' });
}
else {
// If not an ID, and it's a localFile: path, the desktop app would handle opening the file
// This part would typically interface with Electron, Capacitor, etc., not directly with window.open
console.warn(`MarkdownLinkService: Element with ID "${elementId}" not found for scrolling. For desktop, consider implementing native file open for "${path}".`);
}
}
catch (error) {
console.error('MarkdownLinkService: Error attempting to scroll to element or handle desktop link:', error);
}
}
}
/**
* Intercepts click events on anchor elements within Markdown content to handle navigation.
* Differentiates between internal and external links based on provided options and URL structure.
* @param event The click event object.
* @param routerLinkOptions Optional options to configure link handling behavior.
*/
interceptClick(event, routerLinkOptions) {
const element = event.target; // Cast directly for better type inference
// Ensure the clicked element is an anchor or within one
const anchor = element.nodeName.toLowerCase() === 'a' ? element : element.closest('a');
if (!anchor || !anchor.href)
return;
const href = anchor.getAttribute('href');
if (!href)
return;
const isExternalCandidate = this.isExternalUrl(href);
const isInternalCandidate = this.isInternalUrl(href);
const shouldHandleInternal = routerLinkOptions?.internalBrowserHandler || routerLinkOptions?.internalDesktopHandler;
const shouldHandleExternal = routerLinkOptions?.externalBrowserHandler;
// Prioritize handling if specific options are enabled and the link matches the type
if (shouldHandleExternal && isExternalCandidate) {
event.preventDefault();
event.stopPropagation();
this.externalUrlHandler(anchor);
}
else if (shouldHandleInternal && isInternalCandidate) {
event.preventDefault();
event.stopPropagation();
this.internalUrlHandler(anchor, routerLinkOptions);
}
// If no specific handler applies, let the default browser behavior occur.
// This allows for normal behavior for non-intercepted links (e.g., direct asset downloads).
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownLinkService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownLinkService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownLinkService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class MarkdownComponent {
constructor() {
// * == SERVICE INJECTIONS ==
this._markdownService = inject(MarkdownService);
this._markdownLinkService = inject(MarkdownLinkService);
this._element = inject(ElementRef);
this._viewContainerRef = inject(ViewContainerRef);
this._destroyRef = inject(DestroyRef);
// * == INPUTS ==
this.data = model();
this.src = model();
// ? Router link options for internal and external links
this.routerLinkOptions = input();
// ? Disable the sanitizer for the Markdown content
this.disableSanitizer = input(false, { transform: booleanAttribute });
this.disableRouterLinkHandler = input(false, { transform: booleanAttribute });
// ? Whether to render the Markdown inline or not
this.inline = input(false, { transform: booleanAttribute });
// ? Whether to enable the clipboard functionality
this.clipboard = input(false, { transform: booleanAttribute });
this.clipboardButtonComponent = input();
this.clipboardButtonTemplate = input();
this.clipboardButtonTextCopy = input();
this.clipboardButtonTextCopied = input();
this.clipboardLanguageButton = input();
// ? Whether to enable the emoji rendering
this.emoji = input(false, { transform: booleanAttribute });
// ? Options for KaTeX rendering
this.katex = input(false, { transform: booleanAttribute });
this.katexOptions = input();
// ? Whether to enable the Mermaid rendering
this.mermaid = input(false, { transform: booleanAttribute });
this.mermaidOptions = input();
// ? Whether to enable the line highlighting
this.lineHighlight = input(false, { transform: booleanAttribute });
this.line = input();
this.lineOffset = input();
// ? Whether to enable the line numbers
this.lineNumbers = input(false, { transform: booleanAttribute });
this.start = input();
// ? Whether to enable the command line rendering
this.commandLine = input(false, { transform: booleanAttribute });
this.filterOutput = input();
this.host = input();
this.prompt = input();
this.output = input();
this.user = input();
// * == OUTPUTS ==
this.error = output();
this.load = output();
this.ready = output();
/**
* A handler function for processing anchor elements within an internal browser.
* This function modifies the attributes of the provided anchor element to work with a custom routing mechanism.
*
* @param {HTMLAnchorElement} link - The anchor element whose attributes will be modified.
* @private - This method is private and should not be accessed outside of this class
*/
this.internalLinksConverter = (link) => {
const href = link.getAttribute('href');
const [path, fragment] = href.split('#');
link.setAttribute('data-routerLink', path);
link.setAttribute('href', `${path}${fragment ? `#${fragment}` : ''}`);
link.setAttribute('routerLink', `${path}${fragment ? `#${fragment}` : ''}`);
if (fragment)
link.setAttribute('fragment', fragment);
};
this.setupContentLoadingEffect();
}
ngAfterViewInit() {
if (!this.data() && !this.src())
this.handleTransclusion();
}
/**
* Handles document click events and processes them based on application logic.
*
* @param {MouseEvent} event - The mouse click event triggered within the document.
* @return {void}
*/
onDocumentClick(event) {
if (this.disableRouterLinkHandler())
return;
this._markdownLinkService.interceptClick(event, this.routerLinkOptions());
}
/**
* Sets up the content loading effect to handle changes to data and src inputs,
* replacing traditional change detection methods like ngOnChanges for these inputs.
* The method uses reactive programming to monitor changes and trigger respective
* content handling processes. It also listens for a reload signal from the markdownService,
* ensuring the content is reloaded when necessary, with the appropriate cleanup upon
* component destruction.
*
* @private - This method is private and should not be accessed outside of this class
* @return {void} This method does not return a value.
*/
setupContentLoadingEffect() {
// ? Effect for reacting to data() and src() input changes (replaces ngOnChanges for these)
effect(() => {
this.loadContent(); // This will call handleData or handleSrc based on the inputs
// ! Note: We avoid an `else` that triggers `handleTransclusion` here
// ! because transclusion content is only available in ngAfterViewInit.
// ! The initial transclusion is handled in ngAfterViewInit.
});
// Subscribe to markdownService.reload$ and automatically unsubscribe on component destruction
this._markdownService.reload$
.pipe(takeUntilDestroyed(this._destroyRef))
.subscribe(() => {
this.loadContent(); // This call is sufficient. render() will trigger contentRenderedTrigger.update()
});
}
/**
* Renders the Markdown content.
* @param markdown The markdown content to render.
* @param decodeHtml Whether to decode HTML entities.
* @private - This method is private and should not be accessed outside of this class
*/
async render(markdown, decodeHtml = false) {
const parsedOptions = {
decodeHtml,
inline: this.inline(),
emoji: this.emoji(),
mermaid: this.mermaid(),
disableSanitizer: this.disableSanitizer(),
};
const renderOptions = {
clipboard: this.clipboard(),
clipboardOptions: {
buttonComponent: this.clipboardButtonComponent(),
buttonTemplate: this.clipboardButtonTemplate(),
buttonTextCopy: this.clipboardButtonTextCopy(),
buttonTextCopied: this.clipboardButtonTextCopied(),
languageButton: this.clipboardLanguageButton(),
},
katex: this.katex(),
katexOptions: this.katexOptions(),
mermaid: this.mermaid(),
mermaidOptions: this.mermaidOptions(),
};
this._element.nativeElement.innerHTML = await this._markdownService.parse(markdown, parsedOptions);
this.handlePlugins();
this._markdownService.render(this._element.nativeElement, renderOptions, this._viewContainerRef);
this.processInternalLinks(); // Process internal links after rendering
this.ready.emit();
}
/**
* Processes all internal links within a native HTML element and converts them
* if they contain a specific routerLink attribute.
*
* This method queries all anchor elements within the associated native element,
* checks for the presence of the `href` attribute containing `/routerLink:`,
* and applies the `internalLinksConverter` method to each qualifying link.
*
* @private - This method is private and should not be accessed outside of this class
* @return {void} This method does not return a value.
*/
processInternalLinks() {
const links = this._element.nativeElement.querySelectorAll('a');
links.forEach(link => {
if (link.getAttribute('href')?.includes('/routerLink:') === true) {
this.internalLinksConverter(link);
}
});
}
/**
* Fetches a Markdown source using the `src` value, processes it, and emits the result or an error.
*
* The method subscribes to the Markdown source provided by the `markdownService`. On successful retrieval,
* it processes the Markdown using the `render` method and emits the result via the `load` event. In case of
* an error, it emits the error through the `error` event.
*
* @private - This method is private and should not be accessed outside of this class
* @return {void} This method does not return a value.
*/
handleSrc() {
this._markdownService
.getSource(this.src())
.pipe(takeUntilDestroyed(this._destroyRef))
.subscribe({
next: markdown => {
this.render(markdown).then(() => {
this.load.emit(markdown);
});
},
error: (error) => this.error.emit(error),
});
}
/**
* Handles the transclusion of content by rendering the innerHTML of the associated element.
* @private - This method is private and should not be accessed outside of this class
* @return {void} This method does not return a value.
*/
handleTransclusion() {
void this.render(this._element.nativeElement.innerHTML, true);
}
/**
* Handles the initialization of the plugins.
* @private - This method is private and should not be accessed outside of this class
*/
handlePlugins() {
if (this.commandLine()) {
this.setPluginClass(this._element.nativeElement, PrismPlugin.CommandLine);
this.setPluginOptions(this._element.nativeElement, {
dataFilterOutput: this.filterOutput(),
dataHost: this.host(),
dataPrompt: this.prompt(),
dataOutput: this.output(),
dataUser: this.user(),
});
}
if (this.lineHighlight()) {
this.setPluginOptions(this._element.nativeElement, { dataLine: this.line(), dataLineOffset: this.lineOffset() });
}
if (this.lineNumbers()) {
this.setPluginClass(this._element.nativeElement, PrismPlugin.LineNumbers);
this.setPluginOptions(this._element.nativeElement, { dataStart: this.start() });
}
}
/**
* Sets the plugin class to the element with the specified plugin.
* @param element The element to set the plugin class to.
* @param plugin The plugin to set.
* @private - This method is private and should not be accessed outside of this class
*/
setPluginClass(element, plugin) {
const preElements = element.querySelectorAll('pre');
preElements.forEach(preElement => {
const classes = Array.isArray(plugin) ? plugin : [plugin];
preElement.classList.add(...classes);
});
}
/**
* Sets the plugin options to the element with the specified options.
* @param element The element to set the plugin options to.
* @param options The options to set.
* @private - This method is private and should not be accessed outside of this class
*/
setPluginOptions(element, options) {
const preElements = element.querySelectorAll('pre');
preElements.forEach(preElement => {
Object.keys(options).forEach(option => {
const attributeValue = options[option];
if (attributeValue) {
const attributeName = this.toLispCase(option);
preElement.setAttribute(attributeName, attributeValue.toString());
}
});
});
}
/**
* Converts the value to a lisp-case for the plugin options.
* @param value The value to convert to lisp-case.
* @private - This method is private and should not be accessed outside of this class
*/
toLispCase(value) {
return value.replace(/([A-Z])/g, '-$1').toLowerCase();
}
/**
* Loads the content from the data or the src.
* @private - This method is private and should not be accessed outside of this class
*/
loadContent() {
const dataValue = this.data();
const srcValue = this.src();
if (dataValue) {
void this.render(dataValue);
}
else if (srcValue) {
this.handleSrc();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.3", type: MarkdownComponent, isStandalone: true, selector: "ngx-markdown, markdown, [markdown]", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, routerLinkOptions: { classPropertyName: "routerLinkOptions", publicName: "routerLinkOptions", isSignal: true, isRequired: false, transformFunction: null }, disableSanitizer: { classPropertyName: "disableSanitizer", publicName: "disableSanitizer", isSignal: true, isRequired: false, transformFunction: null }, disableRouterLinkHandler: { classPropertyName: "disableRouterLinkHandler", publicName: "disableRouterLinkHandler", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, clipboard: { classPropertyName: "clipboard", publicName: "clipboard", isSignal: true, isRequired: false, transformFunction: null }, clipboardButtonComponent: { classPropertyName: "clipboardButtonComponent", publicName: "clipboardButtonComponent", isSignal: true, isRequired: false, transformFunction: null }, clipboardButtonTemplate: { classPropertyName: "clipboardButtonTemplate", publicName: "clipboardButtonTemplate", isSignal: true, isRequired: false, transformFunction: null }, clipboardButtonTextCopy: { classPropertyName: "clipboardButtonTextCopy", publicName: "clipboardButtonTextCopy", isSignal: true, isRequired: false, transformFunction: null }, clipboardButtonTextCopied: { classPropertyName: "clipboardButtonTextCopied", publicName: "clipboardButtonTextCopied", isSignal: true, isRequired: false, transformFunction: null }, clipboardLanguageButton: { classPropertyName: "clipboardLanguageButton", publicName: "clipboardLanguageButton", isSignal: true, isRequired: false, transformFunction: null }, emoji: { classPropertyName: "emoji", publicName: "emoji", isSignal: true, isRequired: false, transformFunction: null }, katex: { classPropertyName: "katex", publicName: "katex", isSignal: true, isRequired: false, transformFunction: null }, katexOptions: { classPropertyName: "katexOptions", publicName: "katexOptions", isSignal: true, isRequired: false, transformFunction: null }, mermaid: { classPropertyName: "mermaid", publicName: "mermaid", isSignal: true, isRequired: false, transformFunction: null }, mermaidOptions: { classPropertyName: "mermaidOptions", publicName: "mermaidOptions", isSignal: true, isRequired: false, transformFunction: null }, lineHighlight: { classPropertyName: "lineHighlight", publicName: "lineHighlight", isSignal: true, isRequired: false, transformFunction: null }, line: { classPropertyName: "line", publicName: "line", isSignal: true, isRequired: false, transformFunction: null }, lineOffset: { classPropertyName: "lineOffset", publicName: "lineOffset", isSignal: true, isRequired: false, transformFunction: null }, lineNumbers: { classPropertyName: "lineNumbers", publicName: "lineNumbers", isSignal: true, isRequired: false, transformFunction: null }, start: { classPropertyName: "start", publicName: "start", isSignal: true, isRequired: false, transformFunction: null }, commandLine: { classPropertyName: "commandLine", publicName: "commandLine", isSignal: true, isRequired: false, transformFunction: null }, filterOutput: { classPropertyName: "filterOutput", publicName: "filterOutput", isSignal: true, isRequired: false, transformFunction: null }, host: { classPropertyName: "host", publicName: "host", isSignal: true, isRequired: false, transformFunction: null }, prompt: { classPropertyName: "prompt", publicName: "prompt", isSignal: true, isRequired: false, transformFunction: null }, output: { classPropertyName: "output", publicName: "output", isSignal: true, isRequired: false, transformFunction: null }, user: { classPropertyName: "user", publicName: "user", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { data: "dataChange", src: "srcChange", error: "error", load: "load", ready: "ready" }, host: { listeners: { "click": "onDocumentClick($event)" } }, ngImport: i0, template: `
<ng-content />
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownComponent, decorators: [{
type: Component,
args: [{
selector: 'ngx-markdown, markdown, [markdown]',
template: `
<ng-content />
`,
imports: [CommonModule],
}]
}], ctorParameters: () => [], propDecorators: { onDocumentClick: [{
type: HostListener,
args: ['click', ['$event']]
}] } });
class LanguagePipe {
/**
* Transforms a string value by wrapping it in a Markdown code block for a specified language.
*
* @param value The string contents to be wrapped in a code block.
* If null or undefined, it defaults to an empty string.
* @param language The programming language for the code block (e.g., 'typescript', 'html', 'css').
* If null or undefined, it defaults to an empty string.
* @returns A string formatted as a Markdown code block
* Returns an empty string if the input 'value' is not a string after null check,
* or if 'language' is not a string after null check.
*/
transform(value, language) {
const safeValue = value ?? '';
const safeLanguage = language ?? '';
if (typeof safeValue !== 'string') {
console.error(`LanguagePipe: 'value' must be a string. Received type: [${typeof value}]. Returning empty string.`);
return '';
}
if (typeof safeLanguage !== 'string') {
console.error(`LanguagePipe: 'language' must be a string. Received type: [${typeof language}]. Returning value without code block.`);
return safeValue;
}
return `\`\`\`${safeLanguage}\n${safeValue}\n\`\`\``;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: LanguagePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.0.3", ngImport: i0, type: LanguagePipe, isStandalone: true, name: "language" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: LanguagePipe, decorators: [{
type: Pipe,
args: [{
name: 'language',
}]
}] });
class MarkdownPipe {
constructor() {
// * == SERVICE INJECTIONS ==
this._markdownService = inject(MarkdownService);
this._domSanitizer = inject(DomSanitizer);
this._elementRef = inject(ElementRef);
this._viewContainerRef = inject(ViewContainerRef);
this._ngZone = inject(NgZone);
}
/**
* Transforms a Markdown string into SafeHtml and triggers a post-rendering process
* on the host element when the DOM is stable.
*
* @param value The Markdown string to transform. Can be null or undefined.
* @param options Optional configuration for parsing and rendering Markdown.
* @returns A Promise that resolves to SafeHtml ready for binding to [innerHTML].
* Returns an empty string if the input value is null, undefined, or not a string.
*/
async transform(value, options) {
if (value == null)
return '';
if (typeof value !== 'string') {
console.error(`MarkdownPipe has been invoked with an invalid value type [${typeof value}]`);
return value;
}
const parsedMarkdown = await this._markdownService.parse(value, options);
if (this._ngZone) {
this._ngZone.onStable
.pipe(first())
.subscribe(() => this._markdownService.render(this._elementRef.nativeElement, options, this._viewContainerRef));
}
else {
this._markdownService.render(this._elementRef.nativeElement, options, this._viewContainerRef);
}
return this._domSanitizer.bypassSecurityTrustHtml(parsedMarkdown);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.0.3", ngImport: i0, type: MarkdownPipe, isStandalone: true, name: "markdown" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownPipe, decorators: [{
type: Pipe,
args: [{
name: 'markdown',
}]
}] });
const sharedDeclarations = [
ClipboardButtonComponent,
LanguagePipe,
MarkdownComponent,
MarkdownPipe,
];
class MarkdownModule {
static forRoot(markdownModuleConfig) {
return {
ngModule: MarkdownModule,
providers: [
provideMarkdown(markdownModuleConfig),
],
};
}
static forChild() {
return {
ngModule: MarkdownModule,
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.3", ngImport: i0, type: MarkdownModule, imports: [ClipboardButtonComponent,
LanguagePipe,
MarkdownComponent,
MarkdownPipe], exports: [ClipboardButtonComponent,
LanguagePipe,
MarkdownComponent,
MarkdownPipe] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownModule, imports: [MarkdownComponent] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MarkdownModule, decorators: [{
type: NgModule,
args: [{
imports: sharedDeclarations,
exports: sharedDeclarations,
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { CLIPBOARD_OPTIONS, ClipboardButtonComponent, ERROR_CLIPBOARD_NOT_LOADED, ERROR_CLIPBOARD_VIEW_CONTAINER_REQUIRED, ERROR_JOYPIXELS_NOT_LOADED, ERROR_KATEX_NOT_LOADED, ERROR_MERMAID_NOT_LOADED, ERROR_SRC_WITHOUT_HTTP_CLIENT, ExtendedRenderer, KatexSpecificOptions, LanguagePipe, MARKED_EXTENSIONS, MARKED_OPTIONS, MERMAID_OPTIONS, MarkdownComponent, MarkdownModule, MarkdownPipe, MarkdownService, PrismPlugin, SECURITY_CONTEXT, provideMarkdown };
//# sourceMappingURL=fsegurai-ngx-markdown.mjs.map