editor-ctrl-fixes

This commit is contained in:
tonyrewin 2022-10-08 08:24:09 +03:00
parent adc0fe6393
commit a0a33087f6
7 changed files with 153 additions and 386 deletions

View File

@ -29,24 +29,9 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.186.0", "@aws-sdk/client-s3": "^3.186.0",
"@nanostores/persistent": "^0.7.0", "mailgun.js": "^8.0.1"
"@nanostores/router": "^0.7.0",
"@nanostores/solid": "^0.3.0",
"@solid-primitives/memo": "^1.0.2",
"loglevel": "^1.8.0",
"loglevel-plugin-prefix": "^0.8.4",
"mailgun.js": "^8.0.1",
"markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0",
"markdown-it-implicit-figures": "^0.10.0",
"markdown-it-mark": "^3.0.1",
"markdown-it-replace-link": "^1.1.0",
"nanostores": "^0.7.0",
"postcss-modules": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/language-server": "^0.27.0",
"@astrojs/markdown-remark": "^1.1.3",
"@astrojs/solid-js": "^1.1.0", "@astrojs/solid-js": "^1.1.0",
"@astrojs/vercel": "^2.1.0", "@astrojs/vercel": "^2.1.0",
"@babel/core": "^7.18.13", "@babel/core": "^7.18.13",
@ -57,9 +42,13 @@
"@graphql-codegen/urql-introspection": "^2.2.1", "@graphql-codegen/urql-introspection": "^2.2.1",
"@graphql-tools/url-loader": "^7.16.4", "@graphql-tools/url-loader": "^7.16.4",
"@graphql-typed-document-node/core": "^3.1.1", "@graphql-typed-document-node/core": "^3.1.1",
"@nanostores/persistent": "^0.7.0",
"@nanostores/router": "^0.7.0",
"@nanostores/solid": "^0.3.0",
"@popperjs/core": "^2.11.6", "@popperjs/core": "^2.11.6",
"@solid-devtools/debugger": "^0.11.1", "@solid-devtools/debugger": "^0.11.1",
"@solid-devtools/logger": "^0.4.9", "@solid-devtools/logger": "^0.4.9",
"@solid-primitives/memo": "^1.0.2",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/node": "^18.7.19", "@types/node": "^18.7.19",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
@ -69,7 +58,7 @@
"@urql/devtools": "^2.0.3", "@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^1.0.0", "@urql/exchange-auth": "^1.0.0",
"@urql/exchange-graphcache": "^5.0.0", "@urql/exchange-graphcache": "^5.0.0",
"astro": "^1.1.1", "astro": "^1.4.6",
"astro-eslint-parser": "^0.6.1", "astro-eslint-parser": "^0.6.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
@ -94,7 +83,16 @@
"idb": "^7.0.1", "idb": "^7.0.1",
"jest": "^29.0.1", "jest": "^29.0.1",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"loglevel": "^1.8.0",
"loglevel-plugin-prefix": "^0.8.4",
"markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0",
"markdown-it-implicit-figures": "^0.10.0",
"markdown-it-mark": "^3.0.1",
"markdown-it-replace-link": "^1.1.0",
"nanostores": "^0.7.0",
"postcss": "^8.4.16", "postcss": "^8.4.16",
"postcss-modules": "^5.0.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prettier-eslint": "^15.0.1", "prettier-eslint": "^15.0.1",
"prosemirror-commands": "^1.3.1", "prosemirror-commands": "^1.3.1",
@ -111,10 +109,6 @@
"prosemirror-schema-list": "^1.2.2", "prosemirror-schema-list": "^1.2.2",
"prosemirror-state": "^1.4.1", "prosemirror-state": "^1.4.1",
"prosemirror-view": "^1.28.1", "prosemirror-view": "^1.28.1",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
"rehype-toc": "^3.0.2",
"remark-code-titles": "^0.1.2",
"rollup": "~2.79.1", "rollup": "~2.79.1",
"rollup-plugin-visualizer": "^5.8.2", "rollup-plugin-visualizer": "^5.8.2",
"sass": "^1.55.0", "sass": "^1.55.0",

View File

