mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 21:19:01 +01:00 
			
		
		
		
	refactor(share): extract note rendering logic
This commit is contained in:
		
							parent
							
								
									26afab03ce
								
							
						
					
					
						commit
						2c6ba9ba2c
					
				@ -1,10 +1,18 @@
 | 
				
			|||||||
import { JSDOM } from "jsdom";
 | 
					import { JSDOM } from "jsdom";
 | 
				
			||||||
import shaca from "./shaca/shaca.js";
 | 
					import shaca from "./shaca/shaca.js";
 | 
				
			||||||
import assetPath from "../services/asset_path.js";
 | 
					import assetPath, { assetUrlFragment } from "../services/asset_path.js";
 | 
				
			||||||
import shareRoot from "./share_root.js";
 | 
					import shareRoot from "./share_root.js";
 | 
				
			||||||
import escapeHtml from "escape-html";
 | 
					import escapeHtml from "escape-html";
 | 
				
			||||||
import type SNote from "./shaca/entities/snote.js";
 | 
					import type SNote from "./shaca/entities/snote.js";
 | 
				
			||||||
import { t } from "i18next";
 | 
					import { t } from "i18next";
 | 
				
			||||||
 | 
					import SBranch from "./shaca/entities/sbranch.js";
 | 
				
			||||||
 | 
					import options from "../services/options.js";
 | 
				
			||||||
 | 
					import { getResourceDir, isDev, safeExtractMessageAndStackFromError } from "../services/utils.js";
 | 
				
			||||||
 | 
					import app_path from "../services/app_path.js";
 | 
				
			||||||
 | 
					import ejs from "ejs";
 | 
				
			||||||
 | 
					import log from "../services/log.js";
 | 
				
			||||||
 | 
					import { join } from "path";
 | 
				
			||||||
 | 
					import { readFileSync } from "fs";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Represents the output of the content renderer.
 | 
					 * Represents the output of the content renderer.
 | 
				
			||||||
