@liminis/editor 0.1.0-rc.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.
Files changed (219) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +376 -0
  3. package/dist/annotations/anchor-align.d.ts +50 -0
  4. package/dist/annotations/anchor-align.js +74 -0
  5. package/dist/annotations/anchor-model.d.ts +81 -0
  6. package/dist/annotations/anchor-model.js +80 -0
  7. package/dist/annotations/anchor-resolver.d.ts +92 -0
  8. package/dist/annotations/anchor-resolver.js +231 -0
  9. package/dist/annotations/block-structure.d.ts +65 -0
  10. package/dist/annotations/block-structure.js +113 -0
  11. package/dist/annotations/types.d.ts +159 -0
  12. package/dist/annotations/types.js +50 -0
  13. package/dist/annotations.d.ts +25 -0
  14. package/dist/annotations.js +27 -0
  15. package/dist/app/App.d.ts +64 -0
  16. package/dist/app/App.js +180 -0
  17. package/dist/app/editor/AmbientCorrectionPlugin.d.ts +18 -0
  18. package/dist/app/editor/AmbientCorrectionPlugin.js +185 -0
  19. package/dist/app/editor/AnchorScrollPlugin.d.ts +16 -0
  20. package/dist/app/editor/AnchorScrollPlugin.js +134 -0
  21. package/dist/app/editor/AnnotationMarkerPlugin.d.ts +24 -0
  22. package/dist/app/editor/AnnotationMarkerPlugin.js +224 -0
  23. package/dist/app/editor/AnnotationPlugin.d.ts +42 -0
  24. package/dist/app/editor/AnnotationPlugin.js +135 -0
  25. package/dist/app/editor/AnnotationSurface.d.ts +34 -0
  26. package/dist/app/editor/AnnotationSurface.js +115 -0
  27. package/dist/app/editor/AssetContext.d.ts +20 -0
  28. package/dist/app/editor/AssetContext.js +81 -0
  29. package/dist/app/editor/BlockClickPlugin.d.ts +8 -0
  30. package/dist/app/editor/BlockClickPlugin.js +126 -0
  31. package/dist/app/editor/CodeBlockPlugin.d.ts +1 -0
  32. package/dist/app/editor/CodeBlockPlugin.js +219 -0
  33. package/dist/app/editor/CorrectionPanelPlugin.d.ts +14 -0
  34. package/dist/app/editor/CorrectionPanelPlugin.js +333 -0
  35. package/dist/app/editor/DragHandlePlugin.d.ts +1 -0
  36. package/dist/app/editor/DragHandlePlugin.js +300 -0
  37. package/dist/app/editor/Editor.d.ts +84 -0
  38. package/dist/app/editor/Editor.js +539 -0
  39. package/dist/app/editor/FrontmatterPlugin.d.ts +19 -0
  40. package/dist/app/editor/FrontmatterPlugin.js +301 -0
  41. package/dist/app/editor/ImageModal.d.ts +13 -0
  42. package/dist/app/editor/ImageModal.js +267 -0
  43. package/dist/app/editor/ImagePlugin.d.ts +3 -0
  44. package/dist/app/editor/ImagePlugin.js +187 -0
  45. package/dist/app/editor/LinkClickPlugin.d.ts +23 -0
  46. package/dist/app/editor/LinkClickPlugin.js +90 -0
  47. package/dist/app/editor/MarkdownShortcutsPlugin.d.ts +49 -0
  48. package/dist/app/editor/MarkdownShortcutsPlugin.js +373 -0
  49. package/dist/app/editor/OrderedTaskListPlugin.d.ts +21 -0
  50. package/dist/app/editor/OrderedTaskListPlugin.js +78 -0
  51. package/dist/app/editor/SearchPlugin.d.ts +1 -0
  52. package/dist/app/editor/SearchPlugin.js +379 -0
  53. package/dist/app/editor/SelectionContextMenuPlugin.d.ts +29 -0
  54. package/dist/app/editor/SelectionContextMenuPlugin.js +160 -0
  55. package/dist/app/editor/SlashMenu.d.ts +11 -0
  56. package/dist/app/editor/SlashMenu.js +494 -0
  57. package/dist/app/editor/SlashMenuPlugin.d.ts +1 -0
  58. package/dist/app/editor/SlashMenuPlugin.js +168 -0
  59. package/dist/app/editor/TableActionsPlugin.d.ts +1 -0
  60. package/dist/app/editor/TableActionsPlugin.js +398 -0
  61. package/dist/app/editor/TogglePlugin.d.ts +1 -0
  62. package/dist/app/editor/TogglePlugin.js +159 -0
  63. package/dist/app/editor/Toolbar.d.ts +10 -0
  64. package/dist/app/editor/Toolbar.js +263 -0
  65. package/dist/app/editor/WikiLinkExistencePlugin.d.ts +17 -0
  66. package/dist/app/editor/WikiLinkExistencePlugin.js +104 -0
  67. package/dist/app/editor/WikiLinkFormatPlugin.d.ts +17 -0
  68. package/dist/app/editor/WikiLinkFormatPlugin.js +203 -0
  69. package/dist/app/editor/annotation-marks.d.ts +177 -0
  70. package/dist/app/editor/annotation-marks.js +750 -0
  71. package/dist/app/editor/annotationCommands.d.ts +13 -0
  72. package/dist/app/editor/annotationCommands.js +11 -0
  73. package/dist/app/editor/c4/C4InteractiveRenderer.d.ts +35 -0
  74. package/dist/app/editor/c4/C4InteractiveRenderer.js +299 -0
  75. package/dist/app/editor/c4/edge-clipping.d.ts +24 -0
  76. package/dist/app/editor/c4/edge-clipping.js +139 -0
  77. package/dist/app/editor/c4/hooks/useC4DiagramDrag.d.ts +38 -0
  78. package/dist/app/editor/c4/hooks/useC4DiagramDrag.js +112 -0
  79. package/dist/app/editor/c4/layout.d.ts +25 -0
  80. package/dist/app/editor/c4/layout.js +839 -0
  81. package/dist/app/editor/c4/parser.d.ts +19 -0
  82. package/dist/app/editor/c4/parser.js +410 -0
  83. package/dist/app/editor/c4/render-to-string.d.ts +24 -0
  84. package/dist/app/editor/c4/render-to-string.js +34 -0
  85. package/dist/app/editor/c4/renderer.d.ts +64 -0
  86. package/dist/app/editor/c4/renderer.js +569 -0
  87. package/dist/app/editor/c4/types.d.ts +203 -0
  88. package/dist/app/editor/c4/types.js +43 -0
  89. package/dist/app/editor/correction-yaml.d.ts +10 -0
  90. package/dist/app/editor/correction-yaml.js +40 -0
  91. package/dist/app/editor/editorNodes.d.ts +9 -0
  92. package/dist/app/editor/editorNodes.js +36 -0
  93. package/dist/app/editor/index.d.ts +1 -0
  94. package/dist/app/editor/index.js +1 -0
  95. package/dist/app/editor/mark-overlap-resolver.d.ts +6 -0
  96. package/dist/app/editor/mark-overlap-resolver.js +45 -0
  97. package/dist/app/editor/nodes/C4Component.d.ts +14 -0
  98. package/dist/app/editor/nodes/C4Component.js +284 -0
  99. package/dist/app/editor/nodes/C4Node.d.ts +32 -0
  100. package/dist/app/editor/nodes/C4Node.js +112 -0
  101. package/dist/app/editor/nodes/CalloutNode.d.ts +23 -0
  102. package/dist/app/editor/nodes/CalloutNode.js +94 -0
  103. package/dist/app/editor/nodes/CustomLinkNode.d.ts +57 -0
  104. package/dist/app/editor/nodes/CustomLinkNode.js +209 -0
  105. package/dist/app/editor/nodes/CustomListItemNode.d.ts +46 -0
  106. package/dist/app/editor/nodes/CustomListItemNode.js +120 -0
  107. package/dist/app/editor/nodes/CustomListNode.d.ts +27 -0
  108. package/dist/app/editor/nodes/CustomListNode.js +54 -0
  109. package/dist/app/editor/nodes/DefinitionListNode.d.ts +44 -0
  110. package/dist/app/editor/nodes/DefinitionListNode.js +151 -0
  111. package/dist/app/editor/nodes/DiagramContextMenu.d.ts +45 -0
  112. package/dist/app/editor/nodes/DiagramContextMenu.js +116 -0
  113. package/dist/app/editor/nodes/EquationComponent.d.ts +17 -0
  114. package/dist/app/editor/nodes/EquationComponent.js +130 -0
  115. package/dist/app/editor/nodes/EquationNode.d.ts +43 -0
  116. package/dist/app/editor/nodes/EquationNode.js +179 -0
  117. package/dist/app/editor/nodes/FootnoteNode.d.ts +40 -0
  118. package/dist/app/editor/nodes/FootnoteNode.js +151 -0
  119. package/dist/app/editor/nodes/FrontmatterNode.d.ts +24 -0
  120. package/dist/app/editor/nodes/FrontmatterNode.js +88 -0
  121. package/dist/app/editor/nodes/HorizontalRuleNode.d.ts +17 -0
  122. package/dist/app/editor/nodes/HorizontalRuleNode.js +57 -0
  123. package/dist/app/editor/nodes/HtmlNode.d.ts +32 -0
  124. package/dist/app/editor/nodes/HtmlNode.js +121 -0
  125. package/dist/app/editor/nodes/ImageComponent.d.ts +11 -0
  126. package/dist/app/editor/nodes/ImageComponent.js +228 -0
  127. package/dist/app/editor/nodes/ImageNode.d.ts +53 -0
  128. package/dist/app/editor/nodes/ImageNode.js +183 -0
  129. package/dist/app/editor/nodes/ListItemParagraphBreakNode.d.ts +42 -0
  130. package/dist/app/editor/nodes/ListItemParagraphBreakNode.js +95 -0
  131. package/dist/app/editor/nodes/MermaidComponent.d.ts +11 -0
  132. package/dist/app/editor/nodes/MermaidComponent.js +216 -0
  133. package/dist/app/editor/nodes/MermaidNode.d.ts +27 -0
  134. package/dist/app/editor/nodes/MermaidNode.js +96 -0
  135. package/dist/app/editor/nodes/ToggleNode.d.ts +56 -0
  136. package/dist/app/editor/nodes/ToggleNode.js +237 -0
  137. package/dist/app/editor/nodes/diagram-context-menu.d.ts +11 -0
  138. package/dist/app/editor/nodes/diagram-context-menu.js +70 -0
  139. package/dist/app/editor/nodes/diagram-utils.d.ts +13 -0
  140. package/dist/app/editor/nodes/diagram-utils.js +29 -0
  141. package/dist/app/editor/nodes/index.d.ts +30 -0
  142. package/dist/app/editor/nodes/index.js +17 -0
  143. package/dist/app/editor/utils.d.ts +2 -0
  144. package/dist/app/editor/utils.js +17 -0
  145. package/dist/app/mapper/index.d.ts +3 -0
  146. package/dist/app/mapper/index.js +2 -0
  147. package/dist/app/mapper/lexicalToMdast.d.ts +62 -0
  148. package/dist/app/mapper/lexicalToMdast.js +1937 -0
  149. package/dist/app/mapper/mdastToLexical.d.ts +31 -0
  150. package/dist/app/mapper/mdastToLexical.js +1075 -0
  151. package/dist/contract.d.ts +8 -0
  152. package/dist/contract.js +8 -0
  153. package/dist/headless.d.ts +40 -0
  154. package/dist/headless.js +45 -0
  155. package/dist/host/context.d.ts +20 -0
  156. package/dist/host/context.js +24 -0
  157. package/dist/host/defaults.d.ts +21 -0
  158. package/dist/host/defaults.js +47 -0
  159. package/dist/host/messages.d.ts +26 -0
  160. package/dist/host/messages.js +47 -0
  161. package/dist/host/types.d.ts +75 -0
  162. package/dist/host/types.js +13 -0
  163. package/dist/index.d.ts +35 -0
  164. package/dist/index.js +51 -0
  165. package/dist/markdown/annotate-sentinels.d.ts +42 -0
  166. package/dist/markdown/annotate-sentinels.js +86 -0
  167. package/dist/markdown/parse.d.ts +56 -0
  168. package/dist/markdown/parse.js +516 -0
  169. package/dist/markdown/stringify.d.ts +7 -0
  170. package/dist/markdown/stringify.js +439 -0
  171. package/dist/markdown/vendor/mdast-util-wiki-link/LICENSE +21 -0
  172. package/dist/markdown/vendor/mdast-util-wiki-link/README.md +41 -0
  173. package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.d.ts +72 -0
  174. package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.js +127 -0
  175. package/dist/markdown/vendor/mdast-util-wiki-link/index.d.ts +8 -0
  176. package/dist/markdown/vendor/mdast-util-wiki-link/index.js +6 -0
  177. package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.d.ts +17 -0
  178. package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.js +33 -0
  179. package/dist/markdown.d.ts +28 -0
  180. package/dist/markdown.js +44 -0
  181. package/dist/mathjax-config.d.ts +89 -0
  182. package/dist/mathjax-config.js +133 -0
  183. package/dist/nodes.d.ts +34 -0
  184. package/dist/nodes.js +34 -0
  185. package/dist/stores/correctionStore.d.ts +15 -0
  186. package/dist/stores/correctionStore.js +12 -0
  187. package/dist/stores/editorStore.d.ts +16 -0
  188. package/dist/stores/editorStore.js +27 -0
  189. package/dist/styles.css +2477 -0
  190. package/dist/types.d.ts +296 -0
  191. package/dist/types.js +91 -0
  192. package/dist/utils/file-types.d.ts +10 -0
  193. package/dist/utils/file-types.js +46 -0
  194. package/docs/annotations.md +134 -0
  195. package/docs/decisions/README.md +72 -0
  196. package/docs/decisions/adr-002.md +27 -0
  197. package/docs/decisions/adr-007.md +27 -0
  198. package/docs/decisions/adr-010.md +26 -0
  199. package/docs/decisions/adr-024.md +124 -0
  200. package/docs/decisions/adr-024b.md +0 -0
  201. package/docs/decisions/adr-025.md +103 -0
  202. package/docs/decisions/adr-027.md +66 -0
  203. package/docs/decisions/adr-057.md +67 -0
  204. package/docs/decisions/adr-070.md +131 -0
  205. package/docs/decisions/adr-075.md +410 -0
  206. package/docs/decisions/adr-076.md +183 -0
  207. package/docs/decisions/adr-077.md +410 -0
  208. package/docs/decisions/adr-078.md +243 -0
  209. package/docs/decisions/adr-079.md +167 -0
  210. package/docs/decisions/adr-080.md +325 -0
  211. package/docs/decisions/adr-081.md +108 -0
  212. package/docs/decisions/adr-082.md +123 -0
  213. package/docs/decisions/adr-083.md +204 -0
  214. package/docs/decisions/adr-084.md +179 -0
  215. package/docs/editor-api.md +106 -0
  216. package/docs/markdown-pipeline.md +282 -0
  217. package/docs/provenance.md +106 -0
  218. package/docs/zusammen-editor-capability-map.md +232 -0
  219. package/package.json +175 -0
