From d9764365cb695fe2294e046604ab5e5c44497848 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 14 Mar 2025 22:13:31 +0200 Subject: [PATCH] feat(import): allow importing .excalidraw files --- src/services/import/mime.spec.ts | 27 +++++++++++-------- src/services/import/mime.ts | 3 ++- .../import/samples/New note.excalidraw | 1 + src/services/import/single.spec.ts | 7 +++++ src/services/import/single.ts | 8 +++++- src/services/utils.ts | 1 + 6 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 src/services/import/samples/New note.excalidraw diff --git a/src/services/import/mime.spec.ts b/src/services/import/mime.spec.ts index 299bc3e2d..1e32ba3eb 100644 --- a/src/services/import/mime.spec.ts +++ b/src/services/import/mime.spec.ts @@ -21,6 +21,11 @@ describe("#getMime", () => { ["test.ts"], "text/x-typescript" ], + [ + "File extension ('.excalidraw') that is defined in EXTENSION_TO_MIME", + ["test.excalidraw"], "application/json" + ], + [ "File extension with inconsistent capitalization that is defined in EXTENSION_TO_MIME", ["test.gRoOvY"], "text/x-groovy" @@ -41,7 +46,7 @@ describe("#getMime", () => { const [testDesc, fnParams, expected] = testCase; it(`${testDesc}: '${fnParams} should return '${expected}'`, () => { const actual = mimeService.getMime(...fnParams); - expect(actual).toEqual(expected); + expect(actual, testDesc).toEqual(expected); }); }); }); @@ -60,7 +65,7 @@ describe("#getType", () => { ], [ - "w/ import options set and an image mime type – it should return 'image'", + "w/ import options set and an image mime type – it should return 'image'", [{}, "image/jpeg"], "image" ], @@ -75,22 +80,22 @@ describe("#getType", () => { ], [ - "w/ codeImportedAsCode: true and a mime type that is in CODE_MIME_TYPES – it should return 'code'", + "w/ codeImportedAsCode: true and a mime type that is in CODE_MIME_TYPES – it should return 'code'", [{codeImportedAsCode: true}, "text/css"], "code" ], [ - "w/ codeImportedAsCode: false and a mime type that is in CODE_MIME_TYPES – it should return 'file' not 'code'", + "w/ codeImportedAsCode: false and a mime type that is in CODE_MIME_TYPES – it should return 'file' not 'code'", [{codeImportedAsCode: false}, "text/css"], "file" ], [ - "w/ textImportedAsText: true and 'text/html' mime type – it should return 'text'", + "w/ textImportedAsText: true and 'text/html' mime type – it should return 'text'", [{textImportedAsText: true}, "text/html"], "text" ], [ - "w/ textImportedAsText: true and 'text/markdown' mime type – it should return 'text'", + "w/ textImportedAsText: true and 'text/markdown' mime type – it should return 'text'", [{textImportedAsText: true}, "text/markdown"], "text" ], @@ -125,23 +130,23 @@ describe("#normalizeMimeType", () => { const testCases: TestCase[] = [ [ - "empty mime should return undefined", + "empty mime should return undefined", [""], undefined ], [ - "a mime that's defined in CODE_MIME_TYPES should return the same mime", + "a mime that's defined in CODE_MIME_TYPES should return the same mime", ["text/x-python"], "text/x-python" ], [ - "a mime (with capitalization inconsistencies) that's defined in CODE_MIME_TYPES should return the same mime in lowercase", + "a mime (with capitalization inconsistencies) that's defined in CODE_MIME_TYPES should return the same mime in lowercase", ["text/X-pYthOn"], "text/x-python" ], [ - "a mime that's non defined in CODE_MIME_TYPES should return undefined", + "a mime that's non defined in CODE_MIME_TYPES should return undefined", ["application/zip"], undefined ], [ - "a mime that's defined in CODE_MIME_TYPES with a 'rewrite rule' should return the rewritten mime", + "a mime that's defined in CODE_MIME_TYPES with a 'rewrite rule' should return the rewritten mime", ["text/markdown"], "text/x-markdown" ] ]; diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index b329f12ac..bab661c63 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -67,7 +67,8 @@ const EXTENSION_TO_MIME = new Map([ [".rb", "text/x-ruby"], [".scala", "text/x-scala"], [".swift", "text/x-swift"], - [".ts", "text/x-typescript"] + [".ts", "text/x-typescript"], + [".excalidraw", "application/json"] ]); /** @returns false if MIME is not detected */ diff --git a/src/services/import/samples/New note.excalidraw b/src/services/import/samples/New note.excalidraw new file mode 100644 index 000000000..d22349f39 --- /dev/null +++ b/src/services/import/samples/New note.excalidraw @@ -0,0 +1 @@ +{"type":"excalidraw","version":2,"elements":[],"files":{},"appState":{"scrollX":0,"scrollY":0,"zoom":{"value":1}}} \ No newline at end of file diff --git a/src/services/import/single.spec.ts b/src/services/import/single.spec.ts index a1751f872..e04b657df 100644 --- a/src/services/import/single.spec.ts +++ b/src/services/import/single.spec.ts @@ -89,4 +89,11 @@ describe("processNoteContent", () => { expect(importedNote.mime).toBe("text/html"); expect(importedNote.getContent().toString()).toBe("

Hello world

\n

Plain text goes here.

\n"); }); + + it("supports excalidraw note", async () => { + const { importedNote } = await testImport("New note.excalidraw", "application/json"); + expect(importedNote.mime).toBe("application/json"); + expect(importedNote.type).toBe("canvas"); + expect(importedNote.title).toBe("New note"); + }); }); diff --git a/src/services/import/single.ts b/src/services/import/single.ts index b572aea7f..4105e55e3 100644 --- a/src/services/import/single.ts +++ b/src/services/import/single.ts @@ -12,6 +12,7 @@ import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js"; import importUtils from "./utils.js"; import htmlSanitizer from "../html_sanitizer.js"; import type { File } from "./common.js"; +import type { NoteType } from "../../becca/entities/rows.js"; function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNote) { const mime = mimeService.getMime(file.originalname) || file.mimetype; @@ -73,11 +74,16 @@ function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote) const detectedMime = mimeService.getMime(file.originalname) || file.mimetype; const mime = mimeService.normalizeMimeType(detectedMime); + let type: NoteType = "code"; + if (file.originalname.endsWith(".excalidraw")) { + type = "canvas"; + } + const { note } = noteService.createNewNote({ parentNoteId: parentNote.noteId, title, content, - type: "code", + type, mime: mime, isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable() }); diff --git a/src/services/utils.ts b/src/services/utils.ts index 966e14840..3cb84d3a1 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -180,6 +180,7 @@ export function removeTextFileExtension(filePath: string) { case ".markdown": case ".html": case ".htm": + case ".excalidraw": return filePath.substring(0, filePath.length - extension.length); default: return filePath;