@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,26 +1,26 @@
1
- import Image from '@tiptap/extension-image'
2
- import { COLLECTION_ASSET_SRC_ATTR } from '../collectionAssetSrc.js'
3
-
4
- /**
5
- * Collection document images: data URLs in the editor, relative `assets/…` paths on save
6
- * via {@link COLLECTION_ASSET_SRC_ATTR} (see platform-ui `normalizeCollectionDocumentHtml`).
7
- */
8
- export const CollectionImage = Image.extend({
9
- addAttributes() {
10
- return {
11
- ...this.parent?.(),
12
- collectionAssetSrc: {
13
- default: null,
14
- parseHTML: element => element.getAttribute(COLLECTION_ASSET_SRC_ATTR),
15
- renderHTML: attributes => {
16
- const stored = attributes.collectionAssetSrc
17
- if (!stored || typeof stored !== 'string') return {}
18
- return { [COLLECTION_ASSET_SRC_ATTR]: stored }
19
- },
20
- },
21
- }
22
- },
23
- }).configure({
24
- inline: false,
25
- allowBase64: true,
26
- })
1
+ import Image from '@tiptap/extension-image'
2
+ import { COLLECTION_ASSET_SRC_ATTR } from '../collectionAssetSrc.js'
3
+
4
+ /**
5
+ * Collection document images: data URLs in the editor, relative `assets/…` paths on save
6
+ * via {@link COLLECTION_ASSET_SRC_ATTR} (see platform-ui `normalizeCollectionDocumentHtml`).
7
+ */
8
+ export const CollectionImage = Image.extend({
9
+ addAttributes() {
10
+ return {
11
+ ...this.parent?.(),
12
+ collectionAssetSrc: {
13
+ default: null,
14
+ parseHTML: element => element.getAttribute(COLLECTION_ASSET_SRC_ATTR),
15
+ renderHTML: attributes => {
16
+ const stored = attributes.collectionAssetSrc
17
+ if (!stored || typeof stored !== 'string') return {}
18
+ return { [COLLECTION_ASSET_SRC_ATTR]: stored }
19
+ },
20
+ },
21
+ }
22
+ },
23
+ }).configure({
24
+ inline: false,
25
+ allowBase64: true,
26
+ })
@@ -1,215 +1,215 @@
1
- import { Extension } from '@tiptap/core'
2
- import { Plugin, PluginKey } from '@tiptap/pm/state'
3
-
4
- const SUPPORTED_MIME = new Set([
5
- 'image/png',
6
- 'image/jpeg',
7
- 'image/gif',
8
- 'image/webp',
9
- 'image/svg+xml',
10
- ])
11
-
12
- const EXT_FOR_MIME: Record<string, string> = {
13
- 'image/png': 'png',
14
- 'image/jpeg': 'jpg',
15
- 'image/gif': 'gif',
16
- 'image/webp': 'webp',
17
- 'image/svg+xml': 'svg',
18
- }
19
-
20
- export interface CollectionImagePastePayload {
21
- name: string
22
- bytes: Uint8Array
23
- mimeType: string
24
- }
25
-
26
- export interface CollectionImagePasteExtensionOptions {
27
- onError?: (message: string) => void
28
- handleImage: (payload: CollectionImagePastePayload) => Promise<void>
29
- /** Read bytes from an absolute path (external file:// drops). */
30
- readBinary?: (absolutePath: string) => Promise<Uint8Array>
31
- }
32
-
33
- function normalizeMimeType(
34
- mimeType: string | undefined,
35
- name: string,
36
- ): string | undefined {
37
- const trimmed = mimeType?.trim().toLowerCase()
38
- if (trimmed && SUPPORTED_MIME.has(trimmed)) return trimmed
39
- const ext = name.split('.').pop()?.toLowerCase()
40
- if (ext === 'png') return 'image/png'
41
- if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
42
- if (ext === 'gif') return 'image/gif'
43
- if (ext === 'webp') return 'image/webp'
44
- if (ext === 'svg') return 'image/svg+xml'
45
- return undefined
46
- }
47
-
48
- function decodeDataImage(dataUrl: string): CollectionImagePastePayload {
49
- const match = dataUrl.match(/^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i)
50
- if (!match) throw new Error('Unsupported data URL format.')
51
- const [, mimeType, payload] = match
52
- const binary = atob(payload)
53
- const bytes = Uint8Array.from(binary, char => char.charCodeAt(0))
54
- return {
55
- bytes,
56
- mimeType: mimeType.toLowerCase(),
57
- name: `image.${EXT_FOR_MIME[mimeType.toLowerCase()] ?? 'png'}`,
58
- }
59
- }
60
-
61
- function isSupportedImageFile(file: File): boolean {
62
- return Boolean(normalizeMimeType(file.type, file.name))
63
- }
64
-
65
- function collectImageFiles(dt: DataTransfer): File[] {
66
- const seen = new Set<File>()
67
- const add = (file: File | null) => {
68
- if (file && isSupportedImageFile(file)) seen.add(file)
69
- }
70
- for (const file of Array.from(dt.files)) add(file)
71
- for (const item of Array.from(dt.items)) {
72
- if (item.kind === 'file') add(item.getAsFile())
73
- }
74
- return [...seen]
75
- }
76
-
77
- function filePathFromDataTransfer(dt: DataTransfer): string | null {
78
- const uri =
79
- dt
80
- .getData('text/uri-list')
81
- ?.split('\n')
82
- .map(line => line.trim())
83
- .find(line => line && !line.startsWith('#')) ?? null
84
- if (!uri) return null
85
- try {
86
- if (uri.startsWith('file://')) {
87
- return decodeURIComponent(new URL(uri).pathname)
88
- }
89
- if (uri.startsWith('/')) return uri
90
- } catch {
91
- return null
92
- }
93
- return null
94
- }
95
-
96
- function nameFromLocalPath(filePath: string): string {
97
- const normalized = filePath.replace(/\\/g, '/')
98
- return normalized.split('/').pop() || 'image.png'
99
- }
100
-
101
- /** TipTap paste/drop hook — persistence is injected via `handleImage`. */
102
- export function createCollectionImagePasteExtension(
103
- options: CollectionImagePasteExtensionOptions,
104
- ): Extension {
105
- const { onError, handleImage, readBinary } = options
106
-
107
- return Extension.create({
108
- name: 'collectionImagePaste',
109
-
110
- addProseMirrorPlugins() {
111
- const handleFiles = async (files: File[]): Promise<boolean> => {
112
- const images = files.filter(isSupportedImageFile)
113
- if (images.length === 0) {
114
- if (files.length > 0) {
115
- onError?.(
116
- 'Unsupported file type. Supported: PNG, JPEG, GIF, WebP, SVG.',
117
- )
118
- }
119
- return false
120
- }
121
- for (const file of images) {
122
- try {
123
- await handleImage({
124
- name: file.name,
125
- bytes: new Uint8Array(await file.arrayBuffer()),
126
- mimeType: normalizeMimeType(file.type, file.name) ?? 'image/png',
127
- })
128
- } catch (error) {
129
- const message =
130
- error instanceof Error ? error.message : String(error)
131
- onError?.(`Could not save image: ${message}`)
132
- }
133
- }
134
- return true
135
- }
136
-
137
- const handleExternalFilePath = async (
138
- absolutePath: string,
139
- ): Promise<boolean> => {
140
- if (!readBinary) return false
141
- const lower = absolutePath.toLowerCase()
142
- if (!/\.(png|jpe?g|gif|webp|svg)$/.test(lower)) return false
143
- try {
144
- const bytes = await readBinary(absolutePath)
145
- await handleImage({
146
- name: nameFromLocalPath(absolutePath),
147
- bytes,
148
- mimeType: normalizeMimeType(undefined, absolutePath) ?? 'image/png',
149
- })
150
- return true
151
- } catch (error) {
152
- const message = error instanceof Error ? error.message : String(error)
153
- onError?.(`Could not import image: ${message}`)
154
- return false
155
- }
156
- }
157
-
158
- return [
159
- new Plugin({
160
- key: new PluginKey('collectionImagePaste'),
161
- props: {
162
- handlePaste(_view, event) {
163
- const dt = event.clipboardData
164
- if (!dt) return false
165
-
166
- const files = collectImageFiles(dt)
167
- if (files.length > 0) {
168
- event.preventDefault()
169
- void handleFiles(files)
170
- return true
171
- }
172
-
173
- const text = dt.getData('text/plain')?.trim()
174
- if (text?.startsWith('data:image/')) {
175
- event.preventDefault()
176
- void (async () => {
177
- try {
178
- await handleImage(decodeDataImage(text))
179
- } catch (error) {
180
- const message =
181
- error instanceof Error ? error.message : String(error)
182
- onError?.(`Could not save image: ${message}`)
183
- }
184
- })()
185
- return true
186
- }
187
-
188
- return false
189
- },
190
- handleDrop(_view, event) {
191
- const dt = event.dataTransfer
192
- if (!dt) return false
193
-
194
- const files = collectImageFiles(dt)
195
- if (files.length > 0) {
196
- event.preventDefault()
197
- void handleFiles(files)
198
- return true
199
- }
200
-
201
- const externalPath = filePathFromDataTransfer(dt)
202
- if (externalPath) {
203
- event.preventDefault()
204
- void handleExternalFilePath(externalPath)
205
- return true
206
- }
207
-
208
- return false
209
- },
210
- },
211
- }),
212
- ]
213
- },
214
- })
215
- }
1
+ import { Extension } from '@tiptap/core'
2
+ import { Plugin, PluginKey } from '@tiptap/pm/state'
3
+
4
+ const SUPPORTED_MIME = new Set([
5
+ 'image/png',
6
+ 'image/jpeg',
7
+ 'image/gif',
8
+ 'image/webp',
9
+ 'image/svg+xml',
10
+ ])
11
+
12
+ const EXT_FOR_MIME: Record<string, string> = {
13
+ 'image/png': 'png',
14
+ 'image/jpeg': 'jpg',
15
+ 'image/gif': 'gif',
16
+ 'image/webp': 'webp',
17
+ 'image/svg+xml': 'svg',
18
+ }
19
+
20
+ export interface CollectionImagePastePayload {
21
+ name: string
22
+ bytes: Uint8Array
23
+ mimeType: string
24
+ }
25
+
26
+ export interface CollectionImagePasteExtensionOptions {
27
+ onError?: (message: string) => void
28
+ handleImage: (payload: CollectionImagePastePayload) => Promise<void>
29
+ /** Read bytes from an absolute path (external file:// drops). */
30
+ readBinary?: (absolutePath: string) => Promise<Uint8Array>
31
+ }
32
+
33
+ function normalizeMimeType(
34
+ mimeType: string | undefined,
35
+ name: string,
36
+ ): string | undefined {
37
+ const trimmed = mimeType?.trim().toLowerCase()
38
+ if (trimmed && SUPPORTED_MIME.has(trimmed)) return trimmed
39
+ const ext = name.split('.').pop()?.toLowerCase()
40
+ if (ext === 'png') return 'image/png'
41
+ if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
42
+ if (ext === 'gif') return 'image/gif'
43
+ if (ext === 'webp') return 'image/webp'
44
+ if (ext === 'svg') return 'image/svg+xml'
45
+ return undefined
46
+ }
47
+
48
+ function decodeDataImage(dataUrl: string): CollectionImagePastePayload {
49
+ const match = dataUrl.match(/^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i)
50
+ if (!match) throw new Error('Unsupported data URL format.')
51
+ const [, mimeType, payload] = match
52
+ const binary = atob(payload)
53
+ const bytes = Uint8Array.from(binary, char => char.charCodeAt(0))
54
+ return {
55
+ bytes,
56
+ mimeType: mimeType.toLowerCase(),
57
+ name: `image.${EXT_FOR_MIME[mimeType.toLowerCase()] ?? 'png'}`,
58
+ }
59
+ }
60
+
61
+ function isSupportedImageFile(file: File): boolean {
62
+ return Boolean(normalizeMimeType(file.type, file.name))
63
+ }
64
+
65
+ function collectImageFiles(dt: DataTransfer): File[] {
66
+ const seen = new Set<File>()
67
+ const add = (file: File | null) => {
68
+ if (file && isSupportedImageFile(file)) seen.add(file)
69
+ }
70
+ for (const file of Array.from(dt.files)) add(file)
71
+ for (const item of Array.from(dt.items)) {
72
+ if (item.kind === 'file') add(item.getAsFile())
73
+ }
74
+ return [...seen]
75
+ }
76
+
77
+ function filePathFromDataTransfer(dt: DataTransfer): string | null {
78
+ const uri =
79
+ dt
80
+ .getData('text/uri-list')
81
+ ?.split('\n')
82
+ .map(line => line.trim())
83
+ .find(line => line && !line.startsWith('#')) ?? null
84
+ if (!uri) return null
85
+ try {
86
+ if (uri.startsWith('file://')) {
87
+ return decodeURIComponent(new URL(uri).pathname)
88
+ }
89
+ if (uri.startsWith('/')) return uri
90
+ } catch {
91
+ return null
92
+ }
93
+ return null
94
+ }
95
+
96
+ function nameFromLocalPath(filePath: string): string {
97
+ const normalized = filePath.replace(/\\/g, '/')
98
+ return normalized.split('/').pop() || 'image.png'
99
+ }
100
+
101
+ /** TipTap paste/drop hook — persistence is injected via `handleImage`. */
102
+ export function createCollectionImagePasteExtension(
103
+ options: CollectionImagePasteExtensionOptions,
104
+ ): Extension {
105
+ const { onError, handleImage, readBinary } = options
106
+
107
+ return Extension.create({
108
+ name: 'collectionImagePaste',
109
+
110
+ addProseMirrorPlugins() {
111
+ const handleFiles = async (files: File[]): Promise<boolean> => {
112
+ const images = files.filter(isSupportedImageFile)
113
+ if (images.length === 0) {
114
+ if (files.length > 0) {
115
+ onError?.(
116
+ 'Unsupported file type. Supported: PNG, JPEG, GIF, WebP, SVG.',
117
+ )
118
+ }
119
+ return false
120
+ }
121
+ for (const file of images) {
122
+ try {
123
+ await handleImage({
124
+ name: file.name,
125
+ bytes: new Uint8Array(await file.arrayBuffer()),
126
+ mimeType: normalizeMimeType(file.type, file.name) ?? 'image/png',
127
+ })
128
+ } catch (error) {
129
+ const message =
130
+ error instanceof Error ? error.message : String(error)
131
+ onError?.(`Could not save image: ${message}`)
132
+ }
133
+ }
134
+ return true
135
+ }
136
+
137
+ const handleExternalFilePath = async (
138
+ absolutePath: string,
139
+ ): Promise<boolean> => {
140
+ if (!readBinary) return false
141
+ const lower = absolutePath.toLowerCase()
142
+ if (!/\.(png|jpe?g|gif|webp|svg)$/.test(lower)) return false
143
+ try {
144
+ const bytes = await readBinary(absolutePath)
145
+ await handleImage({
146
+ name: nameFromLocalPath(absolutePath),
147
+ bytes,
148
+ mimeType: normalizeMimeType(undefined, absolutePath) ?? 'image/png',
149
+ })
150
+ return true
151
+ } catch (error) {
152
+ const message = error instanceof Error ? error.message : String(error)
153
+ onError?.(`Could not import image: ${message}`)
154
+ return false
155
+ }
156
+ }
157
+
158
+ return [
159
+ new Plugin({
160
+ key: new PluginKey('collectionImagePaste'),
161
+ props: {
162
+ handlePaste(_view, event) {
163
+ const dt = event.clipboardData
164
+ if (!dt) return false
165
+
166
+ const files = collectImageFiles(dt)
167
+ if (files.length > 0) {
168
+ event.preventDefault()
169
+ void handleFiles(files)
170
+ return true
171
+ }
172
+
173
+ const text = dt.getData('text/plain')?.trim()
174
+ if (text?.startsWith('data:image/')) {
175
+ event.preventDefault()
176
+ void (async () => {
177
+ try {
178
+ await handleImage(decodeDataImage(text))
179
+ } catch (error) {
180
+ const message =
181
+ error instanceof Error ? error.message : String(error)
182
+ onError?.(`Could not save image: ${message}`)
183
+ }
184
+ })()
185
+ return true
186
+ }
187
+
188
+ return false
189
+ },
190
+ handleDrop(_view, event) {
191
+ const dt = event.dataTransfer
192
+ if (!dt) return false
193
+
194
+ const files = collectImageFiles(dt)
195
+ if (files.length > 0) {
196
+ event.preventDefault()
197
+ void handleFiles(files)
198
+ return true
199
+ }
200
+
201
+ const externalPath = filePathFromDataTransfer(dt)
202
+ if (externalPath) {
203
+ event.preventDefault()
204
+ void handleExternalFilePath(externalPath)
205
+ return true
206
+ }
207
+
208
+ return false
209
+ },
210
+ },
211
+ }),
212
+ ]
213
+ },
214
+ })
215
+ }