feat(import): allow importing .excalidraw files

This commit is contained in:
Elian Doran 2025-03-14 22:13:31 +02:00
parent ad8c1a4a29
commit d9764365cb
No known key found for this signature in database
6 changed files with 34 additions and 13 deletions

View File

@ -21,6 +21,11 @@ describe("#getMime", () => {
["test.ts"], "text/x-typescript" ["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", "File extension with inconsistent capitalization that is defined in EXTENSION_TO_MIME",
["test.gRoOvY"], "text/x-groovy" ["test.gRoOvY"], "text/x-groovy"
@ -41,7 +46,7 @@ describe("#getMime", () => {
const [testDesc, fnParams, expected] = testCase; const [testDesc, fnParams, expected] = testCase;
it(`${testDesc}: '${fnParams} should return '${expected}'`, () => { it(`${testDesc}: '${fnParams} should return '${expected}'`, () => {
const actual = mimeService.getMime(...fnParams); 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" [{}, "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" [{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" [{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" [{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" [{textImportedAsText: true}, "text/markdown"], "text"
], ],
@ -125,23 +130,23 @@ describe("#normalizeMimeType", () => {
const testCases: TestCase<typeof mimeService.normalizeMimeType, string | undefined>[] = [ const testCases: TestCase<typeof mimeService.normalizeMimeType, string | undefined>[] = [
[ [
"empty mime should return undefined", "empty mime should return undefined",
[""], 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" ["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" ["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 ["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" ["text/markdown"], "text/x-markdown"
] ]
]; ];

View File

@ -67,7 +67,8 @@ const EXTENSION_TO_MIME = new Map<string, string>([
[".rb", "text/x-ruby"], [".rb", "text/x-ruby"],
[".scala", "text/x-scala"], [".scala", "text/x-scala"],
[".swift", "text/x-swift"], [".swift", "text/x-swift"],
[".ts", "text/x-typescript"] [".ts", "text/x-typescript"],
[".excalidraw", "application/json"]
]); ]);
/** @returns false if MIME is not detected */ /** @returns false if MIME is not detected */

View File

@ -0,0 +1 @@
{"type":"excalidraw","version":2,"elements":[],"files":{},"appState":{"scrollX":0,"scrollY":0,"zoom":{"value":1}}}

View File

@ -89,4 +89,11 @@ describe("processNoteContent", () => {
expect(importedNote.mime).toBe("text/html"); expect(importedNote.mime).toBe("text/html");
expect(importedNote.getContent().toString()).toBe("<h2>Hello world</h2>\n<p>Plain text goes here.</p>\n"); expect(importedNote.getContent().toString()).toBe("<h2>Hello world</h2>\n<p>Plain text goes here.</p>\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");
});
}); });

View File

@ -12,6 +12,7 @@ import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
import importUtils from "./utils.js"; import importUtils from "./utils.js";
import htmlSanitizer from "../html_sanitizer.js"; import htmlSanitizer from "../html_sanitizer.js";
import type { File } from "./common.js"; import type { File } from "./common.js";
import type { NoteType } from "../../becca/entities/rows.js";
function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNote) { function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNote) {
const mime = mimeService.getMime(file.originalname) || file.mimetype; 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 detectedMime = mimeService.getMime(file.originalname) || file.mimetype;
const mime = mimeService.normalizeMimeType(detectedMime); const mime = mimeService.normalizeMimeType(detectedMime);
let type: NoteType = "code";
if (file.originalname.endsWith(".excalidraw")) {
type = "canvas";
}
const { note } = noteService.createNewNote({ const { note } = noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,
title, title,
content, content,
type: "code", type,
mime: mime, mime: mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable() isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
}); });

View File

@ -180,6 +180,7 @@ export function removeTextFileExtension(filePath: string) {
case ".markdown": case ".markdown":
case ".html": case ".html":
case ".htm": case ".htm":
case ".excalidraw":
return filePath.substring(0, filePath.length - extension.length); return filePath.substring(0, filePath.length - extension.length);
default: default:
return filePath; return filePath;