@ -16,6 +24,87 @@ export interface Result {
 | 
				
			|||||||
    isEmpty?: boolean;
 | 
					    isEmpty?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } {
 | 
				
			||||||
 | 
					    if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
 | 
				
			||||||
 | 
					        // share root itself is not shared
 | 
				
			||||||
 | 
					        return {};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // every path leads to share root, but which one to choose?
 | 
				
			||||||
 | 
					    // for the sake of simplicity, URLs are not note paths
 | 
				
			||||||
 | 
					    const parentBranch = note.getParentBranches()[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (parentBranch.parentNoteId === shareRoot.SHARE_ROOT_NOTE_ID) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            note,
 | 
				
			||||||
 | 
					            branch: parentBranch
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return getSharedSubTreeRoot(parentBranch.getParentNote());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function renderNoteContent(note: SNote) {
 | 
				
			||||||
 | 
					    const { header, content, isEmpty } = getContent(note);
 | 
				
			||||||
 | 
					    const subRoot = getSharedSubTreeRoot(note);
 | 
				
			||||||
 | 
					    const showLoginInShareTheme = options.getOption("showLoginInShareTheme");
 | 
				
			||||||
 | 
					    const opts = {
 | 
				
			||||||
 | 
					        note,
 | 
				
			||||||
 | 
					        header,
 | 
				
			||||||
 | 
					        content,
 | 
				
			||||||
 | 
					        isEmpty,
 | 
				
			||||||
 | 
					        subRoot,
 | 
				
			||||||
 | 
					        assetPath: isDev ? assetPath : `../${assetPath}`,
 | 
				
			||||||
 | 
					        assetUrlFragment,
 | 
				
			||||||
 | 
					        appPath: isDev ? app_path : `../${app_path}`,
 | 
				
			||||||
 | 
					        showLoginInShareTheme,
 | 
				
			||||||
 | 
					        t,
 | 
				
			||||||
 | 
					        isDev
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check if the user has their own template.
 | 
				
			||||||
 | 
					    if (note.hasRelation("shareTemplate")) {
 | 
				
			||||||
 | 
					        // Get the template note and content
 | 
				
			||||||
 | 
					        const templateId = note.getRelation("shareTemplate")?.value;
 | 
				
			||||||
 | 
					        const templateNote = templateId && shaca.getNote(templateId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Make sure the note type is correct
 | 
				
			||||||
 | 
					        if (templateNote && templateNote.type === "code" && templateNote.mime === "application/x-ejs") {
 | 
				
			||||||
 | 
					            // EJS caches the result of this so we don't need to pre-cache
 | 
				
			||||||
 | 
					            const includer = (path: string) => {
 | 
				
			||||||
 | 
					                const childNote = templateNote.children.find((n) => path === n.title);
 | 
				
			||||||
 | 
					                if (!childNote) throw new Error(`Unable to find child note: ${path}.`);
 | 
				
			||||||
 | 
					                if (childNote.type !== "code" || childNote.mime !== "application/x-ejs") throw new Error("Incorrect child note type.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const template = childNote.getContent();
 | 
				
			||||||
 | 
					                if (typeof template !== "string") throw new Error("Invalid template content type.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return { template };
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Try to render user's template, w/ fallback to default view
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                const content = templateNote.getContent();
 | 
				
			||||||
 | 
					                if (typeof content === "string") {
 | 
				
			||||||
 | 
					                    return ejs.render(content, opts, { includer });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (e: unknown) {
 | 
				
			||||||
 | 
					                const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
 | 
				
			||||||
 | 
					                log.error(`Rendering user provided share template (${templateId}) threw exception ${errMessage} with stacktrace: ${errStack}`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Render with the default view otherwise.
 | 
				
			||||||
 | 
					    const templatePath = join(getResourceDir(), "share-theme", "templates", "page.ejs");
 | 
				
			||||||
 | 
					    return ejs.render(readFileSync(templatePath, "utf-8"), opts, {
 | 
				
			||||||
 | 
					        includer: (path) => {
 | 
				
			||||||
 | 
					            const templatePath = join(getResourceDir(), "share-theme", "templates", `${path}.ejs`);
 | 
				
			||||||
 | 
					            return { filename: templatePath }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getContent(note: SNote) {
 | 
					function getContent(note: SNote) {
 | 
				
			||||||
    if (note.isProtected) {
 | 
					    if (note.isProtected) {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
 | 
				
			|||||||
@ -4,40 +4,12 @@ import type { Request, Response, Router } from "express";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import shaca from "./shaca/shaca.js";
 | 
					import shaca from "./shaca/shaca.js";
 | 
				
			||||||
import shacaLoader from "./shaca/shaca_loader.js";
 | 
					import shacaLoader from "./shaca/shaca_loader.js";
 | 
				
			||||||
import shareRoot from "./share_root.js";
 | 
					 | 
				
			||||||
import contentRenderer from "./content_renderer.js";
 | 
					 | 
				
			||||||
import assetPath, { assetUrlFragment } from "../services/asset_path.js";
 | 
					 | 
				
			||||||
import appPath from "../services/app_path.js";
 | 
					 | 
				
			||||||
import searchService from "../services/search/services/search.js";
 | 
					import searchService from "../services/search/services/search.js";
 | 
				
			||||||
import SearchContext from "../services/search/search_context.js";
 | 
					import SearchContext from "../services/search/search_context.js";
 | 
				
			||||||
import log from "../services/log.js";
 | 
					 | 
				
			||||||
import type SNote from "./shaca/entities/snote.js";
 | 
					import type SNote from "./shaca/entities/snote.js";
 | 
				
			||||||
import type SBranch from "./shaca/entities/sbranch.js";
 | 
					 | 
				
			||||||
import type SAttachment from "./shaca/entities/sattachment.js";
 | 
					import type SAttachment from "./shaca/entities/sattachment.js";
 | 
				
			||||||
import utils, { isDev, safeExtractMessageAndStackFromError } from "../services/utils.js";
 | 
					import utils from "../services/utils.js";
 | 
				
			||||||
import options from "../services/options.js";
 | 
					import { renderNoteContent } from "./content_renderer.js";
 | 
				
			||||||
import { t } from "i18next";
 | 
					 | 
				
			||||||
import ejs from "ejs";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } {
 | 
					 | 
				
			||||||
    if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
 | 
					 | 
				
			||||||
        // share root itself is not shared
 | 
					 | 
				
			||||||
        return {};
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // every path leads to share root, but which one to choose?
 | 
					 | 
				
			||||||
    // for the sake of simplicity, URLs are not note paths
 | 
					 | 
				
			||||||
    const parentBranch = note.getParentBranches()[0];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (parentBranch.parentNoteId === shareRoot.SHARE_ROOT_NOTE_ID) {
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            note,
 | 
					 | 
				
			||||||
            branch: parentBranch
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return getSharedSubTreeRoot(parentBranch.getParentNote());
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function addNoIndexHeader(note: SNote, res: Response) {
 | 
					function addNoIndexHeader(note: SNote, res: Response) {
 | 
				
			||||||
    if (note.isLabelTruthy("shareDisallowRobotIndexing")) {
 | 
					    if (note.isLabelTruthy("shareDisallowRobotIndexing")) {
 | 
				
			||||||
@ -108,8 +80,7 @@ function renderImageAttachment(image: SNote, res: Response, attachmentName: stri
 | 
				
			|||||||
    let svgString = "<svg/>";
 | 
					    let svgString = "<svg/>";
 | 
				
			||||||
    const attachment = image.getAttachmentByTitle(attachmentName);
 | 
					    const attachment = image.getAttachmentByTitle(attachmentName);
 | 
				
			||||||
    if (!attachment) {
 | 
					    if (!attachment) {
 | 
				
			||||||
        res.status(404);
 | 
					
 | 
				
			||||||
        renderDefault(res, "404");
 | 
					 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const content = attachment.getContent();
 | 
					    const content = attachment.getContent();
 | 
				
			||||||
@ -137,12 +108,19 @@ function renderImageAttachment(image: SNote, res: Response, attachmentName: stri
 | 
				
			|||||||
    res.send(svg);
 | 
					    res.send(svg);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function render404(res: Response) {
 | 
				
			||||||
 | 
					    res.status(404);
 | 
				
			||||||
 | 
					    const shareThemePath = `../../share-theme/templates/404.ejs`;
 | 
				
			||||||
 | 
					    res.render(shareThemePath);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function register(router: Router) {
 | 
					function register(router: Router) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function renderNote(note: SNote, req: Request, res: Response) {
 | 
					    function renderNote(note: SNote, req: Request, res: Response) {
 | 
				
			||||||
        if (!note) {
 | 
					        if (!note) {
 | 
				
			||||||
            console.log("Unable to find note ", note);
 | 
					            console.log("Unable to find note ", note);
 | 
				
			||||||
            res.status(404);
 | 
					            res.status(404);
 | 
				
			||||||
            renderDefault(res, "404");
 | 
					            render404(res);
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -159,63 +137,7 @@ function register(router: Router) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        res.send(renderNoteContent(note));
 | 
				
			||||||
        const { header, content, isEmpty } = contentRenderer.getContent(note);
 | 
					 | 
				
			||||||
        const subRoot = getSharedSubTreeRoot(note);
 | 
					 | 
				
			||||||
        const showLoginInShareTheme = options.getOption("showLoginInShareTheme");
 | 
					 | 
				
			||||||
        const opts = {
 | 
					 | 
				
			||||||
            note,
 | 
					 | 
				
			||||||
            header,
 | 
					 | 
				
			||||||
            content,
 | 
					 | 
				
			||||||
            isEmpty,
 | 
					 | 
				
			||||||
            subRoot,
 | 
					 | 
				
			||||||
            assetPath: isDev ? assetPath : `../${assetPath}`,
 | 
					 | 
				
			||||||
            assetUrlFragment,
 | 
					 | 
				
			||||||
            appPath: isDev ? appPath : `../${appPath}`,
 | 
					 | 
				
			||||||
            showLoginInShareTheme,
 | 
					 | 
				
			||||||
            t,
 | 
					 | 
				
			||||||
            isDev
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        let useDefaultView = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Check if the user has their own template
 | 
					 | 
				
			||||||
        if (note.hasRelation("shareTemplate")) {
 | 
					 | 
				
			||||||
            // Get the template note and content
 | 
					 | 
				
			||||||
            const templateId = note.getRelation("shareTemplate")?.value;
 | 
					 | 
				
			||||||
            const templateNote = templateId && shaca.getNote(templateId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Make sure the note type is correct
 | 
					 | 
				
			||||||
            if (templateNote && templateNote.type === "code" && templateNote.mime === "application/x-ejs") {
 | 
					 | 
				
			||||||
                // EJS caches the result of this so we don't need to pre-cache
 | 
					 | 
				
			||||||
                const includer = (path: string) => {
 | 
					 | 
				
			||||||
                    const childNote = templateNote.children.find((n) => path === n.title);
 | 
					 | 
				
			||||||
                    if (!childNote) throw new Error(`Unable to find child note: ${path}.`);
 | 
					 | 
				
			||||||
                    if (childNote.type !== "code" || childNote.mime !== "application/x-ejs") throw new Error("Incorrect child note type.");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    const template = childNote.getContent();
 | 
					 | 
				
			||||||
                    if (typeof template !== "string") throw new Error("Invalid template content type.");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return { template };
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // Try to render user's template, w/ fallback to default view
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    const content = templateNote.getContent();
 | 
					 | 
				
			||||||
                    if (typeof content === "string") {
 | 
					 | 
				
			||||||
                        const ejsResult = ejs.render(content, opts, { includer });
 | 
					 | 
				
			||||||
                        res.send(ejsResult);
 | 
					 | 
				
			||||||
                        useDefaultView = false; // Rendering went okay, don't use default view
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } catch (e: unknown) {
 | 
					 | 
				
			||||||
                    const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
 | 
					 | 
				
			||||||
                    log.error(`Rendering user provided share template (${templateId}) threw exception ${errMessage} with stacktrace: ${errStack}`);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (useDefaultView) {
 | 
					 | 
				
			||||||
            renderDefault(res, "page", opts);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    router.get("/share/", (req, res) => {
 | 
					    router.get("/share/", (req, res) => {
 | 
				
			||||||
@ -399,12 +321,6 @@ function register(router: Router) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function renderDefault(res: Response<any, Record<string, any>>, template: "page" | "404", opts: any = {}) {
 | 
					 | 
				
			||||||
    // Path is relative to apps/server/dist/assets/views
 | 
					 | 
				
			||||||
    const shareThemePath = `../../share-theme/templates/${template}.ejs`;
 | 
					 | 
				
			||||||
    res.render(shareThemePath, opts);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    register
 | 
					    register
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user