@ -1,9 +1,11 @@
import { For, Show, createEffect, createSignal, onCleanup } from 'solid-js' import { Show, createEffect, createSignal, onCleanup } from 'solid-js'
import { unwrap } from 'solid-js/store' import { unwrap } from 'solid-js/store'
// import { undo, redo } from 'prosemirror-history' // import { undo, redo } from 'prosemirror-history'
import { File, useState /*, Config, PrettierConfig */ } from './prosemirror/context' import { Draft, useState } from './prosemirror/context'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import type { Styled } from './Layout' import type { Styled } from './Layout'
import { t } from '../../utils/intl'
// import type { EditorState } from 'prosemirror-state' // import type { EditorState } from 'prosemirror-state'
// import { serialize } from './prosemirror/markdown' // import { serialize } from './prosemirror/markdown'
// import { baseUrl } from '../../graphql/client' // import { baseUrl } from '../../graphql/client'
@ -11,12 +13,10 @@ import type { Styled } from './Layout'
// const copy = async (text: string): Promise<void> => navigator.clipboard.writeText(text) // const copy = async (text: string): Promise<void> => navigator.clipboard.writeText(text)
// const copyAllAsMarkdown = async (state: EditorState): Promise<void> => // const copyAllAsMarkdown = async (state: EditorState): Promise<void> =>
// !isServer && navigator.clipboard.writeText(serialize(state)) // navigator.clipboard.writeText(serialize(state)) && !isServer
const Off = (props: any) => <div class="sidebar-off">{props.children}</div> const Off = (props: any) => <div class="sidebar-off">{props.children}</div>
const Label = (props: Styled) => <h3 class="sidebar-label">{props.children}</h3> const Label = (props: Styled) => <h3 class="sidebar-label">{props.children}</h3>
const Link = ( const Link = (
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string } props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
) => ( ) => (
@ -32,12 +32,12 @@ const Link = (
</button> </button>
) )
type FileLinkProps = { type DraftLinkProps = {
file: File draft: Draft
onOpenFile: (file: File) => void onOpenDraft: (draft: Draft) => void
} }
const FileLink = (props: FileLinkProps) => { const DraftLink = (props: DraftLinkProps) => {
const length = 100 const length = 100
let content = '' let content = ''
const getContent = (node: any) => { const getContent = (node: any) => {
@ -65,14 +65,14 @@ const FileLink = (props: FileLinkProps) => {
} }
const text = () => const text = () =>
props.file.path props.draft.path
? props.file.path.slice(Math.max(0, props.file.path.length - length)) ? props.draft.path.slice(Math.max(0, props.draft.path.length - length))
: getContent(props.file.text?.doc) : getContent(props.draft.text?.doc)
return ( return (
// eslint-disable-next-line solid/no-react-specific-props // eslint-disable-next-line solid/no-react-specific-props
<Link className="file" onClick={() => props.onOpenFile(props.file)} data-testid="open"> <Link className="draft" onClick={() => props.onOpenDraft(props.draft)} data-testid="open">
{text()} {props.file.path && '📎'} {text()} {props.draft.path && '📎'}
</Link> </Link>
) )
} }
@ -88,14 +88,13 @@ export const Sidebar = () => {
// const collabText = () => (store.collab?.started ? 'Stop' : store.collab?.error ? 'Restart 🚨' : 'Start') // const collabText = () => (store.collab?.started ? 'Stop' : store.collab?.error ? 'Restart 🚨' : 'Start')
const editorView = () => unwrap(store.editorView) const editorView = () => unwrap(store.editorView)
// const onToggleMarkdown = () => ctrl.toggleMarkdown() // const onToggleMarkdown = () => ctrl.toggleMarkdown()
const onOpenFile = (file: File) => ctrl.openFile(unwrap(file)) const onOpenDraft = (draft: Draft) => ctrl.openDraft(unwrap(draft))
// const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0 // const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0
// const onUndo = () => undo(editorView().state, editorView().dispatch) // const onUndo = () => undo(editorView().state, editorView().dispatch)
// const onRedo = () => redo(editorView().state, editorView().dispatch) // const onRedo = () => redo(editorView().state, editorView().dispatch)
// const onCopyAllAsMd = () => copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md')) // const onCopyAllAsMd = () => copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
// const onToggleAlwaysOnTop = () => ctrl.updateConfig({ alwaysOnTop: !store.config.alwaysOnTop }) // const onToggleAlwaysOnTop = () => ctrl.updateConfig({ alwaysOnTop: !store.config.alwaysOnTop })
// const onToggleFullscreen = () => ctrl.setFullscreen(!store.fullscreen) // const onNew = () => ctrl.newDraft()
// const onNew = () => ctrl.newFile()
// const onDiscard = () => ctrl.discard() // const onDiscard = () => ctrl.discard()
const [isHidden, setIsHidden] = createSignal<boolean | false>() const [isHidden, setIsHidden] = createSignal<boolean | false>()
@ -106,7 +105,7 @@ export const Sidebar = () => {
toggleSidebar() toggleSidebar()
// const onSaveAs = async () => { // const onSaveAs = async () => {
// const path = 'test' // TODO: save filename await remote.save(editorView().state) // const path = 'test' // TODO: save draftname await remote.save(editorView().state)
// //
// if (path) ctrl.updatePath(path) // if (path) ctrl.updatePath(path)
// } // }
@ -189,27 +188,17 @@ export const Sidebar = () => {
</div> </div>
{/* {/*
<Show when={isTauri && !store.path}>
<Link onClick={onSaveAs}>
Save to file <Keys keys={[mod, 's']} />
</Link>
</Show>
<Link onClick={onNew} data-testid='new'> <Link onClick={onNew} data-testid='new'>
New <Keys keys={[mod, 'n']} /> New <Keys keys={[mod, 'n']} />
</Link> </Link>
<Link <Link
onClick={onDiscard} onClick={onDiscard}
disabled={!store.path && store.files?.length === 0 && isEmpty(store.text)} disabled={!store.path && store.drafts?.length === 0 && isEmpty(store.text)}
data-testid='discard' data-testid='discard'
> >
{store.path ? 'Close' : store.files?.length > 0 && isEmpty(store.text) ? 'Delete ⚠️' : 'Clear'}{' '} {store.path ? 'Close' : store.drafts?.length > 0 && isEmpty(store.text) ? 'Delete ⚠️' : 'Clear'}{' '}
<Keys keys={[mod, 'w']} /> <Keys keys={[mod, 'w']} />
</Link> </Link>
<Show when={isTauri}>
<Link onClick={onToggleFullscreen}>
Fullscreen {store.fullscreen && '✅'} <Keys keys={[alt, 'Enter']} />
</Link>
</Show>
<Link onClick={onUndo}> <Link onClick={onUndo}>
Undo <Keys keys={[mod, 'z']} /> Undo <Keys keys={[mod, 'z']} />
</Link> </Link>
@ -226,15 +215,15 @@ export const Sidebar = () => {
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} /> Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
</Link> </Link>
<Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link> <Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link>
*/}
<Show when={store.files?.length > 0}> <Show when={store.drafts?.length > 0}>
<h4>Files:</h4> <h4>t('Drafts'):</h4>
<p> <p>
<For each={store.files}>{(file) => <FileLink file={file} onOpenFile={onOpenFile} />}</For> <For each={store.drafts}>{(draft) => <DraftLink draft={draft} onOpenDraft={onOpenDraft} />}</For>
</p> </p>
</Show> </Show>
{/*
<Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}> <Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}>
Collab {collabText()} Collab {collabText()}
</Link> </Link>

View File

@ -55,10 +55,11 @@ export interface Collab {
export type LoadingType = 'loading' | 'initialized' export type LoadingType = 'loading' | 'initialized'
export interface File { // TODO: use this interface in prosemirror's context
export interface Draft {
path?: string // used by state
text?: { [key: string]: string } text?: { [key: string]: string }
lastModified?: string lastModified?: string
path?: string
markdown?: boolean markdown?: boolean
} }
@ -68,11 +69,9 @@ export interface State {
extensions?: ProseMirrorExtension[] extensions?: ProseMirrorExtension[]
markdown?: boolean markdown?: boolean
lastModified?: Date lastModified?: Date
files: File[]
config: Config config: Config
error?: ErrorObject error?: ErrorObject
loading: LoadingType loading: LoadingType
fullscreen: boolean
collab?: Collab collab?: Collab
path?: string path?: string
args?: Args args?: Args
@ -103,15 +102,14 @@ const DEFAULT_CONFIG = {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const StateContext = createContext<[Store<State>, any]>([{} as Store<State>, undefined]) export const StateContext = createContext<[Store<State>, any]>([{} as Store<State>, undefined])
export const useState = () => useContext(StateContext) export const useState = () => useContext(StateContext)
export const newState = (props: Partial<State> = {}): State => ({ export const newState = (props: Partial<State> = {}): State => ({
extensions: [], extensions: [],
files: [],
loading: 'loading', loading: 'loading',
fullscreen: false,
markdown: false, markdown: false,
config: DEFAULT_CONFIG, config: DEFAULT_CONFIG,
...props ...props

View File

@ -1,119 +1,116 @@
import { Store, createStore, unwrap } from 'solid-js/store' import { Store, createStore, unwrap } from 'solid-js/store'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import type { EditorState } from 'prosemirror-state' import type { Command, EditorState } from 'prosemirror-state'
import { undo, redo } from 'prosemirror-history' import { undo, redo } from 'prosemirror-history'
import { selectAll, deleteSelection } from 'prosemirror-commands' import { selectAll, deleteSelection } from 'prosemirror-commands'
import { undo as yUndo, redo as yRedo } from 'y-prosemirror' import { undo as yUndo, redo as yRedo } from 'y-prosemirror'
import { debounce } from 'lodash' import debounce from 'lodash/debounce'
import { createSchema, createExtensions, createEmptyText, InitOpts } from '../prosemirror/setup' import { createSchema, createExtensions, createEmptyText, InitOpts } from '../prosemirror/setup'
import { State, File, Config, ServiceError, newState, PeerData } from '../prosemirror/context' import { State, Config, ServiceError, newState, PeerData } from '../prosemirror/context'
import { serialize, createMarkdownParser } from '../prosemirror/markdown' import { serialize, createMarkdownParser } from '../prosemirror/markdown'
import { isEmpty, isInitialized, ProseMirrorExtension } from '../prosemirror/state' import { isEmpty, isInitialized, ProseMirrorExtension } from '../prosemirror/state'
import { isServer } from 'solid-js/web' import { isServer } from 'solid-js/web'
import { roomConnect } from '../prosemirror/p2p' import { roomConnect } from '../prosemirror/p2p'
const mod = 'Ctrl' const mod = 'Ctrl'
const isText = (x): boolean => x && x.doc && x.selection
const isState = (x): boolean => typeof x.lastModified !== 'string' && Array.isArray(x.files)
const isFile = (x): boolean => x && (x.text || x.path)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const createCtrl = (initial: State): [Store<State>, { [key: string]: any }] => { export const createCtrl = (initial: State): [Store<State>, { [key: string]: any }] => {
const [store, setState] = createStore(initial) const [store, setState] = createStore(initial)
const discardText = async () => { const discardText = async () => {
const state = unwrap(store) const state = unwrap(store)
const index = state.files.length - 1
const file = index !== -1 ? state.files[index] : undefined
let next: Partial<State>
if (file) {
next = await createTextFromFile(file)
} else {
const extensions = createExtensions({ const extensions = createExtensions({
config: state.config ?? store.config, config: state.config ?? store.config,
markdown: state.markdown && store.markdown, markdown: state.markdown && store.markdown,
keymap keymap
}) })
next = { setState({
text: createEmptyText(), text: createEmptyText(),
extensions, extensions,
lastModified: undefined, lastModified: undefined,
path: undefined, path: undefined,
markdown: state.markdown markdown: state.markdown,
} collab: state.collab,
}
const files = state.files.filter((f: File) => f !== file)
setState({
files,
...next,
collab: file ? undefined : state.collab,
error: undefined error: undefined
}) })
} }
const addToFiles = (files: File[], prev: State) => {
const text = prev.path ? undefined : (prev.text as EditorState).toJSON()
return [
...files,
{
text,
lastModified: prev.lastModified?.toISOString(),
path: prev.path,
markdown: prev.markdown
}
]
}
const discard = async () => { const discard = async () => {
if (store.path) { if (store.path) {
await discardText() await discardText()
} else if (store.files?.length > 0 && isEmpty(store.text)) {
await discardText()
} else { } else {
selectAll(store.editorView.state, store.editorView.dispatch) selectAll(store.editorView.state, store.editorView.dispatch)
deleteSelection(store.editorView.state, store.editorView.dispatch) deleteSelection(store.editorView.state, store.editorView.dispatch)
} }
return true
} }
const onDiscard = () => { const onDiscard = () => {
discard() discard()
return true return true
} }
const onToggleMarkdown = () => toggleMarkdown()
const onUndo = () => { const onUndo = () => {
if (!isInitialized(store.text)) return if (!isInitialized(store.text)) return false
const text = store.text as EditorState const text = store.text as EditorState
if (store.collab?.started) yUndo(text)
if (store.collab?.started) { else undo(text, store.editorView.dispatch)
yUndo(text)
} else {
undo(text, store.editorView.dispatch)
}
return true return true
} }
const onRedo = () => { const onRedo = () => {
if (!isInitialized(store.text)) return if (!isInitialized(store.text)) return false
const text = store.text as EditorState const text = store.text as EditorState
if (store.collab?.started) yRedo(text)
if (store.collab?.started) { else redo(text, store.editorView.dispatch)
yRedo(text) return true
} else {
redo(text, store.editorView.dispatch)
} }
return true const toggleMarkdown = () => {
const state = unwrap(store)
const editorState = store.text as EditorState
const markdown = !state.markdown
const selection = { type: 'text', anchor: 1, head: 1 }
let doc
if (markdown) {
const lines = serialize(editorState).split('\n')
const nodes = lines.map((text) => {
return text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' }
})
doc = { type: 'doc', content: nodes }
} else {
const schema = createSchema({
config: state.config,
path: state.path,
y: state.collab?.y,
markdown,
keymap
})
const parser = createMarkdownParser(schema)
let textContent = ''
editorState.doc.forEach((node) => {
textContent += `${node.textContent}\n`
})
const text = parser.parse(textContent)
doc = text?.toJSON()
}
const extensions = createExtensions({
config: state.config,
markdown,
path: state.path,
keymap,
y: state.collab?.y
})
setState({
text: { selection, doc },
extensions,
markdown
})
} }
const keymap = { const keymap = {
@ -121,85 +118,52 @@ export const createCtrl = (initial: State): [Store<State>, { [key: string]: any
[`${mod}-z`]: onUndo, [`${mod}-z`]: onUndo,
[`Shift-${mod}-z`]: onRedo, [`Shift-${mod}-z`]: onRedo,
[`${mod}-y`]: onRedo, [`${mod}-y`]: onRedo,
[`${mod}-m`]: onToggleMarkdown [`${mod}-m`]: toggleMarkdown
} } as unknown as { [key: string]: Command }
const createTextFromFile = async (file: File) => {
const state = unwrap(store)
// if (file.path) file = await loadFile(state.config, file.path)
const extensions = createExtensions({
config: state.config,
markdown: file.markdown,
path: file.path,
keymap
})
return {
text: file.text,
extensions,
lastModified: file.lastModified ? new Date(file.lastModified) : undefined,
path: file.path,
markdown: file.markdown
}
}
const fetchData = async (): Promise<State> => { const fetchData = async (): Promise<State> => {
const state: State = unwrap(store)
const room = window.location.pathname?.slice(1).trim()
const args = { room: room || undefined }
if (isServer) return if (isServer) return
const state: State = unwrap(store)
const room = undefined // window.location.pathname?.slice(1) + uuidv4()
// console.debug('[editor-ctrl] got unique room', room)
const args = { room }
const { default: db } = await import('../db') const { default: db } = await import('../db')
const data: string = await db.get('state') const data: string = await db.get('state')
console.debug('[editor-ctrl] got stored state from idb')
let parsed let parsed
let text = state.text
if (data !== undefined) { if (data !== undefined) {
try { try {
parsed = JSON.parse(data) parsed = JSON.parse(data)
if (!parsed) return { ...state, args } if (!parsed) return { ...state, args }
} catch {
throw new ServiceError('invalid_state', data)
}
}
let text = state.text
if (parsed.text) {
if (!isText(parsed.text)) {
throw new ServiceError('invalid_state', parsed.text)
}
text = parsed.text
}
const extensions = createExtensions({ console.debug('[editor-ctrl] json state parsed successfully', parsed)
if (parsed?.text) {
if (!parsed.text || !parsed.text.doc || !parsed.text.selection) {
throw new ServiceError('invalid_state', parsed.text)
} else {
text = parsed.text
console.debug('[editor-ctrl] got text from stored json', parsed)
}
}
return {
...parsed,
text,
extensions: createExtensions({
path: parsed.path, path: parsed.path,
markdown: parsed.markdown, markdown: parsed.markdown,
keymap, keymap,
config: {} as Config config: {} as Config
}) }),
args,
const nState = { lastModified: parsed.lastModified ? new Date(parsed.lastModified) : new Date()
...parsed,
text,
extensions,
// config,
args
} }
} catch {
if (nState.lastModified) { throw new ServiceError('invalid_state', data)
nState.lastModified = new Date(nState.lastModified)
}
for (const file of parsed.files) {
if (!isFile(file)) {
throw new ServiceError('invalid_file', file)
} }
} }
if (!isState(nState)) {
throw new ServiceError('invalid_state', nState)
}
return nState
} }
const getTheme = (state: State) => ({ theme: state.config.theme }) const getTheme = (state: State) => ({ theme: state.config.theme })
@ -208,8 +172,6 @@ export const createCtrl = (initial: State): [Store<State>, { [key: string]: any
setState({ setState({
...newState(), ...newState(),
loading: 'initialized', loading: 'initialized',
files: [],
fullscreen: store.fullscreen,
lastModified: new Date(), lastModified: new Date(),
error: undefined, error: undefined,
text: undefined text: undefined
@ -217,67 +179,34 @@ export const createCtrl = (initial: State): [Store<State>, { [key: string]: any
} }
const init = async () => { const init = async () => {
let data = await fetchData() let state = await fetchData()
console.debug('[editor-ctrl] state initiated', state)
try { try {
if (data.args?.room) { if (state.args?.room) {
data = doStartCollab(data) state = doStartCollab(state)
} else if (data.args?.text) { } else if (!state.text) {
data = await doOpenFile(data, { text: JSON.parse(data.args?.text) })
} else if (!data.text) {
const text = createEmptyText() const text = createEmptyText()
const extensions = createExtensions({ const extensions = createExtensions({
config: data.config, config: state.config,
markdown: data.markdown, markdown: state.markdown,
keymap keymap
}) })
data = { ...data, text, extensions } state = { ...state, text, extensions }
} }
} catch (error) { } catch (error) {
data = { ...data, error } state = { ...state, error }
} }
setState({ setState({
...data, ...state,
config: { ...data.config, ...getTheme(data) }, config: { ...state.config, ...getTheme(state) },
loading: 'initialized' loading: 'initialized'
}) })
} }
const doOpenFile = async (state: State, file: File): Promise<State> => {
const findIndexOfFile = (f: File) => {
for (let i = 0; i < state.files.length; i++) {
if (state.files[i] === f) return i
if (f.path && state.files[i].path === f.path) return i
}
return -1
}
const index = findIndexOfFile(file)
const item = index === -1 ? file : state.files[index]
let files = state.files.filter((f) => f !== item)
if (!isEmpty(state.text) && state.lastModified) {
files = addToFiles(files, state)
}
file.lastModified = item.lastModified
const next = await createTextFromFile(file)
return {
...state,
...next,
files,
collab: undefined,
error: undefined
}
}
const saveState = debounce(async (state: State) => { const saveState = debounce(async (state: State) => {
const data = { const data = {
lastModified: state.lastModified, lastModified: state.lastModified,
files: state.files,
config: state.config, config: state.config,
path: state.path, path: state.path,
markdown: state.markdown, markdown: state.markdown,
@ -322,15 +251,8 @@ export const createCtrl = (initial: State): [Store<State>, { [key: string]: any
let nState = state let nState = state
if ((backup && !isEmpty(state.text)) || state.path) { if ((backup && !isEmpty(state.text)) || state.path) {
let files = state.files
if (!state.error) {
files = addToFiles(files, state)
}
nState = { nState = {
...state, ...state,
files,
lastModified: undefined, lastModified: undefined,
path: undefined, path: undefined,
error: undefined error: undefined
@ -357,54 +279,6 @@ export const createCtrl = (initial: State): [Store<State>, { [key: string]: any
window.history.replaceState(null, '', '/') window.history.replaceState(null, '', '/')
} }
const toggleMarkdown = () => {
const state = unwrap(store)
const editorState = store.text as EditorState
const markdown = !state.markdown
const selection = { type: 'text', anchor: 1, head: 1 }
let doc
if (markdown) {
const lines = serialize(editorState).split('\n')
const nodes = lines.map((text) => {
return text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' }
})
doc = { type: 'doc', content: nodes }
} else {
const schema = createSchema({
config: state.config,
path: state.path,
y: state.collab?.y,
markdown,
keymap
})
const parser = createMarkdownParser(schema)
let textContent = ''
editorState.doc.forEach((node) => {
textContent += `${node.textContent}\n`
})
const text = parser.parse(textContent)
doc = text?.toJSON()
}
const extensions = createExtensions({
config: state.config,
markdown,
path: state.path,
keymap,
y: state.collab?.y
})
setState({
text: { selection, doc },
extensions,
markdown
})
}
const updateConfig = (config: Partial<Config>) => { const updateConfig = (config: Partial<Config>) => {
const state = unwrap(store) const state = unwrap(store)
const extensions = createExtensions({ const extensions = createExtensions({

View File

@ -1,12 +1,12 @@
import { uniqueNamesGenerator, adjectives, animals } from 'unique-names-generator' import { uniqueNamesGenerator, adjectives, animals } from 'unique-names-generator'
import { Awareness } from 'y-protocols/awareness' import { Awareness } from 'y-protocols/awareness'
import { WebrtcProvider } from 'y-webrtc' import { WebrtcProvider } from 'y-webrtc'
import * as Y from 'yjs' import { Doc, XmlFragment } from 'yjs'
import type { Reaction } from '../../../graphql/types.gen' import type { Reaction } from '../../../graphql/types.gen'
import { setReactions } from '../../../stores/editor' import { setReactions } from '../../../stores/editor'
export const roomConnect = (room, username = '', keyname = 'collab'): [Y.XmlFragment, WebrtcProvider] => { export const roomConnect = (room, username = '', keyname = 'collab'): [XmlFragment, WebrtcProvider] => {
const ydoc = new Y.Doc() const ydoc = new Doc()
const yarr = ydoc.getArray(keyname + '-reactions') const yarr = ydoc.getArray(keyname + '-reactions')
const yXmlFragment = ydoc.getXmlFragment(keyname) const yXmlFragment = ydoc.getXmlFragment(keyname)
const webrtcOptions = { const webrtcOptions = {

View File

@ -17,10 +17,11 @@ import table from './extension/table'
import collab from './extension/collab' import collab from './extension/collab'
import type { Config, PeerData } from './context' import type { Config, PeerData } from './context'
import selectionMenu from './extension/selection' import selectionMenu from './extension/selection'
import type { Command } from 'prosemirror-state'
export interface InitOpts { export interface InitOpts {
data?: unknown data?: unknown
keymap?: any keymap?: { [key: string]: Command }
config: Config config: Config
markdown: boolean markdown: boolean
path?: string path?: string

View File

@ -73,24 +73,6 @@
vscode-languageserver-types "^3.17.1" vscode-languageserver-types "^3.17.1"
vscode-uri "^3.0.3" vscode-uri "^3.0.3"
"@astrojs/language-server@^0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@astrojs/language-server/-/language-server-0.27.0.tgz#5182965a1158e77bfcd9211edf0f8a5fef3c2505"
integrity sha512-4nT2KqAhxjjElATs/4Q8nkiUlu+YalJqZIEW4YOGEoSDbju/pw7fy8CJHFOhkPmGux8173N58i6l1cewGcxluw==
dependencies:
"@vscode/emmet-helper" "^2.8.4"
events "^3.3.0"
prettier "^2.7.1"
prettier-plugin-astro "^0.5.3"
source-map "^0.7.3"
vscode-css-languageservice "^6.0.1"
vscode-html-languageservice "^5.0.0"
vscode-languageserver "^8.0.1"
vscode-languageserver-protocol "^3.17.1"
vscode-languageserver-textdocument "^1.0.4"
vscode-languageserver-types "^3.17.1"
vscode-uri "^3.0.3"
"@astrojs/markdown-remark@^1.1.3": "@astrojs/markdown-remark@^1.1.3":
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/@astrojs/markdown-remark/-/markdown-remark-1.1.3.tgz#9fa985a532622043f0863c20f01c6ed01eca31e2" resolved "https://registry.yarnpkg.com/@astrojs/markdown-remark/-/markdown-remark-1.1.3.tgz#9fa985a532622043f0863c20f01c6ed01eca31e2"
@ -2307,11 +2289,6 @@
"@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/sourcemap-codec" "^1.4.10"
"@jsdevtools/rehype-toc@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@jsdevtools/rehype-toc/-/rehype-toc-3.0.2.tgz#29c32e6b40cd4b5dafd96cb90d5057ac5dab4a51"
integrity sha512-n5JEf16Wr4mdkRMZ8wMP/wN9/sHmTjRPbouXjJH371mZ2LEGDl72t8tEsMRNFerQN/QJtivOxqK1frdGa4QK5Q==
"@ljharb/has-package-exports-patterns@^0.0.2": "@ljharb/has-package-exports-patterns@^0.0.2":
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/@ljharb/has-package-exports-patterns/-/has-package-exports-patterns-0.0.2.tgz#c1718939b65efa1f45f53686c2fcfa992b9fb68f" resolved "https://registry.yarnpkg.com/@ljharb/has-package-exports-patterns/-/has-package-exports-patterns-0.0.2.tgz#c1718939b65efa1f45f53686c2fcfa992b9fb68f"
@ -5722,7 +5699,7 @@ git-hooks-list@^3.0.0:
resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.0.0.tgz#6d888988bb445b34e7c2e1eb97cb88358153221e" resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.0.0.tgz#6d888988bb445b34e7c2e1eb97cb88358153221e"
integrity sha512-XDfdemBGJIMAsHHOONHQxEH5dX2kCpE6MGZ1IsNvBuDPBZM3p4EAwAC7ygMjn/1/x+BJX0TK1ara1Zrh7JCFdQ== integrity sha512-XDfdemBGJIMAsHHOONHQxEH5dX2kCpE6MGZ1IsNvBuDPBZM3p4EAwAC7ygMjn/1/x+BJX0TK1ara1Zrh7JCFdQ==
github-slugger@^1.1.1, github-slugger@^1.4.0: github-slugger@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e" resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e"
integrity sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ== integrity sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==
@ -6001,13 +5978,6 @@ hast-util-has-property@^2.0.0:
resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz#c15cd6180f3e535540739fcc9787bcffb5708cae" resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz#c15cd6180f3e535540739fcc9787bcffb5708cae"
integrity sha512-4Qf++8o5v14us4Muv3HRj+Er6wTNGA/N9uCaZMty4JWvyFKLdhULrv4KE1b65AthsSO9TXSZnjuxS8ecIyhb0w== integrity sha512-4Qf++8o5v14us4Muv3HRj+Er6wTNGA/N9uCaZMty4JWvyFKLdhULrv4KE1b65AthsSO9TXSZnjuxS8ecIyhb0w==
hast-util-heading-rank@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/hast-util-heading-rank/-/hast-util-heading-rank-2.1.0.tgz#c39f34fa8330ebfec03a08b5d5019ed56122029c"
integrity sha512-w+Rw20Q/iWp2Bcnr6uTrYU6/ftZLbHKhvc8nM26VIWpDqDMlku2iXUVTeOlsdoih/UKQhY7PHQ+vZ0Aqq8bxtQ==
dependencies:
"@types/hast" "^2.0.0"
hast-util-is-element@^2.0.0: hast-util-is-element@^2.0.0:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz#fc0b0dc7cef3895e839b8d66979d57b0338c68f3" resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz#fc0b0dc7cef3895e839b8d66979d57b0338c68f3"
@ -9312,19 +9282,6 @@ regexpp@^3.0.0, regexpp@^3.2.0:
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
rehype-autolink-headings@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz#0cb874a56f3de6ead1c2268d7f0fc5006f244db5"
integrity sha512-NMYzZIsHM3sA14nC5rAFuUPIOfg+DFmf9EY1YMhaNlB7+3kK/ZlE6kqPfuxr1tsJ1XWkTrMtMoyHosU70d35mA==
dependencies:
"@types/hast" "^2.0.0"
extend "^3.0.0"
hast-util-has-property "^2.0.0"
hast-util-heading-rank "^2.0.0"
hast-util-is-element "^2.0.0"
unified "^10.0.0"
unist-util-visit "^4.0.0"
rehype-parse@^8.0.0: rehype-parse@^8.0.0:
version "8.0.4" version "8.0.4"
resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-8.0.4.tgz#3d17c9ff16ddfef6bbcc8e6a25a99467b482d688" resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-8.0.4.tgz#3d17c9ff16ddfef6bbcc8e6a25a99467b482d688"
@ -9344,19 +9301,6 @@ rehype-raw@^6.1.1:
hast-util-raw "^7.2.0" hast-util-raw "^7.2.0"
unified "^10.0.0" unified "^10.0.0"
rehype-slug@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/rehype-slug/-/rehype-slug-5.0.1.tgz#6e732d0c55b3b1e34187e74b7363fb53229e5f52"
integrity sha512-X5v3wV/meuOX9NFcGhJvUpEjIvQl2gDvjg3z40RVprYFt7q3th4qMmYLULiu3gXvbNX1ppx+oaa6JyY1W67pTA==
dependencies:
"@types/hast" "^2.0.0"
github-slugger "^1.1.1"
hast-util-has-property "^2.0.0"
hast-util-heading-rank "^2.0.0"
hast-util-to-string "^2.0.0"
unified "^10.0.0"
unist-util-visit "^4.0.0"
rehype-stringify@^9.0.0, rehype-stringify@^9.0.3: rehype-stringify@^9.0.0, rehype-stringify@^9.0.3:
version "9.0.3" version "9.0.3"
resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-9.0.3.tgz#70e3bd6d4d29e7acf36b802deed350305d2c3c17" resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-9.0.3.tgz#70e3bd6d4d29e7acf36b802deed350305d2c3c17"
@ -9366,13 +9310,6 @@ rehype-stringify@^9.0.0, rehype-stringify@^9.0.3:
hast-util-to-html "^8.0.0" hast-util-to-html "^8.0.0"
unified "^10.0.0" unified "^10.0.0"
rehype-toc@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rehype-toc/-/rehype-toc-3.0.2.tgz#0373e2abafddeb0606ee38229ff6714da6d86d68"
integrity sha512-DMt376+4i1KJGgHJL7Ezd65qKkJ7Eqp6JSB47BJ90ReBrohI9ufrornArM6f4oJjP2E2DVZZHufWucv/9t7GUQ==
dependencies:
"@jsdevtools/rehype-toc" "3.0.2"
rehype@^12.0.1: rehype@^12.0.1:
version "12.0.1" version "12.0.1"
resolved "https://registry.yarnpkg.com/rehype/-/rehype-12.0.1.tgz#68a317662576dcaa2565a3952e149d6900096bf6" resolved "https://registry.yarnpkg.com/rehype/-/rehype-12.0.1.tgz#68a317662576dcaa2565a3952e149d6900096bf6"
@ -9392,13 +9329,6 @@ relay-runtime@12.0.0:
fbjs "^3.0.0" fbjs "^3.0.0"
invariant "^2.2.4" invariant "^2.2.4"
remark-code-titles@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/remark-code-titles/-/remark-code-titles-0.1.2.tgz#ae41b47c517eae4084c761a59a60df5f0bd54aa8"
integrity sha512-KsHQbaI4FX8Ozxqk7YErxwmBiveUqloKuVqyPG2YPLHojpgomodWgRfG4B+bOtmn/5bfJ8khw4rR0lvgVFl2Uw==
dependencies:
unist-util-visit "^1.4.0"
remark-gfm@^3.0.1: remark-gfm@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f"
@ -10739,11 +10669,6 @@ unist-util-generated@^2.0.0:
resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113" resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113"
integrity sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw== integrity sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==
unist-util-is@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd"
integrity sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==
unist-util-is@^5.0.0: unist-util-is@^5.0.0:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.1.1.tgz#e8aece0b102fa9bc097b0fef8f870c496d4a6236" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.1.1.tgz#e8aece0b102fa9bc097b0fef8f870c496d4a6236"
@ -10797,13 +10722,6 @@ unist-util-visit-children@^1.0.0:
resolved "https://registry.yarnpkg.com/unist-util-visit-children/-/unist-util-visit-children-1.1.4.tgz#e8a087e58a33a2815f76ea1901c15dec2cb4b432" resolved "https://registry.yarnpkg.com/unist-util-visit-children/-/unist-util-visit-children-1.1.4.tgz#e8a087e58a33a2815f76ea1901c15dec2cb4b432"
integrity sha512-sA/nXwYRCQVRwZU2/tQWUqJ9JSFM1X3x7JIOsIgSzrFHcfVt6NkzDtKzyxg2cZWkCwGF9CO8x4QNZRJRMK8FeQ== integrity sha512-sA/nXwYRCQVRwZU2/tQWUqJ9JSFM1X3x7JIOsIgSzrFHcfVt6NkzDtKzyxg2cZWkCwGF9CO8x4QNZRJRMK8FeQ==
unist-util-visit-parents@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz#25e43e55312166f3348cae6743588781d112c1e9"
integrity sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==
dependencies:
unist-util-is "^3.0.0"
unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.1.tgz#868f353e6fce6bf8fa875b251b0f4fec3be709bb" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.1.tgz#868f353e6fce6bf8fa875b251b0f4fec3be709bb"
@ -10812,13 +10730,6 @@ unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1:
"@types/unist" "^2.0.0" "@types/unist" "^2.0.0"
unist-util-is "^5.0.0" unist-util-is "^5.0.0"
unist-util-visit@^1.4.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3"
integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==
dependencies:
unist-util-visit-parents "^2.0.0"
unist-util-visit@^4.0.0, unist-util-visit@^4.1.0: unist-util-visit@^4.0.0, unist-util-visit@^4.1.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.1.tgz#1c4842d70bd3df6cc545276f5164f933390a9aad" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.1.tgz#1c4842d70bd3df6cc545276f5164f933390a9aad"