refactor(client): split text content rendering to separate module

This commit is contained in:
Elian Doran 2025-11-26 15:17:34 +02:00
parent abbb4e793f
commit c32b6393af
No known key found for this signature in database
5 changed files with 103 additions and 101 deletions

View File

@ -2,8 +2,9 @@ import FNote from "./entities/fnote";
import { render } from "preact"; import { render } from "preact";
import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList"; import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList";
import { useCallback, useLayoutEffect, useRef } from "preact/hooks"; import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
import content_renderer, { applyInlineMermaid } from "./services/content_renderer"; import content_renderer from "./services/content_renderer";
import { dynamicRequire, isElectron } from "./services/utils"; import { dynamicRequire, isElectron } from "./services/utils";
import { applyInlineMermaid } from "./services/content_renderer_text";
interface RendererProps { interface RendererProps {
note: FNote; note: FNote;

View File

@ -2,24 +2,21 @@ import renderService from "./render.js";
import protectedSessionService from "./protected_session.js"; import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js"; import protectedSessionHolder from "./protected_session_holder.js";
import openService from "./open.js"; import openService from "./open.js";
import froca from "./froca.js";
import utils from "./utils.js"; import utils from "./utils.js";
import linkService from "./link.js";
import treeService from "./tree.js";
import FNote from "../entities/fnote.js"; import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js"; import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js"; import imageContextMenuService from "../menus/image_context_menu.js";
import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js"; import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
import { getMermaidConfig, loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js"; import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import renderDoc from "./doc_renderer.js"; import renderDoc from "./doc_renderer.js";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
import WheelZoom from 'vanilla-js-wheel-zoom'; import WheelZoom from 'vanilla-js-wheel-zoom';
import { renderMathInElement } from "./math.js";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons"; import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import renderText from "./content_renderer_text.js";
let idCounter = 1; let idCounter = 1;
interface Options { export interface RenderOptions {
tooltip?: boolean; tooltip?: boolean;
trim?: boolean; trim?: boolean;
imageHasZoom?: boolean; imageHasZoom?: boolean;
@ -29,7 +26,7 @@ interface Options {
const CODE_MIME_TYPES = new Set(["application/json"]); const CODE_MIME_TYPES = new Set(["application/json"]);
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) { export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: RenderOptions = {}) {
options = Object.assign( options = Object.assign(
{ {
@ -116,33 +113,6 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
}; };
} }
async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) {
// entity must be FNote
const blob = await note.getBlob();
if (blob && !utils.isHtmlEmpty(blob.content)) {
$renderedContent.append($('<div class="ck-content">').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 rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote && !options.noChildrenList) {
await renderChildrenList($renderedContent, note);
}
}
/** /**
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type. * Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
*/ */
@ -164,7 +134,7 @@ async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HT
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime)); await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
} }
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) { function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
const encodedTitle = encodeURIComponent(entity.title); const encodedTitle = encodeURIComponent(entity.title);
let url; let url;
@ -306,40 +276,6 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery
} }
} }
/**
* @param {jQuery} $renderedContent
* @param {FNote} note
* @returns {Promise<void>}
*/
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
let childNoteIds = note.getChildNoteIds();
if (!childNoteIds.length) {
return;
}
$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) {
$renderedContent.append(
await linkService.createLink(`${note.noteId}/${childNote.noteId}`, {
showTooltip: false,
showNoteIcon: true
})
);
$renderedContent.append("<br>");
}
}
function getRenderingType(entity: FNote | FAttachment) { function getRenderingType(entity: FNote | FAttachment) {
let type: string = ""; let type: string = "";
if ("type" in entity) { if ("type" in entity) {
@ -371,34 +307,6 @@ function getRenderingType(entity: FNote | FAttachment) {
return type; return type;
} }
/** Rewrite the code block from <pre><code> to <div> in order not to apply a codeblock style to it. */
export async function rewriteMermaidDiagramsInContainer(container: HTMLDivElement) {
const mermaidBlocks = container.querySelectorAll('pre:has(code[class="language-mermaid"])');
if (!mermaidBlocks.length) return;
const nodes: HTMLElement[] = [];
for (const mermaidBlock of mermaidBlocks) {
const div = document.createElement("div");
div.classList.add("mermaid-diagram");
div.innerHTML = mermaidBlock.querySelector("code")?.innerHTML ?? "";
mermaidBlock.replaceWith(div);
nodes.push(div);
}
}
export async function applyInlineMermaid(container: HTMLDivElement) {
// Initialize mermaid
const mermaid = (await import("mermaid")).default;
mermaid.initialize(getMermaidConfig());
const nodes = Array.from(container.querySelectorAll<HTMLElement>("div.mermaid-diagram"));
console.log("Got nodes", nodes);
try {
await mermaid.run({ nodes });
} catch (e) {
console.log(e);
}
}
export default { export default {
getRenderedContent getRenderedContent
}; };

View File

@ -0,0 +1,93 @@
import { formatCodeBlocks } from "./syntax_highlight.js";
import { getMermaidConfig } from "./mermaid.js";
import { renderMathInElement } from "./math.js";
import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js";
import tree from "./tree.js";
import froca from "./froca.js";
import link from "./link.js";
import { isHtmlEmpty } from "./utils.js";
import type { RenderOptions } from "./content_renderer.js";
export default async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
// entity must be FNote
const blob = await note.getBlob();
if (blob && !isHtmlEmpty(blob.content)) {
$renderedContent.append($('<div class="ck-content">').html(blob.content));
if ($renderedContent.find("span.math-tex").length > 0) {
renderMathInElement($renderedContent[0], { trust: true });
}
const getNoteIdFromLink = (el: HTMLElement) => tree.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 link.loadReferenceLinkTitle($(el));
}
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote && !options.noChildrenList) {
await renderChildrenList($renderedContent, note);
}
}
/** Rewrite the code block from <pre><code> to <div> in order not to apply a codeblock style to it. */
export async function rewriteMermaidDiagramsInContainer(container: HTMLDivElement) {
const mermaidBlocks = container.querySelectorAll('pre:has(code[class="language-mermaid"])');
if (!mermaidBlocks.length) return;
const nodes: HTMLElement[] = [];
for (const mermaidBlock of mermaidBlocks) {
const div = document.createElement("div");
div.classList.add("mermaid-diagram");
div.innerHTML = mermaidBlock.querySelector("code")?.innerHTML ?? "";
mermaidBlock.replaceWith(div);
nodes.push(div);
}
}
export async function applyInlineMermaid(container: HTMLDivElement) {
// Initialize mermaid
const mermaid = (await import("mermaid")).default;
mermaid.initialize(getMermaidConfig());
const nodes = Array.from(container.querySelectorAll<HTMLElement>("div.mermaid-diagram"));
try {
await mermaid.run({ nodes });
} catch (e) {
console.log(e);
}
}
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
let childNoteIds = note.getChildNoteIds();
if (!childNoteIds.length) {
return;
}
$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) {
$renderedContent.append(
await link.createLink(`${note.noteId}/${childNote.noteId}`, {
showTooltip: false,
showNoteIcon: true
})
);
$renderedContent.append("<br>");
}
}

View File

@ -274,7 +274,7 @@ function getMimeTypeClass(mime: string) {
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`; return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
} }
function isHtmlEmpty(html: string) { export function isHtmlEmpty(html: string) {
if (!html) { if (!html) {
return true; return true;
} else if (typeof html !== "string") { } else if (typeof html !== "string") {

View File

@ -16,7 +16,7 @@ import { formatCodeBlocks } from "../../../services/syntax_highlight";
import TouchBar, { TouchBarButton, TouchBarSpacer } from "../../react/TouchBar"; import TouchBar, { TouchBarButton, TouchBarSpacer } from "../../react/TouchBar";
import appContext from "../../../components/app_context"; import appContext from "../../../components/app_context";
import { applyReferenceLinks } from "./read_only_helper"; import { applyReferenceLinks } from "./read_only_helper";
import { applyInlineMermaid, rewriteMermaidDiagramsInContainer } from "../../../services/content_renderer"; import { applyInlineMermaid, rewriteMermaidDiagramsInContainer } from "../../../services/content_renderer_text";
import clsx from "clsx"; import clsx from "clsx";
export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetProps) { export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetProps) {