npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

nostr-editor

v0.0.3

Published

[![CI Checks](https://github.com/cesardeazevedo/nostr-editor/actions/workflows/ci-checks.yml/badge.svg)](https://github.com/cesardeazevedo/nostr-editor/actions/workflows/ci-checks.yml)

Downloads

80

Readme

nostr-editor

CI Checks

nostr-editor is a collection of Tiptap extensions designed to enhance the user experience when creating and editing nostr notes. It also provides tools for parsing existing notes into a structured content schema.

What is tiptap?

Tiptap is a headless wrapper around ProseMirror, offering a more developer-friendly API for building rich text editors. nostr-editor uses Tiptap to simplify integration with frameworks like React and Svelte, making it easy to create customized nostr-compatible editors.

What is prosemirror?

ProseMirror is the underlying core framework that powers Tiptap and other WYSIWYG (what-you-see-is-what-you-get) editors.

Features

  • Fully customizable extensions
  • Parse existing nostr events, including imeta tags (NIP-94)
  • Automatically convert nostr links to their appropriate nodes during paste operations (nostr:nevent1, nostr:nprofile1, nostr:naddr, nostr:npub, nostr:note1)
  • Handle file uploads to a NIP-96 or blossom compatible server
  • Supports markdown long-form content
  • Supports bolt11 invoices
  • Supports youtube and tweet links
  • Automatically rejects and alerts if the user mistakenly pastes an nsec1 key.

Demo

https://cesardeazevedo.github.io/nostr-editor/

Installing

To use nostr-editor, you'll need to install a few dependencies:

pnpm add nostr-editor @tiptap/starter-kit @tiptap/core tiptap-markdown

react dependencies

pnpm add @tiptap/react

svelte dependencies

pnpm add svelte-tiptap

Usage

React

Here's a basic setup example using React:

import { Editor } from '@tiptap/core'
import { useEditor, EditorContent, ReactNodeViewRenderer } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function MyEditor() {
  const editor = useEditor({
    autofocus: true,
    extensions: [
      StarterKit,
      NostrExtension.configure({
        extend: {
          nprofile: { addNodeView: () => ReactNodeViewRenderer(MyReactMentionComponent) },
          nevent: { addNodeView: () => ReactNodeViewRenderer(MyReactNEventComponent) },
          naddr: { addNodeView: () => ReactNodeViewRenderer(MyReactNaddrComponent) },
          image: { addNodeView: () => ReactNodeViewRenderer(MyReactImageComponent) },
          video: { addNodeView: () => ReactNodeViewRenderer(MyReactVideoComponent) },
          tweet: { addNodeView: () => ReactNodeViewRenderer(MyReactTweetComponent) },
        },
        link: { autolink: true }
      }),
    ],
    onUpdate: () => {
      const contentSchema = editor.getJSON()
      const contentText = editor.getText()
    },
  })

  return (
    <EditorContent editor={editor} />
  )
}

svelte

<script lang="ts">
  import { onMount } from 'svelte'
  import type { Readable } from 'svelte/store'
  import { createEditor, type Editor, EditorContent, SvelteNodeViewRenderer } from 'svelte-tiptap'
  import StarterKit from '@tiptap/starter-kit'
  import { NostrExtension } from 'nostr-editor'

  let editor: Readable<Editor>

  onMount(() => {
    editor = new Editor({
      extensions: [
        StarterKit,
        NostrExtension.configure({
          extend: {
            nprofile: { addNodeView: () => SvelteNodeViewRenderer(MySvelteMentionComponent) },
            nevent: { addNodeView: () => SvelteNodeViewRenderer(MySvelteNEventComponent) },
            naddr: { addNodeView: () =>  SvelteNodeViewRenderer(MySvelteNaddrComponent) },
            image: { addNodeView: () => SvelteNodeViewRenderer(MySvelteImageComponent) },
            video: { addNodeView: () => SvelteNodeViewRenderer(MySvelteVideoComponent) },
            tweet: { addNodeView: () => SvelteNodeViewRenderer(MySvelteTweetComponent) },
          },
        }),
      ],
      content: '',
      onUpdate: () => {
        contentSchema = $editor.getJSON()
        contentText = $editor.getText()
      },
    })
  })
</script>

<main>
  <EditorContent editor={$editor} />
</main>

Rendering node views

nostr-editor is framework-agnostic and does not ship with pre-built components (yet). You should provide your own React or Svelte components for each extension.

NostrExtension.configure({
  extend: {
    nprofile: { addNodeView: () => ReactNodeViewRenderer(MyReactMentionComponent) },
    ...
  },
}),
import type { NodeViewProps } from '@tiptap/core'
import { NodeViewWrapper } from '@tiptap/react'

export function MyReactMentionComponent(props: NodeViewProps) {
  const { pubkey, relays } = props.node.attrs
  const { getProfile } = useNDK() // nostr-tools or other nostr client library
  return (
    <NodeViewWrapper as='span'>
      @{getProfile(pubkey).display_name}
    </NodeViewWrapper>
  )
}

Image Upload

To handle image uploads with nostr-editor, you can configure the extension as follows:

NostrExtension.configure({
  image: {
    defaultUploadUrl: 'https://nostr.build',
    defaultUploadType: 'nip96', // or blossom
  },
  video: {
    defaultUploadUrl: 'https://nostr.build',
    defaultUploadType: 'nip96', // or blossom
  },
  fileUpload: {
    immediateUpload: true, // It will automatically upload when a file is added to the editor, if false, call `editor.commands.uploadFiles()` manually
    sign: async (event) => {
      if ('nostr' in window) {
        const nostr = window.nostr as NostrExtension
        return await nostr.signEvent(event)
      }
    },
    onDrop() {
      // File added to the editor
    },
    onComplete() {
      // All files were successfully uploaded
    },
  },
}),

Trigger a input type='file' popup

Parsing existing notes

You can set the editor an existing nostr event in order to parse it's contents


const event = {
  kind: 1,
  content: 'Hello nostr:nprofile1qy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcprfmhxue69uhhyetvv9ujuem9w3skccne9e3k7mf0wccsqgxxvqas78x0a339m8qgkaf7fam5atmarne8dy3rzfd4l4x6w2qpncmfs8zh'
  ...
}

editor.commands.setEventContent(event)
editor.getJSON()

Response

{
  "type": "doc",
  "content": [
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "Hello " },
        {
          "type": "nprofile",
          "attrs": {
            "nprofile": "nostr:nprofile1qy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcprfmhxue69uhhyetvv9ujuem9w3skccne9e3k7mf0wccsqgxxvqas78x0a339m8qgkaf7fam5atmarne8dy3rzfd4l4x6w2qpncmfs8zh",
            "pubkey": "c6603b0f1ccfec625d9c08b753e4f774eaf7d1cf2769223125b5fd4da728019e",
            "relays": ["wss://nos.lol/", "wss://relay.damus.io/", "wss://relay.getalby.com/v1"]
          }
        }
      ]
    }
  ]
}

