Skip to content

Commit b9d99f2

Browse files
committed
enh(sync): updateFromContent()
This could be the basis for automatically merging changes from an offline editing session and changes by overwriting the doc. Signed-off-by: Max <[email protected]>
1 parent 3c187fb commit b9d99f2

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed

src/helpers/updateFromContent.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { Collaboration } from '@tiptap/extension-collaboration'
7+
import escapeHtml from 'escape-html'
8+
import { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector } from 'yjs'
9+
import { createPlainEditor, createRichEditor } from '../EditorFactory.js'
10+
import markdownit from '../markdownit/index.js'
11+
12+
export const updateFromContent = (
13+
baseDoc: Doc,
14+
content: string,
15+
{ isRichEditor }: { isRichEditor: boolean },
16+
): Uint8Array => {
17+
// work on a copy
18+
const copy = new Doc()
19+
// we might still want to only apply this once
20+
// copy.clientID = 0
21+
applyUpdate(copy, encodeStateAsUpdate(baseDoc))
22+
setContent(copy, content, { isRichEditor })
23+
return encodeStateAsUpdate(copy, encodeStateVector(baseDoc))
24+
}
25+
26+
const setContent = (
27+
doc: Doc,
28+
content: string,
29+
{ isRichEditor }: { isRichEditor: boolean },
30+
) => {
31+
const html = isRichEditor
32+
? markdownit.render(content) + '<p/>'
33+
: `<pre>${escapeHtml(content)}</pre>`
34+
const editor = isRichEditor
35+
? createRichEditor({
36+
extensions: [Collaboration.configure({ document: doc })],
37+
})
38+
: createPlainEditor({
39+
extensions: [Collaboration.configure({ document: doc })],
40+
})
41+
editor.commands.setContent(html)
42+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { Collaboration } from '@tiptap/extension-collaboration'
7+
import { describe, expect, it } from 'vitest'
8+
import * as Y from 'yjs'
9+
import type { Connection } from '../../composables/useConnection.js'
10+
import { createRichEditor } from '../../EditorFactory.js'
11+
import { createMarkdownSerializer } from '../../extensions/Markdown.js'
12+
import { updateFromContent } from '../../helpers/updateFromContent'
13+
14+
15+
describe('apply content', () => {
16+
17+
it('applies content to an empty doc', () => {
18+
const ydoc = new Y.Doc()
19+
const content = '## Hello world'
20+
const update = updateFromContent(ydoc, content, { isRichEditor: true })
21+
expect(contentWithUpdates(Y.encodeStateAsUpdate(ydoc), update)).toBe(content)
22+
})
23+
24+
it('applies content to a doc that already has content', () => {
25+
const ydoc = new Y.Doc()
26+
const initialContent = `
27+
## Hello world
28+
29+
### Section one
30+
31+
Nothing to see here.
32+
33+
### Section two
34+
35+
Carry on!
36+
`
37+
const changeOne = `
38+
## Hello world
39+
40+
### Section one
41+
42+
Uh... wow!
43+
44+
### Section two
45+
46+
Carry on!
47+
`
48+
const changeTwo = `
49+
## Hello world
50+
51+
### Section one
52+
53+
Nothing to see here.
54+
55+
### Section two
56+
57+
What's that?
58+
`
59+
60+
const initialUpdate = updateFromContent(ydoc, initialContent, { isRichEditor: true })
61+
Y.applyUpdate(ydoc, initialUpdate)
62+
const updateOne = updateFromContent(ydoc, changeOne, { isRichEditor: true })
63+
const updateTwo = updateFromContent(ydoc, changeTwo, { isRichEditor: true })
64+
expect(contentWithUpdates(initialUpdate, updateOne, updateTwo))
65+
.toMatchInlineSnapshot(`
66+
"## Hello world
67+
68+
### Section one
69+
70+
Uh... wow!
71+
72+
### Section two
73+
74+
What's that?"
75+
`)
76+
})
77+
78+
})
79+
80+
/**
81+
* Apply all updates to a new ydoc and return the markdown content
82+
* @param updates updates to apply
83+
*/
84+
function contentWithUpdates(...updates: Uint8Array[]) {
85+
const dummyConnection: Connection = {
86+
documentId: 123,
87+
sessionId: 234,
88+
sessionToken: 'token',
89+
baseVersionEtag: 'etag',
90+
filePath: '/file.md',
91+
}
92+
const ydoc = new Y.Doc()
93+
updates.forEach((update) => {
94+
Y.applyUpdate(ydoc, update)
95+
})
96+
const editor = createRichEditor({
97+
connection: dummyConnection,
98+
extensions: [Collaboration.configure({ document: ydoc })],
99+
relativePath: '',
100+
})
101+
const serializer = createMarkdownSerializer(editor.schema)
102+
return serializer.serialize(editor.state.doc)
103+
}

0 commit comments

Comments
 (0)