obsidian#MarkdownRenderer TypeScript Examples

The following examples show how to use obsidian#MarkdownRenderer. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: markdown.tsx    From obsidian-dataview with MIT License 7 votes vote down vote up
/** Hacky preact component which wraps Obsidian's markdown renderer into a neat component. */
export function RawMarkdown({
    content,
    sourcePath,
    inline = true,
    style,
    cls,
    onClick,
}: {
    content: string;
    sourcePath: string;
    inline?: boolean;
    style?: string;
    cls?: string;
    onClick?: (e: preact.JSX.TargetedMouseEvent<HTMLElement>) => void;
}) {
    const container = useRef<HTMLElement | null>(null);
    const component = useContext(DataviewContext).component;

    useEffect(() => {
        if (!container.current) return;

        container.current.innerHTML = "";
        MarkdownRenderer.renderMarkdown(content, container.current, sourcePath, component).then(() => {
            if (!container.current || !inline) return;

            // Unwrap any created paragraph elements if we are inline.
            let paragraph = container.current.querySelector("p");
            while (paragraph) {
                let children = paragraph.childNodes;
                paragraph.replaceWith(...Array.from(children));
                paragraph = container.current.querySelector("p");
            }
        });
    }, [content, sourcePath, container.current]);

    return <span ref={container} style={style} class={cls} onClick={onClick}></span>;
}
Example #2
Source File: renderer.ts    From obsidian-pandoc with MIT License 6 votes vote down vote up
// Note: parentFiles is for internal use (to prevent recursively embedded notes)
// inputFile must be an absolute file path
export default async function render (plugin: PandocPlugin, view: MarkdownView,
    inputFile: string, outputFormat: string, parentFiles: string[] = []):
    Promise<{ html: string, metadata: { [index: string]: string } }>
{
    // Use Obsidian's markdown renderer to render to a hidden <div>
    const markdown = view.data;
    const wrapper = document.createElement('div');
    wrapper.style.display = 'hidden';
    document.body.appendChild(wrapper);
    await MarkdownRenderer.renderMarkdown(markdown, wrapper, path.dirname(inputFile), view);

    // Post-process the HTML in-place
    await postProcessRenderedHTML(plugin, inputFile, wrapper, outputFormat,
        parentFiles, await mermaidCSS(plugin.settings, plugin.vaultBasePath()));
    let html = wrapper.innerHTML;
    document.body.removeChild(wrapper);

    // If it's a top level note, make the HTML a standalone document - inject CSS, a <title>, etc.
    const metadata = getYAMLMetadata(markdown);
    metadata.title ??= fileBaseName(inputFile);
    if (parentFiles.length === 0) {
        html = await standaloneHTML(plugin.settings, html, metadata.title, plugin.vaultBasePath());
    }

    return { html, metadata };
}
Example #3
Source File: render.ts    From obsidian-dataview with MIT License 6 votes vote down vote up
/** Render simple fields compactly, removing wrapping content like paragraph and span. */
export async function renderCompactMarkdown(
    markdown: string,
    container: HTMLElement,
    sourcePath: string,
    component: Component
) {
    let subcontainer = container.createSpan();
    await MarkdownRenderer.renderMarkdown(markdown, subcontainer, sourcePath, component);

    let paragraph = subcontainer.querySelector(":scope > p");
    if (subcontainer.children.length == 1 && paragraph) {
        while (paragraph.firstChild) {
            subcontainer.appendChild(paragraph.firstChild);
        }
        subcontainer.removeChild(paragraph);
    }
}
Example #4
Source File: view.ts    From obsidian-fantasy-calendar with MIT License 6 votes vote down vote up
async display() {
        this.contentEl.empty();
        this.contentEl.createEl("h4", { text: this.event.name });

        await MarkdownRenderer.renderMarkdown(
            this.event.description,
            this.contentEl,
            this.event.note,
            null
        );
    }
