Use lexical-remark for Lexical<->Markdown conversions

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-07-18 11:58:32 +02:00
parent 6a172f525f
commit 3c5025c7f3
5 changed files with 1544 additions and 146 deletions

View File

@ -6,7 +6,6 @@ Copyright (c) Meta Platforms, Inc. and affiliates.
This source code is licensed under the MIT license found in the This source code is licensed under the MIT license found in the
LICENSE file in the /app/soapbox/features/compose/editor directory. LICENSE file in the /app/soapbox/features/compose/editor directory.
*/ */
import { $convertFromMarkdownString, $convertToMarkdownString } from '@lexical/markdown';
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin'; import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
import { AutoLinkPlugin, createLinkMatcherWithRegExp } from '@lexical/react/LexicalAutoLinkPlugin'; import { AutoLinkPlugin, createLinkMatcherWithRegExp } from '@lexical/react/LexicalAutoLinkPlugin';
import { LexicalComposer, InitialConfigType } from '@lexical/react/LexicalComposer'; import { LexicalComposer, InitialConfigType } from '@lexical/react/LexicalComposer';
@ -20,6 +19,7 @@ import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import clsx from 'clsx'; import clsx from 'clsx';
import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical'; import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical';
import { $createRemarkExport, $createRemarkImport } from 'lexical-remark';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -39,7 +39,6 @@ import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin'; import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin';
import MentionPlugin from './plugins/mention-plugin'; import MentionPlugin from './plugins/mention-plugin';
import StatePlugin from './plugins/state-plugin'; import StatePlugin from './plugins/state-plugin';
import { TO_WYSIWYG_TRANSFORMERS } from './transformers';
interface IComposeEditor { interface IComposeEditor {
className?: string className?: string
@ -107,7 +106,7 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
return function() { return function() {
if (compose.content_type === 'text/markdown') { if (compose.content_type === 'text/markdown') {
$convertFromMarkdownString(compose.text, TO_WYSIWYG_TRANSFORMERS); $createRemarkImport({})(compose.text);
} else { } else {
const paragraph = $createParagraphNode(); const paragraph = $createParagraphNode();
const textNode = $createTextNode(compose.text); const textNode = $createTextNode(compose.text);
@ -175,9 +174,7 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
/> />
{autoFocus && <AutoFocusPlugin />} {autoFocus && <AutoFocusPlugin />}
<OnChangePlugin onChange={(_, editor) => { <OnChangePlugin onChange={(_, editor) => {
editor.update(() => { if (editorStateRef) (editorStateRef as any).current = editor.getEditorState().read($createRemarkExport());
if (editorStateRef) (editorStateRef as any).current = $convertToMarkdownString(TO_WYSIWYG_TRANSFORMERS);
});
}} }}
/> />
<HistoryPlugin /> <HistoryPlugin />

View File

@ -1,114 +0,0 @@
// Adapted from: https://github.com/facebook/lexical/issues/2715#issuecomment-1209090485
import {
BOLD_ITALIC_UNDERSCORE,
BOLD_ITALIC_STAR,
BOLD_STAR,
BOLD_UNDERSCORE,
STRIKETHROUGH,
INLINE_CODE,
HEADING,
QUOTE,
ORDERED_LIST,
UNORDERED_LIST,
LINK,
TextMatchTransformer,
} from '@lexical/markdown';
const replaceEscapedChars = (text: string): string => {
// convert "\*" to "*", "\_" to "_", "\~" to "~", ...
return text
.replaceAll('\\*', '*')
.replaceAll('\\_', '_')
.replaceAll('\\-', '-')
.replaceAll('\\#', '#')
.replaceAll('\\>', '>')
.replaceAll('\\+', '+')
.replaceAll('\\~', '~');
};
const replaceUnescapedChars = (text: string, regexes: RegExp[]): string => {
// convert "*" to "", "_" to "", "~" to "" (all chars, which are not escaped - means with "\" in front)
for (const regex of regexes) {
text = text.replaceAll(regex, '');
}
return text;
};
const UNESCAPE_ITALIC_UNDERSCORE_IMPORT_REGEX =
/([\_])(?<!(?:\1|\w).)(?![_*\s])(.*?[^_*\s])(?=\1)([\_])(?!\w|\3)/;
const UNESCAPE_ITALIC_UNDERSCORE_REGEX =
/([\_])(?<!(?:\1|\w).)(?![_*\s])(.*?[^_*\s])(?=\1)([\_])(?!\w|\3)/;
export const UNESCAPE_ITALIC_UNDERSCORE: TextMatchTransformer = {
dependencies: [],
export: () => null,
importRegExp: UNESCAPE_ITALIC_UNDERSCORE_IMPORT_REGEX,
regExp: UNESCAPE_ITALIC_UNDERSCORE_REGEX,
replace: (textNode, _) => {
const notEscapedUnderscoreRegex = /(?<![\\]{1})[\_]{1}/g;
const textContent = replaceUnescapedChars(textNode.getTextContent(), [
notEscapedUnderscoreRegex,
]);
textNode.setTextContent(replaceEscapedChars(textContent));
textNode.setFormat('italic');
},
trigger: '_',
type: 'text-match',
};
const UNESCAPE_ITALIC_STAR_IMPORT_REGEX =
/([\*])(?<!(?:\1|\w).)(?![_*\s])(.*?[^_*\s])(?=\1)([\*])(?!\w|\3)/;
const UNESCAPE_ITALIC_STAR_REGEX =
/([\*])(?<!(?:\1|\w).)(?![_*\s])(.*?[^_*\s])(?=\1)([\*])(?!\w|\3)/;
export const UNESCAPE_ITALIC_STAR: TextMatchTransformer = {
dependencies: [],
export: () => null,
importRegExp: UNESCAPE_ITALIC_STAR_IMPORT_REGEX,
regExp: UNESCAPE_ITALIC_STAR_REGEX,
replace: (textNode, _) => {
const notEscapedStarRegex = /(?<![\\]{1})[\*]{1}/g;
const textContent = replaceUnescapedChars(textNode.getTextContent(), [
notEscapedStarRegex,
]);
textNode.setTextContent(replaceEscapedChars(textContent));
textNode.setFormat('italic');
},
trigger: '*',
type: 'text-match',
};
const UNESCAPE_BACKSLASH_IMPORT_REGEX = /(\\(?:\\\\)?).*?\1*[\~\*\_\{\}\[\]\(\)\#\+\-\.\!]/;
const UNESCAPE_BACKSLASH_REGEX = /(\\(?:\\\\)?).*?\1*[\~\*\_\{\}\[\]\(\)\#\+\-\.\!]$/;
export const UNESCAPE_BACKSLASH: TextMatchTransformer = {
dependencies: [],
export: () => null,
importRegExp: UNESCAPE_BACKSLASH_IMPORT_REGEX,
regExp: UNESCAPE_BACKSLASH_REGEX,
replace: (textNode, _) => {
if (textNode) {
textNode.setTextContent(replaceEscapedChars(textNode.getTextContent()));
}
},
trigger: '\\',
type: 'text-match',
};
export const TO_WYSIWYG_TRANSFORMERS = [
UNESCAPE_BACKSLASH,
BOLD_ITALIC_UNDERSCORE,
BOLD_ITALIC_STAR,
BOLD_STAR,
BOLD_UNDERSCORE,
STRIKETHROUGH,
UNESCAPE_ITALIC_UNDERSCORE,
UNESCAPE_ITALIC_STAR,
INLINE_CODE,
HEADING,
QUOTE,
ORDERED_LIST,
UNORDERED_LIST,
LINK,
];

View File

@ -58,7 +58,6 @@
"@lexical/hashtag": "^0.11.2", "@lexical/hashtag": "^0.11.2",
"@lexical/link": "^0.11.2", "@lexical/link": "^0.11.2",
"@lexical/list": "^0.11.2", "@lexical/list": "^0.11.2",
"@lexical/markdown": "^0.11.2",
"@lexical/react": "^0.11.2", "@lexical/react": "^0.11.2",
"@lexical/rich-text": "^0.11.2", "@lexical/rich-text": "^0.11.2",
"@lexical/selection": "^0.11.2", "@lexical/selection": "^0.11.2",
@ -138,6 +137,7 @@
"intl-pluralrules": "^1.3.1", "intl-pluralrules": "^1.3.1",
"leaflet": "^1.8.0", "leaflet": "^1.8.0",
"lexical": "^0.11.2", "lexical": "^0.11.2",
"lexical-remark": "^0.3.8",
"libphonenumber-js": "^1.10.8", "libphonenumber-js": "^1.10.8",
"line-awesome": "^1.3.0", "line-awesome": "^1.3.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",

View File

@ -160,6 +160,7 @@ const configuration: Configuration = {
// https://github.com/facebook/react/issues/20235#issuecomment-1061708958 // https://github.com/facebook/react/issues/20235#issuecomment-1061708958
'react/jsx-runtime': 'react/jsx-runtime.js', 'react/jsx-runtime': 'react/jsx-runtime.js',
'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js', 'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js',
'process/browser': require.resolve('process/browser'),
}, },
}, },

1564
yarn.lock

File diff suppressed because it is too large Load Diff