feat(export/share): render non-text note types

This commit is contained in:
Elian Doran 2025-06-23 20:00:40 +03:00
parent 35622a2122
commit b475037127
No known key found for this signature in database
4 changed files with 60 additions and 64 deletions

View File

@ -2,7 +2,6 @@
import dateUtils from "../date_utils.js"; import dateUtils from "../date_utils.js";
import path from "path"; import path from "path";
import mimeTypes from "mime-types";
import packageInfo from "../../../package.json" with { type: "json" }; import packageInfo from "../../../package.json" with { type: "json" };
import { getContentDisposition } from "../utils.js"; import { getContentDisposition } from "../utils.js";
import protectedSessionService from "../protected_session.js"; import protectedSessionService from "../protected_session.js";
@ -33,16 +32,15 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
const archive = archiver("zip", { const archive = archiver("zip", {
zlib: { level: 9 } // Sets the compression level. zlib: { level: 9 } // Sets the compression level.
}); });
const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks);
const provider= buildProvider();
const noteIdToMeta: Record<string, NoteMeta> = {}; const noteIdToMeta: Record<string, NoteMeta> = {};
const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks);
function buildProvider() { function buildProvider() {
const providerData: ZipExportProviderData = { const providerData: ZipExportProviderData = {
getNoteTargetUrl, getNoteTargetUrl,
metaFile,
archive, archive,
rootMeta: rootMeta!,
branch, branch,
rewriteFn rewriteFn
}; };
@ -94,36 +92,14 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
} }
let existingExtension = path.extname(fileName).toLowerCase(); let existingExtension = path.extname(fileName).toLowerCase();
let newExtension; const newExtension = provider.mapExtension(type, mime, existingExtension, format);
// 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";
}
}
// 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 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()}`) { if (newExtension && existingExtension !== `.${newExtension.toLowerCase()}`) {
fileName += `.${newExtension}`; fileName += `.${newExtension}`;
} }
return getUniqueFilename(existingFileNames, fileName); return getUniqueFilename(existingFileNames, fileName);
} }
@ -408,9 +384,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
files: [rootMeta] files: [rootMeta]
}; };
const provider= buildProvider(); provider.prepareMeta(metaFile);
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
@ -442,7 +416,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
saveNote(rootMeta, ""); saveNote(rootMeta, "");
provider.afterDone(); provider.afterDone(rootMeta);
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`;

View File

@ -2,6 +2,7 @@ import { Archiver } from "archiver";
import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js"; import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js";
import type BNote from "../../../becca/entities/bnote.js"; import type BNote from "../../../becca/entities/bnote.js";
import type BBranch from "../../../becca/entities/bbranch.js"; import type BBranch from "../../../becca/entities/bbranch.js";
import mimeTypes from "mime-types";
type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string;
@ -24,8 +25,6 @@ export interface AdvancedExportOptions {
export interface ZipExportProviderData { export interface ZipExportProviderData {
branch: BBranch; branch: BBranch;
getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null;
metaFile: NoteMetaFile;
rootMeta: NoteMeta;
archive: Archiver; archive: Archiver;
zipExportOptions?: AdvancedExportOptions; zipExportOptions?: AdvancedExportOptions;
rewriteFn: RewriteLinksFn; rewriteFn: RewriteLinksFn;
@ -33,24 +32,46 @@ export interface ZipExportProviderData {
export abstract class ZipExportProvider { export abstract class ZipExportProvider {
branch: BBranch; branch: BBranch;
metaFile: NoteMetaFile;
getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null;
rootMeta: NoteMeta;
archive: Archiver; archive: Archiver;
zipExportOptions?: AdvancedExportOptions; zipExportOptions?: AdvancedExportOptions;
rewriteFn: RewriteLinksFn; rewriteFn: RewriteLinksFn;
constructor(data: ZipExportProviderData) { constructor(data: ZipExportProviderData) {
this.branch = data.branch; this.branch = data.branch;
this.metaFile = data.metaFile;
this.getNoteTargetUrl = data.getNoteTargetUrl; this.getNoteTargetUrl = data.getNoteTargetUrl;
this.rootMeta = data.rootMeta;
this.archive = data.archive; this.archive = data.archive;
this.zipExportOptions = data.zipExportOptions; this.zipExportOptions = data.zipExportOptions;
this.rewriteFn = data.rewriteFn; 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 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";
}
}
}
} }

View File

@ -10,27 +10,24 @@ export default class HtmlExportProvider extends ZipExportProvider {
private indexMeta: NoteMeta | null = null; private indexMeta: NoteMeta | null = null;
private cssMeta: NoteMeta | null = null; private cssMeta: NoteMeta | null = null;
prepareMeta() { prepareMeta(metaFile) {
this.navigationMeta = { this.navigationMeta = {
noImport: true, noImport: true,
dataFileName: "navigation.html" dataFileName: "navigation.html"
}; };
metaFile.files.push(this.navigationMeta);
this.metaFile.files.push(this.navigationMeta);
this.indexMeta = { this.indexMeta = {
noImport: true, noImport: true,
dataFileName: "index.html" dataFileName: "index.html"
}; };
metaFile.files.push(this.indexMeta);
this.metaFile.files.push(this.indexMeta);
this.cssMeta = { this.cssMeta = {
noImport: true, noImport: true,
dataFileName: "style.css" dataFileName: "style.css"
}; };
metaFile.files.push(this.cssMeta);
this.metaFile.files.push(this.cssMeta);
} }
prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { 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) { if (!this.navigationMeta || !this.indexMeta || !this.cssMeta) {
throw new Error("Missing meta."); throw new Error("Missing meta.");
} }
this.#saveNavigation(this.rootMeta, this.navigationMeta); this.#saveNavigation(rootMeta, this.navigationMeta);
this.#saveIndex(this.rootMeta, this.indexMeta); this.#saveIndex(rootMeta, this.indexMeta);
this.#saveCss(this.rootMeta, this.cssMeta); this.#saveCss(rootMeta, this.cssMeta);
} }
#saveNavigationInner(meta: NoteMeta) { #saveNavigationInner(rootMeta: NoteMeta, meta: NoteMeta) {
let html = "<li>"; let html = "<li>";
const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`); const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`);
if (meta.dataFileName && meta.noteId) { if (meta.dataFileName && meta.noteId) {
const targetUrl = this.getNoteTargetUrl(meta.noteId, this.rootMeta); const targetUrl = this.getNoteTargetUrl(meta.noteId, rootMeta);
html += `<a href="${targetUrl}" target="detail">${escapedTitle}</a>`; html += `<a href="${targetUrl}" target="detail">${escapedTitle}</a>`;
} else { } else {
@ -99,7 +96,7 @@ export default class HtmlExportProvider extends ZipExportProvider {
html += "<ul>"; html += "<ul>";
for (const child of meta.children) { for (const child of meta.children) {
html += this.#saveNavigationInner(child); html += this.#saveNavigationInner(rootMeta, child);
} }
html += "</ul>"; html += "</ul>";
@ -119,7 +116,7 @@ export default class HtmlExportProvider extends ZipExportProvider {
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body> <body>
<ul>${this.#saveNavigationInner(rootMeta)}</ul> <ul>${this.#saveNavigationInner(rootMeta, rootMeta)}</ul>
</body> </body>
</html>`; </html>`;
const prettyHtml = fullHtml.length < 100_000 ? html.prettyPrint(fullHtml, { indent_size: 2 }) : fullHtml; const prettyHtml = fullHtml.length < 100_000 ? html.prettyPrint(fullHtml, { indent_size: 2 }) : fullHtml;

View File

@ -1,5 +1,5 @@
import { join } from "path"; import { join } from "path";
import NoteMeta from "../../meta/note_meta"; import NoteMeta, { NoteMetaFile } from "../../meta/note_meta";
import { ZipExportProvider } from "./abstract_provider"; import { ZipExportProvider } from "./abstract_provider";
import { RESOURCE_DIR } from "../../resource_dir"; import { RESOURCE_DIR } from "../../resource_dir";
import { getResourceDir, isDev } from "../../utils"; import { getResourceDir, isDev } from "../../utils";
@ -13,7 +13,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider {
private assetsMeta: NoteMeta[] = []; private assetsMeta: NoteMeta[] = [];
private indexMeta: NoteMeta | null = null; private indexMeta: NoteMeta | null = null;
prepareMeta(): void { prepareMeta(metaFile: NoteMetaFile): void {
const assets = [ const assets = [
"style.css", "style.css",
"script.js", "script.js",
@ -32,7 +32,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider {
dataFileName: asset dataFileName: asset
}; };
this.assetsMeta.push(assetMeta); this.assetsMeta.push(assetMeta);
this.metaFile.files.push(assetMeta); metaFile.files.push(assetMeta);
} }
this.indexMeta = { this.indexMeta = {
@ -40,7 +40,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider {
dataFileName: "index.html" 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 { 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; return content;
} }
afterDone(): void { afterDone(rootMeta: NoteMeta): void {
this.#saveAssets(this.rootMeta, this.assetsMeta); this.#saveAssets(rootMeta, this.assetsMeta);
this.#saveIndex(); 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) { if (!this.indexMeta?.dataFileName) {
return; return;
} }
const note = this.branch.getNote(); 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 }); this.archive.append(fullHtml, { name: this.indexMeta.dataFileName });
} }