@nkzw/mdx-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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/UPSTREAM.md +21 -0
  4. package/dist/EditorIcon.js +75 -0
  5. package/dist/FormatConstants.js +20 -0
  6. package/dist/MDXEditor.js +189 -0
  7. package/dist/MarkdownEditor.js +281 -0
  8. package/dist/PersistentMarkdownEditor.js +358 -0
  9. package/dist/RealmWithPlugins.js +35 -0
  10. package/dist/core.d.ts +3232 -0
  11. package/dist/core.js +354 -0
  12. package/dist/defaultSvgIcons.js +371 -0
  13. package/dist/directive-editors/AdmonitionDirectiveDescriptor.js +28 -0
  14. package/dist/directive-editors/GenericDirectiveEditor.js +37 -0
  15. package/dist/exportMarkdownFromLexical.js +262 -0
  16. package/dist/horizontalRuleShortcut.js +37 -0
  17. package/dist/importMarkdownToLexical.js +172 -0
  18. package/dist/index.d.ts +86 -0
  19. package/dist/index.js +8 -0
  20. package/dist/jsx-editors/GenericJsxEditor.js +84 -0
  21. package/dist/mdastUtilHtmlComment.js +125 -0
  22. package/dist/persistence.d.ts +128 -0
  23. package/dist/persistence.js +4 -0
  24. package/dist/plugins/codeblock/CodeBlockNode.js +183 -0
  25. package/dist/plugins/codeblock/CodeBlockVisitor.js +14 -0
  26. package/dist/plugins/codeblock/MdastCodeVisitor.js +23 -0
  27. package/dist/plugins/codeblock/findCodeBlockDescriptor.js +8 -0
  28. package/dist/plugins/codeblock/index.js +46 -0
  29. package/dist/plugins/codemirror/CodeMirrorEditor.js +145 -0
  30. package/dist/plugins/codemirror/index.js +115 -0
  31. package/dist/plugins/codemirror/useCodeMirrorRef.js +101 -0
  32. package/dist/plugins/core/GenericHTMLNode.js +118 -0
  33. package/dist/plugins/core/LexicalGenericHTMLNodeVisitor.js +15 -0
  34. package/dist/plugins/core/LexicalLinebreakVisitor.js +10 -0
  35. package/dist/plugins/core/LexicalParagraphVisitor.js +10 -0
  36. package/dist/plugins/core/LexicalRootVisitor.js +10 -0
  37. package/dist/plugins/core/LexicalTextVisitor.js +160 -0
  38. package/dist/plugins/core/MdastBreakVisitor.js +10 -0
  39. package/dist/plugins/core/MdastFormattingVisitor.js +81 -0
  40. package/dist/plugins/core/MdastHTMLNode.js +120 -0
  41. package/dist/plugins/core/MdastHTMLVisitor.js +17 -0
  42. package/dist/plugins/core/MdastParagraphVisitor.js +23 -0
  43. package/dist/plugins/core/MdastRootVisitor.js +9 -0
  44. package/dist/plugins/core/MdastTextVisitor.js +16 -0
  45. package/dist/plugins/core/NestedLexicalEditor.js +221 -0
  46. package/dist/plugins/core/PropertyPopover.js +75 -0
  47. package/dist/plugins/core/SharedHistoryPlugin.js +10 -0
  48. package/dist/plugins/core/index.js +692 -0
  49. package/dist/plugins/core/ui/DownshiftAutoComplete.js +89 -0
  50. package/dist/plugins/core/ui/PopoverUtils.js +22 -0
  51. package/dist/plugins/diff-source/DiffSourceWrapper.js +24 -0
  52. package/dist/plugins/diff-source/DiffViewer.js +84 -0
  53. package/dist/plugins/diff-source/SourceEditor.js +60 -0
  54. package/dist/plugins/diff-source/index.js +27 -0
  55. package/dist/plugins/directives/DirectiveNode.js +107 -0
  56. package/dist/plugins/directives/DirectiveVisitor.js +10 -0
  57. package/dist/plugins/directives/MdastDirectiveVisitor.js +30 -0
  58. package/dist/plugins/directives/index.js +45 -0
  59. package/dist/plugins/frontmatter/FrontmatterEditor.js +137 -0
  60. package/dist/plugins/frontmatter/FrontmatterNode.js +70 -0
  61. package/dist/plugins/frontmatter/LexicalFrontmatterVisitor.js +10 -0
  62. package/dist/plugins/frontmatter/MdastFrontmatterVisitor.js +10 -0
  63. package/dist/plugins/frontmatter/index.js +113 -0
  64. package/dist/plugins/headings/LexicalHeadingVisitor.js +11 -0
  65. package/dist/plugins/headings/MdastHeadingVisitor.js +10 -0
  66. package/dist/plugins/headings/index.js +63 -0
  67. package/dist/plugins/image/EditImageToolbar.js +58 -0
  68. package/dist/plugins/image/ImageDialog.js +132 -0
  69. package/dist/plugins/image/ImageEditor.js +279 -0
  70. package/dist/plugins/image/ImageNode.js +187 -0
  71. package/dist/plugins/image/ImagePlaceholder.js +9 -0
  72. package/dist/plugins/image/ImageResizer.js +223 -0
  73. package/dist/plugins/image/LexicalImageVisitor.js +42 -0
  74. package/dist/plugins/image/MdastImageVisitor.js +91 -0
  75. package/dist/plugins/image/index.js +364 -0
  76. package/dist/plugins/jsx/LexicalJsxNode.js +103 -0
  77. package/dist/plugins/jsx/LexicalJsxVisitor.js +27 -0
  78. package/dist/plugins/jsx/LexicalMdxExpressionNode.js +130 -0
  79. package/dist/plugins/jsx/LexicalMdxExpressionVisitor.js +14 -0
  80. package/dist/plugins/jsx/MdastMdxExpressionVisitor.js +11 -0
  81. package/dist/plugins/jsx/MdastMdxJsEsmVisitor.js +8 -0
  82. package/dist/plugins/jsx/MdastMdxJsxElementVisitor.js +28 -0
  83. package/dist/plugins/jsx/index.js +97 -0
  84. package/dist/plugins/jsx/jsxTagName.js +7 -0
  85. package/dist/plugins/link/AutoLinkPlugin.js +18 -0
  86. package/dist/plugins/link/LexicalLinkVisitor.js +10 -0
  87. package/dist/plugins/link/MdastLinkVisitor.js +14 -0
  88. package/dist/plugins/link/index.js +34 -0
  89. package/dist/plugins/link-dialog/LinkDialog.js +262 -0
  90. package/dist/plugins/link-dialog/index.js +304 -0
  91. package/dist/plugins/lists/CheckListPlugin.js +270 -0
  92. package/dist/plugins/lists/LexicalListItemVisitor.js +41 -0
  93. package/dist/plugins/lists/LexicalListVisitor.js +13 -0
  94. package/dist/plugins/lists/MdastListItemVisitor.js +11 -0
  95. package/dist/plugins/lists/MdastListVisitor.js +19 -0
  96. package/dist/plugins/lists/NotesListItemNode.js +22 -0
  97. package/dist/plugins/lists/index.js +111 -0
  98. package/dist/plugins/markdown-shortcut/index.js +114 -0
  99. package/dist/plugins/maxlength/index.js +36 -0
  100. package/dist/plugins/quote/LexicalQuoteVisitor.js +10 -0
  101. package/dist/plugins/quote/MdastBlockQuoteVisitor.js +10 -0
  102. package/dist/plugins/quote/index.js +18 -0
  103. package/dist/plugins/remote/index.js +52 -0
  104. package/dist/plugins/search/index.js +360 -0
  105. package/dist/plugins/table/LexicalTableVisitor.js +10 -0
  106. package/dist/plugins/table/MdastTableVisitor.js +10 -0
  107. package/dist/plugins/table/TableEditor.js +527 -0
  108. package/dist/plugins/table/TableNode.js +208 -0
  109. package/dist/plugins/table/index.js +66 -0
  110. package/dist/plugins/thematic-break/LexicalThematicBreakVisitor.js +10 -0
  111. package/dist/plugins/thematic-break/MdastThematicBreakVisitor.js +10 -0
  112. package/dist/plugins/thematic-break/index.js +27 -0
  113. package/dist/plugins/toolbar/components/BlockTypeSelect.js +62 -0
  114. package/dist/plugins/toolbar/components/BoldItalicUnderlineToggles.js +98 -0
  115. package/dist/plugins/toolbar/components/ChangeAdmonitionType.js +43 -0
  116. package/dist/plugins/toolbar/components/ChangeCodeMirrorLanguage.js +42 -0
  117. package/dist/plugins/toolbar/components/CodeToggle.js +21 -0
  118. package/dist/plugins/toolbar/components/CreateLink.js +24 -0
  119. package/dist/plugins/toolbar/components/DiffSourceToggleWrapper.js +42 -0
  120. package/dist/plugins/toolbar/components/HighlightToggle.js +28 -0
  121. package/dist/plugins/toolbar/components/InsertAdmonition.js +34 -0
  122. package/dist/plugins/toolbar/components/InsertCodeBlock.js +23 -0
  123. package/dist/plugins/toolbar/components/InsertFrontmatter.js +28 -0
  124. package/dist/plugins/toolbar/components/InsertImage.js +29 -0
  125. package/dist/plugins/toolbar/components/InsertTable.js +25 -0
  126. package/dist/plugins/toolbar/components/InsertThematicBreak.js +23 -0
  127. package/dist/plugins/toolbar/components/KitchenSinkToolbar.js +82 -0
  128. package/dist/plugins/toolbar/components/ListsToggle.js +29 -0
  129. package/dist/plugins/toolbar/components/UndoRedo.js +60 -0
  130. package/dist/plugins/toolbar/index.js +32 -0
  131. package/dist/plugins/toolbar/primitives/DialogButton.js +130 -0
  132. package/dist/plugins/toolbar/primitives/TooltipWrap.js +17 -0
  133. package/dist/plugins/toolbar/primitives/select.js +76 -0
  134. package/dist/plugins/toolbar/primitives/toolbar.js +144 -0
  135. package/dist/registerCodeBoundaryEscape.js +40 -0
  136. package/dist/styles/lexical-theme.module.css.js +62 -0
  137. package/dist/styles/lexicalTheme.js +32 -0
  138. package/dist/styles/ui.module.css.js +296 -0
  139. package/dist/styles.css +2838 -0
  140. package/dist/utils/detectMac.js +16 -0
  141. package/dist/utils/fp.js +44 -0
  142. package/dist/utils/isPartOftheEditorUI.js +12 -0
  143. package/dist/utils/lexicalHelpers.js +185 -0
  144. package/dist/utils/makeHslTransparent.js +6 -0
  145. package/dist/utils/mergeStyleAttributes.js +22 -0
  146. package/dist/utils/uuid4.js +10 -0
  147. package/dist/utils/voidEmitter.js +15 -0
  148. package/package.json +133 -0
