fix: automatically convert Super notes to Markdown for the Plaintext Backups feature
This commit is contained in:
@@ -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<void>
|
||||
setSuperConverter(converter: SuperConverterServiceInterface): void
|
||||
|
||||
getFileBackupInfo(file: { uuid: string }): Promise<FileBackupRecord | undefined>
|
||||
readEncryptedFileFromBackup(uuid: string, onChunk: OnChunkCallback): Promise<'success' | 'failed' | 'aborted'>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface SuperConverterServiceInterface {
|
||||
convertString: (superString: string, toFormat: 'txt' | 'md' | 'html' | 'json') => string
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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<string>()
|
||||
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<void> {
|
||||
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)
|
||||
|
||||
@@ -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<void> {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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:*"
|
||||
|
||||
Reference in New Issue
Block a user