diff --git a/src/components/editor/MarkdownEditor.tsx b/src/components/editor/MarkdownEditor.tsx index cedf741..4f563bd 100644 --- a/src/components/editor/MarkdownEditor.tsx +++ b/src/components/editor/MarkdownEditor.tsx @@ -7,7 +7,7 @@ import { useState, } from "react"; import { useEditor, EditorContent, ReactRenderer } from "@tiptap/react"; -import { Extension } from "@tiptap/core"; +import { Extension, type Editor, type JSONContent } from "@tiptap/core"; import StarterKit from "@tiptap/starter-kit"; import Mention from "@tiptap/extension-mention"; import Placeholder from "@tiptap/extension-placeholder"; @@ -65,9 +65,9 @@ export interface MarkdownEditorHandle { /** Insert a blob attachment with rich preview */ insertBlob: (blob: BlobAttachment) => void; /** Get editor state as JSON (for persistence/drafts) */ - getJSON: () => any; + getJSON: () => JSONContent | null; /** Restore editor content from JSON */ - setContent: (json: any) => void; + setContent: (json: JSONContent) => void; } // Create emoji extension by extending Mention with a different name and custom node view @@ -159,7 +159,7 @@ export const MarkdownEditor = forwardRef< ) => { const [preview, setPreview] = useState(false); const [previewContent, setPreviewContent] = useState(""); - const handleSubmitRef = useRef<(editor: any) => void>(() => {}); + const handleSubmitRef = useRef<(editor: Editor) => void>(() => {}); // Create mention suggestion configuration const mentionSuggestion: Omit = useMemo( @@ -289,7 +289,7 @@ export const MarkdownEditor = forwardRef< // Handle submit const handleSubmit = useCallback( - (editorInstance: any) => { + (editorInstance: Editor) => { if (editorInstance.isEmpty) return; const serialized = serializeEditorToMarkdown(editorInstance); @@ -340,17 +340,16 @@ export const MarkdownEditor = forwardRef< HTMLAttributes: { class: "mention" }, suggestion: { ...mentionSuggestion, - command: ({ editor, range, props }: any) => { - editor - .chain() + command: ({ editor: ed, range: r, props: mentionAttrs }) => { + // items() returns ProfileSearchResult[]; the selected item is + // passed as props at runtime despite being typed as MentionNodeAttrs + const p = mentionAttrs as unknown as ProfileSearchResult; + ed.chain() .focus() - .insertContentAt(range, [ + .insertContentAt(r, [ { type: "mention", - attrs: { - id: props.pubkey, - label: props.displayName, - }, + attrs: { id: p.pubkey, label: p.displayName }, }, { type: "text", text: " " }, ]) @@ -374,18 +373,20 @@ export const MarkdownEditor = forwardRef< HTMLAttributes: { class: "emoji" }, suggestion: { ...emojiSuggestion, - command: ({ editor, range, props }: any) => { - editor - .chain() + command: ({ editor: ed, range: r, props: mentionAttrs }) => { + // items() returns EmojiSearchResult[]; the selected item is + // passed as props at runtime despite being typed as MentionNodeAttrs + const p = mentionAttrs as unknown as EmojiSearchResult; + ed.chain() .focus() - .insertContentAt(range, [ + .insertContentAt(r, [ { type: "emoji", attrs: { - id: props.shortcode, - label: props.shortcode, - url: props.url, - source: props.source, + id: p.shortcode, + label: p.shortcode, + url: p.url, + source: p.source, }, }, { type: "text", text: " " }, @@ -484,7 +485,7 @@ export const MarkdownEditor = forwardRef< if (!isEditorReady()) return null; return editor?.getJSON() || null; }, - setContent: (json: any) => { + setContent: (json: JSONContent) => { if (isEditorReady() && json) { editor?.commands.setContent(json); } diff --git a/src/components/editor/MarkdownToolbar.tsx b/src/components/editor/MarkdownToolbar.tsx index 4c1c212..d1389a6 100644 --- a/src/components/editor/MarkdownToolbar.tsx +++ b/src/components/editor/MarkdownToolbar.tsx @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import type { Editor } from "@tiptap/core"; +import type { Editor, ChainedCommands } from "@tiptap/core"; import { Bold, Italic, @@ -83,7 +83,7 @@ export function MarkdownToolbar({ }>({ open: false, url: "" }); const isActive = useCallback( - (name: string, attrs?: Record) => { + (name: string, attrs?: Record) => { if (!editor) return false; return editor.isActive(name, attrs); }, @@ -91,7 +91,7 @@ export function MarkdownToolbar({ ); const run = useCallback( - (command: (chain: any) => any) => { + (command: (chain: ChainedCommands) => void) => { if (!editor) return; command(editor.chain().focus()); }, diff --git a/src/lib/markdown-serializer.ts b/src/lib/markdown-serializer.ts index 034efc5..367b376 100644 --- a/src/lib/markdown-serializer.ts +++ b/src/lib/markdown-serializer.ts @@ -1,4 +1,10 @@ +import type { Editor } from "@tiptap/core"; +import type { + Node as ProseMirrorNode, + Mark as ProseMirrorMark, +} from "@tiptap/pm/model"; import { nip19 } from "nostr-tools"; +import type { EventPointer, AddressPointer } from "nostr-tools/nip19"; import type { EmojiTag, BlobAttachment, @@ -15,7 +21,7 @@ import type { * Returns both the markdown string and extracted metadata (emoji tags, blob * attachments, address refs) needed for building Nostr events. */ -export function serializeEditorToMarkdown(editor: any): SerializedContent { +export function serializeEditorToMarkdown(editor: Editor): SerializedContent { const emojiTags: EmojiTag[] = []; const blobAttachments: BlobAttachment[] = []; const addressRefs: Array<{ @@ -27,7 +33,7 @@ export function serializeEditorToMarkdown(editor: any): SerializedContent { const seenBlobs = new Set(); const seenAddrs = new Set(); - const ctx = { + const ctx: SerializerContext = { emojiTags, blobAttachments, addressRefs, @@ -55,13 +61,13 @@ interface SerializerContext { * Serialize all block-level children of a node, joined by double newlines. */ function serializeBlocks( - node: any, + node: ProseMirrorNode, ctx: SerializerContext, indent: string, ): string { const blocks: string[] = []; - node.forEach((child: any) => { + node.forEach((child) => { const result = serializeBlock(child, ctx, indent); if (result !== null) { blocks.push(result); @@ -75,7 +81,7 @@ function serializeBlocks( * Serialize a single block-level node to markdown. */ function serializeBlock( - node: any, + node: ProseMirrorNode, ctx: SerializerContext, indent: string, ): string | null { @@ -84,13 +90,13 @@ function serializeBlock( return indent + serializeInline(node, ctx); case "heading": { - const level = node.attrs.level || 1; + const level = (node.attrs.level as number) || 1; const prefix = "#".repeat(Math.min(level, 6)); return `${indent}${prefix} ${serializeInline(node, ctx)}`; } case "codeBlock": { - const lang = node.attrs.language || ""; + const lang = (node.attrs.language as string) || ""; const code = node.textContent; return `${indent}\`\`\`${lang}\n${code}\n${indent}\`\`\``; } @@ -105,7 +111,7 @@ function serializeBlock( case "bulletList": { const items: string[] = []; - node.forEach((item: any) => { + node.forEach((item) => { const content = serializeListItemContent(item, ctx, indent + " "); items.push(`${indent}- ${content}`); }); @@ -114,8 +120,8 @@ function serializeBlock( case "orderedList": { const items: string[] = []; - const start = node.attrs.start || 1; - node.forEach((item: any, _offset: number, idx: number) => { + const start = (node.attrs.start as number) || 1; + node.forEach((item, _offset, idx) => { const num = start + idx; const content = serializeListItemContent(item, ctx, indent + " "); items.push(`${indent}${num}. ${content}`); @@ -127,7 +133,11 @@ function serializeBlock( return `${indent}---`; case "blobAttachment": { - const { url, sha256, mimeType, size, server } = node.attrs; + const url = node.attrs.url as string; + const sha256 = node.attrs.sha256 as string; + const mimeType = node.attrs.mimeType as string | undefined; + const size = node.attrs.size as number | undefined; + const server = node.attrs.server as string | undefined; if (!ctx.seenBlobs.has(sha256)) { ctx.seenBlobs.add(sha256); ctx.blobAttachments.push({ url, sha256, mimeType, size, server }); @@ -140,20 +150,25 @@ function serializeBlock( } case "nostrEventPreview": { - const { type, data } = node.attrs; + const previewType = node.attrs.type as string; + const previewData = node.attrs.data as + | string + | EventPointer + | AddressPointer; // Collect address refs for manual a-tags - if (type === "naddr" && data) { - const key = `${data.kind}:${data.pubkey}:${data.identifier || ""}`; + if (previewType === "naddr" && previewData) { + const addr = previewData as AddressPointer; + const key = `${addr.kind}:${addr.pubkey}:${addr.identifier || ""}`; if (!ctx.seenAddrs.has(key)) { ctx.seenAddrs.add(key); ctx.addressRefs.push({ - kind: data.kind, - pubkey: data.pubkey, - identifier: data.identifier || "", + kind: addr.kind, + pubkey: addr.pubkey, + identifier: addr.identifier || "", }); } } - return `${indent}${renderNostrEventPreviewText(type, data)}`; + return `${indent}${renderNostrEventPreviewText(previewType, previewData)}`; } default: @@ -170,14 +185,14 @@ function serializeBlock( * subsequent blocks get their own lines with indentation. */ function serializeListItemContent( - item: any, + item: ProseMirrorNode, ctx: SerializerContext, continuationIndent: string, ): string { const parts: string[] = []; let first = true; - item.forEach((child: any) => { + item.forEach((child) => { if (first) { // First child is inlined (no leading indent) parts.push(serializeBlock(child, ctx, "") || ""); @@ -194,10 +209,13 @@ function serializeListItemContent( /** * Serialize inline content of a block node (text with marks + inline nodes). */ -function serializeInline(node: any, ctx: SerializerContext): string { +function serializeInline( + node: ProseMirrorNode, + ctx: SerializerContext, +): string { let result = ""; - node.forEach((child: any) => { + node.forEach((child) => { if (child.isText) { let text = child.text || ""; // Apply marks — order matters: link wraps bold wraps italic etc. @@ -218,7 +236,7 @@ function serializeInline(node: any, ctx: SerializerContext): string { * Sort marks so nesting is correct: innermost marks first. * code < strike < italic < bold < link */ -function markPriority(a: any, b: any): number { +function markPriority(a: ProseMirrorMark, b: ProseMirrorMark): number { const order: Record = { code: 0, strike: 1, @@ -232,7 +250,7 @@ function markPriority(a: any, b: any): number { /** * Wrap text with the markdown syntax for a mark. */ -function applyMark(mark: any, text: string): string { +function applyMark(mark: ProseMirrorMark, text: string): string { switch (mark.type.name) { case "bold": return `**${text}**`; @@ -243,7 +261,7 @@ function applyMark(mark: any, text: string): string { case "strike": return `~~${text}~~`; case "link": - return `[${text}](${mark.attrs.href || ""})`; + return `[${text}](${(mark.attrs.href as string) || ""})`; default: return text; } @@ -252,18 +270,23 @@ function applyMark(mark: any, text: string): string { /** * Serialize a non-text inline node (mention, emoji, hardBreak). */ -function serializeInlineNode(node: any, ctx: SerializerContext): string { +function serializeInlineNode( + node: ProseMirrorNode, + ctx: SerializerContext, +): string { switch (node.type.name) { case "mention": { try { - return `nostr:${nip19.npubEncode(node.attrs.id)}`; + return `nostr:${nip19.npubEncode(node.attrs.id as string)}`; } catch { - return `@${node.attrs.label || "unknown"}`; + return `@${(node.attrs.label as string) || "unknown"}`; } } case "emoji": { - const { id, url, source } = node.attrs; + const id = node.attrs.id as string; + const url = node.attrs.url as string; + const source = node.attrs.source as string; if (source === "unicode") { return url || ""; } @@ -286,15 +309,18 @@ function serializeInlineNode(node: any, ctx: SerializerContext): string { /** * Render a nostr event preview node back to its bech32 URI. */ -function renderNostrEventPreviewText(type: string, data: any): string { +function renderNostrEventPreviewText( + type: string, + data: string | EventPointer | AddressPointer, +): string { try { switch (type) { case "note": - return `nostr:${nip19.noteEncode(data)}`; + return `nostr:${nip19.noteEncode(data as string)}`; case "nevent": - return `nostr:${nip19.neventEncode(data)}`; + return `nostr:${nip19.neventEncode(data as EventPointer)}`; case "naddr": - return `nostr:${nip19.naddrEncode(data)}`; + return `nostr:${nip19.naddrEncode(data as AddressPointer)}`; default: return ""; }