Remove svelte from editor

This commit is contained in:
Jon Staab
2025-02-04 20:06:50 -08:00
parent 63cfebe673
commit 0964fe762e
33 changed files with 534 additions and 2655 deletions
@@ -0,0 +1,31 @@
import {HardBreak, type HardBreakOptions} from "@tiptap/extension-hard-break"
export interface BreakOrSubmitOptions extends HardBreakOptions {
/** Handler for when enter is pressed. */
submit: () => void
/** Whether to call `submit` on unmodified Enter */
aggressive?: boolean
}
export const BreakOrSubmit = HardBreak.extend<BreakOrSubmitOptions>({
addKeyboardShortcuts() {
return {
"Shift-Enter": () => this.editor.commands.setHardBreak(),
"Mod-Enter": () => {
this.options.submit()
return true
},
Enter: () => {
if (this.options.aggressive) {
this.options.submit()
return true
}
return false
},
}
},
})
@@ -0,0 +1,75 @@
import {InputRule, mergeAttributes, Node, PasteRule} from "@tiptap/core"
import type {CodeOptions} from "@tiptap/extension-code"
const inputRegex = /(?:^|\s)(`(?!\s+`)((?:[^`]+))`(?!\s+`))$/
const pasteRegex = /(?:^|\s)(`(?!\s+`)((?:[^`]+))`(?!\s+`))/g
export type CodeInlineOptions = object
export const CodeInline = Node.create<CodeOptions>({
name: "codeInline",
content: "text*",
marks: "",
group: "inline",
inline: true,
code: true,
defining: true,
addOptions() {
return {
HTMLAttributes: {},
}
},
parseHTML() {
return [{tag: "code"}]
},
renderHTML({HTMLAttributes}) {
return ["code", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
addKeyboardShortcuts() {
return {
// remove code block when at start of document or code block is empty
Backspace: () => {
const {empty, $anchor, $from} = this.editor.state.selection
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2
if (!empty || $anchor.parent.type.name !== this.name) {
return false
}
if (isAtEnd) {
const {tr} = this.editor.state
tr.delete($from.start(), $from.end() + 1)
this.editor.view.dispatch(tr)
}
return false
},
}
},
addInputRules() {
return [
new InputRule({
find: inputRegex,
handler: ({state, range, match}) => {
const textNode = state.schema.text(match[2])
const codeNode = this.type.create(null, textNode)
state.tr.replaceWith(range.from, range.to, codeNode).insertText(" ")
},
}),
]
},
addPasteRules() {
return [
new PasteRule({
find: pasteRegex,
handler: ({state, range, match}) => {
const textNode = state.schema.text(match[2])
const codeNode = this.type.create(null, textNode)
state.tr.replaceWith(range.from, range.to, codeNode).insertText(" ")
},
}),
]
},
})
+227
View File
@@ -0,0 +1,227 @@
import type {StampedEvent, SignedEvent} from "@welshman/util"
import {deepMergeLeft} from "@welshman/lib"
import type {Extensions, AnyExtension} from "@tiptap/core"
import {CodeBlock} from "@tiptap/extension-code-block"
import type {CodeBlockOptions} from "@tiptap/extension-code-block"
import {Document} from "@tiptap/extension-document"
import {Dropcursor} from "@tiptap/extension-dropcursor"
import type {DropcursorOptions} from "@tiptap/extension-dropcursor"
import {Gapcursor} from "@tiptap/extension-gapcursor"
import {History} from "@tiptap/extension-history"
import type {HistoryOptions} from "@tiptap/extension-history"
import {Paragraph} from "@tiptap/extension-paragraph"
import type {ParagraphOptions} from "@tiptap/extension-paragraph"
import {Text} from "@tiptap/extension-text"
import {Placeholder} from "@tiptap/extension-placeholder"
import type {PlaceholderOptions} from "@tiptap/extension-placeholder"
import type {
NostrOptions,
FileUploadOptions,
ImageOptions,
LinkOptions,
NSecRejectOptions,
} from "nostr-editor"
import {
NostrExtension,
Bolt11Extension,
FileUploadExtension,
ImageExtension,
LinkExtension,
NAddrExtension,
NEventExtension,
NProfileExtension,
TagExtension,
VideoExtension,
NSecRejectExtension,
} from "nostr-editor"
import {WordCount} from "./WordCount.js"
import {CodeInline, type CodeInlineOptions} from "./CodeInline.js"
import {BreakOrSubmit, type BreakOrSubmitOptions} from "./BreakOrSubmit.js"
import {MentionNodeView, Bolt11NodeView, MediaNodeView, EventNodeView} from "../nodeviews/index.js"
export type ChildExtensionOptions<C = any, E = any> =
| false
| {
extend?: Partial<E>
config?: Partial<C>
}
export type EmptyOptions = object
export type WelshmanExtensionOptions = {
bolt11?: false
breakOrSubmit?: ChildExtensionOptions<BreakOrSubmitOptions>
codeInline?: ChildExtensionOptions<CodeInlineOptions>
codeBlock?: ChildExtensionOptions<CodeBlockOptions>
document?: false
dropcursor?: ChildExtensionOptions<DropcursorOptions>
fileUpload?: ChildExtensionOptions<FileUploadOptions>
gapcursor?: false
history?: ChildExtensionOptions<HistoryOptions>
image?: ChildExtensionOptions<ImageOptions>
link?: ChildExtensionOptions<LinkOptions>
naddr?: ChildExtensionOptions<EmptyOptions>
nevent?: ChildExtensionOptions<EmptyOptions>
nprofile?: ChildExtensionOptions<EmptyOptions>
nsecReject?: ChildExtensionOptions<NSecRejectOptions>
paragraph?: ChildExtensionOptions<ParagraphOptions>
placeholder?: ChildExtensionOptions<PlaceholderOptions>
tag?: false
text?: false
video?: false
wordCount?: false
}
export interface WelshmanOptions extends NostrOptions {
submit?: () => void
sign?: (event: StampedEvent) => Promise<SignedEvent>
defaultUploadUrl?: string
defaultUploadType?: "nip96" | "blossom"
extensions?: WelshmanExtensionOptions
}
export const WelshmanExtension = NostrExtension.extend<WelshmanOptions>({
// Return an empty object or else options can't be passed
addOptions() {
return {}
},
addExtensions() {
const {
sign,
submit,
defaultUploadUrl = "https://nostr.build",
defaultUploadType = "nip96",
} = this.options
if (!sign) throw new Error("sign is a required argument to WelshmanExtension")
if (!submit) throw new Error("submit is a required argument to WelshmanExtension")
const extensionOptions = deepMergeLeft(this.options.extensions || {}, {
codeInline: {
extend: {
renderText: (props: any) => "`" + props.node.textContent + "`",
},
},
codeBlock: {
extend: {
renderText: (props: any) => "```" + props.node.textContent + "```",
},
},
bolt11: {
config: {
inline: true,
group: "inline",
},
extend: {
addNodeView: () => Bolt11NodeView,
},
},
image: {
config: {
inline: true,
group: "inline",
defaultUploadUrl,
defaultUploadType,
},
extend: {
addNodeView: () => MediaNodeView,
},
},
video: {
config: {
inline: true,
group: "inline",
defaultUploadUrl,
defaultUploadType,
},
extend: {
addNodeView: () => MediaNodeView,
},
},
nevent: {
config: {
inline: true,
group: "inline",
},
extend: {
addNodeView: () => EventNodeView,
},
},
naddr: {
config: {
inline: true,
group: "inline",
},
extend: {
addNodeView: () => EventNodeView,
},
},
nprofile: {
extend: {
addNodeView: () => MentionNodeView,
},
},
breakOrSubmit: {
config: {
submit,
},
},
fileUpload: {
config: {
sign,
immediateUpload: true,
allowedMimeTypes: [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"video/mp4",
"video/mpeg",
"video/webm",
],
},
},
}) as WelshmanExtensionOptions
const extensions: Extensions = []
const addExtension = (extension: AnyExtension, options?: ChildExtensionOptions | false) => {
if (options === false) return
if (options?.extend) {
extension = extension.extend(options.extend)
}
if (options?.config) {
extension = extension.configure(options.config)
}
extensions.push(extension)
}
addExtension(Document, extensionOptions.document)
addExtension(Text, extensionOptions.text)
addExtension(Paragraph, extensionOptions.paragraph)
addExtension(History, extensionOptions.history)
addExtension(CodeBlock, extensionOptions.codeBlock)
addExtension(CodeInline, extensionOptions.codeInline)
addExtension(Dropcursor, extensionOptions.dropcursor)
addExtension(FileUploadExtension, extensionOptions.fileUpload)
addExtension(Gapcursor, extensionOptions.gapcursor)
addExtension(BreakOrSubmit, extensionOptions.breakOrSubmit)
addExtension(ImageExtension, extensionOptions.image)
addExtension(LinkExtension, extensionOptions.link)
addExtension(NAddrExtension, extensionOptions.naddr)
addExtension(NEventExtension, extensionOptions.nevent)
addExtension(NProfileExtension, extensionOptions.nprofile)
addExtension(NSecRejectExtension, extensionOptions.nsecReject)
addExtension(Placeholder, extensionOptions.placeholder)
addExtension(TagExtension, extensionOptions.tag)
addExtension(VideoExtension, extensionOptions.video)
addExtension(Bolt11Extension, extensionOptions.bolt11)
addExtension(WordCount, extensionOptions.wordCount)
return extensions
},
})
@@ -0,0 +1,19 @@
import {Extension} from "@tiptap/core"
export const WordCount = Extension.create({
name: "wordCount",
addStorage() {
return {
words: 0,
chars: 0,
}
},
onUpdate() {
const text = this.editor.state.doc.textContent
this.storage.words = text.split(/\s+/).filter(word => word.length > 0).length
this.storage.chars = text.length
},
})
+4
View File
@@ -0,0 +1,4 @@
export * from "./BreakOrSubmit.js"
export * from "./CodeInline.js"
export * from "./Welshman.js"
export * from "./WordCount.js"