Notes/src/public/app/services/content_renderer.ts

311 lines
10 KiB
TypeScript
Raw Normal View History

2023-05-20 23:46:45 +02:00
import renderService from "./render.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
import libraryLoader from "./library_loader.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, applySyntaxHighlight } from "./syntax_highlight.js";
import mime_types from "./mime_types.js";
2024-11-25 22:00:19 +02:00
import { loadElkIfNeeded } from "./mermaid.js";
2023-05-20 23:46:45 +02:00
let idCounter = 1;
interface Options {
tooltip?: boolean;
trim?: boolean;
imageHasZoom?: boolean;
}
async function getRenderedContent(this: {} | { ctx: string }, entity: FNote, options: Options = {}) {
2023-05-20 23:46:45 +02:00
options = Object.assign({
tooltip: false
}, options);
const type = getRenderingType(entity);
// attachment supports only image and file/pdf/audio/video
2023-05-21 18:14:17 +02:00
const $renderedContent = $('<div class="rendered-content">');
2023-05-20 23:46:45 +02:00
if (type === 'text') {
await renderText(entity, $renderedContent);
2023-05-20 23:46:45 +02:00
}
else if (type === 'code') {
2023-08-10 13:50:25 +02:00
await renderCode(entity, $renderedContent);
2023-05-20 23:46:45 +02:00
}
else if (['image', 'canvas', 'mindMap'].includes(type)) {
2023-07-14 21:06:15 +02:00
renderImage(entity, $renderedContent, options);
2023-05-20 23:46:45 +02:00
}
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') {
const $content = $('<div>');
await renderService.render(entity, $content);
2023-05-20 23:46:45 +02:00
$renderedContent.append($content);
}
else if (!options.tooltip && type === 'protectedSession') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession);
$renderedContent.append(
$("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>")
.append($button)
);
}
2023-06-05 00:12:08 +02:00
else if (entity instanceof FNote) {
$renderedContent.append(
$("<div>")
.css("display", "flex")
.css("justify-content", "space-around")
.css("align-items", "center")
.css("height", "100%")
2023-06-05 00:12:08 +02:00
.css("font-size", "500%")
.append($("<span>").addClass(entity.getIcon()))
);
2023-05-20 23:46:45 +02:00
}
if (entity instanceof FNote) {
$renderedContent.addClass(entity.getCssClass());
}
return {
$renderedContent,
type
};
}
async function renderText(note: FNote, $renderedContent: JQuery<HTMLElement>) {
2023-05-20 23:46:45 +02:00
// entity must be FNote
const blob = await note.getBlob();
2023-05-20 23:46:45 +02:00
if (blob && !utils.isHtmlEmpty(blob.content)) {
$renderedContent.append($('<div class="ck-content">').html(blob.content));
2023-05-20 23:46:45 +02:00
if ($renderedContent.find('span.math-tex').length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
renderMathInElement($renderedContent[0], {trust: true});
}
const getNoteIdFromLink = (el: HTMLElement) => treeService.getNoteIdFromUrl($(el).attr('href') || "");
2023-05-20 23:46:45 +02:00
const referenceLinks = $renderedContent.find("a.reference-link");
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
2023-05-20 23:46:45 +02:00
await froca.getNotes(noteIdsToPrefetch);
for (const el of referenceLinks) {
2023-05-29 00:19:54 +02:00
await linkService.loadReferenceLinkTitle($(el));
2023-05-20 23:46:45 +02:00
}
await applySyntaxHighlight($renderedContent);
2023-05-20 23:46:45 +02:00
} else {
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, $renderedContent: JQuery<HTMLElement>) {
const blob = await note.getBlob();
2023-05-20 23:46:45 +02:00
const $codeBlock = $("<code>");
$codeBlock.text(blob?.content || "");
$renderedContent.append($("<pre>").append($codeBlock));
await applySingleBlockSyntaxHighlight($codeBlock, mime_types.normalizeMimeTypeForCKEditor(note.mime));
2023-05-20 23:46:45 +02:00
}
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) {
const encodedTitle = encodeURIComponent(entity.title);
2023-05-20 23:46:45 +02:00
2023-05-21 18:14:17 +02:00
let url;
if (entity instanceof FNote) {
url = `api/images/${entity.noteId}/${encodedTitle}?${Math.random()}`;
2023-05-21 18:14:17 +02:00
} else if (entity instanceof FAttachment) {
url = `api/attachments/${entity.attachmentId}/image/${encodedTitle}?${entity.utcDateModified}">`;
2023-05-21 18:14:17 +02:00
}
2023-07-14 21:06:15 +02:00
$renderedContent // styles needed for the zoom to work well
.css('display', 'flex')
.css('align-items', 'center')
.css('justify-content', 'center');
const $img = $("<img>")
.attr("src", url || "")
2023-07-14 21:06:15 +02:00
.attr("id", "attachment-image-" + idCounter++)
.css("max-width", "100%");
$renderedContent.append($img);
if (options.imageHasZoom) {
libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM).then(() => {
WZoom.create(`#${$img.attr("id")}`, {
2023-07-25 22:33:35 +02:00
maxScale: 50,
speed: 1.3,
2023-07-14 21:06:15 +02:00
zoomOnClick: false
});
});
}
imageContextMenuService.setupContextMenu($img);
2023-05-20 23:46:45 +02:00
}
function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: JQuery<HTMLElement>) {
2023-05-20 23:46:45 +02:00
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 = $('<div style="display: flex; flex-direction: column; height: 100%;">');
if (type === 'pdf') {
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
$pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`));
$content.append($pdfPreview);
} else if (type === 'audio') {
const $audioPreview = $('<audio controls></audio>')
2023-06-29 23:32:19 +02:00
.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`))
2023-05-20 23:46:45 +02:00
.attr("type", entity.mime)
.css("width", "100%");
$content.append($audioPreview);
} else if (type === 'video') {
const $videoPreview = $('<video controls></video>')
.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) {
2023-05-29 00:19:54 +02:00
// TODO: we should make this available also for attachments, but there's a problem with "Open externally" support
// in attachment list
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
$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(
$('<div style="display: flex; justify-content: space-evenly; margin-top: 5px;">')
.append($downloadButton)
.append($openButton)
);
}
2023-05-20 23:46:45 +02:00
$renderedContent.append($content);
}
async function renderMermaid(note: FNote, $renderedContent: JQuery<HTMLElement>) {
2023-05-20 23:46:45 +02:00
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
const blob = await note.getBlob();
const content = blob?.content || "";
2023-05-20 23:46:45 +02:00
$renderedContent
.css("display", "flex")
.css("justify-content", "space-around");
const documentStyle = window.getComputedStyle(document.documentElement);
const mermaidTheme = documentStyle.getPropertyValue('--mermaid-theme');
mermaid.mermaidAPI.initialize({startOnLoad: false, theme: mermaidTheme.trim(), securityLevel: 'antiscript'});
try {
2024-11-25 22:00:19 +02:00
await loadElkIfNeeded(content);
2023-07-14 20:03:28 +02:00
const {svg} = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
$renderedContent.append($(svg));
2023-05-20 23:46:45 +02:00
} catch (e) {
const $error = $("<p>The diagram could not displayed.</p>");
$renderedContent.append($error);
}
}
/**
* @param {jQuery} $renderedContent
* @param {FNote} note
* @returns {Promise<void>}
*/
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
2023-05-20 23:46:45 +02:00
$renderedContent.css("padding", "10px");
$renderedContent.addClass("text-with-ellipsis");
let childNoteIds = note.getChildNoteIds();
if (childNoteIds.length > 10) {
childNoteIds = childNoteIds.slice(0, 10);
}
// just load the first 10 child notes
const childNotes = await froca.getNotes(childNoteIds);
for (const childNote of childNotes) {
2023-05-29 00:19:54 +02:00
$renderedContent.append(await linkService.createLink(`${note.noteId}/${childNote.noteId}`, {
2023-05-20 23:46:45 +02:00
showTooltip: false,
showNoteIcon: true
}));
$renderedContent.append("<br>");
}
}
function getRenderingType(entity: FNote | FAttachment) {
let type: string = "";
if ("type" in entity) {
type = entity.type;
} else if ("role" in entity) {
type = entity.role;
}
const mime = ("mime" in entity && entity.mime);
2023-05-20 23:46:45 +02:00
if (type === 'file' && mime === 'application/pdf') {
type = 'pdf';
} else if (type === 'file' && mime && mime.startsWith('audio/')) {
2023-05-20 23:46:45 +02:00
type = 'audio';
} else if (type === 'file' && mime && mime.startsWith('video/')) {
2023-05-20 23:46:45 +02:00
type = 'video';
}
if (entity.isProtected) {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionHolder.touchProtectedSession();
}
else {
type = 'protectedSession';
}
}
return type;
}
export default {
getRenderedContent
};