Example #5
Source File: ICSSettingsTab.ts    From obsidian-ics with MIT License 6 votes vote down vote up
export function getCalendarElement(
	icsName: string,
	icsUrl: string,
): HTMLElement {
	let calendarElement, titleEl;
	calendarElement = createDiv({
		cls: `calendar calendar-${icsName}`,
	});
	titleEl = calendarElement.createEl("summary", {
		cls: `calendar-name ${icsName}`
	});

	//get markdown
	const markdownHolder = createDiv();
	MarkdownRenderer.renderMarkdown(icsName, markdownHolder, "", null);

	const calendarTitleContent = createDiv();
	markdownHolder.children[0]?.tagName === "P" ?
	createDiv() :
	markdownHolder.children[0];

	//get children of markdown element, then remove them
	const markdownElements = Array.from(
		markdownHolder.children[0]?.childNodes || []
	);
	calendarTitleContent.innerHTML = "";
	calendarTitleContent.addClass("calendar-title-content");

	//add markdown children back
	const calendarTitleMarkdown = calendarTitleContent.createDiv(
		"calendar-title-markdown"
	);
	for (let i = 0; i < markdownElements.length; i++) {
		calendarTitleMarkdown.appendChild(markdownElements[i]);
	}
	titleEl.appendChild(calendarTitleContent || createDiv());

	return calendarElement;
}
Example #6
Source File: flashcard-modal.tsx    From obsidian-spaced-repetition with MIT License 6 votes vote down vote up
// slightly modified version of the renderMarkdown function in
    // https://github.com/mgmeyers/obsidian-kanban/blob/main/src/KanbanView.tsx
    async renderMarkdownWrapper(
        markdownString: string,
        containerEl: HTMLElement,
        recursiveDepth = 0
    ): Promise<void> {
        if (recursiveDepth > 4) return;

        MarkdownRenderer.renderMarkdown(
            markdownString,
            containerEl,
            this.currentCard.note.path,
            this.plugin
        );

        containerEl.findAll(".internal-embed").forEach((el) => {
            const link = this.parseLink(el.getAttribute("src"));

            // file does not exist, display dead link
            if (!link.target) {
                el.innerText = link.text;
            } else if (link.target instanceof TFile) {
                if (link.target.extension !== "md") {
                    this.embedMediaFile(el, link.target);
                } else {
                    el.innerText = "";
                    this.renderTransclude(el, link, recursiveDepth);
                }
            }
        });
    }
Example #7
Source File: main.ts    From obsidian-jupyter with MIT License 6 votes vote down vote up
async postprocessor(src: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) {
		// Render the code using the default renderer for python.
		await MarkdownRenderer.renderMarkdown('```python\n' + src + '```', el, '',
											  this.app.workspace.activeLeaf.view);

		// Needed for positioning of the button and hiding Jupyter prompts.
		el.classList.add('obsidian-jupyter');
		// Add a button to run the code.
		let button = el.querySelector('pre').createEl('button', {
			type: 'button',
			text: 'Run',
			cls: 'copy-code-button',
		});
		button.setAttribute('style', `right: 32pt`);
		button.addEventListener('click', () => {
			button.innerText = 'Running...';
			this.getJupyterClient(ctx).request({
				command: 'execute',
				source: `${this.settings.setupScript}\n${src}`,
			}).then(response => {
				// Find the div to paste the output into or create it if necessary.
				let output = el.querySelector('div.obsidian-jupyter-output');
				if (output == null) {
					output = el.createEl('div');
					output.classList.add('obsidian-jupyter-output');
				}
				// Paste the output and reset the button.
				setInnerHTML(output, response);
				button.innerText = 'Run';
			});
		});
	}
Example #8
Source File: main.ts    From obsidian-admonition with MIT License 5 votes vote down vote up
/**
     *  .callout.admonition.is-collapsible.is-collapsed
     *      .callout-title
     *          .callout-icon
     *          .callout-title-inner
     *          .callout-fold
     *      .callout-content
     */
    getAdmonitionElement(
        type: string,
        title: string,
        icon: AdmonitionIconDefinition,
        color?: string,
        collapse?: string
    ): HTMLElement {
        const admonition = createDiv({
            cls: `callout admonition admonition-${type} admonition-plugin ${
                !title?.trim().length ? "no-title" : ""
            }`,
            attr: {
                style: color ? `--callout-color: ${color};` : '',
                "data-callout": type,
                "data-callout-fold": ""
            }
        });
        const titleEl = admonition.createDiv({
            cls: `callout-title admonition-title ${
                !title?.trim().length ? "no-title" : ""
            }`
        });

        if (title && title.trim().length) {
            //build icon element
            const iconEl = titleEl.createDiv(
                "callout-icon admonition-title-icon"
            );
            if (icon && icon.name && icon.type) {
                iconEl.appendChild(
                    this.iconManager.getIconNode(icon) ?? createDiv()
                );
            }

            //get markdown
            const titleInnerEl = titleEl.createDiv(
                "callout-title-inner admonition-title-content"
            );
            MarkdownRenderer.renderMarkdown(title, titleInnerEl, "", null);
            if (
                titleInnerEl.firstElementChild &&
                titleInnerEl.firstElementChild instanceof HTMLParagraphElement
            ) {
                titleInnerEl.setChildrenInPlace(
                    Array.from(titleInnerEl.firstElementChild.childNodes)
                );
            }
        }

        //add them to title element

        if (collapse) {
            admonition.addClass("is-collapsible");
            if (collapse == "closed") {
                admonition.addClass("is-collapsed");
            }
        }
        if (!this.data.dropShadow) {
            admonition.addClass("no-drop");
        }
        return admonition;
    }
