mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	refactor(export/zip): extract into separate provider
This commit is contained in:
		
							parent
							
								
									c5196721d4
								
							
						
					
					
						commit
						dfd575b6eb
					
				@ -2,11 +2,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import html from "html";
 | 
					import html from "html";
 | 
				
			||||||
import dateUtils from "../date_utils.js";
 | 
					import dateUtils from "../date_utils.js";
 | 
				
			||||||
import path, { join } from "path";
 | 
					import path from "path";
 | 
				
			||||||
import mimeTypes from "mime-types";
 | 
					import mimeTypes from "mime-types";
 | 
				
			||||||
import mdService from "./markdown.js";
 | 
					import mdService from "./markdown.js";
 | 
				
			||||||
import packageInfo from "../../../package.json" with { type: "json" };
 | 
					import packageInfo from "../../../package.json" with { type: "json" };
 | 
				
			||||||
import { getContentDisposition, escapeHtml, getResourceDir, isDev } from "../utils.js";
 | 
					import { getContentDisposition, escapeHtml } from "../utils.js";
 | 
				
			||||||
import protectedSessionService from "../protected_session.js";
 | 
					import protectedSessionService from "../protected_session.js";
 | 
				
			||||||
import sanitize from "sanitize-filename";
 | 
					import sanitize from "sanitize-filename";
 | 
				
			||||||
import fs from "fs";
 | 
					import fs from "fs";
 | 
				
			||||||
@ -19,12 +19,10 @@ import type NoteMeta from "../meta/note_meta.js";
 | 
				
			|||||||
import type AttachmentMeta from "../meta/attachment_meta.js";
 | 
					import type AttachmentMeta from "../meta/attachment_meta.js";
 | 
				
			||||||
import type AttributeMeta from "../meta/attribute_meta.js";
 | 
					import type AttributeMeta from "../meta/attribute_meta.js";
 | 
				
			||||||
import type BBranch from "../../becca/entities/bbranch.js";
 | 
					import type BBranch from "../../becca/entities/bbranch.js";
 | 
				
			||||||
import type BNote from "../../becca/entities/bnote.js";
 | 
					 | 
				
			||||||
import type { Response } from "express";
 | 
					import type { Response } from "express";
 | 
				
			||||||
import type { NoteMetaFile } from "../meta/note_meta.js";
 | 
					import type { NoteMetaFile } from "../meta/note_meta.js";
 | 
				
			||||||
//import cssContent from "@triliumnext/ckeditor5/content.css";
 | 
					import HtmlExportProvider from "./zip/html.js";
 | 
				
			||||||
import { renderNoteForExport } from "../../share/content_renderer.js";
 | 
					import { ZipExportProvider } from "./zip/abstract_provider.js";
 | 
				
			||||||
import { RESOURCE_DIR } from "../resource_dir.js";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string;
 | 
					type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -317,7 +315,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function prepareContent(note: BNote | undefined, title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer {
 | 
					    function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer {
 | 
				
			||||||
        if (["html", "markdown"].includes(noteMeta?.format || "")) {
 | 
					        if (["html", "markdown"].includes(noteMeta?.format || "")) {
 | 
				
			||||||
            content = content.toString();
 | 
					            content = content.toString();
 | 
				
			||||||
            content = rewriteFn(content, noteMeta);
 | 
					            content = rewriteFn(content, noteMeta);
 | 
				
			||||||
@ -329,18 +327,11 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
 | 
				
			|||||||
                    throw new Error("Missing note path.");
 | 
					                    throw new Error("Missing note path.");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const basePath = "../".repeat(noteMeta.notePath.length - 1);
 | 
					                const cssUrl = `${"../".repeat(noteMeta.notePath.length - 1)}style.css`;
 | 
				
			||||||
                const htmlTitle = escapeHtml(title);
 | 
					                const htmlTitle = escapeHtml(title);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (note) {
 | 
					                // <base> element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809
 | 
				
			||||||
                    content = renderNoteForExport(note, branch, basePath);
 | 
					                content = `<html>
 | 
				
			||||||
 | 
					 | 
				
			||||||
                    // TODO: Fix double rewrite.
 | 
					 | 
				
			||||||
                    content = rewriteFn(content, noteMeta);
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    const cssUrl = basePath + "style.css";
 | 
					 | 
				
			||||||
                    // <base> element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809
 | 
					 | 
				
			||||||
                    content = `<html>
 | 
					 | 
				
			||||||
<head>
 | 
					<head>
 | 
				
			||||||
    <meta charset="utf-8">
 | 
					    <meta charset="utf-8">
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
@ -356,7 +347,6 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
</html>`;
 | 
					</html>`;
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content;
 | 
					            return content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content;
 | 
				
			||||||
@ -386,7 +376,7 @@ ${markdownContent}`;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            let content: string | Buffer = `<p>This is a clone of a note. Go to its <a href="${targetUrl}">primary location</a>.</p>`;
 | 
					            let content: string | Buffer = `<p>This is a clone of a note. Go to its <a href="${targetUrl}">primary location</a>.</p>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            content = prepareContent(undefined, noteMeta.title, content, noteMeta);
 | 
					            content = prepareContent(noteMeta.title, content, noteMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            archive.append(content, { name: filePathPrefix + noteMeta.dataFileName });
 | 
					            archive.append(content, { name: filePathPrefix + noteMeta.dataFileName });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -402,7 +392,7 @@ ${markdownContent}`;
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (noteMeta.dataFileName) {
 | 
					        if (noteMeta.dataFileName) {
 | 
				
			||||||
            const content = prepareContent(note, noteMeta.title, note.getContent(), noteMeta);
 | 
					            const content = prepareContent(noteMeta.title, note.getContent(), noteMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            archive.append(content, {
 | 
					            archive.append(content, {
 | 
				
			||||||
                name: filePathPrefix + noteMeta.dataFileName,
 | 
					                name: filePathPrefix + noteMeta.dataFileName,
 | 
				
			||||||
@ -438,97 +428,6 @@ ${markdownContent}`;
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function saveNavigation(rootMeta: NoteMeta, navigationMeta: NoteMeta) {
 | 
					 | 
				
			||||||
        if (!navigationMeta.dataFileName) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        function saveNavigationInner(meta: NoteMeta) {
 | 
					 | 
				
			||||||
            let html = "<li>";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (meta.dataFileName && meta.noteId) {
 | 
					 | 
				
			||||||
                const targetUrl = getNoteTargetUrl(meta.noteId, rootMeta);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                html += `<a href="${targetUrl}" target="detail">${escapedTitle}</a>`;
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                html += escapedTitle;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (meta.children && meta.children.length > 0) {
 | 
					 | 
				
			||||||
                html += "<ul>";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                for (const child of meta.children) {
 | 
					 | 
				
			||||||
                    html += saveNavigationInner(child);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                html += "</ul>";
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return `${html}</li>`;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const fullHtml = `<html>
 | 
					 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
    <meta charset="utf-8">
 | 
					 | 
				
			||||||
    <link rel="stylesheet" href="style.css">
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
    <ul>${saveNavigationInner(rootMeta)}</ul>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
</html>`;
 | 
					 | 
				
			||||||
        const prettyHtml = fullHtml.length < 100_000 ? html.prettyPrint(fullHtml, { indent_size: 2 }) : fullHtml;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        archive.append(prettyHtml, { name: navigationMeta.dataFileName });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function saveIndex(rootMeta: NoteMeta, indexMeta: NoteMeta) {
 | 
					 | 
				
			||||||
        let firstNonEmptyNote;
 | 
					 | 
				
			||||||
        let curMeta = rootMeta;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!indexMeta.dataFileName) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        while (!firstNonEmptyNote) {
 | 
					 | 
				
			||||||
            if (curMeta.dataFileName && curMeta.noteId) {
 | 
					 | 
				
			||||||
                firstNonEmptyNote = getNoteTargetUrl(curMeta.noteId, rootMeta);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (curMeta.children && curMeta.children.length > 0) {
 | 
					 | 
				
			||||||
                curMeta = curMeta.children[0];
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const fullHtml = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 | 
					 | 
				
			||||||
<html>
 | 
					 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
    <meta charset="utf-8">
 | 
					 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<frameset cols="25%,75%">
 | 
					 | 
				
			||||||
    <frame name="navigation" src="navigation.html">
 | 
					 | 
				
			||||||
    <frame name="detail" src="${firstNonEmptyNote}">
 | 
					 | 
				
			||||||
</frameset>
 | 
					 | 
				
			||||||
</html>`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        archive.append(fullHtml, { name: indexMeta.dataFileName });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function saveAssets(rootMeta: NoteMeta, assetsMeta: NoteMeta[]) {
 | 
					 | 
				
			||||||
        for (const assetMeta of assetsMeta) {
 | 
					 | 
				
			||||||
            if (!assetMeta.dataFileName) {
 | 
					 | 
				
			||||||
                continue;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let cssContent = getShareThemeAssets(assetMeta.dataFileName);
 | 
					 | 
				
			||||||
            archive.append(cssContent, { name: assetMeta.dataFileName });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const existingFileNames: Record<string, number> = format === "html" ? { navigation: 0, index: 1 } : {};
 | 
					    const existingFileNames: Record<string, number> = format === "html" ? { navigation: 0, index: 1 } : {};
 | 
				
			||||||
    const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames);
 | 
					    const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames);
 | 
				
			||||||
    if (!rootMeta) {
 | 
					    if (!rootMeta) {
 | 
				
			||||||
@ -541,47 +440,23 @@ ${markdownContent}`;
 | 
				
			|||||||
        files: [rootMeta]
 | 
					        files: [rootMeta]
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let navigationMeta: NoteMeta | null = null;
 | 
					    let provider: ZipExportProvider;
 | 
				
			||||||
    let indexMeta: NoteMeta | null = null;
 | 
					    switch (format) {
 | 
				
			||||||
    let assetsMeta: NoteMeta[] = [];
 | 
					        case "html":
 | 
				
			||||||
 | 
					            provider = new HtmlExportProvider({
 | 
				
			||||||
    if (format === "html") {
 | 
					                getNoteTargetUrl,
 | 
				
			||||||
        navigationMeta = {
 | 
					                metaFile,
 | 
				
			||||||
            noImport: true,
 | 
					                archive,
 | 
				
			||||||
            dataFileName: "navigation.html"
 | 
					                rootMeta
 | 
				
			||||||
        };
 | 
					            });
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
        metaFile.files.push(navigationMeta);
 | 
					        case "markdown":
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
        indexMeta = {
 | 
					            throw new Error();
 | 
				
			||||||
            noImport: true,
 | 
					 | 
				
			||||||
            dataFileName: "index.html"
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        metaFile.files.push(indexMeta);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const assets = [
 | 
					 | 
				
			||||||
            "style.css",
 | 
					 | 
				
			||||||
            "script.js",
 | 
					 | 
				
			||||||
            "boxicons.css",
 | 
					 | 
				
			||||||
            "boxicons.eot",
 | 
					 | 
				
			||||||
            "boxicons.woff2",
 | 
					 | 
				
			||||||
            "boxicons.woff",
 | 
					 | 
				
			||||||
            "boxicons.ttf",
 | 
					 | 
				
			||||||
            "boxicons.svg",
 | 
					 | 
				
			||||||
            "icon-color.svg"
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (const asset of assets) {
 | 
					 | 
				
			||||||
            const assetMeta = {
 | 
					 | 
				
			||||||
                noImport: true,
 | 
					 | 
				
			||||||
                dataFileName: asset
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
            assetsMeta.push(assetMeta);
 | 
					 | 
				
			||||||
            metaFile.files.push(assetMeta);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    provider.prepareMeta();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const noteMeta of Object.values(noteIdToMeta)) {
 | 
					    for (const noteMeta of Object.values(noteIdToMeta)) {
 | 
				
			||||||
        // filter out relations which are not inside this export
 | 
					        // filter out relations which are not inside this export
 | 
				
			||||||
        noteMeta.attributes = (noteMeta.attributes || []).filter((attr) => {
 | 
					        noteMeta.attributes = (noteMeta.attributes || []).filter((attr) => {
 | 
				
			||||||
@ -612,15 +487,7 @@ ${markdownContent}`;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    saveNote(rootMeta, "");
 | 
					    saveNote(rootMeta, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (format === "html") {
 | 
					    provider.afterDone();
 | 
				
			||||||
        if (!navigationMeta || !indexMeta || !assetsMeta) {
 | 
					 | 
				
			||||||
            throw new Error("Missing meta.");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        saveNavigation(rootMeta, navigationMeta);
 | 
					 | 
				
			||||||
        saveIndex(rootMeta, indexMeta);
 | 
					 | 
				
			||||||
        saveAssets(rootMeta, assetsMeta);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const note = branch.getNote();
 | 
					    const note = branch.getNote();
 | 
				
			||||||
    const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected()}.zip`;
 | 
					    const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected()}.zip`;
 | 
				
			||||||
@ -651,28 +518,6 @@ async function exportToZipFile(noteId: string, format: "markdown" | "html", zipF
 | 
				
			|||||||
    log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`);
 | 
					    log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getShareThemeAssets(nameWithExtension: string) {
 | 
					 | 
				
			||||||
    // Rename share.css to style.css.
 | 
					 | 
				
			||||||
    if (nameWithExtension === "style.css") {
 | 
					 | 
				
			||||||
        nameWithExtension = "share.css";
 | 
					 | 
				
			||||||
    } else if (nameWithExtension === "script.js") {
 | 
					 | 
				
			||||||
        nameWithExtension = "share.js";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let path: string | undefined;
 | 
					 | 
				
			||||||
    if (nameWithExtension === "icon-color.svg") {
 | 
					 | 
				
			||||||
        path = join(RESOURCE_DIR, "images", nameWithExtension);
 | 
					 | 
				
			||||||
    } else if (isDev) {
 | 
					 | 
				
			||||||
        path = join(getResourceDir(), "..", "..", "client", "dist", "src", nameWithExtension);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!path) {
 | 
					 | 
				
			||||||
        throw new Error("Not yet defined.");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return fs.readFileSync(path);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    exportToZip,
 | 
					    exportToZip,
 | 
				
			||||||
    exportToZipFile
 | 
					    exportToZipFile
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								apps/server/src/services/export/zip/abstract_provider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								apps/server/src/services/export/zip/abstract_provider.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import { Archiver } from "archiver";
 | 
				
			||||||
 | 
					import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ZipExportProviderData {
 | 
				
			||||||
 | 
					    getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null;
 | 
				
			||||||
 | 
					    metaFile: NoteMetaFile;
 | 
				
			||||||
 | 
					    rootMeta: NoteMeta;
 | 
				
			||||||
 | 
					    archive: Archiver;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export abstract class ZipExportProvider {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    metaFile: NoteMetaFile;
 | 
				
			||||||
 | 
					    getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null;
 | 
				
			||||||
 | 
					    rootMeta: NoteMeta;
 | 
				
			||||||
 | 
					    archive: Archiver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(data: ZipExportProviderData) {
 | 
				
			||||||
 | 
					        this.metaFile = data.metaFile;
 | 
				
			||||||
 | 
					        this.getNoteTargetUrl = data.getNoteTargetUrl;
 | 
				
			||||||
 | 
					        this.rootMeta = data.rootMeta;
 | 
				
			||||||
 | 
					        this.archive = data.archive;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    abstract prepareMeta(): void;
 | 
				
			||||||
 | 
					    abstract afterDone(): void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										135
									
								
								apps/server/src/services/export/zip/html.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								apps/server/src/services/export/zip/html.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,135 @@
 | 
				
			|||||||
 | 
					import type NoteMeta from "../../meta/note_meta.js";
 | 
				
			||||||
 | 
					import { escapeHtml } from "../../utils";
 | 
				
			||||||
 | 
					import cssContent from "@triliumnext/ckeditor5/content.css";
 | 
				
			||||||
 | 
					import html from "html";
 | 
				
			||||||
 | 
					import { ZipExportProvider } from "./abstract_provider.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class HtmlExportProvider extends ZipExportProvider {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private navigationMeta: NoteMeta | null = null;
 | 
				
			||||||
 | 
					    private indexMeta: NoteMeta | null = null;
 | 
				
			||||||
 | 
					    private cssMeta: NoteMeta | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    prepareMeta() {
 | 
				
			||||||
 | 
					        this.navigationMeta = {
 | 
				
			||||||
 | 
					            noImport: true,
 | 
				
			||||||
 | 
					            dataFileName: "navigation.html"
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.metaFile.files.push(this.navigationMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.indexMeta = {
 | 
				
			||||||
 | 
					            noImport: true,
 | 
				
			||||||
 | 
					            dataFileName: "index.html"
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.metaFile.files.push(this.indexMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.cssMeta = {
 | 
				
			||||||
 | 
					            noImport: true,
 | 
				
			||||||
 | 
					            dataFileName: "style.css"
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.metaFile.files.push(this.cssMeta);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    afterDone() {
 | 
				
			||||||
 | 
					        if (!this.navigationMeta || !this.indexMeta || !this.cssMeta) {
 | 
				
			||||||
 | 
					            throw new Error("Missing meta.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.#saveNavigation(this.rootMeta, this.navigationMeta);
 | 
				
			||||||
 | 
					        this.#saveIndex(this.rootMeta, this.indexMeta);
 | 
				
			||||||
 | 
					        this.#saveCss(this.rootMeta, this.cssMeta);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #saveNavigationInner(meta: NoteMeta) {
 | 
				
			||||||
 | 
					        let html = "<li>";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (meta.dataFileName && meta.noteId) {
 | 
				
			||||||
 | 
					            const targetUrl = this.getNoteTargetUrl(meta.noteId, this.rootMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            html += `<a href="${targetUrl}" target="detail">${escapedTitle}</a>`;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            html += escapedTitle;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (meta.children && meta.children.length > 0) {
 | 
				
			||||||
 | 
					            html += "<ul>";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (const child of meta.children) {
 | 
				
			||||||
 | 
					                html += this.#saveNavigationInner(child);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            html += "</ul>";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return `${html}</li>`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #saveNavigation(rootMeta: NoteMeta, navigationMeta: NoteMeta) {
 | 
				
			||||||
 | 
					        if (!navigationMeta.dataFileName) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const fullHtml = `<html>
 | 
				
			||||||
 | 
					    <head>
 | 
				
			||||||
 | 
					        <meta charset="utf-8">
 | 
				
			||||||
 | 
					        <link rel="stylesheet" href="style.css">
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					    <body>
 | 
				
			||||||
 | 
					        <ul>${this.#saveNavigationInner(rootMeta)}</ul>
 | 
				
			||||||
 | 
					    </body>
 | 
				
			||||||
 | 
					    </html>`;
 | 
				
			||||||
 | 
					        const prettyHtml = fullHtml.length < 100_000 ? html.prettyPrint(fullHtml, { indent_size: 2 }) : fullHtml;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.archive.append(prettyHtml, { name: navigationMeta.dataFileName });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #saveIndex(rootMeta: NoteMeta, indexMeta: NoteMeta) {
 | 
				
			||||||
 | 
					        let firstNonEmptyNote;
 | 
				
			||||||
 | 
					        let curMeta = rootMeta;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!indexMeta.dataFileName) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while (!firstNonEmptyNote) {
 | 
				
			||||||
 | 
					            if (curMeta.dataFileName && curMeta.noteId) {
 | 
				
			||||||
 | 
					                firstNonEmptyNote = this.getNoteTargetUrl(curMeta.noteId, rootMeta);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (curMeta.children && curMeta.children.length > 0) {
 | 
				
			||||||
 | 
					                curMeta = curMeta.children[0];
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const fullHtml = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="utf-8">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<frameset cols="25%,75%">
 | 
				
			||||||
 | 
					    <frame name="navigation" src="navigation.html">
 | 
				
			||||||
 | 
					    <frame name="detail" src="${firstNonEmptyNote}">
 | 
				
			||||||
 | 
					</frameset>
 | 
				
			||||||
 | 
					</html>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.archive.append(fullHtml, { name: indexMeta.dataFileName });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #saveCss(rootMeta: NoteMeta, cssMeta: NoteMeta) {
 | 
				
			||||||
 | 
					        if (!cssMeta.dataFileName) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.archive.append(cssContent, { name: cssMeta.dataFileName });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user