diff --git a/electron-docs-main.ts b/electron-docs-main.ts index 2646e5194..cc2af4875 100644 --- a/electron-docs-main.ts +++ b/electron-docs-main.ts @@ -9,6 +9,7 @@ import type { WriteStream } from "fs"; import debounce from "./src/public/app/services/debounce.js"; import { extractZip, initializeDatabase, startElectron } from "./electron-utils.js"; import cls from "./src/services/cls.js"; +import type { AdvancedExportOptions } from "./src/services/export/zip.js"; const NOTE_ID_USER_GUIDE = "pOsGYCXsbNQG"; const markdownPath = path.join("docs", "User Guide"); @@ -69,7 +70,54 @@ async function exportData(format: "html" | "markdown", outputPath: string) { // First export as zip. const { exportToZipFile } = (await import("./src/services/export/zip.js")).default; - await exportToZipFile(NOTE_ID_USER_GUIDE, format, zipFilePath); + + const exportOpts: AdvancedExportOptions = {}; + if (format === "html") { + exportOpts.customRewriteLinks = (originalRewriteLinks, getNoteTargetUrl) => { + return (content: string, noteMeta: NoteMeta) => { + content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/[^"]*"/g, (match, targetNoteId) => { + const url = getNoteTargetUrl(targetNoteId, noteMeta); + + return url ? `src="${url}"` : match; + }); + + content = content.replace(/src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image\/[^"]*"/g, (match, targetAttachmentId) => { + const url = findAttachment(targetAttachmentId); + + return url ? `src="${url}"` : match; + }); + + content = content.replace(/href="[^"]*#root[^"]*attachmentId=([a-zA-Z0-9_]+)\/?"/g, (match, targetAttachmentId) => { + const url = findAttachment(targetAttachmentId); + + return url ? `href="${url}"` : match; + }); + + content = content.replace(/href="[^"]*#root[a-zA-Z0-9_\/]*\/([a-zA-Z0-9_]+)[^"]*"/g, (match, targetNoteId) => { + const components = match.split("/"); + components[components.length - 1] = `_help_${components[components.length - 1]}`; + return components.join("/"); + }); + + return content; + + function findAttachment(targetAttachmentId: string) { + let url; + + const attachmentMeta = (noteMeta.attachments || []).find((attMeta) => attMeta.attachmentId === targetAttachmentId); + if (attachmentMeta) { + // easy job here, because attachment will be in the same directory as the note's data file. + url = attachmentMeta.dataFileName; + } else { + console.info(`Could not find attachment meta object for attachmentId '${targetAttachmentId}'`); + } + return url; + } + }; + }; + } + + await exportToZipFile(NOTE_ID_USER_GUIDE, format, zipFilePath, exportOpts); await extractZip(zipFilePath, outputPath); } finally { if (await fsExtra.exists(zipFilePath)) { diff --git a/src/services/export/zip.ts b/src/services/export/zip.ts index 495d074f4..392b58cd2 100644 --- a/src/services/export/zip.ts +++ b/src/services/export/zip.ts @@ -23,7 +23,20 @@ import type { Response } from "express"; import { RESOURCE_DIR } from "../resource_dir.js"; import type { NoteMetaFile } from "../meta/note_meta.js"; -async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true) { +type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; + +export interface AdvancedExportOptions { + /** + * Provides a custom function to rewrite the links found in HTML or Markdown notes. This method is called for every note imported, if it's of the right type. + * + * @param originalRewriteLinks the original rewrite links function. Can be used to access the default behaviour without having to reimplement it. + * @param getNoteTargetUrl the method to obtain a note's target URL, used internally by `originalRewriteLinks` but can be used here as well. + * @returns a function to rewrite the links in HTML or Markdown notes. + */ + customRewriteLinks?: (originalRewriteLinks: RewriteLinksFn, getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null) => RewriteLinksFn; +} + +async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) { if (!["html", "markdown"].includes(format)) { throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`); } @@ -253,6 +266,8 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h return url; } + const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks); + function rewriteLinks(content: string, noteMeta: NoteMeta): string { content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/[^"]*"/g, (match, targetNoteId) => { const url = getNoteTargetUrl(targetNoteId, noteMeta); @@ -297,8 +312,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { if (["html", "markdown"].includes(noteMeta?.format || "")) { content = content.toString(); - - content = rewriteLinks(content, noteMeta); + content = rewriteFn(content, noteMeta); } if (noteMeta.format === "html" && typeof content === "string") { @@ -591,7 +605,7 @@ ${markdownContent}`; taskContext.taskSucceeded(); } -async function exportToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string) { +async function exportToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string, zipExportOptions?: AdvancedExportOptions) { const fileOutputStream = fs.createWriteStream(zipFilePath); const taskContext = new TaskContext("no-progress-reporting"); @@ -601,7 +615,7 @@ async function exportToZipFile(noteId: string, format: "markdown" | "html", zipF throw new ValidationError(`Note ${noteId} not found.`); } - await exportToZip(taskContext, note.getParentBranches()[0], format, fileOutputStream, false); + await exportToZip(taskContext, note.getParentBranches()[0], format, fileOutputStream, false, zipExportOptions); log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`); }