@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.
Files changed (201) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/LICENSE +22 -0
  3. package/README.md +88 -2
  4. package/astryx.integration.mjs +11 -0
  5. package/blocks/RichTextEditorShowcase.template.mjs +13 -0
  6. package/blocks/RichTextEditorShowcase.tsx +19 -0
  7. package/dist/RichTextEditor.d.ts +282 -0
  8. package/dist/RichTextEditor.d.ts.map +1 -0
  9. package/dist/RichTextEditor.js +657 -0
  10. package/dist/RichTextEditorAutoLinkPlugin.d.ts +83 -0
  11. package/dist/RichTextEditorAutoLinkPlugin.d.ts.map +1 -0
  12. package/dist/RichTextEditorAutoLinkPlugin.js +99 -0
  13. package/dist/RichTextEditorToolbar.d.ts +186 -0
  14. package/dist/RichTextEditorToolbar.d.ts.map +1 -0
  15. package/dist/RichTextEditorToolbar.js +688 -0
  16. package/dist/RichTextView.d.ts +92 -0
  17. package/dist/RichTextView.d.ts.map +1 -0
  18. package/dist/RichTextView.js +267 -0
  19. package/dist/codeLanguages.d.ts +14 -0
  20. package/dist/codeLanguages.d.ts.map +1 -0
  21. package/dist/codeLanguages.js +34 -0
  22. package/dist/codeLineTokens.d.ts +53 -0
  23. package/dist/codeLineTokens.d.ts.map +1 -0
  24. package/dist/codeLineTokens.js +188 -0
  25. package/dist/editorNodes.d.ts +8 -0
  26. package/dist/editorNodes.d.ts.map +1 -0
  27. package/dist/editorNodes.js +30 -0
  28. package/dist/editorTheme.d.ts +7 -0
  29. package/dist/editorTheme.d.ts.map +1 -0
  30. package/dist/editorTheme.js +328 -0
  31. package/dist/index.d.ts +31 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +28 -0
  34. package/dist/internal/CodeBackspacePlugin.d.ts +2 -0
  35. package/dist/internal/CodeBackspacePlugin.d.ts.map +1 -0
  36. package/dist/internal/CodeBackspacePlugin.js +70 -0
  37. package/dist/internal/CodeBlockHeaderPlugin.d.ts +11 -0
  38. package/dist/internal/CodeBlockHeaderPlugin.d.ts.map +1 -0
  39. package/dist/internal/CodeBlockHeaderPlugin.js +204 -0
  40. package/dist/internal/CodePastePlugin.d.ts +5 -0
  41. package/dist/internal/CodePastePlugin.d.ts.map +1 -0
  42. package/dist/internal/CodePastePlugin.js +104 -0
  43. package/dist/internal/CodeSyntaxPlugin.d.ts +8 -0
  44. package/dist/internal/CodeSyntaxPlugin.d.ts.map +1 -0
  45. package/dist/internal/CodeSyntaxPlugin.js +397 -0
  46. package/dist/internal/CodeTabPlugin.d.ts +2 -0
  47. package/dist/internal/CodeTabPlugin.d.ts.map +1 -0
  48. package/dist/internal/CodeTabPlugin.js +256 -0
  49. package/dist/internal/DisabledLinkGuardPlugin.d.ts +4 -0
  50. package/dist/internal/DisabledLinkGuardPlugin.d.ts.map +1 -0
  51. package/dist/internal/DisabledLinkGuardPlugin.js +93 -0
  52. package/dist/internal/ImageSourceCaretPlugin.d.ts +3 -0
  53. package/dist/internal/ImageSourceCaretPlugin.d.ts.map +1 -0
  54. package/dist/internal/ImageSourceCaretPlugin.js +105 -0
  55. package/dist/internal/ListItemShortcutsPlugin.d.ts +6 -0
  56. package/dist/internal/ListItemShortcutsPlugin.d.ts.map +1 -0
  57. package/dist/internal/ListItemShortcutsPlugin.js +314 -0
  58. package/dist/internal/MarkdownExtensionsPlugin.d.ts +17 -0
  59. package/dist/internal/MarkdownExtensionsPlugin.d.ts.map +1 -0
  60. package/dist/internal/MarkdownExtensionsPlugin.js +185 -0
  61. package/dist/internal/SelectAllScopePlugin.d.ts +2 -0
  62. package/dist/internal/SelectAllScopePlugin.d.ts.map +1 -0
  63. package/dist/internal/SelectAllScopePlugin.js +67 -0
  64. package/dist/internal/TableColumnFloorPlugin.d.ts +12 -0
  65. package/dist/internal/TableColumnFloorPlugin.d.ts.map +1 -0
  66. package/dist/internal/TableColumnFloorPlugin.js +84 -0
  67. package/dist/internal/TablePastePlugin.d.ts +9 -0
  68. package/dist/internal/TablePastePlugin.d.ts.map +1 -0
  69. package/dist/internal/TablePastePlugin.js +183 -0
  70. package/dist/internal/TableScrollRegionPlugin.d.ts +9 -0
  71. package/dist/internal/TableScrollRegionPlugin.d.ts.map +1 -0
  72. package/dist/internal/TableScrollRegionPlugin.js +95 -0
  73. package/dist/internal/TaskCheckboxPlugin.d.ts +7 -0
  74. package/dist/internal/TaskCheckboxPlugin.d.ts.map +1 -0
  75. package/dist/internal/TaskCheckboxPlugin.js +193 -0
  76. package/dist/internal/ToolbarLinkPopover.d.ts +74 -0
  77. package/dist/internal/ToolbarLinkPopover.d.ts.map +1 -0
  78. package/dist/internal/ToolbarLinkPopover.js +292 -0
  79. package/dist/linkEditing.d.ts +6 -0
  80. package/dist/linkEditing.d.ts.map +1 -0
  81. package/dist/linkEditing.js +20 -0
  82. package/dist/linkForm.d.ts +28 -0
  83. package/dist/linkForm.d.ts.map +1 -0
  84. package/dist/linkForm.js +137 -0
  85. package/dist/linkUtils.d.ts +27 -0
  86. package/dist/linkUtils.d.ts.map +1 -0
  87. package/dist/linkUtils.js +101 -0
  88. package/dist/markdown.d.ts +17 -0
  89. package/dist/markdown.d.ts.map +1 -0
  90. package/dist/markdown.js +17 -0
  91. package/dist/markdownCharacterReferences.d.ts +135 -0
  92. package/dist/markdownCharacterReferences.d.ts.map +1 -0
  93. package/dist/markdownCharacterReferences.js +1127 -0
  94. package/dist/markdownCodeFence.d.ts +4 -0
  95. package/dist/markdownCodeFence.d.ts.map +1 -0
  96. package/dist/markdownCodeFence.js +167 -0
  97. package/dist/markdownExtensionNode.d.ts +88 -0
  98. package/dist/markdownExtensionNode.d.ts.map +1 -0
  99. package/dist/markdownExtensionNode.js +248 -0
  100. package/dist/markdownExtensions.d.ts +115 -0
  101. package/dist/markdownExtensions.d.ts.map +1 -0
  102. package/dist/markdownExtensions.js +392 -0
  103. package/dist/markdownHardLineBreak.d.ts +25 -0
  104. package/dist/markdownHardLineBreak.d.ts.map +1 -0
  105. package/dist/markdownHardLineBreak.js +59 -0
  106. package/dist/markdownHeading.d.ts +20 -0
  107. package/dist/markdownHeading.d.ts.map +1 -0
  108. package/dist/markdownHeading.js +30 -0
  109. package/dist/markdownLink.d.ts +5 -0
  110. package/dist/markdownLink.d.ts.map +1 -0
  111. package/dist/markdownLink.js +80 -0
  112. package/dist/markdownListExport.d.ts +3 -0
  113. package/dist/markdownListExport.d.ts.map +1 -0
  114. package/dist/markdownListExport.js +68 -0
  115. package/dist/markdownListIndentation.d.ts +8 -0
  116. package/dist/markdownListIndentation.d.ts.map +1 -0
  117. package/dist/markdownListIndentation.js +101 -0
  118. package/dist/markdownOrderedList.d.ts +3 -0
  119. package/dist/markdownOrderedList.d.ts.map +1 -0
  120. package/dist/markdownOrderedList.js +51 -0
  121. package/dist/markdownQuote.d.ts +22 -0
  122. package/dist/markdownQuote.d.ts.map +1 -0
  123. package/dist/markdownQuote.js +32 -0
  124. package/dist/markdownSerializers.d.ts +49 -0
  125. package/dist/markdownSerializers.d.ts.map +1 -0
  126. package/dist/markdownSerializers.js +83 -0
  127. package/dist/markdownSource.d.ts +71 -0
  128. package/dist/markdownSource.d.ts.map +1 -0
  129. package/dist/markdownSource.js +804 -0
  130. package/dist/markdownSpaceRuns.d.ts +24 -0
  131. package/dist/markdownSpaceRuns.d.ts.map +1 -0
  132. package/dist/markdownSpaceRuns.js +147 -0
  133. package/dist/markdownTable.d.ts +35 -0
  134. package/dist/markdownTable.d.ts.map +1 -0
  135. package/dist/markdownTable.js +227 -0
  136. package/dist/markdownTaskList.d.ts +48 -0
  137. package/dist/markdownTaskList.d.ts.map +1 -0
  138. package/dist/markdownTaskList.js +166 -0
  139. package/dist/markdownThematicBreak.d.ts +3 -0
  140. package/dist/markdownThematicBreak.d.ts.map +1 -0
  141. package/dist/markdownThematicBreak.js +55 -0
  142. package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts +12 -0
  143. package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts.map +1 -0
  144. package/dist/privateAttributes/layerPortalBoundary.stylex.js +16 -0
  145. package/dist/richtext.css +213 -0
  146. package/dist/textSemantics.d.ts +3 -0
  147. package/dist/textSemantics.d.ts.map +1 -0
  148. package/dist/textSemantics.js +87 -0
  149. package/package.json +158 -3
  150. package/src/RichTextEditor.doc.mjs +308 -0
  151. package/src/RichTextEditor.tsx +1109 -0
  152. package/src/RichTextEditorAutoLinkPlugin.doc.mjs +43 -0
  153. package/src/RichTextEditorAutoLinkPlugin.tsx +126 -0
  154. package/src/RichTextEditorToolbar.doc.mjs +92 -0
  155. package/src/RichTextEditorToolbar.tsx +871 -0
  156. package/src/RichTextView.doc.mjs +101 -0
  157. package/src/RichTextView.tsx +351 -0
  158. package/src/codeLanguages.ts +42 -0
  159. package/src/codeLineTokens.ts +241 -0
  160. package/src/doc-mjs.d.ts +23 -0
  161. package/src/editorNodes.ts +44 -0
  162. package/src/editorTheme.ts +550 -0
  163. package/src/index.ts +59 -0
  164. package/src/internal/CodeBackspacePlugin.tsx +96 -0
  165. package/src/internal/CodeBlockHeaderPlugin.tsx +282 -0
  166. package/src/internal/CodePastePlugin.tsx +144 -0
  167. package/src/internal/CodeSyntaxPlugin.tsx +493 -0
  168. package/src/internal/CodeTabPlugin.tsx +298 -0
  169. package/src/internal/DisabledLinkGuardPlugin.tsx +93 -0
  170. package/src/internal/ImageSourceCaretPlugin.tsx +159 -0
  171. package/src/internal/ListItemShortcutsPlugin.tsx +404 -0
  172. package/src/internal/MarkdownExtensionsPlugin.tsx +289 -0
  173. package/src/internal/SelectAllScopePlugin.tsx +86 -0
  174. package/src/internal/TableColumnFloorPlugin.tsx +99 -0
  175. package/src/internal/TablePastePlugin.tsx +223 -0
  176. package/src/internal/TableScrollRegionPlugin.tsx +107 -0
  177. package/src/internal/TaskCheckboxPlugin.tsx +246 -0
  178. package/src/internal/ToolbarLinkPopover.tsx +392 -0
  179. package/src/linkEditing.ts +20 -0
  180. package/src/linkForm.tsx +156 -0
  181. package/src/linkUtils.ts +106 -0
  182. package/src/markdown.ts +25 -0
  183. package/src/markdownCharacterReferences.ts +1137 -0
  184. package/src/markdownCodeFence.ts +216 -0
  185. package/src/markdownExtensionNode.ts +339 -0
  186. package/src/markdownExtensions.ts +496 -0
  187. package/src/markdownHardLineBreak.ts +73 -0
  188. package/src/markdownHeading.ts +33 -0
  189. package/src/markdownLink.ts +86 -0
  190. package/src/markdownListExport.ts +85 -0
  191. package/src/markdownListIndentation.ts +105 -0
  192. package/src/markdownOrderedList.ts +60 -0
  193. package/src/markdownQuote.ts +35 -0
  194. package/src/markdownSerializers.ts +108 -0
  195. package/src/markdownSource.ts +1045 -0
  196. package/src/markdownSpaceRuns.ts +164 -0
  197. package/src/markdownTable.ts +309 -0
  198. package/src/markdownTaskList.ts +222 -0
  199. package/src/markdownThematicBreak.ts +66 -0
  200. package/src/privateAttributes/layerPortalBoundary.stylex.ts +18 -0
  201. package/src/textSemantics.ts +104 -0
