@puredesktop/platform-editor 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +63 -0
- package/src/CollapsePanel.tsx +35 -0
- package/src/DocumentDiffView.tsx +130 -0
- package/src/DocumentEditor.test.ts +118 -0
- package/src/DocumentEditor.tsx +2631 -0
- package/src/alignedLineDiff.test.ts +52 -0
- package/src/alignedLineDiff.ts +67 -0
- package/src/collectionAssetSrc.ts +2 -0
- package/src/commentUtils.test.ts +350 -0
- package/src/commentUtils.ts +546 -0
- package/src/constants/toolKeys.ts +14 -0
- package/src/contentFormat.test.ts +27 -0
- package/src/contentFormat.ts +18 -0
- package/src/editorExtensions.ts +155 -0
- package/src/extensions/appliedChangeMark.ts +115 -0
- package/src/extensions/autoReviewPrompts.test.ts +529 -0
- package/src/extensions/autoReviewPrompts.ts +1442 -0
- package/src/extensions/collectionImage.ts +26 -0
- package/src/extensions/collectionImagePaste.ts +215 -0
- package/src/extensions/commentMark.ts +223 -0
- package/src/extensions/documentAssetIdentity.ts +54 -0
- package/src/extensions/figure.ts +92 -0
- package/src/extensions/footnote.ts +186 -0
- package/src/extensions/indexMarker.ts +88 -0
- package/src/extensions/mathEditing.ts +239 -0
- package/src/extensions/mermaidBlock.tsx +297 -0
- package/src/extensions/slashCommands.test.ts +170 -0
- package/src/extensions/slashCommands.ts +746 -0
- package/src/extensions/smartTypography.test.ts +74 -0
- package/src/extensions/smartTypography.ts +120 -0
- package/src/index.ts +137 -0
- package/src/insertCollectionImage.test.ts +60 -0
- package/src/insertCollectionImage.ts +63 -0
- package/src/insertInlineAssetKind.test.ts +67 -0
- package/src/insertInlineAssetKind.ts +49 -0
- package/src/mermaidPreview.test.ts +54 -0
- package/src/mermaidPreview.ts +115 -0
- package/src/styled.ts +676 -0
- package/src/toolbar/ToolBtn.tsx +63 -0
- package/src/toolbar/Toolbar.test.tsx +325 -0
- package/src/toolbar/Toolbar.tsx +624 -0
- package/src/toolbar/groups/HeadingGroup.tsx +153 -0
- package/src/toolbar/overlayTypes.ts +28 -0
- package/src/useEditorExtensions.ts +22 -0
- package/src/utils/markdownUtils.ts +331 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildAlignedLineDiff } from './alignedLineDiff.js'
|
|
3
|
+
|
|
4
|
+
describe('buildAlignedLineDiff', () => {
|
|
5
|
+
it('returns matching lines when texts are equal', () => {
|
|
6
|
+
const { leftLines, rightLines } = buildAlignedLineDiff(
|
|
7
|
+
'alpha\nbeta',
|
|
8
|
+
'alpha\nbeta',
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
expect(leftLines).toEqual([
|
|
12
|
+
{ kind: 'same', lineNumber: 1, text: 'alpha' },
|
|
13
|
+
{ kind: 'same', lineNumber: 2, text: 'beta' },
|
|
14
|
+
])
|
|
15
|
+
expect(rightLines).toEqual([
|
|
16
|
+
{ kind: 'same', lineNumber: 1, text: 'alpha' },
|
|
17
|
+
{ kind: 'same', lineNumber: 2, text: 'beta' },
|
|
18
|
+
])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('aligns removals on the left and additions on the right', () => {
|
|
22
|
+
const { leftLines, rightLines } = buildAlignedLineDiff(
|
|
23
|
+
'keep\nold\nstay',
|
|
24
|
+
'keep\nnew\nstay',
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
expect(leftLines.map(line => line.kind)).toEqual([
|
|
28
|
+
'same',
|
|
29
|
+
'removed',
|
|
30
|
+
'pad',
|
|
31
|
+
'same',
|
|
32
|
+
])
|
|
33
|
+
expect(rightLines.map(line => line.kind)).toEqual([
|
|
34
|
+
'same',
|
|
35
|
+
'pad',
|
|
36
|
+
'added',
|
|
37
|
+
'same',
|
|
38
|
+
])
|
|
39
|
+
expect(leftLines[1]?.text).toBe('old')
|
|
40
|
+
expect(rightLines[2]?.text).toBe('new')
|
|
41
|
+
expect(leftLines[1]?.lineNumber).toBe(2)
|
|
42
|
+
expect(rightLines[2]?.lineNumber).toBe(2)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('pads the opposite column for insertions and deletions', () => {
|
|
46
|
+
const { leftLines, rightLines } = buildAlignedLineDiff('a\nb', 'a\nc\nd')
|
|
47
|
+
|
|
48
|
+
expect(leftLines.some(line => line.kind === 'pad')).toBe(true)
|
|
49
|
+
expect(rightLines.some(line => line.kind === 'pad')).toBe(true)
|
|
50
|
+
expect(leftLines.length).toBe(rightLines.length)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { diffLines, type Change } from 'diff'
|
|
2
|
+
|
|
3
|
+
export type AlignedDiffLineKind = 'same' | 'added' | 'removed' | 'pad'
|
|
4
|
+
|
|
5
|
+
export interface AlignedDiffLine {
|
|
6
|
+
kind: AlignedDiffLineKind
|
|
7
|
+
lineNumber: number | null
|
|
8
|
+
text: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AlignedLineDiffResult {
|
|
12
|
+
leftLines: AlignedDiffLine[]
|
|
13
|
+
rightLines: AlignedDiffLine[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stripFinalNewline(value: string): string {
|
|
17
|
+
return value.endsWith('\n') ? value.slice(0, -1) : value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Line-diff two strings into aligned left/right columns.
|
|
22
|
+
* Removals stay on the left; additions on the right; the other side pads.
|
|
23
|
+
*/
|
|
24
|
+
export function buildAlignedLineDiff(
|
|
25
|
+
left: string,
|
|
26
|
+
right: string,
|
|
27
|
+
): AlignedLineDiffResult {
|
|
28
|
+
const parts: Change[] = diffLines(left, right)
|
|
29
|
+
const leftLines: AlignedDiffLine[] = []
|
|
30
|
+
const rightLines: AlignedDiffLine[] = []
|
|
31
|
+
let leftLineNumber = 0
|
|
32
|
+
let rightLineNumber = 0
|
|
33
|
+
|
|
34
|
+
for (const part of parts) {
|
|
35
|
+
const lines = stripFinalNewline(part.value).split('\n')
|
|
36
|
+
if (part.added) {
|
|
37
|
+
for (const text of lines) {
|
|
38
|
+
rightLineNumber += 1
|
|
39
|
+
rightLines.push({
|
|
40
|
+
kind: 'added',
|
|
41
|
+
lineNumber: rightLineNumber,
|
|
42
|
+
text,
|
|
43
|
+
})
|
|
44
|
+
leftLines.push({ kind: 'pad', lineNumber: null, text: '' })
|
|
45
|
+
}
|
|
46
|
+
} else if (part.removed) {
|
|
47
|
+
for (const text of lines) {
|
|
48
|
+
leftLineNumber += 1
|
|
49
|
+
leftLines.push({
|
|
50
|
+
kind: 'removed',
|
|
51
|
+
lineNumber: leftLineNumber,
|
|
52
|
+
text,
|
|
53
|
+
})
|
|
54
|
+
rightLines.push({ kind: 'pad', lineNumber: null, text: '' })
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
for (const text of lines) {
|
|
58
|
+
leftLineNumber += 1
|
|
59
|
+
rightLineNumber += 1
|
|
60
|
+
leftLines.push({ kind: 'same', lineNumber: leftLineNumber, text })
|
|
61
|
+
rightLines.push({ kind: 'same', lineNumber: rightLineNumber, text })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { leftLines, rightLines }
|
|
67
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { Editor } from '@tiptap/core'
|
|
5
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
7
|
+
import {
|
|
8
|
+
COMMENT_STATUS_LABELS,
|
|
9
|
+
COMMENT_TYPE_DEFINITIONS,
|
|
10
|
+
applyCommentMarkToRange,
|
|
11
|
+
applyCommentReplacementById,
|
|
12
|
+
normalizeCommentReviewStatus,
|
|
13
|
+
removeAllCommentMarks,
|
|
14
|
+
removeCommentMarksById,
|
|
15
|
+
serializeCommentReplies,
|
|
16
|
+
stripCommentMarkupFromHtml,
|
|
17
|
+
updateCommentMarksById,
|
|
18
|
+
} from './commentUtils.js'
|
|
19
|
+
import { AppliedChangeMark } from './extensions/appliedChangeMark.js'
|
|
20
|
+
import { CommentMark } from './extensions/commentMark.js'
|
|
21
|
+
|
|
22
|
+
describe('comment marks', () => {
|
|
23
|
+
it('does not scroll or focus the editor by default when creating marks', () => {
|
|
24
|
+
const editor = new Editor({
|
|
25
|
+
extensions: [StarterKit, CommentMark],
|
|
26
|
+
content: '<p>Review this sentence.</p>',
|
|
27
|
+
})
|
|
28
|
+
const dispatchSpy = vi.spyOn(editor.view, 'dispatch')
|
|
29
|
+
const focusSpy = vi.spyOn(editor.view, 'focus')
|
|
30
|
+
|
|
31
|
+
expect(
|
|
32
|
+
applyCommentMarkToRange(editor, 1, 7, {
|
|
33
|
+
commentId: 'comment-quiet',
|
|
34
|
+
commentText: 'No viewport jump.',
|
|
35
|
+
}),
|
|
36
|
+
).toBe(true)
|
|
37
|
+
|
|
38
|
+
expect(dispatchSpy).toHaveBeenCalledOnce()
|
|
39
|
+
expect(dispatchSpy.mock.calls[0]?.[0].scrolledIntoView).toBe(false)
|
|
40
|
+
expect(focusSpy).not.toHaveBeenCalled()
|
|
41
|
+
editor.destroy()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('can opt into scroll and focus behavior for compatibility', () => {
|
|
45
|
+
const mark = { id: 'mark' }
|
|
46
|
+
const transaction = {
|
|
47
|
+
scrolledIntoView: false,
|
|
48
|
+
addMark: vi.fn(() => transaction),
|
|
49
|
+
scrollIntoView: vi.fn(() => {
|
|
50
|
+
transaction.scrolledIntoView = true
|
|
51
|
+
return transaction
|
|
52
|
+
}),
|
|
53
|
+
}
|
|
54
|
+
const focusSpy = vi.fn()
|
|
55
|
+
const dispatchSpy = vi.fn()
|
|
56
|
+
const editor = {
|
|
57
|
+
isDestroyed: false,
|
|
58
|
+
schema: {
|
|
59
|
+
marks: {
|
|
60
|
+
commentMark: {
|
|
61
|
+
create: vi.fn(() => mark),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
state: {
|
|
66
|
+
doc: { content: { size: 24 } },
|
|
67
|
+
tr: transaction,
|
|
68
|
+
},
|
|
69
|
+
view: { dispatch: dispatchSpy },
|
|
70
|
+
commands: { focus: focusSpy },
|
|
71
|
+
} as unknown as Editor
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
applyCommentMarkToRange(
|
|
75
|
+
editor,
|
|
76
|
+
1,
|
|
77
|
+
7,
|
|
78
|
+
{
|
|
79
|
+
commentId: 'comment-scroll',
|
|
80
|
+
commentText: 'Scroll on purpose.',
|
|
81
|
+
},
|
|
82
|
+
{ scrollIntoView: true, focusEditor: true },
|
|
83
|
+
),
|
|
84
|
+
).toBe(true)
|
|
85
|
+
|
|
86
|
+
expect(transaction.scrollIntoView).toHaveBeenCalledOnce()
|
|
87
|
+
expect(dispatchSpy).toHaveBeenCalledWith(transaction)
|
|
88
|
+
expect(focusSpy).toHaveBeenCalledWith(7)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('does not scroll or focus the editor by default when updating or deleting marks', () => {
|
|
92
|
+
const editor = new Editor({
|
|
93
|
+
extensions: [StarterKit, CommentMark],
|
|
94
|
+
content: '<p>Review this sentence.</p>',
|
|
95
|
+
})
|
|
96
|
+
expect(
|
|
97
|
+
applyCommentMarkToRange(editor, 1, 7, {
|
|
98
|
+
commentId: 'comment-static',
|
|
99
|
+
commentText: 'Static viewport.',
|
|
100
|
+
}),
|
|
101
|
+
).toBe(true)
|
|
102
|
+
const dispatchSpy = vi.spyOn(editor.view, 'dispatch')
|
|
103
|
+
const focusSpy = vi.spyOn(editor.view, 'focus')
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
updateCommentMarksById(editor, 'comment-static', {
|
|
107
|
+
commentText: 'Still static.',
|
|
108
|
+
}),
|
|
109
|
+
).toBe(true)
|
|
110
|
+
expect(removeCommentMarksById(editor, 'comment-static')).toBe(true)
|
|
111
|
+
|
|
112
|
+
expect(dispatchSpy).toHaveBeenCalledTimes(2)
|
|
113
|
+
expect(dispatchSpy.mock.calls[0]?.[0].scrolledIntoView).toBe(false)
|
|
114
|
+
expect(dispatchSpy.mock.calls[1]?.[0].scrolledIntoView).toBe(false)
|
|
115
|
+
expect(focusSpy).not.toHaveBeenCalled()
|
|
116
|
+
editor.destroy()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('clears every comment mark without deleting manuscript text', () => {
|
|
120
|
+
const editor = new Editor({
|
|
121
|
+
extensions: [StarterKit, CommentMark],
|
|
122
|
+
content: '<p>Review this sentence and verify that statement.</p>',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(
|
|
126
|
+
applyCommentMarkToRange(editor, 1, 7, {
|
|
127
|
+
commentId: 'comment-clear-1',
|
|
128
|
+
commentText: 'Support this.',
|
|
129
|
+
commentType: 'claim',
|
|
130
|
+
}),
|
|
131
|
+
).toBe(true)
|
|
132
|
+
expect(
|
|
133
|
+
applyCommentMarkToRange(editor, 26, 32, {
|
|
134
|
+
commentId: 'comment-clear-2',
|
|
135
|
+
commentText: 'Verify this.',
|
|
136
|
+
commentType: 'fact-check',
|
|
137
|
+
}),
|
|
138
|
+
).toBe(true)
|
|
139
|
+
|
|
140
|
+
expect(removeAllCommentMarks(editor)).toBe(2)
|
|
141
|
+
expect(editor.getHTML()).not.toContain('data-comment-id=')
|
|
142
|
+
expect(editor.getText()).toContain(
|
|
143
|
+
'Review this sentence and verify that statement.',
|
|
144
|
+
)
|
|
145
|
+
editor.destroy()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('serializes comment text into persisted editor HTML', () => {
|
|
149
|
+
const editor = new Editor({
|
|
150
|
+
extensions: [StarterKit, CommentMark],
|
|
151
|
+
content: '<p>Review this sentence.</p>',
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const start = editor.state.doc
|
|
155
|
+
.textBetween(0, editor.state.doc.content.size)
|
|
156
|
+
.indexOf('Review')
|
|
157
|
+
expect(start).toBeGreaterThanOrEqual(0)
|
|
158
|
+
|
|
159
|
+
const from = start + 1
|
|
160
|
+
const to = from + 'Review'.length
|
|
161
|
+
const applied = applyCommentMarkToRange(editor, from, to, {
|
|
162
|
+
commentId: 'comment-1',
|
|
163
|
+
commentText: 'Clarify the claim.',
|
|
164
|
+
commentType: 'claim',
|
|
165
|
+
subjectText: 'Review',
|
|
166
|
+
author: 'Reviewer',
|
|
167
|
+
createdAt: '2026-06-11T12:00:00.000Z',
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
expect(applied).toBe(true)
|
|
171
|
+
expect(editor.getHTML()).toContain('data-comment-id="comment-1"')
|
|
172
|
+
expect(editor.getHTML()).toContain('data-comment-text="Clarify the claim."')
|
|
173
|
+
expect(editor.getHTML()).toContain('data-comment-type="claim"')
|
|
174
|
+
expect(editor.getHTML()).toContain('data-comment-subject="Review"')
|
|
175
|
+
expect(editor.getHTML()).toContain(
|
|
176
|
+
'data-comment-review-status="needs-support"',
|
|
177
|
+
)
|
|
178
|
+
expect(editor.getHTML()).toContain('data-comment-severity="advisory"')
|
|
179
|
+
expect(editor.getHTML()).toContain('comment-mark--claim')
|
|
180
|
+
expect(editor.getHTML()).toContain('data-comment-author="Reviewer"')
|
|
181
|
+
editor.destroy()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('serializes comment replies and resolution state', () => {
|
|
185
|
+
const editor = new Editor({
|
|
186
|
+
extensions: [StarterKit, CommentMark],
|
|
187
|
+
content: '<p>Review this sentence.</p>',
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
expect(
|
|
191
|
+
applyCommentMarkToRange(editor, 1, 7, {
|
|
192
|
+
commentId: 'comment-2',
|
|
193
|
+
commentText: 'Check this.',
|
|
194
|
+
commentType: 'fact-check',
|
|
195
|
+
subjectText: 'Review',
|
|
196
|
+
author: 'Reviewer',
|
|
197
|
+
createdAt: '2026-06-11T12:00:00.000Z',
|
|
198
|
+
}),
|
|
199
|
+
).toBe(true)
|
|
200
|
+
|
|
201
|
+
expect(
|
|
202
|
+
updateCommentMarksById(editor, 'comment-2', {
|
|
203
|
+
replies: serializeCommentReplies([
|
|
204
|
+
{
|
|
205
|
+
id: 'reply-1',
|
|
206
|
+
text: 'Updated.',
|
|
207
|
+
author: 'Author',
|
|
208
|
+
createdAt: '2026-06-11T12:05:00.000Z',
|
|
209
|
+
},
|
|
210
|
+
]),
|
|
211
|
+
resolvedAt: '2026-06-11T12:10:00.000Z',
|
|
212
|
+
resolvedBy: 'Author',
|
|
213
|
+
}),
|
|
214
|
+
).toBe(true)
|
|
215
|
+
|
|
216
|
+
expect(editor.getHTML()).toContain('data-comment-replies=')
|
|
217
|
+
expect(editor.getHTML()).toContain('Updated.')
|
|
218
|
+
expect(editor.getHTML()).toContain(
|
|
219
|
+
'data-comment-resolved-at="2026-06-11T12:10:00.000Z"',
|
|
220
|
+
)
|
|
221
|
+
expect(editor.getHTML()).toContain('comment-mark--resolved')
|
|
222
|
+
expect(editor.getHTML()).toContain('data-comment-review-status="unverified"')
|
|
223
|
+
|
|
224
|
+
expect(
|
|
225
|
+
updateCommentMarksById(editor, 'comment-2', {
|
|
226
|
+
resolvedAt: null,
|
|
227
|
+
resolvedBy: null,
|
|
228
|
+
}),
|
|
229
|
+
).toBe(true)
|
|
230
|
+
|
|
231
|
+
expect(editor.getHTML()).not.toContain('data-comment-resolved-at')
|
|
232
|
+
editor.destroy()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('updates review status without changing manuscript text', () => {
|
|
236
|
+
const editor = new Editor({
|
|
237
|
+
extensions: [StarterKit, CommentMark],
|
|
238
|
+
content: '<p>Review this sentence.</p>',
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
expect(
|
|
242
|
+
applyCommentMarkToRange(editor, 1, 7, {
|
|
243
|
+
commentId: 'comment-status',
|
|
244
|
+
commentText: 'Needs support.',
|
|
245
|
+
commentType: 'claim',
|
|
246
|
+
subjectText: 'Review',
|
|
247
|
+
}),
|
|
248
|
+
).toBe(true)
|
|
249
|
+
|
|
250
|
+
expect(
|
|
251
|
+
updateCommentMarksById(editor, 'comment-status', {
|
|
252
|
+
reviewStatus: 'supported',
|
|
253
|
+
}),
|
|
254
|
+
).toBe(true)
|
|
255
|
+
|
|
256
|
+
expect(editor.getHTML()).toContain('data-comment-review-status="supported"')
|
|
257
|
+
expect(editor.getText()).toContain('Review this sentence.')
|
|
258
|
+
editor.destroy()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('supports question annotations as a built-in comment type', () => {
|
|
262
|
+
const editor = new Editor({
|
|
263
|
+
extensions: [StarterKit, CommentMark],
|
|
264
|
+
content: '<p>Can this claim be clearer?</p>',
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
expect(
|
|
268
|
+
applyCommentMarkToRange(editor, 1, 4, {
|
|
269
|
+
commentId: 'comment-question',
|
|
270
|
+
commentText: 'Ask the author to clarify.',
|
|
271
|
+
commentType: 'question',
|
|
272
|
+
}),
|
|
273
|
+
).toBe(true)
|
|
274
|
+
|
|
275
|
+
expect(editor.getHTML()).toContain('data-comment-type="question"')
|
|
276
|
+
expect(editor.getHTML()).toContain('comment-mark--question')
|
|
277
|
+
editor.destroy()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('strips stale comment spans while preserving manuscript text', () => {
|
|
281
|
+
const result = stripCommentMarkupFromHtml(
|
|
282
|
+
'<p>Keep <span data-comment-id="c1" data-comment-type="claim" class="comment-mark comment-mark--claim">this text</span> clean.</p>',
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
expect(result.removed).toBe(1)
|
|
286
|
+
expect(result.html).toBe('<p>Keep this text clean.</p>')
|
|
287
|
+
expect(result.html).not.toContain('data-comment-id')
|
|
288
|
+
expect(result.html).not.toContain('comment-mark')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('supports stale copyedit annotations after standards change', () => {
|
|
292
|
+
expect(COMMENT_STATUS_LABELS.stale).toBe('Stale')
|
|
293
|
+
expect(COMMENT_TYPE_DEFINITIONS.copyedit.statuses).toContain('stale')
|
|
294
|
+
expect(normalizeCommentReviewStatus('stale', 'copyedit')).toBe('stale')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('removes all marks for a deleted comment', () => {
|
|
298
|
+
const editor = new Editor({
|
|
299
|
+
extensions: [StarterKit, CommentMark],
|
|
300
|
+
content: '<p>Delete this annotation.</p>',
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
expect(
|
|
304
|
+
applyCommentMarkToRange(editor, 1, 7, {
|
|
305
|
+
commentId: 'comment-delete',
|
|
306
|
+
commentText: 'Remove me.',
|
|
307
|
+
commentType: 'general',
|
|
308
|
+
}),
|
|
309
|
+
).toBe(true)
|
|
310
|
+
|
|
311
|
+
expect(removeCommentMarksById(editor, 'comment-delete')).toBe(true)
|
|
312
|
+
expect(editor.getHTML()).not.toContain('data-comment-id="comment-delete"')
|
|
313
|
+
expect(editor.getHTML()).toContain('Delete this annotation.')
|
|
314
|
+
editor.destroy()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('accepts a copy edit as an applied manuscript change', () => {
|
|
318
|
+
const editor = new Editor({
|
|
319
|
+
extensions: [StarterKit, CommentMark, AppliedChangeMark],
|
|
320
|
+
content: '<p>Review this sentence.</p>',
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
expect(
|
|
324
|
+
applyCommentMarkToRange(editor, 1, 7, {
|
|
325
|
+
commentId: 'comment-edit',
|
|
326
|
+
commentText: 'Revise',
|
|
327
|
+
commentType: 'copy-editing',
|
|
328
|
+
subjectText: 'Review',
|
|
329
|
+
}),
|
|
330
|
+
).toBe(true)
|
|
331
|
+
|
|
332
|
+
expect(
|
|
333
|
+
applyCommentReplacementById(editor, 'comment-edit', 'Revise', {
|
|
334
|
+
originalText: 'Review',
|
|
335
|
+
sourceType: 'copy-editing',
|
|
336
|
+
actor: 'Author',
|
|
337
|
+
createdAt: '2026-06-18T12:00:00.000Z',
|
|
338
|
+
}),
|
|
339
|
+
).toBe(true)
|
|
340
|
+
|
|
341
|
+
const html = editor.getHTML()
|
|
342
|
+
expect(editor.getText()).toContain('Revise this sentence.')
|
|
343
|
+
expect(html).toContain('data-pm-applied-change="true"')
|
|
344
|
+
expect(html).toContain('data-pm-applied-original="Review"')
|
|
345
|
+
expect(html).toContain('data-pm-applied-replacement="Revise"')
|
|
346
|
+
expect(html).toContain('data-pm-applied-source-comment-id="comment-edit"')
|
|
347
|
+
expect(html).not.toContain('data-comment-id="comment-edit"')
|
|
348
|
+
editor.destroy()
|
|
349
|
+
})
|
|
350
|
+
})
|