From a7504fc988c2ff4ec2fa94b40b614622dd3b6e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicole=20Miko=C5=82ajczyk?= Date: Thu, 27 Mar 2025 16:37:16 +0100 Subject: [PATCH] pl-fe: use @lexical/markdown for wysiwyg markdown export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicole Mikołajczyk --- packages/pl-fe/package.json | 1 + .../compose/editor/plugins/state-plugin.tsx | 31 +++++---- .../compose/editor/transformers/index.ts | 63 +++++++++++++++++++ packages/pl-fe/yarn.lock | 2 +- 4 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 packages/pl-fe/src/features/compose/editor/transformers/index.ts diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 5521929ed..92e289b43 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -49,6 +49,7 @@ "@lexical/hashtag": "^0.29.0", "@lexical/link": "^0.29.0", "@lexical/list": "^0.29.0", + "@lexical/markdown": "^0.29.0", "@lexical/react": "^0.29.0", "@lexical/rich-text": "^0.29.0", "@lexical/selection": "^0.29.0", diff --git a/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx index e627569e0..3293c1a3e 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx @@ -1,6 +1,6 @@ import { AutoLinkNode, LinkNode } from '@lexical/link'; +import { $convertToMarkdownString } from '@lexical/markdown'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { $createRemarkExport } from '@mkljczk/lexical-remark'; import { $nodesOfType, $getRoot, type EditorState, $getNodeByKey } from 'lexical'; import debounce from 'lodash/debounce'; import { useCallback, useEffect } from 'react'; @@ -14,6 +14,8 @@ import { useSettings } from 'pl-fe/hooks/use-settings'; import { getStatusIdsFromLinksInContent } from 'pl-fe/utils/status'; import Purify from 'pl-fe/utils/url-purify'; +import { TRANSFORMERS } from '../transformers'; + import type { LanguageIdentificationModel } from 'fasttext.wasm.js/dist/models/language-identification/common.js'; let lidModel: LanguageIdentificationModel; @@ -136,21 +138,18 @@ const StatePlugin: React.FC = ({ composeId, isWysiwyg }) => { useEffect(() => { return editor.registerUpdateListener(({ editorState }) => { const plainText = editorState.read(() => $getRoot().getTextContent()); - let text = plainText; - if (isWysiwyg) { - text = editorState.read($createRemarkExport({ - handlers: { - hashtag: (node) => ({ type: 'text', value: node.getTextContent() }), - mention: (node) => ({ type: 'text', value: node.getTextContent() }), - }, - })); - } - const isEmpty = text === ''; - const data = isEmpty ? null : JSON.stringify(editorState.toJSON()); - dispatch(setEditorState(composeId, data, text)); - checkUrls(editorState); - getQuoteSuggestions(plainText); - detectLanguage(plainText); + editor.update(() => { + let text = plainText; + if (isWysiwyg) { + text = $convertToMarkdownString(TRANSFORMERS); + } + const isEmpty = text === ''; + const data = isEmpty ? null : JSON.stringify(editorState.toJSON()); + dispatch(setEditorState(composeId, data, text)); + checkUrls(editorState); + getQuoteSuggestions(plainText); + detectLanguage(plainText); + }); }); }, [editor]); diff --git a/packages/pl-fe/src/features/compose/editor/transformers/index.ts b/packages/pl-fe/src/features/compose/editor/transformers/index.ts new file mode 100644 index 000000000..c9b84a5cf --- /dev/null +++ b/packages/pl-fe/src/features/compose/editor/transformers/index.ts @@ -0,0 +1,63 @@ +/** + * This source code is derived from code from Meta Platforms, Inc. + * and affiliates, licensed under the MIT license located in the + * LICENSE file in the /src/features/compose/editor directory. + */ + +import { TRANSFORMERS as DEFAULT_TRANSFORMERS, type ElementTransformer, type TextMatchTransformer } from '@lexical/markdown'; +import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode'; +import { LexicalNode } from 'lexical'; + +import { $createImageNode, $isImageNode, ImageNode } from '../nodes/image-node'; + +const IMAGE_TRANSFORMER: TextMatchTransformer = { + dependencies: [ImageNode], + export: (node) => { + if ($isImageNode(node)) { + const src = node.getSrc(); + const alt = node.getAltText(); + return `![${alt.replace(/([[\]])/g, '\\$1')}](${src})`; + } + return null; + }, + importRegExp: /!(?:\[([^[]*)\])(?:\(([^(]+)\))/, + regExp: /!(?:\[([^[]*)\])(?:\(([^(]+)\))$/, + replace: (textNode, match) => { + const [, altText, src] = match; + const imageNode = $createImageNode({ + altText, + src, + }); + textNode.replace(imageNode); + }, + type: 'text-match', +}; + +const HORIZONTAL_RULE_TRANSFORMER: ElementTransformer = { + dependencies: [HorizontalRuleNode], + export: (node: LexicalNode) => { + return $isHorizontalRuleNode(node) ? '***' : null; + }, + regExp: /^(---|\*\*\*|___)\s?$/, + replace: (parentNode, _1, _2, isImport) => { + const line = $createHorizontalRuleNode(); + + // TODO: Get rid of isImport flag + if (isImport || parentNode.getNextSibling() !== null) { + parentNode.replace(line); + } else { + parentNode.insertBefore(line); + } + + line.selectNext(); + }, + type: 'element', +}; + +const TRANSFORMERS = [ + ...DEFAULT_TRANSFORMERS, + HORIZONTAL_RULE_TRANSFORMER, + IMAGE_TRANSFORMER, +]; + +export { TRANSFORMERS }; diff --git a/packages/pl-fe/yarn.lock b/packages/pl-fe/yarn.lock index 3592d90af..3be4b24c5 100644 --- a/packages/pl-fe/yarn.lock +++ b/packages/pl-fe/yarn.lock @@ -1596,7 +1596,7 @@ "@lexical/utils" "0.29.0" lexical "0.29.0" -"@lexical/markdown@0.29.0": +"@lexical/markdown@0.29.0", "@lexical/markdown@^0.29.0": version "0.29.0" resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.29.0.tgz#673ca4fb1cbedf2b17e905cf9a229ce0eb505b65" integrity sha512-4Od8WoDoviv9DxJZVgrIORTIAzyoGOpztbGbIBXguGmwvy7NnHQDh9fZYIYRrdI1Awp1VVGdJ3ku/7KTgSOoRw==