diff --git a/packages/files/src/Domain/Service/BackupServiceInterface.ts b/packages/files/src/Domain/Service/BackupServiceInterface.ts index c00e9e1c3..9453bcfd6 100644 --- a/packages/files/src/Domain/Service/BackupServiceInterface.ts +++ b/packages/files/src/Domain/Service/BackupServiceInterface.ts @@ -1,11 +1,13 @@ import { OnChunkCallback } from '../Chunker/OnChunkCallback' import { DesktopWatchedDirectoriesChanges } from '../Device/DesktopWatchedChanges' import { FileBackupRecord } from '../Device/FileBackupsMapping' +import { SuperConverterServiceInterface } from './SuperConverterServiceInterface' export interface BackupServiceInterface { openAllDirectoriesContainingBackupFiles(): void prependWorkspacePathForPath(path: string): string importWatchedDirectoryChanges(changes: DesktopWatchedDirectoriesChanges): Promise + setSuperConverter(converter: SuperConverterServiceInterface): void getFileBackupInfo(file: { uuid: string }): Promise readEncryptedFileFromBackup(uuid: string, onChunk: OnChunkCallback): Promise<'success' | 'failed' | 'aborted'> diff --git a/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts b/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts new file mode 100644 index 000000000..2f1fa91f5 --- /dev/null +++ b/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts @@ -0,0 +1,3 @@ +export interface SuperConverterServiceInterface { + convertString: (superString: string, toFormat: 'txt' | 'md' | 'html' | 'json') => string +} diff --git a/packages/files/src/Domain/index.ts b/packages/files/src/Domain/index.ts index 1c7e7be80..405e6c28b 100644 --- a/packages/files/src/Domain/index.ts +++ b/packages/files/src/Domain/index.ts @@ -17,6 +17,7 @@ export * from './Device/FileBackupsMapping' export * from './Operations/DownloadAndDecrypt' export * from './Operations/EncryptAndUpload' export * from './Service/BackupServiceInterface' +export * from './Service/SuperConverterServiceInterface' export * from './Service/FilesClientInterface' export * from './Service/ReadAndDecryptBackupFileFileSystemAPI' export * from './Service/ReadAndDecryptBackupFileUsingBackupService' diff --git a/packages/services/package.json b/packages/services/package.json index fba0ef9b0..09f0ccbc6 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -20,6 +20,7 @@ "@standardnotes/common": "^1.46.4", "@standardnotes/domain-core": "^1.12.0", "@standardnotes/encryption": "workspace:^", + "@standardnotes/features": "workspace:^", "@standardnotes/files": "workspace:^", "@standardnotes/models": "workspace:^", "@standardnotes/responses": "workspace:*", diff --git a/packages/services/src/Domain/Backups/BackupService.ts b/packages/services/src/Domain/Backups/BackupService.ts index 54534d8a9..e5f7ce28c 100644 --- a/packages/services/src/Domain/Backups/BackupService.ts +++ b/packages/services/src/Domain/Backups/BackupService.ts @@ -1,3 +1,4 @@ +import { NoteType } from '@standardnotes/features' import { ApplicationStage } from './../Application/ApplicationStage' import { ContentType } from '@standardnotes/common' import { EncryptionProviderInterface } from '@standardnotes/encryption' @@ -20,6 +21,7 @@ import { OnChunkCallback, BackupServiceInterface, DesktopWatchedDirectoriesChanges, + SuperConverterServiceInterface, } from '@standardnotes/files' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface' @@ -44,6 +46,8 @@ export class FilesBackupService extends AbstractService implements BackupService private pendingFiles = new Set() private mappingCache?: FileBackupsMapping['files'] + private markdownConverter!: SuperConverterServiceInterface + constructor( private items: ItemManagerInterface, private api: FilesApiInterface, @@ -89,6 +93,10 @@ export class FilesBackupService extends AbstractService implements BackupService }) } + setSuperConverter(converter: SuperConverterServiceInterface): void { + this.markdownConverter = converter + } + async importWatchedDirectoryChanges(changes: DesktopWatchedDirectoriesChanges): Promise { for (const change of changes) { const existingItem = this.items.findItem(change.itemUuid) @@ -419,10 +427,15 @@ export class FilesBackupService extends AbstractService implements BackupService throw new ClientDisplayableError('No plaintext backups location found') } + if (!this.markdownConverter) { + throw 'Super markdown converter not initialized' + } + for (const note of notes) { const tags = this.items.getSortedTagsForItem(note) const tagNames = tags.map((tag) => this.items.getTagLongTitle(tag)) - await this.device.savePlaintextNoteBackup(location, note.uuid, note.title, tagNames, note.text) + const text = note.noteType === NoteType.Super ? this.markdownConverter.convertString(note.text, 'md') : note.text + await this.device.savePlaintextNoteBackup(location, note.uuid, note.title, tagNames, text) } await this.device.persistPlaintextBackupsMappingFile(location) diff --git a/packages/web/src/javascripts/Application/Device/DesktopManager.ts b/packages/web/src/javascripts/Application/Device/DesktopManager.ts index d8402df00..365607cd3 100644 --- a/packages/web/src/javascripts/Application/Device/DesktopManager.ts +++ b/packages/web/src/javascripts/Application/Device/DesktopManager.ts @@ -1,3 +1,4 @@ +import { InvisibleSuperConverter } from '@/Components/SuperEditor/Tools/InvisibleMarkdownConverter' import { SNComponent, ComponentMutator, @@ -38,6 +39,9 @@ export class DesktopManager private backups: BackupServiceInterface, ) { super(application, new InternalEventBus()) + + const markdownConverter = new InvisibleSuperConverter() + backups.setSuperConverter(markdownConverter) } async handleWatchedDirectoriesChanges(changes: DesktopWatchedDirectoriesChanges): Promise { diff --git a/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx b/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx index 9c05da220..ca0f9aa0c 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx @@ -6,7 +6,7 @@ import Icon from '../Icon/Icon' import Modal, { ModalAction } from '../Modal/Modal' import { EditorMenuItem } from '../NotesOptions/EditorMenuItem' import { NoteViewController } from '../NoteView/Controller/NoteViewController' -import { exportSuperNote } from './SuperNoteExporter' +import { InvisibleSuperConverter } from './Tools/InvisibleMarkdownConverter' const SuperNoteConverter = ({ note, @@ -47,7 +47,7 @@ const SuperNoteConverter = ({ return note.text } - return exportSuperNote(note, format) + return new InvisibleSuperConverter().convertString(note.text, format) }, [format, note]) const componentViewer = useMemo(() => { diff --git a/packages/web/src/javascripts/Components/SuperEditor/SuperNoteExporter.ts b/packages/web/src/javascripts/Components/SuperEditor/SuperNoteExporter.ts deleted file mode 100644 index 1dca7e091..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/SuperNoteExporter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createHeadlessEditor } from '@lexical/headless' -import { $convertToMarkdownString } from '@lexical/markdown' -import { MarkdownTransformers } from './MarkdownTransformers' -import { $generateHtmlFromNodes } from '@lexical/html' -import { BlockEditorNodes } from './Lexical/Nodes/AllNodes' -import BlocksEditorTheme from './Lexical/Theme/Theme' -import { SNNote } from '@standardnotes/models' - -export const exportSuperNote = (note: SNNote, format: 'txt' | 'md' | 'html' | 'json') => { - const headlessEditor = createHeadlessEditor({ - namespace: 'BlocksEditor', - theme: BlocksEditorTheme, - editable: false, - onError: (error: Error) => console.error(error), - nodes: [...BlockEditorNodes], - }) - - headlessEditor.setEditorState(headlessEditor.parseEditorState(note.text)) - - let content: string | undefined - - headlessEditor.update(() => { - switch (format) { - case 'txt': - case 'md': - content = $convertToMarkdownString(MarkdownTransformers) - break - case 'html': - content = $generateHtmlFromNodes(headlessEditor) - break - case 'json': - default: - content = note.text - break - } - }) - - if (!content) { - throw new Error('Could not export note') - } - - return content -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Tools/InvisibleMarkdownConverter.tsx b/packages/web/src/javascripts/Components/SuperEditor/Tools/InvisibleMarkdownConverter.tsx new file mode 100644 index 000000000..b12fee497 --- /dev/null +++ b/packages/web/src/javascripts/Components/SuperEditor/Tools/InvisibleMarkdownConverter.tsx @@ -0,0 +1,57 @@ +import { createHeadlessEditor } from '@lexical/headless' +import { $convertToMarkdownString } from '@lexical/markdown' +import { SuperConverterServiceInterface } from '@standardnotes/snjs' +import { LexicalEditor } from 'lexical' +import BlocksEditorTheme from '../Lexical/Theme/Theme' +import { BlockEditorNodes } from '../Lexical/Nodes/AllNodes' +import { MarkdownTransformers } from '../MarkdownTransformers' +import { $generateHtmlFromNodes } from '@lexical/html' + +export class InvisibleSuperConverter implements SuperConverterServiceInterface { + private editor: LexicalEditor + + constructor() { + this.editor = createHeadlessEditor({ + namespace: 'BlocksEditor', + theme: BlocksEditorTheme, + editable: false, + onError: (error: Error) => console.error(error), + nodes: [...BlockEditorNodes], + }) + } + + convertString(superString: string, format: 'txt' | 'md' | 'html' | 'json'): string { + if (superString.length === 0) { + return superString + } + + this.editor.setEditorState(this.editor.parseEditorState(superString)) + + let content: string | undefined + + this.editor.update( + () => { + switch (format) { + case 'txt': + case 'md': + content = $convertToMarkdownString(MarkdownTransformers) + break + case 'html': + content = $generateHtmlFromNodes(this.editor) + break + case 'json': + default: + content = superString + break + } + }, + { discrete: true }, + ) + + if (!content) { + throw new Error('Could not export note') + } + + return content + } +} diff --git a/packages/web/src/javascripts/Utils/NoteExportUtils.ts b/packages/web/src/javascripts/Utils/NoteExportUtils.ts index 5ffb19fdc..7b1f278aa 100644 --- a/packages/web/src/javascripts/Utils/NoteExportUtils.ts +++ b/packages/web/src/javascripts/Utils/NoteExportUtils.ts @@ -1,5 +1,5 @@ import { WebApplication } from '@/Application/Application' -import { exportSuperNote } from '@/Components/SuperEditor/SuperNoteExporter' +import { InvisibleSuperConverter } from '@/Components/SuperEditor/Tools/InvisibleMarkdownConverter' import { NoteType, PrefKey, SNNote } from '@standardnotes/snjs' export const getNoteFormat = (application: WebApplication, note: SNNote) => { @@ -38,7 +38,8 @@ export const getNoteBlob = (application: WebApplication, note: SNNote) => { type = 'text/plain' break } - const content = note.noteType === NoteType.Super ? exportSuperNote(note, format) : note.text + const content = + note.noteType === NoteType.Super ? new InvisibleSuperConverter().convertString(note.text, format) : note.text const blob = new Blob([content], { type, }) diff --git a/yarn.lock b/yarn.lock index 76ac1092f..694209f72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5235,6 +5235,7 @@ __metadata: "@standardnotes/common": ^1.46.4 "@standardnotes/domain-core": ^1.12.0 "@standardnotes/encryption": "workspace:^" + "@standardnotes/features": "workspace:^" "@standardnotes/files": "workspace:^" "@standardnotes/models": "workspace:^" "@standardnotes/responses": "workspace:*"