fix: automatically convert Super notes to Markdown for the Plaintext Backups feature

This commit is contained in:
Mo
2023-05-02 15:37:23 -05:00
parent 2eddc3a1c6
commit 75ecf4c393
11 changed files with 88 additions and 48 deletions

View File

@@ -1,11 +1,13 @@
import { OnChunkCallback } from '../Chunker/OnChunkCallback' import { OnChunkCallback } from '../Chunker/OnChunkCallback'
import { DesktopWatchedDirectoriesChanges } from '../Device/DesktopWatchedChanges' import { DesktopWatchedDirectoriesChanges } from '../Device/DesktopWatchedChanges'
import { FileBackupRecord } from '../Device/FileBackupsMapping' import { FileBackupRecord } from '../Device/FileBackupsMapping'
import { SuperConverterServiceInterface } from './SuperConverterServiceInterface'
export interface BackupServiceInterface { export interface BackupServiceInterface {
openAllDirectoriesContainingBackupFiles(): void openAllDirectoriesContainingBackupFiles(): void
prependWorkspacePathForPath(path: string): string prependWorkspacePathForPath(path: string): string
importWatchedDirectoryChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void> importWatchedDirectoryChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void>
setSuperConverter(converter: SuperConverterServiceInterface): void
getFileBackupInfo(file: { uuid: string }): Promise<FileBackupRecord | undefined> getFileBackupInfo(file: { uuid: string }): Promise<FileBackupRecord | undefined>
readEncryptedFileFromBackup(uuid: string, onChunk: OnChunkCallback): Promise<'success' | 'failed' | 'aborted'> readEncryptedFileFromBackup(uuid: string, onChunk: OnChunkCallback): Promise<'success' | 'failed' | 'aborted'>

View File

@@ -0,0 +1,3 @@
export interface SuperConverterServiceInterface {
convertString: (superString: string, toFormat: 'txt' | 'md' | 'html' | 'json') => string
}

View File

@@ -17,6 +17,7 @@ export * from './Device/FileBackupsMapping'
export * from './Operations/DownloadAndDecrypt' export * from './Operations/DownloadAndDecrypt'
export * from './Operations/EncryptAndUpload' export * from './Operations/EncryptAndUpload'
export * from './Service/BackupServiceInterface' export * from './Service/BackupServiceInterface'
export * from './Service/SuperConverterServiceInterface'
export * from './Service/FilesClientInterface' export * from './Service/FilesClientInterface'
export * from './Service/ReadAndDecryptBackupFileFileSystemAPI' export * from './Service/ReadAndDecryptBackupFileFileSystemAPI'
export * from './Service/ReadAndDecryptBackupFileUsingBackupService' export * from './Service/ReadAndDecryptBackupFileUsingBackupService'

View File

@@ -20,6 +20,7 @@
"@standardnotes/common": "^1.46.4", "@standardnotes/common": "^1.46.4",
"@standardnotes/domain-core": "^1.12.0", "@standardnotes/domain-core": "^1.12.0",
"@standardnotes/encryption": "workspace:^", "@standardnotes/encryption": "workspace:^",
"@standardnotes/features": "workspace:^",
"@standardnotes/files": "workspace:^", "@standardnotes/files": "workspace:^",
"@standardnotes/models": "workspace:^", "@standardnotes/models": "workspace:^",
"@standardnotes/responses": "workspace:*", "@standardnotes/responses": "workspace:*",

View File

@@ -1,3 +1,4 @@
import { NoteType } from '@standardnotes/features'
import { ApplicationStage } from './../Application/ApplicationStage' import { ApplicationStage } from './../Application/ApplicationStage'
import { ContentType } from '@standardnotes/common' import { ContentType } from '@standardnotes/common'
import { EncryptionProviderInterface } from '@standardnotes/encryption' import { EncryptionProviderInterface } from '@standardnotes/encryption'
@@ -20,6 +21,7 @@ import {
OnChunkCallback, OnChunkCallback,
BackupServiceInterface, BackupServiceInterface,
DesktopWatchedDirectoriesChanges, DesktopWatchedDirectoriesChanges,
SuperConverterServiceInterface,
} from '@standardnotes/files' } from '@standardnotes/files'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface'
@@ -44,6 +46,8 @@ export class FilesBackupService extends AbstractService implements BackupService
private pendingFiles = new Set<string>() private pendingFiles = new Set<string>()
private mappingCache?: FileBackupsMapping['files'] private mappingCache?: FileBackupsMapping['files']
private markdownConverter!: SuperConverterServiceInterface
constructor( constructor(
private items: ItemManagerInterface, private items: ItemManagerInterface,
private api: FilesApiInterface, 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> { async importWatchedDirectoryChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void> {
for (const change of changes) { for (const change of changes) {
const existingItem = this.items.findItem(change.itemUuid) 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') throw new ClientDisplayableError('No plaintext backups location found')
} }
if (!this.markdownConverter) {
throw 'Super markdown converter not initialized'
}
for (const note of notes) { for (const note of notes) {
const tags = this.items.getSortedTagsForItem(note) const tags = this.items.getSortedTagsForItem(note)
const tagNames = tags.map((tag) => this.items.getTagLongTitle(tag)) 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) await this.device.persistPlaintextBackupsMappingFile(location)

View File

@@ -1,3 +1,4 @@
import { InvisibleSuperConverter } from '@/Components/SuperEditor/Tools/InvisibleMarkdownConverter'
import { import {
SNComponent, SNComponent,
ComponentMutator, ComponentMutator,
@@ -38,6 +39,9 @@ export class DesktopManager
private backups: BackupServiceInterface, private backups: BackupServiceInterface,
) { ) {
super(application, new InternalEventBus()) super(application, new InternalEventBus())
const markdownConverter = new InvisibleSuperConverter()
backups.setSuperConverter(markdownConverter)
} }
async handleWatchedDirectoriesChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void> { async handleWatchedDirectoriesChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void> {

View File

@@ -6,7 +6,7 @@ import Icon from '../Icon/Icon'
import Modal, { ModalAction } from '../Modal/Modal' import Modal, { ModalAction } from '../Modal/Modal'
import { EditorMenuItem } from '../NotesOptions/EditorMenuItem' import { EditorMenuItem } from '../NotesOptions/EditorMenuItem'
import { NoteViewController } from '../NoteView/Controller/NoteViewController' import { NoteViewController } from '../NoteView/Controller/NoteViewController'
import { exportSuperNote } from './SuperNoteExporter' import { InvisibleSuperConverter } from './Tools/InvisibleMarkdownConverter'
const SuperNoteConverter = ({ const SuperNoteConverter = ({
note, note,
@@ -47,7 +47,7 @@ const SuperNoteConverter = ({
return note.text return note.text
} }
return exportSuperNote(note, format) return new InvisibleSuperConverter().convertString(note.text, format)
}, [format, note]) }, [format, note])
const componentViewer = useMemo(() => { const componentViewer = useMemo(() => {

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -1,5 +1,5 @@
import { WebApplication } from '@/Application/Application' 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' import { NoteType, PrefKey, SNNote } from '@standardnotes/snjs'
export const getNoteFormat = (application: WebApplication, note: SNNote) => { export const getNoteFormat = (application: WebApplication, note: SNNote) => {
@@ -38,7 +38,8 @@ export const getNoteBlob = (application: WebApplication, note: SNNote) => {
type = 'text/plain' type = 'text/plain'
break 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], { const blob = new Blob([content], {
type, type,
}) })

View File

@@ -5235,6 +5235,7 @@ __metadata:
"@standardnotes/common": ^1.46.4 "@standardnotes/common": ^1.46.4
"@standardnotes/domain-core": ^1.12.0 "@standardnotes/domain-core": ^1.12.0
"@standardnotes/encryption": "workspace:^" "@standardnotes/encryption": "workspace:^"
"@standardnotes/features": "workspace:^"
"@standardnotes/files": "workspace:^" "@standardnotes/files": "workspace:^"
"@standardnotes/models": "workspace:^" "@standardnotes/models": "workspace:^"
"@standardnotes/responses": "workspace:*" "@standardnotes/responses": "workspace:*"