@growgroup/visual-editor 0.1.0

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 (219) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +52 -0
  3. package/README.md +143 -0
  4. package/dist/editor.css +2 -0
  5. package/package.json +94 -0
  6. package/src/components/DeckSlideRender.tsx +21 -0
  7. package/src/components/SaveNote.tsx +21 -0
  8. package/src/components/auth/AuthProvider.tsx +44 -0
  9. package/src/components/ui/accordion.tsx +57 -0
  10. package/src/components/ui/alert-dialog.tsx +141 -0
  11. package/src/components/ui/badge.tsx +36 -0
  12. package/src/components/ui/button.tsx +56 -0
  13. package/src/components/ui/card.tsx +76 -0
  14. package/src/components/ui/checkbox.tsx +30 -0
  15. package/src/components/ui/collapsible.tsx +11 -0
  16. package/src/components/ui/context-menu.tsx +200 -0
  17. package/src/components/ui/dialog.tsx +122 -0
  18. package/src/components/ui/dropdown-menu.tsx +201 -0
  19. package/src/components/ui/input.tsx +22 -0
  20. package/src/components/ui/label.tsx +26 -0
  21. package/src/components/ui/popover.tsx +33 -0
  22. package/src/components/ui/scroll-area.tsx +48 -0
  23. package/src/components/ui/scrubbable-label.tsx +95 -0
  24. package/src/components/ui/select.tsx +159 -0
  25. package/src/components/ui/separator.tsx +31 -0
  26. package/src/components/ui/slider.tsx +28 -0
  27. package/src/components/ui/switch.tsx +29 -0
  28. package/src/components/ui/tabs.tsx +55 -0
  29. package/src/components/ui/textarea.tsx +22 -0
  30. package/src/components/ui/tooltip.tsx +32 -0
  31. package/src/components/viewer/useDeck.ts +58 -0
  32. package/src/editor/EditorContext.tsx +632 -0
  33. package/src/editor/EditorToolbar.tsx +446 -0
  34. package/src/editor/FrontendVisualEditor.tsx +2595 -0
  35. package/src/editor/autosave.ts +34 -0
  36. package/src/editor/components/AiPromptPopover.tsx +378 -0
  37. package/src/editor/components/BreakpointGuides.tsx +323 -0
  38. package/src/editor/components/BreakpointSelector.tsx +180 -0
  39. package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
  40. package/src/editor/components/CssEditorDialog.tsx +168 -0
  41. package/src/editor/components/EditorCanvas.tsx +987 -0
  42. package/src/editor/components/EditorContextMenu.tsx +515 -0
  43. package/src/editor/components/EditorFooter.tsx +81 -0
  44. package/src/editor/components/EditorHeader.tsx +524 -0
  45. package/src/editor/components/EditorLayerPanel.tsx +1197 -0
  46. package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
  47. package/src/editor/components/EditorSidebar.tsx +226 -0
  48. package/src/editor/components/FigmaColorPicker.tsx +1577 -0
  49. package/src/editor/components/HtmlEditorDialog.tsx +71 -0
  50. package/src/editor/components/HtmlImportDialog.tsx +346 -0
  51. package/src/editor/components/JsEditorDialog.tsx +175 -0
  52. package/src/editor/components/LayerTreeItem.tsx +494 -0
  53. package/src/editor/components/MasterComponentEditor.tsx +846 -0
  54. package/src/editor/components/MediaLibraryDialog.tsx +447 -0
  55. package/src/editor/components/PageSettingsDialog.tsx +776 -0
  56. package/src/editor/components/VariablePicker.tsx +355 -0
  57. package/src/editor/components/VariablesDialog.tsx +564 -0
  58. package/src/editor/components/VariantEditor.tsx +687 -0
  59. package/src/editor/components/VariantMatrix.tsx +507 -0
  60. package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
  61. package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
  62. package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
  63. package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
  64. package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
  65. package/src/editor/components/component-panel/VariantList.tsx +260 -0
  66. package/src/editor/components/component-panel/index.ts +12 -0
  67. package/src/editor/components/index.ts +48 -0
  68. package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
  69. package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
  70. package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
  71. package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
  72. package/src/editor/components/multi-page/PageLabel.tsx +34 -0
  73. package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
  74. package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
  75. package/src/editor/components/multi-page/index.ts +7 -0
  76. package/src/editor/components/ppt/PptChrome.tsx +2508 -0
  77. package/src/editor/components/ppt/PptComments.tsx +589 -0
  78. package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
  79. package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
  80. package/src/editor/components/ppt/PptNotes.tsx +246 -0
  81. package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
  82. package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
  83. package/src/editor/components/property-panel/BorderSection.tsx +104 -0
  84. package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
  85. package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
  86. package/src/editor/components/property-panel/EffectSection.tsx +157 -0
  87. package/src/editor/components/property-panel/FillSection.tsx +69 -0
  88. package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
  89. package/src/editor/components/property-panel/ImageSection.tsx +206 -0
  90. package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
  91. package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
  92. package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
  93. package/src/editor/components/property-panel/LinkSection.tsx +103 -0
  94. package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
  95. package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
  96. package/src/editor/components/property-panel/TypographySection.tsx +230 -0
  97. package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
  98. package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
  99. package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
  100. package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
  101. package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
  102. package/src/editor/components/property-panel/index.ts +24 -0
  103. package/src/editor/components/property-panel/unit-utils.ts +261 -0
  104. package/src/editor/components/shell/EditorTopBar.tsx +392 -0
  105. package/src/editor/components/shell/LeftPanel.tsx +47 -0
  106. package/src/editor/components/shell/PagesPanel.tsx +118 -0
  107. package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
  108. package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
  109. package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
  110. package/src/editor/components/variables-panel/index.ts +7 -0
  111. package/src/editor/constants.ts +468 -0
  112. package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
  113. package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
  114. package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
  115. package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
  116. package/src/editor/contexts/EditorRefsContext.tsx +204 -0
  117. package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
  118. package/src/editor/contexts/EditorToolContext.tsx +58 -0
  119. package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
  120. package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
  121. package/src/editor/contexts/EditorViewContext.tsx +101 -0
  122. package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
  123. package/src/editor/contexts/index.ts +17 -0
  124. package/src/editor/editor-skin.css +122 -0
  125. package/src/editor/hooks/index.ts +38 -0
  126. package/src/editor/hooks/useAiReplace.ts +397 -0
  127. package/src/editor/hooks/useAltMeasure.ts +67 -0
  128. package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
  129. package/src/editor/hooks/useCanvasControls.ts +1222 -0
  130. package/src/editor/hooks/useComponentEditMode.ts +658 -0
  131. package/src/editor/hooks/useComponentInstances.ts +515 -0
  132. package/src/editor/hooks/useContextMenuHandler.ts +157 -0
  133. package/src/editor/hooks/useCoordinateTransform.ts +207 -0
  134. package/src/editor/hooks/useDragResize.ts +1317 -0
  135. package/src/editor/hooks/useDrawingMode.ts +472 -0
  136. package/src/editor/hooks/useEditorColors.ts +144 -0
  137. package/src/editor/hooks/useEditorMessages.ts +108 -0
  138. package/src/editor/hooks/useElementActions.ts +1754 -0
  139. package/src/editor/hooks/useElementSelection.ts +1573 -0
  140. package/src/editor/hooks/useFocusManagement.ts +249 -0
  141. package/src/editor/hooks/useGoogleFonts.ts +305 -0
  142. package/src/editor/hooks/useIframeInitializer.ts +584 -0
  143. package/src/editor/hooks/useIframeSetup.ts +360 -0
  144. package/src/editor/hooks/useImageUpload.ts +270 -0
  145. package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
  146. package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
  147. package/src/editor/hooks/useMarqueeSelection.ts +388 -0
  148. package/src/editor/hooks/useMediaLibrary.ts +129 -0
  149. package/src/editor/hooks/usePageSettingsManager.ts +739 -0
  150. package/src/editor/hooks/useResizablePanel.ts +163 -0
  151. package/src/editor/hooks/useRichPaste.ts +255 -0
  152. package/src/editor/hooks/useTouchGestures.ts +197 -0
  153. package/src/editor/index.ts +27 -0
  154. package/src/editor/types/page-settings.ts +513 -0
  155. package/src/editor/types.ts +382 -0
  156. package/src/editor/utils/align-elements.ts +169 -0
  157. package/src/editor/utils/api-json.ts +21 -0
  158. package/src/editor/utils/aspect-lock.ts +50 -0
  159. package/src/editor/utils/component-renderer.ts +702 -0
  160. package/src/editor/utils/component-sync.ts +845 -0
  161. package/src/editor/utils/crop-mode.ts +265 -0
  162. package/src/editor/utils/dom-utils.ts +2238 -0
  163. package/src/editor/utils/element-effects.ts +218 -0
  164. package/src/editor/utils/eyedropper.ts +35 -0
  165. package/src/editor/utils/figma-export.ts +560 -0
  166. package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
  167. package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
  168. package/src/editor/utils/figma-paste.ts +2262 -0
  169. package/src/editor/utils/flex-reorder.ts +231 -0
  170. package/src/editor/utils/flex-utils.ts +1230 -0
  171. package/src/editor/utils/geometry.ts +232 -0
  172. package/src/editor/utils/grid-layout.ts +210 -0
  173. package/src/editor/utils/html-utils.ts +470 -0
  174. package/src/editor/utils/index.ts +11 -0
  175. package/src/editor/utils/ink-style.ts +57 -0
  176. package/src/editor/utils/inline-format.ts +236 -0
  177. package/src/editor/utils/measure-distance.ts +120 -0
  178. package/src/editor/utils/override-apply.ts +526 -0
  179. package/src/editor/utils/override-detection.ts +613 -0
  180. package/src/editor/utils/paste-processors.ts +609 -0
  181. package/src/editor/utils/paste-sanitizer.ts +244 -0
  182. package/src/editor/utils/restore-flow.ts +188 -0
  183. package/src/editor/utils/shape-library.ts +171 -0
  184. package/src/editor/utils/slide-root.ts +29 -0
  185. package/src/editor/utils/smart-guides.ts +218 -0
  186. package/src/editor/utils/style-utils.ts +685 -0
  187. package/src/editor/utils/table-edit.ts +151 -0
  188. package/src/editor/utils/tailwind-mappings.ts +778 -0
  189. package/src/editor/utils/tailwind-utils.ts +804 -0
  190. package/src/editor/utils/text-highlight.ts +216 -0
  191. package/src/editor/utils/theme-to-variables.ts +341 -0
  192. package/src/editor/utils/variant-resolver.ts +456 -0
  193. package/src/editor/utils/viewport-utils.ts +295 -0
  194. package/src/hooks/useEditorHistory.ts +195 -0
  195. package/src/hooks/useEditorShortcuts.ts +260 -0
  196. package/src/index.ts +40 -0
  197. package/src/io.ts +134 -0
  198. package/src/lib/agent/slide-agent/types.ts +547 -0
  199. package/src/lib/agent/website-agent/types.ts +805 -0
  200. package/src/lib/api/auth-fetch.ts +58 -0
  201. package/src/lib/deck.ts +69 -0
  202. package/src/lib/export.ts +47 -0
  203. package/src/lib/firebase/config.ts +29 -0
  204. package/src/lib/firebase/css-variables.ts +378 -0
  205. package/src/lib/firebase/editor-components.ts +677 -0
  206. package/src/lib/firebase/storage.ts +469 -0
  207. package/src/lib/utils.ts +6 -0
  208. package/src/styles/editor.css +52 -0
  209. package/src/styles/skin.css +122 -0
  210. package/src/types/css-variables.ts +645 -0
  211. package/src/types/editor-components.ts +944 -0
  212. package/src/types/editor.ts +420 -0
  213. package/src/types/page-master.ts +197 -0
  214. package/src/types/page.ts +419 -0
  215. package/src/types/slide.ts +402 -0
  216. package/src/types/website-theme.ts +788 -0
  217. package/src/types/website.ts +374 -0
  218. package/src/vendor/firebase-firestore.ts +13 -0
  219. package/src/vendor/firebase-functions.ts +17 -0