@@ -0,0 +1,228 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
+ import { $getNodeByKey } from 'lexical';
5
+ import { $isImageNode } from './ImageNode.js';
6
+ import { useAssetContext } from '../AssetContext.js';
7
+ import { DiagramContextMenu } from './DiagramContextMenu.js';
8
+ import { useEditorHost } from '../../../host/context.js';
9
+ function isAbsoluteUrl(path) {
10
+ return (path.startsWith('http://') ||
11
+ path.startsWith('https://') ||
12
+ path.startsWith('data:') ||
13
+ path.startsWith('blob:') ||
14
+ path.startsWith('vscode-webview-resource:'));
15
+ }
16
+ export function ImageComponent({ src, alt, title, width, height, nodeKey, }) {
17
+ const [editor] = useLexicalComposerContext();
18
+ const { resolveAssetPath, resolveLocalAsset } = useAssetContext();
19
+ const { notifyError } = useEditorHost();
20
+ const imageRef = useRef(null);
21
+ // Resolve the src path for display
22
+ const resolvedSrc = resolveAssetPath(src);
23
+ const [localDataUrl, setLocalDataUrl] = useState(null);
24
+ const [loadError, setLoadError] = useState(false);
25
+ useEffect(() => {
26
+ if (isAbsoluteUrl(resolvedSrc) || !resolveLocalAsset) {
27
+ setLocalDataUrl(null);
28
+ setLoadError(false);
29
+ return;
30
+ }
31
+ let cancelled = false;
32
+ setLoadError(false);
33
+ resolveLocalAsset(resolvedSrc).then((dataUrl) => {
34
+ if (!cancelled) {
35
+ if (dataUrl) {
36
+ setLocalDataUrl(dataUrl);
37
+ }
38
+ else {
39
+ setLoadError(true);
40
+ }
41
+ }
42
+ }).catch(() => {
43
+ if (!cancelled)
44
+ setLoadError(true);
45
+ });
46
+ return () => { cancelled = true; };
47
+ }, [resolvedSrc, resolveLocalAsset]);
48
+ // Use local data URL for relative paths, otherwise use the resolved src directly
49
+ const displaySrc = isAbsoluteUrl(resolvedSrc) ? resolvedSrc : (localDataUrl ?? resolvedSrc);
50
+ // Context menu state
51
+ const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 });
52
+ const handleContextMenu = useCallback((e) => {
53
+ e.preventDefault();
54
+ e.stopPropagation();
55
+ setContextMenu({ visible: true, x: e.clientX, y: e.clientY });
56
+ }, []);
57
+ const handleCopyImage = useCallback(async () => {
58
+ const img = imageRef.current;
59
+ if (!img) {
60
+ setContextMenu((prev) => ({ ...prev, visible: false }));
61
+ return;
62
+ }
63
+ try {
64
+ // Use rendered size (not naturalWidth which can be 0 for viewBox-only SVGs)
65
+ const scale = window.devicePixelRatio || 2;
66
+ const rect = img.getBoundingClientRect();
67
+ const w = rect.width;
68
+ const h = rect.height;
69
+ if (!w || !h) {
70
+ setContextMenu((prev) => ({ ...prev, visible: false }));
71
+ return;
72
+ }
73
+ const canvas = document.createElement('canvas');
74
+ const canvasWidth = Math.round(w * scale);
75
+ const canvasHeight = Math.round(h * scale);
76
+ canvas.width = canvasWidth;
77
+ canvas.height = canvasHeight;
78
+ const ctx = canvas.getContext('2d');
79
+ ctx.scale(canvasWidth / w, canvasHeight / h);
80
+ ctx.drawImage(img, 0, 0, w, h);
81
+ const blob = await new Promise((resolve, reject) => {
82
+ canvas.toBlob((b) => b ? resolve(b) : reject(new Error('toBlob failed')), 'image/png');
83
+ });
84
+ await navigator.clipboard.write([
85
+ new ClipboardItem({ 'image/png': blob })
86
+ ]);
87
+ }
88
+ catch (err) {
89
+ console.error('Failed to copy image to clipboard:', err);
90
+ // The <img> no longer requests CORS mode (see crossOrigin removal below),
91
+ // so drawing a cross-origin image onto the canvas taints it and toBlob()
92
+ // rejects with a SecurityError. That's an accepted tradeoff: display
93
+ // wins over copy for remote images that don't support CORS. Since this
94
+ // is now the primary expected trigger for this catch (rather than an
95
+ // edge case), give it a message that explains the cause instead of the
96
+ // generic DOMException text.
97
+ const isTaintedCanvas = err instanceof DOMException && err.name === 'SecurityError';
98
+ notifyError('Failed to copy image', isTaintedCanvas
99
+ ? 'This image is hosted on another site that does not allow copying — try saving it instead.'
100
+ : err instanceof Error ? err.message : String(err));
101
+ }
102
+ setContextMenu((prev) => ({ ...prev, visible: false }));
103
+ }, [notifyError]);
104
+ const [isResizing, setIsResizing] = useState(false);
105
+ const [isSelected, setIsSelected] = useState(false);
106
+ const [currentWidth, setCurrentWidth] = useState(width);
107
+ const [naturalWidth, setNaturalWidth] = useState(0);
108
+ const [naturalHeight, setNaturalHeight] = useState(0);
109
+ // Track resize start position
110
+ const resizeStartRef = useRef(null);
111
+ // Update current width when prop changes
112
+ useEffect(() => {
113
+ setCurrentWidth(width);
114
+ }, [width]);
115
+ // Handle image load to get natural dimensions
116
+ const handleImageLoad = useCallback(() => {
117
+ if (imageRef.current) {
118
+ setNaturalWidth(imageRef.current.naturalWidth);
119
+ setNaturalHeight(imageRef.current.naturalHeight);
120
+ }
121
+ }, []);
122
+ // Handle click to select image
123
+ const handleClick = useCallback((e) => {
124
+ e.stopPropagation();
125
+ setIsSelected(true);
126
+ }, []);
127
+ // Handle click outside to deselect
128
+ useEffect(() => {
129
+ const handleClickOutside = (e) => {
130
+ if (imageRef.current && !imageRef.current.parentElement?.contains(e.target)) {
131
+ setIsSelected(false);
132
+ }
133
+ };
134
+ document.addEventListener('mousedown', handleClickOutside);
135
+ return () => document.removeEventListener('mousedown', handleClickOutside);
136
+ }, []);
137
+ // Handle resize start
138
+ const handleResizeStart = useCallback((e, corner) => {
139
+ e.preventDefault();
140
+ e.stopPropagation();
141
+ const startWidth = currentWidth || imageRef.current?.offsetWidth || naturalWidth;
142
+ resizeStartRef.current = {
143
+ startX: e.clientX,
144
+ startWidth,
145
+ corner,
146
+ };
147
+ setIsResizing(true);
148
+ }, [currentWidth, naturalWidth]);
149
+ // Use refs for values accessed in event handlers to avoid effect re-registration
150
+ const currentWidthRef = useRef(currentWidth);
151
+ currentWidthRef.current = currentWidth;
152
+ const naturalWidthRef = useRef(naturalWidth);
153
+ naturalWidthRef.current = naturalWidth;
154
+ const naturalHeightRef = useRef(naturalHeight);
155
+ naturalHeightRef.current = naturalHeight;
156
+ // Handle resize move
157
+ useEffect(() => {
158
+ if (!isResizing)
159
+ return;
160
+ const handleMouseMove = (e) => {
161
+ if (!resizeStartRef.current)
162
+ return;
163
+ const { startX, startWidth, corner } = resizeStartRef.current;
164
+ let deltaX = e.clientX - startX;
165
+ // For left corners, invert the delta
166
+ if (corner === 'nw' || corner === 'sw') {
167
+ deltaX = -deltaX;
168
+ }
169
+ const newWidth = Math.max(50, startWidth + deltaX);
170
+ setCurrentWidth(newWidth);
171
+ };
172
+ const handleMouseUp = () => {
173
+ const width = currentWidthRef.current;
174
+ const natWidth = naturalWidthRef.current;
175
+ const natHeight = naturalHeightRef.current;
176
+ if (resizeStartRef.current && width) {
177
+ // Calculate proportional height
178
+ const aspectRatio = natHeight / natWidth;
179
+ const newHeight = Math.round(width * aspectRatio);
180
+ // Update the node with new dimensions
181
+ editor.update(() => {
182
+ const node = $getNodeByKey(nodeKey);
183
+ if ($isImageNode(node)) {
184
+ node.setWidth(width);
185
+ node.setHeight(newHeight);
186
+ }
187
+ });
188
+ }
189
+ resizeStartRef.current = null;
190
+ setIsResizing(false);
191
+ };
192
+ document.addEventListener('mousemove', handleMouseMove);
193
+ document.addEventListener('mouseup', handleMouseUp);
194
+ return () => {
195
+ document.removeEventListener('mousemove', handleMouseMove);
196
+ document.removeEventListener('mouseup', handleMouseUp);
197
+ };
198
+ }, [isResizing, editor, nodeKey]);
199
+ // Calculate display dimensions
200
+ const displayWidth = currentWidth || width;
201
+ const displayHeight = displayWidth && naturalWidth && naturalHeight
202
+ ? Math.round(displayWidth * (naturalHeight / naturalWidth))
203
+ : height;
204
+ // Show placeholder while loading local assets
205
+ if (!isAbsoluteUrl(resolvedSrc) && resolveLocalAsset && !localDataUrl && !loadError) {
206
+ return (_jsx("div", { className: "image-wrapper", children: _jsx("div", { className: "image-placeholder", style: { padding: '1em', opacity: 0.5 }, children: "Loading image\u2026" }) }));
207
+ }
208
+ if (loadError) {
209
+ return (_jsx("div", { className: "image-wrapper", children: _jsxs("div", { className: "image-placeholder", style: { padding: '1em', opacity: 0.5 }, children: ["Image not found: ", src] }) }));
210
+ }
211
+ return (_jsxs("div", { className: `image-wrapper ${isSelected ? 'selected' : ''} ${isResizing ? 'resizing' : ''} ${!displayWidth ? 'no-dimensions' : ''}`, onClick: handleClick, onContextMenu: handleContextMenu, children: [_jsx("img", { ref: imageRef, src: displaySrc, alt: alt, title: title, draggable: false,
212
+ // No crossOrigin attribute: requesting CORS mode makes display of a
213
+ // remote image depend on the host sending Access-Control-Allow-Origin,
214
+ // which most ordinary image hosts (e.g. shields.io badges) don't do —
215
+ // the browser would render nothing instead of the image. The only
216
+ // consumer of an untainted canvas is handleCopyImage's clipboard
217
+ // read-back above, which already handles (and now surfaces) failure
218
+ // when a cross-origin image taints the canvas.
219
+ onLoad: handleImageLoad, style: {
220
+ // If an explicit width is set (from resize), use it.
221
+ // Otherwise, let the image display at natural size but cap at 100%
222
+ // of the container — raster images should never scale up beyond
223
+ // their natural dimensions.
224
+ width: displayWidth ? `${displayWidth}px` : undefined,
225
+ height: displayHeight ? `${displayHeight}px` : undefined,
226
+ maxWidth: '100%',
227
+ } }), isSelected && (_jsxs(_Fragment, { children: [_jsx("div", { className: "image-resize-handle nw", onMouseDown: (e) => handleResizeStart(e, 'nw') }), _jsx("div", { className: "image-resize-handle ne", onMouseDown: (e) => handleResizeStart(e, 'ne') }), _jsx("div", { className: "image-resize-handle sw", onMouseDown: (e) => handleResizeStart(e, 'sw') }), _jsx("div", { className: "image-resize-handle se", onMouseDown: (e) => handleResizeStart(e, 'se') })] })), isSelected && displayWidth && (_jsxs("div", { className: "image-size-badge", children: [displayWidth, " \u00D7 ", displayHeight] })), _jsx(DiagramContextMenu, { visible: contextMenu.visible, x: contextMenu.x, y: contextMenu.y, onCopyImage: handleCopyImage, onClose: () => setContextMenu((prev) => ({ ...prev, visible: false })) })] }));
228
+ }
@@ -0,0 +1,53 @@
1
+ import { DecoratorNode, DOMConversionMap, DOMExportOutput, LexicalNode, NodeKey, SerializedLexicalNode, Spread } from 'lexical';
2
+ export type SerializedImageNode = Spread<{
3
+ src: string;
4
+ alt: string;
5
+ title?: string;
6
+ width?: number;
7
+ height?: number;
8
+ linkUrl?: string;
9
+ }, SerializedLexicalNode>;
10
+ export declare class ImageNode extends DecoratorNode<JSX.Element> {
11
+ __src: string;
12
+ __alt: string;
13
+ __title: string | undefined;
14
+ __displaySrc: string | undefined;
15
+ __width: number | undefined;
16
+ __height: number | undefined;
17
+ __linkUrl: string | undefined;
18
+ static getType(): string;
19
+ static clone(node: ImageNode): ImageNode;
20
+ constructor(src: string, alt: string, options?: {
21
+ title?: string;
22
+ linkUrl?: string;
23
+ key?: NodeKey;
24
+ });
25
+ getSrc(): string;
26
+ getAlt(): string;
27
+ getTitle(): string | undefined;
28
+ setSrc(src: string): void;
29
+ setAlt(alt: string): void;
30
+ setTitle(title: string | undefined): void;
31
+ getDisplaySrc(): string;
32
+ setDisplaySrc(displaySrc: string | undefined): void;
33
+ getWidth(): number | undefined;
34
+ setWidth(width: number | undefined): void;
35
+ getHeight(): number | undefined;
36
+ setHeight(height: number | undefined): void;
37
+ getLinkUrl(): string | undefined;
38
+ setLinkUrl(linkUrl: string | undefined): void;
39
+ createDOM(): HTMLElement;
40
+ updateDOM(): boolean;
41
+ static importDOM(): DOMConversionMap | null;
42
+ exportDOM(): DOMExportOutput;
43
+ static importJSON(serializedNode: SerializedImageNode): ImageNode;
44
+ exportJSON(): SerializedImageNode;
45
+ decorate(): JSX.Element;
46
+ isInline(): boolean;
47
+ }
48
+ export declare function $createImageNode(src: string, alt: string, options?: {
49
+ title?: string;
50
+ displaySrc?: string;
51
+ linkUrl?: string;
52
+ }): ImageNode;
53
+ export declare function $isImageNode(node: LexicalNode | null | undefined): node is ImageNode;
@@ -0,0 +1,183 @@
1
+ import { DecoratorNode, } from 'lexical';
2
+ import { createElement } from 'react';
3
+ import { ImageComponent } from './ImageComponent.js';
4
+ export class ImageNode extends DecoratorNode {
5
+ __src; // Relative path for markdown (e.g., "assets/image.png")
6
+ __alt;
7
+ __title;
8
+ __displaySrc; // Webview URI for display
9
+ __width;
10
+ __height;
11
+ __linkUrl; // Optional URL to link to when clicked
12
+ static getType() {
13
+ return 'image';
14
+ }
15
+ static clone(node) {
16
+ const cloned = new ImageNode(node.__src, node.__alt, {
17
+ title: node.__title,
18
+ linkUrl: node.__linkUrl,
19
+ key: node.__key,
20
+ });
21
+ cloned.__displaySrc = node.__displaySrc;
22
+ cloned.__width = node.__width;
23
+ cloned.__height = node.__height;
24
+ return cloned;
25
+ }
26
+ constructor(src, alt, options) {
27
+ super(options?.key);
28
+ this.__src = src;
29
+ this.__alt = alt;
30
+ this.__title = options?.title;
31
+ this.__linkUrl = options?.linkUrl;
32
+ this.__displaySrc = undefined;
33
+ this.__width = undefined;
34
+ this.__height = undefined;
35
+ }
36
+ getSrc() {
37
+ return this.__src;
38
+ }
39
+ getAlt() {
40
+ return this.__alt;
41
+ }
42
+ getTitle() {
43
+ return this.__title;
44
+ }
45
+ setSrc(src) {
46
+ const writable = this.getWritable();
47
+ writable.__src = src;
48
+ }
49
+ setAlt(alt) {
50
+ const writable = this.getWritable();
51
+ writable.__alt = alt;
52
+ }
53
+ setTitle(title) {
54
+ const writable = this.getWritable();
55
+ writable.__title = title;
56
+ }
57
+ getDisplaySrc() {
58
+ // Return displaySrc if set, otherwise fall back to src (for URLs)
59
+ return this.__displaySrc || this.__src;
60
+ }
61
+ setDisplaySrc(displaySrc) {
62
+ const writable = this.getWritable();
63
+ writable.__displaySrc = displaySrc;
64
+ }
65
+ getWidth() {
66
+ return this.__width;
67
+ }
68
+ setWidth(width) {
69
+ const writable = this.getWritable();
70
+ writable.__width = width;
71
+ }
72
+ getHeight() {
73
+ return this.__height;
74
+ }
75
+ setHeight(height) {
76
+ const writable = this.getWritable();
77
+ writable.__height = height;
78
+ }
79
+ getLinkUrl() {
80
+ return this.__linkUrl;
81
+ }
82
+ setLinkUrl(linkUrl) {
83
+ const writable = this.getWritable();
84
+ writable.__linkUrl = linkUrl;
85
+ }
86
+ createDOM() {
87
+ const element = document.createElement('figure');
88
+ element.className = 'image-block';
89
+ return element;
90
+ }
91
+ updateDOM() {
92
+ return false;
93
+ }
94
+ static importDOM() {
95
+ return {
96
+ img: () => ({
97
+ conversion: (domNode) => {
98
+ const img = domNode;
99
+ const src = img.getAttribute('src') || '';
100
+ const alt = img.getAttribute('alt') || '';
101
+ const title = img.getAttribute('title') || undefined;
102
+ const widthAttr = img.getAttribute('width');
103
+ const heightAttr = img.getAttribute('height');
104
+ const node = new ImageNode(src, alt, { title });
105
+ if (widthAttr) {
106
+ node.__width = parseInt(widthAttr, 10);
107
+ }
108
+ if (heightAttr) {
109
+ node.__height = parseInt(heightAttr, 10);
110
+ }
111
+ return { node };
112
+ },
113
+ priority: 0,
114
+ }),
115
+ };
116
+ }
117
+ exportDOM() {
118
+ const figure = document.createElement('figure');
119
+ figure.className = 'image-block';
120
+ const img = document.createElement('img');
121
+ img.src = this.__src;
122
+ img.alt = this.__alt;
123
+ if (this.__title) {
124
+ img.title = this.__title;
125
+ }
126
+ if (this.__width) {
127
+ img.width = this.__width;
128
+ }
129
+ if (this.__height) {
130
+ img.height = this.__height;
131
+ }
132
+ figure.appendChild(img);
133
+ return { element: figure };
134
+ }
135
+ static importJSON(serializedNode) {
136
+ const node = new ImageNode(serializedNode.src, serializedNode.alt, {
137
+ title: serializedNode.title,
138
+ linkUrl: serializedNode.linkUrl,
139
+ });
140
+ if (serializedNode.width) {
141
+ node.__width = serializedNode.width;
142
+ }
143
+ if (serializedNode.height) {
144
+ node.__height = serializedNode.height;
145
+ }
146
+ return node;
147
+ }
148
+ exportJSON() {
149
+ return {
150
+ type: 'image',
151
+ src: this.__src,
152
+ alt: this.__alt,
153
+ title: this.__title,
154
+ width: this.__width,
155
+ height: this.__height,
156
+ linkUrl: this.__linkUrl,
157
+ version: 1,
158
+ };
159
+ }
160
+ decorate() {
161
+ return createElement(ImageComponent, {
162
+ src: this.getDisplaySrc(),
163
+ alt: this.__alt,
164
+ title: this.__title,
165
+ width: this.__width,
166
+ height: this.__height,
167
+ nodeKey: this.__key,
168
+ });
169
+ }
170
+ isInline() {
171
+ return false;
172
+ }
173
+ }
174
+ export function $createImageNode(src, alt, options) {
175
+ const node = new ImageNode(src, alt, { title: options?.title, linkUrl: options?.linkUrl });
176
+ if (options?.displaySrc) {
177
+ node.__displaySrc = options.displaySrc;
178
+ }
179
+ return node;
180
+ }
181
+ export function $isImageNode(node) {
182
+ return node instanceof ImageNode;
183
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * ListItemParagraphBreakNode — inline sentinel marking the boundary between two
3
+ * consecutive mdast paragraphs flattened onto a single ListItemNode (see
4
+ * convertListItem in mdastToLexical.ts). @lexical/list's ListItemNode.append()
5
+ * unwraps a directly-appended ParagraphNode, flattening its inline content onto the
6
+ * item, so this marker is what convertListItem inserts instead to record where one
7
+ * paragraph ends and the next begins.
8
+ *
9
+ * Its own node type — distinct from LineBreakNode — makes it structurally
10
+ * impossible to collide with a genuine hard break (however many appear
11
+ * consecutively): $isLineBreakNode and $isListItemParagraphBreakNode are mutually
12
+ * exclusive by construction. This replaces the earlier two-consecutive-
13
+ * LineBreakNode marker, which was ambiguous with a real double hard break inside a
14
+ * single mdast paragraph (see #902).
15
+ *
16
+ * Modeled as an inline ElementNode (the same shape @lexical/link's LinkNode uses to
17
+ * sit inline among TextNode siblings) rather than extending LexicalNode directly —
18
+ * the `lexical` package's entry point re-exports LexicalNode via `export type`
19
+ * only (see node_modules/lexical/index.d.ts), so it's usable here solely as a
20
+ * TypeScript type annotation (as every other custom node in this directory does),
21
+ * never as a runtime value in an `extends` clause. ListItemNode.append() only unwraps
22
+ * ListItemNode/ParagraphNode children (see its canMergeWith override in
23
+ * @lexical/list), so this node — being neither — is never merged or unwrapped when
24
+ * appended directly to a list item. ElementNode's default canBeEmpty() is already
25
+ * `true`, so a childless instance is never garbage-collected by reconciliation
26
+ * either.
27
+ */
28
+ import { DOMConversionMap, DOMExportOutput, ElementNode, LexicalNode, SerializedElementNode } from 'lexical';
29
+ export type SerializedListItemParagraphBreakNode = SerializedElementNode;
30
+ export declare class ListItemParagraphBreakNode extends ElementNode {
31
+ static getType(): string;
32
+ static clone(node: ListItemParagraphBreakNode): ListItemParagraphBreakNode;
33
+ isInline(): boolean;
34
+ createDOM(): HTMLElement;
35
+ updateDOM(): boolean;
36
+ exportDOM(): DOMExportOutput;
37
+ static importDOM(): DOMConversionMap | null;
38
+ static importJSON(serializedNode: SerializedListItemParagraphBreakNode): ListItemParagraphBreakNode;
39
+ exportJSON(): SerializedListItemParagraphBreakNode;
40
+ }
41
+ export declare function $createListItemParagraphBreakNode(): ListItemParagraphBreakNode;
42
+ export declare function $isListItemParagraphBreakNode(node: LexicalNode | null | undefined): node is ListItemParagraphBreakNode;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * ListItemParagraphBreakNode — inline sentinel marking the boundary between two
3
+ * consecutive mdast paragraphs flattened onto a single ListItemNode (see
4
+ * convertListItem in mdastToLexical.ts). @lexical/list's ListItemNode.append()
5
+ * unwraps a directly-appended ParagraphNode, flattening its inline content onto the
6
+ * item, so this marker is what convertListItem inserts instead to record where one
7
+ * paragraph ends and the next begins.
8
+ *
9
+ * Its own node type — distinct from LineBreakNode — makes it structurally
10
+ * impossible to collide with a genuine hard break (however many appear
11
+ * consecutively): $isLineBreakNode and $isListItemParagraphBreakNode are mutually
12
+ * exclusive by construction. This replaces the earlier two-consecutive-
13
+ * LineBreakNode marker, which was ambiguous with a real double hard break inside a
14
+ * single mdast paragraph (see #902).
15
+ *
16
+ * Modeled as an inline ElementNode (the same shape @lexical/link's LinkNode uses to
17
+ * sit inline among TextNode siblings) rather than extending LexicalNode directly —
18
+ * the `lexical` package's entry point re-exports LexicalNode via `export type`
19
+ * only (see node_modules/lexical/index.d.ts), so it's usable here solely as a
20
+ * TypeScript type annotation (as every other custom node in this directory does),
21
+ * never as a runtime value in an `extends` clause. ListItemNode.append() only unwraps
22
+ * ListItemNode/ParagraphNode children (see its canMergeWith override in
23
+ * @lexical/list), so this node — being neither — is never merged or unwrapped when
24
+ * appended directly to a list item. ElementNode's default canBeEmpty() is already
25
+ * `true`, so a childless instance is never garbage-collected by reconciliation
26
+ * either.
27
+ */
28
+ import { ElementNode, } from 'lexical';
29
+ const MARKER_ATTRIBUTE = 'data-list-item-paragraph-break';
30
+ function $convertListItemParagraphBreakElement(_domNode) {
31
+ return { node: $createListItemParagraphBreakNode() };
32
+ }
33
+ export class ListItemParagraphBreakNode extends ElementNode {
34
+ static getType() {
35
+ return 'list-item-paragraph-break';
36
+ }
37
+ static clone(node) {
38
+ return new ListItemParagraphBreakNode(node.__key);
39
+ }
40
+ // Sits inline within a ListItemNode's flattened children, alongside TextNode and
41
+ // CustomLinkNode siblings, never as a block-level sibling of its own.
42
+ isInline() {
43
+ return true;
44
+ }
45
+ // Renders two <br> elements — the same visual gap the previous two-LineBreakNode
46
+ // marker produced — so a list item with consecutive paragraphs still shows a
47
+ // blank-line-like separator while editing, instead of the paragraphs visually
48
+ // running together with no boundary at all.
49
+ createDOM() {
50
+ const el = document.createElement('span');
51
+ el.setAttribute(MARKER_ATTRIBUTE, '');
52
+ el.appendChild(document.createElement('br'));
53
+ el.appendChild(document.createElement('br'));
54
+ return el;
55
+ }
56
+ updateDOM() {
57
+ return false;
58
+ }
59
+ exportDOM() {
60
+ const el = document.createElement('span');
61
+ el.setAttribute(MARKER_ATTRIBUTE, '');
62
+ el.appendChild(document.createElement('br'));
63
+ el.appendChild(document.createElement('br'));
64
+ return { element: el };
65
+ }
66
+ static importDOM() {
67
+ return {
68
+ span: (domNode) => {
69
+ if (!domNode.hasAttribute(MARKER_ATTRIBUTE))
70
+ return null;
71
+ return { conversion: $convertListItemParagraphBreakElement, priority: 1 };
72
+ },
73
+ };
74
+ }
75
+ static importJSON(serializedNode) {
76
+ const node = $createListItemParagraphBreakNode();
77
+ node.setFormat(serializedNode.format);
78
+ node.setIndent(serializedNode.indent);
79
+ node.setDirection(serializedNode.direction);
80
+ return node;
81
+ }
82
+ exportJSON() {
83
+ return {
84
+ ...super.exportJSON(),
85
+ type: 'list-item-paragraph-break',
86
+ version: 1,
87
+ };
88
+ }
89
+ }
90
+ export function $createListItemParagraphBreakNode() {
91
+ return new ListItemParagraphBreakNode();
92
+ }
93
+ export function $isListItemParagraphBreakNode(node) {
94
+ return node instanceof ListItemParagraphBreakNode;
95
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * MermaidComponent - React component for rendering and editing Mermaid diagrams
3
+ * Uses Shadow DOM for style isolation
4
+ */
5
+ import { NodeKey } from 'lexical';
6
+ interface MermaidComponentProps {
7
+ code: string;
8
+ nodeKey: NodeKey;
9
+ }
10
+ export default function MermaidComponent({ code, nodeKey, }: MermaidComponentProps): JSX.Element;
11
+ export {};