refactor: importer service (#2674)

This commit is contained in:
Aman Harwara
2023-12-05 02:55:32 +05:30
committed by GitHub
parent 9265e7afe9
commit 85ecb10924
17 changed files with 509 additions and 518 deletions

View File

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

View File

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

View 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>

View File

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

View File

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

View File

@@ -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,
) )

View File

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

View File

@@ -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,
}
: {}),
},
}
} }
} }

View File

@@ -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 []

View File

@@ -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,
}
: {}),
},
}
} }
} }

View File

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

View File

@@ -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) {

View File

@@ -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, }),
}, ]
}
} }
} }

View File

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

View File

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

View File

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

View File

@@ -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) => {