@@ -0,0 +1,609 @@
1
+ /**
2
+ * paste-processors.ts
3
+ *
4
+ * Content type detection and format-specific processors for rich clipboard paste.
5
+ * Handles: Browser HTML, Excel, Word, SVG, and plain text.
6
+ */
7
+
8
+ import { generateElementId } from './dom-utils';
9
+ import {
10
+ sanitizePastedHtml,
11
+ cleanOfficeMarkup,
12
+ cleanExcelMarkup,
13
+ detectDangerousContent,
14
+ } from './paste-sanitizer';
15
+ import { isFigmaContent, processFigmaPaste, type FigmaPasteProcessResult } from './figma-paste';
16
+
17
+ /**
18
+ * Types of content that can be detected in clipboard
19
+ */
20
+ export type ClipboardContentType =
21
+ | 'image'
22
+ | 'html-figma'
23
+ | 'html-excel'
24
+ | 'html-word'
25
+ | 'html-browser'
26
+ | 'svg'
27
+ | 'plain-text'
28
+ | 'unknown';
29
+
30
+ /**
31
+ * Result of a paste processing operation
32
+ */
33
+ export interface PasteProcessResult {
34
+ success: boolean;
35
+ elements: HTMLElement[];
36
+ contentType: ClipboardContentType;
37
+ error?: string;
38
+ /** When true, caller should fallback to image paste handling */
39
+ shouldFallbackToImage?: boolean;
40
+ /** Figma file key for fetching images (only for html-figma type) */
41
+ figmaFileKey?: string;
42
+ /** Image hashes that need to be fetched from Figma API */
43
+ figmaImageHashes?: string[];
44
+ /** Node IDs for fallback rendering when hashes can't be extracted */
45
+ figmaImageNodeIds?: string[];
46
+ }
47
+
48
+ /**
49
+ * Detect the type of content in clipboard data.
50
+ *
51
+ * Priority:
52
+ * 1. HTML content from Office apps (Excel, Word) - these also include images, but HTML is preferred
53
+ * 2. Images (screenshots, pure image paste)
54
+ * 3. Other HTML content (Browser, SVG)
55
+ * 4. Plain text
56
+ *
57
+ * @param clipboardData - DataTransfer object from paste event
58
+ * @returns Detected content type
59
+ */
60
+ export function detectContentType(clipboardData: DataTransfer): ClipboardContentType {
61
+ const types = Array.from(clipboardData.types);
62
+ const items = Array.from(clipboardData.items);
63
+
64
+ // Check for HTML content FIRST
65
+ // Various apps (Figma, Excel, Word) include both HTML and image data,
66
+ // but we want to use the HTML for better fidelity
67
+ if (types.includes('text/html')) {
68
+ const html = clipboardData.getData('text/html');
69
+
70
+ // Detect Figma - highest priority (contains kiwi-encoded design data)
71
+ if (isFigmaContent(html)) {
72
+ return 'html-figma';
73
+ }
74
+
75
+ // Detect Excel - prioritize over image
76
+ if (isExcelContent(html)) {
77
+ return 'html-excel';
78
+ }
79
+
80
+ // Detect Word - prioritize over image
81
+ if (isWordContent(html)) {
82
+ return 'html-word';
83
+ }
84
+
85
+ // For non-Office HTML, check if it's a pure image paste
86
+ // (browsers sometimes wrap pasted images in HTML)
87
+ const hasImage = items.some(item => item.type.startsWith('image/'));
88
+ if (hasImage && isPureImageHtml(html)) {
89
+ return 'image';
90
+ }
91
+
92
+ // Detect standalone SVG (not SVG embedded in HTML document)
93
+ if (isSvgContent(html)) {
94
+ return 'svg';
95
+ }
96
+
97
+ // Generic browser HTML
98
+ return 'html-browser';
99
+ }
100
+
101
+ // Check for images (no HTML available)
102
+ const hasImage = items.some(item => item.type.startsWith('image/'));
103
+ if (hasImage) {
104
+ return 'image';
105
+ }
106
+
107
+ // Plain text fallback
108
+ if (types.includes('text/plain')) {
109
+ const text = clipboardData.getData('text/plain');
110
+ // Check if plain text is pure SVG code
111
+ if (isPureSvgText(text)) {
112
+ return 'svg';
113
+ }
114
+ return 'plain-text';
115
+ }
116
+
117
+ return 'unknown';
118
+ }
119
+
120
+ /**
121
+ * Check if HTML content is from Microsoft Excel
122
+ */
123
+ function isExcelContent(html: string): boolean {
124
+ return (
125
+ html.includes('xmlns:x="urn:schemas-microsoft-com:office:excel"') ||
126
+ html.includes('urn:schemas-microsoft-com:office:excel') ||
127
+ html.includes('mso-number-format') ||
128
+ (html.includes('<table') && html.includes('x:num'))
129
+ );
130
+ }
131
+
132
+ /**
133
+ * Check if HTML content is from Microsoft Word
134
+ */
135
+ function isWordContent(html: string): boolean {
136
+ return (
137
+ html.includes('xmlns:w="urn:schemas-microsoft-com:office:word"') ||
138
+ html.includes('urn:schemas-microsoft-com:office:word') ||
139
+ html.includes('<o:p>') ||
140
+ html.includes('class="Mso') ||
141
+ html.includes('mso-bidi-font-family') ||
142
+ html.includes('mso-fareast-font-family')
143
+ );
144
+ }
145
+
146
+ /**
147
+ * Check if HTML is just a wrapper for an image (browser's image paste)
148
+ */
149
+ function isPureImageHtml(html: string): boolean {
150
+ // Browser wraps pasted images in minimal HTML
151
+ // Check if the HTML contains only an img tag with no other significant content
152
+ const imgMatch = html.match(/<img[^>]*>/gi);
153
+ if (!imgMatch) return false;
154
+
155
+ // Remove img tags and check if there's any other meaningful content
156
+ const withoutImg = html
157
+ .replace(/<img[^>]*>/gi, '')
158
+ .replace(/<\/?html[^>]*>/gi, '')
159
+ .replace(/<\/?head[^>]*>/gi, '')
160
+ .replace(/<\/?body[^>]*>/gi, '')
161
+ .replace(/<\/?meta[^>]*>/gi, '')
162
+ .replace(/<!--[\s\S]*?-->/g, '')
163
+ .trim();
164
+
165
+ // If only whitespace remains, it's a pure image paste
166
+ return withoutImg.length < 50; // Allow some minimal wrapper content
167
+ }
168
+
169
+ /**
170
+ * Check if plain text is pure SVG code
171
+ */
172
+ function isPureSvgText(text: string): boolean {
173
+ const trimmed = text.trim();
174
+ // Must start with <svg or <?xml followed by <svg
175
+ return (
176
+ trimmed.startsWith('<svg') ||
177
+ (trimmed.startsWith('<?xml') && trimmed.includes('<svg'))
178
+ );
179
+ }
180
+
181
+ /**
182
+ * Check if content is standalone SVG (not SVG embedded in HTML document)
183
+ */
184
+ function isSvgContent(html: string): boolean {
185
+ const trimmed = html.trim();
186
+
187
+ // Must start with <svg tag directly (standalone SVG)
188
+ if (trimmed.startsWith('<svg')) {
189
+ return true;
190
+ }
191
+
192
+ // Or XML declaration followed by SVG
193
+ if (trimmed.startsWith('<?xml') && trimmed.includes('<svg')) {
194
+ // Make sure it's not a full HTML document with embedded SVG
195
+ if (!trimmed.includes('<html') && !trimmed.includes('<body') && !trimmed.includes('<div')) {
196
+ return true;
197
+ }
198
+ }
199
+
200
+ // NOT SVG if:
201
+ // - Contains HTML document structure (even without <body>, browsers may add wrapper elements)
202
+ // - Contains multiple top-level elements
203
+ // - Has HTML-like structure around the SVG
204
+
205
+ // Check for HTML wrapper patterns that browsers add
206
+ const hasHtmlWrapper =
207
+ trimmed.includes('<html') ||
208
+ trimmed.includes('<!DOCTYPE') ||
209
+ trimmed.includes('<meta') ||
210
+ trimmed.includes('<span') ||
211
+ trimmed.includes('<div') ||
212
+ trimmed.includes('<p>') ||
213
+ trimmed.includes('<body');
214
+
215
+ if (hasHtmlWrapper) {
216
+ return false;
217
+ }
218
+
219
+ return false;
220
+ }
221
+
222
+ /**
223
+ * Add editor-specific attributes to an element and its children.
224
+ *
225
+ * @param element - Element to process
226
+ * @param prefix - Prefix for element IDs
227
+ */
228
+ function addEditorAttributes(element: HTMLElement, prefix: string = 'paste'): void {
229
+ element.setAttribute('data-editable', 'true');
230
+ element.setAttribute('data-element-id', generateElementId(prefix));
231
+
232
+ // Process children recursively (for nested elements)
233
+ const children = element.querySelectorAll('*');
234
+ children.forEach((child) => {
235
+ if (child instanceof HTMLElement) {
236
+ // Only add data-editable to direct block-level children
237
+ // to avoid making every span/inline element separately selectable
238
+ const display = window.getComputedStyle(child).display;
239
+ if (display === 'block' || display === 'flex' || display === 'grid' || display === 'table') {
240
+ child.setAttribute('data-editable', 'true');
241
+ child.setAttribute('data-element-id', generateElementId(prefix));
242
+ }
243
+ }
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Process browser HTML content.
249
+ * Preserves inline styles that Chrome automatically adds from computed styles.
250
+ *
251
+ * @param html - HTML string from clipboard
252
+ * @param doc - Document to create elements in (iframe document)
253
+ * @returns Array of processed HTMLElements
254
+ */
255
+ export function processBrowserHtml(html: string, doc: Document): HTMLElement[] {
256
+ // Log any dangerous content for debugging
257
+ const dangers = detectDangerousContent(html);
258
+ if (dangers.dangerousPatterns.length > 0) {
259
+ console.warn('[paste-processors] Dangerous content detected:', dangers.dangerousPatterns);
260
+ }
261
+
262
+ // Sanitize while preserving styles
263
+ const sanitized = sanitizePastedHtml(html);
264
+
265
+ // Parse the sanitized HTML
266
+ const parser = new DOMParser();
267
+ const parsedDoc = parser.parseFromString(sanitized, 'text/html');
268
+
269
+ // Extract elements from body
270
+ const elements: HTMLElement[] = [];
271
+ const bodyChildren = Array.from(parsedDoc.body.children);
272
+
273
+ bodyChildren.forEach((child) => {
274
+ if (child instanceof HTMLElement) {
275
+ // Clone to the target document
276
+ const cloned = doc.importNode(child, true) as HTMLElement;
277
+ addEditorAttributes(cloned, 'paste');
278
+ elements.push(cloned);
279
+ }
280
+ });
281
+
282
+ // If no block elements found, wrap text content in a div
283
+ if (elements.length === 0 && parsedDoc.body.textContent?.trim()) {
284
+ const wrapper = doc.createElement('div');
285
+ wrapper.innerHTML = sanitized;
286
+ addEditorAttributes(wrapper, 'paste');
287
+ elements.push(wrapper);
288
+ }
289
+
290
+ return elements;
291
+ }
292
+
293
+ /**
294
+ * Process Excel content.
295
+ * Preserves table structure and cell styles while removing Excel-specific markup.
296
+ *
297
+ * @param html - HTML string from Excel clipboard
298
+ * @param doc - Document to create elements in
299
+ * @returns Array of processed HTMLElements
300
+ */
301
+ export function processExcelContent(html: string, doc: Document): HTMLElement[] {
302
+ // Clean Excel-specific markup
303
+ let cleaned = cleanExcelMarkup(html);
304
+
305
+ // Sanitize
306
+ cleaned = sanitizePastedHtml(cleaned);
307
+
308
+ // Parse
309
+ const parser = new DOMParser();
310
+ const parsedDoc = parser.parseFromString(cleaned, 'text/html');
311
+
312
+ const elements: HTMLElement[] = [];
313
+
314
+ // Find tables (Excel primarily exports as tables)
315
+ const tables = parsedDoc.querySelectorAll('table');
316
+
317
+ if (tables.length > 0) {
318
+ tables.forEach((table) => {
319
+ const cloned = doc.importNode(table, true) as HTMLTableElement;
320
+
321
+ // Add default styling if no styles present
322
+ if (!cloned.style.borderCollapse) {
323
+ cloned.style.borderCollapse = 'collapse';
324
+ }
325
+
326
+ // Add borders to cells if not styled
327
+ const cells = cloned.querySelectorAll('td, th');
328
+ cells.forEach((cell) => {
329
+ const cellEl = cell as HTMLElement;
330
+ if (!cellEl.style.border && !cellEl.style.borderWidth) {
331
+ cellEl.style.border = '1px solid #d1d5db';
332
+ cellEl.style.padding = '8px 12px';
333
+ }
334
+ });
335
+
336
+ addEditorAttributes(cloned, 'excel');
337
+ elements.push(cloned);
338
+ });
339
+ } else {
340
+ // Fallback to browser HTML processing if no tables found
341
+ return processBrowserHtml(cleaned, doc);
342
+ }
343
+
344
+ return elements;
345
+ }
346
+
347
+ /**
348
+ * Process Word content.
349
+ * Preserves text formatting (bold, italic, colors) while removing Word-specific markup.
350
+ *
351
+ * @param html - HTML string from Word clipboard
352
+ * @param doc - Document to create elements in
353
+ * @returns Array of processed HTMLElements
354
+ */
355
+ export function processWordContent(html: string, doc: Document): HTMLElement[] {
356
+ // Clean Word-specific markup
357
+ let cleaned = cleanOfficeMarkup(html);
358
+
359
+ // Sanitize
360
+ cleaned = sanitizePastedHtml(cleaned);
361
+
362
+ // Parse
363
+ const parser = new DOMParser();
364
+ const parsedDoc = parser.parseFromString(cleaned, 'text/html');
365
+
366
+ const elements: HTMLElement[] = [];
367
+ const bodyChildren = Array.from(parsedDoc.body.children);
368
+
369
+ bodyChildren.forEach((child) => {
370
+ if (child instanceof HTMLElement) {
371
+ const cloned = doc.importNode(child, true) as HTMLElement;
372
+
373
+ // Clean up empty paragraphs from Word
374
+ if (cloned.tagName === 'P' && !cloned.textContent?.trim() && !cloned.querySelector('img')) {
375
+ return; // Skip empty paragraphs
376
+ }
377
+
378
+ addEditorAttributes(cloned, 'word');
379
+ elements.push(cloned);
380
+ }
381
+ });
382
+
383
+ // If no elements, fallback to wrapper
384
+ if (elements.length === 0 && parsedDoc.body.textContent?.trim()) {
385
+ const wrapper = doc.createElement('div');
386
+ wrapper.innerHTML = parsedDoc.body.innerHTML;
387
+ addEditorAttributes(wrapper, 'word');
388
+ elements.push(wrapper);
389
+ }
390
+
391
+ return elements;
392
+ }
393
+
394
+ /**
395
+ * Process SVG content.
396
+ * Wraps SVG in a div container and ensures proper attributes.
397
+ *
398
+ * @param content - SVG string (from HTML or plain text)
399
+ * @param doc - Document to create elements in
400
+ * @returns Array of processed HTMLElements
401
+ */
402
+ export function processSvgContent(content: string, doc: Document): HTMLElement[] {
403
+ // Extract SVG element
404
+ let svgString = content;
405
+
406
+ // If it's HTML containing SVG, extract just the SVG
407
+ if (content.includes('<body')) {
408
+ const parser = new DOMParser();
409
+ const parsedDoc = parser.parseFromString(content, 'text/html');
410
+ const svg = parsedDoc.querySelector('svg');
411
+ if (svg) {
412
+ svgString = svg.outerHTML;
413
+ }
414
+ }
415
+
416
+ // Sanitize the SVG
417
+ const sanitized = sanitizePastedHtml(svgString);
418
+
419
+ // Parse SVG
420
+ const parser = new DOMParser();
421
+ const svgDoc = parser.parseFromString(sanitized, 'image/svg+xml');
422
+ const svgElement = svgDoc.querySelector('svg');
423
+
424
+ if (!svgElement) {
425
+ console.warn('[paste-processors] Failed to parse SVG content');
426
+ return [];
427
+ }
428
+
429
+ // Ensure SVG has viewBox
430
+ if (!svgElement.getAttribute('viewBox')) {
431
+ const width = svgElement.getAttribute('width') || '100';
432
+ const height = svgElement.getAttribute('height') || '100';
433
+ svgElement.setAttribute('viewBox', `0 0 ${parseFloat(width)} ${parseFloat(height)}`);
434
+ }
435
+
436
+ // Create wrapper div
437
+ const wrapper = doc.createElement('div');
438
+ wrapper.style.display = 'inline-block';
439
+
440
+ // Import SVG into document
441
+ const importedSvg = doc.importNode(svgElement, true);
442
+ wrapper.appendChild(importedSvg);
443
+
444
+ addEditorAttributes(wrapper, 'svg');
445
+
446
+ return [wrapper];
447
+ }
448
+
449
+ /**
450
+ * Process plain text content.
451
+ * Wraps text in appropriate elements, preserving line breaks.
452
+ *
453
+ * @param text - Plain text string
454
+ * @param doc - Document to create elements in
455
+ * @returns Array of processed HTMLElements
456
+ */
457
+ export function processPlainText(text: string, doc: Document): HTMLElement[] {
458
+ const trimmed = text.trim();
459
+ if (!trimmed) {
460
+ return [];
461
+ }
462
+
463
+ // Split by line breaks and create paragraphs
464
+ const lines = trimmed.split(/\r?\n/);
465
+ const elements: HTMLElement[] = [];
466
+
467
+ // Group consecutive lines into paragraphs
468
+ let currentParagraph: string[] = [];
469
+
470
+ lines.forEach((line, index) => {
471
+ if (line.trim()) {
472
+ currentParagraph.push(line);
473
+ }
474
+
475
+ // Create paragraph when we hit empty line or end
476
+ if ((!line.trim() || index === lines.length - 1) && currentParagraph.length > 0) {
477
+ const p = doc.createElement('p');
478
+ p.innerHTML = currentParagraph.join('<br>');
479
+ p.style.margin = '0 0 1em 0';
480
+ addEditorAttributes(p, 'text');
481
+ elements.push(p);
482
+ currentParagraph = [];
483
+ }
484
+ });
485
+
486
+ // If single line, don't wrap in p, use span
487
+ if (elements.length === 1 && !trimmed.includes('\n')) {
488
+ const span = doc.createElement('span');
489
+ span.textContent = trimmed;
490
+ addEditorAttributes(span, 'text');
491
+ return [span];
492
+ }
493
+
494
+ return elements;
495
+ }
496
+
497
+ /**
498
+ * Process clipboard content based on detected type.
499
+ *
500
+ * @param clipboardData - DataTransfer from paste event
501
+ * @param doc - Document to create elements in (iframe document)
502
+ * @param contentType - Pre-detected content type (optional, will detect if not provided)
503
+ * @returns Processing result with elements and metadata
504
+ */
505
+ export function processClipboardContent(
506
+ clipboardData: DataTransfer,
507
+ doc: Document,
508
+ contentType?: ClipboardContentType
509
+ ): PasteProcessResult {
510
+ const type = contentType || detectContentType(clipboardData);
511
+
512
+ try {
513
+ let elements: HTMLElement[] = [];
514
+
515
+ switch (type) {
516
+ case 'image':
517
+ // Images handled separately by existing useImageUpload hook
518
+ return {
519
+ success: false,
520
+ elements: [],
521
+ contentType: type,
522
+ error: 'Images should be handled by useImageUpload',
523
+ };
524
+
525
+ case 'html-figma': {
526
+ const html = clipboardData.getData('text/html');
527
+ const figmaResult = processFigmaPaste(html, doc);
528
+
529
+ // If Figma decode failed (schema version mismatch), signal to fallback to image
530
+ if (figmaResult.shouldFallbackToImage) {
531
+ return {
532
+ success: false,
533
+ elements: [],
534
+ contentType: type,
535
+ error: 'Figma decode failed, fallback to image',
536
+ shouldFallbackToImage: true,
537
+ };
538
+ }
539
+
540
+ // Return with Figma-specific data for image fetching
541
+ return {
542
+ success: figmaResult.elements.length > 0,
543
+ elements: figmaResult.elements,
544
+ contentType: type,
545
+ figmaFileKey: figmaResult.fileKey,
546
+ figmaImageHashes: figmaResult.imageHashes,
547
+ figmaImageNodeIds: figmaResult.imageNodeIds,
548
+ };
549
+ }
550
+
551
+ case 'html-excel': {
552
+ const html = clipboardData.getData('text/html');
553
+ elements = processExcelContent(html, doc);
554
+ break;
555
+ }
556
+
557
+ case 'html-word': {
558
+ const html = clipboardData.getData('text/html');
559
+ elements = processWordContent(html, doc);
560
+ break;
561
+ }
562
+
563
+ case 'html-browser': {
564
+ const html = clipboardData.getData('text/html');
565
+ elements = processBrowserHtml(html, doc);
566
+ break;
567
+ }
568
+
569
+ case 'svg': {
570
+ // Try HTML first, then plain text
571
+ let content = clipboardData.getData('text/html');
572
+ if (!content || !content.includes('<svg')) {
573
+ content = clipboardData.getData('text/plain');
574
+ }
575
+ elements = processSvgContent(content, doc);
576
+ break;
577
+ }
578
+
579
+ case 'plain-text': {
580
+ const text = clipboardData.getData('text/plain');
581
+ elements = processPlainText(text, doc);
582
+ break;
583
+ }
584
+
585
+ case 'unknown':
586
+ default:
587
+ return {
588
+ success: false,
589
+ elements: [],
590
+ contentType: type,
591
+ error: 'Unknown clipboard content type',
592
+ };
593
+ }
594
+
595
+ return {
596
+ success: elements.length > 0,
597
+ elements,
598
+ contentType: type,
599
+ };
600
+ } catch (error) {
601
+ console.error('[paste-processors] Error processing clipboard content:', error);
602
+ return {
603
+ success: false,
604
+ elements: [],
605
+ contentType: type,
606
+ error: error instanceof Error ? error.message : 'Unknown error',
607
+ };
608
+ }
609
+ }