@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,85 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file markdownListExport.ts
|
|
5
|
+
* @input Uses @lexical/list (ListNode, ListItemNode) and @lexical/markdown
|
|
6
|
+
* (ElementTransformer).
|
|
7
|
+
* @output Exports LIST_EXPORT, which writes lists the way Lexical's list
|
|
8
|
+
* transformers do except for nesting: a nested list is indented to its
|
|
9
|
+
* parent item's content column instead of four spaces per level, and each
|
|
10
|
+
* task item writes `[ ]` or `[x]` (markdownTaskList.ts).
|
|
11
|
+
* @position Part of DEFAULT_TRANSFORMERS (markdownTable.ts), ahead of
|
|
12
|
+
* Lexical's list transformers, which still import. Import reads nesting the
|
|
13
|
+
* CommonMark way (markdownListIndentation.ts), so a child written four
|
|
14
|
+
* spaces under `100. parent` (content column five) would read back at the
|
|
15
|
+
* parent's level; indenting to the content column keeps an edited list's
|
|
16
|
+
* structure when it is read again (spec:AST-062 FR3).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
$isListItemNode,
|
|
21
|
+
$isListNode,
|
|
22
|
+
ListItemNode,
|
|
23
|
+
ListNode,
|
|
24
|
+
} from '@lexical/list';
|
|
25
|
+
import type {ElementTransformer} from '@lexical/markdown';
|
|
26
|
+
import type {ElementNode} from 'lexical';
|
|
27
|
+
import {$getTaskState} from './markdownTaskList';
|
|
28
|
+
|
|
29
|
+
/** The bullet Lexical recorded for a list on import (`-`, `*`, or `+`). */
|
|
30
|
+
function bulletOf(list: ListNode): string {
|
|
31
|
+
// The export may hand over a view of the list (an object inheriting from
|
|
32
|
+
// the node, see markdownSource.ts); Lexical validates the fields of the
|
|
33
|
+
// node it serializes, so read the recorded state from the node itself.
|
|
34
|
+
const serialized = list.getLatest().exportJSON() as {
|
|
35
|
+
$?: {mdListMarker?: unknown};
|
|
36
|
+
};
|
|
37
|
+
const marker = serialized.$?.mdListMarker;
|
|
38
|
+
return marker === '*' || marker === '+' ? marker : '-';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function exportList(
|
|
42
|
+
list: ListNode,
|
|
43
|
+
exportChildren: (node: ElementNode) => string,
|
|
44
|
+
indent: string,
|
|
45
|
+
): string {
|
|
46
|
+
const lines: Array<string> = [];
|
|
47
|
+
const type = list.getListType();
|
|
48
|
+
let index = 0;
|
|
49
|
+
// Where a nested list goes: the content column of the item before it.
|
|
50
|
+
let childIndent = `${indent} `;
|
|
51
|
+
for (const item of list.getChildren()) {
|
|
52
|
+
if (!$isListItemNode(item)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const first = item.getFirstChild();
|
|
56
|
+
if (item.getChildrenSize() === 1 && $isListNode(first)) {
|
|
57
|
+
lines.push(exportList(first, exportChildren, childIndent));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const marker =
|
|
61
|
+
type === 'number' ? `${list.getStart() + index}. ` : `${bulletOf(list)} `;
|
|
62
|
+
// A task item, in any bulleted list, writes its box.
|
|
63
|
+
const task = type === 'number' ? null : $getTaskState(item);
|
|
64
|
+
const checkbox = task == null ? '' : `[${task === 'checked' ? 'x' : ' '}] `;
|
|
65
|
+
let text = exportChildren(item);
|
|
66
|
+
if (type !== 'number') {
|
|
67
|
+
// As Lexical does: keep `1. ` at the start of a bullet item literal.
|
|
68
|
+
text = text.replace(/^(\s{0,3}\d+)(\.\s)/, '$1\\$2');
|
|
69
|
+
}
|
|
70
|
+
lines.push(indent + marker + checkbox + text);
|
|
71
|
+
childIndent = indent + ' '.repeat(marker.length);
|
|
72
|
+
index++;
|
|
73
|
+
}
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const LIST_EXPORT: ElementTransformer = {
|
|
78
|
+
dependencies: [ListNode, ListItemNode],
|
|
79
|
+
export: (node, exportChildren) =>
|
|
80
|
+
$isListNode(node) ? exportList(node, exportChildren, '') : null,
|
|
81
|
+
// Export only: Lexical's list transformers import.
|
|
82
|
+
regExp: /(?!)/,
|
|
83
|
+
replace: () => false,
|
|
84
|
+
type: 'element',
|
|
85
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file markdownListIndentation.ts
|
|
5
|
+
* @input Markdown source text.
|
|
6
|
+
* @output Exports normalizeListIndentation, which rewrites the indentation of
|
|
7
|
+
* every list item line so its nesting reads the same to Lexical as it does
|
|
8
|
+
* to CommonMark.
|
|
9
|
+
* @position Used by markdownSource.ts on the text it imports (never on the
|
|
10
|
+
* bytes it records). Lexical nests a list item one level per four spaces or
|
|
11
|
+
* one tab; CommonMark nests an item under the item whose content it is
|
|
12
|
+
* indented to, so `- a` / ` - b` (two spaces) and `1. a` / ` 1. b`
|
|
13
|
+
* (three) are nested lists that Lexical would flatten (spec:AST-061 FR5).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** A list item line: indentation, marker, and the spaces after it. */
|
|
17
|
+
const LIST_ITEM = /^([ \t]*)([-+*]|\d{1,9}[.)])( {1,4}|\t|$)/;
|
|
18
|
+
const FENCE_OPEN = /^[ \t]*(`{3,}|~{3,})/;
|
|
19
|
+
const BLANK_LINE = /^[ \t]*\r?$/;
|
|
20
|
+
/** A thematic break after its indentation: three or more of one marker. */
|
|
21
|
+
const THEMATIC_BREAK_CONTENT = /^([-*_])(?:[ \t]*\1){2,}[ \t]*\r?$/;
|
|
22
|
+
|
|
23
|
+
/** Columns of leading whitespace, with tabs stopping every four columns. */
|
|
24
|
+
function columnsOf(whitespace: string): number {
|
|
25
|
+
let columns = 0;
|
|
26
|
+
for (const character of whitespace) {
|
|
27
|
+
columns = character === '\t' ? columns + 4 - (columns % 4) : columns + 1;
|
|
28
|
+
}
|
|
29
|
+
return columns;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Rewrites each list item line's indentation as four spaces per CommonMark
|
|
34
|
+
* nesting level, leaving every other line, and every line inside fenced code,
|
|
35
|
+
* as it is. The line count and every line's content after its indentation
|
|
36
|
+
* are unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeListIndentation(markdown: string): string {
|
|
39
|
+
const lines = markdown.split('\n');
|
|
40
|
+
// The content column of each open list item, outermost first.
|
|
41
|
+
const open: Array<number> = [];
|
|
42
|
+
let fence: string | null = null;
|
|
43
|
+
let afterBlankLine = false;
|
|
44
|
+
for (let index = 0; index < lines.length; index++) {
|
|
45
|
+
const line = lines[index];
|
|
46
|
+
if (fence != null) {
|
|
47
|
+
if (
|
|
48
|
+
new RegExp(`^[ \\t]*${fence[0]}{${fence.length},}[ \\t]*\\r?$`).test(
|
|
49
|
+
line,
|
|
50
|
+
)
|
|
51
|
+
) {
|
|
52
|
+
fence = null;
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (BLANK_LINE.test(line)) {
|
|
57
|
+
afterBlankLine = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const leading = /^[ \t]*/.exec(line)?.[0] ?? '';
|
|
61
|
+
const indent = columnsOf(leading);
|
|
62
|
+
if (THEMATIC_BREAK_CONTENT.test(line.slice(leading.length))) {
|
|
63
|
+
// A thematic break, never an item, even where an item could start
|
|
64
|
+
// (CommonMark 0.31 §4.1). Lexical shows it only as a block of its own,
|
|
65
|
+
// so a break inside an item loses its indentation and reads as one.
|
|
66
|
+
while (open.length > 0 && indent < open[open.length - 1]) {
|
|
67
|
+
open.pop();
|
|
68
|
+
}
|
|
69
|
+
if (open.length > 0) {
|
|
70
|
+
lines[index] = line.slice(leading.length);
|
|
71
|
+
}
|
|
72
|
+
afterBlankLine = false;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const item = LIST_ITEM.exec(line);
|
|
76
|
+
if (item == null) {
|
|
77
|
+
const opening = FENCE_OPEN.exec(line);
|
|
78
|
+
if (opening != null) {
|
|
79
|
+
fence = opening[1];
|
|
80
|
+
}
|
|
81
|
+
// A line after a blank line belongs to the items it is indented into;
|
|
82
|
+
// without a blank line it continues the last item's paragraph lazily.
|
|
83
|
+
if (afterBlankLine) {
|
|
84
|
+
while (open.length > 0 && indent < open[open.length - 1]) {
|
|
85
|
+
open.pop();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
afterBlankLine = false;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
afterBlankLine = false;
|
|
92
|
+
// An item nests under every open item whose content it reaches.
|
|
93
|
+
while (open.length > 0 && indent < open[open.length - 1]) {
|
|
94
|
+
open.pop();
|
|
95
|
+
}
|
|
96
|
+
const depth = open.length;
|
|
97
|
+
// The content column, counted across the whole prefix so a tab after
|
|
98
|
+
// the marker stops at the right column; a marker that ends the line has
|
|
99
|
+
// its content one column after it.
|
|
100
|
+
const markerEnd = columnsOf(item[0]) + (item[3] === '' ? 1 : 0);
|
|
101
|
+
open.push(markerEnd);
|
|
102
|
+
lines[index] = ' '.repeat(depth * 4) + line.slice(item[1].length);
|
|
103
|
+
}
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file markdownOrderedList.ts
|
|
5
|
+
* @input Uses @lexical/list (ListNode, ListItemNode) and @lexical/markdown
|
|
6
|
+
* (ORDERED_LIST).
|
|
7
|
+
* @output Exports ORDERED_LIST_KEEPING_START, Lexical's ordered list
|
|
8
|
+
* transformer with one change: a nested numbered list starts at the number
|
|
9
|
+
* its first item was written with.
|
|
10
|
+
* @position Replaces Lexical's ORDERED_LIST in DEFAULT_TRANSFORMERS
|
|
11
|
+
* (markdownTable.ts). Lexical nests an indented item under a numbered list
|
|
12
|
+
* by indenting it into a new list that starts at 1, so `27.` written as the
|
|
13
|
+
* first item of a nested list read as `1.` (or `a.` under spec:AST-061
|
|
14
|
+
* DEC-6). Markdown keeps every list's start, nested or not (DEC-6), so this
|
|
15
|
+
* sets the new nested list's start from the marker. A later item in the same
|
|
16
|
+
* nested list continues its sequence, as Lexical's does.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {$isListItemNode, $isListNode, type ListNode} from '@lexical/list';
|
|
20
|
+
import {ORDERED_LIST, type ElementTransformer} from '@lexical/markdown';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The list `item` was indented into: the last item under `list`, followed
|
|
24
|
+
* down through the wrapper items Lexical nests lists in.
|
|
25
|
+
*/
|
|
26
|
+
function nestedListOfLastItem(list: ListNode): ListNode {
|
|
27
|
+
let current = list;
|
|
28
|
+
for (;;) {
|
|
29
|
+
const last = current.getLastChild();
|
|
30
|
+
const nested = $isListItemNode(last) ? last.getFirstChild() : null;
|
|
31
|
+
if (
|
|
32
|
+
!$isListItemNode(last) ||
|
|
33
|
+
last.getChildrenSize() !== 1 ||
|
|
34
|
+
!$isListNode(nested)
|
|
35
|
+
) {
|
|
36
|
+
return current;
|
|
37
|
+
}
|
|
38
|
+
current = nested;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const ORDERED_LIST_KEEPING_START: ElementTransformer = {
|
|
43
|
+
...ORDERED_LIST,
|
|
44
|
+
replace: (parentNode, children, match, isImport) => {
|
|
45
|
+
const previous = parentNode.getPreviousSibling();
|
|
46
|
+
const result = ORDERED_LIST.replace(parentNode, children, match, isImport);
|
|
47
|
+
if ($isListNode(previous) && previous.getListType() === 'number') {
|
|
48
|
+
const nested = nestedListOfLastItem(previous);
|
|
49
|
+
// The new item opened this nested list: start it at its own number.
|
|
50
|
+
if (
|
|
51
|
+
nested !== previous &&
|
|
52
|
+
nested.getListType() === 'number' &&
|
|
53
|
+
nested.getChildrenSize() === 1
|
|
54
|
+
) {
|
|
55
|
+
nested.setStart(Number(match[2]));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file markdownQuote.ts
|
|
5
|
+
* @input Uses @lexical/markdown (Lexical's QUOTE element transformer).
|
|
6
|
+
* @output Exports QUOTE_MARKERS, Lexical's quote transformer reading a block
|
|
7
|
+
* quote marker as CommonMark does.
|
|
8
|
+
* @position Part of DEFAULT_TRANSFORMERS (markdownTable.ts) in place of
|
|
9
|
+
* Lexical's QUOTE, whose pattern needs a space after `>` and no indentation.
|
|
10
|
+
* Matches core Markdown's block quote marker (CommonMark 0.31 §5.1). Typing
|
|
11
|
+
* shortcuts keep Lexical's own trigger: `> ` makes a quote. Text that starts
|
|
12
|
+
* like a marker is escaped on export by markdownSource.ts.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {QUOTE, type ElementTransformer} from '@lexical/markdown';
|
|
16
|
+
|
|
17
|
+
/** Lexical's own quote marker, which the typing shortcut keeps using. */
|
|
18
|
+
const SHORTCUT_MARKER = /^>\s/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A block quote marker: up to three spaces of indentation, then `>` and the
|
|
22
|
+
* one space or tab that may follow it, so `>a` and ` > a` import as quotes,
|
|
23
|
+
* as in core Markdown. A typed line becomes a quote only at Lexical's own
|
|
24
|
+
* `> `, so typing `>` and Enter, or ` > `, changes nothing new. RichText
|
|
25
|
+
* quotes do not nest: the marker of a quote inside a quote stays in the
|
|
26
|
+
* outer quote's text.
|
|
27
|
+
*/
|
|
28
|
+
export const QUOTE_MARKERS: ElementTransformer = {
|
|
29
|
+
...QUOTE,
|
|
30
|
+
regExp: /^ {0,3}>[ \t]?/,
|
|
31
|
+
replace: (parentNode, children, match, isImport) =>
|
|
32
|
+
isImport || SHORTCUT_MARKER.test(match[0])
|
|
33
|
+
? QUOTE.replace(parentNode, children, match, isImport)
|
|
34
|
+
: false,
|
|
35
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file markdownSerializers.ts
|
|
5
|
+
* @input Uses @lexical/headless (createHeadlessEditor), @lexical/markdown
|
|
6
|
+
* (transformer types), the shared DEFAULT_NODES set, DEFAULT_TRANSFORMERS,
|
|
7
|
+
* and the source-preserving import and export in markdownSource.ts.
|
|
8
|
+
* @output Standalone Markdown <-> serialized EditorState helpers:
|
|
9
|
+
* markdownToEditorStateJSON, editorStateJSONToMarkdown.
|
|
10
|
+
* @position Exported from the server-safe `@blue-patterns/richtext/markdown`
|
|
11
|
+
* entry and re-exported from the @blue-patterns/richtext barrel. Complements the ref's getMarkdown() by working WITHOUT a mounted
|
|
12
|
+
* editor (e.g. to produce a `defaultValue` from Markdown on the server).
|
|
13
|
+
*
|
|
14
|
+
* SYNC: When modified, update:
|
|
15
|
+
* - /packages/richtext/src/index.ts (exports)
|
|
16
|
+
* - /packages/richtext/src/RichTextEditor.doc.mjs (usage notes)
|
|
17
|
+
* - /packages/richtext/src/RichTextEditor.test.tsx (tests)
|
|
18
|
+
*
|
|
19
|
+
* NOTE: `@lexical/headless`, `@lexical/markdown`, and `lexical` are OPTIONAL
|
|
20
|
+
* peer dependencies. Install them to use these helpers. `@lexical/headless`
|
|
21
|
+
* runs Lexical without a DOM, so these work in Node / SSR contexts.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {createHeadlessEditor} from '@lexical/headless';
|
|
25
|
+
import {type Transformer} from '@lexical/markdown';
|
|
26
|
+
import {DEFAULT_NODES} from './editorNodes';
|
|
27
|
+
import {
|
|
28
|
+
$exportMarkdownKeepingSource,
|
|
29
|
+
importMarkdownKeepingSource,
|
|
30
|
+
} from './markdownSource';
|
|
31
|
+
import {DEFAULT_TRANSFORMERS} from './markdownTable';
|
|
32
|
+
import {pluginsOf, type RichTextMarkdownExtension} from './markdownExtensions';
|
|
33
|
+
import type {Klass, LexicalNode} from 'lexical';
|
|
34
|
+
|
|
35
|
+
/** Options shared by the Markdown serializer helpers. */
|
|
36
|
+
export interface MarkdownSerializerOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Markdown transformers to use. Defaults to the editor's default set: the
|
|
39
|
+
* standard `@lexical/markdown` `TRANSFORMERS` plus GFM tables. Pass the same
|
|
40
|
+
* array you give `RichTextEditor`'s `transformers` prop so content
|
|
41
|
+
* round-trips consistently.
|
|
42
|
+
*/
|
|
43
|
+
transformers?: ReadonlyArray<Transformer>;
|
|
44
|
+
/**
|
|
45
|
+
* Extra Lexical nodes to register beyond the default OSS set, mirroring the
|
|
46
|
+
* editor's `nodes` prop. Required for custom node types to serialize.
|
|
47
|
+
*/
|
|
48
|
+
nodes?: ReadonlyArray<Klass<LexicalNode>>;
|
|
49
|
+
/**
|
|
50
|
+
* Markdown plugins to recognize, each adopted with `createRichTextExtension`
|
|
51
|
+
* (spec:AST-064). Their syntax becomes extension nodes holding its exact
|
|
52
|
+
* source; without them, plugin syntax stays text. Pass the same extensions
|
|
53
|
+
* the surfaces that show the document use.
|
|
54
|
+
*/
|
|
55
|
+
extensions?: ReadonlyArray<RichTextMarkdownExtension>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createSerializerEditor(nodes?: ReadonlyArray<Klass<LexicalNode>>) {
|
|
59
|
+
return createHeadlessEditor({
|
|
60
|
+
namespace: 'astryx-editor-serializer',
|
|
61
|
+
nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
|
|
62
|
+
onError(error: Error) {
|
|
63
|
+
throw error;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Convert a Markdown string to a serialized Lexical `EditorState` JSON string
|
|
70
|
+
* — the same shape `RichTextEditor`'s `defaultValue` expects. Runs headless
|
|
71
|
+
* (no DOM), so it is safe in Node / SSR.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```
|
|
75
|
+
* const value = markdownToEditorStateJSON('# Title\n\nHello');
|
|
76
|
+
* <RichTextEditor label="Notes" defaultValue={value} />
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function markdownToEditorStateJSON(
|
|
80
|
+
markdown: string,
|
|
81
|
+
options: MarkdownSerializerOptions = {},
|
|
82
|
+
): string {
|
|
83
|
+
const {transformers = DEFAULT_TRANSFORMERS, nodes, extensions = []} = options;
|
|
84
|
+
const plugins = pluginsOf(extensions, transformers);
|
|
85
|
+
const editor = createSerializerEditor(nodes);
|
|
86
|
+
importMarkdownKeepingSource(editor, markdown, [...transformers], plugins);
|
|
87
|
+
return JSON.stringify(editor.getEditorState().toJSON());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Convert a serialized Lexical `EditorState` JSON string (e.g. the value
|
|
92
|
+
* produced by `RichTextEditor`'s `onChange` or `defaultValue`) to a Markdown
|
|
93
|
+
* string. Runs headless (no DOM), so it is safe in Node / SSR.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```
|
|
97
|
+
* const md = editorStateJSONToMarkdown(savedJson);
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function editorStateJSONToMarkdown(
|
|
101
|
+
editorStateJSON: string,
|
|
102
|
+
options: MarkdownSerializerOptions = {},
|
|
103
|
+
): string {
|
|
104
|
+
const {transformers = DEFAULT_TRANSFORMERS, nodes} = options;
|
|
105
|
+
const editor = createSerializerEditor(nodes);
|
|
106
|
+
const state = editor.parseEditorState(editorStateJSON);
|
|
107
|
+
return state.read(() => $exportMarkdownKeepingSource([...transformers]));
|
|
108
|
+
}
|