From b475037127466737cc2eade8601798d8ef052d47 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 20:00:40 +0300 Subject: [PATCH] feat(export/share): render non-text note types --- apps/server/src/services/export/zip.ts | 38 +++---------------- .../services/export/zip/abstract_provider.ts | 37 ++++++++++++++---- apps/server/src/services/export/zip/html.ts | 27 ++++++------- .../src/services/export/zip/share_theme.ts | 22 ++++++----- 4 files changed, 60 insertions(+), 64 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 9c0f099d1..26af3424f 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -2,7 +2,6 @@ import dateUtils from "../date_utils.js"; import path from "path"; -import mimeTypes from "mime-types"; import packageInfo from "../../../package.json" with { type: "json" }; import { getContentDisposition } from "../utils.js"; import protectedSessionService from "../protected_session.js"; @@ -33,16 +32,15 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h const archive = archiver("zip", { zlib: { level: 9 } // Sets the compression level. }); + const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks); + const provider= buildProvider(); const noteIdToMeta: Record = {}; - const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks); function buildProvider() { const providerData: ZipExportProviderData = { getNoteTargetUrl, - metaFile, archive, - rootMeta: rootMeta!, branch, rewriteFn }; @@ -94,36 +92,14 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h } let existingExtension = path.extname(fileName).toLowerCase(); - let newExtension; - - // the following two are handled specifically since we always want to have these extensions no matter the automatic detection - // and/or existing detected extensions in the note name - if (type === "text" && format === "markdown") { - newExtension = "md"; - } else if (type === "text" && format === "html") { - newExtension = "html"; - } else if (mime === "application/x-javascript" || mime === "text/javascript") { - newExtension = "js"; - } else if (type === "canvas" || mime === "application/json") { - newExtension = "json"; - } else if (existingExtension.length > 0) { - // if the page already has an extension, then we'll just keep it - newExtension = null; - } else { - if (mime?.toLowerCase()?.trim() === "image/jpg") { - newExtension = "jpg"; - } else if (mime?.toLowerCase()?.trim() === "text/mermaid") { - newExtension = "txt"; - } else { - newExtension = mimeTypes.extension(mime) || "dat"; - } - } + const newExtension = provider.mapExtension(type, mime, existingExtension, format); // if the note is already named with the extension (e.g. "image.jpg"), then it's silly to append the exact same extension again if (newExtension && existingExtension !== `.${newExtension.toLowerCase()}`) { fileName += `.${newExtension}`; } + return getUniqueFilename(existingFileNames, fileName); } @@ -408,9 +384,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h files: [rootMeta] }; - const provider= buildProvider(); - - provider.prepareMeta(); + provider.prepareMeta(metaFile); for (const noteMeta of Object.values(noteIdToMeta)) { // filter out relations which are not inside this export @@ -442,7 +416,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h saveNote(rootMeta, ""); - provider.afterDone(); + provider.afterDone(rootMeta); const note = branch.getNote(); const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected()}.zip`; diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts index 0c7a53656..6ca5fdb9a 100644 --- a/apps/server/src/services/export/zip/abstract_provider.ts +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -2,6 +2,7 @@ import { Archiver } from "archiver"; import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js"; import type BNote from "../../../becca/entities/bnote.js"; import type BBranch from "../../../becca/entities/bbranch.js"; +import mimeTypes from "mime-types"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; @@ -24,8 +25,6 @@ export interface AdvancedExportOptions { export interface ZipExportProviderData { branch: BBranch; getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; - metaFile: NoteMetaFile; - rootMeta: NoteMeta; archive: Archiver; zipExportOptions?: AdvancedExportOptions; rewriteFn: RewriteLinksFn; @@ -33,24 +32,46 @@ export interface ZipExportProviderData { export abstract class ZipExportProvider { branch: BBranch; - metaFile: NoteMetaFile; getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; - rootMeta: NoteMeta; archive: Archiver; zipExportOptions?: AdvancedExportOptions; rewriteFn: RewriteLinksFn; constructor(data: ZipExportProviderData) { this.branch = data.branch; - this.metaFile = data.metaFile; this.getNoteTargetUrl = data.getNoteTargetUrl; - this.rootMeta = data.rootMeta; this.archive = data.archive; this.zipExportOptions = data.zipExportOptions; this.rewriteFn = data.rewriteFn; } - abstract prepareMeta(): void; + abstract prepareMeta(metaFile: NoteMetaFile): void; abstract prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer; - abstract afterDone(): void; + abstract afterDone(rootMeta: NoteMeta): void; + + mapExtension(type: string | null, mime: string, existingExtension: string, format: string) { + // the following two are handled specifically since we always want to have these extensions no matter the automatic detection + // and/or existing detected extensions in the note name + if (type === "text" && format === "markdown") { + return "md"; + } else if (type === "text" && format === "html") { + return "html"; + } else if (mime === "application/x-javascript" || mime === "text/javascript") { + return "js"; + } else if (type === "canvas" || mime === "application/json") { + return "json"; + } else if (existingExtension.length > 0) { + // if the page already has an extension, then we'll just keep it + return null; + } else { + if (mime?.toLowerCase()?.trim() === "image/jpg") { + return "jpg"; + } else if (mime?.toLowerCase()?.trim() === "text/mermaid") { + return "txt"; + } else { + return mimeTypes.extension(mime) || "dat"; + } + } + } + } diff --git a/apps/server/src/services/export/zip/html.ts b/apps/server/src/services/export/zip/html.ts index 749d7adc8..8eb5c5d93 100644 --- a/apps/server/src/services/export/zip/html.ts +++ b/apps/server/src/services/export/zip/html.ts @@ -10,27 +10,24 @@ export default class HtmlExportProvider extends ZipExportProvider { private indexMeta: NoteMeta | null = null; private cssMeta: NoteMeta | null = null; - prepareMeta() { + prepareMeta(metaFile) { this.navigationMeta = { noImport: true, dataFileName: "navigation.html" }; - - this.metaFile.files.push(this.navigationMeta); + metaFile.files.push(this.navigationMeta); this.indexMeta = { noImport: true, dataFileName: "index.html" }; - - this.metaFile.files.push(this.indexMeta); + metaFile.files.push(this.indexMeta); this.cssMeta = { noImport: true, dataFileName: "style.css" }; - - this.metaFile.files.push(this.cssMeta); + metaFile.files.push(this.cssMeta); } prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { @@ -72,23 +69,23 @@ export default class HtmlExportProvider extends ZipExportProvider { } } - afterDone() { + afterDone(rootMeta: NoteMeta) { 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); + this.#saveNavigation(rootMeta, this.navigationMeta); + this.#saveIndex(rootMeta, this.indexMeta); + this.#saveCss(rootMeta, this.cssMeta); } - #saveNavigationInner(meta: NoteMeta) { + #saveNavigationInner(rootMeta: NoteMeta, 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); + const targetUrl = this.getNoteTargetUrl(meta.noteId, rootMeta); html += `${escapedTitle}`; } else { @@ -99,7 +96,7 @@ export default class HtmlExportProvider extends ZipExportProvider { html += "
      "; for (const child of meta.children) { - html += this.#saveNavigationInner(child); + html += this.#saveNavigationInner(rootMeta, child); } html += "
    "; @@ -119,7 +116,7 @@ export default class HtmlExportProvider extends ZipExportProvider { -
      ${this.#saveNavigationInner(rootMeta)}
    +
      ${this.#saveNavigationInner(rootMeta, rootMeta)}
    `; const prettyHtml = fullHtml.length < 100_000 ? html.prettyPrint(fullHtml, { indent_size: 2 }) : fullHtml; diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 2b4ba72e8..abe7be42d 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -1,5 +1,5 @@ import { join } from "path"; -import NoteMeta from "../../meta/note_meta"; +import NoteMeta, { NoteMetaFile } from "../../meta/note_meta"; import { ZipExportProvider } from "./abstract_provider"; import { RESOURCE_DIR } from "../../resource_dir"; import { getResourceDir, isDev } from "../../utils"; @@ -13,7 +13,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { private assetsMeta: NoteMeta[] = []; private indexMeta: NoteMeta | null = null; - prepareMeta(): void { + prepareMeta(metaFile: NoteMetaFile): void { const assets = [ "style.css", "script.js", @@ -32,7 +32,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { dataFileName: asset }; this.assetsMeta.push(assetMeta); - this.metaFile.files.push(assetMeta); + metaFile.files.push(assetMeta); } this.indexMeta = { @@ -40,7 +40,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { dataFileName: "index.html" }; - this.metaFile.files.push(this.indexMeta); + metaFile.files.push(this.indexMeta); } prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer { @@ -58,18 +58,22 @@ export default class ShareThemeExportProvider extends ZipExportProvider { return content; } - afterDone(): void { - this.#saveAssets(this.rootMeta, this.assetsMeta); - this.#saveIndex(); + afterDone(rootMeta: NoteMeta): void { + this.#saveAssets(rootMeta, this.assetsMeta); + this.#saveIndex(rootMeta); } - #saveIndex() { + mapExtension(_type: string | null, _mime: string, _existingExtension: string, _format: string): string | null { + return "html"; + } + + #saveIndex(rootMeta: NoteMeta) { if (!this.indexMeta?.dataFileName) { return; } const note = this.branch.getNote(); - const fullHtml = this.prepareContent(this.rootMeta.title ?? "", note.getContent(), this.rootMeta, note, this.branch); + const fullHtml = this.prepareContent(rootMeta.title ?? "", note.getContent(), rootMeta, note, this.branch); this.archive.append(fullHtml, { name: this.indexMeta.dataFileName }); }