Example #9
Source File: main.ts    From obsidian-admonition with MIT License 5 votes vote down vote up
renderAdmonitionContent(
        admonitionElement: HTMLElement,
        type: string,
        content: string,
        ctx: MarkdownPostProcessorContext,
        sourcePath: string,
        src: string
    ) {
        let markdownRenderChild = new MarkdownRenderChild(admonitionElement);
        markdownRenderChild.containerEl = admonitionElement;
        if (ctx && !(typeof ctx == "string")) {
            ctx.addChild(markdownRenderChild);
        }

        if (content && content?.trim().length) {
            /**
             * Render the content as markdown and append it to the admonition.
             */
            const contentEl = this.getAdmonitionContentElement(
                type,
                admonitionElement,
                content
            );
            if (/^`{3,}mermaid/m.test(content)) {
                const wasCollapsed = !admonitionElement.hasAttribute("open");
                if (admonitionElement instanceof HTMLDetailsElement) {
                    admonitionElement.setAttribute("open", "open");
                }
                setImmediate(() => {
                    MarkdownRenderer.renderMarkdown(
                        content,
                        contentEl,
                        sourcePath,
                        markdownRenderChild
                    );
                    if (
                        admonitionElement instanceof HTMLDetailsElement &&
                        wasCollapsed
                    ) {
                        admonitionElement.removeAttribute("open");
                    }
                });
            } else {
                MarkdownRenderer.renderMarkdown(
                    content,
                    contentEl,
                    sourcePath,
                    markdownRenderChild
                );
            }

            if (
                (!content.length || contentEl.textContent.trim() == "") &&
                this.data.hideEmpty
            )
                admonitionElement.addClass("no-content");

            const taskLists = contentEl.querySelectorAll<HTMLInputElement>(
                ".task-list-item-checkbox"
            );
            if (taskLists?.length) {
                const split = src.split("\n");
                let slicer = 0;
                taskLists.forEach((task) => {
                    const line = split
                        .slice(slicer)
                        .findIndex((l) => /^[ \t>]*\- \[.\]/.test(l));

                    if (line == -1) return;
                    task.dataset.line = `${line + slicer + 1}`;
                    slicer = line + slicer + 1;
                });
            }
        }
    }
Example #10
Source File: defineGenericAnnotation.tsx    From obsidian-annotator with GNU Affero General Public License v3.0 4 votes vote down vote up
function patchSidebarMarkdownRendering(iframe: HTMLIFrameElement, filePath: string, plugin: AnnotatorPlugin) {
    type HTMLElementConstructor = typeof window.HTMLElement;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const IframeElement = (iframe.contentWindow as any).Element;
    const existingKeys = new Set([...Object.getOwnPropertyNames(IframeElement.prototype)]);
    for (const key in Element.prototype) {
        try {
            if (!existingKeys.has(key)) {
                IframeElement.prototype[key] = Element.prototype[key];
            }
        } catch (e) {}
    }

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    class ObsidianMarkdown extends ((iframe.contentWindow as any).HTMLElement as HTMLElementConstructor) {
        markdown: string;

        // Whenever an attibute is changed, this function is called. A switch statement is a good way to handle the various attributes.
        // Note that this also gets called the first time the attribute is set, so we do not need any special initialisation code.
        attributeChangedCallback(name, oldValue, newValue) {
            if (name == 'markdownbase64') {
                this.markdown = b64_to_utf8(newValue);
                (async () => {
                    MarkdownRenderer.renderMarkdown(this.markdown, this, filePath, null);
                    const maxDepth = 10;
                    const patchEmbeds = (el: HTMLElement, filePath: string, depth: number) => {
                        if (depth > maxDepth) return;
                        [...el.findAll('.internal-embed')].forEach(async (el: HTMLElement) => {
                            const src = el.getAttribute('src');
                            const target =
                                typeof src === 'string' && plugin.app.metadataCache.getFirstLinkpathDest(src, filePath);
                            if (target instanceof TFile) {
                                el.innerText = '';
                                switch (target.extension) {
                                    case 'md':
                                        el.innerHTML = `<div class="markdown-embed"><div class="markdown-embed-title">${target.basename}</div><div class="markdown-embed-content node-insert-event markdown-embed-page"><div class="markdown-preview-view"></div></div><div class="markdown-embed-link" aria-label="Open link"><svg viewBox="0 0 100 100" class="link" width="20" height="20"><path fill="currentColor" stroke="currentColor" d="M74,8c-4.8,0-9.3,1.9-12.7,5.3l-10,10c-2.9,2.9-4.7,6.6-5.1,10.6C46,34.6,46,35.3,46,36c0,2.7,0.6,5.4,1.8,7.8l3.1-3.1 C50.3,39.2,50,37.6,50,36c0-3.7,1.5-7.3,4.1-9.9l10-10c2.6-2.6,6.2-4.1,9.9-4.1s7.3,1.5,9.9,4.1c2.6,2.6,4.1,6.2,4.1,9.9 s-1.5,7.3-4.1,9.9l-10,10C71.3,48.5,67.7,50,64,50c-1.6,0-3.2-0.3-4.7-0.8l-3.1,3.1c2.4,1.1,5,1.8,7.8,1.8c4.8,0,9.3-1.9,12.7-5.3 l10-10C90.1,35.3,92,30.8,92,26s-1.9-9.3-5.3-12.7C83.3,9.9,78.8,8,74,8L74,8z M62,36c-0.5,0-1,0.2-1.4,0.6l-24,24 c-0.5,0.5-0.7,1.2-0.6,1.9c0.2,0.7,0.7,1.2,1.4,1.4c0.7,0.2,1.4,0,1.9-0.6l24-24c0.6-0.6,0.8-1.5,0.4-2.2C63.5,36.4,62.8,36,62,36 z M36,46c-4.8,0-9.3,1.9-12.7,5.3l-10,10c-3.1,3.1-5,7.2-5.2,11.6c0,0.4,0,0.8,0,1.2c0,4.8,1.9,9.3,5.3,12.7 C16.7,90.1,21.2,92,26,92s9.3-1.9,12.7-5.3l10-10C52.1,73.3,54,68.8,54,64c0-2.7-0.6-5.4-1.8-7.8l-3.1,3.1 c0.5,1.5,0.8,3.1,0.8,4.7c0,3.7-1.5,7.3-4.1,9.9l-10,10C33.3,86.5,29.7,88,26,88s-7.3-1.5-9.9-4.1S12,77.7,12,74 c0-3.7,1.5-7.3,4.1-9.9l10-10c2.6-2.6,6.2-4.1,9.9-4.1c1.6,0,3.2,0.3,4.7,0.8l3.1-3.1C41.4,46.6,38.7,46,36,46L36,46z"></path></svg></div></div>`;
                                        const previewEl = el.getElementsByClassName(
                                            'markdown-preview-view'
                                        )[0] as HTMLElement;
                                        MarkdownRenderer.renderMarkdown(
                                            await plugin.app.vault.cachedRead(target),
                                            previewEl,
                                            target.path,
                                            null
                                        );
                                        await patchEmbeds(previewEl, target.path, depth + 1);
                                        el.addClasses(['is-loaded']);
                                        break;
                                    default:
                                        el.createEl(
                                            'img',
                                            { attr: { src: plugin.app.vault.getResourcePath(target) } },
                                            img => {
                                                if (el.hasAttribute('width'))
                                                    img.setAttribute('width', el.getAttribute('width'));
                                                if (el.hasAttribute('alt'))
                                                    img.setAttribute('alt', el.getAttribute('alt'));
                                            }
                                        );
                                        el.addClasses(['image-embed', 'is-loaded']);
                                        break;
                                }
                            }
                        });
                    };
                    patchEmbeds(this, filePath, 1);
                })();
            }
        }

        // We need to specify which attributes will be watched for changes. If an attribute is not included here, attributeChangedCallback will never be called for it
        static get observedAttributes() {
            return ['markdownbase64'];
        }
    }

    // Now that our class is defined, we can register it
    iframe.contentWindow.customElements.define('obsidian-markdown', ObsidianMarkdown);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (iframe.contentWindow as any).renderObsidianMarkdown = markdown => {
        return `<obsidian-markdown markdownbase64="${utf8_to_b64(markdown)}" />`;
    };
}
Example #11
Source File: ItemModal.ts    From obsidian-rss with GNU General Public License v3.0 4 votes vote down vote up
async display(): Promise<void> {
        this.modalEl.addClass("rss-modal");
        const {contentEl} = this;
        contentEl.empty();

        //don't add any scrolling to modal content
        contentEl.style.height = "100%";
        contentEl.style.overflowY = "hidden";

        const topButtons = contentEl.createDiv('topButtons');

        let actions = Array.of(Action.CREATE_NOTE, Action.PASTE, Action.COPY, Action.OPEN);

        if (this.save) {
            this.readButton = new ButtonComponent(topButtons)
                .setIcon(this.item.read ? 'eye-off' : 'eye')
                .setTooltip(this.item.read ? t("mark_as_unread") : t("mark_as_read"))
                .onClick(async () => {
                    await this.markAsRead();
                });
            this.readButton.buttonEl.setAttribute("tabindex", "-1");
            this.readButton.buttonEl.addClass("rss-button");

            this.favoriteButton = new ButtonComponent(topButtons)
                .setIcon(this.item.favorite ? 'star-glyph' : 'star')
                .setTooltip(this.item.favorite ? t("remove_from_favorites") : t("mark_as_favorite"))
                .onClick(async () => {
                    await this.markAsFavorite();
                });
            this.favoriteButton.buttonEl.setAttribute("tabindex", "-1");
            this.favoriteButton.buttonEl.addClass("rss-button");

            actions = Array.of(Action.TAGS, ...actions);
        }


        actions.forEach((action) => {
            const button = new ButtonComponent(topButtons)
                .setIcon(action.icon)
                .setTooltip(action.name)
                .onClick(async () => {
                    await action.processor(this.plugin, this.item);
                });
            button.buttonEl.setAttribute("tabindex", "-1");
            button.buttonEl.addClass("rss-button");
        });
        //@ts-ignore
        if (this.app.plugins.plugins["obsidian-tts"]) {
            const ttsButton = new ButtonComponent(topButtons)
                .setIcon("headphones")
                .setTooltip(t("read_article_tts"))
                .onClick(async () => {
                    const content = htmlToMarkdown(this.item.content);
                    //@ts-ignore
                    await this.app.plugins.plugins["obsidian-tts"].ttsService.say(this.item.title, content, this.item.language);
                });
            ttsButton.buttonEl.addClass("rss-button");
        }

        const prevButton = new ButtonComponent(topButtons)
            .setIcon("left-arrow-with-tail")
            .setTooltip(t("previous"))
            .onClick(() => {
                this.previous();
            });
        prevButton.buttonEl.addClass("rss-button");

        const nextButton = new ButtonComponent(topButtons)
            .setIcon("right-arrow-with-tail")
            .setTooltip(t("next"))
            .onClick(() => {
                this.next();
            });
        nextButton.buttonEl.addClass("rss-button");

        const title = contentEl.createEl('h1', 'rss-title');
        title.addClass("rss-selectable");
        title.setText(this.item.title);

        const subtitle = contentEl.createEl("h3", "rss-subtitle");
        subtitle.addClass("rss-selectable");
        if (this.item.creator) {
            subtitle.appendText(this.item.creator);
        }
        if (this.item.pubDate) {
            subtitle.appendText(" - " + window.moment(this.item.pubDate).format(this.plugin.settings.dateFormat));
        }
        const tagEl = contentEl.createSpan("tags");
        this.item.tags.forEach((tag) => {
            const tagA = tagEl.createEl("a");
            tagA.setText(tag);
            tagA.addClass("tag", "rss-tag");
        });

        const content = contentEl.createDiv('rss-content');
        content.addClass("rss-scrollable-content", "rss-selectable");

        if (this.item.enclosure && this.plugin.settings.displayMedia) {
            if (this.item.enclosureType.toLowerCase().contains("audio")) {
                const audio = content.createEl("audio", {attr: {controls: "controls"}});
                audio.createEl("source", {attr: {src: this.item.enclosure, type: this.item.enclosureType}});
            }
            if (this.item.enclosureType.toLowerCase().contains("video")) {
                const video = content.createEl("video", {attr: {controls: "controls", width: "100%", height: "100%"}});
                video.createEl("source", {attr: {src: this.item.enclosure, type: this.item.enclosureType}});
            }

            //embedded yt player
            if (this.item.enclosure && this.item.id.startsWith("yt:")) {
                content.createEl("iframe", {
                    attr: {
                        type: "text/html",
                        src: "https://www.youtube.com/embed/" + this.item.enclosure,
                        width: "100%",
                        height: "100%",
                        allowFullscreen: "true"
                    }
                });
            }
        }

        if (this.item.content) {
            //prepend empty yaml to fix rendering errors
            const markdown = "---\n---" + rssToMd(this.plugin, this.item.content);

            await MarkdownRenderer.renderMarkdown(markdown, content, "", this.plugin);

            this.item.highlights.forEach(highlight => {
                if (content.innerHTML.includes(highlight)) {
                    const newNode = contentEl.createEl("mark");
                    newNode.innerHTML = highlight;
                    content.innerHTML = content.innerHTML.replace(highlight, newNode.outerHTML);
                    newNode.remove();
                } else {
                    console.log("Highlight not included");
                    console.log(highlight);
                }
            });

            content.addEventListener('contextmenu', (event) => {
                event.preventDefault();

                const selection = document.getSelection();
                const range = selection.getRangeAt(0);

                const div = contentEl.createDiv();
                const htmlContent = range.cloneContents();
                const html = htmlContent.cloneNode(true);
                div.appendChild(html);
                const selected = div.innerHTML;
                div.remove();

                const menu = new Menu(this.app);

                let previousHighlight: HTMLElement;
                if (this.item.highlights.includes(range.startContainer.parentElement.innerHTML)) {
                    previousHighlight = range.startContainer.parentElement;
                }
                if (this.item.highlights.includes(range.startContainer.parentElement.parentElement.innerHTML)) {
                    previousHighlight = range.startContainer.parentElement.parentElement;
                }

                if(previousHighlight) {
                    menu.addItem(item => {
                        item
                            .setIcon("highlight-glyph")
                            .setTitle(t("highlight_remove"))
                            .onClick(async () => {
                                const replacement = contentEl.createSpan();
                                replacement.innerHTML = previousHighlight.innerHTML;
                                previousHighlight.replaceWith(replacement);
                                this.item.highlights.remove(previousHighlight.innerHTML);

                                const feedContents = this.plugin.settings.items;
                                await this.plugin.writeFeedContent(() => {
                                    return feedContents;
                                });
                            });
                    });
                }else if(!this.item.highlights.includes(selected) && selected.length > 0) {
                    menu.addItem(item => {
                        item
                            .setIcon("highlight-glyph")
                            .setTitle(t("highlight"))
                            .onClick(async () => {
                                const newNode = contentEl.createEl("mark");
                                newNode.innerHTML = selected;
                                range.deleteContents();
                                range.insertNode(newNode);
                                this.item.highlights.push(selected);

                                const feedContents = this.plugin.settings.items;
                                await this.plugin.writeFeedContent(() => {
                                    return feedContents;
                                });

                                //cleaning up twice to remove nested elements
                                this.removeDanglingElements(contentEl);
                                this.removeDanglingElements(contentEl);
                            });
                    });
                }

                if(selected.length > 0) {
                    menu
                        .addItem(item => {
                            item
                                .setIcon("documents")
                                .setTitle(t("copy_to_clipboard"))
                                .onClick(async () => {
                                    await copy(selection.toString());
                                });
                        });
                    //@ts-ignore
                    if (this.app.plugins.plugins["obsidian-tts"]) {
                        menu.addItem(item => {
                            item
                                .setIcon("headphones")
                                .setTitle(t("read_article_tts"))
                                .onClick(() => {
                                    //@ts-ignore
                                    const tts = this.app.plugins.plugins["obsidian-tts"].ttsService;
                                    tts.say("", selection.toString());
                                });
                        });
                    }
                }

                menu.showAtMouseEvent(event);
            });
        }
    }