@puredesktop/platform-editor 1.0.0-beta.1 → 1.0.0-beta.2
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 +1 -1
- package/src/CollapsePanel.tsx +35 -35
- package/src/DocumentDiffView.tsx +130 -130
- package/src/DocumentEditor.test.ts +118 -118
- package/src/DocumentEditor.tsx +2651 -2631
- package/src/alignedLineDiff.test.ts +52 -52
- package/src/alignedLineDiff.ts +67 -67
- package/src/collectionAssetSrc.ts +2 -2
- package/src/commentUtils.test.ts +350 -350
- package/src/commentUtils.ts +546 -546
- package/src/constants/toolKeys.ts +14 -14
- package/src/contentFormat.test.ts +27 -27
- package/src/contentFormat.ts +18 -18
- package/src/editorExtensions.ts +155 -155
- package/src/extensions/appliedChangeMark.ts +115 -115
- package/src/extensions/autoReviewPrompts.test.ts +529 -529
- package/src/extensions/autoReviewPrompts.ts +1442 -1442
- package/src/extensions/collectionImage.ts +26 -26
- package/src/extensions/collectionImagePaste.ts +215 -215
- package/src/extensions/commentMark.ts +223 -223
- package/src/extensions/documentAssetIdentity.ts +54 -54
- package/src/extensions/figure.ts +92 -92
- package/src/extensions/footnote.ts +186 -186
- package/src/extensions/indexMarker.ts +88 -88
- package/src/extensions/mathEditing.ts +239 -239
- package/src/extensions/mermaidBlock.tsx +297 -297
- package/src/extensions/slashCommands.test.ts +170 -170
- package/src/extensions/slashCommands.ts +746 -746
- package/src/extensions/smartTypography.test.ts +74 -74
- package/src/extensions/smartTypography.ts +120 -120
- package/src/index.ts +138 -137
- package/src/insertCollectionImage.test.ts +60 -60
- package/src/insertCollectionImage.ts +63 -63
- package/src/insertInlineAssetKind.test.ts +67 -67
- package/src/insertInlineAssetKind.ts +49 -49
- package/src/mermaidPreview.test.ts +54 -54
- package/src/mermaidPreview.ts +115 -115
- package/src/styled.ts +697 -676
- package/src/toolbar/ToolBtn.tsx +77 -63
- package/src/toolbar/Toolbar.test.tsx +325 -325
- package/src/toolbar/Toolbar.tsx +662 -624
- package/src/toolbar/groups/HeadingGroup.tsx +175 -153
- package/src/toolbar/overlayTypes.ts +28 -28
- package/src/useEditorExtensions.ts +22 -22
- package/src/utils/markdownUtils.ts +331 -331
package/package.json
CHANGED
package/src/CollapsePanel.tsx
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { type ReactNode } from 'react'
|
|
2
|
-
import { styled } from 'styled-components'
|
|
3
|
-
|
|
4
|
-
export interface CollapsePanelProps {
|
|
5
|
-
open: boolean
|
|
6
|
-
children: ReactNode
|
|
7
|
-
className?: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function CollapsePanel({
|
|
11
|
-
open,
|
|
12
|
-
children,
|
|
13
|
-
className,
|
|
14
|
-
}: CollapsePanelProps): React.ReactElement {
|
|
15
|
-
return (
|
|
16
|
-
<StyledPanel data-open={open} className={className}>
|
|
17
|
-
<StyledInner>{children}</StyledInner>
|
|
18
|
-
</StyledPanel>
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const StyledPanel = styled.div`
|
|
23
|
-
display: grid;
|
|
24
|
-
grid-template-rows: 0fr;
|
|
25
|
-
transition: grid-template-rows 0.2s ease;
|
|
26
|
-
overflow: hidden;
|
|
27
|
-
|
|
28
|
-
&[data-open='true'] {
|
|
29
|
-
grid-template-rows: 1fr;
|
|
30
|
-
}
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
const StyledInner = styled.div`
|
|
34
|
-
min-height: 0;
|
|
35
|
-
`
|
|
1
|
+
import { type ReactNode } from 'react'
|
|
2
|
+
import { styled } from 'styled-components'
|
|
3
|
+
|
|
4
|
+
export interface CollapsePanelProps {
|
|
5
|
+
open: boolean
|
|
6
|
+
children: ReactNode
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CollapsePanel({
|
|
11
|
+
open,
|
|
12
|
+
children,
|
|
13
|
+
className,
|
|
14
|
+
}: CollapsePanelProps): React.ReactElement {
|
|
15
|
+
return (
|
|
16
|
+
<StyledPanel data-open={open} className={className}>
|
|
17
|
+
<StyledInner>{children}</StyledInner>
|
|
18
|
+
</StyledPanel>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const StyledPanel = styled.div`
|
|
23
|
+
display: grid;
|
|
24
|
+
grid-template-rows: 0fr;
|
|
25
|
+
transition: grid-template-rows 0.2s ease;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
|
|
28
|
+
&[data-open='true'] {
|
|
29
|
+
grid-template-rows: 1fr;
|
|
30
|
+
}
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
const StyledInner = styled.div`
|
|
34
|
+
min-height: 0;
|
|
35
|
+
`
|
package/src/DocumentDiffView.tsx
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
import { useMemo, type ReactElement } from 'react'
|
|
2
|
-
import { styled } from 'styled-components'
|
|
3
|
-
import {
|
|
4
|
-
buildAlignedLineDiff,
|
|
5
|
-
type AlignedDiffLine,
|
|
6
|
-
type AlignedDiffLineKind,
|
|
7
|
-
} from './alignedLineDiff.js'
|
|
8
|
-
|
|
9
|
-
export interface DocumentDiffViewProps {
|
|
10
|
-
left: string
|
|
11
|
-
right: string
|
|
12
|
-
leftLabel?: string
|
|
13
|
-
rightLabel?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function DocumentDiffView({
|
|
17
|
-
left,
|
|
18
|
-
right,
|
|
19
|
-
leftLabel = 'Yours',
|
|
20
|
-
rightLabel = 'On disk',
|
|
21
|
-
}: DocumentDiffViewProps): ReactElement {
|
|
22
|
-
const { leftLines, rightLines } = useMemo(
|
|
23
|
-
() => buildAlignedLineDiff(left, right),
|
|
24
|
-
[left, right],
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<StyledRoot>
|
|
29
|
-
<StyledHeader>
|
|
30
|
-
<StyledColumnHeader $tone="left">{leftLabel}</StyledColumnHeader>
|
|
31
|
-
<StyledColumnHeader $tone="right">{rightLabel}</StyledColumnHeader>
|
|
32
|
-
</StyledHeader>
|
|
33
|
-
<StyledGrid>
|
|
34
|
-
<StyledColumn>
|
|
35
|
-
{leftLines.map((line, index) => (
|
|
36
|
-
<DiffLineRow key={`left-${index}`} line={line} />
|
|
37
|
-
))}
|
|
38
|
-
</StyledColumn>
|
|
39
|
-
<StyledColumn>
|
|
40
|
-
{rightLines.map((line, index) => (
|
|
41
|
-
<DiffLineRow key={`right-${index}`} line={line} />
|
|
42
|
-
))}
|
|
43
|
-
</StyledColumn>
|
|
44
|
-
</StyledGrid>
|
|
45
|
-
</StyledRoot>
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function DiffLineRow({ line }: { line: AlignedDiffLine }): ReactElement {
|
|
50
|
-
return (
|
|
51
|
-
<StyledLine $kind={line.kind}>
|
|
52
|
-
<StyledLineNo>{line.lineNumber ?? ''}</StyledLineNo>
|
|
53
|
-
<StyledLineText>{line.text || '\u00a0'}</StyledLineText>
|
|
54
|
-
</StyledLine>
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
//#region styled-components
|
|
59
|
-
|
|
60
|
-
const StyledRoot = styled.div`
|
|
61
|
-
display: flex;
|
|
62
|
-
flex-direction: column;
|
|
63
|
-
min-height: 0;
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
const StyledHeader = styled.div`
|
|
67
|
-
display: grid;
|
|
68
|
-
grid-template-columns: 1fr 1fr;
|
|
69
|
-
gap: var(--platform-spacing-sm);
|
|
70
|
-
margin-bottom: var(--platform-spacing-sm);
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
const StyledColumnHeader = styled.div<{ $tone: 'left' | 'right' }>`
|
|
74
|
-
padding: var(--platform-spacing-xs) var(--platform-spacing-sm);
|
|
75
|
-
font-size: var(--platform-typography-font-size-sm);
|
|
76
|
-
font-weight: var(--platform-typography-font-weight-medium);
|
|
77
|
-
color: ${({ $tone }) =>
|
|
78
|
-
$tone === 'left'
|
|
79
|
-
? 'var(--platform-colors-accent)'
|
|
80
|
-
: 'var(--platform-colors-text-secondary)'};
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
const StyledGrid = styled.div`
|
|
84
|
-
display: grid;
|
|
85
|
-
grid-template-columns: 1fr 1fr;
|
|
86
|
-
gap: var(--platform-spacing-sm);
|
|
87
|
-
max-height: 60vh;
|
|
88
|
-
overflow: auto;
|
|
89
|
-
border: 1px solid var(--platform-colors-border);
|
|
90
|
-
border-radius: 0;
|
|
91
|
-
background: var(--platform-colors-bg);
|
|
92
|
-
`
|
|
93
|
-
|
|
94
|
-
const StyledColumn = styled.div`
|
|
95
|
-
font-family: var(--platform-typography-font-family-mono);
|
|
96
|
-
font-size: 12px;
|
|
97
|
-
line-height: 1.45;
|
|
98
|
-
padding: var(--platform-spacing-xs) 0;
|
|
99
|
-
`
|
|
100
|
-
|
|
101
|
-
const StyledLine = styled.div<{ $kind: AlignedDiffLineKind }>`
|
|
102
|
-
display: grid;
|
|
103
|
-
grid-template-columns: 36px 1fr;
|
|
104
|
-
background: ${({ $kind }) =>
|
|
105
|
-
$kind === 'added'
|
|
106
|
-
? 'rgb(from var(--platform-colors-success) r g b / 14%)'
|
|
107
|
-
: $kind === 'removed'
|
|
108
|
-
? 'rgb(from var(--platform-colors-danger) r g b / 14%)'
|
|
109
|
-
: 'transparent'};
|
|
110
|
-
color: ${({ $kind }) =>
|
|
111
|
-
$kind === 'pad'
|
|
112
|
-
? 'var(--platform-colors-text-disabled)'
|
|
113
|
-
: 'var(--platform-colors-text)'};
|
|
114
|
-
min-height: 17px;
|
|
115
|
-
`
|
|
116
|
-
|
|
117
|
-
const StyledLineNo = styled.div`
|
|
118
|
-
color: var(--platform-colors-text-disabled);
|
|
119
|
-
text-align: right;
|
|
120
|
-
padding-right: 8px;
|
|
121
|
-
user-select: none;
|
|
122
|
-
`
|
|
123
|
-
|
|
124
|
-
const StyledLineText = styled.div`
|
|
125
|
-
white-space: pre-wrap;
|
|
126
|
-
word-break: break-word;
|
|
127
|
-
padding-right: var(--platform-spacing-sm);
|
|
128
|
-
`
|
|
129
|
-
|
|
130
|
-
//#endregion
|
|
1
|
+
import { useMemo, type ReactElement } from 'react'
|
|
2
|
+
import { styled } from 'styled-components'
|
|
3
|
+
import {
|
|
4
|
+
buildAlignedLineDiff,
|
|
5
|
+
type AlignedDiffLine,
|
|
6
|
+
type AlignedDiffLineKind,
|
|
7
|
+
} from './alignedLineDiff.js'
|
|
8
|
+
|
|
9
|
+
export interface DocumentDiffViewProps {
|
|
10
|
+
left: string
|
|
11
|
+
right: string
|
|
12
|
+
leftLabel?: string
|
|
13
|
+
rightLabel?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DocumentDiffView({
|
|
17
|
+
left,
|
|
18
|
+
right,
|
|
19
|
+
leftLabel = 'Yours',
|
|
20
|
+
rightLabel = 'On disk',
|
|
21
|
+
}: DocumentDiffViewProps): ReactElement {
|
|
22
|
+
const { leftLines, rightLines } = useMemo(
|
|
23
|
+
() => buildAlignedLineDiff(left, right),
|
|
24
|
+
[left, right],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<StyledRoot>
|
|
29
|
+
<StyledHeader>
|
|
30
|
+
<StyledColumnHeader $tone="left">{leftLabel}</StyledColumnHeader>
|
|
31
|
+
<StyledColumnHeader $tone="right">{rightLabel}</StyledColumnHeader>
|
|
32
|
+
</StyledHeader>
|
|
33
|
+
<StyledGrid>
|
|
34
|
+
<StyledColumn>
|
|
35
|
+
{leftLines.map((line, index) => (
|
|
36
|
+
<DiffLineRow key={`left-${index}`} line={line} />
|
|
37
|
+
))}
|
|
38
|
+
</StyledColumn>
|
|
39
|
+
<StyledColumn>
|
|
40
|
+
{rightLines.map((line, index) => (
|
|
41
|
+
<DiffLineRow key={`right-${index}`} line={line} />
|
|
42
|
+
))}
|
|
43
|
+
</StyledColumn>
|
|
44
|
+
</StyledGrid>
|
|
45
|
+
</StyledRoot>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function DiffLineRow({ line }: { line: AlignedDiffLine }): ReactElement {
|
|
50
|
+
return (
|
|
51
|
+
<StyledLine $kind={line.kind}>
|
|
52
|
+
<StyledLineNo>{line.lineNumber ?? ''}</StyledLineNo>
|
|
53
|
+
<StyledLineText>{line.text || '\u00a0'}</StyledLineText>
|
|
54
|
+
</StyledLine>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#region styled-components
|
|
59
|
+
|
|
60
|
+
const StyledRoot = styled.div`
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
min-height: 0;
|
|
64
|
+
`
|
|
65
|
+
|
|
66
|
+
const StyledHeader = styled.div`
|
|
67
|
+
display: grid;
|
|
68
|
+
grid-template-columns: 1fr 1fr;
|
|
69
|
+
gap: var(--platform-spacing-sm);
|
|
70
|
+
margin-bottom: var(--platform-spacing-sm);
|
|
71
|
+
`
|
|
72
|
+
|
|
73
|
+
const StyledColumnHeader = styled.div<{ $tone: 'left' | 'right' }>`
|
|
74
|
+
padding: var(--platform-spacing-xs) var(--platform-spacing-sm);
|
|
75
|
+
font-size: var(--platform-typography-font-size-sm);
|
|
76
|
+
font-weight: var(--platform-typography-font-weight-medium);
|
|
77
|
+
color: ${({ $tone }) =>
|
|
78
|
+
$tone === 'left'
|
|
79
|
+
? 'var(--platform-colors-accent)'
|
|
80
|
+
: 'var(--platform-colors-text-secondary)'};
|
|
81
|
+
`
|
|
82
|
+
|
|
83
|
+
const StyledGrid = styled.div`
|
|
84
|
+
display: grid;
|
|
85
|
+
grid-template-columns: 1fr 1fr;
|
|
86
|
+
gap: var(--platform-spacing-sm);
|
|
87
|
+
max-height: 60vh;
|
|
88
|
+
overflow: auto;
|
|
89
|
+
border: 1px solid var(--platform-colors-border);
|
|
90
|
+
border-radius: 0;
|
|
91
|
+
background: var(--platform-colors-bg);
|
|
92
|
+
`
|
|
93
|
+
|
|
94
|
+
const StyledColumn = styled.div`
|
|
95
|
+
font-family: var(--platform-typography-font-family-mono);
|
|
96
|
+
font-size: 12px;
|
|
97
|
+
line-height: 1.45;
|
|
98
|
+
padding: var(--platform-spacing-xs) 0;
|
|
99
|
+
`
|
|
100
|
+
|
|
101
|
+
const StyledLine = styled.div<{ $kind: AlignedDiffLineKind }>`
|
|
102
|
+
display: grid;
|
|
103
|
+
grid-template-columns: 36px 1fr;
|
|
104
|
+
background: ${({ $kind }) =>
|
|
105
|
+
$kind === 'added'
|
|
106
|
+
? 'rgb(from var(--platform-colors-success) r g b / 14%)'
|
|
107
|
+
: $kind === 'removed'
|
|
108
|
+
? 'rgb(from var(--platform-colors-danger) r g b / 14%)'
|
|
109
|
+
: 'transparent'};
|
|
110
|
+
color: ${({ $kind }) =>
|
|
111
|
+
$kind === 'pad'
|
|
112
|
+
? 'var(--platform-colors-text-disabled)'
|
|
113
|
+
: 'var(--platform-colors-text)'};
|
|
114
|
+
min-height: 17px;
|
|
115
|
+
`
|
|
116
|
+
|
|
117
|
+
const StyledLineNo = styled.div`
|
|
118
|
+
color: var(--platform-colors-text-disabled);
|
|
119
|
+
text-align: right;
|
|
120
|
+
padding-right: 8px;
|
|
121
|
+
user-select: none;
|
|
122
|
+
`
|
|
123
|
+
|
|
124
|
+
const StyledLineText = styled.div`
|
|
125
|
+
white-space: pre-wrap;
|
|
126
|
+
word-break: break-word;
|
|
127
|
+
padding-right: var(--platform-spacing-sm);
|
|
128
|
+
`
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
import type { Editor } from '@tiptap/core'
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
-
import {
|
|
5
|
-
isReviewRailCommentType,
|
|
6
|
-
isSelectionAnnotatable,
|
|
7
|
-
progressiveAutoReviewBatches,
|
|
8
|
-
} from './DocumentEditor.js'
|
|
9
|
-
|
|
10
|
-
function mockEditorForNodes(
|
|
11
|
-
startNode: Node,
|
|
12
|
-
endNode: Node,
|
|
13
|
-
options: { editable?: boolean; destroyed?: boolean } = {},
|
|
14
|
-
): Editor {
|
|
15
|
-
return {
|
|
16
|
-
isDestroyed: options.destroyed ?? false,
|
|
17
|
-
isEditable: options.editable ?? true,
|
|
18
|
-
view: {
|
|
19
|
-
domAtPos: vi.fn((pos: number) => ({
|
|
20
|
-
node: pos <= 1 ? startNode : endNode,
|
|
21
|
-
})),
|
|
22
|
-
},
|
|
23
|
-
} as unknown as Editor
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe('DocumentEditor annotation selection rules', () => {
|
|
27
|
-
it('shows rail badges for editorial review objects', () => {
|
|
28
|
-
expect(isReviewRailCommentType('copyedit')).toBe(true)
|
|
29
|
-
expect(isReviewRailCommentType('voice')).toBe(true)
|
|
30
|
-
expect(isReviewRailCommentType('claim')).toBe(true)
|
|
31
|
-
expect(isReviewRailCommentType('fact-check')).toBe(true)
|
|
32
|
-
expect(isReviewRailCommentType('general')).toBe(false)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('allows non-empty selections in editable manuscript content', () => {
|
|
36
|
-
const paragraph = document.createElement('p')
|
|
37
|
-
const start = document.createTextNode('Selected')
|
|
38
|
-
const end = document.createTextNode(' text')
|
|
39
|
-
paragraph.append(start, end)
|
|
40
|
-
const editor = mockEditorForNodes(start, end)
|
|
41
|
-
|
|
42
|
-
expect(isSelectionAnnotatable(editor, 1, 4)).toBe(true)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('does not offer annotation creation inside protected generated content', () => {
|
|
46
|
-
const referenceEntry = document.createElement('li')
|
|
47
|
-
referenceEntry.setAttribute('data-pm-reference-entry', 'true')
|
|
48
|
-
referenceEntry.setAttribute('contenteditable', 'false')
|
|
49
|
-
const start = document.createTextNode('Generated')
|
|
50
|
-
const end = document.createTextNode(' reference')
|
|
51
|
-
referenceEntry.append(start, end)
|
|
52
|
-
const editor = mockEditorForNodes(start, end)
|
|
53
|
-
|
|
54
|
-
expect(isSelectionAnnotatable(editor, 1, 4)).toBe(false)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('does not offer annotation creation inside citation atoms', () => {
|
|
58
|
-
const citation = document.createElement('cite')
|
|
59
|
-
citation.className = 'ps-citation'
|
|
60
|
-
const start = document.createTextNode('[1')
|
|
61
|
-
const end = document.createTextNode(']')
|
|
62
|
-
citation.append(start, end)
|
|
63
|
-
const editor = mockEditorForNodes(start, end)
|
|
64
|
-
|
|
65
|
-
expect(isSelectionAnnotatable(editor, 1, 4)).toBe(false)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('does not offer annotation creation for collapsed or read-only selections', () => {
|
|
69
|
-
const text = document.createTextNode('Text')
|
|
70
|
-
|
|
71
|
-
expect(isSelectionAnnotatable(mockEditorForNodes(text, text), 2, 2)).toBe(
|
|
72
|
-
false,
|
|
73
|
-
)
|
|
74
|
-
expect(
|
|
75
|
-
isSelectionAnnotatable(
|
|
76
|
-
mockEditorForNodes(text, text, { editable: false }),
|
|
77
|
-
1,
|
|
78
|
-
4,
|
|
79
|
-
),
|
|
80
|
-
).toBe(false)
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
describe('DocumentEditor progressive editorial review', () => {
|
|
85
|
-
it('front-loads small batches before larger background batches', () => {
|
|
86
|
-
const paragraphs = Array.from({ length: 31 }, (_, index) => ({
|
|
87
|
-
id: `p-${index + 1}`,
|
|
88
|
-
text: `Paragraph ${index + 1}`,
|
|
89
|
-
from: index + 1,
|
|
90
|
-
pos: index + 1,
|
|
91
|
-
to: index + 2,
|
|
92
|
-
}))
|
|
93
|
-
|
|
94
|
-
const batches = progressiveAutoReviewBatches(paragraphs)
|
|
95
|
-
|
|
96
|
-
expect(batches.map(batch => batch.length)).toEqual([3, 3, 3, 3, 12, 7])
|
|
97
|
-
expect(batches[0]?.map(paragraph => paragraph.id)).toEqual([
|
|
98
|
-
'p-1',
|
|
99
|
-
'p-2',
|
|
100
|
-
'p-3',
|
|
101
|
-
])
|
|
102
|
-
expect(batches[4]?.[0]?.id).toBe('p-13')
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('keeps short documents in small startup batches only', () => {
|
|
106
|
-
const paragraphs = Array.from({ length: 5 }, (_, index) => ({
|
|
107
|
-
id: `p-${index + 1}`,
|
|
108
|
-
text: `Paragraph ${index + 1}`,
|
|
109
|
-
from: index + 1,
|
|
110
|
-
pos: index + 1,
|
|
111
|
-
to: index + 2,
|
|
112
|
-
}))
|
|
113
|
-
|
|
114
|
-
expect(
|
|
115
|
-
progressiveAutoReviewBatches(paragraphs).map(batch => batch.length),
|
|
116
|
-
).toEqual([3, 2])
|
|
117
|
-
})
|
|
118
|
-
})
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import type { Editor } from '@tiptap/core'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
isReviewRailCommentType,
|
|
6
|
+
isSelectionAnnotatable,
|
|
7
|
+
progressiveAutoReviewBatches,
|
|
8
|
+
} from './DocumentEditor.js'
|
|
9
|
+
|
|
10
|
+
function mockEditorForNodes(
|
|
11
|
+
startNode: Node,
|
|
12
|
+
endNode: Node,
|
|
13
|
+
options: { editable?: boolean; destroyed?: boolean } = {},
|
|
14
|
+
): Editor {
|
|
15
|
+
return {
|
|
16
|
+
isDestroyed: options.destroyed ?? false,
|
|
17
|
+
isEditable: options.editable ?? true,
|
|
18
|
+
view: {
|
|
19
|
+
domAtPos: vi.fn((pos: number) => ({
|
|
20
|
+
node: pos <= 1 ? startNode : endNode,
|
|
21
|
+
})),
|
|
22
|
+
},
|
|
23
|
+
} as unknown as Editor
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('DocumentEditor annotation selection rules', () => {
|
|
27
|
+
it('shows rail badges for editorial review objects', () => {
|
|
28
|
+
expect(isReviewRailCommentType('copyedit')).toBe(true)
|
|
29
|
+
expect(isReviewRailCommentType('voice')).toBe(true)
|
|
30
|
+
expect(isReviewRailCommentType('claim')).toBe(true)
|
|
31
|
+
expect(isReviewRailCommentType('fact-check')).toBe(true)
|
|
32
|
+
expect(isReviewRailCommentType('general')).toBe(false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('allows non-empty selections in editable manuscript content', () => {
|
|
36
|
+
const paragraph = document.createElement('p')
|
|
37
|
+
const start = document.createTextNode('Selected')
|
|
38
|
+
const end = document.createTextNode(' text')
|
|
39
|
+
paragraph.append(start, end)
|
|
40
|
+
const editor = mockEditorForNodes(start, end)
|
|
41
|
+
|
|
42
|
+
expect(isSelectionAnnotatable(editor, 1, 4)).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('does not offer annotation creation inside protected generated content', () => {
|
|
46
|
+
const referenceEntry = document.createElement('li')
|
|
47
|
+
referenceEntry.setAttribute('data-pm-reference-entry', 'true')
|
|
48
|
+
referenceEntry.setAttribute('contenteditable', 'false')
|
|
49
|
+
const start = document.createTextNode('Generated')
|
|
50
|
+
const end = document.createTextNode(' reference')
|
|
51
|
+
referenceEntry.append(start, end)
|
|
52
|
+
const editor = mockEditorForNodes(start, end)
|
|
53
|
+
|
|
54
|
+
expect(isSelectionAnnotatable(editor, 1, 4)).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('does not offer annotation creation inside citation atoms', () => {
|
|
58
|
+
const citation = document.createElement('cite')
|
|
59
|
+
citation.className = 'ps-citation'
|
|
60
|
+
const start = document.createTextNode('[1')
|
|
61
|
+
const end = document.createTextNode(']')
|
|
62
|
+
citation.append(start, end)
|
|
63
|
+
const editor = mockEditorForNodes(start, end)
|
|
64
|
+
|
|
65
|
+
expect(isSelectionAnnotatable(editor, 1, 4)).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('does not offer annotation creation for collapsed or read-only selections', () => {
|
|
69
|
+
const text = document.createTextNode('Text')
|
|
70
|
+
|
|
71
|
+
expect(isSelectionAnnotatable(mockEditorForNodes(text, text), 2, 2)).toBe(
|
|
72
|
+
false,
|
|
73
|
+
)
|
|
74
|
+
expect(
|
|
75
|
+
isSelectionAnnotatable(
|
|
76
|
+
mockEditorForNodes(text, text, { editable: false }),
|
|
77
|
+
1,
|
|
78
|
+
4,
|
|
79
|
+
),
|
|
80
|
+
).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('DocumentEditor progressive editorial review', () => {
|
|
85
|
+
it('front-loads small batches before larger background batches', () => {
|
|
86
|
+
const paragraphs = Array.from({ length: 31 }, (_, index) => ({
|
|
87
|
+
id: `p-${index + 1}`,
|
|
88
|
+
text: `Paragraph ${index + 1}`,
|
|
89
|
+
from: index + 1,
|
|
90
|
+
pos: index + 1,
|
|
91
|
+
to: index + 2,
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
const batches = progressiveAutoReviewBatches(paragraphs)
|
|
95
|
+
|
|
96
|
+
expect(batches.map(batch => batch.length)).toEqual([3, 3, 3, 3, 12, 7])
|
|
97
|
+
expect(batches[0]?.map(paragraph => paragraph.id)).toEqual([
|
|
98
|
+
'p-1',
|
|
99
|
+
'p-2',
|
|
100
|
+
'p-3',
|
|
101
|
+
])
|
|
102
|
+
expect(batches[4]?.[0]?.id).toBe('p-13')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('keeps short documents in small startup batches only', () => {
|
|
106
|
+
const paragraphs = Array.from({ length: 5 }, (_, index) => ({
|
|
107
|
+
id: `p-${index + 1}`,
|
|
108
|
+
text: `Paragraph ${index + 1}`,
|
|
109
|
+
from: index + 1,
|
|
110
|
+
pos: index + 1,
|
|
111
|
+
to: index + 2,
|
|
112
|
+
}))
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
progressiveAutoReviewBatches(paragraphs).map(batch => batch.length),
|
|
116
|
+
).toEqual([3, 2])
|
|
117
|
+
})
|
|
118
|
+
})
|