import renderService from "./render.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
import openService from "./open.js";
import utils from "./utils.js";
import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js";
import { applySingleBlockSyntaxHighlight } 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 { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import renderText from "./content_renderer_text.js";
let idCounter = 1;
export interface RenderOptions {
tooltip?: boolean;
trim?: boolean;
imageHasZoom?: boolean;
/** If enabled, it will prevent the default behavior in which an empty note would display a list of children. */
noChildrenList?: boolean;
}
const CODE_MIME_TYPES = new Set(["application/json"]);
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: RenderOptions = {}) {
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, options);
} 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 = $(`
Enter protected session`).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
.css("display", "flex")
.css("flex-direction", "column");
$renderedContent.append(
$("
")
.css("display", "flex")
.css("justify-content", "space-around")
.css("align-items", "center")
.css("height", "100%")
.css("font-size", "500%")
.css("flex-grow", "1")
.append($("
").addClass(entity.getIcon()))
);
if (entity.type === "webView" && entity.hasLabel("webViewSrc")) {
const $footer = $("")
.addClass("webview-footer");
const $openButton = $(`
${t("content_renderer.open_externally")}
`)
.appendTo($footer)
.on("click", () => {
const webViewSrc = entity.getLabelValue("webViewSrc");
if (webViewSrc) {
if (utils.isElectron()) {
const electron = utils.dynamicRequire("electron");
electron.shell.openExternal(webViewSrc);
} else {
window.open(webViewSrc, '_blank', 'noopener,noreferrer');
}
}
});
$footer.appendTo($renderedContent);
}
}
if (entity instanceof FNote) {
$renderedContent.addClass(entity.getCssClass());
}
return {
$renderedContent,
type
};
}
/**
* 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: RenderOptions = {}) {
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 = $(`
${t("file_properties.download")}
`);
const $openButton = $(`
${t("file_properties.open")}
`);
$downloadButton.on("click", (e) => {
e.stopPropagation();
openService.downloadFileNote(entity.noteId)
});
$openButton.on("click", async (e) => {
const iconEl = $openButton.find("> .bx");
iconEl.removeClass("bx bx-link-external");
iconEl.addClass("bx bx-loader spin");
e.stopPropagation();
await openService.openNoteExternally(entity.noteId, entity.mime)
iconEl.removeClass("bx bx-loader spin");
iconEl.addClass("bx bx-link-external");
});
// 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($('