UNPKG

@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
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