Compare commits

...

1 commit

Author SHA1 Message Date
b3787eeb3c Temp build for backlinks 2026-01-06 12:09:37 +00:00
4 changed files with 123 additions and 0 deletions

View file

@ -0,0 +1,74 @@
import { Mark, markInputRule } from "@tiptap/core";
export const NoteLink = Mark.create({
name: "noteLink",
inclusive: false,
addAttributes() {
return {
noteId: {
default: null,
parseHTML: (element) => {
return element.getAttribute("data-note-id");
},
renderHTML: (attributes) => {
return {
"data-note-id": attributes.noteId,
};
},
},
title: {
default: "",
parseHTML: (element) => {
return element.getAttribute("data-title");
},
renderHTML: (attributes) => {
return {
"data-title": attributes.title,
};
},
},
};
},
parseHTML() {
return [
{
tag: "span[data-note-id]",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["span", { ...HTMLAttributes, class: "note-link" }, 0];
},
addInputRules() {
return [
markInputRule({
find: /\[\[([^\]]+)\]\]$/,
type: this.type,
getAttributes: (match) => {
const title = match[1];
// TODO: Look up noteId from title
return {
title,
noteId: null, // For now
};
},
}),
];
},
addStorage() {
return {
markdown: {
serialize: (state, mark, parent, index) => {
// Get the text content
const textContent = parent.child(index).text || mark.attrs.title;
// Return the complete link, no wrapping
return `[[${mark.attrs.title}:${mark.attrs.noteId}]]`;
},
},
};
},
});

View file

@ -22,6 +22,7 @@ import SquareCheckIcon from "../assets/fontawesome/svg/square-check.svg?react";
import CodeBracketIcon from "../assets/fontawesome/svg/code-simple.svg?react"; import CodeBracketIcon from "../assets/fontawesome/svg/code-simple.svg?react";
// @ts-ignore // @ts-ignore
import QuoteLeftIcon from "../assets/fontawesome/svg/quote-left.svg?react"; import QuoteLeftIcon from "../assets/fontawesome/svg/quote-left.svg?react";
import { NoteLink } from "@/extensions/NoteLink";
interface TiptapEditorProps { interface TiptapEditorProps {
content: string; content: string;
@ -37,6 +38,7 @@ export const TiptapEditor = ({
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
ListKit, ListKit,
NoteLink,
StarterKit.configure({ StarterKit.configure({
heading: { heading: {
levels: [1, 2, 3, 4, 5, 6], levels: [1, 2, 3, 4, 5, 6],
@ -189,6 +191,19 @@ export const TiptapEditor = ({
</div>*/} </div>*/}
{/* Editor content */} {/* Editor content */}
<button
onClick={() => {
editor
?.chain()
.focus()
.setMark("noteLink", { noteId: 123, title: "Test Note" })
.insertContent("Test Note")
.run();
}}
className="bg-ctp-blue text-ctp-base px-4 py-2 rounded"
>
Insert Test Link
</button>
<EditorContent <EditorContent
editor={editor} editor={editor}
className="editor-content h-min-screen p-4! pt-0!" className="editor-content h-min-screen p-4! pt-0!"

View file

@ -172,3 +172,9 @@
margin-top: 0 !important; margin-top: 0 !important;
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.note-link {
@apply text-ctp-pink-500;
@apply underline;
@apply cursor-pointer;
}

View file

@ -0,0 +1,28 @@
import { Folder, FolderTreeNode, FolderTreeResponse } from "@/api/folders";
import { NoteRead } from "@/api/notes";
export const createNoteMap = (
folderTree: FolderTreeResponse,
): Map<number, NoteRead> => {
const flatenedNotes = flattenNotes(folderTree);
const noteMap = new Map();
for (const note of flatenedNotes) {
noteMap.set(note.id, note);
}
return noteMap;
};
export const flattenNotes = (response: FolderTreeResponse): NoteRead[] => {
const allNotes = [...response.orphanedNotes];
const processFolder = (folder: FolderTreeNode) => {
allNotes.push(...folder.notes);
folder.children.forEach(processFolder);
};
response.folders.forEach(processFolder);
return allNotes;
};