@@ -0,0 +1,281 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useRef, useMemo, useEffect, useImperativeHandle, useState } from "react";
3
+ import { horizontalRuleOnEnterPlugin } from "./horizontalRuleShortcut.js";
4
+ import { MDXEditor } from "./MDXEditor.js";
5
+ import { codeBlockPlugin } from "./plugins/codeblock/index.js";
6
+ import { codeMirrorPlugin } from "./plugins/codemirror/index.js";
7
+ import { headingsPlugin } from "./plugins/headings/index.js";
8
+ import { linkDialogPlugin } from "./plugins/link-dialog/index.js";
9
+ import { linkPlugin } from "./plugins/link/index.js";
10
+ import { listsPlugin } from "./plugins/lists/index.js";
11
+ import { markdownShortcutPlugin } from "./plugins/markdown-shortcut/index.js";
12
+ import { quotePlugin } from "./plugins/quote/index.js";
13
+ import { tablePlugin } from "./plugins/table/index.js";
14
+ import { thematicBreakPlugin } from "./plugins/thematic-break/index.js";
15
+ const canonicalMarkdownOptions = {
16
+ bullet: "-",
17
+ emphasis: "*",
18
+ fences: true,
19
+ listItemIndent: "one",
20
+ rule: "-",
21
+ strong: "*"
22
+ };
23
+ const emptyPlugins = [];
24
+ const cx = (...classes) => classes.filter(Boolean).join(" ");
25
+ const getSystemColorScheme = () => {
26
+ if (typeof window === "undefined") {
27
+ return "light";
28
+ }
29
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
30
+ };
31
+ const getInheritedColorScheme = () => {
32
+ if (typeof document === "undefined") {
33
+ return "light";
34
+ }
35
+ const configuredTheme = document.documentElement.dataset.theme;
36
+ return configuredTheme === "dark" || configuredTheme === "light" ? configuredTheme : getSystemColorScheme();
37
+ };
38
+ const useResolvedColorScheme = (colorScheme) => {
39
+ const getResolved = () => colorScheme === "system" ? getSystemColorScheme() : colorScheme === "inherit" ? getInheritedColorScheme() : colorScheme;
40
+ const [resolved, setResolved] = useState(getResolved);
41
+ useEffect(() => {
42
+ setResolved(getResolved());
43
+ }, [colorScheme]);
44
+ useEffect(() => {
45
+ if (typeof window === "undefined" || colorScheme !== "system" && colorScheme !== "inherit") {
46
+ return;
47
+ }
48
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
49
+ const update = () => setResolved(getResolved());
50
+ const observer = colorScheme === "inherit" && typeof MutationObserver !== "undefined" ? new MutationObserver(update) : null;
51
+ media.addEventListener("change", update);
52
+ observer?.observe(document.documentElement, {
53
+ attributeFilter: ["data-theme"],
54
+ attributes: true
55
+ });
56
+ return () => {
57
+ media.removeEventListener("change", update);
58
+ observer?.disconnect();
59
+ };
60
+ }, [colorScheme]);
61
+ return resolved;
62
+ };
63
+ const openHref = (href, {
64
+ onNavigate,
65
+ onOpenExternalLink,
66
+ resolveLink
67
+ }) => {
68
+ const resolved = resolveLink?.(href);
69
+ if (resolved) {
70
+ onNavigate?.(resolved);
71
+ return;
72
+ }
73
+ if (!/^https?:\/\//.test(href)) {
74
+ return;
75
+ }
76
+ if (onOpenExternalLink) {
77
+ onOpenExternalLink(href);
78
+ } else if (typeof window !== "undefined") {
79
+ window.open(href, "_blank", "noopener,noreferrer");
80
+ }
81
+ };
82
+ const createDefaultPlugins = (onClickLink) => [
83
+ headingsPlugin(),
84
+ listsPlugin(),
85
+ quotePlugin(),
86
+ linkPlugin(),
87
+ linkDialogPlugin({
88
+ onClickLinkCallback: onClickLink
89
+ }),
90
+ tablePlugin(),
91
+ thematicBreakPlugin(),
92
+ codeBlockPlugin({
93
+ defaultCodeBlockLanguage: "txt"
94
+ }),
95
+ codeMirrorPlugin({
96
+ autoLoadLanguageSupport: false,
97
+ codeBlockLanguages: {
98
+ css: "CSS",
99
+ html: "HTML",
100
+ js: "JavaScript",
101
+ json: "JSON",
102
+ jsx: "JSX",
103
+ md: "Markdown",
104
+ sh: "Shell",
105
+ ts: "TypeScript",
106
+ tsx: "TSX",
107
+ txt: "Plain text"
108
+ }
109
+ }),
110
+ markdownShortcutPlugin(),
111
+ horizontalRuleOnEnterPlugin()
112
+ ];
113
+ const MarkdownEditor = forwardRef(
114
+ function MarkdownEditor2({
115
+ additionalPlugins = emptyPlugins,
116
+ ariaLabel,
117
+ autoFocus = false,
118
+ className,
119
+ colorScheme = "system",
120
+ contentClassName,
121
+ defaultValue = "",
122
+ density = "document",
123
+ minHeight,
124
+ onBlur,
125
+ onChange,
126
+ onError,
127
+ onFocus,
128
+ onHeightChange,
129
+ onKeyDown,
130
+ onNavigate,
131
+ onOpenExternalLink,
132
+ overlayContainer,
133
+ placeholder,
134
+ readOnly = false,
135
+ resolveLink,
136
+ spellCheck = true,
137
+ toMarkdownOptions = canonicalMarkdownOptions,
138
+ value,
139
+ variant = "card"
140
+ }, forwardedRef) {
141
+ const initialMarkdownRef = useRef(value ?? defaultValue);
142
+ const currentMarkdownRef = useRef(initialMarkdownRef.current);
143
+ const editorRef = useRef(null);
144
+ const shellRef = useRef(null);
145
+ const handlersRef = useRef({
146
+ onNavigate,
147
+ onOpenExternalLink,
148
+ resolveLink
149
+ });
150
+ handlersRef.current = { onNavigate, onOpenExternalLink, resolveLink };
151
+ const resolvedColorScheme = useResolvedColorScheme(colorScheme);
152
+ const plugins = useMemo(
153
+ () => [
154
+ ...createDefaultPlugins((href) => openHref(href, handlersRef.current)),
155
+ ...additionalPlugins
156
+ ],
157
+ [additionalPlugins]
158
+ );
159
+ useEffect(() => {
160
+ if (value === void 0 || value === currentMarkdownRef.current) {
161
+ return;
162
+ }
163
+ currentMarkdownRef.current = value;
164
+ editorRef.current?.setMarkdown(value);
165
+ }, [value]);
166
+ useEffect(() => {
167
+ const contentEditable = shellRef.current?.querySelector(
168
+ ".mdx-editor-content[contenteditable]"
169
+ );
170
+ if (contentEditable && ariaLabel) {
171
+ contentEditable.setAttribute("aria-label", ariaLabel);
172
+ }
173
+ }, [ariaLabel]);
174
+ useEffect(() => {
175
+ const element = shellRef.current;
176
+ if (!element || !onHeightChange || typeof ResizeObserver === "undefined") {
177
+ return;
178
+ }
179
+ let previousHeight = -1;
180
+ const observer = new ResizeObserver(([entry]) => {
181
+ const height = entry?.borderBoxSize[0]?.blockSize ?? entry?.contentRect.height;
182
+ if (height !== void 0 && height !== previousHeight) {
183
+ previousHeight = height;
184
+ onHeightChange(height);
185
+ }
186
+ });
187
+ observer.observe(element);
188
+ return () => observer.disconnect();
189
+ }, [onHeightChange]);
190
+ useImperativeHandle(
191
+ forwardedRef,
192
+ () => ({
193
+ focus(options) {
194
+ editorRef.current?.focus(void 0, options);
195
+ },
196
+ getMarkdown() {
197
+ return editorRef.current?.getMarkdown() ?? currentMarkdownRef.current;
198
+ },
199
+ getSelectionMarkdown() {
200
+ return editorRef.current?.getSelectionMarkdown() ?? "";
201
+ },
202
+ insertMarkdown(markdown) {
203
+ editorRef.current?.insertMarkdown(markdown);
204
+ },
205
+ setMarkdown(markdown) {
206
+ currentMarkdownRef.current = markdown;
207
+ editorRef.current?.setMarkdown(markdown);
208
+ }
209
+ }),
210
+ []
211
+ );
212
+ const handleChange = (markdown, initialMarkdownNormalize) => {
213
+ currentMarkdownRef.current = markdown;
214
+ if (!initialMarkdownNormalize) {
215
+ onChange?.(markdown);
216
+ }
217
+ };
218
+ const handleLinkClick = (event) => {
219
+ const target = event.target;
220
+ const anchor = target.closest("a");
221
+ if (!anchor?.closest(".mdx-editor-content")) {
222
+ return;
223
+ }
224
+ event.preventDefault();
225
+ if (!event.metaKey && !event.ctrlKey) {
226
+ return;
227
+ }
228
+ const href = anchor.getAttribute("href");
229
+ if (href) {
230
+ openHref(href, handlersRef.current);
231
+ }
232
+ };
233
+ const rootClassName = cx(
234
+ "mdx-editor",
235
+ `mdx-editor-${density}`,
236
+ `mdx-editor-${variant}`,
237
+ resolvedColorScheme === "dark" && "dark-theme",
238
+ className
239
+ );
240
+ return /* @__PURE__ */ jsx(
241
+ "div",
242
+ {
243
+ className: cx("mdx-editor-shell", className),
244
+ "data-color-scheme": resolvedColorScheme,
245
+ "data-density": density,
246
+ "data-variant": variant,
247
+ onClickCapture: handleLinkClick,
248
+ onFocusCapture: onFocus,
249
+ onKeyDownCapture: onKeyDown,
250
+ ref: shellRef,
251
+ style: minHeight === void 0 ? void 0 : { minHeight },
252
+ children: /* @__PURE__ */ jsx(
253
+ MDXEditor,
254
+ {
255
+ autoFocus,
256
+ className: rootClassName,
257
+ contentEditableClassName: cx(
258
+ "mdx-editor-content",
259
+ contentClassName
260
+ ),
261
+ markdown: initialMarkdownRef.current,
262
+ onBlur,
263
+ onChange: handleChange,
264
+ onError: ({ error }) => onError?.(new Error(error)),
265
+ overlayContainer,
266
+ placeholder,
267
+ plugins,
268
+ readOnly,
269
+ ref: editorRef,
270
+ spellCheck,
271
+ toMarkdownOptions,
272
+ trim: false
273
+ }
274
+ )
275
+ }
276
+ );
277
+ }
278
+ );
279
+ export {
280
+ MarkdownEditor
281
+ };
@@ -0,0 +1,358 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useRef, useState, useImperativeHandle, useEffect } from "react";
3
+ import { MarkdownEditor } from "./MarkdownEditor.js";
4
+ const defaultFromStorageContent = (content) => content.endsWith("\n") ? content.slice(0, -1) : content;
5
+ const defaultToStorageContent = (content) => content.endsWith("\n") ? content : `${content}
6
+ `;
7
+ const clearTimer = (timer) => {
8
+ if (timer) {
9
+ clearTimeout(timer);
10
+ }
11
+ };
12
+ function PersistentMarkdownEditorInner({
13
+ adapter,
14
+ debounceMs = 50,
15
+ document,
16
+ fromStorageContent = defaultFromStorageContent,
17
+ lifecycleFlush = true,
18
+ maxWaitMs = 250,
19
+ onBlur,
20
+ onDocumentChange,
21
+ onError,
22
+ onLocalChange,
23
+ onStatusChange,
24
+ toStorageContent = defaultToStorageContent,
25
+ ...editorProps
26
+ }, forwardedRef) {
27
+ const editorRef = useRef(null);
28
+ const callbacksRef = useRef({
29
+ adapter,
30
+ fromStorageContent,
31
+ onDocumentChange,
32
+ onError,
33
+ onLocalChange,
34
+ onStatusChange,
35
+ toStorageContent
36
+ });
37
+ callbacksRef.current = {
38
+ adapter,
39
+ fromStorageContent,
40
+ onDocumentChange,
41
+ onError,
42
+ onLocalChange,
43
+ onStatusChange,
44
+ toStorageContent
45
+ };
46
+ const initialContent = fromStorageContent(document.content);
47
+ const [value, setValue] = useState(initialContent);
48
+ const [conflictDocument, setConflictDocument] = useState(null);
49
+ const [errorMessage, setErrorMessage] = useState(null);
50
+ const sessionRef = useRef({
51
+ conflict: null,
52
+ document,
53
+ inFlight: null,
54
+ inFlightContent: null,
55
+ latestContent: initialContent,
56
+ maxWaitTimer: null,
57
+ savedContent: initialContent,
58
+ trailingTimer: null
59
+ });
60
+ const setStatus = (status) => {
61
+ callbacksRef.current.onStatusChange?.(status);
62
+ };
63
+ const clearSaveTimers = () => {
64
+ const session = sessionRef.current;
65
+ clearTimer(session.trailingTimer);
66
+ clearTimer(session.maxWaitTimer);
67
+ session.trailingTimer = null;
68
+ session.maxWaitTimer = null;
69
+ };
70
+ const hasUnsavedChanges = () => {
71
+ const session = sessionRef.current;
72
+ return session.latestContent !== session.savedContent || session.inFlight !== null;
73
+ };
74
+ const saveOnce = async (keepalive) => {
75
+ const session = sessionRef.current;
76
+ if (session.conflict) {
77
+ return false;
78
+ }
79
+ const editorContent = session.latestContent;
80
+ if (editorContent === session.savedContent) {
81
+ setStatus("saved");
82
+ return true;
83
+ }
84
+ setErrorMessage(null);
85
+ setStatus("saving");
86
+ session.inFlightContent = editorContent;
87
+ const request = callbacksRef.current.adapter.save({
88
+ content: callbacksRef.current.toStorageContent(editorContent),
89
+ document: session.document,
90
+ keepalive
91
+ }).then((result) => {
92
+ const storedContent = callbacksRef.current.fromStorageContent(
93
+ result.document.content
94
+ );
95
+ if (result.status === "conflict" && storedContent !== editorContent && storedContent !== session.latestContent && storedContent !== session.savedContent) {
96
+ session.conflict = result.document;
97
+ setConflictDocument(result.document);
98
+ setStatus("conflict");
99
+ return false;
100
+ }
101
+ session.document = result.document;
102
+ session.savedContent = result.status === "saved" ? editorContent : storedContent;
103
+ callbacksRef.current.onDocumentChange?.(result.document);
104
+ setStatus(
105
+ session.latestContent === session.savedContent ? "saved" : "unsaved"
106
+ );
107
+ return true;
108
+ }).catch((error) => {
109
+ const normalizedError = error instanceof Error ? error : new Error("Failed to save the Markdown document.");
110
+ setErrorMessage(normalizedError.message);
111
+ callbacksRef.current.onError?.(normalizedError);
112
+ setStatus("error");
113
+ return false;
114
+ }).finally(() => {
115
+ session.inFlight = null;
116
+ session.inFlightContent = null;
117
+ });
118
+ session.inFlight = request;
119
+ return request;
120
+ };
121
+ const flush = async ({ keepalive = false } = {}) => {
122
+ clearSaveTimers();
123
+ const session = sessionRef.current;
124
+ while (session.latestContent !== session.savedContent) {
125
+ if (session.conflict) {
126
+ return false;
127
+ }
128
+ if (session.inFlight) {
129
+ if (!await session.inFlight) {
130
+ return false;
131
+ }
132
+ continue;
133
+ }
134
+ if (!await saveOnce(keepalive)) {
135
+ return false;
136
+ }
137
+ }
138
+ if (session.inFlight) {
139
+ return session.inFlight;
140
+ }
141
+ setStatus("saved");
142
+ return true;
143
+ };
144
+ const scheduleSave = () => {
145
+ const session = sessionRef.current;
146
+ clearTimer(session.trailingTimer);
147
+ session.trailingTimer = setTimeout(() => {
148
+ void flush();
149
+ }, debounceMs);
150
+ if (!session.maxWaitTimer) {
151
+ session.maxWaitTimer = setTimeout(() => {
152
+ void flush();
153
+ }, maxWaitMs);
154
+ }
155
+ };
156
+ const applyExternalChange = (nextDocument) => {
157
+ const session = sessionRef.current;
158
+ const diskContent = callbacksRef.current.fromStorageContent(nextDocument.content);
159
+ if (nextDocument.version === session.document.version) {
160
+ return;
161
+ }
162
+ if (diskContent === session.savedContent || diskContent === session.latestContent || diskContent === session.inFlightContent) {
163
+ session.document = nextDocument;
164
+ session.savedContent = diskContent;
165
+ callbacksRef.current.onDocumentChange?.(nextDocument);
166
+ if (session.latestContent === session.savedContent) {
167
+ clearSaveTimers();
168
+ setStatus("saved");
169
+ } else {
170
+ setStatus(session.inFlight ? "saving" : "unsaved");
171
+ }
172
+ return;
173
+ }
174
+ if (session.latestContent !== session.savedContent || session.inFlight !== null) {
175
+ session.conflict = nextDocument;
176
+ setConflictDocument(nextDocument);
177
+ setStatus("conflict");
178
+ return;
179
+ }
180
+ clearSaveTimers();
181
+ session.document = nextDocument;
182
+ session.latestContent = diskContent;
183
+ session.savedContent = diskContent;
184
+ callbacksRef.current.onDocumentChange?.(nextDocument);
185
+ setValue(diskContent);
186
+ setStatus("saved");
187
+ };
188
+ useImperativeHandle(
189
+ forwardedRef,
190
+ () => ({
191
+ applyExternalChange,
192
+ flush,
193
+ focus(options) {
194
+ editorRef.current?.focus(options);
195
+ },
196
+ getMarkdown() {
197
+ return editorRef.current?.getMarkdown() ?? sessionRef.current.latestContent;
198
+ },
199
+ getSelectionMarkdown() {
200
+ return editorRef.current?.getSelectionMarkdown() ?? "";
201
+ },
202
+ hasUnsavedChanges,
203
+ insertMarkdown(markdown) {
204
+ editorRef.current?.insertMarkdown(markdown);
205
+ },
206
+ setMarkdown(markdown) {
207
+ sessionRef.current.latestContent = markdown;
208
+ setValue(markdown);
209
+ }
210
+ })
211
+ );
212
+ useEffect(() => {
213
+ const session = sessionRef.current;
214
+ if (document.id !== session.document.id) {
215
+ clearSaveTimers();
216
+ const nextContent = callbacksRef.current.fromStorageContent(document.content);
217
+ session.conflict = null;
218
+ session.document = document;
219
+ session.inFlight = null;
220
+ session.inFlightContent = null;
221
+ session.latestContent = nextContent;
222
+ session.savedContent = nextContent;
223
+ setConflictDocument(null);
224
+ setErrorMessage(null);
225
+ setValue(nextContent);
226
+ setStatus("saved");
227
+ } else if (document.version !== session.document.version) {
228
+ applyExternalChange(document);
229
+ }
230
+ }, [document]);
231
+ useEffect(() => {
232
+ setStatus("saved");
233
+ if (!lifecycleFlush || typeof window === "undefined") {
234
+ return () => clearSaveTimers();
235
+ }
236
+ const onVisibilityChange = () => {
237
+ if (globalThis.document.visibilityState === "hidden" && hasUnsavedChanges()) {
238
+ void flush({ keepalive: true });
239
+ }
240
+ };
241
+ const onPageHide = () => {
242
+ if (hasUnsavedChanges()) {
243
+ void flush({ keepalive: true });
244
+ }
245
+ };
246
+ const onBeforeUnload = (event) => {
247
+ if (!hasUnsavedChanges()) {
248
+ return;
249
+ }
250
+ void flush({ keepalive: true });
251
+ event.preventDefault();
252
+ event.returnValue = "";
253
+ };
254
+ globalThis.document.addEventListener("visibilitychange", onVisibilityChange);
255
+ window.addEventListener("beforeunload", onBeforeUnload);
256
+ window.addEventListener("pagehide", onPageHide);
257
+ return () => {
258
+ globalThis.document.removeEventListener(
259
+ "visibilitychange",
260
+ onVisibilityChange
261
+ );
262
+ window.removeEventListener("beforeunload", onBeforeUnload);
263
+ window.removeEventListener("pagehide", onPageHide);
264
+ clearSaveTimers();
265
+ };
266
+ }, [lifecycleFlush]);
267
+ const handleChange = (markdown) => {
268
+ const session = sessionRef.current;
269
+ session.latestContent = markdown;
270
+ setValue(markdown);
271
+ callbacksRef.current.onLocalChange?.(markdown);
272
+ if (markdown === session.savedContent) {
273
+ clearSaveTimers();
274
+ setStatus("saved");
275
+ return;
276
+ }
277
+ setStatus("unsaved");
278
+ scheduleSave();
279
+ };
280
+ const useDiskVersion = () => {
281
+ const remote = conflictDocument;
282
+ if (!remote) {
283
+ return;
284
+ }
285
+ const session = sessionRef.current;
286
+ const remoteContent = callbacksRef.current.fromStorageContent(remote.content);
287
+ clearSaveTimers();
288
+ session.conflict = null;
289
+ session.document = remote;
290
+ session.latestContent = remoteContent;
291
+ session.savedContent = remoteContent;
292
+ setConflictDocument(null);
293
+ setErrorMessage(null);
294
+ callbacksRef.current.onDocumentChange?.(remote);
295
+ setValue(remoteContent);
296
+ setStatus("saved");
297
+ };
298
+ const keepMyVersion = () => {
299
+ const remote = conflictDocument;
300
+ if (!remote) {
301
+ return;
302
+ }
303
+ const session = sessionRef.current;
304
+ session.conflict = null;
305
+ session.document = remote;
306
+ session.savedContent = callbacksRef.current.fromStorageContent(remote.content);
307
+ setConflictDocument(null);
308
+ setErrorMessage(null);
309
+ setStatus("unsaved");
310
+ void flush();
311
+ };
312
+ return /* @__PURE__ */ jsxs(
313
+ "div",
314
+ {
315
+ className: [
316
+ "mdx-editor-persistent",
317
+ editorProps.className
318
+ ].filter(Boolean).join(" "),
319
+ children: [
320
+ conflictDocument ? /* @__PURE__ */ jsxs("div", { className: "mdx-editor-notice", "data-kind": "conflict", role: "alert", children: [
321
+ /* @__PURE__ */ jsx("span", { children: "The file on disk changed while you were editing." }),
322
+ /* @__PURE__ */ jsxs("div", { children: [
323
+ /* @__PURE__ */ jsx("button", { onClick: useDiskVersion, type: "button", children: "Use disk version" }),
324
+ /* @__PURE__ */ jsx("button", { onClick: keepMyVersion, type: "button", children: "Keep my version" })
325
+ ] })
326
+ ] }) : null,
327
+ errorMessage ? /* @__PURE__ */ jsxs("div", { className: "mdx-editor-notice", "data-kind": "error", role: "alert", children: [
328
+ /* @__PURE__ */ jsx("span", { children: errorMessage }),
329
+ /* @__PURE__ */ jsx("button", { onClick: () => void flush(), type: "button", children: "Retry" })
330
+ ] }) : null,
331
+ /* @__PURE__ */ jsx(
332
+ MarkdownEditor,
333
+ {
334
+ ...editorProps,
335
+ onBlur: (event) => {
336
+ onBlur?.(event);
337
+ void flush();
338
+ },
339
+ onChange: handleChange,
340
+ onError: (error) => {
341
+ setErrorMessage(error.message);
342
+ callbacksRef.current.onError?.(error);
343
+ setStatus("error");
344
+ },
345
+ ref: editorRef,
346
+ value
347
+ }
348
+ )
349
+ ]
350
+ }
351
+ );
352
+ }
353
+ const PersistentMarkdownEditor = forwardRef(
354
+ PersistentMarkdownEditorInner
355
+ );
356
+ export {
357
+ PersistentMarkdownEditor
358
+ };
@@ -0,0 +1,35 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Realm, RealmContext } from "@mdxeditor/gurx";
4
+ import { tap } from "./utils/fp.js";
5
+ function realmPlugin(plugin) {
6
+ return function(params) {
7
+ return {
8
+ init: (realm) => plugin.init?.(realm, params),
9
+ postInit: (realm) => plugin.postInit?.(realm, params),
10
+ update: (realm) => plugin.update?.(realm, params)
11
+ };
12
+ };
13
+ }
14
+ function RealmWithPlugins({ children, plugins }) {
15
+ const theRealm = React.useMemo(() => {
16
+ return tap(new Realm(), (r) => {
17
+ for (const plugin of plugins) {
18
+ plugin.init?.(r);
19
+ }
20
+ for (const plugin of plugins) {
21
+ plugin.postInit?.(r);
22
+ }
23
+ });
24
+ }, []);
25
+ React.useEffect(() => {
26
+ for (const plugin of plugins) {
27
+ plugin.update?.(theRealm);
28
+ }
29
+ });
30
+ return /* @__PURE__ */ jsx(RealmContext.Provider, { value: theRealm, children });
31
+ }
32
+ export {
33
+ RealmWithPlugins,
34
+ realmPlugin
35
+ };