@blue-patterns/richtext 0.0.0-stage → 0.7.18
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/CHANGELOG.md +275 -0
- package/LICENSE +22 -0
- package/README.md +88 -2
- package/astryx.integration.mjs +11 -0
- package/blocks/RichTextEditorShowcase.template.mjs +13 -0
- package/blocks/RichTextEditorShowcase.tsx +19 -0
- package/dist/RichTextEditor.d.ts +282 -0
- package/dist/RichTextEditor.d.ts.map +1 -0
- package/dist/RichTextEditor.js +657 -0
- package/dist/RichTextEditorAutoLinkPlugin.d.ts +83 -0
- package/dist/RichTextEditorAutoLinkPlugin.d.ts.map +1 -0
- package/dist/RichTextEditorAutoLinkPlugin.js +99 -0
- package/dist/RichTextEditorToolbar.d.ts +186 -0
- package/dist/RichTextEditorToolbar.d.ts.map +1 -0
- package/dist/RichTextEditorToolbar.js +688 -0
- package/dist/RichTextView.d.ts +92 -0
- package/dist/RichTextView.d.ts.map +1 -0
- package/dist/RichTextView.js +267 -0
- package/dist/codeLanguages.d.ts +14 -0
- package/dist/codeLanguages.d.ts.map +1 -0
- package/dist/codeLanguages.js +34 -0
- package/dist/codeLineTokens.d.ts +53 -0
- package/dist/codeLineTokens.d.ts.map +1 -0
- package/dist/codeLineTokens.js +188 -0
- package/dist/editorNodes.d.ts +8 -0
- package/dist/editorNodes.d.ts.map +1 -0
- package/dist/editorNodes.js +30 -0
- package/dist/editorTheme.d.ts +7 -0
- package/dist/editorTheme.d.ts.map +1 -0
- package/dist/editorTheme.js +328 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/internal/CodeBackspacePlugin.d.ts +2 -0
- package/dist/internal/CodeBackspacePlugin.d.ts.map +1 -0
- package/dist/internal/CodeBackspacePlugin.js +70 -0
- package/dist/internal/CodeBlockHeaderPlugin.d.ts +11 -0
- package/dist/internal/CodeBlockHeaderPlugin.d.ts.map +1 -0
- package/dist/internal/CodeBlockHeaderPlugin.js +204 -0
- package/dist/internal/CodePastePlugin.d.ts +5 -0
- package/dist/internal/CodePastePlugin.d.ts.map +1 -0
- package/dist/internal/CodePastePlugin.js +104 -0
- package/dist/internal/CodeSyntaxPlugin.d.ts +8 -0
- package/dist/internal/CodeSyntaxPlugin.d.ts.map +1 -0
- package/dist/internal/CodeSyntaxPlugin.js +397 -0
- package/dist/internal/CodeTabPlugin.d.ts +2 -0
- package/dist/internal/CodeTabPlugin.d.ts.map +1 -0
- package/dist/internal/CodeTabPlugin.js +256 -0
- package/dist/internal/DisabledLinkGuardPlugin.d.ts +4 -0
- package/dist/internal/DisabledLinkGuardPlugin.d.ts.map +1 -0
- package/dist/internal/DisabledLinkGuardPlugin.js +93 -0
- package/dist/internal/ImageSourceCaretPlugin.d.ts +3 -0
- package/dist/internal/ImageSourceCaretPlugin.d.ts.map +1 -0
- package/dist/internal/ImageSourceCaretPlugin.js +105 -0
- package/dist/internal/ListItemShortcutsPlugin.d.ts +6 -0
- package/dist/internal/ListItemShortcutsPlugin.d.ts.map +1 -0
- package/dist/internal/ListItemShortcutsPlugin.js +314 -0
- package/dist/internal/MarkdownExtensionsPlugin.d.ts +17 -0
- package/dist/internal/MarkdownExtensionsPlugin.d.ts.map +1 -0
- package/dist/internal/MarkdownExtensionsPlugin.js +185 -0
- package/dist/internal/SelectAllScopePlugin.d.ts +2 -0
- package/dist/internal/SelectAllScopePlugin.d.ts.map +1 -0
- package/dist/internal/SelectAllScopePlugin.js +67 -0
- package/dist/internal/TableColumnFloorPlugin.d.ts +12 -0
- package/dist/internal/TableColumnFloorPlugin.d.ts.map +1 -0
- package/dist/internal/TableColumnFloorPlugin.js +84 -0
- package/dist/internal/TablePastePlugin.d.ts +9 -0
- package/dist/internal/TablePastePlugin.d.ts.map +1 -0
- package/dist/internal/TablePastePlugin.js +183 -0
- package/dist/internal/TableScrollRegionPlugin.d.ts +9 -0
- package/dist/internal/TableScrollRegionPlugin.d.ts.map +1 -0
- package/dist/internal/TableScrollRegionPlugin.js +95 -0
- package/dist/internal/TaskCheckboxPlugin.d.ts +7 -0
- package/dist/internal/TaskCheckboxPlugin.d.ts.map +1 -0
- package/dist/internal/TaskCheckboxPlugin.js +193 -0
- package/dist/internal/ToolbarLinkPopover.d.ts +74 -0
- package/dist/internal/ToolbarLinkPopover.d.ts.map +1 -0
- package/dist/internal/ToolbarLinkPopover.js +292 -0
- package/dist/linkEditing.d.ts +6 -0
- package/dist/linkEditing.d.ts.map +1 -0
- package/dist/linkEditing.js +20 -0
- package/dist/linkForm.d.ts +28 -0
- package/dist/linkForm.d.ts.map +1 -0
- package/dist/linkForm.js +137 -0
- package/dist/linkUtils.d.ts +27 -0
- package/dist/linkUtils.d.ts.map +1 -0
- package/dist/linkUtils.js +101 -0
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +17 -0
- package/dist/markdownCharacterReferences.d.ts +135 -0
- package/dist/markdownCharacterReferences.d.ts.map +1 -0
- package/dist/markdownCharacterReferences.js +1127 -0
- package/dist/markdownCodeFence.d.ts +4 -0
- package/dist/markdownCodeFence.d.ts.map +1 -0
- package/dist/markdownCodeFence.js +167 -0
- package/dist/markdownExtensionNode.d.ts +88 -0
- package/dist/markdownExtensionNode.d.ts.map +1 -0
- package/dist/markdownExtensionNode.js +248 -0
- package/dist/markdownExtensions.d.ts +115 -0
- package/dist/markdownExtensions.d.ts.map +1 -0
- package/dist/markdownExtensions.js +392 -0
- package/dist/markdownHardLineBreak.d.ts +25 -0
- package/dist/markdownHardLineBreak.d.ts.map +1 -0
- package/dist/markdownHardLineBreak.js +59 -0
- package/dist/markdownHeading.d.ts +20 -0
- package/dist/markdownHeading.d.ts.map +1 -0
- package/dist/markdownHeading.js +30 -0
- package/dist/markdownLink.d.ts +5 -0
- package/dist/markdownLink.d.ts.map +1 -0
- package/dist/markdownLink.js +80 -0
- package/dist/markdownListExport.d.ts +3 -0
- package/dist/markdownListExport.d.ts.map +1 -0
- package/dist/markdownListExport.js +68 -0
- package/dist/markdownListIndentation.d.ts +8 -0
- package/dist/markdownListIndentation.d.ts.map +1 -0
- package/dist/markdownListIndentation.js +101 -0
- package/dist/markdownOrderedList.d.ts +3 -0
- package/dist/markdownOrderedList.d.ts.map +1 -0
- package/dist/markdownOrderedList.js +51 -0
- package/dist/markdownQuote.d.ts +22 -0
- package/dist/markdownQuote.d.ts.map +1 -0
- package/dist/markdownQuote.js +32 -0
- package/dist/markdownSerializers.d.ts +49 -0
- package/dist/markdownSerializers.d.ts.map +1 -0
- package/dist/markdownSerializers.js +83 -0
- package/dist/markdownSource.d.ts +71 -0
- package/dist/markdownSource.d.ts.map +1 -0
- package/dist/markdownSource.js +804 -0
- package/dist/markdownSpaceRuns.d.ts +24 -0
- package/dist/markdownSpaceRuns.d.ts.map +1 -0
- package/dist/markdownSpaceRuns.js +147 -0
- package/dist/markdownTable.d.ts +35 -0
- package/dist/markdownTable.d.ts.map +1 -0
- package/dist/markdownTable.js +227 -0
- package/dist/markdownTaskList.d.ts +48 -0
- package/dist/markdownTaskList.d.ts.map +1 -0
- package/dist/markdownTaskList.js +166 -0
- package/dist/markdownThematicBreak.d.ts +3 -0
- package/dist/markdownThematicBreak.d.ts.map +1 -0
- package/dist/markdownThematicBreak.js +55 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts +12 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.js +16 -0
- package/dist/richtext.css +213 -0
- package/dist/textSemantics.d.ts +3 -0
- package/dist/textSemantics.d.ts.map +1 -0
- package/dist/textSemantics.js +87 -0
- package/package.json +158 -3
- package/src/RichTextEditor.doc.mjs +308 -0
- package/src/RichTextEditor.tsx +1109 -0
- package/src/RichTextEditorAutoLinkPlugin.doc.mjs +43 -0
- package/src/RichTextEditorAutoLinkPlugin.tsx +126 -0
- package/src/RichTextEditorToolbar.doc.mjs +92 -0
- package/src/RichTextEditorToolbar.tsx +871 -0
- package/src/RichTextView.doc.mjs +101 -0
- package/src/RichTextView.tsx +351 -0
- package/src/codeLanguages.ts +42 -0
- package/src/codeLineTokens.ts +241 -0
- package/src/doc-mjs.d.ts +23 -0
- package/src/editorNodes.ts +44 -0
- package/src/editorTheme.ts +550 -0
- package/src/index.ts +59 -0
- package/src/internal/CodeBackspacePlugin.tsx +96 -0
- package/src/internal/CodeBlockHeaderPlugin.tsx +282 -0
- package/src/internal/CodePastePlugin.tsx +144 -0
- package/src/internal/CodeSyntaxPlugin.tsx +493 -0
- package/src/internal/CodeTabPlugin.tsx +298 -0
- package/src/internal/DisabledLinkGuardPlugin.tsx +93 -0
- package/src/internal/ImageSourceCaretPlugin.tsx +159 -0
- package/src/internal/ListItemShortcutsPlugin.tsx +404 -0
- package/src/internal/MarkdownExtensionsPlugin.tsx +289 -0
- package/src/internal/SelectAllScopePlugin.tsx +86 -0
- package/src/internal/TableColumnFloorPlugin.tsx +99 -0
- package/src/internal/TablePastePlugin.tsx +223 -0
- package/src/internal/TableScrollRegionPlugin.tsx +107 -0
- package/src/internal/TaskCheckboxPlugin.tsx +246 -0
- package/src/internal/ToolbarLinkPopover.tsx +392 -0
- package/src/linkEditing.ts +20 -0
- package/src/linkForm.tsx +156 -0
- package/src/linkUtils.ts +106 -0
- package/src/markdown.ts +25 -0
- package/src/markdownCharacterReferences.ts +1137 -0
- package/src/markdownCodeFence.ts +216 -0
- package/src/markdownExtensionNode.ts +339 -0
- package/src/markdownExtensions.ts +496 -0
- package/src/markdownHardLineBreak.ts +73 -0
- package/src/markdownHeading.ts +33 -0
- package/src/markdownLink.ts +86 -0
- package/src/markdownListExport.ts +85 -0
- package/src/markdownListIndentation.ts +105 -0
- package/src/markdownOrderedList.ts +60 -0
- package/src/markdownQuote.ts +35 -0
- package/src/markdownSerializers.ts +108 -0
- package/src/markdownSource.ts +1045 -0
- package/src/markdownSpaceRuns.ts +164 -0
- package/src/markdownTable.ts +309 -0
- package/src/markdownTaskList.ts +222 -0
- package/src/markdownThematicBreak.ts +66 -0
- package/src/privateAttributes/layerPortalBoundary.stylex.ts +18 -0
- package/src/textSemantics.ts +104 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file markdownSpaceRuns.ts
|
|
5
|
+
* @input Lexical nodes to export as Markdown, and the transformers to use
|
|
6
|
+
* @output $convertToMarkdownKeepingTimeLinear: Lexical's Markdown export, in
|
|
7
|
+
* time linear in the text even when a text holds long runs of spaces
|
|
8
|
+
* @position Every Markdown export in this package goes through it
|
|
9
|
+
* (markdownSource.ts, markdownTable.ts). Lexical's text-format export splits
|
|
10
|
+
* each text into leading spaces, content, and trailing spaces with
|
|
11
|
+
* `/^(\s*)(.*?)(\s*)$/s`, which retries from every character of a long run
|
|
12
|
+
* of spaces inside the text, so its time grows with the square of the run.
|
|
13
|
+
* Each such run is exported as private-use stand-ins, which that pattern
|
|
14
|
+
* passes in one step each, and put back afterwards. Leading and trailing
|
|
15
|
+
* spaces, which decide where marks attach, are left as they are.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {ElementNode, LexicalNode} from 'lexical';
|
|
19
|
+
import type {Transformer} from '@lexical/markdown';
|
|
20
|
+
import {$isCodeNode} from '@lexical/code';
|
|
21
|
+
import {$convertToMarkdownString} from '@lexical/markdown';
|
|
22
|
+
import {$isElementNode, $isTextNode} from 'lexical';
|
|
23
|
+
import {absentCharacters} from './markdownCharacterReferences';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A run of spaces with other characters on both sides, long enough for
|
|
27
|
+
* Lexical's pattern to cost noticeably more than one step per space; shorter
|
|
28
|
+
* runs, as ordinary text has, export as they are.
|
|
29
|
+
*/
|
|
30
|
+
const INNER_SPACE_RUN = /(?<=\S)\s{64,}(?=\S)/g;
|
|
31
|
+
|
|
32
|
+
/** Whether Lexical exports the text node's text as written, untouched. */
|
|
33
|
+
function isLiteralText(node: LexicalNode): boolean {
|
|
34
|
+
return (
|
|
35
|
+
$isTextNode(node) &&
|
|
36
|
+
(node.hasFormat('code') || $isCodeNode(node.getParent()))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lexical's `$convertToMarkdownString(transformers, root)`, with every long
|
|
42
|
+
* run of spaces inside a text exported as stand-ins and put back, so the
|
|
43
|
+
* export takes time linear in the text. The tree is read through views, never
|
|
44
|
+
* changed; a tree with no long run exports directly.
|
|
45
|
+
*/
|
|
46
|
+
export function $convertToMarkdownKeepingTimeLinear(
|
|
47
|
+
transformers: Array<Transformer>,
|
|
48
|
+
root: ElementNode,
|
|
49
|
+
): string {
|
|
50
|
+
const spaces = new Set<string>();
|
|
51
|
+
const findRuns = (node: LexicalNode) => {
|
|
52
|
+
if ($isTextNode(node)) {
|
|
53
|
+
if (!isLiteralText(node)) {
|
|
54
|
+
for (const run of node.getTextContent().match(INNER_SPACE_RUN) ?? []) {
|
|
55
|
+
for (const space of run) {
|
|
56
|
+
spaces.add(space);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else if ($isElementNode(node) && !$isCodeNode(node)) {
|
|
61
|
+
node.getChildren().forEach(findRuns);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
root.getChildren().forEach(findRuns);
|
|
65
|
+
if (spaces.size === 0) {
|
|
66
|
+
return $convertToMarkdownString(transformers, root);
|
|
67
|
+
}
|
|
68
|
+
// Every string the export can write — texts, and the properties nodes
|
|
69
|
+
// serialize, such as a link's address and title — so a stand-in occurs
|
|
70
|
+
// nowhere else in the output.
|
|
71
|
+
const written: string[] = [];
|
|
72
|
+
const collect = (node: LexicalNode) => {
|
|
73
|
+
written.push(JSON.stringify(node.exportJSON()));
|
|
74
|
+
if ($isTextNode(node)) {
|
|
75
|
+
written.push(node.getTextContent());
|
|
76
|
+
} else if ($isElementNode(node)) {
|
|
77
|
+
node.getChildren().forEach(collect);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
root.getChildren().forEach(collect);
|
|
81
|
+
const free = absentCharacters(written.join(''));
|
|
82
|
+
const standIns = new Map<string, string>();
|
|
83
|
+
for (const space of spaces) {
|
|
84
|
+
const standIn = free.next().value;
|
|
85
|
+
if (standIn == null) {
|
|
86
|
+
return $convertToMarkdownString(transformers, root);
|
|
87
|
+
}
|
|
88
|
+
standIns.set(space, standIn);
|
|
89
|
+
}
|
|
90
|
+
const placed = {count: 0};
|
|
91
|
+
const children = viewsOf(root.getChildren(), standIns, placed);
|
|
92
|
+
const view = Object.create(root) as typeof root;
|
|
93
|
+
view.getChildren = <T extends LexicalNode>() => children as Array<T>;
|
|
94
|
+
let markdown = $convertToMarkdownString(transformers, view);
|
|
95
|
+
// Every stand-in in the output must be one this export placed; otherwise
|
|
96
|
+
// the plain export, slower but exact, is the answer.
|
|
97
|
+
let found = 0;
|
|
98
|
+
for (const standIn of standIns.values()) {
|
|
99
|
+
found += markdown.split(standIn).length - 1;
|
|
100
|
+
}
|
|
101
|
+
if (found !== placed.count) {
|
|
102
|
+
return $convertToMarkdownString(transformers, root);
|
|
103
|
+
}
|
|
104
|
+
for (const [space, standIn] of standIns) {
|
|
105
|
+
markdown = markdown.split(standIn).join(space);
|
|
106
|
+
}
|
|
107
|
+
return markdown;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Views of `nodes` whose texts hold stand-ins for their inner runs of
|
|
112
|
+
* spaces; a node with no such run is itself. Each view's siblings are the
|
|
113
|
+
* views beside it, as Lexical's exporter reads neighbors to place marks.
|
|
114
|
+
*/
|
|
115
|
+
function viewsOf(
|
|
116
|
+
nodes: ReadonlyArray<LexicalNode>,
|
|
117
|
+
standIns: ReadonlyMap<string, string>,
|
|
118
|
+
placed: {count: number},
|
|
119
|
+
): LexicalNode[] {
|
|
120
|
+
const views = nodes.map(node => viewOf(node, standIns, placed));
|
|
121
|
+
views.forEach((view, index) => {
|
|
122
|
+
if (view !== nodes[index]) {
|
|
123
|
+
view.getPreviousSibling = <T extends LexicalNode>() =>
|
|
124
|
+
(views[index - 1] ?? null) as T | null;
|
|
125
|
+
view.getNextSibling = <T extends LexicalNode>() =>
|
|
126
|
+
(views[index + 1] ?? null) as T | null;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return views;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function viewOf(
|
|
133
|
+
node: LexicalNode,
|
|
134
|
+
standIns: ReadonlyMap<string, string>,
|
|
135
|
+
placed: {count: number},
|
|
136
|
+
): LexicalNode {
|
|
137
|
+
if ($isTextNode(node)) {
|
|
138
|
+
if (isLiteralText(node)) {
|
|
139
|
+
return node;
|
|
140
|
+
}
|
|
141
|
+
const text = node.getTextContent();
|
|
142
|
+
const stood = text.replace(INNER_SPACE_RUN, run => {
|
|
143
|
+
placed.count += run.length;
|
|
144
|
+
return Array.from(run, space => standIns.get(space) ?? space).join('');
|
|
145
|
+
});
|
|
146
|
+
if (stood === text) {
|
|
147
|
+
return node;
|
|
148
|
+
}
|
|
149
|
+
const view = Object.create(node) as typeof node;
|
|
150
|
+
view.getTextContent = () => stood;
|
|
151
|
+
return view;
|
|
152
|
+
}
|
|
153
|
+
if ($isElementNode(node) && !$isCodeNode(node)) {
|
|
154
|
+
const original = node.getChildren();
|
|
155
|
+
const children = viewsOf(original, standIns, placed);
|
|
156
|
+
if (children.every((child, index) => child === original[index])) {
|
|
157
|
+
return node;
|
|
158
|
+
}
|
|
159
|
+
const view = Object.create(node) as typeof node;
|
|
160
|
+
view.getChildren = <T extends LexicalNode>() => children as Array<T>;
|
|
161
|
+
return view;
|
|
162
|
+
}
|
|
163
|
+
return node;
|
|
164
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file markdownTable.ts
|
|
5
|
+
* @input Uses @lexical/markdown (multiline element transformer, inline text
|
|
6
|
+
* transformers) and the @lexical/table nodes.
|
|
7
|
+
* @output Exports TABLE, the GFM pipe-table transformer, and DEFAULT_TRANSFORMERS,
|
|
8
|
+
* the editor's default Markdown transformers (Lexical's standard set plus TABLE).
|
|
9
|
+
* @position Internal to @blue-patterns/richtext. RichTextEditor.tsx (shortcuts,
|
|
10
|
+
* getMarkdown) and markdownSerializers.ts (headless import/export) default to
|
|
11
|
+
* DEFAULT_TRANSFORMERS so tables import, edit, and export the same way.
|
|
12
|
+
*
|
|
13
|
+
* SYNC: Table recognition mirrors core Markdown's parser (a line with a pipe
|
|
14
|
+
* followed by a delimiter row; body rows continue while they contain a pipe),
|
|
15
|
+
* so the same source is a table on both surfaces. Keep the two in step.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
$convertFromMarkdownString,
|
|
20
|
+
CODE,
|
|
21
|
+
HEADING,
|
|
22
|
+
TEXT_FORMAT_TRANSFORMERS,
|
|
23
|
+
TEXT_MATCH_TRANSFORMERS,
|
|
24
|
+
LINK,
|
|
25
|
+
ORDERED_LIST,
|
|
26
|
+
QUOTE,
|
|
27
|
+
TRANSFORMERS,
|
|
28
|
+
type MultilineElementTransformer,
|
|
29
|
+
type Transformer,
|
|
30
|
+
} from '@lexical/markdown';
|
|
31
|
+
import {
|
|
32
|
+
$createTableCellNode,
|
|
33
|
+
$createTableNode,
|
|
34
|
+
$createTableRowNode,
|
|
35
|
+
$isTableCellNode,
|
|
36
|
+
$isTableNode,
|
|
37
|
+
$isTableRowNode,
|
|
38
|
+
TableCellHeaderStates,
|
|
39
|
+
TableCellNode,
|
|
40
|
+
TableNode,
|
|
41
|
+
TableRowNode,
|
|
42
|
+
} from '@lexical/table';
|
|
43
|
+
import type {ElementFormatType} from 'lexical';
|
|
44
|
+
import {HARD_LINE_BREAK} from './markdownHardLineBreak';
|
|
45
|
+
import {THEMATIC_BREAK} from './markdownThematicBreak';
|
|
46
|
+
import {HEADING_MARKERS} from './markdownHeading';
|
|
47
|
+
import {QUOTE_MARKERS} from './markdownQuote';
|
|
48
|
+
import {LIST_EXPORT} from './markdownListExport';
|
|
49
|
+
import {TASK_LIST} from './markdownTaskList';
|
|
50
|
+
import {ORDERED_LIST_KEEPING_START} from './markdownOrderedList';
|
|
51
|
+
import {LINK_KEEPING_DESTINATIONS} from './markdownLink';
|
|
52
|
+
import {BACKTICK_CODE, TILDE_CODE} from './markdownCodeFence';
|
|
53
|
+
import {$convertToMarkdownKeepingTimeLinear} from './markdownSpaceRuns';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Cells hold inline Markdown only, so they are imported and exported with the
|
|
57
|
+
* inline transformers.
|
|
58
|
+
*/
|
|
59
|
+
const CELL_TRANSFORMERS: Array<Transformer> = [
|
|
60
|
+
...TEXT_FORMAT_TRANSFORMERS,
|
|
61
|
+
...TEXT_MATCH_TRANSFORMERS.map(transformer =>
|
|
62
|
+
transformer === LINK ? LINK_KEEPING_DESTINATIONS : transformer,
|
|
63
|
+
),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
type ColumnAlignment = 'left' | 'center' | 'right' | null;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Splits a row into trimmed cell sources: the row's indentation, one leading
|
|
70
|
+
* pipe, trailing spaces, and one trailing pipe are dropped, and the rest
|
|
71
|
+
* splits on unescaped pipes. An escaped pipe stays escaped for the cell's
|
|
72
|
+
* inline import.
|
|
73
|
+
*/
|
|
74
|
+
function splitTableRow(line: string): Array<string> {
|
|
75
|
+
let start = 0;
|
|
76
|
+
let end = line.length;
|
|
77
|
+
while (start < end && (line[start] === ' ' || line[start] === '\t')) {
|
|
78
|
+
start++;
|
|
79
|
+
}
|
|
80
|
+
if (line[start] === '|') {
|
|
81
|
+
start++;
|
|
82
|
+
while (start < end && line[start] === ' ') {
|
|
83
|
+
start++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
while (end > start && line[end - 1] === ' ') {
|
|
87
|
+
end--;
|
|
88
|
+
}
|
|
89
|
+
if (end > start && line[end - 1] === '|') {
|
|
90
|
+
end--;
|
|
91
|
+
}
|
|
92
|
+
const content = line.slice(start, end);
|
|
93
|
+
const cells: Array<string> = [];
|
|
94
|
+
let current = '';
|
|
95
|
+
for (let index = 0; index < content.length; index++) {
|
|
96
|
+
const character = content[index];
|
|
97
|
+
if (character === '\\' && content[index + 1] === '|') {
|
|
98
|
+
current += '\\|';
|
|
99
|
+
index++;
|
|
100
|
+
} else if (character === '|') {
|
|
101
|
+
cells.push(current.trim());
|
|
102
|
+
current = '';
|
|
103
|
+
} else {
|
|
104
|
+
current += character;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
cells.push(current.trim());
|
|
108
|
+
return cells;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A delimiter row: every non-empty cell is dashes with optional colons. */
|
|
112
|
+
function isDelimiterRow(line: string | undefined): boolean {
|
|
113
|
+
if (line == null || !line.includes('|')) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const cells = line
|
|
117
|
+
.split('|')
|
|
118
|
+
.map(cell => cell.trim())
|
|
119
|
+
.filter(cell => cell.length > 0);
|
|
120
|
+
return cells.length > 0 && cells.every(cell => /^:?-+:?$/.test(cell));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function columnAlignment(delimiterCell: string): ColumnAlignment {
|
|
124
|
+
const left = delimiterCell.startsWith(':');
|
|
125
|
+
const right = delimiterCell.endsWith(':');
|
|
126
|
+
if (left && right) {
|
|
127
|
+
return 'center';
|
|
128
|
+
}
|
|
129
|
+
return right ? 'right' : left ? 'left' : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Alignment is stored as the cell's element format. Left and right map to
|
|
134
|
+
* logical start and end, so an aligned column follows the reading direction
|
|
135
|
+
* the way core Markdown's table does.
|
|
136
|
+
*/
|
|
137
|
+
const ALIGNMENT_FORMAT: Record<
|
|
138
|
+
NonNullable<ColumnAlignment>,
|
|
139
|
+
ElementFormatType
|
|
140
|
+
> = {left: 'start', center: 'center', right: 'end'};
|
|
141
|
+
|
|
142
|
+
function delimiterFor(format: ElementFormatType): string {
|
|
143
|
+
switch (format) {
|
|
144
|
+
case 'left':
|
|
145
|
+
case 'start':
|
|
146
|
+
return ':---';
|
|
147
|
+
case 'center':
|
|
148
|
+
return ':---:';
|
|
149
|
+
case 'right':
|
|
150
|
+
case 'end':
|
|
151
|
+
return '---:';
|
|
152
|
+
default:
|
|
153
|
+
return '---';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function $createCell(
|
|
158
|
+
source: string,
|
|
159
|
+
isHeader: boolean,
|
|
160
|
+
alignment: ColumnAlignment,
|
|
161
|
+
): TableCellNode {
|
|
162
|
+
const cell = $createTableCellNode(
|
|
163
|
+
isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS,
|
|
164
|
+
);
|
|
165
|
+
// The pipe was only escaped to keep it out of the row split.
|
|
166
|
+
$convertFromMarkdownString(
|
|
167
|
+
source.replace(/\\\|/g, '|'),
|
|
168
|
+
CELL_TRANSFORMERS,
|
|
169
|
+
cell,
|
|
170
|
+
);
|
|
171
|
+
if (alignment != null) {
|
|
172
|
+
cell.setFormat(ALIGNMENT_FORMAT[alignment]);
|
|
173
|
+
}
|
|
174
|
+
return cell;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function cellMarkdown(cell: TableCellNode): string {
|
|
178
|
+
// A cell is one line of Markdown, so paragraph breaks inside it become
|
|
179
|
+
// spaces, and literal pipes are escaped to stay inside the cell.
|
|
180
|
+
return oneLine($convertToMarkdownKeepingTimeLinear(CELL_TRANSFORMERS, cell))
|
|
181
|
+
.replace(/\|/g, '\\|')
|
|
182
|
+
.trim();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `markdown` with each run of spaces that holds a line break made one space.
|
|
187
|
+
* Read line by line: a pattern matching optional spaces around line breaks
|
|
188
|
+
* retries from every space in a long run without a line break, so its time
|
|
189
|
+
* grows with the square of the run.
|
|
190
|
+
*/
|
|
191
|
+
function oneLine(markdown: string): string {
|
|
192
|
+
if (!markdown.includes('\n')) {
|
|
193
|
+
return markdown;
|
|
194
|
+
}
|
|
195
|
+
const lines = markdown.split('\n');
|
|
196
|
+
let joined = lines[0].trimEnd();
|
|
197
|
+
for (let index = 1; index < lines.length; index++) {
|
|
198
|
+
const isLast = index === lines.length - 1;
|
|
199
|
+
const line = isLast ? lines[index].trimStart() : lines[index].trim();
|
|
200
|
+
// A blank line is part of the run of spaces around it.
|
|
201
|
+
if (line !== '' || isLast) {
|
|
202
|
+
joined += ` ${line}`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return joined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* GFM pipe tables. The header row is the first row and renders as header
|
|
210
|
+
* cells; delimiter-row colons set each column's alignment; rows shorter than
|
|
211
|
+
* the widest row gain empty cells, so no cell is dropped. Export writes the
|
|
212
|
+
* canonical form: outer pipes, one space of padding, and three-dash delimiters.
|
|
213
|
+
*/
|
|
214
|
+
export const TABLE: MultilineElementTransformer = {
|
|
215
|
+
dependencies: [TableNode, TableRowNode, TableCellNode],
|
|
216
|
+
export(node) {
|
|
217
|
+
if (!$isTableNode(node)) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const rows = node.getChildren().filter($isTableRowNode);
|
|
221
|
+
const lines: Array<string> = [];
|
|
222
|
+
rows.forEach((row, rowIndex) => {
|
|
223
|
+
const cells = row.getChildren().filter($isTableCellNode);
|
|
224
|
+
lines.push(`| ${cells.map(cellMarkdown).join(' | ')} |`);
|
|
225
|
+
if (rowIndex === 0) {
|
|
226
|
+
lines.push(
|
|
227
|
+
`| ${cells.map(cell => delimiterFor(cell.getFormatType())).join(' | ')} |`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
},
|
|
233
|
+
regExpStart: /\|/,
|
|
234
|
+
handleImportAfterStartMatch({lines, rootNode, startLineIndex}) {
|
|
235
|
+
if (!isDelimiterRow(lines[startLineIndex + 1])) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const alignments = splitTableRow(lines[startLineIndex + 1]).map(
|
|
239
|
+
columnAlignment,
|
|
240
|
+
);
|
|
241
|
+
const rowSources: Array<Array<string>> = [
|
|
242
|
+
splitTableRow(lines[startLineIndex]),
|
|
243
|
+
];
|
|
244
|
+
let lastLineIndex = startLineIndex + 1;
|
|
245
|
+
while (
|
|
246
|
+
lastLineIndex + 1 < lines.length &&
|
|
247
|
+
lines[lastLineIndex + 1].includes('|') &&
|
|
248
|
+
lines[lastLineIndex + 1].trim() !== ''
|
|
249
|
+
) {
|
|
250
|
+
lastLineIndex++;
|
|
251
|
+
rowSources.push(splitTableRow(lines[lastLineIndex]));
|
|
252
|
+
}
|
|
253
|
+
const columnCount = Math.max(...rowSources.map(cells => cells.length));
|
|
254
|
+
const table = $createTableNode();
|
|
255
|
+
rowSources.forEach((cells, rowIndex) => {
|
|
256
|
+
const row = $createTableRowNode();
|
|
257
|
+
for (let column = 0; column < columnCount; column++) {
|
|
258
|
+
row.append(
|
|
259
|
+
$createCell(
|
|
260
|
+
cells[column] ?? '',
|
|
261
|
+
rowIndex === 0,
|
|
262
|
+
alignments[column] ?? null,
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
table.append(row);
|
|
267
|
+
});
|
|
268
|
+
rootNode.append(table);
|
|
269
|
+
return [true, lastLineIndex];
|
|
270
|
+
},
|
|
271
|
+
replace() {
|
|
272
|
+
// Every table imports through handleImportAfterStartMatch.
|
|
273
|
+
return false;
|
|
274
|
+
},
|
|
275
|
+
type: 'multiline-element',
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The editor's default Markdown transformers: thematic breaks, list export
|
|
280
|
+
* that nests to each parent's content column, GFM task list items, Lexical's
|
|
281
|
+
* standard set (with `~~~` fences beside its backtick ones, both reading the
|
|
282
|
+
* opening line's info string as core does, its ordered list keeping each
|
|
283
|
+
* nested list's start, and its
|
|
284
|
+
* link writing an unbalanced destination's parentheses escaped), hard line
|
|
285
|
+
* breaks for breaks typed in the editor, and GFM tables. Thematic breaks come first so a line such as `* * *` is a rule
|
|
286
|
+
* rather than a list item, and task items before bullets so `- [ ] text` is a
|
|
287
|
+
* task rather than a bullet that starts with `[ ]`.
|
|
288
|
+
*/
|
|
289
|
+
export const DEFAULT_TRANSFORMERS: ReadonlyArray<Transformer> = [
|
|
290
|
+
THEMATIC_BREAK,
|
|
291
|
+
LIST_EXPORT,
|
|
292
|
+
TASK_LIST,
|
|
293
|
+
TILDE_CODE,
|
|
294
|
+
...TRANSFORMERS.map(transformer =>
|
|
295
|
+
transformer === ORDERED_LIST
|
|
296
|
+
? ORDERED_LIST_KEEPING_START
|
|
297
|
+
: transformer === LINK
|
|
298
|
+
? LINK_KEEPING_DESTINATIONS
|
|
299
|
+
: transformer === CODE
|
|
300
|
+
? BACKTICK_CODE
|
|
301
|
+
: transformer === QUOTE
|
|
302
|
+
? QUOTE_MARKERS
|
|
303
|
+
: transformer === HEADING
|
|
304
|
+
? HEADING_MARKERS
|
|
305
|
+
: transformer,
|
|
306
|
+
),
|
|
307
|
+
HARD_LINE_BREAK,
|
|
308
|
+
TABLE,
|
|
309
|
+
];
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file markdownTaskList.ts
|
|
5
|
+
* @input Uses lexical (node state), @lexical/list, and @lexical/markdown
|
|
6
|
+
* (ElementTransformer).
|
|
7
|
+
* @output Exports TASK_LIST, the transformer that imports GFM task list
|
|
8
|
+
* items; $getTaskState / $setTaskState, which read and set whether a list
|
|
9
|
+
* item is a task and whether it is checked; and registerTaskListEditing,
|
|
10
|
+
* the editor's Enter and Mod+Enter behavior for task items.
|
|
11
|
+
* @position Part of DEFAULT_TRANSFORMERS (markdownTable.ts), ahead of
|
|
12
|
+
* Lexical's bullet transformer. It reads exactly the task items core
|
|
13
|
+
* Markdown reads (spec:AST-061 FR5): a `-`, `*`, or `+` bullet, one space,
|
|
14
|
+
* `[ ]`, `[x]`, or `[X]`, and a space. Lexical's own CHECK_LIST also takes a
|
|
15
|
+
* line with no bullet, `[]`, and missing spaces, and reads `[X]` as
|
|
16
|
+
* unchecked; those stay a paragraph or an ordinary bullet here.
|
|
17
|
+
*
|
|
18
|
+
* A task is a property of an item, not of its list: GFM lets task and
|
|
19
|
+
* plain items share one list, and Lexical keeps a checked state only in its
|
|
20
|
+
* all-checkbox lists. So task items live in ordinary bullet lists, beside
|
|
21
|
+
* plain items, and carry their state as node state; LIST_EXPORT writes
|
|
22
|
+
* `[ ]` or `[x]` for each task item. TaskCheckboxPlugin draws them, and
|
|
23
|
+
* registers registerTaskListEditing in the editor.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
$createListItemNode,
|
|
28
|
+
$createListNode,
|
|
29
|
+
$isListItemNode,
|
|
30
|
+
$isListNode,
|
|
31
|
+
ListItemNode,
|
|
32
|
+
ListNode,
|
|
33
|
+
} from '@lexical/list';
|
|
34
|
+
import type {ElementTransformer} from '@lexical/markdown';
|
|
35
|
+
import {$findMatchingParent, mergeRegister} from '@lexical/utils';
|
|
36
|
+
import {
|
|
37
|
+
$getSelection,
|
|
38
|
+
$getState,
|
|
39
|
+
$isRangeSelection,
|
|
40
|
+
$setState,
|
|
41
|
+
COMMAND_PRIORITY_LOW,
|
|
42
|
+
createState,
|
|
43
|
+
INSERT_PARAGRAPH_COMMAND,
|
|
44
|
+
KEY_ENTER_COMMAND,
|
|
45
|
+
type LexicalEditor,
|
|
46
|
+
} from 'lexical';
|
|
47
|
+
|
|
48
|
+
export type TaskState = 'checked' | 'unchecked';
|
|
49
|
+
|
|
50
|
+
// Whether an item is a task. A copy keeps it, so the item Enter makes after
|
|
51
|
+
// a task is a task too.
|
|
52
|
+
const taskKind = createState('astryxTask', {
|
|
53
|
+
parse: (value: unknown): boolean => value === true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Whether a task is checked.
|
|
57
|
+
const taskChecked = createState('astryxTaskChecked', {
|
|
58
|
+
parse: (value: unknown): boolean => value === true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether `item` is a task and whether it is checked; null for a plain item.
|
|
63
|
+
* An item in one of Lexical's own checkbox lists is a task too.
|
|
64
|
+
*/
|
|
65
|
+
export function $getTaskState(item: ListItemNode): TaskState | null {
|
|
66
|
+
if ($getState(item, taskKind)) {
|
|
67
|
+
return $getState(item, taskChecked) ? 'checked' : 'unchecked';
|
|
68
|
+
}
|
|
69
|
+
const list = item.getParent();
|
|
70
|
+
if ($isListNode(list) && list.getListType() === 'check') {
|
|
71
|
+
return item.getChecked() === true ? 'checked' : 'unchecked';
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Makes `item` a task in the given state, or a plain item for null. */
|
|
77
|
+
export function $setTaskState(
|
|
78
|
+
item: ListItemNode,
|
|
79
|
+
state: TaskState | null,
|
|
80
|
+
): void {
|
|
81
|
+
const list = item.getParent();
|
|
82
|
+
if ($isListNode(list) && list.getListType() === 'check') {
|
|
83
|
+
item.setChecked(state === 'checked');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
$setState(item, taskKind, state != null);
|
|
87
|
+
$setState(item, taskChecked, state === 'checked');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const TASK_ITEM = /^(\s*)([-*+])\s\[([ xX])\]\s/;
|
|
91
|
+
|
|
92
|
+
/** Nesting depth from leading whitespace, counted as Lexical's lists do. */
|
|
93
|
+
function indentOf(whitespace: string): number {
|
|
94
|
+
const tabs = whitespace.match(/\t/g)?.length ?? 0;
|
|
95
|
+
const spaces = whitespace.match(/ /g)?.length ?? 0;
|
|
96
|
+
return tabs + Math.floor(spaces / 4);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const TASK_LIST: ElementTransformer = {
|
|
100
|
+
dependencies: [ListNode, ListItemNode],
|
|
101
|
+
// LIST_EXPORT writes every list, task items included.
|
|
102
|
+
export: () => null,
|
|
103
|
+
regExp: TASK_ITEM,
|
|
104
|
+
replace: (parentNode, children, match, isImport) => {
|
|
105
|
+
const item = $createListItemNode();
|
|
106
|
+
$setState(item, taskKind, true);
|
|
107
|
+
$setState(item, taskChecked, match[3] !== ' ');
|
|
108
|
+
// A task item joins the bullet list beside it, as a plain item does.
|
|
109
|
+
const previous = parentNode.getPreviousSibling();
|
|
110
|
+
const next = parentNode.getNextSibling();
|
|
111
|
+
if ($isListNode(next) && next.getListType() === 'bullet') {
|
|
112
|
+
const first = next.getFirstChild();
|
|
113
|
+
if (first == null) {
|
|
114
|
+
next.append(item);
|
|
115
|
+
} else {
|
|
116
|
+
first.insertBefore(item);
|
|
117
|
+
}
|
|
118
|
+
parentNode.remove();
|
|
119
|
+
} else if ($isListNode(previous) && previous.getListType() === 'bullet') {
|
|
120
|
+
previous.append(item);
|
|
121
|
+
parentNode.remove();
|
|
122
|
+
} else {
|
|
123
|
+
const list = $createListNode('bullet');
|
|
124
|
+
list.append(item);
|
|
125
|
+
parentNode.replace(list);
|
|
126
|
+
}
|
|
127
|
+
item.append(...children);
|
|
128
|
+
if (!isImport) {
|
|
129
|
+
item.select(0, 0);
|
|
130
|
+
}
|
|
131
|
+
const indent = indentOf(match[1]);
|
|
132
|
+
if (indent > 0) {
|
|
133
|
+
item.setIndent(indent);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
type: 'element',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** The task item holding the selection's anchor, if any. */
|
|
140
|
+
function $selectedTaskItem(): ListItemNode | null {
|
|
141
|
+
const selection = $getSelection();
|
|
142
|
+
if (!$isRangeSelection(selection)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return $findMatchingParent(
|
|
146
|
+
selection.anchor.getNode(),
|
|
147
|
+
(node): node is ListItemNode =>
|
|
148
|
+
$isListItemNode(node) && $getTaskState(node) != null,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Task editing in an editor:
|
|
154
|
+
*
|
|
155
|
+
* - Enter in a task makes the new item a task, unchecked, as Lexical's own
|
|
156
|
+
* checkbox items do. Lexical copies the item to make the new one, state and
|
|
157
|
+
* all, so the new item's state is set here, where Enter makes it — never
|
|
158
|
+
* inferred later from content, which would uncheck a written `- [x]` with
|
|
159
|
+
* no text. Enter at the start of a task leaves its text, and its state, on
|
|
160
|
+
* the item that holds it, and puts the new, unchecked item before it. Enter
|
|
161
|
+
* in an empty task is left to the list, which ends the list there.
|
|
162
|
+
* - Mod+Enter checks or unchecks the task holding the caret.
|
|
163
|
+
*/
|
|
164
|
+
export function registerTaskListEditing(editor: LexicalEditor): () => void {
|
|
165
|
+
return mergeRegister(
|
|
166
|
+
editor.registerCommand(
|
|
167
|
+
INSERT_PARAGRAPH_COMMAND,
|
|
168
|
+
() => {
|
|
169
|
+
const selection = $getSelection();
|
|
170
|
+
const item = $selectedTaskItem();
|
|
171
|
+
if (
|
|
172
|
+
item == null ||
|
|
173
|
+
!$isRangeSelection(selection) ||
|
|
174
|
+
item.getTextContent().trim() === ''
|
|
175
|
+
) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const state = $getTaskState(item);
|
|
179
|
+
const isAtStart =
|
|
180
|
+
selection.isCollapsed() &&
|
|
181
|
+
selection.anchor.offset === 0 &&
|
|
182
|
+
(selection.anchor.getNode().is(item) ||
|
|
183
|
+
selection.anchor.getNode().is(item.getFirstDescendant()));
|
|
184
|
+
const created = selection.insertParagraph();
|
|
185
|
+
if (!$isListItemNode(created) || $getTaskState(created) == null) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (isAtStart) {
|
|
189
|
+
// The text moved to the new item: it keeps its state, and the item
|
|
190
|
+
// left empty before it is the new, unchecked one.
|
|
191
|
+
$setTaskState(created, state);
|
|
192
|
+
$setTaskState(item, 'unchecked');
|
|
193
|
+
} else {
|
|
194
|
+
$setTaskState(created, 'unchecked');
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
},
|
|
198
|
+
COMMAND_PRIORITY_LOW,
|
|
199
|
+
),
|
|
200
|
+
editor.registerCommand(
|
|
201
|
+
KEY_ENTER_COMMAND,
|
|
202
|
+
event => {
|
|
203
|
+
const isModified =
|
|
204
|
+
event != null &&
|
|
205
|
+
(event.metaKey || event.ctrlKey) &&
|
|
206
|
+
!event.shiftKey &&
|
|
207
|
+
!event.altKey;
|
|
208
|
+
const item = isModified ? $selectedTaskItem() : null;
|
|
209
|
+
if (item == null) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
event?.preventDefault();
|
|
213
|
+
$setTaskState(
|
|
214
|
+
item,
|
|
215
|
+
$getTaskState(item) === 'checked' ? 'unchecked' : 'checked',
|
|
216
|
+
);
|
|
217
|
+
return true;
|
|
218
|
+
},
|
|
219
|
+
COMMAND_PRIORITY_LOW,
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
}
|