@echozedlabs/wysiwyg-lexical 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.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1352 -0
- package/dist/index.js.map +1 -0
- package/dist/sanitizeHtml.d.ts +12 -0
- package/dist/sanitizeHtml.d.ts.map +1 -0
- package/dist/sanitizeHtml.js +25 -0
- package/dist/sanitizeHtml.js.map +1 -0
- package/package.json +75 -0
- package/src/styles.css +456 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1352 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { $applyNodeReplacement, $createParagraphNode, $createTextNode, $getNodeByKey, $getRoot, $getSelection, $insertNodes, $isRangeSelection, COMMAND_PRIORITY_LOW, DecoratorNode, FORMAT_TEXT_COMMAND, SELECTION_CHANGE_COMMAND, createCommand, createEditor, } from 'lexical';
|
|
4
|
+
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
5
|
+
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
6
|
+
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
7
|
+
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
8
|
+
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|
9
|
+
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
|
10
|
+
import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin';
|
|
11
|
+
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
|
|
12
|
+
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
13
|
+
import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
|
|
14
|
+
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
|
15
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
16
|
+
import { $createCodeNode, $isCodeNode, CodeHighlightNode, CodeNode, registerCodeHighlighting, } from '@lexical/code';
|
|
17
|
+
import { LinkNode, AutoLinkNode } from '@lexical/link';
|
|
18
|
+
import { $isListNode, $isListItemNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, } from '@lexical/list';
|
|
19
|
+
import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode, } from '@lexical/rich-text';
|
|
20
|
+
import { $convertFromMarkdownString, $convertToMarkdownString, CHECK_LIST, TRANSFORMERS, } from '@lexical/markdown';
|
|
21
|
+
import { $setBlocksType } from '@lexical/selection';
|
|
22
|
+
import { $createTableCellNode, $createTableNode, $createTableRowNode, $getTableCellNodeFromLexicalNode, $isTableCellNode, $isTableNode, $isTableRowNode, $isTableSelection, TableCellHeaderStates, TableCellNode, TableNode, TableRowNode, } from '@lexical/table';
|
|
23
|
+
import { sanitizeDiagramHtml } from './sanitizeHtml.js';
|
|
24
|
+
const INSERT_MERMAID_COMMAND = createCommand('INSERT_MERMAID_COMMAND');
|
|
25
|
+
const INSERT_PLANTUML_COMMAND = createCommand('INSERT_PLANTUML_COMMAND');
|
|
26
|
+
const INSERT_IMAGE_COMMAND = createCommand('INSERT_IMAGE_COMMAND');
|
|
27
|
+
const INSERT_TABLE_COMMAND = createCommand('INSERT_TABLE_COMMAND');
|
|
28
|
+
const APPLY_TABLE_ACTION_COMMAND = createCommand('APPLY_TABLE_ACTION_COMMAND');
|
|
29
|
+
const DEFAULT_MERMAID_SOURCE = 'graph TD\n A[Start] --> B[Done]';
|
|
30
|
+
const DEFAULT_PLANTUML_SOURCE = '@startuml\nAlice -> Bob: Hello\n@enduml';
|
|
31
|
+
const DEFAULT_IMAGE = {
|
|
32
|
+
src: 'https://placehold.co/960x540?text=Image',
|
|
33
|
+
alt: 'Image',
|
|
34
|
+
};
|
|
35
|
+
const DEFAULT_TABLE_ROWS = [
|
|
36
|
+
['Name', 'Owner', 'Status'],
|
|
37
|
+
['Runbook', 'Platform', 'Draft'],
|
|
38
|
+
['Release notes', 'Docs', 'Ready'],
|
|
39
|
+
];
|
|
40
|
+
const WysiwygRenderServicesContext = React.createContext({});
|
|
41
|
+
const BLOCK_OPTIONS = [
|
|
42
|
+
{ value: 'paragraph', label: 'Paragraph' },
|
|
43
|
+
{ value: 'h1', label: 'Heading 1' },
|
|
44
|
+
{ value: 'h2', label: 'Heading 2' },
|
|
45
|
+
{ value: 'h3', label: 'Heading 3' },
|
|
46
|
+
{ value: 'quote', label: 'Quote' },
|
|
47
|
+
{ value: 'code', label: 'Code block' },
|
|
48
|
+
{ value: 'bullet', label: 'Bulleted list' },
|
|
49
|
+
{ value: 'number', label: 'Numbered list' },
|
|
50
|
+
{ value: 'check', label: 'Checkbox list' },
|
|
51
|
+
];
|
|
52
|
+
const CODE_LANGUAGE_OPTIONS = [
|
|
53
|
+
{ value: 'plain', label: 'Plain text' },
|
|
54
|
+
{ value: 'typescript', label: 'TypeScript' },
|
|
55
|
+
{ value: 'javascript', label: 'JavaScript' },
|
|
56
|
+
{ value: 'markup', label: 'HTML/XML' },
|
|
57
|
+
{ value: 'css', label: 'CSS' },
|
|
58
|
+
{ value: 'python', label: 'Python' },
|
|
59
|
+
{ value: 'markdown', label: 'Markdown' },
|
|
60
|
+
{ value: 'sql', label: 'SQL' },
|
|
61
|
+
{ value: 'java', label: 'Java' },
|
|
62
|
+
{ value: 'cpp', label: 'C++' },
|
|
63
|
+
{ value: 'rust', label: 'Rust' },
|
|
64
|
+
{ value: 'swift', label: 'Swift' },
|
|
65
|
+
{ value: 'powershell', label: 'PowerShell' },
|
|
66
|
+
];
|
|
67
|
+
function normalizeCodeLanguageForControl(language) {
|
|
68
|
+
if (language === 'ts' || language === 'tsx') {
|
|
69
|
+
return 'typescript';
|
|
70
|
+
}
|
|
71
|
+
if (language === 'js' || language === 'jsx') {
|
|
72
|
+
return 'javascript';
|
|
73
|
+
}
|
|
74
|
+
if (language === 'html' || language === 'xml') {
|
|
75
|
+
return 'markup';
|
|
76
|
+
}
|
|
77
|
+
if (language === 'py') {
|
|
78
|
+
return 'python';
|
|
79
|
+
}
|
|
80
|
+
if (language === 'text' || language === 'plaintext') {
|
|
81
|
+
return 'plain';
|
|
82
|
+
}
|
|
83
|
+
const normalized = language ?? 'plain';
|
|
84
|
+
return CODE_LANGUAGE_OPTIONS.some((option) => option.value === normalized) ? normalized : 'plain';
|
|
85
|
+
}
|
|
86
|
+
class MermaidNode extends DecoratorNode {
|
|
87
|
+
__source;
|
|
88
|
+
static getType() {
|
|
89
|
+
return 'mermaid';
|
|
90
|
+
}
|
|
91
|
+
static clone(node) {
|
|
92
|
+
return new MermaidNode(node.__source, node.__key);
|
|
93
|
+
}
|
|
94
|
+
static importJSON(serializedNode) {
|
|
95
|
+
return $createMermaidNode(serializedNode.source);
|
|
96
|
+
}
|
|
97
|
+
constructor(source, key) {
|
|
98
|
+
super(key);
|
|
99
|
+
this.__source = source;
|
|
100
|
+
}
|
|
101
|
+
createDOM() {
|
|
102
|
+
const element = document.createElement('div');
|
|
103
|
+
element.className = 'me-wysiwyg-mermaid-node';
|
|
104
|
+
return element;
|
|
105
|
+
}
|
|
106
|
+
updateDOM() {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
exportJSON() {
|
|
110
|
+
return {
|
|
111
|
+
...super.exportJSON(),
|
|
112
|
+
type: 'mermaid',
|
|
113
|
+
version: 1,
|
|
114
|
+
source: this.__source,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
getTextContent() {
|
|
118
|
+
return `\`\`\`mermaid\n${this.__source}\n\`\`\``;
|
|
119
|
+
}
|
|
120
|
+
setSource(source) {
|
|
121
|
+
const writable = this.getWritable();
|
|
122
|
+
writable.__source = source;
|
|
123
|
+
}
|
|
124
|
+
getSource() {
|
|
125
|
+
return this.getLatest().__source;
|
|
126
|
+
}
|
|
127
|
+
isInline() {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
isIsolated() {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
decorate(editor) {
|
|
134
|
+
return _jsx(MermaidBlock, { source: this.__source, nodeKey: this.__key, editor: editor });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
class PlantUmlNode extends DecoratorNode {
|
|
138
|
+
__source;
|
|
139
|
+
static getType() {
|
|
140
|
+
return 'plantuml';
|
|
141
|
+
}
|
|
142
|
+
static clone(node) {
|
|
143
|
+
return new PlantUmlNode(node.__source, node.__key);
|
|
144
|
+
}
|
|
145
|
+
static importJSON(serializedNode) {
|
|
146
|
+
return $createPlantUmlNode(serializedNode.source);
|
|
147
|
+
}
|
|
148
|
+
constructor(source, key) {
|
|
149
|
+
super(key);
|
|
150
|
+
this.__source = source;
|
|
151
|
+
}
|
|
152
|
+
createDOM() {
|
|
153
|
+
const element = document.createElement('div');
|
|
154
|
+
element.className = 'me-wysiwyg-plantuml-node';
|
|
155
|
+
return element;
|
|
156
|
+
}
|
|
157
|
+
updateDOM() {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
exportJSON() {
|
|
161
|
+
return {
|
|
162
|
+
...super.exportJSON(),
|
|
163
|
+
type: 'plantuml',
|
|
164
|
+
version: 1,
|
|
165
|
+
source: this.__source,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
getTextContent() {
|
|
169
|
+
return `\`\`\`plantuml\n${this.__source}\n\`\`\``;
|
|
170
|
+
}
|
|
171
|
+
setSource(source) {
|
|
172
|
+
const writable = this.getWritable();
|
|
173
|
+
writable.__source = source;
|
|
174
|
+
}
|
|
175
|
+
getSource() {
|
|
176
|
+
return this.getLatest().__source;
|
|
177
|
+
}
|
|
178
|
+
isInline() {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
isIsolated() {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
decorate(editor) {
|
|
185
|
+
return _jsx(PlantUmlBlock, { source: this.__source, nodeKey: this.__key, editor: editor });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
class ImageNode extends DecoratorNode {
|
|
189
|
+
__src;
|
|
190
|
+
__alt;
|
|
191
|
+
__title;
|
|
192
|
+
static getType() {
|
|
193
|
+
return 'image';
|
|
194
|
+
}
|
|
195
|
+
static clone(node) {
|
|
196
|
+
return new ImageNode({ src: node.__src, alt: node.__alt, title: node.__title }, node.__key);
|
|
197
|
+
}
|
|
198
|
+
static importJSON(serializedNode) {
|
|
199
|
+
return $createImageNode({
|
|
200
|
+
src: serializedNode.src,
|
|
201
|
+
alt: serializedNode.alt,
|
|
202
|
+
title: serializedNode.title,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
constructor(payload, key) {
|
|
206
|
+
super(key);
|
|
207
|
+
this.__src = payload.src;
|
|
208
|
+
this.__alt = payload.alt ?? '';
|
|
209
|
+
this.__title = payload.title;
|
|
210
|
+
}
|
|
211
|
+
createDOM() {
|
|
212
|
+
const element = document.createElement('div');
|
|
213
|
+
element.className = 'me-wysiwyg-image-node';
|
|
214
|
+
return element;
|
|
215
|
+
}
|
|
216
|
+
updateDOM() {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
exportJSON() {
|
|
220
|
+
return {
|
|
221
|
+
...super.exportJSON(),
|
|
222
|
+
type: 'image',
|
|
223
|
+
version: 1,
|
|
224
|
+
src: this.__src,
|
|
225
|
+
alt: this.__alt,
|
|
226
|
+
title: this.__title,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
getTextContent() {
|
|
230
|
+
return formatImageMarkdown(this.getImage());
|
|
231
|
+
}
|
|
232
|
+
setImage(payload) {
|
|
233
|
+
const writable = this.getWritable();
|
|
234
|
+
writable.__src = payload.src;
|
|
235
|
+
writable.__alt = payload.alt ?? '';
|
|
236
|
+
writable.__title = payload.title;
|
|
237
|
+
}
|
|
238
|
+
getImage() {
|
|
239
|
+
const latest = this.getLatest();
|
|
240
|
+
return {
|
|
241
|
+
src: latest.__src,
|
|
242
|
+
alt: latest.__alt,
|
|
243
|
+
title: latest.__title,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
isInline() {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
isIsolated() {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
decorate(editor) {
|
|
253
|
+
return _jsx(ImageBlock, { image: this.getImage(), nodeKey: this.__key, editor: editor });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function $createMermaidNode(source) {
|
|
257
|
+
return $applyNodeReplacement(new MermaidNode(source));
|
|
258
|
+
}
|
|
259
|
+
function $isMermaidNode(node) {
|
|
260
|
+
return node instanceof MermaidNode;
|
|
261
|
+
}
|
|
262
|
+
function $createPlantUmlNode(source) {
|
|
263
|
+
return $applyNodeReplacement(new PlantUmlNode(source));
|
|
264
|
+
}
|
|
265
|
+
function $isPlantUmlNode(node) {
|
|
266
|
+
return node instanceof PlantUmlNode;
|
|
267
|
+
}
|
|
268
|
+
function $createImageNode(payload) {
|
|
269
|
+
return $applyNodeReplacement(new ImageNode(payload));
|
|
270
|
+
}
|
|
271
|
+
function $isImageNode(node) {
|
|
272
|
+
return node instanceof ImageNode;
|
|
273
|
+
}
|
|
274
|
+
const MERMAID_TRANSFORMER = {
|
|
275
|
+
type: 'multiline-element',
|
|
276
|
+
dependencies: [MermaidNode],
|
|
277
|
+
regExpStart: /^```mermaid\s*$/,
|
|
278
|
+
regExpEnd: /^```\s*$/,
|
|
279
|
+
replace(rootNode, _children, _startMatch, _endMatch, linesInBetween) {
|
|
280
|
+
rootNode.append($createMermaidNode((linesInBetween ?? []).join('\n')));
|
|
281
|
+
},
|
|
282
|
+
export(node) {
|
|
283
|
+
if (!$isMermaidNode(node)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return `\`\`\`mermaid\n${node.getSource()}\n\`\`\``;
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
const PLANTUML_TRANSFORMER = {
|
|
290
|
+
type: 'multiline-element',
|
|
291
|
+
dependencies: [PlantUmlNode],
|
|
292
|
+
regExpStart: /^```(?:plantuml|puml)\s*$/,
|
|
293
|
+
regExpEnd: /^```\s*$/,
|
|
294
|
+
replace(rootNode, _children, _startMatch, _endMatch, linesInBetween) {
|
|
295
|
+
rootNode.append($createPlantUmlNode((linesInBetween ?? []).join('\n')));
|
|
296
|
+
},
|
|
297
|
+
export(node) {
|
|
298
|
+
if (!$isPlantUmlNode(node)) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
return `\`\`\`plantuml\n${node.getSource()}\n\`\`\``;
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
const IMAGE_TRANSFORMER = {
|
|
305
|
+
type: 'element',
|
|
306
|
+
dependencies: [ImageNode],
|
|
307
|
+
regExp: /^!\[([^\]]*)\]\((\S+?)(?:\s+"((?:[^"\\]|\\.)*)")?\)\s*$/,
|
|
308
|
+
replace(parentNode, _children, match) {
|
|
309
|
+
const [, alt, src, title] = match;
|
|
310
|
+
parentNode.replace($createImageNode({
|
|
311
|
+
src: unescapeMarkdownAttribute(src ?? ''),
|
|
312
|
+
alt: unescapeMarkdownAttribute(alt ?? ''),
|
|
313
|
+
title: title ? unescapeMarkdownAttribute(title) : undefined,
|
|
314
|
+
}));
|
|
315
|
+
},
|
|
316
|
+
export(node) {
|
|
317
|
+
if (!$isImageNode(node)) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return formatImageMarkdown(node.getImage());
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
const TABLE_TRANSFORMER = {
|
|
324
|
+
type: 'multiline-element',
|
|
325
|
+
dependencies: [TableNode, TableRowNode, TableCellNode],
|
|
326
|
+
regExpStart: /^ {0,3}\|.*\|\s*$/,
|
|
327
|
+
handleImportAfterStartMatch({ lines, rootNode, startLineIndex }) {
|
|
328
|
+
const headerLine = lines[startLineIndex] ?? '';
|
|
329
|
+
const separatorLine = lines[startLineIndex + 1] ?? '';
|
|
330
|
+
if (!isMarkdownTableRowLine(headerLine) || !isMarkdownTableSeparatorLine(separatorLine)) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
const headerCells = parseMarkdownTableRow(headerLine);
|
|
334
|
+
const separatorCells = parseMarkdownTableRow(separatorLine);
|
|
335
|
+
if (headerCells.length === 0 || separatorCells.length !== headerCells.length) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
let endLineIndex = startLineIndex + 1;
|
|
339
|
+
const rows = [headerCells];
|
|
340
|
+
for (let lineIndex = startLineIndex + 2; lineIndex < lines.length; lineIndex += 1) {
|
|
341
|
+
const line = lines[lineIndex] ?? '';
|
|
342
|
+
if (!isMarkdownTableRowLine(line)) {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
rows.push(normalizeMarkdownTableRow(parseMarkdownTableRow(line), headerCells.length));
|
|
346
|
+
endLineIndex = lineIndex;
|
|
347
|
+
}
|
|
348
|
+
rootNode.append($createMarkdownTableNode(rows));
|
|
349
|
+
return [true, endLineIndex];
|
|
350
|
+
},
|
|
351
|
+
replace() {
|
|
352
|
+
return false;
|
|
353
|
+
},
|
|
354
|
+
export(node) {
|
|
355
|
+
if (!$isTableNode(node)) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
return formatMarkdownTableNode(node);
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
const WYSIWYG_TRANSFORMERS = [
|
|
362
|
+
IMAGE_TRANSFORMER,
|
|
363
|
+
TABLE_TRANSFORMER,
|
|
364
|
+
MERMAID_TRANSFORMER,
|
|
365
|
+
PLANTUML_TRANSFORMER,
|
|
366
|
+
CHECK_LIST,
|
|
367
|
+
...TRANSFORMERS,
|
|
368
|
+
];
|
|
369
|
+
const WYSIWYG_NODES = [
|
|
370
|
+
HeadingNode,
|
|
371
|
+
QuoteNode,
|
|
372
|
+
ListNode,
|
|
373
|
+
ListItemNode,
|
|
374
|
+
LinkNode,
|
|
375
|
+
AutoLinkNode,
|
|
376
|
+
CodeNode,
|
|
377
|
+
CodeHighlightNode,
|
|
378
|
+
TableNode,
|
|
379
|
+
TableRowNode,
|
|
380
|
+
TableCellNode,
|
|
381
|
+
ImageNode,
|
|
382
|
+
MermaidNode,
|
|
383
|
+
PlantUmlNode,
|
|
384
|
+
];
|
|
385
|
+
const UNORDERED_ITEM = /^ *[-*+] /;
|
|
386
|
+
const CODE_FENCE = /^ *(```|~~~)/;
|
|
387
|
+
/**
|
|
388
|
+
* Merge adjacent unordered lists that Lexical serialized with a blank line
|
|
389
|
+
* between them. Lexical models a checkbox list and a plain bullet list as
|
|
390
|
+
* different node types, so `- [ ] task` followed by `- bullet` exports as two
|
|
391
|
+
* lists separated by a blank line — which is the "weird blank lines" a checkbox
|
|
392
|
+
* introduces when list types are mixed. In Markdown both share the `-` marker
|
|
393
|
+
* and render as a single list, so dropping the blank line is faithful and makes
|
|
394
|
+
* the round-trip tight. Same-type lists are already merged by Lexical; this only
|
|
395
|
+
* affects the check<->bullet boundary. Fenced code blocks are skipped so a code
|
|
396
|
+
* line that merely looks like a list item is never touched.
|
|
397
|
+
*/
|
|
398
|
+
function mergeAdjacentUnorderedLists(markdown) {
|
|
399
|
+
const lines = markdown.split('\n');
|
|
400
|
+
const out = [];
|
|
401
|
+
let inFence = false;
|
|
402
|
+
for (let i = 0; i < lines.length; i++) {
|
|
403
|
+
const line = lines[i];
|
|
404
|
+
if (CODE_FENCE.test(line)) {
|
|
405
|
+
inFence = !inFence;
|
|
406
|
+
}
|
|
407
|
+
if (!inFence &&
|
|
408
|
+
line === '' &&
|
|
409
|
+
UNORDERED_ITEM.test(out[out.length - 1] ?? '') &&
|
|
410
|
+
UNORDERED_ITEM.test(lines[i + 1] ?? '')) {
|
|
411
|
+
continue; // drop the blank line between two adjacent unordered list items
|
|
412
|
+
}
|
|
413
|
+
out.push(line);
|
|
414
|
+
}
|
|
415
|
+
return out.join('\n');
|
|
416
|
+
}
|
|
417
|
+
/** Serialize the current editor body to Markdown with list normalization applied. */
|
|
418
|
+
function serializeWysiwygBody() {
|
|
419
|
+
return mergeAdjacentUnorderedLists($convertToMarkdownString(WYSIWYG_TRANSFORMERS));
|
|
420
|
+
}
|
|
421
|
+
export function WysiwygLexicalEditor({ markdown, readOnly = false, ariaLabel = 'Rich text Markdown editor', placeholder = 'Start typing...', renderServices, toolbarIcons, onChange, onDiagnostics, }) {
|
|
422
|
+
const lastEmittedMarkdown = React.useRef(null);
|
|
423
|
+
const renderServiceValue = React.useMemo(() => ({
|
|
424
|
+
...renderServices,
|
|
425
|
+
reportDiagnostics: onDiagnostics,
|
|
426
|
+
}), [onDiagnostics, renderServices]);
|
|
427
|
+
const editorConfig = React.useMemo(() => ({
|
|
428
|
+
namespace: 'MarkdownEditorWysiwyg',
|
|
429
|
+
nodes: WYSIWYG_NODES,
|
|
430
|
+
editable: !readOnly,
|
|
431
|
+
theme: lexicalTheme,
|
|
432
|
+
onError(error) {
|
|
433
|
+
onDiagnostics?.([
|
|
434
|
+
{
|
|
435
|
+
code: 'wysiwyg.lexical.error',
|
|
436
|
+
message: error.message,
|
|
437
|
+
severity: 'error',
|
|
438
|
+
source: 'mode',
|
|
439
|
+
details: error,
|
|
440
|
+
},
|
|
441
|
+
]);
|
|
442
|
+
},
|
|
443
|
+
editorState: () => {
|
|
444
|
+
importMarkdownBody(markdown);
|
|
445
|
+
},
|
|
446
|
+
}), []);
|
|
447
|
+
return (_jsx("div", { className: "me-wysiwyg", "data-readonly": readOnly ? 'true' : 'false', children: _jsx(WysiwygRenderServicesContext.Provider, { value: renderServiceValue, children: _jsxs(LexicalComposer, { initialConfig: editorConfig, children: [_jsx(EditableStatePlugin, { readOnly: readOnly }), _jsx(ExternalMarkdownPlugin, { markdown: markdown, lastEmittedMarkdown: lastEmittedMarkdown }), _jsx(WysiwygCommandPlugin, {}), !readOnly ? _jsx(WysiwygToolbar, { icons: toolbarIcons }) : null, !readOnly ? _jsx(WysiwygCodeLanguagePlugin, {}) : null, _jsx(WysiwygCodeHighlightPlugin, {}), _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "me-wysiwyg-input", "aria-label": ariaLabel, spellCheck: false }), placeholder: _jsx("div", { className: "me-wysiwyg-placeholder-text", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }), _jsx(HistoryPlugin, {}), _jsx(ListPlugin, {}), _jsx(CheckListPlugin, {}), !readOnly ? _jsx(MarkdownShortcutPlugin, { transformers: WYSIWYG_TRANSFORMERS }) : null, _jsx(LinkPlugin, {}), _jsx(TablePlugin, { hasCellMerge: false, hasCellBackgroundColor: false, hasHorizontalScroll: true }), _jsx(WysiwygChangePlugin, { sourceMarkdown: markdown, lastEmittedMarkdown: lastEmittedMarkdown, onChange: onChange })] }) }) }));
|
|
448
|
+
}
|
|
449
|
+
export function roundTripWysiwygMarkdown(markdown) {
|
|
450
|
+
const envelope = splitMarkdownEnvelope(markdown);
|
|
451
|
+
const editor = createEditor({
|
|
452
|
+
namespace: 'MarkdownEditorWysiwygHeadless',
|
|
453
|
+
nodes: WYSIWYG_NODES,
|
|
454
|
+
theme: lexicalTheme,
|
|
455
|
+
onError(error) {
|
|
456
|
+
throw error;
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
editor.update(() => {
|
|
460
|
+
importMarkdownBody(markdown);
|
|
461
|
+
}, { discrete: true });
|
|
462
|
+
let body = '';
|
|
463
|
+
editor.getEditorState().read(() => {
|
|
464
|
+
body = serializeWysiwygBody();
|
|
465
|
+
});
|
|
466
|
+
return replaceEnvelopeBody(envelope, body);
|
|
467
|
+
}
|
|
468
|
+
/** @internal Test-only semantic inspection helper; not part of the stable package API. */
|
|
469
|
+
export function inspectWysiwygMarkdownForTests(markdown) {
|
|
470
|
+
const editor = createEditor({
|
|
471
|
+
namespace: 'MarkdownEditorWysiwygHeadlessInspect',
|
|
472
|
+
nodes: WYSIWYG_NODES,
|
|
473
|
+
theme: lexicalTheme,
|
|
474
|
+
onError(error) {
|
|
475
|
+
throw error;
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
editor.update(() => {
|
|
479
|
+
importMarkdownBody(markdown);
|
|
480
|
+
}, { discrete: true });
|
|
481
|
+
const lists = [];
|
|
482
|
+
editor.getEditorState().read(() => {
|
|
483
|
+
for (const node of $getRoot().getChildren()) {
|
|
484
|
+
if (!$isListNode(node)) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
lists.push({
|
|
488
|
+
type: 'list',
|
|
489
|
+
listType: node.getListType(),
|
|
490
|
+
items: node.getChildren().map((child) => ({
|
|
491
|
+
checked: $isListItemNode(child) ? child.getChecked() : undefined,
|
|
492
|
+
text: child.getTextContent(),
|
|
493
|
+
})),
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
return lists;
|
|
498
|
+
}
|
|
499
|
+
/** @internal Test-only semantic inspection helper; not part of the stable package API. */
|
|
500
|
+
export function inspectWysiwygMarkdownTablesForTests(markdown) {
|
|
501
|
+
const editor = createEditor({
|
|
502
|
+
namespace: 'MarkdownEditorWysiwygHeadlessInspectTables',
|
|
503
|
+
nodes: WYSIWYG_NODES,
|
|
504
|
+
theme: lexicalTheme,
|
|
505
|
+
onError(error) {
|
|
506
|
+
throw error;
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
editor.update(() => {
|
|
510
|
+
importMarkdownBody(markdown);
|
|
511
|
+
}, { discrete: true });
|
|
512
|
+
const tables = [];
|
|
513
|
+
editor.getEditorState().read(() => {
|
|
514
|
+
for (const node of $getRoot().getChildren()) {
|
|
515
|
+
if (!$isTableNode(node)) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
tables.push({
|
|
519
|
+
type: 'table',
|
|
520
|
+
rows: getTableNodeRows(node),
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
return tables;
|
|
525
|
+
}
|
|
526
|
+
/** @internal Test-only table action helper; not part of the stable package API. */
|
|
527
|
+
export function applyWysiwygTableActionForTests(markdown, action, target = {}) {
|
|
528
|
+
const envelope = splitMarkdownEnvelope(markdown);
|
|
529
|
+
const editor = createEditor({
|
|
530
|
+
namespace: 'MarkdownEditorWysiwygHeadlessTableAction',
|
|
531
|
+
nodes: WYSIWYG_NODES,
|
|
532
|
+
theme: lexicalTheme,
|
|
533
|
+
onError(error) {
|
|
534
|
+
throw error;
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
editor.update(() => {
|
|
538
|
+
importMarkdownBody(markdown);
|
|
539
|
+
const tableNode = $getRoot().getChildren().find($isTableNode);
|
|
540
|
+
if (!$isTableNode(tableNode)) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
$applyMarkdownTableActionAt(tableNode, action, target.rowIndex ?? 0, target.columnIndex ?? 0);
|
|
544
|
+
}, { discrete: true });
|
|
545
|
+
let body = '';
|
|
546
|
+
editor.getEditorState().read(() => {
|
|
547
|
+
body = serializeWysiwygBody();
|
|
548
|
+
});
|
|
549
|
+
return replaceEnvelopeBody(envelope, body);
|
|
550
|
+
}
|
|
551
|
+
function WysiwygCommandPlugin() {
|
|
552
|
+
const [editor] = useLexicalComposerContext();
|
|
553
|
+
React.useEffect(() => {
|
|
554
|
+
const removeMermaidCommand = editor.registerCommand(INSERT_MERMAID_COMMAND, (source) => {
|
|
555
|
+
$insertNodes([$createMermaidNode(source), $createParagraphNode()]);
|
|
556
|
+
return true;
|
|
557
|
+
}, COMMAND_PRIORITY_LOW);
|
|
558
|
+
const removePlantUmlCommand = editor.registerCommand(INSERT_PLANTUML_COMMAND, (source) => {
|
|
559
|
+
$insertNodes([$createPlantUmlNode(source), $createParagraphNode()]);
|
|
560
|
+
return true;
|
|
561
|
+
}, COMMAND_PRIORITY_LOW);
|
|
562
|
+
const removeImageCommand = editor.registerCommand(INSERT_IMAGE_COMMAND, (payload) => {
|
|
563
|
+
$insertNodes([$createImageNode(payload), $createParagraphNode()]);
|
|
564
|
+
return true;
|
|
565
|
+
}, COMMAND_PRIORITY_LOW);
|
|
566
|
+
const removeTableCommand = editor.registerCommand(INSERT_TABLE_COMMAND, () => {
|
|
567
|
+
const tableNode = $createMarkdownTableNode(DEFAULT_TABLE_ROWS);
|
|
568
|
+
$insertNodes([tableNode, $createParagraphNode()]);
|
|
569
|
+
tableNode.selectStart();
|
|
570
|
+
return true;
|
|
571
|
+
}, COMMAND_PRIORITY_LOW);
|
|
572
|
+
const removeTableActionCommand = editor.registerCommand(APPLY_TABLE_ACTION_COMMAND, (action) => {
|
|
573
|
+
const context = $getActiveMarkdownTableContext();
|
|
574
|
+
if (context === null) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
return $applyMarkdownTableActionAt(context.tableNode, action, context.rowIndex, context.columnIndex);
|
|
578
|
+
}, COMMAND_PRIORITY_LOW);
|
|
579
|
+
return () => {
|
|
580
|
+
removeMermaidCommand();
|
|
581
|
+
removePlantUmlCommand();
|
|
582
|
+
removeImageCommand();
|
|
583
|
+
removeTableCommand();
|
|
584
|
+
removeTableActionCommand();
|
|
585
|
+
};
|
|
586
|
+
}, [editor]);
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
function WysiwygCodeHighlightPlugin() {
|
|
590
|
+
const [editor] = useLexicalComposerContext();
|
|
591
|
+
React.useEffect(() => registerCodeHighlighting(editor), [editor]);
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
function WysiwygCodeLanguagePlugin() {
|
|
595
|
+
const [editor] = useLexicalComposerContext();
|
|
596
|
+
const [activeCode, setActiveCode] = React.useState(null);
|
|
597
|
+
const updateCodeLanguageState = React.useCallback(() => {
|
|
598
|
+
const selection = $getSelection();
|
|
599
|
+
if (!$isRangeSelection(selection)) {
|
|
600
|
+
setActiveCode(null);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const topLevelNode = selection.anchor.getNode().getTopLevelElement();
|
|
604
|
+
if (!$isCodeNode(topLevelNode)) {
|
|
605
|
+
setActiveCode(null);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const codeKey = topLevelNode.getKey();
|
|
609
|
+
const language = normalizeCodeLanguageForControl(topLevelNode.getLanguage());
|
|
610
|
+
window.requestAnimationFrame(() => {
|
|
611
|
+
const codeElement = editor.getElementByKey(codeKey);
|
|
612
|
+
const rootElement = editor.getRootElement();
|
|
613
|
+
const container = rootElement?.closest('.me-wysiwyg') ?? null;
|
|
614
|
+
if (codeElement === null || container === null) {
|
|
615
|
+
setActiveCode(null);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const codeRect = codeElement.getBoundingClientRect();
|
|
619
|
+
const containerRect = container.getBoundingClientRect();
|
|
620
|
+
setActiveCode({
|
|
621
|
+
key: codeKey,
|
|
622
|
+
language,
|
|
623
|
+
top: codeRect.top - containerRect.top + container.scrollTop + 6,
|
|
624
|
+
left: codeRect.left - containerRect.left + container.scrollLeft + 6,
|
|
625
|
+
width: Math.max(160, codeRect.width - 12),
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
}, [editor]);
|
|
629
|
+
const readCodeLanguageState = React.useCallback(() => {
|
|
630
|
+
editor.getEditorState().read(updateCodeLanguageState);
|
|
631
|
+
}, [editor, updateCodeLanguageState]);
|
|
632
|
+
React.useEffect(() => {
|
|
633
|
+
readCodeLanguageState();
|
|
634
|
+
const removeUpdateListener = editor.registerUpdateListener(({ editorState }) => {
|
|
635
|
+
editorState.read(updateCodeLanguageState);
|
|
636
|
+
});
|
|
637
|
+
const removeSelectionListener = editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
638
|
+
readCodeLanguageState();
|
|
639
|
+
return false;
|
|
640
|
+
}, COMMAND_PRIORITY_LOW);
|
|
641
|
+
const rootElement = editor.getRootElement();
|
|
642
|
+
const container = rootElement?.closest('.me-wysiwyg');
|
|
643
|
+
container?.addEventListener('scroll', readCodeLanguageState);
|
|
644
|
+
window.addEventListener('resize', readCodeLanguageState);
|
|
645
|
+
return () => {
|
|
646
|
+
removeUpdateListener();
|
|
647
|
+
removeSelectionListener();
|
|
648
|
+
container?.removeEventListener('scroll', readCodeLanguageState);
|
|
649
|
+
window.removeEventListener('resize', readCodeLanguageState);
|
|
650
|
+
};
|
|
651
|
+
}, [editor, readCodeLanguageState, updateCodeLanguageState]);
|
|
652
|
+
if (activeCode === null) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
return (_jsxs("div", { className: "me-wysiwyg-code-language-popover", style: { top: activeCode.top, left: activeCode.left, width: activeCode.width }, children: [_jsx("span", { children: "Code" }), _jsx("select", { "aria-label": "Code block language", value: activeCode.language, onChange: (event) => {
|
|
656
|
+
const nextLanguage = event.currentTarget.value;
|
|
657
|
+
editor.update(() => {
|
|
658
|
+
const node = $getNodeByKey(activeCode.key);
|
|
659
|
+
if ($isCodeNode(node)) {
|
|
660
|
+
node.setLanguage(nextLanguage);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
setActiveCode((current) => current === null ? current : { ...current, language: nextLanguage });
|
|
664
|
+
}, children: CODE_LANGUAGE_OPTIONS.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) })] }));
|
|
665
|
+
}
|
|
666
|
+
function WysiwygToolbar({ icons = {} }) {
|
|
667
|
+
const [editor] = useLexicalComposerContext();
|
|
668
|
+
const [activeBlock, setActiveBlock] = React.useState('paragraph');
|
|
669
|
+
const [activeTable, setActiveTable] = React.useState(null);
|
|
670
|
+
const [isBold, setIsBold] = React.useState(false);
|
|
671
|
+
const [isItalic, setIsItalic] = React.useState(false);
|
|
672
|
+
const [isCode, setIsCode] = React.useState(false);
|
|
673
|
+
const updateToolbarState = React.useCallback(() => {
|
|
674
|
+
const selection = $getSelection();
|
|
675
|
+
const tableContext = $getActiveMarkdownTableContext();
|
|
676
|
+
setActiveTable(tableContext === null ? null : {
|
|
677
|
+
rowCount: tableContext.rowCount,
|
|
678
|
+
columnCount: tableContext.columnCount,
|
|
679
|
+
});
|
|
680
|
+
if (!$isRangeSelection(selection) && !$isTableSelection(selection)) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
setIsBold(selection.hasFormat('bold'));
|
|
684
|
+
setIsItalic(selection.hasFormat('italic'));
|
|
685
|
+
setIsCode(selection.hasFormat('code'));
|
|
686
|
+
if (!$isRangeSelection(selection)) {
|
|
687
|
+
setActiveBlock('paragraph');
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const anchorNode = selection.anchor.getNode();
|
|
691
|
+
const topLevelNode = anchorNode.getTopLevelElement();
|
|
692
|
+
if (topLevelNode === null) {
|
|
693
|
+
setActiveBlock('paragraph');
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if ($isHeadingNode(topLevelNode)) {
|
|
697
|
+
const tag = topLevelNode.getTag();
|
|
698
|
+
setActiveBlock(tag === 'h1' || tag === 'h2' || tag === 'h3' ? tag : 'paragraph');
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if ($isQuoteNode(topLevelNode)) {
|
|
702
|
+
setActiveBlock('quote');
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if ($isCodeNode(topLevelNode)) {
|
|
706
|
+
setActiveBlock('code');
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if ($isListNode(topLevelNode)) {
|
|
710
|
+
const listType = topLevelNode.getListType();
|
|
711
|
+
setActiveBlock(listType === 'number' ? 'number' : listType === 'check' ? 'check' : 'bullet');
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
setActiveBlock('paragraph');
|
|
715
|
+
}, []);
|
|
716
|
+
React.useEffect(() => {
|
|
717
|
+
editor.getEditorState().read(updateToolbarState);
|
|
718
|
+
const removeUpdateListener = editor.registerUpdateListener(({ editorState }) => {
|
|
719
|
+
editorState.read(updateToolbarState);
|
|
720
|
+
});
|
|
721
|
+
const removeSelectionListener = editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
722
|
+
updateToolbarState();
|
|
723
|
+
return false;
|
|
724
|
+
}, COMMAND_PRIORITY_LOW);
|
|
725
|
+
return () => {
|
|
726
|
+
removeUpdateListener();
|
|
727
|
+
removeSelectionListener();
|
|
728
|
+
};
|
|
729
|
+
}, [editor, updateToolbarState]);
|
|
730
|
+
const setBlock = React.useCallback((block) => {
|
|
731
|
+
if (block === 'bullet') {
|
|
732
|
+
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (block === 'number') {
|
|
736
|
+
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (block === 'check') {
|
|
740
|
+
editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
editor.update(() => {
|
|
744
|
+
const selection = $getSelection();
|
|
745
|
+
if (!$isRangeSelection(selection)) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (block === 'paragraph') {
|
|
749
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (block === 'quote') {
|
|
753
|
+
$setBlocksType(selection, () => $createQuoteNode());
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (block === 'code') {
|
|
757
|
+
$setBlocksType(selection, () => $createCodeNode('typescript'));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
$setBlocksType(selection, () => $createHeadingNode(block));
|
|
761
|
+
});
|
|
762
|
+
}, [editor]);
|
|
763
|
+
const insertCodeBlock = React.useCallback(() => {
|
|
764
|
+
editor.update(() => {
|
|
765
|
+
const codeNode = $createCodeNode('typescript');
|
|
766
|
+
codeNode.append($createTextNode('const value = 1;'));
|
|
767
|
+
$insertNodes([codeNode, $createParagraphNode()]);
|
|
768
|
+
codeNode.selectStart();
|
|
769
|
+
});
|
|
770
|
+
}, [editor]);
|
|
771
|
+
const insertBlock = React.useCallback((block) => {
|
|
772
|
+
if (block === 'code') {
|
|
773
|
+
insertCodeBlock();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (block === 'table') {
|
|
777
|
+
editor.dispatchCommand(INSERT_TABLE_COMMAND, undefined);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (block === 'image') {
|
|
781
|
+
editor.dispatchCommand(INSERT_IMAGE_COMMAND, DEFAULT_IMAGE);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (block === 'mermaid') {
|
|
785
|
+
editor.dispatchCommand(INSERT_MERMAID_COMMAND, DEFAULT_MERMAID_SOURCE);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
editor.dispatchCommand(INSERT_PLANTUML_COMMAND, DEFAULT_PLANTUML_SOURCE);
|
|
789
|
+
}, [editor, insertCodeBlock]);
|
|
790
|
+
const applyTableAction = React.useCallback((action) => {
|
|
791
|
+
editor.dispatchCommand(APPLY_TABLE_ACTION_COMMAND, action);
|
|
792
|
+
}, [editor]);
|
|
793
|
+
return (_jsxs("div", { className: "me-wysiwyg-toolbar", role: "toolbar", "aria-label": "Rich text formatting controls", children: [_jsx("span", { className: "me-wysiwyg-toolbar-group", "aria-label": "Block formatting", children: _jsx("select", { className: "me-wysiwyg-block-select", "aria-label": "Current block style", value: activeBlock, onChange: (event) => setBlock(event.currentTarget.value), children: BLOCK_OPTIONS.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) }) }), _jsxs("span", { className: "me-wysiwyg-toolbar-group", "aria-label": "Inline formatting", children: [_jsx("button", { type: "button", title: "Bold", "aria-pressed": isBold, "data-active": isBold ? 'true' : 'false', onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold'), children: icons.bold ?? 'B' }), _jsx("button", { type: "button", title: "Italic", "aria-pressed": isItalic, "data-active": isItalic ? 'true' : 'false', onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic'), children: icons.italic ?? 'I' }), _jsx("button", { type: "button", title: "Inline code", "aria-pressed": isCode, "data-active": isCode ? 'true' : 'false', onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code'), children: icons.inlineCode ?? '</>' })] }), _jsxs("span", { className: "me-wysiwyg-toolbar-group", "aria-label": "List formatting", children: [_jsx("button", { type: "button", "aria-label": "Bulleted list", title: "Bulleted list", "aria-pressed": activeBlock === 'bullet', "data-active": activeBlock === 'bullet' ? 'true' : 'false', onClick: () => setBlock('bullet'), children: icons.bulletedList ?? '•' }), _jsx("button", { type: "button", "aria-label": "Numbered list", title: "Numbered list", "aria-pressed": activeBlock === 'number', "data-active": activeBlock === 'number' ? 'true' : 'false', onClick: () => setBlock('number'), children: icons.numberedList ?? '1.' }), _jsx("button", { type: "button", "aria-label": "Checkbox list", title: "Checkbox list", "aria-pressed": activeBlock === 'check', "data-active": activeBlock === 'check' ? 'true' : 'false', onClick: () => setBlock('check'), children: icons.checkboxList ?? '☑' })] }), _jsx("span", { className: "me-wysiwyg-toolbar-group", "aria-label": "Insert blocks", children: _jsxs("select", { className: "me-wysiwyg-insert-select", "aria-label": "Insert block", value: "", onChange: (event) => {
|
|
794
|
+
insertBlock(event.currentTarget.value);
|
|
795
|
+
}, children: [_jsx("option", { value: "", disabled: true, children: "Insert" }), _jsx("option", { value: "code", children: "Code block" }), _jsx("option", { value: "table", children: "Table" }), _jsx("option", { value: "image", children: "Image" }), _jsx("option", { value: "mermaid", children: "Mermaid diagram" }), _jsx("option", { value: "plantuml", children: "PlantUML diagram" })] }) }), activeTable === null ? null : (_jsxs("span", { className: "me-wysiwyg-toolbar-group", "aria-label": "Table operations", children: [_jsx("button", { className: "me-wysiwyg-table-action", type: "button", "aria-label": "Insert table row", title: "Insert table row", onClick: () => applyTableAction('insert-row'), children: "R+" }), _jsx("button", { className: "me-wysiwyg-table-action", type: "button", "aria-label": "Insert table column", title: "Insert table column", onClick: () => applyTableAction('insert-column'), children: "C+" }), _jsx("button", { className: "me-wysiwyg-table-action", type: "button", "aria-label": "Delete table row", title: "Delete table row", disabled: activeTable.rowCount <= 1, onClick: () => applyTableAction('delete-row'), children: "R-" }), _jsx("button", { className: "me-wysiwyg-table-action", type: "button", "aria-label": "Delete table column", title: "Delete table column", disabled: activeTable.columnCount <= 1, onClick: () => applyTableAction('delete-column'), children: "C-" })] }))] }));
|
|
796
|
+
}
|
|
797
|
+
function MermaidBlock({ source, nodeKey, editor, }) {
|
|
798
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
799
|
+
const [draft, setDraft] = React.useState(source);
|
|
800
|
+
const [html, setHtml] = React.useState('');
|
|
801
|
+
const [error, setError] = React.useState(null);
|
|
802
|
+
const renderServices = React.useContext(WysiwygRenderServicesContext);
|
|
803
|
+
React.useEffect(() => {
|
|
804
|
+
setDraft(source);
|
|
805
|
+
}, [source]);
|
|
806
|
+
React.useEffect(() => {
|
|
807
|
+
if (isEditing) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
let disposed = false;
|
|
811
|
+
const diagramId = `me-wysiwyg-mermaid-${nodeKey.replace(/[^a-zA-Z0-9_-]/g, '-')}`;
|
|
812
|
+
setError(null);
|
|
813
|
+
setHtml('');
|
|
814
|
+
import('mermaid')
|
|
815
|
+
.then(async (module) => {
|
|
816
|
+
const renderer = module.default;
|
|
817
|
+
// Top-level htmlLabels:false -> SVG <text> labels that survive HTML
|
|
818
|
+
// sanitization (DOMPurify strips foreignObject HTML labels, hiding text).
|
|
819
|
+
// Mermaid v11 only honors the top-level flag for flowcharts.
|
|
820
|
+
renderer.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default', htmlLabels: false, flowchart: { htmlLabels: false } });
|
|
821
|
+
const rendered = await renderer.render(diagramId, source);
|
|
822
|
+
if (!disposed) {
|
|
823
|
+
setHtml(rendered.svg);
|
|
824
|
+
}
|
|
825
|
+
})
|
|
826
|
+
.catch((cause) => {
|
|
827
|
+
if (!disposed) {
|
|
828
|
+
const message = formatMermaidRenderError(cause);
|
|
829
|
+
setError(message);
|
|
830
|
+
renderServices.reportDiagnostics?.([{
|
|
831
|
+
code: 'wysiwyg.mermaid.render.failed',
|
|
832
|
+
message,
|
|
833
|
+
severity: 'error',
|
|
834
|
+
source: 'renderer',
|
|
835
|
+
details: cause,
|
|
836
|
+
}]);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
return () => {
|
|
840
|
+
disposed = true;
|
|
841
|
+
};
|
|
842
|
+
}, [isEditing, nodeKey, renderServices, source]);
|
|
843
|
+
function saveSource() {
|
|
844
|
+
editor.update(() => {
|
|
845
|
+
const node = $getNodeByKey(nodeKey);
|
|
846
|
+
if ($isMermaidNode(node)) {
|
|
847
|
+
node.setSource(draft);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
setIsEditing(false);
|
|
851
|
+
}
|
|
852
|
+
return (_jsxs("figure", { className: "me-wysiwyg-mermaid", children: [_jsxs("figcaption", { children: [_jsx("span", { children: "Mermaid" }), _jsx("button", { type: "button", onClick: () => setIsEditing((current) => !current), children: isEditing ? 'Preview' : 'Edit' })] }), isEditing ? (_jsxs("div", { className: "me-wysiwyg-mermaid-editor", children: [_jsx("textarea", { "aria-label": "Mermaid diagram source", value: draft, onChange: (event) => setDraft(event.currentTarget.value), rows: 7 }), _jsx("button", { type: "button", onClick: saveSource, children: "Apply" })] })) : error ? (_jsx("pre", { className: "me-wysiwyg-mermaid-error", children: error })) : html ? (_jsx("div", { className: "me-wysiwyg-mermaid-rendered", dangerouslySetInnerHTML: { __html: sanitizeDiagramHtml(html) } })) : (_jsx("div", { className: "me-wysiwyg-mermaid-loading", children: "Rendering diagram..." }))] }));
|
|
853
|
+
}
|
|
854
|
+
function PlantUmlBlock({ source, nodeKey, editor, }) {
|
|
855
|
+
const services = React.useContext(WysiwygRenderServicesContext);
|
|
856
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
857
|
+
const [draft, setDraft] = React.useState(source);
|
|
858
|
+
const [html, setHtml] = React.useState('');
|
|
859
|
+
const [error, setError] = React.useState(null);
|
|
860
|
+
React.useEffect(() => {
|
|
861
|
+
setDraft(source);
|
|
862
|
+
}, [source]);
|
|
863
|
+
React.useEffect(() => {
|
|
864
|
+
if (isEditing) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (!services.renderPlantUml) {
|
|
868
|
+
setHtml('');
|
|
869
|
+
setError('PlantUML rendering requires a host renderer.');
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const controller = new AbortController();
|
|
873
|
+
setError(null);
|
|
874
|
+
setHtml('');
|
|
875
|
+
Promise.resolve(services.renderPlantUml(source, { signal: controller.signal }))
|
|
876
|
+
.then((result) => {
|
|
877
|
+
if (controller.signal.aborted) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
setHtml(result.html);
|
|
881
|
+
if (result.diagnostics && result.diagnostics.length > 0) {
|
|
882
|
+
services.reportDiagnostics?.(result.diagnostics);
|
|
883
|
+
}
|
|
884
|
+
})
|
|
885
|
+
.catch((cause) => {
|
|
886
|
+
if (!controller.signal.aborted) {
|
|
887
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
return () => {
|
|
891
|
+
controller.abort();
|
|
892
|
+
};
|
|
893
|
+
}, [isEditing, services, source]);
|
|
894
|
+
function saveSource() {
|
|
895
|
+
editor.update(() => {
|
|
896
|
+
const node = $getNodeByKey(nodeKey);
|
|
897
|
+
if ($isPlantUmlNode(node)) {
|
|
898
|
+
node.setSource(draft);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
setIsEditing(false);
|
|
902
|
+
}
|
|
903
|
+
return (_jsxs("figure", { className: "me-wysiwyg-plantuml", children: [_jsxs("figcaption", { children: [_jsx("span", { children: "PlantUML" }), _jsx("button", { type: "button", onClick: () => setIsEditing((current) => !current), children: isEditing ? 'Preview' : 'Edit' })] }), isEditing ? (_jsxs("div", { className: "me-wysiwyg-diagram-editor", children: [_jsx("textarea", { "aria-label": "PlantUML diagram source", value: draft, onChange: (event) => setDraft(event.currentTarget.value), rows: 7 }), _jsx("button", { type: "button", onClick: saveSource, children: "Apply" })] })) : error ? (_jsx("pre", { className: "me-wysiwyg-diagram-error", children: error })) : html ? (_jsx("div", { className: "me-wysiwyg-diagram-rendered", dangerouslySetInnerHTML: { __html: sanitizeDiagramHtml(html) } })) : (_jsx("div", { className: "me-wysiwyg-diagram-loading", children: "Rendering diagram..." }))] }));
|
|
904
|
+
}
|
|
905
|
+
function ImageBlock({ image, nodeKey, editor, }) {
|
|
906
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
907
|
+
const [draft, setDraft] = React.useState(image);
|
|
908
|
+
React.useEffect(() => {
|
|
909
|
+
setDraft(image);
|
|
910
|
+
}, [image]);
|
|
911
|
+
function saveImage() {
|
|
912
|
+
editor.update(() => {
|
|
913
|
+
const node = $getNodeByKey(nodeKey);
|
|
914
|
+
if ($isImageNode(node)) {
|
|
915
|
+
node.setImage({
|
|
916
|
+
src: draft.src.trim(),
|
|
917
|
+
alt: draft.alt,
|
|
918
|
+
title: draft.title?.trim() ? draft.title : undefined,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
setIsEditing(false);
|
|
923
|
+
}
|
|
924
|
+
return (_jsxs("figure", { className: "me-wysiwyg-image", children: [_jsxs("figcaption", { children: [_jsx("span", { children: "Image" }), _jsx("button", { type: "button", onClick: () => setIsEditing((current) => !current), children: isEditing ? 'Preview' : 'Edit' })] }), isEditing ? (_jsxs("div", { className: "me-wysiwyg-image-editor", children: [_jsxs("label", { children: [_jsx("span", { children: "URL" }), _jsx("input", { type: "url", value: draft.src, onChange: (event) => setDraft((current) => ({ ...current, src: event.currentTarget.value })) })] }), _jsxs("label", { children: [_jsx("span", { children: "Alt text" }), _jsx("input", { type: "text", value: draft.alt, onChange: (event) => setDraft((current) => ({ ...current, alt: event.currentTarget.value })) })] }), _jsxs("label", { children: [_jsx("span", { children: "Title" }), _jsx("input", { type: "text", value: draft.title ?? '', onChange: (event) => setDraft((current) => ({ ...current, title: event.currentTarget.value })) })] }), _jsx("button", { type: "button", onClick: saveImage, children: "Apply" })] })) : image.src ? (_jsxs(_Fragment, { children: [_jsx("img", { src: image.src, alt: image.alt, title: image.title }), image.alt || image.title ? (_jsx("p", { children: image.title || image.alt })) : null] })) : (_jsx("pre", { className: "me-wysiwyg-diagram-error", children: "Image URL is empty." }))] }));
|
|
925
|
+
}
|
|
926
|
+
function EditableStatePlugin({ readOnly }) {
|
|
927
|
+
const [editor] = useLexicalComposerContext();
|
|
928
|
+
React.useEffect(() => {
|
|
929
|
+
editor.setEditable(!readOnly);
|
|
930
|
+
}, [editor, readOnly]);
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
function ExternalMarkdownPlugin({ markdown, lastEmittedMarkdown, }) {
|
|
934
|
+
const [editor] = useLexicalComposerContext();
|
|
935
|
+
const lastAppliedMarkdown = React.useRef(markdown);
|
|
936
|
+
React.useEffect(() => {
|
|
937
|
+
if (markdown === lastAppliedMarkdown.current || markdown === lastEmittedMarkdown.current) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
lastAppliedMarkdown.current = markdown;
|
|
941
|
+
editor.update(() => {
|
|
942
|
+
importMarkdownBody(markdown);
|
|
943
|
+
}, { discrete: true });
|
|
944
|
+
}, [editor, lastEmittedMarkdown, markdown]);
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
function WysiwygChangePlugin({ sourceMarkdown, lastEmittedMarkdown, onChange, }) {
|
|
948
|
+
const sourceMarkdownRef = React.useRef(sourceMarkdown);
|
|
949
|
+
sourceMarkdownRef.current = sourceMarkdown;
|
|
950
|
+
return (_jsx(OnChangePlugin, { ignoreSelectionChange: true, onChange: (editorState) => {
|
|
951
|
+
const nextMarkdown = exportEditorState(editorState, sourceMarkdownRef.current);
|
|
952
|
+
if (nextMarkdown === lastEmittedMarkdown.current || nextMarkdown === sourceMarkdownRef.current) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
lastEmittedMarkdown.current = nextMarkdown;
|
|
956
|
+
onChange?.(nextMarkdown, {
|
|
957
|
+
source: 'user',
|
|
958
|
+
mode: 'wysiwyg',
|
|
959
|
+
timestamp: Date.now(),
|
|
960
|
+
});
|
|
961
|
+
} }));
|
|
962
|
+
}
|
|
963
|
+
function importMarkdownBody(markdown) {
|
|
964
|
+
const envelope = splitMarkdownEnvelope(markdown);
|
|
965
|
+
const root = $getRoot();
|
|
966
|
+
root.clear();
|
|
967
|
+
if (envelope.body.trim() === '') {
|
|
968
|
+
root.append($createParagraphNode());
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
$convertFromMarkdownString(envelope.body, WYSIWYG_TRANSFORMERS);
|
|
972
|
+
}
|
|
973
|
+
function exportEditorState(editorState, sourceMarkdown) {
|
|
974
|
+
let body = '';
|
|
975
|
+
editorState.read(() => {
|
|
976
|
+
body = serializeWysiwygBody();
|
|
977
|
+
});
|
|
978
|
+
return replaceEnvelopeBody(splitMarkdownEnvelope(sourceMarkdown), body);
|
|
979
|
+
}
|
|
980
|
+
function splitMarkdownEnvelope(markdown) {
|
|
981
|
+
if (!/^---\r?\n/.test(markdown)) {
|
|
982
|
+
return { rawFrontmatter: '', body: markdown, trailing: '' };
|
|
983
|
+
}
|
|
984
|
+
const firstNewline = markdown.indexOf('\n');
|
|
985
|
+
if (firstNewline === -1) {
|
|
986
|
+
return { rawFrontmatter: '', body: markdown, trailing: '' };
|
|
987
|
+
}
|
|
988
|
+
let cursor = firstNewline + 1;
|
|
989
|
+
while (cursor < markdown.length) {
|
|
990
|
+
const lineEnd = markdown.indexOf('\n', cursor);
|
|
991
|
+
const lineRawEnd = lineEnd === -1 ? markdown.length : lineEnd;
|
|
992
|
+
const line = markdown.slice(cursor, lineRawEnd).replace(/\r$/, '');
|
|
993
|
+
if (line === '---') {
|
|
994
|
+
const frontmatterEnd = lineEnd === -1 ? markdown.length : lineEnd + 1;
|
|
995
|
+
return {
|
|
996
|
+
rawFrontmatter: markdown.slice(0, frontmatterEnd),
|
|
997
|
+
body: markdown.slice(frontmatterEnd),
|
|
998
|
+
trailing: '',
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
if (lineEnd === -1) {
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
cursor = lineEnd + 1;
|
|
1005
|
+
}
|
|
1006
|
+
return { rawFrontmatter: '', body: markdown, trailing: '' };
|
|
1007
|
+
}
|
|
1008
|
+
function replaceEnvelopeBody(envelope, body) {
|
|
1009
|
+
return `${envelope.rawFrontmatter}${body}${envelope.trailing}`;
|
|
1010
|
+
}
|
|
1011
|
+
function $getActiveMarkdownTableContext() {
|
|
1012
|
+
const selection = $getSelection();
|
|
1013
|
+
if (!$isRangeSelection(selection) && !$isTableSelection(selection)) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
const cellNode = $getTableCellNodeFromLexicalNode(selection.focus.getNode()) ??
|
|
1017
|
+
$getTableCellNodeFromLexicalNode(selection.anchor.getNode());
|
|
1018
|
+
if (cellNode === null) {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
const rowNode = cellNode.getParent();
|
|
1022
|
+
const tableNode = rowNode?.getParent();
|
|
1023
|
+
if (!$isTableRowNode(rowNode) || !$isTableNode(tableNode)) {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
const rowIndex = getChildNodeIndex(tableNode, rowNode);
|
|
1027
|
+
const columnIndex = getChildNodeIndex(rowNode, cellNode);
|
|
1028
|
+
if (rowIndex < 0 || columnIndex < 0) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
return {
|
|
1032
|
+
tableNode,
|
|
1033
|
+
rowIndex,
|
|
1034
|
+
columnIndex,
|
|
1035
|
+
rowCount: getTableRowNodes(tableNode).length,
|
|
1036
|
+
columnCount: getMarkdownTableColumnCount(tableNode),
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function $applyMarkdownTableActionAt(tableNode, action, rowIndex, columnIndex) {
|
|
1040
|
+
$normalizeSimpleMarkdownTableShape(tableNode);
|
|
1041
|
+
const rowNodes = getTableRowNodes(tableNode);
|
|
1042
|
+
const rowCount = rowNodes.length;
|
|
1043
|
+
const columnCount = getMarkdownTableColumnCount(tableNode);
|
|
1044
|
+
const safeRowIndex = clampIndex(rowIndex, rowCount);
|
|
1045
|
+
const safeColumnIndex = clampIndex(columnIndex, columnCount);
|
|
1046
|
+
if (action === 'insert-row') {
|
|
1047
|
+
const targetRow = rowNodes[safeRowIndex];
|
|
1048
|
+
if (!$isTableRowNode(targetRow)) {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
const insertedRow = $createEmptyMarkdownTableRow(columnCount);
|
|
1052
|
+
targetRow.insertAfter(insertedRow);
|
|
1053
|
+
$normalizeSimpleMarkdownTableShape(tableNode);
|
|
1054
|
+
$selectMarkdownTableCell(tableNode, safeRowIndex + 1, safeColumnIndex);
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
if (action === 'insert-column') {
|
|
1058
|
+
rowNodes.forEach((rowNode, currentRowIndex) => {
|
|
1059
|
+
const cellNode = rowNode.getChildAtIndex(safeColumnIndex);
|
|
1060
|
+
const nextCell = $createEmptyMarkdownTableCell(currentRowIndex === 0 ? TableCellHeaderStates.COLUMN : TableCellHeaderStates.NO_STATUS);
|
|
1061
|
+
if ($isTableCellNode(cellNode)) {
|
|
1062
|
+
cellNode.insertAfter(nextCell);
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
rowNode.append(nextCell);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
$normalizeSimpleMarkdownTableShape(tableNode);
|
|
1069
|
+
$selectMarkdownTableCell(tableNode, safeRowIndex, safeColumnIndex + 1);
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
if (action === 'delete-row') {
|
|
1073
|
+
if (rowCount <= 1) {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
rowNodes[safeRowIndex]?.remove();
|
|
1077
|
+
$normalizeSimpleMarkdownTableShape(tableNode);
|
|
1078
|
+
$selectMarkdownTableCell(tableNode, Math.min(safeRowIndex, rowCount - 2), safeColumnIndex);
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
if (columnCount <= 1) {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
rowNodes.forEach((rowNode) => {
|
|
1085
|
+
const cellNode = rowNode.getChildAtIndex(safeColumnIndex);
|
|
1086
|
+
if ($isTableCellNode(cellNode)) {
|
|
1087
|
+
cellNode.remove();
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
$normalizeSimpleMarkdownTableShape(tableNode);
|
|
1091
|
+
$selectMarkdownTableCell(tableNode, safeRowIndex, Math.min(safeColumnIndex, columnCount - 2));
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
function $createMarkdownTableNode(rows) {
|
|
1095
|
+
const columnCount = Math.max(1, rows[0]?.length ?? 1);
|
|
1096
|
+
const tableNode = $createTableNode();
|
|
1097
|
+
rows.forEach((row, rowIndex) => {
|
|
1098
|
+
const rowNode = $createTableRowNode();
|
|
1099
|
+
const normalizedRow = normalizeMarkdownTableRow(row, columnCount);
|
|
1100
|
+
normalizedRow.forEach((cellText) => {
|
|
1101
|
+
const cellNode = $createTableCellNode(rowIndex === 0 ? TableCellHeaderStates.COLUMN : TableCellHeaderStates.NO_STATUS);
|
|
1102
|
+
const paragraphNode = $createParagraphNode();
|
|
1103
|
+
paragraphNode.append($createTextNode(cellText));
|
|
1104
|
+
cellNode.append(paragraphNode);
|
|
1105
|
+
rowNode.append(cellNode);
|
|
1106
|
+
});
|
|
1107
|
+
tableNode.append(rowNode);
|
|
1108
|
+
});
|
|
1109
|
+
return tableNode;
|
|
1110
|
+
}
|
|
1111
|
+
function $createEmptyMarkdownTableRow(columnCount) {
|
|
1112
|
+
const rowNode = $createTableRowNode();
|
|
1113
|
+
for (let index = 0; index < Math.max(1, columnCount); index += 1) {
|
|
1114
|
+
rowNode.append($createEmptyMarkdownTableCell(TableCellHeaderStates.NO_STATUS));
|
|
1115
|
+
}
|
|
1116
|
+
return rowNode;
|
|
1117
|
+
}
|
|
1118
|
+
function $createEmptyMarkdownTableCell(headerState) {
|
|
1119
|
+
const cellNode = $createTableCellNode(headerState);
|
|
1120
|
+
cellNode.append($createParagraphNode());
|
|
1121
|
+
return cellNode;
|
|
1122
|
+
}
|
|
1123
|
+
function $normalizeSimpleMarkdownTableShape(tableNode) {
|
|
1124
|
+
const rowNodes = getTableRowNodes(tableNode);
|
|
1125
|
+
const columnCount = Math.max(1, getMarkdownTableColumnCount(tableNode));
|
|
1126
|
+
rowNodes.forEach((rowNode, rowIndex) => {
|
|
1127
|
+
while (rowNode.getChildrenSize() < columnCount) {
|
|
1128
|
+
rowNode.append($createEmptyMarkdownTableCell(rowIndex === 0 ? TableCellHeaderStates.COLUMN : TableCellHeaderStates.NO_STATUS));
|
|
1129
|
+
}
|
|
1130
|
+
rowNode.getChildren().forEach((cellNode, columnIndex) => {
|
|
1131
|
+
if (!$isTableCellNode(cellNode)) {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (columnIndex >= columnCount) {
|
|
1135
|
+
cellNode.remove();
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
cellNode.setHeaderStyles(rowIndex === 0 ? TableCellHeaderStates.COLUMN : TableCellHeaderStates.NO_STATUS, TableCellHeaderStates.BOTH);
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
function getTableRowNodes(tableNode) {
|
|
1143
|
+
return tableNode.getChildren().filter($isTableRowNode);
|
|
1144
|
+
}
|
|
1145
|
+
function getMarkdownTableColumnCount(tableNode) {
|
|
1146
|
+
return Math.max(0, ...getTableRowNodes(tableNode).map((rowNode) => rowNode.getChildrenSize()));
|
|
1147
|
+
}
|
|
1148
|
+
function getChildNodeIndex(parentNode, childNode) {
|
|
1149
|
+
return parentNode.getChildren().findIndex((node) => node.is(childNode));
|
|
1150
|
+
}
|
|
1151
|
+
function $selectMarkdownTableCell(tableNode, rowIndex, columnIndex) {
|
|
1152
|
+
const rowNode = getTableRowNodes(tableNode)[rowIndex];
|
|
1153
|
+
if (!$isTableRowNode(rowNode)) {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const cellNode = rowNode.getChildAtIndex(columnIndex);
|
|
1157
|
+
if ($isTableCellNode(cellNode)) {
|
|
1158
|
+
cellNode.selectStart();
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function clampIndex(index, length) {
|
|
1162
|
+
if (length <= 0) {
|
|
1163
|
+
return 0;
|
|
1164
|
+
}
|
|
1165
|
+
return Math.min(Math.max(index, 0), length - 1);
|
|
1166
|
+
}
|
|
1167
|
+
function formatMermaidRenderError(cause) {
|
|
1168
|
+
const rawMessage = cause instanceof Error ? cause.message : String(cause);
|
|
1169
|
+
const firstLine = rawMessage
|
|
1170
|
+
.split(/\r?\n/)
|
|
1171
|
+
.map((line) => line.trim())
|
|
1172
|
+
.find((line) => line.length > 0 && !/^mermaid version/i.test(line));
|
|
1173
|
+
return `Mermaid diagram could not render${firstLine ? `: ${firstLine}` : '.'}`;
|
|
1174
|
+
}
|
|
1175
|
+
function isMarkdownTableRowLine(line) {
|
|
1176
|
+
return /^ {0,3}\|.*\|\s*$/.test(line);
|
|
1177
|
+
}
|
|
1178
|
+
function isMarkdownTableSeparatorLine(line) {
|
|
1179
|
+
if (!isMarkdownTableRowLine(line)) {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
const cells = parseMarkdownTableRow(line);
|
|
1183
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s/g, '')));
|
|
1184
|
+
}
|
|
1185
|
+
function parseMarkdownTableRow(line) {
|
|
1186
|
+
const trimmed = line.trim();
|
|
1187
|
+
const inner = trimmed.startsWith('|') && trimmed.endsWith('|')
|
|
1188
|
+
? trimmed.slice(1, -1)
|
|
1189
|
+
: trimmed;
|
|
1190
|
+
const cells = [];
|
|
1191
|
+
let current = '';
|
|
1192
|
+
let escaped = false;
|
|
1193
|
+
for (const character of inner) {
|
|
1194
|
+
if (escaped) {
|
|
1195
|
+
current += character === '|' ? character : `\\${character}`;
|
|
1196
|
+
escaped = false;
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (character === '\\') {
|
|
1200
|
+
escaped = true;
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
if (character === '|') {
|
|
1204
|
+
cells.push(normalizeMarkdownTableCell(current));
|
|
1205
|
+
current = '';
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
current += character;
|
|
1209
|
+
}
|
|
1210
|
+
if (escaped) {
|
|
1211
|
+
current += '\\';
|
|
1212
|
+
}
|
|
1213
|
+
cells.push(normalizeMarkdownTableCell(current));
|
|
1214
|
+
return cells;
|
|
1215
|
+
}
|
|
1216
|
+
function normalizeMarkdownTableRow(row, columnCount) {
|
|
1217
|
+
if (row.length === columnCount) {
|
|
1218
|
+
return row;
|
|
1219
|
+
}
|
|
1220
|
+
if (row.length > columnCount) {
|
|
1221
|
+
return row.slice(0, columnCount);
|
|
1222
|
+
}
|
|
1223
|
+
return [...row, ...Array.from({ length: columnCount - row.length }, () => '')];
|
|
1224
|
+
}
|
|
1225
|
+
function normalizeMarkdownTableCell(cell) {
|
|
1226
|
+
return cell.trim().replace(/\s+/g, ' ');
|
|
1227
|
+
}
|
|
1228
|
+
function formatMarkdownTableNode(tableNode) {
|
|
1229
|
+
const rows = getTableNodeRows(tableNode);
|
|
1230
|
+
if (rows.length === 0) {
|
|
1231
|
+
return '';
|
|
1232
|
+
}
|
|
1233
|
+
const columnCount = Math.max(...rows.map((row) => row.length), 1);
|
|
1234
|
+
const normalizedRows = rows.map((row) => normalizeMarkdownTableRow(row, columnCount));
|
|
1235
|
+
const [headerRow = []] = normalizedRows;
|
|
1236
|
+
const delimiterRow = Array.from({ length: columnCount }, () => '---');
|
|
1237
|
+
return [
|
|
1238
|
+
formatMarkdownTableRow(headerRow),
|
|
1239
|
+
formatMarkdownTableRow(delimiterRow),
|
|
1240
|
+
...normalizedRows.slice(1).map(formatMarkdownTableRow),
|
|
1241
|
+
].join('\n');
|
|
1242
|
+
}
|
|
1243
|
+
function getTableNodeRows(tableNode) {
|
|
1244
|
+
return tableNode.getChildren().flatMap((rowNode) => {
|
|
1245
|
+
if (!$isTableRowNode(rowNode)) {
|
|
1246
|
+
return [];
|
|
1247
|
+
}
|
|
1248
|
+
return [
|
|
1249
|
+
rowNode.getChildren().flatMap((cellNode) => {
|
|
1250
|
+
if (!$isTableCellNode(cellNode)) {
|
|
1251
|
+
return [];
|
|
1252
|
+
}
|
|
1253
|
+
return [formatMarkdownTableCellText(cellNode)];
|
|
1254
|
+
}),
|
|
1255
|
+
];
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
function formatMarkdownTableRow(cells) {
|
|
1259
|
+
return `| ${cells.map(escapeMarkdownTableCell).join(' | ')} |`;
|
|
1260
|
+
}
|
|
1261
|
+
function formatMarkdownTableCellText(cellNode) {
|
|
1262
|
+
return cellNode
|
|
1263
|
+
.getChildren()
|
|
1264
|
+
.map((child) => child.getTextContent())
|
|
1265
|
+
.join(' ')
|
|
1266
|
+
.replace(/\s+/g, ' ')
|
|
1267
|
+
.trim();
|
|
1268
|
+
}
|
|
1269
|
+
function escapeMarkdownTableCell(cell) {
|
|
1270
|
+
return cell.replace(/\r?\n/g, ' ').replace(/\|/g, '\\|').trim();
|
|
1271
|
+
}
|
|
1272
|
+
function formatImageMarkdown(image) {
|
|
1273
|
+
const alt = escapeMarkdownAttribute(image.alt ?? '');
|
|
1274
|
+
const src = escapeMarkdownAttribute(image.src);
|
|
1275
|
+
const title = image.title?.trim();
|
|
1276
|
+
return title ? `}")` : ``;
|
|
1277
|
+
}
|
|
1278
|
+
function escapeMarkdownAttribute(value) {
|
|
1279
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\]/g, '\\]');
|
|
1280
|
+
}
|
|
1281
|
+
function unescapeMarkdownAttribute(value) {
|
|
1282
|
+
return value.replace(/\\(["\\\]])/g, '$1');
|
|
1283
|
+
}
|
|
1284
|
+
const lexicalTheme = {
|
|
1285
|
+
paragraph: 'me-wysiwyg-paragraph',
|
|
1286
|
+
heading: {
|
|
1287
|
+
h1: 'me-wysiwyg-heading me-wysiwyg-heading-1',
|
|
1288
|
+
h2: 'me-wysiwyg-heading me-wysiwyg-heading-2',
|
|
1289
|
+
h3: 'me-wysiwyg-heading me-wysiwyg-heading-3',
|
|
1290
|
+
h4: 'me-wysiwyg-heading me-wysiwyg-heading-4',
|
|
1291
|
+
h5: 'me-wysiwyg-heading me-wysiwyg-heading-5',
|
|
1292
|
+
h6: 'me-wysiwyg-heading me-wysiwyg-heading-6',
|
|
1293
|
+
},
|
|
1294
|
+
list: {
|
|
1295
|
+
ul: 'me-wysiwyg-list',
|
|
1296
|
+
ol: 'me-wysiwyg-list',
|
|
1297
|
+
checklist: 'me-wysiwyg-check-list',
|
|
1298
|
+
listitem: 'me-wysiwyg-list-item',
|
|
1299
|
+
listitemChecked: 'me-wysiwyg-list-item-checked',
|
|
1300
|
+
listitemUnchecked: 'me-wysiwyg-list-item-unchecked',
|
|
1301
|
+
},
|
|
1302
|
+
quote: 'me-wysiwyg-quote',
|
|
1303
|
+
code: 'me-wysiwyg-code',
|
|
1304
|
+
table: 'me-wysiwyg-table',
|
|
1305
|
+
tableCell: 'me-wysiwyg-table-cell',
|
|
1306
|
+
tableCellHeader: 'me-wysiwyg-table-cell-header',
|
|
1307
|
+
tableCellSelected: 'me-wysiwyg-table-cell-selected',
|
|
1308
|
+
tableRow: 'me-wysiwyg-table-row',
|
|
1309
|
+
tableScrollableWrapper: 'me-wysiwyg-table-scroll',
|
|
1310
|
+
tableSelection: 'me-wysiwyg-table-selection',
|
|
1311
|
+
codeHighlight: {
|
|
1312
|
+
atrule: 'me-wysiwyg-token-atrule',
|
|
1313
|
+
attr: 'me-wysiwyg-token-attr-name',
|
|
1314
|
+
'attr-name': 'me-wysiwyg-token-attr-name',
|
|
1315
|
+
'attr-value': 'me-wysiwyg-token-attr-value',
|
|
1316
|
+
boolean: 'me-wysiwyg-token-boolean',
|
|
1317
|
+
builtin: 'me-wysiwyg-token-builtin',
|
|
1318
|
+
cdata: 'me-wysiwyg-token-cdata',
|
|
1319
|
+
char: 'me-wysiwyg-token-char',
|
|
1320
|
+
class: 'me-wysiwyg-token-class-name',
|
|
1321
|
+
'class-name': 'me-wysiwyg-token-class-name',
|
|
1322
|
+
comment: 'me-wysiwyg-token-comment',
|
|
1323
|
+
constant: 'me-wysiwyg-token-constant',
|
|
1324
|
+
deleted: 'me-wysiwyg-token-deleted',
|
|
1325
|
+
doctype: 'me-wysiwyg-token-doctype',
|
|
1326
|
+
entity: 'me-wysiwyg-token-entity',
|
|
1327
|
+
function: 'me-wysiwyg-token-function',
|
|
1328
|
+
important: 'me-wysiwyg-token-important',
|
|
1329
|
+
inserted: 'me-wysiwyg-token-inserted',
|
|
1330
|
+
keyword: 'me-wysiwyg-token-keyword',
|
|
1331
|
+
namespace: 'me-wysiwyg-token-namespace',
|
|
1332
|
+
number: 'me-wysiwyg-token-number',
|
|
1333
|
+
operator: 'me-wysiwyg-token-operator',
|
|
1334
|
+
prolog: 'me-wysiwyg-token-prolog',
|
|
1335
|
+
property: 'me-wysiwyg-token-property',
|
|
1336
|
+
punctuation: 'me-wysiwyg-token-punctuation',
|
|
1337
|
+
regex: 'me-wysiwyg-token-regex',
|
|
1338
|
+
selector: 'me-wysiwyg-token-selector',
|
|
1339
|
+
string: 'me-wysiwyg-token-string',
|
|
1340
|
+
symbol: 'me-wysiwyg-token-symbol',
|
|
1341
|
+
tag: 'me-wysiwyg-token-tag',
|
|
1342
|
+
url: 'me-wysiwyg-token-url',
|
|
1343
|
+
variable: 'me-wysiwyg-token-variable',
|
|
1344
|
+
},
|
|
1345
|
+
link: 'me-wysiwyg-link',
|
|
1346
|
+
text: {
|
|
1347
|
+
bold: 'me-wysiwyg-text-bold',
|
|
1348
|
+
italic: 'me-wysiwyg-text-italic',
|
|
1349
|
+
code: 'me-wysiwyg-text-code',
|
|
1350
|
+
},
|
|
1351
|
+
};
|
|
1352
|
+
//# sourceMappingURL=index.js.map
|