@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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/CollapsePanel.tsx +35 -35
  3. package/src/DocumentDiffView.tsx +130 -130
  4. package/src/DocumentEditor.test.ts +118 -118
  5. package/src/DocumentEditor.tsx +2651 -2631
  6. package/src/alignedLineDiff.test.ts +52 -52
  7. package/src/alignedLineDiff.ts +67 -67
  8. package/src/collectionAssetSrc.ts +2 -2
  9. package/src/commentUtils.test.ts +350 -350
  10. package/src/commentUtils.ts +546 -546
  11. package/src/constants/toolKeys.ts +14 -14
  12. package/src/contentFormat.test.ts +27 -27
  13. package/src/contentFormat.ts +18 -18
  14. package/src/editorExtensions.ts +155 -155
  15. package/src/extensions/appliedChangeMark.ts +115 -115
  16. package/src/extensions/autoReviewPrompts.test.ts +529 -529
  17. package/src/extensions/autoReviewPrompts.ts +1442 -1442
  18. package/src/extensions/collectionImage.ts +26 -26
  19. package/src/extensions/collectionImagePaste.ts +215 -215
  20. package/src/extensions/commentMark.ts +223 -223
  21. package/src/extensions/documentAssetIdentity.ts +54 -54
  22. package/src/extensions/figure.ts +92 -92
  23. package/src/extensions/footnote.ts +186 -186
  24. package/src/extensions/indexMarker.ts +88 -88
  25. package/src/extensions/mathEditing.ts +239 -239
  26. package/src/extensions/mermaidBlock.tsx +297 -297
  27. package/src/extensions/slashCommands.test.ts +170 -170
  28. package/src/extensions/slashCommands.ts +746 -746
  29. package/src/extensions/smartTypography.test.ts +74 -74
  30. package/src/extensions/smartTypography.ts +120 -120
  31. package/src/index.ts +138 -137
  32. package/src/insertCollectionImage.test.ts +60 -60
  33. package/src/insertCollectionImage.ts +63 -63
  34. package/src/insertInlineAssetKind.test.ts +67 -67
  35. package/src/insertInlineAssetKind.ts +49 -49
  36. package/src/mermaidPreview.test.ts +54 -54
  37. package/src/mermaidPreview.ts +115 -115
  38. package/src/styled.ts +697 -676
  39. package/src/toolbar/ToolBtn.tsx +77 -63
  40. package/src/toolbar/Toolbar.test.tsx +325 -325
  41. package/src/toolbar/Toolbar.tsx +662 -624
  42. package/src/toolbar/groups/HeadingGroup.tsx +175 -153
  43. package/src/toolbar/overlayTypes.ts +28 -28
  44. package/src/useEditorExtensions.ts +22 -22
  45. package/src/utils/markdownUtils.ts +331 -331