@@ -0,0 +1,93 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file DisabledLinkGuardPlugin.tsx
7
+ * @input The composer's editor, and whether RichTextEditor is disabled
8
+ * @output DisabledLinkGuardPlugin (internal; not re-exported from index.ts):
9
+ * while the editor is disabled, its links add no tab stop and never navigate
10
+ * @position Rendered by RichTextEditor inside its LexicalExtensionComposer
11
+ *
12
+ * A disabled editor is not editable, so the browser treats each link in it as
13
+ * a live link: a tab stop that navigates on a click or on Enter. A disabled
14
+ * control must not navigate, and it keeps the focus order it has when enabled,
15
+ * where its links are not tab stops.
16
+ */
17
+ import { useEffect } from 'react';
18
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
19
+
20
+ // Enter on a focused link and a screen reader's activation arrive as a click.
21
+ // A middle click arrives as auxclick.
22
+ function preventLinkActivation(event) {
23
+ if (event.target instanceof Element && event.target.closest('a[href]')) {
24
+ event.preventDefault();
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Guards the links under one root element. It records the links it changes,
30
+ * so it restores only those.
31
+ */
32
+ function createLinkGuard(root) {
33
+ const guarded = new Set();
34
+ const guard = () => {
35
+ for (const link of root.querySelectorAll('a[href]')) {
36
+ if (!guarded.has(link)) {
37
+ guarded.add(link);
38
+ link.setAttribute('tabindex', '-1');
39
+ // Assistive technology announces each link as disabled. Only the
40
+ // link ignores the pointer, so a wide table still scrolls.
41
+ link.setAttribute('aria-disabled', 'true');
42
+ if (link instanceof HTMLElement) {
43
+ link.style.pointerEvents = 'none';
44
+ }
45
+ }
46
+ }
47
+ };
48
+ guard();
49
+ root.addEventListener('click', preventLinkActivation, true);
50
+ root.addEventListener('auxclick', preventLinkActivation, true);
51
+ return {
52
+ guard,
53
+ release() {
54
+ for (const link of guarded) {
55
+ link.removeAttribute('tabindex');
56
+ link.removeAttribute('aria-disabled');
57
+ if (link instanceof HTMLElement) {
58
+ link.style.removeProperty('pointer-events');
59
+ }
60
+ }
61
+ guarded.clear();
62
+ root.removeEventListener('click', preventLinkActivation, true);
63
+ root.removeEventListener('auxclick', preventLinkActivation, true);
64
+ }
65
+ };
66
+ }
67
+ export function DisabledLinkGuardPlugin({
68
+ isDisabled
69
+ }) {
70
+ const [editor] = useLexicalComposerContext();
71
+ // Synchronizes the editor's link elements, which Lexical renders and owns,
72
+ // with the disabled state: guarded while disabled, restored when enabled.
73
+ useEffect(() => {
74
+ if (!isDisabled) {
75
+ return undefined;
76
+ }
77
+ let linkGuard = null;
78
+ const unregisterRoot = editor.registerRootListener(next => {
79
+ linkGuard?.release();
80
+ linkGuard = next == null ? null : createLinkGuard(next);
81
+ });
82
+ // A new or changed document renders new link elements.
83
+ const unregisterUpdate = editor.registerUpdateListener(() => {
84
+ linkGuard?.guard();
85
+ });
86
+ return () => {
87
+ unregisterUpdate();
88
+ unregisterRoot();
89
+ linkGuard?.release();
90
+ };
91
+ }, [editor, isDisabled]);
92
+ return null;
93
+ }
@@ -0,0 +1,3 @@
1
+ /** Keeps a collapsed caret out of the text holding an image's source. */
2
+ export declare function ImageSourceCaretPlugin(): null;
3
+ //# sourceMappingURL=ImageSourceCaretPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageSourceCaretPlugin.d.ts","sourceRoot":"","sources":["../../src/internal/ImageSourceCaretPlugin.tsx"],"names":[],"mappings":"AAyJA,yEAAyE;AACzE,wBAAgB,sBAAsB,IAAI,IAAI,CAI7C"}
@@ -0,0 +1,105 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file ImageSourceCaretPlugin.tsx
7
+ * @input Uses @lexical/react (composer context), lexical (selection, the
8
+ * selection-change and text-insertion commands, the root element), and
9
+ * $isImageSource from markdownCharacterReferences.ts.
10
+ * @output Exports ImageSourceCaretPlugin.
11
+ * @position Rendered by RichTextEditor, always on. Lexical replaces a whole
12
+ * token with text typed inside it. So a caret inside an image's source
13
+ * moves to an edge: past the image for an arrow key, to the nearer edge
14
+ * for a click or any other move.
15
+ */
16
+ import { useEffect } from 'react';
17
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
18
+ import { mergeRegister } from '@lexical/utils';
19
+ import { $getNodeByKey, $getSelection, $isRangeSelection, COMMAND_PRIORITY_CRITICAL, CONTROLLED_TEXT_INSERTION_COMMAND, SELECTION_CHANGE_COMMAND } from 'lexical';
20
+ import { $isImageSource } from "../markdownCharacterReferences.js";
21
+ /** The collapsed caret, when it is in text. */
22
+ function $caretPoint() {
23
+ const selection = $getSelection();
24
+ return $isRangeSelection(selection) && selection.isCollapsed() && selection.anchor.type === 'text' ? {
25
+ key: selection.anchor.key,
26
+ offset: selection.anchor.offset
27
+ } : null;
28
+ }
29
+
30
+ /**
31
+ * The edge of `token` a caret at `offset` inside it moves to. An arrow key
32
+ * that moved the caret in from one edge, or from the text beside it, moves
33
+ * it on to the other edge.
34
+ */
35
+ function $edgeFor(token, offset, arrowFrom) {
36
+ const size = token.getTextContentSize();
37
+ const nearer = offset < size / 2 ? 0 : size;
38
+ if (arrowFrom == null) {
39
+ return nearer;
40
+ }
41
+ if (arrowFrom.key === token.getKey()) {
42
+ return arrowFrom.offset === 0 ? size : arrowFrom.offset === size ? 0 : nearer;
43
+ }
44
+ const before = token.getPreviousSibling();
45
+ if (before != null && before.getKey() === arrowFrom.key && arrowFrom.offset === before.getTextContentSize()) {
46
+ return size;
47
+ }
48
+ const after = token.getNextSibling();
49
+ if (after != null && after.getKey() === arrowFrom.key && arrowFrom.offset === 0) {
50
+ return 0;
51
+ }
52
+ return nearer;
53
+ }
54
+
55
+ /** Moves a collapsed caret inside an image's source to an edge of it. */
56
+ function $moveCaretOutOfImage(arrowFrom) {
57
+ const caret = $caretPoint();
58
+ if (caret == null) {
59
+ return;
60
+ }
61
+ const node = $getNodeByKey(caret.key);
62
+ if (node == null || !$isImageSource(node) || caret.offset === 0 || caret.offset === node.getTextContentSize()) {
63
+ return;
64
+ }
65
+ const edge = $edgeFor(node, caret.offset, arrowFrom);
66
+ const selection = $getSelection();
67
+ if ($isRangeSelection(selection)) {
68
+ selection.anchor.set(caret.key, edge, 'text');
69
+ selection.focus.set(caret.key, edge, 'text');
70
+ }
71
+ }
72
+ function registerImageSourceCaret(editor) {
73
+ // Where the caret was when an arrow key went down; the next selection
74
+ // change reads it once. A press is a click, never an arrow key.
75
+ let arrowFrom = null;
76
+ const onKeyDown = event => {
77
+ arrowFrom = event.key === 'ArrowLeft' || event.key === 'ArrowRight' ? editor.getEditorState().read($caretPoint) : null;
78
+ };
79
+ const onPointerDown = () => {
80
+ arrowFrom = null;
81
+ };
82
+ return mergeRegister(editor.registerRootListener((root, previousRoot) => {
83
+ previousRoot?.removeEventListener('keydown', onKeyDown, true);
84
+ previousRoot?.removeEventListener('pointerdown', onPointerDown, true);
85
+ root?.addEventListener('keydown', onKeyDown, true);
86
+ root?.addEventListener('pointerdown', onPointerDown, true);
87
+ }), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
88
+ $moveCaretOutOfImage(arrowFrom);
89
+ arrowFrom = null;
90
+ return false;
91
+ }, COMMAND_PRIORITY_CRITICAL),
92
+ // A key typed before the browser reports a caret move reaches the editor
93
+ // with the caret still inside the source.
94
+ editor.registerCommand(CONTROLLED_TEXT_INSERTION_COMMAND, () => {
95
+ $moveCaretOutOfImage(null);
96
+ return false;
97
+ }, COMMAND_PRIORITY_CRITICAL));
98
+ }
99
+
100
+ /** Keeps a collapsed caret out of the text holding an image's source. */
101
+ export function ImageSourceCaretPlugin() {
102
+ const [editor] = useLexicalComposerContext();
103
+ useEffect(() => registerImageSourceCaret(editor), [editor]);
104
+ return null;
105
+ }
@@ -0,0 +1,6 @@
1
+ import { type Transformer } from '@lexical/markdown';
2
+ export declare function ListItemShortcutsPlugin({ transformers, }: {
3
+ /** The editor's Markdown transformers; markers they do not make are left. */
4
+ readonly transformers: ReadonlyArray<Transformer>;
5
+ }): null;
6
+ //# sourceMappingURL=ListItemShortcutsPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ListItemShortcutsPlugin.d.ts","sourceRoot":"","sources":["../../src/internal/ListItemShortcutsPlugin.tsx"],"names":[],"mappings":"AA8BA,OAAO,EAAa,KAAK,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAyW/D,wBAAgB,uBAAuB,CAAC,EACtC,YAAY,GACb,EAAE;IACD,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;CACnD,GAAG,IAAI,CAOP"}
@@ -0,0 +1,314 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file ListItemShortcutsPlugin.tsx
7
+ * @input Uses @lexical/react (composer context), @lexical/list,
8
+ * @lexical/rich-text, @lexical/markdown (Transformer), lexical, and the task
9
+ * state in markdownTaskList.ts.
10
+ * @output Exports ListItemShortcutsPlugin, which re-marks a list item when a
11
+ * Markdown block marker is typed at its start, keeping the item's text:
12
+ * `- `, `1. `, or `[ ] ` switches the item's list between bullets, numbers,
13
+ * and tasks, and `# ` or `> ` takes a top-level item out of its list as a
14
+ * heading or a quote, the rest of the list continuing after it.
15
+ * @position Rendered by RichTextEditor beside MarkdownShortcutPlugin, only
16
+ * when `hasMarkdownShortcuts` is on. Lexical's shortcuts never look inside a
17
+ * list. A marker counts only when the editor's `transformers` make that
18
+ * block from it, and a task takes the shape of the transformer that matched.
19
+ * Each re-mark is one undo step.
20
+ */
21
+ import { useEffect } from 'react';
22
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
23
+ import { $createListNode, $isListItemNode, $isListNode } from '@lexical/list';
24
+ import { CHECK_LIST } from '@lexical/markdown';
25
+ import { $createHeadingNode, $createQuoteNode } from '@lexical/rich-text';
26
+ import { $addUpdateTag, $getNodeByKey, $getSelection, $isRangeSelection, $isRootOrShadowRoot, $isTextNode, COLLABORATION_TAG, HISTORIC_TAG, HISTORY_PUSH_TAG } from 'lexical';
27
+ import { $getTaskState, $setTaskState } from "../markdownTaskList.js";
28
+
29
+ /** What a marker typed at the start of a list item makes of it. */
30
+
31
+ /** Whether `marker` takes an item out of its list, rather than switching it. */
32
+ function isBlockMarker(marker) {
33
+ return marker.kind === 'heading' || marker.kind === 'quote';
34
+ }
35
+
36
+ /**
37
+ * The marker `prefix` is (the text before the caret, ending in the space
38
+ * just typed), and the line that would make the same block at the start of a
39
+ * paragraph. A task marker needs a bullet there, as GFM task items do.
40
+ */
41
+ function markerOf(prefix) {
42
+ const heading = /^(#{1,6}) $/.exec(prefix);
43
+ if (heading != null) {
44
+ return {
45
+ marker: {
46
+ kind: 'heading',
47
+ tag: `h${heading[1].length}`
48
+ },
49
+ line: prefix
50
+ };
51
+ }
52
+ if (prefix === '> ') {
53
+ return {
54
+ marker: {
55
+ kind: 'quote'
56
+ },
57
+ line: prefix
58
+ };
59
+ }
60
+ if (/^[-*+] $/.test(prefix)) {
61
+ return {
62
+ marker: {
63
+ kind: 'bullet'
64
+ },
65
+ line: prefix
66
+ };
67
+ }
68
+ const number = /^(\d{1,9})\. $/.exec(prefix);
69
+ if (number != null) {
70
+ return {
71
+ marker: {
72
+ kind: 'number',
73
+ start: Number(number[1])
74
+ },
75
+ line: prefix
76
+ };
77
+ }
78
+ const task = /^([-*+] )?\[([ xX])\] $/.exec(prefix);
79
+ if (task != null) {
80
+ return {
81
+ marker: {
82
+ kind: 'task',
83
+ checked: task[2] !== ' '
84
+ },
85
+ line: task[1] == null ? `- ${prefix}` : prefix
86
+ };
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /** The first of `transformers` that makes a block from `line`, all of it. */
92
+ function transformerFor(line, transformers) {
93
+ return transformers.find(transformer => transformer.type === 'element' && line.match(transformer.regExp)?.[0] === line);
94
+ }
95
+
96
+ /** Whether `node` is the item Lexical nests a list in, not an item itself. */
97
+ function $holdsNestedList(node) {
98
+ return $isListItemNode(node) && node.getChildrenSize() === 1 && $isListNode(node.getFirstChild());
99
+ }
100
+
101
+ /** The items of `list` that have content of their own. */
102
+ function $itemsOf(list) {
103
+ return list.getChildren().filter(child => $isListItemNode(child) && !$holdsNestedList(child));
104
+ }
105
+
106
+ /**
107
+ * Switches the list holding `item` to bullets, numbers, or tasks. Tasks take
108
+ * the shape the matching transformer reads and writes: Lexical's `check`
109
+ * list for CHECK_LIST, or else RichText's bulleted items with task state
110
+ * (markdownTaskList.ts). Switching to tasks makes every item a task, keeping
111
+ * the state of those that already were, and `item` takes the marker's;
112
+ * switching to bullets or numbers makes every item a plain one. A numbered
113
+ * list starts at the marker's number when `item` is its first item.
114
+ */
115
+ function $switchList(list, item, marker, isCheckList) {
116
+ const items = $itemsOf(list);
117
+ if (marker.kind === 'task' && isCheckList) {
118
+ // A check list already keeps each item's state; only another list is
119
+ // converted, carrying over RichText's task state.
120
+ if (list.getListType() !== 'check') {
121
+ const states = items.map(each => $getTaskState(each));
122
+ for (const each of items) {
123
+ $setTaskState(each, null);
124
+ }
125
+ list.setListType('check');
126
+ items.forEach((each, index) => {
127
+ each.setChecked(states[index] === 'checked');
128
+ });
129
+ }
130
+ item.setChecked(marker.checked);
131
+ return;
132
+ }
133
+ if (marker.kind === 'task') {
134
+ // Read a check list's states first: Lexical reports no checked state
135
+ // once the list is no longer a check list.
136
+ const checkStates = list.getListType() === 'check' ? items.map(each => each.getChecked() === true) : null;
137
+ if (list.getListType() !== 'bullet') {
138
+ list.setListType('bullet');
139
+ }
140
+ items.forEach((each, index) => {
141
+ if (checkStates != null) {
142
+ each.setChecked(undefined);
143
+ $setTaskState(each, checkStates[index] ? 'checked' : 'unchecked');
144
+ } else if ($getTaskState(each) == null) {
145
+ $setTaskState(each, 'unchecked');
146
+ }
147
+ });
148
+ $setTaskState(item, marker.checked ? 'checked' : 'unchecked');
149
+ return;
150
+ }
151
+ for (const each of items) {
152
+ if ($getTaskState(each) != null) {
153
+ $setTaskState(each, null);
154
+ }
155
+ if (each.getChecked() !== undefined) {
156
+ each.setChecked(undefined);
157
+ }
158
+ }
159
+ const wasNumbered = list.getListType() === 'number';
160
+ list.setListType(marker.kind);
161
+ if (marker.kind === 'number') {
162
+ if (items[0]?.is(item) === true) {
163
+ list.setStart(marker.start);
164
+ } else if (!wasNumbered) {
165
+ list.setStart(1);
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Takes `item` out of `list` as `block`, holding the item's content. The
172
+ * items before it stay in `list`; the lists nested under it follow `block`
173
+ * at the top level, since a heading or a quote cannot hold them; and the
174
+ * items after it continue in a list of the same kind, numbered on from where
175
+ * they were. When the last of those nested lists has the same type, Lexical
176
+ * merges the items after it into that list, which numbers them on from its
177
+ * own items.
178
+ */
179
+ function $takeOutOfList(list, item, block) {
180
+ const nested = [];
181
+ let next = item.getNextSibling();
182
+ while ($holdsNestedList(next)) {
183
+ nested.push(next);
184
+ next = next.getNextSibling();
185
+ }
186
+ const after = next == null ? [] : [next, ...next.getNextSiblings()];
187
+ const before = $itemsOf(list).findIndex(each => each.is(item));
188
+ block.append(...item.getChildren());
189
+ list.insertAfter(block);
190
+ let last = block;
191
+ for (const holder of nested) {
192
+ const child = holder.getFirstChild();
193
+ if ($isListNode(child)) {
194
+ last.insertAfter(child);
195
+ last = child;
196
+ }
197
+ holder.remove();
198
+ }
199
+ if (after.length > 0) {
200
+ const type = list.getListType();
201
+ const rest = $createListNode(type, type === 'number' ? list.getStart() + before + 1 : 1);
202
+ rest.append(...after);
203
+ last.insertAfter(rest);
204
+ }
205
+ item.remove();
206
+ if (list.getChildrenSize() === 0) {
207
+ list.remove();
208
+ }
209
+ }
210
+
211
+ /** Re-marks the item whose first text, `key`, starts with `prefix`. */
212
+ function $remark(key, prefix, marker, isCheckList) {
213
+ const text = $getNodeByKey(key);
214
+ const item = text?.getParent();
215
+ const list = item?.getParent();
216
+ if (!$isTextNode(text) || !text.getTextContent().startsWith(prefix) || !$isListItemNode(item) || !text.is(item.getFirstChild()) || !$isListNode(list) || isBlockMarker(marker) && !$isRootOrShadowRoot(list.getParent())) {
217
+ return;
218
+ }
219
+ const rest = text.getTextContent().slice(prefix.length);
220
+ if (rest === '') {
221
+ text.remove();
222
+ } else {
223
+ text.setTextContent(rest);
224
+ }
225
+ if (isBlockMarker(marker)) {
226
+ const block = marker.kind === 'heading' ? $createHeadingNode(marker.tag) : $createQuoteNode();
227
+ $takeOutOfList(list, item, block);
228
+ block.selectStart();
229
+ } else {
230
+ $switchList(list, item, marker, isCheckList);
231
+ // Lexical merges a dirty list with a list of the same type after it, but
232
+ // not with one before it. Marking the list before it joins the two, as
233
+ // the Markdown reads back.
234
+ const previous = list.getPreviousSibling();
235
+ if ($isListNode(previous) && previous.getListType() === list.getListType()) {
236
+ previous.markDirty();
237
+ }
238
+ item.selectStart();
239
+ }
240
+ // Its own undo step, so undo returns to the marker as typed.
241
+ $addUpdateTag(HISTORY_PUSH_TAG);
242
+ }
243
+
244
+ /**
245
+ * Watches for a marker completed by a typed space at the start of a list
246
+ * item's text, and re-marks the item in an update of its own, as Lexical's
247
+ * Markdown shortcuts do for blocks in the root.
248
+ */
249
+ function registerListItemShortcuts(editor, transformers) {
250
+ return editor.registerUpdateListener(({
251
+ tags,
252
+ dirtyLeaves,
253
+ editorState,
254
+ prevEditorState
255
+ }) => {
256
+ if (tags.has(COLLABORATION_TAG) || tags.has(HISTORIC_TAG) || dirtyLeaves.size === 0 || editor.isComposing() || !editor.isEditable()) {
257
+ return;
258
+ }
259
+ const previous = prevEditorState.read(() => {
260
+ const selection = $getSelection();
261
+ return $isRangeSelection(selection) && selection.isCollapsed() ? selection.anchor : null;
262
+ });
263
+ if (previous == null) {
264
+ return;
265
+ }
266
+ const found = editorState.read(() => {
267
+ const selection = $getSelection();
268
+ if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
269
+ return null;
270
+ }
271
+ const {
272
+ key,
273
+ offset
274
+ } = selection.anchor;
275
+ // One character typed, just now, in the same text.
276
+ if (!dirtyLeaves.has(key) || previous.key !== key || previous.offset !== offset - 1) {
277
+ return null;
278
+ }
279
+ const text = selection.anchor.getNode();
280
+ if (!$isTextNode(text) || text.hasFormat('code')) {
281
+ return null;
282
+ }
283
+ const prefix = text.getTextContent().slice(0, offset);
284
+ const match = markerOf(prefix);
285
+ const item = text.getParent();
286
+ const list = item?.getParent();
287
+ const transformer = match == null ? undefined : transformerFor(match.line, transformers);
288
+ if (match == null || transformer == null || !$isListItemNode(item) || !text.is(item.getFirstChild()) || !$isListNode(list)) {
289
+ return null;
290
+ }
291
+ // A heading or a quote can only stand where a top-level block does;
292
+ // in a nested list the marker stays as typed.
293
+ if (isBlockMarker(match.marker) && !$isRootOrShadowRoot(list.getParent())) {
294
+ return null;
295
+ }
296
+ return {
297
+ key,
298
+ prefix,
299
+ marker: match.marker,
300
+ isCheckList: transformer === CHECK_LIST
301
+ };
302
+ });
303
+ if (found != null) {
304
+ editor.update(() => $remark(found.key, found.prefix, found.marker, found.isCheckList));
305
+ }
306
+ });
307
+ }
308
+ export function ListItemShortcutsPlugin({
309
+ transformers
310
+ }) {
311
+ const [editor] = useLexicalComposerContext();
312
+ useEffect(() => registerListItemShortcuts(editor, transformers), [editor, transformers]);
313
+ return null;
314
+ }
@@ -0,0 +1,17 @@
1
+ import type { Transformer } from '@lexical/markdown';
2
+ import type { MarkdownExtensionNode, MarkdownPluginEntry } from '@blue-patterns/core/Markdown/plugins';
3
+ import { type RichTextMarkdownExtension } from '../markdownExtensions.js';
4
+ import { type RichTextExtensionNodeFacts } from '../markdownExtensionNode.js';
5
+ /**
6
+ * The node core derives from a stored node's source with `plugins`, or null
7
+ * when no adopted plugin of the same protocol version reads that exact source
8
+ * as that node.
9
+ */
10
+ export declare function deriveExtensionNode(facts: RichTextExtensionNodeFacts, plugins: ReadonlyArray<MarkdownPluginEntry>): MarkdownExtensionNode | null;
11
+ export interface MarkdownExtensionsPluginProps {
12
+ extensions: ReadonlyArray<RichTextMarkdownExtension>;
13
+ /** The surface's Markdown transformers, checked against the extensions. */
14
+ transformers: ReadonlyArray<Transformer>;
15
+ }
16
+ export declare function MarkdownExtensionsPlugin({ extensions, transformers, }: MarkdownExtensionsPluginProps): null;
17
+ //# sourceMappingURL=MarkdownExtensionsPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownExtensionsPlugin.d.ts","sourceRoot":"","sources":["../../src/internal/MarkdownExtensionsPlugin.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAwBnD,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAY,KAAK,yBAAyB,EAAC,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAKL,KAAK,0BAA0B,EAChC,MAAM,0BAA0B,CAAC;AAElC;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,0BAA0B,EACjC,OAAO,EAAE,aAAa,CAAC,mBAAmB,CAAC,GAC1C,qBAAqB,GAAG,IAAI,CAoB9B;AAyHD,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,aAAa,CAAC,yBAAyB,CAAC,CAAC;IACrD,2EAA2E;IAC3E,YAAY,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;CAC1C;AAED,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,YAAY,GACb,EAAE,6BAA6B,GAAG,IAAI,CAoEtC"}