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