refactor: importer service (#2674)
This commit is contained in:
@@ -1,18 +1,9 @@
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter'
|
||||
import data from './testData'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
|
||||
describe('AegisConverter', () => {
|
||||
const crypto = {
|
||||
generateUUID: () => String(Math.random()),
|
||||
} as unknown as PureCryptoInterface
|
||||
|
||||
const generateUuid = new GenerateUuid(crypto)
|
||||
|
||||
it('should parse entries', () => {
|
||||
const converter = new AegisToAuthenticatorConverter(generateUuid)
|
||||
const converter = new AegisToAuthenticatorConverter()
|
||||
|
||||
const result = converter.parseEntries(data)
|
||||
|
||||
@@ -31,58 +22,4 @@ describe('AegisConverter', () => {
|
||||
notes: 'Some other service',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create note from entries with editor info', () => {
|
||||
const converter = new AegisToAuthenticatorConverter(generateUuid)
|
||||
|
||||
const parsedEntries = converter.parseEntries(data)
|
||||
|
||||
const result = converter.createNoteFromEntries(
|
||||
parsedEntries!,
|
||||
{
|
||||
lastModified: 123456789,
|
||||
name: 'test.json',
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result.content_type).toBe('Note')
|
||||
expect(result.created_at).toBeInstanceOf(Date)
|
||||
expect(result.updated_at).toBeInstanceOf(Date)
|
||||
expect(result.uuid).not.toBeNull()
|
||||
expect(result.content.title).toBe('test')
|
||||
expect(result.content.text).toBe(
|
||||
'[{"service":"TestMail","account":"test@test.com","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"test@test.com","secret":"SOMESERVICESOMESERVICESOMESERVIC","notes":"Some other service"}]',
|
||||
)
|
||||
expect(result.content.noteType).toBe(NoteType.Authentication)
|
||||
expect(result.content.editorIdentifier).toBe(NativeFeatureIdentifier.TYPES.TokenVaultEditor)
|
||||
})
|
||||
|
||||
it('should create note from entries without editor info', () => {
|
||||
const converter = new AegisToAuthenticatorConverter(generateUuid)
|
||||
|
||||
const parsedEntries = converter.parseEntries(data)
|
||||
|
||||
const result = converter.createNoteFromEntries(
|
||||
parsedEntries!,
|
||||
{
|
||||
lastModified: 123456789,
|
||||
name: 'test.json',
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result.content_type).toBe('Note')
|
||||
expect(result.created_at).toBeInstanceOf(Date)
|
||||
expect(result.updated_at).toBeInstanceOf(Date)
|
||||
expect(result.uuid).not.toBeNull()
|
||||
expect(result.content.title).toBe('test')
|
||||
expect(result.content.text).toBe(
|
||||
'[{"service":"TestMail","account":"test@test.com","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"test@test.com","secret":"SOMESERVICESOMESERVICESOMESERVIC","notes":"Some other service"}]',
|
||||
)
|
||||
expect(result.content.noteType).toBeFalsy()
|
||||
expect(result.content.editorIdentifier).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
import { Converter } from '../Converter'
|
||||
|
||||
type AegisData = {
|
||||
db: {
|
||||
@@ -26,19 +23,29 @@ type AuthenticatorEntry = {
|
||||
notes: string
|
||||
}
|
||||
|
||||
export class AegisToAuthenticatorConverter {
|
||||
constructor(private _generateUuid: GenerateUuid) {}
|
||||
export class AegisToAuthenticatorConverter implements Converter {
|
||||
constructor() {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static isValidAegisJson(json: any): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return json.db && json.db.entries && json.db.entries.every((entry: any) => AegisEntryTypes.includes(entry.type))
|
||||
getImportType(): string {
|
||||
return 'aegis'
|
||||
}
|
||||
|
||||
async convertAegisBackupFileToNote(
|
||||
file: File,
|
||||
addEditorInfo: boolean,
|
||||
): Promise<DecryptedTransferPayload<NoteContent>> {
|
||||
getSupportedFileTypes(): string[] {
|
||||
return ['application/json']
|
||||
}
|
||||
|
||||
isContentValid(content: string): boolean {
|
||||
try {
|
||||
const json = JSON.parse(content)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return json.db && json.db.entries && json.db.entries.every((entry: any) => AegisEntryTypes.includes(entry.type))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
convert: Converter['convert'] = async (file, { createNote, readFileAsText }) => {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const entries = this.parseEntries(content)
|
||||
@@ -47,34 +54,21 @@ export class AegisToAuthenticatorConverter {
|
||||
throw new Error('Could not parse entries')
|
||||
}
|
||||
|
||||
return this.createNoteFromEntries(entries, file, addEditorInfo)
|
||||
}
|
||||
const createdAt = file.lastModified ? new Date(file.lastModified) : new Date()
|
||||
const updatedAt = file.lastModified ? new Date(file.lastModified) : new Date()
|
||||
const title = file.name.split('.')[0]
|
||||
const text = JSON.stringify(entries)
|
||||
|
||||
createNoteFromEntries(
|
||||
entries: AuthenticatorEntry[],
|
||||
file: {
|
||||
lastModified: number
|
||||
name: string
|
||||
},
|
||||
addEditorInfo: boolean,
|
||||
): DecryptedTransferPayload<NoteContent> {
|
||||
return {
|
||||
created_at: new Date(file.lastModified),
|
||||
created_at_timestamp: file.lastModified,
|
||||
updated_at: new Date(file.lastModified),
|
||||
updated_at_timestamp: file.lastModified,
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
title: file.name.split('.')[0],
|
||||
text: JSON.stringify(entries),
|
||||
references: [],
|
||||
...(addEditorInfo && {
|
||||
noteType: NoteType.Authentication,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor,
|
||||
}),
|
||||
},
|
||||
}
|
||||
return [
|
||||
createNote({
|
||||
createdAt,
|
||||
updatedAt,
|
||||
title,
|
||||
text,
|
||||
noteType: NoteType.Authentication,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
parseEntries(data: string): AuthenticatorEntry[] | null {
|
||||
|
||||
41
packages/ui-services/src/Import/Converter.ts
Normal file
41
packages/ui-services/src/Import/Converter.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import { DecryptedTransferPayload, ItemContent, NoteContent, TagContent } from '@standardnotes/models'
|
||||
|
||||
export interface Converter {
|
||||
getImportType(): string
|
||||
|
||||
getSupportedFileTypes?: () => string[]
|
||||
getFileExtension?: () => string
|
||||
|
||||
isContentValid: (content: string) => boolean
|
||||
|
||||
convert(
|
||||
file: File,
|
||||
dependencies: {
|
||||
createNote: CreateNoteFn
|
||||
createTag: CreateTagFn
|
||||
canUseSuper: boolean
|
||||
convertHTMLToSuper: (html: string) => string
|
||||
convertMarkdownToSuper: (markdown: string) => string
|
||||
readFileAsText: (file: File) => Promise<string>
|
||||
},
|
||||
): Promise<DecryptedTransferPayload<ItemContent>[]>
|
||||
}
|
||||
|
||||
export type CreateNoteFn = (options: {
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
title: string
|
||||
text: string
|
||||
noteType?: NoteType
|
||||
archived?: boolean
|
||||
pinned?: boolean
|
||||
trashed?: boolean
|
||||
editorIdentifier?: NoteContent['editorIdentifier']
|
||||
}) => DecryptedTransferPayload<NoteContent>
|
||||
|
||||
export type CreateTagFn = (options: {
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
title: string
|
||||
}) => DecryptedTransferPayload<TagContent>
|
||||
@@ -3,12 +3,12 @@
|
||||
*/
|
||||
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { DecryptedTransferPayload, FileItem, NoteContent, TagContent } from '@standardnotes/models'
|
||||
import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models'
|
||||
import { EvernoteConverter, EvernoteResource } from './EvernoteConverter'
|
||||
import { createTestResourceElement, enex, enexWithNoNoteOrTag } from './testData'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
import { SuperConverterServiceInterface } from '@standardnotes/files'
|
||||
import { Converter } from '../Converter'
|
||||
|
||||
// Mock dayjs so dayjs.extend() doesn't throw an error in EvernoteConverter.ts
|
||||
jest.mock('dayjs', () => {
|
||||
@@ -28,43 +28,43 @@ describe('EvernoteConverter', () => {
|
||||
generateUUID: () => String(Math.random()),
|
||||
} as unknown as PureCryptoInterface
|
||||
|
||||
const superConverterService: SuperConverterServiceInterface = {
|
||||
isValidSuperString: () => true,
|
||||
convertOtherFormatToSuperString: (data: string) => data,
|
||||
convertSuperStringToOtherFormat: async (data: string) => data,
|
||||
getEmbeddedFileIDsFromSuperString: () => [],
|
||||
uploadAndReplaceInlineFilesInSuperString: async (
|
||||
superString: string,
|
||||
_uploadFile: (file: File) => Promise<FileItem | undefined>,
|
||||
_linkFile: (file: FileItem) => Promise<void>,
|
||||
_generateUuid: GenerateUuid,
|
||||
) => superString,
|
||||
}
|
||||
|
||||
const generateUuid = new GenerateUuid(crypto)
|
||||
|
||||
it('should throw error if DOMParser is not available', () => {
|
||||
const converter = new EvernoteConverter(superConverterService, generateUuid)
|
||||
const readFileAsText = async (file: File) => file as unknown as string
|
||||
|
||||
const originalDOMParser = window.DOMParser
|
||||
// @ts-ignore
|
||||
window.DOMParser = undefined
|
||||
|
||||
expect(() => converter.parseENEXData(enex)).toThrowError()
|
||||
|
||||
window.DOMParser = originalDOMParser
|
||||
})
|
||||
const dependencies: Parameters<Converter['convert']>[1] = {
|
||||
createNote: ({ text }) =>
|
||||
({
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
text,
|
||||
references: [],
|
||||
},
|
||||
}) as unknown as DecryptedTransferPayload<NoteContent>,
|
||||
createTag: ({ title }) =>
|
||||
({
|
||||
content_type: ContentType.TYPES.Tag,
|
||||
content: {
|
||||
title,
|
||||
references: [],
|
||||
},
|
||||
}) as unknown as DecryptedTransferPayload<TagContent>,
|
||||
convertHTMLToSuper: (data) => data,
|
||||
convertMarkdownToSuper: jest.fn(),
|
||||
readFileAsText,
|
||||
canUseSuper: false,
|
||||
}
|
||||
|
||||
it('should throw error if no note or tag in enex', () => {
|
||||
const converter = new EvernoteConverter(superConverterService, generateUuid)
|
||||
const converter = new EvernoteConverter(generateUuid)
|
||||
|
||||
expect(() => converter.parseENEXData(enexWithNoNoteOrTag)).toThrowError()
|
||||
expect(converter.convert(enexWithNoNoteOrTag as unknown as File, dependencies)).rejects.toThrowError()
|
||||
})
|
||||
|
||||
it('should parse and strip html', () => {
|
||||
const converter = new EvernoteConverter(superConverterService, generateUuid)
|
||||
it('should parse and strip html', async () => {
|
||||
const converter = new EvernoteConverter(generateUuid)
|
||||
|
||||
const result = converter.parseENEXData(enex, false)
|
||||
const result = await converter.convert(enex as unknown as File, dependencies)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.length).toBe(3)
|
||||
@@ -81,10 +81,13 @@ describe('EvernoteConverter', () => {
|
||||
expect((result?.[2] as DecryptedTransferPayload<TagContent>).content.references[1].uuid).toBe(result?.[1].uuid)
|
||||
})
|
||||
|
||||
it('should parse and not strip html', () => {
|
||||
const converter = new EvernoteConverter(superConverterService, generateUuid)
|
||||
it('should parse and not strip html', async () => {
|
||||
const converter = new EvernoteConverter(generateUuid)
|
||||
|
||||
const result = converter.parseENEXData(enex, true)
|
||||
const result = await converter.convert(enex as unknown as File, {
|
||||
...dependencies,
|
||||
canUseSuper: true,
|
||||
})
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.length).toBe(3)
|
||||
@@ -117,7 +120,7 @@ describe('EvernoteConverter', () => {
|
||||
|
||||
const array = [unorderedList1, unorderedList2]
|
||||
|
||||
const converter = new EvernoteConverter(superConverterService, generateUuid)
|
||||
const converter = new EvernoteConverter(generateUuid)
|
||||
converter.convertListsToSuperFormatIfApplicable(array)
|
||||
|
||||
expect(unorderedList1.getAttribute('__lexicallisttype')).toBe('check')
|
||||
@@ -148,14 +151,14 @@ describe('EvernoteConverter', () => {
|
||||
|
||||
const array = [mediaElement1, mediaElement2, mediaElement3]
|
||||
|
||||
const converter = new EvernoteConverter(superConverterService, generateUuid)
|
||||
const converter = new EvernoteConverter(generateUuid)
|
||||
const replacedCount = converter.replaceMediaElementsWithResources(array, resources)
|
||||
|
||||
expect(replacedCount).toBe(1)
|
||||
})
|
||||
|
||||
describe('getResourceFromElement', () => {
|
||||
const converter = new EvernoteConverter(superConverterService, generateUuid)
|
||||
const converter = new EvernoteConverter(generateUuid)
|
||||
|
||||
it('should return undefined if no mime type is present', () => {
|
||||
const resourceElementWithoutMimeType = createTestResourceElement(false)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
import { SuperConverterServiceInterface } from '@standardnotes/files'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import MD5 from 'crypto-js/md5'
|
||||
import Base64 from 'crypto-js/enc-base64'
|
||||
import { Converter } from '../Converter'
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(utc)
|
||||
|
||||
@@ -21,22 +19,28 @@ export type EvernoteResource = {
|
||||
mimeType: string
|
||||
}
|
||||
|
||||
export class EvernoteConverter {
|
||||
constructor(
|
||||
private superConverterService: SuperConverterServiceInterface,
|
||||
private _generateUuid: GenerateUuid,
|
||||
) {}
|
||||
export class EvernoteConverter implements Converter {
|
||||
constructor(private _generateUuid: GenerateUuid) {}
|
||||
|
||||
async convertENEXFileToNotesAndTags(file: File, isEntitledToSuper: boolean): Promise<DecryptedTransferPayload[]> {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const notesAndTags = this.parseENEXData(content, isEntitledToSuper)
|
||||
|
||||
return notesAndTags
|
||||
getImportType(): string {
|
||||
return 'evernote'
|
||||
}
|
||||
|
||||
parseENEXData(data: string, isEntitledToSuper = false) {
|
||||
const xmlDoc = this.loadXMLString(data, 'xml')
|
||||
getFileExtension(): string {
|
||||
return 'enex'
|
||||
}
|
||||
|
||||
isContentValid(content: string): boolean {
|
||||
return content.includes('<en-export') && content.includes('</en-export>')
|
||||
}
|
||||
|
||||
convert: Converter['convert'] = async (
|
||||
file,
|
||||
{ createNote, createTag, canUseSuper, convertHTMLToSuper, readFileAsText },
|
||||
) => {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const xmlDoc = this.loadXMLString(content, 'xml')
|
||||
const xmlNotes = xmlDoc.getElementsByTagName('note')
|
||||
const notes: DecryptedTransferPayload<NoteContent>[] = []
|
||||
const tags: DecryptedTransferPayload<TagContent>[] = []
|
||||
@@ -47,10 +51,6 @@ export class EvernoteConverter {
|
||||
})[0]
|
||||
}
|
||||
|
||||
function addTag(tag: DecryptedTransferPayload<TagContent>) {
|
||||
tags.push(tag)
|
||||
}
|
||||
|
||||
for (const [index, xmlNote] of Array.from(xmlNotes).entries()) {
|
||||
const title = xmlNote.getElementsByTagName('title')[0].textContent
|
||||
const created = xmlNote.getElementsByTagName('created')[0]?.textContent
|
||||
@@ -70,7 +70,7 @@ export class EvernoteConverter {
|
||||
const noteElement = contentXml.getElementsByTagName('en-note')[0]
|
||||
|
||||
const unorderedLists = Array.from(noteElement.getElementsByTagName('ul'))
|
||||
if (isEntitledToSuper) {
|
||||
if (canUseSuper) {
|
||||
this.convertListsToSuperFormatIfApplicable(unorderedLists)
|
||||
}
|
||||
|
||||
@@ -105,37 +105,23 @@ export class EvernoteConverter {
|
||||
}
|
||||
|
||||
let contentHTML = noteElement.innerHTML
|
||||
if (!isEntitledToSuper) {
|
||||
if (!canUseSuper) {
|
||||
contentHTML = contentHTML.replace(/<\/div>/g, '</div>\n')
|
||||
contentHTML = contentHTML.replace(/<li[^>]*>/g, '\n')
|
||||
contentHTML = contentHTML.trim()
|
||||
}
|
||||
const text = !isEntitledToSuper
|
||||
? this.stripHTML(contentHTML)
|
||||
: this.superConverterService.convertOtherFormatToSuperString(contentHTML, 'html')
|
||||
const text = !canUseSuper ? this.stripHTML(contentHTML) : convertHTMLToSuper(contentHTML)
|
||||
|
||||
const createdAtDate = created ? dayjs.utc(created, dateFormat).toDate() : new Date()
|
||||
const updatedAtDate = updated ? dayjs.utc(updated, dateFormat).toDate() : createdAtDate
|
||||
|
||||
const note: DecryptedTransferPayload<NoteContent> = {
|
||||
created_at: createdAtDate,
|
||||
created_at_timestamp: createdAtDate.getTime(),
|
||||
updated_at: updatedAtDate,
|
||||
updated_at_timestamp: updatedAtDate.getTime(),
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
title: !title ? `Imported note ${index + 1} from Evernote` : title,
|
||||
text,
|
||||
references: [],
|
||||
...(isEntitledToSuper
|
||||
? {
|
||||
noteType: NoteType.Super,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
const note = createNote({
|
||||
createdAt: createdAtDate,
|
||||
updatedAt: updatedAtDate,
|
||||
title: !title ? `Imported note ${index + 1} from Evernote` : title,
|
||||
text,
|
||||
noteType: NoteType.Super,
|
||||
})
|
||||
|
||||
const xmlTags = xmlNote.getElementsByTagName('tag')
|
||||
for (const tagXml of Array.from(xmlTags)) {
|
||||
@@ -143,21 +129,12 @@ export class EvernoteConverter {
|
||||
let tag = findTag(tagName)
|
||||
if (!tag) {
|
||||
const now = new Date()
|
||||
tag = {
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Tag,
|
||||
created_at: now,
|
||||
created_at_timestamp: now.getTime(),
|
||||
updated_at: now,
|
||||
updated_at_timestamp: now.getTime(),
|
||||
content: {
|
||||
title: tagName || `Imported tag ${index + 1} from Evernote`,
|
||||
expanded: false,
|
||||
iconString: '',
|
||||
references: [],
|
||||
},
|
||||
}
|
||||
addTag(tag)
|
||||
tag = createTag({
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
title: tagName || `Imported tag ${index + 1} from Evernote`,
|
||||
})
|
||||
tags.push(tag)
|
||||
}
|
||||
|
||||
note.content.references.push({ content_type: tag.content_type, uuid: tag.uuid })
|
||||
@@ -291,13 +268,8 @@ export class EvernoteConverter {
|
||||
}
|
||||
|
||||
loadXMLString(string: string, type: 'html' | 'xml') {
|
||||
let xmlDoc
|
||||
if (window.DOMParser) {
|
||||
const parser = new DOMParser()
|
||||
xmlDoc = parser.parseFromString(string, `text/${type}`)
|
||||
} else {
|
||||
throw new Error('Could not parse XML string')
|
||||
}
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(string, `text/${type}`)
|
||||
return xmlDoc
|
||||
}
|
||||
|
||||
|
||||
@@ -4,33 +4,30 @@
|
||||
|
||||
import { jsonTextContentData, htmlTestData, jsonListContentData } from './testData'
|
||||
import { GoogleKeepConverter } from './GoogleKeepConverter'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
import { FileItem, SuperConverterServiceInterface } from '@standardnotes/snjs'
|
||||
import { ContentType, DecryptedTransferPayload, NoteContent } from '@standardnotes/snjs'
|
||||
import { CreateNoteFn } from '../Converter'
|
||||
|
||||
describe('GoogleKeepConverter', () => {
|
||||
const crypto = {
|
||||
generateUUID: () => String(Math.random()),
|
||||
} as unknown as PureCryptoInterface
|
||||
|
||||
const superConverterService: SuperConverterServiceInterface = {
|
||||
isValidSuperString: () => true,
|
||||
convertOtherFormatToSuperString: (data: string) => data,
|
||||
convertSuperStringToOtherFormat: async (data: string) => data,
|
||||
getEmbeddedFileIDsFromSuperString: () => [],
|
||||
uploadAndReplaceInlineFilesInSuperString: async (
|
||||
superString: string,
|
||||
_uploadFile: (file: File) => Promise<FileItem | undefined>,
|
||||
_linkFile: (file: FileItem) => Promise<void>,
|
||||
_generateUuid: GenerateUuid,
|
||||
) => superString,
|
||||
}
|
||||
const generateUuid = new GenerateUuid(crypto)
|
||||
const createNote: CreateNoteFn = ({ title, text, createdAt, updatedAt, trashed, archived, pinned }) =>
|
||||
({
|
||||
uuid: Math.random().toString(),
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
title,
|
||||
text,
|
||||
trashed,
|
||||
archived,
|
||||
pinned,
|
||||
references: [],
|
||||
},
|
||||
}) as unknown as DecryptedTransferPayload<NoteContent>
|
||||
|
||||
it('should parse json data', () => {
|
||||
const converter = new GoogleKeepConverter(superConverterService, generateUuid)
|
||||
const converter = new GoogleKeepConverter()
|
||||
|
||||
const textContent = converter.tryParseAsJson(jsonTextContentData, false)
|
||||
const textContent = converter.tryParseAsJson(jsonTextContentData, createNote, (md) => md)
|
||||
|
||||
expect(textContent).not.toBeNull()
|
||||
expect(textContent?.created_at).toBeInstanceOf(Date)
|
||||
@@ -43,7 +40,7 @@ describe('GoogleKeepConverter', () => {
|
||||
expect(textContent?.content.archived).toBe(false)
|
||||
expect(textContent?.content.pinned).toBe(false)
|
||||
|
||||
const listContent = converter.tryParseAsJson(jsonListContentData, false)
|
||||
const listContent = converter.tryParseAsJson(jsonListContentData, createNote, (md) => md)
|
||||
|
||||
expect(listContent).not.toBeNull()
|
||||
expect(listContent?.created_at).toBeInstanceOf(Date)
|
||||
@@ -58,13 +55,15 @@ describe('GoogleKeepConverter', () => {
|
||||
})
|
||||
|
||||
it('should parse html data', () => {
|
||||
const converter = new GoogleKeepConverter(superConverterService, generateUuid)
|
||||
const converter = new GoogleKeepConverter()
|
||||
|
||||
const result = converter.tryParseAsHtml(
|
||||
htmlTestData,
|
||||
{
|
||||
name: 'note-2.html',
|
||||
},
|
||||
createNote,
|
||||
(html) => html,
|
||||
false,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
import { SuperConverterServiceInterface } from '@standardnotes/files'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import { Converter, CreateNoteFn } from '../Converter'
|
||||
|
||||
type Content =
|
||||
| {
|
||||
@@ -25,28 +22,44 @@ type GoogleKeepJsonNote = {
|
||||
userEditedTimestampUsec: number
|
||||
} & Content
|
||||
|
||||
export class GoogleKeepConverter {
|
||||
constructor(
|
||||
private superConverterService: SuperConverterServiceInterface,
|
||||
private _generateUuid: GenerateUuid,
|
||||
) {}
|
||||
export class GoogleKeepConverter implements Converter {
|
||||
constructor() {}
|
||||
|
||||
async convertGoogleKeepBackupFileToNote(
|
||||
file: File,
|
||||
isEntitledToSuper: boolean,
|
||||
): Promise<DecryptedTransferPayload<NoteContent>> {
|
||||
const content = await readFileAsText(file)
|
||||
getImportType(): string {
|
||||
return 'google-keep'
|
||||
}
|
||||
|
||||
const possiblePayloadFromJson = this.tryParseAsJson(content, isEntitledToSuper)
|
||||
getSupportedFileTypes(): string[] {
|
||||
return ['text/html', 'application/json']
|
||||
}
|
||||
|
||||
if (possiblePayloadFromJson) {
|
||||
return possiblePayloadFromJson
|
||||
isContentValid(content: string): boolean {
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
return GoogleKeepConverter.isValidGoogleKeepJson(parsed)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
const possiblePayloadFromHtml = this.tryParseAsHtml(content, file, isEntitledToSuper)
|
||||
return false
|
||||
}
|
||||
|
||||
convert: Converter['convert'] = async (
|
||||
file,
|
||||
{ createNote, canUseSuper, convertHTMLToSuper, convertMarkdownToSuper, readFileAsText },
|
||||
) => {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const possiblePayloadFromJson = this.tryParseAsJson(content, createNote, convertMarkdownToSuper)
|
||||
|
||||
if (possiblePayloadFromJson) {
|
||||
return [possiblePayloadFromJson]
|
||||
}
|
||||
|
||||
const possiblePayloadFromHtml = this.tryParseAsHtml(content, file, createNote, convertHTMLToSuper, canUseSuper)
|
||||
|
||||
if (possiblePayloadFromHtml) {
|
||||
return possiblePayloadFromHtml
|
||||
return [possiblePayloadFromHtml]
|
||||
}
|
||||
|
||||
throw new Error('Could not parse Google Keep backup file')
|
||||
@@ -55,7 +68,9 @@ export class GoogleKeepConverter {
|
||||
tryParseAsHtml(
|
||||
data: string,
|
||||
file: { name: string },
|
||||
isEntitledToSuper: boolean,
|
||||
createNote: CreateNoteFn,
|
||||
convertHTMLToSuper: (html: string) => string,
|
||||
canUseSuper: boolean,
|
||||
): DecryptedTransferPayload<NoteContent> {
|
||||
const rootElement = document.createElement('html')
|
||||
rootElement.innerHTML = data
|
||||
@@ -85,18 +100,18 @@ export class GoogleKeepConverter {
|
||||
const checked = item.classList.contains('checked')
|
||||
item.setAttribute('aria-checked', checked ? 'true' : 'false')
|
||||
|
||||
if (!isEntitledToSuper) {
|
||||
if (!canUseSuper) {
|
||||
item.textContent = `- ${checked ? '[x]' : '[ ]'} ${item.textContent?.trim()}\n`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!isEntitledToSuper) {
|
||||
if (!canUseSuper) {
|
||||
// Replace <br> with \n so line breaks get recognised
|
||||
contentElement.innerHTML = contentElement.innerHTML.replace(/<br>/g, '\n')
|
||||
content = contentElement.textContent
|
||||
} else {
|
||||
content = this.superConverterService.convertOtherFormatToSuperString(rootElement.innerHTML, 'html')
|
||||
content = convertHTMLToSuper(rootElement.innerHTML)
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
@@ -105,25 +120,13 @@ export class GoogleKeepConverter {
|
||||
|
||||
const title = rootElement.getElementsByClassName('title')[0]?.textContent || file.name
|
||||
|
||||
return {
|
||||
created_at: date,
|
||||
created_at_timestamp: date.getTime(),
|
||||
updated_at: date,
|
||||
updated_at_timestamp: date.getTime(),
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
title: title,
|
||||
text: content,
|
||||
references: [],
|
||||
...(isEntitledToSuper
|
||||
? {
|
||||
noteType: NoteType.Super,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
return createNote({
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
title: title,
|
||||
text: content,
|
||||
noteType: NoteType.Super,
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -148,7 +151,11 @@ export class GoogleKeepConverter {
|
||||
)
|
||||
}
|
||||
|
||||
tryParseAsJson(data: string, isEntitledToSuper: boolean): DecryptedTransferPayload<NoteContent> | null {
|
||||
tryParseAsJson(
|
||||
data: string,
|
||||
createNote: CreateNoteFn,
|
||||
convertMarkdownToSuper: (md: string) => string,
|
||||
): DecryptedTransferPayload<NoteContent> | null {
|
||||
try {
|
||||
const parsed = JSON.parse(data) as GoogleKeepJsonNote
|
||||
if (!GoogleKeepConverter.isValidGoogleKeepJson(parsed)) {
|
||||
@@ -165,31 +172,17 @@ export class GoogleKeepConverter {
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
if (isEntitledToSuper) {
|
||||
text = this.superConverterService.convertOtherFormatToSuperString(text, 'md')
|
||||
}
|
||||
return {
|
||||
created_at: date,
|
||||
created_at_timestamp: date.getTime(),
|
||||
updated_at: date,
|
||||
updated_at_timestamp: date.getTime(),
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
title: parsed.title,
|
||||
text,
|
||||
references: [],
|
||||
archived: Boolean(parsed.isArchived),
|
||||
trashed: Boolean(parsed.isTrashed),
|
||||
pinned: Boolean(parsed.isPinned),
|
||||
...(isEntitledToSuper
|
||||
? {
|
||||
noteType: NoteType.Super,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
text = convertMarkdownToSuper(text)
|
||||
return createNote({
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
title: parsed.title,
|
||||
text,
|
||||
archived: Boolean(parsed.isArchived),
|
||||
trashed: Boolean(parsed.isTrashed),
|
||||
pinned: Boolean(parsed.isPinned),
|
||||
noteType: NoteType.Super,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return null
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import { SuperConverterServiceInterface } from '@standardnotes/files'
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { Converter } from '../Converter'
|
||||
|
||||
export class HTMLConverter {
|
||||
constructor(
|
||||
private superConverterService: SuperConverterServiceInterface,
|
||||
private _generateUuid: GenerateUuid,
|
||||
) {}
|
||||
export class HTMLConverter implements Converter {
|
||||
constructor() {}
|
||||
|
||||
static isHTMLFile(file: File): boolean {
|
||||
return file.type === 'text/html'
|
||||
getImportType(): string {
|
||||
return 'html'
|
||||
}
|
||||
|
||||
async convertHTMLFileToNote(file: File, isEntitledToSuper: boolean): Promise<DecryptedTransferPayload<NoteContent>> {
|
||||
getSupportedFileTypes(): string[] {
|
||||
return ['text/html']
|
||||
}
|
||||
|
||||
isContentValid(_content: string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
convert: Converter['convert'] = async (file, { createNote, convertHTMLToSuper, readFileAsText }) => {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const { name } = parseFileName(file.name)
|
||||
@@ -24,28 +25,16 @@ export class HTMLConverter {
|
||||
const createdAtDate = file.lastModified ? new Date(file.lastModified) : new Date()
|
||||
const updatedAtDate = file.lastModified ? new Date(file.lastModified) : new Date()
|
||||
|
||||
const text = isEntitledToSuper
|
||||
? this.superConverterService.convertOtherFormatToSuperString(content, 'html')
|
||||
: content
|
||||
const text = convertHTMLToSuper(content)
|
||||
|
||||
return {
|
||||
created_at: createdAtDate,
|
||||
created_at_timestamp: createdAtDate.getTime(),
|
||||
updated_at: updatedAtDate,
|
||||
updated_at_timestamp: updatedAtDate.getTime(),
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
return [
|
||||
createNote({
|
||||
createdAt: createdAtDate,
|
||||
updatedAt: updatedAtDate,
|
||||
title: name,
|
||||
text,
|
||||
references: [],
|
||||
...(isEntitledToSuper
|
||||
? {
|
||||
noteType: NoteType.Super,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
noteType: NoteType.Super,
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,19 +24,13 @@ import {
|
||||
isNote,
|
||||
} from '@standardnotes/models'
|
||||
import { HTMLConverter } from './HTMLConverter/HTMLConverter'
|
||||
import { SuperConverterServiceInterface } from '@standardnotes/snjs/dist/@types'
|
||||
import { SuperConverter } from './SuperConverter/SuperConverter'
|
||||
|
||||
export type NoteImportType = 'plaintext' | 'evernote' | 'google-keep' | 'simplenote' | 'aegis' | 'html' | 'super'
|
||||
import { Converter, CreateNoteFn, CreateTagFn } from './Converter'
|
||||
import { SuperConverterServiceInterface } from '@standardnotes/files'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
export class Importer {
|
||||
aegisConverter: AegisToAuthenticatorConverter
|
||||
googleKeepConverter: GoogleKeepConverter
|
||||
simplenoteConverter: SimplenoteConverter
|
||||
plaintextConverter: PlaintextConverter
|
||||
evernoteConverter: EvernoteConverter
|
||||
htmlConverter: HTMLConverter
|
||||
superConverter: SuperConverter
|
||||
converters: Set<Converter> = new Set()
|
||||
|
||||
constructor(
|
||||
private features: FeaturesClientInterface,
|
||||
@@ -60,83 +54,155 @@ export class Importer {
|
||||
},
|
||||
private _generateUuid: GenerateUuid,
|
||||
) {
|
||||
this.aegisConverter = new AegisToAuthenticatorConverter(_generateUuid)
|
||||
this.googleKeepConverter = new GoogleKeepConverter(this.superConverterService, _generateUuid)
|
||||
this.simplenoteConverter = new SimplenoteConverter(_generateUuid)
|
||||
this.plaintextConverter = new PlaintextConverter(this.superConverterService, _generateUuid)
|
||||
this.evernoteConverter = new EvernoteConverter(this.superConverterService, _generateUuid)
|
||||
this.htmlConverter = new HTMLConverter(this.superConverterService, _generateUuid)
|
||||
this.superConverter = new SuperConverter(this.superConverterService, _generateUuid)
|
||||
this.registerNativeConverters()
|
||||
}
|
||||
|
||||
detectService = async (file: File): Promise<NoteImportType | null> => {
|
||||
registerNativeConverters() {
|
||||
this.converters.add(new AegisToAuthenticatorConverter())
|
||||
this.converters.add(new GoogleKeepConverter())
|
||||
this.converters.add(new SimplenoteConverter())
|
||||
this.converters.add(new PlaintextConverter())
|
||||
this.converters.add(new EvernoteConverter(this._generateUuid))
|
||||
this.converters.add(new HTMLConverter())
|
||||
this.converters.add(new SuperConverter(this.superConverterService))
|
||||
}
|
||||
|
||||
detectService = async (file: File): Promise<string | null> => {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const { ext } = parseFileName(file.name)
|
||||
|
||||
if (ext === 'enex') {
|
||||
return 'evernote'
|
||||
}
|
||||
for (const converter of this.converters) {
|
||||
const isCorrectType = converter.getSupportedFileTypes && converter.getSupportedFileTypes().includes(file.type)
|
||||
const isCorrectExtension = converter.getFileExtension && converter.getFileExtension() === ext
|
||||
|
||||
try {
|
||||
const json = JSON.parse(content)
|
||||
|
||||
if (AegisToAuthenticatorConverter.isValidAegisJson(json)) {
|
||||
return 'aegis'
|
||||
if (!isCorrectType && !isCorrectExtension) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (GoogleKeepConverter.isValidGoogleKeepJson(json)) {
|
||||
return 'google-keep'
|
||||
if (converter.isContentValid(content)) {
|
||||
return converter.getImportType()
|
||||
}
|
||||
|
||||
if (SimplenoteConverter.isValidSimplenoteJson(json)) {
|
||||
return 'simplenote'
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
if (file.type === 'application/json' && this.superConverterService.isValidSuperString(content)) {
|
||||
return 'super'
|
||||
}
|
||||
|
||||
if (PlaintextConverter.isValidPlaintextFile(file)) {
|
||||
return 'plaintext'
|
||||
}
|
||||
|
||||
if (HTMLConverter.isHTMLFile(file)) {
|
||||
return 'html'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async getPayloadsFromFile(file: File, type: NoteImportType): Promise<DecryptedTransferPayload[]> {
|
||||
createNote: CreateNoteFn = ({
|
||||
createdAt,
|
||||
updatedAt,
|
||||
title,
|
||||
text,
|
||||
noteType,
|
||||
editorIdentifier,
|
||||
trashed,
|
||||
archived,
|
||||
pinned,
|
||||
}) => {
|
||||
if (noteType === NoteType.Super && !this.isEntitledToSuper()) {
|
||||
noteType = undefined
|
||||
}
|
||||
|
||||
if (
|
||||
editorIdentifier &&
|
||||
this.features.getFeatureStatus(NativeFeatureIdentifier.create(editorIdentifier).getValue()) !==
|
||||
FeatureStatus.Entitled
|
||||
) {
|
||||
editorIdentifier = undefined
|
||||
}
|
||||
|
||||
return {
|
||||
created_at: createdAt,
|
||||
created_at_timestamp: createdAt.getTime(),
|
||||
updated_at: updatedAt,
|
||||
updated_at_timestamp: updatedAt.getTime(),
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
title,
|
||||
text,
|
||||
references: [],
|
||||
noteType,
|
||||
trashed,
|
||||
archived,
|
||||
pinned,
|
||||
editorIdentifier,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
createTag: CreateTagFn = ({ createdAt, updatedAt, title }) => {
|
||||
return {
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Tag,
|
||||
created_at: createdAt,
|
||||
created_at_timestamp: createdAt.getTime(),
|
||||
updated_at: updatedAt,
|
||||
updated_at_timestamp: updatedAt.getTime(),
|
||||
content: {
|
||||
title: title,
|
||||
expanded: false,
|
||||
iconString: '',
|
||||
references: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
isEntitledToSuper = (): boolean => {
|
||||
return (
|
||||
this.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
|
||||
) === FeatureStatus.Entitled
|
||||
)
|
||||
}
|
||||
|
||||
convertHTMLToSuper = (html: string): string => {
|
||||
if (!this.isEntitledToSuper()) {
|
||||
return html
|
||||
}
|
||||
|
||||
return this.superConverterService.convertOtherFormatToSuperString(html, 'html')
|
||||
}
|
||||
|
||||
convertMarkdownToSuper = (markdown: string): string => {
|
||||
if (!this.isEntitledToSuper()) {
|
||||
return markdown
|
||||
}
|
||||
|
||||
return this.superConverterService.convertOtherFormatToSuperString(markdown, 'md')
|
||||
}
|
||||
|
||||
async getPayloadsFromFile(file: File, type: string): Promise<DecryptedTransferPayload[]> {
|
||||
const isEntitledToSuper =
|
||||
this.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
|
||||
) === FeatureStatus.Entitled
|
||||
if (type === 'super') {
|
||||
if (!isEntitledToSuper) {
|
||||
throw new Error('Importing Super notes requires a subscription.')
|
||||
|
||||
if (type === 'super' && !isEntitledToSuper) {
|
||||
throw new Error('Importing Super notes requires a subscription')
|
||||
}
|
||||
|
||||
for (const converter of this.converters) {
|
||||
const isCorrectType = converter.getImportType() === type
|
||||
|
||||
if (!isCorrectType) {
|
||||
continue
|
||||
}
|
||||
return [await this.superConverter.convertSuperFileToNote(file)]
|
||||
} else if (type === 'aegis') {
|
||||
const isEntitledToAuthenticator =
|
||||
this.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(),
|
||||
) === FeatureStatus.Entitled
|
||||
return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)]
|
||||
} else if (type === 'google-keep') {
|
||||
return [await this.googleKeepConverter.convertGoogleKeepBackupFileToNote(file, isEntitledToSuper)]
|
||||
} else if (type === 'simplenote') {
|
||||
return await this.simplenoteConverter.convertSimplenoteBackupFileToNotes(file)
|
||||
} else if (type === 'evernote') {
|
||||
return await this.evernoteConverter.convertENEXFileToNotesAndTags(file, isEntitledToSuper)
|
||||
} else if (type === 'plaintext') {
|
||||
return [await this.plaintextConverter.convertPlaintextFileToNote(file, isEntitledToSuper)]
|
||||
} else if (type === 'html') {
|
||||
return [await this.htmlConverter.convertHTMLFileToNote(file, isEntitledToSuper)]
|
||||
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
if (!converter.isContentValid(content)) {
|
||||
throw new Error('Content is not valid')
|
||||
}
|
||||
|
||||
return await converter.convert(file, {
|
||||
createNote: this.createNote,
|
||||
createTag: this.createTag,
|
||||
canUseSuper: isEntitledToSuper,
|
||||
convertHTMLToSuper: this.convertHTMLToSuper,
|
||||
convertMarkdownToSuper: this.convertMarkdownToSuper,
|
||||
readFileAsText,
|
||||
})
|
||||
}
|
||||
|
||||
return []
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
import { SuperConverterServiceInterface } from '@standardnotes/files'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { Converter } from '../Converter'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
|
||||
export class PlaintextConverter {
|
||||
constructor(
|
||||
private superConverterService: SuperConverterServiceInterface,
|
||||
private _generateUuid: GenerateUuid,
|
||||
) {}
|
||||
export class PlaintextConverter implements Converter {
|
||||
constructor() {}
|
||||
|
||||
getImportType(): string {
|
||||
return 'plaintext'
|
||||
}
|
||||
|
||||
getSupportedFileTypes(): string[] {
|
||||
return ['text/plain', 'text/markdown']
|
||||
}
|
||||
|
||||
isContentValid(_content: string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
static isValidPlaintextFile(file: File): boolean {
|
||||
return file.type === 'text/plain' || file.type === 'text/markdown'
|
||||
}
|
||||
|
||||
async convertPlaintextFileToNote(
|
||||
file: File,
|
||||
isEntitledToSuper: boolean,
|
||||
): Promise<DecryptedTransferPayload<NoteContent>> {
|
||||
convert: Converter['convert'] = async (file, { createNote, convertMarkdownToSuper, readFileAsText }) => {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const { name } = parseFileName(file.name)
|
||||
@@ -27,24 +29,14 @@ export class PlaintextConverter {
|
||||
const createdAtDate = file.lastModified ? new Date(file.lastModified) : new Date()
|
||||
const updatedAtDate = file.lastModified ? new Date(file.lastModified) : new Date()
|
||||
|
||||
return {
|
||||
created_at: createdAtDate,
|
||||
created_at_timestamp: createdAtDate.getTime(),
|
||||
updated_at: updatedAtDate,
|
||||
updated_at_timestamp: updatedAtDate.getTime(),
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
return [
|
||||
createNote({
|
||||
createdAt: createdAtDate,
|
||||
updatedAt: updatedAtDate,
|
||||
title: name,
|
||||
text: isEntitledToSuper ? this.superConverterService.convertOtherFormatToSuperString(content, 'md') : content,
|
||||
references: [],
|
||||
...(isEntitledToSuper
|
||||
? {
|
||||
noteType: NoteType.Super,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
text: convertMarkdownToSuper(content),
|
||||
noteType: NoteType.Super,
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { SimplenoteConverter } from './SimplenoteConverter'
|
||||
import data from './testData'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { CreateNoteFn } from '../Converter'
|
||||
|
||||
describe('SimplenoteConverter', () => {
|
||||
const crypto = {
|
||||
generateUUID: () => String(Math.random()),
|
||||
} as unknown as PureCryptoInterface
|
||||
|
||||
const generateUuid = new GenerateUuid(crypto)
|
||||
const createNote: CreateNoteFn = ({ title, text, trashed, createdAt, updatedAt }) =>
|
||||
({
|
||||
uuid: Math.random().toString(),
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
title,
|
||||
text,
|
||||
trashed,
|
||||
references: [],
|
||||
},
|
||||
}) as unknown as DecryptedTransferPayload<NoteContent>
|
||||
|
||||
it('should parse', () => {
|
||||
const converter = new SimplenoteConverter(generateUuid)
|
||||
const converter = new SimplenoteConverter()
|
||||
|
||||
const result = converter.parse(data)
|
||||
const result = converter.parse(data, createNote)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.length).toBe(3)
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
import { Converter, CreateNoteFn } from '../Converter'
|
||||
|
||||
type SimplenoteItem = {
|
||||
creationDate: string
|
||||
@@ -17,21 +14,34 @@ type SimplenoteData = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isSimplenoteEntry = (entry: any): boolean => entry.id && entry.content && entry.creationDate && entry.lastModified
|
||||
|
||||
export class SimplenoteConverter {
|
||||
constructor(private _generateUuid: GenerateUuid) {}
|
||||
export class SimplenoteConverter implements Converter {
|
||||
constructor() {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static isValidSimplenoteJson(json: any): boolean {
|
||||
return (
|
||||
(json.activeNotes && json.activeNotes.every(isSimplenoteEntry)) ||
|
||||
(json.trashedNotes && json.trashedNotes.every(isSimplenoteEntry))
|
||||
)
|
||||
getImportType(): string {
|
||||
return 'simplenote'
|
||||
}
|
||||
|
||||
async convertSimplenoteBackupFileToNotes(file: File): Promise<DecryptedTransferPayload<NoteContent>[]> {
|
||||
getSupportedFileTypes(): string[] {
|
||||
return ['application/json']
|
||||
}
|
||||
|
||||
isContentValid(content: string): boolean {
|
||||
try {
|
||||
const json = JSON.parse(content)
|
||||
return (
|
||||
(json.activeNotes && json.activeNotes.every(isSimplenoteEntry)) ||
|
||||
(json.trashedNotes && json.trashedNotes.every(isSimplenoteEntry))
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
convert: Converter['convert'] = async (file, { createNote, readFileAsText }) => {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const notes = this.parse(content)
|
||||
const notes = this.parse(content, createNote)
|
||||
|
||||
if (!notes) {
|
||||
throw new Error('Could not parse notes')
|
||||
@@ -40,7 +50,7 @@ export class SimplenoteConverter {
|
||||
return notes
|
||||
}
|
||||
|
||||
createNoteFromItem(item: SimplenoteItem, trashed: boolean): DecryptedTransferPayload<NoteContent> {
|
||||
createNoteFromItem(item: SimplenoteItem, trashed: boolean, createNote: CreateNoteFn): ReturnType<CreateNoteFn> {
|
||||
const createdAtDate = new Date(item.creationDate)
|
||||
const updatedAtDate = new Date(item.lastModified)
|
||||
|
||||
@@ -50,32 +60,20 @@ export class SimplenoteConverter {
|
||||
hasTitleAndContent && splitItemContent[0].length ? splitItemContent[0] : createdAtDate.toLocaleString()
|
||||
const content = hasTitleAndContent && splitItemContent[1].length ? splitItemContent[1] : item.content
|
||||
|
||||
return {
|
||||
created_at: createdAtDate,
|
||||
created_at_timestamp: createdAtDate.getTime(),
|
||||
updated_at: updatedAtDate,
|
||||
updated_at_timestamp: updatedAtDate.getTime(),
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
title,
|
||||
text: content,
|
||||
references: [],
|
||||
trashed,
|
||||
appData: {
|
||||
'org.standardnotes.sn': {
|
||||
client_updated_at: updatedAtDate,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return createNote({
|
||||
createdAt: createdAtDate,
|
||||
updatedAt: updatedAtDate,
|
||||
title,
|
||||
text: content,
|
||||
trashed,
|
||||
})
|
||||
}
|
||||
|
||||
parse(data: string) {
|
||||
parse(data: string, createNote: CreateNoteFn) {
|
||||
try {
|
||||
const parsed = JSON.parse(data) as SimplenoteData
|
||||
const activeNotes = parsed.activeNotes.reverse().map((item) => this.createNoteFromItem(item, false))
|
||||
const trashedNotes = parsed.trashedNotes.reverse().map((item) => this.createNoteFromItem(item, true))
|
||||
const activeNotes = parsed.activeNotes.reverse().map((item) => this.createNoteFromItem(item, false, createNote))
|
||||
const trashedNotes = parsed.trashedNotes.reverse().map((item) => this.createNoteFromItem(item, true, createNote))
|
||||
|
||||
return [...activeNotes, ...trashedNotes]
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import { SuperConverterServiceInterface } from '@standardnotes/files'
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { GenerateUuid } from '@standardnotes/services'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import { Converter } from '../Converter'
|
||||
|
||||
export class SuperConverter {
|
||||
constructor(
|
||||
private converterService: SuperConverterServiceInterface,
|
||||
private _generateUuid: GenerateUuid,
|
||||
) {}
|
||||
export class SuperConverter implements Converter {
|
||||
constructor(private converterService: SuperConverterServiceInterface) {}
|
||||
|
||||
async convertSuperFileToNote(file: File): Promise<DecryptedTransferPayload<NoteContent>> {
|
||||
getImportType(): string {
|
||||
return 'super'
|
||||
}
|
||||
|
||||
getSupportedFileTypes(): string[] {
|
||||
return ['application/json']
|
||||
}
|
||||
|
||||
isContentValid(content: string): boolean {
|
||||
return this.converterService.isValidSuperString(content)
|
||||
}
|
||||
|
||||
convert: Converter['convert'] = async (file, { createNote, readFileAsText }) => {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
if (!this.converterService.isValidSuperString(content)) {
|
||||
@@ -24,20 +30,14 @@ export class SuperConverter {
|
||||
const createdAtDate = file.lastModified ? new Date(file.lastModified) : new Date()
|
||||
const updatedAtDate = file.lastModified ? new Date(file.lastModified) : new Date()
|
||||
|
||||
return {
|
||||
created_at: createdAtDate,
|
||||
created_at_timestamp: createdAtDate.getTime(),
|
||||
updated_at: updatedAtDate,
|
||||
updated_at_timestamp: updatedAtDate.getTime(),
|
||||
uuid: this._generateUuid.execute().getValue(),
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: {
|
||||
return [
|
||||
createNote({
|
||||
createdAt: createdAtDate,
|
||||
updatedAt: updatedAtDate,
|
||||
title: name,
|
||||
text: content,
|
||||
references: [],
|
||||
noteType: NoteType.Super,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor,
|
||||
},
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdow
|
||||
import { ContentType, SNTag } from '@standardnotes/snjs'
|
||||
import Button from '../Button/Button'
|
||||
import { ClassicFileReader } from '@standardnotes/filepicker'
|
||||
import { NoteImportType } from '@standardnotes/ui-services'
|
||||
|
||||
const ImportModal = ({ importModalController }: { importModalController: ImportModalController }) => {
|
||||
const application = useApplication()
|
||||
@@ -60,7 +59,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
|
||||
)
|
||||
|
||||
const selectFiles = useCallback(
|
||||
async (service?: NoteImportType) => {
|
||||
async (service?: string) => {
|
||||
const files = await ClassicFileReader.selectFiles()
|
||||
|
||||
addFiles(files, service)
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
PreferencesServiceEvent,
|
||||
UuidGenerator,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Importer, NoteImportType } from '@standardnotes/ui-services'
|
||||
import { Importer } from '@standardnotes/ui-services'
|
||||
import { action, makeObservable, observable, runInAction } from 'mobx'
|
||||
import { NavigationController } from '../../Controllers/Navigation/NavigationController'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
@@ -18,7 +18,7 @@ import { AbstractViewController } from '@/Controllers/Abstract/AbstractViewContr
|
||||
type ImportModalFileCommon = {
|
||||
id: string
|
||||
file: File
|
||||
service: NoteImportType | null | undefined
|
||||
service: string | null | undefined
|
||||
}
|
||||
|
||||
export type ImportModalFile = (
|
||||
@@ -107,7 +107,7 @@ export class ImportModalController extends AbstractViewController {
|
||||
this.preferences.setValue(PrefKey.ExistingTagForImports, tag?.uuid).catch(console.error)
|
||||
}
|
||||
|
||||
getImportFromFile = (file: File, service?: NoteImportType) => {
|
||||
getImportFromFile = (file: File, service?: string) => {
|
||||
return {
|
||||
id: UuidGenerator.GenerateUuid(),
|
||||
file,
|
||||
@@ -116,11 +116,11 @@ export class ImportModalController extends AbstractViewController {
|
||||
} as ImportModalFile
|
||||
}
|
||||
|
||||
setFiles = (files: File[], service?: NoteImportType) => {
|
||||
setFiles = (files: File[], service?: string) => {
|
||||
this.files = files.map((file) => this.getImportFromFile(file, service))
|
||||
}
|
||||
|
||||
addFiles = (files: File[], service?: NoteImportType) => {
|
||||
addFiles = (files: File[], service?: string) => {
|
||||
this.files = [...this.files, ...files.map((file) => this.getImportFromFile(file, service))]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ImportModalController, ImportModalFile } from '@/Components/ImportModal/ImportModalController'
|
||||
import { classNames, ContentType, pluralize } from '@standardnotes/snjs'
|
||||
import { Importer, NoteImportType } from '@standardnotes/ui-services'
|
||||
import { Importer } from '@standardnotes/ui-services'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Icon from '../Icon/Icon'
|
||||
|
||||
const NoteImportTypeColors: Record<NoteImportType, string> = {
|
||||
const NoteImportTypeColors: Record<string, string> = {
|
||||
evernote: 'bg-[#14cc45] text-[#000]',
|
||||
simplenote: 'bg-[#3360cc] text-default',
|
||||
'google-keep': 'bg-[#fbbd00] text-[#000]',
|
||||
@@ -15,7 +15,7 @@ const NoteImportTypeColors: Record<NoteImportType, string> = {
|
||||
super: 'bg-accessory-tint-1 text-accessory-tint-1',
|
||||
}
|
||||
|
||||
const NoteImportTypeIcons: Record<NoteImportType, string> = {
|
||||
const NoteImportTypeIcons: Record<string, string> = {
|
||||
evernote: 'evernote',
|
||||
simplenote: 'simplenote',
|
||||
'google-keep': 'gkeep',
|
||||
@@ -39,7 +39,7 @@ const ImportModalFileItem = ({
|
||||
const [changingService, setChangingService] = useState(false)
|
||||
|
||||
const setFileService = useCallback(
|
||||
async (service: NoteImportType | null) => {
|
||||
async (service: string | null) => {
|
||||
if (!service) {
|
||||
setChangingService(true)
|
||||
}
|
||||
@@ -116,7 +116,7 @@ const ImportModalFileItem = ({
|
||||
event.preventDefault()
|
||||
const form = event.target as HTMLFormElement
|
||||
const service = form.elements[0] as HTMLSelectElement
|
||||
void setFileService(service.value as NoteImportType)
|
||||
void setFileService(service.value)
|
||||
setChangingService(false)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -5,11 +5,10 @@ import Icon from '../Icon/Icon'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { FeatureName } from '@/Controllers/FeatureName'
|
||||
import { NativeFeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
|
||||
import { NoteImportType } from '@standardnotes/ui-services'
|
||||
|
||||
type Props = {
|
||||
setFiles: ImportModalController['setFiles']
|
||||
selectFiles: (service?: NoteImportType) => Promise<void>
|
||||
selectFiles: (service?: string) => Promise<void>
|
||||
}
|
||||
|
||||
const ImportModalInitialPage = ({ setFiles, selectFiles }: Props) => {
|
||||
|
||||
Reference in New Issue
Block a user