@multiplatform.one/rich-text 6.3.0 → 6.4.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.
- package/dist/cjs/RichEditor.cjs +5 -0
- package/dist/cjs/RichEditor.native.js +22 -0
- package/dist/cjs/RichEditor.native.js.map +1 -1
- package/dist/cjs/RichTextEditor/index.cjs +2 -0
- package/dist/cjs/RichTextEditor/index.native.js +2 -0
- package/dist/cjs/RichTextEditor/index.native.js.map +1 -1
- package/dist/cjs/editor/SlashCommands.cjs +8 -1
- package/dist/cjs/editor/SlashCommands.native.js +14 -1
- package/dist/cjs/editor/SlashCommands.native.js.map +1 -1
- package/dist/cjs/editor/TentapEditor.native.js +15 -0
- package/dist/cjs/editor/TentapEditor.native.js.map +1 -1
- package/dist/cjs/editor/TiptapEditor.cjs +26 -230
- package/dist/cjs/editor/TiptapEditor.native.js +34 -280
- package/dist/cjs/editor/TiptapEditor.native.js.map +1 -1
- package/dist/cjs/editor/capabilities.cjs +37 -0
- package/dist/cjs/editor/capabilities.native.js +41 -0
- package/dist/cjs/editor/capabilities.native.js.map +1 -0
- package/dist/cjs/editor/extensions.cjs +275 -0
- package/dist/cjs/editor/extensions.native.js +328 -0
- package/dist/cjs/editor/extensions.native.js.map +1 -0
- package/dist/cjs/index.cjs +5 -2
- package/dist/cjs/index.native.js +4 -1
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/toolbar/BubbleToolbar.cjs +9 -6
- package/dist/cjs/toolbar/BubbleToolbar.native.js +12 -7
- package/dist/cjs/toolbar/BubbleToolbar.native.js.map +1 -1
- package/dist/cjs/toolbar/FixedToolbar.cjs +6 -3
- package/dist/cjs/toolbar/MoreDropdown.cjs +17 -9
- package/dist/cjs/toolbar/MoreDropdown.native.js +21 -9
- package/dist/cjs/toolbar/MoreDropdown.native.js.map +1 -1
- package/dist/cjs/toolbar/TableBubble.cjs +15 -2
- package/dist/cjs/toolbar/TableBubble.native.js +17 -2
- package/dist/cjs/toolbar/TableBubble.native.js.map +1 -1
- package/dist/cjs/types.native.js.map +1 -1
- package/dist/esm/RichEditor.mjs +5 -0
- package/dist/esm/RichEditor.mjs.map +1 -1
- package/dist/esm/RichEditor.native.js +22 -0
- package/dist/esm/RichEditor.native.js.map +1 -1
- package/dist/esm/RichTextEditor/index.mjs +2 -0
- package/dist/esm/RichTextEditor/index.mjs.map +1 -1
- package/dist/esm/RichTextEditor/index.native.js +2 -0
- package/dist/esm/RichTextEditor/index.native.js.map +1 -1
- package/dist/esm/editor/SlashCommands.mjs +8 -1
- package/dist/esm/editor/SlashCommands.mjs.map +1 -1
- package/dist/esm/editor/SlashCommands.native.js +14 -1
- package/dist/esm/editor/SlashCommands.native.js.map +1 -1
- package/dist/esm/editor/TentapEditor.native.js +15 -0
- package/dist/esm/editor/TentapEditor.native.js.map +1 -1
- package/dist/esm/editor/TiptapEditor.mjs +28 -221
- package/dist/esm/editor/TiptapEditor.mjs.map +1 -1
- package/dist/esm/editor/TiptapEditor.native.js +36 -271
- package/dist/esm/editor/TiptapEditor.native.js.map +1 -1
- package/dist/esm/editor/capabilities.mjs +10 -0
- package/dist/esm/editor/capabilities.mjs.map +1 -0
- package/dist/esm/editor/capabilities.native.js +11 -0
- package/dist/esm/editor/capabilities.native.js.map +1 -0
- package/dist/esm/editor/extensions.mjs +236 -0
- package/dist/esm/editor/extensions.mjs.map +1 -0
- package/dist/esm/editor/extensions.native.js +286 -0
- package/dist/esm/editor/extensions.native.js.map +1 -0
- package/dist/esm/index.mjs +2 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/toolbar/BubbleToolbar.mjs +9 -6
- package/dist/esm/toolbar/BubbleToolbar.mjs.map +1 -1
- package/dist/esm/toolbar/BubbleToolbar.native.js +12 -7
- package/dist/esm/toolbar/BubbleToolbar.native.js.map +1 -1
- package/dist/esm/toolbar/FixedToolbar.mjs +6 -3
- package/dist/esm/toolbar/FixedToolbar.mjs.map +1 -1
- package/dist/esm/toolbar/MoreDropdown.mjs +17 -9
- package/dist/esm/toolbar/MoreDropdown.mjs.map +1 -1
- package/dist/esm/toolbar/MoreDropdown.native.js +21 -9
- package/dist/esm/toolbar/MoreDropdown.native.js.map +1 -1
- package/dist/esm/toolbar/TableBubble.mjs +15 -2
- package/dist/esm/toolbar/TableBubble.mjs.map +1 -1
- package/dist/esm/toolbar/TableBubble.native.js +17 -2
- package/dist/esm/toolbar/TableBubble.native.js.map +1 -1
- package/dist/esm/types.mjs.map +1 -1
- package/dist/esm/types.native.js.map +1 -1
- package/dist/jsx/RichEditor.mjs +5 -0
- package/dist/jsx/RichEditor.mjs.map +1 -1
- package/dist/jsx/RichEditor.native.js +22 -0
- package/dist/jsx/RichEditor.native.js.map +1 -1
- package/dist/jsx/RichTextEditor/index.mjs +2 -0
- package/dist/jsx/RichTextEditor/index.mjs.map +1 -1
- package/dist/jsx/RichTextEditor/index.native.js +2 -0
- package/dist/jsx/RichTextEditor/index.native.js.map +1 -1
- package/dist/jsx/editor/SlashCommands.mjs +8 -1
- package/dist/jsx/editor/SlashCommands.mjs.map +1 -1
- package/dist/jsx/editor/SlashCommands.native.js +14 -1
- package/dist/jsx/editor/SlashCommands.native.js.map +1 -1
- package/dist/jsx/editor/TentapEditor.native.js +15 -0
- package/dist/jsx/editor/TentapEditor.native.js.map +1 -1
- package/dist/jsx/editor/TiptapEditor.mjs +28 -221
- package/dist/jsx/editor/TiptapEditor.mjs.map +1 -1
- package/dist/jsx/editor/TiptapEditor.native.js +34 -280
- package/dist/jsx/editor/TiptapEditor.native.js.map +1 -1
- package/dist/jsx/editor/capabilities.mjs +10 -0
- package/dist/jsx/editor/capabilities.mjs.map +1 -0
- package/dist/jsx/editor/capabilities.native.js +41 -0
- package/dist/jsx/editor/capabilities.native.js.map +1 -0
- package/dist/jsx/editor/extensions.mjs +236 -0
- package/dist/jsx/editor/extensions.mjs.map +1 -0
- package/dist/jsx/editor/extensions.native.js +328 -0
- package/dist/jsx/editor/extensions.native.js.map +1 -0
- package/dist/jsx/index.js +2 -1
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +2 -1
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +4 -1
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/toolbar/BubbleToolbar.mjs +9 -6
- package/dist/jsx/toolbar/BubbleToolbar.mjs.map +1 -1
- package/dist/jsx/toolbar/BubbleToolbar.native.js +12 -7
- package/dist/jsx/toolbar/BubbleToolbar.native.js.map +1 -1
- package/dist/jsx/toolbar/FixedToolbar.mjs +6 -3
- package/dist/jsx/toolbar/FixedToolbar.mjs.map +1 -1
- package/dist/jsx/toolbar/MoreDropdown.mjs +17 -9
- package/dist/jsx/toolbar/MoreDropdown.mjs.map +1 -1
- package/dist/jsx/toolbar/MoreDropdown.native.js +21 -9
- package/dist/jsx/toolbar/MoreDropdown.native.js.map +1 -1
- package/dist/jsx/toolbar/TableBubble.mjs +15 -2
- package/dist/jsx/toolbar/TableBubble.mjs.map +1 -1
- package/dist/jsx/toolbar/TableBubble.native.js +17 -2
- package/dist/jsx/toolbar/TableBubble.native.js.map +1 -1
- package/dist/jsx/types.mjs.map +1 -1
- package/dist/jsx/types.native.js.map +1 -1
- package/package.json +8 -8
- package/src/RichEditor.native.tsx +20 -0
- package/src/RichEditor.stories.tsx +32 -0
- package/src/RichEditor.tsx +5 -0
- package/src/RichTextEditor/index.tsx +14 -1
- package/src/editor/SlashCommands.tsx +10 -0
- package/src/editor/TentapEditor.native.tsx +13 -0
- package/src/editor/TiptapEditor.tsx +51 -279
- package/src/editor/capabilities.spec.ts +190 -0
- package/src/editor/capabilities.ts +76 -0
- package/src/editor/extensions.ts +315 -0
- package/src/index.ts +3 -0
- package/src/toolbar/BubbleToolbar.tsx +40 -28
- package/src/toolbar/FixedToolbar.tsx +20 -12
- package/src/toolbar/MoreDropdown.tsx +30 -6
- package/src/toolbar/TableBubble.tsx +65 -12
- package/src/types.ts +28 -1
- package/types/RichEditor.d.ts.map +1 -1
- package/types/RichEditor.native.d.ts.map +1 -1
- package/types/RichTextEditor/index.d.ts +7 -1
- package/types/RichTextEditor/index.d.ts.map +1 -1
- package/types/editor/SlashCommands.d.ts.map +1 -1
- package/types/editor/TentapEditor.native.d.ts.map +1 -1
- package/types/editor/TiptapEditor.d.ts.map +1 -1
- package/types/editor/capabilities.d.ts +29 -0
- package/types/editor/capabilities.d.ts.map +1 -0
- package/types/editor/extensions.d.ts +41 -0
- package/types/editor/extensions.d.ts.map +1 -0
- package/types/index.d.ts +2 -1
- package/types/index.d.ts.map +1 -1
- package/types/toolbar/BubbleToolbar.d.ts +4 -2
- package/types/toolbar/BubbleToolbar.d.ts.map +1 -1
- package/types/toolbar/FixedToolbar.d.ts +4 -2
- package/types/toolbar/FixedToolbar.d.ts.map +1 -1
- package/types/toolbar/MoreDropdown.d.ts +4 -2
- package/types/toolbar/MoreDropdown.d.ts.map +1 -1
- package/types/toolbar/TableBubble.d.ts +4 -1
- package/types/toolbar/TableBubble.d.ts.map +1 -1
- package/types/types.d.ts +23 -1
- package/types/types.d.ts.map +1 -1
|
@@ -20,27 +20,6 @@ import {
|
|
|
20
20
|
useState,
|
|
21
21
|
} from "react";
|
|
22
22
|
import { useEditor, EditorContent } from "@tiptap/react";
|
|
23
|
-
import { Extension } from "@tiptap/core";
|
|
24
|
-
import StarterKit from "@tiptap/starter-kit";
|
|
25
|
-
import Placeholder from "@tiptap/extension-placeholder";
|
|
26
|
-
import TextAlign from "@tiptap/extension-text-align";
|
|
27
|
-
import { Color } from "@tiptap/extension-color";
|
|
28
|
-
import { TextStyle } from "@tiptap/extension-text-style";
|
|
29
|
-
import { Highlight } from "@tiptap/extension-highlight";
|
|
30
|
-
import { Subscript } from "@tiptap/extension-subscript";
|
|
31
|
-
import { Superscript } from "@tiptap/extension-superscript";
|
|
32
|
-
import TaskList from "@tiptap/extension-task-list";
|
|
33
|
-
import TaskItem from "@tiptap/extension-task-item";
|
|
34
|
-
import Image from "@tiptap/extension-image";
|
|
35
|
-
import Typography from "@tiptap/extension-typography";
|
|
36
|
-
import CharacterCount from "@tiptap/extension-character-count";
|
|
37
|
-
import { Details, DetailsContent, DetailsSummary } from "@tiptap/extension-details";
|
|
38
|
-
|
|
39
|
-
import { Table } from "@tiptap/extension-table";
|
|
40
|
-
import { TableRow } from "@tiptap/extension-table-row";
|
|
41
|
-
import { TableHeader } from "@tiptap/extension-table-header";
|
|
42
|
-
import { TableCell } from "@tiptap/extension-table-cell";
|
|
43
|
-
import { common, createLowlight } from "lowlight";
|
|
44
23
|
import { View, getTokens, styled, useTheme, useThemeName } from "tamagui";
|
|
45
24
|
import { isWeb } from "@tamagui/constants";
|
|
46
25
|
import { menuRowFrame, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
@@ -50,21 +29,13 @@ import { defaultEditorState } from "../types";
|
|
|
50
29
|
import { RichEditorContext } from "../context";
|
|
51
30
|
import { t } from "../shared/t";
|
|
52
31
|
import { getEditorCSS } from "./editorStyles";
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
import { BlockDragHandle } from "./BlockDragHandle";
|
|
56
|
-
import { MediaUpload } from "./media";
|
|
32
|
+
import { buildEditorExtensions } from "./extensions";
|
|
33
|
+
import { isCommandInCapabilities } from "./capabilities";
|
|
57
34
|
import { createMentionExtension } from "./mentions";
|
|
58
|
-
import {
|
|
59
|
-
import { InlineMath } from "./mathematics";
|
|
60
|
-
import { CodeBlockWithPreview, PreviewChromeStore, setPreviewChrome } from "./codeBlockPreview";
|
|
35
|
+
import { setPreviewChrome } from "./codeBlockPreview";
|
|
61
36
|
import { htmlToMarkdown, markdownToHtml } from "./markdown";
|
|
62
37
|
import { cleanPastedHtml } from "./pasteCleanup";
|
|
63
|
-
import {
|
|
64
|
-
SuggestionChromeStore,
|
|
65
|
-
fallbackSuggestionChrome,
|
|
66
|
-
type SuggestionChrome,
|
|
67
|
-
} from "./suggestionPopup";
|
|
38
|
+
import { fallbackSuggestionChrome, type SuggestionChrome } from "./suggestionPopup";
|
|
68
39
|
import { getEditorStateFromTiptap } from "./useEditorState";
|
|
69
40
|
import { BubbleToolbar } from "../toolbar/BubbleToolbar";
|
|
70
41
|
import { LinkBubble } from "../toolbar/LinkBubble";
|
|
@@ -73,159 +44,9 @@ import { ImageBubble } from "../toolbar/ImageBubble";
|
|
|
73
44
|
import { TableBubble } from "../toolbar/TableBubble";
|
|
74
45
|
import { MathBubble } from "../toolbar/MathBubble";
|
|
75
46
|
|
|
76
|
-
const lowlight = createLowlight(common);
|
|
77
|
-
|
|
78
47
|
/** Enter/exit fade for bubbles and suggestion popups (0 when animation none) */
|
|
79
48
|
const MOTION_MS = 120;
|
|
80
49
|
|
|
81
|
-
/**
|
|
82
|
-
* Tab / Shift-Tab indentation inside code blocks.
|
|
83
|
-
* Inserts or removes 2 spaces at the cursor (or start of each selected line).
|
|
84
|
-
*/
|
|
85
|
-
const CodeBlockTabIndent = Extension.create({
|
|
86
|
-
name: "codeBlockTabIndent",
|
|
87
|
-
addKeyboardShortcuts() {
|
|
88
|
-
return {
|
|
89
|
-
Tab: ({ editor }) => {
|
|
90
|
-
if (!editor.isActive("codeBlock")) return false;
|
|
91
|
-
const { state, dispatch } = editor.view;
|
|
92
|
-
const { from, to } = state.selection;
|
|
93
|
-
|
|
94
|
-
if (from === to) {
|
|
95
|
-
dispatch(state.tr.insertText(" ", from, to));
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const doc = state.doc;
|
|
100
|
-
const lines: { start: number }[] = [];
|
|
101
|
-
doc.nodesBetween(from, to, (node, pos) => {
|
|
102
|
-
if (node.isTextblock) {
|
|
103
|
-
lines.push({ start: pos + 1 });
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
if (lines.length === 0) {
|
|
107
|
-
dispatch(state.tr.insertText(" ", from, to));
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let tr = state.tr;
|
|
112
|
-
let offset = 0;
|
|
113
|
-
for (const line of lines) {
|
|
114
|
-
const lineStart = line.start + offset;
|
|
115
|
-
tr = tr.insertText(" ", lineStart);
|
|
116
|
-
offset += 2;
|
|
117
|
-
}
|
|
118
|
-
dispatch(tr);
|
|
119
|
-
return true;
|
|
120
|
-
},
|
|
121
|
-
"Shift-Tab": ({ editor }) => {
|
|
122
|
-
if (!editor.isActive("codeBlock")) return false;
|
|
123
|
-
const { state, dispatch } = editor.view;
|
|
124
|
-
const { from, to, $from } = state.selection;
|
|
125
|
-
|
|
126
|
-
if (from === to) {
|
|
127
|
-
const lineStart = from - $from.parentOffset;
|
|
128
|
-
const text = state.doc.textBetween(lineStart, from);
|
|
129
|
-
const leadingSpaces = text.match(/^ {1,2}/)?.[0];
|
|
130
|
-
if (leadingSpaces) {
|
|
131
|
-
dispatch(state.tr.delete(lineStart, lineStart + leadingSpaces.length));
|
|
132
|
-
}
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const doc = state.doc;
|
|
137
|
-
let tr = state.tr;
|
|
138
|
-
let offset = 0;
|
|
139
|
-
doc.nodesBetween(from, to, (node, pos) => {
|
|
140
|
-
if (node.isTextblock) {
|
|
141
|
-
const lineStart = pos + 1 + offset;
|
|
142
|
-
const text = tr.doc.textBetween(
|
|
143
|
-
lineStart,
|
|
144
|
-
Math.min(lineStart + node.content.size, tr.doc.content.size),
|
|
145
|
-
);
|
|
146
|
-
const leadingSpaces = text.match(/^ {1,2}/)?.[0];
|
|
147
|
-
if (leadingSpaces) {
|
|
148
|
-
tr = tr.delete(lineStart, lineStart + leadingSpaces.length);
|
|
149
|
-
offset -= leadingSpaces.length;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
dispatch(tr);
|
|
154
|
-
return true;
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Override Shift-Enter and Mod-Enter to create new paragraphs instead of
|
|
162
|
-
* hard breaks (<br>). Mirrors GitLab's approach: HardBreak node stays
|
|
163
|
-
* registered (so <br> in pasted HTML is preserved) but its keyboard
|
|
164
|
-
* shortcuts are replaced with the default enter (split paragraph) behavior.
|
|
165
|
-
*/
|
|
166
|
-
const ParagraphEnterKeys = Extension.create({
|
|
167
|
-
name: "paragraphEnterKeys",
|
|
168
|
-
priority: 1000,
|
|
169
|
-
addKeyboardShortcuts() {
|
|
170
|
-
return {
|
|
171
|
-
"Shift-Enter": ({ editor }) =>
|
|
172
|
-
editor.commands.first([
|
|
173
|
-
() => editor.commands.newlineInCode(),
|
|
174
|
-
() => editor.commands.splitListItem("listItem"),
|
|
175
|
-
() => editor.commands.createParagraphNear(),
|
|
176
|
-
() => editor.commands.liftEmptyBlock(),
|
|
177
|
-
() => editor.commands.splitBlock(),
|
|
178
|
-
]),
|
|
179
|
-
"Mod-Enter": ({ editor }) =>
|
|
180
|
-
editor.commands.first([
|
|
181
|
-
() => editor.commands.newlineInCode(),
|
|
182
|
-
() => editor.commands.createParagraphNear(),
|
|
183
|
-
() => editor.commands.liftEmptyBlock(),
|
|
184
|
-
() => editor.commands.splitBlock(),
|
|
185
|
-
]),
|
|
186
|
-
};
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Tab/Shift-Tab indent/dedent and Alt+Arrow move for list items.
|
|
192
|
-
* Tab sinks (indents), Shift-Tab lifts (dedents).
|
|
193
|
-
* Alt+Up/Down moves the list item up or down among siblings.
|
|
194
|
-
*/
|
|
195
|
-
const ListItemKeys = Extension.create({
|
|
196
|
-
name: "listItemKeys",
|
|
197
|
-
addKeyboardShortcuts() {
|
|
198
|
-
return {
|
|
199
|
-
Tab: ({ editor }) => {
|
|
200
|
-
if (editor.isActive("codeBlock")) return false;
|
|
201
|
-
if (
|
|
202
|
-
editor.isActive("bulletList") ||
|
|
203
|
-
editor.isActive("orderedList") ||
|
|
204
|
-
editor.isActive("taskList")
|
|
205
|
-
) {
|
|
206
|
-
return (
|
|
207
|
-
editor.commands.sinkListItem("listItem") || editor.commands.sinkListItem("taskItem")
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
return false;
|
|
211
|
-
},
|
|
212
|
-
"Shift-Tab": ({ editor }) => {
|
|
213
|
-
if (editor.isActive("codeBlock")) return false;
|
|
214
|
-
if (
|
|
215
|
-
editor.isActive("bulletList") ||
|
|
216
|
-
editor.isActive("orderedList") ||
|
|
217
|
-
editor.isActive("taskList")
|
|
218
|
-
) {
|
|
219
|
-
return (
|
|
220
|
-
editor.commands.liftListItem("listItem") || editor.commands.liftListItem("taskItem")
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
return false;
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
|
|
229
50
|
// ===========================================================================
|
|
230
51
|
// Types
|
|
231
52
|
// ===========================================================================
|
|
@@ -262,6 +83,7 @@ export const TiptapEditor = memo(
|
|
|
262
83
|
forwardRef<RichEditorRef, TiptapEditorProps>(
|
|
263
84
|
(
|
|
264
85
|
{
|
|
86
|
+
capabilities = "full",
|
|
265
87
|
initialContent = "",
|
|
266
88
|
initialJSON,
|
|
267
89
|
placeholder: placeholderProp,
|
|
@@ -318,93 +140,19 @@ export const TiptapEditor = memo(
|
|
|
318
140
|
onChangeRef.current = onChange;
|
|
319
141
|
});
|
|
320
142
|
|
|
143
|
+
// Capability surface is fixed at mount (extension lists cannot swap
|
|
144
|
+
// live); the canonical list comes from the one factory the capability
|
|
145
|
+
// specs also verify (LC-52).
|
|
146
|
+
const [extensions] = useState(() =>
|
|
147
|
+
buildEditorExtensions({ capabilities, placeholder, mentionExtension }),
|
|
148
|
+
);
|
|
149
|
+
|
|
321
150
|
const editor = useEditor({
|
|
322
|
-
extensions
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
},
|
|
328
|
-
paragraph: {
|
|
329
|
-
HTMLAttributes: { dir: "auto" },
|
|
330
|
-
},
|
|
331
|
-
link: {
|
|
332
|
-
openOnClick: false,
|
|
333
|
-
HTMLAttributes: {
|
|
334
|
-
rel: "noopener noreferrer",
|
|
335
|
-
target: "_blank",
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
bulletList: {
|
|
339
|
-
keepMarks: true,
|
|
340
|
-
},
|
|
341
|
-
orderedList: {
|
|
342
|
-
keepMarks: true,
|
|
343
|
-
},
|
|
344
|
-
codeBlock: false,
|
|
345
|
-
hardBreak: false,
|
|
346
|
-
}),
|
|
347
|
-
CodeBlockWithPreview.configure({
|
|
348
|
-
lowlight,
|
|
349
|
-
exitOnTripleEnter: true,
|
|
350
|
-
exitOnArrowDown: false,
|
|
351
|
-
}),
|
|
352
|
-
ParagraphEnterKeys,
|
|
353
|
-
CodeBlockTabIndent,
|
|
354
|
-
ListItemKeys,
|
|
355
|
-
// superscriptTwo/Three would mangle typed TeX (`$E=mc^2$` became
|
|
356
|
-
// `E=mc²` before the math input rule ran) — off, rest stays.
|
|
357
|
-
Typography.configure({
|
|
358
|
-
superscriptTwo: false,
|
|
359
|
-
superscriptThree: false,
|
|
360
|
-
}),
|
|
361
|
-
CharacterCount,
|
|
362
|
-
SlashCommands,
|
|
363
|
-
MarkdownPaste,
|
|
364
|
-
MediaUpload,
|
|
365
|
-
SuggestionChromeStore,
|
|
366
|
-
PreviewChromeStore,
|
|
367
|
-
EmojiSuggestion,
|
|
368
|
-
InlineMath,
|
|
369
|
-
// Collapsible sections (GitLab <details> parity). persist: true
|
|
370
|
-
// is the extension's supported mode: its node view re-syncs the
|
|
371
|
-
// open state from node.attrs on every update, so ephemeral
|
|
372
|
-
// (persist: false) toggles get reverted by the next transaction.
|
|
373
|
-
// The open state therefore lives in the doc and serializes as
|
|
374
|
-
// `<details open>` — valid GitLab markdown.
|
|
375
|
-
Details.configure({ persist: true }),
|
|
376
|
-
DetailsSummary,
|
|
377
|
-
DetailsContent,
|
|
378
|
-
...(mentionExtension ? [mentionExtension] : []),
|
|
379
|
-
BlockDragHandle,
|
|
380
|
-
Table.configure({ resizable: true }),
|
|
381
|
-
TableRow,
|
|
382
|
-
TableHeader,
|
|
383
|
-
TableCell,
|
|
384
|
-
Placeholder.configure({
|
|
385
|
-
placeholder,
|
|
386
|
-
}),
|
|
387
|
-
TextAlign.configure({
|
|
388
|
-
types: ["heading", "paragraph"],
|
|
389
|
-
defaultAlignment: "left",
|
|
390
|
-
}),
|
|
391
|
-
TextStyle,
|
|
392
|
-
Color,
|
|
393
|
-
Highlight.configure({
|
|
394
|
-
multicolor: true,
|
|
395
|
-
}),
|
|
396
|
-
Subscript,
|
|
397
|
-
Superscript,
|
|
398
|
-
TaskList,
|
|
399
|
-
TaskItem.configure({
|
|
400
|
-
nested: true,
|
|
401
|
-
}),
|
|
402
|
-
Image.configure({
|
|
403
|
-
inline: true,
|
|
404
|
-
allowBase64: true,
|
|
405
|
-
}),
|
|
406
|
-
],
|
|
407
|
-
content: initialJSON || initialContent,
|
|
151
|
+
extensions,
|
|
152
|
+
// In markdown capabilities the incoming string IS markdown
|
|
153
|
+
content:
|
|
154
|
+
initialJSON ||
|
|
155
|
+
(capabilities === "markdown" ? markdownToHtml(initialContent) : initialContent),
|
|
408
156
|
editable,
|
|
409
157
|
autofocus: autoFocus,
|
|
410
158
|
// No htmlFor target exists for the contenteditable — name it directly
|
|
@@ -426,7 +174,12 @@ export const TiptapEditor = memo(
|
|
|
426
174
|
changeRafRef.current = requestAnimationFrame(() => {
|
|
427
175
|
const html = editor.getHTML();
|
|
428
176
|
const json = editor.getJSON();
|
|
429
|
-
|
|
177
|
+
// Markdown capabilities serialize the content OUT as markdown —
|
|
178
|
+
// the host's value stays a markdown string end to end.
|
|
179
|
+
onChangeRef.current?.(
|
|
180
|
+
capabilities === "markdown" ? htmlToMarkdown(html) : html,
|
|
181
|
+
json,
|
|
182
|
+
);
|
|
430
183
|
});
|
|
431
184
|
},
|
|
432
185
|
onTransaction: ({ editor }) => {
|
|
@@ -505,15 +258,23 @@ export const TiptapEditor = memo(
|
|
|
505
258
|
}
|
|
506
259
|
}, [editorMode, editor, markdownSource]);
|
|
507
260
|
|
|
508
|
-
const handleMarkdownChange = useCallback(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
261
|
+
const handleMarkdownChange = useCallback(
|
|
262
|
+
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
263
|
+
const md = e.target.value;
|
|
264
|
+
setMarkdownSource(md);
|
|
265
|
+
// Markdown capabilities emit the markdown source itself; the full
|
|
266
|
+
// surface keeps its HTML onChange contract.
|
|
267
|
+
onChangeRef.current?.(capabilities === "markdown" ? md : markdownToHtml(md), null);
|
|
268
|
+
},
|
|
269
|
+
[capabilities],
|
|
270
|
+
);
|
|
513
271
|
|
|
514
272
|
const executeCommand = useCallback(
|
|
515
273
|
(command: EditorCommand, payload?: any) => {
|
|
516
274
|
if (!editor) return;
|
|
275
|
+
// Capability guard: outside the surface the command's extension is
|
|
276
|
+
// not registered, so the chained method would not even exist.
|
|
277
|
+
if (!isCommandInCapabilities(command, capabilities)) return;
|
|
517
278
|
|
|
518
279
|
const chain = editor.chain().focus();
|
|
519
280
|
|
|
@@ -688,8 +449,13 @@ export const TiptapEditor = memo(
|
|
|
688
449
|
getState: () => getEditorStateFromTiptap(editor),
|
|
689
450
|
getHTML: () => editor?.getHTML() || "",
|
|
690
451
|
getJSON: () => editor?.getJSON() || null,
|
|
452
|
+
getMarkdown: () => htmlToMarkdown(editor?.getHTML() || ""),
|
|
691
453
|
setHTML: (html: string) => editor?.commands.setContent(html),
|
|
692
454
|
setJSON: (json: any) => editor?.commands.setContent(json),
|
|
455
|
+
// External value sync (not a user edit): emitUpdate false so the
|
|
456
|
+
// host's own set does not echo back through onChange.
|
|
457
|
+
setMarkdown: (markdown: string) =>
|
|
458
|
+
editor?.commands.setContent(markdownToHtml(markdown), { emitUpdate: false }),
|
|
693
459
|
focus: () => editor?.commands.focus(),
|
|
694
460
|
blur: () => editor?.commands.blur(),
|
|
695
461
|
}),
|
|
@@ -757,17 +523,23 @@ export const TiptapEditor = memo(
|
|
|
757
523
|
{isWeb && <style>{editorCSS}</style>}
|
|
758
524
|
{showBubbleToolbar && (
|
|
759
525
|
<>
|
|
760
|
-
<BubbleToolbar
|
|
526
|
+
<BubbleToolbar
|
|
527
|
+
editor={editor}
|
|
528
|
+
editorRef={internalRef}
|
|
529
|
+
state={defaultEditorState}
|
|
530
|
+
capabilities={capabilities}
|
|
531
|
+
/>
|
|
761
532
|
<LinkBubble editor={editor} />
|
|
762
533
|
<CodeBlockBubble editor={editor} />
|
|
763
534
|
<ImageBubble editor={editor} />
|
|
764
|
-
<TableBubble editor={editor} />
|
|
535
|
+
<TableBubble editor={editor} capabilities={capabilities} />
|
|
765
536
|
</>
|
|
766
537
|
)}
|
|
767
538
|
{/* Always mounted (unlike the optional bubbles): it is the ONLY
|
|
768
539
|
edit path for inline math atoms — without it a typed `$x$`
|
|
769
|
-
node would be a dead end.
|
|
770
|
-
|
|
540
|
+
node would be a dead end. Markdown capabilities have no inline
|
|
541
|
+
math node, so the bubble has nothing to edit and stays out. */}
|
|
542
|
+
{editable && capabilities !== "markdown" && <MathBubble editor={editor} />}
|
|
771
543
|
<EditorContent
|
|
772
544
|
editor={editor}
|
|
773
545
|
style={fullscreen ? { flex: "1 1 0%", minHeight: 0, overflowY: "auto" } : undefined}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VERIFY arm for LC-52 MARKDOWN-EXPRESSIBLE-EDITING.
|
|
3
|
+
*
|
|
4
|
+
* Proves the markdown capability surface is a real constraint, not a
|
|
5
|
+
* visual one: non-markdown extensions are NOT registered on the editor
|
|
6
|
+
* (their commands do not exist), and markdown in → edit → markdown out
|
|
7
|
+
* round-trips for the whole GFM-expressible set.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import { Editor } from "@tiptap/core";
|
|
11
|
+
import { MARKDOWN_COMMANDS, isCommandInCapabilities } from "./capabilities";
|
|
12
|
+
import { buildEditorExtensions } from "./extensions";
|
|
13
|
+
import { htmlToMarkdown, markdownToHtml } from "./markdown";
|
|
14
|
+
import type { EditorCommand } from "../types";
|
|
15
|
+
|
|
16
|
+
function createEditor(capabilities: "full" | "markdown", content = "<p>hello</p>") {
|
|
17
|
+
return new Editor({
|
|
18
|
+
element: document.createElement("div"),
|
|
19
|
+
extensions: buildEditorExtensions({ capabilities }),
|
|
20
|
+
content,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const NON_MARKDOWN_COMMANDS: EditorCommand[] = [
|
|
25
|
+
"underline",
|
|
26
|
+
"highlight",
|
|
27
|
+
"textColor",
|
|
28
|
+
"subscript",
|
|
29
|
+
"superscript",
|
|
30
|
+
"alignLeft",
|
|
31
|
+
"alignCenter",
|
|
32
|
+
"alignRight",
|
|
33
|
+
"alignJustify",
|
|
34
|
+
"mergeCells",
|
|
35
|
+
"splitCell",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
describe("isCommandInCapabilities", () => {
|
|
39
|
+
it("allows everything in full capabilities", () => {
|
|
40
|
+
for (const command of [...MARKDOWN_COMMANDS, ...NON_MARKDOWN_COMMANDS]) {
|
|
41
|
+
expect(isCommandInCapabilities(command, "full")).toBe(true);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects non-markdown commands in markdown capabilities", () => {
|
|
46
|
+
for (const command of NON_MARKDOWN_COMMANDS) {
|
|
47
|
+
expect(isCommandInCapabilities(command, "markdown")).toBe(false);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("keeps the GFM-expressible commands in markdown capabilities", () => {
|
|
52
|
+
for (const command of MARKDOWN_COMMANDS) {
|
|
53
|
+
expect(isCommandInCapabilities(command, "markdown")).toBe(true);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("markdown capabilities: extension registration", () => {
|
|
59
|
+
it("does not register underline/highlight/color/align/sub/sup extensions", () => {
|
|
60
|
+
const editor = createEditor("markdown");
|
|
61
|
+
const names = new Set(editor.extensionManager.extensions.map((e) => e.name));
|
|
62
|
+
expect(names.has("underline")).toBe(false);
|
|
63
|
+
expect(names.has("highlight")).toBe(false);
|
|
64
|
+
expect(names.has("textStyle")).toBe(false);
|
|
65
|
+
expect(names.has("color")).toBe(false);
|
|
66
|
+
expect(names.has("textAlign")).toBe(false);
|
|
67
|
+
expect(names.has("subscript")).toBe(false);
|
|
68
|
+
expect(names.has("superscript")).toBe(false);
|
|
69
|
+
expect(names.has("inlineMath")).toBe(false);
|
|
70
|
+
expect(names.has("details")).toBe(false);
|
|
71
|
+
editor.destroy();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("commands markdown cannot express do not exist on the editor", () => {
|
|
75
|
+
const editor = createEditor("markdown");
|
|
76
|
+
const commands = editor.commands as unknown as Record<string, unknown>;
|
|
77
|
+
expect(commands.toggleUnderline).toBeUndefined();
|
|
78
|
+
expect(commands.toggleHighlight).toBeUndefined();
|
|
79
|
+
expect(commands.setColor).toBeUndefined();
|
|
80
|
+
expect(commands.setTextAlign).toBeUndefined();
|
|
81
|
+
expect(commands.toggleSubscript).toBeUndefined();
|
|
82
|
+
expect(commands.toggleSuperscript).toBeUndefined();
|
|
83
|
+
editor.destroy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("full capabilities keep the whole surface (control)", () => {
|
|
87
|
+
const editor = createEditor("full");
|
|
88
|
+
const names = new Set(editor.extensionManager.extensions.map((e) => e.name));
|
|
89
|
+
expect(names.has("underline")).toBe(true);
|
|
90
|
+
expect(names.has("highlight")).toBe(true);
|
|
91
|
+
expect(names.has("textAlign")).toBe(true);
|
|
92
|
+
const commands = editor.commands as unknown as Record<string, unknown>;
|
|
93
|
+
expect(typeof commands.toggleUnderline).toBe("function");
|
|
94
|
+
expect(typeof commands.toggleHighlight).toBe("function");
|
|
95
|
+
editor.destroy();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("keeps the markdown-expressible surface registered", () => {
|
|
99
|
+
const editor = createEditor("markdown");
|
|
100
|
+
const commands = editor.commands as unknown as Record<string, unknown>;
|
|
101
|
+
for (const name of [
|
|
102
|
+
"toggleBold",
|
|
103
|
+
"toggleItalic",
|
|
104
|
+
"toggleStrike",
|
|
105
|
+
"toggleCode",
|
|
106
|
+
"toggleHeading",
|
|
107
|
+
"toggleBulletList",
|
|
108
|
+
"toggleOrderedList",
|
|
109
|
+
"toggleTaskList",
|
|
110
|
+
"toggleBlockquote",
|
|
111
|
+
"toggleCodeBlock",
|
|
112
|
+
"setLink",
|
|
113
|
+
"setImage",
|
|
114
|
+
"setHorizontalRule",
|
|
115
|
+
"insertTable",
|
|
116
|
+
]) {
|
|
117
|
+
expect(typeof commands[name], name).toBe("function");
|
|
118
|
+
}
|
|
119
|
+
editor.destroy();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("parses pasted underline/highlight HTML without those marks", () => {
|
|
123
|
+
const editor = createEditor(
|
|
124
|
+
"markdown",
|
|
125
|
+
"<p><u>under</u> and <mark>marked</mark> and <strong>bold</strong></p>",
|
|
126
|
+
);
|
|
127
|
+
const html = editor.getHTML();
|
|
128
|
+
expect(html).not.toContain("<u>");
|
|
129
|
+
expect(html).not.toContain("<mark>");
|
|
130
|
+
expect(html).toContain("<strong>bold</strong>");
|
|
131
|
+
editor.destroy();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("markdown round-trip (markdown in → edit → markdown out)", () => {
|
|
136
|
+
const roundTrip = (markdown: string): string => {
|
|
137
|
+
const editor = createEditor("markdown", markdownToHtml(markdown));
|
|
138
|
+
const out = htmlToMarkdown(editor.getHTML());
|
|
139
|
+
editor.destroy();
|
|
140
|
+
return out;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
it("preserves headings, emphasis, strike, and inline code", () => {
|
|
144
|
+
const out = roundTrip(
|
|
145
|
+
"# Title\n\nSome **bold** and _italic_ and ~~gone~~ and `code` text.\n\n## Sub",
|
|
146
|
+
);
|
|
147
|
+
expect(out).toContain("# Title");
|
|
148
|
+
expect(out).toContain("**bold**");
|
|
149
|
+
expect(out).toContain("_italic_");
|
|
150
|
+
expect(out).toContain("~~gone~~");
|
|
151
|
+
expect(out).toContain("`code`");
|
|
152
|
+
expect(out).toContain("## Sub");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("preserves bullet, ordered, and task lists", () => {
|
|
156
|
+
const out = roundTrip("- one\n- two\n\n1. first\n2. second\n\n- [x] done\n- [ ] todo");
|
|
157
|
+
expect(out).toMatch(/[-*] {1,3}one/);
|
|
158
|
+
expect(out).toMatch(/1\. {1,2}first/);
|
|
159
|
+
expect(out).toContain("[x] done");
|
|
160
|
+
expect(out).toContain("[ ] todo");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("preserves blockquotes, fenced code, hr, links, and images", () => {
|
|
164
|
+
const out = roundTrip(
|
|
165
|
+
"> quoted\n\n```js\nconst a = 1;\n```\n\n---\n\n[site](https://example.com) ",
|
|
166
|
+
);
|
|
167
|
+
expect(out).toContain("> quoted");
|
|
168
|
+
expect(out).toContain("```");
|
|
169
|
+
expect(out).toContain("const a = 1;");
|
|
170
|
+
// any valid markdown thematic break (turndown emits "* * *")
|
|
171
|
+
expect(out).toMatch(/^(-{3,}|_{3,}|(\* ?){3,})$/m);
|
|
172
|
+
expect(out).toContain("[site](https://example.com)");
|
|
173
|
+
expect(out).toContain("");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("preserves GFM pipe tables", () => {
|
|
177
|
+
const out = roundTrip("| Name | Role |\n| --- | --- |\n| Ada | Analyst |");
|
|
178
|
+
expect(out).toContain("| Name | Role |");
|
|
179
|
+
expect(out).toContain("| --- | --- |");
|
|
180
|
+
expect(out).toContain("| Ada | Analyst |");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("edits made through markdown commands serialize to markdown", () => {
|
|
184
|
+
const editor = createEditor("markdown", markdownToHtml("plain text"));
|
|
185
|
+
editor.commands.selectAll();
|
|
186
|
+
editor.commands.toggleBold();
|
|
187
|
+
expect(htmlToMarkdown(editor.getHTML())).toContain("**plain text**");
|
|
188
|
+
editor.destroy();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor capability presets.
|
|
3
|
+
*
|
|
4
|
+
* "markdown" constrains the WYSIWYG editor to exactly what GFM markdown can
|
|
5
|
+
* express (owner ruling, LC-52 MARKDOWN-EXPRESSIBLE-EDITING): bold, italic,
|
|
6
|
+
* strikethrough, inline code, links, headings, bullet/ordered/task lists,
|
|
7
|
+
* blockquote, fenced code blocks, images, horizontal rule, and simple pipe
|
|
8
|
+
* tables. Everything markdown cannot express — underline, highlight, text
|
|
9
|
+
* color, alignment, sub/superscript, collapsible details, inline math,
|
|
10
|
+
* mention chips, table cell merge/split — is ABSENT in this mode: the
|
|
11
|
+
* extensions are not registered (commands do not exist) and the toolbar
|
|
12
|
+
* controls do not render.
|
|
13
|
+
*
|
|
14
|
+
* This module is the single source of truth both the runtime (extension
|
|
15
|
+
* factory, toolbars, executeCommand guard) and the VERIFY specs consume.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { EditorCapabilities, EditorCommand } from "../types";
|
|
19
|
+
|
|
20
|
+
/** Commands expressible in GFM markdown (the "markdown" capability surface) */
|
|
21
|
+
export const MARKDOWN_COMMANDS: ReadonlySet<EditorCommand> = new Set<EditorCommand>([
|
|
22
|
+
"bold",
|
|
23
|
+
"italic",
|
|
24
|
+
"strike",
|
|
25
|
+
"code",
|
|
26
|
+
"heading1",
|
|
27
|
+
"heading2",
|
|
28
|
+
"heading3",
|
|
29
|
+
"heading4",
|
|
30
|
+
"heading5",
|
|
31
|
+
"heading6",
|
|
32
|
+
"paragraph",
|
|
33
|
+
"bulletList",
|
|
34
|
+
"orderedList",
|
|
35
|
+
"taskList",
|
|
36
|
+
"blockquote",
|
|
37
|
+
"codeBlock",
|
|
38
|
+
"link",
|
|
39
|
+
"image",
|
|
40
|
+
"undo",
|
|
41
|
+
"redo",
|
|
42
|
+
"horizontalRule",
|
|
43
|
+
"clearFormatting",
|
|
44
|
+
// GFM pipe tables (simple grid — mergeCells/splitCell are NOT expressible)
|
|
45
|
+
"insertTable",
|
|
46
|
+
"addColumnBefore",
|
|
47
|
+
"addColumnAfter",
|
|
48
|
+
"deleteColumn",
|
|
49
|
+
"addRowBefore",
|
|
50
|
+
"addRowAfter",
|
|
51
|
+
"deleteRow",
|
|
52
|
+
"deleteTable",
|
|
53
|
+
"toggleHeaderRow",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/** True when the command exists in the given capability surface */
|
|
57
|
+
export function isCommandInCapabilities(
|
|
58
|
+
command: EditorCommand,
|
|
59
|
+
capabilities: EditorCapabilities = "full",
|
|
60
|
+
): boolean {
|
|
61
|
+
if (capabilities === "full") return true;
|
|
62
|
+
return MARKDOWN_COMMANDS.has(command);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Slash-menu items excluded in markdown capabilities. Mermaid stays out of
|
|
67
|
+
* the INSERT surface to keep it strictly GFM (a hand-typed ``` fence still
|
|
68
|
+
* produces a plain code block); math and details have no markdown syntax.
|
|
69
|
+
* Emoji stays — it inserts a plain unicode character (just text).
|
|
70
|
+
*/
|
|
71
|
+
export const MARKDOWN_EXCLUDED_SLASH_IDS: readonly string[] = [
|
|
72
|
+
"mermaid",
|
|
73
|
+
"mathBlock",
|
|
74
|
+
"inlineMath",
|
|
75
|
+
"details",
|
|
76
|
+
];
|