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" ;
2023-12-28 00:02:09 +01:00
import imageContextMenuService from "../menus/image_context_menu.js" ;
2024-11-07 23:53:02 +02:00
import { applySingleBlockSyntaxHighlight , applySyntaxHighlight } from "./syntax_highlight.js" ;
2024-11-25 22:00:19 +02:00
import { loadElkIfNeeded } from "./mermaid.js" ;
2025-01-11 15:21:32 +02:00
import { normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js" ;
2023-05-20 23:46:45 +02:00
let idCounter = 1 ;
2024-12-21 14:56:51 +02:00
interface Options {
tooltip? : boolean ;
trim? : boolean ;
imageHasZoom? : boolean ;
}
2025-01-09 18:07:02 +02:00
const CODE_MIME_TYPES = new Set ( [ "application/json" ] ) ;
2025-01-04 17:18:56 +02:00
2024-12-21 14:56:51 +02:00
async function getRenderedContent ( this : { } | { ctx : string } , entity : FNote , options : Options = { } ) {
2025-01-09 18:07:02 +02:00
options = Object . assign (
{
tooltip : false
} ,
options
) ;
2023-05-20 23:46:45 +02:00
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
2025-01-09 18:07:02 +02:00
if ( type === "text" ) {
2023-07-25 22:27:15 +02:00
await renderText ( entity , $renderedContent ) ;
2025-01-09 18:07:02 +02:00
} else if ( type === "code" ) {
2023-08-10 13:50:25 +02:00
await renderCode ( entity , $renderedContent ) ;
2025-01-09 18:07:02 +02:00
} else if ( [ "image" , "canvas" , "mindMap" ] . includes ( type ) ) {
2023-07-14 21:06:15 +02:00
renderImage ( entity , $renderedContent , options ) ;
2025-01-09 18:07:02 +02:00
} else if ( ! options . tooltip && [ "file" , "pdf" , "audio" , "video" ] . includes ( type ) ) {
2023-05-20 23:46:45 +02:00
renderFile ( entity , type , $renderedContent ) ;
2025-01-09 18:07:02 +02:00
} else if ( type === "mermaid" ) {
2023-05-20 23:46:45 +02:00
await renderMermaid ( entity , $renderedContent ) ;
2025-01-09 18:07:02 +02:00
} else if ( type === "render" ) {
const $content = $ ( "<div>" ) ;
2023-05-20 23:46:45 +02:00
2024-12-21 14:56:51 +02:00
await renderService . render ( entity , $content ) ;
2023-05-20 23:46:45 +02:00
$renderedContent . append ( $content ) ;
2025-01-09 18:07:02 +02:00
} 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 ) ;
2023-05-20 23:46:45 +02:00
2025-01-09 18:07:02 +02:00
$renderedContent . append ( $ ( "<div>" ) . append ( "<div>This note is protected and to access it you need to enter password.</div>" ) . append ( "<br/>" ) . append ( $button ) ) ;
} else if ( entity instanceof FNote ) {
2023-06-05 00:12:08 +02:00
$renderedContent . append (
$ ( "<div>" )
2023-06-22 23:34:05 +02:00
. 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
} ;
}
2024-12-21 14:56:51 +02:00
async function renderText ( note : FNote , $renderedContent : JQuery < HTMLElement > ) {
2023-05-20 23:46:45 +02:00
// entity must be FNote
2023-07-25 22:27:15 +02:00
const blob = await note . getBlob ( ) ;
2023-05-20 23:46:45 +02:00
2024-12-21 14:56:51 +02:00
if ( blob && ! utils . isHtmlEmpty ( blob . content ) ) {
2023-07-25 22:27:15 +02:00
$renderedContent . append ( $ ( '<div class="ck-content">' ) . html ( blob . content ) ) ;
2023-05-20 23:46:45 +02:00
2025-01-09 18:07:02 +02:00
if ( $renderedContent . find ( "span.math-tex" ) . length > 0 ) {
2023-05-20 23:46:45 +02:00
await libraryLoader . requireLibrary ( libraryLoader . KATEX ) ;
2025-01-09 18:07:02 +02:00
renderMathInElement ( $renderedContent [ 0 ] , { trust : true } ) ;
2023-05-20 23:46:45 +02:00
}
2025-01-09 18:07:02 +02:00
const getNoteIdFromLink = ( el : HTMLElement ) = > treeService . getNoteIdFromUrl ( $ ( el ) . attr ( "href" ) || "" ) ;
2023-05-20 23:46:45 +02:00
const referenceLinks = $renderedContent . find ( "a.reference-link" ) ;
2024-12-21 14:56:51 +02:00
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
}
2024-10-31 22:18:00 +02:00
2024-10-31 22:47:34 +02:00
await applySyntaxHighlight ( $renderedContent ) ;
2023-05-20 23:46:45 +02:00
} else {
await renderChildrenList ( $renderedContent , note ) ;
}
}
2024-11-07 23:53:02 +02:00
/ * *
* Renders a code note , by displaying its content and applying syntax highlighting based on the selected MIME type .
* /
2024-12-21 14:56:51 +02:00
async function renderCode ( note : FNote , $renderedContent : JQuery < HTMLElement > ) {
2023-07-25 22:27:15 +02:00
const blob = await note . getBlob ( ) ;
2023-05-20 23:46:45 +02:00
2024-11-07 23:58:10 +02:00
const $codeBlock = $ ( "<code>" ) ;
2024-12-21 14:56:51 +02:00
$codeBlock . text ( blob ? . content || "" ) ;
2024-11-07 23:58:10 +02:00
$renderedContent . append ( $ ( "<pre>" ) . append ( $codeBlock ) ) ;
2025-01-11 15:21:32 +02:00
await applySingleBlockSyntaxHighlight ( $codeBlock , normalizeMimeTypeForCKEditor ( note . mime ) ) ;
2023-05-20 23:46:45 +02:00
}
2024-12-21 14:56:51 +02:00
function renderImage ( entity : FNote | FAttachment , $renderedContent : JQuery < HTMLElement > , options : Options = { } ) {
2023-11-27 10:10:27 +01:00
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 ) {
2023-11-27 10:10:27 +01:00
url = ` api/images/ ${ entity . noteId } / ${ encodedTitle } ? ${ Math . random ( ) } ` ;
2023-05-21 18:14:17 +02:00
} else if ( entity instanceof FAttachment ) {
2023-11-27 10:10:27 +01:00
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
2025-01-09 18:07:02 +02:00
. css ( "display" , "flex" )
. css ( "align-items" , "center" )
. css ( "justify-content" , "center" ) ;
2023-07-14 21:06:15 +02:00
const $img = $ ( "<img>" )
2024-12-21 14:56:51 +02:00
. 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
} ) ;
} ) ;
}
2023-12-28 00:02:09 +01:00
imageContextMenuService . setupContextMenu ( $img ) ;
2023-05-20 23:46:45 +02:00
}
2024-12-21 14:56:51 +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 ) {
2025-01-09 18:07:02 +02:00
entityType = "notes" ;
2023-05-20 23:46:45 +02:00
entityId = entity . noteId ;
} else if ( entity instanceof FAttachment ) {
2025-01-09 18:07:02 +02:00
entityType = "attachments" ;
2023-05-20 23:46:45 +02:00
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%;">' ) ;
2025-01-09 18:07:02 +02:00
if ( type === "pdf" ) {
2023-05-20 23:46:45 +02:00
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 ) ;
2025-01-09 18:07:02 +02:00
} 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 ) ;
2025-01-09 18:07:02 +02:00
} else if ( type === "video" ) {
const $videoPreview = $ ( "<video controls></video>" )
2023-05-20 23:46:45 +02:00
. attr ( "src" , openService . getUrlForDownload ( ` api/ ${ entityType } / ${ entityId } /open-partial ` ) )
. attr ( "type" , entity . mime )
. css ( "width" , "100%" ) ;
$content . append ( $videoPreview ) ;
}
2025-01-09 18:07:02 +02:00
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>' ) ;
2025-01-09 18:07:02 +02:00
$downloadButton . on ( "click" , ( ) = > openService . downloadFileNote ( entity . noteId ) ) ;
$openButton . on ( "click" , ( ) = > openService . openNoteExternally ( entity . noteId , entity . mime ) ) ;
2023-05-29 00:19:54 +02:00
// open doesn't work for protected notes since it works through a browser which isn't in protected session
$openButton . toggle ( ! entity . isProtected ) ;
2025-01-09 18:07:02 +02:00
$content . append ( $ ( '<div style="display: flex; justify-content: space-evenly; margin-top: 5px;">' ) . append ( $downloadButton ) . append ( $openButton ) ) ;
2023-05-29 00:19:54 +02:00
}
2023-05-20 23:46:45 +02:00
$renderedContent . append ( $content ) ;
}
2024-12-21 14:56:51 +02:00
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 ( ) ;
2024-12-21 14:56:51 +02:00
const content = blob ? . content || "" ;
2023-05-20 23:46:45 +02:00
2025-01-09 18:07:02 +02:00
$renderedContent . css ( "display" , "flex" ) . css ( "justify-content" , "space-around" ) ;
2023-05-20 23:46:45 +02:00
const documentStyle = window . getComputedStyle ( document . documentElement ) ;
2025-01-09 18:07:02 +02:00
const mermaidTheme = documentStyle . getPropertyValue ( "--mermaid-theme" ) ;
2023-05-20 23:46:45 +02:00
2025-01-09 18:07:02 +02:00
mermaid . mermaidAPI . initialize ( { startOnLoad : false , theme : mermaidTheme.trim ( ) , securityLevel : "antiscript" } ) ;
2023-05-20 23:46:45 +02:00
try {
2024-11-25 22:00:19 +02:00
await loadElkIfNeeded ( content ) ;
2025-01-09 18:07:02 +02:00
const { svg } = await mermaid . mermaidAPI . render ( "in-mermaid-graph-" + idCounter ++ , content ) ;
2023-07-14 20:03:28 +02:00
$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 > }
* /
2025-02-07 21:19:58 +02:00
async function renderChildrenList ( $renderedContent : JQuery < HTMLElement > , note : FNote ) {
let childNoteIds = note . getChildNoteIds ( ) ;
if ( ! childNoteIds . length ) {
return ;
}
2023-05-20 23:46:45 +02:00
$renderedContent . css ( "padding" , "10px" ) ;
$renderedContent . addClass ( "text-with-ellipsis" ) ;
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 ) {
2025-01-09 18:07:02 +02:00
$renderedContent . append (
await linkService . createLink ( ` ${ note . noteId } / ${ childNote . noteId } ` , {
showTooltip : false ,
showNoteIcon : true
} )
) ;
2023-05-20 23:46:45 +02:00
$renderedContent . append ( "<br>" ) ;
}
}
2024-12-21 14:56:51 +02:00
function getRenderingType ( entity : FNote | FAttachment ) {
let type : string = "" ;
if ( "type" in entity ) {
type = entity . type ;
} else if ( "role" in entity ) {
type = entity . role ;
}
2025-01-09 18:07:02 +02:00
const mime = "mime" in entity && entity . mime ;
2023-05-20 23:46:45 +02:00
2025-01-09 18:07:02 +02:00
if ( type === "file" && mime === "application/pdf" ) {
type = "pdf" ;
} else if ( type === "file" && mime && CODE_MIME_TYPES . has ( mime ) ) {
2025-01-04 17:18:56 +02:00
type = "code" ;
2025-01-09 18:07:02 +02:00
} else if ( type === "file" && mime && mime . startsWith ( "audio/" ) ) {
type = "audio" ;
} else if ( type === "file" && mime && mime . startsWith ( "video/" ) ) {
type = "video" ;
2023-05-20 23:46:45 +02:00
}
if ( entity . isProtected ) {
if ( protectedSessionHolder . isProtectedSessionAvailable ( ) ) {
protectedSessionHolder . touchProtectedSession ( ) ;
2025-01-09 18:07:02 +02:00
} else {
type = "protectedSession" ;
2023-05-20 23:46:45 +02:00
}
}
return type ;
}
export default {
getRenderedContent
} ;