import { WebApplication } from '@/Application/WebApplication' import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter' import { NoteType, PrefKey, SNNote, PrefDefaults, FileItem, PrefValue } from '@standardnotes/snjs' import { WebApplicationInterface, parseAndCreateZippableFileName } from '@standardnotes/ui-services' import { ZipDirectoryEntry } from '@zip.js/zip.js' // @ts-expect-error Using inline loaders to load CSS as string import superEditorCSS from '!css-loader?{"sourceMap":false}!sass-loader!../Components/SuperEditor/Lexical/Theme/editor.scss' // @ts-expect-error Using inline loaders to load CSS as string import snColorsCSS from '!css-loader?{"sourceMap":false}!sass-loader!@standardnotes/styles/src/Styles/_colors.scss' // @ts-expect-error Using inline loaders to load CSS as string import exportOverridesCSS from '!css-loader?{"sourceMap":false}!sass-loader!../Components/SuperEditor/Lexical/Theme/export-overrides.scss' import { getBase64FromBlob } from './Utils' import { parseFileName } from '@standardnotes/filepicker' export const getNoteFormat = (application: WebApplicationInterface, note: SNNote) => { if (note.noteType === NoteType.Super) { const superNoteExportFormatPref = application.getPreference( PrefKey.SuperNoteExportFormat, PrefDefaults[PrefKey.SuperNoteExportFormat], ) return superNoteExportFormatPref } const editor = application.componentManager.editorForNote(note) return editor.fileType } export const getNoteFileName = (application: WebApplicationInterface, note: SNNote): string => { const format = getNoteFormat(application, note) return `${note.title}.${format}` } const headlessSuperConverter = new HeadlessSuperConverter() const superHTML = (note: SNNote, content: string) => ` ${note.title} ${content} ` const superMarkdown = (note: SNNote, content: string) => `--- title: ${note.title} created_at: ${note.created_at.toISOString()} updated_at: ${note.serverUpdatedAt.toISOString()} uuid: ${note.uuid} --- ${content} ` export const getNoteBlob = async ( application: WebApplication, note: SNNote, superEmbedBehavior: PrefValue[PrefKey.SuperNoteExportEmbedBehavior], ) => { const format = getNoteFormat(application, note) let type: string switch (format) { case 'html': type = 'text/html' break case 'json': type = 'application/json' break case 'md': type = 'text/markdown' break case 'pdf': type = 'application/pdf' break default: type = 'text/plain' break } if (note.noteType === NoteType.Super) { const content = await headlessSuperConverter.convertSuperStringToOtherFormat(note.text, format, { embedBehavior: superEmbedBehavior, getFileItem: (id) => application.items.findItem(id), getFileBase64: async (id) => { const fileItem = application.items.findItem(id) if (!fileItem) { return } const fileBlob = await application.filesController.getFileBlob(fileItem) if (!fileBlob) { return } return await getBase64FromBlob(fileBlob) }, }) const useMDFrontmatter = format === 'md' && application.getPreference( PrefKey.SuperNoteExportUseMDFrontmatter, PrefDefaults[PrefKey.SuperNoteExportUseMDFrontmatter], ) // result is a data url string if format is pdf const result = format === 'html' ? superHTML(note, content) : useMDFrontmatter ? superMarkdown(note, content) : content const blob = format === 'pdf' ? await fetch(result).then((res) => res.blob()) : new Blob([result], { type, }) return blob } const blob = new Blob([note.text], { type, }) return blob } const isSuperNote = (note: SNNote) => { return note.noteType === NoteType.Super } export const noteHasEmbeddedFiles = (note: SNNote) => { return note.text.includes('"type":"snfile"') } const noteRequiresFolder = ( note: SNNote, superExportFormat: PrefValue[PrefKey.SuperNoteExportFormat], superEmbedBehavior: PrefValue[PrefKey.SuperNoteExportEmbedBehavior], ) => { if (!isSuperNote(note)) { return false } if (superExportFormat === 'json' || superExportFormat === 'pdf') { return false } if (superEmbedBehavior !== 'separate') { return false } return noteHasEmbeddedFiles(note) } const addEmbeddedFilesToFolder = async (application: WebApplication, note: SNNote, folder: ZipDirectoryEntry) => { try { const embeddedFileIDs = headlessSuperConverter.getEmbeddedFileIDsFromSuperString(note.text) for (const embeddedFileID of embeddedFileIDs) { const fileItem = application.items.findItem(embeddedFileID) if (!fileItem) { continue } const embeddedFileBlob = await application.filesController.getFileBlob(fileItem) if (!embeddedFileBlob) { continue } folder.addBlob(parseAndCreateZippableFileName(fileItem.title), embeddedFileBlob) } } catch (error) { console.error(error) } } export const createNoteExport = async ( application: WebApplication, notes: SNNote[], ): Promise< | { blob: Blob fileName: string } | undefined > => { if (notes.length === 0) { return } const superExportFormatPref = application.getPreference( PrefKey.SuperNoteExportFormat, PrefDefaults[PrefKey.SuperNoteExportFormat], ) const superEmbedBehaviorPref = superExportFormatPref === 'pdf' ? 'inline' : application.getPreference( PrefKey.SuperNoteExportEmbedBehavior, PrefDefaults[PrefKey.SuperNoteExportEmbedBehavior], ) if (notes.length === 1 && !noteRequiresFolder(notes[0], superExportFormatPref, superEmbedBehaviorPref)) { const blob = await getNoteBlob(application, notes[0], superEmbedBehaviorPref) const fileName = getNoteFileName(application, notes[0]) return { blob, fileName, } } const zip = await import('@zip.js/zip.js') const zipFS = new zip.fs.FS() const { root } = zipFS if (notes.length === 1 && noteRequiresFolder(notes[0], superExportFormatPref, superEmbedBehaviorPref)) { const blob = await getNoteBlob(application, notes[0], superEmbedBehaviorPref) const fileName = parseAndCreateZippableFileName(getNoteFileName(application, notes[0])) root.addBlob(fileName, blob) await addEmbeddedFilesToFolder(application, notes[0], root) const zippedBlob = await zipFS.exportBlob() return { blob: zippedBlob, fileName: fileName + '.zip', } } const filenameCounts: Record = {} for (const note of notes) { const blob = await getNoteBlob(application, note, superEmbedBehaviorPref) const _name = getNoteFileName(application, note) filenameCounts[_name] = filenameCounts[_name] == undefined ? 0 : filenameCounts[_name] + 1 const currentFileNameIndex = filenameCounts[_name] const fileName = parseAndCreateZippableFileName(_name, currentFileNameIndex > 0 ? ` - ${currentFileNameIndex}` : '') if (!noteRequiresFolder(note, superExportFormatPref, superEmbedBehaviorPref)) { root.addBlob(fileName, blob) continue } const { name } = parseFileName(fileName) const folder = root.addDirectory(name) folder.addBlob(fileName, blob) await addEmbeddedFilesToFolder(application, note, folder) } const zippedBlob = await zipFS.exportBlob() return { blob: zippedBlob, fileName: `Standard Notes Export - ${application.archiveService.formattedDateForExports()}.zip`, } }