diff --git a/package.json b/package.json index 653b2c27..977ea70f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@automerge/automerge": "^2.2.2", "@automerge/automerge-codemirror": "^0.0.11", + "@automerge/prosemirror": "0.0.13-alpha.1", "@automerge/automerge-repo": "^1.1.12", "@automerge/automerge-repo-network-broadcastchannel": "^1.1.12", "@automerge/automerge-repo-network-messagechannel": "^1.1.12", @@ -62,6 +63,13 @@ "next-themes": "^0.2.1", "openai": "^4.11.0", "query-string": "^8.2.0", + "prosemirror-commands": "^1.5.2", + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.19.4", + "prosemirror-schema-list": "^1.3.0", + "prosemirror-state": "^1.4.3", + "prosemirror-view": "^1.33.1", + "query-string": "^8.1.0", "react": "^18.2.0", "react-arborist": "^3.3.1", "react-dom": "^18.2.0", @@ -73,6 +81,8 @@ "tailwindcss-animate": "^1.0.7", "use-resize-observer": "^9.1.0", "uuid": "^9.0.1", + "vite-plugin-top-level-await": "^1.4.1", + "vite-plugin-wasm": "^3.3.0", "xstate": "^5.9.1" }, "devDependencies": { @@ -95,7 +105,7 @@ "tailwindcss": "^3.3.3", "tsc-alias": "^1.8.8", "typescript": "^5.0.2", - "vite": "^5.0.12", + "vite": "^5.1.6", "vite-plugin-top-level-await": "^1.4.1", "vite-plugin-wasm": "^3.3.0", "vitest": "^0.34.6" diff --git a/src/datatypes/richtee/datatype.ts b/src/datatypes/richtee/datatype.ts new file mode 100644 index 00000000..c02c15b2 --- /dev/null +++ b/src/datatypes/richtee/datatype.ts @@ -0,0 +1,36 @@ +import { next as am } from "@automerge/automerge" +import { Text } from "lucide-react"; + +export const init = (doc: any) => { + doc.content = "Untitled"; + am.splitBlock(doc, ["content"], 0, {type: new am.RawString("heading"), parents: [], attrs: { level: 1}}) + doc.commentThreads = {}; +}; + +export const getTitle = async (doc: any) => { + const spans = am.spans(doc, ["content"]) + while (spans.length > 0) { + const span = spans.shift() + if (span.type === "block" && span.value.type instanceof am.RawString && span.value.type.val === "heading") { + if (spans[0].type === "text") { + return spans[0].value + } + } + } + return "Untitled Rich Tee" +}; + +export const markCopy = (doc: any) => { + return doc +} + +const RichEssayDatatype = { + id: "rich-text", + name: "Rich Essay", + icon: Text, + init, + getTitle, + markCopy, +}; + +export default RichEssayDatatype; diff --git a/src/datatypes/richtee/index.ts b/src/datatypes/richtee/index.ts new file mode 100644 index 00000000..f2a7f267 --- /dev/null +++ b/src/datatypes/richtee/index.ts @@ -0,0 +1,4 @@ +import { default as RichEssayDatatype } from "./datatype"; +export default RichEssayDatatype; + +export * from "./schema"; diff --git a/src/datatypes/richtee/schema.ts b/src/datatypes/richtee/schema.ts new file mode 100644 index 00000000..5e369eb8 --- /dev/null +++ b/src/datatypes/richtee/schema.ts @@ -0,0 +1,3 @@ +export type RichTextDoc = { + content: string +} diff --git a/src/os/datatypes.ts b/src/os/datatypes.ts index 63622ffe..8d700961 100644 --- a/src/os/datatypes.ts +++ b/src/os/datatypes.ts @@ -18,6 +18,7 @@ import folder from "@/datatypes/folder"; import kanban from "@/datatypes/kanban"; import markdown from "@/datatypes/markdown"; import tldraw from "@/datatypes/tldraw"; +import richText from "@/datatypes/richtee"; import { FileExportMethod } from "./fileExports"; export type CoreDataType = { @@ -126,6 +127,7 @@ export const DATA_TYPES: Record< bot, kanban, folder, + [richText.id]: richText, } as const; export type DatatypeId = keyof typeof DATA_TYPES; diff --git a/src/os/tools.ts b/src/os/tools.ts index 0ca40405..611c70bd 100644 --- a/src/os/tools.ts +++ b/src/os/tools.ts @@ -6,6 +6,7 @@ import folder from "@/tools/folder"; import datagrid from "@/tools/datagrid"; import bot from "@/tools/bot"; import kanban from "@/tools/kanban"; +import richessay from "@/tools/richessay"; import { AutomergeUrl } from "@automerge/automerge-repo"; import { @@ -66,6 +67,7 @@ const getToolsMap = (tools: Tool[]): Record => { export const TOOLS = getToolsMap([ essay, + richessay, tldraw, folder, datagrid, diff --git a/src/tools/richessay/components/ImageForm.tsx b/src/tools/richessay/components/ImageForm.tsx new file mode 100644 index 00000000..7d502397 --- /dev/null +++ b/src/tools/richessay/components/ImageForm.tsx @@ -0,0 +1,20 @@ +import React from "react" + +type Props = { + onImageChosen: (url: string) => void +} + +export default function ImageForm({ onImageChosen }: Props) { + const [imageUrl, setImageUrl] = React.useState("") + + const onFormSubmit = (e: React.FormEvent) => { + e.preventDefault() + onImageChosen(imageUrl) + } + return ( +
+ setImageUrl(e.target.value)} value={imageUrl} /> + +
+ ) +} diff --git a/src/tools/richessay/components/LinkForm.tsx b/src/tools/richessay/components/LinkForm.tsx new file mode 100644 index 00000000..5759ec45 --- /dev/null +++ b/src/tools/richessay/components/LinkForm.tsx @@ -0,0 +1,20 @@ +import React from "react" + +type Props = { + onUrlChosen: (url: string) => void +} + +export default function ImageForm({ onUrlChosen }: Props) { + const [linkUrl, setLinkUrl] = React.useState("") + + const onFormSubmit = (e: React.FormEvent) => { + e.preventDefault() + onUrlChosen(linkUrl) + } + return ( +
+ setLinkUrl(e.target.value)} value={linkUrl} /> + +
+ ) +} diff --git a/src/tools/richessay/components/Modal.tsx b/src/tools/richessay/components/Modal.tsx new file mode 100644 index 00000000..4a789d6d --- /dev/null +++ b/src/tools/richessay/components/Modal.tsx @@ -0,0 +1,30 @@ +import React, { useEffect, useRef, useState } from "react" + +type Props = { + children: React.ReactNode + onClose: () => void + isOpen: boolean +} + +export default function Modal({ children, isOpen, onClose }: Props) { + const modalRef = useRef(null) + const [isModalOpen, setIsOpen] = useState(false) + + useEffect(() => { + setIsOpen(isOpen) + }, [isOpen]) + + useEffect(() => { + if (!modalRef.current) { + return + } + if (isModalOpen) { + modalRef.current.showModal() + } else { + modalRef.current.close() + onClose() + } + }, [isModalOpen]) + + return {children} +} diff --git a/src/tools/richessay/components/RichTeeEditor.tsx b/src/tools/richessay/components/RichTeeEditor.tsx new file mode 100644 index 00000000..f5d8ab93 --- /dev/null +++ b/src/tools/richessay/components/RichTeeEditor.tsx @@ -0,0 +1,26 @@ +import React from "react" +import { AutomergeUrl } from "@automerge/automerge-repo" +import { useDocument, useHandle } from "@automerge/automerge-repo-react-hooks" +import { RichTextEditor } from "./RichTextEditor" + +type TeeDoc = { + content: string +} + +export const RichTeeEditor = ({ docUrl }: { docUrl: AutomergeUrl }) => { + const [doc] = useDocument(docUrl); // used to trigger re-rendering when the doc loads + const handle = useHandle(docUrl); + + if (!doc) { + return null + } + + return ( +
+ +
+ ); +} diff --git a/src/tools/richessay/components/RichTextEditor.tsx b/src/tools/richessay/components/RichTextEditor.tsx new file mode 100644 index 00000000..9d7fbfff --- /dev/null +++ b/src/tools/richessay/components/RichTextEditor.tsx @@ -0,0 +1,425 @@ +import React, {useEffect, useState, useRef} from "react" +import { AutoMirror, basicSchemaAdapter } from "@automerge/prosemirror" +import { EditorView } from "prosemirror-view" +import { MarkType, Schema } from "prosemirror-model" +import { baseKeymap, toggleMark, setBlockType, chainCommands, wrapIn} from "prosemirror-commands" +import { keymap } from "prosemirror-keymap" +import { DocHandle, DocHandleChangePayload } from "@automerge/automerge-repo" +import { type TeeDoc } from "../schema" +import {Command, EditorState, Transaction} from "prosemirror-state" +import { splitListItem, liftListItem, sinkListItem, wrapInList } from "prosemirror-schema-list" +import { Braces, Bold, Italic, List, ListOrdered, Link, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Indent, Outdent, TextQuote, Image} from "lucide-react"; +import "prosemirror-view/style/prosemirror.css" +//import "../../tee/index.css"; +import "../index.css"; +import Modal from "./Modal" +import LinkForm from "./LinkForm" +import ImageForm from "./ImageForm" + +export type EditorProps = { + handle: DocHandle; + path: string[]; +} + +export const RichTextEditor = ({ handle, path }: EditorProps) => { + const editorRoot = useRef(null) + const [view, setView] = useState(null) + const [editorCrashed, setEditorCrashed] = useState(false); + const [imageModalOpen, setImageModalOpen] = useState(false) + const [linkModalOpen, setLinkModalOpen] = useState(false) + + const handleReady = handle.isReady(); + + useEffect(() => { + if (!handleReady) { + return + } + const autoMirror = new AutoMirror(path, basicSchemaAdapter) + const initialDoc = autoMirror.initialize(handle) + const editorConfig = { + schema: autoMirror.schema, + history, + plugins: [ + keymap({ + "Mod-b": toggleBold(autoMirror.schema), + "Mod-i": toggleItalic(autoMirror.schema), + "Mod-l": toggleMark(autoMirror.schema.marks.link, { + href: "https://example.com", + title: "example", + }), + "Enter": splitListItem(autoMirror.schema.nodes.list_item), + }), + keymap({...baseKeymap}), + ], + doc: initialDoc, + } + + const state = EditorState.create(editorConfig) + const view = new EditorView(editorRoot.current, { + state, + dispatchTransaction: (tx: Transaction) => { + try { + const newState = autoMirror.intercept(handle, tx, view.state) + view.updateState(newState) + } catch (e) { + console.error( + "Encountered an error in dispatch function; crashing the editor to notify the user and avoid data loss." + ); + console.error(e); + setEditorCrashed(true) + view.destroy() + } + }, + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const onPatch: (args: DocHandleChangePayload) => void = ( + {doc, patches, patchInfo} + ) => { + console.log(`patch received`) + const newState = autoMirror.reconcilePatch( patchInfo.before, doc, patches, view.state) + view.updateState(newState) + } + handle.on("change", onPatch) + + setView(view) + + return () => { + handle.off("change", onPatch) + view.destroy() + } + }, [handle, handleReady]) + + const onBoldClicked = () => { + if (view) { + toggleBold(view.state.schema)(view.state, view.dispatch, view) + } + } + + const onItalicClicked = () => { + if (view) { + toggleItalic(view.state.schema)(view.state, view.dispatch, view) + } + } + + const onIncreaseIndent = () => { + if (view) { + // If we're in a list, figure out what kind it is + const { $from } = view.state.selection + let listNode = null + for (let i = $from.depth; i > 0; i--) { + if ($from.node(i).type.name === "list_item") { + listNode = $from.node(i - 1) + break + } + } + const listType = listNode ? listNode.type : view.state.schema.nodes.bullet_list + if (listNode) { + chainCommands( + sinkListItem(view.state.schema.nodes.list_item), + wrapInList(listType), + )(view.state, view.dispatch, view) + } + } + } + + const onDecreaseIndent = () => { + if (view) { + liftListItem(view.state.schema.nodes.list_item)(view.state, view.dispatch, view) + } + } + + const onBlockQuoteClicked = () => { + if (view) { + turnSelectionIntoBlockquote(view.state, view.dispatch, view) + } + } + + const onToggleOrderedList = () => { + if (view) { + wrapInList(view.state.schema.nodes.bullet_list)( + view.state, + view.dispatch, + view, + ) + } + } + + const onToggleNumberedList = () => { + if (view) { + wrapInList(view.state.schema.nodes.ordered_list)( + view.state, + view.dispatch, + view, + ) + } + } + + const onHeadingClicked = (level: number) => { + if (view) { + const { $from } = view.state.selection + if ( + $from.node().type.name === "heading" && + $from.node().attrs.level === level + ) { + setBlockType(view.state.schema.nodes.paragraph)( + view.state, + view.dispatch, + view, + ) + } else { + setBlockType(view.state.schema.nodes.heading, { level })( + view.state, + view.dispatch, + view, + ) + } + } + } + + const showImageDialog = () => { + setImageModalOpen(true) + } + + const onImageChosen = (url: string) => { + if (view) { + const { from, to } = view.state.selection + const tr = view.state.tr + tr.replaceRangeWith( + from, + to, + view.state.schema.nodes.image.create({ src: url, title: "", alt: "" }), + ) + view.dispatch(tr) + } + } + + const showLinkDialog = () => { + setLinkModalOpen(true) + } + + const onLinkChosen = (url: string) => { + if (view) { + const { from, to } = view.state.selection + const tr = view.state.tr + tr.addMark(from, to, view.state.schema.marks.link.create({ href: url, title: "" })) + view.dispatch(tr) + } + } + + const onCodeClicked = () => { + if (view) { + setBlockType(view.state.schema.nodes.code_block)(view.state, view.dispatch, view) + } + } + + + if (editorCrashed) { + return ( +
+

⛔️ Error: editor crashed!

+ {import.meta.env.MODE === "development" && ( +

Probably due to hot reload in dev.

+ )} +

+ We're sorry for the inconvenience. Please reload to keep working. Your + data was most likely saved before the crash. +

+

+ If you'd like you can screenshot the dev console as a bug report. +

+
+ ); + } + + return
+ +
+ {/* This has some subtle behavior for responsiveness. + - We use container queries to adjust the width of the editor based on the size of our container. + - We get the right line width by hardcoding a max-width and x-padding + - We take over the full screen on narrow displays (showing comments on mobile is TODO) + */} +
+
+
+
+ { + setImageModalOpen(false) + }} + > + { + setImageModalOpen(false) + onImageChosen(url) + }} + /> + + { + setLinkModalOpen(false) + }} + > + { + setLinkModalOpen(false) + onLinkChosen(url) + }} + /> + +
+} + + +const toggleBold = (schema: Schema) => toggleMarkCommand(schema.marks.strong) +const toggleItalic = (schema: Schema) => toggleMarkCommand(schema.marks.em) + +function toggleMarkCommand(mark: MarkType): Command { + return ( + state: EditorState, + dispatch: ((tr: Transaction) => void) | undefined + ) => { + return toggleMark(mark)(state, dispatch) + } +} + +type MenuBarProps = { + onBoldClicked: () => void + onItalicClicked: () => void + onLinkClicked: () => void + onBlockQuoteClicked: () => void + onToggleOrderedList: () => void + onToggleNumberedList: () => void + onIncreaseIndent: () => void + onDecreaseIndent: () => void + onHeadingClicked: (level: number) => void + onImageClicked: () => void + onCodeClicked: () => void +} + +function MenuBar({ + onBoldClicked, + onItalicClicked, + onLinkClicked, + onBlockQuoteClicked, + onToggleOrderedList, + onToggleNumberedList, + onIncreaseIndent, + onDecreaseIndent, + onHeadingClicked, + onImageClicked, + onCodeClicked, +}: MenuBarProps) { + return ( + + ) +} + +function CaptionedButton({ + caption, + onClick, + children, +}: { + caption: string + onClick: () => void + children: React.ReactNode +}) { + return ( +
+ +

{caption}

+
+ ) +} + +function turnSelectionIntoBlockquote( + state: EditorState, + dispatch: (tr: Transaction) => void | undefined, + view: EditorView, +): boolean { + // Check if the blockquote can be applied + const { $from, $to } = state.selection + const range = $from.blockRange($to) + + if (!range) { + return false + } + + // Check if we can wrap the selection in a blockquote + if (!wrapIn(state.schema.nodes.blockquote)(state, undefined, view)) { + return false + } + + // Apply the blockquote transformation + if (dispatch) { + wrapIn(state.schema.nodes.blockquote)(state, dispatch, view) + } + return true +} diff --git a/src/tools/richessay/components/useHandleReady.ts b/src/tools/richessay/components/useHandleReady.ts new file mode 100644 index 00000000..5dff7ac2 --- /dev/null +++ b/src/tools/richessay/components/useHandleReady.ts @@ -0,0 +1,14 @@ +import { type DocHandle } from "@automerge/automerge-repo" +import {useEffect, useState} from "react" + +export function useHandleReady(handle: DocHandle) { + const [isReady, setIsReady] = useState(handle.isReady()) + useEffect(() => { + if (!isReady) { + handle.whenReady().then(() => { + setIsReady(true) + }) + } + }, [handle]) + return isReady +} diff --git a/src/tools/richessay/index.css b/src/tools/richessay/index.css new file mode 100644 index 00000000..eb5fa741 --- /dev/null +++ b/src/tools/richessay/index.css @@ -0,0 +1,161 @@ +h1 { + font-size: 2rem; + font-weight: 300; + line-height: 2.5rem; + margin-bottom: 1rem; +} + +h2 { + font-size: 1.5rem; + line-height: 2rem; + margin-bottom: 1rem; + margin-top: 2rem; +} + +h3 { + font-size: 1.25rem; + line-height: 1.75rem; + margin: 2rem 0 1rem 0; +} + +h4 { + font-size: 1.1rem; + margin: 2rem 0 1rem 0; +} + +h1+h2, h2+h3 { + margin: -.5rem 0 1rem 0; +} + +ul, ol { + margin-bottom: 1.5rem; + margin-left: 1.3em; +} + +ul { + list-style-type: square; +} +ol { + list-style-type: decimal; +} + +li { + margin-bottom: .5rem; + white-space: normal; +} + +li.no-bullet { + list-style-type: none; + margin-left: -1.3em; +} +li h3, li h4 { + display: inline; + font-size: inherit; +} +li ul { + margin-top: .5rem; +} + +aside { + --aside-offset: 1rem - 0.21rem; /* The 0.21rem is a tweak to make the baseline of the aside match the baseline of the adjacent paragraph */ + --aside-offset-lineheight: 1.5rem; + box-sizing: border-box; + margin-bottom: calc(var(--padding) * 2); + padding: var(--padding); + position: absolute; + right: 0; + width: var(--aside-width); +} + +aside, figcaption, figure img, figure video, article p>img { + border-radius: calc(var(--padding)/4); + max-width: 100%; +} + +aside, aside>*, aside li, #byline, #byline li, figcaption, figcaption>*, blockquote footer, blockquote cite { + font-size: 0.8125rem; + font-weight: 400; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + line-height: 1.25rem; + margin-bottom: 0.5rem; + text-align: left; +} + +aside h4, aside h3, figcaption h4, figcaption h3 { + font-weight: 700; + letter-spacing: 0.15ch; + margin-bottom: 0.25rem; + margin-top: 0; + text-transform: uppercase; +} + +aside code, figcaption code, table code { + line-height: inherit; + padding: .125rem; + white-space: nowrap; +} + +aside p:last-child, aside li:last-child, figcaption p:last-child { + margin-bottom: 0; +} + +.teeMenu { + display: flex; + flex-direction: row; + border: 1px solid #f2f2f2; + padding: 1rem; +} + +.teeMenu button { + padding: 0.25rem; + margin: 0 0.25rem; + width: 2rem; + border: 1px solid #f2f2f2; + background-color: white; +} + +.menubar { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 10px; +} + +.menubar button { + padding: 10px; + border: none; + background-color: #f0f0f0; + cursor: pointer; +} + +.menubar .row { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; + gap: 10px; +} + +.captionedButton { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: stretch; + gap: 10px; + font-size: 10px; +} + +.captionedButton p { + padding: 0; + margin: 0; +} + +blockquote { + background: #f9f9f9; + border-left: 10px solid #ccc; + margin: 1em 10px; + padding: 0.5em 10px; + quotes: "\201C" "\201D" "\2018" "\2019"; +} diff --git a/src/tools/richessay/index.ts b/src/tools/richessay/index.ts new file mode 100644 index 00000000..c051538b --- /dev/null +++ b/src/tools/richessay/index.ts @@ -0,0 +1,8 @@ +import { Tool } from "@/os/tools"; +import { RichTeeEditor } from "./components/RichTeeEditor"; + +export default { + id: "rich-text", + name: "Rich Text Essay", + editorComponent: RichTeeEditor, +} as Tool; diff --git a/src/tools/richessay/schema.ts b/src/tools/richessay/schema.ts new file mode 100644 index 00000000..7887b19b --- /dev/null +++ b/src/tools/richessay/schema.ts @@ -0,0 +1,3 @@ +export type TeeDoc = { + content: string +} diff --git a/vite.config.ts b/vite.config.ts index fcb35fc6..db517784 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,7 +13,9 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, - + worker: { + plugins: () => [topLevelAwait(), wasm()], + }, optimizeDeps: { // This is necessary because otherwise `vite dev` includes two separate // versions of the JS wrapper. This causes problems because the JS @@ -22,6 +24,9 @@ export default defineConfig({ exclude: [ "@automerge/automerge-wasm", "@automerge/automerge-wasm/bundler/bindgen_bg.wasm", + "@automerge/prosemirror", + "prosemirror-model", + "prosemirror-schema-basic", "@syntect/wasm", ], }, diff --git a/yarn.lock b/yarn.lock index 0ad23e0b..0c9d63b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,9 +88,17 @@ uuid "^9.0.0" xstate "^5.9.1" -"@automerge/automerge-wasm@0.17.0", "@automerge/automerge-wasm@file:./src/vendor/automerge-wasm": +"@automerge/automerge-wasm@0.16.0", "@automerge/automerge-wasm@0.17.0", "@automerge/automerge-wasm@file:./src/vendor/automerge-wasm": version "0.17.0" +"@automerge/automerge@2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@automerge/automerge/-/automerge-2.2.1.tgz#adbfa30a1dcb9ded3cadabd401a378f938b9441b" + integrity sha512-LJwvVapEgAox0sEp0iDihE4wqsDAUyKt41Y5m3z1hHdF5DHa7IhqiHhGNCc59H8t3Zl3fnbTgtKabcMWFffvCQ== + dependencies: + "@automerge/automerge-wasm" "0.16.0" + uuid "^9.0.0" + "@automerge/automerge@^2.1.13", "@automerge/automerge@^2.1.9", "@automerge/automerge@^2.2.2": version "2.2.2" resolved "https://registry.yarnpkg.com/@automerge/automerge/-/automerge-2.2.2.tgz#e067accd47af17eee8af0d29634b57e0082f2f52" @@ -99,6 +107,20 @@ "@automerge/automerge-wasm" "0.17.0" uuid "^9.0.0" +"@automerge/prosemirror@0.0.13-alpha.1": + version "0.0.13-alpha.1" + resolved "https://registry.yarnpkg.com/@automerge/prosemirror/-/prosemirror-0.0.13-alpha.1.tgz#2a0ca359029b0eb5c6f8db6ae42fa3a207ee36d6" + integrity sha512-8csBHorOueabtmPQxkH0cPk5rFHA8Orr7pwTmbuV7/BvNGChJoV2HLKLueKBM/2nwai0UrrGxM0yMa0jCYy5kQ== + dependencies: + "@automerge/automerge" "2.2.1" + ordered-map "^0.1.0" + prosemirror-model "^1.19.4" + prosemirror-schema-basic "^1.2.2" + prosemirror-schema-list "^1.3.0" + prosemirror-state "^1.4.3" + prosemirror-transform "^1.7.3" + prosemirror-view "^1.33.1" + "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.2": version "7.24.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" @@ -4619,6 +4641,16 @@ optionator@^0.9.3: type-check "^0.4.0" word-wrap "^1.2.5" +ordered-map@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ordered-map/-/ordered-map-0.1.0.tgz#7425e5eaaa415f3dc1c1b8b48434a9640fc7a520" + integrity sha512-ttpssyEqKXIeTapKeFTFG+oUCnrC+Fmyy1DPjys0vPJtccBE3baKDFTbJPDuKd9/XHbnKKe+KPQKTzBLri6Mtw== + +orderedmap@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2" + integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g== + p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -4838,6 +4870,71 @@ prop-types@^15.6.1, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.13.1" +prosemirror-commands@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz#e94aeea52286f658cd984270de9b4c3fff580852" + integrity sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.0.0" + +prosemirror-keymap@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e" + integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ== + dependencies: + prosemirror-state "^1.0.0" + w3c-keyname "^2.2.0" + +prosemirror-model@^1.0.0, prosemirror-model@^1.19.0, prosemirror-model@^1.19.4, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.21.0.tgz#2d69ed04b4e7c441c3eb87c1c964fab4f9b217df" + integrity sha512-zLpS1mVCZLA7VTp82P+BfMiYVPcX1/z0Mf3gsjKZtzMWubwn2pN7CceMV0DycjlgE5JeXPR7UF4hJPbBV98oWA== + dependencies: + orderedmap "^2.0.0" + +prosemirror-schema-basic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7" + integrity sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw== + dependencies: + prosemirror-model "^1.19.0" + +prosemirror-schema-list@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.3.0.tgz#05374702cf35a3ba5e7ec31079e355a488d52519" + integrity sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.7.3" + +prosemirror-state@^1.0.0, prosemirror-state@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080" + integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-transform "^1.0.0" + prosemirror-view "^1.27.0" + +prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.7.3: + version "1.9.0" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.9.0.tgz#81fd1fbd887929a95369e6dd3d240c23c19313f8" + integrity sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg== + dependencies: + prosemirror-model "^1.21.0" + +prosemirror-view@^1.27.0, prosemirror-view@^1.33.1: + version "1.33.6" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.33.6.tgz#85804eb922411af8e300a07f4f376722b15900b9" + integrity sha512-zRLUNgLIQfd8IfGprsXxWTjdA8xEAFJe8cDNrOptj6Mop9sj+BMeVbJvceyAYCm5G2dOdT2prctH7K9dfnpIMw== + dependencies: + prosemirror-model "^1.20.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.1.0" + psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -4848,7 +4945,7 @@ punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -query-string@^8.2.0: +query-string@^8.1.0: version "8.2.0" resolved "https://registry.yarnpkg.com/query-string/-/query-string-8.2.0.tgz#f0b0ef6caa85f525dbdb745a67d3f8c08d71cc6b" integrity sha512-tUZIw8J0CawM5wyGBiDOAp7ObdRQh4uBor/fUR9ZjmbZVvw95OD9If4w3MQxr99rg0DJZ/9CIORcpEqU5hQG7g== @@ -5295,8 +5392,16 @@ std-env@^3.3.3: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: - name string-width-cjs +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5321,7 +5426,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5733,7 +5845,7 @@ vite-plugin-wasm@^3.3.0: resolved "https://registry.yarnpkg.com/vite-plugin-wasm/-/vite-plugin-wasm-3.3.0.tgz#2908ef2529bf8f33f4e549c8c6fda26ad273ca15" integrity sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg== -"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^5.0.12: +"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^5.1.6: version "5.2.11" resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.11.tgz#726ec05555431735853417c3c0bfb36003ca0cbd" integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ== @@ -5774,7 +5886,7 @@ vitest@^0.34.6: vite-node "0.34.6" why-is-node-running "^2.2.2" -w3c-keyname@^2.2.4: +w3c-keyname@^2.2.0, w3c-keyname@^2.2.4: version "2.2.8" resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==