Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@
"daisyui": "5.5.5",
"dotenv": "^17.2.3",
"effect": "^3.19.6",
"fast-diff": "^1.3.0",
"katex": "^0.16.25",
"loro-codemirror": "^0.3.3",
"loro-crdt": "^1.10.0",
"svelte": "^5.44.0",
"tiptap-markdown": "^0.9.0",
Expand Down
24 changes: 16 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 2 additions & 9 deletions src/lib/components/codemirror/Codemirror.svelte
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
<script lang="ts">
import { browser } from "$app/environment";
import type { Extension, Text } from "@codemirror/state";
import type { Extension } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { onMount, onDestroy } from "svelte";
import type { ClassValue } from "svelte/elements";

interface Props {
doc: string | Text;
extensions: Extension | undefined;
class?: ClassValue;
editorView: EditorView;
}

let {
doc,
extensions = [],
editorView = $bindable(),
...props
}: Props = $props();
let { extensions = [], editorView = $bindable(), ...props }: Props = $props();

let editorElement: HTMLElement;

onMount(() => {
// Initialize CodeMirror
editorView = new EditorView({
doc,
parent: editorElement,
extensions: extensions,
});
Expand Down
70 changes: 34 additions & 36 deletions src/lib/components/codemirror/Editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
ListOrdered,
Strikethrough,
} from "@lucide/svelte";
import { LoroExtensions } from "loro-codemirror";
import Codemirror from "./Codemirror.svelte";
import {
coreExtensions,
Expand All @@ -30,15 +31,19 @@
} from "./Editor.ts";
import Toolbar from "./Toolbar.svelte";
import { wikilinksExtension } from "$lib/editor/wikilinks.ts";
import type { NoteOrFolder } from "$lib/schema.ts";
import type { NoteOrFolder, User } from "$lib/schema.ts";
import { LoroNoteManager } from "$lib/loro.ts";
import { EphemeralStore, UndoManager } from "loro-crdt";
import type { Extension } from "@codemirror/state";
import { onDestroy } from "svelte";

interface Props {
content: string;
onchange: (newContent: string) => void;
manager: LoroNoteManager | undefined;
notesList?: NoteOrFolder[];
user: User | undefined;
}

let { content, onchange, notesList = [] }: Props = $props();
let { manager, notesList = [], user }: Props = $props();

// svelte-ignore non_reactive_update
let editorView: EditorView;
Expand Down Expand Up @@ -98,37 +103,35 @@
},
});

const extensions = [
let loroExtensions: Extension;
if (manager !== undefined && user !== undefined) {
const ephemeral = new EphemeralStore();
const undoManager = new UndoManager(manager.doc, {});

onDestroy(() => {
ephemeral.destroy();
});

loroExtensions = LoroExtensions(
manager.doc,
{
ephemeral,
user: { name: user.username, colorClassName: "bg-primary" },
},
undoManager,
LoroNoteManager.getTextFromDoc,
);
} else {
loroExtensions = [];
}

const extensions: Extension[] = [
coreExtensions,
wikilinksExtension(notesList),
// Update listener
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const newContent = update.state.doc.toString();
console.debug(
"[Prosemark] Content updated. Preview:",
newContent.slice(0, 50),
);
onchange(newContent);
}
}),
loroExtensions,
editorTheme,
];

// Update content if it changes externally (from Loro)
$effect(() => {
if (content !== editorView.state.doc.toString()) {
console.debug("[Prosemark] External content update");
editorView.dispatch({
changes: {
from: 0,
to: editorView.state.doc.length,
insert: content,
},
});
}
});

const tools = [
[
{
Expand Down Expand Up @@ -196,10 +199,5 @@
<div class="flex h-full flex-col">
<Toolbar {tools} />

<Codemirror
bind:editorView
doc={content}
{extensions}
class="flex-1 overflow-y-auto"
/>
<Codemirror bind:editorView {extensions} class="flex-1 overflow-y-auto" />
</div>
37 changes: 17 additions & 20 deletions src/lib/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,28 @@ export async function decryptKey(
return new Uint8Array(decrypted).toBase64();
}

export async function encryptData(
data: Uint8Array<ArrayBuffer>,
noteKey: string,
): Promise<Uint8Array> {
const keyBuffer = Uint8Array.fromBase64(noteKey);
const key = await crypto.subtle.importKey(
async function getCryptoKeyFromBase64(base64Key: string): Promise<CryptoKey> {
const keyBuffer = Uint8Array.fromBase64(base64Key);
return crypto.subtle.importKey(
"raw",
keyBuffer,
{
name: "AES-GCM",
},
false,
["encrypt"],
["encrypt", "decrypt"],
);
}

const IV_LENGTH = 12; // AES-GCM standard IV length

const iv = crypto.getRandomValues(new Uint8Array(12));
export async function encryptData(
data: Uint8Array<ArrayBuffer>,
noteKey: string,
): Promise<Uint8Array> {
const key = await getCryptoKeyFromBase64(noteKey);

const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const encrypted = await crypto.subtle.encrypt(
{
name: "AES-GCM",
Expand All @@ -147,20 +153,11 @@ export async function decryptData(
encrypted: Uint8Array,
noteKey: string,
): Promise<Uint8Array> {
const keyBuffer = Uint8Array.fromBase64(noteKey);
const key = await crypto.subtle.importKey(
"raw",
keyBuffer,
{
name: "AES-GCM",
},
false,
["decrypt"],
);
const key = await getCryptoKeyFromBase64(noteKey);

// Extract IV from first 12 bytes
const iv = encrypted.slice(0, 12);
const data = encrypted.slice(12);
const iv = encrypted.slice(0, IV_LENGTH);
const data = encrypted.slice(IV_LENGTH);

const decrypted = await crypto.subtle.decrypt(
{
Expand Down
Loading