| 
									
										
										
										
											2025-03-10 16:20:48 +02:00
										 |  |  | import fs from "fs/promises"; | 
					
						
							|  |  |  | import fsExtra from "fs-extra"; | 
					
						
							|  |  |  | import path from "path"; | 
					
						
							| 
									
										
										
										
											2025-03-10 16:37:39 +02:00
										 |  |  | import type NoteMeta from "./src/services/meta/note_meta.js"; | 
					
						
							|  |  |  | import type { NoteMetaFile } from "./src/services/meta/note_meta.js"; | 
					
						
							| 
									
										
										
										
											2025-03-10 17:04:17 +02:00
										 |  |  | import { initializeTranslations } from "./src/services/i18n.js"; | 
					
						
							| 
									
										
										
										
											2025-03-10 18:51:40 +02:00
										 |  |  | import archiver, { type Archiver } from "archiver"; | 
					
						
							|  |  |  | import type { WriteStream } from "fs"; | 
					
						
							| 
									
										
										
										
											2025-03-10 19:34:10 +02:00
										 |  |  | import debounce from "./src/public/app/services/debounce.js"; | 
					
						
							| 
									
										
										
										
											2025-04-02 22:18:28 +03:00
										 |  |  | import { extractZip, initializeDatabase, startElectron } from "./electron-utils.js"; | 
					
						
							| 
									
										
										
										
											2025-04-04 18:35:29 +03:00
										 |  |  | import cls from "./src/services/cls.js"; | 
					
						
							| 
									
										
										
										
											2025-04-09 15:48:03 +03:00
										 |  |  | import type { AdvancedExportOptions } from "./src/services/export/zip.js"; | 
					
						
							| 
									
										
										
										
											2025-04-11 14:02:35 +03:00
										 |  |  | import TaskContext from "./src/services/task_context.js"; | 
					
						
							| 
									
										
										
										
											2025-04-11 18:39:12 +03:00
										 |  |  | import { deferred } from "./src/services/utils.js"; | 
					
						
							| 
									
										
										
										
											2025-03-10 16:20:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-10 20:16:56 +02:00
										 |  |  | const NOTE_ID_USER_GUIDE = "pOsGYCXsbNQG"; | 
					
						
							| 
									
										
										
										
											2025-04-11 14:10:47 +03:00
										 |  |  | const NOTE_ID_RELEASE_NOTES = "hD3V4hiu2VW4"; | 
					
						
							| 
									
										
										
										
											2025-03-11 21:58:32 +02:00
										 |  |  | const markdownPath = path.join("docs", "User Guide"); | 
					
						
							| 
									
										
										
										
											2025-04-11 14:10:47 +03:00
										 |  |  | const releaseNotesPath = path.join("docs", "Release Notes"); | 
					
						
							| 
									
										
										
										
											2025-03-11 21:58:32 +02:00
										 |  |  | const htmlPath = path.join("src", "public", "app", "doc_notes", "en", "User Guide"); | 
					
						
							| 
									
										
										
										
											2025-03-10 16:20:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-28 22:02:29 +02:00
										 |  |  | async function main() { | 
					
						
							| 
									
										
										
										
											2025-03-10 17:04:17 +02:00
										 |  |  |     await initializeTranslations(); | 
					
						
							| 
									
										
										
										
											2025-04-11 14:02:35 +03:00
										 |  |  |     await initializeDatabase(true); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-11 18:39:12 +03:00
										 |  |  |     const initializedPromise = deferred<void>(); | 
					
						
							| 
									
										
										
										
											2025-04-11 14:02:35 +03:00
										 |  |  |     cls.init(async () => { | 
					
						
							|  |  |  |         await importData(markdownPath); | 
					
						
							| 
									
										
										
										
											2025-04-11 14:10:47 +03:00
										 |  |  |         await importData(releaseNotesPath); | 
					
						
							| 
									
										
										
										
											2025-04-11 14:02:35 +03:00
										 |  |  |         setOptions(); | 
					
						
							| 
									
										
										
										
											2025-04-11 18:39:12 +03:00
										 |  |  |         initializedPromise.resolve(); | 
					
						
							| 
									
										
										
										
											2025-04-11 14:02:35 +03:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-04-04 18:35:29 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-11 18:39:12 +03:00
										 |  |  |     await initializedPromise; | 
					
						
							| 
									
										
										
										
											2025-02-28 22:02:29 +02:00
										 |  |  |     await startElectron(); | 
					
						
							| 
									
										
										
										
											2025-04-04 18:35:29 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Wait for the import to be finished and the application to be loaded before we listen to changes.
 | 
					
						
							|  |  |  |     setTimeout(() => registerHandlers(), 10_000); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function setOptions() { | 
					
						
							|  |  |  |     const optionsService = (await import("./src/services/options.js")).default; | 
					
						
							| 
									
										
										
										
											2025-04-04 20:01:28 +03:00
										 |  |  |     optionsService.setOption("eraseUnusedAttachmentsAfterSeconds", 10); | 
					
						
							| 
									
										
										
										
											2025-04-04 18:35:29 +03:00
										 |  |  |     optionsService.setOption("eraseUnusedAttachmentsAfterTimeScale", 60); | 
					
						
							| 
									
										
										
										
											2025-04-04 20:01:28 +03:00
										 |  |  |     optionsService.setOption("compressImages", "false"); | 
					
						
							| 
									
										
										
										
											2025-03-10 17:04:17 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-11 14:02:35 +03:00
										 |  |  | async function importData(path: string) { | 
					
						
							|  |  |  |     const buffer = await createImportZip(path); | 
					
						
							|  |  |  |     const importService = (await import("./src/services/import/zip.js")).default; | 
					
						
							|  |  |  |     const context = new TaskContext("no-progress-reporting", "import", false); | 
					
						
							|  |  |  |     const becca = (await import("./src/becca/becca.js")).default; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const rootNote = becca.getRoot(); | 
					
						
							|  |  |  |     if (!rootNote) { | 
					
						
							|  |  |  |         throw new Error("Missing root note for import."); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     await importService.importZip(context, buffer, rootNote, { | 
					
						
							|  |  |  |         preserveIds: true | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function createImportZip(path: string) { | 
					
						
							| 
									
										
										
										
											2025-03-11 20:48:40 +02:00
										 |  |  |     const inputFile = "input.zip"; | 
					
						
							| 
									
										
										
										
											2025-03-10 17:50:58 +02:00
										 |  |  |     const archive = archiver("zip", { | 
					
						
							|  |  |  |         zlib: { level: 0 } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-11 14:02:35 +03:00
										 |  |  |     archive.directory(path, "/"); | 
					
						
							| 
									
										
										
										
											2025-03-10 17:50:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-11 20:48:40 +02:00
										 |  |  |     const outputStream = fsExtra.createWriteStream(inputFile); | 
					
						
							| 
									
										
										
										
											2025-03-10 17:50:58 +02:00
										 |  |  |     archive.pipe(outputStream); | 
					
						
							| 
									
										
										
										
											2025-03-10 18:51:40 +02:00
										 |  |  |     await waitForEnd(archive, outputStream); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-11 20:48:40 +02:00
										 |  |  |     try { | 
					
						
							|  |  |  |         return await fsExtra.readFile(inputFile); | 
					
						
							|  |  |  |     } finally { | 
					
						
							|  |  |  |         await fsExtra.rm(inputFile); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-03-10 18:51:40 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function waitForEnd(archive: Archiver, stream: WriteStream) { | 
					
						
							|  |  |  |     return new Promise<void>(async (res, rej) => { | 
					
						
							|  |  |  |         stream.on("finish", () => res()); | 
					
						
							|  |  |  |         await archive.finalize(); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-10 17:50:58 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-11 14:10:47 +03:00
										 |  |  | async function exportData(noteId: string, format: "html" | "markdown", outputPath: string) { | 
					
						
							| 
									
										
										
										
											2025-03-10 16:20:48 +02:00
										 |  |  |     const zipFilePath = "output.zip"; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-10 16:31:44 +02:00
										 |  |  |     try { | 
					
						
							| 
									
										
										
										
											2025-03-11 21:58:32 +02:00
										 |  |  |         await fsExtra.remove(outputPath); | 
					
						
							|  |  |  |         await fsExtra.mkdir(outputPath); | 
					
						
							| 
									
										
										
										
											2025-03-10 16:31:44 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // First export as zip.
 | 
					
						
							|  |  |  |         const { exportToZipFile } = (await import("./src/services/export/zip.js")).default; | 
					
						
							| 
									
										
										
										
											2025-04-09 15:48:03 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  |         const exportOpts: AdvancedExportOptions = {}; | 
					
						
							|  |  |  |         if (format === "html") { | 
					
						
							| 
									
										
										
										
											2025-04-12 00:27:15 +03:00
										 |  |  |             exportOpts.skipHtmlTemplate = true; | 
					
						
							| 
									
										
										
										
											2025-04-09 15:48:03 +03:00
										 |  |  |             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; | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 }; | 
					
						
							|  |  |  |             }; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-11 14:10:47 +03:00
										 |  |  |         await exportToZipFile(noteId, format, zipFilePath, exportOpts); | 
					
						
							| 
									
										
										
										
											2025-03-30 21:37:10 +03:00
										 |  |  |         await extractZip(zipFilePath, outputPath); | 
					
						
							| 
									
										
										
										
											2025-03-10 16:31:44 +02:00
										 |  |  |     } finally { | 
					
						
							|  |  |  |         if (await fsExtra.exists(zipFilePath)) { | 
					
						
							|  |  |  |             await fsExtra.rm(zipFilePath); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-03-10 16:37:39 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-29 11:25:28 +02:00
										 |  |  |     await cleanUpMeta(outputPath); | 
					
						
							| 
									
										
										
										
											2025-03-10 16:37:39 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-29 11:25:28 +02:00
										 |  |  | async function cleanUpMeta(outputPath: string) { | 
					
						
							|  |  |  |     const metaPath = path.join(outputPath, "!!!meta.json"); | 
					
						
							| 
									
										
										
										
											2025-03-10 16:37:39 +02:00
										 |  |  |     const meta = JSON.parse(await fs.readFile(metaPath, "utf-8")) as NoteMetaFile; | 
					
						
							|  |  |  |     for (const file of meta.files) { | 
					
						
							| 
									
										
										
										
											2025-03-29 11:28:31 +02:00
										 |  |  |         file.notePosition = 1; | 
					
						
							| 
									
										
										
										
											2025-03-10 16:37:39 +02:00
										 |  |  |         traverse(file); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function traverse(el: NoteMeta) { | 
					
						
							|  |  |  |         for (const child of el.children || []) { | 
					
						
							|  |  |  |             traverse(child); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         el.isExpanded = false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     await fs.writeFile(metaPath, JSON.stringify(meta, null, 4)); | 
					
						
							| 
									
										
										
										
											2025-02-28 22:02:29 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-11 20:39:30 +02:00
										 |  |  | async function registerHandlers() { | 
					
						
							|  |  |  |     const events = (await import("./src/services/events.js")).default; | 
					
						
							| 
									
										
										
										
											2025-03-16 14:45:15 +02:00
										 |  |  |     const eraseService = (await import("./src/services/erase.js")).default; | 
					
						
							| 
									
										
										
										
											2025-03-11 21:58:32 +02:00
										 |  |  |     const debouncer = debounce(async () => { | 
					
						
							| 
									
										
										
										
											2025-04-04 20:35:55 +03:00
										 |  |  |         eraseService.eraseUnusedAttachmentsNow(); | 
					
						
							| 
									
										
										
										
											2025-04-11 14:10:47 +03:00
										 |  |  |         await exportData(NOTE_ID_USER_GUIDE, "markdown", markdownPath); | 
					
						
							|  |  |  |         await exportData(NOTE_ID_USER_GUIDE, "html", htmlPath); | 
					
						
							|  |  |  |         await exportData(NOTE_ID_RELEASE_NOTES, "markdown", releaseNotesPath); | 
					
						
							| 
									
										
										
										
											2025-04-04 18:35:29 +03:00
										 |  |  |     }, 10_000); | 
					
						
							| 
									
										
										
										
											2025-03-11 20:39:30 +02:00
										 |  |  |     events.subscribe(events.ENTITY_CHANGED, async (e) => { | 
					
						
							| 
									
										
										
										
											2025-03-11 20:40:25 +02:00
										 |  |  |         if (e.entityName === "options") { | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-11 20:39:30 +02:00
										 |  |  |         console.log("Got entity changed ", e); | 
					
						
							|  |  |  |         debouncer(); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-28 22:02:29 +02:00
										 |  |  | await main(); |