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`,
}
}