@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,539 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useMemo, useState, lazy, Suspense } from 'react';
3
+ import { LexicalComposer } from '@lexical/react/LexicalComposer';
4
+ import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
5
+ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
6
+ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
7
+ import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
8
+ import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
9
+ import { ListPlugin } from '@lexical/react/LexicalListPlugin';
10
+ import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin';
11
+ import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin';
12
+ import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
13
+ import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
14
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
15
+ import { $isCodeNode } from '@lexical/code';
16
+ import { registerCodeHighlighting } from '@lexical/code-prism';
17
+ import { $createParagraphNode, $createRangeSelection, $getRoot, $getSelection, $isElementNode, $isLineBreakNode, $isRangeSelection, $isTextNode, $setSelection, COMMAND_PRIORITY_LOW, KEY_ENTER_COMMAND, } from 'lexical';
18
+ import { mergeRegister } from '@lexical/utils';
19
+ import { Toolbar } from './Toolbar.js';
20
+ import { SlashMenuPlugin } from './SlashMenuPlugin.js';
21
+ import { DragHandlePlugin } from './DragHandlePlugin.js';
22
+ import { MarkdownShortcutsPlugin } from './MarkdownShortcutsPlugin.js';
23
+ import { TableActionsPlugin } from './TableActionsPlugin.js';
24
+ import { CodeBlockPlugin } from './CodeBlockPlugin.js';
25
+ import { TogglePlugin } from './TogglePlugin.js';
26
+ import { ImagePlugin } from './ImagePlugin.js';
27
+ import { BlockClickPlugin } from './BlockClickPlugin.js';
28
+ import { LinkClickPlugin } from './LinkClickPlugin.js';
29
+ import { OrderedTaskListPlugin } from './OrderedTaskListPlugin.js';
30
+ import { SearchPlugin } from './SearchPlugin.js';
31
+ import { FrontmatterPlugin } from './FrontmatterPlugin.js';
32
+ import { WikiLinkExistencePlugin } from './WikiLinkExistencePlugin.js';
33
+ import { WikiLinkFormatPlugin } from './WikiLinkFormatPlugin.js';
34
+ import { AnchorScrollPlugin } from './AnchorScrollPlugin.js';
35
+ import { SelectionContextMenuPlugin } from './SelectionContextMenuPlugin.js';
36
+ import { CorrectionPanelPlugin } from './CorrectionPanelPlugin.js';
37
+ import { AmbientCorrectionPlugin } from './AmbientCorrectionPlugin.js';
38
+ import { AssetContext, createAssetContextValue } from './AssetContext.js';
39
+ import { editorNodes } from './editorNodes.js';
40
+ import { importMarkdownToLexicalInEditorStateWithOffsets, importMarkdownToLexicalWithOffsets, } from '../mapper/mdastToLexical.js';
41
+ import { exportLexicalToMdast } from '../mapper/lexicalToMdast.js';
42
+ import { parseMarkdown } from '../../markdown/parse.js';
43
+ import { stringifyMarkdown } from '../../markdown/stringify.js';
44
+ /**
45
+ * The annotation feature surface, loaded only when a host configures at least
46
+ * one kind. A static import here would defeat FR-004: every consumer would
47
+ * pull the annotation modules into its graph whether or not it uses them.
48
+ */
49
+ const LazyAnnotationSurface = lazy(() => import('./AnnotationSurface.js'));
50
+ /** Stable identity, so an unconfigured `annotations` prop doesn't re-render the surface. */
51
+ const EMPTY_ANNOTATIONS = [];
52
+ const editorTheme = {
53
+ paragraph: 'editor-paragraph',
54
+ heading: {
55
+ h1: 'editor-heading-h1',
56
+ h2: 'editor-heading-h2',
57
+ h3: 'editor-heading-h3',
58
+ h4: 'editor-heading-h4',
59
+ h5: 'editor-heading-h5',
60
+ },
61
+ list: {
62
+ ul: 'editor-list-ul',
63
+ ol: 'editor-list-ol',
64
+ listitem: 'editor-listitem',
65
+ listitemChecked: 'editor-listitem-checked',
66
+ listitemUnchecked: 'editor-listitem-unchecked',
67
+ nested: {
68
+ listitem: 'editor-nested-listitem',
69
+ },
70
+ },
71
+ quote: 'editor-quote',
72
+ code: 'editor-code',
73
+ codeHighlight: {
74
+ atrule: 'editor-tokenAttr',
75
+ attr: 'editor-tokenAttr',
76
+ boolean: 'editor-tokenProperty',
77
+ builtin: 'editor-tokenSelector',
78
+ cdata: 'editor-tokenComment',
79
+ char: 'editor-tokenSelector',
80
+ class: 'editor-tokenFunction',
81
+ 'class-name': 'editor-tokenFunction',
82
+ comment: 'editor-tokenComment',
83
+ constant: 'editor-tokenProperty',
84
+ deleted: 'editor-tokenProperty',
85
+ doctype: 'editor-tokenComment',
86
+ entity: 'editor-tokenOperator',
87
+ function: 'editor-tokenFunction',
88
+ important: 'editor-tokenVariable',
89
+ inserted: 'editor-tokenSelector',
90
+ keyword: 'editor-tokenAttr',
91
+ namespace: 'editor-tokenVariable',
92
+ number: 'editor-tokenProperty',
93
+ operator: 'editor-tokenOperator',
94
+ prolog: 'editor-tokenComment',
95
+ property: 'editor-tokenProperty',
96
+ punctuation: 'editor-tokenPunctuation',
97
+ regex: 'editor-tokenVariable',
98
+ selector: 'editor-tokenSelector',
99
+ string: 'editor-tokenSelector',
100
+ symbol: 'editor-tokenProperty',
101
+ tag: 'editor-tokenProperty',
102
+ url: 'editor-tokenOperator',
103
+ variable: 'editor-tokenVariable',
104
+ },
105
+ link: 'editor-link',
106
+ table: 'editor-table',
107
+ tableRow: 'editor-table-row',
108
+ tableCell: 'editor-table-cell',
109
+ tableCellHeader: 'editor-table-cell-header',
110
+ text: {
111
+ bold: 'editor-text-bold',
112
+ italic: 'editor-text-italic',
113
+ strikethrough: 'editor-text-strikethrough',
114
+ code: 'editor-text-code',
115
+ underline: 'editor-text-underline',
116
+ },
117
+ };
118
+ function editorOnError(error) {
119
+ console.error('Lexical error:', error);
120
+ }
121
+ // Mirror of $exitCodeNodeOnEnter in @lexical/code-core@0.44 (CodeExtension.register).
122
+ // CodePrismExtension requires LexicalExtensionComposer which is incompatible with
123
+ // LexicalComposer, so the Enter×3 escape logic is wired here instead.
124
+ // Re-verify against upstream when bumping @lexical/code past 0.44 (#8360).
125
+ function $exitCodeBlockOnEnter(selection) {
126
+ const { anchor } = selection;
127
+ if (!selection.isCollapsed() || anchor.type !== 'element')
128
+ return false;
129
+ const codeNode = anchor.getNode();
130
+ if (!$isCodeNode(codeNode))
131
+ return false;
132
+ const childrenSize = codeNode.getChildrenSize();
133
+ if (childrenSize < 2 || anchor.offset !== childrenSize)
134
+ return false;
135
+ const lastChild = codeNode.getLastChild();
136
+ if (!$isLineBreakNode(lastChild) || !$isLineBreakNode(lastChild.getPreviousSibling()))
137
+ return false;
138
+ const newElement = $createParagraphNode();
139
+ codeNode.splice(childrenSize - 2, 2, []).insertAfter(newElement, false);
140
+ newElement.select();
141
+ return true;
142
+ }
143
+ // Plugin to enable syntax highlighting in code blocks and wire the Enter×3 escape
144
+ // handler that moved to CodeExtension in Lexical 0.44 (#8360).
145
+ function CodeHighlightPlugin() {
146
+ const [editor] = useLexicalComposerContext();
147
+ useEffect(() => {
148
+ return mergeRegister(registerCodeHighlighting(editor), editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
149
+ const selection = $getSelection();
150
+ if (!$isRangeSelection(selection) || !$exitCodeBlockOnEnter(selection))
151
+ return false;
152
+ event?.preventDefault();
153
+ return true;
154
+ }, COMMAND_PRIORITY_LOW));
155
+ }, [editor]);
156
+ return null;
157
+ }
158
+ // Plugin to initialize editor with markdown content
159
+ function InitializePlugin({ content, onOffsets, }) {
160
+ const [editor] = useLexicalComposerContext();
161
+ const hasInitialized = useRef(false);
162
+ useEffect(() => {
163
+ if (hasInitialized.current)
164
+ return;
165
+ hasInitialized.current = true;
166
+ if (content) {
167
+ const { root } = parseMarkdown(content);
168
+ onOffsets(importMarkdownToLexicalWithOffsets(editor, root), content);
169
+ }
170
+ }, [editor, content, onOffsets]);
171
+ return null;
172
+ }
173
+ // Plugin to auto-focus the editor when it mounts
174
+ function AutoFocusPlugin() {
175
+ const [editor] = useLexicalComposerContext();
176
+ const hasFocused = useRef(false);
177
+ useEffect(() => {
178
+ if (hasFocused.current)
179
+ return;
180
+ hasFocused.current = true;
181
+ // Small delay to ensure the editor is fully ready
182
+ const timeoutId = setTimeout(() => {
183
+ const rootElement = editor.getRootElement();
184
+ if (rootElement) {
185
+ // Focus without scrolling
186
+ rootElement.focus({ preventScroll: true });
187
+ }
188
+ }, 100);
189
+ return () => clearTimeout(timeoutId);
190
+ }, [editor]);
191
+ return null;
192
+ }
193
+ // Plugin to handle dynamic editable state changes
194
+ function EditablePlugin({ editable }) {
195
+ const [editor] = useLexicalComposerContext();
196
+ useEffect(() => {
197
+ editor.setEditable(editable);
198
+ }, [editor, editable]);
199
+ return null;
200
+ }
201
+ // Plugin to track cursor position and report to parent
202
+ function CursorTrackingPlugin({ onCursorChange, }) {
203
+ const [editor] = useLexicalComposerContext();
204
+ useEffect(() => {
205
+ if (!onCursorChange)
206
+ return;
207
+ return editor.registerUpdateListener(({ editorState }) => {
208
+ editorState.read(() => {
209
+ const selection = $getSelection();
210
+ if (!$isRangeSelection(selection))
211
+ return;
212
+ const offset = getGlobalOffsetForPoint(selection.anchor);
213
+ if (offset === null)
214
+ return;
215
+ const textContent = $getRoot().getTextContent();
216
+ const CONTEXT_CHARS = 50;
217
+ const contextBefore = textContent.slice(Math.max(0, offset - CONTEXT_CHARS), offset);
218
+ const contextAfter = textContent.slice(offset, Math.min(textContent.length, offset + CONTEXT_CHARS));
219
+ onCursorChange({
220
+ offset,
221
+ contextBefore,
222
+ contextAfter,
223
+ });
224
+ });
225
+ });
226
+ }, [editor, onCursorChange]);
227
+ return null;
228
+ }
229
+ // Plugin to restore cursor after content reload
230
+ function CursorRestorePlugin({ contentVersion, cursorToRestoreRef, }) {
231
+ const [editor] = useLexicalComposerContext();
232
+ const lastRestoredVersion = useRef(contentVersion);
233
+ useEffect(() => {
234
+ // Only restore when contentVersion actually changes (content was reloaded)
235
+ if (contentVersion === lastRestoredVersion.current)
236
+ return;
237
+ lastRestoredVersion.current = contentVersion;
238
+ // Read ref value inside effect, not during render
239
+ const cursorToRestore = cursorToRestoreRef.current;
240
+ if (!cursorToRestore)
241
+ return;
242
+ console.log('[CursorRestorePlugin] Restoring cursor', {
243
+ contentVersion,
244
+ offset: cursorToRestore.offset,
245
+ });
246
+ // Delay slightly to ensure content has been imported
247
+ requestAnimationFrame(() => {
248
+ editor.update(() => {
249
+ const root = $getRoot();
250
+ const textContent = root.getTextContent();
251
+ // Try to find cursor position using context
252
+ let newOffset = findOffsetByContext(textContent, cursorToRestore.offset, cursorToRestore.contextBefore, cursorToRestore.contextAfter);
253
+ // Clamp to valid range
254
+ newOffset = Math.max(0, Math.min(textContent.length, newOffset));
255
+ const point = findTextPointAtOffset(root, newOffset);
256
+ if (point) {
257
+ const selection = $createRangeSelection();
258
+ selection.anchor.set(point.key, point.offset, 'text');
259
+ selection.focus.set(point.key, point.offset, 'text');
260
+ $setSelection(selection);
261
+ console.log('[CursorRestorePlugin] Selection restored at offset', newOffset);
262
+ }
263
+ });
264
+ // Re-focus the editor
265
+ const rootElement = editor.getRootElement();
266
+ if (rootElement) {
267
+ rootElement.focus({ preventScroll: true });
268
+ }
269
+ });
270
+ }, [editor, contentVersion, cursorToRestoreRef]);
271
+ return null;
272
+ }
273
+ // Find best matching offset using context strings
274
+ function findOffsetByContext(text, fallbackOffset, contextBefore, contextAfter) {
275
+ // Try to find the exact junction of before+after context
276
+ if (contextBefore && contextAfter) {
277
+ const combined = contextBefore + contextAfter;
278
+ const idx = text.indexOf(combined);
279
+ if (idx !== -1) {
280
+ return idx + contextBefore.length;
281
+ }
282
+ }
283
+ // Try to find just the before context (cursor at end of it)
284
+ if (contextBefore.length >= 10) {
285
+ const idx = text.indexOf(contextBefore);
286
+ if (idx !== -1) {
287
+ return idx + contextBefore.length;
288
+ }
289
+ }
290
+ // Try to find just the after context (cursor at start of it)
291
+ if (contextAfter.length >= 10) {
292
+ const idx = text.indexOf(contextAfter);
293
+ if (idx !== -1) {
294
+ return idx;
295
+ }
296
+ }
297
+ // Fallback to absolute offset (clamped)
298
+ return fallbackOffset;
299
+ }
300
+ // Plugin to handle content updates from extension host
301
+ // This should ONLY reload when content comes from external source (file open/reload)
302
+ // NOT when content flows back from editor's own onChange
303
+ function ExternalUpdatePlugin({ content, lastExternalLoadRef, currentContentRef, lastEditorChangeRef, onOffsets, }) {
304
+ const [editor] = useLexicalComposerContext();
305
+ useEffect(() => {
306
+ // Skip if content matches what we already have
307
+ if (content === currentContentRef.current) {
308
+ return;
309
+ }
310
+ // Skip if this change likely came from the editor itself (via onChange callback)
311
+ // This prevents the feedback loop: type -> onChange -> prop change -> reload
312
+ const timeSinceEditorChange = Date.now() - lastEditorChangeRef.current;
313
+ if (timeSinceEditorChange < POST_LOAD_SUPPRESS_MS) {
314
+ // Still update the ref so we don't reload on next render either
315
+ currentContentRef.current = content;
316
+ return;
317
+ }
318
+ console.log('[ExternalUpdatePlugin] Reloading content', {
319
+ contentLen: content.length,
320
+ currentLen: currentContentRef.current.length,
321
+ });
322
+ // Mark that we're loading external content
323
+ lastExternalLoadRef.current = Date.now();
324
+ currentContentRef.current = content;
325
+ // Defer the editor update to avoid React's flushSync warning
326
+ // This moves the Lexical update out of React's commit phase
327
+ queueMicrotask(() => {
328
+ // Double-check content still needs updating (user might have typed during microtask)
329
+ if (content !== currentContentRef.current) {
330
+ console.log('[ExternalUpdatePlugin] Skipping stale reload');
331
+ return;
332
+ }
333
+ const { root } = parseMarkdown(content);
334
+ let spans = [];
335
+ editor.update(() => {
336
+ spans = importMarkdownToLexicalInEditorStateWithOffsets(root);
337
+ }, { discrete: true });
338
+ onOffsets(spans, content);
339
+ });
340
+ }, [editor, content, lastExternalLoadRef, currentContentRef, lastEditorChangeRef, onOffsets]);
341
+ return null;
342
+ }
343
+ // Convert a Lexical selection point to a global character offset
344
+ function getGlobalOffsetForPoint(point) {
345
+ const root = $getRoot();
346
+ let currentOffset = 0;
347
+ let found = false;
348
+ const walk = (node) => {
349
+ if (found)
350
+ return;
351
+ if (node.getKey() === point.key) {
352
+ if ($isTextNode(node)) {
353
+ currentOffset += Math.min(point.offset, node.getTextContent().length);
354
+ found = true;
355
+ return;
356
+ }
357
+ if ($isElementNode(node)) {
358
+ const children = node.getChildren();
359
+ const limit = Math.min(point.offset, children.length);
360
+ for (let i = 0; i < limit; i += 1) {
361
+ currentOffset += children[i].getTextContent().length;
362
+ }
363
+ found = true;
364
+ return;
365
+ }
366
+ }
367
+ if ($isTextNode(node)) {
368
+ currentOffset += node.getTextContent().length;
369
+ return;
370
+ }
371
+ if ($isElementNode(node)) {
372
+ for (const child of node.getChildren()) {
373
+ walk(child);
374
+ if (found)
375
+ return;
376
+ }
377
+ }
378
+ };
379
+ walk(root);
380
+ return found ? currentOffset : null;
381
+ }
382
+ // Find a text node and offset for a given global character offset
383
+ function findTextPointAtOffset(root, offset) {
384
+ const textNodes = root.getAllTextNodes();
385
+ if (textNodes.length === 0) {
386
+ return null;
387
+ }
388
+ let remaining = Math.max(0, offset);
389
+ for (const node of textNodes) {
390
+ const length = node.getTextContent().length;
391
+ if (remaining <= length) {
392
+ return { key: node.getKey(), offset: remaining };
393
+ }
394
+ remaining -= length;
395
+ }
396
+ const lastNode = textNodes[textNodes.length - 1];
397
+ return { key: lastNode.getKey(), offset: lastNode.getTextContent().length };
398
+ }
399
+ // Debounce delay in ms - balances responsiveness with performance
400
+ const DEBOUNCE_DELAY = 100;
401
+ // Window after an external content load (file open/reload) or editor-initiated change
402
+ // during which we suppress onChange/reload to avoid feedback loops and normalization noise.
403
+ const POST_LOAD_SUPPRESS_MS = 500;
404
+ export function Editor({ initialContent, autoFocus = false, contentVersion = 0, cursorToRestoreRef, onChange, onCursorChange, onSelectionContextMenu, assetBaseUri, documentDirUri, imagePathResolution, wikiLinkPromotion, resolveLocalAsset, editable = true, filePath, onSubstitutionDetected, sweepRef, annotationKinds, annotations, activeAnnotationId = null, scrollToAnnotation, onCreateAnnotation, onActivateAnnotation, annotationEditorHandleRef, annotationLogger, }) {
405
+ // Annotations are on only when the host configured at least one kind
406
+ // (FR-004). Everything annotation-specific hangs off this flag, including
407
+ // the dynamic import below — with no kinds, none of those modules is ever
408
+ // loaded and no annotation command is registered.
409
+ const annotationsEnabled = !!annotationKinds && Object.keys(annotationKinds).length > 0;
410
+ // Derived, kind-agnostic affordance lists (FR-001/FR-004/FR-009/FR-010):
411
+ // one entry per configured kind on each surface, label falling back to the
412
+ // kind name (FR-003). Plain data only, so `Toolbar`/`SelectionContextMenuPlugin`
413
+ // never need to know about annotation types.
414
+ const toolbarAnnotationAffordances = useMemo(() => {
415
+ if (!annotationKinds)
416
+ return [];
417
+ return Object.entries(annotationKinds)
418
+ .filter(([, config]) => config.createAffordance?.surface === 'toolbar')
419
+ .map(([kind, config]) => ({ kind, label: config.createAffordance?.label ?? kind }));
420
+ }, [annotationKinds]);
421
+ const contextMenuAnnotationAffordances = useMemo(() => {
422
+ if (!annotationKinds)
423
+ return [];
424
+ return Object.entries(annotationKinds)
425
+ .filter(([, config]) => config.createAffordance?.surface === 'contextMenu')
426
+ .map(([kind, config]) => ({ kind, label: config.createAffordance?.label ?? kind }));
427
+ }, [annotationKinds]);
428
+ const offsetSpansRef = useRef([]);
429
+ const markdownTextRef = useRef(initialContent);
430
+ const [offsetsVersion, setOffsetsVersion] = useState(0);
431
+ // Read inside `handleOffsets` rather than closed over, so the callback's
432
+ // identity stays stable — it is an effect dependency in `InitializePlugin`
433
+ // and `ExternalUpdatePlugin`, and a new identity there would re-run them.
434
+ const annotationsEnabledRef = useRef(annotationsEnabled);
435
+ annotationsEnabledRef.current = annotationsEnabled;
436
+ /**
437
+ * Records the offset table each (re)parse produces.
438
+ *
439
+ * The two ref writes are unconditional on purpose. `InitializePlugin` runs
440
+ * its import exactly once, so gating span collection on `annotationsEnabled`
441
+ * left a host that loads its kinds asynchronously with a permanently empty
442
+ * table and silently placed nothing (an earlier review finding). The cost is
443
+ * one small record per text node on a walk that already visits every node.
444
+ *
445
+ * The `offsetsVersion` bump, on the other hand, is a state update that
446
+ * re-renders `Editor` — so it is gated. With annotations off nothing
447
+ * consumes the version, and a host that never passes `annotationKinds`
448
+ * should not re-render on every parse and external reload to maintain a
449
+ * counter nobody reads (review finding, @handarbeit-pruefer). When kinds
450
+ * arrive later, `AnnotationSurface` mounts and its placement effect runs
451
+ * against the refs, which are already current.
452
+ */
453
+ const handleOffsets = useCallback((spans, markdownText) => {
454
+ offsetSpansRef.current = spans;
455
+ markdownTextRef.current = markdownText;
456
+ if (annotationsEnabledRef.current)
457
+ setOffsetsVersion((v) => v + 1);
458
+ }, []);
459
+ const lastExternalLoadRef = useRef(0);
460
+ const currentContentRef = useRef(initialContent);
461
+ const debounceTimerRef = useRef(null);
462
+ const pendingEditorRef = useRef(null);
463
+ // Track when editor-initiated changes happen to prevent feedback loops
464
+ const lastEditorChangeRef = useRef(0);
465
+ // Fallback ref for cursor restore when not provided
466
+ const fallbackCursorRef = useRef(null);
467
+ const effectiveCursorRef = cursorToRestoreRef ?? fallbackCursorRef;
468
+ const assetContextValue = useMemo(() => createAssetContextValue({ assetBaseUri, documentDirUri, imagePathResolution, resolveLocalAsset }), [assetBaseUri, documentDirUri, imagePathResolution, resolveLocalAsset]);
469
+ // Keep a ref to onChange so the unmount flush always calls the latest handler
470
+ // without causing the effect to re-run (and spuriously flush) on re-renders.
471
+ const onChangeRef = useRef(onChange);
472
+ onChangeRef.current = onChange;
473
+ // Same reasoning as onChangeRef: flushPendingChange must stay a stable
474
+ // ([]) callback, so the latest prop value is read through a ref rather than
475
+ // closed over.
476
+ const wikiLinkPromotionRef = useRef(wikiLinkPromotion);
477
+ wikiLinkPromotionRef.current = wikiLinkPromotion;
478
+ // Export pending editor state and propagate if changed.
479
+ // Shared by the debounce timer callback and unmount flush.
480
+ const flushPendingChange = useCallback(() => {
481
+ const pendingEditor = pendingEditorRef.current;
482
+ if (!pendingEditor)
483
+ return;
484
+ const timeSinceLoad = Date.now() - lastExternalLoadRef.current;
485
+ if (timeSinceLoad < POST_LOAD_SUPPRESS_MS)
486
+ return;
487
+ const mdast = exportLexicalToMdast(pendingEditor, { wikiLinkPromotion: wikiLinkPromotionRef.current });
488
+ const markdown = stringifyMarkdown(mdast);
489
+ if (markdown !== currentContentRef.current) {
490
+ currentContentRef.current = markdown;
491
+ lastEditorChangeRef.current = Date.now();
492
+ onChangeRef.current(markdown);
493
+ }
494
+ }, []);
495
+ // Flush pending debounced change on unmount so we don't lose edits
496
+ // (e.g. when switching from WYSIWYG to raw text view)
497
+ useEffect(() => {
498
+ return () => {
499
+ if (debounceTimerRef.current !== null) {
500
+ clearTimeout(debounceTimerRef.current);
501
+ debounceTimerRef.current = null;
502
+ flushPendingChange();
503
+ }
504
+ };
505
+ }, [flushPendingChange]);
506
+ const handleChange = useCallback((editorState, editor) => {
507
+ void editorState;
508
+ // Skip changes that happen right after external content load
509
+ // These are normalization diffs, not user edits
510
+ const timeSinceExternalLoad = Date.now() - lastExternalLoadRef.current;
511
+ if (timeSinceExternalLoad < POST_LOAD_SUPPRESS_MS) {
512
+ return;
513
+ }
514
+ // Store the latest editor for debounced processing
515
+ pendingEditorRef.current = editor;
516
+ // Clear existing timer
517
+ if (debounceTimerRef.current !== null) {
518
+ clearTimeout(debounceTimerRef.current);
519
+ }
520
+ // Debounce the expensive mdast conversion
521
+ debounceTimerRef.current = window.setTimeout(() => {
522
+ debounceTimerRef.current = null;
523
+ flushPendingChange();
524
+ }, DEBOUNCE_DELAY);
525
+ }, [flushPendingChange]);
526
+ const initialConfig = {
527
+ namespace: 'SlashMD',
528
+ theme: editorTheme,
529
+ nodes: editorNodes,
530
+ onError: editorOnError,
531
+ editable,
532
+ };
533
+ return (_jsx(AssetContext.Provider, { value: assetContextValue, children: _jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsx("div", { className: "relative max-w-[800px] ml-5 mr-1 pl-6 pr-6", children: _jsxs("div", { className: "editor-inner relative", children: [_jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "editor-input", "aria-label": "Markdown editor" }), placeholder: _jsx("div", { className: "editor-placeholder", children: "Type '/' for commands..." }), ErrorBoundary: LexicalErrorBoundary }), _jsx(HistoryPlugin, {}), _jsx(ListPlugin, {}), _jsx(CheckListPlugin, {}), _jsx(OrderedTaskListPlugin, {}), _jsx(TabIndentationPlugin, {}), _jsx(LinkPlugin, {}), _jsx(TablePlugin, {}), _jsx(CodeHighlightPlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true }), _jsx(InitializePlugin, { content: initialContent, onOffsets: handleOffsets }), _jsx(EditablePlugin, { editable: editable }), autoFocus && _jsx(AutoFocusPlugin, {}), _jsx(CursorTrackingPlugin, { onCursorChange: onCursorChange }), _jsx(CursorRestorePlugin, { contentVersion: contentVersion, cursorToRestoreRef: effectiveCursorRef }), _jsx(ExternalUpdatePlugin, { content: initialContent, lastExternalLoadRef: lastExternalLoadRef, currentContentRef: currentContentRef, lastEditorChangeRef: lastEditorChangeRef, onOffsets: handleOffsets }), _jsx(SlashMenuPlugin, {}), _jsx(DragHandlePlugin, {}), _jsx(MarkdownShortcutsPlugin, {}), _jsx(TableActionsPlugin, {}), _jsx(CodeBlockPlugin, {}), _jsx(TogglePlugin, {}), _jsx(ImagePlugin, {}), _jsx(BlockClickPlugin, {}), _jsx(LinkClickPlugin, { editable: editable }), _jsx(Toolbar, { annotationAffordances: toolbarAnnotationAffordances }), _jsx(SearchPlugin, {}), _jsx(FrontmatterPlugin, { filePath: filePath }), _jsx(WikiLinkExistencePlugin, {}), _jsx(WikiLinkFormatPlugin, {}), _jsx(AnchorScrollPlugin, {}), _jsx(SelectionContextMenuPlugin, { onSelectionContextMenu: onSelectionContextMenu,
534
+ // Only kinds that actually ask for a context-menu affordance:
535
+ // with none configured, or one on another surface, AnnotationPlugin
536
+ // would decline the command anyway, and dispatching from here would
537
+ // claim an entry point the kind never opted into.
538
+ annotationAffordances: contextMenuAnnotationAffordances }), _jsx(CorrectionPanelPlugin, {}), onSubstitutionDetected && (_jsx(AmbientCorrectionPlugin, { onSubstitutionDetected: onSubstitutionDetected, sweepRef: sweepRef })), annotationsEnabled && (_jsx(Suspense, { fallback: null, children: _jsx(LazyAnnotationSurface, { kinds: annotationKinds, annotations: annotations ?? EMPTY_ANNOTATIONS, activeAnnotationId: activeAnnotationId, scrollToAnnotation: scrollToAnnotation, onCreateAnnotation: onCreateAnnotation, onActivateAnnotation: onActivateAnnotation, editorHandleRef: annotationEditorHandleRef, offsetSpansRef: offsetSpansRef, markdownTextRef: markdownTextRef, offsetsVersion: offsetsVersion, logger: annotationLogger, wikiLinkPromotion: wikiLinkPromotion }) }))] }) }) }) }));
539
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * FrontmatterPlugin - Shows frontmatter in a collapsible tray overlay
3
+ *
4
+ * The actual FrontmatterNode is hidden via CSS but remains in the Lexical tree.
5
+ * This plugin provides a UI to view/edit frontmatter in a sliding tray.
6
+ *
7
+ * The toggle button is rendered externally (in the editor header).
8
+ * This plugin communicates via Zustand store for state management.
9
+ *
10
+ * For .mdc files (Cursor rules), a specialized form UI is shown with:
11
+ * - alwaysApply dropdown (true/false)
12
+ * - description text input
13
+ * - globs array editor
14
+ */
15
+ interface FrontmatterPluginProps {
16
+ filePath?: string;
17
+ }
18
+ export declare function FrontmatterPlugin({ filePath }: FrontmatterPluginProps): import("react").JSX.Element | null;
19
+ export {};