import renderService from "./render.js"; import protectedSessionService from "./protected_session.js"; import protectedSessionHolder from "./protected_session_holder.js"; import openService from "./open.js"; import froca from "./froca.js"; import utils from "./utils.js"; import linkService from "./link.js"; import treeService from "./tree.js"; import FNote from "../entities/fnote.js"; import FAttachment from "../entities/fattachment.js"; import imageContextMenuService from "../menus/image_context_menu.js"; import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js"; import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js"; import renderDoc from "./doc_renderer.js"; import { t } from "../services/i18n.js"; import WheelZoom from 'vanilla-js-wheel-zoom'; import { renderMathInElement } from "./math.js"; import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons"; let idCounter = 1; interface Options { tooltip?: boolean; trim?: boolean; imageHasZoom?: boolean; } const CODE_MIME_TYPES = new Set(["application/json"]); async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) { options = Object.assign( { tooltip: false }, options ); const type = getRenderingType(entity); // attachment supports only image and file/pdf/audio/video const $renderedContent = $('
'); if (type === "text" || type === "book") { await renderText(entity, $renderedContent); } else if (type === "code") { await renderCode(entity, $renderedContent); } else if (["image", "canvas", "mindMap"].includes(type)) { renderImage(entity, $renderedContent, options); } else if (!options.tooltip && ["file", "pdf", "audio", "video"].includes(type)) { renderFile(entity, type, $renderedContent); } else if (type === "mermaid") { await renderMermaid(entity, $renderedContent); } else if (type === "render" && entity instanceof FNote) { const $content = $("
"); await renderService.render(entity, $content); $renderedContent.append($content); } else if (type === "doc" && "noteId" in entity) { const $content = await renderDoc(entity); $renderedContent.html($content.html()); } else if (!options.tooltip && type === "protectedSession") { const $button = $(``).on("click", protectedSessionService.enterProtectedSession); $renderedContent.append($("
").append("
This note is protected and to access it you need to enter password.
").append("
").append($button)); } else if (entity instanceof FNote) { $renderedContent.append( $("
") .css("display", "flex") .css("justify-content", "space-around") .css("align-items", "center") .css("height", "100%") .css("font-size", "500%") .append($("").addClass(entity.getIcon())) ); } if (entity instanceof FNote) { $renderedContent.addClass(entity.getCssClass()); } return { $renderedContent, type }; } async function renderText(note: FNote | FAttachment, $renderedContent: JQuery) { // entity must be FNote const blob = await note.getBlob(); if (blob && !utils.isHtmlEmpty(blob.content)) { $renderedContent.append($('
').html(blob.content)); if ($renderedContent.find("span.math-tex").length > 0) { renderMathInElement($renderedContent[0], { trust: true }); } const getNoteIdFromLink = (el: HTMLElement) => treeService.getNoteIdFromUrl($(el).attr("href") || ""); const referenceLinks = $renderedContent.find("a.reference-link"); const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el)); await froca.getNotes(noteIdsToPrefetch); for (const el of referenceLinks) { await linkService.loadReferenceLinkTitle($(el)); } await formatCodeBlocks($renderedContent); } else if (note instanceof FNote) { await renderChildrenList($renderedContent, note); } } /** * Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type. */ async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery) { const blob = await note.getBlob(); let content = blob?.content || ""; if (note.mime === "application/json") { try { content = JSON.stringify(JSON.parse(content), null, 4); } catch (e) { // Ignore JSON parsing errors. } } const $codeBlock = $(""); $codeBlock.text(content); $renderedContent.append($("
").append($codeBlock));
    await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
}

function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery, options: Options = {}) {
    const encodedTitle = encodeURIComponent(entity.title);

    let url;

    if (entity instanceof FNote) {
        url = `api/images/${entity.noteId}/${encodedTitle}?${Math.random()}`;
    } else if (entity instanceof FAttachment) {
        url = `api/attachments/${entity.attachmentId}/image/${encodedTitle}?${entity.utcDateModified}">`;
    }

    $renderedContent // styles needed for the zoom to work well
        .css("display", "flex")
        .css("align-items", "center")
        .css("justify-content", "center");

    const $img = $("")
        .attr("src", url || "")
        .attr("id", "attachment-image-" + idCounter++)
        .css("max-width", "100%");

    $renderedContent.append($img);

    if (options.imageHasZoom) {
        const initZoom = async () => {
            const element = document.querySelector(`#${$img.attr("id")}`);
            if (element) {
                WheelZoom.create(`#${$img.attr("id")}`, {
                    maxScale: 50,
                    speed: 1.3,
                    zoomOnClick: false
                });
            } else {
                requestAnimationFrame(initZoom);
            }
        };
        initZoom();
    }

    imageContextMenuService.setupContextMenu($img);
}

function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: JQuery) {
    let entityType, entityId;

    if (entity instanceof FNote) {
        entityType = "notes";
        entityId = entity.noteId;
    } else if (entity instanceof FAttachment) {
        entityType = "attachments";
        entityId = entity.attachmentId;
    } else {
        throw new Error(`Can't recognize entity type of '${entity}'`);
    }

    const $content = $('
'); if (type === "pdf") { const $pdfPreview = $(''); $pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`)); $content.append($pdfPreview); } else if (type === "audio") { const $audioPreview = $("") .attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`)) .attr("type", entity.mime) .css("width", "100%"); $content.append($audioPreview); } else if (type === "video") { const $videoPreview = $("") .attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`)) .attr("type", entity.mime) .css("width", "100%"); $content.append($videoPreview); } if (entityType === "notes" && "noteId" in entity) { // TODO: we should make this available also for attachments, but there's a problem with "Open externally" support // in attachment list const $downloadButton = $(` `); const $openButton = $(` `); $downloadButton.on("click", () => openService.downloadFileNote(entity.noteId)); $openButton.on("click", () => openService.openNoteExternally(entity.noteId, entity.mime)); // open doesn't work for protected notes since it works through a browser which isn't in protected session $openButton.toggle(!entity.isProtected); $content.append($('