mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	feat(edit-docs): rewrite links to allow navigation in help
This commit is contained in:
		| @@ -9,6 +9,7 @@ import type { WriteStream } from "fs"; | |||||||
| import debounce from "./src/public/app/services/debounce.js"; | import debounce from "./src/public/app/services/debounce.js"; | ||||||
| import { extractZip, initializeDatabase, startElectron } from "./electron-utils.js"; | import { extractZip, initializeDatabase, startElectron } from "./electron-utils.js"; | ||||||
| import cls from "./src/services/cls.js"; | import cls from "./src/services/cls.js"; | ||||||
|  | import type { AdvancedExportOptions } from "./src/services/export/zip.js"; | ||||||
|  |  | ||||||
| const NOTE_ID_USER_GUIDE = "pOsGYCXsbNQG"; | const NOTE_ID_USER_GUIDE = "pOsGYCXsbNQG"; | ||||||
| const markdownPath = path.join("docs", "User Guide"); | const markdownPath = path.join("docs", "User Guide"); | ||||||
| @@ -69,7 +70,54 @@ async function exportData(format: "html" | "markdown", outputPath: string) { | |||||||
|  |  | ||||||
|         // First export as zip. |         // First export as zip. | ||||||
|         const { exportToZipFile } = (await import("./src/services/export/zip.js")).default; |         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); |         await extractZip(zipFilePath, outputPath); | ||||||
|     } finally { |     } finally { | ||||||
|         if (await fsExtra.exists(zipFilePath)) { |         if (await fsExtra.exists(zipFilePath)) { | ||||||
|   | |||||||
| @@ -23,7 +23,20 @@ import type { Response } from "express"; | |||||||
| import { RESOURCE_DIR } from "../resource_dir.js"; | import { RESOURCE_DIR } from "../resource_dir.js"; | ||||||
| import type { NoteMetaFile } from "../meta/note_meta.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)) { |     if (!["html", "markdown"].includes(format)) { | ||||||
|         throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`); |         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; |         return url; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks); | ||||||
|  |  | ||||||
|     function rewriteLinks(content: string, noteMeta: NoteMeta): string { |     function rewriteLinks(content: string, noteMeta: NoteMeta): string { | ||||||
|         content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/[^"]*"/g, (match, targetNoteId) => { |         content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/[^"]*"/g, (match, targetNoteId) => { | ||||||
|             const url = getNoteTargetUrl(targetNoteId, noteMeta); |             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 { |     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 = rewriteLinks(content, noteMeta); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (noteMeta.format === "html" && typeof content === "string") { |         if (noteMeta.format === "html" && typeof content === "string") { | ||||||
| @@ -591,7 +605,7 @@ ${markdownContent}`; | |||||||
|     taskContext.taskSucceeded(); |     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 fileOutputStream = fs.createWriteStream(zipFilePath); | ||||||
|     const taskContext = new TaskContext("no-progress-reporting"); |     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.`); |         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}'`); |     log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`); | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user