@@ -1,92 +1,92 @@
1
- import { Node, mergeAttributes } from '@tiptap/core'
2
-
3
- export interface FigureOptions {
4
- HTMLAttributes: Record<string, unknown>
5
- }
6
-
7
- declare module '@tiptap/core' {
8
- interface Commands<ReturnType> {
9
- figure: {
10
- /** Insert a figure block with an image and an editable caption. */
11
- insertFigure: (opts: {
12
- src: string
13
- alt?: string
14
- caption?: string
15
- }) => ReturnType
16
- }
17
- }
18
- }
19
-
20
- /**
21
- * Figure block — wraps an image and an editable caption. Markdown
22
- * round-trip is via `![caption](src)` (CommonMark image syntax). The
23
- * book's render pipeline already wraps such images in
24
- * `<figure><img/><figcaption/></figure>` for PDF output, so the
25
- * caption survives end-to-end even though it serializes through the
26
- * standard image syntax.
27
- *
28
- * Image-only nodes (no caption) are still produced by Image nodes directly
29
- * for inline images that don't deserve a figure wrapper.
30
- */
31
- export const Figure = Node.create<FigureOptions>({
32
- name: 'figure',
33
- group: 'block',
34
- content: 'inline*',
35
- defining: true,
36
- isolating: true,
37
- draggable: true,
38
-
39
- addOptions() {
40
- return { HTMLAttributes: {} }
41
- },
42
-
43
- addAttributes() {
44
- return {
45
- src: {
46
- default: null,
47
- parseHTML: el => el.querySelector('img')?.getAttribute('src') ?? null,
48
- },
49
- alt: {
50
- default: '',
51
- parseHTML: el => el.querySelector('img')?.getAttribute('alt') ?? '',
52
- },
53
- }
54
- },
55
-
56
- parseHTML() {
57
- return [
58
- {
59
- tag: 'figure',
60
- getAttrs: element =>
61
- element instanceof HTMLElement &&
62
- element.getAttribute('data-type') === 'mermaid'
63
- ? false
64
- : null,
65
- contentElement: 'figcaption',
66
- },
67
- ]
68
- },
69
-
70
- renderHTML({ node, HTMLAttributes }) {
71
- const { src, alt } = node.attrs as { src: string | null; alt: string }
72
- return [
73
- 'figure',
74
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
75
- ['img', { src, alt }],
76
- ['figcaption', {}, 0],
77
- ]
78
- },
79
-
80
- addCommands() {
81
- return {
82
- insertFigure:
83
- ({ src, alt = '', caption = '' }) =>
84
- ({ commands }) =>
85
- commands.insertContent({
86
- type: this.name,
87
- attrs: { src, alt },
88
- content: caption ? [{ type: 'text', text: caption }] : [],
89
- }),
90
- }
91
- },
92
- })
1
+ import { Node, mergeAttributes } from '@tiptap/core'
2
+
3
+ export interface FigureOptions {
4
+ HTMLAttributes: Record<string, unknown>
5
+ }
6
+
7
+ declare module '@tiptap/core' {
8
+ interface Commands<ReturnType> {
9
+ figure: {
10
+ /** Insert a figure block with an image and an editable caption. */
11
+ insertFigure: (opts: {
12
+ src: string
13
+ alt?: string
14
+ caption?: string
15
+ }) => ReturnType
16
+ }
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Figure block — wraps an image and an editable caption. Markdown
22
+ * round-trip is via `![caption](src)` (CommonMark image syntax). The
23
+ * book's render pipeline already wraps such images in
24
+ * `<figure><img/><figcaption/></figure>` for PDF output, so the
25
+ * caption survives end-to-end even though it serializes through the
26
+ * standard image syntax.
27
+ *
28
+ * Image-only nodes (no caption) are still produced by Image nodes directly
29
+ * for inline images that don't deserve a figure wrapper.
30
+ */
31
+ export const Figure = Node.create<FigureOptions>({
32
+ name: 'figure',
33
+ group: 'block',
34
+ content: 'inline*',
35
+ defining: true,
36
+ isolating: true,
37
+ draggable: true,
38
+
39
+ addOptions() {
40
+ return { HTMLAttributes: {} }
41
+ },
42
+
43
+ addAttributes() {
44
+ return {
45
+ src: {
46
+ default: null,
47
+ parseHTML: el => el.querySelector('img')?.getAttribute('src') ?? null,
48
+ },
49
+ alt: {
50
+ default: '',
51
+ parseHTML: el => el.querySelector('img')?.getAttribute('alt') ?? '',
52
+ },
53
+ }
54
+ },
55
+
56
+ parseHTML() {
57
+ return [
58
+ {
59
+ tag: 'figure',
60
+ getAttrs: element =>
61
+ element instanceof HTMLElement &&
62
+ element.getAttribute('data-type') === 'mermaid'
63
+ ? false
64
+ : null,
65
+ contentElement: 'figcaption',
66
+ },
67
+ ]
68
+ },
69
+
70
+ renderHTML({ node, HTMLAttributes }) {
71
+ const { src, alt } = node.attrs as { src: string | null; alt: string }
72
+ return [
73
+ 'figure',
74
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
75
+ ['img', { src, alt }],
76
+ ['figcaption', {}, 0],
77
+ ]
78
+ },
79
+
80
+ addCommands() {
81
+ return {
82
+ insertFigure:
83
+ ({ src, alt = '', caption = '' }) =>
84
+ ({ commands }) =>
85
+ commands.insertContent({
86
+ type: this.name,
87
+ attrs: { src, alt },
88
+ content: caption ? [{ type: 'text', text: caption }] : [],
89
+ }),
90
+ }
91
+ },
92
+ })
@@ -1,186 +1,186 @@
1
- import { Node, mergeAttributes } from '@tiptap/core'
2
-
3
- declare module '@tiptap/core' {
4
- interface Commands<ReturnType> {
5
- footnote: {
6
- /** Insert an empty footnote marker at the cursor and open it to edit. */
7
- insertFootnote: () => ReturnType
8
- }
9
- }
10
- }
11
-
12
- /**
13
- * Footnote — an inline, atomic reference marker.
14
- *
15
- * The body shows only a small superscript number; the note text is embedded on
16
- * the node (the `text` attribute) and edited in a popover when the marker is
17
- * clicked. Numbering is by document order, recomputed live. Serializes to
18
- * `<sup data-footnote data-text="…">` for a clean HTML round-trip; the export
19
- * pipeline (prepareFootnotes) turns markers into numbered links plus a
20
- * footnotes section.
21
- */
22
- export const Footnote = Node.create({
23
- name: 'footnote',
24
- group: 'inline',
25
- inline: true,
26
- atom: true,
27
- selectable: true,
28
-
29
- addAttributes() {
30
- return {
31
- text: {
32
- default: '',
33
- parseHTML: el => el.getAttribute('data-text') ?? '',
34
- renderHTML: (attrs: { text?: string }) => ({
35
- 'data-text': attrs.text ?? '',
36
- }),
37
- },
38
- }
39
- },
40
-
41
- parseHTML() {
42
- return [{ tag: 'sup[data-footnote]' }]
43
- },
44
-
45
- renderHTML({ HTMLAttributes }) {
46
- return [
47
- 'sup',
48
- mergeAttributes(HTMLAttributes, {
49
- 'data-footnote': '',
50
- class: 'footnote-ref',
51
- }),
52
- ]
53
- },
54
-
55
- addCommands() {
56
- return {
57
- insertFootnote:
58
- () =>
59
- ({ commands }) =>
60
- commands.insertContent({ type: this.name, attrs: { text: '' } }),
61
- }
62
- },
63
-
64
- addNodeView() {
65
- return ({ node, getPos, editor }) => {
66
- let current = node
67
- const dom = document.createElement('sup')
68
- dom.className = 'footnote-ref'
69
- dom.setAttribute('data-footnote', '')
70
- dom.contentEditable = 'false'
71
-
72
- const renderNumber = (): void => {
73
- let count = 0
74
- let mine = 0
75
- const here = typeof getPos === 'function' ? getPos() : null
76
- editor.state.doc.descendants((child, pos) => {
77
- if (child.type.name === 'footnote') {
78
- count += 1
79
- if (here !== null && pos === here) mine = count
80
- }
81
- return true
82
- })
83
- dom.textContent = String(mine || count || 1)
84
- }
85
- renderNumber()
86
-
87
- let popover: HTMLDivElement | null = null
88
- const closePopover = (): void => {
89
- popover?.remove()
90
- popover = null
91
- document.removeEventListener('mousedown', onDocPointerDown, true)
92
- }
93
- const onDocPointerDown = (event: MouseEvent): void => {
94
- if (
95
- popover &&
96
- event.target !== dom &&
97
- !popover.contains(event.target as globalThis.Node)
98
- ) {
99
- closePopover()
100
- }
101
- }
102
- const openPopover = (): void => {
103
- if (popover) return
104
- popover = document.createElement('div')
105
- popover.className = 'footnote-popover'
106
- Object.assign(popover.style, {
107
- position: 'fixed',
108
- zIndex: '1002',
109
- width: 'min(360px, calc(100vw - 24px))',
110
- padding: '8px',
111
- border: '1px solid var(--platform-colors-border, #d7dbe0)',
112
- borderRadius: '8px',
113
- background: 'var(--platform-colors-surface, #fff)',
114
- boxShadow: '0 18px 42px rgb(23 29 36 / 0.18)',
115
- })
116
- const textarea = document.createElement('textarea')
117
- textarea.value = current.attrs.text ?? ''
118
- textarea.placeholder = 'Footnote text…'
119
- textarea.rows = 3
120
- Object.assign(textarea.style, {
121
- width: '100%',
122
- boxSizing: 'border-box',
123
- resize: 'vertical',
124
- border: '1px solid var(--platform-colors-border, #d7dbe0)',
125
- borderRadius: '6px',
126
- padding: '6px 8px',
127
- font: 'inherit',
128
- fontSize: '13px',
129
- color: 'inherit',
130
- background: 'var(--platform-colors-surface, #fff)',
131
- })
132
- popover.appendChild(textarea)
133
-
134
- const rect = dom.getBoundingClientRect()
135
- popover.style.left = `${Math.max(12, rect.left)}px`
136
- popover.style.top = `${rect.bottom + 6}px`
137
- document.body.appendChild(popover)
138
- textarea.focus()
139
-
140
- const commit = (): void => {
141
- if (typeof getPos !== 'function') return
142
- const pos = getPos()
143
- if (typeof pos !== 'number') return
144
- const next = textarea.value
145
- if (next === (current.attrs.text ?? '')) return
146
- editor.view.dispatch(
147
- editor.state.tr.setNodeMarkup(pos, undefined, {
148
- ...current.attrs,
149
- text: next,
150
- }),
151
- )
152
- }
153
- textarea.addEventListener('blur', () => {
154
- commit()
155
- closePopover()
156
- })
157
- textarea.addEventListener('keydown', event => {
158
- if (event.key === 'Escape') {
159
- event.preventDefault()
160
- commit()
161
- closePopover()
162
- editor.commands.focus()
163
- }
164
- })
165
- document.addEventListener('mousedown', onDocPointerDown, true)
166
- }
167
-
168
- dom.addEventListener('mousedown', event => {
169
- event.preventDefault()
170
- openPopover()
171
- })
172
-
173
- return {
174
- dom,
175
- update: updated => {
176
- if (updated.type.name !== 'footnote') return false
177
- current = updated
178
- renderNumber()
179
- return true
180
- },
181
- ignoreMutation: () => true,
182
- destroy: closePopover,
183
- }
184
- }
185
- },
186
- })
1
+ import { Node, mergeAttributes } from '@tiptap/core'
2
+
3
+ declare module '@tiptap/core' {
4
+ interface Commands<ReturnType> {
5
+ footnote: {
6
+ /** Insert an empty footnote marker at the cursor and open it to edit. */
7
+ insertFootnote: () => ReturnType
8
+ }
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Footnote — an inline, atomic reference marker.
14
+ *
15
+ * The body shows only a small superscript number; the note text is embedded on
16
+ * the node (the `text` attribute) and edited in a popover when the marker is
17
+ * clicked. Numbering is by document order, recomputed live. Serializes to
18
+ * `<sup data-footnote data-text="…">` for a clean HTML round-trip; the export
19
+ * pipeline (prepareFootnotes) turns markers into numbered links plus a
20
+ * footnotes section.
21
+ */
22
+ export const Footnote = Node.create({
23
+ name: 'footnote',
24
+ group: 'inline',
25
+ inline: true,
26
+ atom: true,
27
+ selectable: true,
28
+
29
+ addAttributes() {
30
+ return {
31
+ text: {
32
+ default: '',
33
+ parseHTML: el => el.getAttribute('data-text') ?? '',
34
+ renderHTML: (attrs: { text?: string }) => ({
35
+ 'data-text': attrs.text ?? '',
36
+ }),
37
+ },
38
+ }
39
+ },
40
+
41
+ parseHTML() {
42
+ return [{ tag: 'sup[data-footnote]' }]
43
+ },
44
+
45
+ renderHTML({ HTMLAttributes }) {
46
+ return [
47
+ 'sup',
48
+ mergeAttributes(HTMLAttributes, {
49
+ 'data-footnote': '',
50
+ class: 'footnote-ref',
51
+ }),
52
+ ]
53
+ },
54
+
55
+ addCommands() {
56
+ return {
57
+ insertFootnote:
58
+ () =>
59
+ ({ commands }) =>
60
+ commands.insertContent({ type: this.name, attrs: { text: '' } }),
61
+ }
62
+ },
63
+
64
+ addNodeView() {
65
+ return ({ node, getPos, editor }) => {
66
+ let current = node
67
+ const dom = document.createElement('sup')
68
+ dom.className = 'footnote-ref'
69
+ dom.setAttribute('data-footnote', '')
70
+ dom.contentEditable = 'false'
71
+
72
+ const renderNumber = (): void => {
73
+ let count = 0
74
+ let mine = 0
75
+ const here = typeof getPos === 'function' ? getPos() : null
76
+ editor.state.doc.descendants((child, pos) => {
77
+ if (child.type.name === 'footnote') {
78
+ count += 1
79
+ if (here !== null && pos === here) mine = count
80
+ }
81
+ return true
82
+ })
83
+ dom.textContent = String(mine || count || 1)
84
+ }
85
+ renderNumber()
86
+
87
+ let popover: HTMLDivElement | null = null
88
+ const closePopover = (): void => {
89
+ popover?.remove()
90
+ popover = null
91
+ document.removeEventListener('mousedown', onDocPointerDown, true)
92
+ }
93
+ const onDocPointerDown = (event: MouseEvent): void => {
94
+ if (
95
+ popover &&
96
+ event.target !== dom &&
97
+ !popover.contains(event.target as globalThis.Node)
98
+ ) {
99
+ closePopover()
100
+ }
101
+ }
102
+ const openPopover = (): void => {
103
+ if (popover) return
104
+ popover = document.createElement('div')
105
+ popover.className = 'footnote-popover'
106
+ Object.assign(popover.style, {
107
+ position: 'fixed',
108
+ zIndex: '1002',
109
+ width: 'min(360px, calc(100vw - 24px))',
110
+ padding: '8px',
111
+ border: '1px solid var(--platform-colors-border, #d7dbe0)',
112
+ borderRadius: '8px',
113
+ background: 'var(--platform-colors-surface, #fff)',
114
+ boxShadow: '0 18px 42px rgb(23 29 36 / 0.18)',
115
+ })
116
+ const textarea = document.createElement('textarea')
117
+ textarea.value = current.attrs.text ?? ''
118
+ textarea.placeholder = 'Footnote text…'
119
+ textarea.rows = 3
120
+ Object.assign(textarea.style, {
121
+ width: '100%',
122
+ boxSizing: 'border-box',
123
+ resize: 'vertical',
124
+ border: '1px solid var(--platform-colors-border, #d7dbe0)',
125
+ borderRadius: '6px',
126
+ padding: '6px 8px',
127
+ font: 'inherit',
128
+ fontSize: '13px',
129
+ color: 'inherit',
130
+ background: 'var(--platform-colors-surface, #fff)',
131
+ })
132
+ popover.appendChild(textarea)
133
+
134
+ const rect = dom.getBoundingClientRect()
135
+ popover.style.left = `${Math.max(12, rect.left)}px`
136
+ popover.style.top = `${rect.bottom + 6}px`
137
+ document.body.appendChild(popover)
138
+ textarea.focus()
139
+
140
+ const commit = (): void => {
141
+ if (typeof getPos !== 'function') return
142
+ const pos = getPos()
143
+ if (typeof pos !== 'number') return
144
+ const next = textarea.value
145
+ if (next === (current.attrs.text ?? '')) return
146
+ editor.view.dispatch(
147
+ editor.state.tr.setNodeMarkup(pos, undefined, {
148
+ ...current.attrs,
149
+ text: next,
150
+ }),
151
+ )
152
+ }
153
+ textarea.addEventListener('blur', () => {
154
+ commit()
155
+ closePopover()
156
+ })
157
+ textarea.addEventListener('keydown', event => {
158
+ if (event.key === 'Escape') {
159
+ event.preventDefault()
160
+ commit()
161
+ closePopover()
162
+ editor.commands.focus()
163
+ }
164
+ })
165
+ document.addEventListener('mousedown', onDocPointerDown, true)
166
+ }
167
+
168
+ dom.addEventListener('mousedown', event => {
169
+ event.preventDefault()
170
+ openPopover()
171
+ })
172
+
173
+ return {
174
+ dom,
175
+ update: updated => {
176
+ if (updated.type.name !== 'footnote') return false
177
+ current = updated
178
+ renderNumber()
179
+ return true
180
+ },
181
+ ignoreMutation: () => true,
182
+ destroy: closePopover,
183
+ }
184
+ }
185
+ },
186
+ })