Parsing existing long-form content notes

The same thing as a normal note, just make sure your added the Markdown extension from tiptap-markdown

import { Markdown } from 'tiptap-markdown'

const editor = useEditor({
  autofocus: true,
  extensions: [
    StarterKit,
    Markdown.configure({
      transformCopiedText: true,
      transformPastedText: true,
    }),
    NostrExtension.configure({
      link: { autolink: true }, // needed for markdown links
    }),
  ],
})

Commands

nostr-editor provides several commands to insert various types of content and manage media uploads.

insertNevent

editor.commands.insertNEvent({ nevent: 'nostr:nevent1...' })

insertNprofile

editor.commands.insertNProfile({ nprofile: 'nostr:nprofile1...' })

insertNAddr

editor.commands.insertNAddr({ naddr: 'nostr:naddr1...' })

insertBolt11

editor.commands.insertBolt11({ lnbc: 'lnbc...' })

selectFiles

Triggers a input type='file' click

editor.commands.selectFiles()

uploadFiles

Upload all pending images and videos,

editor.commands.uploadFiles()

This command returns true when the upload starts, not when the upload is completed. You can use onComplete() callback in the fileUpload extension options.

const editor = useEditor({
  extensions: [
    NostrExtension.configure({
      fileUpload: {
        onComplete: () => console.log('All files uploaded'),
      },
    }),
  ],
})

Note: all nostr: prefixes are optional

Roadmap

  • Add support for Asciidoc
  • Issue #2
  • Implement collaborative editing features with Yjs

References

License

MIT