From d7dba6c61ae5168835d018f0e4e20c4071c06712 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 23 Sep 2024 15:18:39 -0700 Subject: [PATCH] Get link extension working better --- src/app/components/ThreadCard.svelte | 2 +- src/lib/editor/LinkExtension.ts | 47 +++++++++-- src/lib/editor/index.ts | 120 ++++++++++++++------------- src/lib/editor/util.ts | 9 +- 4 files changed, 109 insertions(+), 69 deletions(-) diff --git a/src/app/components/ThreadCard.svelte b/src/app/components/ThreadCard.svelte index b50b8d12..aa101be5 100644 --- a/src/app/components/ThreadCard.svelte +++ b/src/app/components/ThreadCard.svelte @@ -16,7 +16,7 @@ let editor: Readable onMount(() => { - editor = createEditor(getViewOptions(root.content)) + editor = createEditor(getViewOptions(root)) }) diff --git a/src/lib/editor/LinkExtension.ts b/src/lib/editor/LinkExtension.ts index e27badbf..0cf7a5da 100644 --- a/src/lib/editor/LinkExtension.ts +++ b/src/lib/editor/LinkExtension.ts @@ -1,14 +1,11 @@ -import {Node, nodePasteRule, type PasteRuleMatch} from "@tiptap/core" +import {last} from '@welshman/lib' +import {Node, InputRule, nodePasteRule, type PasteRuleMatch} from "@tiptap/core" import type {Node as ProsemirrorNode} from "@tiptap/pm/model" import type {MarkdownSerializerState} from "prosemirror-markdown" +import {createPasteRuleMatch, createInputRuleMatch} from './util' export const LINK_REGEX = - /^([a-z\+:]{2,30}:\/\/)?[^<>\(\)\s]+\.[a-z]{2,6}[^\s]*[^<>"'\.!?,:\s\)\(]*/gi - -export const createPasteRuleMatch = >( - match: RegExpMatchArray, - data: T, -): PasteRuleMatch => ({index: match.index!, replaceWith: match[2], text: match[0], match, data}) + /([a-z\+:]{2,30}:\/\/)?[^<>\(\)\s]+\.[a-z]{2,6}[^\s]*[^<>"'\.!?,:\s\)\(]*/gi export interface LinkAttributes { url: string @@ -65,6 +62,42 @@ export const LinkExtension = Node.create({ }, } }, + addInputRules() { + return [ + new InputRule({ + find: text => { + const match = last(Array.from(text.matchAll(LINK_REGEX))) + + if (match && text.length === match.index + match[0].length + 1) { + return { + index: match.index!, + text: match[0], + data: { + url: match[0], + }, + } + } + + return null + }, + handler: ({state, range, match}) => { + const {tr} = state + + if (match[0]) { + try { + tr.insert(range.from - 1, this.type.create(match.data)) + .delete(tr.mapping.map(range.from - 1), tr.mapping.map(range.to)) + .insert(tr.mapping.map(range.to), this.editor.schema.text(last(Array.from(match.input!)))) + } catch (e) { + // If the node was already linkified, the above code breaks for whatever reason + } + } + + tr.scrollIntoView() + }, + }), + ] + }, addPasteRules() { return [ nodePasteRule({ diff --git a/src/lib/editor/index.ts b/src/lib/editor/index.ts index 2444b2f9..0a14559c 100644 --- a/src/lib/editor/index.ts +++ b/src/lib/editor/index.ts @@ -78,68 +78,70 @@ export const getEditorOptions = ({ loading, getPubkeyHints, submitOnEnter, -}: EditorOptions) => { - return { - content: "", - autofocus: true, - extensions: [ - Code, - CodeBlock, - Document, - Dropcursor, - Gapcursor, - History, - Paragraph, - Text, - submitOnEnter ? getModifiedHardBreakExtension() : HardBreakExtension, - LinkExtension.extend({ - addNodeView: () => SvelteNodeViewRenderer(EditLink), - }), - Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})), - NProfileExtension.extend({ - addNodeView: () => SvelteNodeViewRenderer(EditMention), - addProseMirrorPlugins() { - return [ - createSuggestions({ - char: "@", - name: "nprofile", - editor: this.editor, - search: profileSearch, - select: (pubkey: string, props: any) => { - const relays = getPubkeyHints(pubkey) - const nprofile = nprofileEncode({pubkey, relays}) +}: EditorOptions) => ({ + content: "", + autofocus: true, + extensions: [ + Code, + CodeBlock, + Document, + Dropcursor, + Gapcursor, + History, + Paragraph, + Text, + submitOnEnter ? getModifiedHardBreakExtension() : HardBreakExtension, + LinkExtension.extend({ + addNodeView: () => SvelteNodeViewRenderer(EditLink), + }), + Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})), + NProfileExtension.extend({ + addNodeView: () => SvelteNodeViewRenderer(EditMention), + addProseMirrorPlugins() { + return [ + createSuggestions({ + char: "@", + name: "nprofile", + editor: this.editor, + search: profileSearch, + select: (pubkey: string, props: any) => { + const relays = getPubkeyHints(pubkey) + const nprofile = nprofileEncode({pubkey, relays}) - return props.command({pubkey, nprofile, relays}) - }, - suggestionComponent: SuggestionProfile, - suggestionsComponent: Suggestions, - }), - ] - }, - }), - NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})), - NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})), - ImageExtension.extend( - asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)}), - ).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}), - VideoExtension.extend( - asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)}), - ).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}), - FileUploadExtension.configure({ - immediateUpload: false, - sign: (event: StampedEvent) => { - loading.set(true) + return props.command({pubkey, nprofile, relays}) + }, + suggestionComponent: SuggestionProfile, + suggestionsComponent: Suggestions, + }), + ] + }, + }), + NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})), + NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})), + ImageExtension.extend( + asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)}), + ).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}), + VideoExtension.extend( + asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)}), + ).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}), + FileUploadExtension.configure({ + immediateUpload: false, + sign: (event: StampedEvent) => { + loading.set(true) - return signer.get()!.sign(event) - }, - onComplete: () => { - loading.set(false) - submit() - }, - }), - ], + return signer.get()!.sign(event) + }, + onComplete: () => { + loading.set(false) + submit() + }, + }), + ], + onTransaction() { + // @ts-ignore + console.log(this.getJSON()) } -} +}) type ViewOptions = { content: string diff --git a/src/lib/editor/util.ts b/src/lib/editor/util.ts index 76c99cda..67edb1c2 100644 --- a/src/lib/editor/util.ts +++ b/src/lib/editor/util.ts @@ -1,4 +1,4 @@ -import type {JSONContent, PasteRuleMatch} from "@tiptap/core" +import type {JSONContent, PasteRuleMatch, InputRuleMatch} from "@tiptap/core" import {Editor} from "@tiptap/core" import {choice} from "@welshman/lib" @@ -8,10 +8,15 @@ export const asInline = (extend: Record) => ({ ...extend, }) +export const createInputRuleMatch = >( + match: RegExpMatchArray, + data: T, +): InputRuleMatch => ({index: match.index!, text: match[0], match, data}) + export const createPasteRuleMatch = >( match: RegExpMatchArray, data: T, -): PasteRuleMatch => ({index: match.index!, replaceWith: match[2], text: match[0], match, data}) +): PasteRuleMatch => ({index: match.index!, text: match[0], match, data}) export const findNodes = (type: string, json: JSONContent) => { const results: JSONContent[] = []