diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 7034f8e18..6caffac86 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -2,11 +2,11 @@ import html from "html"; import dateUtils from "../date_utils.js"; -import path, { join } from "path"; +import path from "path"; import mimeTypes from "mime-types"; import mdService from "./markdown.js"; 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 sanitize from "sanitize-filename"; 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 AttributeMeta from "../meta/attribute_meta.js"; import type BBranch from "../../becca/entities/bbranch.js"; -import type BNote from "../../becca/entities/bnote.js"; import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; -//import cssContent from "@triliumnext/ckeditor5/content.css"; -import { renderNoteForExport } from "../../share/content_renderer.js"; -import { RESOURCE_DIR } from "../resource_dir.js"; +import HtmlExportProvider from "./zip/html.js"; +import { ZipExportProvider } from "./zip/abstract_provider.js"; 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 || "")) { content = content.toString(); content = rewriteFn(content, noteMeta); @@ -329,18 +327,11 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h 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); - if (note) { - content = renderNoteForExport(note, branch, basePath); - - // TODO: Fix double rewrite. - content = rewriteFn(content, noteMeta); - } else { - const cssUrl = basePath + "style.css"; - // element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 - content = ` + // element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 + content = ` @@ -356,7 +347,6 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h `; - } } return content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content; @@ -386,7 +376,7 @@ ${markdownContent}`; let content: string | Buffer = `

This is a clone of a note. Go to its primary location.

`; - content = prepareContent(undefined, noteMeta.title, content, noteMeta); + content = prepareContent(noteMeta.title, content, noteMeta); archive.append(content, { name: filePathPrefix + noteMeta.dataFileName }); @@ -402,7 +392,7 @@ ${markdownContent}`; } if (noteMeta.dataFileName) { - const content = prepareContent(note, noteMeta.title, note.getContent(), noteMeta); + const content = prepareContent(noteMeta.title, note.getContent(), noteMeta); archive.append(content, { 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 = "
  • "; - - const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`); - - if (meta.dataFileName && meta.noteId) { - const targetUrl = getNoteTargetUrl(meta.noteId, rootMeta); - - html += `${escapedTitle}`; - } else { - html += escapedTitle; - } - - if (meta.children && meta.children.length > 0) { - html += ""; - } - - return `${html}
  • `; - } - - const fullHtml = ` - - - - - - - -`; - 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 = ` - - - - - - - - - -`; - - 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 = format === "html" ? { navigation: 0, index: 1 } : {}; const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames); if (!rootMeta) { @@ -541,47 +440,23 @@ ${markdownContent}`; files: [rootMeta] }; - let navigationMeta: NoteMeta | null = null; - let indexMeta: NoteMeta | null = null; - let assetsMeta: NoteMeta[] = []; - - if (format === "html") { - navigationMeta = { - noImport: true, - dataFileName: "navigation.html" - }; - - metaFile.files.push(navigationMeta); - - indexMeta = { - 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); - } + let provider: ZipExportProvider; + switch (format) { + case "html": + provider = new HtmlExportProvider({ + getNoteTargetUrl, + metaFile, + archive, + rootMeta + }); + break; + case "markdown": + default: + throw new Error(); } + provider.prepareMeta(); + for (const noteMeta of Object.values(noteIdToMeta)) { // filter out relations which are not inside this export noteMeta.attributes = (noteMeta.attributes || []).filter((attr) => { @@ -612,15 +487,7 @@ ${markdownContent}`; saveNote(rootMeta, ""); - if (format === "html") { - if (!navigationMeta || !indexMeta || !assetsMeta) { - throw new Error("Missing meta."); - } - - saveNavigation(rootMeta, navigationMeta); - saveIndex(rootMeta, indexMeta); - saveAssets(rootMeta, assetsMeta); - } + provider.afterDone(); const note = branch.getNote(); 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}'`); } -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 { exportToZip, exportToZipFile diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts new file mode 100644 index 000000000..264dde0a7 --- /dev/null +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -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; +} diff --git a/apps/server/src/services/export/zip/html.ts b/apps/server/src/services/export/zip/html.ts new file mode 100644 index 000000000..517552e1d --- /dev/null +++ b/apps/server/src/services/export/zip/html.ts @@ -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 = "
  • "; + + const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`); + + if (meta.dataFileName && meta.noteId) { + const targetUrl = this.getNoteTargetUrl(meta.noteId, this.rootMeta); + + html += `${escapedTitle}`; + } else { + html += escapedTitle; + } + + if (meta.children && meta.children.length > 0) { + html += ""; + } + + return `${html}
  • `; + } + + #saveNavigation(rootMeta: NoteMeta, navigationMeta: NoteMeta) { + if (!navigationMeta.dataFileName) { + return; + } + + const fullHtml = ` + + + + + + + + `; + 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 = ` + + + + + + + + + +`; + + this.archive.append(fullHtml, { name: indexMeta.dataFileName }); + } + + #saveCss(rootMeta: NoteMeta, cssMeta: NoteMeta) { + if (!cssMeta.dataFileName) { + return; + } + + this.archive.append(cssContent, { name: cssMeta.dataFileName